Compare commits

..

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

33 changed files with 1346 additions and 6400 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.emulator.read_file( this.data.file )
.then( this.isoterminal.convert.Uint8ArrayToString )
.then( (str) => {
console.log("creating editor")
this.createEditor( str )
})
.catch( (e) => {

View File

@ -59,6 +59,7 @@ if( !AFRAME.components.dom ){
.assignUniqueID()
.scaleDOMvsXR()
.triggerKeyboardForInputs()
.stubRequestAnimationFrame()
document.querySelector('#overlay').appendChild(this.el.dom)
this.el.emit('DOMready',{el: this.el.dom})
@ -130,5 +131,10 @@ if( !AFRAME.components.dom ){
return this
},
stubRequestAnimationFrame: async function(){
let s = await AFRAME.utils.require(this.requires)
this.el.setAttribute("requestAnimationFrameXR","")
}
})
}

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

@ -1,62 +1,51 @@
/*
*
* css/html template
*
* exit-AR
* com/dom com/window domrenderer exit-VR
*
*
* xterm.js
* com/isoterminal com/xterm.js com/html-as-texture-in-XR.js
* xterm.css
*
* plane textcanvas enter-VR
* enter-AR
* renderer=canvas
*
* ISOTerminal.js
*
* com/isoterminal/worker.js
*
* v86.js
* feat/*.js
* libv86.js
*
*
*
*
* NOTE: For convenience reasons, events are forwarded between com/isoterminal.js, worker.js and ISOTerminal
* Instead of a melting pot of different functionnames, events are flowing through everything (ISOTerminal.emit())
*/
function ISOTerminal(instance,opts){
// create a neutral isoterminal object which can be decorated
// with prototype functions and has addListener() and dispatchEvent()
let obj = new EventTarget()
obj.instance = instance
obj.opts = opts
// register default event listeners (enable file based features like isoterminal/jsconsole.js e.g.)
for( let event in ISOTerminal.listener )
for( let cb in ISOTerminal.listener[event] )
obj.addEventListener( event, ISOTerminal.listener[event][cb] )
// compose object with functions
for( let i in ISOTerminal.prototype ) obj[i] = ISOTerminal.prototype[i]
obj.emit('init')
return obj
}
ISOTerminal.prototype.emit = function(event,data){
data = data || false
this.dispatchEvent( new CustomEvent(event, {detail: data} ) )
}
ISOTerminal.addEventListener = (event,cb) => {
ISOTerminal.listener = ISOTerminal.listener || {}
ISOTerminal.listener[event] = ISOTerminal.listener[event] || []
ISOTerminal.listener[event].push(cb)
}
// ISOTerminal has defacto support for AFRAME
// but can be decorated to work without it as well
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": 120 },
rows: { type: 'number',"default": 30 },
padding: { type: 'number',"default": 18 },
minimized: { type: 'boolean',"default":false},
maximized: { type: 'boolean',"default":true},
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)
},
init: function(){
init: async 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,99 +59,74 @@ 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
core: "com/isoterminal/core.js",
utils_9p: "com/isoterminal/feat/9pfs_utils.js",
boot: "com/isoterminal/feat/boot.js",
jsconsole: "com/isoterminal/feat/jsconsole.js",
javascript: "com/isoterminal/feat/javascript.js",
indexhtml: "com/isoterminal/feat/index.html.js",
indexjs: "com/isoterminal/feat/index.js.js",
autorestore: "com/isoterminal/feat/autorestore.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 id="screen" style="white-space: pre; font: 14px monospace; "></div>
<canvas style="display: none"></canvas>
<div id="serial"></div>
</div>`,
css: (me) => `.isoterminal{
padding: ${me.com.data.padding}px;
width:100%;
height:90%;
position:relative;
}
.isoterminal div{
display:block;
position:relative;
line-height: ${me.com.data.lineHeight}px;
}
#term {
outline: none !important;
height:100%;
}
@font-face {
font-family: 'Cousine';
font-style: normal;
font-weight: 400;
src: url(./com/isoterminal/assets/Cousine.ttf) format('truetype');
src: url(./assets/Cousine.ttf) format('truetype');
}
@font-face {
font-family: 'Cousine';
font-style: normal;
font-weight: 700;
src: url(./com/isoterminal/assets/CousineBold.ttf) format('truetype');
.isoterminal *{
white-space: pre;
line-height:16px;
display:inline;
overflow: hidden;
}
.isoterminal *,
.xterm-dom-renderer-owner-1 .xterm-rows {
font-size: 14px;
font-family: "Cousine",Liberation Mono,DejaVu Sans Mono,Courier New,monospace;
font-weight:500 !important;
letter-spacing: 0 !important;
text-shadow: 0px 0px 10px #F075;
}
.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;
}
.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 div{ display:block; }
.isoterminal span{ display: inline }
@keyframes fade {
from { opacity: 1.0; }
@ -176,37 +140,22 @@ if( typeof AFRAME != 'undefined '){
to { opacity: 1.0; }
}
.blink{
.blink {
animation:fade 1000ms infinite;
-webkit-animation:fade 1000ms infinite;
}
`
},
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 ){
this.requires.xtermjs = "https://unpkg.com/@xterm/xterm@5.5.0/lib/xterm.js"
this.requires.xtermcss = "https://unpkg.com/@xterm/xterm@5.5.0/css/xterm.css"
this.requires.xterm = "com/isoterminal/feat/xterm.js"
}
await AFRAME.utils.require(this.requires)
await AFRAME.utils.require({ // ISOTerminal plugins
boot: "com/isoterminal/feat/boot.js",
javascript: "com/isoterminal/feat/javascript.js",
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",
})
let s = await AFRAME.utils.require(this.requires)
this.el.setAttribute("selfcontainer","")
@ -225,39 +174,44 @@ 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.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 = this.data.xterm ? 'width: 1024px; height:600px'
: 'width: 720px; height:455px'
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')
// 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.runISO(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")
setTimeout( () => { // important: after window maximize animation to get true size
instance.setAttribute("html-as-texture-in-xr", `domid: #${instance.uid}`) // only show aframe-html in xr
},1500)
})
this.term.addEventListener('status', function(e){
this.isoterminal.addEventListener('status', function(e){
let msg = e.detail
const w = instance.winbox
if(!w) return
@ -273,111 +227,19 @@ if( typeof AFRAME != 'undefined '){
instance.addEventListener('window.onresize', resize )
instance.addEventListener('window.onmaximize', resize )
const focus = (showdom) => (e) => {
this.el.emit('focus',e.detail)
if( this.el.components.window && this.data.renderer == 'canvas'){
this.el.components.window.show( showdom )
const focus = (e) => {
if( this.isoterminal?.emulator?.serial_adapter?.term ){
this.isoterminal.emulator.serial_adapter.term.focus()
}
}
instance.addEventListener('obbcollisionstarted', focus )
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) )
this.el.sceneEl.addEventListener('enter-vr', focus )
this.el.sceneEl.addEventListener('enter-ar', focus )
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:{
@ -388,6 +250,10 @@ if( typeof AFRAME != 'undefined '){
// reactive events for this.data updates
myvalue: function(e){ this.el.dom.querySelector('b').innerText = this.data.myvalue },
ready: function( ){
this.el.dom.style.display = 'none'
},
launcher: async function(){
this.initTerminal()
}

View File

@ -1,297 +0,0 @@
function ISOTerminal(instance,opts){
// create a neutral isoterminal object which can be decorated
// with prototype functions and has addListener() and dispatchEvent()
let obj = new EventTarget()
obj.instance = instance
obj.opts = opts
// register default event listeners (enable file based features like isoterminal/jsconsole.js e.g.)
for( let event in ISOTerminal.listener )
for( let cb in ISOTerminal.listener[event] )
obj.addEventListener( event, ISOTerminal.listener[event][cb] )
// compose object with functions
for( let i in ISOTerminal.prototype ) obj[i] = ISOTerminal.prototype[i]
obj.emit('init')
return obj
}
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] )
})
}
ISOTerminal.addEventListener = (event,cb) => {
ISOTerminal.listener = ISOTerminal.listener || {}
ISOTerminal.listener[event] = ISOTerminal.listener[event] || []
ISOTerminal.listener[event].push(cb)
}
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){
if( ttyNr == undefined) ttyNr = this.serial_input
if( ttyNr == undefined ){
if( this.emulator.serial_adapter ){
this.emulator.serial_adapter.term.paste(str)
}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})
}
)
})
}
}
ISOTerminal.prototype.convert = {
arrayBufferToBase64: function(buffer){
let binary = '';
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (let i = 0; i < len; i++) binary += String.fromCharCode(bytes[i]);
return btoa(binary);
},
base64ToArrayBuffer: function(base64) {
const binaryString = atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
},
toUint8Array: function(str) {
str = String(str) || String("")
// Create a new Uint8Array with the same length as the input string
const uint8Array = new Uint8Array(str.length);
// Iterate over the string and populate the Uint8Array
for (let i = 0; i < str.length; i++) {
uint8Array[i] = str.charCodeAt(i);
}
return uint8Array;
},
Uint8ArrayToString: function(arr){
const decoder = new TextDecoder('utf-8'); // Specify encoding
return decoder.decode(arr);
}
}
ISOTerminal.prototype.start = function(opts){
let me = this
this.opts = {...this.opts, ...opts}
let image = {}
if( opts.iso.match(/\.iso$/) ) image.cdrom = { url: opts.iso }
if( opts.iso.match(/\.bin$/) ) image.bzimage = { url: opts.iso }
opts = { ...image,
uart1:true, // /dev/ttyS1
uart2:true, // /dev/ttyS2
uart3:true, // /dev/ttyS3
wasm_path: "v86.wasm",
memory_size: opts.memory * 1024 * 1024,
vga_memory_size: 2 * 1024 * 1024,
//screen_container: opts.dom,
//serial_container: opts.dom,
bios: {
url: "bios/seabios.bin",
},
vga_bios: {
url: "bios/vgabios.bin",
//urg|: "com/isoterminal/bios/VGABIOS-lgpl-latest.bin",
},
network_relay_url: "wss://relay.widgetry.org/",
cmdline: "rw root=host9p rootfstype=9p rootflags=trans=virtio,cache=loose modules=virtio_pci tsc=reliable init_on_freg|=on vga=ask", //vga=0x122",
//bzimage_initrd_from_filesystem: true,
//filesystem: {
// baseurl: "com/isoterminal/v86/images/alpine-rootfs-flat",
// basefs: "com/isoterminal/v86/images/alpine-fs.json",
// },
//screen_dummy: true,
//disable_jit: false,
filesystem: {},
autostart: true,
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
}
ISOTerminal.prototype.startVM = function(opts){
this.emit('runISO',{...opts, bufferLatency: this.opts.bufferLatency })
const loading = [
'loading quantum bits and bytes',
'preparing quantum flux capacitors',
'crunching peanuts and chakras',
'preparing parallel universe',
'loading quantum state fluctuations',
'preparing godmode',
'loading cat pawns and cuteness',
'beaming up scotty',
'still faster than Windows update',
'loading a microlinux',
'figuring out meaning of life',
'Aligning your chakras now',
'Breathing in good vibes',
'Finding inner peace soon',
'Centering your Zen energy',
'Awakening third eye powers',
'Tuning into the universe',
'Balancing your cosmic karma',
'Stretching time and space',
'Recharging your soul battery',
'Transcending earthly limits'
]
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 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.addEventListener('emulator-started', async (e) => {
// OVERLAY FS *FIXME*
//if( me.opts.overlayfs ){
// fetch(me.opts.overlayfs)
// .then( (f) => {
// f.arrayBuffer().then( (buf) => {
// emulator.create_file('overlayfs.zip', new Uint8Array(buf) )
// })
// })
//}
let line = ''
this.ready = false
this.addEventListener(`serial0-output-string`, async (e) => {
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 )
})
});
}
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()
this.buffer.str += String.fromCharCode(byte)
if( !this.buffer.id ){
cb(this.buffer.str) // send out leading call
this.buffer = resetBuffer()
this.buffer.id = setTimeout( () => { // accumulate succesive calls
if( this.buffer.str ) cb(this.buffer.str)
this.buffer = resetBuffer()
}, this.latency || 250)
}
}
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

Binary file not shown.

View File

@ -1,16 +0,0 @@
--2024-09-23 17:49:04-- https://fonts.gstatic.com/s/cousine/v27/d6lNkaiiRdih4SpP9Z8K6T4.ttf
Resolving fonts.gstatic.com (fonts.gstatic.com)... 142.250.180.195, 2a00:1450:400d:806::2003
Connecting to fonts.gstatic.com (fonts.gstatic.com)|142.250.180.195|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 287924 (281K) [font/ttf]
Saving to: d6lNkaiiRdih4SpP9Z8K6T4.ttf
0K .......... .......... .......... .......... .......... 17% 2.31M 0s
50K .......... .......... .......... .......... .......... 35% 1.91M 0s
100K .......... .......... .......... .......... .......... 53% 9.09M 0s
150K .......... .......... .......... .......... .......... 71% 3.03M 0s
200K .......... .......... .......... .......... .......... 88% 3.40M 0s
250K .......... .......... .......... . 100% 12.4M=0.09s
2024-09-23 17:49:05 (3.23 MB/s) - d6lNkaiiRdih4SpP9Z8K6T4.ttf saved [287924/287924]

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

@ -0,0 +1,193 @@
//ISOTerminal.prototype.exec(cmd_array,stdin){
// // exec(['lua'] "print \"hello\") ---> cat /dev/browser/js/stdin | lua > /dev/browser/js/stdout
//}
ISOTerminal.prototype.serial_input = undefined; // can be set to 0,1,2,3 to define stdinput tty (xterm plugin)
ISOTerminal.prototype.exec = function(shellscript){
//let ts = String(Date.now())+".job"
//this.emulator.create_file(ts, this.toUint8Array(shellscript) )
this.send(shellscript+"\n",1)
}
ISOTerminal.prototype.send = function(str, ttyNr){
if( !ttyNr ) ttyNr = this.serial_input
if( !ttyNr ){
if( this.emulator.serial_adapter ){
this.emulator.serial_adapter.term.paste(str)
}else this.emulator.keyboard_send_text(str) // vga screen
}else{
this.convert.toUint8Array( str ).map( (c) => this.emulator.bus.send(`serial${ttyNr}-input`, c ) )
}
}
ISOTerminal.prototype.convert = {
arrayBufferToBase64: function(buffer){
let binary = '';
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (let i = 0; i < len; i++) binary += String.fromCharCode(bytes[i]);
return window.btoa(binary);
},
base64ToArrayBuffer: function(base64) {
const binaryString = window.atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
},
toUint8Array: function(str) {
str = String(str) || String("")
// Create a new Uint8Array with the same length as the input string
const uint8Array = new Uint8Array(str.length);
// Iterate over the string and populate the Uint8Array
for (let i = 0; i < str.length; i++) {
uint8Array[i] = str.charCodeAt(i);
}
return uint8Array;
},
Uint8ArrayToString: function(arr){
const decoder = new TextDecoder('utf-8'); // Specify encoding
return decoder.decode(arr);
}
}
ISOTerminal.prototype.runISO = function(opts){
let me = this
this.opts = {...this.opts, ...opts}
let image = {}
if( opts.iso.match(/\.iso$/) ) image.cdrom = { url: opts.iso }
if( opts.iso.match(/\.bin$/) ) image.bzimage = { url: opts.iso }
opts = { ...image,
uart1:true, // /dev/ttyS1
uart2:true, // /dev/ttyS2
uart3:true, // /dev/ttyS3
wasm_path: "com/isoterminal/v86.wasm",
memory_size: opts.memory * 1024 * 1024,
vga_memory_size: 2 * 1024 * 1024,
screen_container: opts.dom,
//serial_container: opts.dom,
bios: {
url: "com/isoterminal/bios/seabios.bin",
},
vga_bios: {
url: "com/isoterminal/bios/vgabios.bin",
//urg|: "com/isoterminal/bios/VGABIOS-lgpl-latest.bin",
},
network_relay_url: "wss://relay.widgetry.org/",
cmdline: "rw root=host9p rootfstype=9p rootflags=trans=virtio,cache=loose modules=virtio_pci tsc=reliable init_on_freg|=on vga=ask", //vga=0x122",
//bzimage_initrd_from_filesystem: true,
//filesystem: {
// baseurl: "com/isoterminal/v86/images/alpine-rootfs-flat",
// basefs: "com/isoterminal/v86/images/alpine-fs.json",
// },
//screen_dummy: true,
//disable_jit: false,
filesystem: {},
autostart: true,
};
this.emit('runISO',opts)
let emulator = this.emulator = new V86(opts)
const loading = [
'loading quantum bits and bytes',
'preparing quantum flux capacitors',
'crunching peanuts and chakras',
'preparing parallel universe',
'loading quantum state fluctuations',
'preparing godmode',
'loading cat pawns and cuteness',
'beaming up scotty',
'still faster than Windows update',
'loading a microlinux',
'figuring out meaning of life',
'Aligning your chakras now',
'Breathing in good vibes',
'Finding inner peace soon',
'Centering your Zen energy',
'Awakening third eye powers',
'Tuning into the universe',
'Balancing your cosmic karma',
'Stretching time and space',
'Recharging your soul battery',
'Transcending earthly limits'
]
let loadmsg = loading[ Math.floor(Math.random()*1000) % loading.length ]
this.emit('status',loadmsg)
// replace welcome message https://github.com/copy/v86/blob/3c77b98bc4bc7a5d51a2056ea73d7666ca50fc9d/src/browser/serial.js#L231
let welcome = "This is the serial console. Whatever you type or paste here will be sent to COM1"
let motd = "\r"
let msg = `${loadmsg}, please wait..`
while( msg.length < welcome.length ) msg += " "
msg += "\n"
motd += msg+"\033[0m"
emulator.bus.register("emulator-started", async (e) => {
this.emit('emulator-started',e)
if( emulator.serial_adapter ){
emulator.serial_adapter.term.clear()
emulator.serial_adapter.term.write(motd)
}
if( me.opts.overlayfs ){
fetch(me.opts.overlayfs)
.then( (f) => {
f.arrayBuffer().then( (buf) => {
emulator.create_file('overlayfs.zip', new Uint8Array(buf) )
})
})
}
let line = ''
let ready = false
emulator.add_listener(`serial0-output-byte`, async (byte) => {
this.emit('${this.serial}-output-byte',byte)
var chr = String.fromCharCode(byte);
if(chr < " " && chr !== "\n" && chr !== "\t" || chr > "~")
{
return;
}
if(chr === "\n")
{
var new_line = line;
line = "";
}
else if(chr >= " " && chr <= "~")
{
line += chr;
}
if( !ready && line.match(/^(\/ #|~%|\[.*\]>)/) ){
this.emit('postReady',e)
setTimeout( () => this.emit('ready',e), 500 )
ready = true
}
});
});
}
ISOTerminal.prototype.readFromPipe = function(filename,cb){
this.emulator.add_listener("9p-write-end", async (opts) => {
if ( opts[0] == filename.replace(/.*\//,'') ){
const buf = await this.emulator.read_file("console.tty")
cb( this.convert.Uint8ArrayToString(buf) )
}
})
}

View File

@ -1,95 +1,46 @@
let emulator = this.emulator
let me = this
ISOTerminal.addEventListener('emulator-started', function(){
let emulator = this.emulator
let isoterminal = this
emulator.fs9p.update_file = async function(file,data){
const convert = ISOTerminal.prototype.convert
emulator.fs9p.update_file = async function(file,data){
const p = this.SearchPath(file);
const p = this.SearchPath(file);
if(p.id === -1)
{
return emulator.create_file(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) )
try{
if(p.id === -1)
{
return emulator.create_file(file,data)
}
const inode = this.GetInode(p.id);
const buf = typeof data == 'string' ? isoterminal.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;
isoterminal.exec(`touch ${file}`) // update inode
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){
const convert = ISOTerminal.prototype.convert
}
const p = this.SearchPath(file);
emulator.fs9p.append_file = async function(file,data){
if(p.id === -1)
{
return Promise.resolve(null);
}
const inode = this.GetInode(p.id);
const buf = typeof data == 'string' ? convert.toUint8Array(data) : data
await this.Write(p.id, inode.size, buf.length, buf )
// update inode
inode.size = inode.size + buf.length
const now = Math.round(Date.now() / 1000);
inode.atime = inode.mtime = now;
return new Promise( (resolve,reject) => resolve(buf) )
const p = this.SearchPath(file);
}
if(p.id === -1)
{
return Promise.resolve(null);
}
const inode = this.GetInode(p.id);
const buf = typeof data == 'string' ? isoterminal.convert.toUint8Array(data) : data
await this.Write(p.id, inode.size, buf.length, buf )
// update inode
inode.size = inode.size + buf.length
const now = Math.round(Date.now() / 1000);
inode.atime = inode.mtime = now;
return new Promise( (resolve,reject) => resolve(buf) )
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,72 +1,52 @@
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.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")
})
}
ISOTerminal.addEventListener('emulator-started', function(e){
this.autorestore(e)
})
ISOTerminal.prototype.autorestore = async function(e){
localforage.setDriver([
localforage.INDEXEDDB,
localforage.WEBSQL,
localforage.LOCALSTORAGE
]).then( () => {
localforage.getItem("state", async (err,stateBase64) => {
if( !err && confirm('continue last session?') ){
this.noboot = true // see feat/boot.js
state = this.convert.base64ToArrayBuffer( stateBase64 )
this.emulator.restore_state(state)
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.save = async () => {
const state = await this.emulator.save_state()
console.log( String(this.convert.arrayBufferToBase64(state)).substr(0,5) )
localforage.setItem("state", this.convert.arrayBufferToBase64(state) )
}
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
return confirmationMessage; //Webkit, Safari, Chrome
});
}else{
// inside browser-thread
ISOTerminal.addEventListener('emulator-started', function(e){
this.autorestore(e)
})
ISOTerminal.prototype.autorestore = async function(e){
localforage.setDriver([
localforage.INDEXEDDB,
localforage.WEBSQL,
localforage.LOCALSTORAGE
]).then( () => {
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) }
}
})
this.save = async () => {
await this.worker.save_state()
}
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
return confirmationMessage; //Webkit, Safari, Chrome
});
})
}
}

View File

@ -4,23 +4,24 @@ 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]+'"') )
}
if( typeof document.location[i] == 'string' )
env.push( 'export '+String(i).toUpperCase()+'="'+document.location[i]+'"')
}
this.worker.create_file("profile.browser", this.convert.toUint8Array( env.join('\n') ) )
await this.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")
}
}
if( this.emulator.serial_adapter ) this.emulator.serial_adapter.term.focus()
else{
let els = [...document.querySelectorAll("div#screen")]
els.map( (el) => el.focus() )
}
}

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

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

View File

@ -1,30 +1,32 @@
if( typeof emulator != 'undefined' ){
// inside worker-thread
ISOTerminal.addEventListener('init', function(){
// unix to js device
this.emulator.readFromPipe( 'dev/browser/js', async (data) => {
const buf = await emulator.read_file("dev/browser/js")
const decoder = new TextDecoder('utf-8');
const js = decoder.decode(buf).replace(/^#!\/bin\/js/,'') // remove leftover shebangs if any
try{
this.postMessage({event:'runJavascript',data:[js]})
}catch(e){
console.error(e)
}
})
this.addEventListener('emulator-started', function(e){
}else{
// inside browser-thread
const emulator = this.emulator
ISOTerminal.prototype.runJavascript = function(js){
let $root = document.querySelector("script#root")
if( !$root ){
$root = document.createElement("script")
$root.id = "root"
document.body.appendChild($root)
}
$root.innerHTML = js
}
// unix to js device
this.readFromPipe( '/mnt/index.js', async (data) => {
const buf = await emulator.read_file("index.js")
const decoder = new TextDecoder('utf-8');
const js = decoder.decode(buf)
try{
this.runJavascript(js)
}catch(e){
console.error(e)
}
})
})
})
ISOTerminal.prototype.runJavascript = function(js){
let $root = document.querySelector("script#root")
if( !$root ){
$root = document.createElement("script")
$root.id = "root"
document.body.appendChild($root)
}
$root.innerHTML = js
}

View File

@ -1,43 +1,29 @@
if( typeof emulator != 'undefined' ){
// inside worker-thread
ISOTerminal.addEventListener('init', function(){
// unix to js device
emulator.readFromPipe( 'dev/browser/js', async (data) => {
const convert = ISOTerminal.prototype.convert
const buf = await this.emulator.read_file("dev/browser/js")
const script = convert.Uint8ArrayToString(buf)
let PID=null
try{
if( script.match(/^PID/) ){
PID = script.match(/^PID=([0-9]+);/)[1]
this.addEventListener('emulator-started', function(e){
const emulator = this.emulator
// unix to js device
this.readFromPipe( '/mnt/js', async (data) => {
const buf = await emulator.read_file("js")
const script = this.convert.Uint8ArrayToString(buf)
let PID="?"
try{
if( script.match(/^PID/) ){
PID = script.match(/^PID=([0-9]+);/)[1]
}
let res = (new Function(`${script}`))()
if( res && typeof res != 'string' ) res = JSON.stringify(res,null,2)
// write output to 9p with PID as filename
// *FIXME* not flexible / robust
emulator.create_file(PID, this.convert.toUint8Array(res) )
}catch(e){
console.error(e)
}
this.postMessage({event:'javascript-eval',data:{script,PID}})
}catch(e){
console.error(e)
}
})
}else{
// inside browser-thread
})
ISOTerminal.addEventListener('javascript-eval', async 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
}
}
// update output to 9p with PID as filename (in /mnt/run)
if( PID ){
this.worker.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);
};
@ -27,6 +27,7 @@ ISOTerminal.prototype.redirectConsole = function(handler){
}
ISOTerminal.addEventListener('emulator-started', function(){
let emulator = this.emulator
this.redirectConsole( (str,prefix) => {
let finalStr = ""
@ -34,19 +35,22 @@ 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])
emulator.fs9p.append_file( "console", finalStr )
})
window.addEventListener('error', function(event) {
if( event.filename ){
console.error(event.filename+":"+event.lineno+":"+event.colno)
console.error(event.message);
console.error(event.error);
}else console.error(event)
console.error(event.filename+":"+event.lineno+":"+event.colno)
console.error(event.message);
console.error(event.error);
});
window.addEventListener('unhandledrejection', function(event) {
console.error(event);
console.error('Unhandled promise rejection:', event.reason);
});
// enable/disable logging file (echo 1 > mnt/console.tty)
this.readFromPipe( '/mnt/console.tty', (data) => {
emulator.log_to_tty = ( String(data).trim() == '1')
})
})

View File

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

View File

@ -25,50 +25,9 @@ ISOTerminal.prototype.xtermInit = function(){
term.select(0, 0, 0)
isoterm.emit('status','copied to clipboard')
})
term.onRender( () => {
// xterm relies on requestAnimationFrame (which does not called in immersive mode)
let _window = term._core._coreBrowserService._window
if( !_window._XRSH_proxied ){ // patch the planet!
//_window.requestAnimationFrameAFRAME = function(cb){
// if( term.tid != null ) clearTimeout(term.tid)
// term.tid = setTimeout( function(){
// console.log("render")
// cb()
// term.tid = null
// },100)
//}
this.i = 0
const requestAnimationFrameAFRAME = AFRAME.utils.throttleLeadingAndTrailing(
function(cb){ cb() }
,150
)
// we proxy the _window object of xterm, and reroute
// requestAnimationFrame to requestAnimationFrameAFRAME
_window_new = new Proxy(_window,{
get(me,k){
if( k == '_XRSH_proxied' ) return true
if( k == 'requestAnimationFrame' ){
return requestAnimationFrameAFRAME.bind(me)
}
return typeof me[k] == 'function' ? me[k].bind(me) : me[k]
},
set(me,k,v){
me[k] = v
return true
}
})
term._core._coreBrowserService._window = _window_new
}
})
return term
}
this.addEventListener('emulator-started', function(){
this.emulator.serial_adapter.term.element.querySelector('.xterm-viewport').style.background = 'transparent'
// toggle immersive with ESCAPE
@ -77,9 +36,7 @@ ISOTerminal.prototype.xtermInit = function(){
const resize = (w,h) => {
setTimeout( () => {
if( isoterm?.emulator?.serial_adapter?.term ){
isoterm.xtermAutoResize(isoterm.emulator.serial_adapter.term, isoterm.instance,-3)
}
isoterm.xtermAutoResize(isoterm.emulator.serial_adapter.term, isoterm.instance,-3)
},800) // wait for resize anim
}
isoterm.instance.addEventListener('window.onresize', resize )

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -1,87 +0,0 @@
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)
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
let emulator = this.emulator = new V86(opts);
console.log("[worker.js] started emulator")
// event forwarding
emulator.add_listener("serial0-output-byte", function(byte){
ISOTerminal.prototype.bufferOutput(byte, (str) => { // we buffer to prevent framerate dropping
if( !str ) return
this.postMessage({event:"serial0-output-string",data:str});
}, opts.bufferLatency )
}.bind(this));
emulator.add_listener("serial1-output-byte", function(byte){
ISOTerminal.prototype.bufferOutput(byte, (str) => { // we buffer to prevent framerate dropping
if( !str ) return
this.postMessage({event:"serial1-output-string",data:str});
}, opts.bufferLatency )
}.bind(this));
emulator.add_listener("serial2-output-byte", function(byte){
ISOTerminal.prototype.bufferOutput(byte, (str) => { // we buffer to prevent framerate dropping
if( !str ) return
this.postMessage({event:"serial2-output-string",data:str});
}, opts.bufferLatency )
}.bind(this));
emulator.add_listener("emulator-started", function(){
importScripts("feat/9pfs_utils.js")
this.postMessage({event:"emulator-started",data:false});
}.bind(this));
/*
* 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]) }
// filename will be read from 9pfs: "/mnt/"+filename
emulator.readFromPipe = function(filename,cb){
emulator.add_listener("9p-write-end", async (opts) => {
if ( opts[0] == filename.replace(/.*\//,'') ){
cb()
}
})
}
importScripts("feat/javascript.js")
importScripts("feat/index.html.js")
importScripts("feat/autorestore.js")
}
/*
* 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.onmessage = async 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}})
}
}
}
}

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

@ -0,0 +1,50 @@
/*
* ## requestAnimationFrameXR
*
* reroutes requestAnimationFrame-calls to xrSession.requestAnimationFrame
* reason: in immersive mode this function behaves differently
* (causing HTML apps like xterm.js not getting updated due to relying
* on window.requestAnimationFrame)
*
* ```html
* <a-entity requestAnimationFrameXR dom/>
* ```
*/
if( !AFRAME.systems.requestAnimationFrameXR ){
AFRAME.registerSystem('requestAnimationFrameXR',{
init: function init(){
if( document.location.hostname.match(/localhost/) ) return // allow webxr polyfill during development (they hang in XR)
AFRAME.systems.requestAnimationFrameXR.q = []
this.sceneEl.addEventListener('enter-vr', this.enable )
this.sceneEl.addEventListener('enter-ar', this.enable )
this.sceneEl.addEventListener('exit-vr', this.disable )
this.sceneEl.addEventListener('exit-ar', this.disable )
},
enable: function enable(){
this.requestAnimationFrame = window.requestAnimationFrame
// NOTE: we don't call xrSession.requestAnimationFrame directly like this:
//
// window.requestAnimationFrame = AFRAME.utils.throttleTick( (cb) => this.sceneEl.xrSession.requestAnimationFrame(cb), 50 )
//
// as that breaks webxr polyfill (for in-browser testing)
// instead we defer calls to tick() (which is called both in XR and non-XR)
//
window.requestAnimationFrame = (cb) => AFRAME.systems.requestAnimationFrameXR.q.push(cb)
const q = AFRAME.systems.requestAnimationFrameXR.q
this.tick = AFRAME.utils.throttleTick( () => {
while( q.length != 0 ) (q.pop())()
},50)
},
disable: function disable(){
delete this.tick
window.requestAnimationFrame = this.requestAnimationFrame
}
})
}

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

81
com/xrfragments.js Normal file
View File

@ -0,0 +1,81 @@
AFRAME.registerComponent('xrfragments', {
schema: {
url: { type:"string"}
},
init: function () {
},
events:{
launcher: async function(){
let url = prompt('enter URL to glb/fbx/json/obj/usdz asset', 'https://xrfragment.org/index.glb')
if( !url ) return
await AFRAME.utils.require({
xrfragments: "https://xrfragment.org/dist/xrfragment.aframe.js",
})
// remove objects which are marked to be removed from scene (with noxrf)
let els = [...document.querySelectorAll('[noxrf]') ]
els.map( (el) => el.remove() )
if( !this.el.getAttribute("xrf") ){
this.el.setAttribute("xrf", url )
let ARbutton = document.querySelector('.a-enter-ar-button')
if( ARbutton ){
ARbutton.addEventListener('click', () => {
AFRAME.XRF.reset()
})
}
}else AFRAME.XRF.navigator.to(url)
}
},
manifest: { // HTML5 manifest to identify app to xrsh
"short_name": "XRF",
"name": "XR Fragment URL",
"icons": [ ],
"id": "/?source=pwa",
"start_url": "/?source=pwa",
"background_color": "#3367D6",
"display": "standalone",
"scope": "/",
"theme_color": "#3367D6",
"shortcuts": [
{
"name": "What is the latest news?",
"cli":{
"usage": "helloworld <type> [options]",
"example": "helloworld news",
"args":{
"--latest": {type:"string"}
}
},
"short_name": "Today",
"description": "View weather information for today",
"url": "/today?source=pwa",
"icons": [{ "src": "/images/today.png", "sizes": "192x192" }]
}
],
"description": "Hello world information",
"screenshots": [
{
"src": "/images/screenshot1.png",
"type": "image/png",
"sizes": "540x720",
"form_factor": "narrow"
}
],
"help":`
Helloworld application
This is a help file which describes the application.
It will be rendered thru troika text, and will contain
headers based on non-punctualized lines separated by linebreaks,
in above's case "\nHelloworld application\n" will qualify as header.
`
}
});

View File

@ -1,268 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2019
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* 2019 Mauve Ranger
* 2024 Leon van Kammen
*/
let terminalInstance = 0
const TERMINAL_THEME = {
theme_foreground: {
// 'default': '#ffffff'
},
theme_background: {
// 'default': '#000'
},
theme_cursor: {
// 'default': '#ffffff'
},
theme_selection: {
// 'default': 'rgba(255, 255, 255, 0.3)'
},
theme_black: {
// 'default': '#000000'
},
theme_red: {
// 'default': '#e06c75'
},
theme_brightRed: {
// 'default': '#e06c75'
},
theme_green: {
// 'default': '#A4EFA1'
},
theme_brightGreen: {
// 'default': '#A4EFA1'
},
theme_brightYellow: {
// 'default': '#EDDC96'
},
theme_yellow: {
// 'default': '#EDDC96'
},
theme_magenta: {
// 'default': '#e39ef7'
},
theme_brightMagenta: {
// 'default': '#e39ef7'
},
theme_cyan: {
// 'default': '#5fcbd8'
},
theme_brightBlue: {
// 'default': '#5fcbd8'
},
theme_brightCyan: {
// 'default': '#5fcbd8'
},
theme_blue: {
// 'default': '#5fcbd8'
},
theme_white: {
// 'default': '#d0d0d0'
},
theme_brightBlack: {
// 'default': '#808080'
},
theme_brightWhite: {
// 'default': '#ffffff'
}
}
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 }
}, TERMINAL_THEME),
write: function(message) {
this.term.write(message)
},
init: function () {
const terminalElement = document.createElement('div')
terminalElement.setAttribute('style', `
width: 800px;
height: ${Math.floor( 800 * 0.527 )}px;
overflow: hidden;
`)
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)
// Build up a theme object
const theme = Object.keys(this.data).reduce((theme, key) => {
if (!key.startsWith('theme_')) return theme
const data = this.data[key]
if(!data) return theme
theme[key.slice('theme_'.length)] = data
return theme
}, {})
this.fontSize = 14
const term = this.term = new Terminal({
logLevel:"off",
theme: theme,
allowTransparency: true,
cursorBlink: true,
disableStdin: false,
rows: this.data.rows,
cols: this.data.cols,
fontFamily: 'Cousine, monospace',
fontSize: this.fontSize,
lineHeight: 1.15,
useFlowControl: true,
rendererType: this.renderType // 'dom' // 'canvas'
})
this.term.open(terminalElement)
this.term.focus()
this.setRenderType('dom')
terminalElement.querySelector('.xterm-viewport').style.background = 'transparent'
// now we can scale canvases to the parent element
const $screen = terminalElement.querySelector('.xterm-screen')
$screen.style.width = '100%'
term.on('refresh', AFRAME.utils.throttleLeadingAndTrailing( () => this.update(), 150 ) )
term.on('data', (data) => {
this.el.emit('xterm-input', data)
})
this.el.addEventListener('serial-output-byte', (e) => {
const byte = e.detail
var chr = String.fromCharCode(byte);
this.term.write(chr)
})
this.el.addEventListener('serial-output-string', (e) => {
this.term.write(e.detail)
})
},
update: function(){
if( this.renderType == 'canvas' ){
const material = this.el.planeText.getObject3D('mesh').material
if (!material.map ) return
if( this.cursorCanvas ) this.canvasContext.drawImage(this.cursorCanvas, 0,0)
else console.log("no cursorCanvas")
material.map.needsUpdate = true
//material.needsUpdate = true
}
},
setRenderType: function(type){
if( type.match(/(dom|canvas)/) ){
if( type == 'dom'){
this.el.dom.appendChild(this.el.terminalElement)
this.term.setOption('fontSize', this.fontSize )
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('rendererType',type )
this.renderType = type
this.update()
setTimeout( () => {
this.canvas = this.el.terminalElement.querySelector('.xterm-text-layer')
this.canvas.id = "xterm-canvas"
this.canvasContext = this.canvas.getContext('2d')
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.needsUpdate = true; // Ensure the texture updates
let plane = this.el.planeText.getObject3D("mesh") //this.el.getObject3D('mesh')
if( plane.material ) plane.material.dispose()
plane.material = new THREE.MeshBasicMaterial({
map: canvasTexture, // Set the texture from the canvas
transparent: false, // Set transparency
//side: THREE.DoubleSide // Set to double-sided rendering
//blending: THREE.AdditiveBlending
});
this.el.object3D.scale.x = 0.2
this.el.object3D.scale.y = 0.2
this.el.object3D.scale.z = 0.2
},100)
}
this.el.terminalElement.style.opacity = type == 'canvas' ? 0 : 1
}
},
enterImmersive: function(){
if( this.mode == 'immersive' ) return
this.el.object3D.visible = true
this.mode = "immersive"
this.setRenderType('canvas')
this.term.focus()
},
exitImmersive: function(){
if( this.mode == 'nonimmersive' ) return
this.el.object3D.visible = false
this.mode = "nonimmersive"
this.setRenderType('dom')
},
})