feat(clients/input): system buttons route around local overlays
ci / rust-arm64 (pull_request) Failing after 48s
apple / swift (pull_request) Successful in 1m18s
apple / screenshots (pull_request) Skipped
ci / docs-site (pull_request) Successful in 1m22s
android / android (pull_request) Successful in 3m22s
ci / web (pull_request) Successful in 1m39s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m0s
ci / rust (pull_request) Canceled after 10m44s
windows / build (x86_64-pc-windows-msvc) (pull_request) Canceled after 1m43s

Pressing guide/Steam/QAM collided with the client device's own shell: iOS 26
opens its Game Overlay for the Home press (no app opt-out until iOS 27 makes
it a user setting), and a Gaming-Mode client opened BOTH Steam overlays for
one press — the local one covering the stream.

Two cross-client tier-P settings, zero wire changes:

- system_buttons (auto|forward|local): raw guide+misc1 passthrough. Auto
  forwards everywhere EXCEPT under gamescope, where SteamOS reacts to the
  same physical press no matter what.
- guide_gesture (auto|on|off): hold Select ALONE ~350ms sends the HOST's
  guide, down until release — held on, that's the host's long-press, which
  opens a Gaming-Mode host's QAM for regular pads. A Select tap is delivered
  on release with its up TAP_PRESS (50ms) behind, because per-transition
  sends fold into seq'd GamepadState snapshots and a back-to-back pair can
  coalesce into no press at all. A Select inside a combo (the escape chord)
  passes through untouched. Auto arms it only where the raw press can't
  reach the host cleanly: gamescope, iOS/iPadOS, tvOS.

The same SelectGesture rules live in pf-client-core (pure state machine +
unit tests), the Apple client (mask-diff adaptation in GamepadCapture), and
Android's GamepadRouter. Settings rows on every surface (GTK, WinUI,
console UI, Decky, Apple x2, Android x2) with profile plumbing throughout.

punktfunk-session grows a control socket
($XDG_RUNTIME_DIR[/app/$FLATPAK_ID]/punktfunk-session-ctl.sock — the one
runtime path a flatpak and the host see identically): 'guide'/'qam' verbs
inject synthetic taps. The Decky panel gains a Host menus section (visible
while the client runs) whose buttons press the host's Steam/QAM and close
the local menu so the host's shows through.

iOS 27's GCControllerHomeButtonSettingsManager deep-link is a TODO (the
class needs the Xcode 27 SDK to compile). Docs: input, client-settings,
steam-deck. Design: punktfunk-planning design/system-buttons-routing.md.

Gates: docker clippy --all-targets --locked -D warnings + tests
(pf-client-core 88 incl. 6 new gesture tests, pf-console-ui 47),
cargo fmt --all --check, swift build (macOS), gradle kit+app compile,
decky tsc --noEmit + py_compile. clients/windows not compiled (no box).
This commit is contained in:
2026-08-04 21:11:05 +02:00
parent 454fa2e0cb
commit ac395bbfa9
29 changed files with 1439 additions and 16 deletions
+15
View File
@@ -193,6 +193,21 @@ which forwards *every* connected controller, each as its own player, on Linux, W
console home. Pinning one restricts the session to that controller alone — single-player. The Android
app has no such picker.
**Steam / guide button** (*Guide button* on Apple and Android) — *default: Automatic*, on every
client. Where the guide (Xbox/PS/Steam) and quick-access presses go while streaming: **Send to
host** forwards them raw, **This device** keeps them local. Automatic forwards everywhere except
Gaming Mode, where SteamOS opens its own menus for those buttons no matter what — forwarding raw
there opens *both* menus at once, the local one covering the stream. The full story, including how
to reach the host's menus when the raw press stays local, is on the
[Input page](/docs/input#the-guide-button-xbox--ps--steam-and-quick-access).
**Hold Select for guide***default: Automatic*, on every client. The gesture that presses the
host's guide button from any controller: hold Select (Back/View) on its own for about a third of a
second, and keep holding for the host's long-press (a Gaming-Mode host's Quick Access Menu, on a
regular pad). Automatic arms it only where the raw guide press can't reach the host cleanly —
Gaming Mode, iPhone/iPad, Apple TV — because the gesture has a cost: a Select *tap* arrives a beat
late, and a game that expects a *held* Select would trigger it. Set **On** or **Off** to overrule.
**Capture system shortcuts***default: on.* Offered by the Linux and Windows apps, the console home
and Decky; Windows spells the row out as *Capture system shortcuts (Alt+Tab, Win, …)*. On a Deck it
matters only for a keyboard you attached yourself, for the reason the paragraph below gives: Gaming
+33
View File
@@ -98,6 +98,39 @@ there the client stops opening the controller at all, which is the point of the
**Ctrl+Alt+Shift+D** or the client's own UI to leave instead. The Apple and Android apps keep
watching for the chord either way.
### The guide button (Xbox / PS / Steam) and Quick Access
A controller's **guide button** — the Xbox logo, the PS button, the Deck's **Steam** button — is
meant to open menus **on the host**: the Steam overlay, or a Gaming-Mode host's Steam menu. Some
devices want that button for themselves, so every client also carries a gesture that works
everywhere:
**Hold Select (Back / View) on its own for about a third of a second.** The host sees its guide
button go down, and it stays down for as long as you hold — so keeping it held reads as a long
press on the host, which is how SteamOS opens the **Quick Access Menu** for a regular pad. A quick
tap of Select still reaches the game, delivered when you let go (a beat late). Select pressed as
part of a combo — including the leave chord above — passes through untouched.
What the raw button does, per client:
- **Linux & Windows desktop, macOS, Android** — the guide press is forwarded to the host. If
Steam Big Picture or the Xbox Game Bar is also watching for it *on the device in your hands*,
both may react — that's a local setting on that device, not something the stream can suppress.
- **Steam Deck / Gaming Mode** — the **Steam** and **`…`** buttons stay with the Deck by default:
SteamOS always opens its own menus for them, so forwarding the raw press as well opened BOTH
menus at once, the Deck's on top of the stream. Reach the host's menus with **hold-Select**, or
with the Punktfunk panel's **Host menus** buttons ([Steam Deck page](/docs/steam-deck)). The
old behavior is one setting away: **Steam / guide button → Send to host**.
- **iPhone / iPad** — iOS reserves the Home press for its own Game Overlay, so hold-Select is the
reliable route to the host's overlay. On iOS 27 or later you can also hand the button to the
app yourself, in the system's per-controller Home-button setting.
- **Apple TV** — tvOS never delivers the Home press to apps; hold-Select is the only route.
Both halves are [settings](/docs/client-settings#input), per profile like everything else:
**Steam / guide button** (Automatic / Send to host / This device) and **Hold Select for guide**
(Automatic / On / Off). Automatic picks the behavior above for each platform — the gesture stays
off where the raw button already works, so games that use a *held* Select keep it.
## Mouse modes
There are two, and they are a per-client setting called **Mouse input**:
+9
View File
@@ -111,6 +111,15 @@ for about a second and a half, or close the "game" from the Steam overlay. Eithe
and drops you straight back to Gaming Mode. A quick press of the same four only releases captured
input, so it is safe to hit by accident.
**The Steam and `` buttons stay with the Deck while streaming.** SteamOS opens its own menus for
them no matter what, so forwarding the raw press as well opened *both* menus at once — the Deck's
covering the stream. To reach the **host's** menus instead: **hold Select** for the host's Steam
menu ([how it works](/docs/input#the-guide-button-xbox--ps--steam-and-quick-access)), or open the
Punktfunk panel — while a stream runs it grows a **Host menus** section whose two buttons,
**Steam menu on host** and **Quick access on host**, press the button on the host and close the
Deck's own menu so the host's shows through. Want the raw forwarding back? Settings →
**Steam / guide button** → *Send to host*.
## Updating
The plugin **checks for updates itself** — no Decky store needed. It covers **both** the plugin *and*