From 1463271ce2f16c8bd14c72ab7e7c562f5aa4fbee Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Mon, 12 Mar 2012 22:06:06 -0700 Subject: [PATCH 01/21] tty: move setRawMode() to ReadStream's prototype Also keeping backwards-compatibility for now with exports.setRawMode(). --- lib/tty.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/tty.js b/lib/tty.js index c1bbba1b06a..c02f1a04c0a 100644 --- a/lib/tty.js +++ b/lib/tty.js @@ -25,17 +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); + if (!process.stdin.isTTY) { + throw new Error('can\'t set raw mode on non-tty'); + } + process.stdin.setRawMode(flag); }; @@ -96,6 +96,9 @@ ReadStream.prototype.resume = function() { return net.Socket.prototype.resume.call(this); }; +ReadStream.prototype.setRawMode = function(flag) { + this._handle.setRawMode(flag); +}; ReadStream.prototype.isTTY = true; From a8e9eb1ee6e37caf5ffd6f7800d462320ff04877 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Mon, 12 Mar 2012 22:09:28 -0700 Subject: [PATCH 02/21] tty: remove exports.getWindowSize() and exports.setWindowSize() Unimplemented. --- lib/tty.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/lib/tty.js b/lib/tty.js index c02f1a04c0a..1a6e9bdb946 100644 --- a/lib/tty.js +++ b/lib/tty.js @@ -39,17 +39,6 @@ exports.setRawMode = function(flag) { }; -exports.getWindowSize = function() { - //throw new Error("implement me"); - return 80; -}; - - -exports.setWindowSize = function() { - throw new Error('implement me'); -}; - - function ReadStream(fd) { if (!(this instanceof ReadStream)) return new ReadStream(fd); net.Socket.call(this, { From d115e4206293d77c07d3d37677d46a518e5c15ff Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Mon, 12 Mar 2012 22:12:02 -0700 Subject: [PATCH 03/21] tty: add a ReadStream#isRaw flag Gets set to true/false when setRawMode() is called. --- lib/tty.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/tty.js b/lib/tty.js index 1a6e9bdb946..e00b7a03b23 100644 --- a/lib/tty.js +++ b/lib/tty.js @@ -46,6 +46,7 @@ function ReadStream(fd) { }); this.writable = false; + this.isRaw = false; var self = this, keypressListeners = this.listeners('keypress'); @@ -86,7 +87,9 @@ ReadStream.prototype.resume = function() { }; ReadStream.prototype.setRawMode = function(flag) { + flag = !!flag; this._handle.setRawMode(flag); + this.isRaw = flag; }; ReadStream.prototype.isTTY = true; From 1e20f7471730f3978c28a7019e17182da677c084 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Mon, 12 Mar 2012 22:12:39 -0700 Subject: [PATCH 04/21] tty: remove final traces of stdinHandle --- lib/tty.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/tty.js b/lib/tty.js index e00b7a03b23..edbe77f6666 100644 --- a/lib/tty.js +++ b/lib/tty.js @@ -68,8 +68,6 @@ function ReadStream(fd) { } } - if (!stdinHandle) stdinHandle = this._handle; - this.on('newListener', onNewListener); } inherits(ReadStream, net.Socket); From c958c91de169cf9cc0f39927e21fd2ca0561bdd8 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Mon, 12 Mar 2012 22:45:27 -0700 Subject: [PATCH 05/21] readline: remove use of the tty module --- lib/readline.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/readline.js b/lib/readline.js index 6c867c44963..5c3d2d90cbc 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -31,7 +31,6 @@ 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) { @@ -85,8 +84,7 @@ function Interface(input, output, completer) { // Current line this.line = ''; - // Check process.env.TERM ? - tty.setRawMode(true); + input.setRawMode && input.setRawMode(true); this.enabled = true; // Cursor position on the line. @@ -240,7 +238,7 @@ Interface.prototype._refreshLine = function() { Interface.prototype.pause = function() { if (this.paused) return; if (this.enabled) { - tty.setRawMode(false); + this.input.setRawMode && this.input.setRawMode(false); } this.input.pause(); this.paused = true; @@ -251,7 +249,7 @@ Interface.prototype.pause = function() { Interface.prototype.resume = function() { this.input.resume(); if (this.enabled) { - tty.setRawMode(true); + this.input.setRawMode && this.input.setRawMode(true); } this.paused = false; this.emit('resume'); From 3cc19a72e92369fe8ed7c14f5f09af66d103bf33 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Mon, 12 Mar 2012 22:57:59 -0700 Subject: [PATCH 06/21] tty, readline: transfer the SIGWINCH code to tty.WriteStream This adds: * process.stdout.rows * process.stdout.columns * process.stdout.on('resize', ...) Deprecates: * process.stdout.getWindowSize() --- lib/readline.js | 16 ++-------------- lib/tty.js | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/lib/readline.js b/lib/readline.js index 5c3d2d90cbc..d27672e7291 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -95,29 +95,17 @@ function Interface(input, output, completer) { // a number of lines used by current command this.usedLines = 1; - - var winSize = output.getWindowSize(); - exports.columns = winSize[0]; - exports.rows = winSize[1]; - - if (process.listeners('SIGWINCH').length === 0) { - process.on('SIGWINCH', function() { - var winSize = output.getWindowSize(); - exports.columns = winSize[0]; - exports.rows = winSize[1]; - }); - } } } inherits(Interface, EventEmitter); Interface.prototype.__defineGetter__('columns', function() { - return exports.columns; + return this.output.columns; }); Interface.prototype.__defineGetter__('rows', function() { - return exports.rows; + return this.output.rows; }); Interface.prototype.setPrompt = function(prompt, length) { diff --git a/lib/tty.js b/lib/tty.js index edbe77f6666..cb47c948b96 100644 --- a/lib/tty.js +++ b/lib/tty.js @@ -334,6 +334,18 @@ function WriteStream(fd) { this.readable = false; this.writable = true; + + var self = this; + var winSize = this._handle.getWindowSize(); + this.columns = winSize[0]; + this.rows = winSize[1]; + + process.on('SIGWINCH', function() { + var winSize = self._handle.getWindowSize(); + self.columns = winSize[0]; + self.rows = winSize[1]; + self.emit('resize', winSize); + }); } inherits(WriteStream, net.Socket); exports.WriteStream = WriteStream; @@ -386,6 +398,7 @@ WriteStream.prototype.clearLine = function(dir) { }; +// backwards-compat WriteStream.prototype.getWindowSize = function() { - return this._handle.getWindowSize(); + return [this.columns, this.rows]; }; From f38c01bb135f56aa3265366e5b92c9a59e73e4f0 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Mon, 12 Mar 2012 23:22:19 -0700 Subject: [PATCH 07/21] tty, readline: move the ansi keypress parsing code to realine It doesn't belong in tty. Moving it here will allow us to reuse the logic with other kinds of streams, namely a net.Socket. --- lib/readline.js | 268 ++++++++++++++++++++++++++++++++++++++++++++++++ lib/tty.js | 254 --------------------------------------------- 2 files changed, 268 insertions(+), 254 deletions(-) diff --git a/lib/readline.js b/lib/readline.js index d27672e7291..5e267fd4992 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -76,6 +76,8 @@ function Interface(input, output, completer) { } else { + exports.emitKeypress(input); + // input usually refers to stdin input.on('keypress', function(s, key) { self._ttyWrite(s, key); @@ -724,3 +726,269 @@ Interface.prototype._ttyWrite = function(s, key) { exports.Interface = Interface; + + + +/** + * accepts a readable Stream instance and makes it emit "keypress" events + */ + +function emitKeypress(stream) { + if (stream._emitKeypress) return; + + var keypressListeners = stream.listeners('keypress'); + + function onData(b) { + if (keypressListeners.length) { + exports.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); + } + } + + stream.on('newListener', onNewListener); + stream._emitKeypress = true; +} +exports.emitKeypress = emitKeypress; + +/* + 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) { + exports.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); + } +}; +exports.emitKey = emitKey; diff --git a/lib/tty.js b/lib/tty.js index cb47c948b96..431661bcb56 100644 --- a/lib/tty.js +++ b/lib/tty.js @@ -47,28 +47,6 @@ function ReadStream(fd) { this.writable = false; this.isRaw = 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); - } - } - - this.on('newListener', onNewListener); } inherits(ReadStream, net.Socket); @@ -93,238 +71,6 @@ ReadStream.prototype.setRawMode = function(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); From eef98998b116fbc1fcaabb654c1174c54d6441bb Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Mon, 12 Mar 2012 23:38:52 -0700 Subject: [PATCH 08/21] tty, readline: migrate the tty.WriteStream ansi code functions They didn't belong on tty.WriteStream and this will allow us to resuse this logic on streams other than process.stdout, namely a net.Socket. --- lib/readline.js | 82 +++++++++++++++++++++++++++++++++++++++++++------ lib/tty.js | 43 +++----------------------- 2 files changed, 76 insertions(+), 49 deletions(-) diff --git a/lib/readline.js b/lib/readline.js index 5e267fd4992..0e66ef04c16 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -190,7 +190,7 @@ Interface.prototype._refreshLine = function() { var oldLines = this.usedLines; this._recalcUsedLines(); if (oldLines != this.usedLines) { - this.output.cursorTo(0, this.rows - 1); + exports.cursorTo(this.output, 0, this.rows - 1); for (var i = oldLines; i < this.usedLines; i++) { this.output.write('\r\n'); } @@ -198,9 +198,9 @@ Interface.prototype._refreshLine = function() { // Cursor to left edge. if (this.usedLines === 1) { - this.output.cursorTo(0); + exports.cursorTo(this.output, 0); } else { - this.output.cursorTo(0, this.rows - this.usedLines); + exports.cursorTo(this.output, 0, this.rows - this.usedLines); } // Write the prompt and the current buffer content. @@ -208,19 +208,19 @@ Interface.prototype._refreshLine = function() { this.output.write(buffer); // Erase to right. - this.output.clearLine(1); + exports.clearLine(this.output, 1); var clearLinesCnt = this.usedLines - Math.floor(buffer.length / columns) - 1; for (var i = this.rows - clearLinesCnt; i < this.rows; i++) { - this.output.cursorTo(0, i); - this.output.clearLine(0); + exports.cursorTo(this.output, 0, i); + exports.clearLine(this.output, 0); } // Move cursor to original position. var curPos = this._getCursorPos(); if (this.usedLines === 1) { - this.output.cursorTo(curPos[0]); + exports.cursorTo(this.output, curPos[0]); } else { - this.output.cursorTo(curPos[0], this.rows - this.usedLines + curPos[1]); + exports.cursorTo(this.output, curPos[0], this.rows - this.usedLines + curPos[1]); } }; @@ -499,7 +499,7 @@ Interface.prototype._moveCursor = function(dx) { // check if cursors are in the same line if (oldLine == newLine) { - this.output.moveCursor(dx, 0); + exports.moveCursor(this.output, dx, 0); } else { this._refreshLine(); } @@ -990,5 +990,67 @@ function emitKey(stream, s) { if (key || char) { stream.emit('keypress', char, key); } -}; +} exports.emitKey = emitKey; + + +/** + * 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; diff --git a/lib/tty.js b/lib/tty.js index 431661bcb56..10491d4b55c 100644 --- a/lib/tty.js +++ b/lib/tty.js @@ -100,51 +100,16 @@ exports.WriteStream = WriteStream; WriteStream.prototype.isTTY = true; +// backwards-compat 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'); - } + 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').moveTo(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); }; - - -// backwards-compat WriteStream.prototype.getWindowSize = function() { return [this.columns, this.rows]; }; From 76c25d99385fb25cdd2a9a7c73a480fd89ce67ea Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Mon, 12 Mar 2012 23:41:23 -0700 Subject: [PATCH 09/21] tty: set tty.ReadStream#readable to true Part of the Stream contract. --- lib/tty.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/tty.js b/lib/tty.js index 10491d4b55c..9a18e7716cb 100644 --- a/lib/tty.js +++ b/lib/tty.js @@ -45,6 +45,7 @@ function ReadStream(fd) { handle: new TTY(fd, true) }); + this.readable = true; this.writable = false; this.isRaw = false; } From 22fdb738dfd477914b636112922386455f19f882 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Mon, 12 Mar 2012 23:45:40 -0700 Subject: [PATCH 10/21] readline: add default values for columns/rows For easier interoperability with net.Socket instances. A proper telnet server could set the columns/rows properties manually after negotiating NAWS, for example. --- lib/readline.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/readline.js b/lib/readline.js index 0e66ef04c16..e3d1fe667d0 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -103,11 +103,11 @@ function Interface(input, output, completer) { inherits(Interface, EventEmitter); Interface.prototype.__defineGetter__('columns', function() { - return this.output.columns; + return this.output.columns || 80; }); Interface.prototype.__defineGetter__('rows', function() { - return this.output.rows; + return this.output.rows || 30; }); Interface.prototype.setPrompt = function(prompt, length) { From e1db1d25a448122c7fd27afebdc5ccb4fe115aa3 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Tue, 13 Mar 2012 13:13:25 -0700 Subject: [PATCH 11/21] readline: fix lint --- lib/readline.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/readline.js b/lib/readline.js index e3d1fe667d0..ddda7528559 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -220,7 +220,8 @@ Interface.prototype._refreshLine = function() { if (this.usedLines === 1) { exports.cursorTo(this.output, curPos[0]); } else { - exports.cursorTo(this.output, curPos[0], this.rows - this.usedLines + curPos[1]); + exports.cursorTo(this.output, curPos[0], + this.rows - this.usedLines + curPos[1]); } }; @@ -972,7 +973,7 @@ function emitKey(stream, s) { } 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) { + Array.prototype.forEach.call(s, function(c) { exports.emitKey(stream, c); }); return; From 8fc2aba989bf12d7d46382face79626fb140d486 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Tue, 13 Mar 2012 22:50:47 -0700 Subject: [PATCH 12/21] repl: remove unused readline() function --- lib/repl.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/repl.js b/lib/repl.js index 4ce849a35c8..9616dd43897 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -324,10 +324,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() { From 135852621a92ac21824b9f1a71df41bbebaf542b Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Tue, 13 Mar 2012 22:59:07 -0700 Subject: [PATCH 13/21] readline: add an explicit 'enabled' option, false by default Need to update the process REPL (and probably the debugger) to set this to true. --- lib/readline.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/readline.js b/lib/readline.js index ddda7528559..136254a3041 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -33,12 +33,12 @@ var inherits = require('util').inherits; var EventEmitter = require('events').EventEmitter; -exports.createInterface = function(input, output, completer) { - return new Interface(input, output, completer); +exports.createInterface = function(input, output, completer, enabled) { + return new Interface(input, output, completer, enabled); }; -function Interface(input, output, completer) { +function Interface(input, output, completer, enabled) { if (!(this instanceof Interface)) { return new Interface(input, output, completer); } @@ -63,7 +63,7 @@ function Interface(input, output, completer) { this.setPrompt('> '); - this.enabled = output.isTTY; + this.enabled = enabled; if (parseInt(process.env['NODE_NO_READLINE'], 10)) { this.enabled = false; From bb5b47e2233780da25653366fe172bcbfefdf7db Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Tue, 13 Mar 2012 23:00:11 -0700 Subject: [PATCH 14/21] readline: accept an options object when creating Support for backwards-compat as well. --- lib/readline.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/readline.js b/lib/readline.js index 136254a3041..24a7703fc59 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -42,6 +42,15 @@ function Interface(input, output, completer, enabled) { if (!(this instanceof Interface)) { return new Interface(input, output, completer); } + + if (!input.hasOwnProperty('readable')) { + // an options object was given + output = input.output; + completer = input.completer; + enabled = input.enabled; + input = input.input; + } + EventEmitter.call(this); completer = completer || function() { return []; }; From 8fc8112e527d42be2a8caae7f82d6b1bdd1c587f Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Tue, 13 Mar 2012 23:01:54 -0700 Subject: [PATCH 15/21] repl: accept an options object when creating REPLServer Again, support for the backwards-compat API as well. --- lib/repl.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/lib/repl.js b/lib/repl.js index 9616dd43897..a9e35cd64c9 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -76,6 +76,25 @@ var 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; + if (typeof prompt == 'object') { + // an options object was given + options = prompt; + stream = options.stream || options.socket; + 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; From d2b7b5ba5f239c9b3dba1a125b0f70bedb61e62e Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Tue, 13 Mar 2012 23:02:47 -0700 Subject: [PATCH 16/21] repl: use an options object on readline for the 'enabled' option --- lib/repl.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/repl.js b/lib/repl.js index a9e35cd64c9..669898a5c57 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -139,7 +139,12 @@ function REPLServer(prompt, stream, eval, useGlobal, ignoreUndefined) { 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, + enabled: options.enabled + }); self.rli = rli; this.commands = {}; From 75c45ade1c95b4268f0b1f351fcf415268950481 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Tue, 13 Mar 2012 23:08:37 -0700 Subject: [PATCH 17/21] process: use the new repl API for the global repl --- src/node.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/node.js b/src/node.js index 52467a69b20..18399c34836 100644 --- a/src/node.js +++ b/src/node.js @@ -100,7 +100,12 @@ // If stdin is a TTY. if (NativeModule.require('tty').isatty(0)) { // REPL - var repl = Module.requireRepl().start('> ', null, null, true); + var repl = Module.requireRepl().start({ + prompt: '> ', + enabled: true, + useGlobal: true, + ignoreUndefined: false + }); repl.on('exit', function() { process.exit(); }); From 3b006d5ca92fa0705c8286e8d2a4e8e69094c44e Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Tue, 13 Mar 2012 23:11:23 -0700 Subject: [PATCH 18/21] debugger: use the new repl API for the debugger repl --- lib/_debugger.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/_debugger.js b/lib/_debugger.js index 9dceb8eaa76..46ebaf92f9b 100644 --- a/lib/_debugger.js +++ b/lib/_debugger.js @@ -751,8 +751,13 @@ function Interface(stdin, stdout, args) { // 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> ', + stream: streams, + eval: this.controlEval.bind(this), + useGlobal: false, + ignoreUndefined: true + }); // Kill child process when main process dies process.on('exit', function() { From 20148ce2e2ec30af31ce97ce32553563133cf157 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Tue, 13 Mar 2012 23:17:56 -0700 Subject: [PATCH 19/21] move the NODE_NO_READLINE check to the global/debugger repl It belongs here, since if someone is actually running a net.Socket repl server, then we don't necessarily want this check to take place. --- lib/_debugger.js | 1 + lib/readline.js | 4 ---- src/node.js | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/_debugger.js b/lib/_debugger.js index 46ebaf92f9b..c8d98e0a058 100644 --- a/lib/_debugger.js +++ b/lib/_debugger.js @@ -754,6 +754,7 @@ function Interface(stdin, stdout, args) { this.repl = repl.start({ prompt: 'debug> ', stream: streams, + enabled: !parseInt(process.env['NODE_NO_READLINE'], 10), eval: this.controlEval.bind(this), useGlobal: false, ignoreUndefined: true diff --git a/lib/readline.js b/lib/readline.js index 24a7703fc59..1cede417838 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -74,10 +74,6 @@ function Interface(input, output, completer, enabled) { this.enabled = enabled; - if (parseInt(process.env['NODE_NO_READLINE'], 10)) { - this.enabled = false; - } - if (!this.enabled) { input.on('data', function(data) { self._normalWrite(data); diff --git a/src/node.js b/src/node.js index 18399c34836..33b0bb20a01 100644 --- a/src/node.js +++ b/src/node.js @@ -102,7 +102,7 @@ // REPL var repl = Module.requireRepl().start({ prompt: '> ', - enabled: true, + enabled: !parseInt(process.env['NODE_NO_READLINE'], 10), useGlobal: true, ignoreUndefined: false }); From 35c9d43422b3484617784a2b375d6d15c2c15c84 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Thu, 15 Mar 2012 11:57:29 -0700 Subject: [PATCH 20/21] readline: coerce the 'enabled' var into a boolean --- lib/readline.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/readline.js b/lib/readline.js index 1cede417838..fe99eacfebf 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -40,7 +40,7 @@ exports.createInterface = function(input, output, completer, enabled) { function Interface(input, output, completer, enabled) { if (!(this instanceof Interface)) { - return new Interface(input, output, completer); + return new Interface(input, output, completer, enabled); } if (!input.hasOwnProperty('readable')) { @@ -72,7 +72,7 @@ function Interface(input, output, completer, enabled) { this.setPrompt('> '); - this.enabled = enabled; + this.enabled = !!enabled; if (!this.enabled) { input.on('data', function(data) { From ba477786577a369c4b903ed87573074a82225e9c Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Sat, 17 Mar 2012 12:55:57 -0700 Subject: [PATCH 21/21] readline: emitKeypress() -> emitKeypressEvents() --- lib/readline.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/readline.js b/lib/readline.js index fe99eacfebf..034da78f3c6 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -81,7 +81,7 @@ function Interface(input, output, completer, enabled) { } else { - exports.emitKeypress(input); + exports.emitKeypressEvents(input); // input usually refers to stdin input.on('keypress', function(s, key) { @@ -739,7 +739,7 @@ exports.Interface = Interface; * accepts a readable Stream instance and makes it emit "keypress" events */ -function emitKeypress(stream) { +function emitKeypressEvents(stream) { if (stream._emitKeypress) return; var keypressListeners = stream.listeners('keypress'); @@ -764,7 +764,7 @@ function emitKeypress(stream) { stream.on('newListener', onNewListener); stream._emitKeypress = true; } -exports.emitKeypress = emitKeypress; +exports.emitKeypressEvents = emitKeypressEvents; /* Some patterns seen in terminal key escape codes, derived from combos seen