UNPKG

resume-client-socket.io

Version:

Resume Client API for Socket.IO and Node.JS - Medical Speech to Summarized Text

2,017 lines (1,697 loc) 120 kB
/** * program.js - basic curses-like functionality for blessed. * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License). * https://github.com/chjj/blessed */ /** * Modules */ var EventEmitter = require('events').EventEmitter , StringDecoder = require('string_decoder').StringDecoder , cp = require('child_process') , util = require('util') , fs = require('fs'); var Tput = require('./tput') , colors = require('./colors') , slice = Array.prototype.slice; var nextTick = global.setImmediate || process.nextTick.bind(process); /** * Program */ function Program(options) { var self = this; if (!(this instanceof Program)) { return new Program(options); } Program.bind(this); EventEmitter.call(this); if (!options || options.__proto__ !== Object.prototype) { options = { input: arguments[0], output: arguments[1] }; } this.options = options; this.input = options.input || process.stdin; this.output = options.output || process.stdout; options.log = options.log || options.dump; if (options.log) { this._logger = fs.createWriteStream(options.log); if (options.dump) this.setupDump(); } this.zero = options.zero !== false; this.useBuffer = options.buffer; this.x = 0; this.y = 0; this.savedX = 0; this.savedY = 0; this.cols = this.output.columns || 1; this.rows = this.output.rows || 1; this.scrollTop = 0; this.scrollBottom = this.rows - 1; this._terminal = options.terminal || options.term || process.env.TERM || (process.platform === 'win32' ? 'windows-ansi' : 'xterm'); this._terminal = this._terminal.toLowerCase(); // OSX this.isOSXTerm = process.env.TERM_PROGRAM === 'Apple_Terminal'; this.isiTerm2 = process.env.TERM_PROGRAM === 'iTerm.app' || !!process.env.ITERM_SESSION_ID; // VTE // NOTE: lxterminal does not provide an env variable to check for. // NOTE: gnome-terminal and sakura use a later version of VTE // which provides VTE_VERSION as well as supports SGR events. this.isXFCE = /xfce/i.test(process.env.COLORTERM); this.isTerminator = !!process.env.TERMINATOR_UUID; this.isLXDE = false; this.isVTE = !!process.env.VTE_VERSION || this.isXFCE || this.isTerminator || this.isLXDE; // xterm and rxvt - not accurate this.isRxvt = /rxvt/i.test(process.env.COLORTERM); this.isXterm = false; this.tmux = !!process.env.TMUX; this.tmuxVersion = (function() { if (!self.tmux) return 2; try { var version = cp.execFileSync('tmux', ['-V'], { encoding: 'utf8' }); return +/^tmux ([\d.]+)/i.exec(version.trim().split('\n')[0])[1]; } catch (e) { return 2; } })(); this._buf = ''; this._flush = this.flush.bind(this); if (options.tput !== false) { this.setupTput(); } this.listen(); } Program.global = null; Program.total = 0; Program.instances = []; Program.bind = function(program) { if (!Program.global) { Program.global = program; } if (!~Program.instances.indexOf(program)) { Program.instances.push(program); program.index = Program.total; Program.total++; } if (Program._bound) return; Program._bound = true; unshiftEvent(process, 'exit', Program._exitHandler = function() { Program.instances.forEach(function(program) { // Potentially reset window title on exit: // if (program._originalTitle) { // program.setTitle(program._originalTitle); // } // Ensure the buffer is flushed (it should // always be at this point, but who knows). program.flush(); // Ensure _exiting is set (could technically // use process._exiting). program._exiting = true; }); }); }; Program.prototype.__proto__ = EventEmitter.prototype; Program.prototype.type = 'program'; Program.prototype.log = function() { return this._log('LOG', util.format.apply(util, arguments)); }; Program.prototype.debug = function() { if (!this.options.debug) return; return this._log('DEBUG', util.format.apply(util, arguments)); }; Program.prototype._log = function(pre, msg) { if (!this._logger) return; return this._logger.write(pre + ': ' + msg + '\n-\n'); }; Program.prototype.setupDump = function() { var self = this , write = this.output.write , decoder = new StringDecoder('utf8'); function stringify(data) { return caret(data .replace(/\r/g, '\\r') .replace(/\n/g, '\\n') .replace(/\t/g, '\\t')) .replace(/[^ -~]/g, function(ch) { if (ch.charCodeAt(0) > 0xff) return ch; ch = ch.charCodeAt(0).toString(16); if (ch.length > 2) { if (ch.length < 4) ch = '0' + ch; return '\\u' + ch; } if (ch.length < 2) ch = '0' + ch; return '\\x' + ch; }); } function caret(data) { return data.replace(/[\0\x80\x1b-\x1f\x7f\x01-\x1a]/g, function(ch) { switch (ch) { case '\0': case '\200': ch = '@'; break; case '\x1b': ch = '['; break; case '\x1c': ch = '\\'; break; case '\x1d': ch = ']'; break; case '\x1e': ch = '^'; break; case '\x1f': ch = '_'; break; case '\x7f': ch = '?'; break; default: ch = ch.charCodeAt(0); // From ('A' - 64) to ('Z' - 64). if (ch >= 1 && ch <= 26) { ch = String.fromCharCode(ch + 64); } else { return String.fromCharCode(ch); } break; } return '^' + ch; }); } this.input.on('data', function(data) { self._log('IN', stringify(decoder.write(data))); }); this.output.write = function(data) { self._log('OUT', stringify(data)); return write.apply(this, arguments); }; }; Program.prototype.setupTput = function() { if (this._tputSetup) return; this._tputSetup = true; var self = this , options = this.options , write = this._write.bind(this); var tput = this.tput = new Tput({ terminal: this.terminal, padding: options.padding, extended: options.extended, printf: options.printf, termcap: options.termcap, forceUnicode: options.forceUnicode }); if (tput.error) { nextTick(function() { self.emit('warning', tput.error.message); }); } if (tput.padding) { nextTick(function() { self.emit('warning', 'Terminfo padding has been enabled.'); }); } this.put = function() { var args = slice.call(arguments) , cap = args.shift(); if (tput[cap]) { return this._write(tput[cap].apply(tput, args)); } }; Object.keys(tput).forEach(function(key) { if (self[key] == null) { self[key] = tput[key]; } if (typeof tput[key] !== 'function') { self.put[key] = tput[key]; return; } if (tput.padding) { self.put[key] = function() { return tput._print(tput[key].apply(tput, arguments), write); }; } else { self.put[key] = function() { return self._write(tput[key].apply(tput, arguments)); }; } }); }; Program.prototype.__defineGetter__('terminal', function() { return this._terminal; }); Program.prototype.__defineSetter__('terminal', function(terminal) { this.setTerminal(terminal); return this.terminal; }); Program.prototype.setTerminal = function(terminal) { this._terminal = terminal.toLowerCase(); delete this._tputSetup; this.setupTput(); }; Program.prototype.has = function(name) { return this.tput ? this.tput.has(name) : false; }; Program.prototype.term = function(is) { return this.terminal.indexOf(is) === 0; }; Program.prototype.listen = function() { var self = this; // Potentially reset window title on exit: // if (!this.isRxvt) { // if (!this.isVTE) this.setTitleModeFeature(3); // this.manipulateWindow(21, function(err, data) { // if (err) return; // self._originalTitle = data.text; // }); // } // Listen for keys/mouse on input if (!this.input._blessedInput) { this.input._blessedInput = 1; this._listenInput(); } else { this.input._blessedInput++; } this.on('newListener', this._newHandler = function fn(type) { if (type === 'keypress' || type === 'mouse') { self.removeListener('newListener', fn); if (self.input.setRawMode && !self.input.isRaw) { self.input.setRawMode(true); self.input.resume(); } } }); this.on('newListener', function fn(type) { if (type === 'mouse') { self.removeListener('newListener', fn); self.bindMouse(); } }); // Listen for resize on output if (!this.output._blessedOutput) { this.output._blessedOutput = 1; this._listenOutput(); } else { this.output._blessedOutput++; } }; Program.prototype._listenInput = function() { var keys = require('./keys') , self = this; // Input this.input.on('keypress', this.input._keypressHandler = function(ch, key) { key = key || { ch: ch }; if (key.name === 'undefined' && (key.code === '[M' || key.code === '[I' || key.code === '[O')) { // A mouse sequence. The `keys` module doesn't understand these. return; } if (key.name === 'undefined') { // Not sure what this is, but we should probably ignore it. return; } if (key.name === 'enter' && key.sequence === '\n') { key.name = 'linefeed'; } if (key.name === 'return' && key.sequence === '\r') { self.input.emit('keypress', ch, merge({}, key, { name: 'enter' })); } var name = (key.ctrl ? 'C-' : '') + (key.meta ? 'M-' : '') + (key.shift && key.name ? 'S-' : '') + (key.name || ch); key.full = name; Program.instances.forEach(function(program) { if (program.input !== self.input) return; program.emit('keypress', ch, key); program.emit('key ' + name, ch, key); }); }); this.input.on('data', this.input._dataHandler = function(data) { Program.instances.forEach(function(program) { if (program.input !== self.input) return; program.emit('data', data); }); }); keys.emitKeypressEvents(this.input); }; Program.prototype._listenOutput = function() { var self = this; if (!this.output.isTTY) { nextTick(function() { self.emit('warning', 'Output is not a TTY'); }); } // Output function resize() { Program.instances.forEach(function(program) { if (program.output !== self.output) return; program.cols = program.output.columns; program.rows = program.output.rows; program.emit('resize'); }); } this.output.on('resize', this.output._resizeHandler = function() { Program.instances.forEach(function(program) { if (program.output !== self.output) return; if (!program.options.resizeTimeout) { return resize(); } if (program._resizeTimer) { clearTimeout(program._resizeTimer); delete program._resizeTimer; } var time = typeof program.options.resizeTimeout === 'number' ? program.options.resizeTimeout : 300; program._resizeTimer = setTimeout(resize, time); }); }); }; Program.prototype.destroy = function() { var index = Program.instances.indexOf(this); if (~index) { Program.instances.splice(index, 1); Program.total--; this.flush(); this._exiting = true; Program.global = Program.instances[0]; if (Program.total === 0) { Program.global = null; process.removeListener('exit', Program._exitHandler); delete Program._exitHandler; delete Program._bound; } this.input._blessedInput--; this.output._blessedOutput--; if (this.input._blessedInput === 0) { this.input.removeListener('keypress', this.input._keypressHandler); this.input.removeListener('data', this.input._dataHandler); delete this.input._keypressHandler; delete this.input._dataHandler; if (this.input.setRawMode) { if (this.input.isRaw) { this.input.setRawMode(false); } if (!this.input.destroyed) { this.input.pause(); } } } if (this.output._blessedOutput === 0) { this.output.removeListener('resize', this.output._resizeHandler); delete this.output._resizeHandler; } this.removeListener('newListener', this._newHandler); delete this._newHandler; this.destroyed = true; this.emit('destroy'); } }; Program.prototype.key = function(key, listener) { if (typeof key === 'string') key = key.split(/\s*,\s*/); key.forEach(function(key) { return this.on('key ' + key, listener); }, this); }; Program.prototype.onceKey = function(key, listener) { if (typeof key === 'string') key = key.split(/\s*,\s*/); key.forEach(function(key) { return this.once('key ' + key, listener); }, this); }; Program.prototype.unkey = Program.prototype.removeKey = function(key, listener) { if (typeof key === 'string') key = key.split(/\s*,\s*/); key.forEach(function(key) { return this.removeListener('key ' + key, listener); }, this); }; // XTerm mouse events // http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking // To better understand these // the xterm code is very helpful: // Relevant files: // button.c, charproc.c, misc.c // Relevant functions in xterm/button.c: // BtnCode, EmitButtonCode, EditorButton, SendMousePosition // send a mouse event: // regular/utf8: ^[[M Cb Cx Cy // urxvt: ^[[ Cb ; Cx ; Cy M // sgr: ^[[ Cb ; Cx ; Cy M/m // vt300: ^[[ 24(1/3/5)~ [ Cx , Cy ] \r // locator: CSI P e ; P b ; P r ; P c ; P p & w // motion example of a left click: // ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7< // mouseup, mousedown, mousewheel // left click: ^[[M 3<^[[M#3< // mousewheel up: ^[[M`3> Program.prototype.bindMouse = function() { if (this._boundMouse) return; this._boundMouse = true; var decoder = new StringDecoder('utf8') , self = this; this.on('data', function(data) { var text = decoder.write(data); if (!text) return; self._bindMouse(text, data); }); }; Program.prototype._bindMouse = function(s, buf) { var self = this , key , parts , b , x , y , mod , params , down , page , button; key = { name: undefined, ctrl: false, meta: false, shift: false }; if (Buffer.isBuffer(s)) { if (s[0] > 127 && s[1] === undefined) { s[0] -= 128; s = '\x1b' + s.toString('utf-8'); } else { s = s.toString('utf-8'); } } // if (this.8bit) { // s = s.replace(/\233/g, '\x1b['); // buf = new Buffer(s, 'utf8'); // } // XTerm / X10 for buggy VTE // VTE can only send unsigned chars and no unicode for coords. This limits // them to 0xff. However, normally the x10 protocol does not allow a byte // under 0x20, but since VTE can have the bytes overflow, we can consider // bytes below 0x20 to be up to 0xff + 0x20. This gives a limit of 287. Since // characters ranging from 223 to 248 confuse javascript's utf parser, we // need to parse the raw binary. We can detect whether the terminal is using // a bugged VTE version by examining the coordinates and seeing whether they // are a value they would never otherwise be with a properly implemented x10 // protocol. This method of detecting VTE is only 99% reliable because we // can't check if the coords are 0x00 (255) since that is a valid x10 coord // technically. var bx = s.charCodeAt(4); var by = s.charCodeAt(5); if (buf[0] === 0x1b && buf[1] === 0x5b && buf[2] === 0x4d && (this.isVTE || bx >= 65533 || by >= 65533 || (bx > 0x00 && bx < 0x20) || (by > 0x00 && by < 0x20) || (buf[4] > 223 && buf[4] < 248 && buf.length === 6) || (buf[5] > 223 && buf[5] < 248 && buf.length === 6))) { b = buf[3]; x = buf[4]; y = buf[5]; // unsigned char overflow. if (x < 0x20) x += 0xff; if (y < 0x20) y += 0xff; // Convert the coordinates into a // properly formatted x10 utf8 sequence. s = '\x1b[M' + String.fromCharCode(b) + String.fromCharCode(x) + String.fromCharCode(y); } // XTerm / X10 if (parts = /^\x1b\[M([\x00\u0020-\uffff]{3})/.exec(s)) { b = parts[1].charCodeAt(0); x = parts[1].charCodeAt(1); y = parts[1].charCodeAt(2); key.name = 'mouse'; key.type = 'X10'; key.raw = [b, x, y, parts[0]]; key.buf = buf; key.x = x - 32; key.y = y - 32; if (this.zero) key.x--, key.y--; if (x === 0) key.x = 255; if (y === 0) key.y = 255; mod = b >> 2; key.shift = !!(mod & 1); key.meta = !!((mod >> 1) & 1); key.ctrl = !!((mod >> 2) & 1); b -= 32; if ((b >> 6) & 1) { key.action = b & 1 ? 'wheeldown' : 'wheelup'; key.button = 'middle'; } else if (b === 3) { // NOTE: x10 and urxvt have no way // of telling which button mouseup used. key.action = 'mouseup'; key.button = this._lastButton || 'unknown'; delete this._lastButton; } else { key.action = 'mousedown'; button = b & 3; key.button = button === 0 ? 'left' : button === 1 ? 'middle' : button === 2 ? 'right' : 'unknown'; this._lastButton = key.button; } // Probably a movement. // The *newer* VTE gets mouse movements comepletely wrong. // This presents a problem: older versions of VTE that get it right might // be confused by the second conditional in the if statement. // NOTE: Possibly just switch back to the if statement below. // none, shift, ctrl, alt // gnome: 32, 36, 48, 40 // xterm: 35, _, 51, _ // urxvt: 35, _, _, _ // if (key.action === 'mousedown' && key.button === 'unknown') { if (b === 35 || b === 39 || b === 51 || b === 43 || (this.isVTE && (b === 32 || b === 36 || b === 48 || b === 40))) { delete key.button; key.action = 'mousemove'; } self.emit('mouse', key); return; } // URxvt if (parts = /^\x1b\[(\d+;\d+;\d+)M/.exec(s)) { params = parts[1].split(';'); b = +params[0]; x = +params[1]; y = +params[2]; key.name = 'mouse'; key.type = 'urxvt'; key.raw = [b, x, y, parts[0]]; key.buf = buf; key.x = x; key.y = y; if (this.zero) key.x--, key.y--; mod = b >> 2; key.shift = !!(mod & 1); key.meta = !!((mod >> 1) & 1); key.ctrl = !!((mod >> 2) & 1); // XXX Bug in urxvt after wheelup/down on mousemove // NOTE: This may be different than 128/129 depending // on mod keys. if (b === 128 || b === 129) { b = 67; } b -= 32; if ((b >> 6) & 1) { key.action = b & 1 ? 'wheeldown' : 'wheelup'; key.button = 'middle'; } else if (b === 3) { // NOTE: x10 and urxvt have no way // of telling which button mouseup used. key.action = 'mouseup'; key.button = this._lastButton || 'unknown'; delete this._lastButton; } else { key.action = 'mousedown'; button = b & 3; key.button = button === 0 ? 'left' : button === 1 ? 'middle' : button === 2 ? 'right' : 'unknown'; // NOTE: 0/32 = mousemove, 32/64 = mousemove with left down // if ((b >> 1) === 32) this._lastButton = key.button; } // Probably a movement. // The *newer* VTE gets mouse movements comepletely wrong. // This presents a problem: older versions of VTE that get it right might // be confused by the second conditional in the if statement. // NOTE: Possibly just switch back to the if statement below. // none, shift, ctrl, alt // urxvt: 35, _, _, _ // gnome: 32, 36, 48, 40 // if (key.action === 'mousedown' && key.button === 'unknown') { if (b === 35 || b === 39 || b === 51 || b === 43 || (this.isVTE && (b === 32 || b === 36 || b === 48 || b === 40))) { delete key.button; key.action = 'mousemove'; } self.emit('mouse', key); return; } // SGR if (parts = /^\x1b\[<(\d+;\d+;\d+)([mM])/.exec(s)) { down = parts[2] === 'M'; params = parts[1].split(';'); b = +params[0]; x = +params[1]; y = +params[2]; key.name = 'mouse'; key.type = 'sgr'; key.raw = [b, x, y, parts[0]]; key.buf = buf; key.x = x; key.y = y; if (this.zero) key.x--, key.y--; mod = b >> 2; key.shift = !!(mod & 1); key.meta = !!((mod >> 1) & 1); key.ctrl = !!((mod >> 2) & 1); if ((b >> 6) & 1) { key.action = b & 1 ? 'wheeldown' : 'wheelup'; key.button = 'middle'; } else { key.action = down ? 'mousedown' : 'mouseup'; button = b & 3; key.button = button === 0 ? 'left' : button === 1 ? 'middle' : button === 2 ? 'right' : 'unknown'; } // Probably a movement. // The *newer* VTE gets mouse movements comepletely wrong. // This presents a problem: older versions of VTE that get it right might // be confused by the second conditional in the if statement. // NOTE: Possibly just switch back to the if statement below. // none, shift, ctrl, alt // xterm: 35, _, 51, _ // gnome: 32, 36, 48, 40 // if (key.action === 'mousedown' && key.button === 'unknown') { if (b === 35 || b === 39 || b === 51 || b === 43 || (this.isVTE && (b === 32 || b === 36 || b === 48 || b === 40))) { delete key.button; key.action = 'mousemove'; } self.emit('mouse', key); return; } // 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)) { params = parts[1].split(';'); b = +params[0]; x = +params[1]; y = +params[2]; page = +params[3]; key.name = 'mouse'; key.type = 'dec'; key.raw = [b, x, y, parts[0]]; key.buf = buf; key.x = x; key.y = y; key.page = page; if (this.zero) key.x--, key.y--; key.action = b === 3 ? 'mouseup' : 'mousedown'; key.button = b === 2 ? 'left' : b === 4 ? 'middle' : b === 6 ? 'right' : 'unknown'; self.emit('mouse', key); return; } // vt300 if (parts = /^\x1b\[24([0135])~\[(\d+),(\d+)\]\r/.exec(s)) { b = +parts[1]; x = +parts[2]; y = +parts[3]; key.name = 'mouse'; key.type = 'vt300'; key.raw = [b, x, y, parts[0]]; key.buf = buf; key.x = x; key.y = y; if (this.zero) key.x--, key.y--; key.action = 'mousedown'; key.button = b === 1 ? 'left' : b === 2 ? 'middle' : b === 5 ? 'right' : 'unknown'; self.emit('mouse', key); return; } if (parts = /^\x1b\[(O|I)/.exec(s)) { key.action = parts[1] === 'I' ? 'focus' : 'blur'; self.emit('mouse', key); self.emit(key.action); return; } }; // gpm support for linux vc Program.prototype.enableGpm = function() { var self = this; var gpmclient = require('./gpmclient'); if (this.gpm) return; this.gpm = gpmclient(); this.gpm.on('btndown', function(btn, modifier, x, y) { x--, y--; var key = { name: 'mouse', type: 'GPM', action: 'mousedown', button: self.gpm.ButtonName(btn), raw: [btn, modifier, x, y], x: x, y: y, shift: self.gpm.hasShiftKey(modifier), meta: self.gpm.hasMetaKey(modifier), ctrl: self.gpm.hasCtrlKey(modifier) }; self.emit('mouse', key); }); this.gpm.on('btnup', function(btn, modifier, x, y) { x--, y--; var key = { name: 'mouse', type: 'GPM', action: 'mouseup', button: self.gpm.ButtonName(btn), raw: [btn, modifier, x, y], x: x, y: y, shift: self.gpm.hasShiftKey(modifier), meta: self.gpm.hasMetaKey(modifier), ctrl: self.gpm.hasCtrlKey(modifier) }; self.emit('mouse', key); }); this.gpm.on('move', function(btn, modifier, x, y) { x--, y--; var key = { name: 'mouse', type: 'GPM', action: 'mousemove', button: self.gpm.ButtonName(btn), raw: [btn, modifier, x, y], x: x, y: y, shift: self.gpm.hasShiftKey(modifier), meta: self.gpm.hasMetaKey(modifier), ctrl: self.gpm.hasCtrlKey(modifier) }; self.emit('mouse', key); }); this.gpm.on('drag', function(btn, modifier, x, y) { x--, y--; var key = { name: 'mouse', type: 'GPM', action: 'mousemove', button: self.gpm.ButtonName(btn), raw: [btn, modifier, x, y], x: x, y: y, shift: self.gpm.hasShiftKey(modifier), meta: self.gpm.hasMetaKey(modifier), ctrl: self.gpm.hasCtrlKey(modifier) }; self.emit('mouse', key); }); this.gpm.on('mousewheel', function(btn, modifier, x, y, dx, dy) { var key = { name: 'mouse', type: 'GPM', action: dy > 0 ? 'wheelup' : 'wheeldown', button: self.gpm.ButtonName(btn), raw: [btn, modifier, x, y, dx, dy], x: x, y: y, shift: self.gpm.hasShiftKey(modifier), meta: self.gpm.hasMetaKey(modifier), ctrl: self.gpm.hasCtrlKey(modifier) }; self.emit('mouse', key); }); }; Program.prototype.disableGpm = function() { if (this.gpm) { this.gpm.stop(); delete this.gpm; } }; // All possible responses from the terminal Program.prototype.bindResponse = function() { if (this._boundResponse) return; this._boundResponse = true; var decoder = new StringDecoder('utf8') , self = this; this.on('data', function(data) { data = decoder.write(data); if (!data) return; self._bindResponse(data); }); }; Program.prototype._bindResponse = function(s) { var out = {} , parts; if (Buffer.isBuffer(s)) { if (s[0] > 127 && s[1] === undefined) { s[0] -= 128; s = '\x1b' + s.toString('utf-8'); } else { s = s.toString('utf-8'); } } // CSI P s c // Send Device Attributes (Primary DA). // CSI > P s c // Send Device Attributes (Secondary DA). if (parts = /^\x1b\[(\?|>)(\d*(?:;\d*)*)c/.exec(s)) { parts = parts[2].split(';').map(function(ch) { return +ch || 0; }); out.event = 'device-attributes'; out.code = 'DA'; if (parts[1] === '?') { out.type = 'primary-attribute'; // VT100-style params: if (parts[0] === 1 && parts[2] === 2) { out.term = 'vt100'; out.advancedVideo = true; } else if (parts[0] === 1 && parts[2] === 0) { out.term = 'vt101'; } else if (parts[0] === 6) { out.term = 'vt102'; } else if (parts[0] === 60 && parts[1] === 1 && parts[2] === 2 && parts[3] === 6 && parts[4] === 8 && parts[5] === 9 && parts[6] === 15) { out.term = 'vt220'; } else { // VT200-style params: parts.forEach(function(attr) { switch (attr) { case 1: out.cols132 = true; break; case 2: out.printer = true; break; case 6: out.selectiveErase = true; break; case 8: out.userDefinedKeys = true; break; case 9: out.nationalReplacementCharsets = true; break; case 15: out.technicalCharacters = true; break; case 18: out.userWindows = true; break; case 21: out.horizontalScrolling = true; break; case 22: out.ansiColor = true; break; case 29: out.ansiTextLocator = true; break; } }); } } else { out.type = 'secondary-attribute'; switch (parts[0]) { case 0: out.term = 'vt100'; break; case 1: out.term = 'vt220'; break; case 2: out.term = 'vt240'; break; case 18: out.term = 'vt330'; break; case 19: out.term = 'vt340'; break; case 24: out.term = 'vt320'; break; case 41: out.term = 'vt420'; break; case 61: out.term = 'vt510'; break; case 64: out.term = 'vt520'; break; case 65: out.term = 'vt525'; break; } out.firmwareVersion = parts[1]; out.romCartridgeRegistrationNumber = parts[2]; } // LEGACY out.deviceAttributes = out; this.emit('response', out); this.emit('response ' + out.event, out); return; } // CSI Ps n Device Status Report (DSR). // Ps = 5 -> Status Report. Result (``OK'') is // CSI 0 n // CSI ? Ps n // Device Status Report (DSR, DEC-specific). // 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. 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]) { 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]) { 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]) { 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] && parts[2] === '27' && parts[3] === '1' && parts[4] === '0' && parts[5] === '0') { 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]) { out.type = 'locator-status'; out.status = parts[2] === '53' ? 'available' : 'unavailable'; // LEGACY out.locator = out.status; this.emit('response', out); this.emit('response ' + out.event, out); return; } 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). // 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)) { out.event = 'device-status'; 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 // Window manipulation (from dtterm, as well as extensions). // These controls may be disabled using the allowWindowOps // resource. Valid values for the first (and any additional // parameters) are: // Ps = 1 1 -> Report xterm window state. If the xterm window // is open (non-iconified), it returns CSI 1 t . If the xterm // window is iconified, it returns CSI 2 t . // Ps = 1 3 -> Report xterm window position. Result is CSI 3 // ; x ; y t // Ps = 1 4 -> Report xterm window in pixels. Result is CSI // 4 ; height ; width t // Ps = 1 8 -> Report the size of the text area in characters. // Result is CSI 8 ; height ; width t // 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]) { 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]) { out.type = 'window-position'; out.position = { x: +parts[2], y: +parts[3] }; out.x = out.position.x; out.y = out.position.y; // LEGACY out.windowPosition = out.position; this.emit('response', out); this.emit('response ' + out.event, out); return; } if (parts[1] === '4' && parts[2]) { out.type = 'window-size-pixels'; out.size = { height: +parts[2], width: +parts[3] }; out.height = out.size.height; out.width = out.size.width; // LEGACY out.windowSizePixels = out.size; this.emit('response', out); this.emit('response ' + out.event, out); return; } if (parts[1] === '8' && parts[2]) { out.type = 'textarea-size'; out.size = { height: +parts[2], width: +parts[3] }; out.height = out.size.height; out.width = out.size.width; // LEGACY out.textAreaSizeCharacters = out.size; this.emit('response', out); this.emit('response ' + out.event, out); return; } if (parts[1] === '9' && parts[2]) { out.type = 'screen-size'; out.size = { height: +parts[2], width: +parts[3] }; out.height = out.size.height; out.width = out.size.width; // LEGACY out.screenSizeCharacters = out.size; this.emit('response', out); this.emit('response ' + out.event, out); return; } 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; } // rxvt-unicode does not support window manipulation // Result Normal: OSC l/L 0xEF 0xBF 0xBD // Result ASCII: OSC l/L 0x1c (file separator) // Result UTF8->ASCII: OSC l/L 0xFD // Test with: // echo -ne '\ePtmux;\e\e[>3t\e\\' // sleep 2 && echo -ne '\ePtmux;\e\e[21t\e\\' & cat -v // - // echo -ne '\e[>3t' // sleep 2 && echo -ne '\e[21t' & cat -v if (parts = /^\x1b\](l|L)([^\x07\x1b]*)$/.exec(s)) { parts[2] = 'rxvt'; s = '\x1b]' + parts[1] + parts[2] + '\x1b\\'; } // CSI Ps ; Ps ; Ps t // Window manipulation (from dtterm, as well as extensions). // These controls may be disabled using the allowWindowOps // resource. Valid values for the first (and any additional // parameters) are: // Ps = 2 0 -> Report xterm window's icon label. Result is // OSC L label ST // 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.event = 'window-manipulation'; out.code = ''; if (parts[1] === 'L') { 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') { out.type = 'window-title'; out.text = parts[2]; // LEGACY out.windowTitle = out.text; this.emit('response', out); this.emit('response ' + out.event, out); return; } 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; } // OSC Ps ; Pt BEL // OSC Ps ; Pt ST // Set Text Parameters if (parts = /^\x1b\](\d+);([^\x07\x1b]+)(?:\x07|\x1b\\)/.exec(s)) { out.event = 'text-params'; out.code = 'Set Text Parameters'; out.ps = +s[1]; out.pt = s[2]; this.emit('response', out); this.emit('response ' + out.event, out); } }; Program.prototype.response = function(name, text, callback, noBypass) { var self = this; if (arguments.length === 2) { callback = text; text = name; name = null; } if (!callback) { callback = function() {}; } this.bindResponse(); name = name ? 'response ' + name : 'response'; var onresponse; this.once(name, onresponse = function(event) { if (timeout) clearTimeout(timeout); if (event.type === 'error') { return callback(new Error(event.event + ': ' + event.text)); } return callback(null, event); }); var timeout = setTimeout(function() { self.removeListener(name, onresponse); return callback(new Error('Timeout.')); }, 2000); return noBypass ? this._write(text) : this._twrite(text); }; Program.prototype._owrite = Program.prototype.write = function(text) { if (!this.output.writable) return; return this.output.write(text); }; Program.prototype._buffer = function(text) { if (this._exiting) { this.flush(); this._owrite(text); return; } if (this._buf) { this._buf += text; return; } this._buf = text; nextTick(this._flush); return true; }; Program.prototype.flush = function() { if (!this._buf) return; this._owrite(this._buf); this._buf = ''; }; Program.prototype._write = function(text) { if (this.ret) return text; if (this.useBuffer) { return this._buffer(text); } return this._owrite(text); }; // Example: `DCS tmux; ESC Pt ST` // Real: `DCS tmux; ESC Pt ESC \` Program.prototype._twrite = function(data) { var self = this , iterations = 0 , timer; if (this.tmux) { // Replace all STs with BELs so they can be nested within the DCS code. data = data.replace(/\x1b\\/g, '\x07'); // Wrap in tmux forward DCS: data = '\x1bPtmux;\x1b' + data + '\x1b\\'; // If we've never even flushed yet, it means we're still in // the normal buffer. Wait for alt screen buffer. if (this.output.bytesWritten === 0) { timer = setInterval(function() { if (self.output.bytesWritten > 0 || ++iterations === 50) { clearInterval(timer); self.flush(); self._owrite(data); } }, 100); return true; } // NOTE: Flushing the buffer is required in some cases. // The DCS code must be at the start of the output. this.flush(); // Write out raw now that the buffer is flushed. return this._owrite(data); } return this._write(data); }; Program.prototype.echo = Program.prototype.print = function(text, attr) { return attr ? this._write(this.text(text, attr)) : this._write(text); }; Program.prototype._ncoords = function() { if (this.x < 0) this.x = 0; else if (this.x >= this.cols) this.x = this.cols - 1; if (this.y < 0) this.y = 0; else if (this.y >= this.rows) this.y = this.rows - 1; }; Program.prototype.setx = function(x) { return this.cursorCharAbsolute(x); // return this.charPosAbsolute(x); }; Program.prototype.sety = function(y) { return this.linePosAbsolute(y); }; Program.prototype.move = function(x, y) { return this.cursorPos(y, x); }; // TODO: Fix cud and cuu calls. Program.prototype.omove = function(x, y) { if (!this.zero) { x = (x || 1) - 1; y = (y || 1) - 1; } else { x = x || 0; y = y || 0; } if (y === this.y && x === this.x) { return; } if (y === this.y) { if (x > this.x) { this.cuf(x - this.x); } else if (x < this.x) { this.cub(this.x - x); } } else if (x === this.x) { if (y > this.y) { this.cud(y - this.y); } else if (y < this.y) { this.cuu(this.y - y); } } else { if (!this.zero) x++, y++; this.cup(y, x); } }; Program.prototype.rsetx = function(x) { // return this.HPositionRelative(x); if (!x) return; return x > 0 ? this.forward(x) : this.back(-x); }; Program.prototype.rsety = function(y) { // return this.VPositionRelative(y); if (!y) return; return y > 0 ? this.up(y) : this.down(-y); }; Program.prototype.rmove = function(x, y) { this.rsetx(x); this.rsety(y); }; Program.prototype.simpleInsert = function(ch, i, attr) { return this._write(this.repeat(ch, i), attr); }; Program.prototype.repeat = function(ch, i) { if (!i || i < 0) i = 0; return Array(i + 1).join(ch); }; Program.prototype.__defineGetter__('title', function() { return this._title; }); Program.prototype.__defineSetter__('title', function(title) { this.setTitle(title); return this._title; }); // Specific to iTerm2, but I think it's really cool. // Example: // if (!screen.copyToClipboard(text)) { // execClipboardProgram(text); // } Program.prototype.copyToClipboard = function(text) { if (this.isiTerm2) { this._twrite('\x1b]50;CopyToCliboard=' + text + '\x07'); return true; } return false; }; // Only XTerm and iTerm2. If you know of any others, post them. Program.prototype.cursorShape = function(shape, blink) { if (this.isiTerm2) { switch (shape) { case 'block': if (!blink) { this._twrite('\x1b]50;CursorShape=0;BlinkingCursorEnabled=0\x07'); } else { this._twrite('\x1b]50;CursorShape=0;BlinkingCursorEnabled=1\x07'); } break; case 'underline': if (!blink) { // this._twrite('\x1b]50;CursorShape=n;BlinkingCursorEnabled=0\x07'); } else { // this._twrite('\x1b]50;CursorShape=n;BlinkingCursorEnabled=1\x07'); } break; case 'line': if (!blink) { this._twrite('\x1b]50;CursorShape=1;BlinkingCursorEnabled=0\x07'); } else { this._twrite('\x1b]50;CursorShape=1;BlinkingCursorEnabled=1\x07'); } break; } return true; } else if (this.term('xterm') || this.term('screen')) { switch (shape) { case 'block': if (!blink) { this._twrite('\x1b[0 q'); } else { this._twrite('\x1b[1 q'); } break; case 'underline': if (!blink) { this._twrite('\x1b[2 q'); } else { this._twrite('\x1b[3 q'); } break; case 'line': if (!blink) { this._twrite('\x1b[4 q'); } else { this._twrite('\x1b[5 q'); } break; } return true; } return false; }; Program.prototype.cursorColor = function(color) { if (this.term('xterm') || this.term('rxvt') || this.term('screen')) { this._twrite('\x1b]12;' + color + '\x07'); return true; } return false; }; Program.prototype.cursorReset = Program.prototype.resetCursor = function() { if (this.term('xterm') || this.term('rxvt') || this.term('screen')) { // XXX // return this.resetColors(); this._twrite('\x1b[0 q'); this._twrite('\x1b]112\x07'); // urxvt doesnt support OSC 112 this._twrite('\x1b]12;white\x07'); return true; } return false; }; Program.prototype.getTextParams = function(param, callback) { return this.response('text-params', '\x1b]' + param + ';?\x07', function(err, data) { if (err) return callback(err); return callback(null, data.pt); }); }; Program.prototype.getCursorColor = function(callback) { return this.getTextParams(12, callback); }; /** * Normal */ //Program.prototype.pad = Program.prototype.nul = function() { //if (this.has('pad')) return this.put.pad(); return this._write('\200'); }; Program.prototype.bel = Program.prototype.bell = function() { if (this.has('bel')) return this.put.bel(); return this._write('\x07'); }; Program.prototype.vtab = function() { this.y++; this._ncoords(); return this._write('\x0b'); }; Program.prototype.ff = Program.prototype.form = function() { if (this.has('ff')) return this.put.ff(); return this._write('\x0c'); }; Program.prototype.kbs = Program.prototype.backspace = function() { this.x--; this._ncoords(); if (this.has('kbs')) return this.put.kbs(); return this._write('\x08'); }; Program.prototype.ht = Program.prototype.tab = function() { this.x += 8; this._ncoords(); if (this.has('ht')) return this.put.ht(); return this._write('\t'); }; Program.prototype.shiftOut = function() { // if (this.has('S2')) return this.put.S2(); return this._write('\x0e'); }; Program.prototype.shiftIn = function() { // if (this.has('S3')) return this.put.S3(); return this._write('\x0f'); }; Program.prototype.cr = Program.prototype.return = function() { this.x = 0; if (this.has('cr')) return this.put.cr(); return this._write('\r'); }; Program.prototype.nel = Program.prototype.newline = Program.prototype.feed = function() { if (this.tput && this.tput.bools.eat_newline_glitch && this.x >= this.cols) { return; } this.x = 0; this.y++; this._ncoords(); if (this.has('nel')) return this.put.nel(); return this._write('\n'); }; /** * Esc */ // ESC D Index (IND is 0x84). Program.prototype.ind = Program.prototype.index = function() { this.y++; this._ncoords(); if (this.tput) return this.put.ind(); return this._write('\x1bD'); }; // ESC M Reverse Index (RI is 0x8d). Program.prototype.ri = Program.prototype.reverse = Program.prototype.reverseIndex = function() { this.y--; this._ncoords(); if (this.tput) return this.put.ri(); return this._write('\x1bM'); }; // ESC E Next Line (NEL is 0x85). Program.prototype.nextLine = function() { this.y++; this.x = 0; this._ncoords(); if (this.has('nel')) return this.put.nel(); return this._write('\x1bE'); }; // ESC c Full Reset (RIS). Program.prototype.reset = function() { this.x = this.y = 0; if (this.has('rs1') || this.has('ris')) { return this