mirror of
https://github.com/zadam/trilium.git
synced 2025-10-20 15:19:01 +02:00
264 lines
8.8 KiB
TypeScript
264 lines
8.8 KiB
TypeScript
import appContext, { type CommandNames } from "../components/app_context.js";
|
|
import type NoteTreeWidget from "../widgets/note_tree.js";
|
|
import { t, translationsInitializedPromise } from "./i18n.js";
|
|
import keyboardActions, { Action } from "./keyboard_actions.js";
|
|
import utils from "./utils.js";
|
|
|
|
export interface CommandDefinition {
|
|
id: string;
|
|
name: string;
|
|
description?: string;
|
|
icon?: string;
|
|
shortcut?: string;
|
|
commandName?: CommandNames;
|
|
handler?: () => Promise<unknown> | null | undefined | void;
|
|
aliases?: string[];
|
|
source?: "manual" | "keyboard-action";
|
|
/** Reference to the original keyboard action for scope checking. */
|
|
keyboardAction?: Action;
|
|
}
|
|
|
|
class CommandRegistry {
|
|
private commands: Map<string, CommandDefinition> = new Map();
|
|
private aliases: Map<string, string> = new Map();
|
|
|
|
constructor() {
|
|
this.loadCommands();
|
|
}
|
|
|
|
private async loadCommands() {
|
|
await translationsInitializedPromise;
|
|
this.registerDefaultCommands();
|
|
await this.loadKeyboardActionsAsync();
|
|
}
|
|
|
|
private registerDefaultCommands() {
|
|
this.register({
|
|
id: "export-note",
|
|
name: t("command_palette.export_note_title"),
|
|
description: t("command_palette.export_note_description"),
|
|
icon: "bx bx-export",
|
|
handler: () => {
|
|
const notePath = appContext.tabManager.getActiveContextNotePath();
|
|
if (notePath) {
|
|
appContext.triggerCommand("showExportDialog", {
|
|
notePath,
|
|
defaultType: "single"
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
this.register({
|
|
id: "show-attachments",
|
|
name: t("command_palette.show_attachments_title"),
|
|
description: t("command_palette.show_attachments_description"),
|
|
icon: "bx bx-paperclip",
|
|
handler: () => appContext.triggerCommand("showAttachments")
|
|
});
|
|
|
|
// Special search commands with custom logic
|
|
this.register({
|
|
id: "search-notes",
|
|
name: t("command_palette.search_notes_title"),
|
|
description: t("command_palette.search_notes_description"),
|
|
icon: "bx bx-search",
|
|
handler: () => appContext.triggerCommand("searchNotes", {})
|
|
});
|
|
|
|
this.register({
|
|
id: "search-in-subtree",
|
|
name: t("command_palette.search_subtree_title"),
|
|
description: t("command_palette.search_subtree_description"),
|
|
icon: "bx bx-search-alt",
|
|
handler: () => {
|
|
const notePath = appContext.tabManager.getActiveContextNotePath();
|
|
if (notePath) {
|
|
appContext.triggerCommand("searchInSubtree", { notePath });
|
|
}
|
|
}
|
|
});
|
|
|
|
this.register({
|
|
id: "show-search-history",
|
|
name: t("command_palette.search_history_title"),
|
|
description: t("command_palette.search_history_description"),
|
|
icon: "bx bx-history",
|
|
handler: () => appContext.triggerCommand("showSearchHistory")
|
|
});
|
|
|
|
this.register({
|
|
id: "show-launch-bar",
|
|
name: t("command_palette.configure_launch_bar_title"),
|
|
description: t("command_palette.configure_launch_bar_description"),
|
|
icon: "bx bx-sidebar",
|
|
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;
|
|
}
|
|
|
|
// Skip Electron-only actions if not in Electron environment
|
|
if (action.isElectronOnly && !utils.isElectron()) {
|
|
continue;
|
|
}
|
|
|
|
// Get the primary shortcut (first one in the list)
|
|
const primaryShortcut = action.effectiveShortcuts?.[0];
|
|
|
|
let name = action.friendlyName;
|
|
if (action.scope === "note-tree") {
|
|
name = t("command_palette.tree-action-name", { name: action.friendlyName });
|
|
}
|
|
|
|
// Create a command definition from the keyboard action
|
|
const commandDef: CommandDefinition = {
|
|
id: action.actionName,
|
|
name,
|
|
description: action.description,
|
|
icon: action.iconClass,
|
|
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, ' + ');
|
|
}
|
|
|
|
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;
|