Compare commits

..

No commits in common. "main" and "feat/copy-paste-drag-drop" have entirely different histories.

28 changed files with 2774 additions and 2931 deletions

2
.env
View File

@ -1,2 +0,0 @@
git remote | grep codeberg || git remote add codeberg git@codeberg.org:xrsh/xrsh-com.git
git remote | grep c-frame || git remote add c-frame git@github.com:c-frame/xrsh-com.git

View File

@ -13,7 +13,7 @@ jobs:
- run: "echo \"${{ secrets.SSHKEY_APPS }}\" > ~/.ssh/id_rsa"
- run: ssh-keyscan github.com >> ~/.ssh/known_hosts # see https://gist.github.com/vikpe/34454d69fe03a9617f2b009cc3ba200b
- run: chmod 600 -R ~/.ssh
- run: git remote add github git@github.com:c-frame/xrsh-com.git
- run: git remote add github git@github.com:coderofsalvation/xrsh-apps
- run: git push github main
# *todo* trigger deploy at website
#- run: git clone git@github.com:coderofsalvation/xrsh xrsh.github # now push empty commit to deploy website

View File

@ -1,41 +0,0 @@
#!/usr/bin/env -S awk -f
# a no-nonsense source-to-markdown generator which scans for:
#
# /**
# * # foo
# *
# * this is markdown $(cat bar.md)
# */
#
# var foo; // comment with 2 leading spaces is markdown too $(date)
#
# easily refactorable to hash-based languages (py/bash/perl/lua e.g.)
# by changing the regexes
#
BEGIN{
# printf README.md until '# Component List'
system("grep -B9999 '# Component List' README.md")
print ""
}
/\$\(/ { cmd=$0;
gsub(/^.*\$\(/,"",cmd);
gsub(/\).*/,"",cmd);
cmd | getline stdout; close(cmd);
sub(/\$\(.*\)/,stdout);
}
/\/\*\*/ { doc=1; sub(/^.*\/\*/,""); }
doc && /\*\// { doc=0;
sub(/[[:space:]]*\*\/.*/,"");
sub(/^[[:space:]]*\*[[:space:]]?/,"");
print
}
doc && /^[[:space:]]*\*/ { sub(/^[[:space:]]*\*[[:space:]]?/,"");
print
}
#!doc && /\/\/ / { sub(".*// ","");
# sub("# ","\n# ");
# sub("> ","\n> ");
# print
# }

164
README.md
View File

@ -1,175 +1,21 @@
# XRshell apps & components
<img src='https://codeberg.org/xrsh/xrsh/media/branch/main/xrsh.svg' width="25%"/>
<img src='https://github.com/coderofsalvation/xrshell/raw/main/src/assets/logo.svg' width="25%"/>
This is a library of useful AFRAME components used in [XRSH](https://xrsh.isvery.ninja) [or in any AFRAME app].<br>
Characteristics:
* selfcontained
* auto-loading of dependencies (via [AFRAME.utils.require()](com/require.js) see this [example](com/example/helloworld.js))
This is a library of useful AFRAME components which can be used in any AFRAME app, which are higher-level than usual (and used in [XRSH](https://coderofsalvation.github.io/xrsh):
# Usage
```html
<script src="https://codeberg.org/xrsh/xrsh-com/com/require.js"/>
<script src="https://codeberg.org/xrsh/xrsh-com/com/example/helloworld.js"/>
<script src="https://coderofsalvation.github.io/xrsh/src/com/example/helloworld.js"/>
<a-entity helloworld="foo:1" class="cubes" name="box">
```
See component list below
## Funding
> this README.md is generated by running `echo "$(./README.awk com/*.js)" > README.md`
## Credits
This project is partially 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).
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)
# Component List
## [data_events](com/data_events.js)
allows components to react to data changes
```html
<script>
AFRAME.registerComponent('mycom',{
init: function(){ this.data.foo = 1 },
event: {
foo: (e) => alert("I was updated!")
}
})
</script>
<a-entity mycom data_events/>
```
## [html-as-texture-in-xr](com/html-as-texture-in-xr.js)
shows domid **only** in immersive mode
(wrapper around [aframe-htmlmesh](https://ada.is/aframe-htmlmesh/)
It also sets class 'XR' to the (HTML) body-element in immersive mode.
This allows CSS (in [dom component](com/dom.js)) to visually update accordingly.
> depends on [AFRAME.utils.require](com/require.js)
```html
<style type="text/css">
.XR #foo { color:red; }
</style>
<a-entity html-as-texture-in-xr="domid: #foo">
<b id="foo">hello</b>
</a-entitiy>
```
| property | type |
|--------------|--------------------|
| `domid` | `string` |
| event | target | info |
|--------------|------------|--------------------------------------|
| `3D` | a-scene | fired when going into immersive mode |
| `2D` | a-scene | fired when leaving immersive mode |
## [isoterminal](com/isoterminal.js)
Renders a windowed terminal in both (non)immersive mode.
It displays an interactive javascript console or boots into
a Linux ISO image (via WASM).
```html
<a-entity isoterminal="iso: xrsh.iso" position="0 1.6 -0.3"></a-entity>
```
> depends on [AFRAME.utils.require](com/require.js)
| property | type | default | info |
|------------------|-----------|------------------------|------|
| `iso` | `string` | https`//forgejo.isvery.ninja/assets/xrsh-buildroot/main/xrsh.iso" | |
| `overlayfs` | `string` | *WORK-IN-PROGRESS* | |
| `width` | `number` | 800 ||
| `height` | `number` | 600 ||
| `depth` | `number` | 0.03 ||
| `lineHeight` | `number` | 18 ||
| `prompt` | `boolean` | true | boot straight into ISO or give user choice |
| `padding` | `number`` | 18 | |
| `maximized` | `boolean` | false | |
| `minimized` | `boolean` | false | |
| `muteUntilPrompt`| `boolean` | true | mute stdout until a prompt is detected in ISO |
| `HUD` | `boolean` | false | link to camera movement |
| `transparent` | `boolean` | false | heavy, needs good gpu |
| `memory` | `number` | 60 | VM memory (in MB) [NOTE` quest or smartphone webworker might crash > 40mb ] |
| `bufferLatency` | `number` | 1 | in ms` bufferlatency from webworker to term (batch-update every char to texture) |
| `debug` | `boolean` | false | |
| `emulator` | `string` | fbterm | terminal emulator |
> for more info see [xrsh.isvery.ninja](https://xrsh.isvery.ninja)
Component design:
```
css/html template
┌─────────┐ ┌────────────┐ exit-AR
┌───────►│ com/dom ┼──►│ com/window ├───────────────── exit-VR ◄─┐
│ └─────────┘ └───────────┬┘ │
│ │ │
┌──────────┴────────┐ │ ┌───────────┐ ┌─────────────────────────────┐
│ com/isoterminal ├────────────────────────────►│com/term.js│ │com/html-as-texture-in-XR.js │
└────────┬─┬────────┘ │ └──┬─────┬▲─┘ └─────────────────────────────┘
│ │ ┌────────┐ ┌──▼──────▼──────┐ ││ │
│ └───────►│ plane ├─────►text───┼►div#isoterminal│◄────────────────── 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())
```
## [pastedrop](com/pastedrop.js)
detects user copy/paste and file dragdrop action
and clipboard functions
```html
<a-entity pastedrop/>
```
| event | target | info |
|--------------|--------|------------------------------------------|
| `pasteFile` | self | always translates input to a File object |
## [require](com/require('').js)
automatically requires dependencies
```javascript
await AFRAME.utils.require( this.dependencies ) (*) autoload missing components
await AFRAME.utils.require( this.el.getAttributeNames() ) (*) autoload missing components
await AFRAME.utils.require({foo: "https://foo.com/aframe/components/foo.js"},this)
await AFRAME.utils.require(["./app/foo.js","foo.css"],this)
```
> (*) = prefixes baseURL AFRAME.utils.require.baseURL ('./com/' e.g.)

187
com/codemirror.js Normal file
View File

@ -0,0 +1,187 @@
if( AFRAME.components.codemirror ) delete AFRAME.components.codemirror
AFRAME.registerComponent('codemirror', {
schema: {
file: { type:"string"},
term: { type:"selector", default: "[isoterminal]" },
width: { type:"number", default:700},
height: { type:"number", default:500},
},
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.term
//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: ${me.com.data.width}px !important;
height: ${me.com.data.height-30}px !important;
}
.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:has(> .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}; width: ${this.data.width}px; height: ${this.data.height}px`)
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.throttle( (file,str) => {
this.updateFile(file,str)
}, 1500)
this.editor.on('change', (instance,changeObj) => {
this.editor.updateFile( this.data.file, instance.getValue() )
})
this
.handleFocus()
setTimeout( () => {
this.el.setAttribute("html-as-texture-in-xr", `domid: #${this.el.dom.id}`) // only show aframe-html in xr
},1500)
},
handleFocus: function(){
const focus = (showdom) => (e) => {
if( this.editor ){
this.editor.focus()
}
if( this.el.components.window && this.data.renderer == 'canvas'){
this.el.components.window.show( showdom )
}
}
this.el.addEventListener('obbcollisionstarted', focus(false) )
this.el.sceneEl.addEventListener('enter-vr', focus(false) )
this.el.sceneEl.addEventListener('enter-ar', focus(false) )
this.el.sceneEl.addEventListener('exit-vr', focus(true) )
this.el.sceneEl.addEventListener('exit-ar', focus(true) )
},
updateFile: async function(file,str){
// we don't do via shellcmd: isoterminal.exec(`echo '${str}' > ${file}`,1)
// as it would require all kindof ugly stringescaping
console.log("updating "+file)
await this.isoterminal.worker.update_file(file, this.isoterminal.convert.toUint8Array(str) )
this.isoterminal.exec("touch "+file) // *FIXME* notify filesystem (why does inotifyd need this? v86's 9pfees is cached?)
},
events:{
// component events
DOMready: function(e){
this.isoterminal.worker.read_file(this.data.file)
.then( this.isoterminal.convert.Uint8ArrayToString )
.then( (str) => {
console.log("creating editor")
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

@ -1,5 +1,5 @@
/**
* ## [data_events](com/data_events.js)
/*
* ## data_events
*
* allows components to react to data changes
*

View File

@ -1,5 +1,5 @@
/*
* ## [dom](com/dom.js)
* ## dom
*
* instances reactive DOM component from AFRAME component's `dom` metadata
*
@ -36,6 +36,10 @@ if( !AFRAME.components.dom ){
AFRAME.registerComponent('dom',{
requires: {
"requestAnimationFrameXR": "com/requestAnimationFrameXR.js"
},
init: function(){
Object.values(this.el.components)
.map( (c) => {

View File

@ -1,35 +1,3 @@
/**
* ## [html-as-texture-in-xr](com/html-as-texture-in-xr.js)
*
* shows domid **only** in immersive mode
* (wrapper around [aframe-htmlmesh](https://ada.is/aframe-htmlmesh/)
*
* It also sets class 'XR' to the (HTML) body-element in immersive mode.
* This allows CSS (in [dom component](com/dom.js)) to visually update accordingly.
*
* > depends on [AFRAME.utils.require](com/require.js)
*
* ```html
* <style type="text/css">
* .XR #foo { color:red; }
* </style>
*
* <a-entity html-as-texture-in-xr="domid: #foo">
* <b id="foo">hello</b>
* </a-entitiy>
* ```
*
* | property | type |
* |--------------|--------------------|
* | `domid` | `string` |
*
* | event | target | info |
* |--------------|------------|--------------------------------------|
* | `3D` | a-scene | fired when going into immersive mode |
* | `2D` | a-scene | fired when leaving immersive mode |
*
*/
if( !AFRAME.components['html-as-texture-in-xr'] ){
AFRAME.registerComponent('html-as-texture-in-xr', {

View File

@ -1,68 +1,32 @@
/**
* ## [isoterminal](com/isoterminal.js)
/*
*
* Renders a windowed terminal in both (non)immersive mode.
* It displays an interactive javascript console or boots into
* a Linux ISO image (via WASM).
*
* ```html
* <a-entity isoterminal="iso: xrsh.iso" position="0 1.6 -0.3"></a-entity>
* ```
*
* > depends on [AFRAME.utils.require](com/require.js)
*
* | property | type | default | info |
* |------------------|-----------|------------------------|------|
* | `iso` | `string` | https`//forgejo.isvery.ninja/assets/xrsh-buildroot/main/xrsh.iso" | |
* | `overlayfs` | `string` | *WORK-IN-PROGRESS* | |
* | `width` | `number` | 800 ||
* | `height` | `number` | 600 ||
* | `depth` | `number` | 0.03 ||
* | `lineHeight` | `number` | 18 ||
* | `prompt` | `boolean` | true | boot straight into ISO or give user choice |
* | `padding` | `number`` | 18 | |
* | `maximized` | `boolean` | false | |
* | `minimized` | `boolean` | false | |
* | `muteUntilPrompt`| `boolean` | true | mute stdout until a prompt is detected in ISO |
* | `HUD` | `boolean` | false | link to camera movement |
* | `transparent` | `boolean` | false | heavy, needs good gpu |
* | `memory` | `number` | 60 | VM memory (in MB) [NOTE` quest or smartphone webworker might crash > 40mb ] |
* | `bufferLatency` | `number` | 1 | in ms` bufferlatency from webworker to term (batch-update every char to texture) |
* | `debug` | `boolean` | false | |
* | `emulator` | `string` | fbterm | terminal emulator |
*
* > for more info see [xrsh.isvery.ninja](https://xrsh.isvery.ninja)
*
* Component design:
* ```
* css/html template
*
* exit-AR
* com/dom com/window exit-VR
*
*
*
* com/isoterminal com/term.js com/html-as-texture-in-XR.js
*
*
* plane textdiv#isoterminal enter-VR
* enter-AR
*
*
* ISOTerminal.js
*
* com/isoterminal/worker.js
*
* v86.js
* feat/*.js
* libv86.js
*
*
*
* exit-AR
* com/dom com/window domrenderer exit-VR
*
*
* xterm.js
* com/isoterminal com/xterm.js com/html-as-texture-in-XR.js
* xterm.css
*
* plane textcanvas enter-VR
* enter-AR
* renderer=canvas
*
* ISOTerminal.js
*
* com/isoterminal/worker.js
*
* v86.js
* feat/*.js
* libv86.js
*
*
*
*
* NOTE: For convenience reasons, events are forwarded between com/isoterminal.js, worker.js and ISOTerminal
* Instead of a melting pot of different functionnames, events are flowing through everything (ISOTerminal.emit())
* ```
*/
if( typeof AFRAME != 'undefined '){
@ -71,31 +35,26 @@ if( typeof AFRAME != 'undefined '){
schema: {
iso: { type:"string", "default":"https://forgejo.isvery.ninja/assets/xrsh-buildroot/main/xrsh.iso" },
overlayfs: { type:"string"},
width: { type: 'number',"default": 800 },
height: { type: 'number',"default": 600 },
width: { type: 'number',"default": -1 },
height: { type: 'number',"default": -1 },
depth: { type: 'number',"default": 0.03 },
lineHeight: { type: 'number',"default": 18 },
prompt: { type: 'boolean', "default": true }, // boot straight into ISO or give user choice
padding: { type: 'number',"default": 18 },
maximized: { type: 'boolean',"default":false},
minimized: { type: 'boolean',"default":false},
maximized: { type: 'boolean',"default":true},
muteUntilPrompt:{ type: 'boolean',"default":true}, // mute stdout until a prompt is detected in ISO
HUD: { type: 'boolean',"default":false}, // link to camera movement
transparent: { type:'boolean', "default":false }, // need good gpu
memory: { type: 'number', "default":60 }, // VM memory (in MB) [NOTE: quest or smartphone might crash > 40mb ]
bufferLatency: { type: 'number', "default":1 }, // in ms: bufferlatency from webworker to xterm (batch-update every char to texture)
debug: { type: 'boolean', "default":false },
emulator: { type: 'string', "default": "fbterm" }// terminal emulator
memory: { type: 'number', "default":40 }, // VM memory (in MB) [NOTE: quest or smartphone might crash > 40mb ]
bufferLatency: { type: 'number', "default":1 }, // in ms: bufferlatency from webworker to xterm (batch-update every char to texture)
debug: { type: 'boolean', "default":false }
},
init: function(){
this.el.object3D.visible = false
if( window.innerWidth < this.data.width ){
this.data.maximized = true
}
this.calculateDimension()
this.initHud()
this.setupBox()
this.setupPasteDrop()
fetch(this.data.iso,{method: 'HEAD'})
@ -115,6 +74,7 @@ if( typeof AFRAME != 'undefined '){
window: "com/window.js",
pastedrop: "com/pastedrop.js",
v86: "com/isoterminal/libv86.js",
vt100: "com/isoterminal/VT100.js",
// allow xrsh to selfcontain scene + itself
xhook: "com/lib/xhook.min.js",
selfcontain: "com/selfcontainer.js",
@ -130,18 +90,24 @@ if( typeof AFRAME != 'undefined '){
scale: 0.66,
events: ['click','keydown'],
html: (me) => `<div class="isoterminal">
<input type="file" id="pastedrop" style="position:absolute; left:-9999px;opacity:0"></input>
<div id="term" tabindex="0"></div>
<div id="term" tabindex="0">
<pre></pre>
</div>
</div>`,
css: (me) => `
.isoterminal{
css: (me) => `.isoterminal{
padding: ${me.com.data.padding}px;
width:100%;
height:99%;
resize: both;
overflow: hidden;
height:90%;
position:relative;
}
.isoterminal div{
display:block;
position:relative;
line-height: ${me.com.data.lineHeight}px;
}
#term {
outline: none !important;
}
@font-face {
font-family: 'Cousine';
@ -156,73 +122,6 @@ if( typeof AFRAME != 'undefined '){
src: url(./com/isoterminal/assets/CousineBold.ttf) format('truetype');
}
.isoterminal *{
outline:none;
box-shadow:none;
}
.term {
font-family: 'Cousine';
line-height: ${me.com.data.lineHeight}px;
font-weight: normal;
font-variant-ligatures: none;
color: #f0f0f0;
overflow: hidden;
white-space: nowrap;
}
.term_content a {
color: inherit;
text-decoration: underline;
color:#2AFF;
}
.term_content a span{
text-shadow: 0px 0px 10px #F07A;
}
.term_content a:hover {
color: inherit;
text-decoration: underline;
animation:fade 1000ms infinite;
-webkit-animation:fade 1000ms infinite;
}
.term_cursor {
color: #000000;
background: #70f;
animation:fade 1000ms infinite;
-webkit-animation:fade 1000ms infinite;
}
.term_char_size {
display: inline-block;
visibility: hidden;
position: absolute;
top: 0px;
left: -1000px;
padding: 0px;
}
.term_textarea {
position: absolute;
top: 0px;
left: 0px;
width: 0px;
height: 0px;
padding: 0px;
border: 0px;
margin: 0px;
opacity: 0;
resize: none;
}
.term_scrollbar { background: transparent url(images/bg-scrollbar-track-y.png) no-repeat 0 0; position: relative; background-position: 0 0; float: right; height: 100%; }
.term_track { background: transparent url(images/bg-scrollbar-trackend-y.png) no-repeat 0 100%; height: 100%; width:13px; position: relative; padding: 0 1px; }
.term_thumb { background: transparent url(images/bg-scrollbar-thumb-y.png) no-repeat 50% 100%; height: 20px; width: 25px; cursor: pointer; overflow: hidden; position: absolute; top: 0; left: -5px; }
.term_thumb .term_end { background: transparent url(images/bg-scrollbar-thumb-y.png) no-repeat 50% 0; overflow: hidden; height: 5px; width: 25px; }
.noSelect { user-select: none; -o-user-select: none; -moz-user-select: none; -khtml-user-select: none; -webkit-user-select: none; }
.isoterminal style{ display:none }
blink{
@ -235,6 +134,12 @@ if( typeof AFRAME != 'undefined '){
box-shadow:none;
}
.cursor {
background: #70F !important;
animation:fade 1000ms infinite;
-webkit-animation:fade 1000ms infinite;
}
.XR .cursor {
animation:none;
-webkit-animation:none;
@ -290,10 +195,9 @@ if( typeof AFRAME != 'undefined '){
// * heavily dependent on requestAnimationFrame (conflicts with THREE)
// * typescript-rewrite results in ~300k lib (instead of 96k)
// * v3.12 had slightly better performance but still very heavy
//
await AFRAME.utils.require(this.requires)
let features = { // ISOTerminal plugins
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",
@ -301,13 +205,7 @@ if( typeof AFRAME != 'undefined '){
indexjs: "com/isoterminal/feat/index.js.js",
autorestore: "com/isoterminal/feat/autorestore.js",
pastedropFeat: "com/isoterminal/feat/pastedrop.js",
httpfs: "com/isoterminal/feat/httpfs.js",
}
if( this.data.emulator == 'fbterm' ){
features['fbtermjs'] = "com/isoterminal/term.js"
features['fbterm'] = "com/isoterminal/feat/term.js"
}
await AFRAME.utils.require(features)
})
this.el.setAttribute("selfcontainer","")
@ -329,16 +227,17 @@ if( typeof AFRAME != 'undefined '){
this.term = new ISOTerminal(instance,this.data)
instance.addEventListener('DOMready', () => {
this.term.emit('term_init', {instance, aEntity:this})
this.setupVT100(instance)
setTimeout( () => {
instance.setAttribute("html-as-texture-in-xr", `domid: #term; faceuser: true`)
},100)
//instance.winbox.resize(720,380)
let size = `width: ${this.data.width}; height: ${this.data.height}`
instance.setAttribute("window", `title: xrsh.iso; uid: ${instance.uid}; attach: #overlay; dom: #${instance.dom.id}; ${size}; min: ${this.data.minimized}; max: ${this.data.maximized}; class: no-full, no-max, no-resize`)
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')
// canvas to texture texture
instance.setAttribute("html-as-texture-in-xr", `domid: .winbox#${instance.uid}; faceuser: true`)
// run iso
let opts = {dom:instance.dom}
@ -351,12 +250,6 @@ if( typeof AFRAME != 'undefined '){
instance.setAttribute("dom", "")
instance.setAttribute("pastedrop", "")
// *REMOVE* make a boot-plugin mechanism in feat/term.js
this.term.addEventListener('enable-console', () => {
instance.dom.classList.remove('blink')
})
this.term.addEventListener('ready', (e) => {
instance.dom.classList.remove('blink')
this.term.emit('status',"running")
@ -411,6 +304,46 @@ if( typeof AFRAME != 'undefined '){
console.test.run()
},
setupVT100: function(instance){
const el = this.el.dom.querySelector('#term')
this.term.opts.vt100 = {
cols: this.cols,
rows: this.rows,
el_or_id: el,
max_scroll_lines: this.rows,
nodim: true,
rainbow: [VT100.COLOR_MAGENTA, VT100.COLOR_CYAN ],
xr: AFRAME.scenes[0].renderer.xr
}
this.term.emit('initVT100',this)
this.vt100 = new VT100( this.term.opts.vt100 )
this.vt100.el = el
this.vt100.curs_set( 1, true)
this.vt100.focus()
this.el.addEventListener('focus', () => this.vt100.focus() )
this.vt100.getch( (ch,t) => {
this.term.send( ch )
})
this.el.addEventListener('serial-output-byte', (e) => {
const byte = e.detail
var chr = String.fromCharCode(byte);
this.vt100.addchr(chr)
})
this.el.addEventListener('serial-output-string', (e) => {
this.vt100.write(e.detail)
})
// translate file upload into pasteFile
this.vt100.upload.addEventListener('change', (e) => {
const file = this.vt100.upload.files[0];
const item = {...file, getAsFile: () => file }
this.el.emit('pasteFile', { item, type: file.type });
})
return this
},
setupPasteDrop: function(){
this.el.addEventListener('pasteFile', (e) => {
e.preventDefault() // prevent bubbling up to window (which is triggering this initially)
@ -420,14 +353,25 @@ if( typeof AFRAME != 'undefined '){
return this
},
setupBox: function(){
// setup slightly bigger black backdrop (this.el.getObject3D("mesh"))
const w = this.data.width/950;
const h = this.data.height/950;
this.el.box = document.createElement('a-entity')
this.el.box.setAttribute("geometry",`primitive: box; width:${w}; height:${h}; depth: -${this.data.depth}`)
this.el.box.setAttribute("material","shader:flat; color:black; opacity:0.9; transparent:true; ")
this.el.box.setAttribute("position",`0 0 ${(this.data.depth/2)-0.001}`)
this.el.appendChild(this.el.box)
},
calculateDimension: function(){
if( this.data.width == -1 ) this.data.width = document.body.offsetWidth;
if( this.data.height == -1 ) this.data.height = Math.floor( document.body.offsetHeight - 30 )
if( this.data.height > this.data.width ) this.data.height = this.data.width // mobile smartphone fix
this.data.width -= this.data.padding*2
this.data.height -= this.data.padding*2
this.cols = Math.floor(this.data.width/this.data.lineHeight*2)-1
this.rows = Math.floor( (this.data.height*0.93)/this.data.lineHeight)-1
this.cols = Math.floor(this.data.width/this.data.lineHeight*2)
this.rows = Math.floor(this.data.height*0.53/this.data.lineHeight*1.7)
},
events:{

View File

@ -34,12 +34,11 @@ ISOTerminal.addEventListener = (event,cb) => {
}
ISOTerminal.prototype.exec = function(shellscript){
this.send(`printf "\n\r"; ${shellscript}\n`,1)
this.send(shellscript+"\n",1)
}
ISOTerminal.prototype.hook = function(hookname,args){
let cmd = `{ type hook || source /etc/profile.sh; }; hook ${hookname} "${args.join('" "')}"`
this.exec(cmd)
this.exec(`{ type hook || source /etc/profile.sh; }; hook ${hookname} "${args.join('" "')}"`)
}
ISOTerminal.prototype.serial_input = 0; // can be set to 0,1,2,3 to define stdinput tty (xterm plugin)
@ -54,7 +53,7 @@ ISOTerminal.prototype.send = function(str, ttyNr){
this.convert.toUint8Array( str ).map( (c) => {
this.preventFrameDrop(
() => {
this.worker.postMessage({event:`serial${ttyNr}-input`,data:c})
this.worker.postMessage({event:`serial${ttyNr}-input`,data:c})
}
)
})
@ -124,11 +123,8 @@ ISOTerminal.prototype.start = function(opts){
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",
net_device:{
relay_url:"fetch", // or websocket proxy "wss://relay.widgetry.org/",
type:"virtio"
},
//bzimage_initrd_from_filesystem: true,
//filesystem: {
// baseurl: "com/isoterminal/v86/images/alpine-rootfs-flat",
@ -138,7 +134,6 @@ ISOTerminal.prototype.start = function(opts){
//disable_jit: false,
filesystem: {},
autostart: true,
prompt: this.opts.prompt,
debug: this.opts.debug ? true : false
};
@ -161,8 +156,10 @@ ISOTerminal.prototype.setupWorker = function(opts){
return this
}
ISOTerminal.prototype.getLoaderMsg = function(){
ISOTerminal.prototype.startVM = function(opts){
this.emit('runISO',{...opts, bufferLatency: this.opts.bufferLatency })
const loading = [
'loading quantum bits and bytes',
'preparing quantum flux capacitors',
@ -189,14 +186,15 @@ ISOTerminal.prototype.getLoaderMsg = function(){
const empower = [
"FOSS gives users control over their software, offering freedom to modify and share",
"Feeling powerless? FOSS escapes a mindset known as learned helplessness",
"Feeling powerless with tech? FOSS escapes a mindset known as learned helplessness",
"FOSS breaks this cycle by showing that anyone can learn and contribute",
"Proprietary software can make users dependent, but FOSS offers real choices",
"FOSS communities provide support and encourage users to develop new skills",
"Learned helplessness fades when we realize tech isnt too complex to understand",
"FOSS empowers users to customize and improve their tools",
"Engaging with FOSS helps build confidence and self-reliance in tech",
"FOSS shows that anyone can shape the digital world with curiosity and effort",
"Linux can revive old computers, extending their life and reduces e-waste",
"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",
@ -208,39 +206,32 @@ ISOTerminal.prototype.getLoaderMsg = function(){
"Switching to Linux promotes sustainability by reducing demand for new gadgets and lowering e-waste"
]
let motd = `
const motd = `
\r . . ____ _____________ ________. ._. ._. . .
\r . . .\\ \\/ /\\______ \\/ _____// | \\. .
\r . . . \\ / | _/\\_____ \\/ ~ \\ .
\r . . . / \\ | | \\/ \\ Y / .
\r . . ./___/\\ \\ |____|_ /_______ /\\___|_ /. .
\r . . . . . .\\_/. . . . \\/ . . . .\\/ . . _ \\/ . .
\r https://xrsh.isvery.ninja ▬▬▬▬▬▬▬▬▬▬▬▬
\r https://xrsh.isvery.ninja ▬▬▬▬▬▬▬▬▬▬▬▬
\r local-first, polyglot, unixy WebXR IDE & runtime
\r
\r credits
\r -------
\r @nlnet@nlnet.nl
\r @lvk@mastodon.online
\r @utopiah@mastodon.pirateparty.be 
\r https://www.w3.org/TR/webxr
\r https://xrfragment.org
\r https://threejs.org
\r https://aframe.org
\r https://busybox.net
\r https://buildroot.org
\r`
\r
\r credits
\r -------
\r @nlnet@nlnet.nl
\r @lvk@mastodon.online
\r @utopiah@mastodon.pirateparty.be
\r https://www.w3.org/TR/webxr
\r https://three.org
\r https://aframe.org
`
const text_color = "\r"
const text_reset = "\033[0m"
const loadmsg = "\n\r"+loading[ Math.floor(Math.random()*1000) % loading.length ] + "..please wait \n\n\r"
const empowermsg = "\n\r"+text_reset+'"'+empower[ Math.floor(Math.random()*1000) % empower.length ] + '"\n\r'
return { motd, text_color, text_reset, loadmsg, empowermsg}
}
ISOTerminal.prototype.startVM = function(opts){
this.v86opts = opts
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) => {
@ -261,34 +252,12 @@ ISOTerminal.prototype.startVM = function(opts){
const str = e.detail
// lets scan for a prompt so we can send a 'ready' event to the world
if( !this.ready && str.match(/\n(\/ #|~ #|~%|\[.*\]>)/) ) this.postBoot()
if( !this.ready && str.match(/\n(\/ #|~%|\[.*\]>)/) ) this.postBoot()
if( this.ready || !this.opts.muteUntilPrompt ) this.emit('serial-output-string', e.detail )
})
});
let msglib = this.getLoaderMsg()
let msg = msglib.motd
if( this.opts.prompt ){
msg += `\r
\r 1) boot ${String(this.opts.iso || "").replace(/.*\//,'')} Linux
\r 2) just give me a javascript-console in WebXR [=instant]
\r
\renter number> `
}else{
bootISO()
}
this.emit('status',msglib.loadmsg)
this.emit('serial-output-string', msg)
}
ISOTerminal.prototype.bootISO = function(){
let msglib = this.getLoaderMsg()
let msg = "\n\r" + msglib.empowermsg + msglib.text_color + msglib.loadmsg + msglib.text_reset
this.emit('serial-output-string', msg)
this.emit('runISO',{...this.v86opts, bufferLatency: this.opts.bufferLatency })
}
@ -316,27 +285,6 @@ ISOTerminal.prototype.bufferOutput = function(byte,cb,latency){
}
}
//ISOTerminal.prototype.bufferOutput = function(byte, cb, latency, buffer) {
// const str = String.fromCharCode(byte);
// //if (str === '\r' || str === '\n' || str.charCodeAt(0) < 32 || str.charCodeAt(0) === 127) {
// // cb(str);
// //} else if (str === '\x1b') { // ESC
// // buffer.esc = true;
// //} else if (buffer.esc) {
// // cb('\x1b' + str);
// // buffer.esc = false;
// //} else {
// buffer.str = (buffer.str || '') + str;
// if (Date.now() - (buffer.timestamp || 0) >= latency) {
// console.log(buffer.str)
// cb(buffer.str);
// buffer.str = '';
// buffer.timestamp = Date.now();
// }
// //}
//}
ISOTerminal.prototype.preventFrameDrop = function(cb){
// don't let workers cause framerate dropping
const xr = this.instance.sceneEl.renderer.xr

1421
com/isoterminal/VT100.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -49,47 +49,3 @@ emulator.fs9p.append_file = async function(file,data){
}
emulator.fs9p.read_file_world = async function(file){
const p = this.SearchPath(file);
if(p.id === -1)
{
return Promise.resolve(null);
}
const inode = this.GetInode(p.id);
const perms = this.parseFilePermissions(inode.mode)
if( !perms.world.read ){
return Promise.resolve(null);
}
return this.Read(p.id, 0, inode.size);
}
emulator.fs9p.parseFilePermissions = function(permissionInt) {
// Convert the permission integer to octal
const octalPermissions = permissionInt.toString(8);
// Extract the permission bits (last 3 digits in octal)
const permissionBits = octalPermissions.slice(-3);
function parsePermission(digit) {
const num = parseInt(digit, 10);
return {
read: Boolean(num & 4), // 4 = read
write: Boolean(num & 2), // 2 = write
execute: Boolean(num & 1) // 1 = execute
};
}
// Decode the permissions
const permissions = {
owner: parsePermission(permissionBits[0]),
group: parsePermission(permissionBits[1]),
world: parsePermission(permissionBits[2]),
};
return permissions;
}

View File

@ -14,7 +14,7 @@ ISOTerminal.prototype.boot = async function(e){
env.push( 'export '+String(i).toUpperCase()+'="'+decodeURIComponent( document.location[i]+'"') )
}
}
await this.worker.create_file("profile.browser", this.convert.toUint8Array( env.join('\n') ) )
this.worker.create_file("profile.browser", this.convert.toUint8Array( env.join('\n') ) )
if( this.serial_input == 0 ){
if( !this.noboot ){

View File

@ -1,56 +0,0 @@
if( typeof emulator != 'undefined' ){
}else{
ISOTerminal.addEventListener('ready', function(e){
function getMimeType(file) {
const mimeTypes = {
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
png: 'image/png',
mp4: 'video/mp4',
gif: 'image/gif',
};
const extension = file.split('.').pop().toLowerCase();
return mimeTypes[extension] || 'application/octet-stream'; // Fallback
}
// listen for http request to the filesystem ( file://host/path )
xhook.before( (request,callback) => {
console.log(request.url)
if (request.url.match(/^\/mnt\/.*/) ){
let response
let file = request.url.replace(/^\/mnt\//,'')
let mimetype = getMimeType(file)
this.worker.read_file_world(file)
.then( (data) => {
if( data == null ) throw `/mnt/${file} does not exist in ISO filesystem`"
let blob = new Blob( [data], {type: getMimeType(file) }) // wrap Uint8Array into array
response = {
headers: new Headers({ 'Content-Type': getMimeType(file) }),
data,
url: file,
status: 200,
blob: () => new Promise( (resolve,reject) => resolve(blob) ),
arrayBuffer: blob.arrayBuffer
}
console.log("serving from iso filesystem: "+file)
console.log("*TODO* large files being served partially")
callback(response)
})
.catch( (e) => {
console.error(e)
response = new Response()
response.status = 404
callback(response)
})
return
}else{
callback()
}
})
})
}

View File

@ -22,25 +22,17 @@ if( typeof emulator != 'undefined' ){
ISOTerminal.addEventListener('javascript-eval', async function(e){
const {script,PID} = e.detail
let res;
let res
try{
let f = new Function(`${script}`);
res = f();
res = (new Function(`${script}`))()
if( res && typeof res != 'string' ) res = JSON.stringify(res,null,2)
}catch(err){
console.error(err)
console.dir(err)
res = "error: "+err.toString()
// try to figure out line *FIXME*
let line = err.stack.split("\n").find(e => e.includes("<anonymous>:") || e.includes("Function:"));
if( line ){
let lineIndex = (line.includes("<anonymous>:") && line.indexOf("<anonymous>:") + "<anonymous>:".length) || (line.includes("Function:") && line.indexOf("Function:") + "Function:".length);
let lnr = +line.substring(lineIndex, lineIndex + 1) - 2
res += script.split("\n")[lnr-1]
}else console.dir(script)
console.error(res)
}catch(e){
console.error(e)
console.info(script)
res = "error: "+e.toString()
if( e.filename ){
res += "\n"+e.filename+":"+e.lineno+":"+e.colno
}
}
// update output to 9p with PID as filename (in /mnt/run)
if( PID ){

View File

@ -3,46 +3,38 @@ ISOTerminal.prototype.redirectConsole = function(handler){
const dir = console.dir;
const err = console.error;
const warn = console.warn;
const addLineFeeds = (str) => typeof str == 'string' ? str.replace(/\n/g,"\n\r") : str
console.log = (...args)=>{
const textArg = args[0];
handler( addLineFeeds(textArg) );
handler(textArg+'\n');
log.apply(log, args);
};
console.error = (...args)=>{
const textArg = args[0]
handler( addLineFeeds(textArg), '\x1b[31merror\x1b[0m');
handler( textArg+'\n', '\x1b[31merror\x1b[0m');
err.apply(log, args);
};
console.dir = (...args)=>{
const textArg = args[0]
let str = JSON.stringify(textArg,null,2)+'\n'
handler( addLineFeeds(str) )
handler( JSON.stringify(textArg,null,2)+'\n');
dir.apply(log, args);
};
console.warn = (...args)=>{
const textArg = args[0]
handler( addLineFeeds(textArg),'\x1b[38;5;208mwarn\x1b[0m');
handler(textArg+'\n','\x1b[38;5;208mwarn\x1b[0m');
err.apply(log, args);
};
}
ISOTerminal.prototype.enableConsole = function(opts){
opts = opts || {stdout:false}
ISOTerminal.addEventListener('emulator-started', function(){
this.redirectConsole( (str,prefix) => {
let _str = typeof str == 'string' ? str : JSON.stringify(str)
let finalStr = "";
prefix = prefix ? prefix+' ' : ''
_str.trim().split("\n").map( (line) => {
finalStr += `${opts.stdout ? '' : "\x1b[38;5;165m/dev/browser: \x1b[0m"}`+prefix+line+'\n'
let finalStr = ""
prefix = prefix ? prefix+' ' : ' '
str.trim().split("\n").map( (line) => {
finalStr += '\x1b[38;5;165m/dev/browser: \x1b[0m'+prefix+line+'\n'
})
if( opts.stdout ){
this.emit('serial-output-string', finalStr)
}else this.emit('append_file', ["/dev/browser/console",finalStr])
this.emit('append_file', ["/dev/browser/console",finalStr])
})
window.addEventListener('error', function(event) {
@ -57,23 +49,4 @@ ISOTerminal.prototype.enableConsole = function(opts){
console.error(event);
});
if( opts.stdout ){
this.emit('serial-output-string', "\n\n\r☑ initialized javascript console\n");
this.emit('serial-output-string', "\r☑ please use these functions to print:\n");
this.emit('serial-output-string', "\r└☑ console.log(\"foo\")\n");
this.emit('serial-output-string', "\r└☑ console.warn(\"foo\")\n");
this.emit('serial-output-string', "\r└☑ console.dir({foo:12})\n");
this.emit('serial-output-string', "\r└☑ console.error(\"foo\")\n");
this.emit('serial-output-string', "\r\n");
}
}
ISOTerminal.addEventListener('emulator-started', function(){
this.enableConsole()
})
ISOTerminal.addEventListener('init', function(){
this.addEventListener('enable-console', function(opts){
this.enableConsole(opts.detail)
})
})

View File

@ -17,8 +17,7 @@ if( typeof emulator != 'undefined' ){
ISOTerminal.prototype.pasteFile = async function(data){
const {type,item,pastedText} = data
if( pastedText){
// the terminal handles this (pastes text)
// this.pasteWriteFile( this.convert.toUint8Array(pastedText) ,type, null, true)
this.pasteWriteFile( this.convert.toUint8Array(pastedText) ,type)
}else{
const file = item.getAsFile();
const reader = new FileReader();
@ -30,19 +29,4 @@ if( typeof emulator != 'undefined' ){
}
}
ISOTerminal.prototype.pasteInit = function(opts){
// bind upload input
const {instance, aEntity} = opts
const el = aEntity.el.dom.querySelector('#pastedrop') // upload input
el.addEventListener('change', (e) => {
const file = el.files[0];
const item = {...file, getAsFile: () => file } // pasteFile-event works with File objets
const data = { item, type: file.type }
this.emit( 'pasteFile', data, "worker" ) // impersonate as worker (as worker cannot handle File objet)
})
}
ISOTerminal.addEventListener('init', function(){
this.addEventListener('term_init', (opts) => this.pasteInit(opts.detail) )
})
}

View File

@ -1,141 +0,0 @@
ISOTerminal.addEventListener('init', function(){
this.TermInit()
})
ISOTerminal.prototype.TermInit = function(){
const setupTerm = (opts) => {
if( !opts ) return
const {instance, aEntity} = opts
const el = aEntity.el.dom.querySelector('#term')
opts.termOpts = {
cols: aEntity.cols,
rows: aEntity.rows,
el_or_id: el,
scrollback: aEntity.rows*3,
fontSize: null //
//rainbow: [Term.COLOR_MAGENTA, Term.COLOR_CYAN ],
//xr: AFRAME.scenes[0].renderer.xr,
//map: {
// 'ArrowRight': { ch: false, ctrl: '\x1b\x66' }, // this triggers ash-shell forward-word
// 'ArrowLeft': { ch: false, ctrl: '\x1b\x62' } // backward-word
//}
}
// patch Term-class
Term.prototype.move_textarea = function(){} /* *TODO* *FIXME* does not work in winbox */
Term.prototype.pasteHandler = function(original){
return function (ev){
original.apply(this,[ev])
}
}( Term.prototype.pasteHandler )
Term.prototype.keyDownHandler = function(original){
return function (e){
if ((e.ctrlKey || e.metaKey) && e.key === 'v') {
return true; // bubble up to pasteHandler (see pastedrop.js)
}
original.apply(this,[e])
}
}( Term.prototype.keyDownHandler )
Term.prototype.href = (a) => {
if( a.href ){
this.exec(`source /etc/profile.sh; hook href "${a.href}"`)
}
return false
}
this.term = new Term( opts.termOpts )
this.term.colors = [
/* normal */
"#000000",
"#2FA",
"#7700ff",
"#555555",
"#0000ff",
"#aa00aa",
"#ff00aa",
"#aaaaaa",
/* bright */
"#555555",
"#ff5555",
"#2CF",
"#aa00ff",
"#5555ff",
"#ff55ff",
"#55ffff",
"#ffffff"
];
this.term.open(el)
this.term.el = el
this.term.prompt = "\r> "
// you can override this REPL in index.html via :
//
// <script>
// document.querySelector('[isoterminal]')
// .components
// .isoterminal
// .term
// .term
// .setKeyHandler( (ch) => { ....} )
// </script>
//
// this might change in the future into something
// more convenient
this.term.setKeyHandler( (ch) => {
if( this.ready ){
this.send(ch) // send to v86 webworker
}else{
if( (ch == "\n" || ch == "\r") && typeof this.console == 'undefined' ){
switch( this.lastChar ){
case '1': this.bootISO(); break;
case '2': {
this.emit('enable-console',{stdout:true})
this.emit('status',"javascript console")
this.console = ""
setTimeout( () => this.term.write( this.term.prompt), 100 )
break;
}
}
}else if( this.console != undefined ){
this.term.write(ch)
const reset = () => {
this.console = ""
setTimeout( () => "\n\r"+this.term.write( this.term.prompt),100)
}
if( (ch == "\n" || ch == "\r") ){
try{
this.term.write("\n\r")
if( this.console ) eval(this.console)
reset()
}catch(e){
reset()
throw e // re throw
}
}else{
this.console += ch
}
}else{
this.term.write(ch)
}
this.lastChar = ch
}
})
aEntity.el.addEventListener('focus', () => el.querySelector("textarea").focus() )
aEntity.el.addEventListener('serial-output-string', (e) => {
this.term.write(e.detail)
})
//aEntity.term.emit('initTerm',this)
//aEntity.el.addEventListener('focus', () => this.vt100.focus() )
//aEntity.el.addEventListener('serial-output-string', (e) => {
// this.vt100.write(e.detail)
//})
}
this.addEventListener('term_init', (opts) => setupTerm(opts.detail) )
}

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

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -12,29 +12,26 @@ this.runISO = function(opts){
console.log("[worker.js] started emulator")
// event forwarding
emulator.buf0 = {}
emulator.buf1 = {}
emulator.buf2 = {}
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, emulator.buf0 )
}, 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, emulator.buf1 )
}, 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, emulator.buf2 )
}, opts.bufferLatency )
}.bind(this));
emulator.add_listener("emulator-started", function(){
@ -45,17 +42,10 @@ this.runISO = function(opts){
/*
* forward events/functions so non-worker world can reach them
*/
// stripping '/mnt' is needed (the 9p mounted fs does not know about this)
const stripMountDir = (arr) => {
arr[0] = String(arr[0]).replace(/^\/mnt/,'')
return arr
}
this.create_file = async function(){ return emulator.create_file.apply(emulator, stripMountDir(arguments[0]) ) }
this.read_file = async function(){ return emulator.read_file.apply(emulator, stripMountDir(arguments[0]) ) }
this.read_file_world = async function(){ return emulator.fs9p.read_file_world.apply(emulator.fs9p, stripMountDir(arguments[0]) ) }
this.append_file = async function(){ emulator.fs9p.append_file.apply(emulator.fs9p, stripMountDir(arguments[0])) }
this.update_file = async function(){ emulator.fs9p.update_file.apply(emulator.fs9p, stripMountDir(arguments[0])) }
this.create_file = async function(){ return emulator.create_file.apply(emulator, arguments[0]) }
this.read_file = async function(){ return emulator.read_file.apply(emulator, arguments[0]) }
this.append_file = async function(){ emulator.fs9p.append_file.apply(emulator.fs9p, arguments[0]) }
this.update_file = async function(){ emulator.fs9p.update_file.apply(emulator.fs9p, arguments[0]) }
// filename will be read from 9pfs: "/mnt/"+filename
emulator.readFromPipe = function(filename,cb){

View File

@ -1,18 +1,3 @@
/**
* ## [pastedrop](com/pastedrop.js)
*
* detects user copy/paste and file dragdrop action
* and clipboard functions
*
* ```html
* <a-entity pastedrop/>
* ```
*
* | event | target | info |
* |--------------|--------|------------------------------------------|
* | `pasteFile` | self | always translates input to a File object |
*/
AFRAME.registerComponent('pastedrop', {
schema: {
foo: { type:"string"}
@ -37,16 +22,16 @@ AFRAME.registerComponent('pastedrop', {
})
},
getClipboard: function(){
navigator.clipboard.readText()
.then( async (base64) => {
let mimetype = base64.replace(/;base64,.*/,'')
let data = base64.replace(/.*;base64,/,'')
let type = this.textHeuristic(data)
const term = document.querySelector('[isoterminal]').components.isoterminal.term
this.el.emit('pasteFile',{}) /*TODO* data incompatible */
})
},
//getClipboard: function(){
// navigator.clipboard.readText()
// .then( async (base64) => {
// let mimetype = base64.replace(/;base64,.*/,'')
// let data = base64.replace(/.*;base64,/,'')
// let type = this.textHeuristic(data)
// const term = document.querySelector('[isoterminal]').components.isoterminal.term
// this.el.emit('pasteFile',{}) /*TODO* data incompatible */
// })
//},
onDrop: function(e){
e.preventDefault()

View File

@ -1,17 +1,12 @@
/**
* ## [require](com/require('').js)
*
* automatically requires dependencies
*
* ```javascript
* await AFRAME.utils.require( this.dependencies ) (*) autoload missing components
* await AFRAME.utils.require( this.el.getAttributeNames() ) (*) autoload missing components
* await AFRAME.utils.require({foo: "https://foo.com/aframe/components/foo.js"},this)
* await AFRAME.utils.require(["./app/foo.js","foo.css"],this)
* ```
*
* > (*) = prefixes baseURL AFRAME.utils.require.baseURL ('./com/' e.g.)
*/
// usage:
//
// await AFRAME.utils.require( this.dependencies ) (*) autoload missing components
// await AFRAME.utils.require( this.el.getAttributeNames() ) (*) autoload missing components
// await AFRAME.utils.require({foo: "https://foo.com/aframe/components/foo.js"},this)
// await AFRAME.utils.require(["./app/foo.js","foo.css"],this)
//
// (*) = prefixes baseURL AFRAME.utils.require.baseURL ('./com/' e.g.)
//
AFRAME.utils.require = function(arr_or_obj,opts){
opts = opts || {}
let i = 0

View File

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

View File

@ -1,29 +1,3 @@
/**
* ## [window](com/window.js)
*
* wraps a draggable window around a dom id or [dom](com/dom.js) component.
*
* ```html
* <a-entity window="dom: #mydiv"/>
* ```
*
* > depends on [AFRAME.utils.require](com/require.js)
*
* | property | type | default | info |
* |------------------|-----------|------------------------|------|
* | `title` |`string` | "" | |
* | `width` |`string` | | |
* | `height` |`string` | 260px | |
* | `uid` |`string` | | |
* | `attach` |`selector` | | |
* | `dom` |`selector` | | |
* | `max` |`boolean` | false | |
* | `min` |`boolean` | false | |
* | `x` |`string` | "center" | |
* | `y` |`string` | "center" | |
* | `class` |`array` | [] | |
*/
AFRAME.registerComponent('window', {
schema:{
title: {type:'string',"default":"title"},
@ -35,8 +9,7 @@ AFRAME.registerComponent('window', {
max: {type:'boolean',"default":false},
min: {type:'boolean',"default":false},
x: {type:'string',"default":"center"},
y: {type:'string',"default":"center"},
"class": {type:'array',"default":[]},
y: {type:'string',"default":"center"}
},
dependencies:{
@ -53,23 +26,8 @@ AFRAME.registerComponent('window', {
await AFRAME.utils.require(this.dependencies)
if( !this.el.dom ) return console.error('window element requires dom-component as dependency')
const close = () => {
let e = {halt:false}
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()
return false
}
this.el.addEventListener('close', () => {
close()
this.el.winbox.close()
})
this.el.dom.style.display = 'none'
let winbox = this.el.winbox = new WinBox( this.data.title, {
class: this.data.class,
height:this.data.height,
width:this.data.width,
x: this.data.x,
@ -91,8 +49,15 @@ AFRAME.registerComponent('window', {
this.el.components['obb-collider'].update()
},1000)
},
onclose: close
onclose: () => {
let e = {halt:false}
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()
return false
},
});
this.data.dom.style.display = '' // show

268
com/xterm.js Normal file
View File

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