Merge pull request #154 from Flow-Works/dev
[🪢] Merge `dev` into `master`
This commit is contained in:
commit
518e821014
19 changed files with 1006 additions and 591 deletions
|
|
@ -7,6 +7,6 @@
|
|||
<link rel="shortcut icon" href="./src/assets/flow.png" type="image/png">
|
||||
</head>
|
||||
<body>
|
||||
<script src="./src/kernel.ts" type="module"></script>
|
||||
<script src="./src/bootloader.ts" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
163
package-lock.json
generated
163
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
126
src/bootloader.ts
Normal file
126
src/bootloader.ts
Normal file
|
|
@ -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('<style>* { box-sizing: border-box }</style>')
|
||||
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)
|
||||
}
|
||||
152
src/kernel.ts
152
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<boolean | any> => {
|
||||
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<void> {
|
||||
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
|
||||
async boot (boot: HTML, progress: HTML, args: URLSearchParams): Promise<void> {
|
||||
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')
|
||||
}
|
||||
|
||||
setConfig (data: any, process: ProcessLib): void {
|
||||
if (process.permission === Permission.SYSTEM) {
|
||||
this.config = data
|
||||
document.dispatchEvent(new CustomEvent('config_update', {
|
||||
detail: {
|
||||
config: this.config
|
||||
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))
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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<Executable>
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
149
src/system/Desktop.ts
Normal file
149
src/system/Desktop.ts
Normal file
|
|
@ -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<void> => {
|
||||
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(`
|
||||
<div class="outlined" data-toolbar-id="start"><span class="material-symbols-rounded">space_dashboard</span></div>
|
||||
|
||||
<div data-toolbar-id="apps"></div>
|
||||
<flex></flex>
|
||||
<div class="outlined" data-toolbar-id="plugins"><span class="material-symbols-rounded">expand_less</span></div>
|
||||
<div class="outlined" data-toolbar-id="controls">
|
||||
<span class="material-symbols-rounded battery">battery_2_bar</span>
|
||||
<span class="material-symbols-rounded signal">signal_cellular_4_bar</span>
|
||||
</div>
|
||||
<div class="outlined" data-toolbar-id="calendar"></div>
|
||||
|
||||
`)
|
||||
|
||||
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<void> {
|
||||
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
|
||||
433
src/system/VirtualFS.ts
Normal file
433
src/system/VirtualFS.ts
Normal file
|
|
@ -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<VirtualFS> {
|
||||
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<any> => {
|
||||
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<void> => {
|
||||
this.fileSystem = fileSystemObject
|
||||
await this.save()
|
||||
}
|
||||
|
||||
private readonly save = async (): Promise<void> => {
|
||||
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<void> => {
|
||||
const { current } = await this.navigatePath(path)
|
||||
if (current.permission === Permission.SYSTEM) throw new Error(Errors.EPERM)
|
||||
}
|
||||
|
||||
unlink = async (path: string): Promise<void> => {
|
||||
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<Buffer> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<string[]> => {
|
||||
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<Stats> => {
|
||||
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<void> => {
|
||||
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<boolean> => {
|
||||
console.debug(`exists ${path}`)
|
||||
try {
|
||||
const { current } = await this.navigatePath(path)
|
||||
return current !== undefined
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default VirtualFS
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ const ImageViewer: Process = {
|
|||
}, process)
|
||||
})
|
||||
|
||||
const fs = await process.loadLibrary('lib/VirtualFS')
|
||||
const fs = process.fs
|
||||
const MIMETypes: Record<string, { type: string }> = await process.loadLibrary('lib/MIMETypes')
|
||||
const HTML = await process.loadLibrary('lib/HTML')
|
||||
|
||||
|
|
|
|||
|
|
@ -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,10 +50,11 @@ 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))
|
||||
fs.writeFile('/etc/flow', stringify(config))
|
||||
.then(() => {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent('config_update', {
|
||||
detail: {
|
||||
|
|
@ -62,6 +63,8 @@ const Settings: Process = {
|
|||
})
|
||||
)
|
||||
})
|
||||
.catch(e => console.error(e))
|
||||
})
|
||||
)
|
||||
)
|
||||
.appendTo(win.content)
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
||||
|
|
|
|||
|
|
@ -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<void> => {
|
||||
fileSystem = fileSystemObject
|
||||
}
|
||||
|
||||
export let db: IDBDatabase
|
||||
let fileSystem: { root: Directory }
|
||||
let kernel: Kernel
|
||||
let process: ProcessLib
|
||||
|
||||
export const initializeDatabase = async (dbName: string): Promise<boolean> => {
|
||||
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<any> => {
|
||||
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<void> => {
|
||||
fileSystem = fileSystemObject
|
||||
await save()
|
||||
}
|
||||
|
||||
const save = async (): Promise<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<Buffer> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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<string[]> => {
|
||||
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<Stats> => {
|
||||
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<void> => {
|
||||
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<boolean> => {
|
||||
console.debug(`exists ${path}`)
|
||||
try {
|
||||
const { current } = await navigatePath(path)
|
||||
return current !== undefined
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default VirtualFS
|
||||
10
src/types.ts
10
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<typeof HTML>
|
||||
}
|
||||
}
|
||||
|
||||
export type LoadedLibrary<T> =
|
||||
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
|
||||
|
|
|
|||
|
|
@ -16,18 +16,16 @@ export const getTime = async (): Promise<string> => {
|
|||
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
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@ module.exports = {
|
|||
name: 'FlowOS',
|
||||
plugin: ['typedoc-material-theme'],
|
||||
themeColor: '#1e1e2e',
|
||||
entryPoints: ['src/kernel.ts'],
|
||||
entryPoints: ['src/bootloader.ts'],
|
||||
entryPointStrategy: 'expand'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,5 +29,8 @@ export default defineConfig({
|
|||
disable: false,
|
||||
verbose: true
|
||||
})
|
||||
]
|
||||
],
|
||||
build: {
|
||||
target: 'ESNEXT'
|
||||
}
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue