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

2.8 KiB

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.