feat: a tile that opens Lutris itself
CI / build (pull_request) Successful in 17s
CI / publish (pull_request) Skipped

`defineLibraryPlugin` has taken a `launchers()` hook since kit 0.3.0 — entries that
open the LAUNCHER rather than a title (design D4) — and nothing implemented it. The
host has resolved `launcher_ui` valued `"lutris"` since the same release. So the
whole path existed end to end with no producer at either end.

One entry, config-toggled and on by default: it is the one tile that lets someone
install or fix a game from the couch, and it costs a single card.

`launcher_ui` is valued by STORE ID, never a command: the host turns "lutris" into
the command that opens the window (D1). That is also the only shape available to a
plugin at all — the 2026-08-05 review made `launch.kind = "command"` operator-only,
so a plugin publishing one has its entire reconcile refused.

Deliberately art-less. Lutris ships a square app icon and every client cover-crops a
2:3 tile, so an icon would arrive as a cropped strip. The clients render an art-less
launcher entry on purpose — accent face, the launcher named — which reads as "opens
Lutris" rather than as a game whose poster failed to load.

`launcherEntries` is exported for the same reason `artFile` is: so the shape is
testable without standing up the whole plugin.

Gates: tsc --noEmit clean, 5 tests pass (2 new), biome clean.
Needs a host carrying the M2 wire (`role` + `launcher_ui`), which is on main.
This commit is contained in:
2026-08-06 15:17:12 +02:00
parent 850154df5e
commit bedf43214c
2 changed files with 74 additions and 1 deletions
+46
View File
@@ -38,6 +38,20 @@ const LutrisConfig = Schema.Struct({
"Absolute path to pga.db. Leave empty to find it automatically.",
}),
),
/**
* Publish a tile that opens the Lutris window itself (design D4) alongside the games. On by
* default: it is the one entry that lets someone install or fix a game from the couch, and it
* costs one tile.
*/
launcher: Schema.Boolean.annotate({
title: "Show a Lutris tile",
description:
"Add a tile that opens Lutris itself, so you can manage games from a client.",
}).pipe(
Schema.withDecodingDefaultKey(Effect.succeed(true), {
encodingStrategy: "omit",
}),
),
});
/** Candidate `pga.db` locations: XDG data dir, the classic path, Flatpak. */
@@ -99,6 +113,36 @@ interface GameRow {
directory: string | null;
}
/**
* The tile that opens Lutris itself (design D4).
*
* `launcher_ui` is valued by *store id*, never a command: the host owns turning "lutris" into
* the command that opens the window (D1), which is also why a plugin may publish this at all —
* the 2026-08-05 review made `launch.kind = "command"` operator-only.
*
* No art on purpose. Lutris ships a square app icon, and every client cover-crops a 2:3 tile,
* so an icon would arrive as a cropped strip. The clients render an art-less launcher entry
* deliberately — accent face, the launcher named — which reads as "opens Lutris" rather than as
* a game whose poster failed.
*
* A standalone exported function so it is testable without standing up the whole plugin —
* the same reason `artFile` is exported.
*/
export const launcherEntries = (cfg: {
launcher?: boolean;
}): ProviderEntry[] =>
cfg.launcher === false
? []
: [
{
external_id: "launcher",
title: "Lutris",
role: "launcher",
launch: { kind: "launcher_ui", value: "lutris" },
platform: "PC",
},
];
export const plugin = defineLibraryPlugin({
// One string: plugin id, provider id, store claim, and the id of the built-in scanner this
// replaces. It matches the package name and the repo name too, so there is no mapping to
@@ -160,6 +204,8 @@ export const plugin = defineLibraryPlugin({
});
}),
launchers: launcherEntries,
// Re-scan when Lutris writes: installing a game touches the database, and downloading art
// touches the cover directories.
watchDirs: (cfg) => {