kernel.js

  1. /**
  2. * Web3os Kernel
  3. *
  4. * @description Entrypoint of the web3os kernel
  5. * @author Jay Mathis <code@mathis.network>
  6. * @license MIT
  7. * @see https://github.com/web3os-org/kernel
  8. */
  9. /* global Kernel, Terminal, System */
  10. /* global fetch, File, FileReader, localStorage, location, Notification */
  11. 'use strict'
  12. import rootPkgJson from '../package.json'
  13. import getConfig from './config'
  14. import AwesomeNotifications from 'awesome-notifications'
  15. import bytes from 'bytes'
  16. import colors from 'ansi-colors'
  17. import columnify from 'columnify'
  18. import CustomEvent from 'custom-event-js'
  19. import figlet from 'figlet'
  20. import figletFont from 'figlet/importable-fonts/Graffiti'
  21. import i18next from 'i18next'
  22. import i18nextBrowserLanguageDetector from 'i18next-browser-languagedetector'
  23. import iconify from '@iconify/iconify'
  24. import Keyboard from 'simple-keyboard'
  25. import path from 'path'
  26. import sweetalert from 'sweetalert2'
  27. import topbar from 'topbar'
  28. import { unzip } from 'unzipit'
  29. import { v4 as uuidv4 } from 'uuid'
  30. import 'systemjs'
  31. import 'animate.css'
  32. import 'awesome-notifications/dist/style.css'
  33. // import 'simple-keyboard/build/css/index.css'
  34. import './css/index.css'
  35. import './themes/default/index.css'
  36. import './themes/default/sweetalert2.css'
  37. import '@material/mwc-button'
  38. import '@material/mwc-icon-button'
  39. import '@material/mwc-snackbar'
  40. import AppWindow from './windows'
  41. import locales from './locales'
  42. import README from '../README.md'
  43. import theme from './themes/default/index.js'
  44. import W3OSTerminal from './terminal'
  45. import { fsModules } from './fs'
  46. let BrowserFS
  47. let memory
  48. const Config = getConfig()
  49. const figletFontName = 'Graffiti'
  50. console.time('web3os:boot')
  51. colors.theme(theme)
  52. globalThis.topbar = topbar
  53. globalThis.global = globalThis.global || globalThis
  54. export let eventsProcessed = 0
  55. export const poweronTime = new Date()
  56. export const bootArgs = new URLSearchParams(globalThis.location.search)
  57. export const isMobile = window.matchMedia('only screen and (max-width: 760px)').matches
  58. export const { name, version } = rootPkgJson
  59. /**
  60. * References the initialized i18next instance
  61. * @type {Object}
  62. * @see https://i18next.com
  63. */
  64. export const i18n = i18next
  65. const { t } = i18n
  66. i18n.loadAppLocales = locales => {
  67. for (const [key, data] of Object.entries(locales)) {
  68. i18n.addResourceBundle(key, 'app', data, true)
  69. if (data.common) i18n.addResources(key, 'common', data.common, true)
  70. }
  71. }
  72. export const Web3osTerminal = W3OSTerminal
  73. /**
  74. * Contains miscellaneous utilities
  75. * @todo Do this better
  76. * @type {Object}
  77. */
  78. export const utils = { bytes, colors, path, wait }
  79. /**
  80. * Colorize a string to differentiate numbers and letters
  81. * @param {string} str - The string to colorize
  82. * @param {Object=} options - Options for colorization
  83. * @param {Function=} [options.numbers=colors.blue()] - The function to colorize numbers
  84. * @param {Function=} [options.letters=colors.white()] - The function to colorize letters
  85. */
  86. utils.colorChars = (str, options = {}) => {
  87. if (typeof str !== 'string') throw new Error(t('You must provide a string to colorChars'))
  88. const numbers = options.numbers || colors.blue
  89. const letters = options.letters || colors.white
  90. return str.split('').map(c => isNaN(c) ? letters(c) : numbers(c)).join('')
  91. }
  92. /**
  93. * Contains all registered kernel modules
  94. * @type {Object}
  95. */
  96. export const modules = {}
  97. /**
  98. * Contains all registered kernel intervals
  99. * @type {Object}
  100. */
  101. export const intervals = {}
  102. /**
  103. * References the @iconify/iconify library
  104. */
  105. export const icons = iconify
  106. /**
  107. * Create an icon element
  108. *
  109. * @param {string} id - The @iconify/iconify icon identifier
  110. * @param {Array.<string>} classes - Array of additional classes to apply to the element
  111. * @returns {HTMLElement}
  112. */
  113. export const createIcon = (id, classes) => {
  114. const icon = document.createElement('i')
  115. icon.classList.add(...classes)
  116. icon.dataset.icon = id
  117. return icon
  118. }
  119. /**
  120. * Gives access to the virtual keyboard
  121. *
  122. * This gives us more control and consistency over mobile input
  123. *
  124. * @type {SimpleKeyboard}
  125. * @see https://virtual-keyboard.js.org
  126. */
  127. export let keyboard
  128. /**
  129. * Gives access to the BrowserFS API
  130. * @type {Object}
  131. */
  132. export let fs
  133. /**
  134. * The main kernel event bus
  135. *
  136. * @type {CustomEvent}
  137. *
  138. * @see https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent
  139. * @see https://www.npmjs.com/package/custom-event-js
  140. *
  141. * @property {Function} dispatch - Dispatch an event
  142. * @property {string} dispatch.eventName - The name of the event
  143. * @property {Object} dispatch.detail - The event detail payload
  144. * @property {Function} on - Subscribe to event
  145. * @property {string} on.eventName - The name of the event
  146. * @property {Function} on.callback - Execute when this event is triggered
  147. * @property {Function} off - Unsubscribe from event
  148. * @property {string} off.eventName - The name of the event
  149. * @property {Function} off.callback - Execute when this event is triggered
  150. */
  151. export const events = CustomEvent
  152. /**
  153. * The list of kernel events
  154. */
  155. export const KernelEvents = Config.kernelEvents
  156. /**
  157. * The primary Broadcast Channel for web3os
  158. *
  159. * @type {BroadcastChannel}
  160. * @see https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API
  161. */
  162. export const broadcast = new BroadcastChannel('web3os')
  163. broadcast.onmessage = msg => {
  164. console.log('Incoming Broadcast:', msg)
  165. }
  166. /**
  167. * Send an analytics event to the analytics endpoint
  168. */
  169. export async function analyticsEvent ({ event, user, details, severity }) {
  170. if (/^localhost/.test(location.host)) return
  171. if (get('config', 'analytics-opt-out')) return
  172. if (!user && !get('user', 'uuid')) set('user', 'uuid', uuidv4())
  173. user = user || get('user', 'uuid')
  174. const analyticsEndpoint = get('config', 'analytics-endpoint') || Config.analyticsEndpoint
  175. if (!analyticsEndpoint) return
  176. await fetch(analyticsEndpoint, {
  177. method: 'POST',
  178. headers: { 'Content-Type': 'application/json' },
  179. body: JSON.stringify({ event: event || 'log', severity: severity || 'INFO', user, details: details || {} })
  180. })
  181. }
  182. /**
  183. * Output system information
  184. */
  185. export async function printSystemInfo () {
  186. const isSmall = window.innerWidth <= 445
  187. let output = ''
  188. // if (navigator.deviceMemory) output += `\n${colors.info('RAM:')} >= ${colors.muted(navigator.deviceMemory + 'GB')}\n`
  189. if (navigator.userAgentData) {
  190. const { brand, version } = navigator.userAgentData.brands.slice(-1)?.[0]
  191. const browser = `${brand} v${version}`
  192. output += `${colors.info(`${t('Host')}: `)}\t${location.host}\n`
  193. output += `${colors.info(`${t('Platform')}:`)}\t${navigator.userAgentData.platform || t('Unknown')}\n`
  194. output += `${colors.info(`${t('Browser')}:`)}\t${browser}\n`
  195. }
  196. if (navigator.getBattery) {
  197. const batt = await navigator.getBattery()
  198. batt.addEventListener('chargingchange', () => {
  199. if (batt.charging && !get('config', 'battery-no-charge-confetti')) execute('confetti')
  200. })
  201. output += `${colors.info(`${t('Battery')}:`)}\t${batt.charging ? `${batt.level * 100}% ⚔` : `${batt.level * 100}% šŸ”‹`}\n`
  202. }
  203. if (navigator.storage?.estimate) {
  204. const storageDetails = await navigator.storage.estimate()
  205. const used = bytes(storageDetails.usage)
  206. const free = bytes(storageDetails.quota - storageDetails.usage)
  207. output += `${colors.info(`${t('Storage')}:`)}\t${used} ${isSmall ? '\n\t ' : '/'} ${free}\n`
  208. }
  209. if (console.memory) output += `${colors.info(t('Heap Limit') + ':')}\t${bytes(console.memory.jsHeapSizeLimit)}\n`
  210. if (navigator.hardwareConcurrency) output += `${colors.info(t('Cores') + ':')}\t\t${navigator.hardwareConcurrency}\n`
  211. if (typeof navigator.onLine === 'boolean') output += `${colors.info(t('Online') + ':')}\t\t${navigator.onLine ? t('Yes') : t('No')}\n`
  212. if (navigator.connection) output += `${colors.info(t('Downlink') + ':')}\t~${navigator.connection.downlink} Mbps\n`
  213. log(output)
  214. return output
  215. }
  216. /**
  217. * Output the boot introduction
  218. * */
  219. export async function printBootIntro () {
  220. const isSmall = window.innerWidth <= 445
  221. log(colors.info(`${t('kernel:bootIntroSubtitle', '\t https://web3os.sh')}`))
  222. log(colors.heading.success.bold(`\n${isSmall ? '' : '\t '} web3os kernel v${rootPkgJson.version} `))
  223. log(colors.warning(`${isSmall ? '' : '\t '}⚠ BETA ⚠\n`))
  224. await printSystemInfo()
  225. if (!localStorage.getItem('web3os_first_boot_complete')) {
  226. log(colors.danger(`\n⚠ ${t('kernel:firstBootWarning', 'The first boot will take the longest, please be patient!')} ⚠\n`))
  227. }
  228. if (navigator.userAgentData?.platform === 'Android' || navigator.userAgentData?.platform === 'iPhone' || window.innerWidth < 500 || window.innerHeight < 500) {
  229. log(colors.danger(`\n⚠ šŸ‰ ${t('kernel:mobileExperienceWarning', 'NOTE: The mobile experience is pretty wacky and experimental - proceed with caution!')} ⚠`))
  230. }
  231. log(colors.underline(t('A few examples') + ':'))
  232. log(colors.danger(`\n${t('typeVerb', 'Type')} ${colors.bold.underline(Terminal.createSpecialLink('web3os:execute:help', 'help'))} ${t('kernel:bootIntro.help', 'for help')}`))
  233. log(colors.gray(`${t('typeVerb', 'Type')} ${colors.bold.underline(Terminal.createSpecialLink('web3os:execute:docs', 'docs'))} ${t('kernel:bootIntro.docs', 'to open the documentation')}`))
  234. log(colors.info(`${t('typeVerb', 'Type')} ${colors.bold.underline(Terminal.createSpecialLink('web3os:execute:desktop', 'desktop'))} ${t('kernel:bootIntro.desktop', 'to launch the desktop')}`))
  235. // log(colors.primary(`${t('typeVerb', 'Type')} ${colors.bold.underline('wallet connect')} ${t('kernel:bootIntro.wallet', 'to connect your wallet')}`))
  236. 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')}`))
  237. // log(colors.warning(`${t('typeVerb', 'Type')} ${colors.bold.underline('lsmod')} ${t('kernel:bootIntro.lsmod', 'to list all kernel modules')}`))
  238. 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')}`))
  239. // log(colors.white(`${t('typeVerb', 'Type')} ${colors.bold.underline('repl')} ${t('kernel:bootIntro.repl', 'to run the interactive Javascript terminal')}`))
  240. log(colors.cyan(`${t('typeVerb', 'Type')} ${colors.bold.underline(Terminal.createSpecialLink('web3os:execute:confetti', 'confetti'))} ${t('kernel:bootIntro.confetti', 'to fire the confetti gun šŸŽ‰')}`))
  241. // log(colors.magenta(`${t('typeVerb', 'Type')} ${colors.bold.underline('minipaint')} ${t('kernel:bootIntro.minipaint', 'to draw Artā„¢ šŸŽØ')}`))
  242. if (bootArgs.get('source') !== 'pwa') {
  243. isSmall ? log('\n-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-') : log('\n-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-')
  244. 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')}`))
  245. isSmall ? log('-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-') : log('-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-')
  246. }
  247. log('\nhttps://docs.web3os.sh')
  248. log('https://github.com/web3os-org')
  249. log(colors.muted(`\n${t('Booting')}...`))
  250. }
  251. /**
  252. * Write the kernel memory to localStorage
  253. */
  254. export function updateLocalStorage () { localStorage.setItem('memory', JSON.stringify(memory)) }
  255. /**
  256. * Restore the kernel memory from localStorage
  257. */
  258. export function loadLocalStorage () {
  259. try {
  260. const storedMemory = localStorage.getItem('memory')
  261. memory = storedMemory ? JSON.parse(storedMemory) : { firstBootVersion: rootPkgJson.version }
  262. updateLocalStorage()
  263. } catch (err) {
  264. console.error(err)
  265. log(t('Failed to load memory from local storage'))
  266. memory = {}
  267. }
  268. console.assert(typeof memory === 'object', t('Failed to load memory from local storage'))
  269. }
  270. /**
  271. * Set a value in the kernel memory object
  272. * @param {!string} namespace - The namespace in which to set the value
  273. * @param {!string} key - The key in which to set the value
  274. * @param {!any} value - The value to set
  275. */
  276. export function set (namespace, key, value) {
  277. if (!namespace || namespace === '') throw new Error(t('Invalid namespace'))
  278. if (!key || key === '') throw new Error(t('Invalid key'))
  279. if (!value || value === '') throw new Error(t('Invalid value'))
  280. memory[namespace] = memory[namespace] || {}
  281. memory[namespace][key] = value
  282. updateLocalStorage()
  283. return memory[namespace]?.[key]
  284. }
  285. /**
  286. * Get a value from the kernel memory object
  287. * @param {!string} namespace - The namespace from which to get the value
  288. * @param {?string} key - The key to retrieve, or undefined to get entire namespace
  289. */
  290. export function get (namespace, key) {
  291. if (!key) return memory[namespace] || null
  292. return memory[namespace]?.[key] || null
  293. }
  294. /**
  295. * Dump the kernel memory to a JSON object
  296. * @returns {string} dump The memory dump
  297. */
  298. export function dump () { return JSON.stringify(memory) }
  299. /**
  300. * Restore the kernel memory from a JSON object
  301. * @param {!string} json - The JSON object generated from dump()
  302. */
  303. export function restore (json) {
  304. memory = JSON.parse(json)
  305. updateLocalStorage()
  306. return memory
  307. }
  308. /**
  309. * Delete the specified key from the kernel memory object
  310. * @param {!string} namespace - The namespace containing the key
  311. * @param {!key} key - The key to delete
  312. */
  313. export function deleteKey (namespace, key) {
  314. if (!memory[namespace]?.[key]) throw new Error(t('Invalid namespace or key'))
  315. delete memory[namespace][key]
  316. updateLocalStorage()
  317. }
  318. /**
  319. * Delete the specified namespace from the kernel memory object
  320. * @param {!namespace} namespace - The namespace to delete
  321. */
  322. export function deleteNamespace (namespace) {
  323. if (!memory[namespace]) throw new Error(t('Invalid namespace'))
  324. delete memory[namespace]
  325. updateLocalStorage()
  326. }
  327. /**
  328. * Log a message to the terminal
  329. * @param {!any} message - The message or object to log
  330. * @param {?Object} options - Logging options
  331. * @param {Boolean=} [options.console=true] - Enable logging to both browser console and Terminal
  332. * @param {?Web3osTerminal} options.terminal - The terminal to attach to, or undefined for global Terminal
  333. */
  334. export function log (message, options = { console: true }) {
  335. if (!message) return
  336. message = message.replace(/\\n/gm, '\n')
  337. if (options.console) console.log(message)
  338. const term = options.terminal || globalThis.Terminal
  339. term.log(message)
  340. }
  341. /**
  342. * Manages interactions with the windowing system
  343. * @type Object
  344. * @property {Set} _collection - The collection of windows
  345. *
  346. * @property {Function} create - Create a new window
  347. * @property {Object} create.options - Options to pass to WinBox
  348. *
  349. * @property {Function} find - Find a window by ID
  350. * @property {string} find.id - The ID of the window (usually winbox-#; stored on app.window.id)
  351. *
  352. * @see https://nextapps-de.github.io/winbox/
  353. */
  354. export const windows = {
  355. _collection: new Set(),
  356. create: options => {
  357. const app = new AppWindow(options)
  358. windows._collection.add(app)
  359. return app
  360. },
  361. find: id => {
  362. return Array.from(windows._collection.values()).find(app => app.window.id === id)
  363. }
  364. }
  365. /**
  366. * Show a SweetAlert dialog
  367. * @async
  368. * @param {Object} options - SweetAlert2 options
  369. * @returns {SweetAlertDialog}
  370. */
  371. export async function dialog (options = {}) {
  372. return sweetalert.fire({
  373. heightAuto: false,
  374. denyButtonColor: 'red',
  375. confirmButtonColor: 'green',
  376. customClass: { container: 'web3os-kernel-dialog-container' },
  377. ...options
  378. })
  379. }
  380. /**
  381. * Execute a command
  382. * @async
  383. * @param {string} cmd - The command to execute
  384. * @returns {CommandResult}
  385. */
  386. export async function execute (cmd, options = {}) {
  387. const exec = cmd.split(' ')[0]
  388. const term = options.terminal || globalThis.Terminal
  389. let command = term.aliases[exec] ? modules[term.aliases[exec]] : modules[exec]
  390. if (options.topbar) topbar.show()
  391. if (!command) {
  392. try {
  393. if (fs.existsSync(exec)) {
  394. const data = JSON.parse(fs.readFileSync(exec).toString())
  395. command = modules[data?.name]
  396. } else {
  397. command = await import(`./modules/${exec}`)
  398. }
  399. } catch (err) {
  400. console.error(err)
  401. }
  402. }
  403. options.doPrompt = options.doPrompt || false
  404. if (options.topbar) topbar.hide()
  405. if (!command?.run) {
  406. 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}`))}`))
  407. navigator.vibrate([200, 50, 200])
  408. return term.prompt()
  409. }
  410. try {
  411. if (options.topbar) topbar.show()
  412. const args = cmd.split(' ').slice(1).join(' ')
  413. const result = await command.run(term, args)
  414. if (options.topbar) topbar.hide()
  415. if (options.doPrompt) term.prompt()
  416. return result
  417. } catch (err) {
  418. navigator.vibrate([200, 50, 200])
  419. console.error(command, err)
  420. if (err) term.log(err.message || 'An unknown error occurred')
  421. if (options.doPrompt) term.prompt()
  422. throw err
  423. }
  424. }
  425. /**
  426. * Execute a script file
  427. * @async
  428. * @param {string} path - The path of the script
  429. * @returns {SweetAlertDialog}
  430. */
  431. export async function executeScript (filename, options = {}) {
  432. const term = options.terminal || globalThis.Terminal
  433. if (!filename || filename === '') return term.log(colors.danger('Invalid filename'))
  434. filename = utils.path.resolve(term.cwd, filename)
  435. const value = fs.readFileSync(filename, 'utf8')
  436. const commands = value.split('\n').map(c => c.trim())
  437. for (const cmd of commands) {
  438. if (cmd?.trim().length > 0 && cmd?.[0] !== '#' && cmd?.substr(0, 2) !== '//') await execute(cmd, { terminal: term, doPrompt: false })
  439. }
  440. }
  441. /**
  442. * Execute the /config/autostart.sh script
  443. * @async
  444. * @param {string=} defaultAutoStart - Write autostart script with this content if it doesn't exist
  445. */
  446. export async function autostart (defaultAutoStart) {
  447. try {
  448. if (defaultAutoStart && !fs.existsSync('/config/autostart.sh')) {
  449. fs.writeFileSync('/config/autostart.sh', defaultAutoStart) // Setup default autostart.sh
  450. }
  451. if (fs.existsSync('/config/autostart.sh')) await executeScript('/config/autostart.sh')
  452. } catch (err) {
  453. console.error(err)
  454. log(colors.danger('Failed to complete autostart script'))
  455. } finally {
  456. globalThis.Terminal.prompt()
  457. }
  458. }
  459. /**
  460. * Send an awesome-notification
  461. * @type {AwesomeNotifications}
  462. * @todo awesome-notifications will likely eventually replace sweetalert
  463. * @see https://f3oall.github.io/awesome-notifications
  464. */
  465. export const notify = new AwesomeNotifications({
  466. position: 'top-right',
  467. icons: {
  468. enabled: true,
  469. prefix: '<i class="iconify" data-icon="fa6-regular:',
  470. suffix: '" />'
  471. }
  472. })
  473. /**
  474. * Send a browser/platform notification
  475. * @async
  476. * @param {Object=} options - The notification options (Notification API)
  477. * @see https://developer.mozilla.org/en-US/docs/Web/API/notification
  478. */
  479. export async function systemNotify (options = {}) {
  480. if (Notification.permission !== 'granted') throw new Error('Notification permission denied')
  481. try {
  482. const notification = new Notification(options.title, options)
  483. } catch (err) {
  484. console.warn(err)
  485. navigator.serviceWorker.ready.then(registration => {
  486. registration.showNotification('Notification with ServiceWorker')
  487. })
  488. }
  489. }
  490. /**
  491. * Show a snackbar notification
  492. *
  493. * @deprecated
  494. * This can still be useful I think, but it's being deprecated
  495. * in favor of awesome-notifications. Should this be removed?
  496. *
  497. * @async
  498. * @param {Object=} options - The snackbar options
  499. * @see https://www.npmjs.com/package/@material/mwc-snackbar
  500. */
  501. export async function snackbar (options = {}) {
  502. const snack = document.createElement('mwc-snackbar')
  503. snack.id = options.id || 'snack-' + Math.random()
  504. snack.leading = options.leading || false
  505. snack.closeOnEscape || false
  506. snack.labelText = options.labelText || ''
  507. snack.stacked = options.stacked || false
  508. const closeButton = document.createElement('mwc-icon-button')
  509. closeButton.icon = 'close'
  510. closeButton.slot = 'dismiss'
  511. snack.appendChild(closeButton)
  512. document.body.appendChild(snack)
  513. snack.show()
  514. }
  515. /**
  516. * Sets up the filesystem
  517. *
  518. * These parameters can also be provided as a query parameter:
  519. *
  520. * https://web3os.sh/?initfsUrl=https://my.place.com/initfs.zip
  521. *
  522. * Bare minimum, temporary, fs:
  523. * https://web3os.sh/?mountableFilesystemConfig={ "/": { "fs": "InMemory" }, "/bin": { "fs": "InMemory" } }
  524. *
  525. * @async
  526. * @param {string=} initfsUrl - URL to a zipped filesystem snapshot
  527. * @param {MountableFileSystemOptions=} mountableFilesystemConfig - Filesystem configuration object for BrowserFS
  528. * @see https://jvilk.com/browserfs/2.0.0-beta/index.html
  529. */
  530. export async function setupFilesystem (initfsUrl, mountableFilesystemConfig) {
  531. return new Promise(async (resolve, reject) => {
  532. const browserfs = await import('browserfs')
  533. const filesystem = {}
  534. let initfs
  535. initfsUrl = initfsUrl || bootArgs.get('initfsUrl')
  536. mountableFilesystemConfig = bootArgs.get('mountableFilesystemConfig')
  537. ? JSON.parse(bootArgs.get('mountableFilesystemConfig')) : Config.defaultFilesystemOverlayConfig
  538. if (bootArgs.has('initfsUrl')) {
  539. try {
  540. const result = await dialog({
  541. title: 'Use initfs?',
  542. icon: 'warning',
  543. allowOutsideClick: false,
  544. allowEscapeKey: false,
  545. showDenyButton: true,
  546. showLoaderOnConfirm: true,
  547. focusDeny: true,
  548. confirmButtonText: 'Yes',
  549. html: `
  550. <p>Do you want to overwrite existing files in your filesystem with the initfs located at:</p>
  551. <h4><a href="${initfsUrl}" target="_blank">${initfsUrl}</a></h4>
  552. <p><strong>Be sure you trust the source!</strong></p>
  553. `,
  554. preConfirm: async () => {
  555. try {
  556. return await unzip(initfsUrl)
  557. } catch (err) {
  558. console.error(err)
  559. globalThis.Terminal?.log(colors.danger(`Failed to unzip initfsUrl at ${initfsUrl}`))
  560. return true
  561. }
  562. }
  563. })
  564. if (result.isDenied) throw new Error('User rejected using initfs')
  565. const { entries } = result.value
  566. initfs = entries
  567. globalThis.history.replaceState(null, null, '/') // prevent reload with initfs
  568. } catch (err) {
  569. globalThis.Terminal?.log(colors.danger('Failed to unzip initfsUrl ' + initfsUrl))
  570. globalThis.Terminal?.log(colors.danger(err.message))
  571. console.error(err)
  572. }
  573. }
  574. /**
  575. * Add HTML5FS
  576. * @todo Broken on Firefox
  577. * @see: webkitStorageOptions
  578. */
  579. if (!navigator.userAgent.includes('Firefox')) {
  580. mountableFilesystemConfig['/mount/html5fs'] = {
  581. fs: 'AsyncMirror',
  582. options: {
  583. sync: { fs: 'InMemory' },
  584. async: {
  585. fs: 'HTML5FS',
  586. options: {}
  587. }
  588. }
  589. }
  590. }
  591. browserfs.install(filesystem)
  592. browserfs.configure({
  593. fs: 'MountableFileSystem',
  594. options: mountableFilesystemConfig
  595. }, err => {
  596. if (err) {
  597. console.error(err)
  598. log(colors.danger(`Failed to initialize filesystem: ${err.message}`))
  599. } else {
  600. BrowserFS = filesystem
  601. fs = filesystem.require('fs')
  602. // Use an initfs if available
  603. if (initfs) {
  604. Object.entries(initfs).forEach(async ([name, entry]) => {
  605. const filepath = utils.path.join('/', name)
  606. if (entry.isDirectory) !fs.existsSync(filepath) && fs.mkdirSync(utils.path.join('/', name))
  607. else {
  608. const parentDir = utils.path.parse(filepath).dir
  609. if (!fs.existsSync(parentDir)) fs.mkdirSync(parentDir)
  610. fs.writeFileSync(filepath, BrowserFS.Buffer.from(await entry.arrayBuffer()))
  611. }
  612. })
  613. }
  614. // Prepare required paths for packages
  615. const defaultPackages = bootArgs.get('defaultPackages') || Config.defaultPackages || []
  616. if (!fs.existsSync('/var')) fs.mkdirSync('/var')
  617. if (!fs.existsSync('/var/packages')) fs.mkdirSync('/var/packages')
  618. if (!fs.existsSync('/config')) fs.mkdirSync('/config')
  619. if (!fs.existsSync('/config/packages')) fs.writeFileSync('/config/packages', JSON.stringify(bootArgs.has('noDefaultPackages') ? [] : defaultPackages))
  620. // Populate initial procfs
  621. // TODO: Make procfs separate module and self-updating
  622. fs.writeFileSync('/proc/host', location.host)
  623. fs.writeFileSync('/proc/version', rootPkgJson.version)
  624. fs.writeFileSync('/proc/platform', navigator.userAgentData.platform)
  625. fs.writeFileSync('/proc/querystring', location.search)
  626. fs.writeFileSync('/proc/language', navigator.language)
  627. fs.writeFileSync('/proc/user-agent', navigator.userAgent)
  628. fs.writeFileSync('/proc/user-agent.json', JSON.stringify(navigator.userAgentData, null, 2))
  629. try {
  630. const { downlink, effectiveType, rtt, saveData } = navigator.connection
  631. fs.writeFileSync('/proc/connection', JSON.stringify({ downlink, effectiveType, rtt, saveData }, null, 2))
  632. } catch {}
  633. // Drag and drop on terminal
  634. // const dragenter = e => { e.stopPropagation(); e.preventDefault() }
  635. // const dragover = e => { e.stopPropagation(); e.preventDefault() }
  636. // const drop = e => {
  637. // e.stopPropagation()
  638. // e.preventDefault()
  639. // const dt = e.dataTransfer
  640. // const files = dt.files
  641. // for (const file of files) {
  642. // const reader = new FileReader()
  643. // reader.readAsArrayBuffer(file)
  644. // reader.onload = () => {
  645. // const buffer = BrowserFS.Buffer.from(reader.result)
  646. // const filepath = utils.path.resolve(Terminal.cwd, file.name)
  647. // fs.writeFileSync(filepath, buffer)
  648. // snackbar({ labelText: `Uploaded ${filepath}` })
  649. // }
  650. // }
  651. // }
  652. // Terminal.addEventListener('dragenter', dragenter)
  653. // Terminal.addEventListener('dragover', dragover)
  654. // Terminal.addEventListener('drop', drop)
  655. resolve(fs)
  656. }
  657. })
  658. })
  659. }
  660. /**
  661. * Load the kernel's core internal commands
  662. *
  663. * @todo These are here because they're relatively simple, but many of them should be
  664. * moved to their own respective external modules.
  665. *
  666. * @async
  667. */
  668. async function registerKernelBins () {
  669. const { t } = Kernel.i18n
  670. const kernelBins = {}
  671. kernelBins.alert = { description: t('kernel:bins.descriptions.alert', 'Show an alert'), run: (term, context) => dialog({ text: context }) }
  672. kernelBins.clear = { description: t('kernel:bins.descriptions.clear', 'Clear the terminal'), run: term => term.clear() }
  673. 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()))}
  674. kernelBins.docs = { description: t('kernel:bins.descriptions.docs', 'Open the documentation'), run: term => { modules.www.run(term, '--title "Web3os Documentation" --no-toolbar /docs') }}
  675. kernelBins.dump = { description: t('kernel:bins.descriptions.dump', 'Dump the memory state'), run: term => term.log(dump()) }
  676. kernelBins.echo = { description: t('kernel:bins.descriptions.echo', 'Echo some text to the terminal'), run: (term, context) => term.log(context) }
  677. kernelBins.history = { description: t('kernel:bins.descriptions.history', 'Show command history'), run: term => { return term.log(JSON.stringify(term.history)) } }
  678. kernelBins.import = { description: t('kernel:bins.descriptions.import', 'Import a module from a URL'), run: async (term, context) => await importModuleUrl(context) }
  679. kernelBins.man = { description: t('kernel:bins.descriptions.man', 'Alias of help'), run: (term, context) => modules.help.run(term, context) }
  680. kernelBins.sh = { description: t('kernel:bins.descriptions.sh', 'Execute a web3os script'), run: (term, context) => executeScript(context, { terminal: term }) }
  681. kernelBins.systeminfo = { description: t('kernel:bins.descriptions.systeminfo', 'Print system information'), run: async () => await printSystemInfo() }
  682. 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] }) }
  683. kernelBins.reboot = { description: t('kernel:bins.descriptions.reboot', 'Reload web3os'), run: () => location.reload() }
  684. kernelBins.restore = { description: t('kernel:bins.descriptions.restore', 'Restore the memory state'), run: (term, context) => restore(context) }
  685. kernelBins.snackbar = { description: t('kernel:bins.descriptions.snackbar', 'Show a snackbar; e.g. snackbar Alert!'), run: (term, context) => snackbar({ labelText: context }) }
  686. kernelBins.wait = { description: t('kernel:bins.descriptions.wait', 'Wait for the specified number of milliseconds'), run: (term, context) => wait(context) }
  687. kernelBins.alias = {
  688. description: t('kernel:bins.descriptions.alias', 'Set or list command aliases'),
  689. help: `${t('Usage')}: alias [src] [dest]`,
  690. run: (term, context) => {
  691. if (!context || context === '') return term.log(term.aliases)
  692. const command = context.split(' ')
  693. if (command.length !== 2) throw new Error('You must specify the src and dest commands')
  694. term.aliases[command[0]] = command[1]
  695. }
  696. }
  697. kernelBins.ipecho = {
  698. description: t('kernel:bins.descriptions.ipecho', 'Echo your public IP address'),
  699. run: async (term = globalThis.Terminal) => {
  700. const endpoint = get('config', 'ipecho-endpoint') || Config.ipechoEndpoint || 'https://ipecho.net/plain'
  701. const result = await fetch(endpoint)
  702. const ip = await result.text()
  703. console.log({ ip })
  704. term.log(ip)
  705. return ip
  706. }
  707. }
  708. kernelBins.lsmod = {
  709. description: t('kernel:bins.descriptions.lsmod', 'List loaded kernel modules'),
  710. run: async (term = globalThis.Terminal) => {
  711. let mods = { ...modules }
  712. // This is useless right now, but in the future it may help us transition to other environments:
  713. if (module?.exports) mods = { ...mods, ...module.exports }
  714. const sortedMods = Object.keys(mods).sort()
  715. term.log(sortedMods)
  716. return sortedMods
  717. }
  718. }
  719. kernelBins.memoryinfo = {
  720. description: `${t('kernel:bins.descriptions.memoryinfo', 'Show Javascript heap information')}`,
  721. run: async (term = globalThis.Terminal) => {
  722. const { jsHeapSizeLimit, totalJSHeapSize, usedJSHeapSize } = console.memory
  723. const meminfo = {
  724. jsHeapSizeLimit: bytes(jsHeapSizeLimit),
  725. totalJSHeapSize: bytes(totalJSHeapSize),
  726. usedJSHeapSize: bytes(usedJSHeapSize)
  727. }
  728. term.log(meminfo)
  729. return meminfo
  730. }
  731. }
  732. kernelBins.open = {
  733. description: `${t('kernel:bins.descriptions.open', 'Open a shortcut defined in /config/shortcuts')}`,
  734. run: async (term = globalThis.Terminal, context) => {
  735. if (!fs.existsSync('/config/shortcuts')) throw new Error('/config/shortcuts does not exist')
  736. const shortcuts = JSON.parse(fs.readFileSync('/config/shortcuts', 'utf8'))
  737. if (!context || context === '') return term.log(JSON.stringify(shortcuts, null, 2))
  738. if (!shortcuts[context]) throw new Error('Invalid shortcut')
  739. const shortcut = shortcuts[context]
  740. switch (shortcut.type) {
  741. case 'execute':
  742. const cmds = shortcut.target.split(';').map(cmd => cmd.trim())
  743. for await (const cmd of cmds) term.kernel.execute(cmd)
  744. break
  745. case 'url':
  746. return window.open(shortcut.target, '_blank')
  747. }
  748. }
  749. }
  750. kernelBins.storageinfo = {
  751. description: t('kernel:bins.descriptions.storage', 'Display storage usage information'),
  752. run: async (term = globalThis.Terminal) => {
  753. const rawData = await navigator.storage.estimate()
  754. const data = {
  755. quota: bytes(rawData.quota),
  756. usage: bytes(rawData.usage),
  757. usageDetails: {}
  758. }
  759. Object.entries(rawData.usageDetails).forEach(entry => {
  760. data.usageDetails[entry[0]] = bytes(entry[1])
  761. })
  762. term.log(data)
  763. return data
  764. }
  765. }
  766. kernelBins.set = {
  767. description: t('kernel:bins.descriptions.set', 'Set a kernel memory value'),
  768. help: `${t('Usage')}: set <namespace> <key> <value>`,
  769. run: (term, context = '') => {
  770. const parts = context.split(' ')
  771. const namespace = parts[0]
  772. const key = parts[1]
  773. const value = parts.slice(2, parts.length).join(' ')
  774. term.log(set(namespace, key, value))
  775. }
  776. }
  777. kernelBins.get = {
  778. description: t('kernel:bins.descriptions.get', 'Get a kernel memory namespace or key'),
  779. help: `${t('Usage')}: get <namespace> [key]`,
  780. run: (term, context = '') => {
  781. const parts = context.split(' ')
  782. const namespace = parts[0]
  783. const key = parts[1]
  784. const result = get(namespace, key)
  785. term.log(typeof result === 'string' ? result : JSON.stringify(result))
  786. }
  787. }
  788. kernelBins.unset = {
  789. description: t('kernel:bins.descriptions.unset', 'Delete specified memory namespace or key'),
  790. help: `${t('Usage')}: unset <namespace> [key]`,
  791. run: (term, context = '') => {
  792. try {
  793. const parts = context.split(' ')
  794. if (parts[1]) deleteKey(parts[0], parts[1])
  795. else deleteNamespace(parts[0])
  796. } catch (err) {
  797. console.error(err)
  798. term.log(colors.danger(err.message))
  799. }
  800. }
  801. }
  802. kernelBins.eval = {
  803. description: t('kernel:bins.descriptions.eval', 'Load and evaluate a Javascript file'),
  804. run: (term, context) => {
  805. if (!context || context === '') return term.log(colors.danger('Invalid filename'))
  806. const filename = utils.path.resolve(term.cwd, context)
  807. if (fs.existsSync(filename)) {
  808. const code = fs.readFileSync(filename, 'utf-8')
  809. eval(code) // eslint-disable-line
  810. } else {
  811. eval(context)
  812. }
  813. }
  814. }
  815. kernelBins.clip = {
  816. description: t('kernel:bins.descriptions.clip', 'Copy return value of command to clipboard'),
  817. help: `${t('Usage')}: clip <command>`,
  818. run: async (term, context) => {
  819. if (!context || context === '') return
  820. const parts = context.split(' ')
  821. const mod = modules[parts[0]]
  822. let result = await mod.run(term, parts.splice(1).join(' '))
  823. if (Array.isArray(result) && result.length === 1) result = result[0]
  824. return await navigator.clipboard.writeText(typeof result === 'string' ? result : JSON.stringify(result, null, 2))
  825. }
  826. }
  827. kernelBins.height = {
  828. description: t('kernel:bins.descriptions.height', 'Set body height'),
  829. run: (term, context) => { document.body.style.height = context }
  830. }
  831. kernelBins.width = {
  832. description: t('kernel:bins.descriptions.width', 'Set body width'),
  833. run: (term, context) => { document.body.style.width = context }
  834. }
  835. kernelBins.objectUrl = {
  836. description: t('kernel:bins.descriptions.objectUrl', 'Create an ObjectURL for a file'),
  837. run: (term, filename) => {
  838. const { t } = Kernel.i18n
  839. if (!filename || filename === '') throw new Error(t('invalidFilename', 'Invalid filename'))
  840. const data = fs.readFileSync(utils.path.join(term.cwd, filename))
  841. const file = new File([data], utils.path.parse(filename).base, { type: 'application/octet-stream' })
  842. const url = URL.createObjectURL(file)
  843. term.log(url)
  844. return url
  845. }
  846. }
  847. kernelBins.geo = {
  848. description: t('kernel:bins.descriptions.geo', 'Geolocation Utility'),
  849. run: async (term = globalThis.Terminal) => {
  850. if (!navigator.geolocation) throw new Error(t('kernel:bins.errors.geo.geolocationUnavailable', 'Geolocation is not available'))
  851. return new Promise((resolve, reject) => {
  852. try {
  853. navigator.geolocation.getCurrentPosition(pos => {
  854. const { latitude, longitude } = pos.coords
  855. const link = `https://www.openstreetmap.org/search?query=${latitude}%2C${longitude}`
  856. term.log({ latitude, longitude, link, pos })
  857. resolve({ latitude, longitude, link, pos })
  858. })
  859. } catch (err) {
  860. console.error(err)
  861. if (err.message) term.log(colors.danger(err.message))
  862. reject(err)
  863. }
  864. })
  865. }
  866. }
  867. kernelBins.eyedropper = {
  868. description: t('kernel:bins.descriptions.eyeDropper', 'Pick colors using the eyedropper'),
  869. run: async (term = globalThis.Terminal) => {
  870. const dropper = new EyeDropper()
  871. const color = await dropper.open()
  872. term.log(color)
  873. return color
  874. }
  875. }
  876. for (const [name, mod] of Object.entries(kernelBins)) {
  877. loadModule(mod, { name: `@web3os-core/${name}` })
  878. }
  879. }
  880. /**
  881. * Load the kernel's core external modules
  882. * @async
  883. */
  884. async function registerBuiltinModules () {
  885. const mods = process.env.BUILTIN_MODULES ? process.env.BUILTIN_MODULES.split(',') : (Config.builtinModules || [])
  886. for (const mod of mods) {
  887. try {
  888. const modBin = await import(`./modules/${mod}`)
  889. await loadModule(modBin, { name: `@web3os-core/${mod}` })
  890. } catch (err) {
  891. console.error(err)
  892. Terminal.log(colors.danger(`Error loading module: ${mod}`))
  893. Terminal.log(err.message)
  894. }
  895. }
  896. }
  897. /**
  898. * Load a module into the kernel
  899. * @async
  900. * @param {!ModInfo} mod - The mod to load
  901. * @param {Object=} options - Options for loading the module
  902. */
  903. export async function loadModule (mod, options = {}) {
  904. const { t } = Kernel.i18n
  905. if (!mod) throw new Error(`${t('kernel:invalidModule', 'Invalid module provided to')} kernel.loadModule`)
  906. let { description, help, name, run, version, pkgJson } = options
  907. description = description || mod.description
  908. version = version || pkgJson?.version || mod.version
  909. help = help || mod.help || t('kernel:helpNotExported', 'Help is not exported from this module')
  910. name = name || mod.name || 'module_' + Math.random().toString(36).slice(2)
  911. run = run || mod.run || mod.default
  912. if (!modules[name]) modules[name] = {}
  913. modules[name] = { ...modules[name], ...mod, run, name, version, description, help }
  914. const web3osData = pkgJson?.web3osData
  915. const modInfo = {
  916. name,
  917. version,
  918. description,
  919. web3osData,
  920. help
  921. }
  922. if (run) {
  923. let modBin
  924. if (name.includes('/')) {
  925. modBin = utils.path.join('/bin', name.split('/')[0], name.split('/')[1])
  926. if (!fs.existsSync(`/bin/${name.split('/')[0]}`)) fs.mkdirSync(`/bin/${name.split('/')[0]}`)
  927. } else {
  928. modBin = utils.path.join('/bin', name)
  929. }
  930. fs.writeFileSync(modBin, JSON.stringify(modInfo, null, 2))
  931. }
  932. events.dispatch('ModuleLoaded', { modInfo })
  933. }
  934. /**
  935. * Directly import an ES module from a URL
  936. * @async
  937. * @param {string} url - The URL of the module to import
  938. * @return {Module}
  939. */
  940. export async function importModuleUrl (url) {
  941. return await import(/* webpackIgnore: true */ url)
  942. }
  943. /**
  944. * Directly import a UMD module from a URL
  945. * @async
  946. * @param {string} url - The URL of the module to import
  947. * @return {Module}
  948. */
  949. export async function importUMDModule (url, name, module = { exports: {} }) {
  950. // Dark magic stolen from a lost tome of stackoverflow
  951. const mod = (Function('module', 'exports', await (await fetch(url)).text())
  952. .call(module, module, module.exports), module).exports
  953. mod.default = mod.default || mod
  954. return mod
  955. }
  956. /**
  957. * Load or install the packages defined in /config/packages
  958. * @async
  959. */
  960. export async function loadPackages () {
  961. const tasks = []
  962. const packages = JSON.parse(fs.readFileSync('/config/packages').toString())
  963. for (const pkg of packages) {
  964. tasks.push(new Promise(async (resolve, reject) => {
  965. try {
  966. if (/^(http|ftp).*\:/i.test(pkg)) {
  967. if (modules?.['3pm']) {
  968. await modules['3pm'].install(pkg, { warn: false })
  969. } else {
  970. const waitFor3pm = async () => {
  971. if (!modules?.['3pm']) return setTimeout(waitFor3pm, 500)
  972. await modules['3pm'].install(pkg, { warn: false })
  973. }
  974. await waitFor3pm()
  975. }
  976. return resolve()
  977. }
  978. const pkgJson = JSON.parse(fs.readFileSync(`/var/packages/${pkg}/package.json`))
  979. const main = pkgJson.web3osData.main || pkgJson.main || 'index.js'
  980. const type = pkgJson.web3osData.type || 'es'
  981. const mainUrl = `${pkgJson.web3osData.url}/${main}`
  982. const mod = type === 'umd'
  983. ? await importUMDModule(mainUrl)
  984. : await importModuleUrl(mainUrl)
  985. await loadModule(mod, pkgJson)
  986. resolve()
  987. } catch (err) {
  988. console.error(err)
  989. globalThis.Terminal.log(colors.danger(err.message))
  990. reject(err)
  991. }
  992. }))
  993. }
  994. await Promise.all(tasks)
  995. }
  996. /**
  997. * Show the boot splash screen
  998. * @async
  999. * @todo Make this more customizable
  1000. * @param {string} msg - The message to display
  1001. * @param {Object=} options - The splash screen options
  1002. */
  1003. export async function showSplash (msg, options = {}) {
  1004. const { t } = Kernel.i18n
  1005. document.querySelector('#web3os-splash')?.remove()
  1006. // TODO: Migrating everything to iconify
  1007. const icon = document.createElement('mwc-icon')
  1008. icon.id = 'web3os-splash-icon'
  1009. icon.style.color = options.iconColor || '#03A062'
  1010. icon.style.fontSize = options.iconFontSize || '10em'
  1011. icon.style.marginTop = '2rem'
  1012. icon.innerText = options.icon || 'hourglass_empty'
  1013. if (!options.disableAnimation) icon.classList.add('animate__animated', 'animate__zoomIn')
  1014. const title = document.createElement('h1')
  1015. title.id = 'web3os-splash-title'
  1016. title.innerHTML = options.title || 'web3os'
  1017. title.style.color = options.titleColor || 'white'
  1018. title.style.margin = 0
  1019. title.style.fontSize = options.titleFontSize || 'clamp(0.5rem, 6rem, 7rem)'
  1020. title.style.fontVariant = 'small-caps'
  1021. title.style.textShadow = '4px 4px 4px #888'
  1022. if (!options.disableAnimation) title.classList.add('animate__animated', 'animate__zoomIn')
  1023. const subtitle = document.createElement('h2')
  1024. subtitle.id = 'web3os-splash-subtitle'
  1025. subtitle.innerHTML = options.subtitle || t('kernel:bootIntroSubtitle', 'Made with <span class="heart">♄</span> by Jay Mathis')
  1026. subtitle.style.margin = 0
  1027. subtitle.style.color = options.subtitleColor || '#ccc'
  1028. subtitle.style.fontStyle = options.subtitleFontStyle || 'italic'
  1029. if (subtitle.querySelector('span.heart')) {
  1030. subtitle.querySelector('span.heart').style.color = 'red'
  1031. subtitle.querySelector('span.heart').style.fontSize = '1.5em'
  1032. }
  1033. if (!options.disableAnimation) subtitle.classList.add('animate__animated', 'animate__zoomInDown') && subtitle.style.setProperty('--animate-delay', '0.5s')
  1034. const background = document.createElement('div')
  1035. background.id = 'web3os-splash-background'
  1036. background.style.backgroundColor = '#121212'
  1037. background.style.position = 'absolute'
  1038. background.style.top = 0
  1039. background.style.left = 0
  1040. background.style.width = '100vw'
  1041. background.style.height = '100vh'
  1042. background.style.zIndex = 100001
  1043. const message = document.createElement('h3')
  1044. message.id = 'web3os-splash-message'
  1045. message.style.color = 'silver'
  1046. message.style.fontSize = '2.5rem'
  1047. message.textContent = msg || `šŸ’¾ ${t('Booting', 'Booting')}... šŸ’¾`
  1048. const versionInfo = document.createElement('h4')
  1049. versionInfo.id = 'web3os-splash-version'
  1050. versionInfo.style.color = '#333'
  1051. versionInfo.style.position = 'fixed'
  1052. versionInfo.style.bottom = '0.5rem'
  1053. versionInfo.style.right = '1rem'
  1054. versionInfo.textContent = `v${rootPkgJson.version}`
  1055. const container = document.createElement('div')
  1056. container.id = 'web3os-splash'
  1057. container.style.display = 'flex'
  1058. container.style.position = 'absolute'
  1059. container.style.top = 0
  1060. container.style.left = 0
  1061. container.style.margin = 0
  1062. container.style.flexDirection = 'column'
  1063. container.style.justifyContent = 'center'
  1064. container.style.alignItems = 'center'
  1065. container.style.height = '100vh'
  1066. container.style.width = '100vw'
  1067. container.style.zIndex = 100002
  1068. container.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'
  1069. container.appendChild(title)
  1070. container.appendChild(subtitle)
  1071. container.appendChild(icon)
  1072. container.appendChild(message)
  1073. container.appendChild(versionInfo)
  1074. document.body.appendChild(background)
  1075. document.body.appendChild(container)
  1076. // TODO: Make flexible
  1077. if (!options.disableAnimation) {
  1078. let index = 0
  1079. const icons = ['hourglass_empty', 'hourglass_bottom', 'hourglass_top']
  1080. setTimeout(() => {
  1081. icon.classList.remove('animate__animated', 'animate__zoomIn')
  1082. icon.classList.add('rotating')
  1083. }, 1000)
  1084. setInterval(() => {
  1085. icon.innerText = icons[index]
  1086. index++
  1087. if (!icons[index]) index = 0
  1088. }, 500)
  1089. }
  1090. return () => {
  1091. background.classList.add('animate__animated', 'animate__fadeOut', 'animate__fast')
  1092. container.classList.add('animate__animated', 'animate__fadeOut', 'animate__fast')
  1093. background.addEventListener('animationend', background.remove)
  1094. container.addEventListener('animationend', container.remove)
  1095. }
  1096. }
  1097. /**
  1098. * Boot the kernel
  1099. *
  1100. * This kicks off the process of initializing the filesystem, modules, and other components
  1101. * @async
  1102. */
  1103. export async function boot () {
  1104. i18n
  1105. .use(i18nextBrowserLanguageDetector)
  1106. .init({
  1107. fallbackLng: 'en-US',
  1108. debug: process.env.I18N_DEBUG === 'true',
  1109. ns: ['common', 'kernel', 'app'],
  1110. defaultNS: 'common',
  1111. resources: locales
  1112. })
  1113. topbar.show()
  1114. const bootArgs = new URLSearchParams(globalThis.location.search)
  1115. globalThis.addEventListener('beforeunload', async () => {
  1116. await showSplash(`${t('Rebooting')}...`, { icon: 'autorenew', disableAnimation: true, disableVideoBackground: true })
  1117. document.querySelector('#web3os-splash-icon').classList.add('rotating')
  1118. })
  1119. // const keyboardElement = document.createElement('div')
  1120. // keyboardElement.classList.add('simple-keyboard')
  1121. // // keyboardElement.style.display = 'none'
  1122. // keyboardElement.style.position = 'absolute'
  1123. // keyboardElement.style.top = '0'
  1124. // document.body.appendChild(keyboardElement)
  1125. // keyboard = new Keyboard({
  1126. // onChange: input => events.dispatch('MobileKeyboardChange', input),
  1127. // onKeyPress: button => events.dispatch('MobileKeyboardKeyPress', button)
  1128. // })
  1129. if (!bootArgs.has('nobootsplash')) {
  1130. events.dispatch('ShowSplash')
  1131. const closeSplash = await showSplash()
  1132. setTimeout(closeSplash, 1500) // Prevent splash flash. The splash is pretty and needs to be seen and validated.
  1133. document.querySelector('#web3os-terminal').style.display = 'block'
  1134. setTimeout(globalThis.Terminal?.fit, 50)
  1135. globalThis.Terminal?.focus()
  1136. } else {
  1137. document.querySelector('#web3os-terminal').style.display = 'block'
  1138. setTimeout(globalThis.Terminal?.fit, 50)
  1139. globalThis.Terminal?.focus()
  1140. }
  1141. setInterval(() => globalThis.Terminal?.fit(), 200)
  1142. const isSmall = window.innerWidth <= 445
  1143. figlet.parseFont(figletFontName, figletFont)
  1144. figlet.text('web3os', { font: figletFontName }, async (err, logoFiglet) => {
  1145. if (err) log(err)
  1146. if (logoFiglet && globalThis.innerWidth >= 768) log(`\n${colors.green.bold(logoFiglet)}`)
  1147. else log(`\n${colors.green.bold(`${isSmall ? '' : '\t\t'}šŸ‰ web3os šŸ‰`)}`)
  1148. console.log(`%cweb3os %c${rootPkgJson.version}`, `
  1149. font-family: "Lucida Console", Monaco, monospace;
  1150. font-size: 25px;
  1151. letter-spacing: 2px;
  1152. word-spacing: 2px;
  1153. color: #028550;
  1154. font-weight: 700;
  1155. font-style: normal;
  1156. font-variant: normal;
  1157. text-transform: none;`, null)
  1158. console.log('%chttps://github.com/web3os-org/kernel', 'font-size:14px;')
  1159. console.debug({ Kernel, Terminal, System })
  1160. for (const event of KernelEvents) {
  1161. events.on(event, payload => {
  1162. let data
  1163. switch (event) {
  1164. case 'ModuleLoaded':
  1165. data = payload.detail.modInfo.name
  1166. break
  1167. default:
  1168. data = payload
  1169. }
  1170. eventsProcessed++
  1171. console.debug('Kernel Event:', { event, data })
  1172. })
  1173. }
  1174. if (!bootArgs.has('nobootintro')) await printBootIntro()
  1175. await loadLocalStorage()
  1176. events.dispatch('MemoryLoaded', memory)
  1177. await setupFilesystem()
  1178. events.dispatch('FilesystemLoaded')
  1179. await registerKernelBins()
  1180. events.dispatch('KernelBinsLoaded')
  1181. await registerBuiltinModules()
  1182. events.dispatch('BuiltinModulesLoaded')
  1183. // Load builtin kernel FS modules into the kernel
  1184. for await (const [name, mod] of Object.entries(await fsModules({ BrowserFS, fs, execute, modules, t, utils }))) {
  1185. loadModule(mod, { name: `@web3os-fs/${name}` })
  1186. }
  1187. events.dispatch('FilesystemModulesLoaded')
  1188. await loadPackages()
  1189. events.dispatch('PackagesLoaded')
  1190. // Copy namespaced core modules onto root object
  1191. const web3osCoreNamespaces = ['@web3os-core', '@web3os-fs']
  1192. for (const mod of Object.values(modules)) {
  1193. const [namespace, name] = mod.name.split('/')
  1194. if (web3osCoreNamespaces.includes(namespace)) modules[name] = mod
  1195. }
  1196. // Check for notification permission and request if necessary
  1197. if (Notification?.permission === 'default') Notification.requestPermission()
  1198. if (Notification?.permission === 'denied') log(colors.warning(t('Notification permission denied')))
  1199. localStorage.setItem('web3os_first_boot_complete', 'true')
  1200. events.dispatch('AutostartStart')
  1201. await autostart()
  1202. events.dispatch('AutostartEnd')
  1203. if (modules.confetti) await execute('confetti --startVelocity 90 --particleCount 150')
  1204. topbar.hide()
  1205. const heartbeat = setInterval(() => navigator.vibrate([200, 50, 200]), 1000)
  1206. setTimeout(() => clearInterval(heartbeat), 5000)
  1207. // Expose this globally in case it needs to be cleared externally by an app
  1208. globalThis.web3osBlinkyTitleInterval = setInterval(() => {
  1209. document.title = document.title.includes('_') ? 'web3os# ' : 'web3os# _'
  1210. }, 600)
  1211. events.dispatch('BootComplete')
  1212. console.timeEnd('web3os:boot')
  1213. console.debug('Navigation Performance:', globalThis.performance?.getEntriesByType('navigation')?.[0]?.duration)
  1214. if (location.hostname !== 'localhost') analyticsEvent({ event: 'boot-complete' })
  1215. })
  1216. }
  1217. /**
  1218. * Wait for the specified number of milliseconds
  1219. * @async
  1220. * @param {number} ms - The number of milliseconds to wait
  1221. * @return {Module}
  1222. */
  1223. export async function wait (ms) {
  1224. return new Promise(resolve => setTimeout(resolve, ms))
  1225. }
  1226. // Setup screensaver interval
  1227. let idleTimer
  1228. const resetIdleTime = () => {
  1229. clearTimeout(idleTimer)
  1230. if (!memory) return
  1231. const timeout = get('config', 'screensaver-timeout') || Config.screensaverTimeout
  1232. if (!modules?.screensaver || timeout <= 0) return
  1233. const saver = modules.screensaver.getSaver()
  1234. const startTime = modules.screensaver.getStartTime()
  1235. // Prevent screensaver from immediately exiting to overcome
  1236. // late enter-key event when entering command at terminal
  1237. if (Date.now() - startTime > 500) saver?.exit?.()
  1238. idleTimer = setTimeout(async () => {
  1239. modules.screensaver.run(globalThis.Terminal, get('config', 'screensaver') || 'matrix')
  1240. }, timeout)
  1241. }
  1242. // Activity listeners to reset idle time
  1243. globalThis.addEventListener('mousemove', resetIdleTime)
  1244. globalThis.addEventListener('keydown', resetIdleTime)
  1245. globalThis.addEventListener('keyup', resetIdleTime)
  1246. globalThis.addEventListener('keypress', resetIdleTime)
  1247. globalThis.addEventListener('pointerdown', resetIdleTime)
  1248. // Setup protocol handler
  1249. if (navigator.registerProtocolHandler) navigator.registerProtocolHandler('web+threeos', '?destination=%s')
  1250. // Handle PWA installability
  1251. globalThis.addEventListener('beforeinstallprompt', e => {
  1252. const installer = {
  1253. name: '@web3os-core/install',
  1254. description: t('kernel:bins.descriptions.install', 'Install web3os as a PWA'),
  1255. run: async () => {
  1256. const result = await e.prompt()
  1257. Terminal.log(result)
  1258. analyticsEvent({ event: 'pwa-install', details: result })
  1259. }
  1260. }
  1261. modules[installer.name] = installer
  1262. modules.install = installer
  1263. })
  1264. // Register service worker
  1265. if ('serviceWorker' in navigator && location.hostname !== 'localhost') {
  1266. globalThis.addEventListener('load', () => {
  1267. navigator.serviceWorker.register('/service-worker.js')
  1268. })
  1269. }
  1270. JAVASCRIPT
    Copied!