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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user