127 lines
5.9 KiB
Markdown
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.
|