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 } = options; let current = 0; 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()); } 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("\n"); 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 (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)); console.log("\n"); } } 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 (row > 0) { current -= cols; render(); } break; case "down": if (row < rows - 1 && current + cols < items.length) { current += cols; render(); } break; case "left": if (col > 0) { current--; render(); } break; case "right": if (col < cols - 1 && current < items.length - 1) { current++; render(); } break; case "return": resolved = true; stopKeypress(); 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; } }); }); }