From 8760218f2e690f3c22ada732e2a098bb837eaf86 Mon Sep 17 00:00:00 2001 From: Francis Sunday Date: Wed, 24 Jun 2026 02:53:23 +0100 Subject: [PATCH] fix: update preferences to support registering more complex hotkeys fixes #27 --- src-tauri/src/config.rs | 16 +++---- src/components/Dashboard.tsx | 88 ++++++++++++++++++++++-------------- 2 files changed, 61 insertions(+), 43 deletions(-) diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index a3dd98c..ee3cb81 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -90,7 +90,7 @@ fn normalize_hotkey(raw_hotkey: &str) -> String { } let mut ordered_modifiers: Vec = Vec::new(); - for modifier in ["CommandOrControl", "Ctrl", "Shift", "Alt"] { + for modifier in ["CommandOrControl", "Ctrl", "Alt", "Shift", "Super"] { if modifiers.iter().any(|item| item == modifier) { ordered_modifiers.push(modifier.to_string()); } @@ -106,9 +106,8 @@ fn normalize_hotkey(raw_hotkey: &str) -> String { fn normalize_hotkey_part(part: &str) -> String { let lower = part.to_ascii_lowercase(); match lower.as_str() { - "cmd" | "command" | "commandorcontrol" | "commandorctrl" | "meta" | "super" => { - "CommandOrControl".to_string() - } + "cmd" | "command" | "meta" | "super" => "Super".to_string(), + "commandorcontrol" | "commandorctrl" => "CommandOrControl".to_string(), "ctrl" | "control" => "Ctrl".to_string(), "alt" | "option" => "Alt".to_string(), "shift" => "Shift".to_string(), @@ -148,7 +147,7 @@ fn normalize_key_name(part: &str) -> String { } fn is_modifier(value: &str) -> bool { - matches!(value, "CommandOrControl" | "Shift" | "Alt" | "Ctrl") + matches!(value, "CommandOrControl" | "Super" | "Shift" | "Alt" | "Ctrl") } pub struct ConfigManager { @@ -267,10 +266,11 @@ mod tests { #[test] fn test_normalize_hotkey_formats() { - assert_eq!(normalize_hotkey("Cmd+Shift+2"), "CommandOrControl+Shift+2"); + assert_eq!(normalize_hotkey("Cmd+Shift+2"), "Shift+Super+2"); + assert_eq!(normalize_hotkey("command+shift+s"), "Shift+Super+S"); assert_eq!( - normalize_hotkey("command+shift+s"), - "CommandOrControl+Shift+S" + normalize_hotkey("Ctrl+Alt+Shift+Cmd+S"), + "Ctrl+Alt+Shift+Super+S" ); assert_eq!(normalize_hotkey("ctrl alt p"), "Ctrl+Alt+P"); assert_eq!( diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index 65c4a6e..6ed3b1d 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -6,24 +6,40 @@ import type { AppConfig } from '@/types'; const formatHotkeyForDisplay = (hotkey: string): string => { return hotkey - .replace('CommandOrControl', '⌘') - .replace('Shift', '⇧') - .replace('Alt', '⌥') - .replace('Ctrl', '⌃') - .replace('Comma', ',') - .replace('Period', '.') + .replace(/CommandOrControl/g, '⌘') // legacy / cross-platform + .replace(/Super/g, '⌘') // Command key (macOS) + .replace(/Control/g, '⌃') + .replace(/Ctrl/g, '⌃') + .replace(/Shift/g, '⇧') + .replace(/Alt/g, '⌥') + .replace(/Comma/g, ',') + .replace(/Period/g, '.') .replace(/\+/g, ' '); }; const parseHotkeyFromDisplay = (displayHotkey: string): string => { return displayHotkey - .replace('⌘', 'CommandOrControl') - .replace('⇧', 'Shift') - .replace('⌥', 'Alt') - .replace('⌃', 'Ctrl') + .replace(/⌘/g, 'Super') + .replace(/⌃/g, 'Ctrl') + .replace(/⇧/g, 'Shift') + .replace(/⌥/g, 'Alt') .replace(/ /g, '+'); }; +// Resolve the physical key (event.code) to a hotkey token, immune to Shift +// rewriting the character (the hyper chord always holds Shift, turning 1 -> "!"). +// Restricted to the set the backend/plugin already handle. +const codeToKey = (code: string): string | null => { + if (/^Key[A-Z]$/.test(code)) return code.slice(3); // KeyX -> X + if (/^Digit[0-9]$/.test(code)) return code.slice(5); // Digit1 -> 1 + if (/^F\d{1,2}$/.test(code)) return code; // F1..F12 + if (code === 'Comma') return 'Comma'; + if (code === 'Period') return 'Period'; + return null; +}; + +const MODIFIER_KEYS = new Set(['Meta', 'Control', 'Shift', 'Alt']); + export function Dashboard() { const [config, setConfig] = useState(null); const [originalConfig, setOriginalConfig] = useState(null); @@ -104,39 +120,41 @@ export function Dashboard() { const handleKeyDown = useCallback((event: KeyboardEvent) => { if (!editingShortcut) return; - + event.preventDefault(); - + if (event.key === 'Escape') { cancelEditingShortcut(); return; } - - const keys: string[] = []; - if (event.metaKey || event.ctrlKey) keys.push('⌘'); - if (event.shiftKey) keys.push('⇧'); - if (event.altKey) keys.push('⌥'); - - if (event.key !== 'Meta' && event.key !== 'Control' && - event.key !== 'Shift' && event.key !== 'Alt' && - event.key !== 'Escape') { - const key = event.key.length === 1 ? event.key.toUpperCase() : event.key; - keys.push(key); + + // Canonical macOS order ⌃ ⌥ ⇧ ⌘; Ctrl and Cmd are kept separate so the + // hyper chord (⌃⌥⇧⌘) is representable. + const modifiers: string[] = []; + if (event.ctrlKey) modifiers.push('⌃'); + if (event.altKey) modifiers.push('⌥'); + if (event.shiftKey) modifiers.push('⇧'); + if (event.metaKey) modifiers.push('⌘'); + + // Bare modifier press (incl. each key of the hyper chord): preview, keep waiting. + if (MODIFIER_KEYS.has(event.key)) { + setTempHotkey(modifiers.join(' ')); + return; } - - if (keys.length >= 2) { - const displayHotkey = keys.join(' '); - const tauriHotkey = parseHotkeyFromDisplay(displayHotkey); - - if (editingShortcut === 'capture') { - handleConfigChange({ capture_hotkey: tauriHotkey }); - } - setEditingShortcut(null); - setTempHotkey(''); - } else { - setTempHotkey(keys.join(' ')); + // Base key from the physical key (Shift-proof), require ≥1 modifier. + const key = codeToKey(event.code); + if (!key || modifiers.length === 0) { + setTempHotkey(modifiers.join(' ')); + return; } + + const tauriHotkey = parseHotkeyFromDisplay([...modifiers, key].join(' ')); + if (editingShortcut === 'capture') { + handleConfigChange({ capture_hotkey: tauriHotkey }); + } + setEditingShortcut(null); + setTempHotkey(''); }, [editingShortcut, config]); useEffect(() => {