@vestra-player/sdk 1.6.5 → 1.6.7
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 +15 -0
- package/dist/api-requests.js +2 -0
- package/dist/commands.d.ts +16 -0
- package/dist/commands.js +6 -0
- package/dist/docs-tree.json.gz +0 -0
- package/dist/host-capabilities.js +3 -0
- package/dist/plugin/bindings.d.ts +9 -0
- package/dist/plugin/bindings.js +41 -0
- package/dist/plugin/commands.js +99 -0
- package/dist/plugin/runtime.js +3 -0
- package/dist/plugin/testing.d.ts +28 -0
- package/dist/plugin/testing.js +94 -2
- package/dist/plugin/types.d.ts +75 -0
- package/dist/wire.d.ts +2 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/slint/vestra/widgets.slint +95 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.6.7
|
|
4
|
+
|
|
5
|
+
**Who must change: nobody.** The 1.6.6 release below, published again under a new number: its npm publish failed (the provenance transparency log returned 500, then 409 for the same tarball), so 1.6.6 never reached npm. Nothing else changed.
|
|
6
|
+
|
|
7
|
+
## 1.6.6
|
|
8
|
+
|
|
9
|
+
**Who must change: nobody.** Additions only: two members of `plugin.commands`, a wire method and event, two Slint widget additions and a `TestHost` method. `apiVersion` stays `vestra/1`, no new capability (`commands` covers them). A plugin that does not use them sends exactly what it sent before. One that uses them needs player 0.7.6 (`"minPlayerVersion": "0.7.6"`): the player builds `plugin` from its own SDK, so on older players `plugin.commands.keys` does not exist (a `TypeError`), and their `@vestra/widgets.slint` has no `KeyHint` or `ToolTipKey`, so a Slint file that uses them does not compile.
|
|
10
|
+
|
|
11
|
+
- **`plugin.commands.keys(id)`** (player 0.7.6): the keys of one of the plugin's **own** commands, as the player's menus and command palette write them (`["W"]`, `["Ctrl+B"]`, `["⌘B"]` on macOS, arrows as `←`), always the current binding: the user's keys after a change, `[]` when the user cleared them or turned on Ignore Default Shortcuts, `[]` for a key another command holds; mouse bindings and (outside macOS) `Cmd` bindings left out. Read-only and synchronous; read in a `bind` function, the view follows every change. Another plugin's commands, or an unknown id, read `[]`; it needs `commands` only, not `bindings.manage`. The first read asks the player (`commands.keys`) and subscribes to the `commands.keys` event, which the player sends only to the plugin whose own labels changed, at most every 200 ms; until the first answer every command reads `[]`. See [Key hints beside controls](docs/explanation/commands.md#key-hints-beside-controls).
|
|
12
|
+
- **`plugin.commands.onKeysChange(listener)`** (player 0.7.6): runs after any of the plugin's keys changed. `commands.keys` is a wake event, like `bindings.changed`: a listener (or a binding that reads `keys`) does not keep a plugin running while idle, and a plugin in standby is started for a change of its keys.
|
|
13
|
+
- **`KeyHint`** in `@vestra/widgets.slint`: a key cap (monospace `font-xs`, `text-dim`, a 1px border, both palettes); an empty `text` draws nothing and takes no space.
|
|
14
|
+
- **`ToolTipArea.keys`** (a list of the new struct `ToolTipKey`, `{ key, text }`) and **`ToolTipArea.detail`**: the tip reads "Brightness [W] − [E] + · double-click: Default" with key caps. Without them the tip is the plain text tip as before.
|
|
15
|
+
- **`TestHost.rebind(command, inputs | null)`** changes or clears a key of the plugin's command as the user does; `TestHost` answers `commands.keys` and pushes the event like the player, writing labels for `host.systemInfo.os` (macOS by default: `⇧W`, `⌘B`). `bindings.set` and `bindings.reset` push it too.
|
|
16
|
+
- The [theme-widgets example](examples/theme-widgets) shows a slider's two commands (F7, F8) as key caps and in its tooltip.
|
|
17
|
+
|
|
3
18
|
## 1.6.5
|
|
4
19
|
|
|
5
20
|
**Who must change: nobody, but run `vestra-plugin i18n extract` again if a command sets a `category`.** A fix in the plugin CLI only: no change to the TypeScript API, the wire or `TestHost`, `apiVersion` stays `vestra/1`, no new capability, no player version needed. A plugin whose commands set `category` as a string literal that is not already a message now has those texts in `locales/messages.pot`, so its next `vestra-plugin i18n check` fails with "messages.pot is out of date" until `i18n extract` runs. A category written with `plugin.i18n.text(…)` was already extracted, and a plugin that sets no category sees no change.
|
package/dist/api-requests.js
CHANGED
|
@@ -98,6 +98,8 @@ export const API_REQUESTS = Object.freeze({
|
|
|
98
98
|
"Resolvers.add": [{ method: "sourceResolvers.register" }],
|
|
99
99
|
"Resolvers.onFileTypesChange": [listen("resolvers.fileTypes")],
|
|
100
100
|
"Commands.add": [{ method: "commands.register" }],
|
|
101
|
+
"Commands.keys": [{ method: "commands.keys" }, listen("commands.keys")],
|
|
102
|
+
"Commands.onKeysChange": [listen("commands.keys")],
|
|
101
103
|
"Host.run": [{ method: "host.run" }],
|
|
102
104
|
"Windows.create": [{ method: "ui.open" }],
|
|
103
105
|
"Dialogs.confirm": [{ method: "dialogs.confirm" }],
|
package/dist/commands.d.ts
CHANGED
|
@@ -345,12 +345,28 @@ export type CommandDeclaration = {
|
|
|
345
345
|
*/
|
|
346
346
|
home?: string;
|
|
347
347
|
};
|
|
348
|
+
/**
|
|
349
|
+
* `commands.keys` (player 0.7.6): each of the plugin's own commands, by
|
|
350
|
+
* local id, with the labels of its current keys; the answer to the request
|
|
351
|
+
* and the data of the event of that name.
|
|
352
|
+
*/
|
|
353
|
+
export type CommandKeys = {
|
|
354
|
+
keys: Record<string, string[]>;
|
|
355
|
+
/** The player's binding table revision: a larger one is newer. */
|
|
356
|
+
revision: number;
|
|
357
|
+
};
|
|
348
358
|
export declare function commandClient(transport: Transport): {
|
|
349
359
|
register: (id: string, revision: number, declaration: CommandDeclaration, state: CommandState, when?: CommandWhen) => Promise<{
|
|
350
360
|
revision: number;
|
|
351
361
|
}>;
|
|
352
362
|
/** Remove the command from the menu and the binding table; the id is free again. */
|
|
353
363
|
unregister: (id: string) => Promise<void>;
|
|
364
|
+
/**
|
|
365
|
+
* The key labels of the plugin's own commands, as menus show them
|
|
366
|
+
* (player 0.7.6); later changes come as the `commands.keys` event, in
|
|
367
|
+
* the same shape. `revision` orders answers and events.
|
|
368
|
+
*/
|
|
369
|
+
keys: () => Promise<CommandKeys>;
|
|
354
370
|
update: (id: string, revision: number, state: CommandState, when?: CommandWhen) => Promise<{
|
|
355
371
|
revision: number;
|
|
356
372
|
}>;
|
package/dist/commands.js
CHANGED
|
@@ -692,6 +692,12 @@ export function commandClient(transport) {
|
|
|
692
692
|
}),
|
|
693
693
|
/** Remove the command from the menu and the binding table; the id is free again. */
|
|
694
694
|
unregister: (id) => transport.request("commands.unregister", { id }),
|
|
695
|
+
/**
|
|
696
|
+
* The key labels of the plugin's own commands, as menus show them
|
|
697
|
+
* (player 0.7.6); later changes come as the `commands.keys` event, in
|
|
698
|
+
* the same shape. `revision` orders answers and events.
|
|
699
|
+
*/
|
|
700
|
+
keys: () => transport.request("commands.keys", {}),
|
|
695
701
|
update: (id, revision, state, when) => transport.request("commands.update", {
|
|
696
702
|
id,
|
|
697
703
|
revision,
|
package/dist/docs-tree.json.gz
CHANGED
|
Binary file
|
|
@@ -84,6 +84,7 @@ export const HOST_CAPABILITIES = {
|
|
|
84
84
|
needs: {
|
|
85
85
|
cases: {
|
|
86
86
|
"bindings.changed": "bindings.manage",
|
|
87
|
+
"commands.keys": "commands",
|
|
87
88
|
"packages.changed": null,
|
|
88
89
|
"video.upscaling.changed": "video.upscaling",
|
|
89
90
|
},
|
|
@@ -96,6 +97,7 @@ export const HOST_CAPABILITIES = {
|
|
|
96
97
|
needs: {
|
|
97
98
|
cases: {
|
|
98
99
|
"bindings.changed": "bindings.manage",
|
|
100
|
+
"commands.keys": "commands",
|
|
99
101
|
"packages.changed": null,
|
|
100
102
|
"video.upscaling.changed": "video.upscaling",
|
|
101
103
|
},
|
|
@@ -116,6 +118,7 @@ export const HOST_CAPABILITIES = {
|
|
|
116
118
|
{ name: "commands.update", needs: "commands" },
|
|
117
119
|
{ name: "commands.unregister", needs: "commands" },
|
|
118
120
|
{ name: "host.run", needs: "commands" },
|
|
121
|
+
{ name: "commands.keys", needs: "commands" },
|
|
119
122
|
],
|
|
120
123
|
name: "commands",
|
|
121
124
|
requires: [],
|
|
@@ -79,3 +79,12 @@ export declare function canonical(text: string): string;
|
|
|
79
79
|
export declare function checkBindings(bindings: readonly string[]): string[];
|
|
80
80
|
/** `plugin.bindings` (`bindings.manage`): the table and the user overlay. */
|
|
81
81
|
export declare const bindingsApi: (scope: ScopeImpl) => Bindings;
|
|
82
|
+
/**
|
|
83
|
+
* How the player's menus write a canonical binding (`bindings.rs`
|
|
84
|
+
* `label_in`), for `TestHost`'s `commands.keys`: `Ctrl+O` and arrows as
|
|
85
|
+
* ← → ↑ ↓ (`Esc`, `PgUp`, `Num5`); on macOS (`mac`) symbols in Apple's
|
|
86
|
+
* order ⌃⌥⇧⌘, a `Ctrl` binding as ⌘ (it answers to ⌘ there) and ⌃ only
|
|
87
|
+
* next to ⌘. `null` for a binding menus do not show: a mouse binding, and
|
|
88
|
+
* a `Cmd` one outside macOS.
|
|
89
|
+
*/
|
|
90
|
+
export declare function keyLabel(binding: string, mac: boolean): string | null;
|
package/dist/plugin/bindings.js
CHANGED
|
@@ -219,3 +219,44 @@ export const bindingsApi = (scope) => ({
|
|
|
219
219
|
});
|
|
220
220
|
},
|
|
221
221
|
});
|
|
222
|
+
/** Named keys whose label is not their name (the player's `label_in`). */
|
|
223
|
+
const NAMED_LABELS = {
|
|
224
|
+
ArrowLeft: "←",
|
|
225
|
+
ArrowRight: "→",
|
|
226
|
+
ArrowUp: "↑",
|
|
227
|
+
ArrowDown: "↓",
|
|
228
|
+
Escape: "Esc",
|
|
229
|
+
PageUp: "PgUp",
|
|
230
|
+
PageDown: "PgDn",
|
|
231
|
+
Delete: "Del",
|
|
232
|
+
Plus: "+",
|
|
233
|
+
NumpadPlus: "Num+",
|
|
234
|
+
NumpadMinus: "Num-",
|
|
235
|
+
NumpadMultiply: "Num*",
|
|
236
|
+
NumpadDivide: "Num/",
|
|
237
|
+
NumpadEnter: "NumEnter",
|
|
238
|
+
NumpadDecimal: "Num.",
|
|
239
|
+
};
|
|
240
|
+
/**
|
|
241
|
+
* How the player's menus write a canonical binding (`bindings.rs`
|
|
242
|
+
* `label_in`), for `TestHost`'s `commands.keys`: `Ctrl+O` and arrows as
|
|
243
|
+
* ← → ↑ ↓ (`Esc`, `PgUp`, `Num5`); on macOS (`mac`) symbols in Apple's
|
|
244
|
+
* order ⌃⌥⇧⌘, a `Ctrl` binding as ⌘ (it answers to ⌘ there) and ⌃ only
|
|
245
|
+
* next to ⌘. `null` for a binding menus do not show: a mouse binding, and
|
|
246
|
+
* a `Cmd` one outside macOS.
|
|
247
|
+
*/
|
|
248
|
+
export function keyLabel(binding, mac) {
|
|
249
|
+
if (binding.startsWith("Mouse:"))
|
|
250
|
+
return null;
|
|
251
|
+
const parts = binding.split("+");
|
|
252
|
+
const input = parts.pop();
|
|
253
|
+
const has = (mod) => parts.includes(mod);
|
|
254
|
+
const [ctrl, alt, shift, cmd] = ["Ctrl", "Alt", "Shift", "Cmd"].map(has);
|
|
255
|
+
if (cmd && !mac)
|
|
256
|
+
return null;
|
|
257
|
+
const mods = mac
|
|
258
|
+
? `${ctrl && cmd ? "⌃" : ""}${alt ? "⌥" : ""}${shift ? "⇧" : ""}${ctrl || cmd ? "⌘" : ""}`
|
|
259
|
+
: parts.map((mod) => `${mod}+`).join("");
|
|
260
|
+
const digit = /^Numpad(\d)$/.exec(input)?.[1];
|
|
261
|
+
return mods + (NAMED_LABELS[input] ?? (digit ? `Num${digit}` : input));
|
|
262
|
+
}
|
package/dist/plugin/commands.js
CHANGED
|
@@ -242,6 +242,105 @@ class CommandHandle {
|
|
|
242
242
|
pending.delete(this.id);
|
|
243
243
|
}
|
|
244
244
|
}
|
|
245
|
+
const NONE = Object.freeze([]);
|
|
246
|
+
const KEYS = "commands.keys";
|
|
247
|
+
/**
|
|
248
|
+
* One per plugin: the SDK's copy of the key labels of the plugin's own
|
|
249
|
+
* commands (player 0.7.6). Started by the first `keys` read or
|
|
250
|
+
* `onKeysChange`: it subscribes to `commands.keys` (a mirror, unless a
|
|
251
|
+
* listener or a binding holds it), then asks once; the player pushes every
|
|
252
|
+
* later change. Answers and events carry the binding table's revision, and
|
|
253
|
+
* an older one than what the copy has is dropped.
|
|
254
|
+
*/
|
|
255
|
+
class KeysService {
|
|
256
|
+
#runtime;
|
|
257
|
+
#keys = new Map();
|
|
258
|
+
#revision = -1;
|
|
259
|
+
#started = false;
|
|
260
|
+
#listeners = new Set();
|
|
261
|
+
constructor(runtime) {
|
|
262
|
+
this.#runtime = runtime;
|
|
263
|
+
}
|
|
264
|
+
start() {
|
|
265
|
+
if (this.#started)
|
|
266
|
+
return;
|
|
267
|
+
this.#started = true;
|
|
268
|
+
const runtime = this.#runtime;
|
|
269
|
+
// Without `commands` the plugin has no commands: every read is [].
|
|
270
|
+
if (!runtime.can("commands"))
|
|
271
|
+
return;
|
|
272
|
+
runtime.on(KEYS, (data) => this.#accept(data), "commands", true);
|
|
273
|
+
// Subscribed first: a change after this answer comes as an event.
|
|
274
|
+
runtime.host.commands.keys().then((answer) => this.#accept(answer),
|
|
275
|
+
// An older player (`unsupported`): no keys, as without a binding.
|
|
276
|
+
() => { });
|
|
277
|
+
}
|
|
278
|
+
#accept(data) {
|
|
279
|
+
const answer = data;
|
|
280
|
+
const revision = answer?.revision;
|
|
281
|
+
const keys = answer?.keys;
|
|
282
|
+
if (typeof revision !== "number" || !keys || typeof keys !== "object")
|
|
283
|
+
return;
|
|
284
|
+
if (revision < this.#revision)
|
|
285
|
+
return;
|
|
286
|
+
this.#revision = revision;
|
|
287
|
+
const next = new Map();
|
|
288
|
+
for (const [id, labels] of Object.entries(keys))
|
|
289
|
+
if (Array.isArray(labels) && labels.length)
|
|
290
|
+
next.set(id, Object.freeze(labels.map(String)));
|
|
291
|
+
const changed = [];
|
|
292
|
+
for (const id of new Set([...this.#keys.keys(), ...next.keys()])) {
|
|
293
|
+
const before = this.#keys.get(id) ?? NONE;
|
|
294
|
+
const after = next.get(id) ?? NONE;
|
|
295
|
+
if (before.length !== after.length ||
|
|
296
|
+
before.some((label, at) => label !== after[at]))
|
|
297
|
+
changed.push(id);
|
|
298
|
+
// The same labels keep the same array.
|
|
299
|
+
else if (after !== NONE)
|
|
300
|
+
next.set(id, before);
|
|
301
|
+
}
|
|
302
|
+
this.#keys = next;
|
|
303
|
+
if (!changed.length)
|
|
304
|
+
return;
|
|
305
|
+
this.#runtime.changed(KEYS, changed);
|
|
306
|
+
for (const listener of [...this.#listeners])
|
|
307
|
+
listener();
|
|
308
|
+
}
|
|
309
|
+
get(id) {
|
|
310
|
+
return this.#keys.get(id) ?? NONE;
|
|
311
|
+
}
|
|
312
|
+
watch(listener) {
|
|
313
|
+
this.#listeners.add(listener);
|
|
314
|
+
const held = this.#runtime.hold(KEYS);
|
|
315
|
+
return {
|
|
316
|
+
dispose: async () => {
|
|
317
|
+
this.#listeners.delete(listener);
|
|
318
|
+
await held.dispose();
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const keysOf = (runtime) => runtime.service("commands.keys", () => new KeysService(runtime));
|
|
245
324
|
export const commands = (scope) => ({
|
|
246
325
|
add: (options) => new CommandHandle(scope, options),
|
|
326
|
+
keys(id) {
|
|
327
|
+
if (typeof id !== "string")
|
|
328
|
+
throw failure("invalid", "keys takes a command id of this plugin");
|
|
329
|
+
const service = keysOf(scope.runtime);
|
|
330
|
+
service.start();
|
|
331
|
+
scope.runtime.reading(KEYS);
|
|
332
|
+
scope.runtime.depends(KEYS, id);
|
|
333
|
+
return service.get(id);
|
|
334
|
+
},
|
|
335
|
+
onKeysChange(listener) {
|
|
336
|
+
const service = keysOf(scope.runtime);
|
|
337
|
+
service.start();
|
|
338
|
+
const watch = service.watch(() => scope.run(listener));
|
|
339
|
+
return scope.add({
|
|
340
|
+
dispose: async () => {
|
|
341
|
+
scope.forget(watch);
|
|
342
|
+
await watch.dispose();
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
},
|
|
247
346
|
});
|
package/dist/plugin/runtime.js
CHANGED
|
@@ -13,6 +13,8 @@ const SDK_DIRECTORY = fileURLToPath(new URL("..", import.meta.url));
|
|
|
13
13
|
export const WAKE_EVENTS = [
|
|
14
14
|
"packages.changed",
|
|
15
15
|
"bindings.changed",
|
|
16
|
+
// Player 0.7.6: sent only to the plugin whose own keys changed.
|
|
17
|
+
"commands.keys",
|
|
16
18
|
"appearance.changed",
|
|
17
19
|
"resolvers.fileTypes",
|
|
18
20
|
];
|
|
@@ -25,6 +27,7 @@ const STATE = {
|
|
|
25
27
|
"window.state": { name: "plugin.window.state", when: true },
|
|
26
28
|
"appearance.changed": { name: "plugin.appearance", when: true },
|
|
27
29
|
"timeline.changed": { name: "plugin.timeline.marks", when: false },
|
|
30
|
+
"commands.keys": { name: "plugin.commands.keys", when: false },
|
|
28
31
|
"playback.backend": { name: "plugin.subtitles.state", when: false },
|
|
29
32
|
"resolvers.fileTypes": {
|
|
30
33
|
name: "plugin.resolvers.fileTypes",
|
package/dist/plugin/testing.d.ts
CHANGED
|
@@ -2403,6 +2403,34 @@ export declare class TestHost implements Transport {
|
|
|
2403
2403
|
* ```
|
|
2404
2404
|
*/
|
|
2405
2405
|
get conflicts(): string[][];
|
|
2406
|
+
/**
|
|
2407
|
+
* Change the keys of one of the plugin's commands as the user does in
|
|
2408
|
+
* Keyboard Shortcuts (SDK 1.6.6): `inputs` replaces its keys (`[]`
|
|
2409
|
+
* clears them, as Ignore Default Shortcuts does for every default),
|
|
2410
|
+
* `null` gives it its own defaults back. It writes
|
|
2411
|
+
* {@link TestHost.bindings | bindings.overlay}, and the plugin hears it
|
|
2412
|
+
* as from the player: `bindings.changed` and, when what
|
|
2413
|
+
* `plugin.commands.keys` reads changed, `commands.keys`.
|
|
2414
|
+
*
|
|
2415
|
+
* @example Check that a key hint follows a rebinding
|
|
2416
|
+
* The view showed W for Brightness −; after the user moves it to
|
|
2417
|
+
* Shift+W, it shows the new key. `rebind` changes the overlay and
|
|
2418
|
+
* pushes the new labels, and `settle` lets the binding run again. The
|
|
2419
|
+
* test host is a Mac unless `systemInfo.os` says otherwise, hence ⇧W.
|
|
2420
|
+
* ```ts
|
|
2421
|
+
* import assert from "node:assert/strict";
|
|
2422
|
+
* declare const host: TestHost;
|
|
2423
|
+
* // ---cut---
|
|
2424
|
+
* host.rebind("brightness.down", ["Shift+W"]);
|
|
2425
|
+
* await host.settle();
|
|
2426
|
+
* assert.equal(host.window("picture").props["brightness-down"], "⇧W");
|
|
2427
|
+
* ```
|
|
2428
|
+
*
|
|
2429
|
+
* @capability None.
|
|
2430
|
+
* @throws `Error` `command` is not a command of the plugin.
|
|
2431
|
+
* @since 1.6.6
|
|
2432
|
+
*/
|
|
2433
|
+
rebind(command: string, inputs: readonly string[] | null): void;
|
|
2406
2434
|
/**
|
|
2407
2435
|
* The binding overlay `plugin.bindings.set` writes (`"<plugin>/<command>"`
|
|
2408
2436
|
* to inputs; preset it to simulate saved overrides). `press` honours 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, canonical } from "./bindings.js";
|
|
81
|
+
import { BINDING_INPUTS, canonical, keyLabel } 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";
|
|
@@ -3080,7 +3080,7 @@ export class TestHost {
|
|
|
3080
3080
|
const scope = declaration.focus ? "panel" : "global";
|
|
3081
3081
|
// The user overlay (`bindings.set`) replaces a command's own bindings.
|
|
3082
3082
|
const overridden = this.bindings.overlay.get(this.#bindingKey(id));
|
|
3083
|
-
for (const binding of overridden ?? declaration.bindings) {
|
|
3083
|
+
for (const binding of overridden ?? declaration.bindings ?? []) {
|
|
3084
3084
|
const key = `${scope} ${binding}`;
|
|
3085
3085
|
claims.set(key, [...(claims.get(key) ?? []), id]);
|
|
3086
3086
|
}
|
|
@@ -3091,6 +3091,87 @@ export class TestHost {
|
|
|
3091
3091
|
#bindingKey(id) {
|
|
3092
3092
|
return this.coreCommands.has(id) ? id : `${this.manifestId}/${id}`;
|
|
3093
3093
|
}
|
|
3094
|
+
/**
|
|
3095
|
+
* Change the keys of one of the plugin's commands as the user does in
|
|
3096
|
+
* Keyboard Shortcuts (SDK 1.6.6): `inputs` replaces its keys (`[]`
|
|
3097
|
+
* clears them, as Ignore Default Shortcuts does for every default),
|
|
3098
|
+
* `null` gives it its own defaults back. It writes
|
|
3099
|
+
* {@link TestHost.bindings | bindings.overlay}, and the plugin hears it
|
|
3100
|
+
* as from the player: `bindings.changed` and, when what
|
|
3101
|
+
* `plugin.commands.keys` reads changed, `commands.keys`.
|
|
3102
|
+
*
|
|
3103
|
+
* @example Check that a key hint follows a rebinding
|
|
3104
|
+
* The view showed W for Brightness −; after the user moves it to
|
|
3105
|
+
* Shift+W, it shows the new key. `rebind` changes the overlay and
|
|
3106
|
+
* pushes the new labels, and `settle` lets the binding run again. The
|
|
3107
|
+
* test host is a Mac unless `systemInfo.os` says otherwise, hence ⇧W.
|
|
3108
|
+
* ```ts
|
|
3109
|
+
* import assert from "node:assert/strict";
|
|
3110
|
+
* declare const host: TestHost;
|
|
3111
|
+
* // ---cut---
|
|
3112
|
+
* host.rebind("brightness.down", ["Shift+W"]);
|
|
3113
|
+
* await host.settle();
|
|
3114
|
+
* assert.equal(host.window("picture").props["brightness-down"], "⇧W");
|
|
3115
|
+
* ```
|
|
3116
|
+
*
|
|
3117
|
+
* @capability None.
|
|
3118
|
+
* @throws `Error` `command` is not a command of the plugin.
|
|
3119
|
+
* @since 1.6.6
|
|
3120
|
+
*/
|
|
3121
|
+
rebind(command, inputs) {
|
|
3122
|
+
if (!this.commands.has(command))
|
|
3123
|
+
throw new Error(`test host: no command ${command}`);
|
|
3124
|
+
const key = this.#bindingKey(command);
|
|
3125
|
+
if (inputs === null)
|
|
3126
|
+
this.bindings.overlay.delete(key);
|
|
3127
|
+
else
|
|
3128
|
+
this.bindings.overlay.set(key, inputs.map(canonical));
|
|
3129
|
+
this.emit("bindings.changed", null);
|
|
3130
|
+
this.#keysChanged();
|
|
3131
|
+
}
|
|
3132
|
+
/** The labels `commands.keys` last answered or pushed, as JSON. */
|
|
3133
|
+
#keysSent = "";
|
|
3134
|
+
/** The binding table revision of `commands.keys` (a counter here). */
|
|
3135
|
+
#keysRevision = 0;
|
|
3136
|
+
/**
|
|
3137
|
+
* `commands.keys` as the player answers it: each of the plugin's own
|
|
3138
|
+
* commands with the labels of the keys it holds now (the overlay, else
|
|
3139
|
+
* its defaults; not a key an earlier command holds), as menus write
|
|
3140
|
+
* them on `systemInfo.os`.
|
|
3141
|
+
*/
|
|
3142
|
+
#keys() {
|
|
3143
|
+
const claims = this.#claims();
|
|
3144
|
+
const mac = this.systemInfo.os === "macos";
|
|
3145
|
+
const keys = {};
|
|
3146
|
+
for (const [id, { declaration }] of this.commands) {
|
|
3147
|
+
const scope = declaration.focus ? "panel" : "global";
|
|
3148
|
+
const current = this.bindings.overlay.get(this.#bindingKey(id)) ??
|
|
3149
|
+
declaration.bindings ??
|
|
3150
|
+
[];
|
|
3151
|
+
const labels = [];
|
|
3152
|
+
for (const input of current) {
|
|
3153
|
+
if (claims.get(`${scope} ${input}`)?.[0] !== id)
|
|
3154
|
+
continue;
|
|
3155
|
+
const label = keyLabel(input, mac);
|
|
3156
|
+
if (label !== null && !labels.includes(label))
|
|
3157
|
+
labels.push(label);
|
|
3158
|
+
}
|
|
3159
|
+
keys[id] = labels;
|
|
3160
|
+
}
|
|
3161
|
+
return { keys, revision: this.#keysRevision };
|
|
3162
|
+
}
|
|
3163
|
+
/** Push `commands.keys` when the labels changed and the plugin listens. */
|
|
3164
|
+
#keysChanged() {
|
|
3165
|
+
this.#keysRevision++;
|
|
3166
|
+
if (!this.subscriptions.has("commands.keys"))
|
|
3167
|
+
return;
|
|
3168
|
+
const now = this.#keys();
|
|
3169
|
+
const text = JSON.stringify(now.keys);
|
|
3170
|
+
if (text === this.#keysSent)
|
|
3171
|
+
return;
|
|
3172
|
+
this.#keysSent = text;
|
|
3173
|
+
this.emit("commands.keys", now);
|
|
3174
|
+
}
|
|
3094
3175
|
/**
|
|
3095
3176
|
* The binding overlay `plugin.bindings.set` writes (`"<plugin>/<command>"`
|
|
3096
3177
|
* to inputs; preset it to simulate saved overrides). `press` honours it.
|
|
@@ -4182,11 +4263,20 @@ export class TestHost {
|
|
|
4182
4263
|
await host.settle();
|
|
4183
4264
|
},
|
|
4184
4265
|
});
|
|
4266
|
+
// The player pushes the new command's keys after the answer.
|
|
4267
|
+
queueMicrotask(() => this.#keysChanged());
|
|
4185
4268
|
return { revision: 1 };
|
|
4186
4269
|
},
|
|
4187
4270
|
"commands.unregister": (p) => {
|
|
4188
4271
|
if (!this.commands.delete(String(p.id)))
|
|
4189
4272
|
throw new HostError("missing", `command ${p.id} is not registered`);
|
|
4273
|
+
queueMicrotask(() => this.#keysChanged());
|
|
4274
|
+
},
|
|
4275
|
+
// Player 0.7.6: the labels of the plugin's own commands.
|
|
4276
|
+
"commands.keys": () => {
|
|
4277
|
+
const now = this.#keys();
|
|
4278
|
+
this.#keysSent = JSON.stringify(now.keys);
|
|
4279
|
+
return now;
|
|
4190
4280
|
},
|
|
4191
4281
|
"commands.update": (p) => {
|
|
4192
4282
|
// The request itself, as the player reads it (capabilities/commands.rs
|
|
@@ -5102,6 +5192,7 @@ export class TestHost {
|
|
|
5102
5192
|
throw new HostError("missing", `no command ${key}`);
|
|
5103
5193
|
this.bindings.overlay.set(key, [...p.inputs]);
|
|
5104
5194
|
this.emit("bindings.changed", null);
|
|
5195
|
+
this.#keysChanged();
|
|
5105
5196
|
},
|
|
5106
5197
|
"bindings.reset": (p) => {
|
|
5107
5198
|
if (p.plugin === undefined)
|
|
@@ -5109,6 +5200,7 @@ export class TestHost {
|
|
|
5109
5200
|
else
|
|
5110
5201
|
this.bindings.overlay.delete(`${p.plugin}/${p.command}`);
|
|
5111
5202
|
this.emit("bindings.changed", null);
|
|
5203
|
+
this.#keysChanged();
|
|
5112
5204
|
},
|
|
5113
5205
|
"packages.list": () => structuredClone(this.packages),
|
|
5114
5206
|
"packages.setEnabled": (p) => {
|
package/dist/plugin/types.d.ts
CHANGED
|
@@ -1451,6 +1451,81 @@ export interface Commands {
|
|
|
1451
1451
|
* @since 1.0.0
|
|
1452
1452
|
*/
|
|
1453
1453
|
add(options: CommandOptions): Command;
|
|
1454
|
+
/**
|
|
1455
|
+
* The keys of one of this plugin's own commands (its local id, as in
|
|
1456
|
+
* {@link CommandOptions.id}), written as the player's menus and command
|
|
1457
|
+
* palette write them: `["W"]`, `["Ctrl+B"]`, `["⌘B"]` on macOS, arrows as
|
|
1458
|
+
* `←`. Read-only, and always what is bound now: the user's own keys
|
|
1459
|
+
* after a change in Keyboard Shortcuts, none when the user cleared them
|
|
1460
|
+
* or turned on Ignore Default Shortcuts, and none for a key another
|
|
1461
|
+
* command holds. Mouse bindings are left out, and so are `Cmd`
|
|
1462
|
+
* bindings outside macOS (as in the menus). The first label is the one
|
|
1463
|
+
* the menus show.
|
|
1464
|
+
*
|
|
1465
|
+
* @remarks
|
|
1466
|
+
* For key hints beside a panel's controls (`KeyHint` and the `keys` of
|
|
1467
|
+
* `ToolTipArea` in `@vestra/widgets.slint`). Read it in a
|
|
1468
|
+
* {@link View.bind} function and the view follows every change; or
|
|
1469
|
+
* listen with {@link Commands.onKeysChange}. The first read asks the
|
|
1470
|
+
* player, and until its answer arrives (a few milliseconds) every
|
|
1471
|
+
* command reads `[]`; a binding that read it runs again then.
|
|
1472
|
+
*
|
|
1473
|
+
* Only the plugin's own commands: an id the plugin has not added (or
|
|
1474
|
+
* another plugin's) reads `[]`. Needs nothing beyond `commands`, not
|
|
1475
|
+
* `bindings.manage` (the whole binding table, every plugin's, is
|
|
1476
|
+
* {@link Bindings.list}). Without the `commands` capability every
|
|
1477
|
+
* command reads `[]`. Players before 0.7.6 have no `keys` (the player
|
|
1478
|
+
* builds `plugin` from its own SDK) and no `KeyHint` in their widgets,
|
|
1479
|
+
* so declare `"minPlayerVersion": "0.7.6"`.
|
|
1480
|
+
*
|
|
1481
|
+
* @example Show a slider's keys beside it
|
|
1482
|
+
* The Brightness row shows its two keys, W and E by default, as key caps
|
|
1483
|
+
* (`KeyHint` in `ui/picture.slint`). `keys` is read inside the `bind` function,
|
|
1484
|
+
* so the view gets the new labels whenever the user changes a key or
|
|
1485
|
+
* clears it (an empty list hides the cap).
|
|
1486
|
+
* ```ts
|
|
1487
|
+
* const pictureWindow = plugin.windows.create("ui/picture.slint");
|
|
1488
|
+
* plugin.commands.add({ id: "brightness.down", path: ["@video", "Brightness −"], bindings: ["W"], run() {} });
|
|
1489
|
+
* plugin.commands.add({ id: "brightness.up", path: ["@video", "Brightness +"], bindings: ["E"], run() {} });
|
|
1490
|
+
* pictureWindow.bind(() => ({
|
|
1491
|
+
* // Read in bind: the view follows every key change; "" hides a cap.
|
|
1492
|
+
* brightnessDown: plugin.commands.keys("brightness.down")[0] ?? "",
|
|
1493
|
+
* brightnessUp: plugin.commands.keys("brightness.up")[0] ?? "",
|
|
1494
|
+
* }));
|
|
1495
|
+
* ```
|
|
1496
|
+
*
|
|
1497
|
+
* @capability commands
|
|
1498
|
+
* @player 0.7.6 (declare `"minPlayerVersion": "0.7.6"`; older players
|
|
1499
|
+
* have no `keys`: calling it throws a `TypeError`).
|
|
1500
|
+
* @since 1.6.6
|
|
1501
|
+
*/
|
|
1502
|
+
keys(id: string): readonly string[];
|
|
1503
|
+
/**
|
|
1504
|
+
* Run `listener` after the keys of any of this plugin's commands changed
|
|
1505
|
+
* (read them again with {@link Commands.keys}): the user changed or
|
|
1506
|
+
* cleared a key, turned Ignore Default Shortcuts on or off, a command with
|
|
1507
|
+
* keys was added or removed, or the player's first answer arrived. At most every
|
|
1508
|
+
* 200 ms. A plugin in standby that listens is started for it, as for a
|
|
1509
|
+
* key change in {@link Bindings.onChange}.
|
|
1510
|
+
*
|
|
1511
|
+
* @capability commands
|
|
1512
|
+
*
|
|
1513
|
+
* @example Name the current key in a status line
|
|
1514
|
+
* The OSD tells the user the new key after they change it. The listener
|
|
1515
|
+
* carries no data; it reads the labels with `keys`.
|
|
1516
|
+
* ```ts
|
|
1517
|
+
* plugin.commands.onKeysChange(() => {
|
|
1518
|
+
* const [key] = plugin.commands.keys("toggle-blur");
|
|
1519
|
+
* if (key) void plugin.osd.show("Blur: {}", [key]);
|
|
1520
|
+
* else void plugin.osd.show("Blur has no key");
|
|
1521
|
+
* });
|
|
1522
|
+
* ```
|
|
1523
|
+
*
|
|
1524
|
+
* @player 0.7.6 (declare `"minPlayerVersion": "0.7.6"`; older players
|
|
1525
|
+
* have no `onKeysChange`).
|
|
1526
|
+
* @since 1.6.6
|
|
1527
|
+
*/
|
|
1528
|
+
onKeysChange(listener: () => void): Disposable;
|
|
1454
1529
|
}
|
|
1455
1530
|
/**
|
|
1456
1531
|
* An image file for a Slint `image` property, made by {@link Ui.image}.
|
package/dist/wire.d.ts
CHANGED
|
@@ -441,7 +441,7 @@ export interface Transport {
|
|
|
441
441
|
locale?: string;
|
|
442
442
|
}
|
|
443
443
|
/** The events a plugin subscribes to (`events.subscribe`). */
|
|
444
|
-
type HostEvent = "source.state" | "playback.backend" | "window.state" | "packages.changed" | "bindings.changed" | "timeline.changed" | "appearance.changed" | "resolvers.fileTypes" | "video.upscaling.changed";
|
|
444
|
+
type HostEvent = "source.state" | "playback.backend" | "window.state" | "packages.changed" | "bindings.changed" | "commands.keys" | "timeline.changed" | "appearance.changed" | "resolvers.fileTypes" | "video.upscaling.changed";
|
|
445
445
|
export declare function createWire(transport: Transport): {
|
|
446
446
|
processing: {
|
|
447
447
|
state: () => Promise<import("./processing.ts").ProcessingState>;
|
|
@@ -453,6 +453,7 @@ export declare function createWire(transport: Transport): {
|
|
|
453
453
|
revision: number;
|
|
454
454
|
}>;
|
|
455
455
|
unregister: (id: string) => Promise<void>;
|
|
456
|
+
keys: () => Promise<import("./commands.ts").CommandKeys>;
|
|
456
457
|
update: (id: string, revision: number, state: import("./commands.ts").CommandState, when?: import("./commands.ts").CommandWhen) => Promise<{
|
|
457
458
|
revision: number;
|
|
458
459
|
}>;
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Vestra Player SDK
|
|
2
2
|
|
|
3
|
-
> @vestra-player/sdk 1.6.
|
|
3
|
+
> @vestra-player/sdk 1.6.7: 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,5 +1,5 @@
|
|
|
1
1
|
// Compact controls built on the Vestra theme (core-ui-design.md §2.1):
|
|
2
|
-
// import { Button, Slider, ToolTipArea, UrlInputRow } from "@vestra/widgets.slint";
|
|
2
|
+
// import { Button, KeyHint, Slider, ToolTipArea, ToolTipKey, UrlInputRow } from "@vestra/widgets.slint";
|
|
3
3
|
// import { Choice, Check, Radio, RadioGroup, RadioMark, Tab, Tabs } from "@vestra/widgets.slint";
|
|
4
4
|
// Icons: @vestra/icons.slint. Other controls come from std-widgets, which the
|
|
5
5
|
// host compiles as fluent-dark and switches to light for a light palette.
|
|
@@ -9,10 +9,48 @@
|
|
|
9
9
|
// so a click on a panel control leaves the player's shortcuts (Space, the
|
|
10
10
|
// arrows) with the video, as on macOS. A focused control shows a 1px accent
|
|
11
11
|
// ring.
|
|
12
|
-
import { TextEdit, ListView } from "std-widgets.slint";
|
|
12
|
+
import { TextEdit, ListView, Palette } from "std-widgets.slint";
|
|
13
13
|
import { Theme } from "theme.slint";
|
|
14
14
|
import { Icon } from "icons.slint";
|
|
15
15
|
|
|
16
|
+
// A key cap: the label of a shortcut as the player's menus write it
|
|
17
|
+
// (`plugin.commands.keys`: `W`, `Ctrl+B`, `⌘B` on macOS), in small
|
|
18
|
+
// monospace `text-dim` type inside a 1px border. Put it at the right end of
|
|
19
|
+
// a control's row, the minus key before the plus key (`W` `E`). An empty
|
|
20
|
+
// `text` draws nothing and takes no space, so a command without a key (or
|
|
21
|
+
// one the user cleared) shows no cap; the element stays the same either way.
|
|
22
|
+
export component KeyHint {
|
|
23
|
+
in property <string> text;
|
|
24
|
+
private property <bool> shown: root.text != "";
|
|
25
|
+
visible: root.shown;
|
|
26
|
+
width: root.shown ? max(16px, label.preferred-width + 8px) : 0px;
|
|
27
|
+
height: root.shown ? 15px : 0px;
|
|
28
|
+
accessible-role: AccessibleRole.text;
|
|
29
|
+
accessible-label: root.text;
|
|
30
|
+
Rectangle {
|
|
31
|
+
border-width: 1px;
|
|
32
|
+
border-radius: 3px;
|
|
33
|
+
// The cap's edge: a step above the panel in both palettes.
|
|
34
|
+
border-color: Theme.icon-disabled;
|
|
35
|
+
background: Theme.bg-list;
|
|
36
|
+
label := Text {
|
|
37
|
+
text: root.text;
|
|
38
|
+
font-family: Theme.font-mono;
|
|
39
|
+
font-size: Theme.font-xs;
|
|
40
|
+
color: Theme.text-dim;
|
|
41
|
+
horizontal-alignment: center;
|
|
42
|
+
vertical-alignment: center;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// One key in a tooltip (`ToolTipArea.keys`): its label (`KeyHint`) and what
|
|
48
|
+
// it does, a word or a sign (`−`, `+`). An empty `key` is left out.
|
|
49
|
+
export struct ToolTipKey {
|
|
50
|
+
key: string,
|
|
51
|
+
text: string,
|
|
52
|
+
}
|
|
53
|
+
|
|
16
54
|
// Hover for about half a second to show `text` beside the pointer. The tip
|
|
17
55
|
// closes when the pointer leaves, or presses on content that does not take
|
|
18
56
|
// the press itself; it shows again on the next hover. It is Slint's
|
|
@@ -20,8 +58,16 @@ import { Icon } from "icons.slint";
|
|
|
20
58
|
// pointer, and the tip does not take the keyboard focus. (Up to SDK 1.5.20 the tip was a
|
|
21
59
|
// PopupWindow, which swallowed the first press after it showed and took the
|
|
22
60
|
// focus while it was open.)
|
|
61
|
+
// With `keys` (SDK 1.6.6) the tip names the control's keys after its title,
|
|
62
|
+
// each as a key cap and what it does: "Brightness [W] − [E] +"; `detail`
|
|
63
|
+
// follows dimmed ("· double-click: Default"). Changing `keys` or `detail`
|
|
64
|
+
// changes the tip's text, never which elements exist while hovering.
|
|
23
65
|
export component ToolTipArea {
|
|
24
66
|
in property <string> text;
|
|
67
|
+
in property <[ToolTipKey]> keys;
|
|
68
|
+
in property <string> detail;
|
|
69
|
+
// The tip names keys or a detail: drawn with key caps.
|
|
70
|
+
private property <bool> rich: root.keys.length > 0 || root.detail != "";
|
|
25
71
|
// The tip is showing (read-only); it follows the tip's delay closely,
|
|
26
72
|
// not exactly.
|
|
27
73
|
out property <bool> showing;
|
|
@@ -44,10 +90,56 @@ export component ToolTipArea {
|
|
|
44
90
|
}
|
|
45
91
|
}
|
|
46
92
|
// Removing it closes an open tip.
|
|
47
|
-
if root.text != "" && !root.done: Tooltip {
|
|
93
|
+
if root.text != "" && !root.done && !root.rich: Tooltip {
|
|
48
94
|
// Interpolated text is plain: `*` or `_` in it are not markup.
|
|
49
95
|
text: @markdown("\{root.text}");
|
|
50
96
|
}
|
|
97
|
+
// The tip with key caps, drawn as the text tip is (std-widgets'
|
|
98
|
+
// ToolTipImpl). Its own element: one Tooltip per parent.
|
|
99
|
+
if root.text != "" && !root.done && root.rich: Rectangle {
|
|
100
|
+
width: 100%;
|
|
101
|
+
height: 100%;
|
|
102
|
+
Tooltip {
|
|
103
|
+
Rectangle {
|
|
104
|
+
background: Palette.alternate-background;
|
|
105
|
+
border-radius: 4px;
|
|
106
|
+
border-width: 1px;
|
|
107
|
+
border-color: Palette.border;
|
|
108
|
+
HorizontalLayout {
|
|
109
|
+
padding-left: 9px;
|
|
110
|
+
padding-right: 9px;
|
|
111
|
+
padding-top: 6px;
|
|
112
|
+
padding-bottom: 6px;
|
|
113
|
+
Text {
|
|
114
|
+
text: root.text;
|
|
115
|
+
color: Palette.foreground;
|
|
116
|
+
vertical-alignment: center;
|
|
117
|
+
}
|
|
118
|
+
for entry in root.keys: HorizontalLayout {
|
|
119
|
+
padding-left: entry.key == "" ? 0px : 10px;
|
|
120
|
+
spacing: entry.key == "" ? 0px : 4px;
|
|
121
|
+
VerticalLayout {
|
|
122
|
+
alignment: center;
|
|
123
|
+
KeyHint { text: entry.key; }
|
|
124
|
+
}
|
|
125
|
+
Text {
|
|
126
|
+
text: entry.key == "" ? "" : entry.text;
|
|
127
|
+
color: Palette.foreground;
|
|
128
|
+
vertical-alignment: center;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
HorizontalLayout {
|
|
132
|
+
padding-left: root.detail == "" ? 0px : 10px;
|
|
133
|
+
Text {
|
|
134
|
+
text: root.detail;
|
|
135
|
+
color: Theme.text-dim;
|
|
136
|
+
vertical-alignment: center;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
51
143
|
}
|
|
52
144
|
Timer {
|
|
53
145
|
interval: 500ms;
|