aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Derrick Farris <mr.dcfarris@gmail.com> 2023-01-08 03:49:49 -0600
committerGravatar GitHub <noreply@github.com> 2023-01-08 01:49:49 -0800
commit94409770dece8bb9dfc23f4bdc2f240836035d87 (patch)
tree4cc627eb67c476871e84141a6c7a583e29e98309
parentc505f172b84f5359aa186513f3ef7d6394bfc7b2 (diff)
downloadbun-94409770dece8bb9dfc23f4bdc2f240836035d87.tar.gz
bun-94409770dece8bb9dfc23f4bdc2f240836035d87.tar.zst
bun-94409770dece8bb9dfc23f4bdc2f240836035d87.zip
feat(node:readline): add node:readline and node:readline/promises (#1738)
* feat(readline): WIP: add readline * test(helpers): add deepStrictEqual helper * feat(readline): add readline & readline/promises to loader * fix(node:events): emit newListener on new listener added * feat(readline): finish readline cb interface, add tests * fix(stream): fix Transform.end() * fix(node-test-helpers): correct throws behavior, improve how all asserts work * feat(readline/promises): add readline/promises * feat(assert): add assert.match * test(readline): uncomment more tests * fix(readline): MaxCeil -> MathCeil 🤦 * fix(readline): export promises from node:readline * fix(readline): temp fix for circular dependency * cleanup(readline): remove console.log * fix(readline): change true -> 0 for CommonJS export * perf(readline): micro-optimizations with some getters * perf(readline): lazy load isWritable * cleanup(readline): rename debug flag env var to BUN_JS_DEBUG
-rw-r--r--src/bun.js/assert.exports.js24
-rw-r--r--src/bun.js/bindings/webcore/JSEventEmitter.cpp9
-rw-r--r--src/bun.js/module_loader.zig26
-rw-r--r--src/bun.js/readline.exports.js3128
-rw-r--r--src/bun.js/readline_promises.exports.js218
-rw-r--r--src/bun.js/streams.exports.js2
-rw-r--r--test/bun.js/node-test-helpers.js156
-rw-r--r--test/bun.js/node-test-helpers.ts198
-rw-r--r--test/bun.js/readline.node.test.ts2018
-rw-r--r--test/bun.js/readline_promises.node.test.ts55
10 files changed, 5673 insertions, 161 deletions
diff --git a/src/bun.js/assert.exports.js b/src/bun.js/assert.exports.js
index a66c0c241..179d8494d 100644
--- a/src/bun.js/assert.exports.js
+++ b/src/bun.js/assert.exports.js
@@ -1195,6 +1195,20 @@ var require_assert = __commonJS({
stackStartFn: notStrictEqual,
});
};
+ assert.match = function match(actual, expected, message) {
+ if (arguments.length < 2)
+ throw new ERR_MISSING_ARGS("actual", "expected");
+ if (!isRegExp(expected))
+ throw new ERR_INVALID_ARG_TYPE("expected", "RegExp", expected);
+ expected.test(actual) ||
+ innerFail({
+ actual,
+ expected,
+ message,
+ operator: "match",
+ stackStartFn: match,
+ });
+ };
var Comparison = function Comparison2(obj, keys, actual) {
var _this = this;
_classCallCheck(this, Comparison2),
@@ -1264,10 +1278,12 @@ var require_assert = __commonJS({
);
return (
keys.forEach(function (key) {
- (typeof actual[key] == "string" &&
- isRegExp(expected[key]) &&
- expected[key].test(actual[key])) ||
- compareExceptionKey(actual, expected, key, msg, keys, fn);
+ return (
+ (typeof actual[key] == "string" &&
+ isRegExp(expected[key]) &&
+ expected[key].test(actual[key])) ||
+ compareExceptionKey(actual, expected, key, msg, keys, fn)
+ );
}),
!0
);
diff --git a/src/bun.js/bindings/webcore/JSEventEmitter.cpp b/src/bun.js/bindings/webcore/JSEventEmitter.cpp
index b50f77cae..8d7d1ca25 100644
--- a/src/bun.js/bindings/webcore/JSEventEmitter.cpp
+++ b/src/bun.js/bindings/webcore/JSEventEmitter.cpp
@@ -237,6 +237,15 @@ static inline JSC::EncodedJSValue addListener(JSC::JSGlobalObject* lexicalGlobal
RETURN_IF_EXCEPTION(throwScope, encodedJSValue());
auto result = JSValue::encode(toJS<IDLUndefined>(*lexicalGlobalObject, throwScope, [&]() -> decltype(auto) { return impl.addListenerForBindings(WTFMove(eventType), WTFMove(listener), once, prepend); }));
RETURN_IF_EXCEPTION(throwScope, encodedJSValue());
+
+ JSC::Identifier newListenerEventType = JSC::Identifier::fromString(vm, "newListener"_s);
+ JSC::MarkedArgumentBuffer args;
+ args.append(argument0.value());
+ args.append(argument1.value());
+
+ auto result2 = JSValue::encode(toJS<IDLBoolean>(*lexicalGlobalObject, throwScope, [&]() -> decltype(auto) { return impl.emitForBindings(WTFMove(newListenerEventType), WTFMove(args)); }));
+ RETURN_IF_EXCEPTION(throwScope, encodedJSValue());
+
vm.writeBarrier(&static_cast<JSObject&>(*castedThis), argument1.value());
RELEASE_AND_RETURN(throwScope, JSValue::encode(actualThis));
}
diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig
index 6432fb7ca..46b7d0c09 100644
--- a/src/bun.js/module_loader.zig
+++ b/src/bun.js/module_loader.zig
@@ -1710,6 +1710,24 @@ pub const ModuleLoader = struct {
.hash = 0,
};
},
+ .@"node:readline" => {
+ return ResolvedSource{
+ .allocator = null,
+ .source_code = ZigString.init(jsModuleFromFile(jsc_vm.load_builtins_from_path, "readline.exports.js")),
+ .specifier = ZigString.init("node:readline"),
+ .source_url = ZigString.init("node:readline"),
+ .hash = 0,
+ };
+ },
+ .@"node:readline/promises" => {
+ return ResolvedSource{
+ .allocator = null,
+ .source_code = ZigString.init(jsModuleFromFile(jsc_vm.load_builtins_from_path, "readline_promises.exports.js")),
+ .specifier = ZigString.init("node:readline/promises"),
+ .source_url = ZigString.init("node:readline/promises"),
+ .hash = 0,
+ };
+ },
.@"bun:ffi" => {
return ResolvedSource{
.allocator = null,
@@ -2016,6 +2034,8 @@ pub const HardcodedModule = enum {
@"node:path/win32",
@"node:perf_hooks",
@"node:process",
+ @"node:readline",
+ @"node:readline/promises",
@"node:stream",
@"node:stream/consumers",
@"node:stream/web",
@@ -2061,6 +2081,8 @@ pub const HardcodedModule = enum {
.{ "node:path/win32", HardcodedModule.@"node:path/win32" },
.{ "node:perf_hooks", HardcodedModule.@"node:perf_hooks" },
.{ "node:process", HardcodedModule.@"node:process" },
+ .{ "node:readline", HardcodedModule.@"node:readline" },
+ .{ "node:readline/promises", HardcodedModule.@"node:readline/promises" },
.{ "node:stream", HardcodedModule.@"node:stream" },
.{ "node:stream/consumers", HardcodedModule.@"node:stream/consumers" },
.{ "node:stream/web", HardcodedModule.@"node:stream/web" },
@@ -2119,6 +2141,8 @@ pub const HardcodedModule = enum {
.{ "node:path/win32", "node:path/win32" },
.{ "node:perf_hooks", "node:perf_hooks" },
.{ "node:process", "node:process" },
+ .{ "node:readline", "node:readline" },
+ .{ "node:readline/promises", "node:readline/promises" },
.{ "node:stream", "node:stream" },
.{ "node:stream/consumers", "node:stream/consumers" },
.{ "node:stream/web", "node:stream/web" },
@@ -2138,6 +2162,8 @@ pub const HardcodedModule = enum {
.{ "readable-stream", "node:stream" },
.{ "readable-stream/consumer", "node:stream/consumers" },
.{ "readable-stream/web", "node:stream/web" },
+ .{ "readline", "node:readline" },
+ .{ "readline/promises", "node:readline/promises" },
.{ "stream", "node:stream" },
.{ "stream/consumers", "node:stream/consumers" },
.{ "stream/web", "node:stream/web" },
diff --git a/src/bun.js/readline.exports.js b/src/bun.js/readline.exports.js
new file mode 100644
index 000000000..73b8d24c5
--- /dev/null
+++ b/src/bun.js/readline.exports.js
@@ -0,0 +1,3128 @@
+// Attribution: Some parts of of this module are derived from code originating from the Node.js
+// readline module which is licensed under an MIT license:
+//
+// Copyright Node.js contributors. All rights reserved.
+//
+// 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.
+
+// ----------------------------------------------------------------------------
+// Section: Imports
+// ----------------------------------------------------------------------------
+var { Array, RegExp, String, Bun } = import.meta.primordials;
+var EventEmitter = import.meta.require("node:events");
+var { clearTimeout, setTimeout } = import.meta.require("timers");
+var { StringDecoder } = import.meta.require("string_decoder");
+
+var { inspect } = Bun;
+var debug = process.env.BUN_JS_DEBUG ? console.log : () => {};
+
+// ----------------------------------------------------------------------------
+// Section: Preamble
+// ----------------------------------------------------------------------------
+
+var SymbolAsyncIterator = Symbol.asyncIterator;
+var SymbolIterator = Symbol.iterator;
+var SymbolFor = Symbol.for;
+var SymbolReplace = Symbol.replace;
+var ArrayFrom = Array.from;
+var ArrayIsArray = Array.isArray;
+var ArrayPrototypeFilter = Array.prototype.filter;
+var ArrayPrototypeSort = Array.prototype.sort;
+var ArrayPrototypeIndexOf = Array.prototype.indexOf;
+var ArrayPrototypeJoin = Array.prototype.join;
+var ArrayPrototypeMap = Array.prototype.map;
+var ArrayPrototypePop = Array.prototype.pop;
+var ArrayPrototypePush = Array.prototype.push;
+var ArrayPrototypeSlice = Array.prototype.slice;
+var ArrayPrototypeSplice = Array.prototype.splice;
+var ArrayPrototypeReverse = Array.prototype.reverse;
+var ArrayPrototypeShift = Array.prototype.shift;
+var ArrayPrototypeUnshift = Array.prototype.unshift;
+var RegExpPrototypeExec = RegExp.prototype.exec;
+var RegExpPrototypeSymbolReplace = RegExp.prototype[SymbolReplace];
+var StringFromCharCode = String.fromCharCode;
+var StringPrototypeCharCodeAt = String.prototype.charCodeAt;
+var StringPrototypeCodePointAt = String.prototype.codePointAt;
+var StringPrototypeSlice = String.prototype.slice;
+var StringPrototypeToLowerCase = String.prototype.toLowerCase;
+var StringPrototypeEndsWith = String.prototype.endsWith;
+var StringPrototypeRepeat = String.prototype.repeat;
+var StringPrototypeStartsWith = String.prototype.startsWith;
+var StringPrototypeTrim = String.prototype.trim;
+var StringPrototypeNormalize = String.prototype.normalize;
+var NumberIsNaN = Number.isNaN;
+var NumberIsFinite = Number.isFinite;
+var NumberIsInteger = Number.isInteger;
+var NumberMAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER;
+var NumberMIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER;
+var MathCeil = Math.ceil;
+var MathFloor = Math.floor;
+var MathMax = Math.max;
+var MathMaxApply = Math.max.apply;
+var DateNow = Date.now;
+var FunctionPrototype = Function.prototype;
+var StringPrototype = String.prototype;
+var StringPrototypeSymbolIterator = StringPrototype[SymbolIterator];
+var StringIteratorPrototypeNext = StringPrototypeSymbolIterator.call("").next;
+var ObjectSetPrototypeOf = Object.setPrototypeOf;
+var ObjectDefineProperty = Object.defineProperty;
+var ObjectDefineProperties = Object.defineProperties;
+var ObjectFreeze = Object.freeze;
+var ObjectAssign = Object.assign;
+var ObjectCreate = Object.create;
+var ObjectKeys = Object.keys;
+var ObjectSeal = Object.seal;
+
+var createSafeIterator = (factory, next) => {
+ class SafeIterator {
+ #iterator;
+ constructor(iterable) {
+ this.#iterator = factory.call(iterable);
+ }
+ next() {
+ return next.call(this.#iterator);
+ }
+ [SymbolIterator]() {
+ return this;
+ }
+ }
+ ObjectSetPrototypeOf(SafeIterator.prototype, null);
+ ObjectFreeze(SafeIterator.prototype);
+ ObjectFreeze(SafeIterator);
+ return SafeIterator;
+};
+
+var SafeStringIterator = createSafeIterator(
+ StringPrototypeSymbolIterator,
+ StringIteratorPrototypeNext,
+);
+
+// ----------------------------------------------------------------------------
+// Section: "Internal" modules
+// ----------------------------------------------------------------------------
+
+/**
+ * Returns true if the character represented by a given
+ * Unicode code point is full-width. Otherwise returns false.
+ */
+var isFullWidthCodePoint = (code) => {
+ // Code points are partially derived from:
+ // https://www.unicode.org/Public/UNIDATA/EastAsianWidth.txt
+ return (
+ code >= 0x1100 &&
+ (code <= 0x115f || // Hangul Jamo
+ code === 0x2329 || // LEFT-POINTING ANGLE BRACKET
+ code === 0x232a || // RIGHT-POINTING ANGLE BRACKET
+ // CJK Radicals Supplement .. Enclosed CJK Letters and Months
+ (code >= 0x2e80 && code <= 0x3247 && code !== 0x303f) ||
+ // Enclosed CJK Letters and Months .. CJK Unified Ideographs Extension A
+ (code >= 0x3250 && code <= 0x4dbf) ||
+ // CJK Unified Ideographs .. Yi Radicals
+ (code >= 0x4e00 && code <= 0xa4c6) ||
+ // Hangul Jamo Extended-A
+ (code >= 0xa960 && code <= 0xa97c) ||
+ // Hangul Syllables
+ (code >= 0xac00 && code <= 0xd7a3) ||
+ // CJK Compatibility Ideographs
+ (code >= 0xf900 && code <= 0xfaff) ||
+ // Vertical Forms
+ (code >= 0xfe10 && code <= 0xfe19) ||
+ // CJK Compatibility Forms .. Small Form Variants
+ (code >= 0xfe30 && code <= 0xfe6b) ||
+ // Halfwidth and Fullwidth Forms
+ (code >= 0xff01 && code <= 0xff60) ||
+ (code >= 0xffe0 && code <= 0xffe6) ||
+ // Kana Supplement
+ (code >= 0x1b000 && code <= 0x1b001) ||
+ // Enclosed Ideographic Supplement
+ (code >= 0x1f200 && code <= 0x1f251) ||
+ // Miscellaneous Symbols and Pictographs 0x1f300 - 0x1f5ff
+ // Emoticons 0x1f600 - 0x1f64f
+ (code >= 0x1f300 && code <= 0x1f64f) ||
+ // CJK Unified Ideographs Extension B .. Tertiary Ideographic Plane
+ (code >= 0x20000 && code <= 0x3fffd))
+ );
+};
+
+var isZeroWidthCodePoint = (code) => {
+ return (
+ code <= 0x1f || // C0 control codes
+ (code >= 0x7f && code <= 0x9f) || // C1 control codes
+ (code >= 0x300 && code <= 0x36f) || // Combining Diacritical Marks
+ (code >= 0x200b && code <= 0x200f) || // Modifying Invisible Characters
+ // Combining Diacritical Marks for Symbols
+ (code >= 0x20d0 && code <= 0x20ff) ||
+ (code >= 0xfe00 && code <= 0xfe0f) || // Variation Selectors
+ (code >= 0xfe20 && code <= 0xfe2f) || // Combining Half Marks
+ (code >= 0xe0100 && code <= 0xe01ef)
+ ); // Variation Selectors
+};
+
+/**
+ * Returns the number of columns required to display the given string.
+ */
+var getStringWidth = function getStringWidth(str, removeControlChars = true) {
+ var width = 0;
+
+ if (removeControlChars) str = stripVTControlCharacters(str);
+ str = StringPrototypeNormalize.call(str, "NFC");
+ for (var char of new SafeStringIterator(str)) {
+ var code = StringPrototypeCodePointAt.call(char, 0);
+ if (isFullWidthCodePoint(code)) {
+ width += 2;
+ } else if (!isZeroWidthCodePoint(code)) {
+ width++;
+ }
+ }
+
+ return width;
+};
+
+// Regex used for ansi escape code splitting
+// Adopted from https://github.com/chalk/ansi-regex/blob/HEAD/index.js
+// License: MIT, authors: @sindresorhus, Qix-, arjunmehta and LitoMore
+// Matches all ansi escape code sequences in a string
+var ansiPattern =
+ "[\\u001B\\u009B][[\\]()#;?]*" +
+ "(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*" +
+ "|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)" +
+ "|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))";
+var ansi = new RegExp(ansiPattern, "g");
+
+/**
+ * Remove all VT control characters. Use to estimate displayed string width.
+ */
+function stripVTControlCharacters(str) {
+ validateString(str, "str");
+ return RegExpPrototypeSymbolReplace.call(ansi, str, "");
+}
+
+// Promisify
+
+var kCustomPromisifiedSymbol = SymbolFor("nodejs.util.promisify.custom");
+var kCustomPromisifyArgsSymbol = Symbol("customPromisifyArgs");
+
+function promisify(original) {
+ validateFunction(original, "original");
+
+ if (original[kCustomPromisifiedSymbol]) {
+ var fn = original[kCustomPromisifiedSymbol];
+
+ validateFunction(fn, "util.promisify.custom");
+
+ return ObjectDefineProperty(fn, kCustomPromisifiedSymbol, {
+ __proto__: null,
+ value: fn,
+ enumerable: false,
+ writable: false,
+ configurable: true,
+ });
+ }
+
+ // Names to create an object from in case the callback receives multiple
+ // arguments, e.g. ['bytesRead', 'buffer'] for fs.read.
+ var argumentNames = original[kCustomPromisifyArgsSymbol];
+
+ function fn(...args) {
+ return new Promise((resolve, reject) => {
+ ArrayPrototypePush.call(args, (err, ...values) => {
+ if (err) {
+ return reject(err);
+ }
+ if (argumentNames !== undefined && values.length > 1) {
+ var obj = {};
+ for (var i = 0; i < argumentNames.length; i++)
+ obj[argumentNames[i]] = values[i];
+ resolve(obj);
+ } else {
+ resolve(values[0]);
+ }
+ });
+ ReflectApply(original, this, args);
+ });
+ }
+
+ ObjectSetPrototypeOf(fn, ObjectGetPrototypeOf(original));
+
+ ObjectDefineProperty(fn, kCustomPromisifiedSymbol, {
+ __proto__: null,
+ value: fn,
+ enumerable: false,
+ writable: false,
+ configurable: true,
+ });
+
+ var descriptors = ObjectGetOwnPropertyDescriptors(original);
+ var propertiesValues = ObjectValues(descriptors);
+ for (var i = 0; i < propertiesValues.length; i++) {
+ // We want to use null-prototype objects to not rely on globally mutable
+ // %Object.prototype%.
+ ObjectSetPrototypeOf(propertiesValues[i], null);
+ }
+ return ObjectDefineProperties(fn, descriptors);
+}
+
+promisify.custom = kCustomPromisifiedSymbol;
+
+// Constants
+
+var kUTF16SurrogateThreshold = 0x10000; // 2 ** 16
+var kEscape = "\x1b";
+var kSubstringSearch = Symbol("kSubstringSearch");
+
+var kIsNodeError = Symbol("kIsNodeError");
+
+// Errors
+var errorBases = {};
+var VALID_NODE_ERROR_BASES = {
+ TypeError,
+ RangeError,
+ Error,
+};
+
+function getNodeErrorByName(typeName) {
+ var base = errorBases[typeName];
+ if (base) {
+ return base;
+ }
+ if (!ObjectKeys(VALID_NODE_ERROR_BASES).includes(typeName)) {
+ throw new Error("Invalid NodeError type");
+ }
+
+ var Base = VALID_NODE_ERROR_BASES[typeName];
+
+ class NodeError extends Base {
+ [kIsNodeError] = true;
+ code;
+ constructor(msg, opts) {
+ super(msg, opts);
+ this.code = opts?.code || "ERR_GENERIC";
+ }
+
+ toString() {
+ return `${this.name} [${this.code}]: ${this.message}`;
+ }
+ }
+ errorBases[typeName] = NodeError;
+ return NodeError;
+}
+
+var NodeError = getNodeErrorByName("Error");
+var NodeTypeError = getNodeErrorByName("TypeError");
+var NodeRangeError = getNodeErrorByName("RangeError");
+
+class ERR_INVALID_ARG_TYPE extends NodeTypeError {
+ constructor(name, type, value) {
+ super(
+ `The "${name}" argument must be of type ${type}. Received type ${typeof value}`,
+ { code: "ERR_INVALID_ARG_TYPE" },
+ );
+ }
+}
+
+class ERR_INVALID_ARG_VALUE extends NodeTypeError {
+ constructor(name, value, reason = "not specified") {
+ super(
+ `The value "${String(
+ value,
+ )}" is invalid for argument '${name}'. Reason: ${reason}`,
+ { code: "ERR_INVALID_ARG_VALUE" },
+ );
+ }
+}
+
+class ERR_INVALID_CURSOR_POS extends NodeTypeError {
+ constructor() {
+ super("Cannot set cursor row without setting its column", {
+ code: "ERR_INVALID_CURSOR_POS",
+ });
+ }
+}
+
+class ERR_OUT_OF_RANGE extends NodeRangeError {
+ constructor(name, range, received) {
+ super(
+ `The value of "${name}" is out of range. It must be ${range}. Received ${received}`,
+ { code: "ERR_OUT_OF_RANGE" },
+ );
+ }
+}
+
+class ERR_USE_AFTER_CLOSE extends NodeError {
+ constructor() {
+ super("This socket has been ended by the other party", {
+ code: "ERR_USE_AFTER_CLOSE",
+ });
+ }
+}
+
+// Validators
+
+/**
+ * @callback validateFunction
+ * @param {*} value
+ * @param {string} name
+ * @returns {asserts value is Function}
+ */
+function validateFunction(value, name) {
+ if (typeof value !== "function")
+ throw new ERR_INVALID_ARG_TYPE(name, "Function", value);
+}
+
+/**
+ * @callback validateAbortSignal
+ * @param {*} signal
+ * @param {string} name
+ */
+function validateAbortSignal(signal, name) {
+ if (
+ signal !== undefined &&
+ (signal === null || typeof signal !== "object" || !("aborted" in signal))
+ ) {
+ throw new ERR_INVALID_ARG_TYPE(name, "AbortSignal", signal);
+ }
+}
+
+/**
+ * @callback validateArray
+ * @param {*} value
+ * @param {string} name
+ * @param {number} [minLength]
+ * @returns {asserts value is any[]}
+ */
+function validateArray(value, name, minLength = 0) {
+ // var validateArray = hideStackFrames((value, name, minLength = 0) => {
+ if (!ArrayIsArray(value)) {
+ throw new ERR_INVALID_ARG_TYPE(name, "Array", value);
+ }
+ if (value.length < minLength) {
+ var reason = `must be longer than ${minLength}`;
+ throw new ERR_INVALID_ARG_VALUE(name, value, reason);
+ }
+}
+
+/**
+ * @callback validateString
+ * @param {*} value
+ * @param {string} name
+ * @returns {asserts value is string}
+ */
+function validateString(value, name) {
+ if (typeof value !== "string")
+ throw new ERR_INVALID_ARG_TYPE(name, "string", value);
+}
+
+/**
+ * @callback validateBoolean
+ * @param {*} value
+ * @param {string} name
+ * @returns {asserts value is boolean}
+ */
+function validateBoolean(value, name) {
+ if (typeof value !== "boolean")
+ throw new ERR_INVALID_ARG_TYPE(name, "boolean", value);
+}
+
+/**
+ * @callback validateObject
+ * @param {*} value
+ * @param {string} name
+ * @param {{
+ * allowArray?: boolean,
+ * allowFunction?: boolean,
+ * nullable?: boolean
+ * }} [options]
+ */
+function validateObject(value, name, options = null) {
+ // var validateObject = hideStackFrames((value, name, options = null) => {
+ var allowArray = options?.allowArray ?? false;
+ var allowFunction = options?.allowFunction ?? false;
+ var nullable = options?.nullable ?? false;
+ if (
+ (!nullable && value === null) ||
+ (!allowArray && ArrayIsArray.call(value)) ||
+ (typeof value !== "object" &&
+ (!allowFunction || typeof value !== "function"))
+ ) {
+ throw new ERR_INVALID_ARG_TYPE(name, "object", value);
+ }
+}
+
+/**
+ * @callback validateInteger
+ * @param {*} value
+ * @param {string} name
+ * @param {number} [min]
+ * @param {number} [max]
+ * @returns {asserts value is number}
+ */
+function validateInteger(
+ value,
+ name,
+ min = NumberMIN_SAFE_INTEGER,
+ max = NumberMAX_SAFE_INTEGER,
+) {
+ if (typeof value !== "number")
+ throw new ERR_INVALID_ARG_TYPE(name, "number", value);
+ if (!NumberIsInteger(value))
+ throw new ERR_OUT_OF_RANGE(name, "an integer", value);
+ if (value < min || value > max)
+ throw new ERR_OUT_OF_RANGE(name, `>= ${min} && <= ${max}`, value);
+}
+
+/**
+ * @callback validateUint32
+ * @param {*} value
+ * @param {string} name
+ * @param {number|boolean} [positive=false]
+ * @returns {asserts value is number}
+ */
+function validateUint32(value, name, positive = false) {
+ if (typeof value !== "number") {
+ throw new ERR_INVALID_ARG_TYPE(name, "number", value);
+ }
+
+ if (!NumberIsInteger(value)) {
+ throw new ERR_OUT_OF_RANGE(name, "an integer", value);
+ }
+
+ var min = positive ? 1 : 0; // 2 ** 32 === 4294967296
+ var max = 4_294_967_295;
+
+ if (value < min || value > max) {
+ throw new ERR_OUT_OF_RANGE(name, `>= ${min} && <= ${max}`, value);
+ }
+}
+
+// ----------------------------------------------------------------------------
+// Section: Utils
+// ----------------------------------------------------------------------------
+
+function CSI(strings, ...args) {
+ var ret = `${kEscape}[`;
+ for (var n = 0; n < strings.length; n++) {
+ ret += strings[n];
+ if (n < args.length) ret += args[n];
+ }
+ return ret;
+}
+
+var kClearLine, kClearScreenDown, kClearToLineBeginning, kClearToLineEnd;
+
+CSI.kEscape = kEscape;
+CSI.kClearLine = kClearLine = CSI`2K`;
+CSI.kClearScreenDown = kClearScreenDown = CSI`0J`;
+CSI.kClearToLineBeginning = kClearToLineBeginning = CSI`1K`;
+CSI.kClearToLineEnd = kClearToLineEnd = CSI`0K`;
+
+function charLengthLeft(str, i) {
+ if (i <= 0) return 0;
+ if (
+ (i > 1 &&
+ StringPrototypeCodePointAt.call(str, i - 2) >=
+ kUTF16SurrogateThreshold) ||
+ StringPrototypeCodePointAt.call(str, i - 1) >= kUTF16SurrogateThreshold
+ ) {
+ return 2;
+ }
+ return 1;
+}
+
+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 StringPrototypeCodePointAt.call(str, 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
+*/
+function* emitKeys(stream) {
+ while (true) {
+ var ch = yield;
+ var s = ch;
+ var escaped = false;
+
+ var keySeq = null;
+ var keyName;
+ var keyCtrl = false;
+ var keyMeta = false;
+ var keyShift = false;
+
+ // var 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
+ var code = ch;
+ var 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]$/
+ *
+ */
+ var 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
+ */
+ var cmd = StringPrototypeSlice.call(s, cmdStart);
+ var match;
+
+ if (
+ (match = RegExpPrototypeExec.call(/^(\d\d?)(;(\d))?([~^$])$/, cmd))
+ ) {
+ code += match[1] + match[4];
+ modifier = (match[3] || 1) - 1;
+ } else if (
+ (match = RegExpPrototypeExec.call(/^((\d;)?(\d))?([A-Za-z])$/, cmd))
+ ) {
+ code += match[4];
+ modifier = (match[3] || 1) - 1;
+ } else {
+ code += cmd;
+ }
+ }
+
+ // Parse the key modifier
+ keyCtrl = !!(modifier & 4);
+ keyMeta = !!(modifier & 10);
+ keyShift = !!(modifier & 1);
+ keyCode = code;
+
+ // Parse the key itself
+ switch (code) {
+ /* xterm/gnome ESC [ letter (with modifier) */
+ case "[P":
+ keyName = "f1";
+ break;
+ case "[Q":
+ keyName = "f2";
+ break;
+ case "[R":
+ keyName = "f3";
+ break;
+ case "[S":
+ keyName = "f4";
+ break;
+
+ /* xterm/gnome ESC O letter (without modifier) */
+ case "OP":
+ keyName = "f1";
+ break;
+ case "OQ":
+ keyName = "f2";
+ break;
+ case "OR":
+ keyName = "f3";
+ break;
+ case "OS":
+ keyName = "f4";
+ break;
+
+ /* xterm/rxvt ESC [ number ~ */
+ case "[11~":
+ keyName = "f1";
+ break;
+ case "[12~":
+ keyName = "f2";
+ break;
+ case "[13~":
+ keyName = "f3";
+ break;
+ case "[14~":
+ keyName = "f4";
+ break;
+
+ /* from Cygwin and used in libuv */
+ case "[[A":
+ keyName = "f1";
+ break;
+ case "[[B":
+ keyName = "f2";
+ break;
+ case "[[C":
+ keyName = "f3";
+ break;
+ case "[[D":
+ keyName = "f4";
+ break;
+ case "[[E":
+ keyName = "f5";
+ break;
+
+ /* common */
+ case "[15~":
+ keyName = "f5";
+ break;
+ case "[17~":
+ keyName = "f6";
+ break;
+ case "[18~":
+ keyName = "f7";
+ break;
+ case "[19~":
+ keyName = "f8";
+ break;
+ case "[20~":
+ keyName = "f9";
+ break;
+ case "[21~":
+ keyName = "f10";
+ break;
+ case "[23~":
+ keyName = "f11";
+ break;
+ case "[24~":
+ keyName = "f12";
+ break;
+
+ /* xterm ESC [ letter */
+ case "[A":
+ keyName = "up";
+ break;
+ case "[B":
+ keyName = "down";
+ break;
+ case "[C":
+ keyName = "right";
+ break;
+ case "[D":
+ keyName = "left";
+ break;
+ case "[E":
+ keyName = "clear";
+ break;
+ case "[F":
+ keyName = "end";
+ break;
+ case "[H":
+ keyName = "home";
+ break;
+
+ /* xterm/gnome ESC O letter */
+ case "OA":
+ keyName = "up";
+ break;
+ case "OB":
+ keyName = "down";
+ break;
+ case "OC":
+ keyName = "right";
+ break;
+ case "OD":
+ keyName = "left";
+ break;
+ case "OE":
+ keyName = "clear";
+ break;
+ case "OF":
+ keyName = "end";
+ break;
+ case "OH":
+ keyName = "home";
+ break;
+
+ /* xterm/rxvt ESC [ number ~ */
+ case "[1~":
+ keyName = "home";
+ break;
+ case "[2~":
+ keyName = "insert";
+ break;
+ case "[3~":
+ keyName = "delete";
+ break;
+ case "[4~":
+ keyName = "end";
+ break;
+ case "[5~":
+ keyName = "pageup";
+ break;
+ case "[6~":
+ keyName = "pagedown";
+ break;
+
+ /* putty */
+ case "[[5~":
+ keyName = "pageup";
+ break;
+ case "[[6~":
+ keyName = "pagedown";
+ break;
+
+ /* rxvt */
+ case "[7~":
+ keyName = "home";
+ break;
+ case "[8~":
+ keyName = "end";
+ break;
+
+ /* rxvt keys with modifiers */
+ case "[a":
+ keyName = "up";
+ keyShift = true;
+ break;
+ case "[b":
+ keyName = "down";
+ keyShift = true;
+ break;
+ case "[c":
+ keyName = "right";
+ keyShift = true;
+ break;
+ case "[d":
+ keyName = "left";
+ keyShift = true;
+ break;
+ case "[e":
+ keyName = "clear";
+ keyShift = true;
+ break;
+
+ case "[2$":
+ keyName = "insert";
+ keyShift = true;
+ break;
+ case "[3$":
+ keyName = "delete";
+ keyShift = true;
+ break;
+ case "[5$":
+ keyName = "pageup";
+ keyShift = true;
+ break;
+ case "[6$":
+ keyName = "pagedown";
+ keyShift = true;
+ break;
+ case "[7$":
+ keyName = "home";
+ keyShift = true;
+ break;
+ case "[8$":
+ keyName = "end";
+ keyShift = true;
+ break;
+
+ case "Oa":
+ keyName = "up";
+ keyCtrl = true;
+ break;
+ case "Ob":
+ keyName = "down";
+ keyCtrl = true;
+ break;
+ case "Oc":
+ keyName = "right";
+ keyCtrl = true;
+ break;
+ case "Od":
+ keyName = "left";
+ keyCtrl = true;
+ break;
+ case "Oe":
+ keyName = "clear";
+ keyCtrl = true;
+ break;
+
+ case "[2^":
+ keyName = "insert";
+ keyCtrl = true;
+ break;
+ case "[3^":
+ keyName = "delete";
+ keyCtrl = true;
+ break;
+ case "[5^":
+ keyName = "pageup";
+ keyCtrl = true;
+ break;
+ case "[6^":
+ keyName = "pagedown";
+ keyCtrl = true;
+ break;
+ case "[7^":
+ keyName = "home";
+ keyCtrl = true;
+ break;
+ case "[8^":
+ keyName = "end";
+ keyCtrl = true;
+ break;
+
+ /* misc. */
+ case "[Z":
+ keyName = "tab";
+ keyShift = true;
+ break;
+ default:
+ keyName = "undefined";
+ break;
+ }
+ } else if (ch === "\r") {
+ // carriage return
+ keyName = "return";
+ keyMeta = escaped;
+ } else if (ch === "\n") {
+ // Enter, should have been called linefeed
+ keyName = "enter";
+ keyMeta = escaped;
+ } else if (ch === "\t") {
+ // tab
+ keyName = "tab";
+ keyMeta = escaped;
+ } else if (ch === "\b" || ch === "\x7f") {
+ // backspace or ctrl+h
+ keyName = "backspace";
+ keyMeta = escaped;
+ } else if (ch === kEscape) {
+ // escape key
+ keyName = "escape";
+ keyMeta = escaped;
+ } else if (ch === " ") {
+ keyName = "space";
+ keyMeta = escaped;
+ } else if (!escaped && ch <= "\x1a") {
+ // ctrl+letter
+ keyName = StringFromCharCode(
+ StringPrototypeCharCodeAt.call(ch) +
+ StringPrototypeCharCodeAt.call("a") -
+ 1,
+ );
+ keyCtrl = true;
+ } else if (RegExpPrototypeExec.call(/^[0-9A-Za-z]$/, ch) !== null) {
+ // Letter, number, shift+letter
+ keyName = StringPrototypeToLowerCase.call(ch);
+ keyShift = RegExpPrototypeExec.call(/^[A-Z]$/, ch) !== null;
+ keyMeta = escaped;
+ } else if (escaped) {
+ // Escape sequence timeout
+ keyName = ch.length ? undefined : "escape";
+ keyMeta = true;
+ }
+
+ keySeq = s;
+
+ if (s.length !== 0 && (keyName !== undefined || escaped)) {
+ /* Named character or sequence */
+ stream.emit("keypress", escaped ? undefined : s, {
+ sequence: keySeq,
+ name: keyName,
+ ctrl: keyCtrl,
+ meta: keyMeta,
+ shift: keyShift,
+ });
+ } else if (charLengthAt(s, 0) === s.length) {
+ /* Single unnamed character, e.g. "." */
+ stream.emit("keypress", s, {
+ sequence: keySeq,
+ name: keyName,
+ ctrl: keyCtrl,
+ meta: keyMeta,
+ shift: keyShift,
+ });
+ }
+ /* Unrecognized or broken escape sequence, don't emit anything */
+ }
+}
+
+// This runs in O(n log n).
+function commonPrefix(strings) {
+ if (strings.length === 0) {
+ return "";
+ }
+ if (strings.length === 1) {
+ return strings[0];
+ }
+ var sorted = ArrayPrototypeSort.call(ArrayPrototypeSlice.call(strings));
+ var min = sorted[0];
+ var max = sorted[sorted.length - 1];
+ for (var i = 0; i < min.length; i++) {
+ if (min[i] !== max[i]) {
+ return StringPrototypeSlice.call(min, 0, i);
+ }
+ }
+ return min;
+}
+
+// ----------------------------------------------------------------------------
+// Section: Cursor Functions
+// ----------------------------------------------------------------------------
+
+/**
+ * moves the cursor to the x and y coordinate on the given stream
+ */
+
+function cursorTo(stream, x, y, callback) {
+ if (callback !== undefined) {
+ validateFunction(callback, "callback");
+ }
+
+ if (typeof y === "function") {
+ callback = y;
+ y = undefined;
+ }
+
+ if (NumberIsNaN(x)) throw new ERR_INVALID_ARG_VALUE("x", x);
+ if (NumberIsNaN(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();
+
+ var 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
+ */
+
+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;
+ }
+
+ var 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
+ */
+
+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;
+ }
+
+ var type =
+ dir < 0 ? kClearToLineBeginning : dir > 0 ? kClearToLineEnd : kClearLine;
+ return stream.write(type, callback);
+}
+
+/**
+ * clears the screen from the current position of the cursor down
+ */
+
+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);
+}
+
+// ----------------------------------------------------------------------------
+// Section: Emit keypress events
+// ----------------------------------------------------------------------------
+
+var KEYPRESS_DECODER = Symbol("keypress-decoder");
+var ESCAPE_DECODER = Symbol("escape-decoder");
+
+// GNU readline library - keyseq-timeout is 500ms (default)
+var ESCAPE_CODE_TIMEOUT = 500;
+
+/**
+ * accepts a readable Stream instance and makes it emit "keypress" events
+ */
+
+function emitKeypressEvents(stream, iface = {}) {
+ if (stream[KEYPRESS_DECODER]) return;
+
+ stream[KEYPRESS_DECODER] = new StringDecoder("utf8");
+
+ stream[ESCAPE_DECODER] = emitKeys(stream);
+ stream[ESCAPE_DECODER].next();
+
+ var triggerEscape = () => stream[ESCAPE_DECODER].next("");
+ var { escapeCodeTimeout = ESCAPE_CODE_TIMEOUT } = iface;
+ var timeoutId;
+
+ function onData(input) {
+ if (stream.listenerCount("keypress") > 0) {
+ var 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;
+
+ var length = 0;
+ for (var character of new SafeStringIterator(string)) {
+ 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);
+ }
+}
+
+// ----------------------------------------------------------------------------
+// Section: Interface
+// ----------------------------------------------------------------------------
+
+var kEmptyObject = ObjectFreeze(ObjectCreate(null));
+
+// Some constants regarding configuration of interface
+var kHistorySize = 30;
+var kMaxUndoRedoStackSize = 2048;
+var kMincrlfDelay = 100;
+// \r\n, \n, or \r followed by something other than \n
+var lineEnding = /\r?\n|\r(?!\n)/g;
+
+// Max length of the kill ring
+var kMaxLengthOfKillRing = 32;
+
+// Symbols
+
+// Public symbols
+var kLineObjectStream = Symbol("line object stream");
+var kQuestionCancel = Symbol("kQuestionCancel");
+var kQuestion = Symbol("kQuestion");
+
+// Private symbols
+var kAddHistory = Symbol("_addHistory");
+var kBeforeEdit = Symbol("_beforeEdit");
+var kDecoder = Symbol("_decoder");
+var kDeleteLeft = Symbol("_deleteLeft");
+var kDeleteLineLeft = Symbol("_deleteLineLeft");
+var kDeleteLineRight = Symbol("_deleteLineRight");
+var kDeleteRight = Symbol("_deleteRight");
+var kDeleteWordLeft = Symbol("_deleteWordLeft");
+var kDeleteWordRight = Symbol("_deleteWordRight");
+var kGetDisplayPos = Symbol("_getDisplayPos");
+var kHistoryNext = Symbol("_historyNext");
+var kHistoryPrev = Symbol("_historyPrev");
+var kInsertString = Symbol("_insertString");
+var kLine = Symbol("_line");
+var kLine_buffer = Symbol("_line_buffer");
+var kKillRing = Symbol("_killRing");
+var kKillRingCursor = Symbol("_killRingCursor");
+var kMoveCursor = Symbol("_moveCursor");
+var kNormalWrite = Symbol("_normalWrite");
+var kOldPrompt = Symbol("_oldPrompt");
+var kOnLine = Symbol("_onLine");
+var kPreviousKey = Symbol("_previousKey");
+var kPrompt = Symbol("_prompt");
+var kPushToKillRing = Symbol("_pushToKillRing");
+var kPushToUndoStack = Symbol("_pushToUndoStack");
+var kQuestionCallback = Symbol("_questionCallback");
+var kRedo = Symbol("_redo");
+var kRedoStack = Symbol("_redoStack");
+var kRefreshLine = Symbol("_refreshLine");
+var kSawKeyPress = Symbol("_sawKeyPress");
+var kSawReturnAt = Symbol("_sawReturnAt");
+var kSetRawMode = Symbol("_setRawMode");
+var kTabComplete = Symbol("_tabComplete");
+var kTabCompleter = Symbol("_tabCompleter");
+var kTtyWrite = Symbol("_ttyWrite");
+var kUndo = Symbol("_undo");
+var kUndoStack = Symbol("_undoStack");
+var kWordLeft = Symbol("_wordLeft");
+var kWordRight = Symbol("_wordRight");
+var kWriteToOutput = Symbol("_writeToOutput");
+var kYank = Symbol("_yank");
+var kYanking = Symbol("_yanking");
+var kYankPop = Symbol("_yankPop");
+
+// Event symbols
+var kFirstEventParam = Symbol("nodejs.kFirstEventParam");
+
+// class InterfaceConstructor extends EventEmitter {
+// #onSelfCloseWithTerminal;
+// #onSelfCloseWithoutTerminal;
+
+// #onError;
+// #onData;
+// #onEnd;
+// #onTermEnd;
+// #onKeyPress;
+// #onResize;
+
+// [kSawReturnAt];
+// isCompletionEnabled = true;
+// [kSawKeyPress];
+// [kPreviousKey];
+// escapeCodeTimeout;
+// tabSize;
+
+// line;
+// [kSubstringSearch];
+// output;
+// input;
+// [kUndoStack];
+// [kRedoStack];
+// history;
+// historySize;
+
+// [kKillRing];
+// [kKillRingCursor];
+
+// removeHistoryDuplicates;
+// crlfDelay;
+// completer;
+
+// terminal;
+// [kLineObjectStream];
+
+// cursor;
+// historyIndex;
+
+// constructor(input, output, completer, terminal) {
+// super();
+
+var kOnSelfCloseWithTerminal = Symbol("_onSelfCloseWithTerminal");
+var kOnSelfCloseWithoutTerminal = Symbol("_onSelfCloseWithoutTerminal");
+var kOnKeyPress = Symbol("_onKeyPress");
+var kOnError = Symbol("_onError");
+var kOnData = Symbol("_onData");
+var kOnEnd = Symbol("_onEnd");
+var kOnTermEnd = Symbol("_onTermEnd");
+var kOnResize = Symbol("_onResize");
+
+function onSelfCloseWithTerminal() {
+ var input = this.input;
+ var output = this.output;
+
+ if (!input) throw new Error("Input not set, invalid state for readline!");
+
+ input.removeListener("keypress", this[kOnKeyPress]);
+ input.removeListener("error", this[kOnError]);
+ input.removeListener("end", this[kOnTermEnd]);
+ if (output !== null && output !== undefined) {
+ output.removeListener("resize", this[kOnResize]);
+ }
+}
+
+function onSelfCloseWithoutTerminal() {
+ var input = this.input;
+ if (!input) throw new Error("Input not set, invalid state for readline!");
+
+ input.removeListener("data", this[kOnData]);
+ input.removeListener("error", this[kOnError]);
+ input.removeListener("end", this[kOnEnd]);
+}
+
+function onError(err) {
+ this.emit("error", err);
+}
+
+function onData(data) {
+ debug("onData");
+ this[kNormalWrite](data);
+}
+
+function onEnd() {
+ debug("onEnd");
+ if (typeof this[kLine_buffer] === "string" && this[kLine_buffer].length > 0) {
+ this.emit("line", this[kLine_buffer]);
+ }
+ this.close();
+}
+
+function onTermEnd() {
+ debug("onTermEnd");
+ if (typeof this.line === "string" && this.line.length > 0) {
+ this.emit("line", this.line);
+ }
+ this.close();
+}
+
+function onKeyPress(s, key) {
+ this[kTtyWrite](s, key);
+ if (key && key.sequence) {
+ // If the keySeq is half of a surrogate pair
+ // (>= 0xd800 and <= 0xdfff), refresh the line so
+ // the character is displayed appropriately.
+ var ch = StringPrototypeCodePointAt.call(key.sequence, 0);
+ if (ch >= 0xd800 && ch <= 0xdfff) this[kRefreshLine]();
+ }
+}
+
+function onResize() {
+ this[kRefreshLine]();
+}
+
+function InterfaceConstructor(input, output, completer, terminal) {
+ if (!(this instanceof InterfaceConstructor)) {
+ return new InterfaceConstructor(input, output, completer, terminal);
+ }
+
+ EventEmitter.call(this);
+
+ this[kOnSelfCloseWithoutTerminal] = onSelfCloseWithoutTerminal.bind(this);
+ this[kOnSelfCloseWithTerminal] = onSelfCloseWithTerminal.bind(this);
+
+ this[kOnError] = onError.bind(this);
+ this[kOnData] = onData.bind(this);
+ this[kOnEnd] = onEnd.bind(this);
+ this[kOnTermEnd] = onTermEnd.bind(this);
+ this[kOnKeyPress] = onKeyPress.bind(this);
+ this[kOnResize] = onResize.bind(this);
+
+ this[kSawReturnAt] = 0;
+ this.isCompletionEnabled = true;
+ this[kSawKeyPress] = false;
+ this[kPreviousKey] = null;
+ this.escapeCodeTimeout = ESCAPE_CODE_TIMEOUT;
+ this.tabSize = 8;
+
+ var history;
+ var historySize;
+ var removeHistoryDuplicates = false;
+ var crlfDelay;
+ var prompt = "> ";
+ var 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;
+
+ var tabSize = input.tabSize;
+ if (tabSize !== undefined) {
+ validateUint32(tabSize, "tabSize", true);
+ this.tabSize = tabSize;
+ }
+ removeHistoryDuplicates = input.removeHistoryDuplicates;
+
+ var inputPrompt = input.prompt;
+ if (inputPrompt !== undefined) {
+ prompt = inputPrompt;
+ }
+
+ var inputEscapeCodeTimeout = input.escapeCodeTimeout;
+ if (inputEscapeCodeTimeout !== undefined) {
+ if (NumberIsFinite(inputEscapeCodeTimeout)) {
+ this.escapeCodeTimeout = inputEscapeCodeTimeout;
+ } 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" ||
+ NumberIsNaN(historySize) ||
+ historySize < 0
+ ) {
+ throw new ERR_INVALID_ARG_VALUE("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;
+ }
+
+ this.line = "";
+ this[kSubstringSearch] = null;
+ this.output = output;
+ this.input = input;
+ this[kUndoStack] = [];
+ this[kRedoStack] = [];
+ this.history = history;
+ this.historySize = historySize;
+
+ // The kill ring is a global list of blocks of text that were previously
+ // killed (deleted). If its size exceeds kMaxLengthOfKillRing, the oldest
+ // element will be removed to make room for the latest deletion. With kill
+ // ring, users are able to recall (yank) or cycle (yank pop) among previously
+ // killed texts, quite similar to the behavior of Emacs.
+ this[kKillRing] = [];
+ this[kKillRingCursor] = 0;
+
+ this.removeHistoryDuplicates = !!removeHistoryDuplicates;
+ this.crlfDelay = crlfDelay
+ ? MathMax(kMincrlfDelay, crlfDelay)
+ : kMincrlfDelay;
+ this.completer = completer;
+
+ this.setPrompt(prompt);
+
+ this.terminal = !!terminal;
+
+ this[kLineObjectStream] = undefined;
+
+ input.on("error", this[kOnError]);
+
+ if (!this.terminal) {
+ input.on("data", this[kOnData]);
+ input.on("end", this[kOnEnd]);
+ this.once("close", this[kOnSelfCloseWithoutTerminal]);
+ this[kDecoder] = new StringDecoder("utf8");
+ } else {
+ emitKeypressEvents(input, this);
+
+ // `input` usually refers to stdin
+ input.on("keypress", this[kOnKeyPress]);
+ input.on("end", this[kOnTermEnd]);
+
+ 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", this[kOnResize]);
+
+ this.once("close", this[kOnSelfCloseWithTerminal]);
+ }
+
+ if (signal) {
+ var onAborted = (() => this.close()).bind(this);
+ if (signal.aborted) {
+ process.nextTick(onAborted);
+ } else {
+ signal.addEventListener("abort", onAborted, { once: true });
+ this.once("close", () => signal.removeEventListener("abort", onAborted));
+ }
+ }
+
+ // Current line
+ this.line = "";
+
+ input.resume();
+}
+
+ObjectSetPrototypeOf(InterfaceConstructor.prototype, EventEmitter.prototype);
+ObjectSetPrototypeOf(InterfaceConstructor, EventEmitter);
+
+var _Interface = class Interface extends InterfaceConstructor {
+ // TODO: Enumerate all the properties of the class
+
+ // eslint-disable-next-line no-useless-constructor
+ constructor(input, output, completer, terminal) {
+ super(input, output, completer, terminal);
+ }
+ get columns() {
+ var output = this.output;
+ if (output && output.columns) return 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) {
+ var input = this.input;
+ var { setRawMode, wasInRawMode } = input;
+
+ // TODO: Make this work, for now just stub this and print debug
+ debug("setRawMode", mode, "set!");
+ // if (typeof setRawMode === "function") {
+ // 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]) {
+ var cb = this[kQuestionCallback];
+ this[kQuestionCallback] = null;
+ this.setPrompt(this[kOldPrompt]);
+ cb(line);
+ } else {
+ this.emit("line", line);
+ }
+ }
+
+ [kBeforeEdit](oldText, oldCursor) {
+ this[kPushToUndoStack](oldText, oldCursor);
+ }
+
+ [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 (StringPrototypeTrim.call(this.line).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
+ var dupIndex = ArrayPrototypeIndexOf.call(this.history, this.line);
+ if (dupIndex !== -1)
+ ArrayPrototypeSplice.call(this.history, dupIndex, 1);
+ }
+
+ ArrayPrototypeUnshift.call(this.history, this.line);
+
+ // Only store so many
+ if (this.history.length > this.historySize)
+ ArrayPrototypePop.call(this.history);
+ }
+
+ 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
+ var line = this.history[0];
+
+ // Emit history event to notify listeners of update
+ this.emit("history", this.history);
+
+ return line;
+ }
+
+ [kRefreshLine]() {
+ // line length
+ var line = this[kPrompt] + this.line;
+ var dispPos = this[kGetDisplayPos](line);
+ var lineCols = dispPos.cols;
+ var lineRows = dispPos.rows;
+
+ // cursor position
+ var cursorPos = this.getCursorPos();
+
+ // First move to the bottom of the current line, based on cursor pos
+ var 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);
+
+ var 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;
+ }
+ var string = this[kDecoder].write(b);
+ if (
+ this[kSawReturnAt] &&
+ DateNow() - this[kSawReturnAt] <= this.crlfDelay
+ ) {
+ if (StringPrototypeCodePointAt.call(string) === 10)
+ string = StringPrototypeSlice.call(string, 1);
+ this[kSawReturnAt] = 0;
+ }
+
+ // Run test() on the new string chunk, not on the entire line buffer.
+ var newPartContainsEnding = RegExpPrototypeExec.call(lineEnding, string);
+ if (newPartContainsEnding !== null) {
+ if (this[kLine_buffer]) {
+ string = this[kLine_buffer] + string;
+ this[kLine_buffer] = null;
+ newPartContainsEnding = RegExpPrototypeExec.call(lineEnding, string);
+ }
+ this[kSawReturnAt] = StringPrototypeEndsWith.call(string, "\r")
+ ? DateNow()
+ : 0;
+
+ var indexes = [0, newPartContainsEnding.index, lineEnding.lastIndex];
+ var nextMatch;
+ while (
+ (nextMatch = RegExpPrototypeExec.call(lineEnding, string)) !== null
+ ) {
+ ArrayPrototypePush.call(indexes, nextMatch.index, lineEnding.lastIndex);
+ }
+ var lastIndex = indexes.length - 1;
+ // Either '' or (conceivably) the unfinished portion of the next line
+ this[kLine_buffer] = StringPrototypeSlice.call(
+ string,
+ indexes[lastIndex],
+ );
+ for (var i = 1; i < lastIndex; i += 2) {
+ this[kOnLine](
+ StringPrototypeSlice.call(string, indexes[i - 1], indexes[i]),
+ );
+ }
+ } else if (string) {
+ // No newlines this time, save what we have for next time
+ if (this[kLine_buffer]) {
+ this[kLine_buffer] += string;
+ } else {
+ this[kLine_buffer] = string;
+ }
+ }
+ }
+
+ [kInsertString](c) {
+ this[kBeforeEdit](this.line, this.cursor);
+ if (this.cursor < this.line.length) {
+ var beg = StringPrototypeSlice.call(this.line, 0, this.cursor);
+ var end = StringPrototypeSlice.call(
+ this.line,
+ this.cursor,
+ this.line.length,
+ );
+ this.line = beg + c + end;
+ this.cursor += c.length;
+ this[kRefreshLine]();
+ } else {
+ var oldPos = this.getCursorPos();
+ this.line += c;
+ this.cursor += c.length;
+ var newPos = this.getCursorPos();
+
+ if (oldPos.rows < newPos.rows) {
+ this[kRefreshLine]();
+ } else {
+ this[kWriteToOutput](c);
+ }
+ }
+ }
+
+ async [kTabComplete](lastKeypressWasTab) {
+ this.pause();
+ var string = StringPrototypeSlice.call(this.line, 0, this.cursor);
+ var value;
+ try {
+ value = await this.completer(string);
+ } catch (err) {
+ this[kWriteToOutput](`Tab completion error: ${inspect(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.
+ var prefix = commonPrefix(
+ ArrayPrototypeFilter.call(completions, (e) => e !== ""),
+ );
+ if (
+ StringPrototypeStartsWith.call(prefix, completeOn) &&
+ prefix.length > completeOn.length
+ ) {
+ this[kInsertString](StringPrototypeSlice.call(prefix, completeOn.length));
+ return;
+ } else if (!StringPrototypeStartsWith.call(completeOn, prefix)) {
+ this.line =
+ StringPrototypeSlice.call(
+ this.line,
+ 0,
+ this.cursor - completeOn.length,
+ ) +
+ prefix +
+ StringPrototypeSlice.call(this.line, this.cursor, this.line.length);
+ this.cursor = this.cursor - completeOn.length + prefix.length;
+ this._refreshLine();
+ return;
+ }
+
+ if (!lastKeypressWasTab) {
+ return;
+ }
+
+ this[kBeforeEdit](this.line, this.cursor);
+
+ // Apply/show completions.
+ var completionsWidth = ArrayPrototypeMap.call(completions, (e) =>
+ getStringWidth(e),
+ );
+ var width = MathMaxApply(completionsWidth) + 2; // 2 space padding
+ var maxColumns = MathFloor(this.columns / width) || 1;
+ if (maxColumns === Infinity) {
+ maxColumns = 1;
+ }
+ var output = "\r\n";
+ var lineIndex = 0;
+ var whitespace = 0;
+ for (var i = 0; i < completions.length; i++) {
+ var completion = completions[i];
+ if (completion === "" || lineIndex === maxColumns) {
+ output += "\r\n";
+ lineIndex = 0;
+ whitespace = 0;
+ } else {
+ output += StringPrototypeRepeat.call(" ", 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
+ var leading = StringPrototypeSlice.call(this.line, 0, this.cursor);
+ var reversed = ArrayPrototypeJoin.call(
+ ArrayPrototypeReverse.call(ArrayFrom(leading)),
+ "",
+ );
+ var match = RegExpPrototypeExec.call(/^\s*(?:[^\w\s]+|\w+)?/, reversed);
+ this[kMoveCursor](-match[0].length);
+ }
+ }
+
+ [kWordRight]() {
+ if (this.cursor < this.line.length) {
+ var trailing = StringPrototypeSlice.call(this.line, this.cursor);
+ var match = RegExpPrototypeExec.call(
+ /^(?:\s+|[^\w\s]+|\w+)\s*/,
+ trailing,
+ );
+ this[kMoveCursor](match[0].length);
+ }
+ }
+
+ [kDeleteLeft]() {
+ if (this.cursor > 0 && this.line.length > 0) {
+ this[kBeforeEdit](this.line, this.cursor);
+ // The number of UTF-16 units comprising the character to the left
+ var charSize = charLengthLeft(this.line, this.cursor);
+ this.line =
+ StringPrototypeSlice.call(this.line, 0, this.cursor - charSize) +
+ StringPrototypeSlice.call(this.line, this.cursor, this.line.length);
+
+ this.cursor -= charSize;
+ this[kRefreshLine]();
+ }
+ }
+
+ [kDeleteRight]() {
+ if (this.cursor < this.line.length) {
+ this[kBeforeEdit](this.line, this.cursor);
+ // The number of UTF-16 units comprising the character to the left
+ var charSize = charLengthAt(this.line, this.cursor);
+ this.line =
+ StringPrototypeSlice.call(this.line, 0, this.cursor) +
+ StringPrototypeSlice.call(
+ this.line,
+ this.cursor + charSize,
+ this.line.length,
+ );
+ this[kRefreshLine]();
+ }
+ }
+
+ [kDeleteWordLeft]() {
+ if (this.cursor > 0) {
+ this[kBeforeEdit](this.line, this.cursor);
+ // Reverse the string and match a word near beginning
+ // to avoid quadratic time complexity
+ var leading = StringPrototypeSlice.call(this.line, 0, this.cursor);
+ var reversed = ArrayPrototypeJoin.call(
+ ArrayPrototypeReverse.call(ArrayFrom(leading)),
+ "",
+ );
+ var match = RegExpPrototypeExec.call(/^\s*(?:[^\w\s]+|\w+)?/, reversed);
+ leading = StringPrototypeSlice.call(
+ leading,
+ 0,
+ leading.length - match[0].length,
+ );
+ this.line =
+ leading +
+ StringPrototypeSlice.call(this.line, this.cursor, this.line.length);
+ this.cursor = leading.length;
+ this[kRefreshLine]();
+ }
+ }
+
+ [kDeleteWordRight]() {
+ if (this.cursor < this.line.length) {
+ this[kBeforeEdit](this.line, this.cursor);
+ var trailing = StringPrototypeSlice.call(this.line, this.cursor);
+ var match = RegExpPrototypeExec.call(/^(?:\s+|\W+|\w+)\s*/, trailing);
+ this.line =
+ StringPrototypeSlice.call(this.line, 0, this.cursor) +
+ StringPrototypeSlice.call(trailing, match[0].length);
+ this[kRefreshLine]();
+ }
+ }
+
+ [kDeleteLineLeft]() {
+ this[kBeforeEdit](this.line, this.cursor);
+ var del = StringPrototypeSlice.call(this.line, 0, this.cursor);
+ this.line = StringPrototypeSlice.call(this.line, this.cursor);
+ this.cursor = 0;
+ this[kPushToKillRing](del);
+ this[kRefreshLine]();
+ }
+
+ [kDeleteLineRight]() {
+ this[kBeforeEdit](this.line, this.cursor);
+ var del = StringPrototypeSlice.call(this.line, this.cursor);
+ this.line = StringPrototypeSlice.call(this.line, 0, this.cursor);
+ this[kPushToKillRing](del);
+ this[kRefreshLine]();
+ }
+
+ [kPushToKillRing](del) {
+ if (!del || del === this[kKillRing][0]) return;
+ ArrayPrototypeUnshift.call(this[kKillRing], del);
+ this[kKillRingCursor] = 0;
+ while (this[kKillRing].length > kMaxLengthOfKillRing)
+ ArrayPrototypePop.call(this[kKillRing]);
+ }
+
+ [kYank]() {
+ if (this[kKillRing].length > 0) {
+ this[kYanking] = true;
+ this[kInsertString](this[kKillRing][this[kKillRingCursor]]);
+ }
+ }
+
+ [kYankPop]() {
+ if (!this[kYanking]) {
+ return;
+ }
+ if (this[kKillRing].length > 1) {
+ var lastYank = this[kKillRing][this[kKillRingCursor]];
+ this[kKillRingCursor]++;
+ if (this[kKillRingCursor] >= this[kKillRing].length) {
+ this[kKillRingCursor] = 0;
+ }
+ var currentYank = this[kKillRing][this[kKillRingCursor]];
+ var head = StringPrototypeSlice.call(
+ this.line,
+ 0,
+ this.cursor - lastYank.length,
+ );
+ var tail = StringPrototypeSlice.call(this.line, this.cursor);
+ this.line = head + currentYank + tail;
+ this.cursor = head.length + currentYank.length;
+ this[kRefreshLine]();
+ }
+ }
+
+ clearLine() {
+ this[kMoveCursor](+Infinity);
+ this[kWriteToOutput]("\r\n");
+ this.line = "";
+ this.cursor = 0;
+ this.prevRows = 0;
+ }
+
+ [kLine]() {
+ var line = this[kAddHistory]();
+ this[kUndoStack] = [];
+ this[kRedoStack] = [];
+ this.clearLine();
+ this[kOnLine](line);
+ }
+
+ [kPushToUndoStack](text, cursor) {
+ if (
+ ArrayPrototypePush.call(this[kUndoStack], { text, cursor }) >
+ kMaxUndoRedoStackSize
+ ) {
+ ArrayPrototypeShift.call(this[kUndoStack]);
+ }
+ }
+
+ [kUndo]() {
+ if (this[kUndoStack].length <= 0) return;
+
+ ArrayPrototypePush.call(this[kRedoStack], {
+ text: this.line,
+ cursor: this.cursor,
+ });
+
+ var entry = ArrayPrototypePop.call(this[kUndoStack]);
+ this.line = entry.text;
+ this.cursor = entry.cursor;
+
+ this[kRefreshLine]();
+ }
+
+ [kRedo]() {
+ if (this[kRedoStack].length <= 0) return;
+
+ ArrayPrototypePush.call(this[kUndoStack], {
+ text: this.line,
+ cursor: this.cursor,
+ });
+
+ var entry = ArrayPrototypePop.call(this[kRedoStack]);
+ this.line = entry.text;
+ this.cursor = entry.cursor;
+
+ this[kRefreshLine]();
+ }
+
+ [kHistoryNext]() {
+ if (this.historyIndex >= 0) {
+ this[kBeforeEdit](this.line, this.cursor);
+ var search = this[kSubstringSearch] || "";
+ var index = this.historyIndex - 1;
+ while (
+ index >= 0 &&
+ (!StringPrototypeStartsWith.call(this.history[index], 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) {
+ this[kBeforeEdit](this.line, this.cursor);
+ var search = this[kSubstringSearch] || "";
+ var index = this.historyIndex + 1;
+ while (
+ index < this.history.length &&
+ (!StringPrototypeStartsWith.call(this.history[index], 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) {
+ var offset = 0;
+ var col = this.columns;
+ var rows = 0;
+ str = stripVTControlCharacters(str);
+ for (var char of new SafeStringIterator(str)) {
+ if (char === "\n") {
+ // Rows must be incremented by 1 even if offset = 0 or col = +Infinity.
+ rows += MathCeil(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;
+ }
+ var width = getStringWidth(char, false /* stripVTControlCharacters */);
+ if (width === 0 || width === 1) {
+ offset += width;
+ } else {
+ // width === 2
+ if ((offset + 1) % col === 0) {
+ offset++;
+ }
+ offset += 2;
+ }
+ }
+ var 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() {
+ var strBeforeCursor =
+ this[kPrompt] + StringPrototypeSlice.call(this.line, 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;
+ }
+ var 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;
+ }
+
+ var newPos = this.getCursorPos();
+
+ // Check if cursor stayed on the line.
+ if (oldPos.rows === newPos.rows) {
+ var diffWidth = newPos.cols - oldPos.cols;
+ moveCursor(this.output, diffWidth, 0);
+ } else {
+ this[kRefreshLine]();
+ }
+ }
+
+ // Handle a write from the tty
+ [kTtyWrite](s, key) {
+ var previousKey = this[kPreviousKey];
+ key = key || kEmptyObject;
+ this[kPreviousKey] = key;
+ var { name: keyName, meta: keyMeta, ctrl: keyCtrl, shift: keyShift } = key;
+
+ if (!keyMeta || keyName !== "y") {
+ // Reset yanking state unless we are doing yank pop.
+ this[kYanking] = false;
+ }
+
+ // Activate or deactivate substring search.
+ if (
+ (keyName === "up" || keyName === "down") &&
+ !keyCtrl &&
+ !keyMeta &&
+ !keyShift
+ ) {
+ if (this[kSubstringSearch] === null) {
+ this[kSubstringSearch] = StringPrototypeSlice.call(
+ this.line,
+ 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;
+ }
+ }
+
+ // Undo & Redo
+ if (typeof keySeq === "string") {
+ switch (StringPrototypeCodePointAt.call(keySeq, 0)) {
+ case 0x1f:
+ this[kUndo]();
+ return;
+ case 0x1e:
+ this[kRedo]();
+ return;
+ default:
+ break;
+ }
+ }
+
+ // Ignore escape key, fixes
+ // https://github.com/nodejs/node-v0.x-archive/issues/2876.
+ if (keyName === "escape") return;
+
+ if (keyCtrl && keyShift) {
+ /* Control and shift pressed */
+ switch (keyName) {
+ // 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 (keyCtrl) {
+ /* Control key pressed */
+
+ switch (keyName) {
+ 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 "y": // Yank killed string
+ this[kYank]();
+ 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
+ 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 (keyMeta) {
+ /* Meta key pressed */
+
+ switch (keyName) {
+ 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;
+
+ case "y": // Doing yank pop
+ this[kYankPop]();
+ break;
+ }
+ } else {
+ /* No modifier keys used */
+
+ // \r bookkeeping is only relevant if a \n comes right after.
+ if (this[kSawReturnAt] && keyName !== "enter") this[kSawReturnAt] = 0;
+
+ switch (keyName) {
+ case "return": // Carriage return, i.e. \r
+ this[kSawReturnAt] = DateNow();
+ this[kLine]();
+ break;
+
+ case "enter":
+ // When key interval > crlfDelay
+ if (
+ this[kSawReturnAt] === 0 ||
+ DateNow() - 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
+ ) {
+ var lastKeypressWasTab = previousKey && previousKey.name === "tab";
+ this[kTabComplete](lastKeypressWasTab);
+ break;
+ }
+ // falls through
+ default:
+ if (typeof s === "string" && s) {
+ var nextMatch = RegExpPrototypeExec.call(lineEnding, s);
+ if (nextMatch !== null) {
+ this[kInsertString](
+ StringPrototypeSlice.call(s, 0, nextMatch.index),
+ );
+ var { lastIndex } = lineEnding;
+ while (
+ (nextMatch = RegExpPrototypeExec.call(lineEnding, s)) !== null
+ ) {
+ this[kLine]();
+ this[kInsertString](
+ StringPrototypeSlice.call(s, lastIndex, nextMatch.index),
+ );
+ ({ lastIndex } = lineEnding);
+ }
+ if (lastIndex === s.length) this[kLine]();
+ } else {
+ this[kInsertString](s);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * 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}
+ */
+ [SymbolAsyncIterator]() {
+ if (this[kLineObjectStream] === undefined) {
+ this[kLineObjectStream] = EventEmitter.on(this, "line", {
+ close: ["close"],
+ highWatermark: 1024,
+ [kFirstEventParam]: true,
+ });
+ }
+ return this[kLineObjectStream];
+ }
+};
+
+function Interface(input, output, completer, terminal) {
+ if (!(this instanceof Interface)) {
+ return new Interface(input, output, completer, terminal);
+ }
+
+ if (
+ input?.input &&
+ typeof input.completer === "function" &&
+ input.completer.length !== 2
+ ) {
+ var { completer } = input;
+ input.completer = (v, cb) => cb(null, completer(v));
+ } else if (typeof completer === "function" && completer.length !== 2) {
+ var realCompleter = completer;
+ completer = (v, cb) => cb(null, realCompleter(v));
+ }
+
+ InterfaceConstructor.call(this, input, output, completer, terminal);
+
+ // TODO: Test this
+ if (process.env.TERM === "dumb") {
+ this._ttyWrite = _ttyWriteDumb.bind(this);
+ }
+}
+
+ObjectSetPrototypeOf(Interface.prototype, _Interface.prototype);
+ObjectSetPrototypeOf(Interface, _Interface);
+
+/**
+ * Displays `query` by writing it to the `output`.
+ * @param {string} query
+ * @param {{ signal?: AbortSignal; }} [options]
+ * @param {Function} cb
+ * @returns {void}
+ */
+Interface.prototype.question = function question(query, options, cb) {
+ cb = typeof options === "function" ? options : cb;
+ if (options === null || typeof options !== "object") {
+ options = kEmptyObject;
+ }
+
+ if (options.signal) {
+ validateAbortSignal(options.signal, "options.signal");
+ if (options.signal.aborted) {
+ return;
+ }
+
+ var onAbort = () => {
+ this[kQuestionCancel]();
+ };
+ options.signal.addEventListener("abort", onAbort, { once: true });
+ var cleanup = () => {
+ options.signal.removeEventListener("abort", onAbort);
+ };
+ var originalCb = cb;
+ cb =
+ typeof cb === "function"
+ ? (answer) => {
+ cleanup();
+ return originalCb(answer);
+ }
+ : cleanup;
+ }
+
+ if (typeof cb === "function") {
+ this[kQuestion](query, cb);
+ }
+};
+Interface.prototype.question[promisify.custom] = function question(
+ query,
+ options,
+) {
+ if (options === null || typeof options !== "object") {
+ options = kEmptyObject;
+ }
+
+ if (options.signal && options.signal.aborted) {
+ return PromiseReject(
+ new AbortError(undefined, { cause: options.signal.reason }),
+ );
+ }
+
+ return new Promise((resolve, reject) => {
+ var cb = resolve;
+
+ if (options.signal) {
+ var onAbort = () => {
+ reject(new AbortError(undefined, { cause: options.signal.reason }));
+ };
+ options.signal.addEventListener("abort", onAbort, { once: true });
+ cb = (answer) => {
+ options.signal.removeEventListener("abort", onAbort);
+ resolve(answer);
+ };
+ }
+
+ this.question(query, options, cb);
+ });
+};
+
+/**
+ * Creates a new `readline.Interface` instance.
+ * @param {Readable | {
+ * input: Readable;
+ * output: Writable;
+ * completer?: Function;
+ * terminal?: boolean;
+ * history?: string[];
+ * historySize?: number;
+ * removeHistoryDuplicates?: boolean;
+ * prompt?: string;
+ * crlfDelay?: number;
+ * escapeCodeTimeout?: number;
+ * tabSize?: number;
+ * signal?: AbortSignal;
+ * }} input
+ * @param {Writable} [output]
+ * @param {Function} [completer]
+ * @param {boolean} [terminal]
+ * @returns {Interface}
+ */
+function createInterface(input, output, completer, terminal) {
+ return new Interface(input, output, completer, terminal);
+}
+
+ObjectDefineProperties(Interface.prototype, {
+ // Redirect internal prototype methods to the underscore notation for backward
+ // compatibility.
+ [kSetRawMode]: {
+ __proto__: null,
+ get() {
+ return this._setRawMode;
+ },
+ },
+ [kOnLine]: {
+ __proto__: null,
+ get() {
+ return this._onLine;
+ },
+ },
+ [kWriteToOutput]: {
+ __proto__: null,
+ get() {
+ return this._writeToOutput;
+ },
+ },
+ [kAddHistory]: {
+ __proto__: null,
+ get() {
+ return this._addHistory;
+ },
+ },
+ [kRefreshLine]: {
+ __proto__: null,
+ get() {
+ return this._refreshLine;
+ },
+ },
+ [kNormalWrite]: {
+ __proto__: null,
+ get() {
+ return this._normalWrite;
+ },
+ },
+ [kInsertString]: {
+ __proto__: null,
+ get() {
+ return this._insertString;
+ },
+ },
+ [kTabComplete]: {
+ __proto__: null,
+ get() {
+ return this._tabComplete;
+ },
+ },
+ [kWordLeft]: {
+ __proto__: null,
+ get() {
+ return this._wordLeft;
+ },
+ },
+ [kWordRight]: {
+ __proto__: null,
+ get() {
+ return this._wordRight;
+ },
+ },
+ [kDeleteLeft]: {
+ __proto__: null,
+ get() {
+ return this._deleteLeft;
+ },
+ },
+ [kDeleteRight]: {
+ __proto__: null,
+ get() {
+ return this._deleteRight;
+ },
+ },
+ [kDeleteWordLeft]: {
+ __proto__: null,
+ get() {
+ return this._deleteWordLeft;
+ },
+ },
+ [kDeleteWordRight]: {
+ __proto__: null,
+ get() {
+ return this._deleteWordRight;
+ },
+ },
+ [kDeleteLineLeft]: {
+ __proto__: null,
+ get() {
+ return this._deleteLineLeft;
+ },
+ },
+ [kDeleteLineRight]: {
+ __proto__: null,
+ get() {
+ return this._deleteLineRight;
+ },
+ },
+ [kLine]: {
+ __proto__: null,
+ get() {
+ return this._line;
+ },
+ },
+ [kHistoryNext]: {
+ __proto__: null,
+ get() {
+ return this._historyNext;
+ },
+ },
+ [kHistoryPrev]: {
+ __proto__: null,
+ get() {
+ return this._historyPrev;
+ },
+ },
+ [kGetDisplayPos]: {
+ __proto__: null,
+ get() {
+ return this._getDisplayPos;
+ },
+ },
+ [kMoveCursor]: {
+ __proto__: null,
+ get() {
+ return this._moveCursor;
+ },
+ },
+ [kTtyWrite]: {
+ __proto__: null,
+ get() {
+ return this._ttyWrite;
+ },
+ },
+
+ // Defining proxies for the internal instance properties for backward
+ // compatibility.
+ _decoder: {
+ __proto__: null,
+ get() {
+ return this[kDecoder];
+ },
+ set(value) {
+ this[kDecoder] = value;
+ },
+ },
+ _line_buffer: {
+ __proto__: null,
+ get() {
+ return this[kLine_buffer];
+ },
+ set(value) {
+ this[kLine_buffer] = value;
+ },
+ },
+ _oldPrompt: {
+ __proto__: null,
+ get() {
+ return this[kOldPrompt];
+ },
+ set(value) {
+ this[kOldPrompt] = value;
+ },
+ },
+ _previousKey: {
+ __proto__: null,
+ get() {
+ return this[kPreviousKey];
+ },
+ set(value) {
+ this[kPreviousKey] = value;
+ },
+ },
+ _prompt: {
+ __proto__: null,
+ get() {
+ return this[kPrompt];
+ },
+ set(value) {
+ this[kPrompt] = value;
+ },
+ },
+ _questionCallback: {
+ __proto__: null,
+ get() {
+ return this[kQuestionCallback];
+ },
+ set(value) {
+ this[kQuestionCallback] = value;
+ },
+ },
+ _sawKeyPress: {
+ __proto__: null,
+ get() {
+ return this[kSawKeyPress];
+ },
+ set(value) {
+ this[kSawKeyPress] = value;
+ },
+ },
+ _sawReturnAt: {
+ __proto__: null,
+ get() {
+ return this[kSawReturnAt];
+ },
+ set(value) {
+ this[kSawReturnAt] = value;
+ },
+ },
+});
+
+// Make internal methods public for backward compatibility.
+Interface.prototype._setRawMode = _Interface.prototype[kSetRawMode];
+Interface.prototype._onLine = _Interface.prototype[kOnLine];
+Interface.prototype._writeToOutput = _Interface.prototype[kWriteToOutput];
+Interface.prototype._addHistory = _Interface.prototype[kAddHistory];
+Interface.prototype._refreshLine = _Interface.prototype[kRefreshLine];
+Interface.prototype._normalWrite = _Interface.prototype[kNormalWrite];
+Interface.prototype._insertString = _Interface.prototype[kInsertString];
+Interface.prototype._tabComplete = function (lastKeypressWasTab) {
+ // Overriding parent method because `this.completer` in the legacy
+ // implementation takes a callback instead of being an async function.
+ this.pause();
+ var string = StringPrototypeSlice.call(this.line, 0, this.cursor);
+ this.completer(string, (err, value) => {
+ this.resume();
+
+ if (err) {
+ this._writeToOutput(`Tab completion error: ${inspect(err)}`);
+ return;
+ }
+
+ this[kTabCompleter](lastKeypressWasTab, value);
+ });
+};
+Interface.prototype._wordLeft = _Interface.prototype[kWordLeft];
+Interface.prototype._wordRight = _Interface.prototype[kWordRight];
+Interface.prototype._deleteLeft = _Interface.prototype[kDeleteLeft];
+Interface.prototype._deleteRight = _Interface.prototype[kDeleteRight];
+Interface.prototype._deleteWordLeft = _Interface.prototype[kDeleteWordLeft];
+Interface.prototype._deleteWordRight = _Interface.prototype[kDeleteWordRight];
+Interface.prototype._deleteLineLeft = _Interface.prototype[kDeleteLineLeft];
+Interface.prototype._deleteLineRight = _Interface.prototype[kDeleteLineRight];
+Interface.prototype._line = _Interface.prototype[kLine];
+Interface.prototype._historyNext = _Interface.prototype[kHistoryNext];
+Interface.prototype._historyPrev = _Interface.prototype[kHistoryPrev];
+Interface.prototype._getDisplayPos = _Interface.prototype[kGetDisplayPos];
+Interface.prototype._getCursorPos = _Interface.prototype.getCursorPos;
+Interface.prototype._moveCursor = _Interface.prototype[kMoveCursor];
+Interface.prototype._ttyWrite = _Interface.prototype[kTtyWrite];
+
+function _ttyWriteDumb(s, key) {
+ key = key || kEmptyObject;
+
+ if (key.name === "escape") return;
+
+ if (this[kSawReturnAt] && key.name !== "enter") this[kSawReturnAt] = 0;
+
+ if (keyCtrl) {
+ if (key.name === "c") {
+ if (this.listenerCount("SIGINT") > 0) {
+ this.emit("SIGINT");
+ } else {
+ // This readline instance is finished
+ this.close();
+ }
+
+ return;
+ } else if (key.name === "d") {
+ this.close();
+ return;
+ }
+ }
+
+ switch (key.name) {
+ case "return": // Carriage return, i.e. \r
+ this[kSawReturnAt] = DateNow();
+ this._line();
+ break;
+
+ case "enter":
+ // When key interval > crlfDelay
+ if (
+ this[kSawReturnAt] === 0 ||
+ DateNow() - this[kSawReturnAt] > this.crlfDelay
+ ) {
+ this._line();
+ }
+ this[kSawReturnAt] = 0;
+ break;
+
+ default:
+ if (typeof s === "string" && s) {
+ this.line += s;
+ this.cursor += s.length;
+ this._writeToOutput(s);
+ }
+ }
+}
+
+// ----------------------------------------------------------------------------
+// Exports
+// ----------------------------------------------------------------------------
+export var Interface = Interface;
+export var clearLine = clearLine;
+export var clearScreenDown = clearScreenDown;
+export var createInterface = createInterface;
+export var cursorTo = cursorTo;
+export var emitKeypressEvents = emitKeypressEvents;
+export var moveCursor = moveCursor;
+export var promises = {
+ [SymbolFor("__UNIMPLEMENTED__")]: true,
+};
+
+export default {
+ Interface,
+ clearLine,
+ clearScreenDown,
+ createInterface,
+ cursorTo,
+ emitKeypressEvents,
+ moveCursor,
+ promises,
+
+ [SymbolFor("__BUN_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED__")]: {
+ CSI,
+ _Interface,
+ utils: {
+ getStringWidth,
+ stripVTControlCharacters,
+ },
+ shared: {
+ kEmptyObject,
+ validateBoolean,
+ validateInteger,
+ validateAbortSignal,
+ ERR_INVALID_ARG_TYPE,
+ },
+ symbols: {
+ kQuestion,
+ kQuestionCancel,
+ },
+ },
+ [SymbolFor("CommonJS")]: 0,
+};
diff --git a/src/bun.js/readline_promises.exports.js b/src/bun.js/readline_promises.exports.js
new file mode 100644
index 000000000..615b69ec7
--- /dev/null
+++ b/src/bun.js/readline_promises.exports.js
@@ -0,0 +1,218 @@
+// Attribution: Some parts of of this module are derived from code originating from the Node.js
+// readline module which is licensed under an MIT license:
+//
+// Copyright Node.js contributors. All rights reserved.
+//
+// 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.
+
+var { Promise } = import.meta.primordials;
+var readline = import.meta.require("node:readline");
+var isWritable;
+
+var ArrayPrototypePush = Array.prototype.push;
+var ArrayPrototypeJoin = Array.prototype.join;
+var SymbolFor = Symbol.for;
+var kInternal = SymbolFor("__BUN_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED__");
+
+var {
+ CSI,
+ _Interface,
+ symbols: { kQuestion, kQuestionCancel },
+ shared: {
+ kEmptyObject,
+ validateAbortSignal,
+ validateBoolean,
+ validateInteger,
+ ERR_INVALID_ARG_TYPE,
+ },
+} = readline[kInternal];
+
+var { kClearToLineBeginning, kClearToLineEnd, kClearLine, kClearScreenDown } =
+ CSI;
+
+class AbortError extends Error {
+ code;
+ constructor() {
+ super("The operation was aborted");
+ this.code = "ABORT_ERR";
+ }
+}
+
+export class Readline {
+ #autoCommit = false;
+ #stream;
+ #todo = [];
+
+ constructor(stream, options = undefined) {
+ isWritable ??= import.meta.require("node:stream").isWritable;
+ 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");
+
+ var data = y == null ? CSI`${x + 1}G` : CSI`${y + 1};${x + 1}H`;
+ if (this.#autoCommit) process.nextTick(() => this.#stream.write(data));
+ else ArrayPrototypePush.call(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");
+
+ var 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.call(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);
+
+ var data =
+ dir < 0 ? kClearToLineBeginning : dir > 0 ? kClearToLineEnd : kClearLine;
+ if (this.#autoCommit) process.nextTick(() => this.#stream.write(data));
+ else ArrayPrototypePush.call(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.call(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.call(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 class Interface extends _Interface {
+ // eslint-disable-next-line no-useless-constructor
+ constructor(input, output, completer, terminal) {
+ super(input, output, completer, terminal);
+ }
+ question(query, options = kEmptyObject) {
+ return new Promise((resolve, reject) => {
+ var cb = resolve;
+
+ if (options?.signal) {
+ validateAbortSignal(options.signal, "options.signal");
+ if (options.signal.aborted) {
+ return reject(
+ new AbortError(undefined, { cause: options.signal.reason }),
+ );
+ }
+
+ var onAbort = () => {
+ this[kQuestionCancel]();
+ reject(new AbortError(undefined, { cause: options.signal.reason }));
+ };
+ options.signal.addEventListener("abort", onAbort, { once: true });
+ cb = (answer) => {
+ options.signal.removeEventListener("abort", onAbort);
+ resolve(answer);
+ };
+ }
+
+ this[kQuestion](query, cb);
+ });
+ }
+}
+
+export function createInterface(input, output, completer, terminal) {
+ return new Interface(input, output, completer, terminal);
+}
+
+export default {
+ Readline,
+ Interface,
+ createInterface,
+
+ [SymbolFor("CommonJS")]: 0,
+};
diff --git a/src/bun.js/streams.exports.js b/src/bun.js/streams.exports.js
index 6f03e23eb..d5e8a2183 100644
--- a/src/bun.js/streams.exports.js
+++ b/src/bun.js/streams.exports.js
@@ -5014,7 +5014,7 @@ var require_transform = __commonJS({
if (typeof options.flush === "function") this._flush = options.flush;
}
- this.on("prefinish", prefinish);
+ this.on("prefinish", prefinish.bind(this));
}
ObjectSetPrototypeOf(Transform.prototype, Duplex.prototype);
ObjectSetPrototypeOf(Transform, Duplex);
diff --git a/test/bun.js/node-test-helpers.js b/test/bun.js/node-test-helpers.js
deleted file mode 100644
index f62f1ab3b..000000000
--- a/test/bun.js/node-test-helpers.js
+++ /dev/null
@@ -1,156 +0,0 @@
-import { expect as expect_ } from "bun:test";
-import { gcTick } from "gc";
-import assertNode from "node:assert";
-
-const expect = (actual) => {
- gcTick();
- const ret = expect_(actual);
- gcTick();
- return ret;
-};
-
-export const strictEqual = (...args) => {
- let error = null;
- try {
- assertNode.strictEqual(...args);
- } catch (err) {
- error = err;
- }
- expect(error).toBe(null);
-};
-
-export const throws = (...args) => {
- let error = null;
- try {
- assertNode.throws(...args);
- } catch (err) {
- error = err;
- }
- expect(error).toBe(null);
-};
-
-export const assert = (...args) => {
- let error = null;
- try {
- assertNode(...args);
- } catch (err) {
- error = err;
- }
- expect(error).toBe(null);
-};
-
-export const assertOk = (...args) => {
- let error = null;
- try {
- assertNode.ok(...args);
- } catch (err) {
- error = err;
- }
- expect(error).toBe(null);
-};
-
-export const createCallCheckCtx = (done, timeout = 1500) => {
- const createDone = createDoneDotAll(done);
- // const mustCallChecks = [];
-
- // failed.forEach(function (context) {
- // console.log(
- // "Mismatched %s function calls. Expected %s, actual %d.",
- // context.name,
- // context.messageSegment,
- // context.actual
- // );
- // console.log(context.stack.split("\n").slice(2).join("\n"));
- // });
-
- // TODO: Implement this to be exact only
- function mustCall(fn, exact) {
- return mustCallAtLeast(fn, exact);
- }
-
- function mustSucceed(fn, exact) {
- return mustCall(function (err, ...args) {
- assert.ifError(err);
- if (typeof fn === "function") return fn.apply(this, args);
- }, exact);
- }
-
- function mustCallAtLeast(fn, minimum) {
- return _mustCallInner(fn, minimum, "minimum");
- }
-
- function _mustCallInner(fn, criteria = 1, field) {
- if (process._exiting)
- throw new Error("Cannot use common.mustCall*() in process exit handler");
- if (typeof fn === "number") {
- criteria = fn;
- fn = noop;
- } else if (fn === undefined) {
- fn = noop;
- }
-
- if (typeof criteria !== "number")
- throw new TypeError(`Invalid ${field} value: ${criteria}`);
-
- let actual = 0;
- let expected = criteria;
-
- // mustCallChecks.push(context);
- const done = createDone(timeout);
- const _return = (...args) => {
- const result = fn.apply(this, args);
- actual++;
- if (actual >= expected) {
- done();
- }
- return result;
- };
- // Function instances have own properties that may be relevant.
- // Let's replicate those properties to the returned function.
- // Refs: https://tc39.es/ecma262/#sec-function-instances
- Object.defineProperties(_return, {
- name: {
- value: fn.name,
- writable: false,
- enumerable: false,
- configurable: true,
- },
- length: {
- value: fn.length,
- writable: false,
- enumerable: false,
- configurable: true,
- },
- });
- return _return;
- }
- return {
- mustSucceed,
- mustCall,
- mustCallAtLeast,
- };
-};
-
-export function createDoneDotAll(done) {
- let toComplete = 0;
- let completed = 0;
- function createDoneCb(timeout) {
- toComplete += 1;
- const timer = setTimeout(() => {
- console.log("Timeout");
- done(new Error("Timed out!"));
- }, timeout);
- return (result) => {
- clearTimeout(timer);
- if (result instanceof Error) {
- done(result);
- return;
- }
- completed += 1;
- if (completed === toComplete) {
- done();
- }
- };
- }
- return createDoneCb;
-}
diff --git a/test/bun.js/node-test-helpers.ts b/test/bun.js/node-test-helpers.ts
new file mode 100644
index 000000000..ff7b8ece3
--- /dev/null
+++ b/test/bun.js/node-test-helpers.ts
@@ -0,0 +1,198 @@
+import { expect as expect_ } from "bun:test";
+// @ts-ignore
+import { gcTick } from "gc";
+import assertNode from "node:assert";
+
+type DoneCb = (err?: Error) => any;
+function noop() {}
+
+const expect = (actual) => {
+ gcTick();
+ const ret = expect_(actual);
+ gcTick();
+ return ret;
+};
+
+// Assert
+export const strictEqual = (
+ ...args: Parameters<typeof assertNode.strictEqual>
+) => {
+ assertNode.strictEqual.apply(this, args);
+ expect(true).toBe(true);
+};
+
+export const notStrictEqual = (
+ ...args: Parameters<typeof assertNode.notStrictEqual>
+) => {
+ assertNode.notStrictEqual.apply(this, args);
+ expect(true).toBe(true);
+};
+
+export const deepStrictEqual = (
+ ...args: Parameters<typeof assertNode.deepStrictEqual>
+) => {
+ assertNode.deepStrictEqual.apply(this, args);
+ expect(true).toBe(true);
+};
+
+export const throws = (...args: Parameters<typeof assertNode.throws>) => {
+ assertNode.throws.apply(this, args);
+ expect(true).toBe(true);
+};
+
+export const ok = (...args: Parameters<typeof assertNode.ok>) => {
+ assertNode.ok.apply(this, args);
+ expect(true).toBe(true);
+};
+
+export const ifError = (...args: Parameters<typeof assertNode.ifError>) => {
+ assertNode.ifError.apply(this, args);
+ expect(true).toBe(true);
+};
+
+export const match = (...args: Parameters<typeof assertNode.match>) => {
+ assertNode.match.apply(this, args);
+ expect(true).toBe(true);
+};
+
+export const assert = {
+ strictEqual,
+ deepStrictEqual,
+ notStrictEqual,
+ throws,
+ ok,
+ ifError,
+ match,
+};
+
+// End assert
+
+export const createCallCheckCtx = (done: DoneCb) => {
+ const createDone = createDoneDotAll(done);
+
+ // const mustCallChecks = [];
+
+ // failed.forEach(function (context) {
+ // console.log(
+ // "Mismatched %s function calls. Expected %s, actual %d.",
+ // context.name,
+ // context.messageSegment,
+ // context.actual
+ // );
+ // console.log(context.stack.split("\n").slice(2).join("\n"));
+ // });
+
+ // TODO: Implement this to be exact only
+ function mustCall(fn?: (...args) => any, exact?: number) {
+ return mustCallAtLeast(fn, exact);
+ }
+
+ function mustNotCall(
+ reason: string = "function should not have been called",
+ ) {
+ const localDone = createDone();
+ setTimeout(() => localDone(), 200);
+ return () => {
+ done(new Error(reason));
+ };
+ }
+
+ function mustSucceed(fn: () => any, exact?: number) {
+ return mustCall(function (err, ...args) {
+ ifError(err);
+ // @ts-ignore
+ if (typeof fn === "function") return fn.apply(this, args as []);
+ }, exact);
+ }
+
+ function mustCallAtLeast(fn, minimum) {
+ return _mustCallInner(fn, minimum, "minimum");
+ }
+
+ function _mustCallInner(fn, criteria = 1, field) {
+ if (process._exiting)
+ throw new Error("Cannot use common.mustCall*() in process exit handler");
+ if (typeof fn === "number") {
+ criteria = fn;
+ fn = noop;
+ } else if (fn === undefined) {
+ fn = noop;
+ }
+
+ if (typeof criteria !== "number")
+ throw new TypeError(`Invalid ${field} value: ${criteria}`);
+
+ let actual = 0;
+ let expected = criteria;
+
+ // mustCallChecks.push(context);
+ const done = createDone();
+ const _return = (...args) => {
+ // @ts-ignore
+ const result = fn.apply(this, args);
+ actual++;
+ if (actual >= expected) {
+ done();
+ }
+ return result;
+ };
+ // Function instances have own properties that may be relevant.
+ // Let's replicate those properties to the returned function.
+ // Refs: https://tc39.es/ecma262/#sec-function-instances
+ Object.defineProperties(_return, {
+ name: {
+ value: fn.name,
+ writable: false,
+ enumerable: false,
+ configurable: true,
+ },
+ length: {
+ value: fn.length,
+ writable: false,
+ enumerable: false,
+ configurable: true,
+ },
+ });
+ return _return;
+ }
+ return {
+ mustSucceed,
+ mustCall,
+ mustCallAtLeast,
+ mustNotCall,
+ };
+};
+
+export function createDoneDotAll(done: DoneCb, globalTimeout?: number) {
+ let toComplete = 0;
+ let completed = 0;
+ const globalTimer = globalTimeout
+ ? setTimeout(() => {
+ console.log("Global Timeout");
+ done(new Error("Timed out!"));
+ }, globalTimeout)
+ : undefined;
+ function createDoneCb(timeout?: number) {
+ toComplete += 1;
+ const timer =
+ timeout !== undefined
+ ? setTimeout(() => {
+ console.log("Timeout");
+ done(new Error("Timed out!"));
+ }, timeout)
+ : timeout;
+ return (result?: Error) => {
+ if (timer) clearTimeout(timer);
+ if (globalTimer) clearTimeout(globalTimer);
+ if (result instanceof Error) {
+ done(result);
+ return;
+ }
+ completed += 1;
+ if (completed === toComplete) {
+ done();
+ }
+ };
+ }
+ return createDoneCb;
+}
diff --git a/test/bun.js/readline.node.test.ts b/test/bun.js/readline.node.test.ts
new file mode 100644
index 000000000..c681a8e15
--- /dev/null
+++ b/test/bun.js/readline.node.test.ts
@@ -0,0 +1,2018 @@
+import { beforeEach, describe, it } from "bun:test";
+import readline from "node:readline";
+import { Writable, PassThrough } from "node:stream";
+import { EventEmitter } from "node:events";
+import {
+ createDoneDotAll,
+ createCallCheckCtx,
+ assert,
+} from "./node-test-helpers";
+
+var {
+ CSI,
+ utils: { getStringWidth, stripVTControlCharacters },
+} = readline[Symbol.for("__BUN_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED__")];
+
+// ----------------------------------------------------------------------------
+// Helpers
+// ----------------------------------------------------------------------------
+
+class TestWritable extends Writable {
+ data;
+ constructor() {
+ super();
+ this.data = "";
+ }
+ _write(chunk, encoding, callback) {
+ this.data += chunk.toString();
+ callback();
+ }
+}
+
+class FakeInput extends EventEmitter {
+ resume() {}
+ pause() {}
+ write() {}
+ end() {}
+}
+
+function isWarned(emitter) {
+ for (const name in emitter) {
+ const listeners = emitter[name];
+ if (listeners.warned) return true;
+ }
+ return false;
+}
+
+function getInterface(options) {
+ const fi = new FakeInput();
+ const rli = new readline.Interface({
+ input: fi,
+ output: fi,
+ ...options,
+ });
+ return [rli, fi];
+}
+
+function assertCursorRowsAndCols(rli, rows, cols) {
+ const cursorPos = rli.getCursorPos();
+ assert.strictEqual(cursorPos.rows, rows);
+ assert.strictEqual(cursorPos.cols, cols);
+}
+
+const writable = new TestWritable();
+const input = new FakeInput();
+
+// ----------------------------------------------------------------------------
+// Tests
+// ----------------------------------------------------------------------------
+
+describe("CSI", () => {
+ it("should be defined", () => {
+ assert.ok(CSI);
+ });
+
+ it("should have all the correct clear sequences", () => {
+ assert.strictEqual(CSI.kClearToLineBeginning, "\x1b[1K");
+ assert.strictEqual(CSI.kClearToLineEnd, "\x1b[0K");
+ assert.strictEqual(CSI.kClearLine, "\x1b[2K");
+ assert.strictEqual(CSI.kClearScreenDown, "\x1b[0J");
+ assert.strictEqual(CSI`1${2}3`, "\x1b[123");
+ });
+});
+
+describe("readline.clearScreenDown()", () => {
+ it("should put clear screen sequence into writable when called", (done) => {
+ const { mustCall } = createCallCheckCtx(done);
+
+ assert.strictEqual(readline.clearScreenDown(writable), true);
+ assert.deepStrictEqual(writable.data, CSI.kClearScreenDown);
+ assert.strictEqual(readline.clearScreenDown(writable, mustCall()), true);
+ });
+
+ it("should throw on invalid callback", () => {
+ // Verify that clearScreenDown() throws on invalid callback.
+ assert.throws(() => {
+ readline.clearScreenDown(writable, null);
+ }, /ERR_INVALID_ARG_TYPE/);
+ });
+
+ it("should that clearScreenDown() does not throw on null or undefined stream", (done) => {
+ const { mustCall } = createCallCheckCtx(done);
+ assert.strictEqual(
+ readline.clearScreenDown(
+ null,
+ mustCall((err) => {
+ assert.strictEqual(err, null);
+ }),
+ ),
+ true,
+ );
+ assert.strictEqual(readline.clearScreenDown(undefined, mustCall()), true);
+ });
+});
+
+describe("readline.clearLine()", () => {
+ beforeEach(() => {
+ writable.data = "";
+ });
+
+ it("should clear to the left of cursor when given -1 as direction", () => {
+ assert.strictEqual(readline.clearLine(writable, -1), true);
+ assert.deepStrictEqual(writable.data, CSI.kClearToLineBeginning);
+ });
+
+ it("should clear to the right of cursor when given 1 as direction", () => {
+ assert.strictEqual(readline.clearLine(writable, 1), true);
+ assert.deepStrictEqual(writable.data, CSI.kClearToLineEnd);
+ });
+
+ it("should clear whole line when given 0 as direction", () => {
+ assert.strictEqual(readline.clearLine(writable, 0), true);
+ assert.deepStrictEqual(writable.data, CSI.kClearLine);
+ });
+
+ it("should call callback after clearing line", (done) => {
+ const { mustCall } = createCallCheckCtx(done);
+ assert.strictEqual(readline.clearLine(writable, -1, mustCall()), true);
+ assert.deepStrictEqual(writable.data, CSI.kClearToLineBeginning);
+ });
+
+ it("should throw on an invalid callback", () => {
+ // Verify that clearLine() throws on invalid callback.
+ assert.throws(() => {
+ readline.clearLine(writable, 0, null);
+ }, /ERR_INVALID_ARG_TYPE/);
+ });
+
+ it("shouldn't throw on on null or undefined stream", (done) => {
+ const { mustCall } = createCallCheckCtx(done);
+ // Verify that clearLine() does not throw on null or undefined stream.
+ assert.strictEqual(readline.clearLine(null, 0), true);
+ assert.strictEqual(readline.clearLine(undefined, 0), true);
+ assert.strictEqual(
+ readline.clearLine(
+ null,
+ 0,
+ mustCall((err) => {
+ assert.strictEqual(err, null);
+ }),
+ ),
+ true,
+ );
+ assert.strictEqual(readline.clearLine(undefined, 0, mustCall()), true);
+ });
+});
+
+describe("readline.moveCursor()", () => {
+ it("shouldn't write when moveCursor(0, 0) is called", (done) => {
+ const { mustCall } = createCallCheckCtx(done);
+ // Nothing is written when moveCursor 0, 0
+ [
+ [0, 0, ""],
+ [1, 0, "\x1b[1C"],
+ [-1, 0, "\x1b[1D"],
+ [0, 1, "\x1b[1B"],
+ [0, -1, "\x1b[1A"],
+ [1, 1, "\x1b[1C\x1b[1B"],
+ [-1, 1, "\x1b[1D\x1b[1B"],
+ [-1, -1, "\x1b[1D\x1b[1A"],
+ [1, -1, "\x1b[1C\x1b[1A"],
+ ].forEach((set) => {
+ writable.data = "";
+ assert.strictEqual(readline.moveCursor(writable, set[0], set[1]), true);
+ assert.deepStrictEqual(writable.data, set[2]);
+ writable.data = "";
+ assert.strictEqual(
+ readline.moveCursor(writable, set[0], set[1], mustCall()),
+ true,
+ );
+ assert.deepStrictEqual(writable.data, set[2]);
+ });
+ });
+
+ it("should throw on invalid callback", () => {
+ // Verify that moveCursor() throws on invalid callback.
+ assert.throws(() => {
+ readline.moveCursor(writable, 1, 1, null);
+ }, /ERR_INVALID_ARG_TYPE/);
+ });
+
+ it("should not throw on null or undefined stream", (done) => {
+ const { mustCall } = createCallCheckCtx(done);
+ // Verify that moveCursor() does not throw on null or undefined stream.
+ assert.strictEqual(readline.moveCursor(null, 1, 1), true);
+ assert.strictEqual(readline.moveCursor(undefined, 1, 1), true);
+ assert.strictEqual(
+ readline.moveCursor(
+ null,
+ 1,
+ 1,
+ mustCall((err) => {
+ assert.strictEqual(err, null);
+ }),
+ ),
+ true,
+ );
+ assert.strictEqual(readline.moveCursor(undefined, 1, 1, mustCall()), true);
+ });
+});
+
+describe("readline.cursorTo()", () => {
+ beforeEach(() => {
+ writable.data = "";
+ });
+
+ it("should not throw on undefined or null as stream", (done) => {
+ const { mustCall } = createCallCheckCtx(done);
+ // Undefined or null as stream should not throw.
+ assert.strictEqual(readline.cursorTo(null), true);
+ assert.strictEqual(readline.cursorTo(), true);
+ assert.strictEqual(readline.cursorTo(null, 1, 1, mustCall()), true);
+ assert.strictEqual(
+ readline.cursorTo(
+ undefined,
+ 1,
+ 1,
+ mustCall((err) => {
+ assert.strictEqual(err, null);
+ }),
+ ),
+ true,
+ );
+ });
+
+ it("should not write if given invalid cursor position - [string, undefined]", () => {
+ assert.strictEqual(readline.cursorTo(writable, "a"), true);
+ assert.strictEqual(writable.data, "");
+ });
+
+ it("should not write if given invalid cursor position - [string, string]", () => {
+ assert.strictEqual(readline.cursorTo(writable, "a", "b"), true);
+ assert.strictEqual(writable.data, "");
+ });
+
+ it("should throw when x is not a number", () => {
+ assert.throws(() => readline.cursorTo(writable, "a", 1), {
+ name: "TypeError",
+ code: "ERR_INVALID_CURSOR_POS",
+ message: "Cannot set cursor row without setting its column",
+ });
+ assert.strictEqual(writable.data, "");
+ });
+
+ it("should write when given value cursor positions", (done) => {
+ const { mustCall } = createCallCheckCtx(done);
+
+ assert.strictEqual(readline.cursorTo(writable, 1, "a"), true);
+ assert.strictEqual(writable.data, "\x1b[2G");
+
+ writable.data = "";
+ assert.strictEqual(readline.cursorTo(writable, 1), true);
+ assert.strictEqual(writable.data, "\x1b[2G");
+
+ writable.data = "";
+ assert.strictEqual(readline.cursorTo(writable, 1, 2), true);
+ assert.strictEqual(writable.data, "\x1b[3;2H");
+
+ writable.data = "";
+ assert.strictEqual(readline.cursorTo(writable, 1, 2, mustCall()), true);
+ assert.strictEqual(writable.data, "\x1b[3;2H");
+
+ writable.data = "";
+ assert.strictEqual(readline.cursorTo(writable, 1, mustCall()), true);
+ assert.strictEqual(writable.data, "\x1b[2G");
+ });
+
+ it("should throw on invalid callback", () => {
+ // Verify that cursorTo() throws on invalid callback.
+ assert.throws(() => {
+ readline.cursorTo(writable, 1, 1, null);
+ }, /ERR_INVALID_ARG_TYPE/);
+ });
+
+ it("should throw if x or y is NaN", () => {
+ // Verify that cursorTo() throws if x or y is NaN.
+ assert.throws(() => {
+ readline.cursorTo(writable, NaN);
+ }, /ERR_INVALID_ARG_VALUE/);
+
+ assert.throws(() => {
+ readline.cursorTo(writable, 1, NaN);
+ }, /ERR_INVALID_ARG_VALUE/);
+
+ assert.throws(() => {
+ readline.cursorTo(writable, NaN, NaN);
+ }, /ERR_INVALID_ARG_VALUE/);
+ });
+});
+
+describe("readline.emitKeyPressEvents()", () => {
+ // emitKeypressEvents is thoroughly tested in test-readline-keys.js.
+ // However, that test calls it implicitly. This is just a quick sanity check
+ // to verify that it works when called explicitly.
+
+ const expectedSequence = ["f", "o", "o"];
+ const expectedKeys = [
+ { sequence: "f", name: "f", ctrl: false, meta: false, shift: false },
+ { sequence: "o", name: "o", ctrl: false, meta: false, shift: false },
+ { sequence: "o", name: "o", ctrl: false, meta: false, shift: false },
+ ];
+
+ it("should emit the expected sequence when keypress listener added after called", () => {
+ const stream = new PassThrough();
+ const sequence: any[] = [];
+ const keys: any[] = [];
+
+ readline.emitKeypressEvents(stream);
+ stream.on("keypress", (s, k) => {
+ sequence.push(s);
+ keys.push(k);
+ });
+ stream.write("foo");
+
+ assert.deepStrictEqual(sequence, expectedSequence);
+ assert.deepStrictEqual(keys, expectedKeys);
+ });
+
+ it("should emit the expected sequence when keypress listener added before called", () => {
+ const stream = new PassThrough();
+ const sequence: any[] = [];
+ const keys: any[] = [];
+
+ stream.on("keypress", (s, k) => {
+ sequence.push(s);
+ keys.push(k);
+ });
+ readline.emitKeypressEvents(stream);
+ stream.write("foo");
+
+ assert.deepStrictEqual(sequence, expectedSequence);
+ assert.deepStrictEqual(keys, expectedKeys);
+ });
+
+ it("should allow keypress listeners to be removed and added again", () => {
+ const stream = new PassThrough();
+ const sequence: any[] = [];
+ const keys: any[] = [];
+ const keypressListener = (s, k) => {
+ sequence.push(s);
+ keys.push(k);
+ };
+
+ stream.on("keypress", keypressListener);
+ readline.emitKeypressEvents(stream);
+ stream.removeListener("keypress", keypressListener);
+ stream.write("foo");
+
+ assert.deepStrictEqual(sequence, []);
+ assert.deepStrictEqual(keys, []);
+
+ stream.on("keypress", keypressListener);
+ stream.write("foo");
+
+ assert.deepStrictEqual(sequence, expectedSequence);
+ assert.deepStrictEqual(keys, expectedKeys);
+ });
+});
+
+describe("readline.Interface", () => {
+ it("should allow valid escapeCodeTimeout to be set", () => {
+ const fi = new FakeInput();
+ const rli = new readline.Interface({
+ input: fi,
+ output: fi,
+ escapeCodeTimeout: 50,
+ });
+ assert.strictEqual(rli.escapeCodeTimeout, 50);
+ rli.close();
+ });
+
+ it("should throw on invalid escapeCodeTimeout", () => {
+ [null, {}, NaN, "50"].forEach((invalidInput) => {
+ assert.throws(
+ () => {
+ const fi = new FakeInput();
+ const rli = new readline.Interface({
+ input: fi,
+ output: fi,
+ escapeCodeTimeout: invalidInput,
+ });
+ rli.close();
+ },
+ {
+ name: "TypeError",
+ code: "ERR_INVALID_ARG_VALUE",
+ },
+ );
+ });
+ });
+
+ it("should create valid instances of readline.Interface", () => {
+ const input = new FakeInput();
+ const rl = readline.Interface({ input });
+ assert.ok(rl instanceof readline.Interface);
+ });
+
+ it("should call completer when input emits data", (done) => {
+ const { mustCall } = createCallCheckCtx(done);
+ const fi = new FakeInput();
+ const rli = new readline.Interface(
+ fi,
+ fi,
+ mustCall((line) => [[], line]),
+ true,
+ );
+
+ assert.ok(rli instanceof readline.Interface);
+ fi.emit("data", "a\t");
+ rli.close();
+ });
+
+ it("should allow crlfDelay to be set", () => {
+ [undefined, 50, 0, 100.5, 5000].forEach((crlfDelay) => {
+ const [rli] = getInterface({ crlfDelay });
+ assert.strictEqual(rli.crlfDelay, Math.max(crlfDelay || 100, 100));
+ rli.close();
+ });
+ });
+
+ it("should throw if completer is not a function or is undefined", () => {
+ ["not an array", 123, 123n, {}, true, Symbol(), null].forEach((invalid) => {
+ assert.throws(
+ () => {
+ readline.createInterface({
+ input,
+ completer: invalid,
+ });
+ },
+ {
+ name: "TypeError",
+ code: "ERR_INVALID_ARG_VALUE",
+ },
+ );
+ });
+ });
+
+ it("should throw if history is not an array", () => {
+ ["not an array", 123, 123, {}, true, Symbol(), null].forEach((history) => {
+ assert.throws(
+ () => {
+ readline.createInterface({
+ input,
+ history,
+ });
+ },
+ {
+ name: "TypeError",
+ code: "ERR_INVALID_ARG_TYPE",
+ },
+ );
+ });
+ });
+
+ it("should throw if historySize is not a positive number", () => {
+ ["not a number", -1, NaN, {}, true, Symbol(), null].forEach(
+ (historySize) => {
+ assert.throws(
+ () => {
+ readline.createInterface({
+ input,
+ historySize,
+ });
+ },
+ {
+ // TODO: Revert to Range error when properly implemented errors with multiple bases
+ // name: "RangeError",
+ name: "TypeError",
+ code: "ERR_INVALID_ARG_VALUE",
+ },
+ );
+ },
+ );
+ });
+
+ it("should throw on invalid tabSize", () => {
+ // Check for invalid tab sizes.
+ assert.throws(
+ () =>
+ new readline.Interface({
+ input,
+ tabSize: 0,
+ }),
+ { code: "ERR_OUT_OF_RANGE" },
+ );
+
+ assert.throws(
+ () =>
+ new readline.Interface({
+ input,
+ tabSize: "4",
+ }),
+ { code: "ERR_INVALID_ARG_TYPE" },
+ );
+
+ assert.throws(
+ () =>
+ new readline.Interface({
+ input,
+ tabSize: 4.5,
+ }),
+ {
+ code: "ERR_OUT_OF_RANGE",
+ // message:
+ // 'The value of "tabSize" is out of range. ' +
+ // "It must be an integer. Received 4.5",
+ },
+ );
+ });
+
+ // Sending a single character with no newline
+ it("should not emit line when only a single character sent with no newline", (done) => {
+ const { mustNotCall } = createCallCheckCtx(done);
+ const fi = new FakeInput();
+ const rli = new readline.Interface(fi, {});
+ rli.on("line", mustNotCall());
+ fi.emit("data", "a");
+ rli.close();
+ });
+
+ it("should treat \\r like \\n when alone", (done) => {
+ const { mustCall } = createCallCheckCtx(done);
+ // Sending multiple newlines at once that does not end with a new line and a
+ // `end` event(last line is). \r should behave like \n when alone.
+ const [rli, fi] = getInterface({ terminal: true });
+ const expectedLines = ["foo", "bar", "baz", "bat"];
+ rli.on(
+ "line",
+ mustCall((line) => {
+ assert.strictEqual(line, expectedLines.shift());
+ }, expectedLines.length - 1),
+ );
+ fi.emit("data", expectedLines.join("\r"));
+ rli.close();
+ });
+
+ // \r at start of input should output blank line
+ it("should output blank line when \\r at start of input", (done) => {
+ const { mustCall } = createCallCheckCtx(done);
+ const [rli, fi] = getInterface({ terminal: true });
+ const expectedLines = ["", "foo"];
+ rli.on(
+ "line",
+ mustCall((line) => {
+ assert.strictEqual(line, expectedLines.shift());
+ }, expectedLines.length),
+ );
+ fi.emit("data", "\rfoo\r");
+ rli.close();
+ });
+
+ // \t does not become part of the input when there is a completer function
+ it("should not include \\t in input when there is a completer function", (done) => {
+ const { mustCall } = createCallCheckCtx(done);
+ const completer = (line) => [[], line];
+ const [rli, fi] = getInterface({ terminal: true, completer });
+ rli.on(
+ "line",
+ mustCall((line) => {
+ assert.strictEqual(line, "foo");
+ }),
+ );
+ for (const character of "\tfo\to\t") {
+ fi.emit("data", character);
+ }
+ fi.emit("data", "\n");
+ rli.close();
+ });
+
+ // \t when there is no completer function should behave like an ordinary
+ // character
+ it("should treat \\t as an ordinary character when there is no completer function", (done) => {
+ const { mustCall } = createCallCheckCtx(done);
+ const [rli, fi] = getInterface({ terminal: true });
+ rli.on(
+ "line",
+ mustCall((line) => {
+ assert.strictEqual(line, "\t");
+ }),
+ );
+ fi.emit("data", "\t");
+ fi.emit("data", "\n");
+ rli.close();
+ });
+
+ // Adding history lines should emit the history event with
+ // the history array
+ it("should emit history event when adding history lines", (done) => {
+ const { mustCall } = createCallCheckCtx(done);
+ const [rli, fi] = getInterface({ terminal: true });
+ const expectedLines = ["foo", "bar", "baz", "bat"];
+ rli.on(
+ "history",
+ mustCall((history) => {
+ const expectedHistory = expectedLines
+ .slice(0, history.length)
+ .reverse();
+ assert.deepStrictEqual(history, expectedHistory);
+ }, expectedLines.length),
+ );
+ for (const line of expectedLines) {
+ fi.emit("data", `${line}\n`);
+ }
+ rli.close();
+ });
+
+ // Altering the history array in the listener should not alter
+ // the line being processed
+ it("should not alter the line being processed when history is altered", (done) => {
+ const { mustCall } = createCallCheckCtx(done);
+ const [rli, fi] = getInterface({ terminal: true });
+ const expectedLine = "foo";
+ rli.on(
+ "history",
+ mustCall((history) => {
+ assert.strictEqual(history[0], expectedLine);
+ history.shift();
+ }),
+ );
+ rli.on(
+ "line",
+ mustCall((line) => {
+ assert.strictEqual(line, expectedLine);
+ assert.strictEqual(rli.history.length, 0);
+ }),
+ );
+ fi.emit("data", `${expectedLine}\n`);
+ rli.close();
+ });
+
+ // Duplicate lines are removed from history when
+ // `options.removeHistoryDuplicates` is `true`
+ it("should remove duplicate lines from history when removeHistoryDuplicates is true", () => {
+ const [rli, fi] = getInterface({
+ terminal: true,
+ removeHistoryDuplicates: true,
+ });
+ const expectedLines = ["foo", "bar", "baz", "bar", "bat", "bat"];
+ // ['foo', 'baz', 'bar', bat'];
+ let callCount = 0;
+ rli.on("line", (line) => {
+ assert.strictEqual(line, expectedLines[callCount]);
+ callCount++;
+ });
+ fi.emit("data", `${expectedLines.join("\n")}\n`);
+ assert.strictEqual(callCount, expectedLines.length);
+ fi.emit("keypress", ".", { name: "up" }); // 'bat'
+ assert.strictEqual(rli.line, expectedLines[--callCount]);
+ fi.emit("keypress", ".", { name: "up" }); // 'bar'
+ assert.notStrictEqual(rli.line, expectedLines[--callCount]);
+ assert.strictEqual(rli.line, expectedLines[--callCount]);
+ fi.emit("keypress", ".", { name: "up" }); // 'baz'
+ assert.strictEqual(rli.line, expectedLines[--callCount]);
+ fi.emit("keypress", ".", { name: "up" }); // 'foo'
+ assert.notStrictEqual(rli.line, expectedLines[--callCount]);
+ assert.strictEqual(rli.line, expectedLines[--callCount]);
+ assert.strictEqual(callCount, 0);
+ fi.emit("keypress", ".", { name: "down" }); // 'baz'
+ assert.strictEqual(rli.line, "baz");
+ assert.strictEqual(rli.historyIndex, 2);
+ fi.emit("keypress", ".", { name: "n", ctrl: true }); // 'bar'
+ assert.strictEqual(rli.line, "bar");
+ assert.strictEqual(rli.historyIndex, 1);
+ fi.emit("keypress", ".", { name: "n", ctrl: true });
+ assert.strictEqual(rli.line, "bat");
+ assert.strictEqual(rli.historyIndex, 0);
+ // Activate the substring history search.
+ fi.emit("keypress", ".", { name: "down" }); // 'bat'
+ assert.strictEqual(rli.line, "bat");
+ assert.strictEqual(rli.historyIndex, -1);
+ // Deactivate substring history search.
+ fi.emit("keypress", ".", { name: "backspace" }); // 'ba'
+ assert.strictEqual(rli.historyIndex, -1);
+ assert.strictEqual(rli.line, "ba");
+ // Activate the substring history search.
+ fi.emit("keypress", ".", { name: "down" }); // 'ba'
+ assert.strictEqual(rli.historyIndex, -1);
+ assert.strictEqual(rli.line, "ba");
+ fi.emit("keypress", ".", { name: "down" }); // 'ba'
+ assert.strictEqual(rli.historyIndex, -1);
+ assert.strictEqual(rli.line, "ba");
+ fi.emit("keypress", ".", { name: "up" }); // 'bat'
+ assert.strictEqual(rli.historyIndex, 0);
+ assert.strictEqual(rli.line, "bat");
+ fi.emit("keypress", ".", { name: "up" }); // 'bar'
+ assert.strictEqual(rli.historyIndex, 1);
+ assert.strictEqual(rli.line, "bar");
+ fi.emit("keypress", ".", { name: "up" }); // 'baz'
+ assert.strictEqual(rli.historyIndex, 2);
+ assert.strictEqual(rli.line, "baz");
+ fi.emit("keypress", ".", { name: "up" }); // 'ba'
+ assert.strictEqual(rli.historyIndex, 4);
+ assert.strictEqual(rli.line, "ba");
+ fi.emit("keypress", ".", { name: "up" }); // 'ba'
+ assert.strictEqual(rli.historyIndex, 4);
+ assert.strictEqual(rli.line, "ba");
+ // Deactivate substring history search and reset history index.
+ fi.emit("keypress", ".", { name: "right" }); // 'ba'
+ assert.strictEqual(rli.historyIndex, -1);
+ assert.strictEqual(rli.line, "ba");
+ // Substring history search activated.
+ fi.emit("keypress", ".", { name: "up" }); // 'ba'
+ assert.strictEqual(rli.historyIndex, 0);
+ assert.strictEqual(rli.line, "bat");
+ rli.close();
+ });
+
+ // Duplicate lines are not removed from history when
+ // `options.removeHistoryDuplicates` is `false`
+ it("should not remove duplicate lines from history when removeHistoryDuplicates is false", () => {
+ const [rli, fi] = getInterface({
+ terminal: true,
+ removeHistoryDuplicates: false,
+ });
+ const expectedLines = ["foo", "bar", "baz", "bar", "bat", "bat"];
+ let callCount = 0;
+ rli.on("line", (line) => {
+ assert.strictEqual(line, expectedLines[callCount]);
+ callCount++;
+ });
+ fi.emit("data", `${expectedLines.join("\n")}\n`);
+ assert.strictEqual(callCount, expectedLines.length);
+ fi.emit("keypress", ".", { name: "up" }); // 'bat'
+ assert.strictEqual(rli.line, expectedLines[--callCount]);
+ fi.emit("keypress", ".", { name: "up" }); // 'bar'
+ assert.notStrictEqual(rli.line, expectedLines[--callCount]);
+ assert.strictEqual(rli.line, expectedLines[--callCount]);
+ fi.emit("keypress", ".", { name: "up" }); // 'baz'
+ assert.strictEqual(rli.line, expectedLines[--callCount]);
+ fi.emit("keypress", ".", { name: "up" }); // 'bar'
+ assert.strictEqual(rli.line, expectedLines[--callCount]);
+ fi.emit("keypress", ".", { name: "up" }); // 'foo'
+ assert.strictEqual(rli.line, expectedLines[--callCount]);
+ assert.strictEqual(callCount, 0);
+ rli.close();
+ });
+
+ // Regression test for repl freeze, #1968:
+ // check that nothing fails if 'keypress' event throws.
+ it("should not fail if keypress throws", () => {
+ const [rli, fi] = getInterface({ terminal: true });
+ const keys = [] as string[];
+ const err = new Error("bad thing happened");
+ fi.on("keypress", (key: string) => {
+ keys.push(key);
+ if (key === "X") {
+ throw err;
+ }
+ });
+ assert.throws(
+ () => fi.emit("data", "fooX"),
+ (e) => {
+ console.log("ERRROR!", e);
+ assert.strictEqual(e, err);
+ return true;
+ },
+ );
+ fi.emit("data", "bar");
+ assert.strictEqual(keys.join(""), "fooXbar");
+ rli.close();
+ });
+
+ // History is bound
+ it("should bind history", () => {
+ const [rli, fi] = getInterface({ terminal: true, historySize: 2 });
+ const lines = ["line 1", "line 2", "line 3"];
+ fi.emit("data", lines.join("\n") + "\n");
+ assert.strictEqual(rli.history.length, 2);
+ assert.strictEqual(rli.history[0], "line 3");
+ assert.strictEqual(rli.history[1], "line 2");
+ });
+
+ // Question
+ it("should handle question", () => {
+ const [rli] = getInterface({ terminal: true });
+ const expectedLines = ["foo"];
+ rli.question(expectedLines[0], () => rli.close());
+ assertCursorRowsAndCols(rli, 0, expectedLines[0].length);
+ rli.close();
+ });
+
+ // Sending a multi-line question
+ it("should handle multi-line questions", () => {
+ const [rli] = getInterface({ terminal: true });
+ const expectedLines = ["foo", "bar"];
+ rli.question(expectedLines.join("\n"), () => rli.close());
+ assertCursorRowsAndCols(
+ rli,
+ expectedLines.length - 1,
+ expectedLines.slice(-1)[0].length,
+ );
+ rli.close();
+ });
+
+ it("should handle beginning and end of line", () => {
+ // Beginning and end of line
+ const [rli, fi] = getInterface({ terminal: true, prompt: "" });
+ fi.emit("data", "the quick brown fox");
+ fi.emit("keypress", ".", { ctrl: true, name: "a" });
+ assertCursorRowsAndCols(rli, 0, 0);
+ fi.emit("keypress", ".", { ctrl: true, name: "e" });
+ assertCursorRowsAndCols(rli, 0, 19);
+ rli.close();
+ });
+
+ it("should handle back and forward one character", () => {
+ // Back and Forward one character
+ const [rli, fi] = getInterface({ terminal: true, prompt: "" });
+ fi.emit("data", "the quick brown fox");
+ assertCursorRowsAndCols(rli, 0, 19);
+
+ // Back one character
+ fi.emit("keypress", ".", { ctrl: true, name: "b" });
+ assertCursorRowsAndCols(rli, 0, 18);
+ // Back one character
+ fi.emit("keypress", ".", { ctrl: true, name: "b" });
+ assertCursorRowsAndCols(rli, 0, 17);
+ // Forward one character
+ fi.emit("keypress", ".", { ctrl: true, name: "f" });
+ assertCursorRowsAndCols(rli, 0, 18);
+ // Forward one character
+ fi.emit("keypress", ".", { ctrl: true, name: "f" });
+ assertCursorRowsAndCols(rli, 0, 19);
+ rli.close();
+ });
+
+ // Back and Forward one astral character
+ it("should handle going back and forward one astral character", (done) => {
+ const { mustCall } = createCallCheckCtx(done);
+ const [rli, fi] = getInterface({ terminal: true, prompt: "" });
+ fi.emit("data", "💻");
+
+ // Move left one character/code point
+ fi.emit("keypress", ".", { name: "left" });
+ assertCursorRowsAndCols(rli, 0, 0);
+
+ // Move right one character/code point
+ fi.emit("keypress", ".", { name: "right" });
+ assertCursorRowsAndCols(rli, 0, 2);
+
+ rli.on(
+ "line",
+ mustCall((line) => {
+ assert.strictEqual(line, "💻");
+ }),
+ );
+ fi.emit("data", "\n");
+ rli.close();
+ });
+
+ // Two astral characters left
+ it("should handle two astral characters left", (done) => {
+ const { mustCall } = createCallCheckCtx(done);
+ const [rli, fi] = getInterface({ terminal: true, prompt: "" });
+ fi.emit("data", "💻");
+
+ // Move left one character/code point
+ fi.emit("keypress", ".", { name: "left" });
+ assertCursorRowsAndCols(rli, 0, 0);
+
+ fi.emit("data", "🐕");
+ assertCursorRowsAndCols(rli, 0, 2);
+
+ rli.on(
+ "line",
+ mustCall((line) => {
+ assert.strictEqual(line, "🐕💻");
+ }),
+ );
+ fi.emit("data", "\n");
+ rli.close();
+ });
+
+ // Two astral characters right
+ it("should handle two astral characters right", (done) => {
+ const { mustCall } = createCallCheckCtx(done);
+ const [rli, fi] = getInterface({ terminal: true, prompt: "" });
+ fi.emit("data", "💻");
+
+ // Move left one character/code point
+ fi.emit("keypress", ".", { name: "right" });
+ assertCursorRowsAndCols(rli, 0, 2);
+
+ fi.emit("data", "🐕");
+ assertCursorRowsAndCols(rli, 0, 4);
+
+ rli.on(
+ "line",
+ mustCall((line) => {
+ assert.strictEqual(line, "💻🐕");
+ }),
+ );
+ fi.emit("data", "\n");
+ rli.close();
+ });
+
+ it("should handle wordLeft and wordRight", () => {
+ // `wordLeft` and `wordRight`
+ const [rli, fi] = getInterface({ terminal: true, prompt: "" });
+ fi.emit("data", "the quick brown fox");
+ fi.emit("keypress", ".", { ctrl: true, name: "left" });
+ assertCursorRowsAndCols(rli, 0, 16);
+ fi.emit("keypress", ".", { meta: true, name: "b" });
+ assertCursorRowsAndCols(rli, 0, 10);
+ fi.emit("keypress", ".", { ctrl: true, name: "right" });
+ assertCursorRowsAndCols(rli, 0, 16);
+ fi.emit("keypress", ".", { meta: true, name: "f" });
+ assertCursorRowsAndCols(rli, 0, 19);
+ rli.close();
+ });
+
+ // `deleteWordLeft`
+ it("should handle deleteWordLeft", (done) => {
+ const { mustCall } = createCallCheckCtx(done);
+ [
+ { ctrl: true, name: "w" },
+ { ctrl: true, name: "backspace" },
+ { meta: true, name: "backspace" },
+ ].forEach((deleteWordLeftKey) => {
+ let [rli, fi] = getInterface({ terminal: true, prompt: "" });
+ fi.emit("data", "the quick brown fox");
+ fi.emit("keypress", ".", { ctrl: true, name: "left" });
+ rli.on(
+ "line",
+ mustCall((line) => {
+ assert.strictEqual(line, "the quick fox");
+ }),
+ );
+ fi.emit("keypress", ".", deleteWordLeftKey);
+ fi.emit("data", "\n");
+ rli.close();
+
+ // No effect if pressed at beginning of line
+ [rli, fi] = getInterface({ terminal: true, prompt: "" });
+ fi.emit("data", "the quick brown fox");
+ fi.emit("keypress", ".", { ctrl: true, name: "a" });
+ rli.on(
+ "line",
+ mustCall((line) => {
+ assert.strictEqual(line, "the quick brown fox");
+ }),
+ );
+ fi.emit("keypress", ".", deleteWordLeftKey);
+ fi.emit("data", "\n");
+ rli.close();
+ });
+ });
+
+ // `deleteWordRight`
+ it("should handle deleteWordRight", (done) => {
+ const { mustCall } = createCallCheckCtx(done);
+ [
+ { ctrl: true, name: "delete" },
+ { meta: true, name: "delete" },
+ { meta: true, name: "d" },
+ ].forEach((deleteWordRightKey) => {
+ let [rli, fi] = getInterface({ terminal: true, prompt: "" });
+ fi.emit("data", "the quick brown fox");
+ fi.emit("keypress", ".", { ctrl: true, name: "left" });
+ fi.emit("keypress", ".", { ctrl: true, name: "left" });
+ rli.on(
+ "line",
+ mustCall((line) => {
+ assert.strictEqual(line, "the quick fox");
+ }),
+ );
+ fi.emit("keypress", ".", deleteWordRightKey);
+ fi.emit("data", "\n");
+ rli.close();
+
+ // No effect if pressed at end of line
+ [rli, fi] = getInterface({ terminal: true, prompt: "" });
+ fi.emit("data", "the quick brown fox");
+ rli.on(
+ "line",
+ mustCall((line) => {
+ assert.strictEqual(line, "the quick brown fox");
+ }),
+ );
+ fi.emit("keypress", ".", deleteWordRightKey);
+ fi.emit("data", "\n");
+ rli.close();
+ });
+ });
+
+ // deleteLeft
+ it("should handle deleteLeft", (done) => {
+ const { mustCall } = createCallCheckCtx(done);
+ const [rli, fi] = getInterface({ terminal: true, prompt: "" });
+ fi.emit("data", "the quick brown fox");
+ assertCursorRowsAndCols(rli, 0, 19);
+
+ // Delete left character
+ fi.emit("keypress", ".", { ctrl: true, name: "h" });
+ assertCursorRowsAndCols(rli, 0, 18);
+ rli.on(
+ "line",
+ mustCall((line) => {
+ assert.strictEqual(line, "the quick brown fo");
+ }),
+ );
+ fi.emit("data", "\n");
+ rli.close();
+ });
+
+ // deleteLeft astral character
+ it("should handle deleteLeft astral character", (done) => {
+ const { mustCall } = createCallCheckCtx(done);
+ const [rli, fi] = getInterface({ terminal: true, prompt: "" });
+ fi.emit("data", "💻");
+ assertCursorRowsAndCols(rli, 0, 2);
+ // Delete left character
+ fi.emit("keypress", ".", { ctrl: true, name: "h" });
+ assertCursorRowsAndCols(rli, 0, 0);
+ rli.on(
+ "line",
+ mustCall((line) => {
+ assert.strictEqual(line, "");
+ }),
+ );
+ fi.emit("data", "\n");
+ rli.close();
+ });
+
+ // deleteRight
+ it("should handle deleteRight", (done) => {
+ const { mustCall } = createCallCheckCtx(done);
+ const [rli, fi] = getInterface({ terminal: true, prompt: "" });
+ fi.emit("data", "the quick brown fox");
+
+ // Go to the start of the line
+ fi.emit("keypress", ".", { ctrl: true, name: "a" });
+ assertCursorRowsAndCols(rli, 0, 0);
+
+ // Delete right character
+ fi.emit("keypress", ".", { ctrl: true, name: "d" });
+ assertCursorRowsAndCols(rli, 0, 0);
+ rli.on(
+ "line",
+ mustCall((line) => {
+ assert.strictEqual(line, "he quick brown fox");
+ }),
+ );
+ fi.emit("data", "\n");
+ rli.close();
+ });
+
+ // deleteRight astral character
+ it("should handle deleteRight of astral characters", (done) => {
+ const { mustCall } = createCallCheckCtx(done);
+ const [rli, fi] = getInterface({ terminal: true, prompt: "" });
+ fi.emit("data", "💻");
+
+ // Go to the start of the line
+ fi.emit("keypress", ".", { ctrl: true, name: "a" });
+ assertCursorRowsAndCols(rli, 0, 0);
+
+ // Delete right character
+ fi.emit("keypress", ".", { ctrl: true, name: "d" });
+ assertCursorRowsAndCols(rli, 0, 0);
+ rli.on(
+ "line",
+ mustCall((line) => {
+ assert.strictEqual(line, "");
+ }),
+ );
+ fi.emit("data", "\n");
+ rli.close();
+ });
+
+ // deleteLineLeft
+ it("should handle deleteLineLeft", (done) => {
+ const { mustCall } = createCallCheckCtx(done);
+ const [rli, fi] = getInterface({ terminal: true, prompt: "" });
+ fi.emit("data", "the quick brown fox");
+ assertCursorRowsAndCols(rli, 0, 19);
+
+ // Delete from current to start of line
+ fi.emit("keypress", ".", { ctrl: true, shift: true, name: "backspace" });
+ assertCursorRowsAndCols(rli, 0, 0);
+ rli.on(
+ "line",
+ mustCall((line) => {
+ assert.strictEqual(line, "");
+ }),
+ );
+ fi.emit("data", "\n");
+ rli.close();
+ });
+
+ // deleteLineRight
+ it("should handle deleteLineRight", (done) => {
+ const { mustCall } = createCallCheckCtx(done);
+ const [rli, fi] = getInterface({ terminal: true, prompt: "" });
+ fi.emit("data", "the quick brown fox");
+
+ // Go to the start of the line
+ fi.emit("keypress", ".", { ctrl: true, name: "a" });
+ assertCursorRowsAndCols(rli, 0, 0);
+
+ // Delete from current to end of line
+ fi.emit("keypress", ".", { ctrl: true, shift: true, name: "delete" });
+ assertCursorRowsAndCols(rli, 0, 0);
+ rli.on(
+ "line",
+ mustCall((line) => {
+ assert.strictEqual(line, "");
+ }),
+ );
+ fi.emit("data", "\n");
+ rli.close();
+ });
+
+ // yank
+ it("should handle yank", (done) => {
+ const { mustCall } = createCallCheckCtx(done);
+ const [rli, fi] = getInterface({ terminal: true, prompt: "" });
+ fi.emit("data", "the quick brown fox");
+ assertCursorRowsAndCols(rli, 0, 19);
+
+ // Go to the start of the line
+ fi.emit("keypress", ".", { ctrl: true, name: "a" });
+ // Move forward one char
+ fi.emit("keypress", ".", { ctrl: true, name: "f" });
+ // Delete the right part
+ fi.emit("keypress", ".", { ctrl: true, shift: true, name: "delete" });
+ assertCursorRowsAndCols(rli, 0, 1);
+
+ // Yank
+ fi.emit("keypress", ".", { ctrl: true, name: "y" });
+ assertCursorRowsAndCols(rli, 0, 19);
+
+ rli.on(
+ "line",
+ mustCall((line) => {
+ assert.strictEqual(line, "the quick brown fox");
+ }),
+ );
+
+ fi.emit("data", "\n");
+ rli.close();
+ });
+
+ // yank pop
+ it("should handle yank pop", (done) => {
+ const { mustCall } = createCallCheckCtx(done);
+ const [rli, fi] = getInterface({ terminal: true, prompt: "" });
+ fi.emit("data", "the quick brown fox");
+ assertCursorRowsAndCols(rli, 0, 19);
+
+ // Go to the start of the line
+ fi.emit("keypress", ".", { ctrl: true, name: "a" });
+ // Move forward one char
+ fi.emit("keypress", ".", { ctrl: true, name: "f" });
+ // Delete the right part
+ fi.emit("keypress", ".", { ctrl: true, shift: true, name: "delete" });
+ assertCursorRowsAndCols(rli, 0, 1);
+ // Yank
+ fi.emit("keypress", ".", { ctrl: true, name: "y" });
+ assertCursorRowsAndCols(rli, 0, 19);
+
+ // Go to the start of the line
+ fi.emit("keypress", ".", { ctrl: true, name: "a" });
+ // Move forward four chars
+ fi.emit("keypress", ".", { ctrl: true, name: "f" });
+ fi.emit("keypress", ".", { ctrl: true, name: "f" });
+ fi.emit("keypress", ".", { ctrl: true, name: "f" });
+ fi.emit("keypress", ".", { ctrl: true, name: "f" });
+ // Delete the right part
+ fi.emit("keypress", ".", { ctrl: true, shift: true, name: "delete" });
+ assertCursorRowsAndCols(rli, 0, 4);
+ // Go to the start of the line
+ fi.emit("keypress", ".", { ctrl: true, name: "a" });
+ assertCursorRowsAndCols(rli, 0, 0);
+
+ // Yank: 'quick brown fox|the '
+ fi.emit("keypress", ".", { ctrl: true, name: "y" });
+ // Yank pop: 'he quick brown fox|the'
+ fi.emit("keypress", ".", { meta: true, name: "y" });
+ assertCursorRowsAndCols(rli, 0, 18);
+
+ rli.on(
+ "line",
+ mustCall((line) => {
+ assert.strictEqual(line, "he quick brown foxthe ");
+ }),
+ );
+
+ fi.emit("data", "\n");
+ rli.close();
+ });
+
+ // Close readline interface
+ it("Should close readline interface", () => {
+ const [rli, fi] = getInterface({ terminal: true, prompt: "" });
+ fi.emit("keypress", ".", { ctrl: true, name: "c" });
+ assert.ok(rli.closed);
+ });
+
+ // Multi-line input cursor position
+ it("should handle multi-line input cursors", () => {
+ const [rli, fi] = getInterface({ terminal: true, prompt: "" });
+ fi.columns = 10;
+ fi.emit("data", "multi-line text");
+ assertCursorRowsAndCols(rli, 1, 5);
+ rli.close();
+ });
+
+ // Multi-line input cursor position and long tabs
+ it("should handle long tabs", () => {
+ const [rli, fi] = getInterface({
+ tabSize: 16,
+ terminal: true,
+ prompt: "",
+ });
+ fi.columns = 10;
+ fi.emit("data", "multi-line\ttext \t");
+ assert.strictEqual(rli.cursor, 17);
+ assertCursorRowsAndCols(rli, 3, 2);
+ rli.close();
+ });
+
+ // Check for the default tab size.
+ it("should use the default tab size", () => {
+ const [rli, fi] = getInterface({ terminal: true, prompt: "" });
+ fi.emit("data", "the quick\tbrown\tfox");
+ assert.strictEqual(rli.cursor, 19);
+ // The first tab is 7 spaces long, the second one 3 spaces.
+ assertCursorRowsAndCols(rli, 0, 27);
+ });
+
+ // Multi-line prompt cursor position
+ it("should handle multi-line prompt cursor position", () => {
+ const [rli, fi] = getInterface({
+ terminal: true,
+ prompt: "\nfilledline\nwraping text\n> ",
+ });
+ fi.columns = 10;
+ fi.emit("data", "t");
+ assertCursorRowsAndCols(rli, 4, 3);
+ rli.close();
+ });
+
+ // Undo & Redo
+ it("should undo and redo", () => {
+ const [rli, fi] = getInterface({ terminal: true, prompt: "" });
+ fi.emit("data", "the quick brown fox");
+ assertCursorRowsAndCols(rli, 0, 19);
+
+ // Delete the last eight chars
+ fi.emit("keypress", ".", { ctrl: true, shift: false, name: "b" });
+ fi.emit("keypress", ".", { ctrl: true, shift: false, name: "b" });
+ fi.emit("keypress", ".", { ctrl: true, shift: false, name: "b" });
+ fi.emit("keypress", ".", { ctrl: true, shift: false, name: "b" });
+ fi.emit("keypress", ",", { ctrl: true, shift: false, name: "k" });
+
+ fi.emit("keypress", ".", { ctrl: true, shift: false, name: "b" });
+ fi.emit("keypress", ".", { ctrl: true, shift: false, name: "b" });
+ fi.emit("keypress", ".", { ctrl: true, shift: false, name: "b" });
+ fi.emit("keypress", ".", { ctrl: true, shift: false, name: "b" });
+ fi.emit("keypress", ",", { ctrl: true, shift: false, name: "k" });
+
+ assertCursorRowsAndCols(rli, 0, 11);
+ // Perform undo twice
+ fi.emit("keypress", ",", { sequence: "\x1F" });
+ assert.strictEqual(rli.line, "the quick brown");
+ fi.emit("keypress", ",", { sequence: "\x1F" });
+ assert.strictEqual(rli.line, "the quick brown fox");
+ // Perform redo twice
+ fi.emit("keypress", ",", { sequence: "\x1E" });
+ assert.strictEqual(rli.line, "the quick brown");
+ fi.emit("keypress", ",", { sequence: "\x1E" });
+ assert.strictEqual(rli.line, "the quick b");
+ fi.emit("data", "\n");
+ rli.close();
+ });
+
+ // Clear the whole screen
+ it("should clear the whole screen", (done) => {
+ const { mustCall } = createCallCheckCtx(done);
+ const [rli, fi] = getInterface({ terminal: true, prompt: "" });
+ const lines = ["line 1", "line 2", "line 3"];
+ fi.emit("data", lines.join("\n"));
+ fi.emit("keypress", ".", { ctrl: true, name: "l" });
+ assertCursorRowsAndCols(rli, 0, 6);
+ rli.on(
+ "line",
+ mustCall((line) => {
+ assert.strictEqual(line, "line 3");
+ }),
+ );
+ fi.emit("data", "\n");
+ rli.close();
+ });
+
+ it("should treat wide characters as two columns", () => {
+ assert.strictEqual(getStringWidth("a"), 1);
+ assert.strictEqual(getStringWidth("あ"), 2);
+ assert.strictEqual(getStringWidth("č°˘"), 2);
+ assert.strictEqual(getStringWidth("ęł "), 2);
+ assert.strictEqual(getStringWidth(String.fromCodePoint(0x1f251)), 2);
+ assert.strictEqual(getStringWidth("abcde"), 5);
+ assert.strictEqual(getStringWidth("古池や"), 6);
+ assert.strictEqual(getStringWidth("ノード.js"), 9);
+ assert.strictEqual(getStringWidth("你弽"), 4);
+ assert.strictEqual(getStringWidth("안녕하세요"), 10);
+ assert.strictEqual(getStringWidth("A\ud83c\ude00BC"), 5);
+ assert.strictEqual(getStringWidth("👨‍👩‍👦‍👦"), 8);
+ assert.strictEqual(getStringWidth("🐕𐐷あ💻😀"), 9);
+ // TODO(BridgeAR): This should have a width of 4.
+ assert.strictEqual(getStringWidth("⓬⓪"), 2);
+ assert.strictEqual(getStringWidth("\u0301\u200D\u200E"), 0);
+ });
+
+ // // Check if vt control chars are stripped
+ // assert.strictEqual(stripVTControlCharacters('\u001b[31m> \u001b[39m'), '> ');
+ // assert.strictEqual(
+ // stripVTControlCharacters('\u001b[31m> \u001b[39m> '),
+ // '> > '
+ // );
+ // assert.strictEqual(stripVTControlCharacters('\u001b[31m\u001b[39m'), '');
+ // assert.strictEqual(stripVTControlCharacters('> '), '> ');
+ // assert.strictEqual(getStringWidth('\u001b[31m> \u001b[39m'), 2);
+ // assert.strictEqual(getStringWidth('\u001b[31m> \u001b[39m> '), 4);
+ // assert.strictEqual(getStringWidth('\u001b[31m\u001b[39m'), 0);
+ // assert.strictEqual(getStringWidth('> '), 2);
+
+ // // Check EventEmitter memory leak
+ // for (let i = 0; i < 12; i++) {
+ // const rl = readline.createInterface({
+ // input: process.stdin,
+ // output: process.stdout
+ // });
+ // rl.close();
+ // assert.strictEqual(isWarned(process.stdin._events), false);
+ // assert.strictEqual(isWarned(process.stdout._events), false);
+ // }
+
+ // [true, false].forEach((terminal) => {
+ // // Disable history
+ // {
+ // const [rli, fi] = getInterface({ terminal, historySize: 0 });
+ // assert.strictEqual(rli.historySize, 0);
+
+ // fi.emit('data', 'asdf\n');
+ // assert.deepStrictEqual(rli.history, []);
+ // rli.close();
+ // }
+
+ // // Default history size 30
+ // {
+ // const [rli, fi] = getInterface({ terminal });
+ // assert.strictEqual(rli.historySize, 30);
+
+ // fi.emit('data', 'asdf\n');
+ // assert.deepStrictEqual(rli.history, terminal ? ['asdf'] : []);
+ // rli.close();
+ // }
+
+ // // Sending a full line
+ // {
+ // const [rli, fi] = getInterface({ terminal });
+ // rli.on('line', mustCall((line) => {
+ // assert.strictEqual(line, 'asdf');
+ // }));
+ // fi.emit('data', 'asdf\n');
+ // }
+
+ // // Sending a blank line
+ // {
+ // const [rli, fi] = getInterface({ terminal });
+ // rli.on('line', mustCall((line) => {
+ // assert.strictEqual(line, '');
+ // }));
+ // fi.emit('data', '\n');
+ // }
+
+ // // Sending a single character with no newline and then a newline
+ // {
+ // const [rli, fi] = getInterface({ terminal });
+ // let called = false;
+ // rli.on('line', (line) => {
+ // called = true;
+ // assert.strictEqual(line, 'a');
+ // });
+ // fi.emit('data', 'a');
+ // assert.ok(!called);
+ // fi.emit('data', '\n');
+ // assert.ok(called);
+ // rli.close();
+ // }
+
+ // // Sending multiple newlines at once
+ // {
+ // const [rli, fi] = getInterface({ terminal });
+ // const expectedLines = ['foo', 'bar', 'baz'];
+ // rli.on('line', mustCall((line) => {
+ // assert.strictEqual(line, expectedLines.shift());
+ // }, expectedLines.length));
+ // fi.emit('data', `${expectedLines.join('\n')}\n`);
+ // rli.close();
+ // }
+
+ // // Sending multiple newlines at once that does not end with a new line
+ // {
+ // const [rli, fi] = getInterface({ terminal });
+ // const expectedLines = ['foo', 'bar', 'baz', 'bat'];
+ // rli.on('line', mustCall((line) => {
+ // assert.strictEqual(line, expectedLines.shift());
+ // }, expectedLines.length - 1));
+ // fi.emit('data', expectedLines.join('\n'));
+ // rli.close();
+ // }
+
+ // // Sending multiple newlines at once that does not end with a new(empty)
+ // // line and a `end` event
+ // {
+ // const [rli, fi] = getInterface({ terminal });
+ // const expectedLines = ['foo', 'bar', 'baz', ''];
+ // rli.on('line', mustCall((line) => {
+ // assert.strictEqual(line, expectedLines.shift());
+ // }, expectedLines.length - 1));
+ // rli.on('close', mustCall());
+ // fi.emit('data', expectedLines.join('\n'));
+ // fi.emit('end');
+ // rli.close();
+ // }
+
+ // // Sending a multi-byte utf8 char over multiple writes
+ // {
+ // const buf = Buffer.from('☎', 'utf8');
+ // const [rli, fi] = getInterface({ terminal });
+ // let callCount = 0;
+ // rli.on('line', (line) => {
+ // callCount++;
+ // assert.strictEqual(line, buf.toString('utf8'));
+ // });
+ // for (const i of buf) {
+ // fi.emit('data', Buffer.from([i]));
+ // }
+ // assert.strictEqual(callCount, 0);
+ // fi.emit('data', '\n');
+ // assert.strictEqual(callCount, 1);
+ // rli.close();
+ // }
+
+ // // Calling readline without `new`
+ // {
+ // const [rli, fi] = getInterface({ terminal });
+ // rli.on('line', mustCall((line) => {
+ // assert.strictEqual(line, 'asdf');
+ // }));
+ // fi.emit('data', 'asdf\n');
+ // rli.close();
+ // }
+
+ // // Calling the question callback
+ // {
+ // const [rli] = getInterface({ terminal });
+ // rli.question('foo?', mustCall((answer) => {
+ // assert.strictEqual(answer, 'bar');
+ // }));
+ // rli.write('bar\n');
+ // rli.close();
+ // }
+
+ // // Calling the question callback with abort signal
+ // {
+ // const [rli] = getInterface({ terminal });
+ // const { signal } = new AbortController();
+ // rli.question('foo?', { signal }, mustCall((answer) => {
+ // assert.strictEqual(answer, 'bar');
+ // }));
+ // rli.write('bar\n');
+ // rli.close();
+ // }
+
+ // // Calling the question multiple times
+ // {
+ // const [rli] = getInterface({ terminal });
+ // rli.question('foo?', mustCall((answer) => {
+ // assert.strictEqual(answer, 'baz');
+ // }));
+ // rli.question('bar?', mustNotCall(() => {
+ // }));
+ // rli.write('baz\n');
+ // rli.close();
+ // }
+
+ // // Calling the promisified question
+ // {
+ // const [rli] = getInterface({ terminal });
+ // const question = util.promisify(rli.question).bind(rli);
+ // question('foo?')
+ // .then(mustCall((answer) => {
+ // assert.strictEqual(answer, 'bar');
+ // }));
+ // rli.write('bar\n');
+ // rli.close();
+ // }
+
+ // // Calling the promisified question with abort signal
+ // {
+ // const [rli] = getInterface({ terminal });
+ // const question = util.promisify(rli.question).bind(rli);
+ // const { signal } = new AbortController();
+ // question('foo?', { signal })
+ // .then(mustCall((answer) => {
+ // assert.strictEqual(answer, 'bar');
+ // }));
+ // rli.write('bar\n');
+ // rli.close();
+ // }
+
+ // // Aborting a question
+ // {
+ // const ac = new AbortController();
+ // const signal = ac.signal;
+ // const [rli] = getInterface({ terminal });
+ // rli.on('line', mustCall((line) => {
+ // assert.strictEqual(line, 'bar');
+ // }));
+ // rli.question('hello?', { signal }, mustNotCall());
+ // ac.abort();
+ // rli.write('bar\n');
+ // rli.close();
+ // }
+
+ // // Aborting a promisified question
+ // {
+ // const ac = new AbortController();
+ // const signal = ac.signal;
+ // const [rli] = getInterface({ terminal });
+ // const question = util.promisify(rli.question).bind(rli);
+ // rli.on('line', mustCall((line) => {
+ // assert.strictEqual(line, 'bar');
+ // }));
+ // question('hello?', { signal })
+ // .then(mustNotCall())
+ // .catch(mustCall((error) => {
+ // assert.strictEqual(error.name, 'AbortError');
+ // }));
+ // ac.abort();
+ // rli.write('bar\n');
+ // rli.close();
+ // }
+
+ // // pre-aborted signal
+ // {
+ // const signal = AbortSignal.abort();
+ // const [rli] = getInterface({ terminal });
+ // rli.pause();
+ // rli.on('resume', mustNotCall());
+ // rli.question('hello?', { signal }, mustNotCall());
+ // rli.close();
+ // }
+
+ // // pre-aborted signal promisified question
+ // {
+ // const signal = AbortSignal.abort();
+ // const [rli] = getInterface({ terminal });
+ // const question = util.promisify(rli.question).bind(rli);
+ // rli.on('resume', mustNotCall());
+ // rli.pause();
+ // question('hello?', { signal })
+ // .then(mustNotCall())
+ // .catch(mustCall((error) => {
+ // assert.strictEqual(error.name, 'AbortError');
+ // }));
+ // rli.close();
+ // }
+
+ // // Call question after close
+ // {
+ // const [rli, fi] = getInterface({ terminal });
+ // rli.question('What\'s your name?', mustCall((name) => {
+ // assert.strictEqual(name, 'Node.js');
+ // rli.close();
+ // assert.throws(() => {
+ // rli.question('How are you?', mustNotCall());
+ // }, {
+ // name: 'Error',
+ // code: 'ERR_USE_AFTER_CLOSE'
+ // });
+ // assert.notStrictEqual(rli.getPrompt(), 'How are you?');
+ // }));
+ // fi.emit('data', 'Node.js\n');
+ // }
+
+ // // Call promisified question after close
+ // {
+ // const [rli, fi] = getInterface({ terminal });
+ // const question = util.promisify(rli.question).bind(rli);
+ // question('What\'s your name?').then(mustCall((name) => {
+ // assert.strictEqual(name, 'Node.js');
+ // rli.close();
+ // question('How are you?')
+ // .then(mustNotCall(), expectsError({
+ // code: 'ERR_USE_AFTER_CLOSE',
+ // name: 'Error'
+ // }));
+ // assert.notStrictEqual(rli.getPrompt(), 'How are you?');
+ // }));
+ // fi.emit('data', 'Node.js\n');
+ // }
+
+ // // Can create a new readline Interface with a null output argument
+ // {
+ // const [rli, fi] = getInterface({ output: null, terminal });
+ // rli.on('line', mustCall((line) => {
+ // assert.strictEqual(line, 'asdf');
+ // }));
+ // fi.emit('data', 'asdf\n');
+
+ // rli.setPrompt('ddd> ');
+ // rli.prompt();
+ // rli.write("really shouldn't be seeing this");
+ // rli.question('What do you think of node.js? ', (answer) => {
+ // console.log('Thank you for your valuable feedback:', answer);
+ // rli.close();
+ // });
+ // }
+
+ // // Calling the getPrompt method
+ // {
+ // const expectedPrompts = ['$ ', '> '];
+ // const [rli] = getInterface({ terminal });
+ // for (const prompt of expectedPrompts) {
+ // rli.setPrompt(prompt);
+ // assert.strictEqual(rli.getPrompt(), prompt);
+ // }
+ // }
+
+ // {
+ // const expected = terminal ?
+ // ['\u001b[1G', '\u001b[0J', '$ ', '\u001b[3G'] :
+ // ['$ '];
+
+ // const output = new Writable({
+ // write: mustCall((chunk, enc, cb) => {
+ // assert.strictEqual(chunk.toString(), expected.shift());
+ // cb();
+ // rl.close();
+ // }, expected.length)
+ // });
+
+ // const rl = readline.createInterface({
+ // input: new Readable({ read: mustCall() }),
+ // output,
+ // prompt: '$ ',
+ // terminal
+ // });
+
+ // rl.prompt();
+
+ // assert.strictEqual(rl.getPrompt(), '$ ');
+ // }
+
+ // {
+ // const fi = new FakeInput();
+ // assert.deepStrictEqual(fi.listeners(terminal ? 'keypress' : 'data'), []);
+ // }
+
+ // // Emit two line events when the delay
+ // // between \r and \n exceeds crlfDelay
+ // {
+ // const crlfDelay = 200;
+ // const [rli, fi] = getInterface({ terminal, crlfDelay });
+ // let callCount = 0;
+ // rli.on('line', () => {
+ // callCount++;
+ // });
+ // fi.emit('data', '\r');
+ // setTimeout(mustCall(() => {
+ // fi.emit('data', '\n');
+ // assert.strictEqual(callCount, 2);
+ // rli.close();
+ // }), crlfDelay + 10);
+ // }
+
+ // // For the purposes of the following tests, we do not care about the exact
+ // // value of crlfDelay, only that the behaviour conforms to what's expected.
+ // // Setting it to Infinity allows the test to succeed even under extreme
+ // // CPU stress.
+ // const crlfDelay = Infinity;
+
+ // // Set crlfDelay to `Infinity` is allowed
+ // {
+ // const delay = 200;
+ // const [rli, fi] = getInterface({ terminal, crlfDelay });
+ // let callCount = 0;
+ // rli.on('line', () => {
+ // callCount++;
+ // });
+ // fi.emit('data', '\r');
+ // setTimeout(mustCall(() => {
+ // fi.emit('data', '\n');
+ // assert.strictEqual(callCount, 1);
+ // rli.close();
+ // }), delay);
+ // }
+
+ // // Sending multiple newlines at once that does not end with a new line
+ // // and a `end` event(last line is)
+
+ // // \r\n should emit one line event, not two
+ // {
+ // const [rli, fi] = getInterface({ terminal, crlfDelay });
+ // const expectedLines = ['foo', 'bar', 'baz', 'bat'];
+ // rli.on('line', mustCall((line) => {
+ // assert.strictEqual(line, expectedLines.shift());
+ // }, expectedLines.length - 1));
+ // fi.emit('data', expectedLines.join('\r\n'));
+ // rli.close();
+ // }
+
+ // // \r\n should emit one line event when split across multiple writes.
+ // {
+ // const [rli, fi] = getInterface({ terminal, crlfDelay });
+ // const expectedLines = ['foo', 'bar', 'baz', 'bat'];
+ // let callCount = 0;
+ // rli.on('line', mustCall((line) => {
+ // assert.strictEqual(line, expectedLines[callCount]);
+ // callCount++;
+ // }, expectedLines.length));
+ // expectedLines.forEach((line) => {
+ // fi.emit('data', `${line}\r`);
+ // fi.emit('data', '\n');
+ // });
+ // rli.close();
+ // }
+
+ // // Emit one line event when the delay between \r and \n is
+ // // over the default crlfDelay but within the setting value.
+ // {
+ // const delay = 125;
+ // const [rli, fi] = getInterface({ terminal, crlfDelay });
+ // let callCount = 0;
+ // rli.on('line', () => callCount++);
+ // fi.emit('data', '\r');
+ // setTimeout(mustCall(() => {
+ // fi.emit('data', '\n');
+ // assert.strictEqual(callCount, 1);
+ // rli.close();
+ // }), delay);
+ // }
+ // });
+
+ // // Ensure that the _wordLeft method works even for large input
+ // {
+ // const input = new Readable({
+ // read() {
+ // this.push('\x1B[1;5D'); // CTRL + Left
+ // this.push(null);
+ // },
+ // });
+ // const output = new Writable({
+ // write: mustCall((data, encoding, cb) => {
+ // assert.strictEqual(rl.cursor, rl.line.length - 1);
+ // cb();
+ // }),
+ // });
+ // const rl = new readline.createInterface({
+ // input,
+ // output,
+ // terminal: true,
+ // });
+ // rl.line = `a${' '.repeat(1e6)}a`;
+ // rl.cursor = rl.line.length;
+ // }
+
+ // {
+ // const fi = new FakeInput();
+ // const signal = AbortSignal.abort();
+
+ // const rl = readline.createInterface({
+ // input: fi,
+ // output: fi,
+ // signal,
+ // });
+ // rl.on('close', mustCall());
+ // assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
+ // }
+
+ // {
+ // const fi = new FakeInput();
+ // const ac = new AbortController();
+ // const { signal } = ac;
+ // const rl = readline.createInterface({
+ // input: fi,
+ // output: fi,
+ // signal,
+ // });
+ // assert.strictEqual(getEventListeners(signal, 'abort').length, 1);
+ // rl.on('close', mustCall());
+ // ac.abort();
+ // assert.strictEqual(getEventListeners(signal, 'abort').length, 0);
+ // }
+
+ // {
+ // const fi = new FakeInput();
+ // const ac = new AbortController();
+ // const { signal } = ac;
+ // const rl = readline.createInterface({
+ // input: fi,
+ // output: fi,
+ // signal,
+ // });
+ // assert.strictEqual(getEventListeners(signal, "abort").length, 1);
+ // rl.close();
+ // assert.strictEqual(getEventListeners(signal, "abort").length, 0);
+ // }
+
+ // {
+ // // Constructor throws if signal is not an abort signal
+ // assert.throws(() => {
+ // readline.createInterface({
+ // input: new FakeInput(),
+ // signal: {},
+ // });
+ // }, {
+ // name: 'TypeError',
+ // code: 'ERR_INVALID_ARG_TYPE'
+ // });
+ // }
+});
+
+describe("readline.createInterface()", () => {
+ it("should emit line when input ends line", (done) => {
+ const createDone = createDoneDotAll(done);
+ const lineDone = createDone(2000);
+ const { mustCall } = createCallCheckCtx(createDone(2000));
+ const input = new PassThrough();
+ const rl = readline.createInterface({
+ terminal: true,
+ input: input,
+ });
+
+ rl.on(
+ "line",
+ mustCall((data) => {
+ assert.strictEqual(data, "abc");
+ lineDone();
+ }),
+ );
+
+ input.end("abc");
+ });
+
+ it("should not emit line when input ends without newline", (done) => {
+ const { mustNotCall } = createCallCheckCtx(done);
+
+ const input = new PassThrough();
+ const rl = readline.createInterface({
+ terminal: true,
+ input: input,
+ });
+
+ rl.on("line", mustNotCall("must not be called before newline"));
+ input.write("abc");
+ });
+
+ it("should read line by line", (done) => {
+ const createDone = createDoneDotAll(done);
+ const { mustCall } = createCallCheckCtx(createDone(3000));
+ const lineDone = createDone(2000);
+ const input = new PassThrough();
+ const rl = readline.createInterface({
+ terminal: true,
+ input: input,
+ });
+
+ rl.on(
+ "line",
+ mustCall((data) => {
+ assert.strictEqual(data, "abc");
+ lineDone();
+ }),
+ );
+
+ input.write("abc\n");
+ });
+
+ it("should respond to home and end sequences for common pttys ", () => {
+ const input = new PassThrough();
+ const rl = readline.createInterface({
+ terminal: true,
+ input: input,
+ });
+
+ rl.write("foo");
+ assert.strictEqual(rl.cursor, 3);
+
+ const key = {
+ xterm: {
+ home: ["\x1b[H", { ctrl: true, name: "a" }],
+ end: ["\x1b[F", { ctrl: true, name: "e" }],
+ },
+ gnome: {
+ home: ["\x1bOH", { ctrl: true, name: "a" }],
+ end: ["\x1bOF", { ctrl: true, name: "e" }],
+ },
+ rxvt: {
+ home: ["\x1b[7", { ctrl: true, name: "a" }],
+ end: ["\x1b[8", { ctrl: true, name: "e" }],
+ },
+ putty: {
+ home: ["\x1b[1~", { ctrl: true, name: "a" }],
+ end: ["\x1b[>~", { ctrl: true, name: "e" }],
+ },
+ };
+
+ [key.xterm, key.gnome, key.rxvt, key.putty].forEach((key) => {
+ rl.write.apply(rl, key.home);
+ assert.strictEqual(rl.cursor, 0);
+ rl.write.apply(rl, key.end);
+ assert.strictEqual(rl.cursor, 3);
+ });
+ });
+
+ it("should allow for cursor movement with meta-f and meta-b", () => {
+ const input = new PassThrough();
+ const rl = readline.createInterface({
+ terminal: true,
+ input: input,
+ });
+
+ const key = {
+ xterm: {
+ home: ["\x1b[H", { ctrl: true, name: "a" }],
+ metab: ["\x1bb", { meta: true, name: "b" }],
+ metaf: ["\x1bf", { meta: true, name: "f" }],
+ },
+ };
+
+ rl.write("foo bar.hop/zoo");
+ rl.write.apply(rl, key.xterm.home);
+ [
+ { cursor: 4, key: key.xterm.metaf },
+ { cursor: 7, key: key.xterm.metaf },
+ { cursor: 8, key: key.xterm.metaf },
+ { cursor: 11, key: key.xterm.metaf },
+ { cursor: 12, key: key.xterm.metaf },
+ { cursor: 15, key: key.xterm.metaf },
+ { cursor: 12, key: key.xterm.metab },
+ { cursor: 11, key: key.xterm.metab },
+ { cursor: 8, key: key.xterm.metab },
+ { cursor: 7, key: key.xterm.metab },
+ { cursor: 4, key: key.xterm.metab },
+ { cursor: 0, key: key.xterm.metab },
+ ].forEach(function (action) {
+ rl.write.apply(rl, action.key);
+ assert.strictEqual(rl.cursor, action.cursor);
+ });
+ });
+
+ it("should properly allow for cursor movement with meta-d", () => {
+ const input = new PassThrough();
+ const rl = readline.createInterface({
+ terminal: true,
+ input: input,
+ });
+
+ const key = {
+ xterm: {
+ home: ["\x1b[H", { ctrl: true, name: "a" }],
+ metad: ["\x1bd", { meta: true, name: "d" }],
+ },
+ };
+
+ rl.write("foo bar.hop/zoo");
+ rl.write.apply(rl, key.xterm.home);
+ ["bar.hop/zoo", ".hop/zoo", "hop/zoo", "/zoo", "zoo", ""].forEach(function (
+ expectedLine,
+ ) {
+ rl.write.apply(rl, key.xterm.metad);
+ assert.strictEqual(rl.cursor, 0);
+ assert.strictEqual(rl.line, expectedLine);
+ });
+ });
+
+ // TODO: Actual pseudo-tty test
+ // it("should operate correctly when process.env.DUMB is set", () => {
+ // process.env.TERM = "dumb";
+ // const rl = readline.createInterface({
+ // input: process.stdin,
+ // output: process.stdout,
+ // });
+ // rl.write("text");
+ // rl.write(null, { ctrl: true, name: "u" });
+ // rl.write(null, { name: "return" });
+ // rl.write("text");
+ // rl.write(null, { name: "backspace" });
+ // rl.write(null, { name: "escape" });
+ // rl.write(null, { name: "enter" });
+ // rl.write("text");
+ // rl.write(null, { ctrl: true, name: "c" });
+ // });
+});
diff --git a/test/bun.js/readline_promises.node.test.ts b/test/bun.js/readline_promises.node.test.ts
new file mode 100644
index 000000000..eadd289e3
--- /dev/null
+++ b/test/bun.js/readline_promises.node.test.ts
@@ -0,0 +1,55 @@
+import { describe, it } from "bun:test";
+import readlinePromises from "node:readline/promises";
+import { EventEmitter } from "node:events";
+import {
+ createDoneDotAll,
+ createCallCheckCtx,
+ assert,
+} from "./node-test-helpers";
+
+// ----------------------------------------------------------------------------
+// Helpers
+// ----------------------------------------------------------------------------
+
+class FakeInput extends EventEmitter {
+ output = "";
+ resume() {}
+ pause() {}
+ write(data) {
+ this.output += data;
+ }
+ end() {}
+ reset() {
+ this.output = "";
+ }
+}
+
+// ----------------------------------------------------------------------------
+// Tests
+// ----------------------------------------------------------------------------
+
+describe("readline/promises.createInterface()", () => {
+ it("should throw an error when failed completion", (done) => {
+ const createDone = createDoneDotAll(done);
+ const { mustCall, mustNotCall } = createCallCheckCtx(createDone());
+
+ const fi = new FakeInput();
+ const rli = new readlinePromises.Interface({
+ input: fi,
+ output: fi,
+ terminal: true,
+ completer: mustCall(() => Promise.reject(new Error("message"))),
+ });
+
+ rli.on("line", mustNotCall());
+ fi.emit("data", "\t");
+ const outCheckDone = createDone();
+ process.nextTick(() => {
+ console.log("output", fi.output);
+ assert.match(fi.output, /^Tab completion error/);
+ fi.reset();
+ outCheckDone();
+ });
+ rli.close();
+ });
+});