import { createSignal, createMemo } from "solid-js"; import { MdCommanderCommand, CommanderEntry, CompletionItem, } from "../types"; // ==================== 默认命令 ==================== export const defaultCommands: Record = { help: { command: "help", description: "显示帮助信息或特定命令的帮助", options: { cmd: { option: "cmd", description: "要查询的命令名", type: "enum", values: [], // 运行时填充 }, }, handler: (args) => { if (args.cmd) { return { message: `命令:${args.cmd}\n描述:${defaultCommands[args.cmd]?.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: "掷骰子", options: { formula: { option: "formula", description: "骰子公式,如 2d6+3", type: "string", required: true, }, }, handler: (args) => { const formula = args.formula || "1d6"; const match = formula.match(/^(\d+)?d(\d+)([+-]\d+)?$/i); if (!match) { return { message: `无效的骰子公式:${formula}`, type: "error" }; } const count = parseInt(match[1] || "1"); const sides = parseInt(match[2]); const modifier = parseInt(match[3] || "0"); const rolls: number[] = []; for (let i = 0; i < count; i++) { rolls.push(Math.floor(Math.random() * sides) + 1); } const total = rolls.reduce((a, b) => a + b, 0) + modifier; return { message: `${formula} = [${rolls.join(", ")}]${modifier !== 0 ? (modifier > 0 ? `+${modifier}` : modifier) : ""} = ${total}`, type: "success", }; }, }, }; // ==================== 工具函数 ==================== export function parseInput(input: string): { command?: string; options: Record; incompleteOption?: string; } { const result: { command?: string; options: Record; incompleteOption?: string; } = { 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; } while (i < parts.length) { const part = parts[i]; if (part.startsWith("--")) { 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 { result.incompleteOption = key; } } } i++; } 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); 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", description: cmd.description, insertText: cmd.command, })); if (commandCompletions.length > 0) { return commandCompletions; } } const cmd = commands[parsed.command!]; if (!cmd || !cmd.options) return []; if (parsed.incompleteOption) { const option = cmd.options[parsed.incompleteOption]; if (option?.type === "enum" && option.values) { return option.values .filter((v) => v.startsWith(parsed.incompleteOption)) .map((v) => ({ label: v, type: "value", insertText: v, })); } } 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; } 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 commands = { ...defaultCommands, ...customCommands }; // 更新 help 命令的选项值 if (commands.help?.options?.cmd) { commands.help.options.cmd.values = Object.keys(commands).filter( (k) => k !== "help", ); } const handleCommand = () => { const input = inputValue().trim(); if (!input) return; const parsed = parseInput(input); const commandName = parsed.command; const cmd = commands[commandName!]; let result: { message: string; type?: "success" | "error" | "warning" | "info" }; if (!cmd) { result = { message: `未知命令:${commandName}`, type: "error" }; } else if (cmd.handler) { try { result = cmd.handler(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]); 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); 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 { const optKey = parsed.incompleteOption; if (optKey) { const base = parsed.command || ""; const otherOptions = Object.entries(parsed.options) .filter(([k]) => k !== optKey) .map(([k, v]) => `--${k}=${v}`) .join(" "); newValue = `${base} --${optKey}=${comp.insertText}${otherOptions ? " " + otherOptions : ""}`; } else { newValue = input; } } setInputValue(newValue.trim()); setShowCompletions(false); }; return { inputValue, entries, showCompletions, completions, selectedCompletion, isFocused, setInputValue, setEntries, setShowCompletions, setSelectedCompletion, setIsFocused, handleCommand, updateCompletions, acceptCompletion, commands, }; }