feat(B.1 #4 UX): debounce hotkey press events by 120ms

A rapid double-tap of the global hotkey, evdev autorepeat, or a
sticky-key compositor quirk (KDE's 'slow keys') can all deliver
the same press twice within ~100ms. Without a guard, the recording
toggles into and out of the same frame and the capture is lost.

Gates the evdev 'kon:hotkey-pressed' forwarder in +layout.svelte
behind a 120ms debounce (Date.now()-based; no timers, so no tail
latency for a legitimate single press). The debounce is intentionally
shorter than a deliberate double-press cadence but longer than any
autorepeat interval we've seen in the wild.

The audio-stream-warming half of brief item #4 (Handy #1143) lives
in Workstream A's Phase A.3 warm-up WAV; this covers the UX side.

Co-authored-by: jars <jakejars@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-04-21 11:37:37 +00:00
committed by Jake
parent f3a0673eaa
commit 8e5e034df1

View File

@@ -136,12 +136,25 @@
}
}
// Listen for evdev hotkey events from the Rust backend
// Listen for evdev hotkey events from the Rust backend.
//
// Debounce window: evdev autorepeat, a sticky-key compositor quirk,
// or a user's nervous double-tap can all deliver the same press
// twice within ~100 ms — which, without debouncing, toggles the
// recording into and out of the same frame and loses the capture.
// Matches Handy #1143 ('first press records nothing, second works').
// This is the UX side of brief item #4; the audio-stream warm-up
// side is owned by Workstream A.
const HOTKEY_DEBOUNCE_MS = 120;
let lastHotkeyAtMs = 0;
let unlistenEvdev = null;
async function setupEvdevListener() {
if (!tauriRuntimeAvailable) return;
const { listen } = await import("@tauri-apps/api/event");
unlistenEvdev = await listen("kon:hotkey-pressed", () => {
const now = Date.now();
if (now - lastHotkeyAtMs < HOTKEY_DEBOUNCE_MS) return;
lastHotkeyAtMs = now;
if (page.current !== "dictation") page.current = "dictation";
requestAnimationFrame(() => {
window.dispatchEvent(new CustomEvent("kon:toggle-recording"));