feat: ai's attempt
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
export { useCommander, defaultCommands, parseInput, getCompletions, getResultClass } from './useCommander';
|
||||
export type { UseCommanderReturn } from './useCommander';
|
||||
@@ -0,0 +1,342 @@
|
||||
import { createSignal, createMemo } from "solid-js";
|
||||
import {
|
||||
MdCommanderCommand,
|
||||
CommanderEntry,
|
||||
CompletionItem,
|
||||
} from "../types";
|
||||
|
||||
// ==================== 默认命令 ====================
|
||||
|
||||
export const defaultCommands: Record<string, MdCommanderCommand> = {
|
||||
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<string, string>;
|
||||
incompleteOption?: string;
|
||||
} {
|
||||
const result: {
|
||||
command?: string;
|
||||
options: Record<string, string>;
|
||||
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<string, MdCommanderCommand>,
|
||||
): 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<string, MdCommanderCommand>;
|
||||
}
|
||||
|
||||
export function useCommander(
|
||||
customCommands?: Record<string, MdCommanderCommand>,
|
||||
): UseCommanderReturn {
|
||||
const [inputValue, setInputValue] = createSignal("");
|
||||
const [entries, setEntries] = createSignal<CommanderEntry[]>([]);
|
||||
const [showCompletions, setShowCompletions] = createSignal(false);
|
||||
const [completions, setCompletions] = createSignal<CompletionItem[]>([]);
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user