node/lib/readline.js
Rich Trott 552e25cb6b lib,test: update in preparation for linter update
ESLint 2.9.0 fixes some bugs that resulted in minor issues not being
caught by ESLint 2.7.0. Update instances of our code that will be
flagged when we upgrade to ESLint 2.9.0.

PR-URL: https://github.com/nodejs/node/pull/6498
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Roman Reiss <me@silverwind.io>
2016-06-23 17:25:14 -07:00

1448 lines
37 KiB
JavaScript

// Inspiration for this code comes from Salvatore Sanfilippo's linenoise.
// https://github.com/antirez/linenoise
// Reference:
// * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
// * http://www.3waylabs.com/nw/WWW/products/wizcon/vt220.html
'use strict';
const kHistorySize = 30;
const util = require('util');
const debug = util.debuglog('readline');
const internalUtil = require('internal/util');
const inherits = util.inherits;
const Buffer = require('buffer').Buffer;
const EventEmitter = require('events');
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)) {
// call the constructor preserving original number of arguments
const self = Object.create(Interface.prototype);
Interface.apply(self, arguments);
return self;
}
this._sawReturn = false;
EventEmitter.call(this);
var historySize;
if (arguments.length === 1) {
// an options object was given
output = input.output;
completer = input.completer;
terminal = input.terminal;
historySize = input.historySize;
input = input.input;
}
historySize = historySize || kHistorySize;
if (completer && typeof completer !== 'function') {
throw new TypeError('Argument \'completer\' must be a function');
}
if (typeof historySize !== 'number' ||
isNaN(historySize) ||
historySize < 0) {
throw new TypeError('Argument \'historySize\' must be a positive number');
}
// backwards compat; check the isTTY prop of the output stream
// when `terminal` was not specified
if (terminal === undefined && !(output === null || output === undefined)) {
terminal = !!output.isTTY;
}
var self = this;
this.output = output;
this.input = input;
this.historySize = historySize;
// Check arity, 2 - for async, 1 for sync
if (typeof completer === 'function') {
this.completer = completer.length === 2 ? completer : function(v, cb) {
cb(null, completer(v));
};
}
this.setPrompt('> ');
this.terminal = !!terminal;
function ondata(data) {
self._normalWrite(data);
}
function onend() {
if (typeof self._line_buffer === 'string' &&
self._line_buffer.length > 0) {
self.emit('line', self._line_buffer);
}
self.close();
}
function ontermend() {
if (typeof self.line === 'string' && self.line.length > 0) {
self.emit('line', self.line);
}
self.close();
}
function onkeypress(s, key) {
self._ttyWrite(s, key);
}
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);
});
var StringDecoder = require('string_decoder').StringDecoder; // lazy load
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 (output !== null && output !== undefined)
output.on('resize', onresize);
self.once('close', function() {
input.removeListener('keypress', onkeypress);
input.removeListener('end', ontermend);
if (output !== null && output !== undefined) {
output.removeListener('resize', onresize);
}
});
}
input.resume();
}
inherits(Interface, EventEmitter);
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 (typeof this.input.setRawMode === 'function') {
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 (typeof cb === 'function') {
if (this._questionCallback) {
this.prompt();
} else {
this._oldPrompt = this._prompt;
this.setPrompt(query);
this._questionCallback = cb;
this.prompt();
}
}
};
Interface.prototype._onLine = function(line) {
if (this._questionCallback) {
var cb = this._questionCallback;
this._questionCallback = null;
this.setPrompt(this._oldPrompt);
cb(line);
} else {
this.emit('line', line);
}
};
Interface.prototype._writeToOutput = function _writeToOutput(stringToWrite) {
if (typeof stringToWrite !== 'string')
throw new TypeError('stringToWrite must be a string');
if (this.output !== null && this.output !== undefined)
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 > this.historySize) 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
const lineEnding = /\r?\n|\r(?!\n)/;
Interface.prototype._normalWrite = function(b) {
if (b === undefined) {
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 = string.endsWith('\r');
// 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) {
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) {
debug('tab completion error %j', err);
return;
}
const completions = rv[0];
const 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);
if (!maxColumns || maxColumns === Infinity) {
maxColumns = 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;
};
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 = str.codePointAt(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(this.line.codePointAt(this.cursor))) {
rows++;
cols = 0;
}
return {cols: cols, rows: rows};
};
// This function moves cursor dx places to the right
// (-dx for left) and refreshes the line if it is needed
Interface.prototype._moveCursor = function(dx) {
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 (this.listenerCount('SIGINT') > 0) {
this.emit('SIGINT');
} else {
// This readline instance is finished
this.close();
}
break;
case 'h': // delete left
this._deleteLeft();
break;
case 'd': // delete right or EOF
if (this.cursor === 0 && this.line.length === 0) {
// This readline instance is finished
this.close();
} else if (this.cursor < this.line.length) {
this._deleteRight();
}
break;
case 'u': // delete 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 (this.listenerCount('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 '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;
case 'tab':
// If tab completion enabled, do that...
if (typeof this.completer === 'function') {
this._tabComplete();
break;
}
// falls through
default:
if (s instanceof Buffer)
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
*/
const KEYPRESS_DECODER = Symbol('keypress-decoder');
const ESCAPE_DECODER = Symbol('escape-decoder');
function emitKeypressEvents(stream) {
if (stream[KEYPRESS_DECODER]) return;
var StringDecoder = require('string_decoder').StringDecoder; // lazy load
stream[KEYPRESS_DECODER] = new StringDecoder('utf8');
stream[ESCAPE_DECODER] = emitKeys(stream);
stream[ESCAPE_DECODER].next();
function onData(b) {
if (stream.listenerCount('keypress') > 0) {
var r = stream[KEYPRESS_DECODER].write(b);
if (r) {
for (var i = 0; i < r.length; i++) {
try {
stream[ESCAPE_DECODER].next(r[i]);
} catch (err) {
// if the generator throws (it could happen in the `keypress`
// event), we need to restart it.
stream[ESCAPE_DECODER] = emitKeys(stream);
stream[ESCAPE_DECODER].next();
throw err;
}
}
}
} else {
// Nobody's watching anyway
stream.removeListener('data', onData);
stream.on('newListener', onNewListener);
}
}
function onNewListener(event) {
if (event == 'keypress') {
stream.on('data', onData);
stream.removeListener('newListener', onNewListener);
}
}
if (stream.listenerCount('keypress') > 0) {
stream.on('data', onData);
} else {
stream.on('newListener', onNewListener);
}
}
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
// eslint-disable-next-line no-control-regex
const metaKeyCodeReAnywhere = /(?:\x1b)([a-zA-Z0-9])/;
const functionKeyCodeReAnywhere = new RegExp('(?:\x1b+)(O|N|\\[|\\[\\[)(?:' + [
'(\\d+)(?:;(\\d+))?([~^$])',
'(?:M([@ #!a`])(.)(.))', // mouse
'(?:1;)?(\\d+)?([a-zA-Z])'
].join('|') + ')');
function* emitKeys(stream) {
while (true) {
var ch = yield;
var s = ch;
var escaped = false;
var key = {
sequence: null,
name: undefined,
ctrl: false,
meta: false,
shift: false
};
if (ch === '\x1b') {
escaped = true;
s += (ch = yield);
if (ch === '\x1b') {
s += (ch = yield);
}
}
if (escaped && (ch === 'O' || ch === '[')) {
// ansi escape sequence
var code = ch;
var modifier = 0;
if (ch === 'O') {
// ESC O letter
// ESC O modifier letter
s += (ch = yield);
if (ch >= '0' && ch <= '9') {
modifier = (ch >> 0) - 1;
s += (ch = yield);
}
code += ch;
} else if (ch === '[') {
// ESC [ letter
// ESC [ modifier letter
// ESC [ [ modifier letter
// ESC [ [ num char
s += (ch = yield);
if (ch === '[') {
// \x1b[[A
// ^--- escape codes might have a second bracket
code += ch;
s += (ch = yield);
}
/*
* Here and later we try to buffer just enough data to get
* a complete ascii sequence.
*
* We have basically two classes of ascii characters to process:
*
*
* 1. `\x1b[24;5~` should be parsed as { code: '[24~', modifier: 5 }
*
* This particular example is featuring Ctrl+F12 in xterm.
*
* - `;5` part is optional, e.g. it could be `\x1b[24~`
* - first part can contain one or two digits
*
* So the generic regexp is like /^\d\d?(;\d)?[~^$]$/
*
*
* 2. `\x1b[1;5H` should be parsed as { code: '[H', modifier: 5 }
*
* This particular example is featuring Ctrl+Home in xterm.
*
* - `1;5` part is optional, e.g. it could be `\x1b[H`
* - `1;` part is optional, e.g. it could be `\x1b[5H`
*
* So the generic regexp is like /^((\d;)?\d)?[A-Za-z]$/
*
*/
const cmdStart = s.length - 1;
// skip one or two leading digits
if (ch >= '0' && ch <= '9') {
s += (ch = yield);
if (ch >= '0' && ch <= '9') {
s += (ch = yield);
}
}
// skip modifier
if (ch === ';') {
s += (ch = yield);
if (ch >= '0' && ch <= '9') {
s += (ch = yield);
}
}
/*
* We buffered enough data, now trying to extract code
* and modifier from it
*/
const cmd = s.slice(cmdStart);
var match;
if ((match = cmd.match(/^(\d\d?)(;(\d))?([~^$])$/))) {
code += match[1] + match[4];
modifier = (match[3] || 1) - 1;
} else if ((match = cmd.match(/^((\d;)?(\d))?([A-Za-z])$/))) {
code += match[4];
modifier = (match[3] || 1) - 1;
} else {
code += cmd;
}
}
// 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;
}
} else if (ch === '\r') {
// carriage return
key.name = 'return';
} else if (ch === '\n') {
// enter, should have been called linefeed
key.name = 'enter';
} else if (ch === '\t') {
// tab
key.name = 'tab';
} else if (ch === '\b' || ch === '\x7f') {
// backspace or ctrl+h
key.name = 'backspace';
key.meta = escaped;
} else if (ch === '\x1b') {
// escape key
key.name = 'escape';
key.meta = escaped;
} else if (ch === ' ') {
key.name = 'space';
key.meta = escaped;
} else if (!escaped && ch <= '\x1a') {
// ctrl+letter
key.name = String.fromCharCode(ch.charCodeAt(0) + 'a'.charCodeAt(0) - 1);
key.ctrl = true;
} else if (/^[0-9A-Za-z]$/.test(ch)) {
// letter, number, shift+letter
key.name = ch.toLowerCase();
key.shift = /^[A-Z]$/.test(ch);
key.meta = escaped;
}
key.sequence = s;
if (key.name !== undefined) {
/* Named character or sequence */
stream.emit('keypress', escaped ? undefined : s, key);
} else if (s.length === 1) {
/* Single unnamed character, e.g. "." */
stream.emit('keypress', s);
} else {
/* Unrecognized or broken escape sequence, don't emit anything */
}
}
}
/**
* moves the cursor to the x and y coordinate on the given stream
*/
function cursorTo(stream, x, y) {
if (stream === null || stream === undefined)
return;
if (typeof x !== 'number' && typeof y !== 'number')
return;
if (typeof x !== 'number')
throw new Error("Can't set cursor row without also setting it's column");
if (typeof y !== 'number') {
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 (stream === null || stream === undefined)
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 (stream === null || stream === undefined)
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 (stream === null || stream === undefined)
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 = str.codePointAt(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 = internalUtil.deprecate(codePointAt,
'readline.codePointAt is deprecated. ' +
'Use String.prototype.codePointAt instead.');
/**
* 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;