// Global shortcuts — wraps Electron's globalShortcut, tracks registrations // by an opaque renderer-supplied id so the same accelerator can be // re-bound without the caller juggling state. // // PTT semantics are simulated: Electron's globalShortcut API only delivers // a "pressed" callback — it has no keyup / release event. We fire // SHORTCUT_EVT_FIRED on press, then after a 200ms timer fire // SHORTCUT_EVT_RELEASED. Known limitation; TODO: revisit with // `uiohook-napi` or `node-global-key-listener` if the press-and-release // UX is too loose. import { app, BrowserWindow, globalShortcut, ipcMain } from 'electron'; import { CHANNELS, type ShortcutEvent, type ShortcutKind, type ShortcutRegisterArgs, } from '../ipc-types'; interface Entry { accelerator: string; kind: ShortcutKind; } const registry = new Map(); export function register(mainWindow: BrowserWindow): void { const send = (channel: string, payload: ShortcutEvent): void => { if (mainWindow.isDestroyed()) return; mainWindow.webContents.send(channel, payload); }; ipcMain.handle( CHANNELS.SHORTCUT_REGISTER, async (_evt, args: ShortcutRegisterArgs): Promise => { const { id, accelerator, kind } = args; const prev = registry.get(id); if (prev) { try { globalShortcut.unregister(prev.accelerator); } catch { /* ignore */ } registry.delete(id); } if (globalShortcut.isRegistered(accelerator)) { return false; } const ok = globalShortcut.register(accelerator, () => { const ts = Date.now(); send(CHANNELS.SHORTCUT_EVT_FIRED, { id, ts }); if (kind === 'ptt') { setTimeout(() => { send(CHANNELS.SHORTCUT_EVT_RELEASED, { id, ts: Date.now() }); }, 200); } }); if (!ok) return false; registry.set(id, { accelerator, kind }); return true; }, ); ipcMain.handle(CHANNELS.SHORTCUT_UNREGISTER, async (_evt, id: string): Promise => { const entry = registry.get(id); if (!entry) return; try { globalShortcut.unregister(entry.accelerator); } catch { /* already gone */ } registry.delete(id); }); ipcMain.handle(CHANNELS.SHORTCUT_IS_REGISTERED, async (_evt, id: string): Promise => { const entry = registry.get(id); if (!entry) return false; return globalShortcut.isRegistered(entry.accelerator); }); app.on('will-quit', () => { globalShortcut.unregisterAll(); registry.clear(); }); }