Compare commits

..

No commits in common. "main" and "feat/codemirror" have entirely different histories.

19 changed files with 303 additions and 4735 deletions

View File

@ -4,8 +4,8 @@ AFRAME.registerComponent('codemirror', {
schema: { schema: {
file: { type:"string"}, file: { type:"string"},
term: { type:"selector", default: "[isoterminal]" }, term: { type:"selector", default: "[isoterminal]" },
width: { type:"number", default:700}, width: { type:"number", default:900},
height: { type:"number", default:500}, height: { type:"number", default:700},
}, },
init: function () { init: function () {
@ -36,7 +36,7 @@ AFRAME.registerComponent('codemirror', {
css: (me) => `.CodeMirror{ css: (me) => `.CodeMirror{
width: ${me.com.data.width}px !important; width: ${me.com.data.width}px !important;
height: ${me.com.data.height-30}px !important; height: ${me.com.data.height}px !important;
} }
.codemirror *{ .codemirror *{
font-size: 14px; font-size: 14px;
@ -45,10 +45,7 @@ AFRAME.registerComponent('codemirror', {
letter-spacing: 0 !important; letter-spacing: 0 !important;
text-shadow: 0px 0px 10px #F075; text-shadow: 0px 0px 10px #F075;
} }
#${me.dom.id} .wb-body { overflow:hidden; }
.wb-body:has(> .codemirror){
overflow:hidden;
}
.CodeMirror { .CodeMirror {
margin-top:18px; margin-top:18px;
@ -81,36 +78,17 @@ AFRAME.registerComponent('codemirror', {
this.editor.updateFile( this.data.file, instance.getValue() ) this.editor.updateFile( this.data.file, instance.getValue() )
}) })
this
.handleFocus()
setTimeout( () => { setTimeout( () => {
this.el.setAttribute("html-as-texture-in-xr", `domid: #${this.el.dom.id}`) // only show aframe-html in xr this.el.setAttribute("html-as-texture-in-xr", `domid: #${this.el.dom.id}`) // only show aframe-html in xr
},1500) },1500)
}, },
handleFocus: function(){
const focus = (showdom) => (e) => {
if( this.editor ){
this.editor.focus()
}
if( this.el.components.window && this.data.renderer == 'canvas'){
this.el.components.window.show( showdom )
}
}
this.el.addEventListener('obbcollisionstarted', focus(false) )
this.el.sceneEl.addEventListener('enter-vr', focus(false) )
this.el.sceneEl.addEventListener('enter-ar', focus(false) )
this.el.sceneEl.addEventListener('exit-vr', focus(true) )
this.el.sceneEl.addEventListener('exit-ar', focus(true) )
},
updateFile: async function(file,str){ updateFile: async function(file,str){
// we don't do via shellcmd: isoterminal.exec(`echo '${str}' > ${file}`,1) // we don't do via shellcmd: isoterminal.exec(`echo '${str}' > ${file}`,1)
// as it would require all kindof ugly stringescaping // as it would require all kindof ugly stringescaping
console.log("updating "+file) console.log("updating "+file)
await this.isoterminal.worker.update_file(file, this.isoterminal.convert.toUint8Array(str) ) console.log(str)
this.isoterminal.exec("touch "+file) // *FIXME* notify filesystem (why does inotifyd need this? v86's 9pfees is cached?) await this.isoterminal.worker['emulator.update_file'](file, term.convert.toUint8Array(str) )
}, },
events:{ events:{
@ -118,7 +96,7 @@ AFRAME.registerComponent('codemirror', {
// component events // component events
DOMready: function(e){ DOMready: function(e){
this.isoterminal.worker.read_file(this.data.file) this.isoterminal.worker['emulator.read_file'](this.data.file)
.then( this.isoterminal.convert.Uint8ArrayToString ) .then( this.isoterminal.convert.Uint8ArrayToString )
.then( (str) => { .then( (str) => {
console.log("creating editor") console.log("creating editor")

View File

@ -2,8 +2,7 @@ if( !AFRAME.components['html-as-texture-in-xr'] ){
AFRAME.registerComponent('html-as-texture-in-xr', { AFRAME.registerComponent('html-as-texture-in-xr', {
schema: { schema: {
domid: { type:"string"}, domid: { type:"string"}
faceuser: { type: "boolean", default: false}
}, },
dependencies:{ dependencies:{
@ -20,9 +19,7 @@ if( !AFRAME.components['html-as-texture-in-xr'] ){
let s = await AFRAME.utils.require(this.dependencies) let s = await AFRAME.utils.require(this.dependencies)
this.el.setAttribute("html",`html: ${this.data.domid}; cursor:#cursor; xrlayer: true`) this.el.setAttribute("html",`html: ${this.data.domid}; cursor:#cursor; xrlayer: true`)
this.el.setAttribute("visible", AFRAME.utils.XD() == '3D' ? 'true' : 'false' ) this.el.setAttribute("visible", AFRAME.utils.XD() == '3D' ? 'true' : 'false' )
if( this.data.faceuser ){ this.el.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.5) )
this.el.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.4) )
}
}, },
manifest: { // HTML5 manifest to identify app to xrsh manifest: { // HTML5 manifest to identify app to xrsh

View File

@ -35,28 +35,25 @@ if( typeof AFRAME != 'undefined '){
schema: { schema: {
iso: { type:"string", "default":"https://forgejo.isvery.ninja/assets/xrsh-buildroot/main/xrsh.iso" }, iso: { type:"string", "default":"https://forgejo.isvery.ninja/assets/xrsh-buildroot/main/xrsh.iso" },
overlayfs: { type:"string"}, overlayfs: { type:"string"},
width: { type: 'number',"default": -1 }, cols: { type: 'number',"default": 80 },
height: { type: 'number',"default": -1 }, rows: { type: 'number',"default": 20 },
depth: { type: 'number',"default": 0.03 },
lineHeight: { type: 'number',"default": 18 },
padding: { type: 'number',"default": 18 }, padding: { type: 'number',"default": 18 },
maximized: { type: 'boolean',"default":true}, minimized: { type: 'boolean',"default":false},
maximized: { type: 'boolean',"default":false},
muteUntilPrompt:{ type: 'boolean',"default":true}, // mute stdout until a prompt is detected in ISO muteUntilPrompt:{ type: 'boolean',"default":true}, // mute stdout until a prompt is detected in ISO
HUD: { type: 'boolean',"default":false}, // link to camera movement HUD: { type: 'boolean',"default":false}, // link to camera movement
transparent: { type:'boolean', "default":false }, // need good gpu transparent: { type:'boolean', "default":false }, // need good gpu
memory: { type: 'number', "default":40 }, // VM memory (in MB) [NOTE: quest or smartphone might crash > 40mb ] xterm: { type: 'boolean', "default":true }, // use xterm.js? (=slower)
bufferLatency: { type: 'number', "default":1 }, // in ms: bufferlatency from webworker to xterm (batch-update every char to texture) memory: { type: 'number', "default":64 }, // VM memory (in MB)
bufferLatency: { type: 'number', "default":300 }, // in ms: bufferlatency from webworker to xterm (batch-update every char to texture)
canvasLatency: { type: 'number', "default":500 }, // in ms: time between canvas re-draws
renderer: { type: 'string', "default":"canvas" },// 'dom' or 'canvas' (=faster) for immersive mode
debug: { type: 'boolean', "default":false } debug: { type: 'boolean', "default":false }
}, },
init: function(){ init: function(){
this.el.object3D.visible = false this.el.object3D.visible = false
this.calculateDimension()
this.initHud() this.initHud()
this.setupBox()
this.setupPasteDrop()
fetch(this.data.iso,{method: 'HEAD'}) fetch(this.data.iso,{method: 'HEAD'})
.then( (res) => { .then( (res) => {
if( res.status != 200 ) throw 'not found' if( res.status != 200 ) throw 'not found'
@ -72,42 +69,32 @@ if( typeof AFRAME != 'undefined '){
requires:{ requires:{
com: "com/dom.js", com: "com/dom.js",
window: "com/window.js", window: "com/window.js",
pastedrop: "com/pastedrop.js",
v86: "com/isoterminal/libv86.js", v86: "com/isoterminal/libv86.js",
vt100: "com/isoterminal/VT100.js",
// allow xrsh to selfcontain scene + itself // allow xrsh to selfcontain scene + itself
xhook: "com/lib/xhook.min.js", xhook: "https://jpillora.com/xhook/dist/xhook.min.js",
selfcontain: "com/selfcontainer.js", selfcontain: "com/selfcontainer.js",
// html to texture // html to texture
htmlinxr: "com/html-as-texture-in-xr.js", htmlinxr: "com/html-as-texture-in-xr.js",
// isoterminal global features // isoterminal features
PromiseWorker: "com/isoterminal/PromiseWorker.js", PromiseWorker: "com/isoterminal/PromiseWorker.js",
ISOTerminal: "com/isoterminal/ISOTerminal.js", ISOTerminal: "com/isoterminal/ISOTerminal.js",
localforage: "com/isoterminal/localforage.js", localforage: "https://cdn.rawgit.com/mozilla/localForage/master/dist/localforage.js"
}, },
dom: { dom: {
scale: 0.66, scale: 1.0,
events: ['click','keydown'], events: ['click','keydown'],
html: (me) => `<div class="isoterminal"> html: (me) => `<div class="isoterminal">
<div id="term" tabindex="0">
<pre></pre>
</div>
</div>`, </div>`,
css: (me) => `.isoterminal{ css: (me) => `.isoterminal{
padding: ${me.com.data.padding}px; padding: ${me.com.data.padding}px;
width:100%; width:100%;
height:90%; height:100%;
position:relative;
} }
.isoterminal div{ .isoterminal div{
display:block; display:block;
position:relative; position:relative;
line-height: ${me.com.data.lineHeight}px;
}
#term {
outline: none !important;
} }
@font-face { @font-face {
font-family: 'Cousine'; font-family: 'Cousine';
@ -124,30 +111,15 @@ if( typeof AFRAME != 'undefined '){
.isoterminal style{ display:none } .isoterminal style{ display:none }
blink{
border:none;
padding:none;
}
#overlay .winbox:has(> .isoterminal){ #overlay .winbox:has(> .isoterminal){
background:transparent; background:transparent;
box-shadow:none; box-shadow:none;
} }
.cursor {
background: #70F !important;
animation:fade 1000ms infinite;
-webkit-animation:fade 1000ms infinite;
}
.XR .cursor {
animation:none;
-webkit-animation:none;
}
.wb-body:has(> .isoterminal){ .wb-body:has(> .isoterminal){
background: #000C; background: #000C;
overflow:hidden; overflow:hidden;
border-radius:7px;
} }
.XR .wb-body:has(> .isoterminal){ .XR .wb-body:has(> .isoterminal){
@ -157,12 +129,29 @@ if( typeof AFRAME != 'undefined '){
.XR .isoterminal{ .XR .isoterminal{
background: #000; background: #000;
} }
.isoterminal *{ .isoterminal *,
.isoterminal .xterm-dom-renderer-owner-1 .xterm-rows {
background:transparent !important;
font-size: 14px; font-size: 14px;
font-family: "Cousine",Liberation Mono,DejaVu Sans Mono,Courier New,monospace; font-family: "Cousine",Liberation Mono,DejaVu Sans Mono,Courier New,monospace;
font-weight:500 !important; font-weight:500 !important;
text-shadow: 0px 0px 10px #F075; text-shadow: 0px 0px 10px #F075;
} }
.isoterminal .xterm-rows.xterm-focus .xterm-cursor.xterm-cursor-block {
background-color:#a5F !important;
}
.isoterminal .xterm-rows div{
height:8px;
height:18px;
}
.isoterminal .xterm-rows span{
width:8px;
}
.isoterminal .xterm-helpers {
position:absolute;
opacity:0;
top: -2000px;
}
@keyframes fade { @keyframes fade {
from { opacity: 1.0; } from { opacity: 1.0; }
@ -176,6 +165,7 @@ if( typeof AFRAME != 'undefined '){
to { opacity: 1.0; } to { opacity: 1.0; }
} }
.isoterminal .xterm-rows.xterm-focus .xterm-cursor.xterm-cursor-block,
.blink{ .blink{
animation:fade 1000ms infinite; animation:fade 1000ms infinite;
-webkit-animation:fade 1000ms infinite; -webkit-animation:fade 1000ms infinite;
@ -186,7 +176,8 @@ if( typeof AFRAME != 'undefined '){
initTerminal: async function(singleton){ initTerminal: async function(singleton){
// why not latest xterm or v3.12 with builtin-canvas support? if( this.data.xterm ){
// why 3.12?
// first versions used 1.5.4, a typescript rewrite which: // first versions used 1.5.4, a typescript rewrite which:
// * acts weird with oculus browser keyboard (does not repaint properly after typing) // * acts weird with oculus browser keyboard (does not repaint properly after typing)
// * does not use canvas anymore [which would be ideal for THREE.js texture] // * does not use canvas anymore [which would be ideal for THREE.js texture]
@ -194,7 +185,10 @@ if( typeof AFRAME != 'undefined '){
// * only allows a standalone WebGL addon (conflicts with THREE) // * only allows a standalone WebGL addon (conflicts with THREE)
// * heavily dependent on requestAnimationFrame (conflicts with THREE) // * heavily dependent on requestAnimationFrame (conflicts with THREE)
// * typescript-rewrite results in ~300k lib (instead of 96k) // * typescript-rewrite results in ~300k lib (instead of 96k)
// * v3.12 had slightly better performance but still very heavy this.requires.xtermcss = "//unpkg.com/xterm@3.12.0/dist/xterm.css",
this.requires.xtermjs = "//unpkg.com/xterm@3.12.0/dist/xterm.js",
this.requires.xtermcss = "com/xterm.js"
}
await AFRAME.utils.require(this.requires) await AFRAME.utils.require(this.requires)
await AFRAME.utils.require({ // ISOTerminal plugins await AFRAME.utils.require({ // ISOTerminal plugins
@ -204,8 +198,6 @@ if( typeof AFRAME != 'undefined '){
indexhtml: "com/isoterminal/feat/index.html.js", indexhtml: "com/isoterminal/feat/index.html.js",
indexjs: "com/isoterminal/feat/index.js.js", indexjs: "com/isoterminal/feat/index.js.js",
autorestore: "com/isoterminal/feat/autorestore.js", autorestore: "com/isoterminal/feat/autorestore.js",
pastedropFeat: "com/isoterminal/feat/pastedrop.js",
httpfs: "com/isoterminal/feat/httpfs.js",
}) })
this.el.setAttribute("selfcontainer","") this.el.setAttribute("selfcontainer","")
@ -228,28 +220,34 @@ if( typeof AFRAME != 'undefined '){
this.term = new ISOTerminal(instance,this.data) this.term = new ISOTerminal(instance,this.data)
instance.addEventListener('DOMready', () => { instance.addEventListener('DOMready', () => {
this.setupVT100(instance) if( this.data.renderer == 'dom' ){
setTimeout( () => { instance.setAttribute("html-as-texture-in-xr", `domid: #${this.el.dom.id}`)
instance.setAttribute("html-as-texture-in-xr", `domid: #term; faceuser: true`) }
},100)
//instance.winbox.resize(720,380) //instance.winbox.resize(720,380)
let size = `width: ${this.data.width}; height: ${this.data.height}` let size = `width: ${Math.floor(this.data.cols*8.65)}; height: ${Math.floor(this.data.rows*21.1)}`
instance.setAttribute("window", `title: xrsh.iso; uid: ${instance.uid}; attach: #overlay; dom: #${instance.dom.id}; ${size}; min: ${this.data.minimized}; max: ${this.data.maximized}; class: no-full, no-resize, no-move`) instance.setAttribute("window", `title: xrsh.iso; uid: ${instance.uid}; attach: #overlay; dom: #${instance.dom.id}; ${size}; min: ${this.data.minimized}; max: ${this.data.maximized}`)
}) })
instance.addEventListener('window.oncreate', (e) => { instance.addEventListener('window.oncreate', (e) => {
instance.dom.classList.add('blink') instance.dom.classList.add('blink')
instance.setAttribute("xterm",`cols: ${this.data.cols}; rows: ${this.data.rows}; canvasLatency: ${this.data.canvasLatency}; XRrenderer: ${this.data.renderer}`)
instance.addEventListener("xterm-input", (e) => this.term.send(e.detail,0) )
// run iso // run iso
let opts = {dom:instance.dom} let opts = {dom:instance.dom}
for( let i in this.data ) opts[i] = this.data[i] for( let i in this.data ) opts[i] = this.data[i]
opts.cols = this.cols
opts.rows = this.rows
this.term.start(opts) this.term.start(opts)
}) })
instance.setAttribute("dom", "") instance.setAttribute("dom", "")
instance.setAttribute("pastedrop", "")
this.term.addEventListener('postReady', (e)=>{
// bugfix: send window dimensions to xterm (xterm.js does that from dom-sizechange to xterm via escape codes)
let wb = instance.winbox
if( this.data.maximized ){
wb.restore()
wb.maximize()
}else wb.resize()
})
this.term.addEventListener('ready', (e) => { this.term.addEventListener('ready', (e) => {
instance.dom.classList.remove('blink') instance.dom.classList.remove('blink')
@ -274,7 +272,9 @@ if( typeof AFRAME != 'undefined '){
instance.addEventListener('window.onmaximize', resize ) instance.addEventListener('window.onmaximize', resize )
const focus = (showdom) => (e) => { const focus = (showdom) => (e) => {
this.el.emit('focus',e.detail) if( this.el.components.xterm ){
this.el.components.xterm.term.focus()
}
if( this.el.components.window && this.data.renderer == 'canvas'){ if( this.el.components.window && this.data.renderer == 'canvas'){
this.el.components.window.show( showdom ) this.el.components.window.show( showdom )
} }
@ -305,80 +305,6 @@ if( typeof AFRAME != 'undefined '){
console.test.run() console.test.run()
}, },
setupVT100: function(instance){
const el = this.el.dom.querySelector('#term')
this.term.opts.vt100 = {
cols: this.cols,
rows: this.rows,
el_or_id: el,
max_scroll_lines: this.rows,
nodim: true,
rainbow: [VT100.COLOR_MAGENTA, VT100.COLOR_CYAN ],
xr: AFRAME.scenes[0].renderer.xr,
map: {
'ArrowRight': { ch: false, ctrl: '\x1b\x66' }, // this triggers ash-shell forward-word
'ArrowLeft': { ch: false, ctrl: '\x1b\x62' } // backward-word
}
}
this.term.emit('initVT100',this)
this.vt100 = new VT100( this.term.opts.vt100 )
this.vt100.el = el
this.vt100.curs_set( 1, true)
this.vt100.focus()
this.el.addEventListener('focus', () => this.vt100.focus() )
this.vt100.getch( (ch,t) => {
this.term.send( ch )
})
this.el.addEventListener('serial-output-byte', (e) => {
const byte = e.detail
var chr = String.fromCharCode(byte);
this.vt100.addchr(chr)
})
this.el.addEventListener('serial-output-string', (e) => {
this.vt100.write(e.detail)
})
// translate file upload into pasteFile
this.vt100.upload.addEventListener('change', (e) => {
const file = this.vt100.upload.files[0];
const item = {...file, getAsFile: () => file }
this.el.emit('pasteFile', { item, type: file.type });
})
return this
},
setupPasteDrop: function(){
this.el.addEventListener('pasteFile', (e) => {
e.preventDefault() // prevent bubbling up to window (which is triggering this initially)
if( !this.term.pasteFile ) return // skip if feat/pastedrop.js is not loaded
this.term.pasteFile(e.detail)
})
return this
},
setupBox: function(){
// setup slightly bigger black backdrop (this.el.getObject3D("mesh"))
const w = this.data.width/950;
const h = this.data.height/950;
this.el.box = document.createElement('a-entity')
this.el.box.setAttribute("geometry",`primitive: box; width:${w}; height:${h}; depth: -${this.data.depth}`)
this.el.box.setAttribute("material","shader:flat; color:black; opacity:0.9; transparent:true; ")
this.el.box.setAttribute("position",`0 0 ${(this.data.depth/2)-0.001}`)
this.el.appendChild(this.el.box)
},
calculateDimension: function(){
if( this.data.width == -1 ) this.data.width = document.body.offsetWidth;
if( this.data.height == -1 ) this.data.height = Math.floor( document.body.offsetHeight - 30 )
if( this.data.height > this.data.width ) this.data.height = this.data.width // mobile smartphone fix
this.data.width -= this.data.padding*2
this.data.height -= this.data.padding*2
this.cols = Math.floor(this.data.width/this.data.lineHeight*2)
this.rows = Math.floor(this.data.height*0.53/this.data.lineHeight*1.7)
},
events:{ events:{
// combined AFRAME+DOM reactive events // combined AFRAME+DOM reactive events

View File

@ -17,14 +17,14 @@ function ISOTerminal(instance,opts){
ISOTerminal.prototype.emit = function(event,data,sender){ ISOTerminal.prototype.emit = function(event,data,sender){
data = data || false data = data || false
const evObj = new CustomEvent(event, {detail: data} ) const evObj = new CustomEvent(event, {detail: data} )
this.preventFrameDrop( () => { //this.preventFrameDrop( () => {
// forward event to worker/instance/AFRAME element or component-function // forward event to worker/instance/AFRAME element or component-function
// this feels complex, but actually keeps event- and function-names more concise in codebase // this feels complex, but actually keeps event- and function-names more concise in codebase
this.dispatchEvent( evObj ) this.dispatchEvent( evObj )
if( sender != "instance" && this.instance ) this.instance.dispatchEvent(evObj) if( sender != "instance" && this.instance ) this.instance.dispatchEvent(evObj)
if( sender != "worker" && this.worker ) this.worker.postMessage({event,data}, PromiseWorker.prototype.getTransferable(data) ) if( sender != "worker" && this.worker ) this.worker.postMessage({event,data}, PromiseWorker.prototype.getTransferable(data) )
if( sender !== undefined && typeof this[event] == 'function' ) this[event].apply(this, data && data.push ? data : [data] ) if( sender !== undefined && typeof this[event] == 'function' ) this[event].apply(this, data && data.push ? data : [data] )
}) //})
} }
ISOTerminal.addEventListener = (event,cb) => { ISOTerminal.addEventListener = (event,cb) => {
@ -37,10 +37,6 @@ ISOTerminal.prototype.exec = function(shellscript){
this.send(shellscript+"\n",1) this.send(shellscript+"\n",1)
} }
ISOTerminal.prototype.hook = function(hookname,args){
this.exec(`{ type hook || source /etc/profile.sh; }; hook ${hookname} "${args.join('" "')}"`)
}
ISOTerminal.prototype.serial_input = 0; // can be set to 0,1,2,3 to define stdinput tty (xterm plugin) ISOTerminal.prototype.serial_input = 0; // can be set to 0,1,2,3 to define stdinput tty (xterm plugin)
ISOTerminal.prototype.send = function(str, ttyNr){ ISOTerminal.prototype.send = function(str, ttyNr){
@ -52,9 +48,7 @@ ISOTerminal.prototype.send = function(str, ttyNr){
}else{ }else{
this.convert.toUint8Array( str ).map( (c) => { this.convert.toUint8Array( str ).map( (c) => {
this.preventFrameDrop( this.preventFrameDrop(
() => { () => this.worker.postMessage({event:`serial${ttyNr}-input`,data:c})
this.worker.postMessage({event:`serial${ttyNr}-input`,data:c})
}
) )
}) })
} }
@ -67,11 +61,11 @@ ISOTerminal.prototype.convert = {
const bytes = new Uint8Array(buffer); const bytes = new Uint8Array(buffer);
const len = bytes.byteLength; const len = bytes.byteLength;
for (let i = 0; i < len; i++) binary += String.fromCharCode(bytes[i]); for (let i = 0; i < len; i++) binary += String.fromCharCode(bytes[i]);
return btoa(binary); return window.btoa(binary);
}, },
base64ToArrayBuffer: function(base64) { base64ToArrayBuffer: function(base64) {
const binaryString = atob(base64); const binaryString = window.atob(base64);
const len = binaryString.length; const len = binaryString.length;
const bytes = new Uint8Array(len); const bytes = new Uint8Array(len);
@ -193,6 +187,7 @@ ISOTerminal.prototype.startVM = function(opts){
"Learned helplessness fades when we realize tech isnt too complex to understand", "Learned helplessness fades when we realize tech isnt too complex to understand",
"FOSS empowers users to customize and improve their tools", "FOSS empowers users to customize and improve their tools",
"Engaging with FOSS helps build confidence and self-reliance in tech", "Engaging with FOSS helps build confidence and self-reliance in tech",
"FOSS tools are accessible and often better than closed alternatives",
"FOSS shows that anyone can shape the digital world with curiosity and effort", "FOSS shows that anyone can shape the digital world with curiosity and effort",
"Linux can revive old computers, extending their life and reducing e-waste", "Linux can revive old computers, extending their life and reducing e-waste",
"Many lightweight Linux distributions run smoothly on older hardware", "Many lightweight Linux distributions run smoothly on older hardware",
@ -216,14 +211,11 @@ ISOTerminal.prototype.startVM = function(opts){
\r https://xrsh.isvery.ninja ▬▬▬▬▬▬▬▬▬▬▬▬ \r https://xrsh.isvery.ninja ▬▬▬▬▬▬▬▬▬▬▬▬
\r local-first, polyglot, unixy WebXR IDE & runtime \r local-first, polyglot, unixy WebXR IDE & runtime
\r \r
\r credits \r credits: NLnet | @nlnet@nlnet.nl
\r ------- \r Leon van Kammen | @lvk@mastodon.online
\r @nlnet@nlnet.nl \r Fabien Benetou | @utopiah@mastodon.pirateparty.be
\r @lvk@mastodon.online \r Mr Doob | THREE.js
\r @utopiah@mastodon.pirateparty.be \r Diego Marcos | AFRAME.js
\r https://www.w3.org/TR/webxr
\r https://three.org
\r https://aframe.org
` `
const text_color = "\r" const text_color = "\r"

File diff suppressed because it is too large Load Diff

View File

@ -21,6 +21,7 @@ emulator.fs9p.update_file = async function(file,data){
inode.size = buf.length inode.size = buf.length
const now = Math.round(Date.now() / 1000); const now = Math.round(Date.now() / 1000);
inode.atime = inode.mtime = now; inode.atime = inode.mtime = now;
me.postMessage({event:'exec',data:[`touch /mnt/${file}`]}) // update inode
return new Promise( (resolve,reject) => resolve(buf) ) return new Promise( (resolve,reject) => resolve(buf) )
}catch(e){ }catch(e){
console.error({file,data}) console.error({file,data})
@ -49,47 +50,3 @@ emulator.fs9p.append_file = async function(file,data){
} }
emulator.fs9p.read_file_world = async function(file){
const p = this.SearchPath(file);
if(p.id === -1)
{
return Promise.resolve(null);
}
const inode = this.GetInode(p.id);
const perms = this.parseFilePermissions(inode.mode)
if( !perms.world.read ){
return Promise.resolve(null);
}
return this.Read(p.id, 0, inode.size);
}
emulator.fs9p.parseFilePermissions = function(permissionInt) {
// Convert the permission integer to octal
const octalPermissions = permissionInt.toString(8);
// Extract the permission bits (last 3 digits in octal)
const permissionBits = octalPermissions.slice(-3);
function parsePermission(digit) {
const num = parseInt(digit, 10);
return {
read: Boolean(num & 4), // 4 = read
write: Boolean(num & 2), // 2 = write
execute: Boolean(num & 1) // 1 = execute
};
}
// Decode the permissions
const permissions = {
owner: parsePermission(permissionBits[0]),
group: parsePermission(permissionBits[1]),
world: parsePermission(permissionBits[2]),
};
return permissions;
}

View File

@ -1,30 +1,15 @@
if( typeof emulator != 'undefined' ){ if( typeof emulator != 'undefined' ){
// inside worker-thread // inside worker-thread
importScripts("localforage.js") // we don't instance it again here (just use its functions)
this.restore_state = async function(data){ this['emulator.restore_state'] = async function(data){
return new Promise( (resolve,reject) => { await emulator.restore_state(data)
localforage.getItem("state", async (err,stateBase64) => {
if( stateBase64 && !err ){
state = ISOTerminal.prototype.convert.base64ToArrayBuffer( stateBase64 )
await emulator.restore_state(state)
console.log("restored state") console.log("restored state")
}else return reject("worker.js: emulator.restore_state (could not get state from localforage)") this.postMessage({event:"state_restored",data:false})
resolve()
})
})
} }
this.save_state = async function(){ this['emulator.save_state'] = async function(){
console.log("saving session") console.log("saving session")
let state = await emulator.save_state() let state = await emulator.save_state()
localforage.setDriver([ this.postMessage({event:"state_saved",data:state},[state])
localforage.INDEXEDDB,
localforage.WEBSQL,
localforage.LOCALSTORAGE
]).then( () => {
localforage.setItem("state", ISOTerminal.prototype.convert.arrayBufferToBase64(state) )
console.log("state saved")
})
} }
@ -45,21 +30,30 @@ if( typeof emulator != 'undefined' ){
localforage.getItem("state", async (err,stateBase64) => { localforage.getItem("state", async (err,stateBase64) => {
if( stateBase64 && !err && confirm('continue last session?') ){ if( stateBase64 && !err && confirm('continue last session?') ){
this.noboot = true // see feat/boot.js this.noboot = true // see feat/boot.js
try{ state = this.convert.base64ToArrayBuffer( stateBase64 )
await this.worker.restore_state()
this.addEventListener('state_restored', function(){
// simulate / fastforward boot events // simulate / fastforward boot events
this.postBoot( () => { this.postBoot( () => {
this.send("l\n") this.send("l\n")
this.send("hook wakeup\n") this.send("hook wakeup\n")
}) })
}catch(e){ console.error(e) } })
this.worker.postMessage({event:'emulator.restore_state',data:state})
} }
}) })
this.save = async () => { this.save = async () => {
await this.worker.save_state() const state = await this.worker.postMessage({event:"emulator.save_state",data:false})
} }
this.addEventListener('state_saved', function(e){
const state = e.detail
localforage.setItem("state", this.convert.arrayBufferToBase64(state) )
console.log("state saved")
})
window.addEventListener("beforeunload", function (e) { window.addEventListener("beforeunload", function (e) {
var confirmationMessage = "Sure you want to leave?\nTIP: enter 'save' to continue this session later"; var confirmationMessage = "Sure you want to leave?\nTIP: enter 'save' to continue this session later";
(e || window.event).returnValue = confirmationMessage; //Gecko + IE (e || window.event).returnValue = confirmationMessage; //Gecko + IE

View File

@ -4,17 +4,13 @@ ISOTerminal.addEventListener('ready', function(e){
ISOTerminal.prototype.boot = async function(e){ ISOTerminal.prototype.boot = async function(e){
// set environment // set environment
let env = [ let env = ['export BROWSER=1']
`export LINES=${this.opts.rows}`,
`export COLUMNS=${this.opts.cols}`,
'export BROWSER=1',
]
for ( let i in document.location ){ for ( let i in document.location ){
if( typeof document.location[i] == 'string' ){ if( typeof document.location[i] == 'string' ){
env.push( 'export '+String(i).toUpperCase()+'="'+decodeURIComponent( document.location[i]+'"') ) env.push( 'export '+String(i).toUpperCase()+'="'+decodeURIComponent( document.location[i]+'"') )
} }
} }
this.worker.create_file("profile.browser", this.convert.toUint8Array( env.join('\n') ) ) await this.emit("emulator.create_file", ["profile.browser", this.convert.toUint8Array( env.join('\n') ) ] )
if( this.serial_input == 0 ){ if( this.serial_input == 0 ){
if( !this.noboot ){ if( !this.noboot ){

View File

@ -1,30 +0,0 @@
if( typeof emulator != 'undefined' ){
}else{
ISOTerminal.addEventListener('ready', function(e){
// listen for http request to the filesystem ( file://host/path )
xhook.before( (request,callback) => {
if (request.url.match(/^file:\/\/xrsh\/mnt\/.*/) ){
let response
let file = request.url.replace(/^file:\/\/xrsh\/mnt\//,'')
this.worker.read_file_world(file)
.then( (data) => {
response = new Response( new Blob( [data] ) ) // wrap Uint8Array into array
response.status = 200
callback(response)
})
.catch( (e) => {
response = new Response()
response.status = 404
callback(response)
})
return
}
callback()
})
})
}

View File

@ -6,7 +6,7 @@ if( typeof emulator != 'undefined' ){
const convert = ISOTerminal.prototype.convert const convert = ISOTerminal.prototype.convert
const buf = await this.emulator.read_file("dev/browser/js") const buf = await this.emulator.read_file("dev/browser/js")
const script = convert.Uint8ArrayToString(buf) const script = convert.Uint8ArrayToString(buf)
let PID=null let PID="?"
try{ try{
if( script.match(/^PID/) ){ if( script.match(/^PID/) ){
PID = script.match(/^PID=([0-9]+);/)[1] PID = script.match(/^PID=([0-9]+);/)[1]
@ -35,9 +35,7 @@ if( typeof emulator != 'undefined' ){
} }
} }
// update output to 9p with PID as filename (in /mnt/run) // update output to 9p with PID as filename (in /mnt/run)
if( PID ){ this.emit('fs9p.update_file', [`run/${PID}`, this.convert.toUint8Array(res)] )
this.worker.update_file(`run/${PID}`, this.convert.toUint8Array(res) )
}
}) })
} }

View File

@ -1,32 +0,0 @@
if( typeof emulator != 'undefined' ){
// inside worker-thread
}else{
// inside browser-thread
//
ISOTerminal.prototype.pasteWriteFile = async function(data,type,filename){
this.pasteWriteFile.fileCount = this.pasteWriteFile.fileCount || 0
const file = `clipboard/`+ ( filename || `user-paste-${this.pasteWriteFile.fileCount}`)
await this.worker.create_file(file, data )
// run the xrsh hook
this.hook("clipboard", [ `/mnt/${file}`, type ] )
console.log("clipboard paste: /mnt/"+file)
this.pasteWriteFile.fileCount += 1
}
ISOTerminal.prototype.pasteFile = async function(data){
const {type,item,pastedText} = data
if( pastedText){
this.pasteWriteFile( this.convert.toUint8Array(pastedText) ,type)
}else{
const file = item.getAsFile();
const reader = new FileReader();
reader.onload = (e) => {
const arr = new Uint8Array(e.target.result)
this.pasteWriteFile( arr, type, file.name ); // or use readAsDataURL for images
};
reader.readAsArrayBuffer(file);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -42,11 +42,10 @@ this.runISO = function(opts){
/* /*
* forward events/functions so non-worker world can reach them * forward events/functions so non-worker world can reach them
*/ */
this.create_file = async function(){ return emulator.create_file.apply(emulator, arguments[0]) } this['emulator.create_file'] = async function(){ return emulator.create_file.apply(emulator, arguments[0]) }
this.read_file = async function(){ return emulator.read_file.apply(emulator, arguments[0]) } this['emulator.read_file'] = async function(){ return emulator.read_file.apply(emulator, arguments[0]) }
this.read_file_world = async function(){ return emulator.fs9p.read_file_world.apply(emulator.fs9p, arguments[0]) } this['emulator.append_file'] = async function(){ emulator.fs9p.append_file.apply(emulator.fs9p, arguments[0]) }
this.append_file = async function(){ emulator.fs9p.append_file.apply(emulator.fs9p, arguments[0]) } this['emulator.update_file'] = async function(){ emulator.fs9p.update_file.apply(emulator.fs9p, arguments[0]) }
this.update_file = async function(){ emulator.fs9p.update_file.apply(emulator.fs9p, arguments[0]) }
// filename will be read from 9pfs: "/mnt/"+filename // filename will be read from 9pfs: "/mnt/"+filename
emulator.readFromPipe = function(filename,cb){ emulator.readFromPipe = function(filename,cb){

File diff suppressed because one or more lines are too long

179
com/paste.js Normal file
View File

@ -0,0 +1,179 @@
AFRAME.registerComponent('paste', {
schema: {
foo: { type:"string"}
},
init: function () {
this.el.object3D.visible = false
//this.el.innerHTML = ` `
},
requires:{
osbutton: "com/osbutton.js"
},
events:{
// component events
somecomponent: function( ){ console.log("component requirement mounted") },
ready: function(e){ console.log("requires are loaded") },
launcher: function(e){
const paste = () => {
navigator.clipboard.readText()
.then( (base64) => {
let mimetype = base64.replace(/;base64,.*/,'')
let data = base64.replace(/.*;base64,/,'')
let type = this.textHeuristic(data)
console.log("type="+type)
switch( this.textHeuristic(data) ){
case "aframe": this.insertAFRAME(data); break;
default: this.insertText(data); break;
}
this.count += 1
})
}
navigator.permissions.query({ name: 'clipboard-read' })
.then( (permission) => {
if( permission.state != 'granted' ){
this.el.sceneEl.exitVR()
setTimeout( () => paste(), 500 )
return
}else paste()
})
},
},
textHeuristic: function(text){
// Script type identification clues
const bashClues = ["|", "if ", "fi", "cat"];
const htmlClues = ["/>", "href=", "src="];
const aframeClues = ["<a-entity", "/>", "position="];
const jsClues = ["var ", "let ", "function ", "setTimeout","console."];
// Count occurrences of clues for each script type
const bashCount = bashClues.reduce((acc, clue) => acc + (text.includes(clue) ? 1 : 0), 0);
const htmlCount = htmlClues.reduce((acc, clue) => acc + (text.includes(clue) ? 1 : 0), 0);
const aframeCount = aframeClues.reduce((acc, clue) => acc + (text.includes(clue) ? 1 : 0), 0);
const jsCount = jsClues.reduce((acc, clue) => acc + (text.includes(clue) ? 1 : 0), 0);
// Identify the script with the most clues or return unknown if inconclusive
const maxCount = Math.max(bashCount, htmlCount, jsCount, aframeCount);
if (maxCount === 0) {
return "unknown";
} else if (bashCount === maxCount) {
return "bash";
} else if (htmlCount === maxCount) {
return "html";
} else if (jsCount === maxCount) {
return "javascript";
} else {
return "aframe";
}
},
insertAFRAME: function(data){
let scene = document.createElement('a-entity')
scene.id = "embedAframe"
scene.innerHTML = data
let el = document.createElement('a-text')
el.setAttribute("value",data)
el.setAttribute("color","white")
el.setAttribute("align","center")
el.setAttribute("anchor","align")
let osbutton = this.wrapOSButton(el,"aframe",data)
AFRAME.scenes[0].appendChild(osbutton)
console.log(data)
},
insertText: function(data){
let el = document.createElement('a-text')
el.setAttribute("value",data)
el.setAttribute("color","white")
el.setAttribute("align","center")
el.setAttribute("anchor","align")
let osbutton = this.wrapOSButton(el,"text",data)
AFRAME.scenes[0].appendChild(osbutton)
console.log(data)
},
wrapOSButton: function(el,type,data){
let osbutton = document.createElement('a-entity')
let height = type == 'aframe' ? 0.3 : 0.1
let depth = type == 'aframe' ? 0.3 : 0.05
osbutton.setAttribute("osbutton",`width:0.3; height: ${height}; depth: ${depth}; color:blue `)
osbutton.appendChild(el)
osbutton.object3D.position.copy( this.getPositionInFrontOfCamera() )
return osbutton
},
getPositionInFrontOfCamera: function(distance){
const camera = this.el.sceneEl.camera;
let pos = new THREE.Vector3()
let direction = new THREE.Vector3();
// Get camera's forward direction (without rotation)
camera.getWorldDirection(direction);
camera.getWorldPosition(pos)
direction.normalize();
// Scale the direction by 1 meter
if( !distance ) distance = 1.5
direction.multiplyScalar(distance);
// Add the camera's position to the scaled direction to get the target point
pos.add(direction);
return pos
},
manifest: { // HTML5 manifest to identify app to xrsh
"short_name": "Paste",
"name": "Paste",
"icons": [
{
"src": "https://css.gg/clipboard.svg",
"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": "Paste the clipboard",
"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.
`
}
});

View File

@ -1,115 +0,0 @@
AFRAME.registerComponent('pastedrop', {
schema: {
foo: { type:"string"}
},
init: function () {
window.addEventListener('paste', this.onPaste.bind(this) )
document.body.addEventListener('dragover',(e) => e.preventDefault() )
document.body.addEventListener('drop', this.onDrop.bind(this) )
},
initClipboard: function(){
navigator.permissions.query({ name: 'clipboard-read' })
.then( (permission) => {
if( permission.state != 'granted' ){
this.el.sceneEl.exitVR()
setTimeout( () => this.paste(), 500 )
return
}else this.paste()
})
},
//getClipboard: function(){
// navigator.clipboard.readText()
// .then( async (base64) => {
// let mimetype = base64.replace(/;base64,.*/,'')
// let data = base64.replace(/.*;base64,/,'')
// let type = this.textHeuristic(data)
// const term = document.querySelector('[isoterminal]').components.isoterminal.term
// this.el.emit('pasteFile',{}) /*TODO* data incompatible */
// })
//},
onDrop: function(e){
e.preventDefault()
this.onPaste({...e, type: "paste", clipboardData: e.dataTransfer})
},
onPaste: function(e){
if( e.type != "paste" ) return
const clipboardData = e.clipboardData || navigator.clipboard;
const items = clipboardData.items;
for (let i = 0; i < items.length; i++) {
const item = items[i];
const type = item.type;
// Check if the item is a file
if (item.kind === "file") {
this.el.emit('pasteFile',{item,type})
} else if (type === "text/plain") {
const pastedText = clipboardData.getData("text/plain");
const newType = "text" // let /root/hook.d/mimetype/text further decide whether this is text/plain (or something else)
this.el.emit('pasteFile',{item,type:newType,pastedText})
}
}
},
manifest: { // HTML5 manifest to identify app to xrsh
"short_name": "Paste",
"name": "Paste",
"icons": [
{
"src": "https://css.gg/clipboard.svg",
"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": "Paste the clipboard",
"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.
`
}
});

View File

@ -34,22 +34,16 @@ AFRAME.registerComponent('selfcontainer', {
installProxyServer: function(){ installProxyServer: function(){
if( !window.store ) window.store = {} if( !window.store ) window.store = {}
// selfcontain every webrequest to store (and serve if stored) // selfcontain every webrequest to store (and serve if stored)
let curry = function(me){ let curry = function(me){
return function(request, response, cb){ return function(request, response, cb){
let data = request ? window.store[ request.url ] || false : false let data = request ? window.store[ request.url ] || false : false
if( data ){ // return inline version if( data ){ // return inline version
console.log('selfcontainer.js: serving '+request.url+' from cache') console.log('selfcontained cache: '+request.url)
let res = new Response() let res = new Response()
res[ data.binary ? 'data' : 'text' ] = data.binary ? () => me.convert.base64ToArrayBuffer(data.text) : data.text res[ data.binary ? 'data' : 'text' ] = data.binary ? () => me.convert.base64ToArrayBuffer(data.text) : data.text
cb(res) cb(res)
}else{ }else{
if( request.url.match(/(^file:\/\/xrsh)/) ) return cb(response)
console.log("selfcontainer.js: caching "+request.url)
if( response.text ){ if( response.text ){
data = {text: response.text} data = {text: response.text}
}else{ }else{

View File

@ -9,8 +9,7 @@ AFRAME.registerComponent('window', {
max: {type:'boolean',"default":false}, max: {type:'boolean',"default":false},
min: {type:'boolean',"default":false}, min: {type:'boolean',"default":false},
x: {type:'string',"default":"center"}, x: {type:'string',"default":"center"},
y: {type:'string',"default":"center"}, y: {type:'string',"default":"center"}
"class": {type:'array',"default":[]},
}, },
dependencies:{ dependencies:{
@ -29,7 +28,6 @@ AFRAME.registerComponent('window', {
this.el.dom.style.display = 'none' this.el.dom.style.display = 'none'
let winbox = this.el.winbox = new WinBox( this.data.title, { let winbox = this.el.winbox = new WinBox( this.data.title, {
class: this.data.class,
height:this.data.height, height:this.data.height,
width:this.data.width, width:this.data.width,
x: this.data.x, x: this.data.x,
@ -65,26 +63,9 @@ AFRAME.registerComponent('window', {
this.el.setAttribute("grabbable","") this.el.setAttribute("grabbable","")
if( this.el.object3D.position.x == 0 &&
this.el.object3D.position.y == 0 &&
this.el.object3D.position.z == 0 ){ // position next to previous window
var els = [...document.querySelectorAll('[window]')]
if( els.length < 2 ) return
let current = els[ els.length-1 ]
let last = els[ els.length-2 ]
AFRAME.utils.positionObjectNextToNeighbor( current.object3D , last.object3D, 0.02 )
}
}, },
show: function(state){ show: function(state){
this.el.dom.closest('.winbox').style.display = state ? '' : 'none' this.el.dom.closest('.winbox').style.display = state ? '' : 'none'
} }
}) })
AFRAME.utils.positionObjectNextToNeighbor = function positionObjectNextToNeighbor(object, lastNeighbor = null, margin ){
// *FIXME* this could be more sophisticated :)
object.position.x = lastNeighbor.position.x + margin
object.position.y = lastNeighbor.position.y - margin
object.position.z = lastNeighbor.position.z + margin
}

View File

@ -114,12 +114,10 @@ AFRAME.registerComponent('xterm', {
if( this.data.XRrenderer == 'canvas' ){ if( this.data.XRrenderer == 'canvas' ){
// setup slightly bigger black backdrop (this.el.getObject3D("mesh")) // setup slightly bigger black backdrop (this.el.getObject3D("mesh"))
// and terminal text (this.el.planeText.getObject("mesh")) // and terminal text (this.el.planeText.getObject("mesh"))
const w = 2; this.el.setAttribute("geometry",`primitive: box; width:2.07; height:${this.data.rows*5.3/this.data.cols}*2; depth: -0.12`)
const h = (this.data.rows*5/this.data.cols)
this.el.setAttribute("geometry",`primitive: box; width:${w}; height:${h}; depth: -0.12`)
this.el.setAttribute("material","shader:flat; color:black; opacity:0.5; transparent:true; ") this.el.setAttribute("material","shader:flat; color:black; opacity:0.5; transparent:true; ")
this.el.planeText = document.createElement('a-entity') this.el.planeText = document.createElement('a-entity')
this.el.planeText.setAttribute("geometry",`primitive: plane; width:${w}; height:${h}`) this.el.planeText.setAttribute("geometry",`primitive: plane; width:2; height:${this.data.rows*5/this.data.cols}*2`)
this.el.appendChild(this.el.planeText) this.el.appendChild(this.el.planeText)
// we switch between dom/canvas rendering because canvas looks pixely in nonimmersive mode // we switch between dom/canvas rendering because canvas looks pixely in nonimmersive mode