// @effect-diagnostics globalDate:off globalTimers:off -- Synchronous before-input-event handler; key events must be timed and the watchdog scheduled outside any Effect runtime. import type { QuitConfirmationMode, QuitShortcutHintEvent } from "@t3tools/contracts"; // The quit accelerator is intercepted in before-input-event, which runs // before the native menu accelerator. Quitting from the application menu is // untouched and always quits immediately. export const QUIT_HOLD_DURATION_MS = 1200; export const QUIT_DOUBLE_PRESS_MS = 500; // "Still held" is proven by auto-repeat keydowns, not by the absence of a // release: macOS suppresses a letter keyUp while the command key is down, so a // tap release can go completely unseen and a release-based timer would quit // anyway. Once held, quitting waits for Q keyUp or a quiet grace period after // repeats stop so they cannot reach the next app. Keyboards with // auto-repeat disabled must use a double press or the application menu Quit action. // Supporting holds without repeats requires a native physical key-state check. export const QUIT_HOLD_RELEASE_GRACE_MS = 600; // A slow repeat rate can exceed the fixed grace. Waiting for two observed // cadences keeps the timer behind the next repeat without slowing normal rates. const QUIT_HOLD_REPEAT_CADENCE_MULTIPLIER = 2; export interface QuitHoldKeyInput { readonly type: string; readonly key: string; readonly meta: boolean; readonly control: boolean; readonly alt: boolean; readonly shift: boolean; readonly isAutoRepeat: boolean; } export interface QuitShortcutOptions { readonly platform: NodeJS.Platform; readonly getMode: () => Promise; readonly notify: (event: QuitShortcutHintEvent) => void; readonly concealWindow: () => void; readonly quit: () => void; } export function makeQuitShortcutHandler( options: QuitShortcutOptions, ): (event: { preventDefault: () => void }, input: QuitHoldKeyInput) => void { const modifierKey = options.platform === "darwin" ? "meta" : "control"; let watchdog: NodeJS.Timeout | undefined; let holding = false; let mode: QuitConfirmationMode | undefined; let notified = false; // Set once getMode resolves to hold; auto-repeats may only complete the hold when armed. let armed = false; let quitOnRelease = false; let heldSince = 0; let lastPressAt = 0; let lastRepeatAt = 0; let repeatCadenceMs = 0; // Incremented when a press is superseded or explicitly cancelled. A plain // key release does not invalidate its pending mode read: a direct-mode // press must still quit after that read settles. let generation = 0; const clearWatchdog = () => { if (watchdog !== undefined) { clearTimeout(watchdog); watchdog = undefined; } }; const release = (cancelPendingMode = true, keepDoublePressHint = false) => { if (cancelPendingMode) generation += 1; if (!holding && !notified) return; const keepHint = keepDoublePressHint && mode === "double-click" && notified; holding = false; armed = false; quitOnRelease = false; lastRepeatAt = 0; repeatCadenceMs = 0; if (keepHint) return; mode = undefined; clearWatchdog(); if (notified) { notified = false; options.notify({ state: "up" }); } }; // Dismisses any overlay first so a cancelled quit cannot leave a stale hint. const quitNow = () => { release(); lastPressAt = 0; options.quit(); }; const quitAfterQuietPeriod = () => { clearWatchdog(); const quietPeriodMs = Math.max( QUIT_HOLD_RELEASE_GRACE_MS, repeatCadenceMs * QUIT_HOLD_REPEAT_CADENCE_MULTIPLIER, ); watchdog = setTimeout(quitNow, quietPeriodMs); }; return (event, input) => { const key = input.key.toLowerCase(); if (input.type === "keyUp") { if (key === "q") { const shouldQuit = quitOnRelease; release(false, true); if (shouldQuit) options.quit(); } else if (key === modifierKey) { if (!quitOnRelease) { release(false, true); } else { quitAfterQuietPeriod(); } } return; } if (input.type !== "keyDown") return; const modifierDown = options.platform === "darwin" ? input.meta : input.control; if (input.isAutoRepeat && modifierDown && key === "q") { const now = Date.now(); repeatCadenceMs = now - (lastRepeatAt === 0 ? heldSince : lastRepeatAt); lastRepeatAt = now; } if (quitOnRelease) { event.preventDefault(); if (key === "q") { if (modifierDown) { quitAfterQuietPeriod(); } else { clearWatchdog(); } } return; } if (!modifierDown || input.alt || input.shift || key !== "q") { // Re-pressing the platform modifier is the first half of a second full // quit shortcut, so it must not cancel an active double-press window. if (key === modifierKey && !input.alt && !input.shift) return; // Other keys cancel the hold and the first tap, even after release. // Keep this separate from release(), which also runs when a fresh Q // keydown follows a keyUp that macOS did not deliver. if (!input.isAutoRepeat) { lastPressAt = 0; release(); } return; } event.preventDefault(); if (input.isAutoRepeat) { if (mode === "hold" && armed && Date.now() - heldSince >= QUIT_HOLD_DURATION_MS) { armed = false; quitOnRelease = true; options.concealWindow(); quitAfterQuietPeriod(); } return; } const now = Date.now(); const previousPressAt = lastPressAt; lastPressAt = now; // A fresh keydown supersedes the current physical hold or the hint kept // alive after a detected release. if (holding || notified) release(); generation += 1; // Every mode accepts two presses. Quit before reading settings so a slow // read cannot delay the second press. Repeats never reach this branch. if (previousPressAt !== 0 && now - previousPressAt <= QUIT_DOUBLE_PRESS_MS) { quitNow(); return; } const pressGeneration = generation; holding = true; heldSince = now; void options.getMode().then( (resolvedMode) => { if (generation !== pressGeneration) return; if (resolvedMode === "direct") { quitNow(); return; } if (resolvedMode === "double-click") { const remainingMs = QUIT_DOUBLE_PRESS_MS - (Date.now() - now); if (remainingMs <= 0) { release(); return; } mode = resolvedMode; notified = true; options.notify({ state: "down", mode: resolvedMode }); watchdog = setTimeout(release, remainingMs); return; } // A hold cannot be armed after its physical press has ended. if (!holding) return; mode = resolvedMode; notified = true; options.notify({ state: "down", mode: resolvedMode }); armed = true; // No auto-repeat by then means the key was released (possibly with a // suppressed keyUp) or repeat is disabled; either way, don't quit. watchdog = setTimeout(() => { watchdog = undefined; release(); }, QUIT_HOLD_DURATION_MS + QUIT_HOLD_RELEASE_GRACE_MS); }, // A failed settings read must never strand the quit request. () => { if (generation !== pressGeneration) return; quitNow(); }, ); }; }