diff --git a/com/codemirror.js b/com/codemirror.js index fba6327..c83563d 100644 --- a/com/codemirror.js +++ b/com/codemirror.js @@ -110,6 +110,7 @@ AFRAME.registerComponent('codemirror', { // as it would require all kindof ugly stringescaping console.log("updating "+file) await this.isoterminal.worker.update_file(file, this.isoterminal.convert.toUint8Array(str) ) + this.isoterminal.exec("touch "+file) // *FIXME* notify filesystem (why does inotifyd need this? v86's 9pfees is cached?) }, events:{ diff --git a/com/isoterminal.js b/com/isoterminal.js index 8dd1a1d..664d0db 100644 --- a/com/isoterminal.js +++ b/com/isoterminal.js @@ -55,6 +55,7 @@ if( typeof AFRAME != 'undefined '){ this.calculateDimension() this.initHud() this.setupBox() + this.setupPasteDrop() fetch(this.data.iso,{method: 'HEAD'}) .then( (res) => { @@ -71,10 +72,11 @@ if( typeof AFRAME != 'undefined '){ requires:{ com: "com/dom.js", window: "com/window.js", + pastedrop: "com/pastedrop.js", v86: "com/isoterminal/libv86.js", vt100: "com/isoterminal/VT100.js", // allow xrsh to selfcontain scene + itself - xhook: "https://jpillora.com/xhook/dist/xhook.min.js", + xhook: "com/lib/xhook.min.js", selfcontain: "com/selfcontainer.js", // html to texture htmlinxr: "com/html-as-texture-in-xr.js", @@ -201,7 +203,8 @@ if( typeof AFRAME != 'undefined '){ jsconsole: "com/isoterminal/feat/jsconsole.js", indexhtml: "com/isoterminal/feat/index.html.js", indexjs: "com/isoterminal/feat/index.js.js", - autorestore: "com/isoterminal/feat/autorestore.js", + autorestore: "com/isoterminal/feat/autorestore.js", + pastedropFeat: "com/isoterminal/feat/pastedrop.js", }) this.el.setAttribute("selfcontainer","") @@ -244,7 +247,8 @@ if( typeof AFRAME != 'undefined '){ this.term.start(opts) }) - instance.setAttribute("dom", "") + instance.setAttribute("dom", "") + instance.setAttribute("pastedrop", "") this.term.addEventListener('ready', (e) => { instance.dom.classList.remove('blink') @@ -302,7 +306,7 @@ if( typeof AFRAME != 'undefined '){ setupVT100: function(instance){ const el = this.el.dom.querySelector('#term') - const opts = { + this.term.opts.vt100 = { cols: this.cols, rows: this.rows, el_or_id: el, @@ -311,7 +315,8 @@ if( typeof AFRAME != 'undefined '){ rainbow: [VT100.COLOR_MAGENTA, VT100.COLOR_CYAN ], xr: AFRAME.scenes[0].renderer.xr } - this.vt100 = new VT100( opts ) + this.term.emit('initVT100',this) + this.vt100 = new VT100( this.term.opts.vt100 ) this.vt100.el = el this.vt100.curs_set( 1, true) this.vt100.focus() @@ -328,6 +333,24 @@ if( typeof AFRAME != 'undefined '){ this.el.addEventListener('serial-output-string', (e) => { this.vt100.write(e.detail) }) + + // translate file upload into pasteFile + this.vt100.upload.addEventListener('change', (e) => { + const file = this.vt100.upload.files[0]; + const item = {...file, getAsFile: () => file } + this.el.emit('pasteFile', { item, type: file.type }); + }) + + return this + }, + + setupPasteDrop: function(){ + this.el.addEventListener('pasteFile', (e) => { + e.preventDefault() // prevent bubbling up to window (which is triggering this initially) + if( !this.term.pasteFile ) return // skip if feat/pastedrop.js is not loaded + this.term.pasteFile(e.detail) + }) + return this }, setupBox: function(){ diff --git a/com/isoterminal/ISOTerminal.js b/com/isoterminal/ISOTerminal.js index 8fc822f..a0881ae 100644 --- a/com/isoterminal/ISOTerminal.js +++ b/com/isoterminal/ISOTerminal.js @@ -17,14 +17,14 @@ function ISOTerminal(instance,opts){ ISOTerminal.prototype.emit = function(event,data,sender){ data = data || false const evObj = new CustomEvent(event, {detail: data} ) - //this.preventFrameDrop( () => { + 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) => { @@ -37,6 +37,10 @@ ISOTerminal.prototype.exec = function(shellscript){ this.send(shellscript+"\n",1) } +ISOTerminal.prototype.hook = function(hookname,args){ + this.exec(`{ type hook || source /etc/profile.sh; }; hook ${hookname} "${args.join('" "')}"`) +} + ISOTerminal.prototype.serial_input = 0; // can be set to 0,1,2,3 to define stdinput tty (xterm plugin) ISOTerminal.prototype.send = function(str, ttyNr){ @@ -48,7 +52,9 @@ ISOTerminal.prototype.send = function(str, ttyNr){ }else{ this.convert.toUint8Array( str ).map( (c) => { this.preventFrameDrop( - () => this.worker.postMessage({event:`serial${ttyNr}-input`,data:c}) + () => { + this.worker.postMessage({event:`serial${ttyNr}-input`,data:c}) + } ) }) } @@ -187,7 +193,6 @@ ISOTerminal.prototype.startVM = function(opts){ "Learned helplessness fades when we realize tech isn’t too complex to understand", "FOSS empowers users to customize and improve their tools", "Engaging with FOSS helps build confidence and self-reliance in tech", - "FOSS tools are accessible and often better than closed alternatives", "FOSS shows that anyone can shape the digital world with curiosity and effort", "Linux can revive old computers, extending their life and reducing e-waste", "Many lightweight Linux distributions run smoothly on older hardware", diff --git a/com/isoterminal/VT100.js b/com/isoterminal/VT100.js index 6dd2e0d..a16c5d2 100644 --- a/com/isoterminal/VT100.js +++ b/com/isoterminal/VT100.js @@ -121,7 +121,7 @@ function VT100(opts) } this.scr_ = scr; this.scr_.style.display = 'inline' - this.setupTouchInputFallback() // smartphone + this.setupTouchInputFallback() // smartphone/android this.cursor_vis_ = true; this.cursor_key_mode_ = VT100.CK_CURSOR; this.grab_events_ = false; @@ -218,7 +218,7 @@ VT100.handle_onkeypress_ = function VT100_handle_onkeypress(event,cb) ch = '\n'; } } else { - switch (event.key) { + switch (event.code) { case "Backspace": ch = '\b'; break; @@ -267,13 +267,17 @@ VT100.handle_onkeypress_ = function VT100_handle_onkeypress(event,cb) break; } } - // Stop the event from doing anything else. - event.preventDefault(); - vt.key_buf_.push(ch); + + // Workaround: top the event from doing anything else. + // (prevent input from adding characters instead of via VM) + event.preventDefault() + vt.key_buf_.push(ch); + if( cb ){ cb(vt.key_buf_) vt.key_buf_ = [] }else setTimeout(VT100.go_getch_, 0); + return false; } @@ -287,6 +291,7 @@ VT100.handle_onkeydown_ = function VT100_handle_onkeydown() default: return true; } + event.preventDefault() vt.key_buf_.push(ch); setTimeout(VT100.go_getch_, 0); return false; @@ -1079,7 +1084,7 @@ VT100.prototype.write = function VT100_write(stuff) x = this.csi_parms_[j]; if( x > 89 && x < 98 && this.opts.rainbow ){ const rainbow = this.opts.rainbow - this.fgset( rainbow[ Math.floor( Math.random() * 1000 ) % rainbow.length ] ) + this.fgset( rainbow[ x % rainbow.length ] ) } switch (x) { case 0: @@ -1325,18 +1330,28 @@ VT100.prototype.throttleSmart = function throttleSmart(fn, wait) { VT100.prototype.setupTouchInputFallback = function(){ if( !this.input ){ + this.upload = document.createElement("input") + this.upload.setAttribute("type", "file") + this.upload.style.opacity = '0' + this.upload.style.position = 'absolute' + this.upload.style.left = '-9999px' + this.input = document.createElement("input") + this.input.setAttribute("type", "text") this.input.setAttribute("cols", this.opts.cols ) this.input.setAttribute("rows", this.opts.rows ) this.input.style.opacity = '0' this.input.style.position = 'absolute' this.input.style.left = '-9999px' + this.form = document.createElement("form") this.form.addEventListener("submit", (e) => { e.preventDefault() this.key_buf_.push('\n') setTimeout(VT100.go_getch_, 0); + return false }) + this.form.appendChild(this.upload) this.form.appendChild(this.input) this.scr_.parentElement.appendChild(this.form) @@ -1353,8 +1368,8 @@ VT100.prototype.setupTouchInputFallback = function(){ this.input.handler = (e) => { let ch - let isEnter = String(e?.code).toLowerCase() == "enter" || e?.code == 13 - let isBackspace = String(e?.code).toLowerCase() == "backspace" || e?.code == 8 + let isEnter = String(e?.key).toLowerCase() == "enter" || e?.code == 13 + let isBackspace = String(e?.key).toLowerCase() == "backspace" || e?.code == 8 if( isEnter ){ ch = '\n' }else if( isBackspace ){ @@ -1372,6 +1387,7 @@ VT100.prototype.setupTouchInputFallback = function(){ this.scr_.addEventListener('touchend', (e) => this.focus() ) this.scr_.addEventListener('click', (e) => this.focus() ) + } this.useFallbackInput = true this.focus() @@ -1381,7 +1397,6 @@ VT100.prototype.focus = function(){ setTimeout( () => { const el = this[ this.useFallbackInput ? 'input' : 'scr_' ] el.focus() - console.dir(el) }, 10 ) } diff --git a/com/isoterminal/feat/9pfs_utils.js b/com/isoterminal/feat/9pfs_utils.js index 06230b5..a6fa3eb 100644 --- a/com/isoterminal/feat/9pfs_utils.js +++ b/com/isoterminal/feat/9pfs_utils.js @@ -21,7 +21,6 @@ emulator.fs9p.update_file = async function(file,data){ inode.size = buf.length const now = Math.round(Date.now() / 1000); inode.atime = inode.mtime = now; - me.postMessage({event:'exec',data:[`touch /mnt/${file}`]}) // update inode return new Promise( (resolve,reject) => resolve(buf) ) }catch(e){ console.error({file,data}) diff --git a/com/isoterminal/feat/boot.js b/com/isoterminal/feat/boot.js index 1d1024f..769a732 100644 --- a/com/isoterminal/feat/boot.js +++ b/com/isoterminal/feat/boot.js @@ -14,7 +14,7 @@ ISOTerminal.prototype.boot = async function(e){ env.push( 'export '+String(i).toUpperCase()+'="'+decodeURIComponent( document.location[i]+'"') ) } } - await this.emit("emulator.create_file", ["profile.browser", this.convert.toUint8Array( env.join('\n') ) ] ) + this.worker.create_file("profile.browser", this.convert.toUint8Array( env.join('\n') ) ) if( this.serial_input == 0 ){ if( !this.noboot ){ diff --git a/com/isoterminal/feat/javascript.js b/com/isoterminal/feat/javascript.js index e589f84..d64f26f 100644 --- a/com/isoterminal/feat/javascript.js +++ b/com/isoterminal/feat/javascript.js @@ -6,7 +6,7 @@ if( typeof emulator != 'undefined' ){ const convert = ISOTerminal.prototype.convert const buf = await this.emulator.read_file("dev/browser/js") const script = convert.Uint8ArrayToString(buf) - let PID="?" + let PID=null try{ if( script.match(/^PID/) ){ PID = script.match(/^PID=([0-9]+);/)[1] @@ -35,7 +35,9 @@ if( typeof emulator != 'undefined' ){ } } // update output to 9p with PID as filename (in /mnt/run) - this.emit('fs9p.update_file', [`run/${PID}`, this.convert.toUint8Array(res)] ) + if( PID ){ + this.worker.update_file(`run/${PID}`, this.convert.toUint8Array(res) ) + } }) } diff --git a/com/isoterminal/feat/pastedrop.js b/com/isoterminal/feat/pastedrop.js new file mode 100644 index 0000000..cb90b49 --- /dev/null +++ b/com/isoterminal/feat/pastedrop.js @@ -0,0 +1,32 @@ +if( typeof emulator != 'undefined' ){ + // inside worker-thread + +}else{ + // inside browser-thread + // + ISOTerminal.prototype.pasteWriteFile = async function(data,type,filename){ + this.pasteWriteFile.fileCount = this.pasteWriteFile.fileCount || 0 + const file = `clipboard/`+ ( filename || `user-paste-${this.pasteWriteFile.fileCount}`) + await this.worker.create_file(file, data ) + // run the xrsh hook + this.hook("clipboard", [ `/mnt/${file}`, type ] ) + console.log("clipboard paste: /mnt/"+file) + this.pasteWriteFile.fileCount += 1 + } + + ISOTerminal.prototype.pasteFile = async function(data){ + const {type,item,pastedText} = data + if( pastedText){ + this.pasteWriteFile( this.convert.toUint8Array(pastedText) ,type) + }else{ + const file = item.getAsFile(); + const reader = new FileReader(); + reader.onload = (e) => { + const arr = new Uint8Array(e.target.result) + this.pasteWriteFile( arr, type, file.name ); // or use readAsDataURL for images + }; + reader.readAsArrayBuffer(file); + } + } + +} diff --git a/com/lib/xhook.min.js b/com/lib/xhook.min.js new file mode 100644 index 0000000..b709592 --- /dev/null +++ b/com/lib/xhook.min.js @@ -0,0 +1,4 @@ +//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 diff --git a/com/paste.js b/com/paste.js deleted file mode 100644 index ddb308b..0000000 --- a/com/paste.js +++ /dev/null @@ -1,179 +0,0 @@ -AFRAME.registerComponent('paste', { - schema: { - foo: { type:"string"} - }, - - init: function () { - this.el.object3D.visible = false - //this.el.innerHTML = ` ` - }, - - requires:{ - osbutton: "com/osbutton.js" - }, - - events:{ - - // component events - somecomponent: function( ){ console.log("component requirement mounted") }, - ready: function(e){ console.log("requires are loaded") }, - - launcher: function(e){ - const paste = () => { - navigator.clipboard.readText() - .then( (base64) => { - let mimetype = base64.replace(/;base64,.*/,'') - let data = base64.replace(/.*;base64,/,'') - let type = this.textHeuristic(data) - console.log("type="+type) - switch( this.textHeuristic(data) ){ - case "aframe": this.insertAFRAME(data); break; - default: this.insertText(data); break; - } - this.count += 1 - }) - } - - navigator.permissions.query({ name: 'clipboard-read' }) - .then( (permission) => { - if( permission.state != 'granted' ){ - this.el.sceneEl.exitVR() - setTimeout( () => paste(), 500 ) - return - }else paste() - }) - }, - - }, - - textHeuristic: function(text){ - // Script type identification clues - const bashClues = ["|", "if ", "fi", "cat"]; - const htmlClues = ["/>", "href=", "src="]; - const aframeClues = ["", "position="]; - const jsClues = ["var ", "let ", "function ", "setTimeout","console."]; - // Count occurrences of clues for each script type - const bashCount = bashClues.reduce((acc, clue) => acc + (text.includes(clue) ? 1 : 0), 0); - const htmlCount = htmlClues.reduce((acc, clue) => acc + (text.includes(clue) ? 1 : 0), 0); - const aframeCount = aframeClues.reduce((acc, clue) => acc + (text.includes(clue) ? 1 : 0), 0); - const jsCount = jsClues.reduce((acc, clue) => acc + (text.includes(clue) ? 1 : 0), 0); - - // Identify the script with the most clues or return unknown if inconclusive - const maxCount = Math.max(bashCount, htmlCount, jsCount, aframeCount); - if (maxCount === 0) { - return "unknown"; - } else if (bashCount === maxCount) { - return "bash"; - } else if (htmlCount === maxCount) { - return "html"; - } else if (jsCount === maxCount) { - return "javascript"; - } else { - return "aframe"; - } - }, - - insertAFRAME: function(data){ - let scene = document.createElement('a-entity') - scene.id = "embedAframe" - scene.innerHTML = data - let el = document.createElement('a-text') - el.setAttribute("value",data) - el.setAttribute("color","white") - el.setAttribute("align","center") - el.setAttribute("anchor","align") - let osbutton = this.wrapOSButton(el,"aframe",data) - AFRAME.scenes[0].appendChild(osbutton) - console.log(data) - }, - - insertText: function(data){ - let el = document.createElement('a-text') - el.setAttribute("value",data) - el.setAttribute("color","white") - el.setAttribute("align","center") - el.setAttribute("anchor","align") - let osbutton = this.wrapOSButton(el,"text",data) - AFRAME.scenes[0].appendChild(osbutton) - console.log(data) - }, - - wrapOSButton: function(el,type,data){ - let osbutton = document.createElement('a-entity') - let height = type == 'aframe' ? 0.3 : 0.1 - let depth = type == 'aframe' ? 0.3 : 0.05 - osbutton.setAttribute("osbutton",`width:0.3; height: ${height}; depth: ${depth}; color:blue `) - osbutton.appendChild(el) - osbutton.object3D.position.copy( this.getPositionInFrontOfCamera() ) - return osbutton - }, - - getPositionInFrontOfCamera: function(distance){ - const camera = this.el.sceneEl.camera; - let pos = new THREE.Vector3() - let direction = new THREE.Vector3(); - // Get camera's forward direction (without rotation) - camera.getWorldDirection(direction); - camera.getWorldPosition(pos) - direction.normalize(); - // Scale the direction by 1 meter - if( !distance ) distance = 1.5 - direction.multiplyScalar(distance); - // Add the camera's position to the scaled direction to get the target point - pos.add(direction); - return pos - }, - - manifest: { // HTML5 manifest to identify app to xrsh - "short_name": "Paste", - "name": "Paste", - "icons": [ - { - "src": "https://css.gg/clipboard.svg", - "type": "image/svg+xml", - "sizes": "512x512" - } - ], - "id": "/?source=pwa", - "start_url": "/?source=pwa", - "background_color": "#3367D6", - "display": "standalone", - "scope": "/", - "theme_color": "#3367D6", - "shortcuts": [ - { - "name": "What is the latest news?", - "cli":{ - "usage": "helloworld [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. - ` - } - -}); - diff --git a/com/pastedrop.js b/com/pastedrop.js new file mode 100644 index 0000000..a714aef --- /dev/null +++ b/com/pastedrop.js @@ -0,0 +1,115 @@ +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. + ` + } + +}); +