├── .editorconfig
├── .eslintignore
├── .github
└── FUNDING.yml
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc
├── LICENSE
├── README.md
├── apps
└── .gitkeep
├── desktop
├── .npmrc
├── app
│ ├── app.vue
│ └── public
│ │ └── .gitkeep
├── i18n
│ └── i18n.config.ts
├── nuxt.config.ts
├── owd.config.ts
├── package.json
├── project.json
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.spec.json
├── tsconfig.tsbuildinfo
└── vitest.config.ts
├── eslint.config.mjs
├── nx.json
├── package.json
├── packages
└── core
│ ├── .gitignore
│ ├── .npmrc
│ ├── CHANGELOG.md
│ ├── LICENSE
│ ├── README.md
│ ├── bin
│ └── owd.js
│ ├── index.ts
│ ├── module.ts
│ ├── package.json
│ ├── project.json
│ ├── runtime
│ ├── components
│ │ └── Core
│ │ │ ├── Application
│ │ │ ├── CoreApplicationRender.vue
│ │ │ └── CoreApplicationWindowsRender.vue
│ │ │ ├── Background
│ │ │ └── CoreBackground.vue
│ │ │ ├── Desktop
│ │ │ └── CoreDesktop.vue
│ │ │ ├── Explorer
│ │ │ ├── CoreExplorerFile.vue
│ │ │ └── CoreExplorerFolder.vue
│ │ │ ├── Time
│ │ │ └── CoreTime.vue
│ │ │ └── Window
│ │ │ ├── CoreWindow.vue
│ │ │ ├── CoreWindowContent.vue
│ │ │ └── CoreWindowNav.vue
│ ├── composables
│ │ ├── useApplicationEntries.ts
│ │ ├── useApplicationManager.ts
│ │ ├── useClipboardFs.ts
│ │ ├── useDesktopManager.ts
│ │ └── useTerminalManager.ts
│ ├── core
│ │ ├── controllers
│ │ │ ├── ApplicationController.ts
│ │ │ └── WindowController.ts
│ │ └── managers
│ │ │ ├── ApplicationManager.ts
│ │ │ ├── DesktopManager.ts
│ │ │ └── TerminalManager.ts
│ ├── plugins
│ │ └── resize.client.ts
│ ├── stores
│ │ ├── storeApplicationMeta.ts
│ │ ├── storeApplicationWindows.ts
│ │ ├── storeDesktop.ts
│ │ ├── storeDesktopVolume.ts
│ │ ├── storeDesktopWindow.ts
│ │ └── storeDesktopWorkspace.ts
│ └── utils
│ │ ├── utilApp.ts
│ │ ├── utilCommon.ts
│ │ ├── utilDebug.ts
│ │ ├── utilDesktop.ts
│ │ ├── utilTerminal.ts
│ │ └── utilWindow.ts
│ ├── test
│ ├── basic.test.ts
│ └── fixtures
│ │ └── basic
│ │ ├── app.vue
│ │ ├── nuxt.config.ts
│ │ └── package.json
│ ├── tsconfig.app.json
│ ├── tsconfig.json
│ ├── tsconfig.spec.json
│ ├── types
│ └── index.d.ts
│ └── vitest.config.ts
├── pnpm-workspace.yaml
├── template
├── .editorconfig
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc
├── LICENSE
├── README.md
├── apps
│ └── .gitkeep
├── desktop
│ ├── .npmrc
│ ├── app
│ │ ├── app.vue
│ │ ├── assets
│ │ │ └── styles
│ │ │ │ └── index.scss
│ │ ├── pages
│ │ │ └── index.vue
│ │ └── plugins
│ │ │ └── .gitkeep
│ ├── i18n
│ │ └── i18n.config.ts
│ ├── nuxt.config.ts
│ ├── owd.config.ts
│ ├── package.json
│ ├── project.json
│ ├── tsconfig.app.json
│ ├── tsconfig.json
│ └── tsconfig.spec.json
├── nx.json
├── package.json
├── packages
│ └── .gitkeep
├── pnpm-workspace.yaml
├── presets
│ └── with-tests
│ │ ├── .prettierignore
│ │ ├── .prettierrc
│ │ ├── desktop
│ │ ├── eslint.config.mjs
│ │ ├── tsconfig.tsbuildinfo
│ │ └── vitest.config.ts
│ │ ├── eslint.config.mjs
│ │ └── vitest.workspace.ts
├── themes
│ └── .gitkeep
├── tsconfig.base.json
└── tsconfig.json
├── themes
└── .gitkeep
├── tsconfig.base.json
├── tsconfig.json
└── vitest.workspace.ts
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | **/__placeholder__.js
2 | **/__placeholder__.ts
3 | dist
4 | node_modules
5 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: owdproject
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
2 |
3 | # compiled output
4 | dist
5 | tmp
6 | out-tsc
7 |
8 | # dependencies
9 | node_modules
10 |
11 | # IDEs and editors
12 | /.idea
13 | .project
14 | .classpath
15 | .c9/
16 | *.launch
17 | .settings/
18 | *.sublime-workspace
19 |
20 | # IDE - VSCode
21 | .vscode/*
22 | !.vscode/settings.json
23 | !.vscode/tasks.json
24 | !.vscode/launch.json
25 | !.vscode/extensions.json
26 |
27 | # misc
28 | /.sass-cache
29 | /connect.lock
30 | /coverage
31 | /libpeerconnection.log
32 | npm-debug.log
33 | yarn-error.log
34 | testem.log
35 | /typings
36 |
37 | # System Files
38 | .DS_Store
39 | Thumbs.db
40 |
41 | .nx/cache
42 | .nx/workspace-data
43 |
44 | # Nuxt dev/build outputs
45 | .output
46 | .data
47 | .nuxt
48 | .nitro
49 | .cache
50 | vite.config.*.timestamp*
51 | vitest.config.*.timestamp*
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | shamefully-hoist=true
2 | strict-peer-dependencies=false
3 | link-workspace-packages=true
4 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Add files here to ignore them from prettier formatting
2 | /dist
3 | /coverage
4 | /.nx/cache
5 | /.nx/workspace-data
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "semi": false,
4 | "tabWidth": 2,
5 | "useTabs": false
6 | }
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Open Web Desktop Team ~ github.com/owdproject
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
19 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
20 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Open Web Desktop
5 |
6 | A modular framework for building web-based desktop experiences.
7 |
8 |
9 | ## Overview
10 |
11 | Open Web Desktop (OWD) is a framework designed to provide a simple environment for building web-based desktop experiences. It's built with Vue.js & TypeScript, and it leverages the extensible Nuxt.js architecture.
12 |
13 | [Demo](https://atproto-os.pages.dev/) · [Community](https://discord.gg/zPNaN2HAaA) · [Documentation](https://owdproject.org/)
14 |
15 | ## Features
16 |
17 | - Open-source web desktop environment built with Nuxt.js
18 | - Fully extendable through themes, apps, and modules
19 | - Bundled with popular Vue.js libraries like Pinia and VueUse
20 | - Designed to make the most of the Nuxt.js ecosystem
21 | - Styled with PrimeVue and Tailwind for a consistent UI
22 | - Fully localizable with nuxt-i18n support
23 |
24 | ## Getting started
25 |
26 | Bootstrap a new project by running:
27 |
28 | ```bash
29 | npm create owd
30 | ```
31 |
32 | Once the process is done, you can start to develop:
33 |
34 | ```bash
35 | cd owd-client
36 |
37 | # Run the dev server with hot-reload
38 | pnpm install
39 | pnpm run dev
40 |
41 | # Build for production
42 | pnpm run generate
43 | ```
44 |
45 | ## Extend your desktop
46 |
47 | Thanks to Tailwind and PrimeVue, you can create custom themes from scratch and ensure a consistent look across all apps. Each theme defines its own style, making your desktop both cohesive and uniquely yours.
48 |
49 | [Applications](https://github.com/topics/owd-apps) · [Modules](https://github.com/topics/owd-modules) · [Themes](https://github.com/topics/owd-themes)
50 |
51 | ### 🧩 Install an application
52 |
53 | You can discover new apps by searching for the [owd-apps](https://github.com/topics/owd-apps) tag on GitHub.
54 |
55 | For example, to install the To-do app:
56 |
57 | ```bash
58 | owd install-app @owdproject/app-todo
59 | ```
60 |
61 | This will install the package and automatically register it in your desktop configuration.
62 |
63 | ### 🧩 Install a module
64 |
65 | You can discover new modules by searching for the [owd-modules](https://github.com/topics/owd-modules) tag on GitHub.
66 |
67 | For example, to install the session persistence module:
68 |
69 | ```bash
70 | owd install-module @owdproject/module-pinia-localforage
71 | ```
72 |
73 | ### 🖥️ Themes
74 |
75 | Themes are full desktop environments that style all UI components independently using [PrimeVue](https://primevue.org/).
76 | Each theme provides a unique look and feel while maintaining consistent functionality across all applications.
77 |
78 | You can discover new themes by searching for the [owd-themes](https://github.com/topics/owd-themes) tag on GitHub.
79 |
80 | ```bash
81 | owd install-theme @owdproject/theme-gnome
82 | ```
83 |
84 | ## Sponsors
85 |
86 | Be the first to support this project and help us keep it growing! [Sponsor the project](https://github.com/sponsors/owdproject)
87 |
88 | ## License
89 |
90 | Open Web Desktop is released under the [MIT License](LICENSE).
91 |
--------------------------------------------------------------------------------
/apps/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/owdproject/client/80e6e3eb3b278762afeb631163e129299099acd4/apps/.gitkeep
--------------------------------------------------------------------------------
/desktop/.npmrc:
--------------------------------------------------------------------------------
1 | shamefully-hoist=true
2 | strict-peer-dependencies=false
3 |
--------------------------------------------------------------------------------
/desktop/app/app.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/desktop/app/public/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/owdproject/client/80e6e3eb3b278762afeb631163e129299099acd4/desktop/app/public/.gitkeep
--------------------------------------------------------------------------------
/desktop/i18n/i18n.config.ts:
--------------------------------------------------------------------------------
1 | export default defineI18nConfig(() => {
2 | return {
3 | locale: 'en',
4 | messages: {
5 | en: {
6 | title: 'atproto OS',
7 | },
8 | },
9 | }
10 | })
11 |
--------------------------------------------------------------------------------
/desktop/nuxt.config.ts:
--------------------------------------------------------------------------------
1 | export default defineNuxtConfig({
2 | workspaceDir: '../../',
3 |
4 | ssr: false,
5 |
6 | devServer: {
7 | host: '127.0.0.1',
8 | },
9 |
10 | modules: ['@owdproject/core'],
11 |
12 | i18n: {
13 | strategy: 'no_prefix',
14 | },
15 |
16 | compatibilityDate: '2025-05-15',
17 |
18 | future: {
19 | compatibilityVersion: 4,
20 | },
21 |
22 | devtools: {
23 | enabled: false,
24 | },
25 | })
26 |
--------------------------------------------------------------------------------
/desktop/owd.config.ts:
--------------------------------------------------------------------------------
1 | import { defineDesktopConfig } from '@owdproject/core'
2 |
3 | export default defineDesktopConfig({
4 | theme: '@owdproject/theme-win95',
5 | apps: [
6 | '@owdproject/app-about',
7 | ],
8 | modules: [
9 | ],
10 | })
11 |
--------------------------------------------------------------------------------
/desktop/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@owdproject/client",
3 | "private": true,
4 | "nx": {
5 | "name": "desktop"
6 | },
7 | "scripts": {
8 | "build": "nuxt generate",
9 | "dev": "nuxt dev --host",
10 | "generate": "nuxt generate --dev",
11 | "postinstall": "nuxt prepare",
12 | "preview": "nuxt preview"
13 | },
14 | "dependencies": {
15 | "@owdproject/app-about": "0.1.1",
16 | "@owdproject/core": "^3.1.3",
17 | "@owdproject/theme-win95": "0.1.1"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/desktop/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "desktop",
3 | "targets": {
4 | "serve": {
5 | "executor": "nx:run-commands",
6 | "options": {
7 | "commands": ["pnpm install && pnpm run dev"],
8 | "cwd": "desktop"
9 | }
10 | },
11 | "generate": {
12 | "executor": "nx:run-commands",
13 | "options": {
14 | "commands": ["pnpm install && pnpm run generate"],
15 | "cwd": "desktop"
16 | }
17 | },
18 | "install-app": {
19 | "executor": "@owdproject/nx:install-app"
20 | },
21 | "install-module": {
22 | "executor": "@owdproject/nx:install-module"
23 | },
24 | "install-theme": {
25 | "executor": "@owdproject/nx:install-theme"
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/desktop/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.base.json",
3 | "compilerOptions": {
4 | "outDir": "dist",
5 | "rootDir": "src",
6 | "jsx": "preserve",
7 | "jsxImportSource": "vue",
8 | "resolveJsonModule": true,
9 | "tsBuildInfoFile": "dist/tsconfig.app.tsbuildinfo"
10 | },
11 | "include": [".nuxt/nuxt.d.ts", "app/**/*"],
12 | "exclude": [
13 | "out-tsc",
14 | "dist",
15 | "vite.config.ts",
16 | "vite.config.mts",
17 | "vitest.config.ts",
18 | "vitest.config.mts",
19 | "src/**/*.test.ts",
20 | "src/**/*.spec.ts",
21 | "src/**/*.test.tsx",
22 | "src/**/*.spec.tsx",
23 | "src/**/*.test.js",
24 | "src/**/*.spec.js",
25 | "src/**/*.test.jsx",
26 | "src/**/*.spec.jsx",
27 | "eslint.config.js",
28 | "eslint.config.cjs",
29 | "eslint.config.mjs"
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/desktop/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | {
5 | "path": "./tsconfig.app.json"
6 | },
7 | {
8 | "path": "./tsconfig.spec.json"
9 | }
10 | ],
11 | "extends": "../tsconfig.base.json"
12 | }
13 |
--------------------------------------------------------------------------------
/desktop/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.base.json",
3 | "compilerOptions": {
4 | "outDir": "./out-tsc/vitest",
5 | "types": [
6 | "vitest/globals",
7 | "vitest/importMeta",
8 | "vite/client",
9 | "node",
10 | "vitest"
11 | ],
12 | "jsx": "preserve",
13 | "jsxImportSource": "vue",
14 | "resolveJsonModule": true
15 | },
16 | "include": [
17 | ".nuxt/nuxt.d.ts",
18 | "vite.config.ts",
19 | "vite.config.mts",
20 | "vitest.config.ts",
21 | "vitest.config.mts",
22 | "src/**/*.test.ts",
23 | "src/**/*.spec.ts",
24 | "src/**/*.test.tsx",
25 | "src/**/*.spec.tsx",
26 | "src/**/*.test.js",
27 | "src/**/*.spec.js",
28 | "src/**/*.test.jsx",
29 | "src/**/*.spec.jsx",
30 | "src/**/*.d.ts"
31 | ],
32 | "references": [
33 | {
34 | "path": "./tsconfig.app.json"
35 | }
36 | ]
37 | }
38 |
--------------------------------------------------------------------------------
/desktop/tsconfig.tsbuildinfo:
--------------------------------------------------------------------------------
1 | {"fileNames":[],"fileInfos":[],"root":[],"options":{"composite":true,"declarationMap":true,"emitDeclarationOnly":true,"importHelpers":true,"module":99,"noEmitOnError":true,"noFallthroughCasesInSwitch":true,"noImplicitOverride":true,"noImplicitReturns":true,"noUnusedLocals":true,"skipLibCheck":true,"strict":true,"target":9},"version":"5.8.3"}
--------------------------------------------------------------------------------
/desktop/vitest.config.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { defineConfig } from 'vite'
3 | import vue from '@vitejs/plugin-vue'
4 |
5 | export default defineConfig(() => ({
6 | root: __dirname,
7 | cacheDir: '../../node_modules/.vite/apps/@owdproject/client',
8 | plugins: [vue()],
9 | // Uncomment this if you are using workers.
10 | // worker: {
11 | // plugins: [ nxViteTsPaths() ],
12 | // },
13 | test: {
14 | watch: false,
15 | globals: true,
16 | environment: 'jsdom',
17 | include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
18 | reporters: ['default'],
19 | coverage: {
20 | reportsDirectory: './test-output/vitest/coverage',
21 | provider: 'v8' as const,
22 | },
23 | },
24 | }))
25 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import vuePlugin from 'eslint-plugin-vue'
2 | import vueParser from 'vue-eslint-parser'
3 | import tsParser from '@typescript-eslint/parser'
4 |
5 | export default [
6 | {
7 | files: ['**/*.vue'],
8 | languageOptions: {
9 | parser: vueParser,
10 | parserOptions: {
11 | parser: tsParser,
12 | ecmaVersion: 'latest',
13 | sourceType: 'module',
14 | extraFileExtensions: ['.vue'],
15 | },
16 | },
17 | plugins: {
18 | vue: vuePlugin,
19 | },
20 | rules: {
21 | indent: ['error', 2, { SwitchCase: 1 }],
22 | semi: ['error', 'never'],
23 | quotes: ['error', 'single'],
24 | },
25 | },
26 | ]
27 |
--------------------------------------------------------------------------------
/nx.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/nx/schemas/nx-schema.json",
3 | "namedInputs": {
4 | "default": ["{projectRoot}/**/*", "sharedGlobals"],
5 | "production": [
6 | "default",
7 | "!{projectRoot}/.eslintrc.json",
8 | "!{projectRoot}/eslint.config.mjs",
9 | "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
10 | "!{projectRoot}/tsconfig.spec.json",
11 | "!{projectRoot}/src/test-setup.[jt]s"
12 | ],
13 | "sharedGlobals": []
14 | },
15 | "plugins": [
16 | {
17 | "plugin": "@owdproject/nx"
18 | },
19 | {
20 | "plugin": "@nx/js/typescript",
21 | "options": {
22 | "typecheck": {
23 | "targetName": "typecheck"
24 | },
25 | "build": {
26 | "targetName": "build",
27 | "configName": "tsconfig.lib.json",
28 | "buildDepsName": "build-deps",
29 | "watchDepsName": "watch-deps"
30 | }
31 | }
32 | },
33 | {
34 | "plugin": "@nx/eslint/plugin",
35 | "options": {
36 | "targetName": "lint"
37 | }
38 | },
39 | {
40 | "plugin": "@nx/vite/plugin",
41 | "options": {
42 | "buildTargetName": "build",
43 | "testTargetName": "test",
44 | "serveTargetName": "serve",
45 | "devTargetName": "dev",
46 | "previewTargetName": "preview",
47 | "serveStaticTargetName": "serve-static",
48 | "typecheckTargetName": "typecheck",
49 | "buildDepsTargetName": "build-deps",
50 | "watchDepsTargetName": "watch-deps"
51 | }
52 | },
53 | {
54 | "plugin": "@nx/nuxt/plugin",
55 | "options": {
56 | "buildTargetName": "build",
57 | "serveTargetName": "serve",
58 | "buildDepsTargetName": "build-deps",
59 | "watchDepsTargetName": "watch-deps"
60 | }
61 | }
62 | ],
63 | "targetDefaults": {
64 | "test": {
65 | "dependsOn": ["^build"]
66 | },
67 | "@nx/js:tsc": {
68 | "cache": true,
69 | "dependsOn": ["^build"],
70 | "inputs": ["production", "^production"]
71 | }
72 | },
73 | "release": {
74 | "projectsRelationship": "independent",
75 | "changelog": {
76 | "automaticFromRef": true,
77 | "projectChangelogs": {
78 | "renderOptions": {
79 | "authors": true,
80 | "commitReferences": true,
81 | "versionTitleDate": true,
82 | "applyUsernameToAuthors": true
83 | }
84 | },
85 | "workspaceChangelog": {
86 | "renderOptions": {
87 | "authors": true,
88 | "commitReferences": true,
89 | "versionTitleDate": true,
90 | "applyUsernameToAuthors": true
91 | }
92 | }
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "owd-client",
3 | "private": true,
4 | "description": "Open Web Desktop client",
5 | "homepage": "https://github.com/owdproject/client#readme",
6 | "bugs": {
7 | "url": "https://github.com/owdproject/client/issues"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/owdproject/client.git"
12 | },
13 | "license": "MIT",
14 | "author": {
15 | "name": "Open Web Desktop Team",
16 | "url": "https://github.com/owdproject"
17 | },
18 | "scripts": {
19 | "dev": "nx run desktop:serve",
20 | "generate": "nx run desktop:generate"
21 | },
22 | "devDependencies": {
23 | "@owdproject/nx": "^0.0.1",
24 | "@eslint/eslintrc": "^3.3.1",
25 | "@eslint/js": "^9.26.0",
26 | "@nuxt/devtools": "2.4.0",
27 | "@nuxt/eslint-config": "~1.3.0",
28 | "@nuxt/kit": "^3.17.2",
29 | "@nuxt/ui-templates": "^1.3.4",
30 | "@nx/devkit": "21.0.3",
31 | "@nx/eslint": "21.0.3",
32 | "@nx/eslint-plugin": "21.0.3",
33 | "@nx/jest": "21.0.3",
34 | "@nx/js": "^21.0.3",
35 | "@nx/nuxt": "21.0.3",
36 | "@nx/plugin": "^21.0.3",
37 | "@nx/vite": "21.0.3",
38 | "@nx/web": "21.0.3",
39 | "@nx/workspace": "21.0.3",
40 | "@swc-node/register": "~1.10.10",
41 | "@swc/cli": "~0.6.0",
42 | "@swc/core": "~1.11.24",
43 | "@swc/helpers": "~0.5.17",
44 | "@types/node": "22.15.17",
45 | "@typescript-eslint/parser": "^8.32.0",
46 | "@vitejs/plugin-vue": "^5.2.4",
47 | "@vitest/coverage-v8": "^3.1.3",
48 | "@vitest/ui": "^3.1.3",
49 | "@vue/test-utils": "^2.4.6",
50 | "eslint": "^9.26.0",
51 | "eslint-config-prettier": "^10.1.5",
52 | "h3": "^1.15.3",
53 | "jiti": "2.4.2",
54 | "jsdom": "~26.1.0",
55 | "nuxt": "^3.17.2",
56 | "nx": "21.0.3",
57 | "prettier": "^3.5.3",
58 | "tslib": "^2.8.1",
59 | "typescript": "~5.8.3",
60 | "typescript-eslint": "^8.32.0",
61 | "vite": "^6.3.5",
62 | "vitest": "^3.1.3",
63 | "vue": "^3.5.13",
64 | "vue-router": "^4.5.1",
65 | "vue-tsc": "^2.2.10"
66 | },
67 | "resolutions": {
68 | "vite": "^6.3.2"
69 | },
70 | "packageManager": "pnpm@9.15.2+sha512.93e57b0126f0df74ce6bff29680394c0ba54ec47246b9cf321f0121d8d9bb03f750a705f24edc3c1180853afd7c2c3b94196d0a3d53d3e069d9e2793ef11f321"
71 | }
72 |
--------------------------------------------------------------------------------
/packages/core/.gitignore:
--------------------------------------------------------------------------------
1 | # Dependencies
2 | node_modules
3 |
4 | # Logs
5 | *.log*
6 |
7 | # Temp directories
8 | .temp
9 | .tmp
10 | .cache
11 |
12 | # Yarn
13 | **/.yarn/cache
14 | **/.yarn/*state*
15 |
16 | # Generated dirs
17 | dist
18 |
19 | # Nuxt
20 | .nuxt
21 | .output
22 | .data
23 | .vercel_build_output
24 | .build-*
25 | .netlify
26 |
27 | # Env
28 | .env
29 |
30 | # Testing
31 | reports
32 | coverage
33 | *.lcov
34 | .nyc_output
35 |
36 | # VSCode
37 | .vscode/*
38 | !.vscode/settings.json
39 | !.vscode/tasks.json
40 | !.vscode/launch.json
41 | !.vscode/extensions.json
42 | !.vscode/*.code-snippets
43 |
44 | # Intellij idea
45 | *.iml
46 | .idea
47 |
48 | # OSX
49 | .DS_Store
50 | .AppleDouble
51 | .LSOverride
52 | .AppleDB
53 | .AppleDesktop
54 | Network Trash Folder
55 | Temporary Items
56 | .apdisk
57 |
--------------------------------------------------------------------------------
/packages/core/.npmrc:
--------------------------------------------------------------------------------
1 | shamefully-hoist=true
2 | strict-peer-dependencies=false
3 |
--------------------------------------------------------------------------------
/packages/core/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 3.1.4 (2025-05-22)
2 |
3 | ### 🚀 Features
4 |
5 | - Allow apps to have a menu ([4bfea74](https://github.com/owdproject/client/commit/4bfea74))
6 | - Add methods to better handle window changes ([54d1346](https://github.com/owdproject/client/commit/54d1346))
7 | - Check if owd.config.ts is available and valid ([4920302](https://github.com/owdproject/client/commit/4920302))
8 | - Add fit-parent to CoreWindow.vue ([59e068c](https://github.com/owdproject/client/commit/59e068c))
9 | - Add focus() to WindowController and set methods as public (deprecate actions) ([710f7be](https://github.com/owdproject/client/commit/710f7be))
10 | - Add owd global command ([f98c74c](https://github.com/owdproject/client/commit/f98c74c))
11 | - Add useClipboardFs.ts composable ([cf934f4](https://github.com/owdproject/client/commit/cf934f4))
12 | - Add window utils in ApplicationController.ts ([ffc97d0](https://github.com/owdproject/client/commit/ffc97d0))
13 | - Export most used functions from core ([6635af7](https://github.com/owdproject/client/commit/6635af7))
14 | - Merge owd.config.ts with nuxt.config.ts in module.ts ([069243c](https://github.com/owdproject/client/commit/069243c))
15 | - Implement basic owd.config.ts ([d052552](https://github.com/owdproject/client/commit/d052552))
16 | - Rewrite project ([cbbd45d](https://github.com/owdproject/client/commit/cbbd45d))
17 |
18 | ### 🩹 Fixes
19 |
20 | - Add key on CoreApplicationWindowsRender v-for ([d2e8e3c](https://github.com/owdproject/client/commit/d2e8e3c))
21 | - Check if entries property is set before normalizing in utilApp.ts ([6092c33](https://github.com/owdproject/client/commit/6092c33))
22 | - Add missing min-width ([aa0c1b5](https://github.com/owdproject/client/commit/aa0c1b5))
23 | - Minor improvements ([7f79208](https://github.com/owdproject/client/commit/7f79208))
24 | - Fix imports ([44740cd](https://github.com/owdproject/client/commit/44740cd))
25 | - Set window starting positionZ to 0 ([b99352e](https://github.com/owdproject/client/commit/b99352e))
26 |
27 | ### ❤️ Thank You
28 |
29 | - dxlliv @dxlliv
--------------------------------------------------------------------------------
/packages/core/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Open Web Desktop Team ~ github.com/owdproject
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
19 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
20 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/packages/core/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Open Web Desktop
5 |
6 | A modular framework for building web-based desktop experiences.
7 |
8 |
9 | ## Overview
10 |
11 | Open Web Desktop (OWD) is a framework designed to provide a simple environment for building web-based desktop experiences. It's built with TypeScript on top of the Nuxt.js framework.
12 |
13 | [Demo](https://atproto-os.pages.dev/) · [Community](https://discord.gg/zPNaN2HAaA) · [Documentation](https://owdproject.org/)
14 |
15 | ## Features
16 |
17 | - Open-source web desktop environment built with Nuxt.js
18 | - Fully extendable through themes, apps, and modules
19 | - Bundled with popular Vue.js libraries like Pinia and VueUse
20 | - Designed to make the most of the Nuxt.js ecosystem
21 | - Styled with PrimeVue and Tailwind for a consistent UI
22 | - Fully localizable with nuxt-i18n support
23 |
24 | ## Getting started
25 |
26 | Bootstrap a new project by running:
27 |
28 | ```bash
29 | npm create owd
30 | ```
31 |
32 | Once the process is done, you can start to develop:
33 |
34 | ```bash
35 | cd owd-client
36 |
37 | # Run the dev server with hot-reload
38 | pnpm install
39 | pnpm run dev
40 |
41 | # Build for production
42 | pnpm run generate
43 | ```
44 |
45 | ## Extend your desktop
46 |
47 | Thanks to Tailwind and PrimeVue, you can create custom themes from scratch and ensure a consistent look across all apps. Each theme defines its own style, making your desktop both cohesive and uniquely yours.
48 |
49 | [Applications](https://github.com/topics/owd-apps) · [Modules](https://github.com/topics/owd-modules) · [Themes](https://github.com/topics/owd-themes)
50 |
51 | ### 🧩 Install an application
52 |
53 | You can discover new apps by searching for the [owd-apps](https://github.com/topics/owd-apps) tag on GitHub.
54 |
55 | For example, to install the To-do app:
56 |
57 | ```bash
58 | owd install-app @owdproject/app-todo
59 | ```
60 |
61 | This will install the package and automatically register it in your desktop configuration.
62 |
63 | ### 🧩 Install a module
64 |
65 | You can discover new modules by searching for the [owd-modules](https://github.com/topics/owd-modules) tag on GitHub.
66 |
67 | For example, to install the session persistence module:
68 |
69 | ```bash
70 | owd install-module @owdproject/module-pinia-localforage
71 | ```
72 |
73 | ### 🖥️ Themes
74 |
75 | Themes are full desktop environments that style all UI components independently using [PrimeVue](https://primevue.org/).
76 | Each theme provides a unique look and feel while maintaining consistent functionality across all applications.
77 |
78 | You can discover new themes by searching for the [owd-themes](https://github.com/topics/owd-themes) tag on GitHub.
79 |
80 | ```bash
81 | owd install-theme @owdproject/theme-gnome
82 | ```
83 |
84 | ## Sponsors
85 |
86 | Be the first to support this project and help us keep it growing! [Sponsor the project](https://github.com/sponsors/owdproject)
87 |
88 | ## License
89 |
90 | Open Web Desktop is released under the [MIT License](LICENSE).
91 |
--------------------------------------------------------------------------------
/packages/core/bin/owd.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import { spawn } from 'child_process'
4 |
5 | const [, , cmd, pkgName, ...rest] = process.argv
6 |
7 | const commandMap = {
8 | 'install-app': 'desktop:install-app',
9 | 'install-module': 'desktop:install-module',
10 | 'install-theme': 'desktop:install-theme',
11 | }
12 |
13 | if (!commandMap[cmd]) {
14 | console.error(`Unknown command: ${cmd}`)
15 | process.exit(1)
16 | }
17 |
18 | if (!pkgName) {
19 | console.error('Missing package name (e.g., @owdproject/package)')
20 | process.exit(1)
21 | }
22 |
23 | const nxArgs = ['run', commandMap[cmd], `--name=${pkgName}`, ...rest]
24 |
25 | const child = spawn('pnpm', ['nx', ...nxArgs], {
26 | stdio: 'inherit',
27 | shell: true,
28 | })
29 |
--------------------------------------------------------------------------------
/packages/core/index.ts:
--------------------------------------------------------------------------------
1 | export {
2 | defineDesktopApp,
3 | defineDesktopConfig,
4 | } from './runtime/utils/utilDesktop'
5 | export { registerTailwindPath } from './runtime/utils/utilApp'
6 |
--------------------------------------------------------------------------------
/packages/core/module.ts:
--------------------------------------------------------------------------------
1 | import {
2 | defineNuxtModule,
3 | createResolver,
4 | addComponentsDir,
5 | addImportsDir,
6 | installModule,
7 | addPlugin
8 | } from '@nuxt/kit'
9 | import { deepMerge } from './runtime/utils/utilCommon'
10 | import pkg from './package.json'
11 |
12 | export default defineNuxtModule({
13 | meta: {
14 | name: 'owd-core',
15 | configKey: 'owd'
16 | },
17 | defaults: {
18 | theme: '@owdproject/theme-win95',
19 | apps: [],
20 | modules: [],
21 | },
22 | async setup(_options, _nuxt) {
23 | const { resolve } = createResolver(import.meta.url)
24 |
25 | _nuxt.options.runtimeConfig.public.desktop = {}
26 |
27 | // get open web desktop config
28 |
29 | let clientConfig
30 |
31 | try {
32 |
33 | clientConfig = (
34 | await import(_nuxt.options.rootDir + '/owd.config.ts')
35 | ).default
36 |
37 | } catch (e) {
38 | console.error('/desktop/owd.config.ts not found or invalid')
39 | return
40 | }
41 |
42 | if (!clientConfig.theme) {
43 | clientConfig.theme = '@owdproject/theme-win95'
44 | }
45 |
46 | // extend nuxt.config.ts with owd.config.ts
47 |
48 | _nuxt.options = {
49 | ..._nuxt.options,
50 | ...clientConfig
51 | }
52 |
53 | // set core version to runtime config
54 |
55 | _nuxt.options.runtimeConfig.public.coreVersion = pkg.version
56 |
57 | {
58 | // install open web desktop theme
59 |
60 | if (clientConfig.theme) {
61 | await installModule(clientConfig.theme)
62 | }
63 |
64 | // install open web desktop modules
65 |
66 | if (clientConfig.modules && Array.isArray(clientConfig.modules)) {
67 | for (const modulePath of clientConfig.modules) {
68 | await installModule(modulePath)
69 | }
70 | }
71 |
72 | // install open web desktop apps
73 |
74 | if (clientConfig.apps && Array.isArray(clientConfig.apps)) {
75 | for (const appPath of clientConfig.apps) {
76 | await installModule(appPath)
77 | }
78 | }
79 |
80 | // assign open web desktop config to runtime config
81 | _nuxt.options.runtimeConfig.public.desktop = deepMerge(
82 | _nuxt.options.runtimeConfig.public.desktop,
83 | clientConfig
84 | )
85 | }
86 |
87 | {
88 | // install primevue
89 |
90 | _nuxt.options.primevue = _nuxt.options.primevue || {}
91 | _nuxt.options.primevue.options = _nuxt.options.primevue.options || {}
92 | _nuxt.options.primevue.options.theme =
93 | _nuxt.options.primevue.options.theme || {}
94 |
95 | await installModule('@primevue/nuxt-module')
96 | }
97 |
98 | {
99 | // install tailwind
100 |
101 | const tailwindPaths =
102 | _nuxt.options.runtimeConfig.app.owd?.tailwindPaths || []
103 | tailwindPaths.push('./runtime/components/**/*.{vue,mjs,ts}') // Aggiungi sempre questo al core
104 |
105 | _nuxt.options.tailwindcss = _nuxt.options.tailwindcss || {}
106 | _nuxt.options.tailwindcss.config = _nuxt.options.tailwindcss.config || {}
107 | // @ts-ignore
108 | _nuxt.options.tailwindcss.config.content = tailwindPaths
109 |
110 | await installModule('@nuxtjs/tailwindcss', {
111 | viewer: false
112 | })
113 | }
114 |
115 | {
116 | // install pinia
117 |
118 | await installModule('@pinia/nuxt')
119 | }
120 |
121 | {
122 | // install @nuxt/fonts
123 |
124 | await installModule('@nuxt/fonts')
125 | }
126 |
127 | {
128 | // install @nuxt/icon
129 |
130 | await installModule('@nuxt/icon', {
131 | clientBundle: {
132 | scan: true,
133 | sizeLimitKb: 256
134 | }
135 | })
136 | }
137 |
138 | {
139 | // install @vueuse/nuxt
140 |
141 | await installModule('@vueuse/nuxt')
142 | }
143 |
144 | {
145 | // install @nuxtjs/i18n
146 |
147 | await installModule('@nuxtjs/i18n')
148 | }
149 |
150 | {
151 | // configure scss for vite
152 |
153 | _nuxt.hook('vite:extendConfig', (viteConfig) => {
154 | viteConfig.css = viteConfig.css || {}
155 | viteConfig.css.preprocessorOptions =
156 | viteConfig.css.preprocessorOptions || {}
157 | viteConfig.css.preprocessorOptions.scss = {
158 | api: 'modern-compiler'
159 | }
160 | })
161 | }
162 |
163 | {
164 | // add css
165 |
166 | _nuxt.options.css.push('sanitize.css')
167 | }
168 |
169 | {
170 | // add components
171 |
172 | addComponentsDir({
173 | path: resolve('./runtime/components'),
174 | prefix: '',
175 | global: true
176 | })
177 | }
178 |
179 | {
180 | addPlugin(resolve('./runtime/plugins/resize.client.ts'))
181 |
182 | // add other files
183 |
184 | addImportsDir(resolve('./runtime/composables'))
185 | addImportsDir(resolve('./runtime/core'))
186 | addImportsDir(resolve('./runtime/stores'))
187 | addImportsDir(resolve('./runtime/utils'))
188 | }
189 | }
190 | })
191 |
--------------------------------------------------------------------------------
/packages/core/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@owdproject/core",
3 | "version": "3.1.4",
4 | "keywords": [
5 | "web",
6 | "desktop",
7 | "vue",
8 | "nuxt",
9 | "web-desktop",
10 | "web-os"
11 | ],
12 | "homepage": "https://github.com/owdproject/client#readme",
13 | "bugs": {
14 | "url": "https://github.com/owdproject/client/issues"
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "git+https://github.com/owdproject/client.git"
19 | },
20 | "license": "MIT",
21 | "author": {
22 | "name": "Open Web Desktop Team",
23 | "url": "https://github.com/owdproject"
24 | },
25 | "type": "module",
26 | "types": "types/index.d.ts",
27 | "bin": {
28 | "owd": "./bin/owd.js"
29 | },
30 | "dependencies": {
31 | "@nuxt/fonts": "^0.11.3",
32 | "@nuxt/icon": "^1.12.0",
33 | "@nuxt/kit": "^3.17.2",
34 | "@nuxtjs/i18n": "^9.5.4",
35 | "@nuxtjs/tailwindcss": "^6.14.0",
36 | "@pinia/nuxt": "^0.11.0",
37 | "@primeuix/themes": "^1.1.1",
38 | "@primevue/forms": "^4.3.4",
39 | "@primevue/nuxt-module": "^4.3.4",
40 | "@vueuse/components": "^13.1.0",
41 | "@vueuse/core": "^13.1.0",
42 | "@vueuse/nuxt": "^13.1.0",
43 | "mitt": "^3.0.1",
44 | "nanoid": "^5.1.5",
45 | "nuxt": "^3.17.2",
46 | "pinia": "^3.0.2",
47 | "sanitize.css": "^13.0.0",
48 | "sass": "^1.88.0",
49 | "shellwords": "^1.0.1",
50 | "vue-resizable": "^2.1.7",
51 | "yargs-parser": "^21.1.1"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/packages/core/project.json:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/owdproject/client/80e6e3eb3b278762afeb631163e129299099acd4/packages/core/project.json
--------------------------------------------------------------------------------
/packages/core/runtime/components/Core/Application/CoreApplicationRender.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
18 |
19 |
--------------------------------------------------------------------------------
/packages/core/runtime/components/Core/Application/CoreApplicationWindowsRender.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/packages/core/runtime/components/Core/Background/CoreBackground.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
17 |
--------------------------------------------------------------------------------
/packages/core/runtime/components/Core/Desktop/CoreDesktop.vue:
--------------------------------------------------------------------------------
1 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
87 |
--------------------------------------------------------------------------------
/packages/core/runtime/components/Core/Explorer/CoreExplorerFile.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
30 |
31 |
77 |
--------------------------------------------------------------------------------
/packages/core/runtime/components/Core/Explorer/CoreExplorerFolder.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
--------------------------------------------------------------------------------
/packages/core/runtime/components/Core/Time/CoreTime.vue:
--------------------------------------------------------------------------------
1 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/packages/core/runtime/components/Core/Window/CoreWindow.vue:
--------------------------------------------------------------------------------
1 |
119 |
120 |
121 |
148 |
149 |
150 |
151 |
152 |
169 |
--------------------------------------------------------------------------------
/packages/core/runtime/components/Core/Window/CoreWindowContent.vue:
--------------------------------------------------------------------------------
1 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/packages/core/runtime/components/Core/Window/CoreWindowNav.vue:
--------------------------------------------------------------------------------
1 |
27 |
28 |
29 |
34 |
35 |
36 |
49 |
--------------------------------------------------------------------------------
/packages/core/runtime/composables/useApplicationEntries.ts:
--------------------------------------------------------------------------------
1 | import { useApplicationManager } from './useApplicationManager'
2 | import { computed } from '@vue/reactivity'
3 |
4 | type SortBy =
5 | | 'title'
6 | | 'category'
7 | | ((
8 | a: ApplicationEntryWithInherited,
9 | b: ApplicationEntryWithInherited,
10 | ) => number)
11 | type Visibility =
12 | | 'primary'
13 | | 'all'
14 | | ((entry: ApplicationEntryWithInherited) => boolean)
15 |
16 | export function useApplicationEntries() {
17 | const applicationManager = useApplicationManager()
18 |
19 | const sortedAppEntries = function (
20 | sortBy: SortBy = 'title',
21 | visibility: Visibility = 'primary',
22 | ): Ref {
23 | return computed(() => {
24 | const currentEntries = [...applicationManager.appsEntries]
25 |
26 | // filtering
27 | const filtered =
28 | typeof visibility === 'function'
29 | ? currentEntries.filter(visibility)
30 | : visibility === 'primary'
31 | ? currentEntries.filter((e) => e.visibility !== 'secondary')
32 | : currentEntries
33 |
34 | // sorting
35 | const sorted =
36 | typeof sortBy === 'function'
37 | ? filtered.sort(sortBy)
38 | : sortBy === 'title'
39 | ? filtered.sort((a, b) =>
40 | (a.title || '').localeCompare(b.title || ''),
41 | )
42 | : filtered.sort((a, b) =>
43 | (a.category || '').localeCompare(b.category || ''),
44 | )
45 |
46 | return sorted
47 | })
48 | }
49 |
50 | return { sortedAppEntries }
51 | }
52 |
--------------------------------------------------------------------------------
/packages/core/runtime/composables/useApplicationManager.ts:
--------------------------------------------------------------------------------
1 | import { ApplicationManager } from '../core/managers/ApplicationManager'
2 |
3 | const applicationManager = new ApplicationManager()
4 |
5 | export function useApplicationManager(): IApplicationManager {
6 | return applicationManager
7 | }
8 |
--------------------------------------------------------------------------------
/packages/core/runtime/composables/useClipboardFs.ts:
--------------------------------------------------------------------------------
1 | import { ref } from 'vue'
2 |
3 | type ClipboardOperation = 'copy' | 'cut'
4 |
5 | const clipboardPath = ref(null)
6 | const clipboardType = ref(null)
7 |
8 | export function useClipboardFs() {
9 | function setClipboard(path: string, type: ClipboardOperation) {
10 | clipboardPath.value = path
11 | clipboardType.value = type
12 | }
13 |
14 | function clearClipboard() {
15 | clipboardPath.value = null
16 | clipboardType.value = null
17 | }
18 |
19 | return {
20 | clipboardPath,
21 | clipboardType,
22 | setClipboard,
23 | clearClipboard,
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/packages/core/runtime/composables/useDesktopManager.ts:
--------------------------------------------------------------------------------
1 | import { DesktopManager } from '../core/managers/DesktopManager'
2 |
3 | const desktopManager = new DesktopManager()
4 |
5 | export function useDesktopManager() {
6 | return desktopManager
7 | }
8 |
--------------------------------------------------------------------------------
/packages/core/runtime/composables/useTerminalManager.ts:
--------------------------------------------------------------------------------
1 | import { TerminalManager } from '../core/managers/TerminalManager'
2 |
3 | const terminalManager = new TerminalManager()
4 |
5 | export function useTerminalManager() {
6 | return terminalManager
7 | }
8 |
--------------------------------------------------------------------------------
/packages/core/runtime/core/controllers/ApplicationController.ts:
--------------------------------------------------------------------------------
1 | import { nanoid } from 'nanoid'
2 | import { WindowController } from './WindowController'
3 | import { useApplicationManager } from '../../composables/useApplicationManager'
4 | import { useApplicationWindowsStore } from '../../stores/storeApplicationWindows'
5 | import { useApplicationMetaStore } from '../../stores/storeApplicationMeta'
6 | import { useTerminalManager } from '../../composables/useTerminalManager'
7 | import { useDesktopManager } from '../../composables/useDesktopManager'
8 | import { debugLog, debugError } from '../../utils/utilDebug'
9 | import { useDesktopWorkspaceStore } from '../../stores/storeDesktopWorkspace'
10 | import { reactive } from '@vue/reactivity'
11 |
12 | export class ApplicationController implements IApplicationController {
13 | private readonly applicationManager: IApplicationManager
14 | private readonly desktopManager: IDesktopManager
15 | private readonly terminalManager: ITerminalManager
16 |
17 | public readonly id
18 | public readonly config
19 | public readonly storeWindows
20 | public readonly storeMeta
21 |
22 | public windows = reactive(new Map())
23 |
24 | public isRunning = false
25 |
26 | constructor(id: string, config: ApplicationConfig) {
27 | this.applicationManager = useApplicationManager()
28 | this.terminalManager = useTerminalManager()
29 | this.desktopManager = useDesktopManager()
30 |
31 | this.id = id
32 | this.config = config
33 | this.storeWindows = useApplicationWindowsStore(id)
34 | this.storeMeta = useApplicationMetaStore(id)
35 | }
36 |
37 | public async initApplication(): Promise {
38 | // provides
39 |
40 | // set as default app for specific purposes
41 | // todo improve this and move it in a store
42 | if (this.config.provides) {
43 | const existingDefault = this.desktopManager.getDefaultApp(
44 | this.config.provides.name,
45 | )
46 |
47 | if (!existingDefault) {
48 | this.desktopManager.setDefaultApp(
49 | this.config.provides.name,
50 | this,
51 | this.config.provides.command,
52 | )
53 |
54 | debugLog(
55 | `${this.config.title} has been set as predefined app for "${this.config.provides.name}"`,
56 | )
57 | }
58 | }
59 |
60 | // terminal
61 |
62 | if (this.config.commands) {
63 | for (const commandKey of Object.keys(this.config.commands)) {
64 | this.terminalManager.addCommand({
65 | applicationId: this.id,
66 | name: commandKey,
67 | })
68 | }
69 | }
70 |
71 | // store
72 |
73 | if (this.storeWindows.$persistedState) {
74 | await this.storeWindows.$persistedState.isReady()
75 | }
76 |
77 | if (this.storeMeta.$persistedState) {
78 | await this.storeMeta.$persistedState.isReady()
79 | }
80 |
81 | // set default meta values
82 | // this.storeMeta.meta = this.config.meta ?? {}
83 |
84 | // restore application state
85 | await this.restoreApplication()
86 |
87 | // once app is defined, always run "onReady"
88 | if (typeof this.config.onReady === 'function') {
89 | // todo transform in hook
90 | this.config.onReady(this)
91 | }
92 | }
93 |
94 | /**
95 | * App always tries to restore previous windows
96 | * and returns a boolean if it succeeded
97 | */
98 | public async restoreApplication() {
99 | if (typeof this.config.onRestore === 'function') {
100 | // todo transform in hook
101 | await this.config.onRestore(this)
102 | }
103 |
104 | if (
105 | !this.storeWindows.windows ||
106 | Object.keys(this.storeWindows.windows).length === 0
107 | ) {
108 | return false
109 | }
110 |
111 | this.restoreWindows()
112 |
113 | this.setRunning(true)
114 |
115 | return true
116 | }
117 |
118 | private restoreWindows() {
119 | Object.keys(this.storeWindows.windows).map((windowId) => {
120 | const windowStore: WindowStoredState | undefined =
121 | this.storeWindows.windows[windowId]
122 |
123 | if (windowStore) {
124 | this.openWindow(windowStore.model, windowStore, {
125 | isRestoring: true,
126 | })
127 | }
128 | })
129 |
130 | debugLog('Windows have been restored', this.windows)
131 | }
132 |
133 | public openWindow(
134 | model: string,
135 | windowStoredState: WindowStoredState | undefined,
136 | meta?: any,
137 | ) {
138 | const desktopWorkspaceStore = useDesktopWorkspaceStore()
139 |
140 | if (!this.config.windows || !this.config.windows.hasOwnProperty(model)) {
141 | debugError(`Window model "${model}" not found`)
142 | return
143 | }
144 |
145 | let windowId: string
146 |
147 | if (!windowStoredState) {
148 | windowId = `${model}-${nanoid(6)}`
149 |
150 | const windowConfig: WindowConfig = this.config.windows[
151 | model
152 | ] as WindowConfig
153 | const screenHeight = window.innerHeight
154 | const centerY = (screenHeight - Number(windowConfig.size.height)) / 2
155 | const positionY =
156 | windowConfig.position?.y !== undefined
157 | ? window.scrollY + windowConfig.position.y
158 | : window.scrollY + centerY
159 |
160 | this.storeWindows.windows[windowId] = {
161 | model,
162 | state: {
163 | id: windowId,
164 | active: true,
165 | focused: false,
166 | position: {
167 | x: windowConfig.position?.x,
168 | y: positionY,
169 | },
170 | createdAt: +new Date(),
171 | workspace: desktopWorkspaceStore.active,
172 | },
173 | meta,
174 | }
175 |
176 | windowStoredState = this.storeWindows.windows[windowId]
177 | } else {
178 | // restore previous id if state is defined
179 | windowId = windowStoredState.state.id
180 | }
181 |
182 | const windowConfig = this.config.windows[model] as WindowConfig
183 |
184 | const windowController = new WindowController(
185 | this,
186 | model,
187 | windowConfig,
188 | windowStoredState!,
189 | )
190 |
191 | if (!meta?.isRestoring) {
192 | windowController.actions.bringToFront()
193 | }
194 |
195 | this.windows.set(windowId, windowController)
196 |
197 | this.setRunning(true)
198 |
199 | return windowController
200 | }
201 |
202 | public closeWindow(windowId: string) {
203 | delete this.storeWindows.windows[windowId]
204 | this.windows.delete(windowId)
205 |
206 | if (this.windows.size === 0) {
207 | this.applicationManager.closeApp(this.id)
208 | }
209 | }
210 |
211 | public closeAllWindows() {
212 | this.storeWindows.windows = {}
213 | this.windows.clear()
214 | }
215 |
216 | get windowsOpened() {
217 | return this.windows
218 | }
219 |
220 | public getWindowById(id: string): IWindowController[] {
221 | return this.windows.get(id)
222 | }
223 |
224 | public getWindowsByModel(model: string): IWindowController[] {
225 | return Array.from(this.windows.values()).filter((w) => w.model === model)
226 | }
227 |
228 | public getFirstWindowByModel(model: string): IWindowController | undefined {
229 | return Array.from(this.windows.values()).find((w) => w.model === model)
230 | }
231 |
232 | public setRunning(value: boolean): void {
233 | this.isRunning = value
234 | }
235 |
236 | // meta
237 |
238 | get meta() {
239 | return this.storeMeta.meta
240 | }
241 |
242 | getMeta(key: string) {
243 | return this.meta[key]
244 | }
245 |
246 | setMeta(key: string, value: any) {
247 | this.meta[key] = value
248 | }
249 |
250 | // commands
251 |
252 | async execCommand(command: string): Promise {
253 | return this.applicationManager.execAppCommand(this.id, command)
254 | }
255 | }
256 |
--------------------------------------------------------------------------------
/packages/core/runtime/core/controllers/WindowController.ts:
--------------------------------------------------------------------------------
1 | import { useApplicationManager } from '../../composables/useApplicationManager'
2 | import { useDesktopWindowStore } from '../../stores/storeDesktopWindow'
3 | import { deepClone } from '../../utils/utilCommon'
4 | import { markRaw } from '@vue/reactivity'
5 | import { defineAsyncComponent } from 'vue'
6 |
7 | export class WindowController implements IWindowController {
8 | public readonly application: IApplicationController
9 |
10 | public readonly instanced: boolean = true
11 | public readonly model: string
12 |
13 | public config: WindowConfig = {
14 | title: '',
15 | category: '',
16 |
17 | component: undefined,
18 |
19 | // position
20 | position: {
21 | x: 0,
22 | y: 0,
23 | z: 0,
24 | },
25 |
26 | // sizes
27 | size: {
28 | width: undefined,
29 | height: undefined,
30 | },
31 |
32 | // minimize
33 | minimizable: true,
34 |
35 | // maximize
36 | maximized: false,
37 | maximizable: false,
38 |
39 | // destroy
40 | destroyable: true,
41 |
42 | // draggable
43 | draggable: true,
44 |
45 | // resizable
46 | resizable: true,
47 | }
48 |
49 | public override: WindowOverride = {}
50 |
51 | private storedState: WindowStoredState
52 |
53 | constructor(
54 | application: IApplicationController,
55 | model: string,
56 | windowConfig: WindowConfig,
57 | windowStoredState: WindowStoredState,
58 | ) {
59 | this.application = application
60 | this.model = model
61 |
62 | this.storedState = windowStoredState
63 |
64 | this.setConfig(windowConfig)
65 |
66 | this.restoreState()
67 | }
68 |
69 | private setConfig(config: WindowConfig) {
70 | // component
71 | if (config.component) {
72 | this.config.component = markRaw(defineAsyncComponent(config.component))
73 | }
74 |
75 | // title
76 | if (config.title) this.config.title = config.title
77 | if (config.icon) this.config.icon = config.icon
78 |
79 | // position
80 | if (!this.config.position) this.config.position = { x: 0, y: 0, z: 0 }
81 | if (config.position?.x) this.config.position.x = config.position.x
82 | if (config.position?.y) this.config.position.y = config.position.y
83 | if (config.position?.z) this.config.position.z = config.position.z
84 |
85 | // sizes
86 | if (config.size?.width) this.config.size.width = config.size.width
87 | if (config.size?.height) this.config.size.height = config.size.height
88 | if (config.size?.minWidth) this.config.size.minWidth = config.size.minWidth
89 | if (config.size?.minHeight)
90 | this.config.size.minHeight = config.size.minHeight
91 | if (config.size?.maxWidth) this.config.size.maxWidth = config.size.maxWidth
92 | if (config.size?.maxHeight)
93 | this.config.size.maxHeight = config.size.maxHeight
94 |
95 | // minimize
96 | if (typeof config.minimizable !== 'undefined') {
97 | this.config.minimizable = config.minimizable
98 | }
99 | if (typeof config.maximizable !== 'undefined') {
100 | this.config.maximizable = config.maximizable
101 | }
102 |
103 | // maximize
104 | if (typeof config.maximized !== 'undefined') {
105 | this.config.maximized = config.maximized
106 | }
107 |
108 | // draggable
109 | if (typeof config.draggable !== 'undefined') {
110 | this.config.draggable = config.draggable
111 | }
112 |
113 | // resizable
114 | if (typeof config.resizable !== 'undefined') {
115 | this.config.resizable = config.resizable
116 | }
117 |
118 | // destroy
119 | if (typeof config.destroyable !== 'undefined') {
120 | this.config.destroyable = config.destroyable
121 | }
122 |
123 | const DEFAULT_OVERRIDABLE = {
124 | draggable: true,
125 | resizable: false,
126 | position: false,
127 | size: false,
128 | maximized: false,
129 | destroyable: false,
130 | minimizable: false,
131 | maximizable: false,
132 | }
133 |
134 | DEFAULT_OVERRIDABLE.position = DEFAULT_OVERRIDABLE.draggable
135 | DEFAULT_OVERRIDABLE.size = DEFAULT_OVERRIDABLE.resizable
136 | DEFAULT_OVERRIDABLE.maximized = DEFAULT_OVERRIDABLE.maximizable
137 |
138 | this.config.overridable = {
139 | ...DEFAULT_OVERRIDABLE,
140 | ...(config.overridable || {}),
141 | }
142 | }
143 |
144 | // state
145 |
146 | get state() {
147 | return this.storedState.state
148 | }
149 |
150 | private restoreState() {
151 | const overridable = this.config.overridable || {}
152 |
153 | for (const key in overridable) {
154 | if (overridable[key as keyof typeof overridable]) {
155 | const stateKey = key as keyof WindowState
156 |
157 | if (typeof this.state[stateKey] === 'undefined') {
158 | if (typeof this.config[stateKey] === 'object') {
159 | this.state[stateKey] = deepClone(this.config[stateKey])
160 | } else {
161 | this.state[stateKey] = !!this.config[stateKey]
162 | }
163 | }
164 | }
165 | }
166 | }
167 |
168 | private setState(state: WindowState) {
169 | this.storedState.state = state
170 | }
171 |
172 | // meta
173 |
174 | get meta() {
175 | return this.storedState.meta
176 | }
177 |
178 | // position
179 |
180 | public setPosition(data: { x: number; y: number }) {
181 | if (!this.state.position) {
182 | this.state.position = { x: 0, y: 0 }
183 | }
184 |
185 | this.state.position.x = data.x
186 | this.state.position.y = data.y
187 | }
188 |
189 | public setActive(value: boolean) {
190 | this.state.active = value
191 | }
192 |
193 | public setFocus(value: boolean) {
194 | this.state.focused = value
195 | }
196 |
197 | public focus() {
198 | const applicationManager = useApplicationManager()
199 | const desktopWindowStore = useDesktopWindowStore()
200 |
201 | if (!this.state.position) {
202 | this.state.position = { x: 0, y: 0, z: 0 }
203 | }
204 |
205 | // already focused
206 | if (this.state.focused) {
207 | return
208 | }
209 |
210 | // set focus false on all other windows
211 | for (const [windowId, window] of applicationManager.windowsOpened) {
212 | window.actions.setFocus(false)
213 | }
214 |
215 | this.setFocus(true)
216 |
217 | this.state.position.z = desktopWindowStore.incrementPositionZ()
218 | }
219 |
220 | // common
221 |
222 | get title(): string {
223 | if (typeof this.override.title !== 'undefined') {
224 | return this.override.title
225 | }
226 |
227 | if (this.config.title) {
228 | return this.config.title
229 | }
230 |
231 | return this.application.config.title
232 | }
233 |
234 | get icon() {
235 | if (typeof this.override.icon !== 'undefined') {
236 | return this.override.icon
237 | }
238 |
239 | if (this.config.icon) {
240 | return this.config.icon
241 | }
242 |
243 | return this.application.config.icon
244 | }
245 |
246 | // position
247 |
248 | get position() {
249 | if (typeof this.state.position === 'undefined') {
250 | return this.config.position
251 | }
252 |
253 | return this.state.position
254 | }
255 |
256 | // sizes
257 |
258 | get size() {
259 | const stateSize = this.state.size || {}
260 | const configSize = this.config.size || {}
261 |
262 | return {
263 | width:
264 | typeof stateSize.width !== 'undefined'
265 | ? stateSize.width
266 | : configSize.width,
267 | height:
268 | typeof stateSize.height !== 'undefined'
269 | ? stateSize.height
270 | : configSize.height,
271 | minWidth:
272 | typeof stateSize.minWidth !== 'undefined'
273 | ? stateSize.minWidth
274 | : configSize.minWidth,
275 | maxWidth:
276 | typeof stateSize.maxWidth !== 'undefined'
277 | ? stateSize.maxWidth
278 | : configSize.maxWidth,
279 | minHeight:
280 | typeof stateSize.minHeight !== 'undefined'
281 | ? stateSize.minHeight
282 | : configSize.minHeight,
283 | maxHeight:
284 | typeof stateSize.maxHeight !== 'undefined'
285 | ? stateSize.maxHeight
286 | : (configSize.maxHeight ?? 600),
287 | }
288 | }
289 |
290 | public setSize(data: { width: number; height: number }) {
291 | if (!this.state.size) {
292 | this.state.size = { width: undefined, height: undefined }
293 | }
294 |
295 | this.state.size.width = data.width
296 | this.state.size.height = data.height
297 | }
298 |
299 | // minimize
300 |
301 | get isMinimizable() {
302 | if (this.config.overridable?.minimizable) {
303 | return !!this.state.minimizable
304 | }
305 |
306 | return !!this.config.minimizable
307 | }
308 |
309 | public minimize() {
310 | if (!this.isMinimizable) {
311 | return false
312 | }
313 |
314 | this.state.active = false
315 | return true
316 | }
317 |
318 | public unminimize() {
319 | this.state.active = true
320 | return true
321 | }
322 |
323 | public toggleMinimize() {
324 | this.state.active = !this.state.active
325 | this.focus()
326 | }
327 |
328 | // maximize
329 |
330 | get isMaximizable() {
331 | if (typeof this.state.maximizable === 'undefined') {
332 | return !!this.config.maximizable
333 | }
334 |
335 | return this.state.maximizable
336 | }
337 |
338 | get isMaximized() {
339 | if (this.config.overridable?.maximized) {
340 | return !!this.state.maximized
341 | }
342 |
343 | return !!this.config.maximized
344 | }
345 |
346 | public toggleMaximize() {
347 | if (!this.isMaximizable) {
348 | return false
349 | }
350 |
351 | this.state.maximized = !this.state.maximized
352 | return true
353 | }
354 |
355 | public maximize() {
356 | if (!this.isMaximizable) {
357 | return false
358 | }
359 |
360 | this.state.maximized = true
361 | return true
362 | }
363 |
364 | public unmaximize() {
365 | if (!this.isMaximizable) {
366 | return false
367 | }
368 |
369 | this.state.maximized = false
370 | return true
371 | }
372 |
373 | // destroy
374 |
375 | get isDestroyable() {
376 | if (this.config.overridable?.destroyable) {
377 | return !!this.state.destroyable
378 | }
379 |
380 | return !!this.config.destroyable
381 | }
382 |
383 | public destroy() {
384 | if (!this.isDestroyable) {
385 | return false
386 | }
387 |
388 | this.application.closeWindow(this.state.id)
389 |
390 | return true
391 | }
392 |
393 | // draggable
394 | get isDraggable() {
395 | if (this.config.overridable?.draggable) {
396 | return !!this.state.draggable
397 | }
398 |
399 | return !!this.config.draggable
400 | }
401 |
402 | // resizable
403 | get isResizable() {
404 | if (this.config.overridable?.resizable) {
405 | return !!this.state.resizable
406 | }
407 |
408 | return !!this.config.resizable
409 | }
410 |
411 | // workspace
412 | public setWorkspace(workspaceId: string) {
413 | this.state.workspace = workspaceId
414 | }
415 |
416 | // override
417 |
418 | public setTitleOverride(value: undefined | string) {
419 | this.override.title = value
420 | }
421 |
422 | public resetTitleOverride() {
423 | this.override.title = undefined
424 | }
425 |
426 | // menu
427 |
428 | public menu: any[] = []
429 |
430 | public setMenu(menu: any[]) {
431 | this.menu = menu
432 | }
433 |
434 | // deprecated ?
435 | get actions() {
436 | return {
437 | // position
438 | setActive: this.setActive.bind(this),
439 | setFocus: this.setFocus.bind(this),
440 | bringToFront: this.focus.bind(this),
441 | focus: this.focus.bind(this),
442 | setPosition: this.setPosition.bind(this),
443 |
444 | // size
445 | setSize: this.setSize.bind(this),
446 |
447 | // minimize
448 | minimize: this.minimize.bind(this),
449 | toggleMinimize: this.toggleMinimize.bind(this),
450 |
451 | // maximize
452 | toggleMaximize: this.toggleMaximize.bind(this),
453 | maximize: this.maximize.bind(this),
454 | unmaximize: this.unmaximize.bind(this),
455 |
456 | // destroy
457 | destroy: this.destroy.bind(this),
458 |
459 | // workspace
460 | setWorkspace: this.setWorkspace.bind(this),
461 |
462 | // override
463 | setTitleOverride: this.setTitleOverride.bind(this),
464 | resetTitleOverride: this.resetTitleOverride.bind(this),
465 | }
466 | }
467 | }
468 |
--------------------------------------------------------------------------------
/packages/core/runtime/core/managers/ApplicationManager.ts:
--------------------------------------------------------------------------------
1 | import type { Reactive } from '@vue/reactivity'
2 | import { reactive, markRaw } from '@vue/reactivity'
3 | import { ApplicationController } from '../controllers/ApplicationController'
4 | import { normalizeApplicationConfig } from '../../utils/utilApp'
5 | import { debugLog } from '../../utils/utilDebug'
6 | import * as shellwords from 'shellwords'
7 | import yargsParser from 'yargs-parser'
8 |
9 | export class ApplicationManager implements IApplicationManager {
10 | public apps = reactive(new Map())
11 |
12 | constructor() {}
13 |
14 | /**
15 | * Define new application
16 | *
17 | * @param id
18 | * @param config
19 | */
20 | public async defineApp(id: string, config: ApplicationConfig) {
21 | if (this.isAppDefined(id)) {
22 | debugLog(`App "${id}" is already defined`)
23 | return this.getAppById(id)!
24 | }
25 |
26 | const normalizedConfig = normalizeApplicationConfig(config)
27 | const applicationConfig = markRaw(normalizedConfig)
28 |
29 | const applicationController: IApplicationController = new ApplicationController(id, applicationConfig)
30 | await applicationController.initApplication()
31 |
32 | this.apps.set(id, applicationController)
33 |
34 | return applicationController
35 | }
36 |
37 | /**
38 | * Check if app has been defned
39 | *
40 | * @param {string} id
41 | */
42 | public isAppDefined(id: string) {
43 | return this.getAppById(id)
44 | }
45 |
46 | /**
47 | * Retrieves an app instance by its unique identifier
48 | *
49 | * @param {string} id
50 | */
51 | public getAppById(id: string) {
52 | return this.apps.get(id)
53 | }
54 |
55 | /**
56 | * Check if app is running
57 | *
58 | * @param {string} id
59 | */
60 | public isAppRunning(id: string) {
61 | if (!this.isAppDefined(id)) {
62 | throw Error(`App "${id}" is not defined`)
63 | }
64 |
65 | const applicationController: IApplicationController = this.getAppById(id)!
66 |
67 | if (!applicationController.isRunning) {
68 | return false
69 | }
70 |
71 | return true
72 | }
73 |
74 | /**
75 | * Launch app entry
76 | *
77 | * @param id
78 | * @param entryKey
79 | */
80 | public async launchAppEntry(
81 | id: string,
82 | entryKey: string,
83 | ): Promise {
84 | if (!this.isAppDefined(id)) {
85 | throw Error(`App "${id}" is not defined`)
86 | }
87 |
88 | const applicationController: IApplicationController = this.getAppById(id)!
89 |
90 | if (
91 | applicationController.config.entries &&
92 | !applicationController.config.entries.hasOwnProperty(entryKey)
93 | ) {
94 | throw Error(`App entry "${entryKey}" is not defined in ${id} application`)
95 | }
96 |
97 | const entry: ApplicationEntry = applicationController.config.entries[entryKey]!
98 |
99 | await this.execAppCommand(applicationController.id, entry.command)
100 | }
101 |
102 | /**
103 | * Run app command
104 | *
105 | * @param id
106 | * @param rawCommand
107 | */
108 | public async execAppCommand(
109 | id: string,
110 | rawCommand: string,
111 | ): Promise {
112 | if (!this.isAppDefined(id)) {
113 | throw Error(`App "${id}" is not defined`)
114 | }
115 |
116 | const applicationController: IApplicationController = this.getAppById(id)!
117 |
118 | const args = shellwords.split(rawCommand)
119 | const parsed = yargsParser(args)
120 | const command: string = parsed._[0]
121 |
122 | if (
123 | applicationController.config.commands &&
124 | !applicationController.config.commands.hasOwnProperty(command)
125 | ) {
126 | throw Error(
127 | `App command "${command}" is not defined in ${id} application`,
128 | )
129 | }
130 |
131 | const commandFn: any = applicationController.config.commands![command]
132 |
133 | const commandOutput = await commandFn(
134 | applicationController,
135 | parsed,
136 | )
137 |
138 | applicationController.setRunning(true)
139 |
140 | return commandOutput
141 | }
142 |
143 | /**
144 | * Close application
145 | *
146 | * @param id
147 | */
148 | public closeApp(id: string) {
149 | if (!this.isAppDefined(id)) {
150 | throw Error(`App "${id}" is not defined`)
151 | }
152 |
153 | const applicationController: IApplicationController = this.getAppById(id)!
154 |
155 | applicationController.closeAllWindows()
156 | applicationController.setRunning(false)
157 | }
158 |
159 | /**
160 | * Array of available menu entries for system bars, docks
161 | */
162 | public get appsEntries() {
163 | const entries: Reactive = reactive([])
164 |
165 | for (const applicationController of this.apps.values()) {
166 | if (!applicationController.config.entries) {
167 | continue
168 | }
169 |
170 | for (const entryKey of Object.keys(
171 | applicationController.config.entries,
172 | )) {
173 | const entry: ApplicationEntry =
174 | applicationController.config.entries[entryKey]!
175 |
176 | entries.push({
177 | application: applicationController,
178 | title:
179 | entry.title !== undefined
180 | ? entry.title
181 | : applicationController.config.title,
182 | icon:
183 | entry.icon !== undefined
184 | ? entry.icon
185 | : applicationController.config.icon,
186 | category:
187 | entry.category !== undefined
188 | ? entry.category
189 | : applicationController.config.category,
190 | visibility: entry.visibility ?? 'primary',
191 | command: entry.command,
192 | })
193 | }
194 | }
195 |
196 | return entries
197 | }
198 |
199 | /**
200 | * Array of opened windows for system bars, docks
201 | */
202 | public get windowsOpened() {
203 | const windows: Reactive