diff --git a/lib/helpers.js b/lib/helpers.js
new file mode 100644
index 0000000..81dcdac
--- /dev/null
+++ b/lib/helpers.js
@@ -0,0 +1,151 @@
+/**
+ * helpers.js - helpers for blessed
+ * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
+ * https://github.com/chjj/blessed
+ */
+
+/**
+ * Modules
+ */
+
+var fs = require('fs');
+
+var unicode = require('./unicode');
+
+/**
+ * Helpers
+ */
+
+var helpers = exports;
+
+helpers.merge = function(a, b) {
+ Object.keys(b).forEach(function(key) {
+ a[key] = b[key];
+ });
+ return a;
+};
+
+helpers.asort = function(obj) {
+ return obj.sort(function(a, b) {
+ a = a.name.toLowerCase();
+ b = b.name.toLowerCase();
+
+ if (a[0] === '.' && b[0] === '.') {
+ a = a[1];
+ b = b[1];
+ } else {
+ a = a[0];
+ b = b[0];
+ }
+
+ return a > b ? 1 : (a < b ? -1 : 0);
+ });
+};
+
+helpers.hsort = function(obj) {
+ return obj.sort(function(a, b) {
+ return b.index - a.index;
+ });
+};
+
+helpers.findFile = function(start, target) {
+ return (function read(dir) {
+ var files, file, stat, out;
+
+ if (dir === '/dev' || dir === '/sys'
+ || dir === '/proc' || dir === '/net') {
+ return null;
+ }
+
+ try {
+ files = fs.readdirSync(dir);
+ } catch (e) {
+ files = [];
+ }
+
+ for (var i = 0; i < files.length; i++) {
+ file = files[i];
+
+ if (file === target) {
+ return (dir === '/' ? '' : dir) + '/' + file;
+ }
+
+ try {
+ stat = fs.lstatSync((dir === '/' ? '' : dir) + '/' + file);
+ } catch (e) {
+ stat = null;
+ }
+
+ if (stat && stat.isDirectory() && !stat.isSymbolicLink()) {
+ out = read((dir === '/' ? '' : dir) + '/' + file);
+ if (out) return out;
+ }
+ }
+
+ return null;
+ })(start);
+};
+
+// Escape text for tag-enabled elements.
+helpers.escape = function(text) {
+ return text.replace(/[{}]/g, function(ch) {
+ return ch === '{' ? '{open}' : '{close}';
+ });
+};
+
+helpers.parseTags = function(text) {
+ return Element.prototype._parseTags.call(
+ { parseTags: true, screen: Screen.global }, text);
+};
+
+helpers.generateTags = function(style, text) {
+ var open = ''
+ , close = '';
+
+ Object.keys(style || {}).forEach(function(key) {
+ var val = style[key];
+ if (typeof val === 'string') {
+ val = val.replace(/^light(?!-)/, 'light-');
+ val = val.replace(/^bright(?!-)/, 'bright-');
+ open = '{' + val + '-' + key + '}' + open;
+ close += '{/' + val + '-' + key + '}';
+ } else {
+ if (val === true) {
+ open = '{' + key + '}' + open;
+ close += '{/' + key + '}';
+ }
+ }
+ });
+
+ if (text != null) {
+ return open + text + close;
+ }
+
+ return {
+ open: open,
+ close: close
+ };
+};
+
+helpers.attrToBinary = function(style, element) {
+ return Element.prototype.sattr.call(element || {}, style);
+};
+
+helpers.stripTags = function(text) {
+ if (!text) return '';
+ return text
+ .replace(/{(\/?)([\w\-,;!#]*)}/g, '')
+ .replace(/\x1b\[[\d;]*m/g, '');
+};
+
+helpers.cleanTags = function(text) {
+ return helpers.stripTags(text).trim();
+};
+
+helpers.dropUnicode = function(text) {
+ if (!text) return '';
+ return text
+ .replace(unicode.chars.all, '??')
+ .replace(unicode.chars.combining, '')
+ .replace(unicode.chars.surrogate, '?');
+};
diff --git a/lib/widget.js b/lib/widget.js
index 9abcf45..46449bb 100644
--- a/lib/widget.js
+++ b/lib/widget.js
@@ -4,9699 +4,38 @@
* https://github.com/chjj/blessed
*/
-/**
- * Modules
- */
-
-var EventEmitter = require('./events').EventEmitter
- , assert = require('assert')
- , path = require('path')
- , util = require('util')
- , fs = require('fs')
- , cp = require('child_process');
-
-var colors = require('./colors')
- , program = require('./program')
- , unicode = require('./unicode')
- , widget = exports;
-
-var nextTick = global.setImmediate || process.nextTick.bind(process);
-
-/**
- * Node
- */
-
-function Node(options) {
- if (!(this instanceof Node)) {
- return new Node(options);
- }
-
- EventEmitter.call(this);
-
- options = options || {};
- this.options = options;
- this.screen = this.screen
- || options.screen
- || Screen.global
- || (function(){throw new Error('No active screen.')})();
- this.parent = options.parent || null;
- this.children = [];
- this.$ = this._ = this.data = {};
- this.uid = Node.uid++;
- this.index = -1;
-
- if (this.type !== 'screen') {
- this.detached = true;
- }
-
- if (this.parent) {
- this.parent.append(this);
- }
-
- (options.children || []).forEach(this.append.bind(this));
-}
-
-Node.uid = 0;
-
-Node.prototype.__proto__ = EventEmitter.prototype;
-
-Node.prototype.type = 'node';
-
-Node.prototype.insert = function(element, i) {
- var self = this;
-
- element.detach();
- element.parent = this;
-
- if (i === 0) {
- this.children.unshift(element);
- } else if (i === this.children.length) {
- this.children.push(element);
- } else {
- this.children.splice(i, 0, element);
- }
-
- element.emit('reparent', this);
- this.emit('adopt', element);
-
- (function emit(el) {
- var n = el.detached !== self.detached;
- el.detached = self.detached;
- if (n) el.emit('attach');
- el.children.forEach(emit);
- })(element);
-
- if (!this.screen.focused) {
- this.screen.focused = element;
- }
-};
-
-Node.prototype.prepend = function(element) {
- this.insert(element, 0);
-};
-
-Node.prototype.append = function(element) {
- this.insert(element, this.children.length);
-};
-
-Node.prototype.insertBefore = function(element, other) {
- var i = this.children.indexOf(other);
- if (~i) this.insert(element, i);
-};
-
-Node.prototype.insertAfter = function(element, other) {
- var i = this.children.indexOf(other);
- if (~i) this.insert(element, i + 1);
-};
-
-Node.prototype.remove = function(element) {
- if (element.parent !== this) return;
-
- var i = this.children.indexOf(element);
- if (!~i) return;
-
- element.clearPos();
-
- element.parent = null;
-
- this.children.splice(i, 1);
-
- i = this.screen.clickable.indexOf(element);
- if (~i) this.screen.clickable.splice(i, 1);
- i = this.screen.keyable.indexOf(element);
- if (~i) this.screen.keyable.splice(i, 1);
-
- element.emit('reparent', null);
- this.emit('remove', element);
-
- (function emit(el) {
- var n = el.detached !== true;
- el.detached = true;
- if (n) el.emit('detach');
- el.children.forEach(emit);
- })(element);
-
- if (this.screen.focused === element) {
- this.screen.rewindFocus();
- }
-};
-
-Node.prototype.detach = function() {
- if (this.parent) this.parent.remove(this);
-};
-
-Node.prototype.forDescendants = function(iter, s) {
- if (s) iter(this);
- this.children.forEach(function emit(el) {
- iter(el);
- el.children.forEach(emit);
- });
-};
-
-Node.prototype.forAncestors = function(iter, s) {
- var el = this;
- if (s) iter(this);
- while (el = el.parent) {
- iter(el);
- }
-};
-
-Node.prototype.collectDescendants = function(s) {
- var out = [];
- this.forDescendants(function(el) {
- out.push(el);
- }, s);
- return out;
-};
-
-Node.prototype.collectAncestors = function(s) {
- var out = [];
- this.forAncestors(function(el) {
- out.push(el);
- }, s);
- return out;
-};
-
-Node.prototype.emitDescendants = function() {
- var args = Array.prototype.slice(arguments)
- , iter;
-
- if (typeof args[args.length - 1] === 'function') {
- iter = args.pop();
- }
-
- return this.forDescendants(function(el) {
- if (iter) iter(el);
- el.emit.apply(el, args);
- }, true);
-};
-
-Node.prototype.emitAncestors = function() {
- var args = Array.prototype.slice(arguments)
- , iter;
-
- if (typeof args[args.length - 1] === 'function') {
- iter = args.pop();
- }
-
- return this.forAncestors(function(el) {
- if (iter) iter(el);
- el.emit.apply(el, args);
- }, true);
-};
-
-Node.prototype.hasDescendant = function(target) {
- return (function find(el) {
- for (var i = 0; i < el.children.length; i++) {
- if (el.children[i] === target) {
- return true;
- }
- if (find(el.children[i]) === true) {
- return true;
- }
- }
- return false;
- })(this);
-};
-
-Node.prototype.hasAncestor = function(target) {
- var el = this;
- while (el = el.parent) {
- if (el === target) return true;
- }
- return false;
-};
-
-Node.prototype.get = function(name, value) {
- if (this.data.hasOwnProperty(name)) {
- return this.data[name];
- }
- return value;
-};
-
-Node.prototype.set = function(name, value) {
- return this.data[name] = value;
-};
-
-/**
- * Screen
- */
-
-function Screen(options) {
- var self = this;
-
- if (!(this instanceof Node)) {
- return new Screen(options);
- }
-
- options = options || {};
- if (options.rsety && options.listen) {
- options = { program: options };
- }
-
- this.program = options.program || program.global;
-
- if (!this.program) {
- this.program = program({
- input: options.input,
- output: options.output,
- log: options.log,
- debug: options.debug,
- dump: options.dump,
- term: options.term,
- resizeTimeout: options.resizeTimeout,
- tput: true,
- buffer: true,
- zero: true
- });
- } else {
- this.program.setupTput();
- this.program.useBuffer = true;
- this.program.zero = true;
- this.program.options.resizeTimeout = options.resizeTimeout;
- }
-
- this.tput = this.program.tput;
-
- if (!Screen.global) {
- Screen.global = this;
- }
-
- Node.call(this, options);
-
- this.autoPadding = options.autoPadding !== false;
- this.tabc = Array((options.tabSize || 4) + 1).join(' ');
- this.dockBorders = options.dockBorders;
-
- this.ignoreLocked = options.ignoreLocked || [];
-
- this._unicode = this.tput.unicode || this.tput.numbers.U8 === 1;
- this.fullUnicode = this.options.fullUnicode && this._unicode;
-
- this.dattr = ((0 << 18) | (0x1ff << 9)) | 0x1ff;
-
- this.renders = 0;
- this.position = {
- left: this.left = this.aleft = this.rleft = 0,
- right: this.right = this.aright = this.rright = 0,
- top: this.top = this.atop = this.rtop = 0,
- bottom: this.bottom = this.abottom = this.rbottom = 0,
- get height() { return self.height; },
- get width() { return self.width; }
- };
-
- this.ileft = 0;
- this.itop = 0;
- this.iright = 0;
- this.ibottom = 0;
- this.iheight = 0;
- this.iwidth = 0;
-
- this.padding = {
- left: 0,
- top: 0,
- right: 0,
- bottom: 0
- };
-
- this.hover = null;
- this.history = [];
- this.clickable = [];
- this.keyable = [];
- this.grabKeys = false;
- this.lockKeys = false;
- this.focused;
- this._buf = '';
-
- this._ci = -1;
-
- if (options.title) {
- this.title = options.title;
- }
-
- options.cursor = options.cursor || {
- artificial: options.artificialCursor,
- shape: options.cursorShape,
- blink: options.cursorBlink,
- color: options.cursorColor
- };
-
- this.cursor = {
- artificial: options.cursor.artificial || false,
- shape: options.cursor.shape || 'block',
- blink: options.cursor.blink || false,
- color: options.cursor.color || null,
- _set: false,
- _state: 1,
- _hidden: true
- };
-
- this.program.on('resize', function() {
- self.alloc();
- self.render();
- (function emit(el) {
- el.emit('resize');
- el.children.forEach(emit);
- })(self);
- });
-
- this.program.on('focus', function() {
- self.emit('focus');
- });
-
- this.program.on('blur', function() {
- self.emit('blur');
- });
-
- this.on('newListener', function fn(type) {
- if (type === 'keypress' || type.indexOf('key ') === 0 || type === 'mouse') {
- if (type === 'keypress' || type.indexOf('key ') === 0) self._listenKeys();
- if (type === 'mouse') self._listenMouse();
- }
- if (type === 'mouse'
- || type === 'click'
- || type === 'mouseover'
- || type === 'mouseout'
- || type === 'mousedown'
- || type === 'mouseup'
- || type === 'mousewheel'
- || type === 'wheeldown'
- || type === 'wheelup'
- || type === 'mousemove') {
- self._listenMouse();
- }
- });
-
- this.setMaxListeners(Infinity);
-
- Screen.total++;
-
- process.on('uncaughtException', function(err) {
- if (process.listeners('uncaughtException').length > Screen.total) {
- return;
- }
- self.leave();
- err = err || new Error('Uncaught Exception.');
- console.error(err.stack ? err.stack + '' : err + '');
- nextTick(function() {
- process.exit(1);
- });
- });
-
- ['SIGTERM', 'SIGINT', 'SIGQUIT'].forEach(function(signal) {
- process.on(signal, function() {
- if (process.listeners(signal).length > Screen.total) {
- return;
- }
- nextTick(function() {
- process.exit(0);
- });
- });
- });
-
- process.on('exit', function() {
- self.leave();
- });
-
- this.enter();
-
- this.postEnter();
-}
-
-Screen.global = null;
-
-Screen.total = 0;
-
-Screen.prototype.__proto__ = Node.prototype;
-
-Screen.prototype.type = 'screen';
-
-Screen.prototype.__defineGetter__('title', function() {
- return this.program.title;
-});
-
-Screen.prototype.__defineSetter__('title', function(title) {
- return this.program.title = title;
-});
-
-Screen.prototype.enter = function() {
- if (this.program.isAlt) return;
- if (!this.cursor._set) {
- if (this.options.cursor.shape) {
- this.cursorShape(this.cursor.shape, this.cursor.blink);
- }
- if (this.options.cursor.color) {
- this.cursorColor(this.cursor.color);
- }
- }
- if (process.platform === 'win32') {
- try {
- cp.execSync('cls', { stdio: 'ignore', timeout: 1000 });
- } catch (e) {
- ;
- }
- }
- this.program.alternateBuffer();
- this.program.put.keypad_xmit();
- this.program.csr(0, this.height - 1);
- this.program.hideCursor();
- this.program.cup(0, 0);
- this.alloc();
-};
-
-Screen.prototype.leave = function() {
- if (!this.program.isAlt) return;
- this.program.put.keypad_local();
- if (this.program.scrollTop !== 0
- || this.program.scrollBottom !== this.rows - 1) {
- this.program.csr(0, this.height - 1);
- }
- // XXX For some reason if alloc/clear() is before this
- // line, it doesn't work on linux console.
- this.program.showCursor();
- this.alloc();
- if (this._listenedMouse) {
- this.program.disableMouse();
- }
- this.program.normalBuffer();
- if (this.cursor._set) this.cursorReset();
- this.program.flush();
- if (process.platform === 'win32') {
- try {
- cp.execSync('cls', { stdio: 'ignore', timeout: 1000 });
- } catch (e) {
- ;
- }
- }
-};
-
-Screen.prototype.postEnter = function() {
- var self = this;
- if (this.options.debug) {
- this.debugLog = new Log({
- parent: this,
- hidden: true,
- draggable: true,
- left: 'center',
- top: 'center',
- width: '30%',
- height: '30%',
- border: 'line',
- label: ' {bold}Debug Log{/bold} ',
- tags: true,
- keys: true,
- vi: true,
- mouse: true,
- scrollbar: {
- ch: ' ',
- track: {
- bg: 'yellow'
- },
- style: {
- inverse: true
- }
- }
- });
-
- this.debugLog.toggle = function() {
- if (self.debugLog.hidden) {
- self.saveFocus();
- self.debugLog.show();
- self.debugLog.setFront();
- self.debugLog.focus();
- } else {
- self.debugLog.hide();
- self.restoreFocus();
- }
- self.render();
- };
-
- this.debugLog.key(['q', 'escape'], self.debugLog.toggle);
- this.key('f12', self.debugLog.toggle);
- }
-};
-
-Screen.prototype.log = function() {
- if (this.debugLog) {
- this.debugLog.log.apply(this.debugLog, arguments);
- }
- return this.program.log.apply(this.program, arguments);
-};
-
-Screen.prototype.debug = function() {
- if (this.debugLog) {
- this.debugLog.log.apply(this.debugLog, arguments);
- }
- return this.program.debug.apply(this.program, arguments);
-};
-
-Screen.prototype._listenMouse = function(el) {
- var self = this;
-
- if (el && !~this.clickable.indexOf(el)) {
- el.clickable = true;
- this.clickable.push(el);
- }
-
- if (this._listenedMouse) return;
- this._listenedMouse = true;
-
- this.program.enableMouse();
-
- this.on('render', function() {
- self._needsClickableSort = true;
- });
-
- this.program.on('mouse', function(data) {
- if (self.lockKeys) return;
-
- if (self._needsClickableSort) {
- self.clickable = helpers.hsort(self.clickable);
- self._needsClickableSort = false;
- }
-
- var i = 0
- , el
- , set
- , pos;
-
- for (; i < self.clickable.length; i++) {
- el = self.clickable[i];
-
- if (el.detached || !el.visible) {
- continue;
- }
-
- // if (self.grabMouse && self.focused !== el
- // && !el.hasAncestor(self.focused)) continue;
-
- pos = el.lpos;
- if (!pos) continue;
-
- if (data.x >= pos.xi && data.x < pos.xl
- && data.y >= pos.yi && data.y < pos.yl) {
- el.emit('mouse', data);
- if (data.action === 'mousedown') {
- self.mouseDown = el;
- } else if (data.action === 'mouseup') {
- (self.mouseDown || el).emit('click', data);
- self.mouseDown = null;
- } else if (data.action === 'mousemove') {
- if (self.hover && el.index > self.hover.index) {
- set = false;
- }
- if (self.hover !== el && !set) {
- if (self.hover) {
- self.hover.emit('mouseout', data);
- }
- el.emit('mouseover', data);
- self.hover = el;
- }
- set = true;
- }
- el.emit(data.action, data);
- break;
- }
- }
-
- // Just mouseover?
- if ((data.action === 'mousemove'
- || data.action === 'mousedown'
- || data.action === 'mouseup')
- && self.hover
- && !set) {
- self.hover.emit('mouseout', data);
- self.hover = null;
- }
-
- self.emit('mouse', data);
- self.emit(data.action, data);
- });
-
- // Autofocus highest element.
- // this.on('element click', function(el, data) {
- // var target;
- // do {
- // if (el.clickable === true && el.options.autoFocus !== false) {
- // target = el;
- // }
- // } while (el = el.parent);
- // if (target) target.focus();
- // });
-
- // Autofocus elements with the appropriate option.
- this.on('element click', function(el, data) {
- if (el.clickable === true && el.options.autoFocus !== false) {
- el.focus();
- }
- });
-};
-
-Screen.prototype.enableMouse = function(el) {
- this._listenMouse(el);
-};
-
-Screen.prototype._listenKeys = function(el) {
- var self = this;
-
- if (el && !~this.keyable.indexOf(el)) {
- el.keyable = true;
- this.keyable.push(el);
- }
-
- if (this._listenedKeys) return;
- this._listenedKeys = true;
-
- // NOTE: The event emissions used to be reversed:
- // element + screen
- // They are now:
- // screen + element
- // After the first keypress emitted, the handler
- // checks to make sure grabKeys, lockKeys, and focused
- // weren't changed, and handles those situations appropriately.
- this.program.on('keypress', function(ch, key) {
- if (self.lockKeys && !~self.ignoreLocked.indexOf(key.full)) {
- return;
- }
-
- var focused = self.focused
- , grabKeys = self.grabKeys;
-
- if (!grabKeys) {
- self.emit('keypress', ch, key);
- self.emit('key ' + key.full, ch, key);
- }
-
- // If something changed from the screen key handler, stop.
- if (self.grabKeys !== grabKeys || self.lockKeys) {
- return;
- }
-
- if (focused && focused.keyable) {
- focused.emit('keypress', ch, key);
- focused.emit('key ' + key.full, ch, key);
- }
- });
-};
-
-Screen.prototype.enableKeys = function(el) {
- this._listenKeys(el);
-};
-
-Screen.prototype.enableInput = function(el) {
- this._listenMouse(el);
- this._listenKeys(el);
-};
-
-Screen.prototype._initHover = function() {
- var self = this;
-
- if (this._hoverText) {
- return;
- }
-
- this._hoverText = new Box({
- screen: this,
- left: 0,
- top: 0,
- tags: false,
- height: 'shrink',
- width: 'shrink',
- border: 'line',
- style: {
- border: {
- fg: 'default'
- },
- bg: 'default',
- fg: 'default'
- }
- });
-
- this.on('mousemove', function(data) {
- if (self._hoverText.detached) return;
- self._hoverText.rleft = data.x + 1;
- self._hoverText.rtop = data.y;
- self.render();
- });
-
- this.on('element mouseover', function(el, data) {
- if (!el._hoverOptions) return;
- self._hoverText.parseTags = el.parseTags;
- self._hoverText.setContent(el._hoverOptions.text);
- self.append(self._hoverText);
- self._hoverText.rleft = data.x + 1;
- self._hoverText.rtop = data.y;
- self.render();
- });
-
- this.on('element mouseout', function() {
- if (self._hoverText.detached) return;
- self._hoverText.detach();
- self.render();
- });
-
- this.on('element mouseup', function(el, data) {
- if (!el._hoverOptions) return;
- self.append(self._hoverText);
- self.render();
- });
-};
-
-Screen.prototype.__defineGetter__('cols', function() {
- return this.program.cols;
-});
-
-Screen.prototype.__defineGetter__('rows', function() {
- return this.program.rows;
-});
-
-Screen.prototype.__defineGetter__('width', function() {
- return this.program.cols;
-});
-
-Screen.prototype.__defineGetter__('height', function() {
- return this.program.rows;
-});
-
-Screen.prototype.alloc = function() {
- var x, y;
-
- this.lines = [];
- for (y = 0; y < this.rows; y++) {
- this.lines[y] = [];
- for (x = 0; x < this.cols; x++) {
- this.lines[y][x] = [this.dattr, ' '];
- }
- this.lines[y].dirty = false;
- }
-
- this.olines = [];
- for (y = 0; y < this.rows; y++) {
- this.olines[y] = [];
- for (x = 0; x < this.cols; x++) {
- this.olines[y][x] = [this.dattr, ' '];
- }
- }
-
- this.program.clear();
-};
-
-Screen.prototype.render = function() {
- var self = this;
-
- this.emit('prerender');
-
- this._borderStops = {};
-
- // TODO: Possibly get rid of .dirty altogether.
- // TODO: Could possibly drop .dirty and just clear the `lines` buffer every
- // time before a screen.render. This way clearRegion doesn't have to be
- // called in arbitrary places for the sake of clearing a spot where an
- // element used to be (e.g. when an element moves or is hidden). There could
- // be some overhead though.
- // this.screen.clearRegion(0, this.cols, 0, this.rows);
- this._ci = 0;
- this.children.forEach(function(el) {
- el.index = self._ci++;
- //el._rendering = true;
- el.render();
- //el._rendering = false;
- });
- this._ci = -1;
-
- if (this.screen.dockBorders) {
- this._dockBorders();
- }
-
- this.draw(0, this.lines.length - 1);
-
- // XXX Workaround to deal with cursor pos before the screen has rendered and
- // lpos is not reliable (stale).
- if (this.focused && this.focused._updateCursor) {
- this.focused._updateCursor(true);
- }
-
- this.renders++;
-
- this.emit('render');
-};
-
-Screen.prototype.blankLine = function(ch, dirty) {
- var out = [];
- for (var x = 0; x < this.cols; x++) {
- out[x] = [this.dattr, ch || ' '];
- }
- out.dirty = dirty;
- return out;
-};
-
-Screen.prototype.insertLine = function(n, y, top, bottom) {
- // if (y === top) return this.insertLineNC(n, y, top, bottom);
-
- if (!this.tput.strings.change_scroll_region
- || !this.tput.strings.delete_line
- || !this.tput.strings.insert_line) return;
-
- this._buf += this.tput.csr(top, bottom);
- this._buf += this.tput.cup(y, 0);
- this._buf += this.tput.il(n);
- this._buf += this.tput.csr(0, this.height - 1);
-
- var j = bottom + 1;
-
- while (n--) {
- this.lines.splice(y, 0, this.blankLine());
- this.lines.splice(j, 1);
- this.olines.splice(y, 0, this.blankLine());
- this.olines.splice(j, 1);
- }
-};
-
-Screen.prototype.deleteLine = function(n, y, top, bottom) {
- // if (y === top) return this.deleteLineNC(n, y, top, bottom);
-
- if (!this.tput.strings.change_scroll_region
- || !this.tput.strings.delete_line
- || !this.tput.strings.insert_line) return;
-
- this._buf += this.tput.csr(top, bottom);
- this._buf += this.tput.cup(y, 0);
- this._buf += this.tput.dl(n);
- this._buf += this.tput.csr(0, this.height - 1);
-
- var j = bottom + 1;
-
- while (n--) {
- this.lines.splice(j, 0, this.blankLine());
- this.lines.splice(y, 1);
- this.olines.splice(j, 0, this.blankLine());
- this.olines.splice(y, 1);
- }
-};
-
-// This is how ncurses does it.
-// Scroll down (up cursor-wise).
-// This will only work for top line deletion as opposed to arbitrary lines.
-Screen.prototype.insertLineNC = function(n, y, top, bottom) {
- if (!this.tput.strings.change_scroll_region
- || !this.tput.strings.delete_line) return;
-
- this._buf += this.tput.csr(top, bottom);
- this._buf += this.tput.cup(top, 0);
- this._buf += this.tput.dl(n);
- this._buf += this.tput.csr(0, this.height - 1);
-
- var j = bottom + 1;
-
- while (n--) {
- this.lines.splice(j, 0, this.blankLine());
- this.lines.splice(y, 1);
- this.olines.splice(j, 0, this.blankLine());
- this.olines.splice(y, 1);
- }
-};
-
-// This is how ncurses does it.
-// Scroll up (down cursor-wise).
-// This will only work for bottom line deletion as opposed to arbitrary lines.
-Screen.prototype.deleteLineNC = function(n, y, top, bottom) {
- if (!this.tput.strings.change_scroll_region
- || !this.tput.strings.delete_line) return;
-
- this._buf += this.tput.csr(top, bottom);
- this._buf += this.tput.cup(bottom, 0);
- this._buf += Array(n + 1).join('\n');
- this._buf += this.tput.csr(0, this.height - 1);
-
- var j = bottom + 1;
-
- while (n--) {
- this.lines.splice(j, 0, this.blankLine());
- this.lines.splice(y, 1);
- this.olines.splice(j, 0, this.blankLine());
- this.olines.splice(y, 1);
- }
-};
-
-Screen.prototype.insertBottom = function(top, bottom) {
- return this.deleteLine(1, top, top, bottom);
-};
-
-Screen.prototype.insertTop = function(top, bottom) {
- return this.insertLine(1, top, top, bottom);
-};
-
-Screen.prototype.deleteBottom = function(top, bottom) {
- return this.clearRegion(0, this.width, bottom, bottom);
-};
-
-Screen.prototype.deleteTop = function(top, bottom) {
- // Same as: return this.insertBottom(top, bottom);
- return this.deleteLine(1, top, top, bottom);
-};
-
-// Parse the sides of an element to determine
-// whether an element has uniform cells on
-// both sides. If it does, we can use CSR to
-// optimize scrolling on a scrollable element.
-// Not exactly sure how worthwile this is.
-// This will cause a performance/cpu-usage hit,
-// but will it be less or greater than the
-// performance hit of slow-rendering scrollable
-// boxes with clean sides?
-Screen.prototype.cleanSides = function(el) {
- var pos = el.lpos;
-
- if (!pos) {
- return false;
- }
-
- if (pos._cleanSides != null) {
- return pos._cleanSides;
- }
-
- if (pos.xi <= 0 && pos.xl >= this.width) {
- return pos._cleanSides = true;
- }
-
- if (this.options.fastCSR) {
- // Maybe just do this instead of parsing.
- if (pos.yi < 0) return pos._cleanSides = false;
- if (pos.yl > this.height) return pos._cleanSides = false;
- if (this.width - (pos.xl - pos.xi) < 40) {
- return pos._cleanSides = true;
- }
- return pos._cleanSides = false;
- }
-
- if (!this.options.smartCSR) {
- return false;
- }
-
- // The scrollbar can't update properly, and there's also a
- // chance that the scrollbar may get moved around senselessly.
- // NOTE: In pratice, this doesn't seem to be the case.
- // if (this.scrollbar) {
- // return pos._cleanSides = false;
- // }
-
- // Doesn't matter if we're only a height of 1.
- // if ((pos.yl - el.ibottom) - (pos.yi + el.itop) <= 1) {
- // return pos._cleanSides = false;
- // }
-
- var yi = pos.yi + el.itop
- , yl = pos.yl - el.ibottom
- , first
- , ch
- , x
- , y;
-
- if (pos.yi < 0) return pos._cleanSides = false;
- if (pos.yl > this.height) return pos._cleanSides = false;
- if (pos.xi - 1 < 0) return pos._cleanSides = true;
- if (pos.xl > this.width) return pos._cleanSides = true;
-
- for (x = pos.xi - 1; x >= 0; x--) {
- if (!this.olines[yi]) break;
- first = this.olines[yi][x];
- for (y = yi; y < yl; y++) {
- if (!this.olines[y] || !this.olines[y][x]) break;
- ch = this.olines[y][x];
- if (ch[0] !== first[0] || ch[1] !== first[1]) {
- return pos._cleanSides = false;
- }
- }
- }
-
- for (x = pos.xl; x < this.width; x++) {
- if (!this.olines[yi]) break;
- first = this.olines[yi][x];
- for (y = yi; y < yl; y++) {
- if (!this.olines[y] || !this.olines[y][x]) break;
- ch = this.olines[y][x];
- if (ch[0] !== first[0] || ch[1] !== first[1]) {
- return pos._cleanSides = false;
- }
- }
- }
-
- return pos._cleanSides = true;
-};
-
-Screen.prototype._dockBorders = function() {
- var lines = this.lines
- , stops = this._borderStops
- , i
- , y
- , x
- , ch;
-
- // var keys, stop;
- //
- // keys = Object.keys(this._borderStops)
- // .map(function(k) { return +k; })
- // .sort(function(a, b) { return a - b; });
- //
- // for (i = 0; i < keys.length; i++) {
- // y = keys[i];
- // if (!lines[y]) continue;
- // stop = this._borderStops[y];
- // for (x = stop.xi; x < stop.xl; x++) {
-
- stops = Object.keys(stops)
- .map(function(k) { return +k; })
- .sort(function(a, b) { return a - b; });
-
- for (i = 0; i < stops.length; i++) {
- y = stops[i];
- if (!lines[y]) continue;
- for (x = 0; x < this.width; x++) {
- ch = lines[y][x][1];
- if (angles[ch]) {
- lines[y][x][1] = this._getAngle(lines, x, y);
- }
- }
- }
-};
-
-Screen.prototype._getAngle = function(lines, x, y) {
- var angle = 0
- , attr = lines[y][x][0]
- , ch = lines[y][x][1];
-
- if (lines[y][x - 1] && langles[lines[y][x - 1][1]]) {
- if (!this.options.ignoreDockContrast) {
- if (lines[y][x - 1][0] !== attr) return ch;
- }
- angle |= 1 << 3;
- }
-
- if (lines[y - 1] && uangles[lines[y - 1][x][1]]) {
- if (!this.options.ignoreDockContrast) {
- if (lines[y - 1][x][0] !== attr) return ch;
- }
- angle |= 1 << 2;
- }
-
- if (lines[y][x + 1] && rangles[lines[y][x + 1][1]]) {
- if (!this.options.ignoreDockContrast) {
- if (lines[y][x + 1][0] !== attr) return ch;
- }
- angle |= 1 << 1;
- }
-
- if (lines[y + 1] && dangles[lines[y + 1][x][1]]) {
- if (!this.options.ignoreDockContrast) {
- if (lines[y + 1][x][0] !== attr) return ch;
- }
- angle |= 1 << 0;
- }
-
- // Experimental: fixes this situation:
- // +----------+
- // | <-- empty space here, should be a T angle
- // +-------+ |
- // | | |
- // +-------+ |
- // | |
- // +----------+
- // if (uangles[lines[y][x][1]]) {
- // if (lines[y + 1] && cdangles[lines[y + 1][x][1]]) {
- // if (!this.options.ignoreDockContrast) {
- // if (lines[y + 1][x][0] !== attr) return ch;
- // }
- // angle |= 1 << 0;
- // }
- // }
-
- return angleTable[angle] || ch;
-};
-
-Screen.prototype.draw = function(start, end) {
- // this.emit('predraw');
-
- var x
- , y
- , line
- , out
- , ch
- , data
- , attr
- , fg
- , bg
- , flags;
-
- var main = ''
- , pre
- , post;
-
- var clr
- , neq
- , xx;
-
- var lx = -1
- , ly = -1
- , o;
-
- var acs;
-
- if (this._buf) {
- main += this._buf;
- this._buf = '';
- }
-
- for (y = start; y <= end; y++) {
- line = this.lines[y];
- o = this.olines[y];
-
- if (!line.dirty && !(this.cursor.artificial && y === this.program.y)) {
- continue;
- }
- line.dirty = false;
-
- out = '';
- attr = this.dattr;
-
- for (x = 0; x < line.length; x++) {
- data = line[x][0];
- ch = line[x][1];
-
- // Render the artificial cursor.
- if (this.cursor.artificial
- && !this.cursor._hidden
- && this.cursor._state
- && x === this.program.x
- && y === this.program.y) {
- var cattr = this._cursorAttr(this.cursor, data);
- if (cattr.ch) ch = cattr.ch;
- data = cattr.attr;
- }
-
- // Take advantage of xterm's back_color_erase feature by using a
- // lookahead. Stop spitting out so many damn spaces. NOTE: Is checking
- // the bg for non BCE terminals worth the overhead?
- if (this.options.useBCE
- && ch === ' '
- && (this.tput.bools.back_color_erase
- || (data & 0x1ff) === (this.dattr & 0x1ff))
- && ((data >> 18) & 8) === ((this.dattr >> 18) & 8)) {
- clr = true;
- neq = false;
-
- for (xx = x; xx < line.length; xx++) {
- if (line[xx][0] !== data || line[xx][1] !== ' ') {
- clr = false;
- break;
- }
- if (line[xx][0] !== o[xx][0] || line[xx][1] !== o[xx][1]) {
- neq = true;
- }
- }
-
- if (clr && neq) {
- lx = -1, ly = -1;
- if (data !== attr) {
- out += this.codeAttr(data);
- attr = data;
- }
- out += this.tput.cup(y, x);
- out += this.tput.el();
- for (xx = x; xx < line.length; xx++) {
- o[xx][0] = data;
- o[xx][1] = ' ';
- }
- break;
- }
-
- // If there's more than 10 spaces, use EL regardless
- // and start over drawing the rest of line. Might
- // not be worth it. Try to use ECH if the terminal
- // supports it. Maybe only try to use ECH here.
- // //if (this.tput.strings.erase_chars)
- // if (!clr && neq && (xx - x) > 10) {
- // lx = -1, ly = -1;
- // if (data !== attr) {
- // out += this.codeAttr(data);
- // attr = data;
- // }
- // out += this.tput.cup(y, x);
- // if (this.tput.strings.erase_chars) {
- // // Use erase_chars to avoid erasing the whole line.
- // out += this.tput.ech(xx - x);
- // } else {
- // out += this.tput.el();
- // }
- // out += this.tput.cuf(xx - x);
- // this.fillRegion(data, ' ',
- // x, this.tput.strings.erase_chars ? xx : line.length,
- // y, y + 1);
- // x = xx - 1;
- // continue;
- // }
-
- // Skip to the next line if the
- // rest of the line is already drawn.
- // if (!neq) {
- // for (; xx < line.length; xx++) {
- // if (line[xx][0] !== o[xx][0] || line[xx][1] !== o[xx][1]) {
- // neq = true;
- // break;
- // }
- // }
- // if (!neq) {
- // attr = data;
- // break;
- // }
- // }
- }
-
- // Optimize by comparing the real output
- // buffer to the pending output buffer.
- if (data === o[x][0] && ch === o[x][1]) {
- if (lx === -1) {
- lx = x;
- ly = y;
- }
- continue;
- } else if (lx !== -1) {
- out += y === ly
- ? this.tput.cuf(x - lx)
- : this.tput.cup(y, x);
- lx = -1, ly = -1;
- }
- o[x][0] = data;
- o[x][1] = ch;
-
- if (data !== attr) {
- if (attr !== this.dattr) {
- out += '\x1b[m';
- }
- if (data !== this.dattr) {
- out += '\x1b[';
-
- bg = data & 0x1ff;
- fg = (data >> 9) & 0x1ff;
- flags = data >> 18;
-
- // bold
- if (flags & 1) {
- out += '1;';
- }
-
- // underline
- if (flags & 2) {
- out += '4;';
- }
-
- // blink
- if (flags & 4) {
- out += '5;';
- }
-
- // inverse
- if (flags & 8) {
- out += '7;';
- }
-
- // invisible
- if (flags & 16) {
- out += '8;';
- }
-
- if (bg !== 0x1ff) {
- bg = this._reduceColor(bg);
- if (bg < 16) {
- if (bg < 8) {
- bg += 40;
- } else if (bg < 16) {
- bg -= 8;
- bg += 100;
- }
- out += bg + ';';
- } else {
- out += '48;5;' + bg + ';';
- }
- }
-
- if (fg !== 0x1ff) {
- fg = this._reduceColor(fg);
- if (fg < 16) {
- if (fg < 8) {
- fg += 30;
- } else if (fg < 16) {
- fg -= 8;
- fg += 90;
- }
- out += fg + ';';
- } else {
- out += '38;5;' + fg + ';';
- }
- }
-
- if (out[out.length - 1] === ';') out = out.slice(0, -1);
-
- out += 'm';
- }
- }
-
- // If we find a double-width char, eat the next character which should be
- // a space due to parseContent's behavior.
- if (this.fullUnicode) {
- // If this is a surrogate pair double-width char, we can ignore it
- // because parseContent already counted it as length=2.
- if (unicode.charWidth(line[x][1]) === 2) {
- // NOTE: At cols=44, the bug that is avoided
- // by the angles check occurs in widget-unicode:
- // Might also need: `line[x + 1][0] !== line[x][0]`
- // for borderless boxes?
- if (x === line.length - 1 || angles[line[x + 1][1]]) {
- // If we're at the end, we don't have enough space for a
- // double-width. Overwrite it with a space and ignore.
- ch = ' ';
- o[x][1] = '\0';
- } else {
- // ALWAYS refresh double-width chars because this special cursor
- // behavior is needed. There may be a more efficient way of doing
- // this. See above.
- o[x][1] = '\0';
- // Eat the next character by moving forward and marking as a
- // space (which it is).
- o[++x][1] = '\0';
- }
- }
- }
-
- // Attempt to use ACS for supported characters.
- // This is not ideal, but it's how ncurses works.
- // There are a lot of terminals that support ACS
- // *and UTF8, but do not declare U8. So ACS ends
- // up being used (slower than utf8). Terminals
- // that do not support ACS and do not explicitly
- // support UTF8 get their unicode characters
- // replaced with really ugly ascii characters.
- // It is possible there is a terminal out there
- // somewhere that does not support ACS, but
- // supports UTF8, but I imagine it's unlikely.
- // Maybe remove !this.tput.unicode check, however,
- // this seems to be the way ncurses does it.
- if (this.tput.strings.enter_alt_charset_mode
- && !this.tput.brokenACS && (this.tput.acscr[ch] || acs)) {
- // Fun fact: even if this.tput.brokenACS wasn't checked here,
- // the linux console would still work fine because the acs
- // table would fail the check of: this.tput.acscr[ch]
- if (this.tput.acscr[ch]) {
- if (acs) {
- ch = this.tput.acscr[ch];
- } else {
- ch = this.tput.smacs()
- + this.tput.acscr[ch];
- acs = true;
- }
- } else if (acs) {
- ch = this.tput.rmacs() + ch;
- acs = false;
- }
- } else {
- // U8 is not consistently correct. Some terminfo's
- // terminals that do not declare it may actually
- // support utf8 (e.g. urxvt), but if the terminal
- // does not declare support for ACS (and U8), chances
- // are it does not support UTF8. This is probably
- // the "safest" way to do this. Should fix things
- // like sun-color.
- // NOTE: It could be the case that the $LANG
- // is all that matters in some cases:
- // if (!this.tput.unicode && ch > '~') {
- if (!this.tput.unicode && this.tput.numbers.U8 !== 1 && ch > '~') {
- ch = this.tput.utoa[ch] || '?';
- }
- }
-
- out += ch;
- attr = data;
- }
-
- if (attr !== this.dattr) {
- out += '\x1b[m';
- }
-
- if (out) {
- main += this.tput.cup(y, 0) + out;
- }
- }
-
- if (acs) {
- main += this.tput.rmacs();
- acs = false;
- }
-
- if (main) {
- pre = '';
- post = '';
-
- pre += this.tput.sc();
- post += this.tput.rc();
-
- if (!this.program.cursorHidden) {
- pre += this.tput.civis();
- post += this.tput.cnorm();
- }
-
- // this.program.flush();
- // this.program.output.write(pre + main + post);
- this.program._write(pre + main + post);
- }
-
- // this.emit('draw');
-};
-
-Screen.prototype._reduceColor = function(col) {
- if (col >= 16 && this.tput.colors <= 16) {
- col = colors.ccolors[col];
- } else if (col >= 8 && this.tput.colors <= 8) {
- col -= 8;
- } else if (col >= 2 && this.tput.colors <= 2) {
- col %= 2;
- }
- return col;
-};
-
-// Convert an SGR string to our own attribute format.
-Screen.prototype.attrCode = function(code, cur, def) {
- var flags = (cur >> 18) & 0x1ff
- , fg = (cur >> 9) & 0x1ff
- , bg = cur & 0x1ff
- , c
- , i;
-
- code = code.slice(2, -1).split(';');
- if (!code[0]) code[0] = '0';
-
- for (i = 0; i < code.length; i++) {
- c = +code[i] || 0;
- switch (c) {
- case 0: // normal
- bg = def & 0x1ff;
- fg = (def >> 9) & 0x1ff;
- flags = (def >> 18) & 0x1ff;
- break;
- case 1: // bold
- flags |= 1;
- break;
- case 22:
- flags = (def >> 18) & 0x1ff;
- break;
- case 4: // underline
- flags |= 2;
- break;
- case 24:
- flags = (def >> 18) & 0x1ff;
- break;
- case 5: // blink
- flags |= 4;
- break;
- case 25:
- flags = (def >> 18) & 0x1ff;
- break;
- case 7: // inverse
- flags |= 8;
- break;
- case 27:
- flags = (def >> 18) & 0x1ff;
- break;
- case 8: // invisible
- flags |= 16;
- break;
- case 28:
- flags = (def >> 18) & 0x1ff;
- break;
- case 39: // default fg
- fg = (def >> 9) & 0x1ff;
- break;
- case 49: // default bg
- bg = def & 0x1ff;
- break;
- case 100: // default fg/bg
- fg = (def >> 9) & 0x1ff;
- bg = def & 0x1ff;
- break;
- default: // color
- if (c === 48 && +code[i+1] === 5) {
- i += 2;
- bg = +code[i];
- break;
- } else if (c === 48 && +code[i+1] === 2) {
- i += 2;
- bg = colors.match(+code[i], +code[i+1], +code[i+2]);
- if (bg === -1) bg = def & 0x1ff;
- i += 2;
- break;
- } else if (c === 38 && +code[i+1] === 5) {
- i += 2;
- fg = +code[i];
- break;
- } else if (c === 38 && +code[i+1] === 2) {
- i += 2;
- fg = colors.match(+code[i], +code[i+1], +code[i+2]);
- if (fg === -1) fg = (def >> 9) & 0x1ff;
- i += 2;
- break;
- }
- if (c >= 40 && c <= 47) {
- bg = c - 40;
- } else if (c >= 100 && c <= 107) {
- bg = c - 100;
- bg += 8;
- } else if (c === 49) {
- bg = def & 0x1ff;
- } else if (c >= 30 && c <= 37) {
- fg = c - 30;
- } else if (c >= 90 && c <= 97) {
- fg = c - 90;
- fg += 8;
- } else if (c === 39) {
- fg = (def >> 9) & 0x1ff;
- } else if (c === 100) {
- fg = (def >> 9) & 0x1ff;
- bg = def & 0x1ff;
- }
- break;
- }
- }
-
- return (flags << 18) | (fg << 9) | bg;
-};
-
-// Convert our own attribute format to an SGR string.
-Screen.prototype.codeAttr = function(code) {
- var flags = (code >> 18) & 0x1ff
- , fg = (code >> 9) & 0x1ff
- , bg = code & 0x1ff
- , out = '';
-
- // bold
- if (flags & 1) {
- out += '1;';
- }
-
- // underline
- if (flags & 2) {
- out += '4;';
- }
-
- // blink
- if (flags & 4) {
- out += '5;';
- }
-
- // inverse
- if (flags & 8) {
- out += '7;';
- }
-
- // invisible
- if (flags & 16) {
- out += '8;';
- }
-
- if (bg !== 0x1ff) {
- bg = this._reduceColor(bg);
- if (bg < 16) {
- if (bg < 8) {
- bg += 40;
- } else if (bg < 16) {
- bg -= 8;
- bg += 100;
- }
- out += bg + ';';
- } else {
- out += '48;5;' + bg + ';';
- }
- }
-
- if (fg !== 0x1ff) {
- fg = this._reduceColor(fg);
- if (fg < 16) {
- if (fg < 8) {
- fg += 30;
- } else if (fg < 16) {
- fg -= 8;
- fg += 90;
- }
- out += fg + ';';
- } else {
- out += '38;5;' + fg + ';';
- }
- }
-
- if (out[out.length - 1] === ';') out = out.slice(0, -1);
-
- return '\x1b[' + out + 'm';
-};
-
-Screen.prototype.focusOffset = function(offset) {
- var shown = this.keyable.filter(function(el) {
- return !el.detached && el.visible;
- }).length;
-
- if (!shown || !offset) {
- return;
- }
-
- var i = this.keyable.indexOf(this.focused);
- if (!~i) return;
-
- if (offset > 0) {
- while (offset--) {
- if (++i > this.keyable.length - 1) i = 0;
- if (this.keyable[i].detached || !this.keyable[i].visible) offset++;
- }
- } else {
- offset = -offset;
- while (offset--) {
- if (--i < 0) i = this.keyable.length - 1;
- if (this.keyable[i].detached || !this.keyable[i].visible) offset++;
- }
- }
-
- return this.keyable[i].focus();
-};
-
-Screen.prototype.focusPrev =
-Screen.prototype.focusPrevious = function() {
- return this.focusOffset(-1);
-};
-
-Screen.prototype.focusNext = function() {
- return this.focusOffset(1);
-};
-
-Screen.prototype.focusPush = function(el) {
- if (!el) return;
- var old = this.history[this.history.length - 1];
- if (this.history.length === 10) {
- this.history.shift();
- }
- this.history.push(el);
- this._focus(el, old);
-};
-
-Screen.prototype.focusPop = function() {
- var old = this.history.pop();
- if (this.history.length) {
- this._focus(this.history[this.history.length - 1], old);
- }
- return old;
-};
-
-Screen.prototype.saveFocus = function() {
- return this._savedFocus = this.focused;
-};
-
-Screen.prototype.restoreFocus = function() {
- if (!this._savedFocus) return;
- this._savedFocus.focus();
- delete this._savedFocus;
- return this.focused;
-};
-
-Screen.prototype.rewindFocus = function() {
- var old = this.history.pop()
- , el;
-
- while (this.history.length) {
- el = this.history.pop();
- if (!el.detached && el.visible) {
- this.history.push(el);
- this._focus(el, old);
- return el;
- }
- }
-
- if (old) {
- old.emit('blur');
- }
-};
-
-Screen.prototype._focus = function(self, old) {
- // Find a scrollable ancestor if we have one.
- var el = self;
- while (el = el.parent) {
- if (el.scrollable) break;
- }
-
- // If we're in a scrollable element,
- // automatically scroll to the focused element.
- if (el) {
- // NOTE: This is different from the other "visible" values - it needs the
- // visible height of the scrolling element itself, not the element within
- // it.
- var visible = self.screen.height - el.atop - el.itop - el.abottom - el.ibottom;
- if (self.rtop < el.childBase) {
- el.scrollTo(self.rtop);
- self.screen.render();
- } else if (self.rtop + self.height - self.ibottom > el.childBase + visible) {
- // Explanation for el.itop here: takes into account scrollable elements
- // with borders otherwise the element gets covered by the bottom border:
- el.scrollTo(self.rtop - (el.height - self.height) + el.itop, true);
- self.screen.render();
- }
- }
-
- if (old) {
- old.emit('blur', self);
- }
-
- self.emit('focus', old);
-};
-
-Screen.prototype.__defineGetter__('focused', function() {
- return this.history[this.history.length - 1];
-});
-
-Screen.prototype.__defineSetter__('focused', function(el) {
- return this.focusPush(el);
-});
-
-Screen.prototype.clearRegion = function(xi, xl, yi, yl, override) {
- return this.fillRegion(this.dattr, ' ', xi, xl, yi, yl, override);
-};
-
-Screen.prototype.fillRegion = function(attr, ch, xi, xl, yi, yl, override) {
- var lines = this.lines
- , cell
- , xx;
-
- if (xi < 0) xi = 0;
- if (yi < 0) yi = 0;
-
- for (; yi < yl; yi++) {
- if (!lines[yi]) break;
- for (xx = xi; xx < xl; xx++) {
- cell = lines[yi][xx];
- if (!cell) break;
- if (override || attr !== cell[0] || ch !== cell[1]) {
- lines[yi][xx][0] = attr;
- lines[yi][xx][1] = ch;
- lines[yi].dirty = true;
- }
- }
- }
-};
-
-Screen.prototype.key = function() {
- return this.program.key.apply(this, arguments);
-};
-
-Screen.prototype.onceKey = function() {
- return this.program.onceKey.apply(this, arguments);
-};
-
-Screen.prototype.unkey =
-Screen.prototype.removeKey = function() {
- return this.program.unkey.apply(this, arguments);
-};
-
-Screen.prototype.spawn = function(file, args, options) {
- if (!Array.isArray(args)) {
- options = args;
- args = [];
- }
-
- var screen = this
- , program = screen.program
- , options = options || {}
- , spawn = require('child_process').spawn
- , mouse = program.mouseEnabled
- , ps;
-
- options.stdio = options.stdio || 'inherit';
-
- program.lsaveCursor('spawn');
- // program.csr(0, program.rows - 1);
- program.normalBuffer();
- program.showCursor();
- if (mouse) program.disableMouse();
-
- var write = program.output.write;
- program.output.write = function() {};
- program.input.pause();
- program.input.setRawMode(false);
-
- var resume = function() {
- if (resume.done) return;
- resume.done = true;
-
- program.input.setRawMode(true);
- program.input.resume();
- program.output.write = write;
-
- program.alternateBuffer();
- // program.csr(0, program.rows - 1);
- if (mouse) program.enableMouse();
-
- screen.alloc();
- screen.render();
-
- screen.program.lrestoreCursor('spawn', true);
- };
-
- ps = spawn(file, args, options);
-
- ps.on('error', resume);
-
- ps.on('exit', resume);
-
- return ps;
-};
-
-Screen.prototype.exec = function(file, args, options, callback) {
- var callback = arguments[arguments.length - 1]
- , ps = this.spawn(file, args, options);
-
- ps.on('error', function(err) {
- if (!callback) return;
- return callback(err, false);
- });
-
- ps.on('exit', function(code) {
- if (!callback) return;
- return callback(null, code === 0);
- });
-
- return ps;
-};
-
-Screen.prototype.readEditor = function(options, callback) {
- if (typeof options === 'string') {
- options = { editor: options };
- }
-
- if (!callback) {
- callback = options;
- options = null;
- }
-
- if (!callback) {
- callback = function() {};
- }
-
- options = options || {};
-
- var self = this
- , fs = require('fs')
- , editor = options.editor || process.env.EDITOR || 'vi'
- , name = options.name || process.title || 'blessed'
- , rnd = Math.random().toString(36).split('.').pop()
- , file = '/tmp/' + name + '.' + rnd
- , args = [file]
- , opt;
-
- opt = {
- stdio: 'inherit',
- env: process.env,
- cwd: process.env.HOME
- };
-
- function writeFile(callback) {
- if (!options.value) return callback();
- return fs.writeFile(file, options.value, callback);
- }
-
- return writeFile(function(err) {
- if (err) return callback(err);
- return self.exec(editor, args, opt, function(err, success) {
- if (err) return callback(err);
- return fs.readFile(file, 'utf8', function(err, data) {
- return fs.unlink(file, function() {
- if (!success) return callback(new Error('Unsuccessful.'));
- if (err) return callback(err);
- return callback(null, data);
- });
- });
- });
- });
-};
-
-Screen.prototype.displayImage = function(file, callback) {
- var self = this;
-
- if (!file) {
- if (!callback) return;
- return callback(new Error('No image.'));
- }
-
- var file = path.resolve(process.cwd(), file);
-
- if (!~file.indexOf('://')) {
- file = 'file://' + file;
- }
-
- var args = ['w3m', '-T', 'text/html'];
-
- var input = '
press q to exit'
- + '';
-
- var opt = {
- stdio: ['pipe', 1, 2],
- env: process.env,
- cwd: process.env.HOME
- };
-
- var ps = this.spawn(args[0], args.slice(1), opt);
-
- ps.on('error', function(err) {
- if (!callback) return;
- return callback(err);
- });
-
- ps.on('exit', function(code) {
- if (!callback) return;
- if (code !== 0) return callback(new Error('Exit Code: ' + code));
- return callback(null, code === 0);
- });
-
- ps.stdin.write(input + '\n');
- ps.stdin.end();
-};
-
-Screen.prototype.setEffects = function(el, fel, over, out, effects, temp) {
- if (!effects) return;
-
- var tmp = {};
- if (temp) el[temp] = tmp;
-
- if (typeof el !== 'function') {
- var _el = el;
- el = function() { return _el; };
- }
-
- fel.on(over, function() {
- var element = el();
- Object.keys(effects).forEach(function(key) {
- var val = effects[key];
- if (val !== null && typeof val === 'object') {
- tmp[key] = tmp[key] || {};
- // element.style[key] = element.style[key] || {};
- Object.keys(val).forEach(function(k) {
- var v = val[k];
- tmp[key][k] = element.style[key][k];
- element.style[key][k] = v;
- });
- return;
- }
- tmp[key] = element.style[key];
- element.style[key] = val;
- });
- element.screen.render();
- });
-
- fel.on(out, function() {
- var element = el();
- Object.keys(effects).forEach(function(key) {
- var val = effects[key];
- if (val !== null && typeof val === 'object') {
- tmp[key] = tmp[key] || {};
- // element.style[key] = element.style[key] || {};
- Object.keys(val).forEach(function(k) {
- if (tmp[key].hasOwnProperty(k)) {
- element.style[key][k] = tmp[key][k];
- }
- });
- return;
- }
- if (tmp.hasOwnProperty(key)) {
- element.style[key] = tmp[key];
- }
- });
- element.screen.render();
- });
-};
-
-Screen.prototype.sigtstp = function(callback) {
- var self = this;
- this.program.sigtstp(function() {
- self.alloc();
- self.render();
- self.program.lrestoreCursor('pause', true);
- if (callback) callback();
- });
-};
-
-Screen.prototype.copyToClipboard = function(text) {
- return this.program.copyToClipboard(text);
-};
-
-Screen.prototype.cursorShape = function(shape, blink) {
- var self = this;
-
- this.cursor.shape = shape || 'block';
- this.cursor.blink = blink || false;
- this.cursor._set = true;
-
- if (this.cursor.artificial) {
- if (!this.program.hideCursor_old) {
- var hideCursor = this.program.hideCursor;
- this.program.hideCursor_old = this.program.hideCursor;
- this.program.hideCursor = function() {
- hideCursor.call(self.program);
- self.cursor._hidden = true;
- if (self.renders) self.render();
- };
- }
- if (!this.program.showCursor_old) {
- var showCursor = this.program.showCursor;
- this.program.showCursor_old = this.program.showCursor;
- this.program.showCursor = function() {
- self.cursor._hidden = false;
- if (self.program._exiting) showCursor.call(self.program);
- if (self.renders) self.render();
- };
- }
- if (!this._cursorBlink) {
- this._cursorBlink = setInterval(function() {
- if (!self.cursor.blink) return;
- self.cursor._state ^= 1;
- if (self.renders) self.render();
- }, 500);
- this._cursorBlink.unref();
- }
- return true;
- }
-
- return this.program.cursorShape(this.cursor.shape, this.cursor.blink);
-};
-
-Screen.prototype.cursorColor = function(color) {
- this.cursor.color = color != null
- ? colors.convert(color)
- : null;
- this.cursor._set = true;
-
- if (this.cursor.artificial) {
- return true;
- }
-
- return this.program.cursorColor(colors.ncolors[this.cursor.color]);
-};
-
-Screen.prototype.cursorReset =
-Screen.prototype.resetCursor = function() {
- this.cursor.shape = 'block';
- this.cursor.blink = false;
- this.cursor.color = null;
- this.cursor._set = false;
-
- if (this.cursor.artificial) {
- this.cursor.artificial = false;
- if (this.program.hideCursor_old) {
- this.program.hideCursor = this.program.hideCursor_old;
- delete this.program.hideCursor_old;
- }
- if (this.program.showCursor_old) {
- this.program.showCursor = this.program.showCursor_old;
- delete this.program.showCursor_old;
- }
- if (this._cursorBlink) {
- clearInterval(this._cursorBlink);
- delete this._cursorBlink;
- }
- return true;
- }
-
- return this.program.cursorReset();
-};
-
-Screen.prototype._cursorAttr = function(cursor, dattr) {
- var attr = dattr || this.dattr
- , cattr
- , ch;
-
- if (cursor.shape === 'line') {
- attr &= ~(0x1ff << 9);
- attr |= 7 << 9;
- ch = '\u2502';
- } else if (cursor.shape === 'underline') {
- attr &= ~(0x1ff << 9);
- attr |= 7 << 9;
- attr |= 2 << 18;
- } else if (cursor.shape === 'block') {
- attr &= ~(0x1ff << 9);
- attr |= 7 << 9;
- attr |= 8 << 18;
- } else if (typeof cursor.shape === 'object' && cursor.shape) {
- cattr = Element.prototype.sattr.call(cursor, cursor.shape);
-
- if (cursor.shape.bold || cursor.shape.underline
- || cursor.shape.blink || cursor.shape.inverse
- || cursor.shape.invisible) {
- attr &= ~(0x1ff << 18);
- attr |= ((cattr >> 18) & 0x1ff) << 18;
- }
-
- if (cursor.shape.fg) {
- attr &= ~(0x1ff << 9);
- attr |= ((cattr >> 9) & 0x1ff) << 9;
- }
-
- if (cursor.shape.bg) {
- attr &= ~(0x1ff << 0);
- attr |= cattr & 0x1ff;
- }
-
- if (cursor.shape.ch) {
- ch = cursor.shape.ch;
- }
- }
-
- if (cursor.color != null) {
- attr &= ~(0x1ff << 9);
- attr |= cursor.color << 9;
- }
-
- return {
- ch: ch,
- attr: attr
- };
-};
-
-Screen.prototype.screenshot = function(xi, xl, yi, yl, term) {
- if (xi == null) xi = 0;
- if (xl == null) xl = this.cols;
- if (yi == null) yi = 0;
- if (yl == null) yl = this.rows;
-
- if (xi < 0) xi = 0;
- if (yi < 0) yi = 0;
-
- var x
- , y
- , line
- , out
- , ch
- , data
- , attr;
-
- var sdattr = this.dattr;
-
- if (term) {
- this.dattr = term.defAttr;
- }
-
- var main = '';
-
- for (y = yi; y < yl; y++) {
- line = term
- ? term.lines[y]
- : this.lines[y];
-
- if (!line) break;
-
- out = '';
- attr = this.dattr;
-
- for (x = xi; x < xl; x++) {
- if (!line[x]) break;
-
- data = line[x][0];
- ch = line[x][1];
-
- if (data !== attr) {
- if (attr !== this.dattr) {
- out += '\x1b[m';
- }
- if (data !== this.dattr) {
- var _data = data;
- if (term) {
- if (((_data >> 9) & 0x1ff) === 257) _data |= 0x1ff << 9;
- if ((_data & 0x1ff) === 256) _data |= 0x1ff;
- }
- out += this.codeAttr(_data);
- }
- }
-
- if (this.fullUnicode) {
- if (unicode.charWidth(line[x][1]) === 2) {
- if (x === xl - 1) {
- ch = ' ';
- } else {
- x++;
- }
- }
- }
-
- out += ch;
- attr = data;
- }
-
- if (attr !== this.dattr) {
- out += '\x1b[m';
- }
-
- if (out) {
- main += (y > 0 ? '\n' : '') + out;
- }
- }
-
- main = main.replace(/(?:\s*\x1b\[40m\s*\x1b\[m\s*)*$/, '') + '\n';
-
- if (term) {
- this.dattr = sdattr;
- }
-
- return main;
-};
-
-/**
- * Positioning
- */
-
-Screen.prototype._getPos = function() {
- return this;
-};
-
-/**
- * Element
- */
-
-function Element(options) {
- var self = this;
-
- if (!(this instanceof Node)) {
- return new Element(options);
- }
-
- options = options || {};
-
- // Workaround to get a `scrollable` option.
- if (options.scrollable && !this._ignore && this.type !== 'scrollable-box') {
- Object.getOwnPropertyNames(ScrollableBox.prototype).forEach(function(key) {
- if (key === 'type') return;
- Object.defineProperty(this, key,
- Object.getOwnPropertyDescriptor(ScrollableBox.prototype, key));
- }, this);
- this._ignore = true;
- ScrollableBox.call(this, options);
- delete this._ignore;
- return this;
- }
-
- Node.call(this, options);
-
- this.name = options.name;
-
- options.position = options.position || {
- left: options.left,
- right: options.right,
- top: options.top,
- bottom: options.bottom,
- width: options.width,
- height: options.height
- };
-
- if (options.position.width === 'shrink'
- || options.position.height === 'shrink') {
- if (options.position.width === 'shrink') {
- delete options.position.width;
- }
- if (options.position.height === 'shrink') {
- delete options.position.height;
- }
- options.shrink = true;
- }
-
- this.position = options.position;
-
- this.noOverflow = options.noOverflow;
- this.dockBorders = options.dockBorders;
- this.shadow = options.shadow;
-
- this.style = options.style;
-
- if (!this.style) {
- this.style = {};
- this.style.fg = options.fg;
- this.style.bg = options.bg;
- this.style.bold = options.bold;
- this.style.underline = options.underline;
- this.style.blink = options.blink;
- this.style.inverse = options.inverse;
- this.style.invisible = options.invisible;
- this.style.transparent = options.transparent;
- }
-
- this.hidden = options.hidden || false;
- this.fixed = options.fixed || false;
- this.align = options.align || 'left';
- this.valign = options.valign || 'top';
- this.wrap = options.wrap !== false;
- this.shrink = options.shrink;
- this.fixed = options.fixed;
- this.ch = options.ch || ' ';
-
- if (typeof options.padding === 'number' || !options.padding) {
- options.padding = {
- left: options.padding,
- top: options.padding,
- right: options.padding,
- bottom: options.padding
- };
- }
-
- this.padding = {
- left: options.padding.left || 0,
- top: options.padding.top || 0,
- right: options.padding.right || 0,
- bottom: options.padding.bottom || 0
- };
-
- this.border = options.border;
- if (this.border) {
- if (typeof this.border === 'string') {
- this.border = { type: this.border };
- }
- this.border.type = this.border.type || 'bg';
- if (this.border.type === 'ascii') this.border.type = 'line';
- this.border.ch = this.border.ch || ' ';
- this.style.border = this.style.border || this.border.style;
- if (!this.style.border) {
- this.style.border = {};
- this.style.border.fg = this.border.fg;
- this.style.border.bg = this.border.bg;
- }
- //this.border.style = this.style.border;
- if (this.border.left == null) this.border.left = true;
- if (this.border.top == null) this.border.top = true;
- if (this.border.right == null) this.border.right = true;
- if (this.border.bottom == null) this.border.bottom = true;
- }
-
- if (options.clickable) {
- this.screen._listenMouse(this);
- }
-
- if (options.input || options.keyable) {
- this.screen._listenKeys(this);
- }
-
- this.parseTags = options.parseTags || options.tags;
-
- this.setContent(options.content || '', true);
-
- if (options.label) {
- this.setLabel(options.label);
- }
-
- if (options.hoverText) {
- this.setHover(options.hoverText);
- }
-
- // TODO: Possibly move this to Node for onScreenEvent('mouse', ...).
- this.on('newListener', function fn(type) {
- // type = type.split(' ').slice(1).join(' ');
- if (type === 'mouse'
- || type === 'click'
- || type === 'mouseover'
- || type === 'mouseout'
- || type === 'mousedown'
- || type === 'mouseup'
- || type === 'mousewheel'
- || type === 'wheeldown'
- || type === 'wheelup'
- || type === 'mousemove') {
- self.screen._listenMouse(self);
- } else if (type === 'keypress' || type.indexOf('key ') === 0) {
- self.screen._listenKeys(self);
- }
- });
-
- this.on('resize', function() {
- self.parseContent();
- });
-
- this.on('attach', function() {
- self.parseContent();
- });
-
- this.on('detach', function() {
- delete self.lpos;
- });
-
- if (options.hoverBg != null) {
- options.hoverEffects = options.hoverEffects || {};
- options.hoverEffects.bg = options.hoverBg;
- }
-
- if (this.style.hover) {
- options.hoverEffects = this.style.hover;
- }
-
- if (this.style.focus) {
- options.focusEffects = this.style.focus;
- }
-
- if (options.effects) {
- if (options.effects.hover) options.hoverEffects = options.effects.hover;
- if (options.effects.focus) options.focusEffects = options.effects.focus;
- }
-
- [['hoverEffects', 'mouseover', 'mouseout', '_htemp'],
- ['focusEffects', 'focus', 'blur', '_ftemp']].forEach(function(props) {
- var pname = props[0], over = props[1], out = props[2], temp = props[3];
- self.screen.setEffects(self, self, over, out, self.options[pname], temp);
- });
-
- if (this.options.draggable) {
- this.draggable = true;
- }
-
- if (options.focused) {
- this.focus();
- }
-}
-
-Element.prototype.__proto__ = Node.prototype;
-
-Element.prototype.type = 'element';
-
-Element.prototype.__defineGetter__('focused', function() {
- return this.screen.focused === this;
-});
-
-Element.prototype.sattr = function(style, fg, bg) {
- var bold = style.bold
- , underline = style.underline
- , blink = style.blink
- , inverse = style.inverse
- , invisible = style.invisible;
-
- // if (arguments.length === 1) {
- if (fg == null && bg == null) {
- fg = style.fg;
- bg = style.bg;
- }
-
- // This used to be a loop, but I decided
- // to unroll it for performance's sake.
- if (typeof bold === 'function') bold = bold(this);
- if (typeof underline === 'function') underline = underline(this);
- if (typeof blink === 'function') blink = blink(this);
- if (typeof inverse === 'function') inverse = inverse(this);
- if (typeof invisible === 'function') invisible = invisible(this);
-
- if (typeof fg === 'function') fg = fg(this);
- if (typeof bg === 'function') bg = bg(this);
-
- // return (this.uid << 24)
- // | ((this.dockBorders ? 32 : 0) << 18)
- return ((invisible ? 16 : 0) << 18)
- | ((inverse ? 8 : 0) << 18)
- | ((blink ? 4 : 0) << 18)
- | ((underline ? 2 : 0) << 18)
- | ((bold ? 1 : 0) << 18)
- | (colors.convert(fg) << 9)
- | colors.convert(bg);
-};
-
-Element.prototype.onScreenEvent = function(type, handler) {
- var listeners = this._slisteners = this._slisteners || [];
- listeners.push({ type: type, handler: handler });
- this.screen.on(type, handler);
-};
-
-Element.prototype.onceScreenEvent = function(type, handler) {
- var listeners = this._slisteners = this._slisteners || [];
- var entry = { type: type, handler: handler };
- listeners.push(entry);
- this.screen.once(type, function() {
- var i = listeners.indexOf(entry);
- if (~i) listeners.splice(i, 1);
- return handler.apply(this, arguments);
- });
-};
-
-Element.prototype.removeScreenEvent = function(type, handler) {
- var listeners = this._slisteners = this._slisteners || [];
- for (var i = 0; i < listeners.length; i++) {
- var listener = listeners[i];
- if (listener.type === type && listener.handler === handler) {
- listeners.splice(i, 1);
- if (this._slisteners.length === 0) {
- delete this._slisteners;
- }
- break;
- }
- }
- this.screen.removeListener(type, handler);
-};
-
-Element.prototype.free = function() {
- var listeners = this._slisteners = this._slisteners || [];
- for (var i = 0; i < listeners.length; i++) {
- var listener = listeners[i];
- this.screen.removeListener(listener.type, listener.handler);
- }
- delete this._slisteners;
-};
-
-Element.prototype.destroy = function() {
- this.detach();
- this.free();
- this.emit('destroy');
-};
-
-Element.prototype.hide = function() {
- if (this.hidden) return;
- this.clearPos();
- this.hidden = true;
- this.emit('hide');
- if (this.screen.focused === this) {
- this.screen.rewindFocus();
- }
-};
-
-Element.prototype.show = function() {
- if (!this.hidden) return;
- this.hidden = false;
- this.emit('show');
-};
-
-Element.prototype.toggle = function() {
- return this.hidden ? this.show() : this.hide();
-};
-
-Element.prototype.focus = function() {
- return this.screen.focused = this;
-};
-
-Element.prototype.setContent = function(content, noClear, noTags) {
- if (!noClear) this.clearPos();
- this.content = content || '';
- this.parseContent(noTags);
- this.emit('set content');
-};
-
-Element.prototype.getContent = function() {
- return this._clines.fake.join('\n');
-};
-
-Element.prototype.setText = function(content, noClear) {
- content = content || '';
- content = content.replace(/\x1b\[[\d;]*m/g, '');
- return this.setContent(content, noClear, true);
-};
-
-Element.prototype.getText = function() {
- return this.getContent().replace(/\x1b\[[\d;]*m/g, '');
-};
-
-Element.prototype.parseContent = function(noTags) {
- if (this.detached) return false;
-
- var width = this.width - this.iwidth;
- if (this._clines == null
- || this._clines.width !== width
- || this._clines.content !== this.content) {
- var content = this.content;
-
- content = content
- .replace(/[\x00-\x08\x0b-\x0c\x0e-\x1a\x1c-\x1f\x7f]/g, '')
- .replace(/\x1b(?!\[[\d;]*m)/g, '')
- .replace(/\r\n|\r/g, '\n')
- .replace(/\t/g, this.screen.tabc);
-
- if (this.screen.fullUnicode) {
- // double-width chars will eat the next char after render. create a
- // blank character after it so it doesn't eat the real next char.
- content = content.replace(unicode.chars.all, '$1\x03');
- // iTerm2 cannot render combining characters properly.
- if (this.screen.program.isiTerm2) {
- content = content.replace(unicode.chars.combining, '');
- }
- } else {
- // no double-width: replace them with question-marks.
- content = content.replace(unicode.chars.all, '??');
- // delete combining characters since they're 0-width anyway.
- // NOTE: We could drop this, the non-surrogates would get changed to ? by
- // the unicode filter, and surrogates changed to ? by the surrogate
- // regex. however, the user might expect them to be 0-width.
- // NOTE: Might be better for performance to drop!
- content = content.replace(unicode.chars.combining, '');
- // no surrogate pairs: replace them with question-marks.
- content = content.replace(unicode.chars.surrogate, '?');
- // XXX Deduplicate code here:
- // content = helpers.dropUnicode(content);
- }
-
- if (!noTags) {
- content = this._parseTags(content);
- }
-
- this._clines = this._wrapContent(content, width);
- this._clines.width = width;
- this._clines.content = this.content;
- this._clines.attr = this._parseAttr(this._clines);
- this._clines.ci = [];
- this._clines.reduce(function(total, line) {
- this._clines.ci.push(total);
- return total + line.length + 1;
- }.bind(this), 0);
-
- this._pcontent = this._clines.join('\n');
- this.emit('parsed content');
-
- return true;
- }
-
- // Need to calculate this every time because the default fg/bg may change.
- this._clines.attr = this._parseAttr(this._clines) || this._clines.attr;
-
- return false;
-};
-
-// Convert `{red-fg}foo{/red-fg}` to `\x1b[31mfoo\x1b[39m`.
-Element.prototype._parseTags = function(text) {
- if (!this.parseTags) return text;
- if (!/{\/?[\w\-,;!#]*}/.test(text)) return text;
-
- var program = this.screen.program
- , out = ''
- , state
- , bg = []
- , fg = []
- , flag = []
- , cap
- , slash
- , param
- , attr
- , esc;
-
- for (;;) {
- if (!esc && (cap = /^{escape}/.exec(text))) {
- text = text.substring(cap[0].length);
- esc = true;
- continue;
- }
-
- if (esc && (cap = /^([\s\S]+?){\/escape}/.exec(text))) {
- text = text.substring(cap[0].length);
- out += cap[1];
- esc = false;
- continue;
- }
-
- if (esc) {
- // throw new Error('Unterminated escape tag.');
- out += text;
- break;
- }
-
- if (cap = /^{(\/?)([\w\-,;!#]*)}/.exec(text)) {
- text = text.substring(cap[0].length);
- slash = cap[1] === '/';
- param = cap[2].replace(/-/g, ' ');
-
- if (param === 'open') {
- out += '{';
- continue;
- } else if (param === 'close') {
- out += '}';
- continue;
- }
-
- if (param.slice(-3) === ' bg') state = bg;
- else if (param.slice(-3) === ' fg') state = fg;
- else state = flag;
-
- if (slash) {
- if (!param) {
- out += program._attr('normal');
- bg.length = 0;
- fg.length = 0;
- flag.length = 0;
- } else {
- attr = program._attr(param, false);
- if (attr == null) {
- out += cap[0];
- } else {
- // if (param !== state[state.length - 1]) {
- // throw new Error('Misnested tags.');
- // }
- state.pop();
- if (state.length) {
- out += program._attr(state[state.length - 1]);
- } else {
- out += attr;
- }
- }
- }
- } else {
- if (!param) {
- out += cap[0];
- } else {
- attr = program._attr(param);
- if (attr == null) {
- out += cap[0];
- } else {
- state.push(param);
- out += attr;
- }
- }
- }
-
- continue;
- }
-
- if (cap = /^[\s\S]+?(?={\/?[\w\-,;!#]*})/.exec(text)) {
- text = text.substring(cap[0].length);
- out += cap[0];
- continue;
- }
-
- out += text;
- break;
- }
-
- return out;
-};
-
-Element.prototype._parseAttr = function(lines) {
- var dattr = this.sattr(this.style)
- , attr = dattr
- , attrs = []
- , line
- , i
- , j
- , c;
-
- if (lines[0].attr === attr) {
- return;
- }
-
- for (j = 0; j < lines.length; j++) {
- line = lines[j];
- attrs[j] = attr;
- for (i = 0; i < line.length; i++) {
- if (line[i] === '\x1b') {
- if (c = /^\x1b\[[\d;]*m/.exec(line.substring(i))) {
- attr = this.screen.attrCode(c[0], attr, dattr);
- i += c[0].length - 1;
- }
- }
- }
- }
-
- return attrs;
-};
-
-Element.prototype._align = function(line, width, align) {
- if (!align) return line;
-
- var cline = line.replace(/\x1b\[[\d;]*m/g, '')
- , len = cline.length
- , s = width - len;
-
- if (len === 0) return line;
- if (s < 0) return line;
-
- if (align === 'center') {
- s = Array(((s / 2) | 0) + 1).join(' ');
- return s + line + s;
- } else if (align === 'right') {
- s = Array(s + 1).join(' ');
- return s + line;
- } else if (this.parseTags && ~line.indexOf('{|}')) {
- var parts = line.split('{|}');
- var cparts = cline.split('{|}');
- s = Math.max(width - cparts[0].length - cparts[1].length, 0);
- s = Array(s + 1).join(' ');
- return parts[0] + s + parts[1];
- }
-
- return line;
-};
-
-Element.prototype._wrapContent = function(content, width) {
- var tags = this.parseTags
- , state = this.align
- , wrap = this.wrap
- , margin = 0
- , rtof = []
- , ftor = []
- , fake = []
- , out = []
- , no = 0
- , line
- , align
- , cap
- , total
- , i
- , part
- , j
- , lines
- , rest;
-
- lines = content.split('\n');
-
- if (!content) {
- out.push(content);
- out.rtof = [0];
- out.ftor = [[0]];
- out.fake = lines;
- out.real = out;
- out.mwidth = 0;
- return out;
- }
-
- if (this.scrollbar) margin++;
- if (this.type === 'textarea') margin++;
- if (width > margin) width -= margin;
-
-main:
- for (; no < lines.length; no++) {
- line = lines[no];
- align = state;
-
- ftor.push([]);
-
- // Handle alignment tags.
- if (tags) {
- if (cap = /^{(left|center|right)}/.exec(line)) {
- line = line.substring(cap[0].length);
- align = state = cap[1] !== 'left'
- ? cap[1]
- : null;
- }
- if (cap = /{\/(left|center|right)}$/.exec(line)) {
- line = line.slice(0, -cap[0].length);
- state = null;
- }
- }
-
- // If the string is apparently too long, wrap it.
- while (line.length > width) {
- // Measure the real width of the string.
- for (i = 0, total = 0; i < line.length; i++) {
- while (line[i] === '\x1b') {
- while (line[i] && line[i++] !== 'm');
- }
- if (!line[i]) break;
- if (++total === width) {
- // If we're not wrapping the text, we have to finish up the rest of
- // the control sequences before cutting off the line.
- i++;
- if (!wrap) {
- rest = line.substring(i).match(/\x1b\[[^m]*m/g);
- rest = rest ? rest.join('') : '';
- out.push(this._align(line.substring(0, i) + rest, width, align));
- ftor[no].push(out.length - 1);
- rtof.push(no);
- continue main;
- }
- if (!this.screen.fullUnicode) {
- // Try to find a space to break on.
- if (i !== line.length) {
- j = i;
- while (j > i - 10 && j > 0 && line[--j] !== ' ');
- if (line[j] === ' ') i = j + 1;
- }
- } else {
- // Try to find a character to break on.
- if (i !== line.length) {
- //
- // Compensate for surrogate length
- // counts on wrapping (experimental):
- // NOTE: Could optimize this by putting
- // it in the parent for loop.
- if (unicode.isSurrogate(line, i)) i--;
- for (var s = 0, n = 0; n < i; n++) {
- if (unicode.isSurrogate(line, n)) s++, n++;
- }
- i += s;
- //
- j = i;
- // Break _past_ space.
- // Break _past_ double-width chars.
- // Break _past_ surrogate pairs.
- // Break _past_ combining chars.
- while (j > i - 10 && j > 0) {
- j--;
- if (line[j] === ' '
- || line[j] === '\x03'
- || (unicode.isSurrogate(line, j - 1) && line[j + 1] !== '\x03')
- || unicode.isCombining(line, j)) {
- break;
- }
- }
- if (line[j] === ' '
- || line[j] === '\x03'
- || (unicode.isSurrogate(line, j - 1) && line[j + 1] !== '\x03')
- || unicode.isCombining(line, j)) {
- i = j + 1;
- }
- }
- }
- break;
- }
- }
-
- part = line.substring(0, i);
- line = line.substring(i);
-
- out.push(this._align(part, width, align));
- ftor[no].push(out.length - 1);
- rtof.push(no);
-
- // Make sure we didn't wrap the line to the very end, otherwise
- // we get a pointless empty line after a newline.
- if (line === '') continue main;
-
- // If only an escape code got cut off, at it to `part`.
- if (/^(?:\x1b[\[\d;]*m)+$/.test(line)) {
- out[out.length - 1] += line;
- continue main;
- }
- }
-
- out.push(this._align(line, width, align));
- ftor[no].push(out.length - 1);
- rtof.push(no);
- }
-
- out.rtof = rtof;
- out.ftor = ftor;
- out.fake = lines;
- out.real = out;
-
- out.mwidth = out.reduce(function(current, line) {
- line = line.replace(/\x1b\[[\d;]*m/g, '');
- return line.length > current
- ? line.length
- : current;
- }, 0);
-
- return out;
-};
-
-Element.prototype.__defineGetter__('visible', function() {
- var el = this;
- do {
- if (el.detached) return false;
- if (el.hidden) return false;
- // if (!el.lpos) return false;
- // if (el.position.width === 0 || el.position.height === 0) return false;
- } while (el = el.parent);
- return true;
-});
-
-Element.prototype.__defineGetter__('_detached', function() {
- var el = this;
- do {
- if (el.type === 'screen') return false;
- if (!el.parent) return true;
- } while (el = el.parent);
- return false;
-});
-
-Element.prototype.enableMouse = function() {
- this.screen._listenMouse(this);
-};
-
-Element.prototype.enableKeys = function() {
- this.screen._listenKeys(this);
-};
-
-Element.prototype.enableInput = function() {
- this.screen._listenMouse(this);
- this.screen._listenKeys(this);
-};
-
-Element.prototype.__defineGetter__('draggable', function() {
- return this._draggable === true;
-});
-
-Element.prototype.__defineSetter__('draggable', function(draggable) {
- return draggable ? this.enableDrag(draggable) : this.disableDrag();
-});
-
-Element.prototype.enableDrag = function(verify) {
- var self = this;
-
- if (this._draggable) return true;
-
- if (typeof verify !== 'function') {
- verify = function() { return true; };
- }
-
- this.enableMouse();
-
- this.on('mousedown', this._dragMD = function(data) {
- if (self.screen._dragging) return;
- if (!verify(data)) return;
- self.screen._dragging = self;
- self._drag = {
- x: data.x - self.aleft,
- y: data.y - self.atop
- };
- self.setFront();
- });
-
- this.onScreenEvent('mouse', this._dragM = function(data) {
- if (self.screen._dragging !== self) return;
-
- if (data.action !== 'mousedown') {
- delete self.screen._dragging;
- delete self._drag;
- return;
- }
-
- // This can happen in edge cases where the user is
- // already dragging and element when it is detached.
- if (!self.parent) return;
-
- var ox = self._drag.x
- , oy = self._drag.y
- , px = self.parent.aleft
- , py = self.parent.atop
- , x = data.x - px - ox
- , y = data.y - py - oy;
-
- if (self.position.right != null) {
- if (self.position.left != null) {
- self.width = '100%-' + (self.parent.width - self.width);
- }
- self.position.right = null;
- }
-
- if (self.position.bottom != null) {
- if (self.position.top != null) {
- self.height = '100%-' + (self.parent.height - self.height);
- }
- self.position.bottom = null;
- }
-
- self.rleft = x;
- self.rtop = y;
-
- self.screen.render();
- });
-
- return this._draggable = true;
-};
-
-Element.prototype.disableDrag = function() {
- if (!this._draggable) return false;
- delete this.screen._dragging;
- delete this._drag;
- this.removeListener('mousedown', this._dragMD);
- this.removeScreenEvent('mouse', this._dragM);
- return this._draggable = false;
-};
-
-Element.prototype.key = function(key, listener) {
- // return this.screen.program.key.apply(this, arguments);
- if (typeof key === 'string') key = key.split(/\s*,\s*/);
- key.forEach(function(key) {
- return this.onScreenEvent('key ' + key, listener);
- }, this);
-};
-
-Element.prototype.onceKey = function(key, listener) {
- // return this.screen.program.onceKey.apply(this, arguments);
- if (typeof key === 'string') key = key.split(/\s*,\s*/);
- key.forEach(function(key) {
- return this.onceScreenEvent('key ' + key, listener);
- }, this);
-};
-
-Element.prototype.unkey =
-Element.prototype.removeKey = function(key, listener) {
- // return this.screen.program.unkey.apply(this, arguments);
- if (typeof key === 'string') key = key.split(/\s*,\s*/);
- key.forEach(function(key) {
- return this.removeScreenEvent('key ' + key, listener);
- }, this);
-};
-
-Element.prototype.setIndex = function(index) {
- if (!this.parent) return;
-
- if (index < 0) {
- index = this.parent.children.length + index;
- }
-
- index = Math.max(index, 0);
- index = Math.min(index, this.parent.children.length - 1);
-
- var i = this.parent.children.indexOf(this);
- if (!~i) return;
-
- var item = this.parent.children.splice(i, 1)[0]
- this.parent.children.splice(index, 0, item);
-};
-
-Element.prototype.setFront = function() {
- return this.setIndex(-1);
-};
-
-Element.prototype.setBack = function() {
- return this.setIndex(0);
-};
-
-Element.prototype.clearPos = function(get, override) {
- if (this.detached) return;
- var lpos = this._getCoords(get);
- if (!lpos) return;
- this.screen.clearRegion(
- lpos.xi, lpos.xl,
- lpos.yi, lpos.yl,
- override);
-};
-
-Element.prototype.setLabel = function(options) {
- var self = this;
-
- if (typeof options === 'string') {
- options = { text: options };
- }
-
- if (this._label) {
- this._label.setContent(options.text);
- if (options.side !== 'right') {
- this._label.rleft = 2 + (this.border ? -1 : 0);
- this._label.position.right = undefined;
- if (!this.screen.autoPadding) {
- this._label.rleft = 2;
- }
- } else {
- this._label.rright = 2 + (this.border ? -1 : 0);
- this._label.position.left = undefined;
- if (!this.screen.autoPadding) {
- this._label.rright = 2;
- }
- }
- return;
- }
-
- this._label = new Box({
- screen: this.screen,
- parent: this,
- content: options.text,
- top: this.border ? -1 : 0,
- tags: this.parseTags,
- shrink: true,
- style: this.style.label
- });
-
- if (options.side !== 'right') {
- this._label.rleft = 2 + (this.border ? -1 : 0);
- } else {
- this._label.rright = 2 + (this.border ? -1 : 0);
- }
-
- this._label._isLabel = true;
-
- if (!this.screen.autoPadding) {
- if (options.side !== 'right') {
- this._label.rleft = 2;
- } else {
- this._label.rright = 2;
- }
- this._label.rtop = 0;
- }
-
- var reposition = function() {
- var visible = self.height - self.iheight;
- self._label.rtop = (self.childBase || 0) - (self.border ? 1 : 0);
- if (!self.screen.autoPadding) {
- self._label.rtop = (self.childBase || 0);
- }
- self.screen.render();
- };
-
- this.on('scroll', function() {
- reposition();
- });
-
- this.on('resize', function() {
- nextTick(function() {
- reposition();
- });
- });
-};
-
-Element.prototype.removeLabel = function() {
- if (!this._label) return;
- this._label.detach();
- delete this._label;
-};
-
-Element.prototype.setHover = function(options) {
- var self = this;
-
- if (typeof options === 'string') {
- options = { text: options };
- }
-
- this._hoverOptions = options;
- this.enableMouse();
- this.screen._initHover();
-};
-
-Element.prototype.removeHover = function() {
- delete this._hoverOptions;
- if (!this.screen._hoverText || this.screen._hoverText.detached) return;
- this.screen._hoverText.detach();
- this.screen.render();
-};
-
-/**
- * Positioning
- */
-
-// The below methods are a bit confusing: basically
-// whenever Box.render is called `lpos` gets set on
-// the element, an object containing the rendered
-// coordinates. Since these don't update if the
-// element is moved somehow, they're unreliable in
-// that situation. However, if we can guarantee that
-// lpos is good and up to date, it can be more
-// accurate than the calculated positions below.
-// In this case, if the element is being rendered,
-// it's guaranteed that the parent will have been
-// rendered first, in which case we can use the
-// parant's lpos instead of recalculating it's
-// position (since that might be wrong because
-// it doesn't handle content shrinkage).
-
-Element.prototype._getPos = function() {
- var pos = this.lpos;
-
- assert.ok(pos);
-
- if (pos.aleft != null) return pos;
-
- pos.aleft = pos.xi;
- pos.atop = pos.yi;
- pos.aright = this.screen.cols - pos.xl;
- pos.abottom = this.screen.rows - pos.yl;
- pos.width = pos.xl - pos.xi;
- pos.height = pos.yl - pos.yi;
-
- return pos;
-};
-
-/**
- * Position Getters
- */
-
-Element.prototype._getWidth = function(get) {
- var parent = get ? this.parent._getPos() : this.parent
- , width = this.position.width
- , left
- , expr;
-
- if (typeof width === 'string') {
- if (width === 'half') width = '50%';
- expr = width.split(/(?=\+|-)/);
- width = expr[0];
- width = +width.slice(0, -1) / 100;
- width = parent.width * width | 0;
- width += +(expr[1] || 0);
- return width;
- }
-
- // This is for if the element is being streched or shrunken.
- // Although the width for shrunken elements is calculated
- // in the render function, it may be calculated based on
- // the content width, and the content width is initially
- // decided by the width the element, so it needs to be
- // calculated here.
- if (width == null) {
- left = this.position.left || 0;
- if (typeof left === 'string') {
- if (left === 'center') left = '50%';
- expr = left.split(/(?=\+|-)/);
- left = expr[0];
- left = +left.slice(0, -1) / 100;
- left = parent.width * left | 0;
- left += +(expr[1] || 0);
- }
- width = parent.width - (this.position.right || 0) - left;
- if (this.screen.autoPadding) {
- if ((this.position.left != null || this.position.right == null)
- && this.position.left !== 'center') {
- width -= this.parent.ileft;
- }
- width -= this.parent.iright;
- }
- }
-
- return width;
-};
-
-Element.prototype.__defineGetter__('width', function() {
- return this._getWidth(false);
-});
-
-Element.prototype._getHeight = function(get) {
- var parent = get ? this.parent._getPos() : this.parent
- , height = this.position.height
- , top
- , expr;
-
- if (typeof height === 'string') {
- if (height === 'half') height = '50%';
- expr = height.split(/(?=\+|-)/);
- height = expr[0];
- height = +height.slice(0, -1) / 100;
- height = parent.height * height | 0;
- height += +(expr[1] || 0);
- return height;
- }
-
- // This is for if the element is being streched or shrunken.
- // Although the width for shrunken elements is calculated
- // in the render function, it may be calculated based on
- // the content width, and the content width is initially
- // decided by the width the element, so it needs to be
- // calculated here.
- if (height == null) {
- top = this.position.top || 0;
- if (typeof top === 'string') {
- if (top === 'center') top = '50%';
- expr = top.split(/(?=\+|-)/);
- top = expr[0];
- top = +top.slice(0, -1) / 100;
- top = parent.height * top | 0;
- top += +(expr[1] || 0);
- }
- height = parent.height - (this.position.bottom || 0) - top;
- if (this.screen.autoPadding) {
- if ((this.position.top != null
- || this.position.bottom == null)
- && this.position.top !== 'center') {
- height -= this.parent.itop;
- }
- height -= this.parent.ibottom;
- }
- }
-
- return height;
-};
-
-Element.prototype.__defineGetter__('height', function() {
- return this._getHeight(false);
-});
-
-Element.prototype._getLeft = function(get) {
- var parent = get ? this.parent._getPos() : this.parent
- , left = this.position.left || 0
- , expr;
-
- if (typeof left === 'string') {
- if (left === 'center') left = '50%';
- expr = left.split(/(?=\+|-)/);
- left = expr[0];
- left = +left.slice(0, -1) / 100;
- left = parent.width * left | 0;
- left += +(expr[1] || 0);
- if (this.position.left === 'center') {
- left -= this._getWidth(get) / 2 | 0;
- }
- }
-
- if (this.position.left == null && this.position.right != null) {
- return this.screen.cols - this._getWidth(get) - this._getRight(get);
- }
-
- if (this.screen.autoPadding) {
- if ((this.position.left != null
- || this.position.right == null)
- && this.position.left !== 'center') {
- left += this.parent.ileft;
- }
- }
-
- return (parent.aleft || 0) + left;
-};
-
-Element.prototype.__defineGetter__('aleft', function() {
- return this._getLeft(false);
-});
-
-Element.prototype._getRight = function(get) {
- var parent = get ? this.parent._getPos() : this.parent
- , right;
-
- if (this.position.right == null && this.position.left != null) {
- right = this.screen.cols - (this._getLeft(get) + this._getWidth(get));
- if (this.screen.autoPadding) {
- right += this.parent.iright;
- }
- return right;
- }
-
- right = (parent.aright || 0) + (this.position.right || 0);
-
- if (this.screen.autoPadding) {
- right += this.parent.iright;
- }
-
- return right;
-};
-
-Element.prototype.__defineGetter__('aright', function() {
- return this._getRight(false);
-});
-
-Element.prototype._getTop = function(get) {
- var parent = get ? this.parent._getPos() : this.parent
- , top = this.position.top || 0
- , expr;
-
- if (typeof top === 'string') {
- if (top === 'center') top = '50%';
- expr = top.split(/(?=\+|-)/);
- top = expr[0];
- top = +top.slice(0, -1) / 100;
- top = parent.height * top | 0;
- top += +(expr[1] || 0);
- if (this.position.top === 'center') {
- top -= this._getHeight(get) / 2 | 0;
- }
- }
-
- if (this.position.top == null && this.position.bottom != null) {
- return this.screen.rows - this._getHeight(get) - this._getBottom(get);
- }
-
- if (this.screen.autoPadding) {
- if ((this.position.top != null
- || this.position.bottom == null)
- && this.position.top !== 'center') {
- top += this.parent.itop;
- }
- }
-
- return (parent.atop || 0) + top;
-};
-
-Element.prototype.__defineGetter__('atop', function() {
- return this._getTop(false);
-});
-
-Element.prototype._getBottom = function(get) {
- var parent = get ? this.parent._getPos() : this.parent
- , bottom;
-
- if (this.position.bottom == null && this.position.top != null) {
- bottom = this.screen.rows - (this._getTop(get) + this._getHeight(get));
- if (this.screen.autoPadding) {
- bottom += this.parent.ibottom;
- }
- return bottom;
- }
-
- bottom = (parent.abottom || 0) + (this.position.bottom || 0);
-
- if (this.screen.autoPadding) {
- bottom += this.parent.ibottom;
- }
-
- return bottom;
-};
-
-Element.prototype.__defineGetter__('abottom', function() {
- return this._getBottom(false);
-});
-
-Element.prototype.__defineGetter__('rleft', function() {
- return this.aleft - this.parent.aleft;
-});
-
-Element.prototype.__defineGetter__('rright', function() {
- return this.aright - this.parent.aright;
-});
-
-Element.prototype.__defineGetter__('rtop', function() {
- return this.atop - this.parent.atop;
-});
-
-Element.prototype.__defineGetter__('rbottom', function() {
- return this.abottom - this.parent.abottom;
-});
-
-/**
- * Position Setters
- */
-
-// NOTE:
-// For aright, abottom, right, and bottom:
-// If position.bottom is null, we could simply set top instead.
-// But it wouldn't replicate bottom behavior appropriately if
-// the parent was resized, etc.
-Element.prototype.__defineSetter__('width', function(val) {
- if (this.position.width === val) return;
- if (/^\d+$/.test(val)) val = +val;
- this.emit('resize');
- this.clearPos();
- return this.position.width = val;
-});
-
-Element.prototype.__defineSetter__('height', function(val) {
- if (this.position.height === val) return;
- if (/^\d+$/.test(val)) val = +val;
- this.emit('resize');
- this.clearPos();
- return this.position.height = val;
-});
-
-Element.prototype.__defineSetter__('aleft', function(val) {
- var expr;
- if (typeof val === 'string') {
- if (val === 'center') {
- val = this.screen.width / 2 | 0;
- val -= this.width / 2 | 0;
- } else {
- expr = val.split(/(?=\+|-)/);
- val = expr[0];
- val = +val.slice(0, -1) / 100;
- val = this.screen.width * val | 0;
- val += +(expr[1] || 0);
- }
- }
- val -= this.parent.aleft;
- if (this.position.left === val) return;
- this.emit('move');
- this.clearPos();
- return this.position.left = val;
-});
-
-Element.prototype.__defineSetter__('aright', function(val) {
- val -= this.parent.aright;
- if (this.position.right === val) return;
- this.emit('move');
- this.clearPos();
- return this.position.right = val;
-});
-
-Element.prototype.__defineSetter__('atop', function(val) {
- var expr;
- if (typeof val === 'string') {
- if (val === 'center') {
- val = this.screen.height / 2 | 0;
- val -= this.height / 2 | 0;
- } else {
- expr = val.split(/(?=\+|-)/);
- val = expr[0];
- val = +val.slice(0, -1) / 100;
- val = this.screen.height * val | 0;
- val += +(expr[1] || 0);
- }
- }
- val -= this.parent.atop;
- if (this.position.top === val) return;
- this.emit('move');
- this.clearPos();
- return this.position.top = val;
-});
-
-Element.prototype.__defineSetter__('abottom', function(val) {
- val -= this.parent.abottom;
- if (this.position.bottom === val) return;
- this.emit('move');
- this.clearPos();
- return this.position.bottom = val;
-});
-
-Element.prototype.__defineSetter__('rleft', function(val) {
- if (this.position.left === val) return;
- if (/^\d+$/.test(val)) val = +val;
- this.emit('move');
- this.clearPos();
- return this.position.left = val;
-});
-
-Element.prototype.__defineSetter__('rright', function(val) {
- if (this.position.right === val) return;
- this.emit('move');
- this.clearPos();
- return this.position.right = val;
-});
-
-Element.prototype.__defineSetter__('rtop', function(val) {
- if (this.position.top === val) return;
- if (/^\d+$/.test(val)) val = +val;
- this.emit('move');
- this.clearPos();
- return this.position.top = val;
-});
-
-Element.prototype.__defineSetter__('rbottom', function(val) {
- if (this.position.bottom === val) return;
- this.emit('move');
- this.clearPos();
- return this.position.bottom = val;
-});
-
-Element.prototype.__defineGetter__('ileft', function() {
- return (this.border ? 1 : 0) + this.padding.left;
- // return (this.border && this.border.left ? 1 : 0) + this.padding.left;
-});
-
-Element.prototype.__defineGetter__('itop', function() {
- return (this.border ? 1 : 0) + this.padding.top;
- // return (this.border && this.border.top ? 1 : 0) + this.padding.top;
-});
-
-Element.prototype.__defineGetter__('iright', function() {
- return (this.border ? 1 : 0) + this.padding.right;
- // return (this.border && this.border.right ? 1 : 0) + this.padding.right;
-});
-
-Element.prototype.__defineGetter__('ibottom', function() {
- return (this.border ? 1 : 0) + this.padding.bottom;
- // return (this.border && this.border.bottom ? 1 : 0) + this.padding.bottom;
-});
-
-Element.prototype.__defineGetter__('iwidth', function() {
- // return (this.border
- // ? ((this.border.left ? 1 : 0) + (this.border.right ? 1 : 0)) : 0)
- // + this.padding.left + this.padding.right;
- return (this.border ? 2 : 0) + this.padding.left + this.padding.right;
-});
-
-Element.prototype.__defineGetter__('iheight', function() {
- // return (this.border
- // ? ((this.border.top ? 1 : 0) + (this.border.bottom ? 1 : 0)) : 0)
- // + this.padding.top + this.padding.bottom;
- return (this.border ? 2 : 0) + this.padding.top + this.padding.bottom;
-});
-
-Element.prototype.__defineGetter__('tpadding', function() {
- return this.padding.left + this.padding.top
- + this.padding.right + this.padding.bottom;
-});
-
-/**
- * Relative coordinates as default properties
- */
-
-Element.prototype.__defineGetter__('left', function() {
- return this.rleft;
-});
-
-Element.prototype.__defineGetter__('right', function() {
- return this.rright;
-});
-
-Element.prototype.__defineGetter__('top', function() {
- return this.rtop;
-});
-
-Element.prototype.__defineGetter__('bottom', function() {
- return this.rbottom;
-});
-
-Element.prototype.__defineSetter__('left', function(val) {
- return this.rleft = val;
-});
-
-Element.prototype.__defineSetter__('right', function(val) {
- return this.rright = val;
-});
-
-Element.prototype.__defineSetter__('top', function(val) {
- return this.rtop = val;
-});
-
-Element.prototype.__defineSetter__('bottom', function(val) {
- return this.rbottom = val;
-});
-
-/**
- * Rendering - here be dragons
- */
-
-Element.prototype._getShrinkBox = function(xi, xl, yi, yl, get) {
- if (!this.children.length) {
- return { xi: xi, xl: xi + 1, yi: yi, yl: yi + 1 };
- }
-
- var i, el, ret, mxi = xi, mxl = xi + 1, myi = yi, myl = yi + 1;
-
- // This is a chicken and egg problem. We need to determine how the children
- // will render in order to determine how this element renders, but it in
- // order to figure out how the children will render, they need to know
- // exactly how their parent renders, so, we can give them what we have so
- // far.
- var _lpos;
- if (get) {
- _lpos = this.lpos;
- this.lpos = { xi: xi, xl: xl, yi: yi, yl: yl };
- //this.shrink = false;
- }
-
- for (i = 0; i < this.children.length; i++) {
- el = this.children[i];
-
- ret = el._getCoords(get);
-
- // Or just (seemed to work, but probably not good):
- // ret = el.lpos || this.lpos;
-
- if (!ret) continue;
-
- // Since the parent element is shrunk, and the child elements think it's
- // going to take up as much space as possible, an element anchored to the
- // right or bottom will inadvertantly make the parent's shrunken size as
- // large as possible. So, we can just use the height and/or width the of
- // element.
- // if (get) {
- if (el.position.left == null && el.position.right != null) {
- ret.xl = xi + (ret.xl - ret.xi);
- ret.xi = xi;
- if (this.screen.autoPadding) {
- // Maybe just do this no matter what.
- ret.xl += this.ileft;
- ret.xi += this.ileft;
- }
- }
- if (el.position.top == null && el.position.bottom != null) {
- ret.yl = yi + (ret.yl - ret.yi);
- ret.yi = yi;
- if (this.screen.autoPadding) {
- // Maybe just do this no matter what.
- ret.yl += this.itop;
- ret.yi += this.itop;
- }
- }
-
- if (ret.xi < mxi) mxi = ret.xi;
- if (ret.xl > mxl) mxl = ret.xl;
- if (ret.yi < myi) myi = ret.yi;
- if (ret.yl > myl) myl = ret.yl;
- }
-
- if (get) {
- this.lpos = _lpos;
- //this.shrink = true;
- }
-
- if (this.position.width == null
- && (this.position.left == null
- || this.position.right == null)) {
- if (this.position.left == null && this.position.right != null) {
- xi = xl - (mxl - mxi);
- if (!this.screen.autoPadding) {
- xi -= this.padding.left + this.padding.right;
- } else {
- xi -= this.ileft;
- }
- } else {
- xl = mxl;
- if (!this.screen.autoPadding) {
- xl += this.padding.left + this.padding.right;
- // XXX Temporary workaround until we decide to make autoPadding default.
- // See widget-listtable.js for an example of why this is necessary.
- // XXX Maybe just to this for all this being that this would affect
- // width shrunken normal shrunken lists as well.
- // if (this._isList) {
- if (this.type === 'list-table') {
- xl -= this.padding.left + this.padding.right;
- xl += this.iright;
- }
- } else {
- //xl += this.padding.right;
- xl += this.iright;
- }
- }
- }
-
- if (this.position.height == null
- && (this.position.top == null
- || this.position.bottom == null)
- && (!this.scrollable || this._isList)) {
- // NOTE: Lists get special treatment if they are shrunken - assume they
- // want all list items showing. This is one case we can calculate the
- // height based on items/boxes.
- if (this._isList) {
- myi = 0 - this.itop;
- myl = this.items.length + this.ibottom;
- }
- if (this.position.top == null && this.position.bottom != null) {
- yi = yl - (myl - myi);
- if (!this.screen.autoPadding) {
- yi -= this.padding.top + this.padding.bottom;
- } else {
- yi -= this.itop;
- }
- } else {
- yl = myl;
- if (!this.screen.autoPadding) {
- yl += this.padding.top + this.padding.bottom;
- } else {
- yl += this.ibottom;
- }
- }
- }
-
- return { xi: xi, xl: xl, yi: yi, yl: yl };
-};
-
-Element.prototype._getShrinkContent = function(xi, xl, yi, yl, get) {
- var h = this._clines.length
- , w = this._clines.mwidth || 1;
-
- if (this.position.width == null
- && (this.position.left == null
- || this.position.right == null)) {
- if (this.position.left == null && this.position.right != null) {
- xi = xl - w - this.iwidth;
- } else {
- xl = xi + w + this.iwidth;
- }
- }
-
- if (this.position.height == null
- && (this.position.top == null
- || this.position.bottom == null)
- && (!this.scrollable || this._isList)) {
- if (this.position.top == null && this.position.bottom != null) {
- yi = yl - h - this.iheight;
- } else {
- yl = yi + h + this.iheight;
- }
- }
-
- return { xi: xi, xl: xl, yi: yi, yl: yl };
-};
-
-Element.prototype._getShrink = function(xi, xl, yi, yl, get) {
- var shrinkBox = this._getShrinkBox(xi, xl, yi, yl, get)
- , shrinkContent = this._getShrinkContent(xi, xl, yi, yl, get)
- , xll = xl
- , yll = yl;
-
- // Figure out which one is bigger and use it.
- if (shrinkBox.xl - shrinkBox.xi > shrinkContent.xl - shrinkContent.xi) {
- xi = shrinkBox.xi;
- xl = shrinkBox.xl;
- } else {
- xi = shrinkContent.xi;
- xl = shrinkContent.xl;
- }
-
- if (shrinkBox.yl - shrinkBox.yi > shrinkContent.yl - shrinkContent.yi) {
- yi = shrinkBox.yi;
- yl = shrinkBox.yl;
- } else {
- yi = shrinkContent.yi;
- yl = shrinkContent.yl;
- }
-
- // Recenter shrunken elements.
- if (xl < xll && this.position.left === 'center') {
- xll = (xll - xl) / 2 | 0;
- xi += xll;
- xl += xll;
- }
-
- if (yl < yll && this.position.top === 'center') {
- yll = (yll - yl) / 2 | 0;
- yi += yll;
- yl += yll;
- }
-
- return { xi: xi, xl: xl, yi: yi, yl: yl };
-};
-
-Element.prototype._getCoords = function(get, noscroll) {
- if (this.hidden) return;
-
- // if (this.parent._rendering) {
- // get = true;
- // }
-
- var xi = this._getLeft(get)
- , xl = xi + this._getWidth(get)
- , yi = this._getTop(get)
- , yl = yi + this._getHeight(get)
- , base = this.childBase || 0
- , el = this
- , fixed = this.fixed
- , coords
- , v
- , noleft
- , noright
- , notop
- , nobot
- , ppos
- , b;
-
- // Attempt to shrink the element base on the
- // size of the content and child elements.
- if (this.shrink) {
- coords = this._getShrink(xi, xl, yi, yl, get);
- xi = coords.xi, xl = coords.xl;
- yi = coords.yi, yl = coords.yl;
- }
-
- // Find a scrollable ancestor if we have one.
- while (el = el.parent) {
- if (el.scrollable) {
- if (fixed) {
- fixed = false;
- continue;
- }
- break;
- }
- }
-
- // Check to make sure we're visible and
- // inside of the visible scroll area.
- // NOTE: Lists have a property where only
- // the list items are obfuscated.
-
- // Old way of doing things, this would not render right if a shrunken element
- // with lots of boxes in it was within a scrollable element.
- // See: $ node test/widget-shrink-fail.js
- // var thisparent = this.parent;
-
- var thisparent = el;
- if (el && !noscroll) {
- ppos = thisparent.lpos;
-
- // The shrink option can cause a stack overflow
- // by calling _getCoords on the child again.
- // if (!get && !thisparent.shrink) {
- // ppos = thisparent._getCoords();
- // }
-
- if (!ppos) return;
-
- // TODO: Figure out how to fix base (and cbase to only
- // take into account the *parent's* padding.
-
- yi -= ppos.base;
- yl -= ppos.base;
-
- b = thisparent.border ? 1 : 0;
-
- // XXX
- // Fixes non-`fixed` labels to work with scrolling (they're ON the border):
- // if (this.position.left < 0
- // || this.position.right < 0
- // || this.position.top < 0
- // || this.position.bottom < 0) {
- if (this._isLabel) {
- b = 0;
- }
-
- if (yi < ppos.yi + b) {
- if (yl - 1 < ppos.yi + b) {
- // Is above.
- return;
- } else {
- // Is partially covered above.
- notop = true;
- v = ppos.yi - yi;
- if (this.border) v--;
- if (thisparent.border) v++;
- base += v;
- yi += v;
- }
- } else if (yl > ppos.yl - b) {
- if (yi > ppos.yl - 1 - b) {
- // Is below.
- return;
- } else {
- // Is partially covered below.
- nobot = true;
- v = yl - ppos.yl;
- if (this.border) v--;
- if (thisparent.border) v++;
- yl -= v;
- }
- }
-
- // Shouldn't be necessary.
- // assert.ok(yi < yl);
- if (yi >= yl) return;
-
- // Could allow overlapping stuff in scrolling elements
- // if we cleared the pending buffer before every draw.
- if (xi < el.lpos.xi) {
- xi = el.lpos.xi;
- noleft = true;
- if (this.border) xi--;
- if (thisparent.border) xi++;
- }
- if (xl > el.lpos.xl) {
- xl = el.lpos.xl;
- noright = true;
- if (this.border) xl++;
- if (thisparent.border) xl--;
- }
- //if (xi > xl) return;
- if (xi >= xl) return;
- }
-
- if (this.noOverflow && this.parent.lpos) {
- if (xi < this.parent.lpos.xi + this.parent.ileft) {
- xi = this.parent.lpos.xi + this.parent.ileft;
- }
- if (xl > this.parent.lpos.xl - this.parent.iright) {
- xl = this.parent.lpos.xl - this.parent.iright;
- }
- if (yi < this.parent.lpos.yi + this.parent.itop) {
- yi = this.parent.lpos.yi + this.parent.itop;
- }
- if (yl > this.parent.lpos.yl - this.parent.ibottom) {
- yl = this.parent.lpos.yl - this.parent.ibottom;
- }
- }
-
- // if (this.parent.lpos) {
- // this.parent.lpos._scrollBottom = Math.max(
- // this.parent.lpos._scrollBottom, yl);
- // }
-
- return {
- xi: xi,
- xl: xl,
- yi: yi,
- yl: yl,
- base: base,
- noleft: noleft,
- noright: noright,
- notop: notop,
- nobot: nobot,
- renders: this.screen.renders
- };
-};
-
-Element.prototype.render = function() {
- this._emit('prerender');
-
- this.parseContent();
-
- var coords = this._getCoords(true);
- if (!coords) {
- delete this.lpos;
- return;
- }
-
- if (coords.xl - coords.xi <= 0) {
- coords.xl = Math.max(coords.xl, coords.xi);
- return;
- }
-
- if (coords.yl - coords.yi <= 0) {
- coords.yl = Math.max(coords.yl, coords.yi);
- return;
- }
-
- var lines = this.screen.lines
- , xi = coords.xi
- , xl = coords.xl
- , yi = coords.yi
- , yl = coords.yl
- , x
- , y
- , cell
- , attr
- , ch
- , content = this._pcontent
- , ci = this._clines.ci[coords.base]
- , battr
- , dattr
- , c
- , visible
- , i
- , bch = this.ch;
-
- // Clip content if it's off the edge of the screen
- // if (xi + this.ileft < 0 || yi + this.itop < 0) {
- // var clines = this._clines.slice();
- // if (xi + this.ileft < 0) {
- // for (var i = 0; i < clines.length; i++) {
- // var t = 0;
- // var csi = '';
- // var csis = '';
- // for (var j = 0; j < clines[i].length; j++) {
- // while (clines[i][j] === '\x1b') {
- // csi = '\x1b';
- // while (clines[i][j++] !== 'm') csi += clines[i][j];
- // csis += csi;
- // }
- // if (++t === -(xi + this.ileft) + 1) break;
- // }
- // clines[i] = csis + clines[i].substring(j);
- // }
- // }
- // if (yi + this.itop < 0) {
- // clines = clines.slice(-(yi + this.itop));
- // }
- // content = clines.join('\n');
- // }
-
- if (coords.base >= this._clines.ci.length) {
- ci = this._pcontent.length;
- }
-
- this.lpos = coords;
-
- if (this.border && this.border.type === 'line') {
- this.screen._borderStops[coords.yi] = true;
- this.screen._borderStops[coords.yl - 1] = true;
- // if (!this.screen._borderStops[coords.yi]) {
- // this.screen._borderStops[coords.yi] = { xi: coords.xi, xl: coords.xl };
- // } else {
- // if (this.screen._borderStops[coords.yi].xi > coords.xi) {
- // this.screen._borderStops[coords.yi].xi = coords.xi;
- // }
- // if (this.screen._borderStops[coords.yi].xl < coords.xl) {
- // this.screen._borderStops[coords.yi].xl = coords.xl;
- // }
- // }
- // this.screen._borderStops[coords.yl - 1] = this.screen._borderStops[coords.yi];
- }
-
- dattr = this.sattr(this.style);
- attr = dattr;
-
- // If we're in a scrollable text box, check to
- // see which attributes this line starts with.
- if (ci > 0) {
- attr = this._clines.attr[Math.min(coords.base, this._clines.length - 1)];
- }
-
- if (this.border) xi++, xl--, yi++, yl--;
-
- // If we have padding/valign, that means the
- // content-drawing loop will skip a few cells/lines.
- // To deal with this, we can just fill the whole thing
- // ahead of time. This could be optimized.
- if (this.tpadding || (this.valign && this.valign !== 'top')) {
- if (this.style.transparent) {
- for (y = Math.max(yi, 0); y < yl; y++) {
- if (!lines[y]) break;
- for (x = Math.max(xi, 0); x < xl; x++) {
- if (!lines[y][x]) break;
- lines[y][x][0] = this._blend(attr, lines[y][x][0]);
- lines[y][x][1] = ch;
- lines[y].dirty = true;
- }
- }
- } else {
- this.screen.fillRegion(dattr, bch, xi, xl, yi, yl);
- }
- }
-
- if (this.tpadding) {
- xi += this.padding.left, xl -= this.padding.right;
- yi += this.padding.top, yl -= this.padding.bottom;
- }
-
- // Determine where to place the text if it's vertically aligned.
- if (this.valign === 'middle' || this.valign === 'bottom') {
- visible = yl - yi;
- if (this._clines.length < visible) {
- if (this.valign === 'middle') {
- visible = visible / 2 | 0;
- visible -= this._clines.length / 2 | 0;
- } else if (this.valign === 'bottom') {
- visible -= this._clines.length;
- }
- ci -= visible * (xl - xi);
- }
- }
-
- // Draw the content and background.
- for (y = yi; y < yl; y++) {
- if (!lines[y]) {
- if (y >= this.screen.height || yl < this.ibottom) {
- break;
- } else {
- continue;
- }
- }
- for (x = xi; x < xl; x++) {
- cell = lines[y][x];
- if (!cell) {
- if (x >= this.screen.width || xl < this.iright) {
- break;
- } else {
- continue;
- }
- }
-
- ch = content[ci++] || bch;
-
- // if (!content[ci] && !coords._contentEnd) {
- // coords._contentEnd = { x: x - xi, y: y - yi };
- // }
-
- // Handle escape codes.
- while (ch === '\x1b') {
- if (c = /^\x1b\[[\d;]*m/.exec(content.substring(ci - 1))) {
- ci += c[0].length - 1;
- attr = this.screen.attrCode(c[0], attr, dattr);
- // Ignore foreground changes for selected items.
- if (this.parent._isList && this.parent.interactive
- && this.parent.items[this.parent.selected] === this
- && this.parent.options.invertSelected !== false) {
- attr = (attr & ~(0x1ff << 9)) | (dattr & (0x1ff << 9));
- }
- ch = content[ci] || bch;
- ci++;
- } else {
- break;
- }
- }
-
- // Handle newlines.
- if (ch === '\t') ch = bch;
- if (ch === '\n') {
- // If we're on the first cell and we find a newline and the last cell
- // of the last line was not a newline, let's just treat this like the
- // newline was already "counted".
- if (x === xi && y !== yi && content[ci - 2] !== '\n') {
- x--;
- continue;
- }
- // We could use fillRegion here, name the
- // outer loop, and continue to it instead.
- ch = bch;
- for (; x < xl; x++) {
- cell = lines[y][x];
- if (!cell) break;
- if (this.style.transparent) {
- lines[y][x][0] = this._blend(attr, lines[y][x][0]);
- if (content[ci]) lines[y][x][1] = ch;
- lines[y].dirty = true;
- } else {
- if (attr !== cell[0] || ch !== cell[1]) {
- lines[y][x][0] = attr;
- lines[y][x][1] = ch;
- lines[y].dirty = true;
- }
- }
- }
- continue;
- }
-
- if (this.screen.fullUnicode && content[ci - 1]) {
- var point = unicode.codePointAt(content, ci - 1);
- // Handle combining chars:
- // Make sure they get in the same cell and are counted as 0.
- if (unicode.combining[point]) {
- if (point > 0x00ffff) {
- ch = content[ci - 1] + content[ci];
- ci++;
- }
- if (x - 1 >= xi) {
- lines[y][x - 1][1] += ch;
- } else if (y - 1 >= yi) {
- lines[y - 1][xl - 1][1] += ch;
- }
- x--;
- continue;
- }
- // Handle surrogate pairs:
- // Make sure we put surrogate pair chars in one cell.
- if (point > 0x00ffff) {
- ch = content[ci - 1] + content[ci];
- ci++;
- }
- }
-
- if (this.style.transparent) {
- lines[y][x][0] = this._blend(attr, lines[y][x][0]);
- if (content[ci]) lines[y][x][1] = ch;
- lines[y].dirty = true;
- } else {
- if (attr !== cell[0] || ch !== cell[1]) {
- lines[y][x][0] = attr;
- lines[y][x][1] = ch;
- lines[y].dirty = true;
- }
- }
- }
- }
-
- // Draw the scrollbar.
- // Could possibly draw this after all child elements.
- if (this.scrollbar) {
- // XXX
- // i = this.getScrollHeight();
- i = Math.max(this._clines.length, this._scrollBottom());
- }
- if (coords.notop || coords.nobot) i = -Infinity;
- if (this.scrollbar && (yl - yi) < i) {
- x = xl - 1;
- if (this.scrollbar.ignoreBorder && this.border) x++;
- if (this.alwaysScroll) {
- y = this.childBase / (i - (yl - yi));
- } else {
- y = (this.childBase + this.childOffset) / (i - 1);
- }
- y = yi + ((yl - yi) * y | 0);
- if (y >= yl) y = yl - 1;
- cell = lines[y] && lines[y][x];
- if (cell) {
- if (this.track) {
- ch = this.track.ch || ' ';
- attr = this.sattr(this.style.track,
- this.style.track.fg || this.style.fg,
- this.style.track.bg || this.style.bg);
- this.screen.fillRegion(attr, ch, x, x + 1, yi, yl);
- }
- ch = this.scrollbar.ch || ' ';
- attr = this.sattr(this.style.scrollbar,
- this.style.scrollbar.fg || this.style.fg,
- this.style.scrollbar.bg || this.style.bg);
- if (attr !== cell[0] || ch !== cell[1]) {
- lines[y][x][0] = attr;
- lines[y][x][1] = ch;
- lines[y].dirty = true;
- }
- }
- }
-
- if (this.border) xi--, xl++, yi--, yl++;
-
- if (this.tpadding) {
- xi -= this.padding.left, xl += this.padding.right;
- yi -= this.padding.top, yl += this.padding.bottom;
- }
-
- // Draw the border.
- if (this.border) {
- battr = this.sattr(this.style.border);
- y = yi;
- if (coords.notop) y = -1;
- for (x = xi; x < xl; x++) {
- if (!lines[y]) break;
- if (coords.noleft && x === xi) continue;
- if (coords.noright && x === xl - 1) continue;
- cell = lines[y][x];
- if (!cell) continue;
- if (this.border.type === 'line') {
- if (x === xi) {
- ch = '\u250c'; // '┌'
- if (!this.border.left) {
- if (this.border.top) {
- ch = '\u2500'; // '─'
- } else {
- continue;
- }
- } else {
- if (!this.border.top) {
- ch = '\u2502'; // '│'
- }
- }
- } else if (x === xl - 1) {
- ch = '\u2510'; // '┐'
- if (!this.border.right) {
- if (this.border.top) {
- ch = '\u2500'; // '─'
- } else {
- continue;
- }
- } else {
- if (!this.border.top) {
- ch = '\u2502'; // '│'
- }
- }
- } else {
- ch = '\u2500'; // '─'
- }
- } else if (this.border.type === 'bg') {
- ch = this.border.ch;
- }
- if (!this.border.top && x !== xi && x !== xl - 1) {
- ch = ' ';
- if (dattr !== cell[0] || ch !== cell[1]) {
- lines[y][x][0] = dattr;
- lines[y][x][1] = ch;
- lines[y].dirty = true;
- continue;
- }
- }
- if (battr !== cell[0] || ch !== cell[1]) {
- lines[y][x][0] = battr;
- lines[y][x][1] = ch;
- lines[y].dirty = true;
- }
- }
- y = yi + 1;
- for (; y < yl - 1; y++) {
- if (!lines[y]) continue;
- cell = lines[y][xi];
- if (cell) {
- if (this.border.left) {
- if (this.border.type === 'line') {
- ch = '\u2502'; // '│'
- } else if (this.border.type === 'bg') {
- ch = this.border.ch;
- }
- if (!coords.noleft)
- if (battr !== cell[0] || ch !== cell[1]) {
- lines[y][xi][0] = battr;
- lines[y][xi][1] = ch;
- lines[y].dirty = true;
- }
- } else {
- ch = ' ';
- if (dattr !== cell[0] || ch !== cell[1]) {
- lines[y][xi][0] = dattr;
- lines[y][xi][1] = ch;
- lines[y].dirty = true;
- }
- }
- }
- cell = lines[y][xl - 1];
- if (cell) {
- if (this.border.right) {
- if (this.border.type === 'line') {
- ch = '\u2502'; // '│'
- } else if (this.border.type === 'bg') {
- ch = this.border.ch;
- }
- if (!coords.noright)
- if (battr !== cell[0] || ch !== cell[1]) {
- lines[y][xl - 1][0] = battr;
- lines[y][xl - 1][1] = ch;
- lines[y].dirty = true;
- }
- } else {
- ch = ' ';
- if (dattr !== cell[0] || ch !== cell[1]) {
- lines[y][xl - 1][0] = dattr;
- lines[y][xl - 1][1] = ch;
- lines[y].dirty = true;
- }
- }
- }
- }
- y = yl - 1;
- if (coords.nobot) y = -1;
- for (x = xi; x < xl; x++) {
- if (!lines[y]) break;
- if (coords.noleft && x === xi) continue;
- if (coords.noright && x === xl - 1) continue;
- cell = lines[y][x];
- if (!cell) continue;
- if (this.border.type === 'line') {
- if (x === xi) {
- ch = '\u2514'; // '└'
- if (!this.border.left) {
- if (this.border.bottom) {
- ch = '\u2500'; // '─'
- } else {
- continue;
- }
- } else {
- if (!this.border.bottom) {
- ch = '\u2502'; // '│'
- }
- }
- } else if (x === xl - 1) {
- ch = '\u2518'; // '┘'
- if (!this.border.right) {
- if (this.border.bottom) {
- ch = '\u2500'; // '─'
- } else {
- continue;
- }
- } else {
- if (!this.border.bottom) {
- ch = '\u2502'; // '│'
- }
- }
- } else {
- ch = '\u2500'; // '─'
- }
- } else if (this.border.type === 'bg') {
- ch = this.border.ch;
- }
- if (!this.border.bottom && x !== xi && x !== xl - 1) {
- ch = ' ';
- if (dattr !== cell[0] || ch !== cell[1]) {
- lines[y][x][0] = dattr;
- lines[y][x][1] = ch;
- lines[y].dirty = true;
- }
- continue;
- }
- if (battr !== cell[0] || ch !== cell[1]) {
- lines[y][x][0] = battr;
- lines[y][x][1] = ch;
- lines[y].dirty = true;
- }
- }
- }
-
- if (this.shadow) {
- // right
- y = Math.max(yi + 1, 0);
- for (; y < yl + 1; y++) {
- if (!lines[y]) break;
- x = xl;
- for (; x < xl + 2; x++) {
- if (!lines[y][x]) break;
- // lines[y][x][0] = this._blend(this.dattr, lines[y][x][0]);
- lines[y][x][0] = this._blend(lines[y][x][0]);
- lines[y].dirty = true;
- }
- }
- // bottom
- y = yl;
- for (; y < yl + 1; y++) {
- if (!lines[y]) break;
- for (x = Math.max(xi + 1, 0); x < xl; x++) {
- if (!lines[y][x]) break;
- // lines[y][x][0] = this._blend(this.dattr, lines[y][x][0]);
- lines[y][x][0] = this._blend(lines[y][x][0]);
- lines[y].dirty = true;
- }
- }
- }
-
- this.children.forEach(function(el) {
- if (el.screen._ci !== -1) {
- el.index = el.screen._ci++;
- }
- // if (el.screen._rendering) {
- // el._rendering = true;
- // }
- el.render();
- // if (el.screen._rendering) {
- // el._rendering = false;
- // }
- });
-
- this._emit('render', [coords]);
-
- return coords;
-};
-
-Element.prototype._render = Element.prototype.render;
-
-/**
- * Blending and Shadows
- */
-
-Element.prototype._blend = function blend(attr, attr2) {
- var bg = attr & 0x1ff;
- if (attr2 != null) {
- var bg2 = attr2 & 0x1ff;
- if (bg === 0x1ff) bg = 0;
- if (bg2 === 0x1ff) bg2 = 0;
- bg = colors.mixColors(bg, bg2);
- } else {
- if (blend._cache[bg] != null) {
- bg = blend._cache[bg];
- // } else if (bg < 8) {
- // bg += 8;
- } else if (bg >= 8 && bg <= 15) {
- bg -= 8;
- } else {
- var name = colors.ncolors[bg];
- if (name) {
- for (var i = 0; i < colors.ncolors.length; i++) {
- if (name === colors.ncolors[i] && i !== bg) {
- var c = colors.vcolors[bg];
- var nc = colors.vcolors[i];
- if (nc[0] + nc[1] + nc[2] < c[0] + c[1] + c[2]) {
- blend._cache[bg] = i;
- bg = i;
- break;
- }
- }
- }
- }
- }
- }
-
- attr &= ~0x1ff;
- attr |= bg;
-
- var fg = (attr >> 9) & 0x1ff;
- if (attr2 != null) {
- var fg2 = (attr2 >> 9) & 0x1ff;
- // 0, 7, 188, 231, 251
- if (fg === 0x1ff) {
- // XXX workaround
- fg = 248;
- } else {
- if (fg === 0x1ff) fg = 7;
- if (fg2 === 0x1ff) fg2 = 7;
- fg = colors.mixColors(fg, fg2);
- }
- } else {
- if (blend._cache[fg] != null) {
- fg = blend._cache[fg];
- // } else if (fg < 8) {
- // fg += 8;
- } else if (fg >= 8 && fg <= 15) {
- fg -= 8;
- } else {
- var name = colors.ncolors[fg];
- if (name) {
- for (var i = 0; i < colors.ncolors.length; i++) {
- if (name === colors.ncolors[i] && i !== fg) {
- var c = colors.vcolors[fg];
- var nc = colors.vcolors[i];
- if (nc[0] + nc[1] + nc[2] < c[0] + c[1] + c[2]) {
- blend._cache[fg] = i;
- fg = i;
- break;
- }
- }
- }
- }
- }
- }
-
- attr &= ~(0x1ff << 9);
- attr |= fg << 9;
-
- return attr;
-};
-
-Element.prototype._blend._cache = {};
-
-/**
- * Content Methods
- */
-
-Element.prototype.insertLine = function(i, line) {
- if (typeof line === 'string') line = line.split('\n');
-
- if (i !== i || i == null) {
- i = this._clines.ftor.length;
- }
-
- i = Math.max(i, 0);
-
- while (this._clines.fake.length < i) {
- this._clines.fake.push('');
- this._clines.ftor.push([this._clines.push('') - 1]);
- this._clines.rtof(this._clines.fake.length - 1);
- }
-
- // NOTE: Could possibly compare the first and last ftor line numbers to see
- // if they're the same, or if they fit in the visible region entirely.
- var start = this._clines.length
- , diff
- , real;
-
- if (i >= this._clines.ftor.length) {
- real = this._clines.ftor[this._clines.ftor.length - 1];
- real = real[real.length - 1] + 1;
- } else {
- real = this._clines.ftor[i][0];
- }
-
- for (var j = 0; j < line.length; j++) {
- this._clines.fake.splice(i + j, 0, line[j]);
- }
-
- this.setContent(this._clines.fake.join('\n'), true);
-
- diff = this._clines.length - start;
-
- if (diff > 0) {
- var pos = this._getCoords();
- if (!pos) return;
-
- var height = pos.yl - pos.yi - this.iheight
- , base = this.childBase || 0
- , visible = real >= base && real - base < height;
-
- if (pos && visible && this.screen.cleanSides(this)) {
- this.screen.insertLine(diff,
- pos.yi + this.itop + real - base,
- pos.yi,
- pos.yl - this.ibottom - 1);
- }
- }
-};
-
-Element.prototype.deleteLine = function(i, n) {
- n = n || 1;
-
- if (i !== i || i == null) {
- i = this._clines.ftor.length - 1;
- }
-
- i = Math.max(i, 0);
- i = Math.min(i, this._clines.ftor.length - 1);
-
- // NOTE: Could possibly compare the first and last ftor line numbers to see
- // if they're the same, or if they fit in the visible region entirely.
- var start = this._clines.length
- , diff
- , real = this._clines.ftor[i][0];
-
- while (n--) {
- this._clines.fake.splice(i, 1);
- }
-
- this.setContent(this._clines.fake.join('\n'), true);
-
- diff = start - this._clines.length;
-
- if (diff > 0) {
- var pos = this._getCoords();
- if (!pos) return;
-
- var height = pos.yl - pos.yi - this.iheight
- , base = this.childBase || 0
- , visible = real >= base && real - base < height;
-
- if (pos && visible && this.screen.cleanSides(this)) {
- this.screen.deleteLine(diff,
- pos.yi + this.itop + real - base,
- pos.yi,
- pos.yl - this.ibottom - 1);
- }
- }
-
- if (this._clines.length < height) {
- this.clearPos();
- }
-};
-
-Element.prototype.insertTop = function(line) {
- var fake = this._clines.rtof[this.childBase || 0];
- return this.insertLine(fake, line);
-};
-
-Element.prototype.insertBottom = function(line) {
- var h = (this.childBase || 0) + this.height - this.iheight
- , i = Math.min(h, this._clines.length)
- , fake = this._clines.rtof[i - 1] + 1;
-
- return this.insertLine(fake, line);
-};
-
-Element.prototype.deleteTop = function(n) {
- var fake = this._clines.rtof[this.childBase || 0];
- return this.deleteLine(fake, n);
-};
-
-Element.prototype.deleteBottom = function(n) {
- var h = (this.childBase || 0) + this.height - 1 - this.iheight
- , i = Math.min(h, this._clines.length - 1)
- , n = n || 1
- , fake = this._clines.rtof[i];
-
- return this.deleteLine(fake - (n - 1), n);
-};
-
-Element.prototype.setLine = function(i, line) {
- i = Math.max(i, 0);
- while (this._clines.fake.length < i) {
- this._clines.fake.push('');
- }
- this._clines.fake[i] = line;
- return this.setContent(this._clines.fake.join('\n'), true);
-};
-
-Element.prototype.setBaseLine = function(i, line) {
- var fake = this._clines.rtof[this.childBase || 0];
- return this.setLine(fake + i, line);
-};
-
-Element.prototype.getLine = function(i) {
- i = Math.max(i, 0);
- i = Math.min(i, this._clines.fake.length - 1);
- return this._clines.fake[i];
-};
-
-Element.prototype.getBaseLine = function(i) {
- var fake = this._clines.rtof[this.childBase || 0];
- return this.getLine(fake + i);
-};
-
-Element.prototype.clearLine = function(i) {
- i = Math.min(i, this._clines.fake.length - 1);
- return this.setLine(i, '');
-};
-
-Element.prototype.clearBaseLine = function(i) {
- var fake = this._clines.rtof[this.childBase || 0];
- return this.clearLine(fake + i);
-};
-
-Element.prototype.unshiftLine = function(line) {
- return this.insertLine(0, line);
-};
-
-Element.prototype.shiftLine = function(n) {
- return this.deleteLine(0, n);
-};
-
-Element.prototype.pushLine = function(line) {
- if (!this.content) return this.setLine(0, line);
- return this.insertLine(this._clines.fake.length, line);
-};
-
-Element.prototype.popLine = function(n) {
- return this.deleteLine(this._clines.fake.length - 1, n);
-};
-
-Element.prototype.getLines = function() {
- return this._clines.fake.slice();
-};
-
-Element.prototype.getScreenLines = function() {
- return this._clines.slice();
-};
-
-Element.prototype.strWidth = function(text) {
- text = this.parseTags
- ? helpers.stripTags(text)
- : text;
- return this.screen.fullUnicode
- ? unicode.strWidth(text)
- : helpers.dropUnicode(text).length;
-};
-
-Element.prototype.screenshot = function(xi, xl, yi, yl) {
- xi = this.lpos.xi + this.ileft + (xi || 0);
- if (xl != null) {
- xl = this.lpos.xi + this.ileft + (xl || 0);
- } else {
- xl = this.lpos.xl - this.iright;
- }
- yi = this.lpos.yi + this.itop + (yi || 0);
- if (yl != null) {
- yl = this.lpos.yi + this.itop + (yl || 0);
- } else {
- yl = this.lpos.yl - this.ibottom;
- }
- return this.screen.screenshot(xi, xl, yi, yl);
-};
-
-/**
- * Box
- */
-
-function Box(options) {
- if (!(this instanceof Node)) {
- return new Box(options);
- }
- options = options || {};
- Element.call(this, options);
-}
-
-Box.prototype.__proto__ = Element.prototype;
-
-Box.prototype.type = 'box';
-
-/**
- * Text
- */
-
-function Text(options) {
- if (!(this instanceof Node)) {
- return new Text(options);
- }
- options = options || {};
- options.shrink = true;
- Element.call(this, options);
-}
-
-Text.prototype.__proto__ = Element.prototype;
-
-Text.prototype.type = 'text';
-
-/**
- * Line
- */
-
-function Line(options) {
- var self = this;
-
- if (!(this instanceof Node)) {
- return new Line(options);
- }
-
- options = options || {};
-
- var orientation = options.orientation || 'vertical';
- delete options.orientation;
-
- if (orientation === 'vertical') {
- options.width = 1;
- } else {
- options.height = 1;
- }
-
- Box.call(this, options);
-
- this.ch = !options.type || options.type === 'line'
- ? orientation === 'horizontal' ? '─' : '│'
- : options.ch || ' ';
-
- this.border = {
- type: 'bg',
- __proto__: this
- };
-
- this.style.border = this.style;
-}
-
-Line.prototype.__proto__ = Box.prototype;
-
-Line.prototype.type = 'line';
-
-/**
- * ScrollableBox
- */
-
-function ScrollableBox(options) {
- var self = this;
-
- if (!(this instanceof Node)) {
- return new ScrollableBox(options);
- }
-
- options = options || {};
-
- Box.call(this, options);
-
- if (options.scrollable === false) {
- return this;
- }
-
- this.scrollable = true;
- this.childOffset = 0;
- this.childBase = 0;
- this.baseLimit = options.baseLimit || Infinity;
- this.alwaysScroll = options.alwaysScroll;
-
- this.scrollbar = options.scrollbar;
- if (this.scrollbar) {
- this.scrollbar.ch = this.scrollbar.ch || ' ';
- this.style.scrollbar = this.style.scrollbar || this.scrollbar.style;
- if (!this.style.scrollbar) {
- this.style.scrollbar = {};
- this.style.scrollbar.fg = this.scrollbar.fg;
- this.style.scrollbar.bg = this.scrollbar.bg;
- this.style.scrollbar.bold = this.scrollbar.bold;
- this.style.scrollbar.underline = this.scrollbar.underline;
- this.style.scrollbar.inverse = this.scrollbar.inverse;
- this.style.scrollbar.invisible = this.scrollbar.invisible;
- }
- //this.scrollbar.style = this.style.scrollbar;
- if (this.track || this.scrollbar.track) {
- this.track = this.scrollbar.track || this.track;
- this.style.track = this.style.scrollbar.track || this.style.track;
- this.track.ch = this.track.ch || ' ';
- this.style.track = this.style.track || this.track.style;
- if (!this.style.track) {
- this.style.track = {};
- this.style.track.fg = this.track.fg;
- this.style.track.bg = this.track.bg;
- this.style.track.bold = this.track.bold;
- this.style.track.underline = this.track.underline;
- this.style.track.inverse = this.track.inverse;
- this.style.track.invisible = this.track.invisible;
- }
- this.track.style = this.style.track;
- }
- // Allow controlling of the scrollbar via the mouse:
- if (options.mouse) {
- this.on('mousedown', function(data) {
- if (self._scrollingBar) {
- // Do not allow dragging on the scrollbar:
- delete self.screen._dragging;
- delete self._drag;
- return;
- }
- var x = data.x - self.aleft;
- var y = data.y - self.atop;
- if (x === self.width - self.iright - 1) {
- // Do not allow dragging on the scrollbar:
- delete self.screen._dragging;
- delete self._drag;
- var perc = (y - self.itop) / (self.height - self.iheight);
- self.setScrollPerc(perc * 100 | 0);
- self.screen.render();
- var smd, smu;
- self._scrollingBar = true;
- self.onScreenEvent('mousedown', smd = function(data) {
- var y = data.y - self.atop;
- var perc = y / self.height;
- self.setScrollPerc(perc * 100 | 0);
- self.screen.render();
- });
- // If mouseup occurs out of the window, no mouseup event fires, and
- // scrollbar will drag again on mousedown until another mouseup
- // occurs.
- self.onScreenEvent('mouseup', smu = function(data) {
- self._scrollingBar = false;
- self.removeScreenEvent('mousedown', smd);
- self.removeScreenEvent('mouseup', smu);
- });
- }
- });
- }
- }
-
- if (options.mouse) {
- this.on('wheeldown', function(el, data) {
- self.scroll(self.height / 2 | 0 || 1);
- self.screen.render();
- });
- this.on('wheelup', function(el, data) {
- self.scroll(-(self.height / 2 | 0) || -1);
- self.screen.render();
- });
- }
-
- if (options.keys && !options.ignoreKeys) {
- this.on('keypress', function(ch, key) {
- if (key.name === 'up' || (options.vi && key.name === 'k')) {
- self.scroll(-1);
- self.screen.render();
- return;
- }
- if (key.name === 'down' || (options.vi && key.name === 'j')) {
- self.scroll(1);
- self.screen.render();
- return;
- }
- if (options.vi && key.name === 'u' && key.ctrl) {
- self.scroll(-(self.height / 2 | 0) || -1);
- self.screen.render();
- return;
- }
- if (options.vi && key.name === 'd' && key.ctrl) {
- self.scroll(self.height / 2 | 0 || 1);
- self.screen.render();
- return;
- }
- if (options.vi && key.name === 'b' && key.ctrl) {
- self.scroll(-self.height || -1);
- self.screen.render();
- return;
- }
- if (options.vi && key.name === 'f' && key.ctrl) {
- self.scroll(self.height || 1);
- self.screen.render();
- return;
- }
- if (options.vi && key.name === 'g' && !key.shift) {
- self.scrollTo(0);
- self.screen.render();
- return;
- }
- if (options.vi && key.name === 'g' && key.shift) {
- self.scrollTo(self.getScrollHeight());
- self.screen.render();
- return;
- }
- });
- }
-
- this.on('parsed content', function() {
- self._recalculateIndex();
- });
-
- self._recalculateIndex();
-}
-
-ScrollableBox.prototype.__proto__ = Box.prototype;
-
-ScrollableBox.prototype.type = 'scrollable-box';
-
-// XXX Potentially use this in place of scrollable checks elsewhere.
-ScrollableBox.prototype.__defineGetter__('reallyScrollable', function() {
- if (this.shrink) return this.scrollable;
- return this.getScrollHeight() > this.height;
-});
-
-ScrollableBox.prototype._scrollBottom = function() {
- if (!this.scrollable) return 0;
-
- // We could just calculate the children, but we can
- // optimize for lists by just returning the items.length.
- if (this._isList) {
- return this.items ? this.items.length : 0;
- }
-
- if (this.lpos && this.lpos._scrollBottom) {
- return this.lpos._scrollBottom;
- }
-
- var bottom = this.children.reduce(function(current, el) {
- // el.height alone does not calculate the shrunken height, we need to use
- // getCoords. A shrunken box inside a scrollable element will not grow any
- // larger than the scrollable element's context regardless of how much
- // content is in the shrunken box, unless we do this (call getCoords
- // without the scrollable calculation):
- // See: $ node test/widget-shrink-fail-2.js
- if (!el.detached) {
- var lpos = el._getCoords(false, true);
- if (lpos) {
- return Math.max(current, el.rtop + (lpos.yl - lpos.yi));
- }
- }
- return Math.max(current, el.rtop + el.height);
- }, 0);
-
- // XXX Use this? Makes .getScrollHeight() useless!
- // if (bottom < this._clines.length) bottom = this._clines.length;
-
- if (this.lpos) this.lpos._scrollBottom = bottom;
-
- return bottom;
-};
-
-ScrollableBox.prototype.setScroll =
-ScrollableBox.prototype.scrollTo = function(offset, always) {
- // XXX
- // At first, this appeared to account for the first new calculation of childBase:
- this.scroll(0);
- return this.scroll(offset - (this.childBase + this.childOffset), always);
-};
-
-ScrollableBox.prototype.getScroll = function() {
- return this.childBase + this.childOffset;
-};
-
-ScrollableBox.prototype.scroll = function(offset, always) {
- if (!this.scrollable) return;
-
- if (this.detached) return;
-
- // Handle scrolling.
- var visible = this.height - this.iheight
- , base = this.childBase
- , d
- , p
- , t
- , b
- , max
- , emax;
-
- if (this.alwaysScroll || always) {
- // Semi-workaround
- this.childOffset = offset > 0
- ? visible - 1 + offset
- : offset;
- } else {
- this.childOffset += offset;
- }
-
- if (this.childOffset > visible - 1) {
- d = this.childOffset - (visible - 1);
- this.childOffset -= d;
- this.childBase += d;
- } else if (this.childOffset < 0) {
- d = this.childOffset;
- this.childOffset += -d;
- this.childBase += d;
- }
-
- if (this.childBase < 0) {
- this.childBase = 0;
- } else if (this.childBase > this.baseLimit) {
- this.childBase = this.baseLimit;
- }
-
- // Find max "bottom" value for
- // content and descendant elements.
- // Scroll the content if necessary.
- if (this.childBase === base) {
- return this.emit('scroll');
- }
-
- // When scrolling text, we want to be able to handle SGR codes as well as line
- // feeds. This allows us to take preformatted text output from other programs
- // and put it in a scrollable text box.
- this.parseContent();
-
- // XXX
- // max = this.getScrollHeight() - (this.height - this.iheight);
-
- max = this._clines.length - (this.height - this.iheight);
- if (max < 0) max = 0;
- emax = this._scrollBottom() - (this.height - this.iheight);
- if (emax < 0) emax = 0;
-
- this.childBase = Math.min(this.childBase, Math.max(emax, max));
-
- if (this.childBase < 0) {
- this.childBase = 0;
- } else if (this.childBase > this.baseLimit) {
- this.childBase = this.baseLimit;
- }
-
- // Optimize scrolling with CSR + IL/DL.
- p = this.lpos;
- // Only really need _getCoords() if we want
- // to allow nestable scrolling elements...
- // or if we **really** want shrinkable
- // scrolling elements.
- // p = this._getCoords();
- if (p && this.childBase !== base && this.screen.cleanSides(this)) {
- t = p.yi + this.itop;
- b = p.yl - this.ibottom - 1;
- d = this.childBase - base;
-
- if (d > 0 && d < visible) {
- // scrolled down
- this.screen.deleteLine(d, t, t, b);
- } else if (d < 0 && -d < visible) {
- // scrolled up
- d = -d;
- this.screen.insertLine(d, t, t, b);
- }
- }
-
- return this.emit('scroll');
-};
-
-ScrollableBox.prototype._recalculateIndex = function() {
- var max, emax;
-
- if (this.detached || !this.scrollable) {
- return 0;
- }
-
- // XXX
- // max = this.getScrollHeight() - (this.height - this.iheight);
-
- max = this._clines.length - (this.height - this.iheight);
- if (max < 0) max = 0;
- emax = this._scrollBottom() - (this.height - this.iheight);
- if (emax < 0) emax = 0;
-
- this.childBase = Math.min(this.childBase, Math.max(emax, max));
-
- if (this.childBase < 0) {
- this.childBase = 0;
- } else if (this.childBase > this.baseLimit) {
- this.childBase = this.baseLimit;
- }
-};
-
-ScrollableBox.prototype.resetScroll = function() {
- if (!this.scrollable) return;
- this.childOffset = 0;
- this.childBase = 0;
- return this.emit('scroll');
-};
-
-ScrollableBox.prototype.getScrollHeight = function() {
- return Math.max(this._clines.length, this._scrollBottom());
-};
-
-ScrollableBox.prototype.getScrollPerc = function(s) {
- var pos = this.lpos || this._getCoords();
- if (!pos) return s ? -1 : 0;
-
- var height = (pos.yl - pos.yi) - this.iheight
- , i = this.getScrollHeight()
- , p;
-
- if (height < i) {
- if (this.alwaysScroll) {
- p = this.childBase / (i - height);
- } else {
- p = (this.childBase + this.childOffset) / (i - 1);
- }
- return p * 100;
- }
-
- return s ? -1 : 0;
-};
-
-ScrollableBox.prototype.setScrollPerc = function(i) {
- // XXX
- // var m = this.getScrollHeight();
- var m = Math.max(this._clines.length, this._scrollBottom());
- return this.scrollTo((i / 100) * m | 0);
-};
-
-/**
- * ScrollableText
- */
-
-function ScrollableText(options) {
- if (!(this instanceof Node)) {
- return new ScrollableText(options);
- }
- options = options || {};
- options.alwaysScroll = true;
- ScrollableBox.call(this, options);
-}
-
-ScrollableText.prototype.__proto__ = ScrollableBox.prototype;
-
-ScrollableText.prototype.type = 'scrollable-text';
-
-/**
- * List
- */
-
-function List(options) {
- var self = this;
-
- if (!(this instanceof Node)) {
- return new List(options);
- }
-
- options = options || {};
-
- options.ignoreKeys = true;
- // Possibly put this here: this.items = [];
- options.scrollable = true;
- Box.call(this, options);
-
- this.value = '';
- this.items = [];
- this.ritems = [];
- this.selected = 0;
- this._isList = true;
-
- if (!this.style.selected) {
- this.style.selected = {};
- this.style.selected.bg = options.selectedBg;
- this.style.selected.fg = options.selectedFg;
- this.style.selected.bold = options.selectedBold;
- this.style.selected.underline = options.selectedUnderline;
- this.style.selected.blink = options.selectedBlink;
- this.style.selected.inverse = options.selectedInverse;
- this.style.selected.invisible = options.selectedInvisible;
- }
-
- if (!this.style.item) {
- this.style.item = {};
- this.style.item.bg = options.itemBg;
- this.style.item.fg = options.itemFg;
- this.style.item.bold = options.itemBold;
- this.style.item.underline = options.itemUnderline;
- this.style.item.blink = options.itemBlink;
- this.style.item.inverse = options.itemInverse;
- this.style.item.invisible = options.itemInvisible;
- }
-
- // Legacy: for apps written before the addition of item attributes.
- ['bg', 'fg', 'bold', 'underline',
- 'blink', 'inverse', 'invisible'].forEach(function(name) {
- if (self.style[name] != null && self.style.item[name] == null) {
- self.style.item[name] = self.style[name];
- }
- });
-
- if (this.options.itemHoverBg) {
- this.options.itemHoverEffects = { bg: this.options.itemHoverBg };
- }
-
- if (this.options.itemHoverEffects) {
- this.style.item.hover = this.options.itemHoverEffects;
- }
-
- if (this.options.itemFocusEffects) {
- this.style.item.focus = this.options.itemFocusEffects;
- }
-
- this.interactive = options.interactive !== false;
-
- this.mouse = options.mouse || false;
-
- if (options.items) {
- this.ritems = options.items;
- options.items.forEach(this.add.bind(this));
- }
-
- this.select(0);
-
- if (options.mouse) {
- this.screen._listenMouse(this);
- this.on('element wheeldown', function(el, data) {
- self.select(self.selected + 2);
- self.screen.render();
- });
- this.on('element wheelup', function(el, data) {
- self.select(self.selected - 2);
- self.screen.render();
- });
- }
-
- if (options.keys) {
- this.on('keypress', function(ch, key) {
- if (key.name === 'up' || (options.vi && key.name === 'k')) {
- self.up();
- self.screen.render();
- return;
- }
- if (key.name === 'down' || (options.vi && key.name === 'j')) {
- self.down();
- self.screen.render();
- return;
- }
- if (key.name === 'enter'
- || (options.vi && key.name === 'l' && !key.shift)) {
- self.enterSelected();
- return;
- }
- if (key.name === 'escape' || (options.vi && key.name === 'q')) {
- self.cancelSelected();
- return;
- }
- if (options.vi && key.name === 'u' && key.ctrl) {
- self.move(-((self.height - self.iheight) / 2) | 0);
- self.screen.render();
- return;
- }
- if (options.vi && key.name === 'd' && key.ctrl) {
- self.move((self.height - self.iheight) / 2 | 0);
- self.screen.render();
- return;
- }
- if (options.vi && key.name === 'b' && key.ctrl) {
- self.move(-(self.height - self.iheight));
- self.screen.render();
- return;
- }
- if (options.vi && key.name === 'f' && key.ctrl) {
- self.move(self.height - self.iheight);
- self.screen.render();
- return;
- }
- if (options.vi && key.name === 'h' && key.shift) {
- self.move(self.childBase - self.selected);
- self.screen.render();
- return;
- }
- if (options.vi && key.name === 'm' && key.shift) {
- // TODO: Maybe use Math.min(this.items.length,
- // ... for calculating visible items elsewhere.
- var visible = Math.min(
- self.height - self.iheight,
- self.items.length) / 2 | 0;
- self.move(self.childBase + visible - self.selected);
- self.screen.render();
- return;
- }
- if (options.vi && key.name === 'l' && key.shift) {
- // XXX This goes one too far on lists with an odd number of items.
- self.down(self.childBase
- + Math.min(self.height - self.iheight, self.items.length)
- - self.selected);
- self.screen.render();
- return;
- }
- if (options.vi && key.name === 'g' && !key.shift) {
- self.select(0);
- self.screen.render();
- return;
- }
- if (options.vi && key.name === 'g' && key.shift) {
- self.select(self.items.length - 1);
- self.screen.render();
- return;
- }
-
- if (options.vi && (key.ch === '/' || key.ch === '?')) {
- if (typeof self.options.search !== 'function') {
- return;
- }
- return self.options.search(function(err, value) {
- if (typeof err === 'string' || typeof err === 'function'
- || typeof err === 'number' || (err && err.test)) {
- value = err;
- err = null;
- }
- if (err || !value) return self.screen.render();
- self.select(self.fuzzyFind(value, key.ch === '?'));
- self.screen.render();
- });
- }
- });
- }
-
- this.on('resize', function() {
- var visible = self.height - self.iheight;
- // if (self.selected < visible - 1) {
- if (visible >= self.selected + 1) {
- self.childBase = 0;
- self.childOffset = self.selected;
- } else {
- // Is this supposed to be: self.childBase = visible - self.selected + 1; ?
- self.childBase = self.selected - visible + 1;
- self.childOffset = visible - 1;
- }
- });
-
- this.on('adopt', function(el) {
- if (!~self.items.indexOf(el)) {
- el.fixed = true;
- }
- });
-
- // Ensure children are removed from the
- // item list if they are items.
- this.on('remove', function(el) {
- self.removeItem(el);
- });
-}
-
-List.prototype.__proto__ = Box.prototype;
-
-List.prototype.type = 'list';
-
-List.prototype.add =
-List.prototype.addItem =
-List.prototype.appendItem = function(item) {
- var self = this;
-
- this.ritems.push(item);
-
- // Note: Could potentially use Button here.
- var options = {
- screen: this.screen,
- content: item,
- align: this.align || 'left',
- top: this.items.length,
- left: 0,
- right: (this.scrollbar ? 1 : 0),
- tags: this.parseTags,
- height: 1,
- hoverEffects: this.mouse ? this.style.item.hover : null,
- focusEffects: this.mouse ? this.style.item.focus : null,
- autoFocus: false
- };
-
- if (!this.screen.autoPadding) {
- options.top = this.itop + this.items.length;
- options.left = this.ileft;
- options.right = this.iright + (this.scrollbar ? 1 : 0);
- }
-
- // if (this.shrink) {
- // XXX NOTE: Maybe just do this on all shrinkage once autoPadding is default?
- if (this.shrink && this.options.normalShrink) {
- delete options.right;
- options.width = 'shrink';
- }
-
- ['bg', 'fg', 'bold', 'underline',
- 'blink', 'inverse', 'invisible'].forEach(function(name) {
- options[name] = function() {
- var attr = self.items[self.selected] === item && self.interactive
- ? self.style.selected[name]
- : self.style.item[name];
- if (typeof attr === 'function') attr = attr(item);
- return attr;
- };
- });
-
- if (this.style.transparent) {
- options.transparent = true;
- }
-
- var item = new Box(options);
-
- this.items.push(item);
- this.append(item);
-
- if (this.items.length === 1) {
- this.select(0);
- }
-
- if (this.mouse) {
- item.on('click', function(data) {
- self.focus();
- if (self.items[self.selected] === item) {
- self.emit('action', item, self.selected);
- self.emit('select', item, self.selected);
- return;
- }
- self.select(item);
- self.screen.render();
- });
- }
-
- this.emit('add item');
-};
-
-List.prototype.find =
-List.prototype.fuzzyFind = function(search, back) {
- var start = this.selected + (back ? -1 : 1);
-
- if (typeof search === 'number') search += '';
-
- if (search && search[0] === '/' && search[search.length - 1] === '/') {
- try {
- search = new RegExp(search.slice(1, -1));
- } catch (e) {
- ;
- }
- }
-
- var test = typeof search === 'string'
- ? function(item) { return !!~item.indexOf(search); }
- : (search.test ? search.test.bind(search) : search);
-
- if (typeof test !== 'function') {
- if (this.screen.options.debug) {
- throw new Error('fuzzyFind(): `test` is not a function.');
- }
- return this.selected;
- }
-
- if (!back) {
- for (var i = start; i < this.ritems.length; i++){
- if (test(helpers.cleanTags(this.ritems[i]))) return i;
- }
- for (var i = 0; i < start; i++){
- if (test(helpers.cleanTags(this.ritems[i]))) return i;
- }
- } else {
- for (var i = start; i >= 0; i--){
- if (test(helpers.cleanTags(this.ritems[i]))) return i;
- }
- for (var i = this.ritems.length - 1; i > start; i--){
- if (test(helpers.cleanTags(this.ritems[i]))) return i;
- }
- }
-
- return this.selected;
-};
-
-List.prototype.getItemIndex = function(child) {
- if (typeof child === 'number') {
- return child;
- } else if (typeof child === 'string') {
- var i = this.ritems.indexOf(child);
- if (~i) return i;
- for (i = 0; i < this.ritems.length; i++) {
- if (helpers.cleanTags(this.ritems[i]) === child) {
- return i;
- }
- }
- return -1;
- } else {
- return this.items.indexOf(child);
- }
-};
-
-List.prototype.getItem = function(child) {
- return this.items[this.getItemIndex(child)];
-};
-
-List.prototype.removeItem = function(child) {
- var i = this.getItemIndex(child);
- if (~i && this.items[i]) {
- child = this.items.splice(i, 1)[0];
- this.ritems.splice(i, 1);
- this.remove(child);
- if (i === this.selected) {
- this.select(i - 1);
- }
- }
- this.emit('remove item');
-};
-
-List.prototype.clearItems = function() {
- return this.setItems([]);
-};
-
-List.prototype.setItems = function(items) {
- var items = items.slice()
- , original = this.items.slice()
- , selected = this.selected
- , sel = this.ritems[this.selected]
- , i = 0;
-
- this.select(0);
-
- for (; i < items.length; i++) {
- if (this.items[i]) {
- this.items[i].setContent(items[i]);
- } else {
- this.add(items[i]);
- }
- }
-
- for (; i < original.length; i++) {
- this.remove(original[i]);
- }
-
- this.ritems = items;
-
- // Try to find our old item if it still exists.
- sel = items.indexOf(sel);
- if (~sel) {
- this.select(sel);
- } else if (items.length === original.length) {
- this.select(selected);
- } else {
- this.select(Math.min(selected, items.length - 1));
- }
-
- this.emit('set items');
-};
-
-List.prototype.select = function(index) {
- if (!this.interactive) {
- return;
- }
-
- if (!this.items.length) {
- this.selected = 0;
- this.value = '';
- this.scrollTo(0);
- return;
- }
-
- if (typeof index === 'object') {
- index = this.items.indexOf(index);
- }
-
- if (index < 0) {
- index = 0;
- } else if (index >= this.items.length) {
- index = this.items.length - 1;
- }
-
- if (this.selected === index && this._listInitialized) return;
- this._listInitialized = true;
-
- this.selected = index;
- this.value = helpers.cleanTags(this.ritems[this.selected]);
- if (!this.parent) return;
- this.scrollTo(this.selected);
-
- // XXX Move `action` and `select` events here.
- this.emit('select item', this.items[this.selected], this.selected);
-};
-
-List.prototype.move = function(offset) {
- this.select(this.selected + offset);
-};
-
-List.prototype.up = function(offset) {
- this.move(-(offset || 1));
-};
-
-List.prototype.down = function(offset) {
- this.move(offset || 1);
-};
-
-List.prototype.pick = function(label, callback) {
- if (!callback) {
- callback = label;
- label = null;
- }
-
- if (!this.interactive) {
- return callback();
- }
-
- var self = this;
- var focused = this.screen.focused;
- if (focused && focused._done) focused._done('stop');
- this.screen.saveFocus();
-
- // XXX Keep above:
- // var parent = this.parent;
- // this.detach();
- // parent.append(this);
-
- this.focus();
- this.show();
- this.select(0);
- if (label) this.setLabel(label);
- this.screen.render();
- this.once('action', function(el, selected) {
- if (label) self.removeLabel();
- self.screen.restoreFocus();
- self.hide();
- self.screen.render();
- if (!el) return callback();
- return callback(null, helpers.cleanTags(self.ritems[selected]));
- });
-};
-
-List.prototype.enterSelected = function(i) {
- if (i != null) this.select(i);
- this.emit('action', this.items[this.selected], this.selected);
- this.emit('select', this.items[this.selected], this.selected);
-};
-
-List.prototype.cancelSelected = function(i) {
- if (i != null) this.select(i);
- this.emit('action');
- this.emit('cancel');
-};
-
-/**
- * Form
- */
-
-function Form(options) {
- var self = this;
-
- if (!(this instanceof Node)) {
- return new Form(options);
- }
-
- options = options || {};
-
- options.ignoreKeys = true;
- Box.call(this, options);
-
- if (options.keys) {
- this.screen._listenKeys(this);
- this.on('element keypress', function(el, ch, key) {
- if ((key.name === 'tab' && !key.shift)
- || (el.type === 'textbox' && options.autoNext && key.name === 'enter')
- || key.name === 'down'
- || (options.vi && key.name === 'j')) {
- if (el.type === 'textbox' || el.type === 'textarea') {
- if (key.name === 'j') return;
- if (key.name === 'tab') {
- // Workaround, since we can't stop the tab from being added.
- el.emit('keypress', null, { name: 'backspace' });
- }
- el.emit('keypress', '\x1b', { name: 'escape' });
- }
- self.focusNext();
- return;
- }
-
- if ((key.name === 'tab' && key.shift)
- || key.name === 'up'
- || (options.vi && key.name === 'k')) {
- if (el.type === 'textbox' || el.type === 'textarea') {
- if (key.name === 'k') return;
- el.emit('keypress', '\x1b', { name: 'escape' });
- }
- self.focusPrevious();
- return;
- }
-
- if (key.name === 'escape') {
- self.focus();
- return;
- }
- });
- }
-}
-
-Form.prototype.__proto__ = Box.prototype;
-
-Form.prototype.type = 'form';
-
-Form.prototype._refresh = function() {
- // XXX Possibly remove this if statement and refresh on every focus.
- // Also potentially only include *visible* focusable elements.
- // This would remove the need to check for _selected.visible in previous()
- // and next().
- if (!this._children) {
- var out = [];
-
- this.children.forEach(function fn(el) {
- if (el.keyable) out.push(el);
- el.children.forEach(fn);
- });
-
- this._children = out;
- }
-};
-
-Form.prototype._visible = function() {
- return !!this._children.filter(function(el) {
- return el.visible;
- }).length;
-};
-
-Form.prototype.next = function() {
- this._refresh();
-
- if (!this._visible()) return;
-
- if (!this._selected) {
- this._selected = this._children[0];
- if (!this._selected.visible) return this.next();
- if (this.screen.focused !== this._selected) return this._selected;
- }
-
- var i = this._children.indexOf(this._selected);
- if (!~i || !this._children[i + 1]) {
- this._selected = this._children[0];
- if (!this._selected.visible) return this.next();
- return this._selected;
- }
-
- this._selected = this._children[i + 1];
- if (!this._selected.visible) return this.next();
- return this._selected;
-};
-
-Form.prototype.previous = function() {
- this._refresh();
-
- if (!this._visible()) return;
-
- if (!this._selected) {
- this._selected = this._children[this._children.length - 1];
- if (!this._selected.visible) return this.previous();
- if (this.screen.focused !== this._selected) return this._selected;
- }
-
- var i = this._children.indexOf(this._selected);
- if (!~i || !this._children[i - 1]) {
- this._selected = this._children[this._children.length - 1];
- if (!this._selected.visible) return this.previous();
- return this._selected;
- }
-
- this._selected = this._children[i - 1];
- if (!this._selected.visible) return this.previous();
- return this._selected;
-};
-
-Form.prototype.focusNext = function() {
- var next = this.next();
- if (next) next.focus();
-};
-
-Form.prototype.focusPrevious = function() {
- var previous = this.previous();
- if (previous) previous.focus();
-};
-
-Form.prototype.resetSelected = function() {
- this._selected = null;
-};
-
-Form.prototype.focusFirst = function() {
- this.resetSelected();
- this.focusNext();
-};
-
-Form.prototype.focusLast = function() {
- this.resetSelected();
- this.focusPrevious();
-};
-
-Form.prototype.submit = function() {
- var self = this
- , out = {};
-
- this.children.forEach(function fn(el) {
- if (el.value != null) {
- var name = el.name || el.type;
- if (Array.isArray(out[name])) {
- out[name].push(el.value);
- } else if (out[name]) {
- out[name] = [out[name], el.value];
- } else {
- out[name] = el.value;
- }
- }
- el.children.forEach(fn);
- });
-
- this.emit('submit', out);
-
- return this.submission = out;
-};
-
-Form.prototype.cancel = function() {
- this.emit('cancel');
-};
-
-Form.prototype.reset = function() {
- this.children.forEach(function fn(el) {
- switch (el.type) {
- case 'screen':
- break;
- case 'box':
- break;
- case 'text':
- break;
- case 'line':
- break;
- case 'scrollable-box':
- break;
- case 'list':
- el.select(0);
- return;
- case 'form':
- break;
- case 'input':
- break;
- case 'textbox':
- el.clearInput();
- return;
- case 'textarea':
- el.clearInput();
- return;
- case 'button':
- delete el.value;
- break;
- case 'progress-bar':
- el.setProgress(0);
- break;
- case 'file-manager':
- el.refresh(el.options.cwd);
- return;
- case 'checkbox':
- el.uncheck();
- return;
- case 'radio-set':
- break;
- case 'radio-button':
- el.uncheck();
- return;
- case 'prompt':
- break;
- case 'question':
- break;
- case 'message':
- break;
- case 'info':
- break;
- case 'loading':
- break;
- case 'list-bar':
- //el.select(0);
- break;
- case 'dir-manager':
- el.refresh(el.options.cwd);
- return;
- case 'terminal':
- el.write('');
- return;
- case 'image':
- //el.clearImage();
- return;
- }
- el.children.forEach(fn);
- });
-
- this.emit('reset');
-};
-
-/**
- * Input
- */
-
-function Input(options) {
- if (!(this instanceof Node)) {
- return new Input(options);
- }
- options = options || {};
- Box.call(this, options);
-}
-
-Input.prototype.__proto__ = Box.prototype;
-
-Input.prototype.type = 'input';
-
-/**
- * Textarea
- */
-
-function Textarea(options) {
- var self = this;
-
- if (!(this instanceof Node)) {
- return new Textarea(options);
- }
-
- options = options || {};
-
- options.scrollable = options.scrollable !== false;
-
- Input.call(this, options);
-
- this.screen._listenKeys(this);
-
- this.value = options.value || '';
-
- this.__updateCursor = this._updateCursor.bind(this);
- this.on('resize', this.__updateCursor);
- this.on('move', this.__updateCursor);
-
- if (options.inputOnFocus) {
- this.on('focus', this.readInput.bind(this, null));
- }
-
- if (!options.inputOnFocus && options.keys) {
- this.on('keypress', function(ch, key) {
- if (self._reading) return;
- if (key.name === 'enter' || (options.vi && key.name === 'i')) {
- return self.readInput();
- }
- if (key.name === 'e') {
- return self.readEditor();
- }
- });
- }
-
- if (options.mouse) {
- this.on('click', function(data) {
- if (self._reading) return;
- if (data.button !== 'right') return;
- self.readEditor();
- });
- }
-}
-
-Textarea.prototype.__proto__ = Input.prototype;
-
-Textarea.prototype.type = 'textarea';
-
-Textarea.prototype._updateCursor = function(get) {
- if (this.screen.focused !== this) {
- return;
- }
-
- var lpos = get ? this.lpos : this._getCoords();
- if (!lpos) return;
-
- var last = this._clines[this._clines.length - 1]
- , program = this.screen.program
- , line
- , cx
- , cy;
-
- // Stop a situation where the textarea begins scrolling
- // and the last cline appears to always be empty from the
- // _typeScroll `+ '\n'` thing.
- // Maybe not necessary anymore?
- if (last === '' && this.value[this.value.length - 1] !== '\n') {
- last = this._clines[this._clines.length - 2] || '';
- }
-
- line = Math.min(
- this._clines.length - 1 - (this.childBase || 0),
- (lpos.yl - lpos.yi) - this.iheight - 1);
-
- // When calling clearValue() on a full textarea with a border, the first
- // argument in the above Math.min call ends up being -2. Make sure we stay
- // positive.
- line = Math.max(0, line);
-
- cy = lpos.yi + this.itop + line;
- cx = lpos.xi + this.ileft + this.strWidth(last);
-
- // XXX Not sure, but this may still sometimes
- // cause problems when leaving editor.
- if (cy === program.y && cx === program.x) {
- return;
- }
-
- if (cy === program.y) {
- if (cx > program.x) {
- program.cuf(cx - program.x);
- } else if (cx < program.x) {
- program.cub(program.x - cx);
- }
- } else if (cx === program.x) {
- if (cy > program.y) {
- program.cud(cy - program.y);
- } else if (cy < program.y) {
- program.cuu(program.y - cy);
- }
- } else {
- program.cup(cy, cx);
- }
-};
-
-Textarea.prototype.input =
-Textarea.prototype.setInput =
-Textarea.prototype.readInput = function(callback) {
- var self = this
- , focused = this.screen.focused === this;
-
- if (this._reading) return;
- this._reading = true;
-
- this._callback = callback;
-
- if (!focused) {
- this.screen.saveFocus();
- this.focus();
- }
-
- this.screen.grabKeys = true;
-
- this._updateCursor();
- this.screen.program.showCursor();
- //this.screen.program.sgr('normal');
-
- this._done = function fn(err, value) {
- if (!self._reading) return;
-
- if (fn.done) return;
- fn.done = true;
-
- self._reading = false;
-
- delete self._callback;
- delete self._done;
-
- self.removeListener('keypress', self.__listener);
- delete self.__listener;
-
- self.removeListener('blur', self.__done);
- delete self.__done;
-
- self.screen.program.hideCursor();
- self.screen.grabKeys = false;
-
- if (!focused) {
- self.screen.restoreFocus();
- }
-
- if (self.options.inputOnFocus) {
- self.screen.rewindFocus();
- }
-
- // Ugly
- if (err === 'stop') return;
-
- if (err) {
- self.emit('error', err);
- } else if (value != null) {
- self.emit('submit', value);
- } else {
- self.emit('cancel', value);
- }
- self.emit('action', value);
-
- if (!callback) return;
-
- return err
- ? callback(err)
- : callback(null, value);
- };
-
- // Put this in a nextTick so the current
- // key event doesn't trigger any keys input.
- nextTick(function() {
- self.__listener = self._listener.bind(self);
- self.on('keypress', self.__listener);
- });
-
- this.__done = this._done.bind(this, null, null);
- this.on('blur', this.__done);
-};
-
-Textarea.prototype._listener = function(ch, key) {
- var done = this._done
- , value = this.value;
-
- if (key.name === 'return') return;
- if (key.name === 'enter') {
- ch = '\n';
- }
-
- // TODO: Handle directional keys.
- if (key.name === 'left' || key.name === 'right'
- || key.name === 'up' || key.name === 'down') {
- ;
- }
-
- if (this.options.keys && key.ctrl && key.name === 'e') {
- return this.readEditor();
- }
-
- // TODO: Optimize typing by writing directly
- // to the screen and screen buffer here.
- if (key.name === 'escape') {
- done(null, null);
- } else if (key.name === 'backspace') {
- if (this.value.length) {
- if (this.screen.fullUnicode) {
- if (unicode.isSurrogate(this.value, this.value.length - 2)) {
- // || unicode.isCombining(this.value, this.value.length - 1)) {
- this.value = this.value.slice(0, -2);
- } else {
- this.value = this.value.slice(0, -1);
- }
- } else {
- this.value = this.value.slice(0, -1);
- }
- }
- } else if (ch) {
- if (!/^[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]$/.test(ch)) {
- this.value += ch;
- }
- }
-
- if (this.value !== value) {
- this.screen.render();
- }
-};
-
-Textarea.prototype._typeScroll = function() {
- // XXX Workaround
- var height = this.height - this.iheight;
- if (this._clines.length - this.childBase > height) {
- this.scroll(this._clines.length);
- }
-};
-
-Textarea.prototype.getValue = function() {
- return this.value;
-};
-
-Textarea.prototype.setValue = function(value) {
- if (value == null) {
- value = this.value;
- }
- if (this._value !== value) {
- this.value = value;
- this._value = value;
- this.setContent(this.value);
- this._typeScroll();
- this._updateCursor();
- }
-};
-
-Textarea.prototype.clearInput =
-Textarea.prototype.clearValue = function() {
- return this.setValue('');
-};
-
-Textarea.prototype.submit = function() {
- if (!this.__listener) return;
- return this.__listener('\x1b', { name: 'escape' });
-};
-
-Textarea.prototype.cancel = function() {
- if (!this.__listener) return;
- return this.__listener('\x1b', { name: 'escape' });
-};
-
-Textarea.prototype.render = function() {
- this.setValue();
- return this._render();
-};
-
-Textarea.prototype.editor =
-Textarea.prototype.setEditor =
-Textarea.prototype.readEditor = function(callback) {
- var self = this;
-
- if (this._reading) {
- var _cb = this._callback
- , cb = callback;
-
- this._done('stop');
-
- callback = function(err, value) {
- if (_cb) _cb(err, value);
- if (cb) cb(err, value);
- };
- }
-
- if (!callback) {
- callback = function() {};
- }
-
- return this.screen.readEditor({ value: this.value }, function(err, value) {
- if (err) {
- if (err.message === 'Unsuccessful.') {
- self.screen.render();
- return self.readInput(callback);
- }
- self.screen.render();
- self.readInput(callback);
- return callback(err);
- }
- self.setValue(value);
- self.screen.render();
- return self.readInput(callback);
- });
-};
-
-/**
- * Textbox
- */
-
-function Textbox(options) {
- var self = this;
-
- if (!(this instanceof Node)) {
- return new Textbox(options);
- }
-
- options = options || {};
-
- options.scrollable = false;
-
- Textarea.call(this, options);
-
- this.secret = options.secret;
- this.censor = options.censor;
-}
-
-Textbox.prototype.__proto__ = Textarea.prototype;
-
-Textbox.prototype.type = 'textbox';
-
-Textbox.prototype.__olistener = Textbox.prototype._listener;
-Textbox.prototype._listener = function(ch, key) {
- if (key.name === 'enter') {
- this._done(null, this.value);
- return;
- }
- return this.__olistener(ch, key);
-};
-
-Textbox.prototype.setValue = function(value) {
- var visible, val;
- if (value == null) {
- value = this.value;
- }
- if (this._value !== value) {
- value = value.replace(/\n/g, '');
- this.value = value;
- this._value = value;
- if (this.secret) {
- this.setContent('');
- } else if (this.censor) {
- this.setContent(Array(this.value.length + 1).join('*'));
- } else {
- visible = -(this.width - this.iwidth - 1);
- val = this.value.replace(/\t/g, this.screen.tabc);
- this.setContent(val.slice(visible));
- }
- this._updateCursor();
- }
-};
-
-Textbox.prototype.submit = function() {
- if (!this.__listener) return;
- return this.__listener('\r', { name: 'enter' });
-};
-
-/**
- * Button
- */
-
-function Button(options) {
- var self = this;
-
- if (!(this instanceof Node)) {
- return new Button(options);
- }
-
- options = options || {};
-
- if (options.autoFocus == null) {
- options.autoFocus = false;
- }
-
- Input.call(this, options);
-
- this.on('keypress', function(ch, key) {
- if (key.name === 'enter' || key.name === 'space') {
- return self.press();
- }
- });
-
- if (this.options.mouse) {
- this.on('click', function() {
- return self.press();
- });
- }
-}
-
-Button.prototype.__proto__ = Input.prototype;
-
-Button.prototype.type = 'button';
-
-Button.prototype.press = function() {
- this.focus();
- this.value = true;
- var result = this.emit('press');
- delete this.value;
- return result;
-};
-
-/**
- * ProgressBar
- */
-
-function ProgressBar(options) {
- var self = this;
-
- if (!(this instanceof Node)) {
- return new ProgressBar(options);
- }
-
- options = options || {};
-
- Input.call(this, options);
-
- this.filled = options.filled || 0;
- if (typeof this.filled === 'string') {
- this.filled = +this.filled.slice(0, -1);
- }
- this.value = this.filled;
-
- this.pch = options.pch || ' ';
-
- // XXX Workaround that predates the usage of `el.ch`.
- if (options.ch) {
- this.pch = options.ch;
- this.ch = ' ';
- }
- if (options.bch) {
- this.ch = options.bch;
- }
-
- if (!this.style.bar) {
- this.style.bar = {};
- this.style.bar.fg = options.barFg;
- this.style.bar.bg = options.barBg;
- }
-
- this.orientation = options.orientation || 'horizontal';
-
- if (options.keys) {
- this.on('keypress', function(ch, key) {
- var back, forward;
- if (self.orientation === 'horizontal') {
- back = ['left', 'h'];
- forward = ['right', 'l'];
- } else if (self.orientation === 'vertical') {
- back = ['down', 'j'];
- forward = ['up', 'k'];
- }
- if (key.name === back[0] || (options.vi && key.name === back[1])) {
- self.progress(-5);
- self.screen.render();
- return;
- }
- if (key.name === forward[0] || (options.vi && key.name === forward[1])) {
- self.progress(5);
- self.screen.render();
- return;
- }
- });
- }
-
- if (options.mouse) {
- this.on('click', function(data) {
- var x, y, m, p;
- if (!self.lpos) return;
- if (self.orientation === 'horizontal') {
- x = data.x - self.lpos.xi;
- m = (self.lpos.xl - self.lpos.xi) - self.iwidth;
- p = x / m * 100 | 0;
- } else if (self.orientation === 'vertical') {
- y = data.y - self.lpos.yi;
- m = (self.lpos.yl - self.lpos.yi) - self.iheight;
- p = y / m * 100 | 0;
- }
- self.setProgress(p);
- });
- }
-}
-
-ProgressBar.prototype.__proto__ = Input.prototype;
-
-ProgressBar.prototype.type = 'progress-bar';
-
-ProgressBar.prototype.render = function() {
- var ret = this._render();
- if (!ret) return;
-
- var xi = ret.xi
- , xl = ret.xl
- , yi = ret.yi
- , yl = ret.yl
- , dattr;
-
- if (this.border) xi++, yi++, xl--, yl--;
-
- if (this.orientation === 'horizontal') {
- xl = xi + ((xl - xi) * (this.filled / 100)) | 0;
- } else if (this.orientation === 'vertical') {
- yi = yi + ((yl - yi) - (((yl - yi) * (this.filled / 100)) | 0));
- }
-
- dattr = this.sattr(this.style.bar);
-
- this.screen.fillRegion(dattr, this.pch, xi, xl, yi, yl);
-
- if (this.content) {
- var line = this.screen.lines[yi];
- for (var i = 0; i < this.content.length; i++) {
- line[xi + i][1] = this.content[i];
- }
- line.dirty = true;
- }
-
- return ret;
-};
-
-ProgressBar.prototype.progress = function(filled) {
- this.filled += filled;
- if (this.filled < 0) this.filled = 0;
- else if (this.filled > 100) this.filled = 100;
- if (this.filled === 100) {
- this.emit('complete');
- }
- this.value = this.filled;
-};
-
-ProgressBar.prototype.setProgress = function(filled) {
- this.filled = 0;
- this.progress(filled);
-};
-
-ProgressBar.prototype.reset = function() {
- this.emit('reset');
- this.filled = 0;
- this.value = this.filled;
-};
-
-/**
- * FileManager
- */
-
-function FileManager(options) {
- var self = this;
-
- if (!(this instanceof Node)) {
- return new FileManager(options);
- }
-
- options = options || {};
- options.parseTags = true;
- // options.label = ' {blue-fg}%path{/blue-fg} ';
-
- List.call(this, options);
-
- this.cwd = options.cwd || process.cwd();
- this.file = this.cwd;
- this.value = this.cwd;
-
- if (options.label && ~options.label.indexOf('%path')) {
- this._label.setContent(options.label.replace('%path', this.cwd));
- }
-
- this.on('select', function(item) {
- var value = item.content.replace(/\{[^{}]+\}/g, '').replace(/@$/, '')
- , file = path.resolve(self.cwd, value);
-
- return fs.stat(file, function(err, stat) {
- if (err) {
- return self.emit('error', err, file);
- }
- self.file = file;
- self.value = file;
- if (stat.isDirectory()) {
- self.emit('cd', file, self.cwd);
- self.cwd = file;
- if (options.label && ~options.label.indexOf('%path')) {
- self._label.setContent(options.label.replace('%path', file));
- }
- self.refresh();
- } else {
- self.emit('file', file);
- }
- });
- });
-}
-
-FileManager.prototype.__proto__ = List.prototype;
-
-FileManager.prototype.type = 'file-manager';
-
-FileManager.prototype.refresh = function(cwd, callback) {
- if (!callback) {
- callback = cwd;
- cwd = null;
- }
-
- var self = this;
-
- if (cwd) this.cwd = cwd;
- else cwd = this.cwd;
-
- return fs.readdir(cwd, function(err, list) {
- if (err && err.code === 'ENOENT') {
- self.cwd = cwd !== process.env.HOME
- ? process.env.HOME
- : '/';
- return self.refresh(callback);
- }
-
- if (err) {
- if (callback) return callback(err);
- return self.emit('error', err, cwd);
- }
-
- var dirs = []
- , files = [];
-
- list.unshift('..');
-
- list.forEach(function(name) {
- var f = path.resolve(cwd, name)
- , stat;
-
- try {
- stat = fs.lstatSync(f);
- } catch (e) {
- ;
- }
-
- if ((stat && stat.isDirectory()) || name === '..') {
- dirs.push({
- name: name,
- text: '{light-blue-fg}' + name + '{/light-blue-fg}/',
- dir: true
- });
- } else if (stat && stat.isSymbolicLink()) {
- files.push({
- name: name,
- text: '{light-cyan-fg}' + name + '{/light-cyan-fg}@',
- dir: false
- });
- } else {
- files.push({
- name: name,
- text: name,
- dir: false
- });
- }
- });
-
- dirs = helpers.asort(dirs);
- files = helpers.asort(files);
-
- list = dirs.concat(files).map(function(data) {
- return data.text;
- });
-
- self.setItems(list);
- self.select(0);
- self.screen.render();
-
- self.emit('refresh');
-
- if (callback) callback();
- });
-};
-
-FileManager.prototype.pick = function(cwd, callback) {
- if (!callback) {
- callback = cwd;
- cwd = null;
- }
-
- var self = this
- , focused = this.screen.focused === this
- , hidden = this.hidden
- , onfile
- , oncancel;
-
- function resume() {
- self.removeListener('file', onfile);
- self.removeListener('cancel', oncancel);
- if (hidden) {
- self.hide();
- }
- if (!focused) {
- self.screen.restoreFocus();
- }
- self.screen.render();
- }
-
- this.on('file', onfile = function(file) {
- resume();
- return callback(null, file);
- });
-
- this.on('cancel', oncancel = function() {
- resume();
- return callback();
- });
-
- this.refresh(cwd, function(err) {
- if (err) return callback(err);
-
- if (hidden) {
- self.show();
- }
-
- if (!focused) {
- self.screen.saveFocus();
- self.focus();
- }
-
- self.screen.render();
- });
-};
-
-FileManager.prototype.reset = function(cwd, callback) {
- if (!callback) {
- callback = cwd;
- cwd = null;
- }
- this.cwd = cwd || this.options.cwd;
- this.refresh(callback);
-};
-
-/**
- * Checkbox
- */
-
-function Checkbox(options) {
- var self = this;
-
- if (!(this instanceof Node)) {
- return new Checkbox(options);
- }
-
- options = options || {};
-
- Input.call(this, options);
-
- this.text = options.content || options.text || '';
- this.checked = this.value = options.checked || false;
-
- this.on('keypress', function(ch, key) {
- if (key.name === 'enter' || key.name === 'space') {
- self.toggle();
- self.screen.render();
- }
- });
-
- if (options.mouse) {
- this.on('click', function() {
- self.toggle();
- self.screen.render();
- });
- }
-
- this.on('focus', function(old) {
- var lpos = self.lpos;
- if (!lpos) return;
- self.screen.program.lsaveCursor('checkbox');
- self.screen.program.cup(lpos.yi, lpos.xi + 1);
- self.screen.program.showCursor();
- });
-
- this.on('blur', function() {
- self.screen.program.lrestoreCursor('checkbox', true);
- });
-}
-
-Checkbox.prototype.__proto__ = Input.prototype;
-
-Checkbox.prototype.type = 'checkbox';
-
-Checkbox.prototype.render = function() {
- this.clearPos(true);
- this.setContent('[' + (this.checked ? 'x' : ' ') + '] ' + this.text, true);
- return this._render();
-};
-
-Checkbox.prototype.check = function() {
- if (this.checked) return;
- this.checked = this.value = true;
- this.emit('check');
-};
-
-Checkbox.prototype.uncheck = function() {
- if (!this.checked) return;
- this.checked = this.value = false;
- this.emit('uncheck');
-};
-
-Checkbox.prototype.toggle = function() {
- return this.checked
- ? this.uncheck()
- : this.check();
-};
-
-/**
- * RadioSet
- */
-
-function RadioSet(options) {
- if (!(this instanceof Node)) {
- return new RadioSet(options);
- }
- options = options || {};
- // Possibly inherit parent's style.
- // options.style = this.parent.style;
- Box.call(this, options);
-}
-
-RadioSet.prototype.__proto__ = Box.prototype;
-
-RadioSet.prototype.type = 'radio-set';
-
-/**
- * RadioButton
- */
-
-function RadioButton(options) {
- var self = this;
-
- if (!(this instanceof Node)) {
- return new RadioButton(options);
- }
-
- options = options || {};
-
- Checkbox.call(this, options);
-
- this.on('check', function() {
- var el = self;
- while (el = el.parent) {
- if (el.type === 'radio-set'
- || el.type === 'form') break;
- }
- el = el || self.parent;
- el.forDescendants(function(el) {
- if (el.type !== 'radio-button' || el === self) {
- return;
- }
- el.uncheck();
- });
- });
-}
-
-RadioButton.prototype.__proto__ = Checkbox.prototype;
-
-RadioButton.prototype.type = 'radio-button';
-
-RadioButton.prototype.render = function() {
- this.clearPos(true);
- this.setContent('(' + (this.checked ? '*' : ' ') + ') ' + this.text, true);
- return this._render();
-};
-
-RadioButton.prototype.toggle = RadioButton.prototype.check;
-
-/**
- * Prompt
- */
-
-function Prompt(options) {
- var self = this;
-
- if (!(this instanceof Node)) {
- return new Prompt(options);
- }
-
- options = options || {};
-
- options.hidden = true;
-
- Box.call(this, options);
-
- this._.input = new Textbox({
- parent: this,
- top: 3,
- height: 1,
- left: 2,
- right: 2,
- bg: 'black'
- });
-
- this._.okay = new Button({
- parent: this,
- top: 5,
- height: 1,
- left: 2,
- width: 6,
- content: 'Okay',
- align: 'center',
- bg: 'black',
- hoverBg: 'blue',
- autoFocus: false,
- mouse: true
- });
-
- this._.cancel = new Button({
- parent: this,
- top: 5,
- height: 1,
- shrink: true,
- left: 10,
- width: 8,
- content: 'Cancel',
- align: 'center',
- bg: 'black',
- hoverBg: 'blue',
- autoFocus: false,
- mouse: true
- });
-}
-
-Prompt.prototype.__proto__ = Box.prototype;
-
-Prompt.prototype.type = 'prompt';
-
-Prompt.prototype.input =
-Prompt.prototype.setInput =
-Prompt.prototype.readInput = function(text, value, callback) {
- var self = this;
- var okay, cancel;
-
- if (!callback) {
- callback = value;
- value = '';
- }
-
- // Keep above:
- // var parent = this.parent;
- // this.detach();
- // parent.append(this);
-
- this.show();
- this.setContent(' ' + text);
-
- this._.input.value = value;
-
- this.screen.saveFocus();
-
- this._.okay.on('press', okay = function() {
- self._.input.submit();
- });
-
- this._.cancel.on('press', cancel = function() {
- self._.input.cancel();
- });
-
- this._.input.readInput(function(err, data) {
- self.hide();
- self.screen.restoreFocus();
- self._.okay.removeListener('press', okay);
- self._.cancel.removeListener('press', cancel);
- return callback(err, data);
- });
-
- this.screen.render();
-};
-
-/**
- * Question
- */
-
-function Question(options) {
- var self = this;
-
- if (!(this instanceof Node)) {
- return new Question(options);
- }
-
- options = options || {};
- options.hidden = true;
-
- Box.call(this, options);
-
- this._.okay = new Button({
- screen: this.screen,
- parent: this,
- top: 2,
- height: 1,
- left: 2,
- width: 6,
- content: 'Okay',
- align: 'center',
- bg: 'black',
- hoverBg: 'blue',
- autoFocus: false,
- mouse: true
- });
-
- this._.cancel = new Button({
- screen: this.screen,
- parent: this,
- top: 2,
- height: 1,
- shrink: true,
- left: 10,
- width: 8,
- content: 'Cancel',
- align: 'center',
- bg: 'black',
- hoverBg: 'blue',
- autoFocus: false,
- mouse: true
- });
-}
-
-Question.prototype.__proto__ = Box.prototype;
-
-Question.prototype.type = 'question';
-
-Question.prototype.ask = function(text, callback) {
- var self = this;
- var press, okay, cancel;
-
- // Keep above:
- // var parent = this.parent;
- // this.detach();
- // parent.append(this);
-
- this.show();
- this.setContent(' ' + text);
-
- this.onScreenEvent('keypress', press = function(ch, key) {
- if (key.name === 'mouse') return;
- if (key.name !== 'enter'
- && key.name !== 'escape'
- && key.name !== 'q'
- && key.name !== 'y'
- && key.name !== 'n') {
- return;
- }
- done(null, key.name === 'enter' || key.name === 'y');
- });
-
- this._.okay.on('press', okay = function() {
- done(null, true);
- });
-
- this._.cancel.on('press', cancel = function() {
- done(null, false);
- });
-
- this.screen.saveFocus();
- this.focus();
-
- function done(err, data) {
- self.hide();
- self.screen.restoreFocus();
- self.removeScreenEvent('keypress', press);
- self._.okay.removeListener('press', okay);
- self._.cancel.removeListener('press', cancel);
- return callback(err, data);
- }
-
- this.screen.render();
-};
-
-/**
- * Message / Error
- */
-
-function Message(options) {
- var self = this;
-
- if (!(this instanceof Node)) {
- return new Message(options);
- }
-
- options = options || {};
- options.tags = true;
-
- Box.call(this, options);
-}
-
-Message.prototype.__proto__ = Box.prototype;
-
-Message.prototype.type = 'message';
-
-Message.prototype.log =
-Message.prototype.display = function(text, time, callback) {
- var self = this;
-
- if (typeof time === 'function') {
- callback = time;
- time = null;
- }
-
- if (time == null) time = 3;
-
- // Keep above:
- // var parent = this.parent;
- // this.detach();
- // parent.append(this);
-
- if (this.scrollable) {
- this.screen.saveFocus();
- this.focus();
- this.scrollTo(0);
- }
-
- this.show();
- this.setContent(text);
- this.screen.render();
-
- if (time === Infinity || time === -1 || time === 0) {
- var end = function() {
- if (end.done) return;
- end.done = true;
- if (self.scrollable) {
- try {
- self.screen.restoreFocus();
- } catch (e) {
- ;
- }
- }
- self.hide();
- self.screen.render();
- if (callback) callback();
- };
-
- setTimeout(function() {
- self.onScreenEvent('keypress', function fn(ch, key) {
- if (key.name === 'mouse') return;
- if (self.scrollable) {
- if ((key.name === 'up' || (self.options.vi && key.name === 'k'))
- || (key.name === 'down' || (self.options.vi && key.name === 'j'))
- || (self.options.vi && key.name === 'u' && key.ctrl)
- || (self.options.vi && key.name === 'd' && key.ctrl)
- || (self.options.vi && key.name === 'b' && key.ctrl)
- || (self.options.vi && key.name === 'f' && key.ctrl)
- || (self.options.vi && key.name === 'g' && !key.shift)
- || (self.options.vi && key.name === 'g' && key.shift)) {
- return;
- }
- }
- if (self.options.ignoreKeys && ~self.options.ignoreKeys.indexOf(key.name)) {
- return;
- }
- self.removeScreenEvent('keypress', fn);
- end();
- });
- if (!self.options.mouse) return;
- self.onScreenEvent('mouse', function fn(data) {
- if (data.action === 'mousemove') return;
- self.removeScreenEvent('mouse', fn);
- end();
- });
- }, 10);
-
- return;
- }
-
- setTimeout(function() {
- self.hide();
- self.screen.render();
- if (callback) callback();
- }, time * 1000);
-};
-
-Message.prototype.error = function(text, time, callback) {
- return this.display('{red-fg}Error: ' + text + '{/red-fg}', time, callback);
-};
-
-/**
- * Loading
- */
-
-function Loading(options) {
- var self = this;
-
- if (!(this instanceof Node)) {
- return new Loading(options);
- }
-
- options = options || {};
-
- Box.call(this, options);
-
- this._.icon = new Text({
- parent: this,
- align: 'center',
- top: 2,
- left: 1,
- right: 1,
- height: 1,
- content: '|'
- });
-}
-
-Loading.prototype.__proto__ = Box.prototype;
-
-Loading.prototype.type = 'loading';
-
-Loading.prototype.load = function(text) {
- var self = this;
-
- // XXX Keep above:
- // var parent = this.parent;
- // this.detach();
- // parent.append(this);
-
- this.show();
- this.setContent(text);
-
- if (this._.timer) {
- this.stop();
- }
-
- this.screen.lockKeys = true;
-
- this._.timer = setInterval(function() {
- if (self._.icon.content === '|') {
- self._.icon.setContent('/');
- } else if (self._.icon.content === '/') {
- self._.icon.setContent('-');
- } else if (self._.icon.content === '-') {
- self._.icon.setContent('\\');
- } else if (self._.icon.content === '\\') {
- self._.icon.setContent('|');
- }
- self.screen.render();
- }, 200);
-};
-
-Loading.prototype.stop = function() {
- this.screen.lockKeys = false;
- this.hide();
- if (this._.timer) {
- clearInterval(this._.timer);
- delete this._.timer;
- }
- this.screen.render();
-};
-
-/**
- * Listbar / HorizontalList
- */
-
-function Listbar(options) {
- var self = this;
-
- if (!(this instanceof Node)) {
- return new Listbar(options);
- }
-
- options = options || {};
-
- this.items = [];
- this.ritems = [];
- this.commands = [];
-
- this.leftBase = 0;
- this.leftOffset = 0;
-
- this.mouse = options.mouse || false;
-
- Box.call(this, options);
-
- if (!this.style.selected) {
- this.style.selected = {};
- }
-
- if (!this.style.item) {
- this.style.item = {};
- }
-
- if (options.commands || options.items) {
- this.setItems(options.commands || options.items);
- }
-
- if (options.keys) {
- this.on('keypress', function(ch, key) {
- if (key.name === 'left'
- || (options.vi && key.name === 'h')
- || (key.shift && key.name === 'tab')) {
- self.moveLeft();
- self.screen.render();
- // Stop propagation if we're in a form.
- if (key.name === 'tab') return false;
- return;
- }
- if (key.name === 'right'
- || (options.vi && key.name === 'l')
- || key.name === 'tab') {
- self.moveRight();
- self.screen.render();
- // Stop propagation if we're in a form.
- if (key.name === 'tab') return false;
- return;
- }
- if (key.name === 'enter'
- || (options.vi && key.name === 'k' && !key.shift)) {
- self.emit('action', self.items[self.selected], self.selected);
- self.emit('select', self.items[self.selected], self.selected);
- var item = self.items[self.selected];
- if (item._.cmd.callback) {
- item._.cmd.callback();
- }
- self.screen.render();
- return;
- }
- if (key.name === 'escape' || (options.vi && key.name === 'q')) {
- self.emit('action');
- self.emit('cancel');
- return;
- }
- });
- }
-
- if (options.autoCommandKeys) {
- this.onScreenEvent('keypress', function(ch, key) {
- if (/^[0-9]$/.test(ch)) {
- var i = +ch - 1;
- if (!~i) i = 9;
- return self.selectTab(i);
- }
- });
- }
-
- this.on('focus', function() {
- self.select(self.selected);
- });
-}
-
-Listbar.prototype.__proto__ = Box.prototype;
-
-Listbar.prototype.type = 'listbar';
-
-Listbar.prototype.__defineGetter__('selected', function() {
- return this.leftBase + this.leftOffset;
-});
-
-Listbar.prototype.setItems = function(commands) {
- var self = this;
-
- if (!Array.isArray(commands)) {
- commands = Object.keys(commands).reduce(function(obj, key, i) {
- var cmd = commands[key]
- , cb;
-
- if (typeof cmd === 'function') {
- cb = cmd;
- cmd = { callback: cb };
- }
-
- if (cmd.text == null) cmd.text = key;
- if (cmd.prefix == null) cmd.prefix = ++i + '';
-
- if (cmd.text == null && cmd.callback) {
- cmd.text = cmd.callback.name;
- }
-
- obj.push(cmd);
-
- return obj;
- }, []);
- }
-
- this.items.forEach(function(el) {
- el.detach();
- });
-
- this.items = [];
- this.ritems = [];
- this.commands = [];
-
- commands.forEach(function(cmd) {
- self.add(cmd);
- });
-
- this.emit('set items');
-};
-
-Listbar.prototype.add =
-Listbar.prototype.addItem =
-Listbar.prototype.appendItem = function(item, callback) {
- var self = this
- , prev = this.items[this.items.length - 1]
- , drawn = prev ? prev.aleft + prev.width : 0
- , cmd
- , title
- , len;
-
- if (!this.screen.autoPadding) {
- drawn += this.ileft;
- }
-
- if (typeof item === 'object') {
- cmd = item;
- if (cmd.prefix == null) cmd.prefix = (this.items.length + 1) + '';
- }
-
- if (typeof item === 'string') {
- cmd = {
- prefix: (this.items.length + 1) + '',
- text: item,
- callback: callback
- };
- }
-
- if (typeof item === 'function') {
- cmd = {
- prefix: (this.items.length + 1) + '',
- text: item.name,
- callback: item
- };
- }
-
- if (cmd.keys && cmd.keys[0]) {
- cmd.prefix = cmd.keys[0];
- }
-
- var t = helpers.generateTags(this.style.prefix || { fg: 'lightblack' });
-
- title = (cmd.prefix != null ? t.open + cmd.prefix + t.close + ':' : '') + cmd.text;
-
- len = ((cmd.prefix != null ? cmd.prefix + ':' : '') + cmd.text).length;
-
- var options = {
- screen: this.screen,
- top: 0,
- left: drawn + 1,
- height: 1,
- content: title,
- width: len + 2,
- align: 'center',
- autoFocus: false,
- tags: true,
- mouse: true,
- style: helpers.merge({}, this.style.item),
- noOverflow: true
- };
-
- if (!this.screen.autoPadding) {
- options.top += this.itop;
- options.left += this.ileft;
- }
-
- ['bg', 'fg', 'bold', 'underline',
- 'blink', 'inverse', 'invisible'].forEach(function(name) {
- options.style[name] = function() {
- var attr = self.items[self.selected] === el
- ? self.style.selected[name]
- : self.style.item[name];
- if (typeof attr === 'function') attr = attr(el);
- return attr;
- };
- });
-
- var el = new Box(options);
-
- this._[cmd.text] = el;
- cmd.element = el;
- el._.cmd = cmd;
-
- this.ritems.push(cmd.text);
- this.items.push(el);
- this.commands.push(cmd);
- this.append(el);
-
- if (cmd.callback) {
- if (cmd.keys) {
- this.screen.key(cmd.keys, function(ch, key) {
- self.emit('action', el, self.selected);
- self.emit('select', el, self.selected);
- if (el._.cmd.callback) {
- el._.cmd.callback();
- }
- self.select(el);
- self.screen.render();
- });
- }
- }
-
- if (this.items.length === 1) {
- this.select(0);
- }
-
- if (this.mouse) {
- el.on('click', function(data) {
- self.emit('action', el, self.selected);
- self.emit('select', el, self.selected);
- if (el._.cmd.callback) {
- el._.cmd.callback();
- }
- self.select(el);
- self.screen.render();
- });
- }
-
- this.emit('add item');
-};
-
-Listbar.prototype.render = function() {
- var self = this
- , drawn = 0;
-
- if (!this.screen.autoPadding) {
- drawn += this.ileft;
- }
-
- this.items.forEach(function(el, i) {
- if (i < self.leftBase) {
- el.hide();
- } else {
- el.rleft = drawn + 1;
- drawn += el.width + 2;
- el.show();
- }
- });
-
- return this._render();
-};
-
-Listbar.prototype.select = function(offset) {
- if (typeof offset !== 'number') {
- offset = this.items.indexOf(offset);
- }
-
- var lpos = this._getCoords();
- if (!lpos) return;
-
- var self = this
- , width = (lpos.xl - lpos.xi) - this.iwidth
- , drawn = 0
- , visible = 0
- , el;
-
- if (offset < 0) {
- offset = 0;
- } else if (offset >= this.items.length) {
- offset = this.items.length - 1;
- }
-
- el = this.items[offset];
- if (!el) return;
-
- this.items.forEach(function(el, i) {
- if (i < self.leftBase) return;
-
- var lpos = el._getCoords();
- if (!lpos) return;
-
- if (lpos.xl - lpos.xi <= 0) return;
-
- drawn += (lpos.xl - lpos.xi) + 2;
-
- if (drawn <= width) visible++;
- });
-
- var diff = offset - (this.leftBase + this.leftOffset);
- if (offset > this.leftBase + this.leftOffset) {
- if (offset > this.leftBase + visible - 1) {
- this.leftOffset = 0;
- this.leftBase = offset;
- } else {
- this.leftOffset += diff;
- }
- } else if (offset < this.leftBase + this.leftOffset) {
- diff = -diff;
- if (offset < this.leftBase) {
- this.leftOffset = 0;
- this.leftBase = offset;
- } else {
- this.leftOffset -= diff;
- }
- }
-
- // XXX Move `action` and `select` events here.
- this.emit('select item', el, offset);
-};
-
-Listbar.prototype.removeItem = function(child) {
- var i = typeof child !== 'number'
- ? this.items.indexOf(child)
- : child;
-
- if (~i && this.items[i]) {
- child = this.items.splice(i, 1)[0];
- this.ritems.splice(i, 1);
- this.commands.splice(i, 1);
- this.remove(child);
- if (i === this.selected) {
- this.select(i - 1);
- }
- }
-
- this.emit('remove item');
-};
-
-Listbar.prototype.move = function(offset) {
- this.select(this.selected + offset);
-};
-
-Listbar.prototype.moveLeft = function(offset) {
- this.move(-(offset || 1));
-};
-
-Listbar.prototype.moveRight = function(offset) {
- this.move(offset || 1);
-};
-
-Listbar.prototype.selectTab = function(index) {
- var item = this.items[index];
- if (item) {
- if (item._.cmd.callback) {
- item._.cmd.callback();
- }
- this.select(index);
- this.screen.render();
- }
- this.emit('select tab', item, index);
-};
-
-/**
- * Log
- */
-
-function Log(options) {
- var self = this;
-
- if (!(this instanceof Node)) {
- return new Log(options);
- }
-
- options = options || {};
-
- ScrollableText.call(this, options);
-
- this.scrollback = options.scrollback != null
- ? options.scrollback
- : Infinity;
- this.scrollOnInput = options.scrollOnInput;
-
- this.on('set content', function() {
- if (!self._userScrolled || self.scrollOnInput) {
- nextTick(function() {
- self.setScrollPerc(100);
- self._userScrolled = false;
- self.screen.render();
- });
- }
- });
-}
-
-Log.prototype.__proto__ = ScrollableText.prototype;
-
-Log.prototype.type = 'log';
-
-Log.prototype.log =
-Log.prototype.add = function() {
- var args = Array.prototype.slice.call(arguments);
- if (typeof args[0] === 'object') {
- args[0] = util.inspect(args[0], true, 20, true);
- }
- var text = util.format.apply(util, args);
- this.emit('log', text);
- var ret = this.pushLine(text);
- if (this._clines.fake.length > this.scrollback) {
- this.shiftLine(0, (this.scrollback / 3) | 0);
- }
- return ret;
-};
-
-Log.prototype._scroll = Log.prototype.scroll;
-Log.prototype.scroll = function(offset, always) {
- if (offset === 0) return this._scroll(offset, always);
- this._userScrolled = true;
- var ret = this._scroll(offset, always);
- if (this.getScrollPerc() === 100) {
- this._userScrolled = false;
- }
- return ret;
-};
-
-/**
- * Table
- */
-
-function Table(options) {
- var self = this;
-
- if (!(this instanceof Node)) {
- return new Table(options);
- }
-
- options = options || {};
- options.shrink = true;
- options.style = options.style || {};
- options.style.border = options.style.border || {};
- options.style.header = options.style.header || {};
- options.style.cell = options.style.cell || {};
- options.align = options.align || 'center';
-
- // Regular tables do not get custom height (this would
- // require extra padding). Maybe add in the future.
- delete options.height;
-
- Box.call(this, options);
-
- this.pad = options.pad != null
- ? options.pad
- : 2;
-
- this.setData(options.rows || options.data);
-
- this.on('resize', function() {
- self.setContent('');
- self.setData(self.rows);
- self.screen.render();
- });
-}
-
-Table.prototype.__proto__ = Box.prototype;
-
-Table.prototype.type = 'table';
-
-Table.prototype._calculateMaxes = function() {
- var self = this;
- var maxes = [];
-
- this.rows.forEach(function(row) {
- row.forEach(function(cell, i) {
- var clen = self.strWidth(cell);
- if (!maxes[i] || maxes[i] < clen) {
- maxes[i] = clen;
- }
- });
- });
-
- var total = maxes.reduce(function(total, max) {
- return total + max;
- }, 0);
- total += maxes.length + 1;
-
- // XXX There might be an issue with resizing where on the first resize event
- // width appears to be less than total if it's a percentage or left/right
- // combination.
- if (this.width < total) {
- delete this.position.width;
- }
-
- if (this.position.width != null) {
- var missing = this.width - total;
- var w = missing / maxes.length | 0;
- var wr = missing % maxes.length;
- maxes = maxes.map(function(max, i) {
- if (i === maxes.length - 1) {
- return max + w + wr;
- }
- return max + w;
- });
- } else {
- maxes = maxes.map(function(max) {
- return max + self.pad;
- });
- }
-
- return this._maxes = maxes;
-};
-
-Table.prototype.setRows =
-Table.prototype.setData = function(rows) {
- var self = this
- , text = ''
- , line = ''
- , align = this.align;
-
- this.rows = rows || [];
-
- this._calculateMaxes();
-
- this.rows.forEach(function(row, i) {
- var isHeader = i === 0;
- var isFooter = i === self.rows.length - 1;
- row.forEach(function(cell, i) {
- var width = self._maxes[i];
- var clen = self.strWidth(cell);
-
- if (i !== 0) {
- text += ' ';
- }
-
- while (clen < width) {
- if (align === 'center') {
- cell = ' ' + cell + ' ';
- clen += 2;
- } else if (align === 'left') {
- cell = cell + ' ';
- clen += 1;
- } else if (align === 'right') {
- cell = ' ' + cell;
- clen += 1;
- }
- }
-
- if (clen > width) {
- if (align === 'center') {
- cell = cell.substring(1);
- clen--;
- } else if (align === 'left') {
- cell = cell.slice(0, -1);
- clen--;
- } else if (align === 'right') {
- cell = cell.substring(1);
- clen--;
- }
- }
-
- text += cell;
- });
- if (!isFooter) {
- text += '\n\n';
- }
- });
-
- delete this.align;
- this.setContent(text);
- this.align = align;
-};
-
-Table.prototype.render = function() {
- var self = this;
-
- var coords = this._render();
- if (!coords) return;
-
- this._calculateMaxes();
-
- if (!this._maxes) return coords;
-
- var lines = this.screen.lines
- , xi = coords.xi
- , xl = coords.xl
- , yi = coords.yi
- , yl = coords.yl
- , rx
- , ry
- , i;
-
- var dattr = this.sattr(this.style)
- , hattr = this.sattr(this.style.header)
- , cattr = this.sattr(this.style.cell)
- , battr = this.sattr(this.style.border);
-
- var width = coords.xl - coords.xi - this.iright
- , height = coords.yl - coords.yi - this.ibottom;
-
- // Apply attributes to header cells and cells.
- for (var y = this.itop; y < height; y++) {
- if (!lines[yi + y]) break;
- for (var x = this.ileft; x < width; x++) {
- if (!lines[yi + y][xi + x]) break;
- // Check to see if it's not the default attr. Allows for tags:
- if (lines[yi + y][xi + x][0] !== dattr) continue;
- if (y === this.itop) {
- lines[yi + y][xi + x][0] = hattr;
- } else {
- lines[yi + y][xi + x][0] = cattr;
- }
- }
- }
-
- if (!this.border || this.options.noCellBorders) return coords;
-
- // Draw border with correct angles.
- ry = 0;
- for (i = 0; i < self.rows.length + 1; i++) {
- if (!lines[yi + ry]) break;
- rx = 0;
- self._maxes.forEach(function(max, i) {
- rx += max;
- if (i === 0) {
- if (!lines[yi + ry][xi + 0]) return;
- // left side
- if (ry === 0) {
- // top
- lines[yi + ry][xi + 0][0] = battr;
- // lines[yi + ry][xi + 0][1] = '\u250c'; // '┌'
- } else if (ry / 2 === self.rows.length) {
- // bottom
- lines[yi + ry][xi + 0][0] = battr;
- // lines[yi + ry][xi + 0][1] = '\u2514'; // '└'
- } else {
- // middle
- lines[yi + ry][xi + 0][0] = battr;
- lines[yi + ry][xi + 0][1] = '\u251c'; // '├'
- // XXX If we alter iwidth and ileft for no borders - nothing should be written here
- if (!self.border.left) {
- lines[yi + ry][xi + 0][1] = '\u2500'; // '─'
- }
- }
- } else if (i === self._maxes.length - 1) {
- if (!lines[yi + ry][xi + rx + 1]) return;
- // right side
- if (ry === 0) {
- // top
- lines[yi + ry][xi + ++rx][0] = battr;
- // lines[yi + ry][xi + rx][1] = '\u2510'; // '┐'
- } else if (ry / 2 === self.rows.length) {
- // bottom
- lines[yi + ry][xi + ++rx][0] = battr;
- // lines[yi + ry][xi + rx][1] = '\u2518'; // '┘'
- } else {
- // middle
- lines[yi + ry][xi + ++rx][0] = battr;
- lines[yi + ry][xi + rx][1] = '\u2524'; // '┤'
- // XXX If we alter iwidth and iright for no borders - nothing should be written here
- if (!self.border.right) {
- lines[yi + ry][xi + rx][1] = '\u2500'; // '─'
- }
- }
- return;
- }
- if (!lines[yi + ry][xi + rx + 1]) return;
- // center
- if (ry === 0) {
- // top
- lines[yi + ry][xi + ++rx][0] = battr;
- lines[yi + ry][xi + rx][1] = '\u252c'; // '┬'
- // XXX If we alter iheight and itop for no borders - nothing should be written here
- if (!self.border.top) {
- lines[yi + ry][xi + rx][1] = '\u2502'; // '│'
- }
- } else if (ry / 2 === self.rows.length) {
- // bottom
- lines[yi + ry][xi + ++rx][0] = battr;
- lines[yi + ry][xi + rx][1] = '\u2534'; // '┴'
- // XXX If we alter iheight and ibottom for no borders - nothing should be written here
- if (!self.border.bottom) {
- lines[yi + ry][xi + rx][1] = '\u2502'; // '│'
- }
- } else {
- // middle
- if (self.options.fillCellBorders) {
- var lbg = (ry <= 2 ? hattr : cattr) & 0x1ff;
- lines[yi + ry][xi + ++rx][0] = (battr & ~0x1ff) | lbg;
- } else {
- lines[yi + ry][xi + ++rx][0] = battr;
- }
- lines[yi + ry][xi + rx][1] = '\u253c'; // '┼'
- // ++rx;
- }
- });
- ry += 2;
- }
-
- // Draw internal borders.
- for (ry = 1; ry < self.rows.length * 2; ry++) {
- if (!lines[yi + ry]) break;
- rx = 0;
- self._maxes.slice(0, -1).forEach(function(max, i) {
- rx += max;
- if (!lines[yi + ry][xi + rx + 1]) return;
- if (ry % 2 !== 0) {
- if (self.options.fillCellBorders) {
- var lbg = (ry <= 2 ? hattr : cattr) & 0x1ff;
- lines[yi + ry][xi + ++rx][0] = (battr & ~0x1ff) | lbg;
- } else {
- lines[yi + ry][xi + ++rx][0] = battr;
- }
- lines[yi + ry][xi + rx][1] = '\u2502'; // '│'
- } else {
- rx++;
- }
- });
- rx = 1;
- self._maxes.forEach(function(max, i) {
- while (max--) {
- if (ry % 2 === 0) {
- if (!lines[yi + ry]) break;
- if (!lines[yi + ry][xi + rx + 1]) break;
- if (self.options.fillCellBorders) {
- var lbg = (ry <= 2 ? hattr : cattr) & 0x1ff;
- lines[yi + ry][xi + rx][0] = (battr & ~0x1ff) | lbg;
- } else {
- lines[yi + ry][xi + rx][0] = battr;
- }
- lines[yi + ry][xi + rx][1] = '\u2500'; // '─'
- }
- rx++;
- }
- rx++;
- });
- }
-
- return coords;
-};
-
-/**
- * ListTable
- */
-
-function ListTable(options) {
- var self = this;
-
- if (!(this instanceof Node)) {
- return new ListTable(options);
- }
-
- options = options || {};
- options.shrink = true;
- options.normalShrink = true;
- options.style = options.style || {};
- options.style.border = options.style.border || {};
- options.style.header = options.style.header || {};
- options.style.cell = options.style.cell || {};
- this.__align = options.align || 'center';
- delete options.align;
-
- options.style.selected = options.style.cell.selected;
- options.style.item = options.style.cell;
-
- List.call(this, options);
-
- this._header = new Box({
- parent: this,
- left: this.screen.autoPadding ? 0 : this.ileft,
- top: 0,
- width: 'shrink',
- height: 1,
- style: options.style.header,
- tags: options.parseTags || options.tags
- });
-
- this.on('scroll', function() {
- self._header.setFront();
- var visible = self.height - self.iheight;
- self._header.rtop = 1 + self.childBase - (self.border ? 1 : 0);
- if (!self.screen.autoPadding) {
- self._header.rtop = 1 + self.childBase;
- }
- });
-
- this.pad = options.pad != null
- ? options.pad
- : 2;
-
- this.setData(options.rows || options.data);
-
- this.on('resize', function() {
- var selected = self.selected;
- self.setData(self.rows);
- self.select(selected);
- self.screen.render();
- });
-}
-
-ListTable.prototype.__proto__ = List.prototype;
-
-ListTable.prototype.type = 'list-table';
-
-ListTable.prototype._calculateMaxes = Table.prototype._calculateMaxes;
-
-ListTable.prototype.setRows =
-ListTable.prototype.setData = function(rows) {
- var self = this
- , align = this.__align;
-
- this.clearItems();
-
- this.rows = rows || [];
-
- this._calculateMaxes();
-
- this.addItem('');
-
- this.rows.forEach(function(row, i) {
- var isHeader = i === 0;
- var isFooter = i === self.rows.length - 1;
- var text = '';
- row.forEach(function(cell, i) {
- var width = self._maxes[i];
- var clen = self.strWidth(cell);
-
- if (i !== 0) {
- text += ' ';
- }
-
- while (clen < width) {
- if (align === 'center') {
- cell = ' ' + cell + ' ';
- clen += 2;
- } else if (align === 'left') {
- cell = cell + ' ';
- clen += 1;
- } else if (align === 'right') {
- cell = ' ' + cell;
- clen += 1;
- }
- }
-
- if (clen > width) {
- if (align === 'center') {
- cell = cell.substring(1);
- clen--;
- } else if (align === 'left') {
- cell = cell.slice(0, -1);
- clen--;
- } else if (align === 'right') {
- cell = cell.substring(1);
- clen--;
- }
- }
-
- text += cell;
- });
- if (isHeader) {
- self._header.setContent(text);
- } else {
- self.addItem(text);
- }
- });
-
- this._header.setFront();
-
- this.select(0);
-};
-
-ListTable.prototype._select = ListTable.prototype.select;
-ListTable.prototype.select = function(i) {
- if (i === 0) {
- i = 1;
- }
- if (i <= this.childBase) {
- this.setScroll(this.childBase - 1);
- }
- return this._select(i);
-};
-
-ListTable.prototype.render = function() {
- var self = this;
-
- var coords = this._render();
- if (!coords) return;
-
- this._calculateMaxes();
-
- if (!this._maxes) return coords;
-
- var lines = this.screen.lines
- , xi = coords.xi
- , xl = coords.xl
- , yi = coords.yi
- , yl = coords.yl
- , rx
- , ry
- , i;
-
- var battr = this.sattr(this.style.border);
-
- var width = coords.xl - coords.xi - this.iright
- , height = coords.yl - coords.yi - this.ibottom;
-
- if (!this.border || this.options.noCellBorders) return coords;
-
- // Draw border with correct angles.
- ry = 0;
- for (i = 0; i < height + 1; i++) {
- if (!lines[yi + ry]) break;
- rx = 0;
- self._maxes.slice(0, -1).forEach(function(max, i) {
- rx += max;
- if (!lines[yi + ry][xi + rx + 1]) return;
- // center
- if (ry === 0) {
- // top
- lines[yi + ry][xi + ++rx][0] = battr;
- lines[yi + ry][xi + rx][1] = '\u252c'; // '┬'
- // XXX If we alter iheight and itop for no borders - nothing should be written here
- if (!self.border.top) {
- lines[yi + ry][xi + rx][1] = '\u2502'; // '│'
- }
- } else if (ry === height) {
- // bottom
- lines[yi + ry][xi + ++rx][0] = battr;
- lines[yi + ry][xi + rx][1] = '\u2534'; // '┴'
- // XXX If we alter iheight and ibottom for no borders - nothing should be written here
- if (!self.border.bottom) {
- lines[yi + ry][xi + rx][1] = '\u2502'; // '│'
- }
- } else {
- // middle
- ++rx;
- }
- });
- ry += 1;
- }
-
- // Draw internal borders.
- for (ry = 1; ry < height; ry++) {
- if (!lines[yi + ry]) break;
- rx = 0;
- self._maxes.slice(0, -1).forEach(function(max, i) {
- rx += max;
- if (!lines[yi + ry][xi + rx + 1]) return;
- if (self.options.fillCellBorders !== false) {
- var lbg = lines[yi + ry][xi + rx][0] & 0x1ff;
- lines[yi + ry][xi + ++rx][0] = (battr & ~0x1ff) | lbg;
- } else {
- lines[yi + ry][xi + ++rx][0] = battr;
- }
- lines[yi + ry][xi + rx][1] = '\u2502'; // '│'
- });
- }
-
- return coords;
-};
-
-/**
- * Terminal
- */
-
-function Terminal(options) {
- var self = this;
-
- if (!(this instanceof Node)) {
- return new Terminal(options);
- }
-
- options = options || {};
- options.scrollable = false;
-
- Box.call(this, options);
-
- this.handler = options.handler;
- this.shell = options.shell || process.env.SHELL || 'sh';
- this.args = options.args || [];
-
- this.cursor = this.options.cursor;
- this.cursorBlink = this.options.cursorBlink;
- this.screenKeys = this.options.screenKeys;
-
- this.style = this.style || {};
- this.style.bg = this.style.bg || 'default';
- this.style.fg = this.style.fg || 'default';
-
- this.bootstrap();
-}
-
-Terminal.prototype.__proto__ = Box.prototype;
-
-Terminal.prototype.type = 'terminal';
-
-Terminal.prototype.bootstrap = function() {
- var self = this;
-
- var element = {
- // window
- get document() { return element; },
- navigator: { userAgent: 'node.js' },
-
- // document
- get defaultView() { return element; },
- get documentElement() { return element; },
- createElement: function() { return element; },
-
- // element
- get ownerDocument() { return element; },
- addEventListener: function() {},
- removeEventListener: function() {},
- getElementsByTagName: function(name) { return [element]; },
- getElementById: function() { return element; },
- parentNode: null,
- offsetParent: null,
- appendChild: function() {},
- removeChild: function() {},
- setAttribute: function() {},
- getAttribute: function() {},
- style: {},
- focus: function() {},
- blur: function() {},
- console: console
- };
-
- element.parentNode = element;
- element.offsetParent = element;
-
- this.term = require('term.js')({
- termName: 'xterm',
- cols: this.width - this.iwidth,
- rows: this.height - this.iheight,
- context: element,
- document: element,
- body: element,
- parent: element,
- cursorBlink: this.cursorBlink,
- screenKeys: this.screenKeys
- });
-
- this.term.refresh = function() {
- self.screen.render();
- };
-
- this.term.keyDown = function() {};
- this.term.keyPress = function() {};
-
- this.term.open(element);
-
- // Emits key sequences in html-land.
- // Technically not necessary here.
- // In reality if we wanted to be neat, we would overwrite the keyDown and
- // keyPress methods with our own node.js-keys->terminal-keys methods, but
- // since all the keys are already coming in as escape sequences, we can just
- // send the input directly to the handler/socket (see below).
- // this.term.on('data', function(data) {
- // self.handler(data);
- // });
-
- // Incoming keys and mouse inputs.
- // NOTE: Cannot pass mouse events - coordinates will be off!
- this.screen.program.input.on('data', this._onData = function(data) {
- if (self.screen.focused === self && !self._isMouse(data)) {
- self.handler(data);
- }
- });
-
- this.onScreenEvent('mouse', function(data) {
- if (self.screen.focused !== self) return;
-
- if (data.x < self.aleft + self.ileft) return;
- if (data.y < self.atop + self.itop) return;
- if (data.x > self.aleft - self.ileft + self.width) return;
- if (data.y > self.atop - self.itop + self.height) return;
-
- if (self.term.x10Mouse
- || self.term.vt200Mouse
- || self.term.normalMouse
- || self.term.mouseEvents
- || self.term.utfMouse
- || self.term.sgrMouse
- || self.term.urxvtMouse) {
- ;
- } else {
- return;
- }
-
- var b = data.raw[0]
- , x = data.x - self.aleft
- , y = data.y - self.atop
- , s;
-
- if (self.term.urxvtMouse) {
- if (self.screen.program.sgrMouse) {
- b += 32;
- }
- s = '\x1b[' + b + ';' + (x + 32) + ';' + (y + 32) + 'M';
- } else if (self.term.sgrMouse) {
- if (!self.screen.program.sgrMouse) {
- b -= 32;
- }
- s = '\x1b[<' + b + ';' + x + ';' + y
- + (data.action === 'mousedown' ? 'M' : 'm');
- } else {
- if (self.screen.program.sgrMouse) {
- b += 32;
- }
- s = '\x1b[M'
- + String.fromCharCode(b)
- + String.fromCharCode(x + 32)
- + String.fromCharCode(y + 32);
- }
-
- self.handler(s);
- });
-
- this.on('focus', function() {
- self.term.focus();
- });
-
- this.on('blur', function() {
- self.term.blur();
- });
-
- this.term.on('title', function(title) {
- self.title = title;
- self.emit('title', title);
- });
-
- this.on('resize', function() {
- nextTick(function() {
- self.term.resize(self.width - self.iwidth, self.height - self.iheight);
- });
- });
-
- this.once('render', function() {
- self.term.resize(self.width - self.iwidth, self.height - self.iheight);
- });
-
- if (this.handler) {
- return;
- }
-
- this.pty = require('pty.js').fork(this.shell, this.args, {
- name: 'xterm',
- cols: this.width - this.iwidth,
- rows: this.height - this.iheight,
- cwd: process.env.HOME,
- env: process.env
- });
-
- this.on('resize', function() {
- nextTick(function() {
- self.pty.resize(self.width - self.iwidth, self.height - self.iheight);
- });
- });
-
- this.handler = function(data) {
- self.pty.write(data);
- self.screen.render();
- };
-
- this.pty.on('data', function(data) {
- self.write(data);
- self.screen.render();
- });
-
- this.pty.on('exit', function(code) {
- self.emit('exit', code || null);
- });
-
- this.onScreenEvent('keypress', function() {
- self.screen.render();
- });
-
- this.screen._listenKeys(this);
-
- this.on('destroy', function() {
- self.screen.program.removeListener('data', self._onData);
- self.pty.destroy();
- });
-};
-
-Terminal.prototype.write = function(data) {
- return this.term.write(data);
-};
-
-Terminal.prototype.render = function() {
- var ret = this._render();
- if (!ret) return;
-
- this.dattr = this.sattr(this.style);
-
- var xi = ret.xi + this.ileft
- , xl = ret.xl - this.iright
- , yi = ret.yi + this.itop
- , yl = ret.yl - this.ibottom
- , cursor;
-
- var scrollback = this.term.lines.length - (yl - yi);
-
- for (var y = Math.max(yi, 0); y < yl; y++) {
- var line = this.screen.lines[y];
- if (!line || !this.term.lines[scrollback + y - yi]) break;
-
- if (y === yi + this.term.y
- && this.term.cursorState
- && this.screen.focused === this
- && (this.term.ydisp === this.term.ybase || this.term.selectMode)
- && !this.term.cursorHidden) {
- cursor = xi + this.term.x;
- } else {
- cursor = -1;
- }
-
- for (var x = Math.max(xi, 0); x < xl; x++) {
- if (!line[x] || !this.term.lines[scrollback + y - yi][x - xi]) break;
-
- line[x][0] = this.term.lines[scrollback + y - yi][x - xi][0];
-
- if (x === cursor) {
- if (this.cursor === 'line') {
- line[x][0] = this.dattr;
- line[x][1] = '\u2502';
- continue;
- } else if (this.cursor === 'underline') {
- line[x][0] = this.dattr | (2 << 18);
- } else if (this.cursor === 'block' || !this.cursor) {
- line[x][0] = this.dattr | (8 << 18);
- }
- }
-
- line[x][1] = this.term.lines[scrollback + y - yi][x - xi][1];
-
- // default foreground = 257
- if (((line[x][0] >> 9) & 0x1ff) === 257) {
- line[x][0] &= ~(0x1ff << 9);
- line[x][0] |= ((this.dattr >> 9) & 0x1ff) << 9;
- }
-
- // default background = 256
- if ((line[x][0] & 0x1ff) === 256) {
- line[x][0] &= ~0x1ff;
- line[x][0] |= this.dattr & 0x1ff;
- }
- }
-
- line.dirty = true;
- }
-
- return ret;
-};
-
-Terminal.prototype._isMouse = function(buf) {
- var s = buf;
- 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');
- }
- }
- return (buf[0] === 0x1b && buf[1] === 0x5b && buf[2] === 0x4d)
- || /^\x1b\[M([\x00\u0020-\uffff]{3})/.test(s)
- || /^\x1b\[(\d+;\d+;\d+)M/.test(s)
- || /^\x1b\[<(\d+;\d+;\d+)([mM])/.test(s)
- || /^\x1b\[<(\d+;\d+;\d+;\d+)&w/.test(s)
- || /^\x1b\[24([0135])~\[(\d+),(\d+)\]\r/.test(s)
- || /^\x1b\[(O|I)/.test(s);
-};
-
-Terminal.prototype.setScroll =
-Terminal.prototype.scrollTo = function(offset, always) {
- this.term.ydisp = offset;
- return this.emit('scroll');
-};
-
-Terminal.prototype.getScroll = function() {
- return this.term.ydisp;
-};
-
-Terminal.prototype.scroll = function(offset, always) {
- this.term.scrollDisp(offset);
- return this.emit('scroll');
-};
-
-Terminal.prototype.resetScroll = function() {
- this.term.ydisp = 0;
- this.term.ybase = 0;
- return this.emit('scroll');
-};
-
-Terminal.prototype.getScrollHeight = function() {
- return this.term.rows - 1;
-};
-
-Terminal.prototype.getScrollPerc = function(s) {
- return (this.term.ydisp / this.term.ybase) * 100;
-};
-
-Terminal.prototype.setScrollPerc = function(i) {
- return this.setScroll((i / 100) * this.term.ybase | 0);
-};
-
-Terminal.prototype.screenshot = function(xi, xl, yi, yl) {
- xi = 0 + (xi || 0);
- if (xl != null) {
- xl = 0 + (xl || 0);
- } else {
- xl = this.term.lines[0].length;
- }
- yi = 0 + (yi || 0);
- if (yl != null) {
- yl = 0 + (yl || 0);
- } else {
- yl = this.term.lines.length;
- }
- return this.screen.screenshot(xi, xl, yi, yl, this.term);
-};
-
-/**
- * Image
- * Good example of w3mimgdisplay commands:
- * https://github.com/hut/ranger/blob/master/ranger/ext/img_display.py
- */
-
-function Image(options) {
- var self = this;
-
- if (!(this instanceof Node)) {
- return new Image(options);
- }
-
- options = options || {};
-
- Box.call(this, options);
-
- if (options.w3m) {
- Image.w3mdisplay = options.w3m;
- }
-
- if (Image.hasW3MDisplay == null) {
- if (fs.existsSync(Image.w3mdisplay)) {
- Image.hasW3MDisplay = true;
- } else if (options.search !== false) {
- var file = helpers.findFile('/usr', 'w3mimgdisplay')
- || helpers.findFile('/lib', 'w3mimgdisplay')
- || helpers.findFile('/bin', 'w3mimgdisplay');
- if (file) {
- Image.hasW3MDisplay = true;
- Image.w3mdisplay = file;
- } else {
- Image.hasW3MDisplay = false;
- }
- }
- }
-
- this.on('hide', function() {
- self._lastFile = self.file;
- self.clearImage();
- });
-
- this.on('show', function() {
- if (!self._lastFile) return;
- self.setImage(self._lastFile);
- });
-
- this.on('detach', function() {
- self._lastFile = self.file;
- self.clearImage();
- });
-
- this.on('attach', function() {
- if (!self._lastFile) return;
- self.setImage(self._lastFile);
- });
-
- this.onScreenEvent('resize', function() {
- self._needsRatio = true;
- });
-
- // Get images to overlap properly. Maybe not worth it:
- // this.onScreenEvent('render', function() {
- // self.screen.program.flush();
- // if (!self._noImage) return;
- // function display(el, next) {
- // if (el.type === 'image' && el.file) {
- // el.setImage(el.file, next);
- // } else {
- // next();
- // }
- // }
- // function done(el) {
- // el.children.forEach(recurse);
- // }
- // function recurse(el) {
- // display(el, function() {
- // var pending = el.children.length;
- // el.children.forEach(function(el) {
- // display(el, function() {
- // if (!--pending) done(el);
- // });
- // });
- // });
- // }
- // recurse(self.screen);
- // });
-
- this.onScreenEvent('render', function() {
- self.screen.program.flush();
- if (!self._noImage) {
- self.setImage(self.file);
- }
- });
-
- if (this.options.file || this.options.img) {
- this.setImage(this.options.file || this.options.img);
- }
-}
-
-Image.prototype.__proto__ = Box.prototype;
-
-Image.prototype.type = 'image';
-
-Image.w3mdisplay = '/usr/lib/w3m/w3mimgdisplay';
-
-Image.prototype.spawn = function(file, args, opt, callback) {
- var screen = this.screen
- , opt = opt || {}
- , spawn = require('child_process').spawn
- , ps;
-
- ps = spawn(file, args, opt);
-
- ps.on('error', function(err) {
- if (!callback) return;
- return callback(err);
- });
-
- ps.on('exit', function(code) {
- if (!callback) return;
- if (code !== 0) return callback(new Error('Exit Code: ' + code));
- return callback(null, code === 0);
- });
-
- return ps;
-};
-
-Image.prototype.setImage = function(img, callback) {
- var self = this;
-
- if (this._settingImage) {
- this._queue = this._queue || [];
- this._queue.push([img, callback]);
- return;
- }
- this._settingImage = true;
-
- var reset = function(err, success) {
- self._settingImage = false;
- self._queue = self._queue || [];
- var item = self._queue.shift();
- if (item) {
- self.setImage(item[0], item[1]);
- }
- };
-
- if (Image.hasW3MDisplay === false) {
- reset();
- if (!callback) return;
- return callback(new Error('W3M Image Display not available.'));
- }
-
- if (!img) {
- reset();
- if (!callback) return;
- return callback(new Error('No image.'));
- }
-
- this.file = img;
-
- return this.getPixelRatio(function(err, ratio) {
- if (err) {
- reset();
- if (!callback) return;
- return callback(err);
- }
-
- return self.renderImage(img, ratio, function(err, success) {
- if (err) {
- reset();
- if (!callback) return;
- return callback(err);
- }
-
- if (self.shrink || self.options.autofit) {
- delete self.shrink;
- delete self.options.shrink;
- self.options.autofit = true;
- return self.imageSize(function(err, size) {
- if (err) {
- reset();
- if (!callback) return;
- return callback(err);
- }
-
- if (self._lastSize
- && ratio.tw === self._lastSize.tw
- && ratio.th === self._lastSize.th
- && size.width === self._lastSize.width
- && size.height === self._lastSize.height
- && self.aleft === self._lastSize.aleft
- && self.atop === self._lastSize.atop) {
- reset();
- if (!callback) return;
- return callback(null, success);
- }
-
- self._lastSize = {
- tw: ratio.tw,
- th: ratio.th,
- width: size.width,
- height: size.height,
- aleft: self.aleft,
- atop: self.atop
- };
-
- self.position.width = size.width / ratio.tw | 0;
- self.position.height = size.height / ratio.th | 0;
-
- self._noImage = true;
- self.screen.render();
- self._noImage = false;
-
- reset();
- return self.renderImage(img, ratio, callback);
- });
- }
-
- reset();
- if (!callback) return;
- return callback(null, success);
- });
- });
-};
-
-Image.prototype.renderImage = function(img, ratio, callback) {
- var self = this;
-
- if (cp.execSync) {
- callback = callback || function(err, result) { return result; };
- try {
- return callback(null, this.renderImageSync(img, ratio));
- } catch (e) {
- return callback(e);
- }
- }
-
- if (Image.hasW3MDisplay === false) {
- if (!callback) return;
- return callback(new Error('W3M Image Display not available.'));
- }
-
- if (!ratio) {
- if (!callback) return;
- return callback(new Error('No ratio.'));
- }
-
- // clearImage unsets these:
- var _file = self.file;
- var _lastSize = self._lastSize;
- return self.clearImage(function(err) {
- if (err) return callback(err);
-
- self.file = _file;
- self._lastSize = _lastSize;
-
- var opt = {
- stdio: 'pipe',
- env: process.env,
- cwd: process.env.HOME
- };
-
- var ps = self.spawn(Image.w3mdisplay, [], opt, function(err, success) {
- if (!callback) return;
- return err
- ? callback(err)
- : callback(null, success);
- });
-
- var width = self.width * ratio.tw | 0
- , height = self.height * ratio.th | 0
- , aleft = self.aleft * ratio.tw | 0
- , atop = self.atop * ratio.th | 0;
-
- var input = '0;1;'
- + aleft + ';'
- + atop + ';'
- + width + ';'
- + height + ';;;;;'
- + img
- + '\n4;\n3;\n';
-
- self._props = {
- aleft: aleft,
- atop: atop,
- width: width,
- height: height
- };
-
- ps.stdin.write(input);
- ps.stdin.end();
- });
-};
-
-Image.prototype.clearImage = function(callback) {
- var self = this;
-
- if (cp.execSync) {
- callback = callback || function(err, result) { return result; };
- try {
- return callback(null, this.clearImageSync());
- } catch (e) {
- return callback(e);
- }
- }
-
- if (Image.hasW3MDisplay === false) {
- if (!callback) return;
- return callback(new Error('W3M Image Display not available.'));
- }
-
- if (!this._props) {
- if (!callback) return;
- return callback(null);
- }
-
- var opt = {
- stdio: 'pipe',
- env: process.env,
- cwd: process.env.HOME
- };
-
- var ps = this.spawn(Image.w3mdisplay, [], opt, function(err, success) {
- if (!callback) return;
- return err
- ? callback(err)
- : callback(null, success);
- });
-
- var width = this._props.width + 2
- , height = this._props.height + 2
- , aleft = this._props.aleft
- , atop = this._props.atop;
-
- if (this._drag) {
- aleft -= 10;
- atop -= 10;
- width += 10;
- height += 10;
- }
-
- var input = '6;'
- + aleft + ';'
- + atop + ';'
- + width + ';'
- + height
- + '\n4;\n3;\n';
-
- delete this.file;
- delete this._props;
- delete this._lastSize;
-
- ps.stdin.write(input);
- ps.stdin.end();
-};
-
-Image.prototype.imageSize = function(callback) {
- var self = this;
- var img = this.file;
-
- if (cp.execSync) {
- callback = callback || function(err, result) { return result; };
- try {
- return callback(null, this.imageSizeSync());
- } catch (e) {
- return callback(e);
- }
- }
-
- if (Image.hasW3MDisplay === false) {
- if (!callback) return;
- return callback(new Error('W3M Image Display not available.'));
- }
-
- if (!img) {
- if (!callback) return;
- return callback(new Error('No image.'));
- }
-
- var opt = {
- stdio: 'pipe',
- env: process.env,
- cwd: process.env.HOME
- };
-
- var ps = this.spawn(Image.w3mdisplay, [], opt);
-
- var buf = '';
-
- ps.stdout.setEncoding('utf8');
-
- ps.stdout.on('data', function(data) {
- buf += data;
- });
-
- ps.on('error', function(err) {
- if (!callback) return;
- return callback(err);
- });
-
- ps.on('exit', function() {
- if (!callback) return;
- var size = buf.trim().split(/\s+/);
- return callback(null, {
- raw: buf.trim(),
- width: +size[0],
- height: +size[1]
- });
- });
-
- var input = '5;' + img + '\n';
-
- ps.stdin.write(input);
- ps.stdin.end();
-};
-
-Image.prototype.termSize = function(callback) {
- var self = this;
-
- if (cp.execSync) {
- callback = callback || function(err, result) { return result; };
- try {
- return callback(null, this.termSizeSync());
- } catch (e) {
- return callback(e);
- }
- }
-
- if (Image.hasW3MDisplay === false) {
- if (!callback) return;
- return callback(new Error('W3M Image Display not available.'));
- }
-
- var opt = {
- stdio: 'pipe',
- env: process.env,
- cwd: process.env.HOME
- };
-
- var ps = this.spawn(Image.w3mdisplay, ['-test'], opt);
-
- var buf = '';
-
- ps.stdout.setEncoding('utf8');
-
- ps.stdout.on('data', function(data) {
- buf += data;
- });
-
- ps.on('error', function(err) {
- if (!callback) return;
- return callback(err);
- });
-
- ps.on('exit', function() {
- if (!callback) return;
-
- if (!buf.trim()) {
- // Bug: w3mimgdisplay will sometimes
- // output nothing. Try again:
- return self.termSize(callback);
- }
-
- var size = buf.trim().split(/\s+/);
-
- return callback(null, {
- raw: buf.trim(),
- width: +size[0],
- height: +size[1]
- });
- });
-
- ps.stdin.end();
-};
-
-Image.prototype.getPixelRatio = function(callback) {
- var self = this;
-
- if (cp.execSync) {
- callback = callback || function(err, result) { return result; };
- try {
- return callback(null, this.getPixelRatioSync());
- } catch (e) {
- return callback(e);
- }
- }
-
- // XXX We could cache this, but sometimes it's better
- // to recalculate to be pixel perfect.
- if (this._ratio && !this._needsRatio) {
- return callback(null, this._ratio);
- }
-
- return this.termSize(function(err, dimensions) {
- if (err) return callback(err);
-
- self._ratio = {
- tw: dimensions.width / self.screen.width,
- th: dimensions.height / self.screen.height
- };
-
- self._needsRatio = false;
-
- return callback(null, self._ratio);
- });
-};
-
-Image.prototype.renderImageSync = function(img, ratio) {
- var self = this;
-
- if (Image.hasW3MDisplay === false) {
- throw new Error('W3M Image Display not available.');
- }
-
- if (!ratio) {
- throw new Error('No ratio.');
- }
-
- // clearImage unsets these:
- var _file = this.file;
- var _lastSize = this._lastSize;
-
- this.clearImageSync();
-
- this.file = _file;
- this._lastSize = _lastSize;
-
- var width = this.width * ratio.tw | 0
- , height = this.height * ratio.th | 0
- , aleft = this.aleft * ratio.tw | 0
- , atop = this.atop * ratio.th | 0;
-
- var input = '0;1;'
- + aleft + ';'
- + atop + ';'
- + width + ';'
- + height + ';;;;;'
- + img
- + '\n4;\n3;\n';
-
- this._props = {
- aleft: aleft,
- atop: atop,
- width: width,
- height: height
- };
-
- try {
- cp.execFileSync(Image.w3mdisplay, [], {
- env: process.env,
- encoding: 'utf8',
- input: input,
- timeout: 1000
- });
- } catch (e) {
- ;
- }
-
- return true;
-};
-
-Image.prototype.clearImageSync = function() {
- if (Image.hasW3MDisplay === false) {
- throw new Error('W3M Image Display not available.');
- }
-
- if (!this._props) {
- return false;
- }
-
- var width = this._props.width + 2
- , height = this._props.height + 2
- , aleft = this._props.aleft
- , atop = this._props.atop;
-
- if (this._drag) {
- aleft -= 10;
- atop -= 10;
- width += 10;
- height += 10;
- }
-
- var input = '6;'
- + aleft + ';'
- + atop + ';'
- + width + ';'
- + height
- + '\n4;\n3;\n';
-
- delete this.file;
- delete this._props;
- delete this._lastSize;
-
- try {
- cp.execFileSync(Image.w3mdisplay, [], {
- env: process.env,
- encoding: 'utf8',
- input: input,
- timeout: 1000
- });
- } catch (e) {
- ;
- }
-
- return true;
-};
-
-Image.prototype.imageSizeSync = function() {
- var img = this.file;
-
- if (Image.hasW3MDisplay === false) {
- throw new Error('W3M Image Display not available.');
- }
-
- if (!img) {
- throw new Error('No image.');
- }
-
- var buf = '';
- var input = '5;' + img + '\n';
-
- try {
- buf = cp.execFileSync(Image.w3mdisplay, [], {
- env: process.env,
- encoding: 'utf8',
- input: input,
- timeout: 1000
- });
- } catch (e) {
- ;
- }
-
- var size = buf.trim().split(/\s+/);
-
- return {
- raw: buf.trim(),
- width: +size[0],
- height: +size[1]
- };
-};
-
-Image.prototype.termSizeSync = function(_, recurse) {
- if (Image.hasW3MDisplay === false) {
- throw new Error('W3M Image Display not available.');
- }
-
- var buf = '';
-
- try {
- buf = cp.execFileSync(Image.w3mdisplay, ['-test'], {
- env: process.env,
- encoding: 'utf8',
- timeout: 1000
- });
- } catch (e) {
- ;
- }
-
- if (!buf.trim()) {
- // Bug: w3mimgdisplay will sometimes
- // output nothing. Try again:
- recurse = recurse || 0;
- if (++recurse === 5) {
- throw new Error('Term size not determined.');
- }
- return this.termSizeSync(_, recurse);
- }
-
- var size = buf.trim().split(/\s+/);
-
- return {
- raw: buf.trim(),
- width: +size[0],
- height: +size[1]
- };
-};
-
-Image.prototype.getPixelRatioSync = function() {
- var self = this;
-
- // XXX We could cache this, but sometimes it's better
- // to recalculate to be pixel perfect.
- if (this._ratio && !this._needsRatio) {
- return this._ratio;
- }
- this._needsRatio = false;
-
- var dimensions = this.termSizeSync();
-
- this._ratio = {
- tw: dimensions.width / this.screen.width,
- th: dimensions.height / this.screen.height
- };
-
- return this._ratio;
-};
-
-Image.prototype.displayImage = function(callback) {
- return this.screen.displayImage(this.file, callback);
-};
-
-/**
- * Angle Table
- */
-
-var angles = {
- '\u2518': true, // '┘'
- '\u2510': true, // '┐'
- '\u250c': true, // '┌'
- '\u2514': true, // '└'
- '\u253c': true, // '┼'
- '\u251c': true, // '├'
- '\u2524': true, // '┤'
- '\u2534': true, // '┴'
- '\u252c': true, // '┬'
- '\u2502': true, // '│'
- '\u2500': true // '─'
-};
-
-var langles = {
- '\u250c': true, // '┌'
- '\u2514': true, // '└'
- '\u253c': true, // '┼'
- '\u251c': true, // '├'
- '\u2534': true, // '┴'
- '\u252c': true, // '┬'
- '\u2500': true // '─'
-};
-
-var uangles = {
- '\u2510': true, // '┐'
- '\u250c': true, // '┌'
- '\u253c': true, // '┼'
- '\u251c': true, // '├'
- '\u2524': true, // '┤'
- '\u252c': true, // '┬'
- '\u2502': true // '│'
-};
-
-var rangles = {
- '\u2518': true, // '┘'
- '\u2510': true, // '┐'
- '\u253c': true, // '┼'
- '\u2524': true, // '┤'
- '\u2534': true, // '┴'
- '\u252c': true, // '┬'
- '\u2500': true // '─'
-};
-
-var dangles = {
- '\u2518': true, // '┘'
- '\u2514': true, // '└'
- '\u253c': true, // '┼'
- '\u251c': true, // '├'
- '\u2524': true, // '┤'
- '\u2534': true, // '┴'
- '\u2502': true // '│'
-};
-
-var cdangles = {
- '\u250c': true // '┌'
-};
-
-// Every ACS angle character can be
-// represented by 4 bits ordered like this:
-// [langle][uangle][rangle][dangle]
-var angleTable = {
- '0000': '', // ?
- '0001': '\u2502', // '│' // ?
- '0010': '\u2500', // '─' // ??
- '0011': '\u250c', // '┌'
- '0100': '\u2502', // '│' // ?
- '0101': '\u2502', // '│'
- '0110': '\u2514', // '└'
- '0111': '\u251c', // '├'
- '1000': '\u2500', // '─' // ??
- '1001': '\u2510', // '┐'
- '1010': '\u2500', // '─' // ??
- '1011': '\u252c', // '┬'
- '1100': '\u2518', // '┘'
- '1101': '\u2524', // '┤'
- '1110': '\u2534', // '┴'
- '1111': '\u253c' // '┼'
-};
-
-Object.keys(angleTable).forEach(function(key) {
- angleTable[parseInt(key, 2)] = angleTable[key];
- delete angleTable[key];
-});
-
-/**
- * Helpers
- */
-
-var helpers = {};
-
-helpers.merge = function(a, b) {
- Object.keys(b).forEach(function(key) {
- a[key] = b[key];
- });
- return a;
-};
-
-helpers.asort = function(obj) {
- return obj.sort(function(a, b) {
- a = a.name.toLowerCase();
- b = b.name.toLowerCase();
-
- if (a[0] === '.' && b[0] === '.') {
- a = a[1];
- b = b[1];
- } else {
- a = a[0];
- b = b[0];
- }
-
- return a > b ? 1 : (a < b ? -1 : 0);
- });
-};
-
-helpers.hsort = function(obj) {
- return obj.sort(function(a, b) {
- return b.index - a.index;
- });
-};
-
-helpers.findFile = function(start, target) {
- return (function read(dir) {
- var files, file, stat, out;
-
- if (dir === '/dev' || dir === '/sys'
- || dir === '/proc' || dir === '/net') {
- return null;
- }
-
- try {
- files = fs.readdirSync(dir);
- } catch (e) {
- files = [];
- }
-
- for (var i = 0; i < files.length; i++) {
- file = files[i];
-
- if (file === target) {
- return (dir === '/' ? '' : dir) + '/' + file;
- }
-
- try {
- stat = fs.lstatSync((dir === '/' ? '' : dir) + '/' + file);
- } catch (e) {
- stat = null;
- }
-
- if (stat && stat.isDirectory() && !stat.isSymbolicLink()) {
- out = read((dir === '/' ? '' : dir) + '/' + file);
- if (out) return out;
- }
- }
-
- return null;
- })(start);
-};
-
-// Escape text for tag-enabled elements.
-helpers.escape = function(text) {
- return text.replace(/[{}]/g, function(ch) {
- return ch === '{' ? '{open}' : '{close}';
- });
-};
-
-helpers.parseTags = function(text) {
- return Element.prototype._parseTags.call(
- { parseTags: true, screen: Screen.global }, text);
-};
-
-helpers.generateTags = function(style, text) {
- var open = ''
- , close = '';
-
- Object.keys(style || {}).forEach(function(key) {
- var val = style[key];
- if (typeof val === 'string') {
- val = val.replace(/^light(?!-)/, 'light-');
- val = val.replace(/^bright(?!-)/, 'bright-');
- open = '{' + val + '-' + key + '}' + open;
- close += '{/' + val + '-' + key + '}';
- } else {
- if (val === true) {
- open = '{' + key + '}' + open;
- close += '{/' + key + '}';
- }
- }
- });
-
- if (text != null) {
- return open + text + close;
- }
-
- return {
- open: open,
- close: close
- };
-};
-
-helpers.attrToBinary = function(style, element) {
- return Element.prototype.sattr.call(element || {}, style);
-};
-
-helpers.stripTags = function(text) {
- if (!text) return '';
- return text
- .replace(/{(\/?)([\w\-,;!#]*)}/g, '')
- .replace(/\x1b\[[\d;]*m/g, '');
-};
-
-helpers.cleanTags = function(text) {
- return helpers.stripTags(text).trim();
-};
-
-helpers.dropUnicode = function(text) {
- if (!text) return '';
- return text
- .replace(unicode.chars.all, '??')
- .replace(unicode.chars.combining, '')
- .replace(unicode.chars.surrogate, '?');
-};
-
-/**
- * Expose
- */
-
-exports.Node = exports.node = Node;
-exports.Screen = exports.screen = Screen;
-exports.Element = exports.element = Element;
-exports.Box = exports.box = Box;
-exports.Text = exports.text = Text;
-exports.Line = exports.line = Line;
-exports.ScrollableBox = exports.scrollablebox = ScrollableBox;
-exports.List = exports.list = List;
-exports.ScrollableText = exports.scrollabletext = ScrollableText;
-exports.Form = exports.form = Form;
-exports.Input = exports.input = Input;
-exports.Textbox = exports.textbox = Textbox;
-exports.Textarea = exports.textarea = Textarea;
-exports.Button = exports.button = Button;
-exports.ProgressBar = exports.progressbar = ProgressBar;
-exports.FileManager = exports.filemanager = FileManager;
-
-exports.Checkbox = exports.checkbox = Checkbox;
-exports.RadioSet = exports.radioset = RadioSet;
-exports.RadioButton = exports.radiobutton = RadioButton;
-
-exports.Prompt = exports.prompt = Prompt;
-exports.Question = exports.question = Question;
-exports.Message = exports.message = Message;
-exports.Loading = exports.loading = Loading;
-exports.Listbar = exports.listbar = Listbar;
-
-exports.Log = exports.log = Log;
-exports.Table = exports.table = Table;
-exports.ListTable = exports.listtable = ListTable;
-
-exports.Terminal = exports.terminal = Terminal;
-exports.Image = exports.image = Image;
-
-exports.helpers = helpers;
+exports.Node = exports.node = require('./widgets/node');
+exports.Screen = exports.screen = require('./widgets/screen');
+exports.Element = exports.element = require('./widgets/element');
+exports.Box = exports.box = require('./widgets/box');
+exports.Text = exports.text = require('./widgets/text');
+exports.Line = exports.line = require('./widgets/line');
+exports.ScrollableBox = exports.scrollablebox = require('./widgets/scrollablebox');
+exports.ScrollableText = exports.scrollabletext = require('./widgets/scrollabletext');
+exports.List = exports.list = require('./widgets/list');
+exports.Form = exports.form = require('./widgets/form');
+exports.Input = exports.input = require('./widgets/input');
+exports.Textarea = exports.textarea = require('./widgets/textarea');
+exports.Textbox = exports.textbox = require('./widgets/textbox');
+exports.Button = exports.button = require('./widgets/button');
+exports.ProgressBar = exports.progressbar = require('./widgets/progressbar');
+exports.FileManager = exports.filemanager = require('./widgets/filemanager');
+
+exports.Checkbox = exports.checkbox = require('./widgets/checkbox');
+exports.RadioSet = exports.radioset = require('./widgets/radioset');
+exports.RadioButton = exports.radiobutton = require('./widgets/radiobutton');
+
+exports.Prompt = exports.prompt = require('./widgets/prompt');
+exports.Question = exports.question = require('./widgets/question');
+exports.Message = exports.message = require('./widgets/message');
+exports.Loading = exports.loading = require('./widgets/loading');
+exports.Listbar = exports.listbar = require('./widgets/listbar');
+
+exports.Log = exports.log = require('./widgets/log');
+exports.Table = exports.table = require('./widgets/table');
+exports.ListTable = exports.listtable = require('./widgets/listtable');
+
+exports.Terminal = exports.terminal = require('./widgets/terminal');
+exports.Image = exports.image = require('./widgets/image');
+
+exports.helpers = require('./helpers');
diff --git a/lib/widgets/box.js b/lib/widgets/box.js
new file mode 100644
index 0000000..bb7ca3d
--- /dev/null
+++ b/lib/widgets/box.js
@@ -0,0 +1,32 @@
+/**
+ * box.js - box element for blessed
+ * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
+ * https://github.com/chjj/blessed
+ */
+
+/**
+ * Modules
+ */
+
+var helpers = require('../helpers');
+
+var Node = require('./node');
+var Element = require('./element');
+
+/**
+ * Box
+ */
+
+function Box(options) {
+ if (!(this instanceof Node)) {
+ return new Box(options);
+ }
+ options = options || {};
+ Element.call(this, options);
+}
+
+Box.prototype.__proto__ = Element.prototype;
+
+Box.prototype.type = 'box';
+
+module.exports = Box;
diff --git a/lib/widgets/button.js b/lib/widgets/button.js
new file mode 100644
index 0000000..3d2a553
--- /dev/null
+++ b/lib/widgets/button.js
@@ -0,0 +1,60 @@
+/**
+ * button.js - button element for blessed
+ * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
+ * https://github.com/chjj/blessed
+ */
+
+/**
+ * Modules
+ */
+
+var helpers = require('../helpers');
+
+var Node = require('./node');
+var Input = require('./input');
+
+/**
+ * Button
+ */
+
+function Button(options) {
+ var self = this;
+
+ if (!(this instanceof Node)) {
+ return new Button(options);
+ }
+
+ options = options || {};
+
+ if (options.autoFocus == null) {
+ options.autoFocus = false;
+ }
+
+ Input.call(this, options);
+
+ this.on('keypress', function(ch, key) {
+ if (key.name === 'enter' || key.name === 'space') {
+ return self.press();
+ }
+ });
+
+ if (this.options.mouse) {
+ this.on('click', function() {
+ return self.press();
+ });
+ }
+}
+
+Button.prototype.__proto__ = Input.prototype;
+
+Button.prototype.type = 'button';
+
+Button.prototype.press = function() {
+ this.focus();
+ this.value = true;
+ var result = this.emit('press');
+ delete this.value;
+ return result;
+};
+
+module.exports = Button;
diff --git a/lib/widgets/checkbox.js b/lib/widgets/checkbox.js
new file mode 100644
index 0000000..3e1efc8
--- /dev/null
+++ b/lib/widgets/checkbox.js
@@ -0,0 +1,89 @@
+/**
+ * checkbox.js - checkbox element for blessed
+ * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
+ * https://github.com/chjj/blessed
+ */
+
+/**
+ * Modules
+ */
+
+var helpers = require('../helpers');
+
+var Node = require('./node');
+var Input = require('./input');
+
+/**
+ * Checkbox
+ */
+
+function Checkbox(options) {
+ var self = this;
+
+ if (!(this instanceof Node)) {
+ return new Checkbox(options);
+ }
+
+ options = options || {};
+
+ Input.call(this, options);
+
+ this.text = options.content || options.text || '';
+ this.checked = this.value = options.checked || false;
+
+ this.on('keypress', function(ch, key) {
+ if (key.name === 'enter' || key.name === 'space') {
+ self.toggle();
+ self.screen.render();
+ }
+ });
+
+ if (options.mouse) {
+ this.on('click', function() {
+ self.toggle();
+ self.screen.render();
+ });
+ }
+
+ this.on('focus', function(old) {
+ var lpos = self.lpos;
+ if (!lpos) return;
+ self.screen.program.lsaveCursor('checkbox');
+ self.screen.program.cup(lpos.yi, lpos.xi + 1);
+ self.screen.program.showCursor();
+ });
+
+ this.on('blur', function() {
+ self.screen.program.lrestoreCursor('checkbox', true);
+ });
+}
+
+Checkbox.prototype.__proto__ = Input.prototype;
+
+Checkbox.prototype.type = 'checkbox';
+
+Checkbox.prototype.render = function() {
+ this.clearPos(true);
+ this.setContent('[' + (this.checked ? 'x' : ' ') + '] ' + this.text, true);
+ return this._render();
+};
+
+Checkbox.prototype.check = function() {
+ if (this.checked) return;
+ this.checked = this.value = true;
+ this.emit('check');
+};
+
+Checkbox.prototype.uncheck = function() {
+ if (!this.checked) return;
+ this.checked = this.value = false;
+ this.emit('uncheck');
+};
+
+Checkbox.prototype.toggle = function() {
+ return this.checked
+ ? this.uncheck()
+ : this.check();
+};
+
+module.exports = Checkbox;
diff --git a/lib/widgets/element.js b/lib/widgets/element.js
new file mode 100644
index 0000000..c0872e7
--- /dev/null
+++ b/lib/widgets/element.js
@@ -0,0 +1,2653 @@
+/**
+ * element.js - base element for blessed
+ * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
+ * https://github.com/chjj/blessed
+ */
+
+/**
+ * Modules
+ */
+
+var assert = require('assert');
+
+var colors = require('../colors')
+ , unicode = require('../unicode');
+
+var nextTick = global.setImmediate || process.nextTick.bind(process);
+
+var helpers = require('../helpers');
+
+var Node = require('./node');
+
+/**
+ * Element
+ */
+
+function Element(options) {
+ var self = this;
+
+ if (!(this instanceof Node)) {
+ return new Element(options);
+ }
+
+ options = options || {};
+
+ // Workaround to get a `scrollable` option.
+ if (options.scrollable && !this._ignore && this.type !== 'scrollable-box') {
+ var ScrollableBox = require('./scrollablebox');
+ Object.getOwnPropertyNames(ScrollableBox.prototype).forEach(function(key) {
+ if (key === 'type') return;
+ Object.defineProperty(this, key,
+ Object.getOwnPropertyDescriptor(ScrollableBox.prototype, key));
+ }, this);
+ this._ignore = true;
+ ScrollableBox.call(this, options);
+ delete this._ignore;
+ return this;
+ }
+
+ Node.call(this, options);
+
+ this.name = options.name;
+
+ options.position = options.position || {
+ left: options.left,
+ right: options.right,
+ top: options.top,
+ bottom: options.bottom,
+ width: options.width,
+ height: options.height
+ };
+
+ if (options.position.width === 'shrink'
+ || options.position.height === 'shrink') {
+ if (options.position.width === 'shrink') {
+ delete options.position.width;
+ }
+ if (options.position.height === 'shrink') {
+ delete options.position.height;
+ }
+ options.shrink = true;
+ }
+
+ this.position = options.position;
+
+ this.noOverflow = options.noOverflow;
+ this.dockBorders = options.dockBorders;
+ this.shadow = options.shadow;
+
+ this.style = options.style;
+
+ if (!this.style) {
+ this.style = {};
+ this.style.fg = options.fg;
+ this.style.bg = options.bg;
+ this.style.bold = options.bold;
+ this.style.underline = options.underline;
+ this.style.blink = options.blink;
+ this.style.inverse = options.inverse;
+ this.style.invisible = options.invisible;
+ this.style.transparent = options.transparent;
+ }
+
+ this.hidden = options.hidden || false;
+ this.fixed = options.fixed || false;
+ this.align = options.align || 'left';
+ this.valign = options.valign || 'top';
+ this.wrap = options.wrap !== false;
+ this.shrink = options.shrink;
+ this.fixed = options.fixed;
+ this.ch = options.ch || ' ';
+
+ if (typeof options.padding === 'number' || !options.padding) {
+ options.padding = {
+ left: options.padding,
+ top: options.padding,
+ right: options.padding,
+ bottom: options.padding
+ };
+ }
+
+ this.padding = {
+ left: options.padding.left || 0,
+ top: options.padding.top || 0,
+ right: options.padding.right || 0,
+ bottom: options.padding.bottom || 0
+ };
+
+ this.border = options.border;
+ if (this.border) {
+ if (typeof this.border === 'string') {
+ this.border = { type: this.border };
+ }
+ this.border.type = this.border.type || 'bg';
+ if (this.border.type === 'ascii') this.border.type = 'line';
+ this.border.ch = this.border.ch || ' ';
+ this.style.border = this.style.border || this.border.style;
+ if (!this.style.border) {
+ this.style.border = {};
+ this.style.border.fg = this.border.fg;
+ this.style.border.bg = this.border.bg;
+ }
+ //this.border.style = this.style.border;
+ if (this.border.left == null) this.border.left = true;
+ if (this.border.top == null) this.border.top = true;
+ if (this.border.right == null) this.border.right = true;
+ if (this.border.bottom == null) this.border.bottom = true;
+ }
+
+ if (options.clickable) {
+ this.screen._listenMouse(this);
+ }
+
+ if (options.input || options.keyable) {
+ this.screen._listenKeys(this);
+ }
+
+ this.parseTags = options.parseTags || options.tags;
+
+ this.setContent(options.content || '', true);
+
+ if (options.label) {
+ this.setLabel(options.label);
+ }
+
+ if (options.hoverText) {
+ this.setHover(options.hoverText);
+ }
+
+ // TODO: Possibly move this to Node for onScreenEvent('mouse', ...).
+ this.on('newListener', function fn(type) {
+ // type = type.split(' ').slice(1).join(' ');
+ if (type === 'mouse'
+ || type === 'click'
+ || type === 'mouseover'
+ || type === 'mouseout'
+ || type === 'mousedown'
+ || type === 'mouseup'
+ || type === 'mousewheel'
+ || type === 'wheeldown'
+ || type === 'wheelup'
+ || type === 'mousemove') {
+ self.screen._listenMouse(self);
+ } else if (type === 'keypress' || type.indexOf('key ') === 0) {
+ self.screen._listenKeys(self);
+ }
+ });
+
+ this.on('resize', function() {
+ self.parseContent();
+ });
+
+ this.on('attach', function() {
+ self.parseContent();
+ });
+
+ this.on('detach', function() {
+ delete self.lpos;
+ });
+
+ if (options.hoverBg != null) {
+ options.hoverEffects = options.hoverEffects || {};
+ options.hoverEffects.bg = options.hoverBg;
+ }
+
+ if (this.style.hover) {
+ options.hoverEffects = this.style.hover;
+ }
+
+ if (this.style.focus) {
+ options.focusEffects = this.style.focus;
+ }
+
+ if (options.effects) {
+ if (options.effects.hover) options.hoverEffects = options.effects.hover;
+ if (options.effects.focus) options.focusEffects = options.effects.focus;
+ }
+
+ [['hoverEffects', 'mouseover', 'mouseout', '_htemp'],
+ ['focusEffects', 'focus', 'blur', '_ftemp']].forEach(function(props) {
+ var pname = props[0], over = props[1], out = props[2], temp = props[3];
+ self.screen.setEffects(self, self, over, out, self.options[pname], temp);
+ });
+
+ if (this.options.draggable) {
+ this.draggable = true;
+ }
+
+ if (options.focused) {
+ this.focus();
+ }
+}
+
+Element.prototype.__proto__ = Node.prototype;
+
+Element.prototype.type = 'element';
+
+Element.prototype.__defineGetter__('focused', function() {
+ return this.screen.focused === this;
+});
+
+Element.prototype.sattr = function(style, fg, bg) {
+ var bold = style.bold
+ , underline = style.underline
+ , blink = style.blink
+ , inverse = style.inverse
+ , invisible = style.invisible;
+
+ // if (arguments.length === 1) {
+ if (fg == null && bg == null) {
+ fg = style.fg;
+ bg = style.bg;
+ }
+
+ // This used to be a loop, but I decided
+ // to unroll it for performance's sake.
+ if (typeof bold === 'function') bold = bold(this);
+ if (typeof underline === 'function') underline = underline(this);
+ if (typeof blink === 'function') blink = blink(this);
+ if (typeof inverse === 'function') inverse = inverse(this);
+ if (typeof invisible === 'function') invisible = invisible(this);
+
+ if (typeof fg === 'function') fg = fg(this);
+ if (typeof bg === 'function') bg = bg(this);
+
+ // return (this.uid << 24)
+ // | ((this.dockBorders ? 32 : 0) << 18)
+ return ((invisible ? 16 : 0) << 18)
+ | ((inverse ? 8 : 0) << 18)
+ | ((blink ? 4 : 0) << 18)
+ | ((underline ? 2 : 0) << 18)
+ | ((bold ? 1 : 0) << 18)
+ | (colors.convert(fg) << 9)
+ | colors.convert(bg);
+};
+
+Element.prototype.onScreenEvent = function(type, handler) {
+ var listeners = this._slisteners = this._slisteners || [];
+ listeners.push({ type: type, handler: handler });
+ this.screen.on(type, handler);
+};
+
+Element.prototype.onceScreenEvent = function(type, handler) {
+ var listeners = this._slisteners = this._slisteners || [];
+ var entry = { type: type, handler: handler };
+ listeners.push(entry);
+ this.screen.once(type, function() {
+ var i = listeners.indexOf(entry);
+ if (~i) listeners.splice(i, 1);
+ return handler.apply(this, arguments);
+ });
+};
+
+Element.prototype.removeScreenEvent = function(type, handler) {
+ var listeners = this._slisteners = this._slisteners || [];
+ for (var i = 0; i < listeners.length; i++) {
+ var listener = listeners[i];
+ if (listener.type === type && listener.handler === handler) {
+ listeners.splice(i, 1);
+ if (this._slisteners.length === 0) {
+ delete this._slisteners;
+ }
+ break;
+ }
+ }
+ this.screen.removeListener(type, handler);
+};
+
+Element.prototype.free = function() {
+ var listeners = this._slisteners = this._slisteners || [];
+ for (var i = 0; i < listeners.length; i++) {
+ var listener = listeners[i];
+ this.screen.removeListener(listener.type, listener.handler);
+ }
+ delete this._slisteners;
+};
+
+Element.prototype.destroy = function() {
+ this.detach();
+ this.free();
+ this.emit('destroy');
+};
+
+Element.prototype.hide = function() {
+ if (this.hidden) return;
+ this.clearPos();
+ this.hidden = true;
+ this.emit('hide');
+ if (this.screen.focused === this) {
+ this.screen.rewindFocus();
+ }
+};
+
+Element.prototype.show = function() {
+ if (!this.hidden) return;
+ this.hidden = false;
+ this.emit('show');
+};
+
+Element.prototype.toggle = function() {
+ return this.hidden ? this.show() : this.hide();
+};
+
+Element.prototype.focus = function() {
+ return this.screen.focused = this;
+};
+
+Element.prototype.setContent = function(content, noClear, noTags) {
+ if (!noClear) this.clearPos();
+ this.content = content || '';
+ this.parseContent(noTags);
+ this.emit('set content');
+};
+
+Element.prototype.getContent = function() {
+ return this._clines.fake.join('\n');
+};
+
+Element.prototype.setText = function(content, noClear) {
+ content = content || '';
+ content = content.replace(/\x1b\[[\d;]*m/g, '');
+ return this.setContent(content, noClear, true);
+};
+
+Element.prototype.getText = function() {
+ return this.getContent().replace(/\x1b\[[\d;]*m/g, '');
+};
+
+Element.prototype.parseContent = function(noTags) {
+ if (this.detached) return false;
+
+ var width = this.width - this.iwidth;
+ if (this._clines == null
+ || this._clines.width !== width
+ || this._clines.content !== this.content) {
+ var content = this.content;
+
+ content = content
+ .replace(/[\x00-\x08\x0b-\x0c\x0e-\x1a\x1c-\x1f\x7f]/g, '')
+ .replace(/\x1b(?!\[[\d;]*m)/g, '')
+ .replace(/\r\n|\r/g, '\n')
+ .replace(/\t/g, this.screen.tabc);
+
+ if (this.screen.fullUnicode) {
+ // double-width chars will eat the next char after render. create a
+ // blank character after it so it doesn't eat the real next char.
+ content = content.replace(unicode.chars.all, '$1\x03');
+ // iTerm2 cannot render combining characters properly.
+ if (this.screen.program.isiTerm2) {
+ content = content.replace(unicode.chars.combining, '');
+ }
+ } else {
+ // no double-width: replace them with question-marks.
+ content = content.replace(unicode.chars.all, '??');
+ // delete combining characters since they're 0-width anyway.
+ // NOTE: We could drop this, the non-surrogates would get changed to ? by
+ // the unicode filter, and surrogates changed to ? by the surrogate
+ // regex. however, the user might expect them to be 0-width.
+ // NOTE: Might be better for performance to drop!
+ content = content.replace(unicode.chars.combining, '');
+ // no surrogate pairs: replace them with question-marks.
+ content = content.replace(unicode.chars.surrogate, '?');
+ // XXX Deduplicate code here:
+ // content = helpers.dropUnicode(content);
+ }
+
+ if (!noTags) {
+ content = this._parseTags(content);
+ }
+
+ this._clines = this._wrapContent(content, width);
+ this._clines.width = width;
+ this._clines.content = this.content;
+ this._clines.attr = this._parseAttr(this._clines);
+ this._clines.ci = [];
+ this._clines.reduce(function(total, line) {
+ this._clines.ci.push(total);
+ return total + line.length + 1;
+ }.bind(this), 0);
+
+ this._pcontent = this._clines.join('\n');
+ this.emit('parsed content');
+
+ return true;
+ }
+
+ // Need to calculate this every time because the default fg/bg may change.
+ this._clines.attr = this._parseAttr(this._clines) || this._clines.attr;
+
+ return false;
+};
+
+// Convert `{red-fg}foo{/red-fg}` to `\x1b[31mfoo\x1b[39m`.
+Element.prototype._parseTags = function(text) {
+ if (!this.parseTags) return text;
+ if (!/{\/?[\w\-,;!#]*}/.test(text)) return text;
+
+ var program = this.screen.program
+ , out = ''
+ , state
+ , bg = []
+ , fg = []
+ , flag = []
+ , cap
+ , slash
+ , param
+ , attr
+ , esc;
+
+ for (;;) {
+ if (!esc && (cap = /^{escape}/.exec(text))) {
+ text = text.substring(cap[0].length);
+ esc = true;
+ continue;
+ }
+
+ if (esc && (cap = /^([\s\S]+?){\/escape}/.exec(text))) {
+ text = text.substring(cap[0].length);
+ out += cap[1];
+ esc = false;
+ continue;
+ }
+
+ if (esc) {
+ // throw new Error('Unterminated escape tag.');
+ out += text;
+ break;
+ }
+
+ if (cap = /^{(\/?)([\w\-,;!#]*)}/.exec(text)) {
+ text = text.substring(cap[0].length);
+ slash = cap[1] === '/';
+ param = cap[2].replace(/-/g, ' ');
+
+ if (param === 'open') {
+ out += '{';
+ continue;
+ } else if (param === 'close') {
+ out += '}';
+ continue;
+ }
+
+ if (param.slice(-3) === ' bg') state = bg;
+ else if (param.slice(-3) === ' fg') state = fg;
+ else state = flag;
+
+ if (slash) {
+ if (!param) {
+ out += program._attr('normal');
+ bg.length = 0;
+ fg.length = 0;
+ flag.length = 0;
+ } else {
+ attr = program._attr(param, false);
+ if (attr == null) {
+ out += cap[0];
+ } else {
+ // if (param !== state[state.length - 1]) {
+ // throw new Error('Misnested tags.');
+ // }
+ state.pop();
+ if (state.length) {
+ out += program._attr(state[state.length - 1]);
+ } else {
+ out += attr;
+ }
+ }
+ }
+ } else {
+ if (!param) {
+ out += cap[0];
+ } else {
+ attr = program._attr(param);
+ if (attr == null) {
+ out += cap[0];
+ } else {
+ state.push(param);
+ out += attr;
+ }
+ }
+ }
+
+ continue;
+ }
+
+ if (cap = /^[\s\S]+?(?={\/?[\w\-,;!#]*})/.exec(text)) {
+ text = text.substring(cap[0].length);
+ out += cap[0];
+ continue;
+ }
+
+ out += text;
+ break;
+ }
+
+ return out;
+};
+
+Element.prototype._parseAttr = function(lines) {
+ var dattr = this.sattr(this.style)
+ , attr = dattr
+ , attrs = []
+ , line
+ , i
+ , j
+ , c;
+
+ if (lines[0].attr === attr) {
+ return;
+ }
+
+ for (j = 0; j < lines.length; j++) {
+ line = lines[j];
+ attrs[j] = attr;
+ for (i = 0; i < line.length; i++) {
+ if (line[i] === '\x1b') {
+ if (c = /^\x1b\[[\d;]*m/.exec(line.substring(i))) {
+ attr = this.screen.attrCode(c[0], attr, dattr);
+ i += c[0].length - 1;
+ }
+ }
+ }
+ }
+
+ return attrs;
+};
+
+Element.prototype._align = function(line, width, align) {
+ if (!align) return line;
+
+ var cline = line.replace(/\x1b\[[\d;]*m/g, '')
+ , len = cline.length
+ , s = width - len;
+
+ if (len === 0) return line;
+ if (s < 0) return line;
+
+ if (align === 'center') {
+ s = Array(((s / 2) | 0) + 1).join(' ');
+ return s + line + s;
+ } else if (align === 'right') {
+ s = Array(s + 1).join(' ');
+ return s + line;
+ } else if (this.parseTags && ~line.indexOf('{|}')) {
+ var parts = line.split('{|}');
+ var cparts = cline.split('{|}');
+ s = Math.max(width - cparts[0].length - cparts[1].length, 0);
+ s = Array(s + 1).join(' ');
+ return parts[0] + s + parts[1];
+ }
+
+ return line;
+};
+
+Element.prototype._wrapContent = function(content, width) {
+ var tags = this.parseTags
+ , state = this.align
+ , wrap = this.wrap
+ , margin = 0
+ , rtof = []
+ , ftor = []
+ , fake = []
+ , out = []
+ , no = 0
+ , line
+ , align
+ , cap
+ , total
+ , i
+ , part
+ , j
+ , lines
+ , rest;
+
+ lines = content.split('\n');
+
+ if (!content) {
+ out.push(content);
+ out.rtof = [0];
+ out.ftor = [[0]];
+ out.fake = lines;
+ out.real = out;
+ out.mwidth = 0;
+ return out;
+ }
+
+ if (this.scrollbar) margin++;
+ if (this.type === 'textarea') margin++;
+ if (width > margin) width -= margin;
+
+main:
+ for (; no < lines.length; no++) {
+ line = lines[no];
+ align = state;
+
+ ftor.push([]);
+
+ // Handle alignment tags.
+ if (tags) {
+ if (cap = /^{(left|center|right)}/.exec(line)) {
+ line = line.substring(cap[0].length);
+ align = state = cap[1] !== 'left'
+ ? cap[1]
+ : null;
+ }
+ if (cap = /{\/(left|center|right)}$/.exec(line)) {
+ line = line.slice(0, -cap[0].length);
+ state = null;
+ }
+ }
+
+ // If the string is apparently too long, wrap it.
+ while (line.length > width) {
+ // Measure the real width of the string.
+ for (i = 0, total = 0; i < line.length; i++) {
+ while (line[i] === '\x1b') {
+ while (line[i] && line[i++] !== 'm');
+ }
+ if (!line[i]) break;
+ if (++total === width) {
+ // If we're not wrapping the text, we have to finish up the rest of
+ // the control sequences before cutting off the line.
+ i++;
+ if (!wrap) {
+ rest = line.substring(i).match(/\x1b\[[^m]*m/g);
+ rest = rest ? rest.join('') : '';
+ out.push(this._align(line.substring(0, i) + rest, width, align));
+ ftor[no].push(out.length - 1);
+ rtof.push(no);
+ continue main;
+ }
+ if (!this.screen.fullUnicode) {
+ // Try to find a space to break on.
+ if (i !== line.length) {
+ j = i;
+ while (j > i - 10 && j > 0 && line[--j] !== ' ');
+ if (line[j] === ' ') i = j + 1;
+ }
+ } else {
+ // Try to find a character to break on.
+ if (i !== line.length) {
+ //
+ // Compensate for surrogate length
+ // counts on wrapping (experimental):
+ // NOTE: Could optimize this by putting
+ // it in the parent for loop.
+ if (unicode.isSurrogate(line, i)) i--;
+ for (var s = 0, n = 0; n < i; n++) {
+ if (unicode.isSurrogate(line, n)) s++, n++;
+ }
+ i += s;
+ //
+ j = i;
+ // Break _past_ space.
+ // Break _past_ double-width chars.
+ // Break _past_ surrogate pairs.
+ // Break _past_ combining chars.
+ while (j > i - 10 && j > 0) {
+ j--;
+ if (line[j] === ' '
+ || line[j] === '\x03'
+ || (unicode.isSurrogate(line, j - 1) && line[j + 1] !== '\x03')
+ || unicode.isCombining(line, j)) {
+ break;
+ }
+ }
+ if (line[j] === ' '
+ || line[j] === '\x03'
+ || (unicode.isSurrogate(line, j - 1) && line[j + 1] !== '\x03')
+ || unicode.isCombining(line, j)) {
+ i = j + 1;
+ }
+ }
+ }
+ break;
+ }
+ }
+
+ part = line.substring(0, i);
+ line = line.substring(i);
+
+ out.push(this._align(part, width, align));
+ ftor[no].push(out.length - 1);
+ rtof.push(no);
+
+ // Make sure we didn't wrap the line to the very end, otherwise
+ // we get a pointless empty line after a newline.
+ if (line === '') continue main;
+
+ // If only an escape code got cut off, at it to `part`.
+ if (/^(?:\x1b[\[\d;]*m)+$/.test(line)) {
+ out[out.length - 1] += line;
+ continue main;
+ }
+ }
+
+ out.push(this._align(line, width, align));
+ ftor[no].push(out.length - 1);
+ rtof.push(no);
+ }
+
+ out.rtof = rtof;
+ out.ftor = ftor;
+ out.fake = lines;
+ out.real = out;
+
+ out.mwidth = out.reduce(function(current, line) {
+ line = line.replace(/\x1b\[[\d;]*m/g, '');
+ return line.length > current
+ ? line.length
+ : current;
+ }, 0);
+
+ return out;
+};
+
+Element.prototype.__defineGetter__('visible', function() {
+ var el = this;
+ do {
+ if (el.detached) return false;
+ if (el.hidden) return false;
+ // if (!el.lpos) return false;
+ // if (el.position.width === 0 || el.position.height === 0) return false;
+ } while (el = el.parent);
+ return true;
+});
+
+Element.prototype.__defineGetter__('_detached', function() {
+ var el = this;
+ do {
+ if (el.type === 'screen') return false;
+ if (!el.parent) return true;
+ } while (el = el.parent);
+ return false;
+});
+
+Element.prototype.enableMouse = function() {
+ this.screen._listenMouse(this);
+};
+
+Element.prototype.enableKeys = function() {
+ this.screen._listenKeys(this);
+};
+
+Element.prototype.enableInput = function() {
+ this.screen._listenMouse(this);
+ this.screen._listenKeys(this);
+};
+
+Element.prototype.__defineGetter__('draggable', function() {
+ return this._draggable === true;
+});
+
+Element.prototype.__defineSetter__('draggable', function(draggable) {
+ return draggable ? this.enableDrag(draggable) : this.disableDrag();
+});
+
+Element.prototype.enableDrag = function(verify) {
+ var self = this;
+
+ if (this._draggable) return true;
+
+ if (typeof verify !== 'function') {
+ verify = function() { return true; };
+ }
+
+ this.enableMouse();
+
+ this.on('mousedown', this._dragMD = function(data) {
+ if (self.screen._dragging) return;
+ if (!verify(data)) return;
+ self.screen._dragging = self;
+ self._drag = {
+ x: data.x - self.aleft,
+ y: data.y - self.atop
+ };
+ self.setFront();
+ });
+
+ this.onScreenEvent('mouse', this._dragM = function(data) {
+ if (self.screen._dragging !== self) return;
+
+ if (data.action !== 'mousedown') {
+ delete self.screen._dragging;
+ delete self._drag;
+ return;
+ }
+
+ // This can happen in edge cases where the user is
+ // already dragging and element when it is detached.
+ if (!self.parent) return;
+
+ var ox = self._drag.x
+ , oy = self._drag.y
+ , px = self.parent.aleft
+ , py = self.parent.atop
+ , x = data.x - px - ox
+ , y = data.y - py - oy;
+
+ if (self.position.right != null) {
+ if (self.position.left != null) {
+ self.width = '100%-' + (self.parent.width - self.width);
+ }
+ self.position.right = null;
+ }
+
+ if (self.position.bottom != null) {
+ if (self.position.top != null) {
+ self.height = '100%-' + (self.parent.height - self.height);
+ }
+ self.position.bottom = null;
+ }
+
+ self.rleft = x;
+ self.rtop = y;
+
+ self.screen.render();
+ });
+
+ return this._draggable = true;
+};
+
+Element.prototype.disableDrag = function() {
+ if (!this._draggable) return false;
+ delete this.screen._dragging;
+ delete this._drag;
+ this.removeListener('mousedown', this._dragMD);
+ this.removeScreenEvent('mouse', this._dragM);
+ return this._draggable = false;
+};
+
+Element.prototype.key = function(key, listener) {
+ // return this.screen.program.key.apply(this, arguments);
+ if (typeof key === 'string') key = key.split(/\s*,\s*/);
+ key.forEach(function(key) {
+ return this.onScreenEvent('key ' + key, listener);
+ }, this);
+};
+
+Element.prototype.onceKey = function(key, listener) {
+ // return this.screen.program.onceKey.apply(this, arguments);
+ if (typeof key === 'string') key = key.split(/\s*,\s*/);
+ key.forEach(function(key) {
+ return this.onceScreenEvent('key ' + key, listener);
+ }, this);
+};
+
+Element.prototype.unkey =
+Element.prototype.removeKey = function(key, listener) {
+ // return this.screen.program.unkey.apply(this, arguments);
+ if (typeof key === 'string') key = key.split(/\s*,\s*/);
+ key.forEach(function(key) {
+ return this.removeScreenEvent('key ' + key, listener);
+ }, this);
+};
+
+Element.prototype.setIndex = function(index) {
+ if (!this.parent) return;
+
+ if (index < 0) {
+ index = this.parent.children.length + index;
+ }
+
+ index = Math.max(index, 0);
+ index = Math.min(index, this.parent.children.length - 1);
+
+ var i = this.parent.children.indexOf(this);
+ if (!~i) return;
+
+ var item = this.parent.children.splice(i, 1)[0]
+ this.parent.children.splice(index, 0, item);
+};
+
+Element.prototype.setFront = function() {
+ return this.setIndex(-1);
+};
+
+Element.prototype.setBack = function() {
+ return this.setIndex(0);
+};
+
+Element.prototype.clearPos = function(get, override) {
+ if (this.detached) return;
+ var lpos = this._getCoords(get);
+ if (!lpos) return;
+ this.screen.clearRegion(
+ lpos.xi, lpos.xl,
+ lpos.yi, lpos.yl,
+ override);
+};
+
+Element.prototype.setLabel = function(options) {
+ var self = this;
+ var Box = require('./box');
+
+ if (typeof options === 'string') {
+ options = { text: options };
+ }
+
+ if (this._label) {
+ this._label.setContent(options.text);
+ if (options.side !== 'right') {
+ this._label.rleft = 2 + (this.border ? -1 : 0);
+ this._label.position.right = undefined;
+ if (!this.screen.autoPadding) {
+ this._label.rleft = 2;
+ }
+ } else {
+ this._label.rright = 2 + (this.border ? -1 : 0);
+ this._label.position.left = undefined;
+ if (!this.screen.autoPadding) {
+ this._label.rright = 2;
+ }
+ }
+ return;
+ }
+
+ this._label = new Box({
+ screen: this.screen,
+ parent: this,
+ content: options.text,
+ top: this.border ? -1 : 0,
+ tags: this.parseTags,
+ shrink: true,
+ style: this.style.label
+ });
+
+ if (options.side !== 'right') {
+ this._label.rleft = 2 + (this.border ? -1 : 0);
+ } else {
+ this._label.rright = 2 + (this.border ? -1 : 0);
+ }
+
+ this._label._isLabel = true;
+
+ if (!this.screen.autoPadding) {
+ if (options.side !== 'right') {
+ this._label.rleft = 2;
+ } else {
+ this._label.rright = 2;
+ }
+ this._label.rtop = 0;
+ }
+
+ var reposition = function() {
+ var visible = self.height - self.iheight;
+ self._label.rtop = (self.childBase || 0) - (self.border ? 1 : 0);
+ if (!self.screen.autoPadding) {
+ self._label.rtop = (self.childBase || 0);
+ }
+ self.screen.render();
+ };
+
+ this.on('scroll', function() {
+ reposition();
+ });
+
+ this.on('resize', function() {
+ nextTick(function() {
+ reposition();
+ });
+ });
+};
+
+Element.prototype.removeLabel = function() {
+ if (!this._label) return;
+ this._label.detach();
+ delete this._label;
+};
+
+Element.prototype.setHover = function(options) {
+ var self = this;
+
+ if (typeof options === 'string') {
+ options = { text: options };
+ }
+
+ this._hoverOptions = options;
+ this.enableMouse();
+ this.screen._initHover();
+};
+
+Element.prototype.removeHover = function() {
+ delete this._hoverOptions;
+ if (!this.screen._hoverText || this.screen._hoverText.detached) return;
+ this.screen._hoverText.detach();
+ this.screen.render();
+};
+
+/**
+ * Positioning
+ */
+
+// The below methods are a bit confusing: basically
+// whenever Box.render is called `lpos` gets set on
+// the element, an object containing the rendered
+// coordinates. Since these don't update if the
+// element is moved somehow, they're unreliable in
+// that situation. However, if we can guarantee that
+// lpos is good and up to date, it can be more
+// accurate than the calculated positions below.
+// In this case, if the element is being rendered,
+// it's guaranteed that the parent will have been
+// rendered first, in which case we can use the
+// parant's lpos instead of recalculating it's
+// position (since that might be wrong because
+// it doesn't handle content shrinkage).
+
+Element.prototype._getPos = function() {
+ var pos = this.lpos;
+
+ assert.ok(pos);
+
+ if (pos.aleft != null) return pos;
+
+ pos.aleft = pos.xi;
+ pos.atop = pos.yi;
+ pos.aright = this.screen.cols - pos.xl;
+ pos.abottom = this.screen.rows - pos.yl;
+ pos.width = pos.xl - pos.xi;
+ pos.height = pos.yl - pos.yi;
+
+ return pos;
+};
+
+/**
+ * Position Getters
+ */
+
+Element.prototype._getWidth = function(get) {
+ var parent = get ? this.parent._getPos() : this.parent
+ , width = this.position.width
+ , left
+ , expr;
+
+ if (typeof width === 'string') {
+ if (width === 'half') width = '50%';
+ expr = width.split(/(?=\+|-)/);
+ width = expr[0];
+ width = +width.slice(0, -1) / 100;
+ width = parent.width * width | 0;
+ width += +(expr[1] || 0);
+ return width;
+ }
+
+ // This is for if the element is being streched or shrunken.
+ // Although the width for shrunken elements is calculated
+ // in the render function, it may be calculated based on
+ // the content width, and the content width is initially
+ // decided by the width the element, so it needs to be
+ // calculated here.
+ if (width == null) {
+ left = this.position.left || 0;
+ if (typeof left === 'string') {
+ if (left === 'center') left = '50%';
+ expr = left.split(/(?=\+|-)/);
+ left = expr[0];
+ left = +left.slice(0, -1) / 100;
+ left = parent.width * left | 0;
+ left += +(expr[1] || 0);
+ }
+ width = parent.width - (this.position.right || 0) - left;
+ if (this.screen.autoPadding) {
+ if ((this.position.left != null || this.position.right == null)
+ && this.position.left !== 'center') {
+ width -= this.parent.ileft;
+ }
+ width -= this.parent.iright;
+ }
+ }
+
+ return width;
+};
+
+Element.prototype.__defineGetter__('width', function() {
+ return this._getWidth(false);
+});
+
+Element.prototype._getHeight = function(get) {
+ var parent = get ? this.parent._getPos() : this.parent
+ , height = this.position.height
+ , top
+ , expr;
+
+ if (typeof height === 'string') {
+ if (height === 'half') height = '50%';
+ expr = height.split(/(?=\+|-)/);
+ height = expr[0];
+ height = +height.slice(0, -1) / 100;
+ height = parent.height * height | 0;
+ height += +(expr[1] || 0);
+ return height;
+ }
+
+ // This is for if the element is being streched or shrunken.
+ // Although the width for shrunken elements is calculated
+ // in the render function, it may be calculated based on
+ // the content width, and the content width is initially
+ // decided by the width the element, so it needs to be
+ // calculated here.
+ if (height == null) {
+ top = this.position.top || 0;
+ if (typeof top === 'string') {
+ if (top === 'center') top = '50%';
+ expr = top.split(/(?=\+|-)/);
+ top = expr[0];
+ top = +top.slice(0, -1) / 100;
+ top = parent.height * top | 0;
+ top += +(expr[1] || 0);
+ }
+ height = parent.height - (this.position.bottom || 0) - top;
+ if (this.screen.autoPadding) {
+ if ((this.position.top != null
+ || this.position.bottom == null)
+ && this.position.top !== 'center') {
+ height -= this.parent.itop;
+ }
+ height -= this.parent.ibottom;
+ }
+ }
+
+ return height;
+};
+
+Element.prototype.__defineGetter__('height', function() {
+ return this._getHeight(false);
+});
+
+Element.prototype._getLeft = function(get) {
+ var parent = get ? this.parent._getPos() : this.parent
+ , left = this.position.left || 0
+ , expr;
+
+ if (typeof left === 'string') {
+ if (left === 'center') left = '50%';
+ expr = left.split(/(?=\+|-)/);
+ left = expr[0];
+ left = +left.slice(0, -1) / 100;
+ left = parent.width * left | 0;
+ left += +(expr[1] || 0);
+ if (this.position.left === 'center') {
+ left -= this._getWidth(get) / 2 | 0;
+ }
+ }
+
+ if (this.position.left == null && this.position.right != null) {
+ return this.screen.cols - this._getWidth(get) - this._getRight(get);
+ }
+
+ if (this.screen.autoPadding) {
+ if ((this.position.left != null
+ || this.position.right == null)
+ && this.position.left !== 'center') {
+ left += this.parent.ileft;
+ }
+ }
+
+ return (parent.aleft || 0) + left;
+};
+
+Element.prototype.__defineGetter__('aleft', function() {
+ return this._getLeft(false);
+});
+
+Element.prototype._getRight = function(get) {
+ var parent = get ? this.parent._getPos() : this.parent
+ , right;
+
+ if (this.position.right == null && this.position.left != null) {
+ right = this.screen.cols - (this._getLeft(get) + this._getWidth(get));
+ if (this.screen.autoPadding) {
+ right += this.parent.iright;
+ }
+ return right;
+ }
+
+ right = (parent.aright || 0) + (this.position.right || 0);
+
+ if (this.screen.autoPadding) {
+ right += this.parent.iright;
+ }
+
+ return right;
+};
+
+Element.prototype.__defineGetter__('aright', function() {
+ return this._getRight(false);
+});
+
+Element.prototype._getTop = function(get) {
+ var parent = get ? this.parent._getPos() : this.parent
+ , top = this.position.top || 0
+ , expr;
+
+ if (typeof top === 'string') {
+ if (top === 'center') top = '50%';
+ expr = top.split(/(?=\+|-)/);
+ top = expr[0];
+ top = +top.slice(0, -1) / 100;
+ top = parent.height * top | 0;
+ top += +(expr[1] || 0);
+ if (this.position.top === 'center') {
+ top -= this._getHeight(get) / 2 | 0;
+ }
+ }
+
+ if (this.position.top == null && this.position.bottom != null) {
+ return this.screen.rows - this._getHeight(get) - this._getBottom(get);
+ }
+
+ if (this.screen.autoPadding) {
+ if ((this.position.top != null
+ || this.position.bottom == null)
+ && this.position.top !== 'center') {
+ top += this.parent.itop;
+ }
+ }
+
+ return (parent.atop || 0) + top;
+};
+
+Element.prototype.__defineGetter__('atop', function() {
+ return this._getTop(false);
+});
+
+Element.prototype._getBottom = function(get) {
+ var parent = get ? this.parent._getPos() : this.parent
+ , bottom;
+
+ if (this.position.bottom == null && this.position.top != null) {
+ bottom = this.screen.rows - (this._getTop(get) + this._getHeight(get));
+ if (this.screen.autoPadding) {
+ bottom += this.parent.ibottom;
+ }
+ return bottom;
+ }
+
+ bottom = (parent.abottom || 0) + (this.position.bottom || 0);
+
+ if (this.screen.autoPadding) {
+ bottom += this.parent.ibottom;
+ }
+
+ return bottom;
+};
+
+Element.prototype.__defineGetter__('abottom', function() {
+ return this._getBottom(false);
+});
+
+Element.prototype.__defineGetter__('rleft', function() {
+ return this.aleft - this.parent.aleft;
+});
+
+Element.prototype.__defineGetter__('rright', function() {
+ return this.aright - this.parent.aright;
+});
+
+Element.prototype.__defineGetter__('rtop', function() {
+ return this.atop - this.parent.atop;
+});
+
+Element.prototype.__defineGetter__('rbottom', function() {
+ return this.abottom - this.parent.abottom;
+});
+
+/**
+ * Position Setters
+ */
+
+// NOTE:
+// For aright, abottom, right, and bottom:
+// If position.bottom is null, we could simply set top instead.
+// But it wouldn't replicate bottom behavior appropriately if
+// the parent was resized, etc.
+Element.prototype.__defineSetter__('width', function(val) {
+ if (this.position.width === val) return;
+ if (/^\d+$/.test(val)) val = +val;
+ this.emit('resize');
+ this.clearPos();
+ return this.position.width = val;
+});
+
+Element.prototype.__defineSetter__('height', function(val) {
+ if (this.position.height === val) return;
+ if (/^\d+$/.test(val)) val = +val;
+ this.emit('resize');
+ this.clearPos();
+ return this.position.height = val;
+});
+
+Element.prototype.__defineSetter__('aleft', function(val) {
+ var expr;
+ if (typeof val === 'string') {
+ if (val === 'center') {
+ val = this.screen.width / 2 | 0;
+ val -= this.width / 2 | 0;
+ } else {
+ expr = val.split(/(?=\+|-)/);
+ val = expr[0];
+ val = +val.slice(0, -1) / 100;
+ val = this.screen.width * val | 0;
+ val += +(expr[1] || 0);
+ }
+ }
+ val -= this.parent.aleft;
+ if (this.position.left === val) return;
+ this.emit('move');
+ this.clearPos();
+ return this.position.left = val;
+});
+
+Element.prototype.__defineSetter__('aright', function(val) {
+ val -= this.parent.aright;
+ if (this.position.right === val) return;
+ this.emit('move');
+ this.clearPos();
+ return this.position.right = val;
+});
+
+Element.prototype.__defineSetter__('atop', function(val) {
+ var expr;
+ if (typeof val === 'string') {
+ if (val === 'center') {
+ val = this.screen.height / 2 | 0;
+ val -= this.height / 2 | 0;
+ } else {
+ expr = val.split(/(?=\+|-)/);
+ val = expr[0];
+ val = +val.slice(0, -1) / 100;
+ val = this.screen.height * val | 0;
+ val += +(expr[1] || 0);
+ }
+ }
+ val -= this.parent.atop;
+ if (this.position.top === val) return;
+ this.emit('move');
+ this.clearPos();
+ return this.position.top = val;
+});
+
+Element.prototype.__defineSetter__('abottom', function(val) {
+ val -= this.parent.abottom;
+ if (this.position.bottom === val) return;
+ this.emit('move');
+ this.clearPos();
+ return this.position.bottom = val;
+});
+
+Element.prototype.__defineSetter__('rleft', function(val) {
+ if (this.position.left === val) return;
+ if (/^\d+$/.test(val)) val = +val;
+ this.emit('move');
+ this.clearPos();
+ return this.position.left = val;
+});
+
+Element.prototype.__defineSetter__('rright', function(val) {
+ if (this.position.right === val) return;
+ this.emit('move');
+ this.clearPos();
+ return this.position.right = val;
+});
+
+Element.prototype.__defineSetter__('rtop', function(val) {
+ if (this.position.top === val) return;
+ if (/^\d+$/.test(val)) val = +val;
+ this.emit('move');
+ this.clearPos();
+ return this.position.top = val;
+});
+
+Element.prototype.__defineSetter__('rbottom', function(val) {
+ if (this.position.bottom === val) return;
+ this.emit('move');
+ this.clearPos();
+ return this.position.bottom = val;
+});
+
+Element.prototype.__defineGetter__('ileft', function() {
+ return (this.border ? 1 : 0) + this.padding.left;
+ // return (this.border && this.border.left ? 1 : 0) + this.padding.left;
+});
+
+Element.prototype.__defineGetter__('itop', function() {
+ return (this.border ? 1 : 0) + this.padding.top;
+ // return (this.border && this.border.top ? 1 : 0) + this.padding.top;
+});
+
+Element.prototype.__defineGetter__('iright', function() {
+ return (this.border ? 1 : 0) + this.padding.right;
+ // return (this.border && this.border.right ? 1 : 0) + this.padding.right;
+});
+
+Element.prototype.__defineGetter__('ibottom', function() {
+ return (this.border ? 1 : 0) + this.padding.bottom;
+ // return (this.border && this.border.bottom ? 1 : 0) + this.padding.bottom;
+});
+
+Element.prototype.__defineGetter__('iwidth', function() {
+ // return (this.border
+ // ? ((this.border.left ? 1 : 0) + (this.border.right ? 1 : 0)) : 0)
+ // + this.padding.left + this.padding.right;
+ return (this.border ? 2 : 0) + this.padding.left + this.padding.right;
+});
+
+Element.prototype.__defineGetter__('iheight', function() {
+ // return (this.border
+ // ? ((this.border.top ? 1 : 0) + (this.border.bottom ? 1 : 0)) : 0)
+ // + this.padding.top + this.padding.bottom;
+ return (this.border ? 2 : 0) + this.padding.top + this.padding.bottom;
+});
+
+Element.prototype.__defineGetter__('tpadding', function() {
+ return this.padding.left + this.padding.top
+ + this.padding.right + this.padding.bottom;
+});
+
+/**
+ * Relative coordinates as default properties
+ */
+
+Element.prototype.__defineGetter__('left', function() {
+ return this.rleft;
+});
+
+Element.prototype.__defineGetter__('right', function() {
+ return this.rright;
+});
+
+Element.prototype.__defineGetter__('top', function() {
+ return this.rtop;
+});
+
+Element.prototype.__defineGetter__('bottom', function() {
+ return this.rbottom;
+});
+
+Element.prototype.__defineSetter__('left', function(val) {
+ return this.rleft = val;
+});
+
+Element.prototype.__defineSetter__('right', function(val) {
+ return this.rright = val;
+});
+
+Element.prototype.__defineSetter__('top', function(val) {
+ return this.rtop = val;
+});
+
+Element.prototype.__defineSetter__('bottom', function(val) {
+ return this.rbottom = val;
+});
+
+/**
+ * Rendering - here be dragons
+ */
+
+Element.prototype._getShrinkBox = function(xi, xl, yi, yl, get) {
+ if (!this.children.length) {
+ return { xi: xi, xl: xi + 1, yi: yi, yl: yi + 1 };
+ }
+
+ var i, el, ret, mxi = xi, mxl = xi + 1, myi = yi, myl = yi + 1;
+
+ // This is a chicken and egg problem. We need to determine how the children
+ // will render in order to determine how this element renders, but it in
+ // order to figure out how the children will render, they need to know
+ // exactly how their parent renders, so, we can give them what we have so
+ // far.
+ var _lpos;
+ if (get) {
+ _lpos = this.lpos;
+ this.lpos = { xi: xi, xl: xl, yi: yi, yl: yl };
+ //this.shrink = false;
+ }
+
+ for (i = 0; i < this.children.length; i++) {
+ el = this.children[i];
+
+ ret = el._getCoords(get);
+
+ // Or just (seemed to work, but probably not good):
+ // ret = el.lpos || this.lpos;
+
+ if (!ret) continue;
+
+ // Since the parent element is shrunk, and the child elements think it's
+ // going to take up as much space as possible, an element anchored to the
+ // right or bottom will inadvertantly make the parent's shrunken size as
+ // large as possible. So, we can just use the height and/or width the of
+ // element.
+ // if (get) {
+ if (el.position.left == null && el.position.right != null) {
+ ret.xl = xi + (ret.xl - ret.xi);
+ ret.xi = xi;
+ if (this.screen.autoPadding) {
+ // Maybe just do this no matter what.
+ ret.xl += this.ileft;
+ ret.xi += this.ileft;
+ }
+ }
+ if (el.position.top == null && el.position.bottom != null) {
+ ret.yl = yi + (ret.yl - ret.yi);
+ ret.yi = yi;
+ if (this.screen.autoPadding) {
+ // Maybe just do this no matter what.
+ ret.yl += this.itop;
+ ret.yi += this.itop;
+ }
+ }
+
+ if (ret.xi < mxi) mxi = ret.xi;
+ if (ret.xl > mxl) mxl = ret.xl;
+ if (ret.yi < myi) myi = ret.yi;
+ if (ret.yl > myl) myl = ret.yl;
+ }
+
+ if (get) {
+ this.lpos = _lpos;
+ //this.shrink = true;
+ }
+
+ if (this.position.width == null
+ && (this.position.left == null
+ || this.position.right == null)) {
+ if (this.position.left == null && this.position.right != null) {
+ xi = xl - (mxl - mxi);
+ if (!this.screen.autoPadding) {
+ xi -= this.padding.left + this.padding.right;
+ } else {
+ xi -= this.ileft;
+ }
+ } else {
+ xl = mxl;
+ if (!this.screen.autoPadding) {
+ xl += this.padding.left + this.padding.right;
+ // XXX Temporary workaround until we decide to make autoPadding default.
+ // See widget-listtable.js for an example of why this is necessary.
+ // XXX Maybe just to this for all this being that this would affect
+ // width shrunken normal shrunken lists as well.
+ // if (this._isList) {
+ if (this.type === 'list-table') {
+ xl -= this.padding.left + this.padding.right;
+ xl += this.iright;
+ }
+ } else {
+ //xl += this.padding.right;
+ xl += this.iright;
+ }
+ }
+ }
+
+ if (this.position.height == null
+ && (this.position.top == null
+ || this.position.bottom == null)
+ && (!this.scrollable || this._isList)) {
+ // NOTE: Lists get special treatment if they are shrunken - assume they
+ // want all list items showing. This is one case we can calculate the
+ // height based on items/boxes.
+ if (this._isList) {
+ myi = 0 - this.itop;
+ myl = this.items.length + this.ibottom;
+ }
+ if (this.position.top == null && this.position.bottom != null) {
+ yi = yl - (myl - myi);
+ if (!this.screen.autoPadding) {
+ yi -= this.padding.top + this.padding.bottom;
+ } else {
+ yi -= this.itop;
+ }
+ } else {
+ yl = myl;
+ if (!this.screen.autoPadding) {
+ yl += this.padding.top + this.padding.bottom;
+ } else {
+ yl += this.ibottom;
+ }
+ }
+ }
+
+ return { xi: xi, xl: xl, yi: yi, yl: yl };
+};
+
+Element.prototype._getShrinkContent = function(xi, xl, yi, yl, get) {
+ var h = this._clines.length
+ , w = this._clines.mwidth || 1;
+
+ if (this.position.width == null
+ && (this.position.left == null
+ || this.position.right == null)) {
+ if (this.position.left == null && this.position.right != null) {
+ xi = xl - w - this.iwidth;
+ } else {
+ xl = xi + w + this.iwidth;
+ }
+ }
+
+ if (this.position.height == null
+ && (this.position.top == null
+ || this.position.bottom == null)
+ && (!this.scrollable || this._isList)) {
+ if (this.position.top == null && this.position.bottom != null) {
+ yi = yl - h - this.iheight;
+ } else {
+ yl = yi + h + this.iheight;
+ }
+ }
+
+ return { xi: xi, xl: xl, yi: yi, yl: yl };
+};
+
+Element.prototype._getShrink = function(xi, xl, yi, yl, get) {
+ var shrinkBox = this._getShrinkBox(xi, xl, yi, yl, get)
+ , shrinkContent = this._getShrinkContent(xi, xl, yi, yl, get)
+ , xll = xl
+ , yll = yl;
+
+ // Figure out which one is bigger and use it.
+ if (shrinkBox.xl - shrinkBox.xi > shrinkContent.xl - shrinkContent.xi) {
+ xi = shrinkBox.xi;
+ xl = shrinkBox.xl;
+ } else {
+ xi = shrinkContent.xi;
+ xl = shrinkContent.xl;
+ }
+
+ if (shrinkBox.yl - shrinkBox.yi > shrinkContent.yl - shrinkContent.yi) {
+ yi = shrinkBox.yi;
+ yl = shrinkBox.yl;
+ } else {
+ yi = shrinkContent.yi;
+ yl = shrinkContent.yl;
+ }
+
+ // Recenter shrunken elements.
+ if (xl < xll && this.position.left === 'center') {
+ xll = (xll - xl) / 2 | 0;
+ xi += xll;
+ xl += xll;
+ }
+
+ if (yl < yll && this.position.top === 'center') {
+ yll = (yll - yl) / 2 | 0;
+ yi += yll;
+ yl += yll;
+ }
+
+ return { xi: xi, xl: xl, yi: yi, yl: yl };
+};
+
+Element.prototype._getCoords = function(get, noscroll) {
+ if (this.hidden) return;
+
+ // if (this.parent._rendering) {
+ // get = true;
+ // }
+
+ var xi = this._getLeft(get)
+ , xl = xi + this._getWidth(get)
+ , yi = this._getTop(get)
+ , yl = yi + this._getHeight(get)
+ , base = this.childBase || 0
+ , el = this
+ , fixed = this.fixed
+ , coords
+ , v
+ , noleft
+ , noright
+ , notop
+ , nobot
+ , ppos
+ , b;
+
+ // Attempt to shrink the element base on the
+ // size of the content and child elements.
+ if (this.shrink) {
+ coords = this._getShrink(xi, xl, yi, yl, get);
+ xi = coords.xi, xl = coords.xl;
+ yi = coords.yi, yl = coords.yl;
+ }
+
+ // Find a scrollable ancestor if we have one.
+ while (el = el.parent) {
+ if (el.scrollable) {
+ if (fixed) {
+ fixed = false;
+ continue;
+ }
+ break;
+ }
+ }
+
+ // Check to make sure we're visible and
+ // inside of the visible scroll area.
+ // NOTE: Lists have a property where only
+ // the list items are obfuscated.
+
+ // Old way of doing things, this would not render right if a shrunken element
+ // with lots of boxes in it was within a scrollable element.
+ // See: $ node test/widget-shrink-fail.js
+ // var thisparent = this.parent;
+
+ var thisparent = el;
+ if (el && !noscroll) {
+ ppos = thisparent.lpos;
+
+ // The shrink option can cause a stack overflow
+ // by calling _getCoords on the child again.
+ // if (!get && !thisparent.shrink) {
+ // ppos = thisparent._getCoords();
+ // }
+
+ if (!ppos) return;
+
+ // TODO: Figure out how to fix base (and cbase to only
+ // take into account the *parent's* padding.
+
+ yi -= ppos.base;
+ yl -= ppos.base;
+
+ b = thisparent.border ? 1 : 0;
+
+ // XXX
+ // Fixes non-`fixed` labels to work with scrolling (they're ON the border):
+ // if (this.position.left < 0
+ // || this.position.right < 0
+ // || this.position.top < 0
+ // || this.position.bottom < 0) {
+ if (this._isLabel) {
+ b = 0;
+ }
+
+ if (yi < ppos.yi + b) {
+ if (yl - 1 < ppos.yi + b) {
+ // Is above.
+ return;
+ } else {
+ // Is partially covered above.
+ notop = true;
+ v = ppos.yi - yi;
+ if (this.border) v--;
+ if (thisparent.border) v++;
+ base += v;
+ yi += v;
+ }
+ } else if (yl > ppos.yl - b) {
+ if (yi > ppos.yl - 1 - b) {
+ // Is below.
+ return;
+ } else {
+ // Is partially covered below.
+ nobot = true;
+ v = yl - ppos.yl;
+ if (this.border) v--;
+ if (thisparent.border) v++;
+ yl -= v;
+ }
+ }
+
+ // Shouldn't be necessary.
+ // assert.ok(yi < yl);
+ if (yi >= yl) return;
+
+ // Could allow overlapping stuff in scrolling elements
+ // if we cleared the pending buffer before every draw.
+ if (xi < el.lpos.xi) {
+ xi = el.lpos.xi;
+ noleft = true;
+ if (this.border) xi--;
+ if (thisparent.border) xi++;
+ }
+ if (xl > el.lpos.xl) {
+ xl = el.lpos.xl;
+ noright = true;
+ if (this.border) xl++;
+ if (thisparent.border) xl--;
+ }
+ //if (xi > xl) return;
+ if (xi >= xl) return;
+ }
+
+ if (this.noOverflow && this.parent.lpos) {
+ if (xi < this.parent.lpos.xi + this.parent.ileft) {
+ xi = this.parent.lpos.xi + this.parent.ileft;
+ }
+ if (xl > this.parent.lpos.xl - this.parent.iright) {
+ xl = this.parent.lpos.xl - this.parent.iright;
+ }
+ if (yi < this.parent.lpos.yi + this.parent.itop) {
+ yi = this.parent.lpos.yi + this.parent.itop;
+ }
+ if (yl > this.parent.lpos.yl - this.parent.ibottom) {
+ yl = this.parent.lpos.yl - this.parent.ibottom;
+ }
+ }
+
+ // if (this.parent.lpos) {
+ // this.parent.lpos._scrollBottom = Math.max(
+ // this.parent.lpos._scrollBottom, yl);
+ // }
+
+ return {
+ xi: xi,
+ xl: xl,
+ yi: yi,
+ yl: yl,
+ base: base,
+ noleft: noleft,
+ noright: noright,
+ notop: notop,
+ nobot: nobot,
+ renders: this.screen.renders
+ };
+};
+
+Element.prototype.render = function() {
+ this._emit('prerender');
+
+ this.parseContent();
+
+ var coords = this._getCoords(true);
+ if (!coords) {
+ delete this.lpos;
+ return;
+ }
+
+ if (coords.xl - coords.xi <= 0) {
+ coords.xl = Math.max(coords.xl, coords.xi);
+ return;
+ }
+
+ if (coords.yl - coords.yi <= 0) {
+ coords.yl = Math.max(coords.yl, coords.yi);
+ return;
+ }
+
+ var lines = this.screen.lines
+ , xi = coords.xi
+ , xl = coords.xl
+ , yi = coords.yi
+ , yl = coords.yl
+ , x
+ , y
+ , cell
+ , attr
+ , ch
+ , content = this._pcontent
+ , ci = this._clines.ci[coords.base]
+ , battr
+ , dattr
+ , c
+ , visible
+ , i
+ , bch = this.ch;
+
+ // Clip content if it's off the edge of the screen
+ // if (xi + this.ileft < 0 || yi + this.itop < 0) {
+ // var clines = this._clines.slice();
+ // if (xi + this.ileft < 0) {
+ // for (var i = 0; i < clines.length; i++) {
+ // var t = 0;
+ // var csi = '';
+ // var csis = '';
+ // for (var j = 0; j < clines[i].length; j++) {
+ // while (clines[i][j] === '\x1b') {
+ // csi = '\x1b';
+ // while (clines[i][j++] !== 'm') csi += clines[i][j];
+ // csis += csi;
+ // }
+ // if (++t === -(xi + this.ileft) + 1) break;
+ // }
+ // clines[i] = csis + clines[i].substring(j);
+ // }
+ // }
+ // if (yi + this.itop < 0) {
+ // clines = clines.slice(-(yi + this.itop));
+ // }
+ // content = clines.join('\n');
+ // }
+
+ if (coords.base >= this._clines.ci.length) {
+ ci = this._pcontent.length;
+ }
+
+ this.lpos = coords;
+
+ if (this.border && this.border.type === 'line') {
+ this.screen._borderStops[coords.yi] = true;
+ this.screen._borderStops[coords.yl - 1] = true;
+ // if (!this.screen._borderStops[coords.yi]) {
+ // this.screen._borderStops[coords.yi] = { xi: coords.xi, xl: coords.xl };
+ // } else {
+ // if (this.screen._borderStops[coords.yi].xi > coords.xi) {
+ // this.screen._borderStops[coords.yi].xi = coords.xi;
+ // }
+ // if (this.screen._borderStops[coords.yi].xl < coords.xl) {
+ // this.screen._borderStops[coords.yi].xl = coords.xl;
+ // }
+ // }
+ // this.screen._borderStops[coords.yl - 1] = this.screen._borderStops[coords.yi];
+ }
+
+ dattr = this.sattr(this.style);
+ attr = dattr;
+
+ // If we're in a scrollable text box, check to
+ // see which attributes this line starts with.
+ if (ci > 0) {
+ attr = this._clines.attr[Math.min(coords.base, this._clines.length - 1)];
+ }
+
+ if (this.border) xi++, xl--, yi++, yl--;
+
+ // If we have padding/valign, that means the
+ // content-drawing loop will skip a few cells/lines.
+ // To deal with this, we can just fill the whole thing
+ // ahead of time. This could be optimized.
+ if (this.tpadding || (this.valign && this.valign !== 'top')) {
+ if (this.style.transparent) {
+ for (y = Math.max(yi, 0); y < yl; y++) {
+ if (!lines[y]) break;
+ for (x = Math.max(xi, 0); x < xl; x++) {
+ if (!lines[y][x]) break;
+ lines[y][x][0] = this._blend(attr, lines[y][x][0]);
+ lines[y][x][1] = ch;
+ lines[y].dirty = true;
+ }
+ }
+ } else {
+ this.screen.fillRegion(dattr, bch, xi, xl, yi, yl);
+ }
+ }
+
+ if (this.tpadding) {
+ xi += this.padding.left, xl -= this.padding.right;
+ yi += this.padding.top, yl -= this.padding.bottom;
+ }
+
+ // Determine where to place the text if it's vertically aligned.
+ if (this.valign === 'middle' || this.valign === 'bottom') {
+ visible = yl - yi;
+ if (this._clines.length < visible) {
+ if (this.valign === 'middle') {
+ visible = visible / 2 | 0;
+ visible -= this._clines.length / 2 | 0;
+ } else if (this.valign === 'bottom') {
+ visible -= this._clines.length;
+ }
+ ci -= visible * (xl - xi);
+ }
+ }
+
+ // Draw the content and background.
+ for (y = yi; y < yl; y++) {
+ if (!lines[y]) {
+ if (y >= this.screen.height || yl < this.ibottom) {
+ break;
+ } else {
+ continue;
+ }
+ }
+ for (x = xi; x < xl; x++) {
+ cell = lines[y][x];
+ if (!cell) {
+ if (x >= this.screen.width || xl < this.iright) {
+ break;
+ } else {
+ continue;
+ }
+ }
+
+ ch = content[ci++] || bch;
+
+ // if (!content[ci] && !coords._contentEnd) {
+ // coords._contentEnd = { x: x - xi, y: y - yi };
+ // }
+
+ // Handle escape codes.
+ while (ch === '\x1b') {
+ if (c = /^\x1b\[[\d;]*m/.exec(content.substring(ci - 1))) {
+ ci += c[0].length - 1;
+ attr = this.screen.attrCode(c[0], attr, dattr);
+ // Ignore foreground changes for selected items.
+ if (this.parent._isList && this.parent.interactive
+ && this.parent.items[this.parent.selected] === this
+ && this.parent.options.invertSelected !== false) {
+ attr = (attr & ~(0x1ff << 9)) | (dattr & (0x1ff << 9));
+ }
+ ch = content[ci] || bch;
+ ci++;
+ } else {
+ break;
+ }
+ }
+
+ // Handle newlines.
+ if (ch === '\t') ch = bch;
+ if (ch === '\n') {
+ // If we're on the first cell and we find a newline and the last cell
+ // of the last line was not a newline, let's just treat this like the
+ // newline was already "counted".
+ if (x === xi && y !== yi && content[ci - 2] !== '\n') {
+ x--;
+ continue;
+ }
+ // We could use fillRegion here, name the
+ // outer loop, and continue to it instead.
+ ch = bch;
+ for (; x < xl; x++) {
+ cell = lines[y][x];
+ if (!cell) break;
+ if (this.style.transparent) {
+ lines[y][x][0] = this._blend(attr, lines[y][x][0]);
+ if (content[ci]) lines[y][x][1] = ch;
+ lines[y].dirty = true;
+ } else {
+ if (attr !== cell[0] || ch !== cell[1]) {
+ lines[y][x][0] = attr;
+ lines[y][x][1] = ch;
+ lines[y].dirty = true;
+ }
+ }
+ }
+ continue;
+ }
+
+ if (this.screen.fullUnicode && content[ci - 1]) {
+ var point = unicode.codePointAt(content, ci - 1);
+ // Handle combining chars:
+ // Make sure they get in the same cell and are counted as 0.
+ if (unicode.combining[point]) {
+ if (point > 0x00ffff) {
+ ch = content[ci - 1] + content[ci];
+ ci++;
+ }
+ if (x - 1 >= xi) {
+ lines[y][x - 1][1] += ch;
+ } else if (y - 1 >= yi) {
+ lines[y - 1][xl - 1][1] += ch;
+ }
+ x--;
+ continue;
+ }
+ // Handle surrogate pairs:
+ // Make sure we put surrogate pair chars in one cell.
+ if (point > 0x00ffff) {
+ ch = content[ci - 1] + content[ci];
+ ci++;
+ }
+ }
+
+ if (this.style.transparent) {
+ lines[y][x][0] = this._blend(attr, lines[y][x][0]);
+ if (content[ci]) lines[y][x][1] = ch;
+ lines[y].dirty = true;
+ } else {
+ if (attr !== cell[0] || ch !== cell[1]) {
+ lines[y][x][0] = attr;
+ lines[y][x][1] = ch;
+ lines[y].dirty = true;
+ }
+ }
+ }
+ }
+
+ // Draw the scrollbar.
+ // Could possibly draw this after all child elements.
+ if (this.scrollbar) {
+ // XXX
+ // i = this.getScrollHeight();
+ i = Math.max(this._clines.length, this._scrollBottom());
+ }
+ if (coords.notop || coords.nobot) i = -Infinity;
+ if (this.scrollbar && (yl - yi) < i) {
+ x = xl - 1;
+ if (this.scrollbar.ignoreBorder && this.border) x++;
+ if (this.alwaysScroll) {
+ y = this.childBase / (i - (yl - yi));
+ } else {
+ y = (this.childBase + this.childOffset) / (i - 1);
+ }
+ y = yi + ((yl - yi) * y | 0);
+ if (y >= yl) y = yl - 1;
+ cell = lines[y] && lines[y][x];
+ if (cell) {
+ if (this.track) {
+ ch = this.track.ch || ' ';
+ attr = this.sattr(this.style.track,
+ this.style.track.fg || this.style.fg,
+ this.style.track.bg || this.style.bg);
+ this.screen.fillRegion(attr, ch, x, x + 1, yi, yl);
+ }
+ ch = this.scrollbar.ch || ' ';
+ attr = this.sattr(this.style.scrollbar,
+ this.style.scrollbar.fg || this.style.fg,
+ this.style.scrollbar.bg || this.style.bg);
+ if (attr !== cell[0] || ch !== cell[1]) {
+ lines[y][x][0] = attr;
+ lines[y][x][1] = ch;
+ lines[y].dirty = true;
+ }
+ }
+ }
+
+ if (this.border) xi--, xl++, yi--, yl++;
+
+ if (this.tpadding) {
+ xi -= this.padding.left, xl += this.padding.right;
+ yi -= this.padding.top, yl += this.padding.bottom;
+ }
+
+ // Draw the border.
+ if (this.border) {
+ battr = this.sattr(this.style.border);
+ y = yi;
+ if (coords.notop) y = -1;
+ for (x = xi; x < xl; x++) {
+ if (!lines[y]) break;
+ if (coords.noleft && x === xi) continue;
+ if (coords.noright && x === xl - 1) continue;
+ cell = lines[y][x];
+ if (!cell) continue;
+ if (this.border.type === 'line') {
+ if (x === xi) {
+ ch = '\u250c'; // '┌'
+ if (!this.border.left) {
+ if (this.border.top) {
+ ch = '\u2500'; // '─'
+ } else {
+ continue;
+ }
+ } else {
+ if (!this.border.top) {
+ ch = '\u2502'; // '│'
+ }
+ }
+ } else if (x === xl - 1) {
+ ch = '\u2510'; // '┐'
+ if (!this.border.right) {
+ if (this.border.top) {
+ ch = '\u2500'; // '─'
+ } else {
+ continue;
+ }
+ } else {
+ if (!this.border.top) {
+ ch = '\u2502'; // '│'
+ }
+ }
+ } else {
+ ch = '\u2500'; // '─'
+ }
+ } else if (this.border.type === 'bg') {
+ ch = this.border.ch;
+ }
+ if (!this.border.top && x !== xi && x !== xl - 1) {
+ ch = ' ';
+ if (dattr !== cell[0] || ch !== cell[1]) {
+ lines[y][x][0] = dattr;
+ lines[y][x][1] = ch;
+ lines[y].dirty = true;
+ continue;
+ }
+ }
+ if (battr !== cell[0] || ch !== cell[1]) {
+ lines[y][x][0] = battr;
+ lines[y][x][1] = ch;
+ lines[y].dirty = true;
+ }
+ }
+ y = yi + 1;
+ for (; y < yl - 1; y++) {
+ if (!lines[y]) continue;
+ cell = lines[y][xi];
+ if (cell) {
+ if (this.border.left) {
+ if (this.border.type === 'line') {
+ ch = '\u2502'; // '│'
+ } else if (this.border.type === 'bg') {
+ ch = this.border.ch;
+ }
+ if (!coords.noleft)
+ if (battr !== cell[0] || ch !== cell[1]) {
+ lines[y][xi][0] = battr;
+ lines[y][xi][1] = ch;
+ lines[y].dirty = true;
+ }
+ } else {
+ ch = ' ';
+ if (dattr !== cell[0] || ch !== cell[1]) {
+ lines[y][xi][0] = dattr;
+ lines[y][xi][1] = ch;
+ lines[y].dirty = true;
+ }
+ }
+ }
+ cell = lines[y][xl - 1];
+ if (cell) {
+ if (this.border.right) {
+ if (this.border.type === 'line') {
+ ch = '\u2502'; // '│'
+ } else if (this.border.type === 'bg') {
+ ch = this.border.ch;
+ }
+ if (!coords.noright)
+ if (battr !== cell[0] || ch !== cell[1]) {
+ lines[y][xl - 1][0] = battr;
+ lines[y][xl - 1][1] = ch;
+ lines[y].dirty = true;
+ }
+ } else {
+ ch = ' ';
+ if (dattr !== cell[0] || ch !== cell[1]) {
+ lines[y][xl - 1][0] = dattr;
+ lines[y][xl - 1][1] = ch;
+ lines[y].dirty = true;
+ }
+ }
+ }
+ }
+ y = yl - 1;
+ if (coords.nobot) y = -1;
+ for (x = xi; x < xl; x++) {
+ if (!lines[y]) break;
+ if (coords.noleft && x === xi) continue;
+ if (coords.noright && x === xl - 1) continue;
+ cell = lines[y][x];
+ if (!cell) continue;
+ if (this.border.type === 'line') {
+ if (x === xi) {
+ ch = '\u2514'; // '└'
+ if (!this.border.left) {
+ if (this.border.bottom) {
+ ch = '\u2500'; // '─'
+ } else {
+ continue;
+ }
+ } else {
+ if (!this.border.bottom) {
+ ch = '\u2502'; // '│'
+ }
+ }
+ } else if (x === xl - 1) {
+ ch = '\u2518'; // '┘'
+ if (!this.border.right) {
+ if (this.border.bottom) {
+ ch = '\u2500'; // '─'
+ } else {
+ continue;
+ }
+ } else {
+ if (!this.border.bottom) {
+ ch = '\u2502'; // '│'
+ }
+ }
+ } else {
+ ch = '\u2500'; // '─'
+ }
+ } else if (this.border.type === 'bg') {
+ ch = this.border.ch;
+ }
+ if (!this.border.bottom && x !== xi && x !== xl - 1) {
+ ch = ' ';
+ if (dattr !== cell[0] || ch !== cell[1]) {
+ lines[y][x][0] = dattr;
+ lines[y][x][1] = ch;
+ lines[y].dirty = true;
+ }
+ continue;
+ }
+ if (battr !== cell[0] || ch !== cell[1]) {
+ lines[y][x][0] = battr;
+ lines[y][x][1] = ch;
+ lines[y].dirty = true;
+ }
+ }
+ }
+
+ if (this.shadow) {
+ // right
+ y = Math.max(yi + 1, 0);
+ for (; y < yl + 1; y++) {
+ if (!lines[y]) break;
+ x = xl;
+ for (; x < xl + 2; x++) {
+ if (!lines[y][x]) break;
+ // lines[y][x][0] = this._blend(this.dattr, lines[y][x][0]);
+ lines[y][x][0] = this._blend(lines[y][x][0]);
+ lines[y].dirty = true;
+ }
+ }
+ // bottom
+ y = yl;
+ for (; y < yl + 1; y++) {
+ if (!lines[y]) break;
+ for (x = Math.max(xi + 1, 0); x < xl; x++) {
+ if (!lines[y][x]) break;
+ // lines[y][x][0] = this._blend(this.dattr, lines[y][x][0]);
+ lines[y][x][0] = this._blend(lines[y][x][0]);
+ lines[y].dirty = true;
+ }
+ }
+ }
+
+ this.children.forEach(function(el) {
+ if (el.screen._ci !== -1) {
+ el.index = el.screen._ci++;
+ }
+ // if (el.screen._rendering) {
+ // el._rendering = true;
+ // }
+ el.render();
+ // if (el.screen._rendering) {
+ // el._rendering = false;
+ // }
+ });
+
+ this._emit('render', [coords]);
+
+ return coords;
+};
+
+Element.prototype._render = Element.prototype.render;
+
+/**
+ * Blending and Shadows
+ */
+
+Element.prototype._blend = function blend(attr, attr2) {
+ var bg = attr & 0x1ff;
+ if (attr2 != null) {
+ var bg2 = attr2 & 0x1ff;
+ if (bg === 0x1ff) bg = 0;
+ if (bg2 === 0x1ff) bg2 = 0;
+ bg = colors.mixColors(bg, bg2);
+ } else {
+ if (blend._cache[bg] != null) {
+ bg = blend._cache[bg];
+ // } else if (bg < 8) {
+ // bg += 8;
+ } else if (bg >= 8 && bg <= 15) {
+ bg -= 8;
+ } else {
+ var name = colors.ncolors[bg];
+ if (name) {
+ for (var i = 0; i < colors.ncolors.length; i++) {
+ if (name === colors.ncolors[i] && i !== bg) {
+ var c = colors.vcolors[bg];
+ var nc = colors.vcolors[i];
+ if (nc[0] + nc[1] + nc[2] < c[0] + c[1] + c[2]) {
+ blend._cache[bg] = i;
+ bg = i;
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ attr &= ~0x1ff;
+ attr |= bg;
+
+ var fg = (attr >> 9) & 0x1ff;
+ if (attr2 != null) {
+ var fg2 = (attr2 >> 9) & 0x1ff;
+ // 0, 7, 188, 231, 251
+ if (fg === 0x1ff) {
+ // XXX workaround
+ fg = 248;
+ } else {
+ if (fg === 0x1ff) fg = 7;
+ if (fg2 === 0x1ff) fg2 = 7;
+ fg = colors.mixColors(fg, fg2);
+ }
+ } else {
+ if (blend._cache[fg] != null) {
+ fg = blend._cache[fg];
+ // } else if (fg < 8) {
+ // fg += 8;
+ } else if (fg >= 8 && fg <= 15) {
+ fg -= 8;
+ } else {
+ var name = colors.ncolors[fg];
+ if (name) {
+ for (var i = 0; i < colors.ncolors.length; i++) {
+ if (name === colors.ncolors[i] && i !== fg) {
+ var c = colors.vcolors[fg];
+ var nc = colors.vcolors[i];
+ if (nc[0] + nc[1] + nc[2] < c[0] + c[1] + c[2]) {
+ blend._cache[fg] = i;
+ fg = i;
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ attr &= ~(0x1ff << 9);
+ attr |= fg << 9;
+
+ return attr;
+};
+
+Element.prototype._blend._cache = {};
+
+/**
+ * Content Methods
+ */
+
+Element.prototype.insertLine = function(i, line) {
+ if (typeof line === 'string') line = line.split('\n');
+
+ if (i !== i || i == null) {
+ i = this._clines.ftor.length;
+ }
+
+ i = Math.max(i, 0);
+
+ while (this._clines.fake.length < i) {
+ this._clines.fake.push('');
+ this._clines.ftor.push([this._clines.push('') - 1]);
+ this._clines.rtof(this._clines.fake.length - 1);
+ }
+
+ // NOTE: Could possibly compare the first and last ftor line numbers to see
+ // if they're the same, or if they fit in the visible region entirely.
+ var start = this._clines.length
+ , diff
+ , real;
+
+ if (i >= this._clines.ftor.length) {
+ real = this._clines.ftor[this._clines.ftor.length - 1];
+ real = real[real.length - 1] + 1;
+ } else {
+ real = this._clines.ftor[i][0];
+ }
+
+ for (var j = 0; j < line.length; j++) {
+ this._clines.fake.splice(i + j, 0, line[j]);
+ }
+
+ this.setContent(this._clines.fake.join('\n'), true);
+
+ diff = this._clines.length - start;
+
+ if (diff > 0) {
+ var pos = this._getCoords();
+ if (!pos) return;
+
+ var height = pos.yl - pos.yi - this.iheight
+ , base = this.childBase || 0
+ , visible = real >= base && real - base < height;
+
+ if (pos && visible && this.screen.cleanSides(this)) {
+ this.screen.insertLine(diff,
+ pos.yi + this.itop + real - base,
+ pos.yi,
+ pos.yl - this.ibottom - 1);
+ }
+ }
+};
+
+Element.prototype.deleteLine = function(i, n) {
+ n = n || 1;
+
+ if (i !== i || i == null) {
+ i = this._clines.ftor.length - 1;
+ }
+
+ i = Math.max(i, 0);
+ i = Math.min(i, this._clines.ftor.length - 1);
+
+ // NOTE: Could possibly compare the first and last ftor line numbers to see
+ // if they're the same, or if they fit in the visible region entirely.
+ var start = this._clines.length
+ , diff
+ , real = this._clines.ftor[i][0];
+
+ while (n--) {
+ this._clines.fake.splice(i, 1);
+ }
+
+ this.setContent(this._clines.fake.join('\n'), true);
+
+ diff = start - this._clines.length;
+
+ if (diff > 0) {
+ var pos = this._getCoords();
+ if (!pos) return;
+
+ var height = pos.yl - pos.yi - this.iheight
+ , base = this.childBase || 0
+ , visible = real >= base && real - base < height;
+
+ if (pos && visible && this.screen.cleanSides(this)) {
+ this.screen.deleteLine(diff,
+ pos.yi + this.itop + real - base,
+ pos.yi,
+ pos.yl - this.ibottom - 1);
+ }
+ }
+
+ if (this._clines.length < height) {
+ this.clearPos();
+ }
+};
+
+Element.prototype.insertTop = function(line) {
+ var fake = this._clines.rtof[this.childBase || 0];
+ return this.insertLine(fake, line);
+};
+
+Element.prototype.insertBottom = function(line) {
+ var h = (this.childBase || 0) + this.height - this.iheight
+ , i = Math.min(h, this._clines.length)
+ , fake = this._clines.rtof[i - 1] + 1;
+
+ return this.insertLine(fake, line);
+};
+
+Element.prototype.deleteTop = function(n) {
+ var fake = this._clines.rtof[this.childBase || 0];
+ return this.deleteLine(fake, n);
+};
+
+Element.prototype.deleteBottom = function(n) {
+ var h = (this.childBase || 0) + this.height - 1 - this.iheight
+ , i = Math.min(h, this._clines.length - 1)
+ , n = n || 1
+ , fake = this._clines.rtof[i];
+
+ return this.deleteLine(fake - (n - 1), n);
+};
+
+Element.prototype.setLine = function(i, line) {
+ i = Math.max(i, 0);
+ while (this._clines.fake.length < i) {
+ this._clines.fake.push('');
+ }
+ this._clines.fake[i] = line;
+ return this.setContent(this._clines.fake.join('\n'), true);
+};
+
+Element.prototype.setBaseLine = function(i, line) {
+ var fake = this._clines.rtof[this.childBase || 0];
+ return this.setLine(fake + i, line);
+};
+
+Element.prototype.getLine = function(i) {
+ i = Math.max(i, 0);
+ i = Math.min(i, this._clines.fake.length - 1);
+ return this._clines.fake[i];
+};
+
+Element.prototype.getBaseLine = function(i) {
+ var fake = this._clines.rtof[this.childBase || 0];
+ return this.getLine(fake + i);
+};
+
+Element.prototype.clearLine = function(i) {
+ i = Math.min(i, this._clines.fake.length - 1);
+ return this.setLine(i, '');
+};
+
+Element.prototype.clearBaseLine = function(i) {
+ var fake = this._clines.rtof[this.childBase || 0];
+ return this.clearLine(fake + i);
+};
+
+Element.prototype.unshiftLine = function(line) {
+ return this.insertLine(0, line);
+};
+
+Element.prototype.shiftLine = function(n) {
+ return this.deleteLine(0, n);
+};
+
+Element.prototype.pushLine = function(line) {
+ if (!this.content) return this.setLine(0, line);
+ return this.insertLine(this._clines.fake.length, line);
+};
+
+Element.prototype.popLine = function(n) {
+ return this.deleteLine(this._clines.fake.length - 1, n);
+};
+
+Element.prototype.getLines = function() {
+ return this._clines.fake.slice();
+};
+
+Element.prototype.getScreenLines = function() {
+ return this._clines.slice();
+};
+
+Element.prototype.strWidth = function(text) {
+ text = this.parseTags
+ ? helpers.stripTags(text)
+ : text;
+ return this.screen.fullUnicode
+ ? unicode.strWidth(text)
+ : helpers.dropUnicode(text).length;
+};
+
+Element.prototype.screenshot = function(xi, xl, yi, yl) {
+ xi = this.lpos.xi + this.ileft + (xi || 0);
+ if (xl != null) {
+ xl = this.lpos.xi + this.ileft + (xl || 0);
+ } else {
+ xl = this.lpos.xl - this.iright;
+ }
+ yi = this.lpos.yi + this.itop + (yi || 0);
+ if (yl != null) {
+ yl = this.lpos.yi + this.itop + (yl || 0);
+ } else {
+ yl = this.lpos.yl - this.ibottom;
+ }
+ return this.screen.screenshot(xi, xl, yi, yl);
+};
+
+module.exports = Element;
diff --git a/lib/widgets/filemanager.js b/lib/widgets/filemanager.js
new file mode 100644
index 0000000..39caae9
--- /dev/null
+++ b/lib/widgets/filemanager.js
@@ -0,0 +1,208 @@
+/**
+ * filemanager.js - file manager element for blessed
+ * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
+ * https://github.com/chjj/blessed
+ */
+
+/**
+ * Modules
+ */
+
+var path = require('path')
+ , fs = require('fs');
+
+var helpers = require('../helpers');
+
+var Node = require('./node');
+var List = require('./list');
+
+/**
+ * FileManager
+ */
+
+function FileManager(options) {
+ var self = this;
+
+ if (!(this instanceof Node)) {
+ return new FileManager(options);
+ }
+
+ options = options || {};
+ options.parseTags = true;
+ // options.label = ' {blue-fg}%path{/blue-fg} ';
+
+ List.call(this, options);
+
+ this.cwd = options.cwd || process.cwd();
+ this.file = this.cwd;
+ this.value = this.cwd;
+
+ if (options.label && ~options.label.indexOf('%path')) {
+ this._label.setContent(options.label.replace('%path', this.cwd));
+ }
+
+ this.on('select', function(item) {
+ var value = item.content.replace(/\{[^{}]+\}/g, '').replace(/@$/, '')
+ , file = path.resolve(self.cwd, value);
+
+ return fs.stat(file, function(err, stat) {
+ if (err) {
+ return self.emit('error', err, file);
+ }
+ self.file = file;
+ self.value = file;
+ if (stat.isDirectory()) {
+ self.emit('cd', file, self.cwd);
+ self.cwd = file;
+ if (options.label && ~options.label.indexOf('%path')) {
+ self._label.setContent(options.label.replace('%path', file));
+ }
+ self.refresh();
+ } else {
+ self.emit('file', file);
+ }
+ });
+ });
+}
+
+FileManager.prototype.__proto__ = List.prototype;
+
+FileManager.prototype.type = 'file-manager';
+
+FileManager.prototype.refresh = function(cwd, callback) {
+ if (!callback) {
+ callback = cwd;
+ cwd = null;
+ }
+
+ var self = this;
+
+ if (cwd) this.cwd = cwd;
+ else cwd = this.cwd;
+
+ return fs.readdir(cwd, function(err, list) {
+ if (err && err.code === 'ENOENT') {
+ self.cwd = cwd !== process.env.HOME
+ ? process.env.HOME
+ : '/';
+ return self.refresh(callback);
+ }
+
+ if (err) {
+ if (callback) return callback(err);
+ return self.emit('error', err, cwd);
+ }
+
+ var dirs = []
+ , files = [];
+
+ list.unshift('..');
+
+ list.forEach(function(name) {
+ var f = path.resolve(cwd, name)
+ , stat;
+
+ try {
+ stat = fs.lstatSync(f);
+ } catch (e) {
+ ;
+ }
+
+ if ((stat && stat.isDirectory()) || name === '..') {
+ dirs.push({
+ name: name,
+ text: '{light-blue-fg}' + name + '{/light-blue-fg}/',
+ dir: true
+ });
+ } else if (stat && stat.isSymbolicLink()) {
+ files.push({
+ name: name,
+ text: '{light-cyan-fg}' + name + '{/light-cyan-fg}@',
+ dir: false
+ });
+ } else {
+ files.push({
+ name: name,
+ text: name,
+ dir: false
+ });
+ }
+ });
+
+ dirs = helpers.asort(dirs);
+ files = helpers.asort(files);
+
+ list = dirs.concat(files).map(function(data) {
+ return data.text;
+ });
+
+ self.setItems(list);
+ self.select(0);
+ self.screen.render();
+
+ self.emit('refresh');
+
+ if (callback) callback();
+ });
+};
+
+FileManager.prototype.pick = function(cwd, callback) {
+ if (!callback) {
+ callback = cwd;
+ cwd = null;
+ }
+
+ var self = this
+ , focused = this.screen.focused === this
+ , hidden = this.hidden
+ , onfile
+ , oncancel;
+
+ function resume() {
+ self.removeListener('file', onfile);
+ self.removeListener('cancel', oncancel);
+ if (hidden) {
+ self.hide();
+ }
+ if (!focused) {
+ self.screen.restoreFocus();
+ }
+ self.screen.render();
+ }
+
+ this.on('file', onfile = function(file) {
+ resume();
+ return callback(null, file);
+ });
+
+ this.on('cancel', oncancel = function() {
+ resume();
+ return callback();
+ });
+
+ this.refresh(cwd, function(err) {
+ if (err) return callback(err);
+
+ if (hidden) {
+ self.show();
+ }
+
+ if (!focused) {
+ self.screen.saveFocus();
+ self.focus();
+ }
+
+ self.screen.render();
+ });
+};
+
+FileManager.prototype.reset = function(cwd, callback) {
+ if (!callback) {
+ callback = cwd;
+ cwd = null;
+ }
+ this.cwd = cwd || this.options.cwd;
+ this.refresh(callback);
+};
+
+module.exports = FileManager;
diff --git a/lib/widgets/form.js b/lib/widgets/form.js
new file mode 100644
index 0000000..26c7038
--- /dev/null
+++ b/lib/widgets/form.js
@@ -0,0 +1,266 @@
+/**
+ * form.js - form element for blessed
+ * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
+ * https://github.com/chjj/blessed
+ */
+
+/**
+ * Modules
+ */
+
+var helpers = require('../helpers');
+
+var Node = require('./node');
+var Box = require('./box');
+
+/**
+ * Form
+ */
+
+function Form(options) {
+ var self = this;
+
+ if (!(this instanceof Node)) {
+ return new Form(options);
+ }
+
+ options = options || {};
+
+ options.ignoreKeys = true;
+ Box.call(this, options);
+
+ if (options.keys) {
+ this.screen._listenKeys(this);
+ this.on('element keypress', function(el, ch, key) {
+ if ((key.name === 'tab' && !key.shift)
+ || (el.type === 'textbox' && options.autoNext && key.name === 'enter')
+ || key.name === 'down'
+ || (options.vi && key.name === 'j')) {
+ if (el.type === 'textbox' || el.type === 'textarea') {
+ if (key.name === 'j') return;
+ if (key.name === 'tab') {
+ // Workaround, since we can't stop the tab from being added.
+ el.emit('keypress', null, { name: 'backspace' });
+ }
+ el.emit('keypress', '\x1b', { name: 'escape' });
+ }
+ self.focusNext();
+ return;
+ }
+
+ if ((key.name === 'tab' && key.shift)
+ || key.name === 'up'
+ || (options.vi && key.name === 'k')) {
+ if (el.type === 'textbox' || el.type === 'textarea') {
+ if (key.name === 'k') return;
+ el.emit('keypress', '\x1b', { name: 'escape' });
+ }
+ self.focusPrevious();
+ return;
+ }
+
+ if (key.name === 'escape') {
+ self.focus();
+ return;
+ }
+ });
+ }
+}
+
+Form.prototype.__proto__ = Box.prototype;
+
+Form.prototype.type = 'form';
+
+Form.prototype._refresh = function() {
+ // XXX Possibly remove this if statement and refresh on every focus.
+ // Also potentially only include *visible* focusable elements.
+ // This would remove the need to check for _selected.visible in previous()
+ // and next().
+ if (!this._children) {
+ var out = [];
+
+ this.children.forEach(function fn(el) {
+ if (el.keyable) out.push(el);
+ el.children.forEach(fn);
+ });
+
+ this._children = out;
+ }
+};
+
+Form.prototype._visible = function() {
+ return !!this._children.filter(function(el) {
+ return el.visible;
+ }).length;
+};
+
+Form.prototype.next = function() {
+ this._refresh();
+
+ if (!this._visible()) return;
+
+ if (!this._selected) {
+ this._selected = this._children[0];
+ if (!this._selected.visible) return this.next();
+ if (this.screen.focused !== this._selected) return this._selected;
+ }
+
+ var i = this._children.indexOf(this._selected);
+ if (!~i || !this._children[i + 1]) {
+ this._selected = this._children[0];
+ if (!this._selected.visible) return this.next();
+ return this._selected;
+ }
+
+ this._selected = this._children[i + 1];
+ if (!this._selected.visible) return this.next();
+ return this._selected;
+};
+
+Form.prototype.previous = function() {
+ this._refresh();
+
+ if (!this._visible()) return;
+
+ if (!this._selected) {
+ this._selected = this._children[this._children.length - 1];
+ if (!this._selected.visible) return this.previous();
+ if (this.screen.focused !== this._selected) return this._selected;
+ }
+
+ var i = this._children.indexOf(this._selected);
+ if (!~i || !this._children[i - 1]) {
+ this._selected = this._children[this._children.length - 1];
+ if (!this._selected.visible) return this.previous();
+ return this._selected;
+ }
+
+ this._selected = this._children[i - 1];
+ if (!this._selected.visible) return this.previous();
+ return this._selected;
+};
+
+Form.prototype.focusNext = function() {
+ var next = this.next();
+ if (next) next.focus();
+};
+
+Form.prototype.focusPrevious = function() {
+ var previous = this.previous();
+ if (previous) previous.focus();
+};
+
+Form.prototype.resetSelected = function() {
+ this._selected = null;
+};
+
+Form.prototype.focusFirst = function() {
+ this.resetSelected();
+ this.focusNext();
+};
+
+Form.prototype.focusLast = function() {
+ this.resetSelected();
+ this.focusPrevious();
+};
+
+Form.prototype.submit = function() {
+ var self = this
+ , out = {};
+
+ this.children.forEach(function fn(el) {
+ if (el.value != null) {
+ var name = el.name || el.type;
+ if (Array.isArray(out[name])) {
+ out[name].push(el.value);
+ } else if (out[name]) {
+ out[name] = [out[name], el.value];
+ } else {
+ out[name] = el.value;
+ }
+ }
+ el.children.forEach(fn);
+ });
+
+ this.emit('submit', out);
+
+ return this.submission = out;
+};
+
+Form.prototype.cancel = function() {
+ this.emit('cancel');
+};
+
+Form.prototype.reset = function() {
+ this.children.forEach(function fn(el) {
+ switch (el.type) {
+ case 'screen':
+ break;
+ case 'box':
+ break;
+ case 'text':
+ break;
+ case 'line':
+ break;
+ case 'scrollable-box':
+ break;
+ case 'list':
+ el.select(0);
+ return;
+ case 'form':
+ break;
+ case 'input':
+ break;
+ case 'textbox':
+ el.clearInput();
+ return;
+ case 'textarea':
+ el.clearInput();
+ return;
+ case 'button':
+ delete el.value;
+ break;
+ case 'progress-bar':
+ el.setProgress(0);
+ break;
+ case 'file-manager':
+ el.refresh(el.options.cwd);
+ return;
+ case 'checkbox':
+ el.uncheck();
+ return;
+ case 'radio-set':
+ break;
+ case 'radio-button':
+ el.uncheck();
+ return;
+ case 'prompt':
+ break;
+ case 'question':
+ break;
+ case 'message':
+ break;
+ case 'info':
+ break;
+ case 'loading':
+ break;
+ case 'list-bar':
+ //el.select(0);
+ break;
+ case 'dir-manager':
+ el.refresh(el.options.cwd);
+ return;
+ case 'terminal':
+ el.write('');
+ return;
+ case 'image':
+ //el.clearImage();
+ return;
+ }
+ el.children.forEach(fn);
+ });
+
+ this.emit('reset');
+};
+
+module.exports = Form;
diff --git a/lib/widgets/image.js b/lib/widgets/image.js
new file mode 100644
index 0000000..e4014c6
--- /dev/null
+++ b/lib/widgets/image.js
@@ -0,0 +1,721 @@
+/**
+ * image.js - w3m image element for blessed
+ * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
+ * https://github.com/chjj/blessed
+ */
+
+/**
+ * Modules
+ */
+
+var fs = require('fs')
+ , cp = require('child_process');
+
+var helpers = require('../helpers');
+
+var Node = require('./node');
+var Box = require('./box');
+
+/**
+ * Image
+ * Good example of w3mimgdisplay commands:
+ * https://github.com/hut/ranger/blob/master/ranger/ext/img_display.py
+ */
+
+function Image(options) {
+ var self = this;
+
+ if (!(this instanceof Node)) {
+ return new Image(options);
+ }
+
+ options = options || {};
+
+ Box.call(this, options);
+
+ if (options.w3m) {
+ Image.w3mdisplay = options.w3m;
+ }
+
+ if (Image.hasW3MDisplay == null) {
+ if (fs.existsSync(Image.w3mdisplay)) {
+ Image.hasW3MDisplay = true;
+ } else if (options.search !== false) {
+ var file = helpers.findFile('/usr', 'w3mimgdisplay')
+ || helpers.findFile('/lib', 'w3mimgdisplay')
+ || helpers.findFile('/bin', 'w3mimgdisplay');
+ if (file) {
+ Image.hasW3MDisplay = true;
+ Image.w3mdisplay = file;
+ } else {
+ Image.hasW3MDisplay = false;
+ }
+ }
+ }
+
+ this.on('hide', function() {
+ self._lastFile = self.file;
+ self.clearImage();
+ });
+
+ this.on('show', function() {
+ if (!self._lastFile) return;
+ self.setImage(self._lastFile);
+ });
+
+ this.on('detach', function() {
+ self._lastFile = self.file;
+ self.clearImage();
+ });
+
+ this.on('attach', function() {
+ if (!self._lastFile) return;
+ self.setImage(self._lastFile);
+ });
+
+ this.onScreenEvent('resize', function() {
+ self._needsRatio = true;
+ });
+
+ // Get images to overlap properly. Maybe not worth it:
+ // this.onScreenEvent('render', function() {
+ // self.screen.program.flush();
+ // if (!self._noImage) return;
+ // function display(el, next) {
+ // if (el.type === 'image' && el.file) {
+ // el.setImage(el.file, next);
+ // } else {
+ // next();
+ // }
+ // }
+ // function done(el) {
+ // el.children.forEach(recurse);
+ // }
+ // function recurse(el) {
+ // display(el, function() {
+ // var pending = el.children.length;
+ // el.children.forEach(function(el) {
+ // display(el, function() {
+ // if (!--pending) done(el);
+ // });
+ // });
+ // });
+ // }
+ // recurse(self.screen);
+ // });
+
+ this.onScreenEvent('render', function() {
+ self.screen.program.flush();
+ if (!self._noImage) {
+ self.setImage(self.file);
+ }
+ });
+
+ if (this.options.file || this.options.img) {
+ this.setImage(this.options.file || this.options.img);
+ }
+}
+
+Image.prototype.__proto__ = Box.prototype;
+
+Image.prototype.type = 'image';
+
+Image.w3mdisplay = '/usr/lib/w3m/w3mimgdisplay';
+
+Image.prototype.spawn = function(file, args, opt, callback) {
+ var screen = this.screen
+ , opt = opt || {}
+ , spawn = require('child_process').spawn
+ , ps;
+
+ ps = spawn(file, args, opt);
+
+ ps.on('error', function(err) {
+ if (!callback) return;
+ return callback(err);
+ });
+
+ ps.on('exit', function(code) {
+ if (!callback) return;
+ if (code !== 0) return callback(new Error('Exit Code: ' + code));
+ return callback(null, code === 0);
+ });
+
+ return ps;
+};
+
+Image.prototype.setImage = function(img, callback) {
+ var self = this;
+
+ if (this._settingImage) {
+ this._queue = this._queue || [];
+ this._queue.push([img, callback]);
+ return;
+ }
+ this._settingImage = true;
+
+ var reset = function(err, success) {
+ self._settingImage = false;
+ self._queue = self._queue || [];
+ var item = self._queue.shift();
+ if (item) {
+ self.setImage(item[0], item[1]);
+ }
+ };
+
+ if (Image.hasW3MDisplay === false) {
+ reset();
+ if (!callback) return;
+ return callback(new Error('W3M Image Display not available.'));
+ }
+
+ if (!img) {
+ reset();
+ if (!callback) return;
+ return callback(new Error('No image.'));
+ }
+
+ this.file = img;
+
+ return this.getPixelRatio(function(err, ratio) {
+ if (err) {
+ reset();
+ if (!callback) return;
+ return callback(err);
+ }
+
+ return self.renderImage(img, ratio, function(err, success) {
+ if (err) {
+ reset();
+ if (!callback) return;
+ return callback(err);
+ }
+
+ if (self.shrink || self.options.autofit) {
+ delete self.shrink;
+ delete self.options.shrink;
+ self.options.autofit = true;
+ return self.imageSize(function(err, size) {
+ if (err) {
+ reset();
+ if (!callback) return;
+ return callback(err);
+ }
+
+ if (self._lastSize
+ && ratio.tw === self._lastSize.tw
+ && ratio.th === self._lastSize.th
+ && size.width === self._lastSize.width
+ && size.height === self._lastSize.height
+ && self.aleft === self._lastSize.aleft
+ && self.atop === self._lastSize.atop) {
+ reset();
+ if (!callback) return;
+ return callback(null, success);
+ }
+
+ self._lastSize = {
+ tw: ratio.tw,
+ th: ratio.th,
+ width: size.width,
+ height: size.height,
+ aleft: self.aleft,
+ atop: self.atop
+ };
+
+ self.position.width = size.width / ratio.tw | 0;
+ self.position.height = size.height / ratio.th | 0;
+
+ self._noImage = true;
+ self.screen.render();
+ self._noImage = false;
+
+ reset();
+ return self.renderImage(img, ratio, callback);
+ });
+ }
+
+ reset();
+ if (!callback) return;
+ return callback(null, success);
+ });
+ });
+};
+
+Image.prototype.renderImage = function(img, ratio, callback) {
+ var self = this;
+
+ if (cp.execSync) {
+ callback = callback || function(err, result) { return result; };
+ try {
+ return callback(null, this.renderImageSync(img, ratio));
+ } catch (e) {
+ return callback(e);
+ }
+ }
+
+ if (Image.hasW3MDisplay === false) {
+ if (!callback) return;
+ return callback(new Error('W3M Image Display not available.'));
+ }
+
+ if (!ratio) {
+ if (!callback) return;
+ return callback(new Error('No ratio.'));
+ }
+
+ // clearImage unsets these:
+ var _file = self.file;
+ var _lastSize = self._lastSize;
+ return self.clearImage(function(err) {
+ if (err) return callback(err);
+
+ self.file = _file;
+ self._lastSize = _lastSize;
+
+ var opt = {
+ stdio: 'pipe',
+ env: process.env,
+ cwd: process.env.HOME
+ };
+
+ var ps = self.spawn(Image.w3mdisplay, [], opt, function(err, success) {
+ if (!callback) return;
+ return err
+ ? callback(err)
+ : callback(null, success);
+ });
+
+ var width = self.width * ratio.tw | 0
+ , height = self.height * ratio.th | 0
+ , aleft = self.aleft * ratio.tw | 0
+ , atop = self.atop * ratio.th | 0;
+
+ var input = '0;1;'
+ + aleft + ';'
+ + atop + ';'
+ + width + ';'
+ + height + ';;;;;'
+ + img
+ + '\n4;\n3;\n';
+
+ self._props = {
+ aleft: aleft,
+ atop: atop,
+ width: width,
+ height: height
+ };
+
+ ps.stdin.write(input);
+ ps.stdin.end();
+ });
+};
+
+Image.prototype.clearImage = function(callback) {
+ var self = this;
+
+ if (cp.execSync) {
+ callback = callback || function(err, result) { return result; };
+ try {
+ return callback(null, this.clearImageSync());
+ } catch (e) {
+ return callback(e);
+ }
+ }
+
+ if (Image.hasW3MDisplay === false) {
+ if (!callback) return;
+ return callback(new Error('W3M Image Display not available.'));
+ }
+
+ if (!this._props) {
+ if (!callback) return;
+ return callback(null);
+ }
+
+ var opt = {
+ stdio: 'pipe',
+ env: process.env,
+ cwd: process.env.HOME
+ };
+
+ var ps = this.spawn(Image.w3mdisplay, [], opt, function(err, success) {
+ if (!callback) return;
+ return err
+ ? callback(err)
+ : callback(null, success);
+ });
+
+ var width = this._props.width + 2
+ , height = this._props.height + 2
+ , aleft = this._props.aleft
+ , atop = this._props.atop;
+
+ if (this._drag) {
+ aleft -= 10;
+ atop -= 10;
+ width += 10;
+ height += 10;
+ }
+
+ var input = '6;'
+ + aleft + ';'
+ + atop + ';'
+ + width + ';'
+ + height
+ + '\n4;\n3;\n';
+
+ delete this.file;
+ delete this._props;
+ delete this._lastSize;
+
+ ps.stdin.write(input);
+ ps.stdin.end();
+};
+
+Image.prototype.imageSize = function(callback) {
+ var self = this;
+ var img = this.file;
+
+ if (cp.execSync) {
+ callback = callback || function(err, result) { return result; };
+ try {
+ return callback(null, this.imageSizeSync());
+ } catch (e) {
+ return callback(e);
+ }
+ }
+
+ if (Image.hasW3MDisplay === false) {
+ if (!callback) return;
+ return callback(new Error('W3M Image Display not available.'));
+ }
+
+ if (!img) {
+ if (!callback) return;
+ return callback(new Error('No image.'));
+ }
+
+ var opt = {
+ stdio: 'pipe',
+ env: process.env,
+ cwd: process.env.HOME
+ };
+
+ var ps = this.spawn(Image.w3mdisplay, [], opt);
+
+ var buf = '';
+
+ ps.stdout.setEncoding('utf8');
+
+ ps.stdout.on('data', function(data) {
+ buf += data;
+ });
+
+ ps.on('error', function(err) {
+ if (!callback) return;
+ return callback(err);
+ });
+
+ ps.on('exit', function() {
+ if (!callback) return;
+ var size = buf.trim().split(/\s+/);
+ return callback(null, {
+ raw: buf.trim(),
+ width: +size[0],
+ height: +size[1]
+ });
+ });
+
+ var input = '5;' + img + '\n';
+
+ ps.stdin.write(input);
+ ps.stdin.end();
+};
+
+Image.prototype.termSize = function(callback) {
+ var self = this;
+
+ if (cp.execSync) {
+ callback = callback || function(err, result) { return result; };
+ try {
+ return callback(null, this.termSizeSync());
+ } catch (e) {
+ return callback(e);
+ }
+ }
+
+ if (Image.hasW3MDisplay === false) {
+ if (!callback) return;
+ return callback(new Error('W3M Image Display not available.'));
+ }
+
+ var opt = {
+ stdio: 'pipe',
+ env: process.env,
+ cwd: process.env.HOME
+ };
+
+ var ps = this.spawn(Image.w3mdisplay, ['-test'], opt);
+
+ var buf = '';
+
+ ps.stdout.setEncoding('utf8');
+
+ ps.stdout.on('data', function(data) {
+ buf += data;
+ });
+
+ ps.on('error', function(err) {
+ if (!callback) return;
+ return callback(err);
+ });
+
+ ps.on('exit', function() {
+ if (!callback) return;
+
+ if (!buf.trim()) {
+ // Bug: w3mimgdisplay will sometimes
+ // output nothing. Try again:
+ return self.termSize(callback);
+ }
+
+ var size = buf.trim().split(/\s+/);
+
+ return callback(null, {
+ raw: buf.trim(),
+ width: +size[0],
+ height: +size[1]
+ });
+ });
+
+ ps.stdin.end();
+};
+
+Image.prototype.getPixelRatio = function(callback) {
+ var self = this;
+
+ if (cp.execSync) {
+ callback = callback || function(err, result) { return result; };
+ try {
+ return callback(null, this.getPixelRatioSync());
+ } catch (e) {
+ return callback(e);
+ }
+ }
+
+ // XXX We could cache this, but sometimes it's better
+ // to recalculate to be pixel perfect.
+ if (this._ratio && !this._needsRatio) {
+ return callback(null, this._ratio);
+ }
+
+ return this.termSize(function(err, dimensions) {
+ if (err) return callback(err);
+
+ self._ratio = {
+ tw: dimensions.width / self.screen.width,
+ th: dimensions.height / self.screen.height
+ };
+
+ self._needsRatio = false;
+
+ return callback(null, self._ratio);
+ });
+};
+
+Image.prototype.renderImageSync = function(img, ratio) {
+ var self = this;
+
+ if (Image.hasW3MDisplay === false) {
+ throw new Error('W3M Image Display not available.');
+ }
+
+ if (!ratio) {
+ throw new Error('No ratio.');
+ }
+
+ // clearImage unsets these:
+ var _file = this.file;
+ var _lastSize = this._lastSize;
+
+ this.clearImageSync();
+
+ this.file = _file;
+ this._lastSize = _lastSize;
+
+ var width = this.width * ratio.tw | 0
+ , height = this.height * ratio.th | 0
+ , aleft = this.aleft * ratio.tw | 0
+ , atop = this.atop * ratio.th | 0;
+
+ var input = '0;1;'
+ + aleft + ';'
+ + atop + ';'
+ + width + ';'
+ + height + ';;;;;'
+ + img
+ + '\n4;\n3;\n';
+
+ this._props = {
+ aleft: aleft,
+ atop: atop,
+ width: width,
+ height: height
+ };
+
+ try {
+ cp.execFileSync(Image.w3mdisplay, [], {
+ env: process.env,
+ encoding: 'utf8',
+ input: input,
+ timeout: 1000
+ });
+ } catch (e) {
+ ;
+ }
+
+ return true;
+};
+
+Image.prototype.clearImageSync = function() {
+ if (Image.hasW3MDisplay === false) {
+ throw new Error('W3M Image Display not available.');
+ }
+
+ if (!this._props) {
+ return false;
+ }
+
+ var width = this._props.width + 2
+ , height = this._props.height + 2
+ , aleft = this._props.aleft
+ , atop = this._props.atop;
+
+ if (this._drag) {
+ aleft -= 10;
+ atop -= 10;
+ width += 10;
+ height += 10;
+ }
+
+ var input = '6;'
+ + aleft + ';'
+ + atop + ';'
+ + width + ';'
+ + height
+ + '\n4;\n3;\n';
+
+ delete this.file;
+ delete this._props;
+ delete this._lastSize;
+
+ try {
+ cp.execFileSync(Image.w3mdisplay, [], {
+ env: process.env,
+ encoding: 'utf8',
+ input: input,
+ timeout: 1000
+ });
+ } catch (e) {
+ ;
+ }
+
+ return true;
+};
+
+Image.prototype.imageSizeSync = function() {
+ var img = this.file;
+
+ if (Image.hasW3MDisplay === false) {
+ throw new Error('W3M Image Display not available.');
+ }
+
+ if (!img) {
+ throw new Error('No image.');
+ }
+
+ var buf = '';
+ var input = '5;' + img + '\n';
+
+ try {
+ buf = cp.execFileSync(Image.w3mdisplay, [], {
+ env: process.env,
+ encoding: 'utf8',
+ input: input,
+ timeout: 1000
+ });
+ } catch (e) {
+ ;
+ }
+
+ var size = buf.trim().split(/\s+/);
+
+ return {
+ raw: buf.trim(),
+ width: +size[0],
+ height: +size[1]
+ };
+};
+
+Image.prototype.termSizeSync = function(_, recurse) {
+ if (Image.hasW3MDisplay === false) {
+ throw new Error('W3M Image Display not available.');
+ }
+
+ var buf = '';
+
+ try {
+ buf = cp.execFileSync(Image.w3mdisplay, ['-test'], {
+ env: process.env,
+ encoding: 'utf8',
+ timeout: 1000
+ });
+ } catch (e) {
+ ;
+ }
+
+ if (!buf.trim()) {
+ // Bug: w3mimgdisplay will sometimes
+ // output nothing. Try again:
+ recurse = recurse || 0;
+ if (++recurse === 5) {
+ throw new Error('Term size not determined.');
+ }
+ return this.termSizeSync(_, recurse);
+ }
+
+ var size = buf.trim().split(/\s+/);
+
+ return {
+ raw: buf.trim(),
+ width: +size[0],
+ height: +size[1]
+ };
+};
+
+Image.prototype.getPixelRatioSync = function() {
+ var self = this;
+
+ // XXX We could cache this, but sometimes it's better
+ // to recalculate to be pixel perfect.
+ if (this._ratio && !this._needsRatio) {
+ return this._ratio;
+ }
+ this._needsRatio = false;
+
+ var dimensions = this.termSizeSync();
+
+ this._ratio = {
+ tw: dimensions.width / this.screen.width,
+ th: dimensions.height / this.screen.height
+ };
+
+ return this._ratio;
+};
+
+Image.prototype.displayImage = function(callback) {
+ return this.screen.displayImage(this.file, callback);
+};
+
+module.exports = Image;
diff --git a/lib/widgets/input.js b/lib/widgets/input.js
new file mode 100644
index 0000000..1291fa2
--- /dev/null
+++ b/lib/widgets/input.js
@@ -0,0 +1,32 @@
+/**
+ * input.js - abstract input element for blessed
+ * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
+ * https://github.com/chjj/blessed
+ */
+
+/**
+ * Modules
+ */
+
+var helpers = require('../helpers');
+
+var Node = require('./node');
+var Box = require('./box');
+
+/**
+ * Input
+ */
+
+function Input(options) {
+ if (!(this instanceof Node)) {
+ return new Input(options);
+ }
+ options = options || {};
+ Box.call(this, options);
+}
+
+Input.prototype.__proto__ = Box.prototype;
+
+Input.prototype.type = 'input';
+
+module.exports = Input;
diff --git a/lib/widgets/line.js b/lib/widgets/line.js
new file mode 100644
index 0000000..59e52b3
--- /dev/null
+++ b/lib/widgets/line.js
@@ -0,0 +1,56 @@
+/**
+ * line.js - line element for blessed
+ * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
+ * https://github.com/chjj/blessed
+ */
+
+/**
+ * Modules
+ */
+
+var helpers = require('../helpers');
+
+var Node = require('./node');
+var Box = require('./box');
+
+/**
+ * Line
+ */
+
+function Line(options) {
+ var self = this;
+
+ if (!(this instanceof Node)) {
+ return new Line(options);
+ }
+
+ options = options || {};
+
+ var orientation = options.orientation || 'vertical';
+ delete options.orientation;
+
+ if (orientation === 'vertical') {
+ options.width = 1;
+ } else {
+ options.height = 1;
+ }
+
+ Box.call(this, options);
+
+ this.ch = !options.type || options.type === 'line'
+ ? orientation === 'horizontal' ? '─' : '│'
+ : options.ch || ' ';
+
+ this.border = {
+ type: 'bg',
+ __proto__: this
+ };
+
+ this.style.border = this.style;
+}
+
+Line.prototype.__proto__ = Box.prototype;
+
+Line.prototype.type = 'line';
+
+module.exports = Line;
diff --git a/lib/widgets/list.js b/lib/widgets/list.js
new file mode 100644
index 0000000..e6596c6
--- /dev/null
+++ b/lib/widgets/list.js
@@ -0,0 +1,514 @@
+/**
+ * list.js - list element for blessed
+ * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
+ * https://github.com/chjj/blessed
+ */
+
+/**
+ * Modules
+ */
+
+var helpers = require('../helpers');
+
+var Node = require('./node');
+var Box = require('./box');
+
+/**
+ * List
+ */
+
+function List(options) {
+ var self = this;
+
+ if (!(this instanceof Node)) {
+ return new List(options);
+ }
+
+ options = options || {};
+
+ options.ignoreKeys = true;
+ // Possibly put this here: this.items = [];
+ options.scrollable = true;
+ Box.call(this, options);
+
+ this.value = '';
+ this.items = [];
+ this.ritems = [];
+ this.selected = 0;
+ this._isList = true;
+
+ if (!this.style.selected) {
+ this.style.selected = {};
+ this.style.selected.bg = options.selectedBg;
+ this.style.selected.fg = options.selectedFg;
+ this.style.selected.bold = options.selectedBold;
+ this.style.selected.underline = options.selectedUnderline;
+ this.style.selected.blink = options.selectedBlink;
+ this.style.selected.inverse = options.selectedInverse;
+ this.style.selected.invisible = options.selectedInvisible;
+ }
+
+ if (!this.style.item) {
+ this.style.item = {};
+ this.style.item.bg = options.itemBg;
+ this.style.item.fg = options.itemFg;
+ this.style.item.bold = options.itemBold;
+ this.style.item.underline = options.itemUnderline;
+ this.style.item.blink = options.itemBlink;
+ this.style.item.inverse = options.itemInverse;
+ this.style.item.invisible = options.itemInvisible;
+ }
+
+ // Legacy: for apps written before the addition of item attributes.
+ ['bg', 'fg', 'bold', 'underline',
+ 'blink', 'inverse', 'invisible'].forEach(function(name) {
+ if (self.style[name] != null && self.style.item[name] == null) {
+ self.style.item[name] = self.style[name];
+ }
+ });
+
+ if (this.options.itemHoverBg) {
+ this.options.itemHoverEffects = { bg: this.options.itemHoverBg };
+ }
+
+ if (this.options.itemHoverEffects) {
+ this.style.item.hover = this.options.itemHoverEffects;
+ }
+
+ if (this.options.itemFocusEffects) {
+ this.style.item.focus = this.options.itemFocusEffects;
+ }
+
+ this.interactive = options.interactive !== false;
+
+ this.mouse = options.mouse || false;
+
+ if (options.items) {
+ this.ritems = options.items;
+ options.items.forEach(this.add.bind(this));
+ }
+
+ this.select(0);
+
+ if (options.mouse) {
+ this.screen._listenMouse(this);
+ this.on('element wheeldown', function(el, data) {
+ self.select(self.selected + 2);
+ self.screen.render();
+ });
+ this.on('element wheelup', function(el, data) {
+ self.select(self.selected - 2);
+ self.screen.render();
+ });
+ }
+
+ if (options.keys) {
+ this.on('keypress', function(ch, key) {
+ if (key.name === 'up' || (options.vi && key.name === 'k')) {
+ self.up();
+ self.screen.render();
+ return;
+ }
+ if (key.name === 'down' || (options.vi && key.name === 'j')) {
+ self.down();
+ self.screen.render();
+ return;
+ }
+ if (key.name === 'enter'
+ || (options.vi && key.name === 'l' && !key.shift)) {
+ self.enterSelected();
+ return;
+ }
+ if (key.name === 'escape' || (options.vi && key.name === 'q')) {
+ self.cancelSelected();
+ return;
+ }
+ if (options.vi && key.name === 'u' && key.ctrl) {
+ self.move(-((self.height - self.iheight) / 2) | 0);
+ self.screen.render();
+ return;
+ }
+ if (options.vi && key.name === 'd' && key.ctrl) {
+ self.move((self.height - self.iheight) / 2 | 0);
+ self.screen.render();
+ return;
+ }
+ if (options.vi && key.name === 'b' && key.ctrl) {
+ self.move(-(self.height - self.iheight));
+ self.screen.render();
+ return;
+ }
+ if (options.vi && key.name === 'f' && key.ctrl) {
+ self.move(self.height - self.iheight);
+ self.screen.render();
+ return;
+ }
+ if (options.vi && key.name === 'h' && key.shift) {
+ self.move(self.childBase - self.selected);
+ self.screen.render();
+ return;
+ }
+ if (options.vi && key.name === 'm' && key.shift) {
+ // TODO: Maybe use Math.min(this.items.length,
+ // ... for calculating visible items elsewhere.
+ var visible = Math.min(
+ self.height - self.iheight,
+ self.items.length) / 2 | 0;
+ self.move(self.childBase + visible - self.selected);
+ self.screen.render();
+ return;
+ }
+ if (options.vi && key.name === 'l' && key.shift) {
+ // XXX This goes one too far on lists with an odd number of items.
+ self.down(self.childBase
+ + Math.min(self.height - self.iheight, self.items.length)
+ - self.selected);
+ self.screen.render();
+ return;
+ }
+ if (options.vi && key.name === 'g' && !key.shift) {
+ self.select(0);
+ self.screen.render();
+ return;
+ }
+ if (options.vi && key.name === 'g' && key.shift) {
+ self.select(self.items.length - 1);
+ self.screen.render();
+ return;
+ }
+
+ if (options.vi && (key.ch === '/' || key.ch === '?')) {
+ if (typeof self.options.search !== 'function') {
+ return;
+ }
+ return self.options.search(function(err, value) {
+ if (typeof err === 'string' || typeof err === 'function'
+ || typeof err === 'number' || (err && err.test)) {
+ value = err;
+ err = null;
+ }
+ if (err || !value) return self.screen.render();
+ self.select(self.fuzzyFind(value, key.ch === '?'));
+ self.screen.render();
+ });
+ }
+ });
+ }
+
+ this.on('resize', function() {
+ var visible = self.height - self.iheight;
+ // if (self.selected < visible - 1) {
+ if (visible >= self.selected + 1) {
+ self.childBase = 0;
+ self.childOffset = self.selected;
+ } else {
+ // Is this supposed to be: self.childBase = visible - self.selected + 1; ?
+ self.childBase = self.selected - visible + 1;
+ self.childOffset = visible - 1;
+ }
+ });
+
+ this.on('adopt', function(el) {
+ if (!~self.items.indexOf(el)) {
+ el.fixed = true;
+ }
+ });
+
+ // Ensure children are removed from the
+ // item list if they are items.
+ this.on('remove', function(el) {
+ self.removeItem(el);
+ });
+}
+
+List.prototype.__proto__ = Box.prototype;
+
+List.prototype.type = 'list';
+
+List.prototype.add =
+List.prototype.addItem =
+List.prototype.appendItem = function(item) {
+ var self = this;
+
+ this.ritems.push(item);
+
+ // Note: Could potentially use Button here.
+ var options = {
+ screen: this.screen,
+ content: item,
+ align: this.align || 'left',
+ top: this.items.length,
+ left: 0,
+ right: (this.scrollbar ? 1 : 0),
+ tags: this.parseTags,
+ height: 1,
+ hoverEffects: this.mouse ? this.style.item.hover : null,
+ focusEffects: this.mouse ? this.style.item.focus : null,
+ autoFocus: false
+ };
+
+ if (!this.screen.autoPadding) {
+ options.top = this.itop + this.items.length;
+ options.left = this.ileft;
+ options.right = this.iright + (this.scrollbar ? 1 : 0);
+ }
+
+ // if (this.shrink) {
+ // XXX NOTE: Maybe just do this on all shrinkage once autoPadding is default?
+ if (this.shrink && this.options.normalShrink) {
+ delete options.right;
+ options.width = 'shrink';
+ }
+
+ ['bg', 'fg', 'bold', 'underline',
+ 'blink', 'inverse', 'invisible'].forEach(function(name) {
+ options[name] = function() {
+ var attr = self.items[self.selected] === item && self.interactive
+ ? self.style.selected[name]
+ : self.style.item[name];
+ if (typeof attr === 'function') attr = attr(item);
+ return attr;
+ };
+ });
+
+ if (this.style.transparent) {
+ options.transparent = true;
+ }
+
+ var item = new Box(options);
+
+ this.items.push(item);
+ this.append(item);
+
+ if (this.items.length === 1) {
+ this.select(0);
+ }
+
+ if (this.mouse) {
+ item.on('click', function(data) {
+ self.focus();
+ if (self.items[self.selected] === item) {
+ self.emit('action', item, self.selected);
+ self.emit('select', item, self.selected);
+ return;
+ }
+ self.select(item);
+ self.screen.render();
+ });
+ }
+
+ this.emit('add item');
+};
+
+List.prototype.find =
+List.prototype.fuzzyFind = function(search, back) {
+ var start = this.selected + (back ? -1 : 1);
+
+ if (typeof search === 'number') search += '';
+
+ if (search && search[0] === '/' && search[search.length - 1] === '/') {
+ try {
+ search = new RegExp(search.slice(1, -1));
+ } catch (e) {
+ ;
+ }
+ }
+
+ var test = typeof search === 'string'
+ ? function(item) { return !!~item.indexOf(search); }
+ : (search.test ? search.test.bind(search) : search);
+
+ if (typeof test !== 'function') {
+ if (this.screen.options.debug) {
+ throw new Error('fuzzyFind(): `test` is not a function.');
+ }
+ return this.selected;
+ }
+
+ if (!back) {
+ for (var i = start; i < this.ritems.length; i++){
+ if (test(helpers.cleanTags(this.ritems[i]))) return i;
+ }
+ for (var i = 0; i < start; i++){
+ if (test(helpers.cleanTags(this.ritems[i]))) return i;
+ }
+ } else {
+ for (var i = start; i >= 0; i--){
+ if (test(helpers.cleanTags(this.ritems[i]))) return i;
+ }
+ for (var i = this.ritems.length - 1; i > start; i--){
+ if (test(helpers.cleanTags(this.ritems[i]))) return i;
+ }
+ }
+
+ return this.selected;
+};
+
+List.prototype.getItemIndex = function(child) {
+ if (typeof child === 'number') {
+ return child;
+ } else if (typeof child === 'string') {
+ var i = this.ritems.indexOf(child);
+ if (~i) return i;
+ for (i = 0; i < this.ritems.length; i++) {
+ if (helpers.cleanTags(this.ritems[i]) === child) {
+ return i;
+ }
+ }
+ return -1;
+ } else {
+ return this.items.indexOf(child);
+ }
+};
+
+List.prototype.getItem = function(child) {
+ return this.items[this.getItemIndex(child)];
+};
+
+List.prototype.removeItem = function(child) {
+ var i = this.getItemIndex(child);
+ if (~i && this.items[i]) {
+ child = this.items.splice(i, 1)[0];
+ this.ritems.splice(i, 1);
+ this.remove(child);
+ if (i === this.selected) {
+ this.select(i - 1);
+ }
+ }
+ this.emit('remove item');
+};
+
+List.prototype.clearItems = function() {
+ return this.setItems([]);
+};
+
+List.prototype.setItems = function(items) {
+ var items = items.slice()
+ , original = this.items.slice()
+ , selected = this.selected
+ , sel = this.ritems[this.selected]
+ , i = 0;
+
+ this.select(0);
+
+ for (; i < items.length; i++) {
+ if (this.items[i]) {
+ this.items[i].setContent(items[i]);
+ } else {
+ this.add(items[i]);
+ }
+ }
+
+ for (; i < original.length; i++) {
+ this.remove(original[i]);
+ }
+
+ this.ritems = items;
+
+ // Try to find our old item if it still exists.
+ sel = items.indexOf(sel);
+ if (~sel) {
+ this.select(sel);
+ } else if (items.length === original.length) {
+ this.select(selected);
+ } else {
+ this.select(Math.min(selected, items.length - 1));
+ }
+
+ this.emit('set items');
+};
+
+List.prototype.select = function(index) {
+ if (!this.interactive) {
+ return;
+ }
+
+ if (!this.items.length) {
+ this.selected = 0;
+ this.value = '';
+ this.scrollTo(0);
+ return;
+ }
+
+ if (typeof index === 'object') {
+ index = this.items.indexOf(index);
+ }
+
+ if (index < 0) {
+ index = 0;
+ } else if (index >= this.items.length) {
+ index = this.items.length - 1;
+ }
+
+ if (this.selected === index && this._listInitialized) return;
+ this._listInitialized = true;
+
+ this.selected = index;
+ this.value = helpers.cleanTags(this.ritems[this.selected]);
+ if (!this.parent) return;
+ this.scrollTo(this.selected);
+
+ // XXX Move `action` and `select` events here.
+ this.emit('select item', this.items[this.selected], this.selected);
+};
+
+List.prototype.move = function(offset) {
+ this.select(this.selected + offset);
+};
+
+List.prototype.up = function(offset) {
+ this.move(-(offset || 1));
+};
+
+List.prototype.down = function(offset) {
+ this.move(offset || 1);
+};
+
+List.prototype.pick = function(label, callback) {
+ if (!callback) {
+ callback = label;
+ label = null;
+ }
+
+ if (!this.interactive) {
+ return callback();
+ }
+
+ var self = this;
+ var focused = this.screen.focused;
+ if (focused && focused._done) focused._done('stop');
+ this.screen.saveFocus();
+
+ // XXX Keep above:
+ // var parent = this.parent;
+ // this.detach();
+ // parent.append(this);
+
+ this.focus();
+ this.show();
+ this.select(0);
+ if (label) this.setLabel(label);
+ this.screen.render();
+ this.once('action', function(el, selected) {
+ if (label) self.removeLabel();
+ self.screen.restoreFocus();
+ self.hide();
+ self.screen.render();
+ if (!el) return callback();
+ return callback(null, helpers.cleanTags(self.ritems[selected]));
+ });
+};
+
+List.prototype.enterSelected = function(i) {
+ if (i != null) this.select(i);
+ this.emit('action', this.items[this.selected], this.selected);
+ this.emit('select', this.items[this.selected], this.selected);
+};
+
+List.prototype.cancelSelected = function(i) {
+ if (i != null) this.select(i);
+ this.emit('action');
+ this.emit('cancel');
+};
+
+module.exports = List;
diff --git a/lib/widgets/listbar.js b/lib/widgets/listbar.js
new file mode 100644
index 0000000..af05cb5
--- /dev/null
+++ b/lib/widgets/listbar.js
@@ -0,0 +1,396 @@
+/**
+ * listbar.js - listbar element for blessed
+ * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
+ * https://github.com/chjj/blessed
+ */
+
+/**
+ * Modules
+ */
+
+var helpers = require('../helpers');
+
+var Node = require('./node');
+var Box = require('./box');
+
+/**
+ * Listbar / HorizontalList
+ */
+
+function Listbar(options) {
+ var self = this;
+
+ if (!(this instanceof Node)) {
+ return new Listbar(options);
+ }
+
+ options = options || {};
+
+ this.items = [];
+ this.ritems = [];
+ this.commands = [];
+
+ this.leftBase = 0;
+ this.leftOffset = 0;
+
+ this.mouse = options.mouse || false;
+
+ Box.call(this, options);
+
+ if (!this.style.selected) {
+ this.style.selected = {};
+ }
+
+ if (!this.style.item) {
+ this.style.item = {};
+ }
+
+ if (options.commands || options.items) {
+ this.setItems(options.commands || options.items);
+ }
+
+ if (options.keys) {
+ this.on('keypress', function(ch, key) {
+ if (key.name === 'left'
+ || (options.vi && key.name === 'h')
+ || (key.shift && key.name === 'tab')) {
+ self.moveLeft();
+ self.screen.render();
+ // Stop propagation if we're in a form.
+ if (key.name === 'tab') return false;
+ return;
+ }
+ if (key.name === 'right'
+ || (options.vi && key.name === 'l')
+ || key.name === 'tab') {
+ self.moveRight();
+ self.screen.render();
+ // Stop propagation if we're in a form.
+ if (key.name === 'tab') return false;
+ return;
+ }
+ if (key.name === 'enter'
+ || (options.vi && key.name === 'k' && !key.shift)) {
+ self.emit('action', self.items[self.selected], self.selected);
+ self.emit('select', self.items[self.selected], self.selected);
+ var item = self.items[self.selected];
+ if (item._.cmd.callback) {
+ item._.cmd.callback();
+ }
+ self.screen.render();
+ return;
+ }
+ if (key.name === 'escape' || (options.vi && key.name === 'q')) {
+ self.emit('action');
+ self.emit('cancel');
+ return;
+ }
+ });
+ }
+
+ if (options.autoCommandKeys) {
+ this.onScreenEvent('keypress', function(ch, key) {
+ if (/^[0-9]$/.test(ch)) {
+ var i = +ch - 1;
+ if (!~i) i = 9;
+ return self.selectTab(i);
+ }
+ });
+ }
+
+ this.on('focus', function() {
+ self.select(self.selected);
+ });
+}
+
+Listbar.prototype.__proto__ = Box.prototype;
+
+Listbar.prototype.type = 'listbar';
+
+Listbar.prototype.__defineGetter__('selected', function() {
+ return this.leftBase + this.leftOffset;
+});
+
+Listbar.prototype.setItems = function(commands) {
+ var self = this;
+
+ if (!Array.isArray(commands)) {
+ commands = Object.keys(commands).reduce(function(obj, key, i) {
+ var cmd = commands[key]
+ , cb;
+
+ if (typeof cmd === 'function') {
+ cb = cmd;
+ cmd = { callback: cb };
+ }
+
+ if (cmd.text == null) cmd.text = key;
+ if (cmd.prefix == null) cmd.prefix = ++i + '';
+
+ if (cmd.text == null && cmd.callback) {
+ cmd.text = cmd.callback.name;
+ }
+
+ obj.push(cmd);
+
+ return obj;
+ }, []);
+ }
+
+ this.items.forEach(function(el) {
+ el.detach();
+ });
+
+ this.items = [];
+ this.ritems = [];
+ this.commands = [];
+
+ commands.forEach(function(cmd) {
+ self.add(cmd);
+ });
+
+ this.emit('set items');
+};
+
+Listbar.prototype.add =
+Listbar.prototype.addItem =
+Listbar.prototype.appendItem = function(item, callback) {
+ var self = this
+ , prev = this.items[this.items.length - 1]
+ , drawn = prev ? prev.aleft + prev.width : 0
+ , cmd
+ , title
+ , len;
+
+ if (!this.screen.autoPadding) {
+ drawn += this.ileft;
+ }
+
+ if (typeof item === 'object') {
+ cmd = item;
+ if (cmd.prefix == null) cmd.prefix = (this.items.length + 1) + '';
+ }
+
+ if (typeof item === 'string') {
+ cmd = {
+ prefix: (this.items.length + 1) + '',
+ text: item,
+ callback: callback
+ };
+ }
+
+ if (typeof item === 'function') {
+ cmd = {
+ prefix: (this.items.length + 1) + '',
+ text: item.name,
+ callback: item
+ };
+ }
+
+ if (cmd.keys && cmd.keys[0]) {
+ cmd.prefix = cmd.keys[0];
+ }
+
+ var t = helpers.generateTags(this.style.prefix || { fg: 'lightblack' });
+
+ title = (cmd.prefix != null ? t.open + cmd.prefix + t.close + ':' : '') + cmd.text;
+
+ len = ((cmd.prefix != null ? cmd.prefix + ':' : '') + cmd.text).length;
+
+ var options = {
+ screen: this.screen,
+ top: 0,
+ left: drawn + 1,
+ height: 1,
+ content: title,
+ width: len + 2,
+ align: 'center',
+ autoFocus: false,
+ tags: true,
+ mouse: true,
+ style: helpers.merge({}, this.style.item),
+ noOverflow: true
+ };
+
+ if (!this.screen.autoPadding) {
+ options.top += this.itop;
+ options.left += this.ileft;
+ }
+
+ ['bg', 'fg', 'bold', 'underline',
+ 'blink', 'inverse', 'invisible'].forEach(function(name) {
+ options.style[name] = function() {
+ var attr = self.items[self.selected] === el
+ ? self.style.selected[name]
+ : self.style.item[name];
+ if (typeof attr === 'function') attr = attr(el);
+ return attr;
+ };
+ });
+
+ var el = new Box(options);
+
+ this._[cmd.text] = el;
+ cmd.element = el;
+ el._.cmd = cmd;
+
+ this.ritems.push(cmd.text);
+ this.items.push(el);
+ this.commands.push(cmd);
+ this.append(el);
+
+ if (cmd.callback) {
+ if (cmd.keys) {
+ this.screen.key(cmd.keys, function(ch, key) {
+ self.emit('action', el, self.selected);
+ self.emit('select', el, self.selected);
+ if (el._.cmd.callback) {
+ el._.cmd.callback();
+ }
+ self.select(el);
+ self.screen.render();
+ });
+ }
+ }
+
+ if (this.items.length === 1) {
+ this.select(0);
+ }
+
+ if (this.mouse) {
+ el.on('click', function(data) {
+ self.emit('action', el, self.selected);
+ self.emit('select', el, self.selected);
+ if (el._.cmd.callback) {
+ el._.cmd.callback();
+ }
+ self.select(el);
+ self.screen.render();
+ });
+ }
+
+ this.emit('add item');
+};
+
+Listbar.prototype.render = function() {
+ var self = this
+ , drawn = 0;
+
+ if (!this.screen.autoPadding) {
+ drawn += this.ileft;
+ }
+
+ this.items.forEach(function(el, i) {
+ if (i < self.leftBase) {
+ el.hide();
+ } else {
+ el.rleft = drawn + 1;
+ drawn += el.width + 2;
+ el.show();
+ }
+ });
+
+ return this._render();
+};
+
+Listbar.prototype.select = function(offset) {
+ if (typeof offset !== 'number') {
+ offset = this.items.indexOf(offset);
+ }
+
+ var lpos = this._getCoords();
+ if (!lpos) return;
+
+ var self = this
+ , width = (lpos.xl - lpos.xi) - this.iwidth
+ , drawn = 0
+ , visible = 0
+ , el;
+
+ if (offset < 0) {
+ offset = 0;
+ } else if (offset >= this.items.length) {
+ offset = this.items.length - 1;
+ }
+
+ el = this.items[offset];
+ if (!el) return;
+
+ this.items.forEach(function(el, i) {
+ if (i < self.leftBase) return;
+
+ var lpos = el._getCoords();
+ if (!lpos) return;
+
+ if (lpos.xl - lpos.xi <= 0) return;
+
+ drawn += (lpos.xl - lpos.xi) + 2;
+
+ if (drawn <= width) visible++;
+ });
+
+ var diff = offset - (this.leftBase + this.leftOffset);
+ if (offset > this.leftBase + this.leftOffset) {
+ if (offset > this.leftBase + visible - 1) {
+ this.leftOffset = 0;
+ this.leftBase = offset;
+ } else {
+ this.leftOffset += diff;
+ }
+ } else if (offset < this.leftBase + this.leftOffset) {
+ diff = -diff;
+ if (offset < this.leftBase) {
+ this.leftOffset = 0;
+ this.leftBase = offset;
+ } else {
+ this.leftOffset -= diff;
+ }
+ }
+
+ // XXX Move `action` and `select` events here.
+ this.emit('select item', el, offset);
+};
+
+Listbar.prototype.removeItem = function(child) {
+ var i = typeof child !== 'number'
+ ? this.items.indexOf(child)
+ : child;
+
+ if (~i && this.items[i]) {
+ child = this.items.splice(i, 1)[0];
+ this.ritems.splice(i, 1);
+ this.commands.splice(i, 1);
+ this.remove(child);
+ if (i === this.selected) {
+ this.select(i - 1);
+ }
+ }
+
+ this.emit('remove item');
+};
+
+Listbar.prototype.move = function(offset) {
+ this.select(this.selected + offset);
+};
+
+Listbar.prototype.moveLeft = function(offset) {
+ this.move(-(offset || 1));
+};
+
+Listbar.prototype.moveRight = function(offset) {
+ this.move(offset || 1);
+};
+
+Listbar.prototype.selectTab = function(index) {
+ var item = this.items[index];
+ if (item) {
+ if (item._.cmd.callback) {
+ item._.cmd.callback();
+ }
+ this.select(index);
+ this.screen.render();
+ }
+ this.emit('select tab', item, index);
+};
+
+module.exports = Listbar;
diff --git a/lib/widgets/listtable.js b/lib/widgets/listtable.js
new file mode 100644
index 0000000..e680fee
--- /dev/null
+++ b/lib/widgets/listtable.js
@@ -0,0 +1,238 @@
+/**
+ * listtable.js - list table element for blessed
+ * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
+ * https://github.com/chjj/blessed
+ */
+
+/**
+ * Modules
+ */
+
+var helpers = require('../helpers');
+
+var Node = require('./node');
+var Box = require('./box');
+var List = require('./list');
+var Table = require('./table');
+
+/**
+ * ListTable
+ */
+
+function ListTable(options) {
+ var self = this;
+
+ if (!(this instanceof Node)) {
+ return new ListTable(options);
+ }
+
+ options = options || {};
+ options.shrink = true;
+ options.normalShrink = true;
+ options.style = options.style || {};
+ options.style.border = options.style.border || {};
+ options.style.header = options.style.header || {};
+ options.style.cell = options.style.cell || {};
+ this.__align = options.align || 'center';
+ delete options.align;
+
+ options.style.selected = options.style.cell.selected;
+ options.style.item = options.style.cell;
+
+ List.call(this, options);
+
+ this._header = new Box({
+ parent: this,
+ left: this.screen.autoPadding ? 0 : this.ileft,
+ top: 0,
+ width: 'shrink',
+ height: 1,
+ style: options.style.header,
+ tags: options.parseTags || options.tags
+ });
+
+ this.on('scroll', function() {
+ self._header.setFront();
+ var visible = self.height - self.iheight;
+ self._header.rtop = 1 + self.childBase - (self.border ? 1 : 0);
+ if (!self.screen.autoPadding) {
+ self._header.rtop = 1 + self.childBase;
+ }
+ });
+
+ this.pad = options.pad != null
+ ? options.pad
+ : 2;
+
+ this.setData(options.rows || options.data);
+
+ this.on('resize', function() {
+ var selected = self.selected;
+ self.setData(self.rows);
+ self.select(selected);
+ self.screen.render();
+ });
+}
+
+ListTable.prototype.__proto__ = List.prototype;
+
+ListTable.prototype.type = 'list-table';
+
+ListTable.prototype._calculateMaxes = Table.prototype._calculateMaxes;
+
+ListTable.prototype.setRows =
+ListTable.prototype.setData = function(rows) {
+ var self = this
+ , align = this.__align;
+
+ this.clearItems();
+
+ this.rows = rows || [];
+
+ this._calculateMaxes();
+
+ this.addItem('');
+
+ this.rows.forEach(function(row, i) {
+ var isHeader = i === 0;
+ var isFooter = i === self.rows.length - 1;
+ var text = '';
+ row.forEach(function(cell, i) {
+ var width = self._maxes[i];
+ var clen = self.strWidth(cell);
+
+ if (i !== 0) {
+ text += ' ';
+ }
+
+ while (clen < width) {
+ if (align === 'center') {
+ cell = ' ' + cell + ' ';
+ clen += 2;
+ } else if (align === 'left') {
+ cell = cell + ' ';
+ clen += 1;
+ } else if (align === 'right') {
+ cell = ' ' + cell;
+ clen += 1;
+ }
+ }
+
+ if (clen > width) {
+ if (align === 'center') {
+ cell = cell.substring(1);
+ clen--;
+ } else if (align === 'left') {
+ cell = cell.slice(0, -1);
+ clen--;
+ } else if (align === 'right') {
+ cell = cell.substring(1);
+ clen--;
+ }
+ }
+
+ text += cell;
+ });
+ if (isHeader) {
+ self._header.setContent(text);
+ } else {
+ self.addItem(text);
+ }
+ });
+
+ this._header.setFront();
+
+ this.select(0);
+};
+
+ListTable.prototype._select = ListTable.prototype.select;
+ListTable.prototype.select = function(i) {
+ if (i === 0) {
+ i = 1;
+ }
+ if (i <= this.childBase) {
+ this.setScroll(this.childBase - 1);
+ }
+ return this._select(i);
+};
+
+ListTable.prototype.render = function() {
+ var self = this;
+
+ var coords = this._render();
+ if (!coords) return;
+
+ this._calculateMaxes();
+
+ if (!this._maxes) return coords;
+
+ var lines = this.screen.lines
+ , xi = coords.xi
+ , xl = coords.xl
+ , yi = coords.yi
+ , yl = coords.yl
+ , rx
+ , ry
+ , i;
+
+ var battr = this.sattr(this.style.border);
+
+ var width = coords.xl - coords.xi - this.iright
+ , height = coords.yl - coords.yi - this.ibottom;
+
+ if (!this.border || this.options.noCellBorders) return coords;
+
+ // Draw border with correct angles.
+ ry = 0;
+ for (i = 0; i < height + 1; i++) {
+ if (!lines[yi + ry]) break;
+ rx = 0;
+ self._maxes.slice(0, -1).forEach(function(max, i) {
+ rx += max;
+ if (!lines[yi + ry][xi + rx + 1]) return;
+ // center
+ if (ry === 0) {
+ // top
+ lines[yi + ry][xi + ++rx][0] = battr;
+ lines[yi + ry][xi + rx][1] = '\u252c'; // '┬'
+ // XXX If we alter iheight and itop for no borders - nothing should be written here
+ if (!self.border.top) {
+ lines[yi + ry][xi + rx][1] = '\u2502'; // '│'
+ }
+ } else if (ry === height) {
+ // bottom
+ lines[yi + ry][xi + ++rx][0] = battr;
+ lines[yi + ry][xi + rx][1] = '\u2534'; // '┴'
+ // XXX If we alter iheight and ibottom for no borders - nothing should be written here
+ if (!self.border.bottom) {
+ lines[yi + ry][xi + rx][1] = '\u2502'; // '│'
+ }
+ } else {
+ // middle
+ ++rx;
+ }
+ });
+ ry += 1;
+ }
+
+ // Draw internal borders.
+ for (ry = 1; ry < height; ry++) {
+ if (!lines[yi + ry]) break;
+ rx = 0;
+ self._maxes.slice(0, -1).forEach(function(max, i) {
+ rx += max;
+ if (!lines[yi + ry][xi + rx + 1]) return;
+ if (self.options.fillCellBorders !== false) {
+ var lbg = lines[yi + ry][xi + rx][0] & 0x1ff;
+ lines[yi + ry][xi + ++rx][0] = (battr & ~0x1ff) | lbg;
+ } else {
+ lines[yi + ry][xi + ++rx][0] = battr;
+ }
+ lines[yi + ry][xi + rx][1] = '\u2502'; // '│'
+ });
+ }
+
+ return coords;
+};
+
+module.exports = ListTable;
diff --git a/lib/widgets/loading.js b/lib/widgets/loading.js
new file mode 100644
index 0000000..3f06894
--- /dev/null
+++ b/lib/widgets/loading.js
@@ -0,0 +1,88 @@
+/**
+ * loading.js - loading element for blessed
+ * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
+ * https://github.com/chjj/blessed
+ */
+
+/**
+ * Modules
+ */
+
+var helpers = require('../helpers');
+
+var Node = require('./node');
+var Box = require('./box');
+var Text = require('./text');
+
+/**
+ * Loading
+ */
+
+function Loading(options) {
+ var self = this;
+
+ if (!(this instanceof Node)) {
+ return new Loading(options);
+ }
+
+ options = options || {};
+
+ Box.call(this, options);
+
+ this._.icon = new Text({
+ parent: this,
+ align: 'center',
+ top: 2,
+ left: 1,
+ right: 1,
+ height: 1,
+ content: '|'
+ });
+}
+
+Loading.prototype.__proto__ = Box.prototype;
+
+Loading.prototype.type = 'loading';
+
+Loading.prototype.load = function(text) {
+ var self = this;
+
+ // XXX Keep above:
+ // var parent = this.parent;
+ // this.detach();
+ // parent.append(this);
+
+ this.show();
+ this.setContent(text);
+
+ if (this._.timer) {
+ this.stop();
+ }
+
+ this.screen.lockKeys = true;
+
+ this._.timer = setInterval(function() {
+ if (self._.icon.content === '|') {
+ self._.icon.setContent('/');
+ } else if (self._.icon.content === '/') {
+ self._.icon.setContent('-');
+ } else if (self._.icon.content === '-') {
+ self._.icon.setContent('\\');
+ } else if (self._.icon.content === '\\') {
+ self._.icon.setContent('|');
+ }
+ self.screen.render();
+ }, 200);
+};
+
+Loading.prototype.stop = function() {
+ this.screen.lockKeys = false;
+ this.hide();
+ if (this._.timer) {
+ clearInterval(this._.timer);
+ delete this._.timer;
+ }
+ this.screen.render();
+};
+
+module.exports = Loading;
diff --git a/lib/widgets/log.js b/lib/widgets/log.js
new file mode 100644
index 0000000..e700c3d
--- /dev/null
+++ b/lib/widgets/log.js
@@ -0,0 +1,81 @@
+/**
+ * log.js - log element for blessed
+ * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
+ * https://github.com/chjj/blessed
+ */
+
+/**
+ * Modules
+ */
+
+var util = require('util');
+
+var nextTick = global.setImmediate || process.nextTick.bind(process);
+
+var helpers = require('../helpers');
+
+var Node = require('./node');
+var ScrollableText = require('./scrollabletext');
+
+/**
+ * Log
+ */
+
+function Log(options) {
+ var self = this;
+
+ if (!(this instanceof Node)) {
+ return new Log(options);
+ }
+
+ options = options || {};
+
+ ScrollableText.call(this, options);
+
+ this.scrollback = options.scrollback != null
+ ? options.scrollback
+ : Infinity;
+ this.scrollOnInput = options.scrollOnInput;
+
+ this.on('set content', function() {
+ if (!self._userScrolled || self.scrollOnInput) {
+ nextTick(function() {
+ self.setScrollPerc(100);
+ self._userScrolled = false;
+ self.screen.render();
+ });
+ }
+ });
+}
+
+Log.prototype.__proto__ = ScrollableText.prototype;
+
+Log.prototype.type = 'log';
+
+Log.prototype.log =
+Log.prototype.add = function() {
+ var args = Array.prototype.slice.call(arguments);
+ if (typeof args[0] === 'object') {
+ args[0] = util.inspect(args[0], true, 20, true);
+ }
+ var text = util.format.apply(util, args);
+ this.emit('log', text);
+ var ret = this.pushLine(text);
+ if (this._clines.fake.length > this.scrollback) {
+ this.shiftLine(0, (this.scrollback / 3) | 0);
+ }
+ return ret;
+};
+
+Log.prototype._scroll = Log.prototype.scroll;
+Log.prototype.scroll = function(offset, always) {
+ if (offset === 0) return this._scroll(offset, always);
+ this._userScrolled = true;
+ var ret = this._scroll(offset, always);
+ if (this.getScrollPerc() === 100) {
+ this._userScrolled = false;
+ }
+ return ret;
+};
+
+module.exports = Log;
diff --git a/lib/widgets/message.js b/lib/widgets/message.js
new file mode 100644
index 0000000..2e70425
--- /dev/null
+++ b/lib/widgets/message.js
@@ -0,0 +1,122 @@
+/**
+ * message.js - message element for blessed
+ * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
+ * https://github.com/chjj/blessed
+ */
+
+/**
+ * Modules
+ */
+
+var helpers = require('../helpers');
+
+var Node = require('./node');
+var Box = require('./box');
+
+/**
+ * Message / Error
+ */
+
+function Message(options) {
+ var self = this;
+
+ if (!(this instanceof Node)) {
+ return new Message(options);
+ }
+
+ options = options || {};
+ options.tags = true;
+
+ Box.call(this, options);
+}
+
+Message.prototype.__proto__ = Box.prototype;
+
+Message.prototype.type = 'message';
+
+Message.prototype.log =
+Message.prototype.display = function(text, time, callback) {
+ var self = this;
+
+ if (typeof time === 'function') {
+ callback = time;
+ time = null;
+ }
+
+ if (time == null) time = 3;
+
+ // Keep above:
+ // var parent = this.parent;
+ // this.detach();
+ // parent.append(this);
+
+ if (this.scrollable) {
+ this.screen.saveFocus();
+ this.focus();
+ this.scrollTo(0);
+ }
+
+ this.show();
+ this.setContent(text);
+ this.screen.render();
+
+ if (time === Infinity || time === -1 || time === 0) {
+ var end = function() {
+ if (end.done) return;
+ end.done = true;
+ if (self.scrollable) {
+ try {
+ self.screen.restoreFocus();
+ } catch (e) {
+ ;
+ }
+ }
+ self.hide();
+ self.screen.render();
+ if (callback) callback();
+ };
+
+ setTimeout(function() {
+ self.onScreenEvent('keypress', function fn(ch, key) {
+ if (key.name === 'mouse') return;
+ if (self.scrollable) {
+ if ((key.name === 'up' || (self.options.vi && key.name === 'k'))
+ || (key.name === 'down' || (self.options.vi && key.name === 'j'))
+ || (self.options.vi && key.name === 'u' && key.ctrl)
+ || (self.options.vi && key.name === 'd' && key.ctrl)
+ || (self.options.vi && key.name === 'b' && key.ctrl)
+ || (self.options.vi && key.name === 'f' && key.ctrl)
+ || (self.options.vi && key.name === 'g' && !key.shift)
+ || (self.options.vi && key.name === 'g' && key.shift)) {
+ return;
+ }
+ }
+ if (self.options.ignoreKeys && ~self.options.ignoreKeys.indexOf(key.name)) {
+ return;
+ }
+ self.removeScreenEvent('keypress', fn);
+ end();
+ });
+ if (!self.options.mouse) return;
+ self.onScreenEvent('mouse', function fn(data) {
+ if (data.action === 'mousemove') return;
+ self.removeScreenEvent('mouse', fn);
+ end();
+ });
+ }, 10);
+
+ return;
+ }
+
+ setTimeout(function() {
+ self.hide();
+ self.screen.render();
+ if (callback) callback();
+ }, time * 1000);
+};
+
+Message.prototype.error = function(text, time, callback) {
+ return this.display('{red-fg}Error: ' + text + '{/red-fg}', time, callback);
+};
+
+module.exports = Message;
diff --git a/lib/widgets/node.js b/lib/widgets/node.js
new file mode 100644
index 0000000..b3ebf88
--- /dev/null
+++ b/lib/widgets/node.js
@@ -0,0 +1,231 @@
+/**
+ * node.js - base abstract node for blessed
+ * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
+ * https://github.com/chjj/blessed
+ */
+
+/**
+ * Modules
+ */
+
+var EventEmitter = require('../events').EventEmitter;
+
+var helpers = require('../helpers');
+
+/**
+ * Node
+ */
+
+function Node(options) {
+ if (!(this instanceof Node)) {
+ return new Node(options);
+ }
+
+ EventEmitter.call(this);
+
+ options = options || {};
+ this.options = options;
+ this.screen = this.screen
+ || options.screen
+ || require('./screen').global
+ || (function(){throw new Error('No active screen.')})();
+ this.parent = options.parent || null;
+ this.children = [];
+ this.$ = this._ = this.data = {};
+ this.uid = Node.uid++;
+ this.index = -1;
+
+ if (this.type !== 'screen') {
+ this.detached = true;
+ }
+
+ if (this.parent) {
+ this.parent.append(this);
+ }
+
+ (options.children || []).forEach(this.append.bind(this));
+}
+
+Node.uid = 0;
+
+Node.prototype.__proto__ = EventEmitter.prototype;
+
+Node.prototype.type = 'node';
+
+Node.prototype.insert = function(element, i) {
+ var self = this;
+
+ element.detach();
+ element.parent = this;
+
+ if (i === 0) {
+ this.children.unshift(element);
+ } else if (i === this.children.length) {
+ this.children.push(element);
+ } else {
+ this.children.splice(i, 0, element);
+ }
+
+ element.emit('reparent', this);
+ this.emit('adopt', element);
+
+ (function emit(el) {
+ var n = el.detached !== self.detached;
+ el.detached = self.detached;
+ if (n) el.emit('attach');
+ el.children.forEach(emit);
+ })(element);
+
+ if (!this.screen.focused) {
+ this.screen.focused = element;
+ }
+};
+
+Node.prototype.prepend = function(element) {
+ this.insert(element, 0);
+};
+
+Node.prototype.append = function(element) {
+ this.insert(element, this.children.length);
+};
+
+Node.prototype.insertBefore = function(element, other) {
+ var i = this.children.indexOf(other);
+ if (~i) this.insert(element, i);
+};
+
+Node.prototype.insertAfter = function(element, other) {
+ var i = this.children.indexOf(other);
+ if (~i) this.insert(element, i + 1);
+};
+
+Node.prototype.remove = function(element) {
+ if (element.parent !== this) return;
+
+ var i = this.children.indexOf(element);
+ if (!~i) return;
+
+ element.clearPos();
+
+ element.parent = null;
+
+ this.children.splice(i, 1);
+
+ i = this.screen.clickable.indexOf(element);
+ if (~i) this.screen.clickable.splice(i, 1);
+ i = this.screen.keyable.indexOf(element);
+ if (~i) this.screen.keyable.splice(i, 1);
+
+ element.emit('reparent', null);
+ this.emit('remove', element);
+
+ (function emit(el) {
+ var n = el.detached !== true;
+ el.detached = true;
+ if (n) el.emit('detach');
+ el.children.forEach(emit);
+ })(element);
+
+ if (this.screen.focused === element) {
+ this.screen.rewindFocus();
+ }
+};
+
+Node.prototype.detach = function() {
+ if (this.parent) this.parent.remove(this);
+};
+
+Node.prototype.forDescendants = function(iter, s) {
+ if (s) iter(this);
+ this.children.forEach(function emit(el) {
+ iter(el);
+ el.children.forEach(emit);
+ });
+};
+
+Node.prototype.forAncestors = function(iter, s) {
+ var el = this;
+ if (s) iter(this);
+ while (el = el.parent) {
+ iter(el);
+ }
+};
+
+Node.prototype.collectDescendants = function(s) {
+ var out = [];
+ this.forDescendants(function(el) {
+ out.push(el);
+ }, s);
+ return out;
+};
+
+Node.prototype.collectAncestors = function(s) {
+ var out = [];
+ this.forAncestors(function(el) {
+ out.push(el);
+ }, s);
+ return out;
+};
+
+Node.prototype.emitDescendants = function() {
+ var args = Array.prototype.slice(arguments)
+ , iter;
+
+ if (typeof args[args.length - 1] === 'function') {
+ iter = args.pop();
+ }
+
+ return this.forDescendants(function(el) {
+ if (iter) iter(el);
+ el.emit.apply(el, args);
+ }, true);
+};
+
+Node.prototype.emitAncestors = function() {
+ var args = Array.prototype.slice(arguments)
+ , iter;
+
+ if (typeof args[args.length - 1] === 'function') {
+ iter = args.pop();
+ }
+
+ return this.forAncestors(function(el) {
+ if (iter) iter(el);
+ el.emit.apply(el, args);
+ }, true);
+};
+
+Node.prototype.hasDescendant = function(target) {
+ return (function find(el) {
+ for (var i = 0; i < el.children.length; i++) {
+ if (el.children[i] === target) {
+ return true;
+ }
+ if (find(el.children[i]) === true) {
+ return true;
+ }
+ }
+ return false;
+ })(this);
+};
+
+Node.prototype.hasAncestor = function(target) {
+ var el = this;
+ while (el = el.parent) {
+ if (el === target) return true;
+ }
+ return false;
+};
+
+Node.prototype.get = function(name, value) {
+ if (this.data.hasOwnProperty(name)) {
+ return this.data[name];
+ }
+ return value;
+};
+
+Node.prototype.set = function(name, value) {
+ return this.data[name] = value;
+};
+
+module.exports = Node;
diff --git a/lib/widgets/progressbar.js b/lib/widgets/progressbar.js
new file mode 100644
index 0000000..5e12431
--- /dev/null
+++ b/lib/widgets/progressbar.js
@@ -0,0 +1,155 @@
+/**
+ * progressbar.js - progress bar element for blessed
+ * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
+ * https://github.com/chjj/blessed
+ */
+
+/**
+ * Modules
+ */
+
+var helpers = require('../helpers');
+
+var Node = require('./node');
+var Input = require('./input');
+
+/**
+ * ProgressBar
+ */
+
+function ProgressBar(options) {
+ var self = this;
+
+ if (!(this instanceof Node)) {
+ return new ProgressBar(options);
+ }
+
+ options = options || {};
+
+ Input.call(this, options);
+
+ this.filled = options.filled || 0;
+ if (typeof this.filled === 'string') {
+ this.filled = +this.filled.slice(0, -1);
+ }
+ this.value = this.filled;
+
+ this.pch = options.pch || ' ';
+
+ // XXX Workaround that predates the usage of `el.ch`.
+ if (options.ch) {
+ this.pch = options.ch;
+ this.ch = ' ';
+ }
+ if (options.bch) {
+ this.ch = options.bch;
+ }
+
+ if (!this.style.bar) {
+ this.style.bar = {};
+ this.style.bar.fg = options.barFg;
+ this.style.bar.bg = options.barBg;
+ }
+
+ this.orientation = options.orientation || 'horizontal';
+
+ if (options.keys) {
+ this.on('keypress', function(ch, key) {
+ var back, forward;
+ if (self.orientation === 'horizontal') {
+ back = ['left', 'h'];
+ forward = ['right', 'l'];
+ } else if (self.orientation === 'vertical') {
+ back = ['down', 'j'];
+ forward = ['up', 'k'];
+ }
+ if (key.name === back[0] || (options.vi && key.name === back[1])) {
+ self.progress(-5);
+ self.screen.render();
+ return;
+ }
+ if (key.name === forward[0] || (options.vi && key.name === forward[1])) {
+ self.progress(5);
+ self.screen.render();
+ return;
+ }
+ });
+ }
+
+ if (options.mouse) {
+ this.on('click', function(data) {
+ var x, y, m, p;
+ if (!self.lpos) return;
+ if (self.orientation === 'horizontal') {
+ x = data.x - self.lpos.xi;
+ m = (self.lpos.xl - self.lpos.xi) - self.iwidth;
+ p = x / m * 100 | 0;
+ } else if (self.orientation === 'vertical') {
+ y = data.y - self.lpos.yi;
+ m = (self.lpos.yl - self.lpos.yi) - self.iheight;
+ p = y / m * 100 | 0;
+ }
+ self.setProgress(p);
+ });
+ }
+}
+
+ProgressBar.prototype.__proto__ = Input.prototype;
+
+ProgressBar.prototype.type = 'progress-bar';
+
+ProgressBar.prototype.render = function() {
+ var ret = this._render();
+ if (!ret) return;
+
+ var xi = ret.xi
+ , xl = ret.xl
+ , yi = ret.yi
+ , yl = ret.yl
+ , dattr;
+
+ if (this.border) xi++, yi++, xl--, yl--;
+
+ if (this.orientation === 'horizontal') {
+ xl = xi + ((xl - xi) * (this.filled / 100)) | 0;
+ } else if (this.orientation === 'vertical') {
+ yi = yi + ((yl - yi) - (((yl - yi) * (this.filled / 100)) | 0));
+ }
+
+ dattr = this.sattr(this.style.bar);
+
+ this.screen.fillRegion(dattr, this.pch, xi, xl, yi, yl);
+
+ if (this.content) {
+ var line = this.screen.lines[yi];
+ for (var i = 0; i < this.content.length; i++) {
+ line[xi + i][1] = this.content[i];
+ }
+ line.dirty = true;
+ }
+
+ return ret;
+};
+
+ProgressBar.prototype.progress = function(filled) {
+ this.filled += filled;
+ if (this.filled < 0) this.filled = 0;
+ else if (this.filled > 100) this.filled = 100;
+ if (this.filled === 100) {
+ this.emit('complete');
+ }
+ this.value = this.filled;
+};
+
+ProgressBar.prototype.setProgress = function(filled) {
+ this.filled = 0;
+ this.progress(filled);
+};
+
+ProgressBar.prototype.reset = function() {
+ this.emit('reset');
+ this.filled = 0;
+ this.value = this.filled;
+};
+
+module.exports = ProgressBar;
diff --git a/lib/widgets/prompt.js b/lib/widgets/prompt.js
new file mode 100644
index 0000000..04b05fb
--- /dev/null
+++ b/lib/widgets/prompt.js
@@ -0,0 +1,120 @@
+/**
+ * prompt.js - prompt element for blessed
+ * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
+ * https://github.com/chjj/blessed
+ */
+
+/**
+ * Modules
+ */
+
+var helpers = require('../helpers');
+
+var Node = require('./node');
+var Box = require('./box');
+var Button = require('./button');
+var Textbox = require('./textbox');
+
+/**
+ * Prompt
+ */
+
+function Prompt(options) {
+ var self = this;
+
+ if (!(this instanceof Node)) {
+ return new Prompt(options);
+ }
+
+ options = options || {};
+
+ options.hidden = true;
+
+ Box.call(this, options);
+
+ this._.input = new Textbox({
+ parent: this,
+ top: 3,
+ height: 1,
+ left: 2,
+ right: 2,
+ bg: 'black'
+ });
+
+ this._.okay = new Button({
+ parent: this,
+ top: 5,
+ height: 1,
+ left: 2,
+ width: 6,
+ content: 'Okay',
+ align: 'center',
+ bg: 'black',
+ hoverBg: 'blue',
+ autoFocus: false,
+ mouse: true
+ });
+
+ this._.cancel = new Button({
+ parent: this,
+ top: 5,
+ height: 1,
+ shrink: true,
+ left: 10,
+ width: 8,
+ content: 'Cancel',
+ align: 'center',
+ bg: 'black',
+ hoverBg: 'blue',
+ autoFocus: false,
+ mouse: true
+ });
+}
+
+Prompt.prototype.__proto__ = Box.prototype;
+
+Prompt.prototype.type = 'prompt';
+
+Prompt.prototype.input =
+Prompt.prototype.setInput =
+Prompt.prototype.readInput = function(text, value, callback) {
+ var self = this;
+ var okay, cancel;
+
+ if (!callback) {
+ callback = value;
+ value = '';
+ }
+
+ // Keep above:
+ // var parent = this.parent;
+ // this.detach();
+ // parent.append(this);
+
+ this.show();
+ this.setContent(' ' + text);
+
+ this._.input.value = value;
+
+ this.screen.saveFocus();
+
+ this._.okay.on('press', okay = function() {
+ self._.input.submit();
+ });
+
+ this._.cancel.on('press', cancel = function() {
+ self._.input.cancel();
+ });
+
+ this._.input.readInput(function(err, data) {
+ self.hide();
+ self.screen.restoreFocus();
+ self._.okay.removeListener('press', okay);
+ self._.cancel.removeListener('press', cancel);
+ return callback(err, data);
+ });
+
+ this.screen.render();
+};
+
+module.exports = Prompt;
diff --git a/lib/widgets/question.js b/lib/widgets/question.js
new file mode 100644
index 0000000..7dd96fc
--- /dev/null
+++ b/lib/widgets/question.js
@@ -0,0 +1,116 @@
+/**
+ * question.js - question element for blessed
+ * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
+ * https://github.com/chjj/blessed
+ */
+
+/**
+ * Modules
+ */
+
+var helpers = require('../helpers');
+
+var Node = require('./node');
+var Box = require('./box');
+var Button = require('./button');
+
+/**
+ * Question
+ */
+
+function Question(options) {
+ var self = this;
+
+ if (!(this instanceof Node)) {
+ return new Question(options);
+ }
+
+ options = options || {};
+ options.hidden = true;
+
+ Box.call(this, options);
+
+ this._.okay = new Button({
+ screen: this.screen,
+ parent: this,
+ top: 2,
+ height: 1,
+ left: 2,
+ width: 6,
+ content: 'Okay',
+ align: 'center',
+ bg: 'black',
+ hoverBg: 'blue',
+ autoFocus: false,
+ mouse: true
+ });
+
+ this._.cancel = new Button({
+ screen: this.screen,
+ parent: this,
+ top: 2,
+ height: 1,
+ shrink: true,
+ left: 10,
+ width: 8,
+ content: 'Cancel',
+ align: 'center',
+ bg: 'black',
+ hoverBg: 'blue',
+ autoFocus: false,
+ mouse: true
+ });
+}
+
+Question.prototype.__proto__ = Box.prototype;
+
+Question.prototype.type = 'question';
+
+Question.prototype.ask = function(text, callback) {
+ var self = this;
+ var press, okay, cancel;
+
+ // Keep above:
+ // var parent = this.parent;
+ // this.detach();
+ // parent.append(this);
+
+ this.show();
+ this.setContent(' ' + text);
+
+ this.onScreenEvent('keypress', press = function(ch, key) {
+ if (key.name === 'mouse') return;
+ if (key.name !== 'enter'
+ && key.name !== 'escape'
+ && key.name !== 'q'
+ && key.name !== 'y'
+ && key.name !== 'n') {
+ return;
+ }
+ done(null, key.name === 'enter' || key.name === 'y');
+ });
+
+ this._.okay.on('press', okay = function() {
+ done(null, true);
+ });
+
+ this._.cancel.on('press', cancel = function() {
+ done(null, false);
+ });
+
+ this.screen.saveFocus();
+ this.focus();
+
+ function done(err, data) {
+ self.hide();
+ self.screen.restoreFocus();
+ self.removeScreenEvent('keypress', press);
+ self._.okay.removeListener('press', okay);
+ self._.cancel.removeListener('press', cancel);
+ return callback(err, data);
+ }
+
+ this.screen.render();
+};
+
+module.exports = Question;
diff --git a/lib/widgets/radiobutton.js b/lib/widgets/radiobutton.js
new file mode 100644
index 0000000..b62e93f
--- /dev/null
+++ b/lib/widgets/radiobutton.js
@@ -0,0 +1,59 @@
+/**
+ * radiobutton.js - radio button element for blessed
+ * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
+ * https://github.com/chjj/blessed
+ */
+
+/**
+ * Modules
+ */
+
+var helpers = require('../helpers');
+
+var Node = require('./node');
+var Checkbox = require('./checkbox');
+
+/**
+ * RadioButton
+ */
+
+function RadioButton(options) {
+ var self = this;
+
+ if (!(this instanceof Node)) {
+ return new RadioButton(options);
+ }
+
+ options = options || {};
+
+ Checkbox.call(this, options);
+
+ this.on('check', function() {
+ var el = self;
+ while (el = el.parent) {
+ if (el.type === 'radio-set'
+ || el.type === 'form') break;
+ }
+ el = el || self.parent;
+ el.forDescendants(function(el) {
+ if (el.type !== 'radio-button' || el === self) {
+ return;
+ }
+ el.uncheck();
+ });
+ });
+}
+
+RadioButton.prototype.__proto__ = Checkbox.prototype;
+
+RadioButton.prototype.type = 'radio-button';
+
+RadioButton.prototype.render = function() {
+ this.clearPos(true);
+ this.setContent('(' + (this.checked ? '*' : ' ') + ') ' + this.text, true);
+ return this._render();
+};
+
+RadioButton.prototype.toggle = RadioButton.prototype.check;
+
+module.exports = RadioButton;
diff --git a/lib/widgets/radioset.js b/lib/widgets/radioset.js
new file mode 100644
index 0000000..0461d67
--- /dev/null
+++ b/lib/widgets/radioset.js
@@ -0,0 +1,34 @@
+/**
+ * radioset.js - radio set element for blessed
+ * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
+ * https://github.com/chjj/blessed
+ */
+
+/**
+ * Modules
+ */
+
+var helpers = require('../helpers');
+
+var Node = require('./node');
+var Box = require('./box');
+
+/**
+ * RadioSet
+ */
+
+function RadioSet(options) {
+ if (!(this instanceof Node)) {
+ return new RadioSet(options);
+ }
+ options = options || {};
+ // Possibly inherit parent's style.
+ // options.style = this.parent.style;
+ Box.call(this, options);
+}
+
+RadioSet.prototype.__proto__ = Box.prototype;
+
+RadioSet.prototype.type = 'radio-set';
+
+module.exports = RadioSet;
diff --git a/lib/widgets/screen.js b/lib/widgets/screen.js
new file mode 100644
index 0000000..906dc56
--- /dev/null
+++ b/lib/widgets/screen.js
@@ -0,0 +1,2154 @@
+/**
+ * screen.js - screen node for blessed
+ * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
+ * https://github.com/chjj/blessed
+ */
+
+/**
+ * Modules
+ */
+
+var path = require('path')
+ , fs = require('fs')
+ , cp = require('child_process');
+
+var colors = require('../colors')
+ , program = require('../program')
+ , unicode = require('../unicode');
+
+var nextTick = global.setImmediate || process.nextTick.bind(process);
+
+var helpers = require('../helpers');
+
+var Node = require('./node');
+var Log = require('./log');
+var Box = require('./box');
+
+/**
+ * Screen
+ */
+
+function Screen(options) {
+ var self = this;
+
+ if (!(this instanceof Node)) {
+ return new Screen(options);
+ }
+
+ options = options || {};
+ if (options.rsety && options.listen) {
+ options = { program: options };
+ }
+
+ this.program = options.program || program.global;
+
+ if (!this.program) {
+ this.program = program({
+ input: options.input,
+ output: options.output,
+ log: options.log,
+ debug: options.debug,
+ dump: options.dump,
+ term: options.term,
+ resizeTimeout: options.resizeTimeout,
+ tput: true,
+ buffer: true,
+ zero: true
+ });
+ } else {
+ this.program.setupTput();
+ this.program.useBuffer = true;
+ this.program.zero = true;
+ this.program.options.resizeTimeout = options.resizeTimeout;
+ }
+
+ this.tput = this.program.tput;
+
+ if (!Screen.global) {
+ Screen.global = this;
+ }
+
+ Node.call(this, options);
+
+ this.autoPadding = options.autoPadding !== false;
+ this.tabc = Array((options.tabSize || 4) + 1).join(' ');
+ this.dockBorders = options.dockBorders;
+
+ this.ignoreLocked = options.ignoreLocked || [];
+
+ this._unicode = this.tput.unicode || this.tput.numbers.U8 === 1;
+ this.fullUnicode = this.options.fullUnicode && this._unicode;
+
+ this.dattr = ((0 << 18) | (0x1ff << 9)) | 0x1ff;
+
+ this.renders = 0;
+ this.position = {
+ left: this.left = this.aleft = this.rleft = 0,
+ right: this.right = this.aright = this.rright = 0,
+ top: this.top = this.atop = this.rtop = 0,
+ bottom: this.bottom = this.abottom = this.rbottom = 0,
+ get height() { return self.height; },
+ get width() { return self.width; }
+ };
+
+ this.ileft = 0;
+ this.itop = 0;
+ this.iright = 0;
+ this.ibottom = 0;
+ this.iheight = 0;
+ this.iwidth = 0;
+
+ this.padding = {
+ left: 0,
+ top: 0,
+ right: 0,
+ bottom: 0
+ };
+
+ this.hover = null;
+ this.history = [];
+ this.clickable = [];
+ this.keyable = [];
+ this.grabKeys = false;
+ this.lockKeys = false;
+ this.focused;
+ this._buf = '';
+
+ this._ci = -1;
+
+ if (options.title) {
+ this.title = options.title;
+ }
+
+ options.cursor = options.cursor || {
+ artificial: options.artificialCursor,
+ shape: options.cursorShape,
+ blink: options.cursorBlink,
+ color: options.cursorColor
+ };
+
+ this.cursor = {
+ artificial: options.cursor.artificial || false,
+ shape: options.cursor.shape || 'block',
+ blink: options.cursor.blink || false,
+ color: options.cursor.color || null,
+ _set: false,
+ _state: 1,
+ _hidden: true
+ };
+
+ this.program.on('resize', function() {
+ self.alloc();
+ self.render();
+ (function emit(el) {
+ el.emit('resize');
+ el.children.forEach(emit);
+ })(self);
+ });
+
+ this.program.on('focus', function() {
+ self.emit('focus');
+ });
+
+ this.program.on('blur', function() {
+ self.emit('blur');
+ });
+
+ this.on('newListener', function fn(type) {
+ if (type === 'keypress' || type.indexOf('key ') === 0 || type === 'mouse') {
+ if (type === 'keypress' || type.indexOf('key ') === 0) self._listenKeys();
+ if (type === 'mouse') self._listenMouse();
+ }
+ if (type === 'mouse'
+ || type === 'click'
+ || type === 'mouseover'
+ || type === 'mouseout'
+ || type === 'mousedown'
+ || type === 'mouseup'
+ || type === 'mousewheel'
+ || type === 'wheeldown'
+ || type === 'wheelup'
+ || type === 'mousemove') {
+ self._listenMouse();
+ }
+ });
+
+ this.setMaxListeners(Infinity);
+
+ Screen.total++;
+
+ process.on('uncaughtException', function(err) {
+ if (process.listeners('uncaughtException').length > Screen.total) {
+ return;
+ }
+ self.leave();
+ err = err || new Error('Uncaught Exception.');
+ console.error(err.stack ? err.stack + '' : err + '');
+ nextTick(function() {
+ process.exit(1);
+ });
+ });
+
+ ['SIGTERM', 'SIGINT', 'SIGQUIT'].forEach(function(signal) {
+ process.on(signal, function() {
+ if (process.listeners(signal).length > Screen.total) {
+ return;
+ }
+ nextTick(function() {
+ process.exit(0);
+ });
+ });
+ });
+
+ process.on('exit', function() {
+ self.leave();
+ });
+
+ this.enter();
+
+ this.postEnter();
+}
+
+Screen.global = null;
+
+Screen.total = 0;
+
+Screen.prototype.__proto__ = Node.prototype;
+
+Screen.prototype.type = 'screen';
+
+Screen.prototype.__defineGetter__('title', function() {
+ return this.program.title;
+});
+
+Screen.prototype.__defineSetter__('title', function(title) {
+ return this.program.title = title;
+});
+
+Screen.prototype.enter = function() {
+ if (this.program.isAlt) return;
+ if (!this.cursor._set) {
+ if (this.options.cursor.shape) {
+ this.cursorShape(this.cursor.shape, this.cursor.blink);
+ }
+ if (this.options.cursor.color) {
+ this.cursorColor(this.cursor.color);
+ }
+ }
+ if (process.platform === 'win32') {
+ try {
+ cp.execSync('cls', { stdio: 'ignore', timeout: 1000 });
+ } catch (e) {
+ ;
+ }
+ }
+ this.program.alternateBuffer();
+ this.program.put.keypad_xmit();
+ this.program.csr(0, this.height - 1);
+ this.program.hideCursor();
+ this.program.cup(0, 0);
+ this.alloc();
+};
+
+Screen.prototype.leave = function() {
+ if (!this.program.isAlt) return;
+ this.program.put.keypad_local();
+ if (this.program.scrollTop !== 0
+ || this.program.scrollBottom !== this.rows - 1) {
+ this.program.csr(0, this.height - 1);
+ }
+ // XXX For some reason if alloc/clear() is before this
+ // line, it doesn't work on linux console.
+ this.program.showCursor();
+ this.alloc();
+ if (this._listenedMouse) {
+ this.program.disableMouse();
+ }
+ this.program.normalBuffer();
+ if (this.cursor._set) this.cursorReset();
+ this.program.flush();
+ if (process.platform === 'win32') {
+ try {
+ cp.execSync('cls', { stdio: 'ignore', timeout: 1000 });
+ } catch (e) {
+ ;
+ }
+ }
+};
+
+Screen.prototype.postEnter = function() {
+ var self = this;
+ if (this.options.debug) {
+ this.debugLog = new Log({
+ parent: this,
+ hidden: true,
+ draggable: true,
+ left: 'center',
+ top: 'center',
+ width: '30%',
+ height: '30%',
+ border: 'line',
+ label: ' {bold}Debug Log{/bold} ',
+ tags: true,
+ keys: true,
+ vi: true,
+ mouse: true,
+ scrollbar: {
+ ch: ' ',
+ track: {
+ bg: 'yellow'
+ },
+ style: {
+ inverse: true
+ }
+ }
+ });
+
+ this.debugLog.toggle = function() {
+ if (self.debugLog.hidden) {
+ self.saveFocus();
+ self.debugLog.show();
+ self.debugLog.setFront();
+ self.debugLog.focus();
+ } else {
+ self.debugLog.hide();
+ self.restoreFocus();
+ }
+ self.render();
+ };
+
+ this.debugLog.key(['q', 'escape'], self.debugLog.toggle);
+ this.key('f12', self.debugLog.toggle);
+ }
+};
+
+Screen.prototype.log = function() {
+ if (this.debugLog) {
+ this.debugLog.log.apply(this.debugLog, arguments);
+ }
+ return this.program.log.apply(this.program, arguments);
+};
+
+Screen.prototype.debug = function() {
+ if (this.debugLog) {
+ this.debugLog.log.apply(this.debugLog, arguments);
+ }
+ return this.program.debug.apply(this.program, arguments);
+};
+
+Screen.prototype._listenMouse = function(el) {
+ var self = this;
+
+ if (el && !~this.clickable.indexOf(el)) {
+ el.clickable = true;
+ this.clickable.push(el);
+ }
+
+ if (this._listenedMouse) return;
+ this._listenedMouse = true;
+
+ this.program.enableMouse();
+
+ this.on('render', function() {
+ self._needsClickableSort = true;
+ });
+
+ this.program.on('mouse', function(data) {
+ if (self.lockKeys) return;
+
+ if (self._needsClickableSort) {
+ self.clickable = helpers.hsort(self.clickable);
+ self._needsClickableSort = false;
+ }
+
+ var i = 0
+ , el
+ , set
+ , pos;
+
+ for (; i < self.clickable.length; i++) {
+ el = self.clickable[i];
+
+ if (el.detached || !el.visible) {
+ continue;
+ }
+
+ // if (self.grabMouse && self.focused !== el
+ // && !el.hasAncestor(self.focused)) continue;
+
+ pos = el.lpos;
+ if (!pos) continue;
+
+ if (data.x >= pos.xi && data.x < pos.xl
+ && data.y >= pos.yi && data.y < pos.yl) {
+ el.emit('mouse', data);
+ if (data.action === 'mousedown') {
+ self.mouseDown = el;
+ } else if (data.action === 'mouseup') {
+ (self.mouseDown || el).emit('click', data);
+ self.mouseDown = null;
+ } else if (data.action === 'mousemove') {
+ if (self.hover && el.index > self.hover.index) {
+ set = false;
+ }
+ if (self.hover !== el && !set) {
+ if (self.hover) {
+ self.hover.emit('mouseout', data);
+ }
+ el.emit('mouseover', data);
+ self.hover = el;
+ }
+ set = true;
+ }
+ el.emit(data.action, data);
+ break;
+ }
+ }
+
+ // Just mouseover?
+ if ((data.action === 'mousemove'
+ || data.action === 'mousedown'
+ || data.action === 'mouseup')
+ && self.hover
+ && !set) {
+ self.hover.emit('mouseout', data);
+ self.hover = null;
+ }
+
+ self.emit('mouse', data);
+ self.emit(data.action, data);
+ });
+
+ // Autofocus highest element.
+ // this.on('element click', function(el, data) {
+ // var target;
+ // do {
+ // if (el.clickable === true && el.options.autoFocus !== false) {
+ // target = el;
+ // }
+ // } while (el = el.parent);
+ // if (target) target.focus();
+ // });
+
+ // Autofocus elements with the appropriate option.
+ this.on('element click', function(el, data) {
+ if (el.clickable === true && el.options.autoFocus !== false) {
+ el.focus();
+ }
+ });
+};
+
+Screen.prototype.enableMouse = function(el) {
+ this._listenMouse(el);
+};
+
+Screen.prototype._listenKeys = function(el) {
+ var self = this;
+
+ if (el && !~this.keyable.indexOf(el)) {
+ el.keyable = true;
+ this.keyable.push(el);
+ }
+
+ if (this._listenedKeys) return;
+ this._listenedKeys = true;
+
+ // NOTE: The event emissions used to be reversed:
+ // element + screen
+ // They are now:
+ // screen + element
+ // After the first keypress emitted, the handler
+ // checks to make sure grabKeys, lockKeys, and focused
+ // weren't changed, and handles those situations appropriately.
+ this.program.on('keypress', function(ch, key) {
+ if (self.lockKeys && !~self.ignoreLocked.indexOf(key.full)) {
+ return;
+ }
+
+ var focused = self.focused
+ , grabKeys = self.grabKeys;
+
+ if (!grabKeys) {
+ self.emit('keypress', ch, key);
+ self.emit('key ' + key.full, ch, key);
+ }
+
+ // If something changed from the screen key handler, stop.
+ if (self.grabKeys !== grabKeys || self.lockKeys) {
+ return;
+ }
+
+ if (focused && focused.keyable) {
+ focused.emit('keypress', ch, key);
+ focused.emit('key ' + key.full, ch, key);
+ }
+ });
+};
+
+Screen.prototype.enableKeys = function(el) {
+ this._listenKeys(el);
+};
+
+Screen.prototype.enableInput = function(el) {
+ this._listenMouse(el);
+ this._listenKeys(el);
+};
+
+Screen.prototype._initHover = function() {
+ var self = this;
+
+ if (this._hoverText) {
+ return;
+ }
+
+ this._hoverText = new Box({
+ screen: this,
+ left: 0,
+ top: 0,
+ tags: false,
+ height: 'shrink',
+ width: 'shrink',
+ border: 'line',
+ style: {
+ border: {
+ fg: 'default'
+ },
+ bg: 'default',
+ fg: 'default'
+ }
+ });
+
+ this.on('mousemove', function(data) {
+ if (self._hoverText.detached) return;
+ self._hoverText.rleft = data.x + 1;
+ self._hoverText.rtop = data.y;
+ self.render();
+ });
+
+ this.on('element mouseover', function(el, data) {
+ if (!el._hoverOptions) return;
+ self._hoverText.parseTags = el.parseTags;
+ self._hoverText.setContent(el._hoverOptions.text);
+ self.append(self._hoverText);
+ self._hoverText.rleft = data.x + 1;
+ self._hoverText.rtop = data.y;
+ self.render();
+ });
+
+ this.on('element mouseout', function() {
+ if (self._hoverText.detached) return;
+ self._hoverText.detach();
+ self.render();
+ });
+
+ this.on('element mouseup', function(el, data) {
+ if (!el._hoverOptions) return;
+ self.append(self._hoverText);
+ self.render();
+ });
+};
+
+Screen.prototype.__defineGetter__('cols', function() {
+ return this.program.cols;
+});
+
+Screen.prototype.__defineGetter__('rows', function() {
+ return this.program.rows;
+});
+
+Screen.prototype.__defineGetter__('width', function() {
+ return this.program.cols;
+});
+
+Screen.prototype.__defineGetter__('height', function() {
+ return this.program.rows;
+});
+
+Screen.prototype.alloc = function() {
+ var x, y;
+
+ this.lines = [];
+ for (y = 0; y < this.rows; y++) {
+ this.lines[y] = [];
+ for (x = 0; x < this.cols; x++) {
+ this.lines[y][x] = [this.dattr, ' '];
+ }
+ this.lines[y].dirty = false;
+ }
+
+ this.olines = [];
+ for (y = 0; y < this.rows; y++) {
+ this.olines[y] = [];
+ for (x = 0; x < this.cols; x++) {
+ this.olines[y][x] = [this.dattr, ' '];
+ }
+ }
+
+ this.program.clear();
+};
+
+Screen.prototype.render = function() {
+ var self = this;
+
+ this.emit('prerender');
+
+ this._borderStops = {};
+
+ // TODO: Possibly get rid of .dirty altogether.
+ // TODO: Could possibly drop .dirty and just clear the `lines` buffer every
+ // time before a screen.render. This way clearRegion doesn't have to be
+ // called in arbitrary places for the sake of clearing a spot where an
+ // element used to be (e.g. when an element moves or is hidden). There could
+ // be some overhead though.
+ // this.screen.clearRegion(0, this.cols, 0, this.rows);
+ this._ci = 0;
+ this.children.forEach(function(el) {
+ el.index = self._ci++;
+ //el._rendering = true;
+ el.render();
+ //el._rendering = false;
+ });
+ this._ci = -1;
+
+ if (this.screen.dockBorders) {
+ this._dockBorders();
+ }
+
+ this.draw(0, this.lines.length - 1);
+
+ // XXX Workaround to deal with cursor pos before the screen has rendered and
+ // lpos is not reliable (stale).
+ if (this.focused && this.focused._updateCursor) {
+ this.focused._updateCursor(true);
+ }
+
+ this.renders++;
+
+ this.emit('render');
+};
+
+Screen.prototype.blankLine = function(ch, dirty) {
+ var out = [];
+ for (var x = 0; x < this.cols; x++) {
+ out[x] = [this.dattr, ch || ' '];
+ }
+ out.dirty = dirty;
+ return out;
+};
+
+Screen.prototype.insertLine = function(n, y, top, bottom) {
+ // if (y === top) return this.insertLineNC(n, y, top, bottom);
+
+ if (!this.tput.strings.change_scroll_region
+ || !this.tput.strings.delete_line
+ || !this.tput.strings.insert_line) return;
+
+ this._buf += this.tput.csr(top, bottom);
+ this._buf += this.tput.cup(y, 0);
+ this._buf += this.tput.il(n);
+ this._buf += this.tput.csr(0, this.height - 1);
+
+ var j = bottom + 1;
+
+ while (n--) {
+ this.lines.splice(y, 0, this.blankLine());
+ this.lines.splice(j, 1);
+ this.olines.splice(y, 0, this.blankLine());
+ this.olines.splice(j, 1);
+ }
+};
+
+Screen.prototype.deleteLine = function(n, y, top, bottom) {
+ // if (y === top) return this.deleteLineNC(n, y, top, bottom);
+
+ if (!this.tput.strings.change_scroll_region
+ || !this.tput.strings.delete_line
+ || !this.tput.strings.insert_line) return;
+
+ this._buf += this.tput.csr(top, bottom);
+ this._buf += this.tput.cup(y, 0);
+ this._buf += this.tput.dl(n);
+ this._buf += this.tput.csr(0, this.height - 1);
+
+ var j = bottom + 1;
+
+ while (n--) {
+ this.lines.splice(j, 0, this.blankLine());
+ this.lines.splice(y, 1);
+ this.olines.splice(j, 0, this.blankLine());
+ this.olines.splice(y, 1);
+ }
+};
+
+// This is how ncurses does it.
+// Scroll down (up cursor-wise).
+// This will only work for top line deletion as opposed to arbitrary lines.
+Screen.prototype.insertLineNC = function(n, y, top, bottom) {
+ if (!this.tput.strings.change_scroll_region
+ || !this.tput.strings.delete_line) return;
+
+ this._buf += this.tput.csr(top, bottom);
+ this._buf += this.tput.cup(top, 0);
+ this._buf += this.tput.dl(n);
+ this._buf += this.tput.csr(0, this.height - 1);
+
+ var j = bottom + 1;
+
+ while (n--) {
+ this.lines.splice(j, 0, this.blankLine());
+ this.lines.splice(y, 1);
+ this.olines.splice(j, 0, this.blankLine());
+ this.olines.splice(y, 1);
+ }
+};
+
+// This is how ncurses does it.
+// Scroll up (down cursor-wise).
+// This will only work for bottom line deletion as opposed to arbitrary lines.
+Screen.prototype.deleteLineNC = function(n, y, top, bottom) {
+ if (!this.tput.strings.change_scroll_region
+ || !this.tput.strings.delete_line) return;
+
+ this._buf += this.tput.csr(top, bottom);
+ this._buf += this.tput.cup(bottom, 0);
+ this._buf += Array(n + 1).join('\n');
+ this._buf += this.tput.csr(0, this.height - 1);
+
+ var j = bottom + 1;
+
+ while (n--) {
+ this.lines.splice(j, 0, this.blankLine());
+ this.lines.splice(y, 1);
+ this.olines.splice(j, 0, this.blankLine());
+ this.olines.splice(y, 1);
+ }
+};
+
+Screen.prototype.insertBottom = function(top, bottom) {
+ return this.deleteLine(1, top, top, bottom);
+};
+
+Screen.prototype.insertTop = function(top, bottom) {
+ return this.insertLine(1, top, top, bottom);
+};
+
+Screen.prototype.deleteBottom = function(top, bottom) {
+ return this.clearRegion(0, this.width, bottom, bottom);
+};
+
+Screen.prototype.deleteTop = function(top, bottom) {
+ // Same as: return this.insertBottom(top, bottom);
+ return this.deleteLine(1, top, top, bottom);
+};
+
+// Parse the sides of an element to determine
+// whether an element has uniform cells on
+// both sides. If it does, we can use CSR to
+// optimize scrolling on a scrollable element.
+// Not exactly sure how worthwile this is.
+// This will cause a performance/cpu-usage hit,
+// but will it be less or greater than the
+// performance hit of slow-rendering scrollable
+// boxes with clean sides?
+Screen.prototype.cleanSides = function(el) {
+ var pos = el.lpos;
+
+ if (!pos) {
+ return false;
+ }
+
+ if (pos._cleanSides != null) {
+ return pos._cleanSides;
+ }
+
+ if (pos.xi <= 0 && pos.xl >= this.width) {
+ return pos._cleanSides = true;
+ }
+
+ if (this.options.fastCSR) {
+ // Maybe just do this instead of parsing.
+ if (pos.yi < 0) return pos._cleanSides = false;
+ if (pos.yl > this.height) return pos._cleanSides = false;
+ if (this.width - (pos.xl - pos.xi) < 40) {
+ return pos._cleanSides = true;
+ }
+ return pos._cleanSides = false;
+ }
+
+ if (!this.options.smartCSR) {
+ return false;
+ }
+
+ // The scrollbar can't update properly, and there's also a
+ // chance that the scrollbar may get moved around senselessly.
+ // NOTE: In pratice, this doesn't seem to be the case.
+ // if (this.scrollbar) {
+ // return pos._cleanSides = false;
+ // }
+
+ // Doesn't matter if we're only a height of 1.
+ // if ((pos.yl - el.ibottom) - (pos.yi + el.itop) <= 1) {
+ // return pos._cleanSides = false;
+ // }
+
+ var yi = pos.yi + el.itop
+ , yl = pos.yl - el.ibottom
+ , first
+ , ch
+ , x
+ , y;
+
+ if (pos.yi < 0) return pos._cleanSides = false;
+ if (pos.yl > this.height) return pos._cleanSides = false;
+ if (pos.xi - 1 < 0) return pos._cleanSides = true;
+ if (pos.xl > this.width) return pos._cleanSides = true;
+
+ for (x = pos.xi - 1; x >= 0; x--) {
+ if (!this.olines[yi]) break;
+ first = this.olines[yi][x];
+ for (y = yi; y < yl; y++) {
+ if (!this.olines[y] || !this.olines[y][x]) break;
+ ch = this.olines[y][x];
+ if (ch[0] !== first[0] || ch[1] !== first[1]) {
+ return pos._cleanSides = false;
+ }
+ }
+ }
+
+ for (x = pos.xl; x < this.width; x++) {
+ if (!this.olines[yi]) break;
+ first = this.olines[yi][x];
+ for (y = yi; y < yl; y++) {
+ if (!this.olines[y] || !this.olines[y][x]) break;
+ ch = this.olines[y][x];
+ if (ch[0] !== first[0] || ch[1] !== first[1]) {
+ return pos._cleanSides = false;
+ }
+ }
+ }
+
+ return pos._cleanSides = true;
+};
+
+Screen.prototype._dockBorders = function() {
+ var lines = this.lines
+ , stops = this._borderStops
+ , i
+ , y
+ , x
+ , ch;
+
+ // var keys, stop;
+ //
+ // keys = Object.keys(this._borderStops)
+ // .map(function(k) { return +k; })
+ // .sort(function(a, b) { return a - b; });
+ //
+ // for (i = 0; i < keys.length; i++) {
+ // y = keys[i];
+ // if (!lines[y]) continue;
+ // stop = this._borderStops[y];
+ // for (x = stop.xi; x < stop.xl; x++) {
+
+ stops = Object.keys(stops)
+ .map(function(k) { return +k; })
+ .sort(function(a, b) { return a - b; });
+
+ for (i = 0; i < stops.length; i++) {
+ y = stops[i];
+ if (!lines[y]) continue;
+ for (x = 0; x < this.width; x++) {
+ ch = lines[y][x][1];
+ if (angles[ch]) {
+ lines[y][x][1] = this._getAngle(lines, x, y);
+ }
+ }
+ }
+};
+
+Screen.prototype._getAngle = function(lines, x, y) {
+ var angle = 0
+ , attr = lines[y][x][0]
+ , ch = lines[y][x][1];
+
+ if (lines[y][x - 1] && langles[lines[y][x - 1][1]]) {
+ if (!this.options.ignoreDockContrast) {
+ if (lines[y][x - 1][0] !== attr) return ch;
+ }
+ angle |= 1 << 3;
+ }
+
+ if (lines[y - 1] && uangles[lines[y - 1][x][1]]) {
+ if (!this.options.ignoreDockContrast) {
+ if (lines[y - 1][x][0] !== attr) return ch;
+ }
+ angle |= 1 << 2;
+ }
+
+ if (lines[y][x + 1] && rangles[lines[y][x + 1][1]]) {
+ if (!this.options.ignoreDockContrast) {
+ if (lines[y][x + 1][0] !== attr) return ch;
+ }
+ angle |= 1 << 1;
+ }
+
+ if (lines[y + 1] && dangles[lines[y + 1][x][1]]) {
+ if (!this.options.ignoreDockContrast) {
+ if (lines[y + 1][x][0] !== attr) return ch;
+ }
+ angle |= 1 << 0;
+ }
+
+ // Experimental: fixes this situation:
+ // +----------+
+ // | <-- empty space here, should be a T angle
+ // +-------+ |
+ // | | |
+ // +-------+ |
+ // | |
+ // +----------+
+ // if (uangles[lines[y][x][1]]) {
+ // if (lines[y + 1] && cdangles[lines[y + 1][x][1]]) {
+ // if (!this.options.ignoreDockContrast) {
+ // if (lines[y + 1][x][0] !== attr) return ch;
+ // }
+ // angle |= 1 << 0;
+ // }
+ // }
+
+ return angleTable[angle] || ch;
+};
+
+Screen.prototype.draw = function(start, end) {
+ // this.emit('predraw');
+
+ var x
+ , y
+ , line
+ , out
+ , ch
+ , data
+ , attr
+ , fg
+ , bg
+ , flags;
+
+ var main = ''
+ , pre
+ , post;
+
+ var clr
+ , neq
+ , xx;
+
+ var lx = -1
+ , ly = -1
+ , o;
+
+ var acs;
+
+ if (this._buf) {
+ main += this._buf;
+ this._buf = '';
+ }
+
+ for (y = start; y <= end; y++) {
+ line = this.lines[y];
+ o = this.olines[y];
+
+ if (!line.dirty && !(this.cursor.artificial && y === this.program.y)) {
+ continue;
+ }
+ line.dirty = false;
+
+ out = '';
+ attr = this.dattr;
+
+ for (x = 0; x < line.length; x++) {
+ data = line[x][0];
+ ch = line[x][1];
+
+ // Render the artificial cursor.
+ if (this.cursor.artificial
+ && !this.cursor._hidden
+ && this.cursor._state
+ && x === this.program.x
+ && y === this.program.y) {
+ var cattr = this._cursorAttr(this.cursor, data);
+ if (cattr.ch) ch = cattr.ch;
+ data = cattr.attr;
+ }
+
+ // Take advantage of xterm's back_color_erase feature by using a
+ // lookahead. Stop spitting out so many damn spaces. NOTE: Is checking
+ // the bg for non BCE terminals worth the overhead?
+ if (this.options.useBCE
+ && ch === ' '
+ && (this.tput.bools.back_color_erase
+ || (data & 0x1ff) === (this.dattr & 0x1ff))
+ && ((data >> 18) & 8) === ((this.dattr >> 18) & 8)) {
+ clr = true;
+ neq = false;
+
+ for (xx = x; xx < line.length; xx++) {
+ if (line[xx][0] !== data || line[xx][1] !== ' ') {
+ clr = false;
+ break;
+ }
+ if (line[xx][0] !== o[xx][0] || line[xx][1] !== o[xx][1]) {
+ neq = true;
+ }
+ }
+
+ if (clr && neq) {
+ lx = -1, ly = -1;
+ if (data !== attr) {
+ out += this.codeAttr(data);
+ attr = data;
+ }
+ out += this.tput.cup(y, x);
+ out += this.tput.el();
+ for (xx = x; xx < line.length; xx++) {
+ o[xx][0] = data;
+ o[xx][1] = ' ';
+ }
+ break;
+ }
+
+ // If there's more than 10 spaces, use EL regardless
+ // and start over drawing the rest of line. Might
+ // not be worth it. Try to use ECH if the terminal
+ // supports it. Maybe only try to use ECH here.
+ // //if (this.tput.strings.erase_chars)
+ // if (!clr && neq && (xx - x) > 10) {
+ // lx = -1, ly = -1;
+ // if (data !== attr) {
+ // out += this.codeAttr(data);
+ // attr = data;
+ // }
+ // out += this.tput.cup(y, x);
+ // if (this.tput.strings.erase_chars) {
+ // // Use erase_chars to avoid erasing the whole line.
+ // out += this.tput.ech(xx - x);
+ // } else {
+ // out += this.tput.el();
+ // }
+ // out += this.tput.cuf(xx - x);
+ // this.fillRegion(data, ' ',
+ // x, this.tput.strings.erase_chars ? xx : line.length,
+ // y, y + 1);
+ // x = xx - 1;
+ // continue;
+ // }
+
+ // Skip to the next line if the
+ // rest of the line is already drawn.
+ // if (!neq) {
+ // for (; xx < line.length; xx++) {
+ // if (line[xx][0] !== o[xx][0] || line[xx][1] !== o[xx][1]) {
+ // neq = true;
+ // break;
+ // }
+ // }
+ // if (!neq) {
+ // attr = data;
+ // break;
+ // }
+ // }
+ }
+
+ // Optimize by comparing the real output
+ // buffer to the pending output buffer.
+ if (data === o[x][0] && ch === o[x][1]) {
+ if (lx === -1) {
+ lx = x;
+ ly = y;
+ }
+ continue;
+ } else if (lx !== -1) {
+ out += y === ly
+ ? this.tput.cuf(x - lx)
+ : this.tput.cup(y, x);
+ lx = -1, ly = -1;
+ }
+ o[x][0] = data;
+ o[x][1] = ch;
+
+ if (data !== attr) {
+ if (attr !== this.dattr) {
+ out += '\x1b[m';
+ }
+ if (data !== this.dattr) {
+ out += '\x1b[';
+
+ bg = data & 0x1ff;
+ fg = (data >> 9) & 0x1ff;
+ flags = data >> 18;
+
+ // bold
+ if (flags & 1) {
+ out += '1;';
+ }
+
+ // underline
+ if (flags & 2) {
+ out += '4;';
+ }
+
+ // blink
+ if (flags & 4) {
+ out += '5;';
+ }
+
+ // inverse
+ if (flags & 8) {
+ out += '7;';
+ }
+
+ // invisible
+ if (flags & 16) {
+ out += '8;';
+ }
+
+ if (bg !== 0x1ff) {
+ bg = this._reduceColor(bg);
+ if (bg < 16) {
+ if (bg < 8) {
+ bg += 40;
+ } else if (bg < 16) {
+ bg -= 8;
+ bg += 100;
+ }
+ out += bg + ';';
+ } else {
+ out += '48;5;' + bg + ';';
+ }
+ }
+
+ if (fg !== 0x1ff) {
+ fg = this._reduceColor(fg);
+ if (fg < 16) {
+ if (fg < 8) {
+ fg += 30;
+ } else if (fg < 16) {
+ fg -= 8;
+ fg += 90;
+ }
+ out += fg + ';';
+ } else {
+ out += '38;5;' + fg + ';';
+ }
+ }
+
+ if (out[out.length - 1] === ';') out = out.slice(0, -1);
+
+ out += 'm';
+ }
+ }
+
+ // If we find a double-width char, eat the next character which should be
+ // a space due to parseContent's behavior.
+ if (this.fullUnicode) {
+ // If this is a surrogate pair double-width char, we can ignore it
+ // because parseContent already counted it as length=2.
+ if (unicode.charWidth(line[x][1]) === 2) {
+ // NOTE: At cols=44, the bug that is avoided
+ // by the angles check occurs in widget-unicode:
+ // Might also need: `line[x + 1][0] !== line[x][0]`
+ // for borderless boxes?
+ if (x === line.length - 1 || angles[line[x + 1][1]]) {
+ // If we're at the end, we don't have enough space for a
+ // double-width. Overwrite it with a space and ignore.
+ ch = ' ';
+ o[x][1] = '\0';
+ } else {
+ // ALWAYS refresh double-width chars because this special cursor
+ // behavior is needed. There may be a more efficient way of doing
+ // this. See above.
+ o[x][1] = '\0';
+ // Eat the next character by moving forward and marking as a
+ // space (which it is).
+ o[++x][1] = '\0';
+ }
+ }
+ }
+
+ // Attempt to use ACS for supported characters.
+ // This is not ideal, but it's how ncurses works.
+ // There are a lot of terminals that support ACS
+ // *and UTF8, but do not declare U8. So ACS ends
+ // up being used (slower than utf8). Terminals
+ // that do not support ACS and do not explicitly
+ // support UTF8 get their unicode characters
+ // replaced with really ugly ascii characters.
+ // It is possible there is a terminal out there
+ // somewhere that does not support ACS, but
+ // supports UTF8, but I imagine it's unlikely.
+ // Maybe remove !this.tput.unicode check, however,
+ // this seems to be the way ncurses does it.
+ if (this.tput.strings.enter_alt_charset_mode
+ && !this.tput.brokenACS && (this.tput.acscr[ch] || acs)) {
+ // Fun fact: even if this.tput.brokenACS wasn't checked here,
+ // the linux console would still work fine because the acs
+ // table would fail the check of: this.tput.acscr[ch]
+ if (this.tput.acscr[ch]) {
+ if (acs) {
+ ch = this.tput.acscr[ch];
+ } else {
+ ch = this.tput.smacs()
+ + this.tput.acscr[ch];
+ acs = true;
+ }
+ } else if (acs) {
+ ch = this.tput.rmacs() + ch;
+ acs = false;
+ }
+ } else {
+ // U8 is not consistently correct. Some terminfo's
+ // terminals that do not declare it may actually
+ // support utf8 (e.g. urxvt), but if the terminal
+ // does not declare support for ACS (and U8), chances
+ // are it does not support UTF8. This is probably
+ // the "safest" way to do this. Should fix things
+ // like sun-color.
+ // NOTE: It could be the case that the $LANG
+ // is all that matters in some cases:
+ // if (!this.tput.unicode && ch > '~') {
+ if (!this.tput.unicode && this.tput.numbers.U8 !== 1 && ch > '~') {
+ ch = this.tput.utoa[ch] || '?';
+ }
+ }
+
+ out += ch;
+ attr = data;
+ }
+
+ if (attr !== this.dattr) {
+ out += '\x1b[m';
+ }
+
+ if (out) {
+ main += this.tput.cup(y, 0) + out;
+ }
+ }
+
+ if (acs) {
+ main += this.tput.rmacs();
+ acs = false;
+ }
+
+ if (main) {
+ pre = '';
+ post = '';
+
+ pre += this.tput.sc();
+ post += this.tput.rc();
+
+ if (!this.program.cursorHidden) {
+ pre += this.tput.civis();
+ post += this.tput.cnorm();
+ }
+
+ // this.program.flush();
+ // this.program.output.write(pre + main + post);
+ this.program._write(pre + main + post);
+ }
+
+ // this.emit('draw');
+};
+
+Screen.prototype._reduceColor = function(col) {
+ if (col >= 16 && this.tput.colors <= 16) {
+ col = colors.ccolors[col];
+ } else if (col >= 8 && this.tput.colors <= 8) {
+ col -= 8;
+ } else if (col >= 2 && this.tput.colors <= 2) {
+ col %= 2;
+ }
+ return col;
+};
+
+// Convert an SGR string to our own attribute format.
+Screen.prototype.attrCode = function(code, cur, def) {
+ var flags = (cur >> 18) & 0x1ff
+ , fg = (cur >> 9) & 0x1ff
+ , bg = cur & 0x1ff
+ , c
+ , i;
+
+ code = code.slice(2, -1).split(';');
+ if (!code[0]) code[0] = '0';
+
+ for (i = 0; i < code.length; i++) {
+ c = +code[i] || 0;
+ switch (c) {
+ case 0: // normal
+ bg = def & 0x1ff;
+ fg = (def >> 9) & 0x1ff;
+ flags = (def >> 18) & 0x1ff;
+ break;
+ case 1: // bold
+ flags |= 1;
+ break;
+ case 22:
+ flags = (def >> 18) & 0x1ff;
+ break;
+ case 4: // underline
+ flags |= 2;
+ break;
+ case 24:
+ flags = (def >> 18) & 0x1ff;
+ break;
+ case 5: // blink
+ flags |= 4;
+ break;
+ case 25:
+ flags = (def >> 18) & 0x1ff;
+ break;
+ case 7: // inverse
+ flags |= 8;
+ break;
+ case 27:
+ flags = (def >> 18) & 0x1ff;
+ break;
+ case 8: // invisible
+ flags |= 16;
+ break;
+ case 28:
+ flags = (def >> 18) & 0x1ff;
+ break;
+ case 39: // default fg
+ fg = (def >> 9) & 0x1ff;
+ break;
+ case 49: // default bg
+ bg = def & 0x1ff;
+ break;
+ case 100: // default fg/bg
+ fg = (def >> 9) & 0x1ff;
+ bg = def & 0x1ff;
+ break;
+ default: // color
+ if (c === 48 && +code[i+1] === 5) {
+ i += 2;
+ bg = +code[i];
+ break;
+ } else if (c === 48 && +code[i+1] === 2) {
+ i += 2;
+ bg = colors.match(+code[i], +code[i+1], +code[i+2]);
+ if (bg === -1) bg = def & 0x1ff;
+ i += 2;
+ break;
+ } else if (c === 38 && +code[i+1] === 5) {
+ i += 2;
+ fg = +code[i];
+ break;
+ } else if (c === 38 && +code[i+1] === 2) {
+ i += 2;
+ fg = colors.match(+code[i], +code[i+1], +code[i+2]);
+ if (fg === -1) fg = (def >> 9) & 0x1ff;
+ i += 2;
+ break;
+ }
+ if (c >= 40 && c <= 47) {
+ bg = c - 40;
+ } else if (c >= 100 && c <= 107) {
+ bg = c - 100;
+ bg += 8;
+ } else if (c === 49) {
+ bg = def & 0x1ff;
+ } else if (c >= 30 && c <= 37) {
+ fg = c - 30;
+ } else if (c >= 90 && c <= 97) {
+ fg = c - 90;
+ fg += 8;
+ } else if (c === 39) {
+ fg = (def >> 9) & 0x1ff;
+ } else if (c === 100) {
+ fg = (def >> 9) & 0x1ff;
+ bg = def & 0x1ff;
+ }
+ break;
+ }
+ }
+
+ return (flags << 18) | (fg << 9) | bg;
+};
+
+// Convert our own attribute format to an SGR string.
+Screen.prototype.codeAttr = function(code) {
+ var flags = (code >> 18) & 0x1ff
+ , fg = (code >> 9) & 0x1ff
+ , bg = code & 0x1ff
+ , out = '';
+
+ // bold
+ if (flags & 1) {
+ out += '1;';
+ }
+
+ // underline
+ if (flags & 2) {
+ out += '4;';
+ }
+
+ // blink
+ if (flags & 4) {
+ out += '5;';
+ }
+
+ // inverse
+ if (flags & 8) {
+ out += '7;';
+ }
+
+ // invisible
+ if (flags & 16) {
+ out += '8;';
+ }
+
+ if (bg !== 0x1ff) {
+ bg = this._reduceColor(bg);
+ if (bg < 16) {
+ if (bg < 8) {
+ bg += 40;
+ } else if (bg < 16) {
+ bg -= 8;
+ bg += 100;
+ }
+ out += bg + ';';
+ } else {
+ out += '48;5;' + bg + ';';
+ }
+ }
+
+ if (fg !== 0x1ff) {
+ fg = this._reduceColor(fg);
+ if (fg < 16) {
+ if (fg < 8) {
+ fg += 30;
+ } else if (fg < 16) {
+ fg -= 8;
+ fg += 90;
+ }
+ out += fg + ';';
+ } else {
+ out += '38;5;' + fg + ';';
+ }
+ }
+
+ if (out[out.length - 1] === ';') out = out.slice(0, -1);
+
+ return '\x1b[' + out + 'm';
+};
+
+Screen.prototype.focusOffset = function(offset) {
+ var shown = this.keyable.filter(function(el) {
+ return !el.detached && el.visible;
+ }).length;
+
+ if (!shown || !offset) {
+ return;
+ }
+
+ var i = this.keyable.indexOf(this.focused);
+ if (!~i) return;
+
+ if (offset > 0) {
+ while (offset--) {
+ if (++i > this.keyable.length - 1) i = 0;
+ if (this.keyable[i].detached || !this.keyable[i].visible) offset++;
+ }
+ } else {
+ offset = -offset;
+ while (offset--) {
+ if (--i < 0) i = this.keyable.length - 1;
+ if (this.keyable[i].detached || !this.keyable[i].visible) offset++;
+ }
+ }
+
+ return this.keyable[i].focus();
+};
+
+Screen.prototype.focusPrev =
+Screen.prototype.focusPrevious = function() {
+ return this.focusOffset(-1);
+};
+
+Screen.prototype.focusNext = function() {
+ return this.focusOffset(1);
+};
+
+Screen.prototype.focusPush = function(el) {
+ if (!el) return;
+ var old = this.history[this.history.length - 1];
+ if (this.history.length === 10) {
+ this.history.shift();
+ }
+ this.history.push(el);
+ this._focus(el, old);
+};
+
+Screen.prototype.focusPop = function() {
+ var old = this.history.pop();
+ if (this.history.length) {
+ this._focus(this.history[this.history.length - 1], old);
+ }
+ return old;
+};
+
+Screen.prototype.saveFocus = function() {
+ return this._savedFocus = this.focused;
+};
+
+Screen.prototype.restoreFocus = function() {
+ if (!this._savedFocus) return;
+ this._savedFocus.focus();
+ delete this._savedFocus;
+ return this.focused;
+};
+
+Screen.prototype.rewindFocus = function() {
+ var old = this.history.pop()
+ , el;
+
+ while (this.history.length) {
+ el = this.history.pop();
+ if (!el.detached && el.visible) {
+ this.history.push(el);
+ this._focus(el, old);
+ return el;
+ }
+ }
+
+ if (old) {
+ old.emit('blur');
+ }
+};
+
+Screen.prototype._focus = function(self, old) {
+ // Find a scrollable ancestor if we have one.
+ var el = self;
+ while (el = el.parent) {
+ if (el.scrollable) break;
+ }
+
+ // If we're in a scrollable element,
+ // automatically scroll to the focused element.
+ if (el) {
+ // NOTE: This is different from the other "visible" values - it needs the
+ // visible height of the scrolling element itself, not the element within
+ // it.
+ var visible = self.screen.height - el.atop - el.itop - el.abottom - el.ibottom;
+ if (self.rtop < el.childBase) {
+ el.scrollTo(self.rtop);
+ self.screen.render();
+ } else if (self.rtop + self.height - self.ibottom > el.childBase + visible) {
+ // Explanation for el.itop here: takes into account scrollable elements
+ // with borders otherwise the element gets covered by the bottom border:
+ el.scrollTo(self.rtop - (el.height - self.height) + el.itop, true);
+ self.screen.render();
+ }
+ }
+
+ if (old) {
+ old.emit('blur', self);
+ }
+
+ self.emit('focus', old);
+};
+
+Screen.prototype.__defineGetter__('focused', function() {
+ return this.history[this.history.length - 1];
+});
+
+Screen.prototype.__defineSetter__('focused', function(el) {
+ return this.focusPush(el);
+});
+
+Screen.prototype.clearRegion = function(xi, xl, yi, yl, override) {
+ return this.fillRegion(this.dattr, ' ', xi, xl, yi, yl, override);
+};
+
+Screen.prototype.fillRegion = function(attr, ch, xi, xl, yi, yl, override) {
+ var lines = this.lines
+ , cell
+ , xx;
+
+ if (xi < 0) xi = 0;
+ if (yi < 0) yi = 0;
+
+ for (; yi < yl; yi++) {
+ if (!lines[yi]) break;
+ for (xx = xi; xx < xl; xx++) {
+ cell = lines[yi][xx];
+ if (!cell) break;
+ if (override || attr !== cell[0] || ch !== cell[1]) {
+ lines[yi][xx][0] = attr;
+ lines[yi][xx][1] = ch;
+ lines[yi].dirty = true;
+ }
+ }
+ }
+};
+
+Screen.prototype.key = function() {
+ return this.program.key.apply(this, arguments);
+};
+
+Screen.prototype.onceKey = function() {
+ return this.program.onceKey.apply(this, arguments);
+};
+
+Screen.prototype.unkey =
+Screen.prototype.removeKey = function() {
+ return this.program.unkey.apply(this, arguments);
+};
+
+Screen.prototype.spawn = function(file, args, options) {
+ if (!Array.isArray(args)) {
+ options = args;
+ args = [];
+ }
+
+ var screen = this
+ , program = screen.program
+ , options = options || {}
+ , spawn = require('child_process').spawn
+ , mouse = program.mouseEnabled
+ , ps;
+
+ options.stdio = options.stdio || 'inherit';
+
+ program.lsaveCursor('spawn');
+ // program.csr(0, program.rows - 1);
+ program.normalBuffer();
+ program.showCursor();
+ if (mouse) program.disableMouse();
+
+ var write = program.output.write;
+ program.output.write = function() {};
+ program.input.pause();
+ program.input.setRawMode(false);
+
+ var resume = function() {
+ if (resume.done) return;
+ resume.done = true;
+
+ program.input.setRawMode(true);
+ program.input.resume();
+ program.output.write = write;
+
+ program.alternateBuffer();
+ // program.csr(0, program.rows - 1);
+ if (mouse) program.enableMouse();
+
+ screen.alloc();
+ screen.render();
+
+ screen.program.lrestoreCursor('spawn', true);
+ };
+
+ ps = spawn(file, args, options);
+
+ ps.on('error', resume);
+
+ ps.on('exit', resume);
+
+ return ps;
+};
+
+Screen.prototype.exec = function(file, args, options, callback) {
+ var callback = arguments[arguments.length - 1]
+ , ps = this.spawn(file, args, options);
+
+ ps.on('error', function(err) {
+ if (!callback) return;
+ return callback(err, false);
+ });
+
+ ps.on('exit', function(code) {
+ if (!callback) return;
+ return callback(null, code === 0);
+ });
+
+ return ps;
+};
+
+Screen.prototype.readEditor = function(options, callback) {
+ if (typeof options === 'string') {
+ options = { editor: options };
+ }
+
+ if (!callback) {
+ callback = options;
+ options = null;
+ }
+
+ if (!callback) {
+ callback = function() {};
+ }
+
+ options = options || {};
+
+ var self = this
+ , fs = require('fs')
+ , editor = options.editor || process.env.EDITOR || 'vi'
+ , name = options.name || process.title || 'blessed'
+ , rnd = Math.random().toString(36).split('.').pop()
+ , file = '/tmp/' + name + '.' + rnd
+ , args = [file]
+ , opt;
+
+ opt = {
+ stdio: 'inherit',
+ env: process.env,
+ cwd: process.env.HOME
+ };
+
+ function writeFile(callback) {
+ if (!options.value) return callback();
+ return fs.writeFile(file, options.value, callback);
+ }
+
+ return writeFile(function(err) {
+ if (err) return callback(err);
+ return self.exec(editor, args, opt, function(err, success) {
+ if (err) return callback(err);
+ return fs.readFile(file, 'utf8', function(err, data) {
+ return fs.unlink(file, function() {
+ if (!success) return callback(new Error('Unsuccessful.'));
+ if (err) return callback(err);
+ return callback(null, data);
+ });
+ });
+ });
+ });
+};
+
+Screen.prototype.displayImage = function(file, callback) {
+ var self = this;
+
+ if (!file) {
+ if (!callback) return;
+ return callback(new Error('No image.'));
+ }
+
+ var file = path.resolve(process.cwd(), file);
+
+ if (!~file.indexOf('://')) {
+ file = 'file://' + file;
+ }
+
+ var args = ['w3m', '-T', 'text/html'];
+
+ var input = 'press q to exit'
+ + '';
+
+ var opt = {
+ stdio: ['pipe', 1, 2],
+ env: process.env,
+ cwd: process.env.HOME
+ };
+
+ var ps = this.spawn(args[0], args.slice(1), opt);
+
+ ps.on('error', function(err) {
+ if (!callback) return;
+ return callback(err);
+ });
+
+ ps.on('exit', function(code) {
+ if (!callback) return;
+ if (code !== 0) return callback(new Error('Exit Code: ' + code));
+ return callback(null, code === 0);
+ });
+
+ ps.stdin.write(input + '\n');
+ ps.stdin.end();
+};
+
+Screen.prototype.setEffects = function(el, fel, over, out, effects, temp) {
+ if (!effects) return;
+
+ var tmp = {};
+ if (temp) el[temp] = tmp;
+
+ if (typeof el !== 'function') {
+ var _el = el;
+ el = function() { return _el; };
+ }
+
+ fel.on(over, function() {
+ var element = el();
+ Object.keys(effects).forEach(function(key) {
+ var val = effects[key];
+ if (val !== null && typeof val === 'object') {
+ tmp[key] = tmp[key] || {};
+ // element.style[key] = element.style[key] || {};
+ Object.keys(val).forEach(function(k) {
+ var v = val[k];
+ tmp[key][k] = element.style[key][k];
+ element.style[key][k] = v;
+ });
+ return;
+ }
+ tmp[key] = element.style[key];
+ element.style[key] = val;
+ });
+ element.screen.render();
+ });
+
+ fel.on(out, function() {
+ var element = el();
+ Object.keys(effects).forEach(function(key) {
+ var val = effects[key];
+ if (val !== null && typeof val === 'object') {
+ tmp[key] = tmp[key] || {};
+ // element.style[key] = element.style[key] || {};
+ Object.keys(val).forEach(function(k) {
+ if (tmp[key].hasOwnProperty(k)) {
+ element.style[key][k] = tmp[key][k];
+ }
+ });
+ return;
+ }
+ if (tmp.hasOwnProperty(key)) {
+ element.style[key] = tmp[key];
+ }
+ });
+ element.screen.render();
+ });
+};
+
+Screen.prototype.sigtstp = function(callback) {
+ var self = this;
+ this.program.sigtstp(function() {
+ self.alloc();
+ self.render();
+ self.program.lrestoreCursor('pause', true);
+ if (callback) callback();
+ });
+};
+
+Screen.prototype.copyToClipboard = function(text) {
+ return this.program.copyToClipboard(text);
+};
+
+Screen.prototype.cursorShape = function(shape, blink) {
+ var self = this;
+
+ this.cursor.shape = shape || 'block';
+ this.cursor.blink = blink || false;
+ this.cursor._set = true;
+
+ if (this.cursor.artificial) {
+ if (!this.program.hideCursor_old) {
+ var hideCursor = this.program.hideCursor;
+ this.program.hideCursor_old = this.program.hideCursor;
+ this.program.hideCursor = function() {
+ hideCursor.call(self.program);
+ self.cursor._hidden = true;
+ if (self.renders) self.render();
+ };
+ }
+ if (!this.program.showCursor_old) {
+ var showCursor = this.program.showCursor;
+ this.program.showCursor_old = this.program.showCursor;
+ this.program.showCursor = function() {
+ self.cursor._hidden = false;
+ if (self.program._exiting) showCursor.call(self.program);
+ if (self.renders) self.render();
+ };
+ }
+ if (!this._cursorBlink) {
+ this._cursorBlink = setInterval(function() {
+ if (!self.cursor.blink) return;
+ self.cursor._state ^= 1;
+ if (self.renders) self.render();
+ }, 500);
+ this._cursorBlink.unref();
+ }
+ return true;
+ }
+
+ return this.program.cursorShape(this.cursor.shape, this.cursor.blink);
+};
+
+Screen.prototype.cursorColor = function(color) {
+ this.cursor.color = color != null
+ ? colors.convert(color)
+ : null;
+ this.cursor._set = true;
+
+ if (this.cursor.artificial) {
+ return true;
+ }
+
+ return this.program.cursorColor(colors.ncolors[this.cursor.color]);
+};
+
+Screen.prototype.cursorReset =
+Screen.prototype.resetCursor = function() {
+ this.cursor.shape = 'block';
+ this.cursor.blink = false;
+ this.cursor.color = null;
+ this.cursor._set = false;
+
+ if (this.cursor.artificial) {
+ this.cursor.artificial = false;
+ if (this.program.hideCursor_old) {
+ this.program.hideCursor = this.program.hideCursor_old;
+ delete this.program.hideCursor_old;
+ }
+ if (this.program.showCursor_old) {
+ this.program.showCursor = this.program.showCursor_old;
+ delete this.program.showCursor_old;
+ }
+ if (this._cursorBlink) {
+ clearInterval(this._cursorBlink);
+ delete this._cursorBlink;
+ }
+ return true;
+ }
+
+ return this.program.cursorReset();
+};
+
+Screen.prototype._cursorAttr = function(cursor, dattr) {
+ var attr = dattr || this.dattr
+ , cattr
+ , ch;
+
+ if (cursor.shape === 'line') {
+ attr &= ~(0x1ff << 9);
+ attr |= 7 << 9;
+ ch = '\u2502';
+ } else if (cursor.shape === 'underline') {
+ attr &= ~(0x1ff << 9);
+ attr |= 7 << 9;
+ attr |= 2 << 18;
+ } else if (cursor.shape === 'block') {
+ attr &= ~(0x1ff << 9);
+ attr |= 7 << 9;
+ attr |= 8 << 18;
+ } else if (typeof cursor.shape === 'object' && cursor.shape) {
+ cattr = Element.prototype.sattr.call(cursor, cursor.shape);
+
+ if (cursor.shape.bold || cursor.shape.underline
+ || cursor.shape.blink || cursor.shape.inverse
+ || cursor.shape.invisible) {
+ attr &= ~(0x1ff << 18);
+ attr |= ((cattr >> 18) & 0x1ff) << 18;
+ }
+
+ if (cursor.shape.fg) {
+ attr &= ~(0x1ff << 9);
+ attr |= ((cattr >> 9) & 0x1ff) << 9;
+ }
+
+ if (cursor.shape.bg) {
+ attr &= ~(0x1ff << 0);
+ attr |= cattr & 0x1ff;
+ }
+
+ if (cursor.shape.ch) {
+ ch = cursor.shape.ch;
+ }
+ }
+
+ if (cursor.color != null) {
+ attr &= ~(0x1ff << 9);
+ attr |= cursor.color << 9;
+ }
+
+ return {
+ ch: ch,
+ attr: attr
+ };
+};
+
+Screen.prototype.screenshot = function(xi, xl, yi, yl, term) {
+ if (xi == null) xi = 0;
+ if (xl == null) xl = this.cols;
+ if (yi == null) yi = 0;
+ if (yl == null) yl = this.rows;
+
+ if (xi < 0) xi = 0;
+ if (yi < 0) yi = 0;
+
+ var x
+ , y
+ , line
+ , out
+ , ch
+ , data
+ , attr;
+
+ var sdattr = this.dattr;
+
+ if (term) {
+ this.dattr = term.defAttr;
+ }
+
+ var main = '';
+
+ for (y = yi; y < yl; y++) {
+ line = term
+ ? term.lines[y]
+ : this.lines[y];
+
+ if (!line) break;
+
+ out = '';
+ attr = this.dattr;
+
+ for (x = xi; x < xl; x++) {
+ if (!line[x]) break;
+
+ data = line[x][0];
+ ch = line[x][1];
+
+ if (data !== attr) {
+ if (attr !== this.dattr) {
+ out += '\x1b[m';
+ }
+ if (data !== this.dattr) {
+ var _data = data;
+ if (term) {
+ if (((_data >> 9) & 0x1ff) === 257) _data |= 0x1ff << 9;
+ if ((_data & 0x1ff) === 256) _data |= 0x1ff;
+ }
+ out += this.codeAttr(_data);
+ }
+ }
+
+ if (this.fullUnicode) {
+ if (unicode.charWidth(line[x][1]) === 2) {
+ if (x === xl - 1) {
+ ch = ' ';
+ } else {
+ x++;
+ }
+ }
+ }
+
+ out += ch;
+ attr = data;
+ }
+
+ if (attr !== this.dattr) {
+ out += '\x1b[m';
+ }
+
+ if (out) {
+ main += (y > 0 ? '\n' : '') + out;
+ }
+ }
+
+ main = main.replace(/(?:\s*\x1b\[40m\s*\x1b\[m\s*)*$/, '') + '\n';
+
+ if (term) {
+ this.dattr = sdattr;
+ }
+
+ return main;
+};
+
+/**
+ * Positioning
+ */
+
+Screen.prototype._getPos = function() {
+ return this;
+};
+
+/**
+ * Angle Table
+ */
+
+var angles = {
+ '\u2518': true, // '┘'
+ '\u2510': true, // '┐'
+ '\u250c': true, // '┌'
+ '\u2514': true, // '└'
+ '\u253c': true, // '┼'
+ '\u251c': true, // '├'
+ '\u2524': true, // '┤'
+ '\u2534': true, // '┴'
+ '\u252c': true, // '┬'
+ '\u2502': true, // '│'
+ '\u2500': true // '─'
+};
+
+var langles = {
+ '\u250c': true, // '┌'
+ '\u2514': true, // '└'
+ '\u253c': true, // '┼'
+ '\u251c': true, // '├'
+ '\u2534': true, // '┴'
+ '\u252c': true, // '┬'
+ '\u2500': true // '─'
+};
+
+var uangles = {
+ '\u2510': true, // '┐'
+ '\u250c': true, // '┌'
+ '\u253c': true, // '┼'
+ '\u251c': true, // '├'
+ '\u2524': true, // '┤'
+ '\u252c': true, // '┬'
+ '\u2502': true // '│'
+};
+
+var rangles = {
+ '\u2518': true, // '┘'
+ '\u2510': true, // '┐'
+ '\u253c': true, // '┼'
+ '\u2524': true, // '┤'
+ '\u2534': true, // '┴'
+ '\u252c': true, // '┬'
+ '\u2500': true // '─'
+};
+
+var dangles = {
+ '\u2518': true, // '┘'
+ '\u2514': true, // '└'
+ '\u253c': true, // '┼'
+ '\u251c': true, // '├'
+ '\u2524': true, // '┤'
+ '\u2534': true, // '┴'
+ '\u2502': true // '│'
+};
+
+var cdangles = {
+ '\u250c': true // '┌'
+};
+
+// Every ACS angle character can be
+// represented by 4 bits ordered like this:
+// [langle][uangle][rangle][dangle]
+var angleTable = {
+ '0000': '', // ?
+ '0001': '\u2502', // '│' // ?
+ '0010': '\u2500', // '─' // ??
+ '0011': '\u250c', // '┌'
+ '0100': '\u2502', // '│' // ?
+ '0101': '\u2502', // '│'
+ '0110': '\u2514', // '└'
+ '0111': '\u251c', // '├'
+ '1000': '\u2500', // '─' // ??
+ '1001': '\u2510', // '┐'
+ '1010': '\u2500', // '─' // ??
+ '1011': '\u252c', // '┬'
+ '1100': '\u2518', // '┘'
+ '1101': '\u2524', // '┤'
+ '1110': '\u2534', // '┴'
+ '1111': '\u253c' // '┼'
+};
+
+Object.keys(angleTable).forEach(function(key) {
+ angleTable[parseInt(key, 2)] = angleTable[key];
+ delete angleTable[key];
+});
+
+module.exports = Screen;
diff --git a/lib/widgets/scrollablebox.js b/lib/widgets/scrollablebox.js
new file mode 100644
index 0000000..c78f86d
--- /dev/null
+++ b/lib/widgets/scrollablebox.js
@@ -0,0 +1,387 @@
+/**
+ * scrollablebox.js - scrollable box element for blessed
+ * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
+ * https://github.com/chjj/blessed
+ */
+
+/**
+ * Modules
+ */
+
+var helpers = require('../helpers');
+
+var Node = require('./node');
+var Box = require('./box');
+
+/**
+ * ScrollableBox
+ */
+
+function ScrollableBox(options) {
+ var self = this;
+
+ if (!(this instanceof Node)) {
+ return new ScrollableBox(options);
+ }
+
+ options = options || {};
+
+ Box.call(this, options);
+
+ if (options.scrollable === false) {
+ return this;
+ }
+
+ this.scrollable = true;
+ this.childOffset = 0;
+ this.childBase = 0;
+ this.baseLimit = options.baseLimit || Infinity;
+ this.alwaysScroll = options.alwaysScroll;
+
+ this.scrollbar = options.scrollbar;
+ if (this.scrollbar) {
+ this.scrollbar.ch = this.scrollbar.ch || ' ';
+ this.style.scrollbar = this.style.scrollbar || this.scrollbar.style;
+ if (!this.style.scrollbar) {
+ this.style.scrollbar = {};
+ this.style.scrollbar.fg = this.scrollbar.fg;
+ this.style.scrollbar.bg = this.scrollbar.bg;
+ this.style.scrollbar.bold = this.scrollbar.bold;
+ this.style.scrollbar.underline = this.scrollbar.underline;
+ this.style.scrollbar.inverse = this.scrollbar.inverse;
+ this.style.scrollbar.invisible = this.scrollbar.invisible;
+ }
+ //this.scrollbar.style = this.style.scrollbar;
+ if (this.track || this.scrollbar.track) {
+ this.track = this.scrollbar.track || this.track;
+ this.style.track = this.style.scrollbar.track || this.style.track;
+ this.track.ch = this.track.ch || ' ';
+ this.style.track = this.style.track || this.track.style;
+ if (!this.style.track) {
+ this.style.track = {};
+ this.style.track.fg = this.track.fg;
+ this.style.track.bg = this.track.bg;
+ this.style.track.bold = this.track.bold;
+ this.style.track.underline = this.track.underline;
+ this.style.track.inverse = this.track.inverse;
+ this.style.track.invisible = this.track.invisible;
+ }
+ this.track.style = this.style.track;
+ }
+ // Allow controlling of the scrollbar via the mouse:
+ if (options.mouse) {
+ this.on('mousedown', function(data) {
+ if (self._scrollingBar) {
+ // Do not allow dragging on the scrollbar:
+ delete self.screen._dragging;
+ delete self._drag;
+ return;
+ }
+ var x = data.x - self.aleft;
+ var y = data.y - self.atop;
+ if (x === self.width - self.iright - 1) {
+ // Do not allow dragging on the scrollbar:
+ delete self.screen._dragging;
+ delete self._drag;
+ var perc = (y - self.itop) / (self.height - self.iheight);
+ self.setScrollPerc(perc * 100 | 0);
+ self.screen.render();
+ var smd, smu;
+ self._scrollingBar = true;
+ self.onScreenEvent('mousedown', smd = function(data) {
+ var y = data.y - self.atop;
+ var perc = y / self.height;
+ self.setScrollPerc(perc * 100 | 0);
+ self.screen.render();
+ });
+ // If mouseup occurs out of the window, no mouseup event fires, and
+ // scrollbar will drag again on mousedown until another mouseup
+ // occurs.
+ self.onScreenEvent('mouseup', smu = function(data) {
+ self._scrollingBar = false;
+ self.removeScreenEvent('mousedown', smd);
+ self.removeScreenEvent('mouseup', smu);
+ });
+ }
+ });
+ }
+ }
+
+ if (options.mouse) {
+ this.on('wheeldown', function(el, data) {
+ self.scroll(self.height / 2 | 0 || 1);
+ self.screen.render();
+ });
+ this.on('wheelup', function(el, data) {
+ self.scroll(-(self.height / 2 | 0) || -1);
+ self.screen.render();
+ });
+ }
+
+ if (options.keys && !options.ignoreKeys) {
+ this.on('keypress', function(ch, key) {
+ if (key.name === 'up' || (options.vi && key.name === 'k')) {
+ self.scroll(-1);
+ self.screen.render();
+ return;
+ }
+ if (key.name === 'down' || (options.vi && key.name === 'j')) {
+ self.scroll(1);
+ self.screen.render();
+ return;
+ }
+ if (options.vi && key.name === 'u' && key.ctrl) {
+ self.scroll(-(self.height / 2 | 0) || -1);
+ self.screen.render();
+ return;
+ }
+ if (options.vi && key.name === 'd' && key.ctrl) {
+ self.scroll(self.height / 2 | 0 || 1);
+ self.screen.render();
+ return;
+ }
+ if (options.vi && key.name === 'b' && key.ctrl) {
+ self.scroll(-self.height || -1);
+ self.screen.render();
+ return;
+ }
+ if (options.vi && key.name === 'f' && key.ctrl) {
+ self.scroll(self.height || 1);
+ self.screen.render();
+ return;
+ }
+ if (options.vi && key.name === 'g' && !key.shift) {
+ self.scrollTo(0);
+ self.screen.render();
+ return;
+ }
+ if (options.vi && key.name === 'g' && key.shift) {
+ self.scrollTo(self.getScrollHeight());
+ self.screen.render();
+ return;
+ }
+ });
+ }
+
+ this.on('parsed content', function() {
+ self._recalculateIndex();
+ });
+
+ self._recalculateIndex();
+}
+
+ScrollableBox.prototype.__proto__ = Box.prototype;
+
+ScrollableBox.prototype.type = 'scrollable-box';
+
+// XXX Potentially use this in place of scrollable checks elsewhere.
+ScrollableBox.prototype.__defineGetter__('reallyScrollable', function() {
+ if (this.shrink) return this.scrollable;
+ return this.getScrollHeight() > this.height;
+});
+
+ScrollableBox.prototype._scrollBottom = function() {
+ if (!this.scrollable) return 0;
+
+ // We could just calculate the children, but we can
+ // optimize for lists by just returning the items.length.
+ if (this._isList) {
+ return this.items ? this.items.length : 0;
+ }
+
+ if (this.lpos && this.lpos._scrollBottom) {
+ return this.lpos._scrollBottom;
+ }
+
+ var bottom = this.children.reduce(function(current, el) {
+ // el.height alone does not calculate the shrunken height, we need to use
+ // getCoords. A shrunken box inside a scrollable element will not grow any
+ // larger than the scrollable element's context regardless of how much
+ // content is in the shrunken box, unless we do this (call getCoords
+ // without the scrollable calculation):
+ // See: $ node test/widget-shrink-fail-2.js
+ if (!el.detached) {
+ var lpos = el._getCoords(false, true);
+ if (lpos) {
+ return Math.max(current, el.rtop + (lpos.yl - lpos.yi));
+ }
+ }
+ return Math.max(current, el.rtop + el.height);
+ }, 0);
+
+ // XXX Use this? Makes .getScrollHeight() useless!
+ // if (bottom < this._clines.length) bottom = this._clines.length;
+
+ if (this.lpos) this.lpos._scrollBottom = bottom;
+
+ return bottom;
+};
+
+ScrollableBox.prototype.setScroll =
+ScrollableBox.prototype.scrollTo = function(offset, always) {
+ // XXX
+ // At first, this appeared to account for the first new calculation of childBase:
+ this.scroll(0);
+ return this.scroll(offset - (this.childBase + this.childOffset), always);
+};
+
+ScrollableBox.prototype.getScroll = function() {
+ return this.childBase + this.childOffset;
+};
+
+ScrollableBox.prototype.scroll = function(offset, always) {
+ if (!this.scrollable) return;
+
+ if (this.detached) return;
+
+ // Handle scrolling.
+ var visible = this.height - this.iheight
+ , base = this.childBase
+ , d
+ , p
+ , t
+ , b
+ , max
+ , emax;
+
+ if (this.alwaysScroll || always) {
+ // Semi-workaround
+ this.childOffset = offset > 0
+ ? visible - 1 + offset
+ : offset;
+ } else {
+ this.childOffset += offset;
+ }
+
+ if (this.childOffset > visible - 1) {
+ d = this.childOffset - (visible - 1);
+ this.childOffset -= d;
+ this.childBase += d;
+ } else if (this.childOffset < 0) {
+ d = this.childOffset;
+ this.childOffset += -d;
+ this.childBase += d;
+ }
+
+ if (this.childBase < 0) {
+ this.childBase = 0;
+ } else if (this.childBase > this.baseLimit) {
+ this.childBase = this.baseLimit;
+ }
+
+ // Find max "bottom" value for
+ // content and descendant elements.
+ // Scroll the content if necessary.
+ if (this.childBase === base) {
+ return this.emit('scroll');
+ }
+
+ // When scrolling text, we want to be able to handle SGR codes as well as line
+ // feeds. This allows us to take preformatted text output from other programs
+ // and put it in a scrollable text box.
+ this.parseContent();
+
+ // XXX
+ // max = this.getScrollHeight() - (this.height - this.iheight);
+
+ max = this._clines.length - (this.height - this.iheight);
+ if (max < 0) max = 0;
+ emax = this._scrollBottom() - (this.height - this.iheight);
+ if (emax < 0) emax = 0;
+
+ this.childBase = Math.min(this.childBase, Math.max(emax, max));
+
+ if (this.childBase < 0) {
+ this.childBase = 0;
+ } else if (this.childBase > this.baseLimit) {
+ this.childBase = this.baseLimit;
+ }
+
+ // Optimize scrolling with CSR + IL/DL.
+ p = this.lpos;
+ // Only really need _getCoords() if we want
+ // to allow nestable scrolling elements...
+ // or if we **really** want shrinkable
+ // scrolling elements.
+ // p = this._getCoords();
+ if (p && this.childBase !== base && this.screen.cleanSides(this)) {
+ t = p.yi + this.itop;
+ b = p.yl - this.ibottom - 1;
+ d = this.childBase - base;
+
+ if (d > 0 && d < visible) {
+ // scrolled down
+ this.screen.deleteLine(d, t, t, b);
+ } else if (d < 0 && -d < visible) {
+ // scrolled up
+ d = -d;
+ this.screen.insertLine(d, t, t, b);
+ }
+ }
+
+ return this.emit('scroll');
+};
+
+ScrollableBox.prototype._recalculateIndex = function() {
+ var max, emax;
+
+ if (this.detached || !this.scrollable) {
+ return 0;
+ }
+
+ // XXX
+ // max = this.getScrollHeight() - (this.height - this.iheight);
+
+ max = this._clines.length - (this.height - this.iheight);
+ if (max < 0) max = 0;
+ emax = this._scrollBottom() - (this.height - this.iheight);
+ if (emax < 0) emax = 0;
+
+ this.childBase = Math.min(this.childBase, Math.max(emax, max));
+
+ if (this.childBase < 0) {
+ this.childBase = 0;
+ } else if (this.childBase > this.baseLimit) {
+ this.childBase = this.baseLimit;
+ }
+};
+
+ScrollableBox.prototype.resetScroll = function() {
+ if (!this.scrollable) return;
+ this.childOffset = 0;
+ this.childBase = 0;
+ return this.emit('scroll');
+};
+
+ScrollableBox.prototype.getScrollHeight = function() {
+ return Math.max(this._clines.length, this._scrollBottom());
+};
+
+ScrollableBox.prototype.getScrollPerc = function(s) {
+ var pos = this.lpos || this._getCoords();
+ if (!pos) return s ? -1 : 0;
+
+ var height = (pos.yl - pos.yi) - this.iheight
+ , i = this.getScrollHeight()
+ , p;
+
+ if (height < i) {
+ if (this.alwaysScroll) {
+ p = this.childBase / (i - height);
+ } else {
+ p = (this.childBase + this.childOffset) / (i - 1);
+ }
+ return p * 100;
+ }
+
+ return s ? -1 : 0;
+};
+
+ScrollableBox.prototype.setScrollPerc = function(i) {
+ // XXX
+ // var m = this.getScrollHeight();
+ var m = Math.max(this._clines.length, this._scrollBottom());
+ return this.scrollTo((i / 100) * m | 0);
+};
+
+module.exports = ScrollableBox;
diff --git a/lib/widgets/scrollabletext.js b/lib/widgets/scrollabletext.js
new file mode 100644
index 0000000..2e27bc0
--- /dev/null
+++ b/lib/widgets/scrollabletext.js
@@ -0,0 +1,33 @@
+/**
+ * scrollabletext.js - scrollable text element for blessed
+ * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
+ * https://github.com/chjj/blessed
+ */
+
+/**
+ * Modules
+ */
+
+var helpers = require('../helpers');
+
+var Node = require('./node');
+var ScrollableBox = require('./scrollablebox');
+
+/**
+ * ScrollableText
+ */
+
+function ScrollableText(options) {
+ if (!(this instanceof Node)) {
+ return new ScrollableText(options);
+ }
+ options = options || {};
+ options.alwaysScroll = true;
+ ScrollableBox.call(this, options);
+}
+
+ScrollableText.prototype.__proto__ = ScrollableBox.prototype;
+
+ScrollableText.prototype.type = 'scrollable-text';
+
+module.exports = ScrollableText;
diff --git a/lib/widgets/table.js b/lib/widgets/table.js
new file mode 100644
index 0000000..68e3c82
--- /dev/null
+++ b/lib/widgets/table.js
@@ -0,0 +1,330 @@
+/**
+ * table.js - table element for blessed
+ * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
+ * https://github.com/chjj/blessed
+ */
+
+/**
+ * Modules
+ */
+
+var helpers = require('../helpers');
+
+var Node = require('./node');
+var Box = require('./box');
+
+/**
+ * Table
+ */
+
+function Table(options) {
+ var self = this;
+
+ if (!(this instanceof Node)) {
+ return new Table(options);
+ }
+
+ options = options || {};
+ options.shrink = true;
+ options.style = options.style || {};
+ options.style.border = options.style.border || {};
+ options.style.header = options.style.header || {};
+ options.style.cell = options.style.cell || {};
+ options.align = options.align || 'center';
+
+ // Regular tables do not get custom height (this would
+ // require extra padding). Maybe add in the future.
+ delete options.height;
+
+ Box.call(this, options);
+
+ this.pad = options.pad != null
+ ? options.pad
+ : 2;
+
+ this.setData(options.rows || options.data);
+
+ this.on('resize', function() {
+ self.setContent('');
+ self.setData(self.rows);
+ self.screen.render();
+ });
+}
+
+Table.prototype.__proto__ = Box.prototype;
+
+Table.prototype.type = 'table';
+
+Table.prototype._calculateMaxes = function() {
+ var self = this;
+ var maxes = [];
+
+ this.rows.forEach(function(row) {
+ row.forEach(function(cell, i) {
+ var clen = self.strWidth(cell);
+ if (!maxes[i] || maxes[i] < clen) {
+ maxes[i] = clen;
+ }
+ });
+ });
+
+ var total = maxes.reduce(function(total, max) {
+ return total + max;
+ }, 0);
+ total += maxes.length + 1;
+
+ // XXX There might be an issue with resizing where on the first resize event
+ // width appears to be less than total if it's a percentage or left/right
+ // combination.
+ if (this.width < total) {
+ delete this.position.width;
+ }
+
+ if (this.position.width != null) {
+ var missing = this.width - total;
+ var w = missing / maxes.length | 0;
+ var wr = missing % maxes.length;
+ maxes = maxes.map(function(max, i) {
+ if (i === maxes.length - 1) {
+ return max + w + wr;
+ }
+ return max + w;
+ });
+ } else {
+ maxes = maxes.map(function(max) {
+ return max + self.pad;
+ });
+ }
+
+ return this._maxes = maxes;
+};
+
+Table.prototype.setRows =
+Table.prototype.setData = function(rows) {
+ var self = this
+ , text = ''
+ , line = ''
+ , align = this.align;
+
+ this.rows = rows || [];
+
+ this._calculateMaxes();
+
+ this.rows.forEach(function(row, i) {
+ var isHeader = i === 0;
+ var isFooter = i === self.rows.length - 1;
+ row.forEach(function(cell, i) {
+ var width = self._maxes[i];
+ var clen = self.strWidth(cell);
+
+ if (i !== 0) {
+ text += ' ';
+ }
+
+ while (clen < width) {
+ if (align === 'center') {
+ cell = ' ' + cell + ' ';
+ clen += 2;
+ } else if (align === 'left') {
+ cell = cell + ' ';
+ clen += 1;
+ } else if (align === 'right') {
+ cell = ' ' + cell;
+ clen += 1;
+ }
+ }
+
+ if (clen > width) {
+ if (align === 'center') {
+ cell = cell.substring(1);
+ clen--;
+ } else if (align === 'left') {
+ cell = cell.slice(0, -1);
+ clen--;
+ } else if (align === 'right') {
+ cell = cell.substring(1);
+ clen--;
+ }
+ }
+
+ text += cell;
+ });
+ if (!isFooter) {
+ text += '\n\n';
+ }
+ });
+
+ delete this.align;
+ this.setContent(text);
+ this.align = align;
+};
+
+Table.prototype.render = function() {
+ var self = this;
+
+ var coords = this._render();
+ if (!coords) return;
+
+ this._calculateMaxes();
+
+ if (!this._maxes) return coords;
+
+ var lines = this.screen.lines
+ , xi = coords.xi
+ , xl = coords.xl
+ , yi = coords.yi
+ , yl = coords.yl
+ , rx
+ , ry
+ , i;
+
+ var dattr = this.sattr(this.style)
+ , hattr = this.sattr(this.style.header)
+ , cattr = this.sattr(this.style.cell)
+ , battr = this.sattr(this.style.border);
+
+ var width = coords.xl - coords.xi - this.iright
+ , height = coords.yl - coords.yi - this.ibottom;
+
+ // Apply attributes to header cells and cells.
+ for (var y = this.itop; y < height; y++) {
+ if (!lines[yi + y]) break;
+ for (var x = this.ileft; x < width; x++) {
+ if (!lines[yi + y][xi + x]) break;
+ // Check to see if it's not the default attr. Allows for tags:
+ if (lines[yi + y][xi + x][0] !== dattr) continue;
+ if (y === this.itop) {
+ lines[yi + y][xi + x][0] = hattr;
+ } else {
+ lines[yi + y][xi + x][0] = cattr;
+ }
+ }
+ }
+
+ if (!this.border || this.options.noCellBorders) return coords;
+
+ // Draw border with correct angles.
+ ry = 0;
+ for (i = 0; i < self.rows.length + 1; i++) {
+ if (!lines[yi + ry]) break;
+ rx = 0;
+ self._maxes.forEach(function(max, i) {
+ rx += max;
+ if (i === 0) {
+ if (!lines[yi + ry][xi + 0]) return;
+ // left side
+ if (ry === 0) {
+ // top
+ lines[yi + ry][xi + 0][0] = battr;
+ // lines[yi + ry][xi + 0][1] = '\u250c'; // '┌'
+ } else if (ry / 2 === self.rows.length) {
+ // bottom
+ lines[yi + ry][xi + 0][0] = battr;
+ // lines[yi + ry][xi + 0][1] = '\u2514'; // '└'
+ } else {
+ // middle
+ lines[yi + ry][xi + 0][0] = battr;
+ lines[yi + ry][xi + 0][1] = '\u251c'; // '├'
+ // XXX If we alter iwidth and ileft for no borders - nothing should be written here
+ if (!self.border.left) {
+ lines[yi + ry][xi + 0][1] = '\u2500'; // '─'
+ }
+ }
+ } else if (i === self._maxes.length - 1) {
+ if (!lines[yi + ry][xi + rx + 1]) return;
+ // right side
+ if (ry === 0) {
+ // top
+ lines[yi + ry][xi + ++rx][0] = battr;
+ // lines[yi + ry][xi + rx][1] = '\u2510'; // '┐'
+ } else if (ry / 2 === self.rows.length) {
+ // bottom
+ lines[yi + ry][xi + ++rx][0] = battr;
+ // lines[yi + ry][xi + rx][1] = '\u2518'; // '┘'
+ } else {
+ // middle
+ lines[yi + ry][xi + ++rx][0] = battr;
+ lines[yi + ry][xi + rx][1] = '\u2524'; // '┤'
+ // XXX If we alter iwidth and iright for no borders - nothing should be written here
+ if (!self.border.right) {
+ lines[yi + ry][xi + rx][1] = '\u2500'; // '─'
+ }
+ }
+ return;
+ }
+ if (!lines[yi + ry][xi + rx + 1]) return;
+ // center
+ if (ry === 0) {
+ // top
+ lines[yi + ry][xi + ++rx][0] = battr;
+ lines[yi + ry][xi + rx][1] = '\u252c'; // '┬'
+ // XXX If we alter iheight and itop for no borders - nothing should be written here
+ if (!self.border.top) {
+ lines[yi + ry][xi + rx][1] = '\u2502'; // '│'
+ }
+ } else if (ry / 2 === self.rows.length) {
+ // bottom
+ lines[yi + ry][xi + ++rx][0] = battr;
+ lines[yi + ry][xi + rx][1] = '\u2534'; // '┴'
+ // XXX If we alter iheight and ibottom for no borders - nothing should be written here
+ if (!self.border.bottom) {
+ lines[yi + ry][xi + rx][1] = '\u2502'; // '│'
+ }
+ } else {
+ // middle
+ if (self.options.fillCellBorders) {
+ var lbg = (ry <= 2 ? hattr : cattr) & 0x1ff;
+ lines[yi + ry][xi + ++rx][0] = (battr & ~0x1ff) | lbg;
+ } else {
+ lines[yi + ry][xi + ++rx][0] = battr;
+ }
+ lines[yi + ry][xi + rx][1] = '\u253c'; // '┼'
+ // ++rx;
+ }
+ });
+ ry += 2;
+ }
+
+ // Draw internal borders.
+ for (ry = 1; ry < self.rows.length * 2; ry++) {
+ if (!lines[yi + ry]) break;
+ rx = 0;
+ self._maxes.slice(0, -1).forEach(function(max, i) {
+ rx += max;
+ if (!lines[yi + ry][xi + rx + 1]) return;
+ if (ry % 2 !== 0) {
+ if (self.options.fillCellBorders) {
+ var lbg = (ry <= 2 ? hattr : cattr) & 0x1ff;
+ lines[yi + ry][xi + ++rx][0] = (battr & ~0x1ff) | lbg;
+ } else {
+ lines[yi + ry][xi + ++rx][0] = battr;
+ }
+ lines[yi + ry][xi + rx][1] = '\u2502'; // '│'
+ } else {
+ rx++;
+ }
+ });
+ rx = 1;
+ self._maxes.forEach(function(max, i) {
+ while (max--) {
+ if (ry % 2 === 0) {
+ if (!lines[yi + ry]) break;
+ if (!lines[yi + ry][xi + rx + 1]) break;
+ if (self.options.fillCellBorders) {
+ var lbg = (ry <= 2 ? hattr : cattr) & 0x1ff;
+ lines[yi + ry][xi + rx][0] = (battr & ~0x1ff) | lbg;
+ } else {
+ lines[yi + ry][xi + rx][0] = battr;
+ }
+ lines[yi + ry][xi + rx][1] = '\u2500'; // '─'
+ }
+ rx++;
+ }
+ rx++;
+ });
+ }
+
+ return coords;
+};
+
+module.exports = Table;
diff --git a/lib/widgets/terminal.js b/lib/widgets/terminal.js
new file mode 100644
index 0000000..bcdce31
--- /dev/null
+++ b/lib/widgets/terminal.js
@@ -0,0 +1,380 @@
+/**
+ * terminal.js - term.js terminal element for blessed
+ * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
+ * https://github.com/chjj/blessed
+ */
+
+/**
+ * Modules
+ */
+
+var nextTick = global.setImmediate || process.nextTick.bind(process);
+
+var helpers = require('../helpers');
+
+var Node = require('./node');
+var Box = require('./box');
+
+/**
+ * Terminal
+ */
+
+function Terminal(options) {
+ var self = this;
+
+ if (!(this instanceof Node)) {
+ return new Terminal(options);
+ }
+
+ options = options || {};
+ options.scrollable = false;
+
+ Box.call(this, options);
+
+ this.handler = options.handler;
+ this.shell = options.shell || process.env.SHELL || 'sh';
+ this.args = options.args || [];
+
+ this.cursor = this.options.cursor;
+ this.cursorBlink = this.options.cursorBlink;
+ this.screenKeys = this.options.screenKeys;
+
+ this.style = this.style || {};
+ this.style.bg = this.style.bg || 'default';
+ this.style.fg = this.style.fg || 'default';
+
+ this.bootstrap();
+}
+
+Terminal.prototype.__proto__ = Box.prototype;
+
+Terminal.prototype.type = 'terminal';
+
+Terminal.prototype.bootstrap = function() {
+ var self = this;
+
+ var element = {
+ // window
+ get document() { return element; },
+ navigator: { userAgent: 'node.js' },
+
+ // document
+ get defaultView() { return element; },
+ get documentElement() { return element; },
+ createElement: function() { return element; },
+
+ // element
+ get ownerDocument() { return element; },
+ addEventListener: function() {},
+ removeEventListener: function() {},
+ getElementsByTagName: function(name) { return [element]; },
+ getElementById: function() { return element; },
+ parentNode: null,
+ offsetParent: null,
+ appendChild: function() {},
+ removeChild: function() {},
+ setAttribute: function() {},
+ getAttribute: function() {},
+ style: {},
+ focus: function() {},
+ blur: function() {},
+ console: console
+ };
+
+ element.parentNode = element;
+ element.offsetParent = element;
+
+ this.term = require('term.js')({
+ termName: 'xterm',
+ cols: this.width - this.iwidth,
+ rows: this.height - this.iheight,
+ context: element,
+ document: element,
+ body: element,
+ parent: element,
+ cursorBlink: this.cursorBlink,
+ screenKeys: this.screenKeys
+ });
+
+ this.term.refresh = function() {
+ self.screen.render();
+ };
+
+ this.term.keyDown = function() {};
+ this.term.keyPress = function() {};
+
+ this.term.open(element);
+
+ // Emits key sequences in html-land.
+ // Technically not necessary here.
+ // In reality if we wanted to be neat, we would overwrite the keyDown and
+ // keyPress methods with our own node.js-keys->terminal-keys methods, but
+ // since all the keys are already coming in as escape sequences, we can just
+ // send the input directly to the handler/socket (see below).
+ // this.term.on('data', function(data) {
+ // self.handler(data);
+ // });
+
+ // Incoming keys and mouse inputs.
+ // NOTE: Cannot pass mouse events - coordinates will be off!
+ this.screen.program.input.on('data', this._onData = function(data) {
+ if (self.screen.focused === self && !self._isMouse(data)) {
+ self.handler(data);
+ }
+ });
+
+ this.onScreenEvent('mouse', function(data) {
+ if (self.screen.focused !== self) return;
+
+ if (data.x < self.aleft + self.ileft) return;
+ if (data.y < self.atop + self.itop) return;
+ if (data.x > self.aleft - self.ileft + self.width) return;
+ if (data.y > self.atop - self.itop + self.height) return;
+
+ if (self.term.x10Mouse
+ || self.term.vt200Mouse
+ || self.term.normalMouse
+ || self.term.mouseEvents
+ || self.term.utfMouse
+ || self.term.sgrMouse
+ || self.term.urxvtMouse) {
+ ;
+ } else {
+ return;
+ }
+
+ var b = data.raw[0]
+ , x = data.x - self.aleft
+ , y = data.y - self.atop
+ , s;
+
+ if (self.term.urxvtMouse) {
+ if (self.screen.program.sgrMouse) {
+ b += 32;
+ }
+ s = '\x1b[' + b + ';' + (x + 32) + ';' + (y + 32) + 'M';
+ } else if (self.term.sgrMouse) {
+ if (!self.screen.program.sgrMouse) {
+ b -= 32;
+ }
+ s = '\x1b[<' + b + ';' + x + ';' + y
+ + (data.action === 'mousedown' ? 'M' : 'm');
+ } else {
+ if (self.screen.program.sgrMouse) {
+ b += 32;
+ }
+ s = '\x1b[M'
+ + String.fromCharCode(b)
+ + String.fromCharCode(x + 32)
+ + String.fromCharCode(y + 32);
+ }
+
+ self.handler(s);
+ });
+
+ this.on('focus', function() {
+ self.term.focus();
+ });
+
+ this.on('blur', function() {
+ self.term.blur();
+ });
+
+ this.term.on('title', function(title) {
+ self.title = title;
+ self.emit('title', title);
+ });
+
+ this.on('resize', function() {
+ nextTick(function() {
+ self.term.resize(self.width - self.iwidth, self.height - self.iheight);
+ });
+ });
+
+ this.once('render', function() {
+ self.term.resize(self.width - self.iwidth, self.height - self.iheight);
+ });
+
+ if (this.handler) {
+ return;
+ }
+
+ this.pty = require('pty.js').fork(this.shell, this.args, {
+ name: 'xterm',
+ cols: this.width - this.iwidth,
+ rows: this.height - this.iheight,
+ cwd: process.env.HOME,
+ env: process.env
+ });
+
+ this.on('resize', function() {
+ nextTick(function() {
+ self.pty.resize(self.width - self.iwidth, self.height - self.iheight);
+ });
+ });
+
+ this.handler = function(data) {
+ self.pty.write(data);
+ self.screen.render();
+ };
+
+ this.pty.on('data', function(data) {
+ self.write(data);
+ self.screen.render();
+ });
+
+ this.pty.on('exit', function(code) {
+ self.emit('exit', code || null);
+ });
+
+ this.onScreenEvent('keypress', function() {
+ self.screen.render();
+ });
+
+ this.screen._listenKeys(this);
+
+ this.on('destroy', function() {
+ self.screen.program.removeListener('data', self._onData);
+ self.pty.destroy();
+ });
+};
+
+Terminal.prototype.write = function(data) {
+ return this.term.write(data);
+};
+
+Terminal.prototype.render = function() {
+ var ret = this._render();
+ if (!ret) return;
+
+ this.dattr = this.sattr(this.style);
+
+ var xi = ret.xi + this.ileft
+ , xl = ret.xl - this.iright
+ , yi = ret.yi + this.itop
+ , yl = ret.yl - this.ibottom
+ , cursor;
+
+ var scrollback = this.term.lines.length - (yl - yi);
+
+ for (var y = Math.max(yi, 0); y < yl; y++) {
+ var line = this.screen.lines[y];
+ if (!line || !this.term.lines[scrollback + y - yi]) break;
+
+ if (y === yi + this.term.y
+ && this.term.cursorState
+ && this.screen.focused === this
+ && (this.term.ydisp === this.term.ybase || this.term.selectMode)
+ && !this.term.cursorHidden) {
+ cursor = xi + this.term.x;
+ } else {
+ cursor = -1;
+ }
+
+ for (var x = Math.max(xi, 0); x < xl; x++) {
+ if (!line[x] || !this.term.lines[scrollback + y - yi][x - xi]) break;
+
+ line[x][0] = this.term.lines[scrollback + y - yi][x - xi][0];
+
+ if (x === cursor) {
+ if (this.cursor === 'line') {
+ line[x][0] = this.dattr;
+ line[x][1] = '\u2502';
+ continue;
+ } else if (this.cursor === 'underline') {
+ line[x][0] = this.dattr | (2 << 18);
+ } else if (this.cursor === 'block' || !this.cursor) {
+ line[x][0] = this.dattr | (8 << 18);
+ }
+ }
+
+ line[x][1] = this.term.lines[scrollback + y - yi][x - xi][1];
+
+ // default foreground = 257
+ if (((line[x][0] >> 9) & 0x1ff) === 257) {
+ line[x][0] &= ~(0x1ff << 9);
+ line[x][0] |= ((this.dattr >> 9) & 0x1ff) << 9;
+ }
+
+ // default background = 256
+ if ((line[x][0] & 0x1ff) === 256) {
+ line[x][0] &= ~0x1ff;
+ line[x][0] |= this.dattr & 0x1ff;
+ }
+ }
+
+ line.dirty = true;
+ }
+
+ return ret;
+};
+
+Terminal.prototype._isMouse = function(buf) {
+ var s = buf;
+ 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');
+ }
+ }
+ return (buf[0] === 0x1b && buf[1] === 0x5b && buf[2] === 0x4d)
+ || /^\x1b\[M([\x00\u0020-\uffff]{3})/.test(s)
+ || /^\x1b\[(\d+;\d+;\d+)M/.test(s)
+ || /^\x1b\[<(\d+;\d+;\d+)([mM])/.test(s)
+ || /^\x1b\[<(\d+;\d+;\d+;\d+)&w/.test(s)
+ || /^\x1b\[24([0135])~\[(\d+),(\d+)\]\r/.test(s)
+ || /^\x1b\[(O|I)/.test(s);
+};
+
+Terminal.prototype.setScroll =
+Terminal.prototype.scrollTo = function(offset, always) {
+ this.term.ydisp = offset;
+ return this.emit('scroll');
+};
+
+Terminal.prototype.getScroll = function() {
+ return this.term.ydisp;
+};
+
+Terminal.prototype.scroll = function(offset, always) {
+ this.term.scrollDisp(offset);
+ return this.emit('scroll');
+};
+
+Terminal.prototype.resetScroll = function() {
+ this.term.ydisp = 0;
+ this.term.ybase = 0;
+ return this.emit('scroll');
+};
+
+Terminal.prototype.getScrollHeight = function() {
+ return this.term.rows - 1;
+};
+
+Terminal.prototype.getScrollPerc = function(s) {
+ return (this.term.ydisp / this.term.ybase) * 100;
+};
+
+Terminal.prototype.setScrollPerc = function(i) {
+ return this.setScroll((i / 100) * this.term.ybase | 0);
+};
+
+Terminal.prototype.screenshot = function(xi, xl, yi, yl) {
+ xi = 0 + (xi || 0);
+ if (xl != null) {
+ xl = 0 + (xl || 0);
+ } else {
+ xl = this.term.lines[0].length;
+ }
+ yi = 0 + (yi || 0);
+ if (yl != null) {
+ yl = 0 + (yl || 0);
+ } else {
+ yl = this.term.lines.length;
+ }
+ return this.screen.screenshot(xi, xl, yi, yl, this.term);
+};
+
+module.exports = Terminal;
diff --git a/lib/widgets/text.js b/lib/widgets/text.js
new file mode 100644
index 0000000..f912c29
--- /dev/null
+++ b/lib/widgets/text.js
@@ -0,0 +1,33 @@
+/**
+ * text.js - text element for blessed
+ * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
+ * https://github.com/chjj/blessed
+ */
+
+/**
+ * Modules
+ */
+
+var helpers = require('../helpers');
+
+var Node = require('./node');
+var Element = require('./element');
+
+/**
+ * Text
+ */
+
+function Text(options) {
+ if (!(this instanceof Node)) {
+ return new Text(options);
+ }
+ options = options || {};
+ options.shrink = true;
+ Element.call(this, options);
+}
+
+Text.prototype.__proto__ = Element.prototype;
+
+Text.prototype.type = 'text';
+
+module.exports = Text;
diff --git a/lib/widgets/textarea.js b/lib/widgets/textarea.js
new file mode 100644
index 0000000..cace23a
--- /dev/null
+++ b/lib/widgets/textarea.js
@@ -0,0 +1,340 @@
+/**
+ * textarea.js - textarea element for blessed
+ * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
+ * https://github.com/chjj/blessed
+ */
+
+/**
+ * Modules
+ */
+
+var unicode = require('../unicode');
+
+var nextTick = global.setImmediate || process.nextTick.bind(process);
+
+var helpers = require('../helpers');
+
+var Node = require('./node');
+var Input = require('./input');
+
+/**
+ * Textarea
+ */
+
+function Textarea(options) {
+ var self = this;
+
+ if (!(this instanceof Node)) {
+ return new Textarea(options);
+ }
+
+ options = options || {};
+
+ options.scrollable = options.scrollable !== false;
+
+ Input.call(this, options);
+
+ this.screen._listenKeys(this);
+
+ this.value = options.value || '';
+
+ this.__updateCursor = this._updateCursor.bind(this);
+ this.on('resize', this.__updateCursor);
+ this.on('move', this.__updateCursor);
+
+ if (options.inputOnFocus) {
+ this.on('focus', this.readInput.bind(this, null));
+ }
+
+ if (!options.inputOnFocus && options.keys) {
+ this.on('keypress', function(ch, key) {
+ if (self._reading) return;
+ if (key.name === 'enter' || (options.vi && key.name === 'i')) {
+ return self.readInput();
+ }
+ if (key.name === 'e') {
+ return self.readEditor();
+ }
+ });
+ }
+
+ if (options.mouse) {
+ this.on('click', function(data) {
+ if (self._reading) return;
+ if (data.button !== 'right') return;
+ self.readEditor();
+ });
+ }
+}
+
+Textarea.prototype.__proto__ = Input.prototype;
+
+Textarea.prototype.type = 'textarea';
+
+Textarea.prototype._updateCursor = function(get) {
+ if (this.screen.focused !== this) {
+ return;
+ }
+
+ var lpos = get ? this.lpos : this._getCoords();
+ if (!lpos) return;
+
+ var last = this._clines[this._clines.length - 1]
+ , program = this.screen.program
+ , line
+ , cx
+ , cy;
+
+ // Stop a situation where the textarea begins scrolling
+ // and the last cline appears to always be empty from the
+ // _typeScroll `+ '\n'` thing.
+ // Maybe not necessary anymore?
+ if (last === '' && this.value[this.value.length - 1] !== '\n') {
+ last = this._clines[this._clines.length - 2] || '';
+ }
+
+ line = Math.min(
+ this._clines.length - 1 - (this.childBase || 0),
+ (lpos.yl - lpos.yi) - this.iheight - 1);
+
+ // When calling clearValue() on a full textarea with a border, the first
+ // argument in the above Math.min call ends up being -2. Make sure we stay
+ // positive.
+ line = Math.max(0, line);
+
+ cy = lpos.yi + this.itop + line;
+ cx = lpos.xi + this.ileft + this.strWidth(last);
+
+ // XXX Not sure, but this may still sometimes
+ // cause problems when leaving editor.
+ if (cy === program.y && cx === program.x) {
+ return;
+ }
+
+ if (cy === program.y) {
+ if (cx > program.x) {
+ program.cuf(cx - program.x);
+ } else if (cx < program.x) {
+ program.cub(program.x - cx);
+ }
+ } else if (cx === program.x) {
+ if (cy > program.y) {
+ program.cud(cy - program.y);
+ } else if (cy < program.y) {
+ program.cuu(program.y - cy);
+ }
+ } else {
+ program.cup(cy, cx);
+ }
+};
+
+Textarea.prototype.input =
+Textarea.prototype.setInput =
+Textarea.prototype.readInput = function(callback) {
+ var self = this
+ , focused = this.screen.focused === this;
+
+ if (this._reading) return;
+ this._reading = true;
+
+ this._callback = callback;
+
+ if (!focused) {
+ this.screen.saveFocus();
+ this.focus();
+ }
+
+ this.screen.grabKeys = true;
+
+ this._updateCursor();
+ this.screen.program.showCursor();
+ //this.screen.program.sgr('normal');
+
+ this._done = function fn(err, value) {
+ if (!self._reading) return;
+
+ if (fn.done) return;
+ fn.done = true;
+
+ self._reading = false;
+
+ delete self._callback;
+ delete self._done;
+
+ self.removeListener('keypress', self.__listener);
+ delete self.__listener;
+
+ self.removeListener('blur', self.__done);
+ delete self.__done;
+
+ self.screen.program.hideCursor();
+ self.screen.grabKeys = false;
+
+ if (!focused) {
+ self.screen.restoreFocus();
+ }
+
+ if (self.options.inputOnFocus) {
+ self.screen.rewindFocus();
+ }
+
+ // Ugly
+ if (err === 'stop') return;
+
+ if (err) {
+ self.emit('error', err);
+ } else if (value != null) {
+ self.emit('submit', value);
+ } else {
+ self.emit('cancel', value);
+ }
+ self.emit('action', value);
+
+ if (!callback) return;
+
+ return err
+ ? callback(err)
+ : callback(null, value);
+ };
+
+ // Put this in a nextTick so the current
+ // key event doesn't trigger any keys input.
+ nextTick(function() {
+ self.__listener = self._listener.bind(self);
+ self.on('keypress', self.__listener);
+ });
+
+ this.__done = this._done.bind(this, null, null);
+ this.on('blur', this.__done);
+};
+
+Textarea.prototype._listener = function(ch, key) {
+ var done = this._done
+ , value = this.value;
+
+ if (key.name === 'return') return;
+ if (key.name === 'enter') {
+ ch = '\n';
+ }
+
+ // TODO: Handle directional keys.
+ if (key.name === 'left' || key.name === 'right'
+ || key.name === 'up' || key.name === 'down') {
+ ;
+ }
+
+ if (this.options.keys && key.ctrl && key.name === 'e') {
+ return this.readEditor();
+ }
+
+ // TODO: Optimize typing by writing directly
+ // to the screen and screen buffer here.
+ if (key.name === 'escape') {
+ done(null, null);
+ } else if (key.name === 'backspace') {
+ if (this.value.length) {
+ if (this.screen.fullUnicode) {
+ if (unicode.isSurrogate(this.value, this.value.length - 2)) {
+ // || unicode.isCombining(this.value, this.value.length - 1)) {
+ this.value = this.value.slice(0, -2);
+ } else {
+ this.value = this.value.slice(0, -1);
+ }
+ } else {
+ this.value = this.value.slice(0, -1);
+ }
+ }
+ } else if (ch) {
+ if (!/^[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]$/.test(ch)) {
+ this.value += ch;
+ }
+ }
+
+ if (this.value !== value) {
+ this.screen.render();
+ }
+};
+
+Textarea.prototype._typeScroll = function() {
+ // XXX Workaround
+ var height = this.height - this.iheight;
+ if (this._clines.length - this.childBase > height) {
+ this.scroll(this._clines.length);
+ }
+};
+
+Textarea.prototype.getValue = function() {
+ return this.value;
+};
+
+Textarea.prototype.setValue = function(value) {
+ if (value == null) {
+ value = this.value;
+ }
+ if (this._value !== value) {
+ this.value = value;
+ this._value = value;
+ this.setContent(this.value);
+ this._typeScroll();
+ this._updateCursor();
+ }
+};
+
+Textarea.prototype.clearInput =
+Textarea.prototype.clearValue = function() {
+ return this.setValue('');
+};
+
+Textarea.prototype.submit = function() {
+ if (!this.__listener) return;
+ return this.__listener('\x1b', { name: 'escape' });
+};
+
+Textarea.prototype.cancel = function() {
+ if (!this.__listener) return;
+ return this.__listener('\x1b', { name: 'escape' });
+};
+
+Textarea.prototype.render = function() {
+ this.setValue();
+ return this._render();
+};
+
+Textarea.prototype.editor =
+Textarea.prototype.setEditor =
+Textarea.prototype.readEditor = function(callback) {
+ var self = this;
+
+ if (this._reading) {
+ var _cb = this._callback
+ , cb = callback;
+
+ this._done('stop');
+
+ callback = function(err, value) {
+ if (_cb) _cb(err, value);
+ if (cb) cb(err, value);
+ };
+ }
+
+ if (!callback) {
+ callback = function() {};
+ }
+
+ return this.screen.readEditor({ value: this.value }, function(err, value) {
+ if (err) {
+ if (err.message === 'Unsuccessful.') {
+ self.screen.render();
+ return self.readInput(callback);
+ }
+ self.screen.render();
+ self.readInput(callback);
+ return callback(err);
+ }
+ self.setValue(value);
+ self.screen.render();
+ return self.readInput(callback);
+ });
+};
+
+module.exports = Textarea;
diff --git a/lib/widgets/textbox.js b/lib/widgets/textbox.js
new file mode 100644
index 0000000..13be880
--- /dev/null
+++ b/lib/widgets/textbox.js
@@ -0,0 +1,77 @@
+/**
+ * textbox.js - textbox element for blessed
+ * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
+ * https://github.com/chjj/blessed
+ */
+
+/**
+ * Modules
+ */
+
+var helpers = require('../helpers');
+
+var Node = require('./node');
+var Textarea = require('./textarea');
+
+/**
+ * Textbox
+ */
+
+function Textbox(options) {
+ var self = this;
+
+ if (!(this instanceof Node)) {
+ return new Textbox(options);
+ }
+
+ options = options || {};
+
+ options.scrollable = false;
+
+ Textarea.call(this, options);
+
+ this.secret = options.secret;
+ this.censor = options.censor;
+}
+
+Textbox.prototype.__proto__ = Textarea.prototype;
+
+Textbox.prototype.type = 'textbox';
+
+Textbox.prototype.__olistener = Textbox.prototype._listener;
+Textbox.prototype._listener = function(ch, key) {
+ if (key.name === 'enter') {
+ this._done(null, this.value);
+ return;
+ }
+ return this.__olistener(ch, key);
+};
+
+Textbox.prototype.setValue = function(value) {
+ var visible, val;
+ if (value == null) {
+ value = this.value;
+ }
+ if (this._value !== value) {
+ value = value.replace(/\n/g, '');
+ this.value = value;
+ this._value = value;
+ if (this.secret) {
+ this.setContent('');
+ } else if (this.censor) {
+ this.setContent(Array(this.value.length + 1).join('*'));
+ } else {
+ visible = -(this.width - this.iwidth - 1);
+ val = this.value.replace(/\t/g, this.screen.tabc);
+ this.setContent(val.slice(visible));
+ }
+ this._updateCursor();
+ }
+};
+
+Textbox.prototype.submit = function() {
+ if (!this.__listener) return;
+ return this.__listener('\r', { name: 'enter' });
+};
+
+module.exports = Textbox;