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();
|
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) {
|
function handleKeyDown(e: KeyboardEvent) {
|
||||||
// Capture-phase listener; guard still belts-and-braces.
|
// Capture-phase listener; guard still belts-and-braces.
|
||||||
if (!recording) return;
|
if (!recording) return;
|
||||||
|
updateManualMods(e, true);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
@@ -63,10 +97,10 @@
|
|||||||
if (modifierKeys.has(e.key)) return;
|
if (modifierKeys.has(e.key)) return;
|
||||||
|
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
if (e.ctrlKey) parts.push("Ctrl");
|
if (e.ctrlKey || manualMods.ctrl) parts.push("Ctrl");
|
||||||
if (e.shiftKey) parts.push("Shift");
|
if (e.shiftKey || manualMods.shift) parts.push("Shift");
|
||||||
if (e.altKey) parts.push("Alt");
|
if (e.altKey || manualMods.alt) parts.push("Alt");
|
||||||
if (e.metaKey) parts.push("Super");
|
if (e.metaKey || manualMods.meta) parts.push("Super");
|
||||||
|
|
||||||
// Must have at least one modifier
|
// Must have at least one modifier
|
||||||
if (parts.length === 0) {
|
if (parts.length === 0) {
|
||||||
@@ -82,16 +116,23 @@
|
|||||||
setTimeout(() => { captured = false; }, 1500);
|
setTimeout(() => { captured = false; }, 1500);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the listener only while recording, at the capture phase so no
|
function handleKeyUp(e: KeyboardEvent) {
|
||||||
// descendant handler (or the parent layout's svelte:window keydown) can
|
if (!recording) return;
|
||||||
// swallow the event first. Button-level onkeydown would require the
|
updateManualMods(e, false);
|
||||||
// button to hold keyboard focus after a click, which webkit2gtk on Linux
|
}
|
||||||
// does not guarantee.
|
|
||||||
|
// 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(() => {
|
$effect(() => {
|
||||||
if (!recording) return;
|
if (!recording) return;
|
||||||
const handler = handleKeyDown;
|
manualMods = { ctrl: false, shift: false, alt: false, meta: false };
|
||||||
document.addEventListener("keydown", handler, { capture: true });
|
document.addEventListener("keydown", handleKeyDown, { capture: true });
|
||||||
return () => document.removeEventListener("keydown", handler, { capture: true });
|
document.addEventListener("keyup", handleKeyUp, { capture: true });
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keydown", handleKeyDown, { capture: true });
|
||||||
|
document.removeEventListener("keyup", handleKeyUp, { capture: true });
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
function startRecording() {
|
function startRecording() {
|
||||||
@@ -100,9 +141,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let chips = $derived(settings.globalHotkey.split("+"));
|
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>
|
</script>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
class="inline-flex items-center gap-1.5 rounded-xl px-4 py-2.5
|
class="inline-flex items-center gap-1.5 rounded-xl px-4 py-2.5
|
||||||
{recording
|
{recording
|
||||||
? 'bg-bg-input border-accent shadow-[0_0_0_3px_rgba(232,168,124,0.1)]'
|
? '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'}
|
: 'bg-bg-input border-border hover:border-border'}
|
||||||
border transition-all"
|
border transition-all"
|
||||||
onclick={startRecording}
|
onclick={startRecording}
|
||||||
aria-label="Record hotkey"
|
aria-label={ariaLabel}
|
||||||
|
aria-pressed={recording}
|
||||||
>
|
>
|
||||||
|
<span class="sr-only" aria-live="polite">{srStatus}</span>
|
||||||
{#if recording}
|
{#if recording}
|
||||||
<span class="text-[11px] text-text-tertiary italic">Hold Ctrl/Alt/Super + key...</span>
|
<span class="text-[11px] text-text-tertiary italic">Hold Ctrl/Alt/Super + key...</span>
|
||||||
{:else}
|
{:else}
|
||||||
{#each chips as chip, i}
|
{#each chips as chip, i}
|
||||||
{#if i > 0}
|
{#if i > 0}
|
||||||
<span class="text-[10px] text-text-tertiary">+</span>
|
<span class="text-[10px] text-text-tertiary" aria-hidden="true">+</span>
|
||||||
{/if}
|
{/if}
|
||||||
<span class="px-2 py-0.5 rounded-md bg-bg-elevated border border-border text-[11px] font-medium text-text
|
<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>
|
shadow-[inset_0_1px_2px_rgba(0,0,0,0.3)]">{chip}</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user