From f56af2324951137030e404b6c3bf5b0d2636aa36 Mon Sep 17 00:00:00 2001 From: sbosse Date: Mon, 21 Jul 2025 23:12:52 +0200 Subject: [PATCH] Mon 21 Jul 22:43:21 CEST 2025 --- js/web/app.js | 1439 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1439 insertions(+) create mode 100644 js/web/app.js diff --git a/js/web/app.js b/js/web/app.js new file mode 100644 index 0000000..5d9f038 --- /dev/null +++ b/js/web/app.js @@ -0,0 +1,1439 @@ +/** + ** ============================== + ** O O O OOOO + ** O O O O O O + ** O O O O O O + ** OOOO OOOO O OOO OOOO + ** O O O O O O O + ** O O O O O O O + ** OOOO OOOO O O OOOO + ** ============================== + ** Dr. Stefan Bosse http://www.bsslab.de + ** + ** COPYRIGHT: THIS SOFTWARE, EXECUTABLE AND SOURCE CODE IS OWNED + ** BY THE AUTHOR(S). + ** THIS SOURCE CODE MAY NOT BE COPIED, EXTRACTED, + ** MODIFIED, OR OTHERWISE USED IN A CONTEXT + ** OUTSIDE OF THE SOFTWARE SYSTEM. + ** + ** $AUTHORS: Stefan Bosse + ** $INITIAL: (C) 2006-2021 bLAB + ** $CREATED: 14-08-17 by sbosse. + ** $VERSION: 1.8.2 + ** + ** $INFO: + ** + ** WEB GUI toolkit supporting pages using blessed/curses emulation library. + ** App layout is similar to mobile application programs. + ** A set of styles can defined used as default styles for various elements. + ** + ** type styles = { + ** button?:{fg?,bg?,border?}, + ** checkbox?:{}, + ** dialog?:{fg?,bg?}, + ** filemanager?:{fg?,bg?,arrows?:{fg?,bg?},border?,box:{bg?,border?}, + ** label?:{fg?,bg?},input?:{fg?,bg?}}, + ** input:{fg?,bg?,border?,label?}, + ** keyboard?:{bg?,border?,label?:{fg,bg}}, + ** label:{bold?,fg?,bg?}, + ** + ** New: App Builder from compact template + ** + ** Template (builder) Type Interface + ** ================================= + ** + ** (@: smybolic identifier, parameter name, or wildcard place holder, $: type macro, ?:optional, ..:more, + ** st {a,b,..}: sub type of object type st) + ** + ** typeof @border = { type? : string = 'line'|'none'|.., fg:string is color, .. } + ** + ** typeof @styles = { + ** button?: { fg?, bg?, border? }, + ** input?: { border? }, + ** tree?: { border? }, + ** .., + ** @customstyle : { .. }, + ** .. + ** } + ** + ** Widget event handler: + ** + ** type handler = function (wname:string,wval:boolean|number|string) + ** type handlerT = function (wname:string,label:string,path:string,data:{name:string,parent:node,..}) + ** type handlerL = function (wname:string,label:string,data:{content:string,..}) + ** type handler1 = function (wname:string) is check handler + ** type handlerp = function (pname:string) is page handler + ** type handlerd = function (wname:string,data:*) + ** + ** Widget styles and attributes: + ** + ** $pos = top:number | left:number | right:number | center:boolean + ** $geom = width:number | hight:number + ** $cstyle = fg:string | bg:string + ** + ** Widget descriptor types: + ** + ** type button = { type='button', $pos?, $geom?, content:string, $cstyle, .. , onclick?:handler } + ** type label = { type='label', $pos?, $geom?,content:string, .. } + ** type info = { type='info', $pos?, $geom?, label:string, value?:string, .. } + ** type checkbox = { type='checkbox', $pos?, $geom?, text:string, value:string, .. , onclick?:handler, on?: on {check,uncheck} } + ** type input = { type='input', $pos, $geom?, wrap?:boolean, label:string, value?:string, onchange?:handler, .. } + ** type radiobutton = { type='radiobutton', $pos, $geom?, .. } + ** type group = { type='group', name?:string, @but1:radiobutton, @but2:radiobutton, .., onclick?:handler } + ** type tree = { type='tree', $pos, $geom, .. , onclick?:handlerT } + ** type list = { type='list', depth?:number, $pos, $geom, .. , onclick?:handlerL } + ** + ** type widget = button | label | checkbox | input | group | tree | .. + ** + ** The builder creates widget objects from descriptors that can be accessed by App.pages.@name. + ** + ** method (object of info).update (string) + ** method (object of tree).update ({}) + ** method (object of info).update (string) + ** method (object of info).setValue (string) + ** method (object of input).getValue () + ** method (object of widget).setStyle ({}) + ** + ** type on = { click?:handler|string, onclick?:handler|string, + ** check?:handler1, uncheck?:handler1, + ** keypress:{key: string | string [], handler: fucntion (@char,@key)] [], + ** selected?:handlerd, change?:handler, + ** show?:handlerp, hide?: handlerp } + ** + ** $pageparam = next:string | prev:string | on: on {show,hide} | show:function | hide:function + ** $widget = @name : widget + ** + ** typeof content = { + ** pages : { main: { $widget, $widget, .. , $pageparam, $pageparam, .. }, @p2: { @widget, .. }, .. } + ** info?: {} + ** } + ** + ** App creation (w/o builder, programmatically): + ** ============================================ + ** + ** var ui = App.App({ + ** pages : number, + ** styles : {}, + ** terminal: '', + ** title : string + ** }); + ** ui.pages[index]={ + ** id$ : ui.(options), + ** } + ** ui.pages[index]["$id"].on(event,handler) + ** + ** ui.start(main?) + ** + ** Supported Widget Constructors + ** ============================= + ** + ** Button: button({left,right,top,width,height:number,content:string, action:function|navigation string, styles,....}) -> button + ** Chat Dialog: chat({left,right,top,width,height:number,prompt:string,label:string,bot:style,user:style, styles,.. }} + ** Checkbox: checkbox(left,right,top,width,height:number,content:string, value:boolean, action:function, styles,....}) + ** Info: info({left,right,top,bottom,width,height:number,multiline:boolean,wrap:boolean,label:string, styles,.. }} + ** Input: input({left,right,top,width,height:number,multiline:boolean,wrap:boolean,label:string, styles,..}} + ** Label: label({left,right,top,center:boolean, width,height:number,content:string, styles,..}} + ** Radiobut: radiobutton(left,right,top,width,height:number,content:string, group:number|string, value:boolean, action:function, styles,....}) + ** Status: status({left,right,top,bottom,width,height:number,multiline:boolean,wrap:boolean,label:string, styles,..}} + ** Text: text({left,right,top,width,height:number,multiline:boolean,wrap:boolean,scrollable:boolean,label:string, styles,..}} + ** Terminal: terminal({left,right,top,width,height:number,prompt:string,label:string, styles,..}} + ** + ** type styles = {fg:string,bg:string, bold:boolean, ..} + ** type navigation string = 'prev'|'next'|pageid + ** + ** type checkbox'event = select:boolean + ** type radiobutton'event = select:boolean , check:object + ** type chat'event = eval:string + ** + ** methods chat = { message:function (string), ask:function(string,options,callback), } + ** methods input = { setValue:function (string), getValue:function(), } + ** + ** + ** --- + ** + ** Examples (with template builder, see also ui/app/demo2.js) + ** + ** AppApp = Require('ui/app/app'); + ** App = AppApp.App({}); + ** App.init(); + ** styles = {} + ** content = { pages : { + ** main: { + ** lab1: {type:'label', center:true, top:1, content:'Menu'}, + ** but1: {type:'button', left:1, content:'QAppT', bg:'red', onclick: process.exit }, + ** but2: {type:'button', right:1, content:'SETUP', onclick:'setup' }, + ** }, + ** setup: { + ** lab1: {type:'label', center:true, top:1, content:'Menu'}, + ** but1: {type:'button', left:1, content:'<< MENU', onclick:'main' }, + ** } + ** }} + ** App.builder(styles,content) + ** App.start() + ** + ** ---------------------- + ** + ** Examples (without builder, programmatically): + ** + ** ui=AppApp.App({ + ** pages:7, + ** terminal:this.options.terminal||'xterm-color', + ** title:'JAMAPP (C) Stefan Bosse' + ** }); + ** ui.init(); + ** page=ui.pages[1]; + ** page.b1= ui.button({left:1,content:'QAppT'}); + ** page.b1.on('press', function(data) { + ** return process.exit(0); + ** }); + ** page.l1 = ui.label({center:true,top:1,content:'Menu'}); + ** page.b2 = ui.button({right:1,content:'SETUP'}); + ** page.b2.on('press', function(data) { + ** ui.pages.hide(1); + ** ui.pages.show(2); + ** }); + ** // or + ** page.b2.on('press', function(data) { + ** ui.pages.hide(); + ** ui.pages.show('next'); + ** }); + ** + ** page.l2 = ui.label({center:true,top:1,content:'Setup'}); + ** page.i1 = ui.input({top:4,left:4,label:'Broker IP Address',value:'localhost'}); + ** page.i1.setValue('127.0.0.1'); + ** url=page.i1.getValue(); + ** + ** page.i4 = ui.info({top:16,left:4,width:ui.screen.width-8,label:'JAM Status'}); + ** + ** page.l2 = ui.label({left:4,top:16,content:'Protocol'}); + ** page.ch21 = ui.radiobutton({left:4,top:18,text:'HTTP',value:false,group:2}); + ** page.ch22 = ui.radiobutton({left:4,top:20,text:'TCPIP',value:true,group:2}); + ** + ** page.l3 = ui.label({left:4,top:22,content:'Messages'}); + ** page.ch31 = ui.checkbox({left:4,top:24,text:'Agent ID',value:false}); + ** page.ch32 = ui.checkbox({left:4,top:26,text:'Parent ID',value:false}); + ** + ** $ENDOFINFO + */ +/** Main User Interface providing a multiple page view + * Events: pages -> 'load' + * typeof options = {pages,terminal,forceCursor,styles} +*/ + +/* GUI => ui.js => blessed emulation */ + +var options = { + version: '1.8.2' +} + +/** pre-defined layouts based on screen rows and columns + * + */ +var LAYOUT = { + SMALL:'small', + NORMAL:'normal', + LARGE:'large', + XLARGE:'xlarge', + PORTRAIT:'portrait', + LANDSCAPE:'landscape', + from: function (screen) { + if (screen.width>screen.height) { + if(screen.width<70) return {small:true,landscape:true}; + else return {normal:true,landscape:true}; + } else { + if(screen.height<70) return {small:true,portrait:true}; + else return {normal:true,portrait:true}; + } + } +} + +function App(options) { + var self=this,i; + if (!(this instanceof App)) { + return new App(options); + } + this.options=options||{}; + if (!this.options.pages) this.options.pages=1; + if (!this.options.terminal) { + this.options.terminal= + (process.platform === 'win32' ? 'windows-ansi' : 'xterm-color'); + } + if (this.options.forceCursor==undefined) this.options.forceCursor=true; + + this.page = 1; + this.pages = []; // Pages + this.static = {}; // Top-level widegts visible on all pages + this.styles=options.styles||{}; + + for(i=0;i<=this.options.pages;i++) { + this.pages.push({}); + } + this.pages.show = function (page) { + var thepage,p,current=self.page; + switch (page) { + case 'prev': + if (self.pages[self.page] && self.pages[self.page].prev) page=self.pages[self.page].prev; + else if (self.pages[self.page-1]) page=self.page-1; + break; + case 'next': + if (self.pages[self.page] && self.pages[self.page].next) page=self.pages[self.page].next; + else if (self.pages[self.page+1]) page=self.page+1; + break; + case 'this': + case undefined: + page=self.page; break; + } + thepage=self.pages[page]; + if (self.events[page] && self.events[page]['load']) + self.events[page]['load'](); + for (p in thepage) { + if (thepage[p] && thepage[p].show && !thepage[p].noshow) thepage[p].show(); + } + self.screen.render(); + self.page=page; + }; + this.pages.hide = function (page) { + var thepage,p; + if (page=='this' || page==undefined) page=self.page; + thepage=self.pages[page]; + for (p in thepage) + if (thepage[p] && thepage[p].hide) thepage[p].hide(); + // hide cursor + if (self.options.terminal.indexOf('xterm') != -1 && self.options.forceCursor) + self.screen.program.hideCursor(true); + }; + this.events=[]; + this.pages.on = function(page,event,callback) { + if (!self.events[page]) self.events[page]=[]; + self.events[page][event]=callback; + } +} + +/** Button widget + * Methods: on + * Events: 'press' + * typeof @options ={ + * width,content,center,left,right,top, + * fg is textcolor,bg is button color,border, + * click:function, + * action:function|string + * } + */ +App.prototype.button = function(options) { + var self=this,width=options.width; + if (Comp.obj.isString(options.width)) { + // relative value in %! + width=Comp.pervasives.int_of_string(options.width); + width=this.screen.width*width/100; + } + var obj = GUI.button({ + width: options.width||(options.content.length+4), + left: (options.center?int(this.screen.width/2-width/2):options.left), + right : options.right, + top: options.top||0, + height: 3, + align: 'center', + content: options.content||'?', + mouse:true, + focus:false, + border: options.border||this.getStyle('button.border',{ + type: 'line' + }), + style: { + fg: options.fg||this.getStyle('button.fg','white'), + bg: options.bg||this.getStyle('button.bg','blue'), + bold:true, + border: { + fg: this.getStyle('button.border.fg','black'), + bg: this.getStyle('button.border.bg',undefined), + }, + hover: { + border: { + fg: 'red' + } + } + } + }); + obj.noshow=options.hidden; + this.screen.append(obj); + // generic handler + if (options.click) obj.on('press',options.click); + // dedicated action handlers with page navigation support + if (options.action) { + switch (typeof options.action) { + case 'function': obj.on('press',options.action); break; + case 'string': + if (options.action=='next' || options.action=='prev') { + obj.on('press',function () { + if (self.pages[self.page][options.action]) { + self.pages.hide('this'); + self.pages.show(self.pages[self.page][options.action]); + } + }); + } else { + obj.on('press',function () { + if (self.pages[options.action]) { + self.pages.hide('this'); + self.pages.show(options.action); + } + }); + + } + break; + } + } + return obj; +} + +/** Chat terminal shell widget + * typeof options = {top,left,right,width,height,label:string,bot:style,user:style,fg:style,bg:style} + */ +App.prototype.chat = function(options) { + var self=this, width=options.width||(this.screen.width-(options.left*2||2)); + options.scrollable=true; + options.scrollbar = { + ch: ' ', + track: { + bg: 'yellow' + }, + style: { + fg: 'cyan', + inverse: true + } + }; + options.mouse=true; + var obj = GUI.chat({ + label: options.label||'My Text', + value: options.value||'', + //fg: 'blue', + bg: 'default', + barBg: 'default', + barFg: 'blue', + width: width, + height: options.height||8, + left: (options.center?int(this.screen.width/2-width/2):options.left), + right : options.right, + top: options.top||0, + keys: true, + vi: false, + mouse: true, + inputOnFocus: true, + tags:true, + focus:true, + wrap : options.wrap, + multiline : true, + scrollbar : options.scrollbar, + scrollable : options.scrollable, + //draggable:true, + prompt:options.prompt, + border: this.getStyle('input.border',{ + type: 'line' + }), + style: { + fg: options.fg||this.getStyle('input.fg','blue'), + bg: options.bg||this.getStyle('input.bg',undefined), + user : options.user, + bot : options.bot, + border: { + fg: this.getStyle('input.border.fg','black'), + bg: this.getStyle('input.border.bg',undefined), + }, + label:this.getStyle('input.label',undefined), + focus : { + border: { + fg: 'red' + } + } + } + }); + obj.noshow=options.hidden; + this.screen.append(obj); + obj.on('focus',function () { + if (!self.options.keyboard) // show cursor + return self.options.terminal.indexOf('xterm')!=-1 && self.options.forceCursor? + self.screen.program._write('\x1b[?12;25h'):0; + if (!self._keyboard) + self._keyboard=self.keyboard({ + width:self.screen.width<60?'100%':'80%', + height:self.layout.small?'100%':'90%', + compact:self.layout.small + }); + self._keyboard.setCallback(function (v) {if (v) obj.setValue(v),obj.update();}); + self._keyboard.setValue(obj.getValue()); + self._keyboard.setLabel(obj.getLabel()); + self._keyboard.show(); + }); + return obj; +} + +/** Checkbox widget + * Methods: on + * Events: 'check','uncheck' + * typeof @options ={value?,left?,right?,top?,text,hidden?} + */ +App.prototype.checkbox = function(options) { + var obj = GUI.checkbox({ + checked: options.value||false, + left: options.left, + right : options.right, + top: options.top||0, + mouse: true, + inputOnFocus: true, + height: 1, + text:options.text||'empty' + }); + obj.noshow=options.hidden; + this.screen.append(obj); + return obj; +} + +/** Dialog pop-up window widget + * + * typeof @options = {width,height,center?,left?,right?,top?,okButton?,cancelButton} + */ +App.prototype.dialog = function(options) { + var width=options.width,height=options.height; + if (Comp.obj.isString(options.width)) { + // relative value in %! + width=Comp.pervasives.int_of_string(options.width); + width=int(this.screen.width*width/100); + } + if (Comp.obj.isString(options.height)) { + // relative value in %! + height=Comp.pervasives.int_of_string(options.height); + height=int(this.screen.height*height/100); + } + var obj = GUI.Question({ + width: width, + left: (options.center?int(this.screen.width/2-width/2):options.left), + right : options.right, + top: options.top||(options.center?int(this.screen.height/2-height/2):0), + height: height, + noshow:true, + okButton : options.okButton||'Okay', + cancelButton : options.cancelButton, + style: { + bg:this.getStyle('dialog.bg','red'), + fg:this.getStyle('dialog.fg','white'), + bold:true + } + }); + this.screen.append(obj); + return obj; +} + +/** File manager widget with buttons + * + * typeof @options={fg?,bg?,parent?,border?,label?,height?,width?,top?,left?,autohide?, + * okayBotton?,cancelButton?,input?,box?,arrows?} + */ +App.prototype.fileManager = function(options) { + if (options.box) { + options.box.border=this.getStyle('filemanager.box.border',options.box.border); + options.box.bg=this.getStyle('filemanager.box.bg',options.box.bg); + options.input=this.getStyle('filemanager.input',options.input); + } + if (options.arrows) { + options.arrows.fg=this.getStyle('filemanager.arrows.fg',options.arrows.fg); + options.arrows.bg=this.getStyle('filemanager.arrows.bg',options.arrows.bg); + } + var obj = GUI.FileManager({ + parent:options.parent, + border:options.border||this.getStyle('filemanager.border',{}), + style: { + fg: options.fg||this.getStyle('filemanager.fg',undefined), + bg: options.bg||this.getStyle('filemanager.bg',undefined), + label:options.label||this.getStyle('filemanager.label',undefined), + selected: { + bg: 'blue', + fg:'white' + }, + focus: { + border: { + fg: 'red' + } + } + }, + height: options.height||'half', + width: options.width||'half', + top: options.top||'center', + left: options.left||'center', + label: '%path', + cwd: options.cwd||process.env.PWD||process.env.CWD||process.env.HOME, + autohide:options.autohide, + hidden:options.hidden, + noshow:options.hidden, // no show on page load + keys: true, + vi: true, + scrollbar: { + bg: 'white', + ch: ' ' + }, + okayButton:options.okayButton||'OK', + cancelButton:options.cancelButton||'Cancel', + input:options.input, + arrows:options.arrows, + box:options.box, + border:this.getStyle('filemanager.border',undefined) + }); + + this.screen.append(obj); + return obj; +} + +/** Filter supported widget options, transform special options (click,..). Used by page builder. + * + */ +App.prototype.filterOptions = function(options,wname) { + var self=this,attr,wopts = {}; + for (attr in options) { + switch (attr) { + case 'type': + case 'index': + break; + case 'on': + switch (options.type) { + case 'button' : + if (options.on.click && typeof options.on.click == 'function') + wopts.click=function () { options[attr](wname) }; + if (options.on.onclick && typeof options.on.onclick == 'function') + wopts.click=function () { options[attr](wname) }; + break; + } + break; + case 'click': + case 'onclick': + switch (options.type) { + case 'button' : + if (typeof options[attr] == 'string') // Its a page destination; show new page + wopts.click=function () { + if (!self.pages[options[attr]]) return; + self.pages.hide('this'); + self.pages.show(options[attr]); + }; + else + wopts.click=options[attr]; + break; + } + break; + default: + wopts[attr]=options[attr]; + } + } + return wopts; +} + +/** getStyle + * + */ +App.prototype.getStyle = function(attr,def) { + var path=attr.split('.'),elem,style=this.styles; + while(path.length && style) { + elem=path.shift(); + style=style[elem]; + } + return style!=undefined?style:def; +} + +/** Information message widget + * Methods: setValue + * typeof options = {width,top,left,right, height,label,wrap,multiline,scrollable,color} + */ +App.prototype.info = function(options) { + var width=options.width; + if (Comp.obj.isString(options.width)) { + // relative value in %! + width=Comp.pervasives.int_of_string(options.width); + width=this.screen.width*width/100; + } + if (options.scrollable && !options.scrollbar) { + options.scrollbar = { + ch: ' ', + track: { + bg: 'yellow' + }, + style: { + fg: 'cyan', + inverse: true + } + }; + options.mouse=true; + } + var obj = GUI.textbox({ + top : options.top||1, + left : (options.center?int(this.screen.width/2-width/2):options.left||(options.right?undefined:1)), + right : options.right, + width : options.width||(this.screen.width-(options.left*2||2)), + height: options.height||3, + label : options.label, + value : options.value||'', + focus : true, + wrap : options.wrap, + multiline : options.multiline, + scrollbar : options.scrollbar, + scrollable : options.scrollable, + mouse : options.mouse, + //draggable:true, + border: this.getStyle('info.border',{ + type: 'line' + }), + style: { + fg:options.fg||this.getStyle('info.fg','blue'), + bg: options.bg||this.getStyle('info.bg',undefined), + label:this.getStyle('info.label',undefined), + border: { + fg: this.getStyle('info.border.fg','black'), + bg: this.getStyle('info.border.bg',undefined), + }, + } + }); + + obj.noshow=options.hidden; + this.screen.append(obj); + return obj; +} + +/** Initialite APP and create screen + * + */ +App.prototype.init = function () { + var self=this; + // Information bar visible on all pages + this.screen = GUI.screen({ + smartCSR: false, + terminal: this.options.terminal, + forceCursor:this.options.forceCursor, + }); + this.screen.title = this.options.title||'APP (C) Stefan Bosse'; + this.screen.cursor.color='red'; + this.layout=LAYOUT.from(this.screen); +} + +/** Input field widget + * typeof options = {top,left,right,width,height,label,value} + * method getValue, setValue + * events: {'set content'} + */ +App.prototype.input = function(options) { + var self=this, width=options.width||(this.screen.width-(options.left*2||2)); + var obj = GUI.textbox({ + label: options.label||'My Input', + value: options.value||'', + //fg: 'blue', + bg: 'default', + barBg: 'default', + barFg: 'blue', + width: width, + height: options.height||3, + left: (options.center?int(this.screen.width/2-width/2):options.left), + right : options.right, + top: options.top, + bottom: options.bottom, + keys: true, + mouse: true, + inputOnFocus: true, + focus:true, + wrap:options.wrap, + multiline:options.multiline, + //draggable:true, + border: this.getStyle('input.border',{ + type: 'line' + }), + style: { + fg: options.fg||this.getStyle('input.fg','blue'), + bg: options.bg||this.getStyle('input.bg',undefined), + border: { + fg: this.getStyle('input.border.fg','black'), + bg: this.getStyle('input.border.bg',undefined), + }, + label:this.getStyle('input.label',undefined), + focus : { + border: { + fg: 'red' + } + } + } + }); + obj.noshow=options.hidden; + this.screen.append(obj); + obj.on('focus',function () { + if (!self.options.keyboard) // show cursor + return self.options.terminal.indexOf('xterm')!=-1 && self.options.forceCursor? + self.screen.program._write('\x1b[?12;25h'):0; + if (!self._keyboard) + self._keyboard=self.keyboard({ + width:self.screen.width<60?'100%':'80%', + height:self.layout.small?'100%':'90%', + compact:self.layout.small + }); + self._keyboard.setCallback(function (v) {if (v) obj.setValue(v),obj.update();}); + self._keyboard.setValue(obj.getValue()); + self._keyboard.setLabel(obj.getLabel()); + self._keyboard.show(); + }); + return obj; +} + +/** Software keyboard widget + * + * typeof options = { left,top, width, height, compact, okayButton, cancelButton, border, } + */ +App.prototype.keyboard = function(options) { + var obj = GUI.keyboard({ + parent:options.parent||this.screen, + border: 'line', + height: options.height||'half', + width: options.width||'half', + top: options.top||'center', + left: options.left||'center', + label: 'Keyboard', + hidden:options.hidden, + compact:options.compact, + okayButton:options.okayButton||'OK', + cancelButton:options.cancelButton||(this.layout.small?'CAN':'Cancel'), + delButton:'DEL', + shiftButton:'>>', + border:options.border||this.getStyle('keyboard.border',{}), + style:{ + bg: options.bg||this.getStyle('keyboard.bg',undefined), + label:options.label||this.getStyle('keyboard.label',undefined), + } + }); + this.screen.append(obj); + return obj; +} + +/** Generic label widget + * method setValue(string)|mutable=true + * typeof options = {width?,left?,right?,top?,center?,mutable?,content} + */ +App.prototype.label = function(options) { + var obj = GUI.text({ + width: options.width||(options.content.length), + left: (options.center?int(this.screen.width/2-options.content.length/2):options.left), + right : options.right, + top: options.top||0, + height: options.height||1, + focus:false, + align: 'center', + content: options.content||'?', + style: { + bg:options.style?options.style.bg:this.getStyle('label.bg',undefined), + fg:options.style?options.style.fg:this.getStyle('label.fg',undefined), + bold:this.getStyle('label.bold',false) + } + }); + if (options.mutable) + obj.setValue = function (content) { + obj.setContent(''); + obj.position.left=(options.center?int(this.screen.width/2-content.length/2):options.left); + obj.setContent(content); + }; + + obj.noshow=options.hidden; + this.screen.append(obj); + return obj; +} + +/** Generic list navigator widget with scrollbar + * typeof options = {top,left,width,height,label} + * method set(data:object|array) + */ +App.prototype.list = function(options) { + var obj = GUI.list({ + top: options.top, + left: options.left, + width: options.width||(this.screen.width-options.left*2), + height: options.height||(this.screen.height-options.top-4), + label: options.label||'Log', + focus:true, + mouse:true, + keys:true, + arrows:options.arrows, + border: this.getStyle('list.border',{ + type: 'line' + }), + style: { + bg: options.bg||this.getStyle('list.bg',undefined), + selected:options.selected||{fg:'white',bg:'red',bold:true}, + item:options.item||{bold:true}, + border: { + fg: this.getStyle('list.border.fg','black') + }, + label:this.getStyle('list.label',undefined), + hover: { + border: { + fg: 'red' + } + }, + focus : { + border: { + fg: 'red' + } + } + } + }); + obj.noshow=options.hidden; + obj.set = obj.update = function (data) { + var p,items=[]; + obj.clearItems(); + if (Comp.obj.isArray(data)) items=data; + else for (p in data) { + items.push(p); + } + obj.setItems(items); + obj.screen.render(); + } + this.screen.append(obj); + return obj; +} + +/** Log message widget with scrollbar + * typeof options = {left,top,width,height,label,scrollback,..} + */ +App.prototype.log = function(options) { + if (options.top == undefined) options.top=2; + if (options.left == undefined && options.right==undefined) options.left=1; + var obj = GUI.log({ + top: options.top, + left: options.left, + right: options.right, + width: options.width||(this.screen.width-options.left*2), + height: options.height||(this.screen.height-options.top-4), + label: options.label||'Log', + mouse:true, + keys:true, + scrollback:options.scrollback||100, + border: this.getStyle('log.border',{ + type: 'line' + }), + scrollbar: { + ch: ' ', + track: { + bg: 'yellow' + }, + style: { + fg: 'cyan', + inverse: true + } + }, + alwaysScroll:true, + scrollOnInput:true, + style: { + fg: options.fg||this.getStyle('log.fg','white'), + bg: options.bg||this.getStyle('log.bg','black'), + label:this.getStyle('log.label',undefined), + border: { + fg: this.getStyle('log.border.fg','green'), + bg: this.getStyle('log.border.bg',undefined), + }, + focus: { + border: { + fg: 'red' + } + } + }, + arrows:options.arrows, + }); + obj.noshow=options.hidden; + this.screen.append(obj); + return obj; +} + +/** Apply post option actions (event handling) + * + */ +App.prototype.postOptions = function(options,widget,wname) { + var self=this,attr; + for (attr in options) { + switch (attr) { + case 'on': + switch (options.type) { + case 'checkbox' : + if (options.on.click && typeof options.on.click == 'function') + widget.on('check',function () { options.on.click(wname,true) }), + widget.on('uncheck',function () { options.on.click(wname,false) }); + if (options.on.onclick && typeof options.on.onclick == 'function') + widget.on('check',function () { options.on.onclick(wname,true) }), + widget.on('uncheck',function () { options.on.onclick(wname,false) }); + if (options.on.check && typeof options.on.check == 'function') + widget.on('check',function () { options.on.check(wname,true) }); + if (options.on.uncheck && typeof options.on.uncheck == 'function') + widget.on('uncheck',function () { options.on.uncheck(wname,false) }); + if (options.on.selected && typeof options.on.selected == 'function') + widget.on('selected',function (data) { options.on.uncheck(wname,data) }); + if (options.on.change && typeof options.on.change == 'function') + widget.on('change',function (data) { options.on.change(wname,data) }); + break; + } + break; + case 'click': + case 'onclick': + switch (options.type) { + case 'checkbox' : + if (typeof options[attr] == 'function') + widget.on('check',function () { options[attr](wname,true) }), + widget.on('uncheck',function () { options[attr](wname,false) }) + break; + case 'list': + if (typeof options[attr] == 'function') + widget.on('selected',function (data) { options[attr](wname,data.content,data) }) + break; + case 'tree': + if (typeof options[attr] == 'function') + widget.on('selected',function (data) { + var _data=data,path=data.name; + data=data.parent; + while(data) + path=data.name+(data.name!='/'?'/':'')+path, + data=data.parent; + options[attr](wname,_data.name,path,_data) + }) + break; + } + break; + case 'onchange': + switch (options.type) { + case 'input' : + if (typeof options[attr] == 'function') + widget.on('change',function (data) { + var content = widget.getContent(); + options[attr](wname,content) + }) + break; + } + break; + } + } +} + +/** Radio button widget; can be grouped + * + */ +App.prototype.radiobutton = function(options) { + var obj = GUI.radiobutton({ + checked: options.value||false, + left: options.left, + right : options.right, + top: options.top||0, + group:options.group, + mouse: true, + inputOnFocus: true, + height: 1, + text:options.text||'empty' + }); + obj.noshow=options.hidden; + this.screen.append(obj); + return obj; +} + + +App.prototype.start = function (main) { + var self=this; + if (main==undefined) main=this.pages.main?'main':null; + Object.keys(this.pages).forEach(function (page) { + if (typeof self.pages[page] != 'object') return; + if (Object.keys(self.pages[page]).length==0) return; + self.pages.hide(page); + if (!main) main=page; + }); + if (main) this.pages.show(main); + this.screen.render(); + this.screen.program.hideCursor(this.options.terminal.indexOf('xterm') != -1 && this.options.forceCursor); +} + +/** Status field widget + * typeof options = {top,bottom,left,right,width,height,label,value} + * method getvalue, setValue + * events: {'set content'} + */ +App.prototype.status = function(options) { + var self=this, width=options.width||(this.screen.width-(options.left*2||2)); + var obj = GUI.textbox({ + label: options.label||'My Input', + value: options.value||'', + //fg: 'blue', + bg: 'default', + barBg: 'default', + barFg: 'blue', + width: width, + height: options.height||3, + left: (options.center?int(this.screen.width/2-width/2):options.left), + right : options.right, + top: options.top, + bottom: options.bottom, + wrap:options.wrap, + multiline:options.multiline, + //draggable:true, + border: this.getStyle('input.border',{ + type: 'line' + }), + style: { + fg: options.fg||this.getStyle('input.fg','blue'), + bg: options.bg||this.getStyle('input.bg',undefined), + border: { + fg: this.getStyle('input.border.fg','black'), + bg: this.getStyle('input.border.bg',undefined), + }, + label:this.getStyle('input.label',undefined), + focus : { + border: { + fg: 'red' + } + } + } + }); + obj.noshow=options.hidden; + this.screen.append(obj); + return obj; +} + +/** Table widget + * typeof options = {left,right,top,width,height,label,header,cell,data,..} + */ +App.prototype.table = function(options) { + if (options.top == undefined) options.top=2; + if (options.left == undefined && options.right==undefined) options.left=1; + var obj = GUI.table({ + top: options.top, + left: options.left, + right: options.right, + width: options.width||(this.screen.width-options.left*2), + height: options.height||(this.screen.height-options.top-4), + label: options.label||'Table', + data: options.data, + border: this.getStyle('table.border',{ + type: 'line' + }), + style: { + fg: options.fg||this.getStyle('log.fg','white'), + bg: options.bg||this.getStyle('log.bg','black'), + label:this.getStyle('log.label',undefined), + border: { + fg: this.getStyle('log.border.fg','green'), + bg: this.getStyle('log.border.bg',undefined), + }, + header : options.header, + cell : options.cell, + focus: { + border: { + fg: 'red' + } + } + }, + }); + obj.noshow=options.hidden; + this.screen.append(obj); + return obj; +} + +/** Terminal shell widget + * typeof options = {top,left,right,width,height,label,header:style,cell:style} + */ +App.prototype.terminal = function(options) { + var self=this, width=options.width||(this.screen.width-(options.left*2||2)); + options.scrollable=true; + if (options.scrollable && !options.scrollbar) { + options.scrollbar = { + ch: ' ', + track: { + bg: 'yellow' + }, + style: { + fg: 'cyan', + inverse: true + } + }; + options.mouse=true; + } + var obj = GUI.terminal({ + label: options.label||'My Text', + value: options.value||'', + //fg: 'blue', + bg: 'default', + barBg: 'default', + barFg: 'blue', + width: width, + height: options.height||8, + left: (options.center?int(this.screen.width/2-width/2):options.left), + right : options.right, + top: options.top||0, + keys: true, + vi: false, + mouse: true, + inputOnFocus: true, + focus:true, + wrap : options.wrap, + multiline : true, + scrollbar : options.scrollbar, + scrollable : options.scrollable, + //draggable:true, + prompt:options.prompt, + border: this.getStyle('input.border',{ + type: 'line' + }), + style: { + fg: options.fg||this.getStyle('input.fg','blue'), + bg: options.bg||this.getStyle('input.bg',undefined), + border: { + fg: this.getStyle('input.border.fg','black'), + bg: this.getStyle('input.border.bg',undefined), + }, + label:this.getStyle('input.label',undefined), + focus : { + border: { + fg: 'red' + } + } + } + }); + obj.noshow=options.hidden; + this.screen.append(obj); + obj.on('focus',function () { + if (!self.options.keyboard) // show cursor + return self.options.terminal.indexOf('xterm')!=-1 && self.options.forceCursor? + self.screen.program._write('\x1b[?12;25h'):0; + if (!self._keyboard) + self._keyboard=self.keyboard({ + width:self.screen.width<60?'100%':'80%', + height:self.layout.small?'100%':'90%', + compact:self.layout.small + }); + self._keyboard.setCallback(function (v) {if (v) obj.setValue(v),obj.update();}); + self._keyboard.setValue(obj.getValue()); + self._keyboard.setLabel(obj.getLabel()); + self._keyboard.show(); + }); + return obj; +} + +/** Text field widget + * typeof options = {top,left,right,width,height,label,value} + * method getValue, setValue + * events: {'set content'} + */ +App.prototype.text = function(options) { + var self=this, width=options.width||(this.screen.width-(options.left*2||2)); + if (options.scrollable && !options.scrollbar) { + options.scrollbar = { + ch: ' ', + track: { + bg: 'yellow' + }, + style: { + fg: 'cyan', + inverse: true + } + }; + options.mouse=true; + } + var obj = GUI.textarea({ + label: options.label||'My Text', + value: options.value||'', + //fg: 'blue', + bg: 'default', + barBg: 'default', + barFg: 'blue', + width: width, + height: options.height||8, + left: (options.center?int(this.screen.width/2-width/2):options.left), + right : options.right, + top: options.top||0, + keys: true, + vi: false, + mouse: true, + inputOnFocus: true, + focus:true, + wrap : options.wrap, + multiline : true, + scrollbar : options.scrollbar, + scrollable : options.scrollable, + //draggable:true, + border: this.getStyle('input.border',{ + type: 'line' + }), + style: { + fg: options.fg||this.getStyle('input.fg','blue'), + bg: options.bg||this.getStyle('input.bg',undefined), + border: { + fg: this.getStyle('input.border.fg','black'), + bg: this.getStyle('input.border.bg',undefined), + }, + label:this.getStyle('input.label',undefined), + focus : { + border: { + fg: 'red' + } + } + } + }); + obj.noshow=options.hidden; + this.screen.append(obj); + obj.on('focus',function () { + if (!self.options.keyboard) // show cursor + return self.options.terminal.indexOf('xterm')!=-1 && self.options.forceCursor? + self.screen.program._write('\x1b[?12;25h'):0; + if (!self._keyboard) + self._keyboard=self.keyboard({ + width:self.screen.width<60?'100%':'80%', + height:self.layout.small?'100%':'90%', + compact:self.layout.small + }); + self._keyboard.setCallback(function (v) {if (v) obj.setValue(v),obj.update();}); + self._keyboard.setValue(obj.getValue()); + self._keyboard.setLabel(obj.getLabel()); + self._keyboard.show(); + }); + return obj; +} + +/** Generic data object tree navigator widget with scrollbar + * typeof options = {top,left,width,height,label,depth} + * method set(dats)/update(data) + * + * Data object can contain _update attributes (function) modifying the data content of elements + * before opening a tree branch. Root data _update must call self.update(new data)! + * Deeper _update functions have only to modify the object data passed as an argument. + * Scalar tree leafes can be updated before opening branch by a virtual object: + * {_virtual:string|number|boolean,_update:function (data) {data._value=}} +*/ + +App.prototype.tree = function(options) { + var obj = GUI.tree({ + top: options.top, + left: options.left, + width: options.width||(this.screen.width-options.left*2), + height: options.height||(this.screen.height-options.top-4), + label: options.label||'Log', + focus:true, + arrows:options.arrows, + border: this.getStyle('tree.border',{ + type: 'line' + }), + style: { + bold:true, + border: { + fg: this.getStyle('tree.border.fg','black') + }, + label:this.getStyle('tree.label',undefined), + hover: { + border: { + fg: 'red' + } + }, + focus : { + border: { + fg: 'red' + } + } + } + }); + function makeleaf (element,reference,data) { + var content,children,name,funpat,isfun,p; + children={}; + name = element.toString(); + funpat = /function[\s0-9a-zA-Z_$]*\(/i; + isfun=Comp.obj.isFunction(element)||funpat.test(name); + if (isfun) { + element=Comp.string.sub(name,0,name.indexOf('{')); + } + if (!isfun || (isfun && options.showfun)) { + children[element]={}; + content={children : children,reference:reference,data:data}; + } + return content; + } + function maketree (element,reference) { + var content,children,p; + children={}; + if (element && (Comp.obj.isObject(element) || Comp.obj.isArray(element))) { + // console.log(element) + if (element._update != undefined) element._update(element); + if (element._value != undefined) return makeleaf(element._value,_,element); + for (p in element) { + if (p != '_update') + children[p]={}; + } + content={ + children : children, + data : element + } + } else if (element != undefined) { + return makeleaf(element,reference); + } else { + children[element]={}; + content={children : children}; + } + return content; + }; + obj.noshow=options.hidden; + // Create sub-trees + obj.on('preselect',function(node){ + var content,children,element,data,name; + if (node.name != '/' && !node.extended) { + // Io.out(node.extended); + data = node.data; + if (data != none && (Comp.obj.isObject(data) || Comp.obj.isArray(data))) { + node.children = {}; + if (Comp.obj.isArray(data) && Comp.array.empty(data) && Comp.hashtbl.empty(data)) { + node.children={'[]' : {}}; + } else { + if (data._update != undefined) data._update(data); + if (data._value != undefined) return node.children=makeleaf(data._value,_,data).children; + for (var p in data) { + if (p != '_update') { + element = data[p]; + content=maketree(element,data); + if (content) node.children[p]=content; + } + } + } + } else if (data == none && node.reference) { + node.children = {}; + element=node.reference[node.name]; + name=element.toString(); + var funpat = /function[\s0-9a-zA-Z_$]*\(/i; + var isfun=Comp.obj.isFunction(element)||funpat.test(name); + if (isfun) { + element=Comp.string.sub(name,0,name.indexOf('{')); + } + node.children[element]={}; + } + } else if (node.name == '/' && node.extended) { + if (node.data && node.data._update) { + node.data._update() + } + } + }); + obj.set = obj.update = function (data) { + obj.DATA = { + name:'/', + extended:true, + children: {}, + data:data, + }; + for (var p in data) { + var element=data[p]; + var content=options.depth && options.depth==1?{}:maketree(element,data); + if (content) obj.DATA.children[p]=content; + } + obj.setData(obj.DATA); + }; + obj.DATA = { + name:'/', + extended:true, + children: {}, + }; + obj.setData(obj.DATA); + this.screen.append(obj); + return obj; +} + + +// App App using GUI (blessed emu) + +UI = { + options:options, + LAYOUT:LAYOUT, + UI:App +}