// Derived from Node.js v24.16.0 (MIT; Copyright Node.js contributors, and Joyent, Inc. and // other Node contributors). See LICENSE-node. // node:readline/promises, after Node v24's lib/internal/readline/promises.js: Interface // is the callback Interface (./interface.ts) with question() returning a Promise, and // Readline queues cursor/clear writes on a stream, flushed by commit(). import { CSI, kClearLine, kClearScreenDown, kClearToLineBeginning, kClearToLineEnd } from "./callbacks"; import { AbortError, Interface as CallbackInterface, type InterfaceOptions } from "./interface"; function invalidArgType(name: string, expected: string, value: unknown): TypeError { const kind = name.includes(".") ? "property" : "argument"; const got = value === null || value === undefined ? ` Received ${value}` : typeof value === "object" ? ` Received an instance of ${(value as object).constructor?.name ?? "Object"}` : ` Received type ${typeof value} (${typeof value === "string" ? `'${value}'` : String(value)})`; return Object.assign(new TypeError(`The "${name}" ${kind} must be ${expected}.${got}`), { code: "ERR_INVALID_ARG_TYPE" }); } function outOfRange(name: string, range: string, value: unknown): RangeError { return Object.assign(new RangeError(`The value of "${name}" is out of range. It must be ${range}. Received ${value}`), { code: "ERR_OUT_OF_RANGE" }); } /** Node's validateInteger: a type check, then a range check (always an integer). */ function validateInteger(value: unknown, name: string): asserts value is number { if (typeof value !== "number") throw invalidArgType(name, "of type number", value); if (!Number.isInteger(value)) throw outOfRange(name, "an integer", value); } export class Interface extends CallbackInterface { question(query: string, options: { signal?: AbortSignal } = {}): Promise { return new Promise((resolve, reject) => { let cb: (answer: string) => void = resolve; if (options?.signal) { if (options.signal.aborted) { reject(new AbortError(undefined, { cause: options.signal.reason })); return; } const signal = options.signal; signal.addEventListener("abort", () => { reject(new AbortError(undefined, { cause: signal.reason })); }, { once: true }); cb = (answer: string): void => resolve(answer); } super.question(query, cb); }); } } interface Writable { write(data: string, cb?: (err?: Error | null) => void): boolean; } function isWritable(stream: unknown): stream is Writable { return typeof stream === "object" && stream !== null && typeof (stream as Writable).write === "function"; } /** readline/promises.Readline: queues cursor-control writes on a stream (or, with * autoCommit, sends each immediately) and flushes the queue with commit(). */ export class Readline { #autoCommit: boolean; #stream: Writable; #todo: string[] = []; constructor(stream: unknown, options?: { autoCommit?: boolean }) { if (!isWritable(stream)) throw invalidArgType("stream", "an instance of Writable", stream); this.#stream = stream; this.#autoCommit = false; if (options?.autoCommit != null) { if (typeof options.autoCommit !== "boolean") throw invalidArgType("options.autoCommit", "of type boolean", options.autoCommit); this.#autoCommit = options.autoCommit; } } #queue(data: string): this { if (this.#autoCommit) queueMicrotask(() => this.#stream.write(data)); else this.#todo.push(data); return this; } cursorTo(x: number, y?: number): this { validateInteger(x, "x"); if (y != null) validateInteger(y, "y"); return this.#queue(y == null ? CSI`${x + 1}G` : CSI`${y + 1};${x + 1}H`); } moveCursor(dx: number, dy: number): this { if (!dx && !dy) return this; validateInteger(dx, "dx"); validateInteger(dy, "dy"); let data = ""; if (dx < 0) data += CSI`${-dx}D`; else if (dx > 0) data += CSI`${dx}C`; if (dy < 0) data += CSI`${-dy}A`; else if (dy > 0) data += CSI`${dy}B`; return this.#queue(data); } clearLine(dir: number): this { validateInteger(dir, "dir"); if (dir < -1 || dir > 1) throw outOfRange("dir", ">= -1 && <= 1", dir); return this.#queue(dir < 0 ? kClearToLineBeginning : dir > 0 ? kClearToLineEnd : kClearLine); } clearScreenDown(): this { return this.#queue(kClearScreenDown); } commit(): Promise { return new Promise((resolve) => { this.#stream.write(this.#todo.join(""), () => resolve()); this.#todo = []; }); } rollback(): this { this.#todo = []; return this; } } export function createInterface( input: InterfaceOptions["input"] | InterfaceOptions, output?: InterfaceOptions["output"], completer?: InterfaceOptions["completer"], terminal?: boolean, ): Interface { return new Interface(input, output, completer, terminal); }