InputPilot is a macOS menu bar app built with Swift/SwiftUI that detects the active keyboard and automatically switches to the matching input source (keyboard layout).
This README reflects the current state of the main branch.
- What InputPilot Does
- Core Features
- Privacy and Security
- Requirements
- Build and Run
- How to Use
- Auto-Switch Logic (Important)
- Persisted Data
- Debugging
- Tests
- Project Structure
- Architecture
- Troubleshooting
- Known Limitations
Typical use case: you use multiple keyboards (for example an internal MacBook keyboard and an external keyboard), and you want the input source to follow the keyboard you are actively using.
InputPilot does this by:
- Detecting the keyboard that produced the latest key event via HID.
- Looking up the configured input source for that device.
- Switching input source using Carbon/TIS.
- MenuBarExtra UI with live status.
- Input Monitoring permission flow:
- check status
- request permission
- open System Settings directly
- HID keyboard monitoring:
- start/stop with error handling
- keyDown detection without logging typed characters
- modifier-only handling for anti-flapping behavior
- Input source service (Carbon TIS):
- list enabled/all sources
- read current source
- select source by ID
- Per-device mappings.
- Per-device fallback input source.
- Global fallback input source.
- Undo for the last auto-switch action.
- Debounce + cooldown via
SwitchController. - Conflict detection:
- detects mappings that target missing/disabled sources
- surfaces conflicts in menu and settings
- Debug window with ring-buffer logs:
- live view
- copy to clipboard
- export as
.txt
- Quit action directly in the menu.
- No external dependencies.
- No keylogging.
- No typed text is stored.
- No keycodes or app content are stored.
- HID events are only used for device/event classification and switching logic.
- Debug logs contain technical status/error information only.
- Exported logs are sanitized (sensitive tokens are redacted).
- macOS (current project deployment target:
15.7) - Xcode 15+
- Swift 5
- Open the project:
InputPilot.xcodeproj
- Select the
InputPilotscheme. - Run the app.
- Grant Input Monitoring permission when prompted.
CLI build:
xcodebuild -scheme InputPilot -destination 'platform=macOS' build- Launch the app (keyboard icon in the menu bar).
- If permission is missing in the menu:
- click
Request Permission - if needed, click
Open Input Monitoring Settings
- click
- Press at least one key on each keyboard you want to configure so the device is detected.
- Open
Settings…and configure a mapping for each device.
Auto-Switchon/offPause 15 min/Pause 60 min/ResumeLast switch+UndoOpen DebugQuit InputPilot
Auto-Switch: pause state, last action, latest errorInput Monitoring: permission and active device/source statusInput Sources: current source and IDFallbacks: global fallback and quick action to use current sourceConflicts: invalid mappings withFix...actionKeyboard Device Mappings: mapping, per-device fallback, forget device
- Device mapping
- Per-device fallback
- Global fallback
- Otherwise no action
Auto-switch is active only when isAutoSwitchActive == true:
autoSwitchEnabled == true- not paused (
pauseUntilisnilor in the past)
- Debounce:
400ms(default) - Cooldown after successful switch:
1500ms - Modifier-only keyDown events do not trigger aggressive switching; switching waits for a stable trigger.
- Primary match key:
vendorId + productId + transport + isBuiltIn (+ normalized productName) locationIdis used as a hint/tie-breaker- Goal: stable behavior across port changes and varying HID metadata
InputPilot stores the following in UserDefaults:
- auto-switch enabled flag
- pause-until timestamp
- global fallback input source ID
- device mappings (including per-device fallback)
- migration flag for mapping schema (legacy -> v2)
Not persisted:
lastAction(runtime only)- debug log ring buffer (runtime only)
Open Debug in the menu opens a dedicated window with:
- log list (newest first)
- level (
INFO,WARN,ERROR) - category and timestamp
Copy to ClipboardExport…
Recommended issue workflow:
- Open Debug window.
- Reproduce the issue.
- Export logs.
- Inspect relevant error lines.
Test framework: Swift Testing (import Testing)
Covered areas:
AppStateauto-switch behavior including pause/resume/undoSwitchControllerdebounce/cooldown behaviorMappingStoreroundtrip, conflicts, migrationDebugLogServicering buffer and privacy sanitization
Run tests:
xcodebuild -scheme InputPilot -destination 'platform=macOS' testInputPilot/
App/
AppState.swift
InputPilotApp.swift
Services/
PermissionService.swift
HIDKeyboardMonitor.swift
InputSourceService.swift
SwitchController.swift
DebugLogService.swift
ServiceProtocols.swift
Models/
ActiveKeyboardDevice.swift
KeyboardFingerprint.swift
KeyboardDeviceKey.swift
KeyboardEventKind.swift
InputSourceInfo.swift
InputStatusSnapshot.swift
MappingConflict.swift
SwitchAction.swift
Persistence/
MappingStore.swift
AppSettingsStore.swift
UI/
MenuBarMenuView.swift
SettingsView.swift
DebugLogView.swift
AppStateis the central orchestrator (UI state + switch decisions).- Services are abstracted behind protocols (
PermissionServicing,HIDKeyboardMonitoring,InputSourceServicing,MappingStoring,ClockProviding,DebugLogServicing). SwitchControllerencapsulates debounce/cooldown independent of UI.- Persistence is intentionally lightweight (
UserDefaultsvia stores).
- Verify Input Monitoring permission in macOS Privacy settings.
- Restart the app.
- Check logs for
kIOReturnNotPermittedorkIOReturnNotPrivileged.
- Confirm Input Monitoring is actually
granted. - Press a key on the target keyboard (mouse movement is not enough).
- Check
StatusandActive Keyboard Devicein the menu.
- Verify the device mapping in Settings.
- Ensure the target source is enabled and selectable.
- Check
Conflictsformissing/disabled. - Verify pause state and
Auto-Switchtoggle.
- Debounce/cooldown is active; inspect logs for edge cases.
- For unstable setups (for example KVM), configure explicit mapping/fallback.
- macOS only.
- Input Monitoring permission is required.
- Detection depends on keyboard events; no key event means no active-device update.
- No cloud sync/profile/hotkey management in the current
mainbranch.