@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/LICENSE +21 -0
- package/README.i18n.yaml +37 -0
- package/README.md +91 -2
- package/README.zh.md +92 -0
- package/lib/client.js +2628 -0
- package/lib/index.js +23 -0
- package/lib/protocol.js +524 -0
- package/lib/types/binding.d.ts +55 -0
- package/lib/types/client/dom.d.ts +23 -0
- package/lib/types/client/index.d.ts +47 -0
- package/lib/types/client/native.d.ts +13 -0
- package/lib/types/client/registry.d.ts +80 -0
- package/lib/types/client/storage.d.ts +20 -0
- package/lib/types/client/types.d.ts +146 -0
- package/lib/types/config.d.ts +10 -0
- package/lib/types/configuration.d.ts +100 -0
- package/lib/types/index.d.ts +11 -0
- package/lib/types/native.d.ts +32 -0
- package/lib/types/persistence.d.ts +76 -0
- package/lib/types/protocol.d.ts +6 -0
- package/package.json +68 -3
package/lib/client.js
ADDED
|
@@ -0,0 +1,2628 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@qilin-agent/client-shortcuts",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
module.exports;
|
|
6
|
+
let _qilin_agent_kylin = require("@qilin-agent/kylin");
|
|
7
|
+
let _qilin_agent_client_store = require("@qilin-agent/client-store");
|
|
8
|
+
let _qilin_agent_client_ui_primitives = require("@qilin-agent/client-ui-primitives");
|
|
9
|
+
//#region lib/types/binding.js
|
|
10
|
+
const keyNames = {
|
|
11
|
+
Slash: "/",
|
|
12
|
+
Comma: ",",
|
|
13
|
+
Period: ".",
|
|
14
|
+
Backslash: "\\",
|
|
15
|
+
Backquote: "`",
|
|
16
|
+
Minus: "-",
|
|
17
|
+
Equal: "=",
|
|
18
|
+
BracketLeft: "[",
|
|
19
|
+
BracketRight: "]",
|
|
20
|
+
Semicolon: ";",
|
|
21
|
+
Quote: "'",
|
|
22
|
+
Enter: "Enter",
|
|
23
|
+
Escape: "Esc",
|
|
24
|
+
Space: "Space",
|
|
25
|
+
Tab: "Tab",
|
|
26
|
+
Backspace: "Backspace",
|
|
27
|
+
Delete: "Delete",
|
|
28
|
+
ArrowUp: "↑",
|
|
29
|
+
ArrowDown: "↓",
|
|
30
|
+
ArrowLeft: "←",
|
|
31
|
+
ArrowRight: "→"
|
|
32
|
+
};
|
|
33
|
+
const modifierOrder = [
|
|
34
|
+
"control",
|
|
35
|
+
"alt",
|
|
36
|
+
"shift",
|
|
37
|
+
"meta"
|
|
38
|
+
];
|
|
39
|
+
/**
|
|
40
|
+
* Expand logical modifiers, deduplicate, and validate the physical code.
|
|
41
|
+
* @param binding - declared binding.
|
|
42
|
+
* @param platform - receiving device platform.
|
|
43
|
+
* @returns canonical binding; unsupported codes throw during registration.
|
|
44
|
+
*/
|
|
45
|
+
function normalizeBinding(binding, platform) {
|
|
46
|
+
const codes = [binding.code, ...binding.secondCode === void 0 ? [] : [binding.secondCode]];
|
|
47
|
+
codes.sort();
|
|
48
|
+
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}`);
|
|
49
|
+
if (codes.length === 2 && codes[0] === codes[1]) throw new Error("Shortcut keys must be distinct");
|
|
50
|
+
const modifiers = new Set(binding.modifiers.map((value) => value === "primary" ? platform === "macos" ? "meta" : "control" : value));
|
|
51
|
+
return {
|
|
52
|
+
code: codes[0],
|
|
53
|
+
...codes[1] === void 0 ? {} : { secondCode: codes[1] },
|
|
54
|
+
modifiers: modifierOrder.filter((value) => modifiers.has(value))
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Produce an exact-match index from a normalized binding.
|
|
59
|
+
* @param binding - normalized physical key and modifiers.
|
|
60
|
+
* @returns stable index used for both matching and conflict checks.
|
|
61
|
+
*/
|
|
62
|
+
function bindingKey(binding) {
|
|
63
|
+
const codes = binding.secondCode === void 0 ? [binding.code] : [binding.code, binding.secondCode].sort();
|
|
64
|
+
return [...binding.modifiers, ...codes].join("+");
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Format keycaps and ARIA; Windows separates modifiers with plus signs, while chord keys remain adjacent.
|
|
68
|
+
* @param binding - normalized binding, or null for an unbound command.
|
|
69
|
+
* @param platform - receiving device platform.
|
|
70
|
+
* @returns visible keycaps; two-key chords omit ARIA shortcuts, which only support one non-modifier key.
|
|
71
|
+
*/
|
|
72
|
+
function presentBinding(binding, platform) {
|
|
73
|
+
if (binding === null) return {
|
|
74
|
+
keys: [],
|
|
75
|
+
aria: void 0
|
|
76
|
+
};
|
|
77
|
+
const key = keyNames[binding.code] ?? binding.code.replace(/^(Key|Digit)/u, "");
|
|
78
|
+
const symbols = platform === "macos" ? {
|
|
79
|
+
control: "⌃",
|
|
80
|
+
alt: "⌥",
|
|
81
|
+
shift: "⇧",
|
|
82
|
+
meta: "⌘"
|
|
83
|
+
} : {
|
|
84
|
+
control: "Ctrl",
|
|
85
|
+
alt: "Alt",
|
|
86
|
+
shift: "Shift",
|
|
87
|
+
meta: "Meta"
|
|
88
|
+
};
|
|
89
|
+
const ariaNames = {
|
|
90
|
+
control: "Control",
|
|
91
|
+
alt: "Alt",
|
|
92
|
+
shift: "Shift",
|
|
93
|
+
meta: "Meta"
|
|
94
|
+
};
|
|
95
|
+
const ariaKey = binding.code === "Space" ? "Space" : binding.code === "Escape" ? "Escape" : binding.code.startsWith("Arrow") ? binding.code : key;
|
|
96
|
+
const second = binding.secondCode === void 0 ? [] : [keyNames[binding.secondCode] ?? binding.secondCode.replace(/^(Key|Digit)/u, "")];
|
|
97
|
+
const keys = [...binding.modifiers.map((value) => symbols[value]), key];
|
|
98
|
+
return {
|
|
99
|
+
keys: [...platform === "windows" ? keys.flatMap((label, index) => index === 0 ? [label] : ["+", label]) : keys, ...second],
|
|
100
|
+
aria: binding.secondCode === void 0 ? [...binding.modifiers.map((value) => ariaNames[value]), ariaKey].join("+") : void 0
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Check Web combinations: Windows and macOS also admit any three or four modifiers; Linux retains the limited set.
|
|
105
|
+
* @param binding - normalized candidate.
|
|
106
|
+
* @param platform - receiving device platform.
|
|
107
|
+
* @returns whether this combination is admitted; admission does not guarantee browser or system delivery.
|
|
108
|
+
*/
|
|
109
|
+
function isWebBindingAllowed(binding, platform) {
|
|
110
|
+
if (binding.secondCode !== void 0) return false;
|
|
111
|
+
if (platform === "windows" || platform === "macos") {
|
|
112
|
+
if (binding.modifiers.length >= 3) return true;
|
|
113
|
+
const primary = platform === "macos" ? "meta" : "control";
|
|
114
|
+
if (binding.modifiers.length === 1 && (["Comma", "Backslash"].includes(binding.code) && binding.modifiers[0] === primary || binding.code === "Backquote" && binding.modifiers[0] === "control")) return true;
|
|
115
|
+
if (binding.modifiers.length === 2 && binding.modifiers.includes(primary) && (binding.modifiers.includes("alt") || binding.modifiers.includes("shift"))) return true;
|
|
116
|
+
}
|
|
117
|
+
return [
|
|
118
|
+
{
|
|
119
|
+
code: "Slash",
|
|
120
|
+
modifiers: ["primary"]
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
code: "Comma",
|
|
124
|
+
modifiers: ["primary", "shift"]
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
code: "Period",
|
|
128
|
+
modifiers: ["primary", "shift"]
|
|
129
|
+
}
|
|
130
|
+
].some((candidate) => bindingKey(binding) === bindingKey(normalizeBinding(candidate, platform)));
|
|
131
|
+
}
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region ../../util/values/lib/index.js
|
|
134
|
+
/**
|
|
135
|
+
* Lazily scanned view of one JSON object's top-level fields, built from text
|
|
136
|
+
* that may still be streaming or from an already parsed object. Nothing is
|
|
137
|
+
* scanned until a reader asks; the view remembers every question it answered
|
|
138
|
+
* and reports changed answers when the owner refreshes for publication.
|
|
139
|
+
* Used for model tool-call arguments: a row reads the fields it
|
|
140
|
+
* cares about at whatever granularity it displays, at every stage of the call.
|
|
141
|
+
* @module @qilin-agent/util-values/src/partial-json
|
|
142
|
+
*/
|
|
143
|
+
const SIMPLE_ESCAPES = {
|
|
144
|
+
"\"": "\"",
|
|
145
|
+
"\\": "\\",
|
|
146
|
+
"/": "/",
|
|
147
|
+
b: "\b",
|
|
148
|
+
f: "\f",
|
|
149
|
+
n: "\n",
|
|
150
|
+
r: "\r",
|
|
151
|
+
t: " "
|
|
152
|
+
};
|
|
153
|
+
const CONTENT_ESCAPE = /[\\\u0000-\u001f]/u;
|
|
154
|
+
function isWhitespace(c) {
|
|
155
|
+
return c === " " || c === "\n" || c === "\r" || c === " ";
|
|
156
|
+
}
|
|
157
|
+
function isHex(c) {
|
|
158
|
+
return c >= "0" && c <= "9" || c >= "a" && c <= "f" || c >= "A" && c <= "F";
|
|
159
|
+
}
|
|
160
|
+
(class PartialArguments {
|
|
161
|
+
/** The view of a call with no arguments available. */
|
|
162
|
+
static EMPTY = PartialArguments.fromObject({});
|
|
163
|
+
/**
|
|
164
|
+
* View finished argument text without scanning it until a reader asks.
|
|
165
|
+
* @param text - the complete argument JSON text.
|
|
166
|
+
* @returns a sealed view.
|
|
167
|
+
*/
|
|
168
|
+
static fromText(text) {
|
|
169
|
+
const view = new PartialArguments();
|
|
170
|
+
view.append(text);
|
|
171
|
+
view.sealed = true;
|
|
172
|
+
return view;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* View an already parsed argument payload, such as a PTC dispatch object.
|
|
176
|
+
* @param value - the parsed argument value.
|
|
177
|
+
* @returns a sealed view; a non-object payload has no fields.
|
|
178
|
+
*/
|
|
179
|
+
static fromObject(value) {
|
|
180
|
+
const view = new PartialArguments();
|
|
181
|
+
view.object = typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
182
|
+
view.sealed = true;
|
|
183
|
+
return view;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* The source: text so far or a parsed object, plus whether it can still grow.
|
|
187
|
+
* These are the only enumerable fields, so two views over the same source
|
|
188
|
+
* compare equal structurally however far each has been read.
|
|
189
|
+
*/
|
|
190
|
+
chunks = [];
|
|
191
|
+
object;
|
|
192
|
+
sealed = false;
|
|
193
|
+
#ends = [];
|
|
194
|
+
#size = 0;
|
|
195
|
+
#consumed = 0;
|
|
196
|
+
#mode = "root";
|
|
197
|
+
#escape = false;
|
|
198
|
+
#keyStart = 0;
|
|
199
|
+
#keyEscaped = false;
|
|
200
|
+
#key = "";
|
|
201
|
+
#current = null;
|
|
202
|
+
#nestedEnds = [];
|
|
203
|
+
#nestedInString = false;
|
|
204
|
+
#invalidAt;
|
|
205
|
+
#invalidValue = false;
|
|
206
|
+
#entries = /* @__PURE__ */ new Map();
|
|
207
|
+
#order = [];
|
|
208
|
+
#reads = /* @__PURE__ */ new Map();
|
|
209
|
+
/** Whether this view rejects further appends; does not scan text or register reads. */
|
|
210
|
+
get isSealed() {
|
|
211
|
+
return this.sealed;
|
|
212
|
+
}
|
|
213
|
+
/** Whether indexing or a content read found invalid JSON; unread value contents are not validated. */
|
|
214
|
+
get invalid() {
|
|
215
|
+
this.scan();
|
|
216
|
+
return this.#mode === "invalid" || this.#invalidValue;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Retain streamed argument text without scanning or comparing observed answers.
|
|
220
|
+
* @param fragment - the text following every fragment appended before.
|
|
221
|
+
*/
|
|
222
|
+
append(fragment) {
|
|
223
|
+
if (this.sealed) throw new Error("PartialArguments: cannot append to a sealed view");
|
|
224
|
+
if (fragment.length === 0) return;
|
|
225
|
+
this.chunks.push(fragment);
|
|
226
|
+
this.#size += fragment.length;
|
|
227
|
+
this.#ends.push(this.#size);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Reconcile a streamed prefix with authoritative complete text without joining the fragments.
|
|
231
|
+
* @param text - the final argument text, which replaces missing or conflicting deltas.
|
|
232
|
+
* @returns this view sealed with its caches retained when every character matches; otherwise a new sealed view.
|
|
233
|
+
*/
|
|
234
|
+
settle(text) {
|
|
235
|
+
if (this.object !== void 0 || text.length !== this.#size) return PartialArguments.fromText(text);
|
|
236
|
+
let offset = 0;
|
|
237
|
+
for (const chunk of this.chunks) {
|
|
238
|
+
if (!text.startsWith(chunk, offset)) return PartialArguments.fromText(text);
|
|
239
|
+
offset += chunk.length;
|
|
240
|
+
}
|
|
241
|
+
this.chunks = text.length === 0 ? [] : [text];
|
|
242
|
+
this.#ends = text.length === 0 ? [] : [text.length];
|
|
243
|
+
this.sealed = true;
|
|
244
|
+
return this;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Compare observed answers and advance their publication baseline. Unread views remain unscanned.
|
|
248
|
+
* @returns whether any observed answer changed since its first read or the preceding refresh.
|
|
249
|
+
*/
|
|
250
|
+
refresh() {
|
|
251
|
+
if (this.#reads.size === 0) return false;
|
|
252
|
+
this.scan();
|
|
253
|
+
let changed = false;
|
|
254
|
+
let completions = false;
|
|
255
|
+
for (const read of this.#reads.values()) {
|
|
256
|
+
if (read.completion) {
|
|
257
|
+
completions = true;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
changed = this.refreshRead(read) || changed;
|
|
261
|
+
}
|
|
262
|
+
if (completions) {
|
|
263
|
+
for (const read of this.#reads.values()) if (read.completion) changed = this.refreshRead(read) || changed;
|
|
264
|
+
}
|
|
265
|
+
if (this.sealed) this.#reads.clear();
|
|
266
|
+
return changed;
|
|
267
|
+
}
|
|
268
|
+
refreshRead(read) {
|
|
269
|
+
const now = read.answer();
|
|
270
|
+
if (Object.is(now, read.last)) return false;
|
|
271
|
+
read.last = now;
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Check whether no further fields can arrive.
|
|
276
|
+
* @returns whether the outer object closed, indexing failed, or the view is sealed; unread values are not validated.
|
|
277
|
+
*/
|
|
278
|
+
closed() {
|
|
279
|
+
return this.remember("closed", "", () => this.closedNow());
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* List discovered fields in first-appearance order.
|
|
283
|
+
* @returns top-level keys seen so far, in first-appearance order.
|
|
284
|
+
*/
|
|
285
|
+
keys() {
|
|
286
|
+
return this.remember("keys", "", () => this.keysNow(), (keys) => keys.length);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Check whether a top-level field has appeared.
|
|
290
|
+
* @param key - argument name.
|
|
291
|
+
* @returns whether the field has appeared (a string opened or another value began).
|
|
292
|
+
*/
|
|
293
|
+
has(key) {
|
|
294
|
+
return this.remember("has", key, () => this.hasNow(key));
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Check whether a field's closing delimiter has arrived, without validating its contents.
|
|
298
|
+
* @param key - argument name.
|
|
299
|
+
* @returns whether its delimiter arrived and no content reader has reported an error for this value.
|
|
300
|
+
*/
|
|
301
|
+
complete(key) {
|
|
302
|
+
return this.remember("complete", key, () => this.completeNow(key));
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Read string length without materializing its text.
|
|
306
|
+
* @param key - argument name.
|
|
307
|
+
* @param options - change granularity for a streaming string.
|
|
308
|
+
* @returns decoded UTF-16 length of the string field so far; undefined when absent or not a string.
|
|
309
|
+
*/
|
|
310
|
+
stringLength(key, options) {
|
|
311
|
+
const step = Math.max(1, Math.floor(options?.step ?? 1));
|
|
312
|
+
const offset = options?.offset ?? 0;
|
|
313
|
+
return this.remember(`length:${step}:${offset}`, key, () => this.lengthNow(key), (length) => length === void 0 ? void 0 : Math.ceil((length + offset) / step));
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Check a string against a decoded UTF-16 length limit without materializing it.
|
|
317
|
+
* @param key - argument name.
|
|
318
|
+
* @param maxLength - decoded UTF-16 limit, floored to at least zero.
|
|
319
|
+
* @returns whether the string is longer than the limit; false when absent or not a string.
|
|
320
|
+
*/
|
|
321
|
+
stringExceeds(key, maxLength) {
|
|
322
|
+
const limit = Math.max(0, Math.floor(maxLength));
|
|
323
|
+
return this.remember(`exceeds:${limit}`, key, () => (this.lengthNow(key, limit + 1) ?? 0) > limit);
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Read a decoded string, including a streaming prefix.
|
|
327
|
+
* @param key - argument name.
|
|
328
|
+
* @returns the string field's decoded text so far; undefined when absent or not a string.
|
|
329
|
+
*/
|
|
330
|
+
text(key) {
|
|
331
|
+
return this.remember("text", key, () => this.textNow(key));
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Read at most the first decoded UTF-16 units of a string.
|
|
335
|
+
* @param key - argument name.
|
|
336
|
+
* @param maxLength - maximum decoded UTF-16 length, floored to at least one.
|
|
337
|
+
* @returns the bounded string prefix; undefined when absent or not a string.
|
|
338
|
+
*/
|
|
339
|
+
textPrefix(key, maxLength) {
|
|
340
|
+
const limit = Math.max(1, Math.floor(maxLength));
|
|
341
|
+
return this.remember(`prefix:${limit}`, key, () => this.textPrefixNow(key, limit));
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Read a completed non-string argument.
|
|
345
|
+
* @param key - argument name.
|
|
346
|
+
* @returns the parsed non-string value once it closed; undefined while open, absent, or a string.
|
|
347
|
+
*/
|
|
348
|
+
value(key) {
|
|
349
|
+
return this.remember("value", key, () => this.valueNow(key));
|
|
350
|
+
}
|
|
351
|
+
/** Answer a question and, on a streaming view, remember it for change detection. */
|
|
352
|
+
remember(kind, key, read, comparison) {
|
|
353
|
+
this.scan();
|
|
354
|
+
const result = read();
|
|
355
|
+
if (!this.sealed) {
|
|
356
|
+
const id = `${kind}/${key}`;
|
|
357
|
+
if (!this.#reads.has(id)) this.#reads.set(id, {
|
|
358
|
+
completion: kind === "complete",
|
|
359
|
+
answer: comparison === void 0 ? read : () => comparison(read()),
|
|
360
|
+
last: comparison === void 0 ? result : comparison(result)
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
return result;
|
|
364
|
+
}
|
|
365
|
+
closedNow() {
|
|
366
|
+
return this.sealed || this.#mode === "closed" || this.#mode === "invalid";
|
|
367
|
+
}
|
|
368
|
+
keysNow() {
|
|
369
|
+
return this.object === void 0 ? this.#order : Object.keys(this.object);
|
|
370
|
+
}
|
|
371
|
+
hasNow(key) {
|
|
372
|
+
return this.object === void 0 ? this.#entries.has(key) : Object.hasOwn(this.object, key);
|
|
373
|
+
}
|
|
374
|
+
completeNow(key) {
|
|
375
|
+
if (this.object !== void 0) return Object.hasOwn(this.object, key);
|
|
376
|
+
const entry = this.#entries.get(key);
|
|
377
|
+
return entry !== void 0 && entry.end >= 0 && (entry.kind === "string" ? entry.invalidAt === void 0 : !entry.invalid);
|
|
378
|
+
}
|
|
379
|
+
lengthNow(key, limit = Number.POSITIVE_INFINITY) {
|
|
380
|
+
if (this.object !== void 0) {
|
|
381
|
+
const field = Object.hasOwn(this.object, key) ? this.object[key] : void 0;
|
|
382
|
+
return typeof field === "string" ? field.length : void 0;
|
|
383
|
+
}
|
|
384
|
+
const entry = this.#entries.get(key);
|
|
385
|
+
if (entry?.kind !== "string") return void 0;
|
|
386
|
+
if (entry.text !== void 0 && entry.text.at === entry.end) return entry.text.length;
|
|
387
|
+
const read = entry.length ??= {
|
|
388
|
+
at: entry.start,
|
|
389
|
+
length: 0,
|
|
390
|
+
text: ""
|
|
391
|
+
};
|
|
392
|
+
this.readString(entry, read, limit, false);
|
|
393
|
+
return read.length;
|
|
394
|
+
}
|
|
395
|
+
textNow(key) {
|
|
396
|
+
if (this.object !== void 0) {
|
|
397
|
+
const field = Object.hasOwn(this.object, key) ? this.object[key] : void 0;
|
|
398
|
+
return typeof field === "string" ? field : void 0;
|
|
399
|
+
}
|
|
400
|
+
const entry = this.#entries.get(key);
|
|
401
|
+
if (entry?.kind !== "string") return void 0;
|
|
402
|
+
if (entry.text === void 0 && entry.end >= 0 && entry.needsDecoding && entry.invalidAt === void 0) {
|
|
403
|
+
let text;
|
|
404
|
+
try {
|
|
405
|
+
text = JSON.parse(`"${this.slice(entry.start, entry.end)}"`);
|
|
406
|
+
} catch (_error) {}
|
|
407
|
+
if (text !== void 0) entry.text = {
|
|
408
|
+
at: entry.end,
|
|
409
|
+
length: text.length,
|
|
410
|
+
text
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
const read = entry.text ??= {
|
|
414
|
+
at: entry.start,
|
|
415
|
+
length: 0,
|
|
416
|
+
text: ""
|
|
417
|
+
};
|
|
418
|
+
this.readString(entry, read, Number.POSITIVE_INFINITY, true);
|
|
419
|
+
return read.text;
|
|
420
|
+
}
|
|
421
|
+
textPrefixNow(key, maxLength) {
|
|
422
|
+
if (this.object !== void 0) {
|
|
423
|
+
const field = Object.hasOwn(this.object, key) ? this.object[key] : void 0;
|
|
424
|
+
return typeof field === "string" ? field.slice(0, maxLength) : void 0;
|
|
425
|
+
}
|
|
426
|
+
const entry = this.#entries.get(key);
|
|
427
|
+
if (entry?.kind !== "string") return void 0;
|
|
428
|
+
const prefixes = entry.prefixes ??= /* @__PURE__ */ new Map();
|
|
429
|
+
let read = prefixes.get(maxLength);
|
|
430
|
+
if (read === void 0) {
|
|
431
|
+
read = {
|
|
432
|
+
at: entry.start,
|
|
433
|
+
length: 0,
|
|
434
|
+
text: ""
|
|
435
|
+
};
|
|
436
|
+
prefixes.set(maxLength, read);
|
|
437
|
+
}
|
|
438
|
+
this.readString(entry, read, maxLength, true);
|
|
439
|
+
return read.text;
|
|
440
|
+
}
|
|
441
|
+
valueNow(key) {
|
|
442
|
+
if (this.object !== void 0) {
|
|
443
|
+
if (!Object.hasOwn(this.object, key)) return void 0;
|
|
444
|
+
const field = this.object[key];
|
|
445
|
+
return typeof field === "string" ? void 0 : field;
|
|
446
|
+
}
|
|
447
|
+
const entry = this.#entries.get(key);
|
|
448
|
+
if (entry?.kind !== "value" || entry.end < 0 || entry.invalid) return void 0;
|
|
449
|
+
if (entry.parsed === void 0) try {
|
|
450
|
+
entry.parsed = JSON.parse(this.slice(entry.start, entry.end));
|
|
451
|
+
} catch (_error) {
|
|
452
|
+
entry.invalid = true;
|
|
453
|
+
this.#invalidValue = true;
|
|
454
|
+
}
|
|
455
|
+
return entry.parsed;
|
|
456
|
+
}
|
|
457
|
+
chunkAt(at) {
|
|
458
|
+
let low = 0;
|
|
459
|
+
let high = this.#ends.length;
|
|
460
|
+
while (low < high) {
|
|
461
|
+
const mid = low + high >>> 1;
|
|
462
|
+
if (this.#ends[mid] <= at) low = mid + 1;
|
|
463
|
+
else high = mid;
|
|
464
|
+
}
|
|
465
|
+
return low;
|
|
466
|
+
}
|
|
467
|
+
/** Materialize only a requested range, never the cumulative source. */
|
|
468
|
+
slice(start, end) {
|
|
469
|
+
if (start >= end) return "";
|
|
470
|
+
const first = this.chunkAt(start);
|
|
471
|
+
const last = this.chunkAt(end - 1);
|
|
472
|
+
const base = first === 0 ? 0 : this.#ends[first - 1];
|
|
473
|
+
if (first === last) return this.chunks[first].slice(start - base, end - base);
|
|
474
|
+
const parts = [this.chunks[first].slice(start - base)];
|
|
475
|
+
for (let i = first + 1; i < last; i++) parts.push(this.chunks[i]);
|
|
476
|
+
parts.push(this.chunks[last].slice(0, end - this.#ends[last - 1]));
|
|
477
|
+
return parts.join("");
|
|
478
|
+
}
|
|
479
|
+
readString(entry, read, limit, materialize) {
|
|
480
|
+
const end = Math.min(entry.end < 0 ? this.#consumed : entry.end, entry.invalidAt ?? Number.POSITIVE_INFINITY, this.#invalidAt ?? Number.POSITIVE_INFINITY);
|
|
481
|
+
if (!entry.needsDecoding) {
|
|
482
|
+
const length = Math.min(end - read.at, limit - read.length);
|
|
483
|
+
if (length <= 0) return;
|
|
484
|
+
if (materialize) read.text += this.slice(read.at, read.at + length);
|
|
485
|
+
read.at += length;
|
|
486
|
+
read.length += length;
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
let chunkIndex = this.chunkAt(read.at);
|
|
490
|
+
while (read.at < end && read.length < limit) {
|
|
491
|
+
const base = chunkIndex === 0 ? 0 : this.#ends[chunkIndex - 1];
|
|
492
|
+
const chunk = this.chunks[chunkIndex];
|
|
493
|
+
const remaining = chunk.slice(read.at - base, Math.min(chunk.length, end - base));
|
|
494
|
+
const boundary = remaining.search(CONTENT_ESCAPE);
|
|
495
|
+
const length = Math.min(boundary < 0 ? remaining.length : boundary, limit - read.length);
|
|
496
|
+
if (length > 0) {
|
|
497
|
+
if (materialize) read.text += remaining.slice(0, length);
|
|
498
|
+
read.at += length;
|
|
499
|
+
read.length += length;
|
|
500
|
+
if (read.at === base + chunk.length) chunkIndex++;
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
const type = remaining.length > 1 ? remaining[1] : read.at + 1 < end ? this.chunks[chunkIndex + 1][0] : void 0;
|
|
504
|
+
let decoded;
|
|
505
|
+
let width = 2;
|
|
506
|
+
if (remaining[0] === "\\" && type === void 0 && entry.end < 0) return;
|
|
507
|
+
if (remaining[0] === "\\" && type === "u") {
|
|
508
|
+
const hex = this.slice(read.at + 2, Math.min(end, read.at + 6));
|
|
509
|
+
let valid = true;
|
|
510
|
+
for (let i = 0; i < hex.length; i++) if (!isHex(hex[i])) valid = false;
|
|
511
|
+
if (valid) {
|
|
512
|
+
if (hex.length < 4 && entry.end < 0) return;
|
|
513
|
+
if (hex.length === 4) decoded = String.fromCharCode(Number.parseInt(hex, 16));
|
|
514
|
+
}
|
|
515
|
+
width = 6;
|
|
516
|
+
} else if (remaining[0] === "\\" && type !== void 0) decoded = SIMPLE_ESCAPES[type];
|
|
517
|
+
if (decoded === void 0) {
|
|
518
|
+
entry.invalidAt = read.at;
|
|
519
|
+
this.#invalidValue = true;
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
if (materialize) read.text += decoded;
|
|
523
|
+
read.length++;
|
|
524
|
+
read.at += width;
|
|
525
|
+
while (chunkIndex < this.chunks.length && read.at >= this.#ends[chunkIndex]) chunkIndex++;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
/** Locate new field ranges without decoding or parsing their contents. */
|
|
529
|
+
scan() {
|
|
530
|
+
if (this.object !== void 0 || this.#consumed === this.#size) return;
|
|
531
|
+
for (let i = this.chunkAt(this.#consumed); i < this.chunks.length && this.#invalidAt === void 0; i++) {
|
|
532
|
+
const pending = this.chunks[i];
|
|
533
|
+
const base = i === 0 ? 0 : this.#ends[i - 1];
|
|
534
|
+
for (let index = this.#consumed - base; index < pending.length && this.#mode !== "invalid"; index++) {
|
|
535
|
+
if (this.#mode === "string" || this.#mode === "nested" && this.#nestedInString) {
|
|
536
|
+
const end = this.stringBoundary(pending, index);
|
|
537
|
+
this.#consumed += end - index;
|
|
538
|
+
index = end;
|
|
539
|
+
if (index === pending.length) break;
|
|
540
|
+
}
|
|
541
|
+
this.step(pending[index], this.#consumed);
|
|
542
|
+
this.#consumed++;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/** Only raw quotes and their preceding backslash runs can terminate a string. */
|
|
547
|
+
stringBoundary(fragment, start) {
|
|
548
|
+
let at = start;
|
|
549
|
+
while (true) {
|
|
550
|
+
const quote = fragment.indexOf("\"", at);
|
|
551
|
+
const end = quote < 0 ? fragment.length : quote;
|
|
552
|
+
if (this.#mode === "string") {
|
|
553
|
+
const entry = this.#current;
|
|
554
|
+
if (!entry.needsDecoding && CONTENT_ESCAPE.test(fragment.slice(at, end))) entry.needsDecoding = true;
|
|
555
|
+
}
|
|
556
|
+
let slashStart = end;
|
|
557
|
+
while (slashStart > at && fragment[slashStart - 1] === "\\") slashStart--;
|
|
558
|
+
const escaped = (end - slashStart) % 2 === 1 !== (slashStart === at && this.#escape);
|
|
559
|
+
this.#escape = quote < 0 && escaped;
|
|
560
|
+
if (quote < 0 || !escaped) return end;
|
|
561
|
+
at = quote + 1;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
step(c, at) {
|
|
565
|
+
switch (this.#mode) {
|
|
566
|
+
case "root":
|
|
567
|
+
if (isWhitespace(c)) return;
|
|
568
|
+
if (c === "{") {
|
|
569
|
+
this.#mode = "key-or-end";
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
this.fail();
|
|
573
|
+
return;
|
|
574
|
+
case "key-or-end":
|
|
575
|
+
if (isWhitespace(c)) return;
|
|
576
|
+
if (c === "}") {
|
|
577
|
+
this.#mode = "closed";
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (c === "\"") {
|
|
581
|
+
this.beginKey(at);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
this.fail();
|
|
585
|
+
return;
|
|
586
|
+
case "key-only":
|
|
587
|
+
if (isWhitespace(c)) return;
|
|
588
|
+
if (c === "\"") {
|
|
589
|
+
this.beginKey(at);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
this.fail();
|
|
593
|
+
return;
|
|
594
|
+
case "key":
|
|
595
|
+
this.stepKey(c, at);
|
|
596
|
+
return;
|
|
597
|
+
case "colon":
|
|
598
|
+
if (isWhitespace(c)) return;
|
|
599
|
+
if (c === ":") {
|
|
600
|
+
this.#mode = "value";
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
this.fail();
|
|
604
|
+
return;
|
|
605
|
+
case "value":
|
|
606
|
+
this.beginValue(c, at);
|
|
607
|
+
return;
|
|
608
|
+
case "string": {
|
|
609
|
+
const entry = this.#current;
|
|
610
|
+
entry.end = at;
|
|
611
|
+
this.#current = null;
|
|
612
|
+
this.#mode = "comma-or-end";
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
case "scalar":
|
|
616
|
+
this.stepScalar(c, at);
|
|
617
|
+
return;
|
|
618
|
+
case "nested":
|
|
619
|
+
this.stepNested(c, at);
|
|
620
|
+
return;
|
|
621
|
+
case "comma-or-end":
|
|
622
|
+
if (isWhitespace(c)) return;
|
|
623
|
+
if (c === ",") {
|
|
624
|
+
this.#mode = "key-only";
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
if (c === "}") {
|
|
628
|
+
this.#mode = "closed";
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
this.fail();
|
|
632
|
+
return;
|
|
633
|
+
case "closed":
|
|
634
|
+
if (isWhitespace(c)) return;
|
|
635
|
+
this.fail();
|
|
636
|
+
return;
|
|
637
|
+
/* v8 ignore next 2 -- scan() stops stepping once the view is invalid. */
|
|
638
|
+
case "invalid": return;
|
|
639
|
+
/* v8 ignore next 2 -- Every scanner mode has a handler above. */
|
|
640
|
+
default: assertNever(this.#mode);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
fail() {
|
|
644
|
+
this.#invalidAt = this.#consumed;
|
|
645
|
+
this.#mode = "invalid";
|
|
646
|
+
this.#current = null;
|
|
647
|
+
}
|
|
648
|
+
beginKey(at) {
|
|
649
|
+
this.#mode = "key";
|
|
650
|
+
this.#keyStart = at + 1;
|
|
651
|
+
this.#keyEscaped = false;
|
|
652
|
+
this.#escape = false;
|
|
653
|
+
}
|
|
654
|
+
stepKey(c, at) {
|
|
655
|
+
if (c < " ") {
|
|
656
|
+
this.fail();
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
if (this.#escape) {
|
|
660
|
+
this.#escape = false;
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
if (c === "\\") {
|
|
664
|
+
this.#escape = true;
|
|
665
|
+
this.#keyEscaped = true;
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
if (c !== "\"") return;
|
|
669
|
+
const raw = this.slice(this.#keyStart, at);
|
|
670
|
+
if (this.#keyEscaped) try {
|
|
671
|
+
this.#key = JSON.parse(`"${raw}"`);
|
|
672
|
+
} catch (_error) {
|
|
673
|
+
this.fail();
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
else this.#key = raw;
|
|
677
|
+
this.#mode = "colon";
|
|
678
|
+
}
|
|
679
|
+
open(entry) {
|
|
680
|
+
if (!this.#entries.has(this.#key)) this.#order.push(this.#key);
|
|
681
|
+
this.#entries.set(this.#key, entry);
|
|
682
|
+
this.#current = entry;
|
|
683
|
+
}
|
|
684
|
+
beginValue(c, at) {
|
|
685
|
+
if (isWhitespace(c)) return;
|
|
686
|
+
if (c === "\"") {
|
|
687
|
+
this.open({
|
|
688
|
+
kind: "string",
|
|
689
|
+
start: at + 1,
|
|
690
|
+
end: -1,
|
|
691
|
+
needsDecoding: false,
|
|
692
|
+
invalidAt: void 0,
|
|
693
|
+
length: void 0,
|
|
694
|
+
text: void 0,
|
|
695
|
+
prefixes: void 0
|
|
696
|
+
});
|
|
697
|
+
this.#escape = false;
|
|
698
|
+
this.#mode = "string";
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
if (c === "}" || c === "," || c === ":" || c === "]") {
|
|
702
|
+
this.fail();
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
this.open({
|
|
706
|
+
kind: "value",
|
|
707
|
+
start: at,
|
|
708
|
+
end: -1,
|
|
709
|
+
parsed: void 0,
|
|
710
|
+
invalid: false
|
|
711
|
+
});
|
|
712
|
+
if (c === "{" || c === "[") {
|
|
713
|
+
this.#mode = "nested";
|
|
714
|
+
this.#nestedEnds = [c === "{" ? "}" : "]"];
|
|
715
|
+
this.#nestedInString = false;
|
|
716
|
+
this.#escape = false;
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
this.#mode = "scalar";
|
|
720
|
+
}
|
|
721
|
+
stepScalar(c, at) {
|
|
722
|
+
if (c !== "," && c !== "}" && !isWhitespace(c)) return;
|
|
723
|
+
this.closeValue(at);
|
|
724
|
+
this.#mode = c === "," ? "key-only" : c === "}" ? "closed" : "comma-or-end";
|
|
725
|
+
}
|
|
726
|
+
stepNested(c, at) {
|
|
727
|
+
if (this.#nestedInString) {
|
|
728
|
+
this.#nestedInString = false;
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
if (c === "\"") {
|
|
732
|
+
this.#nestedInString = true;
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
if (c === "{" || c === "[") {
|
|
736
|
+
this.#nestedEnds.push(c === "{" ? "}" : "]");
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
if (c === "}" || c === "]") {
|
|
740
|
+
if (this.#nestedEnds.pop() !== c) {
|
|
741
|
+
this.fail();
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
if (this.#nestedEnds.length === 0) {
|
|
745
|
+
this.closeValue(at + 1);
|
|
746
|
+
this.#mode = "comma-or-end";
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
closeValue(end) {
|
|
751
|
+
const entry = this.#current;
|
|
752
|
+
entry.end = end;
|
|
753
|
+
this.#current = null;
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
/** Duplicate-install-safe JSON and immutable-value helpers. @module @qilin-agent/util-values */
|
|
757
|
+
/**
|
|
758
|
+
* Mark an unreachable closed-union branch.
|
|
759
|
+
* @param value - impossible value; an unhandled typed variant fails at the call site.
|
|
760
|
+
* @param context - optional switch-site label included in the failure message.
|
|
761
|
+
* @returns never; a runtime value that escaped its type always throws.
|
|
762
|
+
*/
|
|
763
|
+
function assertNever(value, context) {
|
|
764
|
+
const rendered = JSON.stringify(value) ?? String(value);
|
|
765
|
+
throw new Error(`unreachable variant${context ? ` in ${context}` : ""}: ${rendered}`);
|
|
766
|
+
}
|
|
767
|
+
//#endregion
|
|
768
|
+
//#region lib/types/configuration.js
|
|
769
|
+
/** Validated preference documents and deterministic conflict resolution, without browser dependencies. */
|
|
770
|
+
const record = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
771
|
+
const commandPattern = /^[a-z][a-zA-Z0-9-]*(?:\.[a-zA-Z][a-zA-Z0-9-]*)+$/u;
|
|
772
|
+
/**
|
|
773
|
+
* Validate JSON binding fields before normalization; unknown fields are rejected to prevent lossy rewrites.
|
|
774
|
+
* @param value - file or IPC input.
|
|
775
|
+
* @returns a binding with a supported physical code, or null for explicit removal.
|
|
776
|
+
*/
|
|
777
|
+
function parseBinding(value) {
|
|
778
|
+
if (value === null) return null;
|
|
779
|
+
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" && [
|
|
780
|
+
"primary",
|
|
781
|
+
"control",
|
|
782
|
+
"alt",
|
|
783
|
+
"shift",
|
|
784
|
+
"meta"
|
|
785
|
+
].includes(modifier))) throw new Error("Invalid shortcut binding");
|
|
786
|
+
const binding = {
|
|
787
|
+
code: value.code,
|
|
788
|
+
modifiers: value.modifiers,
|
|
789
|
+
...typeof value.secondCode === "string" ? { secondCode: value.secondCode } : {}
|
|
790
|
+
};
|
|
791
|
+
normalizeBinding(binding, "windows");
|
|
792
|
+
return binding;
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Decode the complete document while preserving dormant command overrides.
|
|
796
|
+
* @param raw - stored JSON, or null for a missing document.
|
|
797
|
+
* @returns the accepted document or a classified read failure.
|
|
798
|
+
*/
|
|
799
|
+
function parseShortcutDocument(raw) {
|
|
800
|
+
if (raw === null) return {
|
|
801
|
+
schemaVersion: 1,
|
|
802
|
+
profiles: {}
|
|
803
|
+
};
|
|
804
|
+
try {
|
|
805
|
+
const value = JSON.parse(raw);
|
|
806
|
+
if (!record(value)) return "invalid";
|
|
807
|
+
if (typeof value.schemaVersion === "number" && value.schemaVersion > 2) return "future";
|
|
808
|
+
if (value.schemaVersion !== 1 && value.schemaVersion !== 2 || !record(value.profiles) || Object.keys(value).some((key) => key !== "schemaVersion" && key !== "profiles")) return "invalid";
|
|
809
|
+
const profiles = {};
|
|
810
|
+
for (const [profile, overrides] of Object.entries(value.profiles)) {
|
|
811
|
+
if (!/^(desktop|web):(macos|windows|linux)$/u.test(profile) || !record(overrides)) return "invalid";
|
|
812
|
+
const bindings = {};
|
|
813
|
+
for (const [id, binding] of Object.entries(overrides)) {
|
|
814
|
+
if (!commandPattern.test(id)) return "invalid";
|
|
815
|
+
const parsed = parseBinding(binding);
|
|
816
|
+
if (value.schemaVersion === 1 && parsed?.secondCode !== void 0) return "invalid";
|
|
817
|
+
bindings[id] = parsed;
|
|
818
|
+
}
|
|
819
|
+
profiles[profile] = bindings;
|
|
820
|
+
}
|
|
821
|
+
return {
|
|
822
|
+
schemaVersion: value.schemaVersion,
|
|
823
|
+
profiles
|
|
824
|
+
};
|
|
825
|
+
} catch (_error) {
|
|
826
|
+
return "invalid";
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Check system, editor, and browser reservations using expanded physical modifiers.
|
|
831
|
+
* @param binding - normalized candidate.
|
|
832
|
+
* @param runtime - receiving application shell.
|
|
833
|
+
* @param platform - receiving device.
|
|
834
|
+
* @returns the rejection reason, or null when this combination is allowed.
|
|
835
|
+
*/
|
|
836
|
+
function bindingIssue(binding, runtime, platform) {
|
|
837
|
+
const { code, modifiers } = binding;
|
|
838
|
+
if (runtime === "desktop" && (platform === "windows" || platform === "macos")) return null;
|
|
839
|
+
if (binding.secondCode !== void 0) return "unsupported-key";
|
|
840
|
+
if (modifiers.length === 0 || modifiers.every((modifier) => modifier === "shift")) return "modifier-required";
|
|
841
|
+
if ((platform === "windows" || platform === "macos") && modifiers.length >= 3) return null;
|
|
842
|
+
if (runtime === "web" && (platform === "windows" || platform === "macos") && isWebBindingAllowed(binding, platform)) return null;
|
|
843
|
+
const primary = modifiers.includes(platform === "macos" ? "meta" : "control");
|
|
844
|
+
if ([
|
|
845
|
+
"Escape",
|
|
846
|
+
"Tab",
|
|
847
|
+
"Space",
|
|
848
|
+
"Backspace",
|
|
849
|
+
"Delete",
|
|
850
|
+
"ArrowUp",
|
|
851
|
+
"ArrowDown",
|
|
852
|
+
"ArrowLeft",
|
|
853
|
+
"ArrowRight"
|
|
854
|
+
].includes(code) || code === "Enter" && !modifiers.includes("alt") || primary && [
|
|
855
|
+
"KeyC",
|
|
856
|
+
"KeyV",
|
|
857
|
+
"KeyX",
|
|
858
|
+
"KeyZ",
|
|
859
|
+
"KeyY",
|
|
860
|
+
"KeyQ",
|
|
861
|
+
"KeyH"
|
|
862
|
+
].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";
|
|
863
|
+
if (runtime === "web" && !isWebBindingAllowed(binding, platform)) return "unsupported-browser";
|
|
864
|
+
return null;
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Select the command owner's explicit default for one device profile.
|
|
868
|
+
* @param definition - command identity and per-profile defaults.
|
|
869
|
+
* @param runtime - receiving shell.
|
|
870
|
+
* @param platform - receiving device.
|
|
871
|
+
* @returns the declared physical binding, or undefined for an unbound action.
|
|
872
|
+
*/
|
|
873
|
+
function resolveShortcutDefault(definition, runtime, platform) {
|
|
874
|
+
return definition.defaults[`${runtime}:${platform}`];
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Resolve overrides and conflicts independently of registration order. Explicit overrides displace defaults.
|
|
878
|
+
* @param definitions - active commands.
|
|
879
|
+
* @param document - accepted preferences.
|
|
880
|
+
* @param runtime - receiving shell.
|
|
881
|
+
* @param platform - receiving device.
|
|
882
|
+
* @returns every active command, including unavailable conflicting bindings.
|
|
883
|
+
*/
|
|
884
|
+
function effectiveShortcuts(definitions, document, runtime, platform) {
|
|
885
|
+
const overrides = document.profiles[`${runtime}:${platform}`] ?? {};
|
|
886
|
+
const fixed = definitions.flatMap((row) => row.fixed?.map((binding) => ({
|
|
887
|
+
id: row.id,
|
|
888
|
+
binding: normalizeBinding(binding, platform)
|
|
889
|
+
})) ?? []);
|
|
890
|
+
const rows = definitions.filter((row) => row.fixed === void 0).map(({ id, defaults }) => {
|
|
891
|
+
const modified = Object.hasOwn(overrides, id);
|
|
892
|
+
const candidate = modified ? overrides[id] : resolveShortcutDefault({
|
|
893
|
+
id,
|
|
894
|
+
defaults
|
|
895
|
+
}, runtime, platform);
|
|
896
|
+
const binding = candidate == null ? null : normalizeBinding(candidate, platform);
|
|
897
|
+
return {
|
|
898
|
+
id,
|
|
899
|
+
binding,
|
|
900
|
+
modified,
|
|
901
|
+
issue: binding === null ? null : bindingIssue(binding, runtime, platform)
|
|
902
|
+
};
|
|
903
|
+
});
|
|
904
|
+
return rows.map((row) => {
|
|
905
|
+
const binding = row.binding;
|
|
906
|
+
return {
|
|
907
|
+
...row,
|
|
908
|
+
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)])]
|
|
909
|
+
};
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Detect identical combinations or a single key contained in a two-key chord.
|
|
914
|
+
* @param left - normalized candidate.
|
|
915
|
+
* @param right - normalized occupied binding.
|
|
916
|
+
* @returns whether both bindings require the same modifiers and overlap.
|
|
917
|
+
*/
|
|
918
|
+
function overlappingBindings(left, right) {
|
|
919
|
+
if (left.modifiers.join("+") !== right.modifiers.join("+")) return false;
|
|
920
|
+
if (left.secondCode !== void 0 && right.secondCode !== void 0) return bindingKey(left) === bindingKey(right);
|
|
921
|
+
return [left.code, left.secondCode].some((code) => code !== void 0 && (code === right.code || code === right.secondCode));
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Apply an edit without modifying other profiles or dormant overrides.
|
|
925
|
+
* @param document - accepted document.
|
|
926
|
+
* @param edit - validated operation.
|
|
927
|
+
* @param runtime - current shell.
|
|
928
|
+
* @param platform - current device.
|
|
929
|
+
* @returns the candidate document, pending conflict checks and durable storage.
|
|
930
|
+
*/
|
|
931
|
+
function editShortcutDocument(document, edit, runtime, platform) {
|
|
932
|
+
const schemaVersion = runtime === "desktop" && (platform === "macos" || platform === "windows") ? 2 : document.schemaVersion;
|
|
933
|
+
const profile = `${runtime}:${platform}`;
|
|
934
|
+
let overrides = { ...document.profiles[profile] };
|
|
935
|
+
switch (edit.type) {
|
|
936
|
+
case "set":
|
|
937
|
+
overrides[edit.id] = edit.binding;
|
|
938
|
+
break;
|
|
939
|
+
case "reset": {
|
|
940
|
+
const { [edit.id]: _removed, ...remaining } = overrides;
|
|
941
|
+
overrides = remaining;
|
|
942
|
+
break;
|
|
943
|
+
}
|
|
944
|
+
case "reset-all":
|
|
945
|
+
overrides = {};
|
|
946
|
+
break;
|
|
947
|
+
/* v8 ignore next -- parseShortcutEdit validates this closed union before persistence. */
|
|
948
|
+
default: return assertNever(edit, "shortcut edit");
|
|
949
|
+
}
|
|
950
|
+
return {
|
|
951
|
+
schemaVersion,
|
|
952
|
+
profiles: {
|
|
953
|
+
...document.profiles,
|
|
954
|
+
[profile]: overrides
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
//#endregion
|
|
959
|
+
//#region ../../util/crypto/lib/index.js
|
|
960
|
+
/**
|
|
961
|
+
* Random v4 UUID, minted from `crypto.getRandomValues`.
|
|
962
|
+
* @returns the UUID string.
|
|
963
|
+
*/
|
|
964
|
+
function randomUUID() {
|
|
965
|
+
const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
|
|
966
|
+
const hex = Array.from(bytes, (byte, index) => {
|
|
967
|
+
return (index === 6 ? byte & 15 | 64 : index === 8 ? byte & 63 | 128 : byte).toString(16).padStart(2, "0");
|
|
968
|
+
}).join("");
|
|
969
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
970
|
+
}
|
|
971
|
+
//#endregion
|
|
972
|
+
//#region lib/types/persistence.js
|
|
973
|
+
/** Serialized preference transactions; storage owners publish only accepted writes or read diagnostics. */
|
|
974
|
+
/**
|
|
975
|
+
* Create a disabled initial snapshot for asynchronous adapter startup.
|
|
976
|
+
* @returns a fresh configuration with no accepted persisted state.
|
|
977
|
+
*/
|
|
978
|
+
function initialShortcutConfig() {
|
|
979
|
+
return {
|
|
980
|
+
revision: randomUUID(),
|
|
981
|
+
sequence: 0,
|
|
982
|
+
document: {
|
|
983
|
+
schemaVersion: 1,
|
|
984
|
+
profiles: {}
|
|
985
|
+
},
|
|
986
|
+
status: "loading",
|
|
987
|
+
error: null,
|
|
988
|
+
usingDefaults: true
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
/** Single-writer coordinator shared by localStorage and Electron's atomic file adapter. */
|
|
992
|
+
var ShortcutPersistence = class {
|
|
993
|
+
storage;
|
|
994
|
+
runtime;
|
|
995
|
+
platform;
|
|
996
|
+
rereadBeforeWrite;
|
|
997
|
+
publish;
|
|
998
|
+
snapshot = initialShortcutConfig();
|
|
999
|
+
raw;
|
|
1000
|
+
queue = Promise.resolve();
|
|
1001
|
+
definitions = null;
|
|
1002
|
+
active = true;
|
|
1003
|
+
constructor(storage, runtime, platform, rereadBeforeWrite, publish) {
|
|
1004
|
+
this.storage = storage;
|
|
1005
|
+
this.runtime = runtime;
|
|
1006
|
+
this.platform = platform;
|
|
1007
|
+
this.rereadBeforeWrite = rereadBeforeWrite;
|
|
1008
|
+
this.publish = publish;
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Install or revoke a product catalog and invalidate drafts from its previous lifetime.
|
|
1012
|
+
* @param definitions - current trusted definitions, or null while the product is not ready.
|
|
1013
|
+
*/
|
|
1014
|
+
setDefinitions(definitions) {
|
|
1015
|
+
this.definitions = definitions;
|
|
1016
|
+
this.accept({ ...this.snapshot });
|
|
1017
|
+
}
|
|
1018
|
+
/** Stop accepting edits or publishing late completions. */
|
|
1019
|
+
dispose() {
|
|
1020
|
+
this.active = false;
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Read the current file; failures retain the last accepted document and disable ordinary writes.
|
|
1024
|
+
* @returns the accepted snapshot or diagnostic snapshot.
|
|
1025
|
+
*/
|
|
1026
|
+
readCurrent() {
|
|
1027
|
+
return this.serialize(() => this.read());
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Compare the draft revision, validate the complete candidate, then persist before publishing.
|
|
1031
|
+
* @param edit - constrained preference operation.
|
|
1032
|
+
* @param revision - state against which the user reviewed the edit.
|
|
1033
|
+
* @returns a classified outcome and the currently accepted snapshot.
|
|
1034
|
+
*/
|
|
1035
|
+
edit(edit, revision) {
|
|
1036
|
+
return this.serialize(async () => {
|
|
1037
|
+
if (this.rereadBeforeWrite) await this.read();
|
|
1038
|
+
const result = (status) => ({
|
|
1039
|
+
status,
|
|
1040
|
+
snapshot: this.snapshot
|
|
1041
|
+
});
|
|
1042
|
+
if (!this.active || this.definitions === null || this.snapshot.status === "loading") return result("not-ready");
|
|
1043
|
+
if (revision !== this.snapshot.revision) return result("stale");
|
|
1044
|
+
if (this.snapshot.status === "unreadable") return result("unreadable");
|
|
1045
|
+
if ((edit.type === "set" || edit.type === "reset") && !this.definitions.some((row) => row.id === edit.id && row.fixed === void 0)) return result("not-ready");
|
|
1046
|
+
const document = editShortcutDocument(this.snapshot.document, edit, this.runtime, this.platform);
|
|
1047
|
+
const rows = effectiveShortcuts(this.definitions, document, this.runtime, this.platform);
|
|
1048
|
+
const invalid = rows.find((row) => (edit.type === "reset-all" || row.id === edit.id) && (row.issue !== null || row.conflicts.length > 0));
|
|
1049
|
+
const displaced = edit.type === "set" ? rows.find((row) => row.conflicts.includes(edit.id)) : void 0;
|
|
1050
|
+
if (invalid !== void 0 || displaced !== void 0) return {
|
|
1051
|
+
...result("conflict"),
|
|
1052
|
+
...invalid?.issue ? { issue: invalid.issue } : {},
|
|
1053
|
+
conflicts: invalid?.conflicts.length ? invalid.conflicts : displaced === void 0 ? [] : [displaced.id]
|
|
1054
|
+
};
|
|
1055
|
+
try {
|
|
1056
|
+
const raw = `${JSON.stringify(document, null, 2)}\n`;
|
|
1057
|
+
await this.storage.write(raw);
|
|
1058
|
+
this.raw = raw;
|
|
1059
|
+
this.accept({
|
|
1060
|
+
...this.snapshot,
|
|
1061
|
+
document,
|
|
1062
|
+
status: "ready",
|
|
1063
|
+
error: null,
|
|
1064
|
+
usingDefaults: false
|
|
1065
|
+
});
|
|
1066
|
+
return result("saved");
|
|
1067
|
+
} catch (_error) {
|
|
1068
|
+
return result("write-failed");
|
|
1069
|
+
}
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
serialize(operation) {
|
|
1073
|
+
const next = this.queue.then(operation);
|
|
1074
|
+
this.queue = next.catch(() => void 0);
|
|
1075
|
+
return next;
|
|
1076
|
+
}
|
|
1077
|
+
accept(snapshot) {
|
|
1078
|
+
this.snapshot = {
|
|
1079
|
+
...snapshot,
|
|
1080
|
+
sequence: this.snapshot.sequence + 1,
|
|
1081
|
+
revision: randomUUID()
|
|
1082
|
+
};
|
|
1083
|
+
if (!this.active) return;
|
|
1084
|
+
try {
|
|
1085
|
+
this.publish(this.snapshot);
|
|
1086
|
+
} catch (error) {
|
|
1087
|
+
console.error("Shortcut configuration subscriber failed:", error);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
async read() {
|
|
1091
|
+
let raw;
|
|
1092
|
+
try {
|
|
1093
|
+
raw = await this.storage.read();
|
|
1094
|
+
} catch (_error) {
|
|
1095
|
+
if (this.snapshot.error !== "read") this.accept({
|
|
1096
|
+
...this.snapshot,
|
|
1097
|
+
status: "unreadable",
|
|
1098
|
+
error: "read"
|
|
1099
|
+
});
|
|
1100
|
+
return this.snapshot;
|
|
1101
|
+
}
|
|
1102
|
+
if (raw === this.raw && this.snapshot.error !== "read") return this.snapshot;
|
|
1103
|
+
this.raw = raw;
|
|
1104
|
+
const document = parseShortcutDocument(raw);
|
|
1105
|
+
if (typeof document === "string") this.accept({
|
|
1106
|
+
...this.snapshot,
|
|
1107
|
+
status: "unreadable",
|
|
1108
|
+
error: document
|
|
1109
|
+
});
|
|
1110
|
+
else this.accept({
|
|
1111
|
+
...this.snapshot,
|
|
1112
|
+
document,
|
|
1113
|
+
status: "ready",
|
|
1114
|
+
error: null,
|
|
1115
|
+
usingDefaults: raw === null
|
|
1116
|
+
});
|
|
1117
|
+
return this.snapshot;
|
|
1118
|
+
}
|
|
1119
|
+
};
|
|
1120
|
+
//#endregion
|
|
1121
|
+
//#region lib/types/client/registry.js
|
|
1122
|
+
/** Command registration, normalized default bindings, and synchronous dispatch. */
|
|
1123
|
+
/** Application command registry; adapters own event listeners, feature plugins own actions. */
|
|
1124
|
+
var ShortcutRegistry = class {
|
|
1125
|
+
runtime;
|
|
1126
|
+
platform;
|
|
1127
|
+
commands = /* @__PURE__ */ new Map();
|
|
1128
|
+
fixedCommands = /* @__PURE__ */ new Map();
|
|
1129
|
+
conflicts = /* @__PURE__ */ new Map();
|
|
1130
|
+
bindings = /* @__PURE__ */ new Map();
|
|
1131
|
+
state;
|
|
1132
|
+
/** Effective localized rows derived from the accepted configuration. */
|
|
1133
|
+
catalog;
|
|
1134
|
+
/** Accepted preferences and visible read diagnostics. */
|
|
1135
|
+
config;
|
|
1136
|
+
/** Fixed local operations whose owning plugins are mounted. */
|
|
1137
|
+
fixedCatalog = (0, _qilin_agent_client_store.createSnapshotStore)([]);
|
|
1138
|
+
constructor(runtime, platform, config = {
|
|
1139
|
+
...initialShortcutConfig(),
|
|
1140
|
+
status: "ready"
|
|
1141
|
+
}) {
|
|
1142
|
+
this.runtime = runtime;
|
|
1143
|
+
this.platform = platform;
|
|
1144
|
+
this.state = (0, _qilin_agent_client_store.createSnapshotStore)({
|
|
1145
|
+
catalog: [],
|
|
1146
|
+
config
|
|
1147
|
+
});
|
|
1148
|
+
this.catalog = {
|
|
1149
|
+
getSnapshot: () => this.state.getSnapshot().catalog,
|
|
1150
|
+
subscribe: (listener) => this.state.subscribe(listener)
|
|
1151
|
+
};
|
|
1152
|
+
this.config = {
|
|
1153
|
+
getSnapshot: () => this.state.getSnapshot().config,
|
|
1154
|
+
subscribe: (listener) => this.state.subscribe(listener)
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* Return the serializable active catalog for storage validation.
|
|
1159
|
+
* @returns definitions without callbacks or localized labels.
|
|
1160
|
+
*/
|
|
1161
|
+
definitions() {
|
|
1162
|
+
return [...[...this.commands.values()].map(({ id, defaults }) => ({
|
|
1163
|
+
id,
|
|
1164
|
+
defaults
|
|
1165
|
+
})), ...[...this.fixedCommands.values()].map(({ id, bindings }) => ({
|
|
1166
|
+
id,
|
|
1167
|
+
defaults: {},
|
|
1168
|
+
fixed: bindings
|
|
1169
|
+
}))];
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* Register a read-only input action whose keys cannot be assigned to editable commands.
|
|
1173
|
+
* @param command - owner-localized action and readable sequence.
|
|
1174
|
+
* @returns idempotent disposer removing its reference row.
|
|
1175
|
+
*/
|
|
1176
|
+
registerFixed(command) {
|
|
1177
|
+
if (this.fixedCommands.has(command.id) || this.commands.has(command.id)) throw new Error(`Duplicate shortcut command: ${command.id}`);
|
|
1178
|
+
for (const binding of command.bindings) normalizeBinding(binding, this.platform);
|
|
1179
|
+
this.fixedCommands.set(command.id, command);
|
|
1180
|
+
this.refreshLabels();
|
|
1181
|
+
return () => {
|
|
1182
|
+
if (this.fixedCommands.get(command.id) !== command) return;
|
|
1183
|
+
this.fixedCommands.delete(command.id);
|
|
1184
|
+
this.refreshLabels();
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
refreshFixedLabels() {
|
|
1188
|
+
this.fixedCatalog.set([...this.fixedCommands.values()].map((command) => ({
|
|
1189
|
+
id: command.id,
|
|
1190
|
+
label: command.label(),
|
|
1191
|
+
keys: command.keys,
|
|
1192
|
+
group: command.group,
|
|
1193
|
+
bindings: command.bindings.map((binding) => normalizeBinding(binding, this.platform))
|
|
1194
|
+
})));
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Publish accepted preferences and all derived labels atomically.
|
|
1198
|
+
* @param config - storage owner's latest accepted snapshot.
|
|
1199
|
+
*/
|
|
1200
|
+
configure(config) {
|
|
1201
|
+
const current = this.config.getSnapshot();
|
|
1202
|
+
if (config.revision === current.revision && config.status === current.status && config.error === current.error) return;
|
|
1203
|
+
this.refreshLabels(config);
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* Register atomically after checking defaults for all supported platforms and shells.
|
|
1207
|
+
* @param command - feature-owned command definition.
|
|
1208
|
+
* @returns idempotent disposer removing both matching and catalog entries.
|
|
1209
|
+
*/
|
|
1210
|
+
register(command) {
|
|
1211
|
+
if (this.commands.has(command.id) || this.fixedCommands.has(command.id)) throw new Error(`Duplicate shortcut command: ${command.id}`);
|
|
1212
|
+
for (const runtime of ["desktop", "web"]) for (const platform of [
|
|
1213
|
+
"macos",
|
|
1214
|
+
"windows",
|
|
1215
|
+
"linux"
|
|
1216
|
+
]) {
|
|
1217
|
+
const candidate = resolveShortcutDefault(command, runtime, platform);
|
|
1218
|
+
if (candidate === void 0) continue;
|
|
1219
|
+
const binding = normalizeBinding(candidate, platform);
|
|
1220
|
+
if (runtime === "web" && !isWebBindingAllowed(binding, platform)) throw new Error(`Unsupported Web shortcut: ${command.id}`);
|
|
1221
|
+
if (bindingIssue(binding, runtime, platform) !== null) throw new Error(`Reserved shortcut default: ${command.id}`);
|
|
1222
|
+
for (const existing of this.commands.values()) {
|
|
1223
|
+
const other = resolveShortcutDefault(existing, runtime, platform);
|
|
1224
|
+
if (other !== void 0 && overlappingBindings(binding, normalizeBinding(other, platform))) throw new Error(`Conflicting shortcut defaults: ${command.id} and ${existing.id} (${runtime}:${platform})`);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
this.commands.set(command.id, command);
|
|
1228
|
+
this.refreshLabels();
|
|
1229
|
+
return () => {
|
|
1230
|
+
if (this.commands.get(command.id) !== command) return;
|
|
1231
|
+
this.commands.delete(command.id);
|
|
1232
|
+
this.refreshLabels();
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
/**
|
|
1236
|
+
* Recompute effective bindings when preferences, commands, or locale change.
|
|
1237
|
+
* @param config - accepted configuration, defaulting to the current snapshot.
|
|
1238
|
+
*/
|
|
1239
|
+
refreshLabels(config = this.config.getSnapshot()) {
|
|
1240
|
+
this.bindings.clear();
|
|
1241
|
+
this.conflicts.clear();
|
|
1242
|
+
const catalog = effectiveShortcuts(this.definitions(), config.document, this.runtime, this.platform).map((row) => {
|
|
1243
|
+
const command = this.commands.get(row.id);
|
|
1244
|
+
const enabled = config.status !== "loading" && row.issue === null && row.conflicts.length === 0;
|
|
1245
|
+
if (enabled && row.binding !== null) this.bindings.set(bindingKey(row.binding), command);
|
|
1246
|
+
if (config.status !== "loading" && row.issue === null && row.conflicts.length > 0 && row.binding !== null) this.conflicts.set(bindingKey(row.binding), command);
|
|
1247
|
+
return {
|
|
1248
|
+
...row,
|
|
1249
|
+
label: command.label(),
|
|
1250
|
+
aliases: command.aliases,
|
|
1251
|
+
...presentBinding(row.binding, this.platform),
|
|
1252
|
+
aria: enabled ? presentBinding(row.binding, this.platform).aria : void 0
|
|
1253
|
+
};
|
|
1254
|
+
});
|
|
1255
|
+
this.state.set({
|
|
1256
|
+
config,
|
|
1257
|
+
catalog
|
|
1258
|
+
});
|
|
1259
|
+
this.refreshFixedLabels();
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* Invoke a native menu selection independently of its optional key binding.
|
|
1263
|
+
* @param id - registered product command.
|
|
1264
|
+
* @param context - live input owner and modal state.
|
|
1265
|
+
*/
|
|
1266
|
+
invoke(id, context) {
|
|
1267
|
+
const command = this.commands.get(id);
|
|
1268
|
+
if (command === void 0 || context.modal !== null && !command.modals.includes(context.modal)) return;
|
|
1269
|
+
const result = command.resolve({
|
|
1270
|
+
...context,
|
|
1271
|
+
source: "menu"
|
|
1272
|
+
});
|
|
1273
|
+
if (result.status === "handled") result.run();
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* Windows/macOS Desktop bindings override local regions and modal controls independently of their configuration source.
|
|
1277
|
+
* @param gesture - normalized DOM/native input facts.
|
|
1278
|
+
* @param context - synchronous input and modal owner.
|
|
1279
|
+
* @param consume - adapter's preventDefault, called before business execution.
|
|
1280
|
+
* @returns handled, blocked with a reason, or pass for local/system input.
|
|
1281
|
+
*/
|
|
1282
|
+
dispatch(gesture, context, consume) {
|
|
1283
|
+
if (gesture.defaultPrevented || gesture.composing) return { status: "pass" };
|
|
1284
|
+
const modifiers = [
|
|
1285
|
+
"control",
|
|
1286
|
+
"alt",
|
|
1287
|
+
"shift",
|
|
1288
|
+
"meta"
|
|
1289
|
+
].filter((value) => gesture[value]);
|
|
1290
|
+
const pairKey = bindingKey({
|
|
1291
|
+
code: gesture.code,
|
|
1292
|
+
modifiers,
|
|
1293
|
+
...gesture.secondCode === void 0 ? {} : { secondCode: gesture.secondCode }
|
|
1294
|
+
});
|
|
1295
|
+
const key = this.bindings.has(pairKey) || this.conflicts.has(pairKey) ? pairKey : bindingKey({
|
|
1296
|
+
code: gesture.code,
|
|
1297
|
+
modifiers
|
|
1298
|
+
});
|
|
1299
|
+
const command = this.bindings.get(key) ?? this.conflicts.get(key);
|
|
1300
|
+
const priority = this.runtime === "desktop" && (this.platform === "windows" || this.platform === "macos");
|
|
1301
|
+
if (command === void 0 || !priority && !command.regions.includes(context.region)) return { status: "pass" };
|
|
1302
|
+
if (!priority && context.region === "terminal" && gesture.control && !gesture.meta && !gesture.alt && !gesture.shift && (gesture.code === "KeyW" || gesture.code === "KeyR")) return { status: "pass" };
|
|
1303
|
+
if (!this.bindings.has(key)) {
|
|
1304
|
+
consume();
|
|
1305
|
+
return {
|
|
1306
|
+
status: "blocked",
|
|
1307
|
+
commandId: command.id,
|
|
1308
|
+
reason: "conflict"
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
if (!priority && context.modal !== null && !command.modals.includes(context.modal)) {
|
|
1312
|
+
consume();
|
|
1313
|
+
return {
|
|
1314
|
+
status: "blocked",
|
|
1315
|
+
commandId: command.id,
|
|
1316
|
+
reason: "modal"
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
const resolution = command.resolve(context);
|
|
1320
|
+
if (resolution.status === "pass") return resolution;
|
|
1321
|
+
consume();
|
|
1322
|
+
if (resolution.status === "blocked") return {
|
|
1323
|
+
...resolution,
|
|
1324
|
+
commandId: command.id
|
|
1325
|
+
};
|
|
1326
|
+
if (!gesture.repeat) resolution.run();
|
|
1327
|
+
return {
|
|
1328
|
+
status: "handled",
|
|
1329
|
+
commandId: command.id
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
};
|
|
1333
|
+
//#endregion
|
|
1334
|
+
//#region lib/types/client/dom.js
|
|
1335
|
+
/** Main-document keyboard adapter; local controls arbitrate before window bubbling. */
|
|
1336
|
+
/**
|
|
1337
|
+
* Detect the visiting device, never the server operating system.
|
|
1338
|
+
* @param document - product document, marked by Electron preload when present.
|
|
1339
|
+
* @param navigator - browser device identification.
|
|
1340
|
+
* @returns explicit runtime and platform for default resolution.
|
|
1341
|
+
*/
|
|
1342
|
+
function detectEnvironment(document, navigator) {
|
|
1343
|
+
const desktop = document.documentElement.dataset.platform;
|
|
1344
|
+
const device = desktop ?? navigator.platform;
|
|
1345
|
+
return {
|
|
1346
|
+
runtime: desktop === void 0 ? "web" : "desktop",
|
|
1347
|
+
platform: /darwin|mac|iphone|ipad/iu.test(device) ? "macos" : /win/iu.test(device) ? "windows" : "linux"
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* Install document composition tracking and application dispatch after local handlers.
|
|
1352
|
+
* @param window - input window owned by the client plugin.
|
|
1353
|
+
* @param shortcuts - command registry for this window.
|
|
1354
|
+
* @param fixed - optional fixed-sequence consumer after local controls.
|
|
1355
|
+
* @param native - native input owns configurable bindings; DOM delivery only feeds fixed actions.
|
|
1356
|
+
* @returns disposer releasing every listener.
|
|
1357
|
+
*/
|
|
1358
|
+
function installKeyboard(window, shortcuts, fixed, native = false) {
|
|
1359
|
+
const document = window.document;
|
|
1360
|
+
const composition = (0, _qilin_agent_client_ui_primitives.observeComposition)(document);
|
|
1361
|
+
let pending = false;
|
|
1362
|
+
let pendingTimer;
|
|
1363
|
+
const reset = () => {
|
|
1364
|
+
fixed?.({ type: "reset" });
|
|
1365
|
+
};
|
|
1366
|
+
let deadKey = false;
|
|
1367
|
+
const blur = () => {
|
|
1368
|
+
deadKey = false;
|
|
1369
|
+
reset();
|
|
1370
|
+
};
|
|
1371
|
+
const containsModal = (node) => node instanceof Element && (node.matches(_qilin_agent_client_ui_primitives.modalSelector) || node.querySelector(_qilin_agent_client_ui_primitives.modalSelector) !== null);
|
|
1372
|
+
const changedModals = (records) => {
|
|
1373
|
+
if (records.some((record) => record.type === "attributes" ? record.oldValue === "dialog" || record.oldValue === "true" || containsModal(record.target) : [...record.addedNodes, ...record.removedNodes].some(containsModal))) reset();
|
|
1374
|
+
};
|
|
1375
|
+
const observer = fixed === void 0 ? void 0 : new MutationObserver(changedModals);
|
|
1376
|
+
observer?.observe(document.documentElement, {
|
|
1377
|
+
childList: true,
|
|
1378
|
+
subtree: true,
|
|
1379
|
+
attributes: true,
|
|
1380
|
+
attributeFilter: ["role", "aria-modal"],
|
|
1381
|
+
attributeOldValue: true
|
|
1382
|
+
});
|
|
1383
|
+
const capture = () => {
|
|
1384
|
+
if (pending) reset();
|
|
1385
|
+
window.clearTimeout(pendingTimer);
|
|
1386
|
+
if (observer !== void 0) changedModals(observer.takeRecords());
|
|
1387
|
+
pending = true;
|
|
1388
|
+
pendingTimer = window.setTimeout(() => {
|
|
1389
|
+
pending = false;
|
|
1390
|
+
pendingTimer = void 0;
|
|
1391
|
+
reset();
|
|
1392
|
+
}, 0);
|
|
1393
|
+
};
|
|
1394
|
+
const keydown = (event) => {
|
|
1395
|
+
window.clearTimeout(pendingTimer);
|
|
1396
|
+
pendingTimer = void 0;
|
|
1397
|
+
pending = false;
|
|
1398
|
+
const target = event.composedPath().find((value) => value instanceof Element);
|
|
1399
|
+
const element = target instanceof Element ? target : document.activeElement;
|
|
1400
|
+
const region = element?.closest(".xterm") ? "terminal" : element?.closest("input, textarea, select, [contenteditable=\"true\"], [contenteditable=\"\"]") ? "editable" : "page";
|
|
1401
|
+
const top = [...document.querySelectorAll(_qilin_agent_client_ui_primitives.modalSelector)].at(-1);
|
|
1402
|
+
const context = {
|
|
1403
|
+
region,
|
|
1404
|
+
modal: top === void 0 ? null : top.dataset.shortcutModal ?? "other",
|
|
1405
|
+
target: element
|
|
1406
|
+
};
|
|
1407
|
+
const guarded = composition.guards(event) || deadKey || event.getModifierState("AltGraph");
|
|
1408
|
+
const isDead = event.key === "Dead";
|
|
1409
|
+
const commandDeadKey = isDead && shortcuts.runtime === "web" && shortcuts.platform === "macos" && event.code === "KeyN" && event.metaKey && event.altKey && !event.ctrlKey && !event.shiftKey;
|
|
1410
|
+
deadKey = isDead;
|
|
1411
|
+
const gesture = {
|
|
1412
|
+
code: event.code,
|
|
1413
|
+
control: event.ctrlKey,
|
|
1414
|
+
alt: event.altKey,
|
|
1415
|
+
shift: event.shiftKey,
|
|
1416
|
+
meta: event.metaKey,
|
|
1417
|
+
repeat: event.repeat,
|
|
1418
|
+
composing: guarded || isDead,
|
|
1419
|
+
defaultPrevented: event.defaultPrevented
|
|
1420
|
+
};
|
|
1421
|
+
const consume = () => {
|
|
1422
|
+
event.preventDefault();
|
|
1423
|
+
if (commandDeadKey) deadKey = false;
|
|
1424
|
+
};
|
|
1425
|
+
fixed?.({
|
|
1426
|
+
type: "keydown",
|
|
1427
|
+
gesture,
|
|
1428
|
+
context,
|
|
1429
|
+
consume
|
|
1430
|
+
});
|
|
1431
|
+
if (native) return;
|
|
1432
|
+
shortcuts.dispatch({
|
|
1433
|
+
...gesture,
|
|
1434
|
+
composing: guarded || isDead && !commandDeadKey,
|
|
1435
|
+
defaultPrevented: event.defaultPrevented
|
|
1436
|
+
}, context, consume);
|
|
1437
|
+
};
|
|
1438
|
+
document.addEventListener("compositionstart", reset, true);
|
|
1439
|
+
document.addEventListener("compositionend", reset, true);
|
|
1440
|
+
document.addEventListener("focusin", reset, true);
|
|
1441
|
+
document.addEventListener("pointerdown", reset, true);
|
|
1442
|
+
window.addEventListener("keydown", capture, true);
|
|
1443
|
+
window.addEventListener("keydown", keydown);
|
|
1444
|
+
window.addEventListener("blur", blur);
|
|
1445
|
+
return () => {
|
|
1446
|
+
pending = false;
|
|
1447
|
+
window.clearTimeout(pendingTimer);
|
|
1448
|
+
observer?.disconnect();
|
|
1449
|
+
composition.dispose();
|
|
1450
|
+
document.removeEventListener("compositionstart", reset, true);
|
|
1451
|
+
document.removeEventListener("compositionend", reset, true);
|
|
1452
|
+
document.removeEventListener("focusin", reset, true);
|
|
1453
|
+
document.removeEventListener("pointerdown", reset, true);
|
|
1454
|
+
window.removeEventListener("keydown", capture, true);
|
|
1455
|
+
window.removeEventListener("keydown", keydown);
|
|
1456
|
+
window.removeEventListener("blur", blur);
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
//#endregion
|
|
1460
|
+
//#region lib/types/client/storage.js
|
|
1461
|
+
/** Explicit browser and Desktop preference adapters; Desktop never falls back to browser storage. */
|
|
1462
|
+
/** Browser-profile and origin-local preference key. */
|
|
1463
|
+
const SHORTCUT_STORAGE_KEY = "qilin.keybindings.v1";
|
|
1464
|
+
/**
|
|
1465
|
+
* Connect localStorage and same-origin external updates to the shared transaction coordinator.
|
|
1466
|
+
* @param window - owning browser window.
|
|
1467
|
+
* @param platform - visiting device platform.
|
|
1468
|
+
* @param publish - accepts complete configuration snapshots.
|
|
1469
|
+
* @returns adapter and lifecycle disposal.
|
|
1470
|
+
*/
|
|
1471
|
+
function webShortcutStorage(window, platform, publish) {
|
|
1472
|
+
const persistence = new ShortcutPersistence({
|
|
1473
|
+
read: () => window.localStorage.getItem(SHORTCUT_STORAGE_KEY),
|
|
1474
|
+
write: (raw) => {
|
|
1475
|
+
window.localStorage.setItem(SHORTCUT_STORAGE_KEY, raw);
|
|
1476
|
+
}
|
|
1477
|
+
}, "web", platform, true, publish);
|
|
1478
|
+
const changed = (event) => {
|
|
1479
|
+
if (event.key === null || event.key === "qilin.keybindings.v1") persistence.readCurrent();
|
|
1480
|
+
};
|
|
1481
|
+
window.addEventListener("storage", changed);
|
|
1482
|
+
return {
|
|
1483
|
+
get: (definitions) => {
|
|
1484
|
+
persistence.setDefinitions(definitions);
|
|
1485
|
+
return persistence.readCurrent();
|
|
1486
|
+
},
|
|
1487
|
+
edit: (edit, revision) => persistence.edit(edit, revision),
|
|
1488
|
+
subscribe: () => () => {},
|
|
1489
|
+
recording: async () => {},
|
|
1490
|
+
dispose: () => {
|
|
1491
|
+
window.removeEventListener("storage", changed);
|
|
1492
|
+
persistence.dispose();
|
|
1493
|
+
}
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
/**
|
|
1497
|
+
* Read the origin-scoped preload capability; a missing bridge is an explicit configuration failure.
|
|
1498
|
+
* @param window - product window.
|
|
1499
|
+
* @returns the restricted Desktop API, or undefined while the preload is unavailable.
|
|
1500
|
+
*/
|
|
1501
|
+
function desktopShortcutStorage(window) {
|
|
1502
|
+
return window.qilinDesktop?.shortcuts;
|
|
1503
|
+
}
|
|
1504
|
+
//#endregion
|
|
1505
|
+
//#region lib/types/client/native.js
|
|
1506
|
+
/** Desktop gestures use live focus, modal state, and verified embedding ownership. */
|
|
1507
|
+
/**
|
|
1508
|
+
* Route native menu, main-frame, and embedded-frame input through the shared command registry.
|
|
1509
|
+
* @param window - trusted product document.
|
|
1510
|
+
* @param keyboard - top-frame preload capability.
|
|
1511
|
+
* @param registry - window-local command owner.
|
|
1512
|
+
* @param snapshot - latest accepted configuration.
|
|
1513
|
+
* @param reset - clears pending fixed sequences when native input bypasses DOM delivery.
|
|
1514
|
+
* @returns disposer releasing native input.
|
|
1515
|
+
*/
|
|
1516
|
+
function installNativeKeyboard(window, keyboard, registry, snapshot, reset) {
|
|
1517
|
+
return keyboard.subscribe((input) => {
|
|
1518
|
+
if (input.revision !== snapshot().revision) return;
|
|
1519
|
+
reset?.();
|
|
1520
|
+
let target = window.document.activeElement;
|
|
1521
|
+
while (target?.shadowRoot?.activeElement != null && !target.matches("webview[data-sidebar-browser-frame]")) target = target.shadowRoot.activeElement;
|
|
1522
|
+
const top = [...window.document.querySelectorAll(_qilin_agent_client_ui_primitives.modalSelector)].at(-1);
|
|
1523
|
+
const region = target?.closest(".xterm") ? "terminal" : target?.matches("input, textarea, select, [contenteditable=\"true\"], [contenteditable=\"\"]") ? "editable" : "page";
|
|
1524
|
+
const context = {
|
|
1525
|
+
target,
|
|
1526
|
+
region,
|
|
1527
|
+
modal: top?.dataset.shortcutModal ?? (top === void 0 ? null : "other")
|
|
1528
|
+
};
|
|
1529
|
+
if (input.kind === "menu") {
|
|
1530
|
+
registry.invoke(input.commandId, context);
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
if (input.kind === "keyboard") {
|
|
1534
|
+
registry.dispatch({
|
|
1535
|
+
...input,
|
|
1536
|
+
composing: false,
|
|
1537
|
+
defaultPrevented: false
|
|
1538
|
+
}, context, () => {});
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
if (input.kind === "webview") {
|
|
1542
|
+
if (target?.matches("webview[data-sidebar-browser-frame]") !== true || !target.isConnected || input.frameName === "" || target.getAttribute("name") !== input.frameName) return;
|
|
1543
|
+
registry.dispatch({
|
|
1544
|
+
...input,
|
|
1545
|
+
composing: false,
|
|
1546
|
+
defaultPrevented: false
|
|
1547
|
+
}, {
|
|
1548
|
+
...context,
|
|
1549
|
+
source: "webview"
|
|
1550
|
+
}, () => {});
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
if (!(target instanceof HTMLIFrameElement) || !target.isConnected || !target.matches("iframe[data-sidebar-browser-frame], iframe[data-html-preview]") || input.frameName === "" || target.name !== input.frameName) return;
|
|
1554
|
+
registry.dispatch({
|
|
1555
|
+
...input,
|
|
1556
|
+
composing: false,
|
|
1557
|
+
defaultPrevented: false
|
|
1558
|
+
}, {
|
|
1559
|
+
...context,
|
|
1560
|
+
source: "iframe"
|
|
1561
|
+
}, () => {});
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
//#endregion
|
|
1565
|
+
//#region ../../../vendor/cosmokit/lib/index.js
|
|
1566
|
+
/** Return true when a value is `null` or `undefined`. */
|
|
1567
|
+
function isNullable(value) {
|
|
1568
|
+
return value === null || value === void 0;
|
|
1569
|
+
}
|
|
1570
|
+
/** Return true for non-array object values. */
|
|
1571
|
+
function isPlainObject(data) {
|
|
1572
|
+
return data && typeof data === "object" && !Array.isArray(data);
|
|
1573
|
+
}
|
|
1574
|
+
/** Filter object entries and return a new object. */
|
|
1575
|
+
function filterKeys(object, filter) {
|
|
1576
|
+
return Object.fromEntries(Object.entries(object).filter(([key, value]) => filter(key, value)));
|
|
1577
|
+
}
|
|
1578
|
+
/** Map object values while preserving the original key set. */
|
|
1579
|
+
function mapValues(object, transform) {
|
|
1580
|
+
return Object.fromEntries(Object.entries(object).map(([key, value]) => [key, transform(value, key)]));
|
|
1581
|
+
}
|
|
1582
|
+
/** Pick selected keys from an object, optionally including `undefined` values. */
|
|
1583
|
+
function pick(source, keys, forced) {
|
|
1584
|
+
if (!keys) return { ...source };
|
|
1585
|
+
const result = {};
|
|
1586
|
+
for (const key of keys) if (forced || source[key] !== void 0) result[key] = source[key];
|
|
1587
|
+
return result;
|
|
1588
|
+
}
|
|
1589
|
+
/** Shared config references used by schema validators and plugin runtimes. */
|
|
1590
|
+
const write = Symbol.for("cosmokit.volatile.write");
|
|
1591
|
+
function snapshot(value, ancestors = /* @__PURE__ */ new Set()) {
|
|
1592
|
+
if (typeof value === "function") throw new TypeError("volatile config cannot contain functions");
|
|
1593
|
+
if (value === null || typeof value !== "object") return value;
|
|
1594
|
+
if (ancestors.has(value)) throw new TypeError("volatile config cannot contain cycles");
|
|
1595
|
+
ancestors.add(value);
|
|
1596
|
+
try {
|
|
1597
|
+
if (Array.isArray(value)) return Object.freeze(value.map((item) => snapshot(item, ancestors)));
|
|
1598
|
+
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) throw new TypeError("volatile config objects must be plain objects or arrays");
|
|
1599
|
+
return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, item]) => [key, snapshot(item, ancestors)])));
|
|
1600
|
+
} finally {
|
|
1601
|
+
ancestors.delete(value);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
/**
|
|
1605
|
+
* Create a detached reference containing an immutable copy of the supplied data.
|
|
1606
|
+
* @param value - validated config data; class instances and functions are unsupported.
|
|
1607
|
+
* @returns a reference whose value is updated only by its owning runtime.
|
|
1608
|
+
*/
|
|
1609
|
+
function createVolatile(value) {
|
|
1610
|
+
let current = snapshot(value);
|
|
1611
|
+
return Object.freeze({
|
|
1612
|
+
get: () => current,
|
|
1613
|
+
[write]: (value) => {
|
|
1614
|
+
current = value;
|
|
1615
|
+
}
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
/**
|
|
1619
|
+
* Identify references across ESM/CJS copies of the shared library.
|
|
1620
|
+
* @param value - a parsed config value.
|
|
1621
|
+
* @returns whether the value implements the shared reference protocol.
|
|
1622
|
+
*/
|
|
1623
|
+
function isVolatile(value) {
|
|
1624
|
+
return typeof value === "object" && value !== null && write in value;
|
|
1625
|
+
}
|
|
1626
|
+
/** Test values using `instanceof` with a `toStringTag` fallback. */
|
|
1627
|
+
function is(type, value) {
|
|
1628
|
+
if (arguments.length === 1) return (value) => is(type, value);
|
|
1629
|
+
return type in globalThis && value instanceof globalThis[type] || Object.prototype.toString.call(value).slice(8, -1) === type;
|
|
1630
|
+
}
|
|
1631
|
+
function isArrayBufferLike(value) {
|
|
1632
|
+
return is("ArrayBuffer", value) || is("SharedArrayBuffer", value);
|
|
1633
|
+
}
|
|
1634
|
+
function isArrayBufferSource(value) {
|
|
1635
|
+
return isArrayBufferLike(value) || ArrayBuffer.isView(value);
|
|
1636
|
+
}
|
|
1637
|
+
/** Binary source detection and base64/hex conversion helpers. */
|
|
1638
|
+
var Binary;
|
|
1639
|
+
(function(Binary) {
|
|
1640
|
+
Binary.is = isArrayBufferLike;
|
|
1641
|
+
Binary.isSource = isArrayBufferSource;
|
|
1642
|
+
function fromSource(source) {
|
|
1643
|
+
if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
|
|
1644
|
+
else return source;
|
|
1645
|
+
}
|
|
1646
|
+
Binary.fromSource = fromSource;
|
|
1647
|
+
function toBase64(source) {
|
|
1648
|
+
source = fromSource(source);
|
|
1649
|
+
if (typeof Buffer !== "undefined") return Buffer.from(source).toString("base64");
|
|
1650
|
+
let binary = "";
|
|
1651
|
+
const bytes = new Uint8Array(source);
|
|
1652
|
+
for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]);
|
|
1653
|
+
return btoa(binary);
|
|
1654
|
+
}
|
|
1655
|
+
Binary.toBase64 = toBase64;
|
|
1656
|
+
function fromBase64(source) {
|
|
1657
|
+
if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "base64"));
|
|
1658
|
+
return Uint8Array.from(atob(source), (c) => c.charCodeAt(0));
|
|
1659
|
+
}
|
|
1660
|
+
Binary.fromBase64 = fromBase64;
|
|
1661
|
+
function toHex(source) {
|
|
1662
|
+
source = fromSource(source);
|
|
1663
|
+
if (typeof Buffer !== "undefined") return Buffer.from(source).toString("hex");
|
|
1664
|
+
return Array.from(new Uint8Array(source), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1665
|
+
}
|
|
1666
|
+
Binary.toHex = toHex;
|
|
1667
|
+
function fromHex(source) {
|
|
1668
|
+
if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "hex"));
|
|
1669
|
+
const hex = source.length % 2 === 0 ? source : source.slice(0, source.length - 1);
|
|
1670
|
+
const buffer = [];
|
|
1671
|
+
for (let i = 0; i < hex.length; i += 2) buffer.push(parseInt(`${hex[i]}${hex[i + 1]}`, 16));
|
|
1672
|
+
return Uint8Array.from(buffer).buffer;
|
|
1673
|
+
}
|
|
1674
|
+
Binary.fromHex = fromHex;
|
|
1675
|
+
})(Binary || (Binary = {}));
|
|
1676
|
+
Binary.fromBase64;
|
|
1677
|
+
Binary.toBase64;
|
|
1678
|
+
Binary.fromHex;
|
|
1679
|
+
Binary.toHex;
|
|
1680
|
+
/** Deep-clone common JavaScript values while preserving prototypes and cycles. */
|
|
1681
|
+
function clone(source, refs = /* @__PURE__ */ new Map()) {
|
|
1682
|
+
if (!source || typeof source !== "object") return source;
|
|
1683
|
+
if (is("Date", source)) return new Date(source.valueOf());
|
|
1684
|
+
if (is("RegExp", source)) return new RegExp(source.source, source.flags);
|
|
1685
|
+
if (isArrayBufferLike(source)) return source.slice(0);
|
|
1686
|
+
if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
|
|
1687
|
+
const cached = refs.get(source);
|
|
1688
|
+
if (cached) return cached;
|
|
1689
|
+
if (Array.isArray(source)) {
|
|
1690
|
+
const result = [];
|
|
1691
|
+
refs.set(source, result);
|
|
1692
|
+
source.forEach((value, index) => {
|
|
1693
|
+
result[index] = Reflect.apply(clone, null, [value, refs]);
|
|
1694
|
+
});
|
|
1695
|
+
return result;
|
|
1696
|
+
}
|
|
1697
|
+
const result = Object.create(Object.getPrototypeOf(source));
|
|
1698
|
+
refs.set(source, result);
|
|
1699
|
+
for (const key of Reflect.ownKeys(source)) {
|
|
1700
|
+
const descriptor = { ...Reflect.getOwnPropertyDescriptor(source, key) };
|
|
1701
|
+
if ("value" in descriptor) descriptor.value = Reflect.apply(clone, null, [descriptor.value, refs]);
|
|
1702
|
+
Reflect.defineProperty(result, key, descriptor);
|
|
1703
|
+
}
|
|
1704
|
+
return result;
|
|
1705
|
+
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Compare values recursively, treating two volatile references as equal regardless of value.
|
|
1708
|
+
* Strict comparison distinguishes null/undefined, treats opaque objects by identity,
|
|
1709
|
+
* compares URLs by normalized href, treats array holes as undefined, and considers distinct cyclic structures unequal.
|
|
1710
|
+
* @param a - first value.
|
|
1711
|
+
* @param b - second value.
|
|
1712
|
+
* @param strict - whether to require strict data equality outside volatile references.
|
|
1713
|
+
* @returns whether the values compare equal.
|
|
1714
|
+
*/
|
|
1715
|
+
function deepEqual(a, b, strict) {
|
|
1716
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
1717
|
+
function compare(a, b) {
|
|
1718
|
+
if (a === b) return true;
|
|
1719
|
+
if (isVolatile(a) || isVolatile(b)) return isVolatile(a) && isVolatile(b);
|
|
1720
|
+
if (!strict && isNullable(a) && isNullable(b)) return true;
|
|
1721
|
+
if (typeof a !== typeof b || typeof a !== "object" || !a || !b) return false;
|
|
1722
|
+
if (ancestors.has(a)) return false;
|
|
1723
|
+
function check(test, then) {
|
|
1724
|
+
return test(a) ? test(b) ? then(a, b) : false : test(b) ? false : void 0;
|
|
1725
|
+
}
|
|
1726
|
+
ancestors.add(a);
|
|
1727
|
+
try {
|
|
1728
|
+
return check(Array.isArray, (a, b) => {
|
|
1729
|
+
if (a.length !== b.length) return false;
|
|
1730
|
+
for (let index = 0; index < a.length; index++) if (!compare(a[index], b[index])) return false;
|
|
1731
|
+
return true;
|
|
1732
|
+
}) ?? check(is("Date"), (a, b) => a.valueOf() === b.valueOf()) ?? check(is("URL"), (a, b) => a.href === b.href) ?? check(is("RegExp"), (a, b) => a.source === b.source && a.flags === b.flags) ?? check(isArrayBufferLike, (a, b) => {
|
|
1733
|
+
if (a.byteLength !== b.byteLength) return false;
|
|
1734
|
+
const viewA = new Uint8Array(a);
|
|
1735
|
+
const viewB = new Uint8Array(b);
|
|
1736
|
+
for (let i = 0; i < viewA.length; i++) if (viewA[i] !== viewB[i]) return false;
|
|
1737
|
+
return true;
|
|
1738
|
+
}) ?? ((!strict || [a, b].every((value) => Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null)) && Object.keys({
|
|
1739
|
+
...a,
|
|
1740
|
+
...b
|
|
1741
|
+
}).every((key) => compare(a[key], b[key])));
|
|
1742
|
+
} finally {
|
|
1743
|
+
ancestors.delete(a);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
return compare(a, b);
|
|
1747
|
+
}
|
|
1748
|
+
/** Time constants plus parsing and formatting helpers. */
|
|
1749
|
+
var Time;
|
|
1750
|
+
(function(Time) {
|
|
1751
|
+
Time.millisecond = 1;
|
|
1752
|
+
Time.second = 1e3;
|
|
1753
|
+
Time.minute = Time.second * 60;
|
|
1754
|
+
Time.hour = Time.minute * 60;
|
|
1755
|
+
Time.day = Time.hour * 24;
|
|
1756
|
+
Time.week = Time.day * 7;
|
|
1757
|
+
let timezoneOffset = (/* @__PURE__ */ new Date()).getTimezoneOffset();
|
|
1758
|
+
function setTimezoneOffset(offset) {
|
|
1759
|
+
timezoneOffset = offset;
|
|
1760
|
+
}
|
|
1761
|
+
Time.setTimezoneOffset = setTimezoneOffset;
|
|
1762
|
+
function getTimezoneOffset() {
|
|
1763
|
+
return timezoneOffset;
|
|
1764
|
+
}
|
|
1765
|
+
Time.getTimezoneOffset = getTimezoneOffset;
|
|
1766
|
+
function getDateNumber(date = /* @__PURE__ */ new Date(), offset) {
|
|
1767
|
+
if (typeof date === "number") date = new Date(date);
|
|
1768
|
+
if (offset === void 0) offset = timezoneOffset;
|
|
1769
|
+
return Math.floor((date.valueOf() / Time.minute - offset) / 1440);
|
|
1770
|
+
}
|
|
1771
|
+
Time.getDateNumber = getDateNumber;
|
|
1772
|
+
function fromDateNumber(value, offset) {
|
|
1773
|
+
const date = new Date(value * Time.day);
|
|
1774
|
+
if (offset === void 0) offset = timezoneOffset;
|
|
1775
|
+
return new Date(+date + offset * Time.minute);
|
|
1776
|
+
}
|
|
1777
|
+
Time.fromDateNumber = fromDateNumber;
|
|
1778
|
+
const numeric = /\d+(?:\.\d+)?/.source;
|
|
1779
|
+
const timeRegExp = new RegExp(`^${[
|
|
1780
|
+
"w(?:eek(?:s)?)?",
|
|
1781
|
+
"d(?:ay(?:s)?)?",
|
|
1782
|
+
"h(?:our(?:s)?)?",
|
|
1783
|
+
"m(?:in(?:ute)?(?:s)?)?",
|
|
1784
|
+
"s(?:ec(?:ond)?(?:s)?)?"
|
|
1785
|
+
].map((unit) => `(${numeric}${unit})?`).join("")}$`);
|
|
1786
|
+
function parseTime(source) {
|
|
1787
|
+
const capture = timeRegExp.exec(source);
|
|
1788
|
+
if (!capture) return 0;
|
|
1789
|
+
return (parseFloat(capture[1]) * Time.week || 0) + (parseFloat(capture[2]) * Time.day || 0) + (parseFloat(capture[3]) * Time.hour || 0) + (parseFloat(capture[4]) * Time.minute || 0) + (parseFloat(capture[5]) * Time.second || 0);
|
|
1790
|
+
}
|
|
1791
|
+
Time.parseTime = parseTime;
|
|
1792
|
+
function parseDate(date) {
|
|
1793
|
+
const parsed = parseTime(date);
|
|
1794
|
+
if (parsed) date = Date.now() + parsed;
|
|
1795
|
+
else if (/^\d{1,2}(:\d{1,2}){1,2}$/.test(date)) date = `${(/* @__PURE__ */ new Date()).toLocaleDateString()}-${date}`;
|
|
1796
|
+
else if (/^\d{1,2}-\d{1,2}-\d{1,2}(:\d{1,2}){1,2}$/.test(date)) date = `${(/* @__PURE__ */ new Date()).getFullYear()}-${date}`;
|
|
1797
|
+
return date ? new Date(date) : /* @__PURE__ */ new Date();
|
|
1798
|
+
}
|
|
1799
|
+
Time.parseDate = parseDate;
|
|
1800
|
+
function format(ms) {
|
|
1801
|
+
const abs = Math.abs(ms);
|
|
1802
|
+
if (abs >= Time.day - Time.hour / 2) return Math.round(ms / Time.day) + "d";
|
|
1803
|
+
else if (abs >= Time.hour - Time.minute / 2) return Math.round(ms / Time.hour) + "h";
|
|
1804
|
+
else if (abs >= Time.minute - Time.second / 2) return Math.round(ms / Time.minute) + "m";
|
|
1805
|
+
else if (abs >= Time.second) return Math.round(ms / Time.second) + "s";
|
|
1806
|
+
return ms + "ms";
|
|
1807
|
+
}
|
|
1808
|
+
Time.format = format;
|
|
1809
|
+
function toDigits(source, length = 2) {
|
|
1810
|
+
return source.toString().padStart(length, "0");
|
|
1811
|
+
}
|
|
1812
|
+
Time.toDigits = toDigits;
|
|
1813
|
+
function template(template, time = /* @__PURE__ */ new Date()) {
|
|
1814
|
+
return template.replace("yyyy", time.getFullYear().toString()).replace("yy", time.getFullYear().toString().slice(2)).replace("MM", toDigits(time.getMonth() + 1)).replace("dd", toDigits(time.getDate())).replace("hh", toDigits(time.getHours())).replace("mm", toDigits(time.getMinutes())).replace("ss", toDigits(time.getSeconds())).replace("SSS", toDigits(time.getMilliseconds(), 3));
|
|
1815
|
+
}
|
|
1816
|
+
Time.template = template;
|
|
1817
|
+
})(Time || (Time = {}));
|
|
1818
|
+
//#endregion
|
|
1819
|
+
//#region ../../../vendor/schemastery/lib/index.mjs
|
|
1820
|
+
const kSchema = Symbol.for("schemastery");
|
|
1821
|
+
const kValidationError = Symbol.for("ValidationError");
|
|
1822
|
+
globalThis.__schemastery_index__ ??= 0;
|
|
1823
|
+
globalThis.__schemastery_refs__ = void 0;
|
|
1824
|
+
var ValidationError = class extends TypeError {
|
|
1825
|
+
options;
|
|
1826
|
+
name = "ValidationError";
|
|
1827
|
+
constructor(message, options) {
|
|
1828
|
+
let prefix = "$";
|
|
1829
|
+
for (const segment of options.path || []) if (typeof segment === "string") prefix += "." + segment;
|
|
1830
|
+
else if (typeof segment === "number") prefix += "[" + segment + "]";
|
|
1831
|
+
else if (typeof segment === "symbol") prefix += `[Symbol(${segment.toString()})]`;
|
|
1832
|
+
if (prefix.startsWith(".")) prefix = prefix.slice(1);
|
|
1833
|
+
super((prefix === "$" ? "" : `${prefix} `) + message);
|
|
1834
|
+
this.options = options;
|
|
1835
|
+
}
|
|
1836
|
+
static is(error) {
|
|
1837
|
+
return !!error?.[kValidationError];
|
|
1838
|
+
}
|
|
1839
|
+
};
|
|
1840
|
+
Object.defineProperty(ValidationError.prototype, kValidationError, { value: true });
|
|
1841
|
+
const Schema = function(options) {
|
|
1842
|
+
const schema = function(data, options = {}) {
|
|
1843
|
+
return Schema.resolve(data, schema, options)[0];
|
|
1844
|
+
};
|
|
1845
|
+
if (options.refs) {
|
|
1846
|
+
const refs = mapValues(options.refs, (options) => new Schema(options));
|
|
1847
|
+
const getRef = (uid) => refs[uid];
|
|
1848
|
+
for (const key in refs) {
|
|
1849
|
+
const options = refs[key];
|
|
1850
|
+
options.sKey = getRef(options.sKey);
|
|
1851
|
+
options.inner = getRef(options.inner);
|
|
1852
|
+
options.list = options.list && options.list.map(getRef);
|
|
1853
|
+
options.dict = options.dict && mapValues(options.dict, getRef);
|
|
1854
|
+
}
|
|
1855
|
+
return refs[options.uid];
|
|
1856
|
+
}
|
|
1857
|
+
Object.assign(schema, options);
|
|
1858
|
+
if (typeof schema.callback === "string") try {
|
|
1859
|
+
schema.callback = new Function("return " + schema.callback)();
|
|
1860
|
+
} catch {}
|
|
1861
|
+
Object.defineProperty(schema, "uid", { value: globalThis.__schemastery_index__++ });
|
|
1862
|
+
Object.setPrototypeOf(schema, Schema.prototype);
|
|
1863
|
+
schema.meta ||= {};
|
|
1864
|
+
schema.toString = schema.toString.bind(schema);
|
|
1865
|
+
return schema;
|
|
1866
|
+
};
|
|
1867
|
+
Schema.prototype = Object.create(Function.prototype);
|
|
1868
|
+
Schema.prototype[kSchema] = true;
|
|
1869
|
+
Object.defineProperty(Schema.prototype, "~standard", { get() {
|
|
1870
|
+
return {
|
|
1871
|
+
version: 1,
|
|
1872
|
+
vendor: "schemastery",
|
|
1873
|
+
validate: (value) => {
|
|
1874
|
+
try {
|
|
1875
|
+
return { value: Schema.resolve(value, this, {})[0] };
|
|
1876
|
+
} catch (error) {
|
|
1877
|
+
if (ValidationError.is(error)) return { issues: [{
|
|
1878
|
+
message: error.message,
|
|
1879
|
+
path: error.options.path
|
|
1880
|
+
}] };
|
|
1881
|
+
throw error;
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
};
|
|
1885
|
+
} });
|
|
1886
|
+
Schema.ValidationError = ValidationError;
|
|
1887
|
+
Schema.prototype.toJSON = function toJSON() {
|
|
1888
|
+
if (globalThis.__schemastery_refs__) {
|
|
1889
|
+
globalThis.__schemastery_refs__[this.uid] ??= JSON.parse(JSON.stringify({ ...this }));
|
|
1890
|
+
return this.uid;
|
|
1891
|
+
}
|
|
1892
|
+
globalThis.__schemastery_refs__ = { [this.uid]: { ...this } };
|
|
1893
|
+
globalThis.__schemastery_refs__[this.uid] = JSON.parse(JSON.stringify({ ...this }));
|
|
1894
|
+
const result = {
|
|
1895
|
+
uid: this.uid,
|
|
1896
|
+
refs: globalThis.__schemastery_refs__
|
|
1897
|
+
};
|
|
1898
|
+
globalThis.__schemastery_refs__ = void 0;
|
|
1899
|
+
return result;
|
|
1900
|
+
};
|
|
1901
|
+
Schema.prototype.set = function set(key, value) {
|
|
1902
|
+
this.dict[key] = value;
|
|
1903
|
+
return this;
|
|
1904
|
+
};
|
|
1905
|
+
Schema.prototype.push = function push(value) {
|
|
1906
|
+
this.list.push(value);
|
|
1907
|
+
return this;
|
|
1908
|
+
};
|
|
1909
|
+
function mergeDesc(original, messages) {
|
|
1910
|
+
const result = typeof original === "string" ? { "": original } : { ...original };
|
|
1911
|
+
for (const locale in messages) {
|
|
1912
|
+
const value = messages[locale];
|
|
1913
|
+
if (value?.$description || value?.$desc) result[locale] = value.$description || value.$desc;
|
|
1914
|
+
else if (typeof value === "string") result[locale] = value;
|
|
1915
|
+
}
|
|
1916
|
+
return result;
|
|
1917
|
+
}
|
|
1918
|
+
function getInner(value) {
|
|
1919
|
+
return value?.$value ?? value?.$inner;
|
|
1920
|
+
}
|
|
1921
|
+
function extractKeys(data) {
|
|
1922
|
+
return filterKeys(data ?? {}, (key) => !key.startsWith("$"));
|
|
1923
|
+
}
|
|
1924
|
+
Schema.prototype.i18n = function i18n(messages) {
|
|
1925
|
+
const schema = Schema(this);
|
|
1926
|
+
const desc = mergeDesc(schema.meta.description, messages);
|
|
1927
|
+
if (Object.keys(desc).length) schema.meta.description = desc;
|
|
1928
|
+
if (schema.dict) schema.dict = mapValues(schema.dict, (inner, key) => {
|
|
1929
|
+
return inner.i18n(mapValues(messages, (data) => getInner(data)?.[key] ?? data?.[key]));
|
|
1930
|
+
});
|
|
1931
|
+
if (schema.list) schema.list = schema.list.map((inner, index) => {
|
|
1932
|
+
return inner.i18n(mapValues(messages, (data = {}) => {
|
|
1933
|
+
if (Array.isArray(getInner(data))) return getInner(data)[index];
|
|
1934
|
+
if (Array.isArray(data)) return data[index];
|
|
1935
|
+
return extractKeys(data);
|
|
1936
|
+
}));
|
|
1937
|
+
});
|
|
1938
|
+
if (schema.inner) schema.inner = schema.inner.i18n(mapValues(messages, (data) => {
|
|
1939
|
+
if (getInner(data)) return getInner(data);
|
|
1940
|
+
return extractKeys(data);
|
|
1941
|
+
}));
|
|
1942
|
+
if (schema.sKey) schema.sKey = schema.sKey.i18n(mapValues(messages, (data) => data?.$key));
|
|
1943
|
+
return schema;
|
|
1944
|
+
};
|
|
1945
|
+
Schema.prototype.extra = function extra(key, value) {
|
|
1946
|
+
const schema = Schema(this);
|
|
1947
|
+
schema.meta = {
|
|
1948
|
+
...schema.meta,
|
|
1949
|
+
[key]: value
|
|
1950
|
+
};
|
|
1951
|
+
return schema;
|
|
1952
|
+
};
|
|
1953
|
+
for (const key of [
|
|
1954
|
+
"required",
|
|
1955
|
+
"disabled",
|
|
1956
|
+
"collapse",
|
|
1957
|
+
"hidden",
|
|
1958
|
+
"loose"
|
|
1959
|
+
]) Object.assign(Schema.prototype, { [key](value = true) {
|
|
1960
|
+
const schema = Schema(this);
|
|
1961
|
+
schema.meta = {
|
|
1962
|
+
...schema.meta,
|
|
1963
|
+
[key]: value
|
|
1964
|
+
};
|
|
1965
|
+
return schema;
|
|
1966
|
+
} });
|
|
1967
|
+
Schema.prototype.deprecated = function deprecated() {
|
|
1968
|
+
const schema = Schema(this);
|
|
1969
|
+
schema.meta.badges ||= [];
|
|
1970
|
+
schema.meta.badges.push({
|
|
1971
|
+
text: "deprecated",
|
|
1972
|
+
type: "danger"
|
|
1973
|
+
});
|
|
1974
|
+
return schema;
|
|
1975
|
+
};
|
|
1976
|
+
Schema.prototype.experimental = function experimental() {
|
|
1977
|
+
const schema = Schema(this);
|
|
1978
|
+
schema.meta.badges ||= [];
|
|
1979
|
+
schema.meta.badges.push({
|
|
1980
|
+
text: "experimental",
|
|
1981
|
+
type: "warning"
|
|
1982
|
+
});
|
|
1983
|
+
return schema;
|
|
1984
|
+
};
|
|
1985
|
+
Schema.prototype.pattern = function pattern(regexp) {
|
|
1986
|
+
const schema = Schema(this);
|
|
1987
|
+
const pattern = pick(regexp, ["source", "flags"]);
|
|
1988
|
+
schema.meta = {
|
|
1989
|
+
...schema.meta,
|
|
1990
|
+
pattern
|
|
1991
|
+
};
|
|
1992
|
+
return schema;
|
|
1993
|
+
};
|
|
1994
|
+
Schema.prototype.simplify = function simplify(value) {
|
|
1995
|
+
if (isVolatile(value)) value = value.get();
|
|
1996
|
+
if (deepEqual(value, this.meta.default, this.type === "dict")) return null;
|
|
1997
|
+
if (isNullable(value)) return value;
|
|
1998
|
+
if (this.type === "object" || this.type === "dict") {
|
|
1999
|
+
const result = {};
|
|
2000
|
+
for (const key in value) {
|
|
2001
|
+
const item = (this.type === "object" ? this.dict[key] : this.inner)?.simplify(value[key]);
|
|
2002
|
+
if (this.type === "dict" || !isNullable(item)) result[key] = item;
|
|
2003
|
+
}
|
|
2004
|
+
if (deepEqual(result, this.meta.default, this.type === "dict")) return null;
|
|
2005
|
+
return result;
|
|
2006
|
+
} else if (this.type === "array" || this.type === "tuple") {
|
|
2007
|
+
const result = [];
|
|
2008
|
+
value.forEach((value, index) => {
|
|
2009
|
+
const schema = this.type === "array" ? this.inner : this.list[index];
|
|
2010
|
+
const item = schema ? schema.simplify(value) : value;
|
|
2011
|
+
result.push(item);
|
|
2012
|
+
});
|
|
2013
|
+
return result;
|
|
2014
|
+
} else if (this.type === "intersect") {
|
|
2015
|
+
const result = {};
|
|
2016
|
+
for (const item of this.list) Object.assign(result, item.simplify(value));
|
|
2017
|
+
return result;
|
|
2018
|
+
} else if (this.type === "union") for (const schema of this.list) try {
|
|
2019
|
+
Schema.resolve(value, schema, {});
|
|
2020
|
+
return schema.simplify(value);
|
|
2021
|
+
} catch {}
|
|
2022
|
+
return value;
|
|
2023
|
+
};
|
|
2024
|
+
Schema.prototype.toString = function toString(inline) {
|
|
2025
|
+
return formatters[this.type]?.(this, inline) ?? `Schema<${this.type}>`;
|
|
2026
|
+
};
|
|
2027
|
+
Schema.prototype.role = function role(role, extra) {
|
|
2028
|
+
const schema = Schema(this);
|
|
2029
|
+
schema.meta = {
|
|
2030
|
+
...schema.meta,
|
|
2031
|
+
role,
|
|
2032
|
+
extra
|
|
2033
|
+
};
|
|
2034
|
+
return schema;
|
|
2035
|
+
};
|
|
2036
|
+
for (const key of [
|
|
2037
|
+
"default",
|
|
2038
|
+
"link",
|
|
2039
|
+
"comment",
|
|
2040
|
+
"description",
|
|
2041
|
+
"max",
|
|
2042
|
+
"min",
|
|
2043
|
+
"step"
|
|
2044
|
+
]) Object.assign(Schema.prototype, { [key](value) {
|
|
2045
|
+
const schema = Schema(this);
|
|
2046
|
+
schema.meta = {
|
|
2047
|
+
...schema.meta,
|
|
2048
|
+
[key]: value
|
|
2049
|
+
};
|
|
2050
|
+
return schema;
|
|
2051
|
+
} });
|
|
2052
|
+
Schema.prototype.volatile = function volatile() {
|
|
2053
|
+
if (this.meta.volatile) throw new TypeError("volatile schema is already wrapped");
|
|
2054
|
+
return this.extra("volatile", true);
|
|
2055
|
+
};
|
|
2056
|
+
const resolvers = {};
|
|
2057
|
+
const checkedVolatile = Symbol("checked-volatile-schema");
|
|
2058
|
+
function validateVolatileSchema(schema, path = [], blocked = false, seen = /* @__PURE__ */ new Map()) {
|
|
2059
|
+
const states = seen.get(schema) ?? /* @__PURE__ */ new Set();
|
|
2060
|
+
if (states.has(blocked)) return;
|
|
2061
|
+
states.add(blocked);
|
|
2062
|
+
seen.set(schema, states);
|
|
2063
|
+
if (schema.meta?.volatile && blocked) throw new ValidationError("volatile fields require a fixed object path without an enclosing volatile field", { path });
|
|
2064
|
+
const nested = blocked || !!schema.meta?.volatile;
|
|
2065
|
+
if (schema.dict) for (const [key, child] of Object.entries(schema.dict)) validateVolatileSchema(child, [...path, key], nested, seen);
|
|
2066
|
+
if (schema.sKey) validateVolatileSchema(schema.sKey, [...path, "<key>"], true, seen);
|
|
2067
|
+
if (schema.inner && (schema.type !== "lazy" || schema.inner[kSchema])) validateVolatileSchema(schema.inner, [...path, "*"], true, seen);
|
|
2068
|
+
if (schema.list) for (let index = 0; index < schema.list.length; index++) validateVolatileSchema(schema.list[index], [...path, String(index)], true, seen);
|
|
2069
|
+
}
|
|
2070
|
+
Schema.extend = function extend(type, resolve) {
|
|
2071
|
+
resolvers[type] = resolve;
|
|
2072
|
+
};
|
|
2073
|
+
Schema.resolve = function resolve(data, schema, options = {}, strict = false) {
|
|
2074
|
+
if (!schema) return [data];
|
|
2075
|
+
if (!options[checkedVolatile]) {
|
|
2076
|
+
validateVolatileSchema(schema, options.path);
|
|
2077
|
+
options = {
|
|
2078
|
+
...options,
|
|
2079
|
+
[checkedVolatile]: true
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
if (schema.meta?.volatile) {
|
|
2083
|
+
const inner = Schema(schema);
|
|
2084
|
+
inner.meta = {
|
|
2085
|
+
...schema.meta,
|
|
2086
|
+
volatile: false
|
|
2087
|
+
};
|
|
2088
|
+
const [value, adapted] = Schema.resolve(data, inner, options, strict);
|
|
2089
|
+
try {
|
|
2090
|
+
return [createVolatile(value), adapted];
|
|
2091
|
+
} catch (error) {
|
|
2092
|
+
throw new ValidationError(error instanceof Error ? error.message : String(error), options);
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
if (options.ignore?.(data, schema)) return [data];
|
|
2096
|
+
if (isNullable(data) && schema.type !== "lazy") {
|
|
2097
|
+
if (schema.meta.required) throw new ValidationError(`missing required value`, options);
|
|
2098
|
+
let current = schema;
|
|
2099
|
+
let fallback = schema.meta.default;
|
|
2100
|
+
while (current?.type === "intersect" && isNullable(fallback)) {
|
|
2101
|
+
current = current.list[0];
|
|
2102
|
+
fallback = current?.meta.default;
|
|
2103
|
+
}
|
|
2104
|
+
if (isNullable(fallback)) return [data];
|
|
2105
|
+
data = clone(fallback);
|
|
2106
|
+
}
|
|
2107
|
+
const callback = resolvers[schema.type];
|
|
2108
|
+
if (!callback) throw new ValidationError(`unsupported type "${schema.type}"`, options);
|
|
2109
|
+
try {
|
|
2110
|
+
return callback(data, schema, options, strict);
|
|
2111
|
+
} catch (error) {
|
|
2112
|
+
if (!schema.meta.loose) throw error;
|
|
2113
|
+
return [schema.meta.default];
|
|
2114
|
+
}
|
|
2115
|
+
};
|
|
2116
|
+
Schema.from = function from(source) {
|
|
2117
|
+
if (isNullable(source)) return Schema.any();
|
|
2118
|
+
else if ([
|
|
2119
|
+
"string",
|
|
2120
|
+
"number",
|
|
2121
|
+
"boolean"
|
|
2122
|
+
].includes(typeof source)) return Schema.const(source).required();
|
|
2123
|
+
else if (source[kSchema]) return source;
|
|
2124
|
+
else if (typeof source === "function") switch (source) {
|
|
2125
|
+
case String: return Schema.string().required();
|
|
2126
|
+
case Number: return Schema.number().required();
|
|
2127
|
+
case Boolean: return Schema.boolean().required();
|
|
2128
|
+
case Function: return Schema.function().required();
|
|
2129
|
+
default: return Schema.is(source).required();
|
|
2130
|
+
}
|
|
2131
|
+
else throw new TypeError(`cannot infer schema from ${source}`);
|
|
2132
|
+
};
|
|
2133
|
+
Schema.lazy = function lazy(builder) {
|
|
2134
|
+
const toJSON = () => {
|
|
2135
|
+
if (!schema.inner[kSchema]) {
|
|
2136
|
+
schema.inner = schema.builder();
|
|
2137
|
+
schema.inner.meta = {
|
|
2138
|
+
...schema.meta,
|
|
2139
|
+
...schema.inner.meta
|
|
2140
|
+
};
|
|
2141
|
+
}
|
|
2142
|
+
return schema.inner.toJSON();
|
|
2143
|
+
};
|
|
2144
|
+
const schema = new Schema({
|
|
2145
|
+
type: "lazy",
|
|
2146
|
+
builder,
|
|
2147
|
+
inner: { toJSON }
|
|
2148
|
+
});
|
|
2149
|
+
return schema;
|
|
2150
|
+
};
|
|
2151
|
+
Schema.natural = function natural() {
|
|
2152
|
+
return Schema.number().step(1).min(0);
|
|
2153
|
+
};
|
|
2154
|
+
Schema.percent = function percent() {
|
|
2155
|
+
return Schema.number().step(.01).min(0).max(1).role("slider");
|
|
2156
|
+
};
|
|
2157
|
+
Schema.date = function date() {
|
|
2158
|
+
return Schema.union([Schema.is(Date), Schema.transform(Schema.string().role("datetime"), (value, options) => {
|
|
2159
|
+
const date = new Date(value);
|
|
2160
|
+
if (isNaN(+date)) throw new ValidationError(`invalid date "${value}"`, options);
|
|
2161
|
+
return date;
|
|
2162
|
+
}, true)]);
|
|
2163
|
+
};
|
|
2164
|
+
Schema.regExp = function regExp(flag = "") {
|
|
2165
|
+
return Schema.union([Schema.is(RegExp), Schema.transform(Schema.string().role("regexp", { flag }), (value, options) => {
|
|
2166
|
+
try {
|
|
2167
|
+
return new RegExp(value, flag);
|
|
2168
|
+
} catch (e) {
|
|
2169
|
+
throw new ValidationError(e.message, options);
|
|
2170
|
+
}
|
|
2171
|
+
}, true)]);
|
|
2172
|
+
};
|
|
2173
|
+
Schema.arrayBuffer = function arrayBuffer(encoding) {
|
|
2174
|
+
return Schema.union([
|
|
2175
|
+
Schema.is(ArrayBuffer),
|
|
2176
|
+
Schema.is(SharedArrayBuffer),
|
|
2177
|
+
Schema.transform(Schema.any(), (value, options) => {
|
|
2178
|
+
if (Binary.isSource(value)) return Binary.fromSource(value);
|
|
2179
|
+
throw new ValidationError(`expected ArrayBufferSource but got ${value}`, options);
|
|
2180
|
+
}, true),
|
|
2181
|
+
...encoding ? [Schema.transform(Schema.string(), (value, options) => {
|
|
2182
|
+
try {
|
|
2183
|
+
return encoding === "base64" ? Binary.fromBase64(value) : Binary.fromHex(value);
|
|
2184
|
+
} catch (e) {
|
|
2185
|
+
throw new ValidationError(e.message, options);
|
|
2186
|
+
}
|
|
2187
|
+
}, true)] : []
|
|
2188
|
+
]);
|
|
2189
|
+
};
|
|
2190
|
+
Schema.extend("lazy", (data, schema, options, strict) => {
|
|
2191
|
+
if (!schema.inner[kSchema]) {
|
|
2192
|
+
schema.inner = schema.builder();
|
|
2193
|
+
schema.inner.meta = {
|
|
2194
|
+
...schema.meta,
|
|
2195
|
+
...schema.inner.meta
|
|
2196
|
+
};
|
|
2197
|
+
validateVolatileSchema(schema.inner, options.path, true);
|
|
2198
|
+
}
|
|
2199
|
+
return Schema.resolve(data, schema.inner, options, strict);
|
|
2200
|
+
});
|
|
2201
|
+
Schema.extend("any", (data) => {
|
|
2202
|
+
return [data];
|
|
2203
|
+
});
|
|
2204
|
+
Schema.extend("never", (data, _, options) => {
|
|
2205
|
+
throw new ValidationError(`expected nullable but got ${data}`, options);
|
|
2206
|
+
});
|
|
2207
|
+
Schema.extend("const", (data, { value }, options) => {
|
|
2208
|
+
if (deepEqual(data, value)) return [value];
|
|
2209
|
+
throw new ValidationError(`expected ${value} but got ${data}`, options);
|
|
2210
|
+
});
|
|
2211
|
+
function checkWithinRange(data, meta, description, options, skipMin = false) {
|
|
2212
|
+
const { max = Infinity, min = -Infinity } = meta;
|
|
2213
|
+
if (data > max) throw new ValidationError(`expected ${description} <= ${max} but got ${data}`, options);
|
|
2214
|
+
if (data < min && !skipMin) throw new ValidationError(`expected ${description} >= ${min} but got ${data}`, options);
|
|
2215
|
+
}
|
|
2216
|
+
Schema.extend("string", (data, { meta }, options) => {
|
|
2217
|
+
if (typeof data !== "string") throw new ValidationError(`expected string but got ${data}`, options);
|
|
2218
|
+
if (meta.pattern) {
|
|
2219
|
+
const regexp = new RegExp(meta.pattern.source, meta.pattern.flags);
|
|
2220
|
+
if (!regexp.test(data)) throw new ValidationError(`expect string to match regexp ${regexp}`, options);
|
|
2221
|
+
}
|
|
2222
|
+
checkWithinRange(data.length, meta, "string length", options);
|
|
2223
|
+
return [data];
|
|
2224
|
+
});
|
|
2225
|
+
function decimalShift(data, digits) {
|
|
2226
|
+
const str = data.toString();
|
|
2227
|
+
if (str.includes("e")) return data * Math.pow(10, digits);
|
|
2228
|
+
const index = str.indexOf(".");
|
|
2229
|
+
if (index === -1) return data * Math.pow(10, digits);
|
|
2230
|
+
const frac = str.slice(index + 1);
|
|
2231
|
+
const integer = str.slice(0, index);
|
|
2232
|
+
if (frac.length <= digits) return +(integer + frac.padEnd(digits, "0"));
|
|
2233
|
+
return +(integer + frac.slice(0, digits) + "." + frac.slice(digits));
|
|
2234
|
+
}
|
|
2235
|
+
function isMultipleOf(data, min, step) {
|
|
2236
|
+
step = Math.abs(step);
|
|
2237
|
+
if (!/^\d+\.\d+$/.test(step.toString())) return (data - min) % step === 0;
|
|
2238
|
+
const index = step.toString().indexOf(".");
|
|
2239
|
+
const digits = step.toString().slice(index + 1).length;
|
|
2240
|
+
return Math.abs(decimalShift(data, digits) - decimalShift(min, digits)) % decimalShift(step, digits) === 0;
|
|
2241
|
+
}
|
|
2242
|
+
Schema.extend("number", (data, { meta }, options) => {
|
|
2243
|
+
if (typeof data !== "number") throw new ValidationError(`expected number but got ${data}`, options);
|
|
2244
|
+
checkWithinRange(data, meta, "number", options);
|
|
2245
|
+
const { step } = meta;
|
|
2246
|
+
if (step && !isMultipleOf(data, meta.min ?? 0, step)) throw new ValidationError(`expected number multiple of ${step} but got ${data}`, options);
|
|
2247
|
+
return [data];
|
|
2248
|
+
});
|
|
2249
|
+
Schema.extend("boolean", (data, _, options) => {
|
|
2250
|
+
if (typeof data === "boolean") return [data];
|
|
2251
|
+
throw new ValidationError(`expected boolean but got ${data}`, options);
|
|
2252
|
+
});
|
|
2253
|
+
Schema.extend("bitset", (data, { bits, meta }, options) => {
|
|
2254
|
+
let value = 0, keys = [];
|
|
2255
|
+
if (typeof data === "number") {
|
|
2256
|
+
value = data;
|
|
2257
|
+
for (const key in bits) if (data & bits[key]) keys.push(key);
|
|
2258
|
+
} else if (Array.isArray(data)) {
|
|
2259
|
+
keys = data;
|
|
2260
|
+
for (const key of keys) {
|
|
2261
|
+
if (typeof key !== "string") throw new ValidationError(`expected string but got ${key}`, options);
|
|
2262
|
+
if (key in bits) value |= bits[key];
|
|
2263
|
+
}
|
|
2264
|
+
} else throw new ValidationError(`expected number or array but got ${data}`, options);
|
|
2265
|
+
if (value === meta.default) return [value];
|
|
2266
|
+
return [value, keys];
|
|
2267
|
+
});
|
|
2268
|
+
Schema.extend("function", (data, _, options) => {
|
|
2269
|
+
if (typeof data === "function") return [data];
|
|
2270
|
+
throw new ValidationError(`expected function but got ${data}`, options);
|
|
2271
|
+
});
|
|
2272
|
+
Schema.extend("is", (data, { constructor }, options) => {
|
|
2273
|
+
if (typeof constructor === "function") {
|
|
2274
|
+
if (data instanceof constructor) return [data];
|
|
2275
|
+
throw new ValidationError(`expected ${constructor.name} but got ${data}`, options);
|
|
2276
|
+
} else {
|
|
2277
|
+
if (isNullable(data)) throw new ValidationError(`expected ${constructor} but got ${data}`, options);
|
|
2278
|
+
let prototype = Object.getPrototypeOf(data);
|
|
2279
|
+
while (prototype) {
|
|
2280
|
+
if (prototype.constructor?.name === constructor) return [data];
|
|
2281
|
+
prototype = Object.getPrototypeOf(prototype);
|
|
2282
|
+
}
|
|
2283
|
+
throw new ValidationError(`expected ${constructor} but got ${data}`, options);
|
|
2284
|
+
}
|
|
2285
|
+
});
|
|
2286
|
+
function property(data, key, schema, options) {
|
|
2287
|
+
try {
|
|
2288
|
+
const [value, adapted] = Schema.resolve(data[key], schema, {
|
|
2289
|
+
...options,
|
|
2290
|
+
path: [...options.path || [], key]
|
|
2291
|
+
});
|
|
2292
|
+
if (adapted !== void 0) data[key] = adapted;
|
|
2293
|
+
return value;
|
|
2294
|
+
} catch (e) {
|
|
2295
|
+
if (!options?.autofix) throw e;
|
|
2296
|
+
delete data[key];
|
|
2297
|
+
return schema.meta.volatile ? createVolatile(schema.meta.default) : schema.meta.default;
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
Schema.extend("array", (data, { inner, meta }, options) => {
|
|
2301
|
+
if (!Array.isArray(data)) throw new ValidationError(`expected array but got ${data}`, options);
|
|
2302
|
+
checkWithinRange(data.length, meta, "array length", options, !isNullable(inner.meta.default));
|
|
2303
|
+
return [data.map((_, index) => property(data, index, inner, options))];
|
|
2304
|
+
});
|
|
2305
|
+
Schema.extend("dict", (data, { inner, sKey }, options, strict) => {
|
|
2306
|
+
if (!isPlainObject(data)) throw new ValidationError(`expected object but got ${data}`, options);
|
|
2307
|
+
const result = {};
|
|
2308
|
+
for (const key in data) {
|
|
2309
|
+
let rKey;
|
|
2310
|
+
try {
|
|
2311
|
+
rKey = Schema.resolve(key, sKey, options)[0];
|
|
2312
|
+
} catch (error) {
|
|
2313
|
+
if (strict) continue;
|
|
2314
|
+
throw error;
|
|
2315
|
+
}
|
|
2316
|
+
result[rKey] = property(data, key, inner, options);
|
|
2317
|
+
data[rKey] = data[key];
|
|
2318
|
+
if (key !== rKey) delete data[key];
|
|
2319
|
+
}
|
|
2320
|
+
return [result];
|
|
2321
|
+
});
|
|
2322
|
+
Schema.extend("tuple", (data, { list }, options, strict) => {
|
|
2323
|
+
if (!Array.isArray(data)) throw new ValidationError(`expected array but got ${data}`, options);
|
|
2324
|
+
const result = list.map((inner, index) => property(data, index, inner, options));
|
|
2325
|
+
if (strict) return [result];
|
|
2326
|
+
result.push(...data.slice(list.length));
|
|
2327
|
+
return [result];
|
|
2328
|
+
});
|
|
2329
|
+
function merge(result, data) {
|
|
2330
|
+
for (const key in data) {
|
|
2331
|
+
if (key in result) continue;
|
|
2332
|
+
result[key] = data[key];
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
Schema.extend("object", (data, { dict }, options, strict) => {
|
|
2336
|
+
if (!isPlainObject(data)) throw new ValidationError(`expected object but got ${data}`, options);
|
|
2337
|
+
const result = {};
|
|
2338
|
+
for (const key in dict) {
|
|
2339
|
+
const value = property(data, key, dict[key], options);
|
|
2340
|
+
if (!isNullable(value) || key in data) result[key] = value;
|
|
2341
|
+
}
|
|
2342
|
+
if (!strict) merge(result, data);
|
|
2343
|
+
return [result];
|
|
2344
|
+
});
|
|
2345
|
+
Schema.extend("union", (data, { list, toString }, options, strict) => {
|
|
2346
|
+
const messages = [];
|
|
2347
|
+
for (const inner of list) try {
|
|
2348
|
+
return Schema.resolve(data, inner, options, strict);
|
|
2349
|
+
} catch (error) {
|
|
2350
|
+
messages.push(error);
|
|
2351
|
+
}
|
|
2352
|
+
throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options);
|
|
2353
|
+
});
|
|
2354
|
+
Schema.extend("intersect", (data, { list, toString }, options, strict) => {
|
|
2355
|
+
if (!list.length) return [data];
|
|
2356
|
+
let result;
|
|
2357
|
+
for (const inner of list) {
|
|
2358
|
+
const value = Schema.resolve(data, inner, options, true)[0];
|
|
2359
|
+
if (isNullable(value)) continue;
|
|
2360
|
+
if (isNullable(result)) result = value;
|
|
2361
|
+
else if (typeof result !== typeof value) throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options);
|
|
2362
|
+
else if (typeof value === "object") merge(result ??= {}, value);
|
|
2363
|
+
else if (result !== value) throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options);
|
|
2364
|
+
}
|
|
2365
|
+
if (!strict && isPlainObject(data)) merge(result, data);
|
|
2366
|
+
return [result];
|
|
2367
|
+
});
|
|
2368
|
+
Schema.extend("transform", (data, { inner, callback, preserve }, options) => {
|
|
2369
|
+
const [result, adapted = data] = Schema.resolve(data, inner, options, true);
|
|
2370
|
+
if (preserve) return [callback(result)];
|
|
2371
|
+
else return [callback(result), callback(adapted)];
|
|
2372
|
+
});
|
|
2373
|
+
const formatters = {};
|
|
2374
|
+
function defineMethod(name, keys, format) {
|
|
2375
|
+
formatters[name] = format;
|
|
2376
|
+
Object.assign(Schema, { [name](...args) {
|
|
2377
|
+
const schema = new Schema({ type: name });
|
|
2378
|
+
keys.forEach((key, index) => {
|
|
2379
|
+
switch (key) {
|
|
2380
|
+
case "sKey":
|
|
2381
|
+
schema.sKey = args[index] ?? Schema.string();
|
|
2382
|
+
break;
|
|
2383
|
+
case "inner":
|
|
2384
|
+
schema.inner = Schema.from(args[index]);
|
|
2385
|
+
break;
|
|
2386
|
+
case "list":
|
|
2387
|
+
schema.list = args[index].map(Schema.from);
|
|
2388
|
+
break;
|
|
2389
|
+
case "dict":
|
|
2390
|
+
schema.dict = mapValues(args[index], Schema.from);
|
|
2391
|
+
break;
|
|
2392
|
+
case "bits":
|
|
2393
|
+
schema.bits = {};
|
|
2394
|
+
for (const key in args[index]) {
|
|
2395
|
+
if (typeof args[index][key] !== "number") continue;
|
|
2396
|
+
schema.bits[key] = args[index][key];
|
|
2397
|
+
}
|
|
2398
|
+
break;
|
|
2399
|
+
case "callback": {
|
|
2400
|
+
const callback = schema.callback = args[index];
|
|
2401
|
+
callback["toJSON"] ||= () => callback.toString();
|
|
2402
|
+
break;
|
|
2403
|
+
}
|
|
2404
|
+
case "constructor": {
|
|
2405
|
+
const constructor = schema.constructor = args[index];
|
|
2406
|
+
if (typeof constructor === "function") constructor["toJSON"] ||= () => constructor["name"];
|
|
2407
|
+
break;
|
|
2408
|
+
}
|
|
2409
|
+
default: schema[key] = args[index];
|
|
2410
|
+
}
|
|
2411
|
+
});
|
|
2412
|
+
if (name === "object" || name === "dict") schema.meta.default = {};
|
|
2413
|
+
else if (name === "array" || name === "tuple") schema.meta.default = [];
|
|
2414
|
+
else if (name === "bitset") schema.meta.default = 0;
|
|
2415
|
+
return schema;
|
|
2416
|
+
} });
|
|
2417
|
+
}
|
|
2418
|
+
defineMethod("is", ["constructor"], ({ constructor }) => {
|
|
2419
|
+
if (typeof constructor === "function") return constructor.name;
|
|
2420
|
+
else return constructor;
|
|
2421
|
+
});
|
|
2422
|
+
defineMethod("any", [], () => "any");
|
|
2423
|
+
defineMethod("never", [], () => "never");
|
|
2424
|
+
defineMethod("const", ["value"], ({ value }) => typeof value === "string" ? JSON.stringify(value) : value);
|
|
2425
|
+
defineMethod("string", [], () => "string");
|
|
2426
|
+
defineMethod("number", [], () => "number");
|
|
2427
|
+
defineMethod("boolean", [], () => "boolean");
|
|
2428
|
+
defineMethod("bitset", ["bits"], () => "bitset");
|
|
2429
|
+
defineMethod("function", [], () => "function");
|
|
2430
|
+
defineMethod("array", ["inner"], ({ inner }) => `${inner.toString(true)}[]`);
|
|
2431
|
+
defineMethod("dict", ["inner", "sKey"], ({ inner, sKey }) => `{ [key: ${sKey.toString()}]: ${inner.toString()} }`);
|
|
2432
|
+
defineMethod("tuple", ["list"], ({ list }) => `[${list.map((inner) => inner.toString()).join(", ")}]`);
|
|
2433
|
+
defineMethod("object", ["dict"], ({ dict }) => {
|
|
2434
|
+
if (Object.keys(dict).length === 0) return "{}";
|
|
2435
|
+
return `{ ${Object.entries(dict).map(([key, inner]) => {
|
|
2436
|
+
return `${key}${inner.meta.required ? "" : "?"}: ${inner.toString()}`;
|
|
2437
|
+
}).join(", ")} }`;
|
|
2438
|
+
});
|
|
2439
|
+
defineMethod("union", ["list"], ({ list }, inline) => {
|
|
2440
|
+
const result = list.map(({ toString: format }) => format()).join(" | ");
|
|
2441
|
+
return inline ? `(${result})` : result;
|
|
2442
|
+
});
|
|
2443
|
+
defineMethod("intersect", ["list"], ({ list }) => {
|
|
2444
|
+
return `${list.map((inner) => inner.toString(true)).join(" & ")}`;
|
|
2445
|
+
});
|
|
2446
|
+
defineMethod("transform", [
|
|
2447
|
+
"inner",
|
|
2448
|
+
"callback",
|
|
2449
|
+
"preserve"
|
|
2450
|
+
], ({ inner }, isInner) => inner.toString(isInner));
|
|
2451
|
+
//#endregion
|
|
2452
|
+
//#region lib/types/config.js
|
|
2453
|
+
/** Window-local timing accepted by the Host and browser keyboard service. */
|
|
2454
|
+
/** Validated deployment settings for fixed keyboard sequences. */
|
|
2455
|
+
const Config = Schema.object({ stopSequenceMs: Schema.natural().min(1).max(2147483646).default(500) });
|
|
2456
|
+
//#endregion
|
|
2457
|
+
//#region lib/types/client/index.js
|
|
2458
|
+
/** Browser command service, with one keyboard adapter per plugin lifetime. */
|
|
2459
|
+
/** Cordis keyboard provider; Desktop startup requires its native keyboard bridge. */
|
|
2460
|
+
var ShortcutsService = class extends _qilin_agent_kylin.Service {
|
|
2461
|
+
static inject = ["locale"];
|
|
2462
|
+
runtime;
|
|
2463
|
+
platform;
|
|
2464
|
+
catalog;
|
|
2465
|
+
config;
|
|
2466
|
+
fixedCatalog;
|
|
2467
|
+
stopSequenceMs;
|
|
2468
|
+
fixedListeners = /* @__PURE__ */ new Set();
|
|
2469
|
+
adapter;
|
|
2470
|
+
keyboard;
|
|
2471
|
+
active = true;
|
|
2472
|
+
connected = false;
|
|
2473
|
+
registry;
|
|
2474
|
+
constructor(ctx) {
|
|
2475
|
+
const environment = detectEnvironment(document, navigator);
|
|
2476
|
+
const keyboard = environment.runtime === "desktop" ? window.qilinDesktop?.keyboard : void 0;
|
|
2477
|
+
if (environment.runtime === "desktop" && keyboard === void 0) throw new Error("Desktop keyboard bridge unavailable");
|
|
2478
|
+
super(ctx, "shortcuts");
|
|
2479
|
+
this.keyboard = keyboard;
|
|
2480
|
+
this.runtime = environment.runtime;
|
|
2481
|
+
this.platform = environment.platform;
|
|
2482
|
+
const config = Config(globalThis.__QILIN_SHORTCUTS_CONFIG__ ?? {});
|
|
2483
|
+
this.stopSequenceMs = config.stopSequenceMs;
|
|
2484
|
+
this.registry = new ShortcutRegistry(this.runtime, this.platform, initialShortcutConfig());
|
|
2485
|
+
this.catalog = this.registry.catalog;
|
|
2486
|
+
this.config = this.registry.config;
|
|
2487
|
+
this.fixedCatalog = this.registry.fixedCatalog;
|
|
2488
|
+
const publish = (snapshot) => {
|
|
2489
|
+
if (this.active && this.connected && snapshot.sequence >= this.config.getSnapshot().sequence) this.registry.configure(snapshot);
|
|
2490
|
+
};
|
|
2491
|
+
const web = this.runtime === "web" ? webShortcutStorage(window, this.platform, publish) : void 0;
|
|
2492
|
+
this.adapter = web ?? desktopShortcutStorage(window);
|
|
2493
|
+
if (keyboard !== void 0) ctx.effect(() => installNativeKeyboard(window, keyboard, this.registry, () => this.config.getSnapshot(), () => {
|
|
2494
|
+
this.fixedInput({ type: "reset" });
|
|
2495
|
+
}), "shortcuts: native keyboard");
|
|
2496
|
+
ctx.effect(() => {
|
|
2497
|
+
const off = this.adapter?.subscribe(publish);
|
|
2498
|
+
return () => {
|
|
2499
|
+
this.active = false;
|
|
2500
|
+
off?.();
|
|
2501
|
+
web?.dispose();
|
|
2502
|
+
};
|
|
2503
|
+
}, "shortcuts: preferences");
|
|
2504
|
+
this.syncDefinitions();
|
|
2505
|
+
ctx.effect(() => {
|
|
2506
|
+
const off = installKeyboard(window, this.registry, (input) => {
|
|
2507
|
+
this.fixedInput(input);
|
|
2508
|
+
}, keyboard !== void 0 && (this.platform === "macos" || this.platform === "windows"));
|
|
2509
|
+
return () => {
|
|
2510
|
+
off();
|
|
2511
|
+
this.fixedListeners.clear();
|
|
2512
|
+
};
|
|
2513
|
+
}, "shortcuts: keyboard");
|
|
2514
|
+
ctx.effect(() => ctx.locale.subscribe(() => {
|
|
2515
|
+
this.registry.refreshLabels();
|
|
2516
|
+
}), "shortcuts: locale");
|
|
2517
|
+
}
|
|
2518
|
+
register(command) {
|
|
2519
|
+
const off = this.registry.register(command);
|
|
2520
|
+
this.syncDefinitions();
|
|
2521
|
+
return () => {
|
|
2522
|
+
off();
|
|
2523
|
+
this.syncDefinitions();
|
|
2524
|
+
};
|
|
2525
|
+
}
|
|
2526
|
+
registerFixed(command) {
|
|
2527
|
+
const off = this.registry.registerFixed(command);
|
|
2528
|
+
this.syncDefinitions();
|
|
2529
|
+
return () => {
|
|
2530
|
+
off();
|
|
2531
|
+
this.syncDefinitions();
|
|
2532
|
+
};
|
|
2533
|
+
}
|
|
2534
|
+
observeFixedInput(listener) {
|
|
2535
|
+
this.fixedListeners.add(listener);
|
|
2536
|
+
return () => {
|
|
2537
|
+
this.fixedListeners.delete(listener);
|
|
2538
|
+
};
|
|
2539
|
+
}
|
|
2540
|
+
fixedInput(input) {
|
|
2541
|
+
let consumed = false;
|
|
2542
|
+
for (const listener of [...this.fixedListeners]) {
|
|
2543
|
+
if (!this.fixedListeners.has(listener)) continue;
|
|
2544
|
+
try {
|
|
2545
|
+
listener(input.type === "reset" ? input : {
|
|
2546
|
+
...input,
|
|
2547
|
+
gesture: {
|
|
2548
|
+
...input.gesture,
|
|
2549
|
+
defaultPrevented: input.gesture.defaultPrevented || consumed
|
|
2550
|
+
},
|
|
2551
|
+
consume: () => {
|
|
2552
|
+
consumed = true;
|
|
2553
|
+
input.consume();
|
|
2554
|
+
}
|
|
2555
|
+
});
|
|
2556
|
+
} catch (error) {
|
|
2557
|
+
console.error("Fixed shortcut handler failed", error);
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
describeBinding(binding) {
|
|
2562
|
+
const normalized = binding === null ? null : normalizeBinding(binding, this.platform);
|
|
2563
|
+
return {
|
|
2564
|
+
binding: normalized,
|
|
2565
|
+
keys: presentBinding(normalized, this.platform).keys,
|
|
2566
|
+
issue: normalized === null ? null : bindingIssue(normalized, this.runtime, this.platform),
|
|
2567
|
+
conflicts: normalized === null ? [] : [...this.catalog.getSnapshot().filter((row) => row.binding !== null && overlappingBindings(row.binding, normalized)).map((row) => row.id), ...this.fixedCatalog.getSnapshot().filter((row) => row.bindings.some((binding) => overlappingBindings(binding, normalized))).map((row) => row.id)]
|
|
2568
|
+
};
|
|
2569
|
+
}
|
|
2570
|
+
syncDefinitions() {
|
|
2571
|
+
if (!this.active) return;
|
|
2572
|
+
if (this.adapter === void 0) {
|
|
2573
|
+
this.failRead();
|
|
2574
|
+
return;
|
|
2575
|
+
}
|
|
2576
|
+
this.adapter.get(this.registry.definitions()).then((snapshot) => {
|
|
2577
|
+
this.connected = true;
|
|
2578
|
+
if (this.active && snapshot.sequence >= this.config.getSnapshot().sequence) this.registry.configure(snapshot);
|
|
2579
|
+
}, () => {
|
|
2580
|
+
this.failRead();
|
|
2581
|
+
});
|
|
2582
|
+
}
|
|
2583
|
+
failRead() {
|
|
2584
|
+
if (this.active) this.registry.configure({
|
|
2585
|
+
...this.config.getSnapshot(),
|
|
2586
|
+
status: "unreadable",
|
|
2587
|
+
error: "read"
|
|
2588
|
+
});
|
|
2589
|
+
}
|
|
2590
|
+
/**
|
|
2591
|
+
* Persist one reviewed operation while retaining accepted bindings on failure.
|
|
2592
|
+
* @param args - edit and expected revision supplied by the editor.
|
|
2593
|
+
* @returns classified save outcome and accepted snapshot.
|
|
2594
|
+
*/
|
|
2595
|
+
async edit(...args) {
|
|
2596
|
+
if (this.adapter === void 0) return {
|
|
2597
|
+
status: "unreadable",
|
|
2598
|
+
snapshot: this.config.getSnapshot()
|
|
2599
|
+
};
|
|
2600
|
+
let result;
|
|
2601
|
+
try {
|
|
2602
|
+
result = await this.adapter.edit(...args);
|
|
2603
|
+
} catch (error) {
|
|
2604
|
+
if (this.active) console.error("Shortcut preference save failed", error);
|
|
2605
|
+
return {
|
|
2606
|
+
status: "write-failed",
|
|
2607
|
+
snapshot: this.config.getSnapshot()
|
|
2608
|
+
};
|
|
2609
|
+
}
|
|
2610
|
+
if (this.active && result.snapshot.sequence >= this.config.getSnapshot().sequence) this.registry.configure(result.snapshot);
|
|
2611
|
+
return result;
|
|
2612
|
+
}
|
|
2613
|
+
async recording(active) {
|
|
2614
|
+
if (this.adapter === void 0) throw new Error("Desktop shortcuts bridge unavailable");
|
|
2615
|
+
await this.adapter.recording(active);
|
|
2616
|
+
}
|
|
2617
|
+
async closeWindow() {
|
|
2618
|
+
if (this.keyboard === void 0) throw new Error("Desktop keyboard bridge unavailable");
|
|
2619
|
+
await this.keyboard.closeWindow(this.config.getSnapshot().revision);
|
|
2620
|
+
}
|
|
2621
|
+
};
|
|
2622
|
+
//#endregion
|
|
2623
|
+
module.exports = ShortcutsService;
|
|
2624
|
+
return module.exports;
|
|
2625
|
+
}
|
|
2626
|
+
});
|
|
2627
|
+
|
|
2628
|
+
//# sourceMappingURL=client.js.map
|