macoc/docs/superpowers/specs/2026-10-25-caps-f19-remap.md

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.