From b46d1161c5f879e781f65138107b2a413362f35f Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Thu, 18 Jul 2013 01:03:47 -0500 Subject: [PATCH] refactor responses. --- lib/program.js | 515 +++++++++++++++++++++++++++++++++++-------------- lib/widget.js | 2 +- 2 files changed, 374 insertions(+), 143 deletions(-) diff --git a/lib/program.js b/lib/program.js index 68af9c0..c828b01 100644 --- a/lib/program.js +++ b/lib/program.js @@ -264,18 +264,17 @@ Program.prototype.bindMouse = function() { }; Program.prototype._bindMouse = function(s) { - var self = this; - - // FROM: node/lib/readline.js - var ch - , key = { - name: undefined, - ctrl: false, - meta: false, - shift: false - } + var self = this + , key , parts; + key = { + name: undefined, + ctrl: false, + meta: false, + shift: false + }; + if (Buffer.isBuffer(s)) { if (s[0] > 127 && s[1] === undefined) { s[0] -= 128; @@ -447,6 +446,8 @@ Program.prototype._bindMouse = function(s) { } // DEC + // The xterm mouse documentation says there is a + // `<` prefix, the DECRQLP says there is no prefix. if (parts = /^\x1b\[<(\d+;\d+;\d+;\d+)&w/.exec(s)) { var parts = parts[1].split(';') , b = +parts[0] @@ -525,16 +526,8 @@ Program.prototype.bindResponse = function() { }; Program.prototype._bindResponse = function(s) { - var self = this; - - // FROM: node/lib/readline.js - var ch - , key = { - name: undefined, - ctrl: false, - meta: false, - shift: false - } + var self = this + , out = {} , parts; if (Buffer.isBuffer(s)) { @@ -551,11 +544,14 @@ Program.prototype._bindResponse = function(s) { // CSI > P s c // Send Device Attributes (Secondary DA). if (parts = /^\x1b\[(\?|>)(\d*(?:;\d*)*)c/.exec(s)) { - var primary = parts[1] === '?' - , parts = parts[2].split(';').map(function(ch) { return +ch || 0; }) - , out = {}; + parts = parts[2].split(';').map(function(ch) { + return +ch || 0; + }); - if (primary) { + out.event = 'device-attributes'; + out.code = 'DA'; + + if (parts[1] === '?') { out.type = 'primary-attribute'; // VT100-style params: if (parts[0] === 1 && parts[2] === 2) { @@ -645,9 +641,13 @@ Program.prototype._bindResponse = function(s) { out.romCartridgeRegistrationNumber = parts[2]; } - return this.emit('response', { - deviceAttributes: out - }); + // LEGACY + out.deviceAttributes = out; + + this.emit('response', out); + this.emit('response ' + out.event, out); + + return; } // CSI Ps n Device Status Report (DSR). @@ -667,26 +667,50 @@ Program.prototype._bindResponse = function(s) { // CSI ? 5 3 n Locator available, if compiled-in, or // CSI ? 5 0 n No Locator, if not. if (parts = /^\x1b\[(\?)?(\d+)(?:;(\d+);(\d+);(\d+))?n/.exec(s)) { + out.event = 'device-status'; + out.code = 'DSR'; + if (!parts[1] && parts[2] === '0' && !parts[3]) { - return this.emit('response', { - deviceStatus: 'OK' - }); + out.type = 'device-status'; + out.status = 'OK'; + + // LEGACY + out.deviceStatus = out.status; + + this.emit('response', out); + this.emit('response ' + out.event, out); + + return; } if (parts[1] && (parts[2] === '10' || parts[2] === '11') && !parts[3]) { - return this.emit('response', { - printerStatus: parts[2] === '10' - ? 'ready' - : 'not ready' - }); + out.type = 'printer-status'; + out.status = parts[2] === '10' + ? 'ready' + : 'not ready'; + + // LEGACY + out.printerStatus = out.status; + + this.emit('response', out); + this.emit('response ' + out.event, out); + + return; } if (parts[1] && (parts[2] === '20' || parts[2] === '21') && !parts[3]) { - return this.emit('response', { - UDKStatus: parts[2] === '20' - ? 'unlocked' - : 'locked' - }); + out.type = 'udk-status'; + out.status = parts[2] === '20' + ? 'unlocked' + : 'locked'; + + // LEGACY + out.UDKStatus = out.status; + + this.emit('response', out); + this.emit('response ' + out.event, out); + + return; } if (parts[1] @@ -694,22 +718,43 @@ Program.prototype._bindResponse = function(s) { && parts[3] === '1' && parts[4] === '0' && parts[5] === '0') { - return this.emit('response', { - keyboardStatus: 'OK' - }); + out.type = 'keyboard-status'; + out.status = 'OK'; + + // LEGACY + out.keyboardStatus = out.status; + + this.emit('response', out); + this.emit('response ' + out.event, out); + + return; } if (parts[1] && (parts[2] === '53' || parts[2] === '50') && !parts[3]) { - return this.emit('response', { - locator: parts[2] === '53' + out.type = 'locator-status'; + out.status = parts[2] === '53' ? 'available' - : 'unavailable' - }); + : 'unavailable'; + + // LEGACY + out.locator = out.status; + + this.emit('response', out); + this.emit('response ' + out.event, out); + + return; } - return this.emit('response', { - error: 'Unhandled: ' + JSON.stringify(parts) - }); + out.type = 'error'; + out.text = 'Unhandled: ' + JSON.stringify(parts); + + // LEGACY + out.error = out.text; + + this.emit('response', out); + this.emit('response ' + out.event, out); + + return; } // CSI Ps n Device Status Report (DSR). @@ -721,13 +766,27 @@ Program.prototype._bindResponse = function(s) { // 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)) { - return this.emit('response', { - cursor: { - x: +parts[3], - y: +parts[2], - page: !parts[1] ? undefined : 0 - } - }); + out.event = 'device-status-report'; + out.code = 'DSR'; + out.type = 'cursor-status'; + + out.status = { + x: +parts[3], + y: +parts[2], + page: !parts[1] ? undefined : 0 + }; + + out.x = out.status.x; + out.y = out.status.y; + out.page = out.status.page; + + // LEGACY + out.cursor = out.status; + + this.emit('response', out); + this.emit('response ' + out.event, out); + + return; } // CSI Ps ; Ps ; Ps t @@ -747,53 +806,99 @@ Program.prototype._bindResponse = function(s) { // Ps = 1 9 -> Report the size of the screen in characters. // Result is CSI 9 ; height ; width t if (parts = /^\x1b\[(\d+)(?:;(\d+);(\d+))?t/.exec(s)) { + out.event = 'window-manipulation'; + out.code = ''; + if ((parts[1] === '1' || parts[1] === '2') && !parts[2]) { - return this.emit('response', { - windowState: parts[1] === '1' - ? 'non-iconified' - : 'iconified' - }); + out.type = 'window-state'; + out.state = parts[1] === '1' + ? 'non-iconified' + : 'iconified' + + // LEGACY + out.windowState = out.state; + + this.emit('response', out); + this.emit('response ' + out.event, out); + + return; } if (parts[1] === '3' && parts[2]) { - return this.emit('response', { - windowPosition: { - x: +parts[2], - y: +parts[3] - } - }); + out.type = 'window-position'; + + out.position = { + x: +parts[2], + y: +parts[3] + }; + + // LEGACY + out.windowPosition = out.position; + + this.emit('response', out); + this.emit('response ' + out.event, out); + + return; } if (parts[1] === '4' && parts[2]) { - return this.emit('response', { - windowSizePixels: { - height: +parts[2], - width: +parts[3] - } - }); + out.type = 'window-size-pixels'; + out.size = { + height: +parts[2], + width: +parts[3] + }; + + // LEGACY + out.windowSizePixels = out.size; + + this.emit('response', out); + this.emit('response ' + out.event, out); + + return; } if (parts[1] === '8' && parts[2]) { - return this.emit('response', { - textAreaSizeCharacters: { - height: +parts[2], - width: +parts[3] - } - }); + out.type = 'textarea-size'; + out.size = { + height: +parts[2], + width: +parts[3] + }; + + // LEGACY + out.textAreaSizeCharacters = out.size; + + this.emit('response', out); + this.emit('response ' + out.event, out); + + return; } if (parts[1] === '9' && parts[2]) { - return this.emit('response', { - screenSizeCharacters: { - height: +parts[2], - width: +parts[3] - } - }); + out.type = 'screen-size'; + out.size = { + height: +parts[2], + width: +parts[3] + }; + + // LEGACY + out.screenSizeCharacters = out.size; + + this.emit('response', out); + this.emit('response ' + out.event, out); + + return; } - return this.emit('response', { - error: 'Unhandled: ' + JSON.stringify(parts) - }); + out.type = 'error'; + out.text = 'Unhandled: ' + JSON.stringify(parts); + + // LEGACY + out.error = out.text; + + this.emit('response', out); + this.emit('response ' + out.event, out); + + return; } // CSI Ps ; Ps ; Ps t @@ -806,21 +911,136 @@ Program.prototype._bindResponse = function(s) { // Ps = 2 1 -> Report xterm window's title. Result is OSC l // label ST if (parts = /^\x1b\](l|L)([^\x07\x1b]*)(?:\x07|\x1b\\)/.exec(s)) { + out.type = 'window-manipulation'; + out.code = ''; + if (parts[1] === 'L') { - return this.emit('response', { - windowIconLabel: parts[2] - }); + out.type = 'window-icon-label'; + out.text = parts[2]; + + // LEGACY + out.windowIconLabel = out.text; + + this.emit('response', out); + this.emit('response ' + out.event, out); + + return; } if (parts[1] === 'l') { - return this.emit('response', { - windowTitle: parts[2] - }); + out.type = 'window-title'; + out.text = parts[2]; + + // LEGACY + out.windowTitle = out.text; + + this.emit('response', out); + this.emit('response ' + out.event, out); + + return; } - return this.emit('response', { - error: 'Unhandled: ' + JSON.stringify(parts) + out.type = 'error'; + out.text = 'Unhandled: ' + JSON.stringify(parts); + + // LEGACY + out.error = out.text; + + this.emit('response', out); + this.emit('response ' + out.event, out); + + return; + } + + // CSI Ps ' | + // Request Locator Position (DECRQLP). + // -> CSI Pe ; Pb ; Pr ; Pc ; Pp & w + // Parameters are [event;button;row;column;page]. + // Valid values for the event: + // Pe = 0 -> locator unavailable - no other parameters sent. + // Pe = 1 -> request - xterm received a DECRQLP. + // Pe = 2 -> left button down. + // Pe = 3 -> left button up. + // Pe = 4 -> middle button down. + // Pe = 5 -> middle button up. + // Pe = 6 -> right button down. + // Pe = 7 -> right button up. + // Pe = 8 -> M4 button down. + // Pe = 9 -> M4 button up. + // Pe = 1 0 -> locator outside filter rectangle. + // ``button'' parameter is a bitmask indicating which buttons are + // pressed: + // Pb = 0 <- no buttons down. + // Pb & 1 <- right button down. + // Pb & 2 <- middle button down. + // Pb & 4 <- left button down. + // Pb & 8 <- M4 button down. + // ``row'' and ``column'' parameters are the coordinates of the + // locator position in the xterm window, encoded as ASCII deci- + // mal. + // The ``page'' parameter is not used by xterm, and will be omit- + // ted. + // NOTE: + // This is already implemented in the _bindMouse + // method, but it might make more sense here. + // The xterm mouse documentation says there is a + // `<` prefix, the DECRQLP says there is no prefix. + if (parts = /^\x1b\[(\d+(?:;\d+){4})&w/.exec(s)) { + parts = parts[1].split(';').map(function(ch) { + return +ch; }); + + out.event = 'locator-position'; + out.code = 'DECRQLP'; + + switch (parts[0]) { + case 0: + out.status = 'locator-unavailable'; + break; + case 1: + out.status = 'request'; + break; + case 2: + out.status = 'left-button-down'; + break; + case 3: + out.status = 'left-button-up'; + break; + case 4: + out.status = 'middle-button-down'; + break; + case 5: + out.status = 'middle-button-up'; + break; + case 6: + out.status = 'right-button-down'; + break; + case 7: + out.status = 'right-button-up'; + break; + case 8: + out.status = 'm4-button-down'; + break; + case 9: + out.status = 'm4-button-up'; + break; + case 10: + out.status = 'locator-outside'; + break; + } + + out.mask = parts[1]; + out.row = parts[2]; + out.col = parts[3]; + out.page = parts[4]; + + // LEGACY + out.locatorPosition = out; + + this.emit('response', out); + this.emit('response ' + out.event, out); + + return; } }; @@ -853,7 +1073,17 @@ Program.prototype.receive = function(text, callback) { }); }; -Program.prototype.receive_ = function(text, callback) { +Program.prototype.response_ = function(name, text, callback) { + if (arguments.length === 2) { + callback = text; + text = name; + name = null; + } + + if (!callback) { + callback = function() {}; + } + var listeners = (this._events && this._events['keypress']) || [] , bak = listeners.slice() , self = this; @@ -864,16 +1094,14 @@ Program.prototype.receive_ = function(text, callback) { 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.once('response' + (name ? ' ' + name : ''), function(event) { + if (event.type === 'error') { + return callback(new Error(event.event + ': ' + event.text)); + } + return callback(null, event); }); self._bindResponse(data); }); @@ -882,6 +1110,29 @@ Program.prototype.receive_ = function(text, callback) { }); }; +Program.prototype.response = function(name, text, callback) { + if (arguments.length === 2) { + callback = text; + text = name; + name = null; + } + + if (!callback) { + callback = function() {}; + } + + this.bindResponse(); + + this.once('response' + (name ? ' ' + name : ''), function(event) { + if (event.type === 'error') { + return callback(new Error(event.event + ': ' + event.text)); + } + return callback(null, event); + }); + + return this.write(text); +}; + Program.prototype.write = Program.prototype.echo = function(text, attr) { // if (this.output === process.stdout) { @@ -1286,7 +1537,7 @@ Program.prototype.setTitle = function(title) { // OSC Ps ; Pt BEL // Reset colors Program.prototype.resetColors = function(param) { - if (this.tput && this.tput.Cr) { + if (this.tput.has('Cr')) { return this.put.Cr(param); } return this.osc('112;' + param + '\x07'); @@ -1296,7 +1547,7 @@ Program.prototype.resetColors = function(param) { // OSC Ps ; Pt BEL // Change dynamic colors Program.prototype.dynamicColors = function(param) { - if (this.tput && this.tput.Cs) { + if (this.has('Cs')) { return this.put.Cs(param); } return this.osc('12;' + param + '\x07'); @@ -1306,7 +1557,7 @@ Program.prototype.dynamicColors = function(param) { // OSC Ps ; Pt BEL // Sel data Program.prototype.selData = function(a, b) { - if (this.tput && this.tput.Ms) { + if (this.has('Ms')) { return this.put.Ms(a, b); } return this.osc('52;' + a + ';' + b + '\x07'); @@ -1833,20 +2084,13 @@ Program.prototype.setBackground = function(color, val) { Program.prototype.dsr = Program.prototype.deviceStatus = function(param, callback, dec) { if (dec) { - return this.receive('\x1b[?' + (param || '0') + 'n', callback); + return this.response('device-status', '\x1b[?' + (param || '0') + 'n', callback); } - return this.receive('\x1b[' + (param || '0') + 'n', callback); + return this.response('device-status', '\x1b[' + (param || '0') + 'n', callback); }; Program.prototype.getCursor = function(callback) { - return this.deviceStatus('6', function(err, data) { - if (err) return callback(err); - data = data.slice(2, -1).split(';'); - return callback(null, { - x: +data[1], - y: +data[0] - }); - }); + return this.deviceStatus('6', callback); }; /** @@ -1994,13 +2238,7 @@ Program.prototype.HPositionRelative = function(param) { // vim responds with ^[[?0c or ^[[?1c after the terminal's response (?) Program.prototype.da = Program.prototype.sendDeviceAttributes = function(param, callback) { - return this.receive('\x1b[' + (param || '') + 'c', function(err, response) { - if (err) return callback(err); - if (response === '\x1b[?1;2c') { - return callback(null, 'VT100 with Advanced Video Option'); - } - return callback(null, 'Unknown'); - }); + return this.response('device-attributes', '\x1b[' + (param || '') + 'c', callback); }; // CSI Pm d @@ -2717,7 +2955,7 @@ Program.prototype.setCursorStyle = function(param) { break; } // extended tput.Se for param=2 - if (this.tput && this.tput.Ss) { + if (this.has('Ss')) { return this.put.Ss(param); } return this.write('\x1b[' + (param || 1) + ' q'); @@ -2811,18 +3049,11 @@ Program.prototype.manipulateWindow = function() { ? args.pop() : function() {}; - return this.receive('\x1b[' + args.join(';') + 't', callback); + return this.response('window-manipulation', '\x1b[' + args.join(';') + 't', callback); }; Program.prototype.getWindowSize = function(callback) { - return this.manipulateWindow('18', function(err, data) { - if (err) return callback(err); - data = data.slice(2, -1).split(';'); - return callback(null, { - height: +data[0], - width: +data[1] - }); - }); + return this.manipulateWindow('18', callback); }; // CSI Pt; Pl; Pb; Pr; Ps$ t @@ -3025,15 +3256,15 @@ Program.prototype.decrqlp = Program.prototype.req_mouse_pos = Program.prototype.reqmp = Program.prototype.requestLocatorPosition = function(params, callback) { - if (this.tput && this.tput.req_mouse_pos) { - // See also: - // get_mouse / getm / Gm - // mouse_info / minfo / Mi - // Correct for tput? + // See also: + // get_mouse / getm / Gm + // mouse_info / minfo / Mi + // Correct for tput? + if (this.has('req_mouse_pos')) { var code = this.tput.req_mouse_pos.apply(this.tput, params); - return this.receive(code, callback); + return this.response('locator-position', code, callback); } - return this.receive('\x1b[' + (param || '') + '\'|', callback); + return this.response('locator-position', '\x1b[' + (param || '') + '\'|', callback); }; // CSI P m SP } diff --git a/lib/widget.js b/lib/widget.js index 076d8c0..a8b4b5c 100644 --- a/lib/widget.js +++ b/lib/widget.js @@ -335,7 +335,7 @@ function Screen(options) { process.on('uncaughtException', function(err) { reset(); if (err) console.error(err.stack ? err.stack + '' : err + ''); - return process.exit(0); + return process.exit(1); }); process.on('exit', function() {