import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Kbd, KbdGroup } from "@/components/ui/kbd"; import { usePreferencesStore } from "@/modules/settings/preferences"; import { setShortcuts } from "@/modules/settings/store"; import { getBindingTokens, SHORTCUTS, SHORTCUT_GROUPS, type KeyBinding, type Shortcut, type ShortcutId, } from "@/modules/shortcuts/shortcuts"; import { ArrowTurnBackwardIcon, Search01Icon, Delete02Icon, } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import { useEffect, useState, useMemo } from "react"; import { SectionHeader } from "../components/SectionHeader"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; export function ShortcutsSection() { const userShortcuts = usePreferencesStore((s) => s.shortcuts); const [search, setSearch] = useState(""); const [recordingId, setRecordingId] = useState(null); const [resetDialogOpen, setResetDialogOpen] = useState(false); const filteredShortcuts = useMemo(() => { // Filter out internal/non-overridable shortcuts like tab.selectByIndex const base = SHORTCUTS.filter((s) => s.id !== "tab.selectByIndex"); if (!search) return base; const lower = search.toLowerCase(); return base.filter( (s) => s.label.toLowerCase().includes(lower) || s.group.toLowerCase().includes(lower) ); }, [search]); const onRecord = (id: ShortcutId, binding: KeyBinding) => { const next = { ...userShortcuts, [id]: [binding] }; void setShortcuts(next); setRecordingId(null); }; const onClear = (id: ShortcutId) => { const next = { ...userShortcuts, [id]: [] }; void setShortcuts(next); }; const onResetShortcut = (id: ShortcutId) => { const next = { ...userShortcuts }; delete next[id]; void setShortcuts(next); }; const onResetAll = () => { void setShortcuts({}); setResetDialogOpen(false); }; return (
setSearch(e.target.value)} className="h-9 pl-9 text-[12.5px]" />
{SHORTCUT_GROUPS.map((group) => { const items = filteredShortcuts.filter((s) => s.group === group); if (items.length === 0) return null; return (

{group}

{items.map((s) => ( setRecordingId(s.id)} onStopRecording={() => setRecordingId(null)} onRecord={(b) => onRecord(s.id, b)} onClear={() => onClear(s.id)} onReset={() => onResetShortcut(s.id)} userBindings={userShortcuts[s.id]} /> ))}
); })}
Reset all shortcuts? This will revert all your custom keyboard shortcuts to their factory defaults. This action cannot be undone. Cancel Reset All
); } function ShortcutRow({ shortcut, isRecording, onStartRecording, onStopRecording, onRecord, onClear, onReset, userBindings, }: { shortcut: Shortcut; isRecording: boolean; onStartRecording: () => void; onStopRecording: () => void; onRecord: (b: KeyBinding) => void; onClear: () => void; onReset: () => void; userBindings?: KeyBinding[]; }) { const bindings = userBindings !== undefined ? userBindings : shortcut.defaultBindings; const isModified = userBindings !== undefined; const hasBindings = bindings && bindings.length > 0; return (
{shortcut.label}
{isRecording ? ( ) : ( <>
{hasBindings ? ( {getBindingTokens(bindings[0]).map((t, i) => ( {t} ))} ) : ( Unassigned )}
{isModified && ( )}
)}
); } function Recorder({ onRecord, onCancel, }: { onRecord: (b: KeyBinding) => void; onCancel: () => void; }) { const [_mods, setMods] = useState({ ctrl: false, shift: false, alt: false, meta: false, }); useEffect(() => { const onDown = (e: KeyboardEvent) => { e.preventDefault(); e.stopPropagation(); if (e.key === "Escape") { onCancel(); return; } const isMod = ["Control", "Shift", "Alt", "Meta"].includes(e.key); if (isMod) { setMods({ ctrl: e.ctrlKey, shift: e.shiftKey, alt: e.altKey, meta: e.metaKey, }); return; } // Require at least one primary modifier (Ctrl, Alt, Meta). // Reject Shift‑only shortcuts that would insert a character. const hasPrimaryModifier = e.ctrlKey || e.altKey || e.metaKey; const isCharacterKey = e.key.length === 1; // anything that types a glyph // this blocks shortcuts such as Shift+2 which would be "@" and Shift+, which would be "<" on many layouts if (!hasPrimaryModifier && (!e.shiftKey || isCharacterKey)) { return; } onRecord({ key: e.key, ctrl: e.ctrlKey, shift: e.shiftKey, alt: e.altKey, meta: e.metaKey, }); }; const onUp = (e: KeyboardEvent) => { const isMod = ["Control", "Shift", "Alt", "Meta"].includes(e.key); if (isMod) { setMods({ ctrl: e.ctrlKey, shift: e.shiftKey, alt: e.altKey, meta: e.metaKey, }); } }; window.addEventListener("keydown", onDown, { capture: true }); window.addEventListener("keyup", onUp, { capture: true }); return () => { window.removeEventListener("keydown", onDown, { capture: true }); window.removeEventListener("keyup", onUp, { capture: true }); }; }, [onRecord, onCancel]); return (
Recording... (Esc to cancel)
); }