pdf-tools/cli/prompts.ts
Emmaline 7eb7197a1c fix: unifies cursor positioning through input manager
removed: block level preserve history removed until accurate reporting of render heights is available
fix: fixes block multiline rendering
2025-06-09 12:54:52 -06:00

217 lines
5.4 KiB
TypeScript

// deno-lint-disable-must-await-calls
import { Cursor } from "./cursor.ts";
import { colorize } from "./style.ts";
import { TerminalBlock, TerminalLayout } from "./TerminalLayout.ts";
import { type CLICharEvent, InputManager } from "./InputManager.ts";
export async function cliPrompt(
message: string,
block?: TerminalBlock,
): Promise<string> {
const encoder = new TextEncoder();
const input: string[] = [];
let cursorPos = 0;
Cursor.show();
const im = InputManager.getInstance();
im.activate();
const render = () => {
const line = message + " " + input.join("");
const moveTo = `\x1b[${message.length + 2 + cursorPos}G`;
if (block) {
block.setLines([line]);
} else {
Deno.stdout.writeSync(encoder.encode("\r\x1b[K" + line + moveTo));
}
};
render();
const cPos = block?.requestCursorAt(0, message.length + 1);
if (cPos) {
const [row, column] = cPos;
im.setCursor(row, column);
im.setBounds({ top: row, left: column, right: column, bottom: row });
}
const exit = () => {
im.removeEventListener("enter", onEnter);
im.removeEventListener("backspace", onBackspace);
im.removeEventListener("delete", onDelete);
im.removeEventListener("arrow-left", onLeft);
im.removeEventListener("arrow-right", onRight);
im.removeEventListener("char", onKey);
Cursor.hide();
};
let resolve: null | ((value: string) => void) = null;
const onEnter = () => {
exit();
resolve?.(input.join(""));
};
const onBackspace = () => {
if (cursorPos > 0) {
input.splice(cursorPos - 1, 1);
cursorPos--;
render();
}
};
const onDelete = () => {
if (cursorPos < input.length) {
input.splice(cursorPos, 1);
render();
}
};
const onLeft = () => {
if (cursorPos > 0) {
cursorPos--;
render();
}
};
const onRight = () => {
if (cursorPos < input.length) {
cursorPos++;
render();
}
};
const onKey = (e: Event) => {
const ke = (e as CLICharEvent).detail;
cursorPos = im.getRelativeCursor();
input.splice(cursorPos, 0, ke.char);
im.updateBounds({ right: input.length + message.length + 1 });
render();
};
return await new Promise<string>((res) => {
resolve = res;
im.addEventListener("enter", onEnter);
im.addEventListener("backspace", onBackspace);
im.addEventListener("delete", onDelete);
im.addEventListener("arrow-left", onLeft);
im.addEventListener("arrow-right", onRight);
im.addEventListener("char", onKey);
});
}
export async function cliConfirm(message: string, block?: TerminalBlock) {
const im = InputManager.getInstance();
let inpout = "";
function isValidInput(input: string) {
switch (input) {
case "y":
case "n":
return inpout.length === 0;
case "e":
return inpout === "y";
case "s":
return inpout === "ye";
case "o":
return inpout === "n";
default:
return false;
}
}
function onKey(e: CLICharEvent) {
const ke = e.detail;
const char = String.fromCharCode(ke.key);
if (isValidInput(char)) {
inpout += char;
} else {
e.stopImmediatePropagation();
}
}
im.addEventListener("char", onKey);
const value = await cliPrompt(message + " (y/n)", block).then((v) =>
v.charAt(0).toLowerCase() === "y"
);
im.removeEventListener("char", onKey);
return value;
}
export async function cliAlert(message: string, block?: TerminalBlock) {
const im = InputManager.getInstance();
const onKey = (e: CLICharEvent) => {
e.stopImmediatePropagation();
};
im.addEventListener("char", onKey);
await cliPrompt(
message + colorize(" Press Enter to continue", "gray"),
block,
);
im.removeEventListener("char", onKey);
}
export function cliLog(
message: string | object | Array<unknown>,
block?: TerminalBlock,
) {
if (!block) {
console.log(message);
} else {
if (typeof message === "object") message = Deno.inspect(message);
block.setLines(message.split("\n"));
}
}
if (import.meta.main) {
Cursor.hide();
const layout = new TerminalLayout();
const title = new TerminalBlock();
const block = new TerminalBlock();
const footer = new TerminalBlock();
block.setPreserveHistory(true);
// ScrollManager.enable(block);
title.setLines(["Hello, World!"]);
title.setFixedHeight(1);
footer.setLines(["Press Ctrl+C to exit"]);
footer.setFixedHeight(1);
layout.register("title", title);
layout.register("block", block);
layout.register("footer", footer);
InputManager.addEventListener("exit", () => {
layout.clearAll();
// console.clear();
Deno.exit(0);
});
Deno.addSignalListener("SIGINT", () => {
layout.clearAll();
// console.clear();
// Deno.exit(0);
});
const name = await cliPrompt("Enter your name:", block);
cliLog(`Hello, ${name}!`, block);
const single = await cliConfirm("Are you single?", block);
cliLog(single ? "Do you want to go out with me?" : "Okay", block);
// ScrollManager.enable(block);
const loopingConvo = [
"No response?",
"I guess that's okay",
"Maybe I'll see you next week?",
"Wow, really not going to say anything to me?",
"Well, if that's how you feel",
];
let convo = 0;
setInterval(() => {
cliLog(loopingConvo[convo % loopingConvo.length], block);
convo++;
}, 2000);
// setTimeout(async () => {
// await cliAlert("Well, if that's that...", block);
// Deno.exit(0);
// }, 10000);
}