Skip to content
This repository
Browse code

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
Nathan Rajlich authored March 26, 2012
71  doc/api/readline.markdown
Source Rendered
@@ -3,7 +3,7 @@
3 3
     Stability: 3 - Stable
4 4
 
5 5
 To use this module, do `require('readline')`. Readline allows reading of a
6  
-stream (such as STDIN) on a line-by-line basis.
  6
+stream (such as `process.stdin`) on a line-by-line basis.
7 7
 
8 8
 Note that once you've invoked this module, your node program will not
9 9
 terminate until you've paused the interface. Here's how to allow your
@@ -11,35 +11,69 @@ program to gracefully pause:
11 11
 
12 12
     var rl = require('readline');
13 13
 
14  
-    var i = rl.createInterface(process.stdin, process.stdout, null);
15  
-    i.question("What do you think of node.js?", function(answer) {
  14
+    var i = rl.createInterface({
  15
+      input: process.stdin,
  16
+      output: process.stdout
  17
+    });
  18
+
  19
+    i.question("What do you think of node.js? ", function(answer) {
16 20
       // TODO: Log the answer in a database
17  
-      console.log("Thank you for your valuable feedback.");
  21
+      console.log("Thank you for your valuable feedback:", answer);
18 22
 
19 23
       i.pause();
20 24
     });
21 25
 
22  
-## rl.createInterface(input, output, completer)
  26
+## rl.createInterface(options)
  27
+
  28
+Creates a readline `Interface` instance. Accepts an "options" Object that takes
  29
+the following values:
  30
+
  31
+ - `input` - the readable stream to listen to (Required).
  32
+
  33
+ - `output` - the writable stream to write readline data to (Required).
  34
+
  35
+ - `completer` - an optional function that is used for Tab autocompletion. See
  36
+   below for an example of using this.
  37
+
  38
+ - `terminal` - pass `true` if the `input` and `output` streams should be treated
  39
+   like a TTY, and have ANSI/VT100 escape codes written to it. Defaults to
  40
+   checking `isTTY` on the `output` stream upon instantiation.
  41
+
  42
+The `completer` function is given a the current line entered by the user, and
  43
+is supposed to return an Array with 2 entries:
23 44
 
24  
-Takes two streams and creates a readline interface. The `completer` function
25  
-is used for autocompletion. When given a substring, it returns `[[substr1,
26  
-substr2, ...], originalsubstring]`.
  45
+ 1. An Array with matching entries for the completion.
  46
+
  47
+ 2. The substring that was used for the matching.
  48
+
  49
+Which ends up looking something like:
  50
+`[[substr1, substr2, ...], originalsubstring]`.
27 51
 
28 52
 Also `completer` can be run in async mode if it accepts two arguments:
29 53
 
30  
-  function completer(linePartial, callback) {
31  
-    callback(null, [['123'], linePartial]);
32  
-  }
  54
+    function completer(linePartial, callback) {
  55
+      callback(null, [['123'], linePartial]);
  56
+    }
33 57
 
34 58
 `createInterface` is commonly used with `process.stdin` and
35 59
 `process.stdout` in order to accept user input:
36 60
 
37  
-    var readline = require('readline'),
38  
-      rl = readline.createInterface(process.stdin, process.stdout);
  61
+    var readline = require('readline');
  62
+    var rl = readline.createInterface({
  63
+      input: process.stdin,
  64
+      output: process.stdout
  65
+    });
  66
+
  67
+Once you have a readline instance, you most commonly listen for the `"line"` event.
  68
+
  69
+If `terminal` is `true` for this instance then the `output` stream will get the
  70
+best compatability if it defines an `output.columns` property, and fires
  71
+a `"resize"` event on the `output` if/when the columns ever change
  72
+(`process.stdout` does this automatically when it is a TTY).
39 73
 
40 74
 ## Class: Interface
41 75
 
42  
-The class that represents a readline interface with a stdin and stdout
  76
+The class that represents a readline interface with an input and output
43 77
 stream.
44 78
 
45 79
 ### rl.setPrompt(prompt, length)
@@ -72,18 +106,17 @@ Example usage:
72 106
 
73 107
 ### rl.pause()
74 108
 
75  
-Pauses the readline `in` stream, allowing it to be resumed later if needed.
  109
+Pauses the readline `input` stream, allowing it to be resumed later if needed.
76 110
 
77 111
 ### rl.resume()
78 112
 
79  
-Resumes the readline `in` stream.
  113
+Resumes the readline `input` stream.
80 114
 
81 115
 ### rl.write()
82 116
 
83  
-Writes to tty.
  117
+Writes to `output` stream.
84 118
 
85  
-This will also resume the `in` stream used with `createInterface` if it has
86  
-been paused.
  119
+This will also resume the `input` stream if it has been paused.
87 120
 
88 121
 ### Event: 'line'
89 122
 
69  doc/api/repl.markdown
Source Rendered
... ...
@@ -1,8 +1,8 @@
1 1
 # REPL
2 2
 
3  
-A Read-Eval-Print-Loop (REPL) is available both as a standalone program and easily
4  
-includable in other programs.  REPL provides a way to interactively run
5  
-JavaScript and see the results.  It can be used for debugging, testing, or
  3
+A Read-Eval-Print-Loop (REPL) is available both as a standalone program and
  4
+easily includable in other programs. The REPL provides a way to interactively
  5
+run JavaScript and see the results.  It can be used for debugging, testing, or
6 6
 just trying things out.
7 7
 
8 8
 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.
19 19
     2
20 20
     3
21 21
 
22  
-For advanced line-editors, start node with the environmental variable `NODE_NO_READLINE=1`.
23  
-This will start the REPL in canonical terminal settings which will allow you to use with `rlwrap`.
  22
+For advanced line-editors, start node with the environmental variable
  23
+`NODE_NO_READLINE=1`. This will start the main and debugger REPL in canonical
  24
+terminal settings which will allow you to use with `rlwrap`.
24 25
 
25 26
 For example, you could add this to your bashrc file:
26 27
 
27 28
     alias node="env NODE_NO_READLINE=1 rlwrap node"
28 29
 
29 30
 
30  
-## repl.start([prompt], [stream], [eval], [useGlobal], [ignoreUndefined])
  31
+## repl.start(options)
31 32
 
32  
-Returns and starts a REPL with `prompt` as the prompt and `stream` for all I/O.
33  
-`prompt` is optional and defaults to `> `.  `stream` is optional and defaults to
34  
-`process.stdin`. `eval` is optional too and defaults to async wrapper for
35  
-`eval()`.
  33
+Returns and starts a `REPLServer` instance. Accepts an "options" Object that
  34
+takes the following values:
36 35
 
37  
-If `useGlobal` is set to true, then the repl will use the global object,
38  
-instead of running scripts in a separate context. Defaults to `false`.
  36
+ - `prompt` - the prompt and `stream` for all I/O. Defaults to `> `.
39 37
 
40  
-If `ignoreUndefined` is set to true, then the repl will not output return value
41  
-of command if it's `undefined`. Defaults to `false`.
  38
+ - `input` - the readable stream to listen to. Defaults to `process.stdin`.
  39
+
  40
+ - `output` - the writable stream to write readline data to. Defaults to
  41
+   `process.stdout`.
  42
+
  43
+ - `terminal` - pass `true` if the `stream` should be treated like a TTY, and
  44
+   have ANSI/VT100 escape codes written to it. Defaults to checking `isTTY`
  45
+   on the `output` stream upon instantiation.
  46
+
  47
+ - `eval` - function that will be used to eval each given line. Defaults to
  48
+   an async wrapper for `eval()`. See below for an example of a custom `eval`.
  49
+
  50
+ - `useGlobal` - if set to `true`, then the repl will use the `global` object,
  51
+   instead of running scripts in a separate context. Defaults to `false`.
  52
+
  53
+ - `ignoreUndefined` - if set to `true`, then the repl will not output the
  54
+   return value of command if it's `undefined`. Defaults to `false`.
42 55
 
43 56
 You can use your own `eval` function if it has following signature:
44 57
 
@@ -56,16 +69,32 @@ Here is an example that starts a REPL on stdin, a Unix socket, and a TCP socket:
56 69
 
57 70
     connections = 0;
58 71
 
59  
-    repl.start("node via stdin> ");
  72
+    repl.start({
  73
+      prompt: "node via stdin> ",
  74
+      input: process.stdin,
  75
+      output: process.stdout
  76
+    });
60 77
 
61 78
     net.createServer(function (socket) {
62 79
       connections += 1;
63  
-      repl.start("node via Unix socket> ", socket);
  80
+      repl.start({
  81
+        prompt: "node via Unix socket> ",
  82
+        input: socket,
  83
+        output: socket
  84
+      }).on('exit', function() {
  85
+        socket.end();
  86
+      })
64 87
     }).listen("/tmp/node-repl-sock");
65 88
 
66 89
     net.createServer(function (socket) {
67 90
       connections += 1;
68  
-      repl.start("node via TCP socket> ", socket);
  91
+      repl.start({
  92
+        prompt: "node via TCP socket> ",
  93
+        input: socket,
  94
+        output: socket
  95
+      }).on('exit', function() {
  96
+        socket.end();
  97
+      });
69 98
     }).listen(5001);
70 99
 
71 100
 Running this program from the command line will start a REPL on stdin.  Other
@@ -76,6 +105,12 @@ TCP sockets.
76 105
 By starting a REPL from a Unix socket-based server instead of stdin, you can
77 106
 connect to a long-running node process without restarting it.
78 107
 
  108
+For an example of running a "full-featured" (`terminal`) REPL over
  109
+a `net.Server` and `net.Socket` instance, see: https://gist.github.com/2209310
  110
+
  111
+For an example of running a REPL instance over `curl(1)`,
  112
+see: https://gist.github.com/2053342
  113
+
79 114
 ### Event: 'exit'
80 115
 
81 116
 `function () {}`
74  doc/api/tty.markdown
Source Rendered
@@ -2,20 +2,18 @@
2 2
 
3 3
     Stability: 3 - Stable
4 4
 
5  
-Use `require('tty')` to access this module.
6  
-
7  
-Example:
8  
-
9  
-    var tty = require('tty');
10  
-    process.stdin.resume();
11  
-    tty.setRawMode(true);
12  
-    process.stdin.on('keypress', function(char, key) {
13  
-      if (key && key.ctrl && key.name == 'c') {
14  
-        console.log('graceful exit');
15  
-        process.exit()
16  
-      }
17  
-    });
  5
+The `tty` module houses the `tty.ReadStream` and `tty.WriteStream` classes. In
  6
+most cases, you will not need to use this module directly.
  7
+
  8
+When node detects that it is being run inside a TTY context, then `process.stdin`
  9
+will be a `tty.ReadStream` instance and `process.stdout` will be
  10
+a `tty.WriteStream` instance. The preferred way to check if node is being run in
  11
+a TTY context is to check `process.stdout.isTTY`:
18 12
 
  13
+    $ node -p -e "Boolean(process.stdout.isTTY)"
  14
+    true
  15
+    $ node -p -e "Boolean(process.stdout.isTTY)" | cat
  16
+    false
19 17
 
20 18
 
21 19
 ## tty.isatty(fd)
@@ -26,5 +24,51 @@ terminal.
26 24
 
27 25
 ## tty.setRawMode(mode)
28 26
 
29  
-`mode` should be `true` or `false`. This sets the properties of the current
30  
-process's stdin fd to act either as a raw device or default.
  27
+Deprecated. Use `tty.ReadStream#setRawMode()` instead.
  28
+
  29
+
  30
+## Class: ReadStream
  31
+
  32
+A `net.Socket` subclass that represents the readable portion of a tty. In normal
  33
+circumstances, `process.stdin` will be the only `tty.ReadStream` instance in any
  34
+node program (only when `isatty(0)` is true).
  35
+
  36
+### rs.isRaw
  37
+
  38
+A `Boolean` that is initialized to `false`. It represents the current "raw" state
  39
+of the `tty.ReadStream` instance.
  40
+
  41
+### rs.setRawMode(mode)
  42
+
  43
+`mode` should be `true` or `false`. This sets the properties of the
  44
+`tty.ReadStream` to act either as a raw device or default. `isRaw` will be set
  45
+to the resulting mode.
  46
+
  47
+
  48
+## Class WriteStream
  49
+
  50
+A `net.Socket` subclass that represents the writable portion of a tty. In normal
  51
+circumstances, `process.stdout` will be the only `tty.WriteStream` instance
  52
+ever created (and only when `isatty(1)` is true).
  53
+
  54
+### ws.columns
  55
+
  56
+A `Number` that gives the number of columns the TTY currently has. This property
  57
+gets updated on "resize" events.
  58
+
  59
+### ws.rows
  60
+
  61
+A `Number` that gives the number of rows the TTY currently has. This property
  62
+gets updated on "resize" events.
  63
+
  64
+### Event: 'resize'
  65
+
  66
+`function () {}`
  67
+
  68
+Emitted by `refreshSize()` when either of the `columns` or `rows` properties
  69
+has changed.
  70
+
  71
+    process.stdout.on('resize', function() {
  72
+      console.log('screen size has changed!');
  73
+      console.log(process.stdout.columns + 'x' + process.stdout.rows);
  74
+    });
16  lib/_debugger.js
@@ -745,15 +745,17 @@ function Interface(stdin, stdout, args) {
745 745
   this.stdout = stdout;
746 746
   this.args = args;
747 747
 
748  
-  var streams = {
749  
-    stdin: stdin,
750  
-    stdout: stdout
751  
-  };
752  
-
753 748
   // Two eval modes are available: controlEval and debugEval
754 749
   // But controlEval is used by default
755  
-  this.repl = new repl.REPLServer('debug> ', streams,
756  
-                                  this.controlEval.bind(this), false, true);
  750
+  this.repl = repl.start({
  751
+    prompt: 'debug> ',
  752
+    input: this.stdin,
  753
+    output: this.stdout,
  754
+    terminal: !parseInt(process.env['NODE_NO_READLINE'], 10),
  755
+    eval: this.controlEval.bind(this),
  756
+    useGlobal: false,
  757
+    ignoreUndefined: true
  758
+  });
757 759
 
758 760
   // Do not print useless warning
759 761
   repl._builtinLibs.splice(repl._builtinLibs.indexOf('repl'), 1);
434  lib/readline.js
@@ -31,18 +31,32 @@ var kBufSize = 10 * 1024;
31 31
 var util = require('util');
32 32
 var inherits = require('util').inherits;
33 33
 var EventEmitter = require('events').EventEmitter;
34  
-var tty = require('tty');
35 34
 
36 35
 
37  
-exports.createInterface = function(input, output, completer) {
38  
-  return new Interface(input, output, completer);
  36
+exports.createInterface = function(input, output, completer, terminal) {
  37
+  var rl;
  38
+  if (arguments.length === 1) {
  39
+    rl = new Interface(input);
  40
+  } else {
  41
+    rl = new Interface(input, output, completer, terminal);
  42
+  }
  43
+  return rl;
39 44
 };
40 45
 
41 46
 
42  
-function Interface(input, output, completer) {
  47
+function Interface(input, output, completer, terminal) {
43 48
   if (!(this instanceof Interface)) {
44  
-    return new Interface(input, output, completer);
  49
+    return new Interface(input, output, completer, terminal);
45 50
   }
  51
+
  52
+  if (arguments.length === 1) {
  53
+    // an options object was given
  54
+    output = input.output;
  55
+    completer = input.completer;
  56
+    terminal = input.terminal;
  57
+    input = input.input;
  58
+  }
  59
+
46 60
   EventEmitter.call(this);
47 61
 
48 62
   completer = completer || function() { return []; };
@@ -51,6 +65,12 @@ function Interface(input, output, completer) {
51 65
     throw new TypeError('Argument \'completer\' must be a function');
52 66
   }
53 67
 
  68
+  // backwards compat; check the isTTY prop of the output stream
  69
+  //  when `terminal` was not specified
  70
+  if (typeof terminal == 'undefined') {
  71
+    terminal = !!output.isTTY;
  72
+  }
  73
+
54 74
   var self = this;
55 75
 
56 76
   this.output = output;
@@ -64,19 +84,17 @@ function Interface(input, output, completer) {
64 84
 
65 85
   this.setPrompt('> ');
66 86
 
67  
-  this.enabled = output.isTTY;
68  
-
69  
-  if (parseInt(process.env['NODE_NO_READLINE'], 10)) {
70  
-    this.enabled = false;
71  
-  }
  87
+  this.terminal = !!terminal;
72 88
 
73  
-  if (!this.enabled) {
  89
+  if (!this.terminal) {
74 90
     input.on('data', function(data) {
75 91
       self._normalWrite(data);
76 92
     });
77 93
 
78 94
   } else {
79 95
 
  96
+    exports.emitKeypressEvents(input);
  97
+
80 98
     // input usually refers to stdin
81 99
     input.on('keypress', function(s, key) {
82 100
       self._ttyWrite(s, key);
@@ -85,9 +103,10 @@ function Interface(input, output, completer) {
85 103
     // Current line
86 104
     this.line = '';
87 105
 
88  
-    // Check process.env.TERM ?
89  
-    tty.setRawMode(true);
90  
-    this.enabled = true;
  106
+    if (typeof input.setRawMode === 'function') {
  107
+      input.setRawMode(true);
  108
+    }
  109
+    this.terminal = true;
91 110
 
92 111
     // Cursor position on the line.
93 112
     this.cursor = 0;
@@ -95,26 +114,16 @@ function Interface(input, output, completer) {
95 114
     this.history = [];
96 115
     this.historyIndex = -1;
97 116
 
98  
-    var winSize = output.getWindowSize();
99  
-    exports.columns = winSize[0];
100  
-
101  
-    if (process.listeners('SIGWINCH').length === 0) {
102  
-      process.on('SIGWINCH', function() {
103  
-        var winSize = output.getWindowSize();
104  
-        exports.columns = winSize[0];
105  
-
106  
-        // FIXME: when #2922 will be approved, change this to
107  
-        // output.on('resize', ...
108  
-        self._refreshLine();
109  
-      });
110  
-    }
  117
+    output.on('resize', function() {
  118
+      self._refreshLine();
  119
+    });
111 120
   }
112 121
 }
113 122
 
114 123
 inherits(Interface, EventEmitter);
115 124
 
116 125
 Interface.prototype.__defineGetter__('columns', function() {
117  
-  return exports.columns;
  126
+  return this.output.columns || Infinity;
118 127
 });
119 128
 
120 129
 Interface.prototype.setPrompt = function(prompt, length) {
@@ -131,7 +140,7 @@ Interface.prototype.setPrompt = function(prompt, length) {
131 140
 
132 141
 Interface.prototype.prompt = function(preserveCursor) {
133 142
   if (this.paused) this.resume();
134  
-  if (this.enabled) {
  143
+  if (this.terminal) {
135 144
     if (!preserveCursor) this.cursor = 0;
136 145
     this._refreshLine();
137 146
   } else {
@@ -194,13 +203,13 @@ Interface.prototype._refreshLine = function() {
194 203
   // first move to the bottom of the current line, based on cursor pos
195 204
   var prevRows = this.prevRows || 0;
196 205
   if (prevRows > 0) {
197  
-    this.output.moveCursor(0, -prevRows);
  206
+    exports.moveCursor(this.output, 0, -prevRows);
198 207
   }
199 208
 
200 209
   // Cursor to left edge.
201  
-  this.output.cursorTo(0);
  210
+  exports.cursorTo(this.output, 0);
202 211
   // erase data
203  
-  this.output.clearScreenDown();
  212
+  exports.clearScreenDown(this.output);
204 213
 
205 214
   // Write the prompt and the current buffer content.
206 215
   this.output.write(line);
@@ -211,11 +220,11 @@ Interface.prototype._refreshLine = function() {
211 220
   }
212 221
 
213 222
   // Move cursor to original position.
214  
-  this.output.cursorTo(cursorPos.cols);
  223
+  exports.cursorTo(this.output, cursorPos.cols);
215 224
 
216 225
   var diff = lineRows - cursorPos.rows;
217 226
   if (diff > 0) {
218  
-    this.output.moveCursor(0, -diff);
  227
+    exports.moveCursor(this.output, 0, -diff);
219 228
   }
220 229
 
221 230
   this.prevRows = cursorPos.rows;
@@ -224,8 +233,10 @@ Interface.prototype._refreshLine = function() {
224 233
 
225 234
 Interface.prototype.pause = function() {
226 235
   if (this.paused) return;
227  
-  if (this.enabled) {
228  
-    tty.setRawMode(false);
  236
+  if (this.terminal) {
  237
+    if (typeof this.input.setRawMode === 'function') {
  238
+      this.input.setRawMode(true);
  239
+    }
229 240
   }
230 241
   this.input.pause();
231 242
   this.paused = true;
@@ -235,8 +246,10 @@ Interface.prototype.pause = function() {
235 246
 
236 247
 Interface.prototype.resume = function() {
237 248
   this.input.resume();
238  
-  if (this.enabled) {
239  
-    tty.setRawMode(true);
  249
+  if (this.terminal) {
  250
+    if (typeof this.input.setRawMode === 'function') {
  251
+      this.input.setRawMode(true);
  252
+    }
240 253
   }
241 254
   this.paused = false;
242 255
   this.emit('resume');
@@ -245,7 +258,7 @@ Interface.prototype.resume = function() {
245 258
 
246 259
 Interface.prototype.write = function(d, key) {
247 260
   if (this.paused) this.resume();
248  
-  this.enabled ? this._ttyWrite(d, key) : this._normalWrite(d, key);
  261
+  this.terminal ? this._ttyWrite(d, key) : this._normalWrite(d, key);
249 262
 };
250 263
 
251 264
 
@@ -514,7 +527,7 @@ Interface.prototype._moveCursor = function(dx) {
514 527
 
515 528
   // check if cursors are in the same line
516 529
   if (oldPos.rows === newPos.rows) {
517  
-    this.output.moveCursor(this.cursor - oldcursor, 0);
  530
+    exports.moveCursor(this.output, this.cursor - oldcursor, 0);
518 531
     this.prevRows = newPos.rows;
519 532
   } else {
520 533
     this._refreshLine();
@@ -728,3 +741,344 @@ Interface.prototype._ttyWrite = function(s, key) {
728 741
 
729 742
 
730 743
 exports.Interface = Interface;
  744
+
  745
+
  746
+
  747
+/**
  748
+ * accepts a readable Stream instance and makes it emit "keypress" events
  749
+ */
  750
+
  751
+function emitKeypressEvents(stream) {
  752
+  if (stream._emitKeypress) return;
  753
+  stream._emitKeypress = true;
  754
+
  755
+  var keypressListeners = stream.listeners('keypress');
  756
+
  757
+  function onData(b) {
  758
+    if (keypressListeners.length) {
  759
+      emitKey(stream, b);
  760
+    } else {
  761
+      // Nobody's watching anyway
  762
+      stream.removeListener('data', onData);
  763
+      stream.on('newListener', onNewListener);
  764
+    }
  765
+  }
  766
+
  767
+  function onNewListener(event) {
  768
+    if (event == 'keypress') {
  769
+      stream.on('data', onData);
  770
+      stream.removeListener('newListener', onNewListener);
  771
+    }
  772
+  }
  773
+
  774
+  if (keypressListeners.length) {
  775
+    stream.on('data', onData);
  776
+  } else {
  777
+    stream.on('newListener', onNewListener);
  778
+  }
  779
+}
  780
+exports.emitKeypressEvents = emitKeypressEvents;
  781
+
  782
+/*
  783
+  Some patterns seen in terminal key escape codes, derived from combos seen
  784
+  at http://www.midnight-commander.org/browser/lib/tty/key.c
  785
+
  786
+  ESC letter
  787
+  ESC [ letter
  788
+  ESC [ modifier letter
  789
+  ESC [ 1 ; modifier letter
  790
+  ESC [ num char
  791
+  ESC [ num ; modifier char
  792
+  ESC O letter
  793
+  ESC O modifier letter
  794
+  ESC O 1 ; modifier letter
  795
+  ESC N letter
  796
+  ESC [ [ num ; modifier char
  797
+  ESC [ [ 1 ; modifier letter
  798
+  ESC ESC [ num char
  799
+  ESC ESC O letter
  800
+
  801
+  - char is usually ~ but $ and ^ also happen with rxvt
  802
+  - modifier is 1 +
  803
+                (shift     * 1) +
  804
+                (left_alt  * 2) +
  805
+                (ctrl      * 4) +
  806
+                (right_alt * 8)
  807
+  - two leading ESCs apparently mean the same as one leading ESC
  808
+*/
  809
+
  810
+// Regexes used for ansi escape code splitting
  811
+var metaKeyCodeRe = /^(?:\x1b)([a-zA-Z0-9])$/;
  812
+var functionKeyCodeRe =
  813
+    /^(?:\x1b+)(O|N|\[|\[\[)(?:(\d+)(?:;(\d+))?([~^$])|(?:1;)?(\d+)?([a-zA-Z]))/;
  814
+
  815
+function emitKey(stream, s) {
  816
+  var char,
  817
+      key = {
  818
+        name: undefined,
  819
+        ctrl: false,
  820
+        meta: false,
  821
+        shift: false
  822
+      },
  823
+      parts;
  824
+
  825
+  if (Buffer.isBuffer(s)) {
  826
+    if (s[0] > 127 && s[1] === undefined) {
  827
+      s[0] -= 128;
  828
+      s = '\x1b' + s.toString(stream.encoding || 'utf-8');
  829
+    } else {
  830
+      s = s.toString(stream.encoding || 'utf-8');
  831
+    }
  832
+  }
  833
+
  834
+  key.sequence = s;
  835
+
  836
+  if (s === '\r' || s === '\n') {
  837
+    // enter
  838
+    key.name = 'enter';
  839
+
  840
+  } else if (s === '\t') {
  841
+    // tab
  842
+    key.name = 'tab';
  843
+
  844
+  } else if (s === '\b' || s === '\x7f' ||
  845
+             s === '\x1b\x7f' || s === '\x1b\b') {
  846
+    // backspace or ctrl+h
  847
+    key.name = 'backspace';
  848
+    key.meta = (s.charAt(0) === '\x1b');
  849
+
  850
+  } else if (s === '\x1b' || s === '\x1b\x1b') {
  851
+    // escape key
  852
+    key.name = 'escape';
  853
+    key.meta = (s.length === 2);
  854
+
  855
+  } else if (s === ' ' || s === '\x1b ') {
  856
+    key.name = 'space';
  857
+    key.meta = (s.length === 2);
  858
+
  859
+  } else if (s <= '\x1a') {
  860
+    // ctrl+letter
  861
+    key.name = String.fromCharCode(s.charCodeAt(0) + 'a'.charCodeAt(0) - 1);
  862
+    key.ctrl = true;
  863
+
  864
+  } else if (s.length === 1 && s >= 'a' && s <= 'z') {
  865
+    // lowercase letter
  866
+    key.name = s;
  867
+
  868
+  } else if (s.length === 1 && s >= 'A' && s <= 'Z') {
  869
+    // shift+letter
  870
+    key.name = s.toLowerCase();
  871
+    key.shift = true;
  872
+
  873
+  } else if (parts = metaKeyCodeRe.exec(s)) {
  874
+    // meta+character key
  875
+    key.name = parts[1].toLowerCase();
  876
+    key.meta = true;
  877
+    key.shift = /^[A-Z]$/.test(parts[1]);
  878
+
  879
+  } else if (parts = functionKeyCodeRe.exec(s)) {
  880
+    // ansi escape sequence
  881
+
  882
+    // reassemble the key code leaving out leading \x1b's,
  883
+    // the modifier key bitflag and any meaningless "1;" sequence
  884
+    var code = (parts[1] || '') + (parts[2] || '') +
  885
+               (parts[4] || '') + (parts[6] || ''),
  886
+        modifier = (parts[3] || parts[5] || 1) - 1;
  887
+
  888
+    // Parse the key modifier
  889
+    key.ctrl = !!(modifier & 4);
  890
+    key.meta = !!(modifier & 10);
  891
+    key.shift = !!(modifier & 1);
  892
+    key.code = code;
  893
+
  894
+    // Parse the key itself
  895
+    switch (code) {
  896
+      /* xterm/gnome ESC O letter */
  897
+      case 'OP': key.name = 'f1'; break;
  898
+      case 'OQ': key.name = 'f2'; break;
  899
+      case 'OR': key.name = 'f3'; break;
  900
+      case 'OS': key.name = 'f4'; break;
  901
+
  902
+      /* xterm/rxvt ESC [ number ~ */
  903
+      case '[11~': key.name = 'f1'; break;
  904
+      case '[12~': key.name = 'f2'; break;
  905
+      case '[13~': key.name = 'f3'; break;
  906
+      case '[14~': key.name = 'f4'; break;
  907
+
  908
+      /* from Cygwin and used in libuv */
  909
+      case '[[A': key.name = 'f1'; break;
  910
+      case '[[B': key.name = 'f2'; break;
  911
+      case '[[C': key.name = 'f3'; break;
  912
+      case '[[D': key.name = 'f4'; break;
  913
+      case '[[E': key.name = 'f5'; break;
  914
+
  915
+      /* common */
  916
+      case '[15~': key.name = 'f5'; break;
  917
+      case '[17~': key.name = 'f6'; break;
  918
+      case '[18~': key.name = 'f7'; break;
  919
+      case '[19~': key.name = 'f8'; break;
  920
+      case '[20~': key.name = 'f9'; break;
  921
+      case '[21~': key.name = 'f10'; break;
  922
+      case '[23~': key.name = 'f11'; break;
  923
+      case '[24~': key.name = 'f12'; break;
  924
+
  925
+      /* xterm ESC [ letter */
  926
+      case '[A': key.name = 'up'; break;
  927
+      case '[B': key.name = 'down'; break;
  928
+      case '[C': key.name = 'right'; break;
  929
+      case '[D': key.name = 'left'; break;
  930
+      case '[E': key.name = 'clear'; break;
  931
+      case '[F': key.name = 'end'; break;
  932
+      case '[H': key.name = 'home'; break;
  933
+
  934
+      /* xterm/gnome ESC O letter */
  935
+      case 'OA': key.name = 'up'; break;
  936
+      case 'OB': key.name = 'down'; break;
  937
+      case 'OC': key.name = 'right'; break;
  938
+      case 'OD': key.name = 'left'; break;
  939
+      case 'OE': key.name = 'clear'; break;
  940
+      case 'OF': key.name = 'end'; break;
  941
+      case 'OH': key.name = 'home'; break;
  942
+
  943
+      /* xterm/rxvt ESC [ number ~ */
  944
+      case '[1~': key.name = 'home'; break;
  945
+      case '[2~': key.name = 'insert'; break;
  946
+      case '[3~': key.name = 'delete'; break;
  947
+      case '[4~': key.name = 'end'; break;
  948
+      case '[5~': key.name = 'pageup'; break;
  949
+      case '[6~': key.name = 'pagedown'; break;
  950
+
  951
+      /* putty */
  952
+      case '[[5~': key.name = 'pageup'; break;
  953
+      case '[[6~': key.name = 'pagedown'; break;
  954
+
  955
+      /* rxvt */
  956
+      case '[7~': key.name = 'home'; break;
  957
+      case '[8~': key.name = 'end'; break;
  958
+
  959
+      /* rxvt keys with modifiers */
  960
+      case '[a': key.name = 'up'; key.shift = true; break;
  961
+      case '[b': key.name = 'down'; key.shift = true; break;
  962
+      case '[c': key.name = 'right'; key.shift = true; break;
  963
+      case '[d': key.name = 'left'; key.shift = true; break;
  964
+      case '[e': key.name = 'clear'; key.shift = true; break;
  965
+
  966
+      case '[2$': key.name = 'insert'; key.shift = true; break;
  967
+      case '[3$': key.name = 'delete'; key.shift = true; break;
  968
+      case '[5$': key.name = 'pageup'; key.shift = true; break;
  969
+      case '[6$': key.name = 'pagedown'; key.shift = true; break;
  970
+      case '[7$': key.name = 'home'; key.shift = true; break;
  971
+      case '[8$': key.name = 'end'; key.shift = true; break;
  972
+
  973
+      case 'Oa': key.name = 'up'; key.ctrl = true; break;
  974
+      case 'Ob': key.name = 'down'; key.ctrl = true; break;
  975
+      case 'Oc': key.name = 'right'; key.ctrl = true; break;
  976
+      case 'Od': key.name = 'left'; key.ctrl = true; break;
  977
+      case 'Oe': key.name = 'clear'; key.ctrl = true; break;
  978
+
  979
+      case '[2^': key.name = 'insert'; key.ctrl = true; break;
  980
+      case '[3^': key.name = 'delete'; key.ctrl = true; break;
  981
+      case '[5^': key.name = 'pageup'; key.ctrl = true; break;
  982
+      case '[6^': key.name = 'pagedown'; key.ctrl = true; break;
  983
+      case '[7^': key.name = 'home'; key.ctrl = true; break;
  984
+      case '[8^': key.name = 'end'; key.ctrl = true; break;
  985
+
  986
+      /* misc. */
  987
+      case '[Z': key.name = 'tab'; key.shift = true; break;
  988
+      default: key.name = 'undefined'; break;
  989
+
  990
+    }
  991
+  } else if (s.length > 1 && s[0] !== '\x1b') {
  992
+    // Got a longer-than-one string of characters.
  993
+    // Probably a paste, since it wasn't a control sequence.
  994
+    Array.prototype.forEach.call(s, function(c) {
  995
+      emitKey(stream, c);
  996
+    });
  997
+    return;
  998
+  }
  999
+
  1000
+  // Don't emit a key if no name was found
  1001
+  if (key.name === undefined) {
  1002
+    key = undefined;
  1003
+  }
  1004
+
  1005
+  if (s.length === 1) {
  1006
+    char = s;
  1007
+  }
  1008
+
  1009
+  if (key || char) {
  1010
+    stream.emit('keypress', char, key);
  1011
+  }
  1012
+}
  1013
+
  1014
+
  1015
+/**
  1016
+ * moves the cursor to the x and y coordinate on the given stream
  1017
+ */
  1018
+
  1019
+function cursorTo(stream, x, y) {
  1020
+  if (typeof x !== 'number' && typeof y !== 'number')
  1021
+    return;
  1022
+
  1023
+  if (typeof x !== 'number')
  1024
+    throw new Error("Can't set cursor row without also setting it's column");
  1025
+
  1026
+  if (typeof y !== 'number') {
  1027
+    stream.write('\x1b[' + (x + 1) + 'G');
  1028
+  } else {
  1029
+    stream.write('\x1b[' + (y + 1) + ';' + (x + 1) + 'H');
  1030
+  }
  1031
+}
  1032
+exports.cursorTo = cursorTo;
  1033
+
  1034
+
  1035
+/**
  1036
+ * moves the cursor relative to its current location
  1037
+ */
  1038
+
  1039
+function moveCursor(stream, dx, dy) {
  1040
+  if (dx < 0) {
  1041
+    stream.write('\x1b[' + (-dx) + 'D');
  1042
+  } else if (dx > 0) {
  1043
+    stream.write('\x1b[' + dx + 'C');
  1044
+  }
  1045
+
  1046
+  if (dy < 0) {
  1047
+    stream.write('\x1b[' + (-dy) + 'A');
  1048
+  } else if (dy > 0) {
  1049
+    stream.write('\x1b[' + dy + 'B');
  1050
+  }
  1051
+}
  1052
+exports.moveCursor = moveCursor;
  1053
+
  1054
+
  1055
+/**
  1056
+ * clears the current line the cursor is on:
  1057
+ *   -1 for left of the cursor
  1058
+ *   +1 for right of the cursor
  1059
+ *    0 for the entire line
  1060
+ */
  1061
+
  1062
+function clearLine(stream, dir) {
  1063
+  if (dir < 0) {
  1064
+    // to the beginning
  1065
+    stream.write('\x1b[1K');
  1066
+  } else if (dir > 0) {
  1067
+    // to the end
  1068
+    stream.write('\x1b[0K');
  1069
+  } else {
  1070
+    // entire line
  1071
+    stream.write('\x1b[2K');
  1072
+  }
  1073
+}
  1074
+exports.clearLine = clearLine;
  1075
+
  1076
+
  1077
+/**
  1078
+ * clears the screen from the current position of the cursor down
  1079
+ */
  1080
+
  1081
+function clearScreenDown(stream) {
  1082
+  stream.write('\x1b[0J');
  1083
+}
  1084
+exports.clearScreenDown = clearScreenDown;
62  lib/repl.js
@@ -76,6 +76,27 @@ exports._builtinLibs = ['assert', 'buffer', 'child_process', 'cluster',
76 76
 
77 77
 
78 78
 function REPLServer(prompt, stream, eval, useGlobal, ignoreUndefined) {
  79
+  if (!(this instanceof REPLServer)) {
  80
+    return new REPLServer(prompt, stream, eval, useGlobal, ignoreUndefined);
  81
+  }
  82
+
  83
+  var options, input, output;
  84
+  if (typeof prompt == 'object') {
  85
+    // an options object was given
  86
+    options = prompt;
  87
+    stream = options.stream || options.socket;
  88
+    input = options.input;
  89
+    output = options.output;
  90
+    eval = options.eval;
  91
+    useGlobal = options.useGlobal;
  92
+    ignoreUndefined = options.ignoreUndefined;
  93
+    prompt = options.prompt;
  94
+  } else if (typeof prompt != 'string') {
  95
+    throw new Error('An options Object, or a prompt String are required');
  96
+  } else {
  97
+    options = {};
  98
+  }
  99
+
79 100
   EventEmitter.call(this);
80 101
 
81 102
   var self = this;
@@ -99,34 +120,45 @@ function REPLServer(prompt, stream, eval, useGlobal, ignoreUndefined) {
99 120
   self.resetContext();
100 121
   self.bufferedCommand = '';
101 122
 
102  
-  if (stream) {
103  
-    // We're given a duplex socket
104  
-    if (stream.stdin || stream.stdout) {
105  
-      self.outputStream = stream.stdout;
106  
-      self.inputStream = stream.stdin;
  123
+  if (!input && !output) {
  124
+    // legacy API, passing a 'stream'/'socket' option
  125
+    if (!stream) {
  126
+      // use stdin and stdout as the default streams if none were given
  127
+      stream = process;
  128
+    }
  129
+    if (stream.stdin && stream.stdout) {
  130
+      // We're given custom object with 2 streams, or the `process` object
  131
+      input = stream.stdin;
  132
+      output = stream.stdout;
107 133
     } else {
108  
-      self.outputStream = stream;
109  
-      self.inputStream = stream;
  134
+      // We're given a duplex readable/writable Stream, like a `net.Socket`
  135
+      input = stream;
  136
+      output = stream;
110 137
     }
111  
-  } else {
112  
-    self.outputStream = process.stdout;
113  
-    self.inputStream = process.stdin;
114  
-    process.stdin.resume();
115 138
   }
116 139
 
  140
+
  141
+  self.inputStream = input;
  142
+  self.outputStream = output;
  143
+
117 144
   self.prompt = (prompt != undefined ? prompt : '> ');
118 145
 
119 146
   function complete(text, callback) {
120 147
     self.complete(text, callback);
121 148
   }
122 149
 
123  
-  var rli = rl.createInterface(self.inputStream, self.outputStream, complete);
  150
+  var rli = rl.createInterface({
  151
+    input: self.inputStream,
  152
+    output: self.outputStream,
  153
+    completer: complete,
  154
+    terminal: options.terminal
  155
+  });
124 156
   self.rli = rli;
125 157
 
126 158
   this.commands = {};
127 159
   defineDefaultCommands(this);
128 160
 
129  
-  if (rli.enabled && !exports.disableColors &&
  161
+  if (rli.terminal && !exports.disableColors &&
130 162
       exports.writer === util.inspect) {
131 163
     // Turn on ANSI coloring.
132 164
     exports.writer = function(obj, showHidden, depth) {
@@ -322,10 +354,6 @@ REPLServer.prototype.displayPrompt = function(preserveCursor) {
322 354
 };
323 355
 
324 356
 
325  
-// read a line from the stream, then eval it
326  
-REPLServer.prototype.readline = function(cmd) {
327  
-};
328  
-
329 357
 // A stream to push an array into a REPL
330 358
 // used in REPLServer.complete
331 359
 function ArrayStream() {
348  lib/tty.js
@@ -25,28 +25,17 @@ var net = require('net');
25 25
 var TTY = process.binding('tty_wrap').TTY;
26 26
 var isTTY = process.binding('tty_wrap').isTTY;
27 27
 
28  
-var stdinHandle;
29  
-
30  
-
31 28
 exports.isatty = function(fd) {
32 29
   return isTTY(fd);
33 30
 };
34 31
 
35 32
 
  33
+// backwards-compat
36 34
 exports.setRawMode = function(flag) {
37  
-  assert.ok(stdinHandle, 'stdin must be initialized before calling setRawMode');
38  
-  stdinHandle.setRawMode(flag);
39  
-};
40  
-
41  
-
42  
-exports.getWindowSize = function() {
43  
-  //throw new Error("implement me");
44  
-  return 80;
45  
-};
46  
-
47  
-
48  
-exports.setWindowSize = function() {
49  
-  throw new Error('implement me');
  35
+  if (!process.stdin.isTTY) {
  36
+    throw new Error('can\'t set raw mode on non-tty');
  37
+  }
  38
+  process.stdin.setRawMode(flag);
50 39
 };
51 40
 
52 41
 
@@ -56,31 +45,9 @@ function ReadStream(fd) {
56 45
     handle: new TTY(fd, true)
57 46
   });
58 47
 
  48
+  this.readable = true;
59 49
   this.writable = false;
60  
-
61  
-  var self = this,
62  
-      keypressListeners = this.listeners('keypress');
63  
-
64  
-  function onData(b) {
65  
-    if (keypressListeners.length) {
66  
-      self._emitKey(b);
67  
-    } else {
68  
-      // Nobody's watching anyway
69  
-      self.removeListener('data', onData);
70  
-      self.on('newListener', onNewListener);
71  
-    }
72  
-  }
73  
-
74  
-  function onNewListener(event) {
75  
-    if (event == 'keypress') {
76  
-      self.on('data', onData);
77  
-      self.removeListener('newListener', onNewListener);
78  
-    }
79  
-  }
80  
-
81  
-  if (!stdinHandle) stdinHandle = this._handle;
82  
-
83  
-  this.on('newListener', onNewListener);
  50
+  this.isRaw = false;
84 51
 }
85 52
 inherits(ReadStream, net.Socket);
86 53
 
@@ -96,242 +63,15 @@ ReadStream.prototype.resume = function() {
96 63
   return net.Socket.prototype.resume.call(this);
97 64
 };
98 65
 
  66
+ReadStream.prototype.setRawMode = function(flag) {
  67
+  flag = !!flag;
  68
+  this._handle.setRawMode(flag);
  69
+  this.isRaw = flag;
  70
+};
99 71
 
100 72
 ReadStream.prototype.isTTY = true;
101 73
 
102 74
 
103  
-/*
104  
-  Some patterns seen in terminal key escape codes, derived from combos seen
105  
-  at http://www.midnight-commander.org/browser/lib/tty/key.c
106  
-
107  
-  ESC letter
108  
-  ESC [ letter
109  
-  ESC [ modifier letter
110  
-  ESC [ 1 ; modifier letter
111  
-  ESC [ num char
112  
-  ESC [ num ; modifier char
113  
-  ESC O letter
114  
-  ESC O modifier letter
115  
-  ESC O 1 ; modifier letter
116  
-  ESC N letter
117  
-  ESC [ [ num ; modifier char
118  
-  ESC [ [ 1 ; modifier letter
119  
-  ESC ESC [ num char
120  
-  ESC ESC O letter
121  
-
122  
-  - char is usually ~ but $ and ^ also happen with rxvt
123  
-  - modifier is 1 +
124  
-                (shift     * 1) +
125  
-                (left_alt  * 2) +
126  
-                (ctrl      * 4) +
127  
-                (right_alt * 8)
128  
-  - two leading ESCs apparently mean the same as one leading ESC
129  
-*/
130  
-
131  
-
132  
-// Regexes used for ansi escape code splitting
133  
-var metaKeyCodeRe = /^(?:\x1b)([a-zA-Z0-9])$/;
134  
-var functionKeyCodeRe =
135  
-    /^(?:\x1b+)(O|N|\[|\[\[)(?:(\d+)(?:;(\d+))?([~^$])|(?:1;)?(\d+)?([a-zA-Z]))/;
136  
-
137  
-
138  
-ReadStream.prototype._emitKey = function(s) {
139  
-  var char,
140  
-      key = {
141  
-        name: undefined,
142  
-        ctrl: false,
143  
-        meta: false,
144  
-        shift: false
145  
-      },
146  
-      parts;
147  
-
148  
-  if (Buffer.isBuffer(s)) {
149  
-    if (s[0] > 127 && s[1] === undefined) {
150  
-      s[0] -= 128;
151  
-      s = '\x1b' + s.toString(this.encoding || 'utf-8');
152  
-    } else {
153  
-      s = s.toString(this.encoding || 'utf-8');
154  
-    }
155  
-  }
156  
-
157  
-  key.sequence = s;
158  
-
159  
-  if (s === '\r' || s === '\n') {
160  
-    // enter
161  
-    key.name = 'enter';
162  
-
163  
-  } else if (s === '\t') {
164  
-    // tab
165  
-    key.name = 'tab';
166  
-
167  
-  } else if (s === '\b' || s === '\x7f' ||
168  
-             s === '\x1b\x7f' || s === '\x1b\b') {
169  
-    // backspace or ctrl+h
170  
-    key.name = 'backspace';
171  
-    key.meta = (s.charAt(0) === '\x1b');
172  
-
173  
-  } else if (s === '\x1b' || s === '\x1b\x1b') {
174  
-    // escape key
175