diff --git a/README.md b/README.md index 7f86659..d2d6e0a 100644 --- a/README.md +++ b/README.md @@ -572,6 +572,41 @@ Elements are rendered with the lower elements in the children array being painted first. In terms of the painter's algorithm, the lowest indicies in the array are the furthest away, just like in the DOM. +### Optimization and CSR + +You may notice a lot of terminal apps (e.g. mutt, irssi, vim, ncmpcpp) don't +have sidebars, and only have "elements" that take up the entire width of the +screen. The reason for this is speed (and general cleanliness). VT-like +terminals have something called a CSR (change_scroll_region) code, as well as +IL (insert_line), and DL (delete_code) codes. Using these three codes, it is +possible to create a very efficient rendering by avoiding redrawing the entire +screen when a line is inserted or removed. Since blessed is extremely dynamic, +it is hard to do this optimization automatically (blessed assumes you may +create any element of any width in any position). So, there is a solution: + +``` js +var box = new blessed.Box(...); +box.setContent('line 1\nline 2'); +box.insertBottom('line 3'); +box.insertBottom('line 4'); +box.insertTop('line 0'); +``` + +If your element has the same width as the screen, the line insertion will be +optimized by using a combination CSR/IL/DL codes. These methods may be made +smarter in the future to detect whether any elements are being overlapped to +the sides. + +Outputting: + +``` +| line 0 | +| line 1 | +| line 2 | +| line 3 | +| line 4 | +``` + ### Testing - For an interactive test, see `test/widget.js`. diff --git a/lib/program.js b/lib/program.js index adfe221..b966824 100644 --- a/lib/program.js +++ b/lib/program.js @@ -834,7 +834,7 @@ Program.prototype.nextLine = function() { // ESC c Full Reset (RIS). Program.prototype.reset = function() { //this.x = this.y = 1; - if (this.tput) return this.put.ris(); + if (this.tput) return this.put.rs1 ? this.put.rs1() : this.put.ris(); return this.write('\x1bc'); }; @@ -848,7 +848,7 @@ Program.prototype.tabSet = function() { Program.prototype.saveCursor = function() { this.savedX = this.x || 1; this.savedY = this.y || 1; - if (this.tput) return this.put.sc(); // not correct + if (this.tput) return this.put.sc(); return this.esc('7'); }; @@ -856,7 +856,7 @@ Program.prototype.saveCursor = function() { Program.prototype.restoreCursor = function() { this.x = this.savedX || 1; this.y = this.savedY || 1; - if (this.tput) return this.put.rc(); // not correct + if (this.tput) return this.put.rc(); return this.esc('8'); }; @@ -869,6 +869,13 @@ Program.prototype.lineHeight = function() { Program.prototype.charset = function(val, level) { level = level || 0; + // See also: + // acs_chars / acsc / ac + // enter_alt_charset_mode / smacs / as + // exit_alt_charset_mode / rmacs / ae + // enter_pc_charset_mode / smpch / S2 + // exit_pc_charset_mode / rmpch / S3 + // tput: TODO // if (this.tput) return this.put('s' + level, val); @@ -889,12 +896,16 @@ Program.prototype.charset = function(val, level) { switch (val) { case 'SCLD': // DEC Special Character and Line Drawing Set. + if (this.tput) return this.put.smacs(); val = '0'; break; case 'UK': // UK val = 'A'; break; case 'US': // United States (USASCII). + case 'USASCII': + case 'ASCII': + if (this.tput) return this.put.rmacs(); val = 'B'; break; case 'Dutch': // Dutch @@ -934,6 +945,7 @@ Program.prototype.charset = function(val, level) { val = '/A'; break; default: // Default + if (this.tput) return this.put.rmacs(); val = 'B'; break; } @@ -941,6 +953,18 @@ Program.prototype.charset = function(val, level) { return this.write('\x1b(' + val); }; +Program.prototype.enter_alt_charset_mode = +Program.prototype.as = +Program.prototype.smacs = function() { + return this.charset('SCLD'); +}; + +Program.prototype.exit_alt_charset_mode = +Program.prototype.ae = +Program.prototype.rmacs = function() { + return this.charset('US'); +}; + // ESC N // Single Shift Select of G2 Character Set // ( SS2 is 0x8e). This affects next character only. @@ -1157,6 +1181,7 @@ Program.prototype.clear = function() { Program.prototype.el = Program.prototype.eraseInLine = function(param) { if (this.tput) { + //if (this.tput.back_color_erase) ... switch (param) { case 'left': param = 1; @@ -1824,12 +1849,24 @@ Program.prototype.decset = function() { }; Program.prototype.dectcem = +Program.prototype.cnorm = +Program.prototype.cvvis = Program.prototype.showCursor = function() { + this.cursorHidden = false; + // NOTE: In xterm terminfo: + // cnorm stops blinking cursor + // cvvis starts blinking cursor + if (this.tput) return this.put.cnorm(); + //if (this.tput) return this.put.cvvis(); + // return this.write('\x1b[?12l\x1b[?25h'); // cursor_normal + // return this.write('\x1b[?12;25h'); // cursor_visible return this.setMode('?25'); }; Program.prototype.alternate = +Program.prototype.smcup = Program.prototype.alternateBuffer = function() { + if (this.tput) return this.put.smcup(); if (this.term('vt') || this.term('linux')) return; //return this.setMode('?47'); //return this.setMode('?1047'); @@ -1932,19 +1969,26 @@ Program.prototype.decrst = function() { }; Program.prototype.dectcemh = +Program.prototype.cursor_invisible = +Program.prototype.vi = +Program.prototype.civis = Program.prototype.hideCursor = function() { + this.cursorHidden = true; + if (this.tput) return this.put.civis(); return this.resetMode('?25'); }; +Program.prototype.rmcup = Program.prototype.normalBuffer = function() { //return this.resetMode('?47'); //return this.resetMode('?1047'); + if (this.tput) return this.put.rmcup(); return this.resetMode('?1049'); }; Program.prototype.enableMouse = function() { - if (this.term('rxvt')) { - return this.setMouse({ urxvtMouse: true }); + if (this.term('rxvt-unicode')) { + return this.setMouse({ urxvtMouse: true }, true); } if (this.term('xterm') || this.term('screen')) { @@ -1952,35 +1996,51 @@ Program.prototype.enableMouse = function() { allMotion: true, utfMouse: true, sendFocus: true - }); + }, true); } if (this.term('vt')) { - return this.setMouse({ vt200Mouse: true }); + return this.setMouse({ vt200Mouse: true }, true); } }; Program.prototype.disableMouse = function() { - return this.setMouse({ - x10Mouse: false, - vt200Mouse: false, - hiliteTracking: false, - cellMotion: false, - allMotion: false, - sendFocus: false, - utfMouse: false, - sgrMouse: false, - urxvtMouse: false + //return this.setMouse({ + // x10Mouse: false, + // vt200Mouse: false, + // hiliteTracking: false, + // cellMotion: false, + // allMotion: false, + // sendFocus: false, + // utfMouse: false, + // sgrMouse: false, + // urxvtMouse: false + //}, false); + + if (!this._currentMouse) return; + + var obj = {}; + + Object.keys(this._currentMouse).forEach(function(key) { + obj[key] = false; }); + + return this.setMouse(obj, false); }; // Set Mouse -Program.prototype.setMouse = function(opt) { +Program.prototype.setMouse = function(opt, enable) { if (opt.normalMouse != null) { opt.cellMotion = opt.normalMouse; opt.allMotion = opt.normalMouse; } + if (enable) { + this._currentMouse = opt; + } else { + delete this._currentMouse; + } + // Make sure we're not a vtNNN if (this.term('vt')) return; @@ -2191,7 +2251,6 @@ Program.prototype.tabClear = function(param) { // Ps = 1 1 -> Print all pages. Program.prototype.mc = Program.prototype.mediaCopy = function() { - // tput: TODO. See: mc0, mc5p, mc4, mc5 //if (dec) { // this.write('\x1b[?' + Array.prototype.slice.call(arguments).join(';') + 'i'); // return; @@ -2199,6 +2258,34 @@ Program.prototype.mediaCopy = function() { return this.write('\x1b[' + Array.prototype.slice.call(arguments).join(';') + 'i'); }; +Program.prototype.print_screen = +Program.prototype.ps = +Program.prototype.mc0 = function() { + if (this.tput) return this.put.mc0(); + return this.mc('0'); +}; + +Program.prototype.prtr_on = +Program.prototype.po = +Program.prototype.mc5 = function() { + if (this.tput) return this.put.mc5(); + return this.mc('5'); +}; + +Program.prototype.prtr_off = +Program.prototype.pf = +Program.prototype.mc4 = function() { + if (this.tput) return this.put.mc4(); + return this.mc('4'); +}; + +Program.prototype.prtr_non = +Program.prototype.pO = +Program.prototype.mc5p = function() { + if (this.tput) return this.put.mc5p(); + return this.mc('?5'); +}; + // CSI > Ps; Ps m // Set or reset resource-values used by xterm to decide whether // to construct escape sequences holding information about the @@ -2247,8 +2334,14 @@ Program.prototype.setPointerMode = function(param) { // CSI ! p Soft terminal reset (DECSTR). // http://vt100.net/docs/vt220-rm/table4-10.html Program.prototype.decstr = +Program.prototype.rs2 = Program.prototype.softReset = function() { - return this.write('\x1b[!p'); + //if (this.tput) return this.put.init_2string(); + //if (this.tput) return this.put.reset_2string(); + if (this.tput) return this.put.rs2(); + //return this.write('\x1b[!p'); + //return this.write('\x1b[!p\x1b[?3;4l\x1b[4l\x1b>'); // init + return this.write('\x1b[!p\x1b[?3;4l\x1b[4l\x1b>'); // reset }; // CSI Ps$ p @@ -2643,9 +2736,17 @@ Program.prototype.selectiveEraseRectangle = function(params) { // The ``page'' parameter is not used by xterm, and will be omit- // ted. Program.prototype.decrqlp = +Program.prototype.req_mouse_pos = +Program.prototype.reqmp = Program.prototype.requestLocatorPosition = function(params, callback) { - // Correct for tput? - if (this.tput) return this.put.req_mouse_pos.apply(this.put, arguments); + if (this.tput && this.tput.req_mouse_pos) { + // See also: + // get_mouse / getm / Gm + // mouse_info / minfo / Mi + // Correct for tput? + var code = this.tput.req_mouse_pos.apply(this.tput, params); + return this.receive(code, callback); + } return this.receive('\x1b[' + (param || '') + '\'|', callback); }; diff --git a/lib/tput.js b/lib/tput.js index e5b591b..615d4de 100644 --- a/lib/tput.js +++ b/lib/tput.js @@ -524,6 +524,13 @@ Tput.prototype.inject = function(info) { return methods[key].call(self, args); }; }); + + this.info = info; + this.all = info.all; + this.methods = info.methods; + this.bools = info.bools; + this.numbers = info.numbers; + this.strings = info.strings; }; Tput.prototype._compile = function(val) { diff --git a/lib/widget.js b/lib/widget.js index 1359ed6..fdcc855 100644 --- a/lib/widget.js +++ b/lib/widget.js @@ -46,9 +46,12 @@ function Node(options) { Node.prototype.__proto__ = EventEmitter.prototype; +Node.prototype.type = 'node'; + Node.prototype.prepend = function(element) { var old = element.parent; + element.detach(); element.parent = this; if (this._isScreen && !this.focused) { @@ -77,6 +80,7 @@ Node.prototype.prepend = function(element) { Node.prototype.append = function(element) { var old = element.parent; + element.detach(); element.parent = this; if (this._isScreen && !this.focused) { @@ -260,6 +264,8 @@ Screen.global = null; Screen.prototype.__proto__ = Node.prototype; +Screen.prototype.type = 'screen'; + // TODO: Bubble events. Screen.prototype._listenMouse = function(el) { var self = this; @@ -834,6 +840,8 @@ function Element(options) { Element.prototype.__proto__ = Node.prototype; +Element.prototype.type = 'element'; + /* Element._emit = Element.prototype.emit; Element.prototype.emit = function(type) { @@ -1268,6 +1276,8 @@ function Box(options) { Box.prototype.__proto__ = Element.prototype; +Box.prototype.type = 'box'; + // TODO: Optimize. Move elsewhere. Box.prototype._getShrinkSize = function(content) { return { @@ -1315,7 +1325,8 @@ Box.prototype.render = function(stop) { yl = yi_ + this.height; } - if (this.parent.childBase != null && ~this.parent.items.indexOf(this)) { + // Check to make sure we're visible and inside of the visible scroll area. + if (this.parent.childBase != null && (!this.parent.items || ~this.parent.items.indexOf(this))) { var rtop = this.rtop - (this.parent.border ? 1 : 0) , visible = this.parent.height - (this.parent.border ? 2 : 0); @@ -1473,9 +1484,25 @@ outer: } if (this.border) { + // var alt; + // if (this.screen.tput + // && this.screen.tput.strings.enter_alt_charset_mode + // && this.border.type === 'ascii') { + // //this.screen.program.put.smacs(); + // battr |= 32 << 18; + // alt = true; + // } + yi = yi_; for (xi = xi_; xi < xl; xi++) { if (!lines[yi]) break; + + // if (alt) { + // if (xi === xi_) ch = 'l'; + // else if (xi === xl - 1) ch = 'k'; + // else ch = 'q'; + // } else + if (this.border.type === 'ascii') { if (xi === xi_) ch = '┌'; else if (xi === xl - 1) ch = '┐'; @@ -1494,6 +1521,11 @@ outer: yi = yi_ + 1; for (; yi < yl; yi++) { if (!lines[yi]) break; + + // if (alt) { + // ch = 'x'; + // } else + if (this.border.type === 'ascii') { ch = '│'; } else if (this.border.type === 'bg') { @@ -1517,6 +1549,13 @@ outer: yi = yl - 1; for (xi = xi_; xi < xl; xi++) { if (!lines[yi]) break; + + // if (alt) { + // if (xi === xi_) ch = 'm'; + // else if (xi === xl - 1) ch = 'j'; + // else ch = 'q'; + // } else + if (this.border.type === 'ascii') { if (xi === xi_) ch = '└'; else if (xi === xl - 1) ch = '┘'; @@ -1532,6 +1571,11 @@ outer: lines[yi].dirty = true; } } + + // if (alt) { + // //this.screen.program.put.rmacs(); + // battr &= ~(32 << 18); + // } } this.children.forEach(function(el) { @@ -1543,20 +1587,27 @@ outer: // Create a much more efficient rendering by using insert-line, // delete-line, and change screen region codes when possible. +// NOTE: If someone does: +// box.left = box.right = 0; +// screen.render(); +// box.left++; +// box.insertTop('foobar'); +// Things will break because we're using _lastPos instead of render(true). +// Maybe _lastPos could be updated on .left, .right, etc setters? Box.prototype.insertTop = function(line) { - if (!this._lastPos) return; - if (this._lastPos.xi === 0 && this._lastPos.xl === this.screen.width) { + if (this._lastPos && this._lastPos.xi === 0 && this._lastPos.xl === this.screen.width) { this.screen.insertTop(this._lastPos.yi, this._lastPos.yl - 1); } this.setContent(line + '\n' + this.content, true); + // this.screen.render(); }; Box.prototype.insertBottom = function(line) { - if (!this._lastPos) return; - if (this._lastPos.xi === 0 && this._lastPos.xl === this.screen.width) { + if (this._lastPos && this._lastPos.xi === 0 && this._lastPos.xl === this.screen.width) { this.screen.insertBottom(this._lastPos.yi, this._lastPos.yl - 1); } this.setContent(this.content + '\n' + line, true); + // this.screen.render(); }; /** @@ -1606,6 +1657,8 @@ function Line(options) { Line.prototype.__proto__ = Box.prototype; +Line.prototype.type = 'line'; + /** * ScrollableBox */ @@ -1624,6 +1677,8 @@ function ScrollableBox(options) { ScrollableBox.prototype.__proto__ = Box.prototype; +ScrollableBox.prototype.type = 'scrollable-box'; + ScrollableBox.prototype.scroll = function(offset) { var visible = this.height - (this.border ? 2 : 0); // Maybe do for lists: @@ -1664,6 +1719,7 @@ function List(options) { ScrollableBox.call(this, options); this.items = []; + this.ritems = []; this.selected = 0; this.selectedBg = convert(options.selectedBg); @@ -1677,6 +1733,7 @@ function List(options) { this.mouse = options.mouse || false; if (options.items) { + this.ritems = options.items; options.items.forEach(this.add.bind(this)); } @@ -1783,6 +1840,8 @@ function List(options) { List.prototype.__proto__ = ScrollableBox.prototype; +List.prototype.type = 'list'; + List.prototype.add = function(item) { var self = this; @@ -1821,7 +1880,11 @@ List.prototype.remove = function(child) { List.prototype.setItems = function(items) { var i = 0 - , original = this.items.slice(); + , original = this.items.slice() + //, selected = this.selected + , sel = this.ritems[this.selected]; + + this.ritems = items; this.select(0); @@ -1836,6 +1899,11 @@ List.prototype.setItems = function(items) { for (; i < original.length; i++) { this.remove(original[i]); } + + // Try to find our old item if it still exists. + sel = items.indexOf(sel); + if (~sel) this.select(sel); + //this.select(~sel ? sel : selected); }; List.prototype.select = function(index) { @@ -1947,6 +2015,8 @@ function ScrollableText(options) { ScrollableText.prototype.__proto__ = ScrollableBox.prototype; +ScrollableText.prototype.type = 'scrollable-text'; + ScrollableText.prototype._scroll = ScrollableText.prototype.scroll; ScrollableText.prototype.scroll = function(offset) { var base = this.childBase @@ -2013,6 +2083,8 @@ function Input(options) { Input.prototype.__proto__ = Box.prototype; +Input.prototype.type = 'input'; + /** * Textbox */ @@ -2025,10 +2097,27 @@ function Textbox(options) { this.screen._listenKeys(this); this.value = options.value || ''; this.secret = options.secret; + this.censor = options.censor; + + var self = this; + + this.on('resize', updateCursor); + this.on('move', updateCursor); + + function updateCursor() { + //if (!self.visible) return; + if (self.screen.focused !== self) return; + self.screen.program.cup( + self.top + 1 + (self.border ? 1 : 0), + self.left + 1 + (self.border ? 1 : 0) + + self.value.length); + } } Textbox.prototype.__proto__ = Input.prototype; +Textbox.prototype.type = 'textbox'; + Textbox.prototype.setInput = function(callback) { var self = this; @@ -2122,7 +2211,10 @@ Textbox.prototype.render = function(stop) { // Could technically optimize this. if (this.secret) { this.setContent(''); - //this.setContent(Array(this.value.length + 1).join('*')); + return this._render(stop); + } + if (this.censor) { + this.setContent(Array(this.value.length + 1).join('*')); return this._render(stop); } this.setContent(this.value.slice(-(this.width - (this.border ? 2 : 0) - 1))); @@ -2136,19 +2228,31 @@ Textbox.prototype.setEditor = function(callback) { self.screen.program.normalBuffer(); self.screen.program.showCursor(); + var _listenedMouse = self.screen._listenedMouse; + if (self.screen._listenedMouse) { + self.screen.program.disableMouse(); + } return readEditor(function(err, value) { self.screen.program.alternateBuffer(); self.screen.program.hideCursor(); + if (_listenedMouse) { + self.screen.program.enableMouse(); + } self.screen.alloc(); self.screen.render(); if (err) return callback(err); value = value.replace(/[\r\n]/g, ''); self.value = value; - if (!self.secret) { - self.setContent(value); - } + + ////if (self.censor) { + //// self.setContent(Array(self.value.length + 1).join('*')); + ////} else + //if (!self.secret) { + // self.setContent(value); + //} //return callback(null, value); + return self.setInput(callback); }); }; @@ -2166,6 +2270,8 @@ function Textarea(options) { Textarea.prototype.__proto__ = Input.prototype; +Textarea.prototype.type = 'textarea'; + /** * Button */ @@ -2202,6 +2308,8 @@ function Button(options) { Button.prototype.__proto__ = Input.prototype; +Button.prototype.type = 'button'; + Button.prototype.press = function() { var self = this; this.emit('press'); @@ -2237,6 +2345,8 @@ function ProgressBar(options) { ProgressBar.prototype.__proto__ = Input.prototype; +ProgressBar.prototype.type = 'progress-bar'; + ProgressBar.prototype._render = ProgressBar.prototype.render; ProgressBar.prototype.render = function(stop) { // NOTE: Maybe move this `hidden` check down below `stop` check and return `ret`.