stable copy-paste
/ mirror_to_github (push) Successful in 19s Details
/ test (push) Successful in 4s Details

This commit is contained in:
Leon van Kammen 2024-11-15 09:23:06 +00:00
parent de8883673c
commit c5ed500a93
11 changed files with 218 additions and 201 deletions

View File

@ -110,6 +110,7 @@ AFRAME.registerComponent('codemirror', {
// as it would require all kindof ugly stringescaping // as it would require all kindof ugly stringescaping
console.log("updating "+file) console.log("updating "+file)
await this.isoterminal.worker.update_file(file, this.isoterminal.convert.toUint8Array(str) ) 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:{ events:{

View File

@ -55,6 +55,7 @@ if( typeof AFRAME != 'undefined '){
this.calculateDimension() this.calculateDimension()
this.initHud() this.initHud()
this.setupBox() this.setupBox()
this.setupPasteDrop()
fetch(this.data.iso,{method: 'HEAD'}) fetch(this.data.iso,{method: 'HEAD'})
.then( (res) => { .then( (res) => {
@ -71,10 +72,11 @@ if( typeof AFRAME != 'undefined '){
requires:{ requires:{
com: "com/dom.js", com: "com/dom.js",
window: "com/window.js", window: "com/window.js",
pastedrop: "com/pastedrop.js",
v86: "com/isoterminal/libv86.js", v86: "com/isoterminal/libv86.js",
vt100: "com/isoterminal/VT100.js", vt100: "com/isoterminal/VT100.js",
// allow xrsh to selfcontain scene + itself // 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", selfcontain: "com/selfcontainer.js",
// html to texture // html to texture
htmlinxr: "com/html-as-texture-in-xr.js", htmlinxr: "com/html-as-texture-in-xr.js",
@ -202,6 +204,7 @@ if( typeof AFRAME != 'undefined '){
indexhtml: "com/isoterminal/feat/index.html.js", indexhtml: "com/isoterminal/feat/index.html.js",
indexjs: "com/isoterminal/feat/index.js.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","") this.el.setAttribute("selfcontainer","")
@ -245,6 +248,7 @@ if( typeof AFRAME != 'undefined '){
}) })
instance.setAttribute("dom", "") instance.setAttribute("dom", "")
instance.setAttribute("pastedrop", "")
this.term.addEventListener('ready', (e) => { this.term.addEventListener('ready', (e) => {
instance.dom.classList.remove('blink') instance.dom.classList.remove('blink')
@ -302,7 +306,7 @@ if( typeof AFRAME != 'undefined '){
setupVT100: function(instance){ setupVT100: function(instance){
const el = this.el.dom.querySelector('#term') const el = this.el.dom.querySelector('#term')
const opts = { this.term.opts.vt100 = {
cols: this.cols, cols: this.cols,
rows: this.rows, rows: this.rows,
el_or_id: el, el_or_id: el,
@ -311,7 +315,8 @@ if( typeof AFRAME != 'undefined '){
rainbow: [VT100.COLOR_MAGENTA, VT100.COLOR_CYAN ], rainbow: [VT100.COLOR_MAGENTA, VT100.COLOR_CYAN ],
xr: AFRAME.scenes[0].renderer.xr 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.el = el
this.vt100.curs_set( 1, true) this.vt100.curs_set( 1, true)
this.vt100.focus() this.vt100.focus()
@ -328,6 +333,24 @@ if( typeof AFRAME != 'undefined '){
this.el.addEventListener('serial-output-string', (e) => { this.el.addEventListener('serial-output-string', (e) => {
this.vt100.write(e.detail) 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(){ setupBox: function(){

View File

@ -17,14 +17,14 @@ function ISOTerminal(instance,opts){
ISOTerminal.prototype.emit = function(event,data,sender){ ISOTerminal.prototype.emit = function(event,data,sender){
data = data || false data = data || false
const evObj = new CustomEvent(event, {detail: data} ) const evObj = new CustomEvent(event, {detail: data} )
//this.preventFrameDrop( () => { this.preventFrameDrop( () => {
// forward event to worker/instance/AFRAME element or component-function // 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 feels complex, but actually keeps event- and function-names more concise in codebase
this.dispatchEvent( evObj ) this.dispatchEvent( evObj )
if( sender != "instance" && this.instance ) this.instance.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 != "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] ) if( sender !== undefined && typeof this[event] == 'function' ) this[event].apply(this, data && data.push ? data : [data] )
//}) })
} }
ISOTerminal.addEventListener = (event,cb) => { ISOTerminal.addEventListener = (event,cb) => {
@ -37,6 +37,10 @@ ISOTerminal.prototype.exec = function(shellscript){
this.send(shellscript+"\n",1) 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.serial_input = 0; // can be set to 0,1,2,3 to define stdinput tty (xterm plugin)
ISOTerminal.prototype.send = function(str, ttyNr){ ISOTerminal.prototype.send = function(str, ttyNr){
@ -48,7 +52,9 @@ ISOTerminal.prototype.send = function(str, ttyNr){
}else{ }else{
this.convert.toUint8Array( str ).map( (c) => { this.convert.toUint8Array( str ).map( (c) => {
this.preventFrameDrop( 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 isnt too complex to understand", "Learned helplessness fades when we realize tech isnt too complex to understand",
"FOSS empowers users to customize and improve their tools", "FOSS empowers users to customize and improve their tools",
"Engaging with FOSS helps build confidence and self-reliance in tech", "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", "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", "Linux can revive old computers, extending their life and reducing e-waste",
"Many lightweight Linux distributions run smoothly on older hardware", "Many lightweight Linux distributions run smoothly on older hardware",

View File

@ -121,7 +121,7 @@ function VT100(opts)
} }
this.scr_ = scr; this.scr_ = scr;
this.scr_.style.display = 'inline' this.scr_.style.display = 'inline'
this.setupTouchInputFallback() // smartphone this.setupTouchInputFallback() // smartphone/android
this.cursor_vis_ = true; this.cursor_vis_ = true;
this.cursor_key_mode_ = VT100.CK_CURSOR; this.cursor_key_mode_ = VT100.CK_CURSOR;
this.grab_events_ = false; this.grab_events_ = false;
@ -218,7 +218,7 @@ VT100.handle_onkeypress_ = function VT100_handle_onkeypress(event,cb)
ch = '\n'; ch = '\n';
} }
} else { } else {
switch (event.key) { switch (event.code) {
case "Backspace": case "Backspace":
ch = '\b'; ch = '\b';
break; break;
@ -267,13 +267,17 @@ VT100.handle_onkeypress_ = function VT100_handle_onkeypress(event,cb)
break; break;
} }
} }
// Stop the event from doing anything else.
event.preventDefault(); // Workaround: top the event from doing anything else.
// (prevent input from adding characters instead of via VM)
event.preventDefault()
vt.key_buf_.push(ch); vt.key_buf_.push(ch);
if( cb ){ if( cb ){
cb(vt.key_buf_) cb(vt.key_buf_)
vt.key_buf_ = [] vt.key_buf_ = []
}else setTimeout(VT100.go_getch_, 0); }else setTimeout(VT100.go_getch_, 0);
return false; return false;
} }
@ -287,6 +291,7 @@ VT100.handle_onkeydown_ = function VT100_handle_onkeydown()
default: default:
return true; return true;
} }
event.preventDefault()
vt.key_buf_.push(ch); vt.key_buf_.push(ch);
setTimeout(VT100.go_getch_, 0); setTimeout(VT100.go_getch_, 0);
return false; return false;
@ -1079,7 +1084,7 @@ VT100.prototype.write = function VT100_write(stuff)
x = this.csi_parms_[j]; x = this.csi_parms_[j];
if( x > 89 && x < 98 && this.opts.rainbow ){ if( x > 89 && x < 98 && this.opts.rainbow ){
const rainbow = 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) { switch (x) {
case 0: case 0:
@ -1325,18 +1330,28 @@ VT100.prototype.throttleSmart = function throttleSmart(fn, wait) {
VT100.prototype.setupTouchInputFallback = function(){ VT100.prototype.setupTouchInputFallback = function(){
if( !this.input ){ 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 = document.createElement("input")
this.input.setAttribute("type", "text")
this.input.setAttribute("cols", this.opts.cols ) this.input.setAttribute("cols", this.opts.cols )
this.input.setAttribute("rows", this.opts.rows ) this.input.setAttribute("rows", this.opts.rows )
this.input.style.opacity = '0' this.input.style.opacity = '0'
this.input.style.position = 'absolute' this.input.style.position = 'absolute'
this.input.style.left = '-9999px' this.input.style.left = '-9999px'
this.form = document.createElement("form") this.form = document.createElement("form")
this.form.addEventListener("submit", (e) => { this.form.addEventListener("submit", (e) => {
e.preventDefault() e.preventDefault()
this.key_buf_.push('\n') this.key_buf_.push('\n')
setTimeout(VT100.go_getch_, 0); setTimeout(VT100.go_getch_, 0);
return false
}) })
this.form.appendChild(this.upload)
this.form.appendChild(this.input) this.form.appendChild(this.input)
this.scr_.parentElement.appendChild(this.form) this.scr_.parentElement.appendChild(this.form)
@ -1353,8 +1368,8 @@ VT100.prototype.setupTouchInputFallback = function(){
this.input.handler = (e) => { this.input.handler = (e) => {
let ch let ch
let isEnter = String(e?.code).toLowerCase() == "enter" || e?.code == 13 let isEnter = String(e?.key).toLowerCase() == "enter" || e?.code == 13
let isBackspace = String(e?.code).toLowerCase() == "backspace" || e?.code == 8 let isBackspace = String(e?.key).toLowerCase() == "backspace" || e?.code == 8
if( isEnter ){ if( isEnter ){
ch = '\n' ch = '\n'
}else if( isBackspace ){ }else if( isBackspace ){
@ -1372,6 +1387,7 @@ VT100.prototype.setupTouchInputFallback = function(){
this.scr_.addEventListener('touchend', (e) => this.focus() ) this.scr_.addEventListener('touchend', (e) => this.focus() )
this.scr_.addEventListener('click', (e) => this.focus() ) this.scr_.addEventListener('click', (e) => this.focus() )
} }
this.useFallbackInput = true this.useFallbackInput = true
this.focus() this.focus()
@ -1381,7 +1397,6 @@ VT100.prototype.focus = function(){
setTimeout( () => { setTimeout( () => {
const el = this[ this.useFallbackInput ? 'input' : 'scr_' ] const el = this[ this.useFallbackInput ? 'input' : 'scr_' ]
el.focus() el.focus()
console.dir(el)
}, 10 ) }, 10 )
} }

View File

@ -21,7 +21,6 @@ emulator.fs9p.update_file = async function(file,data){
inode.size = buf.length inode.size = buf.length
const now = Math.round(Date.now() / 1000); const now = Math.round(Date.now() / 1000);
inode.atime = inode.mtime = now; inode.atime = inode.mtime = now;
me.postMessage({event:'exec',data:[`touch /mnt/${file}`]}) // update inode
return new Promise( (resolve,reject) => resolve(buf) ) return new Promise( (resolve,reject) => resolve(buf) )
}catch(e){ }catch(e){
console.error({file,data}) console.error({file,data})

View File

@ -14,7 +14,7 @@ ISOTerminal.prototype.boot = async function(e){
env.push( 'export '+String(i).toUpperCase()+'="'+decodeURIComponent( document.location[i]+'"') ) 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.serial_input == 0 ){
if( !this.noboot ){ if( !this.noboot ){

View File

@ -6,7 +6,7 @@ if( typeof emulator != 'undefined' ){
const convert = ISOTerminal.prototype.convert const convert = ISOTerminal.prototype.convert
const buf = await this.emulator.read_file("dev/browser/js") const buf = await this.emulator.read_file("dev/browser/js")
const script = convert.Uint8ArrayToString(buf) const script = convert.Uint8ArrayToString(buf)
let PID="?" let PID=null
try{ try{
if( script.match(/^PID/) ){ if( script.match(/^PID/) ){
PID = script.match(/^PID=([0-9]+);/)[1] 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) // 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) )
}
}) })
} }

View File

@ -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);
}
}
}

4
com/lib/xhook.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -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 = ["<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.
`
}
});

115
com/pastedrop.js Normal file
View File

@ -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 <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.
`
}
});