Skip to main content
Go to Latest
File
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189
// Copyright Joyent, Inc. and other Node contributors.//// Permission is hereby granted, free of charge, to any person obtaining a// copy of this software and associated documentation files (the// "Software"), to deal in the Software without restriction, including// without limitation the rights to use, copy, modify, merge, publish,// distribute, sublicense, and/or sell copies of the Software, and to permit// persons to whom the Software is furnished to do so, subject to the// following conditions://// The above copyright notice and this permission notice shall be included// in all copies or substantial portions of the Software.//// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE// USE OR OTHER DEALINGS IN THE SOFTWARE.
// deno-lint-ignore-file camelcase no-inner-declarations no-this-alias
import { ERR_INVALID_ARG_VALUE } from "../../_errors.ts";import { validateAbortSignal, validateArray, validateString, validateUint32,} from "../validators.js";import { // inspect, getStringWidth, stripVTControlCharacters,} from "../util/inspect.js";import EventEmitter from "../../events.ts";import { emitKeypressEvents } from "./emitKeypressEvents.js";import { charLengthAt, charLengthLeft, commonPrefix, kSubstringSearch,} from "./utils.js";import { clearScreenDown, cursorTo, moveCursor } from "./callbacks.js";
import { StringDecoder } from "../../string_decoder.ts";
// Lazy load Readable for startup performance.let Readable;
const kHistorySize = 30;const kMincrlfDelay = 100;// \r\n, \n, or \r followed by something other than \nconst lineEnding = /\r?\n|\r(?!\n)/;
const kLineObjectStream = Symbol("line object stream");export const kQuestionCancel = Symbol("kQuestionCancel");
// GNU readline library - keyseq-timeout is 500ms (default)const ESCAPE_CODE_TIMEOUT = 500;
export const kAddHistory = Symbol("_addHistory");export const kDecoder = Symbol("_decoder");export const kDeleteLeft = Symbol("_deleteLeft");export const kDeleteLineLeft = Symbol("_deleteLineLeft");export const kDeleteLineRight = Symbol("_deleteLineRight");export const kDeleteRight = Symbol("_deleteRight");export const kDeleteWordLeft = Symbol("_deleteWordLeft");export const kDeleteWordRight = Symbol("_deleteWordRight");export const kGetDisplayPos = Symbol("_getDisplayPos");export const kHistoryNext = Symbol("_historyNext");export const kHistoryPrev = Symbol("_historyPrev");export const kInsertString = Symbol("_insertString");export const kLine = Symbol("_line");export const kLine_buffer = Symbol("_line_buffer");export const kMoveCursor = Symbol("_moveCursor");export const kNormalWrite = Symbol("_normalWrite");export const kOldPrompt = Symbol("_oldPrompt");export const kOnLine = Symbol("_onLine");export const kPreviousKey = Symbol("_previousKey");export const kPrompt = Symbol("_prompt");export const kQuestionCallback = Symbol("_questionCallback");export const kRefreshLine = Symbol("_refreshLine");export const kSawKeyPress = Symbol("_sawKeyPress");export const kSawReturnAt = Symbol("_sawReturnAt");export const kSetRawMode = Symbol("_setRawMode");export const kTabComplete = Symbol("_tabComplete");export const kTabCompleter = Symbol("_tabCompleter");export const kTtyWrite = Symbol("_ttyWrite");export const kWordLeft = Symbol("_wordLeft");export const kWordRight = Symbol("_wordRight");export const kWriteToOutput = Symbol("_writeToOutput");
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]); } }
question(query, cb) { 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) { if (Readable === undefined) { Readable = require("stream").Readable; } 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](); }}