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

5.7 KiB

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.