@vestra-player/sdk 1.5.47 → 1.5.49
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 +26 -0
- package/dist/commands.d.ts +50 -0
- package/dist/commands.js +50 -0
- package/dist/player-runtime.d.ts +1 -1
- package/dist/plugin/commands.js +23 -7
- package/dist/plugin/index.d.ts +11 -4
- package/dist/plugin/index.js +4 -2
- package/dist/plugin/runtime.d.ts +20 -1
- package/dist/plugin/runtime.js +44 -1
- package/dist/plugin/testing.d.ts +5 -2
- package/dist/plugin/testing.js +202 -51
- package/dist/plugin/types.d.ts +34 -7
- package/llms-full.txt +89 -28
- package/llms.txt +20 -20
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.49
|
|
4
|
+
|
|
5
|
+
**Who must change: nobody.** No changes to the plugin API (`@vestra-player/sdk/runtime` gains the additive `RuntimeOptions` and an optional sixth `createPlugin` argument), `apiVersion` stays `vestra/1`, no new capability, no player version needed, and the player's runner needs no change. **Behaviour change, approved by the maintainer (2026-11-01):** in the player, a command declaration it refuses during activation no longer fails the whole plugin. What `commands.add` checks itself (an unknown `@` menu id, more than 8 path segments, an `order` out of range, a malformed or repeated binding, a bad `button`, a reason text of the wrong shape) still throws from `add`. A plugin whose `activate` relied on that failure (none is known) now runs without the refused command.
|
|
6
|
+
|
|
7
|
+
- **A refused command declaration is skipped, not fatal.** When the player answers a `commands.add` made during activation with `invalid`, `conflict` or `limit` (a text over its limit, a path another command has, too many commands), the SDK logs it with the command id and the player's message (`[<plugin>] command <id> skipped, the player refused it (conflict): …`), skips that command and lets the rest of the plugin activate: its other commands, panels, settings pages and resolvers register and the player gets `runtime.ready`. Before, the first refusal made activation fail (`runtime.failed`) and the plugin's menus and panels were withdrawn. The command's `ready` still rejects, and the error reaches the scope's `onError` once activation completes, so a listener added anywhere in `activate` hears it. This is how a slot, sequence or resolver another plugin holds was already treated, and how VS Code treats a bad contribution (logged, the extension activates). Other failures during activation (`capability`, a timeout, an error of the player) still fail it, and a refusal after activation goes to `onError` as before.
|
|
8
|
+
- **A refused command sends no updates.** After a definite refusal of its registration, later state changes (`set`, a binding) send nothing, instead of updates the player answers with `command missing`; dispose it to declare the id again.
|
|
9
|
+
- **`TestHost` refuses a binding listed twice as player 0.6.1 does.** The player now checks a command's bindings with its binding table (`Table::canonical`) inside the registration: each binding parsed and each input once, compared as parsed inputs, before the reason, the id and path conflicts and the limits, with the `invalid command: ` prefix (`invalid command: binding F3 is listed twice`). `TestHost` answered it without the prefix and only after the conflict and limit checks; it now gives the same answer in the same order.
|
|
10
|
+
- **`TestHost` stays strict.** `startPlugin` still rejects when a command declaration is refused during activation, so a plugin's tests show the bad declaration. Fix the declaration, not the test. (`createPlugin` in `@vestra-player/sdk/runtime` takes `{ strictDeclarations }`; `startPlugin` sets it, the player's runner does not.)
|
|
11
|
+
- _Docs_: [Example: Panel menu](docs/examples/panel-menu.md) draws its list in a `ListView` instead of a `for` in a `VerticalLayout`; the [interface guidelines](docs/guidelines/interface.md#panels) gain **Long lists use `ListView`**. `Command.ready`, `Commands`, `Plugin`, `startPlugin`, [The plugin model](docs/explanation/plugins.md), [Host and Node](docs/explanation/host-and-node.md) and [Limits](docs/reference/limits.md) describe the new activation rule.
|
|
12
|
+
|
|
13
|
+
## 1.5.48
|
|
14
|
+
|
|
15
|
+
**Who must change: nobody, but plugin tests may now fail.** No changes to the TypeScript API beyond new fields of `COMMAND_LIMITS`, `apiVersion` stays `vestra/1`, no new capability, no player version needed. `TestHost` now refuses the command declarations the player has always refused: a plugin test that starts failing shows a menu item, key or reason the real player already rejected (the command never appeared, or the plugin failed to activate). Fix the declaration, not the test.
|
|
16
|
+
|
|
17
|
+
- **`TestHost` checks a command declaration as the player does** (`commands.register`, in the player's order, with its codes and messages):
|
|
18
|
+
- the local id: 1–128 bytes of ASCII letters, digits, `.`, `_` and `-` (`invalid`);
|
|
19
|
+
- the title (128 bytes), each path segment (80 bytes) and the group (80 bytes, default `"domain"`): not empty, no control character (C0, DEL and C1), no whitespace at either end (Unicode White_Space, as the player's `trim`). A text value is checked on its English rendering, which is also the key menus merge by. The message names the field: `invalid command: path segment requires 1..=80 bytes, no controls or surrounding whitespace`;
|
|
20
|
+
- at most 8 path segments, an `order` within ±1,000,000, bindings in the player's syntax, each once (`invalid`);
|
|
21
|
+
- the state's reason: at most 512 bytes as shown, the same text rules (`invalid` at registration; `command` on an update, as the player answers every refused update);
|
|
22
|
+
- a path that repeats another command's or is a submenu of one, or one of whose submenus is a command, the player's own commands included (`Audio ▸ Mute ▸ …`, `Playback ▸ Speed` as an item, `Quit`, `Toggle Fullscreen ▸ …`): `conflict` (`command conflict: menu path duplicates a command or collides with a leaf/group`). The total (`COMMAND_LIMITS.total`) counts all 19 of the player's own commands, Toggle Fullscreen, Enter and Leave Fullscreen, Quit and Toggle Sidebar included;
|
|
23
|
+
- an id registered twice: `conflict` (was `command`); an unknown `@` menu id, a bar button without a path or with an unknown icon, give the player's full messages;
|
|
24
|
+
- an update of a command that is not registered rejects with `command` (`command missing: owner 1 / <id>`; was a plain `Error`), a revision that is not a positive integer with `invalid` (`positive revision required`), and a stale one names the current revision.
|
|
25
|
+
- **The seek items follow `setStepSeek`.** `host.coreAction("seek-backward")` and `seek-forward` carry the step in their path and title (`Playback ▸ Seek ▸ Back 2.5 s`), as the player registers them again when the step changes; they were fixed at 5 s.
|
|
26
|
+
- **`COMMAND_LIMITS`** gains the player's declaration limits: `depth` (8), `idBytes` (128), `titleBytes` (128), `segmentBytes` (80), `groupBytes` (80), `reasonBytes` (512) and `order` (1,000,000), so the player's tests can check that they agree.
|
|
27
|
+
- _Commands_: `CommandOptions` and `Commands.add` say which checks the player makes (`ready` rejects with `invalid`) and which `add` makes at once.
|
|
28
|
+
|
|
3
29
|
## 1.5.47
|
|
4
30
|
|
|
5
31
|
**Who must change: nobody.** No changes to the TypeScript API, `apiVersion` stays `vestra/1`, no new capability, no player version needed. The built-in command table follows player 0.6.0's new default keys: a plugin whose documentation or tests list the player's default keys (from `BUILTIN_COMMANDS`, or Escape, Enter, F11 and the side mouse buttons by hand) should update them.
|
package/dist/commands.d.ts
CHANGED
|
@@ -11,6 +11,9 @@ import type { ConditionJson } from "./plugin/when.ts";
|
|
|
11
11
|
* already registered are unaffected, and disposing a command frees its slot
|
|
12
12
|
* at once. {@link TestHost} enforces the same caps.
|
|
13
13
|
*
|
|
14
|
+
* The other fields are the player's rules for one declaration (since
|
|
15
|
+
* 1.5.48): a command past them is refused with `invalid`, the same way.
|
|
16
|
+
*
|
|
14
17
|
* For menus built from media content (tracks, chapters, presets), set your
|
|
15
18
|
* own sensible display cap well below `perPlugin`.
|
|
16
19
|
*
|
|
@@ -41,6 +44,53 @@ export declare const COMMAND_LIMITS: {
|
|
|
41
44
|
readonly perPlugin: 160;
|
|
42
45
|
/** Commands of every plugin plus the player's own, together (1024). */
|
|
43
46
|
readonly total: 1024;
|
|
47
|
+
/**
|
|
48
|
+
* Segments of a menu path, the leaf included.
|
|
49
|
+
*
|
|
50
|
+
* @since 1.5.48
|
|
51
|
+
*/
|
|
52
|
+
readonly depth: 8;
|
|
53
|
+
/**
|
|
54
|
+
* UTF-8 bytes of a command's local id: ASCII letters, digits, `.`,
|
|
55
|
+
* `_` and `-`.
|
|
56
|
+
*
|
|
57
|
+
* @since 1.5.48
|
|
58
|
+
*/
|
|
59
|
+
readonly idBytes: 128;
|
|
60
|
+
/**
|
|
61
|
+
* UTF-8 bytes of a title, a text value counted by its English
|
|
62
|
+
* rendering. Like every command text: not empty, no control characters,
|
|
63
|
+
* no whitespace at either end.
|
|
64
|
+
*
|
|
65
|
+
* @since 1.5.48
|
|
66
|
+
*/
|
|
67
|
+
readonly titleBytes: 128;
|
|
68
|
+
/**
|
|
69
|
+
* UTF-8 bytes of a menu path segment, a text value counted by its
|
|
70
|
+
* English rendering.
|
|
71
|
+
*
|
|
72
|
+
* @since 1.5.48
|
|
73
|
+
*/
|
|
74
|
+
readonly segmentBytes: 80;
|
|
75
|
+
/**
|
|
76
|
+
* UTF-8 bytes of a group.
|
|
77
|
+
*
|
|
78
|
+
* @since 1.5.48
|
|
79
|
+
*/
|
|
80
|
+
readonly groupBytes: 80;
|
|
81
|
+
/**
|
|
82
|
+
* UTF-8 bytes of a state's reason, as shown in the player's
|
|
83
|
+
* language.
|
|
84
|
+
*
|
|
85
|
+
* @since 1.5.48
|
|
86
|
+
*/
|
|
87
|
+
readonly reasonBytes: 512;
|
|
88
|
+
/**
|
|
89
|
+
* The largest `order` either way: an order is within ±1,000,000.
|
|
90
|
+
*
|
|
91
|
+
* @since 1.5.48
|
|
92
|
+
*/
|
|
93
|
+
readonly order: 1000000;
|
|
44
94
|
};
|
|
45
95
|
/**
|
|
46
96
|
* The player's built-in top-level menus, by id. Write `@<id>` as the first
|
package/dist/commands.js
CHANGED
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
* already registered are unaffected, and disposing a command frees its slot
|
|
8
8
|
* at once. {@link TestHost} enforces the same caps.
|
|
9
9
|
*
|
|
10
|
+
* The other fields are the player's rules for one declaration (since
|
|
11
|
+
* 1.5.48): a command past them is refused with `invalid`, the same way.
|
|
12
|
+
*
|
|
10
13
|
* For menus built from media content (tracks, chapters, presets), set your
|
|
11
14
|
* own sensible display cap well below `perPlugin`.
|
|
12
15
|
*
|
|
@@ -37,6 +40,53 @@ export const COMMAND_LIMITS = {
|
|
|
37
40
|
perPlugin: 160,
|
|
38
41
|
/** Commands of every plugin plus the player's own, together (1024). */
|
|
39
42
|
total: 1024,
|
|
43
|
+
/**
|
|
44
|
+
* Segments of a menu path, the leaf included.
|
|
45
|
+
*
|
|
46
|
+
* @since 1.5.48
|
|
47
|
+
*/
|
|
48
|
+
depth: 8,
|
|
49
|
+
/**
|
|
50
|
+
* UTF-8 bytes of a command's local id: ASCII letters, digits, `.`,
|
|
51
|
+
* `_` and `-`.
|
|
52
|
+
*
|
|
53
|
+
* @since 1.5.48
|
|
54
|
+
*/
|
|
55
|
+
idBytes: 128,
|
|
56
|
+
/**
|
|
57
|
+
* UTF-8 bytes of a title, a text value counted by its English
|
|
58
|
+
* rendering. Like every command text: not empty, no control characters,
|
|
59
|
+
* no whitespace at either end.
|
|
60
|
+
*
|
|
61
|
+
* @since 1.5.48
|
|
62
|
+
*/
|
|
63
|
+
titleBytes: 128,
|
|
64
|
+
/**
|
|
65
|
+
* UTF-8 bytes of a menu path segment, a text value counted by its
|
|
66
|
+
* English rendering.
|
|
67
|
+
*
|
|
68
|
+
* @since 1.5.48
|
|
69
|
+
*/
|
|
70
|
+
segmentBytes: 80,
|
|
71
|
+
/**
|
|
72
|
+
* UTF-8 bytes of a group.
|
|
73
|
+
*
|
|
74
|
+
* @since 1.5.48
|
|
75
|
+
*/
|
|
76
|
+
groupBytes: 80,
|
|
77
|
+
/**
|
|
78
|
+
* UTF-8 bytes of a state's reason, as shown in the player's
|
|
79
|
+
* language.
|
|
80
|
+
*
|
|
81
|
+
* @since 1.5.48
|
|
82
|
+
*/
|
|
83
|
+
reasonBytes: 512,
|
|
84
|
+
/**
|
|
85
|
+
* The largest `order` either way: an order is within ±1,000,000.
|
|
86
|
+
*
|
|
87
|
+
* @since 1.5.48
|
|
88
|
+
*/
|
|
89
|
+
order: 1_000_000,
|
|
40
90
|
};
|
|
41
91
|
/**
|
|
42
92
|
* The player's built-in top-level menus, by id. Write `@<id>` as the first
|
package/dist/player-runtime.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* @packageDocumentation
|
|
9
9
|
*/
|
|
10
10
|
export { createPlugin } from "./plugin/index.ts";
|
|
11
|
-
export type { PluginInstance, PluginManifest } from "./plugin/index.ts";
|
|
11
|
+
export type { PluginInstance, PluginManifest, RuntimeOptions, } from "./plugin/index.ts";
|
|
12
12
|
export { HostError } from "./wire.ts";
|
|
13
13
|
export type { Json, Transport } from "./wire.ts";
|
|
14
14
|
export { API_VERSION, CAPABILITIES, CAPABILITY_INFO, MESSAGE_LIMIT, } from "./capabilities.ts";
|
package/dist/plugin/commands.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Menu commands: declaration, state sync (revisions hidden) and dispatch. */
|
|
2
|
-
import { failure, Latest, quiet } from "./runtime.js";
|
|
2
|
+
import { failure, Latest, quiet, uncertain } from "./runtime.js";
|
|
3
3
|
import { checkBindings } from "./bindings.js";
|
|
4
4
|
import { BAR_BUTTON_ICONS, BAR_BUTTON_LIMITS, MENU_IDS } from "../commands.js";
|
|
5
5
|
import { ImageHandle } from "./ui.js";
|
|
@@ -100,6 +100,10 @@ class CommandHandle {
|
|
|
100
100
|
#last = "";
|
|
101
101
|
#disposed = false;
|
|
102
102
|
#registered = false;
|
|
103
|
+
/** Its entry in `runtime.commands` (a later handle may own the id). */
|
|
104
|
+
#run = () => { };
|
|
105
|
+
/** The player refused the declaration: no state updates follow. */
|
|
106
|
+
#refused = false;
|
|
103
107
|
#pump;
|
|
104
108
|
#label;
|
|
105
109
|
/** Where the plugin added it, for the player's log. */
|
|
@@ -124,10 +128,20 @@ class CommandHandle {
|
|
|
124
128
|
// registration itself is revision 1, so every later state follows it.
|
|
125
129
|
const previous = removing(runtime).get(this.id);
|
|
126
130
|
this.#pump = new Latest(async ({ state, when }) => {
|
|
131
|
+
if (this.#refused)
|
|
132
|
+
return;
|
|
127
133
|
const revision = ++this.#revision;
|
|
128
134
|
if (revision === 1) {
|
|
129
135
|
await previous;
|
|
130
|
-
|
|
136
|
+
try {
|
|
137
|
+
await runtime.host.commands.register(this.id, 1, declaration, state, when);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
// A definite refusal: the player has no such command to update.
|
|
141
|
+
if (!uncertain(error))
|
|
142
|
+
this.#refused = true;
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
131
145
|
this.#registered = true;
|
|
132
146
|
}
|
|
133
147
|
else
|
|
@@ -136,12 +150,13 @@ class CommandHandle {
|
|
|
136
150
|
const initial = this.#state();
|
|
137
151
|
this.#last = JSON.stringify(initial);
|
|
138
152
|
this.ready = quiet(runtime
|
|
139
|
-
.
|
|
153
|
+
.declaring(this.#pump.push(initial), `command ${this.id}`, (error) => scope.report(error))
|
|
140
154
|
.then(() => { }));
|
|
141
|
-
|
|
155
|
+
this.#run = () => {
|
|
142
156
|
if (!this.#disposed)
|
|
143
157
|
scope.run(options.run);
|
|
144
|
-
}
|
|
158
|
+
};
|
|
159
|
+
runtime.commands.set(this.id, this.#run);
|
|
145
160
|
this.#binding = runtime.bind(() => this.#sync());
|
|
146
161
|
scope.add(this);
|
|
147
162
|
}
|
|
@@ -177,7 +192,7 @@ class CommandHandle {
|
|
|
177
192
|
return Object.keys(when).length ? { state, when } : { state };
|
|
178
193
|
}
|
|
179
194
|
#sync() {
|
|
180
|
-
if (this.#disposed)
|
|
195
|
+
if (this.#disposed || this.#refused)
|
|
181
196
|
return;
|
|
182
197
|
const state = this.#state();
|
|
183
198
|
const encoded = JSON.stringify(state);
|
|
@@ -196,7 +211,8 @@ class CommandHandle {
|
|
|
196
211
|
this.#disposed = true;
|
|
197
212
|
this.#scope.forget(this);
|
|
198
213
|
const runtime = this.#scope.runtime;
|
|
199
|
-
runtime.commands.
|
|
214
|
+
if (runtime.commands.get(this.id) === this.#run)
|
|
215
|
+
runtime.commands.delete(this.id);
|
|
200
216
|
this.#pump.stop();
|
|
201
217
|
// Removed from the menu once registered. Recorded before any await, so an
|
|
202
218
|
// immediate re-add of the same id registers after this removal.
|
package/dist/plugin/index.d.ts
CHANGED
|
@@ -6,16 +6,23 @@
|
|
|
6
6
|
* @internal
|
|
7
7
|
*/
|
|
8
8
|
import type { Transport } from "../wire.ts";
|
|
9
|
-
import type { PluginManifest } from "./runtime.ts";
|
|
9
|
+
import type { PluginManifest, RuntimeOptions } from "./runtime.ts";
|
|
10
10
|
import type { Plugin } from "./types.ts";
|
|
11
11
|
export type * from "./types.ts";
|
|
12
|
-
export type { PluginManifest };
|
|
12
|
+
export type { PluginManifest, RuntimeOptions };
|
|
13
13
|
export interface PluginInstance {
|
|
14
14
|
plugin: Plugin;
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Wait for registrations started during activation; rejects if one failed.
|
|
17
|
+
* A command declaration the player refused is not a failure (it is
|
|
18
|
+
* logged, skipped and reported to `onError`) unless
|
|
19
|
+
* `RuntimeOptions.strictDeclarations` is set.
|
|
20
|
+
*/
|
|
16
21
|
activated(): Promise<void>;
|
|
17
22
|
dispose(): Promise<void>;
|
|
18
23
|
}
|
|
19
24
|
export declare function createPlugin(transport: Transport, manifest: PluginManifest, directory: string, dataDirectory?: string,
|
|
20
25
|
/** `plugin.app` (the runner passes the player's `VESTRA_APP_INFO`). */
|
|
21
|
-
app?: unknown
|
|
26
|
+
app?: unknown,
|
|
27
|
+
/** `TestHost` sets `strictDeclarations`; the player's runner does not. */
|
|
28
|
+
options?: RuntimeOptions): Promise<PluginInstance>;
|
package/dist/plugin/index.js
CHANGED
|
@@ -5,8 +5,10 @@ import { windowOf } from "./window.js";
|
|
|
5
5
|
import { timelineOf } from "./timeline.js";
|
|
6
6
|
export async function createPlugin(transport, manifest, directory, dataDirectory,
|
|
7
7
|
/** `plugin.app` (the runner passes the player's `VESTRA_APP_INFO`). */
|
|
8
|
-
app
|
|
9
|
-
|
|
8
|
+
app,
|
|
9
|
+
/** `TestHost` sets `strictDeclarations`; the player's runner does not. */
|
|
10
|
+
options) {
|
|
11
|
+
const runtime = new Runtime(transport, manifest, directory, dataDirectory, app, options);
|
|
10
12
|
// Player, window and timeline state are synchronous for plugins: read them first.
|
|
11
13
|
await Promise.all([
|
|
12
14
|
playerOf(runtime).init(),
|
package/dist/plugin/runtime.d.ts
CHANGED
|
@@ -37,6 +37,15 @@ export type Change = ReadonlyMap<string, ReadonlySet<string>>;
|
|
|
37
37
|
* without `update()`) still shows within about this long.
|
|
38
38
|
*/
|
|
39
39
|
export declare const SAFETY_PASS_MS = 1000;
|
|
40
|
+
/** How the plugin is run: the player's runner, or `TestHost`. */
|
|
41
|
+
export interface RuntimeOptions {
|
|
42
|
+
/**
|
|
43
|
+
* A declaration the player refuses during activation fails activation
|
|
44
|
+
* (`startPlugin`, so a plugin's tests show it) instead of being logged,
|
|
45
|
+
* reported to `onError` and skipped (the player's runner).
|
|
46
|
+
*/
|
|
47
|
+
strictDeclarations?: boolean;
|
|
48
|
+
}
|
|
40
49
|
export declare const same: (a: Json | undefined, b: Json | undefined) => boolean;
|
|
41
50
|
/** One value in flight, only the newest waiting. Each push learns its fate. */
|
|
42
51
|
export declare class Latest<T> {
|
|
@@ -74,7 +83,7 @@ export declare class Runtime {
|
|
|
74
83
|
* property writes). Not part of the plugin API; empty for older players.
|
|
75
84
|
*/
|
|
76
85
|
readonly features: ReadonlySet<string>;
|
|
77
|
-
constructor(transport: Transport, manifest: PluginManifest, directory: string, dataDirectory?: string, app?: unknown);
|
|
86
|
+
constructor(transport: Transport, manifest: PluginManifest, directory: string, dataDirectory?: string, app?: unknown, options?: RuntimeOptions);
|
|
78
87
|
can(capability: string): boolean;
|
|
79
88
|
/** Lazily created singletons shared by every scope of this plugin. */
|
|
80
89
|
service<T>(name: string, create: () => T): T;
|
|
@@ -83,6 +92,16 @@ export declare class Runtime {
|
|
|
83
92
|
* plugin reports ready; later ones report failures to their scope.
|
|
84
93
|
*/
|
|
85
94
|
starting<T>(work: Promise<T>, report: (error: unknown) => void): Promise<T>;
|
|
95
|
+
/**
|
|
96
|
+
* A declaration the player checks on its own (`what`: "command x"),
|
|
97
|
+
* started like `starting`. In the player's runner a refusal of it during
|
|
98
|
+
* activation (`invalid`, `conflict`, `limit`) does not fail the plugin:
|
|
99
|
+
* it is logged, skipped and reported to the scope once activation
|
|
100
|
+
* completes, so an `onError` added anywhere in `activate` hears it (SDK
|
|
101
|
+
* 1.5.49; VS Code logs a bad contribution and activates the extension).
|
|
102
|
+
* Under `strictDeclarations` (TestHost) it fails activation as before.
|
|
103
|
+
*/
|
|
104
|
+
declaring<T>(work: Promise<T>, what: string, report: (error: unknown) => void): Promise<T>;
|
|
86
105
|
activated(): Promise<void>;
|
|
87
106
|
bind(evaluate: () => void): Disposable;
|
|
88
107
|
/**
|
package/dist/plugin/runtime.js
CHANGED
|
@@ -86,6 +86,16 @@ function merge(into, from) {
|
|
|
86
86
|
* without `update()`) still shows within about this long.
|
|
87
87
|
*/
|
|
88
88
|
export const SAFETY_PASS_MS = 1000;
|
|
89
|
+
/**
|
|
90
|
+
* The player's answers to a declaration that refuse that one declaration
|
|
91
|
+
* (a command's path, texts or count): during activation they skip it
|
|
92
|
+
* instead of failing the plugin (`Runtime.declaring`, SDK 1.5.49).
|
|
93
|
+
*/
|
|
94
|
+
const DECLARATION_REFUSALS = new Set([
|
|
95
|
+
"invalid",
|
|
96
|
+
"conflict",
|
|
97
|
+
"limit",
|
|
98
|
+
]);
|
|
89
99
|
export const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
90
100
|
/** One value in flight, only the newest waiting. Each push learns its fate. */
|
|
91
101
|
export class Latest {
|
|
@@ -149,6 +159,10 @@ export class Runtime {
|
|
|
149
159
|
/** Registrations that activation must wait for before reporting ready. */
|
|
150
160
|
startup = new Set();
|
|
151
161
|
#activating = true;
|
|
162
|
+
/** `RuntimeOptions.strictDeclarations`. */
|
|
163
|
+
#strict;
|
|
164
|
+
/** Declarations skipped during activation: reported once it completes. */
|
|
165
|
+
#refusals = [];
|
|
152
166
|
#transport;
|
|
153
167
|
/** Each binding with the events whose state it read last time (held). */
|
|
154
168
|
#bindings = new Map();
|
|
@@ -187,8 +201,9 @@ export class Runtime {
|
|
|
187
201
|
* property writes). Not part of the plugin API; empty for older players.
|
|
188
202
|
*/
|
|
189
203
|
features;
|
|
190
|
-
constructor(transport, manifest, directory, dataDirectory, app) {
|
|
204
|
+
constructor(transport, manifest, directory, dataDirectory, app, options = {}) {
|
|
191
205
|
this.#transport = transport;
|
|
206
|
+
this.#strict = options.strictDeclarations ?? false;
|
|
192
207
|
this.dataDirectory = dataDirectory;
|
|
193
208
|
this.local = transport.local;
|
|
194
209
|
this.host = createWire(transport);
|
|
@@ -233,6 +248,32 @@ export class Runtime {
|
|
|
233
248
|
work.catch(() => { });
|
|
234
249
|
return work;
|
|
235
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* A declaration the player checks on its own (`what`: "command x"),
|
|
253
|
+
* started like `starting`. In the player's runner a refusal of it during
|
|
254
|
+
* activation (`invalid`, `conflict`, `limit`) does not fail the plugin:
|
|
255
|
+
* it is logged, skipped and reported to the scope once activation
|
|
256
|
+
* completes, so an `onError` added anywhere in `activate` hears it (SDK
|
|
257
|
+
* 1.5.49; VS Code logs a bad contribution and activates the extension).
|
|
258
|
+
* Under `strictDeclarations` (TestHost) it fails activation as before.
|
|
259
|
+
*/
|
|
260
|
+
declaring(work, what, report) {
|
|
261
|
+
if (!this.#activating || this.#strict)
|
|
262
|
+
return this.starting(work, report);
|
|
263
|
+
const settled = work.then(() => { }, (error) => {
|
|
264
|
+
if (!(error instanceof HostError) ||
|
|
265
|
+
!DECLARATION_REFUSALS.has(error.code))
|
|
266
|
+
throw error;
|
|
267
|
+
console.error(`[${this.manifest.id}] ${what} skipped, the player refused it (${error.code}): ${error.message}`);
|
|
268
|
+
this.#refusals.push(() => report(error));
|
|
269
|
+
});
|
|
270
|
+
// Observed now: `activated()` may come after `activate` awaits, and
|
|
271
|
+
// its `allSettled` still sees the rejection.
|
|
272
|
+
settled.catch(() => { });
|
|
273
|
+
this.startup.add(settled);
|
|
274
|
+
work.catch(() => { });
|
|
275
|
+
return work;
|
|
276
|
+
}
|
|
236
277
|
async activated() {
|
|
237
278
|
while (this.startup.size) {
|
|
238
279
|
const pending = [...this.startup];
|
|
@@ -242,6 +283,8 @@ export class Runtime {
|
|
|
242
283
|
throw result.reason;
|
|
243
284
|
}
|
|
244
285
|
this.#activating = false;
|
|
286
|
+
for (const report of this.#refusals.splice(0))
|
|
287
|
+
report();
|
|
245
288
|
}
|
|
246
289
|
bind(evaluate) {
|
|
247
290
|
const held = new Map();
|
package/dist/plugin/testing.d.ts
CHANGED
|
@@ -2728,8 +2728,11 @@ export declare class TestHost implements Transport {
|
|
|
2728
2728
|
*
|
|
2729
2729
|
* Rejects when `manifest.json` cannot be read or parsed, when `activate`
|
|
2730
2730
|
* throws, or when a registration made during activation fails (for example
|
|
2731
|
-
* `limit`, `conflict`, `invalid`)
|
|
2732
|
-
*
|
|
2731
|
+
* `limit`, `conflict`, `invalid`). Stricter than the player's runner for
|
|
2732
|
+
* commands (SDK 1.5.49): there the SDK logs a command declaration the
|
|
2733
|
+
* player refuses during activation, skips it and activates the rest of the
|
|
2734
|
+
* plugin (`onError` hears it); `startPlugin` fails, so a test shows the
|
|
2735
|
+
* bad declaration.
|
|
2733
2736
|
*
|
|
2734
2737
|
* @param activate - The plugin's default-exported `activate` function.
|
|
2735
2738
|
* @param options - Where the plugin lives and how to start it.
|
package/dist/plugin/testing.js
CHANGED
|
@@ -78,7 +78,7 @@ import { CAPABILITY_INFO, MESSAGE_LIMIT } from "../capabilities.js";
|
|
|
78
78
|
import { OSD_LIMITS } from "./osd.js";
|
|
79
79
|
import { SECRET_LIMITS, SESSION_STORAGE_LIMITS } from "./services.js";
|
|
80
80
|
import { SUBTITLE_LIMITS } from "./subtitles.js";
|
|
81
|
-
import { BINDING_INPUTS } from "./bindings.js";
|
|
81
|
+
import { BINDING_INPUTS, canonical } from "./bindings.js";
|
|
82
82
|
import { THEME_PALETTES } from "../palettes.js";
|
|
83
83
|
import { IMAGE_LIMITS, UI_VALUE_LIMITS, valueLimitProblem } from "./values.js";
|
|
84
84
|
import { validStep } from "./player.js";
|
|
@@ -316,6 +316,64 @@ const TEST_ZH_HANS = {
|
|
|
316
316
|
};
|
|
317
317
|
/** The player's limit of sidebar tabs. */
|
|
318
318
|
const MAX_SIDEBAR_TABS = 8;
|
|
319
|
+
/**
|
|
320
|
+
* The player's refusal of a declaration (commands.rs `CommandError`): code
|
|
321
|
+
* `invalid`, `conflict` or `limit`, its message prefixed as the player's.
|
|
322
|
+
*/
|
|
323
|
+
const commandInvalid = (message) => new HostError("invalid", `invalid command: ${message}`);
|
|
324
|
+
const commandConflict = (message) => new HostError("conflict", `command conflict: ${message}`);
|
|
325
|
+
/**
|
|
326
|
+
* The player's check of a command text (commands.rs `text`): 1 to `limit`
|
|
327
|
+
* UTF-8 bytes, no control character (`char::is_control`: C0, DEL and C1)
|
|
328
|
+
* and no whitespace at either end (`str::trim`: Unicode White_Space).
|
|
329
|
+
*/
|
|
330
|
+
function commandText(value, label, limit) {
|
|
331
|
+
const bytes = Buffer.byteLength(value, "utf8");
|
|
332
|
+
if (bytes === 0 ||
|
|
333
|
+
bytes > limit ||
|
|
334
|
+
/^\p{White_Space}|\p{White_Space}$/u.test(value) ||
|
|
335
|
+
/\p{Cc}/u.test(value))
|
|
336
|
+
throw commandInvalid(`${label} requires 1..=${limit} bytes, no controls or surrounding whitespace`);
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* A menu text (title or path segment) as the player keys it: a string as
|
|
340
|
+
* declared, a text value by its English rendering (capabilities/commands.rs
|
|
341
|
+
* `menu_text`).
|
|
342
|
+
*/
|
|
343
|
+
function menuTextKey(value) {
|
|
344
|
+
if (typeof value === "string")
|
|
345
|
+
return value;
|
|
346
|
+
if (!isTextValue(value))
|
|
347
|
+
throw new HostError("invalid", "title and path segments are strings or texts");
|
|
348
|
+
if ("$vestraList" in value)
|
|
349
|
+
throw new HostError("invalid", "a menu text is a string or a text, not a list");
|
|
350
|
+
const problem = textProblem(value);
|
|
351
|
+
if (problem)
|
|
352
|
+
throw new HostError("invalid", problem);
|
|
353
|
+
return textKey(value);
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* The declaration checks of the player's `Registry::register`, in its
|
|
357
|
+
* order, on the keyed title and path (`@<id>` already replaced).
|
|
358
|
+
*/
|
|
359
|
+
function checkDeclaration(id, title, path, declaration) {
|
|
360
|
+
commandText(id, "localId", COMMAND_LIMITS.idBytes);
|
|
361
|
+
if (!/^[A-Za-z0-9._-]+$/.test(id))
|
|
362
|
+
throw commandInvalid("localId permits ASCII letters, digits, '.', '_' and '-'");
|
|
363
|
+
commandText(title, "title", COMMAND_LIMITS.titleBytes);
|
|
364
|
+
if (path.length > COMMAND_LIMITS.depth)
|
|
365
|
+
throw commandInvalid(`menu path allows at most ${COMMAND_LIMITS.depth} segments`);
|
|
366
|
+
for (const segment of path)
|
|
367
|
+
commandText(segment, "path segment", COMMAND_LIMITS.segmentBytes);
|
|
368
|
+
const group = declaration.group ?? "domain";
|
|
369
|
+
if (typeof group !== "string")
|
|
370
|
+
throw new HostError("invalid", "group is a string");
|
|
371
|
+
commandText(group, "group", COMMAND_LIMITS.groupBytes);
|
|
372
|
+
const order = declaration.order ?? 0;
|
|
373
|
+
if (!Number.isInteger(order) ||
|
|
374
|
+
Math.abs(order) > COMMAND_LIMITS.order)
|
|
375
|
+
throw commandInvalid("order must be within ±1000000");
|
|
376
|
+
}
|
|
319
377
|
/**
|
|
320
378
|
* An in-memory fake Vestra Player for plugin tests.
|
|
321
379
|
*
|
|
@@ -927,8 +985,9 @@ export class TestHost {
|
|
|
927
985
|
/** A command state's reason, refused as the player refuses it. */
|
|
928
986
|
#checkReason(state) {
|
|
929
987
|
const reason = state?.reason;
|
|
930
|
-
|
|
931
|
-
this.#declared(reason, "reason")
|
|
988
|
+
return reason !== null && reason !== undefined
|
|
989
|
+
? this.#declared(reason, "reason")
|
|
990
|
+
: null;
|
|
932
991
|
}
|
|
933
992
|
/**
|
|
934
993
|
* Row patches (player 0.6.0): the `seq` of each property's last write, by
|
|
@@ -2341,19 +2400,36 @@ export class TestHost {
|
|
|
2341
2400
|
["mute", ["Audio", "Mute"], 503, "domain", ["M"], false],
|
|
2342
2401
|
];
|
|
2343
2402
|
const host = this;
|
|
2344
|
-
for (const [id,
|
|
2403
|
+
for (const [id, fixed, order, group, bindings, repeat, radio] of table) {
|
|
2404
|
+
// The seek items carry the step (`setStepSeek`): the player
|
|
2405
|
+
// registers them again when it changes (core_actions.rs `seek_title`).
|
|
2406
|
+
const seek = id === "seek-forward"
|
|
2407
|
+
? "Forward"
|
|
2408
|
+
: id === "seek-backward"
|
|
2409
|
+
? "Back"
|
|
2410
|
+
: null;
|
|
2411
|
+
const pathOf = () => seek === null
|
|
2412
|
+
? fixed
|
|
2413
|
+
: [
|
|
2414
|
+
...fixed.slice(0, -1),
|
|
2415
|
+
`${seek} ${String(Math.round(host.stepSeek * 1000) / 1000)} s`,
|
|
2416
|
+
];
|
|
2417
|
+
const declaration = {
|
|
2418
|
+
order,
|
|
2419
|
+
group,
|
|
2420
|
+
radio: radio ?? false,
|
|
2421
|
+
bindings,
|
|
2422
|
+
repeat,
|
|
2423
|
+
};
|
|
2424
|
+
Object.defineProperties(declaration, {
|
|
2425
|
+
title: { get: () => pathOf().at(-1), enumerable: true },
|
|
2426
|
+
path: { get: pathOf, enumerable: true },
|
|
2427
|
+
});
|
|
2345
2428
|
const command = {
|
|
2346
|
-
declaration
|
|
2347
|
-
title: path.at(-1),
|
|
2348
|
-
path,
|
|
2349
|
-
order,
|
|
2350
|
-
group,
|
|
2351
|
-
radio: radio ?? false,
|
|
2352
|
-
bindings,
|
|
2353
|
-
repeat,
|
|
2354
|
-
},
|
|
2429
|
+
declaration,
|
|
2355
2430
|
// The player's own words in the player's language.
|
|
2356
2431
|
get display() {
|
|
2432
|
+
const path = pathOf();
|
|
2357
2433
|
const leaf = path.at(-1);
|
|
2358
2434
|
const step = /^(Back|Forward) (\S+) s$/.exec(leaf);
|
|
2359
2435
|
const title = step
|
|
@@ -3834,43 +3910,97 @@ export class TestHost {
|
|
|
3834
3910
|
},
|
|
3835
3911
|
"commands.register": (p) => {
|
|
3836
3912
|
const id = String(p.id);
|
|
3837
|
-
// The player's
|
|
3838
|
-
|
|
3839
|
-
|
|
3913
|
+
// The player's checks, in its order (capabilities/commands.rs
|
|
3914
|
+
// `register`, then commands.rs `Registry::register`), with its codes
|
|
3915
|
+
// and messages.
|
|
3916
|
+
if (!Number.isInteger(p.revision) || Number(p.revision) <= 0)
|
|
3917
|
+
throw new HostError("invalid", "positive revision required");
|
|
3918
|
+
// The reason is checked (and shown) when it arrives.
|
|
3919
|
+
const reason = this.#checkReason(p.state);
|
|
3920
|
+
const declaration = p.declaration;
|
|
3921
|
+
const button = declaration.button;
|
|
3922
|
+
const maxOrder = BAR_BUTTON_LIMITS.maxOrder;
|
|
3923
|
+
// Player 0.5.10: a bar button is { icon, order }, its order 0–999,
|
|
3924
|
+
// its icon a name or an image of the plugin.
|
|
3925
|
+
if (button !== undefined && button !== null) {
|
|
3926
|
+
if (typeof button !== "object" ||
|
|
3927
|
+
Array.isArray(button) ||
|
|
3928
|
+
!Object.keys(button).every((key) => key === "icon" || key === "order"))
|
|
3929
|
+
throw new HostError("invalid", "button is { icon, order }");
|
|
3930
|
+
const { icon, order } = button;
|
|
3931
|
+
if (!Number.isInteger(order) || +order < 0 || +order > maxOrder)
|
|
3932
|
+
throw new HostError("invalid", `button order must be an integer 0–${maxOrder}`);
|
|
3933
|
+
if (typeof icon !== "string" && this.imagePath(icon) === null)
|
|
3934
|
+
throw new HostError("invalid", "button icon is an icon name or an image from plugin.ui.image");
|
|
3935
|
+
}
|
|
3936
|
+
// Text values in the title and path are keyed by their English
|
|
3937
|
+
// rendering; the built-in menu ids (MENU_IDS) and the player's old
|
|
3938
|
+
// Chinese menu titles by the menus' keys.
|
|
3939
|
+
const title = menuTextKey(declaration.title);
|
|
3940
|
+
const declared = (declaration.path ?? []);
|
|
3941
|
+
if (!Array.isArray(declared))
|
|
3942
|
+
throw new HostError("invalid", "path is a list of menu texts");
|
|
3943
|
+
const keys = declared.map(menuTextKey);
|
|
3944
|
+
const menu = keys[0]?.startsWith("@") ? keys[0].slice(1) : null;
|
|
3840
3945
|
if (menu !== null && !MENU_IDS.includes(menu))
|
|
3841
|
-
throw
|
|
3842
|
-
if (menu !== null &&
|
|
3843
|
-
throw
|
|
3946
|
+
throw commandInvalid(`unknown menu id @${menu}; the built-in menus are ${MENU_IDS.map((known) => `@${known}`).join(", ")}`);
|
|
3947
|
+
if (menu !== null && keys.length < 2)
|
|
3948
|
+
throw commandInvalid(`menu id @${menu} needs an item after it`);
|
|
3949
|
+
const path = menuKeys(keys);
|
|
3950
|
+
checkDeclaration(id, title, path, declaration);
|
|
3951
|
+
if (button !== undefined && button !== null) {
|
|
3952
|
+
if (path.length === 0)
|
|
3953
|
+
throw commandInvalid("button needs a menu path: a bar button is also a menu item");
|
|
3954
|
+
const { icon } = button;
|
|
3955
|
+
if (typeof icon === "string" &&
|
|
3956
|
+
!BAR_BUTTON_ICONS.includes(icon))
|
|
3957
|
+
throw commandInvalid(`unknown button icon ${JSON.stringify(icon)}: a name of @vestra/icons.slint (${BAR_BUTTON_ICONS.join(", ")}) or an image from plugin.ui.image`);
|
|
3958
|
+
}
|
|
3959
|
+
// The binding table's `Table::canonical` (player 0.6.1): each binding
|
|
3960
|
+
// parsed, and each input once, compared parsed, before the reason,
|
|
3961
|
+
// conflicts and limits.
|
|
3962
|
+
const bindings = (declaration.bindings ?? []);
|
|
3963
|
+
const seen = new Set();
|
|
3964
|
+
for (const binding of bindings.map(String)) {
|
|
3965
|
+
let input;
|
|
3966
|
+
try {
|
|
3967
|
+
input = canonical(binding);
|
|
3968
|
+
}
|
|
3969
|
+
catch (error) {
|
|
3970
|
+
throw commandInvalid(error.message);
|
|
3971
|
+
}
|
|
3972
|
+
if (seen.has(input))
|
|
3973
|
+
throw commandInvalid(`binding ${binding} is listed twice`);
|
|
3974
|
+
seen.add(input);
|
|
3975
|
+
}
|
|
3976
|
+
if (reason !== null)
|
|
3977
|
+
commandText(reason, "reason", COMMAND_LIMITS.reasonBytes);
|
|
3844
3978
|
if (this.commands.has(id))
|
|
3845
|
-
throw
|
|
3846
|
-
// The player's
|
|
3979
|
+
throw commandConflict("owner/localId already registered");
|
|
3980
|
+
// The player's own commands beside the core actions (host/actions.rs
|
|
3981
|
+
// `host_declarations`: Toggle Fullscreen, Quit and three outside the
|
|
3982
|
+
// menu), which take part in conflicts and the total like any other.
|
|
3983
|
+
const hostOnly = BUILTIN_COMMANDS.filter((row) => row.owner === "host" && !this.coreCommands.has(`host/${row.id}`));
|
|
3984
|
+
// A path may not repeat another command's (the player's own
|
|
3985
|
+
// included) nor be a submenu of one, or one of its submenus.
|
|
3986
|
+
if (path.length > 0)
|
|
3987
|
+
for (const existing of [
|
|
3988
|
+
...hostOnly.map((row) => row.path),
|
|
3989
|
+
...[...this.coreCommands.values()].map((command) => command.declaration.path),
|
|
3990
|
+
...[...this.commands.values()].map((command) => menuKeys((command.declaration.path ?? []).map(menuTextKey))),
|
|
3991
|
+
]) {
|
|
3992
|
+
const shared = Math.min(existing.length, path.length);
|
|
3993
|
+
if (existing.length > 0 &&
|
|
3994
|
+
existing.slice(0, shared).every((s, i) => s === path[i]))
|
|
3995
|
+
throw commandConflict("menu path duplicates a command or collides with a leaf/group");
|
|
3996
|
+
}
|
|
3997
|
+
// The player's caps (COMMAND_LIMITS): the total counts all of the
|
|
3998
|
+
// player's own commands too.
|
|
3847
3999
|
if (this.commands.size >= COMMAND_LIMITS.perPlugin ||
|
|
3848
|
-
this.commands.size + this.coreCommands.size
|
|
4000
|
+
this.commands.size + this.coreCommands.size + hostOnly.length >=
|
|
4001
|
+
COMMAND_LIMITS.total)
|
|
3849
4002
|
throw new HostError("limit", "command registry limit reached");
|
|
3850
4003
|
const host = this;
|
|
3851
|
-
const declaration = p.declaration;
|
|
3852
|
-
// Player 0.5.10: a bar button needs a path, an order 0–999 and a
|
|
3853
|
-
// known icon or an image of the plugin.
|
|
3854
|
-
if (declaration.button !== undefined) {
|
|
3855
|
-
const { icon, order } = declaration.button;
|
|
3856
|
-
if (path.length === 0)
|
|
3857
|
-
throw new HostError("invalid", "button needs a menu path");
|
|
3858
|
-
if (!Number.isInteger(order) ||
|
|
3859
|
-
order < 0 ||
|
|
3860
|
-
order > BAR_BUTTON_LIMITS.maxOrder)
|
|
3861
|
-
throw new HostError("invalid", `button order must be an integer 0–${BAR_BUTTON_LIMITS.maxOrder}`);
|
|
3862
|
-
if (typeof icon === "string") {
|
|
3863
|
-
if (!BAR_BUTTON_ICONS.includes(icon))
|
|
3864
|
-
throw new HostError("invalid", `unknown button icon "${icon}"`);
|
|
3865
|
-
}
|
|
3866
|
-
else if (this.imagePath(icon) === null)
|
|
3867
|
-
throw new HostError("invalid", "button icon is an icon name or an image from plugin.ui.image");
|
|
3868
|
-
}
|
|
3869
|
-
// Like the player: the menu texts and the reason are checked (and
|
|
3870
|
-
// shown) when they arrive.
|
|
3871
|
-
for (const segment of [declaration.title, ...(declaration.path ?? [])])
|
|
3872
|
-
this.#declared(segment, "a menu text");
|
|
3873
|
-
this.#checkReason(p.state);
|
|
3874
4004
|
let sent = p.state;
|
|
3875
4005
|
this.commands.set(id, {
|
|
3876
4006
|
declaration,
|
|
@@ -3899,10 +4029,25 @@ export class TestHost {
|
|
|
3899
4029
|
throw new HostError("missing", `command ${p.id} is not registered`);
|
|
3900
4030
|
},
|
|
3901
4031
|
"commands.update": (p) => {
|
|
3902
|
-
|
|
4032
|
+
// The request itself, as the player reads it (capabilities/commands.rs
|
|
4033
|
+
// `parsed`): `invalid`.
|
|
4034
|
+
if (!Number.isInteger(p.revision) || Number(p.revision) <= 0)
|
|
4035
|
+
throw new HostError("invalid", "positive revision required");
|
|
4036
|
+
const reason = this.#checkReason(p.state);
|
|
4037
|
+
// The registry's refusals of an update (commands.rs `Registry::update`)
|
|
4038
|
+
// all come back as `command`; the test host's one plugin is owner 1.
|
|
4039
|
+
const command = this.commands.get(String(p.id));
|
|
4040
|
+
if (!command)
|
|
4041
|
+
throw new HostError("command", `command missing: owner 1 / ${p.id}`);
|
|
3903
4042
|
if (Number(p.revision) <= command.revision)
|
|
3904
|
-
throw new HostError("command", `stale command revision ${p.revision}`);
|
|
3905
|
-
|
|
4043
|
+
throw new HostError("command", `stale command revision ${p.revision}; current is ${command.revision}`);
|
|
4044
|
+
if (reason !== null)
|
|
4045
|
+
try {
|
|
4046
|
+
commandText(reason, "reason", COMMAND_LIMITS.reasonBytes);
|
|
4047
|
+
}
|
|
4048
|
+
catch (error) {
|
|
4049
|
+
throw new HostError("command", error.message);
|
|
4050
|
+
}
|
|
3906
4051
|
command.revision = Number(p.revision);
|
|
3907
4052
|
command.state = p.state;
|
|
3908
4053
|
if (p.when)
|
|
@@ -5283,8 +5428,11 @@ function tellResidency(host) {
|
|
|
5283
5428
|
*
|
|
5284
5429
|
* Rejects when `manifest.json` cannot be read or parsed, when `activate`
|
|
5285
5430
|
* throws, or when a registration made during activation fails (for example
|
|
5286
|
-
* `limit`, `conflict`, `invalid`)
|
|
5287
|
-
*
|
|
5431
|
+
* `limit`, `conflict`, `invalid`). Stricter than the player's runner for
|
|
5432
|
+
* commands (SDK 1.5.49): there the SDK logs a command declaration the
|
|
5433
|
+
* player refuses during activation, skips it and activates the rest of the
|
|
5434
|
+
* plugin (`onError` hears it); `startPlugin` fails, so a test shows the
|
|
5435
|
+
* bad declaration.
|
|
5288
5436
|
*
|
|
5289
5437
|
* @param activate - The plugin's default-exported `activate` function.
|
|
5290
5438
|
* @param options - Where the plugin lives and how to start it.
|
|
@@ -5369,7 +5517,10 @@ export async function startPlugin(activate, options = {}) {
|
|
|
5369
5517
|
const instance = await createPlugin(host, manifest, directory, data,
|
|
5370
5518
|
// Like the player: protocol additions it takes (`app.features` set on
|
|
5371
5519
|
// the host replaces them, e.g. `[]` for an older player's behaviour).
|
|
5372
|
-
{ features: [PATCH_FEATURE], ...host.app }
|
|
5520
|
+
{ features: [PATCH_FEATURE], ...host.app },
|
|
5521
|
+
// Unlike the player's runner, which logs a refused command declaration
|
|
5522
|
+
// and activates the rest: a plugin's tests show the refusal.
|
|
5523
|
+
{ strictDeclarations: true });
|
|
5373
5524
|
await activate(instance.plugin);
|
|
5374
5525
|
// Like the player: a position or resolver another plugin holds is not an
|
|
5375
5526
|
// activation failure.
|
package/dist/plugin/types.d.ts
CHANGED
|
@@ -487,7 +487,11 @@ export interface Scope extends Disposable {
|
|
|
487
487
|
* Registrations started during activation (commands, panels, resolvers,
|
|
488
488
|
* subscriptions) do not need to be awaited: the plugin is reported ready
|
|
489
489
|
* once they all succeed, and activation fails if `activate` throws or any
|
|
490
|
-
* of them fails.
|
|
490
|
+
* of them fails. A command declaration the player refuses is the exception
|
|
491
|
+
* (SDK 1.5.49): the SDK logs and skips it, its `ready` rejects and
|
|
492
|
+
* {@link Scope.onError} hears it, the rest of the plugin activates
|
|
493
|
+
* (`startPlugin` still fails on it). The entry module must
|
|
494
|
+
* **default-export** `activate`.
|
|
491
495
|
*
|
|
492
496
|
* Stopping: the player first revokes everything the plugin owns on its side
|
|
493
497
|
* (menus, windows, panels, filters, subtitle layers, snapshots, jobs,
|
|
@@ -917,7 +921,9 @@ export interface CommandButton {
|
|
|
917
921
|
* give it an order in the menu's range (Video: 400–499).
|
|
918
922
|
*
|
|
919
923
|
* {@link Commands.add} throws `invalid` synchronously for a bad path,
|
|
920
|
-
* order
|
|
924
|
+
* order or binding; the player refuses an empty or overlong text, a control
|
|
925
|
+
* character or whitespace at either end (`COMMAND_LIMITS`) with
|
|
926
|
+
* `invalid` (through `ready`). The same path registered twice, or a path that
|
|
921
927
|
* is both an item and a submenu, fails with `conflict` (through `ready`).
|
|
922
928
|
*
|
|
923
929
|
* @see {@link Commands.add}, [Commands, menus and key bindings](../../docs/explanation/commands.md)
|
|
@@ -1136,8 +1142,21 @@ export interface Command extends Disposable {
|
|
|
1136
1142
|
* Resolves when the host has registered the command. Rejects with
|
|
1137
1143
|
* `conflict` (path already taken), `invalid` or `limit` (more than 160
|
|
1138
1144
|
* commands in this plugin, or 1024 in the whole player). You rarely need
|
|
1139
|
-
* to await it:
|
|
1140
|
-
*
|
|
1145
|
+
* to await it: the refusal goes to {@link Scope.onError}. During
|
|
1146
|
+
* activation the SDK, in the plugin's process, logs the refusal with the
|
|
1147
|
+
* command id and the player's message, skips the command and lets the
|
|
1148
|
+
* rest of the plugin activate (SDK 1.5.49; before, the refusal failed
|
|
1149
|
+
* activation); the error reaches `onError` once activation completes,
|
|
1150
|
+
* so a listener added anywhere in `activate` hears it. `startPlugin`
|
|
1151
|
+
* stays strict: there a refusal during activation fails it, so tests
|
|
1152
|
+
* show the bad declaration. Any other failure during activation
|
|
1153
|
+
* (`capability`, no answer) fails activation.
|
|
1154
|
+
*
|
|
1155
|
+
* Only the player's answer is skipped. What {@link Commands.add} checks
|
|
1156
|
+
* itself still throws from `add` (an unknown `@` menu id, more than 8
|
|
1157
|
+
* path segments, an `order` out of range, a malformed binding or one
|
|
1158
|
+
* listed twice, a bad `button`, a reason text of the wrong shape, an id
|
|
1159
|
+
* already declared), and fails activation unless the plugin catches it.
|
|
1141
1160
|
*
|
|
1142
1161
|
* @throws `conflict` (rejects) Another command already has this menu path,
|
|
1143
1162
|
* or the path would be both a menu item and a submenu.
|
|
@@ -1159,8 +1178,9 @@ export interface Command extends Disposable {
|
|
|
1159
1178
|
* when the user picks the item, presses a key or clicks the button.
|
|
1160
1179
|
* Commands of all plugins merge into the player's menus by their English
|
|
1161
1180
|
* path keys. Registration is asynchronous: `add` returns at once, and
|
|
1162
|
-
* {@link Command.ready} reports the player's answer (during activation
|
|
1163
|
-
* refusal
|
|
1181
|
+
* {@link Command.ready} reports the player's answer (during activation
|
|
1182
|
+
* the SDK logs a refusal and skips that command, SDK 1.5.49;
|
|
1183
|
+
* `startPlugin` fails on it). What `add` checks itself throws from `add`.
|
|
1164
1184
|
*
|
|
1165
1185
|
* @capability commands
|
|
1166
1186
|
*
|
|
@@ -1190,8 +1210,15 @@ export interface Commands {
|
|
|
1190
1210
|
* lists, presets) in a child {@link Scope} and rebuild it.
|
|
1191
1211
|
*
|
|
1192
1212
|
* @throws `invalid` synchronously for a malformed path (including an
|
|
1193
|
-
* unknown `@` menu id), order
|
|
1213
|
+
* unknown `@` menu id), order or binding; `conflict` for an id this
|
|
1194
1214
|
* plugin has already declared and not disposed.
|
|
1215
|
+
* @throws `invalid` (`ready` rejects; during activation, activation
|
|
1216
|
+
* fails) for a text the player refuses: an id, title, path segment,
|
|
1217
|
+
* group or reason that is empty, longer than its `COMMAND_LIMITS`
|
|
1218
|
+
* bytes (a text value counted by its English rendering), has a control
|
|
1219
|
+
* character or whitespace at either end, or an id with other characters
|
|
1220
|
+
* than ASCII letters, digits, `.`, `_` and `-`. `TestHost`
|
|
1221
|
+
* refuses the same (SDK 1.5.48).
|
|
1195
1222
|
*
|
|
1196
1223
|
* @example Pause playback from a menu item
|
|
1197
1224
|
* Playback ▸ Pause (and Ctrl+P) pauses and resumes, shows a check mark
|
package/llms-full.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @vestra-player/sdk 1.5.
|
|
1
|
+
# @vestra-player/sdk 1.5.49: full reference
|
|
2
2
|
|
|
3
3
|
> Everything needed to write a Vestra Player plugin (plugin API `vestra/1`): the guides, every public declaration of `@vestra-player/sdk` and `@vestra-player/sdk/testing` with its documentation, and the complete source of the example plugins. Generated from the SDK's sources by scripts/llms.mjs; the short index is llms.txt.
|
|
4
4
|
|
|
@@ -2357,12 +2357,19 @@ The player bounds what one request may cost it. The limits are guards against ru
|
|
|
2357
2357
|
|
|
2358
2358
|
#### Commands
|
|
2359
2359
|
|
|
2360
|
-
A command added past
|
|
2360
|
+
A command added past `perPlugin` or `total` fails with code `limit` (its `ready` rejects and `onError` hears it; during activation the SDK logs and skips that command and the rest of the plugin activates, SDK 1.5.49, while `startPlugin` fails). Commands already registered are unaffected, and disposing one frees its slot at once. A declaration past the other limits fails with `invalid` the same way (SDK 1.5.48: `TestHost` refuses it too).
|
|
2361
2361
|
|
|
2362
2362
|
| Limit | Value | Meaning |
|
|
2363
2363
|
| --- | --- | --- |
|
|
2364
2364
|
| `COMMAND_LIMITS.perPlugin` | 160 | Commands one plugin instance may have at the same time. |
|
|
2365
2365
|
| `COMMAND_LIMITS.total` | 1024 | Commands of every plugin plus the player's own, together. |
|
|
2366
|
+
| `COMMAND_LIMITS.depth` | 8 | Segments of a menu path, the leaf included. |
|
|
2367
|
+
| `COMMAND_LIMITS.idBytes` | 128 | UTF-8 bytes of a command's local id: ASCII letters, digits, `.`, `_` and `-`. |
|
|
2368
|
+
| `COMMAND_LIMITS.titleBytes` | 128 | UTF-8 bytes of a title, a text value counted by its English rendering. Like every command text: not empty, no control characters, no whitespace at either end. |
|
|
2369
|
+
| `COMMAND_LIMITS.segmentBytes` | 80 | UTF-8 bytes of a menu path segment, a text value counted by its English rendering. |
|
|
2370
|
+
| `COMMAND_LIMITS.groupBytes` | 80 | UTF-8 bytes of a group. |
|
|
2371
|
+
| `COMMAND_LIMITS.reasonBytes` | 512 | UTF-8 bytes of a state's reason, as shown in the player's language. |
|
|
2372
|
+
| `COMMAND_LIMITS.order` | 1000000 | The largest `order` either way: an order is within ±1,000,000. |
|
|
2366
2373
|
|
|
2367
2374
|
#### Bottom-bar buttons
|
|
2368
2375
|
|
|
@@ -3834,6 +3841,10 @@ When the player starts the plugin:
|
|
|
3834
3841
|
|
|
3835
3842
|
You do not need to `await` those registrations. `commands.add`, `panels.mount` and `resolvers.add` return their handle at once and register in the background; their `ready` promise is there for the rare case that you want to wait. **Activation fails** when `activate` throws or rejects, or when one of those registrations fails. A registration made later, after activation, cannot fail activation any more: its failure goes to `onError`.
|
|
3836
3843
|
|
|
3844
|
+
A command declaration the player refuses (`invalid`, `conflict` or `limit`: a bad text, a path another command has, too many commands) does not fail activation either (SDK 1.5.49). The SDK, in the plugin's process, logs it with the command id and the player's message, skips that command and lets the rest of the plugin activate, like VS Code with a bad contribution; the command's `ready` rejects and `onError` hears the refusal once activation completes. `startPlugin` stays strict: there the refusal fails the start, so your tests show the bad declaration.
|
|
3845
|
+
|
|
3846
|
+
This covers only the player's answer. What `commands.add` checks itself still throws from `add`: an unknown `@` menu id, more than 8 path segments, an `order` out of range, a malformed binding or one listed twice, a bad `button`, a reason text of the wrong shape, an id already declared. Uncaught, such a throw fails activation like any other exception in `activate`.
|
|
3847
|
+
|
|
3837
3848
|
Some refusals are deliberately not failures, because they depend on which other plugins are installed: a bottom bar or backdrop that another plugin's panel already holds, the next/previous sequence that another plugin provides, a file type or web host that another plugin already resolves. The plugin keeps running without that part, the player tells the user, and the plugin manager shows it as a warning (PluginInfo.warnings). The player's built-in plugins register first, so they win these contests at start-up.
|
|
3838
3849
|
|
|
3839
3850
|
#### Handles and scopes
|
|
@@ -4572,6 +4583,7 @@ A tool window (Window), a panel (Panel) and a settings page (SettingsPage) all s
|
|
|
4572
4583
|
- **A panel or settings page file exports one component and one global.** The player composes your component into a window it owns (the main window, the floating sidebar window, the Options window), so the component instance is not a stable address: a new instance is made whenever the main window is rebuilt or the sidebar moves to or from its floating window. The **global** is the bridge: properties and callbacks are declared on it, the component reads and calls them (`ListBridge.rows`, `ListBridge.select(index)`), and the plugin talks to the global. The SDK finds the two names from the file's `export component` and `export global` lines.
|
|
4573
4584
|
|
|
4574
4585
|
```slint
|
|
4586
|
+
import { ListView } from "std-widgets.slint";
|
|
4575
4587
|
import { Theme } from "@vestra/theme.slint";
|
|
4576
4588
|
|
|
4577
4589
|
export global ListBridge {
|
|
@@ -4584,7 +4596,9 @@ export global ListBridge {
|
|
|
4584
4596
|
export component ListPanel inherits Rectangle {
|
|
4585
4597
|
background: Theme.bg-list;
|
|
4586
4598
|
min-width: 200px;
|
|
4587
|
-
|
|
4599
|
+
// A ListView builds only the rows in view and scrolls the rest, so the
|
|
4600
|
+
// list costs the same with three rows or thirty thousand.
|
|
4601
|
+
ListView {
|
|
4588
4602
|
for row[index] in ListBridge.rows: Rectangle {
|
|
4589
4603
|
height: 20px;
|
|
4590
4604
|
background: index == ListBridge.selected ? Theme.selected-bg : transparent;
|
|
@@ -4607,9 +4621,6 @@ export component ListPanel inherits Rectangle {
|
|
|
4607
4621
|
}
|
|
4608
4622
|
}
|
|
4609
4623
|
}
|
|
4610
|
-
// Takes the space below the rows. Without it the layout's maximum
|
|
4611
|
-
// height (three 20px rows) would cap the player's main window.
|
|
4612
|
-
Rectangle { vertical-stretch: 1; }
|
|
4613
4624
|
}
|
|
4614
4625
|
}
|
|
4615
4626
|
```
|
|
@@ -5176,7 +5187,7 @@ self.activations.begin(owner, self.now_ms() + 15_000)?;
|
|
|
5176
5187
|
|
|
5177
5188
|
Source: src/host/src/packages/package_manager.rs
|
|
5178
5189
|
|
|
5179
|
-
`boot.ts` sends `runtime.ready` only after `activate` returned and every registration started during activation succeeded (a slot, sequence or resolver another plugin holds does not count as a failure). Otherwise it sends `runtime.failed` with a `code`. A failure or a missed deadline stops the plugin, and the plugin manager lists it as failed with the message. At start-up the installed plugins are started from a queue, about 25 ms of process spawning per UI tick, so the window stays responsive while they come up.
|
|
5190
|
+
`boot.ts` sends `runtime.ready` only after `activate` returned and every registration started during activation succeeded (a slot, sequence or resolver another plugin holds does not count as a failure, and the SDK skips a command declaration the player refuses with `invalid`, `conflict` or `limit`: it logs it and reports it to `onError`, SDK 1.5.49). Otherwise it sends `runtime.failed` with a `code`. A failure or a missed deadline stops the plugin, and the plugin manager lists it as failed with the message. At start-up the installed plugins are started from a queue, about 25 ms of process spawning per UI tick, so the window stays responsive while they come up.
|
|
5180
5191
|
|
|
5181
5192
|
#### The wire protocol
|
|
5182
5193
|
|
|
@@ -5484,6 +5495,7 @@ Use the layout of Theme: small adjustment windows: the same width, row height, l
|
|
|
5484
5495
|
- **No status or path lines in a panel.** Messages go to the OSD; a full path is a tooltip on its row.
|
|
5485
5496
|
- **Show state with colour, not words.** As in the player's playlist: what is playing in `Theme.playing` (with a thin bar at the left, Theme: layouts), what was played in `Theme.text-played`, the selection on `Theme.selected-bg`. A failed item gets the `failed` icon at the end of its row and an OSD message.
|
|
5486
5497
|
- **Lists are plain**: single-line rows, background `Theme.bg-list`, no cards, no stripes, no rounded rows; scroll bars come from `std-widgets.slint` so they follow the palette.
|
|
5498
|
+
- **Long lists use `ListView`.** A list whose length the user decides (a playlist, search results, files, messages) goes in a `ListView` from `std-widgets.slint`, with the `for` inside it: it creates only the rows in view, so a list of thousands of rows scrolls as smoothly as one of ten. A `for` in a layout builds every row up front. Keep a bare `for` for a short list of a fixed size, such as a row of buttons. Example: Panel menu is a panel list drawn this way.
|
|
5487
5499
|
- **Commands of a panel go into its right-click menu**, made of the plugin's commands (panel.showMenu); see Menus.
|
|
5488
5500
|
- **The bottom bar is icons only**: no text buttons, no source path, no status text (the source name is in the window title, status is an OSD message).
|
|
5489
5501
|
|
|
@@ -5820,6 +5832,8 @@ Every SDK release's CHANGELOG entry opens with who must change and what happens
|
|
|
5820
5832
|
|
|
5821
5833
|
| Release | Who must change | Guide |
|
|
5822
5834
|
| --- | --- | --- |
|
|
5835
|
+
| [1.5.49](https://github.com/vestrasys/vestra-player-sdk/blob/main/CHANGELOG.md#1549) | **Nobody.** No changes to the plugin API (`@vestra-player/sdk/runtime` gains the additive `RuntimeOptions` and an optional sixth `createPlugin` argument), `apiVersion` stays `vestra/1`, no new capability, no player version needed, and the player's runner needs no change. | — |
|
|
5836
|
+
| [1.5.48](https://github.com/vestrasys/vestra-player-sdk/blob/main/CHANGELOG.md#1548) | **Nobody, but plugin tests may now fail.** No changes to the TypeScript API beyond new fields of `COMMAND_LIMITS`, `apiVersion` stays `vestra/1`, no new capability, no player version needed. | — |
|
|
5823
5837
|
| [1.5.47](https://github.com/vestrasys/vestra-player-sdk/blob/main/CHANGELOG.md#1547) | **Nobody.** No changes to the TypeScript API, `apiVersion` stays `vestra/1`, no new capability, no player version needed. | — |
|
|
5824
5838
|
| [1.5.46](https://github.com/vestrasys/vestra-player-sdk/blob/main/CHANGELOG.md#1546) | **Nobody.** No changes to the TypeScript API, `apiVersion` stays `vestra/1`, no new capability. | — |
|
|
5825
5839
|
| [1.5.45](https://github.com/vestrasys/vestra-player-sdk/blob/main/CHANGELOG.md#1545) | **Nobody.** No changes to the TypeScript API, `apiVersion` stays `vestra/1`, no new capability, no player version needed. | — |
|
|
@@ -6304,7 +6318,11 @@ initial player, window and timeline state, then calls `activate(plugin)`.
|
|
|
6304
6318
|
Registrations started during activation (commands, panels, resolvers,
|
|
6305
6319
|
subscriptions) do not need to be awaited: the plugin is reported ready
|
|
6306
6320
|
once they all succeed, and activation fails if `activate` throws or any
|
|
6307
|
-
of them fails.
|
|
6321
|
+
of them fails. A command declaration the player refuses is the exception
|
|
6322
|
+
(SDK 1.5.49): the SDK logs and skips it, its `ready` rejects and
|
|
6323
|
+
Scope.onError hears it, the rest of the plugin activates
|
|
6324
|
+
(`startPlugin` still fails on it). The entry module must
|
|
6325
|
+
**default-export** `activate`.
|
|
6308
6326
|
|
|
6309
6327
|
Stopping: the player first revokes everything the plugin owns on its side
|
|
6310
6328
|
(menus, windows, panels, filters, subtitle layers, snapshots, jobs,
|
|
@@ -6701,7 +6719,9 @@ an item without `order` moves the whole menu to the top; to avoid that,
|
|
|
6701
6719
|
give it an order in the menu's range (Video: 400–499).
|
|
6702
6720
|
|
|
6703
6721
|
Commands.add throws `invalid` synchronously for a bad path,
|
|
6704
|
-
order
|
|
6722
|
+
order or binding; the player refuses an empty or overlong text, a control
|
|
6723
|
+
character or whitespace at either end (`COMMAND_LIMITS`) with
|
|
6724
|
+
`invalid` (through `ready`). The same path registered twice, or a path that
|
|
6705
6725
|
is both an item and a submenu, fails with `conflict` (through `ready`).
|
|
6706
6726
|
|
|
6707
6727
|
```ts
|
|
@@ -6861,7 +6881,24 @@ interface Command extends Disposable {
|
|
|
6861
6881
|
checked: Bindable<boolean | null> | Condition;
|
|
6862
6882
|
}>): void;
|
|
6863
6883
|
/**
|
|
6864
|
-
* Resolves when the host has registered the command. Rejects with
|
|
6884
|
+
* Resolves when the host has registered the command. Rejects with
|
|
6885
|
+
* `conflict` (path already taken), `invalid` or `limit` (more than 160
|
|
6886
|
+
* commands in this plugin, or 1024 in the whole player). You rarely need
|
|
6887
|
+
* to await it: the refusal goes to Scope.onError. During
|
|
6888
|
+
* activation the SDK, in the plugin's process, logs the refusal with the
|
|
6889
|
+
* command id and the player's message, skips the command and lets the
|
|
6890
|
+
* rest of the plugin activate (SDK 1.5.49; before, the refusal failed
|
|
6891
|
+
* activation); the error reaches `onError` once activation completes,
|
|
6892
|
+
* so a listener added anywhere in `activate` hears it. `startPlugin`
|
|
6893
|
+
* stays strict: there a refusal during activation fails it, so tests
|
|
6894
|
+
* show the bad declaration. Any other failure during activation
|
|
6895
|
+
* (`capability`, no answer) fails activation.
|
|
6896
|
+
*
|
|
6897
|
+
* Only the player's answer is skipped. What Commands.add checks
|
|
6898
|
+
* itself still throws from `add` (an unknown `@` menu id, more than 8
|
|
6899
|
+
* path segments, an `order` out of range, a malformed binding or one
|
|
6900
|
+
* listed twice, a bad `button`, a reason text of the wrong shape, an id
|
|
6901
|
+
* already declared), and fails activation unless the plugin catches it.
|
|
6865
6902
|
* @throws conflict (rejects) Another command already has this menu path, or the path would be both a menu item and a submenu.
|
|
6866
6903
|
* @throws capability (rejects) The manifest does not declare `commands`.
|
|
6867
6904
|
* @throws invalid (rejects) The player refused the declaration.
|
|
@@ -6898,8 +6935,9 @@ the menu, the binding table and the user's key overrides: it calls `run`
|
|
|
6898
6935
|
when the user picks the item, presses a key or clicks the button.
|
|
6899
6936
|
Commands of all plugins merge into the player's menus by their English
|
|
6900
6937
|
path keys. Registration is asynchronous: `add` returns at once, and
|
|
6901
|
-
Command.ready reports the player's answer (during activation
|
|
6902
|
-
refusal
|
|
6938
|
+
Command.ready reports the player's answer (during activation
|
|
6939
|
+
the SDK logs a refusal and skips that command, SDK 1.5.49;
|
|
6940
|
+
`startPlugin` fails on it). What `add` checks itself throws from `add`.
|
|
6903
6941
|
|
|
6904
6942
|
- @capability commands
|
|
6905
6943
|
|
|
@@ -6911,7 +6949,8 @@ interface Commands {
|
|
|
6911
6949
|
*
|
|
6912
6950
|
* Titles and paths cannot change in place: put changing items (track
|
|
6913
6951
|
* lists, presets) in a child Scope and rebuild it.
|
|
6914
|
-
* @throws invalid synchronously for a malformed path (including an unknown `@` menu id), order
|
|
6952
|
+
* @throws invalid synchronously for a malformed path (including an unknown `@` menu id), order or binding; `conflict` for an id this plugin has already declared and not disposed.
|
|
6953
|
+
* @throws invalid (`ready` rejects; during activation, activation fails) for a text the player refuses: an id, title, path segment, group or reason that is empty, longer than its `COMMAND_LIMITS` bytes (a text value counted by its English rendering), has a control character or whitespace at either end, or an id with other characters than ASCII letters, digits, `.`, `_` and `-`. `TestHost` refuses the same (SDK 1.5.48).
|
|
6915
6954
|
* @capability commands
|
|
6916
6955
|
* @example Pause playback from a menu item
|
|
6917
6956
|
* Playback ▸ Pause (and Ctrl+P) pauses and resumes, shows a check mark
|
|
@@ -8453,6 +8492,7 @@ panel.on("contextMenu", (x: number, y: number) =>
|
|
|
8453
8492
|
`ui/panel.slint`:
|
|
8454
8493
|
|
|
8455
8494
|
```slint
|
|
8495
|
+
import { ListView } from "std-widgets.slint";
|
|
8456
8496
|
import { Theme } from "@vestra/theme.slint";
|
|
8457
8497
|
|
|
8458
8498
|
export global ListBridge {
|
|
@@ -8465,7 +8505,9 @@ export global ListBridge {
|
|
|
8465
8505
|
export component ListPanel inherits Rectangle {
|
|
8466
8506
|
background: Theme.bg-list;
|
|
8467
8507
|
min-width: 200px;
|
|
8468
|
-
|
|
8508
|
+
// A ListView builds only the rows in view and scrolls the rest, so the
|
|
8509
|
+
// list costs the same with three rows or thirty thousand.
|
|
8510
|
+
ListView {
|
|
8469
8511
|
for row[index] in ListBridge.rows: Rectangle {
|
|
8470
8512
|
height: 20px;
|
|
8471
8513
|
background: index == ListBridge.selected ? Theme.selected-bg : transparent;
|
|
@@ -8488,9 +8530,6 @@ export component ListPanel inherits Rectangle {
|
|
|
8488
8530
|
}
|
|
8489
8531
|
}
|
|
8490
8532
|
}
|
|
8491
|
-
// Takes the space below the rows. Without it the layout's maximum
|
|
8492
|
-
// height (three 20px rows) would cap the player's main window.
|
|
8493
|
-
Rectangle { vertical-stretch: 1; }
|
|
8494
8533
|
}
|
|
8495
8534
|
}
|
|
8496
8535
|
```
|
|
@@ -9392,6 +9431,7 @@ panel.bind(() => ({ rows: items, selected }));
|
|
|
9392
9431
|
`ui/panel.slint`:
|
|
9393
9432
|
|
|
9394
9433
|
```slint
|
|
9434
|
+
import { ListView } from "std-widgets.slint";
|
|
9395
9435
|
import { Theme } from "@vestra/theme.slint";
|
|
9396
9436
|
|
|
9397
9437
|
export global ListBridge {
|
|
@@ -9404,7 +9444,9 @@ export global ListBridge {
|
|
|
9404
9444
|
export component ListPanel inherits Rectangle {
|
|
9405
9445
|
background: Theme.bg-list;
|
|
9406
9446
|
min-width: 200px;
|
|
9407
|
-
|
|
9447
|
+
// A ListView builds only the rows in view and scrolls the rest, so the
|
|
9448
|
+
// list costs the same with three rows or thirty thousand.
|
|
9449
|
+
ListView {
|
|
9408
9450
|
for row[index] in ListBridge.rows: Rectangle {
|
|
9409
9451
|
height: 20px;
|
|
9410
9452
|
background: index == ListBridge.selected ? Theme.selected-bg : transparent;
|
|
@@ -9427,9 +9469,6 @@ export component ListPanel inherits Rectangle {
|
|
|
9427
9469
|
}
|
|
9428
9470
|
}
|
|
9429
9471
|
}
|
|
9430
|
-
// Takes the space below the rows. Without it the layout's maximum
|
|
9431
|
-
// height (three 20px rows) would cap the player's main window.
|
|
9432
|
-
Rectangle { vertical-stretch: 1; }
|
|
9433
9472
|
}
|
|
9434
9473
|
}
|
|
9435
9474
|
```
|
|
@@ -17140,8 +17179,11 @@ undeclared capability fail with `capability`, as in the player.
|
|
|
17140
17179
|
|
|
17141
17180
|
Rejects when `manifest.json` cannot be read or parsed, when `activate`
|
|
17142
17181
|
throws, or when a registration made during activation fails (for example
|
|
17143
|
-
`limit`, `conflict`, `invalid`)
|
|
17144
|
-
|
|
17182
|
+
`limit`, `conflict`, `invalid`). Stricter than the player's runner for
|
|
17183
|
+
commands (SDK 1.5.49): there the SDK logs a command declaration the
|
|
17184
|
+
player refuses during activation, skips it and activates the rest of the
|
|
17185
|
+
plugin (`onError` hears it); `startPlugin` fails, so a test shows the
|
|
17186
|
+
bad declaration.
|
|
17145
17187
|
|
|
17146
17188
|
The way into a plugin's tests: one call starts the plugin the way the
|
|
17147
17189
|
player's runner does and returns the host to drive, the `plugin` object
|
|
@@ -17314,6 +17356,9 @@ fails; later the failure goes to `Scope.onError`), commands
|
|
|
17314
17356
|
already registered are unaffected, and disposing a command frees its slot
|
|
17315
17357
|
at once. TestHost enforces the same caps.
|
|
17316
17358
|
|
|
17359
|
+
The other fields are the player's rules for one declaration (since
|
|
17360
|
+
1.5.48): a command past them is refused with `invalid`, the same way.
|
|
17361
|
+
|
|
17317
17362
|
For menus built from media content (tracks, chapters, presets), set your
|
|
17318
17363
|
own sensible display cap well below `perPlugin`.
|
|
17319
17364
|
|
|
@@ -17323,6 +17368,22 @@ const COMMAND_LIMITS: {
|
|
|
17323
17368
|
readonly perPlugin: 160;
|
|
17324
17369
|
/** Commands of every plugin plus the player's own, together (1024). */
|
|
17325
17370
|
readonly total: 1024;
|
|
17371
|
+
/** Segments of a menu path, the leaf included. */
|
|
17372
|
+
readonly depth: 8;
|
|
17373
|
+
/** UTF-8 bytes of a command's local id: ASCII letters, digits, `.`, `_` and `-`. */
|
|
17374
|
+
readonly idBytes: 128;
|
|
17375
|
+
/**
|
|
17376
|
+
* UTF-8 bytes of a title, a text value counted by its English rendering. Like every command text: not empty, no control characters, no whitespace at either end.
|
|
17377
|
+
*/
|
|
17378
|
+
readonly titleBytes: 128;
|
|
17379
|
+
/** UTF-8 bytes of a menu path segment, a text value counted by its English rendering. */
|
|
17380
|
+
readonly segmentBytes: 80;
|
|
17381
|
+
/** UTF-8 bytes of a group. */
|
|
17382
|
+
readonly groupBytes: 80;
|
|
17383
|
+
/** UTF-8 bytes of a state's reason, as shown in the player's language. */
|
|
17384
|
+
readonly reasonBytes: 512;
|
|
17385
|
+
/** The largest `order` either way: an order is within ±1,000,000. */
|
|
17386
|
+
readonly order: 1000000;
|
|
17326
17387
|
};
|
|
17327
17388
|
```
|
|
17328
17389
|
|
|
@@ -19389,7 +19450,7 @@ Add a window teaches the view surface panels share with tool windows; the rules
|
|
|
19389
19450
|
- **The global `ListBridge`** holds the properties and callbacks. A panel is part of the player's main window, and the player makes a new instance of the component whenever it rebuilds that window. The global stays put, so the plugin talks to it, not to the component.
|
|
19390
19451
|
- `rows` is the list (an array in TypeScript) and `selected` the selected index.
|
|
19391
19452
|
- `select(int)` reports a click; `context-menu(float, float)` asks for the menu at a point.
|
|
19392
|
-
- **The component** inherits `Rectangle`, not `Window`: the player places it. `for row[index] in ListBridge.rows`
|
|
19453
|
+
- **The component** inherits `Rectangle`, not `Window`: the player places it. A `ListView` from `std-widgets.slint` fills it; `for row[index] in ListBridge.rows` inside it gives one 20-pixel row per item, and the selected row gets `Theme.selected-bg`. The `ListView` creates only the rows in view and scrolls the rest, so the panel stays cheap however long the list grows; a bare `for` in a layout would build every row up front (and `vestra-plugin check` warns about one over a bound array).
|
|
19393
19454
|
- **The `TouchArea`** of each row: any button going down selects the row. The right button coming up asks for the menu, at the pointer's position in the window (`absolute-position` is the row's position, `mouse-x`/`mouse-y` the pointer's position in it, and `/ 1px` makes a plain number).
|
|
19394
19455
|
|
|
19395
19456
|
#### The code
|
|
@@ -19491,6 +19552,7 @@ export default async function activate(plugin: Plugin) {
|
|
|
19491
19552
|
#### examples/panel-menu/ui/panel.slint
|
|
19492
19553
|
|
|
19493
19554
|
```slint
|
|
19555
|
+
import { ListView } from "std-widgets.slint";
|
|
19494
19556
|
import { Theme } from "@vestra/theme.slint";
|
|
19495
19557
|
|
|
19496
19558
|
export global ListBridge {
|
|
@@ -19503,7 +19565,9 @@ export global ListBridge {
|
|
|
19503
19565
|
export component ListPanel inherits Rectangle {
|
|
19504
19566
|
background: Theme.bg-list;
|
|
19505
19567
|
min-width: 200px;
|
|
19506
|
-
|
|
19568
|
+
// A ListView builds only the rows in view and scrolls the rest, so the
|
|
19569
|
+
// list costs the same with three rows or thirty thousand.
|
|
19570
|
+
ListView {
|
|
19507
19571
|
for row[index] in ListBridge.rows: Rectangle {
|
|
19508
19572
|
height: 20px;
|
|
19509
19573
|
background: index == ListBridge.selected ? Theme.selected-bg : transparent;
|
|
@@ -19526,9 +19590,6 @@ export component ListPanel inherits Rectangle {
|
|
|
19526
19590
|
}
|
|
19527
19591
|
}
|
|
19528
19592
|
}
|
|
19529
|
-
// Takes the space below the rows. Without it the layout's maximum
|
|
19530
|
-
// height (three 20px rows) would cap the player's main window.
|
|
19531
|
-
Rectangle { vertical-stretch: 1; }
|
|
19532
19593
|
}
|
|
19533
19594
|
}
|
|
19534
19595
|
```
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Vestra Player SDK
|
|
2
2
|
|
|
3
|
-
> @vestra-player/sdk 1.5.
|
|
3
|
+
> @vestra-player/sdk 1.5.49: write plugins for Vestra Player in TypeScript (plugin API `vestra/1`). A plugin is a folder with `manifest.json` and an entry module whose default export `activate(plugin)` the player calls; it runs in its own Node.js process (the player's built-in plugins share one). Everything a plugin creates (commands, windows, panels, filters, listeners…) is a handle that is released automatically when the plugin stops. L<n>: a line of llms-full.txt (next to this file), which has every page, declaration and example.
|
|
4
4
|
|
|
5
5
|
**Rules**
|
|
6
6
|
|
|
@@ -80,7 +80,7 @@ export default async function activate(plugin: Plugin) {
|
|
|
80
80
|
- `video.upscaling`: VideoUpscaling.state/set/onChange
|
|
81
81
|
- No capability: `tools`, `storage`, `system`, `app`, `ui` (images and Markdown), `i18n`, reading window.state, appearance.theme, packages.list and rendering.state. Details: L2293–2353.
|
|
82
82
|
|
|
83
|
-
**Test**: `tests/*.test.ts` with `node:test`, run by `vestra-plugin test`. `startPlugin(activate, { directory: new URL("..", import.meta.url) })` from `@vestra-player/sdk/testing` returns `{ host, stop }`, `host` an in-memory player (TestHost): act (`host.command(id).run()`, `host.press(key)`), `await host.settle()`, assert on `host.osd`, `host.window(id).props`… Guide: L1861–2021; TestHost:
|
|
83
|
+
**Test**: `tests/*.test.ts` with `node:test`, run by `vestra-plugin test`. `startPlugin(activate, { directory: new URL("..", import.meta.url) })` from `@vestra-player/sdk/testing` returns `{ host, stop }`, `host` an in-memory player (TestHost): act (`host.command(id).run()`, `host.press(key)`), `await host.settle()`, assert on `host.osd`, `host.window(id).props`… Guide: L1861–2021; TestHost: L15530.
|
|
84
84
|
|
|
85
85
|
**Command line**
|
|
86
86
|
|
|
@@ -108,40 +108,40 @@ export default async function activate(plugin: Plugin) {
|
|
|
108
108
|
- `limit`: A player-wide limit was reached, a property value is over the limits of PropValue
|
|
109
109
|
- `reserved`: The id is `vestra` or starts with `vestra.`
|
|
110
110
|
- `versionMismatch`: The package needs a newer player
|
|
111
|
-
- Others: PluginError, llms-full.txt
|
|
111
|
+
- Others: PluginError, llms-full.txt L5972.
|
|
112
112
|
|
|
113
113
|
## Reference
|
|
114
114
|
|
|
115
115
|
- [llms-full.txt](https://vestrasys.github.io/vestra-player-sdk/llms-full.txt), also node_modules/@vestra-player/sdk/llms-full.txt; pages: [site](https://vestrasys.github.io/vestra-player-sdk/).
|
|
116
116
|
- Tutorials: Getting started L9–134; Add a window L135–213; Add settings and translations L214–367; Test, check, pack and publish L368–424; Write your own blur kernel L425–685
|
|
117
117
|
- How-to: Control playback L686–854; Store a plugin's data L855–961; Load subtitles from a plugin L962–1074; Keep a slider responsive while dragging L1075–1262; Remember a window's state between runs L1263–1414; Offer a GPU rendering switch L1415–1540; Keep a command's state without keeping the plugin running L1541–1697; Translate a plugin L1698–1860; Test a plugin L1861–2021; Publish a plugin L2022–2123; Troubleshooting L2124–2217
|
|
118
|
-
- Reference: Manifest L2218–2292; Capabilities L2293–2353; Limits L2354–
|
|
119
|
-
- Explanation: How a plugin runs
|
|
120
|
-
- Guidelines: Plugin rules
|
|
121
|
-
- Upgrading: Migrate a plugin to English source text
|
|
122
|
-
- [API: @vestra-player/sdk](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk.html):
|
|
123
|
-
- [API: @vestra-player/sdk/testing](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk_testing.html):
|
|
118
|
+
- Reference: Manifest L2218–2292; Capabilities L2293–2353; Limits L2354–2506; Commands, menus and bindings L2507–3033; Platforms L3034–3064; Theme L3065–3414; Command line L3415–3472; Error codes L3473–3520; Translations L3521–3613; Test host L3614–3800
|
|
119
|
+
- Explanation: How a plugin runs L3801–4007; Commands, menus and key bindings L4008–4162; Playback state, controls and sources L4163–4322; Video and audio processing L4323–4443; Windows, panels, settings pages, dialogs and the OSD L4444–4708; GPU and software rendering L4709–4725; The player's Rust core L4726–5067; How the player runs and talks to plugins L5068–5407
|
|
120
|
+
- Guidelines: Plugin rules L5408–5419; Interface guidelines L5420–5537; Menu guidelines L5538–5596
|
|
121
|
+
- Upgrading: Migrate a plugin to English source text L5597–5746; Migrating off package rollback (1.5.16, removed in 1.5.17) L5747–5824; Who must change, release by release L5825–5894
|
|
122
|
+
- [API: @vestra-player/sdk](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk.html): L5895–15203
|
|
123
|
+
- [API: @vestra-player/sdk/testing](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk_testing.html): L15204–18090
|
|
124
124
|
|
|
125
125
|
## API
|
|
126
126
|
|
|
127
127
|
Where each `plugin.<module>` is declared:
|
|
128
128
|
|
|
129
|
-
- Plugin
|
|
130
|
-
- Commands
|
|
129
|
+
- Plugin L6311, Scope L6047, PluginError L5972, Disposable L5910
|
|
130
|
+
- Commands L6927; When L6502; Windows L7875; Panels L9321; Dialogs L9991; Player L10732; video: VideoFilter L11612, VideoView L11712, RenderOptions L11799, VideoShaders L11877, VideoUpscaling L12235; audio: AudioFilter L11657; Subtitles L12471; media: MediaCapture L12875; Resolvers L13105; Packages L13507; Osd L11477; Host L11319; Clipboard L13836; Power L13923; Tools L13982; Storage L14134; System L14333; Secrets L14413; window: MainWindow L9638; App L9818; Rendering L14676; Bindings L13735; Ui L7221; Timeline L9550; Drops L8568; Appearance L8649; I18n L8882; Settings L9165.
|
|
131
131
|
|
|
132
132
|
## Examples
|
|
133
133
|
|
|
134
134
|
Tested plugins in [examples/](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples):
|
|
135
135
|
|
|
136
|
-
- hello: The README Quick start: one menu command and an on-screen message.
|
|
137
|
-
- blur: A tool window with one slider that blurs the playing video.
|
|
138
|
-
- signal-generator: Generates its own video and audio: colour bars and a 1 kHz tone.
|
|
139
|
-
- bookmark: A tool window bound to the playback state: remember the position and jump back to it.
|
|
140
|
-
- panel-menu: A sidebar panel with its own right-click menu.
|
|
141
|
-
- menus: Radio menu items, separator groups and a menu item whose title changes.
|
|
142
|
-
- theme-widgets: A tool window drawn with the player's theme and widgets.
|
|
143
|
-
- markdown: A window that shows a Markdown document.
|
|
144
|
-
- i18n: A plugin in the player's language: English source text, a Chinese translation.
|
|
136
|
+
- hello: The README Quick start: one menu command and an on-screen message. L18525–18620
|
|
137
|
+
- blur: A tool window with one slider that blurs the playing video. L18621–18888
|
|
138
|
+
- signal-generator: Generates its own video and audio: colour bars and a 1 kHz tone. L18889–19227
|
|
139
|
+
- bookmark: A tool window bound to the playback state: remember the position and jump back to it. L19228–19420
|
|
140
|
+
- panel-menu: A sidebar panel with its own right-click menu. L19421–19635
|
|
141
|
+
- menus: Radio menu items, separator groups and a menu item whose title changes. L19636–19801
|
|
142
|
+
- theme-widgets: A tool window drawn with the player's theme and widgets. L19802–19971
|
|
143
|
+
- markdown: A window that shows a Markdown document. L19972–20174
|
|
144
|
+
- i18n: A plugin in the player's language: English source text, a Chinese translation. L20175–20348
|
|
145
145
|
|
|
146
146
|
## Optional
|
|
147
147
|
|