# Capswitch HID-Level Caps Lock Remapping ## Problem macOS filters and toggles physical Caps Lock in the HID / WindowServer path. A Quartz flagsChanged event does not reliably represent physical key-down and key-up, especially for very short presses interleaved with normal typing. Fixing CGEventTap callback timing alone cannot recover events already discarded. ## Chosen approach - Use Apple's hidutil UserKeyMapping to translate HID Caps Lock usage 0x700000039 into HID F19 usage 0x70000006E. - Handle ordinary virtual F19 (keycode 80) keyDown and keyUp through a session-level CGEventTap, suppressing both. - Retain Option modifier tracking through flagsChanged and the pure state machine. Switching occurs synchronously on F19 release, never on F19 down. - Keep TIS source enumeration outside the event tap callback and periodically refresh the list. The TISSelectInputSource call remains synchronous. - Use a small IOKit C shim from SwiftPM to set the system Caps Lock lock state for the Option combination. - Expose F19 down/up and input-switch counts in the status bar for real-Mac acceptance testing without logging typed characters. ## Lifecycle and safety - Create and enable the event tap before applying the global HID remap. - Read and validate all existing mappings first; refuse Caps or F19 conflicts. - Merge with any unrelated mappings and verify the result after writing. - On Pause or Quit, disable interception and remove only Capswitch's exact pair. - Persist an ownership marker for crash recovery; do not remove pre-existing Caps -> F19 mappings not owned by Capswitch. - Reconcile HID state on wake and periodically, since keyboard disconnects or reboot can reset UserKeyMapping. - Run only one Capswitch instance for the same bundle ID. - Refuse app bundle replacement while a Capswitch process is running. - Treat nonstandard/unparseable hidutil output as a hard error, not permission to erase foreign mappings. ## Tests XCTest covers rapid F19 presses mixed with normal keys, key repeat suppression, Caps+Option in either order, pending Option release, router reset/orphan release, hidutil OpenStep mapping output, and foreign mapping preservation in JSON. The real macOS acceptance suite must verify: - Clean Command Line Tools compilation with no Xcode project. - Short Caps taps during continuous typing are observed as F19 down/up. - On Caps release, the next typed letter uses the next configured layout. - No stock input-source animation or HUD is triggered. - Option+Caps toggles real Caps Lock on and off including the LED. - Pause and Quit restore only the managed mapping. - USB/Bluetooth devices and wake/reconnect recovery work. - All Accessibility/TCC and old-process conditions are surfaced to the user. Current Linux host cannot run AppKit or HID hardware tests.