diff --git a/com/codemirror.js b/com/codemirror.js index 11dc5cf..84ca833 100644 --- a/com/codemirror.js +++ b/com/codemirror.js @@ -36,7 +36,7 @@ AFRAME.registerComponent('codemirror', { css: (me) => `.CodeMirror{ width: ${me.com.data.width}px !important; - height: ${me.com.data.height}px !important; + height: ${me.com.data.height-30}px !important; } .codemirror *{ font-size: 14px; @@ -45,7 +45,10 @@ AFRAME.registerComponent('codemirror', { letter-spacing: 0 !important; text-shadow: 0px 0px 10px #F075; } - #${me.dom.id} .wb-body { overflow:hidden; } + + .wb-body:has(> .codemirror){ + overflow:hidden; + } .CodeMirror { margin-top:18px; @@ -78,11 +81,30 @@ AFRAME.registerComponent('codemirror', { this.editor.updateFile( this.data.file, instance.getValue() ) }) + this + .handleFocus() + setTimeout( () => { this.el.setAttribute("html-as-texture-in-xr", `domid: #${this.el.dom.id}`) // only show aframe-html in xr },1500) }, + handleFocus: function(){ + const focus = (showdom) => (e) => { + if( this.editor ){ + this.editor.focus() + } + if( this.el.components.window && this.data.renderer == 'canvas'){ + this.el.components.window.show( showdom ) + } + } + this.el.addEventListener('obbcollisionstarted', focus(false) ) + this.el.sceneEl.addEventListener('enter-vr', focus(false) ) + this.el.sceneEl.addEventListener('enter-ar', focus(false) ) + this.el.sceneEl.addEventListener('exit-vr', focus(true) ) + this.el.sceneEl.addEventListener('exit-ar', focus(true) ) + }, + updateFile: async function(file,str){ // we don't do via shellcmd: isoterminal.exec(`echo '${str}' > ${file}`,1) // as it would require all kindof ugly stringescaping diff --git a/com/html-as-texture-in-xr.js b/com/html-as-texture-in-xr.js index 34b96fc..abf0766 100644 --- a/com/html-as-texture-in-xr.js +++ b/com/html-as-texture-in-xr.js @@ -21,7 +21,7 @@ if( !AFRAME.components['html-as-texture-in-xr'] ){ this.el.setAttribute("html",`html: ${this.data.domid}; cursor:#cursor; xrlayer: true`) this.el.setAttribute("visible", AFRAME.utils.XD() == '3D' ? 'true' : 'false' ) if( this.data.faceuser ){ - this.el.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.5) ) + this.el.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.8) ) } }, diff --git a/com/isoterminal.js b/com/isoterminal.js index 552cde1..2e4ca41 100644 --- a/com/isoterminal.js +++ b/com/isoterminal.js @@ -37,6 +37,7 @@ if( typeof AFRAME != 'undefined '){ overlayfs: { type:"string"}, width: { type: 'number',"default": -1 }, height: { type: 'number',"default": -1 }, + depth: { type: 'number',"default": 0.03 }, lineHeight: { type: 'number',"default": 18 }, padding: { type: 'number',"default": 18 }, minimized: { type: 'boolean',"default":false}, @@ -44,19 +45,18 @@ if( typeof AFRAME != 'undefined '){ muteUntilPrompt:{ type: 'boolean',"default":true}, // mute stdout until a prompt is detected in ISO HUD: { type: 'boolean',"default":false}, // link to camera movement transparent: { type:'boolean', "default":false }, // need good gpu - memory: { type: 'number', "default":64 }, // VM memory (in MB) - bufferLatency: { type: 'number', "default":30 }, // in ms: bufferlatency from webworker to xterm (batch-update every char to texture) + memory: { type: 'number', "default":40 }, // 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) debug: { type: 'boolean', "default":false } }, init: function(){ this.el.object3D.visible = false - if( this.data.width == -1 ) this.data.width = document.body.offsetWidth - if( this.data.height == -1 ) this.data.height = document.body.offsetHeight - this.data.width -= this.data.padding*2 - this.data.height -= this.data.padding*2 + this.calculateDimension() this.initHud() + this.setupBox() + fetch(this.data.iso,{method: 'HEAD'}) .then( (res) => { if( res.status != 200 ) throw 'not found' @@ -79,17 +79,17 @@ if( typeof AFRAME != 'undefined '){ selfcontain: "com/selfcontainer.js", // html to texture htmlinxr: "com/html-as-texture-in-xr.js", - // isoterminal features + // isoterminal global features PromiseWorker: "com/isoterminal/PromiseWorker.js", ISOTerminal: "com/isoterminal/ISOTerminal.js", - localforage: "https://cdn.rawgit.com/mozilla/localForage/master/dist/localforage.js" + localforage: "com/isoterminal/localforage.js", }, dom: { - scale: 1.0, + scale: 0.66, events: ['click','keydown'], html: (me) => `
-
+

                           
`, @@ -105,7 +105,7 @@ if( typeof AFRAME != 'undefined '){ position:relative; line-height: ${me.com.data.lineHeight}px; } - #vt100 { + #term { outline: none !important; } @font-face { @@ -127,10 +127,6 @@ if( typeof AFRAME != 'undefined '){ border:none; padding:none; } - span blink:last-of-type{ - border-right: 8px solid #F07; - padding-right: 3px; - } #overlay .winbox:has(> .isoterminal){ background:transparent; @@ -218,8 +214,10 @@ if( typeof AFRAME != 'undefined '){ this.term = new ISOTerminal(instance,this.data) instance.addEventListener('DOMready', () => { - instance.setAttribute("html-as-texture-in-xr", `domid: #${this.el.dom.id}; faceuser: true`) - setTimeout( () => this.setupVT100(instance),100) + this.setupVT100(instance) + setTimeout( () => { + instance.setAttribute("html-as-texture-in-xr", `domid: #term; faceuser: true`) + },100) //instance.winbox.resize(720,380) let size = `width: ${this.data.width}; height: ${this.data.height}` instance.setAttribute("window", `title: xrsh.iso; uid: ${instance.uid}; attach: #overlay; dom: #${instance.dom.id}; ${size}; min: ${this.data.minimized}; max: ${this.data.maximized}`) @@ -231,6 +229,8 @@ if( typeof AFRAME != 'undefined '){ // run iso let opts = {dom:instance.dom} for( let i in this.data ) opts[i] = this.data[i] + opts.cols = this.cols + opts.rows = this.rows this.term.start(opts) }) @@ -262,6 +262,7 @@ if( typeof AFRAME != 'undefined '){ if( this.el.components.window && this.data.renderer == 'canvas'){ this.el.components.window.show( showdom ) } + this.el.emit('focus',e.detail) } this.el.addEventListener('obbcollisionstarted', focus(false) ) @@ -290,17 +291,20 @@ if( typeof AFRAME != 'undefined '){ }, setupVT100: function(instance){ - const el = this.el.dom.querySelector('#vt100') - this.vt100 = new VT100( - Math.floor(this.data.width/this.data.lineHeight), - Math.floor(this.data.height*0.8/this.data.lineHeight), - el, - 100 - ) + const el = this.el.dom.querySelector('#term') + const opts = { + cols: this.cols, + rows: this.rows, + el_or_id: el, + max_scroll_lines: 100, + nodim: true + } + this.vt100 = new VT100( opts ) + this.vt100.el = el this.vt100.curs_set( 1, true) el.focus() + this.el.addEventListener('focus', () => el.focus()) this.vt100.getch( (ch,t) => { - console.log(ch) this.term.send( ch ) this.vt100.curs_set( 0, true) }) @@ -323,6 +327,27 @@ if( typeof AFRAME != 'undefined '){ //}) }, + 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 = document.body.offsetHeight + 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*1.9) + this.rows = Math.floor(this.data.height*0.5/this.data.lineHeight*1.7) // keep extra height for mobile browser bottom-bar (android) + }, + events:{ // combined AFRAME+DOM reactive events diff --git a/com/isoterminal/ISOTerminal.js b/com/isoterminal/ISOTerminal.js index 3d65a03..8fc822f 100644 --- a/com/isoterminal/ISOTerminal.js +++ b/com/isoterminal/ISOTerminal.js @@ -61,11 +61,11 @@ ISOTerminal.prototype.convert = { const bytes = new Uint8Array(buffer); const len = bytes.byteLength; for (let i = 0; i < len; i++) binary += String.fromCharCode(bytes[i]); - return window.btoa(binary); + return btoa(binary); }, base64ToArrayBuffer: function(base64) { - const binaryString = window.atob(base64); + const binaryString = atob(base64); const len = binaryString.length; const bytes = new Uint8Array(len); @@ -211,11 +211,14 @@ ISOTerminal.prototype.startVM = function(opts){ \r ▬▬▬▬▬▬▬▬ https://xrsh.isvery.ninja ▬▬▬▬▬▬▬▬▬▬▬▬ \r local-first, polyglot, unixy WebXR IDE & runtime \r -\r credits: NLnet | @nlnet@nlnet.nl -\r Leon van Kammen | @lvk@mastodon.online -\r Fabien Benetou | @utopiah@mastodon.pirateparty.be -\r Mr Doob | THREE.js -\r Diego Marcos | AFRAME.js +\r credits +\r ------- +\r @nlnet@nlnet.nl +\r @lvk@mastodon.online +\r @utopiah@mastodon.pirateparty.be +\r https://www.w3.org/TR/webxr +\r https://three.org +\r https://aframe.org ` const text_color = "\r" diff --git a/com/isoterminal/VT100.js b/com/isoterminal/VT100.js index 4d51ba1..e3bdd0e 100644 --- a/com/isoterminal/VT100.js +++ b/com/isoterminal/VT100.js @@ -1,8 +1,15 @@ -// https://raw.githubusercontent.com/vsinitsyn/vt100/refs/heads/coffeescript/public/javascripts/VT100.js +// https://raw.githubusercontent.com/vetupinitsyn/vt100/refs/heads/coffeescript/public/javascripts/VT100.js // // VT100.js -- a text terminal emulator in JavaScript with a ncurses-like // interface and a POSIX-like interface. (The POSIX-like calls are // implemented on top of the ncurses-like calls, not the other way round.) +// +// required markup: +// +//
+//

+//   
+ // // Released under the GNU LGPL v2.1, by Frank Bi // @@ -77,8 +84,10 @@ // interpreted and acted on. // constructor -function VT100(wd, ht, el_or_id, max_scroll_lines, fg, bg) +function VT100(opts) { + this.opts = opts + let {cols, rows, el_or_id, max_scroll_lines, fg, bg, nodim} = opts if (!max_scroll_lines) { max_scroll_lines = 1000; } @@ -92,20 +101,20 @@ function VT100(wd, ht, el_or_id, max_scroll_lines, fg, bg) var r; var c; var scr = typeof el_or_id == 'string' ? document.getElementById(el_or_id) : el_or_id - this.wd_ = wd; - this.ht_ = ht; + this.wd_ = cols; + this.ht_ = rows; // Keep up to max_scroll_lines of scrollback history. this.max_ht_ = max_scroll_lines; this._set_colors(fg, bg); - this.text_ = new Array(ht); - this.attr_ = new Array(ht); - this.redraw_ = new Array(ht); - this.scroll_region_ = [0, ht-1]; + this.text_ = new Array(rows); + this.attr_ = new Array(rows); + this.redraw_ = new Array(rows); + this.scroll_region_ = [0, rows-1]; this.start_row_id = 0; - this.num_rows_ = ht; - for (r = 0; r < ht; ++r) { - this.text_[r] = new Array(wd); - this.attr_[r] = new Array(wd); + this.num_rows_ = rows; + for (r = 0; r < rows; ++r) { + this.text_[r] = new Array(cols); + this.attr_[r] = new Array(cols); this.redraw_[r] = 1; } this.scr_ = scr; @@ -116,10 +125,14 @@ function VT100(wd, ht, el_or_id, max_scroll_lines, fg, bg) this.key_buf_ = []; this.echo_ = false; this.esc_state_ = 0; - this.log_level_ = VT100.DEBUG //WARN; + this.log_level_ = VT100.WARN this.clear_all(); - this.refresh(); + + // rate limit this.refresh + this.refresh = this.throttleSmart( VT100.prototype.refresh.bind(this), 100) + + this.setupTouchInputFallback() // smartphone } // public constants -- colours and colour pairs @@ -172,6 +185,7 @@ VT100.handle_onkeypress_ = function VT100_handle_onkeypress(event,cb) var vt = VT100.the_vt_, ch; if (vt === undefined) return true; + //if ( event.keyCode != undefined || !event.charCode){ // ch = event.keyCode; // if (ch == 13) @@ -180,11 +194,11 @@ VT100.handle_onkeypress_ = function VT100_handle_onkeypress(event,cb) // return true; // ch = String.fromCharCode(ch); //} else { - ch = event.charCode; //dump("ch: " + ch + "\n"); //dump("ctrl?: " + event.ctrlKey + "\n"); - vt.debug("onkeypress:: keyCode: " + event.keyCode + ", ch: " + event.charCode); - if (ch) { + vt.debug("onkeypress:: ch: " + event.code + " ,key: "+event.key); + if (event.key.length == 1) { + ch = event.key.charCodeAt(0) if (ch > 255) return true; if (event.ctrlKey && event.shiftKey) { @@ -203,51 +217,52 @@ VT100.handle_onkeypress_ = function VT100_handle_onkeypress(event,cb) } } else { switch (event.key) { - case "Backspace": - ch = '\b'; - break; - case "Tab": - ch = '\t'; - break; - case event.DOM_VK_RETURN: - case event.DOM_VK_ENTER: - ch = '\r'; - break; - case event.DOM_VK_UP: - if (this.cursor_key_mode_ == VT100.CK_CURSOR) + case "Backspace": + ch = '\b'; + break; + case "Tab": + ch = '\t'; + break; + case "Enter": + ch = '\n'; + break; + case "ArrowUp": + if (this.cursor_key_mode_ == VT100.CK_CURSOR) ch = '\x1b[A'; - else + else ch = '\x1bOA'; - break; - case event.DOM_VK_DOWN: - if (this.cursor_key_mode_ == VT100.CK_CURSOR) + break; + case "ArrowDown": + if (this.cursor_key_mode_ == VT100.CK_CURSOR) ch = '\x1b[B'; - else + else ch = '\x1bOB'; - break; - case event.DOM_VK_RIGHT: - if (this.cursor_key_mode_ == VT100.CK_CURSOR) + break; + case "ArrowRight": + if (this.cursor_key_mode_ == VT100.CK_CURSOR) ch = '\x1b[C'; - else + else ch = '\x1bOC'; - break; - case event.DOM_VK_LEFT: - if (this.cursor_key_mode_ == VT100.CK_CURSOR) + break; + case "ArrowLeft": + if (this.cursor_key_mode_ == VT100.CK_CURSOR) ch = '\x1b[D'; - else + else ch = '\x1bOD'; - break; - case event.DOM_VK_DELETE: - ch = '\x1b[3~'; - break; - case event.DOM_VK_HOME: - ch = '\x1b[H'; - break; - case event.DOM_VK_ESCAPE: - ch = '\x1b'; - break; - default: - return true; + break; + case "Delete": + ch = '\x1b[3~'; + break; + case "Home": + ch = '\x1b[H'; + break; + case "Escape": + ch = '\x1b'; + case "Control": + break; + default: + return true + break; } } // Stop the event from doing anything else. @@ -311,39 +326,39 @@ VT100.prototype.html_colours_ = function VT100_html_colours_(attr) var fg, bg, co0, co1; fg = attr.fg; bg = attr.bg; - switch (attr.mode & (VT100.A_REVERSE | VT100.A_DIM | VT100.A_BOLD)) { - case 0: - case VT100.A_DIM | VT100.A_BOLD: - co0 = '00'; - if (bg == VT100.COLOR_WHITE) - co1 = 'ff'; - else - co1 = 'c0'; - break; - case VT100.A_BOLD: - co0 = '00'; co1 = 'ff'; - break; - case VT100.A_DIM: - if (fg == VT100.COLOR_BLACK) - co0 = '40'; - else - co0 = '00'; - co1 = '40'; - break; - case VT100.A_REVERSE: - case VT100.A_REVERSE | VT100.A_DIM | VT100.A_BOLD: - co0 = 'c0'; co1 = '40'; - break; - case VT100.A_REVERSE | VT100.A_BOLD: - co0 = 'c0'; co1 = '00'; - break; - default: - if (fg == VT100.COLOR_BLACK) - co0 = '80'; - else - co0 = 'c0'; - co1 = 'c0'; - } + switch (attr.mode & (VT100.A_REVERSE | VT100.A_DIM | VT100.A_BOLD)) { + case 0: + case VT100.A_DIM | VT100.A_BOLD: + co0 = '00'; + if (bg == VT100.COLOR_WHITE) + co1 = 'ff'; + else + co1 = 'c0'; + break; + case VT100.A_BOLD: + co0 = '00'; co1 = 'ff'; + break; + case VT100.A_DIM: + if (fg == VT100.COLOR_BLACK) + co0 = '40'; + else + co0 = '00'; + co1 = '40'; + break; + case VT100.A_REVERSE: + case VT100.A_REVERSE | VT100.A_DIM | VT100.A_BOLD: + co0 = 'c0'; co1 = 'ff'; + break; + case VT100.A_REVERSE | VT100.A_BOLD: + co0 = 'c0'; co1 = '00'; + break; + default: + if (fg == VT100.COLOR_BLACK) + co0 = '80'; + else + co0 = 'c0'; + co1 = 'c0'; + } return { f: '#' + (fg & 4 ? co1 : co0) + (fg & 2 ? co1 : co0) + @@ -1283,6 +1298,52 @@ VT100.prototype.warn = function VT100_warn(message) { } } +VT100.prototype.throttleSmart = function throttleSmart(fn, wait) { + let timeout, lastArgs; + return (...args) => { + lastArgs = lastArgs || [] + if (!timeout) { + fn(...args); timeout = setTimeout(() => { fn(...lastArgs); timeout = null; }, wait); + } else lastArgs = args; + }; +} + +VT100.prototype.setupTouchInputFallback = function(){ + this.scr_.addEventListener('touchend', () => { + if( !this.input ){ + this.form = document.createElement("form") + this.form.addEventListener("submit", (e) => { + e.preventDefault() + this.key_buf_.push('\n') + setTimeout(VT100.go_getch_, 0); + }) + this.input = document.createElement("input") + this.input.setAttribute("cols", this.opts.cols ) + this.input.setAttribute("rows", this.opts.rows ) + this.input.style.opacity = '0' + this.input.style.position = 'absolute' + this.input.style.left = '-9999px' + this.form.appendChild(this.input) + this.scr_.parentElement.appendChild(this.form) + + this.input.handler = () => { + let ch = this.input.value + // detect backspace + //if( e.inputType == 'deleteContentBackward' ) ch = '\b' + this.input.value = '' + if( !ch ) return + this.key_buf_.push(ch); + setTimeout(VT100.go_getch_, 0); + this.input.valueLast = this.input.value + } + this.input.addEventListener('input', this.input.handler ) + + } + setTimeout( () => this.input.focus(), 10 ) + }) +} + function dump(x) { // Do nothing + console.log(x) } diff --git a/com/isoterminal/feat/autorestore.js b/com/isoterminal/feat/autorestore.js index 83d4c9f..0213ec3 100644 --- a/com/isoterminal/feat/autorestore.js +++ b/com/isoterminal/feat/autorestore.js @@ -1,15 +1,31 @@ if( typeof emulator != 'undefined' ){ // inside worker-thread + importScripts("localforage.js") // we don't instance it again here (just use its functions) this['emulator.restore_state'] = async function(data){ - await emulator.restore_state(data) - console.log("restored state") - this.postMessage({event:"state_restored",data:false}) + return new Promise( (resolve,reject) => { + localforage.getItem("state", async (err,stateBase64) => { + if( stateBase64 && !err ){ + state = ISOTerminal.prototype.convert.base64ToArrayBuffer( stateBase64 ) + await emulator.restore_state(state) + console.log("restored state") + }else return reject("worker.js: emulator.restore_state (could not get state from localforage)") + resolve() + }) + }) } this['emulator.save_state'] = async function(){ console.log("saving session") let state = await emulator.save_state() - this.postMessage({event:"state_saved",data:state},[state]) + localforage.setDriver([ + localforage.INDEXEDDB, + localforage.WEBSQL, + localforage.LOCALSTORAGE + ]).then( () => { + localforage.setItem("state", ISOTerminal.prototype.convert.arrayBufferToBase64(state) ) + console.log("state saved") + }) + console.dir(state) } @@ -30,30 +46,21 @@ if( typeof emulator != 'undefined' ){ localforage.getItem("state", async (err,stateBase64) => { if( stateBase64 && !err && confirm('continue last session?') ){ this.noboot = true // see feat/boot.js - state = this.convert.base64ToArrayBuffer( stateBase64 ) - - this.addEventListener('state_restored', function(){ + try{ + await this.worker['emulator.restore_state']() // simulate / fastforward boot events this.postBoot( () => { this.send("l\n") this.send("hook wakeup\n") }) - }) - - this.worker.postMessage({event:'emulator.restore_state',data:state}) + }catch(e){ console.error(e) } } }) this.save = async () => { - const state = await this.worker.postMessage({event:"emulator.save_state",data:false}) + await this.worker['emulator.save_state']() } - this.addEventListener('state_saved', function(e){ - const state = e.detail - localforage.setItem("state", this.convert.arrayBufferToBase64(state) ) - console.log("state saved") - }) - window.addEventListener("beforeunload", function (e) { var confirmationMessage = "Sure you want to leave?\nTIP: enter 'save' to continue this session later"; (e || window.event).returnValue = confirmationMessage; //Gecko + IE diff --git a/com/isoterminal/feat/boot.js b/com/isoterminal/feat/boot.js index fe20b6b..1d1024f 100644 --- a/com/isoterminal/feat/boot.js +++ b/com/isoterminal/feat/boot.js @@ -4,7 +4,11 @@ ISOTerminal.addEventListener('ready', function(e){ ISOTerminal.prototype.boot = async function(e){ // set environment - let env = ['export BROWSER=1'] + let env = [ + `export LINES=${this.opts.rows}`, + `export COLUMNS=${this.opts.cols}`, + 'export BROWSER=1', + ] for ( let i in document.location ){ if( typeof document.location[i] == 'string' ){ env.push( 'export '+String(i).toUpperCase()+'="'+decodeURIComponent( document.location[i]+'"') )