197 lines
5.0 KiB
TypeScript
197 lines
5.0 KiB
TypeScript
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);
|
|
}
|
|
}
|