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', {
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) => `<div class="isoterminal"></div>`,
// 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) => `<div class="isoterminal"></div>`,
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 <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){
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 <a-scene>
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 <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 '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\]"

View File

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

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