import { useEffect, useState } from "react"; export interface ShortcutModifierState { metaKey: boolean; ctrlKey: boolean; altKey: boolean; shiftKey: boolean; } const EMPTY_SHORTCUT_MODIFIER_STATE: ShortcutModifierState = { metaKey: false, ctrlKey: false, altKey: false, shiftKey: false, }; export function areShortcutModifierStatesEqual( left: ShortcutModifierState, right: ShortcutModifierState, ): boolean { return ( left.metaKey === right.metaKey && left.ctrlKey === right.ctrlKey && left.altKey === right.altKey && left.shiftKey === right.shiftKey ); } export function useShortcutModifierState(): ShortcutModifierState { const [state, setState] = useState(EMPTY_SHORTCUT_MODIFIER_STATE); useEffect(() => { const onKeyboardEvent = (event: KeyboardEvent) => { setState((current) => shortcutModifierStateAfterKeyboardEvent(current, event)); }; // Dictation tools (Wispr Flow) paste with a synthetic ⌘V whose Meta keyup // never reaches the page, so the tracked state stays "⌘ held" forever and // the thread jump hints stick on screen. A paste is never jump intent, so // treat it like a blur and reset. A physically held modifier re-registers // on the next real key event. const onResetEvent = () => { setState((current) => areShortcutModifierStatesEqual(current, EMPTY_SHORTCUT_MODIFIER_STATE) ? current : EMPTY_SHORTCUT_MODIFIER_STATE, ); }; window.addEventListener("keydown", onKeyboardEvent, true); window.addEventListener("keyup", onKeyboardEvent, true); window.addEventListener("paste", onResetEvent, true); window.addEventListener("blur", onResetEvent); return () => { window.removeEventListener("keydown", onKeyboardEvent, true); window.removeEventListener("keyup", onKeyboardEvent, true); window.removeEventListener("paste", onResetEvent, true); window.removeEventListener("blur", onResetEvent); }; }, []); return state; } function normalizeModifierKey(key: string): keyof ShortcutModifierState | null { switch (key) { case "Meta": case "OS": case "Command": return "metaKey"; case "Control": return "ctrlKey"; case "Alt": case "Option": return "altKey"; case "Shift": return "shiftKey"; default: return null; } } export function shortcutModifierStateAfterKeyboardEvent( currentState: ShortcutModifierState, event: KeyboardEvent, ): ShortcutModifierState { const normalizedModifierKey = normalizeModifierKey(event.key); let nextState: ShortcutModifierState; if (normalizedModifierKey) { nextState = { ...currentState, [normalizedModifierKey]: event.type === "keydown", }; } else { // Flags on non-modifier keys may only clear a bit, never set one. After a // dictation tool's synthetic ⌘V (Wispr Flow), the browser can keep // reporting metaKey=true on real key events (Enter to submit) until the // user physically taps ⌘. Trusting that flag would mark ⌘ as held and // stick the thread jump hints. Setting a bit requires a real modifier // keydown, handled above. nextState = { metaKey: currentState.metaKey && event.metaKey, ctrlKey: currentState.ctrlKey && event.ctrlKey, altKey: currentState.altKey && event.altKey, shiftKey: currentState.shiftKey && event.shiftKey, }; } return areShortcutModifierStatesEqual(currentState, nextState) ? currentState : nextState; }