Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nodejs/node.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorNathan Rajlich <nathan@tootallnate.net>2012-03-27 02:21:25 +0400
committerNathan Rajlich <nathan@tootallnate.net>2012-03-27 02:21:25 +0400
commitaad12d0b265c9b06ae029d6ee168849260a91dd6 (patch)
tree7aa1deffdfc5216b50beb4c43eaf09088643a67b /lib
parentab518ae50e480b5ffe01bdaaabd8c652200d1d55 (diff)
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')
-rw-r--r--lib/_debugger.js16
-rw-r--r--lib/readline.js434
-rw-r--r--lib/repl.js62
-rw-r--r--lib/tty.js348
4 files changed, 483 insertions, 377 deletions
diff --git a/lib/_debugger.js b/lib/_debugger.js
index f454aa9c4cc..dcf046bcf5a 100644
--- a/lib/_debugger.js
+++ b/lib/_debugger.js
@@ -745,15 +745,17 @@ function Interface(stdin, stdout, args) {
this.stdout = stdout;
this.args = args;
- var streams = {
- stdin: stdin,
- stdout: stdout
- };
-
// Two eval modes are available: controlEval and debugEval
// But controlEval is used by default
- this.repl = new repl.REPLServer('debug> ', streams,
- this.controlEval.bind(this), false, true);
+ this.repl = repl.start({
+ prompt: 'debug> ',
+ input: this.stdin,
+ output: this.stdout,
+ terminal: !parseInt(process.env['NODE_NO_READLINE'], 10),
+ eval: this.controlEval.bind(this),
+ useGlobal: false,
+ ignoreUndefined: true
+ });
// Do not print useless warning
repl._builtinLibs.splice(repl._builtinLibs.indexOf('repl'), 1);
diff --git a/lib/readline.js b/lib/readline.js
index 3a3d244f22e..94d88ed2592 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;
diff --git a/lib/repl.js b/lib/repl.js
index 9f2d486b21c..c96cbe14f41 100644
--- a/lib/repl.js
+++ b/lib/repl.js
@@ -76,6 +76,27 @@ exports._builtinLibs = ['assert', 'buffer', 'child_process', 'cluster',
function REPLServer(prompt, stream, eval, useGlobal, ignoreUndefined) {
+ if (!(this instanceof REPLServer)) {
+ return new REPLServer(prompt, stream, eval, useGlobal, ignoreUndefined);
+ }
+
+ var options, input, output;
+ if (typeof prompt == 'object') {
+ // an options object was given
+ options = prompt;
+ stream = options.stream || options.socket;
+ input = options.input;
+ output = options.output;
+ eval = options.eval;
+ useGlobal = options.useGlobal;
+ ignoreUndefined = options.ignoreUndefined;
+ prompt = options.prompt;
+ } else if (typeof prompt != 'string') {
+ throw new Error('An options Object, or a prompt String are required');
+ } else {
+ options = {};
+ }
+
EventEmitter.call(this);
var self = this;
@@ -99,34 +120,45 @@ function REPLServer(prompt, stream, eval, useGlobal, ignoreUndefined) {
self.resetContext();
self.bufferedCommand = '';
- if (stream) {
- // We're given a duplex socket
- if (stream.stdin || stream.stdout) {
- self.outputStream = stream.stdout;
- self.inputStream = stream.stdin;
+ if (!input && !output) {
+ // legacy API, passing a 'stream'/'socket' option
+ if (!stream) {
+ // use stdin and stdout as the default streams if none were given
+ stream = process;
+ }
+ if (stream.stdin && stream.stdout) {
+ // We're given custom object with 2 streams, or the `process` object
+ input = stream.stdin;
+ output = stream.stdout;
} else {
- self.outputStream = stream;
- self.inputStream = stream;
+ // We're given a duplex readable/writable Stream, like a `net.Socket`
+ input = stream;
+ output = stream;
}
- } else {
- self.outputStream = process.stdout;
- self.inputStream = process.stdin;
- process.stdin.resume();
}
+
+ self.inputStream = input;
+ self.outputStream = output;
+
self.prompt = (prompt != undefined ? prompt : '> ');
function complete(text, callback) {
self.complete(text, callback);
}
- var rli = rl.createInterface(self.inputStream, self.outputStream, complete);
+ var rli = rl.createInterface({
+ input: self.inputStream,
+ output: self.outputStream,
+ completer: complete,
+ terminal: options.terminal
+ });
self.rli = rli;
this.commands = {};
defineDefaultCommands(this);
- if (rli.enabled && !exports.disableColors &&
+ if (rli.terminal && !exports.disableColors &&
exports.writer === util.inspect) {
// Turn on ANSI coloring.
exports.writer = function(obj, showHidden, depth) {
@@ -322,10 +354,6 @@ REPLServer.prototype.displayPrompt = function(preserveCursor) {
};
-// read a line from the stream, then eval it
-REPLServer.prototype.readline = function(cmd) {
-};
-
// A stream to push an array into a REPL
// used in REPLServer.complete
function ArrayStream() {
diff --git a/lib/tty.js b/lib/tty.js
index 72fc5e57ab1..5bb73ff0a18 100644
--- a/lib/tty.js
+++ b/lib/tty.js
@@ -25,28 +25,17 @@ var net = require('net');
var TTY = process.binding('tty_wrap').TTY;
var isTTY = process.binding('tty_wrap').isTTY;
-var stdinHandle;
-
-
exports.isatty = function(fd) {
return isTTY(fd);
};
+// backwards-compat
exports.setRawMode = function(flag) {
- assert.ok(stdinHandle, 'stdin must be initialized before calling setRawMode');
- stdinHandle.setRawMode(flag);
-};
-
-
-exports.getWindowSize = function() {
- //throw new Error("implement me");
- return 80;
-};
-
-
-exports.setWindowSize = function() {
- throw new Error('implement me');
+ if (!process.stdin.isTTY) {
+ throw new Error('can\'t set raw mode on non-tty');
+ }
+ process.stdin.setRawMode(flag);
};
@@ -56,31 +45,9 @@ function ReadStream(fd) {
handle: new TTY(fd, true)
});
+ this.readable = true;
this.writable = false;
-
- var self = this,
- keypressListeners = this.listeners('keypress');
-
- function onData(b) {
- if (keypressListeners.length) {
- self._emitKey(b);
- } else {
- // Nobody's watching anyway
- self.removeListener('data', onData);
- self.on('newListener', onNewListener);
- }
- }
-
- function onNewListener(event) {
- if (event == 'keypress') {
- self.on('data', onData);
- self.removeListener('newListener', onNewListener);
- }
- }
-
- if (!stdinHandle) stdinHandle = this._handle;
-
- this.on('newListener', onNewListener);
+ this.isRaw = false;
}
inherits(ReadStream, net.Socket);
@@ -96,242 +63,15 @@ ReadStream.prototype.resume = function() {
return net.Socket.prototype.resume.call(this);
};
+ReadStream.prototype.setRawMode = function(flag) {
+ flag = !!flag;
+ this._handle.setRawMode(flag);
+ this.isRaw = flag;
+};
ReadStream.prototype.isTTY = true;
-/*
- 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]))/;
-
-
-ReadStream.prototype._emitKey = function(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(this.encoding || 'utf-8');
- } else {
- s = s.toString(this.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, this._emitKey, this);
- 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) {
- this.emit('keypress', char, key);
- }
-};
-
function WriteStream(fd) {
if (!(this instanceof WriteStream)) return new WriteStream(fd);
@@ -341,6 +81,10 @@ function WriteStream(fd) {
this.readable = false;
this.writable = true;
+
+ var winSize = this._handle.getWindowSize();
+ this.columns = winSize[0];
+ this.rows = winSize[1];
}
inherits(WriteStream, net.Socket);
exports.WriteStream = WriteStream;
@@ -349,55 +93,33 @@ exports.WriteStream = WriteStream;
WriteStream.prototype.isTTY = true;
-WriteStream.prototype.cursorTo = function(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') {
- this.write('\x1b[' + (x + 1) + 'G');
- } else {
- this.write('\x1b[' + (y + 1) + ';' + (x + 1) + 'H');
+WriteStream.prototype._refreshSize = function() {
+ var oldCols = this.columns;
+ var oldRows = this.rows;
+ var winSize = this._handle.getWindowSize();
+ var newCols = winSize[0];
+ var newRows = winSize[1];
+ if (oldCols !== newCols || oldRows !== newRows) {
+ this.columns = newCols;
+ this.rows = newRows;
+ this.emit('resize');
}
-};
+}
+// backwards-compat
+WriteStream.prototype.cursorTo = function(x, y) {
+ require('readline').cursorTo(this, x, y);
+};
WriteStream.prototype.moveCursor = function(dx, dy) {
- if (dx < 0) {
- this.write('\x1b[' + (-dx) + 'D');
- } else if (dx > 0) {
- this.write('\x1b[' + dx + 'C');
- }
-
- if (dy < 0) {
- this.write('\x1b[' + (-dy) + 'A');
- } else if (dy > 0) {
- this.write('\x1b[' + dy + 'B');
- }
+ require('readline').moveCursor(this, dx, dy);
};
-
-
WriteStream.prototype.clearLine = function(dir) {
- if (dir < 0) {
- // to the beginning
- this.write('\x1b[1K');
- } else if (dir > 0) {
- // to the end
- this.write('\x1b[0K');
- } else {
- // entire line
- this.write('\x1b[2K');
- }
+ require('readline').clearLine(this, dir);
};
-
-
WriteStream.prototype.clearScreenDown = function() {
- this.write('\x1b[0J');
+ require('readline').clearScreenDown(this);
};
-
-
WriteStream.prototype.getWindowSize = function() {
- return this._handle.getWindowSize();
+ return [this.columns, this.rows];
};