Something went wrong. Try again.
Monorepo for the nibling.run platform
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164// 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 readline Interface: lib/internal/readline/interface.js with the// public wrapper from lib/readline.js (callback completers, question(), TERM=dumb). The// methods Node keeps behind symbols are the legacy `_name` methods here, which is how// lib/readline.js dispatches to them anyway, so patching rl._ttyWrite works as in Node.//// Not ported: the REPL's history file, and calling Interface without `new`.import type { EventEmitter as Emitter, EventEmitterConstructor } from "../events";import { EventEmitter } from "../events";import { capture, runInFrame, type Context } from "../async_hooks";import { inspect, stripVTControlCharacters } from "../inspect";import { StringDecoder } from "../string_decoder";import { invalidArgType } from "../errors";import type { GuestProcess } from "../process";import type { Timers } from "../timers";import { clearScreenDown, cursorTo, moveCursor } from "./callbacks";import type { KeypressInterface } from "./emitKeypressEvents";import { ReplHistory, type HistoryOptions } from "./history";import { charLengthAt, charLengthLeft, commonPrefix, getStringWidth, type Key } from "./utils";
export interface ReadlineDeps { process: () => GuestProcess; timers: () => Timers; emitKeypressEvents: (stream: never, iface: KeypressInterface) => void;}
let deps: ReadlineDeps;export function installReadline(d: ReadlineDeps): void { deps = d;}
const kMaxUndoRedoStackSize = 2048;const kMincrlfDelay = 100;const ESCAPE_CODE_TIMEOUT = 500;const kMaxLengthOfKillRing = 32;const kMultilinePrompt = "| ";
/** \r\n, \n, \r not followed by \n, and the Unicode line and paragraph separators. */const lineEnding = /\r?\n|\r(?!\n)|\u2028|\u2029/g;
type Input = Emitter & { resume(): unknown; pause(): unknown; isRaw?: boolean; setRawMode?: (mode: boolean) => unknown;} & HistoryOptions;
type Output = Emitter & { write(data: string, cb?: () => void): boolean; columns?: number; isTTY?: boolean;};
type Completer = (line: string, cb: (err: Error | null, result: [string[], string]) => void) => void;
export interface InterfaceOptions extends HistoryOptions { input: Input; output?: Output | null; completer?: (line: string, cb?: never) => unknown; terminal?: boolean; historySize?: number; prompt?: string; crlfDelay?: number; escapeCodeTimeout?: number; tabSize?: number; signal?: AbortSignal;}
function useAfterClose(): Error { return Object.assign(new Error("readline was closed"), { code: "ERR_USE_AFTER_CLOSE" });}
function invalidArgValue(name: string, value: unknown): TypeError { return Object.assign(new TypeError(`The argument '${name}' is invalid. Received ${inspect(value)}`), { code: "ERR_INVALID_ARG_VALUE" });}
export class AbortError extends Error { readonly code = "ABORT_ERR"; constructor(message = "The operation was aborted", options?: { cause?: unknown }) { super(message, options); this.name = "AbortError"; }}
const EMPTY_KEY: Partial<Key> = Object.freeze({});
export class Interface extends (EventEmitter as unknown as EventEmitterConstructor) { _sawReturnAt = 0; isCompletionEnabled = true; _sawKeyPress = false; _previousKey: Partial<Key> | null = null; escapeCodeTimeout = ESCAPE_CODE_TIMEOUT; tabSize = 8; line = ""; cursor?: number; terminal: boolean; input: Input; output: Output | null | undefined; crlfDelay: number; completer: Completer | undefined; historyManager!: ReplHistory; declare history: string[]; declare historyIndex: number; declare readonly historySize: number; declare readonly isFlushing: boolean; closed?: boolean; paused?: boolean; prevRows?: number; _prompt = ""; _oldPrompt = ""; _questionCallback: ((answer: string) => void) | null = null; _decoder?: StringDecoder; _line_buffer?: string | null;
#isMultiline = false; #substringSearch: string | null = null; #undoStack: Array<{ text: string; cursor: number }> = []; #redoStack: Array<{ text: string; cursor: number }> = []; #previousCursorCols = -1; #killRing: string[] = []; #killRingCursor = 0; #yanking = false; #previousLine = ""; #previousCursor = 0; #previousPrevRows: number | undefined; #lastCommandErrored = false; #lineIterator: AsyncIterableIterator<string> | undefined; /** createInterface()/new Interface() creates the handle (probed under Node: * 'line' and 'close' run in the frame current here, regardless of which frame is * current when the input stream's 'data'/'keypress'/'end' happens to fire). */ readonly #frame: Context = capture();
/** Every event this Interface emits runs in its creation frame (see #frame). */ override emit(event: string | symbol, ...args: unknown[]): boolean { return runInFrame(this.#frame, () => super.emit(event, ...args)); }
constructor(input: Input | InterfaceOptions, output?: Output | null, completer?: InterfaceOptions["completer"], terminal?: boolean) { super(); // lib/readline.js: a one-argument completer is wrapped to the callback form. const options = (input as InterfaceOptions)?.input ? input as InterfaceOptions : null; if (options && typeof options.completer === "function" && options.completer.length !== 2) { const sync = options.completer; options.completer = ((v: string, cb: (err: null, r: unknown) => void) => cb(null, sync(v))) as never; } else if (typeof completer === "function" && completer.length !== 2) { const sync = completer; completer = ((v: string, cb: (err: null, r: unknown) => void) => cb(null, sync(v))) as never; }
let crlfDelay: number | undefined; let prompt = "> "; let signal: AbortSignal | undefined; if (options) { output = options.output; completer = options.completer; terminal = options.terminal; signal = options.signal; if (options.tabSize !== undefined) { if (typeof options.tabSize !== "number") throw invalidArgType("tabSize", "of type number", options.tabSize); if (!Number.isInteger(options.tabSize) || options.tabSize < 1 || options.tabSize > 4294967295) { throw Object.assign(new RangeError(`The value of "tabSize" is out of range. It must be >= 1 && <= 4294967295. Received ${options.tabSize}`), { code: "ERR_OUT_OF_RANGE", }); } this.tabSize = options.tabSize; } if (options.prompt !== undefined) prompt = options.prompt; if (options.escapeCodeTimeout !== undefined) { if (Number.isFinite(options.escapeCodeTimeout)) this.escapeCodeTimeout = options.escapeCodeTimeout; else throw invalidArgValue("input.escapeCodeTimeout", this.escapeCodeTimeout); } if (signal !== undefined && !(signal instanceof AbortSignal)) throw invalidArgType("options.signal", "an instance of AbortSignal", signal); crlfDelay = options.crlfDelay; const { historySize, history, removeHistoryDuplicates } = options; input = options.input; // As Node does, the history options ride on the input stream to the history manager. Object.assign(input, { size: historySize, history, removeHistoryDuplicates }); } const stream = input as Input; this.setupHistoryManager(stream);
if (completer !== undefined && typeof completer !== "function") throw invalidArgValue("completer", completer); if (terminal === undefined && !(output === null || output === undefined)) terminal = Boolean(output.isTTY);
this.output = output; this.input = stream; this.crlfDelay = crlfDelay ? Math.max(kMincrlfDelay, crlfDelay) : kMincrlfDelay; this.completer = completer as Completer | undefined; this.setPrompt(prompt); this.terminal = Boolean(terminal);
const onerror = (err: unknown): void => { this.emit("error", err); }; const ondata = (data: Uint8Array | string): void => { this._normalWrite(data); }; const onend = (): void => { if (typeof this._line_buffer === "string" && this._line_buffer.length > 0) this.emit("line", this._line_buffer); this.close(); }; const ontermend = (): void => { if (typeof this.line === "string" && this.line.length > 0) this.emit("line", this.line); this.close(); }; const onkeypress = (s: string | undefined, key: Key): void => { this._ttyWrite(s, key); if (key?.sequence) { // A lone surrogate redraws once its pair arrives. const ch = key.sequence.codePointAt(0)!; if (ch >= 0xd800 && ch <= 0xdfff) this._refreshLine(); } }; const onresize = (): void => { this._refreshLine(); };
stream.on("error", onerror); if (!this.terminal) { stream.on("data", ondata); stream.on("end", onend); this.once("close", () => { stream.removeListener("data", ondata); stream.removeListener("error", onerror); stream.removeListener("end", onend); }); this._decoder = new StringDecoder("utf8"); } else { deps.emitKeypressEvents(stream as never, this); stream.on("keypress", onkeypress); stream.on("end", ontermend); this._setRawMode(true); this.terminal = true; this.cursor = 0; if (output !== null && output !== undefined) output.on("resize", onresize); this.once("close", () => { stream.removeListener("keypress", onkeypress); stream.removeListener("error", onerror); stream.removeListener("end", ontermend); if (output !== null && output !== undefined) output.removeListener("resize", onresize); }); }
if (signal) { const onAborted = (): void => this.close(); if (signal.aborted) { deps.process().nextTick(onAborted); } else { signal.addEventListener("abort", onAborted, { once: true }); this.once("close", () => signal.removeEventListener("abort", onAborted)); } }
this._setLine(""); stream.resume();
if (deps.process().env.TERM === "dumb") this._ttyWrite = ttyWriteDumb; }
get columns(): number { if (this.output?.columns) return this.output.columns; return Infinity; }
setPrompt(prompt: string): void { this._prompt = prompt; }
getPrompt(): string { return this._prompt; }
setupHistoryManager(options: HistoryOptions): void { this.historyManager = new ReplHistory(this, options); const self = this; Object.defineProperties(this, { history: { configurable: true, enumerable: true, get: () => self.historyManager.history, set: (h: string[]) => { self.historyManager.history = h; }, }, historyIndex: { configurable: true, enumerable: true, get: () => self.historyManager.index, set: (i: number) => { self.historyManager.index = i; }, }, historySize: { configurable: true, enumerable: true, get: () => self.historyManager.size }, isFlushing: { configurable: true, enumerable: true, get: () => self.historyManager.isFlushing }, }); }
_setRawMode(mode: boolean): boolean | undefined { const wasInRawMode = this.input.isRaw; if (typeof this.input.setRawMode === "function") this.input.setRawMode(mode); return wasInRawMode; }
prompt(preserveCursor?: boolean): void { if (this.paused) this.resume(); if (this.terminal && deps.process().env.TERM !== "dumb") { if (!preserveCursor) this.cursor = 0; this._refreshLine(); } else { this._writeToOutput(this._prompt); } }
question(query: string, options?: { signal?: AbortSignal } | ((answer: string) => void), cb?: (answer: string) => void): void { cb = typeof options === "function" ? options : cb; const opts = options === null || typeof options !== "object" ? {} : options; if (opts.signal) { if (!(opts.signal instanceof AbortSignal)) throw invalidArgType("options.signal", "an instance of AbortSignal", opts.signal); if (opts.signal.aborted) return; const signal = opts.signal; const onAbort = (): void => this._questionCancel(); signal.addEventListener("abort", onAbort, { once: true }); const dispose = (): void => signal.removeEventListener("abort", onAbort); const originalCb = cb; cb = typeof originalCb === "function" ? (answer: string) => { dispose(); return originalCb(answer); } : dispose; } if (typeof cb === "function") this._question(query, cb); }
_question(query: string, cb: (answer: string) => void): void { if (this.closed) throw useAfterClose(); if (this._questionCallback) { this.prompt(); } else { this._oldPrompt = this._prompt; this.setPrompt(query); this._questionCallback = cb; this.prompt(); } }
_setLine(line = ""): void { this.line = line; this.#isMultiline = line.includes("\n"); }
_onLine(line: string): void { if (this._questionCallback) { const cb = this._questionCallback; this._questionCallback = null; this.setPrompt(this._oldPrompt); cb(line); } else { this.emit("line", line); } }
_beforeEdit(oldText: string, oldCursor: number): void { this._pushToUndoStack(oldText, oldCursor); }
_questionCancel(): void { if (this._questionCallback) { this._questionCallback = null; this.setPrompt(this._oldPrompt); this.clearLine(); } }
_writeToOutput(stringToWrite: string): void { if (typeof stringToWrite !== "string") throw invalidArgType("stringToWrite", "of type string", stringToWrite); if (this.output !== null && this.output !== undefined) this.output.write(stringToWrite); }
_addHistory(): string { return this.historyManager.addHistory(this.#isMultiline, this.#lastCommandErrored); }
_refreshLine(): void { const line = this._prompt + this.line; const dispPos = this._getDisplayPos(line); const lineCols = dispPos.cols; const lineRows = dispPos.rows; const cursorPos = this.getCursorPos();
// First move to the bottom of the current line, based on cursor pos. const prevRows = this.prevRows || 0; if (prevRows > 0) moveCursor(this.output, 0, -prevRows); cursorTo(this.output, 0); clearScreenDown(this.output);
if (this.#isMultiline) { const lines = this.line.split("\n"); this._writeToOutput(this._prompt + lines[0]); for (let i = 1; i < lines.length; i++) this._writeToOutput(`\n${kMultilinePrompt}${lines[i]}`); } else { this._writeToOutput(line); } // Force the terminal to allocate a new line. if (lineCols === 0) this._writeToOutput(" ");
cursorTo(this.output, cursorPos.cols); const diff = lineRows - cursorPos.rows; if (diff > 0) moveCursor(this.output, 0, -diff); this.prevRows = cursorPos.rows; }
close(): void { if (this.closed) return; this.pause(); if (this.terminal) this._setRawMode(false); this.closed = true; this.emit("close"); }
pause(): this | undefined { if (this.closed) throw useAfterClose(); if (this.paused) return undefined; this.input.pause(); this.paused = true; this.emit("pause"); return this; }
resume(): this | undefined { if (this.closed) throw useAfterClose(); if (!this.paused) return undefined; this.input.resume(); this.paused = false; this.emit("resume"); return this; }
write(d: string | Uint8Array | null | undefined, key?: Partial<Key>): void { if (this.closed) throw useAfterClose(); if (this.paused) this.resume(); if (this.terminal) this._ttyWrite(d as string | undefined, key); else this._normalWrite(d ?? undefined); }
_normalWrite(b: string | Uint8Array | undefined): void { if (b === undefined) return; let string = this._decoder!.write(b); if (this._sawReturnAt && Date.now() - this._sawReturnAt <= this.crlfDelay) { if (string.codePointAt(0) === 10) string = string.slice(1); this._sawReturnAt = 0; } lineEnding.lastIndex = 0; let newPartContainsEnding = lineEnding.exec(string); if (newPartContainsEnding !== null) { if (this._line_buffer) { string = this._line_buffer + string; this._line_buffer = null; lineEnding.lastIndex = 0; newPartContainsEnding = lineEnding.exec(string)!; } this._sawReturnAt = string.endsWith("\r") ? Date.now() : 0; const indexes = [0, newPartContainsEnding.index, lineEnding.lastIndex]; let nextMatch: RegExpExecArray | null; while ((nextMatch = lineEnding.exec(string)) !== null) indexes.push(nextMatch.index, lineEnding.lastIndex); const lastIndex = indexes.length - 1; // Either '' or (conceivably) the unfinished portion of the next line. this._line_buffer = string.slice(indexes[lastIndex]); for (let i = 1; i < lastIndex; i += 2) this._onLine(string.slice(indexes[i - 1], indexes[i])); } else if (string) { this._line_buffer = this._line_buffer ? this._line_buffer + string : string; } }
_insertString(c: string): void { this._beforeEdit(this.line, this.cursor!); if (!this.isCompletionEnabled) { if (this.cursor! < this.line.length) { this.line = this.line.slice(0, this.cursor) + c + this.line.slice(this.cursor); } else { this.line += c; } this.cursor! += c.length; this._writeToOutput(c); return; } if (this.cursor! < this.line.length) { this._setLine(this.line.slice(0, this.cursor) + c + this.line.slice(this.cursor)); this.cursor! += c.length; this._refreshLine(); } else { const oldPos = this.getCursorPos(); this.line += c; this.cursor! += c.length; const newPos = this.getCursorPos(); if (oldPos.rows < newPos.rows) this._refreshLine(); else this._writeToOutput(c); } }
_tabComplete(lastKeypressWasTab: boolean): void { this.pause(); const string = this.line.slice(0, this.cursor); this.completer!(string, (err, value) => { this.resume(); if (err) { this._writeToOutput(`Tab completion error: ${inspect(err)}`); return; } this._tabCompleter(lastKeypressWasTab, value); }); }
_tabCompleter(lastKeypressWasTab: boolean, [completions, completeOn]: [string[], string]): void { if (!completions || completions.length === 0) return; // Complete as far as all the candidates agree. const prefix = commonPrefix(completions.filter((e) => e !== "")); if (prefix.startsWith(completeOn) && prefix.length > completeOn.length) { this._insertString(prefix.slice(completeOn.length)); return; } else if (!completeOn.startsWith(prefix)) { this._setLine(this.line.slice(0, this.cursor! - completeOn.length) + prefix + this.line.slice(this.cursor)); this.cursor = this.cursor! - completeOn.length + prefix.length; this._refreshLine(); return; } if (!lastKeypressWasTab) return; this._beforeEdit(this.line, this.cursor!); // A second tab lists the candidates in columns. const completionsWidth = completions.map((e) => getStringWidth(e)); const width = Math.max(...completionsWidth) + 2; let maxColumns = Math.floor(this.columns / width) || 1; if (maxColumns === Infinity) maxColumns = 1; let output = "\r\n"; let lineIndex = 0; let whitespace = 0; for (let i = 0; i < completions.length; i++) { const completion = completions[i]!; if (completion === "" || lineIndex === maxColumns) { output += "\r\n"; lineIndex = 0; whitespace = 0; } else { output += " ".repeat(whitespace); } if (completion !== "") { output += completion; whitespace = width - completionsWidth[i]!; lineIndex++; } else { output += "\r\n"; } } if (lineIndex !== 0) output += "\r\n\r\n"; this._writeToOutput(output); this._refreshLine(); }
_wordLeft(): void { if (this.cursor! > 0) { const reversed = Array.from(this.line.slice(0, this.cursor)).reverse().join(""); const match = /^\s*(?:[^\w\s]+|\w+)?/.exec(reversed)!; this._moveCursor(-match[0].length); } }
_wordRight(): void { if (this.cursor! < this.line.length) { const match = /^(?:\s+|[^\w\s]+|\w+)\s*/.exec(this.line.slice(this.cursor))!; this._moveCursor(match[0].length); } }
_deleteLeft(): void { if (this.cursor! > 0 && this.line.length > 0) { this._beforeEdit(this.line, this.cursor!); const charSize = charLengthLeft(this.line, this.cursor!); this.line = this.line.slice(0, this.cursor! - charSize) + this.line.slice(this.cursor); this.cursor! -= charSize; this._refreshLine(); } }
_deleteRight(): void { if (this.cursor! < this.line.length) { this._beforeEdit(this.line, this.cursor!); const charSize = charLengthAt(this.line, this.cursor!); this.line = this.line.slice(0, this.cursor) + this.line.slice(this.cursor! + charSize); this._refreshLine(); } }
_deleteWordLeft(): void { if (this.cursor! > 0) { this._beforeEdit(this.line, this.cursor!); let leading = this.line.slice(0, this.cursor); const reversed = Array.from(leading).reverse().join(""); const match = /^\s*(?:[^\w\s]+|\w+)?/.exec(reversed)!; leading = leading.slice(0, leading.length - match[0].length); this.line = leading + this.line.slice(this.cursor); this.cursor = leading.length; this._refreshLine(); } }
_deleteWordRight(): void { if (this.cursor! < this.line.length) { this._beforeEdit(this.line, this.cursor!); const trailing = this.line.slice(this.cursor); const match = /^(?:\s+|\W+|\w+)\s*/.exec(trailing)!; this.line = this.line.slice(0, this.cursor) + trailing.slice(match[0].length); this._refreshLine(); } }
_deleteLineLeft(): void { this._beforeEdit(this.line, this.cursor!); const del = this.line.slice(0, this.cursor); this._setLine(this.line.slice(this.cursor)); this.cursor = 0; this._pushToKillRing(del); this._refreshLine(); }
_deleteLineRight(): void { this._beforeEdit(this.line, this.cursor!); const del = this.line.slice(this.cursor); this._setLine(this.line.slice(0, this.cursor)); this._pushToKillRing(del); this._refreshLine(); }
_pushToKillRing(del: string): void { if (!del || del === this.#killRing[0]) return; this.#killRing.unshift(del); this.#killRingCursor = 0; while (this.#killRing.length > kMaxLengthOfKillRing) this.#killRing.pop(); }
_yank(): void { if (this.#killRing.length > 0) { this.#yanking = true; this._insertString(this.#killRing[this.#killRingCursor]!); } }
_yankPop(): void { if (!this.#yanking) return; if (this.#killRing.length > 1) { const lastYank = this.#killRing[this.#killRingCursor]!; this.#killRingCursor++; if (this.#killRingCursor >= this.#killRing.length) this.#killRingCursor = 0; const currentYank = this.#killRing[this.#killRingCursor]!; const head = this.line.slice(0, this.cursor! - lastYank.length); const tail = this.line.slice(this.cursor); this._setLine(head + currentYank + tail); this.cursor = head.length + currentYank.length; this._refreshLine(); } }
clearLine(): void { this._moveCursor(+Infinity); this._writeToOutput("\r\n"); this._setLine(""); this.cursor = 0; this.prevRows = 0; }
_line(): void { this.#previousLine = this.line; this.#previousCursor = this.cursor!; this.#previousPrevRows = this.prevRows; const line = this._addHistory(); this.#undoStack = []; this.#redoStack = []; this.clearLine(); this._onLine(line); }
_pushToUndoStack(text: string, cursor: number): void { if (this.#undoStack.push({ text, cursor }) > kMaxUndoRedoStackSize) this.#undoStack.shift(); }
_undo(): void { if (this.#undoStack.length <= 0) return; this.#redoStack.push({ text: this.line, cursor: this.cursor! }); const entry = this.#undoStack.pop()!; this._setLine(entry.text); this.cursor = entry.cursor; this._refreshLine(); }
_redo(): void { if (this.#redoStack.length <= 0) return; this.#undoStack.push({ text: this.line, cursor: this.cursor! }); const entry = this.#redoStack.pop()!; this._setLine(entry.text); this.cursor = entry.cursor; this._refreshLine(); }
/** Up or down a row inside a multiline entry, keeping the column where it fits. */ _multilineMove(direction: 1 | -1, splitLines: string[], { rows, cols }: { rows: number; cols: number }): void { const curr = splitLines[rows]!; const down = direction === 1; const adj = splitLines[rows + direction]!; const promptLen = kMultilinePrompt.length; let amountToMove: number; const clamp = down ? curr.length - cols + promptLen + adj.length + 1 : -cols + 1; const shouldClamp = cols > adj.length + 1; if (shouldClamp) { if (this.#previousCursorCols === -1) this.#previousCursorCols = cols; amountToMove = clamp; } else { amountToMove = down ? curr.length + 1 : -adj.length - 1; if (this.#previousCursorCols !== -1) { if (this.#previousCursorCols <= adj.length) { amountToMove += this.#previousCursorCols - cols; this.#previousCursorCols = -1; } else { amountToMove = clamp; } } } this._moveCursor(amountToMove); }
_moveDownOrHistoryNext(): void { const cursorPos = this.getCursorPos(); const splitLines = this.line.split("\n"); if (this.#isMultiline && cursorPos.rows < splitLines.length - 1) { this._multilineMove(1, splitLines, cursorPos); return; } this.#previousCursorCols = -1; this._historyNext(); }
_historyNext(): void { if (!this.historyManager.canNavigateToNext()) return; this._beforeEdit(this.line, this.cursor!); this._setLine(this.historyManager.navigateToNext(this.#substringSearch) ?? ""); this.cursor = this.line.length; this._refreshLine(); }
_moveUpOrHistoryPrev(): void { const cursorPos = this.getCursorPos(); if (this.#isMultiline && cursorPos.rows > 0) { this._multilineMove(-1, this.line.split("\n"), cursorPos); return; } this.#previousCursorCols = -1; this._historyPrev(); }
_historyPrev(): void { if (!this.historyManager.canNavigateToPrevious()) return; this._beforeEdit(this.line, this.cursor!); this._setLine(this.historyManager.navigateToPrevious(this.#substringSearch) ?? ""); this.cursor = this.line.length; this._refreshLine(); }
/** Where `str` would leave the cursor: rows down and the column, wrapping at `columns`. */ _getDisplayPos(str: string): { cols: number; rows: number } { let offset = 0; const col = this.columns; let rows = 0; str = stripVTControlCharacters(str); for (const char of str) { if (char === "\n") { rows += Math.ceil(offset / col) || 1; offset = this.#isMultiline ? kMultilinePrompt.length : 0; continue; } // Tabs are tabSize wide, counted from the line's start. if (char === "\t") { offset += this.tabSize - (offset % this.tabSize); continue; } const width = getStringWidth(char, false); if (width === 0 || width === 1) { offset += width; } else { // A wide character never splits across the right edge. if ((offset + 1) % col === 0) offset++; offset += 2; } } const cols = offset % col; rows += (offset - cols) / col; return { cols, rows }; }
getCursorPos(): { cols: number; rows: number } { return this._getDisplayPos(this._prompt + this.line.slice(0, this.cursor)); }
_getCursorPos(): { cols: number; rows: number } { return this.getCursorPos(); }
_moveCursor(dx: number): void { if (dx === 0) return; const oldPos = this.getCursorPos(); this.cursor! += dx; if (this.cursor! < 0) this.cursor = 0; else if (this.cursor! > this.line.length) this.cursor = this.line.length; const newPos = this.getCursorPos(); if (oldPos.rows === newPos.rows) moveCursor(this.output, newPos.cols - oldPos.cols, 0); else this._refreshLine(); }
/** One keypress in terminal mode: the editing commands GNU readline users expect. */ _ttyWrite(s: string | undefined, key?: Partial<Key>): void { const previousKey = this._previousKey; key ||= EMPTY_KEY; this._previousKey = key; let shouldResetPreviousCursorCols = true;
if (!key.meta || key.name !== "y") this.#yanking = false;
// Up and down search history by what is left of the cursor. if ((key.name === "up" || key.name === "down") && !key.ctrl && !key.meta && !key.shift) { if (this.#substringSearch === null && !this.#isMultiline) this.#substringSearch = this.line.slice(0, this.cursor); } else if (this.#substringSearch !== null) { this.#substringSearch = null; if (this.history.length === this.historyIndex) this.historyIndex = -1; }
// Undo (^_) and redo (^^), whatever the terminal calls them. if (typeof key.sequence === "string") { switch (key.sequence.codePointAt(0)) { case 0x1f: this._undo(); return; case 0x1e: this._redo(); return; } }
if (key.name === "escape") return;
if (key.ctrl && key.shift) { switch (key.name) { case "backspace": this._deleteLineLeft(); break; case "delete": this._deleteLineRight(); break; } } else if (key.ctrl) { switch (key.name) { case "c": if (this.listenerCount("SIGINT") > 0) this.emit("SIGINT"); else this.close(); break; case "h": this._deleteLeft(); break; case "d": // Delete right, or EOF on an empty line. if (this.cursor === 0 && this.line.length === 0) this.close(); else if (this.cursor! < this.line.length) this._deleteRight(); break; case "u": this._deleteLineLeft(); break; case "k": this._deleteLineRight(); break; case "a": this._moveCursor(-Infinity); break; case "e": this._moveCursor(+Infinity); break; case "b": this._moveCursor(-charLengthLeft(this.line, this.cursor!)); break; case "f": this._moveCursor(+charLengthAt(this.line, this.cursor!)); break; case "l": cursorTo(this.output, 0, 0); clearScreenDown(this.output); this._refreshLine(); break; case "n": this._historyNext(); break; case "p": this._historyPrev(); break; case "y": this._yank(); break; case "z": { // Suspend: without a SIGTSTP listener, hand the terminal back and stop. if (this.listenerCount("SIGTSTP") > 0) { this.emit("SIGTSTP"); } else { const proc = deps.process(); proc.once("SIGCONT", () => { // Come back from the background with the interface paused, as Node does. if (!this.paused) { this.pause(); this.emit("SIGCONT"); } this._setRawMode(true); this._refreshLine(); }); this._setRawMode(false); proc.kill(proc.pid, "SIGTSTP"); } break; } case "w": case "backspace": this._deleteWordLeft(); break; case "delete": this._deleteWordRight(); break; case "left": this._wordLeft(); break; case "right": this._wordRight(); break; } } else if (key.meta) { switch (key.name) { case "b": this._wordLeft(); break; case "f": this._wordRight(); break; case "d": case "delete": this._deleteWordRight(); break; case "backspace": this._deleteWordLeft(); break; case "y": this._yankPop(); break; } } else { if (this._sawReturnAt && key.name !== "enter") this._sawReturnAt = 0; switch (key.name) { case "return": this._sawReturnAt = Date.now(); this._line(); break; case "enter": // \r\n arrives as return then enter: one line. if (this._sawReturnAt === 0 || Date.now() - this._sawReturnAt > this.crlfDelay) this._line(); this._sawReturnAt = 0; break; case "backspace": this._deleteLeft(); break; case "delete": this._deleteRight(); break; case "left": this._moveCursor(-charLengthLeft(this.line, this.cursor!)); break; case "right": this._moveCursor(+charLengthAt(this.line, this.cursor!)); break; case "home": this._moveCursor(-Infinity); break; case "end": this._moveCursor(+Infinity); break; case "up": shouldResetPreviousCursorCols = false; this._moveUpOrHistoryPrev(); break; case "down": shouldResetPreviousCursorCols = false; this._moveDownOrHistoryNext(); break; case "tab": if (typeof this.completer === "function" && this.isCompletionEnabled) { this._tabComplete(Boolean(previousKey && previousKey.name === "tab")); break; } // Without a completer a tab is inserted like any other character. default: if (typeof s === "string" && s) { // Pasted text may hold whole lines. lineEnding.lastIndex = 0; let nextMatch: RegExpExecArray | null; let lastIndex = 0; while ((nextMatch = lineEnding.exec(s)) !== null) { this._insertString(s.slice(lastIndex, nextMatch.index)); ({ lastIndex } = lineEnding); this._line(); lineEnding.lastIndex = lastIndex; } this._insertString(s.slice(lastIndex)); } } } if (shouldResetPreviousCursorCols) this.#previousCursorCols = -1; }
/** Lines as they arrive, until 'close' (events.on(rl, 'line') in Node). */ [Symbol.asyncIterator](): AsyncIterableIterator<string> { this.#lineIterator ??= lineIterator(this); return this.#lineIterator; }}
const symbolDispose = (Symbol as { dispose?: symbol }).dispose;if (symbolDispose) { Object.defineProperty(Interface.prototype, symbolDispose, { value: function dispose(this: Interface) { this.close(); }, writable: true, configurable: true, });}
Object.defineProperty(Interface.prototype.question, Symbol.for("nodejs.util.promisify.custom"), { value: function question(this: Interface, query: string, options?: { signal?: AbortSignal }): Promise<string> { const opts = options === null || typeof options !== "object" ? {} : options; if (opts.signal?.aborted) return Promise.reject(new AbortError(undefined, { cause: opts.signal.reason })); return new Promise((resolve, reject) => { let cb = resolve; const signal = opts.signal; if (signal) { const onAbort = (): void => reject(new AbortError(undefined, { cause: signal.reason })); signal.addEventListener("abort", onAbort, { once: true }); cb = (answer) => { signal.removeEventListener("abort", onAbort); resolve(answer); }; } this.question(query, opts, cb); }); },});
/** lib/readline.js under TERM=dumb: no line editing, just keys in and lines out. */function ttyWriteDumb(this: Interface, s: string | undefined, key?: Partial<Key>): void { key ||= EMPTY_KEY; if (key.name === "escape") return; if (this._sawReturnAt && key.name !== "enter") this._sawReturnAt = 0; if (key.ctrl) { if (key.name === "c") { if (this.listenerCount("SIGINT") > 0) this.emit("SIGINT"); else this.close(); return; } else if (key.name === "d") { this.close(); return; } } switch (key.name) { case "return": this._sawReturnAt = Date.now(); this._line(); break; case "enter": if (this._sawReturnAt === 0 || Date.now() - this._sawReturnAt > this.crlfDelay) this._line(); this._sawReturnAt = 0; break; default: if (typeof s === "string" && s) { this.line += s; this.cursor! += s.length; this._writeToOutput(s); } }}
const LINE_HIGH_WATER_MARK = 1024;const LINE_LOW_WATER_MARK = 1;
/** events.on(rl, "line", { close: ["close"], highWaterMark: 1024 }): queued lines pause * the interface past the high-water mark and resume it once drained. */function lineIterator(rl: Interface): AsyncIterableIterator<string> { const queue: string[] = []; const waiting: Array<{ resolve: (r: IteratorResult<string>) => void; reject: (e: unknown) => void }> = []; let finished = false; let error: unknown = null; let paused = false; const done = (): IteratorResult<string> => ({ value: undefined, done: true });
const onLine = (line: string): void => { if (waiting.length) { waiting.shift()!.resolve({ value: line, done: false }); return; } queue.push(line); if (!paused && queue.length > LINE_HIGH_WATER_MARK) { paused = true; rl.pause(); } }; const onError = (err: unknown): void => { if (waiting.length) waiting.shift()!.reject(err); else error = err; void iterator.return!(); }; const onClose = (): void => { removeAll(); finished = true; while (waiting.length) waiting.shift()!.resolve(done()); }; const removeAll = (): void => { rl.removeListener("line", onLine); rl.removeListener("error", onError); rl.removeListener("close", onClose); }; rl.on("line", onLine); rl.on("error", onError); rl.on("close", onClose);
const iterator: AsyncIterableIterator<string> = { next() { const value = queue.shift(); if (value !== undefined) { if (paused && queue.length < LINE_LOW_WATER_MARK) { rl.resume(); paused = false; } return Promise.resolve({ value, done: false }); } if (error) { const p = Promise.reject(error); error = null; return p; } if (finished) return Promise.resolve(done()); return new Promise((resolve, reject) => waiting.push({ resolve, reject })); }, return() { removeAll(); finished = true; while (waiting.length) waiting.shift()!.resolve(done()); return Promise.resolve(done()); }, throw(err: unknown) { error = err; removeAll(); return Promise.reject(err); }, [Symbol.asyncIterator]() { return this; }, }; return iterator;}
export function createInterface( input: Input | InterfaceOptions, output?: Output | null, completer?: InterfaceOptions["completer"], terminal?: boolean,): Interface { return new Interface(input, output, completer, terminal);}