change: input manager and prompt rewrite
This commit is contained in:
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);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user