优化第一阶段
This commit is contained in:
104
lib/grid.js
104
lib/grid.js
@ -1,21 +1,6 @@
|
||||
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)));
|
||||
}
|
||||
import { clearScreen, padEnd, strWidth, centerPad } from "./utils/terminal.js";
|
||||
|
||||
export async function gridSelect(options) {
|
||||
const {
|
||||
@ -25,15 +10,22 @@ export async function gridSelect(options) {
|
||||
title = "",
|
||||
renderHeader = null,
|
||||
updateInfo = null,
|
||||
headerGap = 2, // header后的空行数
|
||||
menuGap = 2, // 菜单和提示文字的间隔
|
||||
rowGap = 1 // 每行菜单后的空行数
|
||||
headerGap = 2,
|
||||
menuGap = 2,
|
||||
rowGap = 1,
|
||||
instructions = "←→ 选择 | Enter 确认 | Esc 退出",
|
||||
onCancel = "exit", // "exit" | "back"
|
||||
mapper,
|
||||
showUpdateButton = true,
|
||||
} = options;
|
||||
|
||||
const hasUpdate = showUpdateButton && !!updateInfo;
|
||||
const mapResult = mapper || ((item, isUpdate) => (isUpdate ? { isUpdate: true } : item));
|
||||
|
||||
let current = 0;
|
||||
let onUpdate = updateInfo ? true : false; // 如果有更新,默认选中更新按钮
|
||||
let onUpdate = hasUpdate;
|
||||
let resolved = false;
|
||||
const rows = Math.ceil(items.length / cols);
|
||||
const rows = Math.ceil((items?.length || 0) / cols);
|
||||
const termWidth = process.stdout.columns || 80;
|
||||
const totalWidth = cols * colWidth;
|
||||
const pad = " ".repeat(Math.max(0, Math.floor((termWidth - totalWidth) / 2)));
|
||||
@ -48,13 +40,23 @@ export async function gridSelect(options) {
|
||||
for (let i = 0; i < headerGap; i++) console.log("");
|
||||
|
||||
if (title) {
|
||||
const titlePad = " ".repeat(Math.max(0, Math.floor((termWidth - title.length - 4) / 2)));
|
||||
const titlePad = centerPad(title, termWidth - 4);
|
||||
console.log(titlePad + color.bgMagenta(color.white(` ${title} `)));
|
||||
}
|
||||
|
||||
console.log(pad + color.dim("↑ ↓ ← → 选择 | Enter 确认 | Esc 退出"));
|
||||
if (instructions) {
|
||||
console.log("\n" + pad + color.dim(instructions));
|
||||
} else {
|
||||
console.log("");
|
||||
}
|
||||
|
||||
for (let i = 0; i < menuGap; i++) console.log("");
|
||||
|
||||
if (!items || items.length === 0) {
|
||||
console.log(color.yellow("暂无可选项"));
|
||||
return;
|
||||
}
|
||||
|
||||
for (let row = 0; row < rows; row++) {
|
||||
let line = "";
|
||||
let descLine = "";
|
||||
@ -68,7 +70,7 @@ export async function gridSelect(options) {
|
||||
} else {
|
||||
line += " " + item.name + " ";
|
||||
}
|
||||
line += " ".repeat(colWidth - strWidth(item.name) - 2);
|
||||
line += " ".repeat(Math.max(0, colWidth - strWidth(item.name) - 2));
|
||||
descLine += padEnd(item.desc || "", colWidth);
|
||||
}
|
||||
}
|
||||
@ -91,50 +93,66 @@ export async function gridSelect(options) {
|
||||
|
||||
switch (key.name) {
|
||||
case "up":
|
||||
if (onUpdate) {
|
||||
// 已在更新按钮,不能再上
|
||||
} else if (row === 0 && updateInfo) {
|
||||
onUpdate = true; render();
|
||||
} else if (row > 0) {
|
||||
current -= cols; render();
|
||||
if (!onUpdate && row === 0 && hasUpdate) {
|
||||
onUpdate = true;
|
||||
render();
|
||||
} else if (!onUpdate && row > 0) {
|
||||
current -= cols;
|
||||
render();
|
||||
}
|
||||
break;
|
||||
case "down":
|
||||
if (onUpdate) {
|
||||
onUpdate = false; render();
|
||||
onUpdate = false;
|
||||
render();
|
||||
} else if (row < rows - 1 && current + cols < items.length) {
|
||||
current += cols; render();
|
||||
current += cols;
|
||||
render();
|
||||
}
|
||||
break;
|
||||
case "left":
|
||||
if (!onUpdate && col > 0) { current--; render(); }
|
||||
if (!onUpdate && col > 0) {
|
||||
current--;
|
||||
render();
|
||||
}
|
||||
break;
|
||||
case "right":
|
||||
if (!onUpdate && col < cols - 1 && current < items.length - 1) { current++; render(); }
|
||||
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]));
|
||||
}
|
||||
setImmediate(() => {
|
||||
const isUpdateSelection = onUpdate && hasUpdate;
|
||||
const payload = isUpdateSelection ? null : items[current];
|
||||
resolve(mapResult(payload, isUpdateSelection));
|
||||
});
|
||||
break;
|
||||
case "escape":
|
||||
resolved = true;
|
||||
stopKeypress();
|
||||
clearScreen();
|
||||
console.log(color.yellow("再见!"));
|
||||
process.exit(0);
|
||||
if (onCancel === "back") {
|
||||
setImmediate(() => resolve("back"));
|
||||
} else {
|
||||
console.log(color.yellow("再见!"));
|
||||
process.exit(0);
|
||||
}
|
||||
break;
|
||||
case "c":
|
||||
if (key.ctrl) {
|
||||
resolved = true;
|
||||
stopKeypress();
|
||||
clearScreen();
|
||||
console.log(color.yellow("再见!"));
|
||||
process.exit(0);
|
||||
if (onCancel === "back") {
|
||||
setImmediate(() => resolve("back"));
|
||||
} else {
|
||||
console.log(color.yellow("再见!"));
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user