diff --git a/lib/program.js b/lib/program.js index 162f2a5..e426848 100644 --- a/lib/program.js +++ b/lib/program.js @@ -44,36 +44,38 @@ Program.prototype.term = function(is) { }; Program.prototype.listen = function() { - if (!this.input.isTTY || !this.output.isTTY) return; + if (!this.input.isTTY || !this.output.isTTY) { + return; + } var readline = require('readline') , self = this; - this._raw = true; + if (!this.input.isRaw) { + this.input.setRawMode(true); - this.input.setRawMode(true); - - this.input.on('keypress', function(ch, key) { - key = key || 0; - if (key.ctrl && key.name === 'c') { - if (process.listeners('SIGINT').length) { - process.emit('SIGINT'); + this.input.on('keypress', function(ch, key) { + key = key || 0; + if (key.ctrl && key.name === 'c') { + if (process.listeners('SIGINT').length) { + process.emit('SIGINT'); + } + if (self.listeners('SIGINT').length) { + self.emit('SIGINT'); + } + return; } - if (self.listeners('SIGINT').length) { - self.emit('SIGINT'); - } - return; - } - self.emit('keypress', ch, key); - }); + self.emit('keypress', ch, key); + }); - this.input.on('data', function(data) { - self.emit('data', data); - }); + this.input.on('data', function(data) { + self.emit('data', data); + }); - readline.emitKeypressEvents(this.input); + readline.emitKeypressEvents(this.input); - this.input.resume(); + this.input.resume(); + } this.output.on('resize', function() { self.cols = self.output.columns; @@ -81,18 +83,15 @@ Program.prototype.listen = function() { self.emit('resize'); }); - // this.getCursor(function(err, cursor) { - // if (err) return; - // self.x = cursor.x; - // self.y = cursor.y; - // }); - this.on('newListener', function fn(type) { if (type === 'mouse') { self.removeListener('newListener', fn); self.bindMouse(); } }); + + // self.bindMouse(); + // self.bindResponse(); }; // XTerm mouse events @@ -142,7 +141,7 @@ Program.prototype._bindMouse = function(s) { } // XTerm / X10 - if (parts = /^\x1b\[M([\x00\u0020-\xffff]{3})/.exec(s)) { + if (parts = /^\x1b\[M([\x00\u0020-\uffff]{3})/.exec(s)) { var b = parts[1].charCodeAt(0) , x = parts[1].charCodeAt(1) , y = parts[1].charCodeAt(2) @@ -352,6 +351,7 @@ Program.prototype._bindMouse = function(s) { } }; +// All possible responses from the terminal Program.prototype.bindResponse = function() { this.on('data', this._bindResponse.bind(this)); this.bindResponse = function() {}; @@ -435,33 +435,18 @@ Program.prototype._bindResponse = function(s) { : 'unavailable' }); } + + return this.emit('response', { + error: 'Unhandled: ' + JSON.stringify(parts) + }); } -// CSI Ps n Device Status Report (DSR). -// Ps = 5 -> Status Report. Result (``OK'') is -// CSI 0 n -// Ps = 6 -> Report Cursor Position (CPR) [row;column]. -// Result is -// CSI r ; c R -// CSI ? Ps n -// Device Status Report (DSR, DEC-specific). -// Ps = 6 -> Report Cursor Position (CPR) [row;column] as CSI -// ? r ; c R (assumes page is zero). -// Ps = 1 5 -> Report Printer status as CSI ? 1 0 n (ready). -// or CSI ? 1 1 n (not ready). -// Ps = 2 5 -> Report UDK status as CSI ? 2 0 n (unlocked) -// or CSI ? 2 1 n (locked). -// Ps = 2 6 -> Report Keyboard status as -// CSI ? 2 7 ; 1 ; 0 ; 0 n (North American). -// The last two parameters apply to VT400 & up, and denote key- -// board ready and LK01 respectively. -// Ps = 5 3 -> Report Locator status as -// CSI ? 5 3 n Locator available, if compiled-in, or -// CSI ? 5 0 n No Locator, if not. - + // CSI Ps n Device Status Report (DSR). // Ps = 6 -> Report Cursor Position (CPR) [row;column]. // Result is // CSI r ; c R + // CSI ? Ps n + // Device Status Report (DSR, DEC-specific). // Ps = 6 -> Report Cursor Position (CPR) [row;column] as CSI // ? r ; c R (assumes page is zero). if (parts = /^\x1b\[(\?)?(\d+);(\d+)R/.exec(s)) { @@ -534,6 +519,10 @@ Program.prototype._bindResponse = function(s) { } }); } + + return this.emit('response', { + error: 'Unhandled: ' + JSON.stringify(parts) + }); } // CSI Ps ; Ps ; Ps t @@ -557,14 +546,19 @@ Program.prototype._bindResponse = function(s) { windowTitle: parts[2] }); } + + return this.emit('response', { + error: 'Unhandled: ' + JSON.stringify(parts) + }); } }; Program.prototype.receive = function(text, callback) { var listeners = this.listeners('keypress') - , bak = listeners.slice(); + , bak = listeners.slice() + , self = this; - if (!this._raw) { + if (!this.input.isRaw) { throw new Error('Input must be raw.'); } @@ -575,26 +569,53 @@ Program.prototype.receive = function(text, callback) { text = null; } - this.input.once('data', function(data) { - listeners.push.apply(listeners, bak); - if (typeof data !== 'string') { - data = data.toString('utf8'); - } - return callback(null, data); - }); + return process.nextTick(function() { + self.input.once('data', function(data) { + listeners.push.apply(listeners, bak); + if (typeof data !== 'string') { + data = data.toString('utf8'); + } + return callback(null, data); + }); - if (text) this.write(text); + if (text) self.write(text); + }); +}; + +Program.prototype.receive_ = function(text, callback) { + var listeners = this.listeners('keypress') + , bak = listeners.slice() + , self = this; + + if (!this.input.isRaw) { + throw new Error('Input must be raw.'); + } + + listeners.length = 0; + + if (!callback) { + callback = text; + text = null; + } + + return process.nextTick(function() { + self.input.once('data', function(data) { + listeners.push.apply(listeners, bak); + self.once('response', function(event) { + return callback(null, event[Object.keys(event)[0]]); + }); + self._bindResponse(data); + }); + + if (text) self.write(text); + }); }; Program.prototype.write = Program.prototype.echo = function(text, attr) { - if (attr) { - this.attr(attr, true); - this.write(text); - this.attr(attr, false); - return; - } - return this.output.write(text); + return attr + ? this.output.write(this.text(text, attr)) + : this.output.write(text); }; Program.prototype.setx = function(x) { @@ -1041,6 +1062,25 @@ Program.prototype.eraseInLine = function(param) { Program.prototype.sgr = Program.prototype.attr = Program.prototype.charAttributes = function(param, val) { + return this.write(this._attr(param, val)); +}; + +Program.prototype.text = function(text, attr) { + return this._attr(attr, true) + text + this._attr(attr, false); +}; + +Program.prototype._attr = function(param, val) { + var self = this + , parts = param.split(/\s*[,;]\s*/) + , out = ''; + + if (parts.length > 1) { + parts.forEach(function(part) { + out += self._attr(part, val); + }); + return out; + } + if (param.indexOf('no ') === 0) { param = param.substring(3); val = false; @@ -1048,118 +1088,233 @@ Program.prototype.charAttributes = function(param, val) { param = param.substring(1); val = false; } + switch (param) { + // attributes case 'normal': - return this.write('\x1b[m'); + return '\x1b[m'; case 'bold': return val === false - ? this.write('\x1b[22m') - : this.write('\x1b[1m'); + ? '\x1b[22m' + : '\x1b[1m'; case 'underlined': return val === false - ? this.write('\x1b[24m') - : this.write('\x1b[4m'); + ? '\x1b[24m' + : '\x1b[4m'; case 'blink': return val === false - ? this.write('\x1b[25m') - : this.write('\x1b[5m'); + ? '\x1b[25m' + : '\x1b[5m'; case 'inverse': return val === false - ? this.write('\x1b[27m') - : this.write('\x1b[7m'); + ? '\x1b[27m' + : '\x1b[7m'; break; case 'invisible': return val === false - ? this.write('\x1b[28m') - : this.write('\x1b[8m'); + ? '\x1b[28m' + : '\x1b[8m'; case 'invisible': return val === false - ? this.write('\x1b[28m') - : this.write('\x1b[8m'); + ? '\x1b[28m' + : '\x1b[8m'; + // 8-color foreground case 'black fg': return val === false - ? this.write('\x1b[39m') - : this.write('\x1b[30m'); + ? '\x1b[39m' + : '\x1b[30m'; case 'red fg': return val === false - ? this.write('\x1b[39m') - : this.write('\x1b[31m'); + ? '\x1b[39m' + : '\x1b[31m'; case 'green fg': - return val === false - ? this.write('\x1b[39m') - : this.write('\x1b[32m'); + return val === false + ? '\x1b[39m' + : '\x1b[32m'; case 'yellow fg': return val === false - ? this.write('\x1b[39m') - : this.write('\x1b[33m'); + ? '\x1b[39m' + : '\x1b[33m'; case 'blue fg': return val === false - ? this.write('\x1b[39m') - : this.write('\x1b[34m'); + ? '\x1b[39m' + : '\x1b[34m'; case 'magenta fg': return val === false - ? this.write('\x1b[39m') - : this.write('\x1b[35m'); + ? '\x1b[39m' + : '\x1b[35m'; case 'cyan fg': return val === false - ? this.write('\x1b[39m') - : this.write('\x1b[36m'); + ? '\x1b[39m' + : '\x1b[36m'; case 'white fg': return val === false - ? this.write('\x1b[39m') - : this.write('\x1b[37m'); + ? '\x1b[39m' + : '\x1b[37m'; case 'default fg': - return this.write('\x1b[39m'); + return '\x1b[39m'; + // 8-color background case 'black bg': return val === false - ? this.write('\x1b[49m') - : this.write('\x1b[40m'); + ? '\x1b[49m' + : '\x1b[40m'; case 'red bg': return val === false - ? this.write('\x1b[49m') - : this.write('\x1b[41m'); + ? '\x1b[49m' + : '\x1b[41m'; case 'green bg': return val === false - ? this.write('\x1b[49m') - : this.write('\x1b[42m'); + ? '\x1b[49m' + : '\x1b[42m'; case 'yellow bg': return val === false - ? this.write('\x1b[49m') - : this.write('\x1b[43m'); + ? '\x1b[49m' + : '\x1b[43m'; case 'blue bg': return val === false - ? this.write('\x1b[49m') - : this.write('\x1b[44m'); + ? '\x1b[49m' + : '\x1b[44m'; case 'magenta bg': return val === false - ? this.write('\x1b[49m') - : this.write('\x1b[45m'); + ? '\x1b[49m' + : '\x1b[45m'; case 'cyan bg': return val === false - ? this.write('\x1b[49m') - : this.write('\x1b[46m'); + ? '\x1b[49m' + : '\x1b[46m'; case 'white bg': return val === false - ? this.write('\x1b[49m') - : this.write('\x1b[47m'); + ? '\x1b[49m' + : '\x1b[47m'; case 'default bg': - return this.write('\x1b[49m'); + return '\x1b[49m'; + + // 16-color foreground + case 'light black fg': + return val === false + ? '\x1b[39m' + : '\x1b[90m'; + case 'light red fg': + return val === false + ? '\x1b[39m' + : '\x1b[91m'; + case 'light green fg': + return val === false + ? '\x1b[39m' + : '\x1b[92m'; + case 'light yellow fg': + return val === false + ? '\x1b[39m' + : '\x1b[93m'; + case 'light blue fg': + return val === false + ? '\x1b[39m' + : '\x1b[94m'; + case 'light magenta fg': + return val === false + ? '\x1b[39m' + : '\x1b[95m'; + case 'light cyan fg': + return val === false + ? '\x1b[39m' + : '\x1b[96m'; + case 'light white fg': + return val === false + ? '\x1b[39m' + : '\x1b[97m'; + + // 16-color background + case 'light black bg': + return val === false + ? '\x1b[49m' + : '\x1b[100m'; + case 'light red bg': + return val === false + ? '\x1b[49m' + : '\x1b[101m'; + case 'light green bg': + return val === false + ? '\x1b[49m' + : '\x1b[102m'; + case 'light yellow bg': + return val === false + ? '\x1b[49m' + : '\x1b[103m'; + case 'light blue bg': + return val === false + ? '\x1b[49m' + : '\x1b[104m'; + case 'light magenta bg': + return val === false + ? '\x1b[49m' + : '\x1b[105m'; + case 'light cyan bg': + return val === false + ? '\x1b[49m' + : '\x1b[106m'; + case 'light white bg': + return val === false + ? '\x1b[49m' + : '\x1b[107m'; + + // non-16-color rxvt default fg and bg + case 'default fg bg': + if (this.term('rxvt')) { + return '\x1b[100m'; + } + return this._attr('default fg') + this._attr('default bg'); default: - return this.write('\x1b[' + param + 'm'); + // 256-color fg and bg + var m = /^(\d+) (fg|bg)$/.exec(param); + if (m) { + var color = +m[1] - 1; + + if (val === false) { + return this._attr('default ' + m[2]); + } + + if (color < 16) { + if (m[2] === 'fg') { + if (color < 8) { + color += 30; + } else if (color < 16) { + color += 90; + } + } else if (m[2] === 'bg') { + if (color < 8) { + color += 40; + } else if (color < 16) { + color += 100; + } + } + return '\x1b[' + color + 'm'; + } + + if (m[2] === 'fg') { + return '\x1b[38;5;' + color + 'm'; + } + + if (m[2] === 'bg') { + return '\x1b[48;5;' + color + 'm'; + } + } + return '\x1b[' + param + 'm'; } }; Program.prototype.fg = Program.prototype.setForeground = function(color, val) { - return this.attr(color + ' fg', val); + color = color.split(/\s*[,;]\s*/).join(' fg, ') + ' fg'; + return this.attr(color, val); }; Program.prototype.bg = Program.prototype.setBackground = function(color, val) { - return this.attr(color + ' bg', val); + color = color.split(/\s*[,;]\s*/).join(' bg, ') + ' bg'; + return this.attr(color, val); }; // CSI Ps n Device Status Report (DSR).