A launcher tile can open Heroic's console mode #402

Merged
enricobuehler merged 1 commits from worktree-heroic-console-and-runners into main 2026-08-26 17:26:22 +00:00
Owner

The host half of punktfunk-plugin-heroic#5. Nothing here is useful without that PR, and that PR must not be published until this ships — see the compatibility note below.

launcher_ui takes heroic-console

Heroic 2.21 added a fullscreen gamepad UI — the Big Picture of that launcher, and the tile you actually want on a stream. Nothing could publish it: a plugin names a launcher UI and the host builds the line, and the only Heroic value the host knew was heroic, which opens the ordinary window.

heroic-console resolves to the same prefix heroic does (native binary if on PATH, else the Flatpak) plus --console --fullscreen. Both flags, because Heroic reads them separately:

// src/backend/constants/environment.ts
export const isCLIFullscreen   = process.argv.includes('--fullscreen')
export const isCLIConsoleMode  = process.argv.includes('--console')

--console only routes the UI to that front end; --fullscreen is what fills the screen. No URI can reach it — heroic:// handles exactly ping and launch — which is the same reason Playnite's fullscreen tile spawns its exe directly instead of going through the registered protocol handler. An older Heroic ignores the unknown --console and still honours --fullscreen, so the tile degrades to a fullscreen desktop UI rather than to nothing.

Why a value and not a heroic_ui kind

A heroic_ui kind mirroring steam_ui would be tidier, and I rejected it on a compatibility asymmetry that runs the other way from what you'd guess:

  • an unknown kind is accepted on the wire and yields no recipe — an unlaunchable tile;
  • an unknown value is a hard 400 in validate_provider_payload, which refuses the entire reconcile.

So either shape has to be gated on minHost in the plugin index regardless, and the value is much the smaller change. The doc comment on launcher_ui_stores is corrected to say the value names a launcher UI, not a launcher — which it already did on Windows, where playnite has always meant Playnite.FullscreenApp.exe rather than the desktop app.

⚠️ The plugin index entry for heroic 0.2.0 must carry a minHost at or above the release this lands in. Without it, the new plugin on an older host 400s every reconcile: a fresh install shows an empty grid, an upgrade quietly stops updating.

Second fix: a Heroic tile that could never launch was still published

resolvable_launcher_ui special-cased only Playnite-on-Windows and returned true for everything else, so a launcher_ui: "heroic" entry survived sanitize_launcher_entries even when heroic_launch_prefix() resolves to nothing — no heroic on the host service's PATH and no ~/.var/app/com.heroicgameslauncher.hgl under its $HOME.

Keeping ~/.config/heroic after uninstalling Heroic is enough to reach it, because the plugin's detect only looks for that directory. The operator got a launcher tile that does nothing. Both Heroic values now probe the prefix, exactly as Playnite probes for its exe — the tile is dropped and the games still sync, which is the whole point of that function.

Also

  • plugin-kit/src/wire.ts: the launch-kind table row for launcher_ui. Comment only — no kit release needed.
  • docs-site/content/docs/game-library.md: the launcher-tile paragraph now says a launcher with both a couch UI and a window gets two tiles.
  • CHANGELOG.md: appended under the current top section, plugin-facing.

Not done here

I did not add inbound validation for the heroic launch kind, though it is the one closed-vocabulary kind without any (steam_ui, launcher_ui, playnite, xbox all have it). I wrote it, then reverted it: it converts a value heroic_command would already refuse from one dead tile into a 400 that refuses the whole reconcile. Everyone it would affect already has a broken tile, and trading that for no library is strictly worse — the exact lesson sanitize_launcher_entries exists for. Worth revisiting only alongside a general "drop the entry, don't 400 the batch" pass.

Verification

rustfmt --check --edition 2024 clean on the changed files. The Linux-gated code cannot be compiled on this Mac, and the emulated CI-image run was queued behind another job's container for the whole session — so ci / rust here is the first real compile of this change. The new assertions live in launcher_ui_accepts_only_launchers_this_host_can_open and launcher_ui_opens_the_launcher_itself; both are written to be correct on a box without Heroic (they assert the tile resolves iff the prefix does).

The host half of [punktfunk-plugin-heroic#5](https://git.unom.io/unom/punktfunk-plugin-heroic/pulls/5). Nothing here is useful without that PR, and that PR must not be published until this ships — see the compatibility note below. ## `launcher_ui` takes `heroic-console` Heroic 2.21 added a fullscreen gamepad UI — the Big Picture of that launcher, and the tile you actually want on a stream. Nothing could publish it: a plugin names a launcher UI and the host builds the line, and the only Heroic value the host knew was `heroic`, which opens the ordinary window. `heroic-console` resolves to the same prefix `heroic` does (native binary if on `PATH`, else the Flatpak) plus `--console --fullscreen`. **Both** flags, because Heroic reads them separately: ```ts // src/backend/constants/environment.ts export const isCLIFullscreen = process.argv.includes('--fullscreen') export const isCLIConsoleMode = process.argv.includes('--console') ``` `--console` only routes the UI to that front end; `--fullscreen` is what fills the screen. No URI can reach it — `heroic://` handles exactly `ping` and `launch` — which is the same reason Playnite's fullscreen tile spawns its exe directly instead of going through the registered protocol handler. An older Heroic ignores the unknown `--console` and still honours `--fullscreen`, so the tile degrades to a fullscreen desktop UI rather than to nothing. ### Why a value and not a `heroic_ui` kind A `heroic_ui` kind mirroring `steam_ui` would be tidier, and I rejected it on a compatibility asymmetry that runs the other way from what you'd guess: - an unknown **kind** is accepted on the wire and yields no recipe — an unlaunchable tile; - an unknown **value** is a hard 400 in `validate_provider_payload`, which refuses the *entire* reconcile. So either shape has to be gated on `minHost` in the plugin index regardless, and the value is much the smaller change. The doc comment on `launcher_ui_stores` is corrected to say the value names a launcher **UI**, not a launcher — which it already did on Windows, where `playnite` has always meant `Playnite.FullscreenApp.exe` rather than the desktop app. > ⚠️ **The plugin index entry for `heroic` 0.2.0 must carry a `minHost` at or above the release this lands in.** Without it, the new plugin on an older host 400s every reconcile: a fresh install shows an empty grid, an upgrade quietly stops updating. ## Second fix: a Heroic tile that could never launch was still published `resolvable_launcher_ui` special-cased only Playnite-on-Windows and returned `true` for everything else, so a `launcher_ui: "heroic"` entry survived `sanitize_launcher_entries` even when `heroic_launch_prefix()` resolves to nothing — no `heroic` on the host service's `PATH` and no `~/.var/app/com.heroicgameslauncher.hgl` under its `$HOME`. Keeping `~/.config/heroic` after uninstalling Heroic is enough to reach it, because the plugin's `detect` only looks for that directory. The operator got a launcher tile that does nothing. Both Heroic values now probe the prefix, exactly as Playnite probes for its exe — the tile is dropped and the games still sync, which is the whole point of that function. ## Also - `plugin-kit/src/wire.ts`: the launch-kind table row for `launcher_ui`. Comment only — no kit release needed. - `docs-site/content/docs/game-library.md`: the launcher-tile paragraph now says a launcher with both a couch UI and a window gets two tiles. - `CHANGELOG.md`: appended under the current top section, plugin-facing. ## Not done here I did **not** add inbound validation for the `heroic` launch kind, though it is the one closed-vocabulary kind without any (`steam_ui`, `launcher_ui`, `playnite`, `xbox` all have it). I wrote it, then reverted it: it converts a value `heroic_command` would already refuse from *one dead tile* into a 400 that refuses the whole reconcile. Everyone it would affect already has a broken tile, and trading that for no library is strictly worse — the exact lesson `sanitize_launcher_entries` exists for. Worth revisiting only alongside a general "drop the entry, don't 400 the batch" pass. ## Verification `rustfmt --check --edition 2024` clean on the changed files. The Linux-gated code cannot be compiled on this Mac, and the emulated CI-image run was queued behind another job's container for the whole session — **so `ci / rust` here is the first real compile of this change.** The new assertions live in `launcher_ui_accepts_only_launchers_this_host_can_open` and `launcher_ui_opens_the_launcher_itself`; both are written to be correct on a box without Heroic (they assert the tile resolves *iff* the prefix does).
enricobuehler added 1 commit 2026-08-26 17:10:49 +00:00
feat(host/library): launcher_ui can open Heroic's console mode
ci / docs-site (pull_request) Successful in 1m18s
ci / web (pull_request) Successful in 1m21s
ci / docs-drift (pull_request) Successful in 28s
ci / bun-nix (pull_request) Successful in 1m54s
ci / rust-arm64 (pull_request) Successful in 2m45s
android / android (pull_request) Successful in 5m18s
ci / rust (pull_request) Successful in 10m21s
ae13b29abd
Heroic 2.21 added a fullscreen gamepad UI — the Big Picture of that launcher,
and the tile you actually want on a stream. Nothing could publish it: a plugin
names a launcher UI and the host builds the line, and the only Heroic value the
host knew was `heroic`, which opens the ordinary window.

`launcher_ui` now also takes `heroic-console` on Linux, resolving to the same
prefix `heroic` does — native binary if on PATH, else the Flatpak — plus
`--console --fullscreen`. Both flags, because Heroic reads them separately:
`--console` only routes the UI to that front end (`isCLIConsoleMode`) and
`--fullscreen` is what fills the screen (`isCLIFullscreen`). No URI can do it —
`heroic://` speaks `ping` and `launch` and nothing else — which is the same
reason Playnite's fullscreen tile spawns its exe directly rather than going
through the registered protocol handler. An older Heroic ignores the unknown
`--console` and still honours `--fullscreen`, so the tile degrades to a
fullscreen desktop UI rather than to nothing.

That makes the value a launcher UI rather than a launcher. It already was one on
Windows, where `playnite` has always meant `Playnite.FullscreenApp.exe`; the doc
comment claiming otherwise is corrected. A `heroic_ui` kind mirroring `steam_ui`
would have been tidier and was rejected on the compatibility asymmetry: an
unknown KIND degrades to an unlaunchable tile, but an unknown VALUE is a hard
400 that refuses the whole reconcile, so either shape has to be gated on
`minHost` in the plugin index and the value is much the smaller change. A plugin
publishing `heroic-console` must set `minHost` to the release carrying this.

Second fix, from the same reading: `resolvable_launcher_ui` special-cased only
Playnite-on-Windows and answered `true` for everything else, so a Heroic tile
survived `sanitize_launcher_entries` even when `heroic_launch_prefix()` resolves
to nothing. Keeping `~/.config/heroic` after uninstalling Heroic is enough to
reach that — the plugin's `detect` only looks for the directory — and the
operator got a launcher tile that does nothing. Both Heroic values now probe the
prefix, exactly as Playnite probes for its exe.
enricobuehler merged commit 2bb6af3b92 into main 2026-08-26 17:26:22 +00:00
enricobuehler deleted branch worktree-heroic-console-and-runners 2026-08-26 17:26:29 +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#402