• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright Joyent, Inc. and other Node contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a
4// copy of this software and associated documentation files (the
5// "Software"), to deal in the Software without restriction, including
6// without limitation the rights to use, copy, modify, merge, publish,
7// distribute, sublicense, and/or sell copies of the Software, and to permit
8// persons to whom the Software is furnished to do so, subject to the
9// following conditions:
10//
11// The above copyright notice and this permission notice shall be included
12// in all copies or substantial portions of the Software.
13//
14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
17// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
19// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
20// USE OR OTHER DEALINGS IN THE SOFTWARE.
21
22// Inspiration for this code comes from Salvatore Sanfilippo's linenoise.
23// https://github.com/antirez/linenoise
24// Reference:
25// * https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
26// * http://www.3waylabs.com/nw/WWW/products/wizcon/vt220.html
27
28'use strict';
29
30const {
31  DateNow,
32  MathCeil,
33  MathFloor,
34  MathMax,
35  NumberIsFinite,
36  NumberIsNaN,
37  ObjectDefineProperty,
38  ObjectSetPrototypeOf,
39  Symbol,
40  SymbolAsyncIterator,
41} = primordials;
42
43const {
44  ERR_INVALID_CALLBACK,
45  ERR_INVALID_CURSOR_POS,
46  ERR_INVALID_OPT_VALUE
47} = require('internal/errors').codes;
48const { validateString } = require('internal/validators');
49const {
50  inspect,
51  getStringWidth,
52  stripVTControlCharacters,
53} = require('internal/util/inspect');
54const EventEmitter = require('events');
55const {
56  charLengthAt,
57  charLengthLeft,
58  commonPrefix,
59  CSI,
60  emitKeys,
61  kSubstringSearch,
62} = require('internal/readline/utils');
63
64const { clearTimeout, setTimeout } = require('timers');
65const {
66  kEscape,
67  kClearToLineBeginning,
68  kClearToLineEnd,
69  kClearLine,
70  kClearScreenDown
71} = CSI;
72
73const { StringDecoder } = require('string_decoder');
74
75// Lazy load Readable for startup performance.
76let Readable;
77
78const kHistorySize = 30;
79const kMincrlfDelay = 100;
80// \r\n, \n, or \r followed by something other than \n
81const lineEnding = /\r?\n|\r(?!\n)/;
82
83const kLineObjectStream = Symbol('line object stream');
84
85const KEYPRESS_DECODER = Symbol('keypress-decoder');
86const ESCAPE_DECODER = Symbol('escape-decoder');
87
88// GNU readline library - keyseq-timeout is 500ms (default)
89const ESCAPE_CODE_TIMEOUT = 500;
90
91function createInterface(input, output, completer, terminal) {
92  return new Interface(input, output, completer, terminal);
93}
94
95
96function Interface(input, output, completer, terminal) {
97  if (!(this instanceof Interface)) {
98    return new Interface(input, output, completer, terminal);
99  }
100
101  this._sawReturnAt = 0;
102  // TODO(BridgeAR): Document this property. The name is not ideal, so we might
103  // want to expose an alias and document that instead.
104  this.isCompletionEnabled = true;
105  this._sawKeyPress = false;
106  this._previousKey = null;
107  this.escapeCodeTimeout = ESCAPE_CODE_TIMEOUT;
108
109  EventEmitter.call(this);
110  let historySize;
111  let removeHistoryDuplicates = false;
112  let crlfDelay;
113  let prompt = '> ';
114
115  if (input && input.input) {
116    // An options object was given
117    output = input.output;
118    completer = input.completer;
119    terminal = input.terminal;
120    historySize = input.historySize;
121    removeHistoryDuplicates = input.removeHistoryDuplicates;
122    if (input.prompt !== undefined) {
123      prompt = input.prompt;
124    }
125    if (input.escapeCodeTimeout !== undefined) {
126      if (NumberIsFinite(input.escapeCodeTimeout)) {
127        this.escapeCodeTimeout = input.escapeCodeTimeout;
128      } else {
129        throw new ERR_INVALID_OPT_VALUE(
130          'escapeCodeTimeout',
131          this.escapeCodeTimeout
132        );
133      }
134    }
135    crlfDelay = input.crlfDelay;
136    input = input.input;
137  }
138
139  if (completer && typeof completer !== 'function') {
140    throw new ERR_INVALID_OPT_VALUE('completer', completer);
141  }
142
143  if (historySize === undefined) {
144    historySize = kHistorySize;
145  }
146
147  if (typeof historySize !== 'number' ||
148      NumberIsNaN(historySize) ||
149      historySize < 0) {
150    throw new ERR_INVALID_OPT_VALUE.RangeError('historySize', historySize);
151  }
152
153  // Backwards compat; check the isTTY prop of the output stream
154  //  when `terminal` was not specified
155  if (terminal === undefined && !(output === null || output === undefined)) {
156    terminal = !!output.isTTY;
157  }
158
159  const self = this;
160
161  this[kSubstringSearch] = null;
162  this.output = output;
163  this.input = input;
164  this.historySize = historySize;
165  this.removeHistoryDuplicates = !!removeHistoryDuplicates;
166  this.crlfDelay = crlfDelay ?
167    MathMax(kMincrlfDelay, crlfDelay) : kMincrlfDelay;
168  // Check arity, 2 - for async, 1 for sync
169  if (typeof completer === 'function') {
170    this.completer = completer.length === 2 ?
171      completer :
172      function completerWrapper(v, cb) {
173        cb(null, completer(v));
174      };
175  }
176
177  this.setPrompt(prompt);
178
179  this.terminal = !!terminal;
180
181  if (process.env.TERM === 'dumb') {
182    this._ttyWrite = _ttyWriteDumb.bind(this);
183  }
184
185  function ondata(data) {
186    self._normalWrite(data);
187  }
188
189  function onend() {
190    if (typeof self._line_buffer === 'string' &&
191        self._line_buffer.length > 0) {
192      self.emit('line', self._line_buffer);
193    }
194    self.close();
195  }
196
197  function ontermend() {
198    if (typeof self.line === 'string' && self.line.length > 0) {
199      self.emit('line', self.line);
200    }
201    self.close();
202  }
203
204  function onkeypress(s, key) {
205    self._ttyWrite(s, key);
206    if (key && key.sequence) {
207      // If the key.sequence is half of a surrogate pair
208      // (>= 0xd800 and <= 0xdfff), refresh the line so
209      // the character is displayed appropriately.
210      const ch = key.sequence.codePointAt(0);
211      if (ch >= 0xd800 && ch <= 0xdfff)
212        self._refreshLine();
213    }
214  }
215
216  function onresize() {
217    self._refreshLine();
218  }
219
220  this[kLineObjectStream] = undefined;
221
222  if (!this.terminal) {
223    function onSelfCloseWithoutTerminal() {
224      input.removeListener('data', ondata);
225      input.removeListener('end', onend);
226    }
227
228    input.on('data', ondata);
229    input.on('end', onend);
230    self.once('close', onSelfCloseWithoutTerminal);
231    this._decoder = new StringDecoder('utf8');
232  } else {
233    function onSelfCloseWithTerminal() {
234      input.removeListener('keypress', onkeypress);
235      input.removeListener('end', ontermend);
236      if (output !== null && output !== undefined) {
237        output.removeListener('resize', onresize);
238      }
239    }
240
241    emitKeypressEvents(input, this);
242
243    // `input` usually refers to stdin
244    input.on('keypress', onkeypress);
245    input.on('end', ontermend);
246
247    // Current line
248    this.line = '';
249
250    this._setRawMode(true);
251    this.terminal = true;
252
253    // Cursor position on the line.
254    this.cursor = 0;
255
256    this.history = [];
257    this.historyIndex = -1;
258
259    if (output !== null && output !== undefined)
260      output.on('resize', onresize);
261
262    self.once('close', onSelfCloseWithTerminal);
263  }
264
265  input.resume();
266}
267
268ObjectSetPrototypeOf(Interface.prototype, EventEmitter.prototype);
269ObjectSetPrototypeOf(Interface, EventEmitter);
270
271ObjectDefineProperty(Interface.prototype, 'columns', {
272  configurable: true,
273  enumerable: true,
274  get: function() {
275    if (this.output && this.output.columns)
276      return this.output.columns;
277    return Infinity;
278  }
279});
280
281Interface.prototype.setPrompt = function(prompt) {
282  this._prompt = prompt;
283};
284
285
286Interface.prototype._setRawMode = function(mode) {
287  const wasInRawMode = this.input.isRaw;
288
289  if (typeof this.input.setRawMode === 'function') {
290    this.input.setRawMode(mode);
291  }
292
293  return wasInRawMode;
294};
295
296
297Interface.prototype.prompt = function(preserveCursor) {
298  if (this.paused) this.resume();
299  if (this.terminal && process.env.TERM !== 'dumb') {
300    if (!preserveCursor) this.cursor = 0;
301    this._refreshLine();
302  } else {
303    this._writeToOutput(this._prompt);
304  }
305};
306
307
308Interface.prototype.question = function(query, cb) {
309  if (typeof cb === 'function') {
310    if (this._questionCallback) {
311      this.prompt();
312    } else {
313      this._oldPrompt = this._prompt;
314      this.setPrompt(query);
315      this._questionCallback = cb;
316      this.prompt();
317    }
318  }
319};
320
321
322Interface.prototype._onLine = function(line) {
323  if (this._questionCallback) {
324    const cb = this._questionCallback;
325    this._questionCallback = null;
326    this.setPrompt(this._oldPrompt);
327    cb(line);
328  } else {
329    this.emit('line', line);
330  }
331};
332
333Interface.prototype._writeToOutput = function _writeToOutput(stringToWrite) {
334  validateString(stringToWrite, 'stringToWrite');
335
336  if (this.output !== null && this.output !== undefined) {
337    this.output.write(stringToWrite);
338  }
339};
340
341Interface.prototype._addHistory = function() {
342  if (this.line.length === 0) return '';
343
344  // If the history is disabled then return the line
345  if (this.historySize === 0) return this.line;
346
347  // If the trimmed line is empty then return the line
348  if (this.line.trim().length === 0) return this.line;
349
350  if (this.history.length === 0 || this.history[0] !== this.line) {
351    if (this.removeHistoryDuplicates) {
352      // Remove older history line if identical to new one
353      const dupIndex = this.history.indexOf(this.line);
354      if (dupIndex !== -1) this.history.splice(dupIndex, 1);
355    }
356
357    this.history.unshift(this.line);
358
359    // Only store so many
360    if (this.history.length > this.historySize) this.history.pop();
361  }
362
363  this.historyIndex = -1;
364  return this.history[0];
365};
366
367
368Interface.prototype._refreshLine = function() {
369  // line length
370  const line = this._prompt + this.line;
371  const dispPos = this._getDisplayPos(line);
372  const lineCols = dispPos.cols;
373  const lineRows = dispPos.rows;
374
375  // cursor position
376  const cursorPos = this.getCursorPos();
377
378  // First move to the bottom of the current line, based on cursor pos
379  const prevRows = this.prevRows || 0;
380  if (prevRows > 0) {
381    moveCursor(this.output, 0, -prevRows);
382  }
383
384  // Cursor to left edge.
385  cursorTo(this.output, 0);
386  // erase data
387  clearScreenDown(this.output);
388
389  // Write the prompt and the current buffer content.
390  this._writeToOutput(line);
391
392  // Force terminal to allocate a new line
393  if (lineCols === 0) {
394    this._writeToOutput(' ');
395  }
396
397  // Move cursor to original position.
398  cursorTo(this.output, cursorPos.cols);
399
400  const diff = lineRows - cursorPos.rows;
401  if (diff > 0) {
402    moveCursor(this.output, 0, -diff);
403  }
404
405  this.prevRows = cursorPos.rows;
406};
407
408
409Interface.prototype.close = function() {
410  if (this.closed) return;
411  this.pause();
412  if (this.terminal) {
413    this._setRawMode(false);
414  }
415  this.closed = true;
416  this.emit('close');
417};
418
419
420Interface.prototype.pause = function() {
421  if (this.paused) return;
422  this.input.pause();
423  this.paused = true;
424  this.emit('pause');
425  return this;
426};
427
428
429Interface.prototype.resume = function() {
430  if (!this.paused) return;
431  this.input.resume();
432  this.paused = false;
433  this.emit('resume');
434  return this;
435};
436
437
438Interface.prototype.write = function(d, key) {
439  if (this.paused) this.resume();
440  if (this.terminal) {
441    this._ttyWrite(d, key);
442  } else {
443    this._normalWrite(d);
444  }
445};
446
447Interface.prototype._normalWrite = function(b) {
448  if (b === undefined) {
449    return;
450  }
451  let string = this._decoder.write(b);
452  if (this._sawReturnAt &&
453      DateNow() - this._sawReturnAt <= this.crlfDelay) {
454    string = string.replace(/^\n/, '');
455    this._sawReturnAt = 0;
456  }
457
458  // Run test() on the new string chunk, not on the entire line buffer.
459  const newPartContainsEnding = lineEnding.test(string);
460
461  if (this._line_buffer) {
462    string = this._line_buffer + string;
463    this._line_buffer = null;
464  }
465  if (newPartContainsEnding) {
466    this._sawReturnAt = string.endsWith('\r') ? DateNow() : 0;
467
468    // Got one or more newlines; process into "line" events
469    const lines = string.split(lineEnding);
470    // Either '' or (conceivably) the unfinished portion of the next line
471    string = lines.pop();
472    this._line_buffer = string;
473    for (let n = 0; n < lines.length; n++)
474      this._onLine(lines[n]);
475  } else if (string) {
476    // No newlines this time, save what we have for next time
477    this._line_buffer = string;
478  }
479};
480
481Interface.prototype._insertString = function(c) {
482  if (this.cursor < this.line.length) {
483    const beg = this.line.slice(0, this.cursor);
484    const end = this.line.slice(this.cursor, this.line.length);
485    this.line = beg + c + end;
486    this.cursor += c.length;
487    this._refreshLine();
488  } else {
489    this.line += c;
490    this.cursor += c.length;
491
492    if (this.getCursorPos().cols === 0) {
493      this._refreshLine();
494    } else {
495      this._writeToOutput(c);
496    }
497  }
498};
499
500Interface.prototype._tabComplete = function(lastKeypressWasTab) {
501  this.pause();
502  this.completer(this.line.slice(0, this.cursor), (err, value) => {
503    this.resume();
504
505    if (err) {
506      this._writeToOutput(`Tab completion error: ${inspect(err)}`);
507      return;
508    }
509
510    // Result and the text that was completed.
511    const [completions, completeOn] = value;
512
513    if (!completions || completions.length === 0) {
514      return;
515    }
516
517    // If there is a common prefix to all matches, then apply that portion.
518    const prefix = commonPrefix(completions.filter((e) => e !== ''));
519    if (prefix.length > completeOn.length) {
520      this._insertString(prefix.slice(completeOn.length));
521      return;
522    }
523
524    if (!lastKeypressWasTab) {
525      return;
526    }
527
528    // Apply/show completions.
529    const completionsWidth = completions.map((e) => getStringWidth(e));
530    const width = MathMax(...completionsWidth) + 2; // 2 space padding
531    let maxColumns = MathFloor(this.columns / width) || 1;
532    if (maxColumns === Infinity) {
533      maxColumns = 1;
534    }
535    let output = '\r\n';
536    let lineIndex = 0;
537    let whitespace = 0;
538    for (let i = 0; i < completions.length; i++) {
539      const completion = completions[i];
540      if (completion === '' || lineIndex === maxColumns) {
541        output += '\r\n';
542        lineIndex = 0;
543        whitespace = 0;
544      } else {
545        output += ' '.repeat(whitespace);
546      }
547      if (completion !== '') {
548        output += completion;
549        whitespace = width - completionsWidth[i];
550        lineIndex++;
551      } else {
552        output += '\r\n';
553      }
554    }
555    if (lineIndex !== 0) {
556      output += '\r\n\r\n';
557    }
558    this._writeToOutput(output);
559    this._refreshLine();
560  });
561};
562
563Interface.prototype._wordLeft = function() {
564  if (this.cursor > 0) {
565    // Reverse the string and match a word near beginning
566    // to avoid quadratic time complexity
567    const leading = this.line.slice(0, this.cursor);
568    const reversed = leading.split('').reverse().join('');
569    const match = reversed.match(/^\s*(?:[^\w\s]+|\w+)?/);
570    this._moveCursor(-match[0].length);
571  }
572};
573
574
575Interface.prototype._wordRight = function() {
576  if (this.cursor < this.line.length) {
577    const trailing = this.line.slice(this.cursor);
578    const match = trailing.match(/^(?:\s+|[^\w\s]+|\w+)\s*/);
579    this._moveCursor(match[0].length);
580  }
581};
582
583Interface.prototype._deleteLeft = function() {
584  if (this.cursor > 0 && this.line.length > 0) {
585    // The number of UTF-16 units comprising the character to the left
586    const charSize = charLengthLeft(this.line, this.cursor);
587    this.line = this.line.slice(0, this.cursor - charSize) +
588                this.line.slice(this.cursor, this.line.length);
589
590    this.cursor -= charSize;
591    this._refreshLine();
592  }
593};
594
595
596Interface.prototype._deleteRight = function() {
597  if (this.cursor < this.line.length) {
598    // The number of UTF-16 units comprising the character to the left
599    const charSize = charLengthAt(this.line, this.cursor);
600    this.line = this.line.slice(0, this.cursor) +
601      this.line.slice(this.cursor + charSize, this.line.length);
602    this._refreshLine();
603  }
604};
605
606
607Interface.prototype._deleteWordLeft = function() {
608  if (this.cursor > 0) {
609    // Reverse the string and match a word near beginning
610    // to avoid quadratic time complexity
611    let leading = this.line.slice(0, this.cursor);
612    const reversed = leading.split('').reverse().join('');
613    const match = reversed.match(/^\s*(?:[^\w\s]+|\w+)?/);
614    leading = leading.slice(0, leading.length - match[0].length);
615    this.line = leading + this.line.slice(this.cursor, this.line.length);
616    this.cursor = leading.length;
617    this._refreshLine();
618  }
619};
620
621
622Interface.prototype._deleteWordRight = function() {
623  if (this.cursor < this.line.length) {
624    const trailing = this.line.slice(this.cursor);
625    const match = trailing.match(/^(?:\s+|\W+|\w+)\s*/);
626    this.line = this.line.slice(0, this.cursor) +
627                trailing.slice(match[0].length);
628    this._refreshLine();
629  }
630};
631
632
633Interface.prototype._deleteLineLeft = function() {
634  this.line = this.line.slice(this.cursor);
635  this.cursor = 0;
636  this._refreshLine();
637};
638
639
640Interface.prototype._deleteLineRight = function() {
641  this.line = this.line.slice(0, this.cursor);
642  this._refreshLine();
643};
644
645
646Interface.prototype.clearLine = function() {
647  this._moveCursor(+Infinity);
648  this._writeToOutput('\r\n');
649  this.line = '';
650  this.cursor = 0;
651  this.prevRows = 0;
652};
653
654
655Interface.prototype._line = function() {
656  const line = this._addHistory();
657  this.clearLine();
658  this._onLine(line);
659};
660
661// TODO(BridgeAR): Add underscores to the search part and a red background in
662// case no match is found. This should only be the visual part and not the
663// actual line content!
664// TODO(BridgeAR): In case the substring based search is active and the end is
665// reached, show a comment how to search the history as before. E.g., using
666// <ctrl> + N. Only show this after two/three UPs or DOWNs, not on the first
667// one.
668Interface.prototype._historyNext = function() {
669  if (this.historyIndex >= 0) {
670    const search = this[kSubstringSearch] || '';
671    let index = this.historyIndex - 1;
672    while (index >= 0 &&
673           (!this.history[index].startsWith(search) ||
674             this.line === this.history[index])) {
675      index--;
676    }
677    if (index === -1) {
678      this.line = search;
679    } else {
680      this.line = this.history[index];
681    }
682    this.historyIndex = index;
683    this.cursor = this.line.length; // Set cursor to end of line.
684    this._refreshLine();
685  }
686};
687
688Interface.prototype._historyPrev = function() {
689  if (this.historyIndex < this.history.length && this.history.length) {
690    const search = this[kSubstringSearch] || '';
691    let index = this.historyIndex + 1;
692    while (index < this.history.length &&
693           (!this.history[index].startsWith(search) ||
694             this.line === this.history[index])) {
695      index++;
696    }
697    if (index === this.history.length) {
698      this.line = search;
699    } else {
700      this.line = this.history[index];
701    }
702    this.historyIndex = index;
703    this.cursor = this.line.length; // Set cursor to end of line.
704    this._refreshLine();
705  }
706};
707
708// Returns the last character's display position of the given string
709Interface.prototype._getDisplayPos = function(str) {
710  let offset = 0;
711  const col = this.columns;
712  let rows = 0;
713  str = stripVTControlCharacters(str);
714  for (const char of str) {
715    if (char === '\n') {
716      // Rows must be incremented by 1 even if offset = 0 or col = +Infinity.
717      rows += MathCeil(offset / col) || 1;
718      offset = 0;
719      continue;
720    }
721    // Tabs must be aligned by an offset of 8.
722    // TODO(BridgeAR): Make the tab size configurable.
723    if (char === '\t') {
724      offset += 8 - (offset % 8);
725      continue;
726    }
727    const width = getStringWidth(char);
728    if (width === 0 || width === 1) {
729      offset += width;
730    } else { // width === 2
731      if ((offset + 1) % col === 0) {
732        offset++;
733      }
734      offset += 2;
735    }
736  }
737  const cols = offset % col;
738  rows += (offset - cols) / col;
739  return { cols, rows };
740};
741
742// Returns current cursor's position and line
743Interface.prototype.getCursorPos = function() {
744  const strBeforeCursor = this._prompt + this.line.substring(0, this.cursor);
745  return this._getDisplayPos(strBeforeCursor);
746};
747Interface.prototype._getCursorPos = Interface.prototype.getCursorPos;
748
749// This function moves cursor dx places to the right
750// (-dx for left) and refreshes the line if it is needed.
751Interface.prototype._moveCursor = function(dx) {
752  if (dx === 0) {
753    return;
754  }
755  const oldPos = this.getCursorPos();
756  this.cursor += dx;
757
758  // Bounds check
759  if (this.cursor < 0) {
760    this.cursor = 0;
761  } else if (this.cursor > this.line.length) {
762    this.cursor = this.line.length;
763  }
764
765  const newPos = this.getCursorPos();
766
767  // Check if cursor stayed on the line.
768  if (oldPos.rows === newPos.rows) {
769    const diffWidth = newPos.cols - oldPos.cols;
770    moveCursor(this.output, diffWidth, 0);
771  } else {
772    this._refreshLine();
773  }
774};
775
776function _ttyWriteDumb(s, key) {
777  key = key || {};
778
779  if (key.name === 'escape') return;
780
781  if (this._sawReturnAt && key.name !== 'enter')
782    this._sawReturnAt = 0;
783
784  if (key.ctrl) {
785    if (key.name === 'c') {
786      if (this.listenerCount('SIGINT') > 0) {
787        this.emit('SIGINT');
788      } else {
789        // This readline instance is finished
790        this.close();
791      }
792
793      return;
794    } else if (key.name === 'd') {
795      this.close();
796      return;
797    }
798  }
799
800  switch (key.name) {
801    case 'return':  // Carriage return, i.e. \r
802      this._sawReturnAt = DateNow();
803      this._line();
804      break;
805
806    case 'enter':
807      // When key interval > crlfDelay
808      if (this._sawReturnAt === 0 ||
809          DateNow() - this._sawReturnAt > this.crlfDelay) {
810        this._line();
811      }
812      this._sawReturnAt = 0;
813      break;
814
815    default:
816      if (typeof s === 'string' && s) {
817        this.line += s;
818        this.cursor += s.length;
819        this._writeToOutput(s);
820      }
821  }
822}
823
824// Handle a write from the tty
825Interface.prototype._ttyWrite = function(s, key) {
826  const previousKey = this._previousKey;
827  key = key || {};
828  this._previousKey = key;
829
830  // Activate or deactivate substring search.
831  if ((key.name === 'up' || key.name === 'down') &&
832      !key.ctrl && !key.meta && !key.shift) {
833    if (this[kSubstringSearch] === null) {
834      this[kSubstringSearch] = this.line.slice(0, this.cursor);
835    }
836  } else if (this[kSubstringSearch] !== null) {
837    this[kSubstringSearch] = null;
838    // Reset the index in case there's no match.
839    if (this.history.length === this.historyIndex) {
840      this.historyIndex = -1;
841    }
842  }
843
844  // Ignore escape key, fixes
845  // https://github.com/nodejs/node-v0.x-archive/issues/2876.
846  if (key.name === 'escape') return;
847
848  if (key.ctrl && key.shift) {
849    /* Control and shift pressed */
850    switch (key.name) {
851      // TODO(BridgeAR): The transmitted escape sequence is `\b` and that is
852      // identical to <ctrl>-h. It should have a unique escape sequence.
853      case 'backspace':
854        this._deleteLineLeft();
855        break;
856
857      case 'delete':
858        this._deleteLineRight();
859        break;
860    }
861
862  } else if (key.ctrl) {
863    /* Control key pressed */
864
865    switch (key.name) {
866      case 'c':
867        if (this.listenerCount('SIGINT') > 0) {
868          this.emit('SIGINT');
869        } else {
870          // This readline instance is finished
871          this.close();
872        }
873        break;
874
875      case 'h': // delete left
876        this._deleteLeft();
877        break;
878
879      case 'd': // delete right or EOF
880        if (this.cursor === 0 && this.line.length === 0) {
881          // This readline instance is finished
882          this.close();
883        } else if (this.cursor < this.line.length) {
884          this._deleteRight();
885        }
886        break;
887
888      case 'u': // Delete from current to start of line
889        this._deleteLineLeft();
890        break;
891
892      case 'k': // Delete from current to end of line
893        this._deleteLineRight();
894        break;
895
896      case 'a': // Go to the start of the line
897        this._moveCursor(-Infinity);
898        break;
899
900      case 'e': // Go to the end of the line
901        this._moveCursor(+Infinity);
902        break;
903
904      case 'b': // back one character
905        this._moveCursor(-charLengthLeft(this.line, this.cursor));
906        break;
907
908      case 'f': // Forward one character
909        this._moveCursor(+charLengthAt(this.line, this.cursor));
910        break;
911
912      case 'l': // Clear the whole screen
913        cursorTo(this.output, 0, 0);
914        clearScreenDown(this.output);
915        this._refreshLine();
916        break;
917
918      case 'n': // next history item
919        this._historyNext();
920        break;
921
922      case 'p': // Previous history item
923        this._historyPrev();
924        break;
925
926      case 'z':
927        if (process.platform === 'win32') break;
928        if (this.listenerCount('SIGTSTP') > 0) {
929          this.emit('SIGTSTP');
930        } else {
931          process.once('SIGCONT', () => {
932            // Don't raise events if stream has already been abandoned.
933            if (!this.paused) {
934              // Stream must be paused and resumed after SIGCONT to catch
935              // SIGINT, SIGTSTP, and EOF.
936              this.pause();
937              this.emit('SIGCONT');
938            }
939            // Explicitly re-enable "raw mode" and move the cursor to
940            // the correct position.
941            // See https://github.com/joyent/node/issues/3295.
942            this._setRawMode(true);
943            this._refreshLine();
944          });
945          this._setRawMode(false);
946          process.kill(process.pid, 'SIGTSTP');
947        }
948        break;
949
950      case 'w': // Delete backwards to a word boundary
951      // TODO(BridgeAR): The transmitted escape sequence is `\b` and that is
952      // identical to <ctrl>-h. It should have a unique escape sequence.
953      // Falls through
954      case 'backspace':
955        this._deleteWordLeft();
956        break;
957
958      case 'delete': // Delete forward to a word boundary
959        this._deleteWordRight();
960        break;
961
962      case 'left':
963        this._wordLeft();
964        break;
965
966      case 'right':
967        this._wordRight();
968        break;
969    }
970
971  } else if (key.meta) {
972    /* Meta key pressed */
973
974    switch (key.name) {
975      case 'b': // backward word
976        this._wordLeft();
977        break;
978
979      case 'f': // forward word
980        this._wordRight();
981        break;
982
983      case 'd': // delete forward word
984      case 'delete':
985        this._deleteWordRight();
986        break;
987
988      case 'backspace': // Delete backwards to a word boundary
989        this._deleteWordLeft();
990        break;
991    }
992
993  } else {
994    /* No modifier keys used */
995
996    // \r bookkeeping is only relevant if a \n comes right after.
997    if (this._sawReturnAt && key.name !== 'enter')
998      this._sawReturnAt = 0;
999
1000    switch (key.name) {
1001      case 'return':  // Carriage return, i.e. \r
1002        this._sawReturnAt = DateNow();
1003        this._line();
1004        break;
1005
1006      case 'enter':
1007        // When key interval > crlfDelay
1008        if (this._sawReturnAt === 0 ||
1009            DateNow() - this._sawReturnAt > this.crlfDelay) {
1010          this._line();
1011        }
1012        this._sawReturnAt = 0;
1013        break;
1014
1015      case 'backspace':
1016        this._deleteLeft();
1017        break;
1018
1019      case 'delete':
1020        this._deleteRight();
1021        break;
1022
1023      case 'left':
1024        // Obtain the code point to the left
1025        this._moveCursor(-charLengthLeft(this.line, this.cursor));
1026        break;
1027
1028      case 'right':
1029        this._moveCursor(+charLengthAt(this.line, this.cursor));
1030        break;
1031
1032      case 'home':
1033        this._moveCursor(-Infinity);
1034        break;
1035
1036      case 'end':
1037        this._moveCursor(+Infinity);
1038        break;
1039
1040      case 'up':
1041        this._historyPrev();
1042        break;
1043
1044      case 'down':
1045        this._historyNext();
1046        break;
1047
1048      case 'tab':
1049        // If tab completion enabled, do that...
1050        if (typeof this.completer === 'function' && this.isCompletionEnabled) {
1051          const lastKeypressWasTab = previousKey && previousKey.name === 'tab';
1052          this._tabComplete(lastKeypressWasTab);
1053          break;
1054        }
1055      // falls through
1056      default:
1057        if (typeof s === 'string' && s) {
1058          const lines = s.split(/\r\n|\n|\r/);
1059          for (let i = 0, len = lines.length; i < len; i++) {
1060            if (i > 0) {
1061              this._line();
1062            }
1063            this._insertString(lines[i]);
1064          }
1065        }
1066    }
1067  }
1068};
1069
1070Interface.prototype[SymbolAsyncIterator] = function() {
1071  if (this[kLineObjectStream] === undefined) {
1072    if (Readable === undefined) {
1073      Readable = require('stream').Readable;
1074    }
1075    const readable = new Readable({
1076      objectMode: true,
1077      read: () => {
1078        this.resume();
1079      },
1080      destroy: (err, cb) => {
1081        this.off('line', lineListener);
1082        this.off('close', closeListener);
1083        this.close();
1084        cb(err);
1085      }
1086    });
1087    const lineListener = (input) => {
1088      if (!readable.push(input)) {
1089        this.pause();
1090      }
1091    };
1092    const closeListener = () => {
1093      readable.push(null);
1094    };
1095    this.on('line', lineListener);
1096    this.on('close', closeListener);
1097    this[kLineObjectStream] = readable;
1098  }
1099
1100  return this[kLineObjectStream][SymbolAsyncIterator]();
1101};
1102
1103/**
1104 * accepts a readable Stream instance and makes it emit "keypress" events
1105 */
1106
1107function emitKeypressEvents(stream, iface = {}) {
1108  if (stream[KEYPRESS_DECODER]) return;
1109
1110  stream[KEYPRESS_DECODER] = new StringDecoder('utf8');
1111
1112  stream[ESCAPE_DECODER] = emitKeys(stream);
1113  stream[ESCAPE_DECODER].next();
1114
1115  const triggerEscape = () => stream[ESCAPE_DECODER].next('');
1116  const { escapeCodeTimeout = ESCAPE_CODE_TIMEOUT } = iface;
1117  let timeoutId;
1118
1119  function onData(input) {
1120    if (stream.listenerCount('keypress') > 0) {
1121      const string = stream[KEYPRESS_DECODER].write(input);
1122      if (string) {
1123        clearTimeout(timeoutId);
1124
1125        // This supports characters of length 2.
1126        iface._sawKeyPress = charLengthAt(string, 0) === string.length;
1127        iface.isCompletionEnabled = false;
1128
1129        let length = 0;
1130        for (const character of string) {
1131          length += character.length;
1132          if (length === string.length) {
1133            iface.isCompletionEnabled = true;
1134          }
1135
1136          try {
1137            stream[ESCAPE_DECODER].next(character);
1138            // Escape letter at the tail position
1139            if (length === string.length && character === kEscape) {
1140              timeoutId = setTimeout(triggerEscape, escapeCodeTimeout);
1141            }
1142          } catch (err) {
1143            // If the generator throws (it could happen in the `keypress`
1144            // event), we need to restart it.
1145            stream[ESCAPE_DECODER] = emitKeys(stream);
1146            stream[ESCAPE_DECODER].next();
1147            throw err;
1148          }
1149        }
1150      }
1151    } else {
1152      // Nobody's watching anyway
1153      stream.removeListener('data', onData);
1154      stream.on('newListener', onNewListener);
1155    }
1156  }
1157
1158  function onNewListener(event) {
1159    if (event === 'keypress') {
1160      stream.on('data', onData);
1161      stream.removeListener('newListener', onNewListener);
1162    }
1163  }
1164
1165  if (stream.listenerCount('keypress') > 0) {
1166    stream.on('data', onData);
1167  } else {
1168    stream.on('newListener', onNewListener);
1169  }
1170}
1171
1172/**
1173 * moves the cursor to the x and y coordinate on the given stream
1174 */
1175
1176function cursorTo(stream, x, y, callback) {
1177  if (callback !== undefined && typeof callback !== 'function')
1178    throw new ERR_INVALID_CALLBACK(callback);
1179
1180  if (typeof y === 'function') {
1181    callback = y;
1182    y = undefined;
1183  }
1184
1185  if (stream == null || (typeof x !== 'number' && typeof y !== 'number')) {
1186    if (typeof callback === 'function')
1187      process.nextTick(callback, null);
1188    return true;
1189  }
1190
1191  if (typeof x !== 'number')
1192    throw new ERR_INVALID_CURSOR_POS();
1193
1194  const data = typeof y !== 'number' ? CSI`${x + 1}G` : CSI`${y + 1};${x + 1}H`;
1195  return stream.write(data, callback);
1196}
1197
1198/**
1199 * moves the cursor relative to its current location
1200 */
1201
1202function moveCursor(stream, dx, dy, callback) {
1203  if (callback !== undefined && typeof callback !== 'function')
1204    throw new ERR_INVALID_CALLBACK(callback);
1205
1206  if (stream == null || !(dx || dy)) {
1207    if (typeof callback === 'function')
1208      process.nextTick(callback, null);
1209    return true;
1210  }
1211
1212  let data = '';
1213
1214  if (dx < 0) {
1215    data += CSI`${-dx}D`;
1216  } else if (dx > 0) {
1217    data += CSI`${dx}C`;
1218  }
1219
1220  if (dy < 0) {
1221    data += CSI`${-dy}A`;
1222  } else if (dy > 0) {
1223    data += CSI`${dy}B`;
1224  }
1225
1226  return stream.write(data, callback);
1227}
1228
1229/**
1230 * clears the current line the cursor is on:
1231 *   -1 for left of the cursor
1232 *   +1 for right of the cursor
1233 *    0 for the entire line
1234 */
1235
1236function clearLine(stream, dir, callback) {
1237  if (callback !== undefined && typeof callback !== 'function')
1238    throw new ERR_INVALID_CALLBACK(callback);
1239
1240  if (stream === null || stream === undefined) {
1241    if (typeof callback === 'function')
1242      process.nextTick(callback, null);
1243    return true;
1244  }
1245
1246  const type = dir < 0 ?
1247    kClearToLineBeginning :
1248    dir > 0 ?
1249      kClearToLineEnd :
1250      kClearLine;
1251  return stream.write(type, callback);
1252}
1253
1254/**
1255 * clears the screen from the current position of the cursor down
1256 */
1257
1258function clearScreenDown(stream, callback) {
1259  if (callback !== undefined && typeof callback !== 'function')
1260    throw new ERR_INVALID_CALLBACK(callback);
1261
1262  if (stream === null || stream === undefined) {
1263    if (typeof callback === 'function')
1264      process.nextTick(callback, null);
1265    return true;
1266  }
1267
1268  return stream.write(kClearScreenDown, callback);
1269}
1270
1271module.exports = {
1272  Interface,
1273  clearLine,
1274  clearScreenDown,
1275  createInterface,
1276  cursorTo,
1277  emitKeypressEvents,
1278  moveCursor
1279};
1280