improves block functionality

adds cli compatible prompts/logs
adds logfile function for debug
adds multiselect support
new fieldRename
adds listFieldNames
This commit is contained in:
2025-04-30 01:17:45 -06:00
parent 2634f40f2b
commit 9535222fb7
14 changed files with 623 additions and 70 deletions

View File

@@ -1,9 +1,10 @@
import { colorize } from "./colorize.ts";
import { TerminalBlock, TerminalLayout } from "./TerminalLayout.ts";
import { colorize } from "./style.ts";
import { TerminalBlock } from "./TerminalLayout.ts";
interface ISelectMenuConfig {
multiSelect?: boolean;
terminalBlock?: TerminalBlock;
initialSelection?: number;
initialSelections?: number[];
}
export function selectMenu(items: string[]) {
@@ -21,18 +22,12 @@ export async function selectMenuInteractive(
Deno.stdin.setRaw(true);
let selected = 0;
if (config?.multiSelect) {
console.warn("Multi-select not implemented yet");
return null;
}
const terminalBlock = config?.terminalBlock || new TerminalBlock();
if (!config?.terminalBlock) {
terminalBlock.setRenderHeight(Deno.consoleSize().rows);
}
console.log(terminalBlock.getRenderHeight());
let range: [number, number] = [terminalBlock.lineCount, 1];
function renderMenu() {
const { rows } = Deno.consoleSize();
const terminalHeight = terminalBlock.getRenderHeight() || rows;
@@ -54,7 +49,7 @@ export async function selectMenuInteractive(
}
}
terminalBlock.setLines(lines);
range = terminalBlock.setLines(lines, range);
}
function numberAndPadding(i: number, prefix?: string) {
@@ -78,7 +73,7 @@ export async function selectMenuInteractive(
if (a === 3) {
Deno.stdin.setRaw(false);
console.log("\nInterrupted\n");
terminalBlock?.["layout"]?.clearAll();
Deno.exit(130);
}
@@ -105,13 +100,112 @@ export async function selectMenuInteractive(
}
}
terminalBlock.clear();
Deno.stdin.setRaw(false);
return options[selected];
}
terminalBlock.setLines(["Selected: " + options[selected]], range);
return await handleInput();
}
export async function multiSelectMenuInteractive(
q: string,
options: string[] | [string, callback][],
config?: ISelectMenuConfig,
): Promise<string[] | null> {
Deno.stdin.setRaw(true);
let selected = 0;
let selectedOptions: number[] = config?.initialSelections || [];
const rawValues = new Set(
options.map((i) => typeof i === "string" ? i : i[0]),
).values().toArray();
if (rawValues.length !== options.length) {
throw new Error("Duplicate options in multi-select menu");
}
const terminalBlock = config?.terminalBlock || new TerminalBlock();
if (!config?.terminalBlock) {
terminalBlock.setRenderHeight(Deno.consoleSize().rows);
}
let range: [number, number] = [terminalBlock.lineCount, 1];
function renderMenu() {
const { rows } = Deno.consoleSize();
const terminalHeight = terminalBlock.getRenderHeight() || rows;
const maxHeight = Math.min(terminalHeight - 1, options.length);
let startPoint = Math.max(0, selected - Math.floor(maxHeight / 2));
const endPoint = Math.min(options.length, startPoint + maxHeight);
if (endPoint - startPoint < maxHeight) {
startPoint = Math.max(0, options.length - maxHeight);
}
const lines: string[] = [];
lines.push(colorize(q, "green"));
for (let i = startPoint; i < endPoint; i++) {
const option = rawValues[i];
const checkbox = selectedOptions.includes(i)
? colorize("◼", "green")
: "◻";
if (i === selected) {
lines.push(`> ${checkbox} ${colorize(option, "porple")}`);
} else {
lines.push(` ${checkbox} ${option}`);
}
}
range = terminalBlock.setLines(lines, range);
}
// Function to handle input
async function handleInput() {
const buf = new Uint8Array(3); // arrow keys send 3 bytes
while (true) {
renderMenu();
const n = await Deno.stdin.read(buf);
if (n === null) break;
const [a, b, c] = buf;
if (a === 3) {
Deno.stdin.setRaw(false);
terminalBlock?.["layout"]?.clearAll();
Deno.exit(130);
}
if (a === 13) { // Enter key
break;
} else if (a === 27 && b === 91) { // Arrow keys
if (c === 65) { // Up
selected = (selected - 1 + options.length) % options.length;
} else if (c === 66) { // Down
selected = (selected + 1) % options.length;
}
} else if (a === 32) { // Space
Deno.stdout.writeSync(new TextEncoder().encode("\x07"));
if (selectedOptions.includes(selected)) {
selectedOptions = selectedOptions.filter((i) => i !== selected);
} else {
selectedOptions.push(selected);
}
}
}
Deno.stdin.setRaw(false);
return selectedOptions;
}
const selections = await handleInput();
for (const optionI of selections) {
const option = options[optionI];
if (Array.isArray(option)) {
await option[1](option[0]);
}
}
const final = selectedOptions.map((i) => rawValues[i]);
terminalBlock.setLines(["Selected: " + final.join(", ")], range);
return final;
}
if (import.meta.main) {
// const layout = new TerminalLayout();
// const block = new TerminalBlock();
@@ -154,7 +248,7 @@ if (import.meta.main) {
// layout.clearAll();
// console.log(val);
const val = await selectMenuInteractive("choose a fruit", [
const val = await multiSelectMenuInteractive("choose a fruit", [
"apple",
"banana",
"cherry",
@@ -183,4 +277,6 @@ if (import.meta.main) {
"zucchini",
]);
console.log(val);
// Deno.stdout.writeSync(new TextEncoder().encode("\x07"));
}