4385 lines
160 KiB
JavaScript
4385 lines
160 KiB
JavaScript
|
||
/* 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[38;5;57m. . ____ _____________ ________. ._. ._. . .
|
||
\r[38;5;93m. . .\\ \\/ /\\______ \\/ _____// | \\. .
|
||
\r[38;5;93m. . . \\ / | _/\\_____ \\/ ~ \\ .
|
||
\r[38;5;129m. . . / \\ | | \\/ \\ Y / .
|
||
\r[38;5;165m. . ./___/\\ \\ |____|_ /_______ /\\___|_ /. .
|
||
\r[38;5;201m. . . . . .\\_/. . . . \\/ . . . .\\/ . . _ \\/ . .
|
||
\r[38;5;165m▬▬▬▬▬▬▬▬ [38;5;51mhttps://xrsh.isvery.ninja[38;5;165m ▬▬▬▬▬▬▬▬▬▬▬▬
|
||
\r[38;5;165mlocal-first, polyglot, unixy WebXR IDE & runtime
|
||
\r[38;5;57m
|
||
\rcredits
|
||
\r-------
|
||
\r[38;5;51mhttps://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[38;5;129m"
|
||
const text_reset = "\033[0m"
|
||
const loadmsg = "\n\r"+loading[ Math.floor(Math.random()*1000) % loading.length ] + "..please wait \n\n\r"
|
||
const empowermsg = "\n\r"+text_reset+'"'+empower[ Math.floor(Math.random()*1000) % empower.length ] + '"\n\r'
|
||
return { motd, text_color, text_reset, loadmsg, empowermsg}
|
||
}
|
||
|
||
ISOTerminal.prototype.startVM = function(opts){
|
||
|
||
this.v86opts = opts
|
||
|
||
this.addEventListener('emulator-started', async (e) => {
|
||
|
||
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 += " ";
|
||
break;
|
||
case 38: // '&'
|
||
outline += "&";
|
||
break;
|
||
case 60: // '<'
|
||
outline += "<";
|
||
break;
|
||
case 62: // '>'
|
||
outline += ">";
|
||
break;
|
||
default:
|
||
if (c < 32) {
|
||
outline += " ";
|
||
} 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, " ");
|
||
if (outline == "")
|
||
outline = " ";
|
||
|
||
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[36m${m.key})[0m ${m.title(this.opts)}\n`
|
||
})
|
||
msg += `\n\r[36menter choice>[0m `
|
||
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 [31m${String(opts.iso || "").replace(/.*\//,'')}[0m 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[36m>[0m "
|
||
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 = "[1;1H[2J\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("[36mkeyboard ip-address> [0m")
|
||
// 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
|
||
};
|