Add overlay gamepad support for keyboard-only mode (#17)

This commit is contained in:
2026-03-11 20:34:46 -07:00
committed by GitHub
parent 2f17859b7b
commit 4d7c80f2e4
49 changed files with 5677 additions and 42 deletions

View File

@@ -0,0 +1,110 @@
# Overlay Controller Support Design
**Date:** 2026-03-11
**Backlog:** `TASK-159`
## Goal
Add controller support to the visible overlay through the Chrome Gamepad API without replacing the existing keyboard-only workflow. Controller input should only supplement keyboard-only mode, preserve existing behavior, and expose controller selection plus raw-input debugging in overlay-local modals.
## Scope
- Poll connected gamepads from the visible overlay renderer.
- Default to the first connected controller unless config specifies a preferred controller.
- Add logical controller bindings and tuning knobs to config.
- Add `Alt+C` controller selection modal.
- Add `Alt+Shift+C` controller debug modal.
- Map controller actions onto existing keyboard-only/Yomitan behaviors.
- Fix stale selected-token highlight cleanup when keyboard-only mode turns off or popup closes.
Out of scope for this pass:
- Raw arbitrary axis/button index remapping in config.
- Controller support outside the visible overlay renderer.
- Haptics or vibration.
## Architecture
Use a renderer-local controller runtime. The overlay already owns keyboard-only token selection, Yomitan popup integration, and modal UX, and the Gamepad API is browser-native. A renderer module can poll `navigator.getGamepads()` on animation frames, normalize sticks/buttons into logical actions, and call the same helpers used by keyboard-only mode.
Avoid synthetic keyboard events as the primary implementation. Analog sticks need deadzones, continuous smooth scrolling, and per-action repeat behavior that do not fit cleanly into key event emulation. Direct logical actions keep tests clear and make the debug modal show the exact values the runtime uses.
## Behavior
Controller actions are active only while keyboard-only mode is enabled, except the controller action that toggles keyboard-only mode can always fire so the user can enter the mode from the controller.
Default logical mappings:
- left stick vertical: smooth Yomitan popup/window scroll when popup is open
- left stick horizontal: move token selection left/right
- right stick vertical: smooth Yomitan popup/window scroll
- right stick horizontal: jump horizontally inside Yomitan popup/window
- `A`: toggle lookup
- `B`: close lookup
- `Y`: toggle keyboard-only mode
- `X`: mine card
- `L1` / `R1`: previous / next Yomitan audio
- `R2`: activate current Yomitan audio button
- `L2`: toggle mpv play/pause
Selection-highlight cleanup:
- disabling keyboard-only mode clears the selected token class immediately
- closing the Yomitan popup also clears the selected token class if keyboard-only mode is no longer active
- helper ownership should live in the shared keyboard-only selection sync path so keyboard and controller exits stay consistent
## Config
Add a top-level `controller` block in resolved config with:
- `enabled`
- `preferredGamepadId`
- `preferredGamepadLabel`
- `smoothScroll`
- `scrollPixelsPerSecond`
- `horizontalJumpPixels`
- `stickDeadzone`
- `triggerDeadzone`
- `repeatDelayMs`
- `repeatIntervalMs`
- `bindings` logical fields for the named actions/sticks
Persist the preferred controller by stable browser-exposed `id` when possible, with label stored as a diagnostic/display fallback.
## UI
Controller selection modal:
- overlay-hosted modal in the visible renderer
- lists currently connected controllers
- highlights current active choice
- selecting one persists config and makes it the active controller immediately if connected
Controller debug modal:
- overlay-hosted modal
- shows selected controller and all connected controllers
- live raw axis array values
- live raw button values, pressed flags, and touched flags if available
## Testing
Test first:
- controller gating outside keyboard-only mode
- logical mapping to existing helpers
- continuous stick scroll and repeat behavior
- modal open shortcuts
- preferred-controller selection persistence
- highlight cleanup on keyboard-only disable and popup close
- config defaults/parse/template generation coverage
## Risks
- Browser gamepad identity strings can differ across OS/browser/runtime versions.
Mitigation: match by exact preferred id first; fall back to first connected controller.
- Continuous stick input can spam actions.
Mitigation: deadzones plus repeat throttling and frame-time-based smooth scroll.
- Popup DOM/audio controls may vary.
Mitigation: target stable Yomitan popup/document selectors and cover with focused renderer tests.

View File

@@ -0,0 +1,245 @@
# Overlay Controller Support Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Add Chrome Gamepad API controller support to the visible overlay as a supplement to keyboard-only mode, including controller selection/debug modals, config-backed logical bindings, and selected-token highlight cleanup.
**Architecture:** Keep controller support in the visible overlay renderer. Poll and normalize gamepad state in a dedicated runtime, route logical actions into the existing keyboard-only/Yomitan helpers, and persist preferred-controller config through the existing config pipeline and preload bridge.
**Tech Stack:** TypeScript, Bun tests, Electron preload IPC, renderer DOM modals, Chrome Gamepad API
---
### Task 1: Track work and lock the design
**Files:**
- Create: `backlog/tasks/task-159 - Add-overlay-controller-support-for-keyboard-only-mode.md`
- Create: `docs/plans/2026-03-11-overlay-controller-support-design.md`
- Create: `docs/plans/2026-03-11-overlay-controller-support.md`
**Step 1: Record the approved scope**
Capture controller-only-in-keyboard-mode behavior, the modal shortcuts, config scope, and the stale selection-highlight cleanup requirement.
**Step 2: Verify the written scope matches the approved design**
Run: `sed -n '1,220p' backlog/tasks/task-159\\ -\\ Add-overlay-controller-support-for-keyboard-only-mode.md && sed -n '1,240p' docs/plans/2026-03-11-overlay-controller-support-design.md`
Expected: task and design doc both mention controller selection/debug modals and highlight cleanup.
### Task 2: Add failing config tests and defaults
**Files:**
- Modify: `src/config/config.test.ts`
- Modify: `src/config/definitions/defaults-core.ts`
- Modify: `src/config/definitions/options-core.ts`
- Modify: `src/config/definitions/template-sections.ts`
- Modify: `src/types.ts`
- Modify: `config.example.jsonc`
**Step 1: Write the failing test**
Add coverage asserting a new `controller` config block resolves with the expected defaults and accepts logical-field overrides.
**Step 2: Run test to verify it fails**
Run: `bun test src/config/config.test.ts`
Expected: FAIL because `controller` config is not defined yet.
**Step 3: Write minimal implementation**
Add the controller config types/defaults/registry/template wiring and regenerate the example config if needed.
**Step 4: Run test to verify it passes**
Run: `bun test src/config/config.test.ts`
Expected: PASS
### Task 3: Add failing keyboard-selection cleanup tests
**Files:**
- Modify: `src/renderer/handlers/keyboard.test.ts`
- Modify: `src/renderer/handlers/keyboard.ts`
- Modify: `src/renderer/state.ts`
**Step 1: Write the failing tests**
Add tests for:
- turning keyboard-only mode off clears `.keyboard-selected`
- closing the popup clears stale selection highlight when keyboard-only mode is off
**Step 2: Run test to verify it fails**
Run: `bun test src/renderer/handlers/keyboard.test.ts`
Expected: FAIL because selection cleanup is incomplete today.
**Step 3: Write minimal implementation**
Centralize selection clearing in the keyboard-only sync helpers and popup-close flow.
**Step 4: Run test to verify it passes**
Run: `bun test src/renderer/handlers/keyboard.test.ts`
Expected: PASS
### Task 4: Add failing controller runtime tests
**Files:**
- Create: `src/renderer/handlers/gamepad-controller.test.ts`
- Create: `src/renderer/handlers/gamepad-controller.ts`
- Modify: `src/renderer/context.ts`
- Modify: `src/renderer/state.ts`
- Modify: `src/renderer/renderer.ts`
**Step 1: Write the failing tests**
Cover:
- first connected controller is selected by default
- preferred controller wins when connected
- controller actions are ignored unless keyboard-only mode is enabled, except keyboard-only toggle
- stick/button mappings invoke the expected logical helpers
- smooth scroll and repeat throttling behavior
**Step 2: Run test to verify it fails**
Run: `bun test src/renderer/handlers/gamepad-controller.test.ts`
Expected: FAIL because controller runtime does not exist.
**Step 3: Write minimal implementation**
Add a renderer-local polling runtime with deadzone handling, action edge detection, repeat timing, and helper callbacks into the keyboard/Yomitan flow.
**Step 4: Run test to verify it passes**
Run: `bun test src/renderer/handlers/gamepad-controller.test.ts`
Expected: PASS
### Task 5: Add failing controller modal tests
**Files:**
- Modify: `src/renderer/index.html`
- Modify: `src/renderer/style.css`
- Create: `src/renderer/modals/controller-select.ts`
- Create: `src/renderer/modals/controller-select.test.ts`
- Create: `src/renderer/modals/controller-debug.ts`
- Create: `src/renderer/modals/controller-debug.test.ts`
- Modify: `src/renderer/renderer.ts`
- Modify: `src/renderer/context.ts`
- Modify: `src/renderer/state.ts`
**Step 1: Write the failing tests**
Add tests for:
- `Alt+C` opens controller selection modal
- `Alt+Shift+C` opens controller debug modal
- selection modal renders connected controllers and persists the chosen device
- debug modal shows live axes/buttons state
**Step 2: Run test to verify it fails**
Run: `bun test src/renderer/modals/controller-select.test.ts src/renderer/modals/controller-debug.test.ts`
Expected: FAIL because modals and shortcuts do not exist.
**Step 3: Write minimal implementation**
Add modal DOM, renderer modules, modal state wiring, and controller runtime integration.
**Step 4: Run test to verify it passes**
Run: `bun test src/renderer/modals/controller-select.test.ts src/renderer/modals/controller-debug.test.ts`
Expected: PASS
### Task 6: Persist controller preference through preload/main wiring
**Files:**
- Modify: `src/preload.ts`
- Modify: `src/types.ts`
- Modify: `src/shared/ipc/contracts.ts`
- Modify: `src/core/services/ipc.ts`
- Modify: `src/main.ts`
- Modify: related main/runtime tests as needed
**Step 1: Write the failing test**
Add coverage for reading current controller config and saving preferred-controller changes from the renderer.
**Step 2: Run test to verify it fails**
Run: `bun test src/core/services/ipc.test.ts`
Expected: FAIL because no controller preference IPC exists yet.
**Step 3: Write minimal implementation**
Expose renderer-safe getters/setters for the controller config fields needed by the selection modal/runtime.
**Step 4: Run test to verify it passes**
Run: `bun test src/core/services/ipc.test.ts`
Expected: PASS
### Task 7: Update docs and config example
**Files:**
- Modify: `config.example.jsonc`
- Modify: `README.md`
- Modify: relevant docs under `docs-site/` for shortcuts/usage/troubleshooting if touched by current docs structure
**Step 1: Write the failing doc/config check if needed**
If config example generation is covered by tests, add/refresh the failing assertion first.
**Step 2: Implement the docs**
Document controller behavior, modal shortcuts, config block, and the keyboard-only-only activation rule.
**Step 3: Run doc/config verification**
Run: `bun run test:config`
Expected: PASS
### Task 8: Run the handoff gate and update the backlog task
**Files:**
- Modify: `backlog/tasks/task-159 - Add-overlay-controller-support-for-keyboard-only-mode.md`
**Step 1: Run targeted verification**
Run:
- `bun test src/config/config.test.ts`
- `bun test src/renderer/handlers/keyboard.test.ts`
- `bun test src/renderer/handlers/gamepad-controller.test.ts`
- `bun test src/renderer/modals/controller-select.test.ts`
- `bun test src/renderer/modals/controller-debug.test.ts`
- `bun test src/core/services/ipc.test.ts`
Expected: PASS
**Step 2: Run broader gate**
Run:
- `bun run typecheck`
- `bun run test:fast`
- `bun run test:env`
- `bun run build`
Expected: PASS, or document exact blockers/failures.
**Step 3: Update backlog notes**
Fill in implementation notes, verification commands, and final summary in `TASK-159`.