The console can switch itself off, and a TV remote can reach everything it offers #354

Merged
enricobuehler merged 4 commits from worktree-console-ui-off-toggle into main 2026-08-20 17:46:39 +00:00
Owner

User-facing fact changed? Yes — two knobs and two routes. docs-site/content/docs/client-settings.md (the Interface section, which owns "Gamepad-optimized browsing" and "Show it") and docs-site/content/docs/game-library.md (the Android route to the shelf) are updated in this PR.


Two problems, both consequences of the console UI becoming the only UI on Android.

The console had no way to leave itself

The console's settings screen had a picker for when it fronts the app but no switch for whether it does — that lived only in the touch settings, which someone already inside the console has no way to reach. The way back out was a setting you could only change from the thing you were trying to get to.

The "Controller-optimized UI" row is now on the console's own settings screen, over the same gamepadUiEnabled the touch switch writes (folded through extra under android.gamepad_ui_enabled, like the other Android-only rows). Turning it off returns to the touch home at once: the console's save bumps the snapshot generation, the host emits its settings event, and Kotlin's applySettings folds the flag back into the state gamepadUiActive reads.

It appears only where "off" has somewhere to land. ConsoleOptions::fallback_ui (new, threaded to Ctx beside deck) is true only for the Android touch shell — false on a TV and for the desktop session, where this console is the interface and an off switch would strand the user in nothing.

The mode row follows the same rule and moves under it. It decides nothing on a TV (the tv term satisfies gamepadUiActive's OR on its own) or while the switch is off, so it is hidden in both cases rather than offered as a control that changes nothing, and it takes the touch screen's own word for it — "Show it". "Controller UI" directly under "Controller-optimized UI" was two rows told apart by their tails.

An Android TV remote could not reach Settings or the library

A TV's only input is a D-pad, OK and Back. The console bound Settings to X and the game library to Y, and neither event can be produced by a remote: the Kotlin bridge emits Move/Confirm/Back and nothing else, the carousel's only action tiles are Add Host and Rescan, and the hint chips that also carry those actions need a finger on a touchscreen. So on a TV the two were simply unreachable. This is the gap the deleted Compose home used to paper over with its own "no face buttons" mapping, and the same wall Apple hit on the Siri Remote.

  • Down on the carousel opens Settings — the free direction beside up, which already opens the host menu. A second route on a pad, the only route on a remote, so the hint bar names the one the device in hand has: ▼ with no pad attached, X with one. That is the whole extent of the legend change.
  • The library gains a row on the host's options menu, which up reaches, on exactly the terms Y offers it (saved and paired). It replaces the menu rather than stacking on it, so Back from the shelf lands on the carousel and not on a menu about the host just left. This is also the route docs/game-library.md has been describing all along — "open its options and choose Library" — which until now did not exist.

Quick access is unchanged for pad users: Y still opens the library from the carousel and the legend still says so. The menu row is purely additive, for input that has no Y at all.

▼ is a new hint glyph — the ▲ triangle stood on its head rather than a second drawing routine — and pressable in the hint bar exactly as ▲ is.

Verification

cargo test -p pf-console-ui and cargo clippy -p pf-console-ui --all-targets -- -D warnings both green in the punktfunk-rust-ci image (198 passed, 0 failed; macOS compiles this crate to a stub, so a host cargo check proves nothing here). The Android Kotlin unit tests pass, and the crate's lib builds for aarch64-linux-android.

New tests: a padless device reaches Settings and the options menu with directions alone and the legend switches between ▼ and X; the Library row appears for a paired host, fetches, replaces the menu, and is absent when unpaired; the console-off switch is offered only with a fallback UI and the mode row only when it decides something.

shell::tests::a_replace_carries_the_screen_it_replaced walks the options menu by exact row count on purpose, so the new row shifted "Edit…" from two down to three — updated, and the test did its job.

**User-facing fact changed?** Yes — two knobs and two routes. `docs-site/content/docs/client-settings.md` (the Interface section, which owns "Gamepad-optimized browsing" and "Show it") and `docs-site/content/docs/game-library.md` (the Android route to the shelf) are updated in this PR. --- Two problems, both consequences of the console UI becoming the only UI on Android. ### The console had no way to leave itself The console's settings screen had a picker for *when* it fronts the app but no switch for *whether* it does — that lived only in the touch settings, which someone already inside the console has no way to reach. The way back out was a setting you could only change from the thing you were trying to get to. The "Controller-optimized UI" row is now on the console's own settings screen, over the same `gamepadUiEnabled` the touch switch writes (folded through `extra` under `android.gamepad_ui_enabled`, like the other Android-only rows). Turning it off returns to the touch home at once: the console's save bumps the snapshot generation, the host emits its settings event, and Kotlin's `applySettings` folds the flag back into the state `gamepadUiActive` reads. It appears only where "off" has somewhere to land. `ConsoleOptions::fallback_ui` (new, threaded to `Ctx` beside `deck`) is true only for the Android touch shell — false on a TV and for the desktop session, where this console **is** the interface and an off switch would strand the user in nothing. The mode row follows the same rule and moves under it. It decides nothing on a TV (the `tv` term satisfies `gamepadUiActive`'s OR on its own) or while the switch is off, so it is hidden in both cases rather than offered as a control that changes nothing, and it takes the touch screen's own word for it — "Show it". "Controller UI" directly under "Controller-optimized UI" was two rows told apart by their tails. ### An Android TV remote could not reach Settings or the library A TV's only input is a D-pad, OK and Back. The console bound Settings to **X** and the game library to **Y**, and neither event can be produced by a remote: the Kotlin bridge emits Move/Confirm/Back and nothing else, the carousel's only action tiles are Add Host and Rescan, and the hint chips that also carry those actions need a finger on a touchscreen. So on a TV the two were simply unreachable. This is the gap the deleted Compose home used to paper over with its own "no face buttons" mapping, and the same wall Apple hit on the Siri Remote. - **Down** on the carousel opens Settings — the free direction beside up, which already opens the host menu. A second route on a pad, the only route on a remote, so the hint bar names the one the device in hand has: ▼ with no pad attached, **X** with one. That is the whole extent of the legend change. - **The library** gains a row on the host's options menu, which **up** reaches, on exactly the terms Y offers it (saved *and* paired). It replaces the menu rather than stacking on it, so Back from the shelf lands on the carousel and not on a menu about the host just left. This is also the route `docs/game-library.md` has been describing all along — "open its options and choose Library" — which until now did not exist. **Quick access is unchanged for pad users:** Y still opens the library from the carousel and the legend still says so. The menu row is purely additive, for input that has no Y at all. ▼ is a new hint glyph — the ▲ triangle stood on its head rather than a second drawing routine — and pressable in the hint bar exactly as ▲ is. ### Verification `cargo test -p pf-console-ui` and `cargo clippy -p pf-console-ui --all-targets -- -D warnings` both green in the `punktfunk-rust-ci` image (198 passed, 0 failed; macOS compiles this crate to a stub, so a host `cargo check` proves nothing here). The Android Kotlin unit tests pass, and the crate's lib builds for `aarch64-linux-android`. New tests: a padless device reaches Settings and the options menu with directions alone and the legend switches between ▼ and X; the Library row appears for a paired host, fetches, replaces the menu, and is absent when unpaired; the console-off switch is offered only with a fallback UI and the mode row only when it decides something. `shell::tests::a_replace_carries_the_screen_it_replaced` walks the options menu by exact row count on purpose, so the new row shifted "Edit…" from two down to three — updated, and the test did its job.
enricobuehler added 3 commits 2026-08-20 07:11:25 +00:00
The Android console's settings screen had a picker for WHEN it fronts the
app but no switch for WHETHER it does — that lived only in the touch
settings, which a user already inside the console has no way to reach.
The console UI became the only UI on Android, so the way back out was a
setting you could only change from the thing you were trying to get to.

Adds the "Controller-optimized UI" row over the same `gamepadUiEnabled`
the touch switch writes, folded through `extra` under
`android.gamepad_ui_enabled` like the other Android-only rows. Off
returns to the touch home immediately: the console's save bumps the
snapshot generation, the host emits its settings event, and Kotlin's
`applySettings` now folds the flag back into the App state that
`gamepadUiActive` reads.

Shown only where "off" has somewhere to land. `ConsoleOptions.fallback_ui`
(new, threaded to `Ctx` beside `deck`) is true only for the Android touch
shell — false on a TV and for the desktop session, where this console IS
the interface and an off switch would strand the user in nothing.

The mode row follows the same rule and moves under it: it decides nothing
on a TV (the tv term satisfies `gamepadUiActive`'s OR on its own) or while
the switch is off, so it is hidden in both cases rather than offered as a
control that changes nothing. Renamed to the touch screen's own word for
it, "Show it" — "Controller UI" directly under "Controller-optimized UI"
was two rows told apart by their tails.
The Interface section owned two facts this changes: which surfaces carry
"Gamepad-optimized browsing" (the Android console home carries it now, so
that home can be left from inside it) and what an Android TV sees — the
mode row used to be described as present-but-inert there, and both rows are
simply absent now.
fix(console-ui): Settings and the library are reachable without face buttons, for a TV remote
ci / docs-drift (pull_request) Successful in 36s
ci / bun-nix (pull_request) Successful in 38s
ci / web (pull_request) Successful in 1m15s
ci / docs-site (pull_request) Successful in 1m16s
ci / rust-arm64 (pull_request) Successful in 1m29s
android / android (pull_request) Canceled after 1m54s
ci / rust (pull_request) Canceled after 1m26s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (pull_request) Canceled after 0s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (pull_request) Canceled after 1m59s
76e6618b84
An Android TV's only input is its remote — a D-pad, OK and Back — and the
console bound both Settings and the game library to face buttons that
remote does not have. X opened Settings, Y opened the shelf, and neither
event could be produced: the Kotlin bridge emits Move/Confirm/Back for a
remote and nothing else, the carousel's only action tiles are Add Host and
Rescan, and the hint chips that also carry those actions need a finger on
a touchscreen. So on a TV the two were simply unreachable. This is the
gap the deleted Compose home used to paper over with its own "no face
buttons" mapping, and the same wall Apple hit on the Siri Remote.

Down on the carousel now opens Settings — the other free direction beside
up, which already opens the host menu. It is a second route on a pad and
the only route on a remote, so the hint bar names the one the device in
hand actually has: ▼ with no pad attached, X with one. That is the whole
extent of the legend change; Y still opens the library and still says so.

The shelf gains a Library row on the host's own options menu, which up
reaches, offered on exactly the terms Y offers it (saved AND paired). It
replaces the menu rather than stacking on it, so Back from the shelf lands
on the carousel and not on a menu about the host just left. This is also
the route `docs/game-library.md` has been describing all along — "open its
options and choose Library" — which until now did not exist.

▼ is a new hint glyph: the ▲ triangle stood on its head rather than a
second drawing routine, and pressable in the hint bar exactly as ▲ is.
enricobuehler added 1 commit 2026-08-20 07:13:37 +00:00
Merge remote-tracking branch 'origin/main' into worktree-console-ui-off-toggle
android / android (pull_request) Successful in 7m15s
windows-client / client (x64, , x86_64-pc-windows-msvc, C:\t) (pull_request) Successful in 6m44s
windows-client / client (arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (pull_request) Successful in 3m8s
ci / bun-nix (pull_request) Successful in 31s
ci / docs-drift (pull_request) Successful in 31s
ci / web (pull_request) Successful in 1m4s
ci / docs-site (pull_request) Successful in 2m3s
ci / rust-arm64 (pull_request) Successful in 2m14s
ci / rust (pull_request) Successful in 6m34s
7af6c323d0
# Conflicts:
#	docs-site/content/docs/client-settings.md
enricobuehler merged commit 6e4cc335c5 into main 2026-08-20 17:46:39 +00:00
enricobuehler deleted branch worktree-console-ui-off-toggle 2026-08-20 17:46:43 +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#354