// 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. // Inspiration for this code comes from Salvatore Sanfilippo's linenoise. // https://github.com/antirez/linenoise // Reference: // * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html // * http://www.3waylabs.com/nw/WWW/products/wizcon/vt220.html 'use strict'; const { Math, Object } = primordials; const { ERR_INVALID_CALLBACK, ERR_INVALID_CURSOR_POS, ERR_INVALID_OPT_VALUE } = require('internal/errors').codes; const { validateString } = require('internal/validators'); const { inspect } = require('internal/util/inspect'); const EventEmitter = require('events'); const { CSI, emitKeys, getStringWidth, isFullWidthCodePoint, kUTF16SurrogateThreshold, stripVTControlCharacters } = require('internal/readline/utils'); const { clearTimeout, setTimeout } = require('timers'); const { kEscape, kClearToBeginning, kClearToEnd, kClearLine, kClearScreenDown } = CSI; // Lazy load StringDecoder for startup performance. let StringDecoder; // Lazy load Readable for startup performance. let Readable; 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'); const KEYPRESS_DECODER = Symbol('keypress-decoder'); const ESCAPE_DECODER = Symbol('escape-decoder'); // GNU readline library - keyseq-timeout is 500ms (default) const ESCAPE_CODE_TIMEOUT = 500; function createInterface(input, output, completer, terminal) { return new Interface(input, output, completer, terminal); } function Interface(input, output, completer, terminal) { if (!(this instanceof Interface)) { return new Interface(input, output, completer, terminal); } if (StringDecoder === undefined) StringDecoder = require('string_decoder').StringDecoder; this._sawReturnAt = 0; this.isCompletionEnabled = true; this._sawKeyPress = false; this._previousKey = null; this.escapeCodeTimeout = ESCAPE_CODE_TIMEOUT; EventEmitter.call(this); var historySize; var removeHistoryDuplicates = false; let crlfDelay; let prompt = '> '; if (input && input.input) { // An options object was given output = input.output; completer = input.completer; terminal = input.terminal; historySize = input.historySize; 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_OPT_VALUE( 'escapeCodeTimeout', this.escapeCodeTimeout ); } } crlfDelay = input.crlfDelay; input = input.input; } if (completer !== undefined && typeof completer !== 'function') { throw new ERR_INVALID_OPT_VALUE('completer', completer); } if (historySize === undefined) { historySize = kHistorySize; } if (typeof historySize !== 'number' || Number.isNaN(historySize) || historySize < 0) { throw new ERR_INVALID_OPT_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.output = output; this.input = input; this.historySize = historySize; this.removeHistoryDuplicates = !!removeHistoryDuplicates; this.crlfDelay = crlfDelay ? Math.max(kMincrlfDelay, crlfDelay) : kMincrlfDelay; // Check arity, 2 - for async, 1 for sync if (typeof completer === 'function') { this.completer = completer.length === 2 ? completer : function completerWrapper(v, cb) { cb(null, completer(v)); }; } this.setPrompt(prompt); this.terminal = !!terminal; if (process.env.TERM === 'dumb') { this._ttyWrite = _ttyWriteDumb.bind(this); } function ondata(data) { self._normalWrite(data); } function onend() { if (typeof self._line_buffer === 'string' && self._line_buffer.length > 0) { self.emit('line', self._line_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._ttyWrite(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._refreshLine(); } } function onresize() { self._refreshLine(); } this[kLineObjectStream] = undefined; if (!this.terminal) { function onSelfCloseWithoutTerminal() { input.removeListener('data', ondata); input.removeListener('end', onend); } input.on('data', ondata); input.on('end', onend); self.once('close', onSelfCloseWithoutTerminal); this._decoder = new StringDecoder('utf8'); } else { function onSelfCloseWithTerminal() { input.removeListener('keypress', onkeypress); 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); // Current line this.line = ''; this._setRawMode(true); this.terminal = true; // Cursor position on the line. this.cursor = 0; this.history = []; this.historyIndex = -1; if (output !== null && output !== undefined) output.on('resize', onresize); self.once('close', onSelfCloseWithTerminal); } input.resume(); } Object.setPrototypeOf(Interface.prototype, EventEmitter.prototype); Object.setPrototypeOf(Interface, EventEmitter); Object.defineProperty(Interface.prototype, 'columns', { configurable: true, enumerable: true, get: function() { var columns = Infinity; if (this.output && this.output.columns) columns = this.output.columns; return columns; } }); Interface.prototype.setPrompt = function(prompt) { this._prompt = prompt; }; Interface.prototype._setRawMode = function(mode) { const wasInRawMode = this.input.isRaw; if (typeof this.input.setRawMode === 'function') { this.input.setRawMode(mode); } return wasInRawMode; }; Interface.prototype.prompt = function(preserveCursor) { if (this.paused) this.resume(); if (this.terminal && process.env.TERM !== 'dumb') { if (!preserveCursor) this.cursor = 0; this._refreshLine(); } else { this._writeToOutput(this._prompt); } }; Interface.prototype.question = function(query, cb) { if (typeof cb === 'function') { if (this._questionCallback) { this.prompt(); } else { this._oldPrompt = this._prompt; this.setPrompt(query); this._questionCallback = cb; this.prompt(); } } }; Interface.prototype._onLine = function(line) { if (this._questionCallback) { var cb = this._questionCallback; this._questionCallback = null; this.setPrompt(this._oldPrompt); cb(line); } else { this.emit('line', line); } }; Interface.prototype._writeToOutput = function _writeToOutput(stringToWrite) { validateString(stringToWrite, 'stringToWrite'); if (this.output !== null && this.output !== undefined) { this.output.write(stringToWrite); } }; Interface.prototype._addHistory = function() { 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; return this.history[0]; }; Interface.prototype._refreshLine = function() { // line length const line = this._prompt + this.line; const dispPos = this._getDisplayPos(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._writeToOutput(line); // Force terminal to allocate a new line if (lineCols === 0) { this._writeToOutput(' '); } // 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; }; Interface.prototype.close = function() { if (this.closed) return; this.pause(); if (this.terminal) { this._setRawMode(false); } this.closed = true; this.emit('close'); }; Interface.prototype.pause = function() { if (this.paused) return; this.input.pause(); this.paused = true; this.emit('pause'); return this; }; Interface.prototype.resume = function() { if (!this.paused) return; this.input.resume(); this.paused = false; this.emit('resume'); return this; }; Interface.prototype.write = function(d, key) { if (this.paused) this.resume(); if (this.terminal) { this._ttyWrite(d, key); } else { this._normalWrite(d); } }; Interface.prototype._normalWrite = function(b) { if (b === undefined) { return; } var string = this._decoder.write(b); if (this._sawReturnAt && Date.now() - this._sawReturnAt <= this.crlfDelay) { string = string.replace(/^\n/, ''); this._sawReturnAt = 0; } // Run test() on the new string chunk, not on the entire line buffer. const newPartContainsEnding = lineEnding.test(string); if (this._line_buffer) { string = this._line_buffer + string; this._line_buffer = null; } if (newPartContainsEnding) { this._sawReturnAt = string.endsWith('\r') ? Date.now() : 0; // Got one or more newlines; process into "line" events var lines = string.split(lineEnding); // Either '' or (conceivably) the unfinished portion of the next line string = lines.pop(); this._line_buffer = string; for (var n = 0; n < lines.length; n++) this._onLine(lines[n]); } else if (string) { // No newlines this time, save what we have for next time this._line_buffer = string; } }; Interface.prototype._insertString = function(c) { if (this.cursor < this.line.length) { var beg = this.line.slice(0, this.cursor); var end = this.line.slice(this.cursor, this.line.length); this.line = beg + c + end; this.cursor += c.length; this._refreshLine(); } else { this.line += c; this.cursor += c.length; if (this._getCursorPos().cols === 0) { this._refreshLine(); } else { this._writeToOutput(c); } // A hack to get the line refreshed if it's needed this._moveCursor(0); } }; Interface.prototype._tabComplete = function(lastKeypressWasTab) { const self = this; self.pause(); self.completer(self.line.slice(0, self.cursor), function onComplete(err, rv) { self.resume(); if (err) { self._writeToOutput(`tab completion error ${inspect(err)}`); return; } const completions = rv[0]; const completeOn = rv[1]; // The text that was completed if (completions && completions.length) { // Apply/show completions. if (lastKeypressWasTab) { self._writeToOutput('\r\n'); var width = completions.reduce(function completionReducer(a, b) { return a.length > b.length ? a : b; }).length + 2; // 2 space padding var maxColumns = Math.floor(self.columns / width); if (!maxColumns || maxColumns === Infinity) { maxColumns = 1; } var group = []; for (var i = 0; i < completions.length; i++) { var c = completions[i]; if (c === '') { handleGroup(self, group, width, maxColumns); group = []; } else { group.push(c); } } handleGroup(self, group, width, maxColumns); } // If there is a common prefix to all matches, then apply that portion. var f = completions.filter((e) => e); var prefix = commonPrefix(f); if (prefix.length > completeOn.length) { self._insertString(prefix.slice(completeOn.length)); } self._refreshLine(); } }); }; // this = Interface instance function handleGroup(self, group, width, maxColumns) { if (group.length === 0) { return; } const minRows = Math.ceil(group.length / maxColumns); for (var row = 0; row < minRows; row++) { for (var col = 0; col < maxColumns; col++) { var idx = row * maxColumns + col; if (idx >= group.length) { break; } var item = group[idx]; self._writeToOutput(item); if (col < maxColumns - 1) { for (var s = 0; s < width - item.length; s++) { self._writeToOutput(' '); } } } self._writeToOutput('\r\n'); } self._writeToOutput('\r\n'); } function commonPrefix(strings) { if (!strings || strings.length === 0) { return ''; } if (strings.length === 1) return strings[0]; const sorted = strings.slice().sort(); const min = sorted[0]; const max = sorted[sorted.length - 1]; for (var i = 0, len = min.length; i < len; i++) { if (min[i] !== max[i]) { return min.slice(0, i); } } return min; } Interface.prototype._wordLeft = function() { if (this.cursor > 0) { // Reverse the string and match a word near beginning // to avoid quadratic time complexity var leading = this.line.slice(0, this.cursor); var reversed = leading.split('').reverse().join(''); var match = reversed.match(/^\s*(?:[^\w\s]+|\w+)?/); this._moveCursor(-match[0].length); } }; Interface.prototype._wordRight = function() { if (this.cursor < this.line.length) { var trailing = this.line.slice(this.cursor); var match = trailing.match(/^(?:\s+|[^\w\s]+|\w+)\s*/); this._moveCursor(match[0].length); } }; function charLengthLeft(str, i) { if (i <= 0) return 0; if ((i > 1 && str.codePointAt(i - 2) >= kUTF16SurrogateThreshold) || str.codePointAt(i - 1) >= kUTF16SurrogateThreshold) { return 2; } return 1; } function charLengthAt(str, i) { if (str.length <= i) return 0; return str.codePointAt(i) >= kUTF16SurrogateThreshold ? 2 : 1; } Interface.prototype._deleteLeft = function() { 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._refreshLine(); } }; Interface.prototype._deleteRight = function() { 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._refreshLine(); } }; Interface.prototype._deleteWordLeft = function() { if (this.cursor > 0) { // Reverse the string and match a word near beginning // to avoid quadratic time complexity var leading = this.line.slice(0, this.cursor); var reversed = leading.split('').reverse().join(''); var 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._refreshLine(); } }; Interface.prototype._deleteWordRight = function() { if (this.cursor < this.line.length) { var trailing = this.line.slice(this.cursor); var match = trailing.match(/^(?:\s+|\W+|\w+)\s*/); this.line = this.line.slice(0, this.cursor) + trailing.slice(match[0].length); this._refreshLine(); } }; Interface.prototype._deleteLineLeft = function() { this.line = this.line.slice(this.cursor); this.cursor = 0; this._refreshLine(); }; Interface.prototype._deleteLineRight = function() { this.line = this.line.slice(0, this.cursor); this._refreshLine(); }; Interface.prototype.clearLine = function() { this._moveCursor(+Infinity); this._writeToOutput('\r\n'); this.line = ''; this.cursor = 0; this.prevRows = 0; }; Interface.prototype._line = function() { const line = this._addHistory(); this.clearLine(); this._onLine(line); }; Interface.prototype._historyNext = function() { if (this.historyIndex > 0) { this.historyIndex--; this.line = this.history[this.historyIndex]; this.cursor = this.line.length; // Set cursor to end of line. this._refreshLine(); } else if (this.historyIndex === 0) { this.historyIndex = -1; this.cursor = 0; this.line = ''; this._refreshLine(); } }; Interface.prototype._historyPrev = function() { if (this.historyIndex + 1 < this.history.length) { this.historyIndex++; this.line = this.history[this.historyIndex]; this.cursor = this.line.length; // Set cursor to end of line. this._refreshLine(); } }; // Returns the last character's display position of the given string Interface.prototype._getDisplayPos = function(str) { var offset = 0; const col = this.columns; var row = 0; var code; str = stripVTControlCharacters(str); for (var i = 0, len = str.length; i < len; i++) { code = str.codePointAt(i); if (code >= kUTF16SurrogateThreshold) { // Surrogates. i++; } if (code === 0x0a) { // new line \n // row must be incremented by 1 even if offset = 0 or col = +Infinity row += Math.ceil(offset / col) || 1; offset = 0; continue; } const width = getStringWidth(code); if (width === 0 || width === 1) { offset += width; } else { // width === 2 if ((offset + 1) % col === 0) { offset++; } offset += 2; } } const cols = offset % col; const rows = row + (offset - cols) / col; return { cols: cols, rows: rows }; }; // Returns current cursor's position and line Interface.prototype._getCursorPos = function() { const columns = this.columns; const strBeforeCursor = this._prompt + this.line.substring(0, this.cursor); const dispPos = this._getDisplayPos( stripVTControlCharacters(strBeforeCursor)); var cols = dispPos.cols; var rows = dispPos.rows; // If the cursor is on a full-width character which steps over the line, // move the cursor to the beginning of the next line. if (cols + 1 === columns && this.cursor < this.line.length && isFullWidthCodePoint(this.line.codePointAt(this.cursor))) { rows++; cols = 0; } return { cols: cols, rows: rows }; }; // This function moves cursor dx places to the right // (-dx for left) and refreshes the line if it is needed Interface.prototype._moveCursor = function(dx) { const oldcursor = this.cursor; 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 cursors are in the same line if (oldPos.rows === newPos.rows) { var diffCursor = this.cursor - oldcursor; var diffWidth; if (diffCursor < 0) { diffWidth = -getStringWidth( this.line.substring(this.cursor, oldcursor) ); } else if (diffCursor > 0) { diffWidth = getStringWidth( this.line.substring(this.cursor, oldcursor) ); } moveCursor(this.output, diffWidth, 0); this.prevRows = newPos.rows; } else { this._refreshLine(); } }; function _ttyWriteDumb(s, key) { key = key || {}; if (key.name === 'escape') return; if (this._sawReturnAt && key.name !== 'enter') this._sawReturnAt = 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._sawReturnAt = Date.now(); this._line(); break; case 'enter': // When key interval > crlfDelay if (this._sawReturnAt === 0 || Date.now() - this._sawReturnAt > this.crlfDelay) { this._line(); } this._sawReturnAt = 0; break; default: if (typeof s === 'string' && s) { this.line += s; this.cursor += s.length; this._writeToOutput(s); } } } // Handle a write from the tty Interface.prototype._ttyWrite = function(s, key) { const previousKey = this._previousKey; key = key || {}; this._previousKey = key; // 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) { case 'backspace': this._deleteLineLeft(); break; case 'delete': this._deleteLineRight(); 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._deleteLeft(); 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._deleteRight(); } break; case 'u': // Delete from current to start of line this._deleteLineLeft(); break; case 'k': // Delete from current to end of line this._deleteLineRight(); break; case 'a': // Go to the start of the line this._moveCursor(-Infinity); break; case 'e': // Go to the end of the line this._moveCursor(+Infinity); break; case 'b': // back one character this._moveCursor(-charLengthLeft(this.line, this.cursor)); break; case 'f': // Forward one character this._moveCursor(+charLengthAt(this.line, this.cursor)); break; case 'l': // Clear the whole screen cursorTo(this.output, 0, 0); clearScreenDown(this.output); this._refreshLine(); break; case 'n': // next history item this._historyNext(); break; case 'p': // Previous history item this._historyPrev(); 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._setRawMode(true); this._refreshLine(); }); this._setRawMode(false); process.kill(process.pid, 'SIGTSTP'); } break; case 'w': // Delete backwards to a word boundary case 'backspace': this._deleteWordLeft(); break; case 'delete': // Delete forward to a word boundary this._deleteWordRight(); break; case 'left': this._wordLeft(); break; case 'right': this._wordRight(); break; } } else if (key.meta) { /* Meta key pressed */ switch (key.name) { case 'b': // backward word this._wordLeft(); break; case 'f': // forward word this._wordRight(); break; case 'd': // delete forward word case 'delete': this._deleteWordRight(); break; case 'backspace': // Delete backwards to a word boundary this._deleteWordLeft(); break; } } else { /* No modifier keys used */ // \r bookkeeping is only relevant if a \n comes right after. if (this._sawReturnAt && key.name !== 'enter') this._sawReturnAt = 0; switch (key.name) { case 'return': // Carriage return, i.e. \r this._sawReturnAt = Date.now(); this._line(); break; case 'enter': // When key interval > crlfDelay if (this._sawReturnAt === 0 || Date.now() - this._sawReturnAt > this.crlfDelay) { this._line(); } this._sawReturnAt = 0; break; case 'backspace': this._deleteLeft(); break; case 'delete': this._deleteRight(); break; case 'left': // Obtain the code point to the left this._moveCursor(-charLengthLeft(this.line, this.cursor)); break; case 'right': this._moveCursor(+charLengthAt(this.line, this.cursor)); break; case 'home': this._moveCursor(-Infinity); break; case 'end': this._moveCursor(+Infinity); break; case 'up': this._historyPrev(); break; case 'down': this._historyNext(); break; case 'tab': // If tab completion enabled, do that... if (typeof this.completer === 'function' && this.isCompletionEnabled) { const lastKeypressWasTab = previousKey && previousKey.name === 'tab'; this._tabComplete(lastKeypressWasTab); break; } // falls through default: if (typeof s === 'string' && s) { var lines = s.split(/\r\n|\n|\r/); for (var i = 0, len = lines.length; i < len; i++) { if (i > 0) { this._line(); } this._insertString(lines[i]); } } } } }; Interface.prototype[Symbol.asyncIterator] = function() { 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)) { this.pause(); } }; const closeListener = () => { readable.push(null); }; this.on('line', lineListener); this.on('close', closeListener); this[kLineObjectStream] = readable; } return this[kLineObjectStream][Symbol.asyncIterator](); }; /** * accepts a readable Stream instance and makes it emit "keypress" events */ function emitKeypressEvents(stream, iface) { if (stream[KEYPRESS_DECODER]) return; if (StringDecoder === undefined) StringDecoder = require('string_decoder').StringDecoder; stream[KEYPRESS_DECODER] = new StringDecoder('utf8'); stream[ESCAPE_DECODER] = emitKeys(stream); stream[ESCAPE_DECODER].next(); const escapeCodeTimeout = () => stream[ESCAPE_DECODER].next(''); let timeoutId; function onData(b) { if (stream.listenerCount('keypress') > 0) { var r = stream[KEYPRESS_DECODER].write(b); if (r) { clearTimeout(timeoutId); if (iface) { iface._sawKeyPress = r.length === 1; } for (var i = 0; i < r.length; i++) { if (r[i] === '\t' && typeof r[i + 1] === 'string' && iface) { iface.isCompletionEnabled = false; } try { stream[ESCAPE_DECODER].next(r[i]); // Escape letter at the tail position if (r[i] === kEscape && i + 1 === r.length) { timeoutId = setTimeout( escapeCodeTimeout, iface ? iface.escapeCodeTimeout : ESCAPE_CODE_TIMEOUT ); } } 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; } finally { if (iface) { iface.isCompletionEnabled = true; } } } } } 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); } } /** * moves the cursor to the x and y coordinate on the given stream */ function cursorTo(stream, x, y, callback) { if (callback !== undefined && typeof callback !== 'function') throw new ERR_INVALID_CALLBACK(callback); if (typeof y === 'function') { callback = y; y = undefined; } if (stream == null || (typeof x !== 'number' && typeof y !== 'number')) { if (typeof callback === 'function') process.nextTick(callback); return true; } if (typeof x !== 'number') throw new ERR_INVALID_CURSOR_POS(); const data = typeof y !== 'number' ? CSI`${x + 1}G` : CSI`${y + 1};${x + 1}H`; return stream.write(data, callback); } /** * moves the cursor relative to its current location */ function moveCursor(stream, dx, dy, callback) { if (callback !== undefined && typeof callback !== 'function') throw new ERR_INVALID_CALLBACK(callback); if (stream == null || !(dx || dy)) { if (typeof callback === 'function') process.nextTick(callback); return true; } let data = ''; if (dx < 0) { data += CSI`${-dx}D`; } else if (dx > 0) { data += CSI`${dx}C`; } if (dy < 0) { data += CSI`${-dy}A`; } else if (dy > 0) { data += CSI`${dy}B`; } return stream.write(data, callback); } /** * clears the current line the cursor is on: * -1 for left of the cursor * +1 for right of the cursor * 0 for the entire line */ function clearLine(stream, dir, callback) { if (callback !== undefined && typeof callback !== 'function') throw new ERR_INVALID_CALLBACK(callback); if (stream === null || stream === undefined) { if (typeof callback === 'function') process.nextTick(callback); return true; } const type = dir < 0 ? kClearToBeginning : dir > 0 ? kClearToEnd : kClearLine; return stream.write(type, callback); } /** * clears the screen from the current position of the cursor down */ function clearScreenDown(stream, callback) { if (callback !== undefined && typeof callback !== 'function') throw new ERR_INVALID_CALLBACK(callback); if (stream === null || stream === undefined) { if (typeof callback === 'function') process.nextTick(callback); return true; } return stream.write(kClearScreenDown, callback); } module.exports = { Interface, clearLine, clearScreenDown, createInterface, cursorTo, emitKeypressEvents, moveCursor };