From 97aaff0a52cce00a03cb6f9e804f8be1b9c777e9 Mon Sep 17 00:00:00 2001 From: sbosse Date: Mon, 21 Jul 2025 23:11:35 +0200 Subject: [PATCH] Mon 21 Jul 22:43:21 CEST 2025 --- js/term/widgets/chat.js | 1023 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 1023 insertions(+) create mode 100644 js/term/widgets/chat.js diff --git a/js/term/widgets/chat.js b/js/term/widgets/chat.js new file mode 100644 index 0000000..a636798 --- /dev/null +++ b/js/term/widgets/chat.js @@ -0,0 +1,1023 @@ +/** + ** ============================== + ** 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: Christopher Jeffrey, Stefan Bosse + ** $INITIAL: (C) 2013-2018, Christopher Jeffrey and contributors + ** $MODIFIED: by sbosse (2017-2021) + ** $REVESIO: 1.5.7 + ** + ** $INFO: + ** + ** chat.js - interactive chat terminal shell based on textarea element for blessed + ** + ** events: 'eval' (high level passing command line after enter key was hit) + ** + ** public methods: + ** + ** print(string) + ** ask(ask,options,callback) + ** + ** typeof options = { choices? : string [], mutual?: boolean, + ** range? : [number,number], + ** $ENDOFINFO + */ + +/** + * Modules + */ +var Comp = Require('com/compat'); + +var unicode = Require('term/unicode'); + +var nextTick = global.setImmediate || process.nextTick.bind(process); + +var Node = Require('term/widgets/node'); +var Input = Require('term/widgets/input'); + +function setCharAt(str,index,chr) { + if(index > str.length-1) return str; + return str.substring(0,index) + chr + str.substring(index+1); +} +/** + * Chat + */ + +function Chat(options) { + var self = this; + + if (!instanceOf(this,Node)) { + return new Chat(options); + } + + options = options || {}; + + options.scrollable = options.scrollable !== false; + + Input.call(this, options); + + this.screen._listenKeys(this); + + this.value = options.value || ''; + // cursor position + this.cpos = {x:-1,y:-1}; + this.cursorControl=true; + this.multiline=options.multiline; + this.tags=true; // we need formatting tag support + this.input='text'; + + this.history=[]; + this.historyTop=0; + + this.__updateCursor = this._updateCursor.bind(this); + this.on('resize', this.__updateCursor); + this.on('move', this.__updateCursor); + + if (options.inputOnFocus) { + this.on('focus', this.readInput.bind(this, null)); + } + + if (!options.inputOnFocus && options.keys) { + this.on('keypress', function(ch, key) { + if (self._reading) return; + if (key.name === 'enter' || (options.vi && key.name === 'i')) { + return self.readInput(); + } + if (key.name === 'e') { + return self.readEditor(); + } + }); + } + + if (options.mouse) { + this.on('click', function(data) { + if (self._reading) return; + if (data.button !== 'right') return; + self.readEditor(); + }); + } + + var offsetY = 0; + if (this._clines) offsetY=this._clines.length-(this.childBase||0); + if (options.prompt) { + this.value=options.prompt; + this.prompt=options.prompt; + this.inputRange={x0:options.prompt.length,y0:offsetY,y1:offsetY,last:0,line:0}; + } else { + this.inputRange={x0:0,y0:offsetY,y1:offsetY,last:0,line:0}; + this.prompt=''; + } +} + +//Chat.prototype.__proto__ = Input.prototype; +inheritPrototype(Chat,Input); + +Chat.prototype.type = 'terminal'; + + +Chat.prototype._updateCursor = function(get) { + var offsetY=this.childBase||0; + if (this.screen.focused !== this) { + // at least update cursor to its bound + this.cpos.y=Math.min(this._clines.length - 1 - offsetY,this.cpos.y); + return; + } + var lpos = get ? this.lpos : this._getCoords(); + if (!lpos) return; + + var last = this._clines[this._clines.length - 1] + , program = this.screen.program + , line + , offsetY = this.childBase||0 + , cx + , cy; + + // Stop a situation where the textarea begins scrolling + // and the last cline appears to always be empty from the + // _typeScroll `+ '\n'` thing. + // Maybe not necessary anymore? + if (last === '' && this.value[this.value.length - 1] !== '\n') { + last = this._clines[this._clines.length - 2] || ''; + } + + line = Math.min( + this._clines.length - 1 - (this.childBase || 0), + (lpos.yl - lpos.yi) - this.iheight - 1); + + // When calling clearValue() on a full textarea with a border, the first + // argument in the above Math.min call ends up being -2. Make sure we stay + // positive. + line = Math.max(0, line); + + if (this.cpos.x==-1 || !this.cursorControl) this.cpos.x = this.strWidth(last); + if (this.cpos.y==-1 || !this.cursorControl) this.cpos.y = line; + this.cpos.y = Math.min(this.cpos.y,line); + this.cpos.x = Math.min(this.cpos.x,this.strWidth(this._clines[offsetY+this.cpos.y])); + + cx = lpos.xi + this.ileft + this.cpos.x; + cy = lpos.yi + this.itop + this.cpos.y; + + // XXX Not sure, but this may still sometimes + // cause problems when leaving editor. + if (cy === program.y && cx === program.x) { + return; + } + + if (cy === program.y) { + if (cx > program.x) { + program.cuf(cx - program.x); + } else if (cx < program.x) { + program.cub(program.x - cx); + } + } else if (cx === program.x) { + if (cy > program.y) { + program.cud(cy - program.y); + } else if (cy < program.y) { + program.cuu(program.y - cy); + } + } else { + program.cup(cy, cx); + } +}; + +Chat.prototype.input = +Chat.prototype.setInput = +Chat.prototype.readInput = function(callback) { + var self = this + , focused = this.screen.focused === this; + + if (this._reading) return; + this._reading = true; + + this._callback = callback; + + if (!focused) { + this.screen.saveFocus(); + this.focus(); + } + + this.screen.grabKeys = true; + + this._updateCursor(); + this.screen.program.showCursor(); + //this.screen.program.sgr('normal'); + + this._done = function fn(err, value) { + if (!self._reading) return; + + if (fn.done) return; + fn.done = true; + + self._reading = false; + + delete self._callback; + delete self._done; + + self.removeListener('keypress', self.__listener); + delete self.__listener; + + self.removeListener('blur', self.__done); + delete self.__done; + + self.screen.program.hideCursor(); + self.screen.grabKeys = false; + + if (!focused) { + self.screen.restoreFocus(); + } + + if (self.options.inputOnFocus) { + self.screen.rewindFocus(); + } + + // Ugly + if (err === 'stop') return; + + 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); + }; + + // Put this in a nextTick so the current + // key event doesn't trigger any keys input. + nextTick(function() { + if (self.__listener) { + // double fired? + return; + } + self.__listener = self._listener.bind(self); + self.on('keypress', self.__listener); + }); + + this.__done = this._done.bind(this, null, null); + this.on('blur', this.__done); +}; + +// Ask request +Chat.prototype.ask = function (message,options,callback) { + var offsetY = this.childBase||0, + prompt = this.prompt, + self=this; + options=options||{}; + if (this.request) { + // pending request; queue new request + return; + } + // restore point + var restorePos = offsetY+this.cpos.y; + + function ask(command,restore) { + offsetY = self.childBase||0; + var start = self.getLinearPos(self.value,offsetY+self.inputRange.y0,0), + end = self.getLinearPos(self.value,offsetY+self.inputRange.y1, + self._clines[offsetY+self.inputRange.y1].length); + var line; + if (restore) start=self.getLinearPos(self.value,restorePos,0); + self.value=self.value.slice(0,start)+self.prompt+command+self.value.slice(end); + self.screen.render(); + self.scrollBottom(); + self.cpos.x=self._clines[self._clines.length-1].length; + self.cpos.y += 10; // bottom + self._updateCursor(true); + self.inputRange.y1=self.cpos.y; + offsetY = self.childBase||0; + // find start y + var y0=self.cpos.y; +// Log(self.cpos,offsetY,y0); + self.inputRange.x0=self.prompt.length; + self.inputRange.x1=null; + + while (self._clines[offsetY+y0].indexOf(self.prompt)!=0) y0--; + self.inputRange.y0=y0; + self.inputRange.last=self.inputRange.y1-self.inputRange.y0; + } + + this.print('{bold}'+message+'{/bold}'); + + // choices + if (options.choices) { + // form can be multiline + var choices = options.choices.slice(); + // Make buttons (with click handler) + this.prompt='? '; + var delim = options.layout=='vertical'?'\n':' ', + indent = options.layout=='vertical'?' ':''; + + if (!options.mutable) { + line=choices.map(function (s,i) { return (i>0?indent:'')+'[ ] '+s}).join(delim); + line += (delim+indent+'[Ok] [Cancel]'); + choices.push(restore1); choices.push(restore1); + } else { + line=choices.map(function (s,i) { return (i>0?indent:'')+'['+s+']'}).join(delim); + line += (delim+indent+'[Cancel]'); + choices.push(restore1); + } + + ask(line); + this.input='mouse'; + this.screen.program.hideCursor(true); + + // get selector positions, install clicked handler + var y=this.inputRange.y0,x=this.inputRange.x0; + + line=this._clines[offsetY+y]; + var actions = choices.map(function (choice,index) { + // find [ ]/[...] in _clines + while (line[x] && line[x] != '[') { + x++; + if (!line[x] && y=o[0].x&&pos.x<=o[1].x; + else if (pos.y>=o[0].y && pos.y<=o[1].y) // multi-line + return pos.x>=o[0].x&&pos.x<=o[1].x + } + } + actions.forEach(function (action) { + if (!action) return; + if (within(action.pos)) { + // fired + if (action.choice) { + if (self.selected.indexOf(action.choice)!=-1) { + // deselect + self.selected=self.selected.filter(function (choice) { return choice!=action.choice }); + self.value=setCharAt(self.value,action.fake,' '); + self.screen.render(); + } else { + // select + if (!options.mutable) { + self.value=setCharAt(self.value,action.fake,'X'); + self.screen.render(); + changed1(); + } + self.selected.push(action.choice); + if (options.mutable) restore1(); + } + } + if (action.action) { + action.action(); + } + } + }) + }; + function changed1() { + if (options.timeout) { + if (self.timer) clearTimeout(self.timer); + self.timer=setTimeout(restore1,options.timeout); + } + } + function restore1 (timeout) { + var result = callback?callback(self.selected):null; + if (self.selected != undefined && self.selected.length) + self.print(self.selected.join(', '),self.style.user); + if (result) self.print(result); + // restore text line + self.prompt=prompt; + ask('',!result && self.selected.length==0); + self.input='text'; + self.removeListener('clicked',clicked1); + if (!timeout && self.timer) clearTimeout(self.timer); + self.timer=null; + self.request=null; + } + this.on('clicked',clicked1); + this.selected=[]; + if (options.timeout) { + this.timer=setTimeout(restore1,options.timeout); + } + } + + // range // + if (options.range) { + // assumption: entire form fits in one ine + var sign=options.range[0]<0, + step=options.step||1, + digits = Math.floor(Math.log10(options.range[1]-options.range[0]+1))+1; + if (sign) digits++; + var value=options.default||options.value||options.range[0]; + line = '[-] '+Comp.printf.sprintf("%"+digits+"d",value)+' [+] [Ok] [Cancel]'; + this.prompt='? '; + ask(line); + var x0=this._clines[offsetY+this.cpos.y].indexOf('[-] ')+4; + var x1=this._clines[offsetY+this.cpos.y].indexOf(' [+]')-1; + this.cpos.x=x1; + this.inputRange.x0=x0; + this.inputRange.x1=x1; + this.inputRange.right=true; + this.inputRange.action=function () { + var _value = Number(self._clines[offsetY+self.cpos.y].slice(x0,x1+1)); + if (_value>= options.range[0] && _value<= options.range[1]) { + self.selected=value; + restore2(); + } else { + // invalid; try again + value=options.range[0]; + line = '[-] '+Comp.printf.sprintf("%"+digits+"d",value)+' [+] [Ok] [Cancel]'; + ask(line); + self.inputRange.x0=x0; + self.inputRange.x1=x1; + self.cpos.x=x1; + self._updateCursor(); + } + } + this._updateCursor(); + actions=[ + {pos:{x:this._clines[offsetY+this.cpos.y].indexOf('[-]')+1,y:this.cpos.y},action:function () { + // decrease value + value=Math.max(options.range[0],value-step); + line = '[-] '+Comp.printf.sprintf("%"+digits+"d",value)+' [+] [Ok] [Cancel]'; + ask(line); + self.inputRange.x0=x0; + self.inputRange.x1=x1; + self.cpos.x=x1; + self._updateCursor(); + }}, + {pos:{x:this._clines[offsetY+this.cpos.y].indexOf('[+]')+1,y:this.cpos.y},action:function () { + // increase value + value=Math.min(options.range[1],value+step); + line = '[-] '+Comp.printf.sprintf("%"+digits+"d",value)+' [+] [Ok] [Cancel]'; + ask(line); + self.inputRange.x0=x0; + self.inputRange.x1=x1; + self.cpos.x=x1; + self._updateCursor(); + }}, + {pos:[{x:this._clines[offsetY+this.cpos.y].indexOf('[Ok]')+1,y:this.cpos.y}, + {x:this._clines[offsetY+this.cpos.y].indexOf('[Ok]')+2,y:this.cpos.y}],action:function () { + // Ok + var _value = Number(self._clines[offsetY+self.cpos.y].slice(x0,x1+1)); + if (_value>= options.range[0] && _value<= options.range[1]) { + self.selected=value; + restore2(); + } else { + // invalid; try again + value=options.range[0]; + line = '[-] '+Comp.printf.sprintf("%"+digits+"d",value)+' [+] [Ok] [Cancel]'; + ask(line); + self.inputRange.x0=x0; + self.inputRange.x1=x1; + self.cpos.x=x1; + self._updateCursor(); + } + }}, + {pos:[{x:this._clines[offsetY+this.cpos.y].indexOf('[Cancel]')+1,y:this.cpos.y}, + {x:this._clines[offsetY+this.cpos.y].indexOf('[Cancel]')+6,y:this.cpos.y}],action:function () { + // Cancel + restore2() + }} + ] + function clicked2(pos) { + var offsetY=self.childBase||0; + function within (o) { + if (typeof o.x != 'undefined') { + return pos.x==o.x && pos.y==o.y + } else if (o.length==2) { + if (o[0].y==o[1].y && o[0].y==pos.y) + return pos.x>=o[0].x&&pos.x<=o[1].x; + else if (pos.y>=o[0].y && pos.y<=o[1].y) // multi-line + return pos.x>=o[0].x&&pos.x<=o[1].x + } + } + actions.forEach(function (action) { + if (!action) return; + if (within(action.pos)) action.action (); + }) + } + function changed2() { + var _value = Number(self._clines[offsetY+self.cpos.y].slice(x0,x1+1)); + if (isNaN(_value) || _value < options.range[0] || _value > options.range[1]) { + } else value=_value; + if (options.timeout) { + if (self.timer) clearTimeout(self.timer); + self.timer=setTimeout(restore2,options.timeout); + } + } + function restore2 (timeout) { + var result = callback?callback(self.selected):null; + if (self.selected != undefined) self.print(self.selected,self.style.user); + if (result) self.print(result); + // restore text line + self.prompt=prompt; + ask('',!result && self.selected == undefined); + self.removeListener('clicked',clicked2); + self.removeListener('modified',changed2); + if (!timeout && self.timer) clearTimeout(self.timer); + self.timer=null; + self.inputRange.action=null; + self.inputRange.right=null; + self.request=null; + } + // this.on('clicked',clicked); + this.selected=null; + this.on('clicked',clicked2); + this.on('modified',changed2); + if (options.timeout) { + this.timer=setTimeout(restore2,options.timeout); + } + } + if (options.text) { + this.selected=null; + function changed3() { + var offsetY=self.childBase||0, + vpos = self.getLinearPos(self.value,offsetY+self.inputRange.y0,self.inputRange.x0); + self.selected= value = self.value.slice(vpos,1000000); + if (options.timeout) { + if (self.timer) clearTimeout(self.timer); + self.timer=setTimeout(restore3,options.timeout); + } + } + function restore3 (timeout) { + var result = callback?callback(self.selected):null; + if (self.selected != undefined) self.print(self.selected,self.style.user); + if (result) self.print(result); + // restore text line + self.prompt=prompt; + // Log(self.inputRange,self.value) + ask('',!result && self.selected == undefined); + self.removeListener('modified',changed3); + if (!timeout && self.timer) clearTimeout(self.timer); + self.timer=null; + self.inputRange.action=null; + self.request=null; + } + this.inputRange.action=function () { + restore3() + } + this.on('modified',changed3); + if (options.timeout) { + this.timer=setTimeout(restore3,options.timeout); + } + } + this.request={message:message,options:options,callback:callback}; +} + + +// Public API: Bot message +Chat.prototype.message = function (line,style) { + return this.print(line,style||this.style.bot); +} + +// Print ONE line (call mutiple times for multi-line text). Auto wrapping is supprted, though. +Chat.prototype.print = function (line,style) { +// Log(this.inputRange,this._clines.length); + var offsetY = this.childBase||0, + cn1 = this._clines.length, y0=this.inputRange.y0, + start = this.getLinearPos(this.value,offsetY+this.inputRange.y0,0), + end = this.getLinearPos(this.value,offsetY+this.inputRange.y1, + this._clines[offsetY+this.inputRange.y1].length); +//Log(this.cpos,this.inputRange,this.value) + + if (style) { + if (style.color) line = '{'+style.color+'-fg}'+line+'{/'+style.color+'-fg}'; + if (style.align=='right') line = '{right}'+line+'{/right}'; + } + + var command = this.value.slice(start,end); + this.value=this.value.slice(0,start)+line+'\n'+command+this.value.slice(end); + this.screen.render(); + // Update inputRange + var cn2= this._clines.length; + this.scrollBottom(); + this.cpos.y += 10; + this.cpos.x=this.inputRange.x0=this.prompt.length; + this._updateCursor(true); + this.inputRange.y0=Math.min(this._clines.length-1,this.cpos.y-this.inputRange.last); + this.inputRange.y1=Math.min(this._clines.length-1,this.cpos.y); +// Log(this.cpos,this.inputRange,this.value) +} + +Chat.prototype._listener = function(ch, key) { + // Cursor position must be synced with scrollablebox and vice versa (if scrollable)! A real mess. + var done = this._done + , self = this + , value = this.value + , clinesLength=this._clines.length + , offsetY = this.childBase||0 // scrollable line offset if any + , newline = false + , lastchar = false + , backspace = false + , controlkey = false + , lastline = (this.cpos.y+offsetY+1) == clinesLength; + +// Log(key) + if (key.name === 'return') return; + if (key.name === 'delete') { + if (this.inputRange.x1 != undefined) { + // ranged edit + vpos=this.getLinearPos(this.value,offsetY+this.cpos.y, this.cpos.x); + // shift right x0..x1 text, delete current char at this position + var vpos0 = vpos-(this.cpos.x-this.inputRange.x0); + this.value = this.value.substr(0,vpos0)+' '+ + this.value.substr(vpos0,vpos-vpos0)+ + this.value.substr(vpos+1,1000000); + this.screen.render(); + } + return; + } + if (key.name === 'enter') { +// Log(this._clines) + if (this.inputRange.action) return this.inputRange.action(); + // clear input line; execute command; create new input line + var start = this.getLinearPos(this.value,offsetY+this.inputRange.y0,0), + end = this.getLinearPos(this.value,offsetY+this.inputRange.y1, + this._clines[offsetY+this.inputRange.y1].length); +// Log(this.inputRange,start,end,this.value,this._clines[0]) + var command = this.value.slice(start+this.prompt.length,end); + if (command && command != this.history[this.historyTop-1]) { + this.history.push(command); + this.historyTop=this.history.length; + } + this.value=this.value.slice(0,start)+this.prompt+this.value.slice(end); + this.screen.render(); + offsetY = this.childBase||0; + self.cpos.y += 10; // bottom + this._updateCursor(true); + this.cpos.x=self._clines[offsetY+self.cpos.y].length; + this.inputRange.y0=this.inputRange.y1=this.cpos.y; this.inputRange.last=0; + this.print(command,this.style.user); + this.emit('eval',command); + return; + } + if (this.input!='text') return; + + + function history(delta) { + if (self.historyTop+delta<0 ) return self.scrollBottom(); + self.historyTop += delta; + var start = self.getLinearPos(self.value,offsetY+self.inputRange.y0,0), + end = self.getLinearPos(self.value,offsetY+self.inputRange.y1, + self._clines[offsetY+self.inputRange.y1].length); + var command = self.history[self.historyTop]||''; + self.historyTop = Math.min(Math.max(0,self.historyTop),self.history.length); + self.value=self.value.slice(0,start)+self.prompt+command+self.value.slice(end); + self.screen.render(); + self.scrollBottom(); + self.cpos.x=self._clines[self._clines.length-1].length; + self.cpos.y += 10; // bottom + self._updateCursor(true); + self.inputRange.y1=self.cpos.y; + offsetY = self.childBase||0; + // find start y + var y0=self.cpos.y; while (self._clines[offsetY+y0].indexOf(self.prompt)!=0) y0--; + self.inputRange.y0=y0; + self.inputRange.last=self.inputRange.y1-self.inputRange.y0; + } + + // Handle cursor positiong by keys. + if (this.cursorControl) switch (key.name) { + case 'left': + controlkey=true; + if (this.cpos.y==this.inputRange.y0 && this.cpos.x>this.inputRange.x0) this.cpos.x--; + else if (this.cpos.y!=this.inputRange.y0 && this.cpos.x>0) this.cpos.x--; + else if (this.cpos.y!=this.inputRange.y0 && this.cpos.x==0) { + this.cpos.y--; + this.cpos.x=this._clines[offsetY+this.cpos.y].length; + } + this._updateCursor(true); + break; + case 'right': + controlkey=true; + if (this.inputRange.x1 != undefined && this.cpos.x>=this.inputRange.x1) return; + if (this.cpos.y>=this.inputRange.y0 && this.cpos.y<=this.inputRange.y1 && + this.cpos.x=this.inputRange.y0 && this.cpos.y=this.inputRange.y0 && this.cpos.y==this.inputRange.y1 && + this.cpos.x0) this.cpos.x--; + else {this.cpos.x=-1; if (offsetY==0 && this.cpos.y>0 && lastline) this.cpos.y--; }; + } + } else if (!controlkey && ch && this.inputRange.x1==undefined) { + if (!/^[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]$/.test(ch)) { + if (!this.cursorControl || + this.cpos.x==-1 || + (this.cpos.x==this._clines[offsetY+this.cpos.y].length && + this.cpos.y==this._clines.length-1-offsetY)) { + // Append new char at end of (last) line + lastchar=true; + this.value += ch; + } else { + // Insert new char into line at current cursor position + vpos=this.getLinearPos(this.value,offsetY+this.cpos.y, this.cpos.x); + // vpos+= this.cpos.x; + this.value = this.value.substr(0,vpos)+ch+ + this.value.substr(vpos,1000000); + } + if (newline) { + this.cpos.x=0; // first left position is zero! + this.cpos.y++; + } else + this.cpos.x++; + } + } else if (!controlkey && ch && this.inputRange.x1!=undefined) { + // Range Edit (e.g., numerical form field) + ////////////// + // shift or overwrite mode in limited char range (e.g., range selector) + if (!/^[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]$/.test(ch)) { + vpos=this.getLinearPos(this.value,offsetY+this.cpos.y, this.cpos.x); + if (this.cpos.x!=this.inputRange.x1) { + // Replace new char into line at current cursor position + // vpos+= this.cpos.x; + this.value = setCharAt(this.value,vpos,ch); + } else { + // shift left x0..x1 text, insert new char at right position + var vpos0 = vpos-(this.inputRange.x1-this.inputRange.x0); + this.value = this.value.substr(0,vpos0)+ + this.value.substr(vpos0+1,vpos-vpos0)+ch+ + this.value.substr(vpos+1,1000000); + + } + if (this.cpos.x < this.inputRange.x1) this.cpos.x++; + } + } + + var rmline=this.cpos.x==-1; + // TODO: clean up this mess; use rtof and ftor attributes of _clines + // to determine where we are (react correctly on line wrap extension and reduction) + if (this.value !== value) { + var cn0=clinesLength, + endofline=this.cpos.x==this._clines[offsetY+this.cpos.y].length+1, + cn1=this._clines.length; + var linelength1=this._clines[offsetY+this.cpos.y] && this._clines[offsetY+this.cpos.y].length; + this.screen.render(); + var linelength2=this._clines[offsetY+this.cpos.y] && this._clines[offsetY+this.cpos.y].length; + var cn2=this._clines.length; +// Log(this.cpos,linelength1,linelength2,cn0,cn2,this.inputRange,lastline,lastchar,endofline); +// Log('L',this.cpos,lastline,lastchar,endofline,linelength1,linelength2); + if (cn2>cn0 && endofline) { + this.scrollBottom(); + // wrap expansion + this.cpos.y++; + this.inputRange.last++; + if (this._clines[offsetY+this.cpos.y] && lastchar) this.cpos.x=this._clines[offsetY+this.cpos.y].length; + else this.cpos.x=linelength1-linelength2-1; + this._updateCursor(true); + this.inputRange.y0=this.cpos.y-this.inputRange.last; + this.inputRange.y1=this.cpos.y; + } else if (cn20 && !lastline && lastchar) this.cpos.y--; + this.inputRange.last--; + if (this._clines[offsetY+this.cpos.y]) this.cpos.x=this._clines[offsetY+this.cpos.y].length; + this._updateCursor(true); + this.inputRange.y0=this.cpos.y-this.inputRange.last; + this.inputRange.y1=this.cpos.y; + offsetY = this.childBase||0; + this.cpos.x=this._clines[offsetY+this.cpos.y].length; + this._updateCursor(true); + } else if (linelength20 && backspace) { + // @fix line deleted; refresh again due to miscalculation of height in scrollablebox! + this.scroll(0); + this.screen.render(); + if (rmline) { + if (this._clines[offsetY+this.cpos.y]) this.cpos.x=this._clines[offsetY+this.cpos.y].length; + else if (this._clines[offsetY+this.cpos.y-1]) this.cpos.x=this._clines[offsetY+this.cpos.y-1].length; + this._updateCursor(true); + } + } + this.emit('modified'); + } + +}; + +// Return start position of nth (c)line in linear value string +Chat.prototype.getLinearPos = function(v,clineIndex,cposx) { + var lines=v.split('\n'), + line=this._clines[clineIndex], // assuming search in plain text line (no ctrl/spaces aligns) + flinenum=this._clines.rtof[clineIndex], + vpos=flinenum>0? + lines.slice(0,flinenum) + .map(function (line) { return line.length+1 }) + .reduce(function (a,b) { return a+b }):0; +// console.log(clineIndex,lines.length,flinenum,lines[flinenum],line); + // TODO: search from a start position in line estimated by _clines.ftor array + function search(part,line) { + // assuming search offset in plain text line (no ctrl/spaces aligns) + var i=line.indexOf(part); + if (i==-1) return 0; + else return i; + } + if (lines[flinenum]) { + return vpos+search(line,lines[flinenum])+cposx; + } else + return vpos+cposx; +} + +Chat.prototype._typeScroll = function() { + // XXX Workaround + var height = this.height - this.iheight; + // Scroll down? +// if (typeof Log != 'undefined') Log(this.childBase,this.childOffset,this.cpos.y,height); + //if (this._clines.length - this.childBase > height) { + if (this.cpos.y == height) { + this.scroll(this._clines.length); + } +}; + +Chat.prototype.getValue = function() { + return this.value; +}; + +Chat.prototype.setValue = function(value) { + if (value == null) { + value = this.value; + } + if (this._value !== value) { + this.value = value; + this._value = value; + this.setContent(this.value); + this._typeScroll(); + this._updateCursor(); + } +}; + +Chat.prototype.clearInput = +Chat.prototype.clearValue = function() { + return this.setValue(''); +}; + +Chat.prototype.submit = function() { + if (!this.__listener) return; + return this.__listener('\x1b', { name: 'escape' }); +}; + +Chat.prototype.cancel = function() { + if (!this.__listener) return; + return this.__listener('\x1b', { name: 'escape' }); +}; + +Chat.prototype.render = function() { + this.setValue(); + return this._render(); +}; + +Chat.prototype.editor = +Chat.prototype.setEditor = +Chat.prototype.readEditor = function(callback) { + var self = this; + + if (this._reading) { + var _cb = this._callback + , cb = callback; + + this._done('stop'); + + callback = function(err, value) { + if (_cb) _cb(err, value); + if (cb) cb(err, value); + }; + } + + if (!callback) { + callback = function() {}; + } + + return this.screen.readEditor({ value: this.value }, function(err, value) { + if (err) { + if (err.message === 'Unsuccessful.') { + self.screen.render(); + return self.readInput(callback); + } + self.screen.render(); + self.readInput(callback); + return callback(err); + } + self.setValue(value); + self.screen.render(); + return self.readInput(callback); + }); +}; + +/** + * Expose + */ + +module.exports = Chat;