57 lines
2.8 KiB
Markdown
57 lines
2.8 KiB
Markdown
# 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.
|