From 982e075a908d4f76beb22ec101cf57ed780b3a71 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Wed, 11 Feb 2015 17:20:22 -0800 Subject: [PATCH] Image and Terminal element. --- README.md | 23 +++- example/multiplex.js | 6 ++ lib/widget.js | 251 +++++++++++++++++++++++++------------------ test/widget-image.js | 13 ++- 4 files changed, 186 insertions(+), 107 deletions(-) diff --git a/README.md b/README.md index 82dce81..c72e5ea 100644 --- a/README.md +++ b/README.md @@ -871,24 +871,37 @@ A radio button which can be used in a form element. #### Terminal (from Box) -A box which spins up a pseudo terminal and renders the output. (Requires -term.js to be installed). +A box which spins up a pseudo terminal and renders the output. Useful for +writing a terminal multiplexer, or something similar to an mc-like file +manager. Requires term.js and pty.js to be installed. See +`example/multiplex.js` for an example terminal multiplexer. ##### Options: - inherits all from Box. +- **handler** - handler for input data. +- **shell** - name of shell. `$SHELL` by default. +- **args** - args for shell. +- **cursor** - can be `line`, `underline`, and `block`. +- Other options similar to term.js'. ##### Properties: - inherits all from Box. +- **term** - reference to the headless term.js terminal. +- **pty** - reference to the pty.js pseudo terminal. ##### Events: - inherits all from Box. +- **title** - window title from terminal. +- Other events similar to ScrollableBox. ##### Methods: - inherits all from Box. +- **write(data)** - write data to the terminal. +- Other methods similar to ScrollableBox. #### Image (from Box) @@ -900,8 +913,9 @@ terminals. ##### Options: - inherits all from Box. -- **file** - path to image -- **w3m** - path to w3mimgdisplay +- **file** - path to image. +- **w3m** - path to w3mimgdisplay. if a proper w3mimgdisplay path is not given, + blessed will search the entire disk for the binary. ##### Properties: @@ -918,6 +932,7 @@ terminals. - **clearImage(callback)** - clear the current image. - **imageSize(img, callback)** - get the size of an image file in pixels. - **termSize(callback)** - get the size of the terminal in pixels. +- **getPixelRatio(callback)** - get the pixel to cell ratio for the terminal. ### Positioning diff --git a/example/multiplex.js b/example/multiplex.js index 20b51e6..9bf6bb3 100755 --- a/example/multiplex.js +++ b/example/multiplex.js @@ -14,6 +14,9 @@ var blessed = require('blessed') var left = blessed.terminal({ parent: screen, + cursor: 'line', + cursorBlink: true, + screenKeys: false, left: 0, top: 2, bottom: 2, @@ -32,6 +35,9 @@ var left = blessed.terminal({ var right = blessed.terminal({ parent: screen, + cursor: 'block', + cursorBlink: true, + screenKeys: false, right: 2, top: 2, bottom: 2, diff --git a/lib/widget.js b/lib/widget.js index 38832ef..d60b548 100644 --- a/lib/widget.js +++ b/lib/widget.js @@ -5364,7 +5364,7 @@ ProgressBar.prototype.render = function() { yi = yi + ((yl - yi) - (((yl - yi) * (this.filled / 100)) | 0)); } - dattr = this.sattr(this, this.style.bar.fg, this.style.bar.bg); + dattr = this.sattr(this.style.bar, this.style.bar.fg, this.style.bar.bg); this.screen.fillRegion(dattr, this.ch, xi, xl, yi, yl); @@ -6481,7 +6481,7 @@ function Terminal(options) { } options = options || {}; - options.scrollable = true; + options.scrollable = false; Box.call(this, options); @@ -6489,6 +6489,10 @@ function Terminal(options) { 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'; @@ -6535,14 +6539,15 @@ Terminal.prototype.bootstrap = function() { element.offsetParent = element; this.term = require('term.js')({ - //termName: this.screen.program.terminal, termName: 'xterm', cols: this.width - this.iwidth, rows: this.height - this.iheight, context: element, document: element, body: element, - parent: element + parent: element, + cursorBlink: this.cursorBlink, + screenKeys: this.screenKeys }); this.term.refresh = function() { @@ -6623,15 +6628,11 @@ Terminal.prototype.bootstrap = function() { }); this.on('focus', function() { - if (self.term.sendFocus) { - self.handler('\x1b[I'); - } + self.term.focus(); }); this.on('blur', function() { - if (self.term.sendFocus) { - self.handler('\x1b[O'); - } + self.term.blur(); }); this.term.on('title', function(title) { @@ -6650,7 +6651,6 @@ Terminal.prototype.bootstrap = function() { }); this.pty = require('pty.js').fork(this.shell, this.args, { - //name: this.screen.program.terminal, name: 'xterm', cols: this.width - this.iwidth, rows: this.height - this.iheight, @@ -6687,7 +6687,7 @@ Terminal.prototype.render = function() { var ret = this._render(); if (!ret) return; - this.dattr = this.sattr(this, this.style.fg, this.style.bg); + this.dattr = this.sattr(this.style, this.style.fg, this.style.bg); var xi = ret.xi + this.ileft , xl = ret.xl - this.iright @@ -6702,7 +6702,7 @@ Terminal.prototype.render = function() { if (!line || !this.term.lines[scrollback + y - yi]) break; if (y === yi + this.term.y - // && this.term.cursorState + && this.term.cursorState && this.screen.focused === this && (this.term.ydisp === this.term.ybase || this.term.selectMode) && !this.term.cursorHidden) { @@ -6713,21 +6713,42 @@ Terminal.prototype.render = function() { for (var x = xi; x < xl; x++) { if (!line[x] || !this.term.lines[scrollback + y - yi][x - xi]) break; + if (x === cursor) { - line[x][0] = 7; - line[x][1] = ' '; + if (this.cursor === 'line') { + line[x][0] = this.dattr; + line[x][1] = '\u2502'; + } else if (this.cursor === 'underline') { + line[x][0] = this.dattr; + line[x][1] = '_'; + } else if (this.cursor === 'block' || !this.cursor) { + line[x][0] = this.dattr | (8 << 18); + line[x][1] = ' '; + } continue; } + line[x][0] = this.term.lines[scrollback + y - yi][x - xi][0]; - // default foreground = 257 - if (((line[x][0] >> 9) & 0x1ff) === 257 && !((line[x][0] >> 18) & 8)) { - line[x][0] = (line[x][0] << 18) | (((this.dattr >> 9) & 0x1ff) << 9) | (line[x][0] & 0x1ff); - } - // default background = 256 - if ((line[x][0] & 0x1ff) === 256 && !((line[x][0] >> 18) & 8)) { - line[x][0] = (line[x][0] << 18) | (((line[x][0] >> 9) & 0x1ff) << 9) | (this.dattr & 0x1ff); - } line[x][1] = this.term.lines[scrollback + y - yi][x - xi][1]; + + // default foreground = 257 + if (((line[x][0] >> 9) & 0x1ff) === 257) { + if (!((line[x][0] >> 18) & 8)) { + line[x][0] &= ~(0x1ff << 9); + line[x][0] |= ((this.dattr >> 9) & 0x1ff) << 9; + } + } + + // default background = 256 + if ((line[x][0] & 0x1ff) === 256) { + if (!((line[x][0] >> 18) & 8)) { + line[x][0] &= ~0x1ff; + line[x][0] |= this.dattr & 0x1ff; + // } else { + // line[x][0] &= ~(0x1ff << 9); + // line[x][0] |= ((this.dattr >> 9) & 0x1ff) << 9; + } + } } line.dirty = true; @@ -6747,6 +6768,7 @@ Terminal.prototype._isMouse = function(buf) { } } 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) @@ -6754,6 +6776,39 @@ Terminal.prototype._isMouse = function(buf) { || /^\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: @@ -6771,12 +6826,44 @@ function Image(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('/', '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); } @@ -6822,12 +6909,6 @@ Image.prototype.spawn = function(file, args, opt, callback) { Image.prototype.setImage = function(img, callback) { var self = this; - if (Image.hasW3MDisplay == null) { - return this._waitForW3MDisplay(function() { - return self.setImage(img, callback); - }); - } - if (Image.hasW3MDisplay === false) { if (!callback) return; return callback(new Error('W3M Image Display not available.')); @@ -6840,9 +6921,6 @@ Image.prototype.setImage = function(img, callback) { this.file = img; - // NOTE: Fall back to screen.displayImage() instead of - // setImage() if w3mimgdisplay is not present? - function renderImage(ratio, callback) { // clearImage unsets these: var _file = self.file; @@ -6953,12 +7031,6 @@ Image.prototype.setImage = function(img, callback) { Image.prototype.clearImage = function(callback) { var self = this; - if (Image.hasW3MDisplay == null) { - return this._waitForW3MDisplay(function() { - return self.clearImage(callback); - }); - } - if (Image.hasW3MDisplay === false) { if (!callback) return; return callback(new Error('W3M Image Display not available.')); @@ -7007,12 +7079,6 @@ Image.prototype.imageSize = function(callback) { var self = this; var img = this.file; - if (Image.hasW3MDisplay == null) { - return this._waitForW3MDisplay(function() { - return self.imageSize(callback); - }); - } - if (Image.hasW3MDisplay === false) { if (!callback) return; return callback(new Error('W3M Image Display not available.')); @@ -7063,12 +7129,6 @@ Image.prototype.imageSize = function(callback) { Image.prototype.termSize = function(callback) { var self = this; - if (Image.hasW3MDisplay == null) { - return this._waitForW3MDisplay(function() { - return self.termSize(callback); - }); - } - if (Image.hasW3MDisplay === false) { if (!callback) return; return callback(new Error('W3M Image Display not available.')); @@ -7099,7 +7159,8 @@ Image.prototype.termSize = function(callback) { if (!callback) return; if (!buf.trim()) { - // XXX Bug + // Bug: w3mimgdisplay will sometimes + // output nothing. Try again: return self.termSize(callback); } @@ -7140,57 +7201,6 @@ Image.prototype.displayImage = function(callback) { return this.screen.displayImage(this.file, callback); }; -Image.prototype._waitForW3MDisplay = function(callback) { - var self = this; - - if (Image.hasW3MDisplay == null) { - if (Image._checkingW3MDisplay) { - setTimeout(function() { - return self._waitForW3MDisplay(callback); - }, 500); - return; - } - - Image._checkingW3MDisplay = true; - - var ps = this.screen.spawn('find', - ['/', '-name', 'w3mimgdisplay'], - { stdio: ['ignore', 'pipe', 'ignore'] }); - - var buf = ''; - - ps.stdout.setEncoding('utf8'); - ps.stdout.on('data', function(data) { - buf += data; - }); - - ps.on('error', function(err) { - Image.hasW3MDisplay = false; - Image._checkingW3MDisplay = false; - if (!callback) return; - return callback(err); - }); - - ps.on('exit', function(code) { - if (!buf.trim()) { - Image.hasW3MDisplay = false; - Image._checkingW3MDisplay = false; - if (!callback) return; - return callback(new Error('find failed.')); - } - Image._checkingW3MDisplay = false; - buf = buf.trim().split('\n')[0].trim(); - Image.w3mdisplay = buf; - Image.hasW3MDisplay = true; - return self._waitForW3MDisplay(callback); - }); - - return; - } - - if (callback) callback(null, Image.w3mdisplay); -}; - /** * Helpers */ @@ -7263,6 +7273,43 @@ var wideChars = new RegExp('([' + '\\uffe8-\\uffee' + '])', '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.statSync((dir === '/' ? '' : dir) + '/' + file); + } catch (e) { + stat = null; + } + + if (stat && stat.isDirectory()) { + out = read((dir === '/' ? '' : dir) + '/' + file); + if (out) return out; + } + } + + return null; + })(start); +} + /** * Helpers */ diff --git a/test/widget-image.js b/test/widget-image.js index 07ddaf1..311c636 100644 --- a/test/widget-image.js +++ b/test/widget-image.js @@ -7,7 +7,10 @@ screen = blessed.screen({ }); // To ensure our w3mimgdisplay search works: -blessed.image.w3mdisplay = '/does/not/exist'; +if (process.argv[2] === 'find') { + blessed.image.w3mdisplay = '/does/not/exist'; + process.argv.length = 2; +} var file = process.argv[2] || __dirname + '/test-image.png'; @@ -35,6 +38,14 @@ setTimeout(function() { image.rtop = 2; image.rleft = 7; screen.render(); + setTimeout(function() { + image.detach(); + screen.render(); + setTimeout(function() { + screen.append(image); + screen.render(); + }, 1000); + }, 1000); }, 1000); }, 5000); });