archy/docs/tv-input-iframe-apps.md
archipelago 265196bc2c
Some checks failed
Demo images / Build & push demo images (push) Failing after 36s
docs: TV input design — keyboard/gamepad inside iframe apps, globally
evdev->uinput gamepad-as-keyboard daemon on kiosk nodes (works in any
iframe, any origin) + shell-side focus handoff via useControllerNav;
CDP/per-app injection rejected. Implementation order included.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 15:31:03 -04:00

85 lines
4.3 KiB
Markdown

# TV input: keyboard/gamepad inside iframe apps — design
**Goal (2026-07-23, user requirement):** on a TV/kiosk node, keyboard and
gamepad control must work *inside iframe apps* (IndeeHub, Jellyfin, fedimint
UI, AIUI…), easily and globally — no per-app hacks.
## What already exists
- `neode-ui/src/composables/useControllerNav.ts` — a complete spatial-nav
system for the shell: reads gamepads (`navigator.getGamepads`), moves focus
between `data-controller-container` regions, plays nav sounds. It stops at
iframe boundaries: nothing is forwarded into frames.
- Keyboard focus DOES enter iframes natively (click/Tab into the frame), and a
focused iframe receives all keys — same- or cross-origin. The gap is
gamepad→app and deliberate focus handoff shell↔frame.
- Kiosk Chromium is our process (archipelago-kiosk-launcher), X11, and the ISO
already ships `xdotool`. Most app iframes are **cross-origin**
(`http://host:port`), so shell-side script injection is impossible for them;
only the nginx-proxied `/app/...` ones are same-origin.
## Recommended architecture — two layers, both global
### Layer 1 (OS, kiosk nodes): gamepad → virtual keyboard, kernel-level
A small host daemon (`archipelago-gamepad-keys`) on kiosk nodes:
- reads game controllers via evdev (`/dev/input/event*`, capability
BTN_GAMEPAD), hotplug-aware (udev monitor or 5s rescan — same pattern as the
audio router);
- emits a **uinput virtual keyboard**: D-pad/left-stick → arrow keys, A →
Enter, B → Escape, X → Space (play/pause), Y → `f` (fullscreen in most
players), shoulders → Tab / Shift+Tab, Start → Enter, Select → Escape;
- ships exactly like the audio router: `image-recipe/configs/` script + unit,
spliced into the ISO, `include_str!` self-heal in `bootstrap.rs`, gated on
the kiosk being installed. The `archipelago` user is in `input` group OR the
unit runs as root (uinput needs it anyway — run as root, it's ~100 lines of
evdev→uinput with no network).
Why this layer wins: the browser sees a real keyboard, so **every iframe —
any origin, any app — just works** the way it does for a physical keyboard
today. Video players, web games, AIUI: all of them already have keyboard
bindings. Zero app cooperation, zero web-platform security fights.
### Layer 2 (shell): deliberate focus handoff into/out of frames
Small extension to `useControllerNav`:
- When spatial nav selects an app-session container and the user presses
A/Enter: call `iframe.focus()` (works cross-origin) — keys (real or
virtual) now flow into the app.
- A dedicated **exit chord** the daemon maps from the gamepad (e.g. Home
button → F12 or a rarely-used key): the shell listens with a *capturing*
window listener; on seeing it, `iframe.blur()` + return focus to the shell
nav. Keyboard users get the same via a documented chord (e.g. long
Escape / Ctrl+Escape — plain Escape stays with the app, players use it).
- Same-origin frames (the `/app/...` proxied set) can additionally get the
full spatial-nav treatment by running the existing nav over
`iframe.contentDocument` — nice-to-have after the layers above land.
### Optional layer 3 (per-app polish): postMessage contract
For OUR app UIs only (AIUI, fedimint, launcher pages): a tiny
`archipelago:input` postMessage contract for semantic actions (back, home,
context-menu) where raw keys aren't expressive enough. Documented in the app
packaging docs; never required for an app to be usable.
## What NOT to do
- ❌ CDP (`--remote-debugging-port` + Input.dispatchKeyEvent): works but adds
a privileged debug port to the kiosk and a daemon↔browser coupling; the
uinput route gets the same result at kernel level with no attack surface.
- ❌ Per-app nav scripts injected into iframes: cross-origin makes this
impossible for most apps, and it's exactly the per-app hack the requirement
rules out.
## Implementation order
1. `archipelago-gamepad-keys` daemon (evdev→uinput, ~python3 stdlib or small
Rust bin) + unit + ISO splice + bootstrap self-heal. Test on Framework PT
with any USB/BT controller.
2. `useControllerNav`: A-button → `iframe.focus()` on the focused app session;
exit-chord capture listener to reclaim focus.
3. (Later) same-origin spatial nav inside `/app/...` frames; postMessage
contract for our own app UIs.