From 590647ece8bcc18181a5771c4e228428dbc334e3 Mon Sep 17 00:00:00 2001 From: Leon van Kammen Date: Tue, 3 Sep 2024 16:33:35 +0000 Subject: [PATCH] refactor: separate features into files **end-of-crazy-prototyping** --- com/isoterminal.js | 685 ++++++++++------------------- com/isoterminal/core.js | 160 +++++++ com/isoterminal/feat/9pfs_utils.js | 45 ++ com/isoterminal/feat/boot.js | 21 + com/isoterminal/feat/javascript.js | 23 + com/isoterminal/feat/jsconsole.js | 47 ++ com/isoterminal/feat/xterm.js | 61 +++ com/isoterminal/mnt/profile | 5 +- com/isoterminal/mnt/profile.xrsh | 16 +- com/isoterminal/mnt/test.awk | 26 -- 10 files changed, 603 insertions(+), 486 deletions(-) create mode 100644 com/isoterminal/core.js create mode 100644 com/isoterminal/feat/9pfs_utils.js create mode 100644 com/isoterminal/feat/boot.js create mode 100644 com/isoterminal/feat/javascript.js create mode 100644 com/isoterminal/feat/jsconsole.js create mode 100644 com/isoterminal/feat/xterm.js delete mode 100755 com/isoterminal/mnt/test.awk diff --git a/com/isoterminal.js b/com/isoterminal.js index 057f67e..e2eb21b 100644 --- a/com/isoterminal.js +++ b/com/isoterminal.js @@ -1,475 +1,258 @@ -AFRAME.registerComponent('isoterminal', { - schema: { - iso: { type:"string", "default":"com/isoterminal/images/buildroot-bzimage.bin" }, - cols: { type: 'number',"default": 120 }, - rows: { type: 'number',"default": 30 }, - padding:{ type: 'number',"default": 18 }, - transparent: { type:'boolean', "default":false } // need good gpu - }, +function ISOTerminal(){ + // create a neutral isoterminal object which can be decorated + // with prototype functions and has addListener() and dispatchEvent() + let obj = new EventTarget() + // 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 +} - init: function(){ - this.el.object3D.visible = false - }, +ISOTerminal.prototype.emit = function(event,data){ + data = data || false + this.dispatchEvent( new CustomEvent(event, {detail: data} ) ) +} - requires:{ - 'window': "com/window.js", - xtermjs: "https://unpkg.com/@xterm/xterm@5.5.0/lib/xterm.js", - xtermcss: "https://unpkg.com/@xterm/xterm@5.5.0/css/xterm.css", - v86: "com/isoterminal/libv86.js" - //axterm: "https://unpkg.com/aframe-xterm-component/aframe-xterm-component.js" - }, +ISOTerminal.addEventListener = (event,cb) => { + ISOTerminal.listener = ISOTerminal.listener || {} + ISOTerminal.listener[event] = ISOTerminal.listener[event] || [] + ISOTerminal.listener[event].push(cb) +} - dom: { - scale: 0.7, - events: ['click','keydown'], - html: (me) => `
`, +// ISOTerminal has defacto support for AFRAME +// but can be decorated to work without it as well - css: (me) => `.isoterminal{ - padding: ${me.com.data.padding}px; - width:100%; - height:100%; - } - .isoterminal *{ - white-space: pre; - font-size: 14px; - font-family: Liberation Mono,DejaVu Sans Mono,Courier New,monospace; - font-weight:700; - display:inline; - overflow: hidden; - } +if( typeof AFRAME != 'undefined '){ - .isoterminal style{ display:none } - - .wb-body:has(> .isoterminal){ - background: #000c; - overflow:hidden; - } - - .isoterminal div{ display:block; } - .isoterminal span{ display: inline } - - @keyframes fade { - from { opacity: 1.0; } - 50% { opacity: 0.5; } - to { opacity: 1.0; } - } - - @-webkit-keyframes fade { - from { opacity: 1.0; } - 50% { opacity: 0.5; } - to { opacity: 1.0; } - } - - .blink { - animation:fade 1000ms infinite; - -webkit-animation:fade 1000ms infinite; - } - ` - }, - - toUint8Array: function(str) { - // Create a new Uint8Array with the same length as the input string - const uint8Array = new Uint8Array(str.length); - - // Iterate over the string and populate the Uint8Array - for (let i = 0; i < str.length; i++) { - uint8Array[i] = str.charCodeAt(i); - } - return uint8Array; - }, - - runISO: function(dom,instance){ - //var term = new Terminal() - //term.open(dom) - //term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ')`` - if( typeof Terminal == undefined ) throw 'xterm terminal not loaded' - // monkeypatch Xterm (which V86 initializes) so we can add our own constructor args - window._Terminal = window.Terminal - window.Terminal = function(opts){ - const term = new window._Terminal({ ...opts, - cursorBlink:true, - onSelectionChange: function(e){ - debugger - } - }) - - term.onSelectionChange( () => { - document.execCommand('copy') - term.select(0, 0, 0) - instance.setStatus('copied to clipboard') - }) - return term - } - - instance.setStatus = (msg) => { - const w = instance.winbox - w.titleBak = w.titleBak || w.title - instance.winbox.setTitle( `${w.titleBak} [${msg}]` ) - } - - let image = {} - if( this.data.iso.match(/\.iso$/) ) image.cdrom = { url: this.data.iso } - if( this.data.iso.match(/\.bin$/) ) image.bzimage = { url: this.data.iso } - - var emulator = window.emulator = dom.emulator = new V86({ ...image, - wasm_path: "com/isoterminal/v86.wasm", - memory_size: 32 * 1024 * 1024, - vga_memory_size: 2 * 1024 * 1024, - serial_container_xtermjs: dom, - //screen_container: dom, //this.canvas.parentElement, - bios: { - url: "com/isoterminal/bios/seabios.bin", - }, - vga_bios: { - url: "com/isoterminal/bios/vgabios.bin", - }, - network_relay_url: "wss://relay.widgetry.org/", - cmdline: "rw root=host9p rootfstype=9p rootflags=trans=virtio,cache=loose modules=virtio_pci tsc=reliable init_on_free=on", - //bzimage_initrd_from_filesystem: true, - //filesystem: { - // baseurl: "com/isoterminal/v86/images/alpine-rootfs-flat", - // basefs: "com/isoterminal/v86/images/alpine-fs.json", - // }, - //screen_dummy: true, - //disable_jit: false, - filesystem: {}, - autostart: true, - }); - - const loading = [ - 'loading quantum bits and bytes', - 'preparing quantum flux capacitors', - 'crunching peanuts and chakras', - 'preparing parallel universe', - 'loading quantum state fluctuations', - 'preparing godmode', - 'loading cat pawns and cuteness', - 'beaming up scotty', - 'still faster than Windows update', - 'loading a microlinux', - 'figuring out meaning of life', - 'Aligning your chakras now', - 'Breathing in good vibes', - 'Finding inner peace soon', - 'Centering your Zen energy', - 'Awakening third eye powers', - 'Tuning into the universe', - 'Balancing your cosmic karma', - 'Stretching time and space', - 'Recharging your soul battery', - 'Transcending earthly limits' - ] - - let motd = "\n\r" - motd += " " + ' ____ _____________ _________ ___ ___ ' + "\n\r" - motd += " " + ' \\ \\/ /\\______ \\/ _____// | \\ ' + "\n\r" - motd += " " + ' \\ / | _/\\_____ \\/ ~ \\ ' + "\n\r" - motd += " " + ' / \\ | | \\/ \\ Y / ' + "\n\r" - motd += " " + ' /___/\\ \\ |____|_ /_______ /\\___|_ / ' + "\n\r" - motd += " " + ' \\_/ \\/ \\/ \\/ ' + "\n\r" - motd += " \n\r" - motd += `${loading[ Math.floor(Math.random()*1000) % loading.length-1 ]}, please wait..\n\r\n\r` - motd += "\033[0m" - - const files = [ - "com/isoterminal/mnt/js", - "com/isoterminal/mnt/jsh", - "com/isoterminal/mnt/xrsh", - "com/isoterminal/mnt/profile", - "com/isoterminal/mnt/profile.sh", - "com/isoterminal/mnt/profile.xrsh", - "com/isoterminal/mnt/profile.js", - "com/isoterminal/mnt/motd", - "com/isoterminal/mnt/v86pipe" - ] - - const redirectConsole = (handler) => { - const log = console.log; - const dir = console.dir; - const err = console.error; - const warn = console.warn; - console.log = (...args)=>{ - const textArg = args[0]; - handler(textArg+'\n'); - log.apply(log, args); - }; - console.error = (...args)=>{ - const textArg = args[0].message?args[0].message:args[0]; - handler( textArg+'\n', '\x1b[31merror\x1b[0m'); - err.apply(log, args); - }; - console.dir = (...args)=>{ - const textArg = args[0].message?args[0].message:args[0]; - handler( JSON.stringify(textArg,null,2)+'\n'); - dir.apply(log, args); - }; - console.warn = (...args)=>{ - const textArg = args[0].message?args[0].message:args[0]; - handler(textArg+'\n','\x1b[38;5;208mwarn\x1b[0m'); - err.apply(log, args); - }; - } - - emulator.bus.register("emulator-started", async () => { - emulator.serial_adapter.term.element.querySelector('.xterm-viewport').style.background = 'transparent' - emulator.serial_adapter.term.clear() - emulator.serial_adapter.term.write(motd) - - emulator.create_file("motd", this.toUint8Array(motd) ) - emulator.create_file("js", this.toUint8Array(`#!/bin/sh - cat /mnt/motd - cat > /dev/null - `)) - - redirectConsole( (str,prefix) => { - if( emulator.log_to_tty ){ - prefix = prefix ? prefix+' ' : ' ' - str.trim().split("\n").map( (line) => { - emulator.serial_adapter.term.write( '\r\x1b[38;5;165m/dev/browser: \x1b[0m'+prefix+line+'\n' ) - }) - emulator.serial_adapter.term.write( '\r' ) - } - emulator.create_file( "console", this.toUint8Array( str ) ) - }) - - let p = files.map( (f) => fetch(f) ) - Promise.all(p) - .then( (files) => { - files.map( (f) => { - f.arrayBuffer().then( (buf) => { - emulator.create_file( f.url.replace(/.*mnt\//,''), new Uint8Array(buf) ) - }) - }) - }) - - //emulator.serial0_send('chmod +x /mnt/js') - //emulator.serial0_send() - let line = '' - let ready = false - emulator.add_listener("serial0-output-byte", async (byte) => { - var chr = String.fromCharCode(byte); - if(chr < " " && chr !== "\n" && chr !== "\t" || chr > "~") - { - return; - } - - if(chr === "\n") - { - var new_line = line; - line = ""; - } - else if(chr >= " " && chr <= "~") - { - line += chr; - } - - //if(!ran_command && line.endsWith("~% ")) - //{ - // ran_command = true; - // emulator.serial0_send("chmod +x /mnt/test-i386\n"); - // emulator.serial0_send("/mnt/test-i386 > /mnt/result\n"); - // emulator.serial0_send("echo test fini''shed\n"); - //} - //console.dir({line,new_line}) - if( !ready && line.match(/^(\/ #|~%)/) ){ - instance.dom.classList.remove('blink') - // set environment - let env = ['export BROWSER=1'] - for ( let i in document.location ){ - if( typeof document.location[i] == 'string' ) - env.push( 'export '+String(i).toUpperCase()+'="'+document.location[i]+'"') - } - await emulator.create_file("profile.browser", this.toUint8Array( env.join('\n') ) ) - let boot = `source /mnt/profile ; js "$(cat /mnt/profile.js)"` - // exec hash as extra boot cmd - if( document.location.hash.length > 1 ){ - boot += ` ; cmd='${decodeURI(document.location.hash.substr(1))}' && $cmd` - } - console.dir(boot) - emulator.serial0_send(boot+"\n") - instance.winbox.maximize() - emulator.serial_adapter.term.focus() - ready = true - //emulator.serial0_send("root\n") - //emulator.serial0_send("mv /mnt/js . && chmod +x js\n") - } - }); - - // unix to js device - emulator.add_listener("9p-write-end", async (opts) => { - const decoder = new TextDecoder('utf-8'); - - if ( opts[0] == 'js' ){ - const buf = await emulator.read_file("dev/browser/js") - const script = decoder.decode(buf) - try{ - let res = (new Function(`${script}`))() - if( res && typeof res != 'string' ) res = JSON.stringify(res,null,2) - }catch(e){ - console.error(e) - } - } - }) - - // enable/disable logging file (echo 1 > mnt/console.tty) - emulator.add_listener("9p-write-end", async (opts) => { - const decoder = new TextDecoder('utf-8'); - if ( opts[0] == 'console.tty' ){ - const buf = await emulator.read_file("console.tty") - const val = decoder.decode(buf) - emulator.log_to_tty = ( String(val).trim() == '1') - } - }) - - }); - - }, - - events:{ - - // combined AFRAME+DOM reactive events - click: function(e){ }, // - keydown: function(e){ }, - - // 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' + AFRAME.registerComponent('isoterminal', { + schema: { + iso: { type:"string", "default":"com/isoterminal/images/buildroot-bzimage.bin" }, + cols: { type: 'number',"default": 120 }, + rows: { type: 'number',"default": 30 }, + padding:{ type: 'number',"default": 18 }, + transparent: { type:'boolean', "default":false } // need good gpu }, - launcher: async function(){ - if( this.instance ){ - const el = document.querySelector('.isoterminal') - return console.warn('TODO: allow multiple terminals (see v86 examples)') - } + init: function(){ + this.el.object3D.visible = false + }, - let s = await AFRAME.utils.require(this.requires) - // instance this component - const instance = this.instance = this.el.cloneNode(false) - this.el.sceneEl.appendChild( instance ) + requires:{ + 'window': "com/window.js", + xtermjs: "https://unpkg.com/@xterm/xterm@5.5.0/lib/xterm.js", + xtermcss: "https://unpkg.com/@xterm/xterm@5.5.0/css/xterm.css", + v86: "com/isoterminal/libv86.js", + // isoterminal features + core: "com/isoterminal/core.js", + utils_9p: "com/isoterminal/feat/9pfs_utils.js", + boot: "com/isoterminal/feat/boot.js", + xterm: "com/isoterminal/feat/xterm.js", + jsconsole: "com/isoterminal/feat/jsconsole.js", + javascript: "com/isoterminal/feat/javascript.js", + }, - instance.addEventListener('DOMready', () => { - this.runISO(instance.dom, instance) - instance.setAttribute("window", `title: ${this.data.iso}; uid: ${instance.uid}; attach: #overlay; dom: #${instance.dom.id}`) - }) + dom: { + scale: 0.7, + events: ['click','keydown'], + html: (me) => `
`, - instance.addEventListener('window.oncreate', (e) => { - instance.dom.classList.add('blink') - }) + css: (me) => `.isoterminal{ + padding: ${me.com.data.padding}px; + width:100%; + height:100%; + } + .isoterminal *{ + white-space: pre; + font-size: 14px; + font-family: Liberation Mono,DejaVu Sans Mono,Courier New,monospace; + font-weight:700; + display:inline; + overflow: hidden; + } - instance.addEventListener('window.onclose', (e) => { - if( !confirm('do you want to kill this virtual machine and all its processes?') ) e.halt = true - }) + .isoterminal style{ display:none } - const resize = (w,h) => { - if( instance.dom.emulator && instance.dom.emulator.serial_adapter ){ - setTimeout( () => { - this.autoResize(instance.dom.emulator.serial_adapter.term,instance,-5) - },800) // wait for resize anim + .wb-body:has(> .isoterminal){ + background: #000c; + overflow:hidden; + } + + .isoterminal div{ display:block; } + .isoterminal span{ display: inline } + + @keyframes fade { + from { opacity: 1.0; } + 50% { opacity: 0.5; } + to { opacity: 1.0; } + } + + @-webkit-keyframes fade { + from { opacity: 1.0; } + 50% { opacity: 0.5; } + to { opacity: 1.0; } + } + + .blink { + animation:fade 1000ms infinite; + -webkit-animation:fade 1000ms infinite; + } + ` + }, + + + events:{ + + // combined AFRAME+DOM reactive events + click: function(e){ }, // + keydown: function(e){ }, + + // 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(){ + if( this.instance ){ + const el = document.querySelector('.isoterminal') + return console.warn('TODO: allow multiple terminals (see v86 examples)') } + + let s = await AFRAME.utils.require(this.requires) + + // instance this component + const instance = this.instance = this.el.cloneNode(false) + this.el.sceneEl.appendChild( instance ) + + // init isoterminal + this.isoterminal = new ISOTerminal() + + instance.addEventListener('DOMready', () => { + instance.setAttribute("window", `title: ${this.data.iso}; uid: ${instance.uid}; attach: #overlay; dom: #${instance.dom.id}`) + }) + + instance.addEventListener('window.oncreate', (e) => { + instance.dom.classList.add('blink') + // run iso + let opts = {dom:instance.dom} + for( let i in this.data ) opts[i] = this.data[i] + this.isoterminal.runISO(opts) + }) + + this.isoterminal.addEventListener('ready', function(e){ + instance.dom.classList.remove('blink') + instance.winbox.maximize() + }) + + this.isoterminal.addEventListener('status', function(e){ + let msg = e.detail + const w = instance.winbox + if(!w) return + w.titleBak = w.titleBak || w.title + instance.winbox.setTitle( `${w.titleBak} [${msg}]` ) + }) + + instance.addEventListener('window.onclose', (e) => { + if( !confirm('do you want to kill this virtual machine and all its processes?') ) e.halt = true + }) + + const resize = (w,h) => { + if( this.isoterminal.emulator && this.isoterminal.emulator.serial_adapter ){ + setTimeout( () => { + this.isoterminal.xtermAutoResize(this.isoterminal.emulator.serial_adapter.term,instance,-5) + },800) // wait for resize anim + } + } + instance.addEventListener('window.onresize', resize ) + instance.addEventListener('window.onmaximize', resize ) + + instance.setAttribute("dom", "") + instance.setAttribute("xd", "") // allows flipping between DOM/WebGL when toggling XD-button + instance.setAttribute("visible", AFRAME.utils.XD() == '3D' ? 'true' : 'false' ) + instance.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.5) ) + + const focus = () => document.querySelector('canvas.a-canvas').focus() + instance.addEventListener('obbcollisionstarted', focus ) + this.el.sceneEl.addEventListener('enter-vr', focus ) + + instance.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera } - instance.addEventListener('window.onresize', resize ) - instance.addEventListener('window.onmaximize', resize ) - instance.setAttribute("dom", "") - instance.setAttribute("xd", "") // allows flipping between DOM/WebGL when toggling XD-button - instance.setAttribute("visible", AFRAME.utils.XD() == '3D' ? 'true' : 'false' ) - instance.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.5) ) + }, - const focus = () => document.querySelector('canvas.a-canvas').focus() - instance.addEventListener('obbcollisionstarted', focus ) - this.el.sceneEl.addEventListener('enter-vr', focus ) + manifest: { // HTML5 manifest to identify app to xrsh + "iso": "linux-x64-4.15.iso", + "short_name": "ISOTerm", + "name": "terminal", + "icons": [ + { + "src": "https://css.gg/terminal.svg", + "src": "data:image/svg+xml;base64,PHN2ZwogIHdpZHRoPSIyNCIKICBoZWlnaHQ9IjI0IgogIHZpZXdCb3g9IjAgMCAyNCAyNCIKICBmaWxsPSJub25lIgogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKPgogIDxwYXRoCiAgICBkPSJNNS4wMzMzIDE0LjgyODRMNi40NDc1MSAxNi4yNDI2TDEwLjY5MDIgMTJMNi40NDc1MSA3Ljc1NzMzTDUuMDMzMyA5LjE3MTU1TDcuODYxNzIgMTJMNS4wMzMzIDE0LjgyODRaIgogICAgZmlsbD0iY3VycmVudENvbG9yIgogIC8+CiAgPHBhdGggZD0iTTE1IDE0SDExVjE2SDE1VjE0WiIgZmlsbD0iY3VycmVudENvbG9yIiAvPgogIDxwYXRoCiAgICBmaWxsLXJ1bGU9ImV2ZW5vZGQiCiAgICBjbGlwLXJ1bGU9ImV2ZW5vZGQiCiAgICBkPSJNMiAyQzAuODk1NDMxIDIgMCAyLjg5NTQzIDAgNFYyMEMwIDIxLjEwNDYgMC44OTU0MyAyMiAyIDIySDIyQzIzLjEwNDYgMjIgMjQgMjEuMTA0NiAyNCAyMFY0QzI0IDIuODk1NDMgMjMuMTA0NiAyIDIyIDJIMlpNMjIgNEgyTDIgMjBIMjJWNFoiCiAgICBmaWxsPSJjdXJyZW50Q29sb3IiCiAgLz4KPC9zdmc+", + "type": "image/svg+xml", + "sizes": "512x512" + } + ], + "id": "/?source=pwa", + "start_url": "/?source=pwa", + "background_color": "#3367D6", + "display": "standalone", + "scope": "/", + "theme_color": "#3367D6", + "shortcuts": [ + { + "name": "What is the latest news?", + "cli":{ + "usage": "helloworld [options]", + "example": "helloworld news", + "args":{ + "--latest": {type:"string"} + } + }, + "short_name": "Today", + "description": "View weather information for today", + "url": "/today?source=pwa", + "icons": [{ "src": "/images/today.png", "sizes": "192x192" }] + } + ], + "description": "Hello world information", + "screenshots": [ + { + "src": "/images/screenshot1.png", + "type": "image/png", + "sizes": "540x720", + "form_factor": "narrow" + } + ], + "help":` + Helloworld application - instance.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera + This is a help file which describes the application. + It will be rendered thru troika text, and will contain + headers based on non-punctualized lines separated by linebreaks, + in above's case "\nHelloworld application\n" will qualify as header. + ` } - }, + }); - autoResize: function(term,instance,rowoffset){ - if( !term.element ) return + // reflect HTML changes to /dev/browser/html + AFRAME.registerSystem('isoterminal',{ - const defaultScrollWidth = 24; - const MINIMUM_COLS = 2; - const MINIMUM_ROWS = 2; + init: function(){ + this.components = [] + // observe HTML changes in + observer = new MutationObserver( (a,b) => { - const dims = term._core._renderService.dimensions; - const scrollbarWidth = (term.options.scrollback === 0 - ? 0 - : (term.options.overviewRuler?.width || defaultScrollWidth )); + console.log("change") + }) + observer.observe( this.sceneEl, {characterData: false, childList: true, attributes: false}); + } - const parentElementStyle = window.getComputedStyle(instance.dom); - const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')); - const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width'))); - const elementStyle = window.getComputedStyle(term.element); - const elementPadding = { - top: parseInt(elementStyle.getPropertyValue('padding-top')), - bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')), - right: parseInt(elementStyle.getPropertyValue('padding-right')), - left: parseInt(elementStyle.getPropertyValue('padding-left')) - }; - const elementPaddingVer = elementPadding.top + elementPadding.bottom; - const elementPaddingHor = elementPadding.right + elementPadding.left; - const availableHeight = parentElementHeight - elementPaddingVer; - const availableWidth = parentElementWidth - elementPaddingHor - scrollbarWidth; - const geometry = { - cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / dims.css.cell.width)), - rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / dims.css.cell.height)) - }; - term.resize(geometry.cols, geometry.rows + (rowoffset||0) ); - }, + }) +} - manifest: { // HTML5 manifest to identify app to xrsh - "iso": "linux-x64-4.15.iso", - "short_name": "ISOTerm", - "name": "terminal", - "icons": [ - { - "src": "https://css.gg/terminal.svg", - "src": "data:image/svg+xml;base64,PHN2ZwogIHdpZHRoPSIyNCIKICBoZWlnaHQ9IjI0IgogIHZpZXdCb3g9IjAgMCAyNCAyNCIKICBmaWxsPSJub25lIgogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKPgogIDxwYXRoCiAgICBkPSJNNS4wMzMzIDE0LjgyODRMNi40NDc1MSAxNi4yNDI2TDEwLjY5MDIgMTJMNi40NDc1MSA3Ljc1NzMzTDUuMDMzMyA5LjE3MTU1TDcuODYxNzIgMTJMNS4wMzMzIDE0LjgyODRaIgogICAgZmlsbD0iY3VycmVudENvbG9yIgogIC8+CiAgPHBhdGggZD0iTTE1IDE0SDExVjE2SDE1VjE0WiIgZmlsbD0iY3VycmVudENvbG9yIiAvPgogIDxwYXRoCiAgICBmaWxsLXJ1bGU9ImV2ZW5vZGQiCiAgICBjbGlwLXJ1bGU9ImV2ZW5vZGQiCiAgICBkPSJNMiAyQzAuODk1NDMxIDIgMCAyLjg5NTQzIDAgNFYyMEMwIDIxLjEwNDYgMC44OTU0MyAyMiAyIDIySDIyQzIzLjEwNDYgMjIgMjQgMjEuMTA0NiAyNCAyMFY0QzI0IDIuODk1NDMgMjMuMTA0NiAyIDIyIDJIMlpNMjIgNEgyTDIgMjBIMjJWNFoiCiAgICBmaWxsPSJjdXJyZW50Q29sb3IiCiAgLz4KPC9zdmc+", - "type": "image/svg+xml", - "sizes": "512x512" - } - ], - "id": "/?source=pwa", - "start_url": "/?source=pwa", - "background_color": "#3367D6", - "display": "standalone", - "scope": "/", - "theme_color": "#3367D6", - "shortcuts": [ - { - "name": "What is the latest news?", - "cli":{ - "usage": "helloworld [options]", - "example": "helloworld news", - "args":{ - "--latest": {type:"string"} - } - }, - "short_name": "Today", - "description": "View weather information for today", - "url": "/today?source=pwa", - "icons": [{ "src": "/images/today.png", "sizes": "192x192" }] - } - ], - "description": "Hello world information", - "screenshots": [ - { - "src": "/images/screenshot1.png", - "type": "image/png", - "sizes": "540x720", - "form_factor": "narrow" - } - ], - "help":` -Helloworld application - -This is a help file which describes the application. -It will be rendered thru troika text, and will contain -headers based on non-punctualized lines separated by linebreaks, -in above's case "\nHelloworld application\n" will qualify as header. - ` - } - -}); diff --git a/com/isoterminal/core.js b/com/isoterminal/core.js new file mode 100644 index 0000000..e7730e1 --- /dev/null +++ b/com/isoterminal/core.js @@ -0,0 +1,160 @@ +ISOTerminal.prototype.toUint8Array = function(str) { + str = String(str) || String("") + // Create a new Uint8Array with the same length as the input string + const uint8Array = new Uint8Array(str.length); + + // Iterate over the string and populate the Uint8Array + for (let i = 0; i < str.length; i++) { + uint8Array[i] = str.charCodeAt(i); + } + return uint8Array; +}, + +ISOTerminal.prototype.runISO = function(opts){ + this.opts = opts + let image = {} + if( opts.iso.match(/\.iso$/) ) image.cdrom = { url: opts.iso } + if( opts.iso.match(/\.bin$/) ) image.bzimage = { url: opts.iso } + + let emulator = this.emulator = new V86({ ...image, + wasm_path: "com/isoterminal/v86.wasm", + memory_size: 32 * 1024 * 1024, + vga_memory_size: 2 * 1024 * 1024, + serial_container_xtermjs: opts.dom, + //screen_container: dom, //this.canvas.parentElement, + bios: { + url: "com/isoterminal/bios/seabios.bin", + }, + vga_bios: { + url: "com/isoterminal/bios/vgabios.bin", + }, + network_relay_url: "wss://relay.widgetry.org/", + cmdline: "rw root=host9p rootfstype=9p rootflags=trans=virtio,cache=loose modules=virtio_pci tsc=reliable init_on_free=on", + //bzimage_initrd_from_filesystem: true, + //filesystem: { + // baseurl: "com/isoterminal/v86/images/alpine-rootfs-flat", + // basefs: "com/isoterminal/v86/images/alpine-fs.json", + // }, + //screen_dummy: true, + //disable_jit: false, + filesystem: {}, + autostart: true, + }); + + + const loading = [ + 'loading quantum bits and bytes', + 'preparing quantum flux capacitors', + 'crunching peanuts and chakras', + 'preparing parallel universe', + 'loading quantum state fluctuations', + 'preparing godmode', + 'loading cat pawns and cuteness', + 'beaming up scotty', + 'still faster than Windows update', + 'loading a microlinux', + 'figuring out meaning of life', + 'Aligning your chakras now', + 'Breathing in good vibes', + 'Finding inner peace soon', + 'Centering your Zen energy', + 'Awakening third eye powers', + 'Tuning into the universe', + 'Balancing your cosmic karma', + 'Stretching time and space', + 'Recharging your soul battery', + 'Transcending earthly limits' + ] + + let loadmsg = loading[ Math.floor(Math.random()*1000) % loading.length-1 ] + this.emit('status',loadmsg) + + let motd = "\n\r" + motd += " " + ' ____ _____________ _________ ___ ___ ' + "\n\r" + motd += " " + ' \\ \\/ /\\______ \\/ _____// | \\ ' + "\n\r" + motd += " " + ' \\ / | _/\\_____ \\/ ~ \\ ' + "\n\r" + motd += " " + ' / \\ | | \\/ \\ Y / ' + "\n\r" + motd += " " + ' /___/\\ \\ |____|_ /_______ /\\___|_ / ' + "\n\r" + motd += " " + ' \\_/ \\/ \\/ \\/ ' + "\n\r" + motd += " \n\r" + motd += `${loadmsg}, please wait..\n\r\n\r` + motd += "\033[0m" + + const files = [ + "com/isoterminal/mnt/js", + "com/isoterminal/mnt/jsh", + "com/isoterminal/mnt/xrsh", + "com/isoterminal/mnt/profile", + "com/isoterminal/mnt/profile.sh", + "com/isoterminal/mnt/profile.xrsh", + "com/isoterminal/mnt/profile.js", + "com/isoterminal/mnt/motd", + "com/isoterminal/mnt/v86pipe" + ] + + emulator.bus.register("emulator-started", async (e) => { + this.emit('emulator-started',e) + emulator.serial_adapter.term.clear() + emulator.serial_adapter.term.write(motd) + + emulator.create_file("motd", this.toUint8Array(motd) ) + emulator.create_file("js", this.toUint8Array(`#!/bin/sh + cat /mnt/motd + cat > /dev/null + `)) + + let p = files.map( (f) => fetch(f) ) + Promise.all(p) + .then( (files) => { + files.map( (f) => { + f.arrayBuffer().then( (buf) => { + emulator.create_file( f.url.replace(/.*mnt\//,''), new Uint8Array(buf) ) + }) + }) + }) + + //emulator.serial0_send('chmod +x /mnt/js') + //emulator.serial0_send() + let line = '' + let ready = false + emulator.add_listener("serial0-output-byte", async (byte) => { + this.emit('serial0-output-byte',byte) + var chr = String.fromCharCode(byte); + if(chr < " " && chr !== "\n" && chr !== "\t" || chr > "~") + { + return; + } + + if(chr === "\n") + { + var new_line = line; + line = ""; + } + else if(chr >= " " && chr <= "~") + { + line += chr; + } + + if( !ready && line.match(/^(\/ #|~%)/) ){ + this.emit('ready') + ready = true + //emulator.serial0_send("root\n") + //emulator.serial0_send("mv /mnt/js . && chmod +x js\n") + } + }); + }); + +} + +ISOTerminal.prototype.readFromPipe = function(filename,cb){ + + this.emulator.add_listener("9p-write-end", async (opts) => { + const decoder = new TextDecoder('utf-8'); + if ( opts[0] == filename.replace(/.*\//,'') ){ + const buf = await this.emulator.read_file("console.tty") + const val = decoder.decode(buf) + cb(val) + } + }) + +} diff --git a/com/isoterminal/feat/9pfs_utils.js b/com/isoterminal/feat/9pfs_utils.js new file mode 100644 index 0000000..18ca419 --- /dev/null +++ b/com/isoterminal/feat/9pfs_utils.js @@ -0,0 +1,45 @@ +ISOTerminal.addEventListener('emulator-started', function(){ + let emulator = this.emulator + let isoterminal = this + + emulator.fs9p.update_file = async function(file,data){ + + 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.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; + return new Promise( (resolve,reject) => resolve(buf) ) + + } + + emulator.fs9p.append_file = async function(file,data){ + + 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.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) ) + + } + +}) diff --git a/com/isoterminal/feat/boot.js b/com/isoterminal/feat/boot.js new file mode 100644 index 0000000..dd9019a --- /dev/null +++ b/com/isoterminal/feat/boot.js @@ -0,0 +1,21 @@ +ISOTerminal.addEventListener('ready', function(){ + this.boot() +}) + +ISOTerminal.prototype.boot = async function(){ + // set environment + let env = ['export BROWSER=1'] + for ( let i in document.location ){ + if( typeof document.location[i] == 'string' ) + env.push( 'export '+String(i).toUpperCase()+'="'+document.location[i]+'"') + } + await this.emulator.create_file("profile.browser", this.toUint8Array( env.join('\n') ) ) + let boot = `source /mnt/profile ; js "$(cat /mnt/profile.js)"` + // exec hash as extra boot cmd + if( document.location.hash.length > 1 ){ + boot += ` ; cmd='${decodeURI(document.location.hash.substr(1))}' && $cmd` + } + this.emulator.serial0_send(boot+"\n") + this.emulator.serial_adapter.term.focus() +} + diff --git a/com/isoterminal/feat/javascript.js b/com/isoterminal/feat/javascript.js new file mode 100644 index 0000000..edabe84 --- /dev/null +++ b/com/isoterminal/feat/javascript.js @@ -0,0 +1,23 @@ +ISOTerminal.addEventListener('init', function(){ + + 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("dev/browser/js") + const decoder = new TextDecoder('utf-8'); + const script = decoder.decode(buf) + try{ + let res = (new Function(`${script}`))() + if( res && typeof res != 'string' ) res = JSON.stringify(res,null,2) + }catch(e){ + console.error(e) + } + }) + + }) + +}) + diff --git a/com/isoterminal/feat/jsconsole.js b/com/isoterminal/feat/jsconsole.js new file mode 100644 index 0000000..0545759 --- /dev/null +++ b/com/isoterminal/feat/jsconsole.js @@ -0,0 +1,47 @@ +ISOTerminal.prototype.redirectConsole = function(handler){ + const log = console.log; + const dir = console.dir; + const err = console.error; + const warn = console.warn; + console.log = (...args)=>{ + const textArg = args[0]; + handler(textArg+'\n'); + log.apply(log, args); + }; + console.error = (...args)=>{ + const textArg = args[0].message?args[0].message:args[0]; + handler( textArg+'\n', '\x1b[31merror\x1b[0m'); + err.apply(log, args); + }; + console.dir = (...args)=>{ + const textArg = args[0].message?args[0].message:args[0]; + handler( JSON.stringify(textArg,null,2)+'\n'); + dir.apply(log, args); + }; + console.warn = (...args)=>{ + const textArg = args[0].message?args[0].message:args[0]; + handler(textArg+'\n','\x1b[38;5;208mwarn\x1b[0m'); + err.apply(log, args); + }; +} + +ISOTerminal.addEventListener('emulator-started', function(){ + let emulator = this.emulator + + this.redirectConsole( (str,prefix) => { + if( emulator.log_to_tty ){ + prefix = prefix ? prefix+' ' : ' ' + str.trim().split("\n").map( (line) => { + emulator.serial_adapter.term.write( '\r\x1b[38;5;165m/dev/browser: \x1b[0m'+prefix+line+'\n' ) + }) + emulator.serial_adapter.term.write( '\r' ) + } + emulator.fs9p.append_file( "console", str ) + }) + + // 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 new file mode 100644 index 0000000..d7a0e13 --- /dev/null +++ b/com/isoterminal/feat/xterm.js @@ -0,0 +1,61 @@ +ISOTerminal.addEventListener('init', function(){ + if( typeof Terminal != 'undefined' ) this.xtermInit() +}) + +ISOTerminal.prototype.xtermInit = function(){ + let isoterm = this + // monkeypatch Xterm (which V86 initializes) so we can add our own constructor args + window._Terminal = window.Terminal + window.Terminal = function(opts){ + const term = new window._Terminal({ ...opts, + cursorBlink:true, + onSelectionChange: function(e){ + debugger + } + }) + + term.onSelectionChange( () => { + document.execCommand('copy') + term.select(0, 0, 0) + isoterm.emit('status','copied to clipboard') + }) + return term + } + + this.addEventListener('emulator-started', function(){ + this.emulator.serial_adapter.term.element.querySelector('.xterm-viewport').style.background = 'transparent' + }) +} + +ISOTerminal.prototype.xtermAutoResize = function(term,instance,rowoffset){ + if( !term.element ) return + + const defaultScrollWidth = 24; + const MINIMUM_COLS = 2; + const MINIMUM_ROWS = 2; + + const dims = term._core._renderService.dimensions; + const scrollbarWidth = (term.options.scrollback === 0 + ? 0 + : (term.options.overviewRuler?.width || defaultScrollWidth )); + + const parentElementStyle = window.getComputedStyle(instance.dom); + const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')); + const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width'))); + const elementStyle = window.getComputedStyle(term.element); + const elementPadding = { + top: parseInt(elementStyle.getPropertyValue('padding-top')), + bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')), + right: parseInt(elementStyle.getPropertyValue('padding-right')), + left: parseInt(elementStyle.getPropertyValue('padding-left')) + }; + const elementPaddingVer = elementPadding.top + elementPadding.bottom; + const elementPaddingHor = elementPadding.right + elementPadding.left; + const availableHeight = parentElementHeight - elementPaddingVer; + const availableWidth = parentElementWidth - elementPaddingHor - scrollbarWidth; + const geometry = { + cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / dims.css.cell.width)), + rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / dims.css.cell.height)) + }; + term.resize(geometry.cols, geometry.rows + (rowoffset||0) ); +} diff --git a/com/isoterminal/mnt/profile b/com/isoterminal/mnt/profile index a2286fa..e587a0f 100644 --- a/com/isoterminal/mnt/profile +++ b/com/isoterminal/mnt/profile @@ -18,7 +18,7 @@ command_not_found_handle(){ echo '----' echo 'js console: ' "type 'jsh'" echo 'js shellfunction:' "type 'alias $1=\"jsh $1\"' to run '$1 yo' as $1('yo')" - echo 'js logging: ' "type 'echo 0 > /dev/browser/console.tty' to disable" + echo 'js log to tty: ' "type 'echo 1 > /dev/browser/console.tty' to enable" echo 'js capture log: ' "type 'tail -f /dev/browser/console'" echo 'jsh<->sh hooks: ' "type 'chmod +x ~/hook.d/*/* && alert helloworld'" } @@ -27,4 +27,5 @@ resize test $HOSTNAME = localhost || clear cat /mnt/motd export PATH=$PATH:/mnt -export PS1="\nxrsh # \033[0m" +export PS1="\n\[\033[38;5;57m\]x\[\033[38;5;93m\]r\[\033[38;5;129m\]s\[\033[38;5;165m\]h \[\033[38;5;201m\]# \[\033[0m\]" + diff --git a/com/isoterminal/mnt/profile.xrsh b/com/isoterminal/mnt/profile.xrsh index c69ad19..abeae4b 100644 --- a/com/isoterminal/mnt/profile.xrsh +++ b/com/isoterminal/mnt/profile.xrsh @@ -23,10 +23,12 @@ test -d /dev/browser || { touch /mnt/dev/browser/html touch /mnt/console.tty ln -s /mnt/dev/browser /dev/browser + ln -s /dev/browser/html ~/index.html # setup console goodies ln -s /mnt/console.tty /dev/browser/console.tty # emulator.write_file() only writes to /mnt/. :( - echo 1 > /dev/browser/console.tty # should be in /proc, but v86 gives 'no such file or dir' when creating it there - v86pipe /mnt/console /dev/browser/console & + echo 0 > /dev/browser/console.tty # should be in /proc, but v86 gives 'no such file or dir' when creating it there + touch /mnt/console + ln -s /mnt/console /dev/browser/console test -f /etc/profile && rm /etc/profile ln -s /mnt/profile /etc/profile @@ -34,11 +36,10 @@ test -d /dev/browser || { setup_hook_dirs(){ # see /mnt/hook for usage mkdir -p ~/hook.d/alert - mkdir -p ~/hook.d/confirm - mkdir -p ~/hook.d/prompt - echo -e "#!/bin/sh\necho hook.d/alert/yo: yo \$*" > ~/hook.d/alert/yo - echo -e "#!/bin/js\nalert(\"hook.d/alert/yo.js \"+args.slice(1).join(' '))" > ~/hook.d/alert/yo.js - echo -e "#!/usr/bin/lua\nprint(\"hook.d/alert/yo.lua: yo \" .. arg[1])" > ~/hook.d/alert/yo.lua + echo -e "#!/bin/sh\necho hook.d/alert/yo: yo \$*" > ~/hook.d/alert/yo + echo -e "#!/bin/js\nalert(\"hook.d/alert/yo.js \"+args.slice(1).join(' '))" > ~/hook.d/alert/yo.js + echo -e "#!/usr/bin/env lua\nprint(\"hook.d/alert/yo.lua: yo \" .. arg[1])" > ~/hook.d/alert/yo.lua + echo -e "#!/usr/bin/awk -f\nBEGIN{\n\tprint \"hook.d/alert/yo.awk: yo \" ARGV[1]\n}" > ~/hook.d/alert/yo.awk } setup_network(){ @@ -52,5 +53,6 @@ test -d /dev/browser || { setup_browser_dev setup_hook_dirs setup_links + setup_network } diff --git a/com/isoterminal/mnt/test.awk b/com/isoterminal/mnt/test.awk deleted file mode 100755 index 8f8a47e..0000000 --- a/com/isoterminal/mnt/test.awk +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/awk -f - -BEGIN { - for (i = 1; i < ARGC; i++) { - options[i] = ARGV[i] - } - ARGC = 0 - selected = 1 - n = length(options) - - while (1) { - printf "\r " - for (i = 1; i <= n; i++) { - if (i == selected) - printf "\033[44m%s\033[0m ", options[i] - else - printf "%s ", options[i] - } - if (c == 0) { - getline dir < "/dev/stdin" - print dir - if (dir == "up" && selected > 1) selected-- - if (dir == "down" && selected < n) selected++ - } - } -}