Compare commits

..

No commits in common. "main" and "feat/vt100" have entirely different histories.

18 changed files with 722 additions and 3703 deletions

View File

@ -4,8 +4,8 @@ AFRAME.registerComponent('codemirror', {
schema: {
file: { type:"string"},
term: { type:"selector", default: "[isoterminal]" },
width: { type:"number", default:700},
height: { type:"number", default:500},
width: { type:"number", default:900},
height: { type:"number", default:700},
},
init: function () {
@ -109,8 +109,8 @@ AFRAME.registerComponent('codemirror', {
// we don't do via shellcmd: isoterminal.exec(`echo '${str}' > ${file}`,1)
// as it would require all kindof ugly stringescaping
console.log("updating "+file)
await this.isoterminal.worker.update_file(file, this.isoterminal.convert.toUint8Array(str) )
this.isoterminal.exec("touch "+file) // *FIXME* notify filesystem (why does inotifyd need this? v86's 9pfees is cached?)
console.log(str)
await this.isoterminal.worker['emulator.update_file'](file, term.convert.toUint8Array(str) )
},
events:{
@ -118,7 +118,7 @@ AFRAME.registerComponent('codemirror', {
// component events
DOMready: function(e){
this.isoterminal.worker.read_file(this.data.file)
this.isoterminal.worker['emulator.read_file'](this.data.file)
.then( this.isoterminal.convert.Uint8ArrayToString )
.then( (str) => {
console.log("creating editor")

View File

@ -21,7 +21,7 @@ if( !AFRAME.components['html-as-texture-in-xr'] ){
this.el.setAttribute("html",`html: ${this.data.domid}; cursor:#cursor; xrlayer: true`)
this.el.setAttribute("visible", AFRAME.utils.XD() == '3D' ? 'true' : 'false' )
if( this.data.faceuser ){
this.el.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.4) )
this.el.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.8) )
}
},

View File

@ -40,6 +40,7 @@ if( typeof AFRAME != 'undefined '){
depth: { type: 'number',"default": 0.03 },
lineHeight: { type: 'number',"default": 18 },
padding: { type: 'number',"default": 18 },
minimized: { type: 'boolean',"default":false},
maximized: { type: 'boolean',"default":true},
muteUntilPrompt:{ type: 'boolean',"default":true}, // mute stdout until a prompt is detected in ISO
HUD: { type: 'boolean',"default":false}, // link to camera movement
@ -55,7 +56,6 @@ if( typeof AFRAME != 'undefined '){
this.calculateDimension()
this.initHud()
this.setupBox()
this.setupPasteDrop()
fetch(this.data.iso,{method: 'HEAD'})
.then( (res) => {
@ -72,11 +72,10 @@ 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: "com/lib/xhook.min.js",
xhook: "https://jpillora.com/xhook/dist/xhook.min.js",
selfcontain: "com/selfcontainer.js",
// html to texture
htmlinxr: "com/html-as-texture-in-xr.js",
@ -98,7 +97,7 @@ if( typeof AFRAME != 'undefined '){
css: (me) => `.isoterminal{
padding: ${me.com.data.padding}px;
width:100%;
height:90%;
height:100%;
position:relative;
}
.isoterminal div{
@ -134,17 +133,6 @@ if( typeof AFRAME != 'undefined '){
box-shadow:none;
}
.cursor {
background: #70F !important;
animation:fade 1000ms infinite;
-webkit-animation:fade 1000ms infinite;
}
.XR .cursor {
animation:none;
-webkit-animation:none;
}
.wb-body:has(> .isoterminal){
background: #000C;
overflow:hidden;
@ -204,8 +192,6 @@ if( typeof AFRAME != 'undefined '){
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",
})
this.el.setAttribute("selfcontainer","")
@ -234,7 +220,7 @@ if( typeof AFRAME != 'undefined '){
},100)
//instance.winbox.resize(720,380)
let size = `width: ${this.data.width}; height: ${this.data.height}`
instance.setAttribute("window", `title: xrsh.iso; uid: ${instance.uid}; attach: #overlay; dom: #${instance.dom.id}; ${size}; min: ${this.data.minimized}; max: ${this.data.maximized}; class: no-full, no-resize, no-move`)
instance.setAttribute("window", `title: xrsh.iso; uid: ${instance.uid}; attach: #overlay; dom: #${instance.dom.id}; ${size}; min: ${this.data.minimized}; max: ${this.data.maximized}`)
})
instance.addEventListener('window.oncreate', (e) => {
@ -249,7 +235,6 @@ if( typeof AFRAME != 'undefined '){
})
instance.setAttribute("dom", "")
instance.setAttribute("pastedrop", "")
this.term.addEventListener('ready', (e) => {
instance.dom.classList.remove('blink')
@ -274,10 +259,10 @@ if( typeof AFRAME != 'undefined '){
instance.addEventListener('window.onmaximize', resize )
const focus = (showdom) => (e) => {
this.el.emit('focus',e.detail)
if( this.el.components.window && this.data.renderer == 'canvas'){
this.el.components.window.show( showdom )
}
this.el.emit('focus',e.detail)
}
this.el.addEventListener('obbcollisionstarted', focus(false) )
@ -307,27 +292,21 @@ if( typeof AFRAME != 'undefined '){
setupVT100: function(instance){
const el = this.el.dom.querySelector('#term')
this.term.opts.vt100 = {
const opts = {
cols: this.cols,
rows: this.rows,
el_or_id: el,
max_scroll_lines: this.rows,
nodim: true,
rainbow: [VT100.COLOR_MAGENTA, VT100.COLOR_CYAN ],
xr: AFRAME.scenes[0].renderer.xr,
map: {
'ArrowRight': { ch: false, ctrl: '\x1b\x66' }, // this triggers ash-shell forward-word
'ArrowLeft': { ch: false, ctrl: '\x1b\x62' } // backward-word
max_scroll_lines: 100,
nodim: true
}
}
this.term.emit('initVT100',this)
this.vt100 = new VT100( this.term.opts.vt100 )
this.vt100 = new VT100( opts )
this.vt100.el = el
this.vt100.curs_set( 1, true)
this.vt100.focus()
this.el.addEventListener('focus', () => this.vt100.focus() )
el.focus()
this.el.addEventListener('focus', () => el.focus())
this.vt100.getch( (ch,t) => {
this.term.send( ch )
this.vt100.curs_set( 0, true)
})
this.el.addEventListener('serial-output-byte', (e) => {
@ -339,23 +318,13 @@ if( typeof AFRAME != 'undefined '){
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
//this.el.dom.querySelector('input').addEventListener('keyup', (e) => {
// VT100.handle_onkeypress_( {charCode : e.charCode || e.keyCode, keyCode: e.keyCode}, (chars) => {
// debugger
// chars.map( (c) => this.term.send(str) )
// })
//})
},
setupBox: function(){
@ -370,13 +339,13 @@ if( typeof AFRAME != 'undefined '){
},
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.width == -1 ) this.data.width = document.body.offsetWidth
if( this.data.height == -1 ) this.data.height = document.body.offsetHeight
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)
this.rows = Math.floor(this.data.height*0.53/this.data.lineHeight*1.7)
this.cols = Math.floor(this.data.width/this.data.lineHeight*1.9)
this.rows = Math.floor(this.data.height*0.5/this.data.lineHeight*1.7) // keep extra height for mobile browser bottom-bar (android)
},
events:{

View File

@ -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,10 +37,6 @@ 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){
@ -52,9 +48,7 @@ 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})
)
})
}
@ -193,6 +187,7 @@ ISOTerminal.prototype.startVM = function(opts){
"Learned helplessness fades when we realize tech isnt 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",

View File

@ -13,8 +13,6 @@
//
// Released under the GNU LGPL v2.1, by Frank Bi <bi@zompower.tk>
//
//
// 2024-08-xx upgraded things to work in WebXR (xterm.js too heavy)
// 2007-08-12 - refresh():
// - factor out colour code to html_colours_()
// - fix handling of A_REVERSE | A_DIM
@ -120,8 +118,6 @@ function VT100(opts)
this.redraw_[r] = 1;
}
this.scr_ = scr;
this.scr_.style.display = 'inline'
this.setupTouchInputFallback() // smartphone/android
this.cursor_vis_ = true;
this.cursor_key_mode_ = VT100.CK_CURSOR;
this.grab_events_ = false;
@ -135,6 +131,8 @@ function VT100(opts)
// rate limit this.refresh
this.refresh = this.throttleSmart( VT100.prototype.refresh.bind(this), 100)
this.setupTouchInputFallback() // smartphone
}
// public constants -- colours and colour pairs
@ -218,7 +216,7 @@ VT100.handle_onkeypress_ = function VT100_handle_onkeypress(event,cb)
ch = '\n';
}
} else {
switch (event.code) {
switch (event.key) {
case "Backspace":
ch = '\b';
break;
@ -266,21 +264,14 @@ VT100.handle_onkeypress_ = function VT100_handle_onkeypress(event,cb)
return true
break;
}
// custom map override
if( vt.opts.map[ event.code ].ch ) ch = vt.opts.map[ event.code ].ch
if( vt.opts.map[ event.code ].ctrl && event.ctrlKey ) ch = vt.opts.map[ event.code ].ctrl
}
// Workaround: top the event from doing anything else.
// (prevent input from adding characters instead of via VM)
event.preventDefault()
// Stop the event from doing anything else.
event.preventDefault();
vt.key_buf_.push(ch);
if( cb ){
cb(vt.key_buf_)
vt.key_buf_ = []
}else setTimeout(VT100.go_getch_, 0);
return false;
}
@ -294,7 +285,6 @@ VT100.handle_onkeydown_ = function VT100_handle_onkeydown()
default:
return true;
}
event.preventDefault()
vt.key_buf_.push(ch);
setTimeout(VT100.go_getch_, 0);
return false;
@ -559,28 +549,24 @@ VT100.prototype.clearpos = function VT100_clearpos(row, col)
this.redraw_[row] = 1;
}
VT100.prototype.curs_set = function(vis, grab, offscreenKB)
VT100.prototype.curs_set = function(vis, grab, eventist)
{
// offscreenKB is a div which receives keys from physical kb's
// but not from touch keyboards (they require an input-field)
// hence setupTouchInputFallback()..this is how we seperate the two
this.info("curs_set:: vis: " + vis + ", grab: " + grab);
if (vis !== undefined){
if (vis !== undefined)
this.cursor_vis_ = (vis > 0);
}
if (offscreenKB === undefined)
offscreenKB = this.scr_;
if (eventist === undefined)
eventist = this.scr_;
if (grab === true || grab === false) {
if (grab === this.grab_events_)
return;
if (grab) {
this.grab_events_ = true;
VT100.the_vt_ = this;
offscreenKB.addEventListener("keypress", VT100.handle_onkeypress_, false);
offscreenKB.addEventListener("keydown", VT100.handle_onkeypress_, false);
eventist.addEventListener("keypress", VT100.handle_onkeypress_, false);
eventist.addEventListener("keydown", VT100.handle_onkeypress_, false);
} else {
offscreenKB.removeEventListener("keypress", VT100.handle_onkeypress_, false);
offscreenKB.removeEventListener("keydown", VT100.handle_onkeypress_, false);
eventist.removeEventListener("keypress", VT100.handle_onkeypress_, false);
eventist.removeEventListener("keydown", VT100.handle_onkeypress_, false);
this.grab_events_ = false;
VT100.the_vt_ = undefined;
}
@ -670,8 +656,7 @@ VT100.prototype.refresh = function VT100_refresh()
for (c = 0; c < wd; ++c) {
added_end_tag = false;
n_at = this.attr_[r][c];
const drawCursor = cv && r == cr && c == cc
if (drawCursor){
if (cv && r == cr && c == cc) {
// Draw the cursor here.
n_at = this._cloneAttr(n_at);
n_at.mode ^= VT100.A_REVERSE;
@ -696,9 +681,7 @@ VT100.prototype.refresh = function VT100_refresh()
start_tag += ';font-weight: bolder';
if (n_at.mode & VT100.A_UNDERLINE)
start_tag += ';text-decoration:underline';
if ( drawCursor )
start_tag += '" class="cursor'
start_tag += '">';
start_tag += ';">';
row_html += start_tag;
end_tag = "</span>" + end_tag;
at = n_at;
@ -738,7 +721,6 @@ VT100.prototype.refresh = function VT100_refresh()
div_element.innerHTML = row_html;
//dump("adding row html: " + row_html + "\n");
}
this.curs_set(1)
}
VT100.prototype.set_max_scroll_lines = function(max_lines)
@ -1085,10 +1067,6 @@ VT100.prototype.write = function VT100_write(stuff)
case 'm':
for (j=0; j<this.csi_parms_.length; ++j) {
x = this.csi_parms_[j];
if( x > 89 && x < 98 && this.opts.rainbow ){
const rainbow = this.opts.rainbow
this.fgset( rainbow[ x % rainbow.length ] )
}
switch (x) {
case 0:
this.standend();
@ -1188,7 +1166,6 @@ VT100.prototype.write = function VT100_write(stuff)
default:
this.warn(" unknown command: " + ch);
this.csi_parms_ = [];
return
break;
}
break;
@ -1332,90 +1309,38 @@ VT100.prototype.throttleSmart = function throttleSmart(fn, wait) {
}
VT100.prototype.setupTouchInputFallback = function(){
this.scr_.addEventListener('touchend', () => {
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.input = document.createElement("input")
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.appendChild(this.input)
this.scr_.parentElement.appendChild(this.form)
this.input.addEventListener('blur', () => {
if( this.input.value != '' ){
ch = '\n'
this.key_buf_.push(ch);
setTimeout(VT100.go_getch_, 0);
this.input.value = ''
}
})
this.input.addEventListener("keydown", VT100.handle_onkeypress_, false);
this.input.handler = (e) => {
let ch
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 ){
ch = '\b' // naive
}else{
ch = this.input.value.substr(-1)
}
this.input.handler = () => {
let ch = this.input.value
// detect backspace
//if( e.inputType == 'deleteContentBackward' ) ch = '\b'
this.input.value = ''
if( !ch ) return
this.key_buf_.push(ch);
setTimeout(VT100.go_getch_, 0);
this.input.lastValue = this.input.value
this.input.valueLast = this.input.value
}
this.input.addEventListener('input', (e) => this.input.handler(e) )
this.scr_.addEventListener('touchend', (e) => this.focus() )
this.scr_.addEventListener('click', (e) => this.focus() )
this.input.addEventListener('input', this.input.handler )
}
this.useFallbackInput = true
this.focus()
}
VT100.prototype.focus = function(){
setTimeout( () => {
const el = this[ this.useFallbackInput ? 'input' : 'scr_' ]
el.focus()
}, 10 )
}
window.keyboard = function(n){
let msg = 'unknown keyboard'
if( n == 0 ){
msg = "using onscreen keyboard"
VT100.the_vt_.useFallbackInput = true
VT100.the_vt_.focus()
}
if( n == 1 ){
msg = "using offscreen keyboard"
VT100.the_vt_.useFallbackInput = false
VT100.the_vt_.focus()
}
return msg
setTimeout( () => this.input.focus(), 10 )
})
}
function dump(x) {

View File

@ -21,6 +21,7 @@ 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})
@ -49,47 +50,3 @@ emulator.fs9p.append_file = async function(file,data){
}
emulator.fs9p.read_file_world = async function(file){
const p = this.SearchPath(file);
if(p.id === -1)
{
return Promise.resolve(null);
}
const inode = this.GetInode(p.id);
const perms = this.parseFilePermissions(inode.mode)
if( !perms.world.read ){
return Promise.resolve(null);
}
return this.Read(p.id, 0, inode.size);
}
emulator.fs9p.parseFilePermissions = function(permissionInt) {
// Convert the permission integer to octal
const octalPermissions = permissionInt.toString(8);
// Extract the permission bits (last 3 digits in octal)
const permissionBits = octalPermissions.slice(-3);
function parsePermission(digit) {
const num = parseInt(digit, 10);
return {
read: Boolean(num & 4), // 4 = read
write: Boolean(num & 2), // 2 = write
execute: Boolean(num & 1) // 1 = execute
};
}
// Decode the permissions
const permissions = {
owner: parsePermission(permissionBits[0]),
group: parsePermission(permissionBits[1]),
world: parsePermission(permissionBits[2]),
};
return permissions;
}

View File

@ -2,7 +2,7 @@ if( typeof emulator != 'undefined' ){
// inside worker-thread
importScripts("localforage.js") // we don't instance it again here (just use its functions)
this.restore_state = async function(data){
this['emulator.restore_state'] = async function(data){
return new Promise( (resolve,reject) => {
localforage.getItem("state", async (err,stateBase64) => {
if( stateBase64 && !err ){
@ -14,7 +14,7 @@ if( typeof emulator != 'undefined' ){
})
})
}
this.save_state = async function(){
this['emulator.save_state'] = async function(){
console.log("saving session")
let state = await emulator.save_state()
localforage.setDriver([
@ -25,6 +25,7 @@ if( typeof emulator != 'undefined' ){
localforage.setItem("state", ISOTerminal.prototype.convert.arrayBufferToBase64(state) )
console.log("state saved")
})
console.dir(state)
}
@ -46,7 +47,7 @@ if( typeof emulator != 'undefined' ){
if( stateBase64 && !err && confirm('continue last session?') ){
this.noboot = true // see feat/boot.js
try{
await this.worker.restore_state()
await this.worker['emulator.restore_state']()
// simulate / fastforward boot events
this.postBoot( () => {
this.send("l\n")
@ -57,7 +58,7 @@ if( typeof emulator != 'undefined' ){
})
this.save = async () => {
await this.worker.save_state()
await this.worker['emulator.save_state']()
}
window.addEventListener("beforeunload", function (e) {

View File

@ -14,7 +14,7 @@ ISOTerminal.prototype.boot = async function(e){
env.push( 'export '+String(i).toUpperCase()+'="'+decodeURIComponent( document.location[i]+'"') )
}
}
this.worker.create_file("profile.browser", this.convert.toUint8Array( env.join('\n') ) )
await this.emit("emulator.create_file", ["profile.browser", this.convert.toUint8Array( env.join('\n') ) ] )
if( this.serial_input == 0 ){
if( !this.noboot ){

View File

@ -1,30 +0,0 @@
if( typeof emulator != 'undefined' ){
}else{
ISOTerminal.addEventListener('ready', function(e){
// listen for http request to the filesystem ( file://host/path )
xhook.before( (request,callback) => {
if (request.url.match(/^file:\/\/xrsh\/mnt\/.*/) ){
let response
let file = request.url.replace(/^file:\/\/xrsh\/mnt\//,'')
this.worker.read_file_world(file)
.then( (data) => {
response = new Response( new Blob( [data] ) ) // wrap Uint8Array into array
response.status = 200
callback(response)
})
.catch( (e) => {
response = new Response()
response.status = 404
callback(response)
})
return
}
callback()
})
})
}

View File

@ -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=null
let PID="?"
try{
if( script.match(/^PID/) ){
PID = script.match(/^PID=([0-9]+);/)[1]
@ -35,9 +35,7 @@ if( typeof emulator != 'undefined' ){
}
}
// update output to 9p with PID as filename (in /mnt/run)
if( PID ){
this.worker.update_file(`run/${PID}`, this.convert.toUint8Array(res) )
}
this.emit('fs9p.update_file', [`run/${PID}`, this.convert.toUint8Array(res)] )
})
}

View File

@ -1,32 +0,0 @@
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);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -42,11 +42,10 @@ this.runISO = function(opts){
/*
* forward events/functions so non-worker world can reach them
*/
this.create_file = async function(){ return emulator.create_file.apply(emulator, arguments[0]) }
this.read_file = async function(){ return emulator.read_file.apply(emulator, arguments[0]) }
this.read_file_world = async function(){ return emulator.fs9p.read_file_world.apply(emulator.fs9p, arguments[0]) }
this.append_file = async function(){ emulator.fs9p.append_file.apply(emulator.fs9p, arguments[0]) }
this.update_file = async function(){ emulator.fs9p.update_file.apply(emulator.fs9p, arguments[0]) }
this['emulator.create_file'] = async function(){ return emulator.create_file.apply(emulator, arguments[0]) }
this['emulator.read_file'] = async function(){ return emulator.read_file.apply(emulator, arguments[0]) }
this['emulator.append_file'] = async function(){ emulator.fs9p.append_file.apply(emulator.fs9p, arguments[0]) }
this['emulator.update_file'] = async function(){ emulator.fs9p.update_file.apply(emulator.fs9p, arguments[0]) }
// filename will be read from 9pfs: "/mnt/"+filename
emulator.readFromPipe = function(filename,cb){

File diff suppressed because one or more lines are too long

179
com/paste.js Normal file
View File

@ -0,0 +1,179 @@
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 = ["<a-entity", "/>", "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 <type> [options]",
"example": "helloworld news",
"args":{
"--latest": {type:"string"}
}
},
"short_name": "Today",
"description": "View weather information for today",
"url": "/today?source=pwa",
"icons": [{ "src": "/images/today.png", "sizes": "192x192" }]
}
],
"description": "Paste the clipboard",
"screenshots": [
{
"src": "/images/screenshot1.png",
"type": "image/png",
"sizes": "540x720",
"form_factor": "narrow"
}
],
"help":`
Helloworld application
This is a help file which describes the application.
It will be rendered thru troika text, and will contain
headers based on non-punctualized lines separated by linebreaks,
in above's case "\nHelloworld application\n" will qualify as header.
`
}
});

View File

@ -1,115 +0,0 @@
AFRAME.registerComponent('pastedrop', {
schema: {
foo: { type:"string"}
},
init: function () {
window.addEventListener('paste', this.onPaste.bind(this) )
document.body.addEventListener('dragover',(e) => e.preventDefault() )
document.body.addEventListener('drop', this.onDrop.bind(this) )
},
initClipboard: function(){
navigator.permissions.query({ name: 'clipboard-read' })
.then( (permission) => {
if( permission.state != 'granted' ){
this.el.sceneEl.exitVR()
setTimeout( () => this.paste(), 500 )
return
}else this.paste()
})
},
//getClipboard: function(){
// navigator.clipboard.readText()
// .then( async (base64) => {
// let mimetype = base64.replace(/;base64,.*/,'')
// let data = base64.replace(/.*;base64,/,'')
// let type = this.textHeuristic(data)
// const term = document.querySelector('[isoterminal]').components.isoterminal.term
// this.el.emit('pasteFile',{}) /*TODO* data incompatible */
// })
//},
onDrop: function(e){
e.preventDefault()
this.onPaste({...e, type: "paste", clipboardData: e.dataTransfer})
},
onPaste: function(e){
if( e.type != "paste" ) return
const clipboardData = e.clipboardData || navigator.clipboard;
const items = clipboardData.items;
for (let i = 0; i < items.length; i++) {
const item = items[i];
const type = item.type;
// Check if the item is a file
if (item.kind === "file") {
this.el.emit('pasteFile',{item,type})
} else if (type === "text/plain") {
const pastedText = clipboardData.getData("text/plain");
const newType = "text" // let /root/hook.d/mimetype/text further decide whether this is text/plain (or something else)
this.el.emit('pasteFile',{item,type:newType,pastedText})
}
}
},
manifest: { // HTML5 manifest to identify app to xrsh
"short_name": "Paste",
"name": "Paste",
"icons": [
{
"src": "https://css.gg/clipboard.svg",
"type": "image/svg+xml",
"sizes": "512x512"
}
],
"id": "/?source=pwa",
"start_url": "/?source=pwa",
"background_color": "#3367D6",
"display": "standalone",
"scope": "/",
"theme_color": "#3367D6",
"shortcuts": [
{
"name": "What is the latest news?",
"cli":{
"usage": "helloworld <type> [options]",
"example": "helloworld news",
"args":{
"--latest": {type:"string"}
}
},
"short_name": "Today",
"description": "View weather information for today",
"url": "/today?source=pwa",
"icons": [{ "src": "/images/today.png", "sizes": "192x192" }]
}
],
"description": "Paste the clipboard",
"screenshots": [
{
"src": "/images/screenshot1.png",
"type": "image/png",
"sizes": "540x720",
"form_factor": "narrow"
}
],
"help":`
Helloworld application
This is a help file which describes the application.
It will be rendered thru troika text, and will contain
headers based on non-punctualized lines separated by linebreaks,
in above's case "\nHelloworld application\n" will qualify as header.
`
}
});

View File

@ -34,22 +34,16 @@ AFRAME.registerComponent('selfcontainer', {
installProxyServer: function(){
if( !window.store ) window.store = {}
// selfcontain every webrequest to store (and serve if stored)
let curry = function(me){
return function(request, response, cb){
let data = request ? window.store[ request.url ] || false : false
if( data ){ // return inline version
console.log('selfcontainer.js: serving '+request.url+' from cache')
console.log('selfcontained cache: '+request.url)
let res = new Response()
res[ data.binary ? 'data' : 'text' ] = data.binary ? () => me.convert.base64ToArrayBuffer(data.text) : data.text
cb(res)
}else{
if( request.url.match(/(^file:\/\/xrsh)/) ) return cb(response)
console.log("selfcontainer.js: caching "+request.url)
if( response.text ){
data = {text: response.text}
}else{

View File

@ -9,8 +9,7 @@ AFRAME.registerComponent('window', {
max: {type:'boolean',"default":false},
min: {type:'boolean',"default":false},
x: {type:'string',"default":"center"},
y: {type:'string',"default":"center"},
"class": {type:'array',"default":[]},
y: {type:'string',"default":"center"}
},
dependencies:{
@ -29,7 +28,6 @@ AFRAME.registerComponent('window', {
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,
@ -72,7 +70,7 @@ AFRAME.registerComponent('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 )
AFRAME.utils.positionObjectNextToNeighbor( current.object3D , last.object3D, els.length )
}
},
@ -81,10 +79,11 @@ AFRAME.registerComponent('window', {
}
})
AFRAME.utils.positionObjectNextToNeighbor = function positionObjectNextToNeighbor(object, lastNeighbor = null, margin ){
AFRAME.utils.positionObjectNextToNeighbor = function positionObjectNextToNeighbor(object, lastNeighbor = null, neighbours, margin = 0.45, degree = 20) {
// *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
object.position.x = lastNeighbor.position.x + ((neighbours-1) * margin)
object.position.y = lastNeighbor.position.y
object.position.z = lastNeighbor.position.z
//object.rotation.y += THREE.MathUtils.degToRad( (neighbours-1) * degree);
}