Merge pull request 'A tile that opens Lutris itself' (#1) from feat/launcher-tile into main
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
@@ -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) => {
|
||||
|
||||
+28
-1
@@ -5,7 +5,7 @@ import { describe, expect, test } from "bun:test";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { artFile } from "../src/plugin.js";
|
||||
import { artFile, launcherEntries } from "../src/plugin.js";
|
||||
|
||||
describe("cover-art lookup", () => {
|
||||
test("refuses a slug that could escape the art roots", () => {
|
||||
@@ -62,3 +62,30 @@ describe("cover-art lookup", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("the launcher tile", () => {
|
||||
test("is published by default, valued by store id and never by a command", () => {
|
||||
// Design D4 + D1: the plugin names a launcher, the host builds the command. A `command`
|
||||
// kind here would be refused outright — the 2026-08-05 review made that operator-only —
|
||||
// so `launcher_ui` valued "lutris" is the only shape that can work, and the host's
|
||||
// `valid_launcher_ui` allow-list is what validates it inbound.
|
||||
const tiles = launcherEntries({});
|
||||
expect(tiles).toHaveLength(1);
|
||||
expect(tiles[0]).toMatchObject({
|
||||
role: "launcher",
|
||||
launch: { kind: "launcher_ui", value: "lutris" },
|
||||
});
|
||||
// `lutris:launcher` — real Lutris ids are numeric, so this cannot collide with a game.
|
||||
expect(tiles[0]?.external_id).toBe("launcher");
|
||||
expect(tiles[0]?.title).toBe("Lutris");
|
||||
// Deliberately art-less: a square app icon cover-cropped into a 2:3 tile looks broken, and
|
||||
// the clients render an art-less launcher entry as "opens Lutris" on purpose.
|
||||
expect(tiles[0]?.art).toBeUndefined();
|
||||
});
|
||||
|
||||
test("an operator can turn it off", () => {
|
||||
expect(launcherEntries({ launcher: false })).toEqual([]);
|
||||
// Absent means default-on, which is what an untouched config decodes to.
|
||||
expect(launcherEntries({ launcher: true })).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user