The Apple TV client had no way to show its statistics overlay #125

Merged
enricobuehler merged 1 commits from worktree-tvos-stats-shortcut into main 2026-08-08 21:48:24 +00:00
Owner

An Apple TV session could not reach the statistics overlay at all.

Not a bug in the HUD — it renders on tvOS, and the docs already describe its tvOS-specific
floor-shaved math. There was simply no trigger:

  • StreamCommands.swift, which carries the ⌃⌥⇧S surface, is #if !os(tvOS)
  • InputCapture.onCycleStats fires only from the macOS keyDown monitor
  • TouchMouse's three-finger tap needs a touchscreen

So the only way to change the tier was Settings before connecting, or a profile's
stats_verbosity. The "Cycle with" table in docs-site/content/docs/stats.md listed every
platform except Apple TV — that table is the quickest way to spot a client missing a surface.

What this adds

Two surfaces, because an Apple TV may have a controller in the room or only the remote.

Select + X on a controller — cycles one tier per completion (off → compact → normal →
detailed → off). Modelled on Android's GamepadRouter.MIC_CHORD (Select + Y), and deliberately
disjoint from the escape chord: X is none of L1+R1+Start+Select, so no way of reaching one chord
passes through the other. It reads the wire mask like the escape chord does, so a Select that the
hold-Select gesture has already turned into a guide cannot cycle the overlay on its way past. Both
buttons still forward to the host — the tier change is local.

It is active on every Apple platform, not only tvOS: a controller in both hands can reach
neither the keyboard combo nor the three-finger tap, which is the same problem on iPhone, iPad and
macOS. Y is left free so a mic chord can be ported onto it later without moving this one.

Hold Play/Pause on the Siri Remote (~0.5 s) — the remote's only spare button, and the only
route to the numbers on an Apple TV with no controller. The right-click is therefore deferred
until the press resolves: a tap still right-clicks, delivered on release with the release trailing
by tapPress. A right button held for half a second is a context menu on every desktop this
streams, so it could not simply go down on contact. The shape mirrors the existing hold-Select
gesture (suppress → tap on release, or the gesture past the threshold).

The trap this had to avoid

openSlot claims every element's preferredSystemGestureState while forwarding is on, but only
the chord elements' while it is off. A new chord therefore has to join that narrowed list —
otherwise, with Forward controllers off, X's press stays the system's on tvOS and the chord
silently never completes. That list is now GamepadCapture.chordElements (escape ∪ stats, with
Select shared once).

Tests

GamepadStatsChordTests pins the invariants whose failure mode is nothing happening, with nothing
logged
: each chord's mask against its GameController alias list, that the two overlap only on
Select and that neither is a subset of the other, that the claim list covers both without
duplicates, and that the tier cycle reaches every tier and returns.

Verification

  • tvOS typecheck (swift build --triple arm64-apple-tvos17.0) — Build complete!
  • swift test262 tests, 0 failures; the new suite passes 5/5

On-glass on a real Apple TV is still owed — both the chord and the deferred right-click.

Docs updated: stats.md gains its missing tvOS rows, input.md gains a "Statistics with a
controller" section and the Apple TV bullet now mentions the hold.

An Apple TV session could not reach the statistics overlay at all. Not a bug in the HUD — it renders on tvOS, and the docs already describe its tvOS-specific floor-shaved math. There was simply no **trigger**: - `StreamCommands.swift`, which carries the ⌃⌥⇧S surface, is `#if !os(tvOS)` - `InputCapture.onCycleStats` fires only from the **macOS** keyDown monitor - `TouchMouse`'s three-finger tap needs a touchscreen So the only way to change the tier was Settings before connecting, or a profile's `stats_verbosity`. The "Cycle with" table in `docs-site/content/docs/stats.md` listed every platform except Apple TV — that table is the quickest way to spot a client missing a surface. ## What this adds Two surfaces, because an Apple TV may have a controller in the room or only the remote. **Select + X on a controller** — cycles one tier per completion (off → compact → normal → detailed → off). Modelled on Android's `GamepadRouter.MIC_CHORD` (Select + Y), and deliberately disjoint from the escape chord: X is none of L1+R1+Start+Select, so no way of reaching one chord passes through the other. It reads the wire mask like the escape chord does, so a Select that the hold-Select gesture has already turned into a guide cannot cycle the overlay on its way past. Both buttons still forward to the host — the tier change is local. It is active on **every Apple platform**, not only tvOS: a controller in both hands can reach neither the keyboard combo nor the three-finger tap, which is the same problem on iPhone, iPad and macOS. Y is left free so a mic chord can be ported onto it later without moving this one. **Hold Play/Pause on the Siri Remote** (~0.5 s) — the remote's only spare button, and the only route to the numbers on an Apple TV with no controller. The right-click is therefore *deferred* until the press resolves: a tap still right-clicks, delivered on release with the release trailing by `tapPress`. A right button held for half a second is a context menu on every desktop this streams, so it could not simply go down on contact. The shape mirrors the existing hold-Select gesture (suppress → tap on release, or the gesture past the threshold). ## The trap this had to avoid `openSlot` claims every element's `preferredSystemGestureState` while forwarding is on, but only the **chord** elements' while it is off. A new chord therefore has to join that narrowed list — otherwise, with **Forward controllers** off, X's press stays the system's on tvOS and the chord silently never completes. That list is now `GamepadCapture.chordElements` (escape ∪ stats, with Select shared once). ## Tests `GamepadStatsChordTests` pins the invariants whose failure mode is *nothing happening, with nothing logged*: each chord's mask against its GameController alias list, that the two overlap only on Select and that neither is a subset of the other, that the claim list covers both without duplicates, and that the tier cycle reaches every tier and returns. ## Verification - tvOS typecheck (`swift build --triple arm64-apple-tvos17.0`) — **Build complete!** - `swift test` — **262 tests, 0 failures**; the new suite passes 5/5 ⏳ **On-glass on a real Apple TV is still owed** — both the chord and the deferred right-click. Docs updated: `stats.md` gains its missing tvOS rows, `input.md` gains a "Statistics with a controller" section and the Apple TV bullet now mentions the hold.
enricobuehler added 1 commit 2026-08-08 21:43:02 +00:00
feat(apple): the statistics overlay is reachable on tvOS
ci / web (pull_request) Successful in 1m0s
ci / bun-nix (pull_request) Successful in 1m11s
ci / docs-site (pull_request) Successful in 1m18s
apple / swift (pull_request) Successful in 1m34s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 4m5s
ci / rust (pull_request) Successful in 6m27s
bed58b75b6
An Apple TV session had no way to the stats overlay at all. Every other client
cycles it in-stream — Ctrl+Alt+Shift+S on the desktops, a three-finger tap on
touch — and tvOS has neither a keyboard nor a screen to tap, so the only route
was Settings before connecting (or a profile). The docs' own "cycle with" table
simply had no row for it.

Two surfaces, because an Apple TV may have a controller in the room or only the
remote:

- Select + X on a controller, cycling one tier per completion. Built like
  Android's mic chord (Select + Y) and deliberately disjoint from the escape
  chord — X is none of its four buttons, so reaching for one can never trip the
  other. Read off the wire mask like the escape chord, so a Select the
  hold-Select gesture has turned into a guide can't cycle the overlay on its way
  past. Available on every Apple platform: a controller in both hands is exactly
  the case the keyboard combo and the three-finger tap can't serve.

- Hold Play/Pause on the Siri Remote. Its right-click is therefore deferred until
  the press resolves — a tap still right-clicks, delivered on release with the
  release trailing by TAP_PRESS — because a right button held for half a second
  is a context menu on every desktop this streams.

A non-forwarding slot now claims the stats chord's elements too, alongside the
escape chord's: on tvOS an unclaimed button's press stays the system's and the
chord would silently never complete.

Tests pin both chords' masks against their GameController alias lists, that the
two overlap only on Select, and that the claim list covers both without
duplicates — the failure mode is nothing happening, with nothing logged.
enricobuehler merged commit 54666e66da into main 2026-08-08 21:48:24 +00:00
enricobuehler deleted branch worktree-tvos-stats-shortcut 2026-08-08 21:48:31 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unom/punktfunk#125