GTK Linux client: - hosts/library: clicking a card was dead — the handler was on FlowBoxChild::activate (never emitted on click); bridge child-activated → child.activate() on the FlowBox (ui_hosts, ui_library). - stream: the Ctrl+Alt+Shift+D/Q/S chords (and all key forwarding) were dropped because the key controller sat on the overlay, which loses focus to the header back button after nav.push+fullscreen — move it to the window and remove it on teardown. - video: a mid-session VAAPI decode error rebuilt a software decoder but never requested a keyframe, so under the infinite GOP the picture stayed gray/frozen forever. Request an IDR on any VAAPI error, keep the hardware decoder, and demote to software only after repeated failures. - stream: fix a per-session Capture↔overlay reference cycle that leaked the overlay subtree + the Arc<NativeClient> on every session end — hold the overlay weakly. - stream: accumulate the fractional wheel remainder so precision-scroll (Deck trackpad / hi-res wheels) sub-unit deltas aren't dropped. - gamepad library: keep the launcher smooth on the Deck — freeze the aurora and trim the visible card range (fewer 3D offscreen passes) on low-power. - gamepad: log full pad identity (vid:pid:name:type:virtual) on attach to diagnose an empty controller list on the Deck. - cli: --connect host:<badport> silently did nothing; default to 9777 + warn. - css: add the missing .pf-neutral pill rule; fix the clipped most-recent accent (inset outline instead of a corner-clipped box-shadow bar). Decky plugin: - surface the on-screen library browser: label the host-row Games button. - fix silent pin data-loss — the detached Games modal captured a frozen pins array, so pinning a second game clobbered the first; mirror pins in a ref and track the modal's pinned ids locally for a live label. - route pair-required hosts through the pairing modal from the fullscreen Stream button (parity with the QAM panel). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Punktfunk — Steam Deck plugin (Decky)
Stream to your Steam Deck without ever leaving Gaming Mode. This
Decky Loader plugin adds a Punktfunk panel to the Quick Access Menu
(the … button): discover hosts on your network, pair with a PIN, tweak stream settings, and launch
a fullscreen, gamescope-focused stream — all from the couch, gamepad-navigable.
The video itself is the native GTK4 Linux client (the io.unom.Punktfunk flatpak); the plugin
discovers, pairs, configures, and launches it the right way so gamescope fullscreens it — the same
Steam-shortcut trick MoonDeck uses. Because it's built from real Steam UI primitives (@decky/ui),
the panel looks and feels native to Gaming Mode.
What it does
- Discover — browses the LAN over mDNS for Punktfunk hosts, in both the QAM panel and a fullscreen page; each host row opens a details view (address, pairing policy, certificate fingerprint to cross-check against the host's log).
- Pair — for a host that requires it, a gamepad-navigable PIN keypad runs the SPAKE2 pairing ceremony headlessly, then remembers the host so future streams connect silently.
- Stream — launches fullscreen via a branded "Punktfunk" Steam shortcut so gamescope focuses it.
- Games — each host row has a games button that opens its library picker: pin titles as one-tap "Stream " rows in the QAM (jump straight into e.g. Playnite on the host), or "Open library on screen" to launch the client's controller-driven, console-style library browser (aurora backdrop + poster coverflow; A plays, B returns to Gaming Mode). Pins survive plugin reinstalls (stored next to the client's config) and follow a host across IP changes (matched by certificate fingerprint).
- Settings — resolution / refresh / bitrate / gamepad type / host compositor / mic, written to the client's config.
- About — plugin version, an explicit "Check for updates" button, the setup-guide link, and a force-stop for a wedged stream client.
To leave a stream: the in-client controller chord (L1 + R1 + Start + Select), or close the "game" from the Steam overlay — either returns you to Gaming Mode.
Install on the Deck
You need Decky Loader and the io.unom.Punktfunk flatpak
(packaging/flatpak) installed on the Deck — SteamOS /usr is
read-only, so the flatpak (which bundles libadwaita/SDL3) is the canonical client. Discovery uses
avahi-browse, which ships on SteamOS/Bazzite.
Recommended — install from URL (published by CI): in Decky → Settings → Developer Mode → Install Plugin from URL, paste:
https://git.unom.io/api/packages/unom/generic/punktfunk-decky/latest/punktfunk.zip
(or a pinned .../punktfunk-decky/<version>/punktfunk.zip). The plugin then self-updates without
the Decky store — when a newer build exists, an Update button appears and drives Decky
Loader's own (SHA-256-verified) install. Installs and updates can take a couple of minutes on some
networks: Decky's installer also contacts its plugin store first, which may be slow or blackholed
before the actual download proceeds.
Build & sideload (development)
cd clients/decky
pnpm install
pnpm build # rollup → dist/index.js
pnpm run package # → out/punktfunk/ + out/punktfunk-v<ver>.zip
DECK=deck@<deck-ip> pnpm run deploy # rsync → /tmp, sudo-install into the root-owned plugins dir, restart loader
~/homebrew/plugins/ is root-owned (the loader runs as root), so deploy.sh stages to a temp dir
then sudo-installs and restarts the loader — set DECKPASS=… to run it non-interactively. A loader
restart is required for an out-of-band install to appear.
Architecture
| File | Role |
|---|---|
src/index.tsx |
Plugin entry: the QAM panel + route registration. |
src/page.tsx |
The /punktfunk fullscreen page — Hosts (with per-host details) / Settings / About tabs. |
src/settings.tsx · src/pair.tsx |
Stream-settings section; the gamepad-navigable PIN-pairing modal. |
src/library.tsx |
The per-host game picker (pin/unpin, "Open library on screen") + the pinned-game launch helper. |
src/hooks.ts · src/boundary.tsx |
Shared discovery/update/pins hooks + actions; the render error boundary. |
src/steam.ts |
Steam-shortcut launch (AddShortcut / SetAppLaunchOptions / RunGame) — the focus-correct stream start. The shortcut's exe is /bin/sh with the wrapper passed as an argument, so the script never needs an exec bit (Decky's zip extraction drops it and the root-owned plugins dir can't be chmodded by the unprivileged backend). Launch extras ride env-prefix tokens: PF_LAUNCH=<id> (pinned game) / PF_BROWSE=1 + PF_MGMT=<port> (on-screen library); ids are validated space/quote-free at pin AND launch time. |
src/backend.ts |
Typed callable bridges to main.py. |
bin/punktfunkrun.sh |
The launch wrapper the Steam shortcut runs (so the window is focusable); maps PF_LAUNCH/PF_BROWSE/PF_MGMT to --launch/--browse/--mgmt. An older flatpak ignores the flags harmlessly (plain stream / hosts page). |
main.py |
Backend: discover (via avahi-browse) / pair / library (headless flatpak --library, TSV) / pins store (decky-pinned.json) / settings / kill_stream / check_update (with an explicit CA-bundle search — Decky's embedded Python has no usable default TLS roots on SteamOS). |
scripts/test-backend.py |
Stdlib-only checks for the backend's pure parsers (TSV, error classes, avahi TXT) + the pins round trip. |
plugin.json · update.json |
Decky manifest; CI-baked update channel. |
Limitations / next steps
- No manual "add host by IP" entry yet (discovery is mDNS-only).
- No in-stream overlay inside the plugin — the client owns the session once launched.
- Pairing needs the operator to arm pairing on the host so it shows the PIN; the plugin can't arm it remotely.
Related
- Documentation — Steam Deck setup guide
- Linux client — the app this plugin launches
- Project README — the host, the other clients, and how it all fits together