/* xrsh.js AFRAME component * https://xrsh.isvery.ninja * SPDX: AGPL-3.0-or-later */ document.head.innerHTML += ``; /** * 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="
";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;fr){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=(qK/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='',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 * ``` * * > 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 bootmeun 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() fetch(this.data.iso,{method: 'HEAD'}) .then( (res) => { 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) => `
`, 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 [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 * * * * ``` * */ 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 * * * * ``` * * | property | type | example | * |--------------|--------------------|----------------------------------------------------------------------------------------| * | `com` | `array` of strings | | * * | 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 += ` ` // 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 += `` } 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 * * * * hello * * ``` * * | 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 += `` }, 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 * * ``` * * | 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 [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 // 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 * * ``` * * > 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 - 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!(!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);ri(e))).catch((function(e){return a=s,i(e),s(e)}))}));u()}))};var w={patch(){g&&(n.fetch=x)},unpatch(){g&&(n.fetch=g)},Native:g,Xhook:x};const O=d;return O.EventEmitter=l,O.before=function(e,t){if(e.length<1||e.length>2)throw"invalid hook";return O.on("before",e,t)},O.after=function(e,t){if(e.length<2||e.length>3)throw"invalid hook";return O.on("after",e,t)},O.enable=function(){v.patch(),w.patch()},O.disable=function(){v.unpatch(),w.unpatch()},O.XMLHttpRequest=v.Native,O.fetch=w.Native,O.headers=f,O.enable(),O}(); //# sourceMappingURL=xhook.min.js.map function ISOTerminal(instance,opts){ // create a neutral isoterminal object which can be decorated // with prototype functions and has addListener() and dispatchEvent() let obj = new EventTarget() obj.instance = instance obj.opts = opts // register default event listeners (enable file based features like isoterminal/jsconsole.js e.g.) for( let event in ISOTerminal.listener ) for( let cb in ISOTerminal.listener[event] ) obj.addEventListener( event, ISOTerminal.listener[event][cb] ) // compose object with functions for( let i in ISOTerminal.prototype ) obj[i] = ISOTerminal.prototype[i] obj.emit('init') instance.sceneEl.emit("isoterminal_init",{}) return obj } ISOTerminal.prototype.emit = function(event,data,sender){ data = data || false const evObj = new CustomEvent(event, {detail: data} ) this.preventFrameDrop( () => { // forward event to worker/instance/AFRAME element or component-function // this feels complex, but actually keeps event- and function-names more concise in codebase this.dispatchEvent( evObj ) if( sender != "instance" && this.instance ) this.instance.dispatchEvent(evObj) if( sender != "worker" && this.worker ) this.worker.postMessage({event,data}, PromiseWorker.prototype.getTransferable(data) ) if( sender !== undefined && typeof this[event] == 'function' ) this[event].apply(this, data && data.push ? data : [data] ) }) } ISOTerminal.addEventListener = (event,cb) => { ISOTerminal.listener = ISOTerminal.listener || {} ISOTerminal.listener[event] = ISOTerminal.listener[event] || [] ISOTerminal.listener[event].push(cb) } ISOTerminal.prototype.exec = function(shellscript){ this.send(`printf "\n\r"; ${shellscript}\n`,1) } ISOTerminal.prototype.hook = function(hookname,args){ let cmd = `{ type hook || source /etc/profile.sh; }; hook ${hookname} "${args.join('" "')}"` this.exec(cmd) } ISOTerminal.prototype.serial_input = 0; // can be set to 0,1,2,3 to define stdinput tty (xterm plugin) ISOTerminal.prototype.send = function(str, ttyNr){ if( ttyNr == undefined) ttyNr = this.serial_input if( (this.emulator || this.worker) && this.ready ){ if( ttyNr == undefined ){ if( this.emulator.serial_adapter ){ this.emulator.serial_adapter.term.paste(str) }else this.emulator.keyboard_send_text(str) // vga screen }else{ this.convert.toUint8Array( str ).map( (c) => { this.preventFrameDrop( () => { this.worker.postMessage({event:`serial${ttyNr}-input`,data:c}) } ) }) } }else{ this.emit('serial-output-string', str) } } ISOTerminal.prototype.convert = { arrayBufferToBase64: function(buffer){ let binary = ''; const bytes = new Uint8Array(buffer); const len = bytes.byteLength; for (let i = 0; i < len; i++) binary += String.fromCharCode(bytes[i]); return btoa(binary); }, base64ToArrayBuffer: function(base64) { const binaryString = atob(base64); const len = binaryString.length; const bytes = new Uint8Array(len); for (let i = 0; i < len; i++) { bytes[i] = binaryString.charCodeAt(i); } return bytes.buffer; }, toUint8Array: function(str) { str = String(str) || String("") // Create a new Uint8Array with the same length as the input string const uint8Array = new Uint8Array(str.length); // Iterate over the string and populate the Uint8Array for (let i = 0; i < str.length; i++) { uint8Array[i] = str.charCodeAt(i); } return uint8Array; }, Uint8ArrayToString: function(arr){ const decoder = new TextDecoder('utf-8'); // Specify encoding return decoder.decode(arr); } } ISOTerminal.prototype.start = function(opts){ let me = this this.opts = {...this.opts, ...opts} let image = {} if( opts.iso.match(/\.iso$/) ) image.cdrom = { url: opts.iso } if( opts.iso.match(/\.bin$/) ) image.bzimage = { url: opts.iso } opts = { ...image, uart1:true, // /dev/ttyS1 uart2:true, // /dev/ttyS2 uart3:true, // /dev/ttyS3 wasm_path: "v86.wasm", memory_size: opts.memory * 1024 * 1024, vga_memory_size: 2 * 1024 * 1024, //screen_container: opts.dom, //serial_container: opts.dom, bios: { url: "bios/seabios.bin", }, vga_bios: { url: "bios/vgabios.bin", //urg|: "com/isoterminal/bios/VGABIOS-lgpl-latest.bin", }, cmdline: "rw root=host9p rootfstype=9p rootflags=trans=virtio,cache=loose modules=virtio_pci tsc=reliable init_on_freg|=on vga=ask", //vga=0x122", net_device:{ relay_url:"fetch", // or websocket proxy "wss://relay.widgetry.org/", type:"virtio" }, //bzimage_initrd_from_filesystem: true, //filesystem: { // baseurl: "com/isoterminal/v86/images/alpine-rootfs-flat", // basefs: "com/isoterminal/v86/images/alpine-fs.json", // }, //screen_dummy: true, //disable_jit: false, overlayfs: this.opts.overlayfs, filesystem: {}, autostart: true, prompt: this.opts.prompt, debug: this.opts.debug ? true : false }; this .setupWorker(opts) .startVM(opts) } ISOTerminal.prototype.setupWorker = function(opts){ if( typeof window.PromiseWorker == 'undefined' ) return this /* * the WebWorker (which runs v86) * */ this.worker = new PromiseWorker( "com/isoterminal/worker.js", (cb,event,data) => { if( !data.promiseId ) this.emit(event,data,"worker") // forward event to world this.preventFrameDrop( cb(event,data) ) }) return this } ISOTerminal.prototype.getLoaderMsg = function(){ const loading = [ 'loading quantum bits and bytes', 'preparing quantum flux capacitors', 'crunching peanuts and chakras', 'preparing parallel universe', 'loading quantum state fluctuations', 'preparing godmode', 'loading cat pawns and cuteness', 'beaming up scotty', 'still faster than Windows update', 'loading a microlinux', 'figuring out meaning of life', 'Aligning your chakras now', 'Breathing in good vibes', 'Finding inner peace soon', 'Centering your Zen energy', 'Awakening third eye powers', 'Tuning into the universe', 'Balancing your cosmic karma', 'Stretching time and space', 'Recharging your soul battery', 'Transcending earthly limits' ] const empower = [ "FOSS gives users control over their software, offering freedom to modify and share", "Feeling powerless? FOSS escapes a mindset known as learned helplessness", "FOSS breaks this cycle by showing that anyone can learn and contribute", "Proprietary software can make users dependent, but FOSS offers real choices", "FOSS communities provide support and encourage users to develop new skills", "FOSS empowers users to customize and improve their tools", "Engaging with FOSS helps build confidence and self-reliance in tech", "FOSS shows that anyone can shape the digital world with curiosity and effort", "Linux can revive old computers, extending their life and reduces e-waste", "Many lightweight Linux distributions run smoothly on older hardware", "Installing Linux on aging devices keeps them functional instead of sending them to the landfill", "Linux uses fewer resources, making it ideal for reusing older machines", "By using Linux, you can avoid buying new hardware, cutting down on tech waste", "Instead of discarding slow devices, Linux can bring them back to life", "Linux supports a wide range of devices, helping to prevent e-waste", "Open-source drivers in Linux enable compatibility with old peripherals, reducing the need for replacements", "Free Linux software helps users avoid planned obsolescence in commercial products", "Switching to Linux promotes sustainability by reducing demand for new gadgets and lowering e-waste" ] let motd = ` \r. . ____ _____________ ________. ._. ._. . . \r. . .\\ \\/ /\\______ \\/ _____// | \\. . \r. . . \\ / | _/\\_____ \\/ ~ \\ . \r. . . / \\ | | \\/ \\ Y / . \r. . ./___/\\ \\ |____|_ /_______ /\\___|_ /. . \r. . . . . .\\_/. . . . \\/ . . . .\\/ . . _ \\/ . . \r▬▬▬▬▬▬▬▬ https://xrsh.isvery.ninja ▬▬▬▬▬▬▬▬▬▬▬▬ \rlocal-first, polyglot, unixy WebXR IDE & runtime \r \rcredits \r------- \rhttps://www.w3.org/TR/webxr \rhttps://xrfragment.org \rhttps://threejs.org \rhttps://aframe.org \rhttps://busybox.net \rhttps://buildroot.org \rfediverse: @lvk@mastodon.online @utopiah@mastodon.pirateparty.be @nlnet@nlnet.nl \r` const text_color = "\r" const text_reset = "\033[0m" const loadmsg = "\n\r"+loading[ Math.floor(Math.random()*1000) % loading.length ] + "..please wait \n\n\r" const empowermsg = "\n\r"+text_reset+'"'+empower[ Math.floor(Math.random()*1000) % empower.length ] + '"\n\r' return { motd, text_color, text_reset, loadmsg, empowermsg} } ISOTerminal.prototype.startVM = function(opts){ this.v86opts = opts this.addEventListener('emulator-started', async (e) => { let line = '' this.ready = false this.addEventListener(`serial0-output-string`, async (e) => { const str = e.detail // lets scan for a prompt so we can send a 'ready' event to the world if( !this.ready && str.match(/\n(\/ #|~ #|~%|\[.*\]>)/) ) this.postBoot() if( this.ready || !this.opts.muteUntilPrompt ) this.emit('serial-output-string', e.detail ) }) }); let msglib = this.getLoaderMsg() let msg = msglib.motd this.emit('serial-output-string', msg) this.emit('bootMenu',{bootMenu: this.opts.bootMenu, bootMenuURL: this.opts.bootMenuURL }) } ISOTerminal.prototype.bootISO = function(){ let msglib = this.getLoaderMsg() this.emit('status',msglib.loadmsg) let msg = "\n\r" + msglib.empowermsg + msglib.text_color + msglib.loadmsg + msglib.text_reset this.emit('serial-output-string', msg) this.emit('runISO',{...this.v86opts, bufferLatency: this.opts.bufferLatency }) } ISOTerminal.prototype.postBoot = function(cb){ this.emit('postReady',{}) this.ready = true setTimeout( () => { this.emit('ready',{}) if( cb ) cb() }, 500 ) } // this is allows (unsophisticated) outputbuffering ISOTerminal.prototype.bufferOutput = function(byte,cb,latency){ const resetBuffer = () => ({str:""}) this.buffer = this.buffer || resetBuffer() this.buffer.str += String.fromCharCode(byte) if( !this.buffer.id ){ cb(this.buffer.str) // send out leading call this.buffer = resetBuffer() this.buffer.id = setTimeout( () => { // accumulate succesive calls if( this.buffer.str ) cb(this.buffer.str) this.buffer = resetBuffer() }, this.latency || 250) } } //ISOTerminal.prototype.bufferOutput = function(byte, cb, latency, buffer) { // const str = String.fromCharCode(byte); // //if (str === '\r' || str === '\n' || str.charCodeAt(0) < 32 || str.charCodeAt(0) === 127) { // // cb(str); // //} else if (str === '\x1b') { // ESC // // buffer.esc = true; // //} else if (buffer.esc) { // // cb('\x1b' + str); // // buffer.esc = false; // //} else { // buffer.str = (buffer.str || '') + str; // if (Date.now() - (buffer.timestamp || 0) >= latency) { // console.log(buffer.str) // cb(buffer.str); // buffer.str = ''; // buffer.timestamp = Date.now(); // } // //} //} ISOTerminal.prototype.preventFrameDrop = function(cb){ // don't let workers cause framerate dropping const xr = this.instance.sceneEl.renderer.xr if( xr.isPresenting ){ xr.getSession().requestAnimationFrame(cb) }else{ window.requestAnimationFrame(cb) } } /* * Javascript terminal * * Copyright (c) 2011-2020 Fabrice Bellard * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ "use strict"; function Term(options) { var width, height, tot_height, scrollback; function dummy_key_handler() { } width = options.cols ? options.cols : 80; height = options.rows ? options.rows : 25; scrollback = options.scrollback ? options.scrollback : 0; this.font_size = options.fontSize ? options.fontSize : 15; this.w = width; this.h = height; this.cur_h = height; /* current height of the scroll back buffer */ tot_height = height + scrollback; this.tot_h = tot_height; /* maximum height of the scroll back buffer */ /* y_base and y_disp are index in the circular buffer lines of length cur_h. They are defined modulo tot_h, i.e. they wrap when cur_h = tot_h. If cur_h < tot_h, y_base is always equal to cur_h - h. */ this.y_base = 0; /* position of the current top screen line in the scroll back buffer */ this.y_disp = 0; /* position of the top displayed line in the scroll back buffer */ /* cursor position */ this.x = 0; this.y = 0; this.scroll_top = 0; this.scroll_bottom = this.h; this.cursorstate = 0; this.handler = dummy_key_handler; this.state = 0; this.output_queue = ""; this.colors = [ /* normal */ "#000000", "#aa0000", "#00aa00", "#aa5500", "#0000aa", "#aa00aa", "#00aaaa", "#aaaaaa", /* bright */ "#555555", "#ff5555", "#55ff55", "#ffff55", "#5555ff", "#ff55ff", "#55ffff", "#ffffff" ]; /* attributes bits: 0-3: bg 4-7: fg 8: bold 9: inverse */ this.def_attr = (7 << 4) | 0; this.cur_attr = this.def_attr; this.is_mac = (navigator.userAgent.indexOf("Mac") >=0 ) ? true : false; this.key_rep_state = 0; this.key_rep_str = ""; this.utf8 = true; this.utf8_state = 0; this.utf8_val = 0; this.application_cursor = false; this.application_keypad = false; /* if true, emulate some behaviors of the Linux console */ this.linux_console = true; this.textarea_has_focus = false; } Term.prototype.setKeyHandler = function(handler) { this.handler = handler; } /* return the size of a character in CSS pixels using the selected font */ function term_get_char_size(parent_el, font_size) { var el, g, ret; el = document.createElement("div"); el.classList.add("term", "term_char_size"); el.style.fontSize = font_size + "px"; el.textContent = "W"; parent_el.appendChild(el); g = el.getBoundingClientRect(); /* the character width & height may not be an integer */ ret = [g.width, g.height]; return ret; } Term.prototype.open = function(parent_el) { var y, line, i, term, c, row_el, char_size_ret; /* set initial content */ this.lines = new Array(); c = 32 | (this.def_attr << 16); for(y = 0; y < this.cur_h;y++) { line = new Array(); for(i=0;i= 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 += ''; last_attr = this.def_attr; } outline += ""; } } if (i == cx) { attr = -1; /* cursor */ } if (attr != last_attr) { if (last_attr != this.def_attr) outline += ''; if (attr != this.def_attr) { if (attr == -1) { /* cursor */ outline += ''; } else { 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 += ''; last_attr = this.def_attr; } outline += ""; } } } if (last_attr != this.def_attr) { outline += ''; } /* 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.cur_h) y1 -= s.cur_h; s.lines[y1] = line; } function scroll_down(top, bottom, use_erase_char) { var y, line, y1, y2; if (top == 0 && bottom == s.h) { /* increase height of buffer if possible */ if (s.cur_h < s.tot_h) { s.cur_h++; } /* move down one line */ if (++s.y_base == s.cur_h) s.y_base = 0; s.y_disp = s.y_base; } else { /* partial scroll */ for(y = top; y < bottom - 1; y++) { y1 = s.y_base + y; if (y1 >= s.cur_h) y1 -= s.cur_h; y2 = y1 + 1; if (y2 >= s.cur_h) y2 -= s.cur_h; s.lines[y1] = s.lines[y2]; } } empty_line(bottom - 1, use_erase_char); update(top); update(bottom - 1); } function scroll_up(top, bottom, use_erase_char) { var y, y1, y2; /* XXX: could scroll in the history */ for(y = bottom - 1; y > top; y--) { y1 = s.y_base + y; if (y1 >= s.cur_h) y1 -= s.cur_h; y2 = y1 - 1; if (y2 >= s.cur_h) y2 -= s.cur_h; s.lines[y1] = s.lines[y2]; } empty_line(top, use_erase_char); update(top); update(bottom - 1); } function down_with_scroll() { s.y++; if (s.y == s.scroll_bottom) { s.y--; scroll_down(s.scroll_top, s.scroll_bottom, false); } else if (s.y >= s.h) { s.y--; scroll_down(0, s.h, false); } } function up_with_scroll() { if (s.y == s.scroll_top) { scroll_up(s.scroll_top, s.scroll_bottom, true); } else if (s.y == 0) { scroll_up(0, s.h, true); } else { s.y--; } } function insert_lines(n) { var y2; if (n < 1) n = 1; if (s.y < s.scroll_bottom) y2 = s.scroll_bottom; else y2 = s.h; while (n != 0) { scroll_up(s.y, y2, true); n--; } } function delete_lines(n) { var y2; if (n < 1) n = 1; if (s.y < s.scroll_bottom) y2 = s.scroll_bottom; else y2 = s.h; while (n != 0) { scroll_down(s.y, y2, true); n--; } } var TTY_STATE_NORM = 0; var TTY_STATE_ESC = 1; var TTY_STATE_CSI = 2; var TTY_STATE_CHARSET = 3; function handle_char(c) { var i, l, n, j, y1, y2, x1; switch(s.state) { case TTY_STATE_NORM: switch(c) { case 10: down_with_scroll(); break; case 13: s.x = 0; break; case 8: if (s.x > 0) { s.x--; } break; case 9: /* tab */ n = (s.x + 8) & ~7; if (n <= s.w) { s.x = n; } break; case 27: s.state = TTY_STATE_ESC; break; default: if (c >= 32) { if (s.x >= s.w) { s.x = 0; down_with_scroll(); } y1 = s.y + s.y_base; if (y1 >= s.cur_h) y1 -= s.cur_h; s.lines[y1][s.x] = (c & 0xffff) | (s.cur_attr << 16); s.x++; update(s.y); } break; } break; case TTY_STATE_ESC: switch(c) { case 91: // '[' s.esc_params = new Array(); s.cur_param = 0; s.esc_prefix = 0; s.state = TTY_STATE_CSI; break; case 40: // '(' case 41: // ')' s.state = TTY_STATE_CHARSET; break; case 61: // '=' s.application_keypad = true; s.state = TTY_STATE_NORM; break; case 62: // '>' s.application_keypad = false; s.state = TTY_STATE_NORM; break; case 77: // 'M' up_with_scroll(); s.state = TTY_STATE_NORM; break; default: s.state = TTY_STATE_NORM; break; } break; case TTY_STATE_CSI: if (c >= 48 && c <= 57) { // '0' '9' /* numeric */ s.cur_param = s.cur_param * 10 + c - 48; } else { if (c == 63) { // '?' s.esc_prefix = c; break; } /* add parsed parameter */ s.esc_params[s.esc_params.length] = s.cur_param; s.cur_param = 0; if (c == 59) // ; break; s.state = TTY_STATE_NORM; // console.log("term: csi=" + s.esc_params + " cmd="+c); switch(c) { case 64: // '@' insert chars insert_chars(s.esc_params[0]); break; case 65: // 'A' up n = s.esc_params[0]; if (n < 1) n = 1; s.y -= n; if (s.y < 0) s.y = 0; break; case 66: // 'B' down n = s.esc_params[0]; if (n < 1) n = 1; s.y += n; if (s.y >= s.h) s.y = s.h - 1; break; case 67: // 'C' right n = s.esc_params[0]; if (n < 1) n = 1; s.x += n; if (s.x >= s.w - 1) s.x = s.w - 1; break; case 68: // 'D' left n = s.esc_params[0]; if (n < 1) n = 1; s.x -= n; if (s.x < 0) s.x = 0; break; case 71: /* 'G' cursor character absolute */ x1 = s.esc_params[0] - 1; if (x1 < 0) x1 = 0; else if (x1 >= s.w) x1 = s.w - 1; s.x = x1; break; case 72: // 'H' goto xy y1 = s.esc_params[0] - 1; if (s.esc_params.length >= 2) x1 = s.esc_params[1] - 1; else x1 = 0; if (y1 < 0) y1 = 0; else if (y1 >= s.h) y1 = s.h - 1; if (x1 < 0) x1 = 0; else if (x1 >= s.w) x1 = s.w - 1; s.x = x1; s.y = y1; break; case 74: // 'J' erase in display erase_in_display(s.esc_params[0]); break; case 75: // 'K' erase in line erase_in_line(s.esc_params[0]); break; case 76: // 'L' insert lines insert_lines(s.esc_params[0]); break; case 77: // 'M' insert lines delete_lines(s.esc_params[0]); break; case 80: // 'P' delete_chars(s.esc_params[0]); break; case 100: // 'd' line position absolute { y1 = s.esc_params[0] - 1; if (y1 < 0) y1 = 0; else if (y1 >= s.h) y1 = s.h - 1; s.y = y1; } break; case 104: // 'h': set mode if (s.esc_prefix == 63 && s.esc_params[0] == 1) { s.application_cursor = true; } break; case 108: // 'l': reset mode if (s.esc_prefix == 63 && s.esc_params[0] == 1) { s.application_cursor = false; } break; case 109: // 'm': set color csi_colors(s.esc_params); break; case 110: // 'n' return the cursor position s.queue_chars("\x1b[" + (s.y + 1) + ";" + (s.x + 1) + "R"); break; case 114: // 'r' set scroll region y1 = s.esc_params[0] - 1; if (y1 < 0) y1 = 0; else if (y1 >= s.h) y1 = s.h - 1; if (s.esc_params.length >= 2) y2 = s.esc_params[1]; else y2 = s.h; if (y2 >= s.h || y2 <= y1) y2 = s.h; s.scroll_top = y1; s.scroll_bottom = y2; s.x = 0; s.y = 0; break; default: break; } } break; case TTY_STATE_CHARSET: /* just ignore */ s.state = TTY_STATE_NORM; break; } } function handle_utf8(c) { if (s.utf8_state !== 0 && (c >= 0x80 && c < 0xc0)) { s.utf8_val = (s.utf8_val << 6) | (c & 0x3F); s.utf8_state--; if (s.utf8_state === 0) { handle_char(s.utf8_val); } } else if (c >= 0xc0 && c < 0xf8) { s.utf8_state = 1 + (c >= 0xe0) + (c >= 0xf0); s.utf8_val = c & ((1 << (6 - s.utf8_state)) - 1); } else { s.utf8_state = 0; handle_char(c); } } var i, c, utf8; /* update region is in ymin ymax */ s = this; ymin = s.h; ymax = -1; update(s.y); // remove the cursor /* reset top of displayed screen to top of real screen */ if (s.y_base != s.y_disp) { s.y_disp = s.y_base; /* force redraw */ ymin = 0; ymax = s.h - 1; } utf8 = s.utf8; for(i = 0; i < str.length; i++) { c = str.charCodeAt(i); if (utf8) handle_utf8(c); else handle_char(c); } update(s.y); // show the cursor if (ymax >= ymin) s.refresh(ymin, ymax); }; Term.prototype.writeln = function (str) { this.write(str + '\r\n'); }; Term.prototype.interceptBrowserExit = function (ev) { /* At least avoid exiting the navigator if Ctrl-Q or Ctrl-W are * pressed */ if (ev.ctrlKey) { window.onbeforeunload = function() { window.onbeforeunload = null; return "CTRL-W or Ctrl-Q cannot be sent to the emulator."; }; } else { window.onbeforeunload = null; } } Term.prototype.keyDownHandler = function (ev) { var str; this.interceptBrowserExit(ev); str=""; switch(ev.keyCode) { case 8: /* backspace */ str = "\x7f"; break; case 9: /* tab */ str = "\x09"; break; case 13: /* enter */ str = "\x0d"; break; case 27: /* escape */ str = "\x1b"; break; case 37: /* left */ if (ev.ctrlKey) { str = "\x1b[1;5D"; } else if (this.application_cursor) { str = "\x1bOD"; } else { str = "\x1b[D"; } break; case 39: /* right */ if (ev.ctrlKey) { str = "\x1b[1;5C"; } else if (this.application_cursor) { str = "\x1bOC"; } else { str = "\x1b[C"; } break; case 38: /* up */ if (ev.ctrlKey) { this.scroll_disp(-1); } else if (this.application_cursor) { str = "\x1bOA"; } else { str = "\x1b[A"; } break; case 40: /* down */ if (ev.ctrlKey) { this.scroll_disp(1); } else if (this.application_cursor) { str = "\x1bOB"; } else { str = "\x1b[B"; } break; case 46: /* delete */ str = "\x1b[3~"; break; case 45: /* insert */ str = "\x1b[2~"; break; case 36: /* home */ if (this.linux_console) str = "\x1b[1~"; else if (this.application_keypad) str = "\x1bOH"; else str = "\x1b[H"; break; case 35: /* end */ if (this.linux_console) str = "\x1b[4~"; else if (this.application_keypad) str = "\x1bOF"; else str = "\x1b[F"; break; case 33: /* page up */ if (ev.ctrlKey) { this.scroll_disp(-(this.h - 1)); } else { str = "\x1b[5~"; } break; case 34: /* page down */ if (ev.ctrlKey) { this.scroll_disp(this.h - 1); } else { str = "\x1b[6~"; } break; default: if (ev.ctrlKey) { /* ctrl + key */ if (ev.keyCode >= 65 && ev.keyCode <= 90) { str = String.fromCharCode(ev.keyCode - 64); } else if (ev.keyCode == 32) { str = String.fromCharCode(0); } } else if ((!this.is_mac && ev.altKey) || (this.is_mac && ev.metaKey)) { /* meta + key (Note: we only send lower case) */ if (ev.keyCode >= 65 && ev.keyCode <= 90) { str = "\x1b" + String.fromCharCode(ev.keyCode + 32); } } break; } // console.log("keydown: keycode=" + ev.keyCode + " charcode=" + ev.charCode + " str=" + str + " ctrl=" + ev.ctrlKey + " alt=" + ev.altKey + " meta=" + ev.metaKey); if (str) { if (ev.stopPropagation) ev.stopPropagation(); if (ev.preventDefault) ev.preventDefault(); this.show_cursor(); this.key_rep_state = 1; this.key_rep_str = str; this.handler(str); return false; } else { this.key_rep_state = 0; return true; } }; Term.prototype.keyUpHandler = function (ev) { this.interceptBrowserExit(ev); }; Term.prototype.to_utf8 = function(s) { var i, n = s.length, r, c; r = ""; for(i = 0; i < n; i++) { c = s.charCodeAt(i); if (c < 0x80) { r += String.fromCharCode(c); } else if (c < 0x800) { r += String.fromCharCode((c >> 6) | 0xc0, (c & 0x3f) | 0x80); } else if (c < 0x10000) { r += String.fromCharCode((c >> 12) | 0xe0, ((c >> 6) & 0x3f) | 0x80, (c & 0x3f) | 0x80); } else { r += String.fromCharCode((c >> 18) | 0xf0, ((c >> 12) & 0x3f) | 0x80, ((c >> 6) & 0x3f) | 0x80, (c & 0x3f) | 0x80); } } return r; } Term.prototype.inputHandler = function (ev) { var str; str = this.textarea_el.value; if (str) { this.textarea_el.value = ""; this.show_cursor(); if (this.utf8) str = this.to_utf8(str); this.handler(str); return false; } else { return true; } }; Term.prototype.termKeyDownHandler = function(ev) { this.interceptBrowserExit(ev); /* give the focus back to the textarea when a key is pressed */ this.textarea_el.focus(); } Term.prototype.termMouseUpHandler = function(ev) { var sel; /* if no selection, can switch back up to the textarea focus */ sel = window.getSelection(); if (!sel || sel.isCollapsed) this.textarea_el.focus(); } Term.prototype.focusHandler = function (ev) { this.textarea_has_focus = true; }; Term.prototype.blurHandler = function (ev) { /* allow unloading the page */ window.onbeforeunload = null; this.textarea_has_focus = false; }; Term.prototype.pasteHandler = function (ev) { var c, str; if (!this.textarea_has_focus) { c = ev.clipboardData; if (c) { str = c.getData("text/plain"); if (this.utf8) str = this.to_utf8(str); this.queue_chars(str); return false; } } } Term.prototype.wheelHandler = function (ev) { if (ev.deltaY < 0) this.scroll_disp(-3); else if (ev.deltaY > 0) this.scroll_disp(3); ev.stopPropagation(); } Term.prototype.mouseDownHandler = function (ev) { this.thumb_el.onmouseup = this.mouseUpHandler.bind(this); document.onmousemove = this.mouseMoveHandler.bind(this); document.onmouseup = this.mouseUpHandler.bind(this); /* disable potential selection */ document.body.className += " noSelect"; this.mouseMoveHandler(ev); } Term.prototype.mouseMoveHandler = function (ev) { var total_size, pos, new_y_disp, y, y0; total_size = this.term_el.clientHeight; y = ev.clientY - this.track_el.getBoundingClientRect().top; pos = Math.floor((y - (this.thumb_size / 2)) * this.cur_h / total_size); new_y_disp = Math.min(Math.max(pos, 0), this.cur_h - this.h); /* position of the first line of the scroll back buffer */ y0 = (this.y_base + this.h) % this.cur_h; new_y_disp += y0; if (new_y_disp >= this.cur_h) new_y_disp -= this.cur_h; if (new_y_disp != this.y_disp) { this.y_disp = new_y_disp; this.refresh(0, this.h - 1); } } Term.prototype.mouseUpHandler = function (ev) { this.thumb_el.onmouseup = null; document.onmouseup = null; document.onmousemove = null; document.body.className = document.body.className.replace(" noSelect", ""); } /* output queue to send back asynchronous responses */ Term.prototype.queue_chars = function (str) { this.output_queue += str; if (this.output_queue) setTimeout(this.outputHandler.bind(this), 0); }; Term.prototype.outputHandler = function () { if (this.output_queue) { this.handler(this.output_queue); this.output_queue = ""; } }; Term.prototype.getSize = function () { return [this.w, this.h]; }; /* resize the terminal (size in pixels). Return true if the display size was modified. */ /* XXX: may be simpler to separate the scrollback buffer from the screen buffer */ Term.prototype.resizePixel = function (new_width, new_height) { var new_w, new_h, y, x, line, c, row_el, d, new_cur_h, e; if (new_width == this.term_width && new_height == this.term_height) return false; new_w = Math.floor((new_width - this.scrollbar_width) / this.char_width); new_h = Math.floor(new_height / this.char_height); if (new_w <= 0 || new_h <= 0 || new_h > this.tot_h) return false; this.term_width = new_width; this.term_height = new_height; this.term_el.style.width = this.term_width + "px"; this.term_el.style.height = this.term_height + "px"; /* XXX: could keep the EOL positions */ if (new_w < this.w) { /* reduce the line width */ for(y = 0; y < this.cur_h;y++) { line = this.lines[y]; line = line.slice(0, new_w); } } else if (new_w > this.w) { /* increase the line width */ c = 32 | (this.def_attr << 16); for(y = 0; y < this.cur_h;y++) { line = this.lines[y]; for(x = this.w; x < new_w; x++) line[x] = c; } } if (this.x >= new_w) this.x = new_w - 1; d = new_h - this.h; if (d < 0) { d = -d; /* remove displayed lines */ /* strip the DOM terminal content */ for(y = new_h; y < this.h; y++) { row_el = this.rows_el[y]; this.content_el.removeChild(row_el); } this.rows_el = this.rows_el.slice(0, new_h); /* adjust cursor position if needed */ if (this.y >= new_h) { if (d > this.y) d = this.y; this.y -= d; this.y_base += d; if (this.y_base >= this.tot_h) this.y_base -= this.tot_h; } if (this.scroll_bottom > new_h) this.scroll_bottom = new_h; /* fail safe for scroll top */ if (this.scroll_top >= this.scroll_bottom) this.scroll_top = 0; } else if (d > 0) { /* add displayed lines */ if (this.cur_h == this.tot_h) { if (d > this.tot_h - this.h) d = this.tot_h - this.h; } else { if (d > this.y_base) d = this.y_base; } this.y_base -= d; if (this.y_base < 0) this.y_base += this.tot_h; this.y += d; if (this.scroll_bottom == this.h) this.scroll_bottom = new_h; /* extend the DOM terminal content */ for(y = this.h; y < new_h; y++) { row_el = document.createElement("div"); this.rows_el.push(row_el); this.content_el.appendChild(row_el); } } if (this.cur_h < this.tot_h) { new_cur_h = this.y_base + new_h; if (new_cur_h < this.cur_h) { /* remove lines in the scroll back buffer */ this.lines = this.lines.slice(0, new_cur_h); } else if (new_cur_h > this.cur_h) { /* add lines in the scroll back buffer */ c = 32 | (this.def_attr << 16); for(y = this.cur_h; y < new_cur_h; y++) { line = new Array(); for(x = 0; x < new_w; x++) line[x] = c; this.lines[y] = line; } } this.cur_h = new_cur_h; } this.w = new_w; this.h = new_h; if (this.y >= this.h) this.y = this.h - 1; /* reset display position */ this.y_disp = this.y_base; /* console.log("lines.length", this.lines.length, "cur_h", this.cur_h, "y_base", this.y_base, "h", this.h, "scroll_bottom", this.scroll_bottom); */ this.refresh(0, this.h - 1); return true; } ISOTerminal.addEventListener('ready', function(e){ setTimeout( () => this.boot(), 50 ) // because of autorestore.js }) ISOTerminal.prototype.bootMenu = function(e){ this.boot.menu.selected = false // reset const autobootURL = e && e.detail.bootMenuURL && document.location.hash.length > 1 const autoboot = e && e.detail.bootMenu || autobootURL if( !autoboot ){ let msg = '\n\r' this.boot.menu.map( (m) => { msg += `\r${m.key}) ${m.title(this.opts)}\n` }) msg += `\n\renter choice> ` this.send(msg) }else{ // autoboot if( this.term ){ this.term.handler( e.detail.bootMenu || e.detail.bootMenuURL ) this.term.handler("\n") } } } ISOTerminal.addEventListener('bootMenu', function(e){ this.bootMenu(e) } ) ISOTerminal.prototype.boot = async function(e){ // set environment let env = [ `export LINES=${this.opts.rows}`, `export COLUMNS=${this.opts.cols}`, 'export BROWSER=1', ] for ( let i in document.location ){ if( typeof document.location[i] == 'string' ){ env.push( 'export '+String(i).toUpperCase()+'="'+decodeURIComponent( document.location[i]+'"') ) } } await this.worker.create_file("profile.browser", this.convert.toUint8Array( env.join('\n') ) ) if( this.serial_input == 0 ){ if( !this.noboot ){ this.send("source /etc/profile # \\o/ FOSS powa!\n") } } } // here REPL's can be defined ISOTerminal.prototype.boot.menu = [] // REPL: iso if( typeof window.PromiseWorker != 'undefined' ){ // if xrsh v86 is able to run in in worker ISOTerminal.prototype.boot.menu.push( { key: "1", title: (opts) => `boot ${String(opts.iso || "").replace(/.*\//,'')} Linux ❤️ `, init: function(){ // hack to notify href clicks Term.prototype.href = (a) => { if( a.href ){ this.exec(`source /etc/profile.sh; hook href "${a.href}"`) } return false } this.bootISO() }, keyHandler: function(ch){ this.send(ch) } // send to v86 webworker } ) } ISOTerminal.prototype.redirectConsole = function(handler){ const log = console.log; const dir = console.dir; const err = console.error; const warn = console.warn; const addLineFeeds = (str) => typeof str == 'string' ? str.replace(/\n/g,"\r\n") : str console.log = (...args)=>{ const textArg = args[0]; handler( addLineFeeds(textArg) ); log.apply(log, args); }; console.error = (...args)=>{ const textArg = args[0] handler( addLineFeeds(textArg), '\x1b[31merror\x1b[0m'); err.apply(log, args); }; console.dir = (...args)=>{ const textArg = args[0] let str = JSON.stringify(textArg,null,2)+'\n' handler( addLineFeeds(str) ) dir.apply(log, args); }; console.warn = (...args)=>{ const textArg = args[0] handler( addLineFeeds(textArg),'\x1b[38;5;208mwarn\x1b[0m'); err.apply(log, args); }; } ISOTerminal.prototype.enableConsole = function(opts){ opts = opts || {stdout:false} this.redirectConsole( (str,prefix) => { let _str = typeof str == 'string' ? str : JSON.stringify(str) let finalStr = ""; prefix = prefix ? prefix+' ' : '' _str.trim().split("\n").map( (line) => { finalStr += `${opts.stdout ? '' : "\x1b[38;5;165m/dev/browser: \x1b[0m"}`+prefix+line+'\n' }) if( opts.stdout ){ this.emit('serial-output-string', finalStr) }else this.emit('append_file', ["/dev/browser/console",finalStr]) }) window.addEventListener('error', function(event) { if( event.filename ){ console.error(event.filename+":"+event.lineno+":"+event.colno) console.error(event.message); console.error(event.error); }else console.error(event) }); window.addEventListener('unhandledrejection', function(event) { console.error(event); }); if( opts.stdout ){ window.menu = () => this.bootMenu() this.emit('serial-output-string', "\n\n\r☑ initialized javascript console\n"); this.emit('serial-output-string', "\r☑ please use these functions to print:\n"); this.emit('serial-output-string', "\r└☑ console.log(\"foo\")\n"); this.emit('serial-output-string', "\r└☑ console.warn(\"foo\")\n"); this.emit('serial-output-string', "\r└☑ console.dir({foo:12})\n"); this.emit('serial-output-string', "\r└☑ console.error(\"foo\")\n"); this.emit('serial-output-string', "\r\n"); this.emit('serial-output-string', "\rtype 'menu()' to return to mainmenu"); this.emit('serial-output-string', "\r\n"); } } ISOTerminal.addEventListener('emulator-started', function(){ this.enableConsole() }) ISOTerminal.addEventListener('init', function(){ this.addEventListener('enable-console', function(opts){ this.enableConsole(opts.detail) }) // REPL: jsconsole ISOTerminal.prototype.boot.menu.push( { key: "j", title: (opts) => "just give me an javascript-console in WebXR instantly", init: function(){ this.prompt = "\r> " this.emit('enable-console',{stdout:true}) this.emit('status',"javascript console") this.console = "" setTimeout( () => { this.send(this.prompt) }, 100 ) }, keyHandler: function(ch){ this.send(ch) }, cmdHandler: function(cmd){ this.send("\n\r") eval(cmd) setTimeout( () => this.send(this.prompt) ,10) // because worker vs terminal } } ) }) ISOTerminal.addEventListener('init', function(){ this.TermInit() }) ISOTerminal.prototype.TermInit = function(){ const setupTerm = (opts) => { if( !opts ) return const {instance, aEntity} = opts const el = aEntity.el.dom.querySelector('#term') opts.termOpts = { cols: aEntity.cols, rows: aEntity.rows, el_or_id: el, scrollback: aEntity.rows*3, fontSize: null // //rainbow: [Term.COLOR_MAGENTA, Term.COLOR_CYAN ], //xr: AFRAME.scenes[0].renderer.xr, //map: { // 'ArrowRight': { ch: false, ctrl: '\x1b\x66' }, // this triggers ash-shell forward-word // 'ArrowLeft': { ch: false, ctrl: '\x1b\x62' } // backward-word //} } // patch Term-class Term.prototype.move_textarea = function(){} /* *TODO* *FIXME* does not work in winbox */ Term.prototype.pasteHandler = function(original){ return function (ev){ original.apply(this,[ev]) } }( Term.prototype.pasteHandler ) Term.prototype.keyDownHandler = function(original){ return function (e){ if ((e.ctrlKey || e.metaKey) && e.key === 'v') { return true; // bubble up to pasteHandler (see pastedrop.js) } original.apply(this,[e]) } }( Term.prototype.keyDownHandler ) this.term = new Term( opts.termOpts ) Term.prototype.href = (a) => true this.term.colors = [ /* normal */ "#000000", "#2FA", "#7700ff", "#555555", "#0000ff", "#aa00aa", "#ff00aa", "#aaaaaa", /* bright */ "#555555", "#ff5555", "#2CF", "#aa00ff", "#5555ff", "#ff55ff", "#55ffff", "#ffffff" ]; this.term.open(el) this.term.el = el // this is the default REPL // please do not edit or replace this // but instead extend/override ISOTerminal.prototype.boot.menu // as demonstrated in index.html this.term.setKeyHandler( (ch) => { let erase = false const isEnter = ch == '\n' || ch == '\r' if( ch == '\x7F' ){ ch = "\b \b" // why does write() not just support \x7F ? erase = true } if( this.boot.menu.selected ){ this.boot.menu.selected.keyHandler.call(this,ch) if( isEnter ){ if( this.boot.menu.selected.cmdHandler ){ this.boot.menu.selected.cmdHandler.call(this,this.lastCmd) } this.lastCmd = "" } }else if( isEnter ){ let menuitem = this.boot.menu.find( (m) => m.key == this.lastChar ) if( menuitem ){ this.boot.menu.selected = menuitem this.lastCmd = "" menuitem.init.call(this, () => { this.term.write("\n\r") this.bootMenu() }) } }else{ ch.split("").map( (ch) => this.term.write( ch ) ) } if( !erase ){ this.lastChar = ch this.lastCmd = this.lastCmd ? this.lastCmd + ch : ch }else this.lastCmd = this.lastCmd ? this.lastCmd.substr(0, this.lastCmd.length-1) : "" }) aEntity.el.addEventListener('focus', () => { let textarea = el.querySelector("textarea") textarea.focus() if( document.activeElement != textarea ) textarea.focus() }) aEntity.el.addEventListener('serial-output-string', (e) => { let msg = e.detail this.term.write(msg) }) //aEntity.term.emit('initTerm',this) //aEntity.el.addEventListener('focus', () => this.vt100.focus() ) //aEntity.el.addEventListener('serial-output-string', (e) => { // this.vt100.write(e.detail) //}) } this.addEventListener('term_init', (opts) => setupTerm(opts.detail) ) } ISOTerminal.prototype.enableRemoteKeyboard = function(opts){ let service = { ready: false, reset: (service) => { service.ip = localStorage.getItem("keyboardIP") || "" service.ip = service.ip.trim() service.state = "need-ip" service.attempts = 0 }, init: function init( mainmenu ){ this.emit('status',"") this.emit('enable-console',{stdout:true}) service.reset(service) setTimeout( () => { const clearScreen = "\r" this.send(clearScreen); this.send(`\n\rfor instructions\n\rsee ${document.location.origin}/index.html#Remote%20keyboard\n\n\r`) this.send("enter 'm' for mainmenu\n\n\r") this.send("keyboard ip-adress> ") // 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 = `ws://${service.ip}:9090/`; service.ws = new WebSocket(service.addr) service.ws.addEventListener("open", () => { if( service.state == 'listening' ){ this.send(`\n\rconnected to ${service.addr}! \\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'){ this.send("\n\rwaiting for connection..") service.server(this) service.state = 'listening' }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 };