aboutsummaryrefslogtreecommitdiff
path: root/lib/readline.js
diff options
context:
space:
mode:
authorNathan Rajlich <nathan@tootallnate.net>2012-03-26 15:21:25 -0700
committerNathan Rajlich <nathan@tootallnate.net>2012-03-26 15:21:25 -0700
commitaad12d0b265c9b06ae029d6ee168849260a91dd6 (patch)
tree7aa1deffdfc5216b50beb4c43eaf09088643a67b /lib/readline.js
parentab518ae50e480b5ffe01bdaaabd8c652200d1d55 (diff)
downloadandroid-node-v8-aad12d0b265c9b06ae029d6ee168849260a91dd6.tar.gz
android-node-v8-aad12d0b265c9b06ae029d6ee168849260a91dd6.tar.bz2
android-node-v8-aad12d0b265c9b06ae029d6ee168849260a91dd6.zip
readline: migrate ansi/vt100 logic from tty to readline
The overall goal here is to make readline more interoperable with other node Streams like say a net.Socket instance, in "terminal" mode. See #2922 for all the details. Closes #2922.
Diffstat (limited to 'lib/readline.js')
-rw-r--r--lib/readline.js434
1 files changed, 394 insertions, 40 deletions
diff --git a/lib/readline.js b/lib/readline.js
index 3a3d244f22..94d88ed259 100644
--- a/lib/readline.js
+++ b/lib/readline.js
@@ -31,18 +31,32 @@ var kBufSize = 10 * 1024;
var util = require('util');
var inherits = require('util').inherits;
var EventEmitter = require('events').EventEmitter;
-var tty = require('tty');
-exports.createInterface = function(input, output, completer) {
- return new Interface(input, output, completer);
+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) {
+function Interface(input, output, completer, terminal) {
if (!(this instanceof Interface)) {
- return new Interface(input, output, completer);
+ return new Interface(input, output, completer, terminal);
}
+
+ if (arguments.length === 1) {
+ // an options object was given
+ output = input.output;
+ completer = input.completer;
+ terminal = input.terminal;
+ input = input.input;
+ }
+
EventEmitter.call(this);
completer = completer || function() { return []; };
@@ -51,6 +65,12 @@ function Interface(input, output, 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 (typeof terminal == 'undefined') {
+ terminal = !!output.isTTY;
+ }
+
var self = this;
this.output = output;
@@ -64,19 +84,17 @@ function Interface(input, output, completer) {
this.setPrompt('> ');
- this.enabled = output.isTTY;
-
- if (parseInt(process.env['NODE_NO_READLINE'], 10)) {
- this.enabled = false;
- }
+ this.terminal = !!terminal;
- if (!this.enabled) {
+ if (!this.terminal) {
input.on('data', function(data) {
self._normalWrite(data);
});
} else {
+ exports.emitKeypressEvents(input);
+
// input usually refers to stdin
input.on('keypress', function(s, key) {
self._ttyWrite(s, key);
@@ -85,9 +103,10 @@ function Interface(input, output, completer) {
// Current line
this.line = '';
- // Check process.env.TERM ?
- tty.setRawMode(true);
- this.enabled = true;
+ if (typeof input.setRawMode === 'function') {
+ input.setRawMode(true);
+ }
+ this.terminal = true;
// Cursor position on the line.
this.cursor = 0;
@@ -95,26 +114,16 @@ function Interface(input, output, completer) {
this.history = [];
this.historyIndex = -1;
- var winSize = output.getWindowSize();
- exports.columns = winSize[0];
-
- if (process.listeners('SIGWINCH').length === 0) {
- process.on('SIGWINCH', function() {
- var winSize = output.getWindowSize();
- exports.columns = winSize[0];
-
- // FIXME: when #2922 will be approved, change this to
- // output.on('resize', ...
- self._refreshLine();
- });
- }
+ output.on('resize', function() {
+ self._refreshLine();
+ });
}
}
inherits(Interface, EventEmitter);
Interface.prototype.__defineGetter__('columns', function() {
- return exports.columns;
+ return this.output.columns || Infinity;
});
Interface.prototype.setPrompt = function(prompt, length) {
@@ -131,7 +140,7 @@ Interface.prototype.setPrompt = function(prompt, length) {
Interface.prototype.prompt = function(preserveCursor) {
if (this.paused) this.resume();
- if (this.enabled) {
+ if (this.terminal) {
if (!preserveCursor) this.cursor = 0;
this._refreshLine();
} else {
@@ -194,13 +203,13 @@ Interface.prototype._refreshLine = function() {
// first move to the bottom of the current line, based on cursor pos
var prevRows = this.prevRows || 0;
if (prevRows > 0) {
- this.output.moveCursor(0, -prevRows);
+ exports.moveCursor(this.output, 0, -prevRows);
}
// Cursor to left edge.
- this.output.cursorTo(0);
+ exports.cursorTo(this.output, 0);
// erase data
- this.output.clearScreenDown();
+ exports.clearScreenDown(this.output);
// Write the prompt and the current buffer content.
this.output.write(line);
@@ -211,11 +220,11 @@ Interface.prototype._refreshLine = function() {
}
// Move cursor to original position.
- this.output.cursorTo(cursorPos.cols);
+ exports.cursorTo(this.output, cursorPos.cols);
var diff = lineRows - cursorPos.rows;
if (diff > 0) {
- this.output.moveCursor(0, -diff);
+ exports.moveCursor(this.output, 0, -diff);
}
this.prevRows = cursorPos.rows;
@@ -224,8 +233,10 @@ Interface.prototype._refreshLine = function() {
Interface.prototype.pause = function() {
if (this.paused) return;
- if (this.enabled) {
- tty.setRawMode(false);
+ if (this.terminal) {
+ if (typeof this.input.setRawMode === 'function') {
+ this.input.setRawMode(true);
+ }
}
this.input.pause();
this.paused = true;
@@ -235,8 +246,10 @@ Interface.prototype.pause = function() {
Interface.prototype.resume = function() {
this.input.resume();
- if (this.enabled) {
- tty.setRawMode(true);
+ if (this.terminal) {
+ if (typeof this.input.setRawMode === 'function') {
+ this.input.setRawMode(true);
+ }
}
this.paused = false;
this.emit('resume');
@@ -245,7 +258,7 @@ Interface.prototype.resume = function() {
Interface.prototype.write = function(d, key) {
if (this.paused) this.resume();
- this.enabled ? this._ttyWrite(d, key) : this._normalWrite(d, key);
+ this.terminal ? this._ttyWrite(d, key) : this._normalWrite(d, key);
};
@@ -514,7 +527,7 @@ Interface.prototype._moveCursor = function(dx) {
// check if cursors are in the same line
if (oldPos.rows === newPos.rows) {
- this.output.moveCursor(this.cursor - oldcursor, 0);
+ exports.moveCursor(this.output, this.cursor - oldcursor, 0);
this.prevRows = newPos.rows;
} else {
this._refreshLine();
@@ -728,3 +741,344 @@ Interface.prototype._ttyWrite = function(s, key) {
exports.Interface = Interface;
+
+
+
+/**
+ * accepts a readable Stream instance and makes it emit "keypress" events
+ */
+
+function emitKeypressEvents(stream) {
+ if (stream._emitKeypress) return;
+ stream._emitKeypress = true;
+
+ var keypressListeners = stream.listeners('keypress');
+
+ function onData(b) {
+ if (keypressListeners.length) {
+ emitKey(stream, b);
+ } 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 (keypressListeners.length) {
+ 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 metaKeyCodeRe = /^(?:\x1b)([a-zA-Z0-9])$/;
+var functionKeyCodeRe =
+ /^(?:\x1b+)(O|N|\[|\[\[)(?:(\d+)(?:;(\d+))?([~^$])|(?:1;)?(\d+)?([a-zA-Z]))/;
+
+function emitKey(stream, s) {
+ var char,
+ key = {
+ name: undefined,
+ ctrl: false,
+ meta: false,
+ shift: false
+ },
+ parts;
+
+ if (Buffer.isBuffer(s)) {
+ if (s[0] > 127 && s[1] === undefined) {
+ s[0] -= 128;
+ s = '\x1b' + s.toString(stream.encoding || 'utf-8');
+ } else {
+ s = s.toString(stream.encoding || 'utf-8');
+ }
+ }
+
+ key.sequence = s;
+
+ if (s === '\r' || s === '\n') {
+ // enter
+ 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 <= '\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[6] || ''),
+ modifier = (parts[3] || parts[5] || 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;
+
+ }
+ } else if (s.length > 1 && s[0] !== '\x1b') {
+ // Got a longer-than-one string of characters.
+ // Probably a paste, since it wasn't a control sequence.
+ Array.prototype.forEach.call(s, function(c) {
+ emitKey(stream, c);
+ });
+ return;
+ }
+
+ // Don't emit a key if no name was found
+ if (key.name === undefined) {
+ key = undefined;
+ }
+
+ if (s.length === 1) {
+ char = s;
+ }
+
+ if (key || char) {
+ stream.emit('keypress', char, key);
+ }
+}
+
+
+/**
+ * moves the cursor to the x and y coordinate on the given stream
+ */
+
+function cursorTo(stream, x, y) {
+ 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 (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 (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) {
+ stream.write('\x1b[0J');
+}
+exports.clearScreenDown = clearScreenDown;