130 lines
3.4 KiB
JavaScript
130 lines
3.4 KiB
JavaScript
import color from "picocolors";
|
|
import { initKeypress, onKey } from "./keyboard.js";
|
|
|
|
function clearScreen() {
|
|
if (process.stdout.isTTY) {
|
|
process.stdout.write("\x1b[2J"); // clear screen
|
|
process.stdout.write("\x1b[3J"); // clear scrollback
|
|
process.stdout.write("\x1b[H"); // move cursor home
|
|
} else {
|
|
console.clear();
|
|
}
|
|
}
|
|
|
|
// 计算字符串显示宽度
|
|
function strWidth(str) {
|
|
let width = 0;
|
|
for (const char of str) {
|
|
width += char.charCodeAt(0) > 127 ? 2 : 1;
|
|
}
|
|
return width;
|
|
}
|
|
|
|
// 右填充到指定宽度
|
|
function padEnd(str, width) {
|
|
const diff = width - strWidth(str);
|
|
return str + " ".repeat(Math.max(0, diff));
|
|
}
|
|
|
|
/**
|
|
* 网格选择菜单
|
|
*/
|
|
export async function gridSelect(options) {
|
|
const {
|
|
items,
|
|
cols = 3,
|
|
colWidth = 24,
|
|
title = "",
|
|
renderHeader = null
|
|
} = options;
|
|
|
|
let current = 0;
|
|
const rows = Math.ceil(items.length / cols);
|
|
const termWidth = process.stdout.columns || 80;
|
|
const totalWidth = cols * colWidth;
|
|
const pad = " ".repeat(Math.max(0, Math.floor((termWidth - totalWidth) / 2)));
|
|
|
|
function render() {
|
|
clearScreen();
|
|
|
|
if (renderHeader) {
|
|
console.log(renderHeader());
|
|
}
|
|
|
|
console.log("");
|
|
|
|
if (title) {
|
|
const titlePad = " ".repeat(Math.max(0, Math.floor((termWidth - title.length - 4) / 2)));
|
|
console.log(titlePad + color.bgMagenta(color.white(` ${title} `)));
|
|
}
|
|
|
|
console.log("");
|
|
console.log(pad + color.dim("↑ ↓ ← → 选择 | Enter 确认 | Esc 退出"));
|
|
console.log("");
|
|
console.log("");
|
|
|
|
for (let row = 0; row < rows; row++) {
|
|
let line = "";
|
|
for (let col = 0; col < cols; col++) {
|
|
const idx = row * cols + col;
|
|
if (idx < items.length) {
|
|
const item = items[idx];
|
|
if (idx === current) {
|
|
line += color.cyan("[" + item.name + "]");
|
|
line += " ".repeat(colWidth - strWidth(item.name) - 2);
|
|
} else {
|
|
line += " " + item.name + " ";
|
|
line += " ".repeat(colWidth - strWidth(item.name) - 2);
|
|
}
|
|
}
|
|
}
|
|
console.log(pad + line);
|
|
|
|
let descLine = "";
|
|
for (let col = 0; col < cols; col++) {
|
|
const idx = row * cols + col;
|
|
if (idx < items.length) {
|
|
descLine += padEnd(items[idx].desc || "", colWidth);
|
|
}
|
|
}
|
|
console.log(pad + color.dim(descLine));
|
|
console.log("");
|
|
console.log("");
|
|
}
|
|
}
|
|
|
|
initKeypress();
|
|
|
|
return new Promise((resolve) => {
|
|
render();
|
|
|
|
onKey((str, key) => {
|
|
if (!key) return;
|
|
|
|
const row = Math.floor(current / cols);
|
|
const col = current % cols;
|
|
|
|
if (key.name === "up" && row > 0) {
|
|
current -= cols;
|
|
render();
|
|
} else if (key.name === "down" && row < rows - 1 && current + cols < items.length) {
|
|
current += cols;
|
|
render();
|
|
} else if (key.name === "left" && col > 0) {
|
|
current--;
|
|
render();
|
|
} else if (key.name === "right" && col < cols - 1 && current < items.length - 1) {
|
|
current++;
|
|
render();
|
|
} else if (key.name === "return") {
|
|
setImmediate(() => resolve(items[current]));
|
|
} else if (key.name === "escape" || (key.ctrl && key.name === "c")) {
|
|
process.stdin.setRawMode(false);
|
|
clearScreen();
|
|
console.log(color.yellow("👋 再见!"));
|
|
process.exit(0);
|
|
}
|
|
});
|
|
});
|
|
}
|