From 901ddb509db23695cf9d71bf733e6a7185317960 Mon Sep 17 00:00:00 2001 From: Kha Nguyen Date: Tue, 22 Jul 2025 18:52:49 -0500 Subject: [PATCH 1/8] fix: prevent action to perform on key press when edit keybindings --- .../src/components/keyboard-shortcuts-help.tsx | 10 +++++++--- apps/web/src/hooks/use-keybindings.ts | 6 ++++-- apps/web/src/stores/keybindings-store.ts | 16 +++++++++++----- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/apps/web/src/components/keyboard-shortcuts-help.tsx b/apps/web/src/components/keyboard-shortcuts-help.tsx index 4c8bfff5..65f879a3 100644 --- a/apps/web/src/components/keyboard-shortcuts-help.tsx +++ b/apps/web/src/components/keyboard-shortcuts-help.tsx @@ -144,6 +144,7 @@ export const KeyboardShortcutsHelp = () => { getKeybindingString, validateKeybinding, getKeybindingsForAction, + setIsRecording, } = useKeybindingsStore(); // Get shortcuts from centralized hook @@ -163,11 +164,11 @@ export const KeyboardShortcutsHelp = () => { // Auto-save the new keybinding const conflict = validateKeybinding( keyString, - recordingShortcut.action + recordingShortcut.action, ); if (conflict) { toast.error( - `Key "${keyString}" is already bound to "${conflict.existingAction}"` + `Key "${keyString}" is already bound to "${conflict.existingAction}"`, ); setRecordingKey(null); setRecordingShortcut(null); @@ -181,14 +182,16 @@ export const KeyboardShortcutsHelp = () => { // Add new keybinding updateKeybinding(keyString, recordingShortcut.action); + setIsRecording(false); setRecordingKey(null); setRecordingShortcut(null); } }; - const handleClickOutside = (e: MouseEvent) => { + const handleClickOutside = () => { setRecordingKey(null); setRecordingShortcut(null); + setIsRecording(false); }; document.addEventListener("keydown", handleKeyDown); @@ -211,6 +214,7 @@ export const KeyboardShortcutsHelp = () => { const handleStartRecording = (keyId: string, shortcut: KeyboardShortcut) => { setRecordingKey(keyId); setRecordingShortcut(shortcut); + setIsRecording(true); }; return ( diff --git a/apps/web/src/hooks/use-keybindings.ts b/apps/web/src/hooks/use-keybindings.ts index 7887f3f1..0e2045ac 100644 --- a/apps/web/src/hooks/use-keybindings.ts +++ b/apps/web/src/hooks/use-keybindings.ts @@ -8,13 +8,15 @@ import { useKeybindingsStore } from "@/stores/keybindings-store"; * the appropriate actions based on keybindings */ export function useKeybindingsListener() { - const { keybindings, getKeybindingString, keybindingsEnabled } = + const { keybindings, getKeybindingString, keybindingsEnabled, isRecording } = useKeybindingsStore(); useEffect(() => { const handleKeyDown = (ev: KeyboardEvent) => { // Do not check keybinds if the mode is disabled if (!keybindingsEnabled) return; + // ignore key events if user is changing keybindings + if (isRecording) return; const binding = getKeybindingString(ev); if (!binding) return; @@ -45,7 +47,7 @@ export function useKeybindingsListener() { return () => { document.removeEventListener("keydown", handleKeyDown); }; - }, [keybindings, getKeybindingString, keybindingsEnabled]); + }, [keybindings, getKeybindingString, keybindingsEnabled, isRecording]); } /** diff --git a/apps/web/src/stores/keybindings-store.ts b/apps/web/src/stores/keybindings-store.ts index 3e525b86..ce2d839d 100644 --- a/apps/web/src/stores/keybindings-store.ts +++ b/apps/web/src/stores/keybindings-store.ts @@ -39,6 +39,7 @@ interface KeybindingsState { keybindings: KeybindingConfig; isCustomized: boolean; keybindingsEnabled: boolean; + isRecording: boolean; // Actions updateKeybinding: (key: ShortcutKey, action: ActionWithOptionalArgs) => void; @@ -48,11 +49,12 @@ interface KeybindingsState { exportKeybindings: () => KeybindingConfig; enableKeybindings: () => void; disableKeybindings: () => void; + setIsRecording: (isRecording: boolean) => void; // Validation validateKeybinding: ( key: ShortcutKey, - action: ActionWithOptionalArgs + action: ActionWithOptionalArgs, ) => KeybindingConflict | null; getKeybindingsForAction: (action: ActionWithOptionalArgs) => ShortcutKey[]; @@ -66,6 +68,7 @@ export const useKeybindingsStore = create()( keybindings: { ...defaultKeybindings }, isCustomized: false, keybindingsEnabled: true, + isRecording: false, updateKeybinding: (key: ShortcutKey, action: ActionWithOptionalArgs) => { set((state) => { @@ -126,7 +129,7 @@ export const useKeybindingsStore = create()( validateKeybinding: ( key: ShortcutKey, - action: ActionWithOptionalArgs + action: ActionWithOptionalArgs, ) => { const { keybindings } = get(); const existingAction = keybindings[key]; @@ -141,11 +144,14 @@ export const useKeybindingsStore = create()( return null; }, + setIsRecording: (isRecording: boolean) => { + set({ isRecording }); + }, getKeybindingsForAction: (action: ActionWithOptionalArgs) => { const { keybindings } = get(); return Object.keys(keybindings).filter( - (key) => keybindings[key as ShortcutKey] === action + (key) => keybindings[key as ShortcutKey] === action, ) as ShortcutKey[]; }, @@ -156,8 +162,8 @@ export const useKeybindingsStore = create()( { name: "opencut-keybindings", version: 1, - } - ) + }, + ), ); // Utility functions From a434459a9c0b88c158c3d7470740d90a263f29f2 Mon Sep 17 00:00:00 2001 From: Kha Nguyen Date: Tue, 22 Jul 2025 18:53:43 -0500 Subject: [PATCH 2/8] display key based on OS --- apps/web/src/hooks/use-keyboard-shortcuts-help.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/web/src/hooks/use-keyboard-shortcuts-help.ts b/apps/web/src/hooks/use-keyboard-shortcuts-help.ts index c7de8466..808bf25f 100644 --- a/apps/web/src/hooks/use-keyboard-shortcuts-help.ts +++ b/apps/web/src/hooks/use-keyboard-shortcuts-help.ts @@ -3,6 +3,7 @@ import { useMemo } from "react"; import { useKeybindingsStore } from "@/stores/keybindings-store"; import { Action } from "@/constants/actions"; +import { getPlatformAlternateKey, getPlatformSpecialKey } from "@/lib/utils"; export interface KeyboardShortcut { id: string; @@ -67,8 +68,8 @@ const actionDescriptions: Record< // Convert key binding format to display format const formatKey = (key: string): string => { return key - .replace("ctrl", "Cmd") - .replace("alt", "Alt") + .replace("ctrl", getPlatformSpecialKey()) + .replace("alt", getPlatformAlternateKey()) .replace("shift", "Shift") .replace("left", "ArrowLeft") .replace("right", "ArrowRight") From f3d5eb9d08ed681977a4dd06fcacb0407be17384 Mon Sep 17 00:00:00 2001 From: Kha Nguyen Date: Tue, 22 Jul 2025 19:02:37 -0500 Subject: [PATCH 3/8] refactor: clean up key mapping logic for human-readable shortcuts --- .../components/keyboard-shortcuts-help.tsx | 20 +------------------ .../src/hooks/use-keyboard-shortcuts-help.ts | 8 ++++---- 2 files changed, 5 insertions(+), 23 deletions(-) diff --git a/apps/web/src/components/keyboard-shortcuts-help.tsx b/apps/web/src/components/keyboard-shortcuts-help.tsx index 65f879a3..db31c527 100644 --- a/apps/web/src/components/keyboard-shortcuts-help.tsx +++ b/apps/web/src/components/keyboard-shortcuts-help.tsx @@ -10,7 +10,6 @@ import { DialogTitle, DialogTrigger, } from "./ui/dialog"; -import { getPlatformSpecialKey } from "@/lib/utils"; import { Keyboard } from "lucide-react"; import { useKeyboardShortcutsHelp, @@ -19,23 +18,6 @@ import { import { useKeybindingsStore } from "@/stores/keybindings-store"; import { toast } from "sonner"; -const modifier: { - [key: string]: string; -} = { - Shift: "Shift", - Alt: "Alt", - ArrowLeft: "←", - ArrowRight: "→", - ArrowUp: "↑", - ArrowDown: "↓", - Space: "Space", -}; - -function getKeyWithModifier(key: string) { - if (key === "Ctrl") return getPlatformSpecialKey(); - return modifier[key] || key; -} - const ShortcutItem = ({ shortcut, recordingKey, @@ -79,7 +61,7 @@ const ShortcutItem = ({ isRecording={recordingKey === keyId} onStartRecording={() => onStartRecording(keyId, shortcut)} > - {getKeyWithModifier(keyPart)} + {keyPart} ); })} diff --git a/apps/web/src/hooks/use-keyboard-shortcuts-help.ts b/apps/web/src/hooks/use-keyboard-shortcuts-help.ts index 808bf25f..2f1a4d4a 100644 --- a/apps/web/src/hooks/use-keyboard-shortcuts-help.ts +++ b/apps/web/src/hooks/use-keyboard-shortcuts-help.ts @@ -71,10 +71,10 @@ const formatKey = (key: string): string => { .replace("ctrl", getPlatformSpecialKey()) .replace("alt", getPlatformAlternateKey()) .replace("shift", "Shift") - .replace("left", "ArrowLeft") - .replace("right", "ArrowRight") - .replace("up", "ArrowUp") - .replace("down", "ArrowDown") + .replace("left", "←") + .replace("right", "→") + .replace("up", "↑") + .replace("down", "↓") .replace("space", "Space") .replace("home", "Home") .replace("end", "End") From 771591bdfadab00fa238e1b160eec600824c7786 Mon Sep 17 00:00:00 2001 From: Kha Nguyen Date: Tue, 22 Jul 2025 19:10:35 -0500 Subject: [PATCH 4/8] fix: better detect letter keys using KeyboardEvent.code --- apps/web/src/stores/keybindings-store.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/web/src/stores/keybindings-store.ts b/apps/web/src/stores/keybindings-store.ts index ce2d839d..ed1d6b8c 100644 --- a/apps/web/src/stores/keybindings-store.ts +++ b/apps/web/src/stores/keybindings-store.ts @@ -217,8 +217,12 @@ function getPressedKey(ev: KeyboardEvent): string | null { if (key === "backspace") return "backspace"; // Check letter keys - const isLetter = key.length === 1 && key >= "a" && key <= "z"; - if (isLetter) return key; + if (code.startsWith("Key")) { + const letter = code.slice(3).toLowerCase(); + if (letter.length === 1 && letter >= "a" && letter <= "z") { + return letter; + } + } // Check number keys using physical position for AZERTY support if (code.startsWith("Digit")) { From f98651cd331c0753e9d13015bf07dad8d0c98047 Mon Sep 17 00:00:00 2001 From: Kha Nguyen Date: Tue, 22 Jul 2025 19:12:57 -0500 Subject: [PATCH 5/8] fix: add reset button for custom shorcuts --- apps/web/src/components/keyboard-shortcuts-help.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/web/src/components/keyboard-shortcuts-help.tsx b/apps/web/src/components/keyboard-shortcuts-help.tsx index db31c527..8b4c6274 100644 --- a/apps/web/src/components/keyboard-shortcuts-help.tsx +++ b/apps/web/src/components/keyboard-shortcuts-help.tsx @@ -6,6 +6,7 @@ import { Dialog, DialogContent, DialogDescription, + DialogFooter, DialogHeader, DialogTitle, DialogTrigger, @@ -127,6 +128,7 @@ export const KeyboardShortcutsHelp = () => { validateKeybinding, getKeybindingsForAction, setIsRecording, + resetToDefaults, } = useKeybindingsStore(); // Get shortcuts from centralized hook @@ -240,6 +242,11 @@ export const KeyboardShortcutsHelp = () => { ))} + + + ); From f6073c47c620deda88affe12a220917a0561e649 Mon Sep 17 00:00:00 2001 From: Kha Nguyen Date: Tue, 22 Jul 2025 19:33:10 -0500 Subject: [PATCH 6/8] highlight all keys when editing shorcuts for an action --- apps/web/src/components/keyboard-shortcuts-help.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/web/src/components/keyboard-shortcuts-help.tsx b/apps/web/src/components/keyboard-shortcuts-help.tsx index 8b4c6274..d04b27fa 100644 --- a/apps/web/src/components/keyboard-shortcuts-help.tsx +++ b/apps/web/src/components/keyboard-shortcuts-help.tsx @@ -21,11 +21,11 @@ import { toast } from "sonner"; const ShortcutItem = ({ shortcut, - recordingKey, + isRecording, onStartRecording, }: { shortcut: KeyboardShortcut; - recordingKey: string | null; + isRecording: boolean; onStartRecording: (keyId: string, shortcut: KeyboardShortcut) => void; }) => { // Filter out lowercase duplicates for display - if both "j" and "J" exist, only show "J" @@ -59,7 +59,7 @@ const ShortcutItem = ({ keyId={keyId} originalKey={key} shortcut={shortcut} - isRecording={recordingKey === keyId} + isRecording={isRecording} onStartRecording={() => onStartRecording(keyId, shortcut)} > {keyPart} @@ -234,7 +234,9 @@ export const KeyboardShortcutsHelp = () => { ))} From 92079777b9c3f8f157163124fa5b02386391cc2f Mon Sep 17 00:00:00 2001 From: Kha Nguyen Date: Tue, 22 Jul 2025 19:47:26 -0500 Subject: [PATCH 7/8] change reset default button label --- apps/web/src/components/keyboard-shortcuts-help.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/keyboard-shortcuts-help.tsx b/apps/web/src/components/keyboard-shortcuts-help.tsx index d04b27fa..f6f2f222 100644 --- a/apps/web/src/components/keyboard-shortcuts-help.tsx +++ b/apps/web/src/components/keyboard-shortcuts-help.tsx @@ -246,7 +246,7 @@ export const KeyboardShortcutsHelp = () => { From 5a7745f806edf4a386b8cfd079861f96c3b92baf Mon Sep 17 00:00:00 2001 From: Kha Nguyen Date: Thu, 24 Jul 2025 00:06:57 -0500 Subject: [PATCH 8/8] chore: remove unused variables and clean up code --- .../components/keyboard-shortcuts-help.tsx | 56 ++++++++----------- 1 file changed, 23 insertions(+), 33 deletions(-) diff --git a/apps/web/src/components/keyboard-shortcuts-help.tsx b/apps/web/src/components/keyboard-shortcuts-help.tsx index f6f2f222..65a73b90 100644 --- a/apps/web/src/components/keyboard-shortcuts-help.tsx +++ b/apps/web/src/components/keyboard-shortcuts-help.tsx @@ -1,6 +1,13 @@ "use client"; -import { useState, useEffect } from "react"; +import { Keyboard } from "lucide-react"; +import { useEffect, useState } from "react"; +import { toast } from "sonner"; +import { + type KeyboardShortcut, + useKeyboardShortcutsHelp, +} from "@/hooks/use-keyboard-shortcuts-help"; +import { useKeybindingsStore } from "@/stores/keybindings-store"; import { Button } from "./ui/button"; import { Dialog, @@ -11,13 +18,6 @@ import { DialogTitle, DialogTrigger, } from "./ui/dialog"; -import { Keyboard } from "lucide-react"; -import { - useKeyboardShortcutsHelp, - KeyboardShortcut, -} from "@/hooks/use-keyboard-shortcuts-help"; -import { useKeybindingsStore } from "@/stores/keybindings-store"; -import { toast } from "sonner"; const ShortcutItem = ({ shortcut, @@ -26,7 +26,7 @@ const ShortcutItem = ({ }: { shortcut: KeyboardShortcut; isRecording: boolean; - onStartRecording: (keyId: string, shortcut: KeyboardShortcut) => void; + onStartRecording: (shortcut: KeyboardShortcut) => void; }) => { // Filter out lowercase duplicates for display - if both "j" and "J" exist, only show "J" const displayKeys = shortcut.keys.filter((key: string) => { @@ -49,18 +49,15 @@ const ShortcutItem = ({
{displayKeys.map((key: string, index: number) => ( -
+
{key.split("+").map((keyPart: string, partIndex: number) => { const keyId = `${shortcut.id}-${index}-${partIndex}`; return ( onStartRecording(keyId, shortcut)} + onStartRecording={() => onStartRecording(shortcut)} > {keyPart} @@ -79,16 +76,10 @@ const ShortcutItem = ({ const EditableShortcutKey = ({ children, - keyId, - originalKey, - shortcut, isRecording, onStartRecording, }: { children: React.ReactNode; - keyId: string; - originalKey: string; - shortcut: KeyboardShortcut; isRecording: boolean; onStartRecording: () => void; }) => { @@ -99,7 +90,9 @@ const EditableShortcutKey = ({ }; return ( - {children} - + ); }; export const KeyboardShortcutsHelp = () => { const [open, setOpen] = useState(false); - const [recordingKey, setRecordingKey] = useState(null); const [recordingShortcut, setRecordingShortcut] = useState(null); @@ -129,6 +121,7 @@ export const KeyboardShortcutsHelp = () => { getKeybindingsForAction, setIsRecording, resetToDefaults, + isRecording, } = useKeybindingsStore(); // Get shortcuts from centralized hook @@ -137,7 +130,7 @@ export const KeyboardShortcutsHelp = () => { const categories = Array.from(new Set(shortcuts.map((s) => s.category))); useEffect(() => { - if (!recordingKey || !recordingShortcut) return; + if (!isRecording || !recordingShortcut) return; const handleKeyDown = (e: KeyboardEvent) => { e.preventDefault(); @@ -154,7 +147,6 @@ export const KeyboardShortcutsHelp = () => { toast.error( `Key "${keyString}" is already bound to "${conflict.existingAction}"`, ); - setRecordingKey(null); setRecordingShortcut(null); return; } @@ -167,13 +159,11 @@ export const KeyboardShortcutsHelp = () => { updateKeybinding(keyString, recordingShortcut.action); setIsRecording(false); - setRecordingKey(null); setRecordingShortcut(null); } }; const handleClickOutside = () => { - setRecordingKey(null); setRecordingShortcut(null); setIsRecording(false); }; @@ -186,17 +176,17 @@ export const KeyboardShortcutsHelp = () => { document.removeEventListener("click", handleClickOutside); }; }, [ - recordingKey, recordingShortcut, getKeybindingString, updateKeybinding, removeKeybinding, validateKeybinding, getKeybindingsForAction, + setIsRecording, + isRecording, ]); - const handleStartRecording = (keyId: string, shortcut: KeyboardShortcut) => { - setRecordingKey(keyId); + const handleStartRecording = (shortcut: KeyboardShortcut) => { setRecordingShortcut(shortcut); setIsRecording(true); }; @@ -230,9 +220,9 @@ export const KeyboardShortcutsHelp = () => {
{shortcuts .filter((shortcut) => shortcut.category === category) - .map((shortcut, index) => ( + .map((shortcut) => (