/**
* Web3os Kernel
*
* @description Entrypoint of the web3os kernel
* @author Jay Mathis <code@mathis.network>
* @license MIT
* @see https://github.com/web3os-org/kernel
*/
/* global Kernel, Terminal, System */
/* global fetch, File, FileReader, localStorage, location, Notification */
'use strict'
import rootPkgJson from '../package.json'
import getConfig from './config'
import AwesomeNotifications from 'awesome-notifications'
import bytes from 'bytes'
import colors from 'ansi-colors'
import columnify from 'columnify'
import CustomEvent from 'custom-event-js'
import figlet from 'figlet'
import figletFont from 'figlet/importable-fonts/Graffiti'
import i18next from 'i18next'
import i18nextBrowserLanguageDetector from 'i18next-browser-languagedetector'
import iconify from '@iconify/iconify'
import Keyboard from 'simple-keyboard'
import path from 'path'
import sweetalert from 'sweetalert2'
import topbar from 'topbar'
import { unzip } from 'unzipit'
import { v4 as uuidv4 } from 'uuid'
import 'systemjs'
import 'animate.css'
import 'awesome-notifications/dist/style.css'
// import 'simple-keyboard/build/css/index.css'
import './css/index.css'
import './themes/default/index.css'
import './themes/default/sweetalert2.css'
import '@material/mwc-button'
import '@material/mwc-icon-button'
import '@material/mwc-snackbar'
import AppWindow from './windows'
import locales from './locales'
import README from '../README.md'
import theme from './themes/default/index.js'
import W3OSTerminal from './terminal'
import { fsModules } from './fs'
let BrowserFS
let memory
const Config = getConfig()
const figletFontName = 'Graffiti'
console.time('web3os:boot')
colors.theme(theme)
globalThis.topbar = topbar
globalThis.global = globalThis.global || globalThis
export let eventsProcessed = 0
export const poweronTime = new Date()
export const bootArgs = new URLSearchParams(globalThis.location.search)
export const isMobile = window.matchMedia('only screen and (max-width: 760px)').matches
export const { name, version } = rootPkgJson
/**
* References the initialized i18next instance
* @type {Object}
* @see https://i18next.com
*/
export const i18n = i18next
const { t } = i18n
i18n.loadAppLocales = locales => {
for (const [key, data] of Object.entries(locales)) {
i18n.addResourceBundle(key, 'app', data, true)
if (data.common) i18n.addResources(key, 'common', data.common, true)
}
}
export const Web3osTerminal = W3OSTerminal
/**
* Contains miscellaneous utilities
* @todo Do this better
* @type {Object}
*/
export const utils = { bytes, colors, path, wait }
/**
* Colorize a string to differentiate numbers and letters
* @param {string} str - The string to colorize
* @param {Object=} options - Options for colorization
* @param {Function=} [options.numbers=colors.blue()] - The function to colorize numbers
* @param {Function=} [options.letters=colors.white()] - The function to colorize letters
*/
utils.colorChars = (str, options = {}) => {
if (typeof str !== 'string') throw new Error(t('You must provide a string to colorChars'))
const numbers = options.numbers || colors.blue
const letters = options.letters || colors.white
return str.split('').map(c => isNaN(c) ? letters(c) : numbers(c)).join('')
}
/**
* Contains all registered kernel modules
* @type {Object}
*/
export const modules = {}
/**
* Contains all registered kernel intervals
* @type {Object}
*/
export const intervals = {}
/**
* References the @iconify/iconify library
*/
export const icons = iconify
/**
* Create an icon element
*
* @param {string} id - The @iconify/iconify icon identifier
* @param {Array.<string>} classes - Array of additional classes to apply to the element
* @returns {HTMLElement}
*/
export const createIcon = (id, classes) => {
const icon = document.createElement('i')
icon.classList.add(...classes)
icon.dataset.icon = id
return icon
}
/**
* Gives access to the virtual keyboard
*
* This gives us more control and consistency over mobile input
*
* @type {SimpleKeyboard}
* @see https://virtual-keyboard.js.org
*/
export let keyboard
/**
* Gives access to the BrowserFS API
* @type {Object}
*/
export let fs
/**
* The main kernel event bus
*
* @type {CustomEvent}
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent
* @see https://www.npmjs.com/package/custom-event-js
*
* @property {Function} dispatch - Dispatch an event
* @property {string} dispatch.eventName - The name of the event
* @property {Object} dispatch.detail - The event detail payload
* @property {Function} on - Subscribe to event
* @property {string} on.eventName - The name of the event
* @property {Function} on.callback - Execute when this event is triggered
* @property {Function} off - Unsubscribe from event
* @property {string} off.eventName - The name of the event
* @property {Function} off.callback - Execute when this event is triggered
*/
export const events = CustomEvent
/**
* The list of kernel events
*/
export const KernelEvents = Config.kernelEvents
/**
* The primary Broadcast Channel for web3os
*
* @type {BroadcastChannel}
* @see https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API
*/
export const broadcast = new BroadcastChannel('web3os')
broadcast.onmessage = msg => {
console.log('Incoming Broadcast:', msg)
}
/**
* Send an analytics event to the analytics endpoint
*/
export async function analyticsEvent ({ event, user, details, severity }) {
if (/^localhost/.test(location.host)) return
if (get('config', 'analytics-opt-out')) return
if (!user && !get('user', 'uuid')) set('user', 'uuid', uuidv4())
user = user || get('user', 'uuid')
const analyticsEndpoint = get('config', 'analytics-endpoint') || Config.analyticsEndpoint
if (!analyticsEndpoint) return
await fetch(analyticsEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ event: event || 'log', severity: severity || 'INFO', user, details: details || {} })
})
}
/**
* Output system information
*/
export async function printSystemInfo () {
const isSmall = window.innerWidth <= 445
let output = ''
// if (navigator.deviceMemory) output += `\n${colors.info('RAM:')} >= ${colors.muted(navigator.deviceMemory + 'GB')}\n`
if (navigator.userAgentData) {
const { brand, version } = navigator.userAgentData.brands.slice(-1)?.[0]
const browser = `${brand} v${version}`
output += `${colors.info(`${t('Host')}: `)}\t${location.host}\n`
output += `${colors.info(`${t('Platform')}:`)}\t${navigator.userAgentData.platform || t('Unknown')}\n`
output += `${colors.info(`${t('Browser')}:`)}\t${browser}\n`
}
if (navigator.getBattery) {
const batt = await navigator.getBattery()
batt.addEventListener('chargingchange', () => {
if (batt.charging && !get('config', 'battery-no-charge-confetti')) execute('confetti')
})
output += `${colors.info(`${t('Battery')}:`)}\t${batt.charging ? `${batt.level * 100}% β‘` : `${batt.level * 100}% π`}\n`
}
if (navigator.storage?.estimate) {
const storageDetails = await navigator.storage.estimate()
const used = bytes(storageDetails.usage)
const free = bytes(storageDetails.quota - storageDetails.usage)
output += `${colors.info(`${t('Storage')}:`)}\t${used} ${isSmall ? '\n\t ' : '/'} ${free}\n`
}
if (console.memory) output += `${colors.info(t('Heap Limit') + ':')}\t${bytes(console.memory.jsHeapSizeLimit)}\n`
if (navigator.hardwareConcurrency) output += `${colors.info(t('Cores') + ':')}\t\t${navigator.hardwareConcurrency}\n`
if (typeof navigator.onLine === 'boolean') output += `${colors.info(t('Online') + ':')}\t\t${navigator.onLine ? t('Yes') : t('No')}\n`
if (navigator.connection) output += `${colors.info(t('Downlink') + ':')}\t~${navigator.connection.downlink} Mbps\n`
log(output)
return output
}
/**
* Output the boot introduction
* */
export async function printBootIntro () {
const isSmall = window.innerWidth <= 445
log(colors.info(`${t('kernel:bootIntroSubtitle', '\t https://web3os.sh')}`))
log(colors.heading.success.bold(`\n${isSmall ? '' : '\t '} web3os kernel v${rootPkgJson.version} `))
log(colors.warning(`${isSmall ? '' : '\t '}β BETA β \n`))
await printSystemInfo()
if (!localStorage.getItem('web3os_first_boot_complete')) {
log(colors.danger(`\nβ ${t('kernel:firstBootWarning', 'The first boot will take the longest, please be patient!')} β \n`))
}
if (navigator.userAgentData?.platform === 'Android' || navigator.userAgentData?.platform === 'iPhone' || window.innerWidth < 500 || window.innerHeight < 500) {
log(colors.danger(`\nβ π ${t('kernel:mobileExperienceWarning', 'NOTE: The mobile experience is pretty wacky and experimental - proceed with caution!')} β `))
}
log(colors.underline(t('A few examples') + ':'))
log(colors.danger(`\n${t('typeVerb', 'Type')} ${colors.bold.underline(Terminal.createSpecialLink('web3os:execute:help', 'help'))} ${t('kernel:bootIntro.help', 'for help')}`))
log(colors.gray(`${t('typeVerb', 'Type')} ${colors.bold.underline(Terminal.createSpecialLink('web3os:execute:docs', 'docs'))} ${t('kernel:bootIntro.docs', 'to open the documentation')}`))
log(colors.info(`${t('typeVerb', 'Type')} ${colors.bold.underline(Terminal.createSpecialLink('web3os:execute:desktop', 'desktop'))} ${t('kernel:bootIntro.desktop', 'to launch the desktop')}`))
// log(colors.primary(`${t('typeVerb', 'Type')} ${colors.bold.underline('wallet connect')} ${t('kernel:bootIntro.wallet', 'to connect your wallet')}`))
log(colors.success(`${t('typeVerb', 'Type')} ${colors.bold.underline(Terminal.createSpecialLink('web3os:execute:files /bin', 'files /bin'))} ${t('kernel:bootIntro.filesBin', 'to explore all executable commands')}`))
// log(colors.warning(`${t('typeVerb', 'Type')} ${colors.bold.underline('lsmod')} ${t('kernel:bootIntro.lsmod', 'to list all kernel modules')}`))
log(colors.muted(`${t('typeVerb', 'Type')} ${colors.bold.underline(`clip <${t('Command')}>`)} ${t('kernel:bootIntro.clip', 'to copy the output of a command to the clipboard')}`))
// log(colors.white(`${t('typeVerb', 'Type')} ${colors.bold.underline('repl')} ${t('kernel:bootIntro.repl', 'to run the interactive Javascript terminal')}`))
log(colors.cyan(`${t('typeVerb', 'Type')} ${colors.bold.underline(Terminal.createSpecialLink('web3os:execute:confetti', 'confetti'))} ${t('kernel:bootIntro.confetti', 'to fire the confetti gun π')}`))
// log(colors.magenta(`${t('typeVerb', 'Type')} ${colors.bold.underline('minipaint')} ${t('kernel:bootIntro.minipaint', 'to draw Artβ’ π¨')}`))
if (bootArgs.get('source') !== 'pwa') {
isSmall ? log('\n-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-') : log('\n-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-')
log(colors.success(`${t('typeVerb', 'Type')} ${colors.bold.underline(Terminal.createSpecialLink('web3os:execute:install', 'install'))} ${t('kernel:bootIntro.install', 'to install web3os to your device')}`))
isSmall ? log('-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-') : log('-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-')
}
log('\nhttps://docs.web3os.sh')
log('https://github.com/web3os-org')
log(colors.muted(`\n${t('Booting')}...`))
}
/**
* Write the kernel memory to localStorage
*/
export function updateLocalStorage () { localStorage.setItem('memory', JSON.stringify(memory)) }
/**
* Restore the kernel memory from localStorage
*/
export function loadLocalStorage () {
try {
const storedMemory = localStorage.getItem('memory')
memory = storedMemory ? JSON.parse(storedMemory) : { firstBootVersion: rootPkgJson.version }
updateLocalStorage()
} catch (err) {
console.error(err)
log(t('Failed to load memory from local storage'))
memory = {}
}
console.assert(typeof memory === 'object', t('Failed to load memory from local storage'))
}
/**
* Set a value in the kernel memory object
* @param {!string} namespace - The namespace in which to set the value
* @param {!string} key - The key in which to set the value
* @param {!any} value - The value to set
*/
export function set (namespace, key, value) {
if (!namespace || namespace === '') throw new Error(t('Invalid namespace'))
if (!key || key === '') throw new Error(t('Invalid key'))
if (!value || value === '') throw new Error(t('Invalid value'))
memory[namespace] = memory[namespace] || {}
memory[namespace][key] = value
updateLocalStorage()
return memory[namespace]?.[key]
}
/**
* Get a value from the kernel memory object
* @param {!string} namespace - The namespace from which to get the value
* @param {?string} key - The key to retrieve, or undefined to get entire namespace
*/
export function get (namespace, key) {
if (!key) return memory[namespace] || null
return memory[namespace]?.[key] || null
}
/**
* Dump the kernel memory to a JSON object
* @returns {string} dump The memory dump
*/
export function dump () { return JSON.stringify(memory) }
/**
* Restore the kernel memory from a JSON object
* @param {!string} json - The JSON object generated from dump()
*/
export function restore (json) {
memory = JSON.parse(json)
updateLocalStorage()
return memory
}
/**
* Delete the specified key from the kernel memory object
* @param {!string} namespace - The namespace containing the key
* @param {!key} key - The key to delete
*/
export function deleteKey (namespace, key) {
if (!memory[namespace]?.[key]) throw new Error(t('Invalid namespace or key'))
delete memory[namespace][key]
updateLocalStorage()
}
/**
* Delete the specified namespace from the kernel memory object
* @param {!namespace} namespace - The namespace to delete
*/
export function deleteNamespace (namespace) {
if (!memory[namespace]) throw new Error(t('Invalid namespace'))
delete memory[namespace]
updateLocalStorage()
}
/**
* Log a message to the terminal
* @param {!any} message - The message or object to log
* @param {?Object} options - Logging options
* @param {Boolean=} [options.console=true] - Enable logging to both browser console and Terminal
* @param {?Web3osTerminal} options.terminal - The terminal to attach to, or undefined for global Terminal
*/
export function log (message, options = { console: true }) {
if (!message) return
message = message.replace(/\\n/gm, '\n')
if (options.console) console.log(message)
const term = options.terminal || globalThis.Terminal
term.log(message)
}
/**
* Manages interactions with the windowing system
* @type Object
* @property {Set} _collection - The collection of windows
*
* @property {Function} create - Create a new window
* @property {Object} create.options - Options to pass to WinBox
*
* @property {Function} find - Find a window by ID
* @property {string} find.id - The ID of the window (usually winbox-#; stored on app.window.id)
*
* @see https://nextapps-de.github.io/winbox/
*/
export const windows = {
_collection: new Set(),
create: options => {
const app = new AppWindow(options)
windows._collection.add(app)
return app
},
find: id => {
return Array.from(windows._collection.values()).find(app => app.window.id === id)
}
}
/**
* Show a SweetAlert dialog
* @async
* @param {Object} options - SweetAlert2 options
* @returns {SweetAlertDialog}
*/
export async function dialog (options = {}) {
return sweetalert.fire({
heightAuto: false,
denyButtonColor: 'red',
confirmButtonColor: 'green',
customClass: { container: 'web3os-kernel-dialog-container' },
...options
})
}
/**
* Execute a command
* @async
* @param {string} cmd - The command to execute
* @returns {CommandResult}
*/
export async function execute (cmd, options = {}) {
const exec = cmd.split(' ')[0]
const term = options.terminal || globalThis.Terminal
let command = term.aliases[exec] ? modules[term.aliases[exec]] : modules[exec]
if (options.topbar) topbar.show()
if (!command) {
try {
if (fs.existsSync(exec)) {
const data = JSON.parse(fs.readFileSync(exec).toString())
command = modules[data?.name]
} else {
command = await import(`./modules/${exec}`)
}
} catch (err) {
console.error(err)
}
}
options.doPrompt = options.doPrompt || false
if (options.topbar) topbar.hide()
if (!command?.run) {
term.log(colors.danger(`Invalid command; try ${Terminal.createSpecialLink('web3os:execute:help', colors.white('help'))} or ${Terminal.createSpecialLink(`web3os:execute:3pm install ${exec}`, colors.blue(`3pm install ${exec}`))}`))
navigator.vibrate([200, 50, 200])
return term.prompt()
}
try {
if (options.topbar) topbar.show()
const args = cmd.split(' ').slice(1).join(' ')
const result = await command.run(term, args)
if (options.topbar) topbar.hide()
if (options.doPrompt) term.prompt()
return result
} catch (err) {
navigator.vibrate([200, 50, 200])
console.error(command, err)
if (err) term.log(err.message || 'An unknown error occurred')
if (options.doPrompt) term.prompt()
throw err
}
}
/**
* Execute a script file
* @async
* @param {string} path - The path of the script
* @returns {SweetAlertDialog}
*/
export async function executeScript (filename, options = {}) {
const term = options.terminal || globalThis.Terminal
if (!filename || filename === '') return term.log(colors.danger('Invalid filename'))
filename = utils.path.resolve(term.cwd, filename)
const value = fs.readFileSync(filename, 'utf8')
const commands = value.split('\n').map(c => c.trim())
for (const cmd of commands) {
if (cmd?.trim().length > 0 && cmd?.[0] !== '#' && cmd?.substr(0, 2) !== '//') await execute(cmd, { terminal: term, doPrompt: false })
}
}
/**
* Execute the /config/autostart.sh script
* @async
* @param {string=} defaultAutoStart - Write autostart script with this content if it doesn't exist
*/
export async function autostart (defaultAutoStart) {
try {
if (defaultAutoStart && !fs.existsSync('/config/autostart.sh')) {
fs.writeFileSync('/config/autostart.sh', defaultAutoStart) // Setup default autostart.sh
}
if (fs.existsSync('/config/autostart.sh')) await executeScript('/config/autostart.sh')
} catch (err) {
console.error(err)
log(colors.danger('Failed to complete autostart script'))
} finally {
globalThis.Terminal.prompt()
}
}
/**
* Send an awesome-notification
* @type {AwesomeNotifications}
* @todo awesome-notifications will likely eventually replace sweetalert
* @see https://f3oall.github.io/awesome-notifications
*/
export const notify = new AwesomeNotifications({
position: 'top-right',
icons: {
enabled: true,
prefix: '<i class="iconify" data-icon="fa6-regular:',
suffix: '" />'
}
})
/**
* Send a browser/platform notification
* @async
* @param {Object=} options - The notification options (Notification API)
* @see https://developer.mozilla.org/en-US/docs/Web/API/notification
*/
export async function systemNotify (options = {}) {
if (Notification.permission !== 'granted') throw new Error('Notification permission denied')
try {
const notification = new Notification(options.title, options)
} catch (err) {
console.warn(err)
navigator.serviceWorker.ready.then(registration => {
registration.showNotification('Notification with ServiceWorker')
})
}
}
/**
* Show a snackbar notification
*
* @deprecated
* This can still be useful I think, but it's being deprecated
* in favor of awesome-notifications. Should this be removed?
*
* @async
* @param {Object=} options - The snackbar options
* @see https://www.npmjs.com/package/@material/mwc-snackbar
*/
export async function snackbar (options = {}) {
const snack = document.createElement('mwc-snackbar')
snack.id = options.id || 'snack-' + Math.random()
snack.leading = options.leading || false
snack.closeOnEscape || false
snack.labelText = options.labelText || ''
snack.stacked = options.stacked || false
const closeButton = document.createElement('mwc-icon-button')
closeButton.icon = 'close'
closeButton.slot = 'dismiss'
snack.appendChild(closeButton)
document.body.appendChild(snack)
snack.show()
}
/**
* Sets up the filesystem
*
* These parameters can also be provided as a query parameter:
*
* https://web3os.sh/?initfsUrl=https://my.place.com/initfs.zip
*
* Bare minimum, temporary, fs:
* https://web3os.sh/?mountableFilesystemConfig={ "/": { "fs": "InMemory" }, "/bin": { "fs": "InMemory" } }
*
* @async
* @param {string=} initfsUrl - URL to a zipped filesystem snapshot
* @param {MountableFileSystemOptions=} mountableFilesystemConfig - Filesystem configuration object for BrowserFS
* @see https://jvilk.com/browserfs/2.0.0-beta/index.html
*/
export async function setupFilesystem (initfsUrl, mountableFilesystemConfig) {
return new Promise(async (resolve, reject) => {
const browserfs = await import('browserfs')
const filesystem = {}
let initfs
initfsUrl = initfsUrl || bootArgs.get('initfsUrl')
mountableFilesystemConfig = bootArgs.get('mountableFilesystemConfig')
? JSON.parse(bootArgs.get('mountableFilesystemConfig')) : Config.defaultFilesystemOverlayConfig
if (bootArgs.has('initfsUrl')) {
try {
const result = await dialog({
title: 'Use initfs?',
icon: 'warning',
allowOutsideClick: false,
allowEscapeKey: false,
showDenyButton: true,
showLoaderOnConfirm: true,
focusDeny: true,
confirmButtonText: 'Yes',
html: `
<p>Do you want to overwrite existing files in your filesystem with the initfs located at:</p>
<h4><a href="${initfsUrl}" target="_blank">${initfsUrl}</a></h4>
<p><strong>Be sure you trust the source!</strong></p>
`,
preConfirm: async () => {
try {
return await unzip(initfsUrl)
} catch (err) {
console.error(err)
globalThis.Terminal?.log(colors.danger(`Failed to unzip initfsUrl at ${initfsUrl}`))
return true
}
}
})
if (result.isDenied) throw new Error('User rejected using initfs')
const { entries } = result.value
initfs = entries
globalThis.history.replaceState(null, null, '/') // prevent reload with initfs
} catch (err) {
globalThis.Terminal?.log(colors.danger('Failed to unzip initfsUrl ' + initfsUrl))
globalThis.Terminal?.log(colors.danger(err.message))
console.error(err)
}
}
/**
* Add HTML5FS
* @todo Broken on Firefox
* @see: webkitStorageOptions
*/
if (!navigator.userAgent.includes('Firefox')) {
mountableFilesystemConfig['/mount/html5fs'] = {
fs: 'AsyncMirror',
options: {
sync: { fs: 'InMemory' },
async: {
fs: 'HTML5FS',
options: {}
}
}
}
}
browserfs.install(filesystem)
browserfs.configure({
fs: 'MountableFileSystem',
options: mountableFilesystemConfig
}, err => {
if (err) {
console.error(err)
log(colors.danger(`Failed to initialize filesystem: ${err.message}`))
} else {
BrowserFS = filesystem
fs = filesystem.require('fs')
// Use an initfs if available
if (initfs) {
Object.entries(initfs).forEach(async ([name, entry]) => {
const filepath = utils.path.join('/', name)
if (entry.isDirectory) !fs.existsSync(filepath) && fs.mkdirSync(utils.path.join('/', name))
else {
const parentDir = utils.path.parse(filepath).dir
if (!fs.existsSync(parentDir)) fs.mkdirSync(parentDir)
fs.writeFileSync(filepath, BrowserFS.Buffer.from(await entry.arrayBuffer()))
}
})
}
// Prepare required paths for packages
const defaultPackages = bootArgs.get('defaultPackages') || Config.defaultPackages || []
if (!fs.existsSync('/var')) fs.mkdirSync('/var')
if (!fs.existsSync('/var/packages')) fs.mkdirSync('/var/packages')
if (!fs.existsSync('/config')) fs.mkdirSync('/config')
if (!fs.existsSync('/config/packages')) fs.writeFileSync('/config/packages', JSON.stringify(bootArgs.has('noDefaultPackages') ? [] : defaultPackages))
// Populate initial procfs
// TODO: Make procfs separate module and self-updating
fs.writeFileSync('/proc/host', location.host)
fs.writeFileSync('/proc/version', rootPkgJson.version)
fs.writeFileSync('/proc/platform', navigator.userAgentData.platform)
fs.writeFileSync('/proc/querystring', location.search)
fs.writeFileSync('/proc/language', navigator.language)
fs.writeFileSync('/proc/user-agent', navigator.userAgent)
fs.writeFileSync('/proc/user-agent.json', JSON.stringify(navigator.userAgentData, null, 2))
try {
const { downlink, effectiveType, rtt, saveData } = navigator.connection
fs.writeFileSync('/proc/connection', JSON.stringify({ downlink, effectiveType, rtt, saveData }, null, 2))
} catch {}
// Drag and drop on terminal
// const dragenter = e => { e.stopPropagation(); e.preventDefault() }
// const dragover = e => { e.stopPropagation(); e.preventDefault() }
// const drop = e => {
// e.stopPropagation()
// e.preventDefault()
// const dt = e.dataTransfer
// const files = dt.files
// for (const file of files) {
// const reader = new FileReader()
// reader.readAsArrayBuffer(file)
// reader.onload = () => {
// const buffer = BrowserFS.Buffer.from(reader.result)
// const filepath = utils.path.resolve(Terminal.cwd, file.name)
// fs.writeFileSync(filepath, buffer)
// snackbar({ labelText: `Uploaded ${filepath}` })
// }
// }
// }
// Terminal.addEventListener('dragenter', dragenter)
// Terminal.addEventListener('dragover', dragover)
// Terminal.addEventListener('drop', drop)
resolve(fs)
}
})
})
}
/**
* Load the kernel's core internal commands
*
* @todo These are here because they're relatively simple, but many of them should be
* moved to their own respective external modules.
*
* @async
*/
async function registerKernelBins () {
const { t } = Kernel.i18n
const kernelBins = {}
kernelBins.alert = { description: t('kernel:bins.descriptions.alert', 'Show an alert'), run: (term, context) => dialog({ text: context }) }
kernelBins.clear = { description: t('kernel:bins.descriptions.clear', 'Clear the terminal'), run: term => term.clear() }
kernelBins.date = { description: t('kernel:bins.descriptions.date', 'Display the date/time'), run: term => term.log(new Intl.DateTimeFormat(Kernel.i18n.language || 'en-US', { dateStyle: 'long', timeStyle: 'short' }).format(new Date()))}
kernelBins.docs = { description: t('kernel:bins.descriptions.docs', 'Open the documentation'), run: term => { modules.www.run(term, '--title "Web3os Documentation" --no-toolbar /docs') }}
kernelBins.dump = { description: t('kernel:bins.descriptions.dump', 'Dump the memory state'), run: term => term.log(dump()) }
kernelBins.echo = { description: t('kernel:bins.descriptions.echo', 'Echo some text to the terminal'), run: (term, context) => term.log(context) }
kernelBins.history = { description: t('kernel:bins.descriptions.history', 'Show command history'), run: term => { return term.log(JSON.stringify(term.history)) } }
kernelBins.import = { description: t('kernel:bins.descriptions.import', 'Import a module from a URL'), run: async (term, context) => await importModuleUrl(context) }
kernelBins.man = { description: t('kernel:bins.descriptions.man', 'Alias of help'), run: (term, context) => modules.help.run(term, context) }
kernelBins.sh = { description: t('kernel:bins.descriptions.sh', 'Execute a web3os script'), run: (term, context) => executeScript(context, { terminal: term }) }
kernelBins.systeminfo = { description: t('kernel:bins.descriptions.systeminfo', 'Print system information'), run: async () => await printSystemInfo() }
kernelBins.systemnotify = { description: t('kernel:bins.descriptions.systemnotify', 'Show a browser/platform notification; e.g., systemnotify Title Body'), run: (term, context) => systemNotify({ title: context.split(' ')[0], body: context.split(' ')[1] }) }
kernelBins.reboot = { description: t('kernel:bins.descriptions.reboot', 'Reload web3os'), run: () => location.reload() }
kernelBins.restore = { description: t('kernel:bins.descriptions.restore', 'Restore the memory state'), run: (term, context) => restore(context) }
kernelBins.snackbar = { description: t('kernel:bins.descriptions.snackbar', 'Show a snackbar; e.g. snackbar Alert!'), run: (term, context) => snackbar({ labelText: context }) }
kernelBins.wait = { description: t('kernel:bins.descriptions.wait', 'Wait for the specified number of milliseconds'), run: (term, context) => wait(context) }
kernelBins.alias = {
description: t('kernel:bins.descriptions.alias', 'Set or list command aliases'),
help: `${t('Usage')}: alias [src] [dest]`,
run: (term, context) => {
if (!context || context === '') return term.log(term.aliases)
const command = context.split(' ')
if (command.length !== 2) throw new Error('You must specify the src and dest commands')
term.aliases[command[0]] = command[1]
}
}
kernelBins.ipecho = {
description: t('kernel:bins.descriptions.ipecho', 'Echo your public IP address'),
run: async (term = globalThis.Terminal) => {
const endpoint = get('config', 'ipecho-endpoint') || Config.ipechoEndpoint || 'https://ipecho.net/plain'
const result = await fetch(endpoint)
const ip = await result.text()
console.log({ ip })
term.log(ip)
return ip
}
}
kernelBins.lsmod = {
description: t('kernel:bins.descriptions.lsmod', 'List loaded kernel modules'),
run: async (term = globalThis.Terminal) => {
let mods = { ...modules }
// This is useless right now, but in the future it may help us transition to other environments:
if (module?.exports) mods = { ...mods, ...module.exports }
const sortedMods = Object.keys(mods).sort()
term.log(sortedMods)
return sortedMods
}
}
kernelBins.memoryinfo = {
description: `${t('kernel:bins.descriptions.memoryinfo', 'Show Javascript heap information')}`,
run: async (term = globalThis.Terminal) => {
const { jsHeapSizeLimit, totalJSHeapSize, usedJSHeapSize } = console.memory
const meminfo = {
jsHeapSizeLimit: bytes(jsHeapSizeLimit),
totalJSHeapSize: bytes(totalJSHeapSize),
usedJSHeapSize: bytes(usedJSHeapSize)
}
term.log(meminfo)
return meminfo
}
}
kernelBins.open = {
description: `${t('kernel:bins.descriptions.open', 'Open a shortcut defined in /config/shortcuts')}`,
run: async (term = globalThis.Terminal, context) => {
if (!fs.existsSync('/config/shortcuts')) throw new Error('/config/shortcuts does not exist')
const shortcuts = JSON.parse(fs.readFileSync('/config/shortcuts', 'utf8'))
if (!context || context === '') return term.log(JSON.stringify(shortcuts, null, 2))
if (!shortcuts[context]) throw new Error('Invalid shortcut')
const shortcut = shortcuts[context]
switch (shortcut.type) {
case 'execute':
const cmds = shortcut.target.split(';').map(cmd => cmd.trim())
for await (const cmd of cmds) term.kernel.execute(cmd)
break
case 'url':
return window.open(shortcut.target, '_blank')
}
}
}
kernelBins.storageinfo = {
description: t('kernel:bins.descriptions.storage', 'Display storage usage information'),
run: async (term = globalThis.Terminal) => {
const rawData = await navigator.storage.estimate()
const data = {
quota: bytes(rawData.quota),
usage: bytes(rawData.usage),
usageDetails: {}
}
Object.entries(rawData.usageDetails).forEach(entry => {
data.usageDetails[entry[0]] = bytes(entry[1])
})
term.log(data)
return data
}
}
kernelBins.set = {
description: t('kernel:bins.descriptions.set', 'Set a kernel memory value'),
help: `${t('Usage')}: set <namespace> <key> <value>`,
run: (term, context = '') => {
const parts = context.split(' ')
const namespace = parts[0]
const key = parts[1]
const value = parts.slice(2, parts.length).join(' ')
term.log(set(namespace, key, value))
}
}
kernelBins.get = {
description: t('kernel:bins.descriptions.get', 'Get a kernel memory namespace or key'),
help: `${t('Usage')}: get <namespace> [key]`,
run: (term, context = '') => {
const parts = context.split(' ')
const namespace = parts[0]
const key = parts[1]
const result = get(namespace, key)
term.log(typeof result === 'string' ? result : JSON.stringify(result))
}
}
kernelBins.unset = {
description: t('kernel:bins.descriptions.unset', 'Delete specified memory namespace or key'),
help: `${t('Usage')}: unset <namespace> [key]`,
run: (term, context = '') => {
try {
const parts = context.split(' ')
if (parts[1]) deleteKey(parts[0], parts[1])
else deleteNamespace(parts[0])
} catch (err) {
console.error(err)
term.log(colors.danger(err.message))
}
}
}
kernelBins.eval = {
description: t('kernel:bins.descriptions.eval', 'Load and evaluate a Javascript file'),
run: (term, context) => {
if (!context || context === '') return term.log(colors.danger('Invalid filename'))
const filename = utils.path.resolve(term.cwd, context)
if (fs.existsSync(filename)) {
const code = fs.readFileSync(filename, 'utf-8')
eval(code) // eslint-disable-line
} else {
eval(context)
}
}
}
kernelBins.clip = {
description: t('kernel:bins.descriptions.clip', 'Copy return value of command to clipboard'),
help: `${t('Usage')}: clip <command>`,
run: async (term, context) => {
if (!context || context === '') return
const parts = context.split(' ')
const mod = modules[parts[0]]
let result = await mod.run(term, parts.splice(1).join(' '))
if (Array.isArray(result) && result.length === 1) result = result[0]
return await navigator.clipboard.writeText(typeof result === 'string' ? result : JSON.stringify(result, null, 2))
}
}
kernelBins.height = {
description: t('kernel:bins.descriptions.height', 'Set body height'),
run: (term, context) => { document.body.style.height = context }
}
kernelBins.width = {
description: t('kernel:bins.descriptions.width', 'Set body width'),
run: (term, context) => { document.body.style.width = context }
}
kernelBins.objectUrl = {
description: t('kernel:bins.descriptions.objectUrl', 'Create an ObjectURL for a file'),
run: (term, filename) => {
const { t } = Kernel.i18n
if (!filename || filename === '') throw new Error(t('invalidFilename', 'Invalid filename'))
const data = fs.readFileSync(utils.path.join(term.cwd, filename))
const file = new File([data], utils.path.parse(filename).base, { type: 'application/octet-stream' })
const url = URL.createObjectURL(file)
term.log(url)
return url
}
}
kernelBins.geo = {
description: t('kernel:bins.descriptions.geo', 'Geolocation Utility'),
run: async (term = globalThis.Terminal) => {
if (!navigator.geolocation) throw new Error(t('kernel:bins.errors.geo.geolocationUnavailable', 'Geolocation is not available'))
return new Promise((resolve, reject) => {
try {
navigator.geolocation.getCurrentPosition(pos => {
const { latitude, longitude } = pos.coords
const link = `https://www.openstreetmap.org/search?query=${latitude}%2C${longitude}`
term.log({ latitude, longitude, link, pos })
resolve({ latitude, longitude, link, pos })
})
} catch (err) {
console.error(err)
if (err.message) term.log(colors.danger(err.message))
reject(err)
}
})
}
}
kernelBins.eyedropper = {
description: t('kernel:bins.descriptions.eyeDropper', 'Pick colors using the eyedropper'),
run: async (term = globalThis.Terminal) => {
const dropper = new EyeDropper()
const color = await dropper.open()
term.log(color)
return color
}
}
for (const [name, mod] of Object.entries(kernelBins)) {
loadModule(mod, { name: `@web3os-core/${name}` })
}
}
/**
* Load the kernel's core external modules
* @async
*/
async function registerBuiltinModules () {
const mods = process.env.BUILTIN_MODULES ? process.env.BUILTIN_MODULES.split(',') : (Config.builtinModules || [])
for (const mod of mods) {
try {
const modBin = await import(`./modules/${mod}`)
await loadModule(modBin, { name: `@web3os-core/${mod}` })
} catch (err) {
console.error(err)
Terminal.log(colors.danger(`Error loading module: ${mod}`))
Terminal.log(err.message)
}
}
}
/**
* Load a module into the kernel
* @async
* @param {!ModInfo} mod - The mod to load
* @param {Object=} options - Options for loading the module
*/
export async function loadModule (mod, options = {}) {
const { t } = Kernel.i18n
if (!mod) throw new Error(`${t('kernel:invalidModule', 'Invalid module provided to')} kernel.loadModule`)
let { description, help, name, run, version, pkgJson } = options
description = description || mod.description
version = version || pkgJson?.version || mod.version
help = help || mod.help || t('kernel:helpNotExported', 'Help is not exported from this module')
name = name || mod.name || 'module_' + Math.random().toString(36).slice(2)
run = run || mod.run || mod.default
if (!modules[name]) modules[name] = {}
modules[name] = { ...modules[name], ...mod, run, name, version, description, help }
const web3osData = pkgJson?.web3osData
const modInfo = {
name,
version,
description,
web3osData,
help
}
if (run) {
let modBin
if (name.includes('/')) {
modBin = utils.path.join('/bin', name.split('/')[0], name.split('/')[1])
if (!fs.existsSync(`/bin/${name.split('/')[0]}`)) fs.mkdirSync(`/bin/${name.split('/')[0]}`)
} else {
modBin = utils.path.join('/bin', name)
}
fs.writeFileSync(modBin, JSON.stringify(modInfo, null, 2))
}
events.dispatch('ModuleLoaded', { modInfo })
}
/**
* Directly import an ES module from a URL
* @async
* @param {string} url - The URL of the module to import
* @return {Module}
*/
export async function importModuleUrl (url) {
return await import(/* webpackIgnore: true */ url)
}
/**
* Directly import a UMD module from a URL
* @async
* @param {string} url - The URL of the module to import
* @return {Module}
*/
export async function importUMDModule (url, name, module = { exports: {} }) {
// Dark magic stolen from a lost tome of stackoverflow
const mod = (Function('module', 'exports', await (await fetch(url)).text())
.call(module, module, module.exports), module).exports
mod.default = mod.default || mod
return mod
}
/**
* Load or install the packages defined in /config/packages
* @async
*/
export async function loadPackages () {
const tasks = []
const packages = JSON.parse(fs.readFileSync('/config/packages').toString())
for (const pkg of packages) {
tasks.push(new Promise(async (resolve, reject) => {
try {
if (/^(http|ftp).*\:/i.test(pkg)) {
if (modules?.['3pm']) {
await modules['3pm'].install(pkg, { warn: false })
} else {
const waitFor3pm = async () => {
if (!modules?.['3pm']) return setTimeout(waitFor3pm, 500)
await modules['3pm'].install(pkg, { warn: false })
}
await waitFor3pm()
}
return resolve()
}
const pkgJson = JSON.parse(fs.readFileSync(`/var/packages/${pkg}/package.json`))
const main = pkgJson.web3osData.main || pkgJson.main || 'index.js'
const type = pkgJson.web3osData.type || 'es'
const mainUrl = `${pkgJson.web3osData.url}/${main}`
const mod = type === 'umd'
? await importUMDModule(mainUrl)
: await importModuleUrl(mainUrl)
await loadModule(mod, pkgJson)
resolve()
} catch (err) {
console.error(err)
globalThis.Terminal.log(colors.danger(err.message))
reject(err)
}
}))
}
await Promise.all(tasks)
}
/**
* Show the boot splash screen
* @async
* @todo Make this more customizable
* @param {string} msg - The message to display
* @param {Object=} options - The splash screen options
*/
export async function showSplash (msg, options = {}) {
const { t } = Kernel.i18n
document.querySelector('#web3os-splash')?.remove()
// TODO: Migrating everything to iconify
const icon = document.createElement('mwc-icon')
icon.id = 'web3os-splash-icon'
icon.style.color = options.iconColor || '#03A062'
icon.style.fontSize = options.iconFontSize || '10em'
icon.style.marginTop = '2rem'
icon.innerText = options.icon || 'hourglass_empty'
if (!options.disableAnimation) icon.classList.add('animate__animated', 'animate__zoomIn')
const title = document.createElement('h1')
title.id = 'web3os-splash-title'
title.innerHTML = options.title || 'web3os'
title.style.color = options.titleColor || 'white'
title.style.margin = 0
title.style.fontSize = options.titleFontSize || 'clamp(0.5rem, 6rem, 7rem)'
title.style.fontVariant = 'small-caps'
title.style.textShadow = '4px 4px 4px #888'
if (!options.disableAnimation) title.classList.add('animate__animated', 'animate__zoomIn')
const subtitle = document.createElement('h2')
subtitle.id = 'web3os-splash-subtitle'
subtitle.innerHTML = options.subtitle || t('kernel:bootIntroSubtitle', 'Made with <span class="heart">β₯</span> by Jay Mathis')
subtitle.style.margin = 0
subtitle.style.color = options.subtitleColor || '#ccc'
subtitle.style.fontStyle = options.subtitleFontStyle || 'italic'
if (subtitle.querySelector('span.heart')) {
subtitle.querySelector('span.heart').style.color = 'red'
subtitle.querySelector('span.heart').style.fontSize = '1.5em'
}
if (!options.disableAnimation) subtitle.classList.add('animate__animated', 'animate__zoomInDown') && subtitle.style.setProperty('--animate-delay', '0.5s')
const background = document.createElement('div')
background.id = 'web3os-splash-background'
background.style.backgroundColor = '#121212'
background.style.position = 'absolute'
background.style.top = 0
background.style.left = 0
background.style.width = '100vw'
background.style.height = '100vh'
background.style.zIndex = 100001
const message = document.createElement('h3')
message.id = 'web3os-splash-message'
message.style.color = 'silver'
message.style.fontSize = '2.5rem'
message.textContent = msg || `πΎ ${t('Booting', 'Booting')}... πΎ`
const versionInfo = document.createElement('h4')
versionInfo.id = 'web3os-splash-version'
versionInfo.style.color = '#333'
versionInfo.style.position = 'fixed'
versionInfo.style.bottom = '0.5rem'
versionInfo.style.right = '1rem'
versionInfo.textContent = `v${rootPkgJson.version}`
const container = document.createElement('div')
container.id = 'web3os-splash'
container.style.display = 'flex'
container.style.position = 'absolute'
container.style.top = 0
container.style.left = 0
container.style.margin = 0
container.style.flexDirection = 'column'
container.style.justifyContent = 'center'
container.style.alignItems = 'center'
container.style.height = '100vh'
container.style.width = '100vw'
container.style.zIndex = 100002
container.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'
container.appendChild(title)
container.appendChild(subtitle)
container.appendChild(icon)
container.appendChild(message)
container.appendChild(versionInfo)
document.body.appendChild(background)
document.body.appendChild(container)
// TODO: Make flexible
if (!options.disableAnimation) {
let index = 0
const icons = ['hourglass_empty', 'hourglass_bottom', 'hourglass_top']
setTimeout(() => {
icon.classList.remove('animate__animated', 'animate__zoomIn')
icon.classList.add('rotating')
}, 1000)
setInterval(() => {
icon.innerText = icons[index]
index++
if (!icons[index]) index = 0
}, 500)
}
return () => {
background.classList.add('animate__animated', 'animate__fadeOut', 'animate__fast')
container.classList.add('animate__animated', 'animate__fadeOut', 'animate__fast')
background.addEventListener('animationend', background.remove)
container.addEventListener('animationend', container.remove)
}
}
/**
* Boot the kernel
*
* This kicks off the process of initializing the filesystem, modules, and other components
* @async
*/
export async function boot () {
i18n
.use(i18nextBrowserLanguageDetector)
.init({
fallbackLng: 'en-US',
debug: process.env.I18N_DEBUG === 'true',
ns: ['common', 'kernel', 'app'],
defaultNS: 'common',
resources: locales
})
topbar.show()
const bootArgs = new URLSearchParams(globalThis.location.search)
globalThis.addEventListener('beforeunload', async () => {
await showSplash(`${t('Rebooting')}...`, { icon: 'autorenew', disableAnimation: true, disableVideoBackground: true })
document.querySelector('#web3os-splash-icon').classList.add('rotating')
})
// const keyboardElement = document.createElement('div')
// keyboardElement.classList.add('simple-keyboard')
// // keyboardElement.style.display = 'none'
// keyboardElement.style.position = 'absolute'
// keyboardElement.style.top = '0'
// document.body.appendChild(keyboardElement)
// keyboard = new Keyboard({
// onChange: input => events.dispatch('MobileKeyboardChange', input),
// onKeyPress: button => events.dispatch('MobileKeyboardKeyPress', button)
// })
if (!bootArgs.has('nobootsplash')) {
events.dispatch('ShowSplash')
const closeSplash = await showSplash()
setTimeout(closeSplash, 1500) // Prevent splash flash. The splash is pretty and needs to be seen and validated.
document.querySelector('#web3os-terminal').style.display = 'block'
setTimeout(globalThis.Terminal?.fit, 50)
globalThis.Terminal?.focus()
} else {
document.querySelector('#web3os-terminal').style.display = 'block'
setTimeout(globalThis.Terminal?.fit, 50)
globalThis.Terminal?.focus()
}
setInterval(() => globalThis.Terminal?.fit(), 200)
const isSmall = window.innerWidth <= 445
figlet.parseFont(figletFontName, figletFont)
figlet.text('web3os', { font: figletFontName }, async (err, logoFiglet) => {
if (err) log(err)
if (logoFiglet && globalThis.innerWidth >= 768) log(`\n${colors.green.bold(logoFiglet)}`)
else log(`\n${colors.green.bold(`${isSmall ? '' : '\t\t'}π web3os π`)}`)
console.log(`%cweb3os %c${rootPkgJson.version}`, `
font-family: "Lucida Console", Monaco, monospace;
font-size: 25px;
letter-spacing: 2px;
word-spacing: 2px;
color: #028550;
font-weight: 700;
font-style: normal;
font-variant: normal;
text-transform: none;`, null)
console.log('%chttps://github.com/web3os-org/kernel', 'font-size:14px;')
console.debug({ Kernel, Terminal, System })
for (const event of KernelEvents) {
events.on(event, payload => {
let data
switch (event) {
case 'ModuleLoaded':
data = payload.detail.modInfo.name
break
default:
data = payload
}
eventsProcessed++
console.debug('Kernel Event:', { event, data })
})
}
if (!bootArgs.has('nobootintro')) await printBootIntro()
await loadLocalStorage()
events.dispatch('MemoryLoaded', memory)
await setupFilesystem()
events.dispatch('FilesystemLoaded')
await registerKernelBins()
events.dispatch('KernelBinsLoaded')
await registerBuiltinModules()
events.dispatch('BuiltinModulesLoaded')
// Load builtin kernel FS modules into the kernel
for await (const [name, mod] of Object.entries(await fsModules({ BrowserFS, fs, execute, modules, t, utils }))) {
loadModule(mod, { name: `@web3os-fs/${name}` })
}
events.dispatch('FilesystemModulesLoaded')
await loadPackages()
events.dispatch('PackagesLoaded')
// Copy namespaced core modules onto root object
const web3osCoreNamespaces = ['@web3os-core', '@web3os-fs']
for (const mod of Object.values(modules)) {
const [namespace, name] = mod.name.split('/')
if (web3osCoreNamespaces.includes(namespace)) modules[name] = mod
}
// Check for notification permission and request if necessary
if (Notification?.permission === 'default') Notification.requestPermission()
if (Notification?.permission === 'denied') log(colors.warning(t('Notification permission denied')))
localStorage.setItem('web3os_first_boot_complete', 'true')
events.dispatch('AutostartStart')
await autostart()
events.dispatch('AutostartEnd')
if (modules.confetti) await execute('confetti --startVelocity 90 --particleCount 150')
topbar.hide()
const heartbeat = setInterval(() => navigator.vibrate([200, 50, 200]), 1000)
setTimeout(() => clearInterval(heartbeat), 5000)
// Expose this globally in case it needs to be cleared externally by an app
globalThis.web3osBlinkyTitleInterval = setInterval(() => {
document.title = document.title.includes('_') ? 'web3os# ' : 'web3os# _'
}, 600)
events.dispatch('BootComplete')
console.timeEnd('web3os:boot')
console.debug('Navigation Performance:', globalThis.performance?.getEntriesByType('navigation')?.[0]?.duration)
if (location.hostname !== 'localhost') analyticsEvent({ event: 'boot-complete' })
})
}
/**
* Wait for the specified number of milliseconds
* @async
* @param {number} ms - The number of milliseconds to wait
* @return {Module}
*/
export async function wait (ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
// Setup screensaver interval
let idleTimer
const resetIdleTime = () => {
clearTimeout(idleTimer)
if (!memory) return
const timeout = get('config', 'screensaver-timeout') || Config.screensaverTimeout
if (!modules?.screensaver || timeout <= 0) return
const saver = modules.screensaver.getSaver()
const startTime = modules.screensaver.getStartTime()
// Prevent screensaver from immediately exiting to overcome
// late enter-key event when entering command at terminal
if (Date.now() - startTime > 500) saver?.exit?.()
idleTimer = setTimeout(async () => {
modules.screensaver.run(globalThis.Terminal, get('config', 'screensaver') || 'matrix')
}, timeout)
}
// Activity listeners to reset idle time
globalThis.addEventListener('mousemove', resetIdleTime)
globalThis.addEventListener('keydown', resetIdleTime)
globalThis.addEventListener('keyup', resetIdleTime)
globalThis.addEventListener('keypress', resetIdleTime)
globalThis.addEventListener('pointerdown', resetIdleTime)
// Setup protocol handler
if (navigator.registerProtocolHandler) navigator.registerProtocolHandler('web+threeos', '?destination=%s')
// Handle PWA installability
globalThis.addEventListener('beforeinstallprompt', e => {
const installer = {
name: '@web3os-core/install',
description: t('kernel:bins.descriptions.install', 'Install web3os as a PWA'),
run: async () => {
const result = await e.prompt()
Terminal.log(result)
analyticsEvent({ event: 'pwa-install', details: result })
}
}
modules[installer.name] = installer
modules.install = installer
})
// Register service worker
if ('serviceWorker' in navigator && location.hostname !== 'localhost') {
globalThis.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
})
}