diff --git a/index.html b/index.html index 62a5c35..edff29d 100644 --- a/index.html +++ b/index.html @@ -7,6 +7,6 @@ - + diff --git a/package-lock.json b/package-lock.json index 8517556..61da784 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "1.1.0", "license": "MIT", "dependencies": { + "ansi-to-html": "^0.7.2", + "chalk": "^5.3.0", "eruda": "^3.0.1", "js-ini": "^1.6.0", "material-symbols": "^0.14.3", @@ -303,6 +305,23 @@ "node": ">=v18" } }, + "node_modules/@commitlint/load/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "optional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/@commitlint/load/node_modules/cosmiconfig": { "version": "8.3.6", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", @@ -400,6 +419,23 @@ "node": ">=v18" } }, + "node_modules/@commitlint/types/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "optional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/@esbuild/android-arm": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", @@ -1877,6 +1913,20 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ansi-to-html": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.7.2.tgz", + "integrity": "sha512-v6MqmEpNlxF+POuyhKkidusCHWWkaLcGRURzivcU3I9tv7k4JVhFcnukrM5Rlk2rUywdZuzYAZ+kbZqWCnfN3g==", + "dependencies": { + "entities": "^2.2.0" + }, + "bin": { + "ansi-to-html": "bin/ansi-to-html" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/ansicolors": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", @@ -2376,16 +2426,11 @@ } }, "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", "engines": { - "node": ">=10" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" @@ -3239,6 +3284,14 @@ "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", "dev": true }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/env-ci": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/env-ci/-/env-ci-11.0.0.tgz", @@ -3906,6 +3959,22 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/eslint/node_modules/eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", @@ -5066,6 +5135,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/inquirer/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/inquirer/node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -5937,6 +6022,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/log-symbols/node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -6055,18 +6156,6 @@ "marked": ">=1 <12" } }, - "node_modules/marked-terminal/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", - "dev": true, - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/material-symbols": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/material-symbols/-/material-symbols-0.14.5.tgz", @@ -9397,6 +9486,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ora/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/ora/node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -11936,6 +12041,22 @@ "vite": ">=2.0.0" } }, + "node_modules/vite-plugin-compression/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/vite-plugin-dynamic-import": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/vite-plugin-dynamic-import/-/vite-plugin-dynamic-import-1.5.0.tgz", diff --git a/package.json b/package.json index 2a92573..9d5c05f 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "flowos", "version": "1.1.0", "description": "The most aesthetic webOS.", - "main": "src/kernel.ts", + "main": "src/bootloader.ts", "scripts": { "docs": "typedoc src/**", "test": "ts-standard", @@ -34,6 +34,8 @@ "vite-plugin-node-polyfills": "^0.15.0" }, "dependencies": { + "ansi-to-html": "^0.7.2", + "chalk": "^5.3.0", "eruda": "^3.0.1", "js-ini": "^1.6.0", "material-symbols": "^0.14.3", diff --git a/src/bootloader.ts b/src/bootloader.ts new file mode 100644 index 0000000..5937b44 --- /dev/null +++ b/src/bootloader.ts @@ -0,0 +1,126 @@ +/** + * FlowOS Bootloader + * + */ + +import Kernel, { spaces } from './kernel' +import HTML from './HTML' + +import logo from './assets/flow.png' + +const body = new HTML(document.body) + +body.html('') +body.style({ + margin: '0', + width: '100vw', + height: '100vh', + overflow: 'hidden' +}) + +const boot = new HTML('div').styleJs({ + display: 'flex', + flexDirection: 'column', + height: '100%', + width: '100%', + background: '#11111b', + padding: '100px', + 'font-family': 'monospace', + userSelect: 'none', + overflow: 'hidden' +}).appendTo(body) + +boot.appendMany( + new HTML('div') + .styleJs({ + display: 'flex', + height: '40px', + alignItems: 'center', + gap: '10px' + }) + .appendMany( + new HTML('img').attr({ + src: logo, + height: '40px' + }), + new HTML('h1').text('FlowOS').styleJs({ + color: 'white' + }) + ), + new HTML('img').attr({ + src: logo + }).styleJs({ + position: 'absolute', + right: '-8vw', + top: '-7vw', + opacity: '0.03', + height: '50vw', + 'pointer-events': 'none', + zIndex: '0' + }) +) + +const terminal = new HTML('div').style({ + color: '#89b4fa', + padding: '10px 3px', + 'word-break': 'break-all', + 'white-space': 'pre-wrap', + flex: '1', + 'user-select': 'text', + position: 'relative', + zIndex: '2' +}).appendTo(boot) + +const progress = new HTML('div').style({ + width: '0', + background: '#89b4fa', + transition: 'width 0.5s cubic-bezier(1,0,0,1)', + height: '5px' +}) +new HTML('div').style({ + height: '5px', + width: '100%', + background: '#181825' +}).appendTo(boot) + .append(progress) + +const write = (content: string): void => { + terminal.text(terminal.getText() + content) +} + +const writeln = (content = ''): void => { + write(`${content}\n`) +} + +const originalConsoleLog = console.log +const originalConsoleError = console.error +const originalConsoleWarn = console.warn +const originalConsoleGroup = console.group +window.console.log = (...args: any) => { + originalConsoleLog(...args) + writeln(args) +} +window.console.warn = (...args: any) => { + originalConsoleWarn(...args) + writeln(args) +} +window.console.error = (...args: any) => { + originalConsoleError(...args) + writeln(args) +} +window.console.group = (...args: any) => { + originalConsoleGroup(...args) + writeln(spaces + String(args)) +} + +try { + const args = new URLSearchParams(window.location.search) + const kernel = new Kernel() + await kernel.boot(boot, progress, args) +} catch (e) { + writeln() + writeln('An error occured while booting FlowOS.') + writeln('Please report this error to Flow Works.') + writeln() + console.error(e.stack) +} diff --git a/src/kernel.ts b/src/kernel.ts index 36ed759..96044d4 100644 --- a/src/kernel.ts +++ b/src/kernel.ts @@ -1,29 +1,44 @@ -import './assets/style.less' -import { version } from '../package.json' -import { v4 as uuid } from 'uuid' +import pkg from '../package.json' +import VirtualFS from './system/VirtualFS' +import HTML from './HTML' +import { Executable, KernelConfig, Package, Permission, Process, ProcessInfo, FileSystem } from './types' import ProcessLib from './structures/ProcessLib' -import ProcLib from './structures/ProcLib' -import { Executable, Process, Package, ProcessInfo, KernelConfig, Permission } from './types' import semver from 'semver' +import ProcLib from './structures/ProcLib' +import { v4 as uuid } from 'uuid' +import eruda from 'eruda' +import { parse } from 'js-ini' -declare global { - interface Window { - kernel: Kernel +export const spaces = ' ' + +const print = { + ok: (action: string, text: string) => console.log(`[ OK ] ${action} ${text}`), + failed: (action: string, text: string, error: any) => console.error(`[FAILED] ${action} ${text}`), + none: (action: string, text: string) => console.group(`${action} ${text}`) +} + +const handle = async (type: 'target' | 'service' | 'mount', name: string, Instance: any): Promise => { + try { + if (type !== 'target') print.none(type === 'mount' ? 'Mounting' : 'Starting', name) + const instance = typeof Instance === 'object' ? Instance : new Instance() + const data = await instance.init() + print.ok( + type === 'service' + ? 'Started' + : type === 'mount' + ? 'Mounted' + : 'Reached target', + name + ) + console.groupEnd() + return typeof Instance === 'object' ? data : instance + } catch (e) { + print.failed('Failed', `to start ${name}`, e) + console.error(`${spaces}${e.stack.split('\n').join(`\n${spaces}`) as string}`) + return false } } -const params = new URLSearchParams(window.location.search) - -async function enableDebug (): Promise { - const { default: eruda } = await import('eruda') - eruda.init() - return await Promise.resolve() -} - -if (params.get('debug') != null) { - enableDebug().catch(e => console.error(e)) -} - export default class Kernel { readonly version: string readonly codename: string @@ -32,31 +47,86 @@ export default class Kernel { [key: string]: Package } = {} - fs: any + fs: FileSystem | false - config: KernelConfig + config: KernelConfig | false lastPid: number = 0 - constructor (version: string) { - this.codename = 'Mochi' - this.version = version + constructor () { + this.codename = 'Pocky' + this.version = pkg.version } - setFS (fs: any, process: ProcessLib): void { - if (process.permission === Permission.SYSTEM) { - this.fs = fs - } - } - - setConfig (data: any, process: ProcessLib): void { - if (process.permission === Permission.SYSTEM) { - this.config = data - document.dispatchEvent(new CustomEvent('config_update', { - detail: { - config: this.config + async boot (boot: HTML, progress: HTML, args: URLSearchParams): Promise { + progress.style({ width: '0%' }) + const bootArgs = args.toString().replace(/=($|&)/g, '=true ') + console.log(`FlowOS - v${pkg.version}, Flow Works (c) ${new Date().getFullYear()}`) + console.log() + console.log(`User Agent : ${navigator.userAgent}`) + console.log(`Boot Args : ${bootArgs === '' ? 'None' : bootArgs}`) + console.log() + console.log('...') + console.log() + if (args.has('debug')) eruda.init() + this.fs = await handle('target', 'Virtual File Systems', VirtualFS) + if (this.fs === false) return + else progress.style({ width: '20%' }) + this.config = await handle('target', 'FlowOS Configuration', { + init: async () => { + if (this.fs === false) return + return parse(Buffer.from(await this.fs.readFile('/etc/flow')).toString()) as any + } + }) + if (this.config === false) return + else progress.style({ width: '40%' }) + const tmp = await handle('mount', 'Temporary Directory (/tmp)', { + init: async () => { + if (this.fs === false) return false + if (await this.fs.exists('/tmp')) { + await this.fs.rmdir('/tmp') } - })) - } + return await this.fs.mkdir('/tmp') + } + }) + if (tmp === false) return + else progress.style({ width: '60%' }) + const sw = await handle('service', 'Service Worker', { + init: async () => { + if (this.config === false) return false + const registrations = await navigator.serviceWorker.getRegistrations() + for (const registration of registrations) { + await registration.unregister() + } + await navigator.serviceWorker.register(`/uv-sw.js?url=${encodeURIComponent(btoa(this.config.SERVER))}&e=${uuid()}`, { + scope: '/service/' + }) + } + }) + if (sw === false) return + else progress.style({ width: '80%' }) + await handle('service', 'Desktop Environment', { + init: () => { + setTimeout(() => { + import('./assets/style.less') + .then(() => { + boot.style({ display: 'none' }) + import('material-symbols') + .then(async () => { + if (this.fs === false) return + console.log() + console.log('Welcome to FlowOS!') + console.log() + progress.style({ width: '100%' }) + setTimeout(() => { + this.startExecutable('Desktop', Permission.SYSTEM).catch(e => console.error(e)) + }, 750) + }) + .catch(e => { throw e }) + }) + .catch(e => { throw e }) + }, 1000) + } + }) } async startExecutable (url: string, permission = Permission.USER, data = {}): Promise<{ procLib: ProcessLib, executable: Process } | any> { @@ -67,7 +137,7 @@ export default class Kernel { const importedExecutable = (await module()) as any executable = importedExecutable.default } catch { - if (this.fs === undefined) throw new Error('Filesystem hasn\'t been initiated.') + if (this.fs === false) throw new Error('Filesystem hasn\'t been initiated.') const dataURL = `data:text/javascript;base64,${Buffer.from(await this.fs.readFile(`/opt/${url}.js`)).toString('base64')}` const importedExecutable = await import(dataURL) executable = importedExecutable.default @@ -111,7 +181,7 @@ export default class Kernel { const importedExecutable = (await module()) as any executable = importedExecutable.default } catch { - if (this.fs === undefined) throw new Error('Filesystem hasn\'t been initiated.') + if (this.fs === false) throw new Error('Filesystem hasn\'t been initiated.') const dataURL = `data:text/javascript;base64,${Buffer.from(await this.fs.readFile(`/opt/${url}.js`)).toString('base64')}` const importedExecutable = await import(dataURL) executable = importedExecutable.default @@ -126,9 +196,3 @@ export default class Kernel { return executable } } - -document.addEventListener('DOMContentLoaded', () => { - import('material-symbols') - const kernel = new Kernel(version) - kernel.startExecutable('BootLoader', Permission.SYSTEM).catch(e => console.error(e)) -}) diff --git a/src/structures/ProcessLib.ts b/src/structures/ProcessLib.ts index 3fa9bf9..65edc9a 100644 --- a/src/structures/ProcessLib.ts +++ b/src/structures/ProcessLib.ts @@ -1,6 +1,6 @@ import semver from 'semver' import Kernel from '../kernel' -import { Process, Executable, Package, Library, Permission, LoadedLibrary, LibraryPath } from '../types' +import { Process, Executable, Package, Library, Permission, LoadedLibrary, LibraryPath, FileSystem } from '../types' import FlowWindow from './FlowWindow' import LibraryLib from './LibraryLib' import ProcLib from './ProcLib' @@ -9,6 +9,7 @@ export default class ProcessLib { readonly pid: number readonly token: string process: Process + fs: FileSystem private readonly _kernel: Kernel readonly kernel: { getExecutable: (url: string) => Promise @@ -17,8 +18,7 @@ export default class ProcessLib { [key: string]: Package } config: any - setConfig: (data: any) => void - setFS: (fs: any) => void + setConfig: (config: any) => any } readonly permission: Permission @@ -32,6 +32,8 @@ export default class ProcessLib { } constructor (url: string, pid: number, token: string, permission = Permission.USER, data = {}, process: Process, kernel: Kernel) { + if (kernel.fs === false) return + this.fs = kernel.fs this.permission = permission this.pid = pid this.token = token @@ -41,8 +43,9 @@ export default class ProcessLib { processList: kernel.processList, packageList: kernel.packageList, config: kernel.config, - setConfig: (data: any) => kernel.setConfig(data, this), - setFS: (fs: any) => kernel.setFS(fs, this) + setConfig: (config) => { + if (this.permission >= Permission.ELEVATED) kernel.config = config + } } this.process = process this.data = data @@ -65,8 +68,7 @@ export default class ProcessLib { const importedExecutable = (await module()) as any executable = importedExecutable.default } catch { - if (this._kernel.fs === undefined) throw new Error('Filesystem hasn\'t been initiated.') - const dataURL = `data:text/javascript;base64,${Buffer.from(await this._kernel.fs.readFile(`/opt/${url}.js`)).toString('base64')}` + const dataURL = `data:text/javascript;base64,${Buffer.from(await this.fs.readFile(`/opt/${url}.js`)).toString('base64')}` const importedExecutable = await import(dataURL) executable = importedExecutable.default } diff --git a/src/system/BootLoader.ts b/src/system/BootLoader.ts index f9fad95..14a4ef4 100644 --- a/src/system/BootLoader.ts +++ b/src/system/BootLoader.ts @@ -1,7 +1,6 @@ import HTML from '../HTML' -import { AppClosedEvent, AppOpenedEvent, Directory, FileSystemObject, Process } from '../types' +import { AppClosedEvent, AppOpenedEvent, Process } from '../types' import { getTime } from '../utils' -import { db, defaultFS, initializeDatabase, read, setFileSystem, write } from './lib/VirtualFS' import nullIcon from '../assets/icons/application-default-icon.svg' import { parse } from 'js-ini' import { v4 as uuid } from 'uuid' @@ -17,39 +16,11 @@ const BootLoader: Process = { const splashElement = splashScreen.getElement() splashElement.appendTo(document.body) - const fs = await process.loadLibrary('lib/VirtualFS') + const { fs } = process const wm = await process.loadLibrary('lib/WindowManager') const launcher = await process.loadLibrary('lib/Launcher') - await initializeDatabase('virtualfs') - db.onerror = (event: Event) => { - const target = event.target as IDBRequest - const errorMessage = target.error !== null ? target.error.message : 'Unknown error' - throw new Error(`[VirtualFS] ${target.error?.name ?? 'Unknown Error'}: ${errorMessage}`) - } - if ('storage' in navigator) { - await navigator.storage?.persist()?.catch(e => console.error(e)) - } else { - console.warn('Persistent storage is not supported.') - } - const fileSystem = await read() as FileSystemObject - if (fileSystem === undefined) { - await write(defaultFS) - } else { - const appsDirectory = ((fileSystem.root.children.home as Directory).children.Applications as Directory).children - const defaultAppsDirectory = ((defaultFS.root.children.home as Directory).children.Applications as Directory).children - for (const file in defaultAppsDirectory) { - if (appsDirectory[file] === undefined && defaultAppsDirectory[file] !== undefined) { - console.log(file) - appsDirectory[file] = defaultAppsDirectory[file] - } - } - await write(fileSystem) - await setFileSystem(fileSystem) - } - const config = Buffer.from(await fs.readFile('/etc/flow')).toString() - process.kernel.setFS(fs) process.kernel.setConfig(parse(config)) if ('serviceWorker' in navigator) { diff --git a/src/system/Desktop.ts b/src/system/Desktop.ts new file mode 100644 index 0000000..fb842a2 --- /dev/null +++ b/src/system/Desktop.ts @@ -0,0 +1,149 @@ +import HTML from '../HTML' +import { AppClosedEvent, AppOpenedEvent, Process } from '../types' +import { getTime } from '../utils' +import nullIcon from '../assets/icons/application-default-icon.svg' + +const BootLoader: Process = { + config: { + name: 'Desktop', + type: 'process', + targetVer: '1.0.0-indev.0' + }, + run: async (process) => { + const splashScreen = await process.loadLibrary('lib/SplashScreen') + const splashElement = splashScreen.getElement() + splashElement.appendTo(document.body) + + const { fs } = process + const wm = await process.loadLibrary('lib/WindowManager') + const launcher = await process.loadLibrary('lib/Launcher') + + const input = new HTML('input').attr({ + type: 'text', + placeholder: 'Search' + }).on('keyup', () => { + apps.elm.innerHTML = '' + renderApps().catch(e => console.error(e)) + }).appendTo(launcher.element) + const apps = new HTML('apps').appendTo(launcher.element) + + const renderApps = async (): Promise => { + apps.html('') + const files = await fs.readdir('/home/Applications/') + files + .filter((x: string) => x.endsWith('.app') && ((input.elm as HTMLInputElement) !== null ? x.toLowerCase().includes((input.elm as HTMLInputElement).value.toLowerCase()) : true)) + .forEach((file: string) => { + fs.readFile(`/home/Applications/${file}`).then(async (data: Uint8Array) => { + const path = Buffer.from(data).toString() + const executable = await process.kernel.getExecutable(path) as Process + + const appElement = new HTML('app').on('click', () => { + process.launch(path).catch((e: any) => console.error(e)) + launcher.toggle() + }).appendTo(apps) + new HTML('img').attr({ + src: executable.config.icon ?? nullIcon, + alt: `${executable.config.name} icon` + }).appendTo(appElement) + new HTML('div').text(executable.config.name).appendTo(appElement) + }).catch((e: any) => console.error(e)) + }) + } + + await renderApps() + document.addEventListener('fs_update', () => { + renderApps().catch(e => console.error(e)) + }) + + launcher.element.on('click', (e: Event) => { + if (e.target !== e.currentTarget) return + launcher.toggle() + }) + + const statusBar = await process.loadLibrary('lib/StatusBar') + + statusBar.element.html(` +
space_dashboard
+ +
+ +
expand_less
+
+ battery_2_bar + signal_cellular_4_bar +
+
+ + `) + + setInterval((): any => { + getTime().then((time) => { + statusBar.element.qs('div[data-toolbar-id="calendar"]')?.text(time) + }).catch(e => console.error) + }, 1000) + + statusBar.element.qs('div[data-toolbar-id="start"]')?.on('click', () => { + launcher.toggle() + }) + + if ('getBattery' in navigator) { + (navigator as any).getBattery().then((battery: any) => { + statusBar.updateBatteryIcon(battery) + + battery.addEventListener('levelchange', () => { + statusBar.updateBatteryIcon(battery) + }) + + battery.addEventListener('chargingchange', () => { + statusBar.updateBatteryIcon(battery) + }) + }) + } else { + const batteryDiv = document.querySelector('div[data-toolbar-id="controls"] > .battery') + if (batteryDiv != null) { + batteryDiv.innerHTML = 'battery_unknown' + } + } + + async function ping (startTime: number): Promise { + fetch(`${process.kernel.config.SERVER as string}/bare/`) + .then(() => { + const endTime = performance.now() + const pingTime = endTime - startTime + statusBar.updateIcon(pingTime) + }) + .catch(() => { + (document.querySelector('div[data-toolbar-id="controls"] > .signal') as HTMLElement).innerHTML = 'signal_cellular_connected_no_internet_4_bar' + }) + } + + setInterval((): any => ping(performance.now()), 10_000) + + document.addEventListener('app_opened', (e: AppOpenedEvent): void => { + new HTML('app').appendMany( + new HTML('img').attr({ + alt: `${e.detail.proc.config.name} icon`, + 'data-id': e.detail.token, + src: e.detail.proc.config.icon ?? nullIcon + }).on('click', () => { + e.detail.win.focus() + e.detail.win.toggleMin() + }) + ).appendTo(statusBar.element.qs('div[data-toolbar-id="apps"]')?.elm as HTMLElement) + }) + + document.addEventListener('app_closed', (e: AppClosedEvent): void => { + statusBar.element.qs('div[data-toolbar-id="apps"]')?.qs(`img[data-id="${e.detail.token}"]`)?.elm.parentElement?.remove() + }) + + document.body.style.flexDirection = 'column-reverse' + + await statusBar.element.appendTo(document.body) + await launcher.element.appendTo(document.body) + await wm.windowArea.appendTo(document.body) + + splashElement.cleanup() + } +} + +export default BootLoader diff --git a/src/system/VirtualFS.ts b/src/system/VirtualFS.ts new file mode 100644 index 0000000..a6dbeee --- /dev/null +++ b/src/system/VirtualFS.ts @@ -0,0 +1,433 @@ +import { Directory, Errors, File, Permission, Stats } from '../types' + +export const defaultFS: { root: Directory } = { + root: { + type: 'directory', + deleteable: false, + permission: Permission.SYSTEM, + children: { + home: { + type: 'directory', + deleteable: false, + permission: Permission.SYSTEM, + children: { + Downloads: { + type: 'directory', + deleteable: false, + permission: Permission.USER, + children: {} + }, + Applications: { + type: 'directory', + deleteable: false, + permission: Permission.USER, + children: { + 'Info.app': { + type: 'file', + deleteable: true, + permission: Permission.USER, + content: Buffer.from('apps/Info') + }, + 'Manager.app': { + type: 'file', + deleteable: true, + permission: Permission.USER, + content: Buffer.from('apps/Manager') + }, + 'Store.app': { + type: 'file', + deleteable: true, + permission: Permission.USER, + content: Buffer.from('apps/Store') + }, + 'TaskManager.app': { + type: 'file', + deleteable: true, + permission: Permission.USER, + content: Buffer.from('apps/TaskManager') + }, + 'Browser.app': { + type: 'file', + deleteable: true, + permission: Permission.USER, + content: Buffer.from('apps/Browser') + }, + 'ImageViewer.app': { + type: 'file', + deleteable: true, + permission: Permission.USER, + content: Buffer.from('apps/ImageViewer') + }, + 'Files.app': { + type: 'file', + deleteable: true, + permission: Permission.USER, + content: Buffer.from('apps/Files') + }, + 'Editor.app': { + type: 'file', + deleteable: true, + permission: Permission.USER, + content: Buffer.from('apps/Editor') + }, + 'Settings.app': { + type: 'file', + deleteable: true, + permission: Permission.USER, + content: Buffer.from('apps/Settings') + } + } + }, + Desktop: { + type: 'directory', + deleteable: false, + permission: Permission.USER, + children: { + 'README.md': { + type: 'file', + deleteable: true, + permission: Permission.USER, + content: Buffer.from('# Welcome to FlowOS!') + }, + 'Info.lnk': { + type: 'file', + deleteable: true, + permission: Permission.USER, + content: Buffer.from('/home/Applications/Info.app') + }, + 'Manager.lnk': { + type: 'file', + deleteable: true, + permission: Permission.USER, + content: Buffer.from('/home/Applications/Manager.app') + }, + 'Store.lnk': { + type: 'file', + deleteable: true, + permission: Permission.USER, + content: Buffer.from('/home/Applications/Store.app') + }, + 'TaskManager.lnk': { + type: 'file', + deleteable: true, + permission: Permission.USER, + content: Buffer.from('/home/Applications/TaskManager.app') + }, + 'Browser.lnk': { + type: 'file', + deleteable: true, + permission: Permission.USER, + content: Buffer.from('/home/Applications/Browser.app') + }, + 'ImageViewer.lnk': { + type: 'file', + deleteable: true, + permission: Permission.USER, + content: Buffer.from('/home/Applications/ImageViewer.app') + }, + 'Files.lnk': { + type: 'file', + deleteable: true, + permission: Permission.USER, + content: Buffer.from('/home/Applications/Files.app') + }, + 'Editor.lnk': { + type: 'file', + deleteable: true, + permission: Permission.USER, + content: Buffer.from('/home/Applications/Editor.app') + } + } + }, + Pictures: { + type: 'directory', + deleteable: false, + permission: Permission.USER, + children: {} + }, + Videos: { + type: 'directory', + deleteable: false, + permission: Permission.USER, + children: {} + }, + Documents: { + type: 'directory', + deleteable: false, + permission: Permission.USER, + children: {} + }, + Music: { + type: 'directory', + deleteable: false, + permission: Permission.USER, + children: {} + } + } + }, + var: { + type: 'directory', + deleteable: false, + permission: Permission.SYSTEM, + children: {} + }, + etc: { + type: 'directory', + deleteable: false, + permission: Permission.SYSTEM, + children: { + flow: { + type: 'file', + deleteable: false, + permission: Permission.ELEVATED, + content: Buffer.from([ + 'SERVER=https://server.flow-works.me', + '24HOUR=FALSE' + ].join('\n')) + }, + hostname: { + type: 'file', + deleteable: false, + permission: Permission.ELEVATED, + content: Buffer.from('flow') + } + } + }, + opt: { + type: 'directory', + deleteable: false, + permission: Permission.SYSTEM, + children: { + apps: { + type: 'directory', + deleteable: false, + permission: Permission.SYSTEM, + children: {} + } + } + } + } + } +} + +class VirtualFS { + private fileSystem: { root: Directory } = defaultFS + private db: IDBDatabase | null = null + async init (dbName = 'virtualfs'): Promise { + return await new Promise((resolve, reject) => { + indexedDB.deleteDatabase(dbName) + const request = indexedDB.open(dbName) + request.onerror = () => { + reject(new Error('Failed to open database')) + } + request.onsuccess = () => { + this.db = request.result + resolve(this) + } + request.onupgradeneeded = () => { + const db = request.result + db.createObjectStore('fs') + } + }) + } + + private setFileSystem (fileSystemObject: { root: Directory }): void { + this.fileSystem = fileSystemObject + } + + private readonly read = async (): Promise => { + const transaction = this.db?.transaction(['fs'], 'readonly') + const store = transaction?.objectStore('fs') + const getRequest = store?.get('fs') + + return await new Promise((resolve, reject) => { + if (getRequest == null) return + getRequest.onsuccess = () => { + resolve(getRequest.result) + } + + getRequest.onerror = () => { + reject(getRequest.error) + } + }) + } + + private readonly write = async (fileSystemObject: { root: Directory }): Promise => { + this.fileSystem = fileSystemObject + await this.save() + } + + private readonly save = async (): Promise => { + const transaction = this.db?.transaction(['fs'], 'readwrite') + const store = transaction?.objectStore('fs') + const putRequest = store?.put(this.fileSystem, 'fs') + + return await new Promise((resolve, reject) => { + if (putRequest == null) return + putRequest.onsuccess = () => { + document.dispatchEvent(new CustomEvent('fs_update', {})) + resolve() + } + + putRequest.onerror = () => { + reject(putRequest.error) + } + }) + } + + private readonly navigatePath = async (path: string): Promise<{ current: Directory | File, parts: string[] }> => { + const parts = path.split('/').filter(x => x !== '') + let current = this.fileSystem.root + for (const part of parts) { + current = current.children[part] as Directory + } + return { current, parts } + } + + private readonly navigatePathParent = async (path: string): Promise<{ current: Directory, parts: string[], filename: string }> => { + const parts = path.split('/').filter(x => x !== '') + const filename = parts.pop() as string + let current = this.fileSystem.root + for (const part of parts) { + current = current.children[part] as Directory + } + return { current, parts, filename } + } + + private readonly handlePermissions = async (path: string): Promise => { + const { current } = await this.navigatePath(path) + if (current.permission === Permission.SYSTEM) throw new Error(Errors.EPERM) + } + + unlink = async (path: string): Promise => { + const { current, filename } = await this.navigatePathParent(path) + + if (!current.children[filename].deleteable) throw new Error(Errors.EPERM) + await this.handlePermissions(path) + + Reflect.deleteProperty(current.children, filename) + + console.debug(`unlink ${path}`) + await this.save() + } + + readFile = async (path: string): Promise => { + const { current } = await this.navigatePath(path) + + await this.handlePermissions(path) + + if (current.type !== 'file') throw new Error(Errors.EISDIR) + + console.debug(`read ${path}`) + return current.content + } + + writeFile = async (path: string, content: string | Buffer): Promise => { + const { current, filename } = await this.navigatePathParent(path) + + let permission + + if (typeof current.children[filename] === 'undefined') { + permission = Permission.USER + } else { + await this.handlePermissions(path) + permission = current.children[filename].permission + } + + current.children[filename] = { + type: 'file', + deleteable: true, + permission, + content: Buffer.from(content) + } + + console.debug(`write ${path}`) + await this.save() + } + + mkdir = async (path: string): Promise => { + const { current, filename } = await this.navigatePathParent(path) + + let permission + + if (typeof current.children[filename] === 'undefined') { + permission = Permission.USER + } else { + await this.handlePermissions(path) + permission = current.children[filename].permission + } + + current.children[filename] = { + type: 'directory', + deleteable: true, + permission: path === '/tmp' ? Permission.USER : permission, + children: {} + } + + console.debug(`mkdir ${path}`) + await this.save() + } + + rmdir = async (path: string): Promise => { + const { current, filename } = await this.navigatePathParent(path) + + if (!current.deleteable) throw new Error(Errors.EPERM) + await this.handlePermissions(path) + + if (current.children[filename].type !== 'directory') throw new Error(Errors.ENOTDIR) + + Reflect.deleteProperty(current.children, filename) + + console.debug(`rmdir ${path}`) + await this.save() + } + + readdir = async (path: string): Promise => { + const { current } = await this.navigatePath(path) + + if (current.type === 'file') throw new Error(Errors.ENOTDIR) + const result = await Promise.all(Object.keys(current.children ?? {})) + + console.debug(`readdir ${path}`) + return result + } + + stat = async (path: string): Promise => { + const { current } = await this.navigatePath(path) + + console.debug(`stat ${path}`) + return { + isDirectory: () => current.type === 'directory', + isFile: () => current.type === 'file' + } + } + + rename = async (oldPath: string, newPath: string): Promise => { + const { current: oldCurrent, filename: oldFilename } = await this.navigatePathParent(oldPath) + const { current: newCurrent, filename: newFilename } = await this.navigatePathParent(newPath) + + if (!oldCurrent.deleteable) throw new Error(Errors.EPERM) + if (!newCurrent.deleteable) throw new Error(Errors.EPERM) + + await this.handlePermissions(oldPath) + await this.handlePermissions(newPath) + + newCurrent.children[newFilename] = oldCurrent.children[oldFilename] + Reflect.deleteProperty(oldCurrent.children, oldFilename) + + console.debug(`rename ${oldPath} -> ${newPath}`) + await this.save() + } + + exists = async (path: string): Promise => { + console.debug(`exists ${path}`) + try { + const { current } = await this.navigatePath(path) + return current !== undefined + } catch (e) { + return false + } + } +} + +export default VirtualFS diff --git a/src/system/apps/Editor.ts b/src/system/apps/Editor.ts index 94f0df4..84743dd 100644 --- a/src/system/apps/Editor.ts +++ b/src/system/apps/Editor.ts @@ -51,7 +51,7 @@ const Editor: Process = { }, process) }) - const fs = await process.loadLibrary('lib/VirtualFS') + const fs = process.fs const data = process.data as EditorConfig diff --git a/src/system/apps/Files.ts b/src/system/apps/Files.ts index 7a9648e..a391426 100644 --- a/src/system/apps/Files.ts +++ b/src/system/apps/Files.ts @@ -18,7 +18,7 @@ const Files: Process = { }, process) }) - const fs = await process.loadLibrary('lib/VirtualFS') + const fs = process.fs const MIMETypes = await process.loadLibrary('lib/MIMETypes') win.content.style.display = 'flex' diff --git a/src/system/apps/ImageViewer.ts b/src/system/apps/ImageViewer.ts index 499cf92..6ef9d82 100644 --- a/src/system/apps/ImageViewer.ts +++ b/src/system/apps/ImageViewer.ts @@ -19,7 +19,7 @@ const ImageViewer: Process = { }, process) }) - const fs = await process.loadLibrary('lib/VirtualFS') + const fs = process.fs const MIMETypes: Record = await process.loadLibrary('lib/MIMETypes') const HTML = await process.loadLibrary('lib/HTML') diff --git a/src/system/apps/Settings.ts b/src/system/apps/Settings.ts index 1df8720..76a6eb5 100644 --- a/src/system/apps/Settings.ts +++ b/src/system/apps/Settings.ts @@ -24,7 +24,7 @@ const Settings: Process = { ) }) - const fs = await process.loadLibrary('lib/VirtualFS') + const { fs } = process const HTML = await process.loadLibrary('lib/HTML') const { Input, Button } = await process.loadLibrary('lib/Components') @@ -50,17 +50,20 @@ const Settings: Process = { }) .appendMany( input, - Button.new().text('Save').on('click', async () => { + Button.new().text('Save').on('click', () => { config[item] = input.getValue() process.kernel.setConfig(config) - await fs.writeFile('/etc/flow', stringify(config)) - document.dispatchEvent( - new CustomEvent('config_update', { - detail: { - config - } + fs.writeFile('/etc/flow', stringify(config)) + .then(() => { + document.dispatchEvent( + new CustomEvent('config_update', { + detail: { + config + } + }) + ) }) - ) + .catch(e => console.error(e)) }) ) ) diff --git a/src/system/apps/Store.ts b/src/system/apps/Store.ts index 8d1d64a..b8c1b6b 100644 --- a/src/system/apps/Store.ts +++ b/src/system/apps/Store.ts @@ -19,7 +19,7 @@ const Store: Process = { }, process) }) - const fs = await process.loadLibrary('lib/VirtualFS') + const { fs } = process const HTML = await process.loadLibrary('lib/HTML') const { Button, Icon } = await process.loadLibrary('lib/Components') diff --git a/src/system/lib/VirtualFS.ts b/src/system/lib/VirtualFS.ts deleted file mode 100644 index ed81d8b..0000000 --- a/src/system/lib/VirtualFS.ts +++ /dev/null @@ -1,461 +0,0 @@ -import Kernel from '../../kernel' -import ProcessLib from '../../structures/ProcessLib' -import { Directory, Errors, File, Library, Permission, Stats } from '../../types' - -console.debug = (...args: any[]) => { - console.log('[VirtualFS]', ...args) -} - -export const defaultFS: { root: Directory } = { - root: { - type: 'directory', - deleteable: false, - permission: Permission.SYSTEM, - children: { - home: { - type: 'directory', - deleteable: false, - permission: Permission.SYSTEM, - children: { - Downloads: { - type: 'directory', - deleteable: false, - permission: Permission.USER, - children: {} - }, - Applications: { - type: 'directory', - deleteable: false, - permission: Permission.USER, - children: { - 'Info.app': { - type: 'file', - deleteable: true, - permission: Permission.USER, - content: Buffer.from('apps/Info') - }, - 'Manager.app': { - type: 'file', - deleteable: true, - permission: Permission.USER, - content: Buffer.from('apps/Manager') - }, - 'Store.app': { - type: 'file', - deleteable: true, - permission: Permission.USER, - content: Buffer.from('apps/Store') - }, - 'TaskManager.app': { - type: 'file', - deleteable: true, - permission: Permission.USER, - content: Buffer.from('apps/TaskManager') - }, - 'Browser.app': { - type: 'file', - deleteable: true, - permission: Permission.USER, - content: Buffer.from('apps/Browser') - }, - 'ImageViewer.app': { - type: 'file', - deleteable: true, - permission: Permission.USER, - content: Buffer.from('apps/ImageViewer') - }, - 'Files.app': { - type: 'file', - deleteable: true, - permission: Permission.USER, - content: Buffer.from('apps/Files') - }, - 'Editor.app': { - type: 'file', - deleteable: true, - permission: Permission.USER, - content: Buffer.from('apps/Editor') - }, - 'Settings.app': { - type: 'file', - deleteable: true, - permission: Permission.USER, - content: Buffer.from('apps/Settings') - } - } - }, - Desktop: { - type: 'directory', - deleteable: false, - permission: Permission.USER, - children: { - 'README.md': { - type: 'file', - deleteable: true, - permission: Permission.USER, - content: Buffer.from('# Welcome to FlowOS!') - }, - 'Info.lnk': { - type: 'file', - deleteable: true, - permission: Permission.USER, - content: Buffer.from('/home/Applications/Info.app') - }, - 'Manager.lnk': { - type: 'file', - deleteable: true, - permission: Permission.USER, - content: Buffer.from('/home/Applications/Manager.app') - }, - 'Store.lnk': { - type: 'file', - deleteable: true, - permission: Permission.USER, - content: Buffer.from('/home/Applications/Store.app') - }, - 'TaskManager.lnk': { - type: 'file', - deleteable: true, - permission: Permission.USER, - content: Buffer.from('/home/Applications/TaskManager.app') - }, - 'Browser.lnk': { - type: 'file', - deleteable: true, - permission: Permission.USER, - content: Buffer.from('/home/Applications/Browser.app') - }, - 'ImageViewer.lnk': { - type: 'file', - deleteable: true, - permission: Permission.USER, - content: Buffer.from('/home/Applications/ImageViewer.app') - }, - 'Files.lnk': { - type: 'file', - deleteable: true, - permission: Permission.USER, - content: Buffer.from('/home/Applications/Files.app') - }, - 'Editor.lnk': { - type: 'file', - deleteable: true, - permission: Permission.USER, - content: Buffer.from('/home/Applications/Editor.app') - } - } - }, - Pictures: { - type: 'directory', - deleteable: false, - permission: Permission.USER, - children: {} - }, - Videos: { - type: 'directory', - deleteable: false, - permission: Permission.USER, - children: {} - }, - Documents: { - type: 'directory', - deleteable: false, - permission: Permission.USER, - children: {} - }, - Music: { - type: 'directory', - deleteable: false, - permission: Permission.USER, - children: {} - } - } - }, - var: { - type: 'directory', - deleteable: false, - permission: Permission.SYSTEM, - children: {} - }, - etc: { - type: 'directory', - deleteable: false, - permission: Permission.SYSTEM, - children: { - flow: { - type: 'file', - deleteable: false, - permission: Permission.ELEVATED, - content: Buffer.from([ - 'SERVER=https://server.flow-works.me', - '24HOUR=FALSE' - ].join('\n')) - }, - hostname: { - type: 'file', - deleteable: false, - permission: Permission.ELEVATED, - content: Buffer.from('flow') - } - } - }, - opt: { - type: 'directory', - deleteable: false, - permission: Permission.SYSTEM, - children: { - apps: { - type: 'directory', - deleteable: false, - permission: Permission.SYSTEM, - children: {} - } - } - } - } - } -} - -export const setFileSystem = async (fileSystemObject: { root: Directory }): Promise => { - fileSystem = fileSystemObject -} - -export let db: IDBDatabase -let fileSystem: { root: Directory } -let kernel: Kernel -let process: ProcessLib - -export const initializeDatabase = async (dbName: string): Promise => { - return await new Promise((resolve, reject) => { - const request = window.indexedDB.open(dbName) - - request.onupgradeneeded = (event: Event) => { - const target = event.target as IDBRequest - const db = target.result - db.createObjectStore('fs') - } - - request.onerror = (event: Event) => { - reject(new Error('[VirtualFS] Error opening database.')) - } - - request.onsuccess = () => { - db = request.result - resolve(true) - } - }) -} - -export const read = async (): Promise => { - const transaction = db.transaction(['fs'], 'readonly') - const store = transaction.objectStore('fs') - const getRequest = store.get('fs') - - return await new Promise((resolve, reject) => { - getRequest.onsuccess = () => { - resolve(getRequest.result) - } - - getRequest.onerror = () => { - reject(getRequest.error) - } - }) -} - -export const write = async (fileSystemObject: { root: Directory }): Promise => { - fileSystem = fileSystemObject - await save() -} - -const save = async (): Promise => { - const transaction = db.transaction(['fs'], 'readwrite') - const store = transaction.objectStore('fs') - const putRequest = store.put(fileSystem, 'fs') - - return await new Promise((resolve, reject) => { - putRequest.onsuccess = () => { - document.dispatchEvent(new CustomEvent('fs_update', {})) - resolve() - } - - putRequest.onerror = () => { - reject(putRequest.error) - } - }) -} - -const handlePermissions = async (path: string): Promise => { - let { current } = (await navigatePath(path)) - - if (current === undefined) current = (await navigatePathParent(path)).current - - if (current.permission === Permission.USER && current.permission > process.permission) { - const uac = await kernel.startExecutable('UserAccessControl', Permission.SYSTEM, { type: 'fs', process, path }) - if (uac.value === false) { - throw new Error(Errors.EACCES) - } - } - if (current.permission === Permission.ELEVATED && current.permission > process.permission) { - const uac = await kernel.startExecutable('UserAccessControl', Permission.SYSTEM, { type: 'fs', process, path }) - if (uac.value === false) { - throw new Error(Errors.EACCES) - } - } - if (current.permission === Permission.SYSTEM && current.permission > process.permission) throw new Error(Errors.EPERM) -} - -const navigatePath = async (path: string): Promise<{ current: Directory | File, parts: string[] }> => { - const parts = path.split('/').filter(x => x !== '') - let current = fileSystem.root - for (const part of parts) { - current = current.children[part] as Directory - } - return { current, parts } -} - -const navigatePathParent = async (path: string): Promise<{ current: Directory, parts: string[], filename: string }> => { - const parts = path.split('/').filter(x => x !== '') - const filename = parts.pop() as string - let current = fileSystem.root - for (const part of parts) { - current = current.children[part] as Directory - } - return { current, parts, filename } -} - -const VirtualFS: Library = { - config: { - name: 'VirtualFS', - type: 'library', - targetVer: '1.0.0-indev.0' - }, - init: (l, k, p) => { - kernel = k - process = p - }, - data: { - unlink: async (path: string): Promise => { - const { current, filename } = await navigatePathParent(path) - - if (!current.children[filename].deleteable) throw new Error(Errors.EPERM) - await handlePermissions(path) - - Reflect.deleteProperty(current.children, filename) - - console.debug(`unlink ${path}`) - await save() - }, - readFile: async (path: string): Promise => { - const { current } = await navigatePath(path) - - await handlePermissions(path) - - if (current.type !== 'file') throw new Error(Errors.EISDIR) - - console.debug(`read ${path}`) - return current.content - }, - writeFile: async (path: string, content: string | Buffer): Promise => { - const { current, filename } = await navigatePathParent(path) - - let permission - - if (typeof current.children[filename] === 'undefined') { - permission = Permission.USER - } else { - await handlePermissions(path) - permission = current.children[filename].permission - } - - current.children[filename] = { - type: 'file', - deleteable: true, - permission, - content: Buffer.from(content) - } - - console.debug(`write ${path}`) - await save() - }, - mkdir: async (path: string): Promise => { - const { current, filename } = await navigatePathParent(path) - - let permission - - if (typeof current.children[filename] === 'undefined') { - permission = Permission.USER - } else { - await handlePermissions(path) - permission = current.children[filename].permission - } - - current.children[filename] = { - type: 'directory', - deleteable: true, - permission, - children: {} - } - - console.debug(`mkdir ${path}`) - await save() - }, - rmdir: async (path: string): Promise => { - const { current, filename } = await navigatePathParent(path) - - if (!current.deleteable) throw new Error(Errors.EPERM) - await handlePermissions(path) - - if (current.children[filename].type !== 'directory') throw new Error(Errors.ENOTDIR) - - Reflect.deleteProperty(current.children, filename) - - console.debug(`rmdir ${path}`) - await save() - }, - readdir: async (path: string): Promise => { - const { current } = await navigatePath(path) - - if (current.type === 'file') throw new Error(Errors.ENOTDIR) - const result = await Promise.all(Object.keys(current.children ?? {})) - - console.debug(`readdir ${path}`) - return result - }, - stat: async (path: string): Promise => { - const { current } = await navigatePath(path) - - console.debug(`stat ${path}`) - return { - isDirectory: () => current.type === 'directory', - isFile: () => current.type === 'file' - } - }, - rename: async (oldPath: string, newPath: string): Promise => { - const { current: oldCurrent, filename: oldFilename } = await navigatePathParent(oldPath) - const { current: newCurrent, filename: newFilename } = await navigatePathParent(newPath) - - if (!oldCurrent.deleteable) throw new Error(Errors.EPERM) - if (!newCurrent.deleteable) throw new Error(Errors.EPERM) - - await handlePermissions(oldPath) - await handlePermissions(newPath) - - newCurrent.children[newFilename] = oldCurrent.children[oldFilename] - Reflect.deleteProperty(oldCurrent.children, oldFilename) - - console.debug(`rename ${oldPath} -> ${newPath}`) - await save() - }, - exists: async (path: string): Promise => { - console.debug(`exists ${path}`) - try { - const { current } = await navigatePath(path) - return current !== undefined - } catch (e) { - return false - } - } - } -} - -export default VirtualFS diff --git a/src/types.ts b/src/types.ts index a013c40..67e54b7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,7 +3,6 @@ import Kernel from './kernel' import FlowWindow from './structures/FlowWindow' import LibraryLib from './structures/LibraryLib' import ProcessLib from './structures/ProcessLib' -import Components from './system/lib/Components' import MIMETypes from './system/lib/MIMETypes' export interface AppClosedEvent extends CustomEvent { @@ -173,15 +172,20 @@ export interface StatusBar { updateIcon: (ms: number) => void } +export interface IComponents { + [key: string]: { + new: (...args: any[]) => InstanceType + } +} + export type LoadedLibrary = - T extends 'lib/VirtualFS' ? FileSystem : T extends 'lib/WindowManager' ? WindowManager : T extends 'lib/HTML' ? typeof HTML : T extends 'lib/Launcher' ? Launcher : T extends 'lib/XOR' ? XOR : T extends 'lib/StatusBar' ? StatusBar : T extends 'lib/MIMETypes' ? typeof MIMETypes.data : - T extends 'lib/Components' ? typeof Components.data : + T extends 'lib/Components' ? IComponents : any export type LibraryPath = 'lib/VirtualFS' | 'lib/WindowManager' | string diff --git a/src/utils.ts b/src/utils.ts index 99261d3..b19d966 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -16,18 +16,16 @@ export const getTime = async (): Promise => { if (hours === 0) { hours = 12 } else if (hours > 12) { - hours = hours % 12 + hours %= 12 } } hours = (hours < 10) ? `0${hours}` : hours minutes = (minutes < 10) ? `0${minutes}` : minutes - const timeString = use24hrs + return use24hrs ? `${hours}:${minutes}` : `${hours}:${minutes} ${period}` - - return timeString } /** diff --git a/typedoc.config.js b/typedoc.config.js index d3f00ed..ed13ba7 100644 --- a/typedoc.config.js +++ b/typedoc.config.js @@ -3,6 +3,6 @@ module.exports = { name: 'FlowOS', plugin: ['typedoc-material-theme'], themeColor: '#1e1e2e', - entryPoints: ['src/kernel.ts'], + entryPoints: ['src/bootloader.ts'], entryPointStrategy: 'expand' } diff --git a/vite.config.js b/vite.config.js index e30b2d7..ba4a959 100644 --- a/vite.config.js +++ b/vite.config.js @@ -29,5 +29,8 @@ export default defineConfig({ disable: false, verbose: true }) - ] + ], + build: { + target: 'ESNEXT' + } })