Compare commits

...

36 Commits

Author SHA1 Message Date
Leon van Kammen 9aff2133a0 improved performance on low-end smartphone
/ mirror_to_github (push) Successful in 19s Details
/ test (push) Successful in 3s Details
2024-10-04 15:49:15 +00:00
Leon van Kammen 2e56d235ed feat/webworker: work in progress [might break]
/ test (push) Successful in 6s Details
2024-10-04 09:08:39 +00:00
Leon van Kammen 0e8d173289 feat/webworker: work in progress [might break]
/ test (push) Successful in 6s Details
2024-10-03 10:41:53 +00:00
Leon van Kammen 0f3c7a0184 feat/webworker: work in progress [might break]
/ test (push) Successful in 5s Details
2024-10-02 19:03:04 +00:00
Leon van Kammen 6b9cd42569 huge refactor to webworker..tears and happiness..
/ test (push) Successful in 4s Details
2024-10-01 17:07:03 +00:00
Leon van Kammen 35ff5dc710 main: work in progress [might break]
/ mirror_to_github (push) Successful in 18s Details
/ test (push) Successful in 5s Details
2024-09-25 13:58:40 +00:00
Leon van Kammen c6ce294e69 xterm fix
/ mirror_to_github (push) Successful in 20s Details
/ test (push) Successful in 5s Details
2024-09-24 16:29:59 +00:00
Leon van Kammen eb086dfd74 xterm fix
/ mirror_to_github (push) Successful in 19s Details
/ test (push) Successful in 3s Details
2024-09-24 14:27:48 +00:00
Leon van Kammen bc82d87541 xterm improvment
/ mirror_to_github (push) Successful in 20s Details
/ test (push) Successful in 4s Details
2024-09-24 09:53:23 +00:00
Leon van Kammen 9d69ef9c57 xterm repaint improvements (still not great)
/ mirror_to_github (push) Successful in 19s Details
/ test (push) Successful in 5s Details
2024-09-23 20:31:28 +00:00
Leon van Kammen 44f79ac02a added Cousine references
/ mirror_to_github (push) Successful in 23s Details
/ test (push) Successful in 5s Details
2024-09-23 16:01:40 +00:00
Leon van Kammen 36b9eaadcb add Cousine fonts
/ mirror_to_github (push) Successful in 22s Details
/ test (push) Successful in 4s Details
2024-09-23 15:51:02 +00:00
Leon van Kammen 17e07b0834 conversion code is in one place now
/ mirror_to_github (push) Successful in 18s Details
/ test (push) Successful in 4s Details
2024-09-23 14:59:29 +00:00
Leon van Kammen 8369dde488 finetuned autorestore and codemirror
/ test (push) Successful in 4s Details
2024-09-23 14:58:48 +00:00
Leon van Kammen f75f34d6a6 added restore + requestAnimationFrameXR to mitigate requestAnimationFrame issues
/ test (push) Successful in 5s Details
2024-09-17 16:59:38 +00:00
Leon van Kammen 10c274fdd4 configured fallback ISO url
/ test (push) Successful in 5s Details
2024-09-16 11:38:55 +00:00
Leon van Kammen 7c34805952 improved xterm<->iso<->uart<->javascript communication + added codemirror startingpoint
/ test (push) Successful in 5s Details
2024-09-16 11:28:28 +00:00
Leon van Kammen 0bec71ab55 added NLnet
/ mirror_to_github (push) Successful in 25s Details
/ test (push) Successful in 6s Details
2024-09-13 17:02:48 +02:00
Leon van Kammen b1bbaac311 trimming the fat..going for a xrsh-driven way to include files
/ mirror_to_github (push) Successful in 20s Details
/ test (push) Successful in 3s Details
2024-09-06 16:02:54 +00:00
Leon van Kammen 31b6f99050 focus
/ mirror_to_github (push) Successful in 24s Details
/ test (push) Successful in 6s Details
2024-09-06 08:39:48 +00:00
Leon van Kammen 658e113853 disabled other menuitems
/ mirror_to_github (push) Successful in 20s Details
/ test (push) Successful in 5s Details
2024-09-06 08:30:24 +00:00
Leon van Kammen 98c3adb791 js now returns output
/ mirror_to_github (push) Successful in 20s Details
/ test (push) Successful in 5s Details
2024-09-05 17:09:40 +00:00
Leon van Kammen af7e837554 bugfix: terminal now works in AR & immersive-pause mode again
/ mirror_to_github (push) Successful in 22s Details
/ test (push) Successful in 4s Details
2024-09-04 18:44:49 +00:00
Leon van Kammen 590647ece8 refactor: separate features into files **end-of-crazy-prototyping**
/ mirror_to_github (push) Successful in 22s Details
/ test (push) Successful in 4s Details
2024-09-03 16:33:35 +00:00
Leon van Kammen f9cdea25e5 main: work in progress [might break]
/ mirror_to_github (push) Successful in 30s Details
/ test (push) Successful in 5s Details
2024-09-02 13:08:45 +00:00
Leon van Kammen 54c547fe4d javascript bridge fixes
/ mirror_to_github (push) Successful in 23s Details
/ test (push) Successful in 5s Details
2024-09-02 13:05:47 +00:00
Leon van Kammen 3c3340144f main: work in progress [might break]
/ mirror_to_github (push) Successful in 17s Details
/ test (push) Successful in 4s Details
2024-08-30 11:14:11 +00:00
Leon van Kammen 2ba3cc185a source /mnt/profile.js on boot + bugfix cmd fragment
/ mirror_to_github (push) Successful in 17s Details
/ test (push) Successful in 4s Details
2024-08-30 11:09:37 +00:00
Leon van Kammen 3d52b69340 main: minor bugfix
/ mirror_to_github (push) Successful in 26s Details
/ test (push) Successful in 8s Details
2024-08-30 10:52:41 +00:00
Leon van Kammen 04c9bb825b main: minor bugfix
/ mirror_to_github (push) Successful in 17s Details
/ test (push) Successful in 3s Details
2024-08-30 10:48:57 +00:00
Leon van Kammen 72b55b7c5d main: minor bugfix
/ mirror_to_github (push) Successful in 26s Details
/ test (push) Successful in 8s Details
2024-08-30 10:42:29 +00:00
Leon van Kammen f7a178b5e7 main: minor bugfix
/ mirror_to_github (push) Successful in 20s Details
/ test (push) Successful in 6s Details
2024-08-30 10:40:59 +00:00
Leon van Kammen fdc1d55637 main: work in progress [might break]
/ mirror_to_github (push) Successful in 19s Details
/ test (push) Successful in 4s Details
2024-08-30 10:35:27 +00:00
Leon van Kammen 4cdba16072 main: work in progress [might break]
/ mirror_to_github (push) Successful in 17s Details
/ test (push) Successful in 4s Details
2024-08-29 17:54:35 +02:00
Leon van Kammen 69a1760a11 improved javascript bridge
/ mirror_to_github (push) Successful in 20s Details
/ test (push) Successful in 4s Details
2024-08-29 15:18:14 +00:00
Leon van Kammen 7bfa591536 various unix utilities to interface with browser
/ test (push) Successful in 4s Details
2024-08-27 18:40:05 +00:00
50 changed files with 2709 additions and 1914 deletions

3
.gitignore vendored Normal file
View File

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

View File

@ -11,3 +11,11 @@ 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

@ -1,75 +0,0 @@
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.
`
}
});

View File

@ -1,182 +0,0 @@
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)
})
}
})

View File

@ -1,111 +0,0 @@
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.
`
}
});

View File

@ -1,130 +0,0 @@
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.
`
}
});

View File

@ -1,95 +0,0 @@
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.
`
}
});

159
com/codemirror.js Normal file
View File

@ -0,0 +1,159 @@
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,7 +18,9 @@
*
*/
AFRAME.registerComponent('data2event',{
if( !AFRAME.components.data2event ){
AFRAME.registerComponent('data2event',{
init: function(){
setTimeout( () => {
@ -43,4 +45,6 @@ AFRAME.registerComponent('data2event',{
})
},
})
})
}

View File

@ -32,7 +32,13 @@
* | `DOMready` | self | fired when dom component (`this.dom`) is created |
*/
AFRAME.registerComponent('dom',{
if( !AFRAME.components.dom ){
AFRAME.registerComponent('dom',{
requires: {
"requestAnimationFrameXR": "com/requestAnimationFrameXR.js"
},
init: function(){
Object.values(this.el.components)
@ -53,7 +59,6 @@ AFRAME.registerComponent('dom',{
.assignUniqueID()
.scaleDOMvsXR()
.triggerKeyboardForInputs()
.stubRequestAnimationFrame()
document.querySelector('#overlay').appendChild(this.el.dom)
this.el.emit('DOMready',{el: this.el.dom})
@ -125,12 +130,5 @@ AFRAME.registerComponent('dom',{
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 )
}
}
})
})
}

View File

@ -57,9 +57,7 @@ AFRAME.registerComponent('helloworld-htmlform', {
const instance = this.el.cloneNode(false)
this.el.sceneEl.appendChild( instance )
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("show-texture-in-xr", "") // only show aframe-html in xr
instance.setAttribute("grabbable","")
instance.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera
@ -75,7 +73,11 @@ AFRAME.registerComponent('helloworld-htmlform', {
root: document.querySelector("#overlay"),
mount: instance.dom,
onclose: () => { instance.dom.style.display = 'none'; return false; },
oncreate: () => instance.setAttribute("html",`html:#${instance.uid}; cursor:#cursor`)
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
}
});
instance.dom.style.display = AFRAME.utils.XD() == '3D' ? 'none' : '' // show/hide

View File

@ -87,7 +87,9 @@ 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
xd: "./com/dom.js", // allow switching between 2D/3D
xrtexture: "./com/html-as-texture-in-xr.js", // allow switching between 3D/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", //
@ -40,9 +40,6 @@ AFRAME.registerComponent('helloworld-window', {
const instance = this.el.cloneNode(false)
this.el.sceneEl.appendChild( instance )
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
@ -59,7 +56,9 @@ AFRAME.registerComponent('helloworld-window', {
root: document.querySelector("#overlay"),
mount: instance.dom,
onclose: () => { instance.dom.style.display = 'none'; return false; },
oncreate: () => instance.setAttribute("html",`html:#${instance.uid}; cursor:#cursor`)
oncreate: () => {
instance.setAttribute("show-texture-in-xr",`domid: #${instance.uid}`) // only show aframe-html texture in xr mode
}
});
instance.dom.style.display = '' // show

View File

@ -0,0 +1,126 @@
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,52 +1,150 @@
AFRAME.registerComponent('isoterminal', {
/*
*
* 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())
*/
if( typeof AFRAME != 'undefined '){
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
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
},
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) )
},
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"
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"
},
dom: {
scale: 0.7,
//scale: 0.5,
events: ['click','keydown'],
html: (me) => `<div class="isoterminal"></div>`,
html: (me) => `<div class="isoterminal">
</div>`,
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;
.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');
}
.isoterminal style{ display:none }
.wb-body:has(> .isoterminal){
background: #000c;
overflow:hidden;
#overlay .winbox:has(> .isoterminal){
background:transparent;
box-shadow:none;
}
.isoterminal div{ display:block; }
.isoterminal span{ display: inline }
.wb-body:has(> .isoterminal){
background: #000C;
overflow:hidden;
border-radius:7px;
}
.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;
}
@keyframes fade {
from { opacity: 1.0; }
@ -60,200 +158,133 @@ AFRAME.registerComponent('isoterminal', {
to { opacity: 1.0; }
}
.blink {
.isoterminal .xterm-rows.xterm-focus .xterm-cursor.xterm-cursor-block,
.blink{
animation:fade 1000ms infinite;
-webkit-animation:fade 1000ms infinite;
}
`
},
toUint8Array: function(str) {
// Create a new Uint8Array with the same length as the input string
const uint8Array = new Uint8Array(str.length);
initTerminal: async function(singleton){
// Iterate over the string and populate the Uint8Array
for (let i = 0; i < str.length; i++) {
uint8Array[i] = str.charCodeAt(i);
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"
}
return uint8Array;
},
runISO: function(dom,instance){
//var term = new Terminal()
//term.open(dom)
//term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ')``
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',
'asking LLM why men have nipples',
'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/profile",
"com/isoterminal/mnt/motd",
]
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
`))
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) )
})
})
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",
})
//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;
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)')
}
instance = this.instance = this.el.cloneNode(false)
this.el.sceneEl.appendChild( instance )
}
if(chr === "\n")
{
var new_line = line;
line = "";
}
else if(chr >= " " && chr <= "~")
{
line += chr;
}
// init isoterminal
this.isoterminal = new ISOTerminal(instance,this.data)
//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.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}`)
})
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')
// 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(`return ${script}`))()
if( res && typeof res != 'string' ) res = JSON.stringify(res,null,2)
emulator.create_file( "dev/browser/js", this.toUint8Array( res || '' ) )
}catch(e){
console.dir(e)
emulator.create_file("dev/browser/js", this.toUint8Array( `[e] `+String(e.stack) ) )
}
}
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) => { }
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 )
}
}
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) )
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:{
@ -265,89 +296,10 @@ AFRAME.registerComponent('isoterminal', {
// 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)')
this.initTerminal()
}
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', () => {
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.addEventListener('window.onclose', (e) => {
if( !confirm('do you want to kill this virtual machine and all its processes?') ) e.halt = true
})
const resize = (w,h) => {
if( 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 )
instance.setAttribute("dom", "")
instance.setAttribute("xd", "") // allows flipping between DOM/WebGL when toggling XD-button
instance.setAttribute("visible", AFRAME.utils.XD() == '3D' ? 'true' : 'false' )
instance.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.5) )
const focus = () => document.querySelector('canvas.a-canvas').focus()
instance.addEventListener('obbcollisionstarted', focus )
this.el.sceneEl.addEventListener('enter-vr', focus )
instance.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera
}
},
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
@ -394,13 +346,14 @@ AFRAME.registerComponent('isoterminal', {
}
],
"help":`
Helloworld application
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.
This is a help file which describes the application.
It will be rendered thru troika text, and will contain
headers based on non-punctualized lines separated by linebreaks,
in above's case "\nHelloworld application\n" will qualify as header.
`
}
});
});
}

View File

@ -0,0 +1,318 @@
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

@ -0,0 +1,16 @@
--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

@ -0,0 +1,48 @@
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

@ -0,0 +1,63 @@
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

@ -0,0 +1,22 @@
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

@ -0,0 +1,32 @@
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

@ -0,0 +1,30 @@
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

@ -0,0 +1,31 @@
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

@ -0,0 +1,53 @@
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

@ -0,0 +1,13 @@
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

@ -0,0 +1,120 @@
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) );
}

File diff suppressed because it is too large Load Diff

View File

@ -1,2 +0,0 @@
#!/bin/sh
jsh alert "$1"

View File

@ -1,10 +0,0 @@
#!/bin/sh
test -f /mnt/V86 && {
jsh confirm $1 $2
}
test -f /mnt/V86 || {
read -p "$(printf "\033[0m")[?] $1 [y/n] $(printf "\033[0m")" y
test $y = y && echo true && exit
echo false
}

View File

@ -1,7 +0,0 @@
PS3="Enter a number: "
select character in Sheldon Leonard Penny Howard Raj
do
echo "Selected character: $character"
echo "Selected number: $REPLY"
done

View File

@ -1,5 +1,31 @@
#!/bin/sh
flock /dev/browser/js -c "echo '$*' > /dev/browser/js; cat /dev/browser/js"
PID=$$
test -z "$1" && { echo "Usage: js 'somefunction(1)'"; exit 0; }
# we use flock, an awesome way to make processes read/write the same file
# whilr preventing 1001 concurrency issues
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
}
# should we use flock, an awesome way to make processes read/write the same file
# while preventing 1001 concurrency issues?
# attempt:
#
# flock /dev/browser/js -c "echo \"$javascript\" > /dev/browser/js"

View File

@ -3,15 +3,42 @@
#
# 'jsh prompt question answer' executes: js prompt('question','answer') )
source /mnt/profile.sh
to_js(){
printf "%s(" "$1"
shift
for arg in "$@"; do
case "$arg" in
(*[\.0-9]*)
printf '%s,' "$arg"
;;
(*)
printf '"%s",' "$arg"
;;
esac
done
printf ")\n"
}
func=$(to_js "$@")
func=${func/,)/)}
/mnt/js "$func"
# run argument as js
test -z "$1" || {
func=$(to_js "$@")
func=${func/,)/)}
js "return $func"
hook "$@"
exit 0
}
# otherwise start repl
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")"
read line
test "$line" = exit && exit
js "$line"
done

View File

@ -5,11 +5,12 @@
 _ _ / \ | | \/ \ Y / _ _ _ _ _ _ _
 . . /___/\ \ |____|_ /_______ /\___|_ /. . . . . . . .
 . . . . . .\_/. . . . \/ . . . .\/ . . _ \/ . . . . . . . .
 ▬ ▬ ▬ https://xrsh.isvery.ninja ▬ ▬ ▬ ▬ ▬ ▬ ▬ ▬ ▬ ▬ ▬ ▬ ▬ ▬
 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
 ▬▬▬▬ https://xrsh.isvery.ninja ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
Open, local-first, hackable & selfcontained XR apps.
Open, local-first, unix hackable & selfcontained XR apps.
credits: all FOSS devs | @lvk@mastodon.online
copy.sh (v86) | @utopiah@mastodon.pirateparty.be
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

View File

@ -1,29 +1,41 @@
install_xrsh(){
chmod +x /mnt/confirm /mnt/prompt /mnt/alert /mnt/js*
# source URL data from v86 (file created by v86 during boot)
test -f /mnt/profile.browser && source /mnt/profile.browser
setup_browser_dev(){
mkdir -p /mnt/dev/browser
touch /mnt/dev/browser/js
touch /mnt/dev/browser/html
ln -s /mnt/dev/browser /dev/browser
test -f /etc/profile && rm /etc/profile
ln -s /mnt/profile /etc/profile
}
# source xrsh env
source /mnt/profile.xrsh
setup_browser_dev
# 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
}
test -d /dev/browser || install_xrsh
test -f /mnt/V86 && {
mount -a
udhcpc 1>>/var/log/network.log 2>>/var/log/network.log &
echo 0 > /proc/sys/kernel/printk
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
}
resize
#clear
test $HOSTNAME = localhost || clear
cat /mnt/motd
export PATH=$PATH:/mnt
export PS1="\nxrsh # \033[0m"
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\]"

View File

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

View File

@ -0,0 +1,57 @@
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

@ -0,0 +1,63 @@
#!/bin/sh
test -d /dev/browser || {
setup_binaries(){
for bin in /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/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
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
}
setup_network(){
test -n "$BROWSER" || return 0
#mount -a
udhcpc 1>>/var/log/network.log 2>>/var/log/network.log &
echo 0 > /proc/sys/kernel/printk
}
setup_binaries
setup_browser_dev
setup_hook_dirs
setup_links
setup_network
}

View File

@ -1,9 +0,0 @@
#!/bin/sh
test -f /mnt/V86 && {
jsh prompt "$1" "$2"
}
test -f /mnt/V86 || {
read -p "$(printf "\033[0m")[?] $1: $(printf "\033[0m")" answer
echo "$answer"
}

View File

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

30
com/isoterminal/mnt/v86pipe Executable file
View File

@ -0,0 +1,30 @@
#!/bin/sh
#
# this daemon allows 'tail -f' on v86 files (which don't persist inode when updated
# via javascript)
# more info see: https://github.com/copy/v86/issues/1140
#
# Hopefully as V86 (or my understanding of it) matures, this will be no longer needed
test -z $2 && { echo "usage: v86pipe <logfile> <namedpipe>"; exit 0; }
# Start reading from the last line in the log file
last_size=0
LOG_FILE=$1
LOG_PIPE=$2
test -f $LOG_FILE || touch $LOG_FILE
test -p $LOG_PIPE || mkfifo $LOG_PIPE
while true; do
# Get the current size of the file using wc -c (count bytes)
current_size=$(wc -c < $LOG_FILE)
test $current_size = $last_size || {
cat $LOG_FILE > $LOG_PIPE
truncate -s 0 $LOG_FILE
}
last_size=$current_size
# Sleep for a moment to avoid excessive CPU usage
sleep 0.2
done

Binary file not shown.

70
com/isoterminal/worker.js Normal file
View File

@ -0,0 +1,70 @@
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)
}

View File

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

View File

@ -45,9 +45,12 @@ AFRAME.registerComponent('launcher', {
cols: { type:"number", "default": 5 }
},
dependencies:['dom'],
dependencies:{
dom: "com/dom.js"
},
init: async function () {
await AFRAME.utils.require(this.dependencies)
this.worldPosition = new THREE.Vector3()
await AFRAME.utils.require({
@ -377,7 +380,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

@ -6,13 +6,14 @@ AFRAME.registerComponent('window', {
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"}
},
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
},
@ -34,14 +35,15 @@ 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: () => {
oncreate: (e) => {
this.el.emit('window.oncreate',{})
// resize after the dom content has been rendered & updated
setTimeout( () => {
winbox.resize( this.el.dom.offsetWidth+'px', this.el.dom.offsetHeight+'px' )
setTimeout( () => this.el.setAttribute("html",`html:#${this.data.uid}; cursor:#cursor`), 1000)
if( !this.data.max ) winbox.resize( this.el.dom.offsetWidth+'px', this.el.dom.offsetHeight+'px' )
// 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()
@ -52,9 +54,8 @@ 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
},
});
@ -62,5 +63,9 @@ AFRAME.registerComponent('window', {
this.el.setAttribute("grabbable","")
},
show: function(state){
this.el.dom.closest('.winbox').style.display = state ? '' : 'none'
}
})

111
com/xd.js
View File

@ -1,111 +0,0 @@
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
}

View File

@ -1,81 +0,0 @@
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.
`
}
});

261
com/xterm.js Normal file
View File

@ -0,0 +1,261 @@
/*
* 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')
},
})