change: input manager and prompt rewrite
This commit is contained in:
parent
569c67583d
commit
7a394c642a
196
cli/InputManager.ts
Normal file
196
cli/InputManager.ts
Normal file
@ -0,0 +1,196 @@
|
||||
interface EventMap {
|
||||
keypress: CLIKeypressEvent;
|
||||
char: CLICharEvent;
|
||||
activate: Event;
|
||||
deactivate: Event;
|
||||
exit: Event;
|
||||
enter: Event;
|
||||
backspace: Event;
|
||||
delete: Event;
|
||||
"arrow-left": Event;
|
||||
"arrow-right": Event;
|
||||
"arrow-up": Event;
|
||||
"arrow-down": Event;
|
||||
[key: string]: Event;
|
||||
}
|
||||
|
||||
interface EventDetailMap {
|
||||
keypress: {
|
||||
key: number;
|
||||
sequence?: Uint8Array;
|
||||
};
|
||||
char: EventDetailMap["keypress"] & {
|
||||
char: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type TypedEventTarget<EventMap extends object> = {
|
||||
new (): IntermediateEventTarget<EventMap>;
|
||||
};
|
||||
|
||||
export interface IntermediateEventTarget<EventMap> extends EventTarget {
|
||||
addEventListener<K extends keyof EventMap>(
|
||||
type: K,
|
||||
listener: (
|
||||
event: EventMap[K] extends Event ? EventMap[K] : Event,
|
||||
) => EventMap[K] extends Event ? void : never,
|
||||
options?: boolean | AddEventListenerOptions,
|
||||
): void;
|
||||
|
||||
addEventListener(
|
||||
type: string,
|
||||
listener: EventListenerOrEventListenerObject,
|
||||
options?: boolean | AddEventListenerOptions,
|
||||
): void;
|
||||
|
||||
removeEventListener<K extends keyof EventMap>(
|
||||
type: K,
|
||||
listener: (
|
||||
event: EventMap[K] extends Event ? EventMap[K] : Event,
|
||||
) => EventMap[K] extends Event ? void : never,
|
||||
options?: boolean | AddEventListenerOptions,
|
||||
): void;
|
||||
|
||||
removeEventListener(
|
||||
type: string,
|
||||
listener: EventListenerOrEventListenerObject,
|
||||
options?: boolean | AddEventListenerOptions,
|
||||
): void;
|
||||
}
|
||||
|
||||
const ManagerEventTarget = EventTarget as TypedEventTarget<EventMap>;
|
||||
|
||||
export class CLICharEvent extends CustomEvent<EventDetailMap["char"]> {
|
||||
constructor(detail: EventDetailMap["char"]) {
|
||||
super("char", { detail, cancelable: true });
|
||||
}
|
||||
}
|
||||
export class CLIKeypressEvent extends CustomEvent<EventDetailMap["keypress"]> {
|
||||
constructor(detail: EventDetailMap["keypress"]) {
|
||||
super("keypress", { detail, cancelable: true });
|
||||
}
|
||||
}
|
||||
|
||||
export class InputManager extends ManagerEventTarget {
|
||||
private static instance = new InputManager();
|
||||
private active = false;
|
||||
|
||||
static getInstance(): InputManager {
|
||||
return this.instance ??= new InputManager();
|
||||
}
|
||||
|
||||
static addEventListener = InputManager.prototype.addEventListener.bind(
|
||||
this.instance,
|
||||
);
|
||||
static removeEventListener = InputManager.prototype.removeEventListener.bind(
|
||||
this.instance,
|
||||
);
|
||||
static dispatchEvent = InputManager.prototype.dispatchEvent.bind(
|
||||
this.instance,
|
||||
);
|
||||
|
||||
activate({ raw = true }: { raw?: boolean } = {}) {
|
||||
if (this.active) return;
|
||||
this.active = true;
|
||||
this.dispatchEvent(new Event("activate"));
|
||||
this.listen(raw);
|
||||
}
|
||||
|
||||
deactivate({ dactivateRaw = true }: { dactivateRaw?: boolean } = {}) {
|
||||
if (!this.active) return;
|
||||
this.active = false;
|
||||
this.dispatchEvent(new Event("deactivate"));
|
||||
if (dactivateRaw) Deno.stdin.setRaw(false);
|
||||
}
|
||||
|
||||
once<T extends string>(type: T): Promise<Event> {
|
||||
return new Promise((resolve) => {
|
||||
const handler = (event: Event) => {
|
||||
this.removeEventListener(type, handler);
|
||||
resolve(event);
|
||||
};
|
||||
this.addEventListener(type, handler);
|
||||
});
|
||||
}
|
||||
|
||||
private async listen(raw: boolean) {
|
||||
if (raw) await Deno.stdin.setRaw(true);
|
||||
const buf = new Uint8Array(64);
|
||||
|
||||
while (this.active) {
|
||||
const n = await Deno.stdin.read(buf);
|
||||
if (n === null) break;
|
||||
|
||||
let i = 0;
|
||||
while (i < n) {
|
||||
const byte = buf[i];
|
||||
|
||||
// Ctrl+C
|
||||
if (byte === 3) {
|
||||
this.dispatchEvent(new Event("exit"));
|
||||
await Deno.stdin.setRaw(false);
|
||||
Deno.exit(130);
|
||||
}
|
||||
|
||||
// Enter
|
||||
if (byte === 13) {
|
||||
this.dispatchEvent(new Event("enter"));
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Backspace
|
||||
if (byte === 127 || byte === 8) {
|
||||
this.dispatchEvent(new Event("backspace"));
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Escape sequences
|
||||
if (byte === 27 && i + 1 < n && buf[i + 1] === 91) {
|
||||
const code = buf[i + 2];
|
||||
switch (code) {
|
||||
case 65:
|
||||
this.dispatchEvent(new Event("arrow-up"));
|
||||
break;
|
||||
case 66:
|
||||
this.dispatchEvent(new Event("arrow-down"));
|
||||
break;
|
||||
case 67:
|
||||
this.dispatchEvent(new Event("arrow-right"));
|
||||
break;
|
||||
case 68:
|
||||
this.dispatchEvent(new Event("arrow-left"));
|
||||
break;
|
||||
case 51:
|
||||
if (i + 3 < n && buf[i + 3] === 126) {
|
||||
this.dispatchEvent(new Event("delete"));
|
||||
i += 4;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
i += 3;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Printable ASCII
|
||||
if (byte >= 32 && byte <= 126) {
|
||||
this.dispatchEvent(
|
||||
new CLICharEvent({ key: byte, char: String.fromCharCode(byte) }),
|
||||
);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Unknown
|
||||
this.dispatchEvent(
|
||||
new CLIKeypressEvent({ key: byte, sequence: buf.slice(i, i + 1) }),
|
||||
);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (raw) await Deno.stdin.setRaw(false);
|
||||
}
|
||||
}
|
316
cli/prompts.ts
316
cli/prompts.ts
@ -1,8 +1,117 @@
|
||||
// deno-lint-disable-must-await-calls
|
||||
import { log } from "util/logfile.ts";
|
||||
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;
|
||||
|
||||
// await Deno.stdin.setRaw(true);
|
||||
|
||||
// const cursorVisible = Cursor["visible"];
|
||||
// Cursor.show();
|
||||
|
||||
// let range: [number, number] = [0, 1];
|
||||
// if (block) {
|
||||
// range = block.setLines([message + " "]);
|
||||
// } else {
|
||||
// Deno.stdout.writeSync(encoder.encode(message + " "));
|
||||
// }
|
||||
|
||||
// const render = () => {
|
||||
// const line = message + " " + input.join("");
|
||||
// const moveTo = `\x1b[${message.length + 2 + cursorPos}G`;
|
||||
|
||||
// if (block) {
|
||||
// block.setPostRenderAction(function () {
|
||||
// Deno.stdout.writeSync(
|
||||
// encoder.encode(`\x1b[${this["lastRenderRow"]};1H`),
|
||||
// );
|
||||
// Deno.stdout.writeSync(encoder.encode(moveTo));
|
||||
// });
|
||||
// range = block.setLines([line], range);
|
||||
// } else {
|
||||
// Deno.stdout.writeSync(encoder.encode("\x1b[K" + line + moveTo));
|
||||
// }
|
||||
// };
|
||||
|
||||
// render();
|
||||
|
||||
// const buf = new Uint8Array(64); // large enough for most pastes
|
||||
// inputLoop:
|
||||
// while (true) {
|
||||
// const n = await Deno.stdin.read(buf);
|
||||
// if (n === null) break;
|
||||
|
||||
// for (let i = 0; i < n; i++) {
|
||||
// const byte = buf[i];
|
||||
|
||||
// // Ctrl+C
|
||||
// if (byte === 3) {
|
||||
// block?.clear();
|
||||
// block?.["layout"]?.clearAll();
|
||||
// await Deno.stdin.setRaw(false);
|
||||
// Deno.exit(130);
|
||||
// }
|
||||
|
||||
// if (byte === 13) { // Enter
|
||||
// break inputLoop;
|
||||
// }
|
||||
|
||||
// // Escape sequence?
|
||||
// if (byte === 27 && i + 1 < n && buf[i + 1] === 91) {
|
||||
// const third = buf[i + 2];
|
||||
// if (third === 68 && cursorPos > 0) cursorPos--; // Left
|
||||
// else if (third === 67 && cursorPos < input.length) cursorPos++; // Right
|
||||
// else if (third === 51 && i + 3 < n && buf[i + 3] === 126) { // Delete
|
||||
// if (cursorPos < input.length) input.splice(cursorPos, 1);
|
||||
// i += 1; // consume tilde
|
||||
// }
|
||||
// i += 2; // consume ESC [ X
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// // Backspace
|
||||
// if (byte === 127 || byte === 8) {
|
||||
// if (cursorPos > 0) {
|
||||
// input.splice(cursorPos - 1, 1);
|
||||
// cursorPos--;
|
||||
// }
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// // Delete (ASCII 46)
|
||||
// if (byte === 46 && cursorPos < input.length) {
|
||||
// input.splice(cursorPos, 1);
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// // Printable
|
||||
// if (byte >= 32 && byte <= 126) {
|
||||
// input.splice(cursorPos, 0, String.fromCharCode(byte));
|
||||
// cursorPos++;
|
||||
// }
|
||||
|
||||
// // Other cases: ignore
|
||||
// }
|
||||
|
||||
// render();
|
||||
// }
|
||||
|
||||
// await Deno.stdin.setRaw(false);
|
||||
// if (!cursorVisible) {
|
||||
// Cursor.hide();
|
||||
// }
|
||||
// Deno.stdout.writeSync(encoder.encode("\n"));
|
||||
|
||||
// return input.join("");
|
||||
// }
|
||||
|
||||
export async function cliPrompt(
|
||||
message: string,
|
||||
@ -11,121 +120,140 @@ export async function cliPrompt(
|
||||
const encoder = new TextEncoder();
|
||||
const input: string[] = [];
|
||||
let cursorPos = 0;
|
||||
let range: [number, number] | undefined;
|
||||
|
||||
await Deno.stdin.setRaw(true);
|
||||
|
||||
const cursorVisible = Cursor["visible"];
|
||||
Cursor.show();
|
||||
|
||||
let range: [number, number] = [0, 1];
|
||||
if (block) {
|
||||
range = block.setLines([message + " "]);
|
||||
} else {
|
||||
Deno.stdout.writeSync(encoder.encode(message + " "));
|
||||
}
|
||||
const im = InputManager.getInstance();
|
||||
im.activate();
|
||||
|
||||
const render = () => {
|
||||
const line = message + " " + input.join("");
|
||||
const moveTo = `\x1b[${message.length + 2 + cursorPos}G`;
|
||||
|
||||
if (block) {
|
||||
block.setPostRenderAction(function () {
|
||||
Deno.stdout.writeSync(
|
||||
encoder.encode(`\x1b[${this["lastRenderRow"]};1H`),
|
||||
);
|
||||
block.setPostRenderAction(() => {
|
||||
Deno.stdout.writeSync(encoder.encode(moveTo));
|
||||
});
|
||||
range = block.setLines([line], range);
|
||||
} else {
|
||||
Deno.stdout.writeSync(encoder.encode("\x1b[K" + line + moveTo));
|
||||
Deno.stdout.writeSync(encoder.encode("\r\x1b[K" + line + moveTo));
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
input.splice(cursorPos, 0, ke.char);
|
||||
cursorPos++;
|
||||
render();
|
||||
};
|
||||
|
||||
render();
|
||||
|
||||
const buf = new Uint8Array(64); // large enough for most pastes
|
||||
inputLoop:
|
||||
while (true) {
|
||||
const n = await Deno.stdin.read(buf);
|
||||
if (n === null) break;
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const byte = buf[i];
|
||||
|
||||
// Ctrl+C
|
||||
if (byte === 3) {
|
||||
block?.clear();
|
||||
block?.["layout"]?.clearAll();
|
||||
await Deno.stdin.setRaw(false);
|
||||
Deno.exit(130);
|
||||
}
|
||||
|
||||
if (byte === 13) { // Enter
|
||||
break inputLoop;
|
||||
}
|
||||
|
||||
// Escape sequence?
|
||||
if (byte === 27 && i + 1 < n && buf[i + 1] === 91) {
|
||||
const third = buf[i + 2];
|
||||
if (third === 68 && cursorPos > 0) cursorPos--; // Left
|
||||
else if (third === 67 && cursorPos < input.length) cursorPos++; // Right
|
||||
else if (third === 51 && i + 3 < n && buf[i + 3] === 126) { // Delete
|
||||
if (cursorPos < input.length) input.splice(cursorPos, 1);
|
||||
i += 1; // consume tilde
|
||||
}
|
||||
i += 2; // consume ESC [ X
|
||||
continue;
|
||||
}
|
||||
|
||||
// Backspace
|
||||
if (byte === 127 || byte === 8) {
|
||||
if (cursorPos > 0) {
|
||||
input.splice(cursorPos - 1, 1);
|
||||
cursorPos--;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Delete (ASCII 46)
|
||||
if (byte === 46 && cursorPos < input.length) {
|
||||
input.splice(cursorPos, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Printable
|
||||
if (byte >= 32 && byte <= 126) {
|
||||
input.splice(cursorPos, 0, String.fromCharCode(byte));
|
||||
cursorPos++;
|
||||
}
|
||||
|
||||
// Other cases: ignore
|
||||
}
|
||||
|
||||
render();
|
||||
}
|
||||
|
||||
await Deno.stdin.setRaw(false);
|
||||
if (!cursorVisible) {
|
||||
Cursor.hide();
|
||||
}
|
||||
Deno.stdout.writeSync(encoder.encode("\n"));
|
||||
|
||||
return input.join("");
|
||||
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 function cliConfirm(message: string, block?: TerminalBlock) {
|
||||
return cliPrompt(message + " (y/n)", block).then((v) =>
|
||||
v.toLowerCase() === "y"
|
||||
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 function cliAlert(message: string, block?: TerminalBlock) {
|
||||
return cliPrompt(
|
||||
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,
|
||||
).then((v) => {
|
||||
return v;
|
||||
});
|
||||
);
|
||||
im.removeEventListener("char", onKey);
|
||||
}
|
||||
|
||||
export function cliLog(
|
||||
@ -158,6 +286,12 @@ if (import.meta.main) {
|
||||
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();
|
||||
|
Loading…
x
Reference in New Issue
Block a user