Compare commits
No commits in common. "main" and "feat/pty" have entirely different histories.
2
.env
2
.env
|
@ -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
|
|
@ -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
|
41
README.awk
41
README.awk
|
@ -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
164
README.md
|
@ -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.)
|
||||
|
|
|
@ -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.emulator.read_file( 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.
|
||||
`
|
||||
}
|
||||
|
||||
});
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* ## [data_events](com/data_events.js)
|
||||
/*
|
||||
* ## data_events
|
||||
*
|
||||
* allows components to react to data changes
|
||||
*
|
||||
|
|
12
com/dom.js
12
com/dom.js
|
@ -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) => {
|
||||
|
@ -55,6 +59,7 @@ if( !AFRAME.components.dom ){
|
|||
.assignUniqueID()
|
||||
.scaleDOMvsXR()
|
||||
.triggerKeyboardForInputs()
|
||||
.stubRequestAnimationFrame()
|
||||
|
||||
document.querySelector('#overlay').appendChild(this.el.dom)
|
||||
this.el.emit('DOMready',{el: this.el.dom})
|
||||
|
@ -126,5 +131,10 @@ if( !AFRAME.components.dom ){
|
|||
return this
|
||||
},
|
||||
|
||||
stubRequestAnimationFrame: async function(){
|
||||
let s = await AFRAME.utils.require(this.requires)
|
||||
this.el.setAttribute("requestAnimationFrameXR","")
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,41 +1,8 @@
|
|||
/**
|
||||
* ## [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', {
|
||||
schema: {
|
||||
domid: { type:"string"},
|
||||
faceuser: { type: "boolean", default: false}
|
||||
domid: { type:"string"}
|
||||
},
|
||||
|
||||
dependencies:{
|
||||
|
@ -52,9 +19,7 @@ if( !AFRAME.components['html-as-texture-in-xr'] ){
|
|||
let s = await AFRAME.utils.require(this.dependencies)
|
||||
this.el.setAttribute("html",`html: ${this.data.domid}; cursor:#cursor; xrlayer: true`)
|
||||
this.el.setAttribute("visible", AFRAME.utils.XD() == '3D' ? 'true' : 'false' )
|
||||
if( this.data.faceuser ){
|
||||
this.el.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.4) )
|
||||
}
|
||||
this.el.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.5) )
|
||||
},
|
||||
|
||||
manifest: { // HTML5 manifest to identify app to xrsh
|
||||
|
|
|
@ -1,103 +1,51 @@
|
|||
/**
|
||||
* ## [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())
|
||||
* ```
|
||||
*/
|
||||
function ISOTerminal(instance,opts){
|
||||
// create a neutral isoterminal object which can be decorated
|
||||
// with prototype functions and has addListener() and dispatchEvent()
|
||||
let obj = new EventTarget()
|
||||
obj.instance = instance
|
||||
obj.opts = opts
|
||||
// register default event listeners (enable file based features like isoterminal/jsconsole.js e.g.)
|
||||
for( let event in ISOTerminal.listener )
|
||||
for( let cb in ISOTerminal.listener[event] )
|
||||
obj.addEventListener( event, ISOTerminal.listener[event][cb] )
|
||||
// compose object with functions
|
||||
for( let i in ISOTerminal.prototype ) obj[i] = ISOTerminal.prototype[i]
|
||||
obj.emit('init')
|
||||
return obj
|
||||
}
|
||||
|
||||
ISOTerminal.prototype.emit = function(event,data){
|
||||
data = data || false
|
||||
this.dispatchEvent( new CustomEvent(event, {detail: data} ) )
|
||||
}
|
||||
|
||||
ISOTerminal.addEventListener = (event,cb) => {
|
||||
ISOTerminal.listener = ISOTerminal.listener || {}
|
||||
ISOTerminal.listener[event] = ISOTerminal.listener[event] || []
|
||||
ISOTerminal.listener[event].push(cb)
|
||||
}
|
||||
|
||||
// ISOTerminal has defacto support for AFRAME
|
||||
// but can be decorated to work without it as well
|
||||
|
||||
if( typeof AFRAME != 'undefined '){
|
||||
|
||||
AFRAME.registerComponent('isoterminal', {
|
||||
schema: {
|
||||
iso: { type:"string", "default":"https://forgejo.isvery.ninja/assets/xrsh-buildroot/main/xrsh.iso" },
|
||||
overlayfs: { type:"string"},
|
||||
width: { type: 'number',"default": 800 },
|
||||
height: { type: 'number',"default": 600 },
|
||||
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},
|
||||
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
|
||||
iso: { type:"string", "default":"https://forgejo.isvery.ninja/assets/xrsh-buildroot/main/xrsh.iso" },
|
||||
overlayfs: { type:"string"},
|
||||
cols: { type: 'number',"default": 120 },
|
||||
rows: { type: 'number',"default": 30 },
|
||||
padding: { type: 'number',"default": 18 },
|
||||
minimized: { type: 'boolean',"default":false},
|
||||
maximized: { type: 'boolean',"default":true},
|
||||
transparent: { type:'boolean', "default":false }, // need good gpu
|
||||
xterm: { type: 'boolean', "default":true }, // use xterm.js? (=slower)
|
||||
memory: { type: 'number', "default":48 } // VM memory (in MB)
|
||||
},
|
||||
|
||||
init: function(){
|
||||
init: async function(){
|
||||
this.el.object3D.visible = false
|
||||
if( window.innerWidth < this.data.width ){
|
||||
this.data.maximized = true
|
||||
}
|
||||
|
||||
this.calculateDimension()
|
||||
this.initHud()
|
||||
this.setupPasteDrop()
|
||||
|
||||
fetch(this.data.iso,{method: 'HEAD'})
|
||||
.then( (res) => {
|
||||
if( res.status != 200 ) throw 'not found'
|
||||
|
@ -111,153 +59,74 @@ if( typeof AFRAME != 'undefined '){
|
|||
},
|
||||
|
||||
requires:{
|
||||
com: "com/dom.js",
|
||||
window: "com/window.js",
|
||||
pastedrop: "com/pastedrop.js",
|
||||
v86: "com/isoterminal/libv86.js",
|
||||
com: "com/dom.js",
|
||||
window: "com/window.js",
|
||||
v86: "com/isoterminal/libv86.js",
|
||||
// allow xrsh to selfcontain scene + itself
|
||||
xhook: "com/lib/xhook.min.js",
|
||||
selfcontain: "com/selfcontainer.js",
|
||||
xhook: "https://jpillora.com/xhook/dist/xhook.min.js",
|
||||
selfcontain: "com/selfcontainer.js",
|
||||
// html to texture
|
||||
htmlinxr: "com/html-as-texture-in-xr.js",
|
||||
// isoterminal global features
|
||||
PromiseWorker: "com/isoterminal/PromiseWorker.js",
|
||||
ISOTerminal: "com/isoterminal/ISOTerminal.js",
|
||||
localforage: "com/isoterminal/localforage.js",
|
||||
htmlinxr: "com/html-as-texture-in-xr.js",
|
||||
// isoterminal features
|
||||
core: "com/isoterminal/core.js",
|
||||
utils_9p: "com/isoterminal/feat/9pfs_utils.js",
|
||||
boot: "com/isoterminal/feat/boot.js",
|
||||
jsconsole: "com/isoterminal/feat/jsconsole.js",
|
||||
javascript: "com/isoterminal/feat/javascript.js",
|
||||
indexhtml: "com/isoterminal/feat/index.html.js",
|
||||
indexjs: "com/isoterminal/feat/index.js.js",
|
||||
autorestore: "com/isoterminal/feat/autorestore.js",
|
||||
localforage: "https://cdn.rawgit.com/mozilla/localForage/master/dist/localforage.js"
|
||||
},
|
||||
|
||||
dom: {
|
||||
scale: 0.66,
|
||||
scale: 0.5,
|
||||
events: ['click','keydown'],
|
||||
html: (me) => `<div class="isoterminal">
|
||||
<input type="file" id="pastedrop" style="position:absolute; left:-9999px;opacity:0"></input>
|
||||
<div id="term" tabindex="0"></div>
|
||||
<div id="screen" style="white-space: pre; font: 14px monospace; "></div>
|
||||
<canvas style="display: none"></canvas>
|
||||
<div id="serial"></div>
|
||||
</div>`,
|
||||
|
||||
css: (me) => `
|
||||
|
||||
.isoterminal{
|
||||
css: (me) => `.isoterminal{
|
||||
padding: ${me.com.data.padding}px;
|
||||
width:100%;
|
||||
height:99%;
|
||||
resize: both;
|
||||
overflow: hidden;
|
||||
height:100%;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Cousine';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(./com/isoterminal/assets/Cousine.ttf) format('truetype');
|
||||
src: url(./assets/Cousine.ttf) format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Cousine';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url(./com/isoterminal/assets/CousineBold.ttf) format('truetype');
|
||||
}
|
||||
|
||||
.isoterminal *{
|
||||
outline:none;
|
||||
box-shadow:none;
|
||||
white-space: pre;
|
||||
line-height:16px;
|
||||
display:inline;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.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;
|
||||
.isoterminal *,
|
||||
.xterm-dom-renderer-owner-1 .xterm-rows {
|
||||
font-size: 14px;
|
||||
font-family: "Cousine",Liberation Mono,DejaVu Sans Mono,Courier New,monospace;
|
||||
font-weight:500 !important;
|
||||
letter-spacing: 0 !important;
|
||||
text-shadow: 0px 0px 10px #F075;
|
||||
}
|
||||
|
||||
.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{
|
||||
border:none;
|
||||
padding:none;
|
||||
}
|
||||
|
||||
#overlay .winbox:has(> .isoterminal){
|
||||
background:transparent;
|
||||
box-shadow:none;
|
||||
}
|
||||
|
||||
.XR .cursor {
|
||||
animation:none;
|
||||
-webkit-animation:none;
|
||||
}
|
||||
|
||||
.wb-body:has(> .isoterminal){
|
||||
background: #000C;
|
||||
background: #000C;
|
||||
overflow:hidden;
|
||||
}
|
||||
|
||||
.XR .wb-body:has(> .isoterminal){
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.XR .isoterminal{
|
||||
.XR .wb-body:has(> .isoterminal){
|
||||
background: #000;
|
||||
}
|
||||
.isoterminal *{
|
||||
font-size: 14px;
|
||||
font-family: "Cousine",Liberation Mono,DejaVu Sans Mono,Courier New,monospace;
|
||||
font-weight:500 !important;
|
||||
text-shadow: 0px 0px 10px #F075;
|
||||
}
|
||||
|
||||
.isoterminal div{ display:block; }
|
||||
.isoterminal span{ display: inline }
|
||||
|
||||
@keyframes fade {
|
||||
from { opacity: 1.0; }
|
||||
|
@ -271,43 +140,22 @@ if( typeof AFRAME != 'undefined '){
|
|||
to { opacity: 1.0; }
|
||||
}
|
||||
|
||||
.blink{
|
||||
.blink {
|
||||
animation:fade 1000ms infinite;
|
||||
-webkit-animation:fade 1000ms infinite;
|
||||
}
|
||||
|
||||
`
|
||||
},
|
||||
|
||||
initTerminal: async function(singleton){
|
||||
|
||||
// why not latest xterm or v3.12 with builtin-canvas support?
|
||||
// first versions used 1.5.4, a typescript rewrite which:
|
||||
// * acts weird with oculus browser keyboard (does not repaint properly after typing)
|
||||
// * does not use canvas anymore [which would be ideal for THREE.js texture]
|
||||
// * does not allow switching between dom/canvas
|
||||
// * only allows a standalone WebGL addon (conflicts with THREE)
|
||||
// * heavily dependent on requestAnimationFrame (conflicts with THREE)
|
||||
// * typescript-rewrite results in ~300k lib (instead of 96k)
|
||||
// * v3.12 had slightly better performance but still very heavy
|
||||
//
|
||||
await AFRAME.utils.require(this.requires)
|
||||
if( this.data.xterm ){
|
||||
this.requires.xtermjs = "https://unpkg.com/@xterm/xterm@5.5.0/lib/xterm.js"
|
||||
this.requires.xtermcss = "https://unpkg.com/@xterm/xterm@5.5.0/css/xterm.css"
|
||||
this.requires.xterm = "com/isoterminal/feat/xterm.js"
|
||||
}
|
||||
|
||||
let features = { // ISOTerminal plugins
|
||||
boot: "com/isoterminal/feat/boot.js",
|
||||
javascript: "com/isoterminal/feat/javascript.js",
|
||||
jsconsole: "com/isoterminal/feat/jsconsole.js",
|
||||
indexhtml: "com/isoterminal/feat/index.html.js",
|
||||
indexjs: "com/isoterminal/feat/index.js.js",
|
||||
autorestore: "com/isoterminal/feat/autorestore.js",
|
||||
pastedropFeat: "com/isoterminal/feat/pastedrop.js",
|
||||
httpfs: "com/isoterminal/feat/httpfs.js",
|
||||
}
|
||||
if( this.data.emulator == 'fbterm' ){
|
||||
features['fbtermjs'] = "com/isoterminal/term.js"
|
||||
features['fbterm'] = "com/isoterminal/feat/term.js"
|
||||
}
|
||||
await AFRAME.utils.require(features)
|
||||
let s = await AFRAME.utils.require(this.requires)
|
||||
|
||||
this.el.setAttribute("selfcontainer","")
|
||||
|
||||
|
@ -326,44 +174,44 @@ if( typeof AFRAME != 'undefined '){
|
|||
}
|
||||
|
||||
// init isoterminal
|
||||
this.term = new ISOTerminal(instance,this.data)
|
||||
this.isoterminal = new ISOTerminal(instance,this.data)
|
||||
|
||||
instance.addEventListener('DOMready', () => {
|
||||
this.term.emit('term_init', {instance, aEntity:this})
|
||||
//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`)
|
||||
let size = this.data.xterm ? 'width: 1024px; height:600px'
|
||||
: 'width: 720px; height:455px'
|
||||
instance.setAttribute("window", `title: xrsh.iso; uid: ${instance.uid}; attach: #overlay; dom: #${instance.dom.id}; ${size}; min: ${this.data.minimized}; max: ${this.data.maximized}`)
|
||||
})
|
||||
|
||||
instance.addEventListener('window.oncreate', (e) => {
|
||||
instance.dom.classList.add('blink')
|
||||
// canvas to texture texture
|
||||
instance.setAttribute("html-as-texture-in-xr", `domid: .winbox#${instance.uid}; faceuser: true`)
|
||||
|
||||
// run iso
|
||||
let opts = {dom:instance.dom}
|
||||
for( let i in this.data ) opts[i] = this.data[i]
|
||||
opts.cols = this.cols
|
||||
opts.rows = this.rows
|
||||
this.term.start(opts)
|
||||
this.isoterminal.runISO(opts)
|
||||
})
|
||||
|
||||
instance.setAttribute("dom", "")
|
||||
instance.setAttribute("pastedrop", "")
|
||||
instance.setAttribute("dom", "")
|
||||
|
||||
|
||||
// *REMOVE* make a boot-plugin mechanism in feat/term.js
|
||||
this.term.addEventListener('enable-console', () => {
|
||||
this.isoterminal.addEventListener('postReady', (e)=>{
|
||||
// bugfix: send window dimensions to xterm (xterm.js does that from dom-sizechange to xterm via escape codes)
|
||||
let wb = instance.winbox
|
||||
if( this.data.maximized ){
|
||||
wb.restore()
|
||||
wb.maximize()
|
||||
}else wb.resize()
|
||||
})
|
||||
|
||||
this.isoterminal.addEventListener('ready', (e)=>{
|
||||
instance.dom.classList.remove('blink')
|
||||
this.isoterminal.emit('status',"running")
|
||||
setTimeout( () => { // important: after window maximize animation to get true size
|
||||
instance.setAttribute("html-as-texture-in-xr", `domid: #${instance.uid}`) // only show aframe-html in xr
|
||||
},1500)
|
||||
})
|
||||
|
||||
this.term.addEventListener('ready', (e) => {
|
||||
instance.dom.classList.remove('blink')
|
||||
this.term.emit('status',"running")
|
||||
if( this.data.debug ) this.runTests()
|
||||
})
|
||||
|
||||
this.term.addEventListener('status', function(e){
|
||||
this.isoterminal.addEventListener('status', function(e){
|
||||
let msg = e.detail
|
||||
const w = instance.winbox
|
||||
if(!w) return
|
||||
|
@ -379,56 +227,19 @@ if( typeof AFRAME != 'undefined '){
|
|||
instance.addEventListener('window.onresize', resize )
|
||||
instance.addEventListener('window.onmaximize', resize )
|
||||
|
||||
const focus = (showdom) => (e) => {
|
||||
this.el.emit('focus',e.detail)
|
||||
if( this.el.components.window && this.data.renderer == 'canvas'){
|
||||
this.el.components.window.show( showdom )
|
||||
const focus = (e) => {
|
||||
if( this.isoterminal?.emulator?.serial_adapter?.term ){
|
||||
this.isoterminal.emulator.serial_adapter.term.focus()
|
||||
}
|
||||
}
|
||||
instance.addEventListener('obbcollisionstarted', focus )
|
||||
|
||||
this.el.addEventListener('obbcollisionstarted', focus(false) )
|
||||
this.el.sceneEl.addEventListener('enter-vr', focus(false) )
|
||||
this.el.sceneEl.addEventListener('enter-ar', focus(false) )
|
||||
this.el.sceneEl.addEventListener('exit-vr', focus(true) )
|
||||
this.el.sceneEl.addEventListener('exit-ar', focus(true) )
|
||||
this.el.sceneEl.addEventListener('enter-vr', focus )
|
||||
this.el.sceneEl.addEventListener('enter-ar', focus )
|
||||
|
||||
instance.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera
|
||||
},
|
||||
|
||||
initHud: function(){
|
||||
if( AFRAME.utils.device.isMobile() ) this.data.HUD = true
|
||||
if( this.data.HUD ){
|
||||
document.querySelector('[camera]').appendChild( this.el )
|
||||
this.el.setAttribute("position","0 -0.03 -0.4")
|
||||
}
|
||||
},
|
||||
|
||||
runTests: async function(){
|
||||
await AFRAME.utils.require({
|
||||
"test_util": "tests/util.js",
|
||||
"test_isoterminal":"tests/ISOTerminal.js"
|
||||
})
|
||||
console.test.run()
|
||||
},
|
||||
|
||||
setupPasteDrop: function(){
|
||||
this.el.addEventListener('pasteFile', (e) => {
|
||||
e.preventDefault() // prevent bubbling up to window (which is triggering this initially)
|
||||
if( !this.term.pasteFile ) return // skip if feat/pastedrop.js is not loaded
|
||||
this.term.pasteFile(e.detail)
|
||||
})
|
||||
return this
|
||||
},
|
||||
|
||||
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
|
||||
},
|
||||
|
||||
events:{
|
||||
|
||||
|
@ -439,6 +250,10 @@ if( typeof AFRAME != 'undefined '){
|
|||
// reactive events for this.data updates
|
||||
myvalue: function(e){ this.el.dom.querySelector('b').innerText = this.data.myvalue },
|
||||
|
||||
ready: function( ){
|
||||
this.el.dom.style.display = 'none'
|
||||
},
|
||||
|
||||
launcher: async function(){
|
||||
this.initTerminal()
|
||||
}
|
||||
|
|
|
@ -1,349 +0,0 @@
|
|||
function ISOTerminal(instance,opts){
|
||||
// create a neutral isoterminal object which can be decorated
|
||||
// with prototype functions and has addListener() and dispatchEvent()
|
||||
let obj = new EventTarget()
|
||||
obj.instance = instance
|
||||
obj.opts = opts
|
||||
// register default event listeners (enable file based features like isoterminal/jsconsole.js e.g.)
|
||||
for( let event in ISOTerminal.listener )
|
||||
for( let cb in ISOTerminal.listener[event] )
|
||||
obj.addEventListener( event, ISOTerminal.listener[event][cb] )
|
||||
// compose object with functions
|
||||
for( let i in ISOTerminal.prototype ) obj[i] = ISOTerminal.prototype[i]
|
||||
obj.emit('init')
|
||||
return obj
|
||||
}
|
||||
|
||||
ISOTerminal.prototype.emit = function(event,data,sender){
|
||||
data = data || false
|
||||
const evObj = new CustomEvent(event, {detail: data} )
|
||||
this.preventFrameDrop( () => {
|
||||
// forward event to worker/instance/AFRAME element or component-function
|
||||
// this feels complex, but actually keeps event- and function-names more concise in codebase
|
||||
this.dispatchEvent( evObj )
|
||||
if( sender != "instance" && this.instance ) this.instance.dispatchEvent(evObj)
|
||||
if( sender != "worker" && this.worker ) this.worker.postMessage({event,data}, PromiseWorker.prototype.getTransferable(data) )
|
||||
if( sender !== undefined && typeof this[event] == 'function' ) this[event].apply(this, data && data.push ? data : [data] )
|
||||
})
|
||||
}
|
||||
|
||||
ISOTerminal.addEventListener = (event,cb) => {
|
||||
ISOTerminal.listener = ISOTerminal.listener || {}
|
||||
ISOTerminal.listener[event] = ISOTerminal.listener[event] || []
|
||||
ISOTerminal.listener[event].push(cb)
|
||||
}
|
||||
|
||||
ISOTerminal.prototype.exec = function(shellscript){
|
||||
this.send(`printf "\n\r"; ${shellscript}\n`,1)
|
||||
}
|
||||
|
||||
ISOTerminal.prototype.hook = function(hookname,args){
|
||||
let cmd = `{ type hook || source /etc/profile.sh; }; hook ${hookname} "${args.join('" "')}"`
|
||||
this.exec(cmd)
|
||||
}
|
||||
|
||||
ISOTerminal.prototype.serial_input = 0; // can be set to 0,1,2,3 to define stdinput tty (xterm plugin)
|
||||
|
||||
ISOTerminal.prototype.send = function(str, ttyNr){
|
||||
if( ttyNr == undefined) ttyNr = this.serial_input
|
||||
if( ttyNr == undefined ){
|
||||
if( this.emulator.serial_adapter ){
|
||||
this.emulator.serial_adapter.term.paste(str)
|
||||
}else this.emulator.keyboard_send_text(str) // vga screen
|
||||
}else{
|
||||
this.convert.toUint8Array( str ).map( (c) => {
|
||||
this.preventFrameDrop(
|
||||
() => {
|
||||
this.worker.postMessage({event:`serial${ttyNr}-input`,data:c})
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ISOTerminal.prototype.convert = {
|
||||
|
||||
arrayBufferToBase64: function(buffer){
|
||||
let binary = '';
|
||||
const bytes = new Uint8Array(buffer);
|
||||
const len = bytes.byteLength;
|
||||
for (let i = 0; i < len; i++) binary += String.fromCharCode(bytes[i]);
|
||||
return btoa(binary);
|
||||
},
|
||||
|
||||
base64ToArrayBuffer: function(base64) {
|
||||
const binaryString = atob(base64);
|
||||
const len = binaryString.length;
|
||||
const bytes = new Uint8Array(len);
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
return bytes.buffer;
|
||||
},
|
||||
|
||||
toUint8Array: function(str) {
|
||||
str = String(str) || String("")
|
||||
// Create a new Uint8Array with the same length as the input string
|
||||
const uint8Array = new Uint8Array(str.length);
|
||||
|
||||
// Iterate over the string and populate the Uint8Array
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
uint8Array[i] = str.charCodeAt(i);
|
||||
}
|
||||
return uint8Array;
|
||||
},
|
||||
|
||||
Uint8ArrayToString: function(arr){
|
||||
const decoder = new TextDecoder('utf-8'); // Specify encoding
|
||||
return decoder.decode(arr);
|
||||
}
|
||||
}
|
||||
|
||||
ISOTerminal.prototype.start = function(opts){
|
||||
|
||||
let me = this
|
||||
this.opts = {...this.opts, ...opts}
|
||||
let image = {}
|
||||
if( opts.iso.match(/\.iso$/) ) image.cdrom = { url: opts.iso }
|
||||
if( opts.iso.match(/\.bin$/) ) image.bzimage = { url: opts.iso }
|
||||
|
||||
opts = { ...image,
|
||||
uart1:true, // /dev/ttyS1
|
||||
uart2:true, // /dev/ttyS2
|
||||
uart3:true, // /dev/ttyS3
|
||||
wasm_path: "v86.wasm",
|
||||
memory_size: opts.memory * 1024 * 1024,
|
||||
vga_memory_size: 2 * 1024 * 1024,
|
||||
//screen_container: opts.dom,
|
||||
//serial_container: opts.dom,
|
||||
bios: {
|
||||
url: "bios/seabios.bin",
|
||||
},
|
||||
vga_bios: {
|
||||
url: "bios/vgabios.bin",
|
||||
//urg|: "com/isoterminal/bios/VGABIOS-lgpl-latest.bin",
|
||||
},
|
||||
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",
|
||||
// basefs: "com/isoterminal/v86/images/alpine-fs.json",
|
||||
// },
|
||||
//screen_dummy: true,
|
||||
//disable_jit: false,
|
||||
filesystem: {},
|
||||
autostart: true,
|
||||
prompt: this.opts.prompt,
|
||||
debug: this.opts.debug ? true : false
|
||||
};
|
||||
|
||||
this
|
||||
.setupWorker(opts)
|
||||
.startVM(opts)
|
||||
}
|
||||
|
||||
ISOTerminal.prototype.setupWorker = function(opts){
|
||||
|
||||
/*
|
||||
* the WebWorker (which runs v86)
|
||||
*
|
||||
*/
|
||||
this.worker = new PromiseWorker( "com/isoterminal/worker.js", (cb,event,data) => {
|
||||
if( !data.promiseId ) this.emit(event,data,"worker") // forward event to world
|
||||
this.preventFrameDrop( cb(event,data) )
|
||||
})
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
ISOTerminal.prototype.getLoaderMsg = function(){
|
||||
|
||||
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? 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",
|
||||
"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",
|
||||
"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"
|
||||
]
|
||||
|
||||
let motd = `
|
||||
\r[38;5;57m . . ____ _____________ ________. ._. ._. . .
|
||||
\r[38;5;93m . . .\\ \\/ /\\______ \\/ _____// | \\. .
|
||||
\r[38;5;93m . . . \\ / | _/\\_____ \\/ ~ \\ .
|
||||
\r[38;5;129m . . . / \\ | | \\/ \\ Y / .
|
||||
\r[38;5;165m . . ./___/\\ \\ |____|_ /_______ /\\___|_ /. .
|
||||
\r[38;5;201m . . . . . .\\_/. . . . \\/ . . . .\\/ . . _ \\/ . .
|
||||
\r[38;5;165m ▬▬▬▬▬▬▬▬ [38;5;51mhttps://xrsh.isvery.ninja[38;5;165m ▬▬▬▬▬▬▬▬▬▬▬▬
|
||||
\r[38;5;165m local-first, polyglot, unixy WebXR IDE & runtime
|
||||
\r[38;5;57m
|
||||
\r credits
|
||||
\r -------
|
||||
\r @nlnet@nlnet.nl
|
||||
\r @lvk@mastodon.online
|
||||
\r @utopiah@mastodon.pirateparty.be [38;5;51m
|
||||
\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`
|
||||
|
||||
const text_color = "\r[38;5;129m"
|
||||
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
|
||||
|
||||
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 )
|
||||
})
|
||||
});
|
||||
|
||||
let msglib = this.getLoaderMsg()
|
||||
let msg = msglib.motd
|
||||
|
||||
if( this.opts.prompt ){
|
||||
msg += `\r
|
||||
\r[36m 1)[0m boot [31m${String(this.opts.iso || "").replace(/.*\//,'')}[0m Linux ❤️
|
||||
\r[36m 2)[0m 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 })
|
||||
}
|
||||
|
||||
|
||||
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.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
|
||||
if( xr.isPresenting ){
|
||||
xr.getSession().requestAnimationFrame(cb)
|
||||
}else{
|
||||
window.requestAnimationFrame(cb)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
/*
|
||||
* This is basically a Javascript Proxy for 'new Worker()'
|
||||
* which allows calling worker-functions via promises.
|
||||
*
|
||||
* It's basically comlink without the fat.
|
||||
*
|
||||
* const w = new PromiseWorker("worker.js", (cb,event_or_fn,data) => {
|
||||
* cb(event_or_fn,data) // decorate/ratelimit/hooks here
|
||||
* })
|
||||
* w.foo().then( console.dir )
|
||||
*
|
||||
* in worker.js define a function: this.foo = () => return {foo:"Bar"}
|
||||
*/
|
||||
|
||||
function PromiseWorker(file, onmessage){
|
||||
|
||||
let proxy
|
||||
const worker = new Worker(file)
|
||||
|
||||
worker.onmessage = (e) => { // handle messages originating from worker
|
||||
const {event,data} = e.data // this is worker.onmessage(...)
|
||||
const cb = (event,data) => () => { //
|
||||
if( data.promiseId ){ //
|
||||
proxy.resolver(data) // forward to promise resolver
|
||||
}
|
||||
}
|
||||
onmessage(cb,event,data)
|
||||
}
|
||||
|
||||
return proxy = new Proxy(this,{
|
||||
get(me,k){
|
||||
if( k.match(/(postMessage|onmessage)/) ) return worker[k].bind(worker)
|
||||
if( k.match(/(resolver|promise)/) ) return this[k].bind(this)
|
||||
// promisify postMessage(...) call
|
||||
return function(){
|
||||
return this.promise(me,{event: k, data: [...arguments] })
|
||||
}
|
||||
},
|
||||
|
||||
promise(me,msg){
|
||||
if( typeof msg != 'object' || !msg.event || !msg.data ){
|
||||
throw 'worker.promise({event:..,data:..}) did not receive correct msg : '+JSON.stringify(msg)
|
||||
}
|
||||
this.resolvers = this.resolvers || {last:1,pending:{}}
|
||||
msg.data.promiseId = this.resolvers.last++
|
||||
// Send id and task to WebWorker
|
||||
worker.postMessage(msg, PromiseWorker.prototype.getTransferable(msg.data) )
|
||||
return new Promise( resolve => this.resolvers.pending[ msg.data.promiseId ] = resolve );
|
||||
},
|
||||
|
||||
resolver(data){
|
||||
if( !data || !data.promiseId ) throw 'promiseId not given'
|
||||
this.resolvers.pending[ data.promiseId ](data.result);
|
||||
delete this.resolvers.pending[ data.promiseId ]; // Prevent memory leak
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
PromiseWorker.prototype.getTransferable = function(data){
|
||||
let objs = []
|
||||
function isTransferable(obj) {
|
||||
return obj instanceof ArrayBuffer ||
|
||||
obj instanceof MessagePort ||
|
||||
obj instanceof ImageBitmap ||
|
||||
(typeof OffscreenCanvas !== 'undefined' && obj instanceof OffscreenCanvas) ||
|
||||
(typeof ReadableStream !== 'undefined' && obj instanceof ReadableStream) ||
|
||||
(typeof WritableStream !== 'undefined' && obj instanceof WritableStream) ||
|
||||
(typeof TransformStream !== 'undefined' && obj instanceof TransformStream);
|
||||
}
|
||||
for( var i in data ){
|
||||
if( isTransferable(data[i]) ) objs.push(data[i])
|
||||
}
|
||||
if( objs.length ) debugger
|
||||
return objs.length ? objs : undefined
|
||||
}
|
Binary file not shown.
|
@ -1,16 +0,0 @@
|
|||
--2024-09-23 17:49:04-- https://fonts.gstatic.com/s/cousine/v27/d6lNkaiiRdih4SpP9Z8K6T4.ttf
|
||||
Resolving fonts.gstatic.com (fonts.gstatic.com)... 142.250.180.195, 2a00:1450:400d:806::2003
|
||||
Connecting to fonts.gstatic.com (fonts.gstatic.com)|142.250.180.195|:443... connected.
|
||||
HTTP request sent, awaiting response... 200 OK
|
||||
Length: 287924 (281K) [font/ttf]
|
||||
Saving to: ‘d6lNkaiiRdih4SpP9Z8K6T4.ttf’
|
||||
|
||||
0K .......... .......... .......... .......... .......... 17% 2.31M 0s
|
||||
50K .......... .......... .......... .......... .......... 35% 1.91M 0s
|
||||
100K .......... .......... .......... .......... .......... 53% 9.09M 0s
|
||||
150K .......... .......... .......... .......... .......... 71% 3.03M 0s
|
||||
200K .......... .......... .......... .......... .......... 88% 3.40M 0s
|
||||
250K .......... .......... .......... . 100% 12.4M=0.09s
|
||||
|
||||
2024-09-23 17:49:05 (3.23 MB/s) - ‘d6lNkaiiRdih4SpP9Z8K6T4.ttf’ saved [287924/287924]
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
//ISOTerminal.prototype.exec(cmd_array,stdin){
|
||||
// // exec(['lua'] "print \"hello\") ---> cat /dev/browser/js/stdin | lua > /dev/browser/js/stdout
|
||||
//}
|
||||
|
||||
ISOTerminal.prototype.serial_input = undefined; // can be set to 0,1,2,3 to define stdinput tty (xterm plugin)
|
||||
|
||||
ISOTerminal.prototype.exec = function(shellscript){
|
||||
//let ts = String(Date.now())+".job"
|
||||
//this.emulator.create_file(ts, this.toUint8Array(shellscript) )
|
||||
this.send(shellscript+"\n",1)
|
||||
}
|
||||
|
||||
ISOTerminal.prototype.send = function(str, ttyNr){
|
||||
if( !ttyNr ) ttyNr = this.serial_input
|
||||
if( !ttyNr ){
|
||||
if( this.emulator.serial_adapter ){
|
||||
this.emulator.serial_adapter.term.paste(str)
|
||||
}else this.emulator.keyboard_send_text(str) // vga screen
|
||||
}else{
|
||||
this.convert.toUint8Array( str ).map( (c) => this.emulator.bus.send(`serial${ttyNr}-input`, c ) )
|
||||
}
|
||||
}
|
||||
|
||||
ISOTerminal.prototype.convert = {
|
||||
|
||||
arrayBufferToBase64: function(buffer){
|
||||
let binary = '';
|
||||
const bytes = new Uint8Array(buffer);
|
||||
const len = bytes.byteLength;
|
||||
for (let i = 0; i < len; i++) binary += String.fromCharCode(bytes[i]);
|
||||
return window.btoa(binary);
|
||||
},
|
||||
|
||||
base64ToArrayBuffer: function(base64) {
|
||||
const binaryString = window.atob(base64);
|
||||
const len = binaryString.length;
|
||||
const bytes = new Uint8Array(len);
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
return bytes.buffer;
|
||||
},
|
||||
|
||||
toUint8Array: function(str) {
|
||||
str = String(str) || String("")
|
||||
// Create a new Uint8Array with the same length as the input string
|
||||
const uint8Array = new Uint8Array(str.length);
|
||||
|
||||
// Iterate over the string and populate the Uint8Array
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
uint8Array[i] = str.charCodeAt(i);
|
||||
}
|
||||
return uint8Array;
|
||||
},
|
||||
|
||||
Uint8ArrayToString: function(arr){
|
||||
const decoder = new TextDecoder('utf-8'); // Specify encoding
|
||||
return decoder.decode(arr);
|
||||
}
|
||||
}
|
||||
|
||||
ISOTerminal.prototype.runISO = function(opts){
|
||||
|
||||
let me = this
|
||||
this.opts = {...this.opts, ...opts}
|
||||
let image = {}
|
||||
if( opts.iso.match(/\.iso$/) ) image.cdrom = { url: opts.iso }
|
||||
if( opts.iso.match(/\.bin$/) ) image.bzimage = { url: opts.iso }
|
||||
|
||||
opts = { ...image,
|
||||
uart1:true, // /dev/ttyS1
|
||||
uart2:true, // /dev/ttyS2
|
||||
uart3:true, // /dev/ttyS3
|
||||
wasm_path: "com/isoterminal/v86.wasm",
|
||||
memory_size: opts.memory * 1024 * 1024,
|
||||
vga_memory_size: 2 * 1024 * 1024,
|
||||
screen_container: opts.dom,
|
||||
//serial_container: opts.dom,
|
||||
bios: {
|
||||
url: "com/isoterminal/bios/seabios.bin",
|
||||
},
|
||||
vga_bios: {
|
||||
url: "com/isoterminal/bios/vgabios.bin",
|
||||
//urg|: "com/isoterminal/bios/VGABIOS-lgpl-latest.bin",
|
||||
},
|
||||
network_relay_url: "wss://relay.widgetry.org/",
|
||||
cmdline: "rw root=host9p rootfstype=9p rootflags=trans=virtio,cache=loose modules=virtio_pci tsc=reliable init_on_freg|=on vga=ask", //vga=0x122",
|
||||
//bzimage_initrd_from_filesystem: true,
|
||||
//filesystem: {
|
||||
// baseurl: "com/isoterminal/v86/images/alpine-rootfs-flat",
|
||||
// basefs: "com/isoterminal/v86/images/alpine-fs.json",
|
||||
// },
|
||||
//screen_dummy: true,
|
||||
//disable_jit: false,
|
||||
filesystem: {},
|
||||
autostart: true,
|
||||
};
|
||||
this.emit('runISO',opts)
|
||||
let emulator = this.emulator = new V86(opts)
|
||||
|
||||
const loading = [
|
||||
'loading quantum bits and bytes',
|
||||
'preparing quantum flux capacitors',
|
||||
'crunching peanuts and chakras',
|
||||
'preparing parallel universe',
|
||||
'loading quantum state fluctuations',
|
||||
'preparing godmode',
|
||||
'loading cat pawns and cuteness',
|
||||
'beaming up scotty',
|
||||
'still faster than Windows update',
|
||||
'loading a microlinux',
|
||||
'figuring out meaning of life',
|
||||
'Aligning your chakras now',
|
||||
'Breathing in good vibes',
|
||||
'Finding inner peace soon',
|
||||
'Centering your Zen energy',
|
||||
'Awakening third eye powers',
|
||||
'Tuning into the universe',
|
||||
'Balancing your cosmic karma',
|
||||
'Stretching time and space',
|
||||
'Recharging your soul battery',
|
||||
'Transcending earthly limits'
|
||||
]
|
||||
|
||||
let loadmsg = loading[ Math.floor(Math.random()*1000) % loading.length ]
|
||||
this.emit('status',loadmsg)
|
||||
|
||||
// replace welcome message https://github.com/copy/v86/blob/3c77b98bc4bc7a5d51a2056ea73d7666ca50fc9d/src/browser/serial.js#L231
|
||||
let welcome = "This is the serial console. Whatever you type or paste here will be sent to COM1"
|
||||
let motd = "\r[38;5;129m"
|
||||
let msg = `${loadmsg}, please wait..`
|
||||
while( msg.length < welcome.length ) msg += " "
|
||||
msg += "\n"
|
||||
motd += msg+"\033[0m"
|
||||
|
||||
emulator.bus.register("emulator-started", async (e) => {
|
||||
this.emit('emulator-started',e)
|
||||
|
||||
if( emulator.serial_adapter ){
|
||||
emulator.serial_adapter.term.clear()
|
||||
emulator.serial_adapter.term.write(motd)
|
||||
}
|
||||
|
||||
|
||||
if( me.opts.overlayfs ){
|
||||
fetch(me.opts.overlayfs)
|
||||
.then( (f) => {
|
||||
f.arrayBuffer().then( (buf) => {
|
||||
emulator.create_file('overlayfs.zip', new Uint8Array(buf) )
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
let line = ''
|
||||
let ready = false
|
||||
emulator.add_listener(`serial0-output-byte`, async (byte) => {
|
||||
this.emit('${this.serial}-output-byte',byte)
|
||||
var chr = String.fromCharCode(byte);
|
||||
if(chr < " " && chr !== "\n" && chr !== "\t" || chr > "~")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if(chr === "\n")
|
||||
{
|
||||
var new_line = line;
|
||||
line = "";
|
||||
}
|
||||
else if(chr >= " " && chr <= "~")
|
||||
{
|
||||
line += chr;
|
||||
}
|
||||
if( !ready && line.match(/^(\/ #|~%|\[.*\]>)/) ){
|
||||
this.emit('postReady',e)
|
||||
setTimeout( () => this.emit('ready',e), 500 )
|
||||
ready = true
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
ISOTerminal.prototype.readFromPipe = function(filename,cb){
|
||||
|
||||
this.emulator.add_listener("9p-write-end", async (opts) => {
|
||||
if ( opts[0] == filename.replace(/.*\//,'') ){
|
||||
const buf = await this.emulator.read_file("console.tty")
|
||||
cb( this.convert.Uint8ArrayToString(buf) )
|
||||
}
|
||||
})
|
||||
|
||||
}
|
|
@ -1,95 +1,46 @@
|
|||
let emulator = this.emulator
|
||||
let me = this
|
||||
ISOTerminal.addEventListener('emulator-started', function(){
|
||||
let emulator = this.emulator
|
||||
let isoterminal = this
|
||||
|
||||
emulator.fs9p.update_file = async function(file,data){
|
||||
const convert = ISOTerminal.prototype.convert
|
||||
emulator.fs9p.update_file = async function(file,data){
|
||||
|
||||
const p = this.SearchPath(file);
|
||||
const p = this.SearchPath(file);
|
||||
|
||||
if(p.id === -1)
|
||||
{
|
||||
return emulator.create_file(file,data)
|
||||
}
|
||||
|
||||
const inode = this.GetInode(p.id);
|
||||
const buf = typeof data == 'string' ? convert.toUint8Array(data) : data || ""
|
||||
if( buf.length == 0 ) return new Promise( (resolve,reject) => resolve(data) )
|
||||
|
||||
try{
|
||||
if(p.id === -1)
|
||||
{
|
||||
return emulator.create_file(file,data)
|
||||
}
|
||||
|
||||
const inode = this.GetInode(p.id);
|
||||
const buf = typeof data == 'string' ? isoterminal.convert.toUint8Array(data) : data
|
||||
await this.Write(p.id,0, buf.length, buf )
|
||||
// update inode
|
||||
inode.size = buf.length
|
||||
const now = Math.round(Date.now() / 1000);
|
||||
inode.atime = inode.mtime = now;
|
||||
isoterminal.exec(`touch ${file}`) // update inode
|
||||
return new Promise( (resolve,reject) => resolve(buf) )
|
||||
}catch(e){
|
||||
console.error({file,data})
|
||||
return new Promise( (resolve,reject) => reject(e) )
|
||||
}
|
||||
}
|
||||
|
||||
emulator.fs9p.append_file = async function(file,data){
|
||||
const convert = ISOTerminal.prototype.convert
|
||||
}
|
||||
|
||||
const p = this.SearchPath(file);
|
||||
emulator.fs9p.append_file = async function(file,data){
|
||||
|
||||
if(p.id === -1)
|
||||
{
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
const inode = this.GetInode(p.id);
|
||||
const buf = typeof data == 'string' ? convert.toUint8Array(data) : data
|
||||
await this.Write(p.id, inode.size, buf.length, buf )
|
||||
// update inode
|
||||
inode.size = inode.size + buf.length
|
||||
const now = Math.round(Date.now() / 1000);
|
||||
inode.atime = inode.mtime = now;
|
||||
return new Promise( (resolve,reject) => resolve(buf) )
|
||||
const p = this.SearchPath(file);
|
||||
|
||||
}
|
||||
if(p.id === -1)
|
||||
{
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
const inode = this.GetInode(p.id);
|
||||
const buf = typeof data == 'string' ? isoterminal.convert.toUint8Array(data) : data
|
||||
await this.Write(p.id, inode.size, buf.length, buf )
|
||||
// update inode
|
||||
inode.size = inode.size + buf.length
|
||||
const now = Math.round(Date.now() / 1000);
|
||||
inode.atime = inode.mtime = now;
|
||||
return new Promise( (resolve,reject) => resolve(buf) )
|
||||
|
||||
emulator.fs9p.read_file_world = async function(file){
|
||||
const p = this.SearchPath(file);
|
||||
}
|
||||
|
||||
if(p.id === -1)
|
||||
{
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
const inode = this.GetInode(p.id);
|
||||
const perms = this.parseFilePermissions(inode.mode)
|
||||
|
||||
if( !perms.world.read ){
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
return this.Read(p.id, 0, inode.size);
|
||||
}
|
||||
|
||||
emulator.fs9p.parseFilePermissions = function(permissionInt) {
|
||||
// Convert the permission integer to octal
|
||||
const octalPermissions = permissionInt.toString(8);
|
||||
|
||||
// Extract the permission bits (last 3 digits in octal)
|
||||
const permissionBits = octalPermissions.slice(-3);
|
||||
|
||||
|
||||
function parsePermission(digit) {
|
||||
const num = parseInt(digit, 10);
|
||||
|
||||
return {
|
||||
read: Boolean(num & 4), // 4 = read
|
||||
write: Boolean(num & 2), // 2 = write
|
||||
execute: Boolean(num & 1) // 1 = execute
|
||||
};
|
||||
}
|
||||
// Decode the permissions
|
||||
const permissions = {
|
||||
owner: parsePermission(permissionBits[0]),
|
||||
group: parsePermission(permissionBits[1]),
|
||||
world: parsePermission(permissionBits[2]),
|
||||
};
|
||||
|
||||
return permissions;
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,72 +1,52 @@
|
|||
if( typeof emulator != 'undefined' ){
|
||||
// inside worker-thread
|
||||
importScripts("localforage.js") // we don't instance it again here (just use its functions)
|
||||
|
||||
this.restore_state = async function(data){
|
||||
return new Promise( (resolve,reject) => {
|
||||
localforage.getItem("state", async (err,stateBase64) => {
|
||||
if( stateBase64 && !err ){
|
||||
state = ISOTerminal.prototype.convert.base64ToArrayBuffer( stateBase64 )
|
||||
await emulator.restore_state(state)
|
||||
console.log("restored state")
|
||||
}else return reject("worker.js: emulator.restore_state (could not get state from localforage)")
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
this.save_state = async function(){
|
||||
console.log("saving session")
|
||||
let state = await emulator.save_state()
|
||||
localforage.setDriver([
|
||||
localforage.INDEXEDDB,
|
||||
localforage.WEBSQL,
|
||||
localforage.LOCALSTORAGE
|
||||
]).then( () => {
|
||||
localforage.setItem("state", ISOTerminal.prototype.convert.arrayBufferToBase64(state) )
|
||||
console.log("state saved")
|
||||
})
|
||||
}
|
||||
ISOTerminal.addEventListener('emulator-started', function(e){
|
||||
this.autorestore(e)
|
||||
})
|
||||
|
||||
ISOTerminal.prototype.autorestore = async function(e){
|
||||
|
||||
localforage.setDriver([
|
||||
localforage.INDEXEDDB,
|
||||
localforage.WEBSQL,
|
||||
localforage.LOCALSTORAGE
|
||||
]).then( () => {
|
||||
|
||||
localforage.getItem("state", async (err,stateBase64) => {
|
||||
if( !err && confirm('continue last session?') ){
|
||||
this.noboot = true // see feat/boot.js
|
||||
state = this.convert.base64ToArrayBuffer( stateBase64 )
|
||||
this.emulator.restore_state(state)
|
||||
this.emit('postReady',e)
|
||||
setTimeout( () => {
|
||||
this.emit('ready',e)
|
||||
// press CTRL+a l (=gnu screen redisplay)
|
||||
setTimeout( () => this.send("l\n"),400 )
|
||||
// reload index.js
|
||||
this.emulator.read_file("root/index.js")
|
||||
.then( this.convert.Uint8ArrayToString )
|
||||
.then( this.runJavascript )
|
||||
.catch( console.error )
|
||||
// reload index.html
|
||||
this.emulator.read_file("root/index.html")
|
||||
.then( this.convert.Uint8ArrayToString )
|
||||
.then( this.runHTML )
|
||||
.catch( console.error )
|
||||
|
||||
}, 500 )
|
||||
}
|
||||
})
|
||||
|
||||
this.save = async () => {
|
||||
const state = await this.emulator.save_state()
|
||||
console.log( String(this.convert.arrayBufferToBase64(state)).substr(0,5) )
|
||||
localforage.setItem("state", this.convert.arrayBufferToBase64(state) )
|
||||
}
|
||||
|
||||
window.addEventListener("beforeunload", function (e) {
|
||||
var confirmationMessage = "Sure you want to leave?\nTIP: enter 'save' to continue this session later";
|
||||
(e || window.event).returnValue = confirmationMessage; //Gecko + IE
|
||||
return confirmationMessage; //Webkit, Safari, Chrome
|
||||
});
|
||||
|
||||
}else{
|
||||
// inside browser-thread
|
||||
ISOTerminal.addEventListener('emulator-started', function(e){
|
||||
this.autorestore(e)
|
||||
})
|
||||
|
||||
ISOTerminal.prototype.autorestore = async function(e){
|
||||
|
||||
localforage.setDriver([
|
||||
localforage.INDEXEDDB,
|
||||
localforage.WEBSQL,
|
||||
localforage.LOCALSTORAGE
|
||||
]).then( () => {
|
||||
|
||||
localforage.getItem("state", async (err,stateBase64) => {
|
||||
if( stateBase64 && !err && confirm('continue last session?') ){
|
||||
this.noboot = true // see feat/boot.js
|
||||
try{
|
||||
await this.worker.restore_state()
|
||||
// simulate / fastforward boot events
|
||||
this.postBoot( () => {
|
||||
this.send("l\n")
|
||||
this.send("hook wakeup\n")
|
||||
})
|
||||
}catch(e){ console.error(e) }
|
||||
}
|
||||
})
|
||||
|
||||
this.save = async () => {
|
||||
await this.worker.save_state()
|
||||
}
|
||||
|
||||
window.addEventListener("beforeunload", function (e) {
|
||||
var confirmationMessage = "Sure you want to leave?\nTIP: enter 'save' to continue this session later";
|
||||
(e || window.event).returnValue = confirmationMessage; //Gecko + IE
|
||||
return confirmationMessage; //Webkit, Safari, Chrome
|
||||
});
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -4,23 +4,24 @@ ISOTerminal.addEventListener('ready', function(e){
|
|||
|
||||
ISOTerminal.prototype.boot = async function(e){
|
||||
// set environment
|
||||
let env = [
|
||||
`export LINES=${this.opts.rows}`,
|
||||
`export COLUMNS=${this.opts.cols}`,
|
||||
'export BROWSER=1',
|
||||
]
|
||||
let env = ['export BROWSER=1']
|
||||
for ( let i in document.location ){
|
||||
if( typeof document.location[i] == 'string' ){
|
||||
env.push( 'export '+String(i).toUpperCase()+'="'+decodeURIComponent( document.location[i]+'"') )
|
||||
}
|
||||
if( typeof document.location[i] == 'string' )
|
||||
env.push( 'export '+String(i).toUpperCase()+'="'+document.location[i]+'"')
|
||||
}
|
||||
await this.worker.create_file("profile.browser", this.convert.toUint8Array( env.join('\n') ) )
|
||||
await this.emulator.create_file("profile.browser", this.convert.toUint8Array( env.join('\n') ) )
|
||||
|
||||
if( this.serial_input == 0 ){
|
||||
if( !this.noboot ){
|
||||
this.send("source /etc/profile # \\o/ FOSS powa!\n")
|
||||
let boot = "source /etc/profile\n"
|
||||
this.send(boot+"\n")
|
||||
}
|
||||
}
|
||||
|
||||
if( this.emulator.serial_adapter ) this.emulator.serial_adapter.term.focus()
|
||||
else{
|
||||
let els = [...document.querySelectorAll("div#screen")]
|
||||
els.map( (el) => el.focus() )
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
}
|
|
@ -1,42 +1,32 @@
|
|||
ISOTerminal.addEventListener('init', function(){
|
||||
|
||||
if( typeof emulator != 'undefined' ){
|
||||
// inside worker-thread
|
||||
this.addEventListener('emulator-started', function(e){
|
||||
|
||||
this.listenIndexHTML = () => {
|
||||
const file = "dev/browser/html"
|
||||
emulator.readFromPipe( file, async (data) => {
|
||||
const buf = await emulator.read_file( file )
|
||||
const emulator = this.emulator
|
||||
|
||||
// unix to js device
|
||||
this.readFromPipe( '/mnt/index.html', async (data) => {
|
||||
const buf = await emulator.read_file("index.html")
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
const html = decoder.decode(buf).replace(/^#!\/bin\/html/,'') // remove leftover shebangs if any
|
||||
const html = decoder.decode(buf)
|
||||
try{
|
||||
this.postMessage({event:'runHTML',data:[html]})
|
||||
this.runHTML(html)
|
||||
}catch(e){
|
||||
console.error(file)
|
||||
console.error(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}else{
|
||||
// inside browser-thread
|
||||
|
||||
ISOTerminal.addEventListener('emulator-started', function(){
|
||||
this.addEventListener('ready', async () => {
|
||||
this.worker.listenIndexHTML()
|
||||
})
|
||||
})
|
||||
|
||||
ISOTerminal.prototype.runHTML = function(html){
|
||||
let $scene = document.querySelector("a-scene")
|
||||
let $root = document.querySelector("a-entity#root")
|
||||
if( !$root ){
|
||||
$root = document.createElement("a-entity")
|
||||
$root.id = "root"
|
||||
$scene.appendChild($root)
|
||||
}
|
||||
console.log(html)
|
||||
$root.innerHTML = html
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
ISOTerminal.prototype.runHTML = function(html){
|
||||
let $scene = document.querySelector("a-scene")
|
||||
let $root = document.querySelector("a-entity#root")
|
||||
if( !$root ){
|
||||
$root = document.createElement("a-entity")
|
||||
$root.id = "root"
|
||||
$scene.appendChild($root)
|
||||
}
|
||||
$root.innerHTML = html
|
||||
}
|
||||
|
|
|
@ -1,30 +1,32 @@
|
|||
if( typeof emulator != 'undefined' ){
|
||||
// inside worker-thread
|
||||
ISOTerminal.addEventListener('init', function(){
|
||||
|
||||
// unix to js device
|
||||
this.emulator.readFromPipe( 'dev/browser/js', async (data) => {
|
||||
const buf = await emulator.read_file("dev/browser/js")
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
const js = decoder.decode(buf).replace(/^#!\/bin\/js/,'') // remove leftover shebangs if any
|
||||
try{
|
||||
this.postMessage({event:'runJavascript',data:[js]})
|
||||
}catch(e){
|
||||
console.error(e)
|
||||
}
|
||||
})
|
||||
|
||||
this.addEventListener('emulator-started', function(e){
|
||||
|
||||
}else{
|
||||
// inside browser-thread
|
||||
const emulator = this.emulator
|
||||
|
||||
ISOTerminal.prototype.runJavascript = function(js){
|
||||
let $root = document.querySelector("script#root")
|
||||
if( !$root ){
|
||||
$root = document.createElement("script")
|
||||
$root.id = "root"
|
||||
document.body.appendChild($root)
|
||||
}
|
||||
$root.innerHTML = js
|
||||
}
|
||||
// unix to js device
|
||||
this.readFromPipe( '/mnt/index.js', async (data) => {
|
||||
const buf = await emulator.read_file("index.js")
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
const js = decoder.decode(buf)
|
||||
try{
|
||||
this.runJavascript(js)
|
||||
}catch(e){
|
||||
console.error(e)
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
ISOTerminal.prototype.runJavascript = function(js){
|
||||
let $root = document.querySelector("script#root")
|
||||
if( !$root ){
|
||||
$root = document.createElement("script")
|
||||
$root.id = "root"
|
||||
document.body.appendChild($root)
|
||||
}
|
||||
$root.innerHTML = js
|
||||
}
|
||||
|
||||
|
|
|
@ -1,51 +1,29 @@
|
|||
if( typeof emulator != 'undefined' ){
|
||||
// inside worker-thread
|
||||
ISOTerminal.addEventListener('init', function(){
|
||||
|
||||
// unix to js device
|
||||
emulator.readFromPipe( 'dev/browser/js', async (data) => {
|
||||
const convert = ISOTerminal.prototype.convert
|
||||
const buf = await this.emulator.read_file("dev/browser/js")
|
||||
const script = convert.Uint8ArrayToString(buf)
|
||||
let PID=null
|
||||
try{
|
||||
if( script.match(/^PID/) ){
|
||||
PID = script.match(/^PID=([0-9]+);/)[1]
|
||||
this.addEventListener('emulator-started', function(e){
|
||||
|
||||
const emulator = this.emulator
|
||||
|
||||
// unix to js device
|
||||
this.readFromPipe( '/mnt/js', async (data) => {
|
||||
const buf = await emulator.read_file("js")
|
||||
const script = this.convert.Uint8ArrayToString(buf)
|
||||
let PID="?"
|
||||
try{
|
||||
if( script.match(/^PID/) ){
|
||||
PID = script.match(/^PID=([0-9]+);/)[1]
|
||||
}
|
||||
let res = (new Function(`${script}`))()
|
||||
if( res && typeof res != 'string' ) res = JSON.stringify(res,null,2)
|
||||
// write output to 9p with PID as filename
|
||||
// *FIXME* not flexible / robust
|
||||
emulator.create_file(PID, this.convert.toUint8Array(res) )
|
||||
}catch(e){
|
||||
console.error(e)
|
||||
}
|
||||
this.postMessage({event:'javascript-eval',data:{script,PID}})
|
||||
}catch(e){
|
||||
console.error(e)
|
||||
}
|
||||
})
|
||||
|
||||
}else{
|
||||
// inside browser-thread
|
||||
})
|
||||
|
||||
ISOTerminal.addEventListener('javascript-eval', async function(e){
|
||||
const {script,PID} = e.detail
|
||||
let res;
|
||||
})
|
||||
|
||||
try{
|
||||
let f = new Function(`${script}`);
|
||||
res = f();
|
||||
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)
|
||||
}
|
||||
// update output to 9p with PID as filename (in /mnt/run)
|
||||
if( PID ){
|
||||
this.worker.update_file(`run/${PID}`, this.convert.toUint8Array(res) )
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
|
|
@ -3,77 +3,54 @@ 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');
|
||||
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]
|
||||
let str = JSON.stringify(textArg,null,2)+'\n'
|
||||
handler( addLineFeeds(str) )
|
||||
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]
|
||||
handler( addLineFeeds(textArg),'\x1b[38;5;208mwarn\x1b[0m');
|
||||
const textArg = args[0].message?args[0].message:args[0];
|
||||
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(){
|
||||
let emulator = this.emulator
|
||||
|
||||
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])
|
||||
emulator.fs9p.append_file( "console", finalStr )
|
||||
})
|
||||
|
||||
window.addEventListener('error', function(event) {
|
||||
if( event.filename ){
|
||||
console.error(event.filename+":"+event.lineno+":"+event.colno)
|
||||
console.error(event.message);
|
||||
console.error(event.error);
|
||||
}else console.error(event)
|
||||
console.error(event.filename+":"+event.lineno+":"+event.colno)
|
||||
console.error(event.message);
|
||||
console.error(event.error);
|
||||
});
|
||||
|
||||
window.addEventListener('unhandledrejection', function(event) {
|
||||
console.error(event);
|
||||
console.error('Unhandled promise rejection:', event.reason);
|
||||
});
|
||||
|
||||
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)
|
||||
// enable/disable logging file (echo 1 > mnt/console.tty)
|
||||
this.readFromPipe( '/mnt/console.tty', (data) => {
|
||||
emulator.log_to_tty = ( String(data).trim() == '1')
|
||||
})
|
||||
|
||||
})
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
if( typeof emulator != 'undefined' ){
|
||||
// inside worker-thread
|
||||
|
||||
}else{
|
||||
// inside browser-thread
|
||||
//
|
||||
ISOTerminal.prototype.pasteWriteFile = async function(data,type,filename){
|
||||
this.pasteWriteFile.fileCount = this.pasteWriteFile.fileCount || 0
|
||||
const file = `clipboard/`+ ( filename || `user-paste-${this.pasteWriteFile.fileCount}`)
|
||||
await this.worker.create_file(file, data )
|
||||
// run the xrsh hook
|
||||
this.hook("clipboard", [ `/mnt/${file}`, type ] )
|
||||
console.log("clipboard paste: /mnt/"+file)
|
||||
this.pasteWriteFile.fileCount += 1
|
||||
}
|
||||
|
||||
ISOTerminal.prototype.pasteFile = async function(data){
|
||||
const {type,item,pastedText} = data
|
||||
if( pastedText){
|
||||
// the terminal handles this (pastes text)
|
||||
// this.pasteWriteFile( this.convert.toUint8Array(pastedText) ,type, null, true)
|
||||
}else{
|
||||
const file = item.getAsFile();
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const arr = new Uint8Array(e.target.result)
|
||||
this.pasteWriteFile( arr, type, file.name ); // or use readAsDataURL for images
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
}
|
||||
}
|
||||
|
||||
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) )
|
||||
})
|
||||
}
|
|
@ -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[36m>[0m "
|
||||
|
||||
|
||||
// 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) )
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
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')
|
||||
})
|
||||
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( () => {
|
||||
isoterm.xtermAutoResize(isoterm.emulator.serial_adapter.term, isoterm.instance,-3)
|
||||
},800) // wait for resize anim
|
||||
}
|
||||
isoterm.instance.addEventListener('window.onresize', resize )
|
||||
isoterm.instance.addEventListener('window.onmaximize', resize )
|
||||
}
|
||||
|
||||
ISOTerminal.prototype.xtermAutoResize = function(term,instance,rowoffset){
|
||||
if( !term.element ) return
|
||||
|
||||
const defaultScrollWidth = 24;
|
||||
const MINIMUM_COLS = 2;
|
||||
const MINIMUM_ROWS = 2;
|
||||
|
||||
const dims = term._core._renderService.dimensions;
|
||||
const scrollbarWidth = (term.options.scrollback === 0
|
||||
? 0
|
||||
: (term.options.overviewRuler?.width || defaultScrollWidth ));
|
||||
|
||||
const parentElementStyle = window.getComputedStyle(instance.dom);
|
||||
const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
|
||||
const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')));
|
||||
const elementStyle = window.getComputedStyle(term.element);
|
||||
const elementPadding = {
|
||||
top: parseInt(elementStyle.getPropertyValue('padding-top')),
|
||||
bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')),
|
||||
right: parseInt(elementStyle.getPropertyValue('padding-right')),
|
||||
left: parseInt(elementStyle.getPropertyValue('padding-left'))
|
||||
};
|
||||
const elementPaddingVer = elementPadding.top + elementPadding.bottom;
|
||||
const elementPaddingHor = elementPadding.right + elementPadding.left;
|
||||
const availableHeight = parentElementHeight - elementPaddingVer;
|
||||
const availableWidth = parentElementWidth - elementPaddingHor - scrollbarWidth;
|
||||
const geometry = {
|
||||
cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / dims.css.cell.width)),
|
||||
rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / dims.css.cell.height))
|
||||
};
|
||||
term.resize(geometry.cols, geometry.rows + (rowoffset||0) );
|
||||
}
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
@ -1,96 +0,0 @@
|
|||
importScripts("libv86.js");
|
||||
importScripts("ISOTerminal.js") // we don't instance it again here (just use its functions)
|
||||
|
||||
this.runISO = function(opts){
|
||||
this.opts = opts
|
||||
if( opts.debug ) console.dir(opts)
|
||||
|
||||
if( opts.cdrom && !opts.cdrom.url.match(/^http/) ) opts.cdrom.url = "../../"+opts.cdrom.url
|
||||
if( opts.bzimage && !opts.cdrom.url.match(/^http/) ) opts.bzimage.url = "../../"+opts.bzimage.url
|
||||
|
||||
let emulator = this.emulator = new V86(opts);
|
||||
console.log("[worker.js] started emulator")
|
||||
|
||||
// event forwarding
|
||||
emulator.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 )
|
||||
}.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 )
|
||||
}.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 )
|
||||
}.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
|
||||
*/
|
||||
|
||||
// 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])) }
|
||||
|
||||
// filename will be read from 9pfs: "/mnt/"+filename
|
||||
emulator.readFromPipe = function(filename,cb){
|
||||
emulator.add_listener("9p-write-end", async (opts) => {
|
||||
if ( opts[0] == filename.replace(/.*\//,'') ){
|
||||
cb()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
importScripts("feat/javascript.js")
|
||||
importScripts("feat/index.html.js")
|
||||
importScripts("feat/autorestore.js")
|
||||
}
|
||||
/*
|
||||
* forward events/functions so non-worker world can reach them
|
||||
*/
|
||||
|
||||
this['serial0-input'] = function(c){ emulator.bus.send( 'serial0-input', c) } // to /dev/ttyS0
|
||||
this['serial1-input'] = function(c){ emulator.bus.send( 'serial1-input', c) } // to /dev/ttyS1
|
||||
this['serial2-input'] = function(c){ emulator.bus.send( 'serial2-input', c) } // to /dev/ttyS2
|
||||
|
||||
this.onmessage = async function(e){
|
||||
let {event,data} = e.data
|
||||
if( this[event] ){
|
||||
if( this.opts?.debug ) console.log(`[worker.js] this.${event}(${JSON.stringify(data).substr(0,60)})`)
|
||||
try{
|
||||
let result = await this[event](data)
|
||||
if( data.promiseId ){ // auto-callback to ISOTerminal.worker.promise(...)
|
||||
this.postMessage({event,data: {...data,result}})
|
||||
}
|
||||
}catch(e){
|
||||
if( data.promiseId ){ // auto-callback to ISOTerminal.worker.promise(...)
|
||||
this.postMessage({event,data: {...data,error:e}})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,179 @@
|
|||
AFRAME.registerComponent('paste', {
|
||||
schema: {
|
||||
foo: { type:"string"}
|
||||
},
|
||||
|
||||
init: function () {
|
||||
this.el.object3D.visible = false
|
||||
//this.el.innerHTML = ` `
|
||||
},
|
||||
|
||||
requires:{
|
||||
osbutton: "com/osbutton.js"
|
||||
},
|
||||
|
||||
events:{
|
||||
|
||||
// component events
|
||||
somecomponent: function( ){ console.log("component requirement mounted") },
|
||||
ready: function(e){ console.log("requires are loaded") },
|
||||
|
||||
launcher: function(e){
|
||||
const paste = () => {
|
||||
navigator.clipboard.readText()
|
||||
.then( (base64) => {
|
||||
let mimetype = base64.replace(/;base64,.*/,'')
|
||||
let data = base64.replace(/.*;base64,/,'')
|
||||
let type = this.textHeuristic(data)
|
||||
console.log("type="+type)
|
||||
switch( this.textHeuristic(data) ){
|
||||
case "aframe": this.insertAFRAME(data); break;
|
||||
default: this.insertText(data); break;
|
||||
}
|
||||
this.count += 1
|
||||
})
|
||||
}
|
||||
|
||||
navigator.permissions.query({ name: 'clipboard-read' })
|
||||
.then( (permission) => {
|
||||
if( permission.state != 'granted' ){
|
||||
this.el.sceneEl.exitVR()
|
||||
setTimeout( () => paste(), 500 )
|
||||
return
|
||||
}else paste()
|
||||
})
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
textHeuristic: function(text){
|
||||
// Script type identification clues
|
||||
const bashClues = ["|", "if ", "fi", "cat"];
|
||||
const htmlClues = ["/>", "href=", "src="];
|
||||
const aframeClues = ["<a-entity", "/>", "position="];
|
||||
const jsClues = ["var ", "let ", "function ", "setTimeout","console."];
|
||||
// Count occurrences of clues for each script type
|
||||
const bashCount = bashClues.reduce((acc, clue) => acc + (text.includes(clue) ? 1 : 0), 0);
|
||||
const htmlCount = htmlClues.reduce((acc, clue) => acc + (text.includes(clue) ? 1 : 0), 0);
|
||||
const aframeCount = aframeClues.reduce((acc, clue) => acc + (text.includes(clue) ? 1 : 0), 0);
|
||||
const jsCount = jsClues.reduce((acc, clue) => acc + (text.includes(clue) ? 1 : 0), 0);
|
||||
|
||||
// Identify the script with the most clues or return unknown if inconclusive
|
||||
const maxCount = Math.max(bashCount, htmlCount, jsCount, aframeCount);
|
||||
if (maxCount === 0) {
|
||||
return "unknown";
|
||||
} else if (bashCount === maxCount) {
|
||||
return "bash";
|
||||
} else if (htmlCount === maxCount) {
|
||||
return "html";
|
||||
} else if (jsCount === maxCount) {
|
||||
return "javascript";
|
||||
} else {
|
||||
return "aframe";
|
||||
}
|
||||
},
|
||||
|
||||
insertAFRAME: function(data){
|
||||
let scene = document.createElement('a-entity')
|
||||
scene.id = "embedAframe"
|
||||
scene.innerHTML = data
|
||||
let el = document.createElement('a-text')
|
||||
el.setAttribute("value",data)
|
||||
el.setAttribute("color","white")
|
||||
el.setAttribute("align","center")
|
||||
el.setAttribute("anchor","align")
|
||||
let osbutton = this.wrapOSButton(el,"aframe",data)
|
||||
AFRAME.scenes[0].appendChild(osbutton)
|
||||
console.log(data)
|
||||
},
|
||||
|
||||
insertText: function(data){
|
||||
let el = document.createElement('a-text')
|
||||
el.setAttribute("value",data)
|
||||
el.setAttribute("color","white")
|
||||
el.setAttribute("align","center")
|
||||
el.setAttribute("anchor","align")
|
||||
let osbutton = this.wrapOSButton(el,"text",data)
|
||||
AFRAME.scenes[0].appendChild(osbutton)
|
||||
console.log(data)
|
||||
},
|
||||
|
||||
wrapOSButton: function(el,type,data){
|
||||
let osbutton = document.createElement('a-entity')
|
||||
let height = type == 'aframe' ? 0.3 : 0.1
|
||||
let depth = type == 'aframe' ? 0.3 : 0.05
|
||||
osbutton.setAttribute("osbutton",`width:0.3; height: ${height}; depth: ${depth}; color:blue `)
|
||||
osbutton.appendChild(el)
|
||||
osbutton.object3D.position.copy( this.getPositionInFrontOfCamera() )
|
||||
return osbutton
|
||||
},
|
||||
|
||||
getPositionInFrontOfCamera: function(distance){
|
||||
const camera = this.el.sceneEl.camera;
|
||||
let pos = new THREE.Vector3()
|
||||
let direction = new THREE.Vector3();
|
||||
// Get camera's forward direction (without rotation)
|
||||
camera.getWorldDirection(direction);
|
||||
camera.getWorldPosition(pos)
|
||||
direction.normalize();
|
||||
// Scale the direction by 1 meter
|
||||
if( !distance ) distance = 1.5
|
||||
direction.multiplyScalar(distance);
|
||||
// Add the camera's position to the scaled direction to get the target point
|
||||
pos.add(direction);
|
||||
return pos
|
||||
},
|
||||
|
||||
manifest: { // HTML5 manifest to identify app to xrsh
|
||||
"short_name": "Paste",
|
||||
"name": "Paste",
|
||||
"icons": [
|
||||
{
|
||||
"src": "https://css.gg/clipboard.svg",
|
||||
"type": "image/svg+xml",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"id": "/?source=pwa",
|
||||
"start_url": "/?source=pwa",
|
||||
"background_color": "#3367D6",
|
||||
"display": "standalone",
|
||||
"scope": "/",
|
||||
"theme_color": "#3367D6",
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "What is the latest news?",
|
||||
"cli":{
|
||||
"usage": "helloworld <type> [options]",
|
||||
"example": "helloworld news",
|
||||
"args":{
|
||||
"--latest": {type:"string"}
|
||||
}
|
||||
},
|
||||
"short_name": "Today",
|
||||
"description": "View weather information for today",
|
||||
"url": "/today?source=pwa",
|
||||
"icons": [{ "src": "/images/today.png", "sizes": "192x192" }]
|
||||
}
|
||||
],
|
||||
"description": "Paste the clipboard",
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "/images/screenshot1.png",
|
||||
"type": "image/png",
|
||||
"sizes": "540x720",
|
||||
"form_factor": "narrow"
|
||||
}
|
||||
],
|
||||
"help":`
|
||||
Helloworld application
|
||||
|
||||
This is a help file which describes the application.
|
||||
It will be rendered thru troika text, and will contain
|
||||
headers based on non-punctualized lines separated by linebreaks,
|
||||
in above's case "\nHelloworld application\n" will qualify as header.
|
||||
`
|
||||
}
|
||||
|
||||
});
|
||||
|
130
com/pastedrop.js
130
com/pastedrop.js
|
@ -1,130 +0,0 @@
|
|||
/**
|
||||
* ## [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"}
|
||||
},
|
||||
|
||||
init: function () {
|
||||
|
||||
window.addEventListener('paste', this.onPaste.bind(this) )
|
||||
|
||||
document.body.addEventListener('dragover',(e) => e.preventDefault() )
|
||||
document.body.addEventListener('drop', this.onDrop.bind(this) )
|
||||
},
|
||||
|
||||
initClipboard: function(){
|
||||
navigator.permissions.query({ name: 'clipboard-read' })
|
||||
.then( (permission) => {
|
||||
if( permission.state != 'granted' ){
|
||||
this.el.sceneEl.exitVR()
|
||||
setTimeout( () => this.paste(), 500 )
|
||||
return
|
||||
}else this.paste()
|
||||
})
|
||||
},
|
||||
|
||||
getClipboard: function(){
|
||||
navigator.clipboard.readText()
|
||||
.then( async (base64) => {
|
||||
let mimetype = base64.replace(/;base64,.*/,'')
|
||||
let data = base64.replace(/.*;base64,/,'')
|
||||
let type = this.textHeuristic(data)
|
||||
const term = document.querySelector('[isoterminal]').components.isoterminal.term
|
||||
this.el.emit('pasteFile',{}) /*TODO* data incompatible */
|
||||
})
|
||||
},
|
||||
|
||||
onDrop: function(e){
|
||||
e.preventDefault()
|
||||
this.onPaste({...e, type: "paste", clipboardData: e.dataTransfer})
|
||||
},
|
||||
|
||||
onPaste: function(e){
|
||||
if( e.type != "paste" ) return
|
||||
|
||||
const clipboardData = e.clipboardData || navigator.clipboard;
|
||||
const items = clipboardData.items;
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
const type = item.type;
|
||||
|
||||
// Check if the item is a file
|
||||
if (item.kind === "file") {
|
||||
this.el.emit('pasteFile',{item,type})
|
||||
} else if (type === "text/plain") {
|
||||
const pastedText = clipboardData.getData("text/plain");
|
||||
const newType = "text" // let /root/hook.d/mimetype/text further decide whether this is text/plain (or something else)
|
||||
this.el.emit('pasteFile',{item,type:newType,pastedText})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
manifest: { // HTML5 manifest to identify app to xrsh
|
||||
"short_name": "Paste",
|
||||
"name": "Paste",
|
||||
"icons": [
|
||||
{
|
||||
"src": "https://css.gg/clipboard.svg",
|
||||
"type": "image/svg+xml",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"id": "/?source=pwa",
|
||||
"start_url": "/?source=pwa",
|
||||
"background_color": "#3367D6",
|
||||
"display": "standalone",
|
||||
"scope": "/",
|
||||
"theme_color": "#3367D6",
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "What is the latest news?",
|
||||
"cli":{
|
||||
"usage": "helloworld <type> [options]",
|
||||
"example": "helloworld news",
|
||||
"args":{
|
||||
"--latest": {type:"string"}
|
||||
}
|
||||
},
|
||||
"short_name": "Today",
|
||||
"description": "View weather information for today",
|
||||
"url": "/today?source=pwa",
|
||||
"icons": [{ "src": "/images/today.png", "sizes": "192x192" }]
|
||||
}
|
||||
],
|
||||
"description": "Paste the clipboard",
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "/images/screenshot1.png",
|
||||
"type": "image/png",
|
||||
"sizes": "540x720",
|
||||
"form_factor": "narrow"
|
||||
}
|
||||
],
|
||||
"help":`
|
||||
Helloworld application
|
||||
|
||||
This is a help file which describes the application.
|
||||
It will be rendered thru troika text, and will contain
|
||||
headers based on non-punctualized lines separated by linebreaks,
|
||||
in above's case "\nHelloworld application\n" will qualify as header.
|
||||
`
|
||||
}
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* ## requestAnimationFrameXR
|
||||
*
|
||||
* reroutes requestAnimationFrame-calls to xrSession.requestAnimationFrame
|
||||
* reason: in immersive mode this function behaves differently
|
||||
* (causing HTML apps like xterm.js not getting updated due to relying
|
||||
* on window.requestAnimationFrame)
|
||||
*
|
||||
* ```html
|
||||
* <a-entity requestAnimationFrameXR dom/>
|
||||
* ```
|
||||
*/
|
||||
|
||||
if( !AFRAME.systems.requestAnimationFrameXR ){
|
||||
|
||||
AFRAME.registerSystem('requestAnimationFrameXR',{
|
||||
|
||||
init: function init(){
|
||||
if( document.location.hostname.match(/localhost/) ) return // allow webxr polyfill during development (they hang in XR)
|
||||
AFRAME.systems.requestAnimationFrameXR.q = []
|
||||
this.sceneEl.addEventListener('enter-vr', this.enable )
|
||||
this.sceneEl.addEventListener('enter-ar', this.enable )
|
||||
this.sceneEl.addEventListener('exit-vr', this.disable )
|
||||
this.sceneEl.addEventListener('exit-ar', this.disable )
|
||||
},
|
||||
|
||||
enable: function enable(){
|
||||
this.requestAnimationFrame = window.requestAnimationFrame
|
||||
// NOTE: we don't call xrSession.requestAnimationFrame directly like this:
|
||||
//
|
||||
// window.requestAnimationFrame = AFRAME.utils.throttleTick( (cb) => this.sceneEl.xrSession.requestAnimationFrame(cb), 50 )
|
||||
//
|
||||
// as that breaks webxr polyfill (for in-browser testing)
|
||||
// instead we defer calls to tick() (which is called both in XR and non-XR)
|
||||
//
|
||||
window.requestAnimationFrame = (cb) => AFRAME.systems.requestAnimationFrameXR.q.push(cb)
|
||||
const q = AFRAME.systems.requestAnimationFrameXR.q
|
||||
this.tick = AFRAME.utils.throttleTick( () => {
|
||||
while( q.length != 0 ) (q.pop())()
|
||||
},50)
|
||||
},
|
||||
|
||||
disable: function disable(){
|
||||
delete this.tick
|
||||
window.requestAnimationFrame = this.requestAnimationFrame
|
||||
}
|
||||
|
||||
|
||||
})
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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,33 +49,19 @@ 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
|
||||
|
||||
this.el.setAttribute("grabbable","")
|
||||
|
||||
if( this.el.object3D.position.x == 0 &&
|
||||
this.el.object3D.position.y == 0 &&
|
||||
this.el.object3D.position.z == 0 ){ // position next to previous window
|
||||
var els = [...document.querySelectorAll('[window]')]
|
||||
if( els.length < 2 ) return
|
||||
let current = els[ els.length-1 ]
|
||||
let last = els[ els.length-2 ]
|
||||
AFRAME.utils.positionObjectNextToNeighbor( current.object3D , last.object3D, 0.02 )
|
||||
}
|
||||
},
|
||||
|
||||
show: function(state){
|
||||
this.el.dom.closest('.winbox').style.display = state ? '' : 'none'
|
||||
}
|
||||
})
|
||||
|
||||
AFRAME.utils.positionObjectNextToNeighbor = function positionObjectNextToNeighbor(object, lastNeighbor = null, margin ){
|
||||
// *FIXME* this could be more sophisticated :)
|
||||
object.position.x = lastNeighbor.position.x + margin
|
||||
object.position.y = lastNeighbor.position.y - margin
|
||||
object.position.z = lastNeighbor.position.z + margin
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
AFRAME.registerComponent('xrfragments', {
|
||||
schema: {
|
||||
url: { type:"string"}
|
||||
},
|
||||
|
||||
init: function () {
|
||||
},
|
||||
|
||||
events:{
|
||||
|
||||
launcher: async function(){
|
||||
let url = prompt('enter URL to glb/fbx/json/obj/usdz asset', 'https://xrfragment.org/index.glb')
|
||||
if( !url ) return
|
||||
await AFRAME.utils.require({
|
||||
xrfragments: "https://xrfragment.org/dist/xrfragment.aframe.js",
|
||||
})
|
||||
|
||||
// remove objects which are marked to be removed from scene (with noxrf)
|
||||
let els = [...document.querySelectorAll('[noxrf]') ]
|
||||
els.map( (el) => el.remove() )
|
||||
|
||||
if( !this.el.getAttribute("xrf") ){
|
||||
this.el.setAttribute("xrf", url )
|
||||
let ARbutton = document.querySelector('.a-enter-ar-button')
|
||||
if( ARbutton ){
|
||||
ARbutton.addEventListener('click', () => {
|
||||
AFRAME.XRF.reset()
|
||||
})
|
||||
}
|
||||
}else AFRAME.XRF.navigator.to(url)
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
manifest: { // HTML5 manifest to identify app to xrsh
|
||||
"short_name": "XRF",
|
||||
"name": "XR Fragment URL",
|
||||
"icons": [ ],
|
||||
"id": "/?source=pwa",
|
||||
"start_url": "/?source=pwa",
|
||||
"background_color": "#3367D6",
|
||||
"display": "standalone",
|
||||
"scope": "/",
|
||||
"theme_color": "#3367D6",
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "What is the latest news?",
|
||||
"cli":{
|
||||
"usage": "helloworld <type> [options]",
|
||||
"example": "helloworld news",
|
||||
"args":{
|
||||
"--latest": {type:"string"}
|
||||
}
|
||||
},
|
||||
"short_name": "Today",
|
||||
"description": "View weather information for today",
|
||||
"url": "/today?source=pwa",
|
||||
"icons": [{ "src": "/images/today.png", "sizes": "192x192" }]
|
||||
}
|
||||
],
|
||||
"description": "Hello world information",
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "/images/screenshot1.png",
|
||||
"type": "image/png",
|
||||
"sizes": "540x720",
|
||||
"form_factor": "narrow"
|
||||
}
|
||||
],
|
||||
"help":`
|
||||
Helloworld application
|
||||
|
||||
This is a help file which describes the application.
|
||||
It will be rendered thru troika text, and will contain
|
||||
headers based on non-punctualized lines separated by linebreaks,
|
||||
in above's case "\nHelloworld application\n" will qualify as header.
|
||||
`
|
||||
}
|
||||
|
||||
});
|
||||
|
Loading…
Reference in New Issue