import appContext, { type CommandNames } from "../components/app_context.js"; import type NoteTreeWidget from "../widgets/note_tree.js"; import keyboardActions, { Action } from "./keyboard_actions.js"; export interface CommandDefinition { id: string; name: string; description?: string; icon?: string; shortcut?: string; commandName?: CommandNames; handler?: () => void | Promise; aliases?: string[]; source?: "manual" | "keyboard-action"; /** Reference to the original keyboard action for scope checking. */ keyboardAction?: Action; } class CommandRegistry { private commands: Map = new Map(); private aliases: Map = new Map(); constructor() { this.registerDefaultCommands(); this.loadKeyboardActionsAsync(); } private registerDefaultCommands() { // Keep only commands with custom handlers or better descriptions this.register({ id: "toggle-left-pane", name: "Toggle Left Pane", description: "Show/hide the note tree sidebar", icon: "bx bx-sidebar", handler: () => appContext.triggerCommand("toggleLeftPane") }); this.register({ id: "show-options", name: "Show Options", description: "Open settings/preferences", icon: "bx bx-cog", commandName: "showOptions", aliases: ["settings", "preferences"] }); this.register({ id: "show-help", name: "Show Help", description: "Open help documentation", icon: "bx bx-help-circle", handler: () => appContext.triggerCommand("showHelp") }); // Special commands with custom handlers this.register({ id: "show-sql-console", name: "Show SQL Console", description: "Open SQL console page", icon: "bx bx-data", commandName: "showSQLConsole" }); this.register({ id: "export-note", name: "Export Note", description: "Export current note", icon: "bx bx-export", handler: () => { const notePath = appContext.tabManager.getActiveContextNotePath(); if (notePath) { appContext.triggerCommand("showExportDialog", { notePath, defaultType: "single" }); } } }); this.register({ id: "show-note-source", name: "Show Note Source", description: "View note in source mode", icon: "bx bx-code", handler: () => appContext.triggerCommand("showNoteSource") }); this.register({ id: "show-attachments", name: "Show Attachments", description: "View note attachments", icon: "bx bx-paperclip", handler: () => appContext.triggerCommand("showAttachments") }); // Special search commands with custom logic this.register({ id: "search-notes", name: "Search Notes", description: "Open advanced search", icon: "bx bx-search", handler: () => appContext.triggerCommand("searchNotes", {}) }); this.register({ id: "search-in-subtree", name: "Search in Subtree", description: "Search within current subtree", icon: "bx bx-search-alt", handler: () => { const notePath = appContext.tabManager.getActiveContextNotePath(); if (notePath) { appContext.triggerCommand("searchInSubtree", { notePath }); } } }); this.register({ id: "show-search-history", name: "Show Search History", description: "View previous searches", icon: "bx bx-history", handler: () => appContext.triggerCommand("showSearchHistory") }); this.register({ id: "show-backend-log", name: "Show Backend Log", description: "View server logs", icon: "bx bx-terminal", handler: () => appContext.triggerCommand("showBackendLog") }); this.register({ id: "show-recent-changes", name: "Show Recent Changes", description: "View recently modified notes", icon: "bx bx-time", handler: () => appContext.triggerCommand("showRecentChanges", { ancestorNoteId: "root" }) }); this.register({ id: "show-launch-bar", name: "Show Launch Bar", description: "Open the launch bar subtree", icon: "bx bx-grid-alt", handler: () => appContext.triggerCommand("showLaunchBarSubtree") }); } private async loadKeyboardActionsAsync() { try { const actions = await keyboardActions.getActions(); this.registerKeyboardActions(actions); } catch (error) { console.error("Failed to load keyboard actions:", error); } } private registerKeyboardActions(actions: Action[]) { for (const action of actions) { // Skip actions that we've already manually registered if (this.commands.has(action.actionName)) { continue; } // Skip actions that don't have a description (likely separators) if (!action.description) { continue; } // Get the primary shortcut (first one in the list) const primaryShortcut = action.effectiveShortcuts?.[0]; // Create a command definition from the keyboard action const commandDef: CommandDefinition = { id: action.actionName, name: action.friendlyName, description: action.description, icon: action.iconClass || this.getIconForAction(action.actionName), shortcut: primaryShortcut ? this.formatShortcut(primaryShortcut) : undefined, commandName: action.actionName as CommandNames, source: "keyboard-action", keyboardAction: action }; this.register(commandDef); } } private formatShortcut(shortcut: string): string { // Convert electron accelerator format to display format return shortcut .replace(/CommandOrControl/g, 'Ctrl') .replace(/\+/g, ' + '); } private getIconForAction(actionName: string): string { // Map common action patterns to icons const iconMap: Record = { // Navigation 'jumpToNote': 'bx bx-search', 'commandPalette': 'bx bx-command', 'scrollToActiveNote': 'bx bx-target-lock', 'backInNoteHistory': 'bx bx-arrow-back', 'forwardInNoteHistory': 'bx bx-arrow-forward', // Tree operations 'collapseTree': 'bx bx-collapse', 'collapseSubtree': 'bx bx-minus-circle', 'expandSubtree': 'bx bx-plus-circle', 'sortChildNotes': 'bx bx-sort', // Note operations 'createNoteAfter': 'bx bx-plus', 'createNoteInto': 'bx bx-plus-circle', 'createNoteIntoInbox': 'bx bx-inbox', 'deleteNotes': 'bx bx-trash', 'editNoteTitle': 'bx bx-edit', 'duplicateSubtree': 'bx bx-copy', // Movement 'moveNoteUp': 'bx bx-up-arrow', 'moveNoteDown': 'bx bx-down-arrow', 'moveNoteUpInHierarchy': 'bx bx-left-arrow', 'moveNoteDownInHierarchy': 'bx bx-right-arrow', // Clipboard 'copyNotesToClipboard': 'bx bx-copy', 'cutNotesToClipboard': 'bx bx-cut', 'pasteNotesFromClipboard': 'bx bx-paste', // Tabs 'openNewTab': 'bx bx-tab', 'closeActiveTab': 'bx bx-x', 'activateNextTab': 'bx bx-chevron-right', 'activatePreviousTab': 'bx bx-chevron-left', 'reopenLastTab': 'bx bx-refresh', // Windows 'openNewWindow': 'bx bx-window-open', 'toggleTray': 'bx bx-hide', 'toggleZenMode': 'bx bx-fullscreen', // Search 'quickSearch': 'bx bx-search-alt', 'searchInSubtree': 'bx bx-search-alt-2', // Other 'runActiveNote': 'bx bx-play', 'showOptions': 'bx bx-cog' }; return iconMap[actionName] || 'bx bx-command'; } register(command: CommandDefinition) { this.commands.set(command.id, command); // Register aliases if (command.aliases) { for (const alias of command.aliases) { this.aliases.set(alias.toLowerCase(), command.id); } } } getCommand(id: string): CommandDefinition | undefined { return this.commands.get(id); } getAllCommands(): CommandDefinition[] { return Array.from(this.commands.values()); } searchCommands(query: string): CommandDefinition[] { const normalizedQuery = query.toLowerCase(); const results: { command: CommandDefinition; score: number }[] = []; for (const command of this.commands.values()) { let score = 0; // Exact match on name if (command.name.toLowerCase() === normalizedQuery) { score = 100; } // Name starts with query else if (command.name.toLowerCase().startsWith(normalizedQuery)) { score = 80; } // Name contains query else if (command.name.toLowerCase().includes(normalizedQuery)) { score = 60; } // Description contains query else if (command.description?.toLowerCase().includes(normalizedQuery)) { score = 40; } // Check aliases else if (command.aliases?.some(alias => alias.toLowerCase().includes(normalizedQuery))) { score = 50; } if (score > 0) { results.push({ command, score }); } } // Sort by score (highest first) and then by name results.sort((a, b) => { if (a.score !== b.score) { return b.score - a.score; } return a.command.name.localeCompare(b.command.name); }); return results.map(r => r.command); } async executeCommand(commandId: string) { const command = this.getCommand(commandId); if (!command) { console.error(`Command not found: ${commandId}`); return; } // Execute custom handler if provided if (command.handler) { await command.handler(); return; } // Handle keyboard action with scope-aware execution if (command.keyboardAction && command.commandName) { if (command.keyboardAction.scope === "note-tree") { this.executeWithNoteTreeFocus(command.commandName); } else { appContext.triggerCommand(command.commandName); } return; } // Fallback for commands without keyboard action reference if (command.commandName) { appContext.triggerCommand(command.commandName); return; } console.error(`Command ${commandId} has no handler or commandName`); } private executeWithNoteTreeFocus(actionName: CommandNames) { const tree = document.querySelector(".tree-wrapper") as HTMLElement; if (!tree) { return; } const treeComponent = appContext.getComponentByEl(tree) as NoteTreeWidget; treeComponent.triggerCommand(actionName, { ntxId: appContext.tabManager.activeNtxId }); } } const commandRegistry = new CommandRegistry(); export default commandRegistry;