225 lines · 7.4 KB · TypeScript
RawDownload
// @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<QuitConfirmationMode>;  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();      },    );  };}