/** * widget.js - high-level interface for blessed * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License). * https://github.com/chjj/blessed */ /** * Modules */ var EventEmitter = require('./events').EventEmitter , assert = require('assert') , path = require('path') , util = require('util') , fs = require('fs'); var colors = require('./colors') , program = require('./program') , widget = exports; var east_asian_width = require('../vendor/east-asian-width'); 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); } } 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(); }; 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: 'bg', label: ' {bold}Debug Log{/bold} ', tags: true, keys: true, vi: true, mouse: true, style: { border: { bg: 'red' } }, 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 = 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); }); // 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.__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 (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; for (x = pos.xi - 1; x >= 0; x--) { first = this.olines[yi][x]; for (y = yi; y < yl; y++) { 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++) { first = this.olines[yi][x]; for (y = yi; y < yl; y++) { ch = this.olines[y][x]; if (ch[0] !== first[0] || ch[1] !== first[1]) { return pos._cleanSides = false; } } } return pos._cleanSides = true; }; Screen.prototype.draw = function(start, end) { // this.emit('predraw'); var x , y , line , out , ch , data , attr , fg , bg , flags , cwid , point; 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 < this.cols; 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 < this.cols; 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 : this.cols, // 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 < this.cols; 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. point = line[x][1].codePointAt(0); if (point <= 0xffff) { cwid = east_asian_width.char_width(point); if (cwid === 2) { // Might also need: `line[x + 1][0] !== line[x][0]` // for borderless boxes? if (x === line.length - 1 || angles[line[x + 1][1]]) { ch = ' '; o[x][1] = ' '; } else { o[++x][1] = ' '; } } else if (cwid === 0) { ch = ' '; } } } // 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) { return this.fillRegion(this.dattr, ' ', xi, xl, yi, yl); }; Screen.prototype.fillRegion = function(attr, ch, xi, xl, yi, yl) { var lines = this.lines , cell , xx; for (; yi < yl; yi++) { if (!lines[yi]) break; for (xx = xi; xx < xl; xx++) { cell = lines[yi][xx]; if (!cell) break; if (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 }; }; /** * 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 screen.on('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, listener) { var self = this; if (this.parent) { this.screen.on(type, listener); } this.on('attach', function() { self.screen.on(type, listener); }); this.on('detach', function() { self.screen.removeListener(type, listener); }); }; 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(wideChars, '$1 '); } else { // no double-width or surrogate pairs: replace them with question-marks. content = content.replace(wideChars, '??'); content = content.replace(/[\ud800-\udbff][\udc00-\udfff]/g, '?'); } 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; }; Element.prototype.textLength = function(text) { // return east_asian_width.str_width(text); if (!this.parseTags) return text.length; return text .replace(/{(\/?)([\w\-,;!#]*)}/g, '') .replace(/\x1b\[[\d;]*m/g, '') .length; }; // 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; } // 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; } 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); // Find all surrogate pairs and compensate for the lack of width // on the line by padding with trailing spaces: if (this.screen.fullUnicode) { for (var i = 0; i < out.length; i++) { // NOTE: Happens at 54 cols with all chars enabled in test. // Check to see if surrogates got split on end and beginning of 2 lines. if (/[\ud800-\udbff]$/.exec(out[i]) && /^[\udc00-\udfff]/.exec(out[i + 1])) { out[i] = out[i] + out[i + 1][0]; out[i + 1] = out[i + 1].substring(1) + ' '; } // Pad the end of the lines if the surrogate is not a double-width char. // var surrogates = out[i].length - punycode.ucs2.decode(out[i]).length; var surrogates = out[i].match(/[\ud800-\udbff][\udc00-\udfff]/g); if (surrogates && surrogates.length) { for (var j = 0; j < surrogates.length; j++) { if (east_asian_width.char_width(surrogates[j].codePointAt(0)) === 1) { out[i] += ' '; } } } } } return out; }; Element.prototype.__defineGetter__('visible', function() { var el = this; do { if (el.detached) return false; if (el.hidden) 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() : this.disableDrag(); }); Element.prototype.enableDrag = function() { var self = this; if (this._draggable) return true; this.enableMouse(); this.on('mousedown', this._dragMD = function(data) { if (self._drag) return; self._drag = { x: data.x - self.aleft, y: data.y - self.atop }; }); this.screen.on('mouse', this._dragM = function(data) { if (!self._drag) return; if (data.action !== 'mousedown') { delete self._drag; 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; self.rleft = x; self.rtop = y; self.screen.render(); }); return this._draggable = true; }; Element.prototype.disableDrag = function() { if (!this._draggable) return false; delete this._drag; this.removeListener('mousedown', this._dragMD); this.screen.removeListener('mouse', this._dragM); return this._draggable = false; }; Element.prototype.key = function() { return this.screen.program.key.apply(this, arguments); }; Element.prototype.onceKey = function() { return this.screen.program.onceKey.apply(this, arguments); }; Element.prototype.unkey = Element.prototype.removeKey = function() { return this.screen.program.unkey.apply(this, arguments); }; 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) { if (this.detached) return; var lpos = this._getCoords(get); if (!lpos) return; this.screen.clearRegion( lpos.xi, lpos.xl, lpos.yi, lpos.yl); }; 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; } this.on('scroll', function() { var visible = self.height - self.iheight; self._label.rtop = self.childBase - (self.border ? 1 : 0); if (!self.screen.autoPadding) { self._label.rtop = self.childBase; } self.screen.render(); }); }; Element.prototype.removeLabel = function() { this._label.detach(); delete this._label; }; Element.prototype.setHover = function(options) { var self = this; if (typeof options === 'string') { options = { text: options }; } if (this._hover) { this._hover.setContent(options.text); return; } this._hover = new Box({ screen: this.screen, content: options.text, left: 0, top: 0, tags: this.parseTags, height: 'shrink', width: 'shrink', border: 'line', style: { border: { fg: 'default' }, bg: 'default', fg: 'default' } }); this._hover._isHover = true; this._hover._.over = false; this.on('mouseover', function(data) { if (!self._hover) return; self._hover._.over = true; }); this.on('mouse', function(data) { if (!self._hover) return; // XXX Does not work as well as it should: // if (!self._hover._.over) return; var el = self._hover , x = data.x , y = data.y; self.screen.append(el); while (el = el.parent) { x -= el.rleft; y -= el.rtop; } self._hover.rleft = x + 1; self._hover.rtop = y; self.screen.render(); }); this.on('mouseout', function() { if (!self._hover) return; self._hover._.over = false; self._hover.detach(); self.screen.render(); }); this.screen.on('element mouseover', function(el) { if (!self._hover) return; if (el === self || el === self._hover) return; self._hover._.over = false; self._hover.detach(); self.screen.render(); }); }; Element.prototype.removeHover = function() { this._hover.detach(); delete this._hover; }; /** * 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). Screen.prototype._getPos = function() { return this; }; 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 || 0 , 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) { 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 || 0 , 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) { 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; } 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 = yi; y < yl; y++) { if (!lines[y]) break; for (x = xi; x < xl; x++) { lines[y][x][0] = 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) { 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] = 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; } // Handle surrogate pairs: // Make sure we put surrogate pair chars in one cell. if (this.screen.fullUnicode && content[ci - 1]) { var code = content[ci - 1].charCodeAt(0); // if (content.codePointAt(ci - 1) > 0xffff) { if (code >= 0xd800 && code <= 0xdbff) { var code2 = (content[ci] || '').charCodeAt(0); if (code2 >= 0xdc00 && code2 <= 0xdfff) { ch = content[ci - 1] + content[ci]; ci++; } else { ch = bch; } } } if (this.style.transparent) { lines[y][x][0] = 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) { 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 = yi + 1; for (; y < yl + 1; y++) { if (!lines[y]) continue; x = xl; for (; x < xl + 2; x++) { if (!lines[y][x]) continue; // lines[y][x][0] = blend(this.dattr, lines[y][x][0]); lines[y][x][0] = blend(lines[y][x][0]); lines[y].dirty = true; } } // bottom y = yl; for (; y < yl + 1; y++) { if (!lines[y]) continue; for (x = xi + 1; x < xl; x++) { if (!lines[y][x]) continue; // lines[y][x][0] = blend(this.dattr, lines[y][x][0]); lines[y][x][0] = 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; }; 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] = getAngle(lines, x, y); } } } }; Element.prototype._render = Element.prototype.render; 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(); }; /** * 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; } } 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._scrollBottom()); 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._scrollBottom() > 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); 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(); 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; } 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) { 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 === '/') { if (typeof self.options.search !== 'function') { return; } return self.options.search(function(searchString) { self.select(self.fuzzyFind(searchString)); 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; }; }); 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(); }); } }; List.prototype.fuzzyFind = function(search) { var index = this.getItemIndex(this.selected); for (var i = 0; i < this.ritems.length; i++){ if (this.ritems[i].indexOf(search) === 0) { return i; } } return index; }; List.prototype.getItemIndex = function(child) { if (typeof child === 'number') { return child; } else if (typeof child === 'string') { return this.ritems.indexOf(child); } 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); } } }; 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)); } }; 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 = this.ritems[this.selected]; if (!this.parent) return; this.scrollTo(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, 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 + last.length; // 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) { 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 = asort(dirs); files = asort(files); list = dirs.concat(files).map(function(data) { return data.text; }); self.setItems(list); self.select(0); self.screen.render(); 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.screen.on('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.screen.removeListener('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.screen.on('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.screen.removeListener('keypress', fn); end(); }); if (!self.options.mouse) return; self.screen.on('mouse', function fn(data) { if (data.action === 'mousemove') return; self.screen.removeListener('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.screen.on('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); }); }; 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 = 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: 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(); }); } }; 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; } } }; 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); } } }; 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(); } }; /** * 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.textLength(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.textLength(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.textLength(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', function(data) { if (self.screen.focused === self && !self._isMouse(data)) { self.handler(data); } }); this.screen.on('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); }); 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.screen.on('keypress', function() { self.screen.render(); }); }; 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 = yi; 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 = xi; 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); }; /** * 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 = findFile('/usr', 'w3mimgdisplay') || findFile('/lib', 'w3mimgdisplay') || 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); }); 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.render = function() { var ret = this._render(); if (!ret) return; if (!this._noImage) { this.setImage(this.file); } return ret; }; 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 (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.')); } this.file = img; function renderImage(ratio, callback) { // 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(); }); } return this.getPixelRatio(function(err, ratio) { if (err) { if (!callback) return; return callback(err); } return renderImage(ratio, function(err, success) { if (err) { 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) { 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) { 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; return renderImage(ratio, callback); }); } if (!callback) return; return callback(null, success); }); }); }; Image.prototype.clearImage = function(callback) { var self = this; 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; 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 (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 (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; // XXX We could cache this, but sometimes it's better // to recalculate to be pixel perfect. // if (this._ratio) { // 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 }; return callback(null, self._ratio); }); }; Image.prototype.displayImage = function(callback) { return this.screen.displayImage(this.file, callback); }; /** * Helpers */ function generateTags(style, text) { var open = '' , close = ''; Object.keys(style || {}).forEach(function(key) { var val = style[key]; if (typeof val === 'string') { val = val.replace(/^light(?!-)/, 'light-'); 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 }; } function merge(a, b) { Object.keys(b).forEach(function(key) { a[key] = b[key]; }); return a; } function asort(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); }); } function hsort(obj) { return obj.sort(function(a, b) { return b.index - a.index; }); } // NOTE: 0x20000 - 0x2fffd and 0x30000 - 0x3fffd are not necessary for this // regex anyway. This regex is used to put a blank char after wide chars to // be eaten, however, if this is a surrogate pair, parseContent already adds // the extra one char because its length equals 2 instead of 1. var wideChars = new RegExp('(' // 0x20000 - 0x2fffd: // + '[\\ud840-\\ud87f][\\udc00-\\udffd]' // + '|' // 0x30000 - 0x3fffd: // + '[\\ud880-\\ud8bf][\\udc00-\\udffd]' // + '|' + '[' + '\\u1100-\\u115f' /* Hangul Jamo init. consonants */ + '\\u2329\\u232a' + '\\u2e80-\\u303e\\u3040-\\ua4cf' /* CJK ... Yi */ + '\\uac00-\\ud7a3' /* Hangul Syllables */ + '\\uf900-\\ufaff' /* CJK Compatibility Ideographs */ + '\\ufe10-\\ufe19' /* Vertical forms */ + '\\ufe30-\\ufe6f' /* CJK Compatibility Forms */ + '\\uff00-\\uff60' /* Fullwidth Forms */ + '\\uffe0-\\uffe6' // + '\\u20000-\\u2fffd' // + '\\u30000-\\u3fffd' + ']' + ')', 'g'); function findFile(start, target) { return (function read(dir) { var files, file, stat, out; if (dir === '/dev' || dir === '/sys' || dir === '/proc') { 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); } 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 // '│' }; // 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]; }); function getAngle(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 (lines[y][x - 1][0] !== attr) return ch; angle |= 1 << 3; } if (lines[y - 1] && uangles[lines[y - 1][x][1]]) { if (lines[y - 1][x][0] !== attr) return ch; angle |= 1 << 2; } if (lines[y][x + 1] && rangles[lines[y][x + 1][1]]) { if (lines[y][x + 1][0] !== attr) return ch; angle |= 1 << 1; } if (lines[y + 1] && dangles[lines[y + 1][x][1]]) { if (lines[y + 1][x][0] !== attr) return ch; angle |= 1 << 0; } return angleTable[angle] || ch; } 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; } blend._cache = {}; /** * Helpers */ var helpers = {}; // 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 = generateTags; helpers.textLength = function(text) { return Element.prototype.textLength.call({ parseTags: true }, text); }; helpers.attrToBinary = function(style, element) { return Element.prototype.sattr.call(element || {}, style); }; helpers.merge = merge; helpers.asort = asort; helpers.findFile = findFile; /** * 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;