/** * Simple event bus for inter-component communication. * * This provides a decoupled way for components to communicate without * direct dependencies. Components can publish events and subscribe to * events they're interested in. * * Usage: * ```tsx * // Subscribe to events * const unsub = EventBus.on("theme.changed", (data) => { * console.log("Theme changed to:", data.theme) * }) * * // Publish events * EventBus.emit("theme.changed", { theme: "dark" }) * * // Cleanup * unsub() * ``` */ type EventHandler = (data: T) => void; // Export EventHandler type for external use export type { EventHandler }; interface EventBusInstance { on(event: string, handler: EventHandler): () => void; once(event: string, handler: EventHandler): () => void; off(event: string, handler: EventHandler): void; emit(event: string, data: T): void; clear(): void; } function createEventBus(): EventBusInstance { const handlers = new Map>(); return { on(event: string, handler: EventHandler): () => void { if (!handlers.has(event)) { handlers.set(event, new Set()); } handlers.get(event)!.add(handler as EventHandler); // Return unsubscribe function return () => { this.off(event, handler); }; }, once(event: string, handler: EventHandler): () => void { const wrappedHandler: EventHandler = (data) => { this.off(event, wrappedHandler); handler(data); }; return this.on(event, wrappedHandler); }, off(event: string, handler: EventHandler): void { const eventHandlers = handlers.get(event); if (eventHandlers) { eventHandlers.delete(handler as EventHandler); if (eventHandlers.size === 0) { handlers.delete(event); } } }, emit(event: string, data: T): void { const eventHandlers = handlers.get(event); if (eventHandlers) { for (const handler of eventHandlers) { try { handler(data); } catch (error) { console.error(`Error in event handler for "${event}":`, error); } } } }, clear(): void { handlers.clear(); }, }; } // Singleton event bus instance const EventBus = createEventBus(); import type { KeybindActionName } from "@/context/KeybindContext"; import type { TABS } from "@/utils/navigation"; import type { PaneId, NavMode } from "@/context/NavigationContext"; // Common event types for the application export type AppEvents = { "theme.changed": { theme: string; mode: "dark" | "light" }; "theme.mode.changed": { mode: "dark" | "light" }; "theme.reload": {}; "navigation.tab.changed": { tab: string; previousTab?: string }; "navigation.layer.changed": { depth: number; previousDepth: number }; "feed.subscribed": { feedId: string; feedUrl: string }; "feed.unsubscribed": { feedId: string }; "player.play": { episodeId: string }; "player.pause": { episodeId: string }; "player.stop": {}; // Emitted when a NEW episode begins playback (not on resume). "player.started": { episodeId: string }; "toast.show": { message: string; variant: "info" | "success" | "warning" | "error"; title?: string; duration?: number; }; "dialog.open": { dialogId: string }; "dialog.close": { dialogId?: string }; "command.execute": { command: string; args?: unknown }; // Yazi-style unified router → active page dispatch. The Shell router // emits these; each page subscribes to the subset it implements. "nav.action": { action: KeybindActionName; tab: TABS; pane: PaneId; mode: NavMode; }; "clipboard.copied": { text: string }; "selection.start": { x: number; y: number }; "selection.end": { text: string }; // Multimedia key events (emitted by useMultimediaKeys, consumed by useAudio) "media.toggle": {}; "media.volumeUp": {}; "media.volumeDown": {}; "media.speedCycle": {}; }; // Type-safe emit and on functions export function emit( event: K, data: AppEvents[K], ): void { EventBus.emit(event, data); } export function on( event: K, handler: EventHandler, ): () => void { return EventBus.on(event, handler); } export function once( event: K, handler: EventHandler, ): () => void { return EventBus.once(event, handler); } export function off( event: K, handler: EventHandler, ): void { EventBus.off(event, handler); }