import { createSignal } from "solid-js"; import { MdCommanderCommand, CommanderEntry, CompletionItem, } from "../types"; import { rollSimple } from "./useDiceRoller"; // ==================== 默认命令 ==================== export const defaultCommands: Record = { help: { command: "help", description: "显示帮助信息或特定命令的帮助", parameters: [ { name: "cmd", description: "要查询的命令名", type: "enum", values: [], // 运行时填充 required: false, }, ], handler: (args) => { const cmdName = args.params.cmd; if (cmdName) { return { message: `命令:${cmdName}\n描述:${defaultCommands[cmdName]?.description || "无描述"}`, type: "info", }; } const cmdList = Object.keys(defaultCommands).join(", "); return { message: `可用命令:${cmdList}`, type: "info", }; }, }, clear: { command: "clear", description: "清空命令历史", handler: () => ({ message: "命令历史已清空", type: "success", }), }, roll: { command: "roll", description: "掷骰子 - 支持骰池、修饰符和组合", parameters: [ { name: "formula", description: "骰子公式,如 3d6+{d4,d8}kh2-5", type: "string", required: true, }, ], handler: (args) => { const formula = args.params.formula || "1d6"; const result = rollSimple(formula); return { message: result.text, isHtml: result.isHtml, type: result.text.startsWith("错误") ? "error" : "success", }; }, }, }; // ==================== 工具函数 ==================== export function parseInput(input: string, commands?: Record): { command?: string; params: Record; options: Record; incompleteParam?: { index: number; value: string }; } { const result: { command?: string; params: Record; options: Record; incompleteParam?: { index: number; value: string }; } = { params: {}, options: {}, }; const trimmed = input.trim(); if (!trimmed) return result; const parts = trimmed.split(/\s+/); let i = 0; // 获取命令 if (parts[0] && !parts[0].startsWith("-")) { result.command = parts[0]; i = 1; } // 获取命令的参数定义 const cmd = result.command ? commands?.[result.command] : undefined; const paramDefs = cmd?.parameters || []; let paramIndex = 0; // 解析参数和选项 while (i < parts.length) { const part = parts[i]; if (part.startsWith("--")) { // 选项 --key=value 或 --key value const eqIndex = part.indexOf("="); if (eqIndex !== -1) { const key = part.slice(2, eqIndex); const value = part.slice(eqIndex + 1); result.options[key] = value; } else { const key = part.slice(2); if (i + 1 < parts.length && !parts[i + 1].startsWith("-")) { result.options[key] = parts[i + 1]; i++; } else { // 未完成的选项 } } } else if (part.startsWith("-") && part.length === 2) { // 短选项 -k value const key = part.slice(1); if (i + 1 < parts.length && !parts[i + 1].startsWith("-")) { result.options[key] = parts[i + 1]; i++; } } else { // 位置参数 if (paramIndex < paramDefs.length) { const paramDef = paramDefs[paramIndex]; result.params[paramDef.name] = part; paramIndex++; } } i++; } // 检查是否有未完成的位置参数 if (paramIndex < paramDefs.length) { const lastPart = parts[parts.length - 1]; if (lastPart && !lastPart.startsWith("-")) { result.incompleteParam = { index: paramIndex, value: lastPart, }; } } return result; } export function getCompletions( input: string, commands: Record, ): CompletionItem[] { const trimmed = input.trim(); if (!trimmed || /^\s*$/.test(trimmed)) { return Object.values(commands).map((cmd) => ({ label: cmd.command, type: "command", description: cmd.description, insertText: cmd.command, })); } const parsed = parseInput(trimmed, commands); if (!parsed.command || !commands[parsed.command]) { const commandCompletions = Object.values(commands) .filter((cmd) => cmd.command.startsWith(parsed.command || "")) .map((cmd) => ({ label: cmd.command, type: "command" as "command", description: cmd.description, insertText: cmd.command, })); if (commandCompletions.length > 0) { return commandCompletions; } } const cmd = commands[parsed.command!]; if (!cmd) return []; // 检查是否需要参数补全 const paramDefs = cmd.parameters || []; const usedParams = Object.keys(parsed.params); // 如果还有未填的参数,提供参数值补全 if (paramDefs.length > usedParams.length) { const paramDef = paramDefs[usedParams.length]; if (paramDef.type === "enum" && paramDef.values) { const currentValue = parsed.incompleteParam?.value || ""; return paramDef.values .filter((v) => v.startsWith(currentValue)) .map((v) => ({ label: v, type: "value", description: paramDef.description, insertText: v, })); } // 其他类型的参数,显示提示 if (parsed.incompleteParam) { return [{ label: `<${paramDef.name}>`, type: "value", description: `${paramDef.type}${paramDef.required !== false ? " (必填)" : ""}: ${paramDef.description || ""}`, insertText: "", }]; } } // 选项补全 if (!cmd.options) return []; const usedOptions = Object.keys(parsed.options); return Object.values(cmd.options) .filter((opt) => !usedOptions.includes(opt.option)) .map((opt) => ({ label: `--${opt.option}`, type: "option", description: opt.description, insertText: `--${opt.option}=${opt.type === "boolean" ? "" : ""}`, })); } export function getResultClass( type?: "success" | "error" | "warning" | "info", ): string { switch (type) { case "success": return "text-green-600"; case "error": return "text-red-600"; case "warning": return "text-yellow-600"; case "info": default: return "text-blue-600"; } } // ==================== Commander Hook ==================== export interface UseCommanderReturn { inputValue: () => string; entries: () => CommanderEntry[]; showCompletions: () => boolean; completions: () => CompletionItem[]; selectedCompletion: () => number; isFocused: () => boolean; setInputValue: (v: string) => void; setEntries: (updater: (prev: CommanderEntry[]) => CommanderEntry[]) => void; setShowCompletions: (v: boolean) => void; setSelectedCompletion: (v: number | ((prev: number) => number)) => void; setIsFocused: (v: boolean) => void; handleCommand: () => void; updateCompletions: () => void; acceptCompletion: () => void; commands: Record; historyIndex: () => number; setHistoryIndex: (v: number) => void; commandHistory: () => string[]; navigateHistory: (direction: 'up' | 'down') => void; } export function useCommander( customCommands?: Record, ): UseCommanderReturn { const [inputValue, setInputValue] = createSignal(""); const [entries, setEntries] = createSignal([]); const [showCompletions, setShowCompletions] = createSignal(false); const [completions, setCompletions] = createSignal([]); const [selectedCompletion, setSelectedCompletion] = createSignal(0); const [isFocused, setIsFocused] = createSignal(false); // 命令历史 const [commandHistory, setCommandHistory] = createSignal([]); const [historyIndex, setHistoryIndex] = createSignal(-1); const commands = { ...defaultCommands, ...customCommands }; // 更新 help 命令的参数值 if (commands.help?.parameters?.[0]) { commands.help.parameters[0].values = Object.keys(commands).filter( (k) => k !== "help", ); } const handleCommand = () => { const input = inputValue().trim(); if (!input) return; const parsed = parseInput(input, commands); const commandName = parsed.command; const cmd = commands[commandName!]; let result: { message: string; type?: "success" | "error" | "warning" | "info"; isHtml?: boolean }; if (!cmd) { result = { message: `未知命令:${commandName}`, type: "error" }; } else if (cmd.handler) { try { result = cmd.handler({ params: parsed.params, options: parsed.options }); } catch (e) { result = { message: `执行错误:${e instanceof Error ? e.message : String(e)}`, type: "error", }; } } else { result = { message: `命令 ${commandName} 已执行(无处理器)`, type: "info" }; } const newEntry: CommanderEntry = { id: Date.now().toString() + Math.random().toString(36).slice(2), command: input, args: parsed.options, result, timestamp: new Date(), }; setEntries((prev) => [...prev, newEntry]); // 添加到命令历史 setCommandHistory((prev) => [...prev, input]); setHistoryIndex(-1); // 重置历史索引 setInputValue(""); setShowCompletions(false); if (commandName === "clear") { setEntries([]); } }; const updateCompletions = () => { const input = inputValue(); const comps = getCompletions(input, commands); setCompletions(comps); setShowCompletions(comps.length > 0 && isFocused()); setSelectedCompletion(0); }; const acceptCompletion = () => { const idx = selectedCompletion(); const comp = completions()[idx]; if (!comp) return; const input = inputValue(); const parsed = parseInput(input, commands); let newValue: string; if (comp.type === "command") { newValue = comp.insertText + " "; } else if (comp.type === "option") { const base = parsed.command || ""; const existingOptions = Object.entries(parsed.options) .map(([k, v]) => `--${k}=${v}`) .join(" "); newValue = `${base} ${existingOptions}${existingOptions ? " " : ""}${comp.insertText}`; } else if (comp.type === "value") { // 参数值补全 const cmd = parsed.command ? commands[parsed.command] : null; const paramDefs = cmd?.parameters || []; const usedParams = Object.keys(parsed.params); if (paramDefs.length > usedParams.length) { // 当前参数的补全 const base = parsed.command || ""; const existingParams = Object.values(parsed.params).join(" "); const existingOptions = Object.entries(parsed.options) .map(([k, v]) => `--${k}=${v}`) .join(" "); newValue = `${base} ${existingParams}${existingParams ? " " : ""}${comp.insertText}${existingOptions ? " " + existingOptions : ""}`; } else { newValue = input; } } else { newValue = input; } setInputValue(newValue.trim()); setShowCompletions(false); }; const navigateHistory = (direction: 'up' | 'down') => { const history = commandHistory(); if (history.length === 0) return; let newIndex = historyIndex(); if (direction === 'up') { // 向上浏览历史(更早的命令) if (newIndex < history.length - 1) { newIndex++; } } else { // 向下浏览历史(更新的命令) if (newIndex > 0) { newIndex--; } else { // 回到当前输入 setInputValue(""); setHistoryIndex(-1); return; } } setHistoryIndex(newIndex); // 从历史末尾获取命令(最新的在前) setInputValue(history[history.length - 1 - newIndex]); }; return { inputValue, entries, showCompletions, completions, selectedCompletion, isFocused, setInputValue, setEntries, setShowCompletions, setSelectedCompletion, setIsFocused, handleCommand, updateCompletions, acceptCompletion, commands, historyIndex, setHistoryIndex, commandHistory, navigateHistory, }; }