Decky client batch:
- Pinned games / library picker: per-host game grid (GamePickerModal),
pin/unpin, one-tap streams surfaced on the Hosts tab and QAM
(usePins/streamPin/resolvePinHost, new src/library.tsx).
- Self-update + client-update plumbing (main.py check_update, hooks.ts
applyUpdate) with a CA-bundle-resolving SSL context and per-channel
manifest polling; steam.ts / punktfunkrun.sh launch tweaks.
- scripts/test-backend.py harness for the backend RPCs; README refresh.
Fix: the fullscreen page wrapped <Tabs> in an overflow-visible box, so
Valve's L1/R1 tab slide + autoFocusContents scrollIntoView panned
#GamepadUI itself — the whole Steam UI slid left until a tab was clicked.
Clip the Tabs wrapper (overflow:hidden), matching Valve's own Tabs
containers. (On-glass verification pending — Deck offline this session.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- The "Punktfunk" shortcut is no longer hidden: it now ships committed
artwork (grid/wide/hero/logo/icon, generated by scripts/gen-steam-art.py
— a pure-stdlib SDF renderer drawing the lens mark + a monoline
"punktfunk" wordmark) applied via SetCustomArtworkForApp /
SetShortcutIcon. Existing installs are unhidden and re-arted once per
ART_VERSION; relaunching the library entry streams to the last host.
- Updates cover the flatpak CLIENT too: check_update compares the
user-scope installed commit against its remote, applyUpdate runs
`flatpak update --user` first (awaited) and the plugin reinstall —
which reloads the panel — last; docs spell out the sudo-less --user
update ("sudo flatpak update" silently skips per-user installs).
- Fullscreen page: DialogButton stretches to 100% width in the gamepad
UI, so the Stream/Pair/Refresh/… actions filled whole rows — sized to
content + right-aligned now; the header drops its Update button (About
tab + QAM banner keep the flow) and the back button gets a real 40px
hit target.
- Settings: the disable-Steam-Input note also shows for Automatic — on a
Deck that now forwards the built-in controller as a Steam Deck pad
(paddles/trackpads/gyro), which needs Steam Input off for the shortcut.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes from live debugging on the Deck:
- check_update() was dead on-device: Decky Loader's embedded (PyInstaller)
Python has no usable default CA paths, so every HTTPS fetch failed with
CERTIFICATE_VERIFY_FAILED. Build the SSL context explicitly: default paths
first, then the known system bundles (SteamOS/Arch, Debian, Fedora/Bazzite,
openSUSE), then certifi if importable. Verification stays on; the check
stays offline-tolerant with its 30-min cache.
- "could not chmod runner" on every use: Decky extracts plugin zips without
exec bits into a root-owned dir the unprivileged backend can't chmod. The
Steam shortcut now launches the runner through /bin/sh with the script as a
%command% argument — no exec bit needed, existing shortcuts migrate on
reuse, the chmod attempt is gone.
UI/structure:
- index.tsx (660 lines) split into page/pair/settings/hooks/boundary modules;
PluginErrorBoundary kept guarding every surface.
- New About section/tab: visible version + channel, explicit check-for-updates
(forces past the cache, always toasts an outcome), setup-guide link, leave-
chord help, and a Force-stop backstop for a wedged stream.
- Host rows open a details modal (address, protocol, pairing policy, paired
state, fingerprint). Settings gain 1280×800 (Deck native), Xbox One and
DualShock 4 pad types, and a host-compositor picker.
- Update flows note the Decky store contact can stall a couple of minutes on
networks that blackhole plugins.deckbrew.xyz (observed live).
- "Punktfunk" in all user-facing strings; plugin id/paths/env unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Finish the client side of the Steam Controller / Steam Deck pipeline.
- C-ABI (core abi.rs): PunktfunkRichInputEx — a size-prefixed superset of
PunktfunkRichInput that can express the second trackpad (surface), a distinct
click vs touch, signed coords + pressure — plus
punktfunk_connection_send_rich_input2 (the struct_size ABI-skew-guard
precedent). The only way a C client (Apple/embedders) can emit a TouchpadEx;
the legacy struct + send_rich_input stay byte-for-byte. punktfunk_core.h
regenerated.
- Decky (clients/decky): a "Steam Deck" gamepad type in Settings + an unmissable
Disable-Steam-Input instruction shown when it's selected (in Game Mode Steam
Input holds 0x1205, so the SDL HIDAPI Steam driver can't open the Deck's
controls until the user disables Steam Input for the shortcut). Plus a
best-effort, feature-detected disableSteamInputForShortcut() in launchStream —
never blocks/throws; the manual toggle is the documented source of truth.
- Apple parity (PunktfunkConnection.swift): GamepadType.steamController/steamDeck
(wire 5/6) + name parsing, so the resolved type round-trips. Capture is blocked
(GameController never surfaces a 0x28DE HID device).
- Android parity (Gamepad.kt): PREF_STEAMCONTROLLER/STEAMDECK + the Valve 0x28DE
PIDs in prefFor(). Rich-input capture stays out of scope (no rich-input plane
yet) — standard buttons/sticks resolve to the host's Steam Deck pad.
Rust workspace clippy/fmt/test green; Decky src/ typechecks clean (only a
pre-existing @decky/api dep resolution error remains); Swift/Kotlin compile on
their CI. The full pipeline is now BUILT; what remains is validation that needs
hardware we don't have (a running Steam on the host, a live Deck client, the
Moonlight paddle regression). Not pushed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Plugin self-update (no Decky store): CI publishes a per-channel manifest.json
({version, immutable per-version artifact, sha256}) beside the zip and bakes
update.json {channel, manifest} into the plugin. main.py `check_update` reads the
installed version from package.json (the value Decky reports — not plugin.json),
fetches the channel manifest, and the frontend shows an "Update to vX" button that
drives Decky Loader's own install RPC (root downloads + SHA-256-verifies + hot-reloads).
CI now stamps a plain-numeric semver (0.3.<run> canary / X.Y.Z stable) into
package.json — a -ciN suffix would mis-order under compare-versions.
Linux client: `--fullscreen` (plus SteamDeck/gamescope env fallback) enters GTK
fullscreen on stream start so Gaming-Mode chrome is hidden; native-mode resolution
falls back to the display's first monitor when the window isn't mapped yet (was
dropping to the 1080p floor — wrong on the Deck's 1280×800); add a confirmed
"Remove saved host" action (KnownHosts::remove_by_fp).
Docs: new docs/steam-deck.md (Decky install/pair/stream/self-update/troubleshooting),
wired into meta.json nav, and cross-linked from clients/install-client/channels. This
is the page docs.punktfunk.unom.io/docs/steam-deck — the website's download link
pointed at it before it existed; committing it makes that link resolve.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The plugin was a QAM launcher whose stream never appeared, with no
pairing. Three fixes, plus a headless --pair mode on the GTK client:
- Stream actually starts (MoonDeck's proven mechanism): gamescope only
focuses the process tree Steam launched via reaper, so a flatpak
spawned from the (root) backend is invisible. The frontend now
registers ONE hidden non-Steam shortcut pointing at bin/punktfunkrun.sh,
passes the host as the shortcut's Steam launch options, and starts it
with SteamClient.Apps.RunGame — gamescope then fullscreen-focuses it.
The wrapper execs `flatpak run io.unom.Punktfunk --connect <host>`.
- Fullscreen page: routerHook.addRoute("/punktfunk") — host list,
per-host Pair/Stream, and a settings section (resolution/refresh/
bitrate/gamepad/mic, written to client-gtk-settings.json).
- Pairing: a gamepad-navigable PIN keypad. The host shows the PIN; the
backend runs the SPAKE2 ceremony headlessly via the client's new
`--pair <PIN> --connect host` CLI mode (app.rs), persisting the host
as paired so the stream then connects silently. Same flatpak =>
shared identity store, verified live (ceremony against a real host).
- Backend (main.py): discover / pair / runner_info / get_settings /
set_settings / kill_stream; uses DECKY_USER_HOME so paths resolve to
the deck user's flatpak install regardless of the plugin's root flag.
CI (decky.yml) and the sideload packager now ship bin/punktfunkrun.sh.
The Steam-shortcut launch and headless-pairing env follow MoonDeck
exactly but need a Deck in Gaming Mode to fully confirm.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>