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