A Decky Loader plugin so a Steam Deck / SteamOS box can launch the punktfunk client from Gaming Mode using REAL Steam UI components (it runs inside Steam's CEF, so the panel is built from @decky/ui — the literal Big Picture primitives, not a replica). - Frontend (src/index.tsx, @decky/api + @decky/ui): a Quick Access Menu panel — Refresh → discover hosts, a native list (name, ip:port, pairing flag), tap to connect with a status toast, Disconnect. - Backend (main.py): discover() shells `avahi-browse -rpt _punktfunk._udp` and parses the host's advertised TXT keys (proto/fp/pair/id from discovery.rs), dedup by id preferring IPv4; connect() resolves + spawns `punktfunk-client --connect host:port` (gamescope composites its video like a game), tracking the child; disconnect() terminates it. - Mirrors the current official Decky template (the API moved to @decky/ui + @decky/api). Frontend builds clean (pnpm build → dist/index.js); main.py py_compiles. dist/ + node_modules gitignored — build on the Deck per README. Spike scope: launcher only, runtime untested (no Deck here). Next on this track: the in-stream Quick-Access overlay (volume/disconnect/stats over the running stream) and a fuller real-components UI. Client decode on the AMD Deck is the existing VAAPI path; the host-encode VAAPI gap is separate (NVIDIA host = NVENC). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4.3 KiB
punktfunk Decky plugin (SteamOS / Steam Deck)
A Decky Loader plugin that adds a punktfunk panel to the Steam
Deck's Quick Access Menu (the QAM, opened with the … button), so you can launch the
punktfunk streaming client from Gaming Mode without dropping to the desktop.
Because Decky plugins run inside Steam's CEF, the panel is built from real Steam UI
primitives (@decky/ui: PanelSection, PanelSectionRow, ButtonItem, Field,
Spinner) — so it looks and feels native to Gaming Mode.
Spike / launcher only. This is a minimal but functional first cut: discover hosts, connect, disconnect. It launches the existing native GTK4 client (
punktfunk-client) over the top of Gaming Mode. An in-stream overlay (latency / bitrate HUD, mid-session controls) and a fuller real-Steam-components UI are the next steps. Runtime behavior on a real Deck is untested — only the build is verified here.
What it does
- Refresh — browses the LAN over mDNS for punktfunk/1 hosts (the
_punktfunk._udpservice) via the backenddiscover(). - Lists discovered hosts — name,
ip:port, and a lock icon for whether pairing is required (pair=requiredin the host's TXT record). - Connect — selecting a host calls
connect(host, port), which launchespunktfunk-client --connect host:port; a toast and the status line reflect the result. - Disconnect —
disconnect()terminates the launched client.
Architecture
| File | Role |
|---|---|
src/index.tsx |
Frontend QAM panel (@decky/ui + @decky/api). |
main.py |
Backend Plugin class: discover / connect / disconnect / status exposed over the Decky bridge. |
plugin.json |
Decky plugin manifest. |
decky.pyi |
Type stub for the injected decky module (vendored from the template). |
Discovery (discover())
Shells out to avahi-browse -rpt _punktfunk._udp (SteamOS and Bazzite ship
avahi-daemon; this avoids bundling python-zeroconf):
-rresolve services,-pparseable output,-tterminate after the cache dump.- Resolved records start with
=and are semicolon-separated:=;iface;protocol;name;type;domain;hostname;address;port;txt. - The
txtcolumn is space-separated, quoted"key=value"tokens. We read the keys the host advertises (crates/punktfunk-host/src/discovery.rs):proto,fp,pair,id. - Records are deduped on the
idTXT key (a host re-advertises per interface and across IPv4/IPv6), preferring the IPv4 address for the user-facing host string.
Client launch (connect())
The client binary punktfunk-client is resolved in order: PATH → /usr/bin →
/usr/local/bin → ~/.local/bin → a flatpak run earth.buehler.punktfunk.Client
fallback. The resolved argv and a clear client-not-found error surface to the UI. The
child PID is tracked so disconnect() (and plugin _unload) can terminate it.
TODO: pin the canonical SteamOS install path once a Deck packaging story for
punktfunk-clientis settled (likely a flatpak, since SteamOS/usris read-only).
Prerequisites
- Decky Loader installed on the Deck (https://decky.xyz/).
punktfunk-client(the GTK4/libadwaita Linux client, cratepunktfunk-client-linux) installed and runnable on the Deck — via.deb/RPM/flatpak, or symlinked into~/.local/bin.- avahi (
avahi-daemon+avahi-browse) for discovery — present on SteamOS/Bazzite. - A punktfunk/1 host on the LAN (
punktfunk-host serve --nativeorm3-host).
Build
pnpm install
pnpm build # rollup → dist/index.js
(npm install && npm run build also works.)
Install on the Deck
Copy the built plugin directory to the Deck and restart Decky:
# the dir must contain: dist/, main.py, plugin.json, package.json
rsync -a --exclude node_modules clients/decky/ deck@<deck-ip>:~/homebrew/plugins/punktfunk/
# then, on the Deck, restart Decky Loader (Settings → Developer → "Restart" / reboot)
The punktfunk panel then appears in the Quick Access Menu.
Limitations / next steps
- Launcher only — no in-stream overlay yet; the client owns the full session once launched.
- mDNS discovery depends on
avahi-browse; no manual "add host by IP" entry yet. - Pairing (PIN ceremony) is handled by the launched client, not the panel.
- Not yet tested on real Deck hardware.