@vestra-player/sdk 1.6.16 → 1.6.18

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.
Files changed (36) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/commands.d.ts +5 -5
  3. package/dist/commands.js +117 -139
  4. package/dist/docs-tree.json.gz +0 -0
  5. package/dist/plugin/runtime.d.ts +26 -0
  6. package/dist/plugin/runtime.js +16 -0
  7. package/dist/plugin/testing/commands.d.ts +65 -0
  8. package/dist/plugin/testing/commands.js +412 -0
  9. package/dist/plugin/testing/core-actions.d.ts +39 -0
  10. package/dist/plugin/testing/core-actions.js +371 -0
  11. package/dist/plugin/testing/declarations.d.ts +37 -0
  12. package/dist/plugin/testing/declarations.js +80 -0
  13. package/dist/plugin/testing/i18n.d.ts +28 -0
  14. package/dist/plugin/testing/i18n.js +107 -0
  15. package/dist/plugin/testing/index.d.ts +3140 -0
  16. package/dist/plugin/testing/index.js +2796 -0
  17. package/dist/plugin/testing/internals.d.ts +76 -0
  18. package/dist/plugin/testing/internals.js +60 -0
  19. package/dist/plugin/testing/jobs.d.ts +9 -0
  20. package/dist/plugin/testing/jobs.js +304 -0
  21. package/dist/plugin/testing/layout.d.ts +85 -0
  22. package/dist/plugin/testing/layout.js +622 -0
  23. package/dist/plugin/testing/media.d.ts +55 -0
  24. package/dist/plugin/testing/media.js +615 -0
  25. package/dist/plugin/testing/osd.d.ts +4 -0
  26. package/dist/plugin/testing/osd.js +57 -0
  27. package/dist/plugin/testing/processing.d.ts +56 -0
  28. package/dist/plugin/testing/processing.js +491 -0
  29. package/dist/plugin/testing/services.d.ts +50 -0
  30. package/dist/plugin/testing/services.js +466 -0
  31. package/dist/plugin/testing/transport.d.ts +29 -0
  32. package/dist/plugin/testing/transport.js +140 -0
  33. package/dist/plugin/testing.d.ts +1 -3140
  34. package/dist/plugin/testing.js +1 -6259
  35. package/llms.txt +1 -1
  36. package/package.json +5 -3
@@ -0,0 +1,412 @@
1
+ /**
2
+ * Plugin commands and key bindings: `commands.*` and `bindings.*`, the
3
+ * binding table, conflicts and the bar buttons.
4
+ *
5
+ * Part of `TestHost` (`./index.ts`): functions over the host and its private
6
+ * state (`Internals`, `./internals.ts`) that the class delegates to.
7
+ * Internal to the SDK; not exported from `@vestra-player/sdk/testing`.
8
+ */
9
+ import { HostError } from "../../wire.js";
10
+ import { BAR_BUTTON_ICONS, BAR_BUTTON_LIMITS, BUILTIN_COMMANDS, COMMAND_LIMITS, MENU_IDS, } from "../../commands.js";
11
+ import { canonical, keyLabel } from "../bindings.js";
12
+ import { evaluate } from "../when.js";
13
+ import { menuKeys, textKey } from "../../i18n.js";
14
+ import { commandInvalid, commandConflict, commandText, menuTextKey, checkDeclaration, } from "./declarations.js";
15
+ import { declaredText, commandDisplay } from "./i18n.js";
16
+ /** A command state's reason, refused as the player refuses it. */
17
+ export function checkReason(host, inner, state) {
18
+ const reason = state?.reason;
19
+ return reason !== null && reason !== undefined
20
+ ? declaredText(host, inner, reason, "reason")
21
+ : null;
22
+ }
23
+ export function bindingClaims(host, inner) {
24
+ const claims = new Map();
25
+ // The player's own commands win a conflict, as in the binding table.
26
+ for (const [id, { declaration }] of [
27
+ ...host.coreCommands,
28
+ ...host.commands,
29
+ ]) {
30
+ const scope = declaration.focus ? "panel" : "global";
31
+ // The user overlay (`bindings.set`) replaces a command's own bindings.
32
+ const overridden = host.bindings.overlay.get(bindingKey(host, inner, id));
33
+ for (const binding of overridden ?? declaration.bindings ?? []) {
34
+ const key = `${scope} ${binding}`;
35
+ claims.set(key, [...(claims.get(key) ?? []), id]);
36
+ }
37
+ }
38
+ return claims;
39
+ }
40
+ /** `host/<id>` for core commands, `<manifest id>/<id>` for the plugin's. */
41
+ export function bindingKey(host, _inner, id) {
42
+ return host.coreCommands.has(id) ? id : `${host.manifestId}/${id}`;
43
+ }
44
+ /**
45
+ * `commands.keys` as the player answers it: each of the plugin's own
46
+ * commands, and the player's core commands as `host/<id>` (SDK 1.6.16),
47
+ * with the labels of the keys it holds now (the overlay, else its
48
+ * defaults; not a key an earlier command holds), as menus write them on
49
+ * `systemInfo.os`.
50
+ */
51
+ export function commandKeys(host, inner) {
52
+ const claims = bindingClaims(host, inner);
53
+ const mac = host.systemInfo.os === "macos";
54
+ const keys = {};
55
+ for (const [id, { declaration }] of [
56
+ ...host.commands,
57
+ ...host.coreCommands,
58
+ ]) {
59
+ const scope = declaration.focus ? "panel" : "global";
60
+ const current = host.bindings.overlay.get(bindingKey(host, inner, id)) ??
61
+ declaration.bindings ??
62
+ [];
63
+ const labels = [];
64
+ for (const input of current) {
65
+ if (claims.get(`${scope} ${input}`)?.[0] !== id)
66
+ continue;
67
+ const label = keyLabel(input, mac);
68
+ if (label !== null && !labels.includes(label))
69
+ labels.push(label);
70
+ }
71
+ keys[id] = labels;
72
+ }
73
+ return { keys, revision: inner.keysRevision };
74
+ }
75
+ /** Push `commands.keys` when the labels changed and the plugin listens. */
76
+ export function keysChanged(host, inner) {
77
+ inner.keysRevision++;
78
+ if (!host.subscriptions.has("commands.keys"))
79
+ return;
80
+ const now = commandKeys(host, inner);
81
+ const text = JSON.stringify(now.keys);
82
+ if (text === inner.keysSent)
83
+ return;
84
+ inner.keysSent = text;
85
+ host.emit("commands.keys", now);
86
+ }
87
+ export function splitCommandKey(_host, _inner, key) {
88
+ const at = key.indexOf("/");
89
+ return { plugin: key.slice(0, at), command: key.slice(at + 1) };
90
+ }
91
+ export function bindingTable(host, inner) {
92
+ const claims = bindingClaims(host, inner);
93
+ const commands = [...host.coreCommands, ...host.commands].map(([id, { declaration }]) => {
94
+ const key = bindingKey(host, inner, id);
95
+ const scope = declaration.focus ? "panel" : "global";
96
+ const current = host.bindings.overlay.get(key) ?? declaration.bindings;
97
+ const shown = host.commands.get(id) ?? host.coreCommands.get(id);
98
+ return {
99
+ ...splitCommandKey(host, inner, key),
100
+ // What the user sees, like the player's binding table.
101
+ title: shown?.display.title ?? String(declaration.title),
102
+ path: declaration.path?.length
103
+ ? (shown?.display.path ?? declaration.path.map(String))
104
+ : null,
105
+ key: declaration.path?.length
106
+ ? menuKeys(declaration.path.map((segment) => typeof segment === "string" ? segment : textKey(segment)))
107
+ : null,
108
+ // Player 0.7.6: the category as shown, `null` without one.
109
+ category: shown?.display.category ?? null,
110
+ scope: scope,
111
+ defaults: declaration.bindings,
112
+ current,
113
+ overridden: host.bindings.overlay.has(key),
114
+ shadowed: current.flatMap((input) => {
115
+ const winner = claims.get(`${scope} ${input}`)?.[0];
116
+ return !winner || winner === id
117
+ ? []
118
+ : [
119
+ {
120
+ input,
121
+ by: splitCommandKey(host, inner, bindingKey(host, inner, winner)),
122
+ },
123
+ ];
124
+ }),
125
+ labels: [...current],
126
+ };
127
+ });
128
+ const declared = new Set(commands.map((c) => `${c.plugin}/${c.command}`));
129
+ const orphans = [...host.bindings.overlay]
130
+ .filter(([key]) => !declared.has(key))
131
+ .map(([key, inputs]) => ({
132
+ ref: splitCommandKey(host, inner, key),
133
+ inputs,
134
+ }));
135
+ return { commands, orphans };
136
+ }
137
+ /** A command's state as the player shows it: its conditions evaluated. */
138
+ export function commandShown(host, _inner, sent, when) {
139
+ if (!when)
140
+ return sent;
141
+ const source = host.source;
142
+ const capabilities = source.capabilities;
143
+ const facts = {
144
+ phase: source.phase,
145
+ paused: source.paused,
146
+ kind: source.kind === "producer" ? "stream" : "media",
147
+ can: {
148
+ pause: capabilities.pause,
149
+ seek: capabilities.seek,
150
+ rate: capabilities.rate,
151
+ restart: capabilities.restart,
152
+ close: capabilities.close,
153
+ snapshot: capabilities.snapshotSource,
154
+ // As the player: the flags merged into the source state.
155
+ next: source.sequence?.next === true,
156
+ previous: source.sequence?.previous === true,
157
+ },
158
+ fullscreen: host.mainWindow.fullscreen,
159
+ theme: host.appearance,
160
+ language: host.language,
161
+ };
162
+ const shown = { ...sent };
163
+ if (when.enabled) {
164
+ shown.enabled = evaluate(when.enabled, facts);
165
+ if (shown.enabled && !shown.busy)
166
+ shown.reason = null;
167
+ }
168
+ if (when.checked)
169
+ shown.checked = evaluate(when.checked, facts);
170
+ return shown;
171
+ }
172
+ /** Implements `TestHost.press`. */
173
+ export async function press(host, inner, binding, options = {}) {
174
+ const claims = bindingClaims(host, inner);
175
+ const ids = [
176
+ ...(options.panel ? (claims.get(`panel ${binding}`) ?? []) : []),
177
+ ...(claims.get(`global ${binding}`) ?? []),
178
+ ];
179
+ if (!ids.length)
180
+ throw new Error(`test host: nothing is bound to ${binding}`);
181
+ const command = (id) => host.coreCommands.get(id) ?? host.command(id);
182
+ const enabled = ids.find((id) => command(id).state.enabled);
183
+ if (enabled !== undefined) {
184
+ await command(enabled).run();
185
+ return "ran";
186
+ }
187
+ // Like the player: only the first disabled command counts; without a
188
+ // reason the key goes on to the focused control.
189
+ const first = ids[0];
190
+ const reason = host.commands.has(first)
191
+ ? command(first).display.reason
192
+ : command(first).state.reason;
193
+ if (typeof reason === "string" && reason !== "") {
194
+ host.osd.push(reason);
195
+ return "reason";
196
+ }
197
+ return "passed";
198
+ }
199
+ /** Implements `TestHost.barButtons`. */
200
+ export function barButtons(host, _inner) {
201
+ return [...host.commands]
202
+ .filter(([, command]) => command.declaration.button !== undefined)
203
+ .slice(0, BAR_BUTTON_LIMITS.perPlugin)
204
+ .map(([id, command]) => {
205
+ const button = command.declaration.button;
206
+ const image = typeof button.icon !== "string";
207
+ const reason = command.display.reason;
208
+ const title = command.display.title.replace(/(…|\.\.\.)\s*$/, "");
209
+ const enabled = command.state.enabled && !command.state.busy;
210
+ return Object.freeze({
211
+ command: id,
212
+ icon: image
213
+ ? String(button.icon.$vestraImage)
214
+ : button.icon,
215
+ image,
216
+ order: button.order,
217
+ enabled,
218
+ checked: command.state.checked === true,
219
+ tooltip: !enabled && reason ? `${title}\n${reason}` : title,
220
+ async click() {
221
+ const current = host.commands.get(id);
222
+ if (current !== command)
223
+ throw new HostError("missing", `command ${id} is gone`);
224
+ await current.run();
225
+ },
226
+ });
227
+ });
228
+ }
229
+ /** The default handlers of this area's wire methods, by method name. */
230
+ export function commandsHandlers(host, inner) {
231
+ return {
232
+ "commands.register": (p) => {
233
+ const id = String(p.id);
234
+ // The player's checks, in its order (capabilities/commands.rs
235
+ // `register`, then commands.rs `Registry::register`), with its codes
236
+ // and messages.
237
+ if (!Number.isInteger(p.revision) || Number(p.revision) <= 0)
238
+ throw new HostError("invalid", "positive revision required");
239
+ // The reason is checked (and shown) when it arrives.
240
+ const reason = checkReason(host, inner, p.state);
241
+ const declaration = p.declaration;
242
+ const button = declaration.button;
243
+ const maxOrder = BAR_BUTTON_LIMITS.maxOrder;
244
+ // Player 0.5.10: a bar button is { icon, order }, its order 0–999,
245
+ // its icon a name or an image of the plugin.
246
+ if (button !== undefined && button !== null) {
247
+ if (typeof button !== "object" ||
248
+ Array.isArray(button) ||
249
+ !Object.keys(button).every((key) => key === "icon" || key === "order"))
250
+ throw new HostError("invalid", "button is { icon, order }");
251
+ const { icon, order } = button;
252
+ if (!Number.isInteger(order) || +order < 0 || +order > maxOrder)
253
+ throw new HostError("invalid", `button order must be an integer 0–${maxOrder}`);
254
+ if (typeof icon !== "string" && host.imagePath(icon) === null)
255
+ throw new HostError("invalid", "button icon is an icon name or an image from plugin.ui.image");
256
+ }
257
+ // Text values in the title and path are keyed by their English
258
+ // rendering; the built-in menu ids (MENU_IDS) and the player's old
259
+ // Chinese menu titles by the menus' keys.
260
+ const title = menuTextKey(declaration.title);
261
+ const declared = (declaration.path ?? []);
262
+ if (!Array.isArray(declared))
263
+ throw new HostError("invalid", "path is a list of menu texts");
264
+ const keys = declared.map(menuTextKey);
265
+ const menu = keys[0]?.startsWith("@") ? keys[0].slice(1) : null;
266
+ if (menu !== null && !MENU_IDS.includes(menu))
267
+ throw commandInvalid(`unknown menu id @${menu}; the built-in menus are ${MENU_IDS.map((known) => `@${known}`).join(", ")}`);
268
+ if (menu !== null && keys.length < 2)
269
+ throw commandInvalid(`menu id @${menu} needs an item after it`);
270
+ const path = menuKeys(keys);
271
+ // Player 0.7.6: the category, keyed like a menu text.
272
+ const category = declaration.category === undefined || declaration.category === null
273
+ ? null
274
+ : menuTextKey(declaration.category);
275
+ checkDeclaration(id, title, path, declaration, category);
276
+ if (button !== undefined && button !== null) {
277
+ if (path.length === 0)
278
+ throw commandInvalid("button needs a menu path: a bar button is also a menu item");
279
+ const { icon } = button;
280
+ if (typeof icon === "string" &&
281
+ !BAR_BUTTON_ICONS.includes(icon))
282
+ 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`);
283
+ }
284
+ // The binding table's `Table::canonical` (player 0.6.1): each binding
285
+ // parsed, and each input once, compared parsed, before the reason,
286
+ // conflicts and limits.
287
+ const bindings = (declaration.bindings ?? []);
288
+ const seen = new Set();
289
+ for (const binding of bindings.map(String)) {
290
+ let input;
291
+ try {
292
+ input = canonical(binding);
293
+ }
294
+ catch (error) {
295
+ throw commandInvalid(error.message);
296
+ }
297
+ if (seen.has(input))
298
+ throw commandInvalid(`binding ${binding} is listed twice`);
299
+ seen.add(input);
300
+ }
301
+ if (reason !== null)
302
+ commandText(reason, "reason", COMMAND_LIMITS.reasonBytes);
303
+ if (host.commands.has(id))
304
+ throw commandConflict("owner/localId already registered");
305
+ // The player's own commands beside the core actions (host/actions.rs
306
+ // `host_declarations`: Toggle Fullscreen, Command Palette…, Quit and
307
+ // four outside the menu), which take part in conflicts and the total
308
+ // like any other.
309
+ const hostOnly = BUILTIN_COMMANDS.filter((row) => row.owner === "host" && !host.coreCommands.has(`host/${row.id}`));
310
+ // A path may not repeat another command's (the player's own
311
+ // included) nor be a submenu of one, or one of its submenus.
312
+ if (path.length > 0)
313
+ for (const existing of [
314
+ ...hostOnly.map((row) => row.path),
315
+ ...[...host.coreCommands.values()].map((command) => command.declaration.path),
316
+ ...[...host.commands.values()].map((command) => menuKeys((command.declaration.path ?? []).map(menuTextKey))),
317
+ ]) {
318
+ const shared = Math.min(existing.length, path.length);
319
+ if (existing.length > 0 &&
320
+ existing.slice(0, shared).every((s, i) => s === path[i]))
321
+ throw commandConflict("menu path duplicates a command or collides with a leaf/group");
322
+ }
323
+ // The player's caps (COMMAND_LIMITS): the total counts all of the
324
+ // player's own commands too.
325
+ if (host.commands.size >= COMMAND_LIMITS.perPlugin ||
326
+ host.commands.size + host.coreCommands.size + hostOnly.length >=
327
+ COMMAND_LIMITS.total)
328
+ throw new HostError("limit", "command registry limit reached");
329
+ let sent = p.state;
330
+ host.commands.set(id, {
331
+ declaration,
332
+ get display() {
333
+ return commandDisplay(host, inner, declaration.path ?? [], declaration.title, this.state.reason, declaration.category ?? null);
334
+ },
335
+ ...(p.when ? { when: p.when } : {}),
336
+ get state() {
337
+ return commandShown(host, inner, sent, this.when);
338
+ },
339
+ set state(state) {
340
+ sent = state;
341
+ },
342
+ revision: Number(p.revision),
343
+ async run() {
344
+ if (this.state.busy || !this.state.enabled)
345
+ throw new HostError("command", `${id} is ${this.state.busy ? "busy" : "disabled"}`);
346
+ host.emit("commands.execute", { id, revision: this.revision });
347
+ await host.settle();
348
+ },
349
+ });
350
+ // The player pushes the new command's keys after the answer.
351
+ queueMicrotask(() => keysChanged(host, inner));
352
+ return { revision: 1 };
353
+ },
354
+ "commands.unregister": (p) => {
355
+ if (!host.commands.delete(String(p.id)))
356
+ throw new HostError("missing", `command ${p.id} is not registered`);
357
+ queueMicrotask(() => keysChanged(host, inner));
358
+ },
359
+ // Player 0.7.6: the labels of the plugin's own commands.
360
+ "commands.keys": () => {
361
+ const now = commandKeys(host, inner);
362
+ inner.keysSent = JSON.stringify(now.keys);
363
+ return now;
364
+ },
365
+ "commands.update": (p) => {
366
+ // The request itself, as the player reads it (capabilities/commands.rs
367
+ // `parsed`): `invalid`.
368
+ if (!Number.isInteger(p.revision) || Number(p.revision) <= 0)
369
+ throw new HostError("invalid", "positive revision required");
370
+ const reason = checkReason(host, inner, p.state);
371
+ // The registry's refusals of an update (commands.rs `Registry::update`)
372
+ // all come back as `command`; the test host's one plugin is owner 1.
373
+ const command = host.commands.get(String(p.id));
374
+ if (!command)
375
+ throw new HostError("command", `command missing: owner 1 / ${p.id}`);
376
+ if (Number(p.revision) <= command.revision)
377
+ throw new HostError("command", `stale command revision ${p.revision}; current is ${command.revision}`);
378
+ if (reason !== null)
379
+ try {
380
+ commandText(reason, "reason", COMMAND_LIMITS.reasonBytes);
381
+ }
382
+ catch (error) {
383
+ throw new HostError("command", error.message);
384
+ }
385
+ command.revision = Number(p.revision);
386
+ command.state = p.state;
387
+ if (p.when)
388
+ command.when = p.when;
389
+ else
390
+ delete command.when;
391
+ return { revision: command.revision };
392
+ },
393
+ "bindings.list": () => bindingTable(host, inner),
394
+ "bindings.set": (p) => {
395
+ const key = `${p.plugin}/${p.command}`;
396
+ const known = bindingTable(host, inner).commands.some((c) => `${c.plugin}/${c.command}` === key);
397
+ if (!known && !host.bindings.overlay.has(key))
398
+ throw new HostError("missing", `no command ${key}`);
399
+ host.bindings.overlay.set(key, [...p.inputs]);
400
+ host.emit("bindings.changed", null);
401
+ keysChanged(host, inner);
402
+ },
403
+ "bindings.reset": (p) => {
404
+ if (p.plugin === undefined)
405
+ host.bindings.overlay.clear();
406
+ else
407
+ host.bindings.overlay.delete(`${p.plugin}/${p.command}`);
408
+ host.emit("bindings.changed", null);
409
+ keysChanged(host, inner);
410
+ },
411
+ };
412
+ }
@@ -0,0 +1,39 @@
1
+ import type { TestHost } from "./index.ts";
2
+ import type { Handler, Internals } from "./internals.ts";
3
+ export declare function coreFacts(host: TestHost, _inner: Internals): {
4
+ ready: boolean;
5
+ ended: boolean;
6
+ toggle: boolean;
7
+ step: boolean;
8
+ rate: boolean;
9
+ close: boolean;
10
+ };
11
+ /** Why one frame that way is impossible (B07), or null; the player's rules. */
12
+ export declare function frameCheck(host: TestHost, inner: Internals, forward: boolean): string | null;
13
+ /** The same enabled rules and reasons as the player's core actions. */
14
+ export declare function coreState(host: TestHost, inner: Internals, action: string): {
15
+ enabled: boolean;
16
+ checked: boolean | null;
17
+ busy: boolean;
18
+ reason: string | null;
19
+ };
20
+ export declare function registerCoreActions(host: TestHost, inner: Internals): void;
21
+ export declare function runCore(host: TestHost, inner: Internals, action: string, seconds?: number): void;
22
+ export declare function windowFacts(host: TestHost, _inner: Internals): {
23
+ fullscreen: boolean;
24
+ maximized: boolean;
25
+ videoArea: {
26
+ width: number;
27
+ height: number;
28
+ };
29
+ pictureInPicture: {
30
+ supported: boolean;
31
+ active: boolean;
32
+ };
33
+ appearance: import("../types.ts").ThemeId;
34
+ fileTypes: string[];
35
+ language: import("../types.ts").LanguagePreference;
36
+ locale: import("../types.ts").Locale;
37
+ };
38
+ /** The default handlers of this area's wire methods, by method name. */
39
+ export declare function coreActionsHandlers(host: TestHost, inner: Internals): Record<string, Handler>;