fix(hotkey): track modifier state manually so Super+key records on KDE Wayland

webkit2gtk under KDE Wayland does not set e.metaKey on subsequent events
after the compositor intercepts Super — but the Super keydown itself is
still delivered with e.key === "Super". Track pressed modifiers from
raw keydown/keyup and OR with the webkit booleans so combos like Super+E
(which OpenWhispr already registers successfully via evdev) can now be
captured in Kon's recorder.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-19 21:49:08 +01:00
parent 39c967c33b
commit 5e09ab9bd3

View File

@@ -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.`,
);
</script>
<button
type="button"
class="inline-flex items-center gap-1.5 rounded-xl px-4 py-2.5
{recording
? 'bg-bg-input border-accent shadow-[0_0_0_3px_rgba(232,168,124,0.1)]'
@@ -111,14 +165,16 @@
: 'bg-bg-input border-border hover:border-border'}
border transition-all"
onclick={startRecording}
aria-label="Record hotkey"
aria-label={ariaLabel}
aria-pressed={recording}
>
<span class="sr-only" aria-live="polite">{srStatus}</span>
{#if recording}
<span class="text-[11px] text-text-tertiary italic">Hold Ctrl/Alt/Super + key...</span>
{:else}
{#each chips as chip, i}
{#if i > 0}
<span class="text-[10px] text-text-tertiary">+</span>
<span class="text-[10px] text-text-tertiary" aria-hidden="true">+</span>
{/if}
<span class="px-2 py-0.5 rounded-md bg-bg-elevated border border-border text-[11px] font-medium text-text
shadow-[inset_0_1px_2px_rgba(0,0,0,0.3)]">{chip}</span>