Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

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.
  • Loading branch information...
commit aad12d0b265c9b06ae029d6ee168849260a91dd6 1 parent ab518ae
@TooTallNate TooTallNate authored
View
71 doc/api/readline.markdown
@@ -3,7 +3,7 @@
Stability: 3 - Stable
To use this module, do `require('readline')`. Readline allows reading of a
-stream (such as STDIN) on a line-by-line basis.
+stream (such as `process.stdin`) on a line-by-line basis.
Note that once you've invoked this module, your node program will not
terminate until you've paused the interface. Here's how to allow your
@@ -11,35 +11,69 @@ program to gracefully pause:
var rl = require('readline');
- var i = rl.createInterface(process.stdin, process.stdout, null);
- i.question("What do you think of node.js?", function(answer) {
+ var i = rl.createInterface({
+ input: process.stdin,
+ output: process.stdout
+ });
+
+ i.question("What do you think of node.js? ", function(answer) {
// TODO: Log the answer in a database
- console.log("Thank you for your valuable feedback.");
+ console.log("Thank you for your valuable feedback:", answer);
i.pause();
});
-## rl.createInterface(input, output, completer)
+## rl.createInterface(options)
+
+Creates a readline `Interface` instance. Accepts an "options" Object that takes
+the following values:
+
+ - `input` - the readable stream to listen to (Required).
+
+ - `output` - the writable stream to write readline data to (Required).
+
+ - `completer` - an optional function that is used for Tab autocompletion. See
+ below for an example of using this.
+
+ - `terminal` - pass `true` if the `input` and `output` streams should be treated
+ like a TTY, and have ANSI/VT100 escape codes written to it. Defaults to
+ checking `isTTY` on the `output` stream upon instantiation.
+
+The `completer` function is given a the current line entered by the user, and
+is supposed to return an Array with 2 entries:
-Takes two streams and creates a readline interface. The `completer` function
-is used for autocompletion. When given a substring, it returns `[[substr1,
-substr2, ...], originalsubstring]`.
+ 1. An Array with matching entries for the completion.
+
+ 2. The substring that was used for the matching.
+
+Which ends up looking something like:
+`[[substr1, substr2, ...], originalsubstring]`.
Also `completer` can be run in async mode if it accepts two arguments:
- function completer(linePartial, callback) {
- callback(null, [['123'], linePartial]);
- }
+ function completer(linePartial, callback) {
+ callback(null, [['123'], linePartial]);
+ }
`createInterface` is commonly used with `process.stdin` and
`process.stdout` in order to accept user input:
- var readline = require('readline'),
- rl = readline.createInterface(process.stdin, process.stdout);
+ var readline = require('readline');
+ var rl = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout
+ });
+
+Once you have a readline instance, you most commonly listen for the `"line"` event.
+
+If `terminal` is `true` for this instance then the `output` stream will get the
+best compatability if it defines an `output.columns` property, and fires
+a `"resize"` event on the `output` if/when the columns ever change
+(`process.stdout` does this automatically when it is a TTY).
## Class: Interface
-The class that represents a readline interface with a stdin and stdout
+The class that represents a readline interface with an input and output
stream.
### rl.setPrompt(prompt, length)
@@ -72,18 +106,17 @@ Example usage:
### rl.pause()
-Pauses the readline `in` stream, allowing it to be resumed later if needed.
+Pauses the readline `input` stream, allowing it to be resumed later if needed.
### rl.resume()
-Resumes the readline `in` stream.
+Resumes the readline `input` stream.
### rl.write()
-Writes to tty.
+Writes to `output` stream.
-This will also resume the `in` stream used with `createInterface` if it has
-been paused.
+This will also resume the `input` stream if it has been paused.
### Event: 'line'
View
69 doc/api/repl.markdown
@@ -1,8 +1,8 @@
# REPL
-A Read-Eval-Print-Loop (REPL) is available both as a standalone program and easily
-includable in other programs. REPL provides a way to interactively run
-JavaScript and see the results. It can be used for debugging, testing, or
+A Read-Eval-Print-Loop (REPL) is available both as a standalone program and
+easily includable in other programs. The REPL provides a way to interactively
+run JavaScript and see the results. It can be used for debugging, testing, or
just trying things out.
By executing `node` without any arguments from the command-line you will be
@@ -19,26 +19,39 @@ dropped into the REPL. It has simplistic emacs line-editing.
2
3
-For advanced line-editors, start node with the environmental variable `NODE_NO_READLINE=1`.
-This will start the REPL in canonical terminal settings which will allow you to use with `rlwrap`.
+For advanced line-editors, start node with the environmental variable
+`NODE_NO_READLINE=1`. This will start the main and debugger REPL in canonical
+terminal settings which will allow you to use with `rlwrap`.
For example, you could add this to your bashrc file:
alias node="env NODE_NO_READLINE=1 rlwrap node"
-## repl.start([prompt], [stream], [eval], [useGlobal], [ignoreUndefined])
+## repl.start(options)
-Returns and starts a REPL with `prompt` as the prompt and `stream` for all I/O.
-`prompt` is optional and defaults to `> `. `stream` is optional and defaults to
-`process.stdin`. `eval` is optional too and defaults to async wrapper for
-`eval()`.
+Returns and starts a `REPLServer` instance. Accepts an "options" Object that
+takes the following values:
-If `useGlobal` is set to true, then the repl will use the global object,
-instead of running scripts in a separate context. Defaults to `false`.
+ - `prompt` - the prompt and `stream` for all I/O. Defaults to `> `.
-If `ignoreUndefined` is set to true, then the repl will not output return value
-of command if it's `undefined`. Defaults to `false`.
+ - `input` - the readable stream to listen to. Defaults to `process.stdin`.
+
+ - `output` - the writable stream to write readline data to. Defaults to
+ `process.stdout`.
+
+ - `terminal` - pass `true` if the `stream` should be treated like a TTY, and
+ have ANSI/VT100 escape codes written to it. Defaults to checking `isTTY`
+ on the `output` stream upon instantiation.
+
+ - `eval` - function that will be used to eval each given line. Defaults to
+ an async wrapper for `eval()`. See below for an example of a custom `eval`.
+
+ - `useGlobal` - if set to `true`, then the repl will use the `global` object,
+ instead of running scripts in a separate context. Defaults to `false`.
+
+ - `ignoreUndefined` - if set to `true`, then the repl will not output the
+ return value of command if it's `undefined`. Defaults to `false`.
You can use your own `eval` function if it has following signature:
@@ -56,16 +69,32 @@ Here is an example that starts a REPL on stdin, a Unix socket, and a TCP socket:
connections = 0;
- repl.start("node via stdin> ");
+ repl.start({
+ prompt: "node via stdin> ",
+ input: process.stdin,
+ output: process.stdout
+ });
net.createServer(function (socket) {
connections += 1;
- repl.start("node via Unix socket> ", socket);
+ repl.start({
+ prompt: "node via Unix socket> ",
+ input: socket,
+ output: socket
+ }).on('exit', function() {
+ socket.end();
+ })
}).listen("/tmp/node-repl-sock");
net.createServer(function (socket) {
connections += 1;
- repl.start("node via TCP socket> ", socket);
+ repl.start({
+ prompt: "node via TCP socket> ",
+ input: socket,
+ output: socket
+ }).on('exit', function() {
+ socket.end();
+ });
}).listen(5001);
Running this program from the command line will start a REPL on stdin. Other
@@ -76,6 +105,12 @@ TCP sockets.
By starting a REPL from a Unix socket-based server instead of stdin, you can
connect to a long-running node process without restarting it.
+For an example of running a "full-featured" (`terminal`) REPL over
+a `net.Server` and `net.Socket` instance, see: https://gist.github.com/2209310
+
+For an example of running a REPL instance over `curl(1)`,
+see: https://gist.github.com/2053342
+
### Event: 'exit'
`function () {}`
View
74 doc/api/tty.markdown
@@ -2,20 +2,18 @@
Stability: 3 - Stable
-Use `require('tty')` to access this module.
-
-Example:
-
- var tty = require('tty');
- process.stdin.resume();
- tty.setRawMode(true);
- process.stdin.on('keypress', function(char, key) {
- if (key && key.ctrl && key.name == 'c') {
- console.log('graceful exit');
- process.exit()
- }
- });
+The `tty` module houses the `tty.ReadStream` and `tty.WriteStream` classes. In
+most cases, you will not need to use this module directly.
+
+When node detects that it is being run inside a TTY context, then `process.stdin`
+will be a `tty.ReadStream` instance and `process.stdout` will be
+a `tty.WriteStream` instance. The preferred way to check if node is being run in
+a TTY context is to check `process.stdout.isTTY`:
+ $ node -p -e "Boolean(process.stdout.isTTY)"
+ true
+ $ node -p -e "Boolean(process.stdout.isTTY)" | cat
+ false
## tty.isatty(fd)
@@ -26,5 +24,51 @@ terminal.
## tty.setRawMode(mode)
-`mode` should be `true` or `false`. This sets the properties of the current
-process's stdin fd to act either as a raw device or default.
+Deprecated. Use `tty.ReadStream#setRawMode()` instead.
+
+
+## Class: ReadStream
+
+A `net.Socket` subclass that represents the readable portion of a tty. In normal
+circumstances, `process.stdin` will be the only `tty.ReadStream` instance in any
+node program (only when `isatty(0)` is true).
+
+### rs.isRaw
+
+A `Boolean` that is initialized to `false`. It represents the current "raw" state
+of the `tty.ReadStream` instance.
+
+### rs.setRawMode(mode)
+
+`mode` should be `true` or `false`. This sets the properties of the
+`tty.ReadStream` to act either as a raw device or default. `isRaw` will be set
+to the resulting mode.
+
+
+## Class WriteStream
+
+A `net.Socket` subclass that represents the writable portion of a tty. In normal
+circumstances, `process.stdout` will be the only `tty.WriteStream` instance
+ever created (and only when `isatty(1)` is true).
+
+### ws.columns
+
+A `Number` that gives the number of columns the TTY currently has. This property
+gets updated on "resize" events.
+
+### ws.rows
+
+A `Number` that gives the number of rows the TTY currently has. This property
+gets updated on "resize" events.
+
+### Event: 'resize'
+
+`function () {}`
+
+Emitted by `refreshSize()` when either of the `columns` or `rows` properties
+has changed.
+
+ process.stdout.on('resize', function() {
+ console.log('screen size has changed!');
+ console.log(process.stdout.columns + 'x' + process.stdout.rows);
+ });
View
16 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);
View
434 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;
View
62 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() {
View
348 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];
};
View
12 src/node.js
@@ -121,7 +121,12 @@
// If -i or --interactive were passed, or stdin is a TTY.
if (process._forceRepl || NativeModule.require('tty').isatty(0)) {
// REPL
- var repl = Module.requireRepl().start('> ', null, null, true);
+ var repl = Module.requireRepl().start({
+ prompt: '> ',
+ terminal: !parseInt(process.env['NODE_NO_READLINE'], 10),
+ useGlobal: true,
+ ignoreUndefined: false
+ });
repl.on('exit', function() {
process.exit();
});
@@ -320,6 +325,11 @@
er = er || new Error('process.stdout cannot be closed.');
stdout.emit('error', er);
};
+ if (stdout.isTTY) {
+ process.on('SIGWINCH', function() {
+ stdout._refreshSize();
+ });
+ }
return stdout;
});
Please sign in to comment.
Something went wrong with that request. Please try again.