diff --git a/README.md b/README.md index 240d8e4..eca533c 100644 --- a/README.md +++ b/README.md @@ -287,6 +287,7 @@ The base element. - **focused** - element is focused. - **hidden** - whether the element is hidden. - **label** - a simple text label for the element. +- **hoverText** - a floating text label for the element which appears on mouseover. - **align** - text alignment: `left`, `center`, or `right`. - **valign** - vertical text alignment: `top`, `middle`, or `bottom`. - **shrink** - shrink/flex/grow to content and child elements. width/height @@ -367,8 +368,12 @@ The base element. - **setIndex(z)** - set the z-index of the element (changes rendering order). - **setFront()** - put the element in front of its siblings. - **setBack()** - put the element in back of its siblings. -- **setLabel(text)** - set the label text for the top-left corner. +- **setLabel(text/options)** - set the label text for the top-left corner. + example options: `{text:'foo',side:'left'}` - **removeLabel()** - remove the label completely. +- **setHover(text/options)** - set the hover text for the bottom-right corner. + example options: `{text:'foo'}` +- **removeHover()** - remove the hover label completely. ###### Content Methods @@ -846,6 +851,28 @@ A radio button which can be used in a form element. - inherits all from Checkbox. +#### Terminal (from Box) + +A box which spins up a pseudo terminal and renders the output. (Requires +term.js to be installed). + +##### Options: + +- inherits all from Box. + +##### Properties: + +- inherits all from Box. + +##### Events: + +- inherits all from Box. + +##### Methods: + +- inherits all from Box. + + ### Positioning Offsets may be a number, a percentage (e.g. `50%`), or a keyword (e.g. diff --git a/example/multiplex.js b/example/multiplex.js new file mode 100755 index 0000000..592ad82 --- /dev/null +++ b/example/multiplex.js @@ -0,0 +1,85 @@ +#!/usr/bin/env node + +/** + * multiplex.js + * https://github.com/chjj/blessed + * Copyright (c) 2013-2015, Christopher Jeffrey (MIT License) + * A terminal multiplexer created by blessed. + */ + +process.title = 'multiplex.js'; + +var blessed = require('blessed') + , pty = require('pty.js'); + +var screen = blessed.screen(); + +var terminals = []; + +var pty0 = pty.fork('bash', [], { + name: process.env.TERM, + cols: process.stdout.columns, + rows: process.stdout.rows, + cwd: process.env.HOME, + env: process.env +}); + +terminals[0] = blessed.terminal({ + parent: screen, + //mouse: true, + left: 0, + top: 2, + bottom: 2, + width: '40%', + border: 'line', + handler: function(data) { + pty0.write(data); + screen.render(); + } +}); + +pty0.on('data', function(data) { + terminals[0].write(data); + //terminals[0].scrollTo(terminals[0]._scrollBottom()); + screen.render(); +}); + +var pty1 = pty.fork('bash', [], { + name: process.env.TERM, + cols: process.stdout.columns, + rows: process.stdout.rows, + cwd: process.env.HOME, + env: process.env +}); + +terminals[1] = blessed.terminal({ + parent: screen, + //mouse: true, + right: 2, + top: 2, + bottom: 2, + width: '40%', + border: 'line', + handler: function(data) { + pty1.write(data); + screen.render(); + } +}); + +pty1.on('data', function(data) { + terminals[1].write(data); + //terminals[1].scrollTo(terminals[1]._scrollBottom()); + screen.render(); +}); + +terminals[0].focus(); + +screen.on('keypress', function() { + screen.render(); +}); + +screen.key('C-c', function() { + process.exit(0); +}); + +screen.render(); diff --git a/lib/colors.js b/lib/colors.js index 6c2d60f..769f959 100644 --- a/lib/colors.js +++ b/lib/colors.js @@ -37,7 +37,7 @@ exports.match = function(r1, g1, b1) { g2 = c[1]; b2 = c[2]; - diff = colorDistance3dWeight(r1, g1, b1, r2, g2, b2); + diff = colorDistance(r1, g1, b1, r2, g2, b2); if (diff === 0) { li = i; @@ -85,49 +85,16 @@ exports.hexToRGB = function(hex) { // As it happens, comparing how similar two colors are is really hard. Here is // one of the simplest solutions, which doesn't require conversion to another -// color space, posted on stackoverflow. Maybe someone better at math can +// color space, posted on stackoverflow[1]. Maybe someone better at math can // propose a superior solution. +// [1] http://stackoverflow.com/questions/1633828 -// http://stackoverflow.com/questions/9018016 -function colorDistance3d(r1, g1, b1, r2, g2, b2) { - var d; - - d = Math.sqrt( - Math.pow(r2 - r1, 2) - + Math.pow(g2 - g1, 2) - + Math.pow(b2 - b1, 2)); - - d /= Math.sqrt( - Math.pow(255, 2) - + Math.pow(255, 2) - + Math.pow(255, 2)); - - return d; -} - -// http://stackoverflow.com/questions/1633828 -function colorDistance3dWeight(r1, g1, b1, r2, g2, b2) { +function colorDistance(r1, g1, b1, r2, g2, b2) { return Math.pow(30 * (r1 - r2), 2) + Math.pow(59 * (g1 - g2), 2) + Math.pow(11 * (b1 - b2), 2); } -function colorDistance3dWeightSqrt(r1, g1, b1, r2, g2, b2) { - var d; - - d = Math.sqrt( - Math.pow(30 * (r2 - r1), 2) - + Math.pow(59 * (g2 - g1), 2) - + Math.pow(11 * (b2 - b1), 2)); - - d /= Math.sqrt( - Math.pow(255, 2) - + Math.pow(255, 2) - + Math.pow(255, 2)); - - return d; -} - exports._cache = {}; // XTerm Colors diff --git a/lib/widget.js b/lib/widget.js index 7025027..17291b7 100644 --- a/lib/widget.js +++ b/lib/widget.js @@ -1617,6 +1617,7 @@ Screen.prototype.setEffects = function(el, fel, over, out, effects, temp) { 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]; @@ -1636,6 +1637,7 @@ Screen.prototype.setEffects = function(el, fel, over, out, effects, temp) { 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]; @@ -1792,6 +1794,10 @@ function Element(options) { 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(' '); @@ -2340,29 +2346,55 @@ Element.prototype.clearPos = function(get) { lpos.yi, lpos.yl); }; -Element.prototype.setLabel = function(text) { +Element.prototype.setLabel = function(options) { var self = this; + if (typeof options === 'string') { + options = { text: options }; + } + if (this._label) { - this._label.setContent(text); + 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: text, - left: 2 + (this.border ? -1 : 0), + 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) { - this._label.rleft = 2; + if (options.side !== 'right') { + this._label.rleft = 2; + } else { + this._label.rright = 2; + } this._label.rtop = 0; } @@ -2381,6 +2413,75 @@ Element.prototype.removeLabel = function() { 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) { + self._hover._.over = true; + }); + + this.on('mouse', function(data) { + 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; + self._hover.rtop = y; + + self.screen.render(); + }); + + this.on('mouseout', function() { + self._hover._.over = false; + self._hover.detach(); + self.screen.render(); + }); +}; + +Element.prototype.removeHover = function() { + this._hover.detach(); + delete this._hover; +}; + /** * Positioning */ @@ -5644,7 +5745,9 @@ Prompt.prototype.__proto__ = Box.prototype; Prompt.prototype.type = 'prompt'; -Prompt.prototype.type = function(text, value, callback) { +Prompt.prototype.input = +Prompt.prototype.setInput = +Prompt.prototype.readInput = function(text, value, callback) { var self = this; var okay, cancel; @@ -5683,8 +5786,6 @@ Prompt.prototype.type = function(text, value, callback) { this.screen.render(); }; -Prompt.prototype.type.toString = function() { return 'prompt'; }; - /** * Question */ @@ -6403,6 +6504,135 @@ Passbox.prototype.__proto__ = Textbox.prototype; Passbox.prototype.type = 'passbox'; +/** + * Terminal + */ + +function Terminal(options) { + var self = this; + + if (!(this instanceof Node)) { + return new Terminal(options); + } + + options = options || {}; + options.scrollable = true; + + Box.call(this, options); + + 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, + parentNode: element, + offsetParent: element, + appendChild: function() {}, + removeChild: function() {}, + setAttribute: function() {}, + getAttribute: function() {}, + style: {}, + focus: function() {}, + blur: function() {}, + console: console + }; + + this.term = require('term.js')({ + cols: this.width - this.iwidth, + rows: this.height - this.iheight, + context: element, + document: element, + body: element, + parent: element + }); + + 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) { + // options.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) { + options.handler(data); + } + }); + + this.term.on('title', function(title) { + self.title = title; + self.emit('title', title); + }); + + this.on('resize', function() { + setImmediate(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); + }); +} + +Terminal.prototype.__proto__ = Box.prototype; + +Terminal.prototype.type = 'terminal'; + +Terminal.prototype.write = function(data) { + return this.term.write(data); +}; + +Terminal.prototype.render = function() { + var ret = this._render(); + if (!ret) return; + + var xi = ret.xi + this.ileft + , xl = ret.xl - this.iright + , yi = ret.yi + this.itop + , yl = ret.yl - this.ibottom; + + var scrollback = this.term.lines.length - (yl - yi); + + for (var y = yi; y < yl; y++) { + var line = this.screen.lines[y]; + for (var x = xi; x < xl; x++) { + line[x][0] = this.term.lines[scrollback + y - yi][x - xi][0]; + line[x][1] = this.term.lines[scrollback + y - yi][x - xi][1]; + } + line.dirty = true; + } + + return ret; +}; + /** * Helpers */ @@ -6521,4 +6751,6 @@ exports.Listbar = exports.listbar = Listbar; exports.DirManager = exports.dirmanager = DirManager; exports.Passbox = exports.passbox = Passbox; +exports.Terminal = exports.terminal = Terminal; + exports.helpers = helpers; diff --git a/test/widget-prompt.js b/test/widget-prompt.js index 63ef12f..d7dfada 100644 --- a/test/widget-prompt.js +++ b/test/widget-prompt.js @@ -69,7 +69,7 @@ var loader = blessed.loading({ vi: true }); -prompt.type('Question?', '', function(err, value) { +prompt.input('Question?', '', function(err, value) { question.ask('Question?', function(err, value) { msg.display('Hello world!', 3, function(err) { msg.display('Hello world again!', -1, function(err) {