Compare commits
No commits in common. "main" and "feat/copy-paste-drag-drop" have entirely different histories.
main
...
feat/copy-
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,187 @@
|
|||
if( AFRAME.components.codemirror ) delete AFRAME.components.codemirror
|
||||
|
||||
AFRAME.registerComponent('codemirror', {
|
||||
schema: {
|
||||
file: { type:"string"},
|
||||
term: { type:"selector", default: "[isoterminal]" },
|
||||
width: { type:"number", default:700},
|
||||
height: { type:"number", default:500},
|
||||
},
|
||||
|
||||
init: function () {
|
||||
this.el.object3D.visible = false
|
||||
if( !this.data.term || !this.data.term.components ) throw 'codemirror cannot get isoterminal'
|
||||
if( this.data.file && this.data.file[0] != '/'){
|
||||
this.data.file = "root/"+this.data.file
|
||||
}
|
||||
this.isoterminal = this.data.term.components.isoterminal.term
|
||||
//this.el.innerHTML = ` `
|
||||
this.requireAll()
|
||||
},
|
||||
|
||||
requireAll: async function(){
|
||||
let s = await AFRAME.utils.require(this.requires)
|
||||
setTimeout( () => this.el.setAttribute("dom",""), 300 )
|
||||
},
|
||||
|
||||
requires:{
|
||||
window: "com/window.js"
|
||||
},
|
||||
|
||||
dom: {
|
||||
scale: 0.5,
|
||||
events: ['click','keydown'],
|
||||
html: (me) => `<div class="codemirror">
|
||||
</div>`,
|
||||
|
||||
css: (me) => `.CodeMirror{
|
||||
width: ${me.com.data.width}px !important;
|
||||
height: ${me.com.data.height-30}px !important;
|
||||
}
|
||||
.codemirror *{
|
||||
font-size: 14px;
|
||||
font-family: "Cousine",Liberation Mono,DejaVu Sans Mono,Courier New,monospace;
|
||||
font-weight:500 !important;
|
||||
letter-spacing: 0 !important;
|
||||
text-shadow: 0px 0px 10px #F075;
|
||||
}
|
||||
|
||||
.wb-body:has(> .codemirror){
|
||||
overflow:hidden;
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
margin-top:18px;
|
||||
}
|
||||
.cm-s-shadowfox.CodeMirror {
|
||||
background:transparent !important;
|
||||
}
|
||||
`
|
||||
},
|
||||
|
||||
createEditor: function(value){
|
||||
this.el.setAttribute("window", `title: codemirror; uid: ${this.el.dom.id}; attach: #overlay; dom: #${this.el.dom.id}; width: ${this.data.width}px; height: ${this.data.height}px`)
|
||||
this.editor = CodeMirror( this.el.dom, {
|
||||
value,
|
||||
mode: "htmlmixed",
|
||||
lineNumbers: true,
|
||||
styleActiveLine: true,
|
||||
matchBrackets: true,
|
||||
Tab: "indentMore",
|
||||
defaultTab: function(cm) {
|
||||
if (cm.somethingSelected()) cm.indentSelection("add");
|
||||
else cm.replaceSelection(" ", "end");
|
||||
}
|
||||
})
|
||||
this.editor.setOption("theme", "shadowfox")
|
||||
this.editor.updateFile = AFRAME.utils.throttle( (file,str) => {
|
||||
this.updateFile(file,str)
|
||||
}, 1500)
|
||||
this.editor.on('change', (instance,changeObj) => {
|
||||
this.editor.updateFile( this.data.file, instance.getValue() )
|
||||
})
|
||||
|
||||
this
|
||||
.handleFocus()
|
||||
|
||||
setTimeout( () => {
|
||||
this.el.setAttribute("html-as-texture-in-xr", `domid: #${this.el.dom.id}`) // only show aframe-html in xr
|
||||
},1500)
|
||||
},
|
||||
|
||||
handleFocus: function(){
|
||||
const focus = (showdom) => (e) => {
|
||||
if( this.editor ){
|
||||
this.editor.focus()
|
||||
}
|
||||
if( this.el.components.window && this.data.renderer == 'canvas'){
|
||||
this.el.components.window.show( showdom )
|
||||
}
|
||||
}
|
||||
this.el.addEventListener('obbcollisionstarted', focus(false) )
|
||||
this.el.sceneEl.addEventListener('enter-vr', focus(false) )
|
||||
this.el.sceneEl.addEventListener('enter-ar', focus(false) )
|
||||
this.el.sceneEl.addEventListener('exit-vr', focus(true) )
|
||||
this.el.sceneEl.addEventListener('exit-ar', focus(true) )
|
||||
},
|
||||
|
||||
updateFile: async function(file,str){
|
||||
// we don't do via shellcmd: isoterminal.exec(`echo '${str}' > ${file}`,1)
|
||||
// as it would require all kindof ugly stringescaping
|
||||
console.log("updating "+file)
|
||||
await this.isoterminal.worker.update_file(file, this.isoterminal.convert.toUint8Array(str) )
|
||||
this.isoterminal.exec("touch "+file) // *FIXME* notify filesystem (why does inotifyd need this? v86's 9pfees is cached?)
|
||||
},
|
||||
|
||||
events:{
|
||||
|
||||
// component events
|
||||
DOMready: function(e){
|
||||
|
||||
this.isoterminal.worker.read_file(this.data.file)
|
||||
.then( this.isoterminal.convert.Uint8ArrayToString )
|
||||
.then( (str) => {
|
||||
console.log("creating editor")
|
||||
this.createEditor( str )
|
||||
})
|
||||
.catch( (e) => {
|
||||
console.log("error opening "+this.data.file+", creating new one")
|
||||
this.createEditor("")
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
manifest: { // HTML5 manifest to identify app to xrsh
|
||||
"short_name": "Paste",
|
||||
"name": "Paste",
|
||||
"icons": [
|
||||
{
|
||||
"src": "https://css.gg/clipboard.svg",
|
||||
"type": "image/svg+xml",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"id": "/?source=pwa",
|
||||
"start_url": "/?source=pwa",
|
||||
"background_color": "#3367D6",
|
||||
"display": "standalone",
|
||||
"scope": "/",
|
||||
"theme_color": "#3367D6",
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "What is the latest news?",
|
||||
"cli":{
|
||||
"usage": "helloworld <type> [options]",
|
||||
"example": "helloworld news",
|
||||
"args":{
|
||||
"--latest": {type:"string"}
|
||||
}
|
||||
},
|
||||
"short_name": "Today",
|
||||
"description": "View weather information for today",
|
||||
"url": "/today?source=pwa",
|
||||
"icons": [{ "src": "/images/today.png", "sizes": "192x192" }]
|
||||
}
|
||||
],
|
||||
"description": "Paste the clipboard",
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "/images/screenshot1.png",
|
||||
"type": "image/png",
|
||||
"sizes": "540x720",
|
||||
"form_factor": "narrow"
|
||||
}
|
||||
],
|
||||
"help":`
|
||||
Helloworld application
|
||||
|
||||
This is a help file which describes the application.
|
||||
It will be rendered thru troika text, and will contain
|
||||
headers based on non-punctualized lines separated by linebreaks,
|
||||
in above's case "\nHelloworld application\n" will qualify as header.
|
||||
`
|
||||
}
|
||||
|
||||
});
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* ## [data_events](com/data_events.js)
|
||||
/*
|
||||
* ## data_events
|
||||
*
|
||||
* allows components to react to data changes
|
||||
*
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -1,35 +1,3 @@
|
|||
/**
|
||||
* ## [html-as-texture-in-xr](com/html-as-texture-in-xr.js)
|
||||
*
|
||||
* shows domid **only** in immersive mode
|
||||
* (wrapper around [aframe-htmlmesh](https://ada.is/aframe-htmlmesh/)
|
||||
*
|
||||
* It also sets class 'XR' to the (HTML) body-element in immersive mode.
|
||||
* This allows CSS (in [dom component](com/dom.js)) to visually update accordingly.
|
||||
*
|
||||
* > depends on [AFRAME.utils.require](com/require.js)
|
||||
*
|
||||
* ```html
|
||||
* <style type="text/css">
|
||||
* .XR #foo { color:red; }
|
||||
* </style>
|
||||
*
|
||||
* <a-entity html-as-texture-in-xr="domid: #foo">
|
||||
* <b id="foo">hello</b>
|
||||
* </a-entitiy>
|
||||
* ```
|
||||
*
|
||||
* | property | type |
|
||||
* |--------------|--------------------|
|
||||
* | `domid` | `string` |
|
||||
*
|
||||
* | event | target | info |
|
||||
* |--------------|------------|--------------------------------------|
|
||||
* | `3D` | a-scene | fired when going into immersive mode |
|
||||
* | `2D` | a-scene | fired when leaving immersive mode |
|
||||
*
|
||||
*/
|
||||
|
||||
if( !AFRAME.components['html-as-texture-in-xr'] ){
|
||||
|
||||
AFRAME.registerComponent('html-as-texture-in-xr', {
|
||||
|
|
|
@ -1,68 +1,32 @@
|
|||
/**
|
||||
* ## [isoterminal](com/isoterminal.js)
|
||||
/*
|
||||
*
|
||||
* Renders a windowed terminal in both (non)immersive mode.
|
||||
* It displays an interactive javascript console or boots into
|
||||
* a Linux ISO image (via WASM).
|
||||
*
|
||||
* ```html
|
||||
* <a-entity isoterminal="iso: xrsh.iso" position="0 1.6 -0.3"></a-entity>
|
||||
* ```
|
||||
*
|
||||
* > depends on [AFRAME.utils.require](com/require.js)
|
||||
*
|
||||
* | property | type | default | info |
|
||||
* |------------------|-----------|------------------------|------|
|
||||
* | `iso` | `string` | https`//forgejo.isvery.ninja/assets/xrsh-buildroot/main/xrsh.iso" | |
|
||||
* | `overlayfs` | `string` | *WORK-IN-PROGRESS* | |
|
||||
* | `width` | `number` | 800 ||
|
||||
* | `height` | `number` | 600 ||
|
||||
* | `depth` | `number` | 0.03 ||
|
||||
* | `lineHeight` | `number` | 18 ||
|
||||
* | `prompt` | `boolean` | true | boot straight into ISO or give user choice |
|
||||
* | `padding` | `number`` | 18 | |
|
||||
* | `maximized` | `boolean` | false | |
|
||||
* | `minimized` | `boolean` | false | |
|
||||
* | `muteUntilPrompt`| `boolean` | true | mute stdout until a prompt is detected in ISO |
|
||||
* | `HUD` | `boolean` | false | link to camera movement |
|
||||
* | `transparent` | `boolean` | false | heavy, needs good gpu |
|
||||
* | `memory` | `number` | 60 | VM memory (in MB) [NOTE` quest or smartphone webworker might crash > 40mb ] |
|
||||
* | `bufferLatency` | `number` | 1 | in ms` bufferlatency from webworker to term (batch-update every char to texture) |
|
||||
* | `debug` | `boolean` | false | |
|
||||
* | `emulator` | `string` | fbterm | terminal emulator |
|
||||
*
|
||||
* > for more info see [xrsh.isvery.ninja](https://xrsh.isvery.ninja)
|
||||
*
|
||||
* Component design:
|
||||
* ```
|
||||
* css/html template
|
||||
*
|
||||
* ┌─────────┐ ┌────────────┐ exit-AR
|
||||
* ┌───────►│ com/dom ┼──►│ com/window ├───────────────── exit-VR ◄─┐
|
||||
* │ └─────────┘ └───────────┬┘ │
|
||||
* │ │ │
|
||||
* ┌──────────┴────────┐ │ ┌───────────┐ ┌─────────────────────────────┐
|
||||
* │ com/isoterminal ├────────────────────────────►│com/term.js│ │com/html-as-texture-in-XR.js │
|
||||
* └────────┬─┬────────┘ │ └──┬─────┬▲─┘ └─────────────────────────────┘
|
||||
* │ │ ┌────────┐ ┌──▼──────▼──────┐ ││ │
|
||||
* │ └───────►│ plane ├─────►text───┼►div#isoterminal│◄────────────────── enter-VR │
|
||||
* │ └────────┘ └────────────────┘ enter-AR ◄─┘
|
||||
* │ │
|
||||
* │ │
|
||||
* │ ISOTerminal.js
|
||||
* │ ┌───────────────────────────┐
|
||||
* │ │ com/isoterminal/worker.js ├
|
||||
* │ └──────────────┌────────────┤
|
||||
* │ │ │ v86.js │
|
||||
* │ │ │ feat/*.js │
|
||||
* │ │ │ libv86.js │
|
||||
* │ │ └────────────┘
|
||||
* │ │
|
||||
* └─────────────────────┘
|
||||
* ┌─────────┐ ┌────────────┐ ┌─────────────┐ exit-AR
|
||||
* ┌───────►│ com/dom ┼──►│ com/window ├─►│ domrenderer │◄────────── exit-VR ◄─┐
|
||||
* │ └─────────┘ └────────────┘ └─────▲───────┘ │
|
||||
* │ │ ┌───────────────┐ │
|
||||
* ┌──────────┴────────┐ ┌─────┴──────┐ │ xterm.js │ ┌─────────────────────────────┐
|
||||
* │ com/isoterminal ├────────────────────────────►│com/xterm.js│◄─┤ │ │com/html-as-texture-in-XR.js │
|
||||
* └────────┬─┬────────┘ └──┬──────┬▲─┘ │ xterm.css │ └─────────────────────────────┘
|
||||
* │ │ ┌────────┐ ┌─────────▼──┐ ││ └───────────────┘ │ ▲
|
||||
* │ └───────►│ plane ├─────►text───┼►canvas │◄────────────────── enter-VR │ │
|
||||
* │ └────────┘ └────────────┘ ││ enter-AR ◄─┘ │
|
||||
* │ renderer=canvas ││ │
|
||||
* │ ││ │
|
||||
* │ ISOTerminal.js ││ │
|
||||
* │ ┌───────────────────────────┐◄────┘│ │
|
||||
* │ │ com/isoterminal/worker.js ├──────┘ │
|
||||
* │ └──────────────┌────────────┤ │
|
||||
* │ │ v86.js │ │
|
||||
* │ │ feat/*.js │ │
|
||||
* │ │ libv86.js │ │
|
||||
* │ └────────────┘ │
|
||||
* │ │
|
||||
* └────────────────────────────────────────────────────────────────────────────────────┘
|
||||
*
|
||||
* NOTE: For convenience reasons, events are forwarded between com/isoterminal.js, worker.js and ISOTerminal
|
||||
* Instead of a melting pot of different functionnames, events are flowing through everything (ISOTerminal.emit())
|
||||
* ```
|
||||
*/
|
||||
|
||||
if( typeof AFRAME != 'undefined '){
|
||||
|
@ -71,31 +35,26 @@ if( typeof AFRAME != 'undefined '){
|
|||
schema: {
|
||||
iso: { type:"string", "default":"https://forgejo.isvery.ninja/assets/xrsh-buildroot/main/xrsh.iso" },
|
||||
overlayfs: { type:"string"},
|
||||
width: { type: 'number',"default": 800 },
|
||||
height: { type: 'number',"default": 600 },
|
||||
width: { type: 'number',"default": -1 },
|
||||
height: { type: 'number',"default": -1 },
|
||||
depth: { type: 'number',"default": 0.03 },
|
||||
lineHeight: { type: 'number',"default": 18 },
|
||||
prompt: { type: 'boolean', "default": true }, // boot straight into ISO or give user choice
|
||||
padding: { type: 'number',"default": 18 },
|
||||
maximized: { type: 'boolean',"default":false},
|
||||
minimized: { type: 'boolean',"default":false},
|
||||
maximized: { type: 'boolean',"default":true},
|
||||
muteUntilPrompt:{ type: 'boolean',"default":true}, // mute stdout until a prompt is detected in ISO
|
||||
HUD: { type: 'boolean',"default":false}, // link to camera movement
|
||||
transparent: { type:'boolean', "default":false }, // need good gpu
|
||||
memory: { type: 'number', "default":60 }, // VM memory (in MB) [NOTE: quest or smartphone might crash > 40mb ]
|
||||
bufferLatency: { type: 'number', "default":1 }, // in ms: bufferlatency from webworker to xterm (batch-update every char to texture)
|
||||
debug: { type: 'boolean', "default":false },
|
||||
emulator: { type: 'string', "default": "fbterm" }// terminal emulator
|
||||
memory: { type: 'number', "default":40 }, // VM memory (in MB) [NOTE: quest or smartphone might crash > 40mb ]
|
||||
bufferLatency: { type: 'number', "default":1 }, // in ms: bufferlatency from webworker to xterm (batch-update every char to texture)
|
||||
debug: { type: 'boolean', "default":false }
|
||||
},
|
||||
|
||||
init: function(){
|
||||
this.el.object3D.visible = false
|
||||
if( window.innerWidth < this.data.width ){
|
||||
this.data.maximized = true
|
||||
}
|
||||
|
||||
this.calculateDimension()
|
||||
this.initHud()
|
||||
this.setupBox()
|
||||
this.setupPasteDrop()
|
||||
|
||||
fetch(this.data.iso,{method: 'HEAD'})
|
||||
|
@ -115,6 +74,7 @@ if( typeof AFRAME != 'undefined '){
|
|||
window: "com/window.js",
|
||||
pastedrop: "com/pastedrop.js",
|
||||
v86: "com/isoterminal/libv86.js",
|
||||
vt100: "com/isoterminal/VT100.js",
|
||||
// allow xrsh to selfcontain scene + itself
|
||||
xhook: "com/lib/xhook.min.js",
|
||||
selfcontain: "com/selfcontainer.js",
|
||||
|
@ -130,18 +90,24 @@ if( typeof AFRAME != 'undefined '){
|
|||
scale: 0.66,
|
||||
events: ['click','keydown'],
|
||||
html: (me) => `<div class="isoterminal">
|
||||
<input type="file" id="pastedrop" style="position:absolute; left:-9999px;opacity:0"></input>
|
||||
<div id="term" tabindex="0"></div>
|
||||
<div id="term" tabindex="0">
|
||||
<pre></pre>
|
||||
</div>
|
||||
</div>`,
|
||||
|
||||
css: (me) => `
|
||||
|
||||
.isoterminal{
|
||||
css: (me) => `.isoterminal{
|
||||
padding: ${me.com.data.padding}px;
|
||||
width:100%;
|
||||
height:99%;
|
||||
resize: both;
|
||||
overflow: hidden;
|
||||
height:90%;
|
||||
position:relative;
|
||||
}
|
||||
.isoterminal div{
|
||||
display:block;
|
||||
position:relative;
|
||||
line-height: ${me.com.data.lineHeight}px;
|
||||
}
|
||||
#term {
|
||||
outline: none !important;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Cousine';
|
||||
|
@ -156,73 +122,6 @@ if( typeof AFRAME != 'undefined '){
|
|||
src: url(./com/isoterminal/assets/CousineBold.ttf) format('truetype');
|
||||
}
|
||||
|
||||
.isoterminal *{
|
||||
outline:none;
|
||||
box-shadow:none;
|
||||
}
|
||||
|
||||
.term {
|
||||
font-family: 'Cousine';
|
||||
line-height: ${me.com.data.lineHeight}px;
|
||||
font-weight: normal;
|
||||
font-variant-ligatures: none;
|
||||
color: #f0f0f0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.term_content a {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
color:#2AFF;
|
||||
}
|
||||
|
||||
.term_content a span{
|
||||
text-shadow: 0px 0px 10px #F07A;
|
||||
}
|
||||
|
||||
.term_content a:hover {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
animation:fade 1000ms infinite;
|
||||
-webkit-animation:fade 1000ms infinite;
|
||||
}
|
||||
|
||||
.term_cursor {
|
||||
color: #000000;
|
||||
background: #70f;
|
||||
animation:fade 1000ms infinite;
|
||||
-webkit-animation:fade 1000ms infinite;
|
||||
}
|
||||
|
||||
.term_char_size {
|
||||
display: inline-block;
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: -1000px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.term_textarea {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
padding: 0px;
|
||||
border: 0px;
|
||||
margin: 0px;
|
||||
opacity: 0;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.term_scrollbar { background: transparent url(images/bg-scrollbar-track-y.png) no-repeat 0 0; position: relative; background-position: 0 0; float: right; height: 100%; }
|
||||
.term_track { background: transparent url(images/bg-scrollbar-trackend-y.png) no-repeat 0 100%; height: 100%; width:13px; position: relative; padding: 0 1px; }
|
||||
.term_thumb { background: transparent url(images/bg-scrollbar-thumb-y.png) no-repeat 50% 100%; height: 20px; width: 25px; cursor: pointer; overflow: hidden; position: absolute; top: 0; left: -5px; }
|
||||
.term_thumb .term_end { background: transparent url(images/bg-scrollbar-thumb-y.png) no-repeat 50% 0; overflow: hidden; height: 5px; width: 25px; }
|
||||
.noSelect { user-select: none; -o-user-select: none; -moz-user-select: none; -khtml-user-select: none; -webkit-user-select: none; }
|
||||
|
||||
.isoterminal style{ display:none }
|
||||
|
||||
blink{
|
||||
|
@ -235,6 +134,12 @@ if( typeof AFRAME != 'undefined '){
|
|||
box-shadow:none;
|
||||
}
|
||||
|
||||
.cursor {
|
||||
background: #70F !important;
|
||||
animation:fade 1000ms infinite;
|
||||
-webkit-animation:fade 1000ms infinite;
|
||||
}
|
||||
|
||||
.XR .cursor {
|
||||
animation:none;
|
||||
-webkit-animation:none;
|
||||
|
@ -290,10 +195,9 @@ if( typeof AFRAME != 'undefined '){
|
|||
// * heavily dependent on requestAnimationFrame (conflicts with THREE)
|
||||
// * typescript-rewrite results in ~300k lib (instead of 96k)
|
||||
// * v3.12 had slightly better performance but still very heavy
|
||||
//
|
||||
await AFRAME.utils.require(this.requires)
|
||||
|
||||
let features = { // ISOTerminal plugins
|
||||
await AFRAME.utils.require(this.requires)
|
||||
await AFRAME.utils.require({ // ISOTerminal plugins
|
||||
boot: "com/isoterminal/feat/boot.js",
|
||||
javascript: "com/isoterminal/feat/javascript.js",
|
||||
jsconsole: "com/isoterminal/feat/jsconsole.js",
|
||||
|
@ -301,13 +205,7 @@ if( typeof AFRAME != 'undefined '){
|
|||
indexjs: "com/isoterminal/feat/index.js.js",
|
||||
autorestore: "com/isoterminal/feat/autorestore.js",
|
||||
pastedropFeat: "com/isoterminal/feat/pastedrop.js",
|
||||
httpfs: "com/isoterminal/feat/httpfs.js",
|
||||
}
|
||||
if( this.data.emulator == 'fbterm' ){
|
||||
features['fbtermjs'] = "com/isoterminal/term.js"
|
||||
features['fbterm'] = "com/isoterminal/feat/term.js"
|
||||
}
|
||||
await AFRAME.utils.require(features)
|
||||
})
|
||||
|
||||
this.el.setAttribute("selfcontainer","")
|
||||
|
||||
|
@ -329,16 +227,17 @@ if( typeof AFRAME != 'undefined '){
|
|||
this.term = new ISOTerminal(instance,this.data)
|
||||
|
||||
instance.addEventListener('DOMready', () => {
|
||||
this.term.emit('term_init', {instance, aEntity:this})
|
||||
this.setupVT100(instance)
|
||||
setTimeout( () => {
|
||||
instance.setAttribute("html-as-texture-in-xr", `domid: #term; faceuser: true`)
|
||||
},100)
|
||||
//instance.winbox.resize(720,380)
|
||||
let size = `width: ${this.data.width}; height: ${this.data.height}`
|
||||
instance.setAttribute("window", `title: xrsh.iso; uid: ${instance.uid}; attach: #overlay; dom: #${instance.dom.id}; ${size}; min: ${this.data.minimized}; max: ${this.data.maximized}; class: no-full, no-max, no-resize`)
|
||||
instance.setAttribute("window", `title: xrsh.iso; uid: ${instance.uid}; attach: #overlay; dom: #${instance.dom.id}; ${size}; min: ${this.data.minimized}; max: ${this.data.maximized}`)
|
||||
})
|
||||
|
||||
instance.addEventListener('window.oncreate', (e) => {
|
||||
instance.dom.classList.add('blink')
|
||||
// canvas to texture texture
|
||||
instance.setAttribute("html-as-texture-in-xr", `domid: .winbox#${instance.uid}; faceuser: true`)
|
||||
|
||||
// run iso
|
||||
let opts = {dom:instance.dom}
|
||||
|
@ -351,12 +250,6 @@ if( typeof AFRAME != 'undefined '){
|
|||
instance.setAttribute("dom", "")
|
||||
instance.setAttribute("pastedrop", "")
|
||||
|
||||
|
||||
// *REMOVE* make a boot-plugin mechanism in feat/term.js
|
||||
this.term.addEventListener('enable-console', () => {
|
||||
instance.dom.classList.remove('blink')
|
||||
})
|
||||
|
||||
this.term.addEventListener('ready', (e) => {
|
||||
instance.dom.classList.remove('blink')
|
||||
this.term.emit('status',"running")
|
||||
|
@ -411,6 +304,46 @@ if( typeof AFRAME != 'undefined '){
|
|||
console.test.run()
|
||||
},
|
||||
|
||||
setupVT100: function(instance){
|
||||
const el = this.el.dom.querySelector('#term')
|
||||
this.term.opts.vt100 = {
|
||||
cols: this.cols,
|
||||
rows: this.rows,
|
||||
el_or_id: el,
|
||||
max_scroll_lines: this.rows,
|
||||
nodim: true,
|
||||
rainbow: [VT100.COLOR_MAGENTA, VT100.COLOR_CYAN ],
|
||||
xr: AFRAME.scenes[0].renderer.xr
|
||||
}
|
||||
this.term.emit('initVT100',this)
|
||||
this.vt100 = new VT100( this.term.opts.vt100 )
|
||||
this.vt100.el = el
|
||||
this.vt100.curs_set( 1, true)
|
||||
this.vt100.focus()
|
||||
this.el.addEventListener('focus', () => this.vt100.focus() )
|
||||
this.vt100.getch( (ch,t) => {
|
||||
this.term.send( ch )
|
||||
})
|
||||
|
||||
this.el.addEventListener('serial-output-byte', (e) => {
|
||||
const byte = e.detail
|
||||
var chr = String.fromCharCode(byte);
|
||||
this.vt100.addchr(chr)
|
||||
})
|
||||
this.el.addEventListener('serial-output-string', (e) => {
|
||||
this.vt100.write(e.detail)
|
||||
})
|
||||
|
||||
// translate file upload into pasteFile
|
||||
this.vt100.upload.addEventListener('change', (e) => {
|
||||
const file = this.vt100.upload.files[0];
|
||||
const item = {...file, getAsFile: () => file }
|
||||
this.el.emit('pasteFile', { item, type: file.type });
|
||||
})
|
||||
|
||||
return this
|
||||
},
|
||||
|
||||
setupPasteDrop: function(){
|
||||
this.el.addEventListener('pasteFile', (e) => {
|
||||
e.preventDefault() // prevent bubbling up to window (which is triggering this initially)
|
||||
|
@ -420,14 +353,25 @@ if( typeof AFRAME != 'undefined '){
|
|||
return this
|
||||
},
|
||||
|
||||
setupBox: function(){
|
||||
// setup slightly bigger black backdrop (this.el.getObject3D("mesh"))
|
||||
const w = this.data.width/950;
|
||||
const h = this.data.height/950;
|
||||
this.el.box = document.createElement('a-entity')
|
||||
this.el.box.setAttribute("geometry",`primitive: box; width:${w}; height:${h}; depth: -${this.data.depth}`)
|
||||
this.el.box.setAttribute("material","shader:flat; color:black; opacity:0.9; transparent:true; ")
|
||||
this.el.box.setAttribute("position",`0 0 ${(this.data.depth/2)-0.001}`)
|
||||
this.el.appendChild(this.el.box)
|
||||
},
|
||||
|
||||
calculateDimension: function(){
|
||||
if( this.data.width == -1 ) this.data.width = document.body.offsetWidth;
|
||||
if( this.data.height == -1 ) this.data.height = Math.floor( document.body.offsetHeight - 30 )
|
||||
if( this.data.height > this.data.width ) this.data.height = this.data.width // mobile smartphone fix
|
||||
this.data.width -= this.data.padding*2
|
||||
this.data.height -= this.data.padding*2
|
||||
this.cols = Math.floor(this.data.width/this.data.lineHeight*2)-1
|
||||
this.rows = Math.floor( (this.data.height*0.93)/this.data.lineHeight)-1
|
||||
this.cols = Math.floor(this.data.width/this.data.lineHeight*2)
|
||||
this.rows = Math.floor(this.data.height*0.53/this.data.lineHeight*1.7)
|
||||
},
|
||||
|
||||
events:{
|
||||
|
|
|
@ -34,12 +34,11 @@ ISOTerminal.addEventListener = (event,cb) => {
|
|||
}
|
||||
|
||||
ISOTerminal.prototype.exec = function(shellscript){
|
||||
this.send(`printf "\n\r"; ${shellscript}\n`,1)
|
||||
this.send(shellscript+"\n",1)
|
||||
}
|
||||
|
||||
ISOTerminal.prototype.hook = function(hookname,args){
|
||||
let cmd = `{ type hook || source /etc/profile.sh; }; hook ${hookname} "${args.join('" "')}"`
|
||||
this.exec(cmd)
|
||||
this.exec(`{ type hook || source /etc/profile.sh; }; hook ${hookname} "${args.join('" "')}"`)
|
||||
}
|
||||
|
||||
ISOTerminal.prototype.serial_input = 0; // can be set to 0,1,2,3 to define stdinput tty (xterm plugin)
|
||||
|
@ -54,7 +53,7 @@ ISOTerminal.prototype.send = function(str, ttyNr){
|
|||
this.convert.toUint8Array( str ).map( (c) => {
|
||||
this.preventFrameDrop(
|
||||
() => {
|
||||
this.worker.postMessage({event:`serial${ttyNr}-input`,data:c})
|
||||
this.worker.postMessage({event:`serial${ttyNr}-input`,data:c})
|
||||
}
|
||||
)
|
||||
})
|
||||
|
@ -124,11 +123,8 @@ ISOTerminal.prototype.start = function(opts){
|
|||
url: "bios/vgabios.bin",
|
||||
//urg|: "com/isoterminal/bios/VGABIOS-lgpl-latest.bin",
|
||||
},
|
||||
network_relay_url: "wss://relay.widgetry.org/",
|
||||
cmdline: "rw root=host9p rootfstype=9p rootflags=trans=virtio,cache=loose modules=virtio_pci tsc=reliable init_on_freg|=on vga=ask", //vga=0x122",
|
||||
net_device:{
|
||||
relay_url:"fetch", // or websocket proxy "wss://relay.widgetry.org/",
|
||||
type:"virtio"
|
||||
},
|
||||
//bzimage_initrd_from_filesystem: true,
|
||||
//filesystem: {
|
||||
// baseurl: "com/isoterminal/v86/images/alpine-rootfs-flat",
|
||||
|
@ -138,7 +134,6 @@ ISOTerminal.prototype.start = function(opts){
|
|||
//disable_jit: false,
|
||||
filesystem: {},
|
||||
autostart: true,
|
||||
prompt: this.opts.prompt,
|
||||
debug: this.opts.debug ? true : false
|
||||
};
|
||||
|
||||
|
@ -161,8 +156,10 @@ ISOTerminal.prototype.setupWorker = function(opts){
|
|||
return this
|
||||
}
|
||||
|
||||
ISOTerminal.prototype.getLoaderMsg = function(){
|
||||
|
||||
ISOTerminal.prototype.startVM = function(opts){
|
||||
|
||||
this.emit('runISO',{...opts, bufferLatency: this.opts.bufferLatency })
|
||||
const loading = [
|
||||
'loading quantum bits and bytes',
|
||||
'preparing quantum flux capacitors',
|
||||
|
@ -189,14 +186,15 @@ ISOTerminal.prototype.getLoaderMsg = function(){
|
|||
|
||||
const empower = [
|
||||
"FOSS gives users control over their software, offering freedom to modify and share",
|
||||
"Feeling powerless? FOSS escapes a mindset known as learned helplessness",
|
||||
"Feeling powerless with tech? FOSS escapes a mindset known as learned helplessness",
|
||||
"FOSS breaks this cycle by showing that anyone can learn and contribute",
|
||||
"Proprietary software can make users dependent, but FOSS offers real choices",
|
||||
"FOSS communities provide support and encourage users to develop new skills",
|
||||
"Learned helplessness fades when we realize tech isn’t too complex to understand",
|
||||
"FOSS empowers users to customize and improve their tools",
|
||||
"Engaging with FOSS helps build confidence and self-reliance in tech",
|
||||
"FOSS shows that anyone can shape the digital world with curiosity and effort",
|
||||
"Linux can revive old computers, extending their life and reduces e-waste",
|
||||
"Linux can revive old computers, extending their life and reducing e-waste",
|
||||
"Many lightweight Linux distributions run smoothly on older hardware",
|
||||
"Installing Linux on aging devices keeps them functional instead of sending them to the landfill",
|
||||
"Linux uses fewer resources, making it ideal for reusing older machines",
|
||||
|
@ -208,39 +206,32 @@ ISOTerminal.prototype.getLoaderMsg = function(){
|
|||
"Switching to Linux promotes sustainability by reducing demand for new gadgets and lowering e-waste"
|
||||
]
|
||||
|
||||
let motd = `
|
||||
const motd = `
|
||||
\r[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 ▬▬▬▬▬▬▬▬ https://xrsh.isvery.ninja ▬▬▬▬▬▬▬▬▬▬▬▬
|
||||
\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`
|
||||
\r
|
||||
\r credits
|
||||
\r -------
|
||||
\r @nlnet@nlnet.nl
|
||||
\r @lvk@mastodon.online
|
||||
\r @utopiah@mastodon.pirateparty.be
|
||||
\r https://www.w3.org/TR/webxr
|
||||
\r https://three.org
|
||||
\r https://aframe.org
|
||||
`
|
||||
|
||||
const text_color = "\r[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
|
||||
const loadmsg = "\n\r "+loading[ Math.floor(Math.random()*1000) % loading.length ] + "..[please wait]"
|
||||
const empowermsg = "\n\r "+text_reset+'"'+empower[ Math.floor(Math.random()*1000) % empower.length ] + '"\n\r'
|
||||
this.emit('status',loadmsg)
|
||||
this.emit('serial-output-string', motd + empowermsg + text_color + loadmsg + text_reset+"\n\r")
|
||||
|
||||
this.addEventListener('emulator-started', async (e) => {
|
||||
|
||||
|
@ -261,34 +252,12 @@ ISOTerminal.prototype.startVM = function(opts){
|
|||
const str = e.detail
|
||||
|
||||
// lets scan for a prompt so we can send a 'ready' event to the world
|
||||
if( !this.ready && str.match(/\n(\/ #|~ #|~%|\[.*\]>)/) ) this.postBoot()
|
||||
if( !this.ready && str.match(/\n(\/ #|~%|\[.*\]>)/) ) this.postBoot()
|
||||
|
||||
if( this.ready || !this.opts.muteUntilPrompt ) this.emit('serial-output-string', e.detail )
|
||||
})
|
||||
});
|
||||
|
||||
let msglib = this.getLoaderMsg()
|
||||
let msg = msglib.motd
|
||||
|
||||
if( this.opts.prompt ){
|
||||
msg += `\r
|
||||
\r[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 })
|
||||
}
|
||||
|
||||
|
||||
|
@ -316,27 +285,6 @@ ISOTerminal.prototype.bufferOutput = function(byte,cb,latency){
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
//ISOTerminal.prototype.bufferOutput = function(byte, cb, latency, buffer) {
|
||||
// const str = String.fromCharCode(byte);
|
||||
// //if (str === '\r' || str === '\n' || str.charCodeAt(0) < 32 || str.charCodeAt(0) === 127) {
|
||||
// // cb(str);
|
||||
// //} else if (str === '\x1b') { // ESC
|
||||
// // buffer.esc = true;
|
||||
// //} else if (buffer.esc) {
|
||||
// // cb('\x1b' + str);
|
||||
// // buffer.esc = false;
|
||||
// //} else {
|
||||
// buffer.str = (buffer.str || '') + str;
|
||||
// if (Date.now() - (buffer.timestamp || 0) >= latency) {
|
||||
// console.log(buffer.str)
|
||||
// cb(buffer.str);
|
||||
// buffer.str = '';
|
||||
// buffer.timestamp = Date.now();
|
||||
// }
|
||||
// //}
|
||||
//}
|
||||
|
||||
ISOTerminal.prototype.preventFrameDrop = function(cb){
|
||||
// don't let workers cause framerate dropping
|
||||
const xr = this.instance.sceneEl.renderer.xr
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -49,47 +49,3 @@ emulator.fs9p.append_file = async function(file,data){
|
|||
|
||||
}
|
||||
|
||||
emulator.fs9p.read_file_world = async function(file){
|
||||
const p = this.SearchPath(file);
|
||||
|
||||
if(p.id === -1)
|
||||
{
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
const inode = this.GetInode(p.id);
|
||||
const perms = this.parseFilePermissions(inode.mode)
|
||||
|
||||
if( !perms.world.read ){
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
return this.Read(p.id, 0, inode.size);
|
||||
}
|
||||
|
||||
emulator.fs9p.parseFilePermissions = function(permissionInt) {
|
||||
// Convert the permission integer to octal
|
||||
const octalPermissions = permissionInt.toString(8);
|
||||
|
||||
// Extract the permission bits (last 3 digits in octal)
|
||||
const permissionBits = octalPermissions.slice(-3);
|
||||
|
||||
|
||||
function parsePermission(digit) {
|
||||
const num = parseInt(digit, 10);
|
||||
|
||||
return {
|
||||
read: Boolean(num & 4), // 4 = read
|
||||
write: Boolean(num & 2), // 2 = write
|
||||
execute: Boolean(num & 1) // 1 = execute
|
||||
};
|
||||
}
|
||||
// Decode the permissions
|
||||
const permissions = {
|
||||
owner: parsePermission(permissionBits[0]),
|
||||
group: parsePermission(permissionBits[1]),
|
||||
world: parsePermission(permissionBits[2]),
|
||||
};
|
||||
|
||||
return permissions;
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ ISOTerminal.prototype.boot = async function(e){
|
|||
env.push( 'export '+String(i).toUpperCase()+'="'+decodeURIComponent( document.location[i]+'"') )
|
||||
}
|
||||
}
|
||||
await this.worker.create_file("profile.browser", this.convert.toUint8Array( env.join('\n') ) )
|
||||
this.worker.create_file("profile.browser", this.convert.toUint8Array( env.join('\n') ) )
|
||||
|
||||
if( this.serial_input == 0 ){
|
||||
if( !this.noboot ){
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
}
|
|
@ -22,25 +22,17 @@ if( typeof emulator != 'undefined' ){
|
|||
|
||||
ISOTerminal.addEventListener('javascript-eval', async function(e){
|
||||
const {script,PID} = e.detail
|
||||
let res;
|
||||
|
||||
let res
|
||||
try{
|
||||
let f = new Function(`${script}`);
|
||||
res = f();
|
||||
res = (new Function(`${script}`))()
|
||||
if( res && typeof res != 'string' ) res = JSON.stringify(res,null,2)
|
||||
}catch(err){
|
||||
console.error(err)
|
||||
console.dir(err)
|
||||
res = "error: "+err.toString()
|
||||
|
||||
// try to figure out line *FIXME*
|
||||
let line = err.stack.split("\n").find(e => e.includes("<anonymous>:") || e.includes("Function:"));
|
||||
if( line ){
|
||||
let lineIndex = (line.includes("<anonymous>:") && line.indexOf("<anonymous>:") + "<anonymous>:".length) || (line.includes("Function:") && line.indexOf("Function:") + "Function:".length);
|
||||
let lnr = +line.substring(lineIndex, lineIndex + 1) - 2
|
||||
res += script.split("\n")[lnr-1]
|
||||
}else console.dir(script)
|
||||
console.error(res)
|
||||
}catch(e){
|
||||
console.error(e)
|
||||
console.info(script)
|
||||
res = "error: "+e.toString()
|
||||
if( e.filename ){
|
||||
res += "\n"+e.filename+":"+e.lineno+":"+e.colno
|
||||
}
|
||||
}
|
||||
// update output to 9p with PID as filename (in /mnt/run)
|
||||
if( PID ){
|
||||
|
|
|
@ -3,46 +3,38 @@ ISOTerminal.prototype.redirectConsole = function(handler){
|
|||
const dir = console.dir;
|
||||
const err = console.error;
|
||||
const warn = console.warn;
|
||||
const addLineFeeds = (str) => typeof str == 'string' ? str.replace(/\n/g,"\n\r") : str
|
||||
|
||||
console.log = (...args)=>{
|
||||
const textArg = args[0];
|
||||
handler( addLineFeeds(textArg) );
|
||||
handler(textArg+'\n');
|
||||
log.apply(log, args);
|
||||
};
|
||||
console.error = (...args)=>{
|
||||
const textArg = args[0]
|
||||
handler( addLineFeeds(textArg), '\x1b[31merror\x1b[0m');
|
||||
handler( textArg+'\n', '\x1b[31merror\x1b[0m');
|
||||
err.apply(log, args);
|
||||
};
|
||||
console.dir = (...args)=>{
|
||||
const textArg = args[0]
|
||||
let str = JSON.stringify(textArg,null,2)+'\n'
|
||||
handler( addLineFeeds(str) )
|
||||
handler( JSON.stringify(textArg,null,2)+'\n');
|
||||
dir.apply(log, args);
|
||||
};
|
||||
console.warn = (...args)=>{
|
||||
const textArg = args[0]
|
||||
handler( addLineFeeds(textArg),'\x1b[38;5;208mwarn\x1b[0m');
|
||||
handler(textArg+'\n','\x1b[38;5;208mwarn\x1b[0m');
|
||||
err.apply(log, args);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
ISOTerminal.prototype.enableConsole = function(opts){
|
||||
|
||||
opts = opts || {stdout:false}
|
||||
ISOTerminal.addEventListener('emulator-started', function(){
|
||||
|
||||
this.redirectConsole( (str,prefix) => {
|
||||
let _str = typeof str == 'string' ? str : JSON.stringify(str)
|
||||
let finalStr = "";
|
||||
prefix = prefix ? prefix+' ' : ''
|
||||
_str.trim().split("\n").map( (line) => {
|
||||
finalStr += `${opts.stdout ? '' : "\x1b[38;5;165m/dev/browser: \x1b[0m"}`+prefix+line+'\n'
|
||||
let finalStr = ""
|
||||
prefix = prefix ? prefix+' ' : ' '
|
||||
str.trim().split("\n").map( (line) => {
|
||||
finalStr += '\x1b[38;5;165m/dev/browser: \x1b[0m'+prefix+line+'\n'
|
||||
})
|
||||
if( opts.stdout ){
|
||||
this.emit('serial-output-string', finalStr)
|
||||
}else this.emit('append_file', ["/dev/browser/console",finalStr])
|
||||
this.emit('append_file', ["/dev/browser/console",finalStr])
|
||||
})
|
||||
|
||||
window.addEventListener('error', function(event) {
|
||||
|
@ -57,23 +49,4 @@ ISOTerminal.prototype.enableConsole = function(opts){
|
|||
console.error(event);
|
||||
});
|
||||
|
||||
if( opts.stdout ){
|
||||
this.emit('serial-output-string', "\n\n\r☑ initialized javascript console\n");
|
||||
this.emit('serial-output-string', "\r☑ please use these functions to print:\n");
|
||||
this.emit('serial-output-string', "\r└☑ console.log(\"foo\")\n");
|
||||
this.emit('serial-output-string', "\r└☑ console.warn(\"foo\")\n");
|
||||
this.emit('serial-output-string', "\r└☑ console.dir({foo:12})\n");
|
||||
this.emit('serial-output-string', "\r└☑ console.error(\"foo\")\n");
|
||||
this.emit('serial-output-string', "\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
ISOTerminal.addEventListener('emulator-started', function(){
|
||||
this.enableConsole()
|
||||
})
|
||||
|
||||
ISOTerminal.addEventListener('init', function(){
|
||||
this.addEventListener('enable-console', function(opts){
|
||||
this.enableConsole(opts.detail)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -17,8 +17,7 @@ if( typeof emulator != 'undefined' ){
|
|||
ISOTerminal.prototype.pasteFile = async function(data){
|
||||
const {type,item,pastedText} = data
|
||||
if( pastedText){
|
||||
// the terminal handles this (pastes text)
|
||||
// this.pasteWriteFile( this.convert.toUint8Array(pastedText) ,type, null, true)
|
||||
this.pasteWriteFile( this.convert.toUint8Array(pastedText) ,type)
|
||||
}else{
|
||||
const file = item.getAsFile();
|
||||
const reader = new FileReader();
|
||||
|
@ -30,19 +29,4 @@ if( typeof emulator != 'undefined' ){
|
|||
}
|
||||
}
|
||||
|
||||
ISOTerminal.prototype.pasteInit = function(opts){
|
||||
// bind upload input
|
||||
const {instance, aEntity} = opts
|
||||
const el = aEntity.el.dom.querySelector('#pastedrop') // upload input
|
||||
el.addEventListener('change', (e) => {
|
||||
const file = el.files[0];
|
||||
const item = {...file, getAsFile: () => file } // pasteFile-event works with File objets
|
||||
const data = { item, type: file.type }
|
||||
this.emit( 'pasteFile', data, "worker" ) // impersonate as worker (as worker cannot handle File objet)
|
||||
})
|
||||
}
|
||||
|
||||
ISOTerminal.addEventListener('init', function(){
|
||||
this.addEventListener('term_init', (opts) => this.pasteInit(opts.detail) )
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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,120 @@
|
|||
ISOTerminal.addEventListener('init', function(){
|
||||
if( typeof Terminal != 'undefined' ) this.xtermInit()
|
||||
})
|
||||
|
||||
ISOTerminal.addEventListener('runISO', function(e){
|
||||
let opts = e.detail
|
||||
opts.serial_container_xtermjs = opts.screen_container
|
||||
delete opts.screen_container
|
||||
})
|
||||
|
||||
ISOTerminal.prototype.xtermInit = function(){
|
||||
this.serial_input = 0 // set input to serial line 0
|
||||
let isoterm = this
|
||||
// monkeypatch Xterm (which V86 initializes) so we can add our own constructor args
|
||||
window._Terminal = window.Terminal
|
||||
window.Terminal = function(opts){
|
||||
const term = new window._Terminal({ ...opts,
|
||||
cursorBlink:true,
|
||||
onSelectionChange: function(e){ console.log("selectchange") },
|
||||
letterSpacing: 0
|
||||
})
|
||||
|
||||
term.onSelectionChange( () => {
|
||||
document.execCommand('copy')
|
||||
term.select(0, 0, 0)
|
||||
isoterm.emit('status','copied to clipboard')
|
||||
})
|
||||
|
||||
term.onRender( () => {
|
||||
// xterm relies on requestAnimationFrame (which does not called in immersive mode)
|
||||
let _window = term._core._coreBrowserService._window
|
||||
if( !_window._XRSH_proxied ){ // patch the planet!
|
||||
|
||||
//_window.requestAnimationFrameAFRAME = function(cb){
|
||||
// if( term.tid != null ) clearTimeout(term.tid)
|
||||
// term.tid = setTimeout( function(){
|
||||
// console.log("render")
|
||||
// cb()
|
||||
// term.tid = null
|
||||
// },100)
|
||||
//}
|
||||
this.i = 0
|
||||
const requestAnimationFrameAFRAME = AFRAME.utils.throttleLeadingAndTrailing(
|
||||
function(cb){ cb() }
|
||||
,150
|
||||
)
|
||||
|
||||
// we proxy the _window object of xterm, and reroute
|
||||
// requestAnimationFrame to requestAnimationFrameAFRAME
|
||||
_window_new = new Proxy(_window,{
|
||||
get(me,k){
|
||||
if( k == '_XRSH_proxied' ) return true
|
||||
if( k == 'requestAnimationFrame' ){
|
||||
return requestAnimationFrameAFRAME.bind(me)
|
||||
}
|
||||
return typeof me[k] == 'function' ? me[k].bind(me) : me[k]
|
||||
},
|
||||
set(me,k,v){
|
||||
me[k] = v
|
||||
return true
|
||||
}
|
||||
})
|
||||
term._core._coreBrowserService._window = _window_new
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
return term
|
||||
}
|
||||
|
||||
|
||||
this.addEventListener('emulator-started', function(){
|
||||
this.emulator.serial_adapter.term.element.querySelector('.xterm-viewport').style.background = 'transparent'
|
||||
// toggle immersive with ESCAPE
|
||||
//document.body.addEventListener('keydown', (e) => e.key == 'Escape' && this.emulator.serial_adapter.term.blur() )
|
||||
})
|
||||
|
||||
const resize = (w,h) => {
|
||||
setTimeout( () => {
|
||||
if( isoterm?.emulator?.serial_adapter?.term ){
|
||||
isoterm.xtermAutoResize(isoterm.emulator.serial_adapter.term, isoterm.instance,-3)
|
||||
}
|
||||
},800) // wait for resize anim
|
||||
}
|
||||
isoterm.instance.addEventListener('window.onresize', resize )
|
||||
isoterm.instance.addEventListener('window.onmaximize', resize )
|
||||
}
|
||||
|
||||
ISOTerminal.prototype.xtermAutoResize = function(term,instance,rowoffset){
|
||||
if( !term.element ) return
|
||||
|
||||
const defaultScrollWidth = 24;
|
||||
const MINIMUM_COLS = 2;
|
||||
const MINIMUM_ROWS = 2;
|
||||
|
||||
const dims = term._core._renderService.dimensions;
|
||||
const scrollbarWidth = (term.options.scrollback === 0
|
||||
? 0
|
||||
: (term.options.overviewRuler?.width || defaultScrollWidth ));
|
||||
|
||||
const parentElementStyle = window.getComputedStyle(instance.dom);
|
||||
const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
|
||||
const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')));
|
||||
const elementStyle = window.getComputedStyle(term.element);
|
||||
const elementPadding = {
|
||||
top: parseInt(elementStyle.getPropertyValue('padding-top')),
|
||||
bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')),
|
||||
right: parseInt(elementStyle.getPropertyValue('padding-right')),
|
||||
left: parseInt(elementStyle.getPropertyValue('padding-left'))
|
||||
};
|
||||
const elementPaddingVer = elementPadding.top + elementPadding.bottom;
|
||||
const elementPaddingHor = elementPadding.right + elementPadding.left;
|
||||
const availableHeight = parentElementHeight - elementPaddingVer;
|
||||
const availableWidth = parentElementWidth - elementPaddingHor - scrollbarWidth;
|
||||
const geometry = {
|
||||
cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / dims.css.cell.width)),
|
||||
rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / dims.css.cell.height))
|
||||
};
|
||||
term.resize(geometry.cols, geometry.rows + (rowoffset||0) );
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
@ -12,29 +12,26 @@ this.runISO = function(opts){
|
|||
console.log("[worker.js] started emulator")
|
||||
|
||||
// event forwarding
|
||||
emulator.buf0 = {}
|
||||
emulator.buf1 = {}
|
||||
emulator.buf2 = {}
|
||||
|
||||
emulator.add_listener("serial0-output-byte", function(byte){
|
||||
ISOTerminal.prototype.bufferOutput(byte, (str) => { // we buffer to prevent framerate dropping
|
||||
if( !str ) return
|
||||
this.postMessage({event:"serial0-output-string",data:str});
|
||||
}, opts.bufferLatency, emulator.buf0 )
|
||||
}, opts.bufferLatency )
|
||||
}.bind(this));
|
||||
|
||||
emulator.add_listener("serial1-output-byte", function(byte){
|
||||
ISOTerminal.prototype.bufferOutput(byte, (str) => { // we buffer to prevent framerate dropping
|
||||
if( !str ) return
|
||||
this.postMessage({event:"serial1-output-string",data:str});
|
||||
}, opts.bufferLatency, emulator.buf1 )
|
||||
}, opts.bufferLatency )
|
||||
}.bind(this));
|
||||
|
||||
emulator.add_listener("serial2-output-byte", function(byte){
|
||||
ISOTerminal.prototype.bufferOutput(byte, (str) => { // we buffer to prevent framerate dropping
|
||||
if( !str ) return
|
||||
this.postMessage({event:"serial2-output-string",data:str});
|
||||
}, opts.bufferLatency, emulator.buf2 )
|
||||
}, opts.bufferLatency )
|
||||
}.bind(this));
|
||||
|
||||
emulator.add_listener("emulator-started", function(){
|
||||
|
@ -45,17 +42,10 @@ this.runISO = function(opts){
|
|||
/*
|
||||
* forward events/functions so non-worker world can reach them
|
||||
*/
|
||||
|
||||
// stripping '/mnt' is needed (the 9p mounted fs does not know about this)
|
||||
const stripMountDir = (arr) => {
|
||||
arr[0] = String(arr[0]).replace(/^\/mnt/,'')
|
||||
return arr
|
||||
}
|
||||
this.create_file = async function(){ return emulator.create_file.apply(emulator, stripMountDir(arguments[0]) ) }
|
||||
this.read_file = async function(){ return emulator.read_file.apply(emulator, stripMountDir(arguments[0]) ) }
|
||||
this.read_file_world = async function(){ return emulator.fs9p.read_file_world.apply(emulator.fs9p, stripMountDir(arguments[0]) ) }
|
||||
this.append_file = async function(){ emulator.fs9p.append_file.apply(emulator.fs9p, stripMountDir(arguments[0])) }
|
||||
this.update_file = async function(){ emulator.fs9p.update_file.apply(emulator.fs9p, stripMountDir(arguments[0])) }
|
||||
this.create_file = async function(){ return emulator.create_file.apply(emulator, arguments[0]) }
|
||||
this.read_file = async function(){ return emulator.read_file.apply(emulator, arguments[0]) }
|
||||
this.append_file = async function(){ emulator.fs9p.append_file.apply(emulator.fs9p, arguments[0]) }
|
||||
this.update_file = async function(){ emulator.fs9p.update_file.apply(emulator.fs9p, arguments[0]) }
|
||||
|
||||
// filename will be read from 9pfs: "/mnt/"+filename
|
||||
emulator.readFromPipe = function(filename,cb){
|
||||
|
|
|
@ -1,18 +1,3 @@
|
|||
/**
|
||||
* ## [pastedrop](com/pastedrop.js)
|
||||
*
|
||||
* detects user copy/paste and file dragdrop action
|
||||
* and clipboard functions
|
||||
*
|
||||
* ```html
|
||||
* <a-entity pastedrop/>
|
||||
* ```
|
||||
*
|
||||
* | event | target | info |
|
||||
* |--------------|--------|------------------------------------------|
|
||||
* | `pasteFile` | self | always translates input to a File object |
|
||||
*/
|
||||
|
||||
AFRAME.registerComponent('pastedrop', {
|
||||
schema: {
|
||||
foo: { type:"string"}
|
||||
|
@ -37,16 +22,16 @@ AFRAME.registerComponent('pastedrop', {
|
|||
})
|
||||
},
|
||||
|
||||
getClipboard: function(){
|
||||
navigator.clipboard.readText()
|
||||
.then( async (base64) => {
|
||||
let mimetype = base64.replace(/;base64,.*/,'')
|
||||
let data = base64.replace(/.*;base64,/,'')
|
||||
let type = this.textHeuristic(data)
|
||||
const term = document.querySelector('[isoterminal]').components.isoterminal.term
|
||||
this.el.emit('pasteFile',{}) /*TODO* data incompatible */
|
||||
})
|
||||
},
|
||||
//getClipboard: function(){
|
||||
// navigator.clipboard.readText()
|
||||
// .then( async (base64) => {
|
||||
// let mimetype = base64.replace(/;base64,.*/,'')
|
||||
// let data = base64.replace(/.*;base64,/,'')
|
||||
// let type = this.textHeuristic(data)
|
||||
// const term = document.querySelector('[isoterminal]').components.isoterminal.term
|
||||
// this.el.emit('pasteFile',{}) /*TODO* data incompatible */
|
||||
// })
|
||||
//},
|
||||
|
||||
onDrop: function(e){
|
||||
e.preventDefault()
|
||||
|
|
|
@ -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,8 +49,15 @@ AFRAME.registerComponent('window', {
|
|||
this.el.components['obb-collider'].update()
|
||||
},1000)
|
||||
},
|
||||
onclose: close
|
||||
|
||||
onclose: () => {
|
||||
let e = {halt:false}
|
||||
this.el.emit('window.onclose',e)
|
||||
if( e.halt ) return true
|
||||
this.data.dom.style.display = 'none';
|
||||
if( this.el.parentNode ) this.el.remove() //parentElement.remove( this.el )
|
||||
this.data.dom.parentElement.remove()
|
||||
return false
|
||||
},
|
||||
});
|
||||
this.data.dom.style.display = '' // show
|
||||
|
||||
|
|
|
@ -0,0 +1,268 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2019
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*
|
||||
* 2019 Mauve Ranger
|
||||
* 2024 Leon van Kammen
|
||||
*/
|
||||
|
||||
let terminalInstance = 0
|
||||
|
||||
const TERMINAL_THEME = {
|
||||
theme_foreground: {
|
||||
// 'default': '#ffffff'
|
||||
},
|
||||
theme_background: {
|
||||
// 'default': '#000'
|
||||
},
|
||||
theme_cursor: {
|
||||
// 'default': '#ffffff'
|
||||
},
|
||||
theme_selection: {
|
||||
// 'default': 'rgba(255, 255, 255, 0.3)'
|
||||
},
|
||||
theme_black: {
|
||||
// 'default': '#000000'
|
||||
},
|
||||
theme_red: {
|
||||
// 'default': '#e06c75'
|
||||
},
|
||||
theme_brightRed: {
|
||||
// 'default': '#e06c75'
|
||||
},
|
||||
theme_green: {
|
||||
// 'default': '#A4EFA1'
|
||||
},
|
||||
theme_brightGreen: {
|
||||
// 'default': '#A4EFA1'
|
||||
},
|
||||
theme_brightYellow: {
|
||||
// 'default': '#EDDC96'
|
||||
},
|
||||
theme_yellow: {
|
||||
// 'default': '#EDDC96'
|
||||
},
|
||||
theme_magenta: {
|
||||
// 'default': '#e39ef7'
|
||||
},
|
||||
theme_brightMagenta: {
|
||||
// 'default': '#e39ef7'
|
||||
},
|
||||
theme_cyan: {
|
||||
// 'default': '#5fcbd8'
|
||||
},
|
||||
theme_brightBlue: {
|
||||
// 'default': '#5fcbd8'
|
||||
},
|
||||
theme_brightCyan: {
|
||||
// 'default': '#5fcbd8'
|
||||
},
|
||||
theme_blue: {
|
||||
// 'default': '#5fcbd8'
|
||||
},
|
||||
theme_white: {
|
||||
// 'default': '#d0d0d0'
|
||||
},
|
||||
theme_brightBlack: {
|
||||
// 'default': '#808080'
|
||||
},
|
||||
theme_brightWhite: {
|
||||
// 'default': '#ffffff'
|
||||
}
|
||||
}
|
||||
|
||||
AFRAME.registerComponent('xterm', {
|
||||
schema: Object.assign({
|
||||
XRrenderer: { type: 'string', default: 'canvas', },
|
||||
cols: { type: 'number', default: 110, },
|
||||
rows: { type: 'number', default: Math.floor( (window.innerHeight * 0.7 ) * 0.054 ) },
|
||||
canvasLatency:{ type:'number', default: 200 }
|
||||
}, TERMINAL_THEME),
|
||||
|
||||
write: function(message) {
|
||||
this.term.write(message)
|
||||
},
|
||||
init: function () {
|
||||
const terminalElement = document.createElement('div')
|
||||
terminalElement.setAttribute('style', `
|
||||
width: 800px;
|
||||
height: ${Math.floor( 800 * 0.527 )}px;
|
||||
overflow: hidden;
|
||||
`)
|
||||
|
||||
this.el.terminalElement = terminalElement
|
||||
|
||||
if( this.data.XRrenderer == 'canvas' ){
|
||||
// setup slightly bigger black backdrop (this.el.getObject3D("mesh"))
|
||||
// and terminal text (this.el.planeText.getObject("mesh"))
|
||||
const w = 2;
|
||||
const h = (this.data.rows*5/this.data.cols)
|
||||
this.el.setAttribute("geometry",`primitive: box; width:${w}; height:${h}; depth: -0.12`)
|
||||
this.el.setAttribute("material","shader:flat; color:black; opacity:0.5; transparent:true; ")
|
||||
this.el.planeText = document.createElement('a-entity')
|
||||
this.el.planeText.setAttribute("geometry",`primitive: plane; width:${w}; height:${h}`)
|
||||
this.el.appendChild(this.el.planeText)
|
||||
|
||||
// we switch between dom/canvas rendering because canvas looks pixely in nonimmersive mode
|
||||
this.el.sceneEl.addEventListener('enter-vr', this.enterImmersive.bind(this) )
|
||||
this.el.sceneEl.addEventListener('enter-ar', this.enterImmersive.bind(this) )
|
||||
this.el.sceneEl.addEventListener('exit-vr', this.exitImmersive.bind(this) )
|
||||
this.el.sceneEl.addEventListener('exit-ar', this.exitImmersive.bind(this) )
|
||||
|
||||
}
|
||||
|
||||
this.tick = AFRAME.utils.throttleLeadingAndTrailing( () => {
|
||||
if( this.el.sceneEl.renderer.xr.isPresenting ){
|
||||
// workaround
|
||||
// xterm relies on window.requestAnimationFrame (which is not called WebXR immersive mode)
|
||||
//this.term._core.viewport._innerRefresh()
|
||||
this.term._core.renderer._renderDebouncer._innerRefresh()
|
||||
}
|
||||
}, this.data.canvasLatency)
|
||||
|
||||
// Build up a theme object
|
||||
const theme = Object.keys(this.data).reduce((theme, key) => {
|
||||
if (!key.startsWith('theme_')) return theme
|
||||
const data = this.data[key]
|
||||
if(!data) return theme
|
||||
theme[key.slice('theme_'.length)] = data
|
||||
return theme
|
||||
}, {})
|
||||
|
||||
this.fontSize = 14
|
||||
|
||||
const term = this.term = new Terminal({
|
||||
logLevel:"off",
|
||||
theme: theme,
|
||||
allowTransparency: true,
|
||||
cursorBlink: true,
|
||||
disableStdin: false,
|
||||
rows: this.data.rows,
|
||||
cols: this.data.cols,
|
||||
fontFamily: 'Cousine, monospace',
|
||||
fontSize: this.fontSize,
|
||||
lineHeight: 1.15,
|
||||
useFlowControl: true,
|
||||
rendererType: this.renderType // 'dom' // 'canvas'
|
||||
})
|
||||
|
||||
this.term.open(terminalElement)
|
||||
this.term.focus()
|
||||
this.setRenderType('dom')
|
||||
|
||||
terminalElement.querySelector('.xterm-viewport').style.background = 'transparent'
|
||||
|
||||
// now we can scale canvases to the parent element
|
||||
const $screen = terminalElement.querySelector('.xterm-screen')
|
||||
$screen.style.width = '100%'
|
||||
|
||||
term.on('refresh', AFRAME.utils.throttleLeadingAndTrailing( () => this.update(), 150 ) )
|
||||
term.on('data', (data) => {
|
||||
this.el.emit('xterm-input', data)
|
||||
})
|
||||
|
||||
this.el.addEventListener('serial-output-byte', (e) => {
|
||||
const byte = e.detail
|
||||
var chr = String.fromCharCode(byte);
|
||||
this.term.write(chr)
|
||||
})
|
||||
|
||||
this.el.addEventListener('serial-output-string', (e) => {
|
||||
this.term.write(e.detail)
|
||||
})
|
||||
|
||||
},
|
||||
|
||||
update: function(){
|
||||
if( this.renderType == 'canvas' ){
|
||||
const material = this.el.planeText.getObject3D('mesh').material
|
||||
if (!material.map ) return
|
||||
if( this.cursorCanvas ) this.canvasContext.drawImage(this.cursorCanvas, 0,0)
|
||||
else console.log("no cursorCanvas")
|
||||
material.map.needsUpdate = true
|
||||
//material.needsUpdate = true
|
||||
}
|
||||
},
|
||||
|
||||
setRenderType: function(type){
|
||||
|
||||
|
||||
if( type.match(/(dom|canvas)/) ){
|
||||
|
||||
if( type == 'dom'){
|
||||
this.el.dom.appendChild(this.el.terminalElement)
|
||||
this.term.setOption('fontSize', this.fontSize )
|
||||
this.term.setOption('rendererType',type )
|
||||
this.renderType = type
|
||||
}
|
||||
|
||||
if( type == 'canvas'){
|
||||
this.el.appendChild(this.el.terminalElement)
|
||||
this.term.setOption('fontSize', this.fontSize * 3 )
|
||||
this.term.setOption('rendererType',type )
|
||||
this.renderType = type
|
||||
this.update()
|
||||
setTimeout( () => {
|
||||
this.canvas = this.el.terminalElement.querySelector('.xterm-text-layer')
|
||||
this.canvas.id = "xterm-canvas"
|
||||
this.canvasContext = this.canvas.getContext('2d')
|
||||
this.cursorCanvas = this.el.terminalElement.querySelector('.xterm-cursor-layer')
|
||||
// Create a texture from the canvas
|
||||
const canvasTexture = new THREE.Texture(this.canvas)
|
||||
//canvasTexture.minFilter = THREE.NearestFilter //LinearFilter
|
||||
//canvasTexture.magFilter = THREE.LinearMipMapLinearFilter //THREE.NearestFilter //LinearFilter
|
||||
canvasTexture.needsUpdate = true; // Ensure the texture updates
|
||||
let plane = this.el.planeText.getObject3D("mesh") //this.el.getObject3D('mesh')
|
||||
if( plane.material ) plane.material.dispose()
|
||||
plane.material = new THREE.MeshBasicMaterial({
|
||||
map: canvasTexture, // Set the texture from the canvas
|
||||
transparent: false, // Set transparency
|
||||
//side: THREE.DoubleSide // Set to double-sided rendering
|
||||
//blending: THREE.AdditiveBlending
|
||||
});
|
||||
this.el.object3D.scale.x = 0.2
|
||||
this.el.object3D.scale.y = 0.2
|
||||
this.el.object3D.scale.z = 0.2
|
||||
},100)
|
||||
}
|
||||
|
||||
this.el.terminalElement.style.opacity = type == 'canvas' ? 0 : 1
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
enterImmersive: function(){
|
||||
if( this.mode == 'immersive' ) return
|
||||
this.el.object3D.visible = true
|
||||
this.mode = "immersive"
|
||||
this.setRenderType('canvas')
|
||||
this.term.focus()
|
||||
},
|
||||
|
||||
exitImmersive: function(){
|
||||
if( this.mode == 'nonimmersive' ) return
|
||||
this.el.object3D.visible = false
|
||||
this.mode = "nonimmersive"
|
||||
this.setRenderType('dom')
|
||||
},
|
||||
|
||||
})
|
Loading…
Reference in New Issue