From c9c3fb820f465c62660c1c092f95f79f47c84e3d Mon Sep 17 00:00:00 2001 From: Victor Phos Date: Fri, 11 Sep 2026 23:48:01 +0000 Subject: [PATCH 01/36] Add About section to Settings with app description and author --- src/features/settings/SettingsScreen.tsx | 44 ++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/features/settings/SettingsScreen.tsx b/src/features/settings/SettingsScreen.tsx index 116d964..c628638 100644 --- a/src/features/settings/SettingsScreen.tsx +++ b/src/features/settings/SettingsScreen.tsx @@ -1,6 +1,8 @@ import { useTheme } from "../../hooks/useTheme"; import { useEffect, useState } from "react"; +const APP_VERSION = "0.6.0"; + export function SettingsScreen() { const theme = useTheme(); const [rounded, setRounded] = useState(() => { @@ -84,6 +86,48 @@ export function SettingsScreen() { +
+
+ About +
+
+
+
+ 115 + Ch +
+
+
Chelete
+
Version {APP_VERSION}
+
+
+
+ A fast, keyboard-driven personal finance tracker for Linux. + Manage accounts, track transactions, and project your balance + over time — without touching the mouse. Built for Omarchy with + live theme integration. +
+
+
+
+
Author
+
The Architect & QA
+
+
Victor Phos
+
+
+
+
+
License
+
+
MIT
+
+
+
+ ); From d56bc4f9a7a76a24083b833d2b9dae142b35ddcb Mon Sep 17 00:00:00 2001 From: Victor Phos Date: Fri, 11 Sep 2026 23:48:01 +0000 Subject: [PATCH 02/36] Add styles for About card in Settings --- src/styles/global.css | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/styles/global.css b/src/styles/global.css index 1d55c1b..74f9399 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -740,6 +740,43 @@ body { margin: 0; } +/* ── About ─────────────────────────────────────────────────── */ + +.about-card { + padding: 16px; +} + +.about-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 12px; +} + +.about-logo { + transform: scale(0.8); + transform-origin: left center; +} + +.about-name { + font-size: 15px; + font-weight: 600; + color: var(--chelete-fg); +} + +.about-version { + font-size: 11px; + color: var(--chelete-fg-muted); + margin-top: 2px; +} + +.about-desc { + font-size: 12px; + line-height: 1.6; + color: var(--chelete-fg-muted); + margin-bottom: 12px; +} + /* ── Icon picker ───────────────────────────────────────────── */ .icon-picker { From 2d249bb6a3aee4369ce7cc494ce91af9be715a6e Mon Sep 17 00:00:00 2001 From: Victor Phos Date: Fri, 11 Sep 2026 23:48:50 +0000 Subject: [PATCH 03/36] Remove Architect and QA credit from About author row --- src/features/settings/SettingsScreen.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/features/settings/SettingsScreen.tsx b/src/features/settings/SettingsScreen.tsx index c628638..7a0ee17 100644 --- a/src/features/settings/SettingsScreen.tsx +++ b/src/features/settings/SettingsScreen.tsx @@ -114,7 +114,6 @@ export function SettingsScreen() {
Author
-
The Architect & QA
Victor Phos
From f242210718ae8b9dcc1e81c9f6a2f6371a35b406 Mon Sep 17 00:00:00 2001 From: Victor Phos Date: Fri, 2 Oct 2026 14:03:19 +0200 Subject: [PATCH 04/36] Remove the React frontend and Tauri shell The app is being rewritten natively in Rust with GPUI. The web UI, its npm tooling and the Tauri configuration go; the Rust backend stays and is moved in the next commit. --- index.html | 13 - package-lock.json | 3648 ----------------- package.json | 35 - public/favicon.svg | 4 - src-tauri/build.rs | 3 - src-tauri/capabilities/default.json | 11 - src-tauri/gen/schemas/acl-manifests.json | 1 - src-tauri/gen/schemas/capabilities.json | 1 - src-tauri/gen/schemas/desktop-schema.json | 2678 ------------ src-tauri/gen/schemas/linux-schema.json | 2678 ------------ src-tauri/tauri.conf.json | 36 - src/App.tsx | 219 - src/components/CategoryIcons.tsx | 115 - src/components/CommandPalette.tsx | 112 - src/components/DateFilter.tsx | 207 - src/components/DonutChart.tsx | 201 - src/components/Pagination.tsx | 42 - src/features/accounts/AccountsScreen.tsx | 327 -- src/features/categories/CategoriesScreen.tsx | 358 -- src/features/overview/OverviewScreen.tsx | 378 -- .../projections/ProjectionsScreen.tsx | 369 -- src/features/settings/SettingsScreen.tsx | 133 - .../subscriptions/SubscriptionsScreen.tsx | 814 ---- src/features/transactions/CsvImportDialog.tsx | 825 ---- .../transactions/TransactionsScreen.tsx | 936 ----- src/hooks/useTheme.ts | 71 - src/main.tsx | 10 - src/services/__tests__/api.test.ts | 156 - src/services/__tests__/format.test.ts | 113 - src/services/__tests__/projection.test.ts | 186 - src/services/api.ts | 100 - src/services/format.ts | 66 - src/services/projection.ts | 138 - src/styles/global.css | 1507 ------- src/types.ts | 198 - src/vite-env.d.ts | 1 - tsconfig.json | 21 - vite.config.ts | 24 - vitest.config.ts | 10 - 39 files changed, 16745 deletions(-) delete mode 100644 index.html delete mode 100644 package-lock.json delete mode 100644 package.json delete mode 100644 public/favicon.svg delete mode 100644 src-tauri/build.rs delete mode 100644 src-tauri/capabilities/default.json delete mode 100644 src-tauri/gen/schemas/acl-manifests.json delete mode 100644 src-tauri/gen/schemas/capabilities.json delete mode 100644 src-tauri/gen/schemas/desktop-schema.json delete mode 100644 src-tauri/gen/schemas/linux-schema.json delete mode 100644 src-tauri/tauri.conf.json delete mode 100644 src/App.tsx delete mode 100644 src/components/CategoryIcons.tsx delete mode 100644 src/components/CommandPalette.tsx delete mode 100644 src/components/DateFilter.tsx delete mode 100644 src/components/DonutChart.tsx delete mode 100644 src/components/Pagination.tsx delete mode 100644 src/features/accounts/AccountsScreen.tsx delete mode 100644 src/features/categories/CategoriesScreen.tsx delete mode 100644 src/features/overview/OverviewScreen.tsx delete mode 100644 src/features/projections/ProjectionsScreen.tsx delete mode 100644 src/features/settings/SettingsScreen.tsx delete mode 100644 src/features/subscriptions/SubscriptionsScreen.tsx delete mode 100644 src/features/transactions/CsvImportDialog.tsx delete mode 100644 src/features/transactions/TransactionsScreen.tsx delete mode 100644 src/hooks/useTheme.ts delete mode 100644 src/main.tsx delete mode 100644 src/services/__tests__/api.test.ts delete mode 100644 src/services/__tests__/format.test.ts delete mode 100644 src/services/__tests__/projection.test.ts delete mode 100644 src/services/api.ts delete mode 100644 src/services/format.ts delete mode 100644 src/services/projection.ts delete mode 100644 src/styles/global.css delete mode 100644 src/types.ts delete mode 100644 src/vite-env.d.ts delete mode 100644 tsconfig.json delete mode 100644 vite.config.ts delete mode 100644 vitest.config.ts diff --git a/index.html b/index.html deleted file mode 100644 index 98261d6..0000000 --- a/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Chelete - - -
- - - diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index b440ab2..0000000 --- a/package-lock.json +++ /dev/null @@ -1,3648 +0,0 @@ -{ - "name": "chelete", - "version": "0.6.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "chelete", - "version": "0.6.0", - "dependencies": { - "@tauri-apps/api": "^2.0.0", - "@tauri-apps/plugin-shell": "^2.0.0", - "lucide-react": "^1.37.0", - "react": "^18.3.1", - "react-datepicker": "^6.9.0", - "react-dom": "^18.3.1", - "recharts": "^3.10.1" - }, - "devDependencies": { - "@tauri-apps/cli": "^2.0.0", - "@types/react": "^18.3.12", - "@types/react-datepicker": "^6.2.0", - "@types/react-dom": "^18.3.1", - "@vitejs/plugin-react": "^4.3.4", - "jsdom": "^30.0.1", - "typescript": "~5.6.2", - "vite": "^6.0.0", - "vitest": "^3.2.7" - } - }, - "node_modules/@asamuzakjp/css-color": { - "version": "6.0.7", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-6.0.7.tgz", - "integrity": "sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@csstools/css-calc": "^3.3.0", - "@csstools/css-color-parser": "^4.1.10", - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0", - "lru-cache": "^11.5.2" - }, - "engines": { - "node": "^22.13.0 || >=24.0.0" - } - }, - "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@asamuzakjp/dom-selector": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-8.3.2.tgz", - "integrity": "sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "bidi-js": "^1.0.3", - "css-tree": "^3.2.1", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.5.2" - }, - "engines": { - "node": "^22.13.0 || >=24.0.0" - } - }, - "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", - "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.29.7", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", - "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", - "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-compilation-targets": "^7.29.7", - "@babel/helper-module-transforms": "^7.29.7", - "@babel/helpers": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", - "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.8", - "@babel/types": "^7.29.8", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", - "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.29.7", - "@babel/helper-validator-option": "^7.29.7", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", - "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", - "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", - "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7", - "@babel/traverse": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", - "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", - "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", - "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", - "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", - "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.8" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", - "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", - "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", - "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", - "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.8", - "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.8", - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.8", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", - "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bramus/specificity": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", - "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "css-tree": "^3.0.0" - }, - "bin": { - "specificity": "bin/cli.js" - } - }, - "node_modules/@csstools/color-helpers": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.1.tgz", - "integrity": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/@csstools/css-calc": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", - "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-color-parser": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.2.tgz", - "integrity": "sha512-3QKjR/vxyjcSXBLgb6lP0S3MGdvwbmqSsvLPbYdVORqPDc8FX1HAJ0Spk38bxaRXgvENTA47tlhhbb5Z2e8hEg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/color-helpers": "^6.1.1", - "@csstools/css-calc": "^3.3.0" - }, - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-parser-algorithms": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", - "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.9.tgz", - "integrity": "sha512-iGGw4OsAYsS6pD29MdJ2bX/nJx65a04ZZiw6x+VwWlP2DdXf6f++Zmuv/OzALpdyfVhjbduIIF2cXM7HWBIe9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "peerDependencies": { - "css-tree": "^3.2.1" - }, - "peerDependenciesMeta": { - "css-tree": { - "optional": true - } - } - }, - "node_modules/@csstools/css-tokenizer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", - "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@exodus/bytes": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", - "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@noble/hashes": "^1.8.0 || ^2.0.0" - }, - "peerDependenciesMeta": { - "@noble/hashes": { - "optional": true - } - } - }, - "node_modules/@floating-ui/core": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", - "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.12" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", - "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.8.0", - "@floating-ui/utils": "^0.2.12" - } - }, - "node_modules/@floating-ui/react": { - "version": "0.26.28", - "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", - "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.1.2", - "@floating-ui/utils": "^0.2.8", - "tabbable": "^6.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", - "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.8.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", - "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", - "license": "MIT" - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", - "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@napi-rs/lzma-linux-x64-gnu": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", - "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^22.20 || ^24.12 || >=25" - } - }, - "node_modules/@reduxjs/toolkit": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", - "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "@standard-schema/utils": "^0.3.0", - "immer": "^11.0.0", - "redux": "^5.0.1", - "redux-thunk": "^3.1.0", - "reselect": "^5.1.0" - }, - "peerDependencies": { - "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", - "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-redux": { - "optional": true - } - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", - "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz", - "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz", - "integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz", - "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz", - "integrity": "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz", - "integrity": "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz", - "integrity": "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz", - "integrity": "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz", - "integrity": "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz", - "integrity": "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz", - "integrity": "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz", - "integrity": "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz", - "integrity": "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz", - "integrity": "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz", - "integrity": "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz", - "integrity": "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz", - "integrity": "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz", - "integrity": "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz", - "integrity": "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz", - "integrity": "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz", - "integrity": "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz", - "integrity": "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz", - "integrity": "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz", - "integrity": "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz", - "integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "license": "MIT" - }, - "node_modules/@standard-schema/utils": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", - "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", - "license": "MIT" - }, - "node_modules/@tauri-apps/api": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.1.tgz", - "integrity": "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==", - "license": "Apache-2.0 OR MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/tauri" - } - }, - "node_modules/@tauri-apps/cli": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.4.tgz", - "integrity": "sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==", - "dev": true, - "license": "Apache-2.0 OR MIT", - "bin": { - "tauri": "tauri.js" - }, - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/tauri" - }, - "optionalDependencies": { - "@tauri-apps/cli-darwin-arm64": "2.11.4", - "@tauri-apps/cli-darwin-x64": "2.11.4", - "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.4", - "@tauri-apps/cli-linux-arm64-gnu": "2.11.4", - "@tauri-apps/cli-linux-arm64-musl": "2.11.4", - "@tauri-apps/cli-linux-riscv64-gnu": "2.11.4", - "@tauri-apps/cli-linux-x64-gnu": "2.11.4", - "@tauri-apps/cli-linux-x64-musl": "2.11.4", - "@tauri-apps/cli-win32-arm64-msvc": "2.11.4", - "@tauri-apps/cli-win32-ia32-msvc": "2.11.4", - "@tauri-apps/cli-win32-x64-msvc": "2.11.4" - } - }, - "node_modules/@tauri-apps/cli-darwin-arm64": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.4.tgz", - "integrity": "sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-darwin-x64": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.4.tgz", - "integrity": "sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.4.tgz", - "integrity": "sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-arm64-gnu": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.4.tgz", - "integrity": "sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-arm64-musl": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.4.tgz", - "integrity": "sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.4.tgz", - "integrity": "sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-x64-gnu": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.4.tgz", - "integrity": "sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-x64-musl": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.4.tgz", - "integrity": "sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-win32-arm64-msvc": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.4.tgz", - "integrity": "sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-win32-ia32-msvc": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.4.tgz", - "integrity": "sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-win32-x64-msvc": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.4.tgz", - "integrity": "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/plugin-shell": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.3.5.tgz", - "integrity": "sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg==", - "license": "MIT OR Apache-2.0", - "dependencies": { - "@tauri-apps/api": "^2.10.1" - } - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/d3-array": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", - "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", - "license": "MIT" - }, - "node_modules/@types/d3-color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", - "license": "MIT" - }, - "node_modules/@types/d3-ease": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", - "license": "MIT" - }, - "node_modules/@types/d3-interpolate": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", - "license": "MIT", - "dependencies": { - "@types/d3-color": "*" - } - }, - "node_modules/@types/d3-path": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", - "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", - "license": "MIT" - }, - "node_modules/@types/d3-scale": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", - "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", - "license": "MIT", - "dependencies": { - "@types/d3-time": "*" - } - }, - "node_modules/@types/d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw==", - "license": "MIT", - "dependencies": { - "@types/d3-path": "*" - } - }, - "node_modules/@types/d3-time": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", - "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", - "license": "MIT" - }, - "node_modules/@types/d3-timer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", - "license": "MIT" - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "18.3.31", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", - "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-datepicker": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-6.2.0.tgz", - "integrity": "sha512-+JtO4Fm97WLkJTH8j8/v3Ldh7JCNRwjMYjRaKh4KHH0M3jJoXtwiD3JBCsdlg3tsFIw9eQSqyAPeVDN2H2oM9Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@floating-ui/react": "^0.26.2", - "@types/react": "*", - "date-fns": "^3.3.1" - } - }, - "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^18.0.0" - } - }, - "node_modules/@types/use-sync-external-store": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", - "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", - "license": "MIT" - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/@vitest/expect": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", - "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.7", - "@vitest/utils": "3.2.7", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", - "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "3.2.7", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", - "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", - "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "3.2.7", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", - "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.7", - "magic-string": "^0.30.17", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", - "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^4.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", - "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.7", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/baseline-browser-mapping": { - "version": "2.11.20", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz", - "integrity": "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/bidi-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", - "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "require-from-string": "^2.0.2" - } - }, - "node_modules/browserslist": { - "version": "4.28.8", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", - "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.11.12", - "caniuse-lite": "^1.0.30001809", - "electron-to-chromium": "^1.5.402", - "node-releases": "^2.0.53", - "update-browserslist-db": "^1.3.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001810", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", - "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/css-tree": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", - "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mdn-data": "2.27.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/d3-array": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "license": "ISC", - "dependencies": { - "internmap": "1 - 2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-format": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", - "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "license": "ISC", - "dependencies": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "license": "ISC", - "dependencies": { - "d3-path": "^3.1.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "license": "ISC", - "dependencies": { - "d3-array": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "license": "ISC", - "dependencies": { - "d3-time": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-timer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/data-urls": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", - "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/data-urls/node_modules/whatwg-url": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", - "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@exodus/bytes": "^1.11.0", - "tr46": "^6.0.0", - "webidl-conversions": "^8.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/date-fns": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", - "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/kossnocorp" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true, - "license": "MIT" - }, - "node_modules/decimal.js-light": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", - "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", - "license": "MIT" - }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.416", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.416.tgz", - "integrity": "sha512-K6bvB2BjnNrugtIih6ewlbBI9DXa976jIdiIlRLHhBoEI9a4JaQjjHyF+A1IQI543aQYR4LnmOrT/K5fZj0aPA==", - "dev": true, - "license": "ISC" - }, - "node_modules/entities": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", - "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-toolkit": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.52.0.tgz", - "integrity": "sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA==", - "license": "MIT", - "workspaces": [ - "docs", - "benchmarks", - "tests/types", - "tests/browser-compat" - ] - }, - "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", - "license": "MIT" - }, - "node_modules/expect-type": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", - "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/html-encoding-sniffer": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", - "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@exodus/bytes": "^1.6.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/immer": { - "version": "11.1.18", - "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.18.tgz", - "integrity": "sha512-EQyQtLiYW029lyoczMl/Hh4Xu7cDecSc58JRYpHyL4tIAu3eqd1yJzQX04d2BZHDkzFFvm6qJEJWOtfDSWAXbQ==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, - "node_modules/internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/jsdom": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.0.1.tgz", - "integrity": "sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/css-color": "^6.0.5", - "@asamuzakjp/dom-selector": "^8.3.0", - "@bramus/specificity": "^2.4.2", - "@csstools/css-syntax-patches-for-csstree": "^1.1.7", - "@exodus/bytes": "^1.15.1", - "css-tree": "^3.2.1", - "data-urls": "^7.0.0", - "decimal.js": "^10.6.0", - "html-encoding-sniffer": "^6.0.0", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.5.2", - "parse5": "^8.0.1", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.2", - "undici": "^8.9.0", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^8.0.1", - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^17.1.0", - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": "^22.22.2 || ^24.15.0 || >=26.0.0" - }, - "peerDependencies": { - "canvas": "^3.2.3" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jsdom/node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lucide-react": { - "version": "1.37.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.37.0.tgz", - "integrity": "sha512-LPsB4rD1TD6wZu1djKOf9vUnS1jTNaHbolXebXDgiTdb6jeA1agIJhJsIybCmjKmQClcOaal1o1OaiYahEftyQ==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/mdn-data": { - "version": "2.27.1", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", - "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.18", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", - "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/node-releases": { - "version": "2.0.54", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", - "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/parse5": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", - "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "entities": "^8.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", - "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.26", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", - "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.17", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-datepicker": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-6.9.0.tgz", - "integrity": "sha512-QTxuzeem7BUfVFWv+g5WuvzT0c5BPo+XTCNbMTZKSZQLU+cMMwSUHwspaxuIcDlwNcOH0tiJ+bh1fJ2yxOGYWA==", - "license": "MIT", - "dependencies": { - "@floating-ui/react": "^0.26.2", - "clsx": "^2.1.0", - "date-fns": "^3.3.1", - "prop-types": "^15.7.2", - "react-onclickoutside": "^6.13.0" - }, - "peerDependencies": { - "react": "^16.9.0 || ^17 || ^18", - "react-dom": "^16.9.0 || ^17 || ^18" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, - "node_modules/react-onclickoutside": { - "version": "6.13.2", - "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.2.tgz", - "integrity": "sha512-h6Hbf1c8b7tIYY4u90mDdBLY4+AGQVMFtIE89HgC0DtVCh/JfKl477gYqUtGLmjZBKK3MJxomP/lFiLbz4sq9A==", - "license": "MIT", - "funding": { - "type": "individual", - "url": "https://github.com/Pomax/react-onclickoutside/blob/master/FUNDING.md" - }, - "peerDependencies": { - "react": "^15.5.x || ^16.x || ^17.x || ^18.x", - "react-dom": "^15.5.x || ^16.x || ^17.x || ^18.x" - } - }, - "node_modules/react-redux": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", - "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", - "license": "MIT", - "dependencies": { - "@types/use-sync-external-store": "^0.0.6", - "use-sync-external-store": "^1.4.0" - }, - "peerDependencies": { - "@types/react": "^18.2.25 || ^19", - "react": "^18.0 || ^19", - "redux": "^5.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "redux": { - "optional": true - } - } - }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/recharts": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.10.1.tgz", - "integrity": "sha512-QXFrvt6IVcw7eeZCoyXTwkIJAX3Dv1nyVhMicXJ47GsGDDpcN8z6o644DibE9XjpBTThtsomLKnTV6lc+cVFUA==", - "license": "MIT", - "workspaces": [ - "www" - ], - "dependencies": { - "@reduxjs/toolkit": "^1.9.0 || 2.x.x", - "clsx": "^2.1.1", - "decimal.js-light": "^2.5.1", - "es-toolkit": "^1.39.3", - "eventemitter3": "^5.0.1", - "immer": "^11.1.8", - "react-redux": "8.x.x || 9.x.x", - "reselect": "5.2.0", - "tiny-invariant": "^1.3.3", - "use-sync-external-store": "^1.2.2", - "victory-vendor": "^37.0.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/redux": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" - }, - "node_modules/redux-thunk": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", - "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", - "license": "MIT", - "peerDependencies": { - "redux": "^5.0.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/reselect": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz", - "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==", - "license": "MIT" - }, - "node_modules/rollup": { - "version": "4.63.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.1.tgz", - "integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.9" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@napi-rs/lzma-linux-x64-gnu": "1.5.1", - "@rollup/rollup-android-arm-eabi": "4.63.1", - "@rollup/rollup-android-arm64": "4.63.1", - "@rollup/rollup-darwin-arm64": "4.63.1", - "@rollup/rollup-darwin-x64": "4.63.1", - "@rollup/rollup-freebsd-arm64": "4.63.1", - "@rollup/rollup-freebsd-x64": "4.63.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", - "@rollup/rollup-linux-arm-musleabihf": "4.63.1", - "@rollup/rollup-linux-arm64-gnu": "4.63.1", - "@rollup/rollup-linux-arm64-musl": "4.63.1", - "@rollup/rollup-linux-loong64-gnu": "4.63.1", - "@rollup/rollup-linux-loong64-musl": "4.63.1", - "@rollup/rollup-linux-ppc64-gnu": "4.63.1", - "@rollup/rollup-linux-ppc64-musl": "4.63.1", - "@rollup/rollup-linux-riscv64-gnu": "4.63.1", - "@rollup/rollup-linux-riscv64-musl": "4.63.1", - "@rollup/rollup-linux-s390x-gnu": "4.63.1", - "@rollup/rollup-linux-x64-gnu": "4.63.1", - "@rollup/rollup-linux-x64-musl": "4.63.1", - "@rollup/rollup-openbsd-x64": "4.63.1", - "@rollup/rollup-openharmony-arm64": "4.63.1", - "@rollup/rollup-win32-arm64-msvc": "4.63.1", - "@rollup/rollup-win32-ia32-msvc": "4.63.1", - "@rollup/rollup-win32-x64-gnu": "4.63.1", - "@rollup/rollup-win32-x64-msvc": "4.63.1", - "fsevents": "~2.3.2" - } - }, - "node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dev": true, - "license": "ISC", - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } - }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "dev": true, - "license": "MIT" - }, - "node_modules/strip-literal": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", - "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/strip-literal/node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true, - "license": "MIT" - }, - "node_modules/tabbable": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz", - "integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==", - "license": "MIT" - }, - "node_modules/tiny-invariant": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", - "license": "MIT" - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, - "node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", - "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tldts": { - "version": "7.4.11", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.11.tgz", - "integrity": "sha512-aBiNayCfTQxuIJBm06M+xR14cYaYlDlSXZbgsnKzKNxDKUVq7KFwTjwBSsb7m9Y5xO8WfPnBc63WaYFMTGlvqw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tldts-core": "^7.4.11" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts-core": { - "version": "7.4.11", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.11.tgz", - "integrity": "sha512-CW3WN2rIIE/Of21mulhgnGOwoDyEFNygyIBOONSdyAuSATgMMUCpLeUlB+E8sAwA5xRV9hYPl+kyZ9citHCaKg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tough-cookie": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", - "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^7.0.5" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/tr46": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", - "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/typescript": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", - "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz", - "integrity": "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=22.19.0" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", - "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/use-sync-external-store": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.7.0.tgz", - "integrity": "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/victory-vendor": { - "version": "37.3.6", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", - "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", - "license": "MIT AND ISC", - "dependencies": { - "@types/d3-array": "^3.0.3", - "@types/d3-ease": "^3.0.0", - "@types/d3-interpolate": "^3.0.1", - "@types/d3-scale": "^4.0.2", - "@types/d3-shape": "^3.1.0", - "@types/d3-time": "^3.0.0", - "@types/d3-timer": "^3.0.0", - "d3-array": "^3.1.6", - "d3-ease": "^3.0.1", - "d3-interpolate": "^3.0.1", - "d3-scale": "^4.0.2", - "d3-shape": "^3.1.0", - "d3-time": "^3.0.0", - "d3-timer": "^3.0.1" - } - }, - "node_modules/vite": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", - "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite-node": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", - "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vitest": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", - "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.7", - "@vitest/mocker": "3.2.7", - "@vitest/pretty-format": "^3.2.7", - "@vitest/runner": "3.2.7", - "@vitest/snapshot": "3.2.7", - "@vitest/spy": "3.2.7", - "@vitest/utils": "3.2.7", - "chai": "^5.2.0", - "debug": "^4.4.1", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", - "pathe": "^2.0.3", - "picomatch": "^4.0.2", - "std-env": "^3.9.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.14", - "tinypool": "^1.1.1", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.4", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.7", - "@vitest/ui": "3.2.7", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/debug": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/w3c-xmlserializer": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", - "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/webidl-conversions": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", - "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=20" - } - }, - "node_modules/whatwg-mimetype": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", - "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/whatwg-url": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.0.tgz", - "integrity": "sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@exodus/bytes": "^1.15.1", - "tr46": "^6.0.0", - "webidl-conversions": "^8.0.1" - }, - "engines": { - "node": "^22.14.0 || >=24.0.0" - } - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/xml-name-validator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true, - "license": "MIT" - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 68d18d6..0000000 --- a/package.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "chelete", - "private": true, - "version": "0.6.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc -b && vite build", - "preview": "vite preview", - "tauri": "tauri", - "test": "vitest run", - "test:watch": "vitest", - "seed": "cd src-tauri && cargo run --bin seed" - }, - "dependencies": { - "@tauri-apps/api": "^2.0.0", - "@tauri-apps/plugin-shell": "^2.0.0", - "lucide-react": "^1.37.0", - "react": "^18.3.1", - "react-datepicker": "^6.9.0", - "react-dom": "^18.3.1", - "recharts": "^3.10.1" - }, - "devDependencies": { - "@tauri-apps/cli": "^2.0.0", - "@types/react": "^18.3.12", - "@types/react-datepicker": "^6.2.0", - "@types/react-dom": "^18.3.1", - "@vitejs/plugin-react": "^4.3.4", - "jsdom": "^30.0.1", - "typescript": "~5.6.2", - "vite": "^6.0.0", - "vitest": "^3.2.7" - } -} diff --git a/public/favicon.svg b/public/favicon.svg deleted file mode 100644 index 91cf06f..0000000 --- a/public/favicon.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src-tauri/build.rs b/src-tauri/build.rs deleted file mode 100644 index d860e1e..0000000 --- a/src-tauri/build.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - tauri_build::build() -} diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json deleted file mode 100644 index 9ea9de6..0000000 --- a/src-tauri/capabilities/default.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "identifier": "default", - "description": "Default application permissions", - "windows": ["main"], - "permissions": [ - "core:default", - "shell:allow-open", - "dialog:allow-open", - "dialog:allow-save" - ] -} diff --git a/src-tauri/gen/schemas/acl-manifests.json b/src-tauri/gen/schemas/acl-manifests.json deleted file mode 100644 index 9a4e12e..0000000 --- a/src-tauri/gen/schemas/acl-manifests.json +++ /dev/null @@ -1 +0,0 @@ -{"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener","allow-supports-multiple-windows"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-supports-multiple-windows":{"identifier":"allow-supports-multiple-windows","description":"Enables the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":["supports_multiple_windows"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_data_store"]}},"deny-remove-listener":{"identifier":"deny-remove-listener","description":"Denies the remove_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_listener"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-set-dock-visibility":{"identifier":"deny-set-dock-visibility","description":"Denies the set_dock_visibility command without any pre-configured scope.","commands":{"allow":[],"deny":["set_dock_visibility"]}},"deny-supports-multiple-windows":{"identifier":"deny-supports-multiple-windows","description":"Denies the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["supports_multiple_windows"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-icon-with-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-icon-with-as-template":{"identifier":"allow-set-icon-with-as-template","description":"Enables the set_icon_with_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_with_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-icon-with-as-template":{"identifier":"deny-set-icon-with-as-template","description":"Denies the set_icon_with_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_with_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-auto-resize":{"identifier":"allow-set-webview-auto-resize","description":"Enables the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":["set_webview_auto_resize"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-auto-resize":{"identifier":"deny-set-webview-auto-resize","description":"Denies the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_auto_resize"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-is-always-on-top","allow-activity-name","allow-scene-identifier","allow-internal-toggle-maximize"]},"permissions":{"allow-activity-name":{"identifier":"allow-activity-name","description":"Enables the activity_name command without any pre-configured scope.","commands":{"allow":["activity_name"],"deny":[]}},"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-always-on-top":{"identifier":"allow-is-always-on-top","description":"Enables the is_always_on_top command without any pre-configured scope.","commands":{"allow":["is_always_on_top"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-scene-identifier":{"identifier":"allow-scene-identifier","description":"Enables the scene_identifier command without any pre-configured scope.","commands":{"allow":["scene_identifier"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-focusable":{"identifier":"allow-set-focusable","description":"Enables the set_focusable command without any pre-configured scope.","commands":{"allow":["set_focusable"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-simple-fullscreen":{"identifier":"allow-set-simple-fullscreen","description":"Enables the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":["set_simple_fullscreen"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-activity-name":{"identifier":"deny-activity-name","description":"Denies the activity_name command without any pre-configured scope.","commands":{"allow":[],"deny":["activity_name"]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-always-on-top":{"identifier":"deny-is-always-on-top","description":"Denies the is_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["is_always_on_top"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-scene-identifier":{"identifier":"deny-scene-identifier","description":"Denies the scene_identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["scene_identifier"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-focusable":{"identifier":"deny-set-focusable","description":"Denies the set_focusable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focusable"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-simple-fullscreen":{"identifier":"deny-set-simple-fullscreen","description":"Denies the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_simple_fullscreen"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"dialog":{"default_permission":{"identifier":"default","description":"This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n","permissions":["allow-message","allow-save","allow-open"]},"permissions":{"allow-ask":{"identifier":"allow-ask","description":"Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)","commands":{"allow":["message"],"deny":[]}},"allow-confirm":{"identifier":"allow-confirm","description":"Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)","commands":{"allow":["message"],"deny":[]}},"allow-message":{"identifier":"allow-message","description":"Enables the message command without any pre-configured scope.","commands":{"allow":["message"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-save":{"identifier":"allow-save","description":"Enables the save command without any pre-configured scope.","commands":{"allow":["save"],"deny":[]}},"deny-ask":{"identifier":"deny-ask","description":"Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)","commands":{"allow":[],"deny":["message"]}},"deny-confirm":{"identifier":"deny-confirm","description":"Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)","commands":{"allow":[],"deny":["message"]}},"deny-message":{"identifier":"deny-message","description":"Denies the message command without any pre-configured scope.","commands":{"allow":[],"deny":["message"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-save":{"identifier":"deny-save","description":"Denies the save command without any pre-configured scope.","commands":{"allow":[],"deny":["save"]}}},"permission_sets":{},"global_scope_schema":null},"shell":{"default_permission":{"identifier":"default","description":"This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n","permissions":["allow-open"]},"permissions":{"allow-execute":{"identifier":"allow-execute","description":"Enables the execute command without any pre-configured scope.","commands":{"allow":["execute"],"deny":[]}},"allow-kill":{"identifier":"allow-kill","description":"Enables the kill command without any pre-configured scope.","commands":{"allow":["kill"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-spawn":{"identifier":"allow-spawn","description":"Enables the spawn command without any pre-configured scope.","commands":{"allow":["spawn"],"deny":[]}},"allow-stdin-write":{"identifier":"allow-stdin-write","description":"Enables the stdin_write command without any pre-configured scope.","commands":{"allow":["stdin_write"],"deny":[]}},"deny-execute":{"identifier":"deny-execute","description":"Denies the execute command without any pre-configured scope.","commands":{"allow":[],"deny":["execute"]}},"deny-kill":{"identifier":"deny-kill","description":"Denies the kill command without any pre-configured scope.","commands":{"allow":[],"deny":["kill"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-spawn":{"identifier":"deny-spawn","description":"Denies the spawn command without any pre-configured scope.","commands":{"allow":[],"deny":["spawn"]}},"deny-stdin-write":{"identifier":"deny-stdin-write","description":"Denies the stdin_write command without any pre-configured scope.","commands":{"allow":[],"deny":["stdin_write"]}}},"permission_sets":{},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","anyOf":[{"additionalProperties":false,"properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellScopeEntryAllowedArgs"}],"description":"The allowed arguments for the command execution."},"cmd":{"description":"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.","type":"string"},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"}},"required":["cmd","name"],"type":"object"},{"additionalProperties":false,"properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellScopeEntryAllowedArgs"}],"description":"The allowed arguments for the command execution."},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"},"sidecar":{"description":"If this command is a sidecar command.","type":"boolean"}},"required":["name","sidecar"],"type":"object"}],"definitions":{"ShellScopeEntryAllowedArg":{"anyOf":[{"description":"A non-configurable argument that is passed to the command in the order it was specified.","type":"string"},{"additionalProperties":false,"description":"A variable that is set while calling the command from the webview API.","properties":{"raw":{"default":false,"description":"Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.","type":"boolean"},"validator":{"description":"[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: ","type":"string"}},"required":["validator"],"type":"object"}],"description":"A command argument allowed to be executed by the webview API."},"ShellScopeEntryAllowedArgs":{"anyOf":[{"description":"Use a simple boolean to allow all or disable all arguments to this command configuration.","type":"boolean"},{"description":"A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.","items":{"$ref":"#/definitions/ShellScopeEntryAllowedArg"},"type":"array"}],"description":"A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration."}},"description":"Shell scope entry.","title":"ShellScopeEntry"}}} \ No newline at end of file diff --git a/src-tauri/gen/schemas/capabilities.json b/src-tauri/gen/schemas/capabilities.json deleted file mode 100644 index 04a998e..0000000 --- a/src-tauri/gen/schemas/capabilities.json +++ /dev/null @@ -1 +0,0 @@ -{"default":{"identifier":"default","description":"Default application permissions","local":true,"windows":["main"],"permissions":["core:default","shell:allow-open","dialog:allow-open","dialog:allow-save"]}} \ No newline at end of file diff --git a/src-tauri/gen/schemas/desktop-schema.json b/src-tauri/gen/schemas/desktop-schema.json deleted file mode 100644 index 6eba1dc..0000000 --- a/src-tauri/gen/schemas/desktop-schema.json +++ /dev/null @@ -1,2678 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "CapabilityFile", - "description": "Capability formats accepted in a capability file.", - "anyOf": [ - { - "description": "A single capability.", - "allOf": [ - { - "$ref": "#/definitions/Capability" - } - ] - }, - { - "description": "A list of capabilities.", - "type": "array", - "items": { - "$ref": "#/definitions/Capability" - } - }, - { - "description": "A list of capabilities.", - "type": "object", - "required": [ - "capabilities" - ], - "properties": { - "capabilities": { - "description": "The list of capabilities.", - "type": "array", - "items": { - "$ref": "#/definitions/Capability" - } - } - } - } - ], - "definitions": { - "Capability": { - "description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```", - "type": "object", - "required": [ - "identifier", - "permissions" - ], - "properties": { - "identifier": { - "description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`", - "type": "string" - }, - "description": { - "description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.", - "default": "", - "type": "string" - }, - "remote": { - "description": "Configure remote URLs that can use the capability permissions.\n\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\n\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\n\n## Example\n\n```json { \"urls\": [\"https://*.mydomain.dev\"] } ```", - "anyOf": [ - { - "$ref": "#/definitions/CapabilityRemote" - }, - { - "type": "null" - } - ] - }, - "local": { - "description": "Whether this capability is enabled for local app URLs or not. Defaults to `true`.", - "default": true, - "type": "boolean" - }, - "windows": { - "description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\n\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`", - "type": "array", - "items": { - "type": "string" - } - }, - "webviews": { - "description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`", - "type": "array", - "items": { - "type": "string" - } - }, - "permissions": { - "description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```", - "type": "array", - "items": { - "$ref": "#/definitions/PermissionEntry" - }, - "uniqueItems": true - }, - "platforms": { - "description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Target" - } - } - } - }, - "CapabilityRemote": { - "description": "Configuration for remote URLs that are associated with the capability.", - "type": "object", - "required": [ - "urls" - ], - "properties": { - "urls": { - "description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api", - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "PermissionEntry": { - "description": "An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.", - "anyOf": [ - { - "description": "Reference a permission or permission set by identifier.", - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ] - }, - { - "description": "Reference a permission or permission set by identifier and extends its scope.", - "type": "object", - "allOf": [ - { - "if": { - "properties": { - "identifier": { - "anyOf": [ - { - "description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`", - "type": "string", - "const": "shell:default", - "markdownDescription": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`" - }, - { - "description": "Enables the execute command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-execute", - "markdownDescription": "Enables the execute command without any pre-configured scope." - }, - { - "description": "Enables the kill command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-kill", - "markdownDescription": "Enables the kill command without any pre-configured scope." - }, - { - "description": "Enables the open command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-open", - "markdownDescription": "Enables the open command without any pre-configured scope." - }, - { - "description": "Enables the spawn command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-spawn", - "markdownDescription": "Enables the spawn command without any pre-configured scope." - }, - { - "description": "Enables the stdin_write command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-stdin-write", - "markdownDescription": "Enables the stdin_write command without any pre-configured scope." - }, - { - "description": "Denies the execute command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-execute", - "markdownDescription": "Denies the execute command without any pre-configured scope." - }, - { - "description": "Denies the kill command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-kill", - "markdownDescription": "Denies the kill command without any pre-configured scope." - }, - { - "description": "Denies the open command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-open", - "markdownDescription": "Denies the open command without any pre-configured scope." - }, - { - "description": "Denies the spawn command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-spawn", - "markdownDescription": "Denies the spawn command without any pre-configured scope." - }, - { - "description": "Denies the stdin_write command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-stdin-write", - "markdownDescription": "Denies the stdin_write command without any pre-configured scope." - } - ] - } - } - }, - "then": { - "properties": { - "allow": { - "items": { - "title": "ShellScopeEntry", - "description": "Shell scope entry.", - "anyOf": [ - { - "type": "object", - "required": [ - "cmd", - "name" - ], - "properties": { - "args": { - "description": "The allowed arguments for the command execution.", - "allOf": [ - { - "$ref": "#/definitions/ShellScopeEntryAllowedArgs" - } - ] - }, - "cmd": { - "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", - "type": "string" - }, - "name": { - "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", - "type": "string" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "name", - "sidecar" - ], - "properties": { - "args": { - "description": "The allowed arguments for the command execution.", - "allOf": [ - { - "$ref": "#/definitions/ShellScopeEntryAllowedArgs" - } - ] - }, - "name": { - "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", - "type": "string" - }, - "sidecar": { - "description": "If this command is a sidecar command.", - "type": "boolean" - } - }, - "additionalProperties": false - } - ] - } - }, - "deny": { - "items": { - "title": "ShellScopeEntry", - "description": "Shell scope entry.", - "anyOf": [ - { - "type": "object", - "required": [ - "cmd", - "name" - ], - "properties": { - "args": { - "description": "The allowed arguments for the command execution.", - "allOf": [ - { - "$ref": "#/definitions/ShellScopeEntryAllowedArgs" - } - ] - }, - "cmd": { - "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", - "type": "string" - }, - "name": { - "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", - "type": "string" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "name", - "sidecar" - ], - "properties": { - "args": { - "description": "The allowed arguments for the command execution.", - "allOf": [ - { - "$ref": "#/definitions/ShellScopeEntryAllowedArgs" - } - ] - }, - "name": { - "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", - "type": "string" - }, - "sidecar": { - "description": "If this command is a sidecar command.", - "type": "boolean" - } - }, - "additionalProperties": false - } - ] - } - } - } - }, - "properties": { - "identifier": { - "description": "Identifier of the permission or permission set.", - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ] - } - } - }, - { - "properties": { - "identifier": { - "description": "Identifier of the permission or permission set.", - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ] - }, - "allow": { - "description": "Data that defines what is allowed by the scope.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Value" - } - }, - "deny": { - "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Value" - } - } - } - } - ], - "required": [ - "identifier" - ] - } - ] - }, - "Identifier": { - "description": "Permission identifier", - "oneOf": [ - { - "description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`", - "type": "string", - "const": "core:default", - "markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`" - }, - { - "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`", - "type": "string", - "const": "core:app:default", - "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`" - }, - { - "description": "Enables the app_hide command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-app-hide", - "markdownDescription": "Enables the app_hide command without any pre-configured scope." - }, - { - "description": "Enables the app_show command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-app-show", - "markdownDescription": "Enables the app_show command without any pre-configured scope." - }, - { - "description": "Enables the bundle_type command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-bundle-type", - "markdownDescription": "Enables the bundle_type command without any pre-configured scope." - }, - { - "description": "Enables the default_window_icon command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-default-window-icon", - "markdownDescription": "Enables the default_window_icon command without any pre-configured scope." - }, - { - "description": "Enables the fetch_data_store_identifiers command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-fetch-data-store-identifiers", - "markdownDescription": "Enables the fetch_data_store_identifiers command without any pre-configured scope." - }, - { - "description": "Enables the identifier command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-identifier", - "markdownDescription": "Enables the identifier command without any pre-configured scope." - }, - { - "description": "Enables the name command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-name", - "markdownDescription": "Enables the name command without any pre-configured scope." - }, - { - "description": "Enables the register_listener command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-register-listener", - "markdownDescription": "Enables the register_listener command without any pre-configured scope." - }, - { - "description": "Enables the remove_data_store command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-remove-data-store", - "markdownDescription": "Enables the remove_data_store command without any pre-configured scope." - }, - { - "description": "Enables the remove_listener command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-remove-listener", - "markdownDescription": "Enables the remove_listener command without any pre-configured scope." - }, - { - "description": "Enables the set_app_theme command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-set-app-theme", - "markdownDescription": "Enables the set_app_theme command without any pre-configured scope." - }, - { - "description": "Enables the set_dock_visibility command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-set-dock-visibility", - "markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope." - }, - { - "description": "Enables the supports_multiple_windows command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-supports-multiple-windows", - "markdownDescription": "Enables the supports_multiple_windows command without any pre-configured scope." - }, - { - "description": "Enables the tauri_version command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-tauri-version", - "markdownDescription": "Enables the tauri_version command without any pre-configured scope." - }, - { - "description": "Enables the version command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-version", - "markdownDescription": "Enables the version command without any pre-configured scope." - }, - { - "description": "Denies the app_hide command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-app-hide", - "markdownDescription": "Denies the app_hide command without any pre-configured scope." - }, - { - "description": "Denies the app_show command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-app-show", - "markdownDescription": "Denies the app_show command without any pre-configured scope." - }, - { - "description": "Denies the bundle_type command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-bundle-type", - "markdownDescription": "Denies the bundle_type command without any pre-configured scope." - }, - { - "description": "Denies the default_window_icon command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-default-window-icon", - "markdownDescription": "Denies the default_window_icon command without any pre-configured scope." - }, - { - "description": "Denies the fetch_data_store_identifiers command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-fetch-data-store-identifiers", - "markdownDescription": "Denies the fetch_data_store_identifiers command without any pre-configured scope." - }, - { - "description": "Denies the identifier command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-identifier", - "markdownDescription": "Denies the identifier command without any pre-configured scope." - }, - { - "description": "Denies the name command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-name", - "markdownDescription": "Denies the name command without any pre-configured scope." - }, - { - "description": "Denies the register_listener command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-register-listener", - "markdownDescription": "Denies the register_listener command without any pre-configured scope." - }, - { - "description": "Denies the remove_data_store command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-remove-data-store", - "markdownDescription": "Denies the remove_data_store command without any pre-configured scope." - }, - { - "description": "Denies the remove_listener command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-remove-listener", - "markdownDescription": "Denies the remove_listener command without any pre-configured scope." - }, - { - "description": "Denies the set_app_theme command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-set-app-theme", - "markdownDescription": "Denies the set_app_theme command without any pre-configured scope." - }, - { - "description": "Denies the set_dock_visibility command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-set-dock-visibility", - "markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope." - }, - { - "description": "Denies the supports_multiple_windows command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-supports-multiple-windows", - "markdownDescription": "Denies the supports_multiple_windows command without any pre-configured scope." - }, - { - "description": "Denies the tauri_version command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-tauri-version", - "markdownDescription": "Denies the tauri_version command without any pre-configured scope." - }, - { - "description": "Denies the version command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-version", - "markdownDescription": "Denies the version command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`", - "type": "string", - "const": "core:event:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`" - }, - { - "description": "Enables the emit command without any pre-configured scope.", - "type": "string", - "const": "core:event:allow-emit", - "markdownDescription": "Enables the emit command without any pre-configured scope." - }, - { - "description": "Enables the emit_to command without any pre-configured scope.", - "type": "string", - "const": "core:event:allow-emit-to", - "markdownDescription": "Enables the emit_to command without any pre-configured scope." - }, - { - "description": "Enables the listen command without any pre-configured scope.", - "type": "string", - "const": "core:event:allow-listen", - "markdownDescription": "Enables the listen command without any pre-configured scope." - }, - { - "description": "Enables the unlisten command without any pre-configured scope.", - "type": "string", - "const": "core:event:allow-unlisten", - "markdownDescription": "Enables the unlisten command without any pre-configured scope." - }, - { - "description": "Denies the emit command without any pre-configured scope.", - "type": "string", - "const": "core:event:deny-emit", - "markdownDescription": "Denies the emit command without any pre-configured scope." - }, - { - "description": "Denies the emit_to command without any pre-configured scope.", - "type": "string", - "const": "core:event:deny-emit-to", - "markdownDescription": "Denies the emit_to command without any pre-configured scope." - }, - { - "description": "Denies the listen command without any pre-configured scope.", - "type": "string", - "const": "core:event:deny-listen", - "markdownDescription": "Denies the listen command without any pre-configured scope." - }, - { - "description": "Denies the unlisten command without any pre-configured scope.", - "type": "string", - "const": "core:event:deny-unlisten", - "markdownDescription": "Denies the unlisten command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`", - "type": "string", - "const": "core:image:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`" - }, - { - "description": "Enables the from_bytes command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-from-bytes", - "markdownDescription": "Enables the from_bytes command without any pre-configured scope." - }, - { - "description": "Enables the from_path command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-from-path", - "markdownDescription": "Enables the from_path command without any pre-configured scope." - }, - { - "description": "Enables the new command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-new", - "markdownDescription": "Enables the new command without any pre-configured scope." - }, - { - "description": "Enables the rgba command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-rgba", - "markdownDescription": "Enables the rgba command without any pre-configured scope." - }, - { - "description": "Enables the size command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-size", - "markdownDescription": "Enables the size command without any pre-configured scope." - }, - { - "description": "Denies the from_bytes command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-from-bytes", - "markdownDescription": "Denies the from_bytes command without any pre-configured scope." - }, - { - "description": "Denies the from_path command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-from-path", - "markdownDescription": "Denies the from_path command without any pre-configured scope." - }, - { - "description": "Denies the new command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-new", - "markdownDescription": "Denies the new command without any pre-configured scope." - }, - { - "description": "Denies the rgba command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-rgba", - "markdownDescription": "Denies the rgba command without any pre-configured scope." - }, - { - "description": "Denies the size command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-size", - "markdownDescription": "Denies the size command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`", - "type": "string", - "const": "core:menu:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`" - }, - { - "description": "Enables the append command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-append", - "markdownDescription": "Enables the append command without any pre-configured scope." - }, - { - "description": "Enables the create_default command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-create-default", - "markdownDescription": "Enables the create_default command without any pre-configured scope." - }, - { - "description": "Enables the get command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-get", - "markdownDescription": "Enables the get command without any pre-configured scope." - }, - { - "description": "Enables the insert command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-insert", - "markdownDescription": "Enables the insert command without any pre-configured scope." - }, - { - "description": "Enables the is_checked command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-is-checked", - "markdownDescription": "Enables the is_checked command without any pre-configured scope." - }, - { - "description": "Enables the is_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-is-enabled", - "markdownDescription": "Enables the is_enabled command without any pre-configured scope." - }, - { - "description": "Enables the items command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-items", - "markdownDescription": "Enables the items command without any pre-configured scope." - }, - { - "description": "Enables the new command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-new", - "markdownDescription": "Enables the new command without any pre-configured scope." - }, - { - "description": "Enables the popup command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-popup", - "markdownDescription": "Enables the popup command without any pre-configured scope." - }, - { - "description": "Enables the prepend command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-prepend", - "markdownDescription": "Enables the prepend command without any pre-configured scope." - }, - { - "description": "Enables the remove command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-remove", - "markdownDescription": "Enables the remove command without any pre-configured scope." - }, - { - "description": "Enables the remove_at command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-remove-at", - "markdownDescription": "Enables the remove_at command without any pre-configured scope." - }, - { - "description": "Enables the set_accelerator command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-accelerator", - "markdownDescription": "Enables the set_accelerator command without any pre-configured scope." - }, - { - "description": "Enables the set_as_app_menu command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-as-app-menu", - "markdownDescription": "Enables the set_as_app_menu command without any pre-configured scope." - }, - { - "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-as-help-menu-for-nsapp", - "markdownDescription": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope." - }, - { - "description": "Enables the set_as_window_menu command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-as-window-menu", - "markdownDescription": "Enables the set_as_window_menu command without any pre-configured scope." - }, - { - "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-as-windows-menu-for-nsapp", - "markdownDescription": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope." - }, - { - "description": "Enables the set_checked command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-checked", - "markdownDescription": "Enables the set_checked command without any pre-configured scope." - }, - { - "description": "Enables the set_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-enabled", - "markdownDescription": "Enables the set_enabled command without any pre-configured scope." - }, - { - "description": "Enables the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-icon", - "markdownDescription": "Enables the set_icon command without any pre-configured scope." - }, - { - "description": "Enables the set_text command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-text", - "markdownDescription": "Enables the set_text command without any pre-configured scope." - }, - { - "description": "Enables the text command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-text", - "markdownDescription": "Enables the text command without any pre-configured scope." - }, - { - "description": "Denies the append command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-append", - "markdownDescription": "Denies the append command without any pre-configured scope." - }, - { - "description": "Denies the create_default command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-create-default", - "markdownDescription": "Denies the create_default command without any pre-configured scope." - }, - { - "description": "Denies the get command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-get", - "markdownDescription": "Denies the get command without any pre-configured scope." - }, - { - "description": "Denies the insert command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-insert", - "markdownDescription": "Denies the insert command without any pre-configured scope." - }, - { - "description": "Denies the is_checked command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-is-checked", - "markdownDescription": "Denies the is_checked command without any pre-configured scope." - }, - { - "description": "Denies the is_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-is-enabled", - "markdownDescription": "Denies the is_enabled command without any pre-configured scope." - }, - { - "description": "Denies the items command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-items", - "markdownDescription": "Denies the items command without any pre-configured scope." - }, - { - "description": "Denies the new command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-new", - "markdownDescription": "Denies the new command without any pre-configured scope." - }, - { - "description": "Denies the popup command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-popup", - "markdownDescription": "Denies the popup command without any pre-configured scope." - }, - { - "description": "Denies the prepend command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-prepend", - "markdownDescription": "Denies the prepend command without any pre-configured scope." - }, - { - "description": "Denies the remove command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-remove", - "markdownDescription": "Denies the remove command without any pre-configured scope." - }, - { - "description": "Denies the remove_at command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-remove-at", - "markdownDescription": "Denies the remove_at command without any pre-configured scope." - }, - { - "description": "Denies the set_accelerator command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-accelerator", - "markdownDescription": "Denies the set_accelerator command without any pre-configured scope." - }, - { - "description": "Denies the set_as_app_menu command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-as-app-menu", - "markdownDescription": "Denies the set_as_app_menu command without any pre-configured scope." - }, - { - "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-as-help-menu-for-nsapp", - "markdownDescription": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope." - }, - { - "description": "Denies the set_as_window_menu command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-as-window-menu", - "markdownDescription": "Denies the set_as_window_menu command without any pre-configured scope." - }, - { - "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-as-windows-menu-for-nsapp", - "markdownDescription": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope." - }, - { - "description": "Denies the set_checked command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-checked", - "markdownDescription": "Denies the set_checked command without any pre-configured scope." - }, - { - "description": "Denies the set_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-enabled", - "markdownDescription": "Denies the set_enabled command without any pre-configured scope." - }, - { - "description": "Denies the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-icon", - "markdownDescription": "Denies the set_icon command without any pre-configured scope." - }, - { - "description": "Denies the set_text command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-text", - "markdownDescription": "Denies the set_text command without any pre-configured scope." - }, - { - "description": "Denies the text command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-text", - "markdownDescription": "Denies the text command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`", - "type": "string", - "const": "core:path:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`" - }, - { - "description": "Enables the basename command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-basename", - "markdownDescription": "Enables the basename command without any pre-configured scope." - }, - { - "description": "Enables the dirname command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-dirname", - "markdownDescription": "Enables the dirname command without any pre-configured scope." - }, - { - "description": "Enables the extname command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-extname", - "markdownDescription": "Enables the extname command without any pre-configured scope." - }, - { - "description": "Enables the is_absolute command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-is-absolute", - "markdownDescription": "Enables the is_absolute command without any pre-configured scope." - }, - { - "description": "Enables the join command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-join", - "markdownDescription": "Enables the join command without any pre-configured scope." - }, - { - "description": "Enables the normalize command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-normalize", - "markdownDescription": "Enables the normalize command without any pre-configured scope." - }, - { - "description": "Enables the resolve command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-resolve", - "markdownDescription": "Enables the resolve command without any pre-configured scope." - }, - { - "description": "Enables the resolve_directory command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-resolve-directory", - "markdownDescription": "Enables the resolve_directory command without any pre-configured scope." - }, - { - "description": "Denies the basename command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-basename", - "markdownDescription": "Denies the basename command without any pre-configured scope." - }, - { - "description": "Denies the dirname command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-dirname", - "markdownDescription": "Denies the dirname command without any pre-configured scope." - }, - { - "description": "Denies the extname command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-extname", - "markdownDescription": "Denies the extname command without any pre-configured scope." - }, - { - "description": "Denies the is_absolute command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-is-absolute", - "markdownDescription": "Denies the is_absolute command without any pre-configured scope." - }, - { - "description": "Denies the join command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-join", - "markdownDescription": "Denies the join command without any pre-configured scope." - }, - { - "description": "Denies the normalize command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-normalize", - "markdownDescription": "Denies the normalize command without any pre-configured scope." - }, - { - "description": "Denies the resolve command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-resolve", - "markdownDescription": "Denies the resolve command without any pre-configured scope." - }, - { - "description": "Denies the resolve_directory command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-resolve-directory", - "markdownDescription": "Denies the resolve_directory command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`", - "type": "string", - "const": "core:resources:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`" - }, - { - "description": "Enables the close command without any pre-configured scope.", - "type": "string", - "const": "core:resources:allow-close", - "markdownDescription": "Enables the close command without any pre-configured scope." - }, - { - "description": "Denies the close command without any pre-configured scope.", - "type": "string", - "const": "core:resources:deny-close", - "markdownDescription": "Denies the close command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`", - "type": "string", - "const": "core:tray:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`" - }, - { - "description": "Enables the get_by_id command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-get-by-id", - "markdownDescription": "Enables the get_by_id command without any pre-configured scope." - }, - { - "description": "Enables the new command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-new", - "markdownDescription": "Enables the new command without any pre-configured scope." - }, - { - "description": "Enables the remove_by_id command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-remove-by-id", - "markdownDescription": "Enables the remove_by_id command without any pre-configured scope." - }, - { - "description": "Enables the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-icon", - "markdownDescription": "Enables the set_icon command without any pre-configured scope." - }, - { - "description": "Enables the set_icon_as_template command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-icon-as-template", - "markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope." - }, - { - "description": "Enables the set_icon_with_as_template command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-icon-with-as-template", - "markdownDescription": "Enables the set_icon_with_as_template command without any pre-configured scope." - }, - { - "description": "Enables the set_menu command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-menu", - "markdownDescription": "Enables the set_menu command without any pre-configured scope." - }, - { - "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-show-menu-on-left-click", - "markdownDescription": "Enables the set_show_menu_on_left_click command without any pre-configured scope." - }, - { - "description": "Enables the set_temp_dir_path command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-temp-dir-path", - "markdownDescription": "Enables the set_temp_dir_path command without any pre-configured scope." - }, - { - "description": "Enables the set_title command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-title", - "markdownDescription": "Enables the set_title command without any pre-configured scope." - }, - { - "description": "Enables the set_tooltip command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-tooltip", - "markdownDescription": "Enables the set_tooltip command without any pre-configured scope." - }, - { - "description": "Enables the set_visible command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-visible", - "markdownDescription": "Enables the set_visible command without any pre-configured scope." - }, - { - "description": "Denies the get_by_id command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-get-by-id", - "markdownDescription": "Denies the get_by_id command without any pre-configured scope." - }, - { - "description": "Denies the new command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-new", - "markdownDescription": "Denies the new command without any pre-configured scope." - }, - { - "description": "Denies the remove_by_id command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-remove-by-id", - "markdownDescription": "Denies the remove_by_id command without any pre-configured scope." - }, - { - "description": "Denies the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-icon", - "markdownDescription": "Denies the set_icon command without any pre-configured scope." - }, - { - "description": "Denies the set_icon_as_template command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-icon-as-template", - "markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope." - }, - { - "description": "Denies the set_icon_with_as_template command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-icon-with-as-template", - "markdownDescription": "Denies the set_icon_with_as_template command without any pre-configured scope." - }, - { - "description": "Denies the set_menu command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-menu", - "markdownDescription": "Denies the set_menu command without any pre-configured scope." - }, - { - "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-show-menu-on-left-click", - "markdownDescription": "Denies the set_show_menu_on_left_click command without any pre-configured scope." - }, - { - "description": "Denies the set_temp_dir_path command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-temp-dir-path", - "markdownDescription": "Denies the set_temp_dir_path command without any pre-configured scope." - }, - { - "description": "Denies the set_title command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-title", - "markdownDescription": "Denies the set_title command without any pre-configured scope." - }, - { - "description": "Denies the set_tooltip command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-tooltip", - "markdownDescription": "Denies the set_tooltip command without any pre-configured scope." - }, - { - "description": "Denies the set_visible command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-visible", - "markdownDescription": "Denies the set_visible command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`", - "type": "string", - "const": "core:webview:default", - "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`" - }, - { - "description": "Enables the clear_all_browsing_data command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-clear-all-browsing-data", - "markdownDescription": "Enables the clear_all_browsing_data command without any pre-configured scope." - }, - { - "description": "Enables the create_webview command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-create-webview", - "markdownDescription": "Enables the create_webview command without any pre-configured scope." - }, - { - "description": "Enables the create_webview_window command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-create-webview-window", - "markdownDescription": "Enables the create_webview_window command without any pre-configured scope." - }, - { - "description": "Enables the get_all_webviews command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-get-all-webviews", - "markdownDescription": "Enables the get_all_webviews command without any pre-configured scope." - }, - { - "description": "Enables the internal_toggle_devtools command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-internal-toggle-devtools", - "markdownDescription": "Enables the internal_toggle_devtools command without any pre-configured scope." - }, - { - "description": "Enables the print command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-print", - "markdownDescription": "Enables the print command without any pre-configured scope." - }, - { - "description": "Enables the reparent command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-reparent", - "markdownDescription": "Enables the reparent command without any pre-configured scope." - }, - { - "description": "Enables the set_webview_auto_resize command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-auto-resize", - "markdownDescription": "Enables the set_webview_auto_resize command without any pre-configured scope." - }, - { - "description": "Enables the set_webview_background_color command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-background-color", - "markdownDescription": "Enables the set_webview_background_color command without any pre-configured scope." - }, - { - "description": "Enables the set_webview_focus command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-focus", - "markdownDescription": "Enables the set_webview_focus command without any pre-configured scope." - }, - { - "description": "Enables the set_webview_position command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-position", - "markdownDescription": "Enables the set_webview_position command without any pre-configured scope." - }, - { - "description": "Enables the set_webview_size command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-size", - "markdownDescription": "Enables the set_webview_size command without any pre-configured scope." - }, - { - "description": "Enables the set_webview_zoom command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-zoom", - "markdownDescription": "Enables the set_webview_zoom command without any pre-configured scope." - }, - { - "description": "Enables the webview_close command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-close", - "markdownDescription": "Enables the webview_close command without any pre-configured scope." - }, - { - "description": "Enables the webview_hide command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-hide", - "markdownDescription": "Enables the webview_hide command without any pre-configured scope." - }, - { - "description": "Enables the webview_position command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-position", - "markdownDescription": "Enables the webview_position command without any pre-configured scope." - }, - { - "description": "Enables the webview_show command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-show", - "markdownDescription": "Enables the webview_show command without any pre-configured scope." - }, - { - "description": "Enables the webview_size command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-size", - "markdownDescription": "Enables the webview_size command without any pre-configured scope." - }, - { - "description": "Denies the clear_all_browsing_data command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-clear-all-browsing-data", - "markdownDescription": "Denies the clear_all_browsing_data command without any pre-configured scope." - }, - { - "description": "Denies the create_webview command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-create-webview", - "markdownDescription": "Denies the create_webview command without any pre-configured scope." - }, - { - "description": "Denies the create_webview_window command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-create-webview-window", - "markdownDescription": "Denies the create_webview_window command without any pre-configured scope." - }, - { - "description": "Denies the get_all_webviews command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-get-all-webviews", - "markdownDescription": "Denies the get_all_webviews command without any pre-configured scope." - }, - { - "description": "Denies the internal_toggle_devtools command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-internal-toggle-devtools", - "markdownDescription": "Denies the internal_toggle_devtools command without any pre-configured scope." - }, - { - "description": "Denies the print command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-print", - "markdownDescription": "Denies the print command without any pre-configured scope." - }, - { - "description": "Denies the reparent command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-reparent", - "markdownDescription": "Denies the reparent command without any pre-configured scope." - }, - { - "description": "Denies the set_webview_auto_resize command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-auto-resize", - "markdownDescription": "Denies the set_webview_auto_resize command without any pre-configured scope." - }, - { - "description": "Denies the set_webview_background_color command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-background-color", - "markdownDescription": "Denies the set_webview_background_color command without any pre-configured scope." - }, - { - "description": "Denies the set_webview_focus command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-focus", - "markdownDescription": "Denies the set_webview_focus command without any pre-configured scope." - }, - { - "description": "Denies the set_webview_position command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-position", - "markdownDescription": "Denies the set_webview_position command without any pre-configured scope." - }, - { - "description": "Denies the set_webview_size command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-size", - "markdownDescription": "Denies the set_webview_size command without any pre-configured scope." - }, - { - "description": "Denies the set_webview_zoom command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-zoom", - "markdownDescription": "Denies the set_webview_zoom command without any pre-configured scope." - }, - { - "description": "Denies the webview_close command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-close", - "markdownDescription": "Denies the webview_close command without any pre-configured scope." - }, - { - "description": "Denies the webview_hide command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-hide", - "markdownDescription": "Denies the webview_hide command without any pre-configured scope." - }, - { - "description": "Denies the webview_position command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-position", - "markdownDescription": "Denies the webview_position command without any pre-configured scope." - }, - { - "description": "Denies the webview_show command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-show", - "markdownDescription": "Denies the webview_show command without any pre-configured scope." - }, - { - "description": "Denies the webview_size command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-size", - "markdownDescription": "Denies the webview_size command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`", - "type": "string", - "const": "core:window:default", - "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`" - }, - { - "description": "Enables the activity_name command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-activity-name", - "markdownDescription": "Enables the activity_name command without any pre-configured scope." - }, - { - "description": "Enables the available_monitors command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-available-monitors", - "markdownDescription": "Enables the available_monitors command without any pre-configured scope." - }, - { - "description": "Enables the center command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-center", - "markdownDescription": "Enables the center command without any pre-configured scope." - }, - { - "description": "Enables the close command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-close", - "markdownDescription": "Enables the close command without any pre-configured scope." - }, - { - "description": "Enables the create command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-create", - "markdownDescription": "Enables the create command without any pre-configured scope." - }, - { - "description": "Enables the current_monitor command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-current-monitor", - "markdownDescription": "Enables the current_monitor command without any pre-configured scope." - }, - { - "description": "Enables the cursor_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-cursor-position", - "markdownDescription": "Enables the cursor_position command without any pre-configured scope." - }, - { - "description": "Enables the destroy command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-destroy", - "markdownDescription": "Enables the destroy command without any pre-configured scope." - }, - { - "description": "Enables the get_all_windows command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-get-all-windows", - "markdownDescription": "Enables the get_all_windows command without any pre-configured scope." - }, - { - "description": "Enables the hide command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-hide", - "markdownDescription": "Enables the hide command without any pre-configured scope." - }, - { - "description": "Enables the inner_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-inner-position", - "markdownDescription": "Enables the inner_position command without any pre-configured scope." - }, - { - "description": "Enables the inner_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-inner-size", - "markdownDescription": "Enables the inner_size command without any pre-configured scope." - }, - { - "description": "Enables the internal_toggle_maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-internal-toggle-maximize", - "markdownDescription": "Enables the internal_toggle_maximize command without any pre-configured scope." - }, - { - "description": "Enables the is_always_on_top command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-always-on-top", - "markdownDescription": "Enables the is_always_on_top command without any pre-configured scope." - }, - { - "description": "Enables the is_closable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-closable", - "markdownDescription": "Enables the is_closable command without any pre-configured scope." - }, - { - "description": "Enables the is_decorated command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-decorated", - "markdownDescription": "Enables the is_decorated command without any pre-configured scope." - }, - { - "description": "Enables the is_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-enabled", - "markdownDescription": "Enables the is_enabled command without any pre-configured scope." - }, - { - "description": "Enables the is_focused command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-focused", - "markdownDescription": "Enables the is_focused command without any pre-configured scope." - }, - { - "description": "Enables the is_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-fullscreen", - "markdownDescription": "Enables the is_fullscreen command without any pre-configured scope." - }, - { - "description": "Enables the is_maximizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-maximizable", - "markdownDescription": "Enables the is_maximizable command without any pre-configured scope." - }, - { - "description": "Enables the is_maximized command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-maximized", - "markdownDescription": "Enables the is_maximized command without any pre-configured scope." - }, - { - "description": "Enables the is_minimizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-minimizable", - "markdownDescription": "Enables the is_minimizable command without any pre-configured scope." - }, - { - "description": "Enables the is_minimized command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-minimized", - "markdownDescription": "Enables the is_minimized command without any pre-configured scope." - }, - { - "description": "Enables the is_resizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-resizable", - "markdownDescription": "Enables the is_resizable command without any pre-configured scope." - }, - { - "description": "Enables the is_visible command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-visible", - "markdownDescription": "Enables the is_visible command without any pre-configured scope." - }, - { - "description": "Enables the maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-maximize", - "markdownDescription": "Enables the maximize command without any pre-configured scope." - }, - { - "description": "Enables the minimize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-minimize", - "markdownDescription": "Enables the minimize command without any pre-configured scope." - }, - { - "description": "Enables the monitor_from_point command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-monitor-from-point", - "markdownDescription": "Enables the monitor_from_point command without any pre-configured scope." - }, - { - "description": "Enables the outer_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-outer-position", - "markdownDescription": "Enables the outer_position command without any pre-configured scope." - }, - { - "description": "Enables the outer_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-outer-size", - "markdownDescription": "Enables the outer_size command without any pre-configured scope." - }, - { - "description": "Enables the primary_monitor command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-primary-monitor", - "markdownDescription": "Enables the primary_monitor command without any pre-configured scope." - }, - { - "description": "Enables the request_user_attention command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-request-user-attention", - "markdownDescription": "Enables the request_user_attention command without any pre-configured scope." - }, - { - "description": "Enables the scale_factor command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-scale-factor", - "markdownDescription": "Enables the scale_factor command without any pre-configured scope." - }, - { - "description": "Enables the scene_identifier command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-scene-identifier", - "markdownDescription": "Enables the scene_identifier command without any pre-configured scope." - }, - { - "description": "Enables the set_always_on_bottom command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-always-on-bottom", - "markdownDescription": "Enables the set_always_on_bottom command without any pre-configured scope." - }, - { - "description": "Enables the set_always_on_top command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-always-on-top", - "markdownDescription": "Enables the set_always_on_top command without any pre-configured scope." - }, - { - "description": "Enables the set_background_color command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-background-color", - "markdownDescription": "Enables the set_background_color command without any pre-configured scope." - }, - { - "description": "Enables the set_badge_count command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-badge-count", - "markdownDescription": "Enables the set_badge_count command without any pre-configured scope." - }, - { - "description": "Enables the set_badge_label command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-badge-label", - "markdownDescription": "Enables the set_badge_label command without any pre-configured scope." - }, - { - "description": "Enables the set_closable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-closable", - "markdownDescription": "Enables the set_closable command without any pre-configured scope." - }, - { - "description": "Enables the set_content_protected command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-content-protected", - "markdownDescription": "Enables the set_content_protected command without any pre-configured scope." - }, - { - "description": "Enables the set_cursor_grab command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-cursor-grab", - "markdownDescription": "Enables the set_cursor_grab command without any pre-configured scope." - }, - { - "description": "Enables the set_cursor_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-cursor-icon", - "markdownDescription": "Enables the set_cursor_icon command without any pre-configured scope." - }, - { - "description": "Enables the set_cursor_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-cursor-position", - "markdownDescription": "Enables the set_cursor_position command without any pre-configured scope." - }, - { - "description": "Enables the set_cursor_visible command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-cursor-visible", - "markdownDescription": "Enables the set_cursor_visible command without any pre-configured scope." - }, - { - "description": "Enables the set_decorations command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-decorations", - "markdownDescription": "Enables the set_decorations command without any pre-configured scope." - }, - { - "description": "Enables the set_effects command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-effects", - "markdownDescription": "Enables the set_effects command without any pre-configured scope." - }, - { - "description": "Enables the set_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-enabled", - "markdownDescription": "Enables the set_enabled command without any pre-configured scope." - }, - { - "description": "Enables the set_focus command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-focus", - "markdownDescription": "Enables the set_focus command without any pre-configured scope." - }, - { - "description": "Enables the set_focusable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-focusable", - "markdownDescription": "Enables the set_focusable command without any pre-configured scope." - }, - { - "description": "Enables the set_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-fullscreen", - "markdownDescription": "Enables the set_fullscreen command without any pre-configured scope." - }, - { - "description": "Enables the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-icon", - "markdownDescription": "Enables the set_icon command without any pre-configured scope." - }, - { - "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-ignore-cursor-events", - "markdownDescription": "Enables the set_ignore_cursor_events command without any pre-configured scope." - }, - { - "description": "Enables the set_max_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-max-size", - "markdownDescription": "Enables the set_max_size command without any pre-configured scope." - }, - { - "description": "Enables the set_maximizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-maximizable", - "markdownDescription": "Enables the set_maximizable command without any pre-configured scope." - }, - { - "description": "Enables the set_min_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-min-size", - "markdownDescription": "Enables the set_min_size command without any pre-configured scope." - }, - { - "description": "Enables the set_minimizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-minimizable", - "markdownDescription": "Enables the set_minimizable command without any pre-configured scope." - }, - { - "description": "Enables the set_overlay_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-overlay-icon", - "markdownDescription": "Enables the set_overlay_icon command without any pre-configured scope." - }, - { - "description": "Enables the set_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-position", - "markdownDescription": "Enables the set_position command without any pre-configured scope." - }, - { - "description": "Enables the set_progress_bar command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-progress-bar", - "markdownDescription": "Enables the set_progress_bar command without any pre-configured scope." - }, - { - "description": "Enables the set_resizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-resizable", - "markdownDescription": "Enables the set_resizable command without any pre-configured scope." - }, - { - "description": "Enables the set_shadow command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-shadow", - "markdownDescription": "Enables the set_shadow command without any pre-configured scope." - }, - { - "description": "Enables the set_simple_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-simple-fullscreen", - "markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope." - }, - { - "description": "Enables the set_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-size", - "markdownDescription": "Enables the set_size command without any pre-configured scope." - }, - { - "description": "Enables the set_size_constraints command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-size-constraints", - "markdownDescription": "Enables the set_size_constraints command without any pre-configured scope." - }, - { - "description": "Enables the set_skip_taskbar command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-skip-taskbar", - "markdownDescription": "Enables the set_skip_taskbar command without any pre-configured scope." - }, - { - "description": "Enables the set_theme command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-theme", - "markdownDescription": "Enables the set_theme command without any pre-configured scope." - }, - { - "description": "Enables the set_title command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-title", - "markdownDescription": "Enables the set_title command without any pre-configured scope." - }, - { - "description": "Enables the set_title_bar_style command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-title-bar-style", - "markdownDescription": "Enables the set_title_bar_style command without any pre-configured scope." - }, - { - "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-visible-on-all-workspaces", - "markdownDescription": "Enables the set_visible_on_all_workspaces command without any pre-configured scope." - }, - { - "description": "Enables the show command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-show", - "markdownDescription": "Enables the show command without any pre-configured scope." - }, - { - "description": "Enables the start_dragging command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-start-dragging", - "markdownDescription": "Enables the start_dragging command without any pre-configured scope." - }, - { - "description": "Enables the start_resize_dragging command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-start-resize-dragging", - "markdownDescription": "Enables the start_resize_dragging command without any pre-configured scope." - }, - { - "description": "Enables the theme command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-theme", - "markdownDescription": "Enables the theme command without any pre-configured scope." - }, - { - "description": "Enables the title command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-title", - "markdownDescription": "Enables the title command without any pre-configured scope." - }, - { - "description": "Enables the toggle_maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-toggle-maximize", - "markdownDescription": "Enables the toggle_maximize command without any pre-configured scope." - }, - { - "description": "Enables the unmaximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-unmaximize", - "markdownDescription": "Enables the unmaximize command without any pre-configured scope." - }, - { - "description": "Enables the unminimize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-unminimize", - "markdownDescription": "Enables the unminimize command without any pre-configured scope." - }, - { - "description": "Denies the activity_name command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-activity-name", - "markdownDescription": "Denies the activity_name command without any pre-configured scope." - }, - { - "description": "Denies the available_monitors command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-available-monitors", - "markdownDescription": "Denies the available_monitors command without any pre-configured scope." - }, - { - "description": "Denies the center command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-center", - "markdownDescription": "Denies the center command without any pre-configured scope." - }, - { - "description": "Denies the close command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-close", - "markdownDescription": "Denies the close command without any pre-configured scope." - }, - { - "description": "Denies the create command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-create", - "markdownDescription": "Denies the create command without any pre-configured scope." - }, - { - "description": "Denies the current_monitor command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-current-monitor", - "markdownDescription": "Denies the current_monitor command without any pre-configured scope." - }, - { - "description": "Denies the cursor_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-cursor-position", - "markdownDescription": "Denies the cursor_position command without any pre-configured scope." - }, - { - "description": "Denies the destroy command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-destroy", - "markdownDescription": "Denies the destroy command without any pre-configured scope." - }, - { - "description": "Denies the get_all_windows command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-get-all-windows", - "markdownDescription": "Denies the get_all_windows command without any pre-configured scope." - }, - { - "description": "Denies the hide command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-hide", - "markdownDescription": "Denies the hide command without any pre-configured scope." - }, - { - "description": "Denies the inner_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-inner-position", - "markdownDescription": "Denies the inner_position command without any pre-configured scope." - }, - { - "description": "Denies the inner_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-inner-size", - "markdownDescription": "Denies the inner_size command without any pre-configured scope." - }, - { - "description": "Denies the internal_toggle_maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-internal-toggle-maximize", - "markdownDescription": "Denies the internal_toggle_maximize command without any pre-configured scope." - }, - { - "description": "Denies the is_always_on_top command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-always-on-top", - "markdownDescription": "Denies the is_always_on_top command without any pre-configured scope." - }, - { - "description": "Denies the is_closable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-closable", - "markdownDescription": "Denies the is_closable command without any pre-configured scope." - }, - { - "description": "Denies the is_decorated command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-decorated", - "markdownDescription": "Denies the is_decorated command without any pre-configured scope." - }, - { - "description": "Denies the is_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-enabled", - "markdownDescription": "Denies the is_enabled command without any pre-configured scope." - }, - { - "description": "Denies the is_focused command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-focused", - "markdownDescription": "Denies the is_focused command without any pre-configured scope." - }, - { - "description": "Denies the is_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-fullscreen", - "markdownDescription": "Denies the is_fullscreen command without any pre-configured scope." - }, - { - "description": "Denies the is_maximizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-maximizable", - "markdownDescription": "Denies the is_maximizable command without any pre-configured scope." - }, - { - "description": "Denies the is_maximized command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-maximized", - "markdownDescription": "Denies the is_maximized command without any pre-configured scope." - }, - { - "description": "Denies the is_minimizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-minimizable", - "markdownDescription": "Denies the is_minimizable command without any pre-configured scope." - }, - { - "description": "Denies the is_minimized command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-minimized", - "markdownDescription": "Denies the is_minimized command without any pre-configured scope." - }, - { - "description": "Denies the is_resizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-resizable", - "markdownDescription": "Denies the is_resizable command without any pre-configured scope." - }, - { - "description": "Denies the is_visible command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-visible", - "markdownDescription": "Denies the is_visible command without any pre-configured scope." - }, - { - "description": "Denies the maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-maximize", - "markdownDescription": "Denies the maximize command without any pre-configured scope." - }, - { - "description": "Denies the minimize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-minimize", - "markdownDescription": "Denies the minimize command without any pre-configured scope." - }, - { - "description": "Denies the monitor_from_point command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-monitor-from-point", - "markdownDescription": "Denies the monitor_from_point command without any pre-configured scope." - }, - { - "description": "Denies the outer_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-outer-position", - "markdownDescription": "Denies the outer_position command without any pre-configured scope." - }, - { - "description": "Denies the outer_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-outer-size", - "markdownDescription": "Denies the outer_size command without any pre-configured scope." - }, - { - "description": "Denies the primary_monitor command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-primary-monitor", - "markdownDescription": "Denies the primary_monitor command without any pre-configured scope." - }, - { - "description": "Denies the request_user_attention command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-request-user-attention", - "markdownDescription": "Denies the request_user_attention command without any pre-configured scope." - }, - { - "description": "Denies the scale_factor command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-scale-factor", - "markdownDescription": "Denies the scale_factor command without any pre-configured scope." - }, - { - "description": "Denies the scene_identifier command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-scene-identifier", - "markdownDescription": "Denies the scene_identifier command without any pre-configured scope." - }, - { - "description": "Denies the set_always_on_bottom command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-always-on-bottom", - "markdownDescription": "Denies the set_always_on_bottom command without any pre-configured scope." - }, - { - "description": "Denies the set_always_on_top command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-always-on-top", - "markdownDescription": "Denies the set_always_on_top command without any pre-configured scope." - }, - { - "description": "Denies the set_background_color command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-background-color", - "markdownDescription": "Denies the set_background_color command without any pre-configured scope." - }, - { - "description": "Denies the set_badge_count command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-badge-count", - "markdownDescription": "Denies the set_badge_count command without any pre-configured scope." - }, - { - "description": "Denies the set_badge_label command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-badge-label", - "markdownDescription": "Denies the set_badge_label command without any pre-configured scope." - }, - { - "description": "Denies the set_closable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-closable", - "markdownDescription": "Denies the set_closable command without any pre-configured scope." - }, - { - "description": "Denies the set_content_protected command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-content-protected", - "markdownDescription": "Denies the set_content_protected command without any pre-configured scope." - }, - { - "description": "Denies the set_cursor_grab command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-cursor-grab", - "markdownDescription": "Denies the set_cursor_grab command without any pre-configured scope." - }, - { - "description": "Denies the set_cursor_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-cursor-icon", - "markdownDescription": "Denies the set_cursor_icon command without any pre-configured scope." - }, - { - "description": "Denies the set_cursor_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-cursor-position", - "markdownDescription": "Denies the set_cursor_position command without any pre-configured scope." - }, - { - "description": "Denies the set_cursor_visible command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-cursor-visible", - "markdownDescription": "Denies the set_cursor_visible command without any pre-configured scope." - }, - { - "description": "Denies the set_decorations command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-decorations", - "markdownDescription": "Denies the set_decorations command without any pre-configured scope." - }, - { - "description": "Denies the set_effects command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-effects", - "markdownDescription": "Denies the set_effects command without any pre-configured scope." - }, - { - "description": "Denies the set_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-enabled", - "markdownDescription": "Denies the set_enabled command without any pre-configured scope." - }, - { - "description": "Denies the set_focus command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-focus", - "markdownDescription": "Denies the set_focus command without any pre-configured scope." - }, - { - "description": "Denies the set_focusable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-focusable", - "markdownDescription": "Denies the set_focusable command without any pre-configured scope." - }, - { - "description": "Denies the set_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-fullscreen", - "markdownDescription": "Denies the set_fullscreen command without any pre-configured scope." - }, - { - "description": "Denies the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-icon", - "markdownDescription": "Denies the set_icon command without any pre-configured scope." - }, - { - "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-ignore-cursor-events", - "markdownDescription": "Denies the set_ignore_cursor_events command without any pre-configured scope." - }, - { - "description": "Denies the set_max_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-max-size", - "markdownDescription": "Denies the set_max_size command without any pre-configured scope." - }, - { - "description": "Denies the set_maximizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-maximizable", - "markdownDescription": "Denies the set_maximizable command without any pre-configured scope." - }, - { - "description": "Denies the set_min_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-min-size", - "markdownDescription": "Denies the set_min_size command without any pre-configured scope." - }, - { - "description": "Denies the set_minimizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-minimizable", - "markdownDescription": "Denies the set_minimizable command without any pre-configured scope." - }, - { - "description": "Denies the set_overlay_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-overlay-icon", - "markdownDescription": "Denies the set_overlay_icon command without any pre-configured scope." - }, - { - "description": "Denies the set_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-position", - "markdownDescription": "Denies the set_position command without any pre-configured scope." - }, - { - "description": "Denies the set_progress_bar command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-progress-bar", - "markdownDescription": "Denies the set_progress_bar command without any pre-configured scope." - }, - { - "description": "Denies the set_resizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-resizable", - "markdownDescription": "Denies the set_resizable command without any pre-configured scope." - }, - { - "description": "Denies the set_shadow command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-shadow", - "markdownDescription": "Denies the set_shadow command without any pre-configured scope." - }, - { - "description": "Denies the set_simple_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-simple-fullscreen", - "markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope." - }, - { - "description": "Denies the set_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-size", - "markdownDescription": "Denies the set_size command without any pre-configured scope." - }, - { - "description": "Denies the set_size_constraints command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-size-constraints", - "markdownDescription": "Denies the set_size_constraints command without any pre-configured scope." - }, - { - "description": "Denies the set_skip_taskbar command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-skip-taskbar", - "markdownDescription": "Denies the set_skip_taskbar command without any pre-configured scope." - }, - { - "description": "Denies the set_theme command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-theme", - "markdownDescription": "Denies the set_theme command without any pre-configured scope." - }, - { - "description": "Denies the set_title command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-title", - "markdownDescription": "Denies the set_title command without any pre-configured scope." - }, - { - "description": "Denies the set_title_bar_style command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-title-bar-style", - "markdownDescription": "Denies the set_title_bar_style command without any pre-configured scope." - }, - { - "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-visible-on-all-workspaces", - "markdownDescription": "Denies the set_visible_on_all_workspaces command without any pre-configured scope." - }, - { - "description": "Denies the show command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-show", - "markdownDescription": "Denies the show command without any pre-configured scope." - }, - { - "description": "Denies the start_dragging command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-start-dragging", - "markdownDescription": "Denies the start_dragging command without any pre-configured scope." - }, - { - "description": "Denies the start_resize_dragging command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-start-resize-dragging", - "markdownDescription": "Denies the start_resize_dragging command without any pre-configured scope." - }, - { - "description": "Denies the theme command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-theme", - "markdownDescription": "Denies the theme command without any pre-configured scope." - }, - { - "description": "Denies the title command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-title", - "markdownDescription": "Denies the title command without any pre-configured scope." - }, - { - "description": "Denies the toggle_maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-toggle-maximize", - "markdownDescription": "Denies the toggle_maximize command without any pre-configured scope." - }, - { - "description": "Denies the unmaximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-unmaximize", - "markdownDescription": "Denies the unmaximize command without any pre-configured scope." - }, - { - "description": "Denies the unminimize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-unminimize", - "markdownDescription": "Denies the unminimize command without any pre-configured scope." - }, - { - "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`", - "type": "string", - "const": "dialog:default", - "markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`" - }, - { - "description": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", - "type": "string", - "const": "dialog:allow-ask", - "markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" - }, - { - "description": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", - "type": "string", - "const": "dialog:allow-confirm", - "markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" - }, - { - "description": "Enables the message command without any pre-configured scope.", - "type": "string", - "const": "dialog:allow-message", - "markdownDescription": "Enables the message command without any pre-configured scope." - }, - { - "description": "Enables the open command without any pre-configured scope.", - "type": "string", - "const": "dialog:allow-open", - "markdownDescription": "Enables the open command without any pre-configured scope." - }, - { - "description": "Enables the save command without any pre-configured scope.", - "type": "string", - "const": "dialog:allow-save", - "markdownDescription": "Enables the save command without any pre-configured scope." - }, - { - "description": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", - "type": "string", - "const": "dialog:deny-ask", - "markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" - }, - { - "description": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", - "type": "string", - "const": "dialog:deny-confirm", - "markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" - }, - { - "description": "Denies the message command without any pre-configured scope.", - "type": "string", - "const": "dialog:deny-message", - "markdownDescription": "Denies the message command without any pre-configured scope." - }, - { - "description": "Denies the open command without any pre-configured scope.", - "type": "string", - "const": "dialog:deny-open", - "markdownDescription": "Denies the open command without any pre-configured scope." - }, - { - "description": "Denies the save command without any pre-configured scope.", - "type": "string", - "const": "dialog:deny-save", - "markdownDescription": "Denies the save command without any pre-configured scope." - }, - { - "description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`", - "type": "string", - "const": "shell:default", - "markdownDescription": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`" - }, - { - "description": "Enables the execute command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-execute", - "markdownDescription": "Enables the execute command without any pre-configured scope." - }, - { - "description": "Enables the kill command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-kill", - "markdownDescription": "Enables the kill command without any pre-configured scope." - }, - { - "description": "Enables the open command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-open", - "markdownDescription": "Enables the open command without any pre-configured scope." - }, - { - "description": "Enables the spawn command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-spawn", - "markdownDescription": "Enables the spawn command without any pre-configured scope." - }, - { - "description": "Enables the stdin_write command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-stdin-write", - "markdownDescription": "Enables the stdin_write command without any pre-configured scope." - }, - { - "description": "Denies the execute command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-execute", - "markdownDescription": "Denies the execute command without any pre-configured scope." - }, - { - "description": "Denies the kill command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-kill", - "markdownDescription": "Denies the kill command without any pre-configured scope." - }, - { - "description": "Denies the open command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-open", - "markdownDescription": "Denies the open command without any pre-configured scope." - }, - { - "description": "Denies the spawn command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-spawn", - "markdownDescription": "Denies the spawn command without any pre-configured scope." - }, - { - "description": "Denies the stdin_write command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-stdin-write", - "markdownDescription": "Denies the stdin_write command without any pre-configured scope." - } - ] - }, - "Value": { - "description": "All supported ACL values.", - "anyOf": [ - { - "description": "Represents a null JSON value.", - "type": "null" - }, - { - "description": "Represents a [`bool`].", - "type": "boolean" - }, - { - "description": "Represents a valid ACL [`Number`].", - "allOf": [ - { - "$ref": "#/definitions/Number" - } - ] - }, - { - "description": "Represents a [`String`].", - "type": "string" - }, - { - "description": "Represents a list of other [`Value`]s.", - "type": "array", - "items": { - "$ref": "#/definitions/Value" - } - }, - { - "description": "Represents a map of [`String`] keys to [`Value`]s.", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Value" - } - } - ] - }, - "Number": { - "description": "A valid ACL number.", - "anyOf": [ - { - "description": "Represents an [`i64`].", - "type": "integer", - "format": "int64" - }, - { - "description": "Represents a [`f64`].", - "type": "number", - "format": "double" - } - ] - }, - "Target": { - "description": "Platform target.", - "oneOf": [ - { - "description": "MacOS.", - "type": "string", - "enum": [ - "macOS" - ] - }, - { - "description": "Windows.", - "type": "string", - "enum": [ - "windows" - ] - }, - { - "description": "Linux.", - "type": "string", - "enum": [ - "linux" - ] - }, - { - "description": "Android.", - "type": "string", - "enum": [ - "android" - ] - }, - { - "description": "iOS.", - "type": "string", - "enum": [ - "iOS" - ] - } - ] - }, - "ShellScopeEntryAllowedArg": { - "description": "A command argument allowed to be executed by the webview API.", - "anyOf": [ - { - "description": "A non-configurable argument that is passed to the command in the order it was specified.", - "type": "string" - }, - { - "description": "A variable that is set while calling the command from the webview API.", - "type": "object", - "required": [ - "validator" - ], - "properties": { - "raw": { - "description": "Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.", - "default": false, - "type": "boolean" - }, - "validator": { - "description": "[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: ", - "type": "string" - } - }, - "additionalProperties": false - } - ] - }, - "ShellScopeEntryAllowedArgs": { - "description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.", - "anyOf": [ - { - "description": "Use a simple boolean to allow all or disable all arguments to this command configuration.", - "type": "boolean" - }, - { - "description": "A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.", - "type": "array", - "items": { - "$ref": "#/definitions/ShellScopeEntryAllowedArg" - } - } - ] - } - } -} \ No newline at end of file diff --git a/src-tauri/gen/schemas/linux-schema.json b/src-tauri/gen/schemas/linux-schema.json deleted file mode 100644 index 6eba1dc..0000000 --- a/src-tauri/gen/schemas/linux-schema.json +++ /dev/null @@ -1,2678 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "CapabilityFile", - "description": "Capability formats accepted in a capability file.", - "anyOf": [ - { - "description": "A single capability.", - "allOf": [ - { - "$ref": "#/definitions/Capability" - } - ] - }, - { - "description": "A list of capabilities.", - "type": "array", - "items": { - "$ref": "#/definitions/Capability" - } - }, - { - "description": "A list of capabilities.", - "type": "object", - "required": [ - "capabilities" - ], - "properties": { - "capabilities": { - "description": "The list of capabilities.", - "type": "array", - "items": { - "$ref": "#/definitions/Capability" - } - } - } - } - ], - "definitions": { - "Capability": { - "description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```", - "type": "object", - "required": [ - "identifier", - "permissions" - ], - "properties": { - "identifier": { - "description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`", - "type": "string" - }, - "description": { - "description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.", - "default": "", - "type": "string" - }, - "remote": { - "description": "Configure remote URLs that can use the capability permissions.\n\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\n\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\n\n## Example\n\n```json { \"urls\": [\"https://*.mydomain.dev\"] } ```", - "anyOf": [ - { - "$ref": "#/definitions/CapabilityRemote" - }, - { - "type": "null" - } - ] - }, - "local": { - "description": "Whether this capability is enabled for local app URLs or not. Defaults to `true`.", - "default": true, - "type": "boolean" - }, - "windows": { - "description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\n\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`", - "type": "array", - "items": { - "type": "string" - } - }, - "webviews": { - "description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`", - "type": "array", - "items": { - "type": "string" - } - }, - "permissions": { - "description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```", - "type": "array", - "items": { - "$ref": "#/definitions/PermissionEntry" - }, - "uniqueItems": true - }, - "platforms": { - "description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Target" - } - } - } - }, - "CapabilityRemote": { - "description": "Configuration for remote URLs that are associated with the capability.", - "type": "object", - "required": [ - "urls" - ], - "properties": { - "urls": { - "description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api", - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "PermissionEntry": { - "description": "An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.", - "anyOf": [ - { - "description": "Reference a permission or permission set by identifier.", - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ] - }, - { - "description": "Reference a permission or permission set by identifier and extends its scope.", - "type": "object", - "allOf": [ - { - "if": { - "properties": { - "identifier": { - "anyOf": [ - { - "description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`", - "type": "string", - "const": "shell:default", - "markdownDescription": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`" - }, - { - "description": "Enables the execute command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-execute", - "markdownDescription": "Enables the execute command without any pre-configured scope." - }, - { - "description": "Enables the kill command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-kill", - "markdownDescription": "Enables the kill command without any pre-configured scope." - }, - { - "description": "Enables the open command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-open", - "markdownDescription": "Enables the open command without any pre-configured scope." - }, - { - "description": "Enables the spawn command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-spawn", - "markdownDescription": "Enables the spawn command without any pre-configured scope." - }, - { - "description": "Enables the stdin_write command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-stdin-write", - "markdownDescription": "Enables the stdin_write command without any pre-configured scope." - }, - { - "description": "Denies the execute command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-execute", - "markdownDescription": "Denies the execute command without any pre-configured scope." - }, - { - "description": "Denies the kill command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-kill", - "markdownDescription": "Denies the kill command without any pre-configured scope." - }, - { - "description": "Denies the open command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-open", - "markdownDescription": "Denies the open command without any pre-configured scope." - }, - { - "description": "Denies the spawn command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-spawn", - "markdownDescription": "Denies the spawn command without any pre-configured scope." - }, - { - "description": "Denies the stdin_write command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-stdin-write", - "markdownDescription": "Denies the stdin_write command without any pre-configured scope." - } - ] - } - } - }, - "then": { - "properties": { - "allow": { - "items": { - "title": "ShellScopeEntry", - "description": "Shell scope entry.", - "anyOf": [ - { - "type": "object", - "required": [ - "cmd", - "name" - ], - "properties": { - "args": { - "description": "The allowed arguments for the command execution.", - "allOf": [ - { - "$ref": "#/definitions/ShellScopeEntryAllowedArgs" - } - ] - }, - "cmd": { - "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", - "type": "string" - }, - "name": { - "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", - "type": "string" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "name", - "sidecar" - ], - "properties": { - "args": { - "description": "The allowed arguments for the command execution.", - "allOf": [ - { - "$ref": "#/definitions/ShellScopeEntryAllowedArgs" - } - ] - }, - "name": { - "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", - "type": "string" - }, - "sidecar": { - "description": "If this command is a sidecar command.", - "type": "boolean" - } - }, - "additionalProperties": false - } - ] - } - }, - "deny": { - "items": { - "title": "ShellScopeEntry", - "description": "Shell scope entry.", - "anyOf": [ - { - "type": "object", - "required": [ - "cmd", - "name" - ], - "properties": { - "args": { - "description": "The allowed arguments for the command execution.", - "allOf": [ - { - "$ref": "#/definitions/ShellScopeEntryAllowedArgs" - } - ] - }, - "cmd": { - "description": "The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.", - "type": "string" - }, - "name": { - "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", - "type": "string" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "name", - "sidecar" - ], - "properties": { - "args": { - "description": "The allowed arguments for the command execution.", - "allOf": [ - { - "$ref": "#/definitions/ShellScopeEntryAllowedArgs" - } - ] - }, - "name": { - "description": "The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.", - "type": "string" - }, - "sidecar": { - "description": "If this command is a sidecar command.", - "type": "boolean" - } - }, - "additionalProperties": false - } - ] - } - } - } - }, - "properties": { - "identifier": { - "description": "Identifier of the permission or permission set.", - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ] - } - } - }, - { - "properties": { - "identifier": { - "description": "Identifier of the permission or permission set.", - "allOf": [ - { - "$ref": "#/definitions/Identifier" - } - ] - }, - "allow": { - "description": "Data that defines what is allowed by the scope.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Value" - } - }, - "deny": { - "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/Value" - } - } - } - } - ], - "required": [ - "identifier" - ] - } - ] - }, - "Identifier": { - "description": "Permission identifier", - "oneOf": [ - { - "description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`", - "type": "string", - "const": "core:default", - "markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`" - }, - { - "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`", - "type": "string", - "const": "core:app:default", - "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`" - }, - { - "description": "Enables the app_hide command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-app-hide", - "markdownDescription": "Enables the app_hide command without any pre-configured scope." - }, - { - "description": "Enables the app_show command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-app-show", - "markdownDescription": "Enables the app_show command without any pre-configured scope." - }, - { - "description": "Enables the bundle_type command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-bundle-type", - "markdownDescription": "Enables the bundle_type command without any pre-configured scope." - }, - { - "description": "Enables the default_window_icon command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-default-window-icon", - "markdownDescription": "Enables the default_window_icon command without any pre-configured scope." - }, - { - "description": "Enables the fetch_data_store_identifiers command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-fetch-data-store-identifiers", - "markdownDescription": "Enables the fetch_data_store_identifiers command without any pre-configured scope." - }, - { - "description": "Enables the identifier command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-identifier", - "markdownDescription": "Enables the identifier command without any pre-configured scope." - }, - { - "description": "Enables the name command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-name", - "markdownDescription": "Enables the name command without any pre-configured scope." - }, - { - "description": "Enables the register_listener command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-register-listener", - "markdownDescription": "Enables the register_listener command without any pre-configured scope." - }, - { - "description": "Enables the remove_data_store command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-remove-data-store", - "markdownDescription": "Enables the remove_data_store command without any pre-configured scope." - }, - { - "description": "Enables the remove_listener command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-remove-listener", - "markdownDescription": "Enables the remove_listener command without any pre-configured scope." - }, - { - "description": "Enables the set_app_theme command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-set-app-theme", - "markdownDescription": "Enables the set_app_theme command without any pre-configured scope." - }, - { - "description": "Enables the set_dock_visibility command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-set-dock-visibility", - "markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope." - }, - { - "description": "Enables the supports_multiple_windows command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-supports-multiple-windows", - "markdownDescription": "Enables the supports_multiple_windows command without any pre-configured scope." - }, - { - "description": "Enables the tauri_version command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-tauri-version", - "markdownDescription": "Enables the tauri_version command without any pre-configured scope." - }, - { - "description": "Enables the version command without any pre-configured scope.", - "type": "string", - "const": "core:app:allow-version", - "markdownDescription": "Enables the version command without any pre-configured scope." - }, - { - "description": "Denies the app_hide command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-app-hide", - "markdownDescription": "Denies the app_hide command without any pre-configured scope." - }, - { - "description": "Denies the app_show command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-app-show", - "markdownDescription": "Denies the app_show command without any pre-configured scope." - }, - { - "description": "Denies the bundle_type command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-bundle-type", - "markdownDescription": "Denies the bundle_type command without any pre-configured scope." - }, - { - "description": "Denies the default_window_icon command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-default-window-icon", - "markdownDescription": "Denies the default_window_icon command without any pre-configured scope." - }, - { - "description": "Denies the fetch_data_store_identifiers command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-fetch-data-store-identifiers", - "markdownDescription": "Denies the fetch_data_store_identifiers command without any pre-configured scope." - }, - { - "description": "Denies the identifier command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-identifier", - "markdownDescription": "Denies the identifier command without any pre-configured scope." - }, - { - "description": "Denies the name command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-name", - "markdownDescription": "Denies the name command without any pre-configured scope." - }, - { - "description": "Denies the register_listener command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-register-listener", - "markdownDescription": "Denies the register_listener command without any pre-configured scope." - }, - { - "description": "Denies the remove_data_store command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-remove-data-store", - "markdownDescription": "Denies the remove_data_store command without any pre-configured scope." - }, - { - "description": "Denies the remove_listener command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-remove-listener", - "markdownDescription": "Denies the remove_listener command without any pre-configured scope." - }, - { - "description": "Denies the set_app_theme command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-set-app-theme", - "markdownDescription": "Denies the set_app_theme command without any pre-configured scope." - }, - { - "description": "Denies the set_dock_visibility command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-set-dock-visibility", - "markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope." - }, - { - "description": "Denies the supports_multiple_windows command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-supports-multiple-windows", - "markdownDescription": "Denies the supports_multiple_windows command without any pre-configured scope." - }, - { - "description": "Denies the tauri_version command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-tauri-version", - "markdownDescription": "Denies the tauri_version command without any pre-configured scope." - }, - { - "description": "Denies the version command without any pre-configured scope.", - "type": "string", - "const": "core:app:deny-version", - "markdownDescription": "Denies the version command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`", - "type": "string", - "const": "core:event:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`" - }, - { - "description": "Enables the emit command without any pre-configured scope.", - "type": "string", - "const": "core:event:allow-emit", - "markdownDescription": "Enables the emit command without any pre-configured scope." - }, - { - "description": "Enables the emit_to command without any pre-configured scope.", - "type": "string", - "const": "core:event:allow-emit-to", - "markdownDescription": "Enables the emit_to command without any pre-configured scope." - }, - { - "description": "Enables the listen command without any pre-configured scope.", - "type": "string", - "const": "core:event:allow-listen", - "markdownDescription": "Enables the listen command without any pre-configured scope." - }, - { - "description": "Enables the unlisten command without any pre-configured scope.", - "type": "string", - "const": "core:event:allow-unlisten", - "markdownDescription": "Enables the unlisten command without any pre-configured scope." - }, - { - "description": "Denies the emit command without any pre-configured scope.", - "type": "string", - "const": "core:event:deny-emit", - "markdownDescription": "Denies the emit command without any pre-configured scope." - }, - { - "description": "Denies the emit_to command without any pre-configured scope.", - "type": "string", - "const": "core:event:deny-emit-to", - "markdownDescription": "Denies the emit_to command without any pre-configured scope." - }, - { - "description": "Denies the listen command without any pre-configured scope.", - "type": "string", - "const": "core:event:deny-listen", - "markdownDescription": "Denies the listen command without any pre-configured scope." - }, - { - "description": "Denies the unlisten command without any pre-configured scope.", - "type": "string", - "const": "core:event:deny-unlisten", - "markdownDescription": "Denies the unlisten command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`", - "type": "string", - "const": "core:image:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`" - }, - { - "description": "Enables the from_bytes command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-from-bytes", - "markdownDescription": "Enables the from_bytes command without any pre-configured scope." - }, - { - "description": "Enables the from_path command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-from-path", - "markdownDescription": "Enables the from_path command without any pre-configured scope." - }, - { - "description": "Enables the new command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-new", - "markdownDescription": "Enables the new command without any pre-configured scope." - }, - { - "description": "Enables the rgba command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-rgba", - "markdownDescription": "Enables the rgba command without any pre-configured scope." - }, - { - "description": "Enables the size command without any pre-configured scope.", - "type": "string", - "const": "core:image:allow-size", - "markdownDescription": "Enables the size command without any pre-configured scope." - }, - { - "description": "Denies the from_bytes command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-from-bytes", - "markdownDescription": "Denies the from_bytes command without any pre-configured scope." - }, - { - "description": "Denies the from_path command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-from-path", - "markdownDescription": "Denies the from_path command without any pre-configured scope." - }, - { - "description": "Denies the new command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-new", - "markdownDescription": "Denies the new command without any pre-configured scope." - }, - { - "description": "Denies the rgba command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-rgba", - "markdownDescription": "Denies the rgba command without any pre-configured scope." - }, - { - "description": "Denies the size command without any pre-configured scope.", - "type": "string", - "const": "core:image:deny-size", - "markdownDescription": "Denies the size command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`", - "type": "string", - "const": "core:menu:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`" - }, - { - "description": "Enables the append command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-append", - "markdownDescription": "Enables the append command without any pre-configured scope." - }, - { - "description": "Enables the create_default command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-create-default", - "markdownDescription": "Enables the create_default command without any pre-configured scope." - }, - { - "description": "Enables the get command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-get", - "markdownDescription": "Enables the get command without any pre-configured scope." - }, - { - "description": "Enables the insert command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-insert", - "markdownDescription": "Enables the insert command without any pre-configured scope." - }, - { - "description": "Enables the is_checked command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-is-checked", - "markdownDescription": "Enables the is_checked command without any pre-configured scope." - }, - { - "description": "Enables the is_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-is-enabled", - "markdownDescription": "Enables the is_enabled command without any pre-configured scope." - }, - { - "description": "Enables the items command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-items", - "markdownDescription": "Enables the items command without any pre-configured scope." - }, - { - "description": "Enables the new command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-new", - "markdownDescription": "Enables the new command without any pre-configured scope." - }, - { - "description": "Enables the popup command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-popup", - "markdownDescription": "Enables the popup command without any pre-configured scope." - }, - { - "description": "Enables the prepend command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-prepend", - "markdownDescription": "Enables the prepend command without any pre-configured scope." - }, - { - "description": "Enables the remove command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-remove", - "markdownDescription": "Enables the remove command without any pre-configured scope." - }, - { - "description": "Enables the remove_at command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-remove-at", - "markdownDescription": "Enables the remove_at command without any pre-configured scope." - }, - { - "description": "Enables the set_accelerator command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-accelerator", - "markdownDescription": "Enables the set_accelerator command without any pre-configured scope." - }, - { - "description": "Enables the set_as_app_menu command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-as-app-menu", - "markdownDescription": "Enables the set_as_app_menu command without any pre-configured scope." - }, - { - "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-as-help-menu-for-nsapp", - "markdownDescription": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope." - }, - { - "description": "Enables the set_as_window_menu command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-as-window-menu", - "markdownDescription": "Enables the set_as_window_menu command without any pre-configured scope." - }, - { - "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-as-windows-menu-for-nsapp", - "markdownDescription": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope." - }, - { - "description": "Enables the set_checked command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-checked", - "markdownDescription": "Enables the set_checked command without any pre-configured scope." - }, - { - "description": "Enables the set_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-enabled", - "markdownDescription": "Enables the set_enabled command without any pre-configured scope." - }, - { - "description": "Enables the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-icon", - "markdownDescription": "Enables the set_icon command without any pre-configured scope." - }, - { - "description": "Enables the set_text command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-set-text", - "markdownDescription": "Enables the set_text command without any pre-configured scope." - }, - { - "description": "Enables the text command without any pre-configured scope.", - "type": "string", - "const": "core:menu:allow-text", - "markdownDescription": "Enables the text command without any pre-configured scope." - }, - { - "description": "Denies the append command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-append", - "markdownDescription": "Denies the append command without any pre-configured scope." - }, - { - "description": "Denies the create_default command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-create-default", - "markdownDescription": "Denies the create_default command without any pre-configured scope." - }, - { - "description": "Denies the get command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-get", - "markdownDescription": "Denies the get command without any pre-configured scope." - }, - { - "description": "Denies the insert command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-insert", - "markdownDescription": "Denies the insert command without any pre-configured scope." - }, - { - "description": "Denies the is_checked command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-is-checked", - "markdownDescription": "Denies the is_checked command without any pre-configured scope." - }, - { - "description": "Denies the is_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-is-enabled", - "markdownDescription": "Denies the is_enabled command without any pre-configured scope." - }, - { - "description": "Denies the items command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-items", - "markdownDescription": "Denies the items command without any pre-configured scope." - }, - { - "description": "Denies the new command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-new", - "markdownDescription": "Denies the new command without any pre-configured scope." - }, - { - "description": "Denies the popup command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-popup", - "markdownDescription": "Denies the popup command without any pre-configured scope." - }, - { - "description": "Denies the prepend command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-prepend", - "markdownDescription": "Denies the prepend command without any pre-configured scope." - }, - { - "description": "Denies the remove command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-remove", - "markdownDescription": "Denies the remove command without any pre-configured scope." - }, - { - "description": "Denies the remove_at command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-remove-at", - "markdownDescription": "Denies the remove_at command without any pre-configured scope." - }, - { - "description": "Denies the set_accelerator command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-accelerator", - "markdownDescription": "Denies the set_accelerator command without any pre-configured scope." - }, - { - "description": "Denies the set_as_app_menu command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-as-app-menu", - "markdownDescription": "Denies the set_as_app_menu command without any pre-configured scope." - }, - { - "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-as-help-menu-for-nsapp", - "markdownDescription": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope." - }, - { - "description": "Denies the set_as_window_menu command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-as-window-menu", - "markdownDescription": "Denies the set_as_window_menu command without any pre-configured scope." - }, - { - "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-as-windows-menu-for-nsapp", - "markdownDescription": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope." - }, - { - "description": "Denies the set_checked command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-checked", - "markdownDescription": "Denies the set_checked command without any pre-configured scope." - }, - { - "description": "Denies the set_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-enabled", - "markdownDescription": "Denies the set_enabled command without any pre-configured scope." - }, - { - "description": "Denies the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-icon", - "markdownDescription": "Denies the set_icon command without any pre-configured scope." - }, - { - "description": "Denies the set_text command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-set-text", - "markdownDescription": "Denies the set_text command without any pre-configured scope." - }, - { - "description": "Denies the text command without any pre-configured scope.", - "type": "string", - "const": "core:menu:deny-text", - "markdownDescription": "Denies the text command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`", - "type": "string", - "const": "core:path:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`" - }, - { - "description": "Enables the basename command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-basename", - "markdownDescription": "Enables the basename command without any pre-configured scope." - }, - { - "description": "Enables the dirname command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-dirname", - "markdownDescription": "Enables the dirname command without any pre-configured scope." - }, - { - "description": "Enables the extname command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-extname", - "markdownDescription": "Enables the extname command without any pre-configured scope." - }, - { - "description": "Enables the is_absolute command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-is-absolute", - "markdownDescription": "Enables the is_absolute command without any pre-configured scope." - }, - { - "description": "Enables the join command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-join", - "markdownDescription": "Enables the join command without any pre-configured scope." - }, - { - "description": "Enables the normalize command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-normalize", - "markdownDescription": "Enables the normalize command without any pre-configured scope." - }, - { - "description": "Enables the resolve command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-resolve", - "markdownDescription": "Enables the resolve command without any pre-configured scope." - }, - { - "description": "Enables the resolve_directory command without any pre-configured scope.", - "type": "string", - "const": "core:path:allow-resolve-directory", - "markdownDescription": "Enables the resolve_directory command without any pre-configured scope." - }, - { - "description": "Denies the basename command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-basename", - "markdownDescription": "Denies the basename command without any pre-configured scope." - }, - { - "description": "Denies the dirname command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-dirname", - "markdownDescription": "Denies the dirname command without any pre-configured scope." - }, - { - "description": "Denies the extname command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-extname", - "markdownDescription": "Denies the extname command without any pre-configured scope." - }, - { - "description": "Denies the is_absolute command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-is-absolute", - "markdownDescription": "Denies the is_absolute command without any pre-configured scope." - }, - { - "description": "Denies the join command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-join", - "markdownDescription": "Denies the join command without any pre-configured scope." - }, - { - "description": "Denies the normalize command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-normalize", - "markdownDescription": "Denies the normalize command without any pre-configured scope." - }, - { - "description": "Denies the resolve command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-resolve", - "markdownDescription": "Denies the resolve command without any pre-configured scope." - }, - { - "description": "Denies the resolve_directory command without any pre-configured scope.", - "type": "string", - "const": "core:path:deny-resolve-directory", - "markdownDescription": "Denies the resolve_directory command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`", - "type": "string", - "const": "core:resources:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`" - }, - { - "description": "Enables the close command without any pre-configured scope.", - "type": "string", - "const": "core:resources:allow-close", - "markdownDescription": "Enables the close command without any pre-configured scope." - }, - { - "description": "Denies the close command without any pre-configured scope.", - "type": "string", - "const": "core:resources:deny-close", - "markdownDescription": "Denies the close command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`", - "type": "string", - "const": "core:tray:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`" - }, - { - "description": "Enables the get_by_id command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-get-by-id", - "markdownDescription": "Enables the get_by_id command without any pre-configured scope." - }, - { - "description": "Enables the new command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-new", - "markdownDescription": "Enables the new command without any pre-configured scope." - }, - { - "description": "Enables the remove_by_id command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-remove-by-id", - "markdownDescription": "Enables the remove_by_id command without any pre-configured scope." - }, - { - "description": "Enables the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-icon", - "markdownDescription": "Enables the set_icon command without any pre-configured scope." - }, - { - "description": "Enables the set_icon_as_template command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-icon-as-template", - "markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope." - }, - { - "description": "Enables the set_icon_with_as_template command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-icon-with-as-template", - "markdownDescription": "Enables the set_icon_with_as_template command without any pre-configured scope." - }, - { - "description": "Enables the set_menu command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-menu", - "markdownDescription": "Enables the set_menu command without any pre-configured scope." - }, - { - "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-show-menu-on-left-click", - "markdownDescription": "Enables the set_show_menu_on_left_click command without any pre-configured scope." - }, - { - "description": "Enables the set_temp_dir_path command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-temp-dir-path", - "markdownDescription": "Enables the set_temp_dir_path command without any pre-configured scope." - }, - { - "description": "Enables the set_title command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-title", - "markdownDescription": "Enables the set_title command without any pre-configured scope." - }, - { - "description": "Enables the set_tooltip command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-tooltip", - "markdownDescription": "Enables the set_tooltip command without any pre-configured scope." - }, - { - "description": "Enables the set_visible command without any pre-configured scope.", - "type": "string", - "const": "core:tray:allow-set-visible", - "markdownDescription": "Enables the set_visible command without any pre-configured scope." - }, - { - "description": "Denies the get_by_id command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-get-by-id", - "markdownDescription": "Denies the get_by_id command without any pre-configured scope." - }, - { - "description": "Denies the new command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-new", - "markdownDescription": "Denies the new command without any pre-configured scope." - }, - { - "description": "Denies the remove_by_id command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-remove-by-id", - "markdownDescription": "Denies the remove_by_id command without any pre-configured scope." - }, - { - "description": "Denies the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-icon", - "markdownDescription": "Denies the set_icon command without any pre-configured scope." - }, - { - "description": "Denies the set_icon_as_template command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-icon-as-template", - "markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope." - }, - { - "description": "Denies the set_icon_with_as_template command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-icon-with-as-template", - "markdownDescription": "Denies the set_icon_with_as_template command without any pre-configured scope." - }, - { - "description": "Denies the set_menu command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-menu", - "markdownDescription": "Denies the set_menu command without any pre-configured scope." - }, - { - "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-show-menu-on-left-click", - "markdownDescription": "Denies the set_show_menu_on_left_click command without any pre-configured scope." - }, - { - "description": "Denies the set_temp_dir_path command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-temp-dir-path", - "markdownDescription": "Denies the set_temp_dir_path command without any pre-configured scope." - }, - { - "description": "Denies the set_title command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-title", - "markdownDescription": "Denies the set_title command without any pre-configured scope." - }, - { - "description": "Denies the set_tooltip command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-tooltip", - "markdownDescription": "Denies the set_tooltip command without any pre-configured scope." - }, - { - "description": "Denies the set_visible command without any pre-configured scope.", - "type": "string", - "const": "core:tray:deny-set-visible", - "markdownDescription": "Denies the set_visible command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`", - "type": "string", - "const": "core:webview:default", - "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`" - }, - { - "description": "Enables the clear_all_browsing_data command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-clear-all-browsing-data", - "markdownDescription": "Enables the clear_all_browsing_data command without any pre-configured scope." - }, - { - "description": "Enables the create_webview command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-create-webview", - "markdownDescription": "Enables the create_webview command without any pre-configured scope." - }, - { - "description": "Enables the create_webview_window command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-create-webview-window", - "markdownDescription": "Enables the create_webview_window command without any pre-configured scope." - }, - { - "description": "Enables the get_all_webviews command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-get-all-webviews", - "markdownDescription": "Enables the get_all_webviews command without any pre-configured scope." - }, - { - "description": "Enables the internal_toggle_devtools command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-internal-toggle-devtools", - "markdownDescription": "Enables the internal_toggle_devtools command without any pre-configured scope." - }, - { - "description": "Enables the print command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-print", - "markdownDescription": "Enables the print command without any pre-configured scope." - }, - { - "description": "Enables the reparent command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-reparent", - "markdownDescription": "Enables the reparent command without any pre-configured scope." - }, - { - "description": "Enables the set_webview_auto_resize command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-auto-resize", - "markdownDescription": "Enables the set_webview_auto_resize command without any pre-configured scope." - }, - { - "description": "Enables the set_webview_background_color command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-background-color", - "markdownDescription": "Enables the set_webview_background_color command without any pre-configured scope." - }, - { - "description": "Enables the set_webview_focus command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-focus", - "markdownDescription": "Enables the set_webview_focus command without any pre-configured scope." - }, - { - "description": "Enables the set_webview_position command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-position", - "markdownDescription": "Enables the set_webview_position command without any pre-configured scope." - }, - { - "description": "Enables the set_webview_size command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-size", - "markdownDescription": "Enables the set_webview_size command without any pre-configured scope." - }, - { - "description": "Enables the set_webview_zoom command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-set-webview-zoom", - "markdownDescription": "Enables the set_webview_zoom command without any pre-configured scope." - }, - { - "description": "Enables the webview_close command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-close", - "markdownDescription": "Enables the webview_close command without any pre-configured scope." - }, - { - "description": "Enables the webview_hide command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-hide", - "markdownDescription": "Enables the webview_hide command without any pre-configured scope." - }, - { - "description": "Enables the webview_position command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-position", - "markdownDescription": "Enables the webview_position command without any pre-configured scope." - }, - { - "description": "Enables the webview_show command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-show", - "markdownDescription": "Enables the webview_show command without any pre-configured scope." - }, - { - "description": "Enables the webview_size command without any pre-configured scope.", - "type": "string", - "const": "core:webview:allow-webview-size", - "markdownDescription": "Enables the webview_size command without any pre-configured scope." - }, - { - "description": "Denies the clear_all_browsing_data command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-clear-all-browsing-data", - "markdownDescription": "Denies the clear_all_browsing_data command without any pre-configured scope." - }, - { - "description": "Denies the create_webview command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-create-webview", - "markdownDescription": "Denies the create_webview command without any pre-configured scope." - }, - { - "description": "Denies the create_webview_window command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-create-webview-window", - "markdownDescription": "Denies the create_webview_window command without any pre-configured scope." - }, - { - "description": "Denies the get_all_webviews command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-get-all-webviews", - "markdownDescription": "Denies the get_all_webviews command without any pre-configured scope." - }, - { - "description": "Denies the internal_toggle_devtools command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-internal-toggle-devtools", - "markdownDescription": "Denies the internal_toggle_devtools command without any pre-configured scope." - }, - { - "description": "Denies the print command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-print", - "markdownDescription": "Denies the print command without any pre-configured scope." - }, - { - "description": "Denies the reparent command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-reparent", - "markdownDescription": "Denies the reparent command without any pre-configured scope." - }, - { - "description": "Denies the set_webview_auto_resize command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-auto-resize", - "markdownDescription": "Denies the set_webview_auto_resize command without any pre-configured scope." - }, - { - "description": "Denies the set_webview_background_color command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-background-color", - "markdownDescription": "Denies the set_webview_background_color command without any pre-configured scope." - }, - { - "description": "Denies the set_webview_focus command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-focus", - "markdownDescription": "Denies the set_webview_focus command without any pre-configured scope." - }, - { - "description": "Denies the set_webview_position command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-position", - "markdownDescription": "Denies the set_webview_position command without any pre-configured scope." - }, - { - "description": "Denies the set_webview_size command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-size", - "markdownDescription": "Denies the set_webview_size command without any pre-configured scope." - }, - { - "description": "Denies the set_webview_zoom command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-set-webview-zoom", - "markdownDescription": "Denies the set_webview_zoom command without any pre-configured scope." - }, - { - "description": "Denies the webview_close command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-close", - "markdownDescription": "Denies the webview_close command without any pre-configured scope." - }, - { - "description": "Denies the webview_hide command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-hide", - "markdownDescription": "Denies the webview_hide command without any pre-configured scope." - }, - { - "description": "Denies the webview_position command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-position", - "markdownDescription": "Denies the webview_position command without any pre-configured scope." - }, - { - "description": "Denies the webview_show command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-show", - "markdownDescription": "Denies the webview_show command without any pre-configured scope." - }, - { - "description": "Denies the webview_size command without any pre-configured scope.", - "type": "string", - "const": "core:webview:deny-webview-size", - "markdownDescription": "Denies the webview_size command without any pre-configured scope." - }, - { - "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`", - "type": "string", - "const": "core:window:default", - "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`" - }, - { - "description": "Enables the activity_name command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-activity-name", - "markdownDescription": "Enables the activity_name command without any pre-configured scope." - }, - { - "description": "Enables the available_monitors command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-available-monitors", - "markdownDescription": "Enables the available_monitors command without any pre-configured scope." - }, - { - "description": "Enables the center command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-center", - "markdownDescription": "Enables the center command without any pre-configured scope." - }, - { - "description": "Enables the close command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-close", - "markdownDescription": "Enables the close command without any pre-configured scope." - }, - { - "description": "Enables the create command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-create", - "markdownDescription": "Enables the create command without any pre-configured scope." - }, - { - "description": "Enables the current_monitor command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-current-monitor", - "markdownDescription": "Enables the current_monitor command without any pre-configured scope." - }, - { - "description": "Enables the cursor_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-cursor-position", - "markdownDescription": "Enables the cursor_position command without any pre-configured scope." - }, - { - "description": "Enables the destroy command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-destroy", - "markdownDescription": "Enables the destroy command without any pre-configured scope." - }, - { - "description": "Enables the get_all_windows command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-get-all-windows", - "markdownDescription": "Enables the get_all_windows command without any pre-configured scope." - }, - { - "description": "Enables the hide command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-hide", - "markdownDescription": "Enables the hide command without any pre-configured scope." - }, - { - "description": "Enables the inner_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-inner-position", - "markdownDescription": "Enables the inner_position command without any pre-configured scope." - }, - { - "description": "Enables the inner_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-inner-size", - "markdownDescription": "Enables the inner_size command without any pre-configured scope." - }, - { - "description": "Enables the internal_toggle_maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-internal-toggle-maximize", - "markdownDescription": "Enables the internal_toggle_maximize command without any pre-configured scope." - }, - { - "description": "Enables the is_always_on_top command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-always-on-top", - "markdownDescription": "Enables the is_always_on_top command without any pre-configured scope." - }, - { - "description": "Enables the is_closable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-closable", - "markdownDescription": "Enables the is_closable command without any pre-configured scope." - }, - { - "description": "Enables the is_decorated command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-decorated", - "markdownDescription": "Enables the is_decorated command without any pre-configured scope." - }, - { - "description": "Enables the is_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-enabled", - "markdownDescription": "Enables the is_enabled command without any pre-configured scope." - }, - { - "description": "Enables the is_focused command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-focused", - "markdownDescription": "Enables the is_focused command without any pre-configured scope." - }, - { - "description": "Enables the is_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-fullscreen", - "markdownDescription": "Enables the is_fullscreen command without any pre-configured scope." - }, - { - "description": "Enables the is_maximizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-maximizable", - "markdownDescription": "Enables the is_maximizable command without any pre-configured scope." - }, - { - "description": "Enables the is_maximized command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-maximized", - "markdownDescription": "Enables the is_maximized command without any pre-configured scope." - }, - { - "description": "Enables the is_minimizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-minimizable", - "markdownDescription": "Enables the is_minimizable command without any pre-configured scope." - }, - { - "description": "Enables the is_minimized command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-minimized", - "markdownDescription": "Enables the is_minimized command without any pre-configured scope." - }, - { - "description": "Enables the is_resizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-resizable", - "markdownDescription": "Enables the is_resizable command without any pre-configured scope." - }, - { - "description": "Enables the is_visible command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-is-visible", - "markdownDescription": "Enables the is_visible command without any pre-configured scope." - }, - { - "description": "Enables the maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-maximize", - "markdownDescription": "Enables the maximize command without any pre-configured scope." - }, - { - "description": "Enables the minimize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-minimize", - "markdownDescription": "Enables the minimize command without any pre-configured scope." - }, - { - "description": "Enables the monitor_from_point command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-monitor-from-point", - "markdownDescription": "Enables the monitor_from_point command without any pre-configured scope." - }, - { - "description": "Enables the outer_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-outer-position", - "markdownDescription": "Enables the outer_position command without any pre-configured scope." - }, - { - "description": "Enables the outer_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-outer-size", - "markdownDescription": "Enables the outer_size command without any pre-configured scope." - }, - { - "description": "Enables the primary_monitor command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-primary-monitor", - "markdownDescription": "Enables the primary_monitor command without any pre-configured scope." - }, - { - "description": "Enables the request_user_attention command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-request-user-attention", - "markdownDescription": "Enables the request_user_attention command without any pre-configured scope." - }, - { - "description": "Enables the scale_factor command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-scale-factor", - "markdownDescription": "Enables the scale_factor command without any pre-configured scope." - }, - { - "description": "Enables the scene_identifier command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-scene-identifier", - "markdownDescription": "Enables the scene_identifier command without any pre-configured scope." - }, - { - "description": "Enables the set_always_on_bottom command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-always-on-bottom", - "markdownDescription": "Enables the set_always_on_bottom command without any pre-configured scope." - }, - { - "description": "Enables the set_always_on_top command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-always-on-top", - "markdownDescription": "Enables the set_always_on_top command without any pre-configured scope." - }, - { - "description": "Enables the set_background_color command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-background-color", - "markdownDescription": "Enables the set_background_color command without any pre-configured scope." - }, - { - "description": "Enables the set_badge_count command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-badge-count", - "markdownDescription": "Enables the set_badge_count command without any pre-configured scope." - }, - { - "description": "Enables the set_badge_label command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-badge-label", - "markdownDescription": "Enables the set_badge_label command without any pre-configured scope." - }, - { - "description": "Enables the set_closable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-closable", - "markdownDescription": "Enables the set_closable command without any pre-configured scope." - }, - { - "description": "Enables the set_content_protected command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-content-protected", - "markdownDescription": "Enables the set_content_protected command without any pre-configured scope." - }, - { - "description": "Enables the set_cursor_grab command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-cursor-grab", - "markdownDescription": "Enables the set_cursor_grab command without any pre-configured scope." - }, - { - "description": "Enables the set_cursor_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-cursor-icon", - "markdownDescription": "Enables the set_cursor_icon command without any pre-configured scope." - }, - { - "description": "Enables the set_cursor_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-cursor-position", - "markdownDescription": "Enables the set_cursor_position command without any pre-configured scope." - }, - { - "description": "Enables the set_cursor_visible command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-cursor-visible", - "markdownDescription": "Enables the set_cursor_visible command without any pre-configured scope." - }, - { - "description": "Enables the set_decorations command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-decorations", - "markdownDescription": "Enables the set_decorations command without any pre-configured scope." - }, - { - "description": "Enables the set_effects command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-effects", - "markdownDescription": "Enables the set_effects command without any pre-configured scope." - }, - { - "description": "Enables the set_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-enabled", - "markdownDescription": "Enables the set_enabled command without any pre-configured scope." - }, - { - "description": "Enables the set_focus command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-focus", - "markdownDescription": "Enables the set_focus command without any pre-configured scope." - }, - { - "description": "Enables the set_focusable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-focusable", - "markdownDescription": "Enables the set_focusable command without any pre-configured scope." - }, - { - "description": "Enables the set_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-fullscreen", - "markdownDescription": "Enables the set_fullscreen command without any pre-configured scope." - }, - { - "description": "Enables the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-icon", - "markdownDescription": "Enables the set_icon command without any pre-configured scope." - }, - { - "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-ignore-cursor-events", - "markdownDescription": "Enables the set_ignore_cursor_events command without any pre-configured scope." - }, - { - "description": "Enables the set_max_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-max-size", - "markdownDescription": "Enables the set_max_size command without any pre-configured scope." - }, - { - "description": "Enables the set_maximizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-maximizable", - "markdownDescription": "Enables the set_maximizable command without any pre-configured scope." - }, - { - "description": "Enables the set_min_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-min-size", - "markdownDescription": "Enables the set_min_size command without any pre-configured scope." - }, - { - "description": "Enables the set_minimizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-minimizable", - "markdownDescription": "Enables the set_minimizable command without any pre-configured scope." - }, - { - "description": "Enables the set_overlay_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-overlay-icon", - "markdownDescription": "Enables the set_overlay_icon command without any pre-configured scope." - }, - { - "description": "Enables the set_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-position", - "markdownDescription": "Enables the set_position command without any pre-configured scope." - }, - { - "description": "Enables the set_progress_bar command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-progress-bar", - "markdownDescription": "Enables the set_progress_bar command without any pre-configured scope." - }, - { - "description": "Enables the set_resizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-resizable", - "markdownDescription": "Enables the set_resizable command without any pre-configured scope." - }, - { - "description": "Enables the set_shadow command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-shadow", - "markdownDescription": "Enables the set_shadow command without any pre-configured scope." - }, - { - "description": "Enables the set_simple_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-simple-fullscreen", - "markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope." - }, - { - "description": "Enables the set_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-size", - "markdownDescription": "Enables the set_size command without any pre-configured scope." - }, - { - "description": "Enables the set_size_constraints command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-size-constraints", - "markdownDescription": "Enables the set_size_constraints command without any pre-configured scope." - }, - { - "description": "Enables the set_skip_taskbar command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-skip-taskbar", - "markdownDescription": "Enables the set_skip_taskbar command without any pre-configured scope." - }, - { - "description": "Enables the set_theme command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-theme", - "markdownDescription": "Enables the set_theme command without any pre-configured scope." - }, - { - "description": "Enables the set_title command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-title", - "markdownDescription": "Enables the set_title command without any pre-configured scope." - }, - { - "description": "Enables the set_title_bar_style command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-title-bar-style", - "markdownDescription": "Enables the set_title_bar_style command without any pre-configured scope." - }, - { - "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-set-visible-on-all-workspaces", - "markdownDescription": "Enables the set_visible_on_all_workspaces command without any pre-configured scope." - }, - { - "description": "Enables the show command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-show", - "markdownDescription": "Enables the show command without any pre-configured scope." - }, - { - "description": "Enables the start_dragging command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-start-dragging", - "markdownDescription": "Enables the start_dragging command without any pre-configured scope." - }, - { - "description": "Enables the start_resize_dragging command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-start-resize-dragging", - "markdownDescription": "Enables the start_resize_dragging command without any pre-configured scope." - }, - { - "description": "Enables the theme command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-theme", - "markdownDescription": "Enables the theme command without any pre-configured scope." - }, - { - "description": "Enables the title command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-title", - "markdownDescription": "Enables the title command without any pre-configured scope." - }, - { - "description": "Enables the toggle_maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-toggle-maximize", - "markdownDescription": "Enables the toggle_maximize command without any pre-configured scope." - }, - { - "description": "Enables the unmaximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-unmaximize", - "markdownDescription": "Enables the unmaximize command without any pre-configured scope." - }, - { - "description": "Enables the unminimize command without any pre-configured scope.", - "type": "string", - "const": "core:window:allow-unminimize", - "markdownDescription": "Enables the unminimize command without any pre-configured scope." - }, - { - "description": "Denies the activity_name command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-activity-name", - "markdownDescription": "Denies the activity_name command without any pre-configured scope." - }, - { - "description": "Denies the available_monitors command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-available-monitors", - "markdownDescription": "Denies the available_monitors command without any pre-configured scope." - }, - { - "description": "Denies the center command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-center", - "markdownDescription": "Denies the center command without any pre-configured scope." - }, - { - "description": "Denies the close command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-close", - "markdownDescription": "Denies the close command without any pre-configured scope." - }, - { - "description": "Denies the create command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-create", - "markdownDescription": "Denies the create command without any pre-configured scope." - }, - { - "description": "Denies the current_monitor command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-current-monitor", - "markdownDescription": "Denies the current_monitor command without any pre-configured scope." - }, - { - "description": "Denies the cursor_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-cursor-position", - "markdownDescription": "Denies the cursor_position command without any pre-configured scope." - }, - { - "description": "Denies the destroy command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-destroy", - "markdownDescription": "Denies the destroy command without any pre-configured scope." - }, - { - "description": "Denies the get_all_windows command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-get-all-windows", - "markdownDescription": "Denies the get_all_windows command without any pre-configured scope." - }, - { - "description": "Denies the hide command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-hide", - "markdownDescription": "Denies the hide command without any pre-configured scope." - }, - { - "description": "Denies the inner_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-inner-position", - "markdownDescription": "Denies the inner_position command without any pre-configured scope." - }, - { - "description": "Denies the inner_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-inner-size", - "markdownDescription": "Denies the inner_size command without any pre-configured scope." - }, - { - "description": "Denies the internal_toggle_maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-internal-toggle-maximize", - "markdownDescription": "Denies the internal_toggle_maximize command without any pre-configured scope." - }, - { - "description": "Denies the is_always_on_top command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-always-on-top", - "markdownDescription": "Denies the is_always_on_top command without any pre-configured scope." - }, - { - "description": "Denies the is_closable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-closable", - "markdownDescription": "Denies the is_closable command without any pre-configured scope." - }, - { - "description": "Denies the is_decorated command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-decorated", - "markdownDescription": "Denies the is_decorated command without any pre-configured scope." - }, - { - "description": "Denies the is_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-enabled", - "markdownDescription": "Denies the is_enabled command without any pre-configured scope." - }, - { - "description": "Denies the is_focused command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-focused", - "markdownDescription": "Denies the is_focused command without any pre-configured scope." - }, - { - "description": "Denies the is_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-fullscreen", - "markdownDescription": "Denies the is_fullscreen command without any pre-configured scope." - }, - { - "description": "Denies the is_maximizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-maximizable", - "markdownDescription": "Denies the is_maximizable command without any pre-configured scope." - }, - { - "description": "Denies the is_maximized command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-maximized", - "markdownDescription": "Denies the is_maximized command without any pre-configured scope." - }, - { - "description": "Denies the is_minimizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-minimizable", - "markdownDescription": "Denies the is_minimizable command without any pre-configured scope." - }, - { - "description": "Denies the is_minimized command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-minimized", - "markdownDescription": "Denies the is_minimized command without any pre-configured scope." - }, - { - "description": "Denies the is_resizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-resizable", - "markdownDescription": "Denies the is_resizable command without any pre-configured scope." - }, - { - "description": "Denies the is_visible command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-is-visible", - "markdownDescription": "Denies the is_visible command without any pre-configured scope." - }, - { - "description": "Denies the maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-maximize", - "markdownDescription": "Denies the maximize command without any pre-configured scope." - }, - { - "description": "Denies the minimize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-minimize", - "markdownDescription": "Denies the minimize command without any pre-configured scope." - }, - { - "description": "Denies the monitor_from_point command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-monitor-from-point", - "markdownDescription": "Denies the monitor_from_point command without any pre-configured scope." - }, - { - "description": "Denies the outer_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-outer-position", - "markdownDescription": "Denies the outer_position command without any pre-configured scope." - }, - { - "description": "Denies the outer_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-outer-size", - "markdownDescription": "Denies the outer_size command without any pre-configured scope." - }, - { - "description": "Denies the primary_monitor command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-primary-monitor", - "markdownDescription": "Denies the primary_monitor command without any pre-configured scope." - }, - { - "description": "Denies the request_user_attention command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-request-user-attention", - "markdownDescription": "Denies the request_user_attention command without any pre-configured scope." - }, - { - "description": "Denies the scale_factor command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-scale-factor", - "markdownDescription": "Denies the scale_factor command without any pre-configured scope." - }, - { - "description": "Denies the scene_identifier command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-scene-identifier", - "markdownDescription": "Denies the scene_identifier command without any pre-configured scope." - }, - { - "description": "Denies the set_always_on_bottom command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-always-on-bottom", - "markdownDescription": "Denies the set_always_on_bottom command without any pre-configured scope." - }, - { - "description": "Denies the set_always_on_top command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-always-on-top", - "markdownDescription": "Denies the set_always_on_top command without any pre-configured scope." - }, - { - "description": "Denies the set_background_color command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-background-color", - "markdownDescription": "Denies the set_background_color command without any pre-configured scope." - }, - { - "description": "Denies the set_badge_count command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-badge-count", - "markdownDescription": "Denies the set_badge_count command without any pre-configured scope." - }, - { - "description": "Denies the set_badge_label command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-badge-label", - "markdownDescription": "Denies the set_badge_label command without any pre-configured scope." - }, - { - "description": "Denies the set_closable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-closable", - "markdownDescription": "Denies the set_closable command without any pre-configured scope." - }, - { - "description": "Denies the set_content_protected command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-content-protected", - "markdownDescription": "Denies the set_content_protected command without any pre-configured scope." - }, - { - "description": "Denies the set_cursor_grab command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-cursor-grab", - "markdownDescription": "Denies the set_cursor_grab command without any pre-configured scope." - }, - { - "description": "Denies the set_cursor_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-cursor-icon", - "markdownDescription": "Denies the set_cursor_icon command without any pre-configured scope." - }, - { - "description": "Denies the set_cursor_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-cursor-position", - "markdownDescription": "Denies the set_cursor_position command without any pre-configured scope." - }, - { - "description": "Denies the set_cursor_visible command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-cursor-visible", - "markdownDescription": "Denies the set_cursor_visible command without any pre-configured scope." - }, - { - "description": "Denies the set_decorations command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-decorations", - "markdownDescription": "Denies the set_decorations command without any pre-configured scope." - }, - { - "description": "Denies the set_effects command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-effects", - "markdownDescription": "Denies the set_effects command without any pre-configured scope." - }, - { - "description": "Denies the set_enabled command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-enabled", - "markdownDescription": "Denies the set_enabled command without any pre-configured scope." - }, - { - "description": "Denies the set_focus command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-focus", - "markdownDescription": "Denies the set_focus command without any pre-configured scope." - }, - { - "description": "Denies the set_focusable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-focusable", - "markdownDescription": "Denies the set_focusable command without any pre-configured scope." - }, - { - "description": "Denies the set_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-fullscreen", - "markdownDescription": "Denies the set_fullscreen command without any pre-configured scope." - }, - { - "description": "Denies the set_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-icon", - "markdownDescription": "Denies the set_icon command without any pre-configured scope." - }, - { - "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-ignore-cursor-events", - "markdownDescription": "Denies the set_ignore_cursor_events command without any pre-configured scope." - }, - { - "description": "Denies the set_max_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-max-size", - "markdownDescription": "Denies the set_max_size command without any pre-configured scope." - }, - { - "description": "Denies the set_maximizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-maximizable", - "markdownDescription": "Denies the set_maximizable command without any pre-configured scope." - }, - { - "description": "Denies the set_min_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-min-size", - "markdownDescription": "Denies the set_min_size command without any pre-configured scope." - }, - { - "description": "Denies the set_minimizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-minimizable", - "markdownDescription": "Denies the set_minimizable command without any pre-configured scope." - }, - { - "description": "Denies the set_overlay_icon command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-overlay-icon", - "markdownDescription": "Denies the set_overlay_icon command without any pre-configured scope." - }, - { - "description": "Denies the set_position command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-position", - "markdownDescription": "Denies the set_position command without any pre-configured scope." - }, - { - "description": "Denies the set_progress_bar command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-progress-bar", - "markdownDescription": "Denies the set_progress_bar command without any pre-configured scope." - }, - { - "description": "Denies the set_resizable command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-resizable", - "markdownDescription": "Denies the set_resizable command without any pre-configured scope." - }, - { - "description": "Denies the set_shadow command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-shadow", - "markdownDescription": "Denies the set_shadow command without any pre-configured scope." - }, - { - "description": "Denies the set_simple_fullscreen command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-simple-fullscreen", - "markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope." - }, - { - "description": "Denies the set_size command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-size", - "markdownDescription": "Denies the set_size command without any pre-configured scope." - }, - { - "description": "Denies the set_size_constraints command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-size-constraints", - "markdownDescription": "Denies the set_size_constraints command without any pre-configured scope." - }, - { - "description": "Denies the set_skip_taskbar command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-skip-taskbar", - "markdownDescription": "Denies the set_skip_taskbar command without any pre-configured scope." - }, - { - "description": "Denies the set_theme command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-theme", - "markdownDescription": "Denies the set_theme command without any pre-configured scope." - }, - { - "description": "Denies the set_title command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-title", - "markdownDescription": "Denies the set_title command without any pre-configured scope." - }, - { - "description": "Denies the set_title_bar_style command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-title-bar-style", - "markdownDescription": "Denies the set_title_bar_style command without any pre-configured scope." - }, - { - "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-set-visible-on-all-workspaces", - "markdownDescription": "Denies the set_visible_on_all_workspaces command without any pre-configured scope." - }, - { - "description": "Denies the show command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-show", - "markdownDescription": "Denies the show command without any pre-configured scope." - }, - { - "description": "Denies the start_dragging command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-start-dragging", - "markdownDescription": "Denies the start_dragging command without any pre-configured scope." - }, - { - "description": "Denies the start_resize_dragging command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-start-resize-dragging", - "markdownDescription": "Denies the start_resize_dragging command without any pre-configured scope." - }, - { - "description": "Denies the theme command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-theme", - "markdownDescription": "Denies the theme command without any pre-configured scope." - }, - { - "description": "Denies the title command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-title", - "markdownDescription": "Denies the title command without any pre-configured scope." - }, - { - "description": "Denies the toggle_maximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-toggle-maximize", - "markdownDescription": "Denies the toggle_maximize command without any pre-configured scope." - }, - { - "description": "Denies the unmaximize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-unmaximize", - "markdownDescription": "Denies the unmaximize command without any pre-configured scope." - }, - { - "description": "Denies the unminimize command without any pre-configured scope.", - "type": "string", - "const": "core:window:deny-unminimize", - "markdownDescription": "Denies the unminimize command without any pre-configured scope." - }, - { - "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`", - "type": "string", - "const": "dialog:default", - "markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`" - }, - { - "description": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", - "type": "string", - "const": "dialog:allow-ask", - "markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" - }, - { - "description": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", - "type": "string", - "const": "dialog:allow-confirm", - "markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" - }, - { - "description": "Enables the message command without any pre-configured scope.", - "type": "string", - "const": "dialog:allow-message", - "markdownDescription": "Enables the message command without any pre-configured scope." - }, - { - "description": "Enables the open command without any pre-configured scope.", - "type": "string", - "const": "dialog:allow-open", - "markdownDescription": "Enables the open command without any pre-configured scope." - }, - { - "description": "Enables the save command without any pre-configured scope.", - "type": "string", - "const": "dialog:allow-save", - "markdownDescription": "Enables the save command without any pre-configured scope." - }, - { - "description": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", - "type": "string", - "const": "dialog:deny-ask", - "markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" - }, - { - "description": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", - "type": "string", - "const": "dialog:deny-confirm", - "markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" - }, - { - "description": "Denies the message command without any pre-configured scope.", - "type": "string", - "const": "dialog:deny-message", - "markdownDescription": "Denies the message command without any pre-configured scope." - }, - { - "description": "Denies the open command without any pre-configured scope.", - "type": "string", - "const": "dialog:deny-open", - "markdownDescription": "Denies the open command without any pre-configured scope." - }, - { - "description": "Denies the save command without any pre-configured scope.", - "type": "string", - "const": "dialog:deny-save", - "markdownDescription": "Denies the save command without any pre-configured scope." - }, - { - "description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`", - "type": "string", - "const": "shell:default", - "markdownDescription": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`" - }, - { - "description": "Enables the execute command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-execute", - "markdownDescription": "Enables the execute command without any pre-configured scope." - }, - { - "description": "Enables the kill command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-kill", - "markdownDescription": "Enables the kill command without any pre-configured scope." - }, - { - "description": "Enables the open command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-open", - "markdownDescription": "Enables the open command without any pre-configured scope." - }, - { - "description": "Enables the spawn command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-spawn", - "markdownDescription": "Enables the spawn command without any pre-configured scope." - }, - { - "description": "Enables the stdin_write command without any pre-configured scope.", - "type": "string", - "const": "shell:allow-stdin-write", - "markdownDescription": "Enables the stdin_write command without any pre-configured scope." - }, - { - "description": "Denies the execute command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-execute", - "markdownDescription": "Denies the execute command without any pre-configured scope." - }, - { - "description": "Denies the kill command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-kill", - "markdownDescription": "Denies the kill command without any pre-configured scope." - }, - { - "description": "Denies the open command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-open", - "markdownDescription": "Denies the open command without any pre-configured scope." - }, - { - "description": "Denies the spawn command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-spawn", - "markdownDescription": "Denies the spawn command without any pre-configured scope." - }, - { - "description": "Denies the stdin_write command without any pre-configured scope.", - "type": "string", - "const": "shell:deny-stdin-write", - "markdownDescription": "Denies the stdin_write command without any pre-configured scope." - } - ] - }, - "Value": { - "description": "All supported ACL values.", - "anyOf": [ - { - "description": "Represents a null JSON value.", - "type": "null" - }, - { - "description": "Represents a [`bool`].", - "type": "boolean" - }, - { - "description": "Represents a valid ACL [`Number`].", - "allOf": [ - { - "$ref": "#/definitions/Number" - } - ] - }, - { - "description": "Represents a [`String`].", - "type": "string" - }, - { - "description": "Represents a list of other [`Value`]s.", - "type": "array", - "items": { - "$ref": "#/definitions/Value" - } - }, - { - "description": "Represents a map of [`String`] keys to [`Value`]s.", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/Value" - } - } - ] - }, - "Number": { - "description": "A valid ACL number.", - "anyOf": [ - { - "description": "Represents an [`i64`].", - "type": "integer", - "format": "int64" - }, - { - "description": "Represents a [`f64`].", - "type": "number", - "format": "double" - } - ] - }, - "Target": { - "description": "Platform target.", - "oneOf": [ - { - "description": "MacOS.", - "type": "string", - "enum": [ - "macOS" - ] - }, - { - "description": "Windows.", - "type": "string", - "enum": [ - "windows" - ] - }, - { - "description": "Linux.", - "type": "string", - "enum": [ - "linux" - ] - }, - { - "description": "Android.", - "type": "string", - "enum": [ - "android" - ] - }, - { - "description": "iOS.", - "type": "string", - "enum": [ - "iOS" - ] - } - ] - }, - "ShellScopeEntryAllowedArg": { - "description": "A command argument allowed to be executed by the webview API.", - "anyOf": [ - { - "description": "A non-configurable argument that is passed to the command in the order it was specified.", - "type": "string" - }, - { - "description": "A variable that is set while calling the command from the webview API.", - "type": "object", - "required": [ - "validator" - ], - "properties": { - "raw": { - "description": "Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.", - "default": false, - "type": "boolean" - }, - "validator": { - "description": "[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: ", - "type": "string" - } - }, - "additionalProperties": false - } - ] - }, - "ShellScopeEntryAllowedArgs": { - "description": "A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.", - "anyOf": [ - { - "description": "Use a simple boolean to allow all or disable all arguments to this command configuration.", - "type": "boolean" - }, - { - "description": "A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.", - "type": "array", - "items": { - "$ref": "#/definitions/ShellScopeEntryAllowedArg" - } - } - ] - } - } -} \ No newline at end of file diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json deleted file mode 100644 index 2414941..0000000 --- a/src-tauri/tauri.conf.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "productName": "Chelete", - "version": "0.6.0", - "identifier": "com.chelete.app", - "build": { - "beforeDevCommand": "npm run dev", - "devUrl": "http://localhost:1420", - "beforeBuildCommand": "npm run build", - "frontendDist": "../dist" - }, - "app": { - "windows": [ - { - "title": "Chelete", - "width": 1100, - "height": 700, - "decorations": false - } - ], - "security": { - "csp": null, - "capabilities": ["default"] - } - }, - "bundle": { - "active": true, - "targets": ["appimage", "deb"], - "icon": [ - "icons/32x32.png", - "icons/128x128.png", - "icons/128x128@2x.png", - "icons/icon.icns", - "icons/icon.ico" - ] - } -} diff --git a/src/App.tsx b/src/App.tsx deleted file mode 100644 index e14e402..0000000 --- a/src/App.tsx +++ /dev/null @@ -1,219 +0,0 @@ -import { useState, useEffect, useCallback } from "react"; -import { useTheme } from "./hooks/useTheme"; -import { OverviewScreen } from "./features/overview/OverviewScreen"; -import { TransactionsScreen } from "./features/transactions/TransactionsScreen"; -import { AccountsScreen } from "./features/accounts/AccountsScreen"; -import { CategoriesScreen } from "./features/categories/CategoriesScreen"; -import { SubscriptionsScreen } from "./features/subscriptions/SubscriptionsScreen"; -import { ProjectionsScreen } from "./features/projections/ProjectionsScreen"; -import { SettingsScreen } from "./features/settings/SettingsScreen"; -import { CommandPalette } from "./components/CommandPalette"; -import type { View } from "./types"; -import { - LayoutDashboard, - ArrowLeftRight, - Wallet, - Tag, - Repeat, - LineChart, - Settings, - Command, - PanelLeftClose, - PanelLeftOpen, - type LucideIcon, -} from "lucide-react"; - -const NAV_ITEMS: { view: View; label: string; shortcut: string; icon: LucideIcon }[] = [ - { view: "overview", label: "Overview", shortcut: "Ctrl+O", icon: LayoutDashboard }, - { view: "transactions", label: "Transactions", shortcut: "Ctrl+T", icon: ArrowLeftRight }, - { view: "accounts", label: "Accounts", shortcut: "Ctrl+A", icon: Wallet }, - { view: "categories", label: "Categories", shortcut: "Ctrl+C", icon: Tag }, - { view: "subscriptions", label: "Subscriptions", shortcut: "Ctrl+U", icon: Repeat }, - { view: "projections", label: "Projections", shortcut: "Ctrl+P", icon: LineChart }, - { view: "settings", label: "Settings", shortcut: "Ctrl+S", icon: Settings }, -]; - -export default function App() { - const [view, setView] = useState("overview"); - const [commandPalette, setCommandPalette] = useState(false); - const [collapsed, setCollapsed] = useState(() => { - return localStorage.getItem("sidebar-collapsed") === "true"; - }); - - useTheme(); - - // Apply saved corner radius preference - useEffect(() => { - const saved = localStorage.getItem("chelete-radius"); - const radius = saved === "square" ? "0px" : "4px"; - document.documentElement.style.setProperty("--chelete-radius", radius); - }, []); - - const toggleCollapsed = () => { - setCollapsed((prev) => { - localStorage.setItem("sidebar-collapsed", String(!prev)); - return !prev; - }); - }; - - // Global keyboard shortcuts - const handleKeyDown = useCallback( - (e: KeyboardEvent) => { - const ctrl = e.ctrlKey || e.metaKey; - - // Command palette: Ctrl+K - if (ctrl && e.key === "k") { - e.preventDefault(); - setCommandPalette((o) => !o); - return; - } - - // Toggle sidebar: Ctrl+B - if (ctrl && e.key === "b") { - e.preventDefault(); - toggleCollapsed(); - return; - } - - // Escape closes command palette - if (e.key === "Escape") { - setCommandPalette(false); - return; - } - - // Skip if typing in an input - const target = e.target as HTMLElement; - if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT") { - return; - } - - if (ctrl) { - const viewMap: Record = { - o: "overview", - t: "transactions", - a: "accounts", - c: "categories", - u: "subscriptions", - p: "projections", - s: "settings", - }; - if (viewMap[e.key]) { - e.preventDefault(); - setView(viewMap[e.key]); - } - } - }, - [] - ); - - useEffect(() => { - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [handleKeyDown]); - - const renderScreen = () => { - switch (view) { - case "overview": - return ; - case "transactions": - return ; - case "accounts": - return ; - case "categories": - return ; - case "subscriptions": - return ; - case "projections": - return ; - case "settings": - return ; - } - }; - - return ( -
- - -
{renderScreen()}
- - setCommandPalette(false)} - onNavigate={(v) => { - setView(v); - setCommandPalette(false); - }} - onAction={(action) => { - setCommandPalette(false); - if (action === "add-transaction") setView("transactions"); - else if (action === "import-transactions") { - setView("transactions"); - window.dispatchEvent(new CustomEvent("chelete-open-import")); - } - else if (action === "add-account") setView("accounts"); - else if (action === "add-category") setView("categories"); - else if (action === "add-subscription") setView("subscriptions"); - }} - /> -
- ); -} diff --git a/src/components/CategoryIcons.tsx b/src/components/CategoryIcons.tsx deleted file mode 100644 index 292399c..0000000 --- a/src/components/CategoryIcons.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { - ShoppingCart, - Car, - Briefcase, - Film, - ShoppingBag, - Zap, - Heart, - BookOpen, - Coffee, - Plane, - Home, - Utensils, - Wifi, - Smartphone, - Music, - Gamepad2, - Gift, - Stethoscope, - GraduationCap, - Building2, - PiggyBank, - CreditCard, - Wallet, - Wrench, - Bot, - Code, - Laptop, - Tv, - Dumbbell, - Dog, - Cat, - Leaf, - Shirt, - Scissors, - Bus, - Train, - Hotel, - Wine, - Landmark, - Shield, - Cloud, - CircleQuestionMark, - UserRound, - type LucideIcon, -} from "lucide-react"; - -export interface CategoryIconDef { - name: string; - icon: LucideIcon; - label: string; -} - -export const CATEGORY_ICONS: CategoryIconDef[] = [ - { name: "utensils", icon: Utensils, label: "Food" }, - { name: "car", icon: Car, label: "Car" }, - { name: "briefcase", icon: Briefcase, label: "Work" }, - { name: "film", icon: Film, label: "Film" }, - { name: "shopping-bag", icon: ShoppingBag, label: "Shopping" }, - { name: "zap", icon: Zap, label: "Bills" }, - { name: "heart", icon: Heart, label: "Health" }, - { name: "book-open", icon: BookOpen, label: "Education" }, - { name: "coffee", icon: Coffee, label: "Coffee" }, - { name: "plane", icon: Plane, label: "Travel" }, - { name: "home", icon: Home, label: "Home" }, - { name: "shopping-cart", icon: ShoppingCart, label: "Cart" }, - { name: "wifi", icon: Wifi, label: "Internet" }, - { name: "smartphone", icon: Smartphone, label: "Phone" }, - { name: "music", icon: Music, label: "Music" }, - { name: "gamepad-2", icon: Gamepad2, label: "Gaming" }, - { name: "gift", icon: Gift, label: "Gift" }, - { name: "stethoscope", icon: Stethoscope, label: "Medical" }, - { name: "graduation-cap", icon: GraduationCap, label: "School" }, - { name: "building-2", icon: Building2, label: "Office" }, - { name: "piggy-bank", icon: PiggyBank, label: "Savings" }, - { name: "credit-card", icon: CreditCard, label: "Credit" }, - { name: "wallet", icon: Wallet, label: "Wallet" }, - { name: "wrench", icon: Wrench, label: "Repairs" }, - { name: "bot", icon: Bot, label: "AI" }, - { name: "code", icon: Code, label: "Code" }, - { name: "laptop", icon: Laptop, label: "Laptop" }, - { name: "tv", icon: Tv, label: "Streaming" }, - { name: "dumbbell", icon: Dumbbell, label: "Fitness" }, - { name: "dog", icon: Dog, label: "Dog" }, - { name: "cat", icon: Cat, label: "Cat" }, - { name: "leaf", icon: Leaf, label: "Nature" }, - { name: "shirt", icon: Shirt, label: "Clothing" }, - { name: "scissors", icon: Scissors, label: "Grooming" }, - { name: "bus", icon: Bus, label: "Bus" }, - { name: "train", icon: Train, label: "Train" }, - { name: "hotel", icon: Hotel, label: "Hotel" }, - { name: "wine", icon: Wine, label: "Drinks" }, - { name: "landmark", icon: Landmark, label: "Taxes" }, - { name: "shield", icon: Shield, label: "Insurance" }, - { name: "cloud", icon: Cloud, label: "Cloud" }, - { name: "circle-question-mark", icon: CircleQuestionMark, label: "Unaccounted" }, - { name: "user-round", icon: UserRound, label: "Woman" }, -]; - -export function CategoryIcon({ - name, - size = 14, - style, -}: { - name: string; - size?: number; - style?: React.CSSProperties; -}) { - const iconEntry = CATEGORY_ICONS.find((i) => i.name === name); - if (iconEntry) { - const Icon = iconEntry.icon; - return ; - } - return null; -} diff --git a/src/components/CommandPalette.tsx b/src/components/CommandPalette.tsx deleted file mode 100644 index 4758973..0000000 --- a/src/components/CommandPalette.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { useEffect, useState, useRef } from "react"; -import type { View } from "../types"; - -interface Command { - id: string; - label: string; - shortcut?: string; - action: () => void; -} - -export function CommandPalette({ - open, - onClose, - onNavigate, - onAction, -}: { - open: boolean; - onClose: () => void; - onNavigate: (view: View) => void; - onAction: (action: string) => void; -}) { - const [query, setQuery] = useState(""); - const [selected, setSelected] = useState(0); - const inputRef = useRef(null); - - const commands: Command[] = [ - { id: "overview", label: "Go to Overview", shortcut: "Ctrl+O", action: () => onNavigate("overview") }, - { id: "transactions", label: "Go to Transactions", shortcut: "Ctrl+T", action: () => onNavigate("transactions") }, - { id: "accounts", label: "Go to Accounts", shortcut: "Ctrl+A", action: () => onNavigate("accounts") }, - { id: "categories", label: "Go to Categories", shortcut: "Ctrl+C", action: () => onNavigate("categories") }, - { id: "subscriptions", label: "Go to Subscriptions", shortcut: "Ctrl+U", action: () => onNavigate("subscriptions") }, - { id: "projections", label: "Go to Projections", shortcut: "Ctrl+P", action: () => onNavigate("projections") }, - { id: "settings", label: "Go to Settings", shortcut: "Ctrl+S", action: () => onNavigate("settings") }, - { id: "add-transaction", label: "Add Transaction", action: () => onAction("add-transaction") }, - { id: "import-transactions", label: "Import Transactions (CSV)", action: () => onAction("import-transactions") }, - { id: "add-account", label: "Add Account", action: () => onAction("add-account") }, - { id: "add-category", label: "Add Category", action: () => onAction("add-category") }, - { id: "add-subscription", label: "Add Subscription", action: () => onAction("add-subscription") }, - ]; - - const filtered = commands.filter((c) => - c.label.toLowerCase().includes(query.toLowerCase()) - ); - - useEffect(() => { - if (open) { - setQuery(""); - setSelected(0); - setTimeout(() => inputRef.current?.focus(), 50); - } - }, [open]); - - useEffect(() => { - setSelected(0); - }, [query]); - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "ArrowDown") { - e.preventDefault(); - setSelected((s) => Math.min(s + 1, filtered.length - 1)); - } else if (e.key === "ArrowUp") { - e.preventDefault(); - setSelected((s) => Math.max(s - 1, 0)); - } else if (e.key === "Enter") { - e.preventDefault(); - if (filtered[selected]) { - filtered[selected].action(); - onClose(); - } - } else if (e.key === "Escape") { - onClose(); - } - }; - - if (!open) return null; - - return ( -
-
e.stopPropagation()}> - setQuery(e.target.value)} - onKeyDown={handleKeyDown} - /> -
- {filtered.map((cmd, i) => ( -
{ - cmd.action(); - onClose(); - }} - onMouseEnter={() => setSelected(i)} - > - {cmd.label} - {cmd.shortcut && {cmd.shortcut}} -
- ))} - {filtered.length === 0 && ( -
- No matching commands -
- )} -
-
-
- ); -} diff --git a/src/components/DateFilter.tsx b/src/components/DateFilter.tsx deleted file mode 100644 index bccd66c..0000000 --- a/src/components/DateFilter.tsx +++ /dev/null @@ -1,207 +0,0 @@ -import { useState, useRef, useEffect } from "react"; -import { ChevronDown, Calendar, Check, X } from "lucide-react"; -import DatePicker from "react-datepicker"; -import "react-datepicker/dist/react-datepicker.css"; - -export type DateFilterValue = - | "today" - | "yesterday" - | "this_week" - | "last_week" - | "this_month" - | "last_month" - | "this_year" - | "last_year" - | "all" - | "custom"; - -interface DateFilterProps { - value: DateFilterValue; - onChange: (value: DateFilterValue) => void; - customStart: string; - customEnd: string; - onCustomStartChange: (v: string) => void; - onCustomEndChange: (v: string) => void; -} - -const FILTERS: { value: DateFilterValue; label: string }[] = [ - { value: "today", label: "Today" }, - { value: "yesterday", label: "Yesterday" }, - { value: "this_week", label: "This Week" }, - { value: "last_week", label: "Last Week" }, - { value: "this_month", label: "This Month" }, - { value: "last_month", label: "Last Month" }, - { value: "this_year", label: "This Year" }, - { value: "last_year", label: "Last Year" }, - { value: "all", label: "All Time" }, - { value: "custom", label: "Custom Range" }, -]; - -export function DateFilter({ - value, - onChange, - customStart, - customEnd, - onCustomStartChange, - onCustomEndChange, -}: DateFilterProps) { - const [menuOpen, setMenuOpen] = useState(false); - const [calendarOpen, setCalendarOpen] = useState(false); - const ref = useRef(null); - - const label = FILTERS.find((f) => f.value === value)?.label || "All Time"; - - useEffect(() => { - const handler = (e: MouseEvent) => { - if (ref.current && !ref.current.contains(e.target as Node)) setMenuOpen(false); - }; - document.addEventListener("mousedown", handler); - return () => document.removeEventListener("mousedown", handler); - }, []); - - const startDate = customStart ? new Date(customStart + "T00:00:00") : null; - const endDate = customEnd ? new Date(customEnd + "T00:00:00") : null; - - return ( -
-
- - - {menuOpen && ( -
- {FILTERS.map((f) => ( - - ))} -
- )} -
- - {value === "custom" && ( - - )} - - {calendarOpen && ( -
setCalendarOpen(false)}> -
e.stopPropagation()}> -
- Select Date Range - -
-
- { - if (date) onCustomStartChange(formatDate(date)); - }} - selectsStart - startDate={startDate} - endDate={endDate} - inline - calendarClassName="df-calendar" - dayClassName={(d) => { - if (!startDate || !endDate) return "df-day"; - const ds = formatDate(d); - if (ds >= formatDate(startDate) && ds <= formatDate(endDate)) return "df-day df-day-inrange"; - return "df-day"; - }} - /> - { - if (date) onCustomEndChange(formatDate(date)); - }} - selectsEnd - startDate={startDate} - endDate={endDate} - minDate={startDate || undefined} - inline - calendarClassName="df-calendar" - dayClassName={(d) => { - if (!startDate || !endDate) return "df-day"; - const ds = formatDate(d); - if (ds >= formatDate(startDate) && ds <= formatDate(endDate)) return "df-day df-day-inrange"; - return "df-day"; - }} - /> -
-
- - -
-
-
- )} -
- ); -} - -function formatDate(d: Date): string { - const y = d.getFullYear(); - const m = String(d.getMonth() + 1).padStart(2, "0"); - const day = String(d.getDate()).padStart(2, "0"); - return `${y}-${m}-${day}`; -} - -export function getDateRange(filter: DateFilterValue, customStart: string, customEnd: string): { start: string; end: string } { - const now = new Date(); - const today = now.toISOString().split("T")[0]; - const toStr = (d: Date) => d.toISOString().split("T")[0]; - - const startOfWeek = new Date(now); startOfWeek.setDate(now.getDate() - now.getDay()); - const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); - const startOfYear = new Date(now.getFullYear(), 0, 1); - - const endOfLastWeek = new Date(now); endOfLastWeek.setDate(now.getDate() - now.getDay() - 1); - const startOfLastWeek = new Date(endOfLastWeek); startOfLastWeek.setDate(endOfLastWeek.getDate() - 6); - - const startOfLastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1); - const endOfLastMonth = new Date(now.getFullYear(), now.getMonth(), 0); - - const startOfLastYear = new Date(now.getFullYear() - 1, 0, 1); - const endOfLastYear = new Date(now.getFullYear() - 1, 11, 31); - - switch (filter) { - case "today": return { start: today, end: today }; - case "yesterday": { const y = toStr(new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1)); return { start: y, end: y }; } - case "this_week": return { start: toStr(startOfWeek), end: today }; - case "last_week": return { start: toStr(startOfLastWeek), end: toStr(endOfLastWeek) }; - case "this_month": return { start: toStr(startOfMonth), end: today }; - case "last_month": return { start: toStr(startOfLastMonth), end: toStr(endOfLastMonth) }; - case "this_year": return { start: toStr(startOfYear), end: today }; - case "last_year": return { start: toStr(startOfLastYear), end: toStr(endOfLastYear) }; - case "all": return { start: "0000-01-01", end: "9999-12-31" }; - case "custom": return { start: customStart || "0000-01-01", end: customEnd || "9999-12-31" }; - } -} - -export function isDateInRange(dateStr: string, start: string, end: string): boolean { - return dateStr >= start && dateStr <= end; -} diff --git a/src/components/DonutChart.tsx b/src/components/DonutChart.tsx deleted file mode 100644 index 686235f..0000000 --- a/src/components/DonutChart.tsx +++ /dev/null @@ -1,201 +0,0 @@ -import { useMemo } from "react"; - -interface DonutChartItem { - label: string; - value: number; - color?: string | null; -} - -interface DonutChartProps { - title: string; - data: DonutChartItem[]; - emptyMessage?: string; -} - -const FALLBACK_COLORS = [ - "#7aa2f7", - "#9ece6a", - "#f7768e", - "#e0af68", - "#7dcfff", - "#bb9af7", - "#ff9e64", - "#73daca", - "#b4f9f8", - "#565f89", -]; - -export function DonutChart({ - title, - data, - emptyMessage = "No data", -}: DonutChartProps) { - const total = useMemo( - () => data.reduce((sum, item) => sum + item.value, 0), - [data] - ); - - const segments = useMemo(() => { - if (total === 0) return []; - let startAngle = 0; - return data.map((item, index) => { - const fraction = item.value / total; - const angle = fraction * 360; - const endAngle = startAngle + angle; - const color = item.color || FALLBACK_COLORS[index % FALLBACK_COLORS.length]; - const segment = { - ...item, - fraction, - startAngle, - endAngle, - color, - }; - startAngle = endAngle; - return segment; - }); - }, [data, total]); - - return ( -
-
{title}
-
- {total === 0 ? ( -
{emptyMessage}
- ) : ( - <> - - - {segments.map((segment, index) => ( - - ))} - - - {title} - - - {formatTotal(total)} - - -
- {segments.map((segment, index) => ( -
- - - {segment.label} - - - {Math.round(segment.fraction * 100)}% - -
- ))} -
- - )} -
-
- ); -} - -function DonutSegment({ - cx, - cy, - innerRadius, - outerRadius, - startAngle, - endAngle, - color, -}: { - cx: number; - cy: number; - innerRadius: number; - outerRadius: number; - startAngle: number; - endAngle: number; - color: string; -}) { - const sweep = endAngle - startAngle; - - // SVG arcs cannot represent a full 360° circle in one path, so split it. - if (sweep >= 360) { - return ( - <> - {donutArcPath(cx, cy, innerRadius, outerRadius, startAngle, startAngle + 180, color)} - {donutArcPath(cx, cy, innerRadius, outerRadius, startAngle + 180, endAngle, color)} - - ); - } - - return donutArcPath(cx, cy, innerRadius, outerRadius, startAngle, endAngle, color); -} - -function donutArcPath( - cx: number, - cy: number, - innerRadius: number, - outerRadius: number, - startAngle: number, - endAngle: number, - color: string -) { - const start = polarToCartesian(cx, cy, outerRadius, endAngle); - const end = polarToCartesian(cx, cy, outerRadius, startAngle); - const innerStart = polarToCartesian(cx, cy, innerRadius, endAngle); - const innerEnd = polarToCartesian(cx, cy, innerRadius, startAngle); - - const largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1"; - - const path = [ - `M ${start.x} ${start.y}`, - `A ${outerRadius} ${outerRadius} 0 ${largeArcFlag} 0 ${end.x} ${end.y}`, - `L ${innerEnd.x} ${innerEnd.y}`, - `A ${innerRadius} ${innerRadius} 0 ${largeArcFlag} 1 ${innerStart.x} ${innerStart.y}`, - "Z", - ].join(" "); - - return ; -} - -function polarToCartesian( - cx: number, - cy: number, - radius: number, - angleInDegrees: number -) { - const angleInRadians = (angleInDegrees * Math.PI) / 180; - return { - x: cx + radius * Math.cos(angleInRadians), - y: cy + radius * Math.sin(angleInRadians), - }; -} - -function formatTotal(amount: number): string { - const dollars = amount / 100; - if (Math.abs(dollars) >= 1000) { - return `$${(dollars / 1000).toFixed(1)}k`; - } - return `$${dollars.toFixed(0)}`; -} diff --git a/src/components/Pagination.tsx b/src/components/Pagination.tsx deleted file mode 100644 index 68ee1f6..0000000 --- a/src/components/Pagination.tsx +++ /dev/null @@ -1,42 +0,0 @@ -export function paginate(items: T[], page: number, perPage: number): T[] { - const start = (page - 1) * perPage; - return items.slice(start, start + perPage); -} - -export function Pagination({ - page, - perPage, - total, - onPageChange, -}: { - page: number; - perPage: number; - total: number; - onPageChange: (page: number) => void; -}) { - const totalPages = Math.max(1, Math.ceil(total / perPage)); - const start = (page - 1) * perPage + 1; - const end = Math.min(page * perPage, total); - - return ( -
- - - {start}–{end} of {total} - - -
- ); -} diff --git a/src/features/accounts/AccountsScreen.tsx b/src/features/accounts/AccountsScreen.tsx deleted file mode 100644 index 090ff4d..0000000 --- a/src/features/accounts/AccountsScreen.tsx +++ /dev/null @@ -1,327 +0,0 @@ -import { useEffect, useState } from "react"; -import { api } from "../../services/api"; -import { formatBalance, accountTypeLabel } from "../../services/format"; -import type { Account } from "../../types"; -import { ArrowRightLeft } from "lucide-react"; - -export function AccountsScreen() { - const [accounts, setAccounts] = useState([]); - const [showAdd, setShowAdd] = useState(false); - const [showTransfer, setShowTransfer] = useState(false); - const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null); - - const showNotification = (message: string, type: "success" | "error" = "success") => { - setToast({ message, type }); - setTimeout(() => setToast(null), 3000); - }; - - const load = () => { - api.getAccounts().then(setAccounts); - }; - - useEffect(() => { - load(); - }, []); - - return ( -
-
-
Accounts
-
- - -
-
- - {showAdd && ( - { - setShowAdd(false); - load(); - }} - onCancel={() => setShowAdd(false)} - /> - )} - - {showTransfer && ( - { - setShowTransfer(false); - load(); - }} - onCancel={() => setShowTransfer(false)} - showNotification={showNotification} - /> - )} - - {toast && ( -
- {toast.message} -
- )} - - {accounts.length === 0 ? ( -
- No accounts yet. Add one to get started. -
- ) : ( -
- {accounts.map((a) => ( -
-
-
-
{accountTypeLabel(a.account_type)}
-
- {a.name} -
-
-
-
{formatBalance(a.balance)}
-
- -
-
- ))} -
- )} -
- ); -} - -function TransferDialog({ - accounts, - onDone, - onCancel, - showNotification, -}: { - accounts: Account[]; - onDone: () => void; - onCancel: () => void; - showNotification: (message: string, type?: "success" | "error") => void; -}) { - const [fromAccountId, setFromAccountId] = useState(accounts[0]?.id || ""); - const [toAccountId, setToAccountId] = useState( - accounts[1]?.id || accounts[0]?.id || "" - ); - const [amount, setAmount] = useState(""); - const [notes, setNotes] = useState(""); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - const cents = Math.round(parseFloat(amount) * 100); - if (isNaN(cents) || cents <= 0 || fromAccountId === toAccountId) return; - - try { - await api.transfer({ - from_account_id: fromAccountId, - to_account_id: toAccountId, - amount: cents, - currency: "USD", - notes: notes || undefined, - }); - showNotification( - `Transferred $${amount} to ${accounts.find((a) => a.id === toAccountId)?.name}` - ); - onDone(); - } catch (err: any) { - showNotification( - `Transfer failed: ${err?.message || String(err)}`, - "error" - ); - } - }; - - return ( -
-
e.stopPropagation()}> -
Transfer Money
-
-
-
- - -
-
- - -
-
- -
- - setAmount(e.target.value)} - autoFocus - /> -
- -
- - setNotes(e.target.value)} - /> -
- -
- - -
-
-
-
- ); -} - -function AddAccountForm({ - onDone, - onCancel, -}: { - onDone: () => void; - onCancel: () => void; -}) { - const [name, setName] = useState(""); - const [type, setType] = useState("cash"); - const [balance, setBalance] = useState(""); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (!name.trim()) return; - - const cents = Math.round(parseFloat(balance || "0") * 100); - await api.createAccount({ - name: name.trim(), - account_type: type, - currency: "USD", - balance: cents, - }); - onDone(); - }; - - return ( -
-
e.stopPropagation()}> -
Add Account
-
-
- - setName(e.target.value)} - autoFocus - /> -
- -
- - -
- -
- - setBalance(e.target.value)} - /> -
- -
- - -
-
-
-
- ); -} diff --git a/src/features/categories/CategoriesScreen.tsx b/src/features/categories/CategoriesScreen.tsx deleted file mode 100644 index 5f39d40..0000000 --- a/src/features/categories/CategoriesScreen.tsx +++ /dev/null @@ -1,358 +0,0 @@ -import { useEffect, useState, useMemo } from "react"; -import { api } from "../../services/api"; -import type { Category, Transaction } from "../../types"; -import { - CATEGORY_ICONS, - CategoryIcon, -} from "../../components/CategoryIcons"; -import { Pencil, Trash2 } from "lucide-react"; - -export function CategoriesScreen() { - const [categories, setCategories] = useState([]); - const [transactions, setTransactions] = useState([]); - const [showAdd, setShowAdd] = useState(false); - const [editing, setEditing] = useState(null); - - const load = () => { - api.getCategories().then(setCategories); - api.getTransactions().then(setTransactions); - }; - - useEffect(() => { - load(); - }, []); - - const expenseCategories = categories.filter( - (c) => c.category_type === "expense" - ); - const incomeCategories = categories.filter( - (c) => c.category_type === "income" - ); - - const transactionCounts = useMemo(() => { - const counts: Record = {}; - transactions.forEach((t) => { - if (t.category_id) { - counts[t.category_id] = (counts[t.category_id] || 0) + 1; - } - }); - return counts; - }, [transactions]); - - const CategoryRow = ({ c }: { c: Category }) => { - const count = transactionCounts[c.id] || 0; - return ( - - - {c.icon && ( - - )} - {c.name} - - - 0 ? "var(--chelete-fg-muted)" : "var(--chelete-fg-subtle)", - }} - > - {count} {count === 1 ? "record" : "records"} - - - - - - - - ); - }; - - return ( -
-
-
Categories
- -
- - {showAdd && ( - { - setShowAdd(false); - load(); - }} - onCancel={() => setShowAdd(false)} - /> - )} - - {editing && ( - { - setEditing(null); - load(); - }} - onCancel={() => setEditing(null)} - /> - )} - -
-
-
- Expenses -
- {expenseCategories.length === 0 ? ( -
- No expense categories -
- ) : ( -
- - - - - - - - - - {expenseCategories.map((c) => ( - - ))} - -
NameRecords
-
- )} -
- -
-
- Income -
- {incomeCategories.length === 0 ? ( -
- No income categories -
- ) : ( -
- - - - - - - - - - {incomeCategories.map((c) => ( - - ))} - -
NameRecords
-
- )} -
-
-
- ); -} - -function AddCategoryForm({ - onDone, - onCancel, -}: { - onDone: () => void; - onCancel: () => void; -}) { - return ( - { - await api.createCategory(values); - onDone(); - }} - onCancel={onCancel} - /> - ); -} - -function EditCategoryForm({ - category, - onDone, - onCancel, -}: { - category: Category; - onDone: () => void; - onCancel: () => void; -}) { - return ( - { - await api.updateCategory({ - id: category.id, - name: values.name, - category_type: values.category_type, - icon: values.icon, - }); - onDone(); - }} - onCancel={onCancel} - /> - ); -} - -function CategoryForm({ - title, - initialName, - initialType, - initialIcon, - onSubmit, - onCancel, -}: { - title: string; - initialName: string; - initialType: string; - initialIcon: string; - onSubmit: (values: { name: string; category_type: string; icon: string }) => void | Promise; - onCancel: () => void; -}) { - const [name, setName] = useState(initialName); - const [type, setType] = useState(initialType); - const [selectedIcon, setSelectedIcon] = useState(initialIcon); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (!name.trim()) return; - - await onSubmit({ - name: name.trim(), - category_type: type, - icon: selectedIcon, - }); - }; - - return ( -
-
e.stopPropagation()}> -
{title}
-
-
- - setName(e.target.value)} - autoFocus - /> -
- -
- - -
- -
- -
- {CATEGORY_ICONS.map((ic) => { - const Icon = ic.icon; - return ( - - ); - })} -
-
- -
- - -
-
-
-
- ); -} diff --git a/src/features/overview/OverviewScreen.tsx b/src/features/overview/OverviewScreen.tsx deleted file mode 100644 index e9de83d..0000000 --- a/src/features/overview/OverviewScreen.tsx +++ /dev/null @@ -1,378 +0,0 @@ -import { useEffect, useState, useMemo } from "react"; -import { api } from "../../services/api"; -import { - formatBalance, - formatMoney, - formatDate, - currentMonth, -} from "../../services/format"; -import type { Overview, Transaction, Account, Category } from "../../types"; -import { - Wallet, - TrendingUp, - TrendingDown, - Activity, - CreditCard, -} from "lucide-react"; -import { - DateFilter, - getDateRange, - isDateInRange, - type DateFilterValue, -} from "../../components/DateFilter"; -import { DonutChart } from "../../components/DonutChart"; -import { Pagination, paginate } from "../../components/Pagination"; -import { CategoryIcon } from "../../components/CategoryIcons"; - -export function OverviewScreen() { - const [overview, setOverview] = useState(null); - const [accounts, setAccounts] = useState([]); - const [categories, setCategories] = useState([]); - const [allTransactions, setAllTransactions] = useState([]); - const [filterType, setFilterType] = useState<"all" | "income" | "expense">("all"); - const [filterAccount, setFilterAccount] = useState("all"); - const [dateFilter, setDateFilter] = useState("all"); - const [customStart, setCustomStart] = useState(""); - const [customEnd, setCustomEnd] = useState(""); - const [transactionPage, setTransactionPage] = useState(1); - const TRANSACTIONS_PER_PAGE = 10; - - useEffect(() => { - api.getOverview().then(setOverview); - api.getAccounts().then(setAccounts); - api.getCategories().then(setCategories); - api.getTransactions().then(setAllTransactions); - }, []); - - const categoryMap = useMemo( - () => Object.fromEntries(categories.map((c) => [c.id, c])), - [categories] - ); - - const { start, end } = getDateRange(dateFilter, customStart, customEnd); - - const filteredTransactions = useMemo(() => { - let txns = allTransactions.filter((t) => isDateInRange(t.transaction_date, start, end)); - if (filterType !== "all") { - txns = txns.filter((t) => t.transaction_type === filterType); - } - if (filterAccount !== "all") { - txns = txns.filter((t) => t.account_id === filterAccount); - } - return txns; - }, [allTransactions, start, end, filterType, filterAccount]); - - useEffect(() => { - setTransactionPage(1); - }, [filterType, filterAccount, dateFilter, customStart, customEnd]); - - const filteredOverview = useMemo(() => { - if (!overview) return null; - const dateTxns = allTransactions.filter((t) => - isDateInRange(t.transaction_date, start, end) - ); - const total_income = dateTxns - .filter((t) => t.transaction_type === "income") - .reduce((sum, t) => sum + t.amount, 0); - const total_expenses = dateTxns - .filter((t) => t.transaction_type === "expense") - .reduce((sum, t) => sum + t.amount, 0); - - const spentByCategory: Record = {}; - const earnedByCategory: Record = {}; - for (const t of dateTxns) { - if (t.category_id) { - if (t.transaction_type === "expense") { - spentByCategory[t.category_id] = - (spentByCategory[t.category_id] || 0) + t.amount; - } else if (t.transaction_type === "income") { - earnedByCategory[t.category_id] = - (earnedByCategory[t.category_id] || 0) + t.amount; - } - } - } - - const category_spending = Object.entries(spentByCategory) - .map(([category_id, spent]) => ({ - category_id, - category_name: categoryMap[category_id]?.name || "Unknown", - spent, - budget_limit: null as number | null, - icon: categoryMap[category_id]?.icon || null, - })) - .sort((a, b) => b.spent - a.spent); - - const income_by_category = Object.entries(earnedByCategory) - .map(([category_id, value]) => ({ - label: categoryMap[category_id]?.name || "Unknown", - value, - color: categoryMap[category_id]?.color || null, - })) - .sort((a, b) => b.value - a.value); - - const expenses_by_category = Object.entries(spentByCategory) - .map(([category_id, value]) => ({ - label: categoryMap[category_id]?.name || "Unknown", - value, - color: categoryMap[category_id]?.color || null, - })) - .sort((a, b) => b.value - a.value); - - return { - ...overview, - total_income, - total_expenses, - category_spending, - income_by_category, - expenses_by_category, - }; - }, [overview, allTransactions, start, end, categoryMap]); - - if (!overview || !filteredOverview) { - return ( -
- Loading... -
- ); - } - - const net = filteredOverview.total_income - filteredOverview.total_expenses; - const savingsRate = - filteredOverview.total_income > 0 - ? Math.round((net / filteredOverview.total_income) * 100) - : 0; - - return ( -
-
-
-
Overview
-
{currentMonth()}
-
-
- - - -
-
-
-
Total Balance
- -
-
- {formatBalance(overview.total_balance)} -
-
-
-
-
Income
- -
-
- {formatMoney(filteredOverview.total_income)} -
-
-
-
-
Expenses
- -
-
- {formatMoney(-filteredOverview.total_expenses)} -
-
-
-
-
Net
- -
-
= 0 ? "positive" : "negative"}`}> - {formatMoney(net)} -
-
Savings rate: {savingsRate}%
-
-
- - {accounts.length > 0 && ( - <> -
- Accounts -
-
- {accounts.map((a) => ( -
-
-
{a.name}
- -
-
{formatBalance(a.balance)}
-
- ))} -
- - )} - -
- Breakdown -
-
- - -
- -
- Recent Transactions -
-
- - -
- - {filteredTransactions.length > 0 ? ( - <> -
- - - - - - - - - - - {paginate(filteredTransactions, transactionPage, TRANSACTIONS_PER_PAGE).map((t) => { - const cat = t.category_id ? categoryMap[t.category_id] : null; - return ( - - - - - - - ); - })} - -
DateDescriptionCategoryAmount
- {formatDate(t.transaction_date)} - {t.description} - {cat ? ( - - {cat.icon && ( - - )} - {cat.name} - - ) : ( - — - )} - - {formatMoney( - t.transaction_type === "expense" ? -t.amount : t.amount - )} -
-
- - - ) : ( -
- No transactions match the selected filters. -
- )} - - {filteredTransactions.length === 0 && overview.accounts.length === 0 && ( -
-
- Welcome to Chelete -
-
- Press{" "} - - Ctrl+K - {" "} - to open the command palette, or{" "} - - a - {" "} - to add your first transaction. -
-
- )} -
- ); -} - - diff --git a/src/features/projections/ProjectionsScreen.tsx b/src/features/projections/ProjectionsScreen.tsx deleted file mode 100644 index e35894a..0000000 --- a/src/features/projections/ProjectionsScreen.tsx +++ /dev/null @@ -1,369 +0,0 @@ -import { useEffect, useMemo, useState } from "react"; -import { api } from "../../services/api"; -import { - computeProjection, - normalizeSubscriptionToPeriod, - PERIOD_PLURAL_LABELS, - PERIOD_LIMITS, - PERIOD_UNIT_LABELS, - type ProjectionPeriodType, -} from "../../services/projection"; -import { formatBalance, formatMoney, formatDateFull } from "../../services/format"; -import type { Subscription } from "../../types"; -import { LineChart, TrendingUp, Target, Calendar, Repeat } from "lucide-react"; -import { - ResponsiveContainer, - LineChart as RechartsLineChart, - Line, - XAxis, - YAxis, - CartesianGrid, - Tooltip, - ReferenceLine, -} from "recharts"; - -const PERIOD_OPTIONS: { value: ProjectionPeriodType; label: string }[] = [ - { value: "weeks", label: "Weeks" }, - { value: "months", label: "Months" }, - { value: "years", label: "Years" }, -]; - -type ExpenseSource = "manual" | "subscriptions"; - -const EXPENSE_SOURCE_OPTIONS: { value: ExpenseSource; label: string }[] = [ - { value: "manual", label: "Manual" }, - { value: "subscriptions", label: "Subscriptions" }, -]; - -export function ProjectionsScreen() { - const [startingBalance, setStartingBalance] = useState("0"); - const [income, setIncome] = useState(""); - const [expense, setExpense] = useState(""); - const [expenseSource, setExpenseSource] = useState("manual"); - const [subscriptions, setSubscriptions] = useState([]); - const [subscriptionsLoaded, setSubscriptionsLoaded] = useState(false); - const [periodType, setPeriodType] = useState("months"); - const [periods, setPeriods] = useState("10"); - - useEffect(() => { - if (expenseSource === "subscriptions" && !subscriptionsLoaded) { - api - .getSubscriptions() - .then((subs) => setSubscriptions(subs.filter((s) => s.is_active))) - .finally(() => setSubscriptionsLoaded(true)); - } - }, [expenseSource, subscriptionsLoaded]); - - const subscriptionsExpense = useMemo(() => { - if (expenseSource !== "subscriptions") return 0; - return subscriptions.reduce( - (sum, s) => sum + normalizeSubscriptionToPeriod(s.amount, s.frequency, periodType), - 0 - ); - }, [subscriptions, expenseSource, periodType]); - - const parsed = useMemo(() => { - const startingBalanceCents = parseFloatDollarsToCents(startingBalance); - const incomeCents = parseFloatDollarsToCents(income); - const manualExpenseCents = parseFloatDollarsToCents(expense); - const expenseCents = - expenseSource === "subscriptions" ? subscriptionsExpense : manualExpenseCents; - const periodCount = parseInt(periods, 10); - const valid = - startingBalanceCents !== null && - incomeCents !== null && - expenseCents !== null && - Number.isFinite(periodCount) && - periodCount >= 1 && - periodCount <= PERIOD_LIMITS[periodType]; - return { startingBalanceCents, incomeCents, expenseCents, periodCount, valid }; - }, [startingBalance, income, expense, expenseSource, subscriptionsExpense, periodType, periods]); - - const result = useMemo(() => { - if (!parsed.valid) return null; - return computeProjection({ - startingBalance: parsed.startingBalanceCents!, - income: parsed.incomeCents!, - expense: parsed.expenseCents!, - periodType, - periods: parsed.periodCount, - }); - }, [parsed, periodType]); - - const summary = result?.summary ?? null; - const growing = (summary?.netPerPeriod ?? 0) > 0; - const declining = (summary?.netPerPeriod ?? 0) < 0; - - return ( -
-
-
-
Projections
-
- Project your balance growth over time -
-
-
- -
-
-
- - setStartingBalance(e.target.value)} - /> -
-
- - setIncome(e.target.value)} - /> -
-
- - -
- {expenseSource === "manual" ? ( -
- - setExpense(e.target.value)} - /> -
- ) : ( -
-
- - - {subscriptions.length} active subscription - {subscriptions.length === 1 ? "" : "s"} - -
- {subscriptions.length > 0 && ( -
- {subscriptions.map((s) => ( -
- {s.name} - - {formatMoney( - normalizeSubscriptionToPeriod(s.amount, s.frequency, periodType) - )} - /{PERIOD_UNIT_LABELS[periodType]} - -
- ))} -
- )} -
- Total: {formatMoney(subscriptionsExpense)}/{PERIOD_UNIT_LABELS[periodType]} -
- {subscriptionsLoaded && subscriptions.length === 0 && ( -
- No active subscriptions found -
- )} -
- )} -
- - -
-
- - setPeriods(e.target.value)} - /> -
- 1 – {PERIOD_LIMITS[periodType]} {PERIOD_PLURAL_LABELS[periodType]} -
-
-
- -
- {!result || !summary ? ( -
- -
Enter your income, expense and period to see a projection.
-
- ) : ( - <> -
-
-
-
- Projected Balance -
- -
-
- {formatBalance(summary.finalBalance)} -
-
- in {parsed.periodCount} {PERIOD_PLURAL_LABELS[periodType]} - {summary.projectedDate - ? ` · ${formatDateFull(summary.projectedDate)}` - : ""} -
-
-
-
-
Total Change
- -
-
= 0 ? "positive" : "negative"}`} - > - {formatMoney(summary.totalIncrease)} -
-
from starting balance
-
-
-
-
- Net per {capitalize(PERIOD_UNIT_LABELS[periodType])} -
- -
-
- {formatMoney(summary.netPerPeriod)} -
-
- {summary.retentionRate !== null - ? `Keeping ${summary.retentionRate.toFixed(1)}% of income` - : "No income entered"} -
-
-
- -
-
Balance Over Time
-
- - - - - formatMoneyShortAxis(v)} - tick={{ fill: "var(--chelete-fg-muted)", fontSize: 11 }} - tickLine={false} - axisLine={false} - width={72} - /> - [formatBalance(Number(value)), "Balance"]} - contentStyle={{ - background: "var(--chelete-surface)", - border: "1px solid var(--chelete-border)", - borderRadius: 4, - fontSize: 12, - color: "var(--chelete-fg)", - }} - labelStyle={{ color: "var(--chelete-fg-muted)" }} - /> - {summary.finalBalance < 0 && ( - - )} - - - -
-
- - )} -
-
-
- ); -} - -function capitalize(word: string): string { - return word.charAt(0).toUpperCase() + word.slice(1); -} - -function parseFloatDollarsToCents(value: string): number | null { - const trimmed = value.trim(); - if (trimmed === "") return null; - const parsed = Number(trimmed); - if (!Number.isFinite(parsed)) return null; - return Math.round(parsed * 100); -} - -function formatMoneyShortAxis(amount: number): string { - const abs = Math.abs(amount); - const dollars = abs / 100; - const sign = amount < 0 ? "-" : ""; - if (dollars >= 1_000_000) return `${sign}$${(dollars / 1_000_000).toFixed(1)}M`; - if (dollars >= 1000) return `${sign}$${(dollars / 1000).toFixed(1)}k`; - return `${sign}$${dollars.toFixed(0)}`; -} \ No newline at end of file diff --git a/src/features/settings/SettingsScreen.tsx b/src/features/settings/SettingsScreen.tsx deleted file mode 100644 index 7a0ee17..0000000 --- a/src/features/settings/SettingsScreen.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { useTheme } from "../../hooks/useTheme"; -import { useEffect, useState } from "react"; - -const APP_VERSION = "0.6.0"; - -export function SettingsScreen() { - const theme = useTheme(); - const [rounded, setRounded] = useState(() => { - return localStorage.getItem("chelete-radius") !== "square"; - }); - - useEffect(() => { - const radius = rounded ? "4px" : "0px"; - document.documentElement.style.setProperty("--chelete-radius", radius); - localStorage.setItem("chelete-radius", rounded ? "rounded" : "square"); - }, [rounded]); - - return ( -
-
-
Settings
-
- -
-
-
- Appearance -
-
-
-
-
Theme
-
- Follows Omarchy system theme -
-
-
- {theme?.name || "Loading..."} -
-
- -
- -
-
-
Corners
-
- {rounded ? "Rounded" : "Square"} corners -
-
- -
-
-
- -
-
- Finance -
-
-
-
-
Base Currency
-
-
USD
-
-
-
-
-
Month Start
-
-
1st
-
-
-
- -
-
- About -
-
-
-
- 115 - Ch -
-
-
Chelete
-
Version {APP_VERSION}
-
-
-
- A fast, keyboard-driven personal finance tracker for Linux. - Manage accounts, track transactions, and project your balance - over time — without touching the mouse. Built for Omarchy with - live theme integration. -
-
-
-
-
Author
-
-
Victor Phos
-
-
-
-
-
License
-
-
MIT
-
-
-
- -
-
- ); -} diff --git a/src/features/subscriptions/SubscriptionsScreen.tsx b/src/features/subscriptions/SubscriptionsScreen.tsx deleted file mode 100644 index e2825c0..0000000 --- a/src/features/subscriptions/SubscriptionsScreen.tsx +++ /dev/null @@ -1,814 +0,0 @@ -import { useEffect, useState, useMemo } from "react"; -import { api } from "../../services/api"; -import { formatMoney } from "../../services/format"; -import type { Subscription, Category, Account } from "../../types"; -import { Pagination, paginate } from "../../components/Pagination"; -import { Eye, Pencil, Trash2, CreditCard } from "lucide-react"; - -const FREQUENCIES = [ - { value: "weekly", label: "Weekly" }, - { value: "bi_weekly", label: "Bi-weekly" }, - { value: "monthly", label: "Monthly" }, - { value: "quarterly", label: "Quarterly" }, - { value: "bi_yearly", label: "Bi-yearly" }, - { value: "yearly", label: "Yearly" }, -]; - -const FREQUENCY_LABELS: Record = Object.fromEntries( - FREQUENCIES.map((f) => [f.value, f.label]) -); - -function getNextPaymentDate( - startDate: string | null, - frequency: string -): string | null { - if (!startDate) return null; - - const start = new Date(startDate + "T00:00:00"); - if (isNaN(start.getTime())) return null; - - const today = new Date(); - today.setHours(0, 0, 0, 0); - - const intervals: Record = { - weekly: { days: 7 }, - bi_weekly: { days: 14 }, - monthly: { months: 1 }, - quarterly: { months: 3 }, - bi_yearly: { months: 6 }, - yearly: { months: 12 }, - }; - - const interval = intervals[frequency]; - if (!interval) return startDate; - - const next = new Date(start); - const maxIterations = 120; - let iterations = 0; - - if (interval.days) { - while (next < today && iterations < maxIterations) { - next.setDate(next.getDate() + interval.days); - iterations++; - } - } else if (interval.months) { - while (next < today && iterations < maxIterations) { - const year = next.getFullYear(); - const month = next.getMonth() + interval.months; - next.setFullYear(year, month, next.getDate()); - iterations++; - } - } - - return next.toISOString().split("T")[0]; -} - -import { - CategoryIcon, -} from "../../components/CategoryIcons"; - -export function SubscriptionsScreen() { - const [subscriptions, setSubscriptions] = useState([]); - const [categories, setCategories] = useState([]); - const [accounts, setAccounts] = useState([]); - const [showAdd, setShowAdd] = useState(false); - const [filterFrequency, setFilterFrequency] = useState("all"); - const [page, setPage] = useState(1); - const [perPage, setPerPage] = useState(25); - const [selectedIds, setSelectedIds] = useState>(new Set()); - const [confirmDelete, setConfirmDelete] = useState<{ - ids: string[]; - count: number; - } | null>(null); - const [viewing, setViewing] = useState(null); - const [editing, setEditing] = useState(null); - const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null); - const PER_PAGE_OPTIONS = [10, 20, 25, 30, 40, 60, 80, 100]; - - const load = () => { - api.getSubscriptions().then(setSubscriptions); - api.getCategories().then(setCategories); - api.getAccounts().then(setAccounts); - }; - - useEffect(() => { - load(); - }, []); - - useEffect(() => { - setPage(1); - }, [filterFrequency, perPage]); - - const categoryMap = useMemo( - () => Object.fromEntries(categories.map((c) => [c.id, c])), - [categories] - ); - - const accountMap = useMemo( - () => Object.fromEntries(accounts.map((a) => [a.id, a])), - [accounts] - ); - - const filteredSubscriptions = useMemo(() => { - if (filterFrequency === "all") return subscriptions; - return subscriptions.filter((s) => s.frequency === filterFrequency); - }, [subscriptions, filterFrequency]); - - const paginatedSubscriptions = useMemo( - () => paginate(filteredSubscriptions, page, perPage), - [filteredSubscriptions, page, perPage] - ); - - const totalAmount = useMemo(() => { - return filteredSubscriptions.reduce((sum, s) => sum + s.amount, 0); - }, [filteredSubscriptions]); - - const selectedOnPage = useMemo(() => { - if (paginatedSubscriptions.length === 0) return false; - return paginatedSubscriptions.every((s) => selectedIds.has(s.id)); - }, [paginatedSubscriptions, selectedIds]); - - const someSelectedOnPage = useMemo(() => { - return paginatedSubscriptions.some((s) => selectedIds.has(s.id)); - }, [paginatedSubscriptions, selectedIds]); - - const toggleSelection = (id: string) => { - setSelectedIds((prev) => { - const next = new Set(prev); - if (next.has(id)) { - next.delete(id); - } else { - next.add(id); - } - return next; - }); - }; - - const togglePageSelection = () => { - setSelectedIds((prev) => { - const next = new Set(prev); - if (selectedOnPage) { - paginatedSubscriptions.forEach((s) => next.delete(s.id)); - } else { - paginatedSubscriptions.forEach((s) => next.add(s.id)); - } - return next; - }); - }; - - const handleDelete = async (ids: string[]) => { - await api.deleteSubscriptions(ids); - setSelectedIds((prev) => { - const next = new Set(prev); - ids.forEach((id) => next.delete(id)); - return next; - }); - load(); - }; - - const showToast = (message: string, type: "success" | "error" = "success") => { - setToast({ message, type }); - setTimeout(() => setToast(null), 3000); - }; - - const handlePay = async (s: Subscription) => { - try { - const today = new Date().toISOString().split("T")[0]; - await api.createTransaction({ - account_id: s.account_id || accounts[0]?.id, - category_id: s.category_id || undefined, - transaction_type: "expense", - amount: s.amount, - currency: s.currency, - description: s.name, - transaction_date: today, - }); - showToast(`Payment recorded for ${s.name}`); - load(); - } catch (err) { - showToast("Failed to record payment", "error"); - } - }; - - return ( -
-
-
Subscriptions
-
- {selectedIds.size > 0 ? ( - - ) : ( - <> - - - )} -
-
- - {showAdd && ( - { - setShowAdd(false); - load(); - }} - onCancel={() => setShowAdd(false)} - /> - )} - - {editing && ( - { - setEditing(null); - load(); - }} - onCancel={() => setEditing(null)} - /> - )} - - {viewing && ( - setViewing(null)} - /> - )} - -
- -
- -
-
-
-
- Total ({filterFrequency === "all" ? "All Periods" : FREQUENCY_LABELS[filterFrequency]}) -
-
-
{formatMoney(totalAmount)}
-
-
-
-
Subscriptions
-
-
{filteredSubscriptions.length}
-
-
- - {filteredSubscriptions.length === 0 ? ( -
- No subscriptions found. -
- ) : ( - <> -
- - - - - - - - - - - - - - - - {paginatedSubscriptions.map((s) => { - const category = s.category_id ? categoryMap[s.category_id] : null; - const account = s.account_id ? accountMap[s.account_id] : null; - const nextPayment = getNextPaymentDate(s.start_date, s.frequency); - return ( - - - - - - - - - - - - ); - })} - -
- { - if (el) el.indeterminate = someSelectedOnPage && !selectedOnPage; - }} - onChange={togglePageSelection} - /> - NameCategoryAccountPeriodStart DateNext PaymentAmount
- toggleSelection(s.id)} - /> - {s.name} - {category ? ( - - {category.icon && ( - - )} - {category.name} - - ) : ( - — - )} - {account?.name || —}{FREQUENCY_LABELS[s.frequency] || s.frequency}{s.start_date || —}{nextPayment || —}{formatMoney(s.amount)} - - - - -
-
- -
-
- Show - - per page -
- -
- - )} - - {confirmDelete && ( - { - await handleDelete(confirmDelete.ids); - setConfirmDelete(null); - }} - onCancel={() => setConfirmDelete(null)} - /> - )} - - {toast && ( -
- {toast.message} -
- )} -
- ); -} - -function DeleteConfirmDialog({ - count, - onConfirm, - onCancel, -}: { - count: number; - onConfirm: () => void; - onCancel: () => void; -}) { - return ( -
-
e.stopPropagation()}> -
- Delete {count} subscription{count === 1 ? "" : "s"}? -
-

- This will permanently remove the selected subscription{count === 1 ? "" : "s"}. This action - cannot be undone. -

-
- - -
-
-
- ); -} - -function ViewSubscriptionDialog({ - subscription, - category, - account, - onClose, -}: { - subscription: Subscription; - category?: Category | null; - account?: Account | null; - onClose: () => void; -}) { - const nextPayment = getNextPaymentDate(subscription.start_date, subscription.frequency); - return ( -
-
e.stopPropagation()}> -
Subscription Details
-
- Name - {subscription.name} - - Amount - {formatMoney(subscription.amount)} - - Period - {FREQUENCY_LABELS[subscription.frequency] || subscription.frequency} - - Start Date - {subscription.start_date || "—"} - - Next Payment - {nextPayment || "—"} - - Account - {account?.name || "—"} - - Category - - {category ? ( - - {category.icon && ( - - )} - {category.name} - - ) : ( - "—" - )} - -
-
- -
-
-
- ); -} - -function AddSubscriptionForm({ - categories, - accounts, - onDone, - onCancel, -}: { - categories: Category[]; - accounts: Account[]; - onDone: () => void; - onCancel: () => void; -}) { - return ( - { - await api.createSubscription(values); - onDone(); - }} - onCancel={onCancel} - /> - ); -} - -function EditSubscriptionForm({ - categories, - accounts, - subscription, - onDone, - onCancel, -}: { - categories: Category[]; - accounts: Account[]; - subscription: Subscription; - onDone: () => void; - onCancel: () => void; -}) { - return ( - { - await api.updateSubscription({ - id: subscription.id, - name: values.name, - amount: values.amount, - currency: "USD", - frequency: values.frequency, - category_id: values.category_id || undefined, - account_id: values.account_id || undefined, - start_date: values.start_date || undefined, - }); - onDone(); - }} - onCancel={onCancel} - /> - ); -} - -function SubscriptionForm({ - title, - categories, - accounts, - initialName = "", - initialAmount = "", - initialFrequency = "monthly", - initialCategoryId = "", - initialAccountId = "", - initialStartDate = "", - onSubmit, - onCancel, -}: { - title: string; - categories: Category[]; - accounts: Account[]; - initialName?: string; - initialAmount?: string; - initialFrequency?: string; - initialCategoryId?: string; - initialAccountId?: string; - initialStartDate?: string; - onSubmit: (values: { - name: string; - amount: number; - currency: string; - frequency: string; - category_id?: string; - account_id?: string; - start_date?: string; - }) => void | Promise; - onCancel: () => void; -}) { - const [name, setName] = useState(initialName); - const [amount, setAmount] = useState(initialAmount); - const [frequency, setFrequency] = useState(initialFrequency); - const [categoryId, setCategoryId] = useState(initialCategoryId); - const [accountId, setAccountId] = useState(initialAccountId); - const [startDate, setStartDate] = useState(initialStartDate); - - const expenseCategories = categories.filter((c) => c.category_type === "expense"); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (!name.trim() || !amount) return; - - const cents = Math.round(parseFloat(amount) * 100); - await onSubmit({ - name: name.trim(), - amount: cents, - currency: "USD", - frequency, - category_id: categoryId || undefined, - account_id: accountId || undefined, - start_date: startDate || undefined, - }); - }; - - return ( -
-
e.stopPropagation()}> -
{title}
-
-
- - setName(e.target.value)} - autoFocus - /> -
- -
- - setAmount(e.target.value)} - /> -
- -
- - -
- -
- - -
- -
- - -
- -
- - setStartDate(e.target.value)} - /> -
- -
- - -
-
-
-
- ); -} diff --git a/src/features/transactions/CsvImportDialog.tsx b/src/features/transactions/CsvImportDialog.tsx deleted file mode 100644 index 4535154..0000000 --- a/src/features/transactions/CsvImportDialog.tsx +++ /dev/null @@ -1,825 +0,0 @@ -import { useEffect, useMemo, useState } from "react"; -import { api } from "../../services/api"; -import { formatMoney, formatDate } from "../../services/format"; -import type { Account, Category, CsvField, CsvMapping, CsvPreview, ImportResult, ImportOptions, ParsedRow } from "../../types"; - -const FIELD_OPTIONS: { value: CsvField; label: string }[] = [ - { value: "date", label: "Date" }, - { value: "description", label: "Description" }, - { value: "merchant", label: "Merchant" }, - { value: "amount", label: "Amount" }, - { value: "income_amount", label: "Income Amount" }, - { value: "expense_amount", label: "Expense Amount" }, - { value: "type", label: "Type (income/expense)" }, - { value: "category", label: "Category" }, - { value: "account", label: "Account" }, - { value: "currency", label: "Currency" }, - { value: "notes", label: "Notes" }, - { value: "ignore", label: "Ignore" }, -]; - -const DEFAULT_TYPE_ALIASES: Record = { - debit: "expense", - credit: "income", - dr: "expense", - cr: "income", - in: "income", - out: "expense", - expense: "expense", - income: "income", -}; - -const MAPPING_STORAGE_KEY = "chelete-csv-mapping"; - -function buildMappingKey(headers: string[]): string { - return headers.map((h) => h.trim().toLowerCase().replace(/[^a-z0-9]/g, "")).join(","); -} - -function loadSavedMapping(headers: string[]): CsvMapping | null { - try { - const raw = localStorage.getItem(MAPPING_STORAGE_KEY); - if (!raw) return null; - const data = JSON.parse(raw); - return data[buildMappingKey(headers)] ?? null; - } catch { - return null; - } -} - -function saveMapping(headers: string[], mapping: CsvMapping) { - try { - const raw = localStorage.getItem(MAPPING_STORAGE_KEY); - const data = raw ? JSON.parse(raw) : {}; - data[buildMappingKey(headers)] = mapping; - localStorage.setItem(MAPPING_STORAGE_KEY, JSON.stringify(data)); - } catch { - // ignore storage errors - } -} - -function suggestField(header: string): CsvField | undefined { - const h = header.trim().toLowerCase().replace(/[\s_-]/g, ""); - if (["date", "transactiondate", "transdate", "posted", "datetime"].includes(h)) return "date"; - if (["description", "payee", "memo", "details", "narrative"].includes(h)) return "description"; - if (h === "merchant") return "merchant"; - if (["amount", "value", "sum"].includes(h)) return "amount"; - if (["credit", "deposit", "income", "received", "inflow"].includes(h)) return "income_amount"; - if (["debit", "withdrawal", "payment", "expense", "outflow"].includes(h)) return "expense_amount"; - if (["type", "transactiontype", "kind"].includes(h)) return "type"; - if (["category", "categoryname"].includes(h)) return "category"; - if (["account", "accountname", "source"].includes(h)) return "account"; - if (["currency", "cur"].includes(h)) return "currency"; - if (["notes", "comment", "note"].includes(h)) return "notes"; - return undefined; -} - -function buildInitialMapping(headers: string[]): CsvMapping { - const columns: Record = {}; - headers.forEach((h) => { - const suggested = suggestField(h); - if (suggested) columns[h] = suggested; - }); - return { - columns, - default_currency: "USD", - type_aliases: { ...DEFAULT_TYPE_ALIASES }, - }; -} - -function fieldColumn(mapping: CsvMapping, field: CsvField): string | undefined { - return Object.entries(mapping.columns).find(([, f]) => f === field)?.[0]; -} - -function effectiveAccount(row: ParsedRow, mapping: CsvMapping): string | null { - if (row.account?.trim()) return row.account.trim().toLowerCase(); - if (mapping.default_account_id) return `__default:${mapping.default_account_id}`; - return null; -} - -function isDuplicatePreview(row: ParsedRow, existing: ParsedRow[], mapping: CsvMapping): boolean { - // Only flag duplicates when there is a meaningful account source. Otherwise rows with - // blank accounts look identical and all get marked as duplicates. - if (!row.date || row.amount_cents == null || !row.description) { - return false; - } - const accountA = effectiveAccount(row, mapping); - if (!accountA) return false; - return existing.some( - (r) => - r.row_index < row.row_index && - r.date === row.date && - r.amount_cents === row.amount_cents && - r.description?.trim().toLowerCase() === row.description?.trim().toLowerCase() && - effectiveAccount(r, mapping) === accountA - ); -} - -interface CsvImportDialogProps { - accounts: Account[]; - categories: Category[]; - onDone: () => void; - onCancel: () => void; -} - -type Stage = "mapping" | "preview" | "result"; - -export function CsvImportDialog({ accounts, categories, onDone, onCancel }: CsvImportDialogProps) { - const [stage, setStage] = useState("mapping"); - const [path, setPath] = useState(null); - const [mapping, setMapping] = useState({ - columns: {}, - default_currency: "USD", - type_aliases: { ...DEFAULT_TYPE_ALIASES }, - }); - const [preview, setPreview] = useState(null); - const [result, setResult] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const openFile = async () => { - setError(null); - try { - const selected = await api.openCsvFileDialog(); - if (!selected) return; - setPath(selected); - // Load preview with empty mapping to get headers. - const initialPreview = await api.previewCsvImport(selected, { - columns: {}, - type_aliases: {}, - }); - const saved = loadSavedMapping(initialPreview.headers); - const initial = saved ?? buildInitialMapping(initialPreview.headers); - setMapping(initial); - const preview = await api.previewCsvImport(selected, initial); - setPreview(preview); - } catch (e) { - setError(String(e)); - } - }; - - useEffect(() => { - openFile(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const updateColumn = (header: string, field: CsvField | "") => { - setMapping((prev) => { - const next: CsvMapping = { - ...prev, - columns: { ...prev.columns }, - }; - if (field === "") { - delete next.columns[header]; - } else { - // Remove any existing column mapped to the same field so each field is unique. - Object.keys(next.columns).forEach((h) => { - if (next.columns[h] === field) { - delete next.columns[h]; - } - }); - next.columns[header] = field; - } - return next; - }); - }; - - const updateDefaultAccount = (id: string) => { - setMapping((prev) => ({ ...prev, default_account_id: id || undefined })); - }; - - const updateDefaultCategory = (id: string) => { - setMapping((prev) => ({ ...prev, default_category_id: id || undefined })); - }; - - const updateCurrency = (currency: string) => { - setMapping((prev) => ({ ...prev, default_currency: currency.trim().toUpperCase() || "USD" })); - }; - - const updateDefaultType = (type: string) => { - setMapping((prev) => ({ - ...prev, - default_transaction_type: type === "" ? undefined : (type as "income" | "expense"), - })); - }; - - const updateTypeAlias = (from: string, to: string) => { - setMapping((prev) => ({ - ...prev, - type_aliases: { ...prev.type_aliases, [from.toLowerCase().trim()]: to }, - })); - }; - - const refreshPreview = async () => { - if (!path) return; - setLoading(true); - setError(null); - try { - const preview = await api.previewCsvImport(path, mapping); - setPreview(preview); - } catch (e) { - setError(String(e)); - } finally { - setLoading(false); - } - }; - - const handlePreview = async () => { - await refreshPreview(); - if (path) { - const currentPreview = await api.previewCsvImport(path, mapping); - setPreview(currentPreview); - saveMapping(currentPreview.headers, mapping); - } - setStage("preview"); - }; - - const handleImport = async (skipDuplicates: boolean) => { - if (!path) return; - setLoading(true); - setError(null); - try { - const options: ImportOptions = { - default_account_id: mapping.default_account_id, - default_category_id: mapping.default_category_id, - default_currency: mapping.default_currency, - skip_duplicates: skipDuplicates, - }; - const res = await api.importTransactions(path, mapping, options); - setResult(res); - setStage("result"); - } catch (e) { - setError(String(e)); - } finally { - setLoading(false); - } - }; - - const validForPreview = useMemo(() => { - const hasDate = fieldColumn(mapping, "date"); - const hasDescription = fieldColumn(mapping, "description"); - const hasAmount = - fieldColumn(mapping, "amount") || - fieldColumn(mapping, "income_amount") || - fieldColumn(mapping, "expense_amount"); - const hasType = - fieldColumn(mapping, "type") || - fieldColumn(mapping, "income_amount") || - fieldColumn(mapping, "expense_amount") || - mapping.default_transaction_type; - return Boolean(hasDate && hasDescription && hasAmount && hasType); - }, [mapping]); - - return ( -
-
e.stopPropagation()} - style={{ minWidth: 560, maxWidth: 760, width: "80vw" }} - > -
Import Transactions from CSV
- - {error && ( -
- {error} -
- )} - - {stage === "mapping" && ( - - )} - - {stage === "preview" && preview && ( - setStage("mapping")} - onImport={(skip) => handleImport(skip)} - onCancel={onCancel} - loading={loading} - /> - )} - - {stage === "result" && result && ( - setStage("preview")} /> - )} -
-
- ); -} - -function MappingStage({ - path, - preview, - mapping, - accounts, - categories, - validForPreview, - onOpenFile, - onColumnChange, - onDefaultAccountChange, - onDefaultCategoryChange, - onCurrencyChange, - onDefaultTypeChange, - onTypeAliasChange, - onPreview, - onCancel, - loading, -}: { - path: string | null; - preview: CsvPreview | null; - mapping: CsvMapping; - accounts: Account[]; - categories: Category[]; - validForPreview: boolean; - onOpenFile: () => void; - onColumnChange: (header: string, field: CsvField | "") => void; - onDefaultAccountChange: (id: string) => void; - onDefaultCategoryChange: (id: string) => void; - onCurrencyChange: (currency: string) => void; - onDefaultTypeChange: (type: string) => void; - onTypeAliasChange: (from: string, to: string) => void; - onPreview: () => void; - onCancel: () => void; - loading: boolean; -}) { - const hasTypeColumn = fieldColumn(mapping, "type") !== undefined; - const hasSeparateAmounts = - fieldColumn(mapping, "income_amount") !== undefined || - fieldColumn(mapping, "expense_amount") !== undefined; - - return ( - <> -
- {!path ? ( - - ) : ( -
- - {path} - - -
- )} -
- - {preview && ( - <> -
- - - - - - - - - {preview.headers.map((header) => ( - - - - - ))} - -
CSV ColumnChelete Field
{header} - -
-
- -
-
- - -
- -
- - -
- -
- - onCurrencyChange(e.target.value)} - maxLength={3} - /> -
- -
- - -
- - {hasTypeColumn && ( -
- -
- - -
-
- )} -
- - {!validForPreview && ( -
- Map at least Date, Description, and an Amount + Type field to continue. - {hasSeparateAmounts ? ( - <> - {" "} - Separate Income and Expense amount columns also satisfy the type requirement. - - ) : null} -
- )} - - )} - -
- - -
- - ); -} - -function AliasRow({ - label, - to, - mapping, - onChange, -}: { - label: string; - to: string; - mapping: CsvMapping; - onChange: (from: string, to: string) => void; -}) { - const from = label.toLowerCase(); - // `to` is the intended direction shown in the select default; read it as a fallback. - const value = mapping.type_aliases[from] || to; - return ( -
- {label} → - -
- ); -} - -function PreviewStage({ - preview, - mapping, - onBack, - onImport, - onCancel, - loading, -}: { - preview: CsvPreview; - mapping: CsvMapping; - onBack: () => void; - onImport: (skipDuplicates: boolean) => void; - onCancel: () => void; - loading: boolean; -}) { - const [skipDuplicates, setSkipDuplicates] = useState(true); - const previewWithDupes = useMemo(() => { - return preview.rows.map((row) => ({ - ...row, - duplicate: isDuplicatePreview(row, preview.rows, mapping), - })); - }, [preview.rows, mapping]); - - const validCount = previewWithDupes.filter((r) => r.errors.length === 0 && !r.duplicate).length; - - return ( - <> -
- - {preview.rows.length} preview rows (of {preview.total_rows} total) - - - {validCount} ready to import - -
- -
- - - - - - - - - - - - - - {previewWithDupes.map((row) => { - const typeField = fieldColumn(mapping, "type"); - const typeLabel = - row.transaction_type || - (row.amount_source === "income_column" ? "income" : undefined) || - (row.amount_source === "expense_column" ? "expense" : undefined) || - (typeField ? "—" : "—"); - const accountName = row.account || - (mapping.default_account_id ? "(default)" : "—"); - const categoryName = row.category || - (mapping.default_category_id ? "(default)" : "—"); - const hasError = row.errors.length > 0; - const isDup = row.duplicate; - - return ( - - - - - - - - - - ); - })} - -
DateDescriptionTypeAmountAccountCategoryStatus
{row.date ? formatDate(row.date) : "—"}{row.description || "—"}{typeLabel} - {row.amount_cents !== undefined ? formatMoney(row.amount_cents) : "—"} - {accountName}{categoryName} - {hasError ? ( - - {row.errors.join("; ")} - - ) : isDup ? ( - Duplicate - ) : ( - OK - )} -
-
- -
- setSkipDuplicates(e.target.checked)} - /> - -
- -
- - - -
- - ); -} - -function ResultStage({ - result, - onDone, - onBack, -}: { - result: ImportResult; - onDone: () => void; - onBack: () => void; -}) { - return ( - <> -
-
-
- {result.imported} -
-
- Imported -
-
-
-
- {result.skipped} -
-
- Skipped -
-
-
-
0 ? "var(--chelete-danger)" : "var(--chelete-fg-muted)", - }} - > - {result.errors.length} -
-
- Errors -
-
-
- - {result.errors.length > 0 && ( -
- {result.errors.map((err, i) => ( -
- {err} -
- ))} -
- )} - -
- - -
- - ); -} diff --git a/src/features/transactions/TransactionsScreen.tsx b/src/features/transactions/TransactionsScreen.tsx deleted file mode 100644 index c2d90ac..0000000 --- a/src/features/transactions/TransactionsScreen.tsx +++ /dev/null @@ -1,936 +0,0 @@ -import { useEffect, useState, useMemo } from "react"; -import { api } from "../../services/api"; -import { formatMoney, formatDate } from "../../services/format"; -import type { Transaction, Account, Category, ExportData } from "../../types"; -import { CsvImportDialog } from "./CsvImportDialog"; -import { - DateFilter, - getDateRange, - isDateInRange, - type DateFilterValue, -} from "../../components/DateFilter"; -import { Pagination, paginate } from "../../components/Pagination"; -import { - ShoppingCart, - Car, - Briefcase, - Film, - ShoppingBag, - Zap, - Heart, - BookOpen, - Coffee, - Plane, - Home, - Utensils, - Wifi, - Smartphone, - Music, - Gamepad2, - Gift, - Stethoscope, - GraduationCap, - Building2, - Trash2, - Pencil, - Eye, - Upload, - Download, - type LucideIcon, -} from "lucide-react"; - -const CATEGORY_ICONS: { name: string; icon: LucideIcon }[] = [ - { name: "utensils", icon: Utensils }, - { name: "car", icon: Car }, - { name: "briefcase", icon: Briefcase }, - { name: "film", icon: Film }, - { name: "shopping-bag", icon: ShoppingBag }, - { name: "zap", icon: Zap }, - { name: "heart", icon: Heart }, - { name: "book-open", icon: BookOpen }, - { name: "coffee", icon: Coffee }, - { name: "plane", icon: Plane }, - { name: "home", icon: Home }, - { name: "shopping-cart", icon: ShoppingCart }, - { name: "wifi", icon: Wifi }, - { name: "smartphone", icon: Smartphone }, - { name: "music", icon: Music }, - { name: "gamepad-2", icon: Gamepad2 }, - { name: "gift", icon: Gift }, - { name: "stethoscope", icon: Stethoscope }, - { name: "graduation-cap", icon: GraduationCap }, - { name: "building-2", icon: Building2 }, -]; - -function CategoryIcon({ - name, - size = 14, - style, -}: { - name: string; - size?: number; - style?: React.CSSProperties; -}) { - const iconEntry = CATEGORY_ICONS.find((i) => i.name === name); - if (iconEntry) { - const Icon = iconEntry.icon; - return ; - } - return null; -} - -export function TransactionsScreen() { - const [transactions, setTransactions] = useState([]); - const [accounts, setAccounts] = useState([]); - const [categories, setCategories] = useState([]); - const [showAdd, setShowAdd] = useState(false); - const [showImport, setShowImport] = useState(false); - const [dateFilter, setDateFilter] = useState("all"); - const [customStart, setCustomStart] = useState(""); - const [customEnd, setCustomEnd] = useState(""); - const [transactionPage, setTransactionPage] = useState(1); - const [transactionsPerPage, setTransactionsPerPage] = useState(25); - const [selectedIds, setSelectedIds] = useState>(new Set()); - const [exportData, setExportData] = useState(null); - const [confirmDelete, setConfirmDelete] = useState<{ - ids: string[]; - count: number; - } | null>(null); - const [editingTransaction, setEditingTransaction] = useState(null); - const [viewingTransaction, setViewingTransaction] = useState(null); - const [showExport, setShowExport] = useState(false); - const PER_PAGE_OPTIONS = [10, 20, 25, 30, 40, 60, 80, 100]; - - const load = () => { - api.getTransactions().then(setTransactions); - api.getAccounts().then(setAccounts); - api.getCategories().then(setCategories); - api.exportData().then(setExportData); - }; - - useEffect(() => { - load(); - }, []); - - useEffect(() => { - const onOpenImport = () => setShowImport(true); - window.addEventListener("chelete-open-import", onOpenImport); - return () => window.removeEventListener("chelete-open-import", onOpenImport); - }, []); - - const accountMap = Object.fromEntries(accounts.map((a) => [a.id, a])); - const categoryMap = Object.fromEntries(categories.map((c) => [c.id, c])); - - const { start, end } = getDateRange(dateFilter, customStart, customEnd); - - const filteredTransactions = useMemo(() => { - return transactions.filter((t) => isDateInRange(t.transaction_date, start, end)); - }, [transactions, start, end]); - - useEffect(() => { - setTransactionPage(1); - }, [dateFilter, customStart, customEnd, transactionsPerPage]); - - const paginatedTransactions = useMemo( - () => paginate(filteredTransactions, transactionPage, transactionsPerPage), - [filteredTransactions, transactionPage, transactionsPerPage] - ); - - const selectedOnPage = useMemo(() => { - if (paginatedTransactions.length === 0) return false; - return paginatedTransactions.every((t) => selectedIds.has(t.id)); - }, [paginatedTransactions, selectedIds]); - - const someSelectedOnPage = useMemo(() => { - return paginatedTransactions.some((t) => selectedIds.has(t.id)); - }, [paginatedTransactions, selectedIds]); - - const toggleSelection = (id: string) => { - setSelectedIds((prev) => { - const next = new Set(prev); - if (next.has(id)) { - next.delete(id); - } else { - next.add(id); - } - return next; - }); - }; - - const togglePageSelection = () => { - setSelectedIds((prev) => { - const next = new Set(prev); - if (selectedOnPage) { - paginatedTransactions.forEach((t) => next.delete(t.id)); - } else { - paginatedTransactions.forEach((t) => next.add(t.id)); - } - return next; - }); - }; - - const handleDelete = async (ids: string[]) => { - await api.deleteTransactions(ids); - setSelectedIds((prev) => { - const next = new Set(prev); - ids.forEach((id) => next.delete(id)); - return next; - }); - load(); - }; - - return ( -
-
-
Transactions
-
- {selectedIds.size > 0 ? ( - - ) : ( - <> - - - - - )} -
-
- - - - {showAdd && ( - { - setShowAdd(false); - load(); - }} - onCancel={() => setShowAdd(false)} - /> - )} - - {editingTransaction && ( - { - setEditingTransaction(null); - load(); - }} - onCancel={() => setEditingTransaction(null)} - /> - )} - - {viewingTransaction && ( - setViewingTransaction(null)} - /> - )} - - {showExport && exportData && ( - setShowExport(false)} - onCancel={() => setShowExport(false)} - /> - )} - - {showImport && ( - { - setShowImport(false); - load(); - }} - onCancel={() => setShowImport(false)} - /> - )} - - {filteredTransactions.length === 0 ? ( -
- {transactions.length === 0 - ? "No transactions yet. Press " - : "No transactions match the selected filters."} - {transactions.length === 0 && ( - <> - - a - {" "} - to add one. - - )} -
- ) : ( - <> -
- - - - - - - - - - - - - - - {paginatedTransactions.map((t) => { - const cat = t.category_id ? categoryMap[t.category_id] : null; - return ( - - - - - - - - - - - ); - })} - -
- { - if (el) el.indeterminate = someSelectedOnPage && !selectedOnPage; - }} - onChange={togglePageSelection} - /> - DateDescriptionTypeCategoryAccountAmount
- toggleSelection(t.id)} - /> - - {formatDate(t.transaction_date)} - {t.description} - - {t.transaction_type} - - - {cat ? ( - - {cat.icon && ( - - )} - {cat.name} - - ) : ( - - — - - )} - {accountMap[t.account_id]?.name || "—"} - {formatMoney( - t.transaction_type === "expense" ? -t.amount : t.amount - )} - - - - -
-
- -
-
- Show - - per page -
- -
- - )} - - {confirmDelete && ( - { - await handleDelete(confirmDelete.ids); - setConfirmDelete(null); - }} - onCancel={() => setConfirmDelete(null)} - /> - )} -
- ); -} - -function ExportDialog({ - onDone, - onCancel, - data, -}: { - onDone: () => void; - onCancel: () => void; - data: ExportData; -}) { - const [format, setFormat] = useState<"json" | "csv">("csv"); - - const handleExport = async () => { - if (format === "json") { - const contents = JSON.stringify(data, null, 2); - const path = await api.saveFileDialog("chelete-export.json", "json"); - if (path) await api.writeExportFile(path, contents); - } else { - const contents = transactionsToCsv(data.transactions, data.accounts, data.categories); - const path = await api.saveFileDialog("chelete-transactions.csv", "csv"); - if (path) await api.writeExportFile(path, contents); - } - onDone(); - }; - - return ( -
-
e.stopPropagation()}> -
Export Data
-
{ - e.preventDefault(); - handleExport(); - }} - > -
- -
- - -
-
- -
- - -
-
-
-
- ); -} - -function escapeCsvField(value: unknown): string { - const str = value == null ? "" : String(value); - if (/[",\n\r]/.test(str)) { - return `"${str.replace(/"/g, '""')}"`; - } - return str; -} - -function transactionsToCsv( - transactions: Transaction[], - accounts: Account[], - categories: Category[] -): string { - const accountMap = Object.fromEntries(accounts.map((a) => [a.id, a.name])); - const categoryMap = Object.fromEntries(categories.map((c) => [c.id, c.name])); - - const headers = [ - "id", - "transaction_date", - "description", - "merchant", - "transaction_type", - "amount", - "currency", - "account_name", - "category_name", - "notes", - "created_at", - "updated_at", - ]; - const rows = transactions.map((t) => [ - t.id, - t.transaction_date, - t.description, - t.merchant ?? "", - t.transaction_type, - (t.amount / 100).toFixed(2), - t.currency, - accountMap[t.account_id] ?? t.account_id, - t.category_id ? categoryMap[t.category_id] ?? t.category_id : "", - t.notes ?? "", - t.created_at, - t.updated_at, - ]); - return [headers, ...rows].map((row) => row.map(escapeCsvField).join(",")).join("\n"); -} - -function ViewTransactionDialog({ - transaction, - account, - category, - onClose, -}: { - transaction: Transaction; - account?: Account; - category?: Category | null; - onClose: () => void; -}) { - return ( -
-
e.stopPropagation()}> -
Transaction Details
-
- Date - {formatDate(transaction.transaction_date)} - - Description - {transaction.description} - - {transaction.merchant && ( - <> - Merchant - {transaction.merchant} - - )} - - Type - - - {transaction.transaction_type} - - - - Amount - - {formatMoney( - transaction.transaction_type === "expense" ? -transaction.amount : transaction.amount - )} - - - Account - {account?.name || "—"} - - Category - - {category ? ( - - {category.icon && ( - - )} - {category.name} - - ) : ( - "—" - )} - - - {transaction.notes && ( - <> - Notes - {transaction.notes} - - )} -
-
- -
-
-
- ); -} - -function DeleteConfirmDialog({ - count, - onConfirm, - onCancel, -}: { - count: number; - onConfirm: () => void; - onCancel: () => void; -}) { - return ( -
-
e.stopPropagation()}> -
Delete {count} transaction{count === 1 ? "" : "s"}?
-

- This will permanently remove the selected transaction{count === 1 ? "" : "s"} and reverse - any account balance changes. This action cannot be undone. -

-
- - -
-
-
- ); -} - -function AddTransactionForm({ - accounts, - categories, - onDone, - onCancel, -}: { - accounts: Account[]; - categories: Category[]; - onDone: () => void; - onCancel: () => void; -}) { - return ( - { - await api.createTransaction(values); - onDone(); - }} - onCancel={onCancel} - /> - ); -} - -function EditTransactionForm({ - accounts, - categories, - transaction, - onDone, - onCancel, -}: { - accounts: Account[]; - categories: Category[]; - transaction: Transaction; - onDone: () => void; - onCancel: () => void; -}) { - return ( - { - await api.updateTransaction({ - id: transaction.id, - account_id: values.account_id, - category_id: values.category_id || undefined, - transaction_type: values.transaction_type, - amount: values.amount, - currency: "USD", - description: values.description, - transaction_date: values.transaction_date, - }); - onDone(); - }} - onCancel={onCancel} - /> - ); -} - -function TransactionForm({ - title, - accounts, - categories, - initialType = "expense", - initialAmount = "", - initialDescription = "", - initialAccountId, - initialCategoryId = "", - initialDate = new Date().toISOString().split("T")[0], - onSubmit, - onCancel, -}: { - title: string; - accounts: Account[]; - categories: Category[]; - initialType?: string; - initialAmount?: string; - initialDescription?: string; - initialAccountId?: string; - initialCategoryId?: string; - initialDate?: string; - onSubmit: (values: { - account_id: string; - category_id?: string; - transaction_type: string; - amount: number; - currency: string; - description: string; - transaction_date: string; - }) => void | Promise; - onCancel: () => void; -}) { - const [type, setType] = useState(initialType); - const [amount, setAmount] = useState(initialAmount); - const [description, setDescription] = useState(initialDescription); - const [accountId, setAccountId] = useState(initialAccountId || accounts[0]?.id || ""); - const [categoryId, setCategoryId] = useState(initialCategoryId); - const [date, setDate] = useState(initialDate); - - const expenseCategories = categories.filter( - (c) => c.category_type === "expense" - ); - const incomeCategories = categories.filter( - (c) => c.category_type === "income" - ); - const filteredCategories = - type === "expense" ? expenseCategories : incomeCategories; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - const cents = Math.round(parseFloat(amount) * 100); - if (isNaN(cents) || cents <= 0) return; - - await onSubmit({ - account_id: accountId, - category_id: categoryId || undefined, - transaction_type: type, - amount: cents, - currency: "USD", - description, - transaction_date: date, - }); - }; - - return ( -
-
e.stopPropagation()}> -
{title}
-
-
- - -
- -
- - setAmount(e.target.value)} - autoFocus - /> -
- -
- - setDescription(e.target.value)} - /> -
- -
-
- - -
- -
- - -
-
- -
- - setDate(e.target.value)} - /> -
- -
- - -
-
-
-
- ); -} diff --git a/src/hooks/useTheme.ts b/src/hooks/useTheme.ts deleted file mode 100644 index d152fd8..0000000 --- a/src/hooks/useTheme.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { useEffect, useState } from "react"; -import { api } from "../services/api"; -import type { OmarchyTheme } from "../types"; - -export function useTheme() { - const [theme, setTheme] = useState(null); - - useEffect(() => { - let lastThemeJson = ""; - - const poll = () => { - api.getOmarchyTheme().then((t) => { - const json = JSON.stringify(t); - if (json !== lastThemeJson) { - lastThemeJson = json; - setTheme(t); - applyTheme(t); - } - }); - }; - - poll(); - const interval = setInterval(poll, 2000); - return () => clearInterval(interval); - }, []); - - return theme; -} - -function applyTheme(theme: OmarchyTheme) { - const root = document.documentElement; - const c = theme.colors; - - root.style.setProperty("--chelete-bg", c.background || "#1a1b26"); - root.style.setProperty("--chelete-bg-secondary", darken(c.background, 0.1)); - root.style.setProperty("--chelete-surface", c.surface || darken(c.background, -0.08)); - root.style.setProperty("--chelete-surface-hover", c.surface_hover || darken(c.background, -0.15)); - root.style.setProperty("--chelete-fg", theme.foreground || "#c0caf5"); - root.style.setProperty("--chelete-fg-muted", c.foreground_muted || "#737aa2"); - root.style.setProperty("--chelete-fg-subtle", c.foreground_subtle || "#565f89"); - root.style.setProperty("--chelete-accent", theme.accent || "#7aa2f7"); - root.style.setProperty("--chelete-success", c.success || c.green || "#9ece6a"); - root.style.setProperty("--chelete-danger", c.danger || c.red || "#f7768e"); - root.style.setProperty("--chelete-warning", c.warning || c.yellow || "#e0af68"); - root.style.setProperty("--chelete-info", c.info || c.cyan || "#7dcfff"); - - const border = c.border || (isLight(c.background) ? darken(c.background, -0.15) : darken(c.background, 0.2)); - root.style.setProperty("--chelete-border", border); -} - -function darken(hex: string, amount: number): string { - const h = hex.replace("#", ""); - const r = parseInt(h.substring(0, 2), 16); - const g = parseInt(h.substring(2, 4), 16); - const b = parseInt(h.substring(4, 6), 16); - - const factor = amount > 0 ? 1 + amount : 1 + amount; - const nr = Math.min(255, Math.max(0, Math.round(r * factor))); - const ng = Math.min(255, Math.max(0, Math.round(g * factor))); - const nb = Math.min(255, Math.max(0, Math.round(b * factor))); - - return `#${nr.toString(16).padStart(2, "0")}${ng.toString(16).padStart(2, "0")}${nb.toString(16).padStart(2, "0")}`; -} - -function isLight(hex: string): boolean { - const h = hex.replace("#", ""); - const r = parseInt(h.substring(0, 2), 16); - const g = parseInt(h.substring(2, 4), 16); - const b = parseInt(h.substring(4, 6), 16); - return (r * 299 + g * 587 + b * 114) / 1000 > 128; -} diff --git a/src/main.tsx b/src/main.tsx deleted file mode 100644 index 9609ffe..0000000 --- a/src/main.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from "react"; -import ReactDOM from "react-dom/client"; -import App from "./App"; -import "./styles/global.css"; - -ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - - - -); diff --git a/src/services/__tests__/api.test.ts b/src/services/__tests__/api.test.ts deleted file mode 100644 index 2497f35..0000000 --- a/src/services/__tests__/api.test.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { api } from "../api"; - -vi.mock("@tauri-apps/api/core", () => ({ - invoke: vi.fn(), -})); - -import { invoke } from "@tauri-apps/api/core"; -const mockInvoke = vi.mocked(invoke); - -beforeEach(() => { - vi.clearAllMocks(); -}); - -describe("api.getAccounts", () => { - it("calls invoke with correct command", async () => { - const mockAccounts = [ - { id: "1", name: "Checking", balance: 500000 }, - { id: "2", name: "Savings", balance: 1500000 }, - ]; - mockInvoke.mockResolvedValue(mockAccounts); - - const result = await api.getAccounts(); - - expect(mockInvoke).toHaveBeenCalledWith("get_accounts"); - expect(result).toEqual(mockAccounts); - }); -}); - -describe("api.createAccount", () => { - it("calls invoke with correct command and request", async () => { - const request = { - name: "New Account", - account_type: "bank", - currency: "USD", - balance: 100000, - }; - const mockAccount = { id: "new-id", ...request, is_active: true }; - mockInvoke.mockResolvedValue(mockAccount); - - const result = await api.createAccount(request); - - expect(mockInvoke).toHaveBeenCalledWith("create_account", { request }); - expect(result).toEqual(mockAccount); - }); -}); - -describe("api.deleteAccount", () => { - it("calls invoke with correct command and id", async () => { - mockInvoke.mockResolvedValue(undefined); - - await api.deleteAccount("account-123"); - - expect(mockInvoke).toHaveBeenCalledWith("delete_account", { - id: "account-123", - }); - }); -}); - -describe("api.getTransactions", () => { - it("calls invoke with correct command", async () => { - mockInvoke.mockResolvedValue([]); - - const result = await api.getTransactions(); - - expect(mockInvoke).toHaveBeenCalledWith("get_transactions"); - expect(result).toEqual([]); - }); -}); - -describe("api.createTransaction", () => { - it("calls invoke with correct command and request", async () => { - const request = { - account_id: "acc-1", - transaction_type: "expense", - amount: 5000, - currency: "USD", - description: "Groceries", - transaction_date: "2026-03-15", - }; - const mockTxn = { id: "txn-1", ...request }; - mockInvoke.mockResolvedValue(mockTxn); - - const result = await api.createTransaction(request); - - expect(mockInvoke).toHaveBeenCalledWith("create_transaction", { request }); - expect(result).toEqual(mockTxn); - }); -}); - -describe("api.getCategories", () => { - it("calls invoke with correct command", async () => { - const mockCategories = [{ id: "cat-1", name: "Food" }]; - mockInvoke.mockResolvedValue(mockCategories); - - const result = await api.getCategories(); - - expect(mockInvoke).toHaveBeenCalledWith("get_categories"); - expect(result).toEqual(mockCategories); - }); -}); - -describe("api.createCategory", () => { - it("calls invoke with correct command and request", async () => { - const request = { - name: "New Category", - category_type: "expense", - icon: "star", - color: "#ff0000", - }; - const mockCategory = { id: "cat-new", ...request }; - mockInvoke.mockResolvedValue(mockCategory); - - const result = await api.createCategory(request); - - expect(mockInvoke).toHaveBeenCalledWith("create_category", { request }); - expect(result).toEqual(mockCategory); - }); -}); - -describe("api.getOverview", () => { - it("calls invoke with correct command", async () => { - const mockOverview = { - total_balance: 2000000, - total_income: 450000, - total_expenses: 120000, - accounts: [], - recent_transactions: [], - category_spending: [], - }; - mockInvoke.mockResolvedValue(mockOverview); - - const result = await api.getOverview(); - - expect(mockInvoke).toHaveBeenCalledWith("get_overview"); - expect(result).toEqual(mockOverview); - }); -}); - -describe("api.getOmarchyTheme", () => { - it("calls invoke with correct command", async () => { - const mockTheme = { - name: "default", - background: "#1a1b26", - foreground: "#c0caf5", - accent: "#7aa2f7", - colors: {}, - }; - mockInvoke.mockResolvedValue(mockTheme); - - const result = await api.getOmarchyTheme(); - - expect(mockInvoke).toHaveBeenCalledWith("get_omarchy_theme"); - expect(result).toEqual(mockTheme); - }); -}); diff --git a/src/services/__tests__/format.test.ts b/src/services/__tests__/format.test.ts deleted file mode 100644 index c0385e6..0000000 --- a/src/services/__tests__/format.test.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { - formatMoney, - formatMoneyShort, - formatBalance, - formatDate, - formatDateFull, - todayISO, - currentMonth, - accountTypeLabel, -} from "../format"; - -describe("formatMoney", () => { - it("formats positive amount with + sign", () => { - expect(formatMoney(1000)).toBe("+$10.00"); - expect(formatMoney(525000)).toBe("+$5,250.00"); - }); - - it("formats negative amount with - sign", () => { - expect(formatMoney(-1000)).toBe("-$10.00"); - expect(formatMoney(-120000)).toBe("-$1,200.00"); - }); - - it("formats zero as positive", () => { - expect(formatMoney(0)).toBe("+$0.00"); - }); - - it("handles small amounts", () => { - expect(formatMoney(50)).toBe("+$0.50"); - expect(formatMoney(-25)).toBe("-$0.25"); - }); -}); - -describe("formatMoneyShort", () => { - it("formats amounts under 1000 normally", () => { - expect(formatMoneyShort(500)).toBe("$5.00"); - expect(formatMoneyShort(12345)).toBe("$123.45"); - }); - - it("formats amounts >= 1000 with k suffix", () => { - expect(formatMoneyShort(100000)).toBe("$1.0k"); - expect(formatMoneyShort(1500000)).toBe("$15.0k"); - expect(formatMoneyShort(5250000)).toBe("$52.5k"); - }); -}); - -describe("formatBalance", () => { - it("formats positive balance without sign", () => { - expect(formatBalance(525000)).toBe("$5,250.00"); - }); - - it("formats negative balance with minus", () => { - expect(formatBalance(-120000)).toBe("$-1,200.00"); - }); - - it("formats zero balance", () => { - expect(formatBalance(0)).toBe("$0.00"); - }); -}); - -describe("formatDate", () => { - it("formats date string to short format", () => { - const result = formatDate("2026-03-15"); - expect(result).toMatch(/Mar/); - expect(result).toContain("15"); - }); -}); - -describe("formatDateFull", () => { - it("formats date string to full format", () => { - const result = formatDateFull("2026-03-15"); - expect(result).toMatch(/Mar/); - expect(result).toContain("15"); - expect(result).toContain("2026"); - }); -}); - -describe("todayISO", () => { - it("returns today in YYYY-MM-DD format", () => { - const result = todayISO(); - expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/); - const today = new Date().toISOString().split("T")[0]; - expect(result).toBe(today); - }); -}); - -describe("currentMonth", () => { - it("returns current month and year", () => { - const result = currentMonth(); - const now = new Date(); - const monthName = now.toLocaleDateString("en-US", { month: "long" }); - const year = now.getFullYear().toString(); - expect(result).toContain(monthName); - expect(result).toContain(year); - }); -}); - -describe("accountTypeLabel", () => { - it("returns correct labels for known types", () => { - expect(accountTypeLabel("cash")).toBe("Cash"); - expect(accountTypeLabel("bank")).toBe("Bank"); - expect(accountTypeLabel("savings")).toBe("Savings"); - expect(accountTypeLabel("credit_card")).toBe("Credit Card"); - expect(accountTypeLabel("mobile_money")).toBe("Mobile Money"); - expect(accountTypeLabel("investment")).toBe("Investment"); - expect(accountTypeLabel("other")).toBe("Other"); - }); - - it("returns the type string for unknown types", () => { - expect(accountTypeLabel("crypto")).toBe("crypto"); - expect(accountTypeLabel("custom")).toBe("custom"); - }); -}); diff --git a/src/services/__tests__/projection.test.ts b/src/services/__tests__/projection.test.ts deleted file mode 100644 index d95a482..0000000 --- a/src/services/__tests__/projection.test.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { - computeProjection, - periodLabel, - normalizeSubscriptionToPeriod, - PERIOD_LIMITS, - type ProjectionInput, -} from "../projection"; - -const baseInput = (overrides: Partial = {}): ProjectionInput => ({ - startingBalance: 0, - income: 0, - expense: 0, - periodType: "months", - periods: 10, - ...overrides, -}); - -describe("computeProjection", () => { - it("matches the canonical example: 1200 income, 100 expense, 10 months -> 11000", () => { - const result = computeProjection( - baseInput({ income: 120000, expense: 10000, periodType: "months", periods: 10 }) - ); - - expect(result.summary.netPerPeriod).toBe(110000); - expect(result.summary.finalBalance).toBe(1100000); - expect(result.summary.totalIncrease).toBe(1100000); - expect(result.points).toHaveLength(11); - expect(result.points[0].label).toBe("Now"); - expect(result.points[0].balance).toBe(0); - expect(result.points[10].label).toBe("10 months"); - expect(result.points[10].balance).toBe(1100000); - }); - - it("balances increase linearly by net per period", () => { - const result = computeProjection( - baseInput({ income: 300000, expense: 120000, periodType: "weeks", periods: 4 }) - ); - - expect(result.points.map((p) => p.balance)).toEqual([0, 180000, 360000, 540000, 720000]); - expect(result.points.map((p) => p.label)).toEqual([ - "Now", - "1 week", - "2 weeks", - "3 weeks", - "4 weeks", - ]); - }); - - it("offsets by starting balance", () => { - const result = computeProjection( - baseInput({ - startingBalance: 500000, - income: 100000, - expense: 0, - periodType: "months", - periods: 3, - }) - ); - - expect(result.summary.finalBalance).toBe(800000); - expect(result.summary.totalIncrease).toBe(300000); - expect(result.points[0].balance).toBe(500000); - expect(result.points[3].balance).toBe(800000); - }); - - it("produces a flat line when income equals expense", () => { - const result = computeProjection( - baseInput({ income: 50000, expense: 50000, periods: 5 }) - ); - - expect(result.summary.netPerPeriod).toBe(0); - expect(result.points.every((p) => p.balance === 0)).toBe(true); - }); - - it("declines when expenses exceed income", () => { - const result = computeProjection( - baseInput({ income: 20000, expense: 50000, periods: 4 }) - ); - - expect(result.summary.netPerPeriod).toBe(-30000); - expect(result.summary.finalBalance).toBe(-120000); - expect(result.points.map((p) => p.balance)).toEqual([0, -30000, -60000, -90000, -120000]); - }); - - it("computes retention rate as net over income", () => { - const result = computeProjection(baseInput({ income: 120000, expense: 10000 })); - - expect(result.summary.retentionRate).toBeCloseTo(91.6667, 3); - }); - - it("returns null retention rate when income is zero", () => { - const result = computeProjection(baseInput({ income: 0, expense: 10000 })); - - expect(result.summary.retentionRate).toBeNull(); - }); - - it("supports years period type", () => { - const result = computeProjection( - baseInput({ income: 1200000, expense: 0, periodType: "years", periods: 2 }) - ); - - expect(result.points.map((p) => p.label)).toEqual(["Now", "1 year", "2 years"]); - expect(result.summary.finalBalance).toBe(2400000); - }); - - it("respects period limits", () => { - expect(PERIOD_LIMITS.months).toBe(600); - - const result = computeProjection(baseInput({ periodType: "months", periods: 600 })); - expect(result.maxPeriods).toBe(600); - expect(result.points).toHaveLength(601); - }); - - it("sets projected date roughly periods ahead", () => { - const result = computeProjection(baseInput({ periodType: "months", periods: 10 })); - - const expected = new Date(); - expected.setDate(expected.getDate() + 300); - const expectedStr = expected.toISOString().split("T")[0]; - expect(result.summary.projectedDate).toBe(expectedStr); - }); -}); - -describe("periodLabel", () => { - it("labels step 0 as Now", () => { - expect(periodLabel("months", 0)).toBe("Now"); - expect(periodLabel("weeks", 0)).toBe("Now"); - expect(periodLabel("years", 0)).toBe("Now"); - }); - - it("uses singular for step 1", () => { - expect(periodLabel("months", 1)).toBe("1 month"); - expect(periodLabel("weeks", 1)).toBe("1 week"); - expect(periodLabel("years", 1)).toBe("1 year"); - }); - - it("uses plural for steps > 1", () => { - expect(periodLabel("months", 10)).toBe("10 months"); - expect(periodLabel("weeks", 3)).toBe("3 weeks"); - expect(periodLabel("years", 2)).toBe("2 years"); - }); -}); - -describe("normalizeSubscriptionToPeriod", () => { - it("normalizes monthly amounts to months unchanged", () => { - expect(normalizeSubscriptionToPeriod(10000, "monthly", "months")).toBe(10000); - }); - - it("converts monthly to weeks", () => { - expect(normalizeSubscriptionToPeriod(12000, "monthly", "weeks")).toBe(2769); - }); - - it("converts monthly to years", () => { - expect(normalizeSubscriptionToPeriod(10000, "monthly", "years")).toBe(120000); - }); - - it("converts yearly to months", () => { - expect(normalizeSubscriptionToPeriod(1200000, "yearly", "months")).toBe(100000); - }); - - it("converts weekly to months", () => { - expect(normalizeSubscriptionToPeriod(1000, "weekly", "months")).toBe(4333); - }); - - it("converts bi_weekly to months", () => { - expect(normalizeSubscriptionToPeriod(2000, "bi_weekly", "months")).toBe(4333); - }); - - it("converts quarterly to months", () => { - expect(normalizeSubscriptionToPeriod(30000, "quarterly", "months")).toBe(10000); - }); - - it("converts bi_yearly to months", () => { - expect(normalizeSubscriptionToPeriod(60000, "bi_yearly", "months")).toBe(10000); - }); - - it("rounds fractional cents", () => { - expect(normalizeSubscriptionToPeriod(500, "weekly", "weeks")).toBe(500); - expect(normalizeSubscriptionToPeriod(1, "monthly", "weeks")).toBe(0); - }); - - it("returns 0 for unknown frequencies", () => { - expect(normalizeSubscriptionToPeriod(10000, "daily", "months")).toBe(0); - }); -}); \ No newline at end of file diff --git a/src/services/api.ts b/src/services/api.ts deleted file mode 100644 index 79778a3..0000000 --- a/src/services/api.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { invoke } from "@tauri-apps/api/core"; -import type { - Account, - Category, - Transaction, - Subscription, - Overview, - OmarchyTheme, - CreateAccountRequest, - CreateTransactionRequest, - CreateCategoryRequest, - UpdateCategoryRequest, - CreateSubscriptionRequest, - CsvMapping, - CsvPreview, - ImportOptions, - ImportResult, - ExportData, -} from "../types"; - -export const api = { - // Accounts - getAccounts: () => invoke("get_accounts"), - createAccount: (request: CreateAccountRequest) => - invoke("create_account", { request }), - updateAccount: (request: { id: string } & Partial) => - invoke("update_account", { request }), - deleteAccount: (id: string) => - invoke("delete_account", { id }), - - // Transactions - getTransactions: () => invoke("get_transactions"), - createTransaction: (request: CreateTransactionRequest) => - invoke("create_transaction", { request }), - updateTransaction: (request: { id: string } & Partial) => - invoke("update_transaction", { request }), - deleteTransaction: (id: string) => - invoke("delete_transaction", { id }), - deleteTransactions: (ids: string[]) => - invoke("delete_transactions", { ids }), - - // Categories - getCategories: () => invoke("get_categories"), - createCategory: (request: CreateCategoryRequest) => - invoke("create_category", { request }), - updateCategory: (request: UpdateCategoryRequest) => - invoke("update_category", { request }), - deleteCategory: (id: string) => - invoke("delete_category", { id }), - - // Subscriptions - getSubscriptions: () => invoke("get_subscriptions"), - createSubscription: (request: CreateSubscriptionRequest) => - invoke("create_subscription", { request }), - updateSubscription: (request: { id: string } & Partial) => - invoke("update_subscription", { request }), - deleteSubscription: (id: string) => - invoke("delete_subscription", { id }), - deleteSubscriptions: (ids: string[]) => - invoke("delete_subscriptions", { ids }), - - // Transfers - transfer: (request: { - from_account_id: string; - to_account_id: string; - amount: number; - currency: string; - notes?: string; - }) => - invoke("transfer", { - fromAccountId: request.from_account_id, - toAccountId: request.to_account_id, - amount: request.amount, - currency: request.currency, - notes: request.notes, - }), - - // Overview - getOverview: () => invoke("get_overview"), - - // Theme - getOmarchyTheme: () => invoke("get_omarchy_theme"), - - // Import - openCsvFileDialog: () => invoke("open_csv_file_dialog"), - previewCsvImport: (path: string, mapping: CsvMapping) => - invoke("preview_csv_import", { path, mapping }), - importTransactions: ( - path: string, - mapping: CsvMapping, - options: ImportOptions - ) => invoke("import_transactions", { path, mapping, options }), - - // Export - exportData: () => invoke("export_data"), - saveFileDialog: (defaultName: string, extension: string) => - invoke("save_file_dialog", { defaultName, extension }), - writeExportFile: (path: string, contents: string) => - invoke("write_export_file", { path, contents }), -}; diff --git a/src/services/format.ts b/src/services/format.ts deleted file mode 100644 index 1fd3ea0..0000000 --- a/src/services/format.ts +++ /dev/null @@ -1,66 +0,0 @@ -export function formatMoney(amount: number, _currency = "USD"): string { - const abs = Math.abs(amount); - const dollars = abs / 100; - const formatted = dollars.toLocaleString("en-US", { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - }); - const sign = amount >= 0 ? "+" : "-"; - return `${sign}$${formatted}`; -} - -export function formatMoneyShort(amount: number): string { - const abs = Math.abs(amount); - const dollars = abs / 100; - if (dollars >= 1000) { - return `$${(dollars / 1000).toFixed(1)}k`; - } - return `$${dollars.toFixed(2)}`; -} - -export function formatBalance(amount: number): string { - const dollars = amount / 100; - return `$${dollars.toLocaleString("en-US", { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - })}`; -} - -export function formatDate(dateStr: string): string { - const d = new Date(dateStr); - return d.toLocaleDateString("en-US", { - month: "short", - day: "numeric", - }); -} - -export function formatDateFull(dateStr: string): string { - const d = new Date(dateStr); - return d.toLocaleDateString("en-US", { - year: "numeric", - month: "short", - day: "numeric", - }); -} - -export function todayISO(): string { - return new Date().toISOString().split("T")[0]; -} - -export function currentMonth(): string { - const d = new Date(); - return d.toLocaleDateString("en-US", { year: "numeric", month: "long" }); -} - -export function accountTypeLabel(type: string): string { - const labels: Record = { - cash: "Cash", - bank: "Bank", - savings: "Savings", - credit_card: "Credit Card", - mobile_money: "Mobile Money", - investment: "Investment", - other: "Other", - }; - return labels[type] || type; -} diff --git a/src/services/projection.ts b/src/services/projection.ts deleted file mode 100644 index 18e4a79..0000000 --- a/src/services/projection.ts +++ /dev/null @@ -1,138 +0,0 @@ -export type ProjectionPeriodType = "weeks" | "months" | "years"; - -export interface ProjectionInput { - startingBalance: number; - income: number; - expense: number; - periodType: ProjectionPeriodType; - periods: number; -} - -export interface ProjectionPoint { - step: number; - label: string; - balance: number; -} - -export interface ProjectionSummary { - netPerPeriod: number; - finalBalance: number; - totalIncrease: number; - retentionRate: number | null; - projectedDate: string | null; -} - -export interface ProjectionResult { - points: ProjectionPoint[]; - summary: ProjectionSummary; - maxPeriods: number; -} - -export const PERIOD_UNIT_LABELS: Record = { - weeks: "week", - months: "month", - years: "year", -}; - -export const PERIOD_PLURAL_LABELS: Record = { - weeks: "weeks", - months: "months", - years: "years", -}; - -export const PERIOD_LIMITS: Record = { - weeks: 260, - months: 600, - years: 60, -}; - -const PERIOD_DAYS: Record = { - weeks: 7, - months: 30, - years: 365, -}; - -export function periodLabel( - periodType: ProjectionPeriodType, - step: number -): string { - if (step === 0) return "Now"; - const unit = PERIOD_UNIT_LABELS[periodType]; - return step === 1 ? `1 ${unit}` : `${step} ${unit}s`; -} - -export type SubscriptionFrequency = - | "weekly" - | "bi_weekly" - | "monthly" - | "quarterly" - | "bi_yearly" - | "yearly"; - -const SUBSCRIPTION_PERIODS_PER_YEAR: Record = { - weekly: 52, - bi_weekly: 26, - monthly: 12, - quarterly: 4, - bi_yearly: 2, - yearly: 1, -}; - -const PROJECTION_PERIODS_PER_YEAR: Record = { - weeks: 52, - months: 12, - years: 1, -}; - -export function normalizeSubscriptionToPeriod( - amount: number, - frequency: string, - periodType: ProjectionPeriodType -): number { - const perYear = - SUBSCRIPTION_PERIODS_PER_YEAR[frequency as SubscriptionFrequency]; - if (!perYear) return 0; - return Math.round((amount * perYear) / PROJECTION_PERIODS_PER_YEAR[periodType]); -} - -export function computeProjection(input: ProjectionInput): ProjectionResult { - const { startingBalance, income, expense, periodType, periods } = input; - const netPerPeriod = income - expense; - const maxPeriods = PERIOD_LIMITS[periodType]; - - const points: ProjectionPoint[] = []; - const balance0 = startingBalance; - points.push({ - step: 0, - label: periodLabel(periodType, 0), - balance: balance0, - }); - - for (let step = 1; step <= periods; step++) { - points.push({ - step, - label: periodLabel(periodType, step), - balance: startingBalance + netPerPeriod * step, - }); - } - - const finalBalance = startingBalance + netPerPeriod * periods; - const totalIncrease = finalBalance - startingBalance; - - const today = new Date(); - today.setHours(0, 0, 0, 0); - today.setDate(today.getDate() + PERIOD_DAYS[periodType] * periods); - const projectedDate = today.toISOString().split("T")[0]; - - return { - points, - summary: { - netPerPeriod, - finalBalance, - totalIncrease, - retentionRate: income > 0 ? (netPerPeriod / income) * 100 : null, - projectedDate, - }, - maxPeriods, - }; -} \ No newline at end of file diff --git a/src/styles/global.css b/src/styles/global.css deleted file mode 100644 index 74f9399..0000000 --- a/src/styles/global.css +++ /dev/null @@ -1,1507 +0,0 @@ -:root { - /* Default theme (Tokyo Night) — overridden at runtime by Omarchy theme */ - --chelete-bg: #1a1b26; - --chelete-bg-secondary: #16161e; - --chelete-surface: #24283b; - --chelete-surface-hover: #2f3349; - --chelete-fg: #c0caf5; - --chelete-fg-muted: #737aa2; - --chelete-fg-subtle: #565f89; - --chelete-accent: #7aa2f7; - --chelete-success: #9ece6a; - --chelete-danger: #f7768e; - --chelete-warning: #e0af68; - --chelete-info: #7dcfff; - --chelete-border: #3b4261; - --chelete-radius: 4px; - --chelete-font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; - --chelete-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace; -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -html, body, #root { - height: 100%; - width: 100%; - overflow: hidden; -} - -body { - font-family: var(--chelete-font); - background: var(--chelete-bg); - color: var(--chelete-fg); - font-size: 13px; - line-height: 1.5; - -webkit-font-smoothing: antialiased; -} - -/* ── Layout ────────────────────────────────────────────────── */ - -.app { - display: flex; - height: 100vh; -} - -.sidebar { - width: 200px; - min-width: 200px; - background: var(--chelete-bg-secondary); - border-right: 1px solid var(--chelete-border); - display: flex; - flex-direction: column; - padding: 16px 0; - overflow-y: auto; - overflow-x: hidden; - transition: width 0.2s ease, min-width 0.2s ease; -} - -.sidebar.collapsed { - width: 52px; - min-width: 52px; - align-items: center; -} - -.sidebar.collapsed .sidebar-logo .element-box { - width: 36px; - height: 36px; -} - -.sidebar.collapsed .element-number { - font-size: 7px; - top: 2px; - left: 3px; -} - -.sidebar.collapsed .element-symbol { - font-size: 16px; - margin-top: 3px; -} - -.sidebar.collapsed .sidebar-link { - justify-content: center; - padding: 10px; -} - -.sidebar.collapsed .sidebar-divider { - width: 24px; - margin: 8px auto; -} - -.sidebar.collapsed nav { - width: 100%; -} - -.sidebar-logo { - display: flex; - flex-direction: row; - align-items: center; - justify-content: flex-start; - gap: 12px; - padding: 0 16px 16px; - border-bottom: 1px solid var(--chelete-border); - margin-bottom: 8px; -} - -.sidebar.collapsed .sidebar-logo { - flex-direction: column; - align-items: center; - justify-content: center; - gap: 0; - padding: 0 8px 16px; -} - -.element-box { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - width: 56px; - height: 56px; - border: 2px solid var(--chelete-accent); - border-radius: 4px; - position: relative; -} - -.element-number { - position: absolute; - top: 3px; - left: 5px; - font-size: 9px; - color: var(--chelete-fg-muted); - font-weight: 500; -} - -.element-symbol { - font-size: 22px; - font-weight: 700; - color: var(--chelete-accent); - line-height: 1; - margin-top: 4px; -} - -.element-name { - font-size: 9px; - color: var(--chelete-fg-muted); - letter-spacing: 0.12em; - text-transform: uppercase; - margin-top: 6px; -} - -.sidebar nav { - display: flex; - flex-direction: column; - gap: 1px; -} - -.sidebar-link { - display: flex; - align-items: center; - gap: 10px; - padding: 8px 16px; - color: var(--chelete-fg-muted); - text-decoration: none; - font-size: 13px; - cursor: pointer; - transition: background 0.1s, color 0.1s; - border: none; - background: none; - width: 100%; - text-align: left; -} - -.sidebar-link:hover { - background: var(--chelete-surface); - color: var(--chelete-fg); -} - -.sidebar-link.active { - background: var(--chelete-surface); - color: var(--chelete-accent); -} - -.sidebar-link .shortcut { - margin-left: auto; - font-size: 11px; - color: var(--chelete-fg-subtle); - font-family: var(--chelete-mono); -} - -.sidebar-divider { - height: 1px; - background: var(--chelete-border); - margin: 8px 16px; -} - -.sidebar-divider-thin { - height: 1px; - background: var(--chelete-border); - margin: 8px 16px; - opacity: 0.6; -} - -.main-content { - flex: 1; - overflow-y: auto; - padding: 24px 32px; -} - -/* ── Header ────────────────────────────────────────────────── */ - -.page-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 24px; -} - -.page-title { - font-size: 18px; - font-weight: 600; - color: var(--chelete-fg); -} - -.page-subtitle { - font-size: 12px; - color: var(--chelete-fg-muted); - margin-top: 2px; -} - -/* ── Cards ─────────────────────────────────────────────────── */ - -.card-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - gap: 12px; - margin-bottom: 24px; -} - -.card { - background: var(--chelete-surface); - border: 1px solid var(--chelete-border); - border-radius: var(--chelete-radius); - padding: 16px; -} - -.card-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 6px; -} - -.card-icon { - color: var(--chelete-fg-muted); -} - -.card-icon.positive { - color: var(--chelete-success); -} - -.card-icon.negative { - color: var(--chelete-danger); -} - -.card-label { - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.06em; - color: var(--chelete-fg-muted); - margin-bottom: 6px; -} - -.card-value { - font-size: 22px; - font-weight: 600; - font-variant-numeric: tabular-nums; - color: var(--chelete-fg); -} - -.card-value.positive { - color: var(--chelete-success); -} - -.card-value.negative { - color: var(--chelete-danger); -} - -.card-change { - font-size: 11px; - margin-top: 4px; - color: var(--chelete-fg-muted); -} - -.overview-filters { - display: flex; - gap: 8px; - margin-bottom: 16px; -} - -/* ── Date filter ───────────────────────────────────────────── */ - -.df-row { - display: flex; - align-items: center; - gap: 12px; -} - -.df { - position: relative; -} - -.df-btn { - display: inline-flex; - align-items: center; - gap: 6px; - height: 28px; - padding: 0 10px; - font-size: 12px; - font-family: var(--chelete-font); - color: var(--chelete-fg-muted); - background: none; - border: 1px solid var(--chelete-border); - border-radius: var(--chelete-radius); - cursor: pointer; - transition: all 0.12s ease; -} - -.df-btn:hover { - color: var(--chelete-fg); - border-color: var(--chelete-fg-subtle); - background: var(--chelete-surface); -} - -.df-chevron-open { - transform: rotate(180deg); - transition: transform 0.15s ease; -} - -.df-menu { - position: absolute; - top: calc(100% + 4px); - left: 0; - min-width: 170px; - background: var(--chelete-bg); - border: 1px solid var(--chelete-border); - border-radius: var(--chelete-radius); - box-shadow: 0 8px 30px rgba(0, 0, 0, 0.35); - z-index: 200; - padding: 4px; - opacity: 0; - transform: translateY(-4px); - animation: df-show 0.12s ease forwards; -} - -@keyframes df-show { - to { opacity: 1; transform: translateY(0); } -} - -.df-opt { - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - height: 30px; - padding: 0 8px; - font-size: 12px; - font-family: var(--chelete-font); - color: var(--chelete-fg); - background: none; - border: none; - border-radius: var(--chelete-radius); - cursor: pointer; - transition: background 0.08s; -} - -.df-opt:hover { - background: var(--chelete-surface); -} - -.df-opt-active { - color: var(--chelete-accent); -} - -.df-range { - display: flex; - align-items: center; - gap: 6px; -} - -.df-date { - height: 28px; - width: 135px; - padding: 0 8px; - font-size: 12px; - font-family: var(--chelete-font); - color: var(--chelete-fg); - background: var(--chelete-surface); - border: 1px solid var(--chelete-border); - border-radius: var(--chelete-radius); - outline: none; - transition: border-color 0.12s; -} - -.df-date:focus { - border-color: var(--chelete-accent); -} - -.df-arrow { - color: var(--chelete-fg-subtle); -} - -/* ── Date picker modal ─────────────────────────────────────── */ - -.df-modal-overlay { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; -} - -.df-modal { - background: var(--chelete-bg); - border: 1px solid var(--chelete-border); - border-radius: var(--chelete-radius); - box-shadow: 0 16px 48px rgba(0, 0, 0, 0.4); - min-width: 520px; -} - -.df-modal-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 14px 16px; - border-bottom: 1px solid var(--chelete-border); -} - -.df-modal-title { - font-size: 13px; - font-weight: 600; - color: var(--chelete-fg); -} - -.df-modal-close { - display: flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - background: none; - border: none; - border-radius: var(--chelete-radius); - color: var(--chelete-fg-muted); - cursor: pointer; - transition: all 0.1s; -} - -.df-modal-close:hover { - background: var(--chelete-surface); - color: var(--chelete-fg); -} - -.df-modal-body { - display: flex; - gap: 16px; - padding: 16px; - justify-content: center; -} - -.df-modal-body .react-datepicker { - border: none !important; - background: transparent !important; -} - -.df-modal-body .react-datepicker-wrapper { - display: block !important; -} - -.df-modal-footer { - display: flex; - justify-content: flex-end; - gap: 8px; - padding: 12px 16px; - border-top: 1px solid var(--chelete-border); -} - -.df-modal-btn { - padding: 6px 14px; - font-size: 12px; - font-family: var(--chelete-font); - color: var(--chelete-fg); - background: var(--chelete-surface); - border: 1px solid var(--chelete-border); - border-radius: var(--chelete-radius); - cursor: pointer; - transition: all 0.1s; -} - -.df-modal-btn:hover { - background: var(--chelete-surface-hover); -} - -.df-modal-btn-primary { - background: var(--chelete-accent); - color: var(--chelete-bg); - border-color: var(--chelete-accent); -} - -.df-modal-btn-primary:hover { - opacity: 0.9; -} - -/* ── React Datepicker overrides ────────────────────────────── */ - -.df-calendar { - font-family: var(--chelete-font) !important; - background: transparent !important; - border: none !important; - color: var(--chelete-fg) !important; - padding: 0 !important; -} - -.df-calendar .react-datepicker__triangle { - display: none !important; -} - -.df-calendar .react-datepicker__header { - background: transparent !important; - border: none !important; - padding: 4px 8px 8px !important; -} - -.df-calendar .react-datepicker__current-month { - font-size: 13px !important; - font-weight: 600 !important; - color: var(--chelete-fg) !important; - font-family: var(--chelete-font) !important; -} - -.df-calendar .react-datepicker__day-names { - border-bottom: 1px solid var(--chelete-border) !important; - margin-bottom: 4px !important; -} - -.df-calendar .react-datepicker__day-name { - font-size: 10px !important; - font-weight: 500 !important; - color: var(--chelete-fg-subtle) !important; - width: 30px !important; - height: 28px !important; - line-height: 28px !important; -} - -.df-calendar .react-datepicker__week { - display: flex !important; -} - -.df-day { - font-size: 12px !important; - font-family: var(--chelete-font) !important; - color: var(--chelete-fg) !important; - width: 30px !important; - height: 30px !important; - line-height: 30px !important; - border-radius: var(--chelete-radius) !important; - transition: background 0.08s !important; - margin: 0 !important; - border: none !important; - background: none !important; -} - -.df-day:hover { - background: var(--chelete-surface-hover) !important; -} - -.df-day-inrange { - background: color-mix(in srgb, var(--chelete-accent) 12%, transparent) !important; - color: var(--chelete-accent) !important; - border-radius: 0 !important; -} - -.react-datepicker__day--selected, -.react-datepicker__day--keyboard-selected { - background: var(--chelete-accent) !important; - color: var(--chelete-bg) !important; - font-weight: 600 !important; - border-radius: var(--chelete-radius) !important; -} - -.react-datepicker__day--outside-month { - color: var(--chelete-fg-subtle) !important; - opacity: 0.4 !important; -} - -.react-datepicker__navigation { - top: 6px !important; - width: 24px !important; - height: 24px !important; -} - -.react-datepicker__navigation-icon::before { - display: none !important; -} - -.react-datepicker__navigation-icon::after { - display: none !important; -} - -.react-datepicker__navigation--previous { - background: none !important; - border: none !important; -} - -.react-datepicker__navigation--next { - background: none !important; - border: none !important; -} - -.react-datepicker__navigation--previous::after { - content: "" !important; - display: block !important; - width: 8px !important; - height: 8px !important; - border-left: 2px solid var(--chelete-fg-muted) !important; - border-bottom: 2px solid var(--chelete-fg-muted) !important; - transform: rotate(45deg) !important; - margin-left: 4px !important; - transition: border-color 0.1s !important; -} - -.react-datepicker__navigation--next::after { - content: "" !important; - display: block !important; - width: 8px !important; - height: 8px !important; - border-right: 2px solid var(--chelete-fg-muted) !important; - border-top: 2px solid var(--chelete-fg-muted) !important; - transform: rotate(45deg) !important; - margin-right: 4px !important; - transition: border-color 0.1s !important; -} - -.react-datepicker__navigation--previous:hover::after { - border-color: var(--chelete-fg) !important; -} - -.react-datepicker__navigation--next:hover::after { - border-color: var(--chelete-fg) !important; -} - -/* Hide month/year dropdowns entirely */ - -.react-datepicker__month-dropdown-container, -.react-datepicker__year-dropdown-container { - display: none !important; -} - -.react-datepicker__month-read-view, -.react-datepicker__year-read-view { - display: none !important; -} - -.react-datepicker__month-select, -.react-datepicker__year-select { - display: none !important; -} - -/* ── Toggle switch ─────────────────────────────────────────── */ - -.toggle-switch { - position: relative; - width: 36px; - height: 20px; - background: var(--chelete-border); - border: none; - border-radius: 10px; - cursor: pointer; - transition: background 0.2s ease; - padding: 0; - flex-shrink: 0; -} - -.toggle-switch.on { - background: var(--chelete-accent); -} - -.toggle-knob { - position: absolute; - top: 2px; - left: 2px; - width: 16px; - height: 16px; - background: white; - border-radius: 50%; - transition: transform 0.2s ease; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); -} - -.toggle-switch.on .toggle-knob { - transform: translateX(16px); -} - -/* ── Settings layout ───────────────────────────────────────── */ - -.settings-row { - display: flex; - align-items: center; - justify-content: space-between; - padding: 10px 0; -} - -.settings-row:not(:first-child) { - padding-top: 0; -} - -.settings-label { - font-size: 13px; - color: var(--chelete-fg); -} - -.settings-desc { - font-size: 11px; - color: var(--chelete-fg-muted); - margin-top: 2px; -} - -.settings-value { - font-size: 12px; - color: var(--chelete-fg-muted); -} - -.settings-divider { - height: 1px; - background: var(--chelete-border); - margin: 0; -} - -/* ── About ─────────────────────────────────────────────────── */ - -.about-card { - padding: 16px; -} - -.about-header { - display: flex; - align-items: center; - gap: 12px; - margin-bottom: 12px; -} - -.about-logo { - transform: scale(0.8); - transform-origin: left center; -} - -.about-name { - font-size: 15px; - font-weight: 600; - color: var(--chelete-fg); -} - -.about-version { - font-size: 11px; - color: var(--chelete-fg-muted); - margin-top: 2px; -} - -.about-desc { - font-size: 12px; - line-height: 1.6; - color: var(--chelete-fg-muted); - margin-bottom: 12px; -} - -/* ── Icon picker ───────────────────────────────────────────── */ - -.icon-picker { - display: flex; - flex-wrap: wrap; - gap: 4px; -} - -.icon-picker-item { - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - border: 1px solid var(--chelete-border); - border-radius: var(--chelete-radius); - background: var(--chelete-surface); - color: var(--chelete-fg-muted); - cursor: pointer; - transition: background 0.1s, color 0.1s, border-color 0.1s; -} - -.icon-picker-item:hover { - background: var(--chelete-surface-hover); - color: var(--chelete-fg); - border-color: var(--chelete-fg-subtle); -} - -.icon-picker-item.selected { - background: var(--chelete-accent); - color: var(--chelete-bg); - border-color: var(--chelete-accent); -} - -.filter-select { - width: auto; - min-width: 140px; -} - -/* ── Tables ────────────────────────────────────────────────── */ - -.table-container { - border: 1px solid var(--chelete-border); - border-radius: var(--chelete-radius); - overflow: hidden; - margin-bottom: 24px; -} - -table { - width: 100%; - border-collapse: collapse; - font-size: 13px; -} - -thead { - background: var(--chelete-surface); -} - -th { - text-align: left; - padding: 10px 12px; - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.06em; - color: var(--chelete-fg-muted); - font-weight: 500; - border-bottom: 1px solid var(--chelete-border); -} - -td { - padding: 10px 12px; - border-bottom: 1px solid var(--chelete-border); - color: var(--chelete-fg); -} - -tr:last-child td { - border-bottom: none; -} - -tr:hover td { - background: var(--chelete-surface-hover); -} - -.amount { - font-variant-numeric: tabular-nums; - font-weight: 500; - text-align: right; -} - -.amount.income { - color: var(--chelete-success); -} - -.amount.expense { - color: var(--chelete-danger); -} - -/* ── Buttons ───────────────────────────────────────────────── */ - -.btn { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 6px 12px; - font-size: 13px; - font-family: var(--chelete-font); - border: 1px solid var(--chelete-border); - border-radius: var(--chelete-radius); - background: var(--chelete-surface); - color: var(--chelete-fg); - cursor: pointer; - transition: background 0.1s, border-color 0.1s; -} - -.btn:hover { - background: var(--chelete-surface-hover); - border-color: var(--chelete-fg-subtle); -} - -.btn-primary { - background: var(--chelete-accent); - color: var(--chelete-bg); - border-color: var(--chelete-accent); -} - -.btn-primary:hover { - opacity: 0.9; -} - -.btn-danger { - color: var(--chelete-danger); - border-color: var(--chelete-danger); - background: color-mix(in srgb, var(--chelete-danger) 10%, transparent); -} - -.btn-danger:hover { - background: color-mix(in srgb, var(--chelete-danger) 20%, transparent); - border-color: var(--chelete-danger); -} - -.type-tag { - display: inline-flex; - align-items: center; - padding: 2px 8px; - border-radius: var(--chelete-radius); - font-size: 11px; - font-weight: 500; - text-transform: capitalize; - border: 1px solid var(--chelete-border); - color: var(--chelete-fg-muted); -} - -.type-tag.income { - color: var(--chelete-success); - border-color: var(--chelete-success); - background: color-mix(in srgb, var(--chelete-success) 10%, transparent); -} - -.type-tag.expense { - color: var(--chelete-danger); - border-color: var(--chelete-danger); - background: color-mix(in srgb, var(--chelete-danger) 10%, transparent); -} - -/* ── Forms ─────────────────────────────────────────────────── */ - -.form-group { - margin-bottom: 14px; -} - -.form-label { - display: block; - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.06em; - color: var(--chelete-fg-muted); - margin-bottom: 4px; -} - -.form-input, -.form-select { - width: 100%; - padding: 8px 10px; - font-size: 13px; - font-family: var(--chelete-font); - background: var(--chelete-bg); - color: var(--chelete-fg); - border: 1px solid var(--chelete-border); - border-radius: var(--chelete-radius); - outline: none; - transition: border-color 0.1s; - appearance: none; - -webkit-appearance: none; - -moz-appearance: none; -} - -.form-select { - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23737aa2' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 10px center; - padding-right: 30px; - cursor: pointer; -} - -.form-select option { - background: var(--chelete-bg); - color: var(--chelete-fg); -} - -.form-input:focus, -.form-select:focus { - border-color: var(--chelete-accent); -} - -.form-input::placeholder { - color: var(--chelete-fg-subtle); -} - -.form-checkbox { - appearance: none; - -webkit-appearance: none; - -moz-appearance: none; - width: 16px; - height: 16px; - margin: 0; - background: var(--chelete-bg); - border: 1px solid var(--chelete-border); - border-radius: calc(var(--chelete-radius) * 1.25); - cursor: pointer; - display: inline-grid; - place-items: center; - transition: border-color 0.15s, background 0.15s; -} - -.form-checkbox:focus { - outline: none; - border-color: var(--chelete-accent); - box-shadow: 0 0 0 2px rgba(122, 162, 247, 0.2); -} - -.form-checkbox:checked { - background: var(--chelete-accent); - border-color: var(--chelete-accent); -} - -.form-checkbox:checked::after { - content: ""; - width: 8px; - height: 8px; - background: var(--chelete-bg); - clip-path: polygon(14% 50%, 38% 74%, 86% 26%, 78% 18%, 38% 58%, 22% 42%); -} - -.form-checkbox:indeterminate { - background: var(--chelete-accent); - border-color: var(--chelete-accent); -} - -.form-checkbox:indeterminate::after { - content: ""; - width: 8px; - height: 2px; - background: var(--chelete-bg); -} - -/* ── Modal / Dialog ────────────────────────────────────────── */ - -.modal-overlay { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.6); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; -} - -.modal { - background: var(--chelete-bg-secondary); - border: 1px solid var(--chelete-border); - border-radius: var(--chelete-radius); - padding: 24px; - min-width: 380px; - max-width: 500px; - box-shadow: 0 16px 48px rgba(0, 0, 0, 0.4); -} - -.modal-title { - font-size: 15px; - font-weight: 600; - margin-bottom: 16px; - color: var(--chelete-fg); -} - -.modal-actions { - display: flex; - justify-content: flex-end; - gap: 8px; - margin-top: 20px; -} - -/* ── Progress bar ──────────────────────────────────────────── */ - -.progress-bar { - height: 6px; - background: var(--chelete-bg); - border-radius: 3px; - overflow: hidden; - margin-top: 8px; -} - -.progress-fill { - height: 100%; - border-radius: 3px; - transition: width 0.3s ease; -} - -.progress-fill.ok { - background: var(--chelete-accent); -} - -.progress-fill.warning { - background: var(--chelete-warning); -} - -.progress-fill.danger { - background: var(--chelete-danger); -} - -/* ── Category tag ──────────────────────────────────────────── */ - -.category-tag { - display: inline-block; - padding: 2px 8px; - font-size: 11px; - border-radius: var(--chelete-radius); - background: var(--chelete-surface); - color: var(--chelete-fg-muted); - border: 1px solid var(--chelete-border); -} - -/* ── Command palette ───────────────────────────────────────── */ - -.command-palette-overlay { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: flex-start; - justify-content: center; - padding-top: 20vh; - z-index: 2000; -} - -.command-palette { - background: var(--chelete-bg-secondary); - border: 1px solid var(--chelete-border); - border-radius: var(--chelete-radius); - width: 460px; - max-height: 360px; - overflow: hidden; - box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5); -} - -.command-palette input { - width: 100%; - padding: 14px 16px; - font-size: 14px; - font-family: var(--chelete-font); - background: transparent; - color: var(--chelete-fg); - border: none; - border-bottom: 1px solid var(--chelete-border); - outline: none; -} - -.command-palette-results { - max-height: 300px; - overflow-y: auto; -} - -.command-palette-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: 10px 16px; - cursor: pointer; - font-size: 13px; - color: var(--chelete-fg); - transition: background 0.05s; -} - -.command-palette-item:hover, -.command-palette-item.selected { - background: var(--chelete-surface); -} - -.command-palette-item .shortcut { - font-size: 11px; - color: var(--chelete-fg-subtle); - font-family: var(--chelete-mono); -} - -/* ── Overview budget bars ──────────────────────────────────── */ - -.budget-row { - margin-bottom: 12px; -} - -.budget-header { - display: flex; - justify-content: space-between; - font-size: 12px; - margin-bottom: 4px; -} - -.budget-name { - color: var(--chelete-fg); -} - -.budget-amounts { - color: var(--chelete-fg-muted); - font-variant-numeric: tabular-nums; -} - -/* ── Scrollbar ─────────────────────────────────────────────── */ - -::-webkit-scrollbar { - width: 6px; -} - -::-webkit-scrollbar-track { - background: transparent; -} - -::-webkit-scrollbar-thumb { - background: var(--chelete-border); - border-radius: 3px; -} - -::-webkit-scrollbar-thumb:hover { - background: var(--chelete-fg-subtle); -} - -/* ── Donut charts ──────────────────────────────────────────── */ - -.donut-chart-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: 16px; - margin-bottom: 24px; -} - -.donut-chart { - background: var(--chelete-surface); - border: 1px solid var(--chelete-border); - border-radius: var(--chelete-radius); - padding: 16px; -} - -.donut-chart-title { - font-size: 12px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.06em; - color: var(--chelete-fg); - margin-bottom: 12px; -} - -.donut-chart-body { - display: flex; - align-items: center; - gap: 16px; -} - -.donut-chart-svg { - width: 140px; - height: 140px; - flex-shrink: 0; -} - -.donut-chart-label { - font-size: 10px; - fill: var(--chelete-fg-muted); - text-transform: uppercase; - letter-spacing: 0.06em; -} - -.donut-chart-value { - font-size: 20px; - font-weight: 600; - fill: var(--chelete-fg); - font-variant-numeric: tabular-nums; -} - -.donut-chart-empty { - color: var(--chelete-fg-muted); - font-size: 13px; - padding: 32px 0; - text-align: center; - flex: 1; -} - -.donut-chart-legend { - display: flex; - flex-direction: column; - gap: 8px; - flex: 1; - min-width: 0; -} - -.donut-chart-legend-item { - display: flex; - align-items: center; - gap: 8px; - font-size: 12px; -} - -.donut-chart-legend-color { - width: 10px; - height: 10px; - border-radius: 2px; - flex-shrink: 0; -} - -.donut-chart-legend-label { - color: var(--chelete-fg); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - flex: 1; - min-width: 0; -} - -.donut-chart-legend-value { - color: var(--chelete-fg-muted); - font-variant-numeric: tabular-nums; - flex-shrink: 0; -} - -/* ── Pagination ────────────────────────────────────────────── */ - -.pagination { - display: flex; - align-items: center; - justify-content: flex-end; - gap: 12px; - margin-top: 12px; - margin-bottom: 24px; -} - -.pagination-btn { - padding: 6px 12px; - font-size: 12px; - font-family: var(--chelete-font); - color: var(--chelete-fg); - background: var(--chelete-surface); - border: 1px solid var(--chelete-border); - border-radius: var(--chelete-radius); - cursor: pointer; - transition: background 0.1s, border-color 0.1s; -} - -.pagination-btn:hover:not(:disabled) { - background: var(--chelete-surface-hover); - border-color: var(--chelete-fg-subtle); -} - -.pagination-btn:disabled { - opacity: 0.4; - cursor: not-allowed; -} - -.pagination-info { - font-size: 12px; - color: var(--chelete-fg-muted); - font-variant-numeric: tabular-nums; -} - -.pagination-per-page { - display: flex; - align-items: center; - gap: 8px; - font-size: 12px; - color: var(--chelete-fg-muted); -} - -.pagination-per-page .form-select { - min-width: 64px; - width: auto; - padding: 4px 24px 4px 8px; - font-size: 12px; - height: 26px; -} - -/* ── Projections ───────────────────────────────────────────── */ - -.projection-layout { - display: grid; - grid-template-columns: 260px 1fr; - gap: 16px; - align-items: start; -} - -.projection-form { - display: flex; - flex-direction: column; - gap: 12px; -} - -.projection-form .form-group { - margin-bottom: 0; -} - -.projection-hint { - font-size: 11px; - color: var(--chelete-fg-muted); - margin-top: 4px; -} - -.projection-subs-box { - background: var(--chelete-surface-hover); - border: 1px solid var(--chelete-border); - border-radius: var(--chelete-radius); - padding: 10px; - font-size: 12px; -} - -.projection-subs-header { - display: flex; - align-items: center; - gap: 6px; - color: var(--chelete-fg-muted); - font-size: 11px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.05em; - margin-bottom: 8px; -} - -.projection-subs-list { - display: flex; - flex-direction: column; - gap: 4px; - margin-bottom: 8px; - max-height: 180px; - overflow-y: auto; -} - -.projection-subs-item { - display: flex; - justify-content: space-between; - align-items: center; - gap: 8px; -} - -.projection-subs-name { - color: var(--chelete-fg); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.projection-subs-amount { - color: var(--chelete-fg-muted); - font-variant-numeric: tabular-nums; - white-space: nowrap; -} - -.projection-subs-total { - border-top: 1px solid var(--chelete-border); - padding-top: 8px; - color: var(--chelete-fg); - font-weight: 600; - font-variant-numeric: tabular-nums; -} - -.projection-subs-empty { - margin-top: 8px; - color: var(--chelete-fg-muted); - font-size: 11px; -} - -.projection-content { - display: flex; - flex-direction: column; - gap: 16px; - min-width: 0; -} - -.projection-empty { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 12px; - padding: 64px 24px; - color: var(--chelete-fg-muted); - font-size: 13px; - text-align: center; - background: var(--chelete-surface); - border: 1px solid var(--chelete-border); - border-radius: var(--chelete-radius); -} - -.projection-chart-card { - padding: 16px; -} - -.projection-chart-title { - font-size: 12px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.06em; - color: var(--chelete-fg); - margin-bottom: 12px; -} - -.projection-chart-container { - width: 100%; - height: 320px; -} - -@media (max-width: 900px) { - .projection-layout { - grid-template-columns: 1fr; - } -} diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index 6ffa79b..0000000 --- a/src/types.ts +++ /dev/null @@ -1,198 +0,0 @@ -export interface Account { - id: string; - name: string; - account_type: string; - currency: string; - balance: number; - color: string | null; - icon: string | null; - is_active: boolean; - sort_order: number; - created_at: string; - updated_at: string; -} - -export interface Category { - id: string; - name: string; - parent_id: string | null; - category_type: string; - icon: string | null; - color: string | null; - sort_order: number; - created_at: string; - updated_at: string; -} - -export interface Transaction { - id: string; - account_id: string; - category_id: string | null; - transaction_type: string; - amount: number; - currency: string; - description: string; - merchant: string | null; - notes: string | null; - transaction_date: string; - created_at: string; - updated_at: string; -} - -export interface Overview { - total_balance: number; - total_income: number; - total_expenses: number; - accounts: Account[]; - recent_transactions: Transaction[]; - category_spending: CategorySpending[]; -} - -export interface CategorySpending { - category_id: string; - category_name: string; - spent: number; - budget_limit: number | null; -} - -export interface OmarchyTheme { - name: string; - background: string; - foreground: string; - accent: string; - colors: Record; -} - -export type View = - | "overview" - | "transactions" - | "accounts" - | "categories" - | "subscriptions" - | "projections" - | "settings"; - -export interface CreateAccountRequest { - name: string; - account_type: string; - currency: string; - balance: number; - color?: string; - icon?: string; -} - -export interface CreateTransactionRequest { - account_id: string; - category_id?: string; - transaction_type: string; - amount: number; - currency: string; - description: string; - merchant?: string; - notes?: string; - transaction_date: string; -} - -export interface CreateCategoryRequest { - name: string; - parent_id?: string; - category_type: string; - icon?: string; - color?: string; -} - -export interface UpdateCategoryRequest { - id: string; - name?: string; - category_type?: string; - icon?: string; - color?: string; -} - -export interface Subscription { - id: string; - name: string; - amount: number; - currency: string; - frequency: string; - category_id: string | null; - account_id: string | null; - start_date: string | null; - is_active: boolean; - created_at: string; - updated_at: string; -} - -export interface CreateSubscriptionRequest { - name: string; - amount: number; - currency: string; - frequency: string; - category_id?: string; - account_id?: string; - start_date?: string; -} - -export type CsvField = - | "date" - | "description" - | "merchant" - | "amount" - | "income_amount" - | "expense_amount" - | "type" - | "category" - | "account" - | "currency" - | "notes" - | "ignore"; - -export interface CsvMapping { - columns: Record; - default_account_id?: string; - default_category_id?: string; - default_currency?: string; - default_transaction_type?: "income" | "expense"; - type_aliases: Record; -} - -export interface ParsedRow { - row_index: number; - date?: string; - description?: string; - merchant?: string; - transaction_type?: string; - amount_cents?: number; - amount_source?: "income_column" | "expense_column" | "amount_column"; - currency?: string; - category?: string; - account?: string; - notes?: string; - errors: string[]; -} - -export interface CsvPreview { - headers: string[]; - rows: ParsedRow[]; - total_rows: number; -} - -export interface ImportOptions { - default_account_id?: string; - default_category_id?: string; - default_currency?: string; - skip_duplicates: boolean; -} - -export interface ImportResult { - imported: number; - skipped: number; - errors: string[]; -} - -export interface ExportData { - accounts: Account[]; - categories: Category[]; - transactions: Transaction[]; - subscriptions: Subscription[]; -} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts deleted file mode 100644 index 11f02fe..0000000 --- a/src/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 20fb0a0..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "isolatedModules": true, - "moduleDetection": "force", - "noEmit": true, - "jsx": "react-jsx", - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "forceConsistentCasingInFileNames": true - }, - "include": ["src"] -} diff --git a/vite.config.ts b/vite.config.ts deleted file mode 100644 index 656069f..0000000 --- a/vite.config.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; - -const host = process.env.TAURI_DEV_HOST; - -export default defineConfig(async () => ({ - plugins: [react()], - clearScreen: false, - server: { - port: 1420, - strictPort: true, - host: host || false, - hmr: host - ? { - protocol: "ws", - host, - port: 1421, - } - : undefined, - watch: { - ignored: ["**/src-tauri/**"], - }, - }, -})); diff --git a/vitest.config.ts b/vitest.config.ts deleted file mode 100644 index 7ed11ae..0000000 --- a/vitest.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - globals: true, - environment: "jsdom", - include: ["src/**/*.test.ts", "src/**/*.test.tsx"], - setupFiles: [], - }, -}); From d63f45e78f46cbb62d0ce0d00b29c68d57793f31 Mon Sep 17 00:00:00 2001 From: Victor Phos Date: Fri, 2 Oct 2026 14:03:37 +0200 Subject: [PATCH 05/36] Make the backend a standalone Rust crate Move the Rust sources to the repository root and drop the Tauri plumbing: command attributes, State/AppHandle arguments, the file-dialog commands and the Omarchy theme reader (native theming comes from gpui-omarchy). Commands now take &DbState. Data stays in ~/.local/share/com.chelete.app so existing databases keep working. The Makefile drives cargo instead of npm. --- .gitignore | 2 - src-tauri/Cargo.toml => Cargo.toml | 8 +- Makefile | 17 +- {src-tauri => assets}/icons/128x128.png | Bin {src-tauri => assets}/icons/128x128@2x.png | Bin {src-tauri => assets}/icons/32x32.png | Bin {src-tauri => assets}/icons/icon.icns | 0 {src-tauri => assets}/icons/icon.ico | Bin src-tauri/src/lib.rs | 53 ----- src-tauri/src/main.rs | 5 - src-tauri/src/theme/mod.rs | 256 --------------------- {src-tauri/src => src}/bin/seed.rs | 0 {src-tauri/src => src}/commands/mod.rs | 118 ++-------- {src-tauri/src => src}/database/mod.rs | 16 +- {src-tauri/src => src}/import/mapping.rs | 0 {src-tauri/src => src}/import/mod.rs | 0 {src-tauri/src => src}/import/parser.rs | 0 src/lib.rs | 4 + src/main.rs | 8 + {src-tauri/src => src}/seed/mod.rs | 4 +- 20 files changed, 51 insertions(+), 440 deletions(-) rename src-tauri/Cargo.toml => Cargo.toml (73%) rename {src-tauri => assets}/icons/128x128.png (100%) rename {src-tauri => assets}/icons/128x128@2x.png (100%) rename {src-tauri => assets}/icons/32x32.png (100%) rename {src-tauri => assets}/icons/icon.icns (100%) rename {src-tauri => assets}/icons/icon.ico (100%) delete mode 100644 src-tauri/src/lib.rs delete mode 100644 src-tauri/src/main.rs delete mode 100644 src-tauri/src/theme/mod.rs rename {src-tauri/src => src}/bin/seed.rs (100%) rename {src-tauri/src => src}/commands/mod.rs (93%) rename {src-tauri/src => src}/database/mod.rs (92%) rename {src-tauri/src => src}/import/mapping.rs (100%) rename {src-tauri/src => src}/import/mod.rs (100%) rename {src-tauri/src => src}/import/parser.rs (100%) create mode 100644 src/lib.rs create mode 100644 src/main.rs rename {src-tauri/src => src}/seed/mod.rs (98%) diff --git a/.gitignore b/.gitignore index a5027e6..931aa3b 100644 --- a/.gitignore +++ b/.gitignore @@ -25,5 +25,3 @@ target/ .DS_Store Thumbs.db -# Tauri -src-tauri/target/ diff --git a/src-tauri/Cargo.toml b/Cargo.toml similarity index 73% rename from src-tauri/Cargo.toml rename to Cargo.toml index 1c04857..e88a42e 100644 --- a/src-tauri/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ default-run = "chelete" [lib] name = "chelete_lib" -crate-type = ["staticlib", "cdylib", "rlib"] +path = "src/lib.rs" [[bin]] name = "chelete" @@ -17,8 +17,6 @@ name = "seed" path = "src/bin/seed.rs" [dependencies] -tauri = { version = "2", features = [] } -tauri-plugin-shell = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" rusqlite = { version = "0.32", features = ["bundled"] } @@ -26,12 +24,8 @@ uuid = { version = "1", features = ["v4"] } chrono = { version = "0.4", features = ["serde"] } toml = "0.8" dirs = "5" -tauri-plugin-dialog = "2.7.3" csv = "1.4.0" -[build-dependencies] -tauri-build = { version = "2", features = [] } - [profile.release] panic = "abort" codegen-units = 1 diff --git a/Makefile b/Makefile index c46469e..f658fda 100644 --- a/Makefile +++ b/Makefile @@ -1,21 +1,16 @@ -.PHONY: dev build test test-watch seed release-patch release-minor release-major aur-update clean +.PHONY: dev build test seed release-patch release-minor release-major aur-update clean dev: - npm run tauri dev + cargo run build: - npm run build - cd src-tauri && cargo build --release + cargo build --release test: - npm run test - cd src-tauri && cargo test - -test-watch: - npm run test:watch + cargo test seed: - npm run seed + cargo run --bin seed release-patch: ./scripts/bump-version.sh patch @@ -30,4 +25,4 @@ aur-update: cd aur && makepkg --printsrcinfo > .SRCINFO clean: - rm -rf dist src-tauri/target node_modules + cargo clean diff --git a/src-tauri/icons/128x128.png b/assets/icons/128x128.png similarity index 100% rename from src-tauri/icons/128x128.png rename to assets/icons/128x128.png diff --git a/src-tauri/icons/128x128@2x.png b/assets/icons/128x128@2x.png similarity index 100% rename from src-tauri/icons/128x128@2x.png rename to assets/icons/128x128@2x.png diff --git a/src-tauri/icons/32x32.png b/assets/icons/32x32.png similarity index 100% rename from src-tauri/icons/32x32.png rename to assets/icons/32x32.png diff --git a/src-tauri/icons/icon.icns b/assets/icons/icon.icns similarity index 100% rename from src-tauri/icons/icon.icns rename to assets/icons/icon.icns diff --git a/src-tauri/icons/icon.ico b/assets/icons/icon.ico similarity index 100% rename from src-tauri/icons/icon.ico rename to assets/icons/icon.ico diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs deleted file mode 100644 index e5730be..0000000 --- a/src-tauri/src/lib.rs +++ /dev/null @@ -1,53 +0,0 @@ -mod commands; -mod database; -mod import; -pub mod seed; -mod theme; - -use database::DbState; -use tauri::Manager; - -#[cfg_attr(mobile, tauri::mobile_entry_point)] -pub fn run() { - tauri::Builder::default() - .plugin(tauri_plugin_shell::init()) - .plugin(tauri_plugin_dialog::init()) - .setup(|app| { - let app_handle = app.handle().clone(); - let db_state = database::init_database(&app_handle)?; - app.handle().manage(DbState(db_state)); - Ok(()) - }) - .invoke_handler(tauri::generate_handler![ - commands::get_accounts, - commands::create_account, - commands::update_account, - commands::delete_account, - commands::get_transactions, - commands::create_transaction, - commands::update_transaction, - commands::delete_transaction, - commands::delete_transactions, - commands::get_categories, - commands::create_category, - commands::update_category, - commands::delete_category, - commands::get_subscriptions, - commands::create_subscription, - commands::update_subscription, - commands::delete_subscription, - commands::delete_subscriptions, - commands::transfer, - commands::get_overview, - commands::get_omarchy_theme, - commands::open_csv_file_dialog, - commands::preview_csv_import, - commands::import_transactions, - commands::export_data, - commands::save_file_dialog, - commands::write_export_file, - seed::seed_database_command, - ]) - .run(tauri::generate_context!()) - .expect("error while running Chelete"); -} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs deleted file mode 100644 index 7748f03..0000000 --- a/src-tauri/src/main.rs +++ /dev/null @@ -1,5 +0,0 @@ -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] - -fn main() { - chelete_lib::run() -} diff --git a/src-tauri/src/theme/mod.rs b/src-tauri/src/theme/mod.rs deleted file mode 100644 index a2553a8..0000000 --- a/src-tauri/src/theme/mod.rs +++ /dev/null @@ -1,256 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::fs; -use std::path::PathBuf; - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct OmarchyTheme { - pub name: String, - pub background: String, - pub foreground: String, - pub accent: String, - pub colors: HashMap, -} - -#[derive(Debug, Deserialize)] -struct ColorsToml { - mode: Option, - accent: Option, - selection: Option, - muted: Option, - background: Option, - dark_background: Option, - darker_background: Option, - lighter_background: Option, - foreground: Option, - dark_foreground: Option, - light_foreground: Option, - bright_foreground: Option, - red: Option, - yellow: Option, - orange: Option, - green: Option, - cyan: Option, - blue: Option, - magenta: Option, - brown: Option, - bright_red: Option, - bright_yellow: Option, - bright_green: Option, - bright_cyan: Option, - bright_blue: Option, - bright_magenta: Option, -} - -pub fn detect_theme() -> Result { - let home = dirs::home_dir().ok_or("Could not find home directory")?; - - // Check the standard Omarchy theme location - let theme_dir = home.join(".local/state/omarchy/current/theme"); - let theme_name_file = home.join(".local/state/omarchy/current/theme.name"); - let colors_file = theme_dir.join("colors.toml"); - - if colors_file.exists() { - // Read theme name - let theme_name = if theme_name_file.exists() { - fs::read_to_string(&theme_name_file) - .unwrap_or_else(|_| "Omarchy".to_string()) - .trim() - .to_string() - } else { - "Omarchy".to_string() - }; - - return parse_omarchy_theme(&colors_file, &theme_name); - } - - // Fallback: generate a default dark theme - Ok(default_theme()) -} - -fn parse_omarchy_theme(path: &PathBuf, theme_name: &str) -> Result { - let content = fs::read_to_string(path).map_err(|e| format!("Failed to read theme file: {}", e))?; - - let parsed: ColorsToml = - toml::from_str(&content).map_err(|e| format!("Failed to parse theme file: {}", e))?; - - let mut color_map = HashMap::new(); - - // Insert all colors from the TOML file - if let Some(v) = &parsed.background { - color_map.insert("background".to_string(), v.clone()); - } - if let Some(v) = &parsed.foreground { - color_map.insert("foreground".to_string(), v.clone()); - } - if let Some(v) = &parsed.accent { - color_map.insert("accent".to_string(), v.clone()); - } - if let Some(v) = &parsed.selection { - color_map.insert("selection".to_string(), v.clone()); - } - if let Some(v) = &parsed.muted { - color_map.insert("muted".to_string(), v.clone()); - } - if let Some(v) = &parsed.dark_background { - color_map.insert("dark_background".to_string(), v.clone()); - } - if let Some(v) = &parsed.darker_background { - color_map.insert("darker_background".to_string(), v.clone()); - } - if let Some(v) = &parsed.lighter_background { - color_map.insert("lighter_background".to_string(), v.clone()); - } - if let Some(v) = &parsed.dark_foreground { - color_map.insert("dark_foreground".to_string(), v.clone()); - } - if let Some(v) = &parsed.light_foreground { - color_map.insert("light_foreground".to_string(), v.clone()); - } - if let Some(v) = &parsed.bright_foreground { - color_map.insert("bright_foreground".to_string(), v.clone()); - } - if let Some(v) = &parsed.red { - color_map.insert("red".to_string(), v.clone()); - color_map.insert("danger".to_string(), v.clone()); - } - if let Some(v) = &parsed.yellow { - color_map.insert("yellow".to_string(), v.clone()); - color_map.insert("warning".to_string(), v.clone()); - } - if let Some(v) = &parsed.orange { - color_map.insert("orange".to_string(), v.clone()); - } - if let Some(v) = &parsed.green { - color_map.insert("green".to_string(), v.clone()); - color_map.insert("success".to_string(), v.clone()); - } - if let Some(v) = &parsed.cyan { - color_map.insert("cyan".to_string(), v.clone()); - color_map.insert("info".to_string(), v.clone()); - } - if let Some(v) = &parsed.blue { - color_map.insert("blue".to_string(), v.clone()); - } - if let Some(v) = &parsed.magenta { - color_map.insert("magenta".to_string(), v.clone()); - } - if let Some(v) = &parsed.brown { - color_map.insert("brown".to_string(), v.clone()); - } - if let Some(v) = &parsed.bright_red { - color_map.insert("bright_red".to_string(), v.clone()); - } - if let Some(v) = &parsed.bright_yellow { - color_map.insert("bright_yellow".to_string(), v.clone()); - } - if let Some(v) = &parsed.bright_green { - color_map.insert("bright_green".to_string(), v.clone()); - } - if let Some(v) = &parsed.bright_cyan { - color_map.insert("bright_cyan".to_string(), v.clone()); - } - if let Some(v) = &parsed.bright_blue { - color_map.insert("bright_blue".to_string(), v.clone()); - } - if let Some(v) = &parsed.bright_magenta { - color_map.insert("bright_magenta".to_string(), v.clone()); - } - - // Derive colors from background - let background = parsed.background.unwrap_or_else(|| "#121212".to_string()); - let foreground = parsed.foreground.unwrap_or_else(|| "#bebebe".to_string()); - let accent = parsed.accent.unwrap_or_else(|| "#e68e0d".to_string()); - - let bg_rgb = hex_to_rgb(&background); - let light = is_light(bg_rgb); - - let (surface, surface_hover, border) = if light { - ( - rgb_to_hex(bg_rgb.0.saturating_sub(8), bg_rgb.1.saturating_sub(8), bg_rgb.2.saturating_sub(8)), - rgb_to_hex(bg_rgb.0.saturating_sub(15), bg_rgb.1.saturating_sub(15), bg_rgb.2.saturating_sub(15)), - rgb_to_hex(bg_rgb.0.saturating_sub(18), bg_rgb.1.saturating_sub(18), bg_rgb.2.saturating_sub(18)), - ) - } else { - ( - rgb_to_hex((bg_rgb.0 as u32 + 12).min(255) as u8, (bg_rgb.1 as u32 + 12).min(255) as u8, (bg_rgb.2 as u32 + 12).min(255) as u8), - rgb_to_hex((bg_rgb.0 as u32 + 20).min(255) as u8, (bg_rgb.1 as u32 + 20).min(255) as u8, (bg_rgb.2 as u32 + 20).min(255) as u8), - rgb_to_hex((bg_rgb.0 as u32 + 28).min(255) as u8, (bg_rgb.1 as u32 + 28).min(255) as u8, (bg_rgb.2 as u32 + 28).min(255) as u8), - ) - }; - color_map.insert("surface".to_string(), surface); - color_map.insert("surface_hover".to_string(), surface_hover); - color_map.insert("border".to_string(), border); - - let fg_rgb = hex_to_rgb(&foreground); - let muted = rgb_to_hex( - (fg_rgb.0 as u32 * 60 / 100) as u8, - (fg_rgb.1 as u32 * 60 / 100) as u8, - (fg_rgb.2 as u32 * 60 / 100) as u8, - ); - color_map.insert("foreground_muted".to_string(), muted); - - let subtle = rgb_to_hex( - (fg_rgb.0 as u32 * 40 / 100) as u8, - (fg_rgb.1 as u32 * 40 / 100) as u8, - (fg_rgb.2 as u32 * 40 / 100) as u8, - ); - color_map.insert("foreground_subtle".to_string(), subtle); - - Ok(OmarchyTheme { - name: theme_name.to_string(), - background, - foreground, - accent, - colors: color_map, - }) -} - -fn default_theme() -> OmarchyTheme { - let mut colors = HashMap::new(); - colors.insert("background".to_string(), "#1a1b26".to_string()); - colors.insert("foreground".to_string(), "#c0caf5".to_string()); - colors.insert("surface".to_string(), "#24283b".to_string()); - colors.insert("surface_hover".to_string(), "#2f3349".to_string()); - colors.insert("border".to_string(), "#3b4261".to_string()); - colors.insert("accent".to_string(), "#7aa2f7".to_string()); - colors.insert("success".to_string(), "#9ece6a".to_string()); - colors.insert("danger".to_string(), "#f7768e".to_string()); - colors.insert("warning".to_string(), "#e0af68".to_string()); - colors.insert("info".to_string(), "#7dcfff".to_string()); - colors.insert("foreground_muted".to_string(), "#737aa2".to_string()); - colors.insert("foreground_subtle".to_string(), "#565f89".to_string()); - colors.insert("black".to_string(), "#15161e".to_string()); - colors.insert("red".to_string(), "#f7768e".to_string()); - colors.insert("green".to_string(), "#9ece6a".to_string()); - colors.insert("yellow".to_string(), "#e0af68".to_string()); - colors.insert("blue".to_string(), "#7aa2f7".to_string()); - colors.insert("magenta".to_string(), "#bb9af7".to_string()); - colors.insert("cyan".to_string(), "#7dcfff".to_string()); - colors.insert("white".to_string(), "#a9b1d6".to_string()); - - OmarchyTheme { - name: "Tokyo Night".to_string(), - background: "#1a1b26".to_string(), - foreground: "#c0caf5".to_string(), - accent: "#7aa2f7".to_string(), - colors, - } -} - -fn hex_to_rgb(hex: &str) -> (u8, u8, u8) { - let hex = hex.trim_start_matches('#'); - let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0); - let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0); - let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0); - (r, g, b) -} - -fn rgb_to_hex(r: u8, g: u8, b: u8) -> String { - format!("#{:02x}{:02x}{:02x}", r, g, b) -} - -fn is_light(rgb: (u8, u8, u8)) -> bool { - let luminance = rgb.0 as u32 * 299 + rgb.1 as u32 * 587 + rgb.2 as u32 * 114; - luminance / 1000 > 128 -} diff --git a/src-tauri/src/bin/seed.rs b/src/bin/seed.rs similarity index 100% rename from src-tauri/src/bin/seed.rs rename to src/bin/seed.rs diff --git a/src-tauri/src/commands/mod.rs b/src/commands/mod.rs similarity index 93% rename from src-tauri/src/commands/mod.rs rename to src/commands/mod.rs index 11bbf8f..8def7ad 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -2,8 +2,6 @@ use crate::database::DbState; use crate::import::{CsvMapping, CsvPreview, ImportResult, parse_csv_preview, parse_csv_rows}; use rusqlite::params; use serde::{Deserialize, Serialize}; -use tauri::State; -use tauri_plugin_dialog::DialogExt; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Account { @@ -180,8 +178,7 @@ fn generate_id() -> String { // ── Accounts ───────────────────────────────────────────────────── -#[tauri::command] -pub fn get_accounts(state: State<'_, DbState>) -> Result, String> { +pub fn get_accounts(state: &DbState) -> Result, String> { let conn = state.0.lock().map_err(|e| e.to_string())?; let mut stmt = conn .prepare( @@ -213,9 +210,8 @@ pub fn get_accounts(state: State<'_, DbState>) -> Result, String> { Ok(accounts) } -#[tauri::command] pub fn create_account( - state: State<'_, DbState>, + state: &DbState, request: CreateAccountRequest, ) -> Result { let conn = state.0.lock().map_err(|e| e.to_string())?; @@ -251,9 +247,8 @@ pub fn create_account( }) } -#[tauri::command] pub fn update_account( - state: State<'_, DbState>, + state: &DbState, request: UpdateAccountRequest, ) -> Result { let conn = state.0.lock().map_err(|e| e.to_string())?; @@ -291,8 +286,7 @@ pub fn update_account( Ok(account) } -#[tauri::command] -pub fn delete_account(state: State<'_, DbState>, id: String) -> Result<(), String> { +pub fn delete_account(state: &DbState, id: String) -> Result<(), String> { let conn = state.0.lock().map_err(|e| e.to_string())?; conn.execute( "UPDATE accounts SET deleted_at = datetime('now') WHERE id = ?1", @@ -328,8 +322,7 @@ fn get_account_by_id(conn: &rusqlite::Connection, id: &str) -> Result) -> Result, String> { +pub fn get_transactions(state: &DbState) -> Result, String> { let conn = state.0.lock().map_err(|e| e.to_string())?; let mut stmt = conn .prepare( @@ -362,9 +355,8 @@ pub fn get_transactions(state: State<'_, DbState>) -> Result, S Ok(transactions) } -#[tauri::command] pub fn create_transaction( - state: State<'_, DbState>, + state: &DbState, request: CreateTransactionRequest, ) -> Result { let conn = state.0.lock().map_err(|e| e.to_string())?; @@ -416,9 +408,8 @@ pub fn create_transaction( }) } -#[tauri::command] pub fn update_transaction( - state: State<'_, DbState>, + state: &DbState, request: UpdateTransactionRequest, ) -> Result { let conn = state.0.lock().map_err(|e| e.to_string())?; @@ -513,14 +504,12 @@ pub fn update_transaction( get_transaction_by_id(&conn, &request.id) } -#[tauri::command] -pub fn delete_transaction(state: State<'_, DbState>, id: String) -> Result<(), String> { +pub fn delete_transaction(state: &DbState, id: String) -> Result<(), String> { let conn = state.0.lock().map_err(|e| e.to_string())?; delete_transactions_inner(&conn, &[id]) } -#[tauri::command] -pub fn delete_transactions(state: State<'_, DbState>, ids: Vec) -> Result<(), String> { +pub fn delete_transactions(state: &DbState, ids: Vec) -> Result<(), String> { let conn = state.0.lock().map_err(|e| e.to_string())?; delete_transactions_inner(&conn, &ids) } @@ -605,8 +594,7 @@ fn get_transaction_by_id( // ── Categories ─────────────────────────────────────────────────── -#[tauri::command] -pub fn get_categories(state: State<'_, DbState>) -> Result, String> { +pub fn get_categories(state: &DbState) -> Result, String> { let conn = state.0.lock().map_err(|e| e.to_string())?; let mut stmt = conn .prepare( @@ -636,9 +624,8 @@ pub fn get_categories(state: State<'_, DbState>) -> Result, String Ok(categories) } -#[tauri::command] pub fn create_category( - state: State<'_, DbState>, + state: &DbState, request: CreateCategoryRequest, ) -> Result { let conn = state.0.lock().map_err(|e| e.to_string())?; @@ -671,9 +658,8 @@ pub fn create_category( }) } -#[tauri::command] pub fn update_category( - state: State<'_, DbState>, + state: &DbState, request: UpdateCategoryRequest, ) -> Result { let conn = state.0.lock().map_err(|e| e.to_string())?; @@ -711,8 +697,7 @@ pub fn update_category( Ok(category) } -#[tauri::command] -pub fn delete_category(state: State<'_, DbState>, id: String) -> Result<(), String> { +pub fn delete_category(state: &DbState, id: String) -> Result<(), String> { let conn = state.0.lock().map_err(|e| e.to_string())?; conn.execute( "UPDATE categories SET deleted_at = datetime('now') WHERE id = ?1", @@ -746,8 +731,7 @@ fn get_category_by_id(conn: &rusqlite::Connection, id: &str) -> Result) -> Result { +pub fn get_overview(state: &DbState) -> Result { let conn = state.0.lock().map_err(|e| e.to_string())?; // Total balance from all active accounts @@ -874,8 +858,7 @@ pub fn get_overview(state: State<'_, DbState>) -> Result { // ── Subscriptions ──────────────────────────────────────────────── -#[tauri::command] -pub fn get_subscriptions(state: State<'_, DbState>) -> Result, String> { +pub fn get_subscriptions(state: &DbState) -> Result, String> { let conn = state.0.lock().map_err(|e| e.to_string())?; let mut stmt = conn .prepare( @@ -907,9 +890,8 @@ pub fn get_subscriptions(state: State<'_, DbState>) -> Result, Ok(subscriptions) } -#[tauri::command] pub fn create_subscription( - state: State<'_, DbState>, + state: &DbState, request: CreateSubscriptionRequest, ) -> Result { let conn = state.0.lock().map_err(|e| e.to_string())?; @@ -946,9 +928,8 @@ pub fn create_subscription( }) } -#[tauri::command] pub fn update_subscription( - state: State<'_, DbState>, + state: &DbState, request: UpdateSubscriptionRequest, ) -> Result { let conn = state.0.lock().map_err(|e| e.to_string())?; @@ -1014,8 +995,7 @@ pub fn update_subscription( get_subscription_by_id(&conn, &request.id) } -#[tauri::command] -pub fn delete_subscription(state: State<'_, DbState>, id: String) -> Result<(), String> { +pub fn delete_subscription(state: &DbState, id: String) -> Result<(), String> { let conn = state.0.lock().map_err(|e| e.to_string())?; conn.execute( "UPDATE subscriptions SET deleted_at = datetime('now') WHERE id = ?1", @@ -1025,8 +1005,7 @@ pub fn delete_subscription(state: State<'_, DbState>, id: String) -> Result<(), Ok(()) } -#[tauri::command] -pub fn delete_subscriptions(state: State<'_, DbState>, ids: Vec) -> Result<(), String> { +pub fn delete_subscriptions(state: &DbState, ids: Vec) -> Result<(), String> { let conn = state.0.lock().map_err(|e| e.to_string())?; if ids.is_empty() { return Ok(()); @@ -1041,9 +1020,8 @@ pub fn delete_subscriptions(state: State<'_, DbState>, ids: Vec) -> Resu Ok(()) } -#[tauri::command] pub fn transfer( - state: State<'_, DbState>, + state: &DbState, from_account_id: String, to_account_id: String, amount: i64, @@ -1155,24 +1133,6 @@ fn get_subscription_by_id(conn: &rusqlite::Connection, id: &str) -> Result Result, String> { - let file_path = app - .dialog() - .file() - .add_filter("CSV files", &["csv"]) - .blocking_pick_file(); - - match file_path { - Some(path) => { - let path_buf = path.into_path().map_err(|e| e.to_string())?; - Ok(Some(path_buf.to_string_lossy().to_string())) - } - None => Ok(None), - } -} - -#[tauri::command] pub fn preview_csv_import( path: String, mapping: CsvMapping, @@ -1180,9 +1140,8 @@ pub fn preview_csv_import( parse_csv_preview(&path, &mapping) } -#[tauri::command] pub fn import_transactions( - state: State<'_, DbState>, + state: &DbState, path: String, mapping: CsvMapping, options: ImportOptions, @@ -1424,38 +1383,12 @@ pub struct ExportData { pub subscriptions: Vec, } -#[tauri::command] -pub async fn save_file_dialog( - app: tauri::AppHandle, - default_name: String, - extension: String, -) -> Result, String> { - let file_path = app - .dialog() - .file() - .set_file_name(default_name) - .add_filter(&format!("{} files", extension.to_uppercase()), - &[extension.trim_start_matches('.')], - ) - .blocking_save_file(); - - match file_path { - Some(path) => { - let path_buf = path.into_path().map_err(|e| e.to_string())?; - Ok(Some(path_buf.to_string_lossy().to_string())) - } - None => Ok(None), - } -} - -#[tauri::command] pub fn write_export_file(path: String, contents: String) -> Result<(), String> { std::fs::write(&path, contents).map_err(|e| format!("Failed to write file: {}", e))?; Ok(()) } -#[tauri::command] -pub fn export_data(state: State<'_, DbState>) -> Result { +pub fn export_data(state: &DbState) -> Result { let conn = state.0.lock().map_err(|e| e.to_string())?; Ok(ExportData { accounts: load_accounts_for_export(&conn)?, @@ -1585,10 +1518,3 @@ fn load_subscriptions_for_export(conn: &rusqlite::Connection) -> Result Result { - crate::theme::detect_theme() -} diff --git a/src-tauri/src/database/mod.rs b/src/database/mod.rs similarity index 92% rename from src-tauri/src/database/mod.rs rename to src/database/mod.rs index 4d0ad12..d298ca2 100644 --- a/src-tauri/src/database/mod.rs +++ b/src/database/mod.rs @@ -1,16 +1,18 @@ use rusqlite::{Connection, Result}; use std::fs; use std::sync::Mutex; -use tauri::AppHandle; -use tauri::Manager; pub struct DbState(pub Mutex); -pub fn init_database(app: &AppHandle) -> Result> { - let app_dir = app - .path() - .app_data_dir() - .expect("failed to get app data dir"); +/// Same directory the Tauri build used, so existing data keeps loading. +pub fn data_dir() -> std::path::PathBuf { + dirs::data_dir() + .expect("failed to get data dir") + .join("com.chelete.app") +} + +pub fn init_database() -> Result> { + let app_dir = data_dir(); fs::create_dir_all(&app_dir).expect("failed to create app data dir"); let db_path = app_dir.join("chelete.db"); diff --git a/src-tauri/src/import/mapping.rs b/src/import/mapping.rs similarity index 100% rename from src-tauri/src/import/mapping.rs rename to src/import/mapping.rs diff --git a/src-tauri/src/import/mod.rs b/src/import/mod.rs similarity index 100% rename from src-tauri/src/import/mod.rs rename to src/import/mod.rs diff --git a/src-tauri/src/import/parser.rs b/src/import/parser.rs similarity index 100% rename from src-tauri/src/import/parser.rs rename to src/import/parser.rs diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..15a9569 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,4 @@ +pub mod commands; +pub mod database; +pub mod import; +pub mod seed; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..8501d4f --- /dev/null +++ b/src/main.rs @@ -0,0 +1,8 @@ +use chelete_lib::database; + +fn main() { + // The native UI arrives in later commits; for now just make sure the + // database opens and migrates. + let db = database::init_database().expect("failed to open database"); + drop(db); +} diff --git a/src-tauri/src/seed/mod.rs b/src/seed/mod.rs similarity index 98% rename from src-tauri/src/seed/mod.rs rename to src/seed/mod.rs index e7d23ed..cdf3153 100644 --- a/src-tauri/src/seed/mod.rs +++ b/src/seed/mod.rs @@ -1,6 +1,5 @@ use crate::database::DbState; use rusqlite::params; -use tauri::State; use uuid::Uuid; fn uid() -> String { @@ -127,8 +126,7 @@ pub fn seed_database(conn: &rusqlite::Connection) -> Result { Ok(true) } -#[tauri::command] -pub fn seed_database_command(state: State<'_, DbState>) -> Result { +pub fn seed_database_command(state: &DbState) -> Result { let conn = state.0.lock().map_err(|e| e.to_string())?; seed_database(&conn) } From 3dc1af1a93fed09c82cd80d8917e519c0ff28eef Mon Sep 17 00:00:00 2001 From: Victor Phos Date: Fri, 2 Oct 2026 14:04:11 +0200 Subject: [PATCH 06/36] Add money formatting and amount parsing Cents-based formatters ported from the web UI (signed, compact, balance), a strict parser for typed amounts, a lenient one for free-form numeric fields, and account type labels. --- src/format.rs | 156 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 2 files changed, 157 insertions(+) create mode 100644 src/format.rs diff --git a/src/format.rs b/src/format.rs new file mode 100644 index 0000000..0eaa332 --- /dev/null +++ b/src/format.rs @@ -0,0 +1,156 @@ +//! Money and label formatting. Amounts are stored as integer cents. + +fn group_thousands(n: u64) -> String { + let digits = n.to_string(); + let mut out = String::new(); + for (i, c) in digits.chars().enumerate() { + if i > 0 && (digits.len() - i) % 3 == 0 { + out.push(','); + } + out.push(c); + } + out +} + +fn dollars(abs_cents: u64) -> String { + format!("{}.{:02}", group_thousands(abs_cents / 100), abs_cents % 100) +} + +/// Signed amount, e.g. `+$5,250.00` / `-$10.00`. +pub fn format_money(cents: i64) -> String { + let sign = if cents >= 0 { '+' } else { '-' }; + format!("{sign}${}", dollars(cents.unsigned_abs())) +} + +/// Unsigned compact amount, e.g. `$123.45` / `$1.5k`. +pub fn format_money_short(cents: i64) -> String { + let abs = cents.unsigned_abs(); + if abs >= 100_000 { + format!("${:.1}k", abs as f64 / 100_000.0) + } else { + format!("${}", dollars(abs)) + } +} + +/// Balance without a plus sign; negatives keep the old `$-1,200.00` shape. +pub fn format_balance(cents: i64) -> String { + let minus = if cents < 0 { "-" } else { "" }; + format!("${minus}{}", dollars(cents.unsigned_abs())) +} + +/// Parse a user-typed dollar amount ("12.5", "-3", "1,250.00") into cents. +/// Returns `None` for empty or malformed input, or more than two decimals. +pub fn parse_cents(input: &str) -> Option { + let cleaned: String = input.trim().chars().filter(|c| *c != ',' && *c != '$').collect(); + if cleaned.is_empty() { + return None; + } + let (negative, digits) = match cleaned.strip_prefix('-') { + Some(rest) => (true, rest), + None => (false, cleaned.strip_prefix('+').unwrap_or(&cleaned)), + }; + let (whole, frac) = match digits.split_once('.') { + Some((w, f)) => (w, f), + None => (digits, ""), + }; + if (whole.is_empty() && frac.is_empty()) + || frac.len() > 2 + || !whole.chars().all(|c| c.is_ascii_digit()) + || !frac.chars().all(|c| c.is_ascii_digit()) + { + return None; + } + let whole: i64 = if whole.is_empty() { 0 } else { whole.parse().ok()? }; + let frac: i64 = match frac.len() { + 0 => 0, + 1 => frac.parse::().ok()? * 10, + _ => frac.parse().ok()?, + }; + let cents = whole.checked_mul(100)?.checked_add(frac)?; + Some(if negative { -cents } else { cents }) +} + +/// Lenient dollars-to-cents for free-form numeric fields: any finite number, +/// rounded to the nearest cent ("1e3", "12.345" and "-4" all parse). +pub fn parse_dollars_lenient(input: &str) -> Option { + let value: f64 = input.trim().parse().ok()?; + value.is_finite().then(|| (value * 100.0).round() as i64) +} + +pub fn account_type_label(kind: &str) -> String { + match kind { + "cash" => "Cash", + "bank" => "Bank", + "savings" => "Savings", + "credit_card" => "Credit Card", + "mobile_money" => "Mobile Money", + "investment" => "Investment", + "other" => "Other", + other => other, + } + .to_string() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn money_is_signed() { + assert_eq!(format_money(1000), "+$10.00"); + assert_eq!(format_money(525000), "+$5,250.00"); + assert_eq!(format_money(-120000), "-$1,200.00"); + assert_eq!(format_money(0), "+$0.00"); + assert_eq!(format_money(50), "+$0.50"); + assert_eq!(format_money(-25), "-$0.25"); + } + + #[test] + fn money_short_uses_k_suffix() { + assert_eq!(format_money_short(500), "$5.00"); + assert_eq!(format_money_short(12345), "$123.45"); + assert_eq!(format_money_short(100000), "$1.0k"); + assert_eq!(format_money_short(5250000), "$52.5k"); + } + + #[test] + fn balance_has_no_plus() { + assert_eq!(format_balance(525000), "$5,250.00"); + assert_eq!(format_balance(-120000), "$-1,200.00"); + assert_eq!(format_balance(0), "$0.00"); + } + + #[test] + fn parses_typed_amounts() { + assert_eq!(parse_cents("12.50"), Some(1250)); + assert_eq!(parse_cents("12.5"), Some(1250)); + assert_eq!(parse_cents("12"), Some(1200)); + assert_eq!(parse_cents(".75"), Some(75)); + assert_eq!(parse_cents("-3"), Some(-300)); + assert_eq!(parse_cents("$1,250.00"), Some(125000)); + assert_eq!(parse_cents(" 0 "), Some(0)); + assert_eq!(parse_cents(""), None); + assert_eq!(parse_cents("abc"), None); + assert_eq!(parse_cents("1.234"), None); + assert_eq!(parse_cents("1.2.3"), None); + assert_eq!(parse_cents("-"), None); + } + + #[test] + fn lenient_parse_rounds_and_rejects_junk() { + assert_eq!(parse_dollars_lenient("12.345"), Some(1235)); + assert_eq!(parse_dollars_lenient(" 1200 "), Some(120000)); + assert_eq!(parse_dollars_lenient("-4"), Some(-400)); + assert_eq!(parse_dollars_lenient("1e3"), Some(100000)); + assert_eq!(parse_dollars_lenient(""), None); + assert_eq!(parse_dollars_lenient("abc"), None); + assert_eq!(parse_dollars_lenient("inf"), None); + assert_eq!(parse_dollars_lenient("NaN"), None); + } + + #[test] + fn account_labels() { + assert_eq!(account_type_label("credit_card"), "Credit Card"); + assert_eq!(account_type_label("weird"), "weird"); + } +} diff --git a/src/lib.rs b/src/lib.rs index 15a9569..fa76cad 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod commands; pub mod database; +pub mod format; pub mod import; pub mod seed; From 8fca13f4616b3bc6daf17e6f5600d0d862f87680 Mon Sep 17 00:00:00 2001 From: Victor Phos Date: Fri, 2 Oct 2026 14:04:20 +0200 Subject: [PATCH 07/36] Add persisted UI preferences A small JSON file next to the database; starts with the sidebar's collapsed state and tolerates missing or unknown fields. --- src/lib.rs | 1 + src/prefs.rs | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 src/prefs.rs diff --git a/src/lib.rs b/src/lib.rs index fa76cad..c213513 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,4 +2,5 @@ pub mod commands; pub mod database; pub mod format; pub mod import; +pub mod prefs; pub mod seed; diff --git a/src/prefs.rs b/src/prefs.rs new file mode 100644 index 0000000..baccb98 --- /dev/null +++ b/src/prefs.rs @@ -0,0 +1,39 @@ +//! Small UI preferences persisted next to the database. +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +pub struct Prefs { + #[serde(default)] + pub sidebar_collapsed: bool, +} + +fn path() -> std::path::PathBuf { + crate::database::data_dir().join("ui.json") +} + +impl Prefs { + pub fn load() -> Self { + std::fs::read_to_string(path()) + .ok() + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or_default() + } + + pub fn save(&self) { + if let Ok(json) = serde_json::to_string_pretty(self) { + let _ = std::fs::create_dir_all(crate::database::data_dir()); + let _ = std::fs::write(path(), json); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn missing_fields_fall_back_to_defaults() { + let p: Prefs = serde_json::from_str("{}").unwrap(); + assert!(!p.sidebar_collapsed); + } +} From 238622dda4c4e2ffb68499034d992b887f8a403c Mon Sep 17 00:00:00 2001 From: Victor Phos Date: Fri, 2 Oct 2026 14:04:21 +0200 Subject: [PATCH 08/36] Add page arithmetic for list screens A Pager with clamped pages, index ranges, a '1-25 of 61' label and the per-page options the web UI offered. --- src/lib.rs | 1 + src/paging.rs | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 src/paging.rs diff --git a/src/lib.rs b/src/lib.rs index c213513..0c9e522 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,5 +2,6 @@ pub mod commands; pub mod database; pub mod format; pub mod import; +pub mod paging; pub mod prefs; pub mod seed; diff --git a/src/paging.rs b/src/paging.rs new file mode 100644 index 0000000..abaed91 --- /dev/null +++ b/src/paging.rs @@ -0,0 +1,93 @@ +//! Page arithmetic shared by the list screens. + +pub const PER_PAGE_OPTIONS: [usize; 8] = [10, 20, 25, 30, 40, 60, 80, 100]; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Pager { + pub page: usize, + pub per_page: usize, + pub total: usize, +} + +impl Pager { + pub fn new(page: usize, per_page: usize, total: usize) -> Self { + let per_page = per_page.max(1); + let pages = total.div_ceil(per_page).max(1); + Self { page: page.clamp(1, pages), per_page, total } + } + + pub fn total_pages(&self) -> usize { + self.total.div_ceil(self.per_page).max(1) + } + + pub fn has_prev(&self) -> bool { + self.page > 1 + } + + pub fn has_next(&self) -> bool { + self.page < self.total_pages() + } + + /// Index range of the current page within the full list. + pub fn range(&self) -> std::ops::Range { + let start = ((self.page - 1) * self.per_page).min(self.total); + start..(start + self.per_page).min(self.total) + } + + /// e.g. `1–10 of 25`. + pub fn label(&self) -> String { + if self.total == 0 { + return "0–0 of 0".into(); + } + let r = self.range(); + format!("{}–{} of {}", r.start + 1, r.end, self.total) + } +} + +pub fn page_slice<'a, T>(items: &'a [T], pager: &Pager) -> &'a [T] { + &items[pager.range()] +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn labels_and_ranges() { + let p = Pager::new(1, 10, 25); + assert_eq!(p.label(), "1–10 of 25"); + assert_eq!(p.range(), 0..10); + assert!(!p.has_prev() && p.has_next()); + let last = Pager::new(3, 10, 25); + assert_eq!(last.label(), "21–25 of 25"); + assert_eq!(last.range(), 20..25); + assert!(last.has_prev() && !last.has_next()); + } + + #[test] + fn page_is_clamped_when_the_list_shrinks() { + let p = Pager::new(9, 10, 12); + assert_eq!(p.page, 2); + assert_eq!(Pager::new(0, 10, 12).page, 1); + } + + #[test] + fn empty_list() { + let p = Pager::new(1, 10, 0); + assert_eq!(p.label(), "0–0 of 0"); + assert_eq!(p.total_pages(), 1); + assert!(!p.has_next() && !p.has_prev()); + assert!(page_slice::(&[], &p).is_empty()); + } + + #[test] + fn slices_the_current_page() { + let items: Vec = (0..25).collect(); + assert_eq!(page_slice(&items, &Pager::new(3, 10, 25)), &[20, 21, 22, 23, 24]); + } + + #[test] + fn zero_per_page_does_not_panic() { + assert_eq!(Pager::new(1, 0, 5).per_page, 1); + } +} From f60452f144dd10c5d20cc23f7899d2c393177907 Mon Sep 17 00:00:00 2001 From: Victor Phos Date: Fri, 2 Oct 2026 14:04:22 +0200 Subject: [PATCH 09/36] Add date-range presets for list filters Today through Last Year, All Time and Custom, as inclusive ISO date strings that compare directly against transaction dates. Weeks start on Sunday. Uses local dates; the web UI went through UTC and could be off by a day. --- src/date_filter.rs | 202 +++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 2 files changed, 203 insertions(+) create mode 100644 src/date_filter.rs diff --git a/src/date_filter.rs b/src/date_filter.rs new file mode 100644 index 0000000..32d3d9f --- /dev/null +++ b/src/date_filter.rs @@ -0,0 +1,202 @@ +//! Date presets for list filters. Ranges are inclusive ISO date strings, so +//! they compare directly against `transaction_date`. +use chrono::{Datelike, Duration, NaiveDate}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DateFilter { + Today, + Yesterday, + ThisWeek, + LastWeek, + ThisMonth, + LastMonth, + ThisYear, + LastYear, + All, + Custom, +} + +impl DateFilter { + pub const ALL_FILTERS: [DateFilter; 10] = [ + DateFilter::Today, + DateFilter::Yesterday, + DateFilter::ThisWeek, + DateFilter::LastWeek, + DateFilter::ThisMonth, + DateFilter::LastMonth, + DateFilter::ThisYear, + DateFilter::LastYear, + DateFilter::All, + DateFilter::Custom, + ]; + + pub fn value(self) -> &'static str { + match self { + DateFilter::Today => "today", + DateFilter::Yesterday => "yesterday", + DateFilter::ThisWeek => "this_week", + DateFilter::LastWeek => "last_week", + DateFilter::ThisMonth => "this_month", + DateFilter::LastMonth => "last_month", + DateFilter::ThisYear => "this_year", + DateFilter::LastYear => "last_year", + DateFilter::All => "all", + DateFilter::Custom => "custom", + } + } + + pub fn label(self) -> &'static str { + match self { + DateFilter::Today => "Today", + DateFilter::Yesterday => "Yesterday", + DateFilter::ThisWeek => "This Week", + DateFilter::LastWeek => "Last Week", + DateFilter::ThisMonth => "This Month", + DateFilter::LastMonth => "Last Month", + DateFilter::ThisYear => "This Year", + DateFilter::LastYear => "Last Year", + DateFilter::All => "All Time", + DateFilter::Custom => "Custom Range", + } + } + + pub fn from_value(value: &str) -> Option { + Self::ALL_FILTERS.into_iter().find(|f| f.value() == value) + } +} + +const MIN: &str = "0000-01-01"; +const MAX: &str = "9999-12-31"; + +fn iso(d: NaiveDate) -> String { + d.format("%Y-%m-%d").to_string() +} + +fn start_of_week(d: NaiveDate) -> NaiveDate { + d - Duration::days(i64::from(d.weekday().num_days_from_sunday())) +} + +/// Inclusive `(start, end)` for a preset. Weeks start on Sunday. `custom` +/// uses the given bounds, leaving either side open when absent. +pub fn date_range( + filter: DateFilter, + custom_start: Option, + custom_end: Option, + today: NaiveDate, +) -> (String, String) { + let first_of_month = today.with_day(1).unwrap(); + let last_month_end = first_of_month - Duration::days(1); + let week_start = start_of_week(today); + let last_week_end = week_start - Duration::days(1); + match filter { + DateFilter::Today => (iso(today), iso(today)), + DateFilter::Yesterday => { + let y = today - Duration::days(1); + (iso(y), iso(y)) + } + DateFilter::ThisWeek => (iso(week_start), iso(today)), + DateFilter::LastWeek => (iso(last_week_end - Duration::days(6)), iso(last_week_end)), + DateFilter::ThisMonth => (iso(first_of_month), iso(today)), + DateFilter::LastMonth => (iso(last_month_end.with_day(1).unwrap()), iso(last_month_end)), + DateFilter::ThisYear => (iso(NaiveDate::from_ymd_opt(today.year(), 1, 1).unwrap()), iso(today)), + DateFilter::LastYear => ( + iso(NaiveDate::from_ymd_opt(today.year() - 1, 1, 1).unwrap()), + iso(NaiveDate::from_ymd_opt(today.year() - 1, 12, 31).unwrap()), + ), + DateFilter::All => (MIN.into(), MAX.into()), + DateFilter::Custom => ( + custom_start.map(iso).unwrap_or_else(|| MIN.into()), + custom_end.map(iso).unwrap_or_else(|| MAX.into()), + ), + } +} + +pub fn is_date_in_range(date: &str, start: &str, end: &str) -> bool { + date >= start && date <= end +} + +#[cfg(test)] +mod tests { + use super::*; + + fn d(y: i32, m: u32, day: u32) -> NaiveDate { + NaiveDate::from_ymd_opt(y, m, day).unwrap() + } + + // Friday 2 October 2026. + fn today() -> NaiveDate { + d(2026, 10, 2) + } + + fn range(f: DateFilter) -> (String, String) { + date_range(f, None, None, today()) + } + + #[test] + fn single_day_presets() { + assert_eq!(range(DateFilter::Today), ("2026-10-02".into(), "2026-10-02".into())); + assert_eq!(range(DateFilter::Yesterday), ("2026-10-01".into(), "2026-10-01".into())); + } + + #[test] + fn weeks_start_on_sunday() { + assert_eq!(range(DateFilter::ThisWeek), ("2026-09-27".into(), "2026-10-02".into())); + assert_eq!(range(DateFilter::LastWeek), ("2026-09-20".into(), "2026-09-26".into())); + } + + #[test] + fn this_week_on_a_sunday_is_just_that_day() { + let sunday = d(2026, 10, 4); + assert_eq!(date_range(DateFilter::ThisWeek, None, None, sunday), ("2026-10-04".into(), "2026-10-04".into())); + } + + #[test] + fn month_presets() { + assert_eq!(range(DateFilter::ThisMonth), ("2026-10-01".into(), "2026-10-02".into())); + assert_eq!(range(DateFilter::LastMonth), ("2026-09-01".into(), "2026-09-30".into())); + } + + #[test] + fn last_month_across_a_year_boundary() { + assert_eq!( + date_range(DateFilter::LastMonth, None, None, d(2026, 1, 15)), + ("2025-12-01".into(), "2025-12-31".into()) + ); + } + + #[test] + fn year_presets() { + assert_eq!(range(DateFilter::ThisYear), ("2026-01-01".into(), "2026-10-02".into())); + assert_eq!(range(DateFilter::LastYear), ("2025-01-01".into(), "2025-12-31".into())); + } + + #[test] + fn all_time_covers_everything() { + let (s, e) = range(DateFilter::All); + assert!(is_date_in_range("1999-05-05", &s, &e)); + assert!(is_date_in_range("2999-05-05", &s, &e)); + } + + #[test] + fn custom_ranges_may_be_open_ended() { + let c = |a, b| date_range(DateFilter::Custom, a, b, today()); + assert_eq!(c(Some(d(2026, 3, 1)), Some(d(2026, 3, 31))), ("2026-03-01".into(), "2026-03-31".into())); + assert_eq!(c(Some(d(2026, 3, 1)), None).1, "9999-12-31"); + assert_eq!(c(None, Some(d(2026, 3, 31))).0, "0000-01-01"); + } + + #[test] + fn range_membership_is_inclusive() { + assert!(is_date_in_range("2026-03-01", "2026-03-01", "2026-03-31")); + assert!(is_date_in_range("2026-03-31", "2026-03-01", "2026-03-31")); + assert!(!is_date_in_range("2026-04-01", "2026-03-01", "2026-03-31")); + } + + #[test] + fn values_round_trip() { + for f in DateFilter::ALL_FILTERS { + assert_eq!(DateFilter::from_value(f.value()), Some(f)); + } + assert_eq!(DateFilter::from_value("fortnight"), None); + } +} diff --git a/src/lib.rs b/src/lib.rs index 0c9e522..204792c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ pub mod commands; pub mod database; +pub mod date_filter; pub mod format; pub mod import; pub mod paging; From 81a342cc9421ab95247ed8883e548208c4b29fb8 Mon Sep 17 00:00:00 2001 From: Victor Phos Date: Fri, 2 Oct 2026 14:04:22 +0200 Subject: [PATCH 10/36] Add subscription scheduling and filtering Frequencies and labels, the next-payment calculation (including the web UI's month-overflow behaviour, but in local time), and helpers to filter by frequency and total amounts. --- src/lib.rs | 1 + src/subscriptions.rs | 171 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 src/subscriptions.rs diff --git a/src/lib.rs b/src/lib.rs index 204792c..22649d9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,3 +6,4 @@ pub mod import; pub mod paging; pub mod prefs; pub mod seed; +pub mod subscriptions; diff --git a/src/subscriptions.rs b/src/subscriptions.rs new file mode 100644 index 0000000..6cc5b54 --- /dev/null +++ b/src/subscriptions.rs @@ -0,0 +1,171 @@ +//! Subscription scheduling and labels. +use crate::commands::Subscription; +use chrono::{Datelike, Duration, NaiveDate}; + +pub const FREQUENCIES: [(&str, &str); 6] = [ + ("weekly", "Weekly"), + ("bi_weekly", "Bi-weekly"), + ("monthly", "Monthly"), + ("quarterly", "Quarterly"), + ("bi_yearly", "Bi-yearly"), + ("yearly", "Yearly"), +]; + +pub fn frequency_label(value: &str) -> String { + FREQUENCIES + .iter() + .find(|(v, _)| *v == value) + .map(|(_, l)| l.to_string()) + .unwrap_or_else(|| value.to_string()) +} + +/// Subscriptions with the given frequency, or all of them for `"all"`. +pub fn filter_by_frequency<'a>(subs: &'a [Subscription], frequency: &str) -> Vec<&'a Subscription> { + subs.iter() + .filter(|s| frequency == "all" || s.frequency == frequency) + .collect() +} + +pub fn total_amount(subs: &[&Subscription]) -> i64 { + subs.iter().map(|s| s.amount).sum() +} + +enum Interval { + Days(i64), + Months(u32), +} + +fn interval(frequency: &str) -> Option { + Some(match frequency { + "weekly" => Interval::Days(7), + "bi_weekly" => Interval::Days(14), + "monthly" => Interval::Months(1), + "quarterly" => Interval::Months(3), + "bi_yearly" => Interval::Months(6), + "yearly" => Interval::Months(12), + _ => return None, + }) +} + +/// Add months the way JavaScript's `setFullYear(y, m + n, d)` does: a day past +/// the end of the target month spills into the following month. +fn add_months_overflowing(date: NaiveDate, months: u32) -> Option { + let total = date.month0() + months; + let year = date.year() + (total / 12) as i32; + let first = NaiveDate::from_ymd_opt(year, total % 12 + 1, 1)?; + first.checked_add_signed(Duration::days(i64::from(date.day()) - 1)) +} + +/// The first payment on or after `today`, stepping forward from `start`. +/// Mirrors the web UI: unknown frequencies return the start date unchanged, +/// and stepping stops after 120 iterations. +pub fn next_payment_date(start: Option<&str>, frequency: &str, today: NaiveDate) -> Option { + let start = NaiveDate::parse_from_str(start?, "%Y-%m-%d").ok()?; + let Some(step) = interval(frequency) else { + return Some(start); + }; + let mut next = start; + let mut iterations = 0; + while next < today && iterations < 120 { + next = match step { + Interval::Days(d) => next.checked_add_signed(Duration::days(d))?, + Interval::Months(m) => add_months_overflowing(next, m)?, + }; + iterations += 1; + } + Some(next) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn d(y: i32, m: u32, day: u32) -> NaiveDate { + NaiveDate::from_ymd_opt(y, m, day).unwrap() + } + + #[test] + fn future_start_is_returned_as_is() { + assert_eq!(next_payment_date(Some("2026-12-01"), "monthly", d(2026, 6, 1)), Some(d(2026, 12, 1))); + } + + #[test] + fn today_counts_as_due() { + assert_eq!(next_payment_date(Some("2026-06-01"), "monthly", d(2026, 6, 1)), Some(d(2026, 6, 1))); + } + + #[test] + fn monthly_rolls_forward() { + assert_eq!(next_payment_date(Some("2026-01-15"), "monthly", d(2026, 6, 20)), Some(d(2026, 7, 15))); + } + + #[test] + fn weekly_and_bi_weekly() { + assert_eq!(next_payment_date(Some("2026-06-01"), "weekly", d(2026, 6, 10)), Some(d(2026, 6, 15))); + assert_eq!(next_payment_date(Some("2026-06-01"), "bi_weekly", d(2026, 6, 10)), Some(d(2026, 6, 15))); + } + + #[test] + fn longer_periods() { + assert_eq!(next_payment_date(Some("2025-01-10"), "quarterly", d(2026, 6, 1)), Some(d(2026, 7, 10))); + assert_eq!(next_payment_date(Some("2025-01-10"), "bi_yearly", d(2026, 6, 1)), Some(d(2026, 7, 10))); + assert_eq!(next_payment_date(Some("2024-03-05"), "yearly", d(2026, 6, 1)), Some(d(2027, 3, 5))); + } + + #[test] + fn month_end_spills_like_javascript() { + // Jan 31 + 1 month is "Feb 31", which JavaScript normalises to Mar 3. + assert_eq!(next_payment_date(Some("2026-01-31"), "monthly", d(2026, 2, 1)), Some(d(2026, 3, 3))); + } + + #[test] + fn missing_or_bad_start_has_no_next_payment() { + assert_eq!(next_payment_date(None, "monthly", d(2026, 1, 1)), None); + assert_eq!(next_payment_date(Some("not a date"), "monthly", d(2026, 1, 1)), None); + } + + #[test] + fn unknown_frequency_returns_the_start() { + assert_eq!(next_payment_date(Some("2020-01-01"), "fortnightly-ish", d(2026, 1, 1)), Some(d(2020, 1, 1))); + } + + #[test] + fn stepping_stops_after_120_iterations() { + // Weekly from 1990 would need ~1,900 steps; the cap leaves it in the past. + let next = next_payment_date(Some("1990-01-01"), "weekly", d(2026, 1, 1)).unwrap(); + assert_eq!(next, d(1990, 1, 1) + Duration::days(7 * 120)); + } + + fn sub(freq: &str, amount: i64) -> Subscription { + Subscription { + id: freq.into(), + name: freq.into(), + amount, + currency: "USD".into(), + frequency: freq.into(), + category_id: None, + account_id: None, + start_date: None, + is_active: true, + created_at: String::new(), + updated_at: String::new(), + } + } + + #[test] + fn filters_and_totals() { + let subs = vec![sub("monthly", 1500), sub("yearly", 12000), sub("monthly", 500)]; + let monthly = filter_by_frequency(&subs, "monthly"); + assert_eq!(monthly.len(), 2); + assert_eq!(total_amount(&monthly), 2000); + assert_eq!(filter_by_frequency(&subs, "all").len(), 3); + assert_eq!(total_amount(&filter_by_frequency(&subs, "all")), 14000); + assert!(filter_by_frequency(&subs, "weekly").is_empty()); + } + + #[test] + fn labels() { + assert_eq!(frequency_label("bi_weekly"), "Bi-weekly"); + assert_eq!(frequency_label("other"), "other"); + } +} From 190e785c79b8ea09bb19c92d18d9325dc30a081e Mon Sep 17 00:00:00 2001 From: Victor Phos Date: Fri, 2 Oct 2026 14:04:23 +0200 Subject: [PATCH 11/36] Add balance projection math Straight-line projections over weeks, months or years, normalising subscription costs to a period. Carries over every case from the web UI's test suite. --- src/lib.rs | 1 + src/projection.rs | 294 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 295 insertions(+) create mode 100644 src/projection.rs diff --git a/src/lib.rs b/src/lib.rs index 22649d9..6b4bebc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,5 +5,6 @@ pub mod format; pub mod import; pub mod paging; pub mod prefs; +pub mod projection; pub mod seed; pub mod subscriptions; diff --git a/src/projection.rs b/src/projection.rs new file mode 100644 index 0000000..8af86f4 --- /dev/null +++ b/src/projection.rs @@ -0,0 +1,294 @@ +//! Balance projections: a straight line from a starting balance, income and +//! expense per period. +use chrono::{Duration, NaiveDate}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PeriodType { + Weeks, + Months, + Years, +} + +impl PeriodType { + pub const ALL: [PeriodType; 3] = [PeriodType::Weeks, PeriodType::Months, PeriodType::Years]; + + pub fn value(self) -> &'static str { + match self { + PeriodType::Weeks => "weeks", + PeriodType::Months => "months", + PeriodType::Years => "years", + } + } + + pub fn from_value(value: &str) -> Option { + Self::ALL.into_iter().find(|p| p.value() == value) + } + + /// Singular: "week", "month", "year". + pub fn unit(self) -> &'static str { + match self { + PeriodType::Weeks => "week", + PeriodType::Months => "month", + PeriodType::Years => "year", + } + } + + /// Plural: "weeks", "months", "years". + pub fn plural(self) -> &'static str { + self.value() + } + + pub fn limit(self) -> u32 { + match self { + PeriodType::Weeks => 260, + PeriodType::Months => 600, + PeriodType::Years => 60, + } + } + + fn days(self) -> i64 { + match self { + PeriodType::Weeks => 7, + PeriodType::Months => 30, + PeriodType::Years => 365, + } + } + + fn per_year(self) -> f64 { + match self { + PeriodType::Weeks => 52.0, + PeriodType::Months => 12.0, + PeriodType::Years => 1.0, + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct ProjectionInput { + pub starting_balance: i64, + pub income: i64, + pub expense: i64, + pub period_type: PeriodType, + pub periods: u32, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ProjectionPoint { + pub step: u32, + pub label: String, + pub balance: i64, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ProjectionSummary { + pub net_per_period: i64, + pub final_balance: i64, + pub total_increase: i64, + /// Share of income kept, as a percentage. `None` without income. + pub retention_rate: Option, + pub projected_date: NaiveDate, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ProjectionResult { + pub points: Vec, + pub summary: ProjectionSummary, + pub max_periods: u32, +} + +pub fn period_label(period_type: PeriodType, step: u32) -> String { + match step { + 0 => "Now".into(), + 1 => format!("1 {}", period_type.unit()), + n => format!("{n} {}s", period_type.unit()), + } +} + +/// A subscription's amount expressed per projection period, in cents. +/// Unknown frequencies contribute nothing. +pub fn normalize_subscription_to_period(amount: i64, frequency: &str, period_type: PeriodType) -> i64 { + let per_year = match frequency { + "weekly" => 52.0, + "bi_weekly" => 26.0, + "monthly" => 12.0, + "quarterly" => 4.0, + "bi_yearly" => 2.0, + "yearly" => 1.0, + _ => return 0, + }; + (amount as f64 * per_year / period_type.per_year()).round() as i64 +} + +pub fn compute_projection(input: ProjectionInput, today: NaiveDate) -> ProjectionResult { + let ProjectionInput { starting_balance, income, expense, period_type, periods } = input; + let net = income - expense; + let points = (0..=periods) + .map(|step| ProjectionPoint { + step, + label: period_label(period_type, step), + balance: starting_balance + net * i64::from(step), + }) + .collect(); + let final_balance = starting_balance + net * i64::from(periods); + ProjectionResult { + points, + summary: ProjectionSummary { + net_per_period: net, + final_balance, + total_increase: final_balance - starting_balance, + retention_rate: (income > 0).then(|| net as f64 / income as f64 * 100.0), + projected_date: today + Duration::days(period_type.days() * i64::from(periods)), + }, + max_periods: period_type.limit(), + } +} + +/// Compact axis label: `$950`, `-$1.5k`, `$2.0M`. +pub fn format_axis(cents: i64) -> String { + let dollars = cents.unsigned_abs() as f64 / 100.0; + let sign = if cents < 0 { "-" } else { "" }; + if dollars >= 1_000_000.0 { + format!("{sign}${:.1}M", dollars / 1_000_000.0) + } else if dollars >= 1000.0 { + format!("{sign}${:.1}k", dollars / 1000.0) + } else { + format!("{sign}${dollars:.0}") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn today() -> NaiveDate { + NaiveDate::from_ymd_opt(2026, 10, 2).unwrap() + } + + fn input(income: i64, expense: i64, period_type: PeriodType, periods: u32) -> ProjectionInput { + ProjectionInput { starting_balance: 0, income, expense, period_type, periods } + } + + fn balances(r: &ProjectionResult) -> Vec { + r.points.iter().map(|p| p.balance).collect() + } + + #[test] + fn canonical_example() { + let r = compute_projection(input(120000, 10000, PeriodType::Months, 10), today()); + assert_eq!(r.summary.net_per_period, 110000); + assert_eq!(r.summary.final_balance, 1100000); + assert_eq!(r.summary.total_increase, 1100000); + assert_eq!(r.points.len(), 11); + assert_eq!(r.points[0].label, "Now"); + assert_eq!(r.points[0].balance, 0); + assert_eq!(r.points[10].label, "10 months"); + assert_eq!(r.points[10].balance, 1100000); + } + + #[test] + fn balances_increase_linearly() { + let r = compute_projection(input(300000, 120000, PeriodType::Weeks, 4), today()); + assert_eq!(balances(&r), vec![0, 180000, 360000, 540000, 720000]); + let labels: Vec<_> = r.points.iter().map(|p| p.label.as_str()).collect(); + assert_eq!(labels, ["Now", "1 week", "2 weeks", "3 weeks", "4 weeks"]); + } + + #[test] + fn offsets_by_starting_balance() { + let r = compute_projection( + ProjectionInput { starting_balance: 500000, income: 100000, expense: 0, period_type: PeriodType::Months, periods: 3 }, + today(), + ); + assert_eq!(r.summary.final_balance, 800000); + assert_eq!(r.summary.total_increase, 300000); + assert_eq!(r.points[0].balance, 500000); + assert_eq!(r.points[3].balance, 800000); + } + + #[test] + fn flat_when_income_equals_expense() { + let r = compute_projection(input(50000, 50000, PeriodType::Months, 5), today()); + assert_eq!(r.summary.net_per_period, 0); + assert!(r.points.iter().all(|p| p.balance == 0)); + } + + #[test] + fn declines_when_expenses_exceed_income() { + let r = compute_projection(input(20000, 50000, PeriodType::Months, 4), today()); + assert_eq!(r.summary.net_per_period, -30000); + assert_eq!(r.summary.final_balance, -120000); + assert_eq!(balances(&r), vec![0, -30000, -60000, -90000, -120000]); + } + + #[test] + fn retention_rate() { + let r = compute_projection(input(120000, 10000, PeriodType::Months, 10), today()); + assert!((r.summary.retention_rate.unwrap() - 91.6667).abs() < 0.001); + let none = compute_projection(input(0, 10000, PeriodType::Months, 10), today()); + assert_eq!(none.summary.retention_rate, None); + } + + #[test] + fn years_period_type() { + let r = compute_projection(input(1200000, 0, PeriodType::Years, 2), today()); + let labels: Vec<_> = r.points.iter().map(|p| p.label.as_str()).collect(); + assert_eq!(labels, ["Now", "1 year", "2 years"]); + assert_eq!(r.summary.final_balance, 2400000); + } + + #[test] + fn respects_period_limits() { + assert_eq!(PeriodType::Months.limit(), 600); + let r = compute_projection(input(0, 0, PeriodType::Months, 600), today()); + assert_eq!(r.max_periods, 600); + assert_eq!(r.points.len(), 601); + } + + #[test] + fn projected_date_is_periods_ahead() { + let r = compute_projection(input(0, 0, PeriodType::Months, 10), today()); + assert_eq!(r.summary.projected_date, today() + Duration::days(300)); + } + + #[test] + fn labels() { + for p in PeriodType::ALL { + assert_eq!(period_label(p, 0), "Now"); + assert_eq!(period_label(p, 1), format!("1 {}", p.unit())); + assert_eq!(period_label(p, 3), format!("3 {}s", p.unit())); + } + } + + #[test] + fn normalizes_subscriptions() { + use PeriodType::*; + let n = normalize_subscription_to_period; + assert_eq!(n(10000, "monthly", Months), 10000); + assert_eq!(n(12000, "monthly", Weeks), 2769); + assert_eq!(n(10000, "monthly", Years), 120000); + assert_eq!(n(1200000, "yearly", Months), 100000); + assert_eq!(n(1000, "weekly", Months), 4333); + assert_eq!(n(2000, "bi_weekly", Months), 4333); + assert_eq!(n(30000, "quarterly", Months), 10000); + assert_eq!(n(60000, "bi_yearly", Months), 10000); + assert_eq!(n(500, "weekly", Weeks), 500); + assert_eq!(n(1, "monthly", Weeks), 0); + assert_eq!(n(10000, "daily", Months), 0); + } + + #[test] + fn axis_labels() { + assert_eq!(format_axis(95000), "$950"); + assert_eq!(format_axis(-150000), "-$1.5k"); + assert_eq!(format_axis(200000000), "$2.0M"); + assert_eq!(format_axis(0), "$0"); + } + + #[test] + fn period_type_round_trips() { + for p in PeriodType::ALL { + assert_eq!(PeriodType::from_value(p.value()), Some(p)); + } + assert_eq!(PeriodType::from_value("decades"), None); + } +} From 90f753b97a53f90d0de6a4d13a7b607b8489e479 Mon Sep 17 00:00:00 2001 From: Victor Phos Date: Fri, 2 Oct 2026 14:04:33 +0200 Subject: [PATCH 12/36] Add dashboard summaries for a date range Income, expense and net totals, savings rate (rounded half up like the web UI), per-category breakdowns sorted largest first, and type/account/date filtering of transactions. --- src/lib.rs | 1 + src/overview.rs | 226 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 227 insertions(+) create mode 100644 src/overview.rs diff --git a/src/lib.rs b/src/lib.rs index 6b4bebc..c2782b9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ pub mod database; pub mod date_filter; pub mod format; pub mod import; +pub mod overview; pub mod paging; pub mod prefs; pub mod projection; diff --git a/src/overview.rs b/src/overview.rs new file mode 100644 index 0000000..27f7c2c --- /dev/null +++ b/src/overview.rs @@ -0,0 +1,226 @@ +//! Dashboard figures for a chosen date range. +use crate::commands::{Category, Transaction}; +use crate::date_filter::is_date_in_range; +use std::collections::HashMap; + +#[derive(Debug, Clone, PartialEq)] +pub struct CategoryTotal { + pub category_id: String, + pub name: String, + pub color: Option, + pub icon: Option, + pub value: i64, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Summary { + pub total_income: i64, + pub total_expenses: i64, + pub net: i64, + /// Whole percent of income kept; zero without income. + pub savings_rate: i64, + pub income_by_category: Vec, + pub expenses_by_category: Vec, +} + +fn totals(map: HashMap, categories: &[Category]) -> Vec { + let mut list: Vec = map + .into_iter() + .map(|(category_id, value)| { + let category = categories.iter().find(|c| c.id == category_id); + CategoryTotal { + name: category.map_or_else(|| "Unknown".to_string(), |c| c.name.clone()), + color: category.and_then(|c| c.color.clone()), + icon: category.and_then(|c| c.icon.clone()), + category_id, + value, + } + }) + .collect(); + // Largest first; ties by name so the order is stable. + list.sort_by(|a, b| b.value.cmp(&a.value).then_with(|| a.name.cmp(&b.name))); + list +} + +/// Totals for transactions dated within `start..=end`. +pub fn summarize(transactions: &[Transaction], categories: &[Category], start: &str, end: &str) -> Summary { + let (mut income, mut expenses) = (0i64, 0i64); + let mut income_by: HashMap = HashMap::new(); + let mut expense_by: HashMap = HashMap::new(); + for t in transactions.iter().filter(|t| is_date_in_range(&t.transaction_date, start, end)) { + match t.transaction_type.as_str() { + "income" => { + income += t.amount; + if let Some(id) = &t.category_id { + *income_by.entry(id.clone()).or_insert(0) += t.amount; + } + } + "expense" => { + expenses += t.amount; + if let Some(id) = &t.category_id { + *expense_by.entry(id.clone()).or_insert(0) += t.amount; + } + } + _ => {} + } + } + let net = income - expenses; + Summary { + total_income: income, + total_expenses: expenses, + net, + // Round half up like the web UI (Math.round). + savings_rate: if income > 0 { (net as f64 / income as f64 * 100.0 + 0.5).floor() as i64 } else { 0 }, + income_by_category: totals(income_by, categories), + expenses_by_category: totals(expense_by, categories), + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TypeFilter { + All, + Income, + Expense, +} + +impl TypeFilter { + pub fn from_value(value: &str) -> Self { + match value { + "income" => TypeFilter::Income, + "expense" => TypeFilter::Expense, + _ => TypeFilter::All, + } + } +} + +/// Transactions in the date range, optionally narrowed by type and account. +pub fn filter_transactions<'a>( + transactions: &'a [Transaction], + start: &str, + end: &str, + kind: TypeFilter, + account_id: Option<&str>, +) -> Vec<&'a Transaction> { + transactions + .iter() + .filter(|t| is_date_in_range(&t.transaction_date, start, end)) + .filter(|t| match kind { + TypeFilter::All => true, + TypeFilter::Income => t.transaction_type == "income", + TypeFilter::Expense => t.transaction_type == "expense", + }) + .filter(|t| account_id.is_none_or(|id| t.account_id == id)) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + fn tx(id: &str, kind: &str, amount: i64, date: &str, cat: Option<&str>, account: &str) -> Transaction { + Transaction { + id: id.into(), + account_id: account.into(), + category_id: cat.map(String::from), + transaction_type: kind.into(), + amount, + currency: "USD".into(), + description: id.into(), + merchant: None, + notes: None, + transaction_date: date.into(), + created_at: String::new(), + updated_at: String::new(), + } + } + + fn cat(id: &str, name: &str, color: Option<&str>) -> Category { + Category { + id: id.into(), + name: name.into(), + parent_id: None, + category_type: "expense".into(), + icon: Some("tag".into()), + color: color.map(String::from), + sort_order: 0, + created_at: String::new(), + updated_at: String::new(), + } + } + + fn sample() -> Vec { + vec![ + tx("pay", "income", 200000, "2026-03-01", Some("job"), "a"), + tx("rent", "expense", 80000, "2026-03-02", Some("home"), "a"), + tx("food1", "expense", 10000, "2026-03-03", Some("food"), "b"), + tx("food2", "expense", 5000, "2026-03-04", Some("food"), "a"), + tx("old", "expense", 99999, "2025-01-01", Some("food"), "a"), + tx("loose", "expense", 700, "2026-03-05", None, "a"), + ] + } + + fn cats() -> Vec { + vec![cat("job", "Salary", Some("#9ece6a")), cat("home", "Rent", None), cat("food", "Food", None)] + } + + #[test] + fn totals_only_include_the_date_range() { + let s = summarize(&sample(), &cats(), "2026-03-01", "2026-03-31"); + assert_eq!(s.total_income, 200000); + assert_eq!(s.total_expenses, 80000 + 10000 + 5000 + 700); + assert_eq!(s.net, 200000 - 95700); + } + + #[test] + fn categories_are_grouped_and_sorted_largest_first() { + let s = summarize(&sample(), &cats(), "2026-03-01", "2026-03-31"); + let names: Vec<_> = s.expenses_by_category.iter().map(|c| (c.name.as_str(), c.value)).collect(); + assert_eq!(names, vec![("Rent", 80000), ("Food", 15000)], "uncategorised spending is not charted"); + assert_eq!(s.income_by_category.len(), 1); + assert_eq!(s.income_by_category[0].color.as_deref(), Some("#9ece6a")); + } + + #[test] + fn unknown_categories_get_a_placeholder_name() { + let t = vec![tx("x", "expense", 100, "2026-03-01", Some("deleted"), "a")]; + let s = summarize(&t, &[], "2026-03-01", "2026-03-31"); + assert_eq!(s.expenses_by_category[0].name, "Unknown"); + } + + #[test] + fn savings_rate_rounds_half_up_and_handles_no_income() { + let s = summarize(&sample(), &cats(), "2026-03-01", "2026-03-31"); + assert_eq!(s.savings_rate, 52, "(200000-95700)/200000 = 52.15%"); + let none = summarize(&sample(), &cats(), "2026-04-01", "2026-04-30"); + assert_eq!((none.total_income, none.savings_rate), (0, 0)); + let half = vec![tx("i", "income", 200, "2026-03-01", None, "a"), tx("e", "expense", 101, "2026-03-01", None, "a")]; + assert_eq!(summarize(&half, &[], "2026-03-01", "2026-03-31").savings_rate, 50, "49.5 rounds up"); + } + + #[test] + fn overspending_gives_a_negative_rate() { + let t = vec![tx("i", "income", 100, "2026-03-01", None, "a"), tx("e", "expense", 250, "2026-03-01", None, "a")]; + let s = summarize(&t, &[], "2026-03-01", "2026-03-31"); + assert_eq!((s.net, s.savings_rate), (-150, -150)); + } + + #[test] + fn filters_combine_range_type_and_account() { + let all = sample(); + let ids = |v: Vec<&Transaction>| v.into_iter().map(|t| t.id.clone()).collect::>(); + let (s, e) = ("2026-03-01", "2026-03-31"); + assert_eq!(ids(filter_transactions(&all, s, e, TypeFilter::All, None)).len(), 5); + assert_eq!(ids(filter_transactions(&all, s, e, TypeFilter::Income, None)), vec!["pay"]); + assert_eq!(ids(filter_transactions(&all, s, e, TypeFilter::Expense, Some("b"))), vec!["food1"]); + assert!(filter_transactions(&all, s, e, TypeFilter::Income, Some("b")).is_empty()); + assert_eq!(ids(filter_transactions(&all, "0000-01-01", "9999-12-31", TypeFilter::All, None)).len(), 6); + } + + #[test] + fn type_filter_parses_values() { + assert_eq!(TypeFilter::from_value("income"), TypeFilter::Income); + assert_eq!(TypeFilter::from_value("expense"), TypeFilter::Expense); + assert_eq!(TypeFilter::from_value("all"), TypeFilter::All); + assert_eq!(TypeFilter::from_value("???"), TypeFilter::All); + } +} From 5b4b3947978f51bdb0ad65514049a9191988f823 Mon Sep 17 00:00:00 2001 From: Victor Phos Date: Fri, 2 Oct 2026 14:04:34 +0200 Subject: [PATCH 13/36] Add CSV and JSON export Transactions to CSV with account and category names resolved and fields quoted where needed, or the whole dataset to JSON. --- src/export.rs | 170 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 2 files changed, 171 insertions(+) create mode 100644 src/export.rs diff --git a/src/export.rs b/src/export.rs new file mode 100644 index 0000000..59ad436 --- /dev/null +++ b/src/export.rs @@ -0,0 +1,170 @@ +//! Data export formats. +use crate::commands::{Account, Category, ExportData, Transaction}; +use std::collections::HashMap; + +fn escape_csv_field(value: &str) -> String { + if value.contains(['"', ',', '\n', '\r']) { + format!("\"{}\"", value.replace('"', "\"\"")) + } else { + value.to_string() + } +} + +pub fn transactions_to_csv(transactions: &[Transaction], accounts: &[Account], categories: &[Category]) -> String { + let account_names: HashMap<&str, &str> = accounts.iter().map(|a| (a.id.as_str(), a.name.as_str())).collect(); + let category_names: HashMap<&str, &str> = categories.iter().map(|c| (c.id.as_str(), c.name.as_str())).collect(); + + let headers = [ + "id", "transaction_date", "description", "merchant", "transaction_type", "amount", "currency", + "account_name", "category_name", "notes", "created_at", "updated_at", + ]; + let mut lines = vec![headers.join(",")]; + for t in transactions { + let fields = [ + t.id.clone(), + t.transaction_date.clone(), + t.description.clone(), + t.merchant.clone().unwrap_or_default(), + t.transaction_type.clone(), + format!("{:.2}", t.amount as f64 / 100.0), + t.currency.clone(), + account_names.get(t.account_id.as_str()).map_or_else(|| t.account_id.clone(), |n| n.to_string()), + t.category_id + .as_deref() + .map(|id| category_names.get(id).map_or_else(|| id.to_string(), |n| n.to_string())) + .unwrap_or_default(), + t.notes.clone().unwrap_or_default(), + t.created_at.clone(), + t.updated_at.clone(), + ]; + lines.push(fields.iter().map(|f| escape_csv_field(f)).collect::>().join(",")); + } + lines.join("\n") +} + +pub fn export_to_json(data: &ExportData) -> Result { + serde_json::to_string_pretty(data).map_err(|e| e.to_string()) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ExportFormat { + Csv, + Json, +} + +impl ExportFormat { + pub fn default_file_name(self) -> &'static str { + match self { + ExportFormat::Csv => "chelete-transactions.csv", + ExportFormat::Json => "chelete-export.json", + } + } + + pub fn contents(self, data: &ExportData) -> Result { + match self { + ExportFormat::Csv => Ok(transactions_to_csv(&data.transactions, &data.accounts, &data.categories)), + ExportFormat::Json => export_to_json(data), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn txn(desc: &str, merchant: Option<&str>, category: Option<&str>) -> Transaction { + Transaction { + id: "t1".into(), + account_id: "a1".into(), + category_id: category.map(String::from), + transaction_type: "expense".into(), + amount: 1234, + currency: "USD".into(), + description: desc.into(), + merchant: merchant.map(String::from), + notes: None, + transaction_date: "2026-03-09".into(), + created_at: "c".into(), + updated_at: "u".into(), + } + } + + fn account() -> Account { + Account { + id: "a1".into(), + name: "Main".into(), + account_type: "bank".into(), + currency: "USD".into(), + balance: 0, + color: None, + icon: None, + is_active: true, + sort_order: 0, + created_at: String::new(), + updated_at: String::new(), + } + } + + fn category() -> Category { + Category { + id: "c1".into(), + name: "Food".into(), + parent_id: None, + category_type: "expense".into(), + icon: None, + color: None, + sort_order: 0, + created_at: String::new(), + updated_at: String::new(), + } + } + + #[test] + fn csv_has_header_and_resolved_names() { + let csv = transactions_to_csv(&[txn("Lunch", Some("Cafe"), Some("c1"))], &[account()], &[category()]); + let mut lines = csv.lines(); + assert_eq!( + lines.next().unwrap(), + "id,transaction_date,description,merchant,transaction_type,amount,currency,account_name,category_name,notes,created_at,updated_at" + ); + assert_eq!(lines.next().unwrap(), "t1,2026-03-09,Lunch,Cafe,expense,12.34,USD,Main,Food,,c,u"); + assert!(lines.next().is_none()); + } + + #[test] + fn csv_quotes_commas_quotes_and_newlines() { + let csv = transactions_to_csv(&[txn("Dinner, \"fancy\"\nlate", None, None)], &[account()], &[]); + assert!(csv.contains("\"Dinner, \"\"fancy\"\"\nlate\""), "{csv}"); + } + + #[test] + fn unknown_ids_fall_back_to_the_raw_id() { + let csv = transactions_to_csv(&[txn("x", None, Some("gone"))], &[], &[]); + assert!(csv.contains(",a1,gone,"), "{csv}"); + } + + #[test] + fn empty_export_is_just_the_header() { + assert_eq!(transactions_to_csv(&[], &[], &[]).lines().count(), 1); + } + + #[test] + fn json_round_trips() { + let data = ExportData { + accounts: vec![account()], + categories: vec![category()], + transactions: vec![txn("Lunch", None, None)], + subscriptions: vec![], + }; + let json = export_to_json(&data).unwrap(); + let back: serde_json::Value = serde_json::from_str(&json).unwrap(); + assert_eq!(back["transactions"][0]["description"], "Lunch"); + assert_eq!(back["accounts"][0]["name"], "Main"); + } + + #[test] + fn file_names() { + assert_eq!(ExportFormat::Csv.default_file_name(), "chelete-transactions.csv"); + assert_eq!(ExportFormat::Json.default_file_name(), "chelete-export.json"); + } +} diff --git a/src/lib.rs b/src/lib.rs index c2782b9..710329d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ pub mod commands; pub mod database; pub mod date_filter; +pub mod export; pub mod format; pub mod import; pub mod overview; From 6c6fe6f9286a6489c2569ce916987757a083a98e Mon Sep 17 00:00:00 2001 From: Victor Phos Date: Fri, 2 Oct 2026 14:04:35 +0200 Subject: [PATCH 14/36] Add CSV import session rules Column assignment (one column per field), readiness for preview, duplicate flagging within a file, and per-layout remembered mappings, on top of the existing parser. Tested against real CSV files. --- src/import/mod.rs | 3 +- src/import/session.rs | 291 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 293 insertions(+), 1 deletion(-) create mode 100644 src/import/session.rs diff --git a/src/import/mod.rs b/src/import/mod.rs index bb3826d..6e69147 100644 --- a/src/import/mod.rs +++ b/src/import/mod.rs @@ -1,5 +1,6 @@ pub mod mapping; pub mod parser; +pub mod session; -pub use mapping::CsvMapping; +pub use mapping::{CsvField, CsvMapping}; pub use parser::{parse_csv_preview, parse_csv_rows, AmountSource, CsvPreview, ImportResult, ParsedRow}; diff --git a/src/import/session.rs b/src/import/session.rs new file mode 100644 index 0000000..e6ab9b2 --- /dev/null +++ b/src/import/session.rs @@ -0,0 +1,291 @@ +//! Rules the CSV import dialog applies on top of the parser: column +//! assignment, readiness, duplicate flagging and remembered mappings. +use super::{AmountSource, CsvField, CsvMapping, ParsedRow}; +use std::collections::HashMap; +use std::path::Path; + +/// Map `header` to `field`, or unmap it. A field can only come from one +/// column, so any other column using it is released. +pub fn set_column(mapping: &mut CsvMapping, header: &str, field: Option) { + match field { + None => { + mapping.columns.remove(header); + } + Some(field) => { + mapping.columns.retain(|_, f| *f != field); + mapping.columns.insert(header.to_string(), field); + } + } +} + +/// Date, description, an amount, and some way to tell income from expense. +pub fn is_valid_for_preview(mapping: &CsvMapping) -> bool { + let has = |f| mapping.field_column(f).is_some(); + let has_amount = has(CsvField::Amount) || has(CsvField::IncomeAmount) || has(CsvField::ExpenseAmount); + let has_type = has(CsvField::Type) + || has(CsvField::IncomeAmount) + || has(CsvField::ExpenseAmount) + || mapping.default_transaction_type.is_some(); + has(CsvField::Date) && has(CsvField::Description) && has_amount && has_type +} + +fn effective_account(row: &ParsedRow, mapping: &CsvMapping) -> Option { + if let Some(account) = row.account.as_deref().map(str::trim).filter(|a| !a.is_empty()) { + return Some(account.to_lowercase()); + } + mapping.default_account_id.as_ref().map(|id| format!("__default:{id}")) +} + +/// A row repeats an earlier row: same date, amount, description and account. +/// Rows with no account source are never flagged, or every blank-account row +/// would look identical. +pub fn is_duplicate_preview(row: &ParsedRow, all: &[ParsedRow], mapping: &CsvMapping) -> bool { + let (Some(date), Some(amount), Some(description)) = (&row.date, row.amount_cents, &row.description) else { + return false; + }; + let Some(account) = effective_account(row, mapping) else { + return false; + }; + let description = description.trim().to_lowercase(); + all.iter().any(|other| { + other.row_index < row.row_index + && other.date.as_ref() == Some(date) + && other.amount_cents == Some(amount) + && other.description.as_deref().map(|d| d.trim().to_lowercase()).as_deref() == Some(description.as_str()) + && effective_account(other, mapping).as_deref() == Some(account.as_str()) + }) +} + +pub fn flag_duplicates(rows: &[ParsedRow], mapping: &CsvMapping) -> Vec { + rows.iter().map(|r| is_duplicate_preview(r, rows, mapping)).collect() +} + +/// Rows that would import: no errors and not a duplicate. +pub fn valid_count(rows: &[ParsedRow], duplicates: &[bool]) -> usize { + rows.iter() + .zip(duplicates) + .filter(|(r, dup)| r.errors.is_empty() && !**dup) + .count() +} + +/// What the preview's Type column shows. Only separate income/expense columns +/// tell us the type before import. +pub fn type_label(row: &ParsedRow) -> &'static str { + match row.amount_source { + Some(AmountSource::IncomeColumn) => "income", + Some(AmountSource::ExpenseColumn) => "expense", + _ => "—", + } +} + +/// Identifies a CSV layout by its headers, ignoring case and punctuation. +pub fn mapping_key(headers: &[String]) -> String { + headers + .iter() + .map(|h| h.trim().to_lowercase().chars().filter(|c| c.is_ascii_alphanumeric()).collect::()) + .collect::>() + .join(",") +} + +/// Column mappings remembered per CSV layout. +#[derive(Debug, Default)] +pub struct SavedMappings(HashMap); + +impl SavedMappings { + pub fn load_from(path: &Path) -> Self { + let map = std::fs::read_to_string(path) + .ok() + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or_default(); + Self(map) + } + + pub fn save_to(&self, path: &Path) -> Result<(), String> { + if let Some(dir) = path.parent() { + std::fs::create_dir_all(dir).map_err(|e| e.to_string())?; + } + let json = serde_json::to_string_pretty(&self.0).map_err(|e| e.to_string())?; + std::fs::write(path, json).map_err(|e| e.to_string()) + } + + pub fn get(&self, headers: &[String]) -> Option<&CsvMapping> { + self.0.get(&mapping_key(headers)) + } + + pub fn insert(&mut self, headers: &[String], mapping: CsvMapping) { + self.0.insert(mapping_key(headers), mapping); + } + + pub fn default_path() -> std::path::PathBuf { + crate::database::data_dir().join("csv-mappings.json") + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::import::{parse_csv_preview, parse_csv_rows}; + + fn headers(list: &[&str]) -> Vec { + list.iter().map(|s| s.to_string()).collect() + } + + fn row(index: usize, date: &str, desc: &str, cents: i64, account: Option<&str>) -> ParsedRow { + ParsedRow { + row_index: index, + date: Some(date.into()), + description: Some(desc.into()), + merchant: None, + transaction_type: None, + amount_cents: Some(cents), + amount_source: None, + currency: None, + category: None, + account: account.map(String::from), + notes: None, + errors: vec![], + } + } + + #[test] + fn a_field_comes_from_one_column_only() { + let mut m = CsvMapping::from_headers(&headers(&["Date", "Memo", "Amount"])); + set_column(&mut m, "Memo", Some(CsvField::Amount)); + assert_eq!(m.columns.get("Memo"), Some(&CsvField::Amount)); + assert_eq!(m.columns.get("Amount"), None, "previous amount column released"); + assert_eq!(m.field_column(CsvField::Amount), Some("Memo")); + set_column(&mut m, "Memo", None); + assert_eq!(m.field_column(CsvField::Amount), None); + } + + #[test] + fn readiness_needs_date_description_amount_and_a_type_source() { + let mut m = CsvMapping::default(); + assert!(!is_valid_for_preview(&m)); + set_column(&mut m, "d", Some(CsvField::Date)); + set_column(&mut m, "p", Some(CsvField::Description)); + set_column(&mut m, "a", Some(CsvField::Amount)); + assert!(!is_valid_for_preview(&m), "amount alone does not say income or expense"); + m.default_transaction_type = Some("expense".into()); + assert!(is_valid_for_preview(&m)); + m.default_transaction_type = None; + set_column(&mut m, "t", Some(CsvField::Type)); + assert!(is_valid_for_preview(&m)); + } + + #[test] + fn separate_income_and_expense_columns_are_enough() { + let mut m = CsvMapping::default(); + set_column(&mut m, "d", Some(CsvField::Date)); + set_column(&mut m, "p", Some(CsvField::Description)); + set_column(&mut m, "in", Some(CsvField::IncomeAmount)); + assert!(is_valid_for_preview(&m)); + } + + #[test] + fn duplicates_match_on_date_amount_description_and_account() { + let m = CsvMapping::default(); + let rows = vec![ + row(1, "2026-03-01", "Coffee", 450, Some("Main")), + row(2, "2026-03-01", " coffee ", 450, Some("main")), + row(3, "2026-03-01", "Coffee", 451, Some("Main")), + row(4, "2026-03-02", "Coffee", 450, Some("Main")), + row(5, "2026-03-01", "Coffee", 450, Some("Other")), + ]; + assert_eq!(flag_duplicates(&rows, &m), vec![false, true, false, false, false]); + assert_eq!(valid_count(&rows, &flag_duplicates(&rows, &m)), 4); + } + + #[test] + fn rows_without_an_account_source_are_never_duplicates() { + let m = CsvMapping::default(); + let rows = vec![row(1, "2026-03-01", "A", 1, None), row(2, "2026-03-01", "A", 1, None)]; + assert_eq!(flag_duplicates(&rows, &m), vec![false, false]); + } + + #[test] + fn a_default_account_makes_blank_accounts_comparable() { + let mut m = CsvMapping::default(); + m.default_account_id = Some("acc-1".into()); + let rows = vec![row(1, "2026-03-01", "A", 1, None), row(2, "2026-03-01", "A", 1, Some(""))]; + assert_eq!(flag_duplicates(&rows, &m), vec![false, true]); + } + + #[test] + fn rows_with_errors_do_not_count_as_valid() { + let m = CsvMapping::default(); + let mut bad = row(1, "2026-03-01", "A", 1, Some("x")); + bad.errors.push("bad date".into()); + let rows = vec![bad, row(2, "2026-03-02", "B", 2, Some("x"))]; + assert_eq!(valid_count(&rows, &flag_duplicates(&rows, &m)), 1); + } + + #[test] + fn type_label_reflects_split_amount_columns() { + let mut r = row(1, "d", "x", 1, None); + assert_eq!(type_label(&r), "—"); + r.amount_source = Some(AmountSource::IncomeColumn); + assert_eq!(type_label(&r), "income"); + r.amount_source = Some(AmountSource::ExpenseColumn); + assert_eq!(type_label(&r), "expense"); + } + + #[test] + fn keys_ignore_case_and_punctuation() { + assert_eq!(mapping_key(&headers(&["Transaction Date", "Amount ($)"])), "transactiondate,amount"); + assert_eq!(mapping_key(&headers(&["transaction_date", "AMOUNT"])), "transactiondate,amount"); + } + + #[test] + fn saved_mappings_round_trip_on_disk() { + let dir = std::env::temp_dir().join(format!("chelete-mappings-{}", std::process::id())); + let path = dir.join("m.json"); + let hs = headers(&["Date", "Memo", "Amount"]); + let mut saved = SavedMappings::load_from(&path); + assert!(saved.get(&hs).is_none(), "missing file starts empty"); + let mut mapping = CsvMapping::from_headers(&hs); + mapping.default_account_id = Some("acc".into()); + saved.insert(&hs, mapping); + saved.save_to(&path).unwrap(); + + let loaded = SavedMappings::load_from(&path); + let got = loaded.get(&headers(&["date", "MEMO", "amount"])).expect("same layout, different case"); + assert_eq!(got.default_account_id.as_deref(), Some("acc")); + assert_eq!(got.columns.get("Date"), Some(&CsvField::Date)); + assert!(loaded.get(&headers(&["Other"])).is_none()); + std::fs::remove_dir_all(&dir).ok(); + } + + #[test] + fn corrupt_saved_file_is_ignored() { + let dir = std::env::temp_dir().join(format!("chelete-mappings-bad-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("m.json"); + std::fs::write(&path, "{ not json").unwrap(); + assert!(SavedMappings::load_from(&path).get(&headers(&["a"])).is_none()); + std::fs::remove_dir_all(&dir).ok(); + } + + #[test] + fn works_end_to_end_on_a_real_csv() { + let dir = std::env::temp_dir().join(format!("chelete-csv-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("bank.csv"); + std::fs::write( + &path, + "Date,Payee,Debit,Credit,Account\n2026-03-01,Coffee,4.50,,Main\n2026-03-01,Coffee,4.50,,Main\n2026-03-02,Salary,,2000.00,Main\n", + ) + .unwrap(); + let p = path.to_string_lossy().to_string(); + + let first = parse_csv_preview(&p, &CsvMapping::default()).unwrap(); + let mapping = CsvMapping::from_headers(&first.headers); + assert!(is_valid_for_preview(&mapping), "headers are recognised automatically"); + + let rows = parse_csv_rows(&p, &mapping).unwrap(); + assert_eq!(rows.len(), 3); + assert_eq!(flag_duplicates(&rows, &mapping), vec![false, true, false]); + assert_eq!(type_label(&rows[2]), "income"); + std::fs::remove_dir_all(&dir).ok(); + } +} From b4d964834ee903554f36866aadc33e02d51278a0 Mon Sep 17 00:00:00 2001 From: Victor Phos Date: Fri, 2 Oct 2026 14:04:35 +0200 Subject: [PATCH 15/36] Skip repeated rows within a single CSV import The import compared rows only against transactions already in the database, so a file that repeated a row imported it twice even though the preview marked it as a duplicate. Rows imported earlier in the same file now count. Adds tests for the transaction, transfer, import and overview commands. --- src/commands/mod.rs | 175 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 174 insertions(+), 1 deletion(-) diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 8def7ad..a7a7632 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1153,7 +1153,7 @@ pub fn import_transactions( let accounts = load_accounts(&tx)?; let categories = load_categories(&tx)?; - let existing = load_existing_transactions(&tx)?; + let mut existing = load_existing_transactions(&tx)?; let mut imported = 0usize; let mut skipped = 0usize; @@ -1237,6 +1237,8 @@ pub fn import_transactions( params![balance_change, account_id], ).map_err(|e| e.to_string())?; + // Later rows in the same file are duplicates of this one too. + existing.push((account_id, date, amount_cents, description)); imported += 1; } @@ -1518,3 +1520,174 @@ fn load_subscriptions_for_export(conn: &rusqlite::Connection) -> Result DbState { + let conn = Connection::open_in_memory().unwrap(); + conn.execute_batch("PRAGMA foreign_keys=ON;").unwrap(); + crate::database::run_migrations(&conn).unwrap(); + crate::seed::seed_database(&conn).unwrap(); + DbState(Mutex::new(conn)) + } + + fn csv(name: &str, body: &str) -> String { + let dir = std::env::temp_dir().join(format!("chelete-cmd-{name}-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("in.csv"); + std::fs::write(&path, body).unwrap(); + path.to_string_lossy().to_string() + } + + fn mapping_for(path: &str, account_id: &str) -> CsvMapping { + let preview = crate::import::parse_csv_preview(path, &CsvMapping::default()).unwrap(); + let mut mapping = CsvMapping::from_headers(&preview.headers); + mapping.default_account_id = Some(account_id.to_string()); + mapping + } + + fn options(account_id: &str, skip: bool) -> ImportOptions { + ImportOptions { + default_account_id: Some(account_id.into()), + default_category_id: None, + default_currency: Some("USD".into()), + skip_duplicates: skip, + } + } + + const REPEATS: &str = "Date,Description,Debit,Credit\n2026-03-01,Coffee,4.50,\n2026-03-01,Coffee,4.50,\n2026-03-02,Salary,,2000.00\n"; + + #[test] + fn import_skips_repeats_within_the_same_file() { + let db = seeded(); + let account = get_accounts(&db).unwrap().remove(0); + let path = csv("within", REPEATS); + let result = import_transactions(&db, path.clone(), mapping_for(&path, &account.id), options(&account.id, true)).unwrap(); + assert_eq!((result.imported, result.skipped), (2, 1)); + let after = get_accounts(&db).unwrap().into_iter().find(|a| a.id == account.id).unwrap(); + assert_eq!(after.balance, account.balance - 450 + 200000, "only one coffee hit the balance"); + } + + #[test] + fn import_skips_rows_already_in_the_database() { + let db = seeded(); + let account = get_accounts(&db).unwrap().remove(0); + let path = csv("existing", REPEATS); + let m = mapping_for(&path, &account.id); + import_transactions(&db, path.clone(), m.clone(), options(&account.id, true)).unwrap(); + let again = import_transactions(&db, path, m, options(&account.id, true)).unwrap(); + assert_eq!((again.imported, again.skipped), (0, 3)); + } + + #[test] + fn import_without_skipping_keeps_every_row() { + let db = seeded(); + let account = get_accounts(&db).unwrap().remove(0); + let before = get_transactions(&db).unwrap().len(); + let path = csv("keepall", REPEATS); + let result = import_transactions(&db, path.clone(), mapping_for(&path, &account.id), options(&account.id, false)).unwrap(); + assert_eq!((result.imported, result.skipped), (3, 0)); + assert_eq!(get_transactions(&db).unwrap().len(), before + 3); + } + + #[test] + fn same_row_on_a_different_account_is_not_a_duplicate() { + let db = seeded(); + let accounts = get_accounts(&db).unwrap(); + let path = csv("twoaccounts", "Date,Description,Debit,Credit\n2026-03-01,Coffee,4.50,\n"); + let first = import_transactions(&db, path.clone(), mapping_for(&path, &accounts[0].id), options(&accounts[0].id, true)).unwrap(); + let second = import_transactions(&db, path.clone(), mapping_for(&path, &accounts[1].id), options(&accounts[1].id, true)).unwrap(); + assert_eq!((first.imported, second.imported), (1, 1)); + } + + fn new_txn(account_id: &str, kind: &str, amount: i64) -> CreateTransactionRequest { + CreateTransactionRequest { + account_id: account_id.into(), + category_id: None, + transaction_type: kind.into(), + amount, + currency: "USD".into(), + description: "t".into(), + merchant: None, + notes: None, + transaction_date: "2026-03-01".into(), + } + } + + fn balance(db: &DbState, id: &str) -> i64 { + get_accounts(db).unwrap().into_iter().find(|a| a.id == id).unwrap().balance + } + + #[test] + fn transactions_move_the_account_balance_and_deleting_reverses_it() { + let db = seeded(); + let id = get_accounts(&db).unwrap().remove(0).id; + let start = balance(&db, &id); + let expense = create_transaction(&db, new_txn(&id, "expense", 1000)).unwrap(); + let income = create_transaction(&db, new_txn(&id, "income", 2500)).unwrap(); + assert_eq!(balance(&db, &id), start - 1000 + 2500); + delete_transactions(&db, vec![expense.id, income.id]).unwrap(); + assert_eq!(balance(&db, &id), start); + } + + #[test] + fn editing_an_amount_adjusts_the_balance_by_the_difference() { + let db = seeded(); + let id = get_accounts(&db).unwrap().remove(0).id; + let t = create_transaction(&db, new_txn(&id, "expense", 1000)).unwrap(); + let after_create = balance(&db, &id); + update_transaction( + &db, + UpdateTransactionRequest { + id: t.id, + account_id: None, + category_id: None, + transaction_type: None, + amount: Some(1600), + currency: None, + description: None, + merchant: None, + notes: None, + transaction_date: None, + }, + ) + .unwrap(); + assert_eq!(balance(&db, &id), after_create - 600); + } + + #[test] + fn transfer_moves_money_and_rejects_the_same_account() { + let db = seeded(); + let accounts = get_accounts(&db).unwrap(); + let (a, b) = (accounts[0].id.clone(), accounts[1].id.clone()); + let (sa, sb) = (balance(&db, &a), balance(&db, &b)); + transfer(&db, a.clone(), b.clone(), 700, "USD".into(), None).unwrap(); + assert_eq!((balance(&db, &a), balance(&db, &b)), (sa - 700, sb + 700)); + assert!(transfer(&db, a.clone(), a, 100, "USD".into(), None).is_err()); + } + + #[test] + fn deleted_accounts_and_categories_disappear_from_lists() { + let db = seeded(); + let account = get_accounts(&db).unwrap().remove(0); + delete_account(&db, account.id.clone()).unwrap(); + assert!(get_accounts(&db).unwrap().iter().all(|a| a.id != account.id)); + let category = get_categories(&db).unwrap().remove(0); + delete_category(&db, category.id.clone()).unwrap(); + assert!(get_categories(&db).unwrap().iter().all(|c| c.id != category.id)); + } + + #[test] + fn overview_totals_match_the_transactions() { + let db = seeded(); + let overview = get_overview(&db).unwrap(); + let accounts_total: i64 = get_accounts(&db).unwrap().iter().map(|a| a.balance).sum(); + assert_eq!(overview.total_balance, accounts_total); + assert!(overview.total_income > 0 && overview.total_expenses > 0); + } +} From 8bdc89f6cebd3c6648d8a4f473feda39853c158d Mon Sep 17 00:00:00 2001 From: Victor Phos Date: Fri, 2 Oct 2026 14:05:27 +0200 Subject: [PATCH 16/36] Add the GPUI app shell A sidebar for the seven views with Lucide icons, Ctrl+O/T/A/C/U/P/S navigation, Ctrl+B to collapse (remembered between runs), and a Ctrl+K command palette. The Overview shows balances and recent transactions from the existing backend; the other views are placeholders for now. Headless tests drive the real keybindings. --- Cargo.toml | 5 + src/main.rs | 43 +++++++- src/ui/mod.rs | 113 +++++++++++++++++++ src/ui/overview.rs | 104 ++++++++++++++++++ src/ui/palette.rs | 201 +++++++++++++++++++++++++++++++++ src/ui/placeholder.rs | 15 +++ src/ui/shell.rs | 250 ++++++++++++++++++++++++++++++++++++++++++ src/ui/sidebar.rs | 113 +++++++++++++++++++ 8 files changed, 839 insertions(+), 5 deletions(-) create mode 100644 src/ui/mod.rs create mode 100644 src/ui/overview.rs create mode 100644 src/ui/palette.rs create mode 100644 src/ui/placeholder.rs create mode 100644 src/ui/shell.rs create mode 100644 src/ui/sidebar.rs diff --git a/Cargo.toml b/Cargo.toml index e88a42e..639ce2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,8 @@ chrono = { version = "0.4", features = ["serde"] } toml = "0.8" dirs = "5" csv = "1.4.0" +gpui-kit = { version = "=0.7.0" } +gpui-omarchy = "0.2.0" [profile.release] panic = "abort" @@ -32,3 +34,6 @@ codegen-units = 1 lto = true opt-level = "s" strip = true + +[dev-dependencies] +gpui-kit = { version = "=0.7.0", features = ["test-support"] } diff --git a/src/main.rs b/src/main.rs index 8501d4f..1aafe82 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,41 @@ -use chelete_lib::database; +mod ui; + +use chelete_lib::database::{self, DbState}; +use chelete_lib::prefs::Prefs; +use gpui_kit::{px, size, AppContext as _, Bounds, WindowBounds, WindowOptions}; +use std::sync::Arc; fn main() { - // The native UI arrives in later commits; for now just make sure the - // database opens and migrates. - let db = database::init_database().expect("failed to open database"); - drop(db); + let db = Arc::new(DbState( + database::init_database().expect("failed to open database"), + )); + + gpui_kit::application() + .with_assets(gpui_kit::assets::AllAssets) + .run(move |cx| { + gpui_kit::init(cx); + gpui_omarchy::init(cx); + ui::bind_keys(cx); + cx.on_action(|_: &ui::Quit, cx| cx.quit()); + cx.on_window_closed(|cx, _| { + if cx.windows().is_empty() { + cx.quit(); + } + }) + .detach(); + + let options = WindowOptions { + window_bounds: Some(WindowBounds::Windowed(Bounds::centered( + None, + size(px(1100.), px(700.)), + cx, + ))), + ..Default::default() + }; + gpui_kit::open_window(options, cx, |window, cx| { + cx.new(|cx| ui::Shell::new(db, Prefs::load(), true, window, cx)) + }) + .expect("open window"); + cx.activate(true); + }); } diff --git a/src/ui/mod.rs b/src/ui/mod.rs new file mode 100644 index 0000000..041172b --- /dev/null +++ b/src/ui/mod.rs @@ -0,0 +1,113 @@ +//! GPUI front end: app shell, sidebar, command palette and screens. +mod overview; +mod palette; +mod placeholder; +mod shell; +mod sidebar; + +pub use shell::Shell; + +use gpui_kit::{actions, App, KeyBinding}; +use gpui_omarchy::IconName; + +actions!( + chelete, + [ + GoOverview, + GoTransactions, + GoAccounts, + GoCategories, + GoSubscriptions, + GoProjections, + GoSettings, + TogglePalette, + ToggleSidebar, + Quit, + PaletteNext, + PalettePrev, + PaletteDismiss, + ] +); + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum View { + Overview, + Transactions, + Accounts, + Categories, + Subscriptions, + Projections, + Settings, +} + +impl View { + pub const ALL: [View; 7] = [ + View::Overview, + View::Transactions, + View::Accounts, + View::Categories, + View::Subscriptions, + View::Projections, + View::Settings, + ]; + + pub fn label(self) -> &'static str { + match self { + View::Overview => "Overview", + View::Transactions => "Transactions", + View::Accounts => "Accounts", + View::Categories => "Categories", + View::Subscriptions => "Subscriptions", + View::Projections => "Projections", + View::Settings => "Settings", + } + } + + pub fn shortcut(self) -> &'static str { + match self { + View::Overview => "Ctrl+O", + View::Transactions => "Ctrl+T", + View::Accounts => "Ctrl+A", + View::Categories => "Ctrl+C", + View::Subscriptions => "Ctrl+U", + View::Projections => "Ctrl+P", + View::Settings => "Ctrl+S", + } + } + + pub fn icon(self) -> IconName { + match self { + View::Overview => IconName::LayoutDashboard, + View::Transactions => IconName::ArrowLeftRight, + View::Accounts => IconName::Wallet, + View::Categories => IconName::Tag, + View::Subscriptions => IconName::Repeat, + View::Projections => IconName::ChartLine, + View::Settings => IconName::Settings, + } + } +} + +/// Context the shell sets on its root element; shortcuts below only fire inside it, +/// so text inputs keep their own Ctrl+A / Ctrl+C / Ctrl+S behavior. +pub const SHELL_CONTEXT: &str = "Shell"; +pub const PALETTE_CONTEXT: &str = "Palette"; + +pub fn bind_keys(cx: &mut App) { + let shell = Some(SHELL_CONTEXT); + cx.bind_keys([ + KeyBinding::new("ctrl-o", GoOverview, shell), + KeyBinding::new("ctrl-t", GoTransactions, shell), + KeyBinding::new("ctrl-a", GoAccounts, shell), + KeyBinding::new("ctrl-c", GoCategories, shell), + KeyBinding::new("ctrl-u", GoSubscriptions, shell), + KeyBinding::new("ctrl-p", GoProjections, shell), + KeyBinding::new("ctrl-s", GoSettings, shell), + KeyBinding::new("ctrl-k", TogglePalette, shell), + KeyBinding::new("ctrl-b", ToggleSidebar, shell), + KeyBinding::new("ctrl-q", Quit, None), + KeyBinding::new("down", PaletteNext, Some(PALETTE_CONTEXT)), + KeyBinding::new("up", PalettePrev, Some(PALETTE_CONTEXT)), + KeyBinding::new("escape", PaletteDismiss, Some(PALETTE_CONTEXT)), + ]); +} diff --git a/src/ui/overview.rs b/src/ui/overview.rs new file mode 100644 index 0000000..bc50d49 --- /dev/null +++ b/src/ui/overview.rs @@ -0,0 +1,104 @@ +use chelete_lib::commands::{self, Overview}; +use chelete_lib::database::DbState; +use chelete_lib::format::{account_type_label, format_balance, format_money}; +use gpui_kit::{div, prelude::*, rems, App, Div, IntoElement, SharedString}; +use gpui_omarchy::chart::PieChart; +use gpui_omarchy::{alert, empty_state, panel, ActiveTheme as _, Status}; + +pub struct OverviewData { + pub result: Result, +} + +impl OverviewData { + pub fn load(db: &DbState) -> Self { + Self { + result: commands::get_overview(db), + } + } +} + +fn stat(title: &'static str, value: String, color: gpui_kit::Hsla, cx: &App) -> Div { + panel(title, cx) + .flex_1() + .child(div().text_size(rems(1.5)).text_color(color).child(value)) +} + +pub fn render(data: &OverviewData, cx: &App) -> impl IntoElement { + let t = cx.omarchy(); + let page = div() + .id("overview") + .size_full() + .overflow_y_scroll() + .p(rems(1.5)) + .flex() + .flex_col() + .gap(rems(1.)); + + let overview = match &data.result { + Ok(o) => o, + Err(e) => { + return page.child(alert(format!("Could not load overview: {e}"), Status::Error, cx)) + } + }; + + let stats = div() + .flex() + .gap(rems(1.)) + .child(stat("Balance", format_balance(overview.total_balance), t.foreground, cx)) + .child(stat("Income", format_money(overview.total_income), t.success, cx)) + .child(stat("Expenses", format_money(-overview.total_expenses), t.danger, cx)); + + let spending: Vec<(SharedString, f32)> = overview + .category_spending + .iter() + .filter(|c| c.spent > 0) + .map(|c| (SharedString::from(c.category_name.clone()), c.spent as f32 / 100.)) + .collect(); + + let spending_panel = panel("Spending by category", cx).flex_1().child(if spending.is_empty() { + empty_state("No spending yet", "Expenses will appear here.", cx).into_any_element() + } else { + div() + .w_full() + .h(rems(16.)) + .child( + PieChart::new(spending) + .id("overview-spending") + .name("Spent") + .value(|(_, v)| *v) + .inner_radius(56.) + .outer_radius(88.) + .pad_angle(0.02) + .label(|(name, v)| format!("{name} ${v:.0}").into()), + ) + .into_any_element() + }); + + let accounts = panel("Accounts", cx).flex_1().children(overview.accounts.iter().map(|a| { + div() + .flex() + .justify_between() + .child( + div() + .child(a.name.clone()) + .child(div().text_color(t.secondary).child(account_type_label(&a.account_type))), + ) + .child(format_balance(a.balance)) + })); + + let recent = panel("Recent transactions", cx).children( + overview.recent_transactions.iter().map(|tx| { + let color = if tx.transaction_type == "income" { t.success } else { t.danger }; + let signed = if tx.transaction_type == "income" { tx.amount } else { -tx.amount }; + div() + .flex() + .justify_between() + .child(div().flex().gap(rems(1.)).child(div().text_color(t.secondary).child(tx.transaction_date.clone())).child(tx.description.clone())) + .child(div().text_color(color).child(format_money(signed))) + }), + ); + + page.child(stats) + .child(div().flex().gap(rems(1.)).child(spending_panel).child(accounts)) + .child(recent) +} diff --git a/src/ui/palette.rs b/src/ui/palette.rs new file mode 100644 index 0000000..926f8e9 --- /dev/null +++ b/src/ui/palette.rs @@ -0,0 +1,201 @@ +use super::{PaletteDismiss, PaletteNext, PalettePrev, View, PALETTE_CONTEXT}; +use gpui_kit::base::input::{InputEvent, InputState}; +use gpui_kit::{ + div, prelude::*, rems, App, Context, Entity, EventEmitter, FocusHandle, Focusable, + IntoElement, MouseButton, Render, SharedString, Subscription, Window, +}; +use gpui_omarchy::{input, keycap, ActiveTheme as _}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Command { + Go(View), + AddTransaction, + ImportTransactions, + AddAccount, + AddCategory, + AddSubscription, +} + +pub enum PaletteEvent { + Run(Command), + Dismiss, +} + +struct Entry { + label: SharedString, + shortcut: Option<&'static str>, + command: Command, +} + +fn entries() -> Vec { + let mut all: Vec = View::ALL + .iter() + .map(|v| Entry { + label: format!("Go to {}", v.label()).into(), + shortcut: Some(v.shortcut()), + command: Command::Go(*v), + }) + .collect(); + for (label, command) in [ + ("Add Transaction", Command::AddTransaction), + ("Import Transactions (CSV)", Command::ImportTransactions), + ("Add Account", Command::AddAccount), + ("Add Category", Command::AddCategory), + ("Add Subscription", Command::AddSubscription), + ] { + all.push(Entry { + label: label.into(), + shortcut: None, + command, + }); + } + all +} + +pub struct Palette { + query: Entity, + selected: usize, + focus: FocusHandle, + _subs: Vec, +} + +impl EventEmitter for Palette {} + +impl Focusable for Palette { + fn focus_handle(&self, _: &App) -> FocusHandle { + self.focus.clone() + } +} + +impl Palette { + pub fn new(window: &mut Window, cx: &mut Context) -> Self { + let query = cx.new(|cx| InputState::new(window, cx).placeholder("Type a command...")); + let sub = cx.subscribe_in(&query, window, |this, _, event: &InputEvent, _, cx| match event { + InputEvent::Change => { + this.selected = 0; + cx.notify(); + } + InputEvent::PressEnter { .. } => this.confirm(cx), + _ => {} + }); + query.update(cx, |state, cx| state.focus(window, cx)); + Self { + query, + selected: 0, + focus: cx.focus_handle(), + _subs: vec![sub], + } + } + + fn matches(&self, cx: &App) -> Vec { + let needle = self.query.read(cx).value().to_lowercase(); + entries() + .into_iter() + .filter(|e| e.label.to_lowercase().contains(&needle)) + .collect() + } + + fn confirm(&mut self, cx: &mut Context) { + let matches = self.matches(cx); + if let Some(entry) = matches.get(self.selected) { + cx.emit(PaletteEvent::Run(entry.command)); + } + } + + fn step(&mut self, delta: isize, cx: &mut Context) { + let len = self.matches(cx).len(); + if len > 0 { + self.selected = (self.selected as isize + delta).clamp(0, len as isize - 1) as usize; + cx.notify(); + } + } +} + +impl Render for Palette { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let t = cx.omarchy().clone(); + let matches = self.matches(cx); + let selected = self.selected.min(matches.len().saturating_sub(1)); + let empty = matches.is_empty(); + let query = input("palette-input", &self.query, window, cx); + + let rows = matches.into_iter().enumerate().map(|(i, entry)| { + let command = entry.command; + let mut row = div() + .id(("palette-row", i)) + .flex() + .items_center() + .justify_between() + .px(rems(0.75)) + .py(rems(0.375)) + .cursor_pointer() + .child(entry.label) + .on_mouse_move(cx.listener(move |this, _, _, cx| { + if this.selected != i { + this.selected = i; + cx.notify(); + } + })) + .on_click(cx.listener(move |_, _, _, cx| cx.emit(PaletteEvent::Run(command)))); + if i == selected { + row = row.bg(t.selected_fill()); + } + if let Some(shortcut) = entry.shortcut { + row = row.child(keycap(shortcut, cx)); + } + row + }); + + div() + .id("palette-backdrop") + .absolute() + .size_full() + .flex() + .justify_center() + .pt(rems(5.)) + .bg(t.background.opacity(0.6)) + .occlude() + .on_mouse_down( + MouseButton::Left, + cx.listener(|_, _, _, cx| cx.emit(PaletteEvent::Dismiss)), + ) + .child( + div() + .key_context(PALETTE_CONTEXT) + .track_focus(&self.focus) + .on_action(cx.listener(|this, _: &PaletteNext, _, cx| this.step(1, cx))) + .on_action(cx.listener(|this, _: &PalettePrev, _, cx| this.step(-1, cx))) + .on_action(cx.listener(|_, _: &PaletteDismiss, _, cx| { + cx.emit(PaletteEvent::Dismiss) + })) + .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) + .w(rems(34.)) + .max_h(rems(24.)) + .flex() + .flex_col() + .border_1() + .border_color(t.border) + .bg(t.background) + .font_family(t.font.clone()) + .text_size(rems(0.75)) + .text_color(t.foreground) + .child(div().p(rems(0.5)).child(query)) + .child( + div() + .id("palette-results") + .flex() + .flex_col() + .overflow_y_scroll() + .children(rows) + .when(empty, |el| { + el.child( + div() + .p(rems(1.)) + .text_color(t.secondary) + .child("No matching commands"), + ) + }), + ), + ) + } +} diff --git a/src/ui/placeholder.rs b/src/ui/placeholder.rs new file mode 100644 index 0000000..4d6ff57 --- /dev/null +++ b/src/ui/placeholder.rs @@ -0,0 +1,15 @@ +use super::View; +use gpui_kit::{div, prelude::*, rems, App, IntoElement}; +use gpui_omarchy::{empty_state, panel, ActiveTheme as _}; + +/// Stand-in for screens that have not been ported from the web UI yet. +pub fn placeholder(view: View, cx: &App) -> impl IntoElement { + let t = cx.omarchy(); + div().size_full().p(rems(1.5)).child( + panel(view.label(), cx).child( + div() + .text_color(t.secondary) + .child(empty_state("Coming soon", "This screen has not been ported yet.", cx)), + ), + ) +} diff --git a/src/ui/shell.rs b/src/ui/shell.rs new file mode 100644 index 0000000..322942f --- /dev/null +++ b/src/ui/shell.rs @@ -0,0 +1,250 @@ +use super::overview::{self, OverviewData}; +use super::palette::{Command, Palette, PaletteEvent}; +use super::placeholder::placeholder; +use super::sidebar::sidebar; +use super::*; +use chelete_lib::database::DbState; +use chelete_lib::prefs::Prefs; +use gpui_kit::{ + div, prelude::*, Context, Entity, FocusHandle, Focusable, IntoElement, Render, Subscription, + Window, +}; +use gpui_omarchy::ActiveTheme as _; +use std::sync::Arc; + +pub struct Shell { + db: Arc, + view: View, + prefs: Prefs, + persist: bool, + overview: OverviewData, + palette: Option>, + focus: FocusHandle, + _palette_sub: Option, +} + +impl Shell { + pub fn new( + db: Arc, + prefs: Prefs, + persist: bool, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let focus = cx.focus_handle(); + focus.focus(window, cx); + Self { + overview: OverviewData::load(&db), + db, + view: View::Overview, + prefs, + persist, + palette: None, + focus, + _palette_sub: None, + } + } + + pub fn view(&self) -> View { + self.view + } + + pub fn collapsed(&self) -> bool { + self.prefs.sidebar_collapsed + } + + pub fn navigate(&mut self, view: View, window: &mut Window, cx: &mut Context) { + self.close_palette(window, cx); + self.view = view; + if view == View::Overview { + self.overview = OverviewData::load(&self.db); + } + cx.notify(); + } + + fn toggle_sidebar(&mut self, cx: &mut Context) { + self.prefs.sidebar_collapsed = !self.prefs.sidebar_collapsed; + if self.persist { + self.prefs.save(); + } + cx.notify(); + } + + fn toggle_palette(&mut self, window: &mut Window, cx: &mut Context) { + if self.palette.is_some() { + self.close_palette(window, cx); + return; + } + let palette = cx.new(|cx| Palette::new(window, cx)); + self._palette_sub = Some(cx.subscribe_in( + &palette, + window, + |this, _, event: &PaletteEvent, window, cx| match event { + PaletteEvent::Dismiss => this.close_palette(window, cx), + PaletteEvent::Run(command) => this.run(*command, window, cx), + }, + )); + self.palette = Some(palette); + cx.notify(); + } + + fn close_palette(&mut self, window: &mut Window, cx: &mut Context) { + if self.palette.take().is_some() { + self._palette_sub = None; + self.focus.focus(window, cx); + cx.notify(); + } + } + + /// Creation flows live on their own screens; until those are ported the + /// "Add …" commands just take you there. + fn run(&mut self, command: Command, window: &mut Window, cx: &mut Context) { + let target = match command { + Command::Go(view) => view, + Command::AddTransaction | Command::ImportTransactions => View::Transactions, + Command::AddAccount => View::Accounts, + Command::AddCategory => View::Categories, + Command::AddSubscription => View::Subscriptions, + }; + self.navigate(target, window, cx); + } +} + +impl Focusable for Shell { + fn focus_handle(&self, _: &gpui_kit::App) -> FocusHandle { + self.focus.clone() + } +} + +impl Render for Shell { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let t = cx.omarchy().clone(); + let content = match self.view { + View::Overview => overview::render(&self.overview, cx).into_any_element(), + other => placeholder(other, cx).into_any_element(), + }; + div() + .key_context(SHELL_CONTEXT) + .track_focus(&self.focus) + .on_action(cx.listener(|this, _: &GoOverview, w, cx| this.navigate(View::Overview, w, cx))) + .on_action(cx.listener(|this, _: &GoTransactions, w, cx| this.navigate(View::Transactions, w, cx))) + .on_action(cx.listener(|this, _: &GoAccounts, w, cx| this.navigate(View::Accounts, w, cx))) + .on_action(cx.listener(|this, _: &GoCategories, w, cx| this.navigate(View::Categories, w, cx))) + .on_action(cx.listener(|this, _: &GoSubscriptions, w, cx| this.navigate(View::Subscriptions, w, cx))) + .on_action(cx.listener(|this, _: &GoProjections, w, cx| this.navigate(View::Projections, w, cx))) + .on_action(cx.listener(|this, _: &GoSettings, w, cx| this.navigate(View::Settings, w, cx))) + .on_action(cx.listener(|this, _: &TogglePalette, w, cx| this.toggle_palette(w, cx))) + .on_action(cx.listener(|this, _: &ToggleSidebar, _, cx| this.toggle_sidebar(cx))) + .size_full() + .relative() + .flex() + .bg(t.background) + .text_color(t.foreground) + .font_family(t.font.clone()) + .child(sidebar(self, cx)) + .child(div().flex_1().min_w_0().h_full().child(content)) + .when_some(self.palette.clone(), |el, palette| el.child(palette)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use chelete_lib::{database, seed}; + use gpui_kit::TestAppContext; + use rusqlite::Connection; + + fn seeded_db() -> Arc { + let conn = Connection::open_in_memory().unwrap(); + conn.execute_batch("PRAGMA foreign_keys=ON;").unwrap(); + database::run_migrations(&conn).unwrap(); + seed::seed_database(&conn).unwrap(); + Arc::new(DbState(std::sync::Mutex::new(conn))) + } + + fn setup(cx: &mut TestAppContext) -> (Entity, &mut gpui_kit::VisualTestContext) { + cx.update(|cx| { + gpui_kit::init(cx); + gpui_omarchy::init(cx); + // Applying a fixed theme stops the filesystem watcher, which the + // deterministic test scheduler rejects. + gpui_omarchy::Theme::tokyo_night().apply(cx); + bind_keys(cx); + }); + let db = seeded_db(); + cx.add_window_view(|window, cx| Shell::new(db, Prefs::default(), false, window, cx)) + } + + fn view_of(shell: &Entity, cx: &mut gpui_kit::VisualTestContext) -> View { + cx.update(|_, cx| shell.read(cx).view()) + } + + #[gpui_kit::test] + fn starts_on_overview_with_seeded_data(cx: &mut TestAppContext) { + let (shell, cx) = setup(cx); + cx.update(|_, cx| { + let shell = shell.read(cx); + assert_eq!(shell.view(), View::Overview); + let overview = shell.overview.result.as_ref().expect("overview loads"); + assert_eq!(overview.accounts.len(), 4); + }); + } + + #[gpui_kit::test] + fn ctrl_shortcuts_switch_views(cx: &mut TestAppContext) { + let (shell, cx) = setup(cx); + for (keys, expected) in [ + ("ctrl-t", View::Transactions), + ("ctrl-a", View::Accounts), + ("ctrl-c", View::Categories), + ("ctrl-u", View::Subscriptions), + ("ctrl-p", View::Projections), + ("ctrl-s", View::Settings), + ("ctrl-o", View::Overview), + ] { + cx.simulate_keystrokes(keys); + assert_eq!(view_of(&shell, cx), expected, "after {keys}"); + } + } + + #[gpui_kit::test] + fn ctrl_b_toggles_sidebar(cx: &mut TestAppContext) { + let (shell, cx) = setup(cx); + cx.simulate_keystrokes("ctrl-b"); + cx.update(|_, cx| assert!(shell.read(cx).collapsed())); + cx.simulate_keystrokes("ctrl-b"); + cx.update(|_, cx| assert!(!shell.read(cx).collapsed())); + } + + #[gpui_kit::test] + fn palette_opens_filters_and_navigates(cx: &mut TestAppContext) { + let (shell, cx) = setup(cx); + cx.simulate_keystrokes("ctrl-k"); + cx.update(|_, cx| assert!(shell.read(cx).palette.is_some(), "palette opens")); + + cx.simulate_input("go to acc"); + cx.simulate_keystrokes("enter"); + cx.update(|_, cx| { + assert!(shell.read(cx).palette.is_none(), "palette closes after running"); + }); + assert_eq!(view_of(&shell, cx), View::Accounts); + } + + #[gpui_kit::test] + fn palette_escape_closes_without_navigating(cx: &mut TestAppContext) { + let (shell, cx) = setup(cx); + cx.simulate_keystrokes("ctrl-k"); + cx.simulate_keystrokes("escape"); + cx.update(|_, cx| assert!(shell.read(cx).palette.is_none())); + assert_eq!(view_of(&shell, cx), View::Overview); + } + + #[gpui_kit::test] + fn palette_arrow_keys_pick_the_command(cx: &mut TestAppContext) { + let (shell, cx) = setup(cx); + cx.simulate_keystrokes("ctrl-k"); + cx.simulate_keystrokes("down down enter"); + // Entries are ordered like View::ALL: Overview, Transactions, Accounts. + assert_eq!(view_of(&shell, cx), View::Accounts); + } +} diff --git a/src/ui/sidebar.rs b/src/ui/sidebar.rs new file mode 100644 index 0000000..54dd437 --- /dev/null +++ b/src/ui/sidebar.rs @@ -0,0 +1,113 @@ +use super::shell::Shell; +use super::{View, TogglePalette, ToggleSidebar}; +use gpui_kit::{div, prelude::*, rems, Context, Div, SharedString}; +use gpui_omarchy::{button, icon, keycap, ActiveTheme as _, ButtonVariant, IconName}; + +fn item( + id: impl Into, + icon_name: IconName, + label: &'static str, + shortcut: &'static str, + active: bool, + collapsed: bool, + cx: &mut Context, +) -> gpui_omarchy::Button { + let id: SharedString = id.into(); + let t = cx.omarchy().clone(); + let variant = if active { + ButtonVariant::Primary + } else { + ButtonVariant::Secondary + }; + button(id, "", variant, cx) + .accessibility_label(label) + .w_full() + .justify_start() + .child(icon(icon_name).size(rems(1.))) + .when(!collapsed, |b| { + b.child(div().flex_1().child(label)) + .child(div().text_color(t.secondary).child(keycap(shortcut, cx))) + }) +} + +pub fn sidebar(shell: &Shell, cx: &mut Context) -> Div { + let t = cx.omarchy().clone(); + let collapsed = shell.collapsed(); + let current = shell.view(); + + let mut nav = Vec::new(); + for view in View::ALL { + nav.push( + item( + format!("nav-{}", view.label()), + view.icon(), + view.label(), + view.shortcut(), + current == view, + collapsed, + cx, + ) + .on_click(cx.listener(move |this, _, window, cx| this.navigate(view, window, cx))), + ); + } + + let command = item( + "nav-command", + IconName::Command, + "Command", + "Ctrl+K", + false, + collapsed, + cx, + ) + .on_click(cx.listener(|_, _, window, cx| { + window.dispatch_action(Box::new(TogglePalette), cx); + })); + + let collapse = item( + "nav-collapse", + if collapsed { + IconName::PanelLeftOpen + } else { + IconName::PanelLeftClose + }, + "Collapse", + "Ctrl+B", + false, + collapsed, + cx, + ) + .on_click(cx.listener(|_, _, window, cx| { + window.dispatch_action(Box::new(ToggleSidebar), cx); + })); + + div() + .flex() + .flex_col() + .flex_shrink_0() + .w(if collapsed { rems(3.25) } else { rems(15.) }) + .h_full() + .gap(rems(0.25)) + .p(rems(0.5)) + .border_r_1() + .border_color(t.border) + .bg(t.surface) + .font_family(t.font.clone()) + .text_size(rems(0.75)) + .text_color(t.foreground) + .child( + div() + .flex() + .items_center() + .justify_center() + .py(rems(0.75)) + .text_color(t.accent) + .font_weight(gpui_kit::FontWeight::BOLD) + .child(if collapsed { "Ch" } else { "115 Ch" }), + ) + .children(nav) + .child(div().flex_1()) + .child(command) + .child(div().h(rems(0.0625)).bg(t.border)) + .child(collapse) +} From 723ece3ddc3e5c57766cb5928b78a0d3509fcf92 Mon Sep 17 00:00:00 2001 From: Victor Phos Date: Fri, 2 Oct 2026 14:05:51 +0200 Subject: [PATCH 17/36] Port the Accounts screen Account cards with add, transfer and delete. Introduces the shared widget layer (page headers, labelled fields, modals, selects, inputs) and toasts that screens raise through ScreenEvent. --- src/ui/accounts.rs | 454 +++++++++++++++++++++++++++++++++++++++++ src/ui/mod.rs | 5 + src/ui/shell.rs | 97 +++++++-- src/ui/test_support.rs | 32 +++ src/ui/widgets.rs | 162 +++++++++++++++ 5 files changed, 728 insertions(+), 22 deletions(-) create mode 100644 src/ui/accounts.rs create mode 100644 src/ui/test_support.rs create mode 100644 src/ui/widgets.rs diff --git a/src/ui/accounts.rs b/src/ui/accounts.rs new file mode 100644 index 0000000..896e997 --- /dev/null +++ b/src/ui/accounts.rs @@ -0,0 +1,454 @@ +use super::widgets::*; +use chelete_lib::commands::{self, Account, CreateAccountRequest}; +use chelete_lib::database::DbState; +use chelete_lib::format::{account_type_label, format_balance, parse_cents}; +use gpui_kit::base::input::InputState; +use gpui_kit::{ + div, prelude::*, rems, Context, Entity, EventEmitter, IntoElement, Render, Subscription, + SharedString, Window, +}; +use gpui_omarchy::{ + button, empty_state, icon, input, panel, select, ActiveTheme as _, ButtonVariant, + ChoiceState, IconName, +}; +use std::sync::Arc; + +enum Dialog { + Add(Entity), + Transfer(Entity), +} + +pub struct AccountsScreen { + db: Arc, + accounts: Result, String>, + dialog: Option, + _dialog_sub: Option, +} + +impl EventEmitter for AccountsScreen {} + +impl AccountsScreen { + pub fn new(db: Arc) -> Self { + let accounts = commands::get_accounts(&db); + Self { db, accounts, dialog: None, _dialog_sub: None } + } + + pub fn reload(&mut self, cx: &mut Context) { + self.accounts = commands::get_accounts(&self.db); + cx.notify(); + } + + pub fn accounts(&self) -> &[Account] { + self.accounts.as_deref().unwrap_or(&[]) + } + + pub fn has_dialog(&self) -> bool { + self.dialog.is_some() + } + + pub fn open_add(&mut self, window: &mut Window, cx: &mut Context) { + let form = cx.new(|cx| AddAccountForm::new(self.db.clone(), window, cx)); + self._dialog_sub = Some(cx.subscribe(&form, Self::on_form_event)); + self.dialog = Some(Dialog::Add(form)); + cx.notify(); + } + + fn open_transfer(&mut self, window: &mut Window, cx: &mut Context) { + let accounts = self.accounts().to_vec(); + let form = cx.new(|cx| TransferForm::new(self.db.clone(), accounts, window, cx)); + self._dialog_sub = Some(cx.subscribe(&form, Self::on_form_event)); + self.dialog = Some(Dialog::Transfer(form)); + cx.notify(); + } + + fn on_form_event(&mut self, _: Entity, event: &FormEvent, cx: &mut Context) { + match event { + FormEvent::Cancel => {} + FormEvent::Saved(message) => { + self.accounts = commands::get_accounts(&self.db); + cx.emit(ScreenEvent::ok(message.clone())); + } + FormEvent::Failed(message) => { + cx.emit(ScreenEvent::error(message.clone())); + return; + } + } + self.dialog = None; + self._dialog_sub = None; + cx.notify(); + } + + fn delete(&mut self, id: String, cx: &mut Context) { + match commands::delete_account(&self.db, id) { + Ok(()) => { + self.accounts = commands::get_accounts(&self.db); + } + Err(e) => cx.emit(ScreenEvent::error(format!("Could not delete account: {e}"))), + } + cx.notify(); + } +} + +impl Render for AccountsScreen { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let t = cx.omarchy().clone(); + let actions = div() + .flex() + .gap(rems(0.5)) + .child( + button("transfer", "Transfer", ButtonVariant::Outline, cx) + .child(icon(IconName::ArrowLeftRight).size(rems(0.875))) + .on_click(cx.listener(|this, _, window, cx| this.open_transfer(window, cx))), + ) + .child( + button("add-account", "+ Add", ButtonVariant::Primary, cx) + .on_click(cx.listener(|this, _, window, cx| this.open_add(window, cx))), + ); + + let body = match &self.accounts { + Err(e) => div().text_color(t.danger).child(format!("Could not load accounts: {e}")).into_any_element(), + Ok(list) if list.is_empty() => { + empty_state("No accounts yet", "Add one to get started.", cx).into_any_element() + } + Ok(list) => { + let cards: Vec<_> = list + .iter() + .map(|a| { + let id = a.id.clone(); + panel(account_type_label(&a.account_type), cx) + .w(rems(16.)) + .child(div().text_size(rems(0.875)).child(a.name.clone())) + .child( + div() + .text_size(rems(1.25)) + .text_color(if a.balance < 0 { t.danger } else { t.foreground }) + .child(format_balance(a.balance)), + ) + .child( + button(SharedString::from(format!("delete-account-{}", a.id)), "Delete", ButtonVariant::Danger, cx) + .on_click(cx.listener(move |this, _, _, cx| this.delete(id.clone(), cx))), + ) + }) + .collect(); + div().flex().flex_wrap().gap(rems(1.)).children(cards).into_any_element() + } + }; + + let dialog = self.dialog.as_ref().map(|d| match d { + Dialog::Add(form) => form.clone().into_any_element(), + Dialog::Transfer(form) => form.clone().into_any_element(), + }); + + div() + .id("accounts") + .relative() + .size_full() + .overflow_y_scroll() + .p(rems(1.5)) + .flex() + .flex_col() + .gap(rems(1.)) + .child(page_header("Accounts", actions, cx)) + .child(body) + .children(dialog) + } +} + +#[derive(Clone, Debug)] +pub enum FormEvent { + Cancel, + Saved(String), + Failed(String), +} + +pub struct AddAccountForm { + db: Arc, + name: Entity, + kind: Entity, + balance: Entity, +} + +impl EventEmitter for AddAccountForm {} + +impl AddAccountForm { + fn new(db: Arc, window: &mut Window, cx: &mut Context) -> Self { + let name = text_input("e.g. Main Bank", window, cx); + name.update(cx, |s, cx| s.focus(window, cx)); + Self { + db, + name, + kind: choice(account_type_options(), Some("cash"), window, cx), + balance: text_input("0.00", window, cx), + } + } + + fn submit(&mut self, cx: &mut Context) { + let name = text_of(&self.name, cx); + if name.is_empty() { + return; + } + let balance_text = text_of(&self.balance, cx); + let balance = if balance_text.is_empty() { + 0 + } else { + match parse_cents(&balance_text) { + Some(c) => c, + None => return cx.emit(FormEvent::Failed("Starting balance is not a valid amount".into())), + } + }; + let request = CreateAccountRequest { + name, + account_type: choice_value(&self.kind, cx).unwrap_or_else(|| "cash".into()), + currency: "USD".into(), + balance, + color: None, + icon: None, + }; + match commands::create_account(&self.db, request) { + Ok(a) => cx.emit(FormEvent::Saved(format!("Added account {}", a.name))), + Err(e) => cx.emit(FormEvent::Failed(format!("Could not add account: {e}"))), + } + } +} + +impl Render for AddAccountForm { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let body = div() + .flex() + .flex_col() + .gap(rems(0.75)) + .child(field("Name", input("account-name", &self.name, window, cx), cx)) + .child(field("Type", select("account-type", &self.kind, window, cx), cx)) + .child(field("Starting Balance", input("account-balance", &self.balance, window, cx), cx)) + .child( + div() + .flex() + .justify_end() + .gap(rems(0.5)) + .child( + button("add-cancel", "Cancel", ButtonVariant::Outline, cx) + .on_click(cx.listener(|_, _, _, cx| cx.emit(FormEvent::Cancel))), + ) + .child( + button("add-submit", "Add", ButtonVariant::Primary, cx) + .on_click(cx.listener(|this, _, _, cx| this.submit(cx))), + ), + ); + modal("Add Account", body, cx.listener(|_, _, _, cx| cx.emit(FormEvent::Cancel)), cx) + } +} + +pub struct TransferForm { + db: Arc, + accounts: Vec, + from: Entity, + to: Entity, + amount: Entity, + notes: Entity, +} + +impl EventEmitter for TransferForm {} + +impl TransferForm { + fn new(db: Arc, accounts: Vec, window: &mut Window, cx: &mut Context) -> Self { + let items: Vec<(String, String)> = + accounts.iter().map(|a| (a.id.clone(), a.name.clone())).collect(); + let first = accounts.first().map(|a| a.id.clone()); + let second = accounts.get(1).or(accounts.first()).map(|a| a.id.clone()); + let amount = text_input("0.00", window, cx); + amount.update(cx, |s, cx| s.focus(window, cx)); + Self { + db, + from: choice(items.clone(), first.as_deref(), window, cx), + to: choice(items, second.as_deref(), window, cx), + amount, + notes: text_input("e.g. Monthly savings", window, cx), + accounts, + } + } + + fn submit(&mut self, cx: &mut Context) { + let (Some(from), Some(to)) = (choice_value(&self.from, cx), choice_value(&self.to, cx)) else { + return; + }; + if from == to { + return cx.emit(FormEvent::Failed("Pick two different accounts".into())); + } + let amount_text = text_of(&self.amount, cx); + let cents = match parse_cents(&amount_text) { + Some(c) if c > 0 => c, + _ => return cx.emit(FormEvent::Failed("Enter an amount greater than zero".into())), + }; + let notes = text_of(&self.notes, cx); + let to_name = self.accounts.iter().find(|a| a.id == to).map(|a| a.name.clone()).unwrap_or_default(); + match commands::transfer( + &self.db, + from, + to, + cents, + "USD".into(), + (!notes.is_empty()).then_some(notes), + ) { + Ok(_) => cx.emit(FormEvent::Saved(format!("Transferred ${amount_text} to {to_name}"))), + Err(e) => cx.emit(FormEvent::Failed(format!("Transfer failed: {e}"))), + } + } +} + +impl Render for TransferForm { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let body = div() + .flex() + .flex_col() + .gap(rems(0.75)) + .child( + div() + .flex() + .gap(rems(0.5)) + .child(field("From", select("transfer-from", &self.from, window, cx), cx)) + .child(field("To", select("transfer-to", &self.to, window, cx), cx)), + ) + .child(field("Amount", input("transfer-amount", &self.amount, window, cx), cx)) + .child(field("Notes", input("transfer-notes", &self.notes, window, cx), cx)) + .child( + div() + .flex() + .justify_end() + .gap(rems(0.5)) + .child( + button("transfer-cancel", "Cancel", ButtonVariant::Outline, cx) + .on_click(cx.listener(|_, _, _, cx| cx.emit(FormEvent::Cancel))), + ) + .child( + button("transfer-submit", "Transfer", ButtonVariant::Primary, cx) + .on_click(cx.listener(|this, _, _, cx| this.submit(cx))), + ), + ); + modal("Transfer Money", body, cx.listener(|_, _, _, cx| cx.emit(FormEvent::Cancel)), cx) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ui::test_support::{init, seeded_db}; + use gpui_kit::{TestAppContext, VisualTestContext}; + use std::cell::RefCell; + use std::rc::Rc; + + fn setup(cx: &mut TestAppContext) -> (Entity, &mut VisualTestContext, Rc>>) { + init(cx); + let db = seeded_db(); + let (screen, vcx) = cx.add_window_view(|_, _| AccountsScreen::new(db)); + let events = Rc::new(RefCell::new(Vec::new())); + let sink = events.clone(); + vcx.update(|_, cx| { + cx.subscribe(&screen, move |_, e: &ScreenEvent, _| sink.borrow_mut().push(e.clone())).detach(); + }); + (screen, vcx, events) + } + + fn open_add(screen: &Entity, cx: &mut VisualTestContext) -> Entity { + cx.update(|window, cx| screen.update(cx, |s, cx| s.open_add(window, cx))); + cx.update(|_, cx| match screen.read(cx).dialog.as_ref() { + Some(Dialog::Add(f)) => f.clone(), + _ => panic!("add form should be open"), + }) + } + + fn type_into(input: &Entity, text: &str, cx: &mut VisualTestContext) { + cx.update(|window, cx| input.update(cx, |i, cx| i.set_value(text, window, cx))); + } + + #[gpui_kit::test] + fn lists_seeded_accounts(cx: &mut TestAppContext) { + let (screen, cx, _) = setup(cx); + cx.update(|_, cx| assert_eq!(screen.read(cx).accounts().len(), 4)); + } + + #[gpui_kit::test] + fn add_account_saves_and_closes(cx: &mut TestAppContext) { + let (screen, cx, events) = setup(cx); + let form = open_add(&screen, cx); + let (name, balance) = cx.update(|_, cx| (form.read(cx).name.clone(), form.read(cx).balance.clone())); + type_into(&name, "Rainy Day", cx); + type_into(&balance, "1,250.50", cx); + cx.update(|_, cx| form.update(cx, |f, cx| f.submit(cx))); + cx.update(|_, cx| { + let screen = screen.read(cx); + assert!(!screen.has_dialog(), "dialog closes after saving"); + let added = screen.accounts().iter().find(|a| a.name == "Rainy Day").expect("account saved"); + assert_eq!(added.balance, 125050); + assert_eq!(added.account_type, "cash"); + }); + assert!(matches!(events.borrow().last(), Some(ScreenEvent::Toast { ok: true, .. }))); + } + + #[gpui_kit::test] + fn blank_name_is_ignored_and_bad_balance_reports(cx: &mut TestAppContext) { + let (screen, cx, events) = setup(cx); + let form = open_add(&screen, cx); + cx.update(|_, cx| form.update(cx, |f, cx| f.submit(cx))); + cx.update(|_, cx| assert!(screen.read(cx).has_dialog(), "blank name keeps the form open")); + + let (name, balance) = cx.update(|_, cx| (form.read(cx).name.clone(), form.read(cx).balance.clone())); + type_into(&name, "Oops", cx); + type_into(&balance, "twelve", cx); + cx.update(|_, cx| form.update(cx, |f, cx| f.submit(cx))); + cx.update(|_, cx| { + assert!(screen.read(cx).has_dialog(), "invalid balance keeps the form open"); + assert_eq!(screen.read(cx).accounts().len(), 4); + }); + assert!(matches!(events.borrow().last(), Some(ScreenEvent::Toast { ok: false, .. }))); + } + + #[gpui_kit::test] + fn transfer_moves_money_between_accounts(cx: &mut TestAppContext) { + let (screen, cx, events) = setup(cx); + let before: Vec = cx.update(|_, cx| screen.read(cx).accounts().iter().map(|a| a.balance).collect()); + cx.update(|window, cx| screen.update(cx, |s, cx| s.open_transfer(window, cx))); + let form = cx.update(|_, cx| match screen.read(cx).dialog.as_ref() { + Some(Dialog::Transfer(f)) => f.clone(), + _ => panic!("transfer form should be open"), + }); + let amount = cx.update(|_, cx| form.read(cx).amount.clone()); + type_into(&amount, "10.00", cx); + cx.update(|_, cx| form.update(cx, |f, cx| f.submit(cx))); + cx.update(|_, cx| { + let screen = screen.read(cx); + assert!(!screen.has_dialog()); + let after: Vec = screen.accounts().iter().map(|a| a.balance).collect(); + assert_eq!(after[0], before[0] - 1000, "source debited"); + assert_eq!(after[1], before[1] + 1000, "destination credited"); + assert_eq!(after.iter().sum::(), before.iter().sum::(), "total unchanged"); + }); + assert!(matches!(events.borrow().last(), Some(ScreenEvent::Toast { ok: true, .. }))); + } + + #[gpui_kit::test] + fn transfer_rejects_zero_amount(cx: &mut TestAppContext) { + let (screen, cx, events) = setup(cx); + cx.update(|window, cx| screen.update(cx, |s, cx| s.open_transfer(window, cx))); + let form = cx.update(|_, cx| match screen.read(cx).dialog.as_ref() { + Some(Dialog::Transfer(f)) => f.clone(), + _ => panic!("transfer form should be open"), + }); + let amount = cx.update(|_, cx| form.read(cx).amount.clone()); + type_into(&amount, "0", cx); + cx.update(|_, cx| form.update(cx, |f, cx| f.submit(cx))); + cx.update(|_, cx| assert!(screen.read(cx).has_dialog())); + assert!(matches!(events.borrow().last(), Some(ScreenEvent::Toast { ok: false, .. }))); + } + + #[gpui_kit::test] + fn delete_removes_the_account(cx: &mut TestAppContext) { + let (screen, cx, _) = setup(cx); + let id = cx.update(|_, cx| screen.read(cx).accounts()[0].id.clone()); + cx.update(|_, cx| screen.update(cx, |s, cx| s.delete(id.clone(), cx))); + cx.update(|_, cx| { + let accounts = screen.read(cx).accounts(); + assert_eq!(accounts.len(), 3); + assert!(accounts.iter().all(|a| a.id != id)); + }); + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 041172b..a9a5b1f 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,9 +1,14 @@ //! GPUI front end: app shell, sidebar, command palette and screens. +mod accounts; mod overview; mod palette; mod placeholder; mod shell; mod sidebar; +mod widgets; + +#[cfg(test)] +mod test_support; pub use shell::Shell; diff --git a/src/ui/shell.rs b/src/ui/shell.rs index 322942f..81e0a2f 100644 --- a/src/ui/shell.rs +++ b/src/ui/shell.rs @@ -1,4 +1,6 @@ +use super::accounts::AccountsScreen; use super::overview::{self, OverviewData}; +use super::widgets::ScreenEvent; use super::palette::{Command, Palette, PaletteEvent}; use super::placeholder::placeholder; use super::sidebar::sidebar; @@ -6,7 +8,7 @@ use super::*; use chelete_lib::database::DbState; use chelete_lib::prefs::Prefs; use gpui_kit::{ - div, prelude::*, Context, Entity, FocusHandle, Focusable, IntoElement, Render, Subscription, + div, prelude::*, rems, Context, Entity, FocusHandle, Focusable, IntoElement, Render, Subscription, Window, }; use gpui_omarchy::ActiveTheme as _; @@ -18,6 +20,9 @@ pub struct Shell { prefs: Prefs, persist: bool, overview: OverviewData, + accounts: Entity, + toast: Option<(u64, String, bool)>, + toast_seq: u64, palette: Option>, focus: FocusHandle, _palette_sub: Option, @@ -33,8 +38,13 @@ impl Shell { ) -> Self { let focus = cx.focus_handle(); focus.focus(window, cx); + let accounts = cx.new(|_| AccountsScreen::new(db.clone())); + cx.subscribe(&accounts, Self::on_screen_event).detach(); Self { overview: OverviewData::load(&db), + accounts, + toast: None, + toast_seq: 0, db, view: View::Overview, prefs, @@ -56,12 +66,38 @@ impl Shell { pub fn navigate(&mut self, view: View, window: &mut Window, cx: &mut Context) { self.close_palette(window, cx); self.view = view; - if view == View::Overview { - self.overview = OverviewData::load(&self.db); + match view { + View::Overview => self.overview = OverviewData::load(&self.db), + View::Accounts => self.accounts.update(cx, |s, cx| s.reload(cx)), + _ => {} } cx.notify(); } + fn on_screen_event(&mut self, _: Entity, event: &ScreenEvent, cx: &mut Context) { + let ScreenEvent::Toast { message, ok } = event; + self.toast_seq += 1; + let seq = self.toast_seq; + self.toast = Some((seq, message.clone(), *ok)); + cx.notify(); + cx.spawn(async move |this, cx| { + cx.background_executor().timer(std::time::Duration::from_secs(3)).await; + this.update(cx, |this, cx| { + if this.toast.as_ref().is_some_and(|(s, ..)| *s == seq) { + this.toast = None; + cx.notify(); + } + }) + .ok(); + }) + .detach(); + } + + #[cfg(test)] + pub(super) fn toast_message(&self) -> Option<&str> { + self.toast.as_ref().map(|(_, m, _)| m.as_str()) + } + fn toggle_sidebar(&mut self, cx: &mut Context) { self.prefs.sidebar_collapsed = !self.prefs.sidebar_collapsed; if self.persist { @@ -102,7 +138,11 @@ impl Shell { let target = match command { Command::Go(view) => view, Command::AddTransaction | Command::ImportTransactions => View::Transactions, - Command::AddAccount => View::Accounts, + Command::AddAccount => { + self.navigate(View::Accounts, window, cx); + self.accounts.update(cx, |s, cx| s.open_add(window, cx)); + return; + } Command::AddCategory => View::Categories, Command::AddSubscription => View::Subscriptions, }; @@ -121,6 +161,7 @@ impl Render for Shell { let t = cx.omarchy().clone(); let content = match self.view { View::Overview => overview::render(&self.overview, cx).into_any_element(), + View::Accounts => self.accounts.clone().into_any_element(), other => placeholder(other, cx).into_any_element(), }; div() @@ -144,33 +185,32 @@ impl Render for Shell { .child(sidebar(self, cx)) .child(div().flex_1().min_w_0().h_full().child(content)) .when_some(self.palette.clone(), |el, palette| el.child(palette)) + .when_some(self.toast.clone(), |el, (_, message, ok)| { + el.child( + div() + .absolute() + .bottom(rems(1.25)) + .right(rems(1.25)) + .px(rems(0.875)) + .py(rems(0.625)) + .border_1() + .border_color(if ok { t.success } else { t.danger }) + .bg(t.surface) + .text_color(if ok { t.success } else { t.danger }) + .child(message), + ) + }) } } #[cfg(test)] mod tests { use super::*; - use chelete_lib::{database, seed}; + use crate::ui::test_support::{init, seeded_db}; use gpui_kit::TestAppContext; - use rusqlite::Connection; - - fn seeded_db() -> Arc { - let conn = Connection::open_in_memory().unwrap(); - conn.execute_batch("PRAGMA foreign_keys=ON;").unwrap(); - database::run_migrations(&conn).unwrap(); - seed::seed_database(&conn).unwrap(); - Arc::new(DbState(std::sync::Mutex::new(conn))) - } fn setup(cx: &mut TestAppContext) -> (Entity, &mut gpui_kit::VisualTestContext) { - cx.update(|cx| { - gpui_kit::init(cx); - gpui_omarchy::init(cx); - // Applying a fixed theme stops the filesystem watcher, which the - // deterministic test scheduler rejects. - gpui_omarchy::Theme::tokyo_night().apply(cx); - bind_keys(cx); - }); + init(cx); let db = seeded_db(); cx.add_window_view(|window, cx| Shell::new(db, Prefs::default(), false, window, cx)) } @@ -247,4 +287,17 @@ mod tests { // Entries are ordered like View::ALL: Overview, Transactions, Accounts. assert_eq!(view_of(&shell, cx), View::Accounts); } + + #[gpui_kit::test] + fn palette_add_account_opens_the_form(cx: &mut TestAppContext) { + let (shell, cx) = setup(cx); + cx.simulate_keystrokes("ctrl-k"); + cx.simulate_input("add account"); + cx.simulate_keystrokes("enter"); + assert_eq!(view_of(&shell, cx), View::Accounts); + cx.update(|_, cx| { + let accounts = shell.read(cx).accounts.clone(); + assert!(accounts.read(cx).has_dialog(), "add form is open"); + }); + } } diff --git a/src/ui/test_support.rs b/src/ui/test_support.rs new file mode 100644 index 0000000..1ffed9d --- /dev/null +++ b/src/ui/test_support.rs @@ -0,0 +1,32 @@ +//! Shared helpers for headless UI tests. +use chelete_lib::database::{self, DbState}; +use chelete_lib::seed; +use gpui_kit::TestAppContext; +use rusqlite::Connection; +use std::sync::{Arc, Mutex}; + +pub fn seeded_db() -> Arc { + let conn = Connection::open_in_memory().unwrap(); + conn.execute_batch("PRAGMA foreign_keys=ON;").unwrap(); + database::run_migrations(&conn).unwrap(); + seed::seed_database(&conn).unwrap(); + Arc::new(DbState(Mutex::new(conn))) +} + +pub fn empty_db() -> Arc { + let conn = Connection::open_in_memory().unwrap(); + conn.execute_batch("PRAGMA foreign_keys=ON;").unwrap(); + database::run_migrations(&conn).unwrap(); + Arc::new(DbState(Mutex::new(conn))) +} + +pub fn init(cx: &mut TestAppContext) { + cx.update(|cx| { + gpui_kit::init(cx); + gpui_omarchy::init(cx); + // Applying a fixed theme stops the filesystem watcher, which the + // deterministic test scheduler rejects. + gpui_omarchy::Theme::tokyo_night().apply(cx); + super::bind_keys(cx); + }); +} diff --git a/src/ui/widgets.rs b/src/ui/widgets.rs new file mode 100644 index 0000000..e577b62 --- /dev/null +++ b/src/ui/widgets.rs @@ -0,0 +1,162 @@ +//! Small building blocks shared by the screens: page headers, form fields, +//! modals, inputs and choice lists. +use gpui_kit::base::input::InputState; +use gpui_kit::{ + div, prelude::*, rems, App, Context, Div, Entity, FontWeight, IntoElement, MouseButton, + MouseDownEvent, SharedString, Window, +}; +use gpui_omarchy::{ActiveTheme as _, ChoiceItem, ChoiceState}; + +/// Emitted by screens and forms so the shell can show a toast. +#[derive(Clone, Debug)] +pub enum ScreenEvent { + Toast { message: String, ok: bool }, +} + +impl ScreenEvent { + pub fn ok(message: impl Into) -> Self { + Self::Toast { message: message.into(), ok: true } + } + pub fn error(message: impl Into) -> Self { + Self::Toast { message: message.into(), ok: false } + } +} + +pub fn page_header(title: &str, actions: impl IntoElement, cx: &App) -> Div { + let t = cx.omarchy(); + div() + .flex() + .items_center() + .justify_between() + .child( + div() + .text_size(rems(1.25)) + .font_weight(FontWeight::BOLD) + .text_color(t.foreground) + .child(title.to_string()), + ) + .child(div().flex().gap(rems(0.5)).child(actions)) +} + +pub fn field(label: &str, body: impl IntoElement, cx: &App) -> Div { + div() + .flex() + .flex_col() + .flex_1() + .gap(rems(0.25)) + .child(div().text_color(cx.omarchy().secondary).child(label.to_string())) + .child(body) +} + +/// A centered dialog over a dimmed, click-to-dismiss backdrop. Place it as the +/// last child of a `relative` container. +pub fn modal( + title: &str, + body: impl IntoElement, + on_dismiss: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static, + cx: &App, +) -> impl IntoElement { + let t = cx.omarchy(); + div() + .id("modal-backdrop") + .absolute() + .top_0() + .left_0() + .size_full() + .flex() + .justify_center() + .items_center() + .bg(t.background.opacity(0.6)) + .occlude() + .on_mouse_down(MouseButton::Left, on_dismiss) + .child( + div() + .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) + .w(rems(26.)) + .max_w_full() + .max_h_full() + .flex() + .flex_col() + .gap(rems(0.875)) + .p(rems(1.125)) + .border_1() + .border_color(t.border) + .bg(t.background) + .font_family(t.font.clone()) + .text_size(rems(0.75)) + .text_color(t.foreground) + .child( + div() + .text_size(rems(1.)) + .font_weight(FontWeight::BOLD) + .child(title.to_string()), + ) + .child(body), + ) +} + +pub fn text_input( + placeholder: &str, + window: &mut Window, + cx: &mut Context, +) -> Entity { + let placeholder = placeholder.to_string(); + cx.new(|cx| InputState::new(window, cx).placeholder(placeholder)) +} + +pub fn text_input_with( + placeholder: &str, + value: &str, + window: &mut Window, + cx: &mut Context, +) -> Entity { + let placeholder = placeholder.to_string(); + let value = value.to_string(); + cx.new(|cx| InputState::new(window, cx).placeholder(placeholder).default_value(value)) +} + +pub fn text_of(input: &Entity, cx: &App) -> String { + input.read(cx).value().trim().to_string() +} + +/// A select over `(value, label)` pairs with `default` pre-selected when it is +/// one of the values. +pub fn choice( + items: Vec<(String, String)>, + default: Option<&str>, + window: &mut Window, + cx: &mut Context, +) -> Entity { + let selected = default.and_then(|d| items.iter().position(|(v, _)| v == d)); + let items: Vec = items + .into_iter() + .map(|(v, l)| ChoiceItem::new(SharedString::from(v), SharedString::from(l))) + .collect(); + cx.new(|cx| { + let state = ChoiceState::new(items, window, cx); + match selected { + Some(i) => state.default_selected(i), + None => state, + } + }) +} + +pub fn choice_value(state: &Entity, cx: &App) -> Option { + state.read(cx).selected().map(|i| i.value.to_string()) +} + +/// The standard account types, in the order the web UI listed them. +pub fn account_type_options() -> Vec<(String, String)> { + [ + ("cash", "Cash"), + ("bank", "Bank"), + ("savings", "Savings"), + ("credit_card", "Credit Card"), + ("mobile_money", "Mobile Money"), + ("investment", "Investment"), + ("other", "Other"), + ] + .into_iter() + .map(|(v, l)| (v.to_string(), l.to_string())) + .collect() +} From e15ecf2e31c39e6965c3adcca6cb2f5c37bb74c1 Mon Sep 17 00:00:00 2001 From: Victor Phos Date: Fri, 2 Oct 2026 14:05:56 +0200 Subject: [PATCH 18/36] Port the Categories and Settings screens Expense and income lists with record counts, add/edit forms and an icon picker drawn from the asset bundle (two Lucide icons were renamed since categories were first saved, so those names are aliased). Settings shows the theme, currency and an About card; the rounded-corners option is dropped because gpui-omarchy draws square controls. --- src/ui/accounts.rs | 1 + src/ui/categories.rs | 460 +++++++++++++++++++++++++++++++++++++++++++ src/ui/icons.rs | 99 ++++++++++ src/ui/mod.rs | 3 + src/ui/settings.rs | 76 +++++++ src/ui/shell.rs | 15 +- 6 files changed, 653 insertions(+), 1 deletion(-) create mode 100644 src/ui/categories.rs create mode 100644 src/ui/icons.rs create mode 100644 src/ui/settings.rs diff --git a/src/ui/accounts.rs b/src/ui/accounts.rs index 896e997..f2f1273 100644 --- a/src/ui/accounts.rs +++ b/src/ui/accounts.rs @@ -42,6 +42,7 @@ impl AccountsScreen { self.accounts.as_deref().unwrap_or(&[]) } + #[cfg(test)] pub fn has_dialog(&self) -> bool { self.dialog.is_some() } diff --git a/src/ui/categories.rs b/src/ui/categories.rs new file mode 100644 index 0000000..cb6a3be --- /dev/null +++ b/src/ui/categories.rs @@ -0,0 +1,460 @@ +use super::accounts::FormEvent; +use super::icons::{category_icon, CATEGORY_ICONS, DEFAULT_ICON}; +use super::widgets::*; +use chelete_lib::commands::{self, Category, CreateCategoryRequest, Transaction, UpdateCategoryRequest}; +use chelete_lib::database::DbState; +use gpui_kit::base::input::InputState; +use gpui_kit::{ + div, prelude::*, px, rems, Context, Entity, EventEmitter, Hsla, IntoElement, Render, + SharedString, Subscription, Window, +}; +use gpui_omarchy::{ + button, icon, icon_button, input, select, ActiveTheme as _, ButtonVariant, ChoiceState, + IconName, +}; +use std::collections::HashMap; +use std::sync::Arc; + +/// How many transactions reference each category. +pub fn record_counts(transactions: &[Transaction]) -> HashMap { + let mut counts = HashMap::new(); + for t in transactions { + if let Some(id) = &t.category_id { + *counts.entry(id.clone()).or_insert(0) += 1; + } + } + counts +} + +fn records_label(count: usize) -> String { + format!("{count} {}", if count == 1 { "record" } else { "records" }) +} + +pub struct CategoriesScreen { + db: Arc, + categories: Result, String>, + counts: HashMap, + form: Option>, + _form_sub: Option, +} + +impl EventEmitter for CategoriesScreen {} + +impl CategoriesScreen { + pub fn new(db: Arc) -> Self { + let mut screen = Self { + db, + categories: Ok(Vec::new()), + counts: HashMap::new(), + form: None, + _form_sub: None, + }; + screen.load(); + screen + } + + fn load(&mut self) { + self.categories = commands::get_categories(&self.db); + self.counts = commands::get_transactions(&self.db) + .map(|t| record_counts(&t)) + .unwrap_or_default(); + } + + pub fn reload(&mut self, cx: &mut Context) { + self.load(); + cx.notify(); + } + + pub fn categories(&self) -> &[Category] { + self.categories.as_deref().unwrap_or(&[]) + } + + #[cfg(test)] + pub fn has_form(&self) -> bool { + self.form.is_some() + } + + pub fn open_add(&mut self, window: &mut Window, cx: &mut Context) { + self.open(None, window, cx); + } + + fn open_edit(&mut self, category: Category, window: &mut Window, cx: &mut Context) { + self.open(Some(category), window, cx); + } + + fn open(&mut self, editing: Option, window: &mut Window, cx: &mut Context) { + let form = cx.new(|cx| CategoryForm::new(self.db.clone(), editing, window, cx)); + self._form_sub = Some(cx.subscribe(&form, |this, _, event: &FormEvent, cx| { + match event { + FormEvent::Cancel => {} + FormEvent::Saved(message) => { + this.load(); + cx.emit(ScreenEvent::ok(message.clone())); + } + FormEvent::Failed(message) => { + cx.emit(ScreenEvent::error(message.clone())); + return; + } + } + this.form = None; + this._form_sub = None; + cx.notify(); + })); + self.form = Some(form); + cx.notify(); + } + + fn delete(&mut self, id: String, cx: &mut Context) { + match commands::delete_category(&self.db, id) { + Ok(()) => self.load(), + Err(e) => cx.emit(ScreenEvent::error(format!("Could not delete category: {e}"))), + } + cx.notify(); + } + + fn section(&self, title: &str, kind: &str, cx: &mut Context) -> impl IntoElement { + let t = cx.omarchy().clone(); + let rows: Vec = self + .categories() + .iter() + .filter(|c| c.category_type == kind) + .cloned() + .collect(); + let body = if rows.is_empty() { + div() + .py(rems(1.)) + .text_color(t.secondary) + .child(format!("No {kind} categories")) + .into_any_element() + } else { + let mut list = div().flex().flex_col().border_1().border_color(t.border); + for (i, c) in rows.into_iter().enumerate() { + let count = self.counts.get(&c.id).copied().unwrap_or(0); + let color = c.color.as_deref().and_then(parse_hex).unwrap_or(t.secondary); + let edit = c.clone(); + let id = c.id.clone(); + list = list.child( + div() + .flex() + .items_center() + .gap(rems(0.5)) + .px(rems(0.5)) + .py(rems(0.25)) + .when(i > 0, |r| r.border_t_1().border_color(t.border)) + .child( + div() + .flex() + .flex_1() + .items_center() + .gap(rems(0.5)) + .children(c.icon.as_deref().map(|n| category_icon(n, px(14.), color))) + .child(c.name.clone()), + ) + .child( + div() + .text_color(if count > 0 { t.secondary } else { t.foreground.opacity(0.4) }) + .child(records_label(count)), + ) + .child( + icon_button( + SharedString::from(format!("edit-cat-{}", c.id)), + IconName::Pencil, + "Edit category", + ButtonVariant::Secondary, + cx, + ) + .on_click(cx.listener(move |this, _, window, cx| { + this.open_edit(edit.clone(), window, cx) + })), + ) + .child( + icon_button( + SharedString::from(format!("del-cat-{}", c.id)), + IconName::Trash, + "Delete category", + ButtonVariant::Danger, + cx, + ) + .on_click(cx.listener(move |this, _, _, cx| this.delete(id.clone(), cx))), + ), + ); + } + list.into_any_element() + }; + div() + .flex_1() + .flex() + .flex_col() + .gap(rems(0.75)) + .child(div().text_size(rems(0.875)).font_weight(gpui_kit::FontWeight::BOLD).child(title.to_string())) + .child(body) + } +} + +pub fn parse_hex(value: &str) -> Option { + let hex = value.trim().trim_start_matches('#'); + if hex.len() != 6 { + return None; + } + u32::from_str_radix(hex, 16).ok().map(|v| gpui_kit::rgb(v).into()) +} + +impl Render for CategoriesScreen { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let add = button("add-category", "+ Add", ButtonVariant::Primary, cx) + .on_click(cx.listener(|this, _, window, cx| this.open_add(window, cx))); + let expenses = self.section("Expenses", "expense", cx); + let income = self.section("Income", "income", cx); + div() + .id("categories") + .relative() + .size_full() + .overflow_y_scroll() + .p(rems(1.5)) + .flex() + .flex_col() + .gap(rems(1.)) + .child(page_header("Categories", add, cx)) + .child(div().flex().gap(rems(1.5)).child(expenses).child(income)) + .children(self.form.clone()) + } +} + +pub struct CategoryForm { + db: Arc, + editing: Option, + name: Entity, + kind: Entity, + selected_icon: String, +} + +impl EventEmitter for CategoryForm {} + +impl CategoryForm { + fn new(db: Arc, editing: Option, window: &mut Window, cx: &mut Context) -> Self { + let (name, kind, icon) = match &editing { + Some(c) => (c.name.clone(), c.category_type.clone(), c.icon.clone().unwrap_or_else(|| DEFAULT_ICON.into())), + None => (String::new(), "expense".into(), DEFAULT_ICON.into()), + }; + let name_input = text_input_with("e.g. Groceries", &name, window, cx); + name_input.update(cx, |s, cx| s.focus(window, cx)); + Self { + db, + editing: editing.map(|c| c.id), + name: name_input, + kind: choice( + vec![("expense".into(), "Expense".into()), ("income".into(), "Income".into())], + Some(&kind), + window, + cx, + ), + selected_icon: icon, + } + } + + fn submit(&mut self, cx: &mut Context) { + let name = text_of(&self.name, cx); + if name.is_empty() { + return; + } + let category_type = choice_value(&self.kind, cx).unwrap_or_else(|| "expense".into()); + let result = match &self.editing { + None => commands::create_category( + &self.db, + CreateCategoryRequest { + name: name.clone(), + parent_id: None, + category_type, + icon: Some(self.selected_icon.clone()), + color: None, + }, + ) + .map(|_| format!("Added category {name}")), + Some(id) => commands::update_category( + &self.db, + UpdateCategoryRequest { + id: id.clone(), + name: Some(name.clone()), + parent_id: None, + category_type: Some(category_type), + icon: Some(Some(self.selected_icon.clone())), + color: None, + }, + ) + .map(|_| format!("Saved category {name}")), + }; + match result { + Ok(message) => cx.emit(FormEvent::Saved(message)), + Err(e) => cx.emit(FormEvent::Failed(format!("Could not save category: {e}"))), + } + } +} + +impl Render for CategoryForm { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let t = cx.omarchy().clone(); + let picker = div().flex().flex_wrap().gap(rems(0.25)).children(CATEGORY_ICONS.iter().map(|(name, label)| { + let selected = self.selected_icon == *name; + let name_owned = name.to_string(); + let fg = if selected { t.accent } else { t.foreground }; + button( + SharedString::from(format!("pick-icon-{name}")), + "", + if selected { ButtonVariant::Primary } else { ButtonVariant::Secondary }, + cx, + ) + .accessibility_label(*label) + .p(rems(0.375)) + .child(category_icon(name, px(16.), fg)) + .on_click(cx.listener(move |this, _, _, cx| { + this.selected_icon = name_owned.clone(); + cx.notify(); + })) + })); + let editing = self.editing.is_some(); + let body = div() + .flex() + .flex_col() + .gap(rems(0.75)) + .child(field("Name", input("category-name", &self.name, window, cx), cx)) + .child(field("Type", select("category-type", &self.kind, window, cx), cx)) + .child(field("Icon", picker, cx)) + .child( + div() + .flex() + .justify_end() + .gap(rems(0.5)) + .child( + button("category-cancel", "Cancel", ButtonVariant::Outline, cx) + .on_click(cx.listener(|_, _, _, cx| cx.emit(FormEvent::Cancel))), + ) + .child( + button("category-submit", if editing { "Save" } else { "Add" }, ButtonVariant::Primary, cx) + .on_click(cx.listener(|this, _, _, cx| this.submit(cx))), + ), + ); + let _ = icon; + modal( + if editing { "Edit Category" } else { "Add Category" }, + body, + cx.listener(|_, _, _, cx| cx.emit(FormEvent::Cancel)), + cx, + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ui::test_support::{init, seeded_db}; + use gpui_kit::{TestAppContext, VisualTestContext}; + + fn setup(cx: &mut TestAppContext) -> (Entity, &mut VisualTestContext) { + init(cx); + let db = seeded_db(); + cx.add_window_view(|_, _| CategoriesScreen::new(db)) + } + + fn type_into(input: &Entity, text: &str, cx: &mut VisualTestContext) { + cx.update(|window, cx| input.update(cx, |i, cx| i.set_value(text, window, cx))); + } + + #[test] + fn counts_records_per_category() { + let tx = |cat: Option<&str>| Transaction { + id: "t".into(), + account_id: "a".into(), + category_id: cat.map(String::from), + transaction_type: "expense".into(), + amount: 1, + currency: "USD".into(), + description: "d".into(), + merchant: None, + notes: None, + transaction_date: "2026-01-01".into(), + created_at: String::new(), + updated_at: String::new(), + }; + let counts = record_counts(&[tx(Some("a")), tx(Some("a")), tx(Some("b")), tx(None)]); + assert_eq!(counts["a"], 2); + assert_eq!(counts["b"], 1); + assert_eq!(counts.len(), 2); + assert_eq!(records_label(1), "1 record"); + assert_eq!(records_label(0), "0 records"); + } + + #[test] + fn parses_hex_colors() { + assert!(parse_hex("#7aa2f7").is_some()); + assert!(parse_hex("7aa2f7").is_some()); + assert!(parse_hex("nope").is_none()); + assert!(parse_hex("#fff").is_none()); + } + + #[gpui_kit::test] + fn lists_seeded_categories_with_counts(cx: &mut TestAppContext) { + let (screen, cx) = setup(cx); + cx.update(|_, cx| { + let s = screen.read(cx); + assert_eq!(s.categories().len(), 8); + assert!(s.counts.values().sum::() > 0, "seeded transactions reference categories"); + }); + } + + #[gpui_kit::test] + fn add_category_saves(cx: &mut TestAppContext) { + let (screen, cx) = setup(cx); + cx.update(|window, cx| screen.update(cx, |s, cx| s.open_add(window, cx))); + let form = cx.update(|_, cx| screen.read(cx).form.clone().expect("form open")); + let name = cx.update(|_, cx| form.read(cx).name.clone()); + type_into(&name, "Pets", cx); + cx.update(|_, cx| form.update(cx, |f, cx| { + f.selected_icon = "dog".into(); + f.submit(cx) + })); + cx.update(|_, cx| { + let s = screen.read(cx); + assert!(!s.has_form()); + let added = s.categories().iter().find(|c| c.name == "Pets").expect("saved"); + assert_eq!(added.icon.as_deref(), Some("dog")); + assert_eq!(added.category_type, "expense"); + }); + } + + #[gpui_kit::test] + fn edit_category_updates_in_place(cx: &mut TestAppContext) { + let (screen, cx) = setup(cx); + let original = cx.update(|_, cx| screen.read(cx).categories()[0].clone()); + cx.update(|window, cx| screen.update(cx, |s, cx| s.open_edit(original.clone(), window, cx))); + let form = cx.update(|_, cx| screen.read(cx).form.clone().expect("form open")); + let name = cx.update(|_, cx| form.read(cx).name.clone()); + cx.update(|_, cx| assert_eq!(name.read(cx).value().as_ref(), original.name)); + type_into(&name, "Renamed", cx); + cx.update(|_, cx| form.update(cx, |f, cx| f.submit(cx))); + cx.update(|_, cx| { + let s = screen.read(cx); + assert_eq!(s.categories().len(), 8, "no duplicate created"); + assert_eq!(s.categories().iter().filter(|c| c.id == original.id).next().unwrap().name, "Renamed"); + }); + } + + #[gpui_kit::test] + fn blank_name_keeps_form_open(cx: &mut TestAppContext) { + let (screen, cx) = setup(cx); + cx.update(|window, cx| screen.update(cx, |s, cx| s.open_add(window, cx))); + let form = cx.update(|_, cx| screen.read(cx).form.clone().unwrap()); + cx.update(|_, cx| form.update(cx, |f, cx| f.submit(cx))); + cx.update(|_, cx| { + assert!(screen.read(cx).has_form()); + assert_eq!(screen.read(cx).categories().len(), 8); + }); + } + + #[gpui_kit::test] + fn delete_removes_category(cx: &mut TestAppContext) { + let (screen, cx) = setup(cx); + let id = cx.update(|_, cx| screen.read(cx).categories()[0].id.clone()); + cx.update(|_, cx| screen.update(cx, |s, cx| s.delete(id.clone(), cx))); + cx.update(|_, cx| assert!(screen.read(cx).categories().iter().all(|c| c.id != id))); + } +} diff --git a/src/ui/icons.rs b/src/ui/icons.rs new file mode 100644 index 0000000..db83c5b --- /dev/null +++ b/src/ui/icons.rs @@ -0,0 +1,99 @@ +//! Category icons. The database stores Lucide names; they are drawn straight +//! from the asset bundle. +use gpui_kit::{prelude::*, svg, Hsla, Pixels, Svg}; + +/// `(stored name, label)` in the order the picker shows them. +pub const CATEGORY_ICONS: &[(&str, &str)] = &[ + ("utensils", "Food"), + ("car", "Car"), + ("briefcase", "Work"), + ("film", "Film"), + ("shopping-bag", "Shopping"), + ("zap", "Bills"), + ("heart", "Health"), + ("book-open", "Education"), + ("coffee", "Coffee"), + ("plane", "Travel"), + ("home", "Home"), + ("shopping-cart", "Cart"), + ("wifi", "Internet"), + ("smartphone", "Phone"), + ("music", "Music"), + ("gamepad-2", "Gaming"), + ("gift", "Gift"), + ("stethoscope", "Medical"), + ("graduation-cap", "School"), + ("building-2", "Office"), + ("piggy-bank", "Savings"), + ("credit-card", "Credit"), + ("wallet", "Wallet"), + ("wrench", "Repairs"), + ("bot", "AI"), + ("code", "Code"), + ("laptop", "Laptop"), + ("tv", "Streaming"), + ("dumbbell", "Fitness"), + ("dog", "Dog"), + ("cat", "Cat"), + ("leaf", "Nature"), + ("shirt", "Clothing"), + ("scissors", "Grooming"), + ("bus", "Bus"), + ("train", "Train"), + ("hotel", "Hotel"), + ("wine", "Drinks"), + ("landmark", "Taxes"), + ("shield", "Insurance"), + ("cloud", "Cloud"), + ("circle-question-mark", "Unaccounted"), + ("user-round", "Woman"), +]; + +pub const DEFAULT_ICON: &str = "utensils"; + +/// Bundle file stem for a stored name. Two Lucide icons were renamed after +/// categories were first saved with the old names. +fn file_stem(name: &str) -> &str { + match name { + "home" => "house", + "train" => "train-front", + other => other, + } +} + +pub fn asset_path(name: &str) -> String { + format!("icons/{}.svg", file_stem(name)) +} + +pub fn category_icon(name: &str, size: Pixels, color: Hsla) -> Svg { + svg().path(asset_path(name)).size(size).flex_shrink_0().text_color(color) +} + +#[cfg(test)] +mod tests { + use super::*; + use gpui_kit::AssetSource as _; + + #[test] + fn every_category_icon_is_in_the_bundle() { + for (name, _) in CATEGORY_ICONS { + let loaded = gpui_kit::assets::AllAssets.load(&asset_path(name)).unwrap(); + assert!(loaded.is_some(), "icon {name} missing from the asset bundle"); + } + } + + #[test] + fn renamed_icons_resolve() { + assert_eq!(asset_path("home"), "icons/house.svg"); + assert_eq!(asset_path("train"), "icons/train-front.svg"); + assert_eq!(asset_path("utensils"), "icons/utensils.svg"); + } + + #[test] + fn names_are_unique() { + let mut names: Vec<_> = CATEGORY_ICONS.iter().map(|(n, _)| *n).collect(); + names.sort(); + names.dedup(); + assert_eq!(names.len(), CATEGORY_ICONS.len()); + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index a9a5b1f..add32e5 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,9 +1,12 @@ //! GPUI front end: app shell, sidebar, command palette and screens. mod accounts; +mod categories; +mod icons; mod overview; mod palette; mod placeholder; mod shell; +mod settings; mod sidebar; mod widgets; diff --git a/src/ui/settings.rs b/src/ui/settings.rs new file mode 100644 index 0000000..55df8aa --- /dev/null +++ b/src/ui/settings.rs @@ -0,0 +1,76 @@ +use super::widgets::page_header; +use gpui_kit::{div, prelude::*, rems, App, Div, IntoElement}; +use gpui_omarchy::{panel, separator, ActiveTheme as _}; + +const VERSION: &str = env!("CARGO_PKG_VERSION"); + +fn row(label: &str, description: Option<&str>, value: impl IntoElement, cx: &App) -> Div { + let t = cx.omarchy(); + div() + .flex() + .items_center() + .justify_between() + .child( + div() + .child(label.to_string()) + .children(description.map(|d| div().text_color(t.secondary).child(d.to_string()))), + ) + .child(div().text_color(t.secondary).child(value)) +} + +pub fn render(cx: &App) -> impl IntoElement { + let t = cx.omarchy(); + let theme_name = t.name.to_string(); + + let appearance = panel("Appearance", cx) + .child(row("Theme", Some("Follows the Omarchy system theme"), theme_name, cx)) + .child(separator(cx)) + .child(row("Corners", Some("Square, like the rest of Omarchy"), "Square", cx)); + + let finance = panel("Finance", cx) + .child(row("Base Currency", None, "USD", cx)) + .child(separator(cx)) + .child(row("Month Start", None, "1st", cx)); + + let about = panel("About", cx) + .child( + div() + .flex() + .items_center() + .gap(rems(0.75)) + .child( + div() + .px(rems(0.75)) + .py(rems(0.5)) + .border_1() + .border_color(t.accent) + .text_color(t.accent) + .child("115 Ch"), + ) + .child( + div() + .child(div().text_size(rems(1.)).font_weight(gpui_kit::FontWeight::BOLD).child("Chelete")) + .child(div().text_color(t.secondary).child(format!("Version {VERSION}"))), + ), + ) + .child(div().text_color(t.secondary).child( + "A fast, keyboard-driven personal finance tracker for Linux. Manage accounts, \ + track transactions, and project your balance over time — without touching the \ + mouse. Built for Omarchy with live theme integration.", + )) + .child(separator(cx)) + .child(row("Author", None, "Victor Phos", cx)) + .child(separator(cx)) + .child(row("License", None, "MIT", cx)); + + div() + .id("settings") + .size_full() + .overflow_y_scroll() + .p(rems(1.5)) + .flex() + .flex_col() + .gap(rems(1.)) + .child(page_header("Settings", div(), cx)) + .child(div().max_w(rems(36.)).flex().flex_col().gap(rems(1.)).child(appearance).child(finance).child(about)) +} diff --git a/src/ui/shell.rs b/src/ui/shell.rs index 81e0a2f..1b4f930 100644 --- a/src/ui/shell.rs +++ b/src/ui/shell.rs @@ -1,4 +1,6 @@ use super::accounts::AccountsScreen; +use super::categories::CategoriesScreen; +use super::settings; use super::overview::{self, OverviewData}; use super::widgets::ScreenEvent; use super::palette::{Command, Palette, PaletteEvent}; @@ -21,6 +23,7 @@ pub struct Shell { persist: bool, overview: OverviewData, accounts: Entity, + categories: Entity, toast: Option<(u64, String, bool)>, toast_seq: u64, palette: Option>, @@ -40,9 +43,12 @@ impl Shell { focus.focus(window, cx); let accounts = cx.new(|_| AccountsScreen::new(db.clone())); cx.subscribe(&accounts, Self::on_screen_event).detach(); + let categories = cx.new(|_| CategoriesScreen::new(db.clone())); + cx.subscribe(&categories, Self::on_screen_event).detach(); Self { overview: OverviewData::load(&db), accounts, + categories, toast: None, toast_seq: 0, db, @@ -69,6 +75,7 @@ impl Shell { match view { View::Overview => self.overview = OverviewData::load(&self.db), View::Accounts => self.accounts.update(cx, |s, cx| s.reload(cx)), + View::Categories => self.categories.update(cx, |s, cx| s.reload(cx)), _ => {} } cx.notify(); @@ -143,7 +150,11 @@ impl Shell { self.accounts.update(cx, |s, cx| s.open_add(window, cx)); return; } - Command::AddCategory => View::Categories, + Command::AddCategory => { + self.navigate(View::Categories, window, cx); + self.categories.update(cx, |s, cx| s.open_add(window, cx)); + return; + } Command::AddSubscription => View::Subscriptions, }; self.navigate(target, window, cx); @@ -162,6 +173,8 @@ impl Render for Shell { let content = match self.view { View::Overview => overview::render(&self.overview, cx).into_any_element(), View::Accounts => self.accounts.clone().into_any_element(), + View::Categories => self.categories.clone().into_any_element(), + View::Settings => settings::render(cx).into_any_element(), other => placeholder(other, cx).into_any_element(), }; div() From a0c6d9c806cd0e78c3cf6762bd07cfef17b1f573 Mon Sep 17 00:00:00 2001 From: Victor Phos Date: Fri, 2 Oct 2026 14:06:09 +0200 Subject: [PATCH 19/36] Bridge the Omarchy theme to gpui-component and add date fields gpui-omarchy styles its own controls, but gpui-component widgets (the date picker) read a separate theme. Copy the Omarchy palette across at startup and whenever the system theme changes, and wrap the single and range date pickers so screens deal in NaiveDate. --- src/main.rs | 2 +- src/ui/date_field.rs | 43 ++++++++++++++++++++++++++++++++++++++++++ src/ui/mod.rs | 7 +++++++ src/ui/test_support.rs | 2 +- src/ui/theme_bridge.rs | 36 +++++++++++++++++++++++++++++++++++ 5 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 src/ui/date_field.rs create mode 100644 src/ui/theme_bridge.rs diff --git a/src/main.rs b/src/main.rs index 1aafe82..e00927d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,7 +15,7 @@ fn main() { .run(move |cx| { gpui_kit::init(cx); gpui_omarchy::init(cx); - ui::bind_keys(cx); + ui::install(cx); cx.on_action(|_: &ui::Quit, cx| cx.quit()); cx.on_window_closed(|cx, _| { if cx.windows().is_empty() { diff --git a/src/ui/date_field.rs b/src/ui/date_field.rs new file mode 100644 index 0000000..2233544 --- /dev/null +++ b/src/ui/date_field.rs @@ -0,0 +1,43 @@ +//! Thin wrappers over gpui-kit's date picker so screens deal in `NaiveDate`. +use chrono::NaiveDate; +use gpui_kit::component::calendar::Date; +use gpui_kit::component::date_picker::DatePickerState; +use gpui_kit::{prelude::*, App, Context, Entity, Window}; + +pub const FORMAT: &str = "%Y-%m-%d"; + +pub fn single( + initial: Option, + window: &mut Window, + cx: &mut Context, +) -> Entity { + cx.new(|cx| { + let mut state = DatePickerState::new(window, cx).date_format(FORMAT); + if let Some(date) = initial { + state.set_date(Date::Single(Some(date)), window, cx); + } + state + }) +} + +pub fn range(window: &mut Window, cx: &mut Context) -> Entity { + cx.new(|cx| DatePickerState::range(window, cx).date_format(FORMAT).number_of_months(2)) +} + +pub fn picked(state: &Entity, cx: &App) -> Option { + match state.read(cx).date() { + Date::Single(d) => d, + Date::Range(start, _) => start, + } +} + +pub fn picked_range(state: &Entity, cx: &App) -> (Option, Option) { + match state.read(cx).date() { + Date::Range(a, b) => (a, b), + Date::Single(d) => (d, d), + } +} + +pub fn parse_iso(value: &str) -> Option { + NaiveDate::parse_from_str(value, FORMAT).ok() +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index add32e5..42fd202 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,6 +1,7 @@ //! GPUI front end: app shell, sidebar, command palette and screens. mod accounts; mod categories; +mod date_field; mod icons; mod overview; mod palette; @@ -8,6 +9,7 @@ mod placeholder; mod shell; mod settings; mod sidebar; +mod theme_bridge; mod widgets; #[cfg(test)] @@ -101,6 +103,11 @@ impl View { pub const SHELL_CONTEXT: &str = "Shell"; pub const PALETTE_CONTEXT: &str = "Palette"; +pub fn install(cx: &mut App) { + bind_keys(cx); + theme_bridge::install(cx); +} + pub fn bind_keys(cx: &mut App) { let shell = Some(SHELL_CONTEXT); cx.bind_keys([ diff --git a/src/ui/test_support.rs b/src/ui/test_support.rs index 1ffed9d..105178a 100644 --- a/src/ui/test_support.rs +++ b/src/ui/test_support.rs @@ -27,6 +27,6 @@ pub fn init(cx: &mut TestAppContext) { // Applying a fixed theme stops the filesystem watcher, which the // deterministic test scheduler rejects. gpui_omarchy::Theme::tokyo_night().apply(cx); - super::bind_keys(cx); + super::install(cx); }); } diff --git a/src/ui/theme_bridge.rs b/src/ui/theme_bridge.rs new file mode 100644 index 0000000..72b7ebc --- /dev/null +++ b/src/ui/theme_bridge.rs @@ -0,0 +1,36 @@ +//! gpui-omarchy themes its own controls; gpui-component widgets (the date +//! picker) read a separate theme. Copy the Omarchy palette across and keep it +//! in step when the system theme changes. +use gpui_kit::component::Theme as ComponentTheme; +use gpui_kit::App; +use gpui_omarchy::ActiveTheme as _; + +pub fn sync(cx: &mut App) { + let o = cx.omarchy().clone(); + let t = ComponentTheme::global_mut(cx); + t.background = o.background; + t.foreground = o.foreground; + t.popover = o.surface; + t.popover_foreground = o.foreground; + t.primary = o.accent; + t.primary_foreground = o.on_accent; + t.accent = o.selection; + t.accent_foreground = o.foreground; + t.muted = o.surface; + t.muted_foreground = o.secondary; + t.border = o.border; + t.input = o.border; + t.ring = o.accent; + t.danger = o.danger; + t.chart_1 = o.chart[0]; + t.chart_2 = o.chart[1]; + t.chart_3 = o.chart[2]; + t.chart_4 = o.chart[3]; + t.chart_5 = o.chart[4]; +} + +/// Sync now and again whenever the Omarchy theme changes. +pub fn install(cx: &mut App) { + sync(cx); + cx.observe_global::(sync).detach(); +} From 5b3322030541da139e750f270b71a4603e4a6261 Mon Sep 17 00:00:00 2001 From: Victor Phos Date: Fri, 2 Oct 2026 14:06:15 +0200 Subject: [PATCH 20/36] Port the Subscriptions screen Frequency filter and totals, a paginated table with bulk selection and a confirmation before deleting, view/edit/add dialogs with the date picker, and one-click payments that record an expense. Adds the shared list widgets (checkboxes, pagination bar, confirm dialog). --- src/ui/mod.rs | 1 + src/ui/shell.rs | 13 +- src/ui/subscriptions.rs | 792 ++++++++++++++++++++++++++++++++++++++++ src/ui/widgets.rs | 136 ++++++- 4 files changed, 940 insertions(+), 2 deletions(-) create mode 100644 src/ui/subscriptions.rs diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 42fd202..7857629 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -9,6 +9,7 @@ mod placeholder; mod shell; mod settings; mod sidebar; +mod subscriptions; mod theme_bridge; mod widgets; diff --git a/src/ui/shell.rs b/src/ui/shell.rs index 1b4f930..fb27b09 100644 --- a/src/ui/shell.rs +++ b/src/ui/shell.rs @@ -1,6 +1,7 @@ use super::accounts::AccountsScreen; use super::categories::CategoriesScreen; use super::settings; +use super::subscriptions::SubscriptionsScreen; use super::overview::{self, OverviewData}; use super::widgets::ScreenEvent; use super::palette::{Command, Palette, PaletteEvent}; @@ -24,6 +25,7 @@ pub struct Shell { overview: OverviewData, accounts: Entity, categories: Entity, + subscriptions: Entity, toast: Option<(u64, String, bool)>, toast_seq: u64, palette: Option>, @@ -45,10 +47,13 @@ impl Shell { cx.subscribe(&accounts, Self::on_screen_event).detach(); let categories = cx.new(|_| CategoriesScreen::new(db.clone())); cx.subscribe(&categories, Self::on_screen_event).detach(); + let subscriptions = cx.new(|cx| SubscriptionsScreen::new(db.clone(), window, cx)); + cx.subscribe(&subscriptions, Self::on_screen_event).detach(); Self { overview: OverviewData::load(&db), accounts, categories, + subscriptions, toast: None, toast_seq: 0, db, @@ -76,6 +81,7 @@ impl Shell { View::Overview => self.overview = OverviewData::load(&self.db), View::Accounts => self.accounts.update(cx, |s, cx| s.reload(cx)), View::Categories => self.categories.update(cx, |s, cx| s.reload(cx)), + View::Subscriptions => self.subscriptions.update(cx, |s, cx| s.reload(cx)), _ => {} } cx.notify(); @@ -155,7 +161,11 @@ impl Shell { self.categories.update(cx, |s, cx| s.open_add(window, cx)); return; } - Command::AddSubscription => View::Subscriptions, + Command::AddSubscription => { + self.navigate(View::Subscriptions, window, cx); + self.subscriptions.update(cx, |s, cx| s.open_add(window, cx)); + return; + } }; self.navigate(target, window, cx); } @@ -174,6 +184,7 @@ impl Render for Shell { View::Overview => overview::render(&self.overview, cx).into_any_element(), View::Accounts => self.accounts.clone().into_any_element(), View::Categories => self.categories.clone().into_any_element(), + View::Subscriptions => self.subscriptions.clone().into_any_element(), View::Settings => settings::render(cx).into_any_element(), other => placeholder(other, cx).into_any_element(), }; diff --git a/src/ui/subscriptions.rs b/src/ui/subscriptions.rs new file mode 100644 index 0000000..95726f6 --- /dev/null +++ b/src/ui/subscriptions.rs @@ -0,0 +1,792 @@ +use super::accounts::FormEvent; +use super::date_field; +use super::icons::category_icon; +use super::widgets::*; +use chelete_lib::commands::{ + self, Account, Category, CreateSubscriptionRequest, CreateTransactionRequest, Subscription, + UpdateSubscriptionRequest, +}; +use chelete_lib::database::DbState; +use chelete_lib::format::{format_money, parse_cents}; +use chelete_lib::paging::{page_slice, Pager}; +use chelete_lib::subscriptions::{filter_by_frequency, frequency_label, next_payment_date, total_amount, FREQUENCIES}; +use gpui_kit::base::input::InputState; +use gpui_kit::component::date_picker::{DatePicker, DatePickerState}; +use gpui_kit::{ + div, prelude::*, px, rems, Context, Div, Entity, EventEmitter, IntoElement, Render, + SharedString, Subscription as GpuiSubscription, Window, +}; +use gpui_omarchy::{ + button, icon_button, input, panel, select, ActiveTheme as _, ButtonVariant, ChoiceState, IconName, +}; +use std::collections::HashSet; +use std::sync::Arc; + +enum Dialog { + Form(Entity), + View(Subscription), + Confirm(Entity, Vec), +} + +pub struct SubscriptionsScreen { + db: Arc, + subscriptions: Vec, + categories: Vec, + accounts: Vec, + filter: Entity, + applied_filter: String, + per_page: Entity, + applied_per_page: usize, + page: usize, + selected: HashSet, + dialog: Option, + _dialog_sub: Option, + _subs: Vec, +} + +impl EventEmitter for SubscriptionsScreen {} + +impl SubscriptionsScreen { + pub fn new(db: Arc, window: &mut Window, cx: &mut Context) -> Self { + let mut items = vec![("all".to_string(), "All Periods".to_string())]; + items.extend(FREQUENCIES.iter().map(|(v, l)| (v.to_string(), l.to_string()))); + let filter = choice(items, Some("all"), window, cx); + let per_page = per_page_choice(25, window, cx); + let subs = vec![ + cx.observe(&filter, |this, _, cx| this.on_controls_changed(cx)), + cx.observe(&per_page, |this, _, cx| this.on_controls_changed(cx)), + ]; + let mut screen = Self { + db, + subscriptions: Vec::new(), + categories: Vec::new(), + accounts: Vec::new(), + filter, + applied_filter: "all".into(), + per_page, + applied_per_page: 25, + page: 1, + selected: HashSet::new(), + dialog: None, + _dialog_sub: None, + _subs: subs, + }; + screen.load(); + screen + } + + fn load(&mut self) { + self.subscriptions = commands::get_subscriptions(&self.db).unwrap_or_default(); + self.categories = commands::get_categories(&self.db).unwrap_or_default(); + self.accounts = commands::get_accounts(&self.db).unwrap_or_default(); + let live: HashSet<&String> = self.subscriptions.iter().map(|s| &s.id).collect(); + self.selected.retain(|id| live.contains(id)); + } + + pub fn reload(&mut self, cx: &mut Context) { + self.load(); + cx.notify(); + } + + fn on_controls_changed(&mut self, cx: &mut Context) { + let filter = choice_value(&self.filter, cx).unwrap_or_else(|| "all".into()); + let per_page = choice_value(&self.per_page, cx) + .and_then(|v| v.parse().ok()) + .unwrap_or(self.applied_per_page); + if filter != self.applied_filter || per_page != self.applied_per_page { + self.applied_filter = filter; + self.applied_per_page = per_page; + self.page = 1; + } + cx.notify(); + } + + pub fn subscriptions(&self) -> &[Subscription] { + &self.subscriptions + } + + fn filtered(&self) -> Vec<&Subscription> { + filter_by_frequency(&self.subscriptions, &self.applied_filter) + } + + fn pager(&self) -> Pager { + Pager::new(self.page, self.applied_per_page, self.filtered().len()) + } + + #[cfg(test)] + fn has_dialog(&self) -> bool { + self.dialog.is_some() + } + + pub fn open_add(&mut self, window: &mut Window, cx: &mut Context) { + self.open_form(None, window, cx); + } + + fn open_form(&mut self, editing: Option, window: &mut Window, cx: &mut Context) { + let form = cx.new(|cx| SubscriptionForm::new(self.db.clone(), &self.categories, &self.accounts, editing, window, cx)); + self._dialog_sub = Some(cx.subscribe(&form, |this, _, event: &FormEvent, cx| { + match event { + FormEvent::Cancel => {} + FormEvent::Saved(message) => { + this.load(); + cx.emit(ScreenEvent::ok(message.clone())); + } + FormEvent::Failed(message) => { + cx.emit(ScreenEvent::error(message.clone())); + return; + } + } + this.close_dialog(cx); + })); + self.dialog = Some(Dialog::Form(form)); + cx.notify(); + } + + fn close_dialog(&mut self, cx: &mut Context) { + self.dialog = None; + self._dialog_sub = None; + cx.notify(); + } + + fn ask_delete(&mut self, ids: Vec, cx: &mut Context) { + if ids.is_empty() { + return; + } + let noun = plural(ids.len(), "subscription"); + let dialog = cx.new(|_| { + ConfirmDialog::new( + format!("Delete {noun}?"), + format!( + "This will permanently remove the selected subscription{}. This action cannot be undone.", + if ids.len() == 1 { "" } else { "s" } + ), + "Delete", + ) + }); + self._dialog_sub = Some(cx.subscribe(&dialog, |this, _, event: &ConfirmEvent, cx| { + if *event == ConfirmEvent::Confirm { + if let Some(Dialog::Confirm(_, ids)) = this.dialog.take() { + this.delete(ids, cx); + } + } + this.close_dialog(cx); + })); + self.dialog = Some(Dialog::Confirm(dialog, ids)); + cx.notify(); + } + + fn delete(&mut self, ids: Vec, cx: &mut Context) { + match commands::delete_subscriptions(&self.db, ids.clone()) { + Ok(()) => { + for id in &ids { + self.selected.remove(id); + } + self.load(); + let pager = self.pager(); + self.page = pager.page; + cx.emit(ScreenEvent::ok(format!("Deleted {}", plural(ids.len(), "subscription")))); + } + Err(e) => cx.emit(ScreenEvent::error(format!("Could not delete: {e}"))), + } + } + + fn pay(&mut self, sub: &Subscription, cx: &mut Context) { + let Some(account_id) = sub.account_id.clone().or_else(|| self.accounts.first().map(|a| a.id.clone())) else { + return cx.emit(ScreenEvent::error("Add an account before recording a payment")); + }; + let result = commands::create_transaction( + &self.db, + CreateTransactionRequest { + account_id, + category_id: sub.category_id.clone(), + transaction_type: "expense".into(), + amount: sub.amount, + currency: sub.currency.clone(), + description: sub.name.clone(), + merchant: None, + notes: None, + transaction_date: chrono::Local::now().date_naive().format("%Y-%m-%d").to_string(), + }, + ); + match result { + Ok(_) => { + self.load(); + cx.emit(ScreenEvent::ok(format!("Payment recorded for {}", sub.name))); + } + Err(_) => cx.emit(ScreenEvent::error("Failed to record payment")), + } + cx.notify(); + } + + fn toggle(&mut self, id: &str, cx: &mut Context) { + if !self.selected.remove(id) { + self.selected.insert(id.to_string()); + } + cx.notify(); + } + + fn toggle_page(&mut self, cx: &mut Context) { + let filtered = self.filtered(); + let ids: Vec = page_slice(&filtered, &self.pager()).iter().map(|s| s.id.clone()).collect(); + let all_selected = !ids.is_empty() && ids.iter().all(|id| self.selected.contains(id)); + for id in ids { + if all_selected { + self.selected.remove(&id); + } else { + self.selected.insert(id); + } + } + cx.notify(); + } + + fn category_name(&self, id: &Option) -> Option<&Category> { + id.as_ref().and_then(|id| self.categories.iter().find(|c| &c.id == id)) + } + + fn account_name(&self, id: &Option) -> Option<&str> { + id.as_ref() + .and_then(|id| self.accounts.iter().find(|a| &a.id == id)) + .map(|a| a.name.as_str()) + } +} + +fn dim(text: impl Into, cx: &gpui_kit::App) -> Div { + div().text_color(cx.omarchy().secondary).child(text.into()) +} + +fn cell(width: Option, child: impl IntoElement) -> Div { + let base = div().min_w_0().px(rems(0.5)); + match width { + Some(w) => base.w(rems(w)).flex_shrink_0().child(child), + None => base.flex_1().child(child), + } +} + +impl Render for SubscriptionsScreen { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let t = cx.omarchy().clone(); + let today = chrono::Local::now().date_naive(); + let filtered = self.filtered(); + let pager = self.pager(); + let rows: Vec = page_slice(&filtered, &pager).iter().map(|s| (*s).clone()).collect(); + let total = total_amount(&filtered); + let count = filtered.len(); + let selected_on_page = rows.iter().filter(|s| self.selected.contains(&s.id)).count(); + let header_state = selection_state(selected_on_page, rows.len()); + let filter_label = if self.applied_filter == "all" { "All Periods".to_string() } else { frequency_label(&self.applied_filter) }; + + let actions = if self.selected.is_empty() { + button("add-subscription", "+ Add", ButtonVariant::Primary, cx) + .on_click(cx.listener(|this, _, window, cx| this.open_add(window, cx))) + .into_any_element() + } else { + let ids: Vec = self.selected.iter().cloned().collect(); + button("delete-selected", format!("Delete {} selected", self.selected.len()), ButtonVariant::Danger, cx) + .on_click(cx.listener(move |this, _, _, cx| this.ask_delete(ids.clone(), cx))) + .into_any_element() + }; + + let stats = div() + .flex() + .gap(rems(1.)) + .child(panel(format!("Total ({filter_label})"), cx).flex_1().child( + div().text_size(rems(1.5)).child(format_money(total)), + )) + .child(panel("Subscriptions", cx).flex_1().child(div().text_size(rems(1.5)).child(count.to_string()))); + + let table = if rows.is_empty() { + div().py(rems(3.)).flex().justify_center().text_color(t.secondary).child("No subscriptions found.").into_any_element() + } else { + let header = div() + .flex() + .items_center() + .py(rems(0.375)) + .bg(t.surface) + .text_color(t.bright) + .font_weight(gpui_kit::FontWeight::BOLD) + .child(cell(Some(2.5), check("select-page", "", header_state, { + let screen = cx.entity(); + move |_, cx| screen.update(cx, |s, cx| s.toggle_page(cx)) + }, cx))) + .child(cell(None, "Name")) + .child(cell(None, "Category")) + .child(cell(None, "Account")) + .child(cell(Some(7.), "Period")) + .child(cell(Some(7.), "Start Date")) + .child(cell(Some(7.), "Next Payment")) + .child(cell(Some(7.), div().flex().justify_end().child("Amount"))) + .child(cell(Some(9.5), "")); + + let mut list = div().flex().flex_col().border_1().border_color(t.border).child(header); + for s in rows { + let next = next_payment_date(s.start_date.as_deref(), &s.frequency, today) + .map(|d| d.format("%Y-%m-%d").to_string()); + let category = self.category_name(&s.category_id).cloned(); + let account = self.account_name(&s.account_id).map(str::to_string); + let checked = self.selected.contains(&s.id); + let row_id = s.id.clone(); + let key = |prefix: &str| SharedString::from(format!("{prefix}-{}", s.id)); + let (view, edit, pay, del) = (s.clone(), s.clone(), s.clone(), s.id.clone()); + list = list.child( + div() + .flex() + .items_center() + .py(rems(0.25)) + .border_t_1() + .border_color(t.border) + .hover(|st| st.bg(t.surface)) + .child(cell(Some(2.5), check(key("sel"), "", if checked { gpui_kit::base::CheckboxState::Checked } else { gpui_kit::base::CheckboxState::Unchecked }, { + let screen = cx.entity(); + move |_, cx| screen.update(cx, |s, cx| s.toggle(&row_id, cx)) + }, cx))) + .child(cell(None, s.name.clone())) + .child(cell(None, match &category { + Some(c) => div() + .flex() + .items_center() + .gap(rems(0.25)) + .children(c.icon.as_deref().map(|n| category_icon(n, px(12.), t.secondary))) + .child(c.name.clone()) + .into_any_element(), + None => dim("—", cx).into_any_element(), + })) + .child(cell(None, account.map(|a| div().child(a)).unwrap_or_else(|| dim("—", cx)))) + .child(cell(Some(7.), frequency_label(&s.frequency))) + .child(cell(Some(7.), s.start_date.clone().map(|d| div().child(d)).unwrap_or_else(|| dim("—", cx)))) + .child(cell(Some(7.), next.map(|d| div().child(d)).unwrap_or_else(|| dim("—", cx)))) + .child(cell(Some(7.), div().flex().justify_end().child(format_money(s.amount)))) + .child(cell( + Some(9.5), + div() + .flex() + .justify_end() + .gap(rems(0.125)) + .child(icon_button(key("view"), IconName::Eye, "View subscription", ButtonVariant::Secondary, cx) + .on_click(cx.listener(move |this, _, _, cx| { this.dialog = Some(Dialog::View(view.clone())); cx.notify(); }))) + .child(icon_button(key("edit"), IconName::Pencil, "Edit subscription", ButtonVariant::Secondary, cx) + .on_click(cx.listener(move |this, _, window, cx| this.open_form(Some(edit.clone()), window, cx)))) + .child(icon_button(key("pay"), IconName::CreditCard, "Record payment", ButtonVariant::Secondary, cx) + .on_click(cx.listener(move |this, _, _, cx| this.pay(&pay.clone(), cx)))) + .child(icon_button(key("del"), IconName::Trash, "Delete subscription", ButtonVariant::Danger, cx) + .on_click(cx.listener(move |this, _, _, cx| this.ask_delete(vec![del.clone()], cx)))), + )), + ); + } + let screen = cx.entity(); + let (prev, next) = (screen.clone(), screen); + div() + .flex() + .flex_col() + .gap(rems(0.75)) + .child(list) + .child(pagination_bar( + pager, + &self.per_page, + move |_, _, cx| prev.update(cx, |s, cx| { s.page = s.page.saturating_sub(1).max(1); cx.notify(); }), + move |_, _, cx| next.update(cx, |s, cx| { s.page += 1; cx.notify(); }), + window, + cx, + )) + .into_any_element() + }; + + let dialog = self.dialog.as_ref().map(|d| match d { + Dialog::Form(f) => f.clone().into_any_element(), + Dialog::Confirm(c, _) => c.clone().into_any_element(), + Dialog::View(s) => self.view_dialog(s, cx).into_any_element(), + }); + + div() + .id("subscriptions") + .relative() + .size_full() + .overflow_y_scroll() + .p(rems(1.5)) + .flex() + .flex_col() + .gap(rems(1.)) + .child(page_header("Subscriptions", actions, cx)) + .child(div().w(rems(14.)).child(select("sub-filter", &self.filter, window, cx))) + .child(stats) + .child(table) + .children(dialog) + } +} + +impl SubscriptionsScreen { + fn view_dialog(&self, s: &Subscription, cx: &mut Context) -> impl IntoElement { + let today = chrono::Local::now().date_naive(); + let next = next_payment_date(s.start_date.as_deref(), &s.frequency, today) + .map(|d| d.format("%Y-%m-%d").to_string()); + let category = self.category_name(&s.category_id).map(|c| c.name.clone()); + let account = self.account_name(&s.account_id).map(str::to_string); + let row = |label: &str, value: String| { + div() + .flex() + .justify_between() + .child(dim(label.to_string(), cx)) + .child(value) + }; + let body = div() + .flex() + .flex_col() + .gap(rems(0.5)) + .child(row("Name", s.name.clone())) + .child(row("Amount", format_money(s.amount))) + .child(row("Period", frequency_label(&s.frequency))) + .child(row("Start Date", s.start_date.clone().unwrap_or_else(|| "—".into()))) + .child(row("Next Payment", next.unwrap_or_else(|| "—".into()))) + .child(row("Account", account.unwrap_or_else(|| "—".into()))) + .child(row("Category", category.unwrap_or_else(|| "—".into()))) + .child( + div().flex().justify_end().pt(rems(0.5)).child( + button("view-close", "Close", ButtonVariant::Primary, cx) + .on_click(cx.listener(|this, _, _, cx| this.close_dialog(cx))), + ), + ); + modal("Subscription", body, cx.listener(|this, _, _, cx| this.close_dialog(cx)), cx) + } +} + +pub struct SubscriptionForm { + db: Arc, + editing: Option, + name: Entity, + amount: Entity, + frequency: Entity, + category: Entity, + account: Entity, + start_date: Entity, +} + +impl EventEmitter for SubscriptionForm {} + +impl SubscriptionForm { + fn new( + db: Arc, + categories: &[Category], + accounts: &[Account], + editing: Option, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let mut category_items = vec![(String::new(), "None".to_string())]; + category_items.extend(categories.iter().filter(|c| c.category_type == "expense").map(|c| (c.id.clone(), c.name.clone()))); + let mut account_items = vec![(String::new(), "None".to_string())]; + account_items.extend(accounts.iter().map(|a| (a.id.clone(), a.name.clone()))); + let frequency_items = FREQUENCIES.iter().map(|(v, l)| (v.to_string(), l.to_string())).collect(); + + let (name, amount, frequency, category, account, start) = match &editing { + Some(s) => ( + s.name.clone(), + format!("{:.2}", s.amount as f64 / 100.0), + s.frequency.clone(), + s.category_id.clone().unwrap_or_default(), + s.account_id.clone().unwrap_or_default(), + s.start_date.as_deref().and_then(date_field::parse_iso), + ), + None => (String::new(), String::new(), "monthly".into(), String::new(), String::new(), None), + }; + let name_input = text_input_with("e.g. Netflix", &name, window, cx); + name_input.update(cx, |s, cx| s.focus(window, cx)); + Self { + db, + editing: editing.map(|s| s.id), + name: name_input, + amount: text_input_with("0.00", &amount, window, cx), + frequency: choice(frequency_items, Some(&frequency), window, cx), + category: choice(category_items, Some(&category), window, cx), + account: choice(account_items, Some(&account), window, cx), + start_date: date_field::single(start, window, cx), + } + } + + fn submit(&mut self, cx: &mut Context) { + let name = text_of(&self.name, cx); + let amount_text = text_of(&self.amount, cx); + if name.is_empty() || amount_text.is_empty() { + return; + } + let Some(amount) = parse_cents(&amount_text) else { + return cx.emit(FormEvent::Failed("Amount is not valid".into())); + }; + let frequency = choice_value(&self.frequency, cx).unwrap_or_else(|| "monthly".into()); + let none_if_blank = |v: Option| v.filter(|s| !s.is_empty()); + let category_id = none_if_blank(choice_value(&self.category, cx)); + let account_id = none_if_blank(choice_value(&self.account, cx)); + let start_date = date_field::picked(&self.start_date, cx).map(|d| d.format(date_field::FORMAT).to_string()); + + let result = match &self.editing { + None => commands::create_subscription( + &self.db, + CreateSubscriptionRequest { + name: name.clone(), + amount, + currency: "USD".into(), + frequency, + category_id, + account_id, + start_date, + }, + ) + .map(|_| format!("Added subscription {name}")), + Some(id) => commands::update_subscription( + &self.db, + UpdateSubscriptionRequest { + id: id.clone(), + name: Some(name.clone()), + amount: Some(amount), + currency: Some("USD".into()), + frequency: Some(frequency), + category_id: Some(category_id), + account_id: Some(account_id), + start_date: Some(start_date), + is_active: None, + }, + ) + .map(|_| format!("Saved subscription {name}")), + }; + match result { + Ok(message) => cx.emit(FormEvent::Saved(message)), + Err(e) => cx.emit(FormEvent::Failed(format!("Could not save subscription: {e}"))), + } + } +} + +impl Render for SubscriptionForm { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let editing = self.editing.is_some(); + let body = div() + .flex() + .flex_col() + .gap(rems(0.75)) + .child(field("Name", input("sub-name", &self.name, window, cx), cx)) + .child( + div() + .flex() + .gap(rems(0.5)) + .child(field("Amount", input("sub-amount", &self.amount, window, cx), cx)) + .child(field("Period", select("sub-frequency", &self.frequency, window, cx), cx)), + ) + .child(field("Category", select("sub-category", &self.category, window, cx), cx)) + .child(field("Account", select("sub-account", &self.account, window, cx), cx)) + .child(field("Start Date", DatePicker::new(&self.start_date).placeholder("Pick a date").cleanable(true), cx)) + .child( + div() + .flex() + .justify_end() + .gap(rems(0.5)) + .child(button("sub-cancel", "Cancel", ButtonVariant::Outline, cx).on_click(cx.listener(|_, _, _, cx| cx.emit(FormEvent::Cancel)))) + .child(button("sub-submit", if editing { "Save" } else { "Add" }, ButtonVariant::Primary, cx).on_click(cx.listener(|this, _, _, cx| this.submit(cx)))), + ); + modal( + if editing { "Edit Subscription" } else { "Add Subscription" }, + body, + cx.listener(|_, _, _, cx| cx.emit(FormEvent::Cancel)), + cx, + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ui::test_support::{init, seeded_db}; + use gpui_kit::component::calendar::Date; + use gpui_kit::{TestAppContext, VisualTestContext}; + + fn setup(cx: &mut TestAppContext) -> (Entity, Arc, &mut VisualTestContext) { + init(cx); + let db = seeded_db(); + let handle = db.clone(); + let (screen, vcx) = cx.add_window_view(move |window, cx| SubscriptionsScreen::new(db, window, cx)); + (screen, handle, vcx) + } + + fn make(db: &DbState, name: &str, amount: i64, frequency: &str) -> Subscription { + commands::create_subscription( + db, + CreateSubscriptionRequest { + name: name.into(), + amount, + currency: "USD".into(), + frequency: frequency.into(), + category_id: None, + account_id: None, + start_date: Some("2026-01-15".into()), + }, + ) + .unwrap() + } + + fn reload(screen: &Entity, cx: &mut VisualTestContext) { + cx.update(|_, cx| screen.update(cx, |s, cx| s.reload(cx))); + } + + fn type_into(input: &Entity, text: &str, cx: &mut VisualTestContext) { + cx.update(|window, cx| input.update(cx, |i, cx| i.set_value(text, window, cx))); + } + + fn form_of(screen: &Entity, cx: &mut VisualTestContext) -> Entity { + cx.update(|_, cx| match screen.read(cx).dialog.as_ref() { + Some(Dialog::Form(f)) => f.clone(), + _ => panic!("form should be open"), + }) + } + + #[gpui_kit::test] + fn add_subscription_with_start_date(cx: &mut TestAppContext) { + let (screen, _db, cx) = setup(cx); + cx.update(|window, cx| screen.update(cx, |s, cx| s.open_add(window, cx))); + let form = form_of(&screen, cx); + let (name, amount, date) = cx.update(|_, cx| { + let f = form.read(cx); + (f.name.clone(), f.amount.clone(), f.start_date.clone()) + }); + type_into(&name, "Netflix", cx); + type_into(&amount, "15.99", cx); + cx.update(|window, cx| { + date.update(cx, |d, cx| d.set_date(Date::Single(chrono::NaiveDate::from_ymd_opt(2026, 3, 9)), window, cx)) + }); + cx.update(|_, cx| form.update(cx, |f, cx| f.submit(cx))); + cx.update(|_, cx| { + let s = screen.read(cx); + assert!(!s.has_dialog()); + let added = s.subscriptions().iter().find(|x| x.name == "Netflix").expect("saved"); + assert_eq!(added.amount, 1599); + assert_eq!(added.frequency, "monthly"); + assert_eq!(added.start_date.as_deref(), Some("2026-03-09")); + }); + } + + #[gpui_kit::test] + fn invalid_or_blank_input_keeps_form_open(cx: &mut TestAppContext) { + let (screen, _db, cx) = setup(cx); + cx.update(|window, cx| screen.update(cx, |s, cx| s.open_add(window, cx))); + let form = form_of(&screen, cx); + cx.update(|_, cx| form.update(cx, |f, cx| f.submit(cx))); + cx.update(|_, cx| assert!(screen.read(cx).has_dialog(), "blank form is ignored")); + let (name, amount) = cx.update(|_, cx| (form.read(cx).name.clone(), form.read(cx).amount.clone())); + type_into(&name, "Bad", cx); + type_into(&amount, "abc", cx); + cx.update(|_, cx| form.update(cx, |f, cx| f.submit(cx))); + cx.update(|_, cx| { + assert!(screen.read(cx).has_dialog()); + assert!(screen.read(cx).subscriptions().is_empty()); + }); + } + + #[gpui_kit::test] + fn edit_updates_and_can_clear_the_category(cx: &mut TestAppContext) { + let (screen, db, cx) = setup(cx); + let cat = commands::get_categories(&db).unwrap().into_iter().find(|c| c.category_type == "expense").unwrap(); + let created = commands::create_subscription(&db, CreateSubscriptionRequest { + name: "Gym".into(), amount: 3000, currency: "USD".into(), frequency: "monthly".into(), + category_id: Some(cat.id.clone()), account_id: None, start_date: None, + }).unwrap(); + reload(&screen, cx); + cx.update(|window, cx| screen.update(cx, |s, cx| s.open_form(Some(created.clone()), window, cx))); + let form = form_of(&screen, cx); + let (amount, category) = cx.update(|_, cx| { + let f = form.read(cx); + assert_eq!(f.amount.read(cx).value().as_ref(), "30.00"); + assert_eq!(choice_value(&f.category, cx).as_deref(), Some(cat.id.as_str())); + (f.amount.clone(), f.category.clone()) + }); + type_into(&amount, "35", cx); + cx.update(|_, cx| category.update(cx, |c, cx| c.set_selected(Some(0), cx))); + cx.update(|_, cx| form.update(cx, |f, cx| f.submit(cx))); + cx.update(|_, cx| { + let s = screen.read(cx); + let saved = s.subscriptions().iter().find(|x| x.id == created.id).unwrap(); + assert_eq!(saved.amount, 3500); + assert_eq!(saved.category_id, None, "choosing None clears the category"); + assert_eq!(s.subscriptions().len(), 1); + }); + } + + #[gpui_kit::test] + fn recording_a_payment_creates_an_expense(cx: &mut TestAppContext) { + let (screen, db, cx) = setup(cx); + let sub = make(&db, "Spotify", 999, "monthly"); + reload(&screen, cx); + let before = commands::get_transactions(&db).unwrap().len(); + cx.update(|_, cx| screen.update(cx, |s, cx| s.pay(&sub, cx))); + let after = commands::get_transactions(&db).unwrap(); + assert_eq!(after.len(), before + 1); + let paid = after.iter().find(|t| t.description == "Spotify").expect("payment transaction"); + assert_eq!(paid.amount, 999); + assert_eq!(paid.transaction_type, "expense"); + } + + #[gpui_kit::test] + fn bulk_delete_goes_through_confirmation(cx: &mut TestAppContext) { + let (screen, db, cx) = setup(cx); + for n in ["A", "B", "C"] { + make(&db, n, 100, "monthly"); + } + reload(&screen, cx); + cx.update(|_, cx| screen.update(cx, |s, cx| s.toggle_page(cx))); + cx.update(|_, cx| assert_eq!(screen.read(cx).selected.len(), 3)); + let ids: Vec = cx.update(|_, cx| screen.read(cx).selected.iter().cloned().collect()); + cx.update(|_, cx| screen.update(cx, |s, cx| s.ask_delete(ids, cx))); + + // Cancelling keeps everything. + let dialog = cx.update(|_, cx| match screen.read(cx).dialog.as_ref() { + Some(Dialog::Confirm(d, _)) => d.clone(), + _ => panic!("confirm dialog should be open"), + }); + cx.update(|_, cx| dialog.update(cx, |_, cx| cx.emit(ConfirmEvent::Cancel))); + cx.update(|_, cx| { + assert!(!screen.read(cx).has_dialog()); + assert_eq!(screen.read(cx).subscriptions().len(), 3); + }); + + // Confirming deletes them and clears the selection. + let ids: Vec = cx.update(|_, cx| screen.read(cx).selected.iter().cloned().collect()); + cx.update(|_, cx| screen.update(cx, |s, cx| s.ask_delete(ids, cx))); + let dialog = cx.update(|_, cx| match screen.read(cx).dialog.as_ref() { + Some(Dialog::Confirm(d, _)) => d.clone(), + _ => panic!("confirm dialog should be open"), + }); + cx.update(|_, cx| dialog.update(cx, |_, cx| cx.emit(ConfirmEvent::Confirm))); + cx.update(|_, cx| { + let s = screen.read(cx); + assert!(s.subscriptions().is_empty()); + assert!(s.selected.is_empty()); + assert!(!s.has_dialog()); + }); + } + + #[gpui_kit::test] + fn frequency_filter_narrows_the_list_and_resets_the_page(cx: &mut TestAppContext) { + let (screen, db, cx) = setup(cx); + make(&db, "M1", 1000, "monthly"); + make(&db, "M2", 500, "monthly"); + make(&db, "Y1", 12000, "yearly"); + reload(&screen, cx); + cx.update(|_, cx| { + let s = screen.read(cx); + assert_eq!(s.filtered().len(), 3); + assert_eq!(total_amount(&s.filtered()), 13500); + }); + cx.update(|_, cx| screen.update(cx, |s, cx| { s.page = 2; cx.notify(); })); + let filter = cx.update(|_, cx| screen.read(cx).filter.clone()); + // Items: all, weekly, bi_weekly, monthly, ... so index 3 is monthly. + cx.update(|_, cx| filter.update(cx, |f, cx| f.set_selected(Some(3), cx))); + cx.update(|_, cx| { + let s = screen.read(cx); + assert_eq!(s.applied_filter, "monthly"); + assert_eq!(s.page, 1, "changing the filter returns to page one"); + assert_eq!(s.filtered().len(), 2); + assert_eq!(total_amount(&s.filtered()), 1500); + }); + } + + #[gpui_kit::test] + fn open_add_shows_the_form(cx: &mut TestAppContext) { + let (screen, _db, cx) = setup(cx); + cx.update(|window, cx| screen.update(cx, |s, cx| s.open_add(window, cx))); + cx.update(|_, cx| assert!(screen.read(cx).has_dialog())); + } +} diff --git a/src/ui/widgets.rs b/src/ui/widgets.rs index e577b62..31004d3 100644 --- a/src/ui/widgets.rs +++ b/src/ui/widgets.rs @@ -55,7 +55,7 @@ pub fn modal( body: impl IntoElement, on_dismiss: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static, cx: &App, -) -> impl IntoElement { +) -> gpui_kit::Stateful
{ let t = cx.omarchy(); div() .id("modal-backdrop") @@ -160,3 +160,137 @@ pub fn account_type_options() -> Vec<(String, String)> { .map(|(v, l)| (v.to_string(), l.to_string())) .collect() } + +// ── lists ─────────────────────────────────────────────────────────── + +use chelete_lib::paging::{Pager, PER_PAGE_OPTIONS}; +use gpui_kit::base::CheckboxState; +use gpui_kit::{ClickEvent, EventEmitter}; +use gpui_kit::base::Checkbox; +use gpui_omarchy::{button, checkbox, select, ButtonVariant}; + +pub fn check( + id: impl Into, + label: &str, + state: CheckboxState, + on_toggle: impl Fn(&mut Window, &mut App) + 'static, + cx: &App, +) -> Checkbox { + checkbox(id, label.to_string(), state, cx) + .on_change(move |_, _: &ClickEvent, window, cx| on_toggle(window, cx)) +} + +pub fn per_page_choice(current: usize, window: &mut Window, cx: &mut Context) -> Entity { + choice( + PER_PAGE_OPTIONS.iter().map(|n| (n.to_string(), n.to_string())).collect(), + Some(¤t.to_string()), + window, + cx, + ) +} + +/// "Show [25] per page Previous 1–25 of 61 Next" +pub fn pagination_bar( + pager: Pager, + per_page: &Entity, + on_prev: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + on_next: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + window: &mut Window, + cx: &mut App, +) -> Div { + let t = cx.omarchy().clone(); + div() + .flex() + .items_center() + .justify_between() + .child( + div() + .flex() + .items_center() + .gap(rems(0.5)) + .child(div().text_color(t.secondary).child("Show")) + .child(div().w(rems(5.)).child(select("per-page", per_page, window, cx))) + .child(div().text_color(t.secondary).child("per page")), + ) + .child( + div() + .flex() + .items_center() + .gap(rems(0.75)) + .child( + button("page-prev", "Previous", ButtonVariant::Outline, cx) + .disabled(!pager.has_prev()) + .on_click(on_prev), + ) + .child(div().text_color(t.secondary).child(pager.label())) + .child( + button("page-next", "Next", ButtonVariant::Outline, cx) + .disabled(!pager.has_next()) + .on_click(on_next), + ), + ) +} + +pub fn selection_state(selected_on_page: usize, page_len: usize) -> CheckboxState { + if page_len > 0 && selected_on_page == page_len { + CheckboxState::Checked + } else if selected_on_page > 0 { + CheckboxState::Indeterminate + } else { + CheckboxState::Unchecked + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ConfirmEvent { + Confirm, + Cancel, +} + +/// A yes/no dialog for destructive actions. +pub struct ConfirmDialog { + title: SharedString, + message: SharedString, + confirm_label: SharedString, +} + +impl EventEmitter for ConfirmDialog {} + +impl ConfirmDialog { + pub fn new(title: impl Into, message: impl Into, confirm_label: impl Into) -> Self { + Self { title: title.into(), message: message.into(), confirm_label: confirm_label.into() } + } +} + +impl Render for ConfirmDialog { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let body = div() + .flex() + .flex_col() + .gap(rems(1.)) + .child(div().text_color(cx.omarchy().secondary).child(self.message.clone())) + .child( + div() + .flex() + .justify_end() + .gap(rems(0.5)) + .child( + button("confirm-cancel", "Cancel", ButtonVariant::Outline, cx) + .on_click(cx.listener(|_, _, _, cx| cx.emit(ConfirmEvent::Cancel))), + ) + .child( + button("confirm-ok", self.confirm_label.clone(), ButtonVariant::Danger, cx) + .on_click(cx.listener(|_, _, _, cx| cx.emit(ConfirmEvent::Confirm))), + ), + ); + modal(&self.title, body, cx.listener(|_, _, _, cx| cx.emit(ConfirmEvent::Cancel)), cx) + } +} + +pub fn plural(count: usize, singular: &str) -> String { + if count == 1 { + format!("{count} {singular}") + } else { + format!("{count} {singular}s") + } +} From fbf5836385d2e7cc0f7375290068d90c14a1c9ac Mon Sep 17 00:00:00 2001 From: Victor Phos Date: Fri, 2 Oct 2026 14:07:02 +0200 Subject: [PATCH 21/36] Port the Projections screen Inputs for starting balance, income, expense (manual or the total of active subscriptions) and period, with stat cards and a balance-over-time line chart that turns red and marks zero when the balance goes negative. Input validation is a pure function with its own tests. --- src/ui/mod.rs | 1 + src/ui/projections.rs | 409 ++++++++++++++++++++++++++++++++++++++++++ src/ui/shell.rs | 6 + 3 files changed, 416 insertions(+) create mode 100644 src/ui/projections.rs diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 7857629..1cb4fef 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -6,6 +6,7 @@ mod icons; mod overview; mod palette; mod placeholder; +mod projections; mod shell; mod settings; mod sidebar; diff --git a/src/ui/projections.rs b/src/ui/projections.rs new file mode 100644 index 0000000..db54b1f --- /dev/null +++ b/src/ui/projections.rs @@ -0,0 +1,409 @@ +use super::widgets::*; +use chelete_lib::commands::{self, Subscription}; +use chelete_lib::database::DbState; +use chelete_lib::format::{format_balance, format_money, parse_dollars_lenient}; +use chelete_lib::projection::{ + compute_projection, format_axis, normalize_subscription_to_period, PeriodType, ProjectionInput, + ProjectionResult, +}; +use gpui_kit::base::input::InputState; +use gpui_kit::{div, prelude::*, rems, Context, Div, Entity, IntoElement, Render, SharedString, Subscription as GpuiSubscription, Window}; +use gpui_omarchy::chart::LineChart; +use gpui_omarchy::{empty_state, input, panel, select, ActiveTheme as _, ChoiceState}; +use std::sync::Arc; + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum ExpenseSource { + Manual, + Subscriptions, +} + +/// Everything the form collects, as typed. +pub struct FormValues<'a> { + pub starting_balance: &'a str, + pub income: &'a str, + pub manual_expense: &'a str, + pub source: ExpenseSource, + pub subscriptions_expense: i64, + pub period_type: PeriodType, + pub periods: &'a str, +} + +/// `None` until every field is valid, mirroring the web form. +pub fn build_input(v: &FormValues) -> Option { + let starting_balance = parse_dollars_lenient(v.starting_balance)?; + let income = parse_dollars_lenient(v.income)?; + let expense = match v.source { + ExpenseSource::Subscriptions => v.subscriptions_expense, + ExpenseSource::Manual => parse_dollars_lenient(v.manual_expense)?, + }; + let periods: u32 = v.periods.trim().parse().ok()?; + if periods < 1 || periods > v.period_type.limit() { + return None; + } + Some(ProjectionInput { starting_balance, income, expense, period_type: v.period_type, periods }) +} + +pub struct ProjectionsScreen { + db: Arc, + starting: Entity, + income: Entity, + expense: Entity, + periods: Entity, + source: Entity, + period_type: Entity, + subscriptions: Vec, + _subs: Vec, +} + +impl ProjectionsScreen { + pub fn new(db: Arc, window: &mut Window, cx: &mut Context) -> Self { + let starting = text_input_with("0.00", "0", window, cx); + let income = text_input("e.g. 1200.00", window, cx); + let expense = text_input("e.g. 100.00", window, cx); + let periods = text_input_with("e.g. 10", "10", window, cx); + let source = choice( + vec![("manual".into(), "Manual".into()), ("subscriptions".into(), "Subscriptions".into())], + Some("manual"), + window, + cx, + ); + let period_type = choice( + PeriodType::ALL.iter().map(|p| (p.value().to_string(), capitalize(p.plural()))).collect(), + Some("months"), + window, + cx, + ); + let mut subs = Vec::new(); + for input in [&starting, &income, &expense, &periods] { + subs.push(cx.observe(input, |_, _, cx| cx.notify())); + } + for choice in [&source, &period_type] { + subs.push(cx.observe(choice, |_, _, cx| cx.notify())); + } + let mut screen = Self { db, starting, income, expense, periods, source, period_type, subscriptions: Vec::new(), _subs: subs }; + screen.load(); + screen + } + + fn load(&mut self) { + self.subscriptions = commands::get_subscriptions(&self.db) + .map(|all| all.into_iter().filter(|s| s.is_active).collect()) + .unwrap_or_default(); + } + + pub fn reload(&mut self, cx: &mut Context) { + self.load(); + cx.notify(); + } + + fn period(&self, cx: &gpui_kit::App) -> PeriodType { + choice_value(&self.period_type, cx) + .and_then(|v| PeriodType::from_value(&v)) + .unwrap_or(PeriodType::Months) + } + + fn expense_source(&self, cx: &gpui_kit::App) -> ExpenseSource { + match choice_value(&self.source, cx).as_deref() { + Some("subscriptions") => ExpenseSource::Subscriptions, + _ => ExpenseSource::Manual, + } + } + + fn subscriptions_expense(&self, period: PeriodType) -> i64 { + self.subscriptions + .iter() + .map(|s| normalize_subscription_to_period(s.amount, &s.frequency, period)) + .sum() + } + + /// The projection for the current form values, if they are valid. + pub fn result(&self, cx: &gpui_kit::App) -> Option<(ProjectionResult, u32)> { + let period_type = self.period(cx); + let (starting, income, expense, periods) = ( + text_of(&self.starting, cx), + text_of(&self.income, cx), + text_of(&self.expense, cx), + text_of(&self.periods, cx), + ); + let input = build_input(&FormValues { + starting_balance: &starting, + income: &income, + manual_expense: &expense, + source: self.expense_source(cx), + subscriptions_expense: self.subscriptions_expense(period_type), + period_type, + periods: &periods, + })?; + Some((compute_projection(input, chrono::Local::now().date_naive()), input.periods)) + } +} + +fn capitalize(word: &str) -> String { + let mut chars = word.chars(); + chars.next().map(|c| c.to_uppercase().collect::() + chars.as_str()).unwrap_or_default() +} + +impl Render for ProjectionsScreen { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let t = cx.omarchy().clone(); + let period = self.period(cx); + let source = self.expense_source(cx); + let sub_total = self.subscriptions_expense(period); + + let expense_block = match source { + ExpenseSource::Manual => field("Expense per Period", input("proj-expense", &self.expense, window, cx), cx).into_any_element(), + ExpenseSource::Subscriptions => { + let n = self.subscriptions.len(); + let mut list = div() + .flex() + .flex_col() + .gap(rems(0.25)) + .p(rems(0.75)) + .border_1() + .border_color(t.border) + .child(div().text_color(t.secondary).child(format!( + "{n} active subscription{}", + if n == 1 { "" } else { "s" } + ))); + for s in &self.subscriptions { + list = list.child( + div() + .flex() + .justify_between() + .child(s.name.clone()) + .child(format!( + "{}/{}", + format_money(normalize_subscription_to_period(s.amount, &s.frequency, period)), + period.unit() + )), + ); + } + list = list.child(div().font_weight(gpui_kit::FontWeight::BOLD).child(format!( + "Total: {}/{}", + format_money(sub_total), + period.unit() + ))); + if n == 0 { + list = list.child(div().text_color(t.secondary).child("No active subscriptions found")); + } + list.into_any_element() + } + }; + + let form = panel("Inputs", cx) + .w(rems(20.)) + .flex_shrink_0() + .child(field("Starting Balance", input("proj-start", &self.starting, window, cx), cx)) + .child(field("Income per Period", input("proj-income", &self.income, window, cx), cx)) + .child(field("Expense Source", select("proj-source", &self.source, window, cx), cx)) + .child(expense_block) + .child(field("Period Type", select("proj-period", &self.period_type, window, cx), cx)) + .child( + field(&format!("Number of {}", period.plural()), input("proj-periods", &self.periods, window, cx), cx) + .child(div().text_color(t.secondary).child(format!("1 – {} {}", period.limit(), period.plural()))), + ); + + let content = match self.result(cx) { + None => empty_state( + "Nothing to project yet", + "Enter your income, expense and period to see a projection.", + cx, + ) + .into_any_element(), + Some((result, periods)) => { + let s = &result.summary; + let color = |v: i64| if v > 0 { t.success } else if v < 0 { t.danger } else { t.foreground }; + let stat = |title: &str, value: String, note: String, tint| -> Div { + panel(title.to_string(), cx) + .flex_1() + .child(div().text_size(rems(1.25)).text_color(tint).child(value)) + .child(div().text_color(t.secondary).child(note)) + }; + let stats = div() + .flex() + .gap(rems(1.)) + .child(stat( + "Projected Balance", + format_balance(s.final_balance), + format!("in {periods} {} · {}", period.plural(), s.projected_date.format("%b %e, %Y")), + color(s.net_per_period), + )) + .child(stat( + "Total Change", + format_money(s.total_increase), + "from starting balance".into(), + color(s.total_increase), + )) + .child(stat( + &format!("Net per {}", capitalize(period.unit())), + format_money(s.net_per_period), + match s.retention_rate { + Some(r) => format!("Keeping {r:.1}% of income"), + None => "No income entered".into(), + }, + color(s.net_per_period), + )); + + let declining = s.net_per_period < 0; + let mut chart = LineChart::new(result.points.clone()) + .id("projection-line") + .name("Balance") + .x(|p| SharedString::from(p.label.clone())) + .y(|p| p.balance as f64 / 100.0) + .stroke(if declining { t.danger } else { t.accent }) + .tick_margin(2) + .linear() + .dot() + .y_tick_format(|v| format_axis((v * 100.0).round() as i64)) + .tooltip_value(|_, v| format_balance((v * 100.0).round() as i64).into()); + if s.final_balance < 0 { + chart = chart.reference_line(0.0); + } + div() + .flex() + .flex_col() + .gap(rems(1.)) + .child(stats) + .child(panel("Balance Over Time", cx).child(div().w_full().h(rems(20.)).child(chart))) + .into_any_element() + } + }; + + div() + .id("projections") + .size_full() + .overflow_y_scroll() + .p(rems(1.5)) + .flex() + .flex_col() + .gap(rems(1.)) + .child(page_header("Projections", div().text_color(t.secondary).child("Project your balance growth over time"), cx)) + .child(div().flex().gap(rems(1.)).items_start().child(form).child(div().flex_1().min_w_0().child(content))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ui::test_support::{init, seeded_db}; + use chelete_lib::commands::CreateSubscriptionRequest; + use gpui_kit::{TestAppContext, VisualTestContext}; + + fn values<'a>(s: &'a str, i: &'a str, e: &'a str, n: &'a str) -> FormValues<'a> { + FormValues { + starting_balance: s, + income: i, + manual_expense: e, + source: ExpenseSource::Manual, + subscriptions_expense: 0, + period_type: PeriodType::Months, + periods: n, + } + } + + #[test] + fn valid_form_builds_an_input() { + let input = build_input(&values("0", "1200", "100", "10")).unwrap(); + assert_eq!((input.starting_balance, input.income, input.expense, input.periods), (0, 120000, 10000, 10)); + } + + #[test] + fn any_empty_or_bad_field_blocks_the_projection() { + assert!(build_input(&values("", "1200", "100", "10")).is_none()); + assert!(build_input(&values("0", "", "100", "10")).is_none()); + assert!(build_input(&values("0", "1200", "", "10")).is_none()); + assert!(build_input(&values("0", "1200", "100", "")).is_none()); + assert!(build_input(&values("0", "abc", "100", "10")).is_none()); + } + + #[test] + fn period_count_is_bounded_by_the_period_type() { + assert!(build_input(&values("0", "1", "1", "0")).is_none()); + assert!(build_input(&values("0", "1", "1", "601")).is_none()); + assert!(build_input(&values("0", "1", "1", "600")).is_some()); + assert!(build_input(&values("0", "1", "1", "2.5")).is_none()); + } + + #[test] + fn subscription_source_ignores_the_manual_expense() { + let mut v = values("0", "1000", "", "3"); + v.source = ExpenseSource::Subscriptions; + v.subscriptions_expense = 4500; + let input = build_input(&v).unwrap(); + assert_eq!(input.expense, 4500, "manual field may be blank in this mode"); + } + + fn setup(cx: &mut TestAppContext) -> (Entity, Arc, &mut VisualTestContext) { + init(cx); + let db = seeded_db(); + let handle = db.clone(); + let (screen, vcx) = cx.add_window_view(move |window, cx| ProjectionsScreen::new(db, window, cx)); + (screen, handle, vcx) + } + + fn type_into(input: &Entity, text: &str, cx: &mut VisualTestContext) { + cx.update(|window, cx| input.update(cx, |i, cx| i.set_value(text, window, cx))); + } + + #[gpui_kit::test] + fn typing_income_and_expense_produces_the_canonical_projection(cx: &mut TestAppContext) { + let (screen, _db, cx) = setup(cx); + cx.update(|_, cx| assert!(screen.read(cx).result(cx).is_none(), "income and expense start empty")); + let (income, expense) = cx.update(|_, cx| (screen.read(cx).income.clone(), screen.read(cx).expense.clone())); + type_into(&income, "1200", cx); + type_into(&expense, "100", cx); + cx.update(|_, cx| { + let (result, periods) = screen.read(cx).result(cx).expect("valid inputs"); + assert_eq!(periods, 10); + assert_eq!(result.summary.final_balance, 1_100_000); + assert_eq!(result.points.len(), 11); + }); + } + + #[gpui_kit::test] + fn subscription_source_totals_active_subscriptions_only(cx: &mut TestAppContext) { + let (screen, db, cx) = setup(cx); + let make = |name: &str, amount: i64| { + commands::create_subscription(&db, CreateSubscriptionRequest { + name: name.into(), amount, currency: "USD".into(), frequency: "monthly".into(), + category_id: None, account_id: None, start_date: None, + }).unwrap() + }; + make("Active", 1000); + let inactive = make("Paused", 5000); + commands::update_subscription(&db, commands::UpdateSubscriptionRequest { + id: inactive.id, name: None, amount: None, currency: None, frequency: None, + category_id: None, account_id: None, start_date: None, is_active: Some(false), + }).unwrap(); + cx.update(|_, cx| screen.update(cx, |s, cx| s.reload(cx))); + + let (income, source) = cx.update(|_, cx| (screen.read(cx).income.clone(), screen.read(cx).source.clone())); + type_into(&income, "100", cx); + cx.update(|_, cx| source.update(cx, |c, cx| c.set_selected(Some(1), cx))); + cx.update(|_, cx| { + let s = screen.read(cx); + assert_eq!(s.subscriptions.len(), 1, "paused subscription excluded"); + let (result, _) = s.result(cx).expect("valid without a manual expense"); + assert_eq!(result.summary.net_per_period, 10000 - 1000); + }); + } + + #[gpui_kit::test] + fn switching_period_type_rescales_subscription_cost(cx: &mut TestAppContext) { + let (screen, db, cx) = setup(cx); + commands::create_subscription(&db, CreateSubscriptionRequest { + name: "Gym".into(), amount: 12000, currency: "USD".into(), frequency: "monthly".into(), + category_id: None, account_id: None, start_date: None, + }).unwrap(); + cx.update(|_, cx| screen.update(cx, |s, cx| s.reload(cx))); + let period_type = cx.update(|_, cx| screen.read(cx).period_type.clone()); + cx.update(|_, cx| { + let s = screen.read(cx); + assert_eq!(s.subscriptions_expense(PeriodType::Months), 12000); + assert_eq!(s.subscriptions_expense(PeriodType::Weeks), 2769); + }); + cx.update(|_, cx| period_type.update(cx, |c, cx| c.set_selected(Some(0), cx))); + cx.update(|_, cx| assert_eq!(screen.read(cx).period(cx), PeriodType::Weeks)); + } +} diff --git a/src/ui/shell.rs b/src/ui/shell.rs index fb27b09..a602b4d 100644 --- a/src/ui/shell.rs +++ b/src/ui/shell.rs @@ -1,5 +1,6 @@ use super::accounts::AccountsScreen; use super::categories::CategoriesScreen; +use super::projections::ProjectionsScreen; use super::settings; use super::subscriptions::SubscriptionsScreen; use super::overview::{self, OverviewData}; @@ -26,6 +27,7 @@ pub struct Shell { accounts: Entity, categories: Entity, subscriptions: Entity, + projections: Entity, toast: Option<(u64, String, bool)>, toast_seq: u64, palette: Option>, @@ -49,11 +51,13 @@ impl Shell { cx.subscribe(&categories, Self::on_screen_event).detach(); let subscriptions = cx.new(|cx| SubscriptionsScreen::new(db.clone(), window, cx)); cx.subscribe(&subscriptions, Self::on_screen_event).detach(); + let projections = cx.new(|cx| ProjectionsScreen::new(db.clone(), window, cx)); Self { overview: OverviewData::load(&db), accounts, categories, subscriptions, + projections, toast: None, toast_seq: 0, db, @@ -82,6 +86,7 @@ impl Shell { View::Accounts => self.accounts.update(cx, |s, cx| s.reload(cx)), View::Categories => self.categories.update(cx, |s, cx| s.reload(cx)), View::Subscriptions => self.subscriptions.update(cx, |s, cx| s.reload(cx)), + View::Projections => self.projections.update(cx, |s, cx| s.reload(cx)), _ => {} } cx.notify(); @@ -185,6 +190,7 @@ impl Render for Shell { View::Accounts => self.accounts.clone().into_any_element(), View::Categories => self.categories.clone().into_any_element(), View::Subscriptions => self.subscriptions.clone().into_any_element(), + View::Projections => self.projections.clone().into_any_element(), View::Settings => settings::render(cx).into_any_element(), other => placeholder(other, cx).into_any_element(), }; From a9cb09ea3050744dcbb4920e788f896156e04bc2 Mon Sep 17 00:00:00 2001 From: Victor Phos Date: Fri, 2 Oct 2026 14:07:34 +0200 Subject: [PATCH 22/36] Add a shared date filter control and table cell helpers A preset dropdown that reveals gpui-kit's two-month range picker (with Last 7/30 days, This/Last month and This year shortcuts) when Custom Range is chosen. Table cell and dim-text helpers move into the widget layer so more screens can use them. --- src/ui/date_filter_control.rs | 77 +++++++++++++++++++++++++++++++++++ src/ui/mod.rs | 1 + src/ui/subscriptions.rs | 12 ------ src/ui/widgets.rs | 13 ++++++ 4 files changed, 91 insertions(+), 12 deletions(-) create mode 100644 src/ui/date_filter_control.rs diff --git a/src/ui/date_filter_control.rs b/src/ui/date_filter_control.rs new file mode 100644 index 0000000..70239bd --- /dev/null +++ b/src/ui/date_filter_control.rs @@ -0,0 +1,77 @@ +//! Preset dropdown plus a custom date range, shared by list screens. +use super::date_field; +use super::widgets::{choice, choice_value}; +use chelete_lib::date_filter::{date_range, DateFilter}; +use chrono::{Datelike, Duration, NaiveDate}; +use gpui_kit::component::date_picker::{DatePicker, DatePickerState, DateRangePreset}; +use gpui_kit::{div, prelude::*, rems, App, Context, Div, Entity, Subscription, Window}; +use gpui_omarchy::{select, ChoiceState}; + +pub struct DateFilterControl { + pub preset: Entity, + pub range: Entity, +} + +impl DateFilterControl { + pub fn new(window: &mut Window, cx: &mut Context) -> Self { + let items = DateFilter::ALL_FILTERS.iter().map(|f| (f.value().to_string(), f.label().to_string())).collect(); + Self { + preset: choice(items, Some(DateFilter::All.value()), window, cx), + range: date_field::range(window, cx), + } + } + + pub fn filter(&self, cx: &App) -> DateFilter { + choice_value(&self.preset, cx) + .and_then(|v| DateFilter::from_value(&v)) + .unwrap_or(DateFilter::All) + } + + /// Inclusive ISO `(start, end)` for the current selection. + pub fn bounds(&self, today: NaiveDate, cx: &App) -> (String, String) { + let (start, end) = date_field::picked_range(&self.range, cx); + date_range(self.filter(cx), start, end, today) + } + + /// Re-render `owner` whenever the selection changes. + pub fn observe(&self, cx: &mut Context, on_change: impl Fn(&mut V, &mut Context) + Clone + 'static) -> Vec { + let a = on_change.clone(); + vec![ + cx.observe(&self.preset, move |this, _, cx| a(this, cx)), + cx.observe(&self.range, move |this, _, cx| on_change(this, cx)), + ] + } + + pub fn render(&self, today: NaiveDate, window: &mut Window, cx: &mut App) -> Div { + let custom = self.filter(cx) == DateFilter::Custom; + let presets = range_presets(today); + div() + .flex() + .items_center() + .gap(rems(0.5)) + .child(div().w(rems(13.)).child(select("date-filter", &self.preset, window, cx))) + .when(custom, |row| { + row.child( + div().w(rems(22.)).child( + DatePicker::new(&self.range) + .presets(presets) + .number_of_months(2) + .placeholder("Start — End") + .cleanable(true), + ), + ) + }) + } +} + +fn range_presets(today: NaiveDate) -> Vec { + let first_of_month = today.with_day(1).unwrap(); + let last_month_end = first_of_month - Duration::days(1); + vec![ + DateRangePreset::range("Last 7 days", today - Duration::days(6), today), + DateRangePreset::range("Last 30 days", today - Duration::days(29), today), + DateRangePreset::range("This month", first_of_month, today), + DateRangePreset::range("Last month", last_month_end.with_day(1).unwrap(), last_month_end), + DateRangePreset::range("This year", NaiveDate::from_ymd_opt(today.year(), 1, 1).unwrap(), today), + ] +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 1cb4fef..b5bb935 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -2,6 +2,7 @@ mod accounts; mod categories; mod date_field; +mod date_filter_control; mod icons; mod overview; mod palette; diff --git a/src/ui/subscriptions.rs b/src/ui/subscriptions.rs index 95726f6..7f4a3b5 100644 --- a/src/ui/subscriptions.rs +++ b/src/ui/subscriptions.rs @@ -250,18 +250,6 @@ impl SubscriptionsScreen { } } -fn dim(text: impl Into, cx: &gpui_kit::App) -> Div { - div().text_color(cx.omarchy().secondary).child(text.into()) -} - -fn cell(width: Option, child: impl IntoElement) -> Div { - let base = div().min_w_0().px(rems(0.5)); - match width { - Some(w) => base.w(rems(w)).flex_shrink_0().child(child), - None => base.flex_1().child(child), - } -} - impl Render for SubscriptionsScreen { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let t = cx.omarchy().clone(); diff --git a/src/ui/widgets.rs b/src/ui/widgets.rs index 31004d3..aa65645 100644 --- a/src/ui/widgets.rs +++ b/src/ui/widgets.rs @@ -294,3 +294,16 @@ pub fn plural(count: usize, singular: &str) -> String { format!("{count} {singular}s") } } + +pub fn dim(text: impl Into, cx: &App) -> Div { + div().text_color(cx.omarchy().secondary).child(text.into()) +} + +/// A table cell: fixed width in rems, or flexible when `None`. +pub fn cell(width: Option, child: impl IntoElement) -> Div { + let base = div().min_w_0().px(rems(0.5)); + match width { + Some(w) => base.w(rems(w)).flex_shrink_0().child(child), + None => base.flex_1().child(child), + } +} From d1223826a756fb61ebb0ed5642093597550dcdd4 Mon Sep 17 00:00:00 2001 From: Victor Phos Date: Fri, 2 Oct 2026 14:08:33 +0200 Subject: [PATCH 23/36] Port the Transactions screen Date filtering, a paginated table with bulk selection, add/edit/view dialogs (the category list follows the Expense/Income toggle), deletion with confirmation that reverses balances, and CSV/JSON export through the native save dialog. --- src/ui/mod.rs | 1 + src/ui/shell.rs | 14 +- src/ui/transactions.rs | 938 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 952 insertions(+), 1 deletion(-) create mode 100644 src/ui/transactions.rs diff --git a/src/ui/mod.rs b/src/ui/mod.rs index b5bb935..5df7c7b 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -13,6 +13,7 @@ mod settings; mod sidebar; mod subscriptions; mod theme_bridge; +mod transactions; mod widgets; #[cfg(test)] diff --git a/src/ui/shell.rs b/src/ui/shell.rs index a602b4d..62c0dd3 100644 --- a/src/ui/shell.rs +++ b/src/ui/shell.rs @@ -3,6 +3,7 @@ use super::categories::CategoriesScreen; use super::projections::ProjectionsScreen; use super::settings; use super::subscriptions::SubscriptionsScreen; +use super::transactions::TransactionsScreen; use super::overview::{self, OverviewData}; use super::widgets::ScreenEvent; use super::palette::{Command, Palette, PaletteEvent}; @@ -28,6 +29,7 @@ pub struct Shell { categories: Entity, subscriptions: Entity, projections: Entity, + transactions: Entity, toast: Option<(u64, String, bool)>, toast_seq: u64, palette: Option>, @@ -52,12 +54,15 @@ impl Shell { let subscriptions = cx.new(|cx| SubscriptionsScreen::new(db.clone(), window, cx)); cx.subscribe(&subscriptions, Self::on_screen_event).detach(); let projections = cx.new(|cx| ProjectionsScreen::new(db.clone(), window, cx)); + let transactions = cx.new(|cx| TransactionsScreen::new(db.clone(), window, cx)); + cx.subscribe(&transactions, Self::on_screen_event).detach(); Self { overview: OverviewData::load(&db), accounts, categories, subscriptions, projections, + transactions, toast: None, toast_seq: 0, db, @@ -87,6 +92,7 @@ impl Shell { View::Categories => self.categories.update(cx, |s, cx| s.reload(cx)), View::Subscriptions => self.subscriptions.update(cx, |s, cx| s.reload(cx)), View::Projections => self.projections.update(cx, |s, cx| s.reload(cx)), + View::Transactions => self.transactions.update(cx, |s, cx| s.reload(cx)), _ => {} } cx.notify(); @@ -155,7 +161,12 @@ impl Shell { fn run(&mut self, command: Command, window: &mut Window, cx: &mut Context) { let target = match command { Command::Go(view) => view, - Command::AddTransaction | Command::ImportTransactions => View::Transactions, + Command::AddTransaction => { + self.navigate(View::Transactions, window, cx); + self.transactions.update(cx, |s, cx| s.open_add(window, cx)); + return; + } + Command::ImportTransactions => View::Transactions, Command::AddAccount => { self.navigate(View::Accounts, window, cx); self.accounts.update(cx, |s, cx| s.open_add(window, cx)); @@ -191,6 +202,7 @@ impl Render for Shell { View::Categories => self.categories.clone().into_any_element(), View::Subscriptions => self.subscriptions.clone().into_any_element(), View::Projections => self.projections.clone().into_any_element(), + View::Transactions => self.transactions.clone().into_any_element(), View::Settings => settings::render(cx).into_any_element(), other => placeholder(other, cx).into_any_element(), }; diff --git a/src/ui/transactions.rs b/src/ui/transactions.rs new file mode 100644 index 0000000..1c4502a --- /dev/null +++ b/src/ui/transactions.rs @@ -0,0 +1,938 @@ +use super::accounts::FormEvent; +use super::date_field; +use super::date_filter_control::DateFilterControl; +use super::icons::category_icon; +use super::widgets::*; +use chelete_lib::commands::{ + self, Account, Category, CreateTransactionRequest, ExportData, Transaction, UpdateTransactionRequest, +}; +use chelete_lib::database::DbState; +use chelete_lib::date_filter::is_date_in_range; +use chelete_lib::export::ExportFormat; +use chelete_lib::format::{format_money, parse_cents}; +use chelete_lib::paging::{page_slice, Pager}; +use gpui_kit::base::input::InputState; +use gpui_kit::component::date_picker::DatePicker; +use gpui_kit::component::date_picker::DatePickerState; +use gpui_kit::{ + div, prelude::*, px, rems, Context, Entity, EventEmitter, IntoElement, Render, SharedString, + Subscription, Window, +}; +use gpui_omarchy::{ + button, icon, icon_button, input, select, ActiveTheme as _, ButtonVariant, ChoiceState, IconName, +}; +use std::collections::HashSet; +use std::path::PathBuf; +use std::sync::Arc; + +enum Dialog { + Form(Entity), + View(Transaction), + Confirm(Entity, Vec), + Export(Entity), +} + +pub struct TransactionsScreen { + db: Arc, + transactions: Vec, + accounts: Vec, + categories: Vec, + date_filter: DateFilterControl, + per_page: Entity, + applied_per_page: usize, + page: usize, + selected: HashSet, + dialog: Option, + _dialog_sub: Option, + _subs: Vec, +} + +impl EventEmitter for TransactionsScreen {} + +impl TransactionsScreen { + pub fn new(db: Arc, window: &mut Window, cx: &mut Context) -> Self { + let date_filter = DateFilterControl::new(window, cx); + let per_page = per_page_choice(25, window, cx); + let mut subs = date_filter.observe(cx, |this: &mut Self, cx| { + this.page = 1; + cx.notify(); + }); + subs.push(cx.observe(&per_page, |this, _, cx| { + let n = choice_value(&this.per_page, cx).and_then(|v| v.parse().ok()).unwrap_or(this.applied_per_page); + if n != this.applied_per_page { + this.applied_per_page = n; + this.page = 1; + } + cx.notify(); + })); + let mut screen = Self { + db, + transactions: Vec::new(), + accounts: Vec::new(), + categories: Vec::new(), + date_filter, + per_page, + applied_per_page: 25, + page: 1, + selected: HashSet::new(), + dialog: None, + _dialog_sub: None, + _subs: subs, + }; + screen.load(); + screen + } + + fn load(&mut self) { + self.transactions = commands::get_transactions(&self.db).unwrap_or_default(); + self.accounts = commands::get_accounts(&self.db).unwrap_or_default(); + self.categories = commands::get_categories(&self.db).unwrap_or_default(); + let live: HashSet<&String> = self.transactions.iter().map(|t| &t.id).collect(); + self.selected.retain(|id| live.contains(id)); + } + + pub fn reload(&mut self, cx: &mut Context) { + self.load(); + cx.notify(); + } + + pub fn transactions(&self) -> &[Transaction] { + &self.transactions + } + + fn filtered(&self, cx: &gpui_kit::App) -> Vec<&Transaction> { + let (start, end) = self.date_filter.bounds(chrono::Local::now().date_naive(), cx); + self.transactions + .iter() + .filter(|t| is_date_in_range(&t.transaction_date, &start, &end)) + .collect() + } + + fn pager(&self, cx: &gpui_kit::App) -> Pager { + Pager::new(self.page, self.applied_per_page, self.filtered(cx).len()) + } + + #[cfg(test)] + fn has_dialog(&self) -> bool { + self.dialog.is_some() + } + + pub fn open_add(&mut self, window: &mut Window, cx: &mut Context) { + self.open_form(None, window, cx); + } + + fn open_form(&mut self, editing: Option, window: &mut Window, cx: &mut Context) { + let form = cx.new(|cx| TransactionForm::new(self.db.clone(), &self.accounts, &self.categories, editing, window, cx)); + self._dialog_sub = Some(cx.subscribe(&form, |this, _, event: &FormEvent, cx| { + match event { + FormEvent::Cancel => {} + FormEvent::Saved(message) => { + this.load(); + cx.emit(ScreenEvent::ok(message.clone())); + } + FormEvent::Failed(message) => { + cx.emit(ScreenEvent::error(message.clone())); + return; + } + } + this.close_dialog(cx); + })); + self.dialog = Some(Dialog::Form(form)); + cx.notify(); + } + + fn close_dialog(&mut self, cx: &mut Context) { + self.dialog = None; + self._dialog_sub = None; + cx.notify(); + } + + fn ask_delete(&mut self, ids: Vec, cx: &mut Context) { + if ids.is_empty() { + return; + } + let noun = plural(ids.len(), "transaction"); + let plural_s = if ids.len() == 1 { "" } else { "s" }; + let dialog = cx.new(|_| { + ConfirmDialog::new( + format!("Delete {noun}?"), + format!("This will permanently remove the selected transaction{plural_s} and adjust account balances. This action cannot be undone."), + "Delete", + ) + }); + self._dialog_sub = Some(cx.subscribe(&dialog, |this, _, event: &ConfirmEvent, cx| { + if *event == ConfirmEvent::Confirm { + if let Some(Dialog::Confirm(_, ids)) = this.dialog.take() { + this.delete(ids, cx); + } + } + this.close_dialog(cx); + })); + self.dialog = Some(Dialog::Confirm(dialog, ids)); + cx.notify(); + } + + fn delete(&mut self, ids: Vec, cx: &mut Context) { + match commands::delete_transactions(&self.db, ids.clone()) { + Ok(()) => { + for id in &ids { + self.selected.remove(id); + } + self.load(); + self.page = self.pager(cx).page; + cx.emit(ScreenEvent::ok(format!("Deleted {}", plural(ids.len(), "transaction")))); + } + Err(e) => cx.emit(ScreenEvent::error(format!("Could not delete: {e}"))), + } + } + + pub fn open_export(&mut self, cx: &mut Context) { + let data = match commands::export_data(&self.db) { + Ok(d) => d, + Err(e) => return cx.emit(ScreenEvent::error(format!("Could not read data: {e}"))), + }; + let dialog = cx.new(|_| ExportDialog::new(data)); + self._dialog_sub = Some(cx.subscribe(&dialog, |this, _, event: &ExportEvent, cx| { + match event { + ExportEvent::Cancel => {} + ExportEvent::Written(path) => cx.emit(ScreenEvent::ok(format!("Exported to {}", path.display()))), + ExportEvent::Failed(message) => { + cx.emit(ScreenEvent::error(message.clone())); + return; + } + } + this.close_dialog(cx); + })); + self.dialog = Some(Dialog::Export(dialog)); + cx.notify(); + } + + fn toggle(&mut self, id: &str, cx: &mut Context) { + if !self.selected.remove(id) { + self.selected.insert(id.to_string()); + } + cx.notify(); + } + + fn toggle_page(&mut self, cx: &mut Context) { + let filtered = self.filtered(cx); + let ids: Vec = page_slice(&filtered, &self.pager(cx)).iter().map(|t| t.id.clone()).collect(); + let all = !ids.is_empty() && ids.iter().all(|id| self.selected.contains(id)); + for id in ids { + if all { + self.selected.remove(&id); + } else { + self.selected.insert(id); + } + } + cx.notify(); + } + + fn category(&self, id: &Option) -> Option<&Category> { + id.as_ref().and_then(|id| self.categories.iter().find(|c| &c.id == id)) + } + + fn account_name(&self, id: &str) -> Option<&str> { + self.accounts.iter().find(|a| a.id == id).map(|a| a.name.as_str()) + } +} + +fn signed(t: &Transaction) -> i64 { + if t.transaction_type == "expense" { + -t.amount + } else { + t.amount + } +} + +fn short_date(iso: &str) -> String { + date_field::parse_iso(iso) + .map(|d| d.format("%b %e").to_string()) + .unwrap_or_else(|| iso.to_string()) +} + +impl Render for TransactionsScreen { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let t = cx.omarchy().clone(); + let today = chrono::Local::now().date_naive(); + let filtered = self.filtered(cx); + let pager = self.pager(cx); + let rows: Vec = page_slice(&filtered, &pager).iter().map(|t| (*t).clone()).collect(); + let none_at_all = self.transactions.is_empty(); + let selected_on_page = rows.iter().filter(|r| self.selected.contains(&r.id)).count(); + let header_state = selection_state(selected_on_page, rows.len()); + drop(filtered); + + let actions = if self.selected.is_empty() { + div() + .flex() + .gap(rems(0.5)) + .child( + button("import-csv", "Import CSV", ButtonVariant::Outline, cx) + .child(icon(IconName::Upload).size(rems(0.875))) + .on_click(cx.listener(|_, _, _, cx| cx.emit(ScreenEvent::error("CSV import is not available yet")))), + ) + .child( + button("export-data", "Export Data", ButtonVariant::Outline, cx) + .child(icon(IconName::Download).size(rems(0.875))) + .on_click(cx.listener(|this, _, _, cx| this.open_export(cx))), + ) + .child( + button("add-transaction", "+ Add", ButtonVariant::Primary, cx) + .on_click(cx.listener(|this, _, window, cx| this.open_add(window, cx))), + ) + .into_any_element() + } else { + let ids: Vec = self.selected.iter().cloned().collect(); + button("delete-selected", format!("Delete {} selected", self.selected.len()), ButtonVariant::Danger, cx) + .on_click(cx.listener(move |this, _, _, cx| this.ask_delete(ids.clone(), cx))) + .into_any_element() + }; + + let body = if rows.is_empty() { + let text = if none_at_all { "No transactions yet. Add one to get started." } else { "No transactions match the selected filters." }; + div().py(rems(3.)).flex().justify_center().text_color(t.secondary).child(text).into_any_element() + } else { + let header = div() + .flex() + .items_center() + .py(rems(0.375)) + .bg(t.surface) + .text_color(t.bright) + .font_weight(gpui_kit::FontWeight::BOLD) + .child(cell(Some(2.5), check("select-page", "", header_state, { + let screen = cx.entity(); + move |_, cx| screen.update(cx, |s, cx| s.toggle_page(cx)) + }, cx))) + .child(cell(Some(6.), "Date")) + .child(cell(None, "Description")) + .child(cell(Some(6.), "Type")) + .child(cell(None, "Category")) + .child(cell(None, "Account")) + .child(cell(Some(8.), div().flex().justify_end().child("Amount"))) + .child(cell(Some(7.5), "")); + let mut list = div().flex().flex_col().border_1().border_color(t.border).child(header); + for tx in rows { + let category = self.category(&tx.category_id).cloned(); + let account = self.account_name(&tx.account_id).map(str::to_string); + let checked = self.selected.contains(&tx.id); + let row_id = tx.id.clone(); + let key = |prefix: &str| SharedString::from(format!("{prefix}-{}", tx.id)); + let (view, edit, del) = (tx.clone(), tx.clone(), tx.id.clone()); + let tint = if tx.transaction_type == "income" { t.success } else { t.danger }; + list = list.child( + div() + .flex() + .items_center() + .py(rems(0.25)) + .border_t_1() + .border_color(t.border) + .hover(|st| st.bg(t.surface)) + .child(cell(Some(2.5), check(key("sel"), "", if checked { gpui_kit::base::CheckboxState::Checked } else { gpui_kit::base::CheckboxState::Unchecked }, { + let screen = cx.entity(); + move |_, cx| screen.update(cx, |s, cx| s.toggle(&row_id, cx)) + }, cx))) + .child(cell(Some(6.), short_date(&tx.transaction_date))) + .child(cell(None, tx.description.clone())) + .child(cell(Some(6.), div().text_color(tint).child(tx.transaction_type.clone()))) + .child(cell(None, match &category { + Some(c) => div() + .flex() + .items_center() + .gap(rems(0.25)) + .children(c.icon.as_deref().map(|n| category_icon(n, px(12.), t.secondary))) + .child(c.name.clone()) + .into_any_element(), + None => dim("—", cx).into_any_element(), + })) + .child(cell(None, account.map(|a| div().child(a)).unwrap_or_else(|| dim("—", cx)))) + .child(cell(Some(8.), div().flex().justify_end().text_color(tint).child(format_money(signed(&tx))))) + .child(cell( + Some(7.5), + div() + .flex() + .justify_end() + .gap(rems(0.125)) + .child(icon_button(key("view"), IconName::Eye, "View transaction", ButtonVariant::Secondary, cx) + .on_click(cx.listener(move |this, _, _, cx| { this.dialog = Some(Dialog::View(view.clone())); cx.notify(); }))) + .child(icon_button(key("edit"), IconName::Pencil, "Edit transaction", ButtonVariant::Secondary, cx) + .on_click(cx.listener(move |this, _, window, cx| this.open_form(Some(edit.clone()), window, cx)))) + .child(icon_button(key("del"), IconName::Trash, "Delete transaction", ButtonVariant::Danger, cx) + .on_click(cx.listener(move |this, _, _, cx| this.ask_delete(vec![del.clone()], cx)))), + )), + ); + } + let screen = cx.entity(); + let (prev, next) = (screen.clone(), screen); + div() + .flex() + .flex_col() + .gap(rems(0.75)) + .child(list) + .child(pagination_bar( + pager, + &self.per_page, + move |_, _, cx| prev.update(cx, |s, cx| { s.page = s.page.saturating_sub(1).max(1); cx.notify(); }), + move |_, _, cx| next.update(cx, |s, cx| { s.page += 1; cx.notify(); }), + window, + cx, + )) + .into_any_element() + }; + + let dialog = self.dialog.as_ref().map(|d| match d { + Dialog::Form(f) => f.clone().into_any_element(), + Dialog::Confirm(c, _) => c.clone().into_any_element(), + Dialog::Export(e) => e.clone().into_any_element(), + Dialog::View(tx) => self.view_dialog(tx, cx).into_any_element(), + }); + let filter_row = self.date_filter.render(today, window, cx); + + div() + .id("transactions") + .relative() + .size_full() + .overflow_y_scroll() + .p(rems(1.5)) + .flex() + .flex_col() + .gap(rems(1.)) + .child(page_header("Transactions", actions, cx)) + .child(filter_row) + .child(body) + .children(dialog) + } +} + +impl TransactionsScreen { + fn view_dialog(&self, tx: &Transaction, cx: &mut Context) -> impl IntoElement { + let category = self.category(&tx.category_id).map(|c| c.name.clone()); + let account = self.account_name(&tx.account_id).map(str::to_string); + let row = |label: &str, value: String| div().flex().justify_between().child(dim(label.to_string(), cx)).child(value); + let mut body = div() + .flex() + .flex_col() + .gap(rems(0.5)) + .child(row("Date", short_date(&tx.transaction_date))) + .child(row("Description", tx.description.clone())); + if let Some(m) = &tx.merchant { + body = body.child(row("Merchant", m.clone())); + } + body = body + .child(row("Type", tx.transaction_type.clone())) + .child(row("Amount", format_money(signed(tx)))) + .child(row("Account", account.unwrap_or_else(|| "—".into()))) + .child(row("Category", category.unwrap_or_else(|| "—".into()))); + if let Some(n) = &tx.notes { + body = body.child(row("Notes", n.clone())); + } + let body = body.child( + div().flex().justify_end().pt(rems(0.5)).child( + button("view-close", "Close", ButtonVariant::Primary, cx) + .on_click(cx.listener(|this, _, _, cx| this.close_dialog(cx))), + ), + ); + modal("Transaction", body, cx.listener(|this, _, _, cx| this.close_dialog(cx)), cx) + } +} + +// ── add / edit ────────────────────────────────────────────────────── + +pub struct TransactionForm { + db: Arc, + categories: Vec, + editing: Option, + kind: String, + amount: Entity, + description: Entity, + account: Entity, + category: Entity, + date: Entity, +} + +impl EventEmitter for TransactionForm {} + +fn category_items(categories: &[Category], kind: &str) -> Vec<(String, String)> { + let mut items = vec![(String::new(), "None".to_string())]; + items.extend(categories.iter().filter(|c| c.category_type == kind).map(|c| (c.id.clone(), c.name.clone()))); + items +} + +impl TransactionForm { + fn new( + db: Arc, + accounts: &[Account], + categories: &[Category], + editing: Option, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let kind = editing.as_ref().map(|t| t.transaction_type.clone()).unwrap_or_else(|| "expense".into()); + let amount = editing.as_ref().map(|t| format!("{:.2}", t.amount as f64 / 100.0)).unwrap_or_default(); + let description = editing.as_ref().map(|t| t.description.clone()).unwrap_or_default(); + let account_id = editing.as_ref().map(|t| t.account_id.clone()).or_else(|| accounts.first().map(|a| a.id.clone())); + let category_id = editing.as_ref().and_then(|t| t.category_id.clone()).unwrap_or_default(); + let date = editing + .as_ref() + .and_then(|t| date_field::parse_iso(&t.transaction_date)) + .unwrap_or_else(|| chrono::Local::now().date_naive()); + let amount_input = text_input_with("0.00", &amount, window, cx); + amount_input.update(cx, |s, cx| s.focus(window, cx)); + Self { + db, + categories: categories.to_vec(), + kind: kind.clone(), + amount: amount_input, + description: text_input_with("e.g. Groceries", &description, window, cx), + account: choice(accounts.iter().map(|a| (a.id.clone(), a.name.clone())).collect(), account_id.as_deref(), window, cx), + category: choice(category_items(categories, &kind), Some(&category_id), window, cx), + date: date_field::single(Some(date), window, cx), + editing, + } + } + + fn set_kind(&mut self, kind: &str, window: &mut Window, cx: &mut Context) { + if self.kind == kind { + return; + } + self.kind = kind.to_string(); + // Categories belong to one type, so the list is rebuilt on a switch. + self.category = choice(category_items(&self.categories, kind), Some(""), window, cx); + cx.notify(); + } + + fn submit(&mut self, cx: &mut Context) { + let amount_text = text_of(&self.amount, cx); + let cents = match parse_cents(&amount_text) { + Some(c) if c > 0 => c, + _ => return, + }; + let Some(account_id) = choice_value(&self.account, cx) else { + return cx.emit(FormEvent::Failed("Add an account first".into())); + }; + let category_id = choice_value(&self.category, cx).filter(|c| !c.is_empty()); + let description = text_of(&self.description, cx); + let date = date_field::picked(&self.date, cx) + .unwrap_or_else(|| chrono::Local::now().date_naive()) + .format(date_field::FORMAT) + .to_string(); + + let result = match &self.editing { + None => commands::create_transaction( + &self.db, + CreateTransactionRequest { + account_id, + category_id, + transaction_type: self.kind.clone(), + amount: cents, + currency: "USD".into(), + description, + merchant: None, + notes: None, + transaction_date: date, + }, + ) + .map(|_| "Transaction added".to_string()), + Some(existing) => commands::update_transaction( + &self.db, + UpdateTransactionRequest { + id: existing.id.clone(), + account_id: Some(account_id), + category_id: Some(category_id), + transaction_type: Some(self.kind.clone()), + amount: Some(cents), + currency: Some("USD".into()), + description: Some(description), + merchant: None, + notes: None, + transaction_date: Some(date), + }, + ) + .map(|_| "Transaction saved".to_string()), + }; + match result { + Ok(message) => cx.emit(FormEvent::Saved(message)), + Err(e) => cx.emit(FormEvent::Failed(format!("Could not save transaction: {e}"))), + } + } +} + +impl Render for TransactionForm { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let editing = self.editing.is_some(); + let kind_button = |id: &'static str, label: &'static str, value: &'static str, cx: &mut Context| { + button(id, label, if self.kind == value { ButtonVariant::Primary } else { ButtonVariant::Outline }, cx) + .flex_1() + .justify_center() + .on_click(cx.listener(move |this, _, window, cx| this.set_kind(value, window, cx))) + }; + let toggle = div() + .flex() + .gap(rems(0.5)) + .child(kind_button("kind-expense", "Expense", "expense", cx)) + .child(kind_button("kind-income", "Income", "income", cx)); + let body = div() + .flex() + .flex_col() + .gap(rems(0.75)) + .child(field("Type", toggle, cx)) + .child(field("Amount", input("tx-amount", &self.amount, window, cx), cx)) + .child(field("Description", input("tx-description", &self.description, window, cx), cx)) + .child( + div() + .flex() + .gap(rems(0.5)) + .child(field("Account", select("tx-account", &self.account, window, cx), cx)) + .child(field("Category", select("tx-category", &self.category, window, cx), cx)), + ) + .child(field("Date", DatePicker::new(&self.date).placeholder("Pick a date"), cx)) + .child( + div() + .flex() + .justify_end() + .gap(rems(0.5)) + .child(button("tx-cancel", "Cancel", ButtonVariant::Outline, cx).on_click(cx.listener(|_, _, _, cx| cx.emit(FormEvent::Cancel)))) + .child(button("tx-submit", if editing { "Save" } else { "Add" }, ButtonVariant::Primary, cx).on_click(cx.listener(|this, _, _, cx| this.submit(cx)))), + ); + modal( + if editing { "Edit Transaction" } else { "Add Transaction" }, + body, + cx.listener(|_, _, _, cx| cx.emit(FormEvent::Cancel)), + cx, + ) + } +} + +// ── export ────────────────────────────────────────────────────────── + +#[derive(Clone, Debug)] +pub enum ExportEvent { + Cancel, + Written(PathBuf), + Failed(String), +} + +pub struct ExportDialog { + data: ExportData, + format: ExportFormat, +} + +impl EventEmitter for ExportDialog {} + +impl ExportDialog { + fn new(data: ExportData) -> Self { + Self { data, format: ExportFormat::Csv } + } + + /// Render the chosen format and write it to `path`. + pub fn write_to(&self, path: &std::path::Path) -> Result<(), String> { + let contents = self.format.contents(&self.data)?; + commands::write_export_file(path.to_string_lossy().to_string(), contents) + } + + fn choose_path_and_write(&mut self, cx: &mut Context) { + let directory = dirs::download_dir().or_else(dirs::home_dir).unwrap_or_else(|| PathBuf::from(".")); + let picked = cx.prompt_for_new_path(&directory, Some(self.format.default_file_name())); + cx.spawn(async move |this, cx| { + let outcome = match picked.await { + Ok(Ok(Some(path))) => this.update(cx, |this, cx| match this.write_to(&path) { + Ok(()) => cx.emit(ExportEvent::Written(path)), + Err(e) => cx.emit(ExportEvent::Failed(format!("Export failed: {e}"))), + }), + Ok(Ok(None)) | Err(_) => Ok(()), + Ok(Err(e)) => this.update(cx, |_, cx| cx.emit(ExportEvent::Failed(format!("Export failed: {e}")))), + }; + outcome.ok(); + }) + .detach(); + } +} + +impl Render for ExportDialog { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let format_button = |id: &'static str, label: &'static str, format: ExportFormat, cx: &mut Context| { + button(id, label, if self.format == format { ButtonVariant::Primary } else { ButtonVariant::Outline }, cx) + .flex_1() + .justify_center() + .on_click(cx.listener(move |this, _, _, cx| { this.format = format; cx.notify(); })) + }; + let summary = match self.format { + ExportFormat::Csv => format!("{} transactions, one row each.", self.data.transactions.len()), + ExportFormat::Json => "Accounts, categories, transactions and subscriptions.".to_string(), + }; + let body = div() + .flex() + .flex_col() + .gap(rems(0.75)) + .child(field( + "Format", + div().flex().gap(rems(0.5)).child(format_button("fmt-csv", "CSV", ExportFormat::Csv, cx)).child(format_button("fmt-json", "JSON", ExportFormat::Json, cx)), + cx, + )) + .child(dim(summary, cx)) + .child( + div() + .flex() + .justify_end() + .gap(rems(0.5)) + .child(button("export-cancel", "Cancel", ButtonVariant::Outline, cx).on_click(cx.listener(|_, _, _, cx| cx.emit(ExportEvent::Cancel)))) + .child(button("export-go", "Export", ButtonVariant::Primary, cx).on_click(cx.listener(|this, _, _, cx| this.choose_path_and_write(cx)))), + ); + modal("Export Data", body, cx.listener(|_, _, _, cx| cx.emit(ExportEvent::Cancel)), cx) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ui::test_support::{init, seeded_db}; + use chelete_lib::date_filter::DateFilter; + use gpui_kit::component::calendar::Date; + use gpui_kit::{TestAppContext, VisualTestContext}; + + fn setup(cx: &mut TestAppContext) -> (Entity, Arc, &mut VisualTestContext) { + init(cx); + let db = seeded_db(); + let handle = db.clone(); + let (screen, vcx) = cx.add_window_view(move |window, cx| TransactionsScreen::new(db, window, cx)); + (screen, handle, vcx) + } + + fn type_into(input: &Entity, text: &str, cx: &mut VisualTestContext) { + cx.update(|window, cx| input.update(cx, |i, cx| i.set_value(text, window, cx))); + } + + fn form_of(screen: &Entity, cx: &mut VisualTestContext) -> Entity { + cx.update(|_, cx| match screen.read(cx).dialog.as_ref() { + Some(Dialog::Form(f)) => f.clone(), + _ => panic!("form should be open"), + }) + } + + fn select_filter(screen: &Entity, filter: DateFilter, cx: &mut VisualTestContext) { + let index = DateFilter::ALL_FILTERS.iter().position(|f| *f == filter).unwrap(); + let preset = cx.update(|_, cx| screen.read(cx).date_filter.preset.clone()); + cx.update(|_, cx| preset.update(cx, |c, cx| c.set_selected(Some(index), cx))); + } + + #[gpui_kit::test] + fn lists_all_seeded_transactions(cx: &mut TestAppContext) { + let (screen, _db, cx) = setup(cx); + cx.update(|_, cx| { + let s = screen.read(cx); + assert_eq!(s.transactions().len(), 25); + assert_eq!(s.filtered(cx).len(), 25, "All Time shows everything"); + assert_eq!(s.pager(cx).label(), "1–25 of 25"); + }); + } + + #[gpui_kit::test] + fn add_expense_updates_the_account_balance(cx: &mut TestAppContext) { + let (screen, db, cx) = setup(cx); + let account = commands::get_accounts(&db).unwrap().remove(0); + cx.update(|window, cx| screen.update(cx, |s, cx| s.open_add(window, cx))); + let form = form_of(&screen, cx); + let (amount, description) = cx.update(|_, cx| (form.read(cx).amount.clone(), form.read(cx).description.clone())); + type_into(&amount, "42.50", cx); + type_into(&description, "Test purchase", cx); + cx.update(|_, cx| form.update(cx, |f, cx| f.submit(cx))); + cx.update(|_, cx| { + let s = screen.read(cx); + assert!(!s.has_dialog()); + assert_eq!(s.transactions().len(), 26); + let added = s.transactions().iter().find(|t| t.description == "Test purchase").expect("saved"); + assert_eq!(added.amount, 4250); + assert_eq!(added.transaction_type, "expense"); + assert_eq!(added.account_id, account.id, "defaults to the first account"); + }); + let after = commands::get_accounts(&db).unwrap().remove(0); + assert_eq!(after.balance, account.balance - 4250); + } + + #[gpui_kit::test] + fn zero_or_blank_amount_is_ignored(cx: &mut TestAppContext) { + let (screen, _db, cx) = setup(cx); + cx.update(|window, cx| screen.update(cx, |s, cx| s.open_add(window, cx))); + let form = form_of(&screen, cx); + cx.update(|_, cx| form.update(cx, |f, cx| f.submit(cx))); + let amount = cx.update(|_, cx| form.read(cx).amount.clone()); + type_into(&amount, "0", cx); + cx.update(|_, cx| form.update(cx, |f, cx| f.submit(cx))); + cx.update(|_, cx| { + assert!(screen.read(cx).has_dialog()); + assert_eq!(screen.read(cx).transactions().len(), 25); + }); + } + + #[gpui_kit::test] + fn switching_to_income_swaps_the_category_list(cx: &mut TestAppContext) { + let (screen, db, cx) = setup(cx); + cx.update(|window, cx| screen.update(cx, |s, cx| s.open_add(window, cx))); + let form = form_of(&screen, cx); + let expense_names: Vec = commands::get_categories(&db).unwrap().into_iter().filter(|c| c.category_type == "expense").map(|c| c.id).collect(); + cx.update(|window, cx| form.update(cx, |f, cx| f.set_kind("income", window, cx))); + let (amount, description, category) = cx.update(|_, cx| { + let f = form.read(cx); + (f.amount.clone(), f.description.clone(), f.category.clone()) + }); + // Pick the first real income category (index 0 is "None"). + cx.update(|_, cx| category.update(cx, |c, cx| c.set_selected(Some(1), cx))); + type_into(&amount, "100", cx); + type_into(&description, "Paycheck", cx); + cx.update(|_, cx| form.update(cx, |f, cx| f.submit(cx))); + cx.update(|_, cx| { + let added = screen.read(cx).transactions().iter().find(|t| t.description == "Paycheck").cloned().expect("saved"); + assert_eq!(added.transaction_type, "income"); + let cat = added.category_id.expect("income category chosen"); + assert!(!expense_names.contains(&cat), "income form must not offer expense categories"); + }); + } + + #[gpui_kit::test] + fn edit_changes_the_transaction_in_place(cx: &mut TestAppContext) { + let (screen, _db, cx) = setup(cx); + let original = cx.update(|_, cx| screen.read(cx).transactions()[0].clone()); + cx.update(|window, cx| screen.update(cx, |s, cx| s.open_form(Some(original.clone()), window, cx))); + let form = form_of(&screen, cx); + let (amount, date) = cx.update(|_, cx| { + let f = form.read(cx); + assert_eq!(f.amount.read(cx).value().as_ref(), format!("{:.2}", original.amount as f64 / 100.0)); + (f.amount.clone(), f.date.clone()) + }); + type_into(&amount, "1.23", cx); + cx.update(|window, cx| date.update(cx, |d, cx| d.set_date(Date::Single(chrono::NaiveDate::from_ymd_opt(2026, 1, 2)), window, cx))); + cx.update(|_, cx| form.update(cx, |f, cx| f.submit(cx))); + cx.update(|_, cx| { + let s = screen.read(cx); + assert_eq!(s.transactions().len(), 25, "no duplicate"); + let saved = s.transactions().iter().find(|t| t.id == original.id).unwrap(); + assert_eq!(saved.amount, 123); + assert_eq!(saved.transaction_date, "2026-01-02"); + }); + } + + #[gpui_kit::test] + fn date_presets_filter_the_list(cx: &mut TestAppContext) { + let (screen, _db, cx) = setup(cx); + select_filter(&screen, DateFilter::Today, cx); + let today = chrono::Local::now().date_naive().format("%Y-%m-%d").to_string(); + cx.update(|_, cx| { + let s = screen.read(cx); + assert!(s.filtered(cx).iter().all(|t| t.transaction_date == today)); + assert!(s.filtered(cx).len() < 25); + }); + select_filter(&screen, DateFilter::LastYear, cx); + cx.update(|_, cx| { + let s = screen.read(cx); + let year = (chrono::Local::now().date_naive().format("%Y").to_string().parse::().unwrap() - 1).to_string(); + assert!(s.filtered(cx).iter().all(|t| t.transaction_date.starts_with(&year))); + }); + } + + #[gpui_kit::test] + fn custom_range_uses_the_range_picker(cx: &mut TestAppContext) { + let (screen, _db, cx) = setup(cx); + let all = cx.update(|_, cx| screen.read(cx).transactions().to_vec()); + let mut dates: Vec<_> = all.iter().map(|t| t.transaction_date.clone()).collect(); + dates.sort(); + let target = dates[dates.len() / 2].clone(); + let target_date = date_field::parse_iso(&target).unwrap(); + select_filter(&screen, DateFilter::Custom, cx); + let range = cx.update(|_, cx| screen.read(cx).date_filter.range.clone()); + cx.update(|window, cx| range.update(cx, |r, cx| r.set_date(Date::Range(Some(target_date), Some(target_date)), window, cx))); + cx.update(|_, cx| { + let shown = screen.read(cx).filtered(cx); + assert!(!shown.is_empty()); + assert!(shown.iter().all(|t| t.transaction_date == target)); + assert_eq!(shown.len(), all.iter().filter(|t| t.transaction_date == target).count()); + }); + } + + #[gpui_kit::test] + fn changing_the_filter_resets_to_page_one(cx: &mut TestAppContext) { + let (screen, _db, cx) = setup(cx); + cx.update(|_, cx| screen.update(cx, |s, cx| { s.page = 3; cx.notify(); })); + select_filter(&screen, DateFilter::ThisYear, cx); + cx.update(|_, cx| assert_eq!(screen.read(cx).page, 1)); + } + + #[gpui_kit::test] + fn per_page_paginates_the_list(cx: &mut TestAppContext) { + let (screen, _db, cx) = setup(cx); + let per_page = cx.update(|_, cx| screen.read(cx).per_page.clone()); + cx.update(|_, cx| per_page.update(cx, |c, cx| c.set_selected(Some(0), cx))); // 10 per page + cx.update(|_, cx| { + let s = screen.read(cx); + assert_eq!(s.applied_per_page, 10); + assert_eq!(s.pager(cx).total_pages(), 3); + assert_eq!(s.pager(cx).label(), "1–10 of 25"); + }); + cx.update(|_, cx| screen.update(cx, |s, cx| { s.page = 3; cx.notify(); })); + cx.update(|_, cx| { + let s = screen.read(cx); + assert_eq!(page_slice(&s.filtered(cx), &s.pager(cx)).len(), 5); + }); + } + + #[gpui_kit::test] + fn bulk_delete_removes_transactions_and_restores_balances(cx: &mut TestAppContext) { + let (screen, db, cx) = setup(cx); + let before: i64 = commands::get_accounts(&db).unwrap().iter().map(|a| a.balance).sum(); + let victim = cx.update(|_, cx| screen.read(cx).transactions()[0].clone()); + cx.update(|_, cx| screen.update(cx, |s, cx| s.toggle(&victim.id, cx))); + let ids: Vec = cx.update(|_, cx| screen.read(cx).selected.iter().cloned().collect()); + assert_eq!(ids.len(), 1); + cx.update(|_, cx| screen.update(cx, |s, cx| s.ask_delete(ids, cx))); + let dialog = cx.update(|_, cx| match screen.read(cx).dialog.as_ref() { + Some(Dialog::Confirm(d, _)) => d.clone(), + _ => panic!("confirm dialog should be open"), + }); + cx.update(|_, cx| dialog.update(cx, |_, cx| cx.emit(ConfirmEvent::Confirm))); + cx.update(|_, cx| { + let s = screen.read(cx); + assert_eq!(s.transactions().len(), 24); + assert!(s.selected.is_empty()); + }); + let after: i64 = commands::get_accounts(&db).unwrap().iter().map(|a| a.balance).sum(); + let delta = if victim.transaction_type == "expense" { victim.amount } else { -victim.amount }; + assert_eq!(after, before + delta, "deleting reverses the balance effect"); + } + + #[gpui_kit::test] + fn export_writes_csv_and_json_files(cx: &mut TestAppContext) { + let (screen, _db, cx) = setup(cx); + cx.update(|_, cx| screen.update(cx, |s, cx| s.open_export(cx))); + let dialog = cx.update(|_, cx| match screen.read(cx).dialog.as_ref() { + Some(Dialog::Export(d)) => d.clone(), + _ => panic!("export dialog should be open"), + }); + let dir = std::env::temp_dir().join(format!("chelete-export-test-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + + let csv_path = dir.join("out.csv"); + cx.update(|_, cx| dialog.read(cx).write_to(&csv_path)).unwrap(); + let csv = std::fs::read_to_string(&csv_path).unwrap(); + assert_eq!(csv.lines().count(), 26, "header plus 25 transactions"); + assert!(csv.starts_with("id,transaction_date,")); + + cx.update(|_, cx| dialog.update(cx, |d, _| d.format = ExportFormat::Json)); + let json_path = dir.join("out.json"); + cx.update(|_, cx| dialog.read(cx).write_to(&json_path)).unwrap(); + let json: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&json_path).unwrap()).unwrap(); + assert_eq!(json["transactions"].as_array().unwrap().len(), 25); + assert_eq!(json["accounts"].as_array().unwrap().len(), 4); + std::fs::remove_dir_all(&dir).ok(); + } + + #[gpui_kit::test] + fn export_to_an_unwritable_path_reports_an_error(cx: &mut TestAppContext) { + let (screen, _db, cx) = setup(cx); + cx.update(|_, cx| screen.update(cx, |s, cx| s.open_export(cx))); + let dialog = cx.update(|_, cx| match screen.read(cx).dialog.as_ref() { + Some(Dialog::Export(d)) => d.clone(), + _ => panic!("export dialog should be open"), + }); + let result = cx.update(|_, cx| dialog.read(cx).write_to(std::path::Path::new("/nonexistent-dir/x/out.csv"))); + assert!(result.is_err()); + } +} From 0c2361e8a4d2d9014295ec4245c45cc70a0e6f0f Mon Sep 17 00:00:00 2001 From: Victor Phos Date: Fri, 2 Oct 2026 14:09:54 +0200 Subject: [PATCH 24/36] Add the CSV import dialog Choose a file with the native picker, review the auto-detected column mapping and defaults, preview the first rows with errors and duplicates flagged, then import and see the counts. Mappings are remembered per CSV layout. Wired into Transactions and the command palette. --- src/ui/csv_import.rs | 692 +++++++++++++++++++++++++++++++++++++++++ src/ui/mod.rs | 1 + src/ui/shell.rs | 6 +- src/ui/transactions.rs | 28 +- src/ui/widgets.rs | 13 +- 5 files changed, 737 insertions(+), 3 deletions(-) create mode 100644 src/ui/csv_import.rs diff --git a/src/ui/csv_import.rs b/src/ui/csv_import.rs new file mode 100644 index 0000000..b17e078 --- /dev/null +++ b/src/ui/csv_import.rs @@ -0,0 +1,692 @@ +use super::widgets::*; +use chelete_lib::commands::{self, Account, Category, ImportOptions}; +use chelete_lib::database::DbState; +use chelete_lib::format::format_money; +use chelete_lib::import::session::{ + flag_duplicates, is_valid_for_preview, set_column, type_label, valid_count, SavedMappings, +}; +use chelete_lib::import::{parse_csv_preview, CsvField, CsvMapping, CsvPreview, ImportResult}; +use gpui_kit::base::input::InputState; +use gpui_kit::base::CheckboxState; +use gpui_kit::{ + div, prelude::*, rems, Context, Entity, EventEmitter, IntoElement, PathPromptOptions, Render, + Subscription, Window, +}; +use gpui_omarchy::{alert, button, input, select, ActiveTheme as _, ButtonVariant, ChoiceState, Status}; +use std::path::PathBuf; +use std::sync::Arc; + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Stage { + Mapping, + Preview, + Result, +} + +#[derive(Clone, Debug)] +pub enum CsvImportEvent { + /// Closed after an import ran; the list should reload. + Done, + Cancel, +} + +fn field_options() -> Vec<(String, String)> { + let mut items = vec![(String::new(), "— not mapped —".to_string())]; + items.extend( + CsvField::all() + .iter() + .map(|f| (field_value(*f).to_string(), f.label().to_string())), + ); + items +} + +fn field_value(field: CsvField) -> &'static str { + match field { + CsvField::Date => "date", + CsvField::Description => "description", + CsvField::Merchant => "merchant", + CsvField::Amount => "amount", + CsvField::IncomeAmount => "income_amount", + CsvField::ExpenseAmount => "expense_amount", + CsvField::Type => "type", + CsvField::Category => "category", + CsvField::Account => "account", + CsvField::Currency => "currency", + CsvField::Notes => "notes", + CsvField::Ignore => "ignore", + } +} + +fn field_from_value(value: &str) -> Option { + CsvField::all().iter().copied().find(|f| field_value(*f) == value) +} + +pub struct CsvImportDialog { + db: Arc, + stage: Stage, + path: Option, + mapping: CsvMapping, + preview: Option, + duplicates: Vec, + result: Option, + error: Option, + skip_duplicates: bool, + column_choices: Vec<(String, Entity)>, + account: Entity, + category: Entity, + default_type: Entity, + debit_alias: Entity, + credit_alias: Entity, + currency: Entity, + accounts: Vec, + categories: Vec, + saved: SavedMappings, + saved_path: PathBuf, + syncing: bool, + _subs: Vec, +} + +impl EventEmitter for CsvImportDialog {} + +impl CsvImportDialog { + pub fn new( + db: Arc, + accounts: Vec, + categories: Vec, + saved_path: PathBuf, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let mut account_items = vec![(String::new(), "None".to_string())]; + account_items.extend(accounts.iter().map(|a| (a.id.clone(), a.name.clone()))); + let mut category_items = vec![(String::new(), "None".to_string())]; + category_items.extend(categories.iter().map(|c| (c.id.clone(), format!("{} ({})", c.name, c.category_type)))); + let type_items = || vec![("expense".to_string(), "Expense".to_string()), ("income".to_string(), "Income".to_string())]; + let account = choice(account_items, Some(""), window, cx); + let category = choice(category_items, Some(""), window, cx); + let default_type = choice( + vec![(String::new(), "None".into()), ("expense".into(), "Expense".into()), ("income".into(), "Income".into())], + Some(""), + window, + cx, + ); + let debit_alias = choice(type_items(), Some("expense"), window, cx); + let credit_alias = choice(type_items(), Some("income"), window, cx); + let currency = text_input_with("USD", "USD", window, cx); + let mut subs = Vec::new(); + for c in [&account, &category, &default_type, &debit_alias, &credit_alias] { + subs.push(cx.observe(c, |this, _, cx| this.read_controls(cx))); + } + subs.push(cx.observe(¤cy, |this, _, cx| this.read_controls(cx))); + Self { + saved: SavedMappings::load_from(&saved_path), + saved_path, + db, + stage: Stage::Mapping, + path: None, + mapping: CsvMapping::default(), + preview: None, + duplicates: Vec::new(), + result: None, + error: None, + skip_duplicates: true, + column_choices: Vec::new(), + account, + category, + default_type, + debit_alias, + credit_alias, + currency, + accounts, + categories, + syncing: false, + _subs: subs, + } + } + + pub fn stage(&self) -> Stage { + self.stage + } + + /// Ask for a file with the system dialog, then load it. + pub fn choose_file(&mut self, window: &mut Window, cx: &mut Context) { + let picked = cx.prompt_for_paths(PathPromptOptions { files: true, directories: false, multiple: false, prompt: None }); + cx.spawn_in(window, async move |this, cx| { + if let Ok(Ok(Some(paths))) = picked.await { + if let Some(path) = paths.into_iter().next() { + this.update_in(cx, |this, window, cx| this.load_file(&path.to_string_lossy(), window, cx)).ok(); + } + } + }) + .detach(); + } + + pub fn load_file(&mut self, path: &str, window: &mut Window, cx: &mut Context) { + self.error = None; + // A first pass with no mapping just reads the headers. + let headers = match parse_csv_preview(path, &CsvMapping::default()) { + Ok(p) => p.headers, + Err(e) => { + self.error = Some(e); + return cx.notify(); + } + }; + self.path = Some(path.to_string()); + self.mapping = self.saved.get(&headers).cloned().unwrap_or_else(|| CsvMapping::from_headers(&headers)); + self.column_choices = headers + .iter() + .map(|h| { + let current = self.mapping.columns.get(h).map(|f| field_value(*f).to_string()).unwrap_or_default(); + let state = choice(field_options(), Some(¤t), window, cx); + let header = h.clone(); + self._subs.push(cx.observe(&state, move |this, _, cx| this.column_changed(&header, cx))); + (h.clone(), state) + }) + .collect(); + self.apply_mapping_to_controls(window, cx); + self.refresh_preview(); + cx.notify(); + } + + fn column_changed(&mut self, header: &str, cx: &mut Context) { + if self.syncing { + return; + } + let Some((_, state)) = self.column_choices.iter().find(|(h, _)| h == header) else { + return; + }; + let field = choice_value(state, cx).and_then(|v| field_from_value(&v)); + set_column(&mut self.mapping, header, field); + self.syncing = true; + // Assigning a field releases it from any other column; reflect that. + for (h, state) in &self.column_choices { + let want = self.mapping.columns.get(h).map(|f| field_value(*f)).unwrap_or(""); + if choice_value(state, cx).as_deref().unwrap_or("") != want { + let index = field_options().iter().position(|(v, _)| v == want); + state.update(cx, |s, cx| s.set_selected(index, cx)); + } + } + self.syncing = false; + cx.notify(); + } + + /// Copy the mapping's defaults into the controls (after loading a saved mapping). + fn apply_mapping_to_controls(&mut self, window: &mut Window, cx: &mut Context) { + self.syncing = true; + let m = &self.mapping; + let index_of = |items: Vec, wanted: &str| items.iter().position(|v| v == wanted); + let accounts: Vec = std::iter::once(String::new()).chain(self.accounts.iter().map(|a| a.id.clone())).collect(); + let categories: Vec = std::iter::once(String::new()).chain(self.categories.iter().map(|c| c.id.clone())).collect(); + let pick = |state: &Entity, idx: Option, cx: &mut Context| state.update(cx, |s, cx| s.set_selected(idx, cx)); + pick(&self.account, index_of(accounts, m.default_account_id.as_deref().unwrap_or("")), cx); + pick(&self.category, index_of(categories, m.default_category_id.as_deref().unwrap_or("")), cx); + pick( + &self.default_type, + index_of(vec![String::new(), "expense".into(), "income".into()], m.default_transaction_type.as_deref().unwrap_or("")), + cx, + ); + let alias = |key: &str, default: &str| m.type_aliases.get(key).cloned().unwrap_or_else(|| default.to_string()); + pick(&self.debit_alias, index_of(vec!["expense".into(), "income".into()], &alias("debit", "expense")), cx); + pick(&self.credit_alias, index_of(vec!["expense".into(), "income".into()], &alias("credit", "income")), cx); + let currency = m.default_currency.clone().unwrap_or_else(|| "USD".into()); + self.currency.update(cx, |s, cx| s.set_value(currency, window, cx)); + self.syncing = false; + } + + /// Controls -> mapping defaults. + fn read_controls(&mut self, cx: &mut Context) { + if self.syncing { + return; + } + let blank = |v: Option| v.filter(|s| !s.is_empty()); + self.mapping.default_account_id = blank(choice_value(&self.account, cx)); + self.mapping.default_category_id = blank(choice_value(&self.category, cx)); + self.mapping.default_transaction_type = blank(choice_value(&self.default_type, cx)); + let currency = text_of(&self.currency, cx).to_uppercase(); + self.mapping.default_currency = Some(if currency.is_empty() { "USD".into() } else { currency }); + for (key, state) in [("debit", &self.debit_alias), ("credit", &self.credit_alias)] { + if let Some(to) = choice_value(state, cx) { + self.mapping.type_aliases.insert(key.to_string(), to); + } + } + cx.notify(); + } + + fn refresh_preview(&mut self) { + let Some(path) = self.path.clone() else { return }; + match parse_csv_preview(&path, &self.mapping) { + Ok(preview) => { + self.duplicates = flag_duplicates(&preview.rows, &self.mapping); + self.preview = Some(preview); + } + Err(e) => self.error = Some(e), + } + } + + pub fn is_ready(&self) -> bool { + self.path.is_some() && is_valid_for_preview(&self.mapping) + } + + pub fn show_preview(&mut self, cx: &mut Context) { + if !self.is_ready() { + return; + } + self.error = None; + self.refresh_preview(); + if self.error.is_none() { + if let Some(p) = &self.preview { + self.saved.insert(&p.headers, self.mapping.clone()); + // Remembering the mapping is a convenience; failing to write it must not block the import. + let _ = self.saved.save_to(&self.saved_path); + } + self.stage = Stage::Preview; + } + cx.notify(); + } + + pub fn import(&mut self, cx: &mut Context) { + let Some(path) = self.path.clone() else { return }; + let options = ImportOptions { + default_account_id: self.mapping.default_account_id.clone(), + default_category_id: self.mapping.default_category_id.clone(), + default_currency: self.mapping.default_currency.clone(), + skip_duplicates: self.skip_duplicates, + }; + match commands::import_transactions(&self.db, path, self.mapping.clone(), options) { + Ok(result) => { + self.result = Some(result); + self.error = None; + self.stage = Stage::Result; + } + Err(e) => self.error = Some(e), + } + cx.notify(); + } + + pub fn ready_count(&self) -> usize { + self.preview.as_ref().map_or(0, |p| valid_count(&p.rows, &self.duplicates)) + } + + fn mapping_stage(&self, window: &mut Window, cx: &mut Context) -> gpui_kit::Div { + let t = cx.omarchy().clone(); + let has_type = self.mapping.field_column(CsvField::Type).is_some(); + let separate = self.mapping.field_column(CsvField::IncomeAmount).is_some() || self.mapping.field_column(CsvField::ExpenseAmount).is_some(); + + let file_row = div() + .flex() + .items_center() + .justify_between() + .gap(rems(0.75)) + .child(match &self.path { + Some(p) => div().text_color(t.secondary).overflow_hidden().child(p.clone()).into_any_element(), + None => div().text_color(t.secondary).child("No file chosen").into_any_element(), + }) + .child( + button("csv-choose", if self.path.is_some() { "Change" } else { "Choose CSV File" }, ButtonVariant::Outline, cx) + .on_click(cx.listener(|this, _, window, cx| this.choose_file(window, cx))), + ); + + let mut body = div().flex().flex_col().gap(rems(0.75)).child(file_row); + if !self.column_choices.is_empty() { + let mut table = div().id("csv-columns").flex().flex_col().max_h(rems(18.)).overflow_y_scroll().border_1().border_color(t.border).child( + div().flex().bg(t.surface).font_weight(gpui_kit::FontWeight::BOLD).child(cell(None, "CSV Column")).child(cell(None, "Chelete Field")), + ); + let choices = self.column_choices.clone(); + for (i, (header, state)) in choices.iter().enumerate() { + table = table.child( + div() + .flex() + .items_center() + .py(rems(0.125)) + .border_t_1() + .border_color(t.border) + .child(cell(None, header.clone())) + .child(cell(None, select(("csv-col", i), state, window, cx))), + ); + } + body = body.child(table); + + let mut defaults = div() + .flex() + .flex_wrap() + .gap(rems(0.5)) + .child(field("Default Account", select("csv-account", &self.account, window, cx), cx)) + .child(field("Default Category", select("csv-category", &self.category, window, cx), cx)) + .child(field("Default Currency", input("csv-currency", &self.currency, window, cx), cx)) + .child(field("Default Type", select("csv-type", &self.default_type, window, cx), cx)); + if has_type { + defaults = defaults + .child(field("Debit means", select("csv-debit", &self.debit_alias, window, cx), cx)) + .child(field("Credit means", select("csv-credit", &self.credit_alias, window, cx), cx)); + } + body = body.child(defaults).child(dim( + if separate { + "Map at least Date and Description plus an amount. Separate Income and Expense columns also satisfy the type requirement." + } else { + "Map at least Date, Description, and an Amount + Type field to continue." + }, + cx, + )); + } + body.child( + div() + .flex() + .justify_end() + .gap(rems(0.5)) + .child(button("csv-cancel", "Cancel", ButtonVariant::Outline, cx).on_click(cx.listener(|_, _, _, cx| cx.emit(CsvImportEvent::Cancel)))) + .child( + button("csv-preview", "Preview", ButtonVariant::Primary, cx) + .disabled(!self.is_ready()) + .on_click(cx.listener(|this, _, _, cx| this.show_preview(cx))), + ), + ) + } + + fn preview_stage(&self, cx: &mut Context) -> gpui_kit::Div { + let t = cx.omarchy().clone(); + let Some(preview) = &self.preview else { + return div(); + }; + let mut table = div().id("csv-preview-rows").flex().flex_col().max_h(rems(20.)).overflow_y_scroll().border_1().border_color(t.border).child( + div() + .flex() + .bg(t.surface) + .font_weight(gpui_kit::FontWeight::BOLD) + .child(cell(Some(6.), "Date")) + .child(cell(None, "Description")) + .child(cell(Some(7.), "Amount")) + .child(cell(Some(5.), "Type")) + .child(cell(Some(8.), "Account")) + .child(cell(Some(8.), "Category")) + .child(cell(Some(10.), "Status")), + ); + for (row, dup) in preview.rows.iter().zip(&self.duplicates) { + let status = if !row.errors.is_empty() { + div().text_color(t.danger).child(row.errors.join("; ")) + } else if *dup { + div().text_color(t.warning).child("Duplicate") + } else { + div().text_color(t.success).child("OK") + }; + table = table.child( + div() + .flex() + .py(rems(0.125)) + .border_t_1() + .border_color(t.border) + .child(cell(Some(6.), row.date.clone().unwrap_or_else(|| "—".into()))) + .child(cell(None, row.description.clone().unwrap_or_else(|| "—".into()))) + .child(cell(Some(7.), row.amount_cents.map(format_money).unwrap_or_else(|| "—".into()))) + .child(cell(Some(5.), type_label(row))) + .child(cell(Some(8.), row.account.clone().unwrap_or_else(|| if self.mapping.default_account_id.is_some() { "(default)".into() } else { "—".into() }))) + .child(cell(Some(8.), row.category.clone().unwrap_or_else(|| if self.mapping.default_category_id.is_some() { "(default)".into() } else { "—".into() }))) + .child(cell(Some(10.), status)), + ); + } + let ready = self.ready_count(); + let screen = cx.entity(); + div() + .flex() + .flex_col() + .gap(rems(0.75)) + .child(dim(format!("{} preview rows (of {} total)", preview.rows.len(), preview.total_rows), cx)) + .child(table) + .child(check( + "csv-skip-dupes", + "Skip exact duplicates (same date, amount, description, account)", + if self.skip_duplicates { CheckboxState::Checked } else { CheckboxState::Unchecked }, + move |_, cx| screen.update(cx, |s, cx| { s.skip_duplicates = !s.skip_duplicates; cx.notify(); }), + cx, + )) + .child( + div() + .flex() + .justify_end() + .gap(rems(0.5)) + .child(button("csv-cancel", "Cancel", ButtonVariant::Outline, cx).on_click(cx.listener(|_, _, _, cx| cx.emit(CsvImportEvent::Cancel)))) + .child(button("csv-back", "Back", ButtonVariant::Outline, cx).on_click(cx.listener(|this, _, _, cx| { this.stage = Stage::Mapping; cx.notify(); }))) + .child( + button("csv-import", format!("Import {ready} Transactions"), ButtonVariant::Primary, cx) + .disabled(ready == 0) + .on_click(cx.listener(|this, _, _, cx| this.import(cx))), + ), + ) + } + + fn result_stage(&self, cx: &mut Context) -> gpui_kit::Div { + let t = cx.omarchy().clone(); + let Some(result) = &self.result else { + return div(); + }; + let stat = |value: usize, label: &str, color| { + div().flex_1().flex().flex_col().items_center().child(div().text_size(rems(1.5)).text_color(color).child(value.to_string())).child(dim(label.to_string(), cx)) + }; + let mut body = div().flex().flex_col().gap(rems(0.75)).child( + div() + .flex() + .child(stat(result.imported, "Imported", t.success)) + .child(stat(result.skipped, "Skipped", t.secondary)) + .child(stat(result.errors.len(), "Errors", if result.errors.is_empty() { t.secondary } else { t.danger })), + ); + if !result.errors.is_empty() { + body = body.child( + div() + .id("csv-errors") + .max_h(rems(10.)) + .overflow_y_scroll() + .flex() + .flex_col() + .gap(rems(0.25)) + .children(result.errors.iter().map(|e| div().text_color(t.danger).child(e.clone()))), + ); + } + body.child( + div().flex().justify_end().gap(rems(0.5)).child(button("csv-done", "Done", ButtonVariant::Primary, cx).on_click(cx.listener(|_, _, _, cx| cx.emit(CsvImportEvent::Done)))), + ) + } +} + +impl Render for CsvImportDialog { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let error = self.error.clone().map(|e| alert(e, Status::Error, cx)); + let stage = match self.stage { + Stage::Mapping => self.mapping_stage(window, cx), + Stage::Preview => self.preview_stage(cx), + Stage::Result => self.result_stage(cx), + }; + let body = div().flex().flex_col().gap(rems(0.75)).children(error).child(stage); + modal_sized( + 52., + "Import Transactions from CSV", + body, + cx.listener(|_, _, _, cx| cx.emit(CsvImportEvent::Cancel)), + cx, + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ui::test_support::{init, seeded_db}; + use gpui_kit::{TestAppContext, VisualTestContext}; + + struct Env { + dir: PathBuf, + } + + impl Env { + fn new(name: &str) -> Self { + let dir = std::env::temp_dir().join(format!("chelete-csv-ui-{name}-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + Self { dir } + } + fn csv(&self, name: &str, body: &str) -> String { + let path = self.dir.join(name); + std::fs::write(&path, body).unwrap(); + path.to_string_lossy().to_string() + } + fn mappings(&self) -> PathBuf { + self.dir.join("mappings.json") + } + } + + impl Drop for Env { + fn drop(&mut self) { + std::fs::remove_dir_all(&self.dir).ok(); + } + } + + fn setup<'a>(cx: &'a mut TestAppContext, env: &Env) -> (Entity, Arc, &'a mut VisualTestContext) { + init(cx); + let db = seeded_db(); + let handle = db.clone(); + let accounts = commands::get_accounts(&db).unwrap(); + let categories = commands::get_categories(&db).unwrap(); + let path = env.mappings(); + let (dialog, vcx) = cx.add_window_view(move |window, cx| CsvImportDialog::new(db, accounts, categories, path, window, cx)); + (dialog, handle, vcx) + } + + fn load(dialog: &Entity, path: &str, cx: &mut VisualTestContext) { + let path = path.to_string(); + cx.update(|window, cx| dialog.update(cx, |d, cx| d.load_file(&path, window, cx))); + } + + const BANK: &str = "Date,Payee,Debit,Credit,Account\n2026-03-01,Coffee,4.50,,Main\n2026-03-01,Coffee,4.50,,Main\n2026-03-02,Salary,,2000.00,Main\n"; + + #[gpui_kit::test] + fn recognises_a_bank_csv_and_imports_it(cx: &mut TestAppContext) { + let env = Env::new("bank"); + let (dialog, db, cx) = setup(cx, &env); + let account = commands::get_accounts(&db).unwrap().remove(0); + let before = commands::get_transactions(&db).unwrap().len(); + load(&dialog, &env.csv("bank.csv", BANK), cx); + + cx.update(|_, cx| { + let d = dialog.read(cx); + assert!(d.is_ready(), "Date/Payee/Debit/Credit are recognised without any clicks"); + assert_eq!(d.column_choices.len(), 5); + assert_eq!(d.stage(), Stage::Mapping); + }); + // The CSV names an account that does not exist, so fall back to a default. + let acct = cx.update(|_, cx| dialog.read(cx).account.clone()); + cx.update(|_, cx| acct.update(cx, |c, cx| c.set_selected(Some(1), cx))); + cx.update(|_, cx| dialog.update(cx, |d, cx| d.show_preview(cx))); + cx.update(|_, cx| { + let d = dialog.read(cx); + assert_eq!(d.stage(), Stage::Preview); + assert_eq!(d.duplicates, vec![false, true, false]); + assert_eq!(d.ready_count(), 2, "the repeated coffee is held back"); + }); + cx.update(|_, cx| dialog.update(cx, |d, cx| d.import(cx))); + cx.update(|_, cx| { + let d = dialog.read(cx); + assert_eq!(d.stage(), Stage::Result); + let result = d.result.as_ref().unwrap(); + assert_eq!((result.imported, result.skipped), (2, 1)); + assert!(result.errors.is_empty(), "{:?}", result.errors); + }); + let after = commands::get_transactions(&db).unwrap(); + assert_eq!(after.len(), before + 2); + let salary = after.iter().find(|t| t.description == "Salary").expect("imported"); + assert_eq!((salary.amount, salary.transaction_type.as_str()), (200000, "income")); + assert_eq!(salary.account_id, account.id); + } + + #[gpui_kit::test] + fn without_skipping_duplicates_both_coffees_import(cx: &mut TestAppContext) { + let env = Env::new("nodedupe"); + let (dialog, db, cx) = setup(cx, &env); + let before = commands::get_transactions(&db).unwrap().len(); + load(&dialog, &env.csv("bank.csv", BANK), cx); + let acct = cx.update(|_, cx| dialog.read(cx).account.clone()); + cx.update(|_, cx| acct.update(cx, |c, cx| c.set_selected(Some(1), cx))); + cx.update(|_, cx| dialog.update(cx, |d, cx| { d.skip_duplicates = false; d.show_preview(cx); d.import(cx) })); + assert_eq!(commands::get_transactions(&db).unwrap().len(), before + 3); + } + + #[gpui_kit::test] + fn assigning_a_field_releases_it_from_the_other_column(cx: &mut TestAppContext) { + let env = Env::new("conflict"); + let (dialog, _db, cx) = setup(cx, &env); + load(&dialog, &env.csv("bank.csv", BANK), cx); + let payee = cx.update(|_, cx| dialog.read(cx).column_choices.iter().find(|(h, _)| h == "Payee").unwrap().1.clone()); + let date_index = field_options().iter().position(|(v, _)| v == "date").unwrap(); + cx.update(|_, cx| payee.update(cx, |c, cx| c.set_selected(Some(date_index), cx))); + cx.update(|_, cx| { + let d = dialog.read(cx); + assert_eq!(d.mapping.field_column(CsvField::Date), Some("Payee")); + assert_eq!(d.mapping.columns.get("Date"), None, "old date column released"); + let released = d.column_choices.iter().find(|(h, _)| h == "Date").unwrap(); + assert_eq!(choice_value(&released.1, cx).as_deref(), Some(""), "its dropdown shows unmapped"); + assert!(!d.is_ready(), "description is now unmapped"); + }); + } + + #[gpui_kit::test] + fn cannot_preview_until_the_mapping_is_complete(cx: &mut TestAppContext) { + let env = Env::new("incomplete"); + let (dialog, _db, cx) = setup(cx, &env); + load(&dialog, &env.csv("odd.csv", "foo,bar\n1,2\n"), cx); + cx.update(|_, cx| { + let d = dialog.read(cx); + assert!(!d.is_ready()); + }); + cx.update(|_, cx| dialog.update(cx, |d, cx| d.show_preview(cx))); + cx.update(|_, cx| assert_eq!(dialog.read(cx).stage(), Stage::Mapping)); + } + + #[gpui_kit::test] + fn a_missing_file_shows_an_error_and_keeps_going(cx: &mut TestAppContext) { + let env = Env::new("missing"); + let (dialog, _db, cx) = setup(cx, &env); + load(&dialog, "/nonexistent/file.csv", cx); + cx.update(|_, cx| { + let d = dialog.read(cx); + assert!(d.error.is_some()); + assert!(d.path.is_none()); + assert!(!d.is_ready()); + }); + } + + #[gpui_kit::test] + fn rows_with_bad_data_are_reported_not_imported(cx: &mut TestAppContext) { + let env = Env::new("baddata"); + let (dialog, db, cx) = setup(cx, &env); + let before = commands::get_transactions(&db).unwrap().len(); + let csv = env.csv("bad.csv", "Date,Description,Amount,Type\n2026-03-01,Fine,10.00,expense\nnot-a-date,Broken,5.00,expense\n"); + load(&dialog, &csv, cx); + let acct = cx.update(|_, cx| dialog.read(cx).account.clone()); + cx.update(|_, cx| acct.update(cx, |c, cx| c.set_selected(Some(1), cx))); + cx.update(|_, cx| dialog.update(cx, |d, cx| d.show_preview(cx))); + cx.update(|_, cx| assert_eq!(dialog.read(cx).ready_count(), 1)); + cx.update(|_, cx| dialog.update(cx, |d, cx| d.import(cx))); + assert_eq!(commands::get_transactions(&db).unwrap().len(), before + 1); + } + + #[gpui_kit::test] + fn the_mapping_is_remembered_for_the_same_layout(cx: &mut TestAppContext) { + let env = Env::new("remember"); + let (dialog, db, cx) = setup(cx, &env); + load(&dialog, &env.csv("bank.csv", BANK), cx); + let acct = cx.update(|_, cx| dialog.read(cx).account.clone()); + cx.update(|_, cx| acct.update(cx, |c, cx| c.set_selected(Some(2), cx))); + let chosen = cx.update(|_, cx| dialog.read(cx).mapping.default_account_id.clone()); + assert!(chosen.is_some()); + cx.update(|_, cx| dialog.update(cx, |d, cx| d.show_preview(cx))); + assert!(env.mappings().exists(), "mapping written on preview"); + + // A second dialog on a same-shaped file starts from the saved choices. + let (db2, accounts, categories) = (db.clone(), commands::get_accounts(&db).unwrap(), commands::get_categories(&db).unwrap()); + let path = env.mappings(); + let second = cx.update(|window, cx| cx.new(|cx| CsvImportDialog::new(db2, accounts, categories, path, window, cx))); + load(&second, &env.csv("bank-again.csv", BANK), cx); + cx.update(|_, cx| { + let d = second.read(cx); + assert!(d.is_ready()); + assert!(d.mapping.default_account_id.is_some(), "default account restored from the saved mapping"); + assert_eq!(choice_value(&d.account, cx).as_deref(), d.mapping.default_account_id.as_deref()); + }); + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 5df7c7b..ae4c60c 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,6 +1,7 @@ //! GPUI front end: app shell, sidebar, command palette and screens. mod accounts; mod categories; +mod csv_import; mod date_field; mod date_filter_control; mod icons; diff --git a/src/ui/shell.rs b/src/ui/shell.rs index 62c0dd3..7ba0799 100644 --- a/src/ui/shell.rs +++ b/src/ui/shell.rs @@ -166,7 +166,11 @@ impl Shell { self.transactions.update(cx, |s, cx| s.open_add(window, cx)); return; } - Command::ImportTransactions => View::Transactions, + Command::ImportTransactions => { + self.navigate(View::Transactions, window, cx); + self.transactions.update(cx, |s, cx| s.open_import(window, cx)); + return; + } Command::AddAccount => { self.navigate(View::Accounts, window, cx); self.accounts.update(cx, |s, cx| s.open_add(window, cx)); diff --git a/src/ui/transactions.rs b/src/ui/transactions.rs index 1c4502a..2c2c544 100644 --- a/src/ui/transactions.rs +++ b/src/ui/transactions.rs @@ -1,4 +1,5 @@ use super::accounts::FormEvent; +use super::csv_import::{CsvImportDialog, CsvImportEvent}; use super::date_field; use super::date_filter_control::DateFilterControl; use super::icons::category_icon; @@ -30,6 +31,7 @@ enum Dialog { View(Transaction), Confirm(Entity, Vec), Export(Entity), + Import(Entity), } pub struct TransactionsScreen { @@ -43,6 +45,7 @@ pub struct TransactionsScreen { page: usize, selected: HashSet, dialog: Option, + mappings_path: PathBuf, _dialog_sub: Option, _subs: Vec, } @@ -76,6 +79,7 @@ impl TransactionsScreen { page: 1, selected: HashSet::new(), dialog: None, + mappings_path: chelete_lib::import::session::SavedMappings::default_path(), _dialog_sub: None, _subs: subs, }; @@ -186,6 +190,27 @@ impl TransactionsScreen { } } + /// Open the CSV import dialog and ask for a file straight away. + pub fn open_import(&mut self, window: &mut Window, cx: &mut Context) { + let dialog = self.new_import_dialog(window, cx); + dialog.update(cx, |d, cx| d.choose_file(window, cx)); + } + + fn new_import_dialog(&mut self, window: &mut Window, cx: &mut Context) -> Entity { + let dialog = cx.new(|cx| { + CsvImportDialog::new(self.db.clone(), self.accounts.clone(), self.categories.clone(), self.mappings_path.clone(), window, cx) + }); + self._dialog_sub = Some(cx.subscribe(&dialog, |this, _, event: &CsvImportEvent, cx| { + if matches!(event, CsvImportEvent::Done) { + this.load(); + } + this.close_dialog(cx); + })); + self.dialog = Some(Dialog::Import(dialog.clone())); + cx.notify(); + dialog + } + pub fn open_export(&mut self, cx: &mut Context) { let data = match commands::export_data(&self.db) { Ok(d) => d, @@ -270,7 +295,7 @@ impl Render for TransactionsScreen { .child( button("import-csv", "Import CSV", ButtonVariant::Outline, cx) .child(icon(IconName::Upload).size(rems(0.875))) - .on_click(cx.listener(|_, _, _, cx| cx.emit(ScreenEvent::error("CSV import is not available yet")))), + .on_click(cx.listener(|this, _, window, cx| this.open_import(window, cx))), ) .child( button("export-data", "Export Data", ButtonVariant::Outline, cx) @@ -384,6 +409,7 @@ impl Render for TransactionsScreen { Dialog::Form(f) => f.clone().into_any_element(), Dialog::Confirm(c, _) => c.clone().into_any_element(), Dialog::Export(e) => e.clone().into_any_element(), + Dialog::Import(i) => i.clone().into_any_element(), Dialog::View(tx) => self.view_dialog(tx, cx).into_any_element(), }); let filter_row = self.date_filter.render(today, window, cx); diff --git a/src/ui/widgets.rs b/src/ui/widgets.rs index aa65645..2fad714 100644 --- a/src/ui/widgets.rs +++ b/src/ui/widgets.rs @@ -55,6 +55,17 @@ pub fn modal( body: impl IntoElement, on_dismiss: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static, cx: &App, +) -> gpui_kit::Stateful
{ + modal_sized(26., title, body, on_dismiss, cx) +} + +/// Like [`modal`] with an explicit width in rems. +pub fn modal_sized( + width: f32, + title: &str, + body: impl IntoElement, + on_dismiss: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static, + cx: &App, ) -> gpui_kit::Stateful
{ let t = cx.omarchy(); div() @@ -72,7 +83,7 @@ pub fn modal( .child( div() .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) - .w(rems(26.)) + .w(rems(width)) .max_w_full() .max_h_full() .flex() From 92b59b09b56ce451f878d223bb6ce8da30e7d974 Mon Sep 17 00:00:00 2001 From: Victor Phos Date: Fri, 2 Oct 2026 14:10:09 +0200 Subject: [PATCH 25/36] Port the Overview filters and charts Date, type and account filters, income and expense donuts coloured by category, account balances, a paginated recent-transactions table and a welcome state. The screen is now an entity that reloads when revisited. Removes the placeholder screen and adds toast and render tests. --- src/ui/categories.rs | 2 +- src/ui/csv_import.rs | 1 + src/ui/mod.rs | 1 - src/ui/overview.rs | 463 +++++++++++++++++++++++++++++++++------- src/ui/placeholder.rs | 15 -- src/ui/shell.rs | 52 ++++- src/ui/subscriptions.rs | 3 +- src/ui/transactions.rs | 1 + src/ui/widgets.rs | 17 ++ 9 files changed, 452 insertions(+), 103 deletions(-) delete mode 100644 src/ui/placeholder.rs diff --git a/src/ui/categories.rs b/src/ui/categories.rs index cb6a3be..b25c4c1 100644 --- a/src/ui/categories.rs +++ b/src/ui/categories.rs @@ -191,7 +191,7 @@ impl CategoriesScreen { } } -pub fn parse_hex(value: &str) -> Option { +pub(super) fn parse_hex(value: &str) -> Option { let hex = value.trim().trim_start_matches('#'); if hex.len() != 6 { return None; diff --git a/src/ui/csv_import.rs b/src/ui/csv_import.rs index b17e078..0cf9602 100644 --- a/src/ui/csv_import.rs +++ b/src/ui/csv_import.rs @@ -144,6 +144,7 @@ impl CsvImportDialog { } } + #[cfg(test)] pub fn stage(&self) -> Stage { self.stage } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index ae4c60c..1db363b 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -7,7 +7,6 @@ mod date_filter_control; mod icons; mod overview; mod palette; -mod placeholder; mod projections; mod shell; mod settings; diff --git a/src/ui/overview.rs b/src/ui/overview.rs index bc50d49..b7c779b 100644 --- a/src/ui/overview.rs +++ b/src/ui/overview.rs @@ -1,104 +1,417 @@ -use chelete_lib::commands::{self, Overview}; +use super::categories::parse_hex; +use super::date_filter_control::DateFilterControl; +use super::icons::category_icon; +use super::widgets::*; +use chelete_lib::commands::{self, Account, Category, Overview, Transaction}; use chelete_lib::database::DbState; -use chelete_lib::format::{account_type_label, format_balance, format_money}; -use gpui_kit::{div, prelude::*, rems, App, Div, IntoElement, SharedString}; +use chelete_lib::format::{format_balance, format_money}; +use chelete_lib::overview::{filter_transactions, summarize, CategoryTotal, TypeFilter}; +use chelete_lib::paging::{page_slice, Pager}; +use gpui_kit::{ + div, prelude::*, px, rems, Context, Div, Entity, Hsla, IntoElement, Render, SharedString, + Subscription, Window, +}; use gpui_omarchy::chart::PieChart; -use gpui_omarchy::{alert, empty_state, panel, ActiveTheme as _, Status}; +use gpui_omarchy::{alert, empty_state, panel, select, ActiveTheme as _, ChoiceState, Status}; +use std::sync::Arc; -pub struct OverviewData { - pub result: Result, +const PER_PAGE: usize = 10; + +pub struct OverviewScreen { + db: Arc, + overview: Result, + transactions: Vec, + accounts: Vec, + categories: Vec, + date_filter: DateFilterControl, + kind: Entity, + account: Entity, + page: usize, + _account_sub: Option, + _subs: Vec, +} + +impl OverviewScreen { + pub fn new(db: Arc, window: &mut Window, cx: &mut Context) -> Self { + let date_filter = DateFilterControl::new(window, cx); + let kind = choice( + vec![("all".into(), "All Types".into()), ("income".into(), "Income".into()), ("expense".into(), "Expense".into())], + Some("all"), + window, + cx, + ); + let account = choice(vec![("all".into(), "All Accounts".into())], Some("all"), window, cx); + let mut subs = date_filter.observe(cx, |this: &mut Self, cx| { + this.page = 1; + cx.notify(); + }); + subs.push(cx.observe(&kind, |this, _, cx| { + this.page = 1; + cx.notify(); + })); + let mut screen = Self { + db, + overview: Ok(empty_overview()), + transactions: Vec::new(), + accounts: Vec::new(), + categories: Vec::new(), + date_filter, + kind, + account, + page: 1, + _account_sub: None, + _subs: subs, + }; + screen.load(window, cx); + screen + } + + fn load(&mut self, window: &mut Window, cx: &mut Context) { + self.overview = commands::get_overview(&self.db); + self.transactions = commands::get_transactions(&self.db).unwrap_or_default(); + self.accounts = commands::get_accounts(&self.db).unwrap_or_default(); + self.categories = commands::get_categories(&self.db).unwrap_or_default(); + + // The account list can change between visits, so rebuild the dropdown + // and keep the previous choice if that account still exists. + let previous = choice_value(&self.account, cx).unwrap_or_else(|| "all".into()); + let mut items = vec![("all".to_string(), "All Accounts".to_string())]; + items.extend(self.accounts.iter().map(|a| (a.id.clone(), a.name.clone()))); + self.account = choice(items, Some(&previous), window, cx); + self._account_sub = Some(cx.observe(&self.account, |this, _, cx| { + this.page = 1; + cx.notify(); + })); + } + + pub fn reload(&mut self, window: &mut Window, cx: &mut Context) { + self.load(window, cx); + cx.notify(); + } + + #[cfg(test)] + pub fn overview(&self) -> Result<&Overview, &String> { + self.overview.as_ref() + } + + fn bounds(&self, cx: &gpui_kit::App) -> (String, String) { + self.date_filter.bounds(chrono::Local::now().date_naive(), cx) + } + + fn visible_transactions(&self, cx: &gpui_kit::App) -> Vec<&Transaction> { + let (start, end) = self.bounds(cx); + let kind = TypeFilter::from_value(&choice_value(&self.kind, cx).unwrap_or_default()); + let account = choice_value(&self.account, cx).filter(|a| a != "all"); + filter_transactions(&self.transactions, &start, &end, kind, account.as_deref()) + } } -impl OverviewData { - pub fn load(db: &DbState) -> Self { - Self { - result: commands::get_overview(db), - } +fn empty_overview() -> Overview { + Overview { + total_balance: 0, + total_income: 0, + total_expenses: 0, + accounts: vec![], + recent_transactions: vec![], + category_spending: vec![], } } -fn stat(title: &'static str, value: String, color: gpui_kit::Hsla, cx: &App) -> Div { - panel(title, cx) +fn stat(title: &str, value: String, tint: Hsla, note: Option, cx: &gpui_kit::App) -> Div { + let card = panel(title.to_string(), cx) .flex_1() - .child(div().text_size(rems(1.5)).text_color(color).child(value)) + .child(div().text_size(rems(1.5)).text_color(tint).child(value)); + match note { + Some(n) => card.child(dim(n, cx)), + None => card, + } } -pub fn render(data: &OverviewData, cx: &App) -> impl IntoElement { +fn donut(title: &str, data: &[CategoryTotal], empty: &str, id: &'static str, cx: &gpui_kit::App) -> Div { let t = cx.omarchy(); - let page = div() - .id("overview") - .size_full() - .overflow_y_scroll() - .p(rems(1.5)) - .flex() - .flex_col() - .gap(rems(1.)); - - let overview = match &data.result { - Ok(o) => o, - Err(e) => { - return page.child(alert(format!("Could not load overview: {e}"), Status::Error, cx)) - } - }; - - let stats = div() - .flex() - .gap(rems(1.)) - .child(stat("Balance", format_balance(overview.total_balance), t.foreground, cx)) - .child(stat("Income", format_money(overview.total_income), t.success, cx)) - .child(stat("Expenses", format_money(-overview.total_expenses), t.danger, cx)); - - let spending: Vec<(SharedString, f32)> = overview - .category_spending + let slices: Vec<(SharedString, f32, Hsla)> = data .iter() - .filter(|c| c.spent > 0) - .map(|c| (SharedString::from(c.category_name.clone()), c.spent as f32 / 100.)) + .enumerate() + .map(|(i, c)| { + let color = c.color.as_deref().and_then(parse_hex).unwrap_or(t.chart[i % t.chart.len()]); + (SharedString::from(c.name.clone()), c.value as f32 / 100.0, color) + }) .collect(); - - let spending_panel = panel("Spending by category", cx).flex_1().child(if spending.is_empty() { - empty_state("No spending yet", "Expenses will appear here.", cx).into_any_element() + let body = if slices.is_empty() { + div().py(rems(1.)).text_color(t.secondary).child(empty.to_string()).into_any_element() } else { div() .w_full() - .h(rems(16.)) + .h(rems(15.)) .child( - PieChart::new(spending) - .id("overview-spending") - .name("Spent") - .value(|(_, v)| *v) - .inner_radius(56.) - .outer_radius(88.) + PieChart::new(slices) + .id(id) + .name("Total") + .value(|(_, v, _)| *v) + .color(|(_, _, c)| *c) + .inner_radius(48.) + .outer_radius(76.) .pad_angle(0.02) - .label(|(name, v)| format!("{name} ${v:.0}").into()), + .label(|(name, v, _)| format!("{name} ${v:.0}").into()), ) .into_any_element() - }); + }; + panel(title.to_string(), cx).flex_1().child(body) +} - let accounts = panel("Accounts", cx).flex_1().children(overview.accounts.iter().map(|a| { - div() +impl Render for OverviewScreen { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let t = cx.omarchy().clone(); + let page = div().id("overview").relative().size_full().overflow_y_scroll().p(rems(1.5)).flex().flex_col().gap(rems(1.)); + + let overview = match &self.overview { + Ok(o) => o, + Err(e) => return page.child(alert(format!("Could not load overview: {e}"), Status::Error, cx)), + }; + + let (start, end) = self.bounds(cx); + let summary = summarize(&self.transactions, &self.categories, &start, &end); + let visible = self.visible_transactions(cx); + let pager = Pager::new(self.page, PER_PAGE, visible.len()); + let rows: Vec = page_slice(&visible, &pager).iter().map(|t| (*t).clone()).collect(); + let nothing_at_all = self.accounts.is_empty() && self.transactions.is_empty(); + drop(visible); + + let today = chrono::Local::now().date_naive(); + let header = page_header("Overview", div().text_color(t.secondary).child(today.format("%B %Y").to_string()), cx); + let filter_row = self.date_filter.render(today, window, cx); + + let net_tint = if summary.net >= 0 { t.success } else { t.danger }; + let stats = div() + .flex() + .gap(rems(1.)) + .child(stat("Total Balance", format_balance(overview.total_balance), t.foreground, None, cx)) + .child(stat("Income", format_money(summary.total_income), t.success, None, cx)) + .child(stat("Expenses", format_money(-summary.total_expenses), t.danger, None, cx)) + .child(stat("Net", format_money(summary.net), net_tint, Some(format!("Savings rate: {}%", summary.savings_rate)), cx)); + + let accounts = (!self.accounts.is_empty()).then(|| { + div() + .flex() + .flex_col() + .gap(rems(0.5)) + .child(div().font_weight(gpui_kit::FontWeight::BOLD).child("Accounts")) + .child(div().flex().flex_wrap().gap(rems(1.)).children(self.accounts.iter().map(|a| { + panel(a.name.clone(), cx).w(rems(14.)).child(div().text_size(rems(1.25)).child(format_balance(a.balance))) + }))) + }); + + let breakdown = div() .flex() - .justify_between() + .flex_col() + .gap(rems(0.5)) + .child(div().font_weight(gpui_kit::FontWeight::BOLD).child("Breakdown")) .child( div() - .child(a.name.clone()) - .child(div().text_color(t.secondary).child(account_type_label(&a.account_type))), - ) - .child(format_balance(a.balance)) - })); + .flex() + .gap(rems(1.)) + .child(donut("Income", &summary.income_by_category, "No income for this period", "overview-income", cx)) + .child(donut("Expenses", &summary.expenses_by_category, "No expenses for this period", "overview-expenses", cx)), + ); - let recent = panel("Recent transactions", cx).children( - overview.recent_transactions.iter().map(|tx| { - let color = if tx.transaction_type == "income" { t.success } else { t.danger }; - let signed = if tx.transaction_type == "income" { tx.amount } else { -tx.amount }; + let list = if rows.is_empty() { + div().py(rems(1.)).text_color(t.secondary).child("No transactions match the selected filters.").into_any_element() + } else { + let mut table = div().flex().flex_col().border_1().border_color(t.border).child( + div() + .flex() + .py(rems(0.375)) + .bg(t.surface) + .text_color(t.bright) + .font_weight(gpui_kit::FontWeight::BOLD) + .child(cell(Some(6.), "Date")) + .child(cell(None, "Description")) + .child(cell(None, "Category")) + .child(cell(Some(9.), div().flex().justify_end().child("Amount"))), + ); + for tx in rows { + let category = tx.category_id.as_ref().and_then(|id| self.categories.iter().find(|c| &c.id == id)); + let tint = if tx.transaction_type == "income" { t.success } else { t.danger }; + let amount = if tx.transaction_type == "expense" { -tx.amount } else { tx.amount }; + let date = chrono::NaiveDate::parse_from_str(&tx.transaction_date, "%Y-%m-%d") + .map(|d| d.format("%b %e").to_string()) + .unwrap_or_else(|_| tx.transaction_date.clone()); + table = table.child( + div() + .flex() + .items_center() + .py(rems(0.25)) + .border_t_1() + .border_color(t.border) + .child(cell(Some(6.), date)) + .child(cell(None, tx.description.clone())) + .child(cell(None, match category { + Some(c) => div() + .flex() + .items_center() + .gap(rems(0.25)) + .children(c.icon.as_deref().map(|n| category_icon(n, px(12.), t.secondary))) + .child(c.name.clone()) + .into_any_element(), + None => dim("—", cx).into_any_element(), + })) + .child(cell(Some(9.), div().flex().justify_end().text_color(tint).child(format_money(amount)))), + ); + } + let screen = cx.entity(); + let (prev, next) = (screen.clone(), screen); div() .flex() - .justify_between() - .child(div().flex().gap(rems(1.)).child(div().text_color(t.secondary).child(tx.transaction_date.clone())).child(tx.description.clone())) - .child(div().text_color(color).child(format_money(signed))) - }), - ); - - page.child(stats) - .child(div().flex().gap(rems(1.)).child(spending_panel).child(accounts)) - .child(recent) + .flex_col() + .gap(rems(0.5)) + .child(table) + .child(simple_pager( + pager, + move |_, _, cx| prev.update(cx, |s, cx| { s.page = s.page.saturating_sub(1).max(1); cx.notify(); }), + move |_, _, cx| next.update(cx, |s, cx| { s.page += 1; cx.notify(); }), + cx, + )) + .into_any_element() + }; + + let recent = div() + .flex() + .flex_col() + .gap(rems(0.5)) + .child(div().font_weight(gpui_kit::FontWeight::BOLD).child("Recent Transactions")) + .child( + div() + .flex() + .gap(rems(0.5)) + .child(div().w(rems(13.)).child(select("overview-kind", &self.kind, window, cx))) + .child(div().w(rems(13.)).child(select("overview-account", &self.account, window, cx))), + ) + .child(list); + + let welcome = nothing_at_all.then(|| { + empty_state("Welcome to Chelete", "Press Ctrl+K to open the command palette and add your first account.", cx) + }); + + page.child(header) + .child(filter_row) + .child(stats) + .children(accounts) + .child(breakdown) + .child(recent) + .children(welcome) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ui::test_support::{empty_db, init, seeded_db}; + use chelete_lib::date_filter::DateFilter; + use gpui_kit::{TestAppContext, VisualTestContext}; + + fn setup(cx: &mut TestAppContext, db: Arc) -> (Entity, &mut VisualTestContext) { + init(cx); + cx.add_window_view(move |window, cx| OverviewScreen::new(db, window, cx)) + } + + fn pick(choice: &Entity, index: usize, cx: &mut VisualTestContext) { + cx.update(|_, cx| choice.update(cx, |c, cx| c.set_selected(Some(index), cx))); + } + + #[gpui_kit::test] + fn loads_totals_from_seeded_data(cx: &mut TestAppContext) { + let (screen, cx) = setup(cx, seeded_db()); + cx.update(|_, cx| { + let s = screen.read(cx); + let o = s.overview().expect("loads"); + assert_eq!(o.accounts.len(), 4); + assert_eq!(s.visible_transactions(cx).len(), 25, "All Time shows everything"); + let (start, end) = s.bounds(cx); + let summary = summarize(&s.transactions, &s.categories, &start, &end); + let sum = |kind: &str| s.transactions.iter().filter(|t| t.transaction_type == kind).map(|t| t.amount).sum::(); + // The summary follows the chosen range (all time here); the backend's own + // income/expense figures are current-month only and are not shown. + assert_eq!(summary.total_income, sum("income")); + assert_eq!(summary.total_expenses, sum("expense")); + let balance: i64 = s.accounts.iter().map(|a| a.balance).sum(); + assert_eq!(o.total_balance, balance, "total balance comes from the accounts"); + }); + } + + #[gpui_kit::test] + fn type_and_account_filters_narrow_the_table(cx: &mut TestAppContext) { + let (screen, cx) = setup(cx, seeded_db()); + let (kind, account) = cx.update(|_, cx| (screen.read(cx).kind.clone(), screen.read(cx).account.clone())); + pick(&kind, 1, cx); // Income + cx.update(|_, cx| { + let s = screen.read(cx); + let shown = s.visible_transactions(cx); + assert!(!shown.is_empty() && shown.iter().all(|t| t.transaction_type == "income")); + }); + pick(&kind, 0, cx); + pick(&account, 1, cx); // first real account + cx.update(|_, cx| { + let s = screen.read(cx); + let wanted = s.accounts[0].id.clone(); + let shown = s.visible_transactions(cx); + assert!(!shown.is_empty() && shown.iter().all(|t| t.account_id == wanted)); + }); + } + + #[gpui_kit::test] + fn date_filter_changes_the_summary_window(cx: &mut TestAppContext) { + let (screen, cx) = setup(cx, seeded_db()); + let preset = cx.update(|_, cx| screen.read(cx).date_filter.preset.clone()); + let index = DateFilter::ALL_FILTERS.iter().position(|f| *f == DateFilter::Today).unwrap(); + cx.update(|_, cx| screen.update(cx, |s, cx| { s.page = 2; cx.notify(); })); + pick(&preset, index, cx); + cx.update(|_, cx| { + let s = screen.read(cx); + assert_eq!(s.page, 1); + let today = chrono::Local::now().date_naive().format("%Y-%m-%d").to_string(); + assert!(s.visible_transactions(cx).iter().all(|t| t.transaction_date == today)); + }); + } + + #[gpui_kit::test] + fn pages_hold_ten_rows(cx: &mut TestAppContext) { + let (screen, cx) = setup(cx, seeded_db()); + cx.update(|_, cx| { + let s = screen.read(cx); + let pager = Pager::new(1, PER_PAGE, s.visible_transactions(cx).len()); + assert_eq!(pager.total_pages(), 3); + assert_eq!(pager.label(), "1–10 of 25"); + }); + } + + #[gpui_kit::test] + fn reload_picks_up_new_accounts_and_keeps_the_selection(cx: &mut TestAppContext) { + let db = seeded_db(); + let (screen, cx) = setup(cx, db.clone()); + let account = cx.update(|_, cx| screen.read(cx).account.clone()); + pick(&account, 2, cx); + let chosen = cx.update(|_, cx| choice_value(&screen.read(cx).account, cx)); + commands::create_account( + &db, + commands::CreateAccountRequest { name: "New".into(), account_type: "cash".into(), currency: "USD".into(), balance: 0, color: None, icon: None }, + ) + .unwrap(); + cx.update(|window, cx| screen.update(cx, |s, cx| s.reload(window, cx))); + cx.update(|_, cx| { + let s = screen.read(cx); + assert_eq!(s.accounts.len(), 5); + assert_eq!(choice_value(&s.account, cx), chosen, "selection survives the rebuild"); + }); + } + + #[gpui_kit::test] + fn an_empty_database_shows_the_welcome_state_without_errors(cx: &mut TestAppContext) { + let (screen, cx) = setup(cx, empty_db()); + cx.update(|_, cx| { + let s = screen.read(cx); + assert!(s.overview().is_ok()); + assert!(s.accounts.is_empty() && s.transactions.is_empty()); + assert!(s.visible_transactions(cx).is_empty()); + }); + } } diff --git a/src/ui/placeholder.rs b/src/ui/placeholder.rs deleted file mode 100644 index 4d6ff57..0000000 --- a/src/ui/placeholder.rs +++ /dev/null @@ -1,15 +0,0 @@ -use super::View; -use gpui_kit::{div, prelude::*, rems, App, IntoElement}; -use gpui_omarchy::{empty_state, panel, ActiveTheme as _}; - -/// Stand-in for screens that have not been ported from the web UI yet. -pub fn placeholder(view: View, cx: &App) -> impl IntoElement { - let t = cx.omarchy(); - div().size_full().p(rems(1.5)).child( - panel(view.label(), cx).child( - div() - .text_color(t.secondary) - .child(empty_state("Coming soon", "This screen has not been ported yet.", cx)), - ), - ) -} diff --git a/src/ui/shell.rs b/src/ui/shell.rs index 7ba0799..8af41d4 100644 --- a/src/ui/shell.rs +++ b/src/ui/shell.rs @@ -4,10 +4,9 @@ use super::projections::ProjectionsScreen; use super::settings; use super::subscriptions::SubscriptionsScreen; use super::transactions::TransactionsScreen; -use super::overview::{self, OverviewData}; +use super::overview::OverviewScreen; use super::widgets::ScreenEvent; use super::palette::{Command, Palette, PaletteEvent}; -use super::placeholder::placeholder; use super::sidebar::sidebar; use super::*; use chelete_lib::database::DbState; @@ -20,11 +19,10 @@ use gpui_omarchy::ActiveTheme as _; use std::sync::Arc; pub struct Shell { - db: Arc, view: View, prefs: Prefs, persist: bool, - overview: OverviewData, + overview: Entity, accounts: Entity, categories: Entity, subscriptions: Entity, @@ -56,8 +54,9 @@ impl Shell { let projections = cx.new(|cx| ProjectionsScreen::new(db.clone(), window, cx)); let transactions = cx.new(|cx| TransactionsScreen::new(db.clone(), window, cx)); cx.subscribe(&transactions, Self::on_screen_event).detach(); + let overview = cx.new(|cx| OverviewScreen::new(db.clone(), window, cx)); Self { - overview: OverviewData::load(&db), + overview, accounts, categories, subscriptions, @@ -65,7 +64,6 @@ impl Shell { transactions, toast: None, toast_seq: 0, - db, view: View::Overview, prefs, persist, @@ -87,7 +85,7 @@ impl Shell { self.close_palette(window, cx); self.view = view; match view { - View::Overview => self.overview = OverviewData::load(&self.db), + View::Overview => self.overview.update(cx, |s, cx| s.reload(window, cx)), View::Accounts => self.accounts.update(cx, |s, cx| s.reload(cx)), View::Categories => self.categories.update(cx, |s, cx| s.reload(cx)), View::Subscriptions => self.subscriptions.update(cx, |s, cx| s.reload(cx)), @@ -201,14 +199,13 @@ impl Render for Shell { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let t = cx.omarchy().clone(); let content = match self.view { - View::Overview => overview::render(&self.overview, cx).into_any_element(), + View::Overview => self.overview.clone().into_any_element(), View::Accounts => self.accounts.clone().into_any_element(), View::Categories => self.categories.clone().into_any_element(), View::Subscriptions => self.subscriptions.clone().into_any_element(), View::Projections => self.projections.clone().into_any_element(), View::Transactions => self.transactions.clone().into_any_element(), View::Settings => settings::render(cx).into_any_element(), - other => placeholder(other, cx).into_any_element(), }; div() .key_context(SHELL_CONTEXT) @@ -271,7 +268,7 @@ mod tests { cx.update(|_, cx| { let shell = shell.read(cx); assert_eq!(shell.view(), View::Overview); - let overview = shell.overview.result.as_ref().expect("overview loads"); + let overview = shell.overview.read(cx).overview().expect("overview loads"); assert_eq!(overview.accounts.len(), 4); }); } @@ -346,4 +343,39 @@ mod tests { assert!(accounts.read(cx).has_dialog(), "add form is open"); }); } + + #[gpui_kit::test] + fn toasts_show_then_clear_themselves(cx: &mut TestAppContext) { + let (shell, cx) = setup(cx); + let accounts = cx.update(|_, cx| shell.read(cx).accounts.clone()); + cx.update(|_, cx| accounts.update(cx, |_, cx| cx.emit(ScreenEvent::ok("Saved it")))); + cx.update(|_, cx| assert_eq!(shell.read(cx).toast_message(), Some("Saved it"))); + cx.executor().advance_clock(std::time::Duration::from_secs(4)); + cx.run_until_parked(); + cx.update(|_, cx| assert_eq!(shell.read(cx).toast_message(), None, "gone after three seconds")); + } + + #[gpui_kit::test] + fn a_newer_toast_is_not_cleared_by_an_older_timer(cx: &mut TestAppContext) { + let (shell, cx) = setup(cx); + let accounts = cx.update(|_, cx| shell.read(cx).accounts.clone()); + cx.update(|_, cx| accounts.update(cx, |_, cx| cx.emit(ScreenEvent::ok("first")))); + cx.executor().advance_clock(std::time::Duration::from_secs(2)); + cx.update(|_, cx| accounts.update(cx, |_, cx| cx.emit(ScreenEvent::error("second")))); + cx.executor().advance_clock(std::time::Duration::from_millis(1500)); + cx.run_until_parked(); + cx.update(|_, cx| assert_eq!(shell.read(cx).toast_message(), Some("second"), "first timer fired but must not clear the newer toast")); + } + + #[gpui_kit::test] + fn every_view_renders(cx: &mut TestAppContext) { + let (shell, cx) = setup(cx); + for view in View::ALL { + cx.update(|window, cx| shell.update(cx, |s, cx| s.navigate(view, window, cx))); + cx.update(|window, cx| { + window.draw(cx).clear(cx); + }); + assert_eq!(view_of(&shell, cx), view); + } + } } diff --git a/src/ui/subscriptions.rs b/src/ui/subscriptions.rs index 7f4a3b5..00a9273 100644 --- a/src/ui/subscriptions.rs +++ b/src/ui/subscriptions.rs @@ -13,7 +13,7 @@ use chelete_lib::subscriptions::{filter_by_frequency, frequency_label, next_paym use gpui_kit::base::input::InputState; use gpui_kit::component::date_picker::{DatePicker, DatePickerState}; use gpui_kit::{ - div, prelude::*, px, rems, Context, Div, Entity, EventEmitter, IntoElement, Render, + div, prelude::*, px, rems, Context, Entity, EventEmitter, IntoElement, Render, SharedString, Subscription as GpuiSubscription, Window, }; use gpui_omarchy::{ @@ -101,6 +101,7 @@ impl SubscriptionsScreen { cx.notify(); } + #[cfg(test)] pub fn subscriptions(&self) -> &[Subscription] { &self.subscriptions } diff --git a/src/ui/transactions.rs b/src/ui/transactions.rs index 2c2c544..afd3f93 100644 --- a/src/ui/transactions.rs +++ b/src/ui/transactions.rs @@ -100,6 +100,7 @@ impl TransactionsScreen { cx.notify(); } + #[cfg(test)] pub fn transactions(&self) -> &[Transaction] { &self.transactions } diff --git a/src/ui/widgets.rs b/src/ui/widgets.rs index 2fad714..b9331f2 100644 --- a/src/ui/widgets.rs +++ b/src/ui/widgets.rs @@ -318,3 +318,20 @@ pub fn cell(width: Option, child: impl IntoElement) -> Div { None => base.flex_1().child(child), } } + +/// Previous / range / Next, for lists with a fixed page size. +pub fn simple_pager( + pager: Pager, + on_prev: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + on_next: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + cx: &App, +) -> Div { + div() + .flex() + .items_center() + .justify_end() + .gap(rems(0.75)) + .child(button("pager-prev", "Previous", ButtonVariant::Outline, cx).disabled(!pager.has_prev()).on_click(on_prev)) + .child(dim(pager.label(), cx)) + .child(button("pager-next", "Next", ButtonVariant::Outline, cx).disabled(!pager.has_next()).on_click(on_next)) +} From 7b66426907d13e48aaf7ab1b57346e0aa3a3cc7b Mon Sep 17 00:00:00 2001 From: Victor Phos Date: Fri, 2 Oct 2026 14:10:18 +0200 Subject: [PATCH 26/36] Close dialogs with Escape and submit forms with Enter Every dialog handles Escape through a shared Modal key context, dialogs without text fields take focus themselves so Escape still reaches them, and pressing Enter in a form field submits the form. --- src/ui/accounts.rs | 41 ++++++++++++++++++++++++++--- src/ui/categories.rs | 5 +++- src/ui/csv_import.rs | 6 ++++- src/ui/mod.rs | 3 +++ src/ui/subscriptions.rs | 36 ++++++++++++++++++++++---- src/ui/transactions.rs | 48 ++++++++++++++++++++++++++-------- src/ui/widgets.rs | 57 +++++++++++++++++++++++++++++++++++++---- 7 files changed, 170 insertions(+), 26 deletions(-) diff --git a/src/ui/accounts.rs b/src/ui/accounts.rs index f2f1273..23e4253 100644 --- a/src/ui/accounts.rs +++ b/src/ui/accounts.rs @@ -167,6 +167,7 @@ pub struct AddAccountForm { name: Entity, kind: Entity, balance: Entity, + _subs: Vec, } impl EventEmitter for AddAccountForm {} @@ -175,11 +176,14 @@ impl AddAccountForm { fn new(db: Arc, window: &mut Window, cx: &mut Context) -> Self { let name = text_input("e.g. Main Bank", window, cx); name.update(cx, |s, cx| s.focus(window, cx)); + let balance = text_input("0.00", window, cx); + let _subs = submit_on_enter(&[&name, &balance], Self::submit, cx); Self { db, name, kind: choice(account_type_options(), Some("cash"), window, cx), - balance: text_input("0.00", window, cx), + balance, + _subs, } } @@ -235,7 +239,7 @@ impl Render for AddAccountForm { .on_click(cx.listener(|this, _, _, cx| this.submit(cx))), ), ); - modal("Add Account", body, cx.listener(|_, _, _, cx| cx.emit(FormEvent::Cancel)), cx) + modal("Add Account", body, cx.listener(|_, _, _, cx| cx.emit(FormEvent::Cancel)), cx).on_action(cx.listener(|_, _: &super::ModalCancel, _, cx| cx.emit(FormEvent::Cancel))) } } @@ -246,6 +250,7 @@ pub struct TransferForm { to: Entity, amount: Entity, notes: Entity, + _subs: Vec, } impl EventEmitter for TransferForm {} @@ -258,13 +263,16 @@ impl TransferForm { let second = accounts.get(1).or(accounts.first()).map(|a| a.id.clone()); let amount = text_input("0.00", window, cx); amount.update(cx, |s, cx| s.focus(window, cx)); + let notes = text_input("e.g. Monthly savings", window, cx); + let _subs = submit_on_enter(&[&amount, ¬es], Self::submit, cx); Self { db, from: choice(items.clone(), first.as_deref(), window, cx), to: choice(items, second.as_deref(), window, cx), amount, - notes: text_input("e.g. Monthly savings", window, cx), + notes, accounts, + _subs, } } @@ -325,7 +333,7 @@ impl Render for TransferForm { .on_click(cx.listener(|this, _, _, cx| this.submit(cx))), ), ); - modal("Transfer Money", body, cx.listener(|_, _, _, cx| cx.emit(FormEvent::Cancel)), cx) + modal("Transfer Money", body, cx.listener(|_, _, _, cx| cx.emit(FormEvent::Cancel)), cx).on_action(cx.listener(|_, _: &super::ModalCancel, _, cx| cx.emit(FormEvent::Cancel))) } } @@ -452,4 +460,29 @@ mod tests { assert!(accounts.iter().all(|a| a.id != id)); }); } + + #[gpui_kit::test] + fn escape_closes_the_open_form(cx: &mut TestAppContext) { + let (screen, cx, _) = setup(cx); + open_add(&screen, cx); + cx.update(|window, cx| window.draw(cx).clear(cx)); + cx.simulate_keystrokes("escape"); + cx.update(|_, cx| assert!(!screen.read(cx).has_dialog(), "escape dismissed the form")); + cx.update(|_, cx| assert_eq!(screen.read(cx).accounts().len(), 4, "nothing was saved")); + } + + #[gpui_kit::test] + fn enter_in_a_field_submits_the_form(cx: &mut TestAppContext) { + let (screen, cx, _) = setup(cx); + let form = open_add(&screen, cx); + let name = cx.update(|_, cx| form.read(cx).name.clone()); + type_into(&name, "Keyboard Only", cx); + cx.update(|window, cx| window.draw(cx).clear(cx)); + cx.simulate_keystrokes("enter"); + cx.update(|_, cx| { + let s = screen.read(cx); + assert!(!s.has_dialog()); + assert!(s.accounts().iter().any(|a| a.name == "Keyboard Only")); + }); + } } diff --git a/src/ui/categories.rs b/src/ui/categories.rs index b25c4c1..1435568 100644 --- a/src/ui/categories.rs +++ b/src/ui/categories.rs @@ -226,6 +226,7 @@ pub struct CategoryForm { name: Entity, kind: Entity, selected_icon: String, + _subs: Vec, } impl EventEmitter for CategoryForm {} @@ -238,6 +239,7 @@ impl CategoryForm { }; let name_input = text_input_with("e.g. Groceries", &name, window, cx); name_input.update(cx, |s, cx| s.focus(window, cx)); + let _subs = submit_on_enter(&[&name_input], Self::submit, cx); Self { db, editing: editing.map(|c| c.id), @@ -249,6 +251,7 @@ impl CategoryForm { cx, ), selected_icon: icon, + _subs, } } @@ -339,7 +342,7 @@ impl Render for CategoryForm { body, cx.listener(|_, _, _, cx| cx.emit(FormEvent::Cancel)), cx, - ) + ).on_action(cx.listener(|_, _: &super::ModalCancel, _, cx| cx.emit(FormEvent::Cancel))) } } diff --git a/src/ui/csv_import.rs b/src/ui/csv_import.rs index 0cf9602..985e081 100644 --- a/src/ui/csv_import.rs +++ b/src/ui/csv_import.rs @@ -83,6 +83,7 @@ pub struct CsvImportDialog { saved: SavedMappings, saved_path: PathBuf, syncing: bool, + focus: ModalFocus, _subs: Vec, } @@ -140,6 +141,7 @@ impl CsvImportDialog { accounts, categories, syncing: false, + focus: ModalFocus::new(cx), _subs: subs, } } @@ -489,6 +491,7 @@ impl CsvImportDialog { impl Render for CsvImportDialog { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + self.focus.ensure(window, cx); let error = self.error.clone().map(|e| alert(e, Status::Error, cx)); let stage = match self.stage { Stage::Mapping => self.mapping_stage(window, cx), @@ -502,7 +505,8 @@ impl Render for CsvImportDialog { body, cx.listener(|_, _, _, cx| cx.emit(CsvImportEvent::Cancel)), cx, - ) + ).track_focus(&self.focus.handle) + .on_action(cx.listener(|_, _: &super::ModalCancel, _, cx| cx.emit(CsvImportEvent::Cancel))) } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 1db363b..ecf428e 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -40,6 +40,7 @@ actions!( PaletteNext, PalettePrev, PaletteDismiss, + ModalCancel, ] ); @@ -106,6 +107,7 @@ impl View { /// so text inputs keep their own Ctrl+A / Ctrl+C / Ctrl+S behavior. pub const SHELL_CONTEXT: &str = "Shell"; pub const PALETTE_CONTEXT: &str = "Palette"; +pub const MODAL_CONTEXT: &str = "Modal"; pub fn install(cx: &mut App) { bind_keys(cx); @@ -128,5 +130,6 @@ pub fn bind_keys(cx: &mut App) { KeyBinding::new("down", PaletteNext, Some(PALETTE_CONTEXT)), KeyBinding::new("up", PalettePrev, Some(PALETTE_CONTEXT)), KeyBinding::new("escape", PaletteDismiss, Some(PALETTE_CONTEXT)), + KeyBinding::new("escape", ModalCancel, Some(MODAL_CONTEXT)), ]); } diff --git a/src/ui/subscriptions.rs b/src/ui/subscriptions.rs index 00a9273..4ec217c 100644 --- a/src/ui/subscriptions.rs +++ b/src/ui/subscriptions.rs @@ -40,6 +40,7 @@ pub struct SubscriptionsScreen { page: usize, selected: HashSet, dialog: Option, + view_focus: gpui_kit::FocusHandle, _dialog_sub: Option, _subs: Vec, } @@ -68,6 +69,7 @@ impl SubscriptionsScreen { page: 1, selected: HashSet::new(), dialog: None, + view_focus: cx.focus_handle(), _dialog_sub: None, _subs: subs, }; @@ -154,7 +156,7 @@ impl SubscriptionsScreen { return; } let noun = plural(ids.len(), "subscription"); - let dialog = cx.new(|_| { + let dialog = cx.new(|cx| { ConfirmDialog::new( format!("Delete {noun}?"), format!( @@ -162,6 +164,7 @@ impl SubscriptionsScreen { if ids.len() == 1 { "" } else { "s" } ), "Delete", + cx, ) }); self._dialog_sub = Some(cx.subscribe(&dialog, |this, _, event: &ConfirmEvent, cx| { @@ -351,7 +354,7 @@ impl Render for SubscriptionsScreen { .justify_end() .gap(rems(0.125)) .child(icon_button(key("view"), IconName::Eye, "View subscription", ButtonVariant::Secondary, cx) - .on_click(cx.listener(move |this, _, _, cx| { this.dialog = Some(Dialog::View(view.clone())); cx.notify(); }))) + .on_click(cx.listener(move |this, _, window, cx| { this.dialog = Some(Dialog::View(view.clone())); this.view_focus.focus(window, cx); cx.notify(); }))) .child(icon_button(key("edit"), IconName::Pencil, "Edit subscription", ButtonVariant::Secondary, cx) .on_click(cx.listener(move |this, _, window, cx| this.open_form(Some(edit.clone()), window, cx)))) .child(icon_button(key("pay"), IconName::CreditCard, "Record payment", ButtonVariant::Secondary, cx) @@ -433,7 +436,8 @@ impl SubscriptionsScreen { .on_click(cx.listener(|this, _, _, cx| this.close_dialog(cx))), ), ); - modal("Subscription", body, cx.listener(|this, _, _, cx| this.close_dialog(cx)), cx) + modal("Subscription", body, cx.listener(|this, _, _, cx| this.close_dialog(cx)), cx).track_focus(&self.view_focus) + .on_action(cx.listener(|this, _: &super::ModalCancel, _, cx| this.close_dialog(cx))) } } @@ -446,6 +450,7 @@ pub struct SubscriptionForm { category: Entity, account: Entity, start_date: Entity, + _subs: Vec, } impl EventEmitter for SubscriptionForm {} @@ -478,15 +483,18 @@ impl SubscriptionForm { }; let name_input = text_input_with("e.g. Netflix", &name, window, cx); name_input.update(cx, |s, cx| s.focus(window, cx)); + let amount_input = text_input_with("0.00", &amount, window, cx); + let _subs = submit_on_enter(&[&name_input, &amount_input], Self::submit, cx); Self { db, editing: editing.map(|s| s.id), name: name_input, - amount: text_input_with("0.00", &amount, window, cx), + amount: amount_input, frequency: choice(frequency_items, Some(&frequency), window, cx), category: choice(category_items, Some(&category), window, cx), account: choice(account_items, Some(&account), window, cx), start_date: date_field::single(start, window, cx), + _subs, } } @@ -573,7 +581,7 @@ impl Render for SubscriptionForm { body, cx.listener(|_, _, _, cx| cx.emit(FormEvent::Cancel)), cx, - ) + ).on_action(cx.listener(|_, _: &super::ModalCancel, _, cx| cx.emit(FormEvent::Cancel))) } } @@ -778,4 +786,22 @@ mod tests { cx.update(|window, cx| screen.update(cx, |s, cx| s.open_add(window, cx))); cx.update(|_, cx| assert!(screen.read(cx).has_dialog())); } + + #[gpui_kit::test] + fn escape_dismisses_the_delete_confirmation_without_deleting(cx: &mut TestAppContext) { + let (screen, db, cx) = setup(cx); + let sub = make(&db, "Keep me", 100, "monthly"); + reload(&screen, cx); + cx.update(|_, cx| screen.update(cx, |s, cx| s.ask_delete(vec![sub.id.clone()], cx))); + cx.update(|_, cx| assert!(screen.read(cx).has_dialog())); + // The dialog takes focus while drawing its first frame; a person presses + // a key some frames later. + cx.update(|window, cx| window.draw(cx).clear(cx)); + cx.update(|window, cx| window.draw(cx).clear(cx)); + cx.simulate_keystrokes("escape"); + cx.update(|_, cx| { + assert!(!screen.read(cx).has_dialog()); + assert_eq!(screen.read(cx).subscriptions().len(), 1); + }); + } } diff --git a/src/ui/transactions.rs b/src/ui/transactions.rs index afd3f93..0cdc486 100644 --- a/src/ui/transactions.rs +++ b/src/ui/transactions.rs @@ -45,6 +45,7 @@ pub struct TransactionsScreen { page: usize, selected: HashSet, dialog: Option, + view_focus: gpui_kit::FocusHandle, mappings_path: PathBuf, _dialog_sub: Option, _subs: Vec, @@ -79,6 +80,7 @@ impl TransactionsScreen { page: 1, selected: HashSet::new(), dialog: None, + view_focus: cx.focus_handle(), mappings_path: chelete_lib::import::session::SavedMappings::default_path(), _dialog_sub: None, _subs: subs, @@ -158,11 +160,12 @@ impl TransactionsScreen { } let noun = plural(ids.len(), "transaction"); let plural_s = if ids.len() == 1 { "" } else { "s" }; - let dialog = cx.new(|_| { + let dialog = cx.new(|cx| { ConfirmDialog::new( format!("Delete {noun}?"), format!("This will permanently remove the selected transaction{plural_s} and adjust account balances. This action cannot be undone."), "Delete", + cx, ) }); self._dialog_sub = Some(cx.subscribe(&dialog, |this, _, event: &ConfirmEvent, cx| { @@ -217,7 +220,7 @@ impl TransactionsScreen { Ok(d) => d, Err(e) => return cx.emit(ScreenEvent::error(format!("Could not read data: {e}"))), }; - let dialog = cx.new(|_| ExportDialog::new(data)); + let dialog = cx.new(|cx| ExportDialog::new(data, cx)); self._dialog_sub = Some(cx.subscribe(&dialog, |this, _, event: &ExportEvent, cx| { match event { ExportEvent::Cancel => {} @@ -380,7 +383,7 @@ impl Render for TransactionsScreen { .justify_end() .gap(rems(0.125)) .child(icon_button(key("view"), IconName::Eye, "View transaction", ButtonVariant::Secondary, cx) - .on_click(cx.listener(move |this, _, _, cx| { this.dialog = Some(Dialog::View(view.clone())); cx.notify(); }))) + .on_click(cx.listener(move |this, _, window, cx| { this.dialog = Some(Dialog::View(view.clone())); this.view_focus.focus(window, cx); cx.notify(); }))) .child(icon_button(key("edit"), IconName::Pencil, "Edit transaction", ButtonVariant::Secondary, cx) .on_click(cx.listener(move |this, _, window, cx| this.open_form(Some(edit.clone()), window, cx)))) .child(icon_button(key("del"), IconName::Trash, "Delete transaction", ButtonVariant::Danger, cx) @@ -459,7 +462,8 @@ impl TransactionsScreen { .on_click(cx.listener(|this, _, _, cx| this.close_dialog(cx))), ), ); - modal("Transaction", body, cx.listener(|this, _, _, cx| this.close_dialog(cx)), cx) + modal("Transaction", body, cx.listener(|this, _, _, cx| this.close_dialog(cx)), cx).track_focus(&self.view_focus) + .on_action(cx.listener(|this, _: &super::ModalCancel, _, cx| this.close_dialog(cx))) } } @@ -475,6 +479,7 @@ pub struct TransactionForm { account: Entity, category: Entity, date: Entity, + _subs: Vec, } impl EventEmitter for TransactionForm {} @@ -505,16 +510,19 @@ impl TransactionForm { .unwrap_or_else(|| chrono::Local::now().date_naive()); let amount_input = text_input_with("0.00", &amount, window, cx); amount_input.update(cx, |s, cx| s.focus(window, cx)); + let description_input = text_input_with("e.g. Groceries", &description, window, cx); + let _subs = submit_on_enter(&[&amount_input, &description_input], Self::submit, cx); Self { db, categories: categories.to_vec(), kind: kind.clone(), amount: amount_input, - description: text_input_with("e.g. Groceries", &description, window, cx), + description: description_input, account: choice(accounts.iter().map(|a| (a.id.clone(), a.name.clone())).collect(), account_id.as_deref(), window, cx), category: choice(category_items(categories, &kind), Some(&category_id), window, cx), date: date_field::single(Some(date), window, cx), editing, + _subs, } } @@ -626,7 +634,7 @@ impl Render for TransactionForm { body, cx.listener(|_, _, _, cx| cx.emit(FormEvent::Cancel)), cx, - ) + ).on_action(cx.listener(|_, _: &super::ModalCancel, _, cx| cx.emit(FormEvent::Cancel))) } } @@ -642,13 +650,14 @@ pub enum ExportEvent { pub struct ExportDialog { data: ExportData, format: ExportFormat, + focus: ModalFocus, } impl EventEmitter for ExportDialog {} impl ExportDialog { - fn new(data: ExportData) -> Self { - Self { data, format: ExportFormat::Csv } + fn new(data: ExportData, cx: &mut Context) -> Self { + Self { data, format: ExportFormat::Csv, focus: ModalFocus::new(cx) } } /// Render the chosen format and write it to `path`. @@ -676,7 +685,8 @@ impl ExportDialog { } impl Render for ExportDialog { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + self.focus.ensure(window, cx); let format_button = |id: &'static str, label: &'static str, format: ExportFormat, cx: &mut Context| { button(id, label, if self.format == format { ButtonVariant::Primary } else { ButtonVariant::Outline }, cx) .flex_1() @@ -705,7 +715,8 @@ impl Render for ExportDialog { .child(button("export-cancel", "Cancel", ButtonVariant::Outline, cx).on_click(cx.listener(|_, _, _, cx| cx.emit(ExportEvent::Cancel)))) .child(button("export-go", "Export", ButtonVariant::Primary, cx).on_click(cx.listener(|this, _, _, cx| this.choose_path_and_write(cx)))), ); - modal("Export Data", body, cx.listener(|_, _, _, cx| cx.emit(ExportEvent::Cancel)), cx) + modal("Export Data", body, cx.listener(|_, _, _, cx| cx.emit(ExportEvent::Cancel)), cx).track_focus(&self.focus.handle) + .on_action(cx.listener(|_, _: &super::ModalCancel, _, cx| cx.emit(ExportEvent::Cancel))) } } @@ -962,4 +973,21 @@ mod tests { let result = cx.update(|_, cx| dialog.read(cx).write_to(std::path::Path::new("/nonexistent-dir/x/out.csv"))); assert!(result.is_err()); } + + #[gpui_kit::test] + fn enter_in_the_description_field_saves_the_transaction(cx: &mut TestAppContext) { + let (screen, _db, cx) = setup(cx); + cx.update(|window, cx| screen.update(cx, |s, cx| s.open_add(window, cx))); + let form = form_of(&screen, cx); + let (amount, description) = cx.update(|_, cx| (form.read(cx).amount.clone(), form.read(cx).description.clone())); + type_into(&amount, "9.99", cx); + type_into(&description, "Enter key", cx); + cx.update(|window, cx| window.draw(cx).clear(cx)); + cx.simulate_keystrokes("enter"); + cx.update(|_, cx| { + let s = screen.read(cx); + assert!(!s.has_dialog()); + assert!(s.transactions().iter().any(|t| t.description == "Enter key" && t.amount == 999)); + }); + } } diff --git a/src/ui/widgets.rs b/src/ui/widgets.rs index b9331f2..e478040 100644 --- a/src/ui/widgets.rs +++ b/src/ui/widgets.rs @@ -70,6 +70,7 @@ pub fn modal_sized( let t = cx.omarchy(); div() .id("modal-backdrop") + .key_context(super::MODAL_CONTEXT) .absolute() .top_0() .left_0() @@ -263,18 +264,24 @@ pub struct ConfirmDialog { title: SharedString, message: SharedString, confirm_label: SharedString, + focus: ModalFocus, } impl EventEmitter for ConfirmDialog {} - impl ConfirmDialog { - pub fn new(title: impl Into, message: impl Into, confirm_label: impl Into) -> Self { - Self { title: title.into(), message: message.into(), confirm_label: confirm_label.into() } + pub fn new( + title: impl Into, + message: impl Into, + confirm_label: impl Into, + cx: &mut Context, + ) -> Self { + Self { title: title.into(), message: message.into(), confirm_label: confirm_label.into(), focus: ModalFocus::new(cx) } } } impl Render for ConfirmDialog { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + self.focus.ensure(window, cx); let body = div() .flex() .flex_col() @@ -294,7 +301,8 @@ impl Render for ConfirmDialog { .on_click(cx.listener(|_, _, _, cx| cx.emit(ConfirmEvent::Confirm))), ), ); - modal(&self.title, body, cx.listener(|_, _, _, cx| cx.emit(ConfirmEvent::Cancel)), cx) + modal(&self.title, body, cx.listener(|_, _, _, cx| cx.emit(ConfirmEvent::Cancel)), cx).track_focus(&self.focus.handle) + .on_action(cx.listener(|_, _: &super::ModalCancel, _, cx| cx.emit(ConfirmEvent::Cancel))) } } @@ -335,3 +343,42 @@ pub fn simple_pager( .child(dim(pager.label(), cx)) .child(button("pager-next", "Next", ButtonVariant::Outline, cx).disabled(!pager.has_next()).on_click(on_next)) } + +/// Keyboard focus for a dialog that has no text field of its own, so Escape +/// still reaches it. Call [`ModalFocus::ensure`] from `render`. +pub struct ModalFocus { + pub handle: gpui_kit::FocusHandle, + taken: bool, +} + +impl ModalFocus { + pub fn new(cx: &mut Context) -> Self { + Self { handle: cx.focus_handle(), taken: false } + } + + pub fn ensure(&mut self, window: &mut Window, cx: &mut Context) { + if !self.taken { + self.taken = true; + self.handle.focus(window, cx); + } + } +} + +/// Run `submit` when Enter is pressed in any of `inputs`. +pub fn submit_on_enter( + inputs: &[&Entity], + submit: fn(&mut V, &mut Context), + cx: &mut Context, +) -> Vec { + use gpui_kit::base::input::InputEvent; + inputs + .iter() + .map(|input| { + cx.subscribe(*input, move |this, _, event: &InputEvent, cx| { + if matches!(event, InputEvent::PressEnter { .. }) { + submit(this, cx); + } + }) + }) + .collect() +} From de857f97c3e526242451192119fdeaadb77f89f2 Mon Sep 17 00:00:00 2001 From: Victor Phos Date: Fri, 2 Oct 2026 14:10:50 +0200 Subject: [PATCH 27/36] Give the window a title, app id and minimum size The app id ("chelete") is what window rules and desktop entries match on; without it the window had an empty class. --- src/main.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index e00927d..a241c7c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ mod ui; use chelete_lib::database::{self, DbState}; use chelete_lib::prefs::Prefs; -use gpui_kit::{px, size, AppContext as _, Bounds, WindowBounds, WindowOptions}; +use gpui_kit::{px, size, AppContext as _, Bounds, SharedString, TitlebarOptions, WindowBounds, WindowOptions}; use std::sync::Arc; fn main() { @@ -30,6 +30,13 @@ fn main() { size(px(1100.), px(700.)), cx, ))), + titlebar: Some(TitlebarOptions { + title: Some(SharedString::from("Chelete")), + ..Default::default() + }), + // Lets window rules and the desktop entry find the app (class "chelete"). + app_id: Some("chelete".into()), + window_min_size: Some(size(px(760.), px(520.))), ..Default::default() }; gpui_kit::open_window(options, cx, |window, cx| { From fdf4a47f3a8d0b994d3acf36d6a25ccdb61e6417 Mon Sep 17 00:00:00 2001 From: Victor Phos Date: Fri, 2 Oct 2026 14:10:50 +0200 Subject: [PATCH 28/36] Move CI, release packaging and tooling to the Rust build Run cargo test in CI, build release tarball, AppImage and .deb from the cargo binary, read the version from Cargo.toml in the bump script, install the GPUI system libraries in the Dockerfile, and package the AUR entry from the release tarball. Adds a desktop entry matching the window class. --- .github/workflows/ci.yml | 28 +++++++++++ .github/workflows/release.yml | 92 +++++++++++++++-------------------- Dockerfile.build | 6 +-- assets/chelete.desktop | 8 +++ aur/PKGBUILD | 22 ++++----- scripts/bump-version.sh | 17 +++---- 6 files changed, 92 insertions(+), 81 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 assets/chelete.desktop diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0a3cb25 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential clang cmake pkg-config \ + libxkbcommon-dev libxkbcommon-x11-dev libwayland-dev \ + libx11-xcb-dev libxcb1-dev libfontconfig1-dev libvulkan-dev libssl-dev + + - uses: Swatinem/rust-cache@v2 + + - name: Test + run: cargo test --locked diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c4801a5..29727d7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,11 +13,6 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: "22" - - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable @@ -25,80 +20,68 @@ jobs: run: | sudo apt-get update sudo apt-get install -y \ - libgtk-3-dev \ - libwebkit2gtk-4.1-dev \ - libappindicator3-dev \ - librsvg2-dev \ - patchelf \ - libssl-dev + build-essential clang cmake pkg-config \ + libxkbcommon-dev libxkbcommon-x11-dev libwayland-dev \ + libx11-xcb-dev libxcb1-dev libfontconfig1-dev libvulkan-dev libssl-dev \ + patchelf - - name: Install frontend dependencies - run: npm ci + - uses: Swatinem/rust-cache@v2 - - name: Build frontend - run: npm run build + - name: Test + run: cargo test --locked - - name: Build Rust backend - working-directory: src-tauri - run: cargo build --release + - name: Build + run: cargo build --release --locked --bin chelete - name: Get version from tag id: version run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT + - name: Create tarball + run: | + NAME="chelete_${{ steps.version.outputs.VERSION }}_linux_x86_64" + mkdir -p "dist/$NAME" + cp target/release/chelete "dist/$NAME/" + cp assets/chelete.desktop "dist/$NAME/" + cp assets/icons/128x128.png "dist/$NAME/chelete.png" + cp LICENSE README.md "dist/$NAME/" + tar -C dist -czf "dist/$NAME.tar.gz" "$NAME" + - name: Create AppImage - working-directory: src-tauri/target/release + working-directory: dist run: | - mkdir -p AppDir/usr/bin - mkdir -p AppDir/usr/share/applications - mkdir -p AppDir/usr/share/icons/hicolor/256x256/apps - cp chelete AppDir/usr/bin/ - cp ../../icons/128x128.png AppDir/chelete.png - cp ../../icons/128x128.png AppDir/usr/share/icons/hicolor/256x256/apps/chelete.png - cat > AppDir/chelete.desktop << EOF - [Desktop Entry] - Name=Chelete - Exec=chelete - Icon=chelete - Type=Application - Categories=Office;Finance; - EOF - cp AppDir/chelete.desktop AppDir/usr/share/applications/chelete.desktop + mkdir -p AppDir/usr/bin AppDir/usr/share/applications AppDir/usr/share/icons/hicolor/256x256/apps + cp ../target/release/chelete AppDir/usr/bin/ + cp ../assets/icons/128x128.png AppDir/chelete.png + cp ../assets/icons/128x128.png AppDir/usr/share/icons/hicolor/256x256/apps/chelete.png + cp ../assets/chelete.desktop AppDir/chelete.desktop + cp ../assets/chelete.desktop AppDir/usr/share/applications/chelete.desktop cat > AppDir/AppRun << 'APPEOF' #!/bin/sh HERE="$(dirname "$(readlink -f "${0}")")" exec "${HERE}/usr/bin/chelete" "$@" APPEOF - chmod +x AppDir/AppRun - chmod +x AppDir/usr/bin/chelete + chmod +x AppDir/AppRun AppDir/usr/bin/chelete wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage chmod +x linuxdeploy-x86_64.AppImage ./linuxdeploy-x86_64.AppImage --appdir AppDir --output appimage - name: Package .deb - working-directory: src-tauri/target/release + working-directory: dist run: | DEB_DIR="chelete_${{ steps.version.outputs.VERSION }}_amd64" - mkdir -p "$DEB_DIR/DEBIAN" - mkdir -p "$DEB_DIR/usr/bin" - mkdir -p "$DEB_DIR/usr/share/applications" - mkdir -p "$DEB_DIR/usr/share/icons/hicolor/256x256/apps" - cp chelete "$DEB_DIR/usr/bin/" - cp ../../icons/128x128.png "$DEB_DIR/usr/share/icons/hicolor/256x256/apps/chelete.png" - cat > "$DEB_DIR/chelete.desktop" << EOF - [Desktop Entry] - Name=Chelete - Exec=chelete - Icon=chelete - Type=Application - Categories=Finance; - EOF + mkdir -p "$DEB_DIR/DEBIAN" "$DEB_DIR/usr/bin" "$DEB_DIR/usr/share/applications" "$DEB_DIR/usr/share/icons/hicolor/256x256/apps" + cp ../target/release/chelete "$DEB_DIR/usr/bin/" + cp ../assets/chelete.desktop "$DEB_DIR/usr/share/applications/" + cp ../assets/icons/128x128.png "$DEB_DIR/usr/share/icons/hicolor/256x256/apps/chelete.png" cat > "$DEB_DIR/DEBIAN/control" << EOF Package: chelete Version: ${{ steps.version.outputs.VERSION }} Architecture: amd64 - Depends: libgtk-3-0, libwebkit2gtk-4.1-0, libappindicator3-1 + Maintainer: Victor Phos + Depends: libvulkan1, libxkbcommon0, libxkbcommon-x11-0, libwayland-client0, libfontconfig1 Description: Personal finance tracker + A fast, keyboard-driven personal finance tracker built for Omarchy. EOF dpkg-deb --build "$DEB_DIR" @@ -107,5 +90,6 @@ jobs: with: generate_release_notes: true files: | - src-tauri/target/release/*.deb - src-tauri/target/release/*.AppImage + dist/*.tar.gz + dist/*.deb + dist/*.AppImage diff --git a/Dockerfile.build b/Dockerfile.build index 52ae3be..ab8b40e 100644 --- a/Dockerfile.build +++ b/Dockerfile.build @@ -1,9 +1,9 @@ FROM rust:1.98 RUN apt-get update && apt-get install -y \ - gcc pkg-config libssl-dev \ - libgtk-3-dev libwebkit2gtk-4.1-dev \ - libappindicator3-dev librsvg2-dev patchelf \ + build-essential clang cmake pkg-config \ + libxkbcommon-dev libxkbcommon-x11-dev libwayland-dev \ + libx11-xcb-dev libxcb1-dev libfontconfig1-dev libvulkan-dev libssl-dev \ && rm -rf /var/lib/apt/lists/* WORKDIR /app diff --git a/assets/chelete.desktop b/assets/chelete.desktop new file mode 100644 index 0000000..be22d40 --- /dev/null +++ b/assets/chelete.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Name=Chelete +Comment=Keyboard-driven personal finance tracker +Exec=chelete +Icon=chelete +Type=Application +Categories=Office;Finance; +StartupWMClass=chelete diff --git a/aur/PKGBUILD b/aur/PKGBUILD index 664a407..3458c16 100644 --- a/aur/PKGBUILD +++ b/aur/PKGBUILD @@ -1,27 +1,23 @@ # Maintainer: phoscoder pkgname=chelete-bin -pkgver=0.1.0 +pkgver=0.6.0 pkgrel=1 -pkgdesc='Personal finance tracker' +pkgdesc='Keyboard-driven personal finance tracker for Omarchy' arch=('x86_64') url='https://github.com/phoscoder/chelete' license=('MIT') -depends=('gtk3' 'webkit2gtk' 'libappindicator-gtk3' 'librsvg') +depends=('vulkan-icd-loader' 'libxkbcommon' 'libxkbcommon-x11' 'wayland' 'fontconfig' 'libx11' 'libxcb') provides=('chelete') conflicts=('chelete') -source=("${url}/releases/download/v${pkgver}/chelete_${pkgver}_amd64.deb") +source=("${url}/releases/download/v${pkgver}/chelete_${pkgver}_linux_x86_64.tar.gz") sha256sums=('SKIP') package() { - # Extract the deb package - bsdtar -xf "${srcdir}/chelete_${pkgver}_amd64.deb" -C "${pkgdir}" - - # Move files to correct locations - mv "${pkgdir}/usr/bin/"* "${pkgdir}/usr/bin/" 2>/dev/null || true - mv "${pkgdir}/usr/share/"* "${pkgdir}/usr/share/" 2>/dev/null || true - - # Remove the DEBIAN directory (not needed after extraction) - rm -rf "${pkgdir}/DEBIAN" + cd "${srcdir}/chelete_${pkgver}_linux_x86_64" + install -Dm755 chelete "${pkgdir}/usr/bin/chelete" + install -Dm644 chelete.desktop "${pkgdir}/usr/share/applications/chelete.desktop" + install -Dm644 chelete.png "${pkgdir}/usr/share/icons/hicolor/256x256/apps/chelete.png" + install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" } diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh index 9e54cf5..357578c 100755 --- a/scripts/bump-version.sh +++ b/scripts/bump-version.sh @@ -8,8 +8,8 @@ if [[ "$VERSION_BUMP" != "patch" && "$VERSION_BUMP" != "minor" && "$VERSION_BUMP exit 1 fi -# Get current version from package.json -CURRENT=$(node -p "require('./package.json').version") +# Current version from the package table in Cargo.toml. +CURRENT=$(grep -m1 '^version = ' Cargo.toml | sed -E 's/version = "([^"]+)"/\1/') IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT" case "$VERSION_BUMP" in @@ -30,17 +30,12 @@ esac NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}" echo "Bumping version: $CURRENT -> $NEW_VERSION" -# Update package.json -sed -i "s/\"version\": \"$CURRENT\"/\"version\": \"$NEW_VERSION\"/" package.json - -# Update tauri.conf.json -sed -i "s/\"version\": \"$CURRENT\"/\"version\": \"$NEW_VERSION\"/" src-tauri/tauri.conf.json - -# Update Cargo.toml -sed -i "s/^version = \"$CURRENT\"/version = \"$NEW_VERSION\"/" src-tauri/Cargo.toml +# Update Cargo.toml (first match only: the package version) and the lockfile entry. +sed -i "0,/^version = \"$CURRENT\"/s//version = \"$NEW_VERSION\"/" Cargo.toml +cargo update --offline --workspace >/dev/null 2>&1 || cargo update --workspace >/dev/null # Create git commit and tag -git add package.json src-tauri/tauri.conf.json src-tauri/Cargo.toml +git add Cargo.toml Cargo.lock git commit -m "chore: release v${NEW_VERSION}" git tag -a "v${NEW_VERSION}" -m "Release v${NEW_VERSION}" From 50082989bb7e34deb4e7ac3dbf300f441f77185d Mon Sep 17 00:00:00 2001 From: Victor Phos Date: Fri, 2 Oct 2026 14:10:51 +0200 Subject: [PATCH 29/36] Rewrite the README for the Rust build Document the dependencies, make targets, data location and keyboard shortcuts in place of the Tauri and npm instructions. --- README.md | 40 ++++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index dd04736..2cab523 100644 --- a/README.md +++ b/README.md @@ -36,23 +36,35 @@ Chelete adapts to whichever theme is set in Omarchy. Switch your system theme an ## Tech Stack -- [Tauri](https://tauri.app/) — native desktop shell -- [React](https://react.dev/) — UI framework -- [TypeScript](https://www.typescriptlang.org/) — type safety -- [SQLite](https://www.sqlite.org/) — local database -- [Lucide](https://lucide.dev/) — icons +- [Rust](https://www.rust-lang.org/) — the whole app, UI included +- [GPUI](https://www.gpui.rs/) through [gpui-kit](https://gpui-kit.com/) — GPU-rendered native UI +- [gpui-omarchy](https://github.com/huacnlee/gpui-omarchy) — Omarchy-styled controls and live theme following +- [SQLite](https://www.sqlite.org/) (rusqlite) — local database +- [Lucide](https://lucide.dev/) — icons, from the gpui-kit asset bundle + +## Requirements + +Build dependencies on Linux: a Rust toolchain, `clang`, `cmake`, `pkg-config`, and the development packages for Wayland, xkbcommon, fontconfig and Vulkan. + +On Arch / Omarchy: + +```bash +sudo pacman -S --needed rust clang cmake pkgconf wayland libxkbcommon libxkbcommon-x11 fontconfig vulkan-icd-loader +``` ## Development ```bash -npm install -npm run tauri dev +make dev # cargo run +make test # cargo test (library and headless UI tests) ``` +The UI tests run the real screens headlessly with simulated keystrokes, so they need no display. + ## Build ```bash -npm run tauri build +make build # cargo build --release ``` ## Usage @@ -66,6 +78,18 @@ make release-major # Bump 0.1.0 -> 1.0.0 + git tag git push origin main --tags # Triggers GitHub Actions release ``` +Data lives in `~/.local/share/com.chelete.app/` (the same place earlier versions used, so existing data carries over). + +### Keyboard + +| Keys | Action | +|:-----|:-------| +| `Ctrl+O` `T` `A` `C` `U` `P` `S` | Overview, Transactions, Accounts, Categories, Subscriptions, Projections, Settings | +| `Ctrl+K` | Command palette | +| `Ctrl+B` | Collapse the sidebar | +| `Esc` / `Enter` | Close a dialog / submit a form | +| `Ctrl+Q` | Quit | + ## Special Thanks - **Victor Phos** — The Architect & QA From 5c79c38b053ededefca7e061504237c82f5b5271 Mon Sep 17 00:00:00 2001 From: Victor Phos Date: Fri, 2 Oct 2026 14:10:51 +0200 Subject: [PATCH 30/36] Track Cargo.lock This is an application with pinned, fast-moving GPUI dependencies, so builds should be reproducible; CI builds with --locked. --- .gitignore | 1 - Cargo.lock | 9153 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 9153 insertions(+), 1 deletion(-) create mode 100644 Cargo.lock diff --git a/.gitignore b/.gitignore index 931aa3b..b8e7488 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ node_modules/ # Build dist/ target/ -*.lock *.tsbuildinfo # Environment diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..396f03d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,9153 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "accesskit" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b7f7f85a7e5f68090000ed7622545829afd484d210358702ae4cb97dd0c320" +dependencies = [ + "enumn", + "uuid", +] + +[[package]] +name = "accesskit_atspi_common" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023da0e5097f46df7092d5280b02efb9bbf8d93298daeced42652463e357d636" +dependencies = [ + "accesskit", + "accesskit_consumer", + "atspi-common", + "phf 0.13.1", + "serde", + "zvariant", +] + +[[package]] +name = "accesskit_consumer" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d10a236f96f87d70732e44520046785431ef01d5bcd6b041317bfadd2f88245" +dependencies = [ + "accesskit", + "hashbrown 0.16.1", +] + +[[package]] +name = "accesskit_macos" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce02dc63b43f0c9296af9ac946312a2dc8814427d7a64d2d600971dac55b6076" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.16.1", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "accesskit_unix" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e156ed3802e35eefe894ef2671bc6c889303d8a7e110b5e1b48f504b91362f" +dependencies = [ + "accesskit", + "accesskit_atspi_common", + "async-channel", + "async-executor", + "async-task", + "atspi", + "futures-lite", + "futures-util", + "serde", + "zbus", +] + +[[package]] +name = "accesskit_windows" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "106c2b961215864d1c2e703ee63269c25c4e80a577ffb2c1017b9c17dcdf83a1" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.16.1", + "static_assertions", + "windows 0.62.2", + "windows-core 0.62.2", +] + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", + "zeroize", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "const-random", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "annotate-snippets" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f211a51805bc641f3ad5b7664c77d2547af685cc33b4cd8d31964027a46f13f1" +dependencies = [ + "anstyle", + "memchr", + "unicode-width", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + +[[package]] +name = "ashpd" +version = "0.13.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb8421aaa9644a5faf26735f258b669b15f063313ef8f8e2bdb28912a1a6f111" +dependencies = [ + "enumflags2", + "futures-channel", + "futures-util", + "getrandom 0.4.3", + "serde", + "serde_repr", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "zbus", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-compression" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee19bd99b43e3691acbad4e840420a4881cea6c0b66a208125a824f8fd53f5a1" +dependencies = [ + "compression-codecs", + "compression-core", + "futures-io", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "atomic" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atspi" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77886257be21c9cd89a4ae7e64860c6f0eefca799bb79127913052bd0eefb3d" +dependencies = [ + "atspi-common", + "atspi-proxies", +] + +[[package]] +name = "atspi-common" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c5617155740c98003016429ad13fe43ce7a77b007479350a9f8bf95a29f63d" +dependencies = [ + "enumflags2", + "serde", + "static_assertions", + "zbus", + "zbus-lockstep", + "zbus-lockstep-macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "atspi-proxies" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2230e48787ed3eb4088996eab66a32ca20c0b67bbd4fd6cdfe79f04f1f04c9fc" +dependencies = [ + "atspi-common", + "serde", + "zbus", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.21", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom 8.0.0", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide 0.8.9", + "object", + "rustc-demangle", + "windows-link 0.2.1", +] + +[[package]] +name = "base62" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3a647dce9631f152a99ec16cfc09267ae55fe280d5dc288964027d458076323" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.13.2", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 2.1.3", + "shlex 1.3.0", + "syn 2.0.119", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec 0.8.0", +] + +[[package]] +name = "bit-set" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ddef2995421ab6a5c779542c81ee77c115206f4ad9d5a8e05f4ff49716a3dd" +dependencies = [ + "bit-vec 0.9.1", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "bitstream-io" +version = "4.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" +dependencies = [ + "no_std_io2", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.4", +] + +[[package]] +name = "blocking" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "borsh" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "553c5d846a6ba5150c65e3b1b8ec073bcf1abc20f9b7220de384a4443ea4e20a" +dependencies = [ + "bytes", + "cfg_aliases", +] + +[[package]] +name = "bstr" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" +dependencies = [ + "memchr", + "serde_core", +] + +[[package]] +name = "built" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "bzip2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" +dependencies = [ + "libbz2-rs-sys", +] + +[[package]] +name = "calloop" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b32313e937c8721c9d17ee9bf60699a6402b4b7c2cb6ac1bee0ffcaa4b4e5e" +dependencies = [ + "bitflags 2.13.2", + "polling", + "rustix", + "tracing", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +dependencies = [ + "calloop", + "rustix", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cbindgen" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadd868a2ce9ca38de7eeafdcec9c7065ef89b42b32f0839278d55f35c54d1ff" +dependencies = [ + "heck 0.4.1", + "indexmap", + "log", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn 2.0.119", + "tempfile", + "toml 0.8.23", +] + +[[package]] +name = "cc" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex 2.0.1", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom 7.1.3", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "cgl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" +dependencies = [ + "libc", +] + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core 0.10.1", +] + +[[package]] +name = "chelete" +version = "0.6.0" +dependencies = [ + "chrono", + "csv", + "dirs 5.0.1", + "gpui-kit", + "gpui-omarchy", + "rusqlite", + "serde", + "serde_json", + "toml 0.8.23", + "uuid", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout", + "zeroize", +] + +[[package]] +name = "clang-sys" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation 0.1.2", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad36507aeb7e16159dfe68db81ccc27571c3ccd4b76fb2fb72fc59e7a4b1b64c" +dependencies = [ + "bitflags 2.13.2", + "block", + "cocoa-foundation 0.2.1", + "core-foundation 0.10.1", + "core-graphics 0.24.0", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81411967c50ee9a1fc11365f8c585f863a22a9697c89239c452292c40ba79b0d" +dependencies = [ + "bitflags 2.13.2", + "block", + "core-foundation 0.10.1", + "core-graphics-types 0.2.0", + "objc", +] + +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "compression-codecs" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98fc98460ba0ad5317075d3632b8dfc45d0be8c4a49347c2a38272019717614a" +dependencies = [ + "bzip2", + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e8ccc4ea9f6acc32d102c0f6d471d11d913ad15f20c04de743374861fa1d414" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "convert_case" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.10.1", + "core-graphics-types 0.2.0", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-helmer-fork" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32eb7c354ae9f6d437a6039099ce7ecd049337a8109b23d73e48e8ffba8e9cd5" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "core-graphics2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4416167a69126e617f8d0a214af0e3c1dbdeffcb100ddf72dcd1a1ac9893c146" +dependencies = [ + "bitflags 2.13.2", + "block", + "cfg-if", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "core-text" +version = "21.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a593227b66cbd4007b2a050dfdd9e1d1318311409c8d600dc82ba1b15ca9c130" +dependencies = [ + "core-foundation 0.10.1", + "core-graphics 0.24.0", + "foreign-types", + "libc", +] + +[[package]] +name = "core-video" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139679cc63eb9504bdbe37e37874b0247136177655f0008588781e90863afa62" +dependencies = [ + "block", + "core-foundation 0.10.1", + "core-graphics2", + "io-surface", + "libc", + "metal", +] + +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + +[[package]] +name = "cosmic-text" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be17b688510d934ce13f48a2beba700e11583e281e0fda99c22bb256a14eda73" +dependencies = [ + "bitflags 2.13.2", + "fontdb", + "harfrust", + "linebender_resource_handle", + "log", + "rangemap", + "rustc-hash 2.1.3", + "self_cell", + "skrifa 0.40.0", + "smol_str", + "swash", + "sys-locale", + "unicode-bidi", + "unicode-linebreak", + "unicode-script", + "unicode-segmentation", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + +[[package]] +name = "ctor" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914a755b7c2d4af2bdcff7ce1739e2db9a1b81a9b07123d8015786ae03c0980d" +dependencies = [ + "link-section", + "linktime-proc-macro", +] + +[[package]] +name = "data-url" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case 0.10.0", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.3", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "libc", + "objc2 0.6.4", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dwrote" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b35532432acc8b19ceed096e35dfa088d3ea037fe4f3c085f1f97f33b4d02" +dependencies = [ + "lazy_static", + "libc", + "winapi", + "wio", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[package]] +name = "embed-resource" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.6+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "encoding_rs" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + +[[package]] +name = "encoding_rs_io" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba3fe847045ecff794b9c138293a80db914678c453ad63fbf0c6a9eb6e00b22" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enum-iterator" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4549325971814bda7a44061bf3fe7e487d447cba01e4220a4b454d630d7a016" +dependencies = [ + "enum-iterator-derive", +] + +[[package]] +name = "enum-iterator-derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "enumn" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etagere" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc89bf99e5dc15954a60f707c1e09d7540e5cd9af85fa75caa0b510bc08c5342" +dependencies = [ + "euclid", + "svg_fmt", +] + +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "exr" +version = "1.74.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711fe42c9964295e01ee3fba3f9fe0e1d24b98886950d68efe81b1c76e21adf3" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide 0.8.9", + "num-complex", + "pulp", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" +dependencies = [ + "getrandom 0.4.3", +] + +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide 0.9.1", + "zlib-rs", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + +[[package]] +name = "float-ord" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" + +[[package]] +name = "float_next_after" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" + +[[package]] +name = "fluent-uri" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "fastrand", + "futures-core", + "futures-sink", + "spin 0.9.9", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "font-types" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "font-types" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8eb065f3251655b3c90e22e5e363f310fc5332fb3402e37bbc94752283248f6" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "fontconfig-parser" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" +dependencies = [ + "roxmltree 0.20.0", +] + +[[package]] +name = "fontdb" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "freetype-sys" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7edc5b9669349acfda99533e9e0bcf26a51862ab43b08ee7745c55d28eb134" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-concurrency" +version = "7.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175cd8cca9e1d45b87f18ffa75088f2099e3c4fe5e2f83e42de112560bea8ea6" +dependencies = [ + "fixedbitset", + "futures-core", + "futures-lite", + "pin-project", + "smallvec", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix", + "windows-link 0.2.1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "gif" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gif" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "globset" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags 1.3.2", + "ignore", + "walkdir", +] + +[[package]] +name = "glow" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29038e1c483364cc6bb3cf78feee1816002e127c331a1eec55a4d202b9e1adb5" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gpu-allocator" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51255ea7cfaadb6c5f1528d43e92a82acb2b96c43365989a28b2d44ee38f8795" +dependencies = [ + "ash", + "hashbrown 0.16.1", + "log", + "presser", + "thiserror 2.0.21", + "windows 0.62.2", +] + +[[package]] +name = "gpu-descriptor" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" +dependencies = [ + "bitflags 2.13.2", + "gpu-descriptor-types", + "hashbrown 0.15.5", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "gpui-base" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64bb52b29a8dcdc8d3f595b8b0839c076f237572ab601ba4577d3da8ae085728" +dependencies = [ + "aho-corasick", + "anyhow", + "ashpd", + "async-channel", + "chrono", + "data-url", + "futures", + "gpui-pre", + "gpui-pre-macros", + "gpui-pre-sum-tree", + "html5ever", + "instant", + "itertools 0.13.0", + "lsp-types", + "markdown", + "markup5ever_rcdom", + "num-traits", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "raw-window-handle", + "regex", + "ropey", + "schemars", + "serde", + "serde_json", + "smallvec", + "smol", + "tracing", + "unicode-segmentation", + "web-time", + "windows 0.58.0", +] + +[[package]] +name = "gpui-component" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac246c9f3d9771e56fbf90bd7d8656d0992d83940b580debf2efab4c44fe91df" +dependencies = [ + "anyhow", + "chrono", + "core-text", + "enum-iterator", + "gpui-base", + "gpui-component-macros", + "gpui-kit-assets", + "gpui-pre", + "gpui-pre-macros", + "gpui-pre-sum-tree", + "instant", + "itertools 0.13.0", + "log", + "lsp-types", + "markdown", + "notify", + "num-traits", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "once_cell", + "paste", + "raw-window-handle", + "regex", + "resvg 0.45.1", + "ropey", + "rust-i18n", + "schemars", + "serde", + "serde_json", + "serde_repr", + "smallvec", + "smol", + "tracing", + "uuid", + "windows 0.58.0", +] + +[[package]] +name = "gpui-component-macros" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b3574bd6e108b9a09c9d584e60626c9140add6d8fe839bf57627577b7977e" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "gpui-kit" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8edb2a8eafdb6e65ad1a80625347b93f8e54cbf3fac82a584e4924cda674d5c2" +dependencies = [ + "gpui-base", + "gpui-component", + "gpui-kit-assets", + "gpui-pre", + "gpui-pre-platform", + "gpui-pre-web", +] + +[[package]] +name = "gpui-kit-assets" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "885a2abd416e29a76ef1553aea06eb2645a9290cf5e1d3fa02d218ecac6d318f" +dependencies = [ + "anyhow", + "gpui-pre", + "gpui-pre-reqwest", + "log", + "rust-embed", + "wasm-bindgen", + "wasm-bindgen-futures", +] + +[[package]] +name = "gpui-omarchy" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e527176d8b0fddd1e67abfdaaef128db907c43d0e03a56b5deaeb523da721110" +dependencies = [ + "async-channel", + "chrono", + "gpui-kit", + "itertools 0.13.0", + "notify", + "num-traits", + "toml 0.8.23", +] + +[[package]] +name = "gpui-pre" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e87a42bb37c7cb4e76dd1ac0ce88851e46e976e0373a47ab3e0757abffee54d" +dependencies = [ + "accesskit", + "anyhow", + "async-channel", + "async-task", + "backtrace", + "bindgen", + "bitflags 2.13.2", + "chrono", + "core-video", + "ctor", + "derive_more", + "embed-resource", + "etagere", + "futures", + "futures-concurrency", + "getrandom 0.3.4", + "gpui-pre-collections", + "gpui-pre-http-client", + "gpui-pre-macros", + "gpui-pre-refineable", + "gpui-pre-scheduler", + "gpui-pre-shared-string", + "gpui-pre-sum-tree", + "gpui-pre-util", + "gpui-pre-util-macros", + "heapless", + "image", + "inventory", + "itertools 0.14.0", + "log", + "lyon", + "num_cpus", + "objc2-core-foundation", + "objc2-core-video", + "parking", + "parking_lot", + "pin-project", + "pollster 0.4.0", + "postage", + "profiling", + "proptest", + "rand 0.9.5", + "raw-window-handle", + "regex", + "resvg 0.46.0", + "schemars", + "seahash", + "serde", + "serde_json", + "slotmap", + "smallvec", + "spin 0.10.1", + "strum", + "taffy", + "thiserror 2.0.21", + "ttf-parser", + "url", + "usvg 0.46.0", + "uuid", + "waker-fn", + "web-time", + "windows 0.62.2", + "zed-font-kit", + "zed-scap", +] + +[[package]] +name = "gpui-pre-apple" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00053551517815ab169fda28fff78dbebf2712722593c7671c511785cb3f79d5" +dependencies = [ + "anyhow", + "block2 0.6.2", + "cbindgen", + "cocoa 0.26.1", + "core-foundation 0.10.1", + "core-video", + "derive_more", + "etagere", + "foreign-types", + "gpui-pre", + "image", + "log", + "metal", + "objc", + "objc2 0.6.4", + "parking_lot", +] + +[[package]] +name = "gpui-pre-collections" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79760925c31ed06924d28d68981b5b22b2ce25665f58f0a33dfea11170e6e28" +dependencies = [ + "gpui-pre-util", + "indexmap", + "rustc-hash 2.1.3", +] + +[[package]] +name = "gpui-pre-derive-refineable" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1aea3f941581a6a9dd545ecd351ef2d336fe6e2a1a4555cb6414944a4984260" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "gpui-pre-http-client" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddce27e989e877a3a1a243f97d7ea4f1ade2532abec418d5a0be9cca802702c7" +dependencies = [ + "anyhow", + "async-compression", + "bytes", + "derive_more", + "futures", + "http", + "http-body", + "log", + "parking_lot", + "serde", + "serde_json", + "serde_urlencoded", + "url", +] + +[[package]] +name = "gpui-pre-linux" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aac7022e347409454777701082201742710052813964a1e25160f7e22c965ad" +dependencies = [ + "accesskit", + "accesskit_unix", + "anyhow", + "as-raw-xcb-connection", + "ashpd", + "bitflags 2.13.2", + "bytemuck", + "calloop", + "calloop-wayland-source", + "filedescriptor", + "futures", + "gpui-pre", + "gpui-pre-collections", + "gpui-pre-http-client", + "gpui-pre-util", + "gpui-pre-wgpu", + "libc", + "log", + "notify-rust", + "oo7", + "open", + "parking_lot", + "raw-window-handle", + "smallvec", + "smol", + "strum", + "url", + "uuid", + "wayland-backend", + "wayland-client", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-plasma", + "wayland-protocols-wlr", + "x11-clipboard", + "x11rb", + "xkbcommon", + "zed-scap", + "zed-xim", +] + +[[package]] +name = "gpui-pre-macos" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a43af845b260b09393e923c4f1e1a67a10fcfc847b4815192bbfd02ed9fe725" +dependencies = [ + "accesskit", + "accesskit_macos", + "anyhow", + "async-task", + "block2 0.6.2", + "cocoa 0.26.1", + "core-foundation 0.10.1", + "core-foundation-sys", + "core-graphics 0.24.0", + "core-text", + "ctor", + "dispatch2", + "foreign-types", + "futures", + "gpui-pre", + "gpui-pre-apple", + "gpui-pre-collections", + "gpui-pre-util", + "image", + "itertools 0.14.0", + "libc", + "log", + "mach2", + "objc", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-graphics", + "objc2-core-media", + "objc2-foundation 0.3.2", + "objc2-screen-capture-kit", + "objc2-user-notifications", + "parking_lot", + "pathfinder_geometry", + "raw-window-handle", + "semver", + "smallvec", + "strum", + "uuid", + "zed-font-kit", +] + +[[package]] +name = "gpui-pre-macros" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1627c5dd3c3351e39e0621d3f3e31dbbe7c7658df90b1d2ec12bfe95d54cd37" +dependencies = [ + "heck 0.5.0", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "gpui-pre-perf" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01954497dd02ba96ab4c252a8668643b0cc3085e7a75dca62e7ba0e3dcd83b02" +dependencies = [ + "gpui-pre-collections", + "serde", + "serde_json", +] + +[[package]] +name = "gpui-pre-platform" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0112105c2ac6757fbf527c9c92a753be9bcbce9f4cf26959a11eaa1db6565aa5" +dependencies = [ + "anyhow", + "console_error_panic_hook", + "gpui-pre", + "gpui-pre-linux", + "gpui-pre-macos", + "gpui-pre-web", + "gpui-pre-wgpu", + "gpui-pre-windows", +] + +[[package]] +name = "gpui-pre-refineable" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd14639e5dfab906bbaeabfe0f94289120dcf2e6ed37217deb13964867e89a5" +dependencies = [ + "gpui-pre-derive-refineable", +] + +[[package]] +name = "gpui-pre-reqwest" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05be23908e707966824f8c51a609904b7f30739e8d915e120a53c1b995ba964c" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-native-certs", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-rustls", + "tokio-socks", + "tokio-util", + "tower", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "windows-registry 0.4.0", +] + +[[package]] +name = "gpui-pre-scheduler" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8049963b716df01b67b58f86000597f222d5b31b15b35069b23a3b07f3c68352" +dependencies = [ + "async-task", + "backtrace", + "chrono", + "flume", + "futures", + "parking_lot", + "rand 0.9.5", + "wasm_thread", + "web-time", +] + +[[package]] +name = "gpui-pre-shared-string" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ef6314bdf5ab162c1913712ec4a91b013ce2a9cfb0b557ecdfdd5793c8b61d3" +dependencies = [ + "schemars", + "serde", + "smol_str", +] + +[[package]] +name = "gpui-pre-sum-tree" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cca7e5fad9d53b2d265860fefdffc5f171966cb99c3fd99ecac15c033f19209" +dependencies = [ + "gpui-pre-ztracing", + "heapless", + "log", + "rayon", + "tracing", +] + +[[package]] +name = "gpui-pre-util" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82e06359ae76f8bb07ed713c54adb80cea6a9a2d3dc715f41aec30cd2aadb4d" +dependencies = [ + "anyhow", + "log", + "which", +] + +[[package]] +name = "gpui-pre-util-macros" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a0da4143bb0e7eb926eed7edd3a61c9d17344207543dcc25fb398f0cb96b2f" +dependencies = [ + "gpui-pre-perf", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "gpui-pre-web" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3870cc909471bb830c3d79496770c754401aad6cfa122147ccb13e8908ad08" +dependencies = [ + "anyhow", + "console_error_panic_hook", + "futures", + "gpui-pre", + "gpui-pre-http-client", + "gpui-pre-scheduler", + "gpui-pre-wgpu", + "js-sys", + "log", + "parking_lot", + "raw-window-handle", + "smallvec", + "unicode-properties", + "unicode-script", + "unicode-segmentation", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm_thread", + "web-sys", + "web-time", +] + +[[package]] +name = "gpui-pre-wgpu" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0b02657b56b09140ce542f5e4434fb96d9fdaba0fd0035ba7dba13c171ddb9b" +dependencies = [ + "anyhow", + "bytemuck", + "cosmic-text", + "etagere", + "gpui-pre", + "gpui-pre-collections", + "gpui-pre-util", + "image", + "itertools 0.14.0", + "log", + "parking_lot", + "profiling", + "raw-window-handle", + "smallvec", + "swash", + "unicode-bidi", + "unicode-segmentation", + "web-sys", + "wgpu", + "zed-font-kit", +] + +[[package]] +name = "gpui-pre-windows" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05592a6f9e3e6bb7a9f2a0d4779271cf747a948db1c07b02448de777ffff8f3" +dependencies = [ + "accesskit", + "accesskit_windows", + "anyhow", + "dunce", + "etagere", + "futures", + "gpui-pre", + "gpui-pre-collections", + "gpui-pre-util", + "image", + "itertools 0.14.0", + "log", + "parking_lot", + "rand 0.9.5", + "raw-window-handle", + "smallvec", + "uuid", + "windows 0.62.2", + "windows-core 0.62.2", + "windows-numerics 0.3.1", + "windows-registry 0.6.1", +] + +[[package]] +name = "gpui-pre-zlog" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7506302091e66d7fb5e04a5b3480e90cff0bbe3157458d7df91a41c59238c48" +dependencies = [ + "anyhow", + "chrono", + "gpui-pre-collections", + "log", +] + +[[package]] +name = "gpui-pre-ztracing" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ed1bc6d53b3ca93785a8573d1066d555b67bc475b344fb28871f9811eb6f218" +dependencies = [ + "gpui-pre-zlog", + "gpui-pre-ztracing-macro", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "gpui-pre-ztracing-macro" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829258540ea51bb9ea4a71686beffe5c99586901ba9f39944d06e4a180ab63fe" + +[[package]] +name = "granit-parser" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e20f99e46474f56bd905c56e817ebddcf377a611f94c53ac4649e4d3fa3c0cd0" +dependencies = [ + "arraydeque", + "smallvec", +] + +[[package]] +name = "h2" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + +[[package]] +name = "harfrust" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9da2e5ae821f6e96664977bf974d6d6a2d6682f9ccee23e62ec1d134246845f9" +dependencies = [ + "bitflags 2.13.2", + "bytemuck", + "core_maths", + "read-fonts 0.37.0", + "smallvec", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heapless" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "html5ever" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-native-certs", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "hyper", + "libc", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ignore" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b69833ed729dc5aa7d19541d96d6cf8e9137194207a04916d658e43168402f" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif 0.14.2", + "image-webp", + "moxcms", + "num-traits", + "png 0.18.1", + "qoi", + "ravif", + "rayon", + "tiff", + "zune-core 0.5.3", + "zune-jpeg 0.5.15", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error 2.0.1", +] + +[[package]] +name = "imagesize" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" + +[[package]] +name = "imagesize" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" + +[[package]] +name = "imgref" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e44b0a4eaa4c82f441d50a963f2d5f05a787240aeee097597033e72accfd22f" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "inotify" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d" +dependencies = [ + "libc", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "inventory" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "io-surface" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "554b8c5d64ec09a3a520fe58e4d48a73e00ff32899cdcbe32a4877afd4968b8e" +dependencies = [ + "cgl", + "core-foundation 0.10.1", + "core-foundation-sys", + "leaky-cow", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "kqueue" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d763e5b24120b4ddf50de6c92308156765aabfbbccebf401da7cff2d70a41ea" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" +dependencies = [ + "bitflags 2.13.2", + "libc", +] + +[[package]] +name = "kurbo" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62" +dependencies = [ + "arrayvec", + "euclid", + "smallvec", +] + +[[package]] +name = "kurbo" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2" +dependencies = [ + "arrayvec", + "euclid", + "polycool", + "smallvec", +] + +[[package]] +name = "lazy_static" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" + +[[package]] +name = "leak" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd100e01f1154f2908dfa7d02219aeab25d0b9c7fa955164192e3245255a0c73" + +[[package]] +name = "leaky-cow" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a8225d44241fd324a8af2806ba635fc7c8a7e9a7de4d5cf3ef54e71f5926fc" +dependencies = [ + "leak", +] + +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + +[[package]] +name = "libbz2-rs-sys" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "libc", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linebender_resource_handle" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" + +[[package]] +name = "link-section" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39c29a617ce3df32c08497bdc1ab6e2376e0b17948ac166a2fbe5977c5954cd9" + +[[package]] +name = "linktime-proc-macro" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e57c38c1e860fd37c604281cdfb1dd2216977fd76a50f85ba2f388ef3219616" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" +dependencies = [ + "serde_core", + "value-bag", +] + +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + +[[package]] +name = "lru-slab" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" + +[[package]] +name = "lsp-types" +version = "0.97.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53353550a17c04ac46c585feb189c2db82154fc84b79c7a66c96c2c644f66071" +dependencies = [ + "bitflags 1.3.2", + "fluent-uri", + "serde", + "serde_json", + "serde_repr", +] + +[[package]] +name = "lyon" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0578bdecb7d6d88987b8b2b1e3a4e2f81df9d0ece1078623324a567904e7b7" +dependencies = [ + "lyon_algorithms", + "lyon_tessellation", +] + +[[package]] +name = "lyon_algorithms" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdfa8785f95e57914ddb35e3b59994aeba6f5e79e9cfd03da1c269f010f36009" +dependencies = [ + "lyon_path", + "num-traits", +] + +[[package]] +name = "lyon_geom" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4336502e29e32af93cf2dad2214ed6003c17ceb5bd499df77b1de663b9042b92" +dependencies = [ + "arrayvec", + "euclid", + "num-traits", +] + +[[package]] +name = "lyon_path" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c463f9c428b7fc5ec885dcd39ce4aa61e29111d0e33483f6f98c74e89d8621e" +dependencies = [ + "lyon_geom", + "num-traits", +] + +[[package]] +name = "lyon_tessellation" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b8dcf906637ecef61b3c0740c7a4e7f27caeb31257cfac0cc579ce15be6005" +dependencies = [ + "float_next_after", + "lyon_path", + "num-traits", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "mac-notification-sys" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd604973958ddcc11b561193c0fb96ba146506ef2f231ef2e7c35fd2cbc9beca" +dependencies = [ + "cc", + "log", + "objc2 0.6.4", + "objc2-foundation 0.3.2", + "time", + "uuid", +] + +[[package]] +name = "mach2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea" +dependencies = [ + "libc", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "markdown" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5cab8f2cadc416a82d2e783a1946388b31654d391d1c7d92cc1f03e295b1deb" +dependencies = [ + "serde", + "unicode-id", +] + +[[package]] +name = "markup5ever" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" +dependencies = [ + "log", + "phf 0.11.3", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "markup5ever_rcdom" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18" +dependencies = [ + "html5ever", + "markup5ever", + "tendril", + "xml5ever", +] + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "metal" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7047791b5bc903b8cd963014b355f71dc9864a9a0b727057676c1dcae5cbc15" +dependencies = [ + "bitflags 2.13.2", + "block", + "core-graphics-types 0.2.0", + "foreign-types", + "log", + "objc", + "paste", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + +[[package]] +name = "naga" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2bf919621e7975acb27d881bae2fb993e0d45c8e0446e85e6272971e00dc8df" +dependencies = [ + "arrayvec", + "bit-set 0.9.1", + "bitflags 2.13.2", + "cfg-if", + "cfg_aliases", + "codespan-reporting", + "half", + "hashbrown 0.16.1", + "hexf-parse", + "indexmap", + "libm", + "log", + "num-traits", + "once_cell", + "rustc-hash 1.1.0", + "spirv", + "thiserror 2.0.21", + "unicode-ident", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "no_std_io2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" +dependencies = [ + "memchr", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "normpath" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b11ce00d2594068e8a27c9146fdc5cf9f3ac38eb42c7cd34d05fea618873ac9f" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "notify" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009" +dependencies = [ + "bitflags 2.13.2", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.52.0", +] + +[[package]] +name = "notify-rust" +version = "4.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe430c879247371c47fdedd328ad4e301e6b30b533c5fd5db1c06d75253d0e46" +dependencies = [ + "futures-lite", + "log", + "mac-notification-sys", + "serde", + "tauri-winrt-notification", + "zbus", +] + +[[package]] +name = "notify-types" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585d3cb5e12e01aed9e8a1f70d5c6b5e86fe2a6e48fc8cd0b3e0b8df6f6eb174" +dependencies = [ + "instant", +] + +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f9a86e097b0d187ad0e65667c2f58b9254671e86e7dbb78036b16692eae099" +dependencies = [ + "libm", + "num-integer", + "num-iter", + "num-traits", + "once_cell", + "rand 0.9.5", + "serde", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "bytemuck", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", + "objc_exception", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "libc", + "objc2 0.5.2", + "objc2-core-data 0.2.2", + "objc2-core-image 0.2.2", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "libc", + "objc2 0.6.4", + "objc2-cloud-kit", + "objc2-core-data 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image 0.3.2", + "objc2-core-text", + "objc2-core-video", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" +dependencies = [ + "dispatch2", + "objc2 0.6.4", + "objc2-core-audio-types", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-core-audio-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-media" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ec576860167a15dd9fce7fbee7512beb4e31f532159d3482d1f9c6caedf31d" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2 0.6.4", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-core-video", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-io-surface", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "libc", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.13.2", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", +] + +[[package]] +name = "objc2-screen-capture-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74b7c5390f477482f001bc354d6571a70db7e4f8d5288e860c45521fbce11394" +dependencies = [ + "block2 0.6.2", + "dispatch2", + "objc2 0.6.4", + "objc2-core-graphics", + "objc2-core-media", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "bitflags 2.13.2", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-core-location", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "oo7" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78f2bfed90f1618b4b48dcad9307f25e14ae894e2949642c87c351601d62cebd" +dependencies = [ + "aes", + "ashpd", + "async-fs", + "async-io", + "async-lock", + "blocking", + "cbc", + "cipher", + "digest 0.10.7", + "endi", + "futures-lite", + "futures-util", + "getrandom 0.4.3", + "hkdf", + "hmac", + "md-5", + "num", + "num-bigint-dig", + "pbkdf2", + "serde", + "serde_bytes", + "sha2 0.10.9", + "subtle", + "zbus", + "zbus_macros", + "zeroize", + "zvariant", +] + +[[package]] +name = "open" +version = "5.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa576c76302b7b808eecc68061e67336c47833ef9d22caa74dda10fa9675eebc" +dependencies = [ + "is-wsl", + "libc", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-float" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c7c9e0d9b23589f26070720bac724174bfec1083e82f7854cdd0267518343c0" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + +[[package]] +name = "pathfinder_geometry" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b7e7b4ea703700ce73ebf128e1450eb69c3a8329199ffbfb9b2a0418e5ad3" +dependencies = [ + "log", + "pathfinder_simd", +] + +[[package]] +name = "pathfinder_simd" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4500030c302e4af1d423f36f3b958d1aecb6c04184356ed5a833bf6b60435777" +dependencies = [ + "rustc_version", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared 0.11.3", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared 0.13.1", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.8", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "pollster" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7" + +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + +[[package]] +name = "polycool" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "postage" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af3fb618632874fb76937c2361a7f22afd393c982a2165595407edc75b06d3c1" +dependencies = [ + "atomic", + "crossbeam-queue", + "futures", + "log", + "parking_lot", + "pin-project", + "pollster 0.2.5", + "static_assertions", + "thiserror 1.0.69", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.15+spec-1.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set 0.8.0", + "bit-vec 0.8.0", + "bitflags 2.13.2", + "num-traits", + "proptest-macro", + "rand 0.9.5", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "proptest-macro" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efaa288b896cb2b345da7b7f2110ab19e51565b83495b56fcec98a62f8b1f33e" +dependencies = [ + "convert_case 0.11.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pulp" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046aa45b989642ec2e4717c8e72d677b13edd831a4d3b6cf37d9a3e54912496a" +dependencies = [ + "bytemuck", + "cfg-if", + "libm", + "num-complex", + "paste", + "pulp-wasm-simd-flag", + "raw-cpuid", + "reborrow", + "version_check", +] + +[[package]] +name = "pulp-wasm-simd-flag" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8f70e07b9c3962945a74e59ca1c511bba65b6419468acc217c457d93f3c740" + +[[package]] +name = "pxfm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.3", + "rustls", + "socket2", + "thiserror 2.0.21", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e750cca55fe4f0439a15d0bb529da9651e79993e8e72c61a899a36d462befbe" +dependencies = [ + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.3", + "rand_pcg", + "ring", + "rustc-hash 2.1.3", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.21", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af66907df18639dcf4db56ca65490cabc4b27a97dbadd96f2926cca73298f016" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "range-alloc" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" + +[[package]] +name = "rangemap" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a611d15b50743feb4c76b7d03edcb0e64f399c26961e4efe6975bc398be6aa3d" + +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools 0.14.0", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand 0.9.5", + "rand_chacha 0.9.0", + "simd_helpers", + "thiserror 2.0.21", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error 2.0.1", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "raw-window-metal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135" +dependencies = [ + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "read-fonts" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b634fabf032fab15307ffd272149b622260f55974d9fad689292a5d33df02e5" +dependencies = [ + "bytemuck", + "core_maths", + "font-types 0.11.3", +] + +[[package]] +name = "read-fonts" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709" +dependencies = [ + "bytemuck", + "font-types 0.12.5", + "once_cell", +] + +[[package]] +name = "reborrow" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "redox_users" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" +dependencies = [ + "libredox", + "thiserror 2.0.21", +] + +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[package]] +name = "resvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8928798c0a55e03c9ca6c4c6846f76377427d2c1e1f7e6de3c06ae57942df43" +dependencies = [ + "gif 0.13.3", + "image-webp", + "log", + "pico-args", + "rgb", + "svgtypes 0.15.3", + "tiny-skia", + "usvg 0.45.1", + "zune-jpeg 0.4.21", +] + +[[package]] +name = "resvg" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b563218631706d614e23059436526d005b50ab5f2d506b55a17eb65c5eb83419" +dependencies = [ + "gif 0.14.2", + "image-webp", + "log", + "pico-args", + "rgb", + "svgtypes 0.16.1", + "tiny-skia", + "usvg 0.46.0", + "zune-jpeg 0.5.15", +] + +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "ropey" +version = "2.0.0-beta.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4045a00dc327d084a2bbf126976e14125b54f23bd30511d45b842eba76c52d74" +dependencies = [ + "str_indices", +] + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + +[[package]] +name = "roxmltree" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb" +dependencies = [ + "memchr", +] + +[[package]] +name = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags 2.13.2", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rust-embed" +version = "8.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e7760e252aaba7b09f4be00e36476cf585bdb68a53552ac954cdf504ab4bc9" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bcfc4d6f53af43755f7a723e4b6b8794fcce052a178dd8c6c1dadc5f5343097" +dependencies = [ + "mime_guess", + "proc-macro2", + "quote", + "rust-embed-utils", + "shellexpand", + "syn 2.0.119", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ffa149f6aa81b58a5b3011d01a857c4ed12c7a732d2c51947a4c7c692185f0" +dependencies = [ + "globset", + "sha2 0.11.0", + "walkdir", +] + +[[package]] +name = "rust-i18n" +version = "4.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8502e73e6461c738659455c5d2cf95bc132760a6f203c717e66ee72b78906d7f" +dependencies = [ + "globwalk", + "regex", + "rust-i18n-macro", + "rust-i18n-support", + "smallvec", +] + +[[package]] +name = "rust-i18n-macro" +version = "4.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54a28dd9d46108d56b9f0c85b220908844233e561c03087890325104f90f2c6" +dependencies = [ + "glob", + "proc-macro2", + "quote", + "rust-i18n-support", + "serde", + "serde_json", + "syn 2.0.119", +] + +[[package]] +name = "rust-i18n-support" +version = "4.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e806c2a9701cbd6043992cd935c30cb1fdb128fa15e7b506f3b46f97045737c0" +dependencies = [ + "arc-swap", + "base62", + "foldhash 0.2.0", + "globwalk", + "itertools 0.11.0", + "normpath", + "serde", + "serde-saphyr", + "serde_json", + "siphasher", + "toml 0.8.23", + "triomphe", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error 1.2.3", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "rustybuzz" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" +dependencies = [ + "bitflags 2.13.2", + "bytemuck", + "core_maths", + "log", + "smallvec", + "ttf-parser", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "indexmap", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 3.0.6", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "screencapturekit" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5eeeb57ac94960cfe5ff4c402be6585ae4c8d29a2cf41b276048c2e849d64e" +dependencies = [ + "screencapturekit-sys", +] + +[[package]] +name = "screencapturekit-sys" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22411b57f7d49e7fe08025198813ee6fd65e1ee5eff4ebc7880c12c82bde4c60" +dependencies = [ + "block", + "dispatch", + "objc", + "objc-foundation", + "objc_id", + "once_cell", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "self_cell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-saphyr" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8050abb251097357e24aff63ba2c52a6309ecb7d23a5474023df960a02694d8" +dependencies = [ + "annotate-snippets", + "base64 0.23.1", + "encoding_rs_io", + "granit-parser", + "nohash-hasher", + "num-traits", + "serde_core", + "smallvec", + "zmij", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_derive_internals" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_fmt" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e497af288b3b95d067a23a4f749f2861121ffcb2f6d8379310dcda040c345ed" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shellexpand" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32824fab5e16e6c4d86dc1ba84489390419a39f97699852b66480bb87d297ed8" +dependencies = [ + "dirs 6.0.0", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "simplecss" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" + +[[package]] +name = "skrifa" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbdfe3d2475fbd7ddd1f3e5cf8288a30eb3e5f95832829570cd88115a7434ac" +dependencies = [ + "bytemuck", + "read-fonts 0.37.0", +] + +[[package]] +name = "skrifa" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68" +dependencies = [ + "bytemuck", + "read-fonts 0.41.0", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" + +[[package]] +name = "smol" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f" +dependencies = [ + "async-channel", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-net", + "async-process", + "blocking", + "futures-lite", +] + +[[package]] +name = "smol_str" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523" +dependencies = [ + "borsh", + "serde_core", +] + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spin" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spirv" +version = "0.4.0+sdk-1.4.341.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "str_indices" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08889ec5408683408db66ad89e0e1f93dff55c73a4ccc71c427d5b277ee47e6" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "sval" +version = "2.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b81b254da21fe1fcc4e3a74fe39b46e25e3a863078f8b71c954d47f84889dbc6" + +[[package]] +name = "sval_buffer" +version = "2.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50be352d2822ffafb59e3e2ddac9d5ee60f2eeadbb7b5a2a951b9f3651e87a6f" +dependencies = [ + "sval", + "sval_ref", + "zerocopy", +] + +[[package]] +name = "sval_dynamic" +version = "2.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048ca293b998d9a45659159f94a64063791e74cdc670164943dbb434405573d" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_fmt" +version = "2.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6b5888e40f80568733217f27b7317b845f463400ced36c424b1a804730e53b2" +dependencies = [ + "itoa", + "ryu", + "sval", +] + +[[package]] +name = "sval_json" +version = "2.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17664d6bb6b74947afaab9d7c991caa9bf5638d4dee16fcbef637f440796049" +dependencies = [ + "itoa", + "ryu", + "sval", +] + +[[package]] +name = "sval_nested" +version = "2.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07c059969ca5ca163ea7fef6c9661758973d17691aba92abdcf5c428f4ec122c" +dependencies = [ + "sval", + "sval_buffer", + "sval_ref", +] + +[[package]] +name = "sval_ref" +version = "2.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d6b29ff568c85c87561807f51d2adfff4b6016c6363133f7cd1652a12548f3" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_serde" +version = "2.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f33ec9edc42b12764d5c90ca0a1d84189c6bde81ed27507f1e661c6e4e05853" +dependencies = [ + "serde_core", + "sval", + "sval_nested", +] + +[[package]] +name = "svg_fmt" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" + +[[package]] +name = "svgtypes" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" +dependencies = [ + "kurbo 0.11.3", + "siphasher", +] + +[[package]] +name = "svgtypes" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "695b5790b3131dafa99b3bbfd25a216edb3d216dad9ca208d4657bfb8f2abc3d" +dependencies = [ + "kurbo 0.13.1", + "siphasher", +] + +[[package]] +name = "swash" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2499c2d826531388872b2268718aed907a39bd785ab0dcfe57fab26283f92e" +dependencies = [ + "skrifa 0.44.0", + "yazi", + "zeno", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + +[[package]] +name = "sysinfo" +version = "0.31.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "rayon", + "windows 0.57.0", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "taffy" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c034e05f6ee85a12daa63863c2245797715075c70649947aa0da54f3f2ab1d0f" +dependencies = [ + "arrayvec", + "serde", + "slotmap", + "smallvec", +] + +[[package]] +name = "tao-core-video-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271450eb289cb4d8d0720c6ce70c72c8c858c93dd61fc625881616752e6b98f6" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "objc", +] + +[[package]] +name = "tauri-winrt-notification" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f37a6c354fd28fc9e322ed9bd47e3959576dad28c9d58ea1cf888cce1c7ccb36" +dependencies = [ + "thiserror 2.0.21", + "windows 0.62.2", + "windows-version", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error 2.0.1", + "weezl", + "zune-jpeg 0.5.15", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png 0.17.16", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-socks" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7e2948f60dbe26b35f2c7fb74ac2854c1fddded0fe9d7548fcc674a246f7615" +dependencies = [ + "either", + "futures-util", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml" +version = "1.1.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_edit" +version = "0.25.15+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "triomphe" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b40688ea6389c8171614b25491f71d4a27946e0c7ce2da1c6de27e25abf1a0ae" +dependencies = [ + "arc-swap", + "serde", + "stable_deref_trait", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" + +[[package]] +name = "unicode-ccc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" + +[[package]] +name = "unicode-id" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65dd75ab2d9d379dbe86d4059e91d8b5a6e7054865eaba1ddbfc7a817ff96392" + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-script" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "usvg" +version = "0.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef" +dependencies = [ + "base64 0.22.1", + "data-url", + "flate2", + "fontdb", + "imagesize 0.13.0", + "kurbo 0.11.3", + "log", + "pico-args", + "roxmltree 0.20.0", + "rustybuzz", + "simplecss", + "siphasher", + "strict-num", + "svgtypes 0.15.3", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + +[[package]] +name = "usvg" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e419dff010bb12512b0ae9e3d2f318dfbdf0167fde7eb05465134d4e8756076f" +dependencies = [ + "base64 0.22.1", + "data-url", + "flate2", + "fontdb", + "imagesize 0.14.0", + "kurbo 0.13.1", + "log", + "pico-args", + "roxmltree 0.21.1", + "rustybuzz", + "simplecss", + "siphasher", + "strict-num", + "svgtypes 0.16.1", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "sha1_smol", + "wasm-bindgen", +] + +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "value-bag" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2799ffb329a792ecfd902b71306c8a815a6ef1c0470fa9953a6aa4d4cecbe511" +dependencies = [ + "value-bag-serde1", + "value-bag-sval2", +] + +[[package]] +name = "value-bag-serde1" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0941feceafbe7a8f59ea1096d45b97002884a41306315ad797b3684b63a81d8c" +dependencies = [ + "erased-serde", + "serde_core", + "serde_fmt", +] + +[[package]] +name = "value-bag-sval2" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "839752af8179287d27eb2b94164641b1ede9e60ab7424163388dc21ebd0508cd" +dependencies = [ + "sval", + "sval_buffer", + "sval_dynamic", + "sval_fmt", + "sval_json", + "sval_ref", + "sval_serde", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.6", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasm_thread" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7516db7f32decdadb1c3b8deb1b7d78b9df7606c5cc2f6241737c2ab3a0258e" +dependencies = [ + "futures", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wayland-backend" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" +dependencies = [ + "cc", + "downcast-rs", + "log", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" +dependencies = [ + "bitflags 2.13.2", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" +dependencies = [ + "rustix", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.13.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "wgpu" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76e8840e1ba2881d4cbb18d2147627a56af426ff064c0401eb0c8410c6325d07" +dependencies = [ + "arrayvec", + "bitflags 2.13.2", + "bytemuck", + "cfg-if", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "js-sys", + "log", + "naga", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f519832254e56965a9940c4af57dcb75f702b6f6fa4a0b172f685395843a4d7" +dependencies = [ + "arrayvec", + "bit-set 0.9.1", + "bit-vec 0.9.1", + "bitflags 2.13.2", + "bytemuck", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "indexmap", + "log", + "naga", + "once_cell", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.21", + "wgpu-core-deps-apple", + "wgpu-core-deps-emscripten", + "wgpu-core-deps-wasm", + "wgpu-core-deps-windows-linux-android", + "wgpu-hal", + "wgpu-naga-bridge", + "wgpu-types", +] + +[[package]] +name = "wgpu-core-deps-apple" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5e39e26c4c0e07589e67d18546cf79ff45383659fc72fca4dd293358a0347f3" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-emscripten" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01e09be551dc939498bdd5f6b2c66e55ab275dad25825267a08605a80fc9f0af" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-wasm" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1fb1798be2a912497d4c224f72d39bb0cb34af50e8bcc29865bc339c943059" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-windows-linux-android" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e592c1bbef6ad047647ae6e666ebd8cee7a32bb4544d9700ec96cbf73230257" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-hal" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ace1c17727311c22a46e4e3faf56ea6de81af99dcc839bdfb54857b94d448d" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set 0.9.1", + "bitflags 2.13.2", + "block2 0.6.2", + "bytemuck", + "cfg-if", + "cfg_aliases", + "glow", + "glutin_wgl_sys", + "gpu-allocator", + "gpu-descriptor", + "hashbrown 0.16.1", + "js-sys", + "khronos-egl", + "libc", + "libloading", + "log", + "naga", + "ndk-sys", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", + "objc2-quartz-core 0.3.2", + "once_cell", + "ordered-float", + "parking_lot", + "portable-atomic", + "portable-atomic-util", + "profiling", + "range-alloc", + "raw-window-handle", + "raw-window-metal", + "renderdoc-sys", + "smallvec", + "thiserror 2.0.21", + "wasm-bindgen", + "wayland-sys", + "web-sys", + "wgpu-naga-bridge", + "wgpu-types", + "windows 0.62.2", + "windows-core 0.62.2", + "windows-result 0.4.1", +] + +[[package]] +name = "wgpu-naga-bridge" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95226013f547544b223281cd16a4fb549aa9dcb562adbda0faae4c73ffbbc161" +dependencies = [ + "naga", + "wgpu-types", +] + +[[package]] +name = "wgpu-types" +version = "29.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84bf84cd9ca8ca45e2b223a3868f1adf9bfc0c66aeac212e76ee7e40fdadf8f5" +dependencies = [ + "bitflags 2.13.2", + "bytemuck", + "js-sys", + "log", + "raw-window-handle", + "web-sys", +] + +[[package]] +name = "which" +version = "8.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae2f2b2b816647a1cab1acc91f5bd20812d53cb344382635ec2181940c8034f" +dependencies = [ + "libc", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections 0.2.0", + "windows-core 0.61.2", + "windows-future 0.2.1", + "windows-link 0.1.3", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", +] + +[[package]] +name = "windows-capture" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a4df73e95feddb9ec1a7e9c2ca6323b8c97d5eeeff78d28f1eccdf19c882b24" +dependencies = [ + "parking_lot", + "rayon", + "thiserror 2.0.21", + "windows 0.61.3", + "windows-future 0.2.1", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result 0.3.4", + "windows-strings 0.3.1", + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wio" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" +dependencies = [ + "winapi", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-clipboard" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "662d74b3d77e396b8e5beb00b9cad6a9eccf40b2ef68cc858784b14c41d535a3" +dependencies = [ + "libc", + "x11rb", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "rustix", + "x11rb-protocol", + "xcursor", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xcb" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6c2ad15e0e922856ee89afe862b8992334bbe7953adad56cd1199358cb30566" +dependencies = [ + "bitflags 2.13.2", + "libc", + "quick-xml", + "x11", +] + +[[package]] +name = "xcursor" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23" + +[[package]] +name = "xim-ctext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ac61a7062c40f3c37b6e82eeeef835d5cc7824b632a72784a89b3963c33284c" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "xim-parser" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dcee45f89572d5a65180af3a84e7ddb24f5ea690a6d3aa9de231281544dd7b7" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "xkbcommon" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d66ca9352cbd4eecbbc40871d8a11b4ac8107cfc528a6e14d7c19c69d0e1ac9" +dependencies = [ + "as-raw-xcb-connection", + "libc", + "memmap2", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "xml-rs" +version = "0.8.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" + +[[package]] +name = "xml5ever" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bbb26405d8e919bc1547a5aa9abc95cbfa438f04844f5fdd9dc7596b748bf69" +dependencies = [ + "log", + "mac", + "markup5ever", +] + +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + +[[package]] +name = "yazi" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5" + +[[package]] +name = "yeslogic-fontconfig-sys" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8b8abf912b9a29ff112e1671c97c33636903d13a69712037190e6805af4f76" +dependencies = [ + "dlib", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.4", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus-lockstep" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6998de05217a084b7578728a9443d04ea4cd80f2a0839b8d78770b76ccd45863" +dependencies = [ + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus-lockstep-macros" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "zbus-lockstep", + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 3.0.6", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" +dependencies = [ + "serde", + "winnow 1.0.4", + "zvariant", +] + +[[package]] +name = "zbus_xml" +version = "5.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1586c021a01ca0a9216dcd874e546382e156a5cbab5fab6cb5f10087e22682a" +dependencies = [ + "serde", + "winnow 1.0.4", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zcheapstr" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473" +dependencies = [ + "serde", +] + +[[package]] +name = "zed-font-kit" +version = "0.14.1-zed" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3898e450f36f852edda72e3f985c34426042c4951790b23b107f93394f9bff5" +dependencies = [ + "bitflags 2.13.2", + "byteorder", + "core-foundation 0.10.1", + "core-graphics 0.24.0", + "core-text", + "dirs 5.0.1", + "dwrote", + "float-ord", + "freetype-sys", + "lazy_static", + "libc", + "log", + "pathfinder_geometry", + "pathfinder_simd", + "walkdir", + "winapi", + "yeslogic-fontconfig-sys", +] + +[[package]] +name = "zed-scap" +version = "0.0.8-zed" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b338d705ae33a43ca00287c11129303a7a0aa57b101b72a1c08c863f698ac8" +dependencies = [ + "anyhow", + "cocoa 0.25.0", + "core-graphics-helmer-fork", + "log", + "objc", + "rand 0.8.8", + "screencapturekit", + "screencapturekit-sys", + "sysinfo", + "tao-core-video-sys", + "windows 0.61.3", + "windows-capture", + "x11", + "xcb", +] + +[[package]] +name = "zed-xim" +version = "0.4.0-zed" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0b46ed118eba34d9ba53d94ddc0b665e0e06a2cf874cfa2dd5dec278148642" +dependencies = [ + "ahash", + "hashbrown 0.14.5", + "log", + "x11rb", + "xim-ctext", + "xim-parser", +] + +[[package]] +name = "zeno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" + +[[package]] +name = "zerocopy" +version = "0.8.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-core" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +dependencies = [ + "zune-core 0.4.12", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core 0.5.3", +] + +[[package]] +name = "zvariant" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147" +dependencies = [ + "endi", + "enumflags2", + "serde", + "serde_bytes", + "winnow 1.0.4", + "zcheapstr", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 3.0.6", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 3.0.6", + "winnow 1.0.4", +] From e8c88042b01a98c7f6037808cd55ae0f689ba1cb Mon Sep 17 00:00:00 2001 From: Victor Phos Date: Fri, 2 Oct 2026 14:44:11 +0200 Subject: [PATCH 31/36] Replace the made-up 115 Ch logo with an element tile A periodic-table style tile reading 17 / Ch / Chlorine. The sidebar shows it with the name along the bottom (just number and symbol when collapsed) and the About card adds the atomic mass 35.45. The tile is set in the font chosen with `omarchy font set`, read from fontconfig by gpui-omarchy, so it matches the terminal. --- src/ui/settings.rs | 12 ++----- src/ui/sidebar.rs | 21 ++++++++--- src/ui/widgets.rs | 89 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 15 deletions(-) diff --git a/src/ui/settings.rs b/src/ui/settings.rs index 55df8aa..04f218b 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -1,4 +1,4 @@ -use super::widgets::page_header; +use super::widgets::{element_tile, page_header, TileDetail}; use gpui_kit::{div, prelude::*, rems, App, Div, IntoElement}; use gpui_omarchy::{panel, separator, ActiveTheme as _}; @@ -38,15 +38,7 @@ pub fn render(cx: &App) -> impl IntoElement { .flex() .items_center() .gap(rems(0.75)) - .child( - div() - .px(rems(0.75)) - .py(rems(0.5)) - .border_1() - .border_color(t.accent) - .text_color(t.accent) - .child("115 Ch"), - ) + .child(element_tile(4.5, TileDetail::Full, cx)) .child( div() .child(div().text_size(rems(1.)).font_weight(gpui_kit::FontWeight::BOLD).child("Chelete")) diff --git a/src/ui/sidebar.rs b/src/ui/sidebar.rs index 54dd437..27b4352 100644 --- a/src/ui/sidebar.rs +++ b/src/ui/sidebar.rs @@ -1,4 +1,5 @@ use super::shell::Shell; +use super::widgets::{element_tile, TileDetail}; use super::{View, TogglePalette, ToggleSidebar}; use gpui_kit::{div, prelude::*, rems, Context, Div, SharedString}; use gpui_omarchy::{button, icon, keycap, ActiveTheme as _, ButtonVariant, IconName}; @@ -99,11 +100,21 @@ pub fn sidebar(shell: &Shell, cx: &mut Context) -> Div { div() .flex() .items_center() - .justify_center() - .py(rems(0.75)) - .text_color(t.accent) - .font_weight(gpui_kit::FontWeight::BOLD) - .child(if collapsed { "Ch" } else { "115 Ch" }), + .gap(rems(0.625)) + .py(rems(0.5)) + .when(collapsed, |row| row.justify_center()) + .child(if collapsed { + element_tile(2.25, TileDetail::Compact, cx) + } else { + element_tile(3.25, TileDetail::Named, cx) + }) + .when(!collapsed, |row| { + row.child( + div() + .child(div().text_size(rems(0.9375)).font_weight(gpui_kit::FontWeight::BOLD).text_color(t.foreground).child("Chelete")) + .child(div().text_color(t.secondary).child("Personal finance")), + ) + }), ) .children(nav) .child(div().flex_1()) diff --git a/src/ui/widgets.rs b/src/ui/widgets.rs index e478040..a2b8ff9 100644 --- a/src/ui/widgets.rs +++ b/src/ui/widgets.rs @@ -382,3 +382,92 @@ pub fn submit_on_enter( }) .collect() } + +// ── logo ──────────────────────────────────────────────────────────── + +pub const ELEMENT_NUMBER: &str = "17"; +pub const ELEMENT_SYMBOL: &str = "Ch"; +pub const ELEMENT_NAME: &str = "Chlorine"; +pub const ELEMENT_MASS: &str = "35.45"; + +/// How much of the element tile to draw. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TileDetail { + /// Atomic number and symbol. + Compact, + /// Adds the element name along the bottom. + Named, + /// Adds the atomic mass in the top right corner as well. + Full, +} + +/// A periodic-table tile: atomic number top left, the symbol large in the +/// middle and the name along the bottom. It is set in the font chosen with +/// `omarchy font set` (read from fontconfig by gpui-omarchy), so it matches the +/// terminal and the rest of the desktop. `size` is the side length in rems. +pub fn element_tile(size: f32, detail: TileDetail, cx: &App) -> Div { + let t = cx.omarchy(); + let small = |factor: f32| rems((size * factor).max(0.5)); + div() + .relative() + .size(rems(size)) + .flex_shrink_0() + .border_1() + .border_color(t.accent) + .text_color(t.accent) + .font_family(t.mono_font.clone()) + .child( + div() + .absolute() + .top(rems(size * 0.05)) + .left(rems(size * 0.09)) + .text_size(small(0.19)) + .child(ELEMENT_NUMBER), + ) + .child( + div() + .size_full() + .flex() + .items_center() + .justify_center() + .when(detail != TileDetail::Compact, |symbol| symbol.pb(rems(size * 0.1))) + .text_size(rems(size * 0.4)) + .font_weight(FontWeight::BOLD) + .child(ELEMENT_SYMBOL), + ) + .when(detail == TileDetail::Full, |tile| { + tile.child( + div() + .absolute() + .top(rems(size * 0.05)) + .right(rems(size * 0.09)) + .text_size(small(0.15)) + .child(ELEMENT_MASS), + ) + }) + .when(detail != TileDetail::Compact, |tile| { + tile.child( + div() + .absolute() + .bottom(rems(size * 0.05)) + .w_full() + .flex() + .justify_center() + .text_size(small(0.15)) + .child(ELEMENT_NAME), + ) + }) +} + +#[cfg(test)] +mod logo_tests { + use super::*; + + #[test] + fn the_tile_reads_17_ch_chlorine() { + assert_eq!(ELEMENT_NUMBER, "17"); + assert_eq!(ELEMENT_SYMBOL, "Ch"); + assert_eq!(ELEMENT_NAME, "Chlorine"); + assert_eq!(ELEMENT_MASS, "35.45"); + } +} From 2e8a881522d49416bccda0134a4641bf7da2f332 Mon Sep 17 00:00:00 2001 From: Victor Phos Date: Fri, 2 Oct 2026 15:06:31 +0200 Subject: [PATCH 32/36] Use the Omarchy font for the whole UI gpui-omarchy defaults to the platform UI font, which resolves to Adwaita Sans here, while Omarchy sets one font with `omarchy font set`. Swap it for that font in both control libraries and put it back after every palette change, so the sidebar, tables, dialogs and date picker match the terminal. --- src/ui/theme_bridge.rs | 48 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/src/ui/theme_bridge.rs b/src/ui/theme_bridge.rs index 72b7ebc..6ffb6b5 100644 --- a/src/ui/theme_bridge.rs +++ b/src/ui/theme_bridge.rs @@ -1,11 +1,30 @@ //! gpui-omarchy themes its own controls; gpui-component widgets (the date //! picker) read a separate theme. Copy the Omarchy palette across and keep it //! in step when the system theme changes. +//! +//! It also swaps the UI font. gpui-omarchy defaults to the platform UI font, +//! which resolves to Adwaita Sans on GNOME-flavoured systems, whereas Omarchy +//! itself uses the one font chosen with `omarchy font set`. Use that everywhere. use gpui_kit::component::Theme as ComponentTheme; -use gpui_kit::App; +use gpui_kit::{App, BorrowAppContext as _}; use gpui_omarchy::ActiveTheme as _; +/// Make the Omarchy font the UI font for both control libraries. +fn use_omarchy_font(cx: &mut App) { + let mono = cx.omarchy().mono_font.clone(); + if cx.omarchy().font != mono { + // Observers see this change too, but by then the fonts already match. + cx.update_global::(|theme, _| theme.font = mono.clone()); + } + // gpui-omarchy copies its UI font into the base theme whenever it applies a palette. + gpui_kit::base::Theme::global_mut(cx).tokens.typography.sans = mono.clone(); + let t = ComponentTheme::global_mut(cx); + t.font_family = mono.clone(); + t.mono_font_family = mono; +} + pub fn sync(cx: &mut App) { + use_omarchy_font(cx); let o = cx.omarchy().clone(); let t = ComponentTheme::global_mut(cx); t.background = o.background; @@ -34,3 +53,30 @@ pub fn install(cx: &mut App) { sync(cx); cx.observe_global::(sync).detach(); } + +#[cfg(test)] +mod tests { + use super::*; + use crate::ui::test_support::init; + use gpui_kit::TestAppContext; + + #[gpui_kit::test] + fn the_ui_uses_the_omarchy_font_even_after_a_theme_change(cx: &mut TestAppContext) { + init(cx); + cx.update(|cx| { + let omarchy = cx.omarchy().clone(); + assert_ne!(omarchy.mono_font.as_ref(), ".SystemUIFont"); + assert_eq!(omarchy.font, omarchy.mono_font, "omarchy controls use the mono font"); + assert_eq!(ComponentTheme::global(cx).font_family, omarchy.mono_font, "so do gpui-component widgets"); + assert_eq!(gpui_kit::base::Theme::global(cx).tokens.typography.sans, omarchy.mono_font); + }); + // Applying a palette resets the font to the platform UI font; it must be put back. + cx.update(|cx| gpui_omarchy::Theme::flexoki_light().apply(cx)); + cx.update(|cx| { + let omarchy = cx.omarchy().clone(); + assert_eq!(omarchy.font, omarchy.mono_font); + assert_eq!(ComponentTheme::global(cx).font_family, omarchy.mono_font); + assert_eq!(gpui_kit::base::Theme::global(cx).tokens.typography.sans, omarchy.mono_font); + }); + } +} From bd593c71c84d10141f5422540c678a9addb697ed Mon Sep 17 00:00:00 2001 From: Victor Phos Date: Fri, 2 Oct 2026 15:48:59 +0200 Subject: [PATCH 33/36] Size body text to match the Omarchy controls Tables, lists and labels outside the controls had no explicit size and inherited the window default, which looked large next to the 12px controls; give the shell root the same 0.75rem body size. --- src/ui/shell.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ui/shell.rs b/src/ui/shell.rs index 8af41d4..b4f99dc 100644 --- a/src/ui/shell.rs +++ b/src/ui/shell.rs @@ -225,6 +225,9 @@ impl Render for Shell { .bg(t.background) .text_color(t.foreground) .font_family(t.font.clone()) + // Everything not sized explicitly (tables, lists, labels) inherits the + // same 12px base the Omarchy controls use. + .text_size(rems(0.75)) .child(sidebar(self, cx)) .child(div().flex_1().min_w_0().h_full().child(content)) .when_some(self.palette.clone(), |el, palette| el.child(palette)) From a687b3483d25300a71e2efd4010cbfbeb744a55e Mon Sep 17 00:00:00 2001 From: Victor Phos Date: Fri, 2 Oct 2026 15:48:59 +0200 Subject: [PATCH 34/36] Add a text size preference A percentage of the toolkit's 16px base, offered in nine steps from 70% to 200% with 110% as the default, plus helpers to step it up or down and to convert it to a pixel size. Preference files written before this existed still load. --- src/prefs.rs | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/src/prefs.rs b/src/prefs.rs index baccb98..5930a61 100644 --- a/src/prefs.rs +++ b/src/prefs.rs @@ -1,10 +1,48 @@ //! Small UI preferences persisted next to the database. use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +/// Text size as a percentage of the toolkit's 16px base. +pub const TEXT_SCALES: [u16; 9] = [70, 80, 90, 100, 110, 125, 150, 175, 200]; +pub const DEFAULT_TEXT_SCALE: u16 = 110; + +/// The next larger (`direction > 0`) or smaller (`< 0`) step, staying put at +/// either end. A value between steps moves to the nearest step in that direction. +pub fn step_text_scale(current: u16, direction: i8) -> u16 { + let steps = TEXT_SCALES; + if direction > 0 { + steps.into_iter().find(|s| *s > current).unwrap_or(steps[steps.len() - 1]) + } else if direction < 0 { + steps.into_iter().rev().find(|s| *s < current).unwrap_or(steps[0]) + } else { + current + } +} + +pub fn clamp_text_scale(percent: u16) -> u16 { + percent.clamp(TEXT_SCALES[0], TEXT_SCALES[TEXT_SCALES.len() - 1]) +} + +/// Pixels for one rem at the given text size. +pub fn rem_size_px(percent: u16) -> f32 { + 16.0 * f32::from(clamp_text_scale(percent)) / 100.0 +} + +fn default_text_scale() -> u16 { + DEFAULT_TEXT_SCALE +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct Prefs { #[serde(default)] pub sidebar_collapsed: bool, + #[serde(default = "default_text_scale")] + pub text_scale: u16, +} + +impl Default for Prefs { + fn default() -> Self { + Self { sidebar_collapsed: false, text_scale: DEFAULT_TEXT_SCALE } + } } fn path() -> std::path::PathBuf { @@ -35,5 +73,49 @@ mod tests { fn missing_fields_fall_back_to_defaults() { let p: Prefs = serde_json::from_str("{}").unwrap(); assert!(!p.sidebar_collapsed); + assert_eq!(p.text_scale, DEFAULT_TEXT_SCALE); + assert_eq!(p, Prefs::default()); + } + + #[test] + fn files_written_before_text_size_existed_still_load() { + let p: Prefs = serde_json::from_str(r#"{"sidebar_collapsed": true}"#).unwrap(); + assert!(p.sidebar_collapsed); + assert_eq!(p.text_scale, DEFAULT_TEXT_SCALE); + } + + #[test] + fn stepping_moves_one_notch_and_stops_at_the_ends() { + assert_eq!(step_text_scale(100, 1), 110); + assert_eq!(step_text_scale(110, 1), 125); + assert_eq!(step_text_scale(110, -1), 100); + assert_eq!(step_text_scale(200, 1), 200); + assert_eq!(step_text_scale(70, -1), 70); + assert_eq!(step_text_scale(110, 0), 110); + } + + #[test] + fn stepping_from_between_notches_goes_to_the_nearest_one_in_that_direction() { + assert_eq!(step_text_scale(105, 1), 110); + assert_eq!(step_text_scale(105, -1), 100); + assert_eq!(step_text_scale(500, -1), 200, "an out-of-range value comes back to the top step"); + assert_eq!(step_text_scale(1, 1), 70); + } + + #[test] + fn every_step_maps_to_a_sensible_rem_size() { + assert_eq!(rem_size_px(100), 16.0); + assert!((rem_size_px(110) - 17.6).abs() < 1e-4); + assert_eq!(rem_size_px(200), 32.0); + assert_eq!(rem_size_px(10), 11.2, "clamped to the smallest step"); + assert_eq!(rem_size_px(900), 32.0, "clamped to the largest step"); + for s in TEXT_SCALES { + assert_eq!(clamp_text_scale(s), s); + } + } + + #[test] + fn the_default_is_one_of_the_offered_steps() { + assert!(TEXT_SCALES.contains(&DEFAULT_TEXT_SCALE)); } } From 2d7750306b2a884442f8f303ad9a1b3710fc9324 Mon Sep 17 00:00:00 2001 From: Victor Phos Date: Fri, 2 Oct 2026 15:49:19 +0200 Subject: [PATCH 35/36] Add a text size setting A Text size dropdown in Settings (70% to 200%) and Ctrl +, Ctrl - and Ctrl 0 shortcuts. Everything is sized in rems, so the setting scales the base size the way a browser zoom does. The window root re-applies the gpui-component theme's font_size as the rem size on every frame, so that value carries the setting; changing only the window's own rem size is undone on the next frame. The choice is remembered and the dropdown and shortcuts stay in step. Tests open the window the way main does (through Root, which plain test windows skip) and check the laid-out sidebar width at several sizes. --- src/ui/mod.rs | 7 ++ src/ui/settings.rs | 77 ++++++++++++++-- src/ui/shell.rs | 194 ++++++++++++++++++++++++++++++++++++++++- src/ui/sidebar.rs | 1 + src/ui/theme_bridge.rs | 12 +++ src/ui/widgets.rs | 1 + 6 files changed, 284 insertions(+), 8 deletions(-) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index ecf428e..019b484 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -41,6 +41,9 @@ actions!( PalettePrev, PaletteDismiss, ModalCancel, + ZoomIn, + ZoomOut, + ZoomReset, ] ); @@ -126,6 +129,10 @@ pub fn bind_keys(cx: &mut App) { KeyBinding::new("ctrl-s", GoSettings, shell), KeyBinding::new("ctrl-k", TogglePalette, shell), KeyBinding::new("ctrl-b", ToggleSidebar, shell), + KeyBinding::new("ctrl-=", ZoomIn, shell), + KeyBinding::new("ctrl-+", ZoomIn, shell), + KeyBinding::new("ctrl--", ZoomOut, shell), + KeyBinding::new("ctrl-0", ZoomReset, shell), KeyBinding::new("ctrl-q", Quit, None), KeyBinding::new("down", PaletteNext, Some(PALETTE_CONTEXT)), KeyBinding::new("up", PalettePrev, Some(PALETTE_CONTEXT)), diff --git a/src/ui/settings.rs b/src/ui/settings.rs index 04f218b..2b93bbe 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -1,6 +1,7 @@ -use super::widgets::{element_tile, page_header, TileDetail}; -use gpui_kit::{div, prelude::*, rems, App, Div, IntoElement}; -use gpui_omarchy::{panel, separator, ActiveTheme as _}; +use super::widgets::{choice, choice_value, element_tile, page_header, TileDetail}; +use chelete_lib::prefs::TEXT_SCALES; +use gpui_kit::{div, prelude::*, rems, App, Context, Div, Entity, EventEmitter, IntoElement, Render, Subscription, Window}; +use gpui_omarchy::{panel, select, separator, ActiveTheme as _, ChoiceState}; const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -18,13 +19,79 @@ fn row(label: &str, description: Option<&str>, value: impl IntoElement, cx: &App .child(div().text_color(t.secondary).child(value)) } -pub fn render(cx: &App) -> impl IntoElement { - let t = cx.omarchy(); + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SettingsEvent { + TextScale(u16), +} + +pub struct SettingsScreen { + scale: Entity, + /// The size the dropdown currently shows, so showing a size chosen + /// elsewhere (the keyboard) is not mistaken for a new choice. + shown: u16, + _sub: Subscription, +} + +impl EventEmitter for SettingsScreen {} + +fn scale_items() -> Vec<(String, String)> { + TEXT_SCALES.iter().map(|s| (s.to_string(), format!("{s}%"))).collect() +} + +impl SettingsScreen { + pub fn new(text_scale: u16, window: &mut Window, cx: &mut Context) -> Self { + let scale = choice(scale_items(), Some(&text_scale.to_string()), window, cx); + let sub = cx.observe(&scale, |this, state, cx| { + if let Some(percent) = choice_value(&state, cx).and_then(|v| v.parse::().ok()) { + if percent != this.shown { + this.shown = percent; + cx.emit(SettingsEvent::TextScale(percent)); + } + } + }); + Self { scale, shown: text_scale, _sub: sub } + } + + /// Reflect a size that was changed elsewhere (a keyboard shortcut). + pub fn show_scale(&mut self, percent: u16, cx: &mut Context) { + if self.shown == percent { + return; + } + self.shown = percent; + let index = TEXT_SCALES.iter().position(|s| *s == percent); + self.scale.update(cx, |state, cx| state.set_selected(index, cx)); + cx.notify(); + } + + #[cfg(test)] + pub fn shown(&self) -> u16 { + self.shown + } + + #[cfg(test)] + pub fn choose(&mut self, percent: u16, cx: &mut Context) { + let index = TEXT_SCALES.iter().position(|s| *s == percent); + self.scale.update(cx, |state, cx| state.set_selected(index, cx)); + } +} + +impl Render for SettingsScreen { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + render_page(&self.scale, window, cx) + } +} + +fn render_page(scale: &Entity, window: &mut Window, cx: &mut App) -> impl IntoElement { + let t = cx.omarchy().clone(); let theme_name = t.name.to_string(); let appearance = panel("Appearance", cx) .child(row("Theme", Some("Follows the Omarchy system theme"), theme_name, cx)) .child(separator(cx)) + .child(separator(cx)) + .child(row("Text size", Some("Ctrl + larger, Ctrl - smaller, Ctrl 0 reset"), div().w(rems(7.)).child(select("text-scale", scale, window, cx)), cx)) + .child(separator(cx)) .child(row("Corners", Some("Square, like the rest of Omarchy"), "Square", cx)); let finance = panel("Finance", cx) diff --git a/src/ui/shell.rs b/src/ui/shell.rs index b4f99dc..50bff61 100644 --- a/src/ui/shell.rs +++ b/src/ui/shell.rs @@ -1,7 +1,7 @@ use super::accounts::AccountsScreen; use super::categories::CategoriesScreen; use super::projections::ProjectionsScreen; -use super::settings; +use super::settings::{SettingsEvent, SettingsScreen}; use super::subscriptions::SubscriptionsScreen; use super::transactions::TransactionsScreen; use super::overview::OverviewScreen; @@ -10,7 +10,7 @@ use super::palette::{Command, Palette, PaletteEvent}; use super::sidebar::sidebar; use super::*; use chelete_lib::database::DbState; -use chelete_lib::prefs::Prefs; +use chelete_lib::prefs::{clamp_text_scale, step_text_scale, Prefs, DEFAULT_TEXT_SCALE}; use gpui_kit::{ div, prelude::*, rems, Context, Entity, FocusHandle, Focusable, IntoElement, Render, Subscription, Window, @@ -28,6 +28,7 @@ pub struct Shell { subscriptions: Entity, projections: Entity, transactions: Entity, + settings: Entity, toast: Option<(u64, String, bool)>, toast_seq: u64, palette: Option>, @@ -53,6 +54,14 @@ impl Shell { cx.subscribe(&subscriptions, Self::on_screen_event).detach(); let projections = cx.new(|cx| ProjectionsScreen::new(db.clone(), window, cx)); let transactions = cx.new(|cx| TransactionsScreen::new(db.clone(), window, cx)); + let text_scale = clamp_text_scale(prefs.text_scale); + super::theme_bridge::set_text_scale(text_scale, window, cx); + let settings = cx.new(|cx| SettingsScreen::new(text_scale, window, cx)); + cx.subscribe_in(&settings, window, |this, _, event: &SettingsEvent, window, cx| { + let SettingsEvent::TextScale(percent) = event; + this.set_text_scale(*percent, window, cx); + }) + .detach(); cx.subscribe(&transactions, Self::on_screen_event).detach(); let overview = cx.new(|cx| OverviewScreen::new(db.clone(), window, cx)); Self { @@ -62,6 +71,7 @@ impl Shell { subscriptions, projections, transactions, + settings, toast: None, toast_seq: 0, view: View::Overview, @@ -120,6 +130,23 @@ impl Shell { self.toast.as_ref().map(|(_, m, _)| m.as_str()) } + #[cfg(test)] + pub fn text_scale(&self) -> u16 { + self.prefs.text_scale + } + + /// Resize all text by scaling the window's rem, as a browser zoom would. + pub fn set_text_scale(&mut self, percent: u16, window: &mut Window, cx: &mut Context) { + let percent = clamp_text_scale(percent); + self.prefs.text_scale = percent; + super::theme_bridge::set_text_scale(percent, window, cx); + if self.persist { + self.prefs.save(); + } + self.settings.update(cx, |s, cx| s.show_scale(percent, cx)); + cx.notify(); + } + fn toggle_sidebar(&mut self, cx: &mut Context) { self.prefs.sidebar_collapsed = !self.prefs.sidebar_collapsed; if self.persist { @@ -205,7 +232,7 @@ impl Render for Shell { View::Subscriptions => self.subscriptions.clone().into_any_element(), View::Projections => self.projections.clone().into_any_element(), View::Transactions => self.transactions.clone().into_any_element(), - View::Settings => settings::render(cx).into_any_element(), + View::Settings => self.settings.clone().into_any_element(), }; div() .key_context(SHELL_CONTEXT) @@ -219,6 +246,15 @@ impl Render for Shell { .on_action(cx.listener(|this, _: &GoSettings, w, cx| this.navigate(View::Settings, w, cx))) .on_action(cx.listener(|this, _: &TogglePalette, w, cx| this.toggle_palette(w, cx))) .on_action(cx.listener(|this, _: &ToggleSidebar, _, cx| this.toggle_sidebar(cx))) + .on_action(cx.listener(|this, _: &ZoomIn, window, cx| { + this.set_text_scale(step_text_scale(this.prefs.text_scale, 1), window, cx) + })) + .on_action(cx.listener(|this, _: &ZoomOut, window, cx| { + this.set_text_scale(step_text_scale(this.prefs.text_scale, -1), window, cx) + })) + .on_action(cx.listener(|this, _: &ZoomReset, window, cx| { + this.set_text_scale(DEFAULT_TEXT_SCALE, window, cx) + })) .size_full() .relative() .flex() @@ -381,4 +417,156 @@ mod tests { assert_eq!(view_of(&shell, cx), view); } } + + fn rem(cx: &mut gpui_kit::VisualTestContext) -> f32 { + cx.update(|window, _| f32::from(window.rem_size())) + } + + #[gpui_kit::test] + fn text_starts_at_the_default_size(cx: &mut TestAppContext) { + let (shell, cx) = setup(cx); + cx.update(|_, cx| assert_eq!(shell.read(cx).text_scale(), 110)); + assert!((rem(cx) - 17.6).abs() < 1e-3); + } + + #[gpui_kit::test] + fn a_saved_size_is_applied_on_startup(cx: &mut TestAppContext) { + init(cx); + let db = seeded_db(); + let prefs = Prefs { text_scale: 150, ..Prefs::default() }; + let (shell, cx) = cx.add_window_view(move |window, cx| Shell::new(db, prefs, false, window, cx)); + cx.update(|_, cx| assert_eq!(shell.read(cx).text_scale(), 150)); + assert_eq!(rem(cx), 24.0); + } + + #[gpui_kit::test] + fn a_nonsense_saved_size_is_pulled_into_range(cx: &mut TestAppContext) { + init(cx); + let db = seeded_db(); + let prefs = Prefs { text_scale: 9000, ..Prefs::default() }; + let (_shell, cx) = cx.add_window_view(move |window, cx| Shell::new(db, prefs, false, window, cx)); + assert_eq!(rem(cx), 32.0, "clamped to 200%"); + } + + #[gpui_kit::test] + fn ctrl_plus_minus_and_zero_change_the_text_size(cx: &mut TestAppContext) { + let (shell, cx) = setup(cx); + cx.simulate_keystrokes("ctrl-="); + cx.update(|_, cx| assert_eq!(shell.read(cx).text_scale(), 125)); + assert_eq!(rem(cx), 20.0); + cx.simulate_keystrokes("ctrl--"); + cx.simulate_keystrokes("ctrl--"); + cx.update(|_, cx| assert_eq!(shell.read(cx).text_scale(), 100)); + assert_eq!(rem(cx), 16.0); + cx.simulate_keystrokes("ctrl-0"); + cx.update(|_, cx| assert_eq!(shell.read(cx).text_scale(), 110), ); + } + + #[gpui_kit::test] + fn zooming_stops_at_the_smallest_and_largest_sizes(cx: &mut TestAppContext) { + let (shell, cx) = setup(cx); + for _ in 0..12 { + cx.simulate_keystrokes("ctrl--"); + } + cx.update(|_, cx| assert_eq!(shell.read(cx).text_scale(), 70)); + for _ in 0..12 { + cx.simulate_keystrokes("ctrl-="); + } + cx.update(|_, cx| assert_eq!(shell.read(cx).text_scale(), 200)); + assert_eq!(rem(cx), 32.0); + } + + #[gpui_kit::test] + fn the_settings_dropdown_changes_the_text_size(cx: &mut TestAppContext) { + let (shell, cx) = setup(cx); + let settings = cx.update(|_, cx| shell.read(cx).settings.clone()); + cx.update(|_, cx| settings.update(cx, |s, cx| s.choose(150, cx))); + cx.update(|_, cx| assert_eq!(shell.read(cx).text_scale(), 150)); + assert_eq!(rem(cx), 24.0); + } + + #[gpui_kit::test] + fn the_keyboard_updates_the_settings_dropdown_without_looping(cx: &mut TestAppContext) { + let (shell, cx) = setup(cx); + let settings = cx.update(|_, cx| shell.read(cx).settings.clone()); + cx.simulate_keystrokes("ctrl-="); + cx.update(|_, cx| { + assert_eq!(settings.read(cx).shown(), 125, "dropdown follows the shortcut"); + assert_eq!(shell.read(cx).text_scale(), 125, "and does not push a second change back"); + }); + cx.simulate_keystrokes("ctrl-0"); + cx.update(|_, cx| assert_eq!(settings.read(cx).shown(), 110)); + } + + fn measure(cx: &mut gpui_kit::VisualTestContext, selector: &'static str) -> f32 { + cx.update(|window, cx| window.draw(cx).clear(cx)); + f32::from(cx.debug_bounds(selector).unwrap_or_else(|| panic!("{selector} not drawn")).size.width) + } + + #[gpui_kit::test] + fn zooming_actually_resizes_what_is_drawn(cx: &mut TestAppContext) { + // Navigate somewhere with a page title first. + let (shell, cx) = setup(cx); + cx.update(|window, cx| shell.update(cx, |s, cx| s.navigate(View::Accounts, window, cx))); + let (sidebar, title) = (measure(cx, "sidebar"), measure(cx, "page-title")); + + cx.simulate_keystrokes("ctrl-="); + let (sidebar_big, title_big) = (measure(cx, "sidebar"), measure(cx, "page-title")); + // 110% -> 125% is a 13.6% increase. + assert!(sidebar_big > sidebar * 1.1, "sidebar should widen: {sidebar} -> {sidebar_big}"); + assert!(title_big > title * 1.1, "title text should grow: {title} -> {title_big}"); + + cx.simulate_keystrokes("ctrl-0"); + let (sidebar_back, title_back) = (measure(cx, "sidebar"), measure(cx, "page-title")); + assert!((sidebar_back - sidebar).abs() < 0.5, "reset restores the width"); + assert!((title_back - title).abs() < 0.5, "reset restores the text"); + } + + #[gpui_kit::test] + fn the_settings_dropdown_resizes_the_layout_too(cx: &mut TestAppContext) { + let (shell, cx) = setup(cx); + let before = measure(cx, "sidebar"); + let settings = cx.update(|_, cx| shell.read(cx).settings.clone()); + cx.update(|_, cx| settings.update(cx, |s, cx| s.choose(200, cx))); + let after = measure(cx, "sidebar"); + assert!(after > before * 1.5, "200% vs 110% should nearly double the sidebar: {before} -> {after}"); + } + + /// The real app opens its window through `gpui_kit::open_window`, which + /// wraps the shell in a Root view. Tests normally skip that wrapper. + #[gpui_kit::test] + fn zooming_works_in_a_window_opened_the_way_main_opens_it(cx: &mut TestAppContext) { + init(cx); + let db = seeded_db(); + let (window, shell) = cx + .update(|cx| { + gpui_kit::open_window(gpui_kit::WindowOptions::default(), cx, |window, cx| { + cx.new(|cx| Shell::new(db, Prefs::default(), false, window, cx)) + }) + }) + .expect("window opens"); + let cx = &mut gpui_kit::VisualTestContext::from_window(window, cx); + let sidebar_is = |cx: &mut gpui_kit::VisualTestContext, expected: f32| { + // Draw a few frames: Root re-applies its own rem size on every one. + for _ in 0..3 { + cx.update(|window, cx| window.draw(cx).clear(cx)); + } + let width = measure(cx, "sidebar"); + assert!((width - expected).abs() < 0.6, "sidebar is 15rem = {expected}px, was {width}px"); + }; + + sidebar_is(cx, 15.0 * 17.6); // 110% is the default + cx.simulate_keystrokes("ctrl-="); + cx.update(|_, cx| assert_eq!(shell.read(cx).text_scale(), 125)); + sidebar_is(cx, 15.0 * 20.0); + cx.simulate_keystrokes("ctrl--"); + cx.simulate_keystrokes("ctrl--"); + sidebar_is(cx, 15.0 * 16.0); + cx.simulate_keystrokes("ctrl-0"); + sidebar_is(cx, 15.0 * 17.6); + + let settings = cx.update(|_, cx| shell.read(cx).settings.clone()); + cx.update(|_, cx| settings.update(cx, |s, cx| s.choose(200, cx))); + sidebar_is(cx, 15.0 * 32.0); + } } diff --git a/src/ui/sidebar.rs b/src/ui/sidebar.rs index 27b4352..9c2fdb0 100644 --- a/src/ui/sidebar.rs +++ b/src/ui/sidebar.rs @@ -83,6 +83,7 @@ pub fn sidebar(shell: &Shell, cx: &mut Context) -> Div { })); div() + .debug_selector(|| "sidebar".into()) .flex() .flex_col() .flex_shrink_0() diff --git a/src/ui/theme_bridge.rs b/src/ui/theme_bridge.rs index 6ffb6b5..1c5f2f9 100644 --- a/src/ui/theme_bridge.rs +++ b/src/ui/theme_bridge.rs @@ -48,6 +48,18 @@ pub fn sync(cx: &mut App) { t.chart_5 = o.chart[4]; } +/// Set the size of one rem for the whole app from a text size percentage. +/// +/// The window's root re-applies `ComponentTheme.font_size` as the rem size on +/// every frame, so that value has to carry the setting; setting only the +/// window's own rem size is undone on the next frame. +pub fn set_text_scale(percent: u16, window: &mut gpui_kit::Window, cx: &mut App) { + let rem = gpui_kit::px(chelete_lib::prefs::rem_size_px(percent)); + ComponentTheme::global_mut(cx).font_size = rem; + window.set_rem_size(rem); + window.refresh(); +} + /// Sync now and again whenever the Omarchy theme changes. pub fn install(cx: &mut App) { sync(cx); diff --git a/src/ui/widgets.rs b/src/ui/widgets.rs index a2b8ff9..6df3a3b 100644 --- a/src/ui/widgets.rs +++ b/src/ui/widgets.rs @@ -30,6 +30,7 @@ pub fn page_header(title: &str, actions: impl IntoElement, cx: &App) -> Div { .justify_between() .child( div() + .debug_selector(|| "page-title".into()) .text_size(rems(1.25)) .font_weight(FontWeight::BOLD) .text_color(t.foreground) From 8b71a1f15edc6330ca54ea55ddf4c1754d75d399 Mon Sep 17 00:00:00 2001 From: Victor Phos Date: Fri, 2 Oct 2026 15:49:19 +0200 Subject: [PATCH 36/36] Document the text size shortcuts --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2cab523..4f85018 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ Data lives in `~/.local/share/com.chelete.app/` (the same place earlier versions | `Ctrl+O` `T` `A` `C` `U` `P` `S` | Overview, Transactions, Accounts, Categories, Subscriptions, Projections, Settings | | `Ctrl+K` | Command palette | | `Ctrl+B` | Collapse the sidebar | +| `Ctrl+=` `Ctrl+-` `Ctrl+0` | Larger text, smaller text, reset (also in Settings) | | `Esc` / `Enter` | Close a dialog / submit a form | | `Ctrl+Q` | Quit |