import { useEffect, useRef } from "react"; // Keyboard shortcuts (SPEC ยง9.4; the list users see is lib/shortcuts.ts). // Ignores keystrokes while typing in inputs, except Escape. "g" starts a // two-key sequence (g i = Inbox). With `enabled` false only Escape works. export interface Shortcuts { enabled?: boolean; onSearch?: () => void; onCompose?: () => void; onReply?: () => void; onReplyAll?: () => void; onForward?: () => void; onArchive?: () => void; onTrash?: () => void; onTogglePin?: () => void; onSnooze?: () => void; onMarkRead?: () => void; onMarkUnread?: () => void; onBack?: () => void; onGoInbox?: () => void; onGoSnoozed?: () => void; onGoDrafts?: () => void; onGoSent?: () => void; onGoSubscriptions?: () => void; onHelp?: () => void; onEscape?: () => void; onNext?: () => void; onPrev?: () => void; } function isTyping(el: EventTarget | null): boolean { if (!(el instanceof HTMLElement)) return false; const tag = el.tagName; return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || el.isContentEditable; } export function useKeyboardShortcuts(shortcuts: Shortcuts): void { const ref = useRef(shortcuts); ref.current = shortcuts; const pendingG = useRef(false); useEffect(() => { const handler = (e: KeyboardEvent) => { const s = ref.current; if (e.key === "Escape") { s.onEscape?.(); return; } if (s.enabled === false || isTyping(e.target)) return; if (e.metaKey || e.ctrlKey || e.altKey) return; const run = (fn: (() => void) | undefined) => { if (!fn) return; e.preventDefault(); fn(); }; if (pendingG.current) { pendingG.current = false; const go: Record void) | undefined> = { i: s.onGoInbox, z: s.onGoSnoozed, d: s.onGoDrafts, t: s.onGoSent, b: s.onGoSubscriptions, }; if (e.key in go) { run(go[e.key]); return; } } // Shift+letter arrives as the capital letter. const actions: Record void) | undefined> = { "/": s.onSearch, "?": s.onHelp, c: s.onCompose, r: s.onReply, a: s.onReplyAll, f: s.onForward, e: s.onArchive, "#": s.onTrash, s: s.onTogglePin, b: s.onSnooze, I: s.onMarkRead, U: s.onMarkUnread, u: s.onBack, j: s.onNext, k: s.onPrev, }; if (e.key === "g") { pendingG.current = true; window.setTimeout(() => (pendingG.current = false), 1000); return; } if (e.key in actions) run(actions[e.key]); }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); }, []); }