/* * Vencord, a Discord client mod * Copyright (c) 2026 Evandro * SPDX-License-Identifier: GPL-3.0-or-later */ export interface KeyEventOptions { keydown?: boolean; keyup?: boolean; portal?: boolean; } export interface PortalState { bound: boolean; matchesTrigger: boolean; triggerDescription: string; } export type DispatchMode = "passthrough" | "pressAsRelease" | "hold"; export type Callback = (isDown: boolean) => void; export interface Scheduler { now(): number; setTimeout(fn: () => void, ms: number): unknown; clearTimeout(handle: unknown): void; } export interface Dispatcher { dispatch: Callback; cancel(): void; } // GNOME repeats a held shortcut after 500 ms and then every 30 ms (org.gnome.desktop.peripherals.keyboard) export const FIRST_PROBE_MS = 1000; export const PROBE_WINDOW_MS = 300; export const ECHO_MS = 150; const MODIFIERS: Record = { ctrl: "CTRL", control: "CTRL", primary: "CTRL", shift: "SHIFT", alt: "ALT", mod1: "ALT", super: "LOGO", logo: "LOGO", meta: "LOGO", mod4: "LOGO" }; export function planRegistration(options: KeyEventOptions, portalActive: boolean, representable: boolean): DispatchMode { if (!portalActive || !representable || options.portal === false) return "passthrough"; if (options.keyup && options.keydown) return "hold"; // GNOME only delivers the release if the main key is let go before the modifiers, so act on the press if (options.keyup) return "pressAsRelease"; return "passthrough"; } export function adjustOptions(options: T, mode: DispatchMode): T { return mode === "pressAsRelease" ? { ...options, keydown: true, keyup: true } : options; } export function createDispatcher(callback: Callback, mode: DispatchMode, rearm: () => void, scheduler: Scheduler): Dispatcher { if (mode === "passthrough") return { dispatch: callback, cancel() { } }; let held = false; let repeated = false; let rearmedAt = -Infinity; let timer: unknown; const end = () => { held = false; if (mode === "hold") callback(false); }; // Discord's native module ignores presses until it sees a release, and GNOME drops the release when a // modifier is let go first. Re-registering resets that state; a held combo then repeats within a few ms. const probe = () => { repeated = false; rearm(); rearmedAt = scheduler.now(); timer = scheduler.setTimeout(() => repeated ? probe() : end(), PROBE_WINDOW_MS); }; const start = () => { held = true; callback(mode === "hold"); timer = scheduler.setTimeout(probe, FIRST_PROBE_MS); }; const dispatch: Callback = isDown => { const echo = scheduler.now() - rearmedAt < ECHO_MS; if (!held) { if (isDown) start(); } else if (isDown) { if (echo) repeated = true; else { scheduler.clearTimeout(timer); start(); } } else if (!echo) { scheduler.clearTimeout(timer); end(); } }; return { dispatch, cancel() { scheduler.clearTimeout(timer); held = false; } }; } export function normalizeTrigger(trigger: string): string { const mods = Array.from(trigger.matchAll(/<([^>]+)>/g), m => m[1]); let rest = trigger.slice(trigger.lastIndexOf(">") + 1); if (mods.length === 0) { const parts = rest.split("+"); rest = parts.pop()!; mods.push(...parts); } const key = rest.trim().split(/\s+/).pop()!; const canonical = mods.map(m => MODIFIERS[m.trim().toLowerCase()] ?? m.trim().toUpperCase()); return [...new Set(canonical)].sort().join("+") + "|" + key.toLowerCase(); } export function fixPortalState(state: S, expected: string | undefined): S { if (!state.bound || state.matchesTrigger || !expected) return state; // GNOME describes the trigger as "Press F12", which never equals "CTRL+SHIFT+F12" literally return normalizeTrigger(expected) === normalizeTrigger(state.triggerDescription) ? { ...state, matchesTrigger: true } : state; }