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