diff --git a/com/isoterminal.js b/com/isoterminal.js index c4ab4fe..dc42a01 100644 --- a/com/isoterminal.js +++ b/com/isoterminal.js @@ -1,32 +1,33 @@ -function ISOTerminal(instance,opts){ - // create a neutral isoterminal object which can be decorated - // with prototype functions and has addListener() and dispatchEvent() - let obj = new EventTarget() - obj.instance = instance - obj.opts = opts - // register default event listeners (enable file based features like isoterminal/jsconsole.js e.g.) - for( let event in ISOTerminal.listener ) - for( let cb in ISOTerminal.listener[event] ) - obj.addEventListener( event, ISOTerminal.listener[event][cb] ) - // compose object with functions - for( let i in ISOTerminal.prototype ) obj[i] = ISOTerminal.prototype[i] - obj.emit('init') - return obj -} - -ISOTerminal.prototype.emit = function(event,data){ - data = data || false - this.dispatchEvent( new CustomEvent(event, {detail: data} ) ) -} - -ISOTerminal.addEventListener = (event,cb) => { - ISOTerminal.listener = ISOTerminal.listener || {} - ISOTerminal.listener[event] = ISOTerminal.listener[event] || [] - ISOTerminal.listener[event].push(cb) -} - -// ISOTerminal has defacto support for AFRAME -// but can be decorated to work without it as well +/* + * + * css/html template + * + * ┌─────────┐ ┌────────────┐ ┌─────────────┐ exit-AR + * ┌───────►│ com/dom ┼──►│ com/window ├─►│ domrenderer │◄────────── exit-VR ◄─┐ + * │ └─────────┘ └────────────┘ └─────▲───────┘ │ + * │ │ ┌───────────────┐ │ + * ┌──────────┴────────┐ ┌─────┴──────┐ │ xterm.js │ ┌─────────────────────────────┐ + * │ com/isoterminal ├────────────────────────────►│com/xterm.js│◄─┤ │ │com/html-as-texture-in-XR.js │ + * └────────┬─┬────────┘ └──┬──────┬▲─┘ │ xterm.css │ └─────────────────────────────┘ + * │ │ ┌────────┐ ┌─────────▼──┐ ││ └───────────────┘ │ ▲ + * │ └───────►│ plane ├─────►text───┼►canvas │◄────────────────── enter-VR │ │ + * │ └────────┘ └────────────┘ ││ enter-AR ◄─┘ │ + * │ ││ │ + * │ ││ │ + * │ ISOTerminal.js ││ │ + * │ ┌───────────────────────────┐◄────┘│ │ + * │ │ com/isoterminal/worker.js ├──────┘ │ + * │ └──────────────┌────────────┤ │ + * │ │ v86.js │ │ + * │ │ feat/*.js │ │ + * │ │ libv86.js │ │ + * │ └────────────┘ │ + * │ │ + * └────────────────────────────────────────────────────────────────────────────────────┘ + * + * NOTE: For convenience reasons, events are forwarded between com/isoterminal.js, worker.js and ISOTerminal + * Instead of a melting pot of different functionnames, events are flowing through everything (ISOTerminal.emit()) + */ if( typeof AFRAME != 'undefined '){ @@ -68,14 +69,7 @@ if( typeof AFRAME != 'undefined '){ // html to texture htmlinxr: "com/html-as-texture-in-xr.js", // isoterminal features - core: "com/isoterminal/core.js", - utils_9p: "com/isoterminal/feat/9pfs_utils.js", - boot: "com/isoterminal/feat/boot.js", - jsconsole: "com/isoterminal/feat/jsconsole.js", - javascript: "com/isoterminal/feat/javascript.js", - indexhtml: "com/isoterminal/feat/index.html.js", - indexjs: "com/isoterminal/feat/index.js.js", - autorestore: "com/isoterminal/feat/autorestore.js", + ISOTerminal: "com/isoterminal/ISOTerminal.js", localforage: "https://cdn.rawgit.com/mozilla/localForage/master/dist/localforage.js" }, @@ -83,9 +77,6 @@ if( typeof AFRAME != 'undefined '){ scale: 0.66, events: ['click','keydown'], html: (me) => `
-
- -
`, css: (me) => `.isoterminal{ @@ -112,7 +103,8 @@ if( typeof AFRAME != 'undefined '){ overflow: hidden; } .isoterminal *, - .xterm-dom-renderer-owner-1 .xterm-rows { + .isotemrinal .xterm-dom-renderer-owner-1 .xterm-rows { + background:transparent !important; font-size: 14px; font-family: "Cousine",Liberation Mono,DejaVu Sans Mono,Courier New,monospace; font-weight:500 !important; @@ -122,6 +114,11 @@ if( typeof AFRAME != 'undefined '){ .isoterminal style{ display:none } + #overlay .winbox:has(> .isoterminal){ + background:transparent; + box-shadow:none; + } + .wb-body:has(> .isoterminal){ background: #000C; overflow:hidden; @@ -135,6 +132,11 @@ if( typeof AFRAME != 'undefined '){ .isoterminal div{ display:block; } .isoterminal span{ display: inline } + .isoterminal .xterm-helpers { + position:absolute; + opacity:0; + } + @keyframes fade { from { opacity: 1.0; } 50% { opacity: 0.5; } @@ -158,13 +160,27 @@ if( typeof AFRAME != 'undefined '){ initTerminal: async function(singleton){ if( this.data.xterm ){ - this.requires.xtermjs = "https://unpkg.com/@xterm/xterm@5.5.0/lib/xterm.js" - this.requires.xtermcss = "https://unpkg.com/@xterm/xterm@5.5.0/css/xterm.css" - this.requires.xterm = "com/isoterminal/feat/xterm.js" - // xterm relies on window.requestAnimationFrame which is not called in XR (xrSession.requestAnimationFrame is) + // why 3.12? + // first versions used 1.5.4, a typescript rewrite which: + // * does not use canvas anymore [which would be ideal for THREE.js texture] + // * does not allow switching between dom/canvas + // * only allows a standalone WebGL addon (conflicts with THREE) + // * heavily dependent on requestAnimationFrame (conflicts with THREE) + // * typescript-rewrite results in ~300k lib (instead of 96k) + this.requires.xtermcss = "//unpkg.com/xterm@3.12.0/dist/xterm.css", + this.requires.xtermjs = "//unpkg.com/xterm@3.12.0/dist/xterm.js", + this.requires.xtermcss = "com/xterm.js" } - let s = await AFRAME.utils.require(this.requires) + await AFRAME.utils.require(this.requires) + await AFRAME.utils.require({ // ISOTerminal plugins + boot: "com/isoterminal/feat/boot.js", + javascript: "com/isoterminal/feat/javascript.js", + jsconsole: "com/isoterminal/feat/jsconsole.js", + //indexhtml: "com/isoterminal/feat/index.html.js", + //indexjs: "com/isoterminal/feat/index.js.js", + //autorestore: "com/isoterminal/feat/autorestore.js", + }) this.el.setAttribute("selfcontainer","") @@ -194,10 +210,12 @@ if( typeof AFRAME != 'undefined '){ instance.addEventListener('window.oncreate', (e) => { instance.dom.classList.add('blink') + instance.setAttribute("xterm","") + instance.addEventListener("xterm-input", (e) => this.isoterminal.send(e.detail,0) ) // run iso let opts = {dom:instance.dom} for( let i in this.data ) opts[i] = this.data[i] - this.isoterminal.runISO(opts) + this.isoterminal.start(opts) }) instance.setAttribute("dom", "") @@ -214,9 +232,6 @@ if( typeof AFRAME != 'undefined '){ this.isoterminal.addEventListener('ready', (e)=>{ instance.dom.classList.remove('blink') this.isoterminal.emit('status',"running") - setTimeout( () => { // important: after window maximize animation to get true size - instance.setAttribute("html-as-texture-in-xr", `domid: #${instance.uid}`) // only show aframe-html in xr - },1500) }) this.isoterminal.addEventListener('status', function(e){ @@ -236,8 +251,8 @@ if( typeof AFRAME != 'undefined '){ instance.addEventListener('window.onmaximize', resize ) const focus = (e) => { - if( this.isoterminal?.emulator?.serial_adapter?.term ){ - this.isoterminal.emulator.serial_adapter.term.focus() + if( this.el.components.xterm ){ + this.el.components.xterm.term.focus() } } instance.addEventListener('obbcollisionstarted', focus ) @@ -257,10 +272,6 @@ if( typeof AFRAME != 'undefined '){ // reactive events for this.data updates myvalue: function(e){ this.el.dom.querySelector('b').innerText = this.data.myvalue }, - ready: function( ){ - this.el.dom.style.display = 'none' - }, - launcher: async function(){ this.initTerminal() } diff --git a/com/isoterminal/core.js b/com/isoterminal/ISOTerminal.js similarity index 100% rename from com/isoterminal/core.js rename to com/isoterminal/ISOTerminal.js diff --git a/com/isoterminal/feat/9pfs_utils.js b/com/isoterminal/feat/9pfs_utils.js index 737bcaf..9232530 100644 --- a/com/isoterminal/feat/9pfs_utils.js +++ b/com/isoterminal/feat/9pfs_utils.js @@ -1,46 +1,47 @@ -ISOTerminal.addEventListener('emulator-started', function(){ - let emulator = this.emulator - let isoterminal = this +let emulator = this.emulator - emulator.fs9p.update_file = async function(file,data){ +emulator.fs9p.update_file = async function(file,data){ + const convert = ISOTerminal.prototype.convert - const p = this.SearchPath(file); + const p = this.SearchPath(file); - if(p.id === -1) - { - return emulator.create_file(file,data) - } - - const inode = this.GetInode(p.id); - const buf = typeof data == 'string' ? isoterminal.convert.toUint8Array(data) : data - await this.Write(p.id,0, buf.length, buf ) - // update inode - inode.size = buf.length - const now = Math.round(Date.now() / 1000); - inode.atime = inode.mtime = now; - isoterminal.exec(`touch ${file}`) // update inode - return new Promise( (resolve,reject) => resolve(buf) ) + if(p.id === -1) + { + return emulator.create_file(file,data) + } + + const inode = this.GetInode(p.id); + const buf = typeof data == 'string' ? convert.toUint8Array(data) : data + await this.Write(p.id,0, buf.length, buf ) + // update inode + inode.size = buf.length + const now = Math.round(Date.now() / 1000); + inode.atime = inode.mtime = now; + this.postMessage({event:'exec',data:[`touch ${file}`]}) // update inode + return new Promise( (resolve,reject) => resolve(buf) ) - } +} - emulator.fs9p.append_file = async function(file,data){ +emulator.fs9p.append_file = async function(file,data){ + const convert = ISOTerminal.prototype.convert - const p = this.SearchPath(file); + const p = this.SearchPath(file); - if(p.id === -1) - { - return Promise.resolve(null); - } - - const inode = this.GetInode(p.id); - const buf = typeof data == 'string' ? isoterminal.convert.toUint8Array(data) : data - await this.Write(p.id, inode.size, buf.length, buf ) - // update inode - inode.size = inode.size + buf.length - const now = Math.round(Date.now() / 1000); - inode.atime = inode.mtime = now; - return new Promise( (resolve,reject) => resolve(buf) ) + if(p.id === -1) + { + return Promise.resolve(null); + } + + const inode = this.GetInode(p.id); + const buf = typeof data == 'string' ? convert.toUint8Array(data) : data + await this.Write(p.id, inode.size, buf.length, buf ) + // update inode + inode.size = inode.size + buf.length + const now = Math.round(Date.now() / 1000); + inode.atime = inode.mtime = now; + return new Promise( (resolve,reject) => resolve(buf) ) - } +} -}) +this['fs9p.append_file'] = function(){ emulator.fs9p.append_file.apply(emulator.fs9p, arguments[0]) } +this['fs9p.update_file'] = function(){ emulator.fs9p.update_file.apply(emulator.fs9p, arguments[0]) } diff --git a/com/isoterminal/feat/autorestore.js b/com/isoterminal/feat/autorestore.js index db4df13..39ac2fa 100644 --- a/com/isoterminal/feat/autorestore.js +++ b/com/isoterminal/feat/autorestore.js @@ -1,4 +1,5 @@ ISOTerminal.addEventListener('emulator-started', function(e){ + return console.log("TODO: autorestore.js") this.autorestore(e) }) diff --git a/com/isoterminal/feat/boot.js b/com/isoterminal/feat/boot.js index 0f48172..d2267d8 100644 --- a/com/isoterminal/feat/boot.js +++ b/com/isoterminal/feat/boot.js @@ -6,10 +6,11 @@ ISOTerminal.prototype.boot = async function(e){ // set environment let env = ['export BROWSER=1'] for ( let i in document.location ){ - if( typeof document.location[i] == 'string' ) + if( typeof document.location[i] == 'string' ){ env.push( 'export '+String(i).toUpperCase()+'="'+decodeURIComponent( document.location[i]+'"') ) + } } - await this.emulator.create_file("profile.browser", this.convert.toUint8Array( env.join('\n') ) ) + await this.emit("emulator.create_file", ["profile.browser", this.convert.toUint8Array( env.join('\n') ) ] ) if( this.serial_input == 0 ){ if( !this.noboot ){ @@ -18,10 +19,5 @@ ISOTerminal.prototype.boot = async function(e){ } } - if( this.emulator.serial_adapter ) this.emulator.serial_adapter.term.focus() - else{ - let els = [...document.querySelectorAll("div#screen")] - els.map( (el) => el.focus() ) - } } diff --git a/com/isoterminal/feat/javascript.js b/com/isoterminal/feat/javascript.js index f2b981a..f7deb93 100644 --- a/com/isoterminal/feat/javascript.js +++ b/com/isoterminal/feat/javascript.js @@ -1,29 +1,32 @@ -ISOTerminal.addEventListener('init', function(){ +if( typeof emulator != 'undefined' ){ + // inside worker-thread - this.addEventListener('emulator-started', function(e){ - - const emulator = this.emulator - - // unix to js device - this.readFromPipe( '/mnt/js', async (data) => { - const buf = await emulator.read_file("js") - const script = this.convert.Uint8ArrayToString(buf) - let PID="?" - try{ - if( script.match(/^PID/) ){ - PID = script.match(/^PID=([0-9]+);/)[1] - } - let res = (new Function(`${script}`))() - if( res && typeof res != 'string' ) res = JSON.stringify(res,null,2) - // write output to 9p with PID as filename - // *FIXME* not flexible / robust - emulator.create_file(PID, this.convert.toUint8Array(res) ) - }catch(e){ - console.error(e) + // unix to js device + emulator.readFromPipe( 'dev/browser/js', async (data) => { + const convert = ISOTerminal.prototype.convert + const buf = await this.emulator.read_file("dev/browser/js") + const script = convert.Uint8ArrayToString(buf) + let PID="?" + try{ + if( script.match(/^PID/) ){ + PID = script.match(/^PID=([0-9]+);/)[1] } - }) + this.postMessage({event:'javascript-eval',data:{script,PID}}) + }catch(e){ + console.error(e) + } + }) + +}else{ + // inside browser-thread - }) - -}) + ISOTerminal.addEventListener('javascript-eval', function(e){ + const {script,PID} = e.detail + let res = (new Function(`${script}`))() + if( res && typeof res != 'string' ) res = JSON.stringify(res,null,2) + // write output to 9p with PID as filename + // *FIXME* not flexible / robust + this.emit('emulator.create_file', [PID, this.convert.toUint8Array(res)] ) + }) +} diff --git a/com/isoterminal/feat/jsconsole.js b/com/isoterminal/feat/jsconsole.js index fcb64c1..3075c20 100644 --- a/com/isoterminal/feat/jsconsole.js +++ b/com/isoterminal/feat/jsconsole.js @@ -27,7 +27,6 @@ ISOTerminal.prototype.redirectConsole = function(handler){ } ISOTerminal.addEventListener('emulator-started', function(){ - let emulator = this.emulator this.redirectConsole( (str,prefix) => { let finalStr = "" @@ -35,7 +34,7 @@ ISOTerminal.addEventListener('emulator-started', function(){ str.trim().split("\n").map( (line) => { finalStr += '\x1b[38;5;165m/dev/browser: \x1b[0m'+prefix+line+'\n' }) - emulator.fs9p.append_file( "console", finalStr ) + this.emit('fs9p.append_file', ["/dev/browser/console",finalStr]) }) window.addEventListener('error', function(event) { @@ -51,9 +50,4 @@ ISOTerminal.addEventListener('emulator-started', function(){ console.error(event); }); - // enable/disable logging file (echo 1 > mnt/console.tty) - this.readFromPipe( '/mnt/console.tty', (data) => { - emulator.log_to_tty = ( String(data).trim() == '1') - }) - }) diff --git a/com/isoterminal/feat/xterm.js b/com/isoterminal/feat/xterm.js index 592f559..70bcb52 100644 --- a/com/isoterminal/feat/xterm.js +++ b/com/isoterminal/feat/xterm.js @@ -27,7 +27,6 @@ ISOTerminal.prototype.xtermInit = function(){ }) term.onRender( () => { - // xterm relies on requestAnimationFrame (which does not called in immersive mode) let _window = term._core._coreBrowserService._window if( !_window._XRSH_proxied ){ // patch the planet! @@ -40,8 +39,10 @@ ISOTerminal.prototype.xtermInit = function(){ // term.tid = null // },100) //} + this.i = 0 const requestAnimationFrameAFRAME = AFRAME.utils.throttleLeadingAndTrailing( - function(cb){ cb() },150 + function(cb){ cb() } + ,150 ) // we proxy the _window object of xterm, and reroute diff --git a/com/isoterminal/worker.js b/com/isoterminal/worker.js new file mode 100644 index 0000000..d0a8315 --- /dev/null +++ b/com/isoterminal/worker.js @@ -0,0 +1,78 @@ +importScripts("libv86.js"); +importScripts("ISOTerminal.js") // we don't instance it again here (just use its functions) + +//var emulator = new V86({ +// wasm_path: "../build/v86.wasm", +// memory_size: 32 * 1024 * 1024, +// vga_memory_size: 2 * 1024 * 1024, +// bios: { +// url: "../bios/seabios.bin", +// }, +// vga_bios: { +// url: "../bios/vgabios.bin", +// }, +// cdrom: { +// url: "../images/linux4.iso", +// }, +// autostart: true, +//}); +// +// +//emulator.add_listener("serial0-output-byte", function(byte) +//{ +// var chr = String.fromCharCode(byte); +// this.postMessage(chr); +//}.bind(this)); +// +//this.onmessage = function(e) +//{ +// emulator.serial0_send(e.data); +//}; + +this.runISO = function(opts){ + if( opts.cdrom ) opts.cdrom.url = "../../"+opts.cdrom.url + if( opts.bzimage ) opts.bzimage.url = "../../"+opts.bzimage.url + + let emulator = this.emulator = new V86(opts); + console.log("worker:started emulator") + + // event forwarding + emulator.add_listener("serial0-output-byte", function(byte){ + this.postMessage({event:"serial0-output-byte",data:byte}); + }.bind(this)); + + emulator.add_listener("emulator-started", function(){ + importScripts("feat/9pfs_utils.js") + this.postMessage({event:"emulator-started",data:false}); + }.bind(this)); + + /* + * forward events/functions so non-worker world can reach them + */ + this['emulator.create_file'] = function(){ emulator.create_file.apply(emulator, arguments[0]) } + this['emulator.read_file'] = function(){ emulator.read_file.apply(emulator, arguments[0]) } + + + // filename will be read from 9pfs: "/mnt/"+filename + emulator.readFromPipe = function(filename,cb){ + emulator.add_listener("9p-write-end", async (opts) => { + if ( opts[0] == filename.replace(/.*\//,'') ){ + cb() + } + }) + } + + importScripts("feat/javascript.js") +} +/* + * forward events/functions so non-worker world can reach them + */ + +this['serial0-input'] = function(c){ + this.emulator.bus.send( 'serial0-input', c) +} + +this.onmessage = function(e){ + let {event,data} = e.data + if( this[event] ) this[event](data) +} diff --git a/com/xterm.js b/com/xterm.js new file mode 100644 index 0000000..f81c986 --- /dev/null +++ b/com/xterm.js @@ -0,0 +1,175 @@ +let terminalInstance = 0 + +const TERMINAL_THEME = { + theme_foreground: { + // 'default': '#ffffff' + }, + theme_background: { + // 'default': '#000' + }, + theme_cursor: { + // 'default': '#ffffff' + }, + theme_selection: { + // 'default': 'rgba(255, 255, 255, 0.3)' + }, + theme_black: { + // 'default': '#000000' + }, + theme_red: { + // 'default': '#e06c75' + }, + theme_brightRed: { + // 'default': '#e06c75' + }, + theme_green: { + // 'default': '#A4EFA1' + }, + theme_brightGreen: { + // 'default': '#A4EFA1' + }, + theme_brightYellow: { + // 'default': '#EDDC96' + }, + theme_yellow: { + // 'default': '#EDDC96' + }, + theme_magenta: { + // 'default': '#e39ef7' + }, + theme_brightMagenta: { + // 'default': '#e39ef7' + }, + theme_cyan: { + // 'default': '#5fcbd8' + }, + theme_brightBlue: { + // 'default': '#5fcbd8' + }, + theme_brightCyan: { + // 'default': '#5fcbd8' + }, + theme_blue: { + // 'default': '#5fcbd8' + }, + theme_white: { + // 'default': '#d0d0d0' + }, + theme_brightBlack: { + // 'default': '#808080' + }, + theme_brightWhite: { + // 'default': '#ffffff' + } +} + +AFRAME.registerComponent('xterm', { + schema: Object.assign({ + cols: { + type: 'number', + default: 110, + }, + rows: { + type: 'number', + default: Math.floor( (window.innerHeight * 0.7 ) * 0.054 ) + }, + }, TERMINAL_THEME), + + write: function(message) { + this.term.write(message) + }, + init: function () { + const terminalElement = document.createElement('div') + terminalElement.setAttribute('style', ` + width: ${Math.floor( window.innerWidth * 0.7 )}px; + height: ${Math.floor( window.innerHeight * 0.7 )}px; + overflow: hidden; + `) + + this.el.dom.appendChild(terminalElement) + //document.body.appendChild(terminalElement) + this.el.terminalElement = terminalElement + + // Build up a theme object + const theme = Object.keys(this.data).reduce((theme, key) => { + if (!key.startsWith('theme_')) return theme + const data = this.data[key] + if(!data) return theme + theme[key.slice('theme_'.length)] = data + return theme + }, {}) + + this.renderType = 'dom' + + const term = new Terminal({ + theme: theme, + allowTransparency: true, + cursorBlink: true, + disableStdin: false, + rows: this.data.rows, + cols: this.data.cols, + fontSize: 14, + lineHeight: 1.15, + rendererType: this.renderType + }) + + this.term = term + + term.open(terminalElement) + term.focus() + + terminalElement.querySelector('.xterm-viewport').style.background = 'transparent' + + ////// now we can scale canvases to the parent element + const $screen = terminalElement.querySelector('.xterm-screen') + $screen.style.width = '100%' + + term.on('refresh', () => { + if( this.renderType == 'canvas' ){ + const material = this.el.getObject3D('mesh').material + if (!material.map) return + this.canvasContext.drawImage(this.cursorCanvas, 0,0) + material.map.needsUpdate = true + } + }) + + term.on('data', (data) => { + this.el.emit('xterm-input', data) + }) + + this.el.addEventListener('click', () => { + term.focus() + }) + + this.el.addEventListener('serial-output-byte', (e) => { + const byte = e.detail + var chr = String.fromCharCode(byte); + this.term.write(chr) + }) + + this.el.addEventListener('serial-output-string', (e) => this.term.write(e.detail) ) + + }, + + setRenderType: function(type){ + + if( type.match(/(dom|canvas)/) ){ + + if( type == 'dom'){ + this.el.removeAttribute('material') + } + + term.setOption('rendererType',type ) + this.renderType = type + + if( type == 'canvas'){ + this.canvas = terminalElement.querySelector('.xterm-text-layer') + this.canvasContext = this.canvas.getContext('2d') + this.cursorCanvas = terminalElement.querySelector('.xterm-cursor-layer') + this.el.setAttribute('material', 'transparent', true) + this.el.setAttribute('material', 'src', '#' + this.canvas.id) + } + } + }, + +})