@vestra-player/sdk 1.6.3 → 1.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.6.4
4
+
5
+ **Who must change: nobody, but check tests that compare whole `bindings.list()` entries.** Additions only: two optional `CommandOptions` fields, one optional `BindingInfo` field, a `COMMAND_LIMITS` entry and a new built-in command. `apiVersion` stays `vestra/1`, no new capability. A plugin that does not set the new fields sends exactly what it sent before; one that sets them needs player 0.7.6 (`"minPlayerVersion": "0.7.6"`), because older players refuse a declaration with fields they do not know (`invalid`).
6
+
7
+ - **`CommandOptions.category`** (player 0.7.6): what a command is about, in a word or two (`"Subtitle Sync"`), English source text translated like the title, or a `Text`. The player's new command palette (Ctrl+Shift+P, ⌘⇧P on macOS) and lists of commands name a command `Category: Title`, so a title such as "Earlier 0.5 s" or "Off" still says what it changes outside its menu. Without one the player uses the menu path above the item, then the plugin's name. 1–80 bytes (`COMMAND_LIMITS.categoryBytes`), checked like the other command texts. See [The command palette and categories](docs/explanation/commands.md#the-command-palette-and-categories).
8
+ - **`CommandOptions.home`** (player 0.7.6): the local id of the plugin's command that opens the window or panel where this command's control lives; the palette shows that place. Checked like a command id.
9
+ - **`BindingInfo.category`** (player 0.7.6): the shown category from `bindings.list`, `null` without one, for a keyboard shortcuts page that names commands like the palette.
10
+ - **`BUILTIN_COMMANDS`** lists the player's **Command Palette** (`host/palette`, `Ctrl+Shift+P`, outside the menu). From player 0.7.6 a plugin that declares `Ctrl+Shift+P` as a default key loses it to this command (the player's own commands win conflicts); no built-in plugin declares it.
11
+ - **`bindings.list()` entries now carry `category`** (`null` when the command declares none), in the player and in `TestHost`. A test that compares a whole entry with `deepEqual` now needs `category` in the expected value.
12
+ - **The palette lists a disabled command only when it gives a `reason`.** A command that is disabled without one is left out, whatever its keys (as VS Code hides a command whose `when` is false), so give a `reason` to a command that should stay findable while it is disabled.
13
+ - `TestHost` checks `category` and `home` as the player does, shows the translated category in `FakeCommand.display.category` (not enumerable, like `reason`) and in its `bindings.list` entries.
14
+
3
15
  ## 1.6.3
4
16
 
5
17
  **Who must change: nobody.** Documentation only: no change to the TypeScript API, the wire or `TestHost`, `apiVersion` stays `vestra/1`, no new capability, no player version needed.
@@ -78,6 +78,13 @@ export declare const COMMAND_LIMITS: {
78
78
  * @since 1.5.48
79
79
  */
80
80
  readonly groupBytes: 80;
81
+ /**
82
+ * UTF-8 bytes of a category (`CommandOptions.category`), a text value
83
+ * counted by its English rendering.
84
+ *
85
+ * @since 1.6.4
86
+ */
87
+ readonly categoryBytes: 80;
81
88
  /**
82
89
  * UTF-8 bytes of a state's reason, as shown in the player's
83
90
  * language.
@@ -327,6 +334,16 @@ export type CommandDeclaration = {
327
334
  icon: string | Json;
328
335
  order: number;
329
336
  };
337
+ /**
338
+ * The command's category, source text or a text value (player 0.7.6).
339
+ * Sent only when set: older players refuse unknown declaration fields.
340
+ */
341
+ category?: string | TextJson;
342
+ /**
343
+ * The local id of the command that opens where this one lives (player
344
+ * 0.7.6). Sent only when set, like `category`.
345
+ */
346
+ home?: string;
330
347
  };
331
348
  export declare function commandClient(transport: Transport): {
332
349
  register: (id: string, revision: number, declaration: CommandDeclaration, state: CommandState, when?: CommandWhen) => Promise<{
package/dist/commands.js CHANGED
@@ -74,6 +74,13 @@ export const COMMAND_LIMITS = {
74
74
  * @since 1.5.48
75
75
  */
76
76
  groupBytes: 80,
77
+ /**
78
+ * UTF-8 bytes of a category (`CommandOptions.category`), a text value
79
+ * counted by its English rendering.
80
+ *
81
+ * @since 1.6.4
82
+ */
83
+ categoryBytes: 80,
77
84
  /**
78
85
  * UTF-8 bytes of a state's reason, as shown in the player's
79
86
  * language.
@@ -594,6 +601,7 @@ host | quit | Quit | Cmd+Q
594
601
  host | enter-fullscreen | (Enter Fullscreen)
595
602
  host | exit-fullscreen | (Leave Fullscreen)
596
603
  host | sidebar | (Toggle Sidebar)
604
+ host | palette | (Command Palette) | Ctrl+Shift+P
597
605
  vestra.info | info.brief | (Brief Info) | Shift+Tab
598
606
  vestra.playlist | playlist.play | (Play) | Enter, P | panel
599
607
  vestra.playlist | playlist.list-add | (Add Files…) | Ctrl+I | panel
Binary file
@@ -189,6 +189,7 @@ export const bindingsApi = (scope) => ({
189
189
  title: info.title,
190
190
  path: info.path,
191
191
  key: info.key ?? null,
192
+ category: info.category ?? null,
192
193
  scope: info.scope,
193
194
  defaults: info.defaults,
194
195
  current: info.current,
@@ -26,13 +26,18 @@ function declare(options) {
26
26
  throw failure("invalid", `command ${options.id}: title or path required`);
27
27
  if (path.length > 8)
28
28
  throw failure("invalid", `command ${options.id}: path has 1–8 segments`);
29
- for (const segment of [title, ...path])
29
+ const category = options.category;
30
+ for (const segment of [
31
+ title,
32
+ ...path,
33
+ ...(category === undefined ? [] : [category]),
34
+ ])
30
35
  if (typeof segment !== "string") {
31
36
  const problem = isTextValue(segment)
32
37
  ? "$vestraList" in segment
33
38
  ? "a menu text is a string or a text, not a list"
34
39
  : textProblem(segment)
35
- : "title and path segments are strings or texts";
40
+ : "title, path segments and category are strings or texts";
36
41
  if (problem)
37
42
  throw failure("invalid", `command ${options.id}: ${problem}`);
38
43
  }
@@ -59,6 +64,14 @@ function declare(options) {
59
64
  // Only when set: players before 0.5.10 refuse the field.
60
65
  if (options.button !== undefined)
61
66
  declaration.button = button(options.id, options.button, path.length > 0);
67
+ // Only when set: players before 0.7.6 refuse the fields.
68
+ if (category !== undefined)
69
+ declaration.category = category;
70
+ if (options.home !== undefined) {
71
+ if (typeof options.home !== "string")
72
+ throw failure("invalid", `command ${options.id}: home is a command id`);
73
+ declaration.home = options.home;
74
+ }
62
75
  return declaration;
63
76
  }
64
77
  /** `CommandOptions.button`, checked as the player checks it, in wire form. */
@@ -270,6 +270,14 @@ export interface FakeCommand {
270
270
  * @since 1.5.11
271
271
  */
272
272
  reason?: string | null;
273
+ /**
274
+ * The category as shown (`CommandOptions.category`, translated with
275
+ * the plugin's catalog), `null` without one. Not enumerable, like
276
+ * `reason`.
277
+ *
278
+ * @since 1.6.4
279
+ */
280
+ category?: string | null;
273
281
  };
274
282
  /**
275
283
  * The conditions the plugin sent for `enabled` and `checked`
@@ -354,7 +354,7 @@ function menuTextKey(value) {
354
354
  * The declaration checks of the player's `Registry::register`, in its
355
355
  * order, on the keyed title and path (`@<id>` already replaced).
356
356
  */
357
- function checkDeclaration(id, title, path, declaration) {
357
+ function checkDeclaration(id, title, path, declaration, category = null) {
358
358
  commandText(id, "localId", COMMAND_LIMITS.idBytes);
359
359
  if (!/^[A-Za-z0-9._-]+$/.test(id))
360
360
  throw commandInvalid("localId permits ASCII letters, digits, '.', '_' and '-'");
@@ -367,6 +367,17 @@ function checkDeclaration(id, title, path, declaration) {
367
367
  if (typeof group !== "string")
368
368
  throw new HostError("invalid", "group is a string");
369
369
  commandText(group, "group", COMMAND_LIMITS.groupBytes);
370
+ // Player 0.7.6: the category and the home command's local id.
371
+ if (category !== null)
372
+ commandText(category, "category", COMMAND_LIMITS.categoryBytes);
373
+ const home = declaration.home;
374
+ if (home !== undefined && home !== null) {
375
+ if (typeof home !== "string")
376
+ throw new HostError("invalid", "home is a command id");
377
+ commandText(home, "home", COMMAND_LIMITS.idBytes);
378
+ if (!/^[A-Za-z0-9._-]+$/.test(home))
379
+ throw commandInvalid("home permits ASCII letters, digits, '.', '_' and '-'");
380
+ }
370
381
  const order = declaration.order ?? 0;
371
382
  if (!Number.isInteger(order) ||
372
383
  Math.abs(order) > COMMAND_LIMITS.order)
@@ -669,7 +680,7 @@ export class TestHost {
669
680
  : formatTemplate(shown, args);
670
681
  }
671
682
  /** A declared menu path and title as the player shows them. */
672
- #display(path, title, reason = null) {
683
+ #display(path, title, reason = null, category = null) {
673
684
  const keys = menuKeys(path.map((segment) => typeof segment === "string" ? segment : textKey(segment)));
674
685
  const display = {
675
686
  title: this.translate(title),
@@ -684,6 +695,10 @@ export class TestHost {
684
695
  value: reason === null ? null : this.translate(reason),
685
696
  enumerable: false,
686
697
  });
698
+ Object.defineProperty(display, "category", {
699
+ value: category === null ? null : this.translate(category),
700
+ enumerable: false,
701
+ });
687
702
  return display;
688
703
  }
689
704
  /**
@@ -3230,6 +3245,8 @@ export class TestHost {
3230
3245
  key: declaration.path?.length
3231
3246
  ? menuKeys(declaration.path.map((segment) => typeof segment === "string" ? segment : textKey(segment)))
3232
3247
  : null,
3248
+ // Player 0.7.6: the category as shown, `null` without one.
3249
+ category: shown?.display.category ?? null,
3233
3250
  scope: scope,
3234
3251
  defaults: declaration.bindings,
3235
3252
  current,
@@ -4086,7 +4103,11 @@ export class TestHost {
4086
4103
  if (menu !== null && keys.length < 2)
4087
4104
  throw commandInvalid(`menu id @${menu} needs an item after it`);
4088
4105
  const path = menuKeys(keys);
4089
- checkDeclaration(id, title, path, declaration);
4106
+ // Player 0.7.6: the category, keyed like a menu text.
4107
+ const category = declaration.category === undefined || declaration.category === null
4108
+ ? null
4109
+ : menuTextKey(declaration.category);
4110
+ checkDeclaration(id, title, path, declaration, category);
4090
4111
  if (button !== undefined && button !== null) {
4091
4112
  if (path.length === 0)
4092
4113
  throw commandInvalid("button needs a menu path: a bar button is also a menu item");
@@ -4144,7 +4165,7 @@ export class TestHost {
4144
4165
  this.commands.set(id, {
4145
4166
  declaration,
4146
4167
  get display() {
4147
- return host.#display(declaration.path ?? [], declaration.title, this.state.reason);
4168
+ return host.#display(declaration.path ?? [], declaration.title, this.state.reason, declaration.category ?? null);
4148
4169
  },
4149
4170
  ...(p.when ? { when: p.when } : {}),
4150
4171
  get state() {
@@ -1112,6 +1112,87 @@ export interface CommandOptions {
1112
1112
  * defaults to the last path segment.
1113
1113
  */
1114
1114
  title?: string | Text;
1115
+ /**
1116
+ * What the command is about, in a word or two (`"Subtitle Sync"`,
1117
+ * `"Picture"`): English source text, shown translated like `title`, or a
1118
+ * {@link Text}. The player's command palette (Ctrl+Shift+P, ⌘⇧P on macOS)
1119
+ * and lists of commands name a command `Category: Title`, so a short title
1120
+ * such as "Earlier 0.5 s", "Off" or "90°" still says what it changes once
1121
+ * it is out of the menu. Without it the player uses the menu path above
1122
+ * the item, then the plugin's name.
1123
+ *
1124
+ * 1–80 UTF-8 bytes (a text value counted by its English rendering), no
1125
+ * control characters, no whitespace at either end
1126
+ * (`COMMAND_LIMITS.categoryBytes`); the player refuses anything else with
1127
+ * `invalid`. Sent only when set: players before 0.7.6 refuse the field
1128
+ * with `invalid`, so declare `"minPlayerVersion": "0.7.6"` when you set
1129
+ * it.
1130
+ *
1131
+ * @example Name commands that have no menu path
1132
+ * The palette lists "Subtitle Sync: Earlier 0.5 s" and "Subtitle Sync:
1133
+ * Later 0.5 s"; without a category both would read as a bare "Earlier
1134
+ * 0.5 s", indistinguishable from an audio delay command of the same name.
1135
+ * ```ts
1136
+ * declare function shift(seconds: number): void;
1137
+ * // ---cut---
1138
+ * for (const [id, title, seconds] of [
1139
+ * ["sync-earlier", "Earlier 0.5 s", -0.5],
1140
+ * ["sync-later", "Later 0.5 s", 0.5],
1141
+ * ] as const)
1142
+ * plugin.commands.add({
1143
+ * id,
1144
+ * title,
1145
+ * category: "Subtitle Sync", // the palette reads "Subtitle Sync: Earlier 0.5 s"
1146
+ * bindings: [id === "sync-earlier" ? "." : ","],
1147
+ * run: () => shift(seconds),
1148
+ * });
1149
+ * ```
1150
+ *
1151
+ * @player 0.7.6 (declare `"minPlayerVersion": "0.7.6"`; older players
1152
+ * refuse the declaration with `invalid`).
1153
+ * @since 1.6.4
1154
+ */
1155
+ category?: string | Text;
1156
+ /**
1157
+ * The id of this plugin's command that opens the window or panel where
1158
+ * this command's control lives (`"tune"` for a Subtitle Tuning… command
1159
+ * whose window has the sync slider). The player's command palette shows
1160
+ * that place next to the command. A local id, like `id`: ASCII letters,
1161
+ * digits, `.`, `_` and `-`, 1–128 bytes; the player refuses anything else
1162
+ * with `invalid`. The command it names need not be registered yet; while
1163
+ * it is not, the palette shows the place it would show without `home`:
1164
+ * the menu path above the item, "Menu" for a top-level item, nothing for
1165
+ * a command outside the menu. Sent only when set: players
1166
+ * before 0.7.6 refuse the field with `invalid`, so declare
1167
+ * `"minPlayerVersion": "0.7.6"` when you set it.
1168
+ *
1169
+ * @example Point a key command at its window
1170
+ * The palette row for "Subtitle Sync: Earlier 0.5 s" also says where the
1171
+ * same setting can be changed by hand: the Subtitle Tuning window that the
1172
+ * `tune` command opens.
1173
+ * ```ts
1174
+ * declare const tuning: { show(): void };
1175
+ * declare function shift(seconds: number): void;
1176
+ * // ---cut---
1177
+ * plugin.commands.add({
1178
+ * id: "tune",
1179
+ * path: ["@subtitles", "Subtitle Tuning…"],
1180
+ * run: () => tuning.show(),
1181
+ * });
1182
+ * plugin.commands.add({
1183
+ * id: "sync-earlier",
1184
+ * title: "Earlier 0.5 s",
1185
+ * category: "Subtitle Sync",
1186
+ * home: "tune", // the palette shows "Subtitle Tuning" as its place
1187
+ * run: () => shift(-0.5),
1188
+ * });
1189
+ * ```
1190
+ *
1191
+ * @player 0.7.6 (declare `"minPlayerVersion": "0.7.6"`; older players
1192
+ * refuse the declaration with `invalid`).
1193
+ * @since 1.6.4
1194
+ */
1195
+ home?: string;
1115
1196
  /**
1116
1197
  * Sibling order in the menu, ascending (integer, default 0). A submenu sits
1117
1198
  * where its first command sits, except a built-in menu, which keeps its
@@ -9201,6 +9282,16 @@ export interface BindingInfo extends BindingRef {
9201
9282
  * @since 1.5.11
9202
9283
  */
9203
9284
  key?: string[] | null;
9285
+ /**
9286
+ * The command's category as shown ({@link CommandOptions.category},
9287
+ * translated), `null` when it declares none (and always from players
9288
+ * before 0.7.6). Always set by `list()`; optional so that test fakes
9289
+ * written for older SDKs still type-check. A list of commands names one `Category: Title`; without a
9290
+ * category, use the menu path (`path`) as before.
9291
+ *
9292
+ * @since 1.6.4
9293
+ */
9294
+ category?: string | null;
9204
9295
  /** Global, or only while the plugin's panel has focus. */
9205
9296
  scope: "global" | "panel";
9206
9297
  /** The command's own bindings. */
package/dist/wire.d.ts CHANGED
@@ -209,6 +209,8 @@ export interface BindingTable {
209
209
  path: string[] | null;
210
210
  /** The path's keys (player 0.4.3). */
211
211
  key?: string[] | null;
212
+ /** The shown category (player 0.7.6). */
213
+ category?: string | null;
212
214
  scope: "global" | "panel";
213
215
  defaults: string[];
214
216
  current: string[];
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # Vestra Player SDK
2
2
 
3
- > @vestra-player/sdk 1.6.3: 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. The documentation is a tree of short pages read by path, like `go doc`: the same pages and API as https://vestrasys.github.io/vestra-player-sdk/.
3
+ > @vestra-player/sdk 1.6.4: 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. The documentation is a tree of short pages read by path, like `go doc`: the same pages and API as https://vestrasys.github.io/vestra-player-sdk/.
4
4
 
5
5
  ## Read the documentation
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vestra-player/sdk",
3
- "version": "1.6.3",
3
+ "version": "1.6.4",
4
4
  "description": "Plugin API types, test host and plugin CLI for Vestra Player (apiVersion vestra/1)",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {