Compare commits

..

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

23 changed files with 477 additions and 5070 deletions

View File

@ -4,8 +4,6 @@ AFRAME.registerComponent('codemirror', {
schema: {
file: { type:"string"},
term: { type:"selector", default: "[isoterminal]" },
width: { type:"number", default:700},
height: { type:"number", default:500},
},
init: function () {
@ -14,7 +12,7 @@ AFRAME.registerComponent('codemirror', {
if( this.data.file && this.data.file[0] != '/'){
this.data.file = "root/"+this.data.file
}
this.isoterminal = this.data.term.components.isoterminal.term
this.isoterminal = this.data.term.components.isoterminal.isoterminal
//this.el.innerHTML = ` `
this.requireAll()
},
@ -34,9 +32,8 @@ AFRAME.registerComponent('codemirror', {
html: (me) => `<div class="codemirror">
</div>`,
css: (me) => `.CodeMirror{
width: ${me.com.data.width}px !important;
height: ${me.com.data.height-30}px !important;
css: (me) => `.codemirror{
width:100%;
}
.codemirror *{
font-size: 14px;
@ -45,11 +42,7 @@ AFRAME.registerComponent('codemirror', {
letter-spacing: 0 !important;
text-shadow: 0px 0px 10px #F075;
}
.wb-body:has(> .codemirror){
overflow:hidden;
}
.wb-body + .codemirror{ overflow:hidden; }
.CodeMirror {
margin-top:18px;
}
@ -60,7 +53,7 @@ AFRAME.registerComponent('codemirror', {
},
createEditor: function(value){
this.el.setAttribute("window", `title: codemirror; uid: ${this.el.dom.id}; attach: #overlay; dom: #${this.el.dom.id}; width: ${this.data.width}px; height: ${this.data.height}px`)
this.el.setAttribute("window", `title: codemirror; uid: ${this.el.dom.id}; attach: #overlay; dom: #${this.el.dom.id};`)
this.editor = CodeMirror( this.el.dom, {
value,
mode: "htmlmixed",
@ -74,54 +67,33 @@ AFRAME.registerComponent('codemirror', {
}
})
this.editor.setOption("theme", "shadowfox")
this.editor.updateFile = AFRAME.utils.throttle( (file,str) => {
this.updateFile(file,str)
}, 1500)
this.editor.updateFile = AFRAME.utils.throttleLeadingAndTrailing( (file,str) => {
this.updateFile(file,str),
2000
})
this.editor.on('change', (instance,changeObj) => {
this.editor.updateFile( this.data.file, instance.getValue() )
})
this
.handleFocus()
setTimeout( () => {
this.el.setAttribute("html-as-texture-in-xr", `domid: #${this.el.dom.id}`) // only show aframe-html in xr
},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){
// we don't do via shellcmd: isoterminal.exec(`echo '${str}' > ${file}`,1)
// as it would require all kindof ugly stringescaping
console.log("updating "+file)
await this.isoterminal.worker.update_file(file, this.isoterminal.convert.toUint8Array(str) )
this.isoterminal.exec("touch "+file) // *FIXME* notify filesystem (why does inotifyd need this? v86's 9pfees is cached?)
await this.isoterminal.emulator.fs9p.update_file( file, str)
},
events:{
// component events
DOMready: function(e){
this.isoterminal.worker.read_file(this.data.file)
this.isoterminal.worker.postMessage.promise({event:'read_file',data: this.data.file })
.then( this.isoterminal.convert.Uint8ArrayToString )
.then( (str) => {
console.log("creating editor")
this.createEditor( str )
})
.catch( (e) => {

View File

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

View File

@ -12,7 +12,7 @@
*
* plane textcanvas enter-VR
* enter-AR
* renderer=canvas
*
*
* ISOTerminal.js
*
@ -33,30 +33,22 @@ if( typeof AFRAME != 'undefined '){
AFRAME.registerComponent('isoterminal', {
schema: {
iso: { type:"string", "default":"https://forgejo.isvery.ninja/assets/xrsh-buildroot/main/xrsh.iso" },
overlayfs: { type:"string"},
width: { type: 'number',"default": -1 },
height: { type: 'number',"default": -1 },
depth: { type: 'number',"default": 0.03 },
lineHeight: { type: 'number',"default": 18 },
padding: { type: 'number',"default": 18 },
maximized: { type: 'boolean',"default":true},
muteUntilPrompt:{ type: 'boolean',"default":true}, // mute stdout until a prompt is detected in ISO
HUD: { type: 'boolean',"default":false}, // link to camera movement
transparent: { type:'boolean', "default":false }, // need good gpu
memory: { type: 'number', "default":40 }, // VM memory (in MB) [NOTE: quest or smartphone might crash > 40mb ]
bufferLatency: { type: 'number', "default":1 }, // in ms: bufferlatency from webworker to xterm (batch-update every char to texture)
debug: { type: 'boolean', "default":false }
iso: { type:"string", "default":"https://forgejo.isvery.ninja/assets/xrsh-buildroot/main/xrsh.iso" },
overlayfs: { type:"string"},
cols: { type: 'number',"default": 80 },
rows: { type: 'number',"default": 20 },
padding: { type: 'number',"default": 18 },
minimized: { type: 'boolean',"default":false},
maximized: { type: 'boolean',"default":false},
transparent: { type:'boolean', "default":false }, // need good gpu
xterm: { type: 'boolean', "default":true }, // use xterm.js? (=slower)
memory: { type: 'number', "default":48 }, // 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":300 } // in ms: time between canvas re-draws
},
init: function(){
this.el.object3D.visible = false
this.calculateDimension()
this.initHud()
this.setupBox()
this.setupPasteDrop()
fetch(this.data.iso,{method: 'HEAD'})
.then( (res) => {
if( res.status != 200 ) throw 'not found'
@ -70,44 +62,33 @@ if( typeof AFRAME != 'undefined '){
},
requires:{
com: "com/dom.js",
window: "com/window.js",
pastedrop: "com/pastedrop.js",
v86: "com/isoterminal/libv86.js",
vt100: "com/isoterminal/VT100.js",
com: "com/dom.js",
window: "com/window.js",
v86: "com/isoterminal/libv86.js",
// allow xrsh to selfcontain scene + itself
xhook: "com/lib/xhook.min.js",
selfcontain: "com/selfcontainer.js",
xhook: "https://jpillora.com/xhook/dist/xhook.min.js",
selfcontain: "com/selfcontainer.js",
// html to texture
htmlinxr: "com/html-as-texture-in-xr.js",
// isoterminal global features
PromiseWorker: "com/isoterminal/PromiseWorker.js",
ISOTerminal: "com/isoterminal/ISOTerminal.js",
localforage: "com/isoterminal/localforage.js",
htmlinxr: "com/html-as-texture-in-xr.js",
// isoterminal features
ISOTerminal: "com/isoterminal/ISOTerminal.js",
localforage: "https://cdn.rawgit.com/mozilla/localForage/master/dist/localforage.js"
},
dom: {
scale: 0.66,
//scale: 0.5,
events: ['click','keydown'],
html: (me) => `<div class="isoterminal">
<div id="term" tabindex="0">
<pre></pre>
</div>
</div>`,
css: (me) => `.isoterminal{
padding: ${me.com.data.padding}px;
width:100%;
height:90%;
position:relative;
height:100%;
}
.isoterminal div{
display:block;
position:relative;
line-height: ${me.com.data.lineHeight}px;
}
#term {
outline: none !important;
}
@font-face {
font-family: 'Cousine';
@ -124,44 +105,42 @@ if( typeof AFRAME != 'undefined '){
.isoterminal style{ display:none }
blink{
border:none;
padding:none;
}
#overlay .winbox:has(> .isoterminal){
background:transparent;
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){
background: #000C;
background: #000C;
overflow:hidden;
border-radius:7px;
}
.XR .wb-body:has(> .isoterminal){
background: transparent;
}
.XR .isoterminal{
.XR .wb-body:has(> .isoterminal){
background: #000;
}
.isoterminal *{
font-size: 14px;
font-family: "Cousine",Liberation Mono,DejaVu Sans Mono,Courier New,monospace;
font-weight:500 !important;
text-shadow: 0px 0px 10px #F075;
.isoterminal *,
.isoterminal .xterm-dom-renderer-owner-1 .xterm-rows {
background:transparent !important;
font-size: 14px;
font-family: "Cousine",Liberation Mono,DejaVu Sans Mono,Courier New,monospace;
font-weight:500 !important;
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 {
@ -176,6 +155,7 @@ if( typeof AFRAME != 'undefined '){
to { opacity: 1.0; }
}
.isoterminal .xterm-rows.xterm-focus .xterm-cursor.xterm-cursor-block,
.blink{
animation:fade 1000ms infinite;
-webkit-animation:fade 1000ms infinite;
@ -186,15 +166,19 @@ if( typeof AFRAME != 'undefined '){
initTerminal: async function(singleton){
// why not latest xterm or v3.12 with builtin-canvas support?
// first versions used 1.5.4, a typescript rewrite which:
// * 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 allow switching between dom/canvas
// * only allows a standalone WebGL addon (conflicts with THREE)
// * heavily dependent on requestAnimationFrame (conflicts with THREE)
// * typescript-rewrite results in ~300k lib (instead of 96k)
// * v3.12 had slightly better performance but still very heavy
if( this.data.xterm ){
// why 3.12?
// first versions used 1.5.4, a typescript rewrite which:
// * 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 allow switching between dom/canvas
// * only allows a standalone WebGL addon (conflicts with THREE)
// * heavily dependent on requestAnimationFrame (conflicts with THREE)
// * typescript-rewrite results in ~300k lib (instead of 96k)
this.requires.xtermcss = "//unpkg.com/xterm@3.12.0/dist/xterm.css",
this.requires.xtermjs = "//unpkg.com/xterm@3.12.0/dist/xterm.js",
this.requires.xtermcss = "com/xterm.js"
}
await AFRAME.utils.require(this.requires)
await AFRAME.utils.require({ // ISOTerminal plugins
@ -203,9 +187,7 @@ if( typeof AFRAME != 'undefined '){
jsconsole: "com/isoterminal/feat/jsconsole.js",
indexhtml: "com/isoterminal/feat/index.html.js",
indexjs: "com/isoterminal/feat/index.js.js",
autorestore: "com/isoterminal/feat/autorestore.js",
pastedropFeat: "com/isoterminal/feat/pastedrop.js",
httpfs: "com/isoterminal/feat/httpfs.js",
autorestore: "com/isoterminal/feat/autorestore.js",
})
this.el.setAttribute("selfcontainer","")
@ -225,39 +207,42 @@ if( typeof AFRAME != 'undefined '){
}
// init isoterminal
this.term = new ISOTerminal(instance,this.data)
this.isoterminal = new ISOTerminal(instance,this.data)
instance.addEventListener('DOMready', () => {
this.setupVT100(instance)
setTimeout( () => {
instance.setAttribute("html-as-texture-in-xr", `domid: #term; faceuser: true`)
},100)
//instance.setAttribute("html-as-texture-in-xr", `domid: #${this.el.dom.id}`)
//instance.winbox.resize(720,380)
let size = `width: ${this.data.width}; height: ${this.data.height}`
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`)
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}`)
})
instance.addEventListener('window.oncreate', (e) => {
instance.dom.classList.add('blink')
instance.setAttribute("xterm",`cols: ${this.data.cols}; rows: ${this.data.rows}; canvasLatency: ${this.data.canvasLatency}`)
instance.addEventListener("xterm-input", (e) => this.isoterminal.send(e.detail,0) )
// run iso
let opts = {dom:instance.dom}
for( let i in this.data ) opts[i] = this.data[i]
opts.cols = this.cols
opts.rows = this.rows
this.term.start(opts)
this.isoterminal.start(opts)
})
instance.setAttribute("dom", "")
instance.setAttribute("pastedrop", "")
instance.setAttribute("dom", "")
this.term.addEventListener('ready', (e) => {
this.isoterminal.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.isoterminal.addEventListener('ready', (e)=>{
instance.dom.classList.remove('blink')
this.term.emit('status',"running")
if( this.data.debug ) this.runTests()
this.isoterminal.emit('status',"running")
})
this.term.addEventListener('status', function(e){
this.isoterminal.addEventListener('status', function(e){
let msg = e.detail
const w = instance.winbox
if(!w) return
@ -274,13 +259,14 @@ if( typeof AFRAME != 'undefined '){
instance.addEventListener('window.onmaximize', resize )
const focus = (showdom) => (e) => {
this.el.emit('focus',e.detail)
if( this.el.components.window && this.data.renderer == 'canvas'){
if( this.el.components.xterm ){
this.el.components.xterm.term.focus()
}
if( this.el.components.window ){
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) )
@ -289,96 +275,6 @@ if( typeof AFRAME != 'undefined '){
instance.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera
},
initHud: function(){
if( AFRAME.utils.device.isMobile() ) this.data.HUD = true
if( this.data.HUD ){
document.querySelector('[camera]').appendChild( this.el )
this.el.setAttribute("position","0 -0.03 -0.4")
}
},
runTests: async function(){
await AFRAME.utils.require({
"test_util": "tests/util.js",
"test_isoterminal":"tests/ISOTerminal.js"
})
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:{
// combined AFRAME+DOM reactive events

View File

@ -17,14 +17,12 @@ function ISOTerminal(instance,opts){
ISOTerminal.prototype.emit = function(event,data,sender){
data = data || false
const evObj = new CustomEvent(event, {detail: data} )
this.preventFrameDrop( () => {
// 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.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 !== undefined && typeof this[event] == 'function' ) this[event].apply(this, data && data.push ? data : [data] )
})
// 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.dispatchEvent( evObj )
if( sender != "instance" && this.instance ) this.instance.dispatchEvent(evObj)
if( sender != "worker" && this.worker ) this.worker.postMessage({event,data})
if( sender !== undefined && typeof this[event] == 'function' ) this[event].apply(this, data && data.push ? data : [data] )
}
ISOTerminal.addEventListener = (event,cb) => {
@ -37,10 +35,6 @@ ISOTerminal.prototype.exec = function(shellscript){
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.send = function(str, ttyNr){
@ -51,11 +45,7 @@ ISOTerminal.prototype.send = function(str, ttyNr){
}else this.emulator.keyboard_send_text(str) // vga screen
}else{
this.convert.toUint8Array( str ).map( (c) => {
this.preventFrameDrop(
() => {
this.worker.postMessage({event:`serial${ttyNr}-input`,data:c})
}
)
this.worker.postMessage({event:`serial${ttyNr}-input`,data:c})
})
}
}
@ -67,11 +57,11 @@ ISOTerminal.prototype.convert = {
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (let i = 0; i < len; i++) binary += String.fromCharCode(bytes[i]);
return btoa(binary);
return window.btoa(binary);
},
base64ToArrayBuffer: function(base64) {
const binaryString = atob(base64);
const binaryString = window.atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
@ -134,30 +124,52 @@ ISOTerminal.prototype.start = function(opts){
//disable_jit: false,
filesystem: {},
autostart: true,
debug: this.opts.debug ? true : false
};
this
.setupWorker(opts)
.startVM(opts)
}
ISOTerminal.prototype.setupWorker = function(opts){
/*
* the WebWorker (which runs v86)
*
*/
this.worker = new PromiseWorker( "com/isoterminal/worker.js", (cb,event,data) => {
if( !data.promiseId ) this.emit(event,data,"worker") // forward event to world
this.preventFrameDrop( cb(event,data) )
})
return this
}
this.worker = new Worker("com/isoterminal/worker.js");
this.worker.onmessage = (e) => {
const xr = this.instance.sceneEl.renderer.xr
const {event,data} = e.data
const cb = (event,data) => () => {
if( data.promiseId ){
this.workerPromise.resolver(data) // forward to promise resolver
}else this.emit(event,data,"worker") // forward event to world
}
// don't let workers cause framerate dropping
if( xr.isPresenting ){
xr.getSession().requestAnimationFrame(cb(event,data))
}else{
window.requestAnimationFrame(cb(event,data))
}
}
/*
* postMessage.promise basically performs this.worker.postMessage
* in a promise way (to easily retrieve async output)
*/
this.worker.postMessage.promise = function(data){
if( typeof data != 'object' ) data = {data}
this.resolvers = this.resolvers || {}
this.id = this.id == undefined ? 0 : this.id
data.id = this.id++
// Send id and task to WebWorker
this.worker.postMessage(data)
return new Promise(resolve => this.resolvers[data.id] = resolve);
}.bind(this.worker.postMessage)
this.worker.postMessage.promise.resolver = function(data){
if( !data || !data.promiseId ) throw 'promiseId not given'
this.resolvers[ data.promiseId ](data);
delete this.resolvers[ data.promiseId ]; // Prevent memory leak
}.bind(this.worker.postMessage)
ISOTerminal.prototype.startVM = function(opts){
this.emit('runISO',{...opts, bufferLatency: this.opts.bufferLatency })
const loading = [
@ -184,54 +196,12 @@ ISOTerminal.prototype.startVM = function(opts){
'Transcending earthly limits'
]
const empower = [
"FOSS gives users control over their software, offering freedom to modify and share",
"Feeling powerless with tech? FOSS escapes a mindset known as learned helplessness",
"FOSS breaks this cycle by showing that anyone can learn and contribute",
"Proprietary software can make users dependent, but FOSS offers real choices",
"FOSS communities provide support and encourage users to develop new skills",
"Learned helplessness fades when we realize tech isnt too complex to understand",
"FOSS empowers users to customize and improve their tools",
"Engaging with FOSS helps build confidence and self-reliance in tech",
"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",
"Many lightweight Linux distributions run smoothly on older hardware",
"Installing Linux on aging devices keeps them functional instead of sending them to the landfill",
"Linux uses fewer resources, making it ideal for reusing older machines",
"By using Linux, you can avoid buying new hardware, cutting down on tech waste",
"Instead of discarding slow devices, Linux can bring them back to life",
"Linux supports a wide range of devices, helping to prevent e-waste",
"Open-source drivers in Linux enable compatibility with old peripherals, reducing the need for replacements",
"Free Linux software helps users avoid planned obsolescence in commercial products",
"Switching to Linux promotes sustainability by reducing demand for new gadgets and lowering e-waste"
]
const motd = `
\r . . ____ _____________ ________. ._. ._. . .
\r . . .\\ \\/ /\\______ \\/ _____// | \\. .
\r . . . \\ / | _/\\_____ \\/ ~ \\ .
\r . . . / \\ | | \\/ \\ Y / .
\r . . ./___/\\ \\ |____|_ /_______ /\\___|_ /. .
\r . . . . . .\\_/. . . . \\/ . . . .\\/ . . _ \\/ . .
\r https://xrsh.isvery.ninja ▬▬▬▬▬▬▬▬▬▬▬▬
\r local-first, polyglot, unixy WebXR IDE & runtime
\r
\r credits
\r -------
\r @nlnet@nlnet.nl
\r @lvk@mastodon.online
\r @utopiah@mastodon.pirateparty.be
\r https://www.w3.org/TR/webxr
\r https://three.org
\r https://aframe.org
`
const loadmsg = loading[ Math.floor(Math.random()*1000) % loading.length ] + "..(please wait..)"
const text_color = "\r"
const text_reset = "\033[0m"
const loadmsg = "\n\r "+loading[ Math.floor(Math.random()*1000) % loading.length ] + "..[please wait]"
const empowermsg = "\n\r "+text_reset+'"'+empower[ Math.floor(Math.random()*1000) % empower.length ] + '"\n\r'
this.emit('status',loadmsg)
this.emit('serial-output-string', motd + empowermsg + text_color + loadmsg + text_reset+"\n\r")
this.emit('serial-output-string', text_color + loadmsg + text_reset + "\n\r")
this.addEventListener('emulator-started', async (e) => {
@ -252,25 +222,17 @@ ISOTerminal.prototype.startVM = function(opts){
const str = e.detail
// lets scan for a prompt so we can send a 'ready' event to the world
if( !this.ready && str.match(/\n(\/ #|~%|\[.*\]>)/) ) this.postBoot()
if( this.ready || !this.opts.muteUntilPrompt ) this.emit('serial-output-string', e.detail )
if( !this.ready && str.match(/\n(\/ #|~%|\[.*\]>)/) ){
this.emit('postReady',{})
this.ready = true
setTimeout( () => this.emit('ready',{}), 500 )
}
if( this.ready ) this.emit('serial-output-string', e.detail )
})
});
}
ISOTerminal.prototype.postBoot = function(cb){
this.emit('postReady',{})
this.ready = true
setTimeout( () => {
this.emit('ready',{})
if( cb ) cb()
}, 500 )
}
// this is allows (unsophisticated) outputbuffering
ISOTerminal.prototype.bufferOutput = function(byte,cb,latency){
const resetBuffer = () => ({str:""})
this.buffer = this.buffer || resetBuffer()
@ -285,13 +247,3 @@ ISOTerminal.prototype.bufferOutput = function(byte,cb,latency){
}
}
ISOTerminal.prototype.preventFrameDrop = function(cb){
// don't let workers cause framerate dropping
const xr = this.instance.sceneEl.renderer.xr
if( xr.isPresenting ){
xr.getSession().requestAnimationFrame(cb)
}else{
window.requestAnimationFrame(cb)
}
}

View File

@ -1,77 +0,0 @@
/*
* This is basically a Javascript Proxy for 'new Worker()'
* which allows calling worker-functions via promises.
*
* It's basically comlink without the fat.
*
* const w = new PromiseWorker("worker.js", (cb,event_or_fn,data) => {
* cb(event_or_fn,data) // decorate/ratelimit/hooks here
* })
* w.foo().then( console.dir )
*
* in worker.js define a function: this.foo = () => return {foo:"Bar"}
*/
function PromiseWorker(file, onmessage){
let proxy
const worker = new Worker(file)
worker.onmessage = (e) => { // handle messages originating from worker
const {event,data} = e.data // this is worker.onmessage(...)
const cb = (event,data) => () => { //
if( data.promiseId ){ //
proxy.resolver(data) // forward to promise resolver
}
}
onmessage(cb,event,data)
}
return proxy = new Proxy(this,{
get(me,k){
if( k.match(/(postMessage|onmessage)/) ) return worker[k].bind(worker)
if( k.match(/(resolver|promise)/) ) return this[k].bind(this)
// promisify postMessage(...) call
return function(){
return this.promise(me,{event: k, data: [...arguments] })
}
},
promise(me,msg){
if( typeof msg != 'object' || !msg.event || !msg.data ){
throw 'worker.promise({event:..,data:..}) did not receive correct msg : '+JSON.stringify(msg)
}
this.resolvers = this.resolvers || {last:1,pending:{}}
msg.data.promiseId = this.resolvers.last++
// Send id and task to WebWorker
worker.postMessage(msg, PromiseWorker.prototype.getTransferable(msg.data) )
return new Promise( resolve => this.resolvers.pending[ msg.data.promiseId ] = resolve );
},
resolver(data){
if( !data || !data.promiseId ) throw 'promiseId not given'
this.resolvers.pending[ data.promiseId ](data.result);
delete this.resolvers.pending[ data.promiseId ]; // Prevent memory leak
}
})
}
PromiseWorker.prototype.getTransferable = function(data){
let objs = []
function isTransferable(obj) {
return obj instanceof ArrayBuffer ||
obj instanceof MessagePort ||
obj instanceof ImageBitmap ||
(typeof OffscreenCanvas !== 'undefined' && obj instanceof OffscreenCanvas) ||
(typeof ReadableStream !== 'undefined' && obj instanceof ReadableStream) ||
(typeof WritableStream !== 'undefined' && obj instanceof WritableStream) ||
(typeof TransformStream !== 'undefined' && obj instanceof TransformStream);
}
for( var i in data ){
if( isTransferable(data[i]) ) objs.push(data[i])
}
if( objs.length ) debugger
return objs.length ? objs : undefined
}

File diff suppressed because it is too large Load Diff

View File

@ -12,20 +12,15 @@ emulator.fs9p.update_file = async function(file,data){
}
const inode = this.GetInode(p.id);
const buf = typeof data == 'string' ? convert.toUint8Array(data) : data || ""
if( buf.length == 0 ) return new Promise( (resolve,reject) => resolve(data) )
const buf = typeof data == 'string' ? convert.toUint8Array(data) : data
await this.Write(p.id,0, buf.length, buf )
// update inode
inode.size = buf.length
const now = Math.round(Date.now() / 1000);
inode.atime = inode.mtime = now;
me.postMessage({event:'exec',data:[`touch /mnt/${file}`]}) // update inode
return new Promise( (resolve,reject) => resolve(buf) )
try{
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) )
}catch(e){
console.error({file,data})
return new Promise( (resolve,reject) => reject(e) )
}
}
emulator.fs9p.append_file = async function(file,data){
@ -49,47 +44,5 @@ 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;
}
this['fs9p.append_file'] = function(){ emulator.fs9p.append_file.apply(emulator.fs9p, arguments[0]) }
this['fs9p.update_file'] = function(){ emulator.fs9p.update_file.apply(emulator.fs9p, arguments[0]) }

View File

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

View File

@ -4,21 +4,18 @@ ISOTerminal.addEventListener('ready', function(e){
ISOTerminal.prototype.boot = async function(e){
// set environment
let env = [
`export LINES=${this.opts.rows}`,
`export COLUMNS=${this.opts.cols}`,
'export BROWSER=1',
]
let env = ['export BROWSER=1']
for ( let i in document.location ){
if( typeof document.location[i] == 'string' ){
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.noboot ){
this.send("source /etc/profile # \\o/ FOSS powa!\n")
let boot = "source /etc/profile\n"
this.send(boot+"\n")
}
}

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

@ -2,31 +2,22 @@
if( typeof emulator != 'undefined' ){
// inside worker-thread
this.listenIndexHTML = () => {
const file = "dev/browser/html"
emulator.readFromPipe( file, async (data) => {
const buf = await emulator.read_file( file )
const decoder = new TextDecoder('utf-8');
const html = decoder.decode(buf).replace(/^#!\/bin\/html/,'') // remove leftover shebangs if any
try{
this.postMessage({event:'runHTML',data:[html]})
}catch(e){
console.error(file)
console.error(e)
}
})
}
// unix to js device
this.emulator.readFromPipe( 'root/index.html', async (data) => {
const buf = await emulator.read_file("root/index.html")
const decoder = new TextDecoder('utf-8');
const html = decoder.decode(buf).replace(/^#!\/bin\/html/,'') // remove leftover shebangs if any
try{
this.postMessage({event:'runHTML',data:[html]})
}catch(e){
console.error(e)
}
})
}else{
// inside browser-thread
ISOTerminal.addEventListener('emulator-started', function(){
this.addEventListener('ready', async () => {
this.worker.listenIndexHTML()
})
})
ISOTerminal.prototype.runHTML = function(html){
let $scene = document.querySelector("a-scene")
let $root = document.querySelector("a-entity#root")
@ -35,7 +26,6 @@ if( typeof emulator != 'undefined' ){
$root.id = "root"
$scene.appendChild($root)
}
console.log(html)
$root.innerHTML = html
}

View File

@ -2,8 +2,8 @@ if( typeof emulator != 'undefined' ){
// inside worker-thread
// unix to js device
this.emulator.readFromPipe( 'dev/browser/js', async (data) => {
const buf = await emulator.read_file("dev/browser/js")
this.emulator.readFromPipe( 'root/index.js', async (data) => {
const buf = await emulator.read_file("root/index.js")
const decoder = new TextDecoder('utf-8');
const js = decoder.decode(buf).replace(/^#!\/bin\/js/,'') // remove leftover shebangs if any
try{

View File

@ -6,7 +6,7 @@ if( typeof emulator != 'undefined' ){
const convert = ISOTerminal.prototype.convert
const buf = await this.emulator.read_file("dev/browser/js")
const script = convert.Uint8ArrayToString(buf)
let PID=null
let PID="?"
try{
if( script.match(/^PID/) ){
PID = script.match(/^PID=([0-9]+);/)[1]
@ -20,24 +20,12 @@ if( typeof emulator != 'undefined' ){
}else{
// inside browser-thread
ISOTerminal.addEventListener('javascript-eval', async function(e){
ISOTerminal.addEventListener('javascript-eval', function(e){
const {script,PID} = e.detail
let res
try{
res = (new Function(`${script}`))()
if( res && typeof res != 'string' ) res = JSON.stringify(res,null,2)
}catch(e){
console.error(e)
console.info(script)
res = "error: "+e.toString()
if( e.filename ){
res += "\n"+e.filename+":"+e.lineno+":"+e.colno
}
}
let res = (new Function(`${script}`))()
if( res && typeof res != 'string' ) res = JSON.stringify(res,null,2)
// update output to 9p with PID as filename (in /mnt/run)
if( PID ){
this.worker.update_file(`run/${PID}`, this.convert.toUint8Array(res) )
}
this.emit('fs9p.update_file', [`run/${PID}`, this.convert.toUint8Array(res)] )
})
}

View File

@ -9,17 +9,17 @@ ISOTerminal.prototype.redirectConsole = function(handler){
log.apply(log, args);
};
console.error = (...args)=>{
const textArg = args[0]
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]
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]
const textArg = args[0].message?args[0].message:args[0];
handler(textArg+'\n','\x1b[38;5;208mwarn\x1b[0m');
err.apply(log, args);
};
@ -34,7 +34,7 @@ ISOTerminal.addEventListener('emulator-started', function(){
str.trim().split("\n").map( (line) => {
finalStr += '\x1b[38;5;165m/dev/browser: \x1b[0m'+prefix+line+'\n'
})
this.emit('append_file', ["/dev/browser/console",finalStr])
this.emit('fs9p.append_file', ["/dev/browser/console",finalStr])
})
window.addEventListener('error', function(event) {
@ -46,6 +46,7 @@ ISOTerminal.addEventListener('emulator-started', function(){
});
window.addEventListener('unhandledrejection', function(event) {
console.error('Unhandled promise rejection:')
console.error(event);
});

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

@ -1,15 +1,41 @@
importScripts("libv86.js");
importScripts("ISOTerminal.js") // we don't instance it again here (just use its functions)
this.runISO = function(opts){
this.opts = opts
if( opts.debug ) console.dir(opts)
//var emulator = new V86({
// wasm_path: "../build/v86.wasm",
// memory_size: 32 * 1024 * 1024,
// vga_memory_size: 2 * 1024 * 1024,
// bios: {
// url: "../bios/seabios.bin",
// },
// vga_bios: {
// url: "../bios/vgabios.bin",
// },
// cdrom: {
// url: "../images/linux4.iso",
// },
// autostart: true,
//});
//
//
//emulator.add_listener("serial0-output-byte", function(byte)
//{
// var chr = String.fromCharCode(byte);
// this.postMessage(chr);
//}.bind(this));
//
//this.onmessage = function(e)
//{
// emulator.serial0_send(e.data);
//};
this.runISO = function(opts){
if( opts.cdrom && !opts.cdrom.url.match(/^http/) ) opts.cdrom.url = "../../"+opts.cdrom.url
if( opts.bzimage && !opts.cdrom.url.match(/^http/) ) opts.bzimage.url = "../../"+opts.bzimage.url
console.dir(opts)
let emulator = this.emulator = new V86(opts);
console.log("[worker.js] started emulator")
console.log("worker:started emulator")
// event forwarding
@ -42,11 +68,8 @@ this.runISO = function(opts){
/*
* forward events/functions so non-worker world can reach them
*/
this.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.read_file_world = async function(){ return emulator.fs9p.read_file_world.apply(emulator.fs9p, arguments[0]) }
this.append_file = async function(){ emulator.fs9p.append_file.apply(emulator.fs9p, arguments[0]) }
this.update_file = async function(){ emulator.fs9p.update_file.apply(emulator.fs9p, arguments[0]) }
this['emulator.create_file'] = function(){ emulator.create_file.apply(emulator, arguments[0]) }
this['emulator.read_file'] = function(){ emulator.read_file.apply(emulator, arguments[0]) }
// filename will be read from 9pfs: "/mnt/"+filename
emulator.readFromPipe = function(filename,cb){
@ -65,23 +88,11 @@ this.runISO = function(opts){
* forward events/functions so non-worker world can reach them
*/
this['serial0-input'] = function(c){ emulator.bus.send( 'serial0-input', c) } // to /dev/ttyS0
this['serial1-input'] = function(c){ emulator.bus.send( 'serial1-input', c) } // to /dev/ttyS1
this['serial2-input'] = function(c){ emulator.bus.send( 'serial2-input', c) } // to /dev/ttyS2
this['serial0-input'] = function(c){ this.emulator.bus.send( 'serial0-input', c) } // to /dev/ttyS0
this['serial1-input'] = function(c){ this.emulator.bus.send( 'serial1-input', c) } // to /dev/ttyS1
this['serial2-input'] = function(c){ this.emulator.bus.send( 'serial2-input', c) } // to /dev/ttyS2
this.onmessage = async function(e){
this.onmessage = function(e){
let {event,data} = e.data
if( this[event] ){
if( this.opts?.debug ) console.log(`[worker.js] this.${event}(${JSON.stringify(data).substr(0,60)})`)
try{
let result = await this[event](data)
if( data.promiseId ){ // auto-callback to ISOTerminal.worker.promise(...)
this.postMessage({event,data: {...data,result}})
}
}catch(e){
if( data.promiseId ){ // auto-callback to ISOTerminal.worker.promise(...)
this.postMessage({event,data: {...data,error:e}})
}
}
}
if( this[event] ) this[event](data)
}

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

View File

@ -9,8 +9,7 @@ AFRAME.registerComponent('window', {
max: {type:'boolean',"default":false},
min: {type:'boolean',"default":false},
x: {type:'string',"default":"center"},
y: {type:'string',"default":"center"},
"class": {type:'array',"default":[]},
y: {type:'string',"default":"center"}
},
dependencies:{
@ -29,7 +28,6 @@ AFRAME.registerComponent('window', {
this.el.dom.style.display = 'none'
let winbox = this.el.winbox = new WinBox( this.data.title, {
class: this.data.class,
height:this.data.height,
width:this.data.width,
x: this.data.x,
@ -65,26 +63,9 @@ AFRAME.registerComponent('window', {
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){
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

@ -92,7 +92,6 @@ const TERMINAL_THEME = {
AFRAME.registerComponent('xterm', {
schema: Object.assign({
XRrenderer: { type: 'string', default: 'canvas', },
cols: { type: 'number', default: 110, },
rows: { type: 'number', default: Math.floor( (window.innerHeight * 0.7 ) * 0.054 ) },
canvasLatency:{ type:'number', default: 200 }
@ -104,40 +103,26 @@ AFRAME.registerComponent('xterm', {
init: function () {
const terminalElement = document.createElement('div')
terminalElement.setAttribute('style', `
width: 800px;
height: ${Math.floor( 800 * 0.527 )}px;
width: ${Math.floor( window.innerWidth * 0.7 )}px;
height: ${Math.floor( window.innerHeight * 0.7 )}px;
overflow: hidden;
`)
// setup slightly bigger black backdrop (this.el.getObject3D("mesh"))
// and terminal text (this.el.planeText.getObject("mesh"))
this.el.setAttribute("geometry",`primitive: box; width:2.07; height:${this.data.rows*5.3/this.data.cols}*2; depth: -0.12`)
this.el.setAttribute("material","shader:flat; color:black; opacity:0.5; transparent:true; ")
this.el.planeText = document.createElement('a-entity')
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.terminalElement = terminalElement
if( this.data.XRrenderer == 'canvas' ){
// setup slightly bigger black backdrop (this.el.getObject3D("mesh"))
// and terminal text (this.el.planeText.getObject("mesh"))
const w = 2;
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.planeText = document.createElement('a-entity')
this.el.planeText.setAttribute("geometry",`primitive: plane; width:${w}; height:${h}`)
this.el.appendChild(this.el.planeText)
// we switch between dom/canvas rendering because canvas looks pixely in nonimmersive mode
this.el.sceneEl.addEventListener('enter-vr', this.enterImmersive.bind(this) )
this.el.sceneEl.addEventListener('enter-ar', this.enterImmersive.bind(this) )
this.el.sceneEl.addEventListener('exit-vr', this.exitImmersive.bind(this) )
this.el.sceneEl.addEventListener('exit-ar', this.exitImmersive.bind(this) )
}
this.tick = AFRAME.utils.throttleLeadingAndTrailing( () => {
if( this.el.sceneEl.renderer.xr.isPresenting ){
// workaround
// xterm relies on window.requestAnimationFrame (which is not called WebXR immersive mode)
//this.term._core.viewport._innerRefresh()
this.term._core.renderer._renderDebouncer._innerRefresh()
}
}, this.data.canvasLatency)
// we switch between dom/canvas rendering because canvas looks pixely in nonimmersive mode
this.el.sceneEl.addEventListener('enter-vr', this.enterImmersive.bind(this) )
this.el.sceneEl.addEventListener('enter-ar', this.enterImmersive.bind(this) )
this.el.sceneEl.addEventListener('exit-vr', this.exitImmersive.bind(this) )
this.el.sceneEl.addEventListener('exit-ar', this.exitImmersive.bind(this) )
// Build up a theme object
const theme = Object.keys(this.data).reduce((theme, key) => {
@ -148,8 +133,6 @@ AFRAME.registerComponent('xterm', {
return theme
}, {})
this.fontSize = 14
const term = this.term = new Terminal({
logLevel:"off",
theme: theme,
@ -158,13 +141,20 @@ AFRAME.registerComponent('xterm', {
disableStdin: false,
rows: this.data.rows,
cols: this.data.cols,
fontFamily: 'Cousine, monospace',
fontSize: this.fontSize,
fontSize: 14,
lineHeight: 1.15,
useFlowControl: true,
rendererType: this.renderType // 'dom' // 'canvas'
})
this.tick = AFRAME.utils.throttle( () => {
if( this.el.sceneEl.renderer.xr.isPresenting ){
// workaround
// xterm relies on window.requestAnimationFrame (which is not called WebXR immersive mode)
//this.term._core.viewport._innerRefresh()
this.term._core.renderer._renderDebouncer._innerRefresh()
}
}, this.data.canvasLatency)
this.term.open(terminalElement)
this.term.focus()
this.setRenderType('dom')
@ -175,7 +165,7 @@ AFRAME.registerComponent('xterm', {
const $screen = terminalElement.querySelector('.xterm-screen')
$screen.style.width = '100%'
term.on('refresh', AFRAME.utils.throttleLeadingAndTrailing( () => this.update(), 150 ) )
term.on('refresh', AFRAME.utils.throttle( () => this.update(), 150 ) )
term.on('data', (data) => {
this.el.emit('xterm-input', data)
})
@ -210,14 +200,14 @@ AFRAME.registerComponent('xterm', {
if( type == 'dom'){
this.el.dom.appendChild(this.el.terminalElement)
this.term.setOption('fontSize', this.fontSize )
this.term.setOption('fontSize', 14 )
this.term.setOption('rendererType',type )
this.renderType = type
}
if( type == 'canvas'){
this.el.appendChild(this.el.terminalElement)
this.term.setOption('fontSize', this.fontSize * 3 )
this.term.setOption('fontSize', 48 )
this.term.setOption('rendererType',type )
this.renderType = type
this.update()
@ -228,8 +218,8 @@ AFRAME.registerComponent('xterm', {
this.cursorCanvas = this.el.terminalElement.querySelector('.xterm-cursor-layer')
// Create a texture from the canvas
const canvasTexture = new THREE.Texture(this.canvas)
//canvasTexture.minFilter = THREE.NearestFilter //LinearFilter
//canvasTexture.magFilter = THREE.LinearMipMapLinearFilter //THREE.NearestFilter //LinearFilter
//canvasTexture.minFilter = THREE.LinearFilter
//canvasTexture.magFilter = THREE.LinearFilter
canvasTexture.needsUpdate = true; // Ensure the texture updates
let plane = this.el.planeText.getObject3D("mesh") //this.el.getObject3D('mesh')
if( plane.material ) plane.material.dispose()