From 4125f90f06c1270a24746b63957db26bc369e0ab Mon Sep 17 00:00:00 2001 From: Leon van Kammen Date: Wed, 15 Jan 2025 18:02:30 +0100 Subject: [PATCH] tweaks for fbterm --- com/isoterminal.js | 129 ++- com/isoterminal/ISOTerminal.js | 23 +- com/isoterminal/feat/boot.js | 2 +- com/isoterminal/feat/pastedrop.js | 16 +- com/isoterminal/feat/term.js | 88 ++ com/isoterminal/term.js | 1460 +++++++++++++++++++++++++++++ 6 files changed, 1661 insertions(+), 57 deletions(-) create mode 100644 com/isoterminal/feat/term.js create mode 100644 com/isoterminal/term.js diff --git a/com/isoterminal.js b/com/isoterminal.js index 6a6c594..759706f 100644 --- a/com/isoterminal.js +++ b/com/isoterminal.js @@ -35,8 +35,8 @@ if( typeof AFRAME != 'undefined '){ schema: { iso: { type:"string", "default":"https://forgejo.isvery.ninja/assets/xrsh-buildroot/main/xrsh.iso" }, overlayfs: { type:"string"}, - width: { type: 'number',"default": 700 }, - height: { type: 'number',"default": 500 }, + width: { type: 'number',"default": 800 }, + height: { type: 'number',"default": 600 }, depth: { type: 'number',"default": 0.03 }, lineHeight: { type: 'number',"default": 18 }, padding: { type: 'number',"default": 18 }, @@ -46,9 +46,9 @@ if( typeof AFRAME != 'undefined '){ HUD: { type: 'boolean',"default":false}, // link to camera movement transparent: { type:'boolean', "default":false }, // need good gpu memory: { type: 'number', "default":60 }, // VM memory (in MB) [NOTE: quest or smartphone might crash > 40mb ] - bufferLatency: { type: 'number', "default":1 }, // in ms: bufferlatency from webworker to xterm (batch-update every char to texture) + bufferLatency: { type: 'number', "default":1 }, // in ms: bufferlatency from webworker to xterm (batch-update every char to texture) debug: { type: 'boolean', "default":false }, - emulator: { type: 'string', "default": "vt100" } + emulator: { type: 'string', "default": "fbterm" }// terminal emulator }, init: function(){ @@ -59,7 +59,6 @@ if( typeof AFRAME != 'undefined '){ this.calculateDimension() this.initHud() - this.setupBox() this.setupPasteDrop() fetch(this.data.iso,{method: 'HEAD'}) @@ -94,26 +93,18 @@ if( typeof AFRAME != 'undefined '){ scale: 0.66, events: ['click','keydown'], html: (me) => `
-
- -
-

-                          
+ +
`, - css: (me) => `.isoterminal{ + css: (me) => ` + + .isoterminal{ padding: ${me.com.data.padding}px; width:100%; - height:90%; - position:relative; - } - .isoterminal div{ - display:block; - position:relative; - line-height: ${me.com.data.lineHeight}px; - } - #term { - outline: none !important; + height:99%; + resize: both; + overflow: hidden; } @font-face { font-family: 'Cousine'; @@ -128,6 +119,73 @@ if( typeof AFRAME != 'undefined '){ src: url(./com/isoterminal/assets/CousineBold.ttf) format('truetype'); } + .isoterminal *{ + outline:none; + box-shadow:none; + } + + .term { + font-family: 'Cousine'; + line-height: ${me.com.data.lineHeight}px; + font-weight: normal; + font-variant-ligatures: none; + color: #f0f0f0; + overflow: hidden; + white-space: nowrap; + } + + .term_content a { + color: inherit; + text-decoration: underline; + color:#2AFF; + } + + .term_content a span{ + text-shadow: 0px 0px 10px #F07A; + } + + .term_content a:hover { + color: inherit; + text-decoration: underline; + animation:fade 1000ms infinite; + -webkit-animation:fade 1000ms infinite; + } + + .term_cursor { + color: #000000; + background: #70f; + animation:fade 1000ms infinite; + -webkit-animation:fade 1000ms infinite; + } + + .term_char_size { + display: inline-block; + visibility: hidden; + position: absolute; + top: 0px; + left: -1000px; + padding: 0px; + } + + .term_textarea { + position: absolute; + top: 0px; + left: 0px; + width: 0px; + height: 0px; + padding: 0px; + border: 0px; + margin: 0px; + opacity: 0; + resize: none; + } + + .term_scrollbar { background: transparent url(images/bg-scrollbar-track-y.png) no-repeat 0 0; position: relative; background-position: 0 0; float: right; height: 100%; } + .term_track { background: transparent url(images/bg-scrollbar-trackend-y.png) no-repeat 0 100%; height: 100%; width:13px; position: relative; padding: 0 1px; } + .term_thumb { background: transparent url(images/bg-scrollbar-thumb-y.png) no-repeat 50% 100%; height: 20px; width: 25px; cursor: pointer; overflow: hidden; position: absolute; top: 0; left: -5px; } + .term_thumb .term_end { background: transparent url(images/bg-scrollbar-thumb-y.png) no-repeat 50% 0; overflow: hidden; height: 5px; width: 25px; } + .noSelect { user-select: none; -o-user-select: none; -moz-user-select: none; -khtml-user-select: none; -webkit-user-select: none; } + .isoterminal style{ display:none } blink{ @@ -140,12 +198,6 @@ if( typeof AFRAME != 'undefined '){ box-shadow:none; } - .cursor { - background: #70F !important; - animation:fade 1000ms infinite; - -webkit-animation:fade 1000ms infinite; - } - .XR .cursor { animation:none; -webkit-animation:none; @@ -214,10 +266,10 @@ if( typeof AFRAME != 'undefined '){ pastedropFeat: "com/isoterminal/feat/pastedrop.js", httpfs: "com/isoterminal/feat/httpfs.js", } - if( this.data.emulator == "vt100" ){ - features['VT100js'] = "com/isoterminal/VT100.js" - features['vt100'] = "com/isoterminal/feat/vt100.js" - } + if( this.data.emulator == 'fbterm' ){ + features['fbtermjs'] = "com/isoterminal/term.js" + features['fbterm'] = "com/isoterminal/feat/term.js" + } await AFRAME.utils.require(features) this.el.setAttribute("selfcontainer","") @@ -325,25 +377,14 @@ if( typeof AFRAME != 'undefined '){ return this }, - setupBox: function(){ - // setup slightly bigger black backdrop (this.el.getObject3D("mesh")) - const w = this.data.width/950; - const h = this.data.height/950; - this.el.box = document.createElement('a-entity') - this.el.box.setAttribute("geometry",`primitive: box; width:${w}; height:${h}; depth: -${this.data.depth}`) - this.el.box.setAttribute("material","shader:flat; color:black; opacity:0.9; transparent:true; ") - this.el.box.setAttribute("position",`0 0 ${(this.data.depth/2)-0.001}`) - this.el.appendChild(this.el.box) - }, - calculateDimension: function(){ if( this.data.width == -1 ) this.data.width = document.body.offsetWidth; if( this.data.height == -1 ) this.data.height = Math.floor( document.body.offsetHeight - 30 ) if( this.data.height > this.data.width ) this.data.height = this.data.width // mobile smartphone fix this.data.width -= this.data.padding*2 this.data.height -= this.data.padding*2 - this.cols = Math.floor(this.data.width/this.data.lineHeight*2) - this.rows = Math.floor( (this.data.height*0.93)/this.data.lineHeight) + this.cols = Math.floor(this.data.width/this.data.lineHeight*2)-1 + this.rows = Math.floor( (this.data.height*0.93)/this.data.lineHeight)-1 }, events:{ diff --git a/com/isoterminal/ISOTerminal.js b/com/isoterminal/ISOTerminal.js index 6a9553b..8e54cbb 100644 --- a/com/isoterminal/ISOTerminal.js +++ b/com/isoterminal/ISOTerminal.js @@ -34,11 +34,12 @@ ISOTerminal.addEventListener = (event,cb) => { } ISOTerminal.prototype.exec = function(shellscript){ - this.send(shellscript+"\n",1) + this.send(`printf "\n\r"; ${shellscript}\n`,1) } ISOTerminal.prototype.hook = function(hookname,args){ - this.exec(`{ type hook || source /etc/profile.sh; }; hook ${hookname} "${args.join('" "')}"`) + let cmd = `{ type hook || source /etc/profile.sh; }; hook ${hookname} "${args.join('" "')}"` + this.exec(cmd) } ISOTerminal.prototype.serial_input = 0; // can be set to 0,1,2,3 to define stdinput tty (xterm plugin) @@ -53,7 +54,7 @@ ISOTerminal.prototype.send = function(str, ttyNr){ this.convert.toUint8Array( str ).map( (c) => { this.preventFrameDrop( () => { - this.worker.postMessage({event:`serial${ttyNr}-input`,data:c}) + this.worker.postMessage({event:`serial${ttyNr}-input`,data:c}) } ) }) @@ -189,15 +190,14 @@ ISOTerminal.prototype.startVM = function(opts){ const empower = [ "FOSS gives users control over their software, offering freedom to modify and share", - "Feeling powerless with tech? FOSS escapes a mindset known as learned helplessness", + "Feeling powerless? FOSS escapes a mindset known as learned helplessness", "FOSS breaks this cycle by showing that anyone can learn and contribute", "Proprietary software can make users dependent, but FOSS offers real choices", "FOSS communities provide support and encourage users to develop new skills", - "Learned helplessness fades when we realize tech isn’t too complex to understand", "FOSS empowers users to customize and improve their tools", "Engaging with FOSS helps build confidence and self-reliance in tech", "FOSS shows that anyone can shape the digital world with curiosity and effort", - "Linux can revive old computers, extending their life and reducing e-waste", + "Linux can revive old computers, extending their life and reduces e-waste", "Many lightweight Linux distributions run smoothly on older hardware", "Installing Linux on aging devices keeps them functional instead of sending them to the landfill", "Linux uses fewer resources, making it ideal for reusing older machines", @@ -216,16 +216,17 @@ ISOTerminal.prototype.startVM = function(opts){ \r . . . / \\ | | \\/ \\ Y / . \r . . ./___/\\ \\ |____|_ /_______ /\\___|_ /. . \r . . . . . .\\_/. . . . \\/ . . . .\\/ . . _ \\/ . . -\r ▬▬▬▬▬▬▬▬ https://xrsh.isvery.ninja ▬▬▬▬▬▬▬▬▬▬▬▬ +\r ▬▬▬▬▬▬▬▬ https://xrsh.isvery.ninja ▬▬▬▬▬▬▬▬▬▬▬▬ \r local-first, polyglot, unixy WebXR IDE & runtime -\r +\r \r credits \r ------- \r @nlnet@nlnet.nl \r @lvk@mastodon.online -\r @utopiah@mastodon.pirateparty.be +\r @utopiah@mastodon.pirateparty.be  \r https://www.w3.org/TR/webxr -\r https://three.org +\r https://xrfragment.org +\r https://threejs.org \r https://aframe.org \r https://busybox.net \r https://buildroot.org @@ -233,7 +234,7 @@ ISOTerminal.prototype.startVM = function(opts){ const text_color = "\r" const text_reset = "\033[0m" - const loadmsg = "\n\r "+loading[ Math.floor(Math.random()*1000) % loading.length ] + "..[please wait]" + const loadmsg = "\n\r "+loading[ Math.floor(Math.random()*1000) % loading.length ] + "..please wait" const empowermsg = "\n\r "+text_reset+'"'+empower[ Math.floor(Math.random()*1000) % empower.length ] + '"\n\r' this.emit('status',loadmsg) this.emit('serial-output-string', motd + empowermsg + text_color + loadmsg + text_reset+"\n\r") diff --git a/com/isoterminal/feat/boot.js b/com/isoterminal/feat/boot.js index 769a732..b325f7c 100644 --- a/com/isoterminal/feat/boot.js +++ b/com/isoterminal/feat/boot.js @@ -14,7 +14,7 @@ ISOTerminal.prototype.boot = async function(e){ env.push( 'export '+String(i).toUpperCase()+'="'+decodeURIComponent( document.location[i]+'"') ) } } - this.worker.create_file("profile.browser", this.convert.toUint8Array( env.join('\n') ) ) + await this.worker.create_file("profile.browser", this.convert.toUint8Array( env.join('\n') ) ) if( this.serial_input == 0 ){ if( !this.noboot ){ diff --git a/com/isoterminal/feat/pastedrop.js b/com/isoterminal/feat/pastedrop.js index cb90b49..ca34864 100644 --- a/com/isoterminal/feat/pastedrop.js +++ b/com/isoterminal/feat/pastedrop.js @@ -17,7 +17,7 @@ if( typeof emulator != 'undefined' ){ ISOTerminal.prototype.pasteFile = async function(data){ const {type,item,pastedText} = data if( pastedText){ - this.pasteWriteFile( this.convert.toUint8Array(pastedText) ,type) + this.pasteWriteFile( this.convert.toUint8Array(pastedText) ,type, null, true) }else{ const file = item.getAsFile(); const reader = new FileReader(); @@ -29,4 +29,18 @@ if( typeof emulator != 'undefined' ){ } } + ISOTerminal.prototype.pasteInit = function(opts){ + // bind upload input + const {instance, aEntity} = opts + const el = aEntity.el.dom.querySelector('#pastedrop') // upload input + el.addEventListener('change', (e) => { + const file = el.files[0]; + const item = {...file, getAsFile: () => file } + this.el.emit('pasteFile', { item, type: file.type }); + }) + } + + ISOTerminal.addEventListener('init', function(){ + this.addEventListener('term_init', (opts) => this.pasteInit(opts.detail) ) + }) } diff --git a/com/isoterminal/feat/term.js b/com/isoterminal/feat/term.js new file mode 100644 index 0000000..db6e421 --- /dev/null +++ b/com/isoterminal/feat/term.js @@ -0,0 +1,88 @@ +ISOTerminal.addEventListener('init', function(){ + this.TermInit() +}) + +ISOTerminal.prototype.TermInit = function(){ + + const setupTerm = (opts) => { + if( !opts ) return + const {instance, aEntity} = opts + const el = aEntity.el.dom.querySelector('#term') + opts.termOpts = { + cols: aEntity.cols, + rows: aEntity.rows, + el_or_id: el, + scrollback: aEntity.rows*3, + fontSize: null // + //rainbow: [Term.COLOR_MAGENTA, Term.COLOR_CYAN ], + //xr: AFRAME.scenes[0].renderer.xr, + //map: { + // 'ArrowRight': { ch: false, ctrl: '\x1b\x66' }, // this triggers ash-shell forward-word + // 'ArrowLeft': { ch: false, ctrl: '\x1b\x62' } // backward-word + //} + } + + // patch Term-class + Term.prototype.move_textarea = function(){} /* *TODO* *FIXME* does not work in winbox */ + + Term.prototype.pasteHandler = function(original){ + return function (ev){ + original.apply(this,[ev]) + } + }( Term.prototype.pasteHandler ) + + Term.prototype.keyDownHandler = function(original){ + return function (e){ + if ((e.ctrlKey || e.metaKey) && e.key === 'v') { + return true; // bubble up to pasteHandler (see pastedrop.js) + } + original.apply(this,[e]) + } + }( Term.prototype.keyDownHandler ) + + Term.prototype.href = (a) => { + if( a.href ){ + this.exec(`source /etc/profile.sh; hook href "${a.href}"`) + } + return false + } + this.term = new Term( opts.termOpts ) + this.term.colors = [ + /* normal */ + "#000000", + "#2FA", + "#7700ff", + "#555555", + "#0000ff", + "#aa00aa", + "#ff00aa", + "#aaaaaa", + /* bright */ + "#555555", + "#ff5555", + "#2CF", + "#aa00ff", + "#5555ff", + "#ff55ff", + "#55ffff", + "#ffffff" + ]; + this.term.open(el) + this.term.el = el + + this.term.setKeyHandler( (ch) => this.send(ch) ) + aEntity.el.addEventListener('focus', () => el.querySelector("textarea").focus() ) + aEntity.el.addEventListener('serial-output-string', (e) => { + this.term.write(e.detail) + }) + //aEntity.term.emit('initTerm',this) + //aEntity.el.addEventListener('focus', () => this.vt100.focus() ) + + //aEntity.el.addEventListener('serial-output-string', (e) => { + // this.vt100.write(e.detail) + //}) + + } + + this.addEventListener('term_init', (opts) => setupTerm(opts.detail) ) +} diff --git a/com/isoterminal/term.js b/com/isoterminal/term.js new file mode 100644 index 0000000..4d5b7cc --- /dev/null +++ b/com/isoterminal/term.js @@ -0,0 +1,1460 @@ +/* + * Javascript terminal + * + * Copyright (c) 2011-2020 Fabrice Bellard + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +"use strict"; + +function Term(options) +{ + var width, height, tot_height, scrollback; + + function dummy_key_handler() + { + } + + width = options.cols ? options.cols : 80; + height = options.rows ? options.rows : 25; + scrollback = options.scrollback ? options.scrollback : 0; + this.font_size = options.fontSize ? options.fontSize : 15; + + this.w = width; + this.h = height; + this.cur_h = height; /* current height of the scroll back buffer */ + tot_height = height + scrollback; + this.tot_h = tot_height; /* maximum height of the scroll back buffer */ + + /* y_base and y_disp are index in the circular buffer lines of + length cur_h. They are defined modulo tot_h, i.e. they wrap + when cur_h = tot_h. If cur_h < tot_h, y_base is always equal to + cur_h - h. */ + this.y_base = 0; /* position of the current top screen line in the + scroll back buffer */ + this.y_disp = 0; /* position of the top displayed line in the + scroll back buffer */ + /* cursor position */ + this.x = 0; + this.y = 0; + this.scroll_top = 0; + this.scroll_bottom = this.h; + this.cursorstate = 0; + this.handler = dummy_key_handler; + this.state = 0; + this.output_queue = ""; + this.colors = [ + /* normal */ + "#000000", + "#aa0000", + "#00aa00", + "#aa5500", + "#0000aa", + "#aa00aa", + "#00aaaa", + "#aaaaaa", + /* bright */ + "#555555", + "#ff5555", + "#55ff55", + "#ffff55", + "#5555ff", + "#ff55ff", + "#55ffff", + "#ffffff" + ]; + /* attributes bits: + 0-3: bg + 4-7: fg + 8: bold + 9: inverse + */ + this.def_attr = (7 << 4) | 0; + this.cur_attr = this.def_attr; + this.is_mac = (navigator.userAgent.indexOf("Mac") >=0 ) ? true : false; + this.key_rep_state = 0; + this.key_rep_str = ""; + + this.utf8 = true; + this.utf8_state = 0; + this.utf8_val = 0; + + this.application_cursor = false; + this.application_keypad = false; + /* if true, emulate some behaviors of the Linux console */ + this.linux_console = true; + + this.textarea_has_focus = false; +} + +Term.prototype.setKeyHandler = function(handler) +{ + this.handler = handler; +} + +/* return the size of a character in CSS pixels using the selected font */ +function term_get_char_size(parent_el, font_size) +{ + var el, g, ret; + el = document.createElement("div"); + el.classList.add("term", "term_char_size"); + el.style.fontSize = font_size + "px"; + el.textContent = "W"; + parent_el.appendChild(el); + g = el.getBoundingClientRect(); + /* the character width & height may not be an integer */ + ret = [g.width, g.height]; + return ret; +} + +Term.prototype.open = function(parent_el) +{ + var y, line, i, term, c, row_el, char_size_ret; + + /* set initial content */ + this.lines = new Array(); + c = 32 | (this.def_attr << 16); + for(y = 0; y < this.cur_h;y++) { + line = new Array(); + for(i=0;i= 0; + } + + function right_trim(str, a) + { + var i, n; + n = a.length; + i = str.length; + while (i >= n && str.substr(i - n, n) == a) + i -= n; + return str.substr(0, i); + } + + for(y = ymin; y <= ymax; y++) { + /* convert to HTML string */ + y1 = y + this.y_disp; + if (y1 >= this.cur_h) + y1 -= this.cur_h; + line = this.lines[y1]; + outline = ""; + w = this.w; + if (y == this.y && this.cursor_state && + this.y_disp == this.y_base) { + cx = this.x; + } else { + cx = -1; + } + last_attr = this.def_attr; + http_link_len = 0; + for(i = 0; i < w; i++) { + c = line[i]; + attr = c >> 16; + c &= 0xffff; + /* test for http link */ + if (c == 0x68 && (w - i) >= 8 && http_link_len == 0) { + /* test http:// or https:// */ + if ((line[i + 1] & 0xffff) == 0x74 && + (line[i + 2] & 0xffff) == 0x74 && + (line[i + 3] & 0xffff) == 0x70 && + (((line[i + 4] & 0xffff) == 0x3a && + (line[i + 5] & 0xffff) == 0x2f && + (line[i + 6] & 0xffff) == 0x2f) || + ((line[i + 4] & 0xffff) == 0x73 && + (line[i + 5] & 0xffff) == 0x3a && + (line[i + 6] & 0xffff) == 0x2f && + (line[i + 7] & 0xffff) == 0x2f))) { + http_link_str = ""; + j = 0; + while ((i + j) < w && + is_http_link_char(line[i + j] & 0xffff)) { + http_link_str += String.fromCharCode(line[i + j] & 0xffff); + j++; + } + http_link_len = j; + if (last_attr != this.def_attr) { + outline += ''; + last_attr = this.def_attr; + } + outline += ""; + } + } + if (i == cx) { + attr = -1; /* cursor */ + } + if (attr != last_attr) { + if (last_attr != this.def_attr) + outline += ''; + if (attr != this.def_attr) { + if (attr == -1) { + /* cursor */ + outline += ''; + } else { + outline += ''; + } + } + } + switch(c) { + case 32: + outline += " "; + break; + case 38: // '&' + outline += "&"; + break; + case 60: // '<' + outline += "<"; + break; + case 62: // '>' + outline += ">"; + break; + default: + if (c < 32) { + outline += " "; + } else { + outline += String.fromCharCode(c); + } + break; + } + last_attr = attr; + if (http_link_len != 0) { + http_link_len--; + if (http_link_len == 0) { + if (last_attr != this.def_attr) { + outline += ''; + last_attr = this.def_attr; + } + outline += ""; + } + } + } + if (last_attr != this.def_attr) { + outline += ''; + } + + /* trim trailing spaces for copy/paste */ + outline = right_trim(outline, " "); + if (outline == "") + outline = " "; + + this.rows_el[y].innerHTML = outline; + } + + this.refresh_scrollbar(); + this.move_textarea(); +}; + +Term.prototype.cursor_timer_cb = function() +{ + this.cursor_state ^= 1; + this.refresh(this.y, this.y); +}; + +Term.prototype.show_cursor = function() +{ + if (!this.cursor_state) { + this.cursor_state = 1; + this.refresh(this.y, this.y); + } +}; + +/* scroll down or up in the scroll back buffer by n lines */ +Term.prototype.scroll_disp = function(n) +{ + var i, y1; + /* slow but it does not really matters */ + if (n >= 0) { + for(i = 0; i < n; i++) { + if (this.y_disp == this.y_base) + break; + if (++this.y_disp == this.cur_h) + this.y_disp = 0; + } + } else { + n = -n; + y1 = this.y_base + this.h; + if (y1 >= this.cur_h) + y1 -= this.cur_h; + for(i = 0; i < n; i++) { + if (this.y_disp == y1) + break; + if (--this.y_disp < 0) + this.y_disp = this.cur_h - 1; + } + } + this.refresh(0, this.h - 1); +}; + +Term.prototype.write = function(str) +{ + var s, ymin, ymax; + + function update(y) + { + ymin = Math.min(ymin, y); + ymax = Math.max(ymax, y); + } + + function get_erase_char() + { + var bg_mask, attr; + bg_mask = 0xf; + attr = (s.def_attr & ~bg_mask) | (s.cur_attr & bg_mask); + return 32 | (attr << 16); + } + + function erase_chars(x1, x2, y) { + var l, i, c, y1; + y1 = s.y_base + y; + if (y1 >= s.cur_h) + y1 -= s.cur_h; + l = s.lines[y1]; + c = get_erase_char(); + for(i = x1; i < x2; i++) + l[i] = c; + update(y); + } + + function erase_to_eol(x, y) { + erase_chars(x, s.w, y); + } + + function erase_in_line(n) { + switch(n) { + case 0: + erase_to_eol(s.x, s.y); + break; + case 1: + erase_chars(0, s.x + 1, s.y); + break; + case 2: + erase_chars(0, s.w, s.y); + break; + } + } + + function erase_in_display(n) { + var y; + switch(n) { + case 0: + erase_to_eol(s.x, s.y); + for(y = s.y + 1; y < s.h; y++) + erase_to_eol(0, y); + break; + case 1: + erase_chars(0, s.x + 1, s.y); + for(y = 0; y < s.y; y++) { + erase_to_eol(0, y); + } + break; + case 2: + for(y = 0; y < s.h; y++) { + erase_to_eol(0, y); + } + break; + } + } + + + function delete_chars(n) + { + var l, i, c, y1, j; + y1 = s.y + s.y_base; + if (y1 >= s.cur_h) + y1 -= s.cur_h; + l = s.lines[y1]; + if (n < 1) + n = 1; + c = get_erase_char(); + j = s.x + n; + for(i = s.x; i < s.w; i++) { + if (j < s.w) + l[i] = l[j]; + else + l[i] = c; + j++; + } + update(s.y); + } + + function insert_chars(n) + { + var l, i, c, y1, x1; + if (n < 1) + n = 1; + if (n > s.w - s.x) + n = s.w - s.x; + y1 = s.y + s.y_base; + if (y1 >= s.cur_h) + y1 -= s.cur_h; + l = s.lines[y1]; + x1 = s.x + n; + for(i = s.w - 1; i >= x1; i--) + l[i] = l[i - n]; + c = get_erase_char(); + for(i = s.x; i < x1; i++) + l[i] = c; + update(s.y); + } + + function csi_colors(esc_params) + { + var j, n, fg, bg, mask; + + if (esc_params.length == 0) { + s.cur_attr= s.def_attr; + } else { + for(j = 0; j < esc_params.length; j++) { + n = esc_params[j]; + if (n >= 30 && n <= 37) { + /* foreground */ + fg = n - 30; + s.cur_attr = (s.cur_attr & ~(0xf << 4)) | (fg << 4); + } else if (n >= 40 && n <= 47) { + /* background */ + bg = n - 40; + s.cur_attr = (s.cur_attr & ~0xf) | bg; + } else if (n >= 90 && n <= 97) { + /* bright foreground */ + fg = n - 90 + 8; + s.cur_attr = (s.cur_attr & ~(0xf << 4)) | (fg << 4); + } else if (n >= 100 && n <= 107) { + /* bright background */ + bg = n - 100 + 8; + s.cur_attr = (s.cur_attr & ~0xf) | bg; + } else if (n == 1) { + /* bold + bright */ + s.cur_attr |= (1 << 8); + } else if (n == 0) { + /* default attr */ + s.cur_attr = s.def_attr; + } else if (n == 7) { + /* inverse */ + s.cur_attr |= (1 << 9); + } else if (n == 27) { + /* not inverse */ + s.cur_attr &= ~(1 << 9); + } else if (n == 39) { + /* reset fg */ + mask = 0x0f << 4; + s.cur_attr = (s.cur_attr & ~mask) | (s.def_attr & mask); + } else if (n == 49) { + /* reset bg */ + mask = 0x0f; + s.cur_attr = (s.cur_attr & ~mask) | (s.def_attr & mask); + } + } + } + } + + function empty_line(y, use_erase_char) { + var line, c, y1, x; + if (use_erase_char) + c = get_erase_char(); + else + c = 32 | (s.def_attr << 16); + line = new Array(); + for(x=0;x= s.cur_h) + y1 -= s.cur_h; + s.lines[y1] = line; + } + + function scroll_down(top, bottom, use_erase_char) + { + var y, line, y1, y2; + + if (top == 0 && bottom == s.h) { + /* increase height of buffer if possible */ + if (s.cur_h < s.tot_h) { + s.cur_h++; + } + /* move down one line */ + if (++s.y_base == s.cur_h) + s.y_base = 0; + s.y_disp = s.y_base; + } else { + /* partial scroll */ + for(y = top; y < bottom - 1; y++) { + y1 = s.y_base + y; + if (y1 >= s.cur_h) + y1 -= s.cur_h; + y2 = y1 + 1; + if (y2 >= s.cur_h) + y2 -= s.cur_h; + s.lines[y1] = s.lines[y2]; + } + } + empty_line(bottom - 1, use_erase_char); + update(top); + update(bottom - 1); + } + + function scroll_up(top, bottom, use_erase_char) { + var y, y1, y2; + /* XXX: could scroll in the history */ + for(y = bottom - 1; y > top; y--) { + y1 = s.y_base + y; + if (y1 >= s.cur_h) + y1 -= s.cur_h; + y2 = y1 - 1; + if (y2 >= s.cur_h) + y2 -= s.cur_h; + s.lines[y1] = s.lines[y2]; + } + empty_line(top, use_erase_char); + update(top); + update(bottom - 1); + } + + function down_with_scroll() { + s.y++; + if (s.y == s.scroll_bottom) { + s.y--; + scroll_down(s.scroll_top, s.scroll_bottom, false); + } else if (s.y >= s.h) { + s.y--; + scroll_down(0, s.h, false); + } + } + + function up_with_scroll() { + if (s.y == s.scroll_top) { + scroll_up(s.scroll_top, s.scroll_bottom, true); + } else if (s.y == 0) { + scroll_up(0, s.h, true); + } else { + s.y--; + } + } + + function insert_lines(n) { + var y2; + if (n < 1) + n = 1; + if (s.y < s.scroll_bottom) + y2 = s.scroll_bottom; + else + y2 = s.h; + while (n != 0) { + scroll_up(s.y, y2, true); + n--; + } + } + + function delete_lines(n) { + var y2; + if (n < 1) + n = 1; + if (s.y < s.scroll_bottom) + y2 = s.scroll_bottom; + else + y2 = s.h; + while (n != 0) { + scroll_down(s.y, y2, true); + n--; + } + } + + var TTY_STATE_NORM = 0; + var TTY_STATE_ESC = 1; + var TTY_STATE_CSI = 2; + var TTY_STATE_CHARSET = 3; + + function handle_char(c) { + var i, l, n, j, y1, y2, x1; + + switch(s.state) { + case TTY_STATE_NORM: + switch(c) { + case 10: + down_with_scroll(); + break; + case 13: + s.x = 0; + break; + case 8: + if (s.x > 0) { + s.x--; + } + break; + case 9: /* tab */ + n = (s.x + 8) & ~7; + if (n <= s.w) { + s.x = n; + } + break; + case 27: + s.state = TTY_STATE_ESC; + break; + default: + if (c >= 32) { + if (s.x >= s.w) { + s.x = 0; + down_with_scroll(); + } + y1 = s.y + s.y_base; + if (y1 >= s.cur_h) + y1 -= s.cur_h; + s.lines[y1][s.x] = (c & 0xffff) | + (s.cur_attr << 16); + s.x++; + update(s.y); + } + break; + } + break; + case TTY_STATE_ESC: + switch(c) { + case 91: // '[' + s.esc_params = new Array(); + s.cur_param = 0; + s.esc_prefix = 0; + s.state = TTY_STATE_CSI; + break; + case 40: // '(' + case 41: // ')' + s.state = TTY_STATE_CHARSET; + break; + case 61: // '=' + s.application_keypad = true; + s.state = TTY_STATE_NORM; + break; + case 62: // '>' + s.application_keypad = false; + s.state = TTY_STATE_NORM; + break; + case 77: // 'M' + up_with_scroll(); + s.state = TTY_STATE_NORM; + break; + default: + s.state = TTY_STATE_NORM; + break; + } + break; + case TTY_STATE_CSI: + if (c >= 48 && c <= 57) { // '0' '9' + /* numeric */ + s.cur_param = s.cur_param * 10 + c - 48; + } else { + if (c == 63) { // '?' + s.esc_prefix = c; + break; + } + /* add parsed parameter */ + s.esc_params[s.esc_params.length] = s.cur_param; + s.cur_param = 0; + if (c == 59) // ; + break; + s.state = TTY_STATE_NORM; + + // console.log("term: csi=" + s.esc_params + " cmd="+c); + switch(c) { + case 64: // '@' insert chars + insert_chars(s.esc_params[0]); + break; + case 65: // 'A' up + n = s.esc_params[0]; + if (n < 1) + n = 1; + s.y -= n; + if (s.y < 0) + s.y = 0; + break; + case 66: // 'B' down + n = s.esc_params[0]; + if (n < 1) + n = 1; + s.y += n; + if (s.y >= s.h) + s.y = s.h - 1; + break; + case 67: // 'C' right + n = s.esc_params[0]; + if (n < 1) + n = 1; + s.x += n; + if (s.x >= s.w - 1) + s.x = s.w - 1; + break; + case 68: // 'D' left + n = s.esc_params[0]; + if (n < 1) + n = 1; + s.x -= n; + if (s.x < 0) + s.x = 0; + break; + case 71: /* 'G' cursor character absolute */ + x1 = s.esc_params[0] - 1; + if (x1 < 0) + x1 = 0; + else if (x1 >= s.w) + x1 = s.w - 1; + s.x = x1; + break; + case 72: // 'H' goto xy + y1 = s.esc_params[0] - 1; + if (s.esc_params.length >= 2) + x1 = s.esc_params[1] - 1; + else + x1 = 0; + if (y1 < 0) + y1 = 0; + else if (y1 >= s.h) + y1 = s.h - 1; + if (x1 < 0) + x1 = 0; + else if (x1 >= s.w) + x1 = s.w - 1; + s.x = x1; + s.y = y1; + break; + case 74: // 'J' erase in display + erase_in_display(s.esc_params[0]); + break; + case 75: // 'K' erase in line + erase_in_line(s.esc_params[0]); + break; + case 76: // 'L' insert lines + insert_lines(s.esc_params[0]); + break; + case 77: // 'M' insert lines + delete_lines(s.esc_params[0]); + break; + case 80: // 'P' + delete_chars(s.esc_params[0]); + break; + case 100: // 'd' line position absolute + { + y1 = s.esc_params[0] - 1; + if (y1 < 0) + y1 = 0; + else if (y1 >= s.h) + y1 = s.h - 1; + s.y = y1; + } + break; + case 104: // 'h': set mode + if (s.esc_prefix == 63 && s.esc_params[0] == 1) { + s.application_cursor = true; + } + break; + case 108: // 'l': reset mode + if (s.esc_prefix == 63 && s.esc_params[0] == 1) { + s.application_cursor = false; + } + break; + case 109: // 'm': set color + csi_colors(s.esc_params); + break; + case 110: // 'n' return the cursor position + s.queue_chars("\x1b[" + (s.y + 1) + ";" + (s.x + 1) + "R"); + break; + case 114: // 'r' set scroll region + y1 = s.esc_params[0] - 1; + if (y1 < 0) + y1 = 0; + else if (y1 >= s.h) + y1 = s.h - 1; + if (s.esc_params.length >= 2) + y2 = s.esc_params[1]; + else + y2 = s.h; + if (y2 >= s.h || y2 <= y1) + y2 = s.h; + s.scroll_top = y1; + s.scroll_bottom = y2; + s.x = 0; + s.y = 0; + break; + default: + break; + } + } + break; + case TTY_STATE_CHARSET: + /* just ignore */ + s.state = TTY_STATE_NORM; + break; + } + } + + function handle_utf8(c) { + if (s.utf8_state !== 0 && (c >= 0x80 && c < 0xc0)) { + s.utf8_val = (s.utf8_val << 6) | (c & 0x3F); + s.utf8_state--; + if (s.utf8_state === 0) { + handle_char(s.utf8_val); + } + } else if (c >= 0xc0 && c < 0xf8) { + s.utf8_state = 1 + (c >= 0xe0) + (c >= 0xf0); + s.utf8_val = c & ((1 << (6 - s.utf8_state)) - 1); + } else { + s.utf8_state = 0; + handle_char(c); + } + } + + var i, c, utf8; + + /* update region is in ymin ymax */ + s = this; + ymin = s.h; + ymax = -1; + update(s.y); // remove the cursor + /* reset top of displayed screen to top of real screen */ + if (s.y_base != s.y_disp) { + s.y_disp = s.y_base; + /* force redraw */ + ymin = 0; + ymax = s.h - 1; + } + utf8 = s.utf8; + for(i = 0; i < str.length; i++) { + c = str.charCodeAt(i); + if (utf8) + handle_utf8(c); + else + handle_char(c); + } + update(s.y); // show the cursor + + if (ymax >= ymin) + s.refresh(ymin, ymax); +}; + +Term.prototype.writeln = function (str) +{ + this.write(str + '\r\n'); +}; + +Term.prototype.interceptBrowserExit = function (ev) +{ + /* At least avoid exiting the navigator if Ctrl-Q or Ctrl-W are + * pressed */ + if (ev.ctrlKey) { + window.onbeforeunload = function() { + window.onbeforeunload = null; + return "CTRL-W or Ctrl-Q cannot be sent to the emulator."; + }; + } else { + window.onbeforeunload = null; + } +} + +Term.prototype.keyDownHandler = function (ev) +{ + var str; + + this.interceptBrowserExit(ev); + + str=""; + switch(ev.keyCode) { + case 8: /* backspace */ + str = "\x7f"; + break; + case 9: /* tab */ + str = "\x09"; + break; + case 13: /* enter */ + str = "\x0d"; + break; + case 27: /* escape */ + str = "\x1b"; + break; + case 37: /* left */ + if (ev.ctrlKey) { + str = "\x1b[1;5D"; + } else if (this.application_cursor) { + str = "\x1bOD"; + } else { + str = "\x1b[D"; + } + break; + case 39: /* right */ + if (ev.ctrlKey) { + str = "\x1b[1;5C"; + } else if (this.application_cursor) { + str = "\x1bOC"; + } else { + str = "\x1b[C"; + } + break; + case 38: /* up */ + if (ev.ctrlKey) { + this.scroll_disp(-1); + } else if (this.application_cursor) { + str = "\x1bOA"; + } else { + str = "\x1b[A"; + } + break; + case 40: /* down */ + if (ev.ctrlKey) { + this.scroll_disp(1); + } else if (this.application_cursor) { + str = "\x1bOB"; + } else { + str = "\x1b[B"; + } + break; + case 46: /* delete */ + str = "\x1b[3~"; + break; + case 45: /* insert */ + str = "\x1b[2~"; + break; + case 36: /* home */ + if (this.linux_console) + str = "\x1b[1~"; + else if (this.application_keypad) + str = "\x1bOH"; + else + str = "\x1b[H"; + break; + case 35: /* end */ + if (this.linux_console) + str = "\x1b[4~"; + else if (this.application_keypad) + str = "\x1bOF"; + else + str = "\x1b[F"; + break; + case 33: /* page up */ + if (ev.ctrlKey) { + this.scroll_disp(-(this.h - 1)); + } else { + str = "\x1b[5~"; + } + break; + case 34: /* page down */ + if (ev.ctrlKey) { + this.scroll_disp(this.h - 1); + } else { + str = "\x1b[6~"; + } + break; + default: + if (ev.ctrlKey) { + /* ctrl + key */ + if (ev.keyCode >= 65 && ev.keyCode <= 90) { + str = String.fromCharCode(ev.keyCode - 64); + } else if (ev.keyCode == 32) { + str = String.fromCharCode(0); + } + } else if ((!this.is_mac && ev.altKey) || + (this.is_mac && ev.metaKey)) { + /* meta + key (Note: we only send lower case) */ + if (ev.keyCode >= 65 && ev.keyCode <= 90) { + str = "\x1b" + String.fromCharCode(ev.keyCode + 32); + } + } + break; + } + // console.log("keydown: keycode=" + ev.keyCode + " charcode=" + ev.charCode + " str=" + str + " ctrl=" + ev.ctrlKey + " alt=" + ev.altKey + " meta=" + ev.metaKey); + if (str) { + if (ev.stopPropagation) + ev.stopPropagation(); + if (ev.preventDefault) + ev.preventDefault(); + + this.show_cursor(); + this.key_rep_state = 1; + this.key_rep_str = str; + this.handler(str); + return false; + } else { + this.key_rep_state = 0; + return true; + } +}; + +Term.prototype.keyUpHandler = function (ev) +{ + this.interceptBrowserExit(ev); +}; + +Term.prototype.to_utf8 = function(s) +{ + var i, n = s.length, r, c; + r = ""; + for(i = 0; i < n; i++) { + c = s.charCodeAt(i); + if (c < 0x80) { + r += String.fromCharCode(c); + } else if (c < 0x800) { + r += String.fromCharCode((c >> 6) | 0xc0, (c & 0x3f) | 0x80); + } else if (c < 0x10000) { + r += String.fromCharCode((c >> 12) | 0xe0, + ((c >> 6) & 0x3f) | 0x80, + (c & 0x3f) | 0x80); + } else { + r += String.fromCharCode((c >> 18) | 0xf0, + ((c >> 12) & 0x3f) | 0x80, + ((c >> 6) & 0x3f) | 0x80, + (c & 0x3f) | 0x80); + } + } + return r; +} + +Term.prototype.inputHandler = function (ev) +{ + var str; + str = this.textarea_el.value; + if (str) { + this.textarea_el.value = ""; + this.show_cursor(); + if (this.utf8) + str = this.to_utf8(str); + this.handler(str); + return false; + } else { + return true; + } +}; + +Term.prototype.termKeyDownHandler = function(ev) +{ + this.interceptBrowserExit(ev); + /* give the focus back to the textarea when a key is pressed */ + this.textarea_el.focus(); +} + +Term.prototype.termMouseUpHandler = function(ev) +{ + var sel; + /* if no selection, can switch back up to the textarea focus */ + sel = window.getSelection(); + if (!sel || sel.isCollapsed) + this.textarea_el.focus(); +} + +Term.prototype.focusHandler = function (ev) +{ + this.textarea_has_focus = true; +}; + +Term.prototype.blurHandler = function (ev) +{ + /* allow unloading the page */ + window.onbeforeunload = null; + this.textarea_has_focus = false; +}; + +Term.prototype.pasteHandler = function (ev) +{ + var c, str; + if (!this.textarea_has_focus) { + c = ev.clipboardData; + if (c) { + str = c.getData("text/plain"); + if (this.utf8) + str = this.to_utf8(str); + this.queue_chars(str); + return false; + } + } +} + +Term.prototype.wheelHandler = function (ev) +{ + if (ev.deltaY < 0) + this.scroll_disp(-3); + else if (ev.deltaY > 0) + this.scroll_disp(3); + ev.stopPropagation(); +} + +Term.prototype.mouseDownHandler = function (ev) +{ + this.thumb_el.onmouseup = this.mouseUpHandler.bind(this); + document.onmousemove = this.mouseMoveHandler.bind(this); + document.onmouseup = this.mouseUpHandler.bind(this); + + /* disable potential selection */ + document.body.className += " noSelect"; + + this.mouseMoveHandler(ev); +} + +Term.prototype.mouseMoveHandler = function (ev) +{ + var total_size, pos, new_y_disp, y, y0; + total_size = this.term_el.clientHeight; + y = ev.clientY - this.track_el.getBoundingClientRect().top; + pos = Math.floor((y - (this.thumb_size / 2)) * this.cur_h / total_size); + new_y_disp = Math.min(Math.max(pos, 0), this.cur_h - this.h); + /* position of the first line of the scroll back buffer */ + y0 = (this.y_base + this.h) % this.cur_h; + new_y_disp += y0; + if (new_y_disp >= this.cur_h) + new_y_disp -= this.cur_h; + if (new_y_disp != this.y_disp) { + this.y_disp = new_y_disp; + this.refresh(0, this.h - 1); + } +} + +Term.prototype.mouseUpHandler = function (ev) +{ + this.thumb_el.onmouseup = null; + document.onmouseup = null; + document.onmousemove = null; + document.body.className = document.body.className.replace(" noSelect", ""); +} + +/* output queue to send back asynchronous responses */ +Term.prototype.queue_chars = function (str) +{ + this.output_queue += str; + if (this.output_queue) + setTimeout(this.outputHandler.bind(this), 0); +}; + +Term.prototype.outputHandler = function () +{ + if (this.output_queue) { + this.handler(this.output_queue); + this.output_queue = ""; + } +}; + +Term.prototype.getSize = function () +{ + return [this.w, this.h]; +}; + +/* resize the terminal (size in pixels). Return true if the display + size was modified. */ +/* XXX: may be simpler to separate the scrollback buffer from the + screen buffer */ +Term.prototype.resizePixel = function (new_width, new_height) +{ + var new_w, new_h, y, x, line, c, row_el, d, new_cur_h, e; + + if (new_width == this.term_width && new_height == this.term_height) + return false; + new_w = Math.floor((new_width - this.scrollbar_width) / + this.char_width); + new_h = Math.floor(new_height / this.char_height); + if (new_w <= 0 || new_h <= 0 || new_h > this.tot_h) + return false; + + this.term_width = new_width; + this.term_height = new_height; + this.term_el.style.width = this.term_width + "px"; + this.term_el.style.height = this.term_height + "px"; + + /* XXX: could keep the EOL positions */ + if (new_w < this.w) { + /* reduce the line width */ + for(y = 0; y < this.cur_h;y++) { + line = this.lines[y]; + line = line.slice(0, new_w); + } + } else if (new_w > this.w) { + /* increase the line width */ + c = 32 | (this.def_attr << 16); + for(y = 0; y < this.cur_h;y++) { + line = this.lines[y]; + for(x = this.w; x < new_w; x++) + line[x] = c; + } + } + + if (this.x >= new_w) + this.x = new_w - 1; + + d = new_h - this.h; + if (d < 0) { + d = -d; + /* remove displayed lines */ + + /* strip the DOM terminal content */ + for(y = new_h; y < this.h; y++) { + row_el = this.rows_el[y]; + this.content_el.removeChild(row_el); + } + this.rows_el = this.rows_el.slice(0, new_h); + + /* adjust cursor position if needed */ + if (this.y >= new_h) { + if (d > this.y) + d = this.y; + this.y -= d; + this.y_base += d; + if (this.y_base >= this.tot_h) + this.y_base -= this.tot_h; + } + + if (this.scroll_bottom > new_h) + this.scroll_bottom = new_h; + /* fail safe for scroll top */ + if (this.scroll_top >= this.scroll_bottom) + this.scroll_top = 0; + + } else if (d > 0) { + /* add displayed lines */ + + if (this.cur_h == this.tot_h) { + if (d > this.tot_h - this.h) + d = this.tot_h - this.h; + } else { + if (d > this.y_base) + d = this.y_base; + } + this.y_base -= d; + if (this.y_base < 0) + this.y_base += this.tot_h; + this.y += d; + + if (this.scroll_bottom == this.h) + this.scroll_bottom = new_h; + + /* extend the DOM terminal content */ + for(y = this.h; y < new_h; y++) { + row_el = document.createElement("div"); + this.rows_el.push(row_el); + this.content_el.appendChild(row_el); + } + } + + if (this.cur_h < this.tot_h) { + new_cur_h = this.y_base + new_h; + if (new_cur_h < this.cur_h) { + /* remove lines in the scroll back buffer */ + this.lines = this.lines.slice(0, new_cur_h); + } else if (new_cur_h > this.cur_h) { + /* add lines in the scroll back buffer */ + c = 32 | (this.def_attr << 16); + for(y = this.cur_h; y < new_cur_h; y++) { + line = new Array(); + for(x = 0; x < new_w; x++) + line[x] = c; + this.lines[y] = line; + } + } + this.cur_h = new_cur_h; + } + + this.w = new_w; + this.h = new_h; + + if (this.y >= this.h) + this.y = this.h - 1; + + /* reset display position */ + this.y_disp = this.y_base; +/* + console.log("lines.length", this.lines.length, "cur_h", this.cur_h, + "y_base", this.y_base, "h", this.h, + "scroll_bottom", this.scroll_bottom); +*/ + this.refresh(0, this.h - 1); + return true; +}