From f431e98c0334c34062760d6673d0547581cbe386 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Thu, 20 Jun 2013 06:43:56 -0500 Subject: [PATCH] major changes. better content formatting and parsing. --- README.md | 9 +- lib/program.js | 12 +++ lib/widget.js | 237 ++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 241 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index f9f1803..c06df48 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,7 @@ Blessed was originally written to only support the xterm terminfo, but can now parse and compile any terminfo to be completely portable accross all terminals. See the `tput` example below. -I want this library to eventually become a high-level library for terminal -widgets. +Blessed also includes an extremely high-level widget library. ## Example Usage @@ -29,7 +28,7 @@ $ tput.js sgr0 $ echo "$(tput.js setaf 2)hello world$(tput.js sgr0)" ``` -The higher level functionality is exposed in the main `blessed` module: +The main functionality is exposed in the main `blessed` module: ``` js var blessed = require('blessed') @@ -135,6 +134,10 @@ The base node which everything inherits from. - **adopt** - received when node is added to a parent. - **remove** - received when node is removed from it's current parent. - **reparent** - received when node gains a new parent. +- **attach** - received when node is attached to the screen directly or + somewhere in its ancestry. +- **detach** - received when node is detached from the screen directly or + somewhere in its ancestry. ##### Methods: diff --git a/lib/program.js b/lib/program.js index 7302256..5fd9eac 100644 --- a/lib/program.js +++ b/lib/program.js @@ -122,6 +122,10 @@ Program.prototype.listen = function() { } return; } + if (key.name === 'undefined' && key.code === '[M') { + // A mouse sequence. The readline module doesn't understand these. + return; + } self.emit('keypress', ch, key); }); @@ -1816,6 +1820,7 @@ Program.prototype.showCursor = function() { Program.prototype.alternate = Program.prototype.alternateBuffer = function() { + if (this.term('vt') || this.term('linux')) return; //return this.setMode('?47'); //return this.setMode('?1047'); return this.setMode('?1049'); @@ -1966,6 +1971,9 @@ Program.prototype.setMouse = function(opt) { opt.allMotion = opt.normalMouse; } + // Make sure we're not a vtNNN + if (this.term('vt')) return; + // Ps = 9 -> Send Mouse X & Y on button press. See the sec- // tion Mouse Tracking. // Ps = 9 -> Don't send Mouse X & Y on button press. @@ -1985,6 +1993,10 @@ Program.prototype.setMouse = function(opt) { else this.resetMode('?1000'); } + // Linux Console actually *does* support mouse reporting. + // See: `$ man console_codes`. + if (this.term('linux')) return; + // Ps = 1 0 0 1 -> Use Hilite Mouse Tracking. // Ps = 1 0 0 1 -> Don't use Hilite Mouse Tracking. if (opt.hiliteTracking != null) { diff --git a/lib/widget.js b/lib/widget.js index 5d58b60..34999ea 100644 --- a/lib/widget.js +++ b/lib/widget.js @@ -33,6 +33,10 @@ function Node(options) { this.parent.append(this); } + if (!this.parent) { + this._detached = true; + } + (options.children || []).forEach(this.append.bind(this)); if (this._isScreen && !this.focused) { @@ -43,6 +47,8 @@ function Node(options) { Node.prototype.__proto__ = EventEmitter.prototype; Node.prototype.prepend = function(element) { + var old = element.parent; + element.parent = this; if (this._isScreen && !this.focused) { @@ -55,9 +61,18 @@ Node.prototype.prepend = function(element) { element.emit('reparent', this); this.emit('adopt', element); + + //if (!old) { + (function emit(el) { + el._detached = false; + el.emit('attach'); + if (el.children) el.children.forEach(emit); + })(element); }; Node.prototype.append = function(element) { + var old = element.parent; + element.parent = this; if (this._isScreen && !this.focused) { @@ -70,6 +85,13 @@ Node.prototype.append = function(element) { element.emit('reparent', this); this.emit('adopt', element); + + //if (!old) { + (function emit(el) { + el._detached = false; + el.emit('attach'); + if (el.children) el.children.forEach(emit); + })(element); }; Node.prototype.remove = function(element) { @@ -93,6 +115,12 @@ Node.prototype.remove = function(element) { element.emit('reparent', null); this.emit('remove', element); + + (function emit(el) { + el._detached = true; + el.emit('detach'); + if (el.children) el.children.forEach(emit); + })(element); }; Node.prototype.detach = function(element) { @@ -736,6 +764,28 @@ function Element(options) { self.screen._listenKeys(self); } }); + + this.screen.on('resize', function() { + self.parseContent(); + }); + + this.on('resize', function() { + self.parseContent(); + }); + + this.on('attach', function() { + self.parseContent(); + }); + + this.parseContent(); + + //if (this.parent) { + // this.parseContent(); + //} else { + // this.once('reparent', function() { + // self.parseContent(); + // }); + //} } Element.prototype.__proto__ = Node.prototype; @@ -757,6 +807,20 @@ Element.prototype.emit = function(type) { }; */ +Element.prototype.parseContent = function() { + if (!this.content) return; + if (this.detached) return; + var w = this.width - (this.border ? 2 : 0); + if (this._clines == null + || this._clines.width !== w + || this._clines.content !== this.content) { + this._clines = wrapContent(this.content, w, this.align); + this._clines.width = w; + this._clines.content = this.content; + this._pcontent = this._clines.join('\n'); + } +}; + Element.prototype.hide = function() { if (this.hidden) return; this.hidden = true; @@ -800,6 +864,7 @@ Element.prototype.setContent = function(content) { // TODO: Maybe simply set _pcontent with _parseTags result. // text = text.replace(/\x1b(?!\[[\d;]*m)/g, ''); this.content = this._parseTags(content || ''); + this.parseContent(); if (ret) { //if (ret && !this.hidden) { this.screen.clearRegion(ret.xi, ret.xl, ret.yi, ret.yl); @@ -833,6 +898,15 @@ Element.prototype.__defineGetter__('visible', function() { return true; }); +Element.prototype.__defineGetter__('detached', function() { + var el = this; + do { + if (el._isScreen) return false; + if (!el.parent) return true; + } while (el = el.parent); + return false; +}); + /** * Positioning */ @@ -1120,6 +1194,10 @@ Element.prototype.__defineSetter__('rbottom', function(val) { return this.options.bottom = this.position.bottom = val; }); +Element.prototype.calcShrink = function(xi_, xl, yi_, yl) { + return [xi_, xl, yi_, yl]; +}; + /** * Box */ @@ -1135,6 +1213,17 @@ Box.prototype.__proto__ = Element.prototype; // TODO: Optimize. Move elsewhere. Box.prototype._getShrinkSize = function(content) { + if (this._clines) { + return { + height: this._clines.length, + width: this._clines.reduce(function(current, line) { + line = line.replace(/\x1b\[[\d;]*m/g, ''); + return line.length > current + ? line.length //+ (lines.length > 1 ? 1 : 0) // for newlines + : current; + }, 0) + }; + } var lines = content.replace(/\x1b\[[\d;]*m/g, '').split('\n'); return { height: lines.length, @@ -1195,6 +1284,13 @@ Box.prototype.render = function(stop) { // NOTE: Could simply change some offsets around and move this below the // shrink block. Could also stop passing a string to _getShrinkSize. +/* + if (this.align === 'center' || this.align === 'right') { + content = '{' + this.align + '}' + content + '{/' + this.align + '}'; + } else +*/ + +/* if (this.align === 'center' || this.align === 'right') { var ncontent = content.replace(/\x1b\[[\d;]*m/g, '') , width = this.width - (this.border ? 2 : 0) - this.padding * 2; @@ -1216,7 +1312,58 @@ Box.prototype.render = function(stop) { } } } +*/ + //if (!this._pcontent && this.content) { + // this._clines = wrapContent(this.content, this.width - (this.border ? 2 : 0)); + // this._pcontent = this._clines.join('\n'); + // content = this._pcontent; + // //content = wrapContent(this.content, this.width - (this.border ? 2 : 0)).join('\n'); + //} + +/* + if (this.tags || this.align) { + var self = this; + content = content.replace(/(^|\n){(center|right)}([^\0]+?){\/\2}(?=\n|$)/g, function(_, start, align, text) { + var lines = text.trim().split('\n') + , line + , i = 0 + , width = self.width - (self.border ? 2 : 0) - self.padding * 2 + , out = '' + , len + , s; + + for (; i < lines.length; i++) { + line = lines[i]; + len = line.replace(/\x1b\[[\d;]*m/g, '').length; + s = width - len; + + if (s < 0) { + out += '\n' + line; + continue; + } + + if (align === 'center') { + s = Array(((s / 2 | 0) - (self.border ? 1 : 0)) + 1).join(' '); + out += '\n' + s + line + (self.shrink ? s : ''); + } else { + s -= self.left; // this shouldn't be necessary + s = Array((s - (self.border ? 2 : 0)) + 1).join(' '); + out += '\n' + s + line; + } + } + + if (out[0] === '\n' && !start) { + out = out.substring(1); + } + + return out; + }); + } +*/ + + // TODO: Check for 'center', recalculate yi, and xi. Better yet, simply + // move this check into this.left/width/etc. if (this.shrink) { var hw = this._getShrinkSize(content) , h = hw.height @@ -1235,6 +1382,10 @@ Box.prototype.render = function(stop) { } } + //if (this.options.vshrink && this._clines) { + // yl = yi_ + this._clines.length + (this.border ? 2 : 0) + this.padding; + //} + var ret = this._lastPos = { xi: xi_, xl: xl, @@ -1785,6 +1936,7 @@ function ScrollableText(options) { }); } +/* this.screen.on('resize', function() { self._recalculateIndex(); }); @@ -1806,6 +1958,7 @@ function ScrollableText(options) { self._recalculateIndex(); }); } +*/ } ScrollableText.prototype.__proto__ = ScrollableBox.prototype; @@ -1827,11 +1980,13 @@ ScrollableText.prototype.scroll = function(offset) { // feeds. This allows us to take preformatted text output from other programs // and put it in a scrollable text box. if (this.content != null) { - w = this.width - (this.border ? 2 : 0); - if (this._clines == null || this._clines.width !== w) { - this._clines = wrapContent(this.content, w); - this._pcontent = this._clines.join('\n'); - } + this._parseContent(); + //w = this.width - (this.border ? 2 : 0); + //if (this._clines == null || this._clines.width !== w) { + // this._clines = wrapContent(this.content, w); + // this._clines.content = this.content; + // this._pcontent = this._clines.join('\n'); + //} max = this._clines.length - 1 - (this.height - (this.border ? 2 : 0)); if (max < 0) max = 0; @@ -1854,14 +2009,14 @@ ScrollableText.prototype.scroll = function(offset) { ScrollableText.prototype._setContent = ScrollableText.prototype.setContent; ScrollableText.prototype.setContent = function(content) { var ret = this._setContent(content); - this._recalculateIndex(); + //this._recalculateIndex(); return ret; }; ScrollableText.prototype._recalculateIndex = function() { - if (!this.parent) return; - this._clines = wrapContent(this.content, this.width - (this.border ? 2 : 0)); - this._pcontent = this._clines.join('\n'); + if (this.detached) return; + //this._clines = wrapContent(this.content, this.width - (this.border ? 2 : 0)); + //this._pcontent = this._clines.join('\n'); var max = this._clines.length - 1 - (this.height - (this.border ? 2 : 0)); if (max < 0) max = 0; @@ -1876,6 +2031,14 @@ ScrollableText.prototype._recalculateIndex = function() { this.contentIndex = t; }; +ScrollableText.prototype._parseContent = ScrollableText.prototype.parseContent; +ScrollableText.prototype.parseContent = function(content) { + var ret = this._parseContent(); + this._recalculateIndex(); + return ret; +}; + + /** * Input */ @@ -2264,11 +2427,44 @@ function readEditor(callback) { }); } -function wrapContent(content, width) { +function sp(line, width, align) { + if (!align) return line; + + var len = line.replace(/\x1b\[[\d;]*m/g, '').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; + } + + return line; +} + +function wrapContent(content, width, state) { var lines = content.split('\n') , out = []; lines.forEach(function(line) { + var align = state + , cap; + + if (cap = /^{(center|right)}/.exec(line)) { + line = line.substring(cap[0].length); + align = state = cap[1]; + } + + if (cap = /{\/(center|right)}$/.exec(line)) { + line = line.slice(0, -cap[0].length); + state = null; + } + var total , i , part @@ -2283,7 +2479,17 @@ function wrapContent(content, width) { while (line[i] && line[i++] !== 'm'); } if (!line[i]) break; - if (++total === width) break; + if (++total === width) { + // Try to find a space to break on: + if (line[i] !== ' ') { + var j = i; + while (j > i - 10 && j > 0 && line[j] !== ' ') j--; + if (line[j] === ' ') i = j + 1; + } else { + i++; + } + break; + } } part = line.substring(0, i - 1); @@ -2292,10 +2498,12 @@ function wrapContent(content, width) { if (esc) { part = part.slice(0, -esc[0].length); line = line.substring(i - 1 - esc[0].length); - out.push(part); + //out.push(part); + out.push(sp(part, width, align)); } else { line = line.substring(i - 1); - out.push(part); + //out.push(part); + out.push(sp(part, width, align)); } } @@ -2304,7 +2512,8 @@ function wrapContent(content, width) { out[out.length-1] += line; return; } - out.push(line); + //out.push(line); + out.push(sp(line, width, align)); }); out.width = width;