// Derived from Node.js v24.16.0 (MIT; Copyright Node.js contributors, and Joyent, Inc. and // other Node contributors). See LICENSE-node. // Port of Node v24's lib/internal/readline/emitKeypressEvents.js: makes a readable emit // 'keypress' for each key in its 'data', for as long as someone listens for keypresses. // A lone ESC waits escapeCodeTimeout (GNU readline's 500 ms keyseq-timeout) for the rest // of a sequence, on a guest timer, so it holds the loop as Node's does. import type { EventEmitter } from "../events"; import { StringDecoder } from "../string_decoder"; import type { Timers } from "../timers"; import { kEscape } from "./callbacks"; import { charLengthAt, emitKeys } from "./utils"; const KEYPRESS_DECODER = Symbol("keypress-decoder"); const ESCAPE_DECODER = Symbol("escape-decoder"); const ESCAPE_CODE_TIMEOUT = 500; interface KeypressStream extends EventEmitter { [KEYPRESS_DECODER]?: StringDecoder; [ESCAPE_DECODER]?: Generator; } /** What emitKeypressEvents reads from and writes to an Interface. */ export interface KeypressInterface { escapeCodeTimeout?: number; _sawKeyPress?: boolean; isCompletionEnabled?: boolean; } export function makeEmitKeypressEvents(timers: () => Timers) { return function emitKeypressEvents(stream: KeypressStream, iface: KeypressInterface = {}): void { if (stream[KEYPRESS_DECODER]) return; stream[KEYPRESS_DECODER] = new StringDecoder("utf8"); stream[ESCAPE_DECODER] = emitKeys(stream as never); stream[ESCAPE_DECODER].next(undefined as never); const triggerEscape = (): void => { stream[ESCAPE_DECODER]!.next(""); }; const { escapeCodeTimeout = ESCAPE_CODE_TIMEOUT } = iface; let timeoutId: unknown; function onData(input: Uint8Array | string): void { if (stream.listenerCount("keypress") > 0) { const string = stream[KEYPRESS_DECODER]!.write(input); if (string) { timers().clearTimeout(timeoutId); // This supports characters of length 2. iface._sawKeyPress = charLengthAt(string, 0) === string.length; iface.isCompletionEnabled = false; let length = 0; for (const character of string) { length += character.length; if (length === string.length) iface.isCompletionEnabled = true; try { stream[ESCAPE_DECODER]!.next(character); // An escape at the tail may begin a sequence still on its way. if (length === string.length && character === kEscape) { timeoutId = timers().setTimeout(triggerEscape, escapeCodeTimeout); } } catch (err) { // A throwing 'keypress' listener kills the generator: restart it. stream[ESCAPE_DECODER] = emitKeys(stream as never); stream[ESCAPE_DECODER].next(undefined as never); throw err; } } } } else { // Nobody is watching. stream.removeListener("data", onData); stream.on("newListener", onNewListener); } } function onNewListener(event: string | symbol): void { if (event === "keypress") { stream.on("data", onData); stream.removeListener("newListener", onNewListener); } } if (stream.listenerCount("keypress") > 0) stream.on("data", onData); else stream.on("newListener", onNewListener); }; }