From 06400d089fcfe4224b03aea9c1e08c997670b0ff Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Thu, 4 Jul 2013 00:10:01 -0500 Subject: [PATCH] add multiline textareas. remove old hoverBg code. --- README.md | 26 +++ lib/widget.js | 376 ++++++++++++++++++++++------------------ test/widget-textarea.js | 30 ++++ 3 files changed, 267 insertions(+), 165 deletions(-) create mode 100644 test/widget-textarea.js diff --git a/README.md b/README.md index 40eba2c..636891a 100644 --- a/README.md +++ b/README.md @@ -221,6 +221,7 @@ The screen on which every other node renders. blessed app after exit. - **exec(file, args, options, callback)** - spawn a process in the foreground, return to blessed app after exit. executes callback on error or exit. +- **readEditor([options], callback)** - read data from text editor. #### Element (from Node) @@ -447,6 +448,31 @@ A box which allows text input. the resulting file. takes a callback which receives the final value. +#### Textarea (from Input/ScrollableText) + +A box which allows multiline text input. + +##### Options: + +- inherits all from Input/ScrollableText. + +##### Properties: + +- inherits all from Input/ScrollableText. + +##### Events: + +- inherits all from Input/ScrollableText. + +##### Methods: + +- inherits all from Input/ScrollableText. +- **readInput(callback)** - grab key events and start reading text from the + keyboard. takes a callback which receives the final value. +- **readEditor(callback)** - open text editor in `$EDITOR`, read the output from + the resulting file. takes a callback which receives the final value. + + #### Button (from Input) A button which can be focused and allows key and mouse input. diff --git a/lib/widget.js b/lib/widget.js index 01f2940..f588d35 100644 --- a/lib/widget.js +++ b/lib/widget.js @@ -880,9 +880,21 @@ Screen.prototype.exec = function(file, args, options, callback) { return ps; }; -Screen.prototype.readEditor = function(callback) { - var fs = require('fs') - , editor = process.env.EDITOR || 'vi' +Screen.prototype.readEditor = function(options, callback) { + if (typeof options === 'string') { + options = { editor: options }; + } + + if (!callback) { + callback = options; + options = null; + } + + options = options || {}; + + var self = this + , fs = require('fs') + , editor = options.editor || process.env.EDITOR || 'vi' , file = '/tmp/blessed.' + Math.random().toString(36) , args = [file] , opt; @@ -893,12 +905,19 @@ Screen.prototype.readEditor = function(callback) { cwd: process.env.HOME }; - return this.exec(editor, args, opt, function(err, success) { - if (err) return callback(err); - return fs.readFile(file, 'utf8', function(err, data) { - return fs.unlink(file, function() { - if (err) return callback(err); - return callback(null, data); + function writeFile(callback) { + if (!options.value) return callback(); + return fs.writeFile(file, options.value, callback); + } + + return writeFile(function() { + return self.exec(editor, args, opt, function(err, success) { + if (err) return callback(err); + return fs.readFile(file, 'utf8', function(err, data) { + return fs.unlink(file, function() { + if (err) return callback(err); + return callback(null, data); + }); }); }); }); @@ -992,9 +1011,9 @@ function Element(options) { } }); - //this.onScreenEvent('resize', function() { - // self.parseContent(); - //}); + // this.onScreenEvent('resize', function() { + // self.parseContent(); + // }); this.on('resize', function() { self.parseContent(); @@ -1004,27 +1023,32 @@ function Element(options) { self.parseContent(); }); + // if (this.options.hoverBg != null) { + // var hoverBg = convert(this.options.hoverBg); + // + // this.on('mouseover', function() { + // // XXX Possibly a better alternative for the below workaround. + // self._bg = self.bg; + // //if (self._bg == null) self._bg = self.bg; + // self.bg = hoverBg; + // self.screen.render(); + // }); + // + // this.on('mouseout', function() { + // // XXX Workaround + // // if (self.parent.type === 'list' + // // && self === self.parent.items[self.parent.selected] + // // && self.bg === self.parent.selectedBg) { + // // return; + // // } + // if (self._bg != null) self.bg = self._bg; + // self.screen.render(); + // }); + // } + if (this.options.hoverBg != null) { - var hoverBg = convert(this.options.hoverBg); - - this.on('mouseover', function() { - // XXX Possibly a better alternative for the below workaround. - self._bg = self.bg; - //if (self._bg == null) self._bg = self.bg; - self.bg = hoverBg; - self.screen.render(); - }); - - this.on('mouseout', function() { - // XXX Workaround - // if (self.parent.type === 'list' - // && self === self.parent.items[self.parent.selected] - // && self.bg === self.parent.selectedBg) { - // return; - // } - if (self._bg != null) self.bg = self._bg; - self.screen.render(); - }); + this.options.hoverEffects = this.options.hoverEffects || {}; + this.options.hoverEffects.bg = this.options.hoverBg; } if (this.options.hoverEffects) { @@ -1036,13 +1060,13 @@ function Element(options) { } }); - this.__h = {}; + this._htemp = {}; this.on('mouseover', function() { Object.keys(effects).forEach(function(key) { var val = effects[key]; - if (self.__h[key] == null) { - self.__h[key] = self[key]; + if (self._htemp[key] == null) { + self._htemp[key] = self[key]; } self[key] = val; }); @@ -1051,8 +1075,8 @@ function Element(options) { this.on('mouseout', function() { Object.keys(effects).forEach(function(key) { - if (self.__h[key] != null) { - self[key] = self.__h[key]; + if (self._htemp[key] != null) { + self[key] = self._htemp[key]; } }); self.screen.render(); @@ -2534,86 +2558,7 @@ Textbox.prototype.setEditor = function(callback) { this.focus(); - 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.censor) { - //// self.setContent(Array(self.value.length + 1).join('*')); - ////} else - //if (!self.secret) { - // self.setContent(value); - //} - //return callback(null, value); - - return self.setInput(callback); - }); -}; - -Textbox.prototype.editor = -Textbox.prototype.readEditor = -Textbox.prototype.setEditor = function(callback) { - var self = this; - - this.focus(); - - var fs = require('fs') - , editor = process.env.EDITOR || 'vi' - , file = '/tmp/blessed.' + Math.random().toString(36) - , args = [file] - , opt; - - opt = { - stdio: 'inherit', - env: process.env, - cwd: process.env.HOME - }; - - return this.screen.exec(editor, args, opt, function(err, success) { - if (err) return callback(err); - return fs.readFile(file, 'utf8', function(err, data) { - fs.unlink(file); - - if (err) return callback(err); - value = value.replace(/[\r\n]/g, ''); - self.value = 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); - }); - }); -}; - -Textbox.prototype.editor = -Textbox.prototype.readEditor = -Textbox.prototype.setEditor = function(callback) { - var self = this; - - this.focus(); - - return this.screen.readEditor(function(err, value) { + return this.screen.readEditor({ value: this.value }, function(err, value) { if (err) return callback(err); value = value.replace(/[\r\n]/g, ''); self.value = value; @@ -2638,13 +2583,160 @@ function Textarea(options) { if (!(this instanceof Textarea)) { return new Textarea(options); } - Input.call(this, options); + + ScrollableText.call(this, options); + + this.screen._listenKeys(this); + + this.value = options.value || ''; + + this.__updateCursor = this.updateCursor.bind(this); + this.on('resize', this.__updateCursor); + this.on('move', this.__updateCursor); } -Textarea.prototype.__proto__ = Input.prototype; +Textarea.prototype.__proto__ = ScrollableText.prototype; Textarea.prototype.type = 'textarea'; +Textarea.prototype.updateCursor = function() { + if (this.screen.focused !== this) return; + + var clen = this._clines.length; + + var last = this._clines[this._clines.length-1]; + if (last.length === this.width - (this.border ? 2 : 0)) { + last = ''; + clen++; + } + + var line = Math.min( + clen - 1 - this.childBase, + this.height - (this.border ? 2 : 0)); + + this.screen.program.cup( + this.top + 1 + (this.border ? 1 : 0) + line, + this.left + 1 + (this.border ? 1 : 0) + last.length); +}; + +Textarea.prototype.input = +Textarea.prototype.readInput = +Textarea.prototype.setInput = function(callback) { + var self = this; + + if (this._timeout != null) { + clearTimeout(this._timeout); + delete this._timeout; + } + + this.focus(); + + this.screen.grabKeys = true; + + this.updateCursor(); + this.screen.program.showCursor(); + this.screen.program.sgr('normal'); + + this._callback = function(err, value) { + self.screen.program.hideCursor(); + self._timeout = setTimeout(function() { + self.screen.grabKeys = false; + }, 1); + return err + ? callback(err) + : callback(null, value); + }; + + this.__listener = this._listener.bind(this); + this.on('keypress', this.__listener); +}; + +Textarea.prototype._listener = function(ch, key) { + var callback = this._callback + , value = this.value; + + if (key.name === 'enter') { + ch = '\n'; + } + + // TODO: Handle directional keys. + if (key.name === 'left' || key.name === 'right' + || key.name === 'up' || key.name === 'down') { + ; + } + + // if (key.name === 'escape' || key.name === 'enter') { + if (key.name === 'escape') { + delete this._callback; + this.removeListener('keypress', this.__listener); + delete this.__listener; + callback(null, key.name === 'enter' ? value : null); + } else if (key.name === 'backspace') { + if (this.value.length) { + this.value = this.value.slice(0, -1); +/* + var last = this._clines[this._clines.length-1]; + if (last.length === this.width - (this.border ? 2 : 0)) { + last = ''; + } + if (last.length === 0) { + this.screen.program.cuu(); + this.screen.program.cuf(this.width - (this.border ? 2 : 0)); + } else { + this.screen.program.cub(); + } +*/ + } + } else { + if (ch) { + this.value += ch; +/* + var last = this._clines[this._clines.length-1]; + if (last.length === this.width - (this.border ? 2 : 0)) { + last = ''; + } + if (last.length < this.width - (this.border ? 2 : 0) - 1) { + this.screen.program.cuf(); + } else { + this.screen.program.cud(); + this.screen.program.cub(this.width - (this.border ? 2 : 0)); + } +*/ + } + } + + if (this.value !== value) { + this.setContent(this.value); + this.updateCursor(); + this.screen.render(); + } +}; + +Textarea.prototype.submit = function() { + // return this._listener(null, { name: 'enter' }); + return this._listener('\x1b', { name: 'escape' }); +}; + +Textarea.prototype.cancel = function() { + return this._listener('\x1b', { name: 'escape' }); +}; + +Textarea.prototype.editor = +Textarea.prototype.readEditor = +Textarea.prototype.setEditor = function(callback) { + var self = this; + + this.focus(); + + return this.screen.readEditor({ value: this.value }, function(err, value) { + if (err) return callback(err); + self.value = value; + self.setContent(self.value); + self.screen.render(); + return self.setInput(callback); + }); +}; + /** * Button */ @@ -2859,52 +2951,6 @@ function attrCode(code, cur) { return (flags << 18) | (fg << 9) | bg; } -function readEditor(callback) { - var spawn = require('child_process').spawn - , fs = require('fs') - , editor = process.env.EDITOR || 'vi' - , file = '/tmp/blessed.' + Math.random().toString(36); - - var write = process.stdout.write; - process.stdout.write = function() {}; - - try { - process.stdin.pause(); - } catch (e) { - ; - } - - var resume = function() { - try { - process.stdin.resume(); - } catch (e) { - ; - } - process.stdout.write = write; - }; - - var ps = spawn(editor, [file], { - stdio: 'inherit', - env: process.env, - cwd: process.env.HOME - }); - - ps.on('error', function(err) { - resume(); - return callback(err); - }); - - ps.on('exit', function(code) { - resume(); - return fs.readFile(file, 'utf8', function(err, data) { - return fs.unlink(file, function() { - if (err) return callback(err); - return callback(null, data); - }); - }); - }); -} - function sp(line, width, align) { if (!align) return line; diff --git a/test/widget-textarea.js b/test/widget-textarea.js new file mode 100644 index 0000000..398cc31 --- /dev/null +++ b/test/widget-textarea.js @@ -0,0 +1,30 @@ +var blessed = require('blessed'); + +var screen = blessed.screen({ + tput: true +}); + +var box = blessed.textarea({ + parent: screen, + // Possibly support: + // align: 'center', + bg: 'blue', + height: 'half', + width: 'half', + top: 'center', + left: 'center' +}); + +screen.render(); + +screen.key('q', function() { + process.exit(0); +}); + +screen.key('i', function() { + box.readInput(function() {}); +}); + +screen.key('e', function() { + box.readEditor(function() {}); +});