()
| 22 | * Must be used inside a CommandRegistryProvider. |
| 23 | */ |
| 24 | export function useKeyboardShortcuts() { |
| 25 | const { commandsRef } = useCommandRegistry(); |
| 26 | const pendingSequenceRef = useRef<string | null>(null); |
| 27 | const sequenceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); |
| 28 | |
| 29 | useEffect(() => { |
| 30 | const clearSequence = () => { |
| 31 | pendingSequenceRef.current = null; |
| 32 | if (sequenceTimerRef.current) { |
| 33 | clearTimeout(sequenceTimerRef.current); |
| 34 | sequenceTimerRef.current = null; |
| 35 | } |
| 36 | }; |
| 37 | |
| 38 | const handler = (e: KeyboardEvent) => { |
| 39 | // Ignore bare modifier keypresses |
| 40 | if (["Meta", "Control", "Shift", "Alt"].includes(e.key)) return; |
| 41 | |
| 42 | const inInput = isTypingTarget(e.target); |
| 43 | const commands = commandsRef.current; |
| 44 | |
| 45 | // --- Sequence matching (e.g. "g" then "d") --- |
| 46 | if (pendingSequenceRef.current) { |
| 47 | const seq = `${pendingSequenceRef.current} ${e.key.toLowerCase()}`; |
| 48 | const match = commands.find( |
| 49 | (cmd) => |
| 50 | (!inInput || cmd.global) && |
| 51 | (!cmd.when || cmd.when()) && |
| 52 | cmd.keys.includes(seq) |
| 53 | ); |
| 54 | clearSequence(); |
| 55 | if (match) { |
| 56 | e.preventDefault(); |
| 57 | match.action(); |
| 58 | return; |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | // --- Single combo matching --- |
| 63 | const singleMatch = commands.find((cmd) => { |
| 64 | if (inInput && !cmd.global) return false; |
| 65 | if (cmd.when && !cmd.when()) return false; |
| 66 | return cmd.keys.some((k) => { |
| 67 | // Sequence keys contain a space; skip them in the single pass |
| 68 | if (k.includes(" ")) return false; |
| 69 | return matchesEvent(parseKey(k), e); |
| 70 | }); |
| 71 | }); |
| 72 | |
| 73 | if (singleMatch) { |
| 74 | e.preventDefault(); |
| 75 | singleMatch.action(); |
| 76 | return; |
| 77 | } |
| 78 | |
| 79 | // --- Start-of-sequence detection (single bare key that starts a sequence) --- |
| 80 | // Only when not in an input and no modifier held |
| 81 | if (!inInput && !e.metaKey && !e.ctrlKey && !e.altKey) { |
nothing calls this directly
no test coverage detected