Files
punktfunk/clients/decky
enricobuehler b3f98a5d7d
ci / rust (push) Successful in 2m7s
ci / web (push) Successful in 26s
ci / docs-site (push) Successful in 29s
apple / swift (push) Successful in 1m15s
ci / bench (push) Successful in 1m35s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
deb / build-publish (push) Successful in 2m20s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 4m52s
docker / deploy-docs (push) Successful in 16s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m23s
feat(clients/decky): SteamOS Gaming-Mode launcher plugin (spike)
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>
2026-06-14 12:50:57 +00:00
..

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

  1. Refresh — browses the LAN over mDNS for punktfunk/1 hosts (the _punktfunk._udp service) via the backend discover().
  2. Lists discovered hosts — name, ip:port, and a lock icon for whether pairing is required (pair=required in the host's TXT record).
  3. Connect — selecting a host calls connect(host, port), which launches punktfunk-client --connect host:port; a toast and the status line reflect the result.
  4. Disconnectdisconnect() 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):

  • -r resolve services, -p parseable output, -t terminate after the cache dump.
  • Resolved records start with = and are semicolon-separated: =;iface;protocol;name;type;domain;hostname;address;port;txt.
  • The txt column 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 id TXT 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-client is settled (likely a flatpak, since SteamOS /usr is read-only).

Prerequisites

  • Decky Loader installed on the Deck (https://decky.xyz/).
  • punktfunk-client (the GTK4/libadwaita Linux client, crate punktfunk-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 --native or m3-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.