diff --git a/lib/tty.js b/lib/tty_legacy.js similarity index 100% rename from lib/tty.js rename to lib/tty_legacy.js diff --git a/lib/tty_uv.js b/lib/tty_uv.js new file mode 100644 index 0000000000..95e7bd3951 --- /dev/null +++ b/lib/tty_uv.js @@ -0,0 +1,371 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +var assert = require('assert'); +var inherits = require('util').inherits; +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); +}; + + +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"); +}; + + +function ReadStream(fd) { + if (!(this instanceof ReadStream)) return new ReadStream(fd); + net.Socket.call(this, { + handle: new TTY(fd) + }); + + 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); +} +inherits(ReadStream, net.Socket); +exports.ReadStream = ReadStream; + + +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'); + } + } + + 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); + + // 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; + + /* 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; + + } + } 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); + net.Socket.call(this, { + handle: new TTY(fd) + }); + + this.readable = false; +} +inherits(WriteStream, net.Socket); +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.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'); + } +}; + + +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'); + } +}; + diff --git a/src/node.js b/src/node.js index 761f53609d..dd8f850104 100644 --- a/src/node.js +++ b/src/node.js @@ -447,6 +447,9 @@ case 'net': return process.features.uv ? 'net_uv' : 'net_legacy'; + case 'tty': + return process.features.uv ? 'tty_uv' : 'tty_legacy'; + case 'child_process': return process.features.uv ? 'child_process_uv' : 'child_process_legacy'; diff --git a/src/tty_wrap.cc b/src/tty_wrap.cc index f78739808d..3c85aaeb07 100644 --- a/src/tty_wrap.cc +++ b/src/tty_wrap.cc @@ -6,6 +6,16 @@ namespace node { +#define UNWRAP \ + assert(!args.Holder().IsEmpty()); \ + assert(args.Holder()->InternalFieldCount() > 0); \ + TTYWrap* wrap = \ + static_cast(args.Holder()->GetPointerFromInternalField(0)); \ + if (!wrap) { \ + SetErrno(UV_EBADF); \ + return scope.Close(Integer::New(-1)); \ + } + using v8::Object; using v8::Handle; using v8::Local; @@ -40,6 +50,9 @@ class TTYWrap : StreamWrap { NODE_SET_PROTOTYPE_METHOD(t, "readStop", StreamWrap::ReadStop); NODE_SET_PROTOTYPE_METHOD(t, "write", StreamWrap::Write); + NODE_SET_PROTOTYPE_METHOD(t, "getWindowSize", TTYWrap::GetWindowSize); + NODE_SET_PROTOTYPE_METHOD(t, "setRawMode", SetRawMode); + NODE_SET_METHOD(target, "isTTY", IsTTY); target->Set(String::NewSymbol("TTY"), t->GetFunction()); @@ -53,6 +66,48 @@ class TTYWrap : StreamWrap { return uv_guess_handle(fd) == UV_TTY ? v8::True() : v8::False(); } + static Handle GetWindowSize(const Arguments& args) { + HandleScope scope; + + UNWRAP + + int width, height; + int r = uv_tty_get_winsize(&wrap->handle_, &width, &height); + + if (r) { + SetErrno(uv_last_error(uv_default_loop()).code); + return v8::Undefined(); + } + + Local a = v8::Array::New(2); + a->Set(0, Integer::New(width)); + a->Set(1, Integer::New(height)); + + return scope.Close(a); + } + + static Handle SetRawMode(const Arguments& args) { + HandleScope scope; + + assert(!args.Holder().IsEmpty()); + assert(args.Holder()->InternalFieldCount() > 0); + TTYWrap* wrap = + static_cast(args.Holder()->GetPointerFromInternalField(0)); + + if (!wrap) { + SetErrno(UV_EBADF); + return scope.Close(Integer::New(-1)); + } + + int r = uv_tty_set_mode(&wrap->handle_, args[0]->IsTrue()); + + if (r) { + SetErrno(uv_last_error(uv_default_loop()).code); + } + + return scope.Close(Integer::New(r)); + } + static Handle New(const Arguments& args) { HandleScope scope;