Files
punktfunk/clients/decky
enricobuehler 57ae00a9c8
ci / rust (push) Failing after 41s
apple / swift (push) Successful in 1m8s
ci / web (push) Successful in 55s
ci / docs-site (push) Successful in 1m6s
android / android (push) Successful in 3m20s
deb / build-publish (push) Successful in 2m55s
decky / build-publish (push) Successful in 27s
apple / screenshots (push) Successful in 5m46s
ci / bench (push) Successful in 5m5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 34s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 3m20s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 2m31s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 53s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m18s
docker / deploy-docs (push) Has been cancelled
flatpak / build-publish (push) Has been cancelled
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
fix(clients): GTK + Decky polish batch from live Deck/Windows testing
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>
2026-07-04 07:37:04 +00:00
..

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

  1. 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).
  2. 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.
  3. Stream — launches fullscreen via a branded "Punktfunk" Steam shortcut so gamescope focuses it.
  4. 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).
  5. Settings — resolution / refresh / bitrate / gamepad type / host compositor / mic, written to the client's config.
  6. 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 ModeInstall 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.