diff --git a/src/lib/components/HotkeyRecorder.svelte b/src/lib/components/HotkeyRecorder.svelte index f9f24c5..9d778d1 100644 --- a/src/lib/components/HotkeyRecorder.svelte +++ b/src/lib/components/HotkeyRecorder.svelte @@ -4,7 +4,49 @@ let recording = $state(false); let captured = $state(false); - const modifierKeys = new Set(["Control", "Shift", "Alt", "Meta"]); + // Linux webkit2gtk fires e.key === "Super" for the Windows/Super key; + // Firefox/older Chrome may fire "OS" or "Hyper". All of these should be + // treated as modifiers, not as the trigger key. + const modifierKeys = new Set([ + "Control", "Shift", "Alt", "Meta", + "Super", "OS", "Hyper", + ]); + + // Map KeyboardEvent.code (physical key, layout-independent, shift-independent) + // to the unshifted name the kon-hotkey evdev parser understands. Browser + // e.key returns "+" for Shift+Equal — parser wants "=". Same for | → \, etc. + const codeToParserKey: Record = { + Equal: "=", Minus: "-", + BracketLeft: "[", BracketRight: "]", + Backslash: "\\", IntlBackslash: "\\", + Semicolon: ";", Quote: "'", + Comma: ",", Period: ".", Slash: "/", + Backquote: "`", + Space: "Space", + // Numpad keys intentionally NOT mapped here. They are physically distinct + // evdev codes from their main-keyboard counterparts; mapping them would + // silently bind the wrong key. Let resolveTriggerKey fall through so the + // backend parser rejects and the toast surfaces the failure. + }; + + function resolveTriggerKey(e: KeyboardEvent): string { + // Letters and digits: e.key is reliable (always A-Z / 0-9 for those codes). + if (/^Key[A-Z]$/.test(e.code)) return e.code.slice(3); // KeyA → A + if (/^Digit[0-9]$/.test(e.code)) return e.code.slice(5); // Digit1 → 1 + if (/^Numpad[0-9]$/.test(e.code)) return e.code.slice(6); // Numpad1 → 1 + if (/^F\d{1,2}$/.test(e.code)) return e.code; // F1..F12 + + // Shifted punctuation / numpad operators: use the physical-key mapping. + if (codeToParserKey[e.code]) return codeToParserKey[e.code]; + + // Named keys (Escape, Enter, Tab, arrows, etc.): e.key is the right choice + // — but only if it's multi-char (single chars here are usually dead-keys + // or IME output we shouldn't save). + if (e.key.length > 1) return e.key; + + // Fallback: uppercase single char. Better than nothing. + return e.key.toUpperCase(); + } function handleKeyDown(e: KeyboardEvent) { // Capture-phase listener; guard still belts-and-braces. @@ -32,7 +74,7 @@ return; } - parts.push(e.key.length === 1 ? e.key.toUpperCase() : e.key); + parts.push(resolveTriggerKey(e)); settings.globalHotkey = parts.join("+"); saveSettings(); recording = false; @@ -72,7 +114,7 @@ aria-label="Record hotkey" > {#if recording} - Press new shortcut... + Hold Ctrl/Alt/Super + key... {:else} {#each chips as chip, i} {#if i > 0} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index db55a38..8a0bf62 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -117,6 +117,17 @@ } } catch (err) { console.error("Hotkey registration failed:", err); + const msg = err instanceof Error ? err.message : String(err); + toasts.error( + "Hotkey not registered", + `${hotkey} — ${msg}. Reverted to ${registeredHotkey ?? "previous value"}.`, + ); + // Revert the in-memory setting to the last successfully-registered hotkey + // so the UI does not lie about what is actually bound. + if (registeredHotkey && registeredHotkey !== hotkey) { + settings.globalHotkey = registeredHotkey; + saveSettings(); + } } }