diff --git a/src/lib/components/HotkeyRecorder.svelte b/src/lib/components/HotkeyRecorder.svelte index 9d778d1..b8d7d49 100644 --- a/src/lib/components/HotkeyRecorder.svelte +++ b/src/lib/components/HotkeyRecorder.svelte @@ -48,9 +48,43 @@ return e.key.toUpperCase(); } + // Manual modifier-state tracker. webkit2gtk on KDE Wayland does not + // populate e.metaKey when the compositor intercepts Super — the Super + // keydown arrives as e.key === "Super" with e.metaKey still false on the + // next event. We track modifier state from raw keydown/keyup and OR it + // with the webkit booleans when building the combo so Super+letter can + // be recorded. + let manualMods = { + ctrl: false, + shift: false, + alt: false, + meta: false, + }; + + function updateManualMods(e: KeyboardEvent, down: boolean) { + switch (e.key) { + case "Control": + manualMods.ctrl = down; + break; + case "Shift": + manualMods.shift = down; + break; + case "Alt": + manualMods.alt = down; + break; + case "Meta": + case "Super": + case "OS": + case "Hyper": + manualMods.meta = down; + break; + } + } + function handleKeyDown(e: KeyboardEvent) { // Capture-phase listener; guard still belts-and-braces. if (!recording) return; + updateManualMods(e, true); e.preventDefault(); e.stopPropagation(); @@ -63,10 +97,10 @@ if (modifierKeys.has(e.key)) return; const parts: string[] = []; - if (e.ctrlKey) parts.push("Ctrl"); - if (e.shiftKey) parts.push("Shift"); - if (e.altKey) parts.push("Alt"); - if (e.metaKey) parts.push("Super"); + if (e.ctrlKey || manualMods.ctrl) parts.push("Ctrl"); + if (e.shiftKey || manualMods.shift) parts.push("Shift"); + if (e.altKey || manualMods.alt) parts.push("Alt"); + if (e.metaKey || manualMods.meta) parts.push("Super"); // Must have at least one modifier if (parts.length === 0) { @@ -82,16 +116,23 @@ setTimeout(() => { captured = false; }, 1500); } - // Register the listener only while recording, at the capture phase so no - // descendant handler (or the parent layout's svelte:window keydown) can - // swallow the event first. Button-level onkeydown would require the - // button to hold keyboard focus after a click, which webkit2gtk on Linux - // does not guarantee. + function handleKeyUp(e: KeyboardEvent) { + if (!recording) return; + updateManualMods(e, false); + } + + // Register capture-phase keydown + keyup listeners only while recording. + // Document-level capture so no descendant (or the parent layout's + // svelte:window keydown) can swallow the event first. $effect(() => { if (!recording) return; - const handler = handleKeyDown; - document.addEventListener("keydown", handler, { capture: true }); - return () => document.removeEventListener("keydown", handler, { capture: true }); + manualMods = { ctrl: false, shift: false, alt: false, meta: false }; + document.addEventListener("keydown", handleKeyDown, { capture: true }); + document.addEventListener("keyup", handleKeyUp, { capture: true }); + return () => { + document.removeEventListener("keydown", handleKeyDown, { capture: true }); + document.removeEventListener("keyup", handleKeyUp, { capture: true }); + }; }); function startRecording() { @@ -100,9 +141,22 @@ } let chips = $derived(settings.globalHotkey.split("+")); + let ariaLabel = $derived( + recording + ? "Press a new global hotkey. Press Escape to cancel." + : `Record hotkey. Current shortcut: ${settings.globalHotkey}`, + ); + let srStatus = $derived( + recording + ? "Listening for a new shortcut. Press Escape to cancel." + : captured + ? `Shortcut saved as ${settings.globalHotkey}.` + : `Current shortcut: ${settings.globalHotkey}. Click to change.`, + );