fix(hotkey): resolve physical-key codes; Super-as-modifier; surface backend registration errors
Three correctness fixes to the hotkey recorder based on dogfood logs: 1. Modifier set now includes Super / OS / Hyper (webkit2gtk on Linux fires e.key === "Super" for the Windows key — previously that key got captured as the final trigger, producing invalid 'Ctrl+Shift+Super' strings the evdev parser rejected). 2. resolveTriggerKey() uses e.code (physical, shift-independent key) to resolve shifted punctuation back to the unshifted name the evdev parser understands: '+' -> '=', '|' -> '\\', etc. Letters and digits also use e.code (KeyA -> A, Digit1 -> 1) to avoid layout quirks. 3. Numpad keys intentionally not mapped to main-keyboard equivalents — they are distinct evdev codes. Leaves the parser to reject them so the user gets a toast instead of a silently-wrong binding. Registration failures now surface as a toast and revert settings.globalHotkey to the last successfully-registered value (if any), so the UI cannot lie about what is actually bound. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<string, string> = {
|
||||
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}
|
||||
<span class="text-[11px] text-text-tertiary italic">Press new shortcut...</span>
|
||||
<span class="text-[11px] text-text-tertiary italic">Hold Ctrl/Alt/Super + key...</span>
|
||||
{:else}
|
||||
{#each chips as chip, i}
|
||||
{#if i > 0}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user