import { createContext, useCallback, useContext, useMemo, useRef, useState, type ReactNode, } from "react"; export interface CommandPaletteItem { key: string; label: string; hint?: string; } export interface RuntimeCommand { id: string; title: string; hint?: string; section?: string; keybinds?: string[]; disabled?: boolean; visible?: boolean; run: () => void | Promise; } interface RegisteredCommand extends RuntimeCommand { order: number; registrationId: number; } export interface CommandRegistry { commands: RuntimeCommand[]; registerCommand: (command: RuntimeCommand) => () => void; registerCommands: (commands: RuntimeCommand[]) => () => void; unregisterCommand: (id: string) => void; clearCommands: () => void; runCommand: (id: string) => Promise; runKeybinding: (binding: string) => Promise; } interface CommandRegistryContextValue extends CommandRegistry {} const CommandRegistryContext = createContext(null); export interface CommandRegistryProviderProps { children: ReactNode; initialCommands?: RuntimeCommand[]; } export function normalizeBinding(binding: string): string { return binding.trim().toLowerCase(); } function isCommandVisible(command: RuntimeCommand): boolean { return command.visible !== false; } export function commandToPaletteItem(command: RuntimeCommand): CommandPaletteItem { const keybindHint = command.keybinds && command.keybinds.length > 0 ? command.keybinds.map(normalizeBinding).join(", ") : null; const hint = [command.section, command.hint, keybindHint].filter(Boolean).join(" ยท ") || undefined; return { key: command.id, label: command.title, hint, }; } export function shouldCleanupRegisteredCommand( entry: { id: string; registrationId: number }, commandId: string, registrationId: number, ): boolean { return entry.id === commandId && entry.registrationId === registrationId; } export function CommandRegistryProvider({ children, initialCommands = [], }: CommandRegistryProviderProps) { const orderCounterRef = useRef(0); const registrationCounterRef = useRef(0); const toRegistered = useCallback( (command: RuntimeCommand, registrationId: number): RegisteredCommand => ({ ...command, order: orderCounterRef.current++, registrationId, }), [], ); const [commands, setCommands] = useState(() => initialCommands.map((command) => toRegistered(command, registrationCounterRef.current++)), ); const commandMapRef = useRef>(new Map()); commandMapRef.current = new Map(commands.map((command) => [command.id, command])); const registerCommand = useCallback( (command: RuntimeCommand) => { const registrationId = registrationCounterRef.current++; setCommands((prev) => { const existingIndex = prev.findIndex((entry) => entry.id === command.id); if (existingIndex < 0) { return [...prev, toRegistered(command, registrationId)]; } const existing = prev[existingIndex]; if (!existing) return prev; const next = [...prev]; next[existingIndex] = { ...existing, ...command, order: existing.order, registrationId, }; return next; }); return () => { setCommands((prev) => prev.filter( (entry) => !shouldCleanupRegisteredCommand(entry, command.id, registrationId), ), ); }; }, [toRegistered], ); const registerCommands = useCallback( (nextCommands: RuntimeCommand[]) => { const unregisterHandlers = nextCommands.map((command) => registerCommand(command)); return () => { for (const unregister of unregisterHandlers) unregister(); }; }, [registerCommand], ); const unregisterCommand = useCallback((id: string) => { setCommands((prev) => prev.filter((entry) => entry.id !== id)); }, []); const clearCommands = useCallback(() => { setCommands([]); }, []); const runCommand = useCallback(async (id: string): Promise => { const command = commandMapRef.current.get(id); if (!command || command.disabled || !isCommandVisible(command)) { return false; } await command.run(); return true; }, []); const runKeybinding = useCallback(async (binding: string): Promise => { const normalized = normalizeBinding(binding); if (!normalized) return false; const ordered = [...commandMapRef.current.values()].sort( (left, right) => left.order - right.order, ); for (const command of ordered) { if (command.disabled || !isCommandVisible(command)) { continue; } const bindings = command.keybinds?.map(normalizeBinding) ?? []; if (!bindings.includes(normalized)) { continue; } await command.run(); return true; } return false; }, []); const value = useMemo( () => ({ commands: [...commands] .sort((left, right) => left.order - right.order) .map((command) => ({ id: command.id, title: command.title, hint: command.hint, section: command.section, keybinds: command.keybinds, disabled: command.disabled, visible: command.visible, run: command.run, })), registerCommand, registerCommands, unregisterCommand, clearCommands, runCommand, runKeybinding, }), [ clearCommands, commands, registerCommand, registerCommands, runCommand, runKeybinding, unregisterCommand, ], ); return ( {children} ); } export function useCommandRegistry(): CommandRegistry { const context = useContext(CommandRegistryContext); if (!context) { throw new Error( "Command registry is not available. Wrap your app in .", ); } return context; } export function useCommandRegistryItems(): CommandPaletteItem[] { const registry = useCommandRegistry(); return useMemo( () => registry.commands .filter((command) => !command.disabled && isCommandVisible(command)) .map(commandToPaletteItem), [registry.commands], ); }