use-kbd 0.3.0 → 0.4.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/README.md +35 -36
- package/dist/index.cjs +1374 -285
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +405 -56
- package/dist/index.d.ts +405 -56
- package/dist/index.js +1339 -281
- package/dist/index.js.map +1 -1
- package/package.json +6 -8
- package/src/styles.css +255 -65
package/dist/index.cjs
CHANGED
|
@@ -2,9 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
var jsxRuntime = require('react/jsx-runtime');
|
|
4
4
|
var react = require('react');
|
|
5
|
-
var base = require('@rdub/base');
|
|
6
5
|
|
|
7
|
-
// src/
|
|
6
|
+
// src/types.ts
|
|
7
|
+
function extractCaptures(state) {
|
|
8
|
+
return state.filter(
|
|
9
|
+
(e) => (e.type === "digit" || e.type === "digits") && e.value !== void 0
|
|
10
|
+
).map((e) => e.value);
|
|
11
|
+
}
|
|
12
|
+
function isDigitPlaceholder(elem) {
|
|
13
|
+
return elem.type === "digit" || elem.type === "digits";
|
|
14
|
+
}
|
|
15
|
+
function countPlaceholders(seq) {
|
|
16
|
+
return seq.filter(isDigitPlaceholder).length;
|
|
17
|
+
}
|
|
8
18
|
function createTwoColumnRenderer(config) {
|
|
9
19
|
const { headers, getRows } = config;
|
|
10
20
|
const [labelHeader, leftHeader, rightHeader] = headers;
|
|
@@ -187,34 +197,55 @@ function useActionsRegistry(options = {}) {
|
|
|
187
197
|
}
|
|
188
198
|
return bindings;
|
|
189
199
|
}, [keymap]);
|
|
200
|
+
const getFirstBindingForAction = react.useCallback((actionId) => {
|
|
201
|
+
return getBindingsForAction(actionId)[0];
|
|
202
|
+
}, [getBindingsForAction]);
|
|
190
203
|
const setBinding = react.useCallback((actionId, key) => {
|
|
191
|
-
|
|
192
|
-
...prev,
|
|
193
|
-
[key]: actionId
|
|
194
|
-
}));
|
|
195
|
-
}, [updateOverrides]);
|
|
196
|
-
const removeBinding = react.useCallback((key) => {
|
|
197
|
-
const actionsWithDefault = [];
|
|
198
|
-
for (const [id, { config }] of actionsRef.current) {
|
|
199
|
-
if (config.defaultBindings?.includes(key)) {
|
|
200
|
-
actionsWithDefault.push(id);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
if (actionsWithDefault.length > 0) {
|
|
204
|
+
if (isDefaultBinding(key, actionId)) {
|
|
204
205
|
updateRemovedDefaults((prev) => {
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
if (
|
|
209
|
-
|
|
206
|
+
const existing = prev[actionId] ?? [];
|
|
207
|
+
if (existing.includes(key)) {
|
|
208
|
+
const filtered = existing.filter((k) => k !== key);
|
|
209
|
+
if (filtered.length === 0) {
|
|
210
|
+
const { [actionId]: _, ...rest } = prev;
|
|
211
|
+
return rest;
|
|
210
212
|
}
|
|
213
|
+
return { ...prev, [actionId]: filtered };
|
|
211
214
|
}
|
|
212
|
-
return
|
|
215
|
+
return prev;
|
|
216
|
+
});
|
|
217
|
+
} else {
|
|
218
|
+
updateOverrides((prev) => ({
|
|
219
|
+
...prev,
|
|
220
|
+
[key]: actionId
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
223
|
+
}, [updateOverrides, updateRemovedDefaults, isDefaultBinding]);
|
|
224
|
+
const removeBinding = react.useCallback((actionId, key) => {
|
|
225
|
+
const action = actionsRef.current.get(actionId);
|
|
226
|
+
const isDefault = action?.config.defaultBindings?.includes(key);
|
|
227
|
+
if (isDefault) {
|
|
228
|
+
updateRemovedDefaults((prev) => {
|
|
229
|
+
const existing = prev[actionId] ?? [];
|
|
230
|
+
if (existing.includes(key)) return prev;
|
|
231
|
+
return { ...prev, [actionId]: [...existing, key] };
|
|
213
232
|
});
|
|
214
233
|
}
|
|
215
234
|
updateOverrides((prev) => {
|
|
216
|
-
const
|
|
217
|
-
|
|
235
|
+
const boundAction = prev[key];
|
|
236
|
+
if (boundAction === actionId) {
|
|
237
|
+
const { [key]: _, ...rest } = prev;
|
|
238
|
+
return rest;
|
|
239
|
+
}
|
|
240
|
+
if (Array.isArray(boundAction) && boundAction.includes(actionId)) {
|
|
241
|
+
const newActions = boundAction.filter((a) => a !== actionId);
|
|
242
|
+
if (newActions.length === 0) {
|
|
243
|
+
const { [key]: _, ...rest } = prev;
|
|
244
|
+
return rest;
|
|
245
|
+
}
|
|
246
|
+
return { ...prev, [key]: newActions.length === 1 ? newActions[0] : newActions };
|
|
247
|
+
}
|
|
248
|
+
return prev;
|
|
218
249
|
});
|
|
219
250
|
}, [updateOverrides, updateRemovedDefaults]);
|
|
220
251
|
const resetOverrides = react.useCallback(() => {
|
|
@@ -232,6 +263,7 @@ function useActionsRegistry(options = {}) {
|
|
|
232
263
|
keymap,
|
|
233
264
|
actionRegistry,
|
|
234
265
|
getBindingsForAction,
|
|
266
|
+
getFirstBindingForAction,
|
|
235
267
|
overrides,
|
|
236
268
|
setBinding,
|
|
237
269
|
removeBinding,
|
|
@@ -244,12 +276,48 @@ function useActionsRegistry(options = {}) {
|
|
|
244
276
|
keymap,
|
|
245
277
|
actionRegistry,
|
|
246
278
|
getBindingsForAction,
|
|
279
|
+
getFirstBindingForAction,
|
|
247
280
|
overrides,
|
|
248
281
|
setBinding,
|
|
249
282
|
removeBinding,
|
|
250
283
|
resetOverrides
|
|
251
284
|
]);
|
|
252
285
|
}
|
|
286
|
+
|
|
287
|
+
// src/constants.ts
|
|
288
|
+
var DEFAULT_SEQUENCE_TIMEOUT = Infinity;
|
|
289
|
+
var ACTION_MODAL = "__hotkeys:modal";
|
|
290
|
+
var ACTION_OMNIBAR = "__hotkeys:omnibar";
|
|
291
|
+
var ACTION_LOOKUP = "__hotkeys:lookup";
|
|
292
|
+
|
|
293
|
+
// src/utils.ts
|
|
294
|
+
var { max } = Math;
|
|
295
|
+
var SHIFTED_SYMBOLS = /* @__PURE__ */ new Set([
|
|
296
|
+
"!",
|
|
297
|
+
"@",
|
|
298
|
+
"#",
|
|
299
|
+
"$",
|
|
300
|
+
"%",
|
|
301
|
+
"^",
|
|
302
|
+
"&",
|
|
303
|
+
"*",
|
|
304
|
+
"(",
|
|
305
|
+
")",
|
|
306
|
+
"_",
|
|
307
|
+
"+",
|
|
308
|
+
"{",
|
|
309
|
+
"}",
|
|
310
|
+
"|",
|
|
311
|
+
":",
|
|
312
|
+
'"',
|
|
313
|
+
"<",
|
|
314
|
+
">",
|
|
315
|
+
"?",
|
|
316
|
+
"~"
|
|
317
|
+
]);
|
|
318
|
+
function isShiftedSymbol(key) {
|
|
319
|
+
return SHIFTED_SYMBOLS.has(key);
|
|
320
|
+
}
|
|
253
321
|
function isMac() {
|
|
254
322
|
if (typeof navigator === "undefined") return false;
|
|
255
323
|
return /Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
|
@@ -310,7 +378,18 @@ function formatKeyForDisplay(key) {
|
|
|
310
378
|
}
|
|
311
379
|
return key;
|
|
312
380
|
}
|
|
381
|
+
var DIGIT_PLACEHOLDER = "__DIGIT__";
|
|
382
|
+
var DIGITS_PLACEHOLDER = "__DIGITS__";
|
|
383
|
+
function isPlaceholderSentinel(key) {
|
|
384
|
+
return key === DIGIT_PLACEHOLDER || key === DIGITS_PLACEHOLDER;
|
|
385
|
+
}
|
|
313
386
|
function formatSingleCombination(combo) {
|
|
387
|
+
if (combo.key === DIGIT_PLACEHOLDER) {
|
|
388
|
+
return { display: "#", id: "\\d" };
|
|
389
|
+
}
|
|
390
|
+
if (combo.key === DIGITS_PLACEHOLDER) {
|
|
391
|
+
return { display: "##", id: "\\d+" };
|
|
392
|
+
}
|
|
314
393
|
const mac = isMac();
|
|
315
394
|
const parts = [];
|
|
316
395
|
const idParts = [];
|
|
@@ -356,6 +435,10 @@ function formatCombination(input) {
|
|
|
356
435
|
const single = formatSingleCombination(input);
|
|
357
436
|
return { ...single, isSequence: false };
|
|
358
437
|
}
|
|
438
|
+
function formatBinding(binding) {
|
|
439
|
+
const parsed = parseHotkeyString(binding);
|
|
440
|
+
return formatCombination(parsed).display;
|
|
441
|
+
}
|
|
359
442
|
function isModifierKey(key) {
|
|
360
443
|
return ["Control", "Alt", "Shift", "Meta"].includes(key);
|
|
361
444
|
}
|
|
@@ -419,6 +502,111 @@ function parseCombinationId(id) {
|
|
|
419
502
|
}
|
|
420
503
|
return sequence[0];
|
|
421
504
|
}
|
|
505
|
+
var NO_MODIFIERS = { ctrl: false, alt: false, shift: false, meta: false };
|
|
506
|
+
function parseSeqElem(str) {
|
|
507
|
+
if (str === "\\d") {
|
|
508
|
+
return { type: "digit" };
|
|
509
|
+
}
|
|
510
|
+
if (str === "\\d+") {
|
|
511
|
+
return { type: "digits" };
|
|
512
|
+
}
|
|
513
|
+
if (str.length === 1 && /^[A-Z]$/.test(str)) {
|
|
514
|
+
return {
|
|
515
|
+
type: "key",
|
|
516
|
+
key: str.toLowerCase(),
|
|
517
|
+
modifiers: { ctrl: false, alt: false, shift: true, meta: false }
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
const parts = str.toLowerCase().split("+");
|
|
521
|
+
const key = parts[parts.length - 1];
|
|
522
|
+
return {
|
|
523
|
+
type: "key",
|
|
524
|
+
key,
|
|
525
|
+
modifiers: {
|
|
526
|
+
ctrl: parts.includes("ctrl") || parts.includes("control"),
|
|
527
|
+
alt: parts.includes("alt") || parts.includes("option"),
|
|
528
|
+
shift: parts.includes("shift"),
|
|
529
|
+
meta: parts.includes("meta") || parts.includes("cmd") || parts.includes("command")
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
function parseKeySeq(hotkeyStr) {
|
|
534
|
+
if (!hotkeyStr.trim()) return [];
|
|
535
|
+
const parts = hotkeyStr.trim().split(/\s+/);
|
|
536
|
+
return parts.map(parseSeqElem);
|
|
537
|
+
}
|
|
538
|
+
function formatSeqElem(elem) {
|
|
539
|
+
if (elem.type === "digit") {
|
|
540
|
+
return { display: "\u27E8#\u27E9", id: "\\d" };
|
|
541
|
+
}
|
|
542
|
+
if (elem.type === "digits") {
|
|
543
|
+
return { display: "\u27E8##\u27E9", id: "\\d+" };
|
|
544
|
+
}
|
|
545
|
+
const mac = isMac();
|
|
546
|
+
const parts = [];
|
|
547
|
+
const idParts = [];
|
|
548
|
+
if (elem.modifiers.ctrl) {
|
|
549
|
+
parts.push(mac ? "\u2303" : "Ctrl");
|
|
550
|
+
idParts.push("ctrl");
|
|
551
|
+
}
|
|
552
|
+
if (elem.modifiers.meta) {
|
|
553
|
+
parts.push(mac ? "\u2318" : "Win");
|
|
554
|
+
idParts.push("meta");
|
|
555
|
+
}
|
|
556
|
+
if (elem.modifiers.alt) {
|
|
557
|
+
parts.push(mac ? "\u2325" : "Alt");
|
|
558
|
+
idParts.push("alt");
|
|
559
|
+
}
|
|
560
|
+
if (elem.modifiers.shift) {
|
|
561
|
+
parts.push(mac ? "\u21E7" : "Shift");
|
|
562
|
+
idParts.push("shift");
|
|
563
|
+
}
|
|
564
|
+
parts.push(formatKeyForDisplay(elem.key));
|
|
565
|
+
idParts.push(elem.key);
|
|
566
|
+
return {
|
|
567
|
+
display: mac ? parts.join("") : parts.join("+"),
|
|
568
|
+
id: idParts.join("+")
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
function formatKeySeq(seq) {
|
|
572
|
+
if (seq.length === 0) {
|
|
573
|
+
return { display: "", id: "", isSequence: false };
|
|
574
|
+
}
|
|
575
|
+
const formatted = seq.map(formatSeqElem);
|
|
576
|
+
if (seq.length === 1) {
|
|
577
|
+
return { ...formatted[0], isSequence: false };
|
|
578
|
+
}
|
|
579
|
+
return {
|
|
580
|
+
display: formatted.map((f) => f.display).join(" "),
|
|
581
|
+
id: formatted.map((f) => f.id).join(" "),
|
|
582
|
+
isSequence: true
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
function hasDigitPlaceholders(seq) {
|
|
586
|
+
return seq.some((elem) => elem.type === "digit" || elem.type === "digits");
|
|
587
|
+
}
|
|
588
|
+
function keySeqToHotkeySequence(seq) {
|
|
589
|
+
return seq.map((elem) => {
|
|
590
|
+
if (elem.type === "digit") {
|
|
591
|
+
return { key: "\\d", modifiers: NO_MODIFIERS };
|
|
592
|
+
}
|
|
593
|
+
if (elem.type === "digits") {
|
|
594
|
+
return { key: "\\d+", modifiers: NO_MODIFIERS };
|
|
595
|
+
}
|
|
596
|
+
return { key: elem.key, modifiers: elem.modifiers };
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
function hotkeySequenceToKeySeq(seq) {
|
|
600
|
+
return seq.map((combo) => {
|
|
601
|
+
if (combo.key === "\\d" && !combo.modifiers.ctrl && !combo.modifiers.alt && !combo.modifiers.shift && !combo.modifiers.meta) {
|
|
602
|
+
return { type: "digit" };
|
|
603
|
+
}
|
|
604
|
+
if (combo.key === "\\d+" && !combo.modifiers.ctrl && !combo.modifiers.alt && !combo.modifiers.shift && !combo.modifiers.meta) {
|
|
605
|
+
return { type: "digits" };
|
|
606
|
+
}
|
|
607
|
+
return { type: "key", key: combo.key, modifiers: combo.modifiers };
|
|
608
|
+
});
|
|
609
|
+
}
|
|
422
610
|
function isPrefix(a, b) {
|
|
423
611
|
if (a.length >= b.length) return false;
|
|
424
612
|
for (let i = 0; i < a.length; i++) {
|
|
@@ -429,11 +617,43 @@ function isPrefix(a, b) {
|
|
|
429
617
|
function combinationsEqual(a, b) {
|
|
430
618
|
return a.key === b.key && a.modifiers.ctrl === b.modifiers.ctrl && a.modifiers.alt === b.modifiers.alt && a.modifiers.shift === b.modifiers.shift && a.modifiers.meta === b.modifiers.meta;
|
|
431
619
|
}
|
|
620
|
+
function isDigitKey(key) {
|
|
621
|
+
return /^[0-9]$/.test(key);
|
|
622
|
+
}
|
|
623
|
+
function seqElemsCouldConflict(a, b) {
|
|
624
|
+
if (a.type === "digit" && b.type === "digit") return true;
|
|
625
|
+
if (a.type === "digit" && b.type === "key" && isDigitKey(b.key)) return true;
|
|
626
|
+
if (a.type === "key" && isDigitKey(a.key) && b.type === "digit") return true;
|
|
627
|
+
if (a.type === "digits" && b.type === "digits") return true;
|
|
628
|
+
if (a.type === "digits" && b.type === "digit") return true;
|
|
629
|
+
if (a.type === "digit" && b.type === "digits") return true;
|
|
630
|
+
if (a.type === "digits" && b.type === "key" && isDigitKey(b.key)) return true;
|
|
631
|
+
if (a.type === "key" && isDigitKey(a.key) && b.type === "digits") return true;
|
|
632
|
+
if (a.type === "key" && b.type === "key") {
|
|
633
|
+
return a.key === b.key && a.modifiers.ctrl === b.modifiers.ctrl && a.modifiers.alt === b.modifiers.alt && a.modifiers.shift === b.modifiers.shift && a.modifiers.meta === b.modifiers.meta;
|
|
634
|
+
}
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
function keySeqIsPrefix(a, b) {
|
|
638
|
+
if (a.length >= b.length) return false;
|
|
639
|
+
for (let i = 0; i < a.length; i++) {
|
|
640
|
+
if (!seqElemsCouldConflict(a[i], b[i])) return false;
|
|
641
|
+
}
|
|
642
|
+
return true;
|
|
643
|
+
}
|
|
644
|
+
function keySeqsCouldConflict(a, b) {
|
|
645
|
+
if (a.length !== b.length) return false;
|
|
646
|
+
for (let i = 0; i < a.length; i++) {
|
|
647
|
+
if (!seqElemsCouldConflict(a[i], b[i])) return false;
|
|
648
|
+
}
|
|
649
|
+
return true;
|
|
650
|
+
}
|
|
432
651
|
function findConflicts(keymap) {
|
|
433
652
|
const conflicts = /* @__PURE__ */ new Map();
|
|
434
653
|
const entries = Object.entries(keymap).map(([key, actionOrActions]) => ({
|
|
435
654
|
key,
|
|
436
655
|
sequence: parseHotkeyString(key),
|
|
656
|
+
keySeq: parseKeySeq(key),
|
|
437
657
|
actions: Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions]
|
|
438
658
|
}));
|
|
439
659
|
const keyToActions = /* @__PURE__ */ new Map();
|
|
@@ -450,7 +670,36 @@ function findConflicts(keymap) {
|
|
|
450
670
|
for (let j = i + 1; j < entries.length; j++) {
|
|
451
671
|
const a = entries[i];
|
|
452
672
|
const b = entries[j];
|
|
453
|
-
if (
|
|
673
|
+
if (keySeqsCouldConflict(a.keySeq, b.keySeq) && a.key !== b.key) {
|
|
674
|
+
const existingA = conflicts.get(a.key) ?? [];
|
|
675
|
+
if (!existingA.includes(`conflicts with: ${b.key}`)) {
|
|
676
|
+
conflicts.set(a.key, [...existingA, ...a.actions, `conflicts with: ${b.key}`]);
|
|
677
|
+
}
|
|
678
|
+
const existingB = conflicts.get(b.key) ?? [];
|
|
679
|
+
if (!existingB.includes(`conflicts with: ${a.key}`)) {
|
|
680
|
+
conflicts.set(b.key, [...existingB, ...b.actions, `conflicts with: ${a.key}`]);
|
|
681
|
+
}
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
if (keySeqIsPrefix(a.keySeq, b.keySeq)) {
|
|
685
|
+
const existingA = conflicts.get(a.key) ?? [];
|
|
686
|
+
if (!existingA.includes(`prefix of: ${b.key}`)) {
|
|
687
|
+
conflicts.set(a.key, [...existingA, ...a.actions, `prefix of: ${b.key}`]);
|
|
688
|
+
}
|
|
689
|
+
const existingB = conflicts.get(b.key) ?? [];
|
|
690
|
+
if (!existingB.includes(`has prefix: ${a.key}`)) {
|
|
691
|
+
conflicts.set(b.key, [...existingB, ...b.actions, `has prefix: ${a.key}`]);
|
|
692
|
+
}
|
|
693
|
+
} else if (keySeqIsPrefix(b.keySeq, a.keySeq)) {
|
|
694
|
+
const existingB = conflicts.get(b.key) ?? [];
|
|
695
|
+
if (!existingB.includes(`prefix of: ${a.key}`)) {
|
|
696
|
+
conflicts.set(b.key, [...existingB, ...b.actions, `prefix of: ${a.key}`]);
|
|
697
|
+
}
|
|
698
|
+
const existingA = conflicts.get(a.key) ?? [];
|
|
699
|
+
if (!existingA.includes(`has prefix: ${b.key}`)) {
|
|
700
|
+
conflicts.set(a.key, [...existingA, ...a.actions, `has prefix: ${b.key}`]);
|
|
701
|
+
}
|
|
702
|
+
} else if (isPrefix(a.sequence, b.sequence)) {
|
|
454
703
|
const existingA = conflicts.get(a.key) ?? [];
|
|
455
704
|
if (!existingA.includes(`prefix of: ${b.key}`)) {
|
|
456
705
|
conflicts.set(a.key, [...existingA, ...a.actions, `prefix of: ${b.key}`]);
|
|
@@ -489,6 +738,7 @@ function getSequenceCompletions(pendingKeys, keymap) {
|
|
|
489
738
|
const completions = [];
|
|
490
739
|
for (const [hotkeyStr, actionOrActions] of Object.entries(keymap)) {
|
|
491
740
|
const sequence = parseHotkeyString(hotkeyStr);
|
|
741
|
+
const keySeq = parseKeySeq(hotkeyStr);
|
|
492
742
|
if (sequence.length <= pendingKeys.length) continue;
|
|
493
743
|
let isPrefix2 = true;
|
|
494
744
|
for (let i = 0; i < pendingKeys.length; i++) {
|
|
@@ -498,13 +748,13 @@ function getSequenceCompletions(pendingKeys, keymap) {
|
|
|
498
748
|
}
|
|
499
749
|
}
|
|
500
750
|
if (isPrefix2) {
|
|
501
|
-
const
|
|
502
|
-
const nextKeys =
|
|
751
|
+
const remainingKeySeq = keySeq.slice(pendingKeys.length);
|
|
752
|
+
const nextKeys = formatKeySeq(remainingKeySeq).display;
|
|
503
753
|
const actions = Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions];
|
|
504
754
|
completions.push({
|
|
505
755
|
nextKeys,
|
|
506
756
|
fullSequence: hotkeyStr,
|
|
507
|
-
display:
|
|
757
|
+
display: formatKeySeq(keySeq),
|
|
508
758
|
actions
|
|
509
759
|
});
|
|
510
760
|
}
|
|
@@ -593,7 +843,7 @@ function searchActions(query, actions, keymap) {
|
|
|
593
843
|
for (const keyword of action.keywords) {
|
|
594
844
|
const kwMatch = fuzzyMatch(query, keyword);
|
|
595
845
|
if (kwMatch.matched) {
|
|
596
|
-
keywordScore =
|
|
846
|
+
keywordScore = max(keywordScore, kwMatch.score);
|
|
597
847
|
}
|
|
598
848
|
}
|
|
599
849
|
}
|
|
@@ -646,6 +896,92 @@ function sequencesMatch(a, b) {
|
|
|
646
896
|
}
|
|
647
897
|
return true;
|
|
648
898
|
}
|
|
899
|
+
function isDigit(key) {
|
|
900
|
+
return /^[0-9]$/.test(key);
|
|
901
|
+
}
|
|
902
|
+
function initMatchState(seq) {
|
|
903
|
+
return seq.map((elem) => {
|
|
904
|
+
if (elem.type === "digit") return { type: "digit" };
|
|
905
|
+
if (elem.type === "digits") return { type: "digits" };
|
|
906
|
+
return { type: "key", key: elem.key, modifiers: elem.modifiers };
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
function matchesKeyElem(combo, elem) {
|
|
910
|
+
const shiftMatches = isShiftedChar(combo.key) ? elem.modifiers.shift ? combo.modifiers.shift : true : combo.modifiers.shift === elem.modifiers.shift;
|
|
911
|
+
return combo.modifiers.ctrl === elem.modifiers.ctrl && combo.modifiers.alt === elem.modifiers.alt && shiftMatches && combo.modifiers.meta === elem.modifiers.meta && combo.key === elem.key;
|
|
912
|
+
}
|
|
913
|
+
function advanceMatchState(state, pattern, combo) {
|
|
914
|
+
const newState = [...state];
|
|
915
|
+
let pos = 0;
|
|
916
|
+
for (let i = 0; i < state.length; i++) {
|
|
917
|
+
const elem = state[i];
|
|
918
|
+
if (elem.type === "key" && !elem.matched) break;
|
|
919
|
+
if (elem.type === "digit" && elem.value === void 0) break;
|
|
920
|
+
if (elem.type === "digits" && elem.value === void 0) {
|
|
921
|
+
if (!elem.partial) break;
|
|
922
|
+
if (isDigit(combo.key)) {
|
|
923
|
+
const newPartial = (elem.partial || "") + combo.key;
|
|
924
|
+
newState[i] = { type: "digits", partial: newPartial };
|
|
925
|
+
return { status: "partial", state: newState };
|
|
926
|
+
} else {
|
|
927
|
+
const digitValue = parseInt(elem.partial, 10);
|
|
928
|
+
newState[i] = { type: "digits", value: digitValue };
|
|
929
|
+
pos = i + 1;
|
|
930
|
+
break;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
pos++;
|
|
934
|
+
}
|
|
935
|
+
if (pos >= pattern.length) {
|
|
936
|
+
return { status: "failed" };
|
|
937
|
+
}
|
|
938
|
+
const currentPattern = pattern[pos];
|
|
939
|
+
if (currentPattern.type === "digit") {
|
|
940
|
+
if (!isDigit(combo.key) || combo.modifiers.ctrl || combo.modifiers.alt || combo.modifiers.meta) {
|
|
941
|
+
return { status: "failed" };
|
|
942
|
+
}
|
|
943
|
+
newState[pos] = { type: "digit", value: parseInt(combo.key, 10) };
|
|
944
|
+
} else if (currentPattern.type === "digits") {
|
|
945
|
+
if (!isDigit(combo.key) || combo.modifiers.ctrl || combo.modifiers.alt || combo.modifiers.meta) {
|
|
946
|
+
return { status: "failed" };
|
|
947
|
+
}
|
|
948
|
+
newState[pos] = { type: "digits", partial: combo.key };
|
|
949
|
+
} else {
|
|
950
|
+
if (!matchesKeyElem(combo, currentPattern)) {
|
|
951
|
+
return { status: "failed" };
|
|
952
|
+
}
|
|
953
|
+
newState[pos] = { type: "key", key: currentPattern.key, modifiers: currentPattern.modifiers, matched: true };
|
|
954
|
+
}
|
|
955
|
+
const isComplete = newState.every((elem) => {
|
|
956
|
+
if (elem.type === "key") return elem.matched === true;
|
|
957
|
+
if (elem.type === "digit") return elem.value !== void 0;
|
|
958
|
+
if (elem.type === "digits") return elem.value !== void 0;
|
|
959
|
+
return false;
|
|
960
|
+
});
|
|
961
|
+
if (isComplete) {
|
|
962
|
+
const captures = newState.filter(
|
|
963
|
+
(e) => (e.type === "digit" || e.type === "digits") && e.value !== void 0
|
|
964
|
+
).map((e) => e.value);
|
|
965
|
+
return { status: "matched", state: newState, captures };
|
|
966
|
+
}
|
|
967
|
+
return { status: "partial", state: newState };
|
|
968
|
+
}
|
|
969
|
+
function isCollectingDigits(state) {
|
|
970
|
+
return state.some((elem) => elem.type === "digits" && elem.partial !== void 0 && elem.value === void 0);
|
|
971
|
+
}
|
|
972
|
+
function finalizeDigits(state) {
|
|
973
|
+
return state.map((elem) => {
|
|
974
|
+
if (elem.type === "digits" && elem.partial !== void 0 && elem.value === void 0) {
|
|
975
|
+
return { type: "digits", value: parseInt(elem.partial, 10) };
|
|
976
|
+
}
|
|
977
|
+
return elem;
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
function extractMatchCaptures(state) {
|
|
981
|
+
return state.filter(
|
|
982
|
+
(e) => (e.type === "digit" || e.type === "digits") && e.value !== void 0
|
|
983
|
+
).map((e) => e.value);
|
|
984
|
+
}
|
|
649
985
|
function useHotkeys(keymap, handlers, options = {}) {
|
|
650
986
|
const {
|
|
651
987
|
enabled = true,
|
|
@@ -653,7 +989,7 @@ function useHotkeys(keymap, handlers, options = {}) {
|
|
|
653
989
|
preventDefault = true,
|
|
654
990
|
stopPropagation = true,
|
|
655
991
|
enableOnFormTags = false,
|
|
656
|
-
sequenceTimeout =
|
|
992
|
+
sequenceTimeout = DEFAULT_SEQUENCE_TIMEOUT,
|
|
657
993
|
onTimeout = "submit",
|
|
658
994
|
onSequenceStart,
|
|
659
995
|
onSequenceProgress,
|
|
@@ -669,11 +1005,13 @@ function useHotkeys(keymap, handlers, options = {}) {
|
|
|
669
1005
|
const timeoutRef = react.useRef(null);
|
|
670
1006
|
const pendingKeysRef = react.useRef([]);
|
|
671
1007
|
pendingKeysRef.current = pendingKeys;
|
|
1008
|
+
const matchStatesRef = react.useRef(/* @__PURE__ */ new Map());
|
|
672
1009
|
const parsedKeymapRef = react.useRef([]);
|
|
673
1010
|
react.useEffect(() => {
|
|
674
1011
|
parsedKeymapRef.current = Object.entries(keymap).map(([key, actionOrActions]) => ({
|
|
675
1012
|
key,
|
|
676
1013
|
sequence: parseHotkeyString(key),
|
|
1014
|
+
keySeq: parseKeySeq(key),
|
|
677
1015
|
actions: Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions]
|
|
678
1016
|
}));
|
|
679
1017
|
}, [keymap]);
|
|
@@ -681,6 +1019,7 @@ function useHotkeys(keymap, handlers, options = {}) {
|
|
|
681
1019
|
setPendingKeys([]);
|
|
682
1020
|
setIsAwaitingSequence(false);
|
|
683
1021
|
setTimeoutStartedAt(null);
|
|
1022
|
+
matchStatesRef.current.clear();
|
|
684
1023
|
if (timeoutRef.current) {
|
|
685
1024
|
clearTimeout(timeoutRef.current);
|
|
686
1025
|
timeoutRef.current = null;
|
|
@@ -690,7 +1029,7 @@ function useHotkeys(keymap, handlers, options = {}) {
|
|
|
690
1029
|
clearPending();
|
|
691
1030
|
onSequenceCancel?.();
|
|
692
1031
|
}, [clearPending, onSequenceCancel]);
|
|
693
|
-
const tryExecute = react.useCallback((sequence, e) => {
|
|
1032
|
+
const tryExecute = react.useCallback((sequence, e, captures) => {
|
|
694
1033
|
for (const entry of parsedKeymapRef.current) {
|
|
695
1034
|
if (sequencesMatch(sequence, entry.sequence)) {
|
|
696
1035
|
for (const action of entry.actions) {
|
|
@@ -702,7 +1041,27 @@ function useHotkeys(keymap, handlers, options = {}) {
|
|
|
702
1041
|
if (stopPropagation) {
|
|
703
1042
|
e.stopPropagation();
|
|
704
1043
|
}
|
|
705
|
-
handler(e);
|
|
1044
|
+
handler(e, captures);
|
|
1045
|
+
return true;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
return false;
|
|
1051
|
+
}, [preventDefault, stopPropagation]);
|
|
1052
|
+
const tryExecuteKeySeq = react.useCallback((matchKey, matchState, captures, e) => {
|
|
1053
|
+
for (const entry of parsedKeymapRef.current) {
|
|
1054
|
+
if (entry.key === matchKey) {
|
|
1055
|
+
for (const action of entry.actions) {
|
|
1056
|
+
const handler = handlersRef.current[action];
|
|
1057
|
+
if (handler) {
|
|
1058
|
+
if (preventDefault) {
|
|
1059
|
+
e.preventDefault();
|
|
1060
|
+
}
|
|
1061
|
+
if (stopPropagation) {
|
|
1062
|
+
e.stopPropagation();
|
|
1063
|
+
}
|
|
1064
|
+
handler(e, captures.length > 0 ? captures : void 0);
|
|
706
1065
|
return true;
|
|
707
1066
|
}
|
|
708
1067
|
}
|
|
@@ -732,7 +1091,8 @@ function useHotkeys(keymap, handlers, options = {}) {
|
|
|
732
1091
|
const handleKeyDown = (e) => {
|
|
733
1092
|
if (!enableOnFormTags) {
|
|
734
1093
|
const eventTarget = e.target;
|
|
735
|
-
|
|
1094
|
+
const isTextInput = eventTarget instanceof HTMLInputElement && ["text", "email", "password", "search", "tel", "url", "number", "date", "datetime-local", "month", "time", "week"].includes(eventTarget.type);
|
|
1095
|
+
if (isTextInput || eventTarget instanceof HTMLTextAreaElement || eventTarget instanceof HTMLSelectElement || eventTarget.isContentEditable) {
|
|
736
1096
|
return;
|
|
737
1097
|
}
|
|
738
1098
|
}
|
|
@@ -759,6 +1119,77 @@ function useHotkeys(keymap, handlers, options = {}) {
|
|
|
759
1119
|
}
|
|
760
1120
|
const currentCombo = eventToCombination(e);
|
|
761
1121
|
const newSequence = [...pendingKeysRef.current, currentCombo];
|
|
1122
|
+
let keySeqMatched = false;
|
|
1123
|
+
let keySeqPartial = false;
|
|
1124
|
+
const matchStates = matchStatesRef.current;
|
|
1125
|
+
const hasPartialMatches = matchStates.size > 0;
|
|
1126
|
+
for (const entry of parsedKeymapRef.current) {
|
|
1127
|
+
let state = matchStates.get(entry.key);
|
|
1128
|
+
if (hasPartialMatches && !state) {
|
|
1129
|
+
continue;
|
|
1130
|
+
}
|
|
1131
|
+
if (!state) {
|
|
1132
|
+
state = initMatchState(entry.keySeq);
|
|
1133
|
+
matchStates.set(entry.key, state);
|
|
1134
|
+
}
|
|
1135
|
+
const result = advanceMatchState(state, entry.keySeq, currentCombo);
|
|
1136
|
+
if (result.status === "matched") {
|
|
1137
|
+
if (tryExecuteKeySeq(entry.key, result.state, result.captures, e)) {
|
|
1138
|
+
clearPending();
|
|
1139
|
+
keySeqMatched = true;
|
|
1140
|
+
break;
|
|
1141
|
+
}
|
|
1142
|
+
} else if (result.status === "partial") {
|
|
1143
|
+
matchStates.set(entry.key, result.state);
|
|
1144
|
+
keySeqPartial = true;
|
|
1145
|
+
} else {
|
|
1146
|
+
matchStates.delete(entry.key);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
if (keySeqMatched) {
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
if (keySeqPartial) {
|
|
1153
|
+
setPendingKeys(newSequence);
|
|
1154
|
+
setIsAwaitingSequence(true);
|
|
1155
|
+
if (pendingKeysRef.current.length === 0) {
|
|
1156
|
+
onSequenceStart?.(newSequence);
|
|
1157
|
+
} else {
|
|
1158
|
+
onSequenceProgress?.(newSequence);
|
|
1159
|
+
}
|
|
1160
|
+
if (preventDefault) {
|
|
1161
|
+
e.preventDefault();
|
|
1162
|
+
}
|
|
1163
|
+
if (Number.isFinite(sequenceTimeout)) {
|
|
1164
|
+
setTimeoutStartedAt(Date.now());
|
|
1165
|
+
timeoutRef.current = setTimeout(() => {
|
|
1166
|
+
for (const [key, state] of matchStates.entries()) {
|
|
1167
|
+
if (isCollectingDigits(state)) {
|
|
1168
|
+
const finalizedState = finalizeDigits(state);
|
|
1169
|
+
const entry = parsedKeymapRef.current.find((e2) => e2.key === key);
|
|
1170
|
+
if (entry) {
|
|
1171
|
+
const isComplete = finalizedState.every((elem) => {
|
|
1172
|
+
if (elem.type === "key") return elem.matched === true;
|
|
1173
|
+
if (elem.type === "digit") return elem.value !== void 0;
|
|
1174
|
+
if (elem.type === "digits") return elem.value !== void 0;
|
|
1175
|
+
return false;
|
|
1176
|
+
});
|
|
1177
|
+
if (isComplete) {
|
|
1178
|
+
void extractMatchCaptures(finalizedState);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
setPendingKeys([]);
|
|
1184
|
+
setIsAwaitingSequence(false);
|
|
1185
|
+
setTimeoutStartedAt(null);
|
|
1186
|
+
matchStatesRef.current.clear();
|
|
1187
|
+
onSequenceCancel?.();
|
|
1188
|
+
timeoutRef.current = null;
|
|
1189
|
+
}, sequenceTimeout);
|
|
1190
|
+
}
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
762
1193
|
const exactMatch = tryExecute(newSequence, e);
|
|
763
1194
|
if (exactMatch) {
|
|
764
1195
|
clearPending();
|
|
@@ -773,25 +1204,27 @@ function useHotkeys(keymap, handlers, options = {}) {
|
|
|
773
1204
|
} else {
|
|
774
1205
|
onSequenceProgress?.(newSequence);
|
|
775
1206
|
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
1207
|
+
if (Number.isFinite(sequenceTimeout)) {
|
|
1208
|
+
setTimeoutStartedAt(Date.now());
|
|
1209
|
+
timeoutRef.current = setTimeout(() => {
|
|
1210
|
+
if (onTimeout === "submit") {
|
|
1211
|
+
setPendingKeys((current) => {
|
|
1212
|
+
if (current.length > 0) {
|
|
1213
|
+
onSequenceCancel?.();
|
|
1214
|
+
}
|
|
1215
|
+
return [];
|
|
1216
|
+
});
|
|
1217
|
+
setIsAwaitingSequence(false);
|
|
1218
|
+
setTimeoutStartedAt(null);
|
|
1219
|
+
} else {
|
|
1220
|
+
setPendingKeys([]);
|
|
1221
|
+
setIsAwaitingSequence(false);
|
|
1222
|
+
setTimeoutStartedAt(null);
|
|
1223
|
+
onSequenceCancel?.();
|
|
1224
|
+
}
|
|
1225
|
+
timeoutRef.current = null;
|
|
1226
|
+
}, sequenceTimeout);
|
|
1227
|
+
}
|
|
795
1228
|
if (preventDefault) {
|
|
796
1229
|
e.preventDefault();
|
|
797
1230
|
}
|
|
@@ -811,21 +1244,23 @@ function useHotkeys(keymap, handlers, options = {}) {
|
|
|
811
1244
|
if (preventDefault) {
|
|
812
1245
|
e.preventDefault();
|
|
813
1246
|
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
1247
|
+
if (Number.isFinite(sequenceTimeout)) {
|
|
1248
|
+
setTimeoutStartedAt(Date.now());
|
|
1249
|
+
timeoutRef.current = setTimeout(() => {
|
|
1250
|
+
if (onTimeout === "submit") {
|
|
1251
|
+
setPendingKeys([]);
|
|
1252
|
+
setIsAwaitingSequence(false);
|
|
1253
|
+
setTimeoutStartedAt(null);
|
|
1254
|
+
onSequenceCancel?.();
|
|
1255
|
+
} else {
|
|
1256
|
+
setPendingKeys([]);
|
|
1257
|
+
setIsAwaitingSequence(false);
|
|
1258
|
+
setTimeoutStartedAt(null);
|
|
1259
|
+
onSequenceCancel?.();
|
|
1260
|
+
}
|
|
1261
|
+
timeoutRef.current = null;
|
|
1262
|
+
}, sequenceTimeout);
|
|
1263
|
+
}
|
|
829
1264
|
}
|
|
830
1265
|
}
|
|
831
1266
|
};
|
|
@@ -847,6 +1282,7 @@ function useHotkeys(keymap, handlers, options = {}) {
|
|
|
847
1282
|
clearPending,
|
|
848
1283
|
cancelSequence,
|
|
849
1284
|
tryExecute,
|
|
1285
|
+
tryExecuteKeySeq,
|
|
850
1286
|
hasPotentialMatch,
|
|
851
1287
|
hasSequenceExtension,
|
|
852
1288
|
onSequenceStart,
|
|
@@ -858,12 +1294,10 @@ function useHotkeys(keymap, handlers, options = {}) {
|
|
|
858
1294
|
var HotkeysContext = react.createContext(null);
|
|
859
1295
|
var DEFAULT_CONFIG = {
|
|
860
1296
|
storageKey: "use-kbd",
|
|
861
|
-
sequenceTimeout:
|
|
1297
|
+
sequenceTimeout: DEFAULT_SEQUENCE_TIMEOUT,
|
|
862
1298
|
disableConflicts: true,
|
|
863
1299
|
minViewportWidth: 768,
|
|
864
|
-
enableOnTouch: false
|
|
865
|
-
modalTrigger: "?",
|
|
866
|
-
omnibarTrigger: "meta+k"
|
|
1300
|
+
enableOnTouch: false
|
|
867
1301
|
};
|
|
868
1302
|
function HotkeysProvider({
|
|
869
1303
|
config: configProp = {},
|
|
@@ -912,16 +1346,12 @@ function HotkeysProvider({
|
|
|
912
1346
|
const openOmnibar = react.useCallback(() => setIsOmnibarOpen(true), []);
|
|
913
1347
|
const closeOmnibar = react.useCallback(() => setIsOmnibarOpen(false), []);
|
|
914
1348
|
const toggleOmnibar = react.useCallback(() => setIsOmnibarOpen((prev) => !prev), []);
|
|
915
|
-
const
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
map[config.omnibarTrigger] = "__hotkeys:omnibar";
|
|
922
|
-
}
|
|
923
|
-
return map;
|
|
924
|
-
}, [registry.keymap, config.modalTrigger, config.omnibarTrigger]);
|
|
1349
|
+
const [isLookupOpen, setIsLookupOpen] = react.useState(false);
|
|
1350
|
+
const openLookup = react.useCallback(() => setIsLookupOpen(true), []);
|
|
1351
|
+
const closeLookup = react.useCallback(() => setIsLookupOpen(false), []);
|
|
1352
|
+
const toggleLookup = react.useCallback(() => setIsLookupOpen((prev) => !prev), []);
|
|
1353
|
+
const [isEditingBinding, setIsEditingBinding] = react.useState(false);
|
|
1354
|
+
const keymap = registry.keymap;
|
|
925
1355
|
const conflicts = react.useMemo(() => findConflicts(keymap), [keymap]);
|
|
926
1356
|
const hasConflicts2 = conflicts.size > 0;
|
|
927
1357
|
const effectiveKeymap = react.useMemo(() => {
|
|
@@ -941,20 +1371,24 @@ function HotkeysProvider({
|
|
|
941
1371
|
for (const [id, action] of registry.actions) {
|
|
942
1372
|
map[id] = action.config.handler;
|
|
943
1373
|
}
|
|
944
|
-
map["__hotkeys:modal"] = toggleModal;
|
|
945
|
-
map["__hotkeys:omnibar"] = toggleOmnibar;
|
|
946
1374
|
return map;
|
|
947
|
-
}, [registry.actions
|
|
948
|
-
const hotkeysEnabled = isEnabled && !
|
|
1375
|
+
}, [registry.actions]);
|
|
1376
|
+
const hotkeysEnabled = isEnabled && !isEditingBinding && !isOmnibarOpen && !isLookupOpen;
|
|
949
1377
|
const {
|
|
950
1378
|
pendingKeys,
|
|
951
1379
|
isAwaitingSequence,
|
|
1380
|
+
cancelSequence,
|
|
952
1381
|
timeoutStartedAt: sequenceTimeoutStartedAt,
|
|
953
1382
|
sequenceTimeout
|
|
954
1383
|
} = useHotkeys(effectiveKeymap, handlers, {
|
|
955
1384
|
enabled: hotkeysEnabled,
|
|
956
1385
|
sequenceTimeout: config.sequenceTimeout
|
|
957
1386
|
});
|
|
1387
|
+
react.useEffect(() => {
|
|
1388
|
+
if (isAwaitingSequence && isModalOpen) {
|
|
1389
|
+
closeModal();
|
|
1390
|
+
}
|
|
1391
|
+
}, [isAwaitingSequence, isModalOpen, closeModal]);
|
|
958
1392
|
const searchActionsHelper = react.useCallback(
|
|
959
1393
|
(query) => searchActions(query, registry.actionRegistry, keymap),
|
|
960
1394
|
[registry.actionRegistry, keymap]
|
|
@@ -974,9 +1408,16 @@ function HotkeysProvider({
|
|
|
974
1408
|
openOmnibar,
|
|
975
1409
|
closeOmnibar,
|
|
976
1410
|
toggleOmnibar,
|
|
1411
|
+
isLookupOpen,
|
|
1412
|
+
openLookup,
|
|
1413
|
+
closeLookup,
|
|
1414
|
+
toggleLookup,
|
|
1415
|
+
isEditingBinding,
|
|
1416
|
+
setIsEditingBinding,
|
|
977
1417
|
executeAction: registry.execute,
|
|
978
1418
|
pendingKeys,
|
|
979
1419
|
isAwaitingSequence,
|
|
1420
|
+
cancelSequence,
|
|
980
1421
|
sequenceTimeoutStartedAt,
|
|
981
1422
|
sequenceTimeout,
|
|
982
1423
|
conflicts,
|
|
@@ -994,8 +1435,14 @@ function HotkeysProvider({
|
|
|
994
1435
|
openOmnibar,
|
|
995
1436
|
closeOmnibar,
|
|
996
1437
|
toggleOmnibar,
|
|
1438
|
+
isLookupOpen,
|
|
1439
|
+
openLookup,
|
|
1440
|
+
closeLookup,
|
|
1441
|
+
toggleLookup,
|
|
1442
|
+
isEditingBinding,
|
|
997
1443
|
pendingKeys,
|
|
998
1444
|
isAwaitingSequence,
|
|
1445
|
+
cancelSequence,
|
|
999
1446
|
sequenceTimeoutStartedAt,
|
|
1000
1447
|
sequenceTimeout,
|
|
1001
1448
|
conflicts,
|
|
@@ -1029,9 +1476,9 @@ function useAction(id, config) {
|
|
|
1029
1476
|
react.useEffect(() => {
|
|
1030
1477
|
registryRef.current.register(id, {
|
|
1031
1478
|
...config,
|
|
1032
|
-
handler: () => {
|
|
1479
|
+
handler: (e, captures) => {
|
|
1033
1480
|
if (enabledRef.current) {
|
|
1034
|
-
handlerRef.current();
|
|
1481
|
+
handlerRef.current(e, captures);
|
|
1035
1482
|
}
|
|
1036
1483
|
}
|
|
1037
1484
|
});
|
|
@@ -1065,9 +1512,9 @@ function useActions(actions) {
|
|
|
1065
1512
|
for (const [id, config] of Object.entries(actions)) {
|
|
1066
1513
|
registryRef.current.register(id, {
|
|
1067
1514
|
...config,
|
|
1068
|
-
handler: () => {
|
|
1515
|
+
handler: (e, captures) => {
|
|
1069
1516
|
if (enabledRef.current[id]) {
|
|
1070
|
-
handlersRef.current[id]?.();
|
|
1517
|
+
handlersRef.current[id]?.(e, captures);
|
|
1071
1518
|
}
|
|
1072
1519
|
}
|
|
1073
1520
|
});
|
|
@@ -1103,7 +1550,7 @@ function useRecordHotkey(options = {}) {
|
|
|
1103
1550
|
onTab: onTabProp,
|
|
1104
1551
|
onShiftTab: onShiftTabProp,
|
|
1105
1552
|
preventDefault = true,
|
|
1106
|
-
sequenceTimeout =
|
|
1553
|
+
sequenceTimeout = DEFAULT_SEQUENCE_TIMEOUT,
|
|
1107
1554
|
pauseTimeout = false
|
|
1108
1555
|
} = options;
|
|
1109
1556
|
const onCapture = useEventCallback(onCaptureProp);
|
|
@@ -1121,6 +1568,7 @@ function useRecordHotkey(options = {}) {
|
|
|
1121
1568
|
const pauseTimeoutRef = react.useRef(pauseTimeout);
|
|
1122
1569
|
pauseTimeoutRef.current = pauseTimeout;
|
|
1123
1570
|
const pendingKeysRef = react.useRef([]);
|
|
1571
|
+
const hashCycleRef = react.useRef(0);
|
|
1124
1572
|
const clearTimeout_ = react.useCallback(() => {
|
|
1125
1573
|
if (timeoutRef.current) {
|
|
1126
1574
|
clearTimeout(timeoutRef.current);
|
|
@@ -1150,6 +1598,7 @@ function useRecordHotkey(options = {}) {
|
|
|
1150
1598
|
pressedKeysRef.current.clear();
|
|
1151
1599
|
hasNonModifierRef.current = false;
|
|
1152
1600
|
currentComboRef.current = null;
|
|
1601
|
+
hashCycleRef.current = 0;
|
|
1153
1602
|
onCancel?.();
|
|
1154
1603
|
}, [clearTimeout_, onCancel]);
|
|
1155
1604
|
const commit = react.useCallback(() => {
|
|
@@ -1170,6 +1619,7 @@ function useRecordHotkey(options = {}) {
|
|
|
1170
1619
|
pressedKeysRef.current.clear();
|
|
1171
1620
|
hasNonModifierRef.current = false;
|
|
1172
1621
|
currentComboRef.current = null;
|
|
1622
|
+
hashCycleRef.current = 0;
|
|
1173
1623
|
return cancel;
|
|
1174
1624
|
}, [cancel, clearTimeout_]);
|
|
1175
1625
|
react.useEffect(() => {
|
|
@@ -1180,9 +1630,13 @@ function useRecordHotkey(options = {}) {
|
|
|
1180
1630
|
}
|
|
1181
1631
|
} else if (isRecording && pendingKeysRef.current.length > 0 && !timeoutRef.current) {
|
|
1182
1632
|
const currentSequence = pendingKeysRef.current;
|
|
1183
|
-
|
|
1633
|
+
if (sequenceTimeout === 0) {
|
|
1184
1634
|
submit(currentSequence);
|
|
1185
|
-
}
|
|
1635
|
+
} else if (Number.isFinite(sequenceTimeout)) {
|
|
1636
|
+
timeoutRef.current = setTimeout(() => {
|
|
1637
|
+
submit(currentSequence);
|
|
1638
|
+
}, sequenceTimeout);
|
|
1639
|
+
}
|
|
1186
1640
|
}
|
|
1187
1641
|
}, [pauseTimeout, isRecording, sequenceTimeout, submit]);
|
|
1188
1642
|
react.useEffect(() => {
|
|
@@ -1241,22 +1695,23 @@ function useRecordHotkey(options = {}) {
|
|
|
1241
1695
|
key = e.code.slice(5);
|
|
1242
1696
|
}
|
|
1243
1697
|
pressedKeysRef.current.add(key);
|
|
1698
|
+
let nonModifierKey = "";
|
|
1699
|
+
for (const k of pressedKeysRef.current) {
|
|
1700
|
+
if (!isModifierKey(k)) {
|
|
1701
|
+
nonModifierKey = normalizeKey(k);
|
|
1702
|
+
hasNonModifierRef.current = true;
|
|
1703
|
+
break;
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1244
1706
|
const combo = {
|
|
1245
|
-
key:
|
|
1707
|
+
key: nonModifierKey,
|
|
1246
1708
|
modifiers: {
|
|
1247
1709
|
ctrl: e.ctrlKey,
|
|
1248
1710
|
alt: e.altKey,
|
|
1249
|
-
shift: e.shiftKey,
|
|
1711
|
+
shift: e.shiftKey && !isShiftedSymbol(nonModifierKey),
|
|
1250
1712
|
meta: e.metaKey
|
|
1251
1713
|
}
|
|
1252
1714
|
};
|
|
1253
|
-
for (const k of pressedKeysRef.current) {
|
|
1254
|
-
if (!isModifierKey(k)) {
|
|
1255
|
-
combo.key = normalizeKey(k);
|
|
1256
|
-
hasNonModifierRef.current = true;
|
|
1257
|
-
break;
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
1715
|
if (combo.key) {
|
|
1261
1716
|
currentComboRef.current = combo;
|
|
1262
1717
|
setActiveKeys(combo);
|
|
@@ -1281,16 +1736,41 @@ function useRecordHotkey(options = {}) {
|
|
|
1281
1736
|
pressedKeysRef.current.delete(key);
|
|
1282
1737
|
const shouldComplete = pressedKeysRef.current.size === 0 || e.key === "Meta" && hasNonModifierRef.current;
|
|
1283
1738
|
if (shouldComplete && hasNonModifierRef.current && currentComboRef.current) {
|
|
1284
|
-
|
|
1739
|
+
let combo = currentComboRef.current;
|
|
1285
1740
|
pressedKeysRef.current.clear();
|
|
1286
1741
|
hasNonModifierRef.current = false;
|
|
1287
1742
|
currentComboRef.current = null;
|
|
1288
1743
|
setActiveKeys(null);
|
|
1289
|
-
|
|
1744
|
+
let newSequence;
|
|
1745
|
+
const noModifiers = !combo.modifiers.ctrl && !combo.modifiers.alt && !combo.modifiers.meta && !combo.modifiers.shift;
|
|
1746
|
+
if (combo.key === "#" && noModifiers) {
|
|
1747
|
+
const pending = pendingKeysRef.current;
|
|
1748
|
+
const lastCombo = pending[pending.length - 1];
|
|
1749
|
+
if (hashCycleRef.current === 0) {
|
|
1750
|
+
combo = { key: DIGIT_PLACEHOLDER, modifiers: { ctrl: false, alt: false, shift: false, meta: false } };
|
|
1751
|
+
newSequence = [...pending, combo];
|
|
1752
|
+
hashCycleRef.current = 1;
|
|
1753
|
+
} else if (hashCycleRef.current === 1 && lastCombo?.key === DIGIT_PLACEHOLDER) {
|
|
1754
|
+
newSequence = [...pending.slice(0, -1), { key: DIGITS_PLACEHOLDER, modifiers: { ctrl: false, alt: false, shift: false, meta: false } }];
|
|
1755
|
+
hashCycleRef.current = 2;
|
|
1756
|
+
} else if (hashCycleRef.current === 2 && lastCombo?.key === DIGITS_PLACEHOLDER) {
|
|
1757
|
+
newSequence = [...pending.slice(0, -1), { key: "#", modifiers: { ctrl: false, alt: false, shift: false, meta: false } }];
|
|
1758
|
+
hashCycleRef.current = 3;
|
|
1759
|
+
} else {
|
|
1760
|
+
combo = { key: DIGIT_PLACEHOLDER, modifiers: { ctrl: false, alt: false, shift: false, meta: false } };
|
|
1761
|
+
newSequence = [...pending, combo];
|
|
1762
|
+
hashCycleRef.current = 1;
|
|
1763
|
+
}
|
|
1764
|
+
} else {
|
|
1765
|
+
hashCycleRef.current = 0;
|
|
1766
|
+
newSequence = [...pendingKeysRef.current, combo];
|
|
1767
|
+
}
|
|
1290
1768
|
pendingKeysRef.current = newSequence;
|
|
1291
1769
|
setPendingKeys(newSequence);
|
|
1292
1770
|
clearTimeout_();
|
|
1293
|
-
if (
|
|
1771
|
+
if (sequenceTimeout === 0) {
|
|
1772
|
+
submit(newSequence);
|
|
1773
|
+
} else if (!pauseTimeoutRef.current && Number.isFinite(sequenceTimeout)) {
|
|
1294
1774
|
timeoutRef.current = setTimeout(() => {
|
|
1295
1775
|
submit(newSequence);
|
|
1296
1776
|
}, sequenceTimeout);
|
|
@@ -1316,6 +1796,7 @@ function useRecordHotkey(options = {}) {
|
|
|
1316
1796
|
display,
|
|
1317
1797
|
pendingKeys,
|
|
1318
1798
|
activeKeys,
|
|
1799
|
+
sequenceTimeout,
|
|
1319
1800
|
combination
|
|
1320
1801
|
// deprecated
|
|
1321
1802
|
};
|
|
@@ -1416,6 +1897,7 @@ function useEditableHotkeys(defaults, handlers, options = {}) {
|
|
|
1416
1897
|
sequenceTimeout
|
|
1417
1898
|
};
|
|
1418
1899
|
}
|
|
1900
|
+
var { max: max2, min } = Math;
|
|
1419
1901
|
function useOmnibar(options) {
|
|
1420
1902
|
const {
|
|
1421
1903
|
actions,
|
|
@@ -1492,10 +1974,10 @@ function useOmnibar(options) {
|
|
|
1492
1974
|
});
|
|
1493
1975
|
}, [onOpen, onClose]);
|
|
1494
1976
|
const selectNext = react.useCallback(() => {
|
|
1495
|
-
setSelectedIndex((prev) =>
|
|
1977
|
+
setSelectedIndex((prev) => min(prev + 1, results.length - 1));
|
|
1496
1978
|
}, [results.length]);
|
|
1497
1979
|
const selectPrev = react.useCallback(() => {
|
|
1498
|
-
setSelectedIndex((prev) =>
|
|
1980
|
+
setSelectedIndex((prev) => max2(prev - 1, 0));
|
|
1499
1981
|
}, []);
|
|
1500
1982
|
const resetSelection = react.useCallback(() => {
|
|
1501
1983
|
setSelectedIndex(0);
|
|
@@ -1561,23 +2043,377 @@ function useOmnibar(options) {
|
|
|
1561
2043
|
isAwaitingSequence
|
|
1562
2044
|
};
|
|
1563
2045
|
}
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
2046
|
+
var baseStyle = {
|
|
2047
|
+
width: "1em",
|
|
2048
|
+
height: "1em",
|
|
2049
|
+
verticalAlign: "middle"
|
|
2050
|
+
};
|
|
2051
|
+
function Up({ className, style }) {
|
|
2052
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2053
|
+
"svg",
|
|
2054
|
+
{
|
|
2055
|
+
className,
|
|
2056
|
+
style: { ...baseStyle, ...style },
|
|
2057
|
+
viewBox: "0 0 24 24",
|
|
2058
|
+
fill: "none",
|
|
2059
|
+
stroke: "currentColor",
|
|
2060
|
+
strokeWidth: "3",
|
|
2061
|
+
strokeLinecap: "round",
|
|
2062
|
+
strokeLinejoin: "round",
|
|
2063
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 19V5M5 12l7-7 7 7" })
|
|
2064
|
+
}
|
|
2065
|
+
);
|
|
2066
|
+
}
|
|
2067
|
+
function Down({ className, style }) {
|
|
2068
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2069
|
+
"svg",
|
|
2070
|
+
{
|
|
2071
|
+
className,
|
|
2072
|
+
style: { ...baseStyle, ...style },
|
|
2073
|
+
viewBox: "0 0 24 24",
|
|
2074
|
+
fill: "none",
|
|
2075
|
+
stroke: "currentColor",
|
|
2076
|
+
strokeWidth: "3",
|
|
2077
|
+
strokeLinecap: "round",
|
|
2078
|
+
strokeLinejoin: "round",
|
|
2079
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 5v14M5 12l7 7 7-7" })
|
|
2080
|
+
}
|
|
2081
|
+
);
|
|
2082
|
+
}
|
|
2083
|
+
function Left({ className, style }) {
|
|
2084
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2085
|
+
"svg",
|
|
2086
|
+
{
|
|
2087
|
+
className,
|
|
2088
|
+
style: { ...baseStyle, ...style },
|
|
2089
|
+
viewBox: "0 0 24 24",
|
|
2090
|
+
fill: "none",
|
|
2091
|
+
stroke: "currentColor",
|
|
2092
|
+
strokeWidth: "3",
|
|
2093
|
+
strokeLinecap: "round",
|
|
2094
|
+
strokeLinejoin: "round",
|
|
2095
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M19 12H5M12 5l-7 7 7 7" })
|
|
2096
|
+
}
|
|
2097
|
+
);
|
|
2098
|
+
}
|
|
2099
|
+
function Right({ className, style }) {
|
|
2100
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2101
|
+
"svg",
|
|
2102
|
+
{
|
|
2103
|
+
className,
|
|
2104
|
+
style: { ...baseStyle, ...style },
|
|
2105
|
+
viewBox: "0 0 24 24",
|
|
2106
|
+
fill: "none",
|
|
2107
|
+
stroke: "currentColor",
|
|
2108
|
+
strokeWidth: "3",
|
|
2109
|
+
strokeLinecap: "round",
|
|
2110
|
+
strokeLinejoin: "round",
|
|
2111
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5 12h14M12 5l7 7-7 7" })
|
|
2112
|
+
}
|
|
2113
|
+
);
|
|
2114
|
+
}
|
|
2115
|
+
function Enter({ className, style }) {
|
|
2116
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2117
|
+
"svg",
|
|
2118
|
+
{
|
|
2119
|
+
className,
|
|
2120
|
+
style: { ...baseStyle, ...style },
|
|
2121
|
+
viewBox: "0 0 24 24",
|
|
2122
|
+
fill: "none",
|
|
2123
|
+
stroke: "currentColor",
|
|
2124
|
+
strokeWidth: "3",
|
|
2125
|
+
strokeLinecap: "round",
|
|
2126
|
+
strokeLinejoin: "round",
|
|
2127
|
+
children: [
|
|
2128
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 10l-4 4 4 4" }),
|
|
2129
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M19 6v8a2 2 0 01-2 2H5" })
|
|
2130
|
+
]
|
|
2131
|
+
}
|
|
2132
|
+
);
|
|
2133
|
+
}
|
|
2134
|
+
function Backspace({ className, style }) {
|
|
2135
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2136
|
+
"svg",
|
|
2137
|
+
{
|
|
2138
|
+
className,
|
|
2139
|
+
style: { ...baseStyle, ...style },
|
|
2140
|
+
viewBox: "0 0 24 24",
|
|
2141
|
+
fill: "none",
|
|
2142
|
+
stroke: "currentColor",
|
|
2143
|
+
strokeWidth: "2",
|
|
2144
|
+
strokeLinecap: "round",
|
|
2145
|
+
strokeLinejoin: "round",
|
|
2146
|
+
children: [
|
|
2147
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M21 4H8l-7 8 7 8h13a2 2 0 002-2V6a2 2 0 00-2-2z" }),
|
|
2148
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "9", x2: "12", y2: "15" }),
|
|
2149
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", y1: "9", x2: "18", y2: "15" })
|
|
2150
|
+
]
|
|
2151
|
+
}
|
|
2152
|
+
);
|
|
2153
|
+
}
|
|
2154
|
+
function getKeyIcon(key) {
|
|
2155
|
+
switch (key.toLowerCase()) {
|
|
2156
|
+
case "arrowup":
|
|
2157
|
+
return Up;
|
|
2158
|
+
case "arrowdown":
|
|
2159
|
+
return Down;
|
|
2160
|
+
case "arrowleft":
|
|
2161
|
+
return Left;
|
|
2162
|
+
case "arrowright":
|
|
2163
|
+
return Right;
|
|
2164
|
+
case "enter":
|
|
2165
|
+
return Enter;
|
|
2166
|
+
case "backspace":
|
|
2167
|
+
return Backspace;
|
|
2168
|
+
default:
|
|
2169
|
+
return null;
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
var baseStyle2 = {
|
|
2173
|
+
width: "1.2em",
|
|
2174
|
+
height: "1.2em",
|
|
2175
|
+
marginRight: "2px",
|
|
2176
|
+
verticalAlign: "middle"
|
|
2177
|
+
};
|
|
2178
|
+
var wideStyle = {
|
|
2179
|
+
...baseStyle2,
|
|
2180
|
+
width: "1.4em"
|
|
2181
|
+
};
|
|
2182
|
+
var Command = react.forwardRef(
|
|
2183
|
+
({ className, style, ...props }, ref) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
2184
|
+
"svg",
|
|
2185
|
+
{
|
|
2186
|
+
ref,
|
|
2187
|
+
className,
|
|
2188
|
+
style: { ...baseStyle2, ...style },
|
|
2189
|
+
viewBox: "0 0 24 24",
|
|
2190
|
+
fill: "currentColor",
|
|
2191
|
+
...props,
|
|
2192
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6 4a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h2v4H6a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2v-2h4v2a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2v-2a2 2 0 0 0-2-2h-2v-4h2a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2v2h-4V6a2 2 0 0 0-2-2H6zm4 6h4v4h-4v-4z" })
|
|
2193
|
+
}
|
|
2194
|
+
)
|
|
2195
|
+
);
|
|
2196
|
+
Command.displayName = "Command";
|
|
2197
|
+
var Ctrl = react.forwardRef(
|
|
2198
|
+
({ className, style, ...props }, ref) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
2199
|
+
"svg",
|
|
2200
|
+
{
|
|
2201
|
+
ref,
|
|
2202
|
+
className,
|
|
2203
|
+
style: { ...baseStyle2, ...style },
|
|
2204
|
+
viewBox: "0 0 24 24",
|
|
2205
|
+
fill: "none",
|
|
2206
|
+
stroke: "currentColor",
|
|
2207
|
+
strokeWidth: "3",
|
|
2208
|
+
strokeLinecap: "round",
|
|
2209
|
+
strokeLinejoin: "round",
|
|
2210
|
+
...props,
|
|
2211
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6 15l6-6 6 6" })
|
|
2212
|
+
}
|
|
2213
|
+
)
|
|
2214
|
+
);
|
|
2215
|
+
Ctrl.displayName = "Ctrl";
|
|
2216
|
+
var Shift = react.forwardRef(
|
|
2217
|
+
({ className, style, ...props }, ref) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
2218
|
+
"svg",
|
|
2219
|
+
{
|
|
2220
|
+
ref,
|
|
2221
|
+
className,
|
|
2222
|
+
style: { ...wideStyle, ...style },
|
|
2223
|
+
viewBox: "0 0 28 24",
|
|
2224
|
+
fill: "none",
|
|
2225
|
+
stroke: "currentColor",
|
|
2226
|
+
strokeWidth: "2",
|
|
2227
|
+
strokeLinejoin: "round",
|
|
2228
|
+
...props,
|
|
2229
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M14 3L3 14h6v7h10v-7h6L14 3z" })
|
|
2230
|
+
}
|
|
2231
|
+
)
|
|
2232
|
+
);
|
|
2233
|
+
Shift.displayName = "Shift";
|
|
2234
|
+
var Option = react.forwardRef(
|
|
2235
|
+
({ className, style, ...props }, ref) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
2236
|
+
"svg",
|
|
2237
|
+
{
|
|
2238
|
+
ref,
|
|
2239
|
+
className,
|
|
2240
|
+
style: { ...baseStyle2, ...style },
|
|
2241
|
+
viewBox: "0 0 24 24",
|
|
2242
|
+
fill: "none",
|
|
2243
|
+
stroke: "currentColor",
|
|
2244
|
+
strokeWidth: "2.5",
|
|
2245
|
+
strokeLinecap: "round",
|
|
2246
|
+
strokeLinejoin: "round",
|
|
2247
|
+
...props,
|
|
2248
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 6h6l8 12h6M14 6h6" })
|
|
2249
|
+
}
|
|
2250
|
+
)
|
|
2251
|
+
);
|
|
2252
|
+
Option.displayName = "Option";
|
|
2253
|
+
var Alt = react.forwardRef(
|
|
2254
|
+
({ className, style, ...props }, ref) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
2255
|
+
"svg",
|
|
2256
|
+
{
|
|
2257
|
+
ref,
|
|
2258
|
+
className,
|
|
2259
|
+
style: { ...baseStyle2, ...style },
|
|
2260
|
+
viewBox: "0 0 24 24",
|
|
2261
|
+
fill: "none",
|
|
2262
|
+
stroke: "currentColor",
|
|
2263
|
+
strokeWidth: "2.5",
|
|
2264
|
+
strokeLinecap: "round",
|
|
2265
|
+
strokeLinejoin: "round",
|
|
2266
|
+
...props,
|
|
2267
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 18h8M12 18l4-6M12 18l4 0M16 12l4-6h-8" })
|
|
2268
|
+
}
|
|
2269
|
+
)
|
|
2270
|
+
);
|
|
2271
|
+
Alt.displayName = "Alt";
|
|
2272
|
+
var isMac2 = typeof navigator !== "undefined" && /Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
|
2273
|
+
function getModifierIcon(modifier) {
|
|
2274
|
+
switch (modifier) {
|
|
2275
|
+
case "meta":
|
|
2276
|
+
return Command;
|
|
2277
|
+
case "ctrl":
|
|
2278
|
+
return Ctrl;
|
|
2279
|
+
case "shift":
|
|
2280
|
+
return Shift;
|
|
2281
|
+
case "opt":
|
|
2282
|
+
return Option;
|
|
2283
|
+
case "alt":
|
|
2284
|
+
return isMac2 ? Option : Alt;
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
var ModifierIcon = react.forwardRef(
|
|
2288
|
+
({ modifier, ...props }, ref) => {
|
|
2289
|
+
const Icon = getModifierIcon(modifier);
|
|
2290
|
+
return /* @__PURE__ */ jsxRuntime.jsx(Icon, { ref, ...props });
|
|
2291
|
+
}
|
|
2292
|
+
);
|
|
2293
|
+
ModifierIcon.displayName = "ModifierIcon";
|
|
2294
|
+
function KeyCombo({ combo }) {
|
|
2295
|
+
const { key, modifiers } = combo;
|
|
2296
|
+
const parts = [];
|
|
2297
|
+
if (modifiers.meta) {
|
|
2298
|
+
parts.push(/* @__PURE__ */ jsxRuntime.jsx(ModifierIcon, { modifier: "meta", className: "kbd-modifier-icon" }, "meta"));
|
|
2299
|
+
}
|
|
2300
|
+
if (modifiers.ctrl) {
|
|
2301
|
+
parts.push(/* @__PURE__ */ jsxRuntime.jsx(ModifierIcon, { modifier: "ctrl", className: "kbd-modifier-icon" }, "ctrl"));
|
|
2302
|
+
}
|
|
2303
|
+
if (modifiers.alt) {
|
|
2304
|
+
parts.push(/* @__PURE__ */ jsxRuntime.jsx(ModifierIcon, { modifier: "alt", className: "kbd-modifier-icon" }, "alt"));
|
|
2305
|
+
}
|
|
2306
|
+
if (modifiers.shift) {
|
|
2307
|
+
parts.push(/* @__PURE__ */ jsxRuntime.jsx(ModifierIcon, { modifier: "shift", className: "kbd-modifier-icon" }, "shift"));
|
|
2308
|
+
}
|
|
2309
|
+
const KeyIcon = getKeyIcon(key);
|
|
2310
|
+
if (KeyIcon) {
|
|
2311
|
+
parts.push(/* @__PURE__ */ jsxRuntime.jsx(KeyIcon, { className: "kbd-key-icon" }, "key"));
|
|
2312
|
+
} else {
|
|
2313
|
+
parts.push(/* @__PURE__ */ jsxRuntime.jsx("span", { children: formatKeyForDisplay(key) }, "key"));
|
|
2314
|
+
}
|
|
2315
|
+
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: parts });
|
|
2316
|
+
}
|
|
2317
|
+
function SeqElemDisplay({ elem }) {
|
|
2318
|
+
if (elem.type === "digit") {
|
|
2319
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "kbd-placeholder", title: "Any single digit (0-9)", children: "#" });
|
|
2320
|
+
}
|
|
2321
|
+
if (elem.type === "digits") {
|
|
2322
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "kbd-placeholder", title: "One or more digits (0-9)", children: "##" });
|
|
2323
|
+
}
|
|
2324
|
+
return /* @__PURE__ */ jsxRuntime.jsx(KeyCombo, { combo: { key: elem.key, modifiers: elem.modifiers } });
|
|
2325
|
+
}
|
|
2326
|
+
function BindingDisplay({ binding }) {
|
|
2327
|
+
const sequence = parseKeySeq(binding);
|
|
2328
|
+
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: sequence.map((elem, i) => /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
|
|
2329
|
+
i > 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "kbd-sequence-sep", children: " " }),
|
|
2330
|
+
/* @__PURE__ */ jsxRuntime.jsx(SeqElemDisplay, { elem })
|
|
2331
|
+
] }, i)) });
|
|
2332
|
+
}
|
|
2333
|
+
function Kbd({
|
|
2334
|
+
action,
|
|
2335
|
+
separator = " / ",
|
|
2336
|
+
all = false,
|
|
2337
|
+
fallback = null,
|
|
2338
|
+
className,
|
|
2339
|
+
clickable = true
|
|
2340
|
+
}) {
|
|
2341
|
+
const ctx = useMaybeHotkeysContext();
|
|
2342
|
+
const warnedRef = react.useRef(false);
|
|
2343
|
+
const bindings = ctx ? all ? ctx.registry.getBindingsForAction(action) : [ctx.registry.getFirstBindingForAction(action)].filter(Boolean) : [];
|
|
2344
|
+
react.useEffect(() => {
|
|
2345
|
+
if (!ctx) return;
|
|
2346
|
+
if (warnedRef.current) return;
|
|
2347
|
+
const timer = setTimeout(() => {
|
|
2348
|
+
if (!ctx.registry.actions.has(action)) {
|
|
2349
|
+
console.warn(`Kbd: Action "${action}" not found in registry`);
|
|
2350
|
+
warnedRef.current = true;
|
|
2351
|
+
}
|
|
2352
|
+
}, 100);
|
|
2353
|
+
return () => clearTimeout(timer);
|
|
2354
|
+
}, [ctx, action]);
|
|
2355
|
+
if (!ctx) {
|
|
2356
|
+
return null;
|
|
2357
|
+
}
|
|
2358
|
+
if (bindings.length === 0) {
|
|
2359
|
+
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: fallback });
|
|
2360
|
+
}
|
|
2361
|
+
const content = bindings.map((binding, i) => /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
|
|
2362
|
+
i > 0 && separator,
|
|
2363
|
+
/* @__PURE__ */ jsxRuntime.jsx(BindingDisplay, { binding })
|
|
2364
|
+
] }, binding));
|
|
2365
|
+
if (clickable) {
|
|
2366
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2367
|
+
"kbd",
|
|
2368
|
+
{
|
|
2369
|
+
className: `${className || ""} kbd-clickable`.trim(),
|
|
2370
|
+
onClick: () => ctx.executeAction(action),
|
|
2371
|
+
role: "button",
|
|
2372
|
+
tabIndex: 0,
|
|
2373
|
+
onKeyDown: (e) => {
|
|
2374
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
2375
|
+
e.preventDefault();
|
|
2376
|
+
ctx.executeAction(action);
|
|
2377
|
+
}
|
|
2378
|
+
},
|
|
2379
|
+
children: content
|
|
2380
|
+
}
|
|
2381
|
+
);
|
|
2382
|
+
}
|
|
2383
|
+
return /* @__PURE__ */ jsxRuntime.jsx("kbd", { className, children: content });
|
|
2384
|
+
}
|
|
2385
|
+
function Key(props) {
|
|
2386
|
+
return /* @__PURE__ */ jsxRuntime.jsx(Kbd, { ...props, clickable: false });
|
|
2387
|
+
}
|
|
2388
|
+
function Kbds(props) {
|
|
2389
|
+
return /* @__PURE__ */ jsxRuntime.jsx(Kbd, { ...props, all: true });
|
|
2390
|
+
}
|
|
2391
|
+
function KbdModal(props) {
|
|
2392
|
+
return /* @__PURE__ */ jsxRuntime.jsx(Kbd, { ...props, action: ACTION_MODAL });
|
|
2393
|
+
}
|
|
2394
|
+
function KbdOmnibar(props) {
|
|
2395
|
+
return /* @__PURE__ */ jsxRuntime.jsx(Kbd, { ...props, action: ACTION_OMNIBAR });
|
|
2396
|
+
}
|
|
2397
|
+
function KbdLookup(props) {
|
|
2398
|
+
return /* @__PURE__ */ jsxRuntime.jsx(Kbd, { ...props, action: ACTION_LOOKUP });
|
|
2399
|
+
}
|
|
2400
|
+
function buildActionMap(keymap) {
|
|
2401
|
+
const map = /* @__PURE__ */ new Map();
|
|
2402
|
+
for (const [key, actionOrActions] of Object.entries(keymap)) {
|
|
2403
|
+
const actions = Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions];
|
|
2404
|
+
for (const action of actions) {
|
|
2405
|
+
map.set(action, key);
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
return map;
|
|
2409
|
+
}
|
|
2410
|
+
function KeybindingEditor({
|
|
2411
|
+
keymap,
|
|
2412
|
+
defaults,
|
|
2413
|
+
descriptions,
|
|
2414
|
+
onChange,
|
|
2415
|
+
onReset,
|
|
2416
|
+
className,
|
|
1581
2417
|
children
|
|
1582
2418
|
}) {
|
|
1583
2419
|
const [editingAction, setEditingAction] = react.useState(null);
|
|
@@ -1748,127 +2584,203 @@ function KeybindingEditor({
|
|
|
1748
2584
|
] })
|
|
1749
2585
|
] });
|
|
1750
2586
|
}
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
strokeLinejoin: "round",
|
|
1816
|
-
children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 6h6l8 12h6M14 6h6" })
|
|
2587
|
+
function LookupModal({ defaultBinding = "meta+shift+k" } = {}) {
|
|
2588
|
+
const {
|
|
2589
|
+
isLookupOpen,
|
|
2590
|
+
closeLookup,
|
|
2591
|
+
toggleLookup,
|
|
2592
|
+
registry,
|
|
2593
|
+
executeAction
|
|
2594
|
+
} = useHotkeysContext();
|
|
2595
|
+
useAction(ACTION_LOOKUP, {
|
|
2596
|
+
label: "Key lookup",
|
|
2597
|
+
group: "Global",
|
|
2598
|
+
defaultBindings: defaultBinding ? [defaultBinding] : [],
|
|
2599
|
+
handler: react.useCallback(() => toggleLookup(), [toggleLookup])
|
|
2600
|
+
});
|
|
2601
|
+
const [pendingKeys, setPendingKeys] = react.useState([]);
|
|
2602
|
+
const [selectedIndex, setSelectedIndex] = react.useState(0);
|
|
2603
|
+
const allBindings = react.useMemo(() => {
|
|
2604
|
+
const results = [];
|
|
2605
|
+
const keymap = registry.keymap;
|
|
2606
|
+
for (const [binding, actionOrActions] of Object.entries(keymap)) {
|
|
2607
|
+
if (binding.startsWith("__")) continue;
|
|
2608
|
+
const actions = Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions];
|
|
2609
|
+
const sequence = parseHotkeyString(binding);
|
|
2610
|
+
const keySeq = parseKeySeq(binding);
|
|
2611
|
+
const display = formatKeySeq(keySeq).display;
|
|
2612
|
+
const labels = actions.map((actionId) => {
|
|
2613
|
+
const action = registry.actions.get(actionId);
|
|
2614
|
+
return action?.config.label || actionId;
|
|
2615
|
+
});
|
|
2616
|
+
results.push({ binding, sequence, keySeq, display, actions, labels });
|
|
2617
|
+
}
|
|
2618
|
+
results.sort((a, b) => a.binding.localeCompare(b.binding));
|
|
2619
|
+
return results;
|
|
2620
|
+
}, [registry.keymap, registry.actions]);
|
|
2621
|
+
const filteredBindings = react.useMemo(() => {
|
|
2622
|
+
if (pendingKeys.length === 0) return allBindings;
|
|
2623
|
+
return allBindings.filter((result) => {
|
|
2624
|
+
if (result.sequence.length < pendingKeys.length) return false;
|
|
2625
|
+
for (let i = 0; i < pendingKeys.length; i++) {
|
|
2626
|
+
const pending = pendingKeys[i];
|
|
2627
|
+
const target = result.sequence[i];
|
|
2628
|
+
if (pending.key !== target.key) return false;
|
|
2629
|
+
if (pending.modifiers.ctrl !== target.modifiers.ctrl) return false;
|
|
2630
|
+
if (pending.modifiers.alt !== target.modifiers.alt) return false;
|
|
2631
|
+
if (pending.modifiers.shift !== target.modifiers.shift) return false;
|
|
2632
|
+
if (pending.modifiers.meta !== target.modifiers.meta) return false;
|
|
2633
|
+
}
|
|
2634
|
+
return true;
|
|
2635
|
+
});
|
|
2636
|
+
}, [allBindings, pendingKeys]);
|
|
2637
|
+
const groupedByNextKey = react.useMemo(() => {
|
|
2638
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2639
|
+
for (const result of filteredBindings) {
|
|
2640
|
+
if (result.sequence.length > pendingKeys.length) {
|
|
2641
|
+
const nextCombo = result.sequence[pendingKeys.length];
|
|
2642
|
+
const nextKey = formatCombination([nextCombo]).display;
|
|
2643
|
+
const existing = groups.get(nextKey) || [];
|
|
2644
|
+
existing.push(result);
|
|
2645
|
+
groups.set(nextKey, existing);
|
|
2646
|
+
} else {
|
|
2647
|
+
const existing = groups.get("") || [];
|
|
2648
|
+
existing.push(result);
|
|
2649
|
+
groups.set("", existing);
|
|
2650
|
+
}
|
|
1817
2651
|
}
|
|
1818
|
-
|
|
1819
|
-
}
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
stroke: "currentColor",
|
|
1829
|
-
strokeWidth: "2.5",
|
|
1830
|
-
strokeLinecap: "round",
|
|
1831
|
-
strokeLinejoin: "round",
|
|
1832
|
-
children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 18h8M12 18l4-6M12 18l4 0M16 12l4-6h-8" })
|
|
2652
|
+
return groups;
|
|
2653
|
+
}, [filteredBindings, pendingKeys]);
|
|
2654
|
+
const formattedPendingKeys = react.useMemo(() => {
|
|
2655
|
+
if (pendingKeys.length === 0) return "";
|
|
2656
|
+
return formatCombination(pendingKeys).display;
|
|
2657
|
+
}, [pendingKeys]);
|
|
2658
|
+
react.useEffect(() => {
|
|
2659
|
+
if (isLookupOpen) {
|
|
2660
|
+
setPendingKeys([]);
|
|
2661
|
+
setSelectedIndex(0);
|
|
1833
2662
|
}
|
|
1834
|
-
);
|
|
2663
|
+
}, [isLookupOpen]);
|
|
2664
|
+
react.useEffect(() => {
|
|
2665
|
+
setSelectedIndex(0);
|
|
2666
|
+
}, [filteredBindings.length]);
|
|
2667
|
+
react.useEffect(() => {
|
|
2668
|
+
if (!isLookupOpen) return;
|
|
2669
|
+
const handleKeyDown = (e) => {
|
|
2670
|
+
if (e.key === "Escape") {
|
|
2671
|
+
e.preventDefault();
|
|
2672
|
+
if (pendingKeys.length > 0) {
|
|
2673
|
+
setPendingKeys([]);
|
|
2674
|
+
} else {
|
|
2675
|
+
closeLookup();
|
|
2676
|
+
}
|
|
2677
|
+
return;
|
|
2678
|
+
}
|
|
2679
|
+
if (e.key === "Backspace") {
|
|
2680
|
+
e.preventDefault();
|
|
2681
|
+
setPendingKeys((prev) => prev.slice(0, -1));
|
|
2682
|
+
return;
|
|
2683
|
+
}
|
|
2684
|
+
if (e.key === "ArrowDown") {
|
|
2685
|
+
e.preventDefault();
|
|
2686
|
+
setSelectedIndex((prev) => Math.min(prev + 1, filteredBindings.length - 1));
|
|
2687
|
+
return;
|
|
2688
|
+
}
|
|
2689
|
+
if (e.key === "ArrowUp") {
|
|
2690
|
+
e.preventDefault();
|
|
2691
|
+
setSelectedIndex((prev) => Math.max(prev - 1, 0));
|
|
2692
|
+
return;
|
|
2693
|
+
}
|
|
2694
|
+
if (e.key === "Enter") {
|
|
2695
|
+
e.preventDefault();
|
|
2696
|
+
const selected = filteredBindings[selectedIndex];
|
|
2697
|
+
if (selected && selected.actions.length > 0) {
|
|
2698
|
+
closeLookup();
|
|
2699
|
+
executeAction(selected.actions[0]);
|
|
2700
|
+
}
|
|
2701
|
+
return;
|
|
2702
|
+
}
|
|
2703
|
+
if (isModifierKey(e.key)) return;
|
|
2704
|
+
e.preventDefault();
|
|
2705
|
+
const newCombo = {
|
|
2706
|
+
key: normalizeKey(e.key),
|
|
2707
|
+
modifiers: {
|
|
2708
|
+
ctrl: e.ctrlKey,
|
|
2709
|
+
alt: e.altKey,
|
|
2710
|
+
shift: e.shiftKey,
|
|
2711
|
+
meta: e.metaKey
|
|
2712
|
+
}
|
|
2713
|
+
};
|
|
2714
|
+
setPendingKeys((prev) => [...prev, newCombo]);
|
|
2715
|
+
};
|
|
2716
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
2717
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
2718
|
+
}, [isLookupOpen, pendingKeys, filteredBindings, selectedIndex, closeLookup, executeAction]);
|
|
2719
|
+
const handleBackdropClick = react.useCallback(() => {
|
|
2720
|
+
closeLookup();
|
|
2721
|
+
}, [closeLookup]);
|
|
2722
|
+
if (!isLookupOpen) return null;
|
|
2723
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "kbd-lookup-backdrop", onClick: handleBackdropClick, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "kbd-lookup", onClick: (e) => e.stopPropagation(), children: [
|
|
2724
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "kbd-lookup-header", children: [
|
|
2725
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "kbd-lookup-search", children: formattedPendingKeys ? /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "kbd-sequence-keys", children: formattedPendingKeys }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "kbd-lookup-placeholder", children: "Type keys to filter..." }) }),
|
|
2726
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "kbd-lookup-hint", children: [
|
|
2727
|
+
"\u2191\u2193 navigate \xB7 Enter select \xB7 Esc ",
|
|
2728
|
+
pendingKeys.length > 0 ? "clear" : "close",
|
|
2729
|
+
" \xB7 \u232B back"
|
|
2730
|
+
] })
|
|
2731
|
+
] }),
|
|
2732
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "kbd-lookup-results", children: filteredBindings.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "kbd-lookup-empty", children: "No matching shortcuts" }) : filteredBindings.map((result, index) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2733
|
+
"div",
|
|
2734
|
+
{
|
|
2735
|
+
className: `kbd-lookup-result ${index === selectedIndex ? "selected" : ""}`,
|
|
2736
|
+
onClick: () => {
|
|
2737
|
+
closeLookup();
|
|
2738
|
+
if (result.actions.length > 0) {
|
|
2739
|
+
executeAction(result.actions[0]);
|
|
2740
|
+
}
|
|
2741
|
+
},
|
|
2742
|
+
onMouseEnter: () => setSelectedIndex(index),
|
|
2743
|
+
children: [
|
|
2744
|
+
/* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "kbd-kbd", children: result.display }),
|
|
2745
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "kbd-lookup-labels", children: result.labels.join(", ") })
|
|
2746
|
+
]
|
|
2747
|
+
},
|
|
2748
|
+
result.binding
|
|
2749
|
+
)) }),
|
|
2750
|
+
pendingKeys.length > 0 && groupedByNextKey.size > 1 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "kbd-lookup-continuations", children: [
|
|
2751
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "kbd-lookup-continuations-label", children: "Continue with:" }),
|
|
2752
|
+
Array.from(groupedByNextKey.keys()).filter((k) => k !== "").slice(0, 8).map((nextKey) => /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "kbd-kbd kbd-small", children: nextKey }, nextKey)),
|
|
2753
|
+
groupedByNextKey.size > 9 && /* @__PURE__ */ jsxRuntime.jsx("span", { children: "..." })
|
|
2754
|
+
] })
|
|
2755
|
+
] }) });
|
|
1835
2756
|
}
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
case "meta":
|
|
1840
|
-
return CommandIcon;
|
|
1841
|
-
case "ctrl":
|
|
1842
|
-
return CtrlIcon;
|
|
1843
|
-
case "shift":
|
|
1844
|
-
return ShiftIcon;
|
|
1845
|
-
case "opt":
|
|
1846
|
-
return OptIcon;
|
|
1847
|
-
case "alt":
|
|
1848
|
-
return isMac2 ? OptIcon : AltIcon;
|
|
2757
|
+
function SeqElemBadge({ elem }) {
|
|
2758
|
+
if (elem.type === "digit") {
|
|
2759
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "kbd-placeholder", title: "Any single digit (0-9)", children: "#" });
|
|
1849
2760
|
}
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
return /* @__PURE__ */ jsxRuntime.
|
|
2761
|
+
if (elem.type === "digits") {
|
|
2762
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "kbd-placeholder", title: "One or more digits (0-9)", children: "##" });
|
|
2763
|
+
}
|
|
2764
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2765
|
+
elem.modifiers.meta && /* @__PURE__ */ jsxRuntime.jsx(ModifierIcon, { modifier: "meta", className: "kbd-modifier-icon" }),
|
|
2766
|
+
elem.modifiers.ctrl && /* @__PURE__ */ jsxRuntime.jsx(ModifierIcon, { modifier: "ctrl", className: "kbd-modifier-icon" }),
|
|
2767
|
+
elem.modifiers.alt && /* @__PURE__ */ jsxRuntime.jsx(ModifierIcon, { modifier: "alt", className: "kbd-modifier-icon" }),
|
|
2768
|
+
elem.modifiers.shift && /* @__PURE__ */ jsxRuntime.jsx(ModifierIcon, { modifier: "shift", className: "kbd-modifier-icon" }),
|
|
2769
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: formatKeyForDisplay(elem.key) })
|
|
2770
|
+
] });
|
|
1854
2771
|
}
|
|
1855
2772
|
function BindingBadge({ binding }) {
|
|
1856
|
-
const
|
|
1857
|
-
return /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "kbd-kbd", children:
|
|
2773
|
+
const keySeq = parseKeySeq(binding);
|
|
2774
|
+
return /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "kbd-kbd", children: keySeq.map((elem, i) => /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
|
|
1858
2775
|
i > 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "kbd-sequence-sep", children: " " }),
|
|
1859
|
-
|
|
1860
|
-
combo.modifiers.ctrl && /* @__PURE__ */ jsxRuntime.jsx(ModifierIcon, { modifier: "ctrl", className: "kbd-modifier-icon" }),
|
|
1861
|
-
combo.modifiers.alt && /* @__PURE__ */ jsxRuntime.jsx(ModifierIcon, { modifier: "alt", className: "kbd-modifier-icon" }),
|
|
1862
|
-
combo.modifiers.shift && /* @__PURE__ */ jsxRuntime.jsx(ModifierIcon, { modifier: "shift", className: "kbd-modifier-icon" }),
|
|
1863
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { children: combo.key.length === 1 ? combo.key.toUpperCase() : combo.key })
|
|
2776
|
+
/* @__PURE__ */ jsxRuntime.jsx(SeqElemBadge, { elem })
|
|
1864
2777
|
] }, i)) });
|
|
1865
2778
|
}
|
|
1866
2779
|
function Omnibar({
|
|
1867
2780
|
actions: actionsProp,
|
|
1868
2781
|
handlers: handlersProp,
|
|
1869
2782
|
keymap: keymapProp,
|
|
1870
|
-
|
|
1871
|
-
enabled: enabledProp,
|
|
2783
|
+
defaultBinding = "meta+k",
|
|
1872
2784
|
isOpen: isOpenProp,
|
|
1873
2785
|
onOpen: onOpenProp,
|
|
1874
2786
|
onClose: onCloseProp,
|
|
@@ -1883,7 +2795,12 @@ function Omnibar({
|
|
|
1883
2795
|
const ctx = useMaybeHotkeysContext();
|
|
1884
2796
|
const actions = actionsProp ?? ctx?.registry.actionRegistry ?? {};
|
|
1885
2797
|
const keymap = keymapProp ?? ctx?.registry.keymap ?? {};
|
|
1886
|
-
|
|
2798
|
+
useAction(ACTION_OMNIBAR, {
|
|
2799
|
+
label: "Command palette",
|
|
2800
|
+
group: "Global",
|
|
2801
|
+
defaultBindings: defaultBinding ? [defaultBinding] : [],
|
|
2802
|
+
handler: react.useCallback(() => ctx?.toggleOmnibar(), [ctx?.toggleOmnibar])
|
|
2803
|
+
});
|
|
1887
2804
|
const handleExecute = react.useCallback((actionId) => {
|
|
1888
2805
|
if (onExecuteProp) {
|
|
1889
2806
|
onExecuteProp(actionId);
|
|
@@ -1922,9 +2839,9 @@ function Omnibar({
|
|
|
1922
2839
|
actions,
|
|
1923
2840
|
handlers: handlersProp,
|
|
1924
2841
|
keymap,
|
|
1925
|
-
openKey,
|
|
1926
|
-
|
|
1927
|
-
|
|
2842
|
+
openKey: "",
|
|
2843
|
+
// Trigger is handled via useAction, not useOmnibar
|
|
2844
|
+
enabled: false,
|
|
1928
2845
|
onOpen: handleOpen,
|
|
1929
2846
|
onClose: handleClose,
|
|
1930
2847
|
onExecute: handleExecute,
|
|
@@ -1938,6 +2855,18 @@ function Omnibar({
|
|
|
1938
2855
|
});
|
|
1939
2856
|
}
|
|
1940
2857
|
}, [isOpen]);
|
|
2858
|
+
react.useEffect(() => {
|
|
2859
|
+
if (!isOpen) return;
|
|
2860
|
+
const handleGlobalKeyDown = (e) => {
|
|
2861
|
+
if (e.key === "Escape") {
|
|
2862
|
+
e.preventDefault();
|
|
2863
|
+
e.stopPropagation();
|
|
2864
|
+
close();
|
|
2865
|
+
}
|
|
2866
|
+
};
|
|
2867
|
+
document.addEventListener("keydown", handleGlobalKeyDown, true);
|
|
2868
|
+
return () => document.removeEventListener("keydown", handleGlobalKeyDown, true);
|
|
2869
|
+
}, [isOpen, close]);
|
|
1941
2870
|
const handleKeyDown = react.useCallback(
|
|
1942
2871
|
(e) => {
|
|
1943
2872
|
switch (e.key) {
|
|
@@ -2024,6 +2953,7 @@ function SequenceModal() {
|
|
|
2024
2953
|
const {
|
|
2025
2954
|
pendingKeys,
|
|
2026
2955
|
isAwaitingSequence,
|
|
2956
|
+
cancelSequence,
|
|
2027
2957
|
sequenceTimeoutStartedAt: timeoutStartedAt,
|
|
2028
2958
|
sequenceTimeout,
|
|
2029
2959
|
getCompletions,
|
|
@@ -2056,7 +2986,7 @@ function SequenceModal() {
|
|
|
2056
2986
|
if (!isAwaitingSequence || pendingKeys.length === 0) {
|
|
2057
2987
|
return null;
|
|
2058
2988
|
}
|
|
2059
|
-
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "kbd-sequence-backdrop", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "kbd-sequence", children: [
|
|
2989
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "kbd-sequence-backdrop", onClick: cancelSequence, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "kbd-sequence", onClick: (e) => e.stopPropagation(), children: [
|
|
2060
2990
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "kbd-sequence-current", children: [
|
|
2061
2991
|
/* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "kbd-sequence-keys", children: formattedPendingKeys }),
|
|
2062
2992
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "kbd-sequence-ellipsis", children: "\u2026" })
|
|
@@ -2070,7 +3000,7 @@ function SequenceModal() {
|
|
|
2070
3000
|
timeoutStartedAt
|
|
2071
3001
|
),
|
|
2072
3002
|
completions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "kbd-sequence-completions", children: Array.from(groupedCompletions.entries()).map(([nextKey, comps]) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "kbd-sequence-completion", children: [
|
|
2073
|
-
/* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "kbd-kbd", children: nextKey
|
|
3003
|
+
/* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "kbd-kbd", children: nextKey }),
|
|
2074
3004
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "kbd-sequence-arrow", children: "\u2192" }),
|
|
2075
3005
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "kbd-sequence-actions", children: comps.flatMap((c) => c.actions).map((action, i) => /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
2076
3006
|
i > 0 && ", ",
|
|
@@ -2080,6 +3010,8 @@ function SequenceModal() {
|
|
|
2080
3010
|
completions.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "kbd-sequence-empty", children: "No matching shortcuts" })
|
|
2081
3011
|
] }) });
|
|
2082
3012
|
}
|
|
3013
|
+
var DefaultTooltip = ({ children }) => /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
|
|
3014
|
+
var TooltipContext = react.createContext(DefaultTooltip);
|
|
2083
3015
|
function parseActionId(actionId) {
|
|
2084
3016
|
const colonIndex = actionId.indexOf(":");
|
|
2085
3017
|
if (colonIndex > 0) {
|
|
@@ -2087,22 +3019,47 @@ function parseActionId(actionId) {
|
|
|
2087
3019
|
}
|
|
2088
3020
|
return { group: "General", name: actionId };
|
|
2089
3021
|
}
|
|
2090
|
-
function organizeShortcuts(keymap, labels, descriptions, groupNames, groupOrder) {
|
|
3022
|
+
function organizeShortcuts(keymap, labels, descriptions, groupNames, groupOrder, actionRegistry, showUnbound = true) {
|
|
2091
3023
|
const actionBindings = getActionBindings(keymap);
|
|
2092
3024
|
const groupMap = /* @__PURE__ */ new Map();
|
|
3025
|
+
const includedActions = /* @__PURE__ */ new Set();
|
|
3026
|
+
const getGroupName = (actionId) => {
|
|
3027
|
+
const registeredGroup = actionRegistry?.[actionId]?.group;
|
|
3028
|
+
if (registeredGroup) return registeredGroup;
|
|
3029
|
+
const { group: groupKey } = parseActionId(actionId);
|
|
3030
|
+
return groupNames?.[groupKey] ?? groupKey;
|
|
3031
|
+
};
|
|
2093
3032
|
for (const [actionId, bindings] of actionBindings) {
|
|
2094
|
-
|
|
2095
|
-
const
|
|
3033
|
+
includedActions.add(actionId);
|
|
3034
|
+
const { name } = parseActionId(actionId);
|
|
3035
|
+
const groupName = getGroupName(actionId);
|
|
2096
3036
|
if (!groupMap.has(groupName)) {
|
|
2097
3037
|
groupMap.set(groupName, { name: groupName, shortcuts: [] });
|
|
2098
3038
|
}
|
|
2099
3039
|
groupMap.get(groupName).shortcuts.push({
|
|
2100
3040
|
actionId,
|
|
2101
|
-
label: labels?.[actionId] ?? name,
|
|
3041
|
+
label: labels?.[actionId] ?? actionRegistry?.[actionId]?.label ?? name,
|
|
2102
3042
|
description: descriptions?.[actionId],
|
|
2103
3043
|
bindings
|
|
2104
3044
|
});
|
|
2105
3045
|
}
|
|
3046
|
+
if (actionRegistry && showUnbound) {
|
|
3047
|
+
for (const [actionId, action] of Object.entries(actionRegistry)) {
|
|
3048
|
+
if (includedActions.has(actionId)) continue;
|
|
3049
|
+
const { name } = parseActionId(actionId);
|
|
3050
|
+
const groupName = getGroupName(actionId);
|
|
3051
|
+
if (!groupMap.has(groupName)) {
|
|
3052
|
+
groupMap.set(groupName, { name: groupName, shortcuts: [] });
|
|
3053
|
+
}
|
|
3054
|
+
groupMap.get(groupName).shortcuts.push({
|
|
3055
|
+
actionId,
|
|
3056
|
+
label: labels?.[actionId] ?? action.label ?? name,
|
|
3057
|
+
description: descriptions?.[actionId],
|
|
3058
|
+
bindings: []
|
|
3059
|
+
// No bindings
|
|
3060
|
+
});
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
2106
3063
|
for (const group of groupMap.values()) {
|
|
2107
3064
|
group.shortcuts.sort((a, b) => a.actionId.localeCompare(b.actionId));
|
|
2108
3065
|
}
|
|
@@ -2143,11 +3100,25 @@ function KeyDisplay({
|
|
|
2143
3100
|
if (modifiers.shift) {
|
|
2144
3101
|
parts.push(/* @__PURE__ */ jsxRuntime.jsx(ModifierIcon, { modifier: "shift", className: "kbd-modifier-icon" }, "shift"));
|
|
2145
3102
|
}
|
|
2146
|
-
const
|
|
2147
|
-
|
|
3103
|
+
const KeyIcon = getKeyIcon(key);
|
|
3104
|
+
if (KeyIcon) {
|
|
3105
|
+
parts.push(/* @__PURE__ */ jsxRuntime.jsx(KeyIcon, { className: "kbd-key-icon" }, "key"));
|
|
3106
|
+
} else {
|
|
3107
|
+
parts.push(/* @__PURE__ */ jsxRuntime.jsx("span", { children: formatKeyForDisplay(key) }, "key"));
|
|
3108
|
+
}
|
|
2148
3109
|
return /* @__PURE__ */ jsxRuntime.jsx("span", { className, children: parts });
|
|
2149
3110
|
}
|
|
2150
|
-
function
|
|
3111
|
+
function SeqElemDisplay2({ elem, className }) {
|
|
3112
|
+
const Tooltip = react.useContext(TooltipContext);
|
|
3113
|
+
if (elem.type === "digit") {
|
|
3114
|
+
return /* @__PURE__ */ jsxRuntime.jsx(Tooltip, { title: "Any single digit (0-9)", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: `kbd-placeholder ${className || ""}`, children: "#" }) });
|
|
3115
|
+
}
|
|
3116
|
+
if (elem.type === "digits") {
|
|
3117
|
+
return /* @__PURE__ */ jsxRuntime.jsx(Tooltip, { title: "One or more digits (0-9)", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: `kbd-placeholder ${className || ""}`, children: "##" }) });
|
|
3118
|
+
}
|
|
3119
|
+
return /* @__PURE__ */ jsxRuntime.jsx(KeyDisplay, { combo: { key: elem.key, modifiers: elem.modifiers }, className });
|
|
3120
|
+
}
|
|
3121
|
+
function BindingDisplay2({
|
|
2151
3122
|
binding,
|
|
2152
3123
|
className,
|
|
2153
3124
|
editable,
|
|
@@ -2158,10 +3129,12 @@ function BindingDisplay({
|
|
|
2158
3129
|
onEdit,
|
|
2159
3130
|
onRemove,
|
|
2160
3131
|
pendingKeys,
|
|
2161
|
-
activeKeys
|
|
3132
|
+
activeKeys,
|
|
3133
|
+
timeoutDuration = DEFAULT_SEQUENCE_TIMEOUT
|
|
2162
3134
|
}) {
|
|
2163
3135
|
const sequence = parseHotkeyString(binding);
|
|
2164
|
-
const
|
|
3136
|
+
const keySeq = parseKeySeq(binding);
|
|
3137
|
+
formatKeySeq(keySeq);
|
|
2165
3138
|
let kbdClassName = "kbd-kbd";
|
|
2166
3139
|
if (editable && !isEditing) kbdClassName += " editable";
|
|
2167
3140
|
if (isEditing) kbdClassName += " editing";
|
|
@@ -2192,7 +3165,17 @@ function BindingDisplay({
|
|
|
2192
3165
|
} else {
|
|
2193
3166
|
content = "...";
|
|
2194
3167
|
}
|
|
2195
|
-
return /* @__PURE__ */ jsxRuntime.
|
|
3168
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("kbd", { className: kbdClassName, tabIndex: editable ? 0 : void 0, children: [
|
|
3169
|
+
content,
|
|
3170
|
+
pendingKeys && pendingKeys.length > 0 && Number.isFinite(timeoutDuration) && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3171
|
+
"span",
|
|
3172
|
+
{
|
|
3173
|
+
className: "kbd-timeout-bar",
|
|
3174
|
+
style: { animationDuration: `${timeoutDuration}ms` }
|
|
3175
|
+
},
|
|
3176
|
+
pendingKeys.length
|
|
3177
|
+
)
|
|
3178
|
+
] });
|
|
2196
3179
|
}
|
|
2197
3180
|
return /* @__PURE__ */ jsxRuntime.jsxs("kbd", { className: kbdClassName, onClick: handleClick, tabIndex: editable ? 0 : void 0, onKeyDown: editable && onEdit ? (e) => {
|
|
2198
3181
|
if (e.key === "Enter" || e.key === " ") {
|
|
@@ -2200,10 +3183,13 @@ function BindingDisplay({
|
|
|
2200
3183
|
onEdit();
|
|
2201
3184
|
}
|
|
2202
3185
|
} : void 0, children: [
|
|
2203
|
-
|
|
3186
|
+
keySeq.length > 1 ? keySeq.map((elem, i) => /* @__PURE__ */ jsxRuntime.jsxs(react.Fragment, { children: [
|
|
2204
3187
|
i > 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "kbd-sequence-sep", children: " " }),
|
|
2205
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2206
|
-
] }, i)) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
3188
|
+
/* @__PURE__ */ jsxRuntime.jsx(SeqElemDisplay2, { elem })
|
|
3189
|
+
] }, i)) : keySeq.length === 1 ? /* @__PURE__ */ jsxRuntime.jsx(SeqElemDisplay2, { elem: keySeq[0] }) : (
|
|
3190
|
+
// Fallback for legacy parsing
|
|
3191
|
+
/* @__PURE__ */ jsxRuntime.jsx(KeyDisplay, { combo: sequence[0] })
|
|
3192
|
+
),
|
|
2207
3193
|
editable && onRemove && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2208
3194
|
"button",
|
|
2209
3195
|
{
|
|
@@ -2228,8 +3214,7 @@ function ShortcutsModal({
|
|
|
2228
3214
|
groupRenderers,
|
|
2229
3215
|
isOpen: isOpenProp,
|
|
2230
3216
|
onClose: onCloseProp,
|
|
2231
|
-
|
|
2232
|
-
autoRegisterOpen,
|
|
3217
|
+
defaultBinding = "?",
|
|
2233
3218
|
editable = false,
|
|
2234
3219
|
onBindingChange,
|
|
2235
3220
|
onBindingAdd,
|
|
@@ -2240,7 +3225,9 @@ function ShortcutsModal({
|
|
|
2240
3225
|
backdropClassName = "kbd-backdrop",
|
|
2241
3226
|
modalClassName = "kbd-modal",
|
|
2242
3227
|
title = "Keyboard Shortcuts",
|
|
2243
|
-
hint
|
|
3228
|
+
hint,
|
|
3229
|
+
showUnbound,
|
|
3230
|
+
TooltipComponent: TooltipComponentProp = DefaultTooltip
|
|
2244
3231
|
}) {
|
|
2245
3232
|
const ctx = useMaybeHotkeysContext();
|
|
2246
3233
|
const contextLabels = react.useMemo(() => {
|
|
@@ -2279,28 +3266,30 @@ function ShortcutsModal({
|
|
|
2279
3266
|
const descriptions = descriptionsProp ?? contextDescriptions;
|
|
2280
3267
|
const groupNames = groupNamesProp ?? contextGroups;
|
|
2281
3268
|
const handleBindingChange = onBindingChange ?? (ctx ? (action, oldKey, newKey) => {
|
|
2282
|
-
if (oldKey) ctx.registry.removeBinding(oldKey);
|
|
3269
|
+
if (oldKey) ctx.registry.removeBinding(action, oldKey);
|
|
2283
3270
|
ctx.registry.setBinding(action, newKey);
|
|
2284
3271
|
} : void 0);
|
|
2285
3272
|
const handleBindingAdd = onBindingAdd ?? (ctx ? (action, key) => {
|
|
2286
3273
|
ctx.registry.setBinding(action, key);
|
|
2287
3274
|
} : void 0);
|
|
2288
|
-
const handleBindingRemove = onBindingRemove ?? (ctx ? (
|
|
2289
|
-
ctx.registry.removeBinding(key);
|
|
3275
|
+
const handleBindingRemove = onBindingRemove ?? (ctx ? (action, key) => {
|
|
3276
|
+
ctx.registry.removeBinding(action, key);
|
|
2290
3277
|
} : void 0);
|
|
2291
3278
|
const handleReset = onReset ?? (ctx ? () => {
|
|
2292
3279
|
ctx.registry.resetOverrides();
|
|
2293
3280
|
} : void 0);
|
|
2294
|
-
const shouldAutoRegisterOpen = autoRegisterOpen ?? !ctx;
|
|
2295
3281
|
const [internalIsOpen, setInternalIsOpen] = react.useState(false);
|
|
2296
3282
|
const isOpen = isOpenProp ?? ctx?.isModalOpen ?? internalIsOpen;
|
|
2297
3283
|
const [editingAction, setEditingAction] = react.useState(null);
|
|
2298
3284
|
const [editingKey, setEditingKey] = react.useState(null);
|
|
2299
3285
|
const [addingAction, setAddingAction] = react.useState(null);
|
|
2300
3286
|
const [pendingConflict, setPendingConflict] = react.useState(null);
|
|
3287
|
+
const [hasPendingConflictState, setHasPendingConflictState] = react.useState(false);
|
|
2301
3288
|
const editingActionRef = react.useRef(null);
|
|
2302
3289
|
const editingKeyRef = react.useRef(null);
|
|
2303
3290
|
const addingActionRef = react.useRef(null);
|
|
3291
|
+
const setIsEditingBindingRef = react.useRef(ctx?.setIsEditingBinding);
|
|
3292
|
+
setIsEditingBindingRef.current = ctx?.setIsEditingBinding;
|
|
2304
3293
|
const conflicts = react.useMemo(() => findConflicts(keymap), [keymap]);
|
|
2305
3294
|
const actionBindings = react.useMemo(() => getActionBindings(keymap), [keymap]);
|
|
2306
3295
|
const close = react.useCallback(() => {
|
|
@@ -2318,13 +3307,19 @@ function ShortcutsModal({
|
|
|
2318
3307
|
ctx.closeModal();
|
|
2319
3308
|
}
|
|
2320
3309
|
}, [onCloseProp, ctx]);
|
|
2321
|
-
|
|
3310
|
+
react.useCallback(() => {
|
|
2322
3311
|
if (ctx?.openModal) {
|
|
2323
3312
|
ctx.openModal();
|
|
2324
3313
|
} else {
|
|
2325
3314
|
setInternalIsOpen(true);
|
|
2326
3315
|
}
|
|
2327
3316
|
}, [ctx]);
|
|
3317
|
+
useAction(ACTION_MODAL, {
|
|
3318
|
+
label: "Show shortcuts",
|
|
3319
|
+
group: "Global",
|
|
3320
|
+
defaultBindings: defaultBinding ? [defaultBinding] : [],
|
|
3321
|
+
handler: react.useCallback(() => ctx?.toggleModal() ?? setInternalIsOpen((prev) => !prev), [ctx?.toggleModal])
|
|
3322
|
+
});
|
|
2328
3323
|
const checkConflict = react.useCallback((newKey, forAction) => {
|
|
2329
3324
|
const existingActions = keymap[newKey];
|
|
2330
3325
|
if (!existingActions) return null;
|
|
@@ -2332,7 +3327,24 @@ function ShortcutsModal({
|
|
|
2332
3327
|
const conflicts2 = actions.filter((a) => a !== forAction);
|
|
2333
3328
|
return conflicts2.length > 0 ? conflicts2 : null;
|
|
2334
3329
|
}, [keymap]);
|
|
2335
|
-
const
|
|
3330
|
+
const combinationsEqual2 = react.useCallback((a, b) => {
|
|
3331
|
+
return a.key === b.key && a.modifiers.ctrl === b.modifiers.ctrl && a.modifiers.alt === b.modifiers.alt && a.modifiers.shift === b.modifiers.shift && a.modifiers.meta === b.modifiers.meta;
|
|
3332
|
+
}, []);
|
|
3333
|
+
const isSequencePrefix = react.useCallback((a, b) => {
|
|
3334
|
+
if (a.length >= b.length) return false;
|
|
3335
|
+
for (let i = 0; i < a.length; i++) {
|
|
3336
|
+
if (!combinationsEqual2(a[i], b[i])) return false;
|
|
3337
|
+
}
|
|
3338
|
+
return true;
|
|
3339
|
+
}, [combinationsEqual2]);
|
|
3340
|
+
const sequencesEqual = react.useCallback((a, b) => {
|
|
3341
|
+
if (a.length !== b.length) return false;
|
|
3342
|
+
for (let i = 0; i < a.length; i++) {
|
|
3343
|
+
if (!combinationsEqual2(a[i], b[i])) return false;
|
|
3344
|
+
}
|
|
3345
|
+
return true;
|
|
3346
|
+
}, [combinationsEqual2]);
|
|
3347
|
+
const { isRecording, startRecording, cancel, pendingKeys, activeKeys, sequenceTimeout } = useRecordHotkey({
|
|
2336
3348
|
onCapture: react.useCallback(
|
|
2337
3349
|
(_sequence, display) => {
|
|
2338
3350
|
const currentAddingAction = addingActionRef.current;
|
|
@@ -2360,6 +3372,7 @@ function ShortcutsModal({
|
|
|
2360
3372
|
setEditingAction(null);
|
|
2361
3373
|
setEditingKey(null);
|
|
2362
3374
|
setAddingAction(null);
|
|
3375
|
+
setIsEditingBindingRef.current?.(false);
|
|
2363
3376
|
},
|
|
2364
3377
|
[checkConflict, handleBindingChange, handleBindingAdd]
|
|
2365
3378
|
),
|
|
@@ -2371,6 +3384,7 @@ function ShortcutsModal({
|
|
|
2371
3384
|
setEditingKey(null);
|
|
2372
3385
|
setAddingAction(null);
|
|
2373
3386
|
setPendingConflict(null);
|
|
3387
|
+
setIsEditingBindingRef.current?.(false);
|
|
2374
3388
|
}, []),
|
|
2375
3389
|
// Tab to next/prev editable kbd and start editing
|
|
2376
3390
|
onTab: react.useCallback(() => {
|
|
@@ -2395,7 +3409,7 @@ function ShortcutsModal({
|
|
|
2395
3409
|
prev.click();
|
|
2396
3410
|
}
|
|
2397
3411
|
}, []),
|
|
2398
|
-
pauseTimeout: pendingConflict !== null
|
|
3412
|
+
pauseTimeout: pendingConflict !== null || hasPendingConflictState
|
|
2399
3413
|
});
|
|
2400
3414
|
const startEditingBinding = react.useCallback(
|
|
2401
3415
|
(action, key) => {
|
|
@@ -2406,9 +3420,10 @@ function ShortcutsModal({
|
|
|
2406
3420
|
setEditingAction(action);
|
|
2407
3421
|
setEditingKey(key);
|
|
2408
3422
|
setPendingConflict(null);
|
|
3423
|
+
ctx?.setIsEditingBinding(true);
|
|
2409
3424
|
startRecording();
|
|
2410
3425
|
},
|
|
2411
|
-
[startRecording]
|
|
3426
|
+
[startRecording, ctx?.setIsEditingBinding]
|
|
2412
3427
|
);
|
|
2413
3428
|
const startAddingBinding = react.useCallback(
|
|
2414
3429
|
(action) => {
|
|
@@ -2419,9 +3434,10 @@ function ShortcutsModal({
|
|
|
2419
3434
|
setEditingKey(null);
|
|
2420
3435
|
setAddingAction(action);
|
|
2421
3436
|
setPendingConflict(null);
|
|
3437
|
+
ctx?.setIsEditingBinding(true);
|
|
2422
3438
|
startRecording();
|
|
2423
3439
|
},
|
|
2424
|
-
[startRecording]
|
|
3440
|
+
[startRecording, ctx?.setIsEditingBinding]
|
|
2425
3441
|
);
|
|
2426
3442
|
const startEditing = react.useCallback(
|
|
2427
3443
|
(action, bindingIndex) => {
|
|
@@ -2443,7 +3459,8 @@ function ShortcutsModal({
|
|
|
2443
3459
|
setEditingKey(null);
|
|
2444
3460
|
setAddingAction(null);
|
|
2445
3461
|
setPendingConflict(null);
|
|
2446
|
-
|
|
3462
|
+
ctx?.setIsEditingBinding(false);
|
|
3463
|
+
}, [cancel, ctx?.setIsEditingBinding]);
|
|
2447
3464
|
const removeBinding = react.useCallback(
|
|
2448
3465
|
(action, key) => {
|
|
2449
3466
|
handleBindingRemove?.(action, key);
|
|
@@ -2453,6 +3470,31 @@ function ShortcutsModal({
|
|
|
2453
3470
|
const reset = react.useCallback(() => {
|
|
2454
3471
|
handleReset?.();
|
|
2455
3472
|
}, [handleReset]);
|
|
3473
|
+
const pendingConflictInfo = react.useMemo(() => {
|
|
3474
|
+
if (!isRecording || pendingKeys.length === 0) {
|
|
3475
|
+
return { hasConflict: false, conflictingKeys: /* @__PURE__ */ new Set() };
|
|
3476
|
+
}
|
|
3477
|
+
const conflictingKeys = /* @__PURE__ */ new Set();
|
|
3478
|
+
for (const key of Object.keys(keymap)) {
|
|
3479
|
+
if (editingKey && key.toLowerCase() === editingKey.toLowerCase()) continue;
|
|
3480
|
+
const keySequence = parseHotkeyString(key);
|
|
3481
|
+
if (sequencesEqual(pendingKeys, keySequence)) {
|
|
3482
|
+
conflictingKeys.add(key);
|
|
3483
|
+
continue;
|
|
3484
|
+
}
|
|
3485
|
+
if (isSequencePrefix(pendingKeys, keySequence)) {
|
|
3486
|
+
conflictingKeys.add(key);
|
|
3487
|
+
continue;
|
|
3488
|
+
}
|
|
3489
|
+
if (isSequencePrefix(keySequence, pendingKeys)) {
|
|
3490
|
+
conflictingKeys.add(key);
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
return { hasConflict: conflictingKeys.size > 0, conflictingKeys };
|
|
3494
|
+
}, [isRecording, pendingKeys, keymap, editingKey, sequencesEqual, isSequencePrefix]);
|
|
3495
|
+
react.useEffect(() => {
|
|
3496
|
+
setHasPendingConflictState(pendingConflictInfo.hasConflict);
|
|
3497
|
+
}, [pendingConflictInfo.hasConflict]);
|
|
2456
3498
|
const renderEditableKbd = react.useCallback(
|
|
2457
3499
|
(actionId, key, showRemove = false) => {
|
|
2458
3500
|
const isEditingThis = editingAction === actionId && editingKey === key && !addingAction;
|
|
@@ -2464,13 +3506,15 @@ function ShortcutsModal({
|
|
|
2464
3506
|
const defaultActions = Array.isArray(defaultAction) ? defaultAction : [defaultAction];
|
|
2465
3507
|
return defaultActions.includes(actionId);
|
|
2466
3508
|
})() : true;
|
|
3509
|
+
const isPendingConflict = pendingConflictInfo.conflictingKeys.has(key);
|
|
2467
3510
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2468
|
-
|
|
3511
|
+
BindingDisplay2,
|
|
2469
3512
|
{
|
|
2470
3513
|
binding: key,
|
|
2471
3514
|
editable,
|
|
2472
3515
|
isEditing: isEditingThis,
|
|
2473
3516
|
isConflict,
|
|
3517
|
+
isPendingConflict,
|
|
2474
3518
|
isDefault,
|
|
2475
3519
|
onEdit: () => {
|
|
2476
3520
|
if (isRecording && !(editingAction === actionId && editingKey === key)) {
|
|
@@ -2491,24 +3535,27 @@ function ShortcutsModal({
|
|
|
2491
3535
|
},
|
|
2492
3536
|
onRemove: editable && showRemove ? () => removeBinding(actionId, key) : void 0,
|
|
2493
3537
|
pendingKeys,
|
|
2494
|
-
activeKeys
|
|
3538
|
+
activeKeys,
|
|
3539
|
+
timeoutDuration: pendingConflictInfo.hasConflict ? Infinity : sequenceTimeout
|
|
2495
3540
|
},
|
|
2496
3541
|
key
|
|
2497
3542
|
);
|
|
2498
3543
|
},
|
|
2499
|
-
[editingAction, editingKey, addingAction, conflicts, defaults, editable, startEditingBinding, removeBinding, pendingKeys, activeKeys, isRecording, cancel, handleBindingAdd, handleBindingChange]
|
|
3544
|
+
[editingAction, editingKey, addingAction, conflicts, defaults, editable, startEditingBinding, removeBinding, pendingKeys, activeKeys, isRecording, cancel, handleBindingAdd, handleBindingChange, sequenceTimeout, pendingConflictInfo]
|
|
2500
3545
|
);
|
|
2501
3546
|
const renderAddButton = react.useCallback(
|
|
2502
3547
|
(actionId) => {
|
|
2503
3548
|
const isAddingThis = addingAction === actionId;
|
|
2504
3549
|
if (isAddingThis) {
|
|
2505
3550
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2506
|
-
|
|
3551
|
+
BindingDisplay2,
|
|
2507
3552
|
{
|
|
2508
3553
|
binding: "",
|
|
2509
3554
|
isEditing: true,
|
|
3555
|
+
isPendingConflict: pendingConflictInfo.hasConflict,
|
|
2510
3556
|
pendingKeys,
|
|
2511
|
-
activeKeys
|
|
3557
|
+
activeKeys,
|
|
3558
|
+
timeoutDuration: pendingConflictInfo.hasConflict ? Infinity : sequenceTimeout
|
|
2512
3559
|
}
|
|
2513
3560
|
);
|
|
2514
3561
|
}
|
|
@@ -2537,13 +3584,14 @@ function ShortcutsModal({
|
|
|
2537
3584
|
}
|
|
2538
3585
|
);
|
|
2539
3586
|
},
|
|
2540
|
-
[addingAction, pendingKeys, activeKeys, startAddingBinding, isRecording, cancel, handleBindingAdd, handleBindingChange]
|
|
3587
|
+
[addingAction, pendingKeys, activeKeys, startAddingBinding, isRecording, cancel, handleBindingAdd, handleBindingChange, sequenceTimeout, pendingConflictInfo]
|
|
2541
3588
|
);
|
|
2542
3589
|
const renderCell = react.useCallback(
|
|
2543
3590
|
(actionId, keys) => {
|
|
3591
|
+
const showAddButton = editable && (multipleBindings || keys.length === 0);
|
|
2544
3592
|
return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "kbd-action-bindings", children: [
|
|
2545
3593
|
keys.map((key) => /* @__PURE__ */ jsxRuntime.jsx(react.Fragment, { children: renderEditableKbd(actionId, key, true) }, key)),
|
|
2546
|
-
|
|
3594
|
+
showAddButton && renderAddButton(actionId)
|
|
2547
3595
|
] });
|
|
2548
3596
|
},
|
|
2549
3597
|
[renderEditableKbd, renderAddButton, editable, multipleBindings]
|
|
@@ -2560,14 +3608,10 @@ function ShortcutsModal({
|
|
|
2560
3608
|
editingKey,
|
|
2561
3609
|
addingAction
|
|
2562
3610
|
}), [renderCell, renderEditableKbd, renderAddButton, startEditingBinding, startAddingBinding, removeBinding, isRecording, editingAction, editingKey, addingAction]);
|
|
2563
|
-
const modalKeymap = shouldAutoRegisterOpen ? { [openKey]: "openShortcuts" } : {};
|
|
2564
3611
|
useHotkeys(
|
|
2565
|
-
{
|
|
2566
|
-
{
|
|
2567
|
-
|
|
2568
|
-
closeShortcuts: close
|
|
2569
|
-
},
|
|
2570
|
-
{ enabled: shouldAutoRegisterOpen || isOpen }
|
|
3612
|
+
{ escape: "closeShortcuts" },
|
|
3613
|
+
{ closeShortcuts: close },
|
|
3614
|
+
{ enabled: isOpen }
|
|
2571
3615
|
);
|
|
2572
3616
|
react.useEffect(() => {
|
|
2573
3617
|
if (!isOpen || !editingAction && !addingAction) return;
|
|
@@ -2581,6 +3625,19 @@ function ShortcutsModal({
|
|
|
2581
3625
|
window.addEventListener("keydown", handleEscape, true);
|
|
2582
3626
|
return () => window.removeEventListener("keydown", handleEscape, true);
|
|
2583
3627
|
}, [isOpen, editingAction, addingAction, cancelEditing]);
|
|
3628
|
+
react.useEffect(() => {
|
|
3629
|
+
if (!isOpen || !ctx) return;
|
|
3630
|
+
const handleMetaK = (e) => {
|
|
3631
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
|
3632
|
+
e.preventDefault();
|
|
3633
|
+
e.stopPropagation();
|
|
3634
|
+
close();
|
|
3635
|
+
ctx.openOmnibar();
|
|
3636
|
+
}
|
|
3637
|
+
};
|
|
3638
|
+
window.addEventListener("keydown", handleMetaK, true);
|
|
3639
|
+
return () => window.removeEventListener("keydown", handleMetaK, true);
|
|
3640
|
+
}, [isOpen, ctx, close]);
|
|
2584
3641
|
const handleBackdropClick = react.useCallback(
|
|
2585
3642
|
(e) => {
|
|
2586
3643
|
if (e.target === e.currentTarget) {
|
|
@@ -2600,9 +3657,10 @@ function ShortcutsModal({
|
|
|
2600
3657
|
},
|
|
2601
3658
|
[editingAction, addingAction, cancelEditing]
|
|
2602
3659
|
);
|
|
3660
|
+
const effectiveShowUnbound = showUnbound ?? editable;
|
|
2603
3661
|
const shortcutGroups = react.useMemo(
|
|
2604
|
-
() => organizeShortcuts(keymap, labels, descriptions, groupNames, groupOrder),
|
|
2605
|
-
[keymap, labels, descriptions, groupNames, groupOrder]
|
|
3662
|
+
() => organizeShortcuts(keymap, labels, descriptions, groupNames, groupOrder, ctx?.registry.actionRegistry, effectiveShowUnbound),
|
|
3663
|
+
[keymap, labels, descriptions, groupNames, groupOrder, ctx?.registry.actionRegistry, effectiveShowUnbound]
|
|
2606
3664
|
);
|
|
2607
3665
|
if (!isOpen) return null;
|
|
2608
3666
|
if (children) {
|
|
@@ -2632,7 +3690,7 @@ function ShortcutsModal({
|
|
|
2632
3690
|
renderCell(actionId, bindings)
|
|
2633
3691
|
] }, actionId));
|
|
2634
3692
|
};
|
|
2635
|
-
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: backdropClassName, onClick: handleBackdropClick, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: modalClassName, role: "dialog", "aria-modal": "true", "aria-label": "Keyboard shortcuts", onClick: handleModalClick, children: [
|
|
3693
|
+
return /* @__PURE__ */ jsxRuntime.jsx(TooltipContext.Provider, { value: TooltipComponentProp, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: backdropClassName, onClick: handleBackdropClick, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: modalClassName, role: "dialog", "aria-modal": "true", "aria-label": "Keyboard shortcuts", onClick: handleModalClick, children: [
|
|
2636
3694
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "kbd-modal-header", children: [
|
|
2637
3695
|
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "kbd-modal-title", children: title }),
|
|
2638
3696
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "kbd-modal-header-buttons", children: [
|
|
@@ -2701,37 +3759,68 @@ function ShortcutsModal({
|
|
|
2701
3759
|
)
|
|
2702
3760
|
] })
|
|
2703
3761
|
] })
|
|
2704
|
-
] }) });
|
|
3762
|
+
] }) }) });
|
|
2705
3763
|
}
|
|
2706
3764
|
|
|
3765
|
+
exports.ACTION_LOOKUP = ACTION_LOOKUP;
|
|
3766
|
+
exports.ACTION_MODAL = ACTION_MODAL;
|
|
3767
|
+
exports.ACTION_OMNIBAR = ACTION_OMNIBAR;
|
|
2707
3768
|
exports.ActionsRegistryContext = ActionsRegistryContext;
|
|
2708
|
-
exports.
|
|
2709
|
-
exports.
|
|
2710
|
-
exports.
|
|
3769
|
+
exports.Alt = Alt;
|
|
3770
|
+
exports.Backspace = Backspace;
|
|
3771
|
+
exports.Command = Command;
|
|
3772
|
+
exports.Ctrl = Ctrl;
|
|
3773
|
+
exports.DEFAULT_SEQUENCE_TIMEOUT = DEFAULT_SEQUENCE_TIMEOUT;
|
|
3774
|
+
exports.DIGITS_PLACEHOLDER = DIGITS_PLACEHOLDER;
|
|
3775
|
+
exports.DIGIT_PLACEHOLDER = DIGIT_PLACEHOLDER;
|
|
3776
|
+
exports.Down = Down;
|
|
3777
|
+
exports.Enter = Enter;
|
|
2711
3778
|
exports.HotkeysProvider = HotkeysProvider;
|
|
3779
|
+
exports.Kbd = Kbd;
|
|
3780
|
+
exports.KbdLookup = KbdLookup;
|
|
3781
|
+
exports.KbdModal = KbdModal;
|
|
3782
|
+
exports.KbdOmnibar = KbdOmnibar;
|
|
3783
|
+
exports.Kbds = Kbds;
|
|
3784
|
+
exports.Key = Key;
|
|
2712
3785
|
exports.KeybindingEditor = KeybindingEditor;
|
|
3786
|
+
exports.Left = Left;
|
|
3787
|
+
exports.LookupModal = LookupModal;
|
|
2713
3788
|
exports.ModifierIcon = ModifierIcon;
|
|
2714
3789
|
exports.Omnibar = Omnibar;
|
|
2715
|
-
exports.
|
|
3790
|
+
exports.Option = Option;
|
|
3791
|
+
exports.Right = Right;
|
|
2716
3792
|
exports.SequenceModal = SequenceModal;
|
|
2717
|
-
exports.
|
|
3793
|
+
exports.Shift = Shift;
|
|
2718
3794
|
exports.ShortcutsModal = ShortcutsModal;
|
|
3795
|
+
exports.Up = Up;
|
|
3796
|
+
exports.countPlaceholders = countPlaceholders;
|
|
2719
3797
|
exports.createTwoColumnRenderer = createTwoColumnRenderer;
|
|
3798
|
+
exports.extractCaptures = extractCaptures;
|
|
2720
3799
|
exports.findConflicts = findConflicts;
|
|
3800
|
+
exports.formatBinding = formatBinding;
|
|
2721
3801
|
exports.formatCombination = formatCombination;
|
|
2722
3802
|
exports.formatKeyForDisplay = formatKeyForDisplay;
|
|
3803
|
+
exports.formatKeySeq = formatKeySeq;
|
|
2723
3804
|
exports.fuzzyMatch = fuzzyMatch;
|
|
2724
3805
|
exports.getActionBindings = getActionBindings;
|
|
2725
3806
|
exports.getConflictsArray = getConflictsArray;
|
|
3807
|
+
exports.getKeyIcon = getKeyIcon;
|
|
2726
3808
|
exports.getModifierIcon = getModifierIcon;
|
|
2727
3809
|
exports.getSequenceCompletions = getSequenceCompletions;
|
|
2728
3810
|
exports.hasConflicts = hasConflicts;
|
|
3811
|
+
exports.hasDigitPlaceholders = hasDigitPlaceholders;
|
|
3812
|
+
exports.hotkeySequenceToKeySeq = hotkeySequenceToKeySeq;
|
|
3813
|
+
exports.isDigitPlaceholder = isDigitPlaceholder;
|
|
2729
3814
|
exports.isMac = isMac;
|
|
2730
3815
|
exports.isModifierKey = isModifierKey;
|
|
3816
|
+
exports.isPlaceholderSentinel = isPlaceholderSentinel;
|
|
2731
3817
|
exports.isSequence = isSequence;
|
|
3818
|
+
exports.isShiftedSymbol = isShiftedSymbol;
|
|
3819
|
+
exports.keySeqToHotkeySequence = keySeqToHotkeySequence;
|
|
2732
3820
|
exports.normalizeKey = normalizeKey;
|
|
2733
3821
|
exports.parseCombinationId = parseCombinationId;
|
|
2734
3822
|
exports.parseHotkeyString = parseHotkeyString;
|
|
3823
|
+
exports.parseKeySeq = parseKeySeq;
|
|
2735
3824
|
exports.searchActions = searchActions;
|
|
2736
3825
|
exports.useAction = useAction;
|
|
2737
3826
|
exports.useActions = useActions;
|