import { isRegistered, register, type ShortcutEvent, unregister, } from '@tauri-apps/plugin-global-shortcut'; // Maps a KeyboardEvent.code (what our PTT settings store) into the shortcut // string accepted by tauri-plugin-global-shortcut. The plugin follows the // [keyboard-types] crate naming which mostly matches DOM `event.code`, but // single-key aliases (e.g. "Space", "F5") work as-is. export function codeToShortcut(code: string): string { if (code.startsWith('Key')) return code.slice(3); // KeyV -> V if (code.startsWith('Digit')) return code.slice(5); // Digit1 -> 1 // Space, F1..F24, Escape, Enter, Tab, Arrow*, etc. pass through unchanged. return code; } export async function registerPttShortcut( code: string, onPress: () => void, onRelease: () => void, ): Promise { const shortcut = codeToShortcut(code); try { if (await isRegistered(shortcut)) { await unregister(shortcut); } await register(shortcut, (event: ShortcutEvent) => { if (event.state === 'Pressed') onPress(); else if (event.state === 'Released') onRelease(); }); return true; } catch (err: unknown) { console.warn('registerPttShortcut failed', { code, err }); return false; } } export async function unregisterPttShortcut(code: string): Promise { const shortcut = codeToShortcut(code); try { if (await isRegistered(shortcut)) { await unregister(shortcut); } } catch (err: unknown) { console.warn('unregisterPttShortcut failed', { code, err }); } } // Press-only global shortcut (for toggles like Mute/Deafen). Accepts an // already-formatted accelerator string (e.g. "CommandOrControl+Shift+M") // since these bindings may include modifier chords — the KeyboardEvent.code // variant used by PTT can't express that. export async function registerGlobalShortcutPress( shortcut: string, onPress: () => void, ): Promise { try { if (await isRegistered(shortcut)) { await unregister(shortcut); } await register(shortcut, (event: ShortcutEvent) => { if (event.state === 'Pressed') onPress(); }); return true; } catch (err: unknown) { console.warn('registerGlobalShortcutPress failed', { shortcut, err }); return false; } } export async function unregisterGlobalShortcut(shortcut: string): Promise { try { if (await isRegistered(shortcut)) { await unregister(shortcut); } } catch (err: unknown) { console.warn('unregisterGlobalShortcut failed', { shortcut, err }); } } // Detects whether we're running under Tauri. When running in a pure web // preview (vite dev in a browser without Tauri), importing the plugin still // works but calls fall through to window.__TAURI_INTERNALS__ which doesn't // exist — use this guard to skip registration cleanly. export function isTauriRuntime(): boolean { return typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window; } // --- Soundboard shortcuts -------------------------------------------------- // // Separate from the single PTT shortcut: the soundboard needs to register // many fire-and-forget press bindings at once, keep track of which ids own // which accelerators so we can unregister just one, and expose conflict // detection for the settings UI. interface SoundShortcutRegistration { shortcut: string; onPress: () => void; } // Map of logical id (sound uuid) -> registration. const soundRegistry = new Map(); export async function registerSoundShortcut( id: string, code: string, onPress: () => void, ): Promise { if (!isTauriRuntime()) return false; const shortcut = codeToShortcut(code); // Unregister any previous binding for this id first — caller may be // re-registering after the user changed the hotkey for the same sound. await unregisterSoundShortcut(id); try { if (await isRegistered(shortcut)) { await unregister(shortcut); } await register(shortcut, (event: ShortcutEvent) => { if (event.state === 'Pressed') onPress(); }); soundRegistry.set(id, { shortcut, onPress }); return true; } catch (err: unknown) { console.warn('registerSoundShortcut failed', { id, code, err }); return false; } } export async function unregisterSoundShortcut(id: string): Promise { const reg = soundRegistry.get(id); if (!reg) return; soundRegistry.delete(id); if (!isTauriRuntime()) return; try { if (await isRegistered(reg.shortcut)) { await unregister(reg.shortcut); } } catch (err: unknown) { console.warn('unregisterSoundShortcut failed', { id, err }); } } export async function unregisterAllSoundShortcuts(): Promise { const ids = Array.from(soundRegistry.keys()); await Promise.all(ids.map((id) => unregisterSoundShortcut(id))); } // Resolve a DOM code to the registry's current owner (if any). Used by the // settings UI to surface conflicts before saving a new hotkey. export function soundShortcutOwnerFor(code: string): string | null { const shortcut = codeToShortcut(code); for (const [id, reg] of soundRegistry) { if (reg.shortcut === shortcut) return id; } return null; }