interface EventMap { keypress: CLIKeypressEvent; char: CLICharEvent; activate: Event; deactivate: Event; exit: Event; enter: Event; backspace: Event; escape: 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 = { new (): IntermediateEventTarget; }; export interface IntermediateEventTarget extends EventTarget { addEventListener( 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( 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; export class CLICharEvent extends CustomEvent { constructor(detail: EventDetailMap["char"]) { super("char", { detail, cancelable: true }); } } export class CLIKeypressEvent extends CustomEvent { 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(type: T): Promise { 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; } if (byte === 27 && i + 1 >= n) { this.dispatchEvent(new Event("escape")); 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); } dispatchKey(key: string) { switch (key) { case "enter": case "backspace": case "arrow-up": case "arrow-down": case "arrow-right": case "arrow-left": case "delete": case "escape": this.dispatchEvent(new Event(key)); break; default: this.dispatchEvent( new CLICharEvent({ key: key.charCodeAt(0), char: key }), ); } } }