├── .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 | 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 | 19 | -------------------------------------------------------------------------------- /packages/core/runtime/components/Core/Application/CoreApplicationWindowsRender.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 20 | -------------------------------------------------------------------------------- /packages/core/runtime/components/Core/Background/CoreBackground.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | -------------------------------------------------------------------------------- /packages/core/runtime/components/Core/Desktop/CoreDesktop.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | 79 | 80 | 87 | -------------------------------------------------------------------------------- /packages/core/runtime/components/Core/Explorer/CoreExplorerFile.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 30 | 31 | 77 | -------------------------------------------------------------------------------- /packages/core/runtime/components/Core/Explorer/CoreExplorerFolder.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | -------------------------------------------------------------------------------- /packages/core/runtime/components/Core/Time/CoreTime.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 57 | -------------------------------------------------------------------------------- /packages/core/runtime/components/Core/Window/CoreWindow.vue: -------------------------------------------------------------------------------- 1 | 119 | 120 | 151 | 152 | 169 | -------------------------------------------------------------------------------- /packages/core/runtime/components/Core/Window/CoreWindowContent.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /packages/core/runtime/components/Core/Window/CoreWindowNav.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 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[]> = reactive([]) 204 | 205 | for (const applicationController of this.apps.values()) { 206 | if (applicationController.isRunning) { 207 | windows.push(...applicationController.windows) 208 | } 209 | } 210 | 211 | return windows 212 | } 213 | 214 | /** 215 | * Array of opened windows for system bars, docks 216 | */ 217 | public get appsRunning() { 218 | const applications: Reactive = reactive([]) 219 | 220 | for (const applicationController of this.apps.values()) { 221 | if (applicationController.isRunning) { 222 | applications.push(applicationController) 223 | } 224 | } 225 | 226 | return applications 227 | } 228 | 229 | public getWindowOpenedId(windowId: string) { 230 | const mapWindowFound: Map | undefined = 231 | this.windowsOpened.find((window: any) => { 232 | return window[1].state.id === windowId 233 | }) 234 | 235 | if (mapWindowFound) { 236 | // @ts-ignore 237 | return mapWindowFound[1] as IWindowController 238 | } 239 | } 240 | 241 | /** 242 | * Gets all unique categories from the installed apps 243 | */ 244 | public get appCategories(): string[] { 245 | const categories = new Set() 246 | 247 | for (const applicationController of this.apps.values()) { 248 | if (applicationController.config.category) { 249 | categories.add(applicationController.config.category) 250 | } 251 | } 252 | return Array.from(categories).sort() 253 | } 254 | 255 | /** 256 | * Gets the apps ordered by category 257 | */ 258 | public get appsByCategory(): { 259 | [category: string]: IApplicationController[] 260 | } { 261 | const categorizedApps: { 262 | [category: string]: IApplicationController[] 263 | } = {} 264 | 265 | for (const applicationController of this.apps.values()) { 266 | const category = applicationController.config.category || 'other' 267 | if (!categorizedApps[category]) { 268 | categorizedApps[category] = [] 269 | } 270 | categorizedApps[category].push(applicationController) 271 | } 272 | 273 | for (const category in categorizedApps) { 274 | categorizedApps[category]!.sort((a, b) => 275 | a.config.title.localeCompare(b.config.title), 276 | ) 277 | } 278 | 279 | return categorizedApps 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /packages/core/runtime/core/managers/DesktopManager.ts: -------------------------------------------------------------------------------- 1 | export class DesktopManager { 2 | private defaultApps: DefaultAppsConfig = {} 3 | 4 | constructor() {} 5 | 6 | get config(): DesktopConfig { 7 | const runtimeConfig = useRuntimeConfig() 8 | 9 | return runtimeConfig.public.desktop 10 | } 11 | 12 | public hasFeature(featureName: string) { 13 | return this.config.features?.indexOf(featureName) !== -1 14 | } 15 | 16 | public setConfig(config: DesktopConfig) { 17 | const runtimeConfig = useRuntimeConfig() 18 | 19 | runtimeConfig.public.desktop = deepMerge( 20 | runtimeConfig.public.desktop, 21 | config 22 | ) as DesktopConfig 23 | 24 | this.loadDefaultAppsFromConfig() 25 | } 26 | 27 | public setDefaultApp( 28 | feature: string, 29 | application: IApplicationController, 30 | command: string, 31 | ) { 32 | this.defaultApps[feature] = { 33 | application, 34 | command, 35 | } 36 | } 37 | 38 | public getDefaultApp(feature: string): DefaultAppConfig { 39 | return this.defaultApps[feature] as DefaultAppConfig 40 | } 41 | 42 | private loadDefaultAppsFromConfig() { 43 | if (this.config.defaultApps) { 44 | this.defaultApps = { ...this.config.defaultApps } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/core/runtime/core/managers/TerminalManager.ts: -------------------------------------------------------------------------------- 1 | import { useApplicationManager } from '../../composables/useApplicationManager' 2 | import { debugLog } from '../../utils/utilDebug' 3 | 4 | export class TerminalManager { 5 | private commands: Map = new Map() 6 | 7 | constructor() {} 8 | 9 | public addCommand(command: TerminalCommand) { 10 | for (const existingCommand of this.commands.keys()) { 11 | if ( 12 | existingCommand.startsWith(command.name) || 13 | command.name.startsWith(existingCommand) 14 | ) { 15 | throw new Error(`Command prefix conflict with "${existingCommand}"`) 16 | } 17 | } 18 | 19 | this.commands.set(command.name, command) 20 | debugLog( 21 | `Registered terminal command: ${command.name} → ${command.applicationId}`, 22 | ) 23 | } 24 | 25 | public listCommands(): string[] { 26 | return Array.from(this.commands.keys()) 27 | } 28 | 29 | public async execCommand(input: string): Promise { 30 | const applicationManager = useApplicationManager() 31 | 32 | const [name, ...args] = input.trim().split(/\s+/) 33 | 34 | if (!name) { 35 | return 36 | } 37 | 38 | if (!this.commands.has(name)) { 39 | return { 40 | message: `Command "${name}" not found`, 41 | } 42 | } 43 | 44 | const application = this.commands.get(name)! 45 | 46 | return applicationManager.execAppCommand(application.applicationId, input) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/core/runtime/plugins/resize.client.ts: -------------------------------------------------------------------------------- 1 | import VueResizable from 'vue-resizable/src/components/vue-resizable.vue' 2 | import { defineNuxtPlugin } from 'nuxt/app' 3 | 4 | export default defineNuxtPlugin((nuxtApp) => { 5 | nuxtApp.vueApp.component('vue-resizable', VueResizable) 6 | }) 7 | -------------------------------------------------------------------------------- /packages/core/runtime/stores/storeApplicationMeta.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | export const useApplicationMetaStore = (appId: string, states: any = {}) => { 4 | return defineStore( 5 | `owd/application/${appId}/meta`, 6 | () => { 7 | return states 8 | }, 9 | { 10 | persistedState: { 11 | persist: true, 12 | }, 13 | }, 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /packages/core/runtime/stores/storeApplicationWindows.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { ref } from 'vue' 3 | 4 | export const useApplicationWindowsStore = (appId: string) => { 5 | return defineStore( 6 | `owd/application/${appId}/windows`, 7 | () => { 8 | const windows: Ref<{ [windowId: string]: WindowStoredState }> = ref({}) 9 | 10 | return { 11 | windows, 12 | } 13 | }, 14 | { 15 | persistedState: { 16 | persist: true, 17 | }, 18 | }, 19 | )() 20 | } 21 | -------------------------------------------------------------------------------- /packages/core/runtime/stores/storeDesktop.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { ref } from 'vue' 3 | 4 | export const useDesktopStore = defineStore( 5 | 'owd/desktop', 6 | () => { 7 | const state = ref<{ 8 | workspace: { 9 | overview: boolean 10 | active: string 11 | list: string[] 12 | } 13 | volume: { 14 | master: number 15 | } 16 | window: { 17 | positionZ: number 18 | } 19 | }>({ 20 | workspace: { 21 | overview: false, 22 | active: '', 23 | list: [], 24 | }, 25 | volume: { 26 | master: 100, 27 | }, 28 | window: { 29 | positionZ: 0, 30 | }, 31 | }) 32 | 33 | return { 34 | state, 35 | } 36 | }, 37 | { 38 | // @ts-expect-error 39 | persistedState: { 40 | persist: true, 41 | }, 42 | }, 43 | ) 44 | -------------------------------------------------------------------------------- /packages/core/runtime/stores/storeDesktopVolume.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { computed } from 'vue' 3 | import { useDesktopStore } from './storeDesktop' 4 | 5 | export const useDesktopVolumeStore = defineStore('owd/desktop/volume', () => { 6 | const desktopStore = useDesktopStore() 7 | 8 | const master = computed(() => desktopStore.state.volume.master) 9 | 10 | function setMasterVolume(value: number) { 11 | desktopStore.state.volume.master = value 12 | } 13 | 14 | return { 15 | master, 16 | setMasterVolume, 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /packages/core/runtime/stores/storeDesktopWindow.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { computed } from 'vue' 3 | import { useDesktopStore } from './storeDesktop' 4 | 5 | export const useDesktopWindowStore = defineStore('owd/desktop/window', () => { 6 | const desktopStore = useDesktopStore() 7 | 8 | const positionZ = computed(() => desktopStore.state.window.positionZ) 9 | 10 | function incrementPositionZ() { 11 | desktopStore.state.window.positionZ++ 12 | 13 | return desktopStore.state.window.positionZ 14 | } 15 | 16 | return { 17 | positionZ, 18 | incrementPositionZ, 19 | } 20 | }) 21 | -------------------------------------------------------------------------------- /packages/core/runtime/stores/storeDesktopWorkspace.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { computed } from 'vue' 3 | import { useDesktopStore } from './storeDesktop' 4 | import { nanoid } from 'nanoid' 5 | 6 | export const useDesktopWorkspaceStore = defineStore( 7 | 'owd/desktop/workspace', 8 | () => { 9 | const desktopStore = useDesktopStore() 10 | 11 | const active = computed(() => { 12 | return desktopStore.state.workspace.active 13 | }) 14 | 15 | const overview = computed(() => { 16 | return desktopStore.state.workspace.overview 17 | }) 18 | 19 | const list = computed(() => { 20 | return desktopStore.state.workspace.list 21 | }) 22 | 23 | function setupWorkspaces() { 24 | if (desktopStore.state.workspace.list.length === 0) { 25 | createWorkspace() 26 | desktopStore.state.workspace.active = desktopStore.state.workspace 27 | .list[0] as string 28 | } 29 | 30 | if (desktopStore.state.workspace.list.length === 1) { 31 | createWorkspace() 32 | } 33 | } 34 | 35 | function setOverview(value: boolean) { 36 | desktopStore.state.workspace.overview = value 37 | } 38 | 39 | function setWorkspace(value: string) { 40 | desktopStore.state.workspace.active = value 41 | } 42 | 43 | function createWorkspace() { 44 | desktopStore.state.workspace.list.push(nanoid(8)) 45 | } 46 | 47 | const workspaceActiveIndex = computed(() => { 48 | return desktopStore.state.workspace.list.findIndex( 49 | (workspace) => workspace === desktopStore.state.workspace.active, 50 | ) 51 | }) 52 | 53 | return { 54 | active, 55 | overview, 56 | list, 57 | workspaceActiveIndex, 58 | setupWorkspaces, 59 | setOverview, 60 | setWorkspace, 61 | } 62 | }, 63 | ) 64 | -------------------------------------------------------------------------------- /packages/core/runtime/utils/utilApp.ts: -------------------------------------------------------------------------------- 1 | import type { Nuxt } from '@nuxt/schema' 2 | 3 | export function normalizeApplicationConfig( 4 | config: ApplicationConfig, 5 | ): ApplicationConfig { 6 | const normalizedEntries: Record = {} 7 | 8 | if (config.entries) { 9 | for (const [key, entry] of Object.entries(config.entries)) { 10 | normalizedEntries[key] = { 11 | ...entry, 12 | title: entry.title ?? config.title, 13 | icon: entry.icon ?? config.icon, 14 | category: entry.category ?? config.category, 15 | visibility: entry.visibility ?? 'primary', 16 | } 17 | } 18 | } 19 | 20 | return { 21 | ...config, 22 | version: config.version ?? 'unknown', 23 | description: config.description ?? '', 24 | category: config.category ?? 'other', 25 | entries: normalizedEntries, 26 | } 27 | } 28 | 29 | export function registerTailwindPath(nuxt: Nuxt, path: string) { 30 | nuxt.options.runtimeConfig.app.owd = nuxt.options.runtimeConfig.app.owd || {} 31 | nuxt.options.runtimeConfig.app.owd.tailwindPaths = 32 | nuxt.options.runtimeConfig.app.owd.tailwindPaths || [] 33 | 34 | if (!nuxt.options.runtimeConfig.app.owd.tailwindPaths.includes(path)) { 35 | nuxt.options.runtimeConfig.app.owd.tailwindPaths.push(path) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/core/runtime/utils/utilCommon.ts: -------------------------------------------------------------------------------- 1 | export function deepClone(obj: T): T { 2 | return JSON.parse(JSON.stringify(obj)) 3 | } 4 | 5 | export function deepEqual(obj1: any, obj2: any) { 6 | const keys1 = Object.keys(obj1) 7 | const keys2 = Object.keys(obj2) 8 | if (keys1.length !== keys2.length) { 9 | return false 10 | } 11 | for (const key of keys1) { 12 | if (!obj2.hasOwnProperty(key)) { 13 | return false 14 | } 15 | if (typeof obj1[key] === 'object' && typeof obj2[key] === 'object') { 16 | if (!deepEqual(obj1[key], obj2[key])) { 17 | return false 18 | } 19 | } else { 20 | if (obj1[key] !== obj2[key]) { 21 | return false 22 | } 23 | } 24 | } 25 | return true 26 | } 27 | 28 | export function deepMerge(target: T, source: Partial): T { 29 | function isObject(value: any): value is Record { 30 | return ( 31 | value !== null && 32 | typeof value === 'object' && 33 | !Array.isArray(value) && 34 | !(value instanceof Date) && 35 | !(value instanceof RegExp) 36 | ) 37 | } 38 | 39 | if (isObject(target) && isObject(source)) { 40 | const result = { ...target } 41 | 42 | for (const key in source) { 43 | const targetValue = (target as any)[key] 44 | const sourceValue = (source as any)[key] 45 | 46 | if (isObject(sourceValue) && isObject(targetValue)) { 47 | // @ts-ignore 48 | result[key] = deepMerge(targetValue, sourceValue) 49 | } else { 50 | result[key] = sourceValue 51 | } 52 | } 53 | 54 | return result 55 | } 56 | 57 | return source as T 58 | } 59 | -------------------------------------------------------------------------------- /packages/core/runtime/utils/utilDebug.ts: -------------------------------------------------------------------------------- 1 | export function isDebugMode() { 2 | return import.meta.env.MODE === 'development' 3 | } 4 | 5 | export function debugLog(...messages: any[]) { 6 | if (!isDebugMode()) { 7 | return false 8 | } 9 | 10 | console.log('[OWD]', ...messages) 11 | return true 12 | } 13 | 14 | export function debugWarn(...messages: any[]) { 15 | if (!isDebugMode()) { 16 | return false 17 | } 18 | 19 | console.warn('[OWD]', ...messages) 20 | return true 21 | } 22 | 23 | export function debugError(...messages: any[]) { 24 | if (!isDebugMode()) { 25 | return false 26 | } 27 | 28 | console.error('[OWD]', ...messages) 29 | return true 30 | } 31 | -------------------------------------------------------------------------------- /packages/core/runtime/utils/utilDesktop.ts: -------------------------------------------------------------------------------- 1 | import { useApplicationManager } from '../composables/useApplicationManager' 2 | 3 | export function defineDesktopApp(config: ApplicationConfig) { 4 | const applicationManager = useApplicationManager() 5 | return applicationManager.defineApp(config.id, config) 6 | } 7 | 8 | export function defineDesktopConfig(config: DesktopConfig) { 9 | return config 10 | } 11 | -------------------------------------------------------------------------------- /packages/core/runtime/utils/utilTerminal.ts: -------------------------------------------------------------------------------- 1 | export function shellEscape(str: string): string { 2 | if (str === '') { 3 | return "''"; 4 | } 5 | return `${str.replace(/'/g, `'\\''`)}`; 6 | } 7 | -------------------------------------------------------------------------------- /packages/core/runtime/utils/utilWindow.ts: -------------------------------------------------------------------------------- 1 | // support for controller-less windows 2 | // (for example: dummy windows in docs) 3 | export function handleWindowControllerProps(windowController: any) { 4 | if (windowController.instanced) { 5 | return windowController 6 | } 7 | 8 | return { 9 | ...windowController, 10 | ...windowController.config, 11 | state: { 12 | ...windowController.config, 13 | }, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/core/test/basic.test.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url' 2 | import { describe, it, expect } from 'vitest' 3 | import { setup, $fetch } from '@nuxt/test-utils/e2e' 4 | 5 | describe('ssr', async () => { 6 | await setup({ 7 | rootDir: fileURLToPath(new URL('./fixtures/basic', import.meta.url)), 8 | }) 9 | 10 | it('renders the index page', async () => { 11 | // Get response to a server-rendered page with `$fetch`. 12 | const html = await $fetch('/') 13 | expect(html).toContain('
basic
') 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /packages/core/test/fixtures/basic/app.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/core/test/fixtures/basic/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import MyModule from '../../../module' 2 | 3 | export default defineNuxtConfig({ 4 | modules: [MyModule], 5 | }) 6 | -------------------------------------------------------------------------------- /packages/core/test/fixtures/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "basic", 4 | "type": "module" 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "rootDir": "runtime", 6 | "jsx": "preserve", 7 | "jsxImportSource": "vue", 8 | "resolveJsonModule": true, 9 | "tsBuildInfoFile": "dist/tsconfig.app.tsbuildinfo" 10 | }, 11 | "include": [".nuxt/nuxt.d.ts", "runtime/**/*", "types/index.d.ts"], 12 | "exclude": [ 13 | "out-tsc", 14 | "dist", 15 | "vite.config.ts", 16 | "vite.config.mts", 17 | "vitest.config.ts", 18 | "vitest.config.mts", 19 | "runtime/**/*.test.ts", 20 | "runtime/**/*.spec.ts", 21 | "runtime/**/*.test.tsx", 22 | "runtime/**/*.spec.tsx", 23 | "runtime/**/*.test.js", 24 | "runtime/**/*.spec.js", 25 | "runtime/**/*.test.jsx", 26 | "runtime/**/*.spec.jsx", 27 | "eslint.config.js", 28 | "eslint.config.cjs", 29 | "eslint.config.mjs" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /packages/core/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 | -------------------------------------------------------------------------------- /packages/core/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 | "runtime/**/*.test.ts", 23 | "runtime/**/*.spec.ts", 24 | "runtime/**/*.test.tsx", 25 | "runtime/**/*.spec.tsx", 26 | "runtime/**/*.test.js", 27 | "runtime/**/*.spec.js", 28 | "runtime/**/*.test.jsx", 29 | "runtime/**/*.spec.jsx", 30 | "runtime/**/*.d.ts" 31 | ], 32 | "references": [ 33 | { 34 | "path": "./tsconfig.app.json" 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /packages/core/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { Nuxt } from '@nuxt/schema' 2 | import { useApplicationManager } from '../runtime/composables/useApplicationManager' 3 | 4 | declare module 'nuxt/schema' { 5 | interface PublicRuntimeConfig { 6 | owd: { 7 | tailwindPaths: [] 8 | } 9 | } 10 | } 11 | 12 | interface IApplicationManager { 13 | apps: Map 14 | 15 | get appsEntries(): Reactive 16 | 17 | get appsRunning(): Reactive 18 | 19 | get windowsOpened(): Reactive 20 | 21 | get appCategories(): string[] 22 | 23 | get appsByCategory(): { [category: string]: IApplicationController[] } 24 | 25 | defineApp( 26 | id: string, 27 | config: ApplicationConfig, 28 | ): Promise 29 | 30 | closeApp(id: string): void 31 | 32 | launchAppEntry( 33 | id: string, 34 | entry: string, 35 | ): Promise 36 | execAppCommand( 37 | id: string, 38 | command: string, 39 | meta: undefined | any, 40 | ): Promise 41 | 42 | isAppDefined(id: string): boolean 43 | 44 | isAppRunning(id: string): boolean 45 | } 46 | 47 | type ApplicationCommand = (app: IApplicationController, args: any) => void 48 | 49 | interface ApplicationConfig { 50 | id: string 51 | title: string 52 | icon?: string 53 | category?: string 54 | version?: string 55 | description?: string 56 | provides?: ApplicationConfigProvide 57 | singleton?: boolean 58 | meta?: IApplicationMeta 59 | permissions?: ApplicationPermission[] 60 | windows?: { [key: string]: WindowConfig } 61 | entries: { [key: string]: ApplicationEntry } 62 | commands?: { [key: string]: ApplicationCommand } 63 | 64 | onReady?(app: IApplicationController): void | Promise 65 | 66 | onLaunch?(app: IApplicationController): void | Promise 67 | 68 | onRestore?(app: IApplicationController): void | Promise 69 | 70 | onClose?(app: IApplicationController): void | Promise 71 | } 72 | 73 | interface ApplicationConfigProvide { 74 | name: string 75 | command: string 76 | } 77 | 78 | type IApplicationMeta = { [key: string]: any } 79 | 80 | interface IApplicationController { 81 | id: string 82 | config: ApplicationConfig 83 | 84 | get meta(): { [key: string]: any } 85 | 86 | store: Pinia 87 | windows: Reactive> 88 | 89 | isRunning: boolean 90 | 91 | setRunning(value: boolean): void 92 | 93 | initApplication(): Promise 94 | 95 | restoreApplication(): Promise 96 | 97 | getWindowsByModel(model: string): IWindowController[] 98 | 99 | getFirstWindowByModel(model: string): IWindowController | undefined 100 | 101 | openWindow( 102 | model: string, 103 | windowStoredState?: WindowStoredState, 104 | options?: { 105 | isRestoring?: boolean 106 | }, 107 | ) 108 | 109 | closeWindow(windowId: string): void 110 | 111 | closeAllWindows(): void 112 | 113 | execCommand(command: string): Promise 114 | 115 | // deprecated 116 | get windowsOpened(): Reactive> 117 | } 118 | 119 | interface IWindowController { 120 | application: IApplicationController 121 | instanced: boolean 122 | model: string 123 | 124 | config: WindowConfig 125 | override: WindowOverride 126 | 127 | get state(): WindowState 128 | 129 | // common 130 | get title(): string 131 | 132 | get icon(): string | undefined 133 | 134 | // sizes 135 | get size(): WindowSize 136 | 137 | // minimize 138 | get isMinimizable(): boolean 139 | 140 | // maximize 141 | get isMaximizable(): boolean 142 | 143 | get isMaximized(): boolean 144 | 145 | // destroy 146 | get isDestroyable(): boolean 147 | 148 | // draggable 149 | get isDraggable(): boolean 150 | 151 | // resizable 152 | get isResizable(): boolean 153 | 154 | get actions(): { 155 | // position 156 | setActive(value: boolean) 157 | setFocus(value: boolean) 158 | bringToFront() 159 | setPosition(data: WindowPosition) 160 | 161 | // size 162 | setSize(data: WindowSize) 163 | 164 | // minimize 165 | minimize(): boolean 166 | 167 | // maximize 168 | toggleMaximize(): boolean 169 | maximize(): boolean 170 | unmaximize(): boolean 171 | 172 | // destroy 173 | destroy(): boolean 174 | 175 | // workspace 176 | setWorkspace(workspaceId: string) 177 | 178 | // override 179 | setTitleOverride(title: string | undefined): void 180 | resetTitleOverride(): void 181 | } 182 | } 183 | 184 | interface WindowContent { 185 | padded?: boolean 186 | centered?: boolean 187 | } 188 | 189 | interface WindowConfig { 190 | title?: string 191 | category?: string 192 | icon?: string 193 | 194 | component?: Raw 195 | 196 | // position 197 | position?: WindowPosition 198 | 199 | // sizes 200 | size: WindowSize 201 | 202 | // minimize 203 | minimizable?: boolean 204 | 205 | // maximize 206 | maximized?: boolean 207 | maximizable?: boolean 208 | 209 | // destroy 210 | destroyable?: boolean 211 | 212 | // draggable 213 | draggable?: boolean 214 | 215 | // draggable 216 | resizable?: boolean 217 | 218 | overridable?: Partial< 219 | Record< 220 | | 'position' 221 | | 'size' 222 | | 'maximized' 223 | | 'draggable' 224 | | 'resizable' 225 | | 'destroyable' 226 | | 'minimizable', 227 | boolean 228 | > 229 | > 230 | } 231 | 232 | interface WindowStoredState { 233 | model: string 234 | state: WindowState 235 | meta: any 236 | } 237 | 238 | interface WindowOverride { 239 | title?: undefined | string 240 | icon?: undefined | string 241 | } 242 | 243 | interface WindowState { 244 | id: string 245 | createdAt: number 246 | 247 | category?: string 248 | workspace: string 249 | 250 | // position 251 | position?: WindowPosition 252 | 253 | // sizes 254 | size?: WindowSize 255 | 256 | // focused 257 | focused: boolean 258 | 259 | // minimize 260 | active?: boolean 261 | minimizable?: boolean 262 | 263 | // maximize 264 | maximized?: boolean 265 | maximizable?: boolean 266 | 267 | // destroy 268 | destroyable?: boolean 269 | 270 | // draggable 271 | draggable?: boolean 272 | 273 | // draggable 274 | resizable?: boolean 275 | } 276 | 277 | interface WindowSize { 278 | width?: WindowSizeValue 279 | height?: WindowSizeValue 280 | minWidth?: WindowSizeValue 281 | minHeight?: WindowSizeValue 282 | maxWidth?: WindowSizeValue 283 | maxHeight?: WindowSizeValue 284 | } 285 | 286 | interface WindowPosition { 287 | x?: number 288 | y?: number 289 | z?: number 290 | } 291 | 292 | type WindowSizeValue = number | string | undefined 293 | 294 | interface ApplicationEntry { 295 | title?: string 296 | icon?: string 297 | category?: string 298 | visibility?: ApplicationEntryVisibility 299 | command: string | any 300 | } 301 | 302 | interface ApplicationEntryWithInherited extends ApplicationEntry { 303 | application: IApplicationController 304 | visibility: ApplicationEntryVisibility 305 | } 306 | 307 | type ApplicationEntryVisibility = 'primary' | 'secondary' | 'hidden' 308 | 309 | // DESKTOP 310 | 311 | interface IDesktopManager { 312 | defaultApps: DefaultAppsConfig 313 | 314 | getDefaultApp(feature: string) 315 | 316 | setDefaultApp( 317 | feature: string, 318 | application: IApplicationController, 319 | command: ApplicationCommand, 320 | ) 321 | } 322 | 323 | interface DesktopConfig { 324 | theme?: string 325 | modules?: string[] 326 | apps?: string[] 327 | 328 | name?: stringThis 329 | defaultApps?: DefaultAppsConfig 330 | features?: string[] 331 | 332 | systemBar?: DesktopSystemBarConfig 333 | dockBar?: DesktopDockBarConfig 334 | workspaces?: DesktopWorkspacesConfig 335 | } 336 | 337 | interface DesktopWindowsConfig { 338 | position: 'relative' | 'absolute' | 'fixed' 339 | } 340 | 341 | interface DesktopDockBarConfig { 342 | enabled?: boolean 343 | position?: 'top' | 'bottom' 344 | } 345 | 346 | interface DesktopSystemBarConfig { 347 | enabled?: boolean 348 | position?: 'top' | 'bottom' 349 | startButton?: boolean 350 | } 351 | 352 | interface DesktopWorkspacesConfig { 353 | enabled?: boolean 354 | } 355 | 356 | // TERMINAL 357 | 358 | type TerminalCommand = { 359 | name: string 360 | applicationId: string 361 | } 362 | 363 | interface CommandOutput { 364 | text: string 365 | isError?: boolean 366 | } 367 | 368 | // DEFAULT APP 369 | 370 | interface DefaultAppsConfig { 371 | terminal?: DefaultAppConfig 372 | browser?: DefaultAppConfig 373 | editor?: DefaultAppConfig 374 | 375 | [key: string]: DefaultAppConfig 376 | } 377 | 378 | interface DefaultAppConfig { 379 | application: IApplicationController 380 | entry: ApplicationEntry 381 | } 382 | 383 | export function defineDesktopApp(config: ApplicationConfig) 384 | export function defineDesktopConfig(config: DesktopConfig) 385 | export function registerTailwindPath(nuxt: Nuxt, path: string): void 386 | 387 | export {} 388 | -------------------------------------------------------------------------------- /packages/core/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/packages/core', 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 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'desktop' 3 | - 'apps/*' 4 | - 'packages/*' 5 | - 'themes/*' 6 | - 'plugins/*' 7 | -------------------------------------------------------------------------------- /template/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /template/.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* -------------------------------------------------------------------------------- /template/.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | link-workspace-packages=true 4 | -------------------------------------------------------------------------------- /template/.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | /dist 3 | /coverage 4 | /.nx/cache 5 | /.nx/workspace-data -------------------------------------------------------------------------------- /template/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false, 4 | "tabWidth": 4, 5 | "useTabs": false 6 | } 7 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/apps/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owdproject/client/80e6e3eb3b278762afeb631163e129299099acd4/template/apps/.gitkeep -------------------------------------------------------------------------------- /template/desktop/.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | -------------------------------------------------------------------------------- /template/desktop/app/app.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /template/desktop/app/assets/styles/index.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owdproject/client/80e6e3eb3b278762afeb631163e129299099acd4/template/desktop/app/assets/styles/index.scss -------------------------------------------------------------------------------- /template/desktop/app/pages/index.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /template/desktop/app/plugins/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owdproject/client/80e6e3eb3b278762afeb631163e129299099acd4/template/desktop/app/plugins/.gitkeep -------------------------------------------------------------------------------- /template/desktop/i18n/i18n.config.ts: -------------------------------------------------------------------------------- 1 | export default defineI18nConfig(() => { 2 | return { 3 | locale: 'en', 4 | messages: { 5 | en: { 6 | title: 'Open Web Desktop', 7 | }, 8 | }, 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /template/desktop/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin' 2 | import { defineNuxtConfig } from 'nuxt/config' 3 | 4 | export default defineNuxtConfig({ 5 | ssr: false, 6 | devServer: { 7 | host: '127.0.0.1', 8 | }, 9 | 10 | modules: ['@owdproject/core'], 11 | 12 | css: ['./desktop/app/assets/styles/index.scss'], 13 | 14 | i18n: { 15 | strategy: 'no_prefix', 16 | }, 17 | 18 | future: { 19 | compatibilityVersion: 4, 20 | }, 21 | 22 | imports: { 23 | autoImport: true, 24 | }, 25 | devtools: { 26 | enabled: false, 27 | }, 28 | typescript: { 29 | typeCheck: true, 30 | tsConfig: { 31 | extends: '../tsconfig.app.json', 32 | }, 33 | }, 34 | future: { 35 | compatibilityVersion: 4, 36 | }, 37 | compatibilityDate: '2025-05-13', 38 | vite: { 39 | plugins: [nxViteTsPaths()], 40 | }, 41 | }) 42 | -------------------------------------------------------------------------------- /template/desktop/owd.config.ts: -------------------------------------------------------------------------------- 1 | import { defineDesktopConfig } from '@owdproject/core/runtime/utils/utilDesktop' 2 | 3 | export default defineDesktopConfig({ 4 | apps: ['@owdproject/app-about'], 5 | modules: [], 6 | theme: '@owdproject/theme-win95', 7 | }) 8 | -------------------------------------------------------------------------------- /template/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/core": "^3.1.3", 16 | "@owdproject/theme-win95": "^0.1.0", 17 | "@owdproject/app-about": "^0.1.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /template/desktop/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "desktopx", 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 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/desktop/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [], 4 | "extends": "../tsconfig.base.json" 5 | } 6 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/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 | }, 68 | "release": { 69 | "projectsRelationship": "independent", 70 | "changelog": { 71 | "automaticFromRef": true, 72 | "projectChangelogs": { 73 | "renderOptions": { 74 | "authors": true, 75 | "commitReferences": true, 76 | "versionTitleDate": true, 77 | "applyUsernameToAuthors": true 78 | } 79 | }, 80 | "workspaceChangelog": { 81 | "renderOptions": { 82 | "authors": true, 83 | "commitReferences": true, 84 | "versionTitleDate": true, 85 | "applyUsernameToAuthors": true 86 | } 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /template/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": "GPL-3.0-or-later", 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 | "@eslint/eslintrc": "^3.3.1", 24 | "@eslint/js": "^9.26.0", 25 | "@nuxt/devtools": "2.4.0", 26 | "@nuxt/eslint-config": "~1.3.0", 27 | "@nuxt/kit": "^3.17.2", 28 | "@nuxt/ui-templates": "^1.3.4", 29 | "@nx/eslint": "21.0.3", 30 | "@nx/eslint-plugin": "21.0.3", 31 | "@nx/js": "^21.0.3", 32 | "@nx/nuxt": "21.0.3", 33 | "@nx/vite": "21.0.3", 34 | "@nx/web": "21.0.3", 35 | "@nx/workspace": "21.0.3", 36 | "@owdproject/nx": "^0.0.1", 37 | "@swc-node/register": "~1.10.10", 38 | "@swc/core": "~1.11.24", 39 | "@swc/helpers": "~0.5.17", 40 | "@types/node": "22.15.17", 41 | "@typescript-eslint/parser": "^8.32.0", 42 | "@vitejs/plugin-vue": "^5.2.4", 43 | "@vitest/coverage-v8": "^3.1.3", 44 | "@vitest/ui": "^3.1.3", 45 | "@vue/test-utils": "^2.4.6", 46 | "eslint": "^9.26.0", 47 | "eslint-config-prettier": "^10.1.5", 48 | "h3": "^1.15.3", 49 | "jiti": "2.4.2", 50 | "jsdom": "~26.1.0", 51 | "nuxt": "^3.17.2", 52 | "nx": "21.0.3", 53 | "prettier": "^3.5.3", 54 | "tslib": "^2.8.1", 55 | "typescript": "~5.8.3", 56 | "typescript-eslint": "^8.32.0", 57 | "vite": "^6.3.5", 58 | "vitest": "^3.1.3", 59 | "vue": "^3.5.13", 60 | "vue-router": "^4.5.1", 61 | "vue-tsc": "^2.2.10" 62 | }, 63 | "workspaces": [ 64 | "apps/*", 65 | "desktop/*", 66 | "packages/*", 67 | "themes/*" 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /template/packages/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owdproject/client/80e6e3eb3b278762afeb631163e129299099acd4/template/packages/.gitkeep -------------------------------------------------------------------------------- /template/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'desktop' 3 | - 'apps/*' 4 | - 'packages/*' 5 | - 'themes/*' 6 | -------------------------------------------------------------------------------- /template/presets/with-tests/.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | /dist 3 | /coverage 4 | /.nx/cache 5 | /.nx/workspace-data -------------------------------------------------------------------------------- /template/presets/with-tests/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /template/presets/with-tests/desktop/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { FlatCompat } from '@eslint/eslintrc'; 2 | import { dirname } from 'path'; 3 | import { fileURLToPath } from 'url'; 4 | import js from '@eslint/js'; 5 | import baseConfig from '../eslint.config.mjs'; 6 | const compat = new FlatCompat({ 7 | baseDirectory: dirname(fileURLToPath(import.meta.url)), 8 | recommendedConfig: js.configs.recommended, 9 | }); 10 | 11 | export default [ 12 | ...baseConfig, 13 | { 14 | files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.vue'], 15 | // Override or add rules here 16 | rules: {}, 17 | }, 18 | //...compat.extends('@nuxt/eslint-config'), 19 | { 20 | files: ['**/*.vue'], 21 | languageOptions: { 22 | parserOptions: { 23 | parser: await import('@typescript-eslint/parser'), 24 | }, 25 | }, 26 | }, 27 | { 28 | ignores: ['.nuxt/**', '.output/**', 'node_modules'], 29 | }, 30 | ]; 31 | -------------------------------------------------------------------------------- /template/presets/with-tests/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"} -------------------------------------------------------------------------------- /template/presets/with-tests/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: [ 18 | '{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}', 19 | ], 20 | reporters: ['default'], 21 | coverage: { 22 | reportsDirectory: './test-output/vitest/coverage', 23 | provider: 'v8' as const, 24 | }, 25 | }, 26 | })); 27 | -------------------------------------------------------------------------------- /template/presets/with-tests/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import nx from '@nx/eslint-plugin'; 2 | 3 | export default [ 4 | ...nx.configs['flat/base'], 5 | ...nx.configs['flat/typescript'], 6 | ...nx.configs['flat/javascript'], 7 | { 8 | ignores: [ 9 | '**/dist', 10 | '**/vite.config.*.timestamp*', 11 | '**/vitest.config.*.timestamp*', 12 | ], 13 | }, 14 | { 15 | files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.vue'], 16 | rules: { 17 | '@nx/enforce-module-boundaries': [ 18 | 'error', 19 | { 20 | enforceBuildableLibDependency: true, 21 | allow: ['^.*/eslint(\\.base)?\\.config\\.[cm]?js$'], 22 | depConstraints: [ 23 | { 24 | sourceTag: '*', 25 | onlyDependOnLibsWithTags: ['*'], 26 | }, 27 | ], 28 | }, 29 | ], 30 | }, 31 | }, 32 | { 33 | files: [ 34 | '**/*.ts', 35 | '**/*.tsx', 36 | '**/*.cts', 37 | '**/*.mts', 38 | '**/*.js', 39 | '**/*.jsx', 40 | '**/*.cjs', 41 | '**/*.mjs', 42 | ], 43 | // Override or add rules here 44 | rules: {}, 45 | }, 46 | ]; 47 | -------------------------------------------------------------------------------- /template/presets/with-tests/vitest.workspace.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | '**/vite.config.{mjs,js,ts,mts}', 3 | '**/vitest.config.{mjs,js,ts,mts}', 4 | ]; 5 | -------------------------------------------------------------------------------- /template/themes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owdproject/client/80e6e3eb3b278762afeb631163e129299099acd4/template/themes/.gitkeep -------------------------------------------------------------------------------- /template/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "declarationMap": true, 5 | "emitDeclarationOnly": true, 6 | "importHelpers": true, 7 | "isolatedModules": true, 8 | "lib": ["es2022"], 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "noEmitOnError": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "noImplicitOverride": true, 14 | "noImplicitReturns": true, 15 | "noUnusedLocals": true, 16 | "skipLibCheck": true, 17 | "strict": true, 18 | "target": "es2022", 19 | "customConditions": ["development"] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /template/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compileOnSave": false, 4 | "files": [] 5 | } 6 | -------------------------------------------------------------------------------- /themes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owdproject/client/80e6e3eb3b278762afeb631163e129299099acd4/themes/.gitkeep -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "declarationMap": true, 5 | "emitDeclarationOnly": true, 6 | "importHelpers": true, 7 | "isolatedModules": true, 8 | "lib": ["es2022"], 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "noEmitOnError": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "noImplicitOverride": true, 14 | "noImplicitReturns": true, 15 | "noUnusedLocals": true, 16 | "skipLibCheck": true, 17 | "strict": true, 18 | "target": "es2022", 19 | "customConditions": ["development"] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compileOnSave": false, 4 | "files": [], 5 | "references": [ 6 | { 7 | "path": "./desktop" 8 | }, 9 | { 10 | "path": "./packages/core" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /vitest.workspace.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | '**/vite.config.{mjs,js,ts,mts}', 3 | '**/vitest.config.{mjs,js,ts,mts}', 4 | ] 5 | --------------------------------------------------------------------------------