@qilin-agent/client-shortcuts 0.0.0-stage → 3.1.3

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/lib/index.js ADDED
@@ -0,0 +1,23 @@
1
+ import z from "@qilin-agent/schemastery";
2
+ //#region lib/types/config.js
3
+ /** Window-local timing accepted by the Host and browser keyboard service. */
4
+ /** Validated deployment settings for fixed keyboard sequences. */
5
+ const Config = z.object({ stopSequenceMs: z.natural().min(1).max(2147483646).default(500) });
6
+ //#endregion
7
+ //#region lib/types/index.js
8
+ /**
9
+ * Embed validated keyboard settings in product pages.
10
+ * @param ctx - Host context serving browser pages.
11
+ * @param config - sequence timing adopted when the page loads.
12
+ */
13
+ function apply(ctx, config) {
14
+ ctx.on("webserver/index-inject", (table) => {
15
+ table.push({
16
+ kind: "global",
17
+ name: "__QILIN_SHORTCUTS_CONFIG__",
18
+ value: config
19
+ });
20
+ });
21
+ }
22
+ //#endregion
23
+ export { Config, apply };
@@ -0,0 +1,524 @@
1
+ import { assertNever } from "@qilin-agent/util-values";
2
+ import { randomUUID } from "@qilin-agent/util-crypto";
3
+ //#region lib/types/binding.js
4
+ const keyNames = {
5
+ Slash: "/",
6
+ Comma: ",",
7
+ Period: ".",
8
+ Backslash: "\\",
9
+ Backquote: "`",
10
+ Minus: "-",
11
+ Equal: "=",
12
+ BracketLeft: "[",
13
+ BracketRight: "]",
14
+ Semicolon: ";",
15
+ Quote: "'",
16
+ Enter: "Enter",
17
+ Escape: "Esc",
18
+ Space: "Space",
19
+ Tab: "Tab",
20
+ Backspace: "Backspace",
21
+ Delete: "Delete",
22
+ ArrowUp: "↑",
23
+ ArrowDown: "↓",
24
+ ArrowLeft: "←",
25
+ ArrowRight: "→"
26
+ };
27
+ const modifierOrder = [
28
+ "control",
29
+ "alt",
30
+ "shift",
31
+ "meta"
32
+ ];
33
+ /**
34
+ * Expand logical modifiers, deduplicate, and validate the physical code.
35
+ * @param binding - declared binding.
36
+ * @param platform - receiving device platform.
37
+ * @returns canonical binding; unsupported codes throw during registration.
38
+ */
39
+ function normalizeBinding(binding, platform) {
40
+ const codes = [binding.code, ...binding.secondCode === void 0 ? [] : [binding.secondCode]];
41
+ codes.sort();
42
+ for (const code of codes) if (!/^(Key[A-Z]|Digit[0-9]|F([1-9]|1[0-9]|2[0-4]))$/u.test(code) && !Object.hasOwn(keyNames, code)) throw new Error(`Unsupported shortcut code: ${code}`);
43
+ if (codes.length === 2 && codes[0] === codes[1]) throw new Error("Shortcut keys must be distinct");
44
+ const modifiers = new Set(binding.modifiers.map((value) => value === "primary" ? platform === "macos" ? "meta" : "control" : value));
45
+ return {
46
+ code: codes[0],
47
+ ...codes[1] === void 0 ? {} : { secondCode: codes[1] },
48
+ modifiers: modifierOrder.filter((value) => modifiers.has(value))
49
+ };
50
+ }
51
+ /**
52
+ * Produce an exact-match index from a normalized binding.
53
+ * @param binding - normalized physical key and modifiers.
54
+ * @returns stable index used for both matching and conflict checks.
55
+ */
56
+ function bindingKey(binding) {
57
+ const codes = binding.secondCode === void 0 ? [binding.code] : [binding.code, binding.secondCode].sort();
58
+ return [...binding.modifiers, ...codes].join("+");
59
+ }
60
+ /**
61
+ * Format keycaps and ARIA; Windows separates modifiers with plus signs, while chord keys remain adjacent.
62
+ * @param binding - normalized binding, or null for an unbound command.
63
+ * @param platform - receiving device platform.
64
+ * @returns visible keycaps; two-key chords omit ARIA shortcuts, which only support one non-modifier key.
65
+ */
66
+ function presentBinding(binding, platform) {
67
+ if (binding === null) return {
68
+ keys: [],
69
+ aria: void 0
70
+ };
71
+ const key = keyNames[binding.code] ?? binding.code.replace(/^(Key|Digit)/u, "");
72
+ const symbols = platform === "macos" ? {
73
+ control: "⌃",
74
+ alt: "⌥",
75
+ shift: "⇧",
76
+ meta: "⌘"
77
+ } : {
78
+ control: "Ctrl",
79
+ alt: "Alt",
80
+ shift: "Shift",
81
+ meta: "Meta"
82
+ };
83
+ const ariaNames = {
84
+ control: "Control",
85
+ alt: "Alt",
86
+ shift: "Shift",
87
+ meta: "Meta"
88
+ };
89
+ const ariaKey = binding.code === "Space" ? "Space" : binding.code === "Escape" ? "Escape" : binding.code.startsWith("Arrow") ? binding.code : key;
90
+ const second = binding.secondCode === void 0 ? [] : [keyNames[binding.secondCode] ?? binding.secondCode.replace(/^(Key|Digit)/u, "")];
91
+ const keys = [...binding.modifiers.map((value) => symbols[value]), key];
92
+ return {
93
+ keys: [...platform === "windows" ? keys.flatMap((label, index) => index === 0 ? [label] : ["+", label]) : keys, ...second],
94
+ aria: binding.secondCode === void 0 ? [...binding.modifiers.map((value) => ariaNames[value]), ariaKey].join("+") : void 0
95
+ };
96
+ }
97
+ /**
98
+ * Check Web combinations: Windows and macOS also admit any three or four modifiers; Linux retains the limited set.
99
+ * @param binding - normalized candidate.
100
+ * @param platform - receiving device platform.
101
+ * @returns whether this combination is admitted; admission does not guarantee browser or system delivery.
102
+ */
103
+ function isWebBindingAllowed(binding, platform) {
104
+ if (binding.secondCode !== void 0) return false;
105
+ if (platform === "windows" || platform === "macos") {
106
+ if (binding.modifiers.length >= 3) return true;
107
+ const primary = platform === "macos" ? "meta" : "control";
108
+ if (binding.modifiers.length === 1 && (["Comma", "Backslash"].includes(binding.code) && binding.modifiers[0] === primary || binding.code === "Backquote" && binding.modifiers[0] === "control")) return true;
109
+ if (binding.modifiers.length === 2 && binding.modifiers.includes(primary) && (binding.modifiers.includes("alt") || binding.modifiers.includes("shift"))) return true;
110
+ }
111
+ return [
112
+ {
113
+ code: "Slash",
114
+ modifiers: ["primary"]
115
+ },
116
+ {
117
+ code: "Comma",
118
+ modifiers: ["primary", "shift"]
119
+ },
120
+ {
121
+ code: "Period",
122
+ modifiers: ["primary", "shift"]
123
+ }
124
+ ].some((candidate) => bindingKey(binding) === bindingKey(normalizeBinding(candidate, platform)));
125
+ }
126
+ //#endregion
127
+ //#region lib/types/configuration.js
128
+ /** Validated preference documents and deterministic conflict resolution, without browser dependencies. */
129
+ const record = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
130
+ const commandPattern = /^[a-z][a-zA-Z0-9-]*(?:\.[a-zA-Z][a-zA-Z0-9-]*)+$/u;
131
+ /**
132
+ * Validate JSON binding fields before normalization; unknown fields are rejected to prevent lossy rewrites.
133
+ * @param value - file or IPC input.
134
+ * @returns a binding with a supported physical code, or null for explicit removal.
135
+ */
136
+ function parseBinding(value) {
137
+ if (value === null) return null;
138
+ if (!record(value) || Object.keys(value).some((key) => key !== "code" && key !== "secondCode" && key !== "modifiers") || typeof value.code !== "string" || !Array.isArray(value.modifiers) || Object.hasOwn(value, "secondCode") && typeof value.secondCode !== "string" || !value.modifiers.every((modifier) => typeof modifier === "string" && [
139
+ "primary",
140
+ "control",
141
+ "alt",
142
+ "shift",
143
+ "meta"
144
+ ].includes(modifier))) throw new Error("Invalid shortcut binding");
145
+ const binding = {
146
+ code: value.code,
147
+ modifiers: value.modifiers,
148
+ ...typeof value.secondCode === "string" ? { secondCode: value.secondCode } : {}
149
+ };
150
+ normalizeBinding(binding, "windows");
151
+ return binding;
152
+ }
153
+ /**
154
+ * Decode the complete document while preserving dormant command overrides.
155
+ * @param raw - stored JSON, or null for a missing document.
156
+ * @returns the accepted document or a classified read failure.
157
+ */
158
+ function parseShortcutDocument(raw) {
159
+ if (raw === null) return {
160
+ schemaVersion: 1,
161
+ profiles: {}
162
+ };
163
+ try {
164
+ const value = JSON.parse(raw);
165
+ if (!record(value)) return "invalid";
166
+ if (typeof value.schemaVersion === "number" && value.schemaVersion > 2) return "future";
167
+ if (value.schemaVersion !== 1 && value.schemaVersion !== 2 || !record(value.profiles) || Object.keys(value).some((key) => key !== "schemaVersion" && key !== "profiles")) return "invalid";
168
+ const profiles = {};
169
+ for (const [profile, overrides] of Object.entries(value.profiles)) {
170
+ if (!/^(desktop|web):(macos|windows|linux)$/u.test(profile) || !record(overrides)) return "invalid";
171
+ const bindings = {};
172
+ for (const [id, binding] of Object.entries(overrides)) {
173
+ if (!commandPattern.test(id)) return "invalid";
174
+ const parsed = parseBinding(binding);
175
+ if (value.schemaVersion === 1 && parsed?.secondCode !== void 0) return "invalid";
176
+ bindings[id] = parsed;
177
+ }
178
+ profiles[profile] = bindings;
179
+ }
180
+ return {
181
+ schemaVersion: value.schemaVersion,
182
+ profiles
183
+ };
184
+ } catch (_error) {
185
+ return "invalid";
186
+ }
187
+ }
188
+ /**
189
+ * Check system, editor, and browser reservations using expanded physical modifiers.
190
+ * @param binding - normalized candidate.
191
+ * @param runtime - receiving application shell.
192
+ * @param platform - receiving device.
193
+ * @returns the rejection reason, or null when this combination is allowed.
194
+ */
195
+ function bindingIssue(binding, runtime, platform) {
196
+ const { code, modifiers } = binding;
197
+ if (runtime === "desktop" && (platform === "windows" || platform === "macos")) return null;
198
+ if (binding.secondCode !== void 0) return "unsupported-key";
199
+ if (modifiers.length === 0 || modifiers.every((modifier) => modifier === "shift")) return "modifier-required";
200
+ if ((platform === "windows" || platform === "macos") && modifiers.length >= 3) return null;
201
+ if (runtime === "web" && (platform === "windows" || platform === "macos") && isWebBindingAllowed(binding, platform)) return null;
202
+ const primary = modifiers.includes(platform === "macos" ? "meta" : "control");
203
+ if ([
204
+ "Escape",
205
+ "Tab",
206
+ "Space",
207
+ "Backspace",
208
+ "Delete",
209
+ "ArrowUp",
210
+ "ArrowDown",
211
+ "ArrowLeft",
212
+ "ArrowRight"
213
+ ].includes(code) || code === "Enter" && !modifiers.includes("alt") || primary && [
214
+ "KeyC",
215
+ "KeyV",
216
+ "KeyX",
217
+ "KeyZ",
218
+ "KeyY",
219
+ "KeyQ",
220
+ "KeyH"
221
+ ].includes(code) || primary && code === "KeyA" && !modifiers.includes("shift") || platform !== "macos" && (modifiers.includes("meta") || modifiers.includes("alt") && ["F4", "F2"].includes(code)) || platform === "macos" && modifiers.includes("control") && modifiers.includes("meta") || platform === "macos" && modifiers.includes("alt") && !primary) return "reserved";
222
+ if (runtime === "web" && !isWebBindingAllowed(binding, platform)) return "unsupported-browser";
223
+ return null;
224
+ }
225
+ /**
226
+ * Select the command owner's explicit default for one device profile.
227
+ * @param definition - command identity and per-profile defaults.
228
+ * @param runtime - receiving shell.
229
+ * @param platform - receiving device.
230
+ * @returns the declared physical binding, or undefined for an unbound action.
231
+ */
232
+ function resolveShortcutDefault(definition, runtime, platform) {
233
+ return definition.defaults[`${runtime}:${platform}`];
234
+ }
235
+ /**
236
+ * Resolve overrides and conflicts independently of registration order. Explicit overrides displace defaults.
237
+ * @param definitions - active commands.
238
+ * @param document - accepted preferences.
239
+ * @param runtime - receiving shell.
240
+ * @param platform - receiving device.
241
+ * @returns every active command, including unavailable conflicting bindings.
242
+ */
243
+ function effectiveShortcuts(definitions, document, runtime, platform) {
244
+ const overrides = document.profiles[`${runtime}:${platform}`] ?? {};
245
+ const fixed = definitions.flatMap((row) => row.fixed?.map((binding) => ({
246
+ id: row.id,
247
+ binding: normalizeBinding(binding, platform)
248
+ })) ?? []);
249
+ const rows = definitions.filter((row) => row.fixed === void 0).map(({ id, defaults }) => {
250
+ const modified = Object.hasOwn(overrides, id);
251
+ const candidate = modified ? overrides[id] : resolveShortcutDefault({
252
+ id,
253
+ defaults
254
+ }, runtime, platform);
255
+ const binding = candidate == null ? null : normalizeBinding(candidate, platform);
256
+ return {
257
+ id,
258
+ binding,
259
+ modified,
260
+ issue: binding === null ? null : bindingIssue(binding, runtime, platform)
261
+ };
262
+ });
263
+ return rows.map((row) => {
264
+ const binding = row.binding;
265
+ return {
266
+ ...row,
267
+ conflicts: binding === null ? [] : [...new Set([...rows.filter((other) => other.id !== row.id && other.binding !== null && other.issue === null && overlappingBindings(other.binding, binding) && (!row.modified || other.modified)).map((other) => other.id), ...fixed.filter((other) => overlappingBindings(other.binding, binding)).map((other) => other.id)])]
268
+ };
269
+ });
270
+ }
271
+ /**
272
+ * Detect identical combinations or a single key contained in a two-key chord.
273
+ * @param left - normalized candidate.
274
+ * @param right - normalized occupied binding.
275
+ * @returns whether both bindings require the same modifiers and overlap.
276
+ */
277
+ function overlappingBindings(left, right) {
278
+ if (left.modifiers.join("+") !== right.modifiers.join("+")) return false;
279
+ if (left.secondCode !== void 0 && right.secondCode !== void 0) return bindingKey(left) === bindingKey(right);
280
+ return [left.code, left.secondCode].some((code) => code !== void 0 && (code === right.code || code === right.secondCode));
281
+ }
282
+ /**
283
+ * Apply an edit without modifying other profiles or dormant overrides.
284
+ * @param document - accepted document.
285
+ * @param edit - validated operation.
286
+ * @param runtime - current shell.
287
+ * @param platform - current device.
288
+ * @returns the candidate document, pending conflict checks and durable storage.
289
+ */
290
+ function editShortcutDocument(document, edit, runtime, platform) {
291
+ const schemaVersion = runtime === "desktop" && (platform === "macos" || platform === "windows") ? 2 : document.schemaVersion;
292
+ const profile = `${runtime}:${platform}`;
293
+ let overrides = { ...document.profiles[profile] };
294
+ switch (edit.type) {
295
+ case "set":
296
+ overrides[edit.id] = edit.binding;
297
+ break;
298
+ case "reset": {
299
+ const { [edit.id]: _removed, ...remaining } = overrides;
300
+ overrides = remaining;
301
+ break;
302
+ }
303
+ case "reset-all":
304
+ overrides = {};
305
+ break;
306
+ /* v8 ignore next -- parseShortcutEdit validates this closed union before persistence. */
307
+ default: return assertNever(edit, "shortcut edit");
308
+ }
309
+ return {
310
+ schemaVersion,
311
+ profiles: {
312
+ ...document.profiles,
313
+ [profile]: overrides
314
+ }
315
+ };
316
+ }
317
+ /**
318
+ * Validate a preference edit at the Desktop IPC boundary.
319
+ * @param value - untrusted renderer request.
320
+ * @returns the constrained operation; malformed requests throw.
321
+ */
322
+ function parseShortcutEdit(value) {
323
+ if (!record(value)) throw new Error("Invalid shortcut edit");
324
+ if (value.type === "reset-all" && Object.keys(value).length === 1) return { type: value.type };
325
+ if (typeof value.id !== "string" || !commandPattern.test(value.id)) throw new Error("Invalid shortcut command");
326
+ if (value.type === "reset" && Object.keys(value).length === 2) return {
327
+ type: "reset",
328
+ id: value.id
329
+ };
330
+ if (value.type === "set" && Object.keys(value).length === 3) return {
331
+ type: "set",
332
+ id: value.id,
333
+ binding: parseBinding(value.binding)
334
+ };
335
+ throw new Error("Invalid shortcut edit");
336
+ }
337
+ /**
338
+ * Validate the trusted product's serializable command catalog at IPC ingress.
339
+ * @param value - renderer-supplied active command definitions.
340
+ * @returns validated definitions; duplicate IDs, overlapping defaults, and unsupported combinations throw.
341
+ */
342
+ function parseShortcutDefinitions(value) {
343
+ if (!Array.isArray(value)) throw new Error("Invalid shortcut catalog");
344
+ const ids = /* @__PURE__ */ new Set();
345
+ for (const entry of value) {
346
+ if (!record(entry) || typeof entry.id !== "string" || !commandPattern.test(entry.id) || ids.has(entry.id) || !record(entry.defaults) || Object.keys(entry).some((key) => key !== "id" && key !== "defaults" && key !== "fixed")) throw new Error("Invalid shortcut definition");
347
+ ids.add(entry.id);
348
+ if (Object.hasOwn(entry, "fixed")) {
349
+ if (!Array.isArray(entry.fixed) || entry.fixed.length === 0 || Object.keys(entry.defaults).length > 0) throw new Error("Invalid fixed shortcut definition");
350
+ for (const binding of entry.fixed) if (parseBinding(binding) === null) throw new Error("Invalid fixed shortcut binding");
351
+ }
352
+ for (const [profile, candidate] of Object.entries(entry.defaults)) {
353
+ if (!/^(desktop|web):(macos|windows|linux)$/u.test(profile)) throw new Error("Invalid shortcut profile");
354
+ if (parseBinding(candidate) === null) throw new Error("Invalid shortcut default");
355
+ }
356
+ }
357
+ const definitions = value;
358
+ for (const runtime of ["desktop", "web"]) for (const platform of [
359
+ "macos",
360
+ "windows",
361
+ "linux"
362
+ ]) {
363
+ const bindings = [];
364
+ for (const entry of definitions) {
365
+ const binding = resolveShortcutDefault(entry, runtime, platform);
366
+ if (binding === void 0) continue;
367
+ const normalized = normalizeBinding(binding, platform);
368
+ if (bindings.some((other) => overlappingBindings(other, normalized)) || bindingIssue(normalized, runtime, platform) !== null) throw new Error("Conflicting or reserved shortcut default");
369
+ bindings.push(normalized);
370
+ }
371
+ }
372
+ return definitions;
373
+ }
374
+ //#endregion
375
+ //#region lib/types/persistence.js
376
+ /** Serialized preference transactions; storage owners publish only accepted writes or read diagnostics. */
377
+ /**
378
+ * Create a disabled initial snapshot for asynchronous adapter startup.
379
+ * @returns a fresh configuration with no accepted persisted state.
380
+ */
381
+ function initialShortcutConfig() {
382
+ return {
383
+ revision: randomUUID(),
384
+ sequence: 0,
385
+ document: {
386
+ schemaVersion: 1,
387
+ profiles: {}
388
+ },
389
+ status: "loading",
390
+ error: null,
391
+ usingDefaults: true
392
+ };
393
+ }
394
+ /** Single-writer coordinator shared by localStorage and Electron's atomic file adapter. */
395
+ var ShortcutPersistence = class {
396
+ storage;
397
+ runtime;
398
+ platform;
399
+ rereadBeforeWrite;
400
+ publish;
401
+ snapshot = initialShortcutConfig();
402
+ raw;
403
+ queue = Promise.resolve();
404
+ definitions = null;
405
+ active = true;
406
+ constructor(storage, runtime, platform, rereadBeforeWrite, publish) {
407
+ this.storage = storage;
408
+ this.runtime = runtime;
409
+ this.platform = platform;
410
+ this.rereadBeforeWrite = rereadBeforeWrite;
411
+ this.publish = publish;
412
+ }
413
+ /**
414
+ * Install or revoke a product catalog and invalidate drafts from its previous lifetime.
415
+ * @param definitions - current trusted definitions, or null while the product is not ready.
416
+ */
417
+ setDefinitions(definitions) {
418
+ this.definitions = definitions;
419
+ this.accept({ ...this.snapshot });
420
+ }
421
+ /** Stop accepting edits or publishing late completions. */
422
+ dispose() {
423
+ this.active = false;
424
+ }
425
+ /**
426
+ * Read the current file; failures retain the last accepted document and disable ordinary writes.
427
+ * @returns the accepted snapshot or diagnostic snapshot.
428
+ */
429
+ readCurrent() {
430
+ return this.serialize(() => this.read());
431
+ }
432
+ /**
433
+ * Compare the draft revision, validate the complete candidate, then persist before publishing.
434
+ * @param edit - constrained preference operation.
435
+ * @param revision - state against which the user reviewed the edit.
436
+ * @returns a classified outcome and the currently accepted snapshot.
437
+ */
438
+ edit(edit, revision) {
439
+ return this.serialize(async () => {
440
+ if (this.rereadBeforeWrite) await this.read();
441
+ const result = (status) => ({
442
+ status,
443
+ snapshot: this.snapshot
444
+ });
445
+ if (!this.active || this.definitions === null || this.snapshot.status === "loading") return result("not-ready");
446
+ if (revision !== this.snapshot.revision) return result("stale");
447
+ if (this.snapshot.status === "unreadable") return result("unreadable");
448
+ if ((edit.type === "set" || edit.type === "reset") && !this.definitions.some((row) => row.id === edit.id && row.fixed === void 0)) return result("not-ready");
449
+ const document = editShortcutDocument(this.snapshot.document, edit, this.runtime, this.platform);
450
+ const rows = effectiveShortcuts(this.definitions, document, this.runtime, this.platform);
451
+ const invalid = rows.find((row) => (edit.type === "reset-all" || row.id === edit.id) && (row.issue !== null || row.conflicts.length > 0));
452
+ const displaced = edit.type === "set" ? rows.find((row) => row.conflicts.includes(edit.id)) : void 0;
453
+ if (invalid !== void 0 || displaced !== void 0) return {
454
+ ...result("conflict"),
455
+ ...invalid?.issue ? { issue: invalid.issue } : {},
456
+ conflicts: invalid?.conflicts.length ? invalid.conflicts : displaced === void 0 ? [] : [displaced.id]
457
+ };
458
+ try {
459
+ const raw = `${JSON.stringify(document, null, 2)}\n`;
460
+ await this.storage.write(raw);
461
+ this.raw = raw;
462
+ this.accept({
463
+ ...this.snapshot,
464
+ document,
465
+ status: "ready",
466
+ error: null,
467
+ usingDefaults: false
468
+ });
469
+ return result("saved");
470
+ } catch (_error) {
471
+ return result("write-failed");
472
+ }
473
+ });
474
+ }
475
+ serialize(operation) {
476
+ const next = this.queue.then(operation);
477
+ this.queue = next.catch(() => void 0);
478
+ return next;
479
+ }
480
+ accept(snapshot) {
481
+ this.snapshot = {
482
+ ...snapshot,
483
+ sequence: this.snapshot.sequence + 1,
484
+ revision: randomUUID()
485
+ };
486
+ if (!this.active) return;
487
+ try {
488
+ this.publish(this.snapshot);
489
+ } catch (error) {
490
+ console.error("Shortcut configuration subscriber failed:", error);
491
+ }
492
+ }
493
+ async read() {
494
+ let raw;
495
+ try {
496
+ raw = await this.storage.read();
497
+ } catch (_error) {
498
+ if (this.snapshot.error !== "read") this.accept({
499
+ ...this.snapshot,
500
+ status: "unreadable",
501
+ error: "read"
502
+ });
503
+ return this.snapshot;
504
+ }
505
+ if (raw === this.raw && this.snapshot.error !== "read") return this.snapshot;
506
+ this.raw = raw;
507
+ const document = parseShortcutDocument(raw);
508
+ if (typeof document === "string") this.accept({
509
+ ...this.snapshot,
510
+ status: "unreadable",
511
+ error: document
512
+ });
513
+ else this.accept({
514
+ ...this.snapshot,
515
+ document,
516
+ status: "ready",
517
+ error: null,
518
+ usingDefaults: raw === null
519
+ });
520
+ return this.snapshot;
521
+ }
522
+ };
523
+ //#endregion
524
+ export { ShortcutPersistence, bindingIssue, bindingKey, editShortcutDocument, effectiveShortcuts, initialShortcutConfig, isWebBindingAllowed, normalizeBinding, overlappingBindings, parseBinding, parseShortcutDefinitions, parseShortcutDocument, parseShortcutEdit, presentBinding, resolveShortcutDefault };
@@ -0,0 +1,55 @@
1
+ /** Physical-key protocol shared by browser commands and desktop adapters; no DOM or runtime state. */
2
+ import type { Branded } from '@qilin-agent/brand';
3
+ /** Stable command identity owned by the registering feature. */
4
+ export type ShortcutCommandId = Branded<'ShortcutCommandId'>;
5
+ /** Operating system of the device receiving input. */
6
+ export type ShortcutPlatform = 'macos' | 'windows' | 'linux';
7
+ /** Application shell selecting the default bindings. */
8
+ export type ShortcutRuntime = 'desktop' | 'web';
9
+ /** Runtime and operating system selecting one explicit default and preference profile. */
10
+ export type ShortcutProfile = `${ShortcutRuntime}:${ShortcutPlatform}`;
11
+ /** Logical primary expands to Meta on macOS and Control elsewhere. */
12
+ export type ShortcutModifier = 'primary' | 'control' | 'alt' | 'shift' | 'meta';
13
+ /** One or two distinct physical keys held together, plus an exact set of modifiers. */
14
+ export interface ShortcutBinding {
15
+ readonly code: string;
16
+ readonly secondCode?: string;
17
+ readonly modifiers: readonly ShortcutModifier[];
18
+ }
19
+ /** Normalized modifier order is also the keycap order. */
20
+ export interface NormalizedBinding {
21
+ readonly code: string;
22
+ readonly secondCode?: string;
23
+ readonly modifiers: readonly ('control' | 'alt' | 'shift' | 'meta')[];
24
+ }
25
+ /**
26
+ * Expand logical modifiers, deduplicate, and validate the physical code.
27
+ * @param binding - declared binding.
28
+ * @param platform - receiving device platform.
29
+ * @returns canonical binding; unsupported codes throw during registration.
30
+ */
31
+ export declare function normalizeBinding(binding: ShortcutBinding, platform: ShortcutPlatform): NormalizedBinding;
32
+ /**
33
+ * Produce an exact-match index from a normalized binding.
34
+ * @param binding - normalized physical key and modifiers.
35
+ * @returns stable index used for both matching and conflict checks.
36
+ */
37
+ export declare function bindingKey(binding: NormalizedBinding): string;
38
+ /**
39
+ * Format keycaps and ARIA; Windows separates modifiers with plus signs, while chord keys remain adjacent.
40
+ * @param binding - normalized binding, or null for an unbound command.
41
+ * @param platform - receiving device platform.
42
+ * @returns visible keycaps; two-key chords omit ARIA shortcuts, which only support one non-modifier key.
43
+ */
44
+ export declare function presentBinding(binding: NormalizedBinding | null, platform: ShortcutPlatform): {
45
+ keys: readonly string[];
46
+ aria: string | undefined;
47
+ };
48
+ /**
49
+ * Check Web combinations: Windows and macOS also admit any three or four modifiers; Linux retains the limited set.
50
+ * @param binding - normalized candidate.
51
+ * @param platform - receiving device platform.
52
+ * @returns whether this combination is admitted; admission does not guarantee browser or system delivery.
53
+ */
54
+ export declare function isWebBindingAllowed(binding: NormalizedBinding, platform: ShortcutPlatform): boolean;
55
+ //# sourceMappingURL=binding.d.ts.map
@@ -0,0 +1,23 @@
1
+ import type { ShortcutFixedInput } from './types.ts';
2
+ import type { ShortcutRegistry } from './registry.ts';
3
+ import type { ShortcutPlatform, ShortcutRuntime } from '../protocol.ts';
4
+ /**
5
+ * Detect the visiting device, never the server operating system.
6
+ * @param document - product document, marked by Electron preload when present.
7
+ * @param navigator - browser device identification.
8
+ * @returns explicit runtime and platform for default resolution.
9
+ */
10
+ export declare function detectEnvironment(document: Document, navigator: Navigator): {
11
+ runtime: ShortcutRuntime;
12
+ platform: ShortcutPlatform;
13
+ };
14
+ /**
15
+ * Install document composition tracking and application dispatch after local handlers.
16
+ * @param window - input window owned by the client plugin.
17
+ * @param shortcuts - command registry for this window.
18
+ * @param fixed - optional fixed-sequence consumer after local controls.
19
+ * @param native - native input owns configurable bindings; DOM delivery only feeds fixed actions.
20
+ * @returns disposer releasing every listener.
21
+ */
22
+ export declare function installKeyboard(window: Window, shortcuts: Pick<ShortcutRegistry, 'dispatch' | 'runtime' | 'platform'>, fixed?: (input: ShortcutFixedInput) => void, native?: boolean): () => void;
23
+ //# sourceMappingURL=dom.d.ts.map
@@ -0,0 +1,47 @@
1
+ /** Browser command service, with one keyboard adapter per plugin lifetime. */
2
+ import { Service } from '@qilin-agent/kylin';
3
+ import type { Context } from '@qilin-agent/kylin';
4
+ import type { Shortcuts } from './types.ts';
5
+ import type { ShortcutFixedInput } from './types.ts';
6
+ export type { ShortcutCatalogEntry, ShortcutCommand, ShortcutContext, ShortcutGesture, Shortcuts } from './types.ts';
7
+ export type { ShortcutFixedInput, ShortcutFixedCommand, ShortcutFixedCatalogEntry } from './types.ts';
8
+ export type { ShortcutBinding, ShortcutCommandId, ShortcutPlatform, ShortcutRuntime } from '../protocol.ts';
9
+ declare module '@qilin-agent/kylin' {
10
+ interface Context {
11
+ /** Window-local application commands and effective keycap catalog. */
12
+ shortcuts: Shortcuts;
13
+ }
14
+ }
15
+ /** Cordis keyboard provider; Desktop startup requires its native keyboard bridge. */
16
+ export default class ShortcutsService extends Service implements Shortcuts {
17
+ static inject: string[];
18
+ readonly runtime: Shortcuts['runtime'];
19
+ readonly platform: Shortcuts['platform'];
20
+ readonly catalog: Shortcuts['catalog'];
21
+ readonly config: Shortcuts['config'];
22
+ readonly fixedCatalog: Shortcuts['fixedCatalog'];
23
+ readonly stopSequenceMs: number;
24
+ private readonly fixedListeners;
25
+ private readonly adapter;
26
+ private readonly keyboard;
27
+ private active;
28
+ private connected;
29
+ private readonly registry;
30
+ constructor(ctx: Context);
31
+ register(command: Parameters<Shortcuts['register']>[0]): () => void;
32
+ registerFixed(command: Parameters<Shortcuts['registerFixed']>[0]): () => void;
33
+ observeFixedInput(listener: (input: ShortcutFixedInput) => void): () => void;
34
+ private fixedInput;
35
+ describeBinding(binding: Parameters<Shortcuts['describeBinding']>[0]): ReturnType<Shortcuts['describeBinding']>;
36
+ private syncDefinitions;
37
+ private failRead;
38
+ /**
39
+ * Persist one reviewed operation while retaining accepted bindings on failure.
40
+ * @param args - edit and expected revision supplied by the editor.
41
+ * @returns classified save outcome and accepted snapshot.
42
+ */
43
+ edit(...args: Parameters<Shortcuts['edit']>): ReturnType<Shortcuts['edit']>;
44
+ recording(active: boolean): Promise<void>;
45
+ closeWindow(): Promise<void>;
46
+ }
47
+ //# sourceMappingURL=index.d.ts.map