diff --git a/lib/widget.js b/lib/widget.js index 38b7605..54c30f9 100644 --- a/lib/widget.js +++ b/lib/widget.js @@ -2563,6 +2563,7 @@ Element.prototype.free = function() { Element.prototype.destroy = function() { this.detach(); this.free(); + this.emit('destroy'); }; Element.prototype.hide = function() { @@ -3070,6 +3071,10 @@ Element.prototype.enableDrag = function(verify) { return; } + // This can happen in edge cases where the user is + // already dragging and element when it is detached. + if (!self.parent) return; + var ox = self._drag.x , oy = self._drag.y , px = self.parent.aleft @@ -8467,7 +8472,7 @@ Terminal.prototype.bootstrap = function() { // Incoming keys and mouse inputs. // NOTE: Cannot pass mouse events - coordinates will be off! - this.screen.program.input.on('data', function(data) { + this.screen.program.input.on('data', this._onData = function(data) { if (self.screen.focused === self && !self._isMouse(data)) { self.handler(data); } @@ -8582,6 +8587,11 @@ Terminal.prototype.bootstrap = function() { }); this.screen._listenKeys(this); + + this.on('destroy', function() { + self.screen.program.removeListener('data', self._onData); + self.pty.destroy(); + }); }; Terminal.prototype.write = function(data) { @@ -8779,6 +8789,44 @@ function Image(options) { self.setImage(self._lastFile); }); + this.onScreenEvent('resize', function() { + self._needsRatio = true; + }); + + // Get images to overlap properly. Maybe not worth it: + // this.onScreenEvent('render', function() { + // self.screen.program.flush(); + // if (!self._noImage) return; + // function display(el, next) { + // if (el.type === 'image' && el.file) { + // el.setImage(el.file, next); + // } else { + // next(); + // } + // } + // function done(el) { + // el.children.forEach(recurse); + // } + // function recurse(el) { + // display(el, function() { + // var pending = el.children.length; + // el.children.forEach(function(el) { + // display(el, function() { + // if (!--pending) done(el); + // }); + // }); + // }); + // } + // recurse(self.screen); + // }); + + this.onScreenEvent('render', function() { + self.screen.program.flush(); + if (!self._noImage) { + self.setImage(self.file); + } + }); + if (this.options.file || this.options.img) { this.setImage(this.options.file || this.options.img); } @@ -8790,15 +8838,6 @@ 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 || {} @@ -8824,74 +8863,46 @@ Image.prototype.spawn = function(file, args, opt, callback) { Image.prototype.setImage = function(img, callback) { var self = this; + if (this._settingImage) { + this._queue = this._queue || []; + this._queue.push([img, callback]); + return; + } + this._settingImage = true; + + var reset = function(err, success) { + self._settingImage = false; + self._queue = self._queue || []; + var item = self._queue.shift(); + if (item) { + self.setImage(item[0], item[1]); + } + }; + if (Image.hasW3MDisplay === false) { + reset(); if (!callback) return; return callback(new Error('W3M Image Display not available.')); } if (!img) { + reset(); 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) { + reset(); if (!callback) return; return callback(err); } - return renderImage(ratio, function(err, success) { + return self.renderImage(img, ratio, function(err, success) { if (err) { + reset(); if (!callback) return; return callback(err); } @@ -8902,6 +8913,7 @@ Image.prototype.setImage = function(img, callback) { self.options.autofit = true; return self.imageSize(function(err, size) { if (err) { + reset(); if (!callback) return; return callback(err); } @@ -8913,6 +8925,7 @@ Image.prototype.setImage = function(img, callback) { && size.height === self._lastSize.height && self.aleft === self._lastSize.aleft && self.atop === self._lastSize.atop) { + reset(); if (!callback) return; return callback(null, success); } @@ -8933,24 +8946,103 @@ Image.prototype.setImage = function(img, callback) { self.screen.render(); self._noImage = false; - return renderImage(ratio, callback); + reset(); + return self.renderImage(img, ratio, callback); }); } + reset(); if (!callback) return; return callback(null, success); }); }); }; -Image.prototype.clearImage = function(callback) { +Image.prototype.renderImage = function(img, ratio, callback) { var self = this; + if (cp.execSync) { + callback = callback || function() {}; + try { + return callback(null, this.renderImageSync(img, ratio)); + } catch (e) { + return callback(e); + } + } + if (Image.hasW3MDisplay === false) { if (!callback) return; return callback(new Error('W3M Image Display not available.')); } + if (!ratio) { + if (!callback) return; + return callback(new Error('No ratio.')); + } + + // 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(); + }); +}; + +Image.prototype.clearImage = function(callback) { + var self = this; + + if (cp.execSync) { + callback = callback || function() {}; + try { + return callback(null, this.clearImageSync()); + } catch (e) { + return callback(e); + } + } + + if (Image.hasW3MDisplay === false) { + if (!callback) return; + return callback(new Error('W3M Image Display not available.')); + } if (!this._props) { if (!callback) return; @@ -8975,6 +9067,13 @@ Image.prototype.clearImage = function(callback) { , aleft = this._props.aleft , atop = this._props.atop; + if (this._drag) { + aleft -= 10; + atop -= 10; + width += 10; + height += 10; + } + var input = '6;' + aleft + ';' + atop + ';' @@ -8994,6 +9093,15 @@ Image.prototype.imageSize = function(callback) { var self = this; var img = this.file; + if (cp.execSync) { + callback = callback || function() {}; + try { + return callback(null, this.imageSizeSync()); + } catch (e) { + return callback(e); + } + } + if (Image.hasW3MDisplay === false) { if (!callback) return; return callback(new Error('W3M Image Display not available.')); @@ -9044,6 +9152,15 @@ Image.prototype.imageSize = function(callback) { Image.prototype.termSize = function(callback) { var self = this; + if (cp.execSync) { + callback = callback || function() {}; + try { + return callback(null, this.termSizeSync()); + } catch (e) { + return callback(e); + } + } + if (Image.hasW3MDisplay === false) { if (!callback) return; return callback(new Error('W3M Image Display not available.')); @@ -9094,11 +9211,20 @@ Image.prototype.termSize = function(callback) { Image.prototype.getPixelRatio = function(callback) { var self = this; + if (cp.execSync) { + callback = callback || function() {}; + try { + return callback(null, this.getPixelRatioSync()); + } catch (e) { + return callback(e); + } + } + // XXX We could cache this, but sometimes it's better // to recalculate to be pixel perfect. - // if (this._ratio) { - // return callback(null, this._ratio); - // } + if (this._ratio && !this._needsRatio) { + return callback(null, this._ratio); + } return this.termSize(function(err, dimensions) { if (err) return callback(err); @@ -9108,10 +9234,201 @@ Image.prototype.getPixelRatio = function(callback) { th: dimensions.height / self.screen.height }; + self._needsRatio = false; + return callback(null, self._ratio); }); }; +Image.prototype.renderImageSync = function(img, ratio) { + var self = this; + + if (Image.hasW3MDisplay === false) { + throw new Error('W3M Image Display not available.'); + } + + if (!ratio) { + throw new Error('No ratio.'); + } + + // clearImage unsets these: + var _file = this.file; + var _lastSize = this._lastSize; + + this.clearImageSync(); + + this.file = _file; + this._lastSize = _lastSize; + + var width = this.width * ratio.tw | 0 + , height = this.height * ratio.th | 0 + , aleft = this.aleft * ratio.tw | 0 + , atop = this.atop * ratio.th | 0; + + var input = '0;1;' + + aleft + ';' + + atop + ';' + + width + ';' + + height + ';;;;;' + + img + + '\n4;\n3;\n'; + + this._props = { + aleft: aleft, + atop: atop, + width: width, + height: height + }; + + try { + cp.execFileSync(Image.w3mdisplay, [], { + env: process.env, + encoding: 'utf8', + input: input, + timeout: 1000 + }); + } catch (e) { + ; + } + + return true; +}; + +Image.prototype.clearImageSync = function() { + if (Image.hasW3MDisplay === false) { + throw new Error('W3M Image Display not available.'); + } + + if (!this._props) { + return false; + } + + var width = this._props.width + 2 + , height = this._props.height + 2 + , aleft = this._props.aleft + , atop = this._props.atop; + + if (this._drag) { + aleft -= 10; + atop -= 10; + width += 10; + height += 10; + } + + var input = '6;' + + aleft + ';' + + atop + ';' + + width + ';' + + height + + '\n4;\n3;\n'; + + delete this.file; + delete this._props; + delete this._lastSize; + + try { + cp.execFileSync(Image.w3mdisplay, [], { + env: process.env, + encoding: 'utf8', + input: input, + timeout: 1000 + }); + } catch (e) { + ; + } + + return true; +}; + +Image.prototype.imageSizeSync = function() { + var img = this.file; + + if (Image.hasW3MDisplay === false) { + throw new Error('W3M Image Display not available.'); + } + + if (!img) { + throw new Error('No image.'); + } + + var buf = ''; + var input = '5;' + img + '\n'; + + try { + buf = cp.execFileSync(Image.w3mdisplay, [], { + env: process.env, + encoding: 'utf8', + input: input, + timeout: 1000 + }); + } catch (e) { + ; + } + + var size = buf.trim().split(/\s+/); + + return { + raw: buf.trim(), + width: +size[0], + height: +size[1] + }; +}; + +Image.prototype.termSizeSync = function(_, recurse) { + if (Image.hasW3MDisplay === false) { + throw new Error('W3M Image Display not available.'); + } + + var buf = ''; + + try { + buf = cp.execFileSync(Image.w3mdisplay, ['-test'], { + env: process.env, + encoding: 'utf8', + timeout: 1000 + }); + } catch (e) { + ; + } + + if (!buf.trim()) { + // Bug: w3mimgdisplay will sometimes + // output nothing. Try again: + if (++recurse === 5) { + throw new Error('Term size not determined.'); + } + return this.termSizeSync(_, recurse); + } + + var size = buf.trim().split(/\s+/); + + return { + raw: buf.trim(), + width: +size[0], + height: +size[1] + }; +}; + +Image.prototype.getPixelRatioSync = function() { + var self = this; + + // XXX We could cache this, but sometimes it's better + // to recalculate to be pixel perfect. + if (this._ratio && !this._needsRatio) { + return this._ratio; + } + this._needsRatio = false; + + var dimensions = this.termSizeSync(); + + this._ratio = { + tw: dimensions.width / this.screen.width, + th: dimensions.height / this.screen.height + }; + + return this._ratio; +}; + Image.prototype.displayImage = function(callback) { return this.screen.displayImage(this.file, callback); }; diff --git a/test/widget-image.js b/test/widget-image.js index 413c7b9..1a42297 100644 --- a/test/widget-image.js +++ b/test/widget-image.js @@ -22,7 +22,8 @@ var image = blessed.image({ height: 'shrink', style: { bg: 'green' - } + }, + draggable: true }); setTimeout(function() { @@ -45,6 +46,7 @@ setTimeout(function() { screen.render(); setTimeout(function() { screen.append(image); + image.enableMouse(); screen.render(); }, 1000); }, 1000);