Files
yinx-cli/lib/utils/stepui.js
2025-12-20 11:51:35 +08:00

125 lines
4.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import color from "picocolors";
import { initKeypress, onKey } from "../keyboard.js";
export function createStepUI(options) {
const { title, getSteps } = options;
let steps = [];
let results = [];
let completed = new Set();
let currentStep = 0;
let currentOption = 0;
function initResults() {
steps = typeof getSteps === "function" ? getSteps() : getSteps;
results = steps.map(s => s.type === "multiselect" ? [...(s.default || [])] : s.default);
completed = new Set();
currentStep = 0;
currentOption = 0;
}
function renderNav() {
const nav = steps.map((step, i) => {
if (completed.has(i)) return color.green("☑ " + step.name);
if (i === currentStep) return color.bgCyan(color.black(" " + step.name + " "));
return color.dim("□ " + step.name);
});
return "← " + nav.join(" ") + " " + color.green("✓Submit") + " →";
}
function renderOptions() {
const step = steps[currentStep];
const lines = [color.cyan(step.message), ""];
if (!step.options || !step.options.length) {
lines.push(color.dim(step.emptyMessage || "无可用选项"));
return lines.join("\n");
}
step.options.forEach((opt, i) => {
const isCurrent = i === currentOption;
const isSelected = step.type === "multiselect"
? results[currentStep]?.includes(opt.value)
: results[currentStep] === opt.value;
const prefix = step.type === "multiselect"
? (isSelected ? color.green("◉ ") : "○ ")
: (isSelected ? color.green("● ") : "○ ");
const cursor = isCurrent ? color.cyan(" ") : " ";
const label = isCurrent ? color.cyan(opt.label) : opt.label;
const check = isSelected ? color.green(" ✓") : "";
lines.push(cursor + prefix + label + check);
if (opt.hint) lines.push(" " + color.dim(opt.hint));
});
return lines.join("\n");
}
function render() {
console.clear();
console.log(color.bgCyan(color.black(` ${title} `)));
console.log("\n" + renderNav());
console.log(color.dim("\n← → 切换步骤 | ↑ ↓ 选择 | Space 选中 | Tab 提交 | Esc 返回\n"));
console.log(renderOptions());
}
function runInteractive() {
initResults();
initKeypress();
return new Promise(resolve => {
render();
onKey((str, key) => {
if (!key) return;
const step = steps[currentStep];
const optCount = step.options?.length || 0;
if (key.name === "left") {
if (currentStep > 0) { currentStep--; currentOption = 0; }
render();
} else if (key.name === "right") {
if (currentStep < steps.length - 1) { currentStep++; currentOption = 0; }
render();
} else if (key.name === "up") {
if (!optCount) return;
currentOption = (currentOption - 1 + optCount) % optCount;
render();
} else if (key.name === "down") {
if (!optCount) return;
currentOption = (currentOption + 1) % optCount;
render();
} else if (key.name === "space") {
if (!optCount) return;
const opt = step.options[currentOption];
if (step.type === "multiselect") {
const list = results[currentStep];
const idx = list.indexOf(opt.value);
if (idx >= 0) list.splice(idx, 1);
else list.push(opt.value);
} else {
results[currentStep] = opt.value;
}
completed.add(currentStep);
render();
} else if (key.name === "return") {
if (!optCount) return;
if (step.type === "select") {
results[currentStep] = step.options[currentOption].value;
}
completed.add(currentStep);
if (currentStep < steps.length - 1) { currentStep++; currentOption = 0; }
render();
} else if (key.name === "tab") {
resolve({ steps, results });
} else if (key.name === "escape" || (key.ctrl && key.name === "c")) {
resolve(null);
}
});
});
}
function showSummary(lines) {
console.clear();
console.log(color.bgCyan(color.black(` ${title} `)));
console.log("\n" + color.green("配置完成!当前设置:"));
lines.forEach(line => console.log(" " + line));
console.log();
}
return { runInteractive, showSummary, initResults };
}