@vestra-player/sdk 1.5.8 → 1.5.11

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 (43) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +4 -1
  3. package/bin/vestra-plugin.mjs +170 -7
  4. package/dist/commands.d.ts +7 -4
  5. package/dist/i18n-tools.d.ts +67 -0
  6. package/dist/i18n-tools.js +787 -0
  7. package/dist/i18n.d.ts +222 -0
  8. package/dist/i18n.js +972 -0
  9. package/dist/player-runtime.d.ts +2 -0
  10. package/dist/player-runtime.js +2 -0
  11. package/dist/plugin/appearance.js +27 -1
  12. package/dist/plugin/bindings.js +1 -0
  13. package/dist/plugin/commands.js +24 -4
  14. package/dist/plugin/index.js +9 -1
  15. package/dist/plugin/language.d.ts +8 -0
  16. package/dist/plugin/language.js +79 -0
  17. package/dist/plugin/osd.d.ts +11 -0
  18. package/dist/plugin/osd.js +64 -9
  19. package/dist/plugin/packages.js +10 -0
  20. package/dist/plugin/runtime.d.ts +3 -1
  21. package/dist/plugin/runtime.js +6 -0
  22. package/dist/plugin/scope.d.ts +1 -0
  23. package/dist/plugin/scope.js +2 -0
  24. package/dist/plugin/subtitles.js +9 -1
  25. package/dist/plugin/testing.d.ts +184 -5
  26. package/dist/plugin/testing.js +459 -67
  27. package/dist/plugin/timeline.js +3 -1
  28. package/dist/plugin/types.d.ts +523 -54
  29. package/dist/plugin/ui.js +29 -10
  30. package/dist/plugin/values.d.ts +55 -0
  31. package/dist/plugin/values.js +117 -0
  32. package/dist/plugin/window.d.ts +2 -0
  33. package/dist/plugin/window.js +4 -0
  34. package/dist/source-resolvers.d.ts +0 -1
  35. package/dist/source-resolvers.js +9 -1
  36. package/dist/subtitles.d.ts +3 -1
  37. package/dist/wire.d.ts +38 -6
  38. package/dist/wire.js +13 -2
  39. package/llms-full.txt +646 -58
  40. package/llms.txt +44 -40
  41. package/package.json +1 -1
  42. package/slint/vestra/markdown.slint +5 -5
  43. package/slint/vestra/widgets.slint +4 -4
package/dist/plugin/ui.js CHANGED
@@ -1,4 +1,6 @@
1
1
  /** Tool windows, docked panels and system dialogs. */
2
+ import { shownText, templateArgs } from "./osd.js";
3
+ import { isTextValue } from "../i18n.js";
2
4
  import { readFileSync } from "node:fs";
3
5
  import { basename, extname, resolve } from "node:path";
4
6
  import { failure, Latest, quiet } from "./runtime.js";
@@ -646,15 +648,27 @@ export const panels = (scope) => ({
646
648
  });
647
649
  export const dialogs = (scope) => {
648
650
  const open = async (kind, options = {}) => {
649
- const result = await scope.runtime.host.dialogs.open({ kind, ...options });
651
+ const result = await scope.runtime.host.dialogs.open({
652
+ kind,
653
+ ...(options.title === undefined
654
+ ? {}
655
+ : { title: shownText(options.title, undefined, "title") }),
656
+ ...(options.fileName === undefined
657
+ ? {}
658
+ : { fileName: shownText(options.fileName, undefined, "fileName") }),
659
+ });
650
660
  return result.cancelled ? [] : result.paths;
651
661
  };
652
662
  return {
653
663
  async confirm(options) {
654
- const { message, detail, confirm, cancel, destructive, checkbox } = options ?? {};
655
- const length = (text, min, max) => typeof text === "string" &&
656
- [...text].length >= min &&
657
- [...text].length <= max;
664
+ const { message, detail, confirm, cancel, destructive, checkbox, args } = options ?? {};
665
+ const values = templateArgs(args);
666
+ // A string's length is checked here; a text value's (shown) length by
667
+ // the player.
668
+ const length = (text, min, max) => isTextValue(text) ||
669
+ (typeof text === "string" &&
670
+ [...text].length >= min &&
671
+ [...text].length <= max);
658
672
  if (!length(message, 1, 200) ||
659
673
  !length(confirm, 1, 40) ||
660
674
  (detail !== undefined && !length(detail, 0, 2000)) ||
@@ -662,13 +676,18 @@ export const dialogs = (scope) => {
662
676
  (checkbox !== undefined && !length(checkbox, 1, 40)) ||
663
677
  (destructive !== undefined && typeof destructive !== "boolean"))
664
678
  throw failure("invalid", "confirm needs message 1–200, confirm/cancel/checkbox 1–40, detail ≤ 2000 characters");
679
+ // `args` fills the string templates; a text value carries its own.
680
+ const shown = (text, what) => shownText(text, undefined, what);
665
681
  const answer = await scope.runtime.host.dialogs.confirm({
666
- message,
667
- confirm,
668
- ...(detail === undefined ? {} : { detail }),
669
- ...(cancel === undefined ? {} : { cancel }),
682
+ message: shown(message, "message"),
683
+ confirm: shown(confirm, "confirm"),
684
+ ...(detail === undefined ? {} : { detail: shown(detail, "detail") }),
685
+ ...(cancel === undefined ? {} : { cancel: shown(cancel, "cancel") }),
670
686
  ...(destructive === undefined ? {} : { destructive }),
671
- ...(checkbox === undefined ? {} : { checkbox }),
687
+ ...(checkbox === undefined
688
+ ? {}
689
+ : { checkbox: shown(checkbox, "checkbox") }),
690
+ ...(values === undefined ? {} : { args: values }),
672
691
  });
673
692
  return {
674
693
  confirmed: answer.confirmed === true,
@@ -0,0 +1,55 @@
1
+ /** The player's limits on one property value, for {@link TestHost}. */
2
+ import type { Json } from "../wire.ts";
3
+ /**
4
+ * The player's limits on one property value (a `PropValue` as written
5
+ * by `View.set` or `View.bind`, or an initial property of a
6
+ * window, panel or settings page). The player's tests check that these
7
+ * agree with the player; {@link TestHost} enforces them the same way.
8
+ *
9
+ * A value over a limit is refused whole with code `limit` (the property
10
+ * keeps what it showed) and reported to `Scope.onError`: a write by
11
+ * `View.set` or `View.bind` as described there. As an initial property it
12
+ * makes `Window.show` reject; a panel or settings page opens without it and
13
+ * the failure goes to the plugin's `onError`.
14
+ *
15
+ * They bound the work one write costs the player's interface thread; they
16
+ * are not meant as list sizes to aim for. A list that can grow without
17
+ * bound (a playlist, a history) should send the rows near the view and say
18
+ * how many there are, not the whole list.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * import { UI_VALUE_LIMITS } from "@vestra-player/sdk/testing";
23
+ *
24
+ * // 8 fields per row: 9 values (the row and its fields).
25
+ * const window = Math.floor(UI_VALUE_LIMITS.values / 9) - 1;
26
+ * ```
27
+ *
28
+ * @since 1.5.10
29
+ */
30
+ export declare const UI_VALUE_LIMITS: {
31
+ /** Nesting depth; the value itself is depth 0 (8). */
32
+ readonly depth: 8;
33
+ /**
34
+ * Values in all, counting every array, object, string, number, boolean
35
+ * and null, the value itself included; a Markdown document is one
36
+ * (16384).
37
+ */
38
+ readonly values: 16384;
39
+ /** Rows of one array (4096). */
40
+ readonly arrayRows: 4096;
41
+ /** UTF-8 bytes of one string or object field name (64 KiB). */
42
+ readonly stringBytes: number;
43
+ /**
44
+ * UTF-8 bytes of all strings and field names together, the text of the
45
+ * value's Markdown documents included (1 MiB).
46
+ */
47
+ readonly textBytes: number;
48
+ };
49
+ /**
50
+ * Why the player would refuse `value` (in wire form) as a property value,
51
+ * or `null`. The same checks, order and messages as the player's
52
+ * conversion (`host/src/ui_values.rs`). An image reference as the whole
53
+ * value is not counted (the player loads it instead).
54
+ */
55
+ export declare function valueLimitProblem(value: Json): string | null;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * The player's limits on one property value (a `PropValue` as written
3
+ * by `View.set` or `View.bind`, or an initial property of a
4
+ * window, panel or settings page). The player's tests check that these
5
+ * agree with the player; {@link TestHost} enforces them the same way.
6
+ *
7
+ * A value over a limit is refused whole with code `limit` (the property
8
+ * keeps what it showed) and reported to `Scope.onError`: a write by
9
+ * `View.set` or `View.bind` as described there. As an initial property it
10
+ * makes `Window.show` reject; a panel or settings page opens without it and
11
+ * the failure goes to the plugin's `onError`.
12
+ *
13
+ * They bound the work one write costs the player's interface thread; they
14
+ * are not meant as list sizes to aim for. A list that can grow without
15
+ * bound (a playlist, a history) should send the rows near the view and say
16
+ * how many there are, not the whole list.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * import { UI_VALUE_LIMITS } from "@vestra-player/sdk/testing";
21
+ *
22
+ * // 8 fields per row: 9 values (the row and its fields).
23
+ * const window = Math.floor(UI_VALUE_LIMITS.values / 9) - 1;
24
+ * ```
25
+ *
26
+ * @since 1.5.10
27
+ */
28
+ export const UI_VALUE_LIMITS = {
29
+ /** Nesting depth; the value itself is depth 0 (8). */
30
+ depth: 8,
31
+ /**
32
+ * Values in all, counting every array, object, string, number, boolean
33
+ * and null, the value itself included; a Markdown document is one
34
+ * (16384).
35
+ */
36
+ values: 16384,
37
+ /** Rows of one array (4096). */
38
+ arrayRows: 4096,
39
+ /** UTF-8 bytes of one string or object field name (64 KiB). */
40
+ stringBytes: 64 * 1024,
41
+ /**
42
+ * UTF-8 bytes of all strings and field names together, the text of the
43
+ * value's Markdown documents included (1 MiB).
44
+ */
45
+ textBytes: 1024 * 1024,
46
+ };
47
+ const L = UI_VALUE_LIMITS;
48
+ const bytes = (text) => Buffer.byteLength(text, "utf8");
49
+ const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
50
+ /** Text of a Markdown document in wire form, or `null` for other JSON. */
51
+ function documentText(value) {
52
+ if (!isObject(value))
53
+ return null;
54
+ const doc = value.$vestraMarkdown;
55
+ return isObject(doc) && typeof doc.text === "string" ? doc.text : null;
56
+ }
57
+ /**
58
+ * Why the player would refuse `value` (in wire form) as a property value,
59
+ * or `null`. The same checks, order and messages as the player's
60
+ * conversion (`host/src/ui_values.rs`). An image reference as the whole
61
+ * value is not counted (the player loads it instead).
62
+ */
63
+ export function valueLimitProblem(value) {
64
+ if (isObject(value) && "$vestraImage" in value)
65
+ return null;
66
+ const budget = { values: 0, text: 0 };
67
+ const text = (s) => {
68
+ const n = bytes(s);
69
+ if (n > L.stringBytes)
70
+ return `UI string or field name exceeds ${L.stringBytes} bytes`;
71
+ if (budget.text + n > L.textBytes)
72
+ return `UI value text exceeds ${L.textBytes} total bytes`;
73
+ budget.text += n;
74
+ return null;
75
+ };
76
+ const walk = (value, depth) => {
77
+ if (depth > L.depth)
78
+ return `UI value depth exceeds ${L.depth} (root is depth 0)`;
79
+ if (budget.values === L.values)
80
+ return `UI value exceeds ${L.values} total nodes`;
81
+ budget.values += 1;
82
+ const doc = documentText(value);
83
+ if (doc !== null) {
84
+ const n = bytes(doc);
85
+ if (budget.text + n > L.textBytes)
86
+ return `UI value text exceeds ${L.textBytes} total bytes`;
87
+ budget.text += n;
88
+ return null;
89
+ }
90
+ if (typeof value === "string")
91
+ return text(value);
92
+ if (Array.isArray(value)) {
93
+ if (value.length > L.arrayRows)
94
+ return `UI array exceeds ${L.arrayRows} rows`;
95
+ if (value.length > L.values - budget.values)
96
+ return "UI array cannot fit in remaining node budget";
97
+ for (const item of value) {
98
+ const problem = walk(item, depth + 1);
99
+ if (problem)
100
+ return problem;
101
+ }
102
+ return null;
103
+ }
104
+ if (isObject(value)) {
105
+ const entries = Object.entries(value);
106
+ if (entries.length > L.values - budget.values)
107
+ return "UI object cannot fit in remaining node budget";
108
+ for (const [name, item] of entries) {
109
+ const problem = text(name) ?? walk(item, depth + 1);
110
+ if (problem)
111
+ return problem;
112
+ }
113
+ }
114
+ return null;
115
+ };
116
+ return walk(value, 0);
117
+ }
@@ -21,6 +21,8 @@ export declare class WindowService {
21
21
  appearance: string | undefined;
22
22
  /** The file resolvers' extensions, as the reply carried them (`plugin.resolvers.fileTypes`). */
23
23
  fileTypes: string[] | undefined;
24
+ /** The saved interface language, as the reply carried it (`plugin.appearance.language`). */
25
+ language: string | undefined;
24
26
  accept(facts: WindowFacts): void;
25
27
  read(): Promise<void>;
26
28
  watch(listener: (state: WindowState) => void): {
@@ -54,9 +54,13 @@ export class WindowService {
54
54
  appearance;
55
55
  /** The file resolvers' extensions, as the reply carried them (`plugin.resolvers.fileTypes`). */
56
56
  fileTypes;
57
+ /** The saved interface language, as the reply carried it (`plugin.appearance.language`). */
58
+ language;
57
59
  accept(facts) {
58
60
  if (facts.appearance)
59
61
  this.appearance = facts.appearance;
62
+ if (facts.language)
63
+ this.language = facts.language;
60
64
  if (Array.isArray(facts.fileTypes))
61
65
  this.fileTypes = [...facts.fileTypes];
62
66
  const next = view(facts);
@@ -1,4 +1,3 @@
1
- /** Trusted extraction providers. Declarations cross JSON; handler code stays in JS. */
2
1
  import type { Transport } from "./wire.ts";
3
2
  /** One stream of a {@link ResolvedMedia}. */
4
3
  export type ResolvedResource = {
@@ -1,6 +1,14 @@
1
+ /** Trusted extraction providers. Declarations cross JSON; handler code stays in JS. */
2
+ import { isTextValue, textProblem } from "./i18n.js";
1
3
  const isProducer = (result) => result?.kind === "stream";
2
4
  /** The words of a failure, as the user will read them. */
3
- const reason = (error) => (error instanceof Error ? error.message : String(error)).slice(0, 2048);
5
+ const reason = (error) => {
6
+ // `plugin.i18n.error(…)`: the player shows the text translated.
7
+ const text = error?.text;
8
+ if (error instanceof Error && isTextValue(text) && !textProblem(text))
9
+ return text;
10
+ return (error instanceof Error ? error.message : String(error)).slice(0, 2048);
11
+ };
4
12
  export function sourceResolverClient(transport) {
5
13
  return {
6
14
  /** Requires source.resolve. One registered host/scheme pair, and one file
@@ -1,4 +1,5 @@
1
1
  /** Subtitle wire types; source pixels and renderer confirmation remain distinct. */
2
+ import type { TextJson } from "./i18n.ts";
2
3
  import type { Transport } from "./wire.ts";
3
4
  export type SubtitleTrack = {
4
5
  kind: "embedded";
@@ -169,7 +170,8 @@ export declare function subtitleClient(transport: Transport): {
169
170
  expected: SubtitleStamp;
170
171
  inputOrigin?: SubtitleTrack;
171
172
  format?: "srt" | "ass";
172
- title?: string;
173
+ /** Source text is data; a text value is shown translated (player 0.4.3). */
174
+ title?: string | TextJson;
173
175
  select?: "primary" | "secondary" | "none";
174
176
  }) => Promise<SubtitleState>;
175
177
  select: (target: SubtitleTrack | null, expected: SubtitleStamp, operationId: number, role?: "primary" | "secondary") => Promise<SubtitleState>;
package/dist/wire.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { SourceAudioTrack, SourceStamp } from "./source.ts";
2
+ import type { ArgJson, ListJson, TextJson } from "./i18n.ts";
2
3
  export type { ResolvedMedia, ResolvedResource, SourceResolverDeclaration, } from "./source-resolvers.ts";
3
4
  /** Working contract. Trusted plugin runtime, not a permissions sandbox. */
4
5
  export declare const API_VERSION: "vestra/1";
@@ -112,6 +113,13 @@ export interface PackageInfo {
112
113
  slot?: string;
113
114
  owner?: string;
114
115
  }[];
116
+ /** The manifest's display texts in the interface language (player 0.4.3). */
117
+ display?: {
118
+ name: string | null;
119
+ punchline: string | null;
120
+ description: string | null;
121
+ tags: string[];
122
+ };
115
123
  }
116
124
  /** Where an installed package came from (`packages.install` `source`). */
117
125
  export type WireSource = {
@@ -134,6 +142,8 @@ export interface BindingTable {
134
142
  command: string;
135
143
  title: string;
136
144
  path: string[] | null;
145
+ /** The path's keys (player 0.4.3). */
146
+ key?: string[] | null;
137
147
  scope: "global" | "panel";
138
148
  defaults: string[];
139
149
  current: string[];
@@ -171,6 +181,10 @@ export interface WindowFacts {
171
181
  appearance?: string;
172
182
  /** The file resolvers' extensions (`plugin.resolvers.fileTypes`; player 0.4.2). */
173
183
  fileTypes?: string[];
184
+ /** The saved interface language, `"system"` or a locale (player 0.4.3). */
185
+ language?: string;
186
+ /** The interface language of this run (player 0.4.3). */
187
+ locale?: string;
174
188
  }
175
189
  /** One timeline mark as the player merges them (`timeline.marks`). */
176
190
  export interface WireMark {
@@ -276,11 +290,25 @@ export interface WireSystemInfo {
276
290
  primary: boolean;
277
291
  }[];
278
292
  }
293
+ /**
294
+ * Template arguments on the wire (`osd.show`, `osd.panel`, `dialogs.confirm`,
295
+ * a text's `args`): data, or text values (player 0.4.3).
296
+ */
297
+ export type TemplateArgsJson = ArgJson[] | {
298
+ [name: string]: ArgJson;
299
+ };
300
+ /** A text the player shows: source text, or a text value (player 0.4.3). */
301
+ export type TextValueJson = string | TextJson | ListJson;
279
302
  export interface Transport {
280
303
  request<T>(method: string, params?: Json): Promise<T>;
281
304
  on(event: string, listener: (data: unknown) => void): () => void;
282
305
  /** Replaces the real local services (test host). */
283
306
  local?: LocalServices;
307
+ /**
308
+ * The interface language of this run (`plugin.i18n.locale`): the runner
309
+ * passes the player's `VESTRA_LOCALE`, the test host its `locale`.
310
+ */
311
+ locale?: string;
284
312
  }
285
313
  export declare function createWire(transport: Transport): {
286
314
  processing: {
@@ -300,12 +328,13 @@ export declare function createWire(transport: Transport): {
300
328
  dialogs: {
301
329
  open: (options: {
302
330
  kind: "openFile" | "openFiles" | "openDirectory" | "saveFile";
303
- title?: string;
304
- fileName?: string;
331
+ title?: TextValueJson;
332
+ fileName?: TextValueJson;
305
333
  }) => Promise<{
306
334
  cancelled: boolean;
307
335
  paths: string[];
308
336
  }>;
337
+ /** Message and detail are templates formatted with `args` (player 0.4.3). */
309
338
  confirm: (options: {
310
339
  [key: string]: Json;
311
340
  }) => Promise<{
@@ -351,6 +380,8 @@ export declare function createWire(transport: Transport): {
351
380
  /** The palette (`ui.theme` to change it). */
352
381
  appearance: {
353
382
  set: (theme: string) => Promise<void>;
383
+ /** The interface language for the next start (player 0.4.3). */
384
+ setLanguage: (language: string) => Promise<void>;
354
385
  };
355
386
  /** Settings pages in the player's options window (`ui.settings`). */
356
387
  settings: {
@@ -400,7 +431,7 @@ export declare function createWire(transport: Transport): {
400
431
  set: (marks: {
401
432
  time: number;
402
433
  kind: string;
403
- title?: string;
434
+ title?: string | TextJson;
404
435
  }[], sourceId?: number) => Promise<{
405
436
  applied: boolean;
406
437
  }>;
@@ -469,9 +500,10 @@ export declare function createWire(transport: Transport): {
469
500
  } | null) => Promise<void>;
470
501
  step: (direction: "next" | "previous") => Promise<void>;
471
502
  };
503
+ /** `text` is a template in the plugin's catalog, formatted with `args` (player 0.4.3). */
472
504
  osd: {
473
- show: (text: string) => Promise<void>;
474
- panel: (text: string | null) => Promise<void>;
505
+ show: (text: TextValueJson, args?: TemplateArgsJson) => Promise<void>;
506
+ panel: (text: TextValueJson | null, args?: TemplateArgsJson, maxColumns?: number) => Promise<void>;
475
507
  };
476
508
  layout: {
477
509
  state: () => Promise<{
@@ -642,7 +674,7 @@ export declare function createWire(transport: Transport): {
642
674
  expected: import("./subtitles.ts").SubtitleStamp;
643
675
  inputOrigin?: import("./subtitles.ts").SubtitleTrack;
644
676
  format?: "srt" | "ass";
645
- title?: string;
677
+ title?: string | TextJson;
646
678
  select?: "primary" | "secondary" | "none";
647
679
  }) => Promise<import("./subtitles.ts").SubtitleState>;
648
680
  select: (target: import("./subtitles.ts").SubtitleTrack | null, expected: import("./subtitles.ts").SubtitleStamp, operationId: number, role?: "primary" | "secondary") => Promise<import("./subtitles.ts").SubtitleState>;
package/dist/wire.js CHANGED
@@ -69,6 +69,7 @@ export function createWire(transport) {
69
69
  commands: commandClient(transport),
70
70
  dialogs: {
71
71
  open: (options) => transport.request("dialogs.open", options),
72
+ /** Message and detail are templates formatted with `args` (player 0.4.3). */
72
73
  confirm: (options) => transport.request("dialogs.confirm", options),
73
74
  },
74
75
  /** The main window: its state, size, level, pointer hiding and picture-in-picture. */
@@ -99,6 +100,8 @@ export function createWire(transport) {
99
100
  /** The palette (`ui.theme` to change it). */
100
101
  appearance: {
101
102
  set: (theme) => transport.request("appearance.set", { theme }),
103
+ /** The interface language for the next start (player 0.4.3). */
104
+ setLanguage: (language) => transport.request("appearance.setLanguage", { language }),
102
105
  },
103
106
  /** Settings pages in the player's options window (`ui.settings`). */
104
107
  settings: {
@@ -190,9 +193,17 @@ export function createWire(transport) {
190
193
  set: (flags) => transport.request("sequence.set", { flags }),
191
194
  step: (direction) => transport.request("sequence.step", { direction }),
192
195
  },
196
+ /** `text` is a template in the plugin's catalog, formatted with `args` (player 0.4.3). */
193
197
  osd: {
194
- show: (text) => transport.request("osd.show", { text }),
195
- panel: (text) => transport.request("osd.panel", { text }),
198
+ show: (text, args) => transport.request("osd.show", {
199
+ text,
200
+ ...(args === undefined ? {} : { args }),
201
+ }),
202
+ panel: (text, args, maxColumns) => transport.request("osd.panel", {
203
+ text,
204
+ ...(args === undefined ? {} : { args }),
205
+ ...(maxColumns === undefined ? {} : { maxColumns }),
206
+ }),
196
207
  },
197
208
  layout: {
198
209
  state: () => transport.request("layout.state"),