vt
Version:
terminal emulation library for javascript.
634 lines (552 loc) • 14.8 kB
JavaScript
var myUtil = require('./util');
var inherits = require('util').inherits;
function setterFor(objName) {
return function(name, value) {
if("_"+objName+"sCow" in this) {
if(this["_"+objName+"sCow"] === true)
this["_"+objName+"s"] = myUtil.extend({}, this["_"+objName+"s"]);
this["_"+objName+"sCow"] = false;
}
var obj = this["_"+objName+"s"];
if(!(name in obj))
throw new Error("Unknown "+objName+" `"+name+"`");
this.emit(objName+"change", name, value, obj[name]);
obj[name] = value;
};
}
function TermBuffer(width, height, attr) {
TermBuffer.super_.call(this);
this.height = height || 24;
this.width = width || 80;
this._defaultAttr = myUtil.extend({
fg: null,
bg: null,
bold: false,
underline: false,
italic: false,
blink: false,
inverse: false
}, attr || {});
this._attributesCow = true;
this._tabs = [];
this.on('newListener', this._newListener);
this.on('removeListener', this._removeListener);
// Reset all on first use
this.reset();
}
inherits(TermBuffer, require('events').EventEmitter);
module.exports = TermBuffer;
TermBuffer.prototype._newListener = function(ev, cb) {
var i;
switch(ev) {
case 'lineinsert':
for(i = 0; i < this.getBufferHeight(); i++)
cb.call(this, i, this.getLine(i));
break;
case 'resize':
cb.call(this, this.width, this.height);
break;
case 'cursormove':
cb.call(this, this.cursor.x, this.cursor.y);
break;
}
};
TermBuffer.prototype._removeListener = function(ev, cb) {
var i;
if(ev == 'lineremove') {
for(i = 0; i < this.getBufferHeight(); i++)
cb.call(this, 0);
}
};
TermBuffer.prototype.reset = function() {
if(this._buffer)
this._removeLine(0, this.getBufferHeight());
this._buffer = this._defBuffer = {
str: [], attr: []
};
this._altBuffer = {
str: [], attr: []
};
this._modes = {
cursor: true,
cursorBlink: false,
appKeypad: false,
wrap: true,
insert: false,
crlf: false,
mousebtn: false,
mousemtn: false,
reverse: false,
graphic: false
};
this._metas = {
title: "",
icon: ""
};
this.resetAttribute();
this.cursor = {x:0,y:0};
this._savedCursor = {x:0,y:0};
this._scrollRegion = [0, this.height-1];
this._leds = [!!0,!!0,!!0,!!0];
this._lineAttr = {
doubletop: false,
doublebottom: false,
doublewidth: false
};
};
TermBuffer.prototype._createLine = function(line) {
if(line === undefined || typeof line === 'string')
line = { str: line || "", attr: {0: this._defaultAttr} };
else if(!line || typeof line.str !== 'string' || typeof line.attr !== 'object')
throw new Error('line objects must contain attr and str' + line);
for(var i in line.attr) {
if(+i > line.str.length || line.attr[i] === undefined)
delete line.attr[i];
}
return line;
};
TermBuffer.prototype.inject = function(str) {
var i, j, line;
var lines = str.split('\n');
var c = this.cursor, cx;
for(i = 0; i < lines.length; i++) {
cx = null;
// Carriage Return
line = lines[i].split('\r');
lines[i] = line[0];
for(j = 1; j < line.length && lines[i].length < this.width; j++) {
lines[i] = line[j] + lines[i].substr(line[j].length);
cx = line[j].length;
}
// Handle long lines
if(lines[i].length > this.width - c.x && lines[i].length > 0) {
if(c.x >= this.width)
c.x = this.width - 1;
if(this._modes.wrap)
lines.splice(i, 1,
lines[i].substr(0, this.width - c.x),
lines[i].substr(this.width - c.x)
);
else {
lines[i] = lines[i].substr(0, this.width - c.x - 1) +
lines[i].substr(-1);
}
}
// write line
this._lineInject(lines[i]);
if(i + 1 !== lines.length) {
c.y++;
if(this._modes.crlf)
c.x = 0;
if(c.y > this._scrollRegion[1]) {
c.y--;
this._removeLine(this._scrollRegion[0]);
this._insertLine(this._scrollRegion[1]);
}
}
if(cx !== null)
c.x = cx;
}
return this.setCursor();
};
var graphics = {
'`': '\u25C6',
'a': '\u2592',
'b': '\u2409',
'c': '\u240C',
'd': '\u240D',
'e': '\u240A',
'f': '\u00B0',
'g': '\u00B1',
'h': '\u2424',
'i': '\u240B',
'j': '\u2518',
'k': '\u2510',
'l': '\u250C',
'm': '\u2514',
'n': '\u253C',
'o': '\u23BA',
'p': '\u23BB',
'q': '\u2500',
'r': '\u23BC',
's': '\u23BD',
't': '\u251C',
'u': '\u2524',
'v': '\u2534',
'w': '\u252C',
'x': '\u2502',
'y': '\u2264',
'z': '\u2265',
'{': '\u03C0',
'|': '\u2260',
'}': '\u00A3',
'~': '\u00B7'
};
TermBuffer.prototype._graphConvert = function(content) {
if(this._modes.graphic) {
var result = "";
for(i = 0; i < content.length; i++) {
result += (content[i] in graphics) ?
graphics[content[i]] :
content[i];
}
return result;
} else {
return content;
}
};
TermBuffer.prototype._lineInject = function(content) {
var c = this.cursor;
var line = this.getLine();
if(this._modes.insert) {
var args = Array(content.length);
args.unshift(line.attr, line.str.length+1, c.x, 0);
myUtil.objSplice.apply(0, args);
line.str = line.str.substr(0, c.x) + myUtil.repeat(' ',c.x - line.str.length) +
this._graphConvert(content) + line.str.substr(c.x);
line.str = line.str.substr(0, this.width);
}
else {
line.str = line.str.substr(0, c.x) +
myUtil.repeat(' ', c.x - line.str.length) +
this._graphConvert(content) + line.str.substr(c.x + content.length);
}
this._applyAttributes(line, c.x, content.length);
this.setLine(line);
c.x += content.length;
};
TermBuffer.prototype.removeChar = function(count) {
var c = this.cursor, line = this.getLine(c.y);
var last = line.attr[line.str.length];
myUtil.objSplice(line.attr, line.str.length+1, c.x, count);
line.str = line.str.substr(0, c.x) + line.str.substr(c.x+count);
line.attr[line.str.length] = last;
this.setLine(c.y, line);
};
TermBuffer.prototype.insertBlank = function(count) {
var c = this.cursor, line = this.getLine(c.y);
var last = line.attr[line.str.length];
myUtil.objSplice(line.attr, line.str.length+1, c.x, 0, Array(count));
line.str = line.str.substr(0, c.x) +
myUtil.repeat(' ', count) + line.str.substr(c.x+count);
line.attr[line.str.length] = last;
this.setLine(c.y, line);
};
TermBuffer.prototype.removeLine = function(count) {
this._removeLine(this.cursor.y, +count);
if(this._scrollRegion[1] !== this.height-1 && this.cursor.y <= this._scrollRegion[1])
this._insertLine(this._scrollRegion[1] + 1 - count, +count);
};
TermBuffer.prototype._removeLine = function(line, count) {
var i;
if(count === undefined)
count = 1;
count = this._buffer.str.splice(line, count).length;
this._buffer.attr.splice(line, count);
for(i = 0; i < count; i++)
this.emit('lineremove', line);
return count;
};
TermBuffer.prototype.setLine = function(nbr, line) {
if(typeof nbr === 'object' && line === undefined) {
line = nbr;
nbr = this.cursor.y;
}
line = this._createLine(line);
if(this._buffer.str.length <= nbr) {
this._insertLine(nbr, line);
}
else {
if(line.str.length > this.width)
line.attr[this.width] = line.attr[line.str.length];
this._buffer.str[nbr] = line.str.substr(0, this.width);
this._buffer.attr[nbr] = line.attr;
this.emit('linechange', nbr, line);
}
};
TermBuffer.prototype.insertLine = function(count) {
this._insertLine(this.cursor.y, +count);
};
TermBuffer.prototype._insertLine = function(nbr, line) {
var h = this.getBufferHeight();
var start = Math.min(h, nbr);
var end = nbr + 1;
if(typeof line === 'number') {
end = nbr + line;
line = undefined;
}
for(i = start; i < end; i++) {
if(this.height === this.getBufferHeight())
this._removeLine(this._scrollRegion[1], 1);
line = this._createLine(line);
this._buffer.str.splice(start, 0, line.str);
this._buffer.attr.splice(start, 0, line.attr);
this.emit('lineinsert', start, line);
line = undefined;
}
};
TermBuffer.prototype._applyAttributes = function(line, index, len) {
var i, prev;
for(i = index+len; i > 0 && line.attr[i] === undefined; i--);
prev = line.attr[i];
for(i = index; i < index+len; i++)
delete line.attr[i];
line.attr[index] = this._attributes;
if(index + len <= this.width)
line.attr[index + len] = prev;
this._attributesCow = true;
return this;
};
TermBuffer.prototype.setCursor = function(x, y) {
var c = this.cursor, oldX, oldY, line;
if(typeof x !== 'number')
x = c.x;
if(typeof y !== 'number')
y = c.y;
if(x < 0)
x = 0;
else if(x > this.width)
x = this.width;
if(y < 0)
y = 0;
else if(y >= this.height)
y = this.height - 1;
if(c.x != x || c.y != y || arguments.length === 0) {
oldX = c.x;
oldY = c.y;
c.x = x;
c.y = y;
this.emit('cursormove', x, y);
}
return this;
};
TermBuffer.prototype.resize = function(width, height) {
var line;
this._removeLine(0, Math.max(0, this.height - height));
this.height = height;
this.width = width;
for(var i = 0; i < this._buffer.str.length; i++)
this.setLine(i, this.getLine(i));
this.setScrollRegion(0, this.height-1);
this.emit('resize', width, height);
this.setCursor();
return this;
};
TermBuffer.prototype.mvCursor = function(x, y) {
if(x || y)
this.setCursor(this.cursor.x + x, this.cursor.y + y);
return this;
};
TermBuffer.prototype.scroll = function(scroll) {
// positive: down; negative: up
var i;
var count = Math.min(Math.abs(scroll), this._scrollRegion[1] - this._scrollRegion[0]);
if(scroll > 0) {
this._removeLine(this._scrollRegion[0], count);
for(i = 0; i < count; i++) {
this._insertLine(this._scrollRegion[1] +1 - count);
}
}
else {
this._removeLine(this._scrollRegion[1] +1 -count, count);
for(i = 0; i < count; i++) {
this._insertLine(this._scrollRegion[0]);
}
}
};
TermBuffer.prototype.toString = function() {
return this._buffer.str.join('\n');
};
TermBuffer.prototype.prevLine = function() {
if(this.cursor.y == this._scrollRegion[0])
this.scroll(-1);
else
this.mvCursor(0, -1);
return this;
};
TermBuffer.prototype.nextLine = function() {
if(this.cursor.y == this._scrollRegion[1])
this.scroll(1);
else
this.mvCursor(0, 1);
return this;
};
TermBuffer.prototype.resetAttribute = function(name) {
if(name)
this.setAttribute(name, this._defaultAttr[name]);
else {
this._attributesCow = true;
this._attributes = this._defaultAttr;
}
return this;
};
TermBuffer.prototype.saveCursor = function() {
this._savedCursor.x = this.cursor.x;
this._savedCursor.y = this.cursor.y;
return this;
};
TermBuffer.prototype.restoreCursor = function() {
return this.setCursor(this._savedCursor.x, this._savedCursor.y);
};
TermBuffer.prototype.eraseCharacters = function(count) {
var c = this.cursor, line = this.getLine(c.y);
line.str = line.str.substr(0, c.x) + myUtil.repeat(' ', count) +
line.str.substr(c.x + count);
line.str = line.str.substr(0, this.width);
this._applyAttributes(line, c.x, count);
this.setLine(c.y, line);
};
TermBuffer.prototype.eraseInDisplay = function(n) {
var c = this.cursor, i, line, self = this;
var chLine = function() {
line = self._createLine();
self._applyAttributes(line, 0, self.width);
self.setLine(i, line);
};
switch(n || 0) {
case 'below':
case 0:
n = 0;
for(i = c.y+1; i < this.height; i++)
chLine();
break;
case 'above':
case 1:
n = 1;
for(i = 0; i < c.y-1; i++)
chLine();
break;
case 'all':
case 2:
for(i = 0; i < this.height; i++)
chLine();
return this;
}
return this.eraseInLine(n);
};
TermBuffer.prototype.eraseInLine = function(n) {
var c = this.cursor;
var line = this.getLine();
switch(n || 0) {
case 'toRight':
case 0:
line.str = line.str.substr(0, c.x);
this._applyAttributes(line, c.x, this.width);
break;
case 'toLeft':
case 1:
line.str = myUtil.repeat(' ',c.x) + line.str.substr(c.x);
this._applyAttributes(line, 0, c.x);
break;
case 'all':
case 2:
l = this._createLine();
break;
}
this.setLine(c.y, line);
return this;
};
TermBuffer.prototype.setScrollRegion = function(n, m) {
this._scrollRegion[0] = +n;
this._scrollRegion[1] = +m;
return this;
};
TermBuffer.prototype.switchBuffer = function(alt) {
var i;
var active, inactive;
if(alt) {
active = this._altBuffer;
inactive = this._defBuffer;
}
else {
active = this._defBuffer;
inactive = this._altBuffer;
}
if(active === this._buffer)
return;
for(i = active.length; i < inactive.length; i++)
this.emit('lineremove', active.length, this.getLine(i));
this._buffer = active;
for(i = 0; i < active.length && i < inactive.length; i++)
this.emit('linechange', active.length, this.getLine(i));
for(; i < active.length; i++)
this.emit('lineinsert', i, this.getLine(i));
return this;
};
TermBuffer.prototype.setLed = function(led, value) {
var l = this._leds;
if (led < 4) { // we only have 4 leds (0,1,2,3)
this._leds[led] = (value || true);
this.emit('ledchange', l[0], l[1], l[2], l[3]);
}
return this;
};
TermBuffer.prototype.getBufferHeight = function() {
return this._buffer.str.length;
};
TermBuffer.prototype.getLed = function(n) {
return this._leds[n];
};
TermBuffer.prototype.getLine = function(n) {
if(n === undefined)
n = this.cursor.y;
if(this._buffer.str[n])
return {
str: this._buffer.str[n],
attr: this._buffer.attr[n]
};
else
return this._createLine();
};
TermBuffer.prototype.getMode = function(n) {
return this._modes[n];
};
TermBuffer.prototype.mvTab = function(n) {
var x = this.cursor.x;
var tabMax = this._tabs[this._tabs.length - 1] || 0;
var positive = n > 0;
n = Math.abs(n);
while(n !== 0 && x > 0 && x < this.width-1) {
x += positive ? 1 : -1;
// TODO: indexOf is not supported by IE
if(~myUtil.indexOf(this._tabs, x) || (x > tabMax && x % 8 === 0))
n--;
}
this.setCursor(x);
};
TermBuffer.prototype.setTab = function(pos) {
// Set the default to current cursor if no tab position is specified
if(pos === undefined) {
pos = this.cursor.x;
}
// Only add the tab position if it is not there already
if (~myUtil.indexOf(this._tabs, pos)) {
this._tabs.push(pos);
this._tabs.sort();
}
};
TermBuffer.prototype.removeTab = function(pos) {
var i, tabs = this._tabs;
for(i = 0; i < tabs.length && tabs[i] !== pos; i++);
tabs.splice(index,1);
};
TermBuffer.prototype.tabClear = function(n) {
switch(n || 'current') {
case 'current':
case 0:
for(var i = this._tabs.length - 1; i >= 0; i--) {
if(this._tabs[i] < this.cursor.x) {
this._tabs.splice(i, 1);
break;
}
}
break;
case 'all':
case 3:
this._tabs = [];
break;
}
};
TermBuffer.prototype.setAttribute = setterFor("attribute");
TermBuffer.prototype.setMode = setterFor("mode");
TermBuffer.prototype.setMeta = setterFor("meta");