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;