xrsh/xrsh.js
Leon van Kammen 5a3c4ecc13
Some checks failed
/ mirror_to_github (push) Failing after 2m29s
/ test (push) Successful in 15s
update build
2025-04-01 14:56:56 +02:00

4385 lines
160 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* xrsh.js AFRAME component
* https://xrsh.isvery.ninja
* SPDX: AGPL-3.0-or-later
*/
document.head.innerHTML += `<style type='text/css'>
/*
* HTML-2-WebGL limitations / guidelines for html-mesh compatibility:
* - no icon libraries (favicon e.g.)
* - in case of 'border-radius: 2px 3px 4px 5px' (2px will apply to all corners)
* - dont use transform: scale(1.2) e.g.
* - dont use pseudoclasses like :before or :after (they mess up html-to-canvas conversion)
* - use visibility:hidden/visible instead of display:none
*/
:root {
--xrsh-primary: #3a51ff;
--xrsh-primary-fg: #FFF;
--xrsh-light-primary: #00a3Ff;
--xrsh-secondary: #872eff;
--xrsh-third: #ce7df2;
--xrsh-box-shadow: #0005;
--xrsh-dark-gray: #343334;
--xrsh-gray: #424280;
--xrsh-white: #fdfdfd;
--xrsh-light-gray: #efefef;
--xrsh-lighter-gray: #e4e2fb96;
--xrsh-window-radius: 10px;
--xrsh-font-sans-serif: system-ui, -apple-system, segoe ui, roboto, ubuntu, helvetica, cantarell, noto sans, sans-serif;
--xrsh-font-monospace: menlo, monaco, lucida console, liberation mono, dejavu sans mono, bitstream vera sans mono, courier new, monospace, serif;
--xrsh-font-size-0: 12px;
--xrsh-font-size-1: 14px;
--xrsh-font-size-2: 17px;
--xrsh-font-size-3: 21px;
--xrsh-modal-button-bg: #CCC;
--xrsh-modal-button-fg: #FFF;
}
body,
html.a-fullscreen body{
color: var(--xrsh-light-gray);
font-size: var(--xrsh-font-size-1);
font-family: var(--xrsh-font-sans-serif);
accent-color: var(--xrsh-light-primary);
line-height:22px;
}
h1,h2,h3,h4,h5{
color: var(--xrsh-light-gray);
}
h1 { font-size: var(--xrsh-font-size-3); }
h2,h3,h4{ font-size: var(--xrsh-font-size-2); }
a,a:visited,a:active{
color: var(--xrsh-light-primary);
}
/*
* Form elements
*/
button,.btn,input[type=submit]{
border-radius:7px;
background: var(--xrsh-primary);
color: var(--xrsh-primary-fg);
transition:0.3s;
padding: 10px;
font-weight: bold;
border: none;
cursor:pointer;
}
button:hover,.btn:hover,input[type=submit]:hover{
background: var(--xrsh-white);
color: var(--xrsh-dark-gray) !important;
}
legend{
font-size: var(--xrsh-font-size-0);
margin-bottom: 15px;
border-bottom: 1px solid var(--xrsh-light-primary);
}
body .winbox fieldset,
fieldset{
border: none;
padding: 0;
margin: 0;
margin-bottom: 0px;
margin-bottom: 5px;
}
label{
margin-left:10px;
}
button,input,.btn{
margin-bottom:10px;
}
[type="checkbox"], [type="radio"]{
transform: scale(1.4);
margin-left:3px;
}
/*
* WinBox (windows) theme
*/
.winbox {
box-shadow:none !important;
background:transparent !important;
}
.winbox .wb-body{
background: var(--xrsh-dark-gray);
margin-top: -6px !important;
z-index: 100;
border: 1px solid #FFF3;
border-radius:7px;
}
.winbox .wb-header{
background:transparent !important;
z-index:1000;
font-weight: bold;
border-radius: var(--xrsh-window-radius) var(--xrsh-window-radius) 0px 0px;
line-height:36px;
}
.winbox.max .wb-header{
border-radius:0px;
}
.winbox .wb-body{
top:0px;
padding-top:35px;
}
.winbox .wb-control{
height:29px;
}
.winbox .wb-drag{
padding-left:19px;
}
.winbox .wb-control > span{
background-size: 12px auto !important;
}
.winbox *{
color: var(--xrsh-light-gray);
font-size: var(--xrsh-font-size-1);
font-family: var(--xrsh-font-sans-serif);
}
.winbox input[type=text]{
color: var(--xrsh-dark-gray);
border-radius: 6px;
color: var(--xrsh-dark-gray);
height: 25px;
margin-bottom: 2px;
border-block: none;
border: none;
}
.winbox .wb-body *::-webkit-scrollbar {
width: 4px;
height: 4px;
}
.winbox .wb-body *::-webkit-scrollbar-track {
background: var(--xrsh-gray);
border-left: 9px solid white;
border-right: 9px solid white;
}
.winbox .wb-body *::-webkit-scrollbar-thumb {
background: var(--xrsh-white);
}
/*
* icons for non-XR (please source from https://css.gg
*
* (NOTE: they will not be visible in XR)
*/
.gg-stack,
.gg-stack::after,
.gg-stack::before {
display: block;
box-sizing: border-box;
width: 14px;
height: 14px;
border: 2px solid
}
.gg-stack {
margin-right: 8px;
margin-top: 8px;
transform: scale(var(--ggs,1)) translate(14px,-1px);
position: relative
}
.gg-stack::after,
.gg-stack::before {
content: "";
position: absolute;
border-left: 0;
border-bottom: 0;
right: -5px;
top: -5px
}
.gg-stack::before {
right: -8px;
top: -8px
}
</style>`;
/**
* WinBox.js v0.2.82 (Bundle)
* Author and Copyright: Thomas Wilkerling
* Licence: Apache-2.0
* Hosted by Nextapps GmbH
* https://github.com/nextapps-de/winbox
*/
(function(){'use strict';var e,aa=document.createElement("style");aa.innerHTML="@keyframes wb-fade-in{0%{opacity:0}to{opacity:.85}}.winbox{position:fixed;left:0;top:0;background:#0050ff;box-shadow:0 14px 28px rgba(0,0,0,.25),0 10px 10px rgba(0,0,0,.22);transition:width .3s,height .3s,left .3s,top .3s;transition-timing-function:cubic-bezier(.3,1,.3,1);contain:layout size;text-align:left;touch-action:none}.wb-body,.wb-header{position:absolute;left:0}.wb-header{top:0;width:100%;height:35px;line-height:35px;color:#fff;overflow:hidden;z-index:1}.wb-body{top:35px;right:0;bottom:0;overflow:auto;-webkit-overflow-scrolling:touch;overflow-scrolling:touch;will-change:contents;background:#fff;margin-top:0!important;contain:strict;z-index:0}.wb-control *,.wb-icon{background-repeat:no-repeat}.wb-drag{height:100%;padding-left:10px;cursor:move}.wb-title{font-family:Arial,sans-serif;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.wb-icon{display:none;width:20px;height:100%;margin:-1px 8px 0-3px;float:left;background-size:100%;background-position:center}.wb-e,.wb-w{width:10px;top:0}.wb-n,.wb-s{left:0;height:10px;position:absolute}.wb-n{top:-5px;right:0;cursor:n-resize;z-index:2}.wb-e{position:absolute;right:-5px;bottom:0;cursor:w-resize;z-index:2}.wb-s{bottom:-5px;right:0;cursor:n-resize;z-index:2}.wb-nw,.wb-sw,.wb-w{left:-5px}.wb-w{position:absolute;bottom:0;cursor:w-resize;z-index:2}.wb-ne,.wb-nw,.wb-sw{width:15px;height:15px;z-index:2;position:absolute}.wb-nw{top:-5px;cursor:nw-resize}.wb-ne,.wb-sw{cursor:ne-resize}.wb-ne{top:-5px;right:-5px}.wb-se,.wb-sw{bottom:-5px}.wb-se{position:absolute;right:-5px;width:15px;height:15px;cursor:nw-resize;z-index:2}.wb-control{float:right;height:100%;max-width:100%;text-align:center}.wb-control *{display:inline-block;width:30px;height:100%;max-width:100%;background-position:center;cursor:pointer}.no-close .wb-close,.no-full .wb-full,.no-header .wb-header,.no-max .wb-max,.no-min .wb-min,.no-resize .wb-body~div,.wb-body .wb-hide,.wb-show,.winbox.hide,.winbox.min .wb-body>*,.winbox.min .wb-full,.winbox.min .wb-min,.winbox.modal .wb-full,.winbox.modal .wb-max,.winbox.modal .wb-min{display:none}.winbox.max .wb-drag,.winbox.min .wb-drag{cursor:default}.wb-min{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAyIj48cGF0aCBmaWxsPSIjZmZmIiBkPSJNOCAwaDdhMSAxIDAgMCAxIDAgMkgxYTEgMSAwIDAgMSAwLTJoN3oiLz48L3N2Zz4=);background-size:14px auto;background-position:center calc(50% + 6px)}.wb-max{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9IiNmZmYiIHZpZXdCb3g9IjAgMCA5NiA5NiI+PHBhdGggZD0iTTIwIDcxLjMxMUMxNS4zNCA2OS42NyAxMiA2NS4yMyAxMiA2MFYyMGMwLTYuNjMgNS4zNy0xMiAxMi0xMmg0MGM1LjIzIDAgOS42NyAzLjM0IDExLjMxMSA4SDI0Yy0yLjIxIDAtNCAxLjc5LTQgNHY1MS4zMTF6Ii8+PHBhdGggZD0iTTkyIDc2VjM2YzAtNi42My01LjM3LTEyLTEyLTEySDQwYy02LjYzIDAtMTIgNS4zNy0xMiAxMnY0MGMwIDYuNjMgNS4zNyAxMiAxMiAxMmg0MGM2LjYzIDAgMTItNS4zNyAxMi0xMnptLTUyIDRjLTIuMjEgMC00LTEuNzktNC00VjM2YzAtMi4yMSAxLjc5LTQgNC00aDQwYzIuMjEgMCA0IDEuNzkgNCA0djQwYzAgMi4yMS0xLjc5IDQtNCA0SDQweiIvPjwvc3ZnPg==);background-size:17px auto}.wb-close{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0xIC0xIDE4IDE4Ij48cGF0aCBmaWxsPSIjZmZmIiBkPSJtMS42MTMuMjEuMDk0LjA4M0w4IDYuNTg1IDE0LjI5My4yOTNsLjA5NC0uMDgzYTEgMSAwIDAgMSAxLjQwMyAxLjQwM2wtLjA4My4wOTRMOS40MTUgOGw2LjI5MiA2LjI5M2ExIDEgMCAwIDEtMS4zMiAxLjQ5N2wtLjA5NC0uMDgzTDggOS40MTVsLTYuMjkzIDYuMjkyLS4wOTQuMDgzQTEgMSAwIDAgMSAuMjEgMTQuMzg3bC4wODMtLjA5NEw2LjU4NSA4IC4yOTMgMS43MDdBMSAxIDAgMCAxIDEuNjEzLjIxeiIvPjwvc3ZnPg==);background-size:15px auto;background-position:5px center}.wb-full{background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2Utd2lkdGg9IjIuNSIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNOCAzSDVhMiAyIDAgMCAwLTIgMnYzbTE4IDBWNWEyIDIgMCAwIDAtMi0yaC0zbTAgMThoM2EyIDIgMCAwIDAgMi0ydi0zTTMgMTZ2M2EyIDIgMCAwIDAgMiAyaDMiLz48L3N2Zz4=);background-size:16px auto}.winbox.max .wb-body~div,.winbox.min .wb-body~div,.winbox.modal .wb-body~div,.winbox.modal .wb-drag,body.wb-lock iframe{pointer-events:none}.winbox.max{box-shadow:none}.winbox.max .wb-body{margin:0!important}.winbox iframe{position:absolute;width:100%;height:100%;border:0}body.wb-lock .winbox{will-change:left,top,width,height;transition:none}.winbox.modal:before{content:'';position:absolute;top:0;left:0;right:0;bottom:0;background:inherit;border-radius:inherit}.winbox.modal:after{content:'';position:absolute;top:-50vh;left:-50vw;right:-50vw;bottom:-50vh;background:#0d1117;animation:wb-fade-in .2s ease-out forwards;z-index:-1}.no-animation{transition:none}.no-shadow{box-shadow:none}.no-header .wb-body{top:0}.no-move:not(.min) .wb-title{pointer-events:none}.wb-body .wb-show{display:revert}";
var h=document.getElementsByTagName("head")[0];h.firstChild?h.insertBefore(aa,h.firstChild):h.appendChild(aa);var ba=document.createElement("div");ba.innerHTML="<div class=wb-header><div class=wb-control><span class=wb-min></span><span class=wb-max></span><span class=wb-full></span><span class=wb-close></span></div><div class=wb-drag><div class=wb-icon></div><div class=wb-title></div></div></div><div class=wb-body></div><div class=wb-n></div><div class=wb-s></div><div class=wb-w></div><div class=wb-e></div><div class=wb-nw></div><div class=wb-ne></div><div class=wb-se></div><div class=wb-sw></div>";function k(a,b,c,f){a&&a.addEventListener(b,c,f||!1)}function l(a,b){var c=window,f=m;c&&c.removeEventListener(a,b,f||!1)}function t(a,b){a.stopPropagation();b&&a.preventDefault()}function u(a,b,c){c=""+c;a["_s_"+b]!==c&&(a.style.setProperty(b,c),a["_s_"+b]=c)};/*
self.max &&*/
var x=[],A=[],ca={capture:!0,passive:!1},m={capture:!0,passive:!0},B,da=0,E=10,F,J,ha,K,P,ia;
function U(a,b){if(!(this instanceof U))return new U(a);B||ja();if(a){if(b){var c=a;a=b}if("string"===typeof a)c=a;else{var f=a.id;var d=a.index;var n=a.root;var p=a.template;c=c||a.title;var v=a.icon;var L=a.mount;var Q=a.html;var g=a.url;var q=a.width;var r=a.height;var w=a.minwidth;var C=a.minheight;var y=a.maxwidth;var z=a.maxheight;var ea=a.autosize;var D=a.overflow;var G=a.min;var H=a.max;var I=a.hidden;var fa=a.modal;var X=a.x||(fa?"center":0);var Y=a.y||(fa?"center":0);var M=a.top;var N=a.left;
var R=a.bottom;var S=a.right;var la=a.background;var O=a.border;var T=a.header;var Z=a["class"];var ma=a.oncreate;var ra=a.onclose;var sa=a.onfocus;var ta=a.onblur;var ua=a.onmove;var va=a.onresize;var wa=a.onfullscreen;var xa=a.onmaximize;var ya=a.onminimize;var za=a.onrestore;var Aa=a.onhide;var Ba=a.onshow;var Ca=a.onload}}this.g=(p||ba).cloneNode(!0);this.g.id=this.id=f||"winbox-"+ ++da;this.g.className="winbox"+(Z?" "+("string"===typeof Z?Z:Z.join(" ")):"")+(fa?" modal":"");this.g.winbox=this;
this.window=this.g;this.body=this.g.getElementsByClassName("wb-body")[0];this.h=T||35;A.push(this);la&&this.setBackground(la);O?u(this.body,"margin",O+(isNaN(O)?"":"px")):O=0;T&&(b=this.g.getElementsByClassName("wb-header")[0],u(b,"height",T+"px"),u(b,"line-height",T+"px"),u(this.body,"top",T+"px"));c&&this.setTitle(c);v&&this.setIcon(v);L?this.mount(L):Q?this.body.innerHTML=Q:g&&this.setUrl(g,Ca);M=M?V(M,P):0;R=R?V(R,P):0;N=N?V(N,K):0;S=S?V(S,K):0;c=K-N-S;v=P-M-R;y=y?V(y,c):c;z=z?V(z,v):v;w=w?V(w,
y):150;C=C?V(C,z):this.h;ea?((n||B).appendChild(this.body),q=Math.max(Math.min(this.body.clientWidth+2*O+1,y),w),r=Math.max(Math.min(this.body.clientHeight+this.h+O+1,z),C),this.g.appendChild(this.body)):(q=q?V(q,y):Math.max(y/2,w)|0,r=r?V(r,z):Math.max(z/2,C)|0);X=X?V(X,c,q):N;Y=Y?V(Y,v,r):M;this.x=X;this.y=Y;this.width=q;this.height=r;this.s=w;this.o=C;this.m=y;this.l=z;this.top=M;this.right=S;this.bottom=R;this.left=N;this.index=d;this.j=D;this.focused=this.hidden=this.full=this.max=this.min=!1;
this.onclose=ra;this.onfocus=sa;this.onblur=ta;this.onmove=ua;this.onresize=va;this.onfullscreen=wa;this.onmaximize=xa;this.onminimize=ya;this.onrestore=za;this.onhide=Aa;this.onshow=Ba;I?this.hide():this.focus();if(d||0===d)this.index=d,u(this.g,"z-index",d),d>E&&(E=d);H?this.maximize():G?this.minimize():this.resize().move();ka(this);(n||B).appendChild(this.g);ma&&ma.call(this,a)}U["new"]=function(a){return new U(a)};U.stack=function(){return A};
function V(a,b,c){"string"===typeof a&&("center"===a?a=(b-c)/2+.5|0:"right"===a||"bottom"===a?a=b-c:(c=parseFloat(a),a="%"===(""+c!==a&&a.substring((""+c).length))?b/100*c+.5|0:c));return a}
function ja(){B=document.body;B[J="requestFullscreen"]||B[J="msRequestFullscreen"]||B[J="webkitRequestFullscreen"]||B[J="mozRequestFullscreen"]||(J="");ha=J&&J.replace("request","exit").replace("mozRequest","mozCancel").replace("Request","Exit");k(window,"resize",function(){na();oa()});k(B,"mousedown",function(){ia=!1},!0);k(B,"mousedown",function(){if(!ia){var a=A.length;if(a)for(--a;0<=a;a--){var b=A[a];if(b.focused){b.blur();break}}}});na()}
function ka(a){W(a,"drag");W(a,"n");W(a,"s");W(a,"w");W(a,"e");W(a,"nw");W(a,"ne");W(a,"se");W(a,"sw");k(a.g.getElementsByClassName("wb-min")[0],"click",function(b){t(b);a.min?a.restore().focus():a.minimize()});k(a.g.getElementsByClassName("wb-max")[0],"click",function(b){t(b);a.max?a.restore().focus():a.maximize().focus()});J?k(a.g.getElementsByClassName("wb-full")[0],"click",function(b){t(b);a.fullscreen().focus()}):a.addClass("no-full");k(a.g.getElementsByClassName("wb-close")[0],"click",function(b){t(b);
a.close()||(a=null)});k(a.g,"mousedown",function(){ia=!0},!0);k(a.body,"mousedown",function(){a.focus()},!0)}function pa(a){x.splice(x.indexOf(a),1);oa();a.removeClass("min");a.min=!1;a.g.title=""}function oa(){for(var a=x.length,b={},c={},f=0,d;f<a;f++)d=x[f],d=d.left+":"+d.top,c[d]?c[d]++:(b[d]=0,c[d]=1);f=0;for(var n,p;f<a;f++)d=x[f],n=d.left+":"+d.top,p=Math.min((K-d.left-d.right)/c[n],250),d.resize(p+1|0,d.h,!0).move(d.left+b[n]*p|0,P-d.bottom-d.h,!0),b[n]++}
function W(a,b){function c(g){t(g,!0);a.focus();if("drag"===b){if(a.min){a.restore();return}if(!a.g.classList.contains("no-max")){var q=Date.now(),r=q-Q;Q=q;if(300>r){a.max?a.restore():a.maximize();return}}}a.min||(B.classList.add("wb-lock"),(p=g.touches)&&(p=p[0])?(g=p,k(window,"touchmove",f,m),k(window,"touchend",d,m)):(k(window,"mousemove",f,m),k(window,"mouseup",d,m)),v=g.pageX,L=g.pageY)}function f(g){t(g);p&&(g=g.touches[0]);var q=g.pageX;g=g.pageY;var r=q-v,w=g-L,C=a.width,y=a.height,z=a.x,
ea=a.y,D;if("drag"===b){if(a.g.classList.contains("no-move"))return;a.x+=r;a.y+=w;var G=D=1}else{if("e"===b||"se"===b||"ne"===b){a.width+=r;var H=1}else if("w"===b||"sw"===b||"nw"===b)a.x+=r,a.width-=r,G=H=1;if("s"===b||"se"===b||"sw"===b){a.height+=w;var I=1}else if("n"===b||"ne"===b||"nw"===b)a.y+=w,a.height-=w,D=I=1}H&&(a.width=Math.max(Math.min(a.width,a.m,K-a.x-a.right),a.s),H=a.width!==C);I&&(a.height=Math.max(Math.min(a.height,a.l,P-a.y-a.bottom),a.o),I=a.height!==y);(H||I)&&a.resize();G&&
(a.max&&(a.x=(q<K/3?a.left:q>K/3*2?K-a.width-a.right:K/2-a.width/2)+r),a.x=Math.max(Math.min(a.x,a.j?K-30:K-a.width-a.right),a.j?30-a.width:a.left),G=a.x!==z);D&&(a.max&&(a.y=a.top+w),a.y=Math.max(Math.min(a.y,a.j?P-a.h:P-a.height-a.bottom),a.top),D=a.y!==ea);if(G||D)a.max&&a.restore(),a.move();if(H||G)v=q;if(I||D)L=g}function d(g){t(g);B.classList.remove("wb-lock");p?(l("touchmove",f),l("touchend",d)):(l("mousemove",f),l("mouseup",d))}var n=a.g.getElementsByClassName("wb-"+b)[0];if(n){var p,v,L,
Q=0;k(n,"mousedown",c,ca);k(n,"touchstart",c,ca)}}function na(){var a=document.documentElement;K=a.clientWidth;P=a.clientHeight}e=U.prototype;e.mount=function(a){this.unmount();a.i||(a.i=a.parentNode);this.body.textContent="";this.body.appendChild(a);return this};e.unmount=function(a){var b=this.body.firstChild;if(b){var c=a||b.i;c&&c.appendChild(b);b.i=a}return this};
e.setTitle=function(a){var b=this.g.getElementsByClassName("wb-title")[0];a=this.title=a;var c=b.firstChild;c?c.nodeValue=a:b.textContent=a;return this};e.setIcon=function(a){var b=this.g.getElementsByClassName("wb-icon")[0];u(b,"background-image","url("+a+")");u(b,"display","inline-block");return this};e.setBackground=function(a){u(this.g,"background",a);return this};
e.setUrl=function(a,b){var c=this.body.firstChild;c&&"iframe"===c.tagName.toLowerCase()?c.src=a:(this.body.innerHTML='<iframe src="'+a+'"></iframe>',b&&(this.body.firstChild.onload=b));return this};e.focus=function(a){if(!1===a)return this.blur();if(!this.focused){a=A.length;if(1<a)for(var b=1;b<=a;b++){var c=A[a-b];if(c.focused){c.blur();A.push(A.splice(A.indexOf(this),1)[0]);break}}u(this.g,"z-index",++E);this.index=E;this.addClass("focus");this.focused=!0;this.onfocus&&this.onfocus()}return this};
e.blur=function(a){if(!1===a)return this.focus();this.focused&&(this.removeClass("focus"),this.focused=!1,this.onblur&&this.onblur());return this};e.hide=function(a){if(!1===a)return this.show();if(!this.hidden)return this.onhide&&this.onhide(),this.hidden=!0,this.addClass("hide")};e.show=function(a){if(!1===a)return this.hide();if(this.hidden)return this.onshow&&this.onshow(),this.hidden=!1,this.removeClass("hide")};
e.minimize=function(a){if(!1===a)return this.restore();F&&qa();this.max&&(this.removeClass("max"),this.max=!1);this.min||(x.push(this),oa(),this.g.title=this.title,this.addClass("min"),this.min=!0,this.focused&&(this.blur(),Da()),this.onminimize&&this.onminimize());return this};function Da(){var a=A.length;if(a)for(--a;0<=a;a--){var b=A[a];if(!b.min){b.focus();break}}}
e.restore=function(){F&&qa();this.min&&(pa(this),this.resize().move(),this.onrestore&&this.onrestore());this.max&&(this.max=!1,this.removeClass("max").resize().move(),this.onrestore&&this.onrestore());return this};e.maximize=function(a){if(!1===a)return this.restore();F&&qa();this.min&&pa(this);this.max||(this.addClass("max").resize(K-this.left-this.right,P-this.top-this.bottom,!0).move(this.left,this.top,!0),this.max=!0,this.onmaximize&&this.onmaximize());return this};
e.fullscreen=function(a){this.min&&(pa(this),this.resize().move());if(!F||!qa())this.body[J](),F=this,this.full=!0,this.onfullscreen&&this.onfullscreen();else if(!1===a)return this.restore();return this};function qa(){F.full=!1;if(document.fullscreen||document.fullscreenElement||document.webkitFullscreenElement||document.mozFullScreenElement)return document[ha](),!0}
e.close=function(a){if(this.onclose&&this.onclose(a))return!0;this.min&&pa(this);A.splice(A.indexOf(this),1);this.unmount();this.g.remove();this.g.textContent="";this.g=this.body=this.g.winbox=null;this.focused&&Da()};e.move=function(a,b,c){a||0===a?c||(this.x=a?a=V(a,K-this.left-this.right,this.width):0,this.y=b?b=V(b,P-this.top-this.bottom,this.height):0):(a=this.x,b=this.y);u(this.g,"left",a+"px");u(this.g,"top",b+"px");this.onmove&&this.onmove(a,b);return this};
e.resize=function(a,b,c){a||0===a?c||(this.width=a?a=V(a,this.m):0,this.height=b?b=V(b,this.l):0,a=Math.max(a,this.s),b=Math.max(b,this.o)):(a=this.width,b=this.height);u(this.g,"width",a+"px");u(this.g,"height",b+"px");this.onresize&&this.onresize(a,b);return this};
e.addControl=function(a){var b=a["class"],c=a.image,f=a.click;a=a.index;var d=document.createElement("span"),n=this.g.getElementsByClassName("wb-control")[0],p=this;b&&(d.className=b);c&&u(d,"background-image","url("+c+")");f&&(d.onclick=function(v){f.call(this,v,p)});n.insertBefore(d,n.childNodes[a||0]);return this};e.removeControl=function(a){(a=this.g.getElementsByClassName(a)[0])&&a.remove();return this};e.addClass=function(a){this.g.classList.add(a);return this};
e.removeClass=function(a){this.g.classList.remove(a);return this};e.toggleClass=function(a){return this.g.classList.contains(a)?this.removeClass(a):this.addClass(a)};window.WinBox=U;}).call(this);
/**
* ## [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` | '' | zip URL/file to autoextract on top of filesystem |
* | `width` | `number` | 800 ||
* | `height` | `number` | 600 ||
* | `depth` | `number` | 0.03 ||
* | `lineHeight` | `number` | 18 ||
* | `bootMenu` | `string` | "" | character to auto-enter in bootMenu |
* | `bootMenuURL` | `string` | "" | character to auto-enter in bootMenu when URL has fragment (#foo.zip e.g.) |
* | `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())
* ```
*/
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 },
bootOnFragment: { type: 'string',"default": "nothing" },
bootMenu: { type: 'string', "default": '' }, // show bootMenu if empty, or autosend key to bootMenu (key '1' e.g.)
bootMenuURL: { type: 'string', "default": '' }, // show bootMenu if empty, or autosend key to bootMenu (key '1' e.g.)
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
},
init: function(){
this.el.object3D.visible = false
if( window.innerWidth < this.data.width ){
this.data.maximized = true
}
this.calculateDimension()
this.initHud()
this.setupPasteDrop()
this.setupEvents()
console.log(this.data.iso)
fetch(this.data.iso,{method: 'HEAD'})
.then( (res) => {
const size = res.headers.get('Content-Length');
console.dir({res,size})
debugger
if( res.status != 200 ) throw 'not found'
})
.catch( (e) => {
console.warn(this.data.iso+" could not be loaded, loading fallback ISO URL:")
console.warn(this.schema.iso.default)
this.data.iso = this.schema.iso.default
})
.finally( () => this.initTerminal(true) )
},
requires:{
com: "com/dom.js",
window: "com/window.js",
pastedrop: "com/pastedrop.js",
v86: "com/isoterminal/libv86.js",
// allow xrsh to selfcontain scene + itself
xhook: "com/lib/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",
},
dom: {
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>`,
css: (me) => `
.isoterminal{
padding: ${me.com.data.padding}px;
width:100%;
height:99%;
resize: both;
overflow: hidden;
}
@font-face {
font-family: 'Cousine';
font-style: normal;
font-weight: 400;
src: url(./com/isoterminal/assets/Cousine.ttf) format('truetype');
}
@font-face {
font-family: 'Cousine';
font-style: normal;
font-weight: 700;
src: url(./com/isoterminal/assets/CousineBold.ttf) format('truetype');
}
.isoterminal *{
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{
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;
overflow:hidden;
}
.XR .wb-body:has(> .isoterminal){
background: transparent;
}
.XR .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;
}
@keyframes fade {
from { opacity: 1.0; }
50% { opacity: 0.5; }
to { opacity: 1.0; }
}
@-webkit-keyframes fade {
from { opacity: 1.0; }
50% { opacity: 0.5; }
to { opacity: 1.0; }
}
.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)
let features = { // ISOTerminal plugins
boot: "com/isoterminal/feat/boot.js",
javascript: "com/isoterminal/feat/javascript.js",
jsconsole: "com/isoterminal/feat/jsconsole.js",
remotekeyboard: "com/isoterminal/feat/remotekeyboard.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)
this.el.setAttribute("selfcontainer","")
// *DISABLED* instance this component
// rason: we only need one term for now (more = too cpu heavy)
let instance
if( singleton ){
instance = this.el
}else{
if( this.instance ){
const el = document.querySelector('.isoterminal')
return console.warn('TODO: allow multiple terminals for future beefy devices(see v86 examples)')
}
instance = this.instance = this.el.cloneNode(false)
this.el.sceneEl.appendChild( instance )
}
// init isoterminal
this.term = 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; grabbable: components.html.el.object3D.children.${this.el.children.length}`)
})
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)
})
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")
if( this.data.debug ) this.runTests()
})
this.term.addEventListener('status', function(e){
let msg = e.detail
const w = instance.winbox
if(!w) return
w.titleBak = w.titleBak || w.title
w.setTitle( `${w.titleBak} [${msg}]` )
})
instance.addEventListener('window.onclose', (e) => {
if( !confirm('do you want to kill this virtual machine and all its processes?') ) e.halt = true
})
const resize = (w,h) => { }
instance.addEventListener('window.onresize', resize )
instance.addEventListener('window.onmaximize', resize )
const focus = (e) => {
this.el.emit('focus',e.detail)
}
this.el.addEventListener('obbcollisionstarted', focus )
this.el.sceneEl.addEventListener('exit-vr', focus )
this.el.sceneEl.addEventListener('exit-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
},
setupEvents: function(){
this.el.addEventListener('exec', (e) => this.term.exec( e.detail ) )
this.el.addEventListener('hook', (e) => this.term.hook( e.detail[0], e.detail[1] ) )
this.el.addEventListener('send', (e) => this.term.send( e.detail[0], e.detail[1] || 0 ) )
this.el.addEventListener('create_file', async (e) => await this.term.worker.create_file( e.detail[0], this.term.convert.toUint8Array(e.detail[1]) ) )
this.el.addEventListener('update_file', async (e) => await this.term.worker.update_file( e.detail[0], this.term.convert.toUint8Array(e.detail[1]) ) )
this.el.addEventListener('append_file', async (e) => await this.term.worker.append_file( e.detail[0], this.term.convert.toUint8Array(e.detail[1]) ) )
this.el.addEventListener('read_file', async (e) => {
const buf = await this.term.worker.read_file( e.detail[0] )
const str = new TextDecoder().decode(buf)
if( typeof e.detail[1] == 'function' ) e.detail[1](str)
else console.log(str)
})
},
events:{
// combined AFRAME+DOM reactive events
click: function(e){ }, //
keydown: function(e){ },
// reactive events for this.data updates
myvalue: function(e){ this.el.dom.querySelector('b').innerText = this.data.myvalue },
launcher: async function(){
if( !this.term.instance ){
this.initTerminal()
}else{
// toggle visibility
this.el.winbox[ this.el.winbox.min ? 'restore' : 'minimize' ]()
}
}
},
manifest: { // HTML5 manifest to identify app to xrsh
"iso": "linux-x64-4.15.iso",
"short_name": "ISOTerm",
"name": "terminal",
"icons": [
{
"src": "https://css.gg/terminal.svg",
"src": "data:image/svg+xml;base64,PHN2ZwogIHdpZHRoPSIyNCIKICBoZWlnaHQ9IjI0IgogIHZpZXdCb3g9IjAgMCAyNCAyNCIKICBmaWxsPSJub25lIgogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKPgogIDxwYXRoCiAgICBkPSJNNS4wMzMzIDE0LjgyODRMNi40NDc1MSAxNi4yNDI2TDEwLjY5MDIgMTJMNi40NDc1MSA3Ljc1NzMzTDUuMDMzMyA5LjE3MTU1TDcuODYxNzIgMTJMNS4wMzMzIDE0LjgyODRaIgogICAgZmlsbD0iY3VycmVudENvbG9yIgogIC8+CiAgPHBhdGggZD0iTTE1IDE0SDExVjE2SDE1VjE0WiIgZmlsbD0iY3VycmVudENvbG9yIiAvPgogIDxwYXRoCiAgICBmaWxsLXJ1bGU9ImV2ZW5vZGQiCiAgICBjbGlwLXJ1bGU9ImV2ZW5vZGQiCiAgICBkPSJNMiAyQzAuODk1NDMxIDIgMCAyLjg5NTQzIDAgNFYyMEMwIDIxLjEwNDYgMC44OTU0MyAyMiAyIDIySDIyQzIzLjEwNDYgMjIgMjQgMjEuMTA0NiAyNCAyMFY0QzI0IDIuODk1NDMgMjMuMTA0NiAyIDIyIDJIMlpNMjIgNEgyTDIgMjBIMjJWNFoiCiAgICBmaWxsPSJjdXJyZW50Q29sb3IiCiAgLz4KPC9zdmc+",
"type": "image/svg+xml",
"sizes": "512x512"
}
],
"id": "/?source=pwa",
"start_url": "/?source=pwa",
"background_color": "#3367D6",
"display": "standalone",
"scope": "/",
"theme_color": "#3367D6",
"shortcuts": [
{
"name": "What is the latest news?",
"cli":{
"usage": "helloworld <type> [options]",
"example": "helloworld news",
"args":{
"--latest": {type:"string"}
}
},
"short_name": "Today",
"description": "View weather information for today",
"url": "/today?source=pwa",
"icons": [{ "src": "/images/today.png", "sizes": "192x192" }]
}
],
"description": "Hello world information",
"screenshots": [
{
"src": "/images/screenshot1.png",
"type": "image/png",
"sizes": "540x720",
"form_factor": "narrow"
}
],
"help":`
Helloworld application
This is a help file which describes the application.
It will be rendered thru troika text, and will contain
headers based on non-punctualized lines separated by linebreaks,
in above's case "\nHelloworld application\n" will qualify as header.
`
}
});
}
/**
* ## [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/>
* ```
*
*/
if( !AFRAME.components.data2event ){
AFRAME.registerComponent('data2event',{
init: function(){
setTimeout( () => {
for( let i in this.el.components ){
let com = this.el.components[i]
if( typeof com.data == 'object' ){
com.data = this.reactify( this.el, com.data)
}
}
},50)
},
reactify: function(el,data){
return new Proxy(data, {
get(me,k,v) {
return me[k]
},
set(me,k,v){
me[k] = v
el.emit(k,{el,k,v})
}
})
},
})
}
/*
* ## [dom](com/dom.js)
*
* instances reactive DOM component from AFRAME component's `dom` metadata
*
* ```html
* <script>
* AFRAME.registerComponent('mycom',{
* init: function(){ this.data.foo = 1 },
* dom: {
* scale: 3,
* events: ['click'],
* html: (me) => `<div id="iconmenu">${me.data.foo}</div>`,
* css: (me) => `#iconmenu { display:block }`
* },
* event: {
* click: (e) => alert(e),
* foo: (e) => alert("I was updated!")
* }
* })
* </script>
*
* <a-entity mycom dom/>
* ```
*
* | property | type | example |
* |--------------|--------------------|----------------------------------------------------------------------------------------|
* | `com` | `array` of strings | <a-entity app="app/launcher.js; registers: https://foo.com/index.json, ./index.json"/> |
*
* | event | target | info |
* |--------------|-------------------------------------------------------------------------------------------------------------|
* | `DOMready` | self | fired when dom component (`this.dom`) is created |
*/
if( !AFRAME.components.dom ){
AFRAME.registerComponent('dom',{
init: function(){
Object.values(this.el.components)
.map( (c) => {
if( c.dom && c.attrName != "dom"){
this.dom = c.dom
this.com = c
}
})
if( !this.dom || !this.com){
return console.warn('dom.js did not find a .dom object inside component')
}
this
.ensureOverlay()
.addCSS()
.createReactiveDOMElement()
.assignUniqueID()
.scaleDOMvsXR()
.triggerKeyboardForInputs()
document.querySelector('#overlay').appendChild(this.el.dom)
this.el.emit('DOMready',{el: this.el.dom})
},
ensureOverlay: function(){
// ensure overlay
let overlay = document.querySelector('#overlay')
if( !overlay ){
overlay = document.createElement('div')
overlay.id = "overlay"
overlay.setAttribute('style','position:fixed;top:0px;left:0px;right:0px;bottom:0px;pointer-events:none')
document.body.appendChild(overlay)
document.head.innerHTML += `
<style type="text/css">#overlay * { pointer-events:all }</style>
`
// sceneEl.setAttribute("webxr","overlayElement:#overlay")
}
return this
},
reactify: function(el,data){
return new Proxy(data, {
get(me,k,v) {
return me[k]
},
set(me,k,v){
me[k] = v
el.emit(k,{el,k,v})
}
})
},
// creates el.dom (the 2D DOM object)
createReactiveDOMElement: function(){
this.el.dom = document.createElement('div')
this.el.dom.innerHTML = this.dom.html(this)
this.el.dom.className = this.dom.attrName
this.com.data = this.reactify( this.el, this.com.data )
this.el.dom = this.el.dom.children[0]
if( this.dom.events ){
this.dom.events.map( (e) => {
this.el.dom.addEventListener(e, (ev) => this.el.emit(e,ev) )
})
}
return this
},
assignUniqueID: function(){
// assign unique app id so it's easy to reference (by html-mesh component e.g.)
if( !this.el.uid ) this.el.uid = this.el.dom.id = '_'+String(Math.random()).substr(10)
return this
},
addCSS: function(){
if( this.dom.css && !document.head.querySelector(`style#${this.com.attrName}`) ){
document.head.innerHTML += `<style id="${this.com.attrName}">${this.dom.css(this)}</style>`
}
return this
},
scaleDOMvsXR: function(){
if( this.dom.scale ) this.el.setAttribute('scale',`${this.dom.scale} ${this.dom.scale} ${this.dom.scale}`)
return this
},
triggerKeyboardForInputs: function(){
// https://developer.oculus.com/documentation/web/webxr-keyboard ;
[...this.el.dom.querySelectorAll('[type=text]')].map( (input) => {
let triggerKeyboard = function(){
this.focus()
console.log("focus")
}
input.addEventListener('click', triggerKeyboard )
})
return this
},
})
}
/**
* ## [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"},
doublesided: {type: "boolean", default: true},
faceuser: { type: "boolean", default: false}
},
dependencies:{
html: "com/lib/aframe-html.js"
},
init: async function () {
let el = document.querySelector(this.data.domid)
if( ! el ){
return console.error("html-as-texture-in-xr: cannot get dom element "+this.data.domid)
}
let s = await AFRAME.utils.require(this.dependencies)
this.forwardClickToMesh();
this.el.sceneEl.addEventListener('enter-vr', () => this.enableDoubleSided() )
this.el.setAttribute("html",`html: ${this.data.domid}; cursor:#cursor; `)
this.el.setAttribute("visible", AFRAME.utils.XD() == '3D' ? 'true' : 'false' )
if( this.data.faceuser ){
this.el.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.4) )
}
},
forwardClickToMesh: function(){
// monkeypatch: forward click to mesh
const handle = AFRAME.components['html'].Component.prototype.handle
AFRAME.components['html'].Component.prototype.handle = function(type,evt){
if( !this.el.sceneEl.renderer.xr.isPresenting ) return // ignore events in desktop mode
if( this.el.sceneEl.renderer.xr.isPresenting && type.match(/^mouse/) ) return // ignore mouse-events in XR
if( type == 'click' && evt.detail.length && evt.detail[0].uv ){
const mesh = this.el.object3D.children[0]
const uv = evt.detail[0].uv;
const _pointer = new THREE.Vector2();
const _event = { type: '', data: _pointer };
_event.type = type;
_event.data.set( uv.x, 1 - uv.y );
mesh.dispatchEvent( _event );
}
return handle.apply(this,[type,evt])
}
},
enableDoubleSided: function(){
// enable doubleside
this.el.object3D.traverse( (o) => {
if( o.constructor && String(o.constructor).match(/HTMLMesh/) ){
o.material.side = THREE.DoubleSide
}
})
}
});
AFRAME.utils.XD = function(){
return document.body.classList.contains('XR') ? '3D' : '2D'
}
AFRAME.utils.XD.toggle = function(state){
state = state != undefined ? state : state || !document.body.className.match(/XR/)
document.body.classList[ state ? 'add' : 'remove'](['XR'])
AFRAME.scenes[0].emit( state ? '3D' : '2D')
}
AFRAME.utils.XD.getPositionInFrontOfCamera = function(distance){
const camera = AFRAME.scenes[0].camera;
let pos = new THREE.Vector3()
let direction = new THREE.Vector3();
// Get camera's forward direction (without rotation)
camera.getWorldDirection(direction);
camera.getWorldPosition(pos)
direction.normalize();
// Scale the direction by 1 meter
if( !distance ) distance = 1.5
direction.multiplyScalar(distance);
// Add the camera's position to the scaled direction to get the target point
pos.add(direction);
return pos
}
AFRAME.registerSystem('html-as-texture-in-xr',{
init: function(){
this.sceneEl.addEventListener('enter-vr',() => AFRAME.utils.XD.toggle(true) )
this.sceneEl.addEventListener('exit-vr', () => AFRAME.utils.XD.toggle(false) )
this.sceneEl.addEventListener('2D', () => this.showElements(false) )
this.sceneEl.addEventListener('3D', () => this.showElements(true) )
document.head.innerHTML += `<style type="text/css">
.XR #toggle_overlay{
background: transparent;
color: #3aacff;
}
/*
.XR #overlay{
visibility: hidden;
}
*/
</style>`
},
showElements: function(state){
let els = [...document.querySelectorAll('[html-as-texture-in-xr]')]
els = els.filter( (el) => el != this.el ? el : null ) // filter out self
els.map( (el) => el.setAttribute("visible", state ? true : false ) )
}
})
}
/**
* ## [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.
`
}
});
/**
* ## [require](com/require('').js)
*
* automatically requires dependencies or missing components
*
* ```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.)
*/
AFRAME.utils.require = function(arr_or_obj,opts){
opts = opts || {}
let i = 0
let deps = []
let packagesArr = arr_or_obj.map ? arr_or_obj : Object.values(arr_or_obj)
const parseURI = function(uri){
return {
id: String(uri).split("/").pop(), // 'a/b/c/mycom.js' => 'mycom.js'
component: String(uri).split("/").pop().replace(/\..*/,''), // 'mycom.js' => 'mycom'
type: String(uri).split(".").pop() // 'mycom.js' => 'js'
}
}
packagesArr.map( (package) => {
try{
package = package.match(/\./) ? package : AFRAME.utils.require.baseURL+package+".js"
let id = Object.keys(arr_or_obj)[i++]
if( id.match(/^[0-9]/) ){ // if AFRAME component.dependency -array was passed
id = parseURI(package).component
}
// prevent duplicate requests
if( AFRAME.utils.require.required[id] ) return // already loaded before
AFRAME.utils.require.required[id] = true
if( !document.body.querySelector(`script#${id}`) &&
!document.body.querySelector(`link#${id}`) ){
let {type} = parseURI(package)
let p = new Promise( (resolve,reject) => {
switch(type){
case "js": let script = document.createElement("script")
script.id = id
script.onload = () => setTimeout( () => resolve(id), 50 )
script.onerror = (e) => reject(e)
script.src = package
document.body.appendChild(script)
break;
case "css": let link = document.createElement("link")
link.id = id
link.href = package
link.rel = 'stylesheet'
link.onload = () => setTimeout( () => resolve(id), 50 )
link.onerror = (e) => reject(e)
document.body.appendChild(link)
break;
}
})
deps.push(p)
}
}catch(e){
console.error(`package ${package} could not be retrieved..aborting :(`);
console.dir(e)
if( opts.halt ) throw e;
}
})
return Promise.all(deps)
}
AFRAME.utils.require.required = {}
AFRAME.utils.require.baseURL = './com/'
//// this component will scan the DOM for missing components and lazy load them
//AFRAME.registerSystem('require',{
//
// init: function(){
// this.components = []
// // observe HTML changes in <a-scene>
// observer = new MutationObserver( (a,b) => this.getMissingComponents(a,b) )
// observer.observe( this.sceneEl, {characterData: false, childList: true, attributes: false});
// },
//
// getMissingComponents: function(mutationsList,observer){
// let els = [...this.sceneEl.getElementsByTagName("*")]
// let seen = []
//
// els.map( async (el) => {
// let attrs = el.getAttributeNames()
// .filter( (a) => a.match(/(^aframe-injected|^data-aframe|^id$|^class$|^on)/) ? null : a )
// for( let attr in attrs ){
// let component = attrs[attr]
// if( el.components && !el.components[component] ){
// console.info(`require.js: lazy-loading missing <${el.tagName.toLowerCase()} ${component} ... > (TODO: fix selectors in schema)`)
// // require && remount
// try{
// await AFRAME.utils.require([component])
// el.removeAttribute(component)
// el.setAttribute(component, el.getAttribute(component) )
// }catch(e){ } // give up, normal AFRAME behaviour follows
// }
// }
// })
// }
//
//})
/**
* ## [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"},
width: {type:'string'}, // wrap
height: {type:'string',"default":'260px'},
uid: {type:'string'},
attach: {type:'selector'},
grabbable: {type:'string', "default":"components.html.el.object3D.children.0"},
dom: {type:'selector'},
max: {type:'boolean',"default":false},
min: {type:'boolean',"default":false},
x: {type:'string',"default":"center"},
y: {type:'string',"default":"center"},
"class": {type:'array',"default":[]},
autoresize:{type:'bool', "default": false}
},
dependencies:{
dom: "com/dom.js",
winboxjs: "assets/winbox.bundle.min.js", // deadsimple windows: https://nextapps-de.github.io/winbox
},
init: function(){
setTimeout( () => this.setupWindow(), 10 ) // init after other components
},
setupWindow: async function(){
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,
y: this.data.y,
id: this.data.uid || String(Math.random()).substr(4), // important hint for html-mesh
root: this.data.attach || document.body,
mount: this.data.dom,
max: this.data.max,
min: this.data.min,
onresize: () => this.el.emit('window.onresize',{}),
onmaximize: () => this.el.emit('window.onmaximize',{}),
oncreate: (e) => {
this.el.emit('window.oncreate',{})
// resize after the dom content has been rendered & updated
setTimeout( () => {
if( !this.data.max && this.data.autoresize ) winbox.resize( this.el.dom.offsetWidth+'px', this.el.dom.offsetHeight+'px' )
// hint grabbable's obb-collider to track the window-object
this.el.components['obb-collider'].data.trackedObject3D = this.data.grabbable
this.el.components['obb-collider'].update()
},1000)
},
onclose: close
});
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 ){
if( lastNeighbor == null || object == null) return
// *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
}
;
(function (three) {
'use strict';
// This is a copy of https://github.com/mrdoob/three.js/blob/0403020848c26a9605eb91c99a949111ad4a532e/examples/jsm/interactive/HTMLMesh.js
class HTMLMesh extends three.Mesh {
constructor( dom ) {
const texture = new HTMLTexture( dom );
const geometry = new three.PlaneGeometry( texture.image.width * 0.001, texture.image.height * 0.001 );
const material = new three.MeshBasicMaterial( { map: texture, toneMapped: false, transparent: true } );
super( geometry, material );
function onEvent( event ) {
material.map.dispatchDOMEvent( event );
}
this.addEventListener( 'mousedown', onEvent );
this.addEventListener( 'mousemove', onEvent );
this.addEventListener( 'mouseup', onEvent );
this.addEventListener( 'click', onEvent );
this.dispose = function () {
geometry.dispose();
material.dispose();
material.map.dispose();
canvases.delete( dom );
this.removeEventListener( 'mousedown', onEvent );
this.removeEventListener( 'mousemove', onEvent );
this.removeEventListener( 'mouseup', onEvent );
this.removeEventListener( 'click', onEvent );
};
}
}
class HTMLTexture extends three.CanvasTexture {
constructor( dom ) {
super( html2canvas( dom ) );
this.dom = dom;
this.anisotropy = 16;
this.encoding = three.sRGBEncoding;
this.minFilter = three.LinearFilter;
this.magFilter = three.LinearFilter;
// Create an observer on the DOM, and run html2canvas update in the next loop
const observer = new MutationObserver( () => {
if ( ! this.scheduleUpdate ) {
// ideally should use xr.requestAnimationFrame, here setTimeout to avoid passing the renderer
this.scheduleUpdate = setTimeout( () => this.update(), 16 );
}
} );
const config = { attributes: true, childList: true, subtree: true, characterData: true };
observer.observe( dom, config );
this.observer = observer;
}
dispatchDOMEvent( event ) {
if ( event.data ) {
htmlevent( this.dom, event.type, event.data.x, event.data.y );
}
}
update() {
this.image = html2canvas( this.dom );
this.needsUpdate = true;
this.scheduleUpdate = null;
}
dispose() {
if ( this.observer ) {
this.observer.disconnect();
}
this.scheduleUpdate = clearTimeout( this.scheduleUpdate );
super.dispose();
}
}
//
const canvases = new WeakMap();
function html2canvas( element ) {
const range = document.createRange();
const color = new three.Color();
function Clipper( context ) {
const clips = [];
let isClipping = false;
function doClip() {
if ( isClipping ) {
isClipping = false;
context.restore();
}
if ( clips.length === 0 ) return;
let minX = - Infinity, minY = - Infinity;
let maxX = Infinity, maxY = Infinity;
for ( let i = 0; i < clips.length; i ++ ) {
const clip = clips[ i ];
minX = Math.max( minX, clip.x );
minY = Math.max( minY, clip.y );
maxX = Math.min( maxX, clip.x + clip.width );
maxY = Math.min( maxY, clip.y + clip.height );
}
context.save();
context.beginPath();
context.rect( minX, minY, maxX - minX, maxY - minY );
context.clip();
isClipping = true;
}
return {
add: function ( clip ) {
clips.push( clip );
doClip();
},
remove: function () {
clips.pop();
doClip();
}
};
}
function drawText( style, x, y, string ) {
if ( string !== '' ) {
if ( style.textTransform === 'uppercase' ) {
string = string.toUpperCase();
}
context.font = style.fontWeight + ' ' + style.fontSize + ' ' + style.fontFamily;
context.textBaseline = 'top';
context.fillStyle = style.color;
context.fillText( string, x, y + parseFloat( style.fontSize ) * 0.1 );
}
}
function buildRectPath( x, y, w, h, r ) {
if ( w < 2 * r ) r = w / 2;
if ( h < 2 * r ) r = h / 2;
context.beginPath();
context.moveTo( x + r, y );
context.arcTo( x + w, y, x + w, y + h, r );
context.arcTo( x + w, y + h, x, y + h, r );
context.arcTo( x, y + h, x, y, r );
context.arcTo( x, y, x + w, y, r );
context.closePath();
}
function drawBorder( style, which, x, y, width, height ) {
const borderWidth = style[ which + 'Width' ];
const borderStyle = style[ which + 'Style' ];
const borderColor = style[ which + 'Color' ];
if ( borderWidth !== '0px' && borderStyle !== 'none' && borderColor !== 'transparent' && borderColor !== 'rgba(0, 0, 0, 0)' ) {
context.strokeStyle = borderColor;
context.lineWidth = parseFloat( borderWidth );
context.beginPath();
context.moveTo( x, y );
context.lineTo( x + width, y + height );
context.stroke();
}
}
function drawElement( element, style ) {
let x = 0, y = 0, width = 0, height = 0;
if ( element.nodeType === Node.TEXT_NODE ) {
// text
range.selectNode( element );
const rect = range.getBoundingClientRect();
x = rect.left - offset.left - 0.5;
y = rect.top - offset.top - 0.5;
width = rect.width;
height = rect.height;
drawText( style, x, y, element.nodeValue.trim() );
} else if ( element.nodeType === Node.COMMENT_NODE ) {
return;
} else if ( element instanceof HTMLCanvasElement ) {
// Canvas element
if ( element.style.display === 'none' ) return;
const rect = element.getBoundingClientRect();
x = rect.left - offset.left - 0.5;
y = rect.top - offset.top - 0.5;
context.save();
const dpr = window.devicePixelRatio;
context.scale( 1 / dpr, 1 / dpr );
context.drawImage( element, x, y );
context.restore();
} else if ( element instanceof HTMLImageElement ) {
if ( element.style.display === 'none' ) return;
const rect = element.getBoundingClientRect();
x = rect.left - offset.left - 0.5;
y = rect.top - offset.top - 0.5;
width = rect.width;
height = rect.height;
context.drawImage( element, x, y, width, height );
} else {
if ( element.style.display === 'none' ) return;
const rect = element.getBoundingClientRect();
x = rect.left - offset.left - 0.5;
y = rect.top - offset.top - 0.5;
width = rect.width;
height = rect.height;
style = window.getComputedStyle( element );
// Get the border of the element used for fill and border
buildRectPath( x, y, width, height, parseFloat( style.borderRadius ) );
const backgroundColor = style.backgroundColor;
if ( backgroundColor !== 'transparent' && backgroundColor !== 'rgba(0, 0, 0, 0)' ) {
context.fillStyle = backgroundColor;
context.fill();
}
// If all the borders match then stroke the round rectangle
const borders = [ 'borderTop', 'borderLeft', 'borderBottom', 'borderRight' ];
let match = true;
let prevBorder = null;
for ( const border of borders ) {
if ( prevBorder !== null ) {
match = ( style[ border + 'Width' ] === style[ prevBorder + 'Width' ] ) &&
( style[ border + 'Color' ] === style[ prevBorder + 'Color' ] ) &&
( style[ border + 'Style' ] === style[ prevBorder + 'Style' ] );
}
if ( match === false ) break;
prevBorder = border;
}
if ( match === true ) {
// They all match so stroke the rectangle from before allows for border-radius
const width = parseFloat( style.borderTopWidth );
if ( style.borderTopWidth !== '0px' && style.borderTopStyle !== 'none' && style.borderTopColor !== 'transparent' && style.borderTopColor !== 'rgba(0, 0, 0, 0)' ) {
context.strokeStyle = style.borderTopColor;
context.lineWidth = width;
context.stroke();
}
} else {
// Otherwise draw individual borders
drawBorder( style, 'borderTop', x, y, width, 0 );
drawBorder( style, 'borderLeft', x, y, 0, height );
drawBorder( style, 'borderBottom', x, y + height, width, 0 );
drawBorder( style, 'borderRight', x + width, y, 0, height );
}
if ( element instanceof HTMLInputElement ) {
let accentColor = style.accentColor;
if ( accentColor === undefined || accentColor === 'auto' ) accentColor = style.color;
color.set( accentColor );
const luminance = Math.sqrt( 0.299 * ( color.r ** 2 ) + 0.587 * ( color.g ** 2 ) + 0.114 * ( color.b ** 2 ) );
const accentTextColor = luminance < 0.5 ? 'white' : '#111111';
if ( element.type === 'radio' ) {
buildRectPath( x, y, width, height, height );
context.fillStyle = 'white';
context.strokeStyle = accentColor;
context.lineWidth = 1;
context.fill();
context.stroke();
if ( element.checked ) {
buildRectPath( x + 2, y + 2, width - 4, height - 4, height );
context.fillStyle = accentColor;
context.strokeStyle = accentTextColor;
context.lineWidth = 2;
context.fill();
context.stroke();
}
}
if ( element.type === 'checkbox' ) {
buildRectPath( x, y, width, height, 2 );
context.fillStyle = element.checked ? accentColor : 'white';
context.strokeStyle = element.checked ? accentTextColor : accentColor;
context.lineWidth = 1;
context.stroke();
context.fill();
if ( element.checked ) {
const currentTextAlign = context.textAlign;
context.textAlign = 'center';
const properties = {
color: accentTextColor,
fontFamily: style.fontFamily,
fontSize: height + 'px',
fontWeight: 'bold'
};
drawText( properties, x + ( width / 2 ), y, '✔' );
context.textAlign = currentTextAlign;
}
}
if ( element.type === 'range' ) {
const [ min, max, value ] = [ 'min', 'max', 'value' ].map( property => parseFloat( element[ property ] ) );
const position = ( ( value - min ) / ( max - min ) ) * ( width - height );
buildRectPath( x, y + ( height / 4 ), width, height / 2, height / 4 );
context.fillStyle = accentTextColor;
context.strokeStyle = accentColor;
context.lineWidth = 1;
context.fill();
context.stroke();
buildRectPath( x, y + ( height / 4 ), position + ( height / 2 ), height / 2, height / 4 );
context.fillStyle = accentColor;
context.fill();
buildRectPath( x + position, y, height, height, height / 2 );
context.fillStyle = accentColor;
context.fill();
}
if ( element.type === 'color' || element.type === 'text' || element.type === 'number' ) {
clipper.add( { x: x, y: y, width: width, height: height } );
drawText( style, x + parseInt( style.paddingLeft ), y + parseInt( style.paddingTop ), element.value );
clipper.remove();
}
}
}
/*
// debug
context.strokeStyle = '#' + Math.random().toString( 16 ).slice( - 3 );
context.strokeRect( x - 0.5, y - 0.5, width + 1, height + 1 );
*/
const isClipping = style.overflow === 'auto' || style.overflow === 'hidden';
if ( isClipping ) clipper.add( { x: x, y: y, width: width, height: height } );
for ( let i = 0; i < element.childNodes.length; i ++ ) {
drawElement( element.childNodes[ i ], style );
}
if ( isClipping ) clipper.remove();
}
const offset = element.getBoundingClientRect();
let canvas = canvases.get( element );
if ( canvas === undefined ) {
canvas = document.createElement( 'canvas' );
canvas.width = offset.width;
canvas.height = offset.height;
canvases.set( element, canvas );
}
const context = canvas.getContext( '2d'/*, { alpha: false }*/ );
const clipper = new Clipper( context );
// console.time( 'drawElement' );
context.clearRect(0, 0, canvas.width, canvas.height);
drawElement( element );
// console.timeEnd( 'drawElement' );
return canvas;
}
function htmlevent( element, event, x, y ) {
const mouseEventInit = {
clientX: ( x * element.offsetWidth ) + element.offsetLeft,
clientY: ( y * element.offsetHeight ) + element.offsetTop,
view: element.ownerDocument.defaultView
};
// TODO: Find out why this is added. Keep commented out when this file is updated
// window.dispatchEvent( new MouseEvent( event, mouseEventInit ) );
const rect = element.getBoundingClientRect();
x = x * rect.width + rect.left;
y = y * rect.height + rect.top;
function traverse( element ) {
if ( element.nodeType !== Node.TEXT_NODE && element.nodeType !== Node.COMMENT_NODE ) {
const rect = element.getBoundingClientRect();
if ( x > rect.left && x < rect.right && y > rect.top && y < rect.bottom ) {
element.dispatchEvent( new MouseEvent( event, mouseEventInit ) );
if ( element instanceof HTMLInputElement && element.type === 'range' && ( event === 'mousedown' || event === 'click' ) ) {
const [ min, max ] = [ 'min', 'max' ].map( property => parseFloat( element[ property ] ) );
const width = rect.width;
const offsetX = x - rect.x;
const proportion = offsetX / width;
element.value = min + ( max - min ) * proportion;
element.dispatchEvent( new InputEvent( 'input', { bubbles: true } ) );
}
}
for ( let i = 0; i < element.childNodes.length; i ++ ) {
traverse( element.childNodes[ i ] );
}
}
}
traverse( element );
}
/* jshint esversion: 9, -W097 */
const schemaHTML = {
html: {
type: 'selector',
},
cursor: {
type: 'selector',
}
};
{
schemaHTML.html.description = `HTML element to use.`;
schemaHTML.cursor.description = `Visual indicator for where the user is currently pointing`;
}
const _pointer = new THREE.Vector2();
const _event = { type: '', data: _pointer };
AFRAME.registerComponent('html', {
schema: schemaHTML,
init() {
this.rerender = this.rerender.bind(this);
this.handle = this.handle.bind(this);
this.onClick = e => this.handle('click', e);
this.onMouseLeave = e => this.handle('mouseleave', e);
this.onMouseEnter = e => this.handle('mouseenter', e);
this.onMouseUp = e => this.handle('mouseup', e);
this.onMouseDown = e => this.handle('mousedown', e);
this.mouseMoveDetail = {
detail: {
cursorEl: null,
intersection: null
}
};
},
play() {
this.el.addEventListener('click', this.onClick);
this.el.addEventListener('mouseleave', this.onMouseLeave);
this.el.addEventListener('mouseenter', this.onMouseEnter);
this.el.addEventListener('mouseup', this.onMouseUp);
this.el.addEventListener('mousedown', this.onMouseDown);
},
pause() {
this.el.removeEventListener('click', this.onClick);
this.el.removeEventListener('mouseleave', this.onMouseLeave);
this.el.removeEventListener('mouseenter', this.onMouseEnter);
this.el.removeEventListener('mouseup', this.onMouseUp);
this.el.removeEventListener('mousedown', this.onMouseDown);
},
update() {
this.remove();
if (!this.data.html) return;
const mesh = new HTMLMesh(this.data.html);
this.el.setObject3D('html', mesh);
this.data.html.addEventListener('input', this.rerender);
this.data.html.addEventListener('change', this.rerender);
this.cursor = this.data.cursor ? this.data.cursor.object3D : null;
},
tick() {
if (this.activeRaycaster) {
const intersection = this.activeRaycaster.components.raycaster.getIntersection(this.el);
this.mouseMoveDetail.detail.cursorEl = this.activeRaycaster;
this.mouseMoveDetail.detail.intersection = intersection;
this.handle('mousemove', this.mouseMoveDetail);
}
},
handle(type, evt) {
const intersection = evt.detail.intersection;
const raycaster = evt.detail.cursorEl;
if (type === 'mouseenter') {
this.activeRaycaster = raycaster;
}
if (type === 'mouseleave' && this.activeRaycaster === raycaster) {
this.activeRaycaster = null;
}
if (this.cursor) this.cursor.visible = false;
if (intersection) {
const mesh = this.el.getObject3D('html');
const uv = intersection.uv;
_event.type = type;
_event.data.set( uv.x, 1 - uv.y );
mesh.dispatchEvent( _event );
if (this.cursor) {
this.cursor.visible = true;
this.cursor.parent.worldToLocal(this.cursor.position.copy(intersection.point));
}
}
},
rerender() {
const mesh = this.el.getObject3D('html');
if (mesh && !mesh.material.map.scheduleUpdate) {
mesh.material.map.scheduleUpdate = setTimeout( () => mesh.material.map.update(), 16 );
}
},
remove() {
const mesh = this.el.getObject3D('html');
if (mesh) {
this.el.removeObject3D('html');
this.data.html.removeEventListener('input', this.rerender);
this.data.html.removeEventListener('change', this.rerender);
mesh.dispose();
}
this.activeRaycaster = null;
this.mouseMoveDetail.detail.cursorEl = null;
this.mouseMoveDetail.detail.intersection = null;
this.cursor = null;
},
});
})(THREE);
//XHook - v1.6.2 - https://github.com/jpillora/xhook
//Jaime Pillora <dev@jpillora.com> - MIT Copyright 2023
var xhook=function(){"use strict";const e=(e,t)=>Array.prototype.slice.call(e,t);let t=null;"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?t=self:"undefined"!=typeof global?t=global:window&&(t=window);const n=t,o=t.document,r=["load","loadend","loadstart"],s=["progress","abort","error","timeout"],a=e=>["returnValue","totalSize","position"].includes(e),i=function(e,t){for(let n in e){if(a(n))continue;const o=e[n];try{t[n]=o}catch(e){}}return t},c=function(e,t,n){const o=e=>function(o){const r={};for(let e in o){if(a(e))continue;const s=o[e];r[e]=s===t?n:s}return n.dispatchEvent(e,r)};for(let r of Array.from(e))n._has(r)&&(t[`on${r}`]=o(r))},u=function(e){if(o&&null!=o.createEventObject){const t=o.createEventObject();return t.type=e,t}try{return new Event(e)}catch(t){return{type:e}}},l=function(t){let n={};const o=e=>n[e]||[],r={addEventListener:function(e,t,r){n[e]=o(e),n[e].indexOf(t)>=0||(r=void 0===r?n[e].length:r,n[e].splice(r,0,t))},removeEventListener:function(e,t){if(void 0===e)return void(n={});void 0===t&&(n[e]=[]);const r=o(e).indexOf(t);-1!==r&&o(e).splice(r,1)},dispatchEvent:function(){const n=e(arguments),s=n.shift();t||(n[0]=i(n[0],u(s)),Object.defineProperty(n[0],"target",{writable:!1,value:this}));const a=r[`on${s}`];a&&a.apply(r,n);const c=o(s).concat(o("*"));for(let e=0;e<c.length;e++){c[e].apply(r,n)}},_has:e=>!(!n[e]&&!r[`on${e}`])};return t&&(r.listeners=t=>e(o(t)),r.on=r.addEventListener,r.off=r.removeEventListener,r.fire=r.dispatchEvent,r.once=function(e,t){var n=function(){return r.off(e,n),t.apply(null,arguments)};return r.on(e,n)},r.destroy=()=>n={}),r};var f=function(e,t){switch(typeof e){case"object":return n=e,Object.entries(n).map((([e,t])=>`${e.toLowerCase()}: ${t}`)).join("\r\n");case"string":return function(e,t){const n=e.split("\r\n");null==t&&(t={});for(let e of n)if(/([^:]+):\s*(.+)/.test(e)){const e=null!=RegExp.$1?RegExp.$1.toLowerCase():void 0,n=RegExp.$2;null==t[e]&&(t[e]=n)}return t}(e,t)}var n;return[]};const d=l(!0),p=e=>void 0===e?null:e,h=n.XMLHttpRequest,y=function(){const e=new h,t={};let n,o,a,u=null;var y=0;const v=function(){if(a.status=u||e.status,-1!==u&&(a.statusText=e.statusText),-1===u);else{const t=f(e.getAllResponseHeaders());for(let e in t){const n=t[e];if(!a.headers[e]){const t=e.toLowerCase();a.headers[t]=n}}}},b=function(){x.status=a.status,x.statusText=a.statusText},g=function(){n||x.dispatchEvent("load",{}),x.dispatchEvent("loadend",{}),n&&(x.readyState=0)},E=function(e){for(;e>y&&y<4;)x.readyState=++y,1===y&&x.dispatchEvent("loadstart",{}),2===y&&b(),4===y&&(b(),"text"in a&&(x.responseText=a.text),"xml"in a&&(x.responseXML=a.xml),"data"in a&&(x.response=a.data),"finalUrl"in a&&(x.responseURL=a.finalUrl)),x.dispatchEvent("readystatechange",{}),4===y&&(!1===t.async?g():setTimeout(g,0))},m=function(e){if(4!==e)return void E(e);const n=d.listeners("after");var o=function(){if(n.length>0){const e=n.shift();2===e.length?(e(t,a),o()):3===e.length&&t.async?e(t,a,o):o()}else E(4)};o()};var x=l();t.xhr=x,e.onreadystatechange=function(t){try{2===e.readyState&&v()}catch(e){}4===e.readyState&&(o=!1,v(),function(){if(e.responseType&&"text"!==e.responseType)"document"===e.responseType?(a.xml=e.responseXML,a.data=e.responseXML):a.data=e.response;else{a.text=e.responseText,a.data=e.responseText;try{a.xml=e.responseXML}catch(e){}}"responseURL"in e&&(a.finalUrl=e.responseURL)}()),m(e.readyState)};const w=function(){n=!0};x.addEventListener("error",w),x.addEventListener("timeout",w),x.addEventListener("abort",w),x.addEventListener("progress",(function(t){y<3?m(3):e.readyState<=3&&x.dispatchEvent("readystatechange",{})})),"withCredentials"in e&&(x.withCredentials=!1),x.status=0;for(let e of Array.from(s.concat(r)))x[`on${e}`]=null;if(x.open=function(e,r,s,i,c){y=0,n=!1,o=!1,t.headers={},t.headerNames={},t.status=0,t.method=e,t.url=r,t.async=!1!==s,t.user=i,t.pass=c,a={},a.headers={},m(1)},x.send=function(n){let u,l;for(u of["type","timeout","withCredentials"])l="type"===u?"responseType":u,l in x&&(t[u]=x[l]);t.body=n;const f=d.listeners("before");var p=function(){if(!f.length)return function(){for(u of(c(s,e,x),x.upload&&c(s.concat(r),e.upload,x.upload),o=!0,e.open(t.method,t.url,t.async,t.user,t.pass),["type","timeout","withCredentials"]))l="type"===u?"responseType":u,u in t&&(e[l]=t[u]);for(let n in t.headers){const o=t.headers[n];n&&e.setRequestHeader(n,o)}e.send(t.body)}();const n=function(e){if("object"==typeof e&&("number"==typeof e.status||"number"==typeof a.status))return i(e,a),"data"in e||(e.data=e.response||e.text),void m(4);p()};n.head=function(e){i(e,a),m(2)},n.progress=function(e){i(e,a),m(3)};const d=f.shift();1===d.length?n(d(t)):2===d.length&&t.async?d(t,n):n()};p()},x.abort=function(){u=-1,o?e.abort():x.dispatchEvent("abort",{})},x.setRequestHeader=function(e,n){const o=null!=e?e.toLowerCase():void 0,r=t.headerNames[o]=t.headerNames[o]||e;t.headers[r]&&(n=t.headers[r]+", "+n),t.headers[r]=n},x.getResponseHeader=e=>p(a.headers[e?e.toLowerCase():void 0]),x.getAllResponseHeaders=()=>p(f(a.headers)),e.overrideMimeType&&(x.overrideMimeType=function(){e.overrideMimeType.apply(e,arguments)}),e.upload){let e=l();x.upload=e,t.upload=e}return x.UNSENT=0,x.OPENED=1,x.HEADERS_RECEIVED=2,x.LOADING=3,x.DONE=4,x.response="",x.responseText="",x.responseXML=null,x.readyState=0,x.statusText="",x};y.UNSENT=0,y.OPENED=1,y.HEADERS_RECEIVED=2,y.LOADING=3,y.DONE=4;var v={patch(){h&&(n.XMLHttpRequest=y)},unpatch(){h&&(n.XMLHttpRequest=h)},Native:h,Xhook:y};function b(e,t,n,o){return new(n||(n=Promise))((function(r,s){function a(e){try{c(o.next(e))}catch(e){s(e)}}function i(e){try{c(o.throw(e))}catch(e){s(e)}}function c(e){var t;e.done?r(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(a,i)}c((o=o.apply(e,t||[])).next())}))}const g=n.fetch;function E(e){return e instanceof Headers?m([...e.entries()]):Array.isArray(e)?m(e):e}function m(e){return e.reduce(((e,[t,n])=>(e[t]=n,e)),{})}const x=function(e,t={headers:{}}){let n=Object.assign(Object.assign({},t),{isFetch:!0});if(e instanceof Request){const o=function(e){let t={};return["method","headers","body","mode","credentials","cache","redirect","referrer","referrerPolicy","integrity","keepalive","signal","url"].forEach((n=>t[n]=e[n])),t}(e),r=Object.assign(Object.assign({},E(o.headers)),E(n.headers));n=Object.assign(Object.assign(Object.assign({},o),t),{headers:r,acceptedRequest:!0})}else n.url=e;const o=d.listeners("before"),r=d.listeners("after");return new Promise((function(t,s){let a=t;const i=function(e){if(!r.length)return a(e);const t=r.shift();return 2===t.length?(t(n,e),i(e)):3===t.length?t(n,e,i):i(e)},c=function(e){if(void 0!==e){const n=new Response(e.body||e.text,e);return t(n),void i(n)}u()},u=function(){if(!o.length)return void l();const e=o.shift();return 1===e.length?c(e(n)):2===e.length?e(n,c):void 0},l=()=>b(this,void 0,void 0,(function*(){const{url:t,isFetch:o,acceptedRequest:r}=n,c=function(e,t){var n={};for(var o in e)Object.prototype.hasOwnProperty.call(e,o)&&t.indexOf(o)<0&&(n[o]=e[o]);if(null!=e&&"function"==typeof Object.getOwnPropertySymbols){var r=0;for(o=Object.getOwnPropertySymbols(e);r<o.length;r++)t.indexOf(o[r])<0&&Object.prototype.propertyIsEnumerable.call(e,o[r])&&(n[o[r]]=e[o[r]])}return n}(n,["url","isFetch","acceptedRequest"]);return e instanceof Request&&c.body instanceof ReadableStream&&(c.body=yield new Response(c.body).text()),g(t,c).then((e=>i(e))).catch((function(e){return a=s,i(e),s(e)}))}));u()}))};var w={patch(){g&&(n.fetch=x)},unpatch(){g&&(n.fetch=g)},Native:g,Xhook:x};const O=d;return O.EventEmitter=l,O.before=function(e,t){if(e.length<1||e.length>2)throw"invalid hook";return O.on("before",e,t)},O.after=function(e,t){if(e.length<2||e.length>3)throw"invalid hook";return O.on("after",e,t)},O.enable=function(){v.patch(),w.patch()},O.disable=function(){v.unpatch(),w.unpatch()},O.XMLHttpRequest=v.Native,O.fetch=w.Native,O.headers=f,O.enable(),O}();
//# sourceMappingURL=xhook.min.js.map
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')
instance.sceneEl.emit("isoterminal_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( (this.emulator || this.worker) && this.ready ){
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})
}
)
})
}
}else{
this.emit('serial-output-string', str)
}
}
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,
overlayfs: this.opts.overlayfs,
filesystem: {},
autostart: true,
prompt: this.opts.prompt,
debug: this.opts.debug ? true : false
};
this
.setupWorker(opts)
.startVM(opts)
}
ISOTerminal.prototype.setupWorker = function(opts){
if( typeof window.PromiseWorker == 'undefined' ) return this
/*
* 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. . ____ _____________ ________. ._. ._. . .
\r. . .\\ \\/ /\\______ \\/ _____// | \\. .
\r. . . \\ / | _/\\_____ \\/ ~ \\ .
\r. . . / \\ | | \\/ \\ Y / .
\r. . ./___/\\ \\ |____|_ /_______ /\\___|_ /. .
\r. . . . . .\\_/. . . . \\/ . . . .\\/ . . _ \\/ . .
\r▬▬▬▬▬▬▬▬ https://xrsh.isvery.ninja ▬▬▬▬▬▬▬▬▬▬▬▬
\rlocal-first, polyglot, unixy WebXR IDE & runtime
\r
\rcredits
\r-------
\rhttps://www.w3.org/TR/webxr
\rhttps://xrfragment.org
\rhttps://threejs.org
\rhttps://aframe.org
\rhttps://busybox.net
\rhttps://buildroot.org
\rfediverse: @lvk@mastodon.online @utopiah@mastodon.pirateparty.be @nlnet@nlnet.nl
\r`
const text_color = "\r"
const text_reset = "\033[0m"
const loadmsg = "\n\r"+loading[ Math.floor(Math.random()*1000) % loading.length ] + "..please wait \n\n\r"
const empowermsg = "\n\r"+text_reset+'"'+empower[ Math.floor(Math.random()*1000) % empower.length ] + '"\n\r'
return { motd, text_color, text_reset, loadmsg, empowermsg}
}
ISOTerminal.prototype.startVM = function(opts){
this.v86opts = opts
this.addEventListener('emulator-started', async (e) => {
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
this.emit('serial-output-string', msg)
this.emit('bootMenu',{bootMenu: this.opts.bootMenu, bootMenuURL: this.opts.bootMenuURL })
}
ISOTerminal.prototype.bootISO = function(){
let msglib = this.getLoaderMsg()
this.emit('status',msglib.loadmsg)
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)
}
}
/*
* Javascript terminal
*
* Copyright (c) 2011-2020 Fabrice Bellard
*
* 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.
*/
"use strict";
function Term(options)
{
var width, height, tot_height, scrollback;
function dummy_key_handler()
{
}
width = options.cols ? options.cols : 80;
height = options.rows ? options.rows : 25;
scrollback = options.scrollback ? options.scrollback : 0;
this.font_size = options.fontSize ? options.fontSize : 15;
this.w = width;
this.h = height;
this.cur_h = height; /* current height of the scroll back buffer */
tot_height = height + scrollback;
this.tot_h = tot_height; /* maximum height of the scroll back buffer */
/* y_base and y_disp are index in the circular buffer lines of
length cur_h. They are defined modulo tot_h, i.e. they wrap
when cur_h = tot_h. If cur_h < tot_h, y_base is always equal to
cur_h - h. */
this.y_base = 0; /* position of the current top screen line in the
scroll back buffer */
this.y_disp = 0; /* position of the top displayed line in the
scroll back buffer */
/* cursor position */
this.x = 0;
this.y = 0;
this.scroll_top = 0;
this.scroll_bottom = this.h;
this.cursorstate = 0;
this.handler = dummy_key_handler;
this.state = 0;
this.output_queue = "";
this.colors = [
/* normal */
"#000000",
"#aa0000",
"#00aa00",
"#aa5500",
"#0000aa",
"#aa00aa",
"#00aaaa",
"#aaaaaa",
/* bright */
"#555555",
"#ff5555",
"#55ff55",
"#ffff55",
"#5555ff",
"#ff55ff",
"#55ffff",
"#ffffff"
];
/* attributes bits:
0-3: bg
4-7: fg
8: bold
9: inverse
*/
this.def_attr = (7 << 4) | 0;
this.cur_attr = this.def_attr;
this.is_mac = (navigator.userAgent.indexOf("Mac") >=0 ) ? true : false;
this.key_rep_state = 0;
this.key_rep_str = "";
this.utf8 = true;
this.utf8_state = 0;
this.utf8_val = 0;
this.application_cursor = false;
this.application_keypad = false;
/* if true, emulate some behaviors of the Linux console */
this.linux_console = true;
this.textarea_has_focus = false;
}
Term.prototype.setKeyHandler = function(handler)
{
this.handler = handler;
}
/* return the size of a character in CSS pixels using the selected font */
function term_get_char_size(parent_el, font_size)
{
var el, g, ret;
el = document.createElement("div");
el.classList.add("term", "term_char_size");
el.style.fontSize = font_size + "px";
el.textContent = "W";
parent_el.appendChild(el);
g = el.getBoundingClientRect();
/* the character width & height may not be an integer */
ret = [g.width, g.height];
return ret;
}
Term.prototype.open = function(parent_el)
{
var y, line, i, term, c, row_el, char_size_ret;
/* set initial content */
this.lines = new Array();
c = 32 | (this.def_attr << 16);
for(y = 0; y < this.cur_h;y++) {
line = new Array();
for(i=0;i<this.w;i++)
line[i] = c;
this.lines[y] = line;
}
char_size_ret = term_get_char_size(parent_el, this.font_size);
/* size of the character in CSS pixels */
this.char_width = char_size_ret[0];
this.char_height = char_size_ret[1];
this.scrollbar_width = 15;
/* size of term_el in CSS pixels */
this.term_width = Math.ceil(this.w * this.char_width) +
this.scrollbar_width;
this.term_height = Math.ceil(this.h * this.char_height);
/* create the terminal window */
this.term_el = document.createElement("div");
this.term_el.className = "term";
/* XXX: could compute the font metrics */
this.term_el.style.fontSize = this.font_size + "px";
this.term_el.style.width = this.term_width + "px";
this.term_el.style.height = this.term_height + "px";
/* allow the terminal to take the focus */
this.term_el.setAttribute("tabindex", "0");
/* scroll bar */
this.scrollbar_el = document.createElement("div");
this.scrollbar_el.className = "term_scrollbar";
this.scrollbar_el.style.width = this.scrollbar_width + "px";
this.term_el.appendChild(this.scrollbar_el);
this.track_el = document.createElement("div");
this.track_el.className = "term_track";
this.track_el.onmousedown = this.mouseMoveHandler.bind(this);
this.scrollbar_el.appendChild(this.track_el);
this.thumb_el = document.createElement("div");
this.thumb_el.className = "term_thumb";
this.thumb_el.onmousedown = this.mouseDownHandler.bind(this);
this.track_el.appendChild(this.thumb_el);
this.end_el = document.createElement("div");
this.end_el.className = "term_end";
this.thumb_el.appendChild(this.end_el);
/* current scrollbar position */
this.thumb_size = -1;
this.thumb_pos = -1;
/* terminal content */
this.content_el = document.createElement("div");
this.content_el.className = "term_content";
this.content_el.style.width = (this.w) + "ch";
this.term_el.appendChild(this.content_el);
this.rows_el = [];
for(y=0;y<this.h;y++) {
row_el = document.createElement("div");
this.rows_el.push(row_el);
this.content_el.appendChild(row_el);
}
/* dummy textarea to get the input events and for the virtual
keyboard on mobile devices */
this.textarea_el = document.createElement("textarea");
this.textarea_el.classList.add("term_textarea");
this.textarea_el.setAttribute("autocorrect", "off");
this.textarea_el.setAttribute("autocapitalize", "off");
this.textarea_el.setAttribute("spellcheck", "false");
this.textarea_el.setAttribute("tabindex", "-1");
this.term_el.appendChild(this.textarea_el);
this.parent_el = parent_el;
parent_el.appendChild(this.term_el);
this.refresh(0, this.h - 1);
/* textarea_el events */
// key handler
this.textarea_el.addEventListener("keydown",
this.keyDownHandler.bind(this), true);
this.textarea_el.addEventListener("keyup",
this.keyUpHandler.bind(this), true);
/* keypress is deprecated, so use input */
this.textarea_el.addEventListener("input",
this.inputHandler.bind(this), true);
this.textarea_el.addEventListener("focus",
this.focusHandler.bind(this), true);
this.textarea_el.addEventListener("blur",
this.blurHandler.bind(this), true);
/* term_el events */
this.term_el.addEventListener("keydown",
this.termKeyDownHandler.bind(this),
true);
this.term_el.addEventListener("paste",
this.pasteHandler.bind(this), true);
this.term_el.addEventListener("mouseup",
this.termMouseUpHandler.bind(this),
true);
this.term_el.addEventListener("wheel",
this.wheelHandler.bind(this), false);
// cursor blinking
term = this;
setInterval(function() { term.cursor_timer_cb(); }, 1000);
this.term_el.focus();
};
Term.prototype.refresh_scrollbar = function ()
{
var total_size, thumb_pos, thumb_size, y, y0;
total_size = this.term_el.clientHeight;
thumb_size = Math.ceil(this.h * total_size / this.cur_h);
/* position of the first line of the scroll back buffer */
y0 = (this.y_base + this.h) % this.cur_h;
y = this.y_disp - y0;
if (y < 0)
y += this.cur_h;
thumb_pos = Math.floor(y * total_size / this.cur_h);
thumb_size = Math.max(thumb_size, 30);
thumb_size = Math.min(thumb_size, total_size);
thumb_pos = Math.min(thumb_pos, total_size - thumb_size);
// console.log("pos=" + thumb_pos + " size=" + thumb_size);
if (thumb_pos != this.thumb_pos || thumb_size != this.thumb_size) {
this.thumb_pos = thumb_pos;
this.thumb_size = thumb_size;
this.thumb_el.style.top = thumb_pos + "px";
this.thumb_el.style.height = thumb_size + "px";
}
}
/* move the text area at the cursor position so that the browser shows
* the correct position when the virtual keyboard is used */
Term.prototype.move_textarea = function()
{
var x, y, base_x, base_y, pos;
pos = this.term_el.getBoundingClientRect();
base_x = pos.left + window.scrollX;
base_y = pos.top + window.scrollY;
/* position relative to the body */
x = Math.ceil(this.x * this.char_width + base_x);
y = Math.ceil(this.y * this.char_height + base_y);
this.textarea_el.style.width = Math.ceil(this.char_width) + "px";
this.textarea_el.style.height = Math.ceil(this.char_height) + "px";
this.textarea_el.style.left = x + "px";
this.textarea_el.style.top = y + "px";
this.textarea_el.style.zIndex = 1000;
}
Term.prototype.refresh = function(ymin, ymax)
{
var el, y, line, outline, c, w, i, j, cx, attr, last_attr, fg, bg, y1;
var http_link_len, http_link_str, bold, tmp, inverse;
function is_http_link_char(c)
{
var str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~:/?#[]@!$&'()*+,;=%`.";
return str.indexOf(String.fromCharCode(c)) >= 0;
}
function right_trim(str, a)
{
var i, n;
n = a.length;
i = str.length;
while (i >= n && str.substr(i - n, n) == a)
i -= n;
return str.substr(0, i);
}
for(y = ymin; y <= ymax; y++) {
/* convert to HTML string */
y1 = y + this.y_disp;
if (y1 >= this.cur_h)
y1 -= this.cur_h;
line = this.lines[y1];
outline = "";
w = this.w;
if (y == this.y && this.cursor_state &&
this.y_disp == this.y_base) {
cx = this.x;
} else {
cx = -1;
}
last_attr = this.def_attr;
http_link_len = 0;
for(i = 0; i < w; i++) {
c = line[i];
attr = c >> 16;
c &= 0xffff;
/* test for http link */
if (c == 0x68 && (w - i) >= 8 && http_link_len == 0) {
/* test http:// or https:// */
if ((line[i + 1] & 0xffff) == 0x74 &&
(line[i + 2] & 0xffff) == 0x74 &&
(line[i + 3] & 0xffff) == 0x70 &&
(((line[i + 4] & 0xffff) == 0x3a &&
(line[i + 5] & 0xffff) == 0x2f &&
(line[i + 6] & 0xffff) == 0x2f) ||
((line[i + 4] & 0xffff) == 0x73 &&
(line[i + 5] & 0xffff) == 0x3a &&
(line[i + 6] & 0xffff) == 0x2f &&
(line[i + 7] & 0xffff) == 0x2f))) {
http_link_str = "";
j = 0;
while ((i + j) < w &&
is_http_link_char(line[i + j] & 0xffff)) {
http_link_str += String.fromCharCode(line[i + j] & 0xffff);
j++;
}
http_link_len = j;
if (last_attr != this.def_attr) {
outline += '</span>';
last_attr = this.def_attr;
}
outline += "<a href='" + http_link_str + "' onclick='return Term.prototype.href(this)' target='_blank'>";
}
}
if (i == cx) {
attr = -1; /* cursor */
}
if (attr != last_attr) {
if (last_attr != this.def_attr)
outline += '</span>';
if (attr != this.def_attr) {
if (attr == -1) {
/* cursor */
outline += '<span class="term_cursor">';
} else {
outline += '<span style="';
fg = (attr >> 4) & 0xf;
bg = attr & 0xf;
bold = (attr >> 8) & 1;
inverse = (attr >> 9) & 1;
if (inverse) {
tmp = fg;
fg = bg;
bg = tmp;
}
if (bold) {
/* metrics are not OK for all fonts, so disabled */
/* outline += 'font-weight:bold;'; */
/* use the bright color */
if (fg < 8)
fg += 8;
}
if (fg != 7) {
outline += 'color:' + this.colors[fg] + ';';
}
if (bg != 0) {
outline += 'background-color:' +
this.colors[bg] + ';';
}
outline += '">';
}
}
}
switch(c) {
case 32:
outline += "&nbsp;";
break;
case 38: // '&'
outline += "&amp;";
break;
case 60: // '<'
outline += "&lt;";
break;
case 62: // '>'
outline += "&gt;";
break;
default:
if (c < 32) {
outline += "&nbsp;";
} else {
outline += String.fromCharCode(c);
}
break;
}
last_attr = attr;
if (http_link_len != 0) {
http_link_len--;
if (http_link_len == 0) {
if (last_attr != this.def_attr) {
outline += '</span>';
last_attr = this.def_attr;
}
outline += "</a>";
}
}
}
if (last_attr != this.def_attr) {
outline += '</span>';
}
/* trim trailing spaces for copy/paste */
outline = right_trim(outline, "&nbsp;");
if (outline == "")
outline = "&nbsp;";
this.rows_el[y].innerHTML = outline;
}
this.refresh_scrollbar();
this.move_textarea();
};
Term.prototype.cursor_timer_cb = function()
{
this.cursor_state ^= 1;
this.refresh(this.y, this.y);
};
Term.prototype.show_cursor = function()
{
if (!this.cursor_state) {
this.cursor_state = 1;
this.refresh(this.y, this.y);
}
};
/* scroll down or up in the scroll back buffer by n lines */
Term.prototype.scroll_disp = function(n)
{
var i, y1;
/* slow but it does not really matters */
if (n >= 0) {
for(i = 0; i < n; i++) {
if (this.y_disp == this.y_base)
break;
if (++this.y_disp == this.cur_h)
this.y_disp = 0;
}
} else {
n = -n;
y1 = this.y_base + this.h;
if (y1 >= this.cur_h)
y1 -= this.cur_h;
for(i = 0; i < n; i++) {
if (this.y_disp == y1)
break;
if (--this.y_disp < 0)
this.y_disp = this.cur_h - 1;
}
}
this.refresh(0, this.h - 1);
};
Term.prototype.write = function(str)
{
var s, ymin, ymax;
function update(y)
{
ymin = Math.min(ymin, y);
ymax = Math.max(ymax, y);
}
function get_erase_char()
{
var bg_mask, attr;
bg_mask = 0xf;
attr = (s.def_attr & ~bg_mask) | (s.cur_attr & bg_mask);
return 32 | (attr << 16);
}
function erase_chars(x1, x2, y) {
var l, i, c, y1;
y1 = s.y_base + y;
if (y1 >= s.cur_h)
y1 -= s.cur_h;
l = s.lines[y1];
c = get_erase_char();
for(i = x1; i < x2; i++)
l[i] = c;
update(y);
}
function erase_to_eol(x, y) {
erase_chars(x, s.w, y);
}
function erase_in_line(n) {
switch(n) {
case 0:
erase_to_eol(s.x, s.y);
break;
case 1:
erase_chars(0, s.x + 1, s.y);
break;
case 2:
erase_chars(0, s.w, s.y);
break;
}
}
function erase_in_display(n) {
var y;
switch(n) {
case 0:
erase_to_eol(s.x, s.y);
for(y = s.y + 1; y < s.h; y++)
erase_to_eol(0, y);
break;
case 1:
erase_chars(0, s.x + 1, s.y);
for(y = 0; y < s.y; y++) {
erase_to_eol(0, y);
}
break;
case 2:
for(y = 0; y < s.h; y++) {
erase_to_eol(0, y);
}
break;
}
}
function delete_chars(n)
{
var l, i, c, y1, j;
y1 = s.y + s.y_base;
if (y1 >= s.cur_h)
y1 -= s.cur_h;
l = s.lines[y1];
if (n < 1)
n = 1;
c = get_erase_char();
j = s.x + n;
for(i = s.x; i < s.w; i++) {
if (j < s.w)
l[i] = l[j];
else
l[i] = c;
j++;
}
update(s.y);
}
function insert_chars(n)
{
var l, i, c, y1, x1;
if (n < 1)
n = 1;
if (n > s.w - s.x)
n = s.w - s.x;
y1 = s.y + s.y_base;
if (y1 >= s.cur_h)
y1 -= s.cur_h;
l = s.lines[y1];
x1 = s.x + n;
for(i = s.w - 1; i >= x1; i--)
l[i] = l[i - n];
c = get_erase_char();
for(i = s.x; i < x1; i++)
l[i] = c;
update(s.y);
}
function csi_colors(esc_params)
{
var j, n, fg, bg, mask;
if (esc_params.length == 0) {
s.cur_attr= s.def_attr;
} else {
for(j = 0; j < esc_params.length; j++) {
n = esc_params[j];
if (n >= 30 && n <= 37) {
/* foreground */
fg = n - 30;
s.cur_attr = (s.cur_attr & ~(0xf << 4)) | (fg << 4);
} else if (n >= 40 && n <= 47) {
/* background */
bg = n - 40;
s.cur_attr = (s.cur_attr & ~0xf) | bg;
} else if (n >= 90 && n <= 97) {
/* bright foreground */
fg = n - 90 + 8;
s.cur_attr = (s.cur_attr & ~(0xf << 4)) | (fg << 4);
} else if (n >= 100 && n <= 107) {
/* bright background */
bg = n - 100 + 8;
s.cur_attr = (s.cur_attr & ~0xf) | bg;
} else if (n == 1) {
/* bold + bright */
s.cur_attr |= (1 << 8);
} else if (n == 0) {
/* default attr */
s.cur_attr = s.def_attr;
} else if (n == 7) {
/* inverse */
s.cur_attr |= (1 << 9);
} else if (n == 27) {
/* not inverse */
s.cur_attr &= ~(1 << 9);
} else if (n == 39) {
/* reset fg */
mask = 0x0f << 4;
s.cur_attr = (s.cur_attr & ~mask) | (s.def_attr & mask);
} else if (n == 49) {
/* reset bg */
mask = 0x0f;
s.cur_attr = (s.cur_attr & ~mask) | (s.def_attr & mask);
}
}
}
}
function empty_line(y, use_erase_char) {
var line, c, y1, x;
if (use_erase_char)
c = get_erase_char();
else
c = 32 | (s.def_attr << 16);
line = new Array();
for(x=0;x<s.w;x++)
line[x] = c;
y1 = s.y_base + y;
if (y1 >= s.cur_h)
y1 -= s.cur_h;
s.lines[y1] = line;
}
function scroll_down(top, bottom, use_erase_char)
{
var y, line, y1, y2;
if (top == 0 && bottom == s.h) {
/* increase height of buffer if possible */
if (s.cur_h < s.tot_h) {
s.cur_h++;
}
/* move down one line */
if (++s.y_base == s.cur_h)
s.y_base = 0;
s.y_disp = s.y_base;
} else {
/* partial scroll */
for(y = top; y < bottom - 1; y++) {
y1 = s.y_base + y;
if (y1 >= s.cur_h)
y1 -= s.cur_h;
y2 = y1 + 1;
if (y2 >= s.cur_h)
y2 -= s.cur_h;
s.lines[y1] = s.lines[y2];
}
}
empty_line(bottom - 1, use_erase_char);
update(top);
update(bottom - 1);
}
function scroll_up(top, bottom, use_erase_char) {
var y, y1, y2;
/* XXX: could scroll in the history */
for(y = bottom - 1; y > top; y--) {
y1 = s.y_base + y;
if (y1 >= s.cur_h)
y1 -= s.cur_h;
y2 = y1 - 1;
if (y2 >= s.cur_h)
y2 -= s.cur_h;
s.lines[y1] = s.lines[y2];
}
empty_line(top, use_erase_char);
update(top);
update(bottom - 1);
}
function down_with_scroll() {
s.y++;
if (s.y == s.scroll_bottom) {
s.y--;
scroll_down(s.scroll_top, s.scroll_bottom, false);
} else if (s.y >= s.h) {
s.y--;
scroll_down(0, s.h, false);
}
}
function up_with_scroll() {
if (s.y == s.scroll_top) {
scroll_up(s.scroll_top, s.scroll_bottom, true);
} else if (s.y == 0) {
scroll_up(0, s.h, true);
} else {
s.y--;
}
}
function insert_lines(n) {
var y2;
if (n < 1)
n = 1;
if (s.y < s.scroll_bottom)
y2 = s.scroll_bottom;
else
y2 = s.h;
while (n != 0) {
scroll_up(s.y, y2, true);
n--;
}
}
function delete_lines(n) {
var y2;
if (n < 1)
n = 1;
if (s.y < s.scroll_bottom)
y2 = s.scroll_bottom;
else
y2 = s.h;
while (n != 0) {
scroll_down(s.y, y2, true);
n--;
}
}
var TTY_STATE_NORM = 0;
var TTY_STATE_ESC = 1;
var TTY_STATE_CSI = 2;
var TTY_STATE_CHARSET = 3;
function handle_char(c) {
var i, l, n, j, y1, y2, x1;
switch(s.state) {
case TTY_STATE_NORM:
switch(c) {
case 10:
down_with_scroll();
break;
case 13:
s.x = 0;
break;
case 8:
if (s.x > 0) {
s.x--;
}
break;
case 9: /* tab */
n = (s.x + 8) & ~7;
if (n <= s.w) {
s.x = n;
}
break;
case 27:
s.state = TTY_STATE_ESC;
break;
default:
if (c >= 32) {
if (s.x >= s.w) {
s.x = 0;
down_with_scroll();
}
y1 = s.y + s.y_base;
if (y1 >= s.cur_h)
y1 -= s.cur_h;
s.lines[y1][s.x] = (c & 0xffff) |
(s.cur_attr << 16);
s.x++;
update(s.y);
}
break;
}
break;
case TTY_STATE_ESC:
switch(c) {
case 91: // '['
s.esc_params = new Array();
s.cur_param = 0;
s.esc_prefix = 0;
s.state = TTY_STATE_CSI;
break;
case 40: // '('
case 41: // ')'
s.state = TTY_STATE_CHARSET;
break;
case 61: // '='
s.application_keypad = true;
s.state = TTY_STATE_NORM;
break;
case 62: // '>'
s.application_keypad = false;
s.state = TTY_STATE_NORM;
break;
case 77: // 'M'
up_with_scroll();
s.state = TTY_STATE_NORM;
break;
default:
s.state = TTY_STATE_NORM;
break;
}
break;
case TTY_STATE_CSI:
if (c >= 48 && c <= 57) { // '0' '9'
/* numeric */
s.cur_param = s.cur_param * 10 + c - 48;
} else {
if (c == 63) { // '?'
s.esc_prefix = c;
break;
}
/* add parsed parameter */
s.esc_params[s.esc_params.length] = s.cur_param;
s.cur_param = 0;
if (c == 59) // ;
break;
s.state = TTY_STATE_NORM;
// console.log("term: csi=" + s.esc_params + " cmd="+c);
switch(c) {
case 64: // '@' insert chars
insert_chars(s.esc_params[0]);
break;
case 65: // 'A' up
n = s.esc_params[0];
if (n < 1)
n = 1;
s.y -= n;
if (s.y < 0)
s.y = 0;
break;
case 66: // 'B' down
n = s.esc_params[0];
if (n < 1)
n = 1;
s.y += n;
if (s.y >= s.h)
s.y = s.h - 1;
break;
case 67: // 'C' right
n = s.esc_params[0];
if (n < 1)
n = 1;
s.x += n;
if (s.x >= s.w - 1)
s.x = s.w - 1;
break;
case 68: // 'D' left
n = s.esc_params[0];
if (n < 1)
n = 1;
s.x -= n;
if (s.x < 0)
s.x = 0;
break;
case 71: /* 'G' cursor character absolute */
x1 = s.esc_params[0] - 1;
if (x1 < 0)
x1 = 0;
else if (x1 >= s.w)
x1 = s.w - 1;
s.x = x1;
break;
case 72: // 'H' goto xy
y1 = s.esc_params[0] - 1;
if (s.esc_params.length >= 2)
x1 = s.esc_params[1] - 1;
else
x1 = 0;
if (y1 < 0)
y1 = 0;
else if (y1 >= s.h)
y1 = s.h - 1;
if (x1 < 0)
x1 = 0;
else if (x1 >= s.w)
x1 = s.w - 1;
s.x = x1;
s.y = y1;
break;
case 74: // 'J' erase in display
erase_in_display(s.esc_params[0]);
break;
case 75: // 'K' erase in line
erase_in_line(s.esc_params[0]);
break;
case 76: // 'L' insert lines
insert_lines(s.esc_params[0]);
break;
case 77: // 'M' insert lines
delete_lines(s.esc_params[0]);
break;
case 80: // 'P'
delete_chars(s.esc_params[0]);
break;
case 100: // 'd' line position absolute
{
y1 = s.esc_params[0] - 1;
if (y1 < 0)
y1 = 0;
else if (y1 >= s.h)
y1 = s.h - 1;
s.y = y1;
}
break;
case 104: // 'h': set mode
if (s.esc_prefix == 63 && s.esc_params[0] == 1) {
s.application_cursor = true;
}
break;
case 108: // 'l': reset mode
if (s.esc_prefix == 63 && s.esc_params[0] == 1) {
s.application_cursor = false;
}
break;
case 109: // 'm': set color
csi_colors(s.esc_params);
break;
case 110: // 'n' return the cursor position
s.queue_chars("\x1b[" + (s.y + 1) + ";" + (s.x + 1) + "R");
break;
case 114: // 'r' set scroll region
y1 = s.esc_params[0] - 1;
if (y1 < 0)
y1 = 0;
else if (y1 >= s.h)
y1 = s.h - 1;
if (s.esc_params.length >= 2)
y2 = s.esc_params[1];
else
y2 = s.h;
if (y2 >= s.h || y2 <= y1)
y2 = s.h;
s.scroll_top = y1;
s.scroll_bottom = y2;
s.x = 0;
s.y = 0;
break;
default:
break;
}
}
break;
case TTY_STATE_CHARSET:
/* just ignore */
s.state = TTY_STATE_NORM;
break;
}
}
function handle_utf8(c) {
if (s.utf8_state !== 0 && (c >= 0x80 && c < 0xc0)) {
s.utf8_val = (s.utf8_val << 6) | (c & 0x3F);
s.utf8_state--;
if (s.utf8_state === 0) {
handle_char(s.utf8_val);
}
} else if (c >= 0xc0 && c < 0xf8) {
s.utf8_state = 1 + (c >= 0xe0) + (c >= 0xf0);
s.utf8_val = c & ((1 << (6 - s.utf8_state)) - 1);
} else {
s.utf8_state = 0;
handle_char(c);
}
}
var i, c, utf8;
/* update region is in ymin ymax */
s = this;
ymin = s.h;
ymax = -1;
update(s.y); // remove the cursor
/* reset top of displayed screen to top of real screen */
if (s.y_base != s.y_disp) {
s.y_disp = s.y_base;
/* force redraw */
ymin = 0;
ymax = s.h - 1;
}
utf8 = s.utf8;
for(i = 0; i < str.length; i++) {
c = str.charCodeAt(i);
if (utf8)
handle_utf8(c);
else
handle_char(c);
}
update(s.y); // show the cursor
if (ymax >= ymin)
s.refresh(ymin, ymax);
};
Term.prototype.writeln = function (str)
{
this.write(str + '\r\n');
};
Term.prototype.interceptBrowserExit = function (ev)
{
/* At least avoid exiting the navigator if Ctrl-Q or Ctrl-W are
* pressed */
if (ev.ctrlKey) {
window.onbeforeunload = function() {
window.onbeforeunload = null;
return "CTRL-W or Ctrl-Q cannot be sent to the emulator.";
};
} else {
window.onbeforeunload = null;
}
}
Term.prototype.keyDownHandler = function (ev)
{
var str;
this.interceptBrowserExit(ev);
str="";
switch(ev.keyCode) {
case 8: /* backspace */
str = "\x7f";
break;
case 9: /* tab */
str = "\x09";
break;
case 13: /* enter */
str = "\x0d";
break;
case 27: /* escape */
str = "\x1b";
break;
case 37: /* left */
if (ev.ctrlKey) {
str = "\x1b[1;5D";
} else if (this.application_cursor) {
str = "\x1bOD";
} else {
str = "\x1b[D";
}
break;
case 39: /* right */
if (ev.ctrlKey) {
str = "\x1b[1;5C";
} else if (this.application_cursor) {
str = "\x1bOC";
} else {
str = "\x1b[C";
}
break;
case 38: /* up */
if (ev.ctrlKey) {
this.scroll_disp(-1);
} else if (this.application_cursor) {
str = "\x1bOA";
} else {
str = "\x1b[A";
}
break;
case 40: /* down */
if (ev.ctrlKey) {
this.scroll_disp(1);
} else if (this.application_cursor) {
str = "\x1bOB";
} else {
str = "\x1b[B";
}
break;
case 46: /* delete */
str = "\x1b[3~";
break;
case 45: /* insert */
str = "\x1b[2~";
break;
case 36: /* home */
if (this.linux_console)
str = "\x1b[1~";
else if (this.application_keypad)
str = "\x1bOH";
else
str = "\x1b[H";
break;
case 35: /* end */
if (this.linux_console)
str = "\x1b[4~";
else if (this.application_keypad)
str = "\x1bOF";
else
str = "\x1b[F";
break;
case 33: /* page up */
if (ev.ctrlKey) {
this.scroll_disp(-(this.h - 1));
} else {
str = "\x1b[5~";
}
break;
case 34: /* page down */
if (ev.ctrlKey) {
this.scroll_disp(this.h - 1);
} else {
str = "\x1b[6~";
}
break;
default:
if (ev.ctrlKey) {
/* ctrl + key */
if (ev.keyCode >= 65 && ev.keyCode <= 90) {
str = String.fromCharCode(ev.keyCode - 64);
} else if (ev.keyCode == 32) {
str = String.fromCharCode(0);
}
} else if ((!this.is_mac && ev.altKey) ||
(this.is_mac && ev.metaKey)) {
/* meta + key (Note: we only send lower case) */
if (ev.keyCode >= 65 && ev.keyCode <= 90) {
str = "\x1b" + String.fromCharCode(ev.keyCode + 32);
}
}
break;
}
// console.log("keydown: keycode=" + ev.keyCode + " charcode=" + ev.charCode + " str=" + str + " ctrl=" + ev.ctrlKey + " alt=" + ev.altKey + " meta=" + ev.metaKey);
if (str) {
if (ev.stopPropagation)
ev.stopPropagation();
if (ev.preventDefault)
ev.preventDefault();
this.show_cursor();
this.key_rep_state = 1;
this.key_rep_str = str;
this.handler(str);
return false;
} else {
this.key_rep_state = 0;
return true;
}
};
Term.prototype.keyUpHandler = function (ev)
{
this.interceptBrowserExit(ev);
};
Term.prototype.to_utf8 = function(s)
{
var i, n = s.length, r, c;
r = "";
for(i = 0; i < n; i++) {
c = s.charCodeAt(i);
if (c < 0x80) {
r += String.fromCharCode(c);
} else if (c < 0x800) {
r += String.fromCharCode((c >> 6) | 0xc0, (c & 0x3f) | 0x80);
} else if (c < 0x10000) {
r += String.fromCharCode((c >> 12) | 0xe0,
((c >> 6) & 0x3f) | 0x80,
(c & 0x3f) | 0x80);
} else {
r += String.fromCharCode((c >> 18) | 0xf0,
((c >> 12) & 0x3f) | 0x80,
((c >> 6) & 0x3f) | 0x80,
(c & 0x3f) | 0x80);
}
}
return r;
}
Term.prototype.inputHandler = function (ev)
{
var str;
str = this.textarea_el.value;
if (str) {
this.textarea_el.value = "";
this.show_cursor();
if (this.utf8)
str = this.to_utf8(str);
this.handler(str);
return false;
} else {
return true;
}
};
Term.prototype.termKeyDownHandler = function(ev)
{
this.interceptBrowserExit(ev);
/* give the focus back to the textarea when a key is pressed */
this.textarea_el.focus();
}
Term.prototype.termMouseUpHandler = function(ev)
{
var sel;
/* if no selection, can switch back up to the textarea focus */
sel = window.getSelection();
if (!sel || sel.isCollapsed)
this.textarea_el.focus();
}
Term.prototype.focusHandler = function (ev)
{
this.textarea_has_focus = true;
};
Term.prototype.blurHandler = function (ev)
{
/* allow unloading the page */
window.onbeforeunload = null;
this.textarea_has_focus = false;
};
Term.prototype.pasteHandler = function (ev)
{
var c, str;
if (!this.textarea_has_focus) {
c = ev.clipboardData;
if (c) {
str = c.getData("text/plain");
if (this.utf8)
str = this.to_utf8(str);
this.queue_chars(str);
return false;
}
}
}
Term.prototype.wheelHandler = function (ev)
{
if (ev.deltaY < 0)
this.scroll_disp(-3);
else if (ev.deltaY > 0)
this.scroll_disp(3);
ev.stopPropagation();
}
Term.prototype.mouseDownHandler = function (ev)
{
this.thumb_el.onmouseup = this.mouseUpHandler.bind(this);
document.onmousemove = this.mouseMoveHandler.bind(this);
document.onmouseup = this.mouseUpHandler.bind(this);
/* disable potential selection */
document.body.className += " noSelect";
this.mouseMoveHandler(ev);
}
Term.prototype.mouseMoveHandler = function (ev)
{
var total_size, pos, new_y_disp, y, y0;
total_size = this.term_el.clientHeight;
y = ev.clientY - this.track_el.getBoundingClientRect().top;
pos = Math.floor((y - (this.thumb_size / 2)) * this.cur_h / total_size);
new_y_disp = Math.min(Math.max(pos, 0), this.cur_h - this.h);
/* position of the first line of the scroll back buffer */
y0 = (this.y_base + this.h) % this.cur_h;
new_y_disp += y0;
if (new_y_disp >= this.cur_h)
new_y_disp -= this.cur_h;
if (new_y_disp != this.y_disp) {
this.y_disp = new_y_disp;
this.refresh(0, this.h - 1);
}
}
Term.prototype.mouseUpHandler = function (ev)
{
this.thumb_el.onmouseup = null;
document.onmouseup = null;
document.onmousemove = null;
document.body.className = document.body.className.replace(" noSelect", "");
}
/* output queue to send back asynchronous responses */
Term.prototype.queue_chars = function (str)
{
this.output_queue += str;
if (this.output_queue)
setTimeout(this.outputHandler.bind(this), 0);
};
Term.prototype.outputHandler = function ()
{
if (this.output_queue) {
this.handler(this.output_queue);
this.output_queue = "";
}
};
Term.prototype.getSize = function ()
{
return [this.w, this.h];
};
/* resize the terminal (size in pixels). Return true if the display
size was modified. */
/* XXX: may be simpler to separate the scrollback buffer from the
screen buffer */
Term.prototype.resizePixel = function (new_width, new_height)
{
var new_w, new_h, y, x, line, c, row_el, d, new_cur_h, e;
if (new_width == this.term_width && new_height == this.term_height)
return false;
new_w = Math.floor((new_width - this.scrollbar_width) /
this.char_width);
new_h = Math.floor(new_height / this.char_height);
if (new_w <= 0 || new_h <= 0 || new_h > this.tot_h)
return false;
this.term_width = new_width;
this.term_height = new_height;
this.term_el.style.width = this.term_width + "px";
this.term_el.style.height = this.term_height + "px";
/* XXX: could keep the EOL positions */
if (new_w < this.w) {
/* reduce the line width */
for(y = 0; y < this.cur_h;y++) {
line = this.lines[y];
line = line.slice(0, new_w);
}
} else if (new_w > this.w) {
/* increase the line width */
c = 32 | (this.def_attr << 16);
for(y = 0; y < this.cur_h;y++) {
line = this.lines[y];
for(x = this.w; x < new_w; x++)
line[x] = c;
}
}
if (this.x >= new_w)
this.x = new_w - 1;
d = new_h - this.h;
if (d < 0) {
d = -d;
/* remove displayed lines */
/* strip the DOM terminal content */
for(y = new_h; y < this.h; y++) {
row_el = this.rows_el[y];
this.content_el.removeChild(row_el);
}
this.rows_el = this.rows_el.slice(0, new_h);
/* adjust cursor position if needed */
if (this.y >= new_h) {
if (d > this.y)
d = this.y;
this.y -= d;
this.y_base += d;
if (this.y_base >= this.tot_h)
this.y_base -= this.tot_h;
}
if (this.scroll_bottom > new_h)
this.scroll_bottom = new_h;
/* fail safe for scroll top */
if (this.scroll_top >= this.scroll_bottom)
this.scroll_top = 0;
} else if (d > 0) {
/* add displayed lines */
if (this.cur_h == this.tot_h) {
if (d > this.tot_h - this.h)
d = this.tot_h - this.h;
} else {
if (d > this.y_base)
d = this.y_base;
}
this.y_base -= d;
if (this.y_base < 0)
this.y_base += this.tot_h;
this.y += d;
if (this.scroll_bottom == this.h)
this.scroll_bottom = new_h;
/* extend the DOM terminal content */
for(y = this.h; y < new_h; y++) {
row_el = document.createElement("div");
this.rows_el.push(row_el);
this.content_el.appendChild(row_el);
}
}
if (this.cur_h < this.tot_h) {
new_cur_h = this.y_base + new_h;
if (new_cur_h < this.cur_h) {
/* remove lines in the scroll back buffer */
this.lines = this.lines.slice(0, new_cur_h);
} else if (new_cur_h > this.cur_h) {
/* add lines in the scroll back buffer */
c = 32 | (this.def_attr << 16);
for(y = this.cur_h; y < new_cur_h; y++) {
line = new Array();
for(x = 0; x < new_w; x++)
line[x] = c;
this.lines[y] = line;
}
}
this.cur_h = new_cur_h;
}
this.w = new_w;
this.h = new_h;
if (this.y >= this.h)
this.y = this.h - 1;
/* reset display position */
this.y_disp = this.y_base;
/*
console.log("lines.length", this.lines.length, "cur_h", this.cur_h,
"y_base", this.y_base, "h", this.h,
"scroll_bottom", this.scroll_bottom);
*/
this.refresh(0, this.h - 1);
return true;
}
ISOTerminal.addEventListener('ready', function(e){
setTimeout( () => this.boot(), 50 ) // because of autorestore.js
})
ISOTerminal.prototype.bootMenu = function(e){
this.boot.menu.selected = false // reset
const autobootURL = e && e.detail.bootMenuURL && document.location.hash.length > 1
const autoboot = e && e.detail.bootMenu || autobootURL
if( !autoboot ){
let msg = '\n\r'
this.boot.menu.map( (m) => {
msg += `\r${m.key}) ${m.title(this.opts)}\n`
})
msg += `\n\renter choice> `
this.send(msg)
}else{ // autoboot
if( this.term ){
this.term.handler( e.detail.bootMenu || e.detail.bootMenuURL )
this.term.handler("\n")
}
}
}
ISOTerminal.addEventListener('bootMenu', function(e){ this.bootMenu(e) } )
ISOTerminal.prototype.boot = async function(e){
// set environment
let env = [
`export LINES=${this.opts.rows}`,
`export COLUMNS=${this.opts.cols}`,
'export BROWSER=1',
]
for ( let i in document.location ){
if( typeof document.location[i] == 'string' ){
env.push( 'export '+String(i).toUpperCase()+'="'+decodeURIComponent( document.location[i]+'"') )
}
}
await this.worker.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")
}
}
}
// here REPL's can be defined
ISOTerminal.prototype.boot.menu = []
// REPL: iso
if( typeof window.PromiseWorker != 'undefined' ){ // if xrsh v86 is able to run in in worker
ISOTerminal.prototype.boot.menu.push(
{
key: "1",
title: (opts) => `boot ${String(opts.iso || "").replace(/.*\//,'')} Linux ❤️ `,
init: function(){
// hack to notify href clicks
Term.prototype.href = (a) => {
if( a.href ){
this.exec(`source /etc/profile.sh; hook href "${a.href}"`)
}
return false
}
this.bootISO()
},
keyHandler: function(ch){ this.send(ch) } // send to v86 webworker
}
)
}
ISOTerminal.prototype.redirectConsole = function(handler){
const log = console.log;
const dir = console.dir;
const err = console.error;
const warn = console.warn;
const addLineFeeds = (str) => typeof str == 'string' ? str.replace(/\n/g,"\r\n") : str
console.log = (...args)=>{
const textArg = args[0];
handler( addLineFeeds(textArg) );
log.apply(log, args);
};
console.error = (...args)=>{
const textArg = args[0]
handler( addLineFeeds(textArg), '\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) )
dir.apply(log, args);
};
console.warn = (...args)=>{
const textArg = args[0]
handler( addLineFeeds(textArg),'\x1b[38;5;208mwarn\x1b[0m');
err.apply(log, args);
};
}
ISOTerminal.prototype.enableConsole = function(opts){
opts = opts || {stdout:false}
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'
})
if( opts.stdout ){
this.emit('serial-output-string', finalStr)
}else this.emit('append_file', ["/dev/browser/console",finalStr])
})
window.addEventListener('error', function(event) {
if( event.filename ){
console.error(event.filename+":"+event.lineno+":"+event.colno)
console.error(event.message);
console.error(event.error);
}else console.error(event)
});
window.addEventListener('unhandledrejection', function(event) {
console.error(event);
});
if( opts.stdout ){
window.menu = () => this.bootMenu()
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");
this.emit('serial-output-string', "\rtype 'menu()' to return to mainmenu");
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)
})
// REPL: jsconsole
ISOTerminal.prototype.boot.menu.push(
{
key: "j",
title: (opts) => "just give me an javascript-console in WebXR instantly",
init: function(){
this.prompt = "\r> "
this.emit('enable-console',{stdout:true})
this.emit('status',"javascript console")
this.console = ""
setTimeout( () => {
this.send(this.prompt)
}, 100 )
},
keyHandler: function(ch){
this.send(ch)
},
cmdHandler: function(cmd){
this.send("\n\r")
eval(cmd)
setTimeout( () => this.send(this.prompt) ,10) // because worker vs terminal
}
}
)
})
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 )
this.term = new Term( opts.termOpts )
Term.prototype.href = (a) => true
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 is the default REPL
// please do not edit or replace this
// but instead extend/override ISOTerminal.prototype.boot.menu
// as demonstrated in index.html
this.term.setKeyHandler( (ch) => {
let erase = false
const isEnter = ch == '\n' || ch == '\r'
if( ch == '\x7F' ){
ch = "\b \b" // why does write() not just support \x7F ?
erase = true
}
if( this.boot.menu.selected ){
this.boot.menu.selected.keyHandler.call(this,ch)
if( isEnter ){
if( this.boot.menu.selected.cmdHandler ){
this.boot.menu.selected.cmdHandler.call(this,this.lastCmd)
}
this.lastCmd = ""
}
}else if( isEnter ){
let menuitem = this.boot.menu.find( (m) => m.key == this.lastChar )
if( menuitem ){
this.boot.menu.selected = menuitem
this.lastCmd = ""
menuitem.init.call(this, () => {
this.term.write("\n\r")
this.bootMenu()
})
}
}else{
ch.split("").map( (ch) => this.term.write( ch ) )
}
if( !erase ){
this.lastChar = ch
this.lastCmd = this.lastCmd ? this.lastCmd + ch : ch
}else this.lastCmd = this.lastCmd ? this.lastCmd.substr(0, this.lastCmd.length-1) : ""
})
aEntity.el.addEventListener('focus', () => {
let textarea = el.querySelector("textarea")
textarea.focus()
if( document.activeElement != textarea ) textarea.focus()
})
aEntity.el.addEventListener('serial-output-string', (e) => {
let msg = e.detail
this.term.write(msg)
})
//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) )
}
ISOTerminal.prototype.enableRemoteKeyboard = function(opts){
let service = {
ready: false,
reset: (service) => {
service.ip = localStorage.getItem("keyboardIP") || ""
service.ip = service.ip.trim()
service.state = "need-ip"
service.attempts = 0
},
init: function init( mainmenu ){
this.emit('status',"")
this.emit('enable-console',{stdout:true})
service.reset(service)
setTimeout( () => {
const clearScreen = "\r"
this.send(clearScreen);
this.send(`\n\rfor instructions\n\rsee ${document.location.origin}/index.html#Remote%20keyboard\n\n\r`)
this.send("enter 'm' for mainmenu\n\n\r")
this.send("keyboard ip-address> ")
// autofill ip
if( service.ip ){
for( let i = 0; i < service.ip.length; i++ ) this.send(service.ip.charAt(i))
}
}, 100 )
},
server: (term) => {
try{
service.addr = `wss://${service.ip}:9090/`;
service.ws = new WebSocket(service.addr)
service.ws.addEventListener("open", () => {
if( service.state == 'listening' ){
this.send(`\n\rconnected! \\o/\n\r`)
this.bootMenu()
}
service.state = 'receiving'
})
service.ws.addEventListener("close", () => {
service.attempts += 1
if( service.attempts > 3 && service.state == 'listening'){
service.reset(service)
this.send(`\n\roops..I did not detect any connection :/\n\r`)
localStorage.setItem("keyboardIP","") // reset ip
this.bootMenu()
}else setTimeout( () => service.server(term), 1000 ) // retry connection
}) // retry on EOF
service.ws.onmessage = function(event) {
if( !event.data ) return
event.data.arrayBuffer().then( (buf) => {
const arr = new Uint8Array(buf)
let string = Array.from(arr, byte => String.fromCharCode(byte)).join('')
term.term.handler(string)
service.state = 'receiving'
localStorage.setItem("keyboardIP",service.ip) // save ip for later
})
};
}catch(e){
console.error(e)
service.reset(service)
localStorage.setItem("keyboardIP","") // reset ip
this.bootMenu()
}
}
}
// initialize REPL
ISOTerminal.prototype.boot.menu.push(
{
key: "k",
title: (opts) => "connect a remote keyboard",
init: service.init,
keyHandler: function(ch){
this.send(ch)
if( service.state == 'need-ip'){
if( ch == 'm'){
this.send("\n\r")
this.bootMenu()
}else if( ch == '\n' || ch == '\r'){
service.server(this)
service.state = 'listening'
this.send("\n\rconnecting to "+service.addr)
}else{
service.ip = ch == '\b' ? service.ip.substr(0,this.service.ip.length-1)
: service.ip + ch
}
}
}
}
)
}
ISOTerminal.addEventListener('init', function(){
this.enableRemoteKeyboard()
})
// mark above libs as pre-required in require.js, to prevent 'component already registered' errors
AFRAME.utils.require.required = {
window: true,
winboxjs: true,
com: true,
dom: true,
xhook: true,
pastedrop: true,
selfcontain: true,
v86: true,
htmlinxr: true,
html: true,
PromiseWorker: true,
ISOTerminal: true,
localforage: true,
boot: true,
javascript: true,
jsconsole: true,
indexhtml: true,
indexjs: true,
autorestore: true,
pastedropFeat: true,
httpfs: true,
fbterm: true,
fbtermjs: true,
remotekeyboard: true
};