import color from "picocolors"; import { initKeypress, onKey, stopKeypress } from "./keyboard.js"; function clearScreen() { process.stdout.write('\x1Bc'); } function strWidth(str) { let width = 0; for (const char of str) { width += char.charCodeAt(0) > 127 ? 2 : 1; } return width; } function padEnd(str, width) { return str + " ".repeat(Math.max(0, width - strWidth(str))); } export async function gridSelect(options) { const { items, cols = 3, colWidth = 24, title = "", renderHeader = null, updateInfo = null, headerGap = 2, // header后的空行数 menuGap = 2, // 菜单和提示文字的间隔 rowGap = 1 // 每行菜单后的空行数 } = options; let current = 0; let onUpdate = updateInfo ? true : false; // 如果有更新,默认选中更新按钮 let resolved = false; 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(onUpdate)); } for (let i = 0; i < headerGap; i++) 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(pad + color.dim("↑ ↓ ← → 选择 | Enter 确认 | Esc 退出")); for (let i = 0; i < menuGap; i++) console.log(""); for (let row = 0; row < rows; row++) { let line = ""; let descLine = ""; for (let col = 0; col < cols; col++) { const idx = row * cols + col; if (idx < items.length) { const item = items[idx]; if (!onUpdate && idx === current) { line += color.cyan("[" + item.name + "]"); } else { line += " " + item.name + " "; } line += " ".repeat(colWidth - strWidth(item.name) - 2); descLine += padEnd(item.desc || "", colWidth); } } console.log(pad + line); console.log(pad + color.dim(descLine)); for (let i = 0; i < rowGap; i++) console.log(""); } } return new Promise(resolve => { initKeypress(); render(); onKey((str, key) => { if (!key || resolved) return; const row = Math.floor(current / cols); const col = current % cols; switch (key.name) { case "up": if (onUpdate) { // 已在更新按钮,不能再上 } else if (row === 0 && updateInfo) { onUpdate = true; render(); } else if (row > 0) { current -= cols; render(); } break; case "down": if (onUpdate) { onUpdate = false; render(); } else if (row < rows - 1 && current + cols < items.length) { current += cols; render(); } break; case "left": if (!onUpdate && col > 0) { current--; render(); } break; case "right": if (!onUpdate && col < cols - 1 && current < items.length - 1) { current++; render(); } break; case "return": resolved = true; stopKeypress(); if (onUpdate) { setImmediate(() => resolve({ isUpdate: true })); } else { setImmediate(() => resolve(items[current])); } break; case "escape": resolved = true; stopKeypress(); clearScreen(); console.log(color.yellow("再见!")); process.exit(0); break; case "c": if (key.ctrl) { resolved = true; stopKeypress(); clearScreen(); console.log(color.yellow("再见!")); process.exit(0); } break; } }); }); }