TV OS — core primitives design (for review)
TL;DR
We're building the homelab's TV OS primitives first, apps second — YouTube (via FreeTube) and games plug in afterwards. Five primitives were approved in brainstorm: multi-device identity, a semantic intent bus, a native+external app model, a single-page shell with KWin/ydotool for focus and input injection, and a FastAPI+WebSocket router. The first increment de-risks the Wayland layer, then builds phone pairing, the Home Screen, and external-app launching. Review this, say "looks good", and I write the implementation plan.
What you asked for
Start the TV OS: a Home Screen on the TV with a QR for phones to pair as a remote, an app menu, first app = YouTube. Then refined to: build out the OS and its primitives first, be deliberate about the design choices that every later app and game will repeat, and leave YouTube for the next increment.
Decisions already made with you
The five primitives
P1 — Device session + identity. Scan the QR → the phone gets a persistent device id (stored in localStorage, re-presented on reconnect). Any number connect at once. The *active app* decides roles: shared control on the Home Screen, one player per phone in a game.
P2 — Intent schema. Phones emit semantic intents, never keycodes. Native apps read them directly; external apps get them translated to keystrokes through a per-app keymap. Versioned, so new intent types (motion) add without breaking clients.
nav dir: up | down | left | right | ok | back | home (home is global)
media action: playpause | seekfwd | seekback | volup | voldown | mute
text value: string (search / input)
button name: string (buzz, a, b … for games)
motion t, ax, ay, az, alpha, beta, gamma (deferred — motion games)
P3 — App model. One interface, two kinds behind it:
App { id, name, icon, kind: native | external, launch, keymap? }
launch() start it, make it the active surface
stop() tear down, return to the shell
on_intent(intent) native -> delivered to the app's view
external -> keymap -> keystroke -> injected into its window
P4 — Shell + focus. The shell owns the TV surface as one single-page web app; native apps are views inside it with shared device state and smooth transitions. External apps launch as OS windows over it. A global Home always escapes any app. KWin (KDE on Wayland) does launch / kill / focus-return; ydotool injects keystrokes.
P5 — Router. A FastAPI + WebSocket service on the homelab (reusing the games/ room patterns) — the single state machine: device registry ↔ intent stream ↔ the active app's handler. Endpoints /ws/remote (phones) and /ws/tv (the shell); the TV page at /, the phone remote at /remote.
phones --WS(intents)--> ROUTER --> active app
(id) (state) |- native -> render in TV shell (SPA)
'- external -> keymap -> ydotool -> app window
TV <-- shell / app view (SPA) ----' (KWin owns focus)
Authority on the shell (default): last-touch-drives. Any phone can nudge the cursor and select, no hard lock. Games override with real per-player roles. (Flag if you'd rather one host phone holds the shell.)
Stack
Python 3.12, FastAPI + uvicorn, WebSockets, pydantic-settings — same as games/. Vanilla HTML/JS for the TV shell and the phone remote. ydotool for injection, KWin for focus. Deployed on cachyos-homelab as a systemd service; the shell runs in a kiosk browser autostarted in the KDE session. Plain HTTP over the LAN for now — a d-pad remote needs no secure context; HTTPS (Cloudflare DNS-01) comes only when motion games need DeviceMotion.
First increment — build order
Done when: a phone scans the TV's QR, moves the Home Screen cursor, opens the test app and sees its presses echoed, opens an external app and drives it with the same d-pad, and home returns to the Home Screen every time.
Testing
Deferred (on purpose)
What I need from you
Commit log
79025bc docs: TV OS core primitives design spec (xpoes123/tv, main)