1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-04 17:18:23 -05:00
denoland-deno/ext/node/polyfills/internal/readline/interface.mjs

1234 lines
32 KiB
JavaScript
Raw Permalink Normal View History

// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
// TODO(petamoriken): enable prefer-primordials for node polyfills
// deno-lint-ignore-file prefer-primordials
// deno-lint-ignore-file camelcase no-inner-declarations no-this-alias
2023-07-08 14:34:08 -04:00
import {
ERR_INVALID_ARG_VALUE,
ERR_USE_AFTER_CLOSE,
} from "ext:deno_node/internal/errors.ts";
import {
validateAbortSignal,
validateArray,
validateString,
validateUint32,
} from "ext:deno_node/internal/validators.mjs";
import {
// inspect,
getStringWidth,
stripVTControlCharacters,
} from "ext:deno_node/internal/util/inspect.mjs";
import EventEmitter from "node:events";
import { emitKeypressEvents } from "ext:deno_node/internal/readline/emitKeypressEvents.mjs";
import {
charLengthAt,
charLengthLeft,
commonPrefix,
kSubstringSearch,
} from "ext:deno_node/internal/readline/utils.mjs";
2023-07-08 14:34:08 -04:00
import {
clearScreenDown,
cursorTo,
moveCursor,
} from "ext:deno_node/internal/readline/callbacks.mjs";
import { Readable } from "ext:deno_node/_stream.mjs";
import process from "node:process";
import { StringDecoder } from "node:string_decoder";
import {
kAddHistory,
kDecoder,
kDeleteLeft,
kDeleteLineLeft,
kDeleteLineRight,
kDeleteRight,
kDeleteWordLeft,
kDeleteWordRight,
kGetDisplayPos,
kHistoryNext,
kHistoryPrev,
kInsertString,
kLine,
kLine_buffer,
kMoveCursor,
kNormalWrite,
kOldPrompt,
kOnLine,
kPreviousKey,
kPrompt,
kQuestionCallback,
kRefreshLine,
kSawKeyPress,
kSawReturnAt,
kSetRawMode,
kTabComplete,
kTabCompleter,
kTtyWrite,
kWordLeft,
kWordRight,
kWriteToOutput,
} from "ext:deno_node/internal/readline/symbols.mjs";
const kHistorySize = 30;
const kMincrlfDelay = 100;
// \r\n, \n, or \r followed by something other than \n
const lineEnding = /\r?\n|\r(?!\n)/;
const kLineObjectStream = Symbol("line object stream");
export const kQuestionCancel = Symbol("kQuestionCancel");
export const kQuestion = Symbol("kQuestion");
// GNU readline library - keyseq-timeout is 500ms (default)
const ESCAPE_CODE_TIMEOUT = 500;
export {
kAddHistory,
kDecoder,
kDeleteLeft,
kDeleteLineLeft,
kDeleteLineRight,
kDeleteRight,
kDeleteWordLeft,
kDeleteWordRight,
kGetDisplayPos,
kHistoryNext,
kHistoryPrev,
kInsertString,
kLine,
kLine_buffer,
kMoveCursor,
kNormalWrite,
kOldPrompt,
kOnLine,
kPreviousKey,
kPrompt,
kQuestionCallback,
kRefreshLine,
kSawKeyPress,
kSawReturnAt,
kSetRawMode,
kTabComplete,
kTabCompleter,
kTtyWrite,
kWordLeft,
kWordRight,
kWriteToOutput,
};
export function InterfaceConstructor(input, output, completer, terminal) {
this[kSawReturnAt] = 0;
// TODO(BridgeAR): Document this property. The name is not ideal, so we
// might want to expose an alias and document that instead.
this.isCompletionEnabled = true;
this[kSawKeyPress] = false;
this[kPreviousKey] = null;
this.escapeCodeTimeout = ESCAPE_CODE_TIMEOUT;
this.tabSize = 8;
Function.prototype.call(EventEmitter, this);
let history;
let historySize;
let removeHistoryDuplicates = false;
let crlfDelay;
let prompt = "> ";
let signal;
if (input?.input) {
// An options object was given
output = input.output;
completer = input.completer;
terminal = input.terminal;
history = input.history;
historySize = input.historySize;
signal = input.signal;
if (input.tabSize !== undefined) {
validateUint32(input.tabSize, "tabSize", true);
this.tabSize = input.tabSize;
}
removeHistoryDuplicates = input.removeHistoryDuplicates;
if (input.prompt !== undefined) {
prompt = input.prompt;
}
if (input.escapeCodeTimeout !== undefined) {
if (Number.isFinite(input.escapeCodeTimeout)) {
this.escapeCodeTimeout = input.escapeCodeTimeout;
} else {
throw new ERR_INVALID_ARG_VALUE(
"input.escapeCodeTimeout",
this.escapeCodeTimeout,
);
}
}
if (signal) {
validateAbortSignal(signal, "options.signal");
}
crlfDelay = input.crlfDelay;
input = input.input;
}
if (completer !== undefined && typeof completer !== "function") {
throw new ERR_INVALID_ARG_VALUE("completer", completer);
}
if (history === undefined) {
history = [];
} else {
validateArray(history, "history");
}
if (historySize === undefined) {
historySize = kHistorySize;
}
if (
typeof historySize !== "number" ||
Number.isNaN(historySize) ||
historySize < 0
) {
throw new ERR_INVALID_ARG_VALUE.RangeError("historySize", historySize);
}
// Backwards compat; check the isTTY prop of the output stream
// when `terminal` was not specified
if (terminal === undefined && !(output === null || output === undefined)) {
terminal = !!output.isTTY;
}
const self = this;
this.line = "";
this[kSubstringSearch] = null;
this.output = output;
this.input = input;
this.history = history;
this.historySize = historySize;
this.removeHistoryDuplicates = !!removeHistoryDuplicates;
this.crlfDelay = crlfDelay
? Math.max(kMincrlfDelay, crlfDelay)
: kMincrlfDelay;
this.completer = completer;
this.setPrompt(prompt);
this.terminal = !!terminal;
function onerror(err) {
self.emit("error", err);
}
function ondata(data) {
self[kNormalWrite](data);
}
function onend() {
if (
typeof self[kLine_buffer] === "string" &&
self[kLine_buffer].length > 0
) {
self.emit("line", self[kLine_buffer]);
}
self.close();
}
function ontermend() {
if (typeof self.line === "string" && self.line.length > 0) {
self.emit("line", self.line);
}
self.close();
}
function onkeypress(s, key) {
self[kTtyWrite](s, key);
if (key && key.sequence) {
// If the key.sequence is half of a surrogate pair
// (>= 0xd800 and <= 0xdfff), refresh the line so
// the character is displayed appropriately.
const ch = key.sequence.codePointAt(0);
if (ch >= 0xd800 && ch <= 0xdfff) self[kRefreshLine]();
}
}
function onresize() {
self[kRefreshLine]();
}
this[kLineObjectStream] = undefined;
input.on("error", onerror);
if (!this.terminal) {
function onSelfCloseWithoutTerminal() {
input.removeListener("data", ondata);
input.removeListener("error", onerror);
input.removeListener("end", onend);
}
input.on("data", ondata);
input.on("end", onend);
self.once("close", onSelfCloseWithoutTerminal);
this[kDecoder] = new StringDecoder("utf8");
} else {
function onSelfCloseWithTerminal() {
input.removeListener("keypress", onkeypress);
input.removeListener("error", onerror);
input.removeListener("end", ontermend);
if (output !== null && output !== undefined) {
output.removeListener("resize", onresize);
}
}
emitKeypressEvents(input, this);
// `input` usually refers to stdin
input.on("keypress", onkeypress);
input.on("end", ontermend);
this[kSetRawMode](true);
this.terminal = true;
// Cursor position on the line.
this.cursor = 0;
this.historyIndex = -1;
if (output !== null && output !== undefined) {
output.on("resize", onresize);
}
self.once("close", onSelfCloseWithTerminal);
}
if (signal) {
const onAborted = () => self.close();
if (signal.aborted) {
process.nextTick(onAborted);
} else {
signal.addEventListener("abort", onAborted, { once: true });
self.once("close", () => signal.removeEventListener("abort", onAborted));
}
}
// Current line
this.line = "";
input.resume();
}
Object.setPrototypeOf(InterfaceConstructor.prototype, EventEmitter.prototype);
Object.setPrototypeOf(InterfaceConstructor, EventEmitter);
export class Interface extends InterfaceConstructor {
// eslint-disable-next-line no-useless-constructor
constructor(input, output, completer, terminal) {
super(input, output, completer, terminal);
}
get columns() {
if (this.output && this.output.columns) return this.output.columns;
return Infinity;
}
/**
* Sets the prompt written to the output.
* @param {string} prompt
* @returns {void}
*/
setPrompt(prompt) {
this[kPrompt] = prompt;
}
/**
* Returns the current prompt used by `rl.prompt()`.
* @returns {string}
*/
getPrompt() {
return this[kPrompt];
}
[kSetRawMode](mode) {
const wasInRawMode = this.input.isRaw;
if (typeof this.input.setRawMode === "function") {
this.input.setRawMode(mode);
}
return wasInRawMode;
}
/**
* Writes the configured `prompt` to a new line in `output`.
* @param {boolean} [preserveCursor]
* @returns {void}
*/
prompt(preserveCursor) {
if (this.paused) this.resume();
if (this.terminal && process.env.TERM !== "dumb") {
if (!preserveCursor) this.cursor = 0;
this[kRefreshLine]();
} else {
this[kWriteToOutput](this[kPrompt]);
}
}
[kQuestion](query, cb) {
if (this.closed) {
throw new ERR_USE_AFTER_CLOSE("readline");
}
if (this[kQuestionCallback]) {
this.prompt();
} else {
this[kOldPrompt] = this[kPrompt];
this.setPrompt(query);
this[kQuestionCallback] = cb;
this.prompt();
}
}
[kOnLine](line) {
if (this[kQuestionCallback]) {
const cb = this[kQuestionCallback];
this[kQuestionCallback] = null;
this.setPrompt(this[kOldPrompt]);
cb(line);
} else {
this.emit("line", line);
}
}
[kQuestionCancel]() {
if (this[kQuestionCallback]) {
this[kQuestionCallback] = null;
this.setPrompt(this[kOldPrompt]);
this.clearLine();
}
}
[kWriteToOutput](stringToWrite) {
validateString(stringToWrite, "stringToWrite");
if (this.output !== null && this.output !== undefined) {
this.output.write(stringToWrite);
}
}
[kAddHistory]() {
if (this.line.length === 0) return "";
// If the history is disabled then return the line
if (this.historySize === 0) return this.line;
// If the trimmed line is empty then return the line
if (this.line.trim().length === 0) return this.line;
if (this.history.length === 0 || this.history[0] !== this.line) {
if (this.removeHistoryDuplicates) {
// Remove older history line if identical to new one
const dupIndex = this.history.indexOf(this.line);
if (dupIndex !== -1) this.history.splice(dupIndex, 1);
}
this.history.unshift(this.line);
// Only store so many
if (this.history.length > this.historySize) {
this.history.pop();
}
}
this.historyIndex = -1;
// The listener could change the history object, possibly
// to remove the last added entry if it is sensitive and should
// not be persisted in the history, like a password
const line = this.history[0];
// Emit history event to notify listeners of update
this.emit("history", this.history);
return line;
}
[kRefreshLine]() {
// line length
const line = this[kPrompt] + this.line;
const dispPos = this[kGetDisplayPos](line);
const lineCols = dispPos.cols;
const lineRows = dispPos.rows;
// cursor position
const cursorPos = this.getCursorPos();
// First move to the bottom of the current line, based on cursor pos
const prevRows = this.prevRows || 0;
if (prevRows > 0) {
moveCursor(this.output, 0, -prevRows);
}
// Cursor to left edge.
cursorTo(this.output, 0);
// erase data
clearScreenDown(this.output);
// Write the prompt and the current buffer content.
this[kWriteToOutput](line);
// Force terminal to allocate a new line
if (lineCols === 0) {
this[kWriteToOutput](" ");
}
// Move cursor to original position.
cursorTo(this.output, cursorPos.cols);
const diff = lineRows - cursorPos.rows;
if (diff > 0) {
moveCursor(this.output, 0, -diff);
}
this.prevRows = cursorPos.rows;
}
/**
* Closes the `readline.Interface` instance.
* @returns {void}
*/
close() {
if (this.closed) return;
this.pause();
if (this.terminal) {
this[kSetRawMode](false);
}
this.closed = true;
this.emit("close");
}
/**
* Pauses the `input` stream.
* @returns {void | Interface}
*/
pause() {
if (this.paused) return;
this.input.pause();
this.paused = true;
this.emit("pause");
return this;
}
/**
* Resumes the `input` stream if paused.
* @returns {void | Interface}
*/
resume() {
if (!this.paused) return;
this.input.resume();
this.paused = false;
this.emit("resume");
return this;
}
/**
* Writes either `data` or a `key` sequence identified by
* `key` to the `output`.
* @param {string} d
* @param {{
* ctrl?: boolean;
* meta?: boolean;
* shift?: boolean;
* name?: string;
* }} [key]
* @returns {void}
*/
write(d, key) {
if (this.paused) this.resume();
if (this.terminal) {
this[kTtyWrite](d, key);
} else {
this[kNormalWrite](d);
}
}
[kNormalWrite](b) {
if (b === undefined) {
return;
}
let string = this[kDecoder].write(b);
if (
this[kSawReturnAt] &&
Date.now() - this[kSawReturnAt] <= this.crlfDelay
) {
string = string.replace(/^\n/, "");
this[kSawReturnAt] = 0;
}
// Run test() on the new string chunk, not on the entire line buffer.
const newPartContainsEnding = lineEnding.test(string);
if (this[kLine_buffer]) {
string = this[kLine_buffer] + string;
this[kLine_buffer] = null;
}
if (newPartContainsEnding) {
this[kSawReturnAt] = string.endsWith("\r") ? Date.now() : 0;
// Got one or more newlines; process into "line" events
const lines = string.split(lineEnding);
// Either '' or (conceivably) the unfinished portion of the next line
string = lines.pop();
this[kLine_buffer] = string;
for (let n = 0; n < lines.length; n++) this[kOnLine](lines[n]);
} else if (string) {
// No newlines this time, save what we have for next time
this[kLine_buffer] = string;
}
}
[kInsertString](c) {
if (this.cursor < this.line.length) {
const beg = this.line.slice(0, this.cursor);
const end = this.line.slice(
this.cursor,
this.line.length,
);
this.line = beg + c + end;
this.cursor += c.length;
this[kRefreshLine]();
} else {
this.line += c;
this.cursor += c.length;
if (this.getCursorPos().cols === 0) {
this[kRefreshLine]();
} else {
this[kWriteToOutput](c);
}
}
}
async [kTabComplete](lastKeypressWasTab) {
this.pause();
const string = this.line.slice(0, this.cursor);
let value;
try {
value = await this.completer(string);
} catch (err) {
// TODO(bartlomieju): inspect is not ported yet
// this[kWriteToOutput](`Tab completion error: ${inspect(err)}`);
this[kWriteToOutput](`Tab completion error: ${err}`);
return;
} finally {
this.resume();
}
this[kTabCompleter](lastKeypressWasTab, value);
}
[kTabCompleter](lastKeypressWasTab, { 0: completions, 1: completeOn }) {
// Result and the text that was completed.
if (!completions || completions.length === 0) {
return;
}
// If there is a common prefix to all matches, then apply that portion.
const prefix = commonPrefix(
completions.filter((e) => e !== ""),
);
if (
prefix.startsWith(completeOn) &&
prefix.length > completeOn.length
) {
this[kInsertString](prefix.slice(completeOn.length));
return;
} else if (!completeOn.startsWith(prefix)) {
this.line = this.line.slice(0, this.cursor - completeOn.length) +
prefix +
this.line.slice(this.cursor, this.line.length);
this.cursor = this.cursor - completeOn.length + prefix.length;
this._refreshLine();
return;
}
if (!lastKeypressWasTab) {
return;
}
// Apply/show completions.
const completionsWidth = completions.map(
(e) => getStringWidth(e),
);
const width = Math.max.apply(completionsWidth) + 2; // 2 space padding
let maxColumns = Math.floor(this.columns / width) || 1;
if (maxColumns === Infinity) {
maxColumns = 1;
}
let output = "\r\n";
let lineIndex = 0;
let whitespace = 0;
for (let i = 0; i < completions.length; i++) {
const completion = completions[i];
if (completion === "" || lineIndex === maxColumns) {
output += "\r\n";
lineIndex = 0;
whitespace = 0;
} else {
output += " ".repeat(whitespace);
}
if (completion !== "") {
output += completion;
whitespace = width - completionsWidth[i];
lineIndex++;
} else {
output += "\r\n";
}
}
if (lineIndex !== 0) {
output += "\r\n\r\n";
}
this[kWriteToOutput](output);
this[kRefreshLine]();
}
[kWordLeft]() {
if (this.cursor > 0) {
// Reverse the string and match a word near beginning
// to avoid quadratic time complexity
const leading = this.line.slice(0, this.cursor);
const reversed = Array.from(leading).reverse().join("");
const match = reversed.match(/^\s*(?:[^\w\s]+|\w+)?/);
this[kMoveCursor](-match[0].length);
}
}
[kWordRight]() {
if (this.cursor < this.line.length) {
const trailing = this.line.slice(this.cursor);
const match = trailing.match(/^(?:\s+|[^\w\s]+|\w+)\s*/);
this[kMoveCursor](match[0].length);
}
}
[kDeleteLeft]() {
if (this.cursor > 0 && this.line.length > 0) {
// The number of UTF-16 units comprising the character to the left
const charSize = charLengthLeft(this.line, this.cursor);
this.line = this.line.slice(0, this.cursor - charSize) +
this.line.slice(this.cursor, this.line.length);
this.cursor -= charSize;
this[kRefreshLine]();
}
}
[kDeleteRight]() {
if (this.cursor < this.line.length) {
// The number of UTF-16 units comprising the character to the left
const charSize = charLengthAt(this.line, this.cursor);
this.line = this.line.slice(0, this.cursor) +
this.line.slice(
this.cursor + charSize,
this.line.length,
);
this[kRefreshLine]();
}
}
[kDeleteWordLeft]() {
if (this.cursor > 0) {
// Reverse the string and match a word near beginning
// to avoid quadratic time complexity
let leading = this.line.slice(0, this.cursor);
const reversed = Array.from(leading).reverse().join("");
const match = reversed.match(/^\s*(?:[^\w\s]+|\w+)?/);
leading = leading.slice(
0,
leading.length - match[0].length,
);
this.line = leading +
this.line.slice(this.cursor, this.line.length);
this.cursor = leading.length;
this[kRefreshLine]();
}
}
[kDeleteWordRight]() {
if (this.cursor < this.line.length) {
const trailing = this.line.slice(this.cursor);
const match = trailing.match(/^(?:\s+|\W+|\w+)\s*/);
this.line = this.line.slice(0, this.cursor) +
trailing.slice(match[0].length);
this[kRefreshLine]();
}
}
[kDeleteLineLeft]() {
this.line = this.line.slice(this.cursor);
this.cursor = 0;
this[kRefreshLine]();
}
[kDeleteLineRight]() {
this.line = this.line.slice(0, this.cursor);
this[kRefreshLine]();
}
clearLine() {
this[kMoveCursor](+Infinity);
this[kWriteToOutput]("\r\n");
this.line = "";
this.cursor = 0;
this.prevRows = 0;
}
[kLine]() {
const line = this[kAddHistory]();
this.clearLine();
this[kOnLine](line);
}
// TODO(BridgeAR): Add underscores to the search part and a red background in
// case no match is found. This should only be the visual part and not the
// actual line content!
// TODO(BridgeAR): In case the substring based search is active and the end is
// reached, show a comment how to search the history as before. E.g., using
// <ctrl> + N. Only show this after two/three UPs or DOWNs, not on the first
// one.
[kHistoryNext]() {
if (this.historyIndex >= 0) {
const search = this[kSubstringSearch] || "";
let index = this.historyIndex - 1;
while (
index >= 0 &&
(!this.history[index].startsWith(search) ||
this.line === this.history[index])
) {
index--;
}
if (index === -1) {
this.line = search;
} else {
this.line = this.history[index];
}
this.historyIndex = index;
this.cursor = this.line.length; // Set cursor to end of line.
this[kRefreshLine]();
}
}
[kHistoryPrev]() {
if (this.historyIndex < this.history.length && this.history.length) {
const search = this[kSubstringSearch] || "";
let index = this.historyIndex + 1;
while (
index < this.history.length &&
(!this.history[index].startsWith(search) ||
this.line === this.history[index])
) {
index++;
}
if (index === this.history.length) {
this.line = search;
} else {
this.line = this.history[index];
}
this.historyIndex = index;
this.cursor = this.line.length; // Set cursor to end of line.
this[kRefreshLine]();
}
}
// Returns the last character's display position of the given string
[kGetDisplayPos](str) {
let offset = 0;
const col = this.columns;
let rows = 0;
str = stripVTControlCharacters(str);
for (const char of str[Symbol.iterator]()) {
if (char === "\n") {
// Rows must be incremented by 1 even if offset = 0 or col = +Infinity.
rows += Math.ceil(offset / col) || 1;
offset = 0;
continue;
}
// Tabs must be aligned by an offset of the tab size.
if (char === "\t") {
offset += this.tabSize - (offset % this.tabSize);
continue;
}
const width = getStringWidth(char);
if (width === 0 || width === 1) {
offset += width;
} else {
// width === 2
if ((offset + 1) % col === 0) {
offset++;
}
offset += 2;
}
}
const cols = offset % col;
rows += (offset - cols) / col;
return { cols, rows };
}
/**
* Returns the real position of the cursor in relation
* to the input prompt + string.
* @returns {{
* rows: number;
* cols: number;
* }}
*/
getCursorPos() {
const strBeforeCursor = this[kPrompt] +
this.line.slice(0, this.cursor);
return this[kGetDisplayPos](strBeforeCursor);
}
// This function moves cursor dx places to the right
// (-dx for left) and refreshes the line if it is needed.
[kMoveCursor](dx) {
if (dx === 0) {
return;
}
const oldPos = this.getCursorPos();
this.cursor += dx;
// Bounds check
if (this.cursor < 0) {
this.cursor = 0;
} else if (this.cursor > this.line.length) {
this.cursor = this.line.length;
}
const newPos = this.getCursorPos();
// Check if cursor stayed on the line.
if (oldPos.rows === newPos.rows) {
const diffWidth = newPos.cols - oldPos.cols;
moveCursor(this.output, diffWidth, 0);
} else {
this[kRefreshLine]();
}
}
// Handle a write from the tty
[kTtyWrite](s, key) {
const previousKey = this[kPreviousKey];
key = key || {};
this[kPreviousKey] = key;
// Activate or deactivate substring search.
if (
(key.name === "up" || key.name === "down") &&
!key.ctrl &&
!key.meta &&
!key.shift
) {
if (this[kSubstringSearch] === null) {
this[kSubstringSearch] = this.line.slice(
0,
this.cursor,
);
}
} else if (this[kSubstringSearch] !== null) {
this[kSubstringSearch] = null;
// Reset the index in case there's no match.
if (this.history.length === this.historyIndex) {
this.historyIndex = -1;
}
}
// Ignore escape key, fixes
// https://github.com/nodejs/node-v0.x-archive/issues/2876.
if (key.name === "escape") return;
if (key.ctrl && key.shift) {
/* Control and shift pressed */
switch (key.name) {
// TODO(BridgeAR): The transmitted escape sequence is `\b` and that is
// identical to <ctrl>-h. It should have a unique escape sequence.
case "backspace":
this[kDeleteLineLeft]();
break;
case "delete":
this[kDeleteLineRight]();
break;
}
} else if (key.ctrl) {
/* Control key pressed */
switch (key.name) {
case "c":
if (this.listenerCount("SIGINT") > 0) {
this.emit("SIGINT");
} else {
// This readline instance is finished
this.close();
}
break;
case "h": // delete left
this[kDeleteLeft]();
break;
case "d": // delete right or EOF
if (this.cursor === 0 && this.line.length === 0) {
// This readline instance is finished
this.close();
} else if (this.cursor < this.line.length) {
this[kDeleteRight]();
}
break;
case "u": // Delete from current to start of line
this[kDeleteLineLeft]();
break;
case "k": // Delete from current to end of line
this[kDeleteLineRight]();
break;
case "a": // Go to the start of the line
this[kMoveCursor](-Infinity);
break;
case "e": // Go to the end of the line
this[kMoveCursor](+Infinity);
break;
case "b": // back one character
this[kMoveCursor](-charLengthLeft(this.line, this.cursor));
break;
case "f": // Forward one character
this[kMoveCursor](+charLengthAt(this.line, this.cursor));
break;
case "l": // Clear the whole screen
cursorTo(this.output, 0, 0);
clearScreenDown(this.output);
this[kRefreshLine]();
break;
case "n": // next history item
this[kHistoryNext]();
break;
case "p": // Previous history item
this[kHistoryPrev]();
break;
case "z":
if (process.platform === "win32") break;
if (this.listenerCount("SIGTSTP") > 0) {
this.emit("SIGTSTP");
} else {
process.once("SIGCONT", () => {
// Don't raise events if stream has already been abandoned.
if (!this.paused) {
// Stream must be paused and resumed after SIGCONT to catch
// SIGINT, SIGTSTP, and EOF.
this.pause();
this.emit("SIGCONT");
}
// Explicitly re-enable "raw mode" and move the cursor to
// the correct position.
// See https://github.com/joyent/node/issues/3295.
this[kSetRawMode](true);
this[kRefreshLine]();
});
this[kSetRawMode](false);
process.kill(process.pid, "SIGTSTP");
}
break;
case "w": // Delete backwards to a word boundary
// TODO(BridgeAR): The transmitted escape sequence is `\b` and that is
// identical to <ctrl>-h. It should have a unique escape sequence.
// Falls through
case "backspace":
this[kDeleteWordLeft]();
break;
case "delete": // Delete forward to a word boundary
this[kDeleteWordRight]();
break;
case "left":
this[kWordLeft]();
break;
case "right":
this[kWordRight]();
break;
}
} else if (key.meta) {
/* Meta key pressed */
switch (key.name) {
case "b": // backward word
this[kWordLeft]();
break;
case "f": // forward word
this[kWordRight]();
break;
case "d": // delete forward word
case "delete":
this[kDeleteWordRight]();
break;
case "backspace": // Delete backwards to a word boundary
this[kDeleteWordLeft]();
break;
}
} else {
/* No modifier keys used */
// \r bookkeeping is only relevant if a \n comes right after.
if (this[kSawReturnAt] && key.name !== "enter") this[kSawReturnAt] = 0;
switch (key.name) {
case "return": // Carriage return, i.e. \r
this[kSawReturnAt] = Date.now();
this[kLine]();
break;
case "enter":
// When key interval > crlfDelay
if (
this[kSawReturnAt] === 0 ||
Date.now() - this[kSawReturnAt] > this.crlfDelay
) {
this[kLine]();
}
this[kSawReturnAt] = 0;
break;
case "backspace":
this[kDeleteLeft]();
break;
case "delete":
this[kDeleteRight]();
break;
case "left":
// Obtain the code point to the left
this[kMoveCursor](-charLengthLeft(this.line, this.cursor));
break;
case "right":
this[kMoveCursor](+charLengthAt(this.line, this.cursor));
break;
case "home":
this[kMoveCursor](-Infinity);
break;
case "end":
this[kMoveCursor](+Infinity);
break;
case "up":
this[kHistoryPrev]();
break;
case "down":
this[kHistoryNext]();
break;
case "tab":
// If tab completion enabled, do that...
if (
typeof this.completer === "function" &&
this.isCompletionEnabled
) {
const lastKeypressWasTab = previousKey &&
previousKey.name === "tab";
this[kTabComplete](lastKeypressWasTab);
break;
}
// falls through
default:
if (typeof s === "string" && s) {
const lines = s.split(/\r\n|\n|\r/);
for (let i = 0, len = lines.length; i < len; i++) {
if (i > 0) {
this[kLine]();
}
this[kInsertString](lines[i]);
}
}
}
}
}
/**
* Creates an `AsyncIterator` object that iterates through
* each line in the input stream as a string.
* @typedef {{
* [Symbol.asyncIterator]: () => InterfaceAsyncIterator,
* next: () => Promise<string>
* }} InterfaceAsyncIterator
* @returns {InterfaceAsyncIterator}
*/
[Symbol.asyncIterator]() {
if (this[kLineObjectStream] === undefined) {
const readable = new Readable({
objectMode: true,
read: () => {
this.resume();
},
destroy: (err, cb) => {
this.off("line", lineListener);
this.off("close", closeListener);
this.close();
cb(err);
},
});
const lineListener = (input) => {
if (!readable.push(input)) {
// TODO(rexagod): drain to resume flow
this.pause();
}
};
const closeListener = () => {
readable.push(null);
};
const errorListener = (err) => {
readable.destroy(err);
};
this.on("error", errorListener);
this.on("line", lineListener);
this.on("close", closeListener);
this[kLineObjectStream] = readable;
}
return this[kLineObjectStream][Symbol.asyncIterator]();
}
}