1407 lines
		
	
	
		
			36 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1407 lines
		
	
	
		
			36 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /**
 | |
|  **      ==============================
 | |
|  **       O           O      O   OOOO
 | |
|  **       O           O     O O  O   O
 | |
|  **       O           O     O O  O   O
 | |
|  **       OOOO   OOOO O     OOO  OOOO
 | |
|  **       O   O       O    O   O O   O
 | |
|  **       O   O       O    O   O O   O
 | |
|  **       OOOO        OOOO O   O OOOO
 | |
|  **      ==============================
 | |
|  **      Dr. Stefan Bosse http://www.bsslab.de
 | |
|  **
 | |
|  **      COPYRIGHT: THIS SOFTWARE, EXECUTABLE AND SOURCE CODE IS OWNED
 | |
|  **                 BY THE AUTHOR(S).
 | |
|  **                 THIS SOURCE CODE MAY NOT BE COPIED, EXTRACTED,
 | |
|  **                 MODIFIED, OR OTHERWISE USED IN A CONTEXT
 | |
|  **                 OUTSIDE OF THE SOFTWARE SYSTEM.
 | |
|  **
 | |
|  **    $AUTHORS:     Joyent, Inc. and other Node contributors, Stefan Bosse
 | |
|  **    $INITIAL:     (C) 2006-2018 bLAB
 | |
|  **    $VERSION:     1.2.5
 | |
|  **
 | |
|  **    $INFO:
 | |
|  **
 | |
| 
 | |
| // 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
 | |
| 
 | |
| */
 | |
| 
 | |
| var kHistorySize = 30;
 | |
| 
 | |
| var util = Require('util');
 | |
| var Buffer = Require('buffer').Buffer;
 | |
| var inherits = Require('util').inherits;
 | |
| var EventEmitter = Require('events').EventEmitter;
 | |
| var StringDecoder = Require('string_decoder').StringDecoder;
 | |
| 
 | |
| // listenerCount isn't in node 0.10, so here's a basic polyfill
 | |
| EventEmitter._listenerCount = EventEmitter._listenerCount || function (ee, event) {
 | |
|   var listeners = ee && ee._events && ee._events[event]
 | |
|   if (Array.isArray(listeners)) {
 | |
|     return listeners.length
 | |
|   } else if (typeof listeners === 'function') {
 | |
|     return 1
 | |
|   } else {
 | |
|     return 0
 | |
|   }
 | |
| }
 | |
| 
 | |
| exports.createInterface = function(input, output, completer, terminal) {
 | |
|   var rl;
 | |
|   if (arguments.length === 1) {
 | |
|     rl = new Interface(input);
 | |
|   } else {
 | |
|     rl = new Interface(input, output, completer, terminal);
 | |
|   }
 | |
|   return rl;
 | |
| };
 | |
| 
 | |
| 
 | |
| function Interface(input, output, completer, terminal) {
 | |
|   if (!(this instanceof Interface)) {
 | |
|     return new Interface(input, output, completer, terminal);
 | |
|   }
 | |
| 
 | |
|   this._sawReturn = false;
 | |
| 
 | |
|   EventEmitter.call(this);
 | |
| 
 | |
|   if (arguments.length === 1) {
 | |
|     // an options object was given
 | |
|     output = input.output;
 | |
|     completer = input.completer;
 | |
|     terminal = input.terminal;
 | |
|     input = input.input;
 | |
|   }
 | |
| 
 | |
|   completer = completer || function() { return []; };
 | |
| 
 | |
|   if (!util.isFunction(completer)) {
 | |
|     throw new TypeError('Argument \'completer\' must be a function');
 | |
|   }
 | |
| 
 | |
|   // backwards compat; check the isTTY prop of the output stream
 | |
|   //  when `terminal` was not specified
 | |
|   if (util.isUndefined(terminal) && !util.isNullOrUndefined(output)) {
 | |
|     terminal = !!output.isTTY;
 | |
|   }
 | |
| 
 | |
|   var self = this;
 | |
| 
 | |
|   this.output = output;
 | |
|   this.input = input;
 | |
| 
 | |
|   // Check arity, 2 - for async, 1 for sync
 | |
|   this.completer = completer.length === 2 ? completer : function(v, callback) {
 | |
|     callback(null, completer(v));
 | |
|   };
 | |
| 
 | |
|   this.setPrompt('> ');
 | |
| 
 | |
|   this.terminal = !!terminal;
 | |
| 
 | |
|   function ondata(data) {
 | |
|     self._normalWrite(data);
 | |
|   }
 | |
| 
 | |
|   function onend() {
 | |
|     if (util.isString(self._line_buffer) && self._line_buffer.length > 0) {
 | |
|       self.emit('line', self._line_buffer);
 | |
|     }
 | |
|     self.close();
 | |
|   }
 | |
| 
 | |
|   function ontermend() {
 | |
|     if (util.isString(self.line) && self.line.length > 0) {
 | |
|       self.emit('line', self.line);
 | |
|     }
 | |
|     self.close();
 | |
|   }
 | |
| 
 | |
|   function onkeypress(s, key) {
 | |
|     self._ttyWrite(s, key);
 | |
|   }
 | |
| 
 | |
|   function onresize() {
 | |
|     self._refreshLine();
 | |
|   }
 | |
| 
 | |
|   if (!this.terminal) {
 | |
|     input.on('data', ondata);
 | |
|     input.on('end', onend);
 | |
|     self.once('close', function() {
 | |
|       input.removeListener('data', ondata);
 | |
|       input.removeListener('end', onend);
 | |
|     });
 | |
|     this._decoder = new StringDecoder('utf8');
 | |
| 
 | |
|   } else {
 | |
| 
 | |
|     exports.emitKeypressEvents(input);
 | |
| 
 | |
|     // 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 (!util.isNullOrUndefined(output))
 | |
|       output.on('resize', onresize);
 | |
| 
 | |
|     self.once('close', function() {
 | |
|       input.removeListener('keypress', onkeypress);
 | |
|       input.removeListener('end', ontermend);
 | |
|       if (!util.isNullOrUndefined(output)) {
 | |
|         output.removeListener('resize', onresize);
 | |
|       }
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   input.resume();
 | |
| }
 | |
| 
 | |
| inherits(Interface, EventEmitter);
 | |
| 
 | |
| Object.defineProperty(Interface.prototype,'columns',{
 | |
|   get: function() {
 | |
|     var columns = Infinity;
 | |
|     if (this.output && this.output.columns)
 | |
|       columns = this.output.columns;
 | |
|     return columns;
 | |
|   }
 | |
| });
 | |
| /* Depricated
 | |
| Interface.prototype.__defineGetter__('columns', 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) {
 | |
|   if (util.isFunction(this.input.setRawMode)) {
 | |
|     return this.input.setRawMode(mode);
 | |
|   }
 | |
| };
 | |
| 
 | |
| 
 | |
| Interface.prototype.prompt = function(preserveCursor) {
 | |
|   if (this.paused) this.resume();
 | |
|   if (this.terminal) {
 | |
|     if (!preserveCursor) this.cursor = 0;
 | |
|     this._refreshLine();
 | |
|   } else {
 | |
|     this._writeToOutput(this._prompt);
 | |
|   }
 | |
| };
 | |
| 
 | |
| 
 | |
| Interface.prototype.question = function(query, cb) {
 | |
|   if (util.isFunction(cb)) {
 | |
|     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) {
 | |
|   if (!util.isString(stringToWrite))
 | |
|     throw new TypeError('stringToWrite must be a string');
 | |
| 
 | |
|   if (!util.isNullOrUndefined(this.output))
 | |
|     this.output.write(stringToWrite);
 | |
| };
 | |
| 
 | |
| Interface.prototype._addHistory = function() {
 | |
|   if (this.line.length === 0) return '';
 | |
| 
 | |
|   if (this.history.length === 0 || this.history[0] !== this.line) {
 | |
|     this.history.unshift(this.line);
 | |
| 
 | |
|     // Only store so many
 | |
|     if (this.history.length > kHistorySize) this.history.pop();
 | |
|   }
 | |
| 
 | |
|   this.historyIndex = -1;
 | |
|   return this.history[0];
 | |
| };
 | |
| 
 | |
| 
 | |
| Interface.prototype._refreshLine = function() {
 | |
|   // line length
 | |
|   var line = this._prompt + this.line;
 | |
|   var dispPos = this._getDisplayPos(line);
 | |
|   var lineCols = dispPos.cols;
 | |
|   var lineRows = dispPos.rows;
 | |
| 
 | |
|   // cursor position
 | |
|   var cursorPos = this._getCursorPos();
 | |
| 
 | |
|   // first move to the bottom of the current line, based on cursor pos
 | |
|   var prevRows = this.prevRows || 0;
 | |
|   if (prevRows > 0) {
 | |
|     exports.moveCursor(this.output, 0, -prevRows);
 | |
|   }
 | |
| 
 | |
|   // Cursor to left edge.
 | |
|   exports.cursorTo(this.output, 0);
 | |
|   // erase data
 | |
|   exports.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.
 | |
|   exports.cursorTo(this.output, cursorPos.cols);
 | |
| 
 | |
|   var diff = lineRows - cursorPos.rows;
 | |
|   if (diff > 0) {
 | |
|     exports.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();
 | |
|   this.terminal ? this._ttyWrite(d, key) : this._normalWrite(d);
 | |
| };
 | |
| 
 | |
| // \r\n, \n, or \r followed by something other than \n
 | |
| var lineEnding = /\r?\n|\r(?!\n)/;
 | |
| Interface.prototype._normalWrite = function(b) {
 | |
|   if (util.isUndefined(b)) {
 | |
|     return;
 | |
|   }
 | |
|   var string = this._decoder.write(b);
 | |
|   if (this._sawReturn) {
 | |
|     string = string.replace(/^\n/, '');
 | |
|     this._sawReturn = false;
 | |
|   }
 | |
| 
 | |
|   // Run test() on the new string chunk, not on the entire line buffer.
 | |
|   var newPartContainsEnding = lineEnding.test(string);
 | |
| 
 | |
|   if (this._line_buffer) {
 | |
|     string = this._line_buffer + string;
 | |
|     this._line_buffer = null;
 | |
|   }
 | |
|   if (newPartContainsEnding) {
 | |
|     this._sawReturn = /\r$/.test(string);
 | |
| 
 | |
|     // got one or more newlines; process into "line" events
 | |
|     var lines = string.split(lineEnding);
 | |
|     // either '' or (concievably) the unfinished portion of the next line
 | |
|     string = lines.pop();
 | |
|     this._line_buffer = string;
 | |
|     lines.forEach(function(line) {
 | |
|       this._onLine(line);
 | |
|     }, this);
 | |
|   } else if (string) {
 | |
|     // no newlines this time, save what we have for next time
 | |
|     this._line_buffer = string;
 | |
|   }
 | |
| };
 | |
| 
 | |
| Interface.prototype._insertString = function(c) {
 | |
|   //BUG: Problem when adding tabs with following content.
 | |
|   //     Perhaps the bug is in _refreshLine(). Not sure.
 | |
|   //     A hack would be to insert spaces instead of literal '\t'.
 | |
|   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() {
 | |
|   var self = this;
 | |
| 
 | |
|   self.pause();
 | |
|   self.completer(self.line.slice(0, self.cursor), function(err, rv) {
 | |
|     self.resume();
 | |
| 
 | |
|     if (err) {
 | |
|       // XXX Log it somewhere?
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     var completions = rv[0],
 | |
|         completeOn = rv[1];  // the text that was completed
 | |
|     if (completions && completions.length) {
 | |
|       // Apply/show completions.
 | |
|       if (completions.length === 1) {
 | |
|         self._insertString(completions[0].slice(completeOn.length));
 | |
|       } else {
 | |
|         self._writeToOutput('\r\n');
 | |
|         var width = completions.reduce(function(a, b) {
 | |
|           return a.length > b.length ? a : b;
 | |
|         }).length + 2;  // 2 space padding
 | |
|         var maxColumns = Math.floor(self.columns / width) || 1;
 | |
|         var group = [], c;
 | |
|         for (var i = 0, compLen = completions.length; i < compLen; i++) {
 | |
|           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(function(e) { if (e) return 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;
 | |
|   }
 | |
|   var 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, itemLen = item.length; s < width - itemLen;
 | |
|              s++) {
 | |
|           self._writeToOutput(' ');
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|     self._writeToOutput('\r\n');
 | |
|   }
 | |
|   self._writeToOutput('\r\n');
 | |
| }
 | |
| 
 | |
| function commonPrefix(strings) {
 | |
|   if (!strings || strings.length == 0) {
 | |
|     return '';
 | |
|   }
 | |
|   var sorted = strings.slice().sort();
 | |
|   var min = sorted[0];
 | |
|   var 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) {
 | |
|     var leading = this.line.slice(0, this.cursor);
 | |
|     var match = leading.match(/([^\w\s]+|\w+|)\s*$/);
 | |
|     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+|\w+)\s*/);
 | |
|     this._moveCursor(match[0].length);
 | |
|   }
 | |
| };
 | |
| 
 | |
| 
 | |
| Interface.prototype._deleteLeft = function() {
 | |
|   if (this.cursor > 0 && this.line.length > 0) {
 | |
|     this.line = this.line.slice(0, this.cursor - 1) +
 | |
|                 this.line.slice(this.cursor, this.line.length);
 | |
| 
 | |
|     this.cursor--;
 | |
|     this._refreshLine();
 | |
|   }
 | |
| };
 | |
| 
 | |
| 
 | |
| Interface.prototype._deleteRight = function() {
 | |
|   this.line = this.line.slice(0, this.cursor) +
 | |
|               this.line.slice(this.cursor + 1, this.line.length);
 | |
|   this._refreshLine();
 | |
| };
 | |
| 
 | |
| 
 | |
| Interface.prototype._deleteWordLeft = function() {
 | |
|   if (this.cursor > 0) {
 | |
|     var leading = this.line.slice(0, this.cursor);
 | |
|     var match = leading.match(/([^\w\s]+|\w+|)\s*$/);
 | |
|     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;
 | |
| };
 | |
| 
 | |
| // Get current input line content
 | |
| Interface.prototype.getLine = function() {
 | |
|   return this.line;  
 | |
| };
 | |
| 
 | |
| 
 | |
| // Insert a message before actual prompt input line
 | |
| Interface.prototype.insertOutput = function(msg) {
 | |
|   this._moveCursor(+Infinity);
 | |
|   this._writeToOutput('\r');
 | |
|   this._writeToOutput(msg+'\n');
 | |
|   this._writeToOutput(this._prompt+this.line);
 | |
|   
 | |
| };
 | |
| 
 | |
| Interface.prototype._line = function() {
 | |
|   var 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;
 | |
|   var col = this.columns;
 | |
|   var row = 0;
 | |
|   var code;
 | |
|   str = stripVTControlCharacters(str);
 | |
|   for (var i = 0, len = str.length; i < len; i++) {
 | |
|     code = codePointAt(str, i);
 | |
|     if (code >= 0x10000) { // surrogates
 | |
|       i++;
 | |
|     }
 | |
|     if (code === 0x0a) { // new line \n
 | |
|       offset = 0;
 | |
|       row += 1;
 | |
|       continue;
 | |
|     }
 | |
|     if (isFullWidthCodePoint(code)) {
 | |
|       if ((offset + 1) % col === 0) {
 | |
|         offset++;
 | |
|       }
 | |
|       offset += 2;
 | |
|     } else {
 | |
|       offset++;
 | |
|     }
 | |
|   }
 | |
|   var cols = offset % col;
 | |
|   var rows = row + (offset - cols) / col;
 | |
|   return {cols: cols, rows: rows};
 | |
| };
 | |
| 
 | |
| 
 | |
| // Returns current cursor's position and line
 | |
| Interface.prototype._getCursorPos = function() {
 | |
|   var columns = this.columns;
 | |
|   var strBeforeCursor = this._prompt + this.line.substring(0, this.cursor);
 | |
|   var 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(codePointAt(this.line, 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) {
 | |
|   var oldcursor = this.cursor;
 | |
|   var 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;
 | |
| 
 | |
|   var 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)
 | |
|           );
 | |
|     }
 | |
|     exports.moveCursor(this.output, diffWidth, 0);
 | |
|     this.prevRows = newPos.rows;
 | |
|   } else {
 | |
|     this._refreshLine();
 | |
|   }
 | |
| };
 | |
| 
 | |
| 
 | |
| // handle a write from the tty
 | |
| Interface.prototype._ttyWrite = function(s, key) {
 | |
|   key = key || {};
 | |
| 
 | |
|   // Ignore escape key - Fixes #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 (EventEmitter.listenerCount(this, '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 the whole line
 | |
|         this.cursor = 0;
 | |
|         this.line = '';
 | |
|         this._refreshLine();
 | |
|         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(-1);
 | |
|         break;
 | |
| 
 | |
|       case 'f': // forward one character
 | |
|         this._moveCursor(+1);
 | |
|         break;
 | |
| 
 | |
|       case 'l': // clear the whole screen
 | |
|         exports.cursorTo(this.output, 0, 0);
 | |
|         exports.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 (EventEmitter.listenerCount(this, 'SIGTSTP') > 0) {
 | |
|           this.emit('SIGTSTP');
 | |
|         } else {
 | |
|           process.once('SIGCONT', (function(self) {
 | |
|             return function() {
 | |
|               // Don't raise events if stream has already been abandoned.
 | |
|               if (!self.paused) {
 | |
|                 // Stream must be paused and resumed after SIGCONT to catch
 | |
|                 // SIGINT, SIGTSTP, and EOF.
 | |
|                 self.pause();
 | |
|                 self.emit('SIGCONT');
 | |
|               }
 | |
|               // explicitly re-enable "raw mode" and move the cursor to
 | |
|               // the correct position.
 | |
|               // See https://github.com/joyent/node/issues/3295.
 | |
|               self._setRawMode(true);
 | |
|               self._refreshLine();
 | |
|             };
 | |
|           })(this));
 | |
|           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._sawReturn && key.name !== 'enter')
 | |
|       this._sawReturn = false;
 | |
| 
 | |
|     switch (key.name) {
 | |
|       case 'return':  // carriage return, i.e. \r
 | |
|         this._sawReturn = true;
 | |
|         this._line();
 | |
|         break;
 | |
| 
 | |
|       case 'enter':
 | |
|         if (this._sawReturn)
 | |
|           this._sawReturn = false;
 | |
|         else
 | |
|           this._line();
 | |
|         break;
 | |
| 
 | |
|       case 'backspace':
 | |
|         this._deleteLeft();
 | |
|         break;
 | |
| 
 | |
|       case 'delete':
 | |
|         this._deleteRight();
 | |
|         break;
 | |
| 
 | |
|       case 'tab': // tab completion
 | |
|         this._tabComplete();
 | |
|         break;
 | |
| 
 | |
|       case 'left':
 | |
|         this._moveCursor(-1);
 | |
|         break;
 | |
| 
 | |
|       case 'right':
 | |
|         this._moveCursor(+1);
 | |
|         break;
 | |
| 
 | |
|       case 'home':
 | |
|         this._moveCursor(-Infinity);
 | |
|         break;
 | |
| 
 | |
|       case 'end':
 | |
|         this._moveCursor(+Infinity);
 | |
|         break;
 | |
| 
 | |
|       case 'up':
 | |
|         this._historyPrev();
 | |
|         break;
 | |
| 
 | |
|       case 'down':
 | |
|         this._historyNext();
 | |
|         break;
 | |
| 
 | |
|       default:
 | |
|         if (util.isBuffer(s))
 | |
|           s = s.toString('utf-8');
 | |
| 
 | |
|         if (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]);
 | |
|           }
 | |
|         }
 | |
|     }
 | |
|   }
 | |
| };
 | |
| 
 | |
| 
 | |
| exports.Interface = Interface;
 | |
| 
 | |
| 
 | |
| 
 | |
| /**
 | |
|  * accepts a readable Stream instance and makes it emit "keypress" events
 | |
|  */
 | |
| 
 | |
| function emitKeypressEvents(stream) {
 | |
|   if (stream._keypressDecoder) return;
 | |
|   var StringDecoder = Require('string_decoder').StringDecoder; // lazy load
 | |
|   stream._keypressDecoder = new StringDecoder('utf8');
 | |
| 
 | |
|   function onData(b) {
 | |
|     if (EventEmitter.listenerCount(stream, 'keypress') > 0) {
 | |
|       var r = stream._keypressDecoder.write(b);
 | |
|       if (r) emitKeys(stream, r);
 | |
|     } 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 (EventEmitter.listenerCount(stream, 'keypress') > 0) {
 | |
|     stream.on('data', onData);
 | |
|   } else {
 | |
|     stream.on('newListener', onNewListener);
 | |
|   }
 | |
| }
 | |
| exports.emitKeypressEvents = emitKeypressEvents;
 | |
| 
 | |
| /*
 | |
|   Some patterns seen in terminal key escape codes, derived from combos seen
 | |
|   at http://www.midnight-commander.org/browser/lib/tty/key.c
 | |
| 
 | |
|   ESC letter
 | |
|   ESC [ letter
 | |
|   ESC [ modifier letter
 | |
|   ESC [ 1 ; modifier letter
 | |
|   ESC [ num char
 | |
|   ESC [ num ; modifier char
 | |
|   ESC O letter
 | |
|   ESC O modifier letter
 | |
|   ESC O 1 ; modifier letter
 | |
|   ESC N letter
 | |
|   ESC [ [ num ; modifier char
 | |
|   ESC [ [ 1 ; modifier letter
 | |
|   ESC ESC [ num char
 | |
|   ESC ESC O letter
 | |
| 
 | |
|   - char is usually ~ but $ and ^ also happen with rxvt
 | |
|   - modifier is 1 +
 | |
|                 (shift     * 1) +
 | |
|                 (left_alt  * 2) +
 | |
|                 (ctrl      * 4) +
 | |
|                 (right_alt * 8)
 | |
|   - two leading ESCs apparently mean the same as one leading ESC
 | |
| */
 | |
| 
 | |
| // Regexes used for ansi escape code splitting
 | |
| var metaKeyCodeReAnywhere = /(?:\x1b)([a-zA-Z0-9])/;
 | |
| var metaKeyCodeRe = new RegExp('^' + metaKeyCodeReAnywhere.source + '$');
 | |
| var functionKeyCodeReAnywhere = new RegExp('(?:\x1b+)(O|N|\\[|\\[\\[)(?:' + [
 | |
|   '(\\d+)(?:;(\\d+))?([~^$])',
 | |
|   '(?:M([@ #!a`])(.)(.))', // mouse
 | |
|   '(?:1;)?(\\d+)?([a-zA-Z])'
 | |
| ].join('|') + ')');
 | |
| var functionKeyCodeRe = new RegExp('^' + functionKeyCodeReAnywhere.source);
 | |
| var escapeCodeReAnywhere = new RegExp([
 | |
|   functionKeyCodeReAnywhere.source, metaKeyCodeReAnywhere.source, /\x1b./.source
 | |
| ].join('|'));
 | |
| 
 | |
| function emitKeys(stream, s) {
 | |
|   if (util.isBuffer(s)) {
 | |
|     if (s[0] > 127 && util.isUndefined(s[1])) {
 | |
|       s[0] -= 128;
 | |
|       s = '\x1b' + s.toString(stream.encoding || 'utf-8');
 | |
|     } else {
 | |
|       s = s.toString(stream.encoding || 'utf-8');
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   var buffer = [];
 | |
|   var match;
 | |
|   while (match = escapeCodeReAnywhere.exec(s)) {
 | |
|     buffer = buffer.concat(s.slice(0, match.index).split(''));
 | |
|     buffer.push(match[0]);
 | |
|     s = s.slice(match.index + match[0].length);
 | |
|   }
 | |
|   buffer = buffer.concat(s.split(''));
 | |
| 
 | |
|   buffer.forEach(function(s) {
 | |
|     var ch,
 | |
|         key = {
 | |
|           sequence: s,
 | |
|           name: undefined,
 | |
|           ctrl: false,
 | |
|           meta: false,
 | |
|           shift: false
 | |
|         },
 | |
|         parts;
 | |
| 
 | |
|     if (s === '\r') {
 | |
|       // carriage return
 | |
|       key.name = 'return';
 | |
| 
 | |
|     } else if (s === '\n') {
 | |
|       // enter, should have been called linefeed
 | |
|       key.name = 'enter';
 | |
| 
 | |
|     } else if (s === '\t') {
 | |
|       // tab
 | |
|       key.name = 'tab';
 | |
| 
 | |
|     } else if (s === '\b' || s === '\x7f' ||
 | |
|                s === '\x1b\x7f' || s === '\x1b\b') {
 | |
|       // backspace or ctrl+h
 | |
|       key.name = 'backspace';
 | |
|       key.meta = (s.charAt(0) === '\x1b');
 | |
| 
 | |
|     } else if (s === '\x1b' || s === '\x1b\x1b') {
 | |
|       // escape key
 | |
|       key.name = 'escape';
 | |
|       key.meta = (s.length === 2);
 | |
| 
 | |
|     } else if (s === ' ' || s === '\x1b ') {
 | |
|       key.name = 'space';
 | |
|       key.meta = (s.length === 2);
 | |
| 
 | |
|     } else if (s.length === 1 && s <= '\x1a') {
 | |
|       // ctrl+letter
 | |
|       key.name = String.fromCharCode(s.charCodeAt(0) + 'a'.charCodeAt(0) - 1);
 | |
|       key.ctrl = true;
 | |
| 
 | |
|     } else if (s.length === 1 && s >= 'a' && s <= 'z') {
 | |
|       // lowercase letter
 | |
|       key.name = s;
 | |
| 
 | |
|     } else if (s.length === 1 && s >= 'A' && s <= 'Z') {
 | |
|       // shift+letter
 | |
|       key.name = s.toLowerCase();
 | |
|       key.shift = true;
 | |
| 
 | |
|     } else if (parts = metaKeyCodeRe.exec(s)) {
 | |
|       // meta+character key
 | |
|       key.name = parts[1].toLowerCase();
 | |
|       key.meta = true;
 | |
|       key.shift = /^[A-Z]$/.test(parts[1]);
 | |
| 
 | |
|     } else if (parts = functionKeyCodeRe.exec(s)) {
 | |
|       // ansi escape sequence
 | |
| 
 | |
|       // reassemble the key code leaving out leading \x1b's,
 | |
|       // the modifier key bitflag and any meaningless "1;" sequence
 | |
|       var code = (parts[1] || '') + (parts[2] || '') +
 | |
|                  (parts[4] || '') + (parts[9] || ''),
 | |
|           modifier = (parts[3] || parts[8] || 1) - 1;
 | |
| 
 | |
|       // Parse the key modifier
 | |
|       key.ctrl = !!(modifier & 4);
 | |
|       key.meta = !!(modifier & 10);
 | |
|       key.shift = !!(modifier & 1);
 | |
|       key.code = code;
 | |
| 
 | |
|       // Parse the key itself
 | |
|       switch (code) {
 | |
|         /* xterm/gnome ESC O letter */
 | |
|         case 'OP': key.name = 'f1'; break;
 | |
|         case 'OQ': key.name = 'f2'; break;
 | |
|         case 'OR': key.name = 'f3'; break;
 | |
|         case 'OS': key.name = 'f4'; break;
 | |
| 
 | |
|         /* xterm/rxvt ESC [ number ~ */
 | |
|         case '[11~': key.name = 'f1'; break;
 | |
|         case '[12~': key.name = 'f2'; break;
 | |
|         case '[13~': key.name = 'f3'; break;
 | |
|         case '[14~': key.name = 'f4'; break;
 | |
| 
 | |
|         /* from Cygwin and used in libuv */
 | |
|         case '[[A': key.name = 'f1'; break;
 | |
|         case '[[B': key.name = 'f2'; break;
 | |
|         case '[[C': key.name = 'f3'; break;
 | |
|         case '[[D': key.name = 'f4'; break;
 | |
|         case '[[E': key.name = 'f5'; break;
 | |
| 
 | |
|         /* common */
 | |
|         case '[15~': key.name = 'f5'; break;
 | |
|         case '[17~': key.name = 'f6'; break;
 | |
|         case '[18~': key.name = 'f7'; break;
 | |
|         case '[19~': key.name = 'f8'; break;
 | |
|         case '[20~': key.name = 'f9'; break;
 | |
|         case '[21~': key.name = 'f10'; break;
 | |
|         case '[23~': key.name = 'f11'; break;
 | |
|         case '[24~': key.name = 'f12'; break;
 | |
| 
 | |
|         /* xterm ESC [ letter */
 | |
|         case '[A': key.name = 'up'; break;
 | |
|         case '[B': key.name = 'down'; break;
 | |
|         case '[C': key.name = 'right'; break;
 | |
|         case '[D': key.name = 'left'; break;
 | |
|         case '[E': key.name = 'clear'; break;
 | |
|         case '[F': key.name = 'end'; break;
 | |
|         case '[H': key.name = 'home'; break;
 | |
| 
 | |
|         /* xterm/gnome ESC O letter */
 | |
|         case 'OA': key.name = 'up'; break;
 | |
|         case 'OB': key.name = 'down'; break;
 | |
|         case 'OC': key.name = 'right'; break;
 | |
|         case 'OD': key.name = 'left'; break;
 | |
|         case 'OE': key.name = 'clear'; break;
 | |
|         case 'OF': key.name = 'end'; break;
 | |
|         case 'OH': key.name = 'home'; break;
 | |
| 
 | |
|         /* xterm/rxvt ESC [ number ~ */
 | |
|         case '[1~': key.name = 'home'; break;
 | |
|         case '[2~': key.name = 'insert'; break;
 | |
|         case '[3~': key.name = 'delete'; break;
 | |
|         case '[4~': key.name = 'end'; break;
 | |
|         case '[5~': key.name = 'pageup'; break;
 | |
|         case '[6~': key.name = 'pagedown'; break;
 | |
| 
 | |
|         /* putty */
 | |
|         case '[[5~': key.name = 'pageup'; break;
 | |
|         case '[[6~': key.name = 'pagedown'; break;
 | |
| 
 | |
|         /* rxvt */
 | |
|         case '[7~': key.name = 'home'; break;
 | |
|         case '[8~': key.name = 'end'; break;
 | |
| 
 | |
|         /* rxvt keys with modifiers */
 | |
|         case '[a': key.name = 'up'; key.shift = true; break;
 | |
|         case '[b': key.name = 'down'; key.shift = true; break;
 | |
|         case '[c': key.name = 'right'; key.shift = true; break;
 | |
|         case '[d': key.name = 'left'; key.shift = true; break;
 | |
|         case '[e': key.name = 'clear'; key.shift = true; break;
 | |
| 
 | |
|         case '[2$': key.name = 'insert'; key.shift = true; break;
 | |
|         case '[3$': key.name = 'delete'; key.shift = true; break;
 | |
|         case '[5$': key.name = 'pageup'; key.shift = true; break;
 | |
|         case '[6$': key.name = 'pagedown'; key.shift = true; break;
 | |
|         case '[7$': key.name = 'home'; key.shift = true; break;
 | |
|         case '[8$': key.name = 'end'; key.shift = true; break;
 | |
| 
 | |
|         case 'Oa': key.name = 'up'; key.ctrl = true; break;
 | |
|         case 'Ob': key.name = 'down'; key.ctrl = true; break;
 | |
|         case 'Oc': key.name = 'right'; key.ctrl = true; break;
 | |
|         case 'Od': key.name = 'left'; key.ctrl = true; break;
 | |
|         case 'Oe': key.name = 'clear'; key.ctrl = true; break;
 | |
| 
 | |
|         case '[2^': key.name = 'insert'; key.ctrl = true; break;
 | |
|         case '[3^': key.name = 'delete'; key.ctrl = true; break;
 | |
|         case '[5^': key.name = 'pageup'; key.ctrl = true; break;
 | |
|         case '[6^': key.name = 'pagedown'; key.ctrl = true; break;
 | |
|         case '[7^': key.name = 'home'; key.ctrl = true; break;
 | |
|         case '[8^': key.name = 'end'; key.ctrl = true; break;
 | |
| 
 | |
|         /* misc. */
 | |
|         case '[Z': key.name = 'tab'; key.shift = true; break;
 | |
|         default: key.name = 'undefined'; break;
 | |
| 
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // Don't emit a key if no name was found
 | |
|     if (util.isUndefined(key.name)) {
 | |
|       key = undefined;
 | |
|     }
 | |
| 
 | |
|     if (s.length === 1) {
 | |
|       ch = s;
 | |
|     }
 | |
| 
 | |
|     if (key || ch) {
 | |
|       stream.emit('keypress', ch, key);
 | |
|     }
 | |
|   });
 | |
| }
 | |
| 
 | |
| 
 | |
| /**
 | |
|  * moves the cursor to the x and y coordinate on the given stream
 | |
|  */
 | |
| 
 | |
| function cursorTo(stream, x, y) {
 | |
|   if (util.isNullOrUndefined(stream))
 | |
|     return;
 | |
| 
 | |
|   if (!util.isNumber(x) && !util.isNumber(y))
 | |
|     return;
 | |
| 
 | |
|   if (!util.isNumber(x))
 | |
|     throw new Error("Can't set cursor row without also setting it's column");
 | |
| 
 | |
|   if (!util.isNumber(y)) {
 | |
|     stream.write('\x1b[' + (x + 1) + 'G');
 | |
|   } else {
 | |
|     stream.write('\x1b[' + (y + 1) + ';' + (x + 1) + 'H');
 | |
|   }
 | |
| }
 | |
| exports.cursorTo = cursorTo;
 | |
| 
 | |
| 
 | |
| /**
 | |
|  * moves the cursor relative to its current location
 | |
|  */
 | |
| 
 | |
| function moveCursor(stream, dx, dy) {
 | |
|   if (util.isNullOrUndefined(stream))
 | |
|     return;
 | |
| 
 | |
|   if (dx < 0) {
 | |
|     stream.write('\x1b[' + (-dx) + 'D');
 | |
|   } else if (dx > 0) {
 | |
|     stream.write('\x1b[' + dx + 'C');
 | |
|   }
 | |
| 
 | |
|   if (dy < 0) {
 | |
|     stream.write('\x1b[' + (-dy) + 'A');
 | |
|   } else if (dy > 0) {
 | |
|     stream.write('\x1b[' + dy + 'B');
 | |
|   }
 | |
| }
 | |
| exports.moveCursor = moveCursor;
 | |
| 
 | |
| 
 | |
| /**
 | |
|  * 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) {
 | |
|   if (util.isNullOrUndefined(stream))
 | |
|     return;
 | |
| 
 | |
|   if (dir < 0) {
 | |
|     // to the beginning
 | |
|     stream.write('\x1b[1K');
 | |
|   } else if (dir > 0) {
 | |
|     // to the end
 | |
|     stream.write('\x1b[0K');
 | |
|   } else {
 | |
|     // entire line
 | |
|     stream.write('\x1b[2K');
 | |
|   }
 | |
| }
 | |
| exports.clearLine = clearLine;
 | |
| 
 | |
| 
 | |
| /**
 | |
|  * clears the screen from the current position of the cursor down
 | |
|  */
 | |
| 
 | |
| function clearScreenDown(stream) {
 | |
|   if (util.isNullOrUndefined(stream))
 | |
|     return;
 | |
| 
 | |
|   stream.write('\x1b[0J');
 | |
| }
 | |
| exports.clearScreenDown = clearScreenDown;
 | |
| 
 | |
| 
 | |
| /**
 | |
|  * Returns the number of columns required to display the given string.
 | |
|  */
 | |
| 
 | |
| function getStringWidth(str) {
 | |
|   var width = 0;
 | |
|   str = stripVTControlCharacters(str);
 | |
|   for (var i = 0, len = str.length; i < len; i++) {
 | |
|     var code = codePointAt(str, i);
 | |
|     if (code >= 0x10000) { // surrogates
 | |
|       i++;
 | |
|     }
 | |
|     if (isFullWidthCodePoint(code)) {
 | |
|       width += 2;
 | |
|     } else {
 | |
|       width++;
 | |
|     }
 | |
|   }
 | |
|   return width;
 | |
| }
 | |
| exports.getStringWidth = getStringWidth;
 | |
| 
 | |
| 
 | |
| /**
 | |
|  * Returns true if the character represented by a given
 | |
|  * Unicode code point is full-width. Otherwise returns false.
 | |
|  */
 | |
| 
 | |
| function isFullWidthCodePoint(code) {
 | |
|   if (isNaN(code)) {
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   // Code points are derived from:
 | |
|   // http://www.unicode.org/Public/UNIDATA/EastAsianWidth.txt
 | |
|   if (code >= 0x1100 && (
 | |
|       code <= 0x115f ||  // Hangul Jamo
 | |
|       0x2329 === code || // LEFT-POINTING ANGLE BRACKET
 | |
|       0x232a === code || // RIGHT-POINTING ANGLE BRACKET
 | |
|       // CJK Radicals Supplement .. Enclosed CJK Letters and Months
 | |
|       (0x2e80 <= code && code <= 0x3247 && code !== 0x303f) ||
 | |
|       // Enclosed CJK Letters and Months .. CJK Unified Ideographs Extension A
 | |
|       0x3250 <= code && code <= 0x4dbf ||
 | |
|       // CJK Unified Ideographs .. Yi Radicals
 | |
|       0x4e00 <= code && code <= 0xa4c6 ||
 | |
|       // Hangul Jamo Extended-A
 | |
|       0xa960 <= code && code <= 0xa97c ||
 | |
|       // Hangul Syllables
 | |
|       0xac00 <= code && code <= 0xd7a3 ||
 | |
|       // CJK Compatibility Ideographs
 | |
|       0xf900 <= code && code <= 0xfaff ||
 | |
|       // Vertical Forms
 | |
|       0xfe10 <= code && code <= 0xfe19 ||
 | |
|       // CJK Compatibility Forms .. Small Form Variants
 | |
|       0xfe30 <= code && code <= 0xfe6b ||
 | |
|       // Halfwidth and Fullwidth Forms
 | |
|       0xff01 <= code && code <= 0xff60 ||
 | |
|       0xffe0 <= code && code <= 0xffe6 ||
 | |
|       // Kana Supplement
 | |
|       0x1b000 <= code && code <= 0x1b001 ||
 | |
|       // Enclosed Ideographic Supplement
 | |
|       0x1f200 <= code && code <= 0x1f251 ||
 | |
|       // CJK Unified Ideographs Extension B .. Tertiary Ideographic Plane
 | |
|       0x20000 <= code && code <= 0x3fffd)) {
 | |
|     return true;
 | |
|   }
 | |
|   return false;
 | |
| }
 | |
| exports.isFullWidthCodePoint = isFullWidthCodePoint;
 | |
| 
 | |
| 
 | |
| /**
 | |
|  * Returns the Unicode code point for the character at the
 | |
|  * given index in the given string. Similar to String.charCodeAt(),
 | |
|  * but this function handles surrogates (code point >= 0x10000).
 | |
|  */
 | |
| 
 | |
| function codePointAt(str, index) {
 | |
|   var code = str.charCodeAt(index);
 | |
|   var low;
 | |
|   if (0xd800 <= code && code <= 0xdbff) { // High surrogate
 | |
|     low = str.charCodeAt(index + 1);
 | |
|     if (!isNaN(low)) {
 | |
|       code = 0x10000 + (code - 0xd800) * 0x400 + (low - 0xdc00);
 | |
|     }
 | |
|   }
 | |
|   return code;
 | |
| }
 | |
| exports.codePointAt = codePointAt;
 | |
| 
 | |
| 
 | |
| /**
 | |
|  * Tries to remove all VT control characters. Use to estimate displayed
 | |
|  * string width. May be buggy due to not running a real state machine
 | |
|  */
 | |
| function stripVTControlCharacters(str) {
 | |
|   str = str.replace(new RegExp(functionKeyCodeReAnywhere.source, 'g'), '');
 | |
|   return str.replace(new RegExp(metaKeyCodeReAnywhere.source, 'g'), '');
 | |
| }
 | |
| exports.stripVTControlCharacters = stripVTControlCharacters;
 | |
| 
 |