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:
--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).
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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_uitakesheroic-consoleHeroic 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-consoleresolves to the same prefixheroicdoes (native binary if onPATH, else the Flatpak) plus--console --fullscreen. Both flags, because Heroic reads them separately:--consoleonly routes the UI to that front end;--fullscreenis what fills the screen. No URI can reach it —heroic://handles exactlypingandlaunch— 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--consoleand still honours--fullscreen, so the tile degrades to a fullscreen desktop UI rather than to nothing.Why a value and not a
heroic_uikindA
heroic_uikind mirroringsteam_uiwould be tidier, and I rejected it on a compatibility asymmetry that runs the other way from what you'd guess:validate_provider_payload, which refuses the entire reconcile.So either shape has to be gated on
minHostin the plugin index regardless, and the value is much the smaller change. The doc comment onlauncher_ui_storesis corrected to say the value names a launcher UI, not a launcher — which it already did on Windows, whereplaynitehas always meantPlaynite.FullscreenApp.exerather than the desktop app.Second fix: a Heroic tile that could never launch was still published
resolvable_launcher_uispecial-cased only Playnite-on-Windows and returnedtruefor everything else, so alauncher_ui: "heroic"entry survivedsanitize_launcher_entrieseven whenheroic_launch_prefix()resolves to nothing — noheroicon the host service'sPATHand no~/.var/app/com.heroicgameslauncher.hglunder its$HOME.Keeping
~/.config/heroicafter uninstalling Heroic is enough to reach it, because the plugin'sdetectonly 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 forlauncher_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
heroiclaunch kind, though it is the one closed-vocabulary kind without any (steam_ui,launcher_ui,playnite,xboxall have it). I wrote it, then reverted it: it converts a valueheroic_commandwould 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 lessonsanitize_launcher_entriesexists for. Worth revisiting only alongside a general "drop the entry, don't 400 the batch" pass.Verification
rustfmt --check --edition 2024clean 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 — soci / rusthere is the first real compile of this change. The new assertions live inlauncher_ui_accepts_only_launchers_this_host_can_openandlauncher_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).launcher_uican open Heroic's console modeenricobuehler referenced this pull request from unom/punktfunk-plugin-index2026-08-26 18:18:37 +00:00