import { EventsOn } from '@runtime/runtime'; import { GetShortcuts, SetShortcut, SetShortcuts, ResetShortcuts, } from '@go/config/config.js'; import { Events } from '../events'; import { dictByName } from '@utils/binding'; export interface ShortcutsState { bindings: Map; // action → key combo loaded: boolean; } type Subscriber = () => void; /** Panel scope prefixes for scope-aware lookups. An action with one of * these prefixes is only ever resolved inside its own panel, so it can * reuse a key the global table also binds. */ const PANEL_PREFIXES = ['tracklist.', 'autotag.'] as const; /** * ShortcutsStore holds the current keyboard shortcut bindings * and syncs them with the Go backend via Wails bindings. */ class ShortcutsStore { private state: ShortcutsState = { bindings: new Map(), loaded: false, }; private subscribers = new Set(); private notifyQueued = false; constructor() { this.initializeEventListeners(); this.loadFromBackend(); } // =================================================================== // WAILS EVENT BRIDGE // =================================================================== private initializeEventListeners(): void { EventsOn( Events.ShortcutsConfigChanged, (data: Record) => { this.state = { bindings: new Map(Object.entries(data)), loaded: true, }; this.notify(); }, ); } private async loadFromBackend(): Promise { try { const raw = await dictByName(GetShortcuts()); this.state = { bindings: new Map(Object.entries(raw)), loaded: true, }; this.notify(); } catch { // Use empty bindings on failure; they'll be populated // when the backend pushes a ShortcutsConfigChanged event. this.state.loaded = true; this.notify(); } } // =================================================================== // STATE ACCESS // =================================================================== getState(): Readonly { return this.state; } /** Returns the full action→key bindings map. */ getBindings(): Map { return this.state.bindings; } /** Look up the key combo for a given action. */ getKeyForAction(action: string): string | undefined { return this.state.bindings.get(action); } /** * Reverse lookup: find the action bound to a given key combo. * If a panel scope is provided, panel-specific bindings are checked * first, then global bindings. */ getActionForKey( key: string, scope?: string, ): string | undefined { // If we have a panel scope, check panel-specific bindings first. if (scope && scope.startsWith('panel:')) { const panelPrefix = scope.replace('panel:', '') + '.'; for (const [action, boundKey] of this.state.bindings) { if ( action.startsWith(panelPrefix) && boundKey === key ) { return action; } } } // Fall back to global (non-panel) bindings. for (const [action, boundKey] of this.state.bindings) { if (boundKey !== key) continue; const isPanel = PANEL_PREFIXES.some((p) => action.startsWith(p), ); if (!isPanel) return action; } return undefined; } /** * Check for binding conflicts. Returns the conflicting binding * or null if no conflict. */ findConflict( key: string, _scope: string, excludeAction: string, ): { action: string; key: string } | null { for (const [action, boundKey] of this.state.bindings) { if (action === excludeAction) continue; if (boundKey === key) return { action, key: boundKey }; } return null; } // =================================================================== // ACTIONS // =================================================================== /** Update a single shortcut binding. */ async updateBinding( action: string, key: string, ): Promise { await SetShortcut(action, key); } /** Replace all bindings at once. */ async setAll( bindings: Record, ): Promise { await SetShortcuts(bindings); } /** Reset all shortcuts to defaults. */ async resetAll(): Promise { await ResetShortcuts(); } // =================================================================== // SUBSCRIPTION SYSTEM // =================================================================== subscribe(callback: Subscriber): () => void { this.subscribers.add(callback); return () => this.subscribers.delete(callback); } private notify(): void { if (this.notifyQueued) return; this.notifyQueued = true; queueMicrotask(() => { this.notifyQueued = false; this.subscribers.forEach((cb) => cb()); }); } } // Singleton instance. export const shortcutsStore = new ShortcutsStore();