forked from unom/punktfunk
feat(host/library): launcher_ui can open Heroic's console mode
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.
This commit is contained in:
@@ -420,6 +420,31 @@ feature surfaces ("--browse needs the console UI", exit non-zero).
|
||||
identity-selection gate runs where the origin-isolation gate already did. Both have the same
|
||||
property: a failure mode only a browser would catch.
|
||||
|
||||
### `launcher_ui` grows a second Heroic value, for its console mode
|
||||
|
||||
**Plugin-facing.** `launcher_ui` accepts **`heroic-console`** on Linux, alongside `heroic` and
|
||||
`lutris`. It resolves to the same prefix `heroic` does — the native binary if on `PATH`, else the
|
||||
Flatpak — plus `--console --fullscreen`.
|
||||
|
||||
Heroic 2.21 added a fullscreen gamepad UI, and it takes **two** flags: `--console` only routes the
|
||||
UI to that front end (`isCLIConsoleMode`), and `--fullscreen` is what fills the screen
|
||||
(`isCLIFullscreen`). Neither is reachable by URI — `heroic://` speaks only `ping` and `launch` — so
|
||||
this is the same shape as Playnite's fullscreen tile on Windows, where the registered protocol
|
||||
handler can only open the desktop app.
|
||||
|
||||
That makes `launcher_ui`'s value a launcher **UI** rather than a launcher, which it already was on
|
||||
Windows (`playnite` has always meant `Playnite.FullscreenApp.exe`). A `heroic_ui` kind mirroring
|
||||
`steam_ui` would have been tidier and was rejected: an unknown *kind* degrades to an unlaunchable
|
||||
tile on an N-1 host, 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 the smaller change.
|
||||
**A plugin publishing `heroic-console` must set `minHost` to this release.**
|
||||
|
||||
Also here: `resolvable_launcher_ui` now probes `heroic_launch_prefix()` for both Heroic values, the
|
||||
way it already did for Playnite. Both tiles are dropped from a reconcile on a box where Heroic
|
||||
cannot be resolved, instead of being published as tiles that do nothing — reachable by keeping
|
||||
`~/.config/heroic` after uninstalling Heroic, since the plugin's `detect` only looks for that
|
||||
directory.
|
||||
|
||||
---
|
||||
|
||||
## v0.31.3
|
||||
|
||||
@@ -165,6 +165,21 @@ fn command_for(spec: &LaunchSpec) -> Option<String> {
|
||||
// The same resolution the `heroic` game launches use (native binary, else Flatpak), just
|
||||
// without `--no-gui` and without a URI: that opens Heroic's window, which IS the tile.
|
||||
"heroic" => heroic_launch_prefix(),
|
||||
// Heroic's console mode — its couch front end, the Big Picture of this launcher.
|
||||
//
|
||||
// It takes TWO flags, which is not obvious and is why this is the host's business and
|
||||
// not a plugin's: `--console` only routes the UI to that front end, and `--fullscreen`
|
||||
// is what actually fills the screen (Heroic reads them separately —
|
||||
// `isCLIConsoleMode` / `isCLIFullscreen`). Neither is a URI: `heroic://` speaks only
|
||||
// `ping` and `launch`, so a protocol hand-off cannot reach console mode at all — the
|
||||
// same reason Playnite's fullscreen tile spawns its exe directly.
|
||||
//
|
||||
// Console mode arrived in Heroic 2.21.0. An older Heroic ignores the unknown
|
||||
// `--console` and honours `--fullscreen`, so the tile degrades to a fullscreen desktop
|
||||
// UI rather than to nothing.
|
||||
"heroic-console" => {
|
||||
heroic_launch_prefix().map(|p| format!("{p} --console --fullscreen"))
|
||||
}
|
||||
// Bare `lutris` opens the Lutris window; with a `lutris:rungameid/…` URI it launches a
|
||||
// game instead (the `lutris_id` kind above).
|
||||
"lutris" => Some("lutris".into()),
|
||||
@@ -532,9 +547,16 @@ pub(crate) fn valid_playnite_id(value: &str) -> bool {
|
||||
|
||||
/// The launcher UIs **this host** can open, as `launcher_ui` values (D4).
|
||||
///
|
||||
/// One kind for every launcher but Steam, rather than one kind each: they all have exactly a single
|
||||
/// UI to open, so the value is just which launcher. Steam keeps its own [`valid_steam_ui`] kind
|
||||
/// because it has two (Big Picture and the desktop client), which is a genuinely different choice.
|
||||
/// One kind for every launcher but Steam, rather than one kind each. A value names a launcher *UI*,
|
||||
/// which for most of them is the same thing as naming the launcher — and where it is not, the value
|
||||
/// says which one: `heroic` opens Heroic's window, `heroic-console` its couch front end, and on
|
||||
/// Windows `playnite` has always meant Playnite's **Fullscreen** app rather than its desktop one.
|
||||
/// Steam keeps its own [`valid_steam_ui`] kind because it was the first launcher with two UIs worth
|
||||
/// opening, and because both of its values are the same length of word — splitting Heroic's two out
|
||||
/// into a `heroic_ui` kind today would buy symmetry and cost every N-1 host: an unknown *kind*
|
||||
/// degrades to an unlaunchable tile, but an unknown *value* is a hard 400 that refuses the whole
|
||||
/// reconcile, so a new value is the shape that has to be gated on `minHost` in the plugin index
|
||||
/// either way.
|
||||
///
|
||||
/// Platform-gated, because a value naming a launcher this OS cannot run is not a tile that merely
|
||||
/// looks odd — it is one that fails at launch. Validated inbound too, so a plugin gets a 400 it can
|
||||
@@ -548,7 +570,7 @@ pub(crate) fn valid_playnite_id(value: &str) -> bool {
|
||||
fn launcher_ui_stores() -> &'static [&'static str] {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
&["heroic", "lutris"]
|
||||
&["heroic", "heroic-console", "lutris"]
|
||||
}
|
||||
// Playnite's activation is verified (2026-08-06, on the .173 box); Epic, GOG Galaxy and the
|
||||
// Xbox app are still unwired — each needs its own verified activation, and an unverified guess
|
||||
@@ -594,6 +616,14 @@ pub(crate) fn resolvable_launcher_ui(value: &str) -> bool {
|
||||
if value == "playnite" {
|
||||
return playnite_fullscreen_exe().is_some();
|
||||
}
|
||||
// Same question for both Heroic tiles, and the same answer: they resolve to whatever
|
||||
// `heroic_launch_prefix` finds, so when that finds nothing the tile is dead and must not be
|
||||
// published. Keeping `~/.config/heroic` around after uninstalling Heroic is enough to reach
|
||||
// this — the plugin's `detect` only looks for that directory.
|
||||
#[cfg(target_os = "linux")]
|
||||
if matches!(value, "heroic" | "heroic-console") {
|
||||
return heroic_launch_prefix().is_some();
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@@ -1123,10 +1153,21 @@ mod tests {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
assert!(known_launcher_ui("heroic"));
|
||||
assert!(known_launcher_ui("heroic-console"));
|
||||
assert!(known_launcher_ui("lutris"));
|
||||
// Not wired on this OS — outside the vocabulary, so it is refused inbound rather than
|
||||
// becoming a tile that does nothing.
|
||||
assert!(!known_launcher_ui("gog"));
|
||||
// Both Heroic tiles resolve through the same probe, so a box without Heroic drops both
|
||||
// rather than publishing one dead tile beside the other.
|
||||
assert_eq!(
|
||||
resolvable_launcher_ui("heroic"),
|
||||
heroic_launch_prefix().is_some()
|
||||
);
|
||||
assert_eq!(
|
||||
resolvable_launcher_ui("heroic-console"),
|
||||
heroic_launch_prefix().is_some()
|
||||
);
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
@@ -1253,6 +1294,21 @@ mod tests {
|
||||
if let Some(cmd) = ui("heroic") {
|
||||
assert!(!cmd.contains("--no-gui"), "the GUI is the point: {cmd:?}");
|
||||
assert!(!cmd.contains("heroic://"), "no game URI: {cmd:?}");
|
||||
assert!(
|
||||
!cmd.contains("--console"),
|
||||
"that is the other tile: {cmd:?}"
|
||||
);
|
||||
}
|
||||
// Console mode needs BOTH flags — `--console` alone routes the UI without filling the
|
||||
// screen, which from a couch is the bug this tile exists to avoid. Same prefix as the
|
||||
// window tile, so it is `None` on the same boxes.
|
||||
assert_eq!(ui("heroic-console").is_some(), ui("heroic").is_some());
|
||||
if let Some(cmd) = ui("heroic-console") {
|
||||
assert!(cmd.contains("--console"), "{cmd:?}");
|
||||
assert!(cmd.contains("--fullscreen"), "{cmd:?}");
|
||||
assert!(!cmd.contains("--no-gui"), "the GUI is the point: {cmd:?}");
|
||||
// Gamescope spawns by `split_whitespace`, so every token has to stand alone.
|
||||
assert!(cmd.split_whitespace().any(|t| t == "--console"), "{cmd:?}");
|
||||
}
|
||||
assert_eq!(ui("nonsense"), None);
|
||||
assert_eq!(ui(""), None);
|
||||
|
||||
@@ -113,9 +113,11 @@ A [plugin](/docs/plugins) can own a slice of the library and keep it in sync —
|
||||
Manager and Playnite plugins get your collection into the grid, box art and all.
|
||||
|
||||
A library plugin can also publish a **launcher tile** — an entry that opens Steam Big Picture,
|
||||
Heroic, Lutris or Playnite itself rather than a game, so you can install or fix something from the
|
||||
couch. Clients group those into their own row above your titles, each drawing its launcher's logo.
|
||||
A launcher tile you don't want is a switch in that plugin's settings.
|
||||
Heroic's console mode, Lutris or Playnite Fullscreen itself rather than a game, so you can install
|
||||
or fix something from the couch. Where a launcher has both a couch UI and an ordinary window, they
|
||||
are separate tiles: Steam Big Picture beside the Steam client, Heroic Console Mode beside the Heroic
|
||||
window. Clients group them all into their own row above your titles, each drawing its launcher's
|
||||
logo. A launcher tile you don't want is a switch in that plugin's settings.
|
||||
|
||||
Entries a plugin owns are read-only to you. The host refuses a hand edit or delete of one, because
|
||||
the next sync would overwrite it anyway — change the title at its source and let the plugin sync
|
||||
|
||||
@@ -28,7 +28,7 @@ export type Artwork = typeof Artwork.Type;
|
||||
* | `command` | a shell command (operator-trust tier) | both |
|
||||
* | `steam_appid` | digits — an appid, or a 64-bit non-Steam-shortcut game id | both |
|
||||
* | `steam_ui` | `bigpicture` \| `desktop` — opens the Steam client itself | both |
|
||||
* | `launcher_ui` | a store id (`heroic`, `lutris`) — opens that launcher's own UI | linux |
|
||||
* | `launcher_ui` | which launcher UI to open: `heroic` \| `heroic-console` \| `lutris` | linux |
|
||||
* | `lutris_id` | digits — a pga.db game id | linux |
|
||||
* | `heroic` | `<runner>:<appName>`, runner ∈ legendary/gog/nile | linux |
|
||||
* | `epic` | `<namespace>:<catalogItemId>:<appName>` or a bare appName | windows |
|
||||
|
||||
Reference in New Issue
Block a user