From 7981f8d74979b1cf0388b8ad3618b0365f205937 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Wed, 17 Jul 2013 00:53:34 -0500 Subject: [PATCH] add shrinkBox. add forms. add RadioSet. misc refactor. --- README.md | 125 ++++++++++++- lib/widget.js | 439 +++++++++++++++++++++++++++++++++++++++----- test/widget-form.js | 125 +++++++++++++ 3 files changed, 638 insertions(+), 51 deletions(-) create mode 100644 test/widget-form.js diff --git a/README.md b/README.md index 1211530..24dd418 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,7 @@ The base node which everything inherits from. ##### Properties: - inherits all from EventEmitter. +- **type** - type of the node (e.g. `box`). - **options** - original options object. - **parent** - parent node. - **screen** - parent screen. @@ -241,7 +242,8 @@ The base element. - **label** - a simple text label for the element. - **align** - text alignment: `left`, `center`, or `right`. - **valign** - vertical text alignment: `top`, `middle`, or `bottom`. -- **shrink** - shrink/flex/grow to content width during render. +- **shrink** - shrink/flex/grow to content width/height during render. +- **shrinkBox** - shrink/flex/grow to combined coordinates of all child boxes. - **padding** - amount of padding on the inside of the element. ##### Properties: @@ -268,6 +270,7 @@ The base element. - **rright** - calculated relative right offset. - **rtop** - calculated relative top offset. - **rbottom** - calculated relative bottom offset. +- **name** - name of the element. useful for form submission. ##### Events: @@ -456,6 +459,39 @@ pre-existing newlines and escape codes. - inherits all from ScrollableBox. + +#### Form (from Box) + +A form which can contain form elements. + +##### Options: + +- inherits all from Box. +- **keys** - allow default keys (tab, vi keys, enter). +- **vi** - allow vi keys. + +##### Properties: + +- inherits all from Box. +- **submission** - last submitted data. + +##### Events: + +- inherits all from Box. +- **submit** - form is submitted. receives a data object. +- **cancel** - form is discarded. +- **reset** - form is cleared. + +##### Methods: + +- inherits all from Box. +- **focusNext()** - focus next form element. +- **focusPrevious()** - focus previous form element. +- **submit()** - submit the form. +- **cancel()** - discard the form. +- **reset()** - clear the form. + + #### Input (from Box) A form input. @@ -476,6 +512,9 @@ A box which allows text input. ##### Events: - inherits all from Input. +- **submit** - value is submitted (enter). +- **cancel** - value is discared (escape). +- **action** - either submit or cancel. ##### Methods: @@ -502,6 +541,9 @@ A box which allows multiline text input. ##### Events: - inherits all from Input/ScrollableText. +- **submit** - value is submitted (enter). +- **cancel** - value is discared (escape). +- **action** - either submit or cancel. ##### Methods: @@ -542,7 +584,7 @@ A button which can be focused and allows key and mouse input. #### ProgressBar (from Input) -A progress bar allowing various styles. +A progress bar allowing various styles. This can also be used as a form input. ##### Options: @@ -551,6 +593,9 @@ A progress bar allowing various styles. (can be contained in `style`: e.g. `style.bar.fg`). - **ch** - the character to fill the bar with (default is space). - **filled** - the amount filled (0 - 100). +- **value** - same as `filled`. +- **keys** - enable key support. +- **mouse** - enable mouse support. ##### Properties: @@ -566,6 +611,7 @@ A progress bar allowing various styles. - inherits all from Input. - **progress(amount)** - progress the bar by a fill amount. +- **setProgress(amount)** - set progress to specific amount. - **reset()** - reset the bar. @@ -598,6 +644,81 @@ A very simple file manager for selecting files. - **reset([cwd], [callback])** - reset back to original cwd. +#### Checkbox (from Input) + +A checkbox which can be used in a form element. + +##### Options: + +- inherits all from Input. +- **checked** - whether the element is checked or not. +- **mouse** - enable mouse support. + +##### Properties: + +- inherits all from Input. +- **text** - the text next to the checkbox (do not use setContent, use + `check.text = ''`). +- **checked** - whether the element is checked or not. +- **value** - same as `checked`. + +##### Events: + +- inherits all from Input. +- **check** - received when element is checked. +- **uncheck** received when element is unchecked. + +##### Methods: + +- inherits all from Input. +- **check()** - check the element. +- **uncheck()** - uncheck the element. +- **toggle()** - toggle checked state. + + +#### RadioSet (from Box) + +An element wrapping RadioButtons. RadioButtons within this element will be +mutually exclusive with each other. + +##### Options: + +- inherits all from Box. + +##### Properties: + +- inherits all from Box. + +##### Events: + +- inherits all from Box. + +##### Methods: + +- inherits all from Box. + + +#### RadioButton (from Checkbox) + +A radio button which can be used in a form element. + +##### Options: + +- inherits all from Checkbox. + +##### Properties: + +- inherits all from Checkbox. + +##### Events: + +- inherits all from Checkbox. + +##### Methods: + +- inherits all from Checkbox. + + ### Positioning Offsets may be a number, a percentage (e.g. `50%`), or a keyword (e.g. diff --git a/lib/widget.js b/lib/widget.js index 4853ac4..9cc91fa 100644 --- a/lib/widget.js +++ b/lib/widget.js @@ -119,8 +119,8 @@ Node.prototype.remove = function(element) { if (this.type !== 'screen') { i = this.screen.clickable.indexOf(element); if (~i) this.screen.clickable.splice(i, 1); - i = this.screen.input.indexOf(element); - if (~i) this.screen.input.splice(i, 1); + i = this.screen.keyable.indexOf(element); + if (~i) this.screen.keyable.splice(i, 1); } if (this.type === 'screen' && this.focused === element) { @@ -267,7 +267,7 @@ function Screen(options) { this.hover = null; this.history = []; this.clickable = []; - this.input = []; + this.keyable = []; this.grabKeys = false; this.lockKeys = false; this.focused; @@ -372,6 +372,7 @@ Screen.prototype._listenMouse = function(el) { var self = this; if (el && !~this.clickable.indexOf(el)) { + el.clickable = true; this.clickable.push(el); } @@ -471,7 +472,8 @@ Screen.prototype._listenMouse = function(el) { Screen.prototype._listenKeys = function(el) { var self = this; - if (el && !~this.input.indexOf(el)) { + if (el && !~this.keyable.indexOf(el)) { + el.keyable = true; // Listen for click, but do not enable // mouse if it's not enabled yet. if (el.options.autoFocus !== false) { @@ -480,7 +482,7 @@ Screen.prototype._listenKeys = function(el) { el.on('click', el.focus.bind(el)); this._listenedMouse = lm; } - this.input.push(el); + this.keyable.push(el); } if (this._listenedKeys) return; @@ -509,11 +511,11 @@ Screen.prototype._listenKeys = function(el) { return; } - if (~self.input.indexOf(focused)) { + if (~self.keyable.indexOf(focused)) { focused.emit('keypress', ch, key); focused.emit('key ' + key.full, ch, key); - // self.emit('element keypress', focused, ch, key); - // self.emit('element key ' + key.full, focused, ch, key); + self.emit('element keypress', focused, ch, key); + self.emit('element key ' + key.full, focused, ch, key); } }); }; @@ -1075,25 +1077,25 @@ Screen.prototype.codeAttr = function(code) { }; Screen.prototype.focus = function(offset) { - var shown = this.input.filter(function(el) { + var shown = this.keyable.filter(function(el) { return el.visible; }); if (!shown.length || !offset) return; - var i = this.input.indexOf(this.focused); + var i = this.keyable.indexOf(this.focused); if (!~i) return; if (offset > 0) { while (offset--) { - if (++i > this.input.length - 1) i = 0; - if (!this.input[i].visible) offset++; + if (++i > this.keyable.length - 1) i = 0; + if (!this.keyable[i].visible) offset++; } } else { offset = -offset; while (offset--) { - if (--i < 0) i = this.input.length - 1; - if (!this.input[i].visible) offset++; + if (--i < 0) i = this.keyable.length - 1; + if (!this.keyable[i].visible) offset++; } } - return this.input[i].focus(); + return this.keyable[i].focus(); }; Screen.prototype.focusPrev = function() { @@ -1359,6 +1361,8 @@ function Element(options) { Node.call(this, options); + this.name = options.name; + this.position = { left: options.left || 0, right: options.right || 0, @@ -1408,7 +1412,7 @@ function Element(options) { this.screen._listenMouse(this); } - if (options.input) { + if (options.input || options.keyable) { this.screen._listenKeys(this); } @@ -1968,7 +1972,9 @@ Box.prototype.render = function(stop) { , xll , yll , ret - , cci; + , cci + , el + , i; if (this.position.width) { xl = xi_ + this.width; @@ -1995,6 +2001,29 @@ Box.prototype.render = function(stop) { } } + // TODO: Possibly do both shrinkBox and shrink + // and use whichever values are higher. + // Slower than below, but more foolproof. + if (this.options.shrinkBox && this.children.length) { + xl = 0, yl = 0; + for (i = 0; i < this.children.length; i++) { + el = this.children[i]; + + // Recurse + el.options._shrinkBox = !!el.options.shrinkBox; + el.options.shrinkBox = true; + + ret = el.render(true); + + // Reset + el.options.shrinkBox = el.options._shrinkBox; + delete el.options._shrinkBox; + + if (ret.xl > xl) xl = ret.xl; + if (ret.yl > yl) yl = ret.yl; + } + } + // TODO: Check for 'center', recalculate yi, and xi. Better // yet, simply move this check into this.left/width/etc. if (this.shrink) { @@ -2039,6 +2068,11 @@ Box.prototype.render = function(stop) { } } + // NOTE: Won't work because parent is rendered first. + //if (this.options.shrinkBox && this._lastPos) { + // xl = this._lastPos.xl, yl = this._lastPos.yl; + //} + // TODO: // Calculate whether we moved/resized by checking the previous _lastPos. // Maybe clear based on that. Possibly emit events here. @@ -2046,9 +2080,14 @@ Box.prototype.render = function(stop) { xi: xi_, xl: xl, yi: yi_, - yl: yl + yl: yl, + //mxl: xl, + //myl: yl }; + //this.parent._lastPos.mxl = Math.max(this.parent._lastPos.mxl, xl); + //this.parent._lastPos.myl = Math.max(this.parent._lastPos.myl, yl); + if (stop) return ret; battr = this.border @@ -2574,6 +2613,7 @@ function List(options) { ScrollableBox.call(this, options); + this.value = ''; this.items = []; this.ritems = []; this.selected = 0; @@ -2782,6 +2822,9 @@ List.prototype.remove = function(child) { this._remove(child); }; +List.prototype.appendItem = List.prototype.add; +List.prototype.removeItem = List.prototype.remove; + List.prototype.setItems = function(items) { var i = 0 , original = this.items.slice() @@ -2825,6 +2868,7 @@ List.prototype.select = function(index) { var diff = index - this.selected; this.selected = index; + this.value = this.ritems[this.selected]; this.scroll(diff); }; @@ -2977,6 +3021,203 @@ ScrollableText.prototype._recalculateIndex = function() { this.contentIndex = t; }; +/** + * Form + */ + +function Form(options) { + var self = this; + + if (!(this instanceof Form)) { + return new Form(options); + } + + Box.call(this, options); + + if (options.keys) { + this.screen._listenKeys(this); + this.screen.on('element keypress', function(el, ch, key) { + // Make sure we're not entering input into a textbox. + if (self.screen.grabKeys || self.screen.lockKeys) { + return; + } + + // Make sure we're a form or input element. + if (el !== self && !el.hasAncestor(self)) { + return; + } + + if ((key.name === 'tab' && !key.shift) || key.name === 'down' || (options.vi && key.name === 'j')) { + self.focusNext(); + return; + } + + if ((key.name === 'tab' && key.shift) || key.name === 'up' || (options.vi && key.name === 'k')) { + self.focusPrevious(); + return; + } + + if (key.name === 'escape') { + self.focus(); + return; + } + }); + } +} + +Form.prototype.__proto__ = Box.prototype; + +Form.prototype.type = 'form'; + +Form.prototype._refresh = function() { + if (!this._children) { + var out = []; + + this.children.forEach(function fn(el) { + if (el.keyable) out.push(el); + el.children.forEach(fn); + }); + + this._children = out; + } +}; + +Form.prototype.next = function() { + this._refresh(); + + if (!this._selected) { + return this._selected = this._children[0]; + } + + var i = this._children.indexOf(this._selected); + if (!~i || !this._children[i + 1]) { + return this._selected = this._children[0]; + } + + return this._selected = this._children[i + 1]; +}; + +Form.prototype.previous = function() { + this._refresh(); + + if (!this._selected) { + return this._selected = this._children[this._children.length - 1]; + } + + var i = this._children.indexOf(this._selected); + if (!~i || !this._children[i - 1]) { + return this._selected = this._children[this._children.length - 1]; + } + + return this._selected = this._children[i - 1]; +}; + +Form.prototype.focusNext = function() { + this.next().focus(); +}; + +Form.prototype.focusPrevious = function() { + this.previous().focus(); +}; + +Form.prototype.submit = function() { + var self = this + , out = {}; + + (function get(el) { + if (el.value != null) { + var name = el.name || el.type; + if (Array.isArray(out[name])) { + out[name].push(el.value); + } else if (out[name]) { + out[name] = [out[name], el.value]; + } else { + out[name] = el.value; + } + } + el.children.forEach(get); + })(this); + + this.emit('submit', out); + + return this.submission = out; +}; + +Form.prototype.cancel = function() { + this.emit('cancel'); +}; + +Form.prototype.reset = function() { + (function clear(el) { + switch (el.type) { + case 'screen': + break; + case 'box': + break; + case 'text': + break; + case 'line': + break; + case 'scrollable-box': + break; + case 'list': + el.select(0); + return; + case 'form': + break; + case 'input': + break; + case 'textbox': + el.clearInput(); + return; + case 'textarea': + el.clearInput(); + return; + case 'button': + break; + case 'progress-bar': + el.setProgress(0); + break; + case 'file-manager': + el.refresh(el.options.cwd); + return; + case 'checkbox': + el.uncheck(); + return; + case 'radio-set': + break; + case 'radio-button': + el.uncheck(); + return; + case 'prompt': + break; + case 'question': + break; + case 'message': + break; + case 'info': + break; + case 'loading': + break; + case 'pick-list': + el.select(0); + break; + case 'list-bar': + //el.select(0); + break; + case 'dir-manager': + el.refresh(el.options.cwd); + return; + case 'passbox': + el.clearInput(); + return; + } + el.children.forEach(clear); + })(this); + + this.emit('reset'); +}; + /** * Input */ @@ -2997,6 +3238,8 @@ Input.prototype.type = 'input'; */ function Textbox(options) { + var self = this; + if (!(this instanceof Textbox)) { return new Textbox(options); } @@ -3009,8 +3252,6 @@ function Textbox(options) { this.secret = options.secret; this.censor = options.censor; - var self = this; - this.on('resize', updateCursor); this.on('move', updateCursor); @@ -3057,6 +3298,17 @@ Textbox.prototype.setInput = function(callback) { self.screen.restoreFocus(); } + if (err) { + self.emit('error', err); + } else if (value != null) { + self.emit('submit', value); + } else { + self.emit('cancel', value); + } + self.emit('action', value); + + if (!callback) return; + return err ? callback(err) : callback(null, value); @@ -3072,7 +3324,6 @@ Textbox.prototype._listener = function(ch, key) { if (key.name === 'escape' || key.name === 'enter') { delete this._callback; - this.value = ''; this.removeListener('keypress', this.__listener); delete this.__listener; callback(null, key.name === 'enter' ? value : null); @@ -3140,7 +3391,7 @@ Textbox.prototype.readEditor = Textbox.prototype.setEditor = function(callback) { var self = this; return this.screen.readEditor({ value: this.value }, function(err, value) { - if (err) return callback(err); + if (err) return callback && callback(err); value = value.replace(/[\r\n]/g, ''); self.value = value; return self.readInput(callback); @@ -3243,6 +3494,17 @@ Textarea.prototype.setInput = function(callback) { self.screen.restoreFocus(); } + if (err) { + self.emit('error', err); + } else if (value != null) { + self.emit('submit', value); + } else { + self.emit('cancel', value); + } + self.emit('action', value); + + if (!callback) return; + return err ? callback(err) : callback(null, value); @@ -3315,7 +3577,7 @@ Textarea.prototype.readEditor = Textarea.prototype.setEditor = function(callback) { var self = this; return this.screen.readEditor({ value: this.value }, function(err, value) { - if (err) return callback(err); + if (err) return callback && callback(err); self.value = value; self.setContent(self.value); self._typeScroll(); @@ -3390,17 +3652,61 @@ function ProgressBar(options) { return new ProgressBar(options); } Input.call(this, options); + this.filled = options.filled || 0; if (typeof this.filled === 'string') { this.filled = +this.filled.slice(0, -1); } + this.value = this.filled; + this.ch = options.ch || ' '; + if (!this.style.bar) { this.style.bar = {}; this.style.bar.fg = options.barFg; this.style.bar.bg = options.barBg; } + this.orientation = options.orientation || 'horizontal'; + + if (options.keys) { + this.on('keypress', function(ch, key) { + var back, forward; + if (self.orientation === 'horizontal') { + back = ['left', 'h']; + forward = ['right', 'l']; + } else if (self.orientation === 'vertical') { + back = ['down', 'j']; + forward = ['up', 'k']; + } + if (key.name === back[0] || (options.vi && key.name === back[1])) { + self.progress(-5); + self.screen.render(); + return; + } + if (key.name === forward[0] || (options.vi && key.name === forward[1])) { + self.progress(5); + self.screen.render(); + return; + } + }); + } + + if (options.mouse) { + this.on('click', function(data) { + var x, y, m, p; + if (self.orientation === 'horizontal') { + x = data.x - self.left; + m = self.width - (self.border ? 2 : 0) - self.padding * 2; + p = x / m * 100 | 0; + } else if (self.orientation === 'vertical') { + y = data.y - self.top; + m = self.height - (self.border ? 2 : 0) - self.padding * 2; + p = y / m * 100 | 0; + } + self.setProgress(p); + }); + } } ProgressBar.prototype.__proto__ = Input.prototype; @@ -3444,11 +3750,18 @@ ProgressBar.prototype.progress = function(filled) { if (this.filled === 100) { this.emit('complete'); } + this.value = this.filled; +}; + +ProgressBar.prototype.setProgress = function(filled) { + this.filled = 0; + this.progress(filled); }; ProgressBar.prototype.reset = function() { this.emit('reset'); this.filled = 0; + this.value = this.filled; }; /** @@ -3467,6 +3780,8 @@ function FileManager(options) { List.call(this, options); this.cwd = options.cwd || process.cwd(); + this.file = this.cwd; + this.value = this.cwd; this.on('select', function(item) { var value = item.content.replace(/\{[^{}]+\}/g, '').replace(/@$/, '') @@ -3476,6 +3791,8 @@ function FileManager(options) { if (err) { return self.emit('error', err, file); } + self.file = file; + self.value = file; if (stat.isDirectory()) { self.emit('cd', file, self.cwd); self.cwd = file; @@ -3497,8 +3814,10 @@ FileManager.prototype.refresh = function(cwd, callback) { cwd = null; } - var self = this - , cwd = cwd || this.cwd; + var self = this; + + if (cwd) this.cwd = cwd; + else cwd = this.cwd; return fs.readdir(cwd, function(err, list) { if (err && err.code === 'ENOENT') { @@ -3636,30 +3955,32 @@ function Checkbox(options) { Input.call(this, options); - this.value = options.value || ''; - this.checked = options.checked || false; + this.text = options.content || options.text || ''; + this.checked = this.value = options.checked || false; this.on('keypress', function(ch, key) { if (key.name === 'enter' || key.name === 'space') { - self.check(); + self.toggle(); + self.screen.render(); } }); - if (this.options.mouse) { + if (options.mouse) { this.on('click', function() { self.check(); + self.screen.render(); }); } - this.on('focus', function() { - self.program.saveCursor(); - self.program.cup(this.top, this.left + 1); - self.program.showCursor(); + this.on('focus', function(old) { + self.screen.program.saveCursor(); + self.screen.program.cup(this.top, this.left + 1); + self.screen.program.showCursor(); }); this.on('blur', function() { - self.program.hideCursor(); - self.program.restoreCursor(); + self.screen.program.hideCursor(); + self.screen.program.restoreCursor(); }); } @@ -3669,19 +3990,23 @@ Checkbox.prototype.type = 'checkbox'; Checkbox.prototype._render = Checkbox.prototype.render; Checkbox.prototype.render = function(stop) { - this.setContent('[' + (this.checked ? 'x' : ' ') + '] ' + this.value); + if (this.type === 'radio-button') { + this.setContent('(' + (this.checked ? '*' : ' ') + ') ' + this.text); + } else { + this.setContent('[' + (this.checked ? 'x' : ' ') + '] ' + this.text); + } return this._render(stop); }; Checkbox.prototype.check = function() { if (this.checked) return; - this.checked = true; + this.checked = this.value = true; this.emit('check'); }; Checkbox.prototype.uncheck = function() { if (!this.checked) return; - this.checked = false; + this.checked = this.value = false; this.emit('uncheck'); }; @@ -3691,12 +4016,24 @@ Checkbox.prototype.toggle = function() { : this.check(); }; -Checkbox.prototype.setChecked = function(val) { - val = !!val; - if (this.checked === val) return; - this.checked = val; - this.emit('check', val); -}; +/** + * RadioSet + */ + +function RadioSet(options) { + var self = this; + + if (!(this instanceof RadioSet)) { + return new RadioSet(options); + } + + Box.call(this, options); +} + +RadioSet.prototype.__proto__ = Box.prototype; + +RadioSet.prototype.type = 'radio-set'; + /** * RadioButton @@ -3711,10 +4048,9 @@ function RadioButton(options) { Checkbox.call(this, options); - self.group = options.group || []; - this.on('check', function() { - self.group.forEach(function(el) { + if (self.parent.type !== 'radio-set') return; + self.parent.children.forEach(function(el) { if (el === self) return; el.uncheck(); }); @@ -3725,6 +4061,8 @@ RadioButton.prototype.__proto__ = Checkbox.prototype; RadioButton.prototype.type = 'radio-button'; +RadioButton.prototype.toggle = RadioButton.prototype.check; + /** * Prompt */ @@ -4075,7 +4413,7 @@ function PickList(options) { PickList.prototype.__proto__ = List.prototype; -PickList.prototype.type = 'popup-menu'; +PickList.prototype.type = 'pick-list'; PickList.prototype.pick = function(callback) { this.screen.saveFocus(); @@ -4109,7 +4447,7 @@ function Listbar(options) { Listbar.prototype.__proto__ = Box.prototype; -Listbar.prototype.type = 'menubar'; +Listbar.prototype.type = 'listbar'; Listbar.prototype.setOptions = Listbar.prototype.setCommands = @@ -4387,6 +4725,7 @@ exports.Line = exports.line = Line; exports.ScrollableBox = exports.scrollablebox = ScrollableBox; exports.List = exports.list = List; exports.ScrollableText = exports.scrollabletext = ScrollableText; +exports.Form = exports.form = Form; exports.Input = exports.input = Input; exports.Textbox = exports.textbox = Textbox; exports.Textarea = exports.textarea = Textarea; @@ -4395,7 +4734,9 @@ exports.ProgressBar = exports.progressbar = ProgressBar; exports.FileManager = exports.filemanager = FileManager; exports.Checkbox = exports.checkbox = Checkbox; +exports.RadioSet = exports.radioset = RadioSet; exports.RadioButton = exports.radiobutton = RadioButton; + exports.Prompt = exports.prompt = Prompt; exports.Question = exports.question = Question; exports.Message = exports.message = Message; diff --git a/test/widget-form.js b/test/widget-form.js new file mode 100644 index 0000000..16202df --- /dev/null +++ b/test/widget-form.js @@ -0,0 +1,125 @@ +var blessed = require('blessed') + , screen = blessed.screen(); + +var form = blessed.form({ + parent: screen, + mouse: true, + keys: true, + vi: true, + left: 0, + top: 0, + width: '100%', + height: 5, + bg: 'green', + content: 'foobar' +}); + +form.on('submit', function(data) { + output.setContent(JSON.stringify(data, null, 2)); + screen.render(); +}); + +var set = blessed.radioset({ + parent: form, + left: 0, + top: 0, + shrinkBox: true, + height: 1, + bg: 'magenta' +}); + +var radio1 = blessed.radiobutton({ + parent: set, + mouse: true, + keys: true, + shrink: true, + bg: 'magenta', + height: 1, + left: 0, + top: 0, + name: 'radio1', + content: 'radio1' +}); + +var radio2 = blessed.radiobutton({ + parent: set, + mouse: true, + keys: true, + shrink: true, + bg: 'magenta', + height: 1, + left: 15, + top: 0, + name: 'radio2', + content: 'radio2' +}); + +var text = blessed.textbox({ + parent: form, + mouse: true, + keys: true, + bg: 'blue', + height: 1, + width: 20, + left: 1, + top: 1, + name: 'text' +}); + +text.on('focus', function() { + text.readInput(); +}); + +var check = blessed.checkbox({ + parent: form, + mouse: true, + keys: true, + shrink: true, + bg: 'magenta', + height: 1, + left: 24, + top: 1, + name: 'check', + content: 'check' +}); + +var submit = blessed.button({ + parent: form, + mouse: true, + keys: true, + height: 1, + left: 30, + top: 0, + shrink: true, + bg: 'blue', + name: 'submit', + content: 'submit', + focusEffects: { + bg: 'red' + } +}); + +submit.on('press', function() { + form.submit(); +}); + +var output = blessed.scrollabletext({ + parent: screen, + mouse: true, + keys: true, + left: 0, + top: 5, + width: '100%', + bg: 'red', + content: 'foobar' +}); + +screen.key('q', function() { + return process.exit(0); +}); + +form.focus(); + +form.submit(); + +screen.render();