Files
yinx-cli/lib/utils/stepui.js
2025-12-20 14:13:59 +08:00

174 lines
5.1 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, stopKeypress } from "../keyboard.js";
function clearScreen() {
process.stdout.write('\x1Bc');
}
export function createStepUI(options) {
const { title, getSteps } = options;
let steps = [];
let results = [];
let completed = new Set();
let currentStep = 0;
let currentOption = 0;
let resolved = false;
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;
resolved = false;
}
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");
}
const termHeight = process.stdout.rows || 24;
const maxVisible = Math.max(5, termHeight - 8);
const total = step.options.length;
let start = 0;
if (total > maxVisible) {
start = Math.max(0, Math.min(currentOption - Math.floor(maxVisible / 2), total - maxVisible));
}
const end = Math.min(start + maxVisible, total);
if (start > 0) lines.push(color.dim(" ↑ 更多选项..."));
for (let i = start; i < end; i++) {
const opt = step.options[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 (end < total) lines.push(color.dim(" ↓ 更多选项..."));
return lines.join("\n");
}
function render() {
clearScreen();
console.log(color.bgCyan(color.black(` ${title} `)));
console.log("\n" + renderNav());
console.log(color.dim("\n← → 切换步骤 | ↑ ↓ 选择 | Space 选中 | Tab 提交 | Esc 返回\n"));
console.log(renderOptions());
}
function handleKey(key, resolve) {
if (!key || resolved) return;
const step = steps[currentStep];
const optCount = step.options?.length || 0;
switch (key.name) {
case "left":
if (currentStep > 0) { currentStep--; currentOption = 0; }
render();
break;
case "right":
if (currentStep < steps.length - 1) { currentStep++; currentOption = 0; }
render();
break;
case "up":
if (optCount) {
currentOption = (currentOption - 1 + optCount) % optCount;
render();
}
break;
case "down":
if (optCount) {
currentOption = (currentOption + 1) % optCount;
render();
}
break;
case "space":
if (optCount) {
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();
}
break;
case "return":
if (optCount && step.type === "select") {
results[currentStep] = step.options[currentOption].value;
completed.add(currentStep);
if (currentStep < steps.length - 1) { currentStep++; currentOption = 0; }
render();
}
break;
case "tab":
resolved = true;
stopKeypress();
setImmediate(() => resolve({ steps, results }));
break;
case "escape":
resolved = true;
stopKeypress();
setImmediate(() => resolve(null));
break;
case "c":
if (key.ctrl) {
resolved = true;
stopKeypress();
setImmediate(() => resolve(null));
}
break;
}
}
function runInteractive() {
initResults();
initKeypress();
return new Promise(resolve => {
render();
onKey((str, key) => handleKey(key, resolve));
});
}
function showSummary(lines) {
clearScreen();
console.log(color.bgCyan(color.black(` ${title} `)));
console.log("\n" + color.green("配置完成!当前设置:"));
lines.forEach(line => console.log(" " + line));
console.log();
}
return { runInteractive, showSummary, initResults };
}