@vestra-player/sdk 1.5.29 → 1.5.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.31
4
+
5
+ **Who must change: nobody.** No API changes, `apiVersion` stays `vestra/1`, no player version needed. Only documentation, its checks and the generated `llms-full.txt` change.
6
+
7
+ - **The Commands, Playback and Services categories reach the full reference standard** (they are now in `STRICT_CATEGORIES`, so CI keeps them complete): `Commands`, `Command`, `Host` and their option types; `Player`, `Media`, `Scrub`, `Stream` and the playback state and stream types; `Storage`, `JsonStore`, `SessionStorage`, `Secrets`, `Clipboard`, `System`, `Tools`, `Power`. Every method has an example (what it does and how it works), `@capability`, `@since` and a `@throws` per error code it can reject with, checked against the SDK and the player's source; every type has `@category`, so the API reference lists them under their category instead of "Other". Methods with an explained example: 75 of 191 → 121.
8
+ - Error codes the reference now names, from the player's source: a control while nothing is open is `invalid` (`close` is not: closing with nothing open just succeeds); `setRate` takes 0.05–10 (the player's own keys stay within 0.25–4); `setOutput`'s volume outside 0–200 is `invalid`; `Stream.stop` rejects only when closing the source fails, and releases the stream either way.
9
+ - **Corrections from the review against the player's source.** `stale` means any change of the source since the state a call is based on: another file or a close, but also any pause, seek or speed change by the user or another plugin. A second sequence provider is refused with `conflict` (`sequence-taken`) without failing activation, and the built-in playlist registers first. `VESTRA_FFMPEG` / `VESTRA_FFPROBE` replace the `PATH` search instead of coming before it. More `@throws`: `capability` for `Secrets` and `Clipboard`; for a generated stream `busy` and `unsupported` for `seek` and `setRate`, `busy` for `setPaused` and `unsupported` for `restart`; `replaced`, `busy` and the resolver failures (`backend`, `unsupported`, `unavailable`) for `open`; `capability` for `Command.ready`; `aborted` for `openStream`; `stale` and `unsupported` for `Scrub.seek`; `disabled` and `unsupported` (players before 0.5.5) for session storage; `failed` and `timeout` (macOS) for `System`. *Store a plugin's data* no longer promises that a quit right after `save` loses nothing (a stopping plugin gets about 300 ms).
10
+ - New how-to guides: *Control playback* (reading and following the state, `stale` and `ifCurrent`, `Media`, core actions, Next and Previous) and *Store a plugin's data* (settings files, session values, secrets, your own files).
11
+ - `doc-lint`: `--list explained` lists the methods without an explained example; its summary line no longer says "Not enforced yet" (every example has been enforced since 1.5.30).
12
+ - Comments and docs name the player's host files at their new places (`src/host/src/packages/manifest.rs`, `ui/ui_image.rs`, `ui/ui_values.rs`, `playback/effects.rs`, `ui/theme.rs`).
13
+
14
+ ## 1.5.30
15
+
16
+ **Who must change: nobody.** No API changes, `apiVersion` stays `vestra/1`, no player version needed. Only documentation, its checks and the generated `llms-full.txt` change.
17
+
18
+ - **Every example explains what it does and how it works.** The standard, in *Maintaining* ("Examples in reference comments"): an `@example` has a task title, a lead saying what it does and how it works (checked against the SDK and the player's source), code that compiles on its own with comments on the key lines, and error handling where the user can make a call fail. All 159 examples of the API now meet it (none did before this release), including the 65 of the UI category, the test helpers (`TestHost`, `startPlugin`, `HostError`), the limits and tables (`COMMAND_LIMITS`, `MENU_IDS`, `BUILTIN_*`, `BAR_BUTTON_ICONS`, `CAPABILITY_INFO`, `BINDING_INPUTS`, `OSD_LIMITS`, `SESSION_STORAGE_LIMITS`, `SECRET_LIMITS`, `UI_VALUE_LIMITS`, `THEME_PALETTES`) and the 1.5.29 bar buttons (`CommandButton`, `TestHost.barButtons`). Every example now type checks (61 did not). Examples no longer compose shown sentences in TypeScript: OSD and dialog texts are templates with arguments.
19
+ - **Corrections found on the way.** `Subtitles.setStyle`, `SubtitlesState.style` and `SubtitleStyle`: when several plugins set a style layer, the effective one is the layer of the plugin whose manifest id sorts last, not the latest call. `COMMAND_LIMITS`: `add` never throws `limit`; the player refuses the registration (activation fails, or `Scope.onError`). `startPlugin`'s restart example gives both runs the same `dataDirectory` instead of copying the data before `stop()` (which could miss a pending store write). `TestHost.settle` says the debounced store write is not waited for. `TestHost.dialogAnswers` is one queue for every kind of dialog. `TestHost.openElsewhere`: calls that return nothing (`TimelineLayer.set` with `ifCurrent`) drop the write silently instead of rejecting. Resolver examples (`I18n.error`, `FileResolverOptions`, `TestHost.otherResolvers`) use a made-up `.ch8` format instead of `.nes`, which the store plugin VestraNES owns.
20
+ - **`llms-full.txt` carries the examples and `@remarks`** of every declaration and member (hidden setup lines dropped, `{@includeCode}` inlined), so the plugin workshop's model sees them: 536 KB → 693 KB (about 137k → 177k tokens). `llms.txt` wording trimmed to stay under its 9 KB limit (9,134 bytes).
21
+ - **Hidden setup lines**: in an `@example` code block, lines above `// ---cut---` are type checked but not shown on the site or in `llms-full.txt`.
22
+ - **Checks.** `docs:check` fails on any block that does not compile (it only warned for most examples; `--lenient` keeps the old behaviour for work in progress). `doc-lint` gains `example-lead` (task title, lead, comments, one `// ---cut---` at most; `See` markers for trivial siblings, whose link must resolve, at most 20 % of a declaration's methods) and `stale-refs` (removed or handed-over names such as `vestra.nes`, `.nes` resolvers), enforced for every example and Markdown code block, and prints per category the share of methods with an explained example. `EXAMPLE_BASELINE` keeps known violations per problem when a rule is tightened; it is empty, and `test/doc-baseline.test.ts` keeps it from growing.
23
+ - The parts of `examples/bookmark`, `examples/markdown` and `examples/panel-menu` that the reference includes gained comments on their key lines.
24
+ - Test only: the storage test in `test/round2.test.ts` waits for the debounced file to appear and for `flush()` instead of a fixed 60 ms, and stops its plugins before removing their data directory, so it no longer fails now and then with `ENOENT` on a loaded machine.
25
+
3
26
  ## 1.5.29
4
27
 
5
28
  **Who must change: nobody.** Additions only, `apiVersion` stays `vestra/1`. A plugin that does not use `button` is unaffected. A plugin that uses it on a player before 0.5.10 keeps working without a button: the player builds the `plugin` object with its own SDK copy, which does not send the field, so the command and its menu entry register as before (declare `"minPlayerVersion": "0.5.10"` when the button matters). A plugin that mounts its own bottom bar should place `BarButtons` (below); until it does, `vestra-plugin check` warns and the player logs the buttons it hides.
@@ -49,13 +49,23 @@ export interface CapabilityInfo {
49
49
  * `plugin.i18n`, reading `plugin.window.state`, `plugin.appearance.theme`
50
50
  * and `plugin.packages.list`).
51
51
  *
52
- * @example
52
+ * @example Check in a test that the manifest declares every required capability
53
+ * The test fails, naming the missing capability, when the manifest declares
54
+ * a capability without one its members also need. It reads the plugin's own
55
+ * `manifest.json` and looks each declared name up in `CAPABILITY_INFO`,
56
+ * whose `requires` lists the capabilities that must be declared with it;
57
+ * at run time the player would refuse such a call with code `capability`.
53
58
  * ```ts
59
+ * import assert from "node:assert/strict";
60
+ * import { readFile } from "node:fs/promises";
54
61
  * import { CAPABILITY_INFO } from "@vestra-player/sdk/testing";
55
62
  *
56
- * const manifest = JSON.parse(await readFile("manifest.json", "utf8"));
63
+ * const manifest: { capabilities: string[] } = JSON.parse(
64
+ * await readFile(new URL("../manifest.json", import.meta.url), "utf8"),
65
+ * );
57
66
  * for (const capability of manifest.capabilities)
58
- * for (const needed of CAPABILITY_INFO[capability].requires)
67
+ * // An unknown name has no entry: `vestra-plugin check` reports it.
68
+ * for (const needed of CAPABILITY_INFO[capability]?.requires ?? [])
59
69
  * assert.ok(manifest.capabilities.includes(needed), needed);
60
70
  * ```
61
71
  *
@@ -13,13 +13,23 @@ import { capabilityInfo } from "./capability-rules.js";
13
13
  * `plugin.i18n`, reading `plugin.window.state`, `plugin.appearance.theme`
14
14
  * and `plugin.packages.list`).
15
15
  *
16
- * @example
16
+ * @example Check in a test that the manifest declares every required capability
17
+ * The test fails, naming the missing capability, when the manifest declares
18
+ * a capability without one its members also need. It reads the plugin's own
19
+ * `manifest.json` and looks each declared name up in `CAPABILITY_INFO`,
20
+ * whose `requires` lists the capabilities that must be declared with it;
21
+ * at run time the player would refuse such a call with code `capability`.
17
22
  * ```ts
23
+ * import assert from "node:assert/strict";
24
+ * import { readFile } from "node:fs/promises";
18
25
  * import { CAPABILITY_INFO } from "@vestra-player/sdk/testing";
19
26
  *
20
- * const manifest = JSON.parse(await readFile("manifest.json", "utf8"));
27
+ * const manifest: { capabilities: string[] } = JSON.parse(
28
+ * await readFile(new URL("../manifest.json", import.meta.url), "utf8"),
29
+ * );
21
30
  * for (const capability of manifest.capabilities)
22
- * for (const needed of CAPABILITY_INFO[capability].requires)
31
+ * // An unknown name has no entry: `vestra-plugin check` reports it.
32
+ * for (const needed of CAPABILITY_INFO[capability]?.requires ?? [])
23
33
  * assert.ok(manifest.capabilities.includes(needed), needed);
24
34
  * ```
25
35
  *
@@ -4,18 +4,29 @@ import type { ListJson, TextJson } from "./i18n.ts";
4
4
  /**
5
5
  * The player's command registry caps, the same values as the player's own
6
6
  * table (the player's tests check that they agree). They are runaway guards,
7
- * not product limits: a command added past them fails with code `limit`
8
- * (its `ready` rejects; during activation, activation fails), commands
7
+ * not product limits: a command added past them is refused by the player
8
+ * with code `limit` (its `ready` rejects; during activation, activation
9
+ * fails; later the failure goes to `Scope.onError`), commands
9
10
  * already registered are unaffected, and disposing a command frees its slot
10
11
  * at once. {@link TestHost} enforces the same caps.
11
12
  *
12
13
  * For menus built from media content (tracks, chapters, presets), set your
13
14
  * own sensible display cap well below `perPlugin`.
14
15
  *
15
- * @example
16
+ * @example Cap a chapter menu well below the command limit
17
+ * The Chapters submenu lists at most 50 chapters, however many the file
18
+ * has. Every menu item is a command, and the player counts them per plugin.
19
+ * `add` itself returns at once and never throws `limit`: past `perPlugin`
20
+ * the player refuses the registration, which fails the activation or, later,
21
+ * reaches `Scope.onError`. A try/catch around `add` cannot catch that, so a
22
+ * menu built from media content keeps its own cap and leaves room for the
23
+ * plugin's fixed commands.
16
24
  * ```ts
25
+ * declare const chapters: { title: string; time: number }[];
26
+ * // ---cut---
17
27
  * import { COMMAND_LIMITS } from "@vestra-player/sdk/testing";
18
28
  *
29
+ * // 20 slots stay free for the plugin's own commands.
19
30
  * const shown = chapters.slice(0, Math.min(50, COMMAND_LIMITS.perPlugin - 20));
20
31
  * ```
21
32
  */
@@ -36,11 +47,21 @@ export declare const COMMAND_LIMITS: {
36
47
  * `@video` as a menu of its own. The player's tests check that this list
37
48
  * agrees with the player's.
38
49
  *
39
- * @example
50
+ * @example Put a command into the built-in Video menu
51
+ * "Blur" appears in the player's Video menu in every interface language.
52
+ * The player replaces a leading `@video` with that menu's key ("Video")
53
+ * before it merges paths, so the item joins the built-in menu; an id that
54
+ * is not in `MENU_IDS` makes `add` fail with `invalid`, which a test can
55
+ * rule out ahead of time.
40
56
  * ```ts
57
+ * import type { Plugin } from "@vestra-player/sdk";
58
+ * declare const plugin: Plugin;
59
+ * // ---cut---
60
+ * import assert from "node:assert/strict";
41
61
  * import { MENU_IDS } from "@vestra-player/sdk/testing";
42
62
  *
43
- * assert.ok(MENU_IDS.includes("video"));
63
+ * plugin.commands.add({ id: "blur", path: ["@video", "Blur"], run() {} });
64
+ * assert.ok(MENU_IDS.includes("video")); // a menu id, not a title
44
65
  * ```
45
66
  */
46
67
  export declare const MENU_IDS: readonly ["open", "playback", "subtitles", "video", "audio", "screen", "settings"];
@@ -50,12 +71,16 @@ export declare const MENU_IDS: readonly ["open", "playback", "subtitles", "video
50
71
  * in your own Slint with `Icon { name: "store"; }`). The player's tests check
51
72
  * that this list is its icon set.
52
73
  *
53
- * @example
74
+ * @example Check in a test that the button's icon exists
75
+ * The test fails before release if the plugin's bar button names an icon
76
+ * the player does not have. `commands.add` would refuse an unknown name
77
+ * with `invalid`; the player's tests keep `BAR_BUTTON_ICONS` equal to its
78
+ * icon set, so checking against it is the same check without a player.
54
79
  * ```ts
55
80
  * import assert from "node:assert/strict";
56
81
  * import { BAR_BUTTON_ICONS } from "@vestra-player/sdk/testing";
57
82
  *
58
- * assert.ok(BAR_BUTTON_ICONS.includes("store"));
83
+ * assert.ok(BAR_BUTTON_ICONS.includes("store")); // the icon main.ts uses
59
84
  * ```
60
85
  *
61
86
  * @since 1.5.29
@@ -108,13 +133,23 @@ export interface BuiltinSubmenu {
108
133
  * listed. The player's tests check that this list agrees with what its
109
134
  * built-in plugins declare.
110
135
  *
111
- * @example
136
+ * @example Add an item to the built-in Snapshot submenu
137
+ * "Copy Snapshot" appears in Video ▸ Snapshot next to the built-in capture
138
+ * plugin's items. Paths merge by their English keys, so a command whose
139
+ * path continues with the submenu's keys (`["Video", "Snapshot"]`, the menu
140
+ * written as `@video`) lands in the same submenu whatever the interface
141
+ * language.
112
142
  * ```ts
143
+ * import type { Plugin } from "@vestra-player/sdk";
144
+ * declare const plugin: Plugin;
145
+ * declare function copySnapshot(): Promise<void>;
146
+ * // ---cut---
113
147
  * import { BUILTIN_SUBMENUS } from "@vestra-player/sdk/testing";
114
148
  *
115
149
  * const snapshot = BUILTIN_SUBMENUS.find((menu) => menu.path.at(-1) === "Snapshot")!;
116
150
  * plugin.commands.add({
117
151
  * id: "snapshot-to-clipboard",
152
+ * // ["@video", "Snapshot", "Copy Snapshot"]; @video stands for "Video".
118
153
  * path: ["@video", ...snapshot.path.slice(1), "Copy Snapshot"],
119
154
  * run: copySnapshot,
120
155
  * });
@@ -157,7 +192,12 @@ export interface BuiltinCommand {
157
192
  * placeholders (`No Audio Tracks`) are. The player's tests check that this
158
193
  * list agrees with what the player and its built-in plugins register.
159
194
  *
160
- * @example
195
+ * @example Check in a test that a default key is still free
196
+ * The test fails if the player or a built-in plugin already binds Ctrl+K
197
+ * globally, before users ever see a conflict. It collects the default
198
+ * bindings of every built-in command that is not limited to its panel's
199
+ * focus (`focus: null`) and looks for the key the plugin wants; bindings
200
+ * are canonical, so compare them as the player writes them.
161
201
  * ```ts
162
202
  * import assert from "node:assert/strict";
163
203
  * import { BUILTIN_COMMANDS } from "@vestra-player/sdk/testing";
package/dist/commands.js CHANGED
@@ -1,18 +1,29 @@
1
1
  /**
2
2
  * The player's command registry caps, the same values as the player's own
3
3
  * table (the player's tests check that they agree). They are runaway guards,
4
- * not product limits: a command added past them fails with code `limit`
5
- * (its `ready` rejects; during activation, activation fails), commands
4
+ * not product limits: a command added past them is refused by the player
5
+ * with code `limit` (its `ready` rejects; during activation, activation
6
+ * fails; later the failure goes to `Scope.onError`), commands
6
7
  * already registered are unaffected, and disposing a command frees its slot
7
8
  * at once. {@link TestHost} enforces the same caps.
8
9
  *
9
10
  * For menus built from media content (tracks, chapters, presets), set your
10
11
  * own sensible display cap well below `perPlugin`.
11
12
  *
12
- * @example
13
+ * @example Cap a chapter menu well below the command limit
14
+ * The Chapters submenu lists at most 50 chapters, however many the file
15
+ * has. Every menu item is a command, and the player counts them per plugin.
16
+ * `add` itself returns at once and never throws `limit`: past `perPlugin`
17
+ * the player refuses the registration, which fails the activation or, later,
18
+ * reaches `Scope.onError`. A try/catch around `add` cannot catch that, so a
19
+ * menu built from media content keeps its own cap and leaves room for the
20
+ * plugin's fixed commands.
13
21
  * ```ts
22
+ * declare const chapters: { title: string; time: number }[];
23
+ * // ---cut---
14
24
  * import { COMMAND_LIMITS } from "@vestra-player/sdk/testing";
15
25
  *
26
+ * // 20 slots stay free for the plugin's own commands.
16
27
  * const shown = chapters.slice(0, Math.min(50, COMMAND_LIMITS.perPlugin - 20));
17
28
  * ```
18
29
  */
@@ -33,11 +44,21 @@ export const COMMAND_LIMITS = {
33
44
  * `@video` as a menu of its own. The player's tests check that this list
34
45
  * agrees with the player's.
35
46
  *
36
- * @example
47
+ * @example Put a command into the built-in Video menu
48
+ * "Blur" appears in the player's Video menu in every interface language.
49
+ * The player replaces a leading `@video` with that menu's key ("Video")
50
+ * before it merges paths, so the item joins the built-in menu; an id that
51
+ * is not in `MENU_IDS` makes `add` fail with `invalid`, which a test can
52
+ * rule out ahead of time.
37
53
  * ```ts
54
+ * import type { Plugin } from "@vestra-player/sdk";
55
+ * declare const plugin: Plugin;
56
+ * // ---cut---
57
+ * import assert from "node:assert/strict";
38
58
  * import { MENU_IDS } from "@vestra-player/sdk/testing";
39
59
  *
40
- * assert.ok(MENU_IDS.includes("video"));
60
+ * plugin.commands.add({ id: "blur", path: ["@video", "Blur"], run() {} });
61
+ * assert.ok(MENU_IDS.includes("video")); // a menu id, not a title
41
62
  * ```
42
63
  */
43
64
  export const MENU_IDS = [
@@ -55,12 +76,16 @@ export const MENU_IDS = [
55
76
  * in your own Slint with `Icon { name: "store"; }`). The player's tests check
56
77
  * that this list is its icon set.
57
78
  *
58
- * @example
79
+ * @example Check in a test that the button's icon exists
80
+ * The test fails before release if the plugin's bar button names an icon
81
+ * the player does not have. `commands.add` would refuse an unknown name
82
+ * with `invalid`; the player's tests keep `BAR_BUTTON_ICONS` equal to its
83
+ * icon set, so checking against it is the same check without a player.
59
84
  * ```ts
60
85
  * import assert from "node:assert/strict";
61
86
  * import { BAR_BUTTON_ICONS } from "@vestra-player/sdk/testing";
62
87
  *
63
- * assert.ok(BAR_BUTTON_ICONS.includes("store"));
88
+ * assert.ok(BAR_BUTTON_ICONS.includes("store")); // the icon main.ts uses
64
89
  * ```
65
90
  *
66
91
  * @since 1.5.29
@@ -135,13 +160,23 @@ export const BAR_BUTTON_LIMITS = {
135
160
  * listed. The player's tests check that this list agrees with what its
136
161
  * built-in plugins declare.
137
162
  *
138
- * @example
163
+ * @example Add an item to the built-in Snapshot submenu
164
+ * "Copy Snapshot" appears in Video ▸ Snapshot next to the built-in capture
165
+ * plugin's items. Paths merge by their English keys, so a command whose
166
+ * path continues with the submenu's keys (`["Video", "Snapshot"]`, the menu
167
+ * written as `@video`) lands in the same submenu whatever the interface
168
+ * language.
139
169
  * ```ts
170
+ * import type { Plugin } from "@vestra-player/sdk";
171
+ * declare const plugin: Plugin;
172
+ * declare function copySnapshot(): Promise<void>;
173
+ * // ---cut---
140
174
  * import { BUILTIN_SUBMENUS } from "@vestra-player/sdk/testing";
141
175
  *
142
176
  * const snapshot = BUILTIN_SUBMENUS.find((menu) => menu.path.at(-1) === "Snapshot")!;
143
177
  * plugin.commands.add({
144
178
  * id: "snapshot-to-clipboard",
179
+ * // ["@video", "Snapshot", "Copy Snapshot"]; @video stands for "Video".
145
180
  * path: ["@video", ...snapshot.path.slice(1), "Copy Snapshot"],
146
181
  * run: copySnapshot,
147
182
  * });
@@ -533,7 +568,12 @@ vestra.playlist | playlist.settings | (Playlist Settings…) | F5 | panel
533
568
  * placeholders (`No Audio Tracks`) are. The player's tests check that this
534
569
  * list agrees with what the player and its built-in plugins register.
535
570
  *
536
- * @example
571
+ * @example Check in a test that a default key is still free
572
+ * The test fails if the player or a built-in plugin already binds Ctrl+K
573
+ * globally, before users ever see a conflict. It collects the default
574
+ * bindings of every built-in command that is not limited to its panel's
575
+ * focus (`focus: null`) and looks for the key the plugin wants; bindings
576
+ * are canonical, so compare them as the player writes them.
537
577
  * ```ts
538
578
  * import assert from "node:assert/strict";
539
579
  * import { BUILTIN_COMMANDS } from "@vestra-player/sdk/testing";
package/dist/index.d.ts CHANGED
@@ -12,7 +12,12 @@
12
12
  * documents the manifest capability it needs. Errors are described at
13
13
  * {@link PluginError}. Tests use `@vestra-player/sdk/testing`.
14
14
  *
15
- * @example
15
+ * @example Add a menu item that shows a message
16
+ * The player gets a Tools ▸ Hello menu item; picking it shows "Hello" on
17
+ * the video. The player imports the entry module and calls `activate` with
18
+ * the plugin's `Plugin` object; `commands.add` registers the item (manifest
19
+ * capability `commands`), and the player calls `run` when the user picks it
20
+ * or presses a key bound to it. `osd.show` needs `ui.osd`.
16
21
  * ```ts
17
22
  * import type { Plugin } from "@vestra-player/sdk";
18
23
  *
package/dist/manifest.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * The manifest rules `vestra-plugin check` applies; the same table as the
3
- * player's `host/src/manifest.rs` (the player's tests compare the limits).
3
+ * player's `src/host/src/packages/manifest.rs` (the player's tests compare the limits).
4
4
  * Paths in the manifest are checked against the plugin directory.
5
5
  */
6
6
  import { existsSync, lstatSync, readFileSync, realpathSync } from "node:fs";
@@ -11,11 +11,25 @@
11
11
  * the palette (`slint/palette.slint`); the player's tests check both
12
12
  * palettes against its own.
13
13
  *
14
- * @example
14
+ * @example Check in a test that a colour stays readable in both themes
15
+ * The test fails if the plugin's own highlight colour gets too close to
16
+ * the panel background of either palette. Plugins draw with `Theme` tokens
17
+ * in Slint and the player switches palettes; a colour the plugin picks
18
+ * itself does not switch, so a test compares it with each palette's values
19
+ * (hex strings, `#rrggbb`).
15
20
  * ```ts
21
+ * import assert from "node:assert/strict";
16
22
  * import { THEME_PALETTES } from "@vestra-player/sdk/testing";
17
23
  *
18
- * const accent = THEME_PALETTES["vestra-light"].accent; // "#d4128a"
24
+ * const highlight = "#e07000"; // the plugin's own orange
25
+ * const luminance = (hex: string) => {
26
+ * const [r, g, b] = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255);
27
+ * return 0.2126 * r! + 0.7152 * g! + 0.0722 * b!; // rough, without gamma
28
+ * };
29
+ * for (const [name, palette] of Object.entries(THEME_PALETTES)) {
30
+ * const gap = Math.abs(luminance(highlight) - luminance(palette["bg-panel"]!));
31
+ * assert.ok(gap > 0.3, `${name}: highlight too close to bg-panel`);
32
+ * }
19
33
  * ```
20
34
  *
21
35
  * @since 1.5.14
package/dist/palettes.js CHANGED
@@ -11,11 +11,25 @@
11
11
  * the palette (`slint/palette.slint`); the player's tests check both
12
12
  * palettes against its own.
13
13
  *
14
- * @example
14
+ * @example Check in a test that a colour stays readable in both themes
15
+ * The test fails if the plugin's own highlight colour gets too close to
16
+ * the panel background of either palette. Plugins draw with `Theme` tokens
17
+ * in Slint and the player switches palettes; a colour the plugin picks
18
+ * itself does not switch, so a test compares it with each palette's values
19
+ * (hex strings, `#rrggbb`).
15
20
  * ```ts
21
+ * import assert from "node:assert/strict";
16
22
  * import { THEME_PALETTES } from "@vestra-player/sdk/testing";
17
23
  *
18
- * const accent = THEME_PALETTES["vestra-light"].accent; // "#d4128a"
24
+ * const highlight = "#e07000"; // the plugin's own orange
25
+ * const luminance = (hex: string) => {
26
+ * const [r, g, b] = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255);
27
+ * return 0.2126 * r! + 0.7152 * g! + 0.0722 * b!; // rough, without gamma
28
+ * };
29
+ * for (const [name, palette] of Object.entries(THEME_PALETTES)) {
30
+ * const gap = Math.abs(luminance(highlight) - luminance(palette["bg-panel"]!));
31
+ * assert.ok(gap > 0.3, `${name}: highlight too close to bg-panel`);
32
+ * }
19
33
  * ```
20
34
  *
21
35
  * @since 1.5.14
@@ -11,12 +11,36 @@ import type { Bindings } from "./types.ts";
11
11
  * `"Shift+1"`). Anything else fails with code `invalid`. The player's tests
12
12
  * check that these agree with the player.
13
13
  *
14
- * @example
14
+ * @example Check in a test that the plugin's default keys are well formed
15
+ * The test fails on a binding the player would refuse with `invalid`, such
16
+ * as modifiers out of order or a misspelt key name. `isBinding` applies the
17
+ * same grammar as the player's parser: the part after the last `+` is the
18
+ * input, everything before it modifiers in the order of `modifiers`; an
19
+ * input is a listed name or one other printable character, and `Shift`
20
+ * never goes with a digit or such a character.
15
21
  * ```ts
22
+ * import assert from "node:assert/strict";
16
23
  * import { BINDING_INPUTS } from "@vestra-player/sdk/testing";
17
24
  *
18
- * assert.ok(BINDING_INPUTS.named.includes("PageUp"));
19
- * assert.ok(BINDING_INPUTS.mouse.includes("Mouse:WheelUp"));
25
+ * const { modifiers, letters, digits, functionKeys, named, mouse } = BINDING_INPUTS;
26
+ * const names = new Set([...letters, ...functionKeys, ...named, ...mouse]);
27
+ * function isBinding(binding: string): boolean {
28
+ * // "Ctrl++" is not a binding: the + key is written "Plus".
29
+ * const parts = binding.split("+");
30
+ * const input = parts.pop()!;
31
+ * const order = parts.map((m) => (modifiers as readonly string[]).indexOf(m));
32
+ * // Each modifier known, at most once, in the listed order.
33
+ * if (order.some((at, i) => at < 0 || (i > 0 && at <= order[i - 1]!)))
34
+ * return false;
35
+ * if (names.has(input)) return true; // letters are upper case: "A"
36
+ * const typed = digits.includes(input) || /^[^\p{L}\p{N}\p{C}\s+]$/u.test(input);
37
+ * // ">" rather than "Shift+.": the character already says Shift.
38
+ * return typed && !parts.includes("Shift");
39
+ * }
40
+ * for (const binding of ["Ctrl+Shift+PageUp", "Alt+Mouse:WheelUp", ">"])
41
+ * assert.ok(isBinding(binding), binding);
42
+ * for (const binding of ["Shift+Ctrl+A", "Ctrl+a", "Shift+."])
43
+ * assert.ok(!isBinding(binding), binding);
20
44
  * ```
21
45
  *
22
46
  * @since 1.5.14
@@ -18,12 +18,36 @@ import { failure } from "./runtime.js";
18
18
  * `"Shift+1"`). Anything else fails with code `invalid`. The player's tests
19
19
  * check that these agree with the player.
20
20
  *
21
- * @example
21
+ * @example Check in a test that the plugin's default keys are well formed
22
+ * The test fails on a binding the player would refuse with `invalid`, such
23
+ * as modifiers out of order or a misspelt key name. `isBinding` applies the
24
+ * same grammar as the player's parser: the part after the last `+` is the
25
+ * input, everything before it modifiers in the order of `modifiers`; an
26
+ * input is a listed name or one other printable character, and `Shift`
27
+ * never goes with a digit or such a character.
22
28
  * ```ts
29
+ * import assert from "node:assert/strict";
23
30
  * import { BINDING_INPUTS } from "@vestra-player/sdk/testing";
24
31
  *
25
- * assert.ok(BINDING_INPUTS.named.includes("PageUp"));
26
- * assert.ok(BINDING_INPUTS.mouse.includes("Mouse:WheelUp"));
32
+ * const { modifiers, letters, digits, functionKeys, named, mouse } = BINDING_INPUTS;
33
+ * const names = new Set([...letters, ...functionKeys, ...named, ...mouse]);
34
+ * function isBinding(binding: string): boolean {
35
+ * // "Ctrl++" is not a binding: the + key is written "Plus".
36
+ * const parts = binding.split("+");
37
+ * const input = parts.pop()!;
38
+ * const order = parts.map((m) => (modifiers as readonly string[]).indexOf(m));
39
+ * // Each modifier known, at most once, in the listed order.
40
+ * if (order.some((at, i) => at < 0 || (i > 0 && at <= order[i - 1]!)))
41
+ * return false;
42
+ * if (names.has(input)) return true; // letters are upper case: "A"
43
+ * const typed = digits.includes(input) || /^[^\p{L}\p{N}\p{C}\s+]$/u.test(input);
44
+ * // ">" rather than "Shift+.": the character already says Shift.
45
+ * return typed && !parts.includes("Shift");
46
+ * }
47
+ * for (const binding of ["Ctrl+Shift+PageUp", "Alt+Mouse:WheelUp", ">"])
48
+ * assert.ok(isBinding(binding), binding);
49
+ * for (const binding of ["Shift+Ctrl+A", "Ctrl+a", "Shift+."])
50
+ * assert.ok(!isBinding(binding), binding);
27
51
  * ```
28
52
  *
29
53
  * @since 1.5.14
@@ -8,11 +8,23 @@ import type { TemplateArgsJson, TextValueJson } from "../wire.ts";
8
8
  * rejects, `OsdPanel.set` reports to `Scope.onError`. The player's tests
9
9
  * check that these agree with the player.
10
10
  *
11
- * @example
11
+ * @example Keep a long list inside the information block's limits
12
+ * The block shows the first 32 rows of a list, each cut at 60 columns. The
13
+ * player counts the text as shown (after it is translated and formatted); a
14
+ * block over `panelLines` lines or `panelBytes` bytes is refused whole with
15
+ * `invalid`, reported to `Scope.onError` because `set` never throws, so
16
+ * the plugin trims the list itself (for very long non-ASCII rows, check the
17
+ * bytes too).
12
18
  * ```ts
19
+ * import type { Plugin } from "@vestra-player/sdk";
20
+ * declare const plugin: Plugin;
21
+ * declare const rows: string[];
22
+ * // ---cut---
13
23
  * import { OSD_LIMITS } from "@vestra-player/sdk/testing";
14
24
  *
25
+ * const panel = plugin.osd.panel();
15
26
  * const lines = rows.slice(0, OSD_LIMITS.panelLines);
27
+ * // maxColumns cuts each longer line and ends it with "…".
16
28
  * panel.set(lines.join("\n"), null, { maxColumns: 60 });
17
29
  * ```
18
30
  *
@@ -9,11 +9,23 @@ import { isTextValue, textProblem } from "../i18n.js";
9
9
  * rejects, `OsdPanel.set` reports to `Scope.onError`. The player's tests
10
10
  * check that these agree with the player.
11
11
  *
12
- * @example
12
+ * @example Keep a long list inside the information block's limits
13
+ * The block shows the first 32 rows of a list, each cut at 60 columns. The
14
+ * player counts the text as shown (after it is translated and formatted); a
15
+ * block over `panelLines` lines or `panelBytes` bytes is refused whole with
16
+ * `invalid`, reported to `Scope.onError` because `set` never throws, so
17
+ * the plugin trims the list itself (for very long non-ASCII rows, check the
18
+ * bytes too).
13
19
  * ```ts
20
+ * import type { Plugin } from "@vestra-player/sdk";
21
+ * declare const plugin: Plugin;
22
+ * declare const rows: string[];
23
+ * // ---cut---
14
24
  * import { OSD_LIMITS } from "@vestra-player/sdk/testing";
15
25
  *
26
+ * const panel = plugin.osd.panel();
16
27
  * const lines = rows.slice(0, OSD_LIMITS.panelLines);
28
+ * // maxColumns cuts each longer line and ends it with "…".
17
29
  * panel.set(lines.join("\n"), null, { maxColumns: 60 });
18
30
  * ```
19
31
  *
@@ -13,13 +13,31 @@ export declare function tools(scope: ScopeImpl): Tools;
13
13
  * with the key and the request around the value). The player's tests check
14
14
  * that these agree with the player.
15
15
  *
16
- * @example
16
+ * @example Remember a position per URL without breaking the limits
17
+ * The plugin keeps the last position of each URL for this player session
18
+ * and skips a URL too long to be a key. Keys are counted in UTF-8 bytes, so
19
+ * the length is measured with `Buffer.byteLength`, not `length`; a `set`
20
+ * that would take the plugin's entries over 1 MiB rejects with `limit` and
21
+ * changes nothing, so the plugin clears the old entries and goes on.
17
22
  * ```ts
18
- * import assert from "node:assert/strict";
23
+ * import type { Plugin, PluginError } from "@vestra-player/sdk";
24
+ * declare const plugin: Plugin;
25
+ * declare const url: string;
26
+ * declare const position: number;
27
+ * // ---cut---
19
28
  * import { SESSION_STORAGE_LIMITS } from "@vestra-player/sdk/testing";
20
29
  *
21
- * const key = "https://example.com/a.mkv";
22
- * assert.ok(Buffer.byteLength(key) <= SESSION_STORAGE_LIMITS.keyBytes);
30
+ * const session = plugin.storage.session;
31
+ * if (Buffer.byteLength(url) <= SESSION_STORAGE_LIMITS.keyBytes) {
32
+ * try {
33
+ * await session.set(url, position);
34
+ * } catch (error) {
35
+ * if ((error as PluginError).code !== "limit") throw error;
36
+ * // Full: start over rather than lose the newest position.
37
+ * for (const key of await session.keys()) await session.delete(key);
38
+ * await session.set(url, position);
39
+ * }
40
+ * }
23
41
  * ```
24
42
  *
25
43
  * @since 1.5.25
@@ -46,11 +64,34 @@ export declare function system(scope: ScopeImpl): System;
46
64
  * fails with code `invalid`; a new name beyond `names` fails with `limit`.
47
65
  * The player's tests check that these agree with the player.
48
66
  *
49
- * @example
67
+ * @example Store an API token only when it fits
68
+ * The plugin keeps a token the user pasted, or tells them it is empty or
69
+ * too long. Values are 1–1024 bytes counted in UTF-8, so the check uses
70
+ * `Buffer.byteLength`; `secrets.set` would refuse other values with
71
+ * `invalid`. The store itself can still refuse (`unsupported` without a
72
+ * system store, `unavailable` when it cannot be reached, `failed` when the
73
+ * user denies access): only
74
+ * those codes get the "could not be saved" message, anything else (a
75
+ * missing `secrets` capability) is a bug and is thrown on.
50
76
  * ```ts
77
+ * import type { Plugin, PluginError } from "@vestra-player/sdk";
78
+ * declare const plugin: Plugin;
79
+ * declare const token: string;
80
+ * // ---cut---
51
81
  * import { SECRET_LIMITS } from "@vestra-player/sdk/testing";
52
82
  *
53
- * assert.ok(Buffer.byteLength(token) <= SECRET_LIMITS.valueBytes);
83
+ * if (!token || Buffer.byteLength(token) > SECRET_LIMITS.valueBytes) {
84
+ * await plugin.osd.show("Paste a token of at most 1024 bytes.");
85
+ * } else {
86
+ * try {
87
+ * await plugin.secrets.set("api-token", token);
88
+ * } catch (error) {
89
+ * const { code } = error as PluginError;
90
+ * if (!["unsupported", "unavailable", "failed"].includes(code)) throw error;
91
+ * // No keychain or access denied: ask again next time.
92
+ * await plugin.osd.show("The token could not be saved.");
93
+ * }
94
+ * }
54
95
  * ```
55
96
  *
56
97
  * @since 1.5.14