macoc/docs/superpowers/plans/2026-10-25-capswitch.md

127 lines
5.9 KiB
Markdown

# Capswitch Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (\`- [ ]\`) syntax for tracking.
**Goal:** Build a native Swift menu-bar app that suppresses physical Caps Lock, switches to the next enabled keyboard input source on Caps release, and preserves real Caps Lock on any Caps+Option gesture.
**Architecture:** A pure Swift gesture state machine owns Caps/Option classification. A macOS adapter uses CGEventTap for global flagsChanged events, Carbon TIS APIs for input source selection, and AppKit NSStatusItem for the menu bar. A Taskfile and shell-free Swift build command assemble an unsigned/ad-hoc signed \`.app\` without an Xcode project.
**Tech Stack:** Swift 5-compatible source; AppKit, CoreGraphics, ApplicationServices, Carbon; Taskfile v3; XCTest on macOS; no third-party dependencies.
**Spec:** \`docs/superpowers/specs/2026-10-25-capswitch-design.md\`
## Global Constraints
- Target macOS 14.0+; validate on the user's macOS 14.8.9.
- Work directly on \`main\`; repository is initially empty.
- Use Taskfiles for build, test, lint, bundle, and clean scripts.
- No Xcode project, Storyboard, CocoaPods, Homebrew, or runtime dependency.
- Plain Caps switches only on Caps release when Option was not seen during the gesture.
- Caps+Option preserves real Caps Lock in either key order.
- Use a stable bundle identifier and LSUIElement menu-bar app configuration.
- Accessibility is required for the global event tap; show its state in the menu.
- Linux verification must distinguish host-unavailable Swift tooling from actual failures.
## Review Focus
- Caps release must switch exactly once and never on Caps press.
- Option before Caps and Caps before Option must both toggle real Caps Lock.
- Option released after Caps must complete the pending Caps toggle without switching layout.
- Tap reset/sleep recovery must clear pending state safely.
- Input source enumeration must ignore disabled/non-keyboard sources and keep the configured order.
---
### Task 1: Repository skeleton and plan
**Files:**
- Create: \`docs/superpowers/specs/2026-10-25-capswitch-design.md\`
- Create: \`docs/superpowers/plans/2026-10-25-capswitch.md\`
- Create: \`Taskfile.yml\`
- Create: \`.gitignore\`
- Create: \`README.md\`
- [ ] Write the design/spec and repository workflow files.
- [ ] Run Taskfile syntax/help checks.
- [ ] Commit the skeleton.
### Task 2: Gesture state machine
**Files:**
- Create: \`Sources/Capswitch/CapsGestureMachine.swift\`
- Test: \`Tests/CapswitchTests/CapsGestureMachineTests.swift\`
**Interfaces:**
- \`enum GestureEvent\`: \`capsDown(optionDown: Bool)\`, \`capsUp(optionDown: Bool)\`, \`optionDown\`, \`optionUp\`, \`reset\`.
- \`enum GestureAction\`: \`none\`, \`switchToNextInputSource\`, \`toggleCapsLock\`.
- \`struct CapsGestureMachine\` with \`mutating func handle(_ event: GestureEvent) -> GestureAction\`.
- [ ] Write failing XCTest cases for plain Caps, both Option orders, delayed Option release, and reset.
- [ ] Run the targeted test on macOS and verify it fails before implementation; on Linux record Swift unavailable.
- [ ] Implement the minimal state machine.
- [ ] Run the targeted test and the full test task.
### Task 3: macOS event tap and input source adapter
**Files:**
- Create: \`Sources/Capswitch/MacOSInputController.swift\`
- Modify: \`Sources/Capswitch/CapsGestureMachine.swift\` only if adapter-facing visibility requires it.
**Interfaces:**
- \`final class MacOSInputController\` with \`start() throws\`, \`stop()\`, and \`onInputSourceChanged: (() -> Void)?\`.
- Uses keycode 57, flagsChanged transitions, CGEventTap recovery, TIS selection, and synthetic Caps events.
- Synthetic Caps events must be marked/guarded so they do not re-enter the gesture state machine.
- [ ] Add adapter tests or compile-only checks where platform APIs are available.
- [ ] Implement the event tap and Carbon input source enumeration.
- [ ] Add tap timeout recovery and reset handling.
- [ ] Build on macOS with Taskfile.
### Task 4: Status bar app and permissions
**Files:**
- Create: \`Sources/Capswitch/AppDelegate.swift\`
- Create: \`Sources/Capswitch/StatusBarController.swift\`
- Create: \`Resources/Info.plist\`
- [ ] Add LSUIElement app delegate and status item.
- [ ] Add current-layout display, permission status, pause, launch-at-login placeholder, and quit menu actions.
- [ ] Add Accessibility check and System Settings URL.
- [ ] Build the .app bundle.
### Task 5: CLI packaging and documentation
**Files:**
- Modify: \`Taskfile.yml\`
- Modify: \`README.md\`
- Create: \`scripts/build-app.sh\` only if Taskfile cannot express the bundle operation clearly.
- [ ] Add \`task build\`, \`task bundle\`, \`task test\`, \`task lint\`, \`task clean\`, and \`task verify\`.
- [ ] Document one-time Command Line Tools setup, Accessibility permission, and first launch.
- [ ] Run all Linux-safe checks and report the Mac-only build limitation.
### Task 6: Final verification and delivery
- [ ] Inspect diff and repository status.
- [ ] Run Taskfile verification and any available tests.
- [ ] Commit on \`main\`.
- [ ] Push \`main\` to \`origin\`.
- [ ] Report commit, remote branch, and Mac-only checks still required.
## 2026-10-25 follow-up: HID remediation
The event-tap flagsChanged design is superseded for physical Caps by:
- docs/superpowers/specs/2026-10-25-caps-f19-remap.md
The original Caps Lock event-tap approach was not reliable during fast typing.
The code now relies on a Caps-to-F19 HID mapping, direct F19 keyDown/keyUp,
a managed remap lifecycle, and a small IOKit bridge for true Caps Lock state.
The original spec is retained for historical debugging context.
## In-process HID release
The latest native design is in
docs/superpowers/specs/2026-10-25-in-process-hid-remap.md.
Capswitch now uses IOHIDEventSystemClient and IOHIDServiceClientSetProperty
inside CapswitchHID, replacing all subprocess-based remapping.