Compare commits

..

No commits in common. "main" and "feat/javascript-gateway" have entirely different histories.

50 changed files with 2042 additions and 2594 deletions

3
.gitignore vendored
View File

@ -1,3 +0,0 @@
com/isoterminal/mnt/hook.d
com/isoterminal/mnt/console*
com/isoterminal/mnt/dev/*

View File

@ -11,11 +11,3 @@ This is a library of useful AFRAME components which can be used in any AFRAME ap
<a-entity helloworld="foo:1" class="cubes" name="box">
```
## Funding
This project is funded through [NGI0 Entrust](https://nlnet.nl/entrust), a fund established by [NLnet](https://nlnet.nl) with financial support from the European Commission's [Next Generation Internet](https://ngi.eu) program. Learn more at the [NLnet project page](https://nlnet.nl/project/xrsh).
[<img src="https://nlnet.nl/logo/banner.png" alt="NLnet foundation logo" width="20%" />](https://nlnet.nl)
[<img src="https://nlnet.nl/image/logos/NGI0_tag.svg" alt="NGI Zero Logo" width="20%" />](https://nlnet.nl/entrust)

View File

@ -0,0 +1,75 @@
AFRAME.registerComponent('aframe-inspector', {
schema: {
foo: { type:"string"}
},
init: function () {
document.querySelector('a-scene').setAttribute("inspector","url: https://cdn.jsdelivr.net/gh/aframevr/aframe-inspector@master/dist/aframe-inspector.min.js")
},
requires:{ },
events:{
// component events
ready: function(e){ },
launcher: function(e){
$('[inspector]').components.inspector.openInspector()
},
},
manifest: { // HTML5 manifest to identify app to xrsh
"short_name": "inspector",
"name": "AFRAME inspector",
"icons": [
{
"src": "https://css.gg/list-tree.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": "use CTRL+ALT+i to launch inspector",
"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.
`
}
});

182
com.refactor/isoterminal.js Normal file
View File

@ -0,0 +1,182 @@
AFRAME.registerComponent('isoterminal', {
schema: {
cols: { type: 'number', default: 120 },
rows: { type: 'number', default: 50 },
transparent: { type:'boolean', default:false } // need good gpu
},
init: function(){
this.el.object3D.visible = false
},
requires:{
html: "https://unpkg.com/aframe-htmlmesh@2.1.0/build/aframe-html.js", // html to AFRAME
winboxjs: "https://unpkg.com/winbox@0.2.82/dist/winbox.bundle.min.js", // deadsimple windows: https://nextapps-de.github.io/winbox
winboxcss: "https://unpkg.com/winbox@0.2.82/dist/css/winbox.min.css", //
xtermcss: "https://unpkg.com/xterm@3.12.0/dist/xterm.css",
xtermjs: "https://unpkg.com/xterm@3.12.0/dist/xterm.js",
axterm: "https://unpkg.com/aframe-xterm-component/aframe-xterm-component.js"
},
dom: {
scale: 3,
events: ['click','keydown'],
html: (me) => `<div></div>`,
css: (me) => `.isoterminal{
overflow:hidden;
}`
},
createTerminal: function(){
this.el.object3D.visible = true
const term = this.term = new Terminal({
allowTransparency: this.data.transparent,
cursorBlink: true,
disableStdin: false,
rows: this.data.rows,
cols: this.data.cols,
fontSize: 16
})
term.open(this.el.dom)
this.canvas = this.el.dom.querySelector('.xterm-text-layer')
this.canvas.id = 'terminal-' + (terminalInstance++)
this.canvasContext = this.canvas.getContext('2d')
this.cursorCanvas = this.el.dom.querySelector('.xterm-cursor-layer')
//this.el.setAttribute('material', `transparent: ${this.data.transparent?'true':'false'}; src: #${this.canvas.id}` )
term.on('refresh', () => {
console.log("refresh")
})
term.on('data', (data) => {
this.el.emit('xterm-data', data)
})
this.el.addEventListener('click', () => {
term.focus()
})
const message = 'Hello from \x1B[1;3;31mWebVR\x1B[0m !\r\n$ '
term.write(message)
},
events:{
// combined AFRAME+DOM reactive events
click: function(e){ }, //
keydown: function(e){ }, //
// reactive events for this.data updates
myvalue: function(e){ this.el.dom.querySelector('b').innerText = this.data.myvalue },
ready: function( ){
this.el.dom.style.display = 'none'
},
launcher: function(){
this.el.dom.style.display = ''
this.createTerminal()
new WinBox( this.manifest.iso + ' ' + this.manifest.name, {
width: window.innerWidth*0.8,
height: window.innerHeight*0.8,
x:"center",
y:"center",
id: this.el.uid, // important hint for html-mesh
root: document.querySelector("#overlay"),
mount: this.el.dom,
onclose: () => {
if( !confirm('do you want to kill this virtual machine and all its processes?') ) return true
this.el.dom.style.display = 'none'
return false
}
});
},
},
manifest: { // HTML5 manifest to identify app to xrsh
"iso": "linux-x64-4.15.iso",
"short_name": "ISOTerm",
"name": "terminal",
"icons": [
{
"src": "https://css.gg/terminal.svg",
"type": "image/svg+xml",
"sizes": "512x512"
}
],
"id": "/?source=pwa",
"start_url": "/?source=pwa",
"background_color": "#3367D6",
"display": "standalone",
"scope": "/",
"theme_color": "#3367D6",
"shortcuts": [
{
"name": "What is the latest news?",
"cli":{
"usage": "helloworld <type> [options]",
"example": "helloworld news",
"args":{
"--latest": {type:"string"}
}
},
"short_name": "Today",
"description": "View weather information for today",
"url": "/today?source=pwa",
"icons": [{ "src": "/images/today.png", "sizes": "192x192" }]
}
],
"description": "Hello world information",
"screenshots": [
{
"src": "/images/screenshot1.png",
"type": "image/png",
"sizes": "540x720",
"form_factor": "narrow"
}
],
"help":`
Helloworld application
This is a help file which describes the application.
It will be rendered thru troika text, and will contain
headers based on non-punctualized lines separated by linebreaks,
in above's case "\nHelloworld application\n" will qualify as header.
`
}
});
AFRAME.registerComponent('xterm-shell', {
dependencies: ['xterm'],
init: function() {
const message = 'Run \x1B[1;3;31m\'node server.js\'\x1B[0m to open a shell\r\n'
const xterm = this.el.components['xterm']
xterm.write(message)
const socket = new WebSocket('ws://localhost:8080/')
// Listen on data, write it to the terminal
socket.onmessage = ({data}) => {
xterm.write(data)
}
socket.onclose = () => {
xterm.write('\r\nConnection closed.\r\n')
}
// Listen on user input, send it to the connection
this.el.addEventListener('xterm-data', ({detail}) => {
socket.send(detail)
})
}
})

111
com.refactor/load.js Normal file
View File

@ -0,0 +1,111 @@
AFRAME.registerComponent('load', {
schema: {
foo: { type:"string"}
},
init: function () {
this.el.object3D.visible = false
//this.el.innerHTML = ` `
},
requires:{
// somecomponent: "https://unpkg.com/some-aframe-component/mycom.min.js"
},
events:{
// component events
somecomponent: function( ){ console.log("component requirement mounted") },
ready: function(e){ console.log("requires are loaded") },
launcher: function(e){
this.filedialog( (data) => {
if( data.match(/<html/) && confirm('reload new XR shell environment?') ){
return document.write(data)
}
if( data.match(/<a-/) ){
let el = document.createElement('a-entity')
el.innerHTML = data
AFRAME.scenes[0].appendChild(el)
}
})
},
},
filedialog: function(onDone){
if( !this.fileEl ){
let el = this.fileEl = document.createElement('input')
el.type = "file"
el.addEventListener('change', (e) =>{
var file = e.target.files[0];
if (!file) {
return;
}
var reader = new FileReader();
reader.onload = function(e) {
var contents = e.target.result;
onDone(contents)
};
reader.readAsText(file);
})
document.body.appendChild(el)
}
this.fileEl.click()
},
manifest: { // HTML5 manifest to identify app to xrsh
"short_name": "Hello world",
"name": "Hello world",
"icons": [
{
"src": "https://css.gg/arrow-up-r.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": "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.
`
}
});

130
com.refactor/manual.js Normal file
View File

@ -0,0 +1,130 @@
AFRAME.registerComponent('manual', {
schema: {
foo: { type:"string"}
},
init: function () {
// requires are loaded
this.el.addEventListener('ready', () => this.el.dom.style.display = 'none' )
},
requires:{
html: "https://unpkg.com/aframe-htmlmesh@2.1.0/build/aframe-html.js", // html to AFRAME
winboxjs: "https://unpkg.com/winbox@0.2.82/dist/winbox.bundle.min.js", // deadsimple windows: https://nextapps-de.github.io/winbox
winboxcss: "https://unpkg.com/winbox@0.2.82/dist/css/winbox.min.css", // deadsimple windows: https://nextapps-de.github.io/winbox
},
dom: {
scale: 3,
events: ['click','keydown'],
html: (me) => `<div>
<h1>Welcome to XRSHell</h1>
<br>
<!-- <img src="https://i.imgur.com/BW22wrb.png"/> -->
<br><br>
The <b>xrsh</b> (xrshell) brings the <a href="https://en.wikipedia.org/wiki/Free_and_open-source_software" target="_blank">FOSS</a>- and <a href="https://en.wikipedia.org/wiki/Linux" target="_blank">Linux</a>-soul to <a href="https://en.wikipedia.org/wiki/WebXR" target="_blank">WebXR</a>, promoting the use of (interactive text) terminal and user-provided operating systems inside WebXR.
<br><br>Technically, <b>xrsh</b> is a bundle of freshly created re-usable FOSS WebXR components.<br>These provide a common filesystem interface for interacting with WebXR, offering the well-known linux/unix toolchain including a commandline to invoke, store, edit and run WebXR utilities - regardless of their implementation.
<br><br>Think of it as termux for the VR/AR headset browser, which can be used to e.g. livecode (using terminal auto-completion!) for XR component (registries).
<br>
<ul>
<li><a href="https://forgejo.isvery.ninja/xrsh" target="_blank">source xrsh</a></li>
<li><a href="https://forgejo.isvery.ninja/xrsh-apps" target="_blank">source xrsh apps</a></li>
<li><a href="https://forgejo.isvery.ninja/xrsh-media" target="_blank">roadmap meeting recordings</a></li>
</ul>
</div>`,
css: (me) => `.manual > div { padding:11px }`
},
events:{
// component events
html: function( ){ console.log("html-mesh requirement mounted") },
// combined AFRAME+DOM reactive events
click: function(e){ }, //
keydown: function(e){ }, //
// reactive events for this.data updates
myvalue: function(e){ this.el.dom.querySelector('b').innerText = this.data.myvalue },
launcher: function(){
new WinBox( this.manifest.name, {
width: '70%',
height: '70%',
x:"center",
y:"center",
id: this.el.uid, // important hint for html-mesh
root: document.querySelector("#overlay"),
mount: this.el.dom,
onclose: () => { this.el.dom.style.display = 'none'; return false; }
});
this.el.dom.style.display = ''
this.el.setAttribute("xrf", document.location.search || "https://coderofsalvation.github.io/xrsh-media/assets/background.glb")
// navigate with: AFRAME.XRF.navigator.to("https://xrfragment.org/index.glb")
},
DOMready: function( ){
this.el.dom.style.display = 'none'
console.log("this.el.dom has been added to DOM")
this.data.myvalue = 1
setInterval( () => this.data.myvalue++, 100 )
}
},
manifest: { // HTML5 manifest to identify app to xrsh
"short_name": "XRSH Manual",
"name": "XRSH Manual",
"icons": [
{
"src": "https://css.gg/coffee.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": "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.
`
}
});

95
com.refactor/vconsole.js Normal file
View File

@ -0,0 +1,95 @@
AFRAME.registerComponent('vconsole', {
schema: {
foo: { type:"string"}
},
init: function () {
//AFRAME.XRF.navigator.to("https://coderofsalvation.github.io/xrsh-media/assets/background.glb")
document.head.innerHTML += `
<style type="text/css">
.vc-mask { display: none !important }
.vc-switch { display: none !important }
.vc-panel {
right:unset !important;
width:100%;
max-width:600px;
z-index:100 !important;
}
</style>
`
},
requires:{
vconsole: "https://unpkg.com/vconsole@latest/dist/vconsole.min.js"
},
events:{
// requires are loaded
ready: function(e){
this.vConsole = new window.VConsole()
document.querySelector('.vc-mask').remove()
document.querySelector('.vc-switch').remove()
},
launcher: function(){
if( !this.vConsole ) return
let panel = document.querySelector('.vc-panel')
if( panel.style.display == 'none' ) this.vConsole.show()
else this.vConsole.hide()
},
},
manifest: { // HTML5 manifest to identify app to xrsh
"short_name": "Hello world",
"name": "Hello world",
"icons": [
{
"src": "https://css.gg/border-bottom.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": "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,159 +0,0 @@
if( AFRAME.components.codemirror ) delete AFRAME.components.codemirror
AFRAME.registerComponent('codemirror', {
schema: {
file: { type:"string"},
term: { type:"selector", default: "[isoterminal]" },
},
init: function () {
this.el.object3D.visible = false
if( !this.data.term || !this.data.term.components ) throw 'codemirror cannot get isoterminal'
if( this.data.file && this.data.file[0] != '/'){
this.data.file = "root/"+this.data.file
}
this.isoterminal = this.data.term.components.isoterminal.isoterminal
//this.el.innerHTML = ` `
this.requireAll()
},
requireAll: async function(){
let s = await AFRAME.utils.require(this.requires)
setTimeout( () => this.el.setAttribute("dom",""), 300 )
},
requires:{
window: "com/window.js"
},
dom: {
scale: 0.5,
events: ['click','keydown'],
html: (me) => `<div class="codemirror">
</div>`,
css: (me) => `.codemirror{
width:100%;
}
.codemirror *{
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;
}
.wb-body + .codemirror{ overflow:hidden; }
.CodeMirror {
margin-top:18px;
}
.cm-s-shadowfox.CodeMirror {
background:transparent !important;
}
`
},
createEditor: function(value){
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",
lineNumbers: true,
styleActiveLine: true,
matchBrackets: true,
Tab: "indentMore",
defaultTab: function(cm) {
if (cm.somethingSelected()) cm.indentSelection("add");
else cm.replaceSelection(" ", "end");
}
})
this.editor.setOption("theme", "shadowfox")
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() )
})
setTimeout( () => {
this.el.setAttribute("html-as-texture-in-xr", `domid: #${this.el.dom.id}`) // only show aframe-html in xr
},1500)
},
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.emulator.fs9p.update_file( file, str)
},
events:{
// component events
DOMready: function(e){
this.isoterminal.worker.postMessage.promise({event:'read_file',data: this.data.file })
.then( this.isoterminal.convert.Uint8ArrayToString )
.then( (str) => {
this.createEditor( str )
})
.catch( (e) => {
console.log("error opening "+this.data.file+", creating new one")
this.createEditor("")
})
},
},
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

@ -18,33 +18,29 @@
*
*/
if( !AFRAME.components.data2event ){
AFRAME.registerComponent('data2event',{
AFRAME.registerComponent('data2event',{
init: function(){
setTimeout( () => {
for( let i in this.el.components ){
let com = this.el.components[i]
if( typeof com.data == 'object' ){
com.data = this.reactify( this.el, com.data)
}
init: function(){
setTimeout( () => {
for( let i in this.el.components ){
let com = this.el.components[i]
if( typeof com.data == 'object' ){
com.data = this.reactify( this.el, com.data)
}
},50)
},
}
},50)
},
reactify: function(el,data){
return new Proxy(data, {
get(me,k,v) {
return me[k]
},
set(me,k,v){
me[k] = v
el.emit(k,{el,k,v})
}
})
},
reactify: function(el,data){
return new Proxy(data, {
get(me,k,v) {
return me[k]
},
set(me,k,v){
me[k] = v
el.emit(k,{el,k,v})
}
})
},
})
}
})

View File

@ -32,103 +32,105 @@
* | `DOMready` | self | fired when dom component (`this.dom`) is created |
*/
if( !AFRAME.components.dom ){
AFRAME.registerComponent('dom',{
AFRAME.registerComponent('dom',{
requires: {
"requestAnimationFrameXR": "com/requestAnimationFrameXR.js"
},
init: function(){
Object.values(this.el.components)
.map( (c) => {
if( c.dom && c.attrName != "dom"){
this.dom = c.dom
this.com = c
}
})
if( !this.dom || !this.com){
return console.warn('dom.js did not find a .dom object inside component')
init: function(){
Object.values(this.el.components)
.map( (c) => {
if( c.dom && c.attrName != "dom"){
this.dom = c.dom
this.com = c
}
})
if( !this.dom || !this.com){
return console.warn('dom.js did not find a .dom object inside component')
}
this
.ensureOverlay()
.addCSS()
.createReactiveDOMElement()
.assignUniqueID()
.scaleDOMvsXR()
.triggerKeyboardForInputs()
this
.ensureOverlay()
.addCSS()
.createReactiveDOMElement()
.assignUniqueID()
.scaleDOMvsXR()
.triggerKeyboardForInputs()
.stubRequestAnimationFrame()
document.querySelector('#overlay').appendChild(this.el.dom)
this.el.emit('DOMready',{el: this.el.dom})
},
document.querySelector('#overlay').appendChild(this.el.dom)
this.el.emit('DOMready',{el: this.el.dom})
},
ensureOverlay: function(){
// ensure overlay
let overlay = document.querySelector('#overlay')
if( !overlay ){
overlay = document.createElement('div')
overlay.id = "overlay"
overlay.setAttribute('style','position:fixed;top:0px;left:0px;right:0px;bottom:0px')
document.body.appendChild(overlay)
// sceneEl.setAttribute("webxr","overlayElement:#overlay")
ensureOverlay: function(){
// ensure overlay
let overlay = document.querySelector('#overlay')
if( !overlay ){
overlay = document.createElement('div')
overlay.id = "overlay"
overlay.setAttribute('style','position:fixed;top:0px;left:0px;right:0px;bottom:0px')
document.body.appendChild(overlay)
// sceneEl.setAttribute("webxr","overlayElement:#overlay")
}
return this
},
reactify: function(el,data){
return new Proxy(data, {
get(me,k,v) {
return me[k]
},
set(me,k,v){
me[k] = v
el.emit(k,{el,k,v})
}
return this
},
})
},
reactify: function(el,data){
return new Proxy(data, {
get(me,k,v) {
return me[k]
},
set(me,k,v){
me[k] = v
el.emit(k,{el,k,v})
}
})
},
// creates el.dom (the 2D DOM object)
createReactiveDOMElement: function(){
this.el.dom = document.createElement('div')
this.el.dom.innerHTML = this.dom.html(this)
this.el.dom.className = this.dom.attrName
this.com.data = this.reactify( this.el, this.com.data )
if( this.dom.events ) this.dom.events.map( (e) => this.el.dom.addEventListener(e, (ev) => this.el.emit(e,ev) ) )
this.el.dom = this.el.dom.children[0]
return this
},
// creates el.dom (the 2D DOM object)
createReactiveDOMElement: function(){
this.el.dom = document.createElement('div')
this.el.dom.innerHTML = this.dom.html(this)
this.el.dom.className = this.dom.attrName
this.com.data = this.reactify( this.el, this.com.data )
if( this.dom.events ) this.dom.events.map( (e) => this.el.dom.addEventListener(e, (ev) => this.el.emit(e,ev) ) )
this.el.dom = this.el.dom.children[0]
return this
},
assignUniqueID: function(){
// assign unique app id so it's easy to reference (by html-mesh component e.g.)
if( !this.el.uid ) this.el.uid = this.el.dom.id = '_'+String(Math.random()).substr(10)
return this
},
assignUniqueID: function(){
// assign unique app id so it's easy to reference (by html-mesh component e.g.)
if( !this.el.uid ) this.el.uid = this.el.dom.id = '_'+String(Math.random()).substr(10)
return this
},
addCSS: function(){
if( this.dom.css && !document.head.querySelector(`style#${this.com.attrName}`) ){
document.head.innerHTML += `<style id="${this.com.attrName}">${this.dom.css(this)}</style>`
}
return this
},
addCSS: function(){
if( this.dom.css && !document.head.querySelector(`style#${this.com.attrName}`) ){
document.head.innerHTML += `<style id="${this.com.attrName}">${this.dom.css(this)}</style>`
scaleDOMvsXR: function(){
if( this.dom.scale ) this.el.setAttribute('scale',`${this.dom.scale} ${this.dom.scale} ${this.dom.scale}`)
return this
},
triggerKeyboardForInputs: function(){
// https://developer.oculus.com/documentation/web/webxr-keyboard ;
[...this.el.dom.querySelectorAll('[type=text]')].map( (input) => {
let triggerKeyboard = function(){
this.focus()
console.log("focus")
}
return this
},
input.addEventListener('click', triggerKeyboard )
})
return this
},
scaleDOMvsXR: function(){
if( this.dom.scale ) this.el.setAttribute('scale',`${this.dom.scale} ${this.dom.scale} ${this.dom.scale}`)
return this
},
stubRequestAnimationFrame: function(){
// stub, because WebXR with overrule this (it will not call the callback as expected in immersive mode)
const requestAnimationFrame = window.requestAnimationFrame
window.requestAnimationFrame = (cb) => {
setTimeout( cb, 25 )
}
}
triggerKeyboardForInputs: function(){
// https://developer.oculus.com/documentation/web/webxr-keyboard ;
[...this.el.dom.querySelectorAll('[type=text]')].map( (input) => {
let triggerKeyboard = function(){
this.focus()
console.log("focus")
}
input.addEventListener('click', triggerKeyboard )
})
return this
},
})
}
})

View File

@ -57,7 +57,9 @@ AFRAME.registerComponent('helloworld-htmlform', {
const instance = this.el.cloneNode(false)
this.el.sceneEl.appendChild( instance )
instance.setAttribute("dom", "")
instance.setAttribute("show-texture-in-xr", "") // only show aframe-html in xr
instance.setAttribute("xd", "") // allows flipping between DOM/WebGL when toggling XD-button
instance.setAttribute("visible", AFRAME.utils.XD() == '3D' ? 'true' : 'false' )
instance.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.5) )
instance.setAttribute("grabbable","")
instance.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera
@ -73,11 +75,7 @@ AFRAME.registerComponent('helloworld-htmlform', {
root: document.querySelector("#overlay"),
mount: instance.dom,
onclose: () => { instance.dom.style.display = 'none'; return false; },
oncreate: () => {
instance.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.5) )
instance.setAttribute("html",`html:#${instance.uid}; cursor:#cursor`)
instance.setAttribute("show-texture-in-xr","") // only show aframe-html texture in xr mode
}
oncreate: () => instance.setAttribute("html",`html:#${instance.uid}; cursor:#cursor`)
});
instance.dom.style.display = AFRAME.utils.XD() == '3D' ? 'none' : '' // show/hide

View File

@ -87,9 +87,7 @@ AFRAME.registerComponent('helloworld-iframe', {
onclose: () => { instance.dom.style.display = 'none'; return false; },
oncreate: () => {
com.data.url = URL
instance.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.5) )
instance.setAttribute("html",`html:#${instance.uid}; cursor:#cursor`)
instance.setAttribute("show-texture-in-xr","") // only show aframe-html texture in xr mode
}
});
instance.dom.style.display = ''

View File

@ -5,7 +5,7 @@ AFRAME.registerComponent('helloworld-window', {
requires: {
dom: "./com/dom.js", // interpret .dom object
xrtexture: "./com/html-as-texture-in-xr.js", // allow switching between 3D/3D
xd: "./com/dom.js", // allow switching between 2D/3D
html: "https://unpkg.com/aframe-htmlmesh@2.1.0/build/aframe-html.js", // html to AFRAME
winboxjs: "https://unpkg.com/winbox@0.2.82/dist/winbox.bundle.min.js", // deadsimple windows: https://nextapps-de.github.io/winbox
winboxcss: "https://unpkg.com/winbox@0.2.82/dist/css/winbox.min.css", //
@ -39,7 +39,10 @@ AFRAME.registerComponent('helloworld-window', {
// instance this component
const instance = this.el.cloneNode(false)
this.el.sceneEl.appendChild( instance )
instance.setAttribute("dom", "")
instance.setAttribute("dom", "")
instance.setAttribute("xd", "") // allows flipping between DOM/WebGL when toggling XD-button
instance.setAttribute("visible", AFRAME.utils.XD() == '3D' ? 'true' : 'false' )
instance.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.5) )
instance.setAttribute("grabbable","")
instance.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera
@ -56,9 +59,7 @@ AFRAME.registerComponent('helloworld-window', {
root: document.querySelector("#overlay"),
mount: instance.dom,
onclose: () => { instance.dom.style.display = 'none'; return false; },
oncreate: () => {
instance.setAttribute("show-texture-in-xr",`domid: #${instance.uid}`) // only show aframe-html texture in xr mode
}
oncreate: () => instance.setAttribute("html",`html:#${instance.uid}; cursor:#cursor`)
});
instance.dom.style.display = '' // show

View File

@ -1,126 +0,0 @@
if( !AFRAME.components['html-as-texture-in-xr'] ){
AFRAME.registerComponent('html-as-texture-in-xr', {
schema: {
domid: { type:"string"}
},
dependencies:{
html: "https://unpkg.com/aframe-htmlmesh@2.1.0/build/aframe-html.js", // html to AFRAME
//html: "https://coderofsalvation.github.io/aframe-htmlmesh/build/aframe-html.js"
//html: "com/aframe-html.js"
},
init: async function () {
let el = document.querySelector(this.data.domid)
if( ! el ){
return console.error("html-as-texture-in-xr: cannot get dom element "+this.data.dom.id)
}
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' )
this.el.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.5) )
},
manifest: { // HTML5 manifest to identify app to xrsh
"short_name": "show-texture-in-xr",
"name": "2D/3D switcher",
"icons": [],
"id": "/?source=pwa",
"start_url": "/?source=pwa",
"background_color": "#3367D6",
"display": "standalone",
"scope": "/",
"theme_color": "#3367D6",
"category":"system",
"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": "use ESC-key to toggle between 2D / 3D",
"screenshots": [
{
"src": "/images/screenshot1.png",
"type": "image/png",
"sizes": "540x720",
"form_factor": "narrow"
}
],
"help":`
Helloworld application
`
}
});
AFRAME.utils.XD = function(){
return document.body.classList.contains('XR') ? '3D' : '2D'
}
AFRAME.utils.XD.toggle = function(state){
state = state != undefined ? state : state || !document.body.className.match(/XR/)
document.body.classList[ state ? 'add' : 'remove'](['XR'])
AFRAME.scenes[0].emit( state ? '3D' : '2D')
}
AFRAME.utils.XD.getPositionInFrontOfCamera = function(distance){
const camera = AFRAME.scenes[0].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
}
AFRAME.registerSystem('html-as-texture-in-xr',{
init: function(){
this.sceneEl.addEventListener('enter-vr',() => AFRAME.utils.XD.toggle(true) )
this.sceneEl.addEventListener('exit-vr', () => AFRAME.utils.XD.toggle(false) )
this.sceneEl.addEventListener('2D', () => this.showElements(false) )
this.sceneEl.addEventListener('3D', () => this.showElements(true) )
document.head.innerHTML += `<style type="text/css">
.XR #toggle_overlay{
background: transparent;
color: #3aacff;
}
/*
.XR #overlay{
visibility: hidden;
}
*/
</style>`
},
showElements: function(state){
let els = [...document.querySelectorAll('[html-as-texture-in-xr]')]
els = els.filter( (el) => el != this.el ? el : null ) // filter out self
els.map( (el) => el.setAttribute("visible", state ? true : false ) )
}
})
}

View File

@ -1,359 +1,479 @@
/*
*
* 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
*
*
* 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())
*/
AFRAME.registerComponent('isoterminal', {
schema: {
iso: { type:"string", "default":"com/isoterminal/xrsh.iso" },
cols: { type: 'number',"default": 120 },
rows: { type: 'number',"default": 30 },
padding:{ type: 'number',"default": 18 },
transparent: { type:'boolean', "default":false } // need good gpu
},
if( typeof AFRAME != 'undefined '){
init: function(){
this.el.object3D.visible = false
},
AFRAME.registerComponent('isoterminal', {
schema: {
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},
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
xterm: { type: 'boolean', "default":true }, // use xterm.js? (=slower)
memory: { type: 'number', "default":64 }, // VM memory (in MB)
bufferLatency: { type: 'number', "default":300 }, // in ms: bufferlatency from webworker to xterm (batch-update every char to texture)
canvasLatency: { type: 'number', "default":300 } // in ms: time between canvas re-draws
},
requires:{
'window': "com/window.js",
xtermjs: "https://unpkg.com/@xterm/xterm@5.5.0/lib/xterm.js",
xtermcss: "https://unpkg.com/@xterm/xterm@5.5.0/css/xterm.css",
v86: "com/isoterminal/libv86.js"
//axterm: "https://unpkg.com/aframe-xterm-component/aframe-xterm-component.js"
},
init: function(){
this.el.object3D.visible = false
this.initHud()
fetch(this.data.iso,{method: 'HEAD'})
.then( (res) => {
if( res.status != 200 ) throw 'not found'
})
.catch( (e) => {
console.warn(this.data.iso+" could not be loaded, loading fallback ISO URL:")
console.warn(this.schema.iso.default)
this.data.iso = this.schema.iso.default
})
.finally( () => this.initTerminal(true) )
},
dom: {
scale: 0.7,
events: ['click','keydown'],
html: (me) => `<div class="isoterminal"></div>`,
requires:{
com: "com/dom.js",
window: "com/window.js",
v86: "com/isoterminal/libv86.js",
// allow xrsh to selfcontain scene + itself
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 features
ISOTerminal: "com/isoterminal/ISOTerminal.js",
localforage: "https://cdn.rawgit.com/mozilla/localForage/master/dist/localforage.js"
},
css: (me) => `.isoterminal{
padding: ${me.com.data.padding}px;
width:100%;
height:100%;
}
.isoterminal *{
white-space: pre;
font-size: 14px;
font-family: Liberation Mono,DejaVu Sans Mono,Courier New,monospace;
font-weight:700;
display:inline;
overflow: hidden;
}
dom: {
//scale: 0.5,
events: ['click','keydown'],
html: (me) => `<div class="isoterminal">
</div>`,
.isoterminal style{ display:none }
css: (me) => `.isoterminal{
padding: ${me.com.data.padding}px;
width:100%;
height:100%;
}
.isoterminal div{
display:block;
position:relative;
}
@font-face {
font-family: 'Cousine';
font-style: normal;
font-weight: 400;
src: url(./com/isoterminal/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');
}
.wb-body:has(> .isoterminal){
background: #000c;
overflow:hidden;
}
.isoterminal style{ display:none }
.isoterminal div{ display:block; }
.isoterminal span{ display: inline }
#overlay .winbox:has(> .isoterminal){
background:transparent;
box-shadow:none;
}
@keyframes fade {
from { opacity: 1.0; }
50% { opacity: 0.5; }
to { opacity: 1.0; }
}
.wb-body:has(> .isoterminal){
background: #000C;
overflow:hidden;
border-radius:7px;
}
@-webkit-keyframes fade {
from { opacity: 1.0; }
50% { opacity: 0.5; }
to { opacity: 1.0; }
}
.XR .wb-body:has(> .isoterminal){
background: #000;
}
.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;
}
.blink {
animation:fade 1000ms infinite;
-webkit-animation:fade 1000ms infinite;
}
`
},
@keyframes fade {
from { opacity: 1.0; }
50% { opacity: 0.5; }
to { opacity: 1.0; }
}
toUint8Array: function(str) {
// Create a new Uint8Array with the same length as the input string
const uint8Array = new Uint8Array(str.length);
// Iterate over the string and populate the Uint8Array
for (let i = 0; i < str.length; i++) {
uint8Array[i] = str.charCodeAt(i);
}
return uint8Array;
},
@-webkit-keyframes fade {
from { opacity: 1.0; }
50% { opacity: 0.5; }
to { opacity: 1.0; }
}
.isoterminal .xterm-rows.xterm-focus .xterm-cursor.xterm-cursor-block,
.blink{
animation:fade 1000ms infinite;
-webkit-animation:fade 1000ms infinite;
}
`
},
initTerminal: async function(singleton){
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
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",
})
this.el.setAttribute("selfcontainer","")
// *DISABLED* instance this component
// rason: we only need one term for now (more = too cpu heavy)
let instance
if( singleton ){
instance = this.el
}else{
if( this.instance ){
const el = document.querySelector('.isoterminal')
return console.warn('TODO: allow multiple terminals for future beefy devices(see v86 examples)')
runISO: function(dom,instance){
//var term = new Terminal()
//term.open(dom)
//term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ')``
if( typeof Terminal == undefined ) throw 'xterm terminal not loaded'
// monkeypatch Xterm (which V86 initializes) so we can add our own constructor args
window._Terminal = window.Terminal
window.Terminal = function(opts){
const term = new window._Terminal({ ...opts,
cursorBlink:true,
onSelectionChange: function(e){
debugger
}
instance = this.instance = this.el.cloneNode(false)
this.el.sceneEl.appendChild( instance )
})
term.onSelectionChange( () => {
document.execCommand('copy')
term.select(0, 0, 0)
instance.setStatus('copied to clipboard')
})
return term
}
instance.setStatus = (msg) => {
const w = instance.winbox
w.titleBak = w.titleBak || w.title
instance.winbox.setTitle( `${w.titleBak} [${msg}]` )
}
var emulator = window.emulator = dom.emulator = new V86({
wasm_path: "com/isoterminal/v86.wasm",
memory_size: 32 * 1024 * 1024,
vga_memory_size: 2 * 1024 * 1024,
serial_container_xtermjs: dom,
//screen_container: dom, //this.canvas.parentElement,
bios: {
url: "com/isoterminal/bios/seabios.bin",
},
vga_bios: {
url: "com/isoterminal/bios/vgabios.bin",
},
network_relay_url: "wss://relay.widgetry.org/",
cdrom: {
url: this.data.iso,
},
cmdline: "rw root=host9p rootfstype=9p rootflags=trans=virtio,cache=loose modules=virtio_pci tsc=reliable init_on_free=on",
bzimage:{
url: "com/isoterminal/images/buildroot-bzimage.bin"
},
//bzimage_initrd_from_filesystem: true,
//filesystem: {
// baseurl: "com/isoterminal/v86/images/alpine-rootfs-flat",
// basefs: "com/isoterminal/v86/images/alpine-fs.json",
// },
//screen_dummy: true,
//disable_jit: false,
filesystem: {},
autostart: true,
});
const loading = [
'loading quantum bits and bytes',
'preparing quantum flux capacitors',
'crunching peanuts and chakras',
'preparing parallel universe',
'loading quantum state fluctuations',
'preparing godmode',
'loading cat pawns and cuteness',
'beaming up scotty',
'still faster than Windows update',
'loading a microlinux',
'figuring out meaning of life',
'Aligning your chakras now',
'Breathing in good vibes',
'Finding inner peace soon',
'Centering your Zen energy',
'Awakening third eye powers',
'Tuning into the universe',
'Balancing your cosmic karma',
'Stretching time and space',
'Recharging your soul battery',
'Transcending earthly limits'
]
let motd = "\n\r"
motd += " " + ' ____ _____________ _________ ___ ___ ' + "\n\r"
motd += " " + ' \\ \\/ /\\______ \\/ _____// | \\ ' + "\n\r"
motd += " " + ' \\ / | _/\\_____ \\/ ~ \\ ' + "\n\r"
motd += " " + ' / \\ | | \\/ \\ Y / ' + "\n\r"
motd += " " + ' /___/\\ \\ |____|_ /_______ /\\___|_ / ' + "\n\r"
motd += " " + ' \\_/ \\/ \\/ \\/ ' + "\n\r"
motd += " \n\r"
motd += `${loading[ Math.floor(Math.random()*1000) % loading.length-1 ]}, please wait..\n\r\n\r`
motd += "\033[0m"
const files = [
"com/isoterminal/mnt/js",
"com/isoterminal/mnt/jsh",
"com/isoterminal/mnt/confirm",
"com/isoterminal/mnt/prompt",
"com/isoterminal/mnt/alert",
"com/isoterminal/mnt/hook",
"com/isoterminal/mnt/xrsh",
"com/isoterminal/mnt/profile",
"com/isoterminal/mnt/profile.xrsh",
"com/isoterminal/mnt/profile.js",
"com/isoterminal/mnt/motd",
"com/isoterminal/mnt/v86pipe"
]
const redirectConsole = (handler) => {
const log = console.log;
const dir = console.dir;
const err = console.error;
const warn = console.warn;
console.log = (...args)=>{
const textArg = args[0];
handler(textArg+'\n');
log.apply(log, args);
};
console.error = (...args)=>{
const textArg = args[0].message?args[0].message:args[0];
handler( textArg+'\n', '\x1b[31merror\x1b[0m');
err.apply(log, args);
};
console.dir = (...args)=>{
const textArg = args[0].message?args[0].message:args[0];
handler( JSON.stringify(textArg,null,2)+'\n');
dir.apply(log, args);
};
console.warn = (...args)=>{
const textArg = args[0].message?args[0].message:args[0];
handler(textArg+'\n','\x1b[38;5;208mwarn\x1b[0m');
err.apply(log, args);
};
}
emulator.bus.register("emulator-started", async () => {
emulator.serial_adapter.term.element.querySelector('.xterm-viewport').style.background = 'transparent'
emulator.serial_adapter.term.clear()
emulator.serial_adapter.term.write(motd)
emulator.create_file("motd", this.toUint8Array(motd) )
emulator.create_file("js", this.toUint8Array(`#!/bin/sh
cat /mnt/motd
cat > /dev/null
`))
redirectConsole( (str,prefix) => {
if( emulator.log_to_tty ){
prefix = prefix ? prefix+' ' : ' '
str.trim().split("\n").map( (line) => {
emulator.serial_adapter.term.write( '\r\x1b[38;5;165m/dev/browser: \x1b[0m'+prefix+line+'\n' )
})
emulator.serial_adapter.term.write( '\r' )
}
emulator.create_file( "console", this.toUint8Array( str ) )
})
let p = files.map( (f) => fetch(f) )
Promise.all(p)
.then( (files) => {
files.map( (f) => {
f.arrayBuffer().then( (buf) => {
emulator.create_file( f.url.replace(/.*mnt\//,''), new Uint8Array(buf) )
})
})
})
//emulator.serial0_send('chmod +x /mnt/js')
//emulator.serial0_send()
let line = ''
let ready = false
emulator.add_listener("serial0-output-byte", async (byte) => {
var chr = String.fromCharCode(byte);
if(chr < " " && chr !== "\n" && chr !== "\t" || chr > "~")
{
return;
}
if(chr === "\n")
{
var new_line = line;
line = "";
}
else if(chr >= " " && chr <= "~")
{
line += chr;
}
//if(!ran_command && line.endsWith("~% "))
//{
// ran_command = true;
// emulator.serial0_send("chmod +x /mnt/test-i386\n");
// emulator.serial0_send("/mnt/test-i386 > /mnt/result\n");
// emulator.serial0_send("echo test fini''shed\n");
//}
//console.dir({line,new_line})
if( !ready && line.match(/^(\/ #|~%)/) ){
instance.dom.classList.remove('blink')
// set environment
let env = ['export BROWSER=1']
for ( let i in document.location ){
if( typeof document.location[i] == 'string' )
env.push( 'export '+String(i).toUpperCase()+'="'+document.location[i]+'"')
}
env.map( (e) => emulator.serial0_send(`echo '${e}' >> /mnt/profile\n`) )
let boot = `source /mnt/profile`
// exec hash as extra boot cmd
if( document.location.hash.length > 1 ){
boot += `&& cmd='${decodeURI(document.location.hash.substr(1))}' && $cmd`
}
emulator.serial0_send(boot+"\n")
instance.winbox.maximize()
emulator.serial_adapter.term.focus()
ready = true
//emulator.serial0_send("root\n")
//emulator.serial0_send("mv /mnt/js . && chmod +x js\n")
}
});
// unix to js device
emulator.add_listener("9p-write-end", async (opts) => {
const decoder = new TextDecoder('utf-8');
if ( opts[0] == 'js' ){
const buf = await emulator.read_file("dev/browser/js")
const script = decoder.decode(buf)
try{
let res = (new Function(`${script}`))()
if( res && typeof res != 'string' ) res = JSON.stringify(res,null,2)
}catch(e){
console.error(e)
}
}
})
// enable/disable logging file (echo 1 > mnt/console.tty)
emulator.add_listener("9p-write-end", async (opts) => {
const decoder = new TextDecoder('utf-8');
if ( opts[0] == 'console.tty' ){
const buf = await emulator.read_file("console.tty")
const val = decoder.decode(buf)
emulator.log_to_tty = ( String(val).trim() == '1')
}
})
});
},
events:{
// combined AFRAME+DOM reactive events
click: function(e){ }, //
keydown: function(e){ },
// reactive events for this.data updates
myvalue: function(e){ this.el.dom.querySelector('b').innerText = this.data.myvalue },
ready: function( ){
this.el.dom.style.display = 'none'
},
launcher: async function(){
if( this.instance ){
const el = document.querySelector('.isoterminal')
return console.warn('TODO: allow multiple terminals (see v86 examples)')
}
// init isoterminal
this.isoterminal = new ISOTerminal(instance,this.data)
let s = await AFRAME.utils.require(this.requires)
// instance this component
const instance = this.instance = this.el.cloneNode(false)
this.el.sceneEl.appendChild( instance )
instance.addEventListener('DOMready', () => {
//instance.setAttribute("html-as-texture-in-xr", `domid: #${this.el.dom.id}`)
//instance.winbox.resize(720,380)
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}`)
this.runISO(instance.dom, instance)
instance.setAttribute("window", `title: ${this.data.iso}; uid: ${instance.uid}; attach: #overlay; dom: #${instance.dom.id}`)
})
instance.addEventListener('window.oncreate', (e) => {
instance.dom.classList.add('blink')
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]
this.isoterminal.start(opts)
})
instance.setAttribute("dom", "")
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.isoterminal.emit('status',"running")
})
this.isoterminal.addEventListener('status', function(e){
let msg = e.detail
const w = instance.winbox
if(!w) return
w.titleBak = w.titleBak || w.title
w.setTitle( `${w.titleBak} [${msg}]` )
})
instance.addEventListener('window.onclose', (e) => {
if( !confirm('do you want to kill this virtual machine and all its processes?') ) e.halt = true
})
const resize = (w,h) => { }
const resize = (w,h) => {
if( instance.dom.emulator && instance.dom.emulator.serial_adapter ){
setTimeout( () => {
this.autoResize(instance.dom.emulator.serial_adapter.term,instance,-5)
},500) // wait for resize anim
}
}
instance.addEventListener('window.onresize', resize )
instance.addEventListener('window.onmaximize', resize )
const focus = (showdom) => (e) => {
if( this.el.components.xterm ){
this.el.components.xterm.term.focus()
}
if( this.el.components.window ){
this.el.components.window.show( showdom )
}
}
instance.setAttribute("dom", "")
instance.setAttribute("xd", "") // allows flipping between DOM/WebGL when toggling XD-button
instance.setAttribute("visible", AFRAME.utils.XD() == '3D' ? 'true' : 'false' )
instance.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.5) )
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) )
const focus = () => document.querySelector('canvas.a-canvas').focus()
instance.addEventListener('obbcollisionstarted', focus )
this.el.sceneEl.addEventListener('enter-vr', focus )
instance.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera
},
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")
}
},
events:{
// combined AFRAME+DOM reactive events
click: function(e){ }, //
keydown: function(e){ },
// reactive events for this.data updates
myvalue: function(e){ this.el.dom.querySelector('b').innerText = this.data.myvalue },
launcher: async function(){
this.initTerminal()
}
},
manifest: { // HTML5 manifest to identify app to xrsh
"iso": "linux-x64-4.15.iso",
"short_name": "ISOTerm",
"name": "terminal",
"icons": [
{
"src": "https://css.gg/terminal.svg",
"src": "data:image/svg+xml;base64,PHN2ZwogIHdpZHRoPSIyNCIKICBoZWlnaHQ9IjI0IgogIHZpZXdCb3g9IjAgMCAyNCAyNCIKICBmaWxsPSJub25lIgogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKPgogIDxwYXRoCiAgICBkPSJNNS4wMzMzIDE0LjgyODRMNi40NDc1MSAxNi4yNDI2TDEwLjY5MDIgMTJMNi40NDc1MSA3Ljc1NzMzTDUuMDMzMyA5LjE3MTU1TDcuODYxNzIgMTJMNS4wMzMzIDE0LjgyODRaIgogICAgZmlsbD0iY3VycmVudENvbG9yIgogIC8+CiAgPHBhdGggZD0iTTE1IDE0SDExVjE2SDE1VjE0WiIgZmlsbD0iY3VycmVudENvbG9yIiAvPgogIDxwYXRoCiAgICBmaWxsLXJ1bGU9ImV2ZW5vZGQiCiAgICBjbGlwLXJ1bGU9ImV2ZW5vZGQiCiAgICBkPSJNMiAyQzAuODk1NDMxIDIgMCAyLjg5NTQzIDAgNFYyMEMwIDIxLjEwNDYgMC44OTU0MyAyMiAyIDIySDIyQzIzLjEwNDYgMjIgMjQgMjEuMTA0NiAyNCAyMFY0QzI0IDIuODk1NDMgMjMuMTA0NiAyIDIyIDJIMlpNMjIgNEgyTDIgMjBIMjJWNFoiCiAgICBmaWxsPSJjdXJyZW50Q29sb3IiCiAgLz4KPC9zdmc+",
"type": "image/svg+xml",
"sizes": "512x512"
}
],
"id": "/?source=pwa",
"start_url": "/?source=pwa",
"background_color": "#3367D6",
"display": "standalone",
"scope": "/",
"theme_color": "#3367D6",
"shortcuts": [
{
"name": "What is the latest news?",
"cli":{
"usage": "helloworld <type> [options]",
"example": "helloworld news",
"args":{
"--latest": {type:"string"}
}
},
"short_name": "Today",
"description": "View weather information for today",
"url": "/today?source=pwa",
"icons": [{ "src": "/images/today.png", "sizes": "192x192" }]
}
],
"description": "Hello world information",
"screenshots": [
{
"src": "/images/screenshot1.png",
"type": "image/png",
"sizes": "540x720",
"form_factor": "narrow"
}
],
"help":`
Helloworld application
This is a help file which describes the application.
It will be rendered thru troika text, and will contain
headers based on non-punctualized lines separated by linebreaks,
in above's case "\nHelloworld application\n" will qualify as header.
`
}
});
}
},
autoResize: function(term,instance,rowoffset){
if( !term.element ) return
const defaultScrollWidth = 24;
const MINIMUM_COLS = 2;
const MINIMUM_ROWS = 2;
const dims = term._core._renderService.dimensions;
const scrollbarWidth = (term.options.scrollback === 0
? 0
: (term.options.overviewRuler?.width || defaultScrollWidth ));
const parentElementStyle = window.getComputedStyle(instance.dom);
const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')));
const elementStyle = window.getComputedStyle(term.element);
const elementPadding = {
top: parseInt(elementStyle.getPropertyValue('padding-top')),
bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')),
right: parseInt(elementStyle.getPropertyValue('padding-right')),
left: parseInt(elementStyle.getPropertyValue('padding-left'))
};
const elementPaddingVer = elementPadding.top + elementPadding.bottom;
const elementPaddingHor = elementPadding.right + elementPadding.left;
const availableHeight = parentElementHeight - elementPaddingVer;
const availableWidth = parentElementWidth - elementPaddingHor - scrollbarWidth;
const geometry = {
cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / dims.css.cell.width)),
rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / dims.css.cell.height))
};
term.resize(geometry.cols, geometry.rows + (rowoffset||0) );
},
manifest: { // HTML5 manifest to identify app to xrsh
"iso": "linux-x64-4.15.iso",
"short_name": "ISOTerm",
"name": "terminal",
"icons": [
{
"src": "https://css.gg/terminal.svg",
"src": "data:image/svg+xml;base64,PHN2ZwogIHdpZHRoPSIyNCIKICBoZWlnaHQ9IjI0IgogIHZpZXdCb3g9IjAgMCAyNCAyNCIKICBmaWxsPSJub25lIgogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKPgogIDxwYXRoCiAgICBkPSJNNS4wMzMzIDE0LjgyODRMNi40NDc1MSAxNi4yNDI2TDEwLjY5MDIgMTJMNi40NDc1MSA3Ljc1NzMzTDUuMDMzMyA5LjE3MTU1TDcuODYxNzIgMTJMNS4wMzMzIDE0LjgyODRaIgogICAgZmlsbD0iY3VycmVudENvbG9yIgogIC8+CiAgPHBhdGggZD0iTTE1IDE0SDExVjE2SDE1VjE0WiIgZmlsbD0iY3VycmVudENvbG9yIiAvPgogIDxwYXRoCiAgICBmaWxsLXJ1bGU9ImV2ZW5vZGQiCiAgICBjbGlwLXJ1bGU9ImV2ZW5vZGQiCiAgICBkPSJNMiAyQzAuODk1NDMxIDIgMCAyLjg5NTQzIDAgNFYyMEMwIDIxLjEwNDYgMC44OTU0MyAyMiAyIDIySDIyQzIzLjEwNDYgMjIgMjQgMjEuMTA0NiAyNCAyMFY0QzI0IDIuODk1NDMgMjMuMTA0NiAyIDIyIDJIMlpNMjIgNEgyTDIgMjBIMjJWNFoiCiAgICBmaWxsPSJjdXJyZW50Q29sb3IiCiAgLz4KPC9zdmc+",
"type": "image/svg+xml",
"sizes": "512x512"
}
],
"id": "/?source=pwa",
"start_url": "/?source=pwa",
"background_color": "#3367D6",
"display": "standalone",
"scope": "/",
"theme_color": "#3367D6",
"shortcuts": [
{
"name": "What is the latest news?",
"cli":{
"usage": "helloworld <type> [options]",
"example": "helloworld news",
"args":{
"--latest": {type:"string"}
}
},
"short_name": "Today",
"description": "View weather information for today",
"url": "/today?source=pwa",
"icons": [{ "src": "/images/today.png", "sizes": "192x192" }]
}
],
"description": "Hello world information",
"screenshots": [
{
"src": "/images/screenshot1.png",
"type": "image/png",
"sizes": "540x720",
"form_factor": "narrow"
}
],
"help":`
Helloworld application
This is a help file which describes the application.
It will be rendered thru troika text, and will contain
headers based on non-punctualized lines separated by linebreaks,
in above's case "\nHelloworld application\n" will qualify as header.
`
}
});

View File

@ -1,318 +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}, this.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.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 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.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,
};
/*
* the WebWorker (which runs v86)
*
*/
this.worker = new Worker("com/isoterminal/worker.js");
this.worker.onmessage = (e) => {
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
}
this.preventFrameDrop( 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.preventFrameDrop( () => this.worker.postMessage(data,getTransferable(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)
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 tools are accessible and often better than closed alternatives",
"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: NLnet | @nlnet@nlnet.nl
\r MrDoob | THREE.js
\r Diego Marcos | AFRAME.js
\r Leon van Kammen | @lvk@mastodon.online
\r Fabien Benetou | @utopiah@mastodon.pirateparty.be
`
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)
}
}
ISOTerminal.prototype.getTransferable = function(data){
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);
}
if( isTransferable(data) ) console.log("Transferable!")
if( isTransferable(data) ) return isTransferable(data) ? [data] : undefined
}

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]

View File

@ -1,48 +0,0 @@
let emulator = this.emulator
let me = this
emulator.fs9p.update_file = async function(file,data){
const convert = ISOTerminal.prototype.convert
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
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) )
}
emulator.fs9p.append_file = async function(file,data){
const convert = ISOTerminal.prototype.convert
const p = this.SearchPath(file);
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) )
}
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,63 +0,0 @@
if( typeof emulator != 'undefined' ){
// inside worker-thread
this['emulator.restore_state'] = async function(data){
await emulator.restore_state(data)
console.log("restored state")
this.postMessage({event:"state_restored",data:false})
}
this['emulator.save_state'] = async function(){
console.log("saving session")
let state = await emulator.save_state()
this.postMessage({event:"state_saved",data:state})
}
}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
state = this.convert.base64ToArrayBuffer( stateBase64 )
this.addEventListener('state_restored', function(){
// simulate / fastforward boot events
this.postBoot( () => this.send("l\n") )
})
this.worker.postMessage({event:'emulator.restore_state',data:state})
}
})
this.save = async () => {
const state = await this.worker.postMessage({event:"emulator.save_state",data:false})
}
this.addEventListener('state_saved', function(e){
const state = e.detail
localforage.setItem("state", this.convert.arrayBufferToBase64(state) )
console.log("state saved")
})
window.addEventListener("beforeunload", function (e) {
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

@ -1,22 +0,0 @@
ISOTerminal.addEventListener('ready', function(e){
setTimeout( () => this.boot(), 50 ) // because of autorestore.js
})
ISOTerminal.prototype.boot = async function(e){
// set environment
let env = ['export BROWSER=1']
for ( let i in document.location ){
if( typeof document.location[i] == 'string' ){
env.push( 'export '+String(i).toUpperCase()+'="'+decodeURIComponent( document.location[i]+'"') )
}
}
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")
}
}
}

View File

@ -1,32 +0,0 @@
if( typeof emulator != 'undefined' ){
// inside worker-thread
// 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.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 +0,0 @@
if( typeof emulator != 'undefined' ){
// inside worker-thread
// unix to js device
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{
this.postMessage({event:'runJavascript',data:[js]})
}catch(e){
console.error(e)
}
})
}else{
// inside browser-thread
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,31 +0,0 @@
if( typeof emulator != 'undefined' ){
// inside worker-thread
// 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="?"
try{
if( script.match(/^PID/) ){
PID = script.match(/^PID=([0-9]+);/)[1]
}
this.postMessage({event:'javascript-eval',data:{script,PID}})
}catch(e){
console.error(e)
}
})
}else{
// inside browser-thread
ISOTerminal.addEventListener('javascript-eval', function(e){
const {script,PID} = e.detail
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)
this.emit('fs9p.update_file', [`run/${PID}`, this.convert.toUint8Array(res)] )
})
}

View File

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

View File

@ -1,13 +0,0 @@
ISOTerminal.addEventListener('ready', function(){
this.screenButtonsCreate()
})
ISOTerminal.prototype.screenButtonsCreate = function(){
let el = document.createElement("a-plane")
el.setAttribute("height","1")
el.setAttribute("width","1")
el.setAttribute("scale","0.1 0.07 1")
el.setAttribute("position", "-0.326 -0.270 0")
this.instance.appendChild(el)
}

View File

@ -1,120 +0,0 @@
ISOTerminal.addEventListener('init', function(){
if( typeof Terminal != 'undefined' ) this.xtermInit()
})
ISOTerminal.addEventListener('runISO', function(e){
let opts = e.detail
opts.serial_container_xtermjs = opts.screen_container
delete opts.screen_container
})
ISOTerminal.prototype.xtermInit = function(){
this.serial_input = 0 // set input to serial line 0
let isoterm = this
// monkeypatch Xterm (which V86 initializes) so we can add our own constructor args
window._Terminal = window.Terminal
window.Terminal = function(opts){
const term = new window._Terminal({ ...opts,
cursorBlink:true,
onSelectionChange: function(e){ console.log("selectchange") },
letterSpacing: 0
})
term.onSelectionChange( () => {
document.execCommand('copy')
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
//document.body.addEventListener('keydown', (e) => e.key == 'Escape' && this.emulator.serial_adapter.term.blur() )
})
const resize = (w,h) => {
setTimeout( () => {
if( isoterm?.emulator?.serial_adapter?.term ){
isoterm.xtermAutoResize(isoterm.emulator.serial_adapter.term, isoterm.instance,-3)
}
},800) // wait for resize anim
}
isoterm.instance.addEventListener('window.onresize', resize )
isoterm.instance.addEventListener('window.onmaximize', resize )
}
ISOTerminal.prototype.xtermAutoResize = function(term,instance,rowoffset){
if( !term.element ) return
const defaultScrollWidth = 24;
const MINIMUM_COLS = 2;
const MINIMUM_ROWS = 2;
const dims = term._core._renderService.dimensions;
const scrollbarWidth = (term.options.scrollback === 0
? 0
: (term.options.overviewRuler?.width || defaultScrollWidth ));
const parentElementStyle = window.getComputedStyle(instance.dom);
const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')));
const elementStyle = window.getComputedStyle(term.element);
const elementPadding = {
top: parseInt(elementStyle.getPropertyValue('padding-top')),
bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')),
right: parseInt(elementStyle.getPropertyValue('padding-right')),
left: parseInt(elementStyle.getPropertyValue('padding-left'))
};
const elementPaddingVer = elementPadding.top + elementPadding.bottom;
const elementPaddingHor = elementPadding.right + elementPadding.left;
const availableHeight = parentElementHeight - elementPaddingVer;
const availableWidth = parentElementWidth - elementPaddingHor - scrollbarWidth;
const geometry = {
cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / dims.css.cell.width)),
rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / dims.css.cell.height))
};
term.resize(geometry.cols, geometry.rows + (rowoffset||0) );
}

Binary file not shown.

File diff suppressed because it is too large Load Diff

6
com/isoterminal/mnt/alert Executable file
View File

@ -0,0 +1,6 @@
#!/bin/sh
title=$1
shift
msg="$*"
echo "$title $(printf "\033[0m")$msg"
hook alert $title "$msg"

4
com/isoterminal/mnt/confirm Executable file
View File

@ -0,0 +1,4 @@
#!/bin/sh
read -p "$(printf "\033[0m")[?] $1 [y/n] $(printf "\033[0m")" y
test $y = y && echo true && exit
echo false

11
com/isoterminal/mnt/hook Executable file
View File

@ -0,0 +1,11 @@
#!/bin/sh
test -z $1 && { echo "usage: hook <cmd_or_jsfunction> [args]"; exit 0; }
cmd=$1
shift
test -d ~/hook.d/$cmd && {
find ~/hook.d/$cmd/ -type f -executable | while read hook; do
{ $hook "$@" || true; } | awk '{ gsub(/\/root\/\//,"",$1); $1 = sprintf("%-40s", $1)} 1'
done
}

View File

@ -1,28 +1,16 @@
#!/bin/sh
PID=$$
test -z "$1" && { echo "Usage: js 'somefunction(1)'"; exit 0; }
test -n "$BROWSER" || { alert warning "/dev/browser not active (are you running outside of v86?)"; }
javascript="$*"
# if we are run as shebang, use the file as input
test -f "$1" && {
javascript="args = String('$*').split(' '); $(cat $1 | tail +2)"
}
# below is not ideal
# first I tried /dev/ttyS* https://github.com/copy/v86/issues/530
# and differentiate processes by prefixing output by PID's
# to parse it later with AWK, but it was very hairy
OUTPUT=/mnt/$PID
echo -n "PID=$PID; $javascript" > /dev/browser/js
sleep 0.1
test -f $OUTPUT && {
cat $OUTPUT
rm $OUTPUT
}
case "$1" in
*/*) javascript="args = String('$*').split(' '); $(cat $1 | tail +2)"
;;
esac
echo -n "$javascript" > /dev/browser/js
# should we use flock, an awesome way to make processes read/write the same file
# while preventing 1001 concurrency issues?

View File

@ -3,8 +3,6 @@
#
# 'jsh prompt question answer' executes: js prompt('question','answer') )
source /mnt/profile.sh
to_js(){
printf "%s(" "$1"
shift
@ -25,7 +23,7 @@ to_js(){
test -z "$1" || {
func=$(to_js "$@")
func=${func/,)/)}
js "return $func"
js "$func"
hook "$@"
exit 0
}
@ -35,7 +33,7 @@ echo "jsh> type 'exit' or CTRL-C to quit"
echo "jsh> HINT: to run alert('foo') outside this REPL, run 'jsh alert foo'"
echo "jsh>"
while true; do
echo -n -e "\n$(printf "\033[0m")jsh> $(printf "\033[0m")"
echo -n -e "\r$(printf "\033[0m")jsh> $(printf "\033[0m")"
read line
test "$line" = exit && exit
js "$line"

View File

@ -5,12 +5,11 @@
 _ _ / \ | | \/ \ Y / _ _ _ _ _ _ _
 . . /___/\ \ |____|_ /_______ /\___|_ /. . . . . . . .
 . . . . . .\_/. . . . \/ . . . .\/ . . _ \/ . . . . . . . .
 ▬▬▬▬ https://xrsh.isvery.ninja ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
 ▬ ▬ ▬ https://xrsh.isvery.ninja ▬ ▬ ▬ ▬ ▬ ▬ ▬ ▬ ▬ ▬ ▬ ▬ ▬ ▬
 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Open, local-first, unix hackable & selfcontained XR apps.
Open, local-first, hackable & selfcontained XR apps.
credits: NLnet | @nlnet@nlnet.nl https://nlnet.nl/project
all FOSS devs | copy.sh (v86) aframe.io (AFRAME)
Leon van Kammen | @lvk@mastodon.online
Fabien Benetou | @utopiah@mastodon.pirateparty.be
credits: all FOSS devs | @lvk@mastodon.online
copy.sh (v86) | @utopiah@mastodon.pirateparty.be

View File

@ -1,41 +1,22 @@
# source URL data from v86 (file created by v86 during boot)
test -f /mnt/profile.browser && source /mnt/profile.browser
# source xrsh env
# install xrsh env
source /mnt/profile.xrsh
# source shell functions
source /mnt/profile.sh
# source js functions
./.profile.js
## forward not-found commands to javascript (via jsh)
command_not_found_handle(){
echo "[!] '$1' not found, did you mean $1(...) (javascript?)"
test -n "$ONBOARDING" && echo "[i] type 'help' for handy commands"
test -n "$ONBOARDING" || help
echo "$1 not found, did you mean $1(...) (javascript?)"
alert '[XRSH TIPS]'
alert 'js console: ' "type 'jsh'"
alert 'js shellfunction:' "type 'alias $1=\"jsh $1\"' to run '$1 yo' as $1('yo')"
alert 'js logging: ' "type 'echo 0 > /dev/browser/console.tty' to disable"
alert 'js capture log: ' "type 'tail -f /dev/browser/console'"
alert 'jsh<->sh hooks: ' "type 'chmod +x ~/hook.d/alert/* && alert helloworld'"
}
help(){
echo ""
echo 'TIPS'
echo '----'
echo 'js run ' "type 'js 'alert(\"hello\")'"
echo 'js console.log: ' "type 'console document.baseURI"
echo 'js function as cmd: ' "type 'alias $1=\"jsh $1\"' to run '$1 yo' as $1('yo')"
echo 'js inspect: ' "type 'js \"return document.baseURI\"'"
echo 'js console disable: ' "type 'echo 0 > /dev/browser/console.tty' to enable"
echo 'js capture console: ' "type 'tail -f /dev/browser/console'"
echo 'jsh<->sh hooks: ' "type 'chmod +x ~/hook.d/*/* && alert helloworld'"
echo 'include file into page' "type 'require <url.js|css>'"
echo 'create AFRAME a-entity' "type 'a_entity <componentname> [...]"
ONBOARDING=1
}
# source javascript functions
#js "$(cat /mnt/profile.js)"
resize
test $HOSTNAME = localhost || clear
#clear
cat /mnt/motd
export PATH=$PATH:/mnt:~/bin
export PS1="\n\[\033[38;5;57m\]x\[\033[38;5;93m\]r\[\033[38;5;129m\]s\[\033[38;5;165m\]h \[\033[38;5;201m\]# \[\033[0m\]"
export PATH=$PATH:/mnt
export PS1="\nxrsh # \033[0m"

View File

@ -1,6 +1,3 @@
#!/bin/js
window.helloworld = function(){
alert("hello world")
return "hello world"
}

View File

@ -1,57 +0,0 @@
hook(){
test -z "$1" && { echo "usage: hook <cmd_or_jsfunction> [args]"; return 0; }
cmd=$1
shift
test -d ~/hook.d/$cmd && {
find ~/hook.d/$cmd/ -type f -executable | while read hook; do
{ $hook "$@" || true; } | awk '{ gsub(/\/root\/\//,"",$1); $1 = sprintf("%-40s", $1)} 1'
done
}
}
alert(){
test -z "$1" && { echo "usage: alert <title> <message>"; return 0; }
title=$1
test -z "$1" || shift
msg="$*"
printf "%s \033[0m%s\n" "$title" "$msg"
hook alert $title "$msg"
}
confirm(){
test -z "$1" && { echo "usage: confirm <question>"; return 0; }
read -p "$(printf "\033[0m")[?] $1 [y/n] $(printf "\033[0m")" y
test $y = y && echo true && return 0
test $y = y || echo false
hook confirm $1 $y
}
prompt(){
test -z "$1" && { echo "usage: prompt <question> [answer_default]"; return 0; }
test -n "$2" && answer="[$2] " && answer_fallback="$2"
read -p "$(printf "\033[0m")[?] $1: $answer $(printf "\033[0m")" answer
test -z "$answer" && answer="$answer_fallback"
echo "$answer"
hook prompt $1 $answer
}
console(){
js 'return '$1
}
require(){
file=$(basename "$1")
js '(async () => {
await AFRAME.utils.require({"'$file'": "'$1'"})
})()'
}
a_entity(){
code="let el = document.createElement('a-entity')"
for i in "$@"; do
code="$code;\nel.setAttribute('$i','')\n";
done
code="document.querySelector('a-scene').appendChild(el)"
echo "$code"
js "$code"
}

View File

@ -3,53 +3,39 @@
test -d /dev/browser || {
setup_binaries(){
for bin in /mnt/js* /mnt/v86pipe /mnt/xrsh; do
for bin in /mnt/prompt /mnt/alert /mnt/confirm /mnt/hook /mnt/js* /mnt/v86pipe /mnt/xrsh; do
chmod +x $bin
ln -s $bin /bin/.
done
}
setup_links(){
ln -s /mnt/profile ~/.profile
ln -s /mnt/profile.js ~/.profile.js
ln -s /mnt/profile.browser ~/.profile.browser
ln -s /mnt/profile.sh ~/.profile.sh
ln -s /mnt/motd ~/.motd
ln -s ~/.profile.js ~/index.js
chmod +x ~/.profile.js
}
setup_browser_dev(){
mkdir -p /mnt/dev/browser
touch /mnt/dev/browser/js
touch /mnt/dev/browser/html
touch /mnt/console.tty
ln -s /mnt/dev/browser /dev/browser
# emulator.write_file() only writes to /mnt/. :(
# should be in /proc, but v86 gives 'no such file or dir' when creating it there
ln -s /mnt/console.tty /dev/browser/console.tty
echo 1 > /dev/browser/console.tty
touch /mnt/console && ln -s /mnt/console /dev/browser/console
touch /mnt/index.html && ln -s /mnt/index.html /dev/browser/index.html
ln -s /dev/browser/index.html ~/index.html
# setup console goodies
ln -s /mnt/console.tty /dev/browser/console.tty # emulator.write_file() only writes to /mnt/. :(
echo 1 > /dev/browser/console.tty # should be in /proc, but v86 gives 'no such file or dir' when creating it there
v86pipe /mnt/console /dev/browser/console &
test -f /etc/profile && rm /etc/profile
ln -s /mnt/profile /etc/profile
}
setup_hook_dirs(){ # see /mnt/hook for usage
mkdir ~/bin
mkdir -p ~/hook.d/alert
echo -e "#!/bin/sh\necho hook.d/alert/yo: yo \$*" > ~/hook.d/alert/yo
echo -e "#!/bin/js\nstr = \"hook.d/alert/yo.js yo \"+args.slice(1).join(' ')\nalert(str)\nreturn str" > ~/hook.d/alert/yo.js
echo -e "#!/usr/bin/env lua\nprint(\"hook.d/alert/yo.lua: yo \" .. arg[1])" > ~/hook.d/alert/yo.lua
echo -e "#!/usr/bin/awk -f\nBEGIN{\n\tprint \"hook.d/alert/yo.awk: yo \" ARGV[1]\n}" > ~/hook.d/alert/yo.awk
echo -e "#!/bin/sh\necho hello \$*" > ~/bin/hello
chmod +x ~/bin/hello
mkdir -p ~/hook.d/confirm
mkdir -p ~/hook.d/prompt
echo -e "#!/bin/sh\necho hook.d/alert/yo: yo \$*" > ~/hook.d/alert/yo
echo -e "#!/bin/js\nalert(\"hook.d/alert/yo.js \"+args.slice(1).join(' '))" > ~/hook.d/alert/yo.js
echo -e "#!/usr/bin/lua\nprint(\"hook.d/alert/yo.lua: yo \" .. arg[1])" > ~/hook.d/alert/yo.lua
}
setup_network(){
test -n "$BROWSER" || return 0
#mount -a
mount -a
udhcpc 1>>/var/log/network.log 2>>/var/log/network.log &
echo 0 > /proc/sys/kernel/printk
}
@ -57,7 +43,5 @@ test -d /dev/browser || {
setup_binaries
setup_browser_dev
setup_hook_dirs
setup_links
setup_network
}

3
com/isoterminal/mnt/prompt Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
read -p "$(printf "\033[0m")[?] $1: $(printf "\033[0m")" answer
echo "$answer"

26
com/isoterminal/mnt/test.awk Executable file
View File

@ -0,0 +1,26 @@
#!/bin/awk -f
BEGIN {
for (i = 1; i < ARGC; i++) {
options[i] = ARGV[i]
}
ARGC = 0
selected = 1
n = length(options)
while (1) {
printf "\r "
for (i = 1; i <= n; i++) {
if (i == selected)
printf "\033[44m%s\033[0m ", options[i]
else
printf "%s ", options[i]
}
if (c == 0) {
getline dir < "/dev/stdin"
print dir
if (dir == "up" && selected > 1) selected--
if (dir == "down" && selected < n) selected++
}
}
}

62
com/isoterminal/mnt/xrsh Executable file
View File

@ -0,0 +1,62 @@
#!/bin/sh
#
# a minimalistic terminal muxer
# Save the original stdout and stderr file descriptors for later restoration
exec 3>&1 4>&2
# Function to check if a session is already running on the given VT
is_session_running() {
vt_number=$1
# Check if any process is running on /dev/tty<vt_number>
fuser /dev/tty"$vt_number" >/dev/null 2>&1
return $?
}
# Function to mute the output of a session
mute_session() {
vt_number=$1
if is_session_running "$vt_number"; then
# Redirect stdout and stderr of the session to /dev/null
exec > /dev/null 2>&1
fi
}
# Function to unmute the current session (restore stdout and stderr)
unmute_session() {
exec 1>&3 2>&4 # Restore stdout and stderr from file descriptors 3 and 4
}
# Function to start a new session if not already running
start_or_switch_session() {
vt_number=$1
# Mute all other sessions except the one we're switching to
for vt in $(seq 1 12); do # Assuming you have up to 12 VTs, adjust as needed
if [ "$vt" != "$vt_number" ]; then
mute_session "$vt"
fi
done
if is_session_running "$vt_number"; then
echo "Switching to existing session on VT$vt_number"
unmute_session # Unmute the session we're switching to
chvt "$vt_number"
else
echo "Starting a new session on VT$vt_number"
openvt -c "$vt_number" -- /bin/sh &
sleep 1 # Give the session a moment to start
unmute_session # Unmute the new session
chvt "$vt_number"
fi
}
# Ensure a session number is provided
if [ "$#" -ne 1 ]; then
echo "Usage: $0 <session_number>"
exit 1
fi
# Start or switch to the session
start_or_switch_session $1

Binary file not shown.

View File

@ -1,70 +0,0 @@
importScripts("libv86.js");
importScripts("ISOTerminal.js") // we don't instance it again here (just use its functions)
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: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['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){
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){ 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 = function(e){
let {event,data} = e.data
if( this[event] ) this[event](data)
}

1
com/isoterminal/xrsh.iso Symbolic link
View File

@ -0,0 +1 @@
buildroot.iso

View File

@ -45,12 +45,9 @@ AFRAME.registerComponent('launcher', {
cols: { type:"number", "default": 5 }
},
dependencies:{
dom: "com/dom.js"
},
dependencies:['dom'],
init: async function () {
await AFRAME.utils.require(this.dependencies)
this.worldPosition = new THREE.Vector3()
await AFRAME.utils.require({
@ -380,7 +377,7 @@ AFRAME.registerSystem('launcher',{
updateLauncher: function(){
let launcher = document.querySelector('[launcher]')
if( launcher ) launcher.components.launcher.render()
if( launcher ) launcher.components['launcher'].render()
}
})

View File

@ -1,19 +1,18 @@
AFRAME.registerComponent('window', {
schema:{
title: {type:'string',"default":"title"},
width: {type:'string'}, // wrap
height: {type:'string',"default":'260px'},
uid: {type:'string'},
attach: {type:'selector'},
dom: {type:'selector'},
max: {type:'boolean',"default":false},
min: {type:'boolean',"default":false},
x: {type:'string',"default":"center"},
y: {type:'string',"default":"center"}
title: {type:'string',"default":"title"},
width: {type:'string'}, // wrap
height: {type:'string',"default":'260px'},
uid: {type:'string'},
attach: {type:'selector'},
dom: {type:'selector'},
x: {type:'string',"default":"center"},
y: {type:'string',"default":"center"}
},
dependencies:{
dom: "com/dom.js",
html: "https://unpkg.com/aframe-htmlmesh@2.1.0/build/aframe-html.js", // html to AFRAME
winboxjs: "https://unpkg.com/winbox@0.2.82/dist/winbox.bundle.min.js", // deadsimple windows: https://nextapps-de.github.io/winbox
//winboxcss: "https://unpkg.com/winbox@0.2.82/dist/css/winbox.min.css", // main theme
},
@ -35,15 +34,14 @@ AFRAME.registerComponent('window', {
id: this.data.uid || String(Math.random()).substr(4), // important hint for html-mesh
root: this.data.attach || document.body,
mount: this.data.dom,
max: this.data.max,
min: this.data.min,
onresize: () => this.el.emit('window.onresize',{}),
onmaximize: () => this.el.emit('window.onmaximize',{}),
oncreate: (e) => {
oncreate: () => {
this.el.emit('window.oncreate',{})
// resize after the dom content has been rendered & updated
setTimeout( () => {
if( !this.data.max ) winbox.resize( this.el.dom.offsetWidth+'px', this.el.dom.offsetHeight+'px' )
winbox.resize( this.el.dom.offsetWidth+'px', this.el.dom.offsetHeight+'px' )
setTimeout( () => this.el.setAttribute("html",`html:#${this.data.uid}; cursor:#cursor`), 1000)
// hint grabbable's obb-collider to track the window-object
this.el.components['obb-collider'].data.trackedObject3D = 'components.html.el.object3D.children.0'
this.el.components['obb-collider'].update()
@ -54,8 +52,9 @@ AFRAME.registerComponent('window', {
this.el.emit('window.onclose',e)
if( e.halt ) return true
this.data.dom.style.display = 'none';
if( this.el.parentNode ) this.el.remove() //parentElement.remove( this.el )
this.data.dom.parentElement.remove()
debugger
this.el.parentElement.remove( this.el )
return false
},
});
@ -63,9 +62,5 @@ AFRAME.registerComponent('window', {
this.el.setAttribute("grabbable","")
},
show: function(state){
this.el.dom.closest('.winbox').style.display = state ? '' : 'none'
}
})

111
com/xd.js Normal file
View File

@ -0,0 +1,111 @@
AFRAME.registerComponent('xd', {
schema: {
foo: { type:"string"}
},
init: function () {
if( Object.keys(this.el.components).length > 1 ) return // we collect a-entities which wish to be toggled in this.showElements()
this.el.sceneEl.addEventListener('enter-vr',() => this.toggle(true) )
this.el.sceneEl.addEventListener('exit-vr', () => this.toggle(false) )
this.el.sceneEl.addEventListener('2D', () => this.showElements(false) )
this.el.sceneEl.addEventListener('3D', () => this.showElements(true) )
// toggle immersive with ESCAPE
document.body.addEventListener('keydown', (e) => e.key == 'Escape' && this.toggle() )
document.head.innerHTML += `<style type="text/css">
.XR #toggle_overlay{
background: transparent;
color: #3aacff;
}
.XR #overlay{
visibility: hidden;
}
</style>`
this.events.launcher = () => this.toggle()
},
showElements: function(state){
let els = [...document.querySelectorAll('[xd]')]
els = els.filter( (el) => el != this.el ? el : null ) // filter out self
els.map( (el) => el.setAttribute("visible", state ? "true" : false ) )
},
// draw a button so we can toggle apps between 2D / XR
toggle: function(state){
state = state != undefined ? state : state || !document.body.className.match(/XR/)
document.body.classList[ state ? 'add' : 'remove'](['XR'])
AFRAME.scenes[0].emit( state ? '3D' : '2D')
},
manifest: { // HTML5 manifest to identify app to xrsh
"short_name": "XD",
"name": "2D/3D switcher",
"icons": [],
"id": "/?source=pwa",
"start_url": "/?source=pwa",
"background_color": "#3367D6",
"display": "standalone",
"scope": "/",
"theme_color": "#3367D6",
"category":"system",
"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": "use ESC-key to toggle between 2D / 3D",
"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.
`
}
});
AFRAME.utils.XD = function(){
return document.body.classList.contains('XR') ? '3D' : '2D'
}
AFRAME.utils.XD.getPositionInFrontOfCamera = function(distance){
const camera = AFRAME.scenes[0].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
}

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,261 +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({
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;
`)
// 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
// 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) => {
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,
fontSize: this.fontSize,
lineHeight: 1.15,
useFlowControl: true,
rendererType: this.renderType // 'dom' // 'canvas'
})
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)
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')
},
})