refactor: separate features into files **end-of-crazy-prototyping**
/ mirror_to_github (push) Successful in 22s Details
/ test (push) Successful in 4s Details

This commit is contained in:
Leon van Kammen 2024-09-03 16:33:35 +00:00
parent f9cdea25e5
commit 590647ece8
10 changed files with 603 additions and 486 deletions

View File

@ -1,475 +1,258 @@
AFRAME.registerComponent('isoterminal', { function ISOTerminal(){
schema: { // create a neutral isoterminal object which can be decorated
iso: { type:"string", "default":"com/isoterminal/images/buildroot-bzimage.bin" }, // with prototype functions and has addListener() and dispatchEvent()
cols: { type: 'number',"default": 120 }, let obj = new EventTarget()
rows: { type: 'number',"default": 30 }, // register default event listeners (enable file based features like isoterminal/jsconsole.js e.g.)
padding:{ type: 'number',"default": 18 }, for( let event in ISOTerminal.listener )
transparent: { type:'boolean', "default":false } // need good gpu 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(){ ISOTerminal.prototype.emit = function(event,data){
this.el.object3D.visible = false data = data || false
}, this.dispatchEvent( new CustomEvent(event, {detail: data} ) )
}
requires:{ ISOTerminal.addEventListener = (event,cb) => {
'window': "com/window.js", ISOTerminal.listener = ISOTerminal.listener || {}
xtermjs: "https://unpkg.com/@xterm/xterm@5.5.0/lib/xterm.js", ISOTerminal.listener[event] = ISOTerminal.listener[event] || []
xtermcss: "https://unpkg.com/@xterm/xterm@5.5.0/css/xterm.css", ISOTerminal.listener[event].push(cb)
v86: "com/isoterminal/libv86.js" }
//axterm: "https://unpkg.com/aframe-xterm-component/aframe-xterm-component.js"
},
dom: { // ISOTerminal has defacto support for AFRAME
scale: 0.7, // but can be decorated to work without it as well
events: ['click','keydown'],
html: (me) => `<div class="isoterminal"></div>`,
css: (me) => `.isoterminal{ if( typeof AFRAME != 'undefined '){
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;
}
.isoterminal style{ display:none } AFRAME.registerComponent('isoterminal', {
schema: {
.wb-body:has(> .isoterminal){ iso: { type:"string", "default":"com/isoterminal/images/buildroot-bzimage.bin" },
background: #000c; cols: { type: 'number',"default": 120 },
overflow:hidden; rows: { type: 'number',"default": 30 },
} padding:{ type: 'number',"default": 18 },
transparent: { type:'boolean', "default":false } // need good gpu
.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'
}, },
launcher: async function(){ init: function(){
if( this.instance ){ this.el.object3D.visible = false
const el = document.querySelector('.isoterminal') },
return console.warn('TODO: allow multiple terminals (see v86 examples)')
}
let s = await AFRAME.utils.require(this.requires) requires:{
// instance this component 'window': "com/window.js",
const instance = this.instance = this.el.cloneNode(false) xtermjs: "https://unpkg.com/@xterm/xterm@5.5.0/lib/xterm.js",
this.el.sceneEl.appendChild( instance ) 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', () => { dom: {
this.runISO(instance.dom, instance) scale: 0.7,
instance.setAttribute("window", `title: ${this.data.iso}; uid: ${instance.uid}; attach: #overlay; dom: #${instance.dom.id}`) events: ['click','keydown'],
}) html: (me) => `<div class="isoterminal"></div>`,
instance.addEventListener('window.oncreate', (e) => { css: (me) => `.isoterminal{
instance.dom.classList.add('blink') 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) => { .isoterminal style{ display:none }
if( !confirm('do you want to kill this virtual machine and all its processes?') ) e.halt = true
})
const resize = (w,h) => { .wb-body:has(> .isoterminal){
if( instance.dom.emulator && instance.dom.emulator.serial_adapter ){ background: #000c;
setTimeout( () => { overflow:hidden;
this.autoResize(instance.dom.emulator.serial_adapter.term,instance,-5) }
},800) // wait for resize anim
.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() manifest: { // HTML5 manifest to identify app to xrsh
instance.addEventListener('obbcollisionstarted', focus ) "iso": "linux-x64-4.15.iso",
this.el.sceneEl.addEventListener('enter-vr', focus ) "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 <type> [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){ // reflect HTML changes to /dev/browser/html
if( !term.element ) return AFRAME.registerSystem('isoterminal',{
const defaultScrollWidth = 24; init: function(){
const MINIMUM_COLS = 2; this.components = []
const MINIMUM_ROWS = 2; // observe HTML changes in <a-scene>
observer = new MutationObserver( (a,b) => {
const dims = term._core._renderService.dimensions; console.log("change")
const scrollbarWidth = (term.options.scrollback === 0 })
? 0 observer.observe( this.sceneEl, {characterData: false, childList: true, attributes: false});
: (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) );
},
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 <type> [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.
`
}
});

160
com/isoterminal/core.js Normal file
View File

@ -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)
}
})
}

View File

@ -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) )
}
})

View File

@ -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()
}

View File

@ -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)
}
})
})
})

View File

@ -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')
})
})

View File

@ -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) );
}

View File

@ -18,7 +18,7 @@ command_not_found_handle(){
echo '----' echo '----'
echo 'js console: ' "type 'jsh'" echo 'js console: ' "type 'jsh'"
echo 'js shellfunction:' "type 'alias $1=\"jsh $1\"' to run '$1 yo' as $1('yo')" 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 'js capture log: ' "type 'tail -f /dev/browser/console'"
echo 'jsh<->sh hooks: ' "type 'chmod +x ~/hook.d/*/* && alert helloworld'" echo 'jsh<->sh hooks: ' "type 'chmod +x ~/hook.d/*/* && alert helloworld'"
} }
@ -27,4 +27,5 @@ resize
test $HOSTNAME = localhost || clear test $HOSTNAME = localhost || clear
cat /mnt/motd cat /mnt/motd
export PATH=$PATH:/mnt 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\]"

View File

@ -23,10 +23,12 @@ test -d /dev/browser || {
touch /mnt/dev/browser/html touch /mnt/dev/browser/html
touch /mnt/console.tty touch /mnt/console.tty
ln -s /mnt/dev/browser /dev/browser ln -s /mnt/dev/browser /dev/browser
ln -s /dev/browser/html ~/index.html
# setup console goodies # setup console goodies
ln -s /mnt/console.tty /dev/browser/console.tty # emulator.write_file() only writes to /mnt/. :( 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 echo 0 > /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 & touch /mnt/console
ln -s /mnt/console /dev/browser/console
test -f /etc/profile && rm /etc/profile test -f /etc/profile && rm /etc/profile
ln -s /mnt/profile /etc/profile ln -s /mnt/profile /etc/profile
@ -34,11 +36,10 @@ test -d /dev/browser || {
setup_hook_dirs(){ # see /mnt/hook for usage setup_hook_dirs(){ # see /mnt/hook for usage
mkdir -p ~/hook.d/alert mkdir -p ~/hook.d/alert
mkdir -p ~/hook.d/confirm echo -e "#!/bin/sh\necho hook.d/alert/yo: yo \$*" > ~/hook.d/alert/yo
mkdir -p ~/hook.d/prompt echo -e "#!/bin/js\nalert(\"hook.d/alert/yo.js \"+args.slice(1).join(' '))" > ~/hook.d/alert/yo.js
echo -e "#!/bin/sh\necho hook.d/alert/yo: yo \$*" > ~/hook.d/alert/yo echo -e "#!/usr/bin/env lua\nprint(\"hook.d/alert/yo.lua: yo \" .. arg[1])" > ~/hook.d/alert/yo.lua
echo -e "#!/bin/js\nalert(\"hook.d/alert/yo.js \"+args.slice(1).join(' '))" > ~/hook.d/alert/yo.js echo -e "#!/usr/bin/awk -f\nBEGIN{\n\tprint \"hook.d/alert/yo.awk: yo \" ARGV[1]\n}" > ~/hook.d/alert/yo.awk
echo -e "#!/usr/bin/lua\nprint(\"hook.d/alert/yo.lua: yo \" .. arg[1])" > ~/hook.d/alert/yo.lua
} }
setup_network(){ setup_network(){
@ -52,5 +53,6 @@ test -d /dev/browser || {
setup_browser_dev setup_browser_dev
setup_hook_dirs setup_hook_dirs
setup_links setup_links
setup_network
} }

View File

@ -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++
}
}
}