@inklu/keys 1.0.0
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.md +258 -0
- package/dist/attributes-lCPeoWII.d.ts +285 -0
- package/dist/attributes.d.ts +2 -0
- package/dist/attributes.js +72 -0
- package/dist/index-WDNLK0dX.d.ts +93 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/react.d.ts +77 -0
- package/dist/react.js +214 -0
- package/dist/src-B305icXC.js +981 -0
- package/package.json +98 -0
|
@@ -0,0 +1,981 @@
|
|
|
1
|
+
import { HotkeyRecorder, HotkeySequenceRecorder, detectPlatform, formatForDisplay, getHotkeyManager, getSequenceManager, normalizeHotkey, validateHotkey } from "@tanstack/hotkeys";
|
|
2
|
+
//#region src/bindings.ts
|
|
3
|
+
/** Comparing, validating and serializing hotkey bindings. */
|
|
4
|
+
/**
|
|
5
|
+
* Creates a sequence binding from ordered hotkey steps.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* keys: seq("G", "I")
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
function seq(...steps) {
|
|
13
|
+
return { sequence: steps };
|
|
14
|
+
}
|
|
15
|
+
/** Type guard checking if a binding is a sequence. */
|
|
16
|
+
function isSequence(binding) {
|
|
17
|
+
return typeof binding === "object" && binding !== null && "sequence" in binding;
|
|
18
|
+
}
|
|
19
|
+
/** Normalizes a binding input into an array of bindings. */
|
|
20
|
+
function toBindings(input) {
|
|
21
|
+
if (input == null) return [];
|
|
22
|
+
const candidates = Array.isArray(input) ? input : [input];
|
|
23
|
+
const bindings = [];
|
|
24
|
+
for (const candidate of candidates) {
|
|
25
|
+
if (candidate == null) continue;
|
|
26
|
+
if (isSequence(candidate)) {
|
|
27
|
+
if (candidate.sequence.length > 0) bindings.push({ sequence: [...candidate.sequence] });
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const trimmed = String(candidate).trim();
|
|
31
|
+
if (trimmed) bindings.push(trimmed);
|
|
32
|
+
}
|
|
33
|
+
return bindings;
|
|
34
|
+
}
|
|
35
|
+
/** Generates a normalized signature for binding comparison and conflict detection. */
|
|
36
|
+
function bindingSignature(binding, platform) {
|
|
37
|
+
if (isSequence(binding)) return `seq:${binding.sequence.map((step) => safeNormalize(step, platform)).join(" ")}`;
|
|
38
|
+
return `chord:${safeNormalize(binding, platform)}`;
|
|
39
|
+
}
|
|
40
|
+
function safeNormalize(hotkey, platform) {
|
|
41
|
+
try {
|
|
42
|
+
return normalizeHotkey(hotkey, platform);
|
|
43
|
+
} catch {
|
|
44
|
+
return hotkey;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Converts a binding to a JSON-serializable format. */
|
|
48
|
+
function serializeBinding(binding) {
|
|
49
|
+
return isSequence(binding) ? [...binding.sequence] : binding;
|
|
50
|
+
}
|
|
51
|
+
/** Deserializes a stored value into a valid binding, or `null` if invalid. */
|
|
52
|
+
function deserializeBinding(value) {
|
|
53
|
+
if (typeof value === "string") {
|
|
54
|
+
const trimmed = value.trim();
|
|
55
|
+
return trimmed && isBindingValid(trimmed) ? trimmed : null;
|
|
56
|
+
}
|
|
57
|
+
if (Array.isArray(value)) {
|
|
58
|
+
const steps = value.filter((step) => typeof step === "string" && step !== "");
|
|
59
|
+
if (steps.length === 0) return null;
|
|
60
|
+
const binding = steps.length === 1 ? steps[0] : { sequence: steps };
|
|
61
|
+
return isBindingValid(binding) ? binding : null;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
/** Validates a binding's syntax without logging warnings or errors. */
|
|
66
|
+
function validateBinding(binding) {
|
|
67
|
+
const steps = isSequence(binding) ? binding.sequence : [binding];
|
|
68
|
+
if (steps.length === 0) return {
|
|
69
|
+
valid: false,
|
|
70
|
+
errors: ["Sequence has no steps"],
|
|
71
|
+
warnings: []
|
|
72
|
+
};
|
|
73
|
+
const errors = [];
|
|
74
|
+
const warnings = [];
|
|
75
|
+
for (const step of steps) {
|
|
76
|
+
const result = validateHotkey(step);
|
|
77
|
+
errors.push(...result.errors);
|
|
78
|
+
warnings.push(...result.warnings);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
valid: errors.length === 0,
|
|
82
|
+
errors,
|
|
83
|
+
warnings
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** Checks whether a binding has valid syntax. */
|
|
87
|
+
function isBindingValid(binding) {
|
|
88
|
+
return validateBinding(binding).valid;
|
|
89
|
+
}
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/display.ts
|
|
92
|
+
/** Platform-aware hotkey display formatting and accessibility attributes. */
|
|
93
|
+
const TOKEN_SEPARATOR = "\0";
|
|
94
|
+
/** Formats a binding for display on the target platform (e.g. `⌘K` or `Ctrl+K`). */
|
|
95
|
+
function formatBinding(binding, options) {
|
|
96
|
+
if (isSequence(binding)) return binding.sequence.map((step) => formatForDisplay(step, options)).join(" ");
|
|
97
|
+
return formatForDisplay(binding, options);
|
|
98
|
+
}
|
|
99
|
+
/** Splits a binding into display tokens for rendering `<kbd>` elements. */
|
|
100
|
+
function bindingTokens(binding, options) {
|
|
101
|
+
if (isSequence(binding)) return binding.sequence.map((step) => formatForDisplay(step, options));
|
|
102
|
+
return formatForDisplay(binding, {
|
|
103
|
+
...options,
|
|
104
|
+
separatorToken: TOKEN_SEPARATOR
|
|
105
|
+
}).split(TOKEN_SEPARATOR).map((token) => token.trim()).filter((token) => token.length > 0);
|
|
106
|
+
}
|
|
107
|
+
/** Generates an `aria-keyshortcuts` attribute string for a binding. */
|
|
108
|
+
function ariaKeyShortcuts(binding, platform) {
|
|
109
|
+
if (isSequence(binding)) return "";
|
|
110
|
+
try {
|
|
111
|
+
return resolveMod(normalizeHotkey(binding, platform), platform);
|
|
112
|
+
} catch {
|
|
113
|
+
return "";
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function resolveMod(hotkey, platform) {
|
|
117
|
+
if (!hotkey.includes("Mod")) return hotkey;
|
|
118
|
+
const modifier = (platform ?? detectPlatform()) === "mac" ? "Meta" : "Control";
|
|
119
|
+
return hotkey.split("+").map((part) => part === "Mod" ? modifier : part).join("+");
|
|
120
|
+
}
|
|
121
|
+
//#endregion
|
|
122
|
+
//#region src/rebind.ts
|
|
123
|
+
/** Controller for recording and applying hotkey rebindings. */
|
|
124
|
+
const RECORDING_SCOPE = "inklu-keys:recording";
|
|
125
|
+
const NO_STEPS = [];
|
|
126
|
+
function toRecordedBinding(sequence) {
|
|
127
|
+
if (sequence.length === 0) return null;
|
|
128
|
+
if (sequence.length === 1) return sequence[0];
|
|
129
|
+
return { sequence: [...sequence] };
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Creates a rebinder instance for capturing and remapping a command's keys.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```ts
|
|
136
|
+
* const rebinder = createRebinder(commands, "palette.open", {
|
|
137
|
+
* onRecord: (binding, conflicts) => {
|
|
138
|
+
* if (conflicts.length) console.warn("Conflict with", conflicts[0].id);
|
|
139
|
+
* },
|
|
140
|
+
* });
|
|
141
|
+
* rebinder.start();
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
function createRebinder(registry, id, options = {}) {
|
|
145
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
146
|
+
let state = {
|
|
147
|
+
recording: false,
|
|
148
|
+
binding: null,
|
|
149
|
+
steps: NO_STEPS,
|
|
150
|
+
conflicts: []
|
|
151
|
+
};
|
|
152
|
+
let scope = null;
|
|
153
|
+
const notify = (next) => {
|
|
154
|
+
state = next;
|
|
155
|
+
for (const listener of listeners) listener();
|
|
156
|
+
};
|
|
157
|
+
const releaseScope = () => {
|
|
158
|
+
scope?.();
|
|
159
|
+
scope = null;
|
|
160
|
+
};
|
|
161
|
+
const record = (binding) => {
|
|
162
|
+
releaseScope();
|
|
163
|
+
const conflicts = registry.conflicts(binding, {
|
|
164
|
+
ignore: id,
|
|
165
|
+
scope: registry.get(id)?.scope
|
|
166
|
+
});
|
|
167
|
+
const blocked = options.rejectConflicts === true && conflicts.length > 0;
|
|
168
|
+
if (options.apply !== false && !blocked) {
|
|
169
|
+
if (options.add) registry.keymap.add(id, binding);
|
|
170
|
+
else registry.keymap.set(id, binding);
|
|
171
|
+
}
|
|
172
|
+
notify({
|
|
173
|
+
recording: false,
|
|
174
|
+
binding,
|
|
175
|
+
steps: NO_STEPS,
|
|
176
|
+
conflicts
|
|
177
|
+
});
|
|
178
|
+
options.onRecord?.(binding, conflicts);
|
|
179
|
+
};
|
|
180
|
+
const cancelled = () => {
|
|
181
|
+
releaseScope();
|
|
182
|
+
notify({
|
|
183
|
+
...state,
|
|
184
|
+
recording: false,
|
|
185
|
+
steps: NO_STEPS
|
|
186
|
+
});
|
|
187
|
+
options.onCancel?.();
|
|
188
|
+
};
|
|
189
|
+
const cleared = () => {
|
|
190
|
+
releaseScope();
|
|
191
|
+
notify({
|
|
192
|
+
recording: false,
|
|
193
|
+
binding: null,
|
|
194
|
+
steps: NO_STEPS,
|
|
195
|
+
conflicts: []
|
|
196
|
+
});
|
|
197
|
+
options.onClear?.();
|
|
198
|
+
};
|
|
199
|
+
const recorder = options.mode === "sequence" ? new HotkeySequenceRecorder({
|
|
200
|
+
ignoreInputs: options.ignoreInputs ?? true,
|
|
201
|
+
idleTimeoutMs: options.idleTimeout,
|
|
202
|
+
onRecord: (sequence) => {
|
|
203
|
+
const binding = toRecordedBinding(sequence);
|
|
204
|
+
if (binding === null) return;
|
|
205
|
+
record(binding);
|
|
206
|
+
},
|
|
207
|
+
onCancel: cancelled,
|
|
208
|
+
onClear: cleared
|
|
209
|
+
}) : new HotkeyRecorder({
|
|
210
|
+
ignoreInputs: options.ignoreInputs ?? true,
|
|
211
|
+
onRecord: (hotkey) => record(hotkey),
|
|
212
|
+
onCancel: cancelled,
|
|
213
|
+
onClear: cleared
|
|
214
|
+
});
|
|
215
|
+
const stepsSubscription = recorder instanceof HotkeySequenceRecorder ? recorder.store.subscribe(() => {
|
|
216
|
+
if (!state.recording) return;
|
|
217
|
+
const steps = recorder.store.state.steps;
|
|
218
|
+
if (steps.length === state.steps.length) return;
|
|
219
|
+
notify({
|
|
220
|
+
...state,
|
|
221
|
+
steps: [...steps]
|
|
222
|
+
});
|
|
223
|
+
}) : null;
|
|
224
|
+
return {
|
|
225
|
+
start: () => {
|
|
226
|
+
if (state.recording) return;
|
|
227
|
+
scope ??= registry.pushScope(RECORDING_SCOPE, { exclusive: true });
|
|
228
|
+
recorder.start();
|
|
229
|
+
notify({
|
|
230
|
+
recording: true,
|
|
231
|
+
binding: null,
|
|
232
|
+
steps: NO_STEPS,
|
|
233
|
+
conflicts: []
|
|
234
|
+
});
|
|
235
|
+
},
|
|
236
|
+
stop: () => {
|
|
237
|
+
if (!state.recording) return;
|
|
238
|
+
recorder.stop();
|
|
239
|
+
releaseScope();
|
|
240
|
+
notify({
|
|
241
|
+
...state,
|
|
242
|
+
recording: false,
|
|
243
|
+
steps: NO_STEPS
|
|
244
|
+
});
|
|
245
|
+
},
|
|
246
|
+
cancel: () => {
|
|
247
|
+
if (!state.recording) return;
|
|
248
|
+
recorder.cancel();
|
|
249
|
+
},
|
|
250
|
+
commit: () => {
|
|
251
|
+
if (!state.recording || !(recorder instanceof HotkeySequenceRecorder)) return;
|
|
252
|
+
recorder.commit();
|
|
253
|
+
},
|
|
254
|
+
get state() {
|
|
255
|
+
return state;
|
|
256
|
+
},
|
|
257
|
+
subscribe: (listener) => {
|
|
258
|
+
listeners.add(listener);
|
|
259
|
+
return () => {
|
|
260
|
+
listeners.delete(listener);
|
|
261
|
+
};
|
|
262
|
+
},
|
|
263
|
+
destroy: () => {
|
|
264
|
+
releaseScope();
|
|
265
|
+
stepsSubscription?.unsubscribe();
|
|
266
|
+
recorder.destroy();
|
|
267
|
+
listeners.clear();
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
//#endregion
|
|
272
|
+
//#region src/binder.ts
|
|
273
|
+
/** Bridge connecting commands to underlying hotkey event listeners. */
|
|
274
|
+
var Binder = class {
|
|
275
|
+
#host;
|
|
276
|
+
#target;
|
|
277
|
+
#bound = /* @__PURE__ */ new Map();
|
|
278
|
+
constructor(host, target) {
|
|
279
|
+
this.#host = host;
|
|
280
|
+
this.#target = target;
|
|
281
|
+
}
|
|
282
|
+
/** Syncs hotkey registrations for a command with its current bindings. */
|
|
283
|
+
sync(id) {
|
|
284
|
+
const definition = this.#host.getDefinition(id);
|
|
285
|
+
if (!definition) {
|
|
286
|
+
this.release(id);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const existing = this.#bound.get(id) ?? [];
|
|
290
|
+
const reusable = new Map(existing.map((bound) => [bound.signature, bound]));
|
|
291
|
+
const next = [];
|
|
292
|
+
const enabled = this.#host.isReachable(id);
|
|
293
|
+
for (const binding of this.#host.getBindings(id)) {
|
|
294
|
+
const signature = bindingSignature(binding, this.#host.platform());
|
|
295
|
+
if (next.some((bound) => bound.signature === signature)) continue;
|
|
296
|
+
const kept = reusable.get(signature);
|
|
297
|
+
if (kept) {
|
|
298
|
+
reusable.delete(signature);
|
|
299
|
+
kept.setEnabled(enabled);
|
|
300
|
+
next.push(kept);
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
const bound = this.#register(id, binding, signature, definition, enabled);
|
|
304
|
+
if (bound) next.push(bound);
|
|
305
|
+
}
|
|
306
|
+
for (const stale of reusable.values()) stale.unregister();
|
|
307
|
+
if (next.length === 0) this.#bound.delete(id);
|
|
308
|
+
else this.#bound.set(id, next);
|
|
309
|
+
}
|
|
310
|
+
/** Updates enabled state for all active registrations without re-registering. */
|
|
311
|
+
refreshEnabled() {
|
|
312
|
+
for (const [id, bindings] of this.#bound) {
|
|
313
|
+
const enabled = this.#host.isReachable(id);
|
|
314
|
+
for (const bound of bindings) bound.setEnabled(enabled);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/** Returns all registered sequences with their manager registration IDs. */
|
|
318
|
+
sequences() {
|
|
319
|
+
const entries = [];
|
|
320
|
+
for (const [id, bindings] of this.#bound) for (const bound of bindings) {
|
|
321
|
+
if (bound.sequenceId === null || !isSequence(bound.binding)) continue;
|
|
322
|
+
entries.push({
|
|
323
|
+
id,
|
|
324
|
+
binding: bound.binding,
|
|
325
|
+
registrationId: bound.sequenceId
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
return entries;
|
|
329
|
+
}
|
|
330
|
+
/** Removes all registrations for a specific command. */
|
|
331
|
+
release(id) {
|
|
332
|
+
const bindings = this.#bound.get(id);
|
|
333
|
+
if (!bindings) return;
|
|
334
|
+
for (const bound of bindings) bound.unregister();
|
|
335
|
+
this.#bound.delete(id);
|
|
336
|
+
}
|
|
337
|
+
/** Removes all registrations. */
|
|
338
|
+
destroy() {
|
|
339
|
+
for (const bindings of this.#bound.values()) for (const bound of bindings) bound.unregister();
|
|
340
|
+
this.#bound.clear();
|
|
341
|
+
}
|
|
342
|
+
#register(id, binding, signature, definition, enabled) {
|
|
343
|
+
const callback = (event) => this.#handle(id, binding, event);
|
|
344
|
+
const options = {
|
|
345
|
+
preventDefault: false,
|
|
346
|
+
stopPropagation: false,
|
|
347
|
+
conflictBehavior: "allow",
|
|
348
|
+
enabled,
|
|
349
|
+
ignoreInputs: definition.ignoreInputs,
|
|
350
|
+
platform: this.#host.platform(),
|
|
351
|
+
target: this.#target,
|
|
352
|
+
meta: { name: definition.title }
|
|
353
|
+
};
|
|
354
|
+
try {
|
|
355
|
+
const handle = isSequence(binding) ? getSequenceManager().register([...binding.sequence], callback, {
|
|
356
|
+
...options,
|
|
357
|
+
timeout: definition.sequenceTimeout
|
|
358
|
+
}) : getHotkeyManager().register(binding, callback, {
|
|
359
|
+
...options,
|
|
360
|
+
requireReset: definition.requireReset === true
|
|
361
|
+
});
|
|
362
|
+
return {
|
|
363
|
+
signature,
|
|
364
|
+
binding,
|
|
365
|
+
sequenceId: isSequence(binding) ? handle.id : null,
|
|
366
|
+
setEnabled: (value) => handle.setOptions({ enabled: value }),
|
|
367
|
+
unregister: () => handle.unregister()
|
|
368
|
+
};
|
|
369
|
+
} catch {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
#handle(id, binding, event) {
|
|
374
|
+
if (!this.#host.canRun(id)) return;
|
|
375
|
+
const definition = this.#host.getDefinition(id);
|
|
376
|
+
if (!definition) return;
|
|
377
|
+
if (definition.preventDefault !== false) event.preventDefault();
|
|
378
|
+
this.#host.execute(id, {
|
|
379
|
+
source: "keyboard",
|
|
380
|
+
event,
|
|
381
|
+
binding
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
//#endregion
|
|
386
|
+
//#region src/keymap.ts
|
|
387
|
+
/** Keymap managing default bindings and user overrides. */
|
|
388
|
+
const DEFAULT_STORAGE_KEY = "inklu.keys";
|
|
389
|
+
var Keymap = class {
|
|
390
|
+
#defaults = /* @__PURE__ */ new Map();
|
|
391
|
+
#overrides = /* @__PURE__ */ new Map();
|
|
392
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
393
|
+
#persistence;
|
|
394
|
+
constructor(persistence) {
|
|
395
|
+
this.#persistence = persistence;
|
|
396
|
+
}
|
|
397
|
+
/** Sets a command's default bindings. */
|
|
398
|
+
setDefault(loose, input) {
|
|
399
|
+
const id = loose;
|
|
400
|
+
const bindings = toBindings(input);
|
|
401
|
+
if (bindings.length === 0) this.#defaults.delete(id);
|
|
402
|
+
else this.#defaults.set(id, bindings);
|
|
403
|
+
}
|
|
404
|
+
/** Removes a command's default bindings, leaving overrides intact. */
|
|
405
|
+
clearDefault(loose) {
|
|
406
|
+
this.#defaults.delete(loose);
|
|
407
|
+
}
|
|
408
|
+
/** Checks whether a command has an active user override. */
|
|
409
|
+
isOverridden(loose) {
|
|
410
|
+
return this.#overrides.has(loose);
|
|
411
|
+
}
|
|
412
|
+
get(loose) {
|
|
413
|
+
const id = loose;
|
|
414
|
+
return this.#overrides.get(id) ?? this.#defaults.get(id) ?? [];
|
|
415
|
+
}
|
|
416
|
+
/** Sets a user binding override for a command. Passing `null` unbinds it. */
|
|
417
|
+
set(loose, input) {
|
|
418
|
+
this.#overrides.set(loose, input === null ? [] : toBindings(input));
|
|
419
|
+
this.#changed();
|
|
420
|
+
}
|
|
421
|
+
/** Adds an additional binding override for a command. */
|
|
422
|
+
add(loose, input) {
|
|
423
|
+
const id = loose;
|
|
424
|
+
this.#overrides.set(id, [...this.get(id), ...toBindings(input)]);
|
|
425
|
+
this.#changed();
|
|
426
|
+
}
|
|
427
|
+
/** Resets user overrides to defaults for one command or all commands. */
|
|
428
|
+
reset(loose) {
|
|
429
|
+
const id = loose;
|
|
430
|
+
if (id === void 0) {
|
|
431
|
+
if (this.#overrides.size === 0) return;
|
|
432
|
+
this.#overrides.clear();
|
|
433
|
+
} else if (!this.#overrides.delete(id)) return;
|
|
434
|
+
this.#changed();
|
|
435
|
+
}
|
|
436
|
+
/** Exports user overrides as a serialized keymap object. */
|
|
437
|
+
export() {
|
|
438
|
+
const data = {};
|
|
439
|
+
for (const [id, bindings] of this.#overrides) data[id] = bindings.map(serializeBinding);
|
|
440
|
+
return data;
|
|
441
|
+
}
|
|
442
|
+
/** Imports serialized user overrides and saves to storage. */
|
|
443
|
+
import(data) {
|
|
444
|
+
this.#read(data);
|
|
445
|
+
this.#changed();
|
|
446
|
+
}
|
|
447
|
+
/** Loads persisted overrides from storage. */
|
|
448
|
+
load() {
|
|
449
|
+
const storage = this.#storage();
|
|
450
|
+
if (!storage) return;
|
|
451
|
+
try {
|
|
452
|
+
const raw = storage.getItem(this.#storageKey());
|
|
453
|
+
if (!raw) return;
|
|
454
|
+
this.#read(JSON.parse(raw));
|
|
455
|
+
for (const listener of this.#listeners) listener();
|
|
456
|
+
} catch {}
|
|
457
|
+
}
|
|
458
|
+
/** Subscribes to keymap changes. Returns an unsubscribe function. */
|
|
459
|
+
subscribe(listener) {
|
|
460
|
+
this.#listeners.add(listener);
|
|
461
|
+
return () => {
|
|
462
|
+
this.#listeners.delete(listener);
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
#read(data) {
|
|
466
|
+
if (typeof data !== "object" || data === null) return;
|
|
467
|
+
this.#overrides.clear();
|
|
468
|
+
for (const [id, value] of Object.entries(data)) {
|
|
469
|
+
if (!Array.isArray(value)) continue;
|
|
470
|
+
const bindings = value.map(deserializeBinding).filter((binding) => binding !== null);
|
|
471
|
+
this.#overrides.set(id, bindings);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
#changed() {
|
|
475
|
+
this.#persist();
|
|
476
|
+
for (const listener of this.#listeners) listener();
|
|
477
|
+
}
|
|
478
|
+
#persist() {
|
|
479
|
+
const storage = this.#storage();
|
|
480
|
+
if (!storage) return;
|
|
481
|
+
try {
|
|
482
|
+
const data = this.export();
|
|
483
|
+
if (Object.keys(data).length === 0) storage.removeItem(this.#storageKey());
|
|
484
|
+
else storage.setItem(this.#storageKey(), JSON.stringify(data));
|
|
485
|
+
} catch {}
|
|
486
|
+
}
|
|
487
|
+
#storageKey() {
|
|
488
|
+
return this.#persistence?.key ?? DEFAULT_STORAGE_KEY;
|
|
489
|
+
}
|
|
490
|
+
#storage() {
|
|
491
|
+
try {
|
|
492
|
+
return this.#persistence?.storage() ?? null;
|
|
493
|
+
} catch {
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
//#endregion
|
|
499
|
+
//#region src/scopes.ts
|
|
500
|
+
/** Wildcard scope identifier for commands that remain active across exclusive scopes. */
|
|
501
|
+
const ALWAYS_SCOPE = "*";
|
|
502
|
+
/** Stack managing active scopes and command reachability. */
|
|
503
|
+
var ScopeStack = class {
|
|
504
|
+
#entries = [];
|
|
505
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
506
|
+
/** Activates a scope until the returned disposal function is called. */
|
|
507
|
+
push(name, options = {}) {
|
|
508
|
+
const entry = {
|
|
509
|
+
name,
|
|
510
|
+
exclusive: options.exclusive === true
|
|
511
|
+
};
|
|
512
|
+
this.#entries.push(entry);
|
|
513
|
+
this.#notify();
|
|
514
|
+
let released = false;
|
|
515
|
+
return () => {
|
|
516
|
+
if (released) return;
|
|
517
|
+
released = true;
|
|
518
|
+
const index = this.#entries.indexOf(entry);
|
|
519
|
+
if (index !== -1) this.#entries.splice(index, 1);
|
|
520
|
+
this.#notify();
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
/** Checks whether commands in the specified scope can execute given the current scope stack. */
|
|
524
|
+
isActive(scope) {
|
|
525
|
+
if (scope === "*") return true;
|
|
526
|
+
const barrier = this.#barrierIndex();
|
|
527
|
+
if (barrier === -1) return scope === void 0 || this.#entries.some((entry) => entry.name === scope);
|
|
528
|
+
if (scope === void 0) return false;
|
|
529
|
+
for (let index = barrier; index < this.#entries.length; index++) if (this.#entries[index]?.name === scope) return true;
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
/** Checks whether an exclusive scope is active. */
|
|
533
|
+
hasBarrier() {
|
|
534
|
+
return this.#barrierIndex() !== -1;
|
|
535
|
+
}
|
|
536
|
+
/** Clears all active scopes. */
|
|
537
|
+
clear() {
|
|
538
|
+
if (this.#entries.length === 0) return;
|
|
539
|
+
this.#entries = [];
|
|
540
|
+
this.#notify();
|
|
541
|
+
}
|
|
542
|
+
/** Subscribes to scope stack changes. Returns an unsubscribe function. */
|
|
543
|
+
subscribe(listener) {
|
|
544
|
+
this.#listeners.add(listener);
|
|
545
|
+
return () => {
|
|
546
|
+
this.#listeners.delete(listener);
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
#barrierIndex() {
|
|
550
|
+
for (let index = this.#entries.length - 1; index >= 0; index--) if (this.#entries[index]?.exclusive) return index;
|
|
551
|
+
return -1;
|
|
552
|
+
}
|
|
553
|
+
#notify() {
|
|
554
|
+
for (const listener of this.#listeners) listener();
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
//#endregion
|
|
558
|
+
//#region src/registry.ts
|
|
559
|
+
/** Command registry managing command definitions, keymaps, scopes, and execution. */
|
|
560
|
+
const EMPTY_PENDING = [];
|
|
561
|
+
const SSR_PLATFORM = "windows";
|
|
562
|
+
var CommandRegistry = class {
|
|
563
|
+
#stacks = /* @__PURE__ */ new Map();
|
|
564
|
+
#order = [];
|
|
565
|
+
#enabled = /* @__PURE__ */ new Map();
|
|
566
|
+
#keymap;
|
|
567
|
+
#scopes = new ScopeStack();
|
|
568
|
+
#binder;
|
|
569
|
+
#options;
|
|
570
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
571
|
+
#pendingListeners = /* @__PURE__ */ new Set();
|
|
572
|
+
#pending = EMPTY_PENDING;
|
|
573
|
+
#pendingSubscription = null;
|
|
574
|
+
#snapshot = null;
|
|
575
|
+
#index = null;
|
|
576
|
+
#mounts = 0;
|
|
577
|
+
#hydrated = false;
|
|
578
|
+
#barrier = false;
|
|
579
|
+
constructor(definitions, options = {}) {
|
|
580
|
+
this.#options = options;
|
|
581
|
+
this.#keymap = new Keymap(options.persist);
|
|
582
|
+
this.#binder = new Binder({
|
|
583
|
+
getDefinition: (id) => this.#definition(id),
|
|
584
|
+
getBindings: (id) => this.#keymap.get(id),
|
|
585
|
+
canRun: (id) => this.canRun(id),
|
|
586
|
+
isReachable: (id) => this.#isReachable(id),
|
|
587
|
+
platform: () => this.#platform(),
|
|
588
|
+
execute: (id, context) => {
|
|
589
|
+
this.#execute(id, context);
|
|
590
|
+
}
|
|
591
|
+
}, options.target);
|
|
592
|
+
for (const [id, definition] of Object.entries(definitions)) this.#push(id, definition);
|
|
593
|
+
this.#keymap.load();
|
|
594
|
+
this.#keymap.subscribe(() => this.#onKeymapChange());
|
|
595
|
+
this.#scopes.subscribe(() => this.#onScopeChange());
|
|
596
|
+
this.#audit();
|
|
597
|
+
}
|
|
598
|
+
get keymap() {
|
|
599
|
+
return this.#keymap;
|
|
600
|
+
}
|
|
601
|
+
get(id) {
|
|
602
|
+
this.#snapshotList();
|
|
603
|
+
return this.#index?.get(id);
|
|
604
|
+
}
|
|
605
|
+
list() {
|
|
606
|
+
return this.#snapshotList();
|
|
607
|
+
}
|
|
608
|
+
run(id, options = {}) {
|
|
609
|
+
if (!this.canRun(id)) return false;
|
|
610
|
+
this.#execute(id, {
|
|
611
|
+
source: options.source ?? "api",
|
|
612
|
+
event: options.event
|
|
613
|
+
});
|
|
614
|
+
return true;
|
|
615
|
+
}
|
|
616
|
+
canRun(id) {
|
|
617
|
+
return this.whyNotRun(id) === null;
|
|
618
|
+
}
|
|
619
|
+
whyNotRun(loose) {
|
|
620
|
+
const id = loose;
|
|
621
|
+
const definition = this.#definition(id);
|
|
622
|
+
if (!definition) return "unknown";
|
|
623
|
+
if (!(this.#enabled.get(id) ?? false)) return "disabled";
|
|
624
|
+
if (!this.#scopes.isActive(definition.scope)) return "out-of-scope";
|
|
625
|
+
if (!definition.when) return null;
|
|
626
|
+
try {
|
|
627
|
+
return definition.when() === false ? "guarded" : null;
|
|
628
|
+
} catch (error) {
|
|
629
|
+
this.#reportError(error, {
|
|
630
|
+
id,
|
|
631
|
+
source: "api",
|
|
632
|
+
commands: this
|
|
633
|
+
});
|
|
634
|
+
return "guarded";
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
setEnabled(loose, enabled) {
|
|
638
|
+
const id = loose;
|
|
639
|
+
if (!this.#stacks.has(id)) return;
|
|
640
|
+
if (this.#enabled.get(id) === enabled) return;
|
|
641
|
+
this.#enabled.set(id, enabled);
|
|
642
|
+
this.#binder.refreshEnabled();
|
|
643
|
+
this.#invalidate();
|
|
644
|
+
}
|
|
645
|
+
add(loose, definition) {
|
|
646
|
+
const id = loose;
|
|
647
|
+
const entry = this.#push(id, definition);
|
|
648
|
+
if (this.#mounts > 0) this.#binder.sync(id);
|
|
649
|
+
this.#invalidate();
|
|
650
|
+
this.#auditAdded(id);
|
|
651
|
+
return {
|
|
652
|
+
run: (options) => this.run(id, options),
|
|
653
|
+
dispose: () => this.#pop(id, entry)
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
remove(loose) {
|
|
657
|
+
const id = loose;
|
|
658
|
+
if (!this.#stacks.has(id)) return;
|
|
659
|
+
this.#drop(id);
|
|
660
|
+
this.#invalidate();
|
|
661
|
+
}
|
|
662
|
+
pushScope(name, options) {
|
|
663
|
+
return this.#scopes.push(name, options);
|
|
664
|
+
}
|
|
665
|
+
conflicts(binding, options = {}) {
|
|
666
|
+
const platform = this.#platform();
|
|
667
|
+
const signatures = new Set(toBindings(binding).map((entry) => bindingSignature(entry, platform)));
|
|
668
|
+
const conflicts = [];
|
|
669
|
+
for (const id of this.#order) {
|
|
670
|
+
if (id === options.ignore) continue;
|
|
671
|
+
const scope = this.#definition(id)?.scope;
|
|
672
|
+
if (!scopesOverlap(scope, options.scope)) continue;
|
|
673
|
+
for (const existing of this.#keymap.get(id)) {
|
|
674
|
+
if (!signatures.has(bindingSignature(existing, platform))) continue;
|
|
675
|
+
const display = formatBinding(existing, this.#displayOptions());
|
|
676
|
+
conflicts.push({
|
|
677
|
+
id,
|
|
678
|
+
binding: existing,
|
|
679
|
+
display,
|
|
680
|
+
scope
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return conflicts;
|
|
685
|
+
}
|
|
686
|
+
display(id) {
|
|
687
|
+
const binding = this.#keymap.get(id)[0];
|
|
688
|
+
return binding ? formatBinding(binding, this.#displayOptions()) : "";
|
|
689
|
+
}
|
|
690
|
+
tokens(id) {
|
|
691
|
+
const binding = this.#keymap.get(id)[0];
|
|
692
|
+
return binding ? bindingTokens(binding, this.#displayOptions()) : [];
|
|
693
|
+
}
|
|
694
|
+
mount() {
|
|
695
|
+
this.#mounts++;
|
|
696
|
+
if (this.#mounts === 1) {
|
|
697
|
+
this.#hydrated = true;
|
|
698
|
+
for (const id of this.#order) this.#binder.sync(id);
|
|
699
|
+
this.#invalidate();
|
|
700
|
+
}
|
|
701
|
+
let released = false;
|
|
702
|
+
return () => {
|
|
703
|
+
if (released) return;
|
|
704
|
+
released = true;
|
|
705
|
+
if (this.#mounts === 0) return;
|
|
706
|
+
this.#mounts--;
|
|
707
|
+
if (this.#mounts === 0) this.#binder.destroy();
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
pending() {
|
|
711
|
+
return this.#pending;
|
|
712
|
+
}
|
|
713
|
+
subscribePending(listener) {
|
|
714
|
+
this.#pendingListeners.add(listener);
|
|
715
|
+
if (this.#pendingListeners.size === 1) this.#watchPending();
|
|
716
|
+
return () => {
|
|
717
|
+
this.#pendingListeners.delete(listener);
|
|
718
|
+
if (this.#pendingListeners.size > 0) return;
|
|
719
|
+
this.#pendingSubscription?.unsubscribe();
|
|
720
|
+
this.#pendingSubscription = null;
|
|
721
|
+
this.#pending = EMPTY_PENDING;
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
subscribe(listener) {
|
|
725
|
+
this.#listeners.add(listener);
|
|
726
|
+
return () => {
|
|
727
|
+
this.#listeners.delete(listener);
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
destroy() {
|
|
731
|
+
this.#mounts = 0;
|
|
732
|
+
this.#binder.destroy();
|
|
733
|
+
this.#stacks.clear();
|
|
734
|
+
this.#order = [];
|
|
735
|
+
this.#enabled.clear();
|
|
736
|
+
this.#scopes.clear();
|
|
737
|
+
this.#listeners.clear();
|
|
738
|
+
this.#pendingListeners.clear();
|
|
739
|
+
this.#pendingSubscription?.unsubscribe();
|
|
740
|
+
this.#pendingSubscription = null;
|
|
741
|
+
this.#pending = EMPTY_PENDING;
|
|
742
|
+
this.#invalidate();
|
|
743
|
+
}
|
|
744
|
+
#onScopeChange() {
|
|
745
|
+
const barrier = this.#scopes.hasBarrier();
|
|
746
|
+
this.#binder.refreshEnabled();
|
|
747
|
+
if (barrier && !this.#barrier) getSequenceManager().resetAll();
|
|
748
|
+
this.#barrier = barrier;
|
|
749
|
+
this.#invalidate();
|
|
750
|
+
}
|
|
751
|
+
#watchPending() {
|
|
752
|
+
const store = getSequenceManager().registrations;
|
|
753
|
+
const read = () => {
|
|
754
|
+
const next = this.#readPending(store.state);
|
|
755
|
+
if (samePending(next, this.#pending)) return;
|
|
756
|
+
this.#pending = next;
|
|
757
|
+
for (const listener of this.#pendingListeners) listener();
|
|
758
|
+
};
|
|
759
|
+
this.#pendingSubscription = store.subscribe(read);
|
|
760
|
+
read();
|
|
761
|
+
}
|
|
762
|
+
#readPending(state) {
|
|
763
|
+
const options = this.#displayOptions();
|
|
764
|
+
const pending = [];
|
|
765
|
+
for (const { id, binding, registrationId } of this.#binder.sequences()) {
|
|
766
|
+
const matched = state.get(registrationId)?.matchedStepCount ?? 0;
|
|
767
|
+
if (matched === 0 || matched >= binding.sequence.length) continue;
|
|
768
|
+
pending.push({
|
|
769
|
+
id,
|
|
770
|
+
binding,
|
|
771
|
+
matched,
|
|
772
|
+
total: binding.sequence.length,
|
|
773
|
+
remaining: binding.sequence.slice(matched).map((step) => formatBinding(step, options))
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
return pending.length === 0 ? EMPTY_PENDING : pending;
|
|
777
|
+
}
|
|
778
|
+
#definition(id) {
|
|
779
|
+
const stack = this.#stacks.get(id);
|
|
780
|
+
return stack?.[stack.length - 1]?.definition;
|
|
781
|
+
}
|
|
782
|
+
#push(id, definition) {
|
|
783
|
+
let stack = this.#stacks.get(id);
|
|
784
|
+
if (!stack) {
|
|
785
|
+
stack = [];
|
|
786
|
+
this.#stacks.set(id, stack);
|
|
787
|
+
this.#order.push(id);
|
|
788
|
+
}
|
|
789
|
+
const entry = { definition };
|
|
790
|
+
stack.push(entry);
|
|
791
|
+
this.#applyTop(id);
|
|
792
|
+
return entry;
|
|
793
|
+
}
|
|
794
|
+
#pop(id, entry) {
|
|
795
|
+
const stack = this.#stacks.get(id);
|
|
796
|
+
if (!stack) return;
|
|
797
|
+
const index = stack.indexOf(entry);
|
|
798
|
+
if (index === -1) return;
|
|
799
|
+
stack.splice(index, 1);
|
|
800
|
+
if (stack.length === 0) this.#drop(id);
|
|
801
|
+
else {
|
|
802
|
+
this.#applyTop(id);
|
|
803
|
+
if (this.#mounts > 0) this.#binder.sync(id);
|
|
804
|
+
}
|
|
805
|
+
this.#invalidate();
|
|
806
|
+
}
|
|
807
|
+
#applyTop(id) {
|
|
808
|
+
const definition = this.#definition(id);
|
|
809
|
+
if (!definition) return;
|
|
810
|
+
this.#enabled.set(id, definition.enabled !== false);
|
|
811
|
+
this.#keymap.setDefault(id, definition.keys);
|
|
812
|
+
}
|
|
813
|
+
#drop(id) {
|
|
814
|
+
this.#stacks.delete(id);
|
|
815
|
+
const index = this.#order.indexOf(id);
|
|
816
|
+
if (index !== -1) this.#order.splice(index, 1);
|
|
817
|
+
this.#enabled.delete(id);
|
|
818
|
+
this.#keymap.clearDefault(id);
|
|
819
|
+
this.#binder.release(id);
|
|
820
|
+
}
|
|
821
|
+
#isReachable(id) {
|
|
822
|
+
if (!(this.#enabled.get(id) ?? false)) return false;
|
|
823
|
+
return this.#scopes.isActive(this.#definition(id)?.scope);
|
|
824
|
+
}
|
|
825
|
+
#execute(id, context) {
|
|
826
|
+
const definition = this.#definition(id);
|
|
827
|
+
if (!definition) return;
|
|
828
|
+
const full = {
|
|
829
|
+
id,
|
|
830
|
+
...context,
|
|
831
|
+
commands: this
|
|
832
|
+
};
|
|
833
|
+
if (this.#options.onRun) try {
|
|
834
|
+
this.#options.onRun(full);
|
|
835
|
+
} catch (error) {
|
|
836
|
+
this.#reportError(error, full);
|
|
837
|
+
}
|
|
838
|
+
try {
|
|
839
|
+
const result = definition.run(full);
|
|
840
|
+
if (result instanceof Promise) result.catch((error) => this.#reportError(error, full));
|
|
841
|
+
} catch (error) {
|
|
842
|
+
this.#reportError(error, full);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
#reportError(error, context) {
|
|
846
|
+
if (this.#options.onError) {
|
|
847
|
+
this.#options.onError(error, context);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
console.error(`[@inklu/keys] command "${context.id}" failed`, error);
|
|
851
|
+
}
|
|
852
|
+
#onKeymapChange() {
|
|
853
|
+
if (this.#mounts > 0) for (const id of this.#order) this.#binder.sync(id);
|
|
854
|
+
this.#invalidate();
|
|
855
|
+
}
|
|
856
|
+
#platform() {
|
|
857
|
+
return this.#options.platform ?? (this.#hydrated ? detectPlatform() : SSR_PLATFORM);
|
|
858
|
+
}
|
|
859
|
+
#displayOptions() {
|
|
860
|
+
return {
|
|
861
|
+
platform: this.#platform(),
|
|
862
|
+
...this.#options.display
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
#snapshotList() {
|
|
866
|
+
if (this.#snapshot) return this.#snapshot;
|
|
867
|
+
const platform = this.#platform();
|
|
868
|
+
const options = this.#displayOptions();
|
|
869
|
+
const commands = [];
|
|
870
|
+
const index = /* @__PURE__ */ new Map();
|
|
871
|
+
for (const id of this.#order) {
|
|
872
|
+
const definition = this.#definition(id);
|
|
873
|
+
if (!definition) continue;
|
|
874
|
+
const bindings = [...this.#keymap.get(id)];
|
|
875
|
+
const command = {
|
|
876
|
+
id,
|
|
877
|
+
title: definition.title,
|
|
878
|
+
scope: definition.scope,
|
|
879
|
+
bindings,
|
|
880
|
+
display: bindings.map((binding) => formatBinding(binding, options)),
|
|
881
|
+
tokens: bindings.map((binding) => bindingTokens(binding, options)),
|
|
882
|
+
aria: bindings.map((binding) => ariaKeyShortcuts(binding, platform)).filter((value) => value.length > 0).join(" "),
|
|
883
|
+
enabled: this.#enabled.get(id) ?? false,
|
|
884
|
+
reachable: this.#isReachable(id),
|
|
885
|
+
overridden: this.#keymap.isOverridden(id)
|
|
886
|
+
};
|
|
887
|
+
commands.push(command);
|
|
888
|
+
index.set(id, command);
|
|
889
|
+
}
|
|
890
|
+
this.#snapshot = commands;
|
|
891
|
+
this.#index = index;
|
|
892
|
+
return commands;
|
|
893
|
+
}
|
|
894
|
+
#invalidate() {
|
|
895
|
+
this.#snapshot = null;
|
|
896
|
+
this.#index = null;
|
|
897
|
+
for (const listener of this.#listeners) listener();
|
|
898
|
+
}
|
|
899
|
+
#warnAboutConflicts() {
|
|
900
|
+
const seen = /* @__PURE__ */ new Map();
|
|
901
|
+
const collisions = [];
|
|
902
|
+
for (const id of this.#order) {
|
|
903
|
+
const scope = this.#definition(id)?.scope;
|
|
904
|
+
for (const binding of this.#keymap.get(id)) {
|
|
905
|
+
const key = `${scope ?? ""}::${bindingSignature(binding, this.#platform())}`;
|
|
906
|
+
const owner = seen.get(key);
|
|
907
|
+
if (owner === void 0) {
|
|
908
|
+
seen.set(key, id);
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
collisions.push(`${formatBinding(binding, this.#displayOptions())} — "${owner}" and "${id}"`);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
if (collisions.length > 0) console.warn(`[@inklu/keys] ${collisions.length} conflicting binding(s):\n ${collisions.join("\n ")}`);
|
|
915
|
+
}
|
|
916
|
+
#warnAboutBindings(id) {
|
|
917
|
+
for (const binding of this.#keymap.get(id)) {
|
|
918
|
+
const { valid, errors } = validateBinding(binding);
|
|
919
|
+
if (valid) continue;
|
|
920
|
+
const resolved = formatBinding(binding, this.#displayOptions());
|
|
921
|
+
console.warn(`[@inklu/keys] "${id}" has an unrecognized binding ${describeBinding(binding)}, which resolves to "${resolved}". ${errors.join("; ")}.`);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
#audit() {
|
|
925
|
+
if (!isDevelopment()) return;
|
|
926
|
+
for (const id of this.#order) this.#warnAboutBindings(id);
|
|
927
|
+
this.#warnAboutConflicts();
|
|
928
|
+
}
|
|
929
|
+
#auditAdded(id) {
|
|
930
|
+
if (!isDevelopment()) return;
|
|
931
|
+
this.#warnAboutBindings(id);
|
|
932
|
+
const scope = this.#definition(id)?.scope;
|
|
933
|
+
for (const conflict of this.conflicts(this.#keymap.get(id), {
|
|
934
|
+
ignore: id,
|
|
935
|
+
scope
|
|
936
|
+
})) console.warn(`[@inklu/keys] "${id}" binds ${conflict.display}, which "${conflict.id}" already uses.`);
|
|
937
|
+
}
|
|
938
|
+
};
|
|
939
|
+
function describeBinding(binding) {
|
|
940
|
+
return typeof binding === "string" ? `"${binding}"` : `"${binding.sequence.join(" ")}"`;
|
|
941
|
+
}
|
|
942
|
+
function isDevelopment() {
|
|
943
|
+
const environment = globalThis.process?.env?.NODE_ENV;
|
|
944
|
+
return environment !== void 0 && environment !== "production";
|
|
945
|
+
}
|
|
946
|
+
function samePending(a, b) {
|
|
947
|
+
if (a.length !== b.length) return false;
|
|
948
|
+
return a.every((entry, index) => {
|
|
949
|
+
const other = b[index];
|
|
950
|
+
return other !== void 0 && entry.id === other.id && entry.matched === other.matched;
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
function scopesOverlap(a, b) {
|
|
954
|
+
if (a === void 0 || b === void 0) return true;
|
|
955
|
+
if (a === "*" || b === "*") return true;
|
|
956
|
+
return a === b;
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Creates a command registry.
|
|
960
|
+
*
|
|
961
|
+
* @example
|
|
962
|
+
* ```ts
|
|
963
|
+
* export const commands = createCommands({
|
|
964
|
+
* "palette.open": {
|
|
965
|
+
* title: "Open command palette",
|
|
966
|
+
* keys: "Mod+K",
|
|
967
|
+
* run: () => setPaletteOpen(true),
|
|
968
|
+
* },
|
|
969
|
+
* "nav.inbox": {
|
|
970
|
+
* title: "Go to inbox",
|
|
971
|
+
* keys: seq("G", "I"),
|
|
972
|
+
* run: () => navigate("/inbox"),
|
|
973
|
+
* },
|
|
974
|
+
* });
|
|
975
|
+
* ```
|
|
976
|
+
*/
|
|
977
|
+
function createCommands(definitions, options) {
|
|
978
|
+
return new CommandRegistry(definitions, options);
|
|
979
|
+
}
|
|
980
|
+
//#endregion
|
|
981
|
+
export { bindingTokens as a, seq as c, ariaKeyShortcuts as i, toBindings as l, ALWAYS_SCOPE as n, formatBinding as o, createRebinder as r, bindingSignature as s, createCommands as t, validateBinding as u };
|