reworked autorestore + added touchkeyboard compatibility
/ mirror_to_github (push) Successful in 30s Details
/ test (push) Successful in 5s Details

This commit is contained in:
Leon van Kammen 2024-10-23 16:50:07 +00:00
parent ce44449fc4
commit 4f83e1af0b
7 changed files with 262 additions and 140 deletions

View File

@ -36,7 +36,7 @@ AFRAME.registerComponent('codemirror', {
css: (me) => `.CodeMirror{
width: ${me.com.data.width}px !important;
height: ${me.com.data.height}px !important;
height: ${me.com.data.height-30}px !important;
}
.codemirror *{
font-size: 14px;
@ -45,7 +45,10 @@ AFRAME.registerComponent('codemirror', {
letter-spacing: 0 !important;
text-shadow: 0px 0px 10px #F075;
}
#${me.dom.id} .wb-body { overflow:hidden; }
.wb-body:has(> .codemirror){
overflow:hidden;
}
.CodeMirror {
margin-top:18px;
@ -78,11 +81,30 @@ AFRAME.registerComponent('codemirror', {
this.editor.updateFile( this.data.file, instance.getValue() )
})
this
.handleFocus()
setTimeout( () => {
this.el.setAttribute("html-as-texture-in-xr", `domid: #${this.el.dom.id}`) // only show aframe-html in xr
},1500)
},
handleFocus: function(){
const focus = (showdom) => (e) => {
if( this.editor ){
this.editor.focus()
}
if( this.el.components.window && this.data.renderer == 'canvas'){
this.el.components.window.show( showdom )
}
}
this.el.addEventListener('obbcollisionstarted', focus(false) )
this.el.sceneEl.addEventListener('enter-vr', focus(false) )
this.el.sceneEl.addEventListener('enter-ar', focus(false) )
this.el.sceneEl.addEventListener('exit-vr', focus(true) )
this.el.sceneEl.addEventListener('exit-ar', focus(true) )
},
updateFile: async function(file,str){
// we don't do via shellcmd: isoterminal.exec(`echo '${str}' > ${file}`,1)
// as it would require all kindof ugly stringescaping

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.5) )
this.el.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.8) )
}
},

View File

@ -37,6 +37,7 @@ if( typeof AFRAME != 'undefined '){
overlayfs: { type:"string"},
width: { type: 'number',"default": -1 },
height: { type: 'number',"default": -1 },
depth: { type: 'number',"default": 0.03 },
lineHeight: { type: 'number',"default": 18 },
padding: { type: 'number',"default": 18 },
minimized: { type: 'boolean',"default":false},
@ -44,19 +45,18 @@ if( typeof AFRAME != 'undefined '){
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":64 }, // VM memory (in MB)
bufferLatency: { type: 'number', "default":30 }, // in ms: bufferlatency from webworker to xterm (batch-update every char to texture)
memory: { type: 'number', "default":40 }, // 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 }
},
init: function(){
this.el.object3D.visible = false
if( this.data.width == -1 ) this.data.width = document.body.offsetWidth
if( this.data.height == -1 ) this.data.height = document.body.offsetHeight
this.data.width -= this.data.padding*2
this.data.height -= this.data.padding*2
this.calculateDimension()
this.initHud()
this.setupBox()
fetch(this.data.iso,{method: 'HEAD'})
.then( (res) => {
if( res.status != 200 ) throw 'not found'
@ -79,17 +79,17 @@ if( typeof AFRAME != 'undefined '){
selfcontain: "com/selfcontainer.js",
// html to texture
htmlinxr: "com/html-as-texture-in-xr.js",
// isoterminal features
// isoterminal global features
PromiseWorker: "com/isoterminal/PromiseWorker.js",
ISOTerminal: "com/isoterminal/ISOTerminal.js",
localforage: "https://cdn.rawgit.com/mozilla/localForage/master/dist/localforage.js"
localforage: "com/isoterminal/localforage.js",
},
dom: {
scale: 1.0,
scale: 0.66,
events: ['click','keydown'],
html: (me) => `<div class="isoterminal">
<div id="vt100" tabindex="0">
<div id="term" tabindex="0">
<pre></pre>
</div>
</div>`,
@ -105,7 +105,7 @@ if( typeof AFRAME != 'undefined '){
position:relative;
line-height: ${me.com.data.lineHeight}px;
}
#vt100 {
#term {
outline: none !important;
}
@font-face {
@ -127,10 +127,6 @@ if( typeof AFRAME != 'undefined '){
border:none;
padding:none;
}
span blink:last-of-type{
border-right: 8px solid #F07;
padding-right: 3px;
}
#overlay .winbox:has(> .isoterminal){
background:transparent;
@ -218,8 +214,10 @@ if( typeof AFRAME != 'undefined '){
this.term = new ISOTerminal(instance,this.data)
instance.addEventListener('DOMready', () => {
instance.setAttribute("html-as-texture-in-xr", `domid: #${this.el.dom.id}; faceuser: true`)
setTimeout( () => this.setupVT100(instance),100)
this.setupVT100(instance)
setTimeout( () => {
instance.setAttribute("html-as-texture-in-xr", `domid: #term; faceuser: true`)
},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}`)
@ -231,6 +229,8 @@ if( typeof AFRAME != 'undefined '){
// 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)
})
@ -262,6 +262,7 @@ if( typeof AFRAME != 'undefined '){
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) )
@ -290,17 +291,20 @@ if( typeof AFRAME != 'undefined '){
},
setupVT100: function(instance){
const el = this.el.dom.querySelector('#vt100')
this.vt100 = new VT100(
Math.floor(this.data.width/this.data.lineHeight),
Math.floor(this.data.height*0.8/this.data.lineHeight),
el,
100
)
const el = this.el.dom.querySelector('#term')
const opts = {
cols: this.cols,
rows: this.rows,
el_or_id: el,
max_scroll_lines: 100,
nodim: true
}
this.vt100 = new VT100( opts )
this.vt100.el = el
this.vt100.curs_set( 1, true)
el.focus()
this.el.addEventListener('focus', () => el.focus())
this.vt100.getch( (ch,t) => {
console.log(ch)
this.term.send( ch )
this.vt100.curs_set( 0, true)
})
@ -323,6 +327,27 @@ if( typeof AFRAME != 'undefined '){
//})
},
setupBox: function(){
// setup slightly bigger black backdrop (this.el.getObject3D("mesh"))
const w = this.data.width/950;
const h = this.data.height/950;
this.el.box = document.createElement('a-entity')
this.el.box.setAttribute("geometry",`primitive: box; width:${w}; height:${h}; depth: -${this.data.depth}`)
this.el.box.setAttribute("material","shader:flat; color:black; opacity:0.9; transparent:true; ")
this.el.box.setAttribute("position",`0 0 ${(this.data.depth/2)-0.001}`)
this.el.appendChild(this.el.box)
},
calculateDimension: function(){
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*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:{
// combined AFRAME+DOM reactive events

View File

@ -61,11 +61,11 @@ ISOTerminal.prototype.convert = {
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (let i = 0; i < len; i++) binary += String.fromCharCode(bytes[i]);
return window.btoa(binary);
return btoa(binary);
},
base64ToArrayBuffer: function(base64) {
const binaryString = window.atob(base64);
const binaryString = atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
@ -211,11 +211,14 @@ ISOTerminal.prototype.startVM = function(opts){
\r https://xrsh.isvery.ninja ▬▬▬▬▬▬▬▬▬▬▬▬
\r local-first, polyglot, unixy WebXR IDE & runtime
\r
\r credits: NLnet | @nlnet@nlnet.nl
\r Leon van Kammen | @lvk@mastodon.online
\r Fabien Benetou | @utopiah@mastodon.pirateparty.be
\r Mr Doob | THREE.js
\r Diego Marcos | AFRAME.js
\r credits
\r -------
\r @nlnet@nlnet.nl
\r @lvk@mastodon.online
\r @utopiah@mastodon.pirateparty.be
\r https://www.w3.org/TR/webxr
\r https://three.org
\r https://aframe.org
`
const text_color = "\r"

View File

@ -1,8 +1,15 @@
// https://raw.githubusercontent.com/vsinitsyn/vt100/refs/heads/coffeescript/public/javascripts/VT100.js
// https://raw.githubusercontent.com/vetupinitsyn/vt100/refs/heads/coffeescript/public/javascripts/VT100.js
//
// VT100.js -- a text terminal emulator in JavaScript with a ncurses-like
// interface and a POSIX-like interface. (The POSIX-like calls are
// implemented on top of the ncurses-like calls, not the other way round.)
//
// required markup:
//
// <div id="term" tabindex="0">
// <pre></pre>
// </div>
//
// Released under the GNU LGPL v2.1, by Frank Bi <bi@zompower.tk>
//
@ -77,8 +84,10 @@
// interpreted and acted on.
// constructor
function VT100(wd, ht, el_or_id, max_scroll_lines, fg, bg)
function VT100(opts)
{
this.opts = opts
let {cols, rows, el_or_id, max_scroll_lines, fg, bg, nodim} = opts
if (!max_scroll_lines) {
max_scroll_lines = 1000;
}
@ -92,20 +101,20 @@ function VT100(wd, ht, el_or_id, max_scroll_lines, fg, bg)
var r;
var c;
var scr = typeof el_or_id == 'string' ? document.getElementById(el_or_id) : el_or_id
this.wd_ = wd;
this.ht_ = ht;
this.wd_ = cols;
this.ht_ = rows;
// Keep up to max_scroll_lines of scrollback history.
this.max_ht_ = max_scroll_lines;
this._set_colors(fg, bg);
this.text_ = new Array(ht);
this.attr_ = new Array(ht);
this.redraw_ = new Array(ht);
this.scroll_region_ = [0, ht-1];
this.text_ = new Array(rows);
this.attr_ = new Array(rows);
this.redraw_ = new Array(rows);
this.scroll_region_ = [0, rows-1];
this.start_row_id = 0;
this.num_rows_ = ht;
for (r = 0; r < ht; ++r) {
this.text_[r] = new Array(wd);
this.attr_[r] = new Array(wd);
this.num_rows_ = rows;
for (r = 0; r < rows; ++r) {
this.text_[r] = new Array(cols);
this.attr_[r] = new Array(cols);
this.redraw_[r] = 1;
}
this.scr_ = scr;
@ -116,10 +125,14 @@ function VT100(wd, ht, el_or_id, max_scroll_lines, fg, bg)
this.key_buf_ = [];
this.echo_ = false;
this.esc_state_ = 0;
this.log_level_ = VT100.DEBUG //WARN;
this.log_level_ = VT100.WARN
this.clear_all();
this.refresh();
// rate limit this.refresh
this.refresh = this.throttleSmart( VT100.prototype.refresh.bind(this), 100)
this.setupTouchInputFallback() // smartphone
}
// public constants -- colours and colour pairs
@ -172,6 +185,7 @@ VT100.handle_onkeypress_ = function VT100_handle_onkeypress(event,cb)
var vt = VT100.the_vt_, ch;
if (vt === undefined)
return true;
//if ( event.keyCode != undefined || !event.charCode){
// ch = event.keyCode;
// if (ch == 13)
@ -180,11 +194,11 @@ VT100.handle_onkeypress_ = function VT100_handle_onkeypress(event,cb)
// return true;
// ch = String.fromCharCode(ch);
//} else {
ch = event.charCode;
//dump("ch: " + ch + "\n");
//dump("ctrl?: " + event.ctrlKey + "\n");
vt.debug("onkeypress:: keyCode: " + event.keyCode + ", ch: " + event.charCode);
if (ch) {
vt.debug("onkeypress:: ch: " + event.code + " ,key: "+event.key);
if (event.key.length == 1) {
ch = event.key.charCodeAt(0)
if (ch > 255)
return true;
if (event.ctrlKey && event.shiftKey) {
@ -203,51 +217,52 @@ VT100.handle_onkeypress_ = function VT100_handle_onkeypress(event,cb)
}
} else {
switch (event.key) {
case "Backspace":
ch = '\b';
break;
case "Tab":
ch = '\t';
break;
case event.DOM_VK_RETURN:
case event.DOM_VK_ENTER:
ch = '\r';
break;
case event.DOM_VK_UP:
if (this.cursor_key_mode_ == VT100.CK_CURSOR)
case "Backspace":
ch = '\b';
break;
case "Tab":
ch = '\t';
break;
case "Enter":
ch = '\n';
break;
case "ArrowUp":
if (this.cursor_key_mode_ == VT100.CK_CURSOR)
ch = '\x1b[A';
else
else
ch = '\x1bOA';
break;
case event.DOM_VK_DOWN:
if (this.cursor_key_mode_ == VT100.CK_CURSOR)
break;
case "ArrowDown":
if (this.cursor_key_mode_ == VT100.CK_CURSOR)
ch = '\x1b[B';
else
else
ch = '\x1bOB';
break;
case event.DOM_VK_RIGHT:
if (this.cursor_key_mode_ == VT100.CK_CURSOR)
break;
case "ArrowRight":
if (this.cursor_key_mode_ == VT100.CK_CURSOR)
ch = '\x1b[C';
else
else
ch = '\x1bOC';
break;
case event.DOM_VK_LEFT:
if (this.cursor_key_mode_ == VT100.CK_CURSOR)
break;
case "ArrowLeft":
if (this.cursor_key_mode_ == VT100.CK_CURSOR)
ch = '\x1b[D';
else
else
ch = '\x1bOD';
break;
case event.DOM_VK_DELETE:
ch = '\x1b[3~';
break;
case event.DOM_VK_HOME:
ch = '\x1b[H';
break;
case event.DOM_VK_ESCAPE:
ch = '\x1b';
break;
default:
return true;
break;
case "Delete":
ch = '\x1b[3~';
break;
case "Home":
ch = '\x1b[H';
break;
case "Escape":
ch = '\x1b';
case "Control":
break;
default:
return true
break;
}
}
// Stop the event from doing anything else.
@ -311,39 +326,39 @@ VT100.prototype.html_colours_ = function VT100_html_colours_(attr)
var fg, bg, co0, co1;
fg = attr.fg;
bg = attr.bg;
switch (attr.mode & (VT100.A_REVERSE | VT100.A_DIM | VT100.A_BOLD)) {
case 0:
case VT100.A_DIM | VT100.A_BOLD:
co0 = '00';
if (bg == VT100.COLOR_WHITE)
co1 = 'ff';
else
co1 = 'c0';
break;
case VT100.A_BOLD:
co0 = '00'; co1 = 'ff';
break;
case VT100.A_DIM:
if (fg == VT100.COLOR_BLACK)
co0 = '40';
else
co0 = '00';
co1 = '40';
break;
case VT100.A_REVERSE:
case VT100.A_REVERSE | VT100.A_DIM | VT100.A_BOLD:
co0 = 'c0'; co1 = '40';
break;
case VT100.A_REVERSE | VT100.A_BOLD:
co0 = 'c0'; co1 = '00';
break;
default:
if (fg == VT100.COLOR_BLACK)
co0 = '80';
else
co0 = 'c0';
co1 = 'c0';
}
switch (attr.mode & (VT100.A_REVERSE | VT100.A_DIM | VT100.A_BOLD)) {
case 0:
case VT100.A_DIM | VT100.A_BOLD:
co0 = '00';
if (bg == VT100.COLOR_WHITE)
co1 = 'ff';
else
co1 = 'c0';
break;
case VT100.A_BOLD:
co0 = '00'; co1 = 'ff';
break;
case VT100.A_DIM:
if (fg == VT100.COLOR_BLACK)
co0 = '40';
else
co0 = '00';
co1 = '40';
break;
case VT100.A_REVERSE:
case VT100.A_REVERSE | VT100.A_DIM | VT100.A_BOLD:
co0 = 'c0'; co1 = 'ff';
break;
case VT100.A_REVERSE | VT100.A_BOLD:
co0 = 'c0'; co1 = '00';
break;
default:
if (fg == VT100.COLOR_BLACK)
co0 = '80';
else
co0 = 'c0';
co1 = 'c0';
}
return {
f: '#' + (fg & 4 ? co1 : co0) +
(fg & 2 ? co1 : co0) +
@ -1283,6 +1298,52 @@ VT100.prototype.warn = function VT100_warn(message) {
}
}
VT100.prototype.throttleSmart = function throttleSmart(fn, wait) {
let timeout, lastArgs;
return (...args) => {
lastArgs = lastArgs || []
if (!timeout) {
fn(...args); timeout = setTimeout(() => { fn(...lastArgs); timeout = null; }, wait);
} else lastArgs = args;
};
}
VT100.prototype.setupTouchInputFallback = function(){
this.scr_.addEventListener('touchend', () => {
if( !this.input ){
this.form = document.createElement("form")
this.form.addEventListener("submit", (e) => {
e.preventDefault()
this.key_buf_.push('\n')
setTimeout(VT100.go_getch_, 0);
})
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.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.valueLast = this.input.value
}
this.input.addEventListener('input', this.input.handler )
}
setTimeout( () => this.input.focus(), 10 )
})
}
function dump(x) {
// Do nothing
console.log(x)
}

View File

@ -1,15 +1,31 @@
if( typeof emulator != 'undefined' ){
// inside worker-thread
importScripts("localforage.js") // we don't instance it again here (just use its functions)
this['emulator.restore_state'] = async function(data){
await emulator.restore_state(data)
console.log("restored state")
this.postMessage({event:"state_restored",data:false})
return new Promise( (resolve,reject) => {
localforage.getItem("state", async (err,stateBase64) => {
if( stateBase64 && !err ){
state = ISOTerminal.prototype.convert.base64ToArrayBuffer( stateBase64 )
await emulator.restore_state(state)
console.log("restored state")
}else return reject("worker.js: emulator.restore_state (could not get state from localforage)")
resolve()
})
})
}
this['emulator.save_state'] = async function(){
console.log("saving session")
let state = await emulator.save_state()
this.postMessage({event:"state_saved",data:state},[state])
localforage.setDriver([
localforage.INDEXEDDB,
localforage.WEBSQL,
localforage.LOCALSTORAGE
]).then( () => {
localforage.setItem("state", ISOTerminal.prototype.convert.arrayBufferToBase64(state) )
console.log("state saved")
})
console.dir(state)
}
@ -30,30 +46,21 @@ if( typeof emulator != 'undefined' ){
localforage.getItem("state", async (err,stateBase64) => {
if( stateBase64 && !err && confirm('continue last session?') ){
this.noboot = true // see feat/boot.js
state = this.convert.base64ToArrayBuffer( stateBase64 )
this.addEventListener('state_restored', function(){
try{
await this.worker['emulator.restore_state']()
// simulate / fastforward boot events
this.postBoot( () => {
this.send("l\n")
this.send("hook wakeup\n")
})
})
this.worker.postMessage({event:'emulator.restore_state',data:state})
}catch(e){ console.error(e) }
}
})
this.save = async () => {
const state = await this.worker.postMessage({event:"emulator.save_state",data:false})
await this.worker['emulator.save_state']()
}
this.addEventListener('state_saved', function(e){
const state = e.detail
localforage.setItem("state", this.convert.arrayBufferToBase64(state) )
console.log("state saved")
})
window.addEventListener("beforeunload", function (e) {
var confirmationMessage = "Sure you want to leave?\nTIP: enter 'save' to continue this session later";
(e || window.event).returnValue = confirmationMessage; //Gecko + IE

View File

@ -4,7 +4,11 @@ ISOTerminal.addEventListener('ready', function(e){
ISOTerminal.prototype.boot = async function(e){
// set environment
let env = ['export BROWSER=1']
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]+'"') )