diff --git a/core/terminal.js b/core/terminal.js
index e497051..a49a177 100644
--- a/core/terminal.js
+++ b/core/terminal.js
@@ -5,6 +5,7 @@
License, v2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ modified Simon.G.Andrews Nov 2022 for supporting ANSI terminal attribute Issue #154
------------------------------------------------------------------
VT100 terminal window
------------------------------------------------------------------
@@ -47,6 +48,14 @@
// maximum lines on the terminal
var MAX_LINES = 2048;
+ /* Display Attributes of each termText line as array of array of objects.
+ Set during recievedCharacter() processing. Object properties:
+ pos: int - character position of style on termtext line
+ type: int - type of attribute. Corresponding to attribute ID in attributes
+ value: string - parameter (depending on type) eg reset,on,off,colour - see setActiveStyles(obj)
+ */
+ var termAttribute = [];
+
// mapping of display attribute type/ID to HTML in line style - in response to ANSI Seq - esc [ ...
const attributes = [
{ID:0,name:'resetAll',styleStr:''}, // 0m resets all
@@ -59,36 +68,46 @@
{ID:6,name:'background',styleStr:'background-color:'} //style+colour 40->47 set, 49 clear
]
- /* Display Attributes of each termText line as array of array of objects
- set during recievedCharacter() processing. object properties:
- seq: int - sequence order of attribute in termtext line
- pos: int - character position of style on termtext line
- type: int - type of attribute corresponding to attribute ID
- value: string - parameter (depending on type) eg reset,on,off,colour
- */
- var termAttribute = [];
+ // maintain current state mapping for handleReceivedCharacter()
+ var currentState = 'start';
- /* Styles currently active during HTML creation. As array:
- index = ID-1 of corresponding attribute
+ // maintain buffer of attribute objects for currrent control char sequence
+ // enables handling delimited multiple display attributes using bufferAttrib() & sendAttrib()
+ var attribBuffer = [];
+
+ /* Styles currently active for HTML creation. As array:
+ index = ID-1 of corresponding attribute in attributes[]
value (string) = undefined || complete style string (as per attributes[]) inc colour/font weight value
*/
var activeStyles = [];
- const fullModalAttribs = true; //set true then any active attribute styles carry to new lines
-
- // map of ANSI display attribute colours for code 0->7, index = code
- const colorMap = ["black","red","green","yellow","blue","magenta","cyan","white"];
-
- // updates active style for a termAttribute object
- function setActiveStyles(obj){
+ // updates active style for a passed termAttribute object
+ function setActiveStyles(obj) {
switch (obj.value) {
- case 'reset' : activeStyles = [] ; break;
- case 'on' : activeStyles[obj.type-1] = attributes[obj.type].styleStr ; break;
- case 'off' : activeStyles[obj.type-1] = undefined ; break;
- default : activeStyles[obj.type-1] = attributes[obj.type].styleStr + obj.value; break;
+ case "reset": {
+ activeStyles = [];
+ break;
+ }
+ case "on": {
+ activeStyles[obj.type - 1] = attributes[obj.type].styleStr;
+ break;
+ }
+ case "off": {
+ activeStyles[obj.type - 1] = undefined;
+ break;
+ }
+ default: {
+ activeStyles[obj.type - 1] = attributes[obj.type].styleStr + obj.value;
+ break;
+ }
}
}
+// TODO const fullModalAttribs = true; //set true then any active attribute styles carry to new lines
+
+ // map of ANSI display attribute colours for code 0->7, index = code
+ const colorMap = ["black","red","green","yellow","blue","magenta","cyan","white"];
+
function init()
{
// Add stuff we need
@@ -374,7 +393,8 @@
};
/* check for any terminal-inline-image and if they are still BMP
- then convert them to PNG using canvas */
+ then convert them to PNG using canvas
+ */
function convertInlineImages() {
imageTimeout = null;
var images = document.getElementsByClassName("terminal-inline-image");
@@ -398,7 +418,7 @@
})(images[i]);
}
- /// send the given characters as if they were typed
+ // send the given characters as if they were typed
var typeCharacters = function(s) {
onInputData(s);
}
@@ -424,20 +444,23 @@
Espruino.callProcessor("terminalClear");
};
- /**
- * function buildAttribSpans
- * returns {string} updated line with HTML spans created to style as per corresponding line attributes
- * with text elements in line Escaped via Utils.escapeHTML()
- * param {string} line - line form termText[]
- * param {array of objects} attribs - line attributes as defined in termAttribute[] for the line of text
+ /*
+ function buildAttribSpans
+ returns {string} updated line with HTML spans created to style as per corresponding line attributes
+ with text elements in line Escaped via Utils.escapeHTML()
+ param {string} line - line form termText[]
+ param {array of objects} attribs - line attributes as defined in termAttribute[] for the line of text
+ TODO - support full modal or not effect of attributes
*/
var buildAttribSpans = function (line, attribs) {
- // full modal mode uses any current attribte styles set on previous lines
- if (!fullModalAttribs && !attribs) return Espruino.Core.Utils.escapeHTML(line);
- if (!attribs) return "" + Espruino.Core.Utils.escapeHTML(line) + "" ;
+ // if no attributes for line use any active styles
+ if (!attribs && !activeStyles) return ( Espruino.Core.Utils.escapeHTML(line) );
+ if (!attribs) return ( "" + Espruino.Core.Utils.escapeHTML(line) + "" );
+ // this reduce produces a sequence of HTML spans, one span for each of the attrib objects in the line
+ // consumes all the text for the line uinsg the pos's in the attrib.
var result = attribs.reduce(function (acc, obj, i, arr) {
- setActiveStyles(obj);
+ setActiveStyles(obj); // styles from attributes are acumulating in activeStyles until cleared
let end = !arr[i + 1] ? line.length : arr[i + 1].pos;
if (end == obj.pos) return acc; // no text just styles
return (
@@ -500,10 +523,10 @@
var line = termText[y];
if (y == termCursorY) { // current line
var ch = Espruino.Core.Utils.getSubString(line,termCursorX,1);
- line = Espruino.Core.Utils.escapeHTML(
- Espruino.Core.Utils.getSubString(line,0,termCursorX)) +
- "" + Espruino.Core.Utils.escapeHTML(ch) + "" +
- Espruino.Core.Utils.escapeHTML(Espruino.Core.Utils.getSubString(line,termCursorX+1));
+ line =
+ buildAttribSpans(Espruino.Core.Utils.getSubString(line,0,termCursorX)) +
+ "" + Espruino.Core.Utils.escapeHTML(ch) + "" +
+ buildAttribSpans(Espruino.Core.Utils.getSubString(line,termCursorX+1))
} else {
line = buildAttribSpans(line,termAttribute[y]);
// handle URLs
@@ -518,7 +541,6 @@
imageTimeout = setTimeout(convertInlineImages, 1000);
}
-
// extra text is for stuff like tutorials
if (termExtraText[y])
line = termExtraText[y] + line;
@@ -533,6 +555,9 @@
} else if (elements[y].html()!=line)
elements[y].html(line);
}
+ // finished updating lines so reset current Styles
+ activeStyles = [];
+
// now show the line where the cursor is
if (elements[termCursorY]!==undefined) {
terminal[0].scrollTop = elements[termCursorY][0].offsetTop;
@@ -556,9 +581,153 @@
return str.substr(0,s+1);
}
+ /* Recieved Characters State Maps **
+ Nextstate is chosen for the FIRST match of current state (state) and match of the recieved character (inChrs).
+ If inChrs is RegExp then regex match is used otherwise charaacter equivalent is tested.
+ ccReset()is executed when no match found.
+ Action function is executed when a match, where typeof action is function.
+
+ ccStateMapStart - shortend list when in state start - Execute addCharacters() when no match found.
+ ccStateMapCChars - other states to hanfle control characters - Execute ccReset() when no match found.
+
+ all characters recieved in the current control sequence are in string termControlChars.
+ */
+ var ccStateMapStart =[
+ {state:'start',inChrs:'\x1B',nextState:'wait@ESC',action:'' }, // escape
+ {state:'start',inChrs:'\x08',nextState:'start',action:() => cursorLeft() }, // backspace
+ {state:'start',inChrs:'\x0A',nextState:'start',action:() => cursorLF() }, // line feed
+ {state:'start',inChrs:'\x0D',nextState:'start',action:() => cursorCR() }, // Carrage return
+ {state:'start',inChrs:/[\x11\x13\xC2]/,nextState:'start',action: () => ccReset() } // ignore: xon, xoff, UTF8 for <255
+ ]
+
+ var ccStateMapCChars =[
+ {state:'wait@ESC',inChrs:'[',nextState:'wait@CSI',action: '' },
+
+ {state:'wait@CSI',inChrs:'A',nextState:'start',action: () => cursorUp() },
+ {state:'wait@CSI',inChrs:'B',nextState:'start',action: () => cursorDown() },
+ {state:'wait@CSI',inChrs:'C',nextState:'start',action: () => cursorRight() },
+ {state:'wait@CSI',inChrs:'D',nextState:'start',action: () => cursorLeft() },
+ {state:'wait@CSI',inChrs:'J',nextState:'start',action: () => delEndOfScreen() },
+ {state:'wait@CSI',inChrs:'K',nextState:'start',action: () => delEndOfLine() },
+
+ {state:'wait@CSI',inChrs:'m',nextState:'start',action: () => saveAttrib({type:0,value:'reset'})}, // m after CSI (no params) - reset all attribs
+ {state:'wait@CSI',inChrs:'0',nextState:'wait@CSI_0',action:''},
+ {state:'wait@CSI',inChrs:'1',nextState:'wait@CSI_1',action:''},
+ {state:'wait@CSI',inChrs:'2',nextState:'wait@CSI_2',action:''},
+ {state:'wait@CSI',inChrs:'3',nextState:'wait@CSI_3',action:''},
+ {state:'wait@CSI',inChrs:'4',nextState:'wait@CSI_4',action:''},
+ {state:'wait@CSI',inChrs:'9',nextState:'wait@CSI_9',action:''},
+ {state:'wait@CSI',inChrs:'?',nextState:'wait@Custom',action:''},
+
+ {state:'wait@CSI_0',inChrs:'m',nextState:'start',action: () => saveAttrib({type:0,value:'reset'})},
+ {state:'wait@CSI_0',inChrs:';',nextState:'wait@Delim',action: () => bufferAttrib({type:0,value:'reset'})},
+ {state:'wait@CSI_1',inChrs:'m',nextState:'start',action: () => saveAttrib({type:2,value:'bolder'})},
+ {state:'wait@CSI_1',inChrs:';',nextState:'wait@Delim',action: () => bufferAttrib({type:2,value:'bolder'})},
+ {state:'wait@CSI_2',inChrs:'m',nextState:'start',action: () => saveAttrib({type:2,value:'lighter'})},
+ {state:'wait@CSI_2',inChrs:';',nextState:'wait@Delim',action: () => bufferAttrib({type:2,value:'lighter'})},
+ {state:'wait@CSI_2',inChrs:/[234]/,nextState:'wait@Clear',action:''},
+ {state:'wait@CSI_2',inChrs:'9',nextState:'wait@ClearStrikeOut',action:''},
+
+ {state:'wait@CSI_3',inChrs:/[1-7]/,nextState:'wait@FColor',action:''},
+ {state:'wait@CSI_3',inChrs:'m',nextState:'start',action: () => saveAttrib({type:3,value:'on'})},
+ {state:'wait@CSI_3',inChrs:';',nextState:'wait@Delim',action: () => bufferAttrib({type:3,value:'on'})},
+ {state:'wait@CSI_4',inChrs:/[1-7]/,nextState:'wait@BColor',action:''},
+ {state:'wait@CSI_4',inChrs:'m',nextState:'start',action: () => saveAttrib({type:4,value: 'on'})},
+ {state:'wait@CSI_4',inChrs:';',nextState:'wait@Delim',action: () => bufferAttrib({type:4,value: 'on'})},
+ {state:'wait@CSI_9',inChrs:'m',nextState:'start',action: () => saveAttrib({type:5,value: 'on'})},
+ {state:'wait@CSI_9',inChrs:';',nextState:'wait@Delim',action: () => bufferAttrib({type:5,value: 'on'})},
+ {state:'wait@Clear',inChrs:'m',nextState:'start',action: () => saveAttrib({type:+termControlChars.slice(-2,-1),value: 'off'})},
+ {state:'wait@Clear',inChrs:';',nextState:'wait@Delim',action: () => bufferAttrib({type:+termControlChars.slice(-2,-1),value: 'off'})},
+ {state:'wait@ClearStrikeOut',inChrs:'m',nextState:'start',action: () => saveAttrib({type:5,value: 'off'})},
+ {state:'wait@ClearStrikeOut',inChrs:';',nextState:'wait@Delim',action: () => bufferAttrib({type:5,value: 'off'})},
+ {state:'wait@FColor',inChrs:'m',nextState:'start',action: () => saveAttrib({type:1,value:colorMap[termControlChars.slice(-2,-1)]})},
+ {state:'wait@FColor',inChrs:';',nextState:'wait@Delim',action: () => bufferAttrib({type:1,value:colorMap[termControlChars.slice(-2,-1)]})},
+ {state:'wait@BColor',inChrs:'m',nextState:'start',action: () => saveAttrib({type:6,value:colorMap[termControlChars.slice(-2,-1)]})},
+ {state:'wait@BColor',inChrs:';',nextState:'wait@Delim',action: ( )=> bufferAttrib({type:6,value:colorMap[termControlChars.slice(-2,-1)]})},
+ {state:'wait@Delim',inChrs:'0',nextState:'wait@CSI_0',action:''},
+ {state:'wait@Delim',inChrs:'1',nextState:'wait@CSI_1',action:''},
+ {state:'wait@Delim',inChrs:'2',nextState:'wait@CSI_2',action:''},
+ {state:'wait@Delim',inChrs:'3',nextState:'wait@CSI_3',action:''},
+ {state:'wait@Delim',inChrs:'4',nextState:'wait@CSI_4',action:''},
+ {state:'wait@Delim',inChrs:'9',nextState:'wait@CSI_9',action:''},
+ {state:'wait@Delim',inChrs:'m',nextState:'start',action: () => saveAttrib({type:0,value:'reset'})}, // m without param - assume 0 and reset all
+ {state:'wait@Delim',inChrs:';',nextState:'wait@Delim',action: () => bufferAttrib({type:0,value:'reset'})}, // null betweeen delims - reset all attribs
+
+ {state:'wait@Custom',inChrs:'7',nextState:'wait@Custom_7',action:''},
+ {state:'wait@Custom_7',inChrs:'l',nextState:'start',action: () => ccReset()},
+ {state:'wait@Custom_7',inChrs:/^l/,nextState:'start',action: () => {console.log("Expected 27, 91, 63, 55, 108 - no line overflow sequence");ccReset()}},
+ ]
+
+ // action functions used in statemaps
+ function cursorLeft() {
+ if (termCursorX > 0) termCursorX--;
+ ccReset();
+ };
+
+ function cursorRight() {
+ termCursorX++;
+ ccReset();
+ };
+
+ function cursorUp() {
+ if (termCursorY > 0) termCursorY--;
+ ccReset();
+ };
+
+ function cursorDown() {
+ termCursorY++;
+ while (termCursorY >= termText.length) termText.push("");
+ ccReset();
+ };
+
+ function cursorLF() {
+ Espruino.callProcessor("terminalNewLine", termText[termCursorY]);
+ termCursorX = 0; termCursorY++;
+ while (termCursorY >= termText.length) termText.push("");
+ ccReset();
+ }
+
+ function cursorCR() {
+ termCursorX = 0;
+ ccReset();
+ };
+
+ function delEndOfScreen() {
+ termText[termCursorY] = termText[termCursorY].substr(0,termCursorX);
+ termText = termText.slice(0,termCursorY+1);
+ ccReset();
+ }
+
+ function delEndOfLine() {
+ termText[termCursorY] = termText[termCursorY].substr(0,termCursorX);
+ ccReset();
+ }
+
+ function bufferAttrib(obj) {
+ // add attribute obj to attribBuffer
+ obj.pos = termCursorX;
+ attribBuffer = !attribBuffer ? [].concat(obj): attribBuffer.concat(obj);
+ };
+
+ function saveAttrib(obj) {
+ // add attribute obj to attribBuffer and commit buffer to attributes for line
+ obj.pos = termCursorX;
+ attribBuffer = !attribBuffer ? [].concat(obj): attribBuffer.concat(obj);
+ termAttribute[termCursorY] = attribBuffer;
+ ccReset();
+ };
+
+ function ccReset(){
+ // reset current escape sequence processing
+ termControlChars = '';
+ attribBuffer = [];
+ }
+
// Add Character string (str) to termText for output
- var addCharacters = function (str){
- if (termText[termCursorY]===undefined) termText[termCursorY]="";
+ function addCharacters(str){
+ if (termText[termCursorY]===undefined) {
+ termText[termCursorY]="";
+ }
termText[termCursorY] = trimRight(
Espruino.Core.Utils.getSubString(termText[termCursorY],0,termCursorX) +
str +
@@ -574,147 +743,44 @@
}
}
- var handleReceivedCharacter = function (/*char*/ch) {
- switch (termControlChars.length) {
- case 0 : {
- switch (ch) {
- case 8 : // BS
- if (termCursorX>0) termCursorX--;
- break;
- case 10 : // line feed
- Espruino.callProcessor("terminalNewLine", termText[termCursorY]);
- termCursorX = 0; termCursorY++;
- while (termCursorY >= termText.length) termText.push("");
- break;
- case 13 : // carriage return
- termCursorX = 0;
- break;
- case 27 : // Esc
- termControlChars = String.fromCharCode(27);
- break;
- case 19 : break; // XOFF
- case 17 : break; // XON
- case 0xC2 : break; // UTF8 for <255 - ignore this
- default : addCharacters(String.fromCharCode(ch)); // Else actually add character
- }
- break;
+ var handleReceivedCharacter = function (cCode) {
+ var ch = String.fromCharCode(cCode);
+ var newState = {};
+ // console.log('** got char > ' +ch);
+ termControlChars += ch; // add recieved characters - cleared with ccReset()
+
+ // use statemaps to identify any actions to take on input character from a given state
+ if (currentState == 'start'){ // search the shorter statemap
+ newState = ccStateMapStart.find(
+ (states) =>
+ states.state === 'start' &&
+ (states.inChrs instanceof RegExp
+ ? states.inChrs.test(ch)
+ : states.inChrs === ch)
+ );
+ if (!newState) addCharacters(ch) // no specific mapping so add character to terminal
+ else {
+ currentState = newState.nextState;
+ if (typeof newState.action === 'function') newState.action(); // actions reset state if req'd
}
- case 1 :
- if (termControlChars == '\x1B') // Esc
- switch (ch) {
- case 91: termControlChars += '['; break; // Esc [
- default: termControlChars = '';
- } else termControlChars = '';
- break;
- case 2 :
- if (termControlChars == '\x1B[') { // Esc [
- switch (ch) {
- case 63: termControlChars += '?'; break; // Esc [ ?
- case 65: // up
- if (termCursorY > 0) termCursorY--;
- termControlChars = '';
- break; // old FIXME should add extra lines in...
- case 66: // down
- termCursorY++;
- while (termCursorY >= termText.length) termText.push("");
- termControlChars = '';
- break; // old FIXME should add extra lines in...
- case 67: //right
- termCursorX++;
- termControlChars = '';
- break;
- case 68: // left
- if (termCursorX > 0) termCursorX--;
- termControlChars = '';
- break;
- case 74: // Delete to right + down
- termText[termCursorY] = termText[termCursorY].substr(0,termCursorX);
- termText = termText.slice(0,termCursorY+1);
- termControlChars = '';
- break;
- case 75: // K Delete to right
- termText[termCursorY] = termText[termCursorY].substr(0,termCursorX);
- termControlChars = '';
- break;
- default:
- termControlChars += String.fromCharCode(ch)
- if (/\x1B\[[0-49]/.test(termControlChars)) break; // setting display attribute - more to come
- termControlChars = '';
- }
- } else termControlChars = '';
- break;
- case 3 :
- termControlChars += String.fromCharCode(ch);
- if (/\x1B\[\?7/.test(termControlChars)) break; // Esc [ ? 7 - line overflow sequence - more to come
- if (/\x1B\[[34][0-79]/.test(termControlChars)) break; // colour device attribute - more to come
- if (/\x1B\[[2][2349]/.test(termControlChars)) break; // device attribute - more to come
-
- if (/\x1B\[0m/.test(termControlChars)) { //reset all - add to term line attributes
- termAttribute[termCursorY] = !termAttribute[termCursorY]
- ? [].concat({seq:0,pos:termCursorX,type:0,value:'reset'})
- : termAttribute[termCursorY].concat({seq:termAttribute[termCursorY].length,pos:termCursorX,type:0,value:'reset'})
- termControlChars = '';
- break;
- }
- if (/\x1B\[[12]m/.test(termControlChars)) { //set bold/faint text - add to term line attribute
- let weight = termControlChars.slice(2,3)==1? 'bolder': 'lighter';
- termAttribute[termCursorY] = !termAttribute[termCursorY]
- ? [].concat({seq:0,pos:termCursorX,type:2,value:weight})
- : termAttribute[termCursorY].concat({seq:termAttribute[termCursorY].length,pos:termCursorX,type:2,value:weight})
- termControlChars = '';
- break;
+ } else{ // search full control character state map
+ newState = ccStateMapCChars.find(
+ (states) =>
+ states.state === currentState &&
+ (states.inChrs instanceof RegExp
+ ? states.inChrs.test(ch)
+ : states.inChrs === ch)
+ );
+ if (!newState){ // back to start state when no match found
+ currentState = 'start';
+ ccReset();
}
- if (/\x1B\[[349]m/.test(termControlChars)) { //set italic/underline/line-thru - add to term line attributes
- let attribType = termControlChars.slice(2,3)=='9'? 5: +termControlChars.slice(2,3);
- termAttribute[termCursorY] = !termAttribute[termCursorY]
- ? [].concat({seq:0,pos:termCursorX,type:attribType,value:'on'})
- : termAttribute[termCursorY].concat({seq:termAttribute[termCursorY].length,pos:termCursorX,type:attribType,value:'on'})
- termControlChars = '';
- break;
+ else {
+ currentState = newState.nextState;
+ if (typeof newState.action === 'function') newState.action();
}
- termControlChars = '';
- break;
- case 4 :
- termControlChars += String.fromCharCode(ch);
- if (/\x1B\[\?7/.test(termControlChars)) { // Esc [ ? 7
- if (ch!=108) { // Esc [ ? 7 l
- console.log("Expected 27, 91, 63, 55, 108 - no line overflow sequence");
- }
- termControlChars = ''; // got Esc [ ? 7 l or not - reset term control chars
- break;
- }
- if (/\x1B\[[3][0-7]m/.test(termControlChars)) { //set foreground colour - add to term line attributes
- termAttribute[termCursorY] = !termAttribute[termCursorY]
- ? [].concat( {seq:0,pos:termCursorX,type:1,value:colorMap[termControlChars.slice(3,4)]})
- : termAttribute[termCursorY].concat( {seq:termAttribute[termCursorY].length,pos:termCursorX,type:1,value:colorMap[termControlChars.slice(3,4)]})
- termControlChars = '';
- break;
- }
- if (/\x1B\[[4][0-7]m/.test(termControlChars)) { //set background colour - add to term line attributes
- termAttribute[termCursorY] = !termAttribute[termCursorY]
- ? [].concat( {seq:0,pos:termCursorX,type:6,value:colorMap[termControlChars.slice(3,4)]})
- : termAttribute[termCursorY].concat( {seq:termAttribute[termCursorY].length,pos:termCursorX,type:6,value:colorMap[termControlChars.slice(3,4)]})
- termControlChars = '';
- break;
- }
- if (/\x1B\[2[2349]m/.test(termControlChars)) { //turn off fontWeight(bold,faint)/italic/underline/line-thru - add to term line attributes
- let attribType = termControlChars.slice(3,4)=='9'? 5: +termControlChars.slice(3,4);
- termAttribute[termCursorY] = !termAttribute[termCursorY]
- ? [].concat( {seq:0,pos:termCursorX,type:attribType,value:'off'})
- : termAttribute[termCursorY].concat( {seq:termAttribute[termCursorY].length,pos:termCursorX,type:attribType,value:'off'})
- termControlChars = '';
- break;
- }
- if (/\x1B\[[34]9m/.test(termControlChars)) { //turn off foreground/background colour - add to term line attributes
- let attribType = termControlChars.slice(2,3)=='3'? 1: 6;
- termAttribute[termCursorY] = !termAttribute[termCursorY]
- ? [].concat( {seq:0,pos:termCursorX,type:attribType,value:'off'})
- : termAttribute[termCursorY].concat( {seq:termAttribute[termCursorY].length,pos:termCursorX,type:attribType,value:'off'})
- termControlChars = '';
- break;
- }
- default: termControlChars = '';
- }
+ }
+ console.log ('** new state > ' + currentState);
}
// ----------------------------------------------------------------------------------------------------------