summaryrefslogtreecommitdiff
path: root/ext/node/polyfills/internal/readline
diff options
context:
space:
mode:
Diffstat (limited to 'ext/node/polyfills/internal/readline')
-rw-r--r--ext/node/polyfills/internal/readline/callbacks.mjs137
-rw-r--r--ext/node/polyfills/internal/readline/emitKeypressEvents.mjs106
-rw-r--r--ext/node/polyfills/internal/readline/interface.mjs1223
-rw-r--r--ext/node/polyfills/internal/readline/promises.mjs139
-rw-r--r--ext/node/polyfills/internal/readline/symbols.mjs34
-rw-r--r--ext/node/polyfills/internal/readline/utils.mjs580
6 files changed, 2219 insertions, 0 deletions
diff --git a/ext/node/polyfills/internal/readline/callbacks.mjs b/ext/node/polyfills/internal/readline/callbacks.mjs
new file mode 100644
index 000000000..3be88c899
--- /dev/null
+++ b/ext/node/polyfills/internal/readline/callbacks.mjs
@@ -0,0 +1,137 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+"use strict";
+
+import { ERR_INVALID_ARG_VALUE, ERR_INVALID_CURSOR_POS } from "internal:deno_node/polyfills/internal/errors.ts";
+
+import { validateFunction } from "internal:deno_node/polyfills/internal/validators.mjs";
+
+import { CSI } from "internal:deno_node/polyfills/internal/readline/utils.mjs";
+
+const {
+ kClearLine,
+ kClearScreenDown,
+ kClearToLineBeginning,
+ kClearToLineEnd,
+} = CSI;
+
+/**
+ * moves the cursor to the x and y coordinate on the given stream
+ */
+
+export function cursorTo(stream, x, y, callback) {
+ if (callback !== undefined) {
+ validateFunction(callback, "callback");
+ }
+
+ if (typeof y === "function") {
+ callback = y;
+ y = undefined;
+ }
+
+ if (Number.isNaN(x)) throw new ERR_INVALID_ARG_VALUE("x", x);
+ if (Number.isNaN(y)) throw new ERR_INVALID_ARG_VALUE("y", y);
+
+ if (stream == null || (typeof x !== "number" && typeof y !== "number")) {
+ if (typeof callback === "function") process.nextTick(callback, null);
+ return true;
+ }
+
+ if (typeof x !== "number") throw new ERR_INVALID_CURSOR_POS();
+
+ const data = typeof y !== "number" ? CSI`${x + 1}G` : CSI`${y + 1};${x + 1}H`;
+ return stream.write(data, callback);
+}
+
+/**
+ * moves the cursor relative to its current location
+ */
+
+export function moveCursor(stream, dx, dy, callback) {
+ if (callback !== undefined) {
+ validateFunction(callback, "callback");
+ }
+
+ if (stream == null || !(dx || dy)) {
+ if (typeof callback === "function") process.nextTick(callback, null);
+ return true;
+ }
+
+ 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 stream.write(data, callback);
+}
+
+/**
+ * clears the current line the cursor is on:
+ * -1 for left of the cursor
+ * +1 for right of the cursor
+ * 0 for the entire line
+ */
+
+export function clearLine(stream, dir, callback) {
+ if (callback !== undefined) {
+ validateFunction(callback, "callback");
+ }
+
+ if (stream === null || stream === undefined) {
+ if (typeof callback === "function") process.nextTick(callback, null);
+ return true;
+ }
+
+ const type = dir < 0
+ ? kClearToLineBeginning
+ : dir > 0
+ ? kClearToLineEnd
+ : kClearLine;
+ return stream.write(type, callback);
+}
+
+/**
+ * clears the screen from the current position of the cursor down
+ */
+
+export function clearScreenDown(stream, callback) {
+ if (callback !== undefined) {
+ validateFunction(callback, "callback");
+ }
+
+ if (stream === null || stream === undefined) {
+ if (typeof callback === "function") process.nextTick(callback, null);
+ return true;
+ }
+
+ return stream.write(kClearScreenDown, callback);
+}
diff --git a/ext/node/polyfills/internal/readline/emitKeypressEvents.mjs b/ext/node/polyfills/internal/readline/emitKeypressEvents.mjs
new file mode 100644
index 000000000..7f68dac47
--- /dev/null
+++ b/ext/node/polyfills/internal/readline/emitKeypressEvents.mjs
@@ -0,0 +1,106 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import { charLengthAt, CSI, emitKeys } from "internal:deno_node/polyfills/internal/readline/utils.mjs";
+import { kSawKeyPress } from "internal:deno_node/polyfills/internal/readline/symbols.mjs";
+import { clearTimeout, setTimeout } from "internal:deno_node/polyfills/timers.ts";
+
+const {
+ kEscape,
+} = CSI;
+
+import { StringDecoder } from "internal:deno_node/polyfills/string_decoder.ts";
+
+const KEYPRESS_DECODER = Symbol("keypress-decoder");
+const ESCAPE_DECODER = Symbol("escape-decoder");
+
+// GNU readline library - keyseq-timeout is 500ms (default)
+const ESCAPE_CODE_TIMEOUT = 500;
+
+/**
+ * accepts a readable Stream instance and makes it emit "keypress" events
+ */
+
+export function emitKeypressEvents(stream, iface = {}) {
+ if (stream[KEYPRESS_DECODER]) return;
+
+ stream[KEYPRESS_DECODER] = new StringDecoder("utf8");
+
+ stream[ESCAPE_DECODER] = emitKeys(stream);
+ stream[ESCAPE_DECODER].next();
+
+ const triggerEscape = () => stream[ESCAPE_DECODER].next("");
+ const { escapeCodeTimeout = ESCAPE_CODE_TIMEOUT } = iface;
+ let timeoutId;
+
+ function onData(input) {
+ if (stream.listenerCount("keypress") > 0) {
+ const string = stream[KEYPRESS_DECODER].write(input);
+ if (string) {
+ clearTimeout(timeoutId);
+
+ // This supports characters of length 2.
+ iface[kSawKeyPress] = charLengthAt(string, 0) === string.length;
+ iface.isCompletionEnabled = false;
+
+ let length = 0;
+ for (const character of string[Symbol.iterator]()) {
+ length += character.length;
+ if (length === string.length) {
+ iface.isCompletionEnabled = true;
+ }
+
+ try {
+ stream[ESCAPE_DECODER].next(character);
+ // Escape letter at the tail position
+ if (length === string.length && character === kEscape) {
+ timeoutId = setTimeout(triggerEscape, escapeCodeTimeout);
+ }
+ } catch (err) {
+ // If the generator throws (it could happen in the `keypress`
+ // event), we need to restart it.
+ stream[ESCAPE_DECODER] = emitKeys(stream);
+ stream[ESCAPE_DECODER].next();
+ throw err;
+ }
+ }
+ }
+ } else {
+ // Nobody's watching anyway
+ stream.removeListener("data", onData);
+ stream.on("newListener", onNewListener);
+ }
+ }
+
+ function onNewListener(event) {
+ 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);
+ }
+}
diff --git a/ext/node/polyfills/internal/readline/interface.mjs b/ext/node/polyfills/internal/readline/interface.mjs
new file mode 100644
index 000000000..41d05fbf2
--- /dev/null
+++ b/ext/node/polyfills/internal/readline/interface.mjs
@@ -0,0 +1,1223 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+// deno-lint-ignore-file camelcase no-inner-declarations no-this-alias
+
+import { ERR_INVALID_ARG_VALUE, ERR_USE_AFTER_CLOSE } from "internal:deno_node/polyfills/internal/errors.ts";
+import {
+ validateAbortSignal,
+ validateArray,
+ validateString,
+ validateUint32,
+} from "internal:deno_node/polyfills/internal/validators.mjs";
+import {
+ // inspect,
+ getStringWidth,
+ stripVTControlCharacters,
+} from "internal:deno_node/polyfills/internal/util/inspect.mjs";
+import EventEmitter from "internal:deno_node/polyfills/events.ts";
+import { emitKeypressEvents } from "internal:deno_node/polyfills/internal/readline/emitKeypressEvents.mjs";
+import {
+ charLengthAt,
+ charLengthLeft,
+ commonPrefix,
+ kSubstringSearch,
+} from "internal:deno_node/polyfills/internal/readline/utils.mjs";
+import { clearScreenDown, cursorTo, moveCursor } from "internal:deno_node/polyfills/internal/readline/callbacks.mjs";
+import { Readable } from "internal:deno_node/polyfills/_stream.mjs";
+
+import { StringDecoder } from "internal:deno_node/polyfills/string_decoder.ts";
+import {
+ kAddHistory,
+ kDecoder,
+ kDeleteLeft,
+ kDeleteLineLeft,
+ kDeleteLineRight,
+ kDeleteRight,
+ kDeleteWordLeft,
+ kDeleteWordRight,
+ kGetDisplayPos,
+ kHistoryNext,
+ kHistoryPrev,
+ kInsertString,
+ kLine,
+ kLine_buffer,
+ kMoveCursor,
+ kNormalWrite,
+ kOldPrompt,
+ kOnLine,
+ kPreviousKey,
+ kPrompt,
+ kQuestionCallback,
+ kRefreshLine,
+ kSawKeyPress,
+ kSawReturnAt,
+ kSetRawMode,
+ kTabComplete,
+ kTabCompleter,
+ kTtyWrite,
+ kWordLeft,
+ kWordRight,
+ kWriteToOutput,
+} from "internal:deno_node/polyfills/internal/readline/symbols.mjs";
+
+const kHistorySize = 30;
+const kMincrlfDelay = 100;
+// \r\n, \n, or \r followed by something other than \n
+const lineEnding = /\r?\n|\r(?!\n)/;
+
+const kLineObjectStream = Symbol("line object stream");
+export const kQuestionCancel = Symbol("kQuestionCancel");
+export const kQuestion = Symbol("kQuestion");
+
+// GNU readline library - keyseq-timeout is 500ms (default)
+const ESCAPE_CODE_TIMEOUT = 500;
+
+export {
+ kAddHistory,
+ kDecoder,
+ kDeleteLeft,
+ kDeleteLineLeft,
+ kDeleteLineRight,
+ kDeleteRight,
+ kDeleteWordLeft,
+ kDeleteWordRight,
+ kGetDisplayPos,
+ kHistoryNext,
+ kHistoryPrev,
+ kInsertString,
+ kLine,
+ kLine_buffer,
+ kMoveCursor,
+ kNormalWrite,
+ kOldPrompt,
+ kOnLine,
+ kPreviousKey,
+ kPrompt,
+ kQuestionCallback,
+ kRefreshLine,
+ kSawKeyPress,
+ kSawReturnAt,
+ kSetRawMode,
+ kTabComplete,
+ kTabCompleter,
+ kTtyWrite,
+ kWordLeft,
+ kWordRight,
+ kWriteToOutput,
+};
+
+export function InterfaceConstructor(input, output, completer, terminal) {
+ this[kSawReturnAt] = 0;
+ // TODO(BridgeAR): Document this property. The name is not ideal, so we
+ // might want to expose an alias and document that instead.
+ this.isCompletionEnabled = true;
+ this[kSawKeyPress] = false;
+ this[kPreviousKey] = null;
+ this.escapeCodeTimeout = ESCAPE_CODE_TIMEOUT;
+ this.tabSize = 8;
+ Function.prototype.call(EventEmitter, this);
+
+ let history;
+ let historySize;
+ let removeHistoryDuplicates = false;
+ let crlfDelay;
+ let prompt = "> ";
+ let signal;
+
+ if (input?.input) {
+ // An options object was given
+ output = input.output;
+ completer = input.completer;
+ terminal = input.terminal;
+ history = input.history;
+ historySize = input.historySize;
+ signal = input.signal;
+ if (input.tabSize !== undefined) {
+ validateUint32(input.tabSize, "tabSize", true);
+ this.tabSize = input.tabSize;
+ }
+ removeHistoryDuplicates = input.removeHistoryDuplicates;
+ if (input.prompt !== undefined) {
+ prompt = input.prompt;
+ }
+ if (input.escapeCodeTimeout !== undefined) {
+ if (Number.isFinite(input.escapeCodeTimeout)) {
+ this.escapeCodeTimeout = input.escapeCodeTimeout;
+ } else {
+ throw new ERR_INVALID_ARG_VALUE(
+ "input.escapeCodeTimeout",
+ this.escapeCodeTimeout,
+ );
+ }
+ }
+
+ if (signal) {
+ validateAbortSignal(signal, "options.signal");
+ }
+
+ crlfDelay = input.crlfDelay;
+ input = input.input;
+ }
+
+ if (completer !== undefined && typeof completer !== "function") {
+ throw new ERR_INVALID_ARG_VALUE("completer", completer);
+ }
+
+ if (history === undefined) {
+ history = [];
+ } else {
+ validateArray(history, "history");
+ }
+
+ if (historySize === undefined) {
+ historySize = kHistorySize;
+ }
+
+ if (
+ typeof historySize !== "number" ||
+ Number.isNaN(historySize) ||
+ historySize < 0
+ ) {
+ throw new ERR_INVALID_ARG_VALUE.RangeError("historySize", historySize);
+ }
+
+ // Backwards compat; check the isTTY prop of the output stream
+ // when `terminal` was not specified
+ if (terminal === undefined && !(output === null || output === undefined)) {
+ terminal = !!output.isTTY;
+ }
+
+ const self = this;
+
+ this.line = "";
+ this[kSubstringSearch] = null;
+ this.output = output;
+ this.input = input;
+ this.history = history;
+ this.historySize = historySize;
+ this.removeHistoryDuplicates = !!removeHistoryDuplicates;
+ this.crlfDelay = crlfDelay
+ ? Math.max(kMincrlfDelay, crlfDelay)
+ : kMincrlfDelay;
+ this.completer = completer;
+
+ this.setPrompt(prompt);
+
+ this.terminal = !!terminal;
+
+ function onerror(err) {
+ self.emit("error", err);
+ }
+
+ function ondata(data) {
+ self[kNormalWrite](data);
+ }
+
+ function onend() {
+ if (
+ typeof self[kLine_buffer] === "string" &&
+ self[kLine_buffer].length > 0
+ ) {
+ self.emit("line", self[kLine_buffer]);
+ }
+ self.close();
+ }
+
+ function ontermend() {
+ if (typeof self.line === "string" && self.line.length > 0) {
+ self.emit("line", self.line);
+ }
+ self.close();
+ }
+
+ function onkeypress(s, key) {
+ self[kTtyWrite](s, key);
+ if (key && key.sequence) {
+ // If the key.sequence is half of a surrogate pair
+ // (>= 0xd800 and <= 0xdfff), refresh the line so
+ // the character is displayed appropriately.
+ const ch = key.sequence.codePointAt(0);
+ if (ch >= 0xd800 && ch <= 0xdfff) self[kRefreshLine]();
+ }
+ }
+
+ function onresize() {
+ self[kRefreshLine]();
+ }
+
+ this[kLineObjectStream] = undefined;
+
+ input.on("error", onerror);
+
+ if (!this.terminal) {
+ function onSelfCloseWithoutTerminal() {
+ input.removeListener("data", ondata);
+ input.removeListener("error", onerror);
+ input.removeListener("end", onend);
+ }
+
+ input.on("data", ondata);
+ input.on("end", onend);
+ self.once("close", onSelfCloseWithoutTerminal);
+ this[kDecoder] = new StringDecoder("utf8");
+ } else {
+ function onSelfCloseWithTerminal() {
+ input.removeListener("keypress", onkeypress);
+ input.removeListener("error", onerror);
+ input.removeListener("end", ontermend);
+ if (output !== null && output !== undefined) {
+ output.removeListener("resize", onresize);
+ }
+ }
+
+ emitKeypressEvents(input, this);
+
+ // `input` usually refers to stdin
+ input.on("keypress", onkeypress);
+ input.on("end", ontermend);
+
+ this[kSetRawMode](true);
+ this.terminal = true;
+
+ // Cursor position on the line.
+ this.cursor = 0;
+
+ this.historyIndex = -1;
+
+ if (output !== null && output !== undefined) {
+ output.on("resize", onresize);
+ }
+
+ self.once("close", onSelfCloseWithTerminal);
+ }
+
+ if (signal) {
+ const onAborted = () => self.close();
+ if (signal.aborted) {
+ process.nextTick(onAborted);
+ } else {
+ signal.addEventListener("abort", onAborted, { once: true });
+ self.once("close", () => signal.removeEventListener("abort", onAborted));
+ }
+ }
+
+ // Current line
+ this.line = "";
+
+ input.resume();
+}
+
+Object.setPrototypeOf(InterfaceConstructor.prototype, EventEmitter.prototype);
+Object.setPrototypeOf(InterfaceConstructor, EventEmitter);
+
+export class Interface extends InterfaceConstructor {
+ // eslint-disable-next-line no-useless-constructor
+ constructor(input, output, completer, terminal) {
+ super(input, output, completer, terminal);
+ }
+ get columns() {
+ if (this.output && this.output.columns) return this.output.columns;
+ return Infinity;
+ }
+
+ /**
+ * Sets the prompt written to the output.
+ * @param {string} prompt
+ * @returns {void}
+ */
+ setPrompt(prompt) {
+ this[kPrompt] = prompt;
+ }
+
+ /**
+ * Returns the current prompt used by `rl.prompt()`.
+ * @returns {string}
+ */
+ getPrompt() {
+ return this[kPrompt];
+ }
+
+ [kSetRawMode](mode) {
+ const wasInRawMode = this.input.isRaw;
+
+ if (typeof this.input.setRawMode === "function") {
+ this.input.setRawMode(mode);
+ }
+
+ return wasInRawMode;
+ }
+
+ /**
+ * Writes the configured `prompt` to a new line in `output`.
+ * @param {boolean} [preserveCursor]
+ * @returns {void}
+ */
+ prompt(preserveCursor) {
+ if (this.paused) this.resume();
+ if (this.terminal && process.env.TERM !== "dumb") {
+ if (!preserveCursor) this.cursor = 0;
+ this[kRefreshLine]();
+ } else {
+ this[kWriteToOutput](this[kPrompt]);
+ }
+ }
+
+ [kQuestion](query, cb) {
+ if (this.closed) {
+ throw new ERR_USE_AFTER_CLOSE("readline");
+ }
+ if (this[kQuestionCallback]) {
+ this.prompt();
+ } else {
+ this[kOldPrompt] = this[kPrompt];
+ this.setPrompt(query);
+ this[kQuestionCallback] = cb;
+ this.prompt();
+ }
+ }
+
+ [kOnLine](line) {
+ if (this[kQuestionCallback]) {
+ const cb = this[kQuestionCallback];
+ this[kQuestionCallback] = null;
+ this.setPrompt(this[kOldPrompt]);
+ cb(line);
+ } else {
+ this.emit("line", line);
+ }
+ }
+
+ [kQuestionCancel]() {
+ if (this[kQuestionCallback]) {
+ this[kQuestionCallback] = null;
+ this.setPrompt(this[kOldPrompt]);
+ this.clearLine();
+ }
+ }
+
+ [kWriteToOutput](stringToWrite) {
+ validateString(stringToWrite, "stringToWrite");
+
+ if (this.output !== null && this.output !== undefined) {
+ this.output.write(stringToWrite);
+ }
+ }
+
+ [kAddHistory]() {
+ if (this.line.length === 0) return "";
+
+ // If the history is disabled then return the line
+ if (this.historySize === 0) return this.line;
+
+ // If the trimmed line is empty then return the line
+ if (this.line.trim().length === 0) return this.line;
+
+ if (this.history.length === 0 || this.history[0] !== this.line) {
+ if (this.removeHistoryDuplicates) {
+ // Remove older history line if identical to new one
+ const dupIndex = this.history.indexOf(this.line);
+ if (dupIndex !== -1) this.history.splice(dupIndex, 1);
+ }
+
+ this.history.unshift(this.line);
+
+ // Only store so many
+ if (this.history.length > this.historySize) {
+ this.history.pop();
+ }
+ }
+
+ this.historyIndex = -1;
+
+ // The listener could change the history object, possibly
+ // to remove the last added entry if it is sensitive and should
+ // not be persisted in the history, like a password
+ const line = this.history[0];
+
+ // Emit history event to notify listeners of update
+ this.emit("history", this.history);
+
+ return line;
+ }
+
+ [kRefreshLine]() {
+ // line length
+ const line = this[kPrompt] + this.line;
+ const dispPos = this[kGetDisplayPos](line);
+ const lineCols = dispPos.cols;
+ const lineRows = dispPos.rows;
+
+ // cursor position
+ 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);
+ }
+
+ // Cursor to left edge.
+ cursorTo(this.output, 0);
+ // erase data
+ clearScreenDown(this.output);
+
+ // Write the prompt and the current buffer content.
+ this[kWriteToOutput](line);
+
+ // Force terminal to allocate a new line
+ if (lineCols === 0) {
+ this[kWriteToOutput](" ");
+ }
+
+ // Move cursor to original position.
+ cursorTo(this.output, cursorPos.cols);
+
+ const diff = lineRows - cursorPos.rows;
+ if (diff > 0) {
+ moveCursor(this.output, 0, -diff);
+ }
+
+ this.prevRows = cursorPos.rows;
+ }
+
+ /**
+ * Closes the `readline.Interface` instance.
+ * @returns {void}
+ */
+ close() {
+ if (this.closed) return;
+ this.pause();
+ if (this.terminal) {
+ this[kSetRawMode](false);
+ }
+ this.closed = true;
+ this.emit("close");
+ }
+
+ /**
+ * Pauses the `input` stream.
+ * @returns {void | Interface}
+ */
+ pause() {
+ if (this.paused) return;
+ this.input.pause();
+ this.paused = true;
+ this.emit("pause");
+ return this;
+ }
+
+ /**
+ * Resumes the `input` stream if paused.
+ * @returns {void | Interface}
+ */
+ resume() {
+ if (!this.paused) return;
+ this.input.resume();
+ this.paused = false;
+ this.emit("resume");
+ return this;
+ }
+
+ /**
+ * Writes either `data` or a `key` sequence identified by
+ * `key` to the `output`.
+ * @param {string} d
+ * @param {{
+ * ctrl?: boolean;
+ * meta?: boolean;
+ * shift?: boolean;
+ * name?: string;
+ * }} [key]
+ * @returns {void}
+ */
+ write(d, key) {
+ if (this.paused) this.resume();
+ if (this.terminal) {
+ this[kTtyWrite](d, key);
+ } else {
+ this[kNormalWrite](d);
+ }
+ }
+
+ [kNormalWrite](b) {
+ if (b === undefined) {
+ return;
+ }
+ let string = this[kDecoder].write(b);
+ if (
+ this[kSawReturnAt] &&
+ Date.now() - this[kSawReturnAt] <= this.crlfDelay
+ ) {
+ string = string.replace(/^\n/, "");
+ this[kSawReturnAt] = 0;
+ }
+
+ // Run test() on the new string chunk, not on the entire line buffer.
+ const newPartContainsEnding = lineEnding.test(string);
+
+ if (this[kLine_buffer]) {
+ string = this[kLine_buffer] + string;
+ this[kLine_buffer] = null;
+ }
+ if (newPartContainsEnding) {
+ this[kSawReturnAt] = string.endsWith("\r") ? Date.now() : 0;
+
+ // Got one or more newlines; process into "line" events
+ const lines = string.split(lineEnding);
+ // Either '' or (conceivably) the unfinished portion of the next line
+ string = lines.pop();
+ this[kLine_buffer] = string;
+ for (let n = 0; n < lines.length; n++) this[kOnLine](lines[n]);
+ } else if (string) {
+ // No newlines this time, save what we have for next time
+ this[kLine_buffer] = string;
+ }
+ }
+
+ [kInsertString](c) {
+ if (this.cursor < this.line.length) {
+ const beg = this.line.slice(0, this.cursor);
+ const end = this.line.slice(
+ this.cursor,
+ this.line.length,
+ );
+ this.line = beg + c + end;
+ this.cursor += c.length;
+ this[kRefreshLine]();
+ } else {
+ this.line += c;
+ this.cursor += c.length;
+
+ if (this.getCursorPos().cols === 0) {
+ this[kRefreshLine]();
+ } else {
+ this[kWriteToOutput](c);
+ }
+ }
+ }
+
+ async [kTabComplete](lastKeypressWasTab) {
+ this.pause();
+ const string = this.line.slice(0, this.cursor);
+ let value;
+ try {
+ value = await this.completer(string);
+ } catch (err) {
+ // TODO(bartlomieju): inspect is not ported yet
+ // this[kWriteToOutput](`Tab completion error: ${inspect(err)}`);
+ this[kWriteToOutput](`Tab completion error: ${err}`);
+ return;
+ } finally {
+ this.resume();
+ }
+ this[kTabCompleter](lastKeypressWasTab, value);
+ }
+
+ [kTabCompleter](lastKeypressWasTab, { 0: completions, 1: completeOn }) {
+ // Result and the text that was completed.
+
+ if (!completions || completions.length === 0) {
+ return;
+ }
+
+ // If there is a common prefix to all matches, then apply that portion.
+ const prefix = commonPrefix(
+ completions.filter((e) => e !== ""),
+ );
+ if (
+ prefix.startsWith(completeOn) &&
+ prefix.length > completeOn.length
+ ) {
+ this[kInsertString](prefix.slice(completeOn.length));
+ return;
+ } else if (!completeOn.startsWith(prefix)) {
+ this.line = this.line.slice(0, this.cursor - completeOn.length) +
+ prefix +
+ this.line.slice(this.cursor, this.line.length);
+ this.cursor = this.cursor - completeOn.length + prefix.length;
+ this._refreshLine();
+ return;
+ }
+
+ if (!lastKeypressWasTab) {
+ return;
+ }
+
+ // Apply/show completions.
+ const completionsWidth = completions.map(
+ (e) => getStringWidth(e),
+ );
+ const width = Math.max.apply(completionsWidth) + 2; // 2 space padding
+ 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[kWriteToOutput](output);
+ this[kRefreshLine]();
+ }
+
+ [kWordLeft]() {
+ if (this.cursor > 0) {
+ // Reverse the string and match a word near beginning
+ // to avoid quadratic time complexity
+ const leading = this.line.slice(0, this.cursor);
+ const reversed = Array.from(leading).reverse().join("");
+ const match = reversed.match(/^\s*(?:[^\w\s]+|\w+)?/);
+ this[kMoveCursor](-match[0].length);
+ }
+ }
+
+ [kWordRight]() {
+ if (this.cursor < this.line.length) {
+ const trailing = this.line.slice(this.cursor);
+ const match = trailing.match(/^(?:\s+|[^\w\s]+|\w+)\s*/);
+ this[kMoveCursor](match[0].length);
+ }
+ }
+
+ [kDeleteLeft]() {
+ if (this.cursor > 0 && this.line.length > 0) {
+ // The number of UTF-16 units comprising the character to the left
+ const charSize = charLengthLeft(this.line, this.cursor);
+ this.line = this.line.slice(0, this.cursor - charSize) +
+ this.line.slice(this.cursor, this.line.length);
+
+ this.cursor -= charSize;
+ this[kRefreshLine]();
+ }
+ }
+
+ [kDeleteRight]() {
+ if (this.cursor < this.line.length) {
+ // The number of UTF-16 units comprising the character to the left
+ const charSize = charLengthAt(this.line, this.cursor);
+ this.line = this.line.slice(0, this.cursor) +
+ this.line.slice(
+ this.cursor + charSize,
+ this.line.length,
+ );
+ this[kRefreshLine]();
+ }
+ }
+
+ [kDeleteWordLeft]() {
+ if (this.cursor > 0) {
+ // Reverse the string and match a word near beginning
+ // to avoid quadratic time complexity
+ let leading = this.line.slice(0, this.cursor);
+ const reversed = Array.from(leading).reverse().join("");
+ const match = reversed.match(/^\s*(?:[^\w\s]+|\w+)?/);
+ leading = leading.slice(
+ 0,
+ leading.length - match[0].length,
+ );
+ this.line = leading +
+ this.line.slice(this.cursor, this.line.length);
+ this.cursor = leading.length;
+ this[kRefreshLine]();
+ }
+ }
+
+ [kDeleteWordRight]() {
+ if (this.cursor < this.line.length) {
+ const trailing = this.line.slice(this.cursor);
+ const match = trailing.match(/^(?:\s+|\W+|\w+)\s*/);
+ this.line = this.line.slice(0, this.cursor) +
+ trailing.slice(match[0].length);
+ this[kRefreshLine]();
+ }
+ }
+
+ [kDeleteLineLeft]() {
+ this.line = this.line.slice(this.cursor);
+ this.cursor = 0;
+ this[kRefreshLine]();
+ }
+
+ [kDeleteLineRight]() {
+ this.line = this.line.slice(0, this.cursor);
+ this[kRefreshLine]();
+ }
+
+ clearLine() {
+ this[kMoveCursor](+Infinity);
+ this[kWriteToOutput]("\r\n");
+ this.line = "";
+ this.cursor = 0;
+ this.prevRows = 0;
+ }
+
+ [kLine]() {
+ const line = this[kAddHistory]();
+ this.clearLine();
+ this[kOnLine](line);
+ }
+
+ // TODO(BridgeAR): Add underscores to the search part and a red background in
+ // case no match is found. This should only be the visual part and not the
+ // actual line content!
+ // TODO(BridgeAR): In case the substring based search is active and the end is
+ // reached, show a comment how to search the history as before. E.g., using
+ // <ctrl> + N. Only show this after two/three UPs or DOWNs, not on the first
+ // one.
+ [kHistoryNext]() {
+ if (this.historyIndex >= 0) {
+ const search = this[kSubstringSearch] || "";
+ let index = this.historyIndex - 1;
+ while (
+ index >= 0 &&
+ (!this.history[index].startsWith(search) ||
+ this.line === this.history[index])
+ ) {
+ index--;
+ }
+ if (index === -1) {
+ this.line = search;
+ } else {
+ this.line = this.history[index];
+ }
+ this.historyIndex = index;
+ this.cursor = this.line.length; // Set cursor to end of line.
+ this[kRefreshLine]();
+ }
+ }
+
+ [kHistoryPrev]() {
+ if (this.historyIndex < this.history.length && this.history.length) {
+ const search = this[kSubstringSearch] || "";
+ let index = this.historyIndex + 1;
+ while (
+ index < this.history.length &&
+ (!this.history[index].startsWith(search) ||
+ this.line === this.history[index])
+ ) {
+ index++;
+ }
+ if (index === this.history.length) {
+ this.line = search;
+ } else {
+ this.line = this.history[index];
+ }
+ this.historyIndex = index;
+ this.cursor = this.line.length; // Set cursor to end of line.
+ this[kRefreshLine]();
+ }
+ }
+
+ // Returns the last character's display position of the given string
+ [kGetDisplayPos](str) {
+ let offset = 0;
+ const col = this.columns;
+ let rows = 0;
+ str = stripVTControlCharacters(str);
+ for (const char of str[Symbol.iterator]()) {
+ if (char === "\n") {
+ // Rows must be incremented by 1 even if offset = 0 or col = +Infinity.
+ rows += Math.ceil(offset / col) || 1;
+ offset = 0;
+ continue;
+ }
+ // Tabs must be aligned by an offset of the tab size.
+ if (char === "\t") {
+ offset += this.tabSize - (offset % this.tabSize);
+ continue;
+ }
+ const width = getStringWidth(char);
+ if (width === 0 || width === 1) {
+ offset += width;
+ } else {
+ // width === 2
+ if ((offset + 1) % col === 0) {
+ offset++;
+ }
+ offset += 2;
+ }
+ }
+ const cols = offset % col;
+ rows += (offset - cols) / col;
+ return { cols, rows };
+ }
+
+ /**
+ * Returns the real position of the cursor in relation
+ * to the input prompt + string.
+ * @returns {{
+ * rows: number;
+ * cols: number;
+ * }}
+ */
+ getCursorPos() {
+ const strBeforeCursor = this[kPrompt] +
+ this.line.slice(0, this.cursor);
+ return this[kGetDisplayPos](strBeforeCursor);
+ }
+
+ // This function moves cursor dx places to the right
+ // (-dx for left) and refreshes the line if it is needed.
+ [kMoveCursor](dx) {
+ if (dx === 0) {
+ return;
+ }
+ const oldPos = this.getCursorPos();
+ this.cursor += dx;
+
+ // Bounds check
+ if (this.cursor < 0) {
+ this.cursor = 0;
+ } else if (this.cursor > this.line.length) {
+ this.cursor = this.line.length;
+ }
+
+ const newPos = this.getCursorPos();
+
+ // Check if cursor stayed on the line.
+ if (oldPos.rows === newPos.rows) {
+ const diffWidth = newPos.cols - oldPos.cols;
+ moveCursor(this.output, diffWidth, 0);
+ } else {
+ this[kRefreshLine]();
+ }
+ }
+
+ // Handle a write from the tty
+ [kTtyWrite](s, key) {
+ const previousKey = this[kPreviousKey];
+ key = key || {};
+ this[kPreviousKey] = key;
+
+ // Activate or deactivate substring search.
+ if (
+ (key.name === "up" || key.name === "down") &&
+ !key.ctrl &&
+ !key.meta &&
+ !key.shift
+ ) {
+ if (this[kSubstringSearch] === null) {
+ this[kSubstringSearch] = this.line.slice(
+ 0,
+ this.cursor,
+ );
+ }
+ } else if (this[kSubstringSearch] !== null) {
+ this[kSubstringSearch] = null;
+ // Reset the index in case there's no match.
+ if (this.history.length === this.historyIndex) {
+ this.historyIndex = -1;
+ }
+ }
+
+ // Ignore escape key, fixes
+ // https://github.com/nodejs/node-v0.x-archive/issues/2876.
+ if (key.name === "escape") return;
+
+ if (key.ctrl && key.shift) {
+ /* Control and shift pressed */
+ switch (key.name) {
+ // TODO(BridgeAR): The transmitted escape sequence is `\b` and that is
+ // identical to <ctrl>-h. It should have a unique escape sequence.
+ case "backspace":
+ this[kDeleteLineLeft]();
+ break;
+
+ case "delete":
+ this[kDeleteLineRight]();
+ break;
+ }
+ } else if (key.ctrl) {
+ /* Control key pressed */
+
+ switch (key.name) {
+ case "c":
+ if (this.listenerCount("SIGINT") > 0) {
+ this.emit("SIGINT");
+ } else {
+ // This readline instance is finished
+ this.close();
+ }
+ break;
+
+ case "h": // delete left
+ this[kDeleteLeft]();
+ break;
+
+ case "d": // delete right or EOF
+ if (this.cursor === 0 && this.line.length === 0) {
+ // This readline instance is finished
+ this.close();
+ } else if (this.cursor < this.line.length) {
+ this[kDeleteRight]();
+ }
+ break;
+
+ case "u": // Delete from current to start of line
+ this[kDeleteLineLeft]();
+ break;
+
+ case "k": // Delete from current to end of line
+ this[kDeleteLineRight]();
+ break;
+
+ case "a": // Go to the start of the line
+ this[kMoveCursor](-Infinity);
+ break;
+
+ case "e": // Go to the end of the line
+ this[kMoveCursor](+Infinity);
+ break;
+
+ case "b": // back one character
+ this[kMoveCursor](-charLengthLeft(this.line, this.cursor));
+ break;
+
+ case "f": // Forward one character
+ this[kMoveCursor](+charLengthAt(this.line, this.cursor));
+ break;
+
+ case "l": // Clear the whole screen
+ cursorTo(this.output, 0, 0);
+ clearScreenDown(this.output);
+ this[kRefreshLine]();
+ break;
+
+ case "n": // next history item
+ this[kHistoryNext]();
+ break;
+
+ case "p": // Previous history item
+ this[kHistoryPrev]();
+ break;
+
+ case "z":
+ if (process.platform === "win32") break;
+ if (this.listenerCount("SIGTSTP") > 0) {
+ this.emit("SIGTSTP");
+ } else {
+ process.once("SIGCONT", () => {
+ // Don't raise events if stream has already been abandoned.
+ if (!this.paused) {
+ // Stream must be paused and resumed after SIGCONT to catch
+ // SIGINT, SIGTSTP, and EOF.
+ this.pause();
+ this.emit("SIGCONT");
+ }
+ // Explicitly re-enable "raw mode" and move the cursor to
+ // the correct position.
+ // See https://github.com/joyent/node/issues/3295.
+ this[kSetRawMode](true);
+ this[kRefreshLine]();
+ });
+ this[kSetRawMode](false);
+ process.kill(process.pid, "SIGTSTP");
+ }
+ break;
+
+ case "w": // Delete backwards to a word boundary
+ // TODO(BridgeAR): The transmitted escape sequence is `\b` and that is
+ // identical to <ctrl>-h. It should have a unique escape sequence.
+ // Falls through
+ case "backspace":
+ this[kDeleteWordLeft]();
+ break;
+
+ case "delete": // Delete forward to a word boundary
+ this[kDeleteWordRight]();
+ break;
+
+ case "left":
+ this[kWordLeft]();
+ break;
+
+ case "right":
+ this[kWordRight]();
+ break;
+ }
+ } else if (key.meta) {
+ /* Meta key pressed */
+
+ switch (key.name) {
+ case "b": // backward word
+ this[kWordLeft]();
+ break;
+
+ case "f": // forward word
+ this[kWordRight]();
+ break;
+
+ case "d": // delete forward word
+ case "delete":
+ this[kDeleteWordRight]();
+ break;
+
+ case "backspace": // Delete backwards to a word boundary
+ this[kDeleteWordLeft]();
+ break;
+ }
+ } else {
+ /* No modifier keys used */
+
+ // \r bookkeeping is only relevant if a \n comes right after.
+ if (this[kSawReturnAt] && key.name !== "enter") this[kSawReturnAt] = 0;
+
+ switch (key.name) {
+ case "return": // Carriage return, i.e. \r
+ this[kSawReturnAt] = Date.now();
+ this[kLine]();
+ break;
+
+ case "enter":
+ // When key interval > crlfDelay
+ if (
+ this[kSawReturnAt] === 0 ||
+ Date.now() - this[kSawReturnAt] > this.crlfDelay
+ ) {
+ this[kLine]();
+ }
+ this[kSawReturnAt] = 0;
+ break;
+
+ case "backspace":
+ this[kDeleteLeft]();
+ break;
+
+ case "delete":
+ this[kDeleteRight]();
+ break;
+
+ case "left":
+ // Obtain the code point to the left
+ this[kMoveCursor](-charLengthLeft(this.line, this.cursor));
+ break;
+
+ case "right":
+ this[kMoveCursor](+charLengthAt(this.line, this.cursor));
+ break;
+
+ case "home":
+ this[kMoveCursor](-Infinity);
+ break;
+
+ case "end":
+ this[kMoveCursor](+Infinity);
+ break;
+
+ case "up":
+ this[kHistoryPrev]();
+ break;
+
+ case "down":
+ this[kHistoryNext]();
+ break;
+
+ case "tab":
+ // If tab completion enabled, do that...
+ if (
+ typeof this.completer === "function" &&
+ this.isCompletionEnabled
+ ) {
+ const lastKeypressWasTab = previousKey &&
+ previousKey.name === "tab";
+ this[kTabComplete](lastKeypressWasTab);
+ break;
+ }
+ // falls through
+ default:
+ if (typeof s === "string" && s) {
+ const lines = s.split(/\r\n|\n|\r/);
+ for (let i = 0, len = lines.length; i < len; i++) {
+ if (i > 0) {
+ this[kLine]();
+ }
+ this[kInsertString](lines[i]);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Creates an `AsyncIterator` object that iterates through
+ * each line in the input stream as a string.
+ * @typedef {{
+ * [Symbol.asyncIterator]: () => InterfaceAsyncIterator,
+ * next: () => Promise<string>
+ * }} InterfaceAsyncIterator
+ * @returns {InterfaceAsyncIterator}
+ */
+ [Symbol.asyncIterator]() {
+ if (this[kLineObjectStream] === undefined) {
+ const readable = new Readable({
+ objectMode: true,
+ read: () => {
+ this.resume();
+ },
+ destroy: (err, cb) => {
+ this.off("line", lineListener);
+ this.off("close", closeListener);
+ this.close();
+ cb(err);
+ },
+ });
+ const lineListener = (input) => {
+ if (!readable.push(input)) {
+ // TODO(rexagod): drain to resume flow
+ this.pause();
+ }
+ };
+ const closeListener = () => {
+ readable.push(null);
+ };
+ const errorListener = (err) => {
+ readable.destroy(err);
+ };
+ this.on("error", errorListener);
+ this.on("line", lineListener);
+ this.on("close", closeListener);
+ this[kLineObjectStream] = readable;
+ }
+
+ return this[kLineObjectStream][Symbol.asyncIterator]();
+ }
+}
diff --git a/ext/node/polyfills/internal/readline/promises.mjs b/ext/node/polyfills/internal/readline/promises.mjs
new file mode 100644
index 000000000..36aa3de12
--- /dev/null
+++ b/ext/node/polyfills/internal/readline/promises.mjs
@@ -0,0 +1,139 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+// Copyright Joyent, Inc. and other Node contributors.
+
+import { ArrayPrototypeJoin, ArrayPrototypePush } from "internal:deno_node/polyfills/internal/primordials.mjs";
+
+import { CSI } from "internal:deno_node/polyfills/internal/readline/utils.mjs";
+import { validateBoolean, validateInteger } from "internal:deno_node/polyfills/internal/validators.mjs";
+import { isWritable } from "internal:deno_node/polyfills/internal/streams/utils.mjs";
+import { ERR_INVALID_ARG_TYPE } from "internal:deno_node/polyfills/internal/errors.ts";
+
+const {
+ kClearToLineBeginning,
+ kClearToLineEnd,
+ kClearLine,
+ kClearScreenDown,
+} = CSI;
+
+export class Readline {
+ #autoCommit = false;
+ #stream;
+ #todo = [];
+
+ constructor(stream, options = undefined) {
+ if (!isWritable(stream)) {
+ throw new ERR_INVALID_ARG_TYPE("stream", "Writable", stream);
+ }
+ this.#stream = stream;
+ if (options?.autoCommit != null) {
+ validateBoolean(options.autoCommit, "options.autoCommit");
+ this.#autoCommit = options.autoCommit;
+ }
+ }
+
+ /**
+ * Moves the cursor to the x and y coordinate on the given stream.
+ * @param {integer} x
+ * @param {integer} [y]
+ * @returns {Readline} this
+ */
+ cursorTo(x, y = undefined) {
+ validateInteger(x, "x");
+ if (y != null) validateInteger(y, "y");
+
+ const data = y == null ? CSI`${x + 1}G` : CSI`${y + 1};${x + 1}H`;
+ if (this.#autoCommit) process.nextTick(() => this.#stream.write(data));
+ else ArrayPrototypePush(this.#todo, data);
+
+ return this;
+ }
+
+ /**
+ * Moves the cursor relative to its current location.
+ * @param {integer} dx
+ * @param {integer} dy
+ * @returns {Readline} this
+ */
+ moveCursor(dx, dy) {
+ if (dx || dy) {
+ 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`;
+ }
+ if (this.#autoCommit) process.nextTick(() => this.#stream.write(data));
+ else ArrayPrototypePush(this.#todo, data);
+ }
+ return this;
+ }
+
+ /**
+ * Clears the current line the cursor is on.
+ * @param {-1|0|1} dir Direction to clear:
+ * -1 for left of the cursor
+ * +1 for right of the cursor
+ * 0 for the entire line
+ * @returns {Readline} this
+ */
+ clearLine(dir) {
+ validateInteger(dir, "dir", -1, 1);
+
+ const data = dir < 0
+ ? kClearToLineBeginning
+ : dir > 0
+ ? kClearToLineEnd
+ : kClearLine;
+ if (this.#autoCommit) process.nextTick(() => this.#stream.write(data));
+ else ArrayPrototypePush(this.#todo, data);
+ return this;
+ }
+
+ /**
+ * Clears the screen from the current position of the cursor down.
+ * @returns {Readline} this
+ */
+ clearScreenDown() {
+ if (this.#autoCommit) {
+ process.nextTick(() => this.#stream.write(kClearScreenDown));
+ } else {
+ ArrayPrototypePush(this.#todo, kClearScreenDown);
+ }
+ return this;
+ }
+
+ /**
+ * Sends all the pending actions to the associated `stream` and clears the
+ * internal list of pending actions.
+ * @returns {Promise<void>} Resolves when all pending actions have been
+ * flushed to the associated `stream`.
+ */
+ commit() {
+ return new Promise((resolve) => {
+ this.#stream.write(ArrayPrototypeJoin(this.#todo, ""), resolve);
+ this.#todo = [];
+ });
+ }
+
+ /**
+ * Clears the internal list of pending actions without sending it to the
+ * associated `stream`.
+ * @returns {Readline} this
+ */
+ rollback() {
+ this.#todo = [];
+ return this;
+ }
+}
+
+export default Readline;
diff --git a/ext/node/polyfills/internal/readline/symbols.mjs b/ext/node/polyfills/internal/readline/symbols.mjs
new file mode 100644
index 000000000..3a05c64d2
--- /dev/null
+++ b/ext/node/polyfills/internal/readline/symbols.mjs
@@ -0,0 +1,34 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+// Copyright Joyent, Inc. and Node.js contributors. All rights reserved. MIT license.
+
+export const kAddHistory = Symbol("_addHistory");
+export const kDecoder = Symbol("_decoder");
+export const kDeleteLeft = Symbol("_deleteLeft");
+export const kDeleteLineLeft = Symbol("_deleteLineLeft");
+export const kDeleteLineRight = Symbol("_deleteLineRight");
+export const kDeleteRight = Symbol("_deleteRight");
+export const kDeleteWordLeft = Symbol("_deleteWordLeft");
+export const kDeleteWordRight = Symbol("_deleteWordRight");
+export const kGetDisplayPos = Symbol("_getDisplayPos");
+export const kHistoryNext = Symbol("_historyNext");
+export const kHistoryPrev = Symbol("_historyPrev");
+export const kInsertString = Symbol("_insertString");
+export const kLine = Symbol("_line");
+export const kLine_buffer = Symbol("_line_buffer");
+export const kMoveCursor = Symbol("_moveCursor");
+export const kNormalWrite = Symbol("_normalWrite");
+export const kOldPrompt = Symbol("_oldPrompt");
+export const kOnLine = Symbol("_onLine");
+export const kPreviousKey = Symbol("_previousKey");
+export const kPrompt = Symbol("_prompt");
+export const kQuestionCallback = Symbol("_questionCallback");
+export const kRefreshLine = Symbol("_refreshLine");
+export const kSawKeyPress = Symbol("_sawKeyPress");
+export const kSawReturnAt = Symbol("_sawReturnAt");
+export const kSetRawMode = Symbol("_setRawMode");
+export const kTabComplete = Symbol("_tabComplete");
+export const kTabCompleter = Symbol("_tabCompleter");
+export const kTtyWrite = Symbol("_ttyWrite");
+export const kWordLeft = Symbol("_wordLeft");
+export const kWordRight = Symbol("_wordRight");
+export const kWriteToOutput = Symbol("_writeToOutput");
diff --git a/ext/node/polyfills/internal/readline/utils.mjs b/ext/node/polyfills/internal/readline/utils.mjs
new file mode 100644
index 000000000..6224f112b
--- /dev/null
+++ b/ext/node/polyfills/internal/readline/utils.mjs
@@ -0,0 +1,580 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+"use strict";
+
+const kUTF16SurrogateThreshold = 0x10000; // 2 ** 16
+const kEscape = "\x1b";
+export const kSubstringSearch = Symbol("kSubstringSearch");
+
+export function CSI(strings, ...args) {
+ let ret = `${kEscape}[`;
+ for (let n = 0; n < strings.length; n++) {
+ ret += strings[n];
+ if (n < args.length) {
+ ret += args[n];
+ }
+ }
+ return ret;
+}
+
+CSI.kEscape = kEscape;
+CSI.kClearToLineBeginning = CSI`1K`;
+CSI.kClearToLineEnd = CSI`0K`;
+CSI.kClearLine = CSI`2K`;
+CSI.kClearScreenDown = CSI`0J`;
+
+// TODO(BridgeAR): Treat combined characters as single character, i.e,
+// 'a\u0301' and '\u0301a' (both have the same visual output).
+// Check Canonical_Combining_Class in
+// http://userguide.icu-project.org/strings/properties
+export function charLengthLeft(str, i) {
+ if (i <= 0) {
+ return 0;
+ }
+ if (
+ (i > 1 &&
+ str.codePointAt(i - 2) >= kUTF16SurrogateThreshold) ||
+ str.codePointAt(i - 1) >= kUTF16SurrogateThreshold
+ ) {
+ return 2;
+ }
+ return 1;
+}
+
+export function charLengthAt(str, i) {
+ if (str.length <= i) {
+ // Pretend to move to the right. This is necessary to autocomplete while
+ // moving to the right.
+ return 1;
+ }
+ return str.codePointAt(i) >= kUTF16SurrogateThreshold ? 2 : 1;
+}
+
+/*
+ Some patterns seen in terminal key escape codes, derived from combos seen
+ at http://www.midnight-commander.org/browser/lib/tty/key.c
+
+ ESC letter
+ ESC [ letter
+ ESC [ modifier letter
+ ESC [ 1 ; modifier letter
+ ESC [ num char
+ ESC [ num ; modifier char
+ ESC O letter
+ ESC O modifier letter
+ ESC O 1 ; modifier letter
+ ESC N letter
+ ESC [ [ num ; modifier char
+ ESC [ [ 1 ; modifier letter
+ ESC ESC [ num char
+ ESC ESC O letter
+
+ - char is usually ~ but $ and ^ also happen with rxvt
+ - modifier is 1 +
+ (shift * 1) +
+ (left_alt * 2) +
+ (ctrl * 4) +
+ (right_alt * 8)
+ - two leading ESCs apparently mean the same as one leading ESC
+*/
+export function* emitKeys(stream) {
+ while (true) {
+ let ch = yield;
+ let s = ch;
+ let escaped = false;
+ const key = {
+ sequence: null,
+ name: undefined,
+ ctrl: false,
+ meta: false,
+ shift: false,
+ };
+
+ if (ch === kEscape) {
+ escaped = true;
+ s += ch = yield;
+
+ if (ch === kEscape) {
+ s += ch = yield;
+ }
+ }
+
+ if (escaped && (ch === "O" || ch === "[")) {
+ // ANSI escape sequence
+ let code = ch;
+ let modifier = 0;
+
+ if (ch === "O") {
+ // ESC O letter
+ // ESC O modifier letter
+ s += ch = yield;
+
+ if (ch >= "0" && ch <= "9") {
+ modifier = (ch >> 0) - 1;
+ s += ch = yield;
+ }
+
+ code += ch;
+ } else if (ch === "[") {
+ // ESC [ letter
+ // ESC [ modifier letter
+ // ESC [ [ modifier letter
+ // ESC [ [ num char
+ s += ch = yield;
+
+ if (ch === "[") {
+ // \x1b[[A
+ // ^--- escape codes might have a second bracket
+ code += ch;
+ s += ch = yield;
+ }
+
+ /*
+ * Here and later we try to buffer just enough data to get
+ * a complete ascii sequence.
+ *
+ * We have basically two classes of ascii characters to process:
+ *
+ * 1. `\x1b[24;5~` should be parsed as { code: '[24~', modifier: 5 }
+ *
+ * This particular example is featuring Ctrl+F12 in xterm.
+ *
+ * - `;5` part is optional, e.g. it could be `\x1b[24~`
+ * - first part can contain one or two digits
+ *
+ * So the generic regexp is like /^\d\d?(;\d)?[~^$]$/
+ *
+ * 2. `\x1b[1;5H` should be parsed as { code: '[H', modifier: 5 }
+ *
+ * This particular example is featuring Ctrl+Home in xterm.
+ *
+ * - `1;5` part is optional, e.g. it could be `\x1b[H`
+ * - `1;` part is optional, e.g. it could be `\x1b[5H`
+ *
+ * So the generic regexp is like /^((\d;)?\d)?[A-Za-z]$/
+ */
+ const cmdStart = s.length - 1;
+
+ // Skip one or two leading digits
+ if (ch >= "0" && ch <= "9") {
+ s += ch = yield;
+
+ if (ch >= "0" && ch <= "9") {
+ s += ch = yield;
+ }
+ }
+
+ // skip modifier
+ if (ch === ";") {
+ s += ch = yield;
+
+ if (ch >= "0" && ch <= "9") {
+ s += yield;
+ }
+ }
+
+ /*
+ * We buffered enough data, now trying to extract code
+ * and modifier from it
+ */
+ const cmd = s.slice(cmdStart);
+ let match;
+
+ if ((match = cmd.match(/^(\d\d?)(;(\d))?([~^$])$/))) {
+ code += match[1] + match[4];
+ modifier = (match[3] || 1) - 1;
+ } else if (
+ (match = cmd.match(/^((\d;)?(\d))?([A-Za-z])$/))
+ ) {
+ code += match[4];
+ modifier = (match[3] || 1) - 1;
+ } else {
+ code += cmd;
+ }
+ }
+
+ // Parse the key modifier
+ key.ctrl = !!(modifier & 4);
+ key.meta = !!(modifier & 10);
+ key.shift = !!(modifier & 1);
+ key.code = code;
+
+ // Parse the key itself
+ switch (code) {
+ /* xterm/gnome ESC [ letter (with modifier) */
+ case "[P":
+ key.name = "f1";
+ break;
+ case "[Q":
+ key.name = "f2";
+ break;
+ case "[R":
+ key.name = "f3";
+ break;
+ case "[S":
+ key.name = "f4";
+ break;
+
+ /* xterm/gnome ESC O letter (without modifier) */
+
+ case "OP":
+ key.name = "f1";
+ break;
+ case "OQ":
+ key.name = "f2";
+ break;
+ case "OR":
+ key.name = "f3";
+ break;
+ case "OS":
+ key.name = "f4";
+ break;
+
+ /* xterm/rxvt ESC [ number ~ */
+
+ case "[11~":
+ key.name = "f1";
+ break;
+ case "[12~":
+ key.name = "f2";
+ break;
+ case "[13~":
+ key.name = "f3";
+ break;
+ case "[14~":
+ key.name = "f4";
+ break;
+
+ /* from Cygwin and used in libuv */
+
+ case "[[A":
+ key.name = "f1";
+ break;
+ case "[[B":
+ key.name = "f2";
+ break;
+ case "[[C":
+ key.name = "f3";
+ break;
+ case "[[D":
+ key.name = "f4";
+ break;
+ case "[[E":
+ key.name = "f5";
+ break;
+
+ /* common */
+
+ case "[15~":
+ key.name = "f5";
+ break;
+ case "[17~":
+ key.name = "f6";
+ break;
+ case "[18~":
+ key.name = "f7";
+ break;
+ case "[19~":
+ key.name = "f8";
+ break;
+ case "[20~":
+ key.name = "f9";
+ break;
+ case "[21~":
+ key.name = "f10";
+ break;
+ case "[23~":
+ key.name = "f11";
+ break;
+ case "[24~":
+ key.name = "f12";
+ break;
+
+ /* xterm ESC [ letter */
+
+ case "[A":
+ key.name = "up";
+ break;
+ case "[B":
+ key.name = "down";
+ break;
+ case "[C":
+ key.name = "right";
+ break;
+ case "[D":
+ key.name = "left";
+ break;
+ case "[E":
+ key.name = "clear";
+ break;
+ case "[F":
+ key.name = "end";
+ break;
+ case "[H":
+ key.name = "home";
+ break;
+
+ /* xterm/gnome ESC O letter */
+
+ case "OA":
+ key.name = "up";
+ break;
+ case "OB":
+ key.name = "down";
+ break;
+ case "OC":
+ key.name = "right";
+ break;
+ case "OD":
+ key.name = "left";
+ break;
+ case "OE":
+ key.name = "clear";
+ break;
+ case "OF":
+ key.name = "end";
+ break;
+ case "OH":
+ key.name = "home";
+ break;
+
+ /* xterm/rxvt ESC [ number ~ */
+
+ case "[1~":
+ key.name = "home";
+ break;
+ case "[2~":
+ key.name = "insert";
+ break;
+ case "[3~":
+ key.name = "delete";
+ break;
+ case "[4~":
+ key.name = "end";
+ break;
+ case "[5~":
+ key.name = "pageup";
+ break;
+ case "[6~":
+ key.name = "pagedown";
+ break;
+
+ /* putty */
+
+ case "[[5~":
+ key.name = "pageup";
+ break;
+ case "[[6~":
+ key.name = "pagedown";
+ break;
+
+ /* rxvt */
+
+ case "[7~":
+ key.name = "home";
+ break;
+ case "[8~":
+ key.name = "end";
+ break;
+
+ /* rxvt keys with modifiers */
+
+ case "[a":
+ key.name = "up";
+ key.shift = true;
+ break;
+ case "[b":
+ key.name = "down";
+ key.shift = true;
+ break;
+ case "[c":
+ key.name = "right";
+ key.shift = true;
+ break;
+ case "[d":
+ key.name = "left";
+ key.shift = true;
+ break;
+ case "[e":
+ key.name = "clear";
+ key.shift = true;
+ break;
+
+ case "[2$":
+ key.name = "insert";
+ key.shift = true;
+ break;
+ case "[3$":
+ key.name = "delete";
+ key.shift = true;
+ break;
+ case "[5$":
+ key.name = "pageup";
+ key.shift = true;
+ break;
+ case "[6$":
+ key.name = "pagedown";
+ key.shift = true;
+ break;
+ case "[7$":
+ key.name = "home";
+ key.shift = true;
+ break;
+ case "[8$":
+ key.name = "end";
+ key.shift = true;
+ break;
+
+ case "Oa":
+ key.name = "up";
+ key.ctrl = true;
+ break;
+ case "Ob":
+ key.name = "down";
+ key.ctrl = true;
+ break;
+ case "Oc":
+ key.name = "right";
+ key.ctrl = true;
+ break;
+ case "Od":
+ key.name = "left";
+ key.ctrl = true;
+ break;
+ case "Oe":
+ key.name = "clear";
+ key.ctrl = true;
+ break;
+
+ case "[2^":
+ key.name = "insert";
+ key.ctrl = true;
+ break;
+ case "[3^":
+ key.name = "delete";
+ key.ctrl = true;
+ break;
+ case "[5^":
+ key.name = "pageup";
+ key.ctrl = true;
+ break;
+ case "[6^":
+ key.name = "pagedown";
+ key.ctrl = true;
+ break;
+ case "[7^":
+ key.name = "home";
+ key.ctrl = true;
+ break;
+ case "[8^":
+ key.name = "end";
+ key.ctrl = true;
+ break;
+
+ /* misc. */
+
+ case "[Z":
+ key.name = "tab";
+ key.shift = true;
+ break;
+ default:
+ key.name = "undefined";
+ break;
+ }
+ } else if (ch === "\r") {
+ // carriage return
+ key.name = "return";
+ key.meta = escaped;
+ } else if (ch === "\n") {
+ // Enter, should have been called linefeed
+ key.name = "enter";
+ key.meta = escaped;
+ } else if (ch === "\t") {
+ // tab
+ key.name = "tab";
+ key.meta = escaped;
+ } else if (ch === "\b" || ch === "\x7f") {
+ // backspace or ctrl+h
+ key.name = "backspace";
+ key.meta = escaped;
+ } else if (ch === kEscape) {
+ // escape key
+ key.name = "escape";
+ key.meta = escaped;
+ } else if (ch === " ") {
+ key.name = "space";
+ key.meta = escaped;
+ } else if (!escaped && ch <= "\x1a") {
+ // ctrl+letter
+ key.name = String.fromCharCode(
+ ch.charCodeAt() + "a".charCodeAt() - 1,
+ );
+ key.ctrl = true;
+ } else if (/^[0-9A-Za-z]$/.test(ch)) {
+ // Letter, number, shift+letter
+ key.name = ch.toLowerCase();
+ key.shift = /^[A-Z]$/.test(ch);
+ key.meta = escaped;
+ } else if (escaped) {
+ // Escape sequence timeout
+ key.name = ch.length ? undefined : "escape";
+ key.meta = true;
+ }
+
+ key.sequence = s;
+
+ if (s.length !== 0 && (key.name !== undefined || escaped)) {
+ /* Named character or sequence */
+ stream.emit("keypress", escaped ? undefined : s, key);
+ } else if (charLengthAt(s, 0) === s.length) {
+ /* Single unnamed character, e.g. "." */
+ stream.emit("keypress", s, key);
+ }
+ /* Unrecognized or broken escape sequence, don't emit anything */
+ }
+}
+
+// This runs in O(n log n).
+export function commonPrefix(strings) {
+ if (strings.length === 1) {
+ return strings[0];
+ }
+ const sorted = strings.slice().sort();
+ const min = sorted[0];
+ const max = sorted[sorted.length - 1];
+ for (let i = 0; i < min.length; i++) {
+ if (min[i] !== max[i]) {
+ return min.slice(0, i);
+ }
+ }
+ return min;
+}
+
+export default {
+ CSI,
+ charLengthAt,
+ charLengthLeft,
+ emitKeys,
+ commonPrefix,
+ kSubstringSearch,
+};