mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 10:58:56 +00:00
2774 lines
76 KiB
TypeScript
2774 lines
76 KiB
TypeScript
// Hardcoded module "node:readline"
|
|
// 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
|
|
// ----------------------------------------------------------------------------
|
|
const EventEmitter = require("node:events");
|
|
const { StringDecoder } = require("node:string_decoder");
|
|
const { promisify } = require("internal/promisify");
|
|
|
|
const {
|
|
validateFunction,
|
|
validateAbortSignal,
|
|
validateArray,
|
|
validateString,
|
|
validateBoolean,
|
|
validateInteger,
|
|
validateUint32,
|
|
validateNumber,
|
|
} = require("internal/validators");
|
|
|
|
const internalGetStringWidth = $newZigFunction("string.zig", "String.jsGetStringWidth", 1);
|
|
|
|
const PromiseReject = Promise.reject;
|
|
|
|
var isWritable;
|
|
|
|
var { inspect } = Bun;
|
|
var debug = process.env.BUN_JS_DEBUG ? console.log : () => {};
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Section: Preamble
|
|
// ----------------------------------------------------------------------------
|
|
|
|
const SymbolAsyncIterator = Symbol.asyncIterator;
|
|
const SymbolIterator = Symbol.iterator;
|
|
const SymbolFor = Symbol.for;
|
|
const SymbolReplace = Symbol.replace;
|
|
const ArrayFrom = Array.from;
|
|
const ArrayPrototypeFilter = Array.prototype.filter;
|
|
const ArrayPrototypeSort = Array.prototype.sort;
|
|
const ArrayPrototypeIndexOf = Array.prototype.indexOf;
|
|
const ArrayPrototypeJoin = Array.prototype.join;
|
|
const ArrayPrototypeMap = Array.prototype.map;
|
|
const ArrayPrototypePop = Array.prototype.pop;
|
|
const ArrayPrototypePush = Array.prototype.push;
|
|
const ArrayPrototypeSlice = Array.prototype.slice;
|
|
const ArrayPrototypeSplice = Array.prototype.splice;
|
|
const ArrayPrototypeReverse = Array.prototype.reverse;
|
|
const ArrayPrototypeShift = Array.prototype.shift;
|
|
const ArrayPrototypeUnshift = Array.prototype.unshift;
|
|
const RegExpPrototypeExec = RegExp.prototype.exec;
|
|
const RegExpPrototypeSymbolReplace = RegExp.prototype[SymbolReplace];
|
|
const StringFromCharCode = String.fromCharCode;
|
|
const StringPrototypeCharCodeAt = String.prototype.charCodeAt;
|
|
const StringPrototypeCodePointAt = String.prototype.codePointAt;
|
|
const StringPrototypeSlice = String.prototype.slice;
|
|
const StringPrototypeToLowerCase = String.prototype.toLowerCase;
|
|
const StringPrototypeEndsWith = String.prototype.endsWith;
|
|
const StringPrototypeRepeat = String.prototype.repeat;
|
|
const StringPrototypeStartsWith = String.prototype.startsWith;
|
|
const StringPrototypeTrim = String.prototype.trim;
|
|
const StringPrototypeNormalize = String.prototype.normalize;
|
|
const NumberIsNaN = Number.isNaN;
|
|
const NumberIsFinite = Number.isFinite;
|
|
const NumberIsInteger = Number.isInteger;
|
|
const NumberMAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER;
|
|
const NumberMIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER;
|
|
const MathCeil = Math.ceil;
|
|
const MathFloor = Math.floor;
|
|
const MathMax = Math.max;
|
|
const DateNow = Date.now;
|
|
const FunctionPrototype = Function.prototype;
|
|
const StringPrototype = String.prototype;
|
|
const StringPrototypeSymbolIterator = StringPrototype[SymbolIterator];
|
|
const StringIteratorPrototypeNext = StringPrototypeSymbolIterator.$call("").next;
|
|
const ObjectSetPrototypeOf = Object.setPrototypeOf;
|
|
const ObjectDefineProperty = Object.defineProperty;
|
|
const ObjectDefineProperties = Object.defineProperties;
|
|
const ObjectFreeze = Object.freeze;
|
|
const ObjectAssign = Object.assign;
|
|
const ObjectCreate = Object.create;
|
|
const ObjectKeys = Object.keys;
|
|
const 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 the number of columns required to display the given string.
|
|
*/
|
|
var getStringWidth = function getStringWidth(str, removeControlChars = true) {
|
|
if (removeControlChars) str = stripVTControlCharacters(str);
|
|
str = StringPrototypeNormalize.$call(str, "NFC");
|
|
return internalGetStringWidth(str);
|
|
};
|
|
|
|
// 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, "");
|
|
}
|
|
|
|
// Constants
|
|
|
|
const kUTF16SurrogateThreshold = 0x10000; // 2 ** 16
|
|
const kEscape = "\x1b";
|
|
const kSubstringSearch = Symbol("kSubstringSearch");
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// 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: string, i: number) {
|
|
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) {
|
|
let ch = yield;
|
|
let s = ch;
|
|
let escaped = false;
|
|
const key: {
|
|
sequence: string | null;
|
|
name?: string;
|
|
code?: string;
|
|
ctrl: boolean;
|
|
meta: boolean;
|
|
shift: boolean;
|
|
} = {
|
|
sequence: null,
|
|
name: undefined,
|
|
ctrl: false,
|
|
meta: false,
|
|
shift: false,
|
|
};
|
|
|
|
if (ch === kEscape) {
|
|
escaped = true;
|
|
s += ch = yield;
|
|
|
|
if (ch === kEscape) {
|
|
s += ch = yield;
|
|
}
|
|
}
|
|
|
|
if (escaped && (ch === "O" || ch === "[")) {
|
|
// ANSI escape sequence
|
|
let code = ch;
|
|
let modifier = 0;
|
|
|
|
if (ch === "O") {
|
|
// ESC O letter
|
|
// ESC O modifier letter
|
|
s += ch = yield;
|
|
|
|
if (ch >= "0" && ch <= "9") {
|
|
modifier = (ch >> 0) - 1;
|
|
s += ch = yield;
|
|
}
|
|
|
|
code += ch;
|
|
} else if (ch === "[") {
|
|
// ESC [ letter
|
|
// ESC [ modifier letter
|
|
// ESC [ [ modifier letter
|
|
// ESC [ [ num char
|
|
s += ch = yield;
|
|
|
|
if (ch === "[") {
|
|
// \x1b[[A
|
|
// ^--- escape codes might have a second bracket
|
|
code += ch;
|
|
s += ch = yield;
|
|
}
|
|
|
|
/*
|
|
* Here and later we try to buffer just enough data to get
|
|
* a complete ascii sequence.
|
|
*
|
|
* We have basically two classes of ascii characters to process:
|
|
*
|
|
*
|
|
* 1. `\x1b[24;5~` should be parsed as { code: '[24~', modifier: 5 }
|
|
*
|
|
* This particular example is featuring Ctrl+F12 in xterm.
|
|
*
|
|
* - `;5` part is optional, e.g. it could be `\x1b[24~`
|
|
* - first part can contain one or two digits
|
|
* - there is also special case when there can be 3 digits
|
|
* but without modifier. They are the case of paste bracket mode
|
|
*
|
|
* So the generic regexp is like /^(?:\d\d?(;\d)?[~^$]|\d{3}~)$/
|
|
*
|
|
*
|
|
* 2. `\x1b[1;5H` should be parsed as { code: '[H', modifier: 5 }
|
|
*
|
|
* This particular example is featuring Ctrl+Home in xterm.
|
|
*
|
|
* - `1;5` part is optional, e.g. it could be `\x1b[H`
|
|
* - `1;` part is optional, e.g. it could be `\x1b[5H`
|
|
*
|
|
* So the generic regexp is like /^((\d;)?\d)?[A-Za-z]$/
|
|
*
|
|
*/
|
|
const cmdStart = s.length - 1;
|
|
|
|
// Skip one or two leading digits
|
|
if (ch >= "0" && ch <= "9") {
|
|
s += ch = yield;
|
|
|
|
if (ch >= "0" && ch <= "9") {
|
|
s += ch = yield;
|
|
|
|
if (ch >= "0" && ch <= "9") {
|
|
s += ch = yield;
|
|
}
|
|
}
|
|
}
|
|
|
|
// skip modifier
|
|
if (ch === ";") {
|
|
s += ch = yield;
|
|
|
|
if (ch >= "0" && ch <= "9") {
|
|
s += yield;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* We buffered enough data, now trying to extract code
|
|
* and modifier from it
|
|
*/
|
|
const cmd = StringPrototypeSlice.$call(s, cmdStart);
|
|
let match;
|
|
|
|
if ((match = RegExpPrototypeExec.$call(/^(?:(\d\d?)(?:;(\d))?([~^$])|(\d{3}~))$/, cmd))) {
|
|
if (match[4]) {
|
|
code += match[4];
|
|
} else {
|
|
code += match[1] + match[3];
|
|
modifier = (match[2] || 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
|
|
key.ctrl = !!(modifier & 4);
|
|
key.meta = !!(modifier & 10);
|
|
key.shift = !!(modifier & 1);
|
|
key.code = code;
|
|
|
|
// Parse the key itself
|
|
switch (code) {
|
|
/* xterm/gnome ESC [ letter (with modifier) */
|
|
case "[P":
|
|
key.name = "f1";
|
|
break;
|
|
case "[Q":
|
|
key.name = "f2";
|
|
break;
|
|
case "[R":
|
|
key.name = "f3";
|
|
break;
|
|
case "[S":
|
|
key.name = "f4";
|
|
break;
|
|
|
|
/* xterm/gnome ESC O letter (without modifier) */
|
|
case "OP":
|
|
key.name = "f1";
|
|
break;
|
|
case "OQ":
|
|
key.name = "f2";
|
|
break;
|
|
case "OR":
|
|
key.name = "f3";
|
|
break;
|
|
case "OS":
|
|
key.name = "f4";
|
|
break;
|
|
|
|
/* xterm/rxvt ESC [ number ~ */
|
|
case "[11~":
|
|
key.name = "f1";
|
|
break;
|
|
case "[12~":
|
|
key.name = "f2";
|
|
break;
|
|
case "[13~":
|
|
key.name = "f3";
|
|
break;
|
|
case "[14~":
|
|
key.name = "f4";
|
|
break;
|
|
|
|
/* paste bracket mode */
|
|
case "[200~":
|
|
key.name = "paste-start";
|
|
break;
|
|
case "[201~":
|
|
key.name = "paste-end";
|
|
break;
|
|
|
|
/* from Cygwin and used in libuv */
|
|
case "[[A":
|
|
key.name = "f1";
|
|
break;
|
|
case "[[B":
|
|
key.name = "f2";
|
|
break;
|
|
case "[[C":
|
|
key.name = "f3";
|
|
break;
|
|
case "[[D":
|
|
key.name = "f4";
|
|
break;
|
|
case "[[E":
|
|
key.name = "f5";
|
|
break;
|
|
|
|
/* common */
|
|
case "[15~":
|
|
key.name = "f5";
|
|
break;
|
|
case "[17~":
|
|
key.name = "f6";
|
|
break;
|
|
case "[18~":
|
|
key.name = "f7";
|
|
break;
|
|
case "[19~":
|
|
key.name = "f8";
|
|
break;
|
|
case "[20~":
|
|
key.name = "f9";
|
|
break;
|
|
case "[21~":
|
|
key.name = "f10";
|
|
break;
|
|
case "[23~":
|
|
key.name = "f11";
|
|
break;
|
|
case "[24~":
|
|
key.name = "f12";
|
|
break;
|
|
|
|
/* xterm ESC [ letter */
|
|
case "[A":
|
|
key.name = "up";
|
|
break;
|
|
case "[B":
|
|
key.name = "down";
|
|
break;
|
|
case "[C":
|
|
key.name = "right";
|
|
break;
|
|
case "[D":
|
|
key.name = "left";
|
|
break;
|
|
case "[E":
|
|
key.name = "clear";
|
|
break;
|
|
case "[F":
|
|
key.name = "end";
|
|
break;
|
|
case "[H":
|
|
key.name = "home";
|
|
break;
|
|
|
|
/* xterm/gnome ESC O letter */
|
|
case "OA":
|
|
key.name = "up";
|
|
break;
|
|
case "OB":
|
|
key.name = "down";
|
|
break;
|
|
case "OC":
|
|
key.name = "right";
|
|
break;
|
|
case "OD":
|
|
key.name = "left";
|
|
break;
|
|
case "OE":
|
|
key.name = "clear";
|
|
break;
|
|
case "OF":
|
|
key.name = "end";
|
|
break;
|
|
case "OH":
|
|
key.name = "home";
|
|
break;
|
|
|
|
/* xterm/rxvt ESC [ number ~ */
|
|
case "[1~":
|
|
key.name = "home";
|
|
break;
|
|
case "[2~":
|
|
key.name = "insert";
|
|
break;
|
|
case "[3~":
|
|
key.name = "delete";
|
|
break;
|
|
case "[4~":
|
|
key.name = "end";
|
|
break;
|
|
case "[5~":
|
|
key.name = "pageup";
|
|
break;
|
|
case "[6~":
|
|
key.name = "pagedown";
|
|
break;
|
|
|
|
/* putty */
|
|
case "[[5~":
|
|
key.name = "pageup";
|
|
break;
|
|
case "[[6~":
|
|
key.name = "pagedown";
|
|
break;
|
|
|
|
/* rxvt */
|
|
case "[7~":
|
|
key.name = "home";
|
|
break;
|
|
case "[8~":
|
|
key.name = "end";
|
|
break;
|
|
|
|
/* rxvt keys with modifiers */
|
|
case "[a":
|
|
key.name = "up";
|
|
key.shift = true;
|
|
break;
|
|
case "[b":
|
|
key.name = "down";
|
|
key.shift = true;
|
|
break;
|
|
case "[c":
|
|
key.name = "right";
|
|
key.shift = true;
|
|
break;
|
|
case "[d":
|
|
key.name = "left";
|
|
key.shift = true;
|
|
break;
|
|
case "[e":
|
|
key.name = "clear";
|
|
key.shift = true;
|
|
break;
|
|
|
|
case "[2$":
|
|
key.name = "insert";
|
|
key.shift = true;
|
|
break;
|
|
case "[3$":
|
|
key.name = "delete";
|
|
key.shift = true;
|
|
break;
|
|
case "[5$":
|
|
key.name = "pageup";
|
|
key.shift = true;
|
|
break;
|
|
case "[6$":
|
|
key.name = "pagedown";
|
|
key.shift = true;
|
|
break;
|
|
case "[7$":
|
|
key.name = "home";
|
|
key.shift = true;
|
|
break;
|
|
case "[8$":
|
|
key.name = "end";
|
|
key.shift = true;
|
|
break;
|
|
|
|
case "Oa":
|
|
key.name = "up";
|
|
key.ctrl = true;
|
|
break;
|
|
case "Ob":
|
|
key.name = "down";
|
|
key.ctrl = true;
|
|
break;
|
|
case "Oc":
|
|
key.name = "right";
|
|
key.ctrl = true;
|
|
break;
|
|
case "Od":
|
|
key.name = "left";
|
|
key.ctrl = true;
|
|
break;
|
|
case "Oe":
|
|
key.name = "clear";
|
|
key.ctrl = true;
|
|
break;
|
|
|
|
case "[2^":
|
|
key.name = "insert";
|
|
key.ctrl = true;
|
|
break;
|
|
case "[3^":
|
|
key.name = "delete";
|
|
key.ctrl = true;
|
|
break;
|
|
case "[5^":
|
|
key.name = "pageup";
|
|
key.ctrl = true;
|
|
break;
|
|
case "[6^":
|
|
key.name = "pagedown";
|
|
key.ctrl = true;
|
|
break;
|
|
case "[7^":
|
|
key.name = "home";
|
|
key.ctrl = true;
|
|
break;
|
|
case "[8^":
|
|
key.name = "end";
|
|
key.ctrl = true;
|
|
break;
|
|
|
|
/* misc. */
|
|
case "[Z":
|
|
key.name = "tab";
|
|
key.shift = true;
|
|
break;
|
|
default:
|
|
key.name = "undefined";
|
|
break;
|
|
}
|
|
} else if (ch === "\r") {
|
|
// carriage return
|
|
key.name = "return";
|
|
key.meta = escaped;
|
|
} else if (ch === "\n") {
|
|
// Enter, should have been called linefeed
|
|
key.name = "enter";
|
|
key.meta = escaped;
|
|
} else if (ch === "\t") {
|
|
// tab
|
|
key.name = "tab";
|
|
key.meta = escaped;
|
|
} else if (ch === "\b" || ch === "\x7f") {
|
|
// backspace or ctrl+h
|
|
key.name = "backspace";
|
|
key.meta = escaped;
|
|
} else if (ch === kEscape) {
|
|
// escape key
|
|
key.name = "escape";
|
|
key.meta = escaped;
|
|
} else if (ch === " ") {
|
|
key.name = "space";
|
|
key.meta = escaped;
|
|
} else if (!escaped && ch <= "\x1a") {
|
|
// ctrl+letter
|
|
key.name = StringFromCharCode(StringPrototypeCharCodeAt.$call(ch, 0) + StringPrototypeCharCodeAt.$call("a", 0) - 1); // prettier-ignore
|
|
key.ctrl = true;
|
|
} else if (RegExpPrototypeExec.$call(/^[0-9A-Za-z]$/, ch) !== null) {
|
|
// Letter, number, shift+letter
|
|
key.name = StringPrototypeToLowerCase.$call(ch);
|
|
key.shift = RegExpPrototypeExec.$call(/^[A-Z]$/, ch) !== null;
|
|
key.meta = escaped;
|
|
} else if (escaped) {
|
|
// Escape sequence timeout
|
|
key.name = ch.length ? undefined : "escape";
|
|
key.meta = true;
|
|
}
|
|
|
|
key.sequence = s;
|
|
|
|
if (s.length !== 0 && (key.name !== undefined || escaped)) {
|
|
/* Named character or sequence */
|
|
stream.emit("keypress", escaped ? undefined : s, key);
|
|
} else if (charLengthAt(s, 0) === s.length) {
|
|
/* Single unnamed character, e.g. "." */
|
|
stream.emit("keypress", s, key);
|
|
}
|
|
/* Unrecognized or broken escape sequence, don't emit anything */
|
|
}
|
|
}
|
|
|
|
// This runs in O(n log n).
|
|
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 $ERR_INVALID_ARG_VALUE("x", x);
|
|
if (NumberIsNaN(y)) throw $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 $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 = SymbolFor("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 $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 $ERR_INVALID_ARG_VALUE("completer", completer);
|
|
}
|
|
|
|
if (history === undefined) {
|
|
history = [];
|
|
} else {
|
|
validateArray(history, "history");
|
|
}
|
|
|
|
if (historySize === undefined) {
|
|
historySize = kHistorySize;
|
|
}
|
|
|
|
validateNumber(historySize, "historySize", 0);
|
|
|
|
// Backwards compat; check the isTTY prop of the output stream
|
|
// when `terminal` was not specified
|
|
if (terminal === undefined && !(output == null)) {
|
|
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) {
|
|
this[kDecoder] = new StringDecoder("utf8");
|
|
input.on("data", this[kOnData]);
|
|
input.on("end", this[kOnEnd]);
|
|
this.once("close", this[kOnSelfCloseWithoutTerminal]);
|
|
} 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();
|
|
}
|
|
InterfaceConstructor.prototype = {};
|
|
|
|
ObjectSetPrototypeOf(InterfaceConstructor.prototype, EventEmitter.prototype);
|
|
// ObjectSetPrototypeOf(InterfaceConstructor, EventEmitter);
|
|
|
|
var _Interface = class Interface extends InterfaceConstructor {
|
|
// 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) {
|
|
const wasInRawMode = this.input.isRaw;
|
|
|
|
var setRawMode = this.input.setRawMode;
|
|
if (typeof setRawMode === "function") {
|
|
setRawMode.$call(this.input, 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 $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;
|
|
lineEnding.lastIndex = 0; // Start the search from the beginning of the string.
|
|
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 = MathMax.$apply(null, 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, sequence: keySeq } = 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) {
|
|
// Erase state of previous searches.
|
|
lineEnding.lastIndex = 0;
|
|
let nextMatch;
|
|
// Keep track of the end of the last match.
|
|
let lastIndex = 0;
|
|
while ((nextMatch = RegExpPrototypeExec.$call(lineEnding, s)) !== null) {
|
|
this[kInsertString](StringPrototypeSlice.$call(s, lastIndex, nextMatch.index));
|
|
({ lastIndex } = lineEnding);
|
|
this[kLine]();
|
|
// Restore lastIndex as the call to kLine could have mutated it.
|
|
lineEnding.lastIndex = lastIndex;
|
|
}
|
|
// This ensures that the last line is written if it doesn't end in a newline.
|
|
// Note that the last line may be the first line, in which case this still works.
|
|
this[kInsertString](StringPrototypeSlice.$call(s, lastIndex));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
}
|
|
Interface.prototype = {};
|
|
|
|
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;
|
|
}
|
|
|
|
var signal = options?.signal;
|
|
if (signal) {
|
|
validateAbortSignal(signal, "options.signal");
|
|
if (signal.aborted) {
|
|
return;
|
|
}
|
|
|
|
var onAbort = () => {
|
|
this[kQuestionCancel]();
|
|
};
|
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
var cleanup = () => {
|
|
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;
|
|
}
|
|
|
|
var signal = options?.signal;
|
|
|
|
if (signal && signal.aborted) {
|
|
return PromiseReject($makeAbortError(undefined, { cause: signal.reason }));
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
var cb = resolve;
|
|
if (signal) {
|
|
var onAbort = () => {
|
|
reject($makeAbortError(undefined, { cause: signal.reason }));
|
|
};
|
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
cb = answer => {
|
|
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]: {
|
|
get() {
|
|
return this._setRawMode;
|
|
},
|
|
},
|
|
[kOnLine]: {
|
|
get() {
|
|
return this._onLine;
|
|
},
|
|
},
|
|
[kWriteToOutput]: {
|
|
get() {
|
|
return this._writeToOutput;
|
|
},
|
|
},
|
|
[kAddHistory]: {
|
|
get() {
|
|
return this._addHistory;
|
|
},
|
|
},
|
|
[kRefreshLine]: {
|
|
get() {
|
|
return this._refreshLine;
|
|
},
|
|
},
|
|
[kNormalWrite]: {
|
|
get() {
|
|
return this._normalWrite;
|
|
},
|
|
},
|
|
[kInsertString]: {
|
|
get() {
|
|
return this._insertString;
|
|
},
|
|
},
|
|
[kTabComplete]: {
|
|
get() {
|
|
return this._tabComplete;
|
|
},
|
|
},
|
|
[kWordLeft]: {
|
|
get() {
|
|
return this._wordLeft;
|
|
},
|
|
},
|
|
[kWordRight]: {
|
|
get() {
|
|
return this._wordRight;
|
|
},
|
|
},
|
|
[kDeleteLeft]: {
|
|
get() {
|
|
return this._deleteLeft;
|
|
},
|
|
},
|
|
[kDeleteRight]: {
|
|
get() {
|
|
return this._deleteRight;
|
|
},
|
|
},
|
|
[kDeleteWordLeft]: {
|
|
get() {
|
|
return this._deleteWordLeft;
|
|
},
|
|
},
|
|
[kDeleteWordRight]: {
|
|
get() {
|
|
return this._deleteWordRight;
|
|
},
|
|
},
|
|
[kDeleteLineLeft]: {
|
|
get() {
|
|
return this._deleteLineLeft;
|
|
},
|
|
},
|
|
[kDeleteLineRight]: {
|
|
get() {
|
|
return this._deleteLineRight;
|
|
},
|
|
},
|
|
[kLine]: {
|
|
get() {
|
|
return this._line;
|
|
},
|
|
},
|
|
[kHistoryNext]: {
|
|
get() {
|
|
return this._historyNext;
|
|
},
|
|
},
|
|
[kHistoryPrev]: {
|
|
get() {
|
|
return this._historyPrev;
|
|
},
|
|
},
|
|
[kGetDisplayPos]: {
|
|
get() {
|
|
return this._getDisplayPos;
|
|
},
|
|
},
|
|
[kMoveCursor]: {
|
|
get() {
|
|
return this._moveCursor;
|
|
},
|
|
},
|
|
[kTtyWrite]: {
|
|
get() {
|
|
return this._ttyWrite;
|
|
},
|
|
},
|
|
|
|
// Defining proxies for the internal instance properties for backward
|
|
// compatibility.
|
|
_decoder: {
|
|
get() {
|
|
return this[kDecoder];
|
|
},
|
|
set(value) {
|
|
this[kDecoder] = value;
|
|
},
|
|
},
|
|
_line_buffer: {
|
|
get() {
|
|
return this[kLine_buffer];
|
|
},
|
|
set(value) {
|
|
this[kLine_buffer] = value;
|
|
},
|
|
},
|
|
_oldPrompt: {
|
|
get() {
|
|
return this[kOldPrompt];
|
|
},
|
|
set(value) {
|
|
this[kOldPrompt] = value;
|
|
},
|
|
},
|
|
_previousKey: {
|
|
get() {
|
|
return this[kPreviousKey];
|
|
},
|
|
set(value) {
|
|
this[kPreviousKey] = value;
|
|
},
|
|
},
|
|
_prompt: {
|
|
get() {
|
|
return this[kPrompt];
|
|
},
|
|
set(value) {
|
|
this[kPrompt] = value;
|
|
},
|
|
},
|
|
_questionCallback: {
|
|
get() {
|
|
return this[kQuestionCallback];
|
|
},
|
|
set(value) {
|
|
this[kQuestionCallback] = value;
|
|
},
|
|
},
|
|
_sawKeyPress: {
|
|
get() {
|
|
return this[kSawKeyPress];
|
|
},
|
|
set(value) {
|
|
this[kSawKeyPress] = value;
|
|
},
|
|
},
|
|
_sawReturnAt: {
|
|
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 (key.ctrl) {
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
class Readline {
|
|
#autoCommit = false;
|
|
#stream;
|
|
#todo = [];
|
|
|
|
constructor(stream, options = undefined) {
|
|
isWritable ??= require("node:stream").isWritable;
|
|
if (!isWritable(stream)) throw $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() {
|
|
const { resolve, promise } = $newPromiseCapability(Promise);
|
|
this.#stream.write(ArrayPrototypeJoin.$call(this.#todo, ""), resolve);
|
|
this.#todo = [];
|
|
|
|
return promise;
|
|
}
|
|
|
|
/**
|
|
* Clears the internal list of pending actions without sending it to the
|
|
* associated `stream`.
|
|
* @returns {Readline} this
|
|
*/
|
|
rollback() {
|
|
this.#todo = [];
|
|
return this;
|
|
}
|
|
}
|
|
|
|
var PromisesInterface = 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) {
|
|
var signal = options?.signal;
|
|
if (signal) {
|
|
validateAbortSignal(signal, "options.signal");
|
|
if (signal.aborted) {
|
|
return PromiseReject($makeAbortError(undefined, { cause: signal.reason }));
|
|
}
|
|
}
|
|
const { promise, resolve, reject } = $newPromiseCapability(Promise);
|
|
var cb = resolve;
|
|
if (options?.signal) {
|
|
var onAbort = () => {
|
|
this[kQuestionCancel]();
|
|
reject($makeAbortError(undefined, { cause: signal.reason }));
|
|
};
|
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
cb = answer => {
|
|
signal.removeEventListener("abort", onAbort);
|
|
resolve(answer);
|
|
};
|
|
}
|
|
this[kQuestion](query, cb);
|
|
return promise;
|
|
}
|
|
};
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Exports
|
|
// ----------------------------------------------------------------------------
|
|
export default {
|
|
Interface,
|
|
clearLine,
|
|
clearScreenDown,
|
|
createInterface,
|
|
cursorTo,
|
|
emitKeypressEvents,
|
|
moveCursor,
|
|
promises: {
|
|
Readline,
|
|
Interface: PromisesInterface,
|
|
createInterface(input, output, completer, terminal) {
|
|
return new PromisesInterface(input, output, completer, terminal);
|
|
},
|
|
},
|
|
|
|
[SymbolFor("__BUN_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED__")]: {
|
|
CSI,
|
|
utils: {
|
|
getStringWidth,
|
|
stripVTControlCharacters,
|
|
},
|
|
},
|
|
};
|