Browse Source

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.
v0.9.1-release
Nathan Rajlich 13 years ago
parent
commit
aad12d0b26
  1. 71
      doc/api/readline.markdown
  2. 69
      doc/api/repl.markdown
  3. 74
      doc/api/tty.markdown
  4. 16
      lib/_debugger.js
  5. 434
      lib/readline.js
  6. 62
      lib/repl.js
  7. 348
      lib/tty.js
  8. 12
      src/node.js

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'

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 () {}`

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);
});

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);

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;

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() {

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];
};

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;
});

Loading…
Cancel
Save