diff options
author | Nathan Rajlich <nathan@tootallnate.net> | 2012-03-27 02:21:25 +0400 |
---|---|---|
committer | Nathan Rajlich <nathan@tootallnate.net> | 2012-03-27 02:21:25 +0400 |
commit | aad12d0b265c9b06ae029d6ee168849260a91dd6 (patch) | |
tree | 7aa1deffdfc5216b50beb4c43eaf09088643a67b /lib | |
parent | ab518ae50e480b5ffe01bdaaabd8c652200d1d55 (diff) |
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.
Diffstat (limited to 'lib')
-rw-r--r-- | lib/_debugger.js | 16 | ||||
-rw-r--r-- | lib/readline.js | 434 | ||||
-rw-r--r-- | lib/repl.js | 62 | ||||
-rw-r--r-- | lib/tty.js | 348 |
4 files changed, 483 insertions, 377 deletions
diff --git a/lib/_debugger.js b/lib/_debugger.js index f454aa9c4cc..dcf046bcf5a 100644 --- a/lib/_debugger.js +++ b/lib/_debugger.js @@ -745,15 +745,17 @@ function Interface(stdin, stdout, args) { this.stdout = stdout; this.args = args; - var streams = { - stdin: stdin, - stdout: stdout - }; - // Two eval modes are available: controlEval and debugEval // But controlEval is used by default - this.repl = new repl.REPLServer('debug> ', streams, - this.controlEval.bind(this), false, true); + this.repl = repl.start({ + prompt: 'debug> ', + input: this.stdin, + output: this.stdout, + terminal: !parseInt(process.env['NODE_NO_READLINE'], 10), + eval: this.controlEval.bind(this), + useGlobal: false, + ignoreUndefined: true + }); // Do not print useless warning repl._builtinLibs.splice(repl._builtinLibs.indexOf('repl'), 1); diff --git a/lib/readline.js b/lib/readline.js index 3a3d244f22e..94d88ed2592 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -31,18 +31,32 @@ var kBufSize = 10 * 1024; var util = require('util'); var inherits = require('util').inherits; var EventEmitter = require('events').EventEmitter; -var tty = require('tty'); -exports.createInterface = function(input, output, completer) { - return new Interface(input, output, completer); +exports.createInterface = function(input, output, completer, terminal) { + var rl; + if (arguments.length === 1) { + rl = new Interface(input); + } else { + rl = new Interface(input, output, completer, terminal); + } + return rl; }; -function Interface(input, output, completer) { +function Interface(input, output, completer, terminal) { if (!(this instanceof Interface)) { - return new Interface(input, output, completer); + return new Interface(input, output, completer, terminal); } + + if (arguments.length === 1) { + // an options object was given + output = input.output; + completer = input.completer; + terminal = input.terminal; + input = input.input; + } + EventEmitter.call(this); completer = completer || function() { return []; }; @@ -51,6 +65,12 @@ function Interface(input, output, completer) { throw new TypeError('Argument \'completer\' must be a function'); } + // backwards compat; check the isTTY prop of the output stream + // when `terminal` was not specified + if (typeof terminal == 'undefined') { + terminal = !!output.isTTY; + } + var self = this; this.output = output; @@ -64,19 +84,17 @@ function Interface(input, output, completer) { this.setPrompt('> '); - this.enabled = output.isTTY; - - if (parseInt(process.env['NODE_NO_READLINE'], 10)) { - this.enabled = false; - } + this.terminal = !!terminal; - if (!this.enabled) { + if (!this.terminal) { input.on('data', function(data) { self._normalWrite(data); }); } else { + exports.emitKeypressEvents(input); + // input usually refers to stdin input.on('keypress', function(s, key) { self._ttyWrite(s, key); @@ -85,9 +103,10 @@ function Interface(input, output, completer) { // Current line this.line = ''; - // Check process.env.TERM ? - tty.setRawMode(true); - this.enabled = true; + if (typeof input.setRawMode === 'function') { + input.setRawMode(true); + } + this.terminal = true; // Cursor position on the line. this.cursor = 0; @@ -95,26 +114,16 @@ function Interface(input, output, completer) { this.history = []; this.historyIndex = -1; - var winSize = output.getWindowSize(); - exports.columns = winSize[0]; - - if (process.listeners('SIGWINCH').length === 0) { - process.on('SIGWINCH', function() { - var winSize = output.getWindowSize(); - exports.columns = winSize[0]; - - // FIXME: when #2922 will be approved, change this to - // output.on('resize', ... - self._refreshLine(); - }); - } + output.on('resize', function() { + self._refreshLine(); + }); } } inherits(Interface, EventEmitter); Interface.prototype.__defineGetter__('columns', function() { - return exports.columns; + return this.output.columns || Infinity; }); Interface.prototype.setPrompt = function(prompt, length) { @@ -131,7 +140,7 @@ Interface.prototype.setPrompt = function(prompt, length) { Interface.prototype.prompt = function(preserveCursor) { if (this.paused) this.resume(); - if (this.enabled) { + if (this.terminal) { if (!preserveCursor) this.cursor = 0; this._refreshLine(); } else { @@ -194,13 +203,13 @@ Interface.prototype._refreshLine = function() { // first move to the bottom of the current line, based on cursor pos var prevRows = this.prevRows || 0; if (prevRows > 0) { - this.output.moveCursor(0, -prevRows); + exports.moveCursor(this.output, 0, -prevRows); } // Cursor to left edge. - this.output.cursorTo(0); + exports.cursorTo(this.output, 0); // erase data - this.output.clearScreenDown(); + exports.clearScreenDown(this.output); // Write the prompt and the current buffer content. this.output.write(line); @@ -211,11 +220,11 @@ Interface.prototype._refreshLine = function() { } // Move cursor to original position. - this.output.cursorTo(cursorPos.cols); + exports.cursorTo(this.output, cursorPos.cols); var diff = lineRows - cursorPos.rows; if (diff > 0) { - this.output.moveCursor(0, -diff); + exports.moveCursor(this.output, 0, -diff); } this.prevRows = cursorPos.rows; @@ -224,8 +233,10 @@ Interface.prototype._refreshLine = function() { Interface.prototype.pause = function() { if (this.paused) return; - if (this.enabled) { - tty.setRawMode(false); + if (this.terminal) { + if (typeof this.input.setRawMode === 'function') { + this.input.setRawMode(true); + } } this.input.pause(); this.paused = true; @@ -235,8 +246,10 @@ Interface.prototype.pause = function() { Interface.prototype.resume = function() { this.input.resume(); - if (this.enabled) { - tty.setRawMode(true); + if (this.terminal) { + if (typeof this.input.setRawMode === 'function') { + this.input.setRawMode(true); + } } this.paused = false; this.emit('resume'); @@ -245,7 +258,7 @@ Interface.prototype.resume = function() { Interface.prototype.write = function(d, key) { if (this.paused) this.resume(); - this.enabled ? this._ttyWrite(d, key) : this._normalWrite(d, key); + this.terminal ? this._ttyWrite(d, key) : this._normalWrite(d, key); }; @@ -514,7 +527,7 @@ Interface.prototype._moveCursor = function(dx) { // check if cursors are in the same line if (oldPos.rows === newPos.rows) { - this.output.moveCursor(this.cursor - oldcursor, 0); + exports.moveCursor(this.output, this.cursor - oldcursor, 0); this.prevRows = newPos.rows; } else { this._refreshLine(); @@ -728,3 +741,344 @@ Interface.prototype._ttyWrite = function(s, key) { exports.Interface = Interface; + + + +/** + * accepts a readable Stream instance and makes it emit "keypress" events + */ + +function emitKeypressEvents(stream) { + if (stream._emitKeypress) return; + stream._emitKeypress = true; + + var keypressListeners = stream.listeners('keypress'); + + function onData(b) { + if (keypressListeners.length) { + emitKey(stream, b); + } else { + // Nobody's watching anyway + stream.removeListener('data', onData); + stream.on('newListener', onNewListener); + } + } + + function onNewListener(event) { + if (event == 'keypress') { + stream.on('data', onData); + stream.removeListener('newListener', onNewListener); + } + } + + if (keypressListeners.length) { + stream.on('data', onData); + } else { + stream.on('newListener', onNewListener); + } +} +exports.emitKeypressEvents = emitKeypressEvents; + +/* + Some patterns seen in terminal key escape codes, derived from combos seen + at http://www.midnight-commander.org/browser/lib/tty/key.c + + ESC letter + ESC [ letter + ESC [ modifier letter + ESC [ 1 ; modifier letter + ESC [ num char + ESC [ num ; modifier char + ESC O letter + ESC O modifier letter + ESC O 1 ; modifier letter + ESC N letter + ESC [ [ num ; modifier char + ESC [ [ 1 ; modifier letter + ESC ESC [ num char + ESC ESC O letter + + - char is usually ~ but $ and ^ also happen with rxvt + - modifier is 1 + + (shift * 1) + + (left_alt * 2) + + (ctrl * 4) + + (right_alt * 8) + - two leading ESCs apparently mean the same as one leading ESC +*/ + +// Regexes used for ansi escape code splitting +var metaKeyCodeRe = /^(?:\x1b)([a-zA-Z0-9])$/; +var functionKeyCodeRe = + /^(?:\x1b+)(O|N|\[|\[\[)(?:(\d+)(?:;(\d+))?([~^$])|(?:1;)?(\d+)?([a-zA-Z]))/; + +function emitKey(stream, s) { + var char, + key = { + name: undefined, + ctrl: false, + meta: false, + shift: false + }, + parts; + + if (Buffer.isBuffer(s)) { + if (s[0] > 127 && s[1] === undefined) { + s[0] -= 128; + s = '\x1b' + s.toString(stream.encoding || 'utf-8'); + } else { + s = s.toString(stream.encoding || 'utf-8'); + } + } + + key.sequence = s; + + if (s === '\r' || s === '\n') { + // enter + key.name = 'enter'; + + } else if (s === '\t') { + // tab + key.name = 'tab'; + + } else if (s === '\b' || s === '\x7f' || + s === '\x1b\x7f' || s === '\x1b\b') { + // backspace or ctrl+h + key.name = 'backspace'; + key.meta = (s.charAt(0) === '\x1b'); + + } else if (s === '\x1b' || s === '\x1b\x1b') { + // escape key + key.name = 'escape'; + key.meta = (s.length === 2); + + } else if (s === ' ' || s === '\x1b ') { + key.name = 'space'; + key.meta = (s.length === 2); + + } else if (s <= '\x1a') { + // ctrl+letter + key.name = String.fromCharCode(s.charCodeAt(0) + 'a'.charCodeAt(0) - 1); + key.ctrl = true; + + } else if (s.length === 1 && s >= 'a' && s <= 'z') { + // lowercase letter + key.name = s; + + } else if (s.length === 1 && s >= 'A' && s <= 'Z') { + // shift+letter + key.name = s.toLowerCase(); + key.shift = true; + + } else if (parts = metaKeyCodeRe.exec(s)) { + // meta+character key + key.name = parts[1].toLowerCase(); + key.meta = true; + key.shift = /^[A-Z]$/.test(parts[1]); + + } else if (parts = functionKeyCodeRe.exec(s)) { + // ansi escape sequence + + // reassemble the key code leaving out leading \x1b's, + // the modifier key bitflag and any meaningless "1;" sequence + var code = (parts[1] || '') + (parts[2] || '') + + (parts[4] || '') + (parts[6] || ''), + modifier = (parts[3] || parts[5] || 1) - 1; + + // Parse the key modifier + key.ctrl = !!(modifier & 4); + key.meta = !!(modifier & 10); + key.shift = !!(modifier & 1); + key.code = code; + + // Parse the key itself + switch (code) { + /* xterm/gnome ESC O letter */ + case 'OP': key.name = 'f1'; break; + case 'OQ': key.name = 'f2'; break; + case 'OR': key.name = 'f3'; break; + case 'OS': key.name = 'f4'; break; + + /* xterm/rxvt ESC [ number ~ */ + case '[11~': key.name = 'f1'; break; + case '[12~': key.name = 'f2'; break; + case '[13~': key.name = 'f3'; break; + case '[14~': key.name = 'f4'; break; + + /* from Cygwin and used in libuv */ + case '[[A': key.name = 'f1'; break; + case '[[B': key.name = 'f2'; break; + case '[[C': key.name = 'f3'; break; + case '[[D': key.name = 'f4'; break; + case '[[E': key.name = 'f5'; break; + + /* common */ + case '[15~': key.name = 'f5'; break; + case '[17~': key.name = 'f6'; break; + case '[18~': key.name = 'f7'; break; + case '[19~': key.name = 'f8'; break; + case '[20~': key.name = 'f9'; break; + case '[21~': key.name = 'f10'; break; + case '[23~': key.name = 'f11'; break; + case '[24~': key.name = 'f12'; break; + + /* xterm ESC [ letter */ + case '[A': key.name = 'up'; break; + case '[B': key.name = 'down'; break; + case '[C': key.name = 'right'; break; + case '[D': key.name = 'left'; break; + case '[E': key.name = 'clear'; break; + case '[F': key.name = 'end'; break; + case '[H': key.name = 'home'; break; + + /* xterm/gnome ESC O letter */ + case 'OA': key.name = 'up'; break; + case 'OB': key.name = 'down'; break; + case 'OC': key.name = 'right'; break; + case 'OD': key.name = 'left'; break; + case 'OE': key.name = 'clear'; break; + case 'OF': key.name = 'end'; break; + case 'OH': key.name = 'home'; break; + + /* xterm/rxvt ESC [ number ~ */ + case '[1~': key.name = 'home'; break; + case '[2~': key.name = 'insert'; break; + case '[3~': key.name = 'delete'; break; + case '[4~': key.name = 'end'; break; + case '[5~': key.name = 'pageup'; break; + case '[6~': key.name = 'pagedown'; break; + + /* putty */ + case '[[5~': key.name = 'pageup'; break; + case '[[6~': key.name = 'pagedown'; break; + + /* rxvt */ + case '[7~': key.name = 'home'; break; + case '[8~': key.name = 'end'; break; + + /* rxvt keys with modifiers */ + case '[a': key.name = 'up'; key.shift = true; break; + case '[b': key.name = 'down'; key.shift = true; break; + case '[c': key.name = 'right'; key.shift = true; break; + case '[d': key.name = 'left'; key.shift = true; break; + case '[e': key.name = 'clear'; key.shift = true; break; + + case '[2$': key.name = 'insert'; key.shift = true; break; + case '[3$': key.name = 'delete'; key.shift = true; break; + case '[5$': key.name = 'pageup'; key.shift = true; break; + case '[6$': key.name = 'pagedown'; key.shift = true; break; + case '[7$': key.name = 'home'; key.shift = true; break; + case '[8$': key.name = 'end'; key.shift = true; break; + + case 'Oa': key.name = 'up'; key.ctrl = true; break; + case 'Ob': key.name = 'down'; key.ctrl = true; break; + case 'Oc': key.name = 'right'; key.ctrl = true; break; + case 'Od': key.name = 'left'; key.ctrl = true; break; + case 'Oe': key.name = 'clear'; key.ctrl = true; break; + + case '[2^': key.name = 'insert'; key.ctrl = true; break; + case '[3^': key.name = 'delete'; key.ctrl = true; break; + case '[5^': key.name = 'pageup'; key.ctrl = true; break; + case '[6^': key.name = 'pagedown'; key.ctrl = true; break; + case '[7^': key.name = 'home'; key.ctrl = true; break; + case '[8^': key.name = 'end'; key.ctrl = true; break; + + /* misc. */ + case '[Z': key.name = 'tab'; key.shift = true; break; + default: key.name = 'undefined'; break; + + } + } else if (s.length > 1 && s[0] !== '\x1b') { + // Got a longer-than-one string of characters. + // Probably a paste, since it wasn't a control sequence. + Array.prototype.forEach.call(s, function(c) { + emitKey(stream, c); + }); + return; + } + + // Don't emit a key if no name was found + if (key.name === undefined) { + key = undefined; + } + + if (s.length === 1) { + char = s; + } + + if (key || char) { + stream.emit('keypress', char, key); + } +} + + +/** + * moves the cursor to the x and y coordinate on the given stream + */ + +function cursorTo(stream, x, y) { + if (typeof x !== 'number' && typeof y !== 'number') + return; + + if (typeof x !== 'number') + throw new Error("Can't set cursor row without also setting it's column"); + + if (typeof y !== 'number') { + stream.write('\x1b[' + (x + 1) + 'G'); + } else { + stream.write('\x1b[' + (y + 1) + ';' + (x + 1) + 'H'); + } +} +exports.cursorTo = cursorTo; + + +/** + * moves the cursor relative to its current location + */ + +function moveCursor(stream, dx, dy) { + if (dx < 0) { + stream.write('\x1b[' + (-dx) + 'D'); + } else if (dx > 0) { + stream.write('\x1b[' + dx + 'C'); + } + + if (dy < 0) { + stream.write('\x1b[' + (-dy) + 'A'); + } else if (dy > 0) { + stream.write('\x1b[' + dy + 'B'); + } +} +exports.moveCursor = moveCursor; + + +/** + * clears the current line the cursor is on: + * -1 for left of the cursor + * +1 for right of the cursor + * 0 for the entire line + */ + +function clearLine(stream, dir) { + if (dir < 0) { + // to the beginning + stream.write('\x1b[1K'); + } else if (dir > 0) { + // to the end + stream.write('\x1b[0K'); + } else { + // entire line + stream.write('\x1b[2K'); + } +} +exports.clearLine = clearLine; + + +/** + * clears the screen from the current position of the cursor down + */ + +function clearScreenDown(stream) { + stream.write('\x1b[0J'); +} +exports.clearScreenDown = clearScreenDown; diff --git a/lib/repl.js b/lib/repl.js index 9f2d486b21c..c96cbe14f41 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -76,6 +76,27 @@ exports._builtinLibs = ['assert', 'buffer', 'child_process', 'cluster', function REPLServer(prompt, stream, eval, useGlobal, ignoreUndefined) { + if (!(this instanceof REPLServer)) { + return new REPLServer(prompt, stream, eval, useGlobal, ignoreUndefined); + } + + var options, input, output; + if (typeof prompt == 'object') { + // an options object was given + options = prompt; + stream = options.stream || options.socket; + input = options.input; + output = options.output; + eval = options.eval; + useGlobal = options.useGlobal; + ignoreUndefined = options.ignoreUndefined; + prompt = options.prompt; + } else if (typeof prompt != 'string') { + throw new Error('An options Object, or a prompt String are required'); + } else { + options = {}; + } + EventEmitter.call(this); var self = this; @@ -99,34 +120,45 @@ function REPLServer(prompt, stream, eval, useGlobal, ignoreUndefined) { self.resetContext(); self.bufferedCommand = ''; - if (stream) { - // We're given a duplex socket - if (stream.stdin || stream.stdout) { - self.outputStream = stream.stdout; - self.inputStream = stream.stdin; + if (!input && !output) { + // legacy API, passing a 'stream'/'socket' option + if (!stream) { + // use stdin and stdout as the default streams if none were given + stream = process; + } + if (stream.stdin && stream.stdout) { + // We're given custom object with 2 streams, or the `process` object + input = stream.stdin; + output = stream.stdout; } else { - self.outputStream = stream; - self.inputStream = stream; + // We're given a duplex readable/writable Stream, like a `net.Socket` + input = stream; + output = stream; } - } else { - self.outputStream = process.stdout; - self.inputStream = process.stdin; - process.stdin.resume(); } + + self.inputStream = input; + self.outputStream = output; + self.prompt = (prompt != undefined ? prompt : '> '); function complete(text, callback) { self.complete(text, callback); } - var rli = rl.createInterface(self.inputStream, self.outputStream, complete); + var rli = rl.createInterface({ + input: self.inputStream, + output: self.outputStream, + completer: complete, + terminal: options.terminal + }); self.rli = rli; this.commands = {}; defineDefaultCommands(this); - if (rli.enabled && !exports.disableColors && + if (rli.terminal && !exports.disableColors && exports.writer === util.inspect) { // Turn on ANSI coloring. exports.writer = function(obj, showHidden, depth) { @@ -322,10 +354,6 @@ REPLServer.prototype.displayPrompt = function(preserveCursor) { }; -// read a line from the stream, then eval it -REPLServer.prototype.readline = function(cmd) { -}; - // A stream to push an array into a REPL // used in REPLServer.complete function ArrayStream() { diff --git a/lib/tty.js b/lib/tty.js index 72fc5e57ab1..5bb73ff0a18 100644 --- a/lib/tty.js +++ b/lib/tty.js @@ -25,28 +25,17 @@ var net = require('net'); var TTY = process.binding('tty_wrap').TTY; var isTTY = process.binding('tty_wrap').isTTY; -var stdinHandle; - - exports.isatty = function(fd) { return isTTY(fd); }; +// backwards-compat exports.setRawMode = function(flag) { - assert.ok(stdinHandle, 'stdin must be initialized before calling setRawMode'); - stdinHandle.setRawMode(flag); -}; - - -exports.getWindowSize = function() { - //throw new Error("implement me"); - return 80; -}; - - -exports.setWindowSize = function() { - throw new Error('implement me'); + if (!process.stdin.isTTY) { + throw new Error('can\'t set raw mode on non-tty'); + } + process.stdin.setRawMode(flag); }; @@ -56,31 +45,9 @@ function ReadStream(fd) { handle: new TTY(fd, true) }); + this.readable = true; this.writable = false; - - var self = this, - keypressListeners = this.listeners('keypress'); - - function onData(b) { - if (keypressListeners.length) { - self._emitKey(b); - } else { - // Nobody's watching anyway - self.removeListener('data', onData); - self.on('newListener', onNewListener); - } - } - - function onNewListener(event) { - if (event == 'keypress') { - self.on('data', onData); - self.removeListener('newListener', onNewListener); - } - } - - if (!stdinHandle) stdinHandle = this._handle; - - this.on('newListener', onNewListener); + this.isRaw = false; } inherits(ReadStream, net.Socket); @@ -96,242 +63,15 @@ ReadStream.prototype.resume = function() { return net.Socket.prototype.resume.call(this); }; +ReadStream.prototype.setRawMode = function(flag) { + flag = !!flag; + this._handle.setRawMode(flag); + this.isRaw = flag; +}; ReadStream.prototype.isTTY = true; -/* - Some patterns seen in terminal key escape codes, derived from combos seen - at http://www.midnight-commander.org/browser/lib/tty/key.c - - ESC letter - ESC [ letter - ESC [ modifier letter - ESC [ 1 ; modifier letter - ESC [ num char - ESC [ num ; modifier char - ESC O letter - ESC O modifier letter - ESC O 1 ; modifier letter - ESC N letter - ESC [ [ num ; modifier char - ESC [ [ 1 ; modifier letter - ESC ESC [ num char - ESC ESC O letter - - - char is usually ~ but $ and ^ also happen with rxvt - - modifier is 1 + - (shift * 1) + - (left_alt * 2) + - (ctrl * 4) + - (right_alt * 8) - - two leading ESCs apparently mean the same as one leading ESC -*/ - - -// Regexes used for ansi escape code splitting -var metaKeyCodeRe = /^(?:\x1b)([a-zA-Z0-9])$/; -var functionKeyCodeRe = - /^(?:\x1b+)(O|N|\[|\[\[)(?:(\d+)(?:;(\d+))?([~^$])|(?:1;)?(\d+)?([a-zA-Z]))/; - - -ReadStream.prototype._emitKey = function(s) { - var char, - key = { - name: undefined, - ctrl: false, - meta: false, - shift: false - }, - parts; - - if (Buffer.isBuffer(s)) { - if (s[0] > 127 && s[1] === undefined) { - s[0] -= 128; - s = '\x1b' + s.toString(this.encoding || 'utf-8'); - } else { - s = s.toString(this.encoding || 'utf-8'); - } - } - - key.sequence = s; - - if (s === '\r' || s === '\n') { - // enter - key.name = 'enter'; - - } else if (s === '\t') { - // tab - key.name = 'tab'; - - } else if (s === '\b' || s === '\x7f' || - s === '\x1b\x7f' || s === '\x1b\b') { - // backspace or ctrl+h - key.name = 'backspace'; - key.meta = (s.charAt(0) === '\x1b'); - - } else if (s === '\x1b' || s === '\x1b\x1b') { - // escape key - key.name = 'escape'; - key.meta = (s.length === 2); - - } else if (s === ' ' || s === '\x1b ') { - key.name = 'space'; - key.meta = (s.length === 2); - - } else if (s <= '\x1a') { - // ctrl+letter - key.name = String.fromCharCode(s.charCodeAt(0) + 'a'.charCodeAt(0) - 1); - key.ctrl = true; - - } else if (s.length === 1 && s >= 'a' && s <= 'z') { - // lowercase letter - key.name = s; - - } else if (s.length === 1 && s >= 'A' && s <= 'Z') { - // shift+letter - key.name = s.toLowerCase(); - key.shift = true; - - } else if (parts = metaKeyCodeRe.exec(s)) { - // meta+character key - key.name = parts[1].toLowerCase(); - key.meta = true; - key.shift = /^[A-Z]$/.test(parts[1]); - - } else if (parts = functionKeyCodeRe.exec(s)) { - // ansi escape sequence - - // reassemble the key code leaving out leading \x1b's, - // the modifier key bitflag and any meaningless "1;" sequence - var code = (parts[1] || '') + (parts[2] || '') + - (parts[4] || '') + (parts[6] || ''), - modifier = (parts[3] || parts[5] || 1) - 1; - - // Parse the key modifier - key.ctrl = !!(modifier & 4); - key.meta = !!(modifier & 10); - key.shift = !!(modifier & 1); - key.code = code; - - // Parse the key itself - switch (code) { - /* xterm/gnome ESC O letter */ - case 'OP': key.name = 'f1'; break; - case 'OQ': key.name = 'f2'; break; - case 'OR': key.name = 'f3'; break; - case 'OS': key.name = 'f4'; break; - - /* xterm/rxvt ESC [ number ~ */ - case '[11~': key.name = 'f1'; break; - case '[12~': key.name = 'f2'; break; - case '[13~': key.name = 'f3'; break; - case '[14~': key.name = 'f4'; break; - - /* from Cygwin and used in libuv */ - case '[[A': key.name = 'f1'; break; - case '[[B': key.name = 'f2'; break; - case '[[C': key.name = 'f3'; break; - case '[[D': key.name = 'f4'; break; - case '[[E': key.name = 'f5'; break; - - /* common */ - case '[15~': key.name = 'f5'; break; - case '[17~': key.name = 'f6'; break; - case '[18~': key.name = 'f7'; break; - case '[19~': key.name = 'f8'; break; - case '[20~': key.name = 'f9'; break; - case '[21~': key.name = 'f10'; break; - case '[23~': key.name = 'f11'; break; - case '[24~': key.name = 'f12'; break; - - /* xterm ESC [ letter */ - case '[A': key.name = 'up'; break; - case '[B': key.name = 'down'; break; - case '[C': key.name = 'right'; break; - case '[D': key.name = 'left'; break; - case '[E': key.name = 'clear'; break; - case '[F': key.name = 'end'; break; - case '[H': key.name = 'home'; break; - - /* xterm/gnome ESC O letter */ - case 'OA': key.name = 'up'; break; - case 'OB': key.name = 'down'; break; - case 'OC': key.name = 'right'; break; - case 'OD': key.name = 'left'; break; - case 'OE': key.name = 'clear'; break; - case 'OF': key.name = 'end'; break; - case 'OH': key.name = 'home'; break; - - /* xterm/rxvt ESC [ number ~ */ - case '[1~': key.name = 'home'; break; - case '[2~': key.name = 'insert'; break; - case '[3~': key.name = 'delete'; break; - case '[4~': key.name = 'end'; break; - case '[5~': key.name = 'pageup'; break; - case '[6~': key.name = 'pagedown'; break; - - /* putty */ - case '[[5~': key.name = 'pageup'; break; - case '[[6~': key.name = 'pagedown'; break; - - /* rxvt */ - case '[7~': key.name = 'home'; break; - case '[8~': key.name = 'end'; break; - - /* rxvt keys with modifiers */ - case '[a': key.name = 'up'; key.shift = true; break; - case '[b': key.name = 'down'; key.shift = true; break; - case '[c': key.name = 'right'; key.shift = true; break; - case '[d': key.name = 'left'; key.shift = true; break; - case '[e': key.name = 'clear'; key.shift = true; break; - - case '[2$': key.name = 'insert'; key.shift = true; break; - case '[3$': key.name = 'delete'; key.shift = true; break; - case '[5$': key.name = 'pageup'; key.shift = true; break; - case '[6$': key.name = 'pagedown'; key.shift = true; break; - case '[7$': key.name = 'home'; key.shift = true; break; - case '[8$': key.name = 'end'; key.shift = true; break; - - case 'Oa': key.name = 'up'; key.ctrl = true; break; - case 'Ob': key.name = 'down'; key.ctrl = true; break; - case 'Oc': key.name = 'right'; key.ctrl = true; break; - case 'Od': key.name = 'left'; key.ctrl = true; break; - case 'Oe': key.name = 'clear'; key.ctrl = true; break; - - case '[2^': key.name = 'insert'; key.ctrl = true; break; - case '[3^': key.name = 'delete'; key.ctrl = true; break; - case '[5^': key.name = 'pageup'; key.ctrl = true; break; - case '[6^': key.name = 'pagedown'; key.ctrl = true; break; - case '[7^': key.name = 'home'; key.ctrl = true; break; - case '[8^': key.name = 'end'; key.ctrl = true; break; - - /* misc. */ - case '[Z': key.name = 'tab'; key.shift = true; break; - default: key.name = 'undefined'; break; - - } - } else if (s.length > 1 && s[0] !== '\x1b') { - // Got a longer-than-one string of characters. - // Probably a paste, since it wasn't a control sequence. - Array.prototype.forEach.call(s, this._emitKey, this); - return; - } - - // Don't emit a key if no name was found - if (key.name === undefined) { - key = undefined; - } - - if (s.length === 1) { - char = s; - } - - if (key || char) { - this.emit('keypress', char, key); - } -}; - function WriteStream(fd) { if (!(this instanceof WriteStream)) return new WriteStream(fd); @@ -341,6 +81,10 @@ function WriteStream(fd) { this.readable = false; this.writable = true; + + var winSize = this._handle.getWindowSize(); + this.columns = winSize[0]; + this.rows = winSize[1]; } inherits(WriteStream, net.Socket); exports.WriteStream = WriteStream; @@ -349,55 +93,33 @@ exports.WriteStream = WriteStream; WriteStream.prototype.isTTY = true; -WriteStream.prototype.cursorTo = function(x, y) { - if (typeof x !== 'number' && typeof y !== 'number') - return; - - if (typeof x !== 'number') - throw new Error("Can't set cursor row without also setting it's column"); - - if (typeof y !== 'number') { - this.write('\x1b[' + (x + 1) + 'G'); - } else { - this.write('\x1b[' + (y + 1) + ';' + (x + 1) + 'H'); +WriteStream.prototype._refreshSize = function() { + var oldCols = this.columns; + var oldRows = this.rows; + var winSize = this._handle.getWindowSize(); + var newCols = winSize[0]; + var newRows = winSize[1]; + if (oldCols !== newCols || oldRows !== newRows) { + this.columns = newCols; + this.rows = newRows; + this.emit('resize'); } -}; +} +// backwards-compat +WriteStream.prototype.cursorTo = function(x, y) { + require('readline').cursorTo(this, x, y); +}; WriteStream.prototype.moveCursor = function(dx, dy) { - if (dx < 0) { - this.write('\x1b[' + (-dx) + 'D'); - } else if (dx > 0) { - this.write('\x1b[' + dx + 'C'); - } - - if (dy < 0) { - this.write('\x1b[' + (-dy) + 'A'); - } else if (dy > 0) { - this.write('\x1b[' + dy + 'B'); - } + require('readline').moveCursor(this, dx, dy); }; - - WriteStream.prototype.clearLine = function(dir) { - if (dir < 0) { - // to the beginning - this.write('\x1b[1K'); - } else if (dir > 0) { - // to the end - this.write('\x1b[0K'); - } else { - // entire line - this.write('\x1b[2K'); - } + require('readline').clearLine(this, dir); }; - - WriteStream.prototype.clearScreenDown = function() { - this.write('\x1b[0J'); + require('readline').clearScreenDown(this); }; - - WriteStream.prototype.getWindowSize = function() { - return this._handle.getWindowSize(); + return [this.columns, this.rows]; }; |