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
JavaScript
/**
* 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