From 41f213be185a22d654cfa5bc9867e2df6704a650 Mon Sep 17 00:00:00 2001 From: Ryan Dahl Date: Mon, 31 May 2010 11:50:35 -0700 Subject: [PATCH] Add binding to termios, implement readline for repl --- bin/node-repl | 4 - lib/readline.js | 251 ++++++++++++++++++++++++++++++++++++++++++++++ lib/repl.js | 130 ++++++++++++++---------- src/node.cc | 18 +++- src/node_stdio.cc | 91 +++++++++++++++-- src/node_stdio.h | 1 + 6 files changed, 426 insertions(+), 69 deletions(-) create mode 100644 lib/readline.js diff --git a/bin/node-repl b/bin/node-repl index 9dfdd0ab5a..c23fb49643 100755 --- a/bin/node-repl +++ b/bin/node-repl @@ -2,10 +2,6 @@ puts = require("sys").puts; -puts("Welcome to the Node.js REPL."); -puts("Enter ECMAScript at the prompt."); -puts("Tip 1: Use 'rlwrap node-repl' for a better interface"); -puts("Tip 2: Type Control-D to exit."); puts("Type '.help' for options."); require('repl').start(); diff --git a/lib/readline.js b/lib/readline.js new file mode 100644 index 0000000000..c75f7e1abf --- /dev/null +++ b/lib/readline.js @@ -0,0 +1,251 @@ +// Insperation for this code comes from Salvatore Sanfilippo's linenoise. +// http://github.com/antirez/linenoise +// Reference: +// * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html +// * http://www.3waylabs.com/nw/WWW/products/wizcon/vt220.html + +var kHistorySize = 30; +var kBufSize = 10*1024; + + +var Buffer = require('buffer').Buffer; +var inherits = require('sys').inherits; +var EventEmitter = require('events').EventEmitter; +var stdio = process.binding('stdio'); + + + +exports.createInterface = function (output, isTTY) { + return new Interface(output, isTTY); +}; + +function Interface (output, isTTY) { + this.output = output; + + this.setPrompt("node> "); + + // Current line + this.buf = new Buffer(kBufSize); + this.buf.used = 0; + + if (!isTTY) { + this._tty = false; + } else { + // input refers to stdin + + // Check process.env.TERM ? + stdio.setRawMode(true); + this._tty = true; + this.columns = stdio.getColumns(); + + // Cursor position on the line. + this.cursor = 0; + + this.history = []; + this.historyIndex = -1; + } +} + +inherits(Interface, EventEmitter); + + +Interface.prototype.setPrompt = function (prompt, length) { + this._prompt = prompt; + this._promptLength = length ? length : Buffer.byteLength(prompt); +}; + + +Interface.prototype.prompt = function () { + if (this._tty) { + this.buf.used = 0; + this.cursor = 0; + this._refreshLine(); + } else { + this.output.write(this._prompt); + } +}; + + +Interface.prototype._addHistory = function () { + if (this.buf.used === 0) return ""; + + var b = new Buffer(this.buf.used); + this.buf.copy(b, 0, 0, this.buf.used); + this.buf.used = 0; + + this.history.unshift(b); + this.historyIndex = -1; + + this.cursor = 0; + + // Only store so many + if (this.history.length > kHistorySize) this.history.pop(); + + return b.toString('utf8'); +}; + + +Interface.prototype._refreshLine = function () { + if (this._closed) return; + + stdio.setRawMode(true); + + // Cursor to left edge. + this.output.write('\x1b[0G'); + + // Write the prompt and the current buffer content. + this.output.write(this._prompt); + if (this.buf.used > 0) { + this.output.write(this.buf.slice(0, this.buf.used)); + } + + // Erase to right. + this.output.write('\x1b[0K'); + + // Move cursor to original position. + this.output.write('\x1b[0G\x1b[' + (this._promptLength + this.cursor) + 'C'); +}; + + +Interface.prototype.close = function (d) { + if (this._tty) { + stdio.setRawMode(false); + } + this.emit('close'); + this._closed = true; +}; + + +Interface.prototype.write = function (d) { + if (this._closed) return; + return this._tty ? this._ttyWrite(d) : this._normalWrite(d); +}; + + +Interface.prototype._normalWrite = function (b) { + for (var i = 0; i < b.length; i++) { + var code = b instanceof Buffer ? b[i] : b.charCodeAt(i); + if (code === '\n'.charCodeAt(0) || code === '\r'.charCodeAt(0)) { + var s = this.buf.toString('utf8', 0, this.buf.used); + this.emit('line', s); + this.buf.used = 0; + } else { + this.buf[this.buf.used++] = code; + } + } +}; + + +Interface.prototype._ttyWrite = function (b) { + switch (b[0]) { + /* ctrl+c */ + case 3: + //process.kill(process.pid, "SIGINT"); + this.close(); + break; + + case 4: /* ctrl+d */ + this.close(); + break; + + case 13: /* enter */ + var line = this._addHistory(); + this.output.write('\n\x1b[0G'); + stdio.setRawMode(false); + this.emit('line', line); + break; + + case 127: /* backspace */ + case 8: /* ctrl+h */ + if (this.cursor > 0 && this.buf.used > 0) { + for (var i = this.cursor; i < this.buf.used; i++) { + this.buf[i-1] = this.buf[i]; + } + this.cursor--; + this.buf.used--; + this._refreshLine(); + } + break; + + case 21: /* Ctrl+u, delete the whole line. */ + this.cursor = this.buf.used = 0; + this._refreshLine(); + break; + + case 11: /* Ctrl+k, delete from current to end of line. */ + this.buf.used = this.cursor; + this._refreshLine(); + break; + + case 1: /* Ctrl+a, go to the start of the line */ + this.cursor = 0; + this._refreshLine(); + break; + + case 5: /* ctrl+e, go to the end of the line */ + this.cursor = this.buf.used; + this._refreshLine(); + break; + + case 27: /* escape sequence */ + if (b[1] === 91 && b[2] === 68) { + // left arrow + if (this.cursor > 0) { + this.cursor--; + this._refreshLine(); + } + } else if (b[1] === 91 && b[2] === 67) { + // right arrow + if (this.cursor != this.buf.used) { + this.cursor++; + this._refreshLine(); + } + } else if (b[1] === 91 && b[2] === 65) { + // up arrow + if (this.historyIndex + 1 < this.history.length) { + this.historyIndex++; + this.history[this.historyIndex].copy(this.buf, 0); + this.buf.used = this.history[this.historyIndex].length; + // set cursor to end of line. + this.cursor = this.buf.used; + + this._refreshLine(); + } + + } else if (b[1] === 91 && b[2] === 66) { + // down arrow + if (this.historyIndex > 0) { + this.historyIndex--; + this.history[this.historyIndex].copy(this.buf, 0); + this.buf.used = this.history[this.historyIndex].length; + // set cursor to end of line. + this.cursor = this.buf.used; + this._refreshLine(); + + } else if (this.historyIndex === 0) { + this.historyIndex = -1; + this.cursor = 0; + this.buf.used = 0; + this._refreshLine(); + } + } + break; + + default: + if (this.buf.used < kBufSize) { + for (var i = this.buf.used + 1; this.cursor < i; i--) { + this.buf[i] = this.buf[i-1]; + } + this.buf[this.cursor++] = b[0]; + this.buf.used++; + + if (this.buf.used == this.cursor) { + this.output.write(b); + } else { + this._refreshLine(); + } + } + break; + } +}; + diff --git a/lib/repl.js b/lib/repl.js index 671e9d8f23..afb28ea1e2 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -15,6 +15,7 @@ var sys = require('sys'); var evalcx = process.binding('evals').Script.runInNewContext; var path = require("path"); +var rl = require('readline'); var scope; function cwdRequire (id) { @@ -40,12 +41,79 @@ function REPLServer(prompt, stream) { if (!scope) setScope(); self.scope = scope; self.buffered_cmd = ''; - self.prompt = prompt || "node> "; + self.stream = stream || process.openStdin(); - self.stream.setEncoding('utf8'); + self.prompt = "node> " || prompt; + + var isTTY = (self.stream.fd < 3); + var rli = self.rli = rl.createInterface(self.stream, isTTY); + rli.setPrompt(self.prompt); + self.stream.addListener("data", function (chunk) { - self.readline.call(self, chunk); + rli.write(chunk); + }); + + rli.addListener('line', function (cmd) { + cmd = trimWhitespace(cmd); + + var flushed = true; + + // Check to see if a REPL keyword was used. If it returns true, + // display next prompt and return. + if (self.parseREPLKeyword(cmd) === true) return; + + // The catchall for errors + try { + self.buffered_cmd += cmd; + // This try is for determining if the command is complete, or should + // continue onto the next line. + try { + // Scope the readline with self.scope + // with(){} and eval() are considered bad. + var ret = evalcx(self.buffered_cmd, scope, "repl"); + if (ret !== undefined) { + scope._ = ret; + flushed = self.stream.write(exports.writer(ret) + "\n"); + } + + self.buffered_cmd = ''; + } catch (e) { + // instanceof doesn't work across context switches. + if (!(e && e.constructor && e.constructor.name === "SyntaxError")) { + throw e; + } + } + } catch (e) { + // On error: Print the error and clear the buffer + if (e.stack) { + flushed = self.stream.write(e.stack + "\n"); + } else { + flushed = self.stream.write(e.toString() + "\n"); + } + self.buffered_cmd = ''; + } + + // need to make sure the buffer is flushed before displaying the prompt + // again. This is really ugly. Need to have callbacks from + // net.Stream.write() + if (flushed) { + self.displayPrompt(); + } else { + self.displayPromptOnDrain = true; + } + }); + + self.stream.addListener('drain', function () { + if (self.displayPromptOnDrain) { + self.displayPrompt(); + self.displayPromptOnDrain = false; + } + }); + + rli.addListener('close', function () { + self.stream.destroy(); }); + self.displayPrompt(); } exports.REPLServer = REPLServer; @@ -57,54 +125,12 @@ exports.start = function (prompt, source) { }; REPLServer.prototype.displayPrompt = function () { - var self = this; - self.stream.write(self.buffered_cmd.length ? '... ' : self.prompt); + this.rli.setPrompt(this.buffered_cmd.length ? '... ' : this.prompt); + this.rli.prompt(); }; // read a line from the stream, then eval it REPLServer.prototype.readline = function (cmd) { - var self = this; - - cmd = self.trimWhitespace(cmd); - - // Check to see if a REPL keyword was used. If it returns true, - // display next prompt and return. - if (self.parseREPLKeyword(cmd) === true) { - return; - } - - // The catchall for errors - try { - self.buffered_cmd += cmd; - // This try is for determining if the command is complete, or should - // continue onto the next line. - try { - // Scope the readline with self.scope - // with(){} and eval() are considered bad. - var ret = evalcx(self.buffered_cmd, scope, "repl"); - if (ret !== undefined) { - scope._ = ret; - self.stream.write(exports.writer(ret) + "\n"); - } - - self.buffered_cmd = ''; - } catch (e) { - // instanceof doesn't work across context switches. - if (!(e && e.constructor && e.constructor.name === "SyntaxError")) { - throw e; - } - } - } catch (e) { - // On error: Print the error and clear the buffer - if (e.stack) { - self.stream.write(e.stack + "\n"); - } else { - self.stream.write(e.toString() + "\n"); - } - self.buffered_cmd = ''; - } - - self.displayPrompt(); }; /** @@ -142,20 +168,14 @@ REPLServer.prototype.parseREPLKeyword = function (cmd) { return false; }; -/** - * Trims Whitespace from a line. - * - * @param {String} cmd The string to trim the whitespace from - * @returns {String} The trimmed string - */ -REPLServer.prototype.trimWhitespace = function (cmd) { +function trimWhitespace (cmd) { var trimmer = /^\s*(.+)\s*$/m, matches = trimmer.exec(cmd); if (matches && matches.length === 2) { return matches[1]; } -}; +} /** * Converts commands that use var and function () to use the diff --git a/src/node.cc b/src/node.cc index 046c95d983..cabe462623 100644 --- a/src/node.cc +++ b/src/node.cc @@ -976,6 +976,9 @@ const char* ToCString(const v8::String::Utf8Value& value) { static void ReportException(TryCatch &try_catch, bool show_line) { Handle message = try_catch.Message(); + node::Stdio::DisableRawMode(STDIN_FILENO); + fprintf(stderr, "\n\n"); + if (show_line && !message.IsEmpty()) { // Print (filename):(line number): (message). String::Utf8Value filename(message->GetScriptResourceName()); @@ -1223,8 +1226,6 @@ static Handle SetUid(const Arguments& args) { v8::Handle Exit(const v8::Arguments& args) { HandleScope scope; - fflush(stderr); - Stdio::Flush(); exit(args[0]->IntegerValue()); return Undefined(); } @@ -1855,6 +1856,7 @@ static Handle Binding(const Arguments& args) { exports->Set(String::New("posix"), String::New(native_posix)); exports->Set(String::New("querystring"), String::New(native_querystring)); exports->Set(String::New("repl"), String::New(native_repl)); + exports->Set(String::New("readline"), String::New(native_readline)); exports->Set(String::New("sys"), String::New(native_sys)); exports->Set(String::New("tcp"), String::New(native_tcp)); exports->Set(String::New("uri"), String::New(native_uri)); @@ -2078,6 +2080,14 @@ static void ParseArgs(int *argc, char **argv) { option_end_index = i; } + +static void AtExit() { + node::Stdio::Flush(); + node::Stdio::DisableRawMode(STDIN_FILENO); + fprintf(stderr, "\n"); +} + + } // namespace node @@ -2179,12 +2189,12 @@ int main(int argc, char *argv[]) { Persistent context = Context::New(); Context::Scope context_scope(context); + atexit(node::AtExit); + // Create all the objects, load modules, do everything. // so your next reading stop should be node::Load()! node::Load(argc, argv); - node::Stdio::Flush(); - #ifndef NDEBUG // Clean up. context.Dispose(); diff --git a/src/node_stdio.cc b/src/node_stdio.cc index 94ee73a961..29bd527384 100644 --- a/src/node_stdio.cc +++ b/src/node_stdio.cc @@ -6,6 +6,9 @@ #include #include +#include +#include + using namespace v8; namespace node { @@ -14,6 +17,81 @@ static int stdout_flags = -1; static int stdin_flags = -1; +static struct termios orig_termios; /* in order to restore at exit */ +static int rawmode = 0; /* for atexit() function to check if restore is needed*/ + + +static int EnableRawMode(int fd) { + struct termios raw; + + if (rawmode) return 0; + + //if (!isatty(fd)) goto fatal; + if (tcgetattr(fd, &orig_termios) == -1) goto fatal; + + raw = orig_termios; /* modify the original mode */ + /* input modes: no break, no CR to NL, no parity check, no strip char, + * no start/stop output control. */ + raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); + /* output modes - disable post processing */ + raw.c_oflag &= ~(OPOST); + /* control modes - set 8 bit chars */ + raw.c_cflag |= (CS8); + /* local modes - choing off, canonical off, no extended functions, + * no signal chars (^Z,^C) */ + raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); + /* control chars - set return condition: min number of bytes and timer. + * We want read to return every single byte, without timeout. */ + raw.c_cc[VMIN] = 1; raw.c_cc[VTIME] = 0; /* 1 byte, no timer */ + + /* put terminal in raw mode after flushing */ + if (tcsetattr(fd, TCSAFLUSH, &raw) < 0) goto fatal; + rawmode = 1; + return 0; + +fatal: + errno = ENOTTY; + return -1; +} + + +void Stdio::DisableRawMode(int fd) { + /* Don't even check the return value as it's too late. */ + if (rawmode && tcsetattr(fd, TCSAFLUSH, &orig_termios) != -1) { + rawmode = 0; + } +} + +// process.binding('stdio').setRawMode(true); +static Handle SetRawMode (const Arguments& args) { + HandleScope scope; + + if (args[0]->IsFalse()) { + Stdio::DisableRawMode(STDIN_FILENO); + } else { + if (0 != EnableRawMode(STDIN_FILENO)) { + return ThrowException(ErrnoException(errno, "EnableRawMode")); + } + } + + return rawmode ? True() : False(); +} + + +// process.binding('stdio').getColumns(); +static Handle GetColumns (const Arguments& args) { + HandleScope scope; + + struct winsize ws; + + if (ioctl(1, TIOCGWINSZ, &ws) == -1) { + return scope.Close(Integer::New(80)); + } + + return scope.Close(Integer::NewFromUnsigned(ws.ws_col)); +} + + /* STDERR IS ALWAY SYNC ALWAYS UTF8 */ static Handle WriteError (const Arguments& args) @@ -94,13 +172,12 @@ void Stdio::Flush() { fcntl(STDIN_FILENO, F_SETFL, stdin_flags & ~O_NONBLOCK); } - if (STDOUT_FILENO >= 0) { - if (stdout_flags != -1) { - fcntl(STDOUT_FILENO, F_SETFL, stdout_flags & ~O_NONBLOCK); - } - - close(STDOUT_FILENO); + if (stdout_flags != -1) { + fcntl(STDOUT_FILENO, F_SETFL, stdout_flags & ~O_NONBLOCK); } + + fflush(stdout); + fflush(stderr); } @@ -120,6 +197,8 @@ void Stdio::Initialize(v8::Handle target) { NODE_SET_METHOD(target, "openStdin", OpenStdin); NODE_SET_METHOD(target, "isStdoutBlocking", IsStdoutBlocking); NODE_SET_METHOD(target, "isStdinBlocking", IsStdinBlocking); + NODE_SET_METHOD(target, "setRawMode", SetRawMode); + NODE_SET_METHOD(target, "getColumns", GetColumns); } diff --git a/src/node_stdio.h b/src/node_stdio.h index 4763943873..97a6140de5 100644 --- a/src/node_stdio.h +++ b/src/node_stdio.h @@ -10,6 +10,7 @@ class Stdio { public: static void Initialize (v8::Handle target); static void Flush (); + static void DisableRawMode(int fd); }; } // namespace node