# `keyboard` — `KeyboardController` _(no submodules)_ ## Purpose Browser-tier device adapter that normalises DOM `keydown`/`keyup` events into the canonical 37-field `ControllerInput` shape exposed by `BaseController`. The adapter exists so games, UIs, and headless test harnesses can consume the same controller surface whether the user is pressing physical keys, dispatching synthetic events from a test double, or driving the adapter from a non-DOM emitter such as a custom virtual keyboard. The class never touches a global at module load time. The target is either supplied via `options.target` or resolved lazily from `globalThis.window` when `attach()` runs. Tests inject an `EventTargetLike` double and drive the adapter directly — no jsdom or browser environment is required. ## Features - Configurable `keyMap` covering arrow keys, WASD, ZXCV face buttons, QERT triggers, and `Digit1`–`Digit4` function keys out of the box. - Validates every key-map entry against `CONTROLLER_INPUT_KEYS` at construction; invalid entries throw `ControllerError(INVALID_KEY_MAP)` with the offending tuple in `context.entry`. - `attach()` / `detach()` are idempotent and survive `destroy()` cleanly (no listeners leak, `attach()` after destroy is a no-op). - Repeats and double-presses are suppressed (`event.repeat === true` and already-pressed keys are ignored). - Optional `preventDefault: true` to suppress default browser behaviour (e.g. spacebar scrolling) without leaking that decision into the input pipeline. - Inherits the full `BaseController` surface — `onInput`, `onAnyInput`, `onCombo`, `setThreshold`, `getDirectionalVector`, `update()`, telemetry instrumentation. ## Usage: Basic ```ts import { KeyboardController } from "@suaveplan/controller/keyboard"; const kb = new KeyboardController(); kb.onInput("up", (_input, value) => { console.log(value === 255 ? "pressed up" : "released up"); }); // later kb.destroy(); ``` ## Usage: Advanced Drive the adapter from a synthetic emitter — no DOM required: ```ts import { KeyboardController } from "@suaveplan/controller/keyboard"; import type { EventTargetLike } from "@suaveplan/controller/keyboard"; const listeners = new Map void>>(); const target: EventTargetLike = { addEventListener(type, listener) { const bucket = listeners.get(type) ?? new Set(); bucket.add(listener as never); listeners.set(type, bucket); }, removeEventListener(type, listener) { listeners.get(type)?.delete(listener as never); }, }; const kb = new KeyboardController({ target, keyMap: { Space: "a", KeyJ: "b" }, preventDefault: true, pollRate: 120, }); const dispatch = (type: string, event: unknown) => { for (const cb of listeners.get(type) ?? []) cb(event as never); }; dispatch("keydown", { code: "Space" }); console.log(kb.getValue("a")); // 255 dispatch("keyup", { code: "Space" }); kb.destroy(); ``` ## API Reference - `class KeyboardController extends BaseController` - `constructor(options?: KeyboardControllerOptions)` - `attach(): void` — bind `keydown`/`keyup` to the resolved target. Throws `ControllerError(KEYBOARD_TARGET_MISSING)` when no target is available. - `detach(): void` — idempotent removal of listeners. - `getPressedKeys(): readonly string[]` — snapshot of physical keys currently held down (by `code` or `key` identifier). - `getKeyMap(): KeyboardKeyMap` — immutable key → input map. - `destroy(): void` — detaches listeners and clears state. - `DEFAULT_KEYBOARD_KEY_MAP: KeyboardKeyMap` — frozen default map used when `options.keyMap` is omitted. - `interface KeyboardControllerOptions extends BaseControllerOptions` — adds `target`, `keyMap`, `autoAttach`, `preventDefault`. - `interface EventTargetLike` — structural subset of `EventTarget`. - `interface KeyboardEventLike` — structural subset of `KeyboardEvent` (`code`, `key`, `repeat`, `preventDefault`). - `type KeyboardKeyMap = Readonly>`. ## Implementation Notes - The adapter writes through `BaseController.setState({ [input]: value })` rather than reaching into `pendingStateUpdates`. This keeps the OTel counters (`controller_inputs_total`, `controller_state_changes_total`) and the `InputEventEmitter` firing on the same code path as direct programmatic updates. - Key identification consults `event.code` first, then `event.key`. Both must be strings; the adapter rejects events whose identifier is `undefined`. This matches the upstream behaviour and aligns with W3C UI Events. - Repeat suppression is double-guarded: the adapter ignores `event.repeat === true` and also short-circuits when the key is already tracked as pressed. - The Zod options schema covers every public option; rejections are surfaced as `ControllerError(INVALID_OPTIONS)` with the Zod issues in `context.issues`. The schema is `.strict()` so unknown keys are rejected at the boundary — typos surface as construction errors rather than silent ignores. - `setState` accepts a partial `ControllerInput`, so the adapter computes the field per event rather than buffering. This keeps the per-event latency below the `pollRate` gate that `BaseController` applies to `update()`.