import color from "picocolors"; import { initKeypress, onKey } from "./keyboard.js"; // 计算字符串显示宽度 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() { console.clear(); 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); console.clear(); console.log(color.yellow("👋 再见!")); process.exit(0); } }); }); }