diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..ba575d3
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,16 @@
+* text=auto
+
+*.md text eol=lf
+*.ts text eol=lf
+*.tsx text eol=lf
+*.json text eol=lf
+*.rs text eol=lf
+*.toml text eol=lf
+*.css text eol=lf
+*.html text eol=lf
+
+*.ps1 text eol=crlf
+
+*.png binary
+*.ico binary
+*.icns binary
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..51b6032
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,24 @@
+node_modules/
+dist/
+src-tauri/target/
+src-tauri/gen/
+
+# Local planning/evidence generated during agent work.
+docs/audits/
+docs/goals/
+docs/superpowers/
+docs/*-brief.md
+
+# Runtime/generated local state.
+*.log
+*.tmp
+*.bak
+.env
+.env.*
+
+# OS/editor noise.
+.DS_Store
+Thumbs.db
+.idea/
+.vscode/
+
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..1d6de75
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,89 @@
+# Инструкции для агентов
+
+## Контекст проекта
+
+ProxyWarden - standalone Windows desktop client в корне репозитория. Это Tauri 2 + React/TypeScript UI + Rust backend для маршрутизации выбранных Windows-приложений через внешний SOCKS5-прокси или опциональный Local sing-box.
+
+Не возвращать старую идею `APP_MODE=windows` и не подключать Windows-клиент к отдельному Node gateway/server. Текущий рабочий путь - `src`, `src-tauri`, `scripts` в корне репозитория.
+
+## Основные инварианты
+
+- Три компонента должны оставаться разделенными: Control App, ProxiFyre, Local sing-box.
+- ProxiFyre - обязательный слой для per-app routing; Local sing-box - необязательный runtime.
+- Внешний SOCKS5 flow должен работать без установленного Local sing-box.
+- Profile apply не должен скрыто устанавливать, удалять, запускать или чинить компоненты. Install/start/stop/uninstall - только явные действия пользователя.
+- Source of truth - JSON под `C:\ProgramData\ProxyWarden\config` и `state`.
+- `C:\ProgramData\ProxyWarden\generated\proxifyre-app-config.json` и `sing-box-config.json` - derived artifacts, их можно пересоздать.
+- Subscription URL и другие секреты нельзя показывать полностью в UI, diagnostics или логах.
+- Summary panel должен оставаться read-only: без apply/install/start/stop/delete/input/subscription mutations.
+
+## Структура
+
+- `src/app/App.tsx` - основная React-оркестрация, вкладки `Сводка`, `ProxiFyre`, `VPN / Прокси`, вызовы Tauri-команд и transient UI state.
+- `src/app/readiness.ts` - gating применимости маршрута. Не обходить его локальными проверками в JSX.
+- `src/app/viewModel.ts` - маленькие display/view-model helpers.
+- `src/ui/*` - общие presentational-компоненты. Для новых кнопок, вкладок, service rows, pills, полей и лог-дока сначала расширять эти компоненты.
+- `src/api/tauriCommands.ts` - единственная TypeScript-обертка над `invoke(...)`; держать DTO в синхронизации с Rust.
+- `src/domain/types.ts` - TypeScript-зеркало доменных DTO.
+- `src-tauri/src/models.rs` - Rust-модели и default values.
+- `src-tauri/src/validation.rs` - нормализация входов.
+- `src-tauri/src/storage.rs` и `activity.rs` - JSON storage, backup/tmp writes, activity cap/sort.
+- `src-tauri/src/adapters/proxy_router.rs` - adapter boundary для proxy-router.
+- `src-tauri/src/adapters/proxifyre.rs` - первый adapter, генерирует ProxiFyre `app-config.json`.
+- `src-tauri/src/adapters/singbox.rs` - генерация локального `sing-box` конфига из subscription cache и выбранного сервера.
+- `src-tauri/src/component_detection.rs` - detection ProxiFyre/Proxifier/Local sing-box.
+- `src-tauri/src/commands.rs` - Tauri command handlers, installer/service orchestration, structured errors.
+- `src-tauri/src/main.rs` - реальная Tauri entrypoint-регистрация команд.
+- `src-tauri/src/lib.rs` сейчас scaffold/stale; не считать его источником регистрации команд без отдельной cleanup-задачи.
+- `scripts/*.ps1` - явные installer entrypoints. `-PlanOnly` должен возвращать structured JSON без side effects.
+
+## Правила изменений
+
+- Не создавать второй источник правды для профилей, targets, components, subscription или activity.
+- Не писать generated config напрямую из React.
+- Не парсить raw PowerShell/stdout в UI. Backend/helper boundary должен возвращать structured JSON/error DTO.
+- Не привязывать UI напрямую к деталям ProxiFyre, если изменение относится к общему proxy-router поведению.
+- Не делать Local sing-box обязательным для external target.
+- Для service/install операций сохранять UAC/admin boundary и человекочитаемые ошибки.
+- При удалении install folders сохранять safe-path checks; не ослаблять рекурсивное удаление.
+- В UI держать стиль компактной Windows-утилиты, а не landing/dashboard. Использовать existing `Button`, `Tabs`, `ServiceControlRow`, `StatusPill`, `Field`, `ActionMenu`, `LogDock`.
+- Apply actions должны быть disabled с объяснением, когда нет приложений, ProxiFyre отсутствует, proxy input неверный или local route не готов.
+
+## Проверка
+
+Минимум для frontend/UI:
+
+```powershell
+npm run build
+```
+
+Rust/backend:
+
+```powershell
+cd D:\repos\ProxyWarden\src-tauri
+cargo test
+```
+
+Tauri/toolchain:
+
+```powershell
+npm run tauri -- info
+npm run tauri -- dev
+npm run tauri -- build
+```
+
+Installer boundaries:
+
+```powershell
+& .\scripts\install-control-app.ps1 -PlanOnly
+& .\scripts\install-proxyfier.ps1 -PlanOnly
+& .\scripts\install-singbox.ps1 -PlanOnly
+```
+
+Для UI-изменений проверять browser-preview на desktop и narrow viewport. Browser-preview не доказывает native Tauri commands или elevated service lane.
+
+## Известные риски
+
+- Реальные elevated install/start/stop/uninstall операции для ProxiFyre и Local sing-box считаются `implemented but unproven`, пока они не проверены на Windows с UAC/admin confirmation.
+- Исторические planning/evidence файлы лежат в ignored `docs`-папках и не должны попадать в коммиты.
+- Старые документы могут ссылаться на `apps/windows-client`; текущая структура репозитория - standalone client в корне.
diff --git a/README.md b/README.md
index f7950cc..86737e6 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,142 @@
# ProxyWarden
+ProxyWarden - это Windows-приложение для маршрутизации выбранных программ через прокси или VPN-сервер. Оно не меняет глобальные настройки прокси в Windows: вы сами выбираете, какие приложения должны идти через маршрут, например Discord, Telegram, браузер, игру или конкретный `.exe`.
+
+## Что умеет приложение
+
+- добавлять приложения по имени процесса, папке или конкретному EXE-файлу;
+- отправлять выбранные приложения через внешний SOCKS5-прокси;
+- при необходимости поднимать локальный `sing-box` и использовать сервер из подписки;
+- показывать состояние компонентов: установлен ли ProxiFyre, запущены ли службы, выбран ли сервер;
+- генерировать конфиги для ProxiFyre и `sing-box` из сохраненных настроек;
+- не устанавливать скрыто лишние компоненты при применении профиля.
+
+## Из чего состоит ProxyWarden
+
+ProxyWarden разделен на три независимые части.
+
+**Control App** - само desktop-приложение. В нем вы настраиваете маршрут, выбираете приложения, смотрите статус и запускаете явные действия.
+
+**ProxiFyre** - обязательный слой для маршрутизации отдельных Windows-приложений. Он заставляет выбранные программы ходить через SOCKS5-прокси даже тогда, когда сами программы не умеют работать с прокси.
+
+**Local sing-box** - необязательный локальный VPN/proxy runtime. Он нужен только если вы хотите вставить subscription URL, выбрать сервер и получить локальный SOCKS5 endpoint `127.0.0.1:1080`. Если у вас уже есть внешний SOCKS5-прокси, `sing-box` можно не устанавливать.
+
+## Типичный сценарий с внешним прокси
+
+1. Запустите ProxyWarden.
+2. Установите или проверьте ProxiFyre.
+3. На вкладке `VPN / Прокси` выберите `Внешний прокси`.
+4. Введите адрес в формате `host:port` или `socks5://host:port`.
+5. На вкладке `ProxiFyre` добавьте приложения, которые нужно маршрутизировать.
+6. Нажмите `Применить в ProxiFyre`.
+
+Результат: выбранные приложения идут через внешний SOCKS5-прокси. Local sing-box для этого сценария не нужен.
+
+## Типичный сценарий с Local sing-box
+
+1. Запустите ProxyWarden.
+2. Установите ProxiFyre.
+3. Установите Local sing-box.
+4. Вставьте subscription URL, загрузите список серверов и выберите сервер.
+5. Добавьте приложения для маршрутизации.
+6. Примените маршрут.
+
+Результат: выбранные приложения идут по цепочке:
+
+```text
+Приложения -> ProxiFyre -> Local sing-box 127.0.0.1:1080 -> выбранный сервер
+```
+
+## Где хранятся настройки
+
+Пользовательские настройки хранятся в `C:\ProgramData\ProxyWarden`:
+
+```text
+C:\ProgramData\ProxyWarden\config\profiles.json
+C:\ProgramData\ProxyWarden\config\targets.json
+C:\ProgramData\ProxyWarden\config\components.json
+C:\ProgramData\ProxyWarden\config\local-singbox.json
+C:\ProgramData\ProxyWarden\state\activity.json
+C:\ProgramData\ProxyWarden\state\singbox-subscription-cache.json
+```
+
+Сгенерированные файлы лежат отдельно и могут быть пересозданы:
+
+```text
+C:\ProgramData\ProxyWarden\generated\proxifyre-app-config.json
+C:\ProgramData\ProxyWarden\generated\sing-box-config.json
+```
+
+Важно: редактировать вручную лучше исходные настройки, а не generated-файлы. При записи настроек приложение создает backup рядом с исходным JSON.
+
+## Установка из исходников
+
+Нужны:
+
+- Windows 10/11;
+- Node.js и npm;
+- Rust через rustup;
+- Visual Studio Build Tools с MSVC и Windows SDK;
+- Microsoft Edge WebView2 Runtime.
+
+Команды:
+
+```powershell
+cd D:\repos\ProxyWarden
+npm install
+npm run tauri -- dev
+```
+
+Собрать установочный пакет:
+
+```powershell
+npm run tauri -- build
+```
+
+Запустить только browser-preview без нативных Tauri-команд:
+
+```powershell
+npm run dev -- --host 127.0.0.1
+```
+
+В browser-preview можно проверить интерфейс, но нельзя управлять Windows-службами и нативными компонентами.
+
+## Явные installer-скрипты
+
+В репозитории есть отдельные entrypoint-скрипты:
+
+```powershell
+& .\scripts\install-control-app.ps1 -PlanOnly
+& .\scripts\install-proxyfier.ps1 -PlanOnly
+& .\scripts\install-singbox.ps1 -PlanOnly
+```
+
+`-PlanOnly` показывает план в JSON и ничего не устанавливает. Реальная установка требует прав администратора. Компоненты устанавливаются отдельно: применение профиля не должно незаметно устанавливать Control App, ProxiFyre или Local sing-box.
+
+## Проверка для разработчика
+
+Frontend:
+
+```powershell
+npm run build
+```
+
+Rust-тесты:
+
+```powershell
+cd D:\repos\ProxyWarden\src-tauri
+cargo test
+```
+
+Информация Tauri:
+
+```powershell
+npm run tauri -- info
+```
+
+## Ограничения текущей версии
+
+- Поддерживается основной путь через SOCKS5.
+- ProxiFyre является текущим backend-слоем маршрутизации; архитектура оставляет место для другого proxy-router adapter.
+- Local sing-box остается опциональным и не требуется для внешнего SOCKS5-прокси.
+- Реальные elevated-операции установки, удаления и управления Windows-службами нужно проверять на Windows с правами администратора.
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..1179dfc
--- /dev/null
+++ b/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ ProxyWarden
+
+
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..45f8033
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,2126 @@
+{
+ "name": "proxywarden",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "proxywarden",
+ "version": "0.1.0",
+ "dependencies": {
+ "@tauri-apps/api": "^2.0.0",
+ "@tauri-apps/plugin-dialog": "^2.7.1",
+ "lucide-react": "^1.23.0",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0"
+ },
+ "devDependencies": {
+ "@tauri-apps/cli": "^2.0.0",
+ "@types/react": "^19.0.0",
+ "@types/react-dom": "^19.0.0",
+ "@vitejs/plugin-react": "^5.0.0",
+ "typescript": "^5.8.0",
+ "vite": "^7.0.0"
+ }
+ },
+ "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.7",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
+ "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@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.7",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
+ "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.7"
+ },
+ "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.7",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
+ "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-globals": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
+ "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
+ "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/@esbuild/aix-ppc64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
+ "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
+ "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
+ "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
+ "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
+ "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
+ "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
+ "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
+ "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
+ "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
+ "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
+ "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
+ "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
+ "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
+ "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
+ "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
+ "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
+ "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
+ "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
+ "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
+ "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "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.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "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/@rolldown/pluginutils": {
+ "version": "1.0.0-rc.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
+ "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz",
+ "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz",
+ "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz",
+ "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz",
+ "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz",
+ "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz",
+ "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz",
+ "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz",
+ "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz",
+ "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz",
+ "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz",
+ "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz",
+ "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz",
+ "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz",
+ "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz",
+ "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz",
+ "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz",
+ "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz",
+ "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz",
+ "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz",
+ "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz",
+ "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz",
+ "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz",
+ "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz",
+ "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz",
+ "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "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,
+ "libc": [
+ "glibc"
+ ],
+ "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,
+ "libc": [
+ "musl"
+ ],
+ "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,
+ "libc": [
+ "glibc"
+ ],
+ "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,
+ "libc": [
+ "glibc"
+ ],
+ "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,
+ "libc": [
+ "musl"
+ ],
+ "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-dialog": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.7.1.tgz",
+ "integrity": "sha512-OK1UBXYt+ojcmxMktzzuyonYIFta8CmAASpX+CA+DTGK24KlHjhYI6x2iOJ/TjZF4N7/ACK1oFmEOjIY9IhzOQ==",
+ "license": "MIT OR Apache-2.0",
+ "dependencies": {
+ "@tauri-apps/api": "^2.11.0"
+ }
+ },
+ "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/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/react": {
+ "version": "19.2.17",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
+ "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz",
+ "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.29.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-rc.3",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.18.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.41",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.41.tgz",
+ "integrity": "sha512-WwS7MHhqGHHlaVsqRZnhvCEMS0owDX+SxRlve7JkuH7My1Ara3ZriTmCQupPfYjxMZ8I/tgxtJYr2t7taHaH4A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.4",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz",
+ "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==",
+ "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.10.38",
+ "caniuse-lite": "^1.0.30001799",
+ "electron-to-chromium": "^1.5.376",
+ "node-releases": "^2.0.48",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001800",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001800.tgz",
+ "integrity": "sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==",
+ "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/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/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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/electron-to-chromium": {
+ "version": "1.5.385",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.385.tgz",
+ "integrity": "sha512-78sa/M08MNAYHQfjoWMvOlKQqZ0ElhSm/L5HNUc96VZ3b+KvDVnngFm8sYQy0XrhTRgAhggHr5abA7yTvRdo4Q==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/esbuild": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
+ "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.28.1",
+ "@esbuild/android-arm": "0.28.1",
+ "@esbuild/android-arm64": "0.28.1",
+ "@esbuild/android-x64": "0.28.1",
+ "@esbuild/darwin-arm64": "0.28.1",
+ "@esbuild/darwin-x64": "0.28.1",
+ "@esbuild/freebsd-arm64": "0.28.1",
+ "@esbuild/freebsd-x64": "0.28.1",
+ "@esbuild/linux-arm": "0.28.1",
+ "@esbuild/linux-arm64": "0.28.1",
+ "@esbuild/linux-ia32": "0.28.1",
+ "@esbuild/linux-loong64": "0.28.1",
+ "@esbuild/linux-mips64el": "0.28.1",
+ "@esbuild/linux-ppc64": "0.28.1",
+ "@esbuild/linux-riscv64": "0.28.1",
+ "@esbuild/linux-s390x": "0.28.1",
+ "@esbuild/linux-x64": "0.28.1",
+ "@esbuild/netbsd-arm64": "0.28.1",
+ "@esbuild/netbsd-x64": "0.28.1",
+ "@esbuild/openbsd-arm64": "0.28.1",
+ "@esbuild/openbsd-x64": "0.28.1",
+ "@esbuild/openharmony-arm64": "0.28.1",
+ "@esbuild/sunos-x64": "0.28.1",
+ "@esbuild/win32-arm64": "0.28.1",
+ "@esbuild/win32-ia32": "0.28.1",
+ "@esbuild/win32-x64": "0.28.1"
+ }
+ },
+ "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/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/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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/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.23.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.23.0.tgz",
+ "integrity": "sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.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.15",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
+ "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
+ "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.50",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz",
+ "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "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.5",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
+ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.16",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
+ "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
+ "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.12",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.7",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
+ "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.7",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
+ "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.7"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
+ "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz",
+ "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==",
+ "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": {
+ "@rollup/rollup-android-arm-eabi": "4.62.2",
+ "@rollup/rollup-android-arm64": "4.62.2",
+ "@rollup/rollup-darwin-arm64": "4.62.2",
+ "@rollup/rollup-darwin-x64": "4.62.2",
+ "@rollup/rollup-freebsd-arm64": "4.62.2",
+ "@rollup/rollup-freebsd-x64": "4.62.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.62.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.62.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.62.2",
+ "@rollup/rollup-linux-arm64-musl": "4.62.2",
+ "@rollup/rollup-linux-loong64-gnu": "4.62.2",
+ "@rollup/rollup-linux-loong64-musl": "4.62.2",
+ "@rollup/rollup-linux-ppc64-gnu": "4.62.2",
+ "@rollup/rollup-linux-ppc64-musl": "4.62.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.62.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.62.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.62.2",
+ "@rollup/rollup-linux-x64-gnu": "4.62.2",
+ "@rollup/rollup-linux-x64-musl": "4.62.2",
+ "@rollup/rollup-openbsd-x64": "4.62.2",
+ "@rollup/rollup-openharmony-arm64": "4.62.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.62.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.62.2",
+ "@rollup/rollup-win32-x64-gnu": "4.62.2",
+ "@rollup/rollup-win32-x64-msvc": "4.62.2",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "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/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/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/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "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/vite": {
+ "version": "7.3.6",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz",
+ "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "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/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
new file mode 100644
index 0000000..3018c15
--- /dev/null
+++ b/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "proxywarden",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "description": "Standalone Windows desktop proxy management app for ProxyWarden.",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview",
+ "tauri": "tauri"
+ },
+ "dependencies": {
+ "@tauri-apps/api": "^2.0.0",
+ "@tauri-apps/plugin-dialog": "^2.7.1",
+ "lucide-react": "^1.23.0",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0"
+ },
+ "devDependencies": {
+ "@tauri-apps/cli": "^2.0.0",
+ "@types/react": "^19.0.0",
+ "@types/react-dom": "^19.0.0",
+ "@vitejs/plugin-react": "^5.0.0",
+ "typescript": "^5.8.0",
+ "vite": "^7.0.0"
+ }
+}
diff --git a/scripts/install-control-app.ps1 b/scripts/install-control-app.ps1
new file mode 100644
index 0000000..a49e460
--- /dev/null
+++ b/scripts/install-control-app.ps1
@@ -0,0 +1,79 @@
+param(
+ [string]$InstallRoot = "C:\Program Files\ProxyWarden\ControlApp",
+ [string]$DataRoot = "C:\ProgramData\ProxyWarden",
+ [switch]$PlanOnly,
+ [switch]$Force
+)
+
+$ErrorActionPreference = "Stop"
+
+function New-Result {
+ param(
+ [bool]$Success,
+ [string]$Action,
+ [bool]$Changed,
+ [string]$Message,
+ [hashtable]$Details = @{}
+ )
+
+ [ordered]@{
+ success = $Success
+ action = $Action
+ changed = $Changed
+ message = $Message
+ details = $Details
+ } | ConvertTo-Json -Depth 6
+}
+
+function Test-IsAdministrator {
+ $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
+ $principal = [Security.Principal.WindowsPrincipal]::new($identity)
+ $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
+}
+
+function Ensure-Directory {
+ param([string]$Path)
+ if (-not (Test-Path -LiteralPath $Path)) {
+ New-Item -ItemType Directory -Path $Path -Force | Out-Null
+ return $true
+ }
+ return $false
+}
+
+try {
+ $details = @{
+ installRoot = $InstallRoot
+ dataRoot = $DataRoot
+ planOnly = [bool]$PlanOnly
+ }
+
+ if ($PlanOnly) {
+ New-Result -Success $true -Action "install-control-app" -Changed $false -Message "Control App install plan is ready." -Details $details
+ exit 0
+ }
+
+ if (-not (Test-IsAdministrator)) {
+ New-Result -Success $false -Action "install-control-app" -Changed $false -Message "Administrator rights are required." -Details $details
+ exit 1
+ }
+
+ $changed = $false
+ $changed = (Ensure-Directory -Path $InstallRoot) -or $changed
+ $changed = (Ensure-Directory -Path (Join-Path $DataRoot "config")) -or $changed
+ $changed = (Ensure-Directory -Path (Join-Path $DataRoot "state")) -or $changed
+ $changed = (Ensure-Directory -Path (Join-Path $DataRoot "generated")) -or $changed
+
+ $markerPath = Join-Path $InstallRoot "install-control-app.marker.json"
+ if ((-not (Test-Path -LiteralPath $markerPath)) -or $Force) {
+ @{ component = "control-app"; installedAt = (Get-Date).ToString("o") } |
+ ConvertTo-Json -Depth 4 |
+ Set-Content -LiteralPath $markerPath -Encoding UTF8
+ $changed = $true
+ }
+
+ $details.markerPath = $markerPath
+ New-Result -Success $true -Action "install-control-app" -Changed $changed -Message "Control App directories are installed." -Details $details
+} catch {
+ New-Result -Success $false -Action "install-control-app" -Changed $false -Message $_.Exception.Message
+ exit 1
+}
diff --git a/scripts/install-proxyfier.ps1 b/scripts/install-proxyfier.ps1
new file mode 100644
index 0000000..521c438
--- /dev/null
+++ b/scripts/install-proxyfier.ps1
@@ -0,0 +1,96 @@
+param(
+ [string]$InstallRoot = "C:\Tools\ProxiFyre",
+ [string]$PackagePath = "",
+ [string]$ServiceName = "ProxiFyreService",
+ [switch]$PlanOnly,
+ [switch]$Force
+)
+
+$ErrorActionPreference = "Stop"
+
+function New-Result {
+ param(
+ [bool]$Success,
+ [string]$Action,
+ [bool]$Changed,
+ [string]$Message,
+ [hashtable]$Details = @{}
+ )
+
+ [ordered]@{
+ success = $Success
+ action = $Action
+ changed = $Changed
+ message = $Message
+ details = $Details
+ } | ConvertTo-Json -Depth 6
+}
+
+function Test-IsAdministrator {
+ $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
+ $principal = [Security.Principal.WindowsPrincipal]::new($identity)
+ $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
+}
+
+function Backup-File {
+ param([string]$Path)
+ if (Test-Path -LiteralPath $Path) {
+ $backup = "$Path.bak"
+ Copy-Item -LiteralPath $Path -Destination $backup -Force
+ return $backup
+ }
+ return $null
+}
+
+try {
+ $details = @{
+ installRoot = $InstallRoot
+ packagePath = $PackagePath
+ serviceName = $ServiceName
+ planOnly = [bool]$PlanOnly
+ }
+
+ if ($PlanOnly) {
+ New-Result -Success $true -Action "install-proxyfier" -Changed $false -Message "Proxyfier install plan is ready." -Details $details
+ exit 0
+ }
+
+ if (-not (Test-IsAdministrator)) {
+ New-Result -Success $false -Action "install-proxyfier" -Changed $false -Message "Administrator rights are required." -Details $details
+ exit 1
+ }
+
+ if ([string]::IsNullOrWhiteSpace($PackagePath) -or -not (Test-Path -LiteralPath $PackagePath)) {
+ New-Result -Success $false -Action "install-proxyfier" -Changed $false -Message "PackagePath is required and must point to a local ProxiFyre package." -Details $details
+ exit 2
+ }
+
+ $changed = $false
+ if (-not (Test-Path -LiteralPath $InstallRoot)) {
+ New-Item -ItemType Directory -Path $InstallRoot -Force | Out-Null
+ $changed = $true
+ }
+
+ $configPath = Join-Path $InstallRoot "app-config.json"
+ $backupPath = Backup-File -Path $configPath
+ if ($backupPath) {
+ $details.backupPath = $backupPath
+ }
+
+ $markerPath = Join-Path $InstallRoot "install-proxyfier.marker.json"
+ if ((-not (Test-Path -LiteralPath $markerPath)) -or $Force) {
+ @{
+ component = "proxyfier"
+ packagePath = $PackagePath
+ serviceName = $ServiceName
+ installedAt = (Get-Date).ToString("o")
+ } | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $markerPath -Encoding UTF8
+ $changed = $true
+ }
+
+ $details.markerPath = $markerPath
+ New-Result -Success $true -Action "install-proxyfier" -Changed $changed -Message "Proxyfier install boundary completed." -Details $details
+} catch {
+ New-Result -Success $false -Action "install-proxyfier" -Changed $false -Message $_.Exception.Message
+ exit 1
+}
diff --git a/scripts/install-singbox.ps1 b/scripts/install-singbox.ps1
new file mode 100644
index 0000000..594eaf8
--- /dev/null
+++ b/scripts/install-singbox.ps1
@@ -0,0 +1,270 @@
+param(
+ [string]$InstallRoot = "C:\Program Files\ProxyWarden\sing-box",
+ [string]$ServiceName = "ProxyWardenSingBox",
+ [string]$ConfigSource = "C:\ProgramData\ProxyWarden\generated\sing-box-config.json",
+ [switch]$PlanOnly,
+ [switch]$Force,
+ [switch]$Uninstall
+)
+
+$ErrorActionPreference = "Stop"
+
+$SingBoxReleaseApi = "https://api.github.com/repos/SagerNet/sing-box/releases/latest"
+$WinSwReleaseApi = "https://api.github.com/repos/winsw/winsw/releases/latest"
+$WrapperFile = "$ServiceName.exe"
+$ConfigFile = "config.json"
+
+function New-Result {
+ param(
+ [bool]$Success,
+ [string]$Action,
+ [bool]$Changed,
+ [string]$Message,
+ [hashtable]$Details = @{}
+ )
+
+ [ordered]@{
+ success = $Success
+ action = $Action
+ changed = $Changed
+ message = $Message
+ details = $Details
+ } | ConvertTo-Json -Depth 8
+}
+
+function Test-IsAdministrator {
+ $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
+ $principal = [Security.Principal.WindowsPrincipal]::new($identity)
+ $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
+}
+
+function Get-NativeArchitecture {
+ $processor = Get-CimInstance Win32_Processor | Select-Object -First 1
+ if ($null -ne $processor -and $processor.Architecture -eq 12) { return "arm64" }
+ if ([Environment]::Is64BitOperatingSystem) { return "amd64" }
+ return "386"
+}
+
+function Get-WinSwArchitecture {
+ param([string]$Arch)
+ if ($Arch -eq "arm64") { return "arm64" }
+ if ($Arch -eq "386") { return "x86" }
+ return "x64"
+}
+
+function Invoke-Download {
+ param([string]$Uri, [string]$Path)
+ Invoke-WebRequest -UseBasicParsing -Uri $Uri -OutFile $Path -Headers @{ "User-Agent" = "proxywarden" }
+}
+
+function Select-Asset {
+ param(
+ [object[]]$Assets,
+ [string]$Pattern,
+ [string]$Label
+ )
+
+ $asset = $Assets | Where-Object { $_.name -match $Pattern } | Select-Object -First 1
+ if ($null -eq $asset) {
+ throw "Не найден release asset для $Label по шаблону $Pattern."
+ }
+ return $asset
+}
+
+function Test-SafeInstallRoot {
+ param([string]$Path)
+ $full = [System.IO.Path]::GetFullPath($Path).TrimEnd("\")
+ $leaf = Split-Path -Leaf $full
+ $parent = Split-Path -Parent $full
+ if ($leaf -ne "sing-box") { return $false }
+ return $parent -match "\\ProxyWarden$|\\proxywarden$"
+}
+
+function Backup-File {
+ param([string]$Path)
+ if (Test-Path -LiteralPath $Path) {
+ $backup = "$Path.bak"
+ Copy-Item -LiteralPath $Path -Destination $backup -Force
+ return $backup
+ }
+ return $null
+}
+
+function Write-Utf8NoBomFile {
+ param(
+ [string]$Path,
+ [string]$Value
+ )
+
+ $encoding = New-Object System.Text.UTF8Encoding $false
+ [System.IO.File]::WriteAllText($Path, $Value, $encoding)
+}
+
+function Write-WinSwConfig {
+ param(
+ [string]$Root,
+ [string]$Name
+ )
+
+ $xmlPath = Join-Path $Root "$Name.xml"
+ $logDir = Join-Path $Root "logs"
+ New-Item -ItemType Directory -Path $logDir -Force | Out-Null
+ $xml = @"
+
+ $Name
+ ProxyWarden Local sing-box
+ Local sing-box runtime managed by ProxyWarden.
+ %BASE%\sing-box.exe
+ run -c "%BASE%\config.json"
+ %BASE%\logs
+
+ 10485760
+ 4
+
+
+
+"@
+ Write-Utf8NoBomFile -Path $xmlPath -Value $xml
+ return $xmlPath
+}
+
+function Stop-And-Uninstall-Service {
+ param(
+ [string]$Root,
+ [string]$Name
+ )
+
+ $wrapper = Join-Path $Root "$Name.exe"
+ $service = Get-Service -Name $Name -ErrorAction SilentlyContinue
+ if ($null -ne $service -and $service.Status -ne "Stopped") {
+ Stop-Service -Name $Name -Force -ErrorAction SilentlyContinue
+ $service = Get-Service -Name $Name -ErrorAction SilentlyContinue
+ if ($null -ne $service) {
+ try { $service.WaitForStatus("Stopped", [TimeSpan]::FromSeconds(15)) } catch {}
+ }
+ }
+
+ if (Test-Path -LiteralPath $wrapper) {
+ Push-Location $Root
+ try { & $wrapper uninstall | Out-Null } finally { Pop-Location }
+ }
+
+ $service = Get-Service -Name $Name -ErrorAction SilentlyContinue
+ if ($null -ne $service) {
+ sc.exe delete $Name | Out-Null
+ }
+}
+
+try {
+ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+ $installRootFull = [System.IO.Path]::GetFullPath($InstallRoot)
+ $details = @{
+ installRoot = $installRootFull
+ serviceName = $ServiceName
+ configSource = $ConfigSource
+ singboxReleaseApi = $SingBoxReleaseApi
+ winswReleaseApi = $WinSwReleaseApi
+ planOnly = [bool]$PlanOnly
+ uninstall = [bool]$Uninstall
+ }
+
+ if ($PlanOnly) {
+ $details.items = @(
+ @{ id = "sing-box-binary"; name = "sing-box.exe"; source = $SingBoxReleaseApi; target = (Join-Path $installRootFull "sing-box.exe") },
+ @{ id = "winsw-wrapper"; name = $WrapperFile; source = $WinSwReleaseApi; target = (Join-Path $installRootFull $WrapperFile) },
+ @{ id = "windows-service"; name = $ServiceName; target = "Windows Service" },
+ @{ id = "config"; name = $ConfigFile; source = $ConfigSource; target = (Join-Path $installRootFull $ConfigFile) }
+ )
+ New-Result -Success $true -Action "install-singbox.plan" -Changed $false -Message "Local sing-box install plan is ready." -Details $details
+ exit 0
+ }
+
+ if (-not (Test-IsAdministrator)) {
+ New-Result -Success $false -Action "install-singbox" -Changed $false -Message "Administrator rights are required." -Details $details
+ exit 1
+ }
+
+ if ($Uninstall) {
+ if (-not (Test-SafeInstallRoot -Path $installRootFull)) {
+ New-Result -Success $false -Action "uninstall-singbox" -Changed $false -Message "Unsafe InstallRoot for recursive uninstall." -Details $details
+ exit 2
+ }
+
+ Stop-And-Uninstall-Service -Root $installRootFull -Name $ServiceName
+ if (Test-Path -LiteralPath $installRootFull) {
+ Remove-Item -LiteralPath $installRootFull -Recurse -Force
+ }
+ New-Result -Success $true -Action "uninstall-singbox" -Changed $true -Message "Local sing-box service and install folder were removed." -Details $details
+ exit 0
+ }
+
+ $changed = $false
+ New-Item -ItemType Directory -Path $installRootFull -Force | Out-Null
+ $workDir = Join-Path ([System.IO.Path]::GetTempPath()) ("proxywarden-singbox-" + [guid]::NewGuid().ToString("N"))
+ $extractDir = Join-Path $workDir "extract"
+ New-Item -ItemType Directory -Path $extractDir -Force | Out-Null
+
+ try {
+ $arch = Get-NativeArchitecture
+ $winswArch = Get-WinSwArchitecture -Arch $arch
+ $details.architecture = $arch
+ $details.winswArchitecture = $winswArch
+
+ $singboxRelease = Invoke-RestMethod -Uri $SingBoxReleaseApi -Headers @{ "User-Agent" = "proxywarden" }
+ $singboxAsset = Select-Asset $singboxRelease.assets "windows-$arch\.zip$" "sing-box"
+ $singboxZip = Join-Path $workDir $singboxAsset.name
+ Invoke-Download $singboxAsset.browser_download_url $singboxZip
+ Expand-Archive -LiteralPath $singboxZip -DestinationPath $extractDir -Force
+ $singboxExe = Get-ChildItem -LiteralPath $extractDir -Recurse -Filter "sing-box.exe" | Select-Object -First 1
+ if ($null -eq $singboxExe) { throw "В архиве sing-box не найден sing-box.exe." }
+ Copy-Item -LiteralPath $singboxExe.FullName -Destination (Join-Path $installRootFull "sing-box.exe") -Force
+ $changed = $true
+
+ $winswRelease = Invoke-RestMethod -Uri $WinSwReleaseApi -Headers @{ "User-Agent" = "proxywarden" }
+ $winswAsset = Select-Asset $winswRelease.assets "WinSW-$winswArch\.exe$" "WinSW"
+ Invoke-Download $winswAsset.browser_download_url (Join-Path $installRootFull $WrapperFile)
+ $changed = $true
+
+ $configTarget = Join-Path $installRootFull $ConfigFile
+ $backupPath = Backup-File -Path $configTarget
+ if ($backupPath) { $details.backupPath = $backupPath }
+ if (Test-Path -LiteralPath $ConfigSource) {
+ Copy-Item -LiteralPath $ConfigSource -Destination $configTarget -Force
+ } elseif (-not (Test-Path -LiteralPath $configTarget)) {
+ Write-Utf8NoBomFile -Path $configTarget -Value '{"log":{"level":"info","timestamp":true},"inbounds":[],"outbounds":[{"type":"direct","tag":"direct"}],"route":{"final":"direct"}}'
+ }
+
+ $xmlPath = Write-WinSwConfig -Root $installRootFull -Name $ServiceName
+ $details.configPath = $configTarget
+ $details.wrapperConfigPath = $xmlPath
+
+ if ($Force) {
+ Stop-And-Uninstall-Service -Root $installRootFull -Name $ServiceName
+ }
+
+ Push-Location $installRootFull
+ try {
+ $service = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
+ if ($null -eq $service) {
+ & ".\$WrapperFile" install
+ if ($LASTEXITCODE -ne 0) { throw "WinSW install завершился с кодом $LASTEXITCODE." }
+ $changed = $true
+ }
+ & ".\$WrapperFile" start
+ if ($LASTEXITCODE -ne 0) {
+ Start-Service -Name $ServiceName -ErrorAction Stop
+ }
+ } finally {
+ Pop-Location
+ }
+ } finally {
+ if (Test-Path -LiteralPath $workDir) {
+ Remove-Item -LiteralPath $workDir -Recurse -Force -ErrorAction SilentlyContinue
+ }
+ }
+
+ New-Result -Success $true -Action "install-singbox" -Changed $changed -Message "Local sing-box service is installed and started." -Details $details
+} catch {
+ New-Result -Success $false -Action "install-singbox" -Changed $false -Message $_.Exception.Message
+ exit 1
+}
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
new file mode 100644
index 0000000..e0c66e8
--- /dev/null
+++ b/src-tauri/Cargo.lock
@@ -0,0 +1,4850 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "alloc-no-stdlib"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
+
+[[package]]
+name = "alloc-stdlib"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195"
+dependencies = [
+ "alloc-no-stdlib",
+]
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
+
+[[package]]
+name = "atk"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b"
+dependencies = [
+ "atk-sys",
+ "glib",
+ "libc",
+]
+
+[[package]]
+name = "atk-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+
+[[package]]
+name = "base64"
+version = "0.21.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bit-set"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
+dependencies = [
+ "bit-vec",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
+
+[[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.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
+dependencies = [
+ "serde_core",
+]
+
+[[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 = "block2"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
+dependencies = [
+ "objc2",
+]
+
+[[package]]
+name = "brotli"
+version = "8.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+ "brotli-decompressor",
+]
+
+[[package]]
+name = "brotli-decompressor"
+version = "5.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+]
+
+[[package]]
+name = "bs58"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4"
+dependencies = [
+ "tinyvec",
+]
+
+[[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.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "cairo-rs"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2"
+dependencies = [
+ "bitflags 2.13.0",
+ "cairo-sys-rs",
+ "glib",
+ "libc",
+ "once_cell",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "cairo-sys-rs"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "camino"
+version = "1.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f2d30e4173c4026932d51d31d6b0613b1fd3014bf3f9f8943d4ba139c437ba0"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "cargo-platform"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "cargo_metadata"
+version = "0.19.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba"
+dependencies = [
+ "camino",
+ "cargo-platform",
+ "semver",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "cargo_toml"
+version = "0.22.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77"
+dependencies = [
+ "serde",
+ "toml 0.9.12+spec-1.1.0",
+]
+
+[[package]]
+name = "cc"
+version = "1.2.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cesu8"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+
+[[package]]
+name = "cfb"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f"
+dependencies = [
+ "byteorder",
+ "fnv",
+ "uuid",
+]
+
+[[package]]
+name = "cfg-expr"
+version = "0.15.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
+dependencies = [
+ "smallvec",
+ "target-lexicon",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "chacha20"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.3.0",
+ "rand_core",
+]
+
+[[package]]
+name = "chrono"
+version = "0.4.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
+dependencies = [
+ "iana-time-zone",
+ "num-traits",
+ "serde",
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "combine"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
+[[package]]
+name = "cookie"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
+dependencies = [
+ "time",
+ "version_check",
+]
+
+[[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.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97"
+dependencies = [
+ "bitflags 2.13.0",
+ "core-foundation",
+ "core-graphics-types",
+ "foreign-types",
+ "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.0",
+ "core-foundation",
+ "libc",
+]
+
+[[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.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[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 = "cssparser"
+version = "0.36.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2"
+dependencies = [
+ "cssparser-macros",
+ "dtoa-short",
+ "itoa",
+ "phf",
+ "smallvec",
+]
+
+[[package]]
+name = "cssparser-macros"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
+dependencies = [
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "ctor"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98"
+dependencies = [
+ "ctor-proc-macro",
+ "dtor",
+]
+
+[[package]]
+name = "ctor-proc-macro"
+version = "0.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1"
+
+[[package]]
+name = "darling"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
+dependencies = [
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "dbus"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e"
+dependencies = [
+ "libc",
+ "libdbus-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+dependencies = [
+ "serde_core",
+]
+
+[[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 = [
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "dirs"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
+dependencies = [
+ "dirs-sys",
+]
+
+[[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",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "dispatch2"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
+dependencies = [
+ "bitflags 2.13.0",
+ "block2",
+ "libc",
+ "objc2",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "dlopen2"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4"
+dependencies = [
+ "dlopen2_derive",
+ "libc",
+ "once_cell",
+ "winapi",
+]
+
+[[package]]
+name = "dlopen2_derive"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "dom_query"
+version = "0.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89"
+dependencies = [
+ "bit-set",
+ "cssparser",
+ "foldhash",
+ "html5ever",
+ "precomputed-hash",
+ "selectors",
+ "tendril",
+]
+
+[[package]]
+name = "dpi"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "dtoa"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590"
+
+[[package]]
+name = "dtoa-short"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87"
+dependencies = [
+ "dtoa",
+]
+
+[[package]]
+name = "dtor"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4"
+dependencies = [
+ "dtor-proc-macro",
+]
+
+[[package]]
+name = "dtor-proc-macro"
+version = "0.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5"
+
+[[package]]
+name = "dunce"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
+
+[[package]]
+name = "dyn-clone"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
+
+[[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.2+spec-1.1.0",
+ "vswhom",
+ "winreg",
+]
+
+[[package]]
+name = "embed_plist"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
+
+[[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 = "fastrand"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
+
+[[package]]
+name = "fdeflate"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
+dependencies = [
+ "simd-adler32",
+]
+
+[[package]]
+name = "field-offset"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f"
+dependencies = [
+ "memoffset",
+ "rustc_version",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "flate2"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foldhash"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+
+[[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.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[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 = "futures-channel"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
+
+[[package]]
+name = "futures-task"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+
+[[package]]
+name = "futures-util"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+dependencies = [
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "gdk"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691"
+dependencies = [
+ "cairo-rs",
+ "gdk-pixbuf",
+ "gdk-sys",
+ "gio",
+ "glib",
+ "libc",
+ "pango",
+]
+
+[[package]]
+name = "gdk-pixbuf"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec"
+dependencies = [
+ "gdk-pixbuf-sys",
+ "gio",
+ "glib",
+ "libc",
+ "once_cell",
+]
+
+[[package]]
+name = "gdk-pixbuf-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7"
+dependencies = [
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gdk-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7"
+dependencies = [
+ "cairo-sys-rs",
+ "gdk-pixbuf-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pango-sys",
+ "pkg-config",
+ "system-deps",
+]
+
+[[package]]
+name = "gdkwayland-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69"
+dependencies = [
+ "gdk-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pkg-config",
+ "system-deps",
+]
+
+[[package]]
+name = "gdkx11"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe"
+dependencies = [
+ "gdk",
+ "gdkx11-sys",
+ "gio",
+ "glib",
+ "libc",
+ "x11",
+]
+
+[[package]]
+name = "gdkx11-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d"
+dependencies = [
+ "gdk-sys",
+ "glib-sys",
+ "libc",
+ "system-deps",
+ "x11",
+]
+
+[[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 = "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",
+ "libc",
+ "r-efi 5.3.0",
+ "wasip2",
+]
+
+[[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",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "gio"
+version = "0.18.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "gio-sys",
+ "glib",
+ "libc",
+ "once_cell",
+ "pin-project-lite",
+ "smallvec",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "gio-sys"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+ "winapi",
+]
+
+[[package]]
+name = "glib"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5"
+dependencies = [
+ "bitflags 2.13.0",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-task",
+ "futures-util",
+ "gio-sys",
+ "glib-macros",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "memchr",
+ "once_cell",
+ "smallvec",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "glib-macros"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc"
+dependencies = [
+ "heck 0.4.1",
+ "proc-macro-crate 2.0.2",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "glib-sys"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898"
+dependencies = [
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
+
+[[package]]
+name = "gobject-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gtk"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a"
+dependencies = [
+ "atk",
+ "cairo-rs",
+ "field-offset",
+ "futures-channel",
+ "gdk",
+ "gdk-pixbuf",
+ "gio",
+ "glib",
+ "gtk-sys",
+ "gtk3-macros",
+ "libc",
+ "pango",
+ "pkg-config",
+]
+
+[[package]]
+name = "gtk-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414"
+dependencies = [
+ "atk-sys",
+ "cairo-sys-rs",
+ "gdk-pixbuf-sys",
+ "gdk-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pango-sys",
+ "system-deps",
+]
+
+[[package]]
+name = "gtk3-macros"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d"
+dependencies = [
+ "proc-macro-crate 1.3.1",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[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 = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "html5ever"
+version = "0.38.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2"
+dependencies = [
+ "log",
+ "markup5ever",
+]
+
+[[package]]
+name = "http"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+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 = "hyper"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "http",
+ "http-body",
+ "httparse",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+ "webpki-roots",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "ipnet",
+ "libc",
+ "percent-encoding",
+ "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 = "ico"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371"
+dependencies = [
+ "byteorder",
+ "png 0.17.16",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
+
+[[package]]
+name = "icu_properties"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
+
+[[package]]
+name = "icu_provider"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[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 = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown 0.12.3",
+ "serde",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.17.1",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "infer"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7"
+dependencies = [
+ "cfb",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "javascriptcore-rs"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc"
+dependencies = [
+ "bitflags 1.3.2",
+ "glib",
+ "javascriptcore-rs-sys",
+]
+
+[[package]]
+name = "javascriptcore-rs-sys"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "jni"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
+dependencies = [
+ "cesu8",
+ "cfg-if",
+ "combine",
+ "jni-sys 0.3.1",
+ "log",
+ "thiserror 1.0.69",
+ "walkdir",
+ "windows-sys 0.45.0",
+]
+
+[[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.118",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "json-patch"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08"
+dependencies = [
+ "jsonptr",
+ "serde",
+ "serde_json",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "jsonptr"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "keyboard-types"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a"
+dependencies = [
+ "bitflags 2.13.0",
+ "serde",
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "libappindicator"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a"
+dependencies = [
+ "glib",
+ "gtk",
+ "gtk-sys",
+ "libappindicator-sys",
+ "log",
+]
+
+[[package]]
+name = "libappindicator-sys"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf"
+dependencies = [
+ "gtk-sys",
+ "libloading",
+ "once_cell",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.186"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+
+[[package]]
+name = "libdbus-sys"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043"
+dependencies = [
+ "pkg-config",
+]
+
+[[package]]
+name = "libloading"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
+dependencies = [
+ "cfg-if",
+ "winapi",
+]
+
+[[package]]
+name = "libredox"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "litemap"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
+
+[[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.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
+
+[[package]]
+name = "lru-slab"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
+
+[[package]]
+name = "markup5ever"
+version = "0.38.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862"
+dependencies = [
+ "log",
+ "tendril",
+ "web_atoms",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
+
+[[package]]
+name = "memoffset"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[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 = "mio"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "muda"
+version = "0.19.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878"
+dependencies = [
+ "crossbeam-channel",
+ "dpi",
+ "gtk",
+ "keyboard-types",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "once_cell",
+ "png 0.18.1",
+ "serde",
+ "thiserror 2.0.18",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "ndk"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
+dependencies = [
+ "bitflags 2.13.0",
+ "jni-sys 0.3.1",
+ "log",
+ "ndk-sys",
+ "num_enum",
+ "raw-window-handle",
+ "thiserror 1.0.69",
+]
+
+[[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 = "num-conv"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_enum"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"
+dependencies = [
+ "num_enum_derive",
+ "rustversion",
+]
+
+[[package]]
+name = "num_enum_derive"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
+dependencies = [
+ "proc-macro-crate 3.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "objc2"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
+dependencies = [
+ "objc2-encode",
+ "objc2-exception-helper",
+]
+
+[[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.0",
+ "block2",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-foundation",
+]
+
+[[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.0",
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-core-data"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
+dependencies = [
+ "objc2",
+ "objc2-foundation",
+]
+
+[[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.0",
+ "dispatch2",
+ "objc2",
+]
+
+[[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.0",
+ "dispatch2",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-io-surface",
+]
+
+[[package]]
+name = "objc2-core-image"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006"
+dependencies = [
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-core-location"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009"
+dependencies = [
+ "objc2",
+ "objc2-foundation",
+]
+
+[[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.0",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+]
+
+[[package]]
+name = "objc2-encode"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
+
+[[package]]
+name = "objc2-exception-helper"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "objc2-foundation"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
+dependencies = [
+ "bitflags 2.13.0",
+ "block2",
+ "libc",
+ "objc2",
+ "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.0",
+ "objc2",
+ "objc2-core-foundation",
+]
+
+[[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.0",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-ui-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22"
+dependencies = [
+ "bitflags 2.13.0",
+ "block2",
+ "objc2",
+ "objc2-cloud-kit",
+ "objc2-core-data",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-core-image",
+ "objc2-core-location",
+ "objc2-core-text",
+ "objc2-foundation",
+ "objc2-quartz-core",
+ "objc2-user-notifications",
+]
+
+[[package]]
+name = "objc2-user-notifications"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e"
+dependencies = [
+ "objc2",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-web-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f"
+dependencies = [
+ "bitflags 2.13.0",
+ "block2",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
+name = "pango"
+version = "0.18.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4"
+dependencies = [
+ "gio",
+ "glib",
+ "libc",
+ "once_cell",
+ "pango-sys",
+]
+
+[[package]]
+name = "pango-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[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 = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "phf"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
+dependencies = [
+ "phf_macros",
+ "phf_shared",
+ "serde",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
+dependencies = [
+ "fastrand",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
+
+[[package]]
+name = "plist"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1"
+dependencies = [
+ "base64 0.22.1",
+ "indexmap 2.14.0",
+ "quick-xml",
+ "serde",
+ "time",
+]
+
+[[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",
+]
+
+[[package]]
+name = "png"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
+dependencies = [
+ "bitflags 2.13.0",
+ "crc32fast",
+ "fdeflate",
+ "flate2",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "potential_utf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "precomputed-hash"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
+
+[[package]]
+name = "proc-macro-crate"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
+dependencies = [
+ "once_cell",
+ "toml_edit 0.19.15",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24"
+dependencies = [
+ "toml_datetime 0.6.3",
+ "toml_edit 0.20.2",
+]
+
+[[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.12+spec-1.1.0",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "proxywarden"
+version = "0.1.0"
+dependencies = [
+ "base64 0.22.1",
+ "reqwest 0.12.28",
+ "serde",
+ "serde_json",
+ "tauri",
+ "tauri-build",
+ "tauri-plugin-dialog",
+ "url",
+]
+
+[[package]]
+name = "quick-xml"
+version = "0.39.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "quinn"
+version = "0.11.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
+dependencies = [
+ "bytes",
+ "cfg_aliases",
+ "pin-project-lite",
+ "quinn-proto",
+ "quinn-udp",
+ "rustc-hash",
+ "rustls",
+ "socket2",
+ "thiserror 2.0.18",
+ "tokio",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-proto"
+version = "0.11.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560"
+dependencies = [
+ "bytes",
+ "getrandom 0.4.3",
+ "lru-slab",
+ "rand",
+ "rand_pcg",
+ "ring",
+ "rustc-hash",
+ "rustls",
+ "rustls-pki-types",
+ "slab",
+ "thiserror 2.0.18",
+ "tinyvec",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-udp"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
+dependencies = [
+ "cfg_aliases",
+ "libc",
+ "once_cell",
+ "socket2",
+ "tracing",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
+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.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
+dependencies = [
+ "chacha20",
+ "getrandom 0.4.3",
+ "rand_core",
+]
+
+[[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",
+]
+
+[[package]]
+name = "raw-window-handle"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags 2.13.0",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
+dependencies = [
+ "getrandom 0.2.17",
+ "libredox",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "ref-cast"
+version = "1.0.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d"
+dependencies = [
+ "ref-cast-impl",
+]
+
+[[package]]
+name = "ref-cast-impl"
+version = "1.0.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "regex"
+version = "1.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+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 = "reqwest"
+version = "0.12.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "percent-encoding",
+ "pin-project-lite",
+ "quinn",
+ "rustls",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-rustls",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "webpki-roots",
+]
+
+[[package]]
+name = "reqwest"
+version = "0.13.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde",
+ "serde_json",
+ "sync_wrapper",
+ "tokio",
+ "tokio-util",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams",
+ "web-sys",
+]
+
+[[package]]
+name = "rfd"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
+dependencies = [
+ "block2",
+ "dispatch2",
+ "glib-sys",
+ "gobject-sys",
+ "gtk-sys",
+ "js-sys",
+ "log",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "raw-window-handle",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "windows-sys 0.60.2",
+]
+
+[[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 = "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 = "rustls"
+version = "0.23.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f"
+dependencies = [
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046"
+dependencies = [
+ "web-time",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[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 = "schemars"
+version = "0.8.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
+dependencies = [
+ "dyn-clone",
+ "indexmap 1.9.3",
+ "schemars_derive",
+ "serde",
+ "serde_json",
+ "url",
+ "uuid",
+]
+
+[[package]]
+name = "schemars"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
+dependencies = [
+ "dyn-clone",
+ "ref-cast",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc"
+dependencies = [
+ "dyn-clone",
+ "ref-cast",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars_derive"
+version = "0.8.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde_derive_internals",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "selectors"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c"
+dependencies = [
+ "bitflags 2.13.0",
+ "cssparser",
+ "derive_more",
+ "log",
+ "new_debug_unreachable",
+ "phf",
+ "phf_codegen",
+ "precomputed-hash",
+ "rustc-hash",
+ "servo_arc",
+ "smallvec",
+]
+
+[[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.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde-untagged"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058"
+dependencies = [
+ "erased-serde",
+ "serde",
+ "serde_core",
+ "typeid",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "serde_derive_internals"
+version = "0.29.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.150"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_repr"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[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 = "serde_with"
+version = "3.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c"
+dependencies = [
+ "base64 0.22.1",
+ "bs58",
+ "chrono",
+ "hex",
+ "indexmap 1.9.3",
+ "indexmap 2.14.0",
+ "schemars 0.9.0",
+ "schemars 1.2.1",
+ "serde_core",
+ "serde_json",
+ "serde_with_macros",
+ "time",
+]
+
+[[package]]
+name = "serde_with_macros"
+version = "3.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "serialize-to-javascript"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5"
+dependencies = [
+ "serde",
+ "serde_json",
+ "serialize-to-javascript-impl",
+]
+
+[[package]]
+name = "serialize-to-javascript-impl"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "servo_arc"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930"
+dependencies = [
+ "stable_deref_trait",
+]
+
+[[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",
+]
+
+[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
+
+[[package]]
+name = "siphasher"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "smallvec"
+version = "1.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
+
+[[package]]
+name = "socket2"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "softbuffer"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3"
+dependencies = [
+ "bytemuck",
+ "js-sys",
+ "ndk",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-foundation",
+ "objc2-quartz-core",
+ "raw-window-handle",
+ "redox_syscall",
+ "tracing",
+ "wasm-bindgen",
+ "web-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "soup3"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f"
+dependencies = [
+ "futures-channel",
+ "gio",
+ "glib",
+ "libc",
+ "soup3-sys",
+]
+
+[[package]]
+name = "soup3-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27"
+dependencies = [
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "string_cache"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901"
+dependencies = [
+ "new_debug_unreachable",
+ "parking_lot",
+ "phf_shared",
+ "precomputed-hash",
+]
+
+[[package]]
+name = "string_cache_codegen"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "swift-rs"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7"
+dependencies = [
+ "base64 0.21.7",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.118"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
+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.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "system-deps"
+version = "6.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
+dependencies = [
+ "cfg-expr",
+ "heck 0.5.0",
+ "pkg-config",
+ "toml 0.8.2",
+ "version-compare",
+]
+
+[[package]]
+name = "tao"
+version = "0.35.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9"
+dependencies = [
+ "bitflags 2.13.0",
+ "block2",
+ "core-foundation",
+ "core-graphics",
+ "crossbeam-channel",
+ "dbus",
+ "dispatch2",
+ "dlopen2",
+ "dpi",
+ "gdkwayland-sys",
+ "gdkx11-sys",
+ "gtk",
+ "jni",
+ "libc",
+ "log",
+ "ndk",
+ "ndk-sys",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-foundation",
+ "objc2-ui-kit",
+ "once_cell",
+ "parking_lot",
+ "percent-encoding",
+ "raw-window-handle",
+ "tao-macros",
+ "unicode-segmentation",
+ "url",
+ "windows",
+ "windows-core 0.61.2",
+ "windows-version",
+ "x11-dl",
+]
+
+[[package]]
+name = "tao-macros"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "target-lexicon"
+version = "0.12.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
+
+[[package]]
+name = "tauri"
+version = "2.11.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "667b20e2726d572dea2de7370da16e188eb06008faf9a92fab7cdc46791190b5"
+dependencies = [
+ "anyhow",
+ "bytes",
+ "cookie",
+ "dirs",
+ "dunce",
+ "embed_plist",
+ "getrandom 0.3.4",
+ "glob",
+ "gtk",
+ "heck 0.5.0",
+ "http",
+ "jni",
+ "libc",
+ "log",
+ "mime",
+ "muda",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-foundation",
+ "objc2-ui-kit",
+ "objc2-web-kit",
+ "percent-encoding",
+ "plist",
+ "raw-window-handle",
+ "reqwest 0.13.4",
+ "serde",
+ "serde_json",
+ "serde_repr",
+ "serialize-to-javascript",
+ "swift-rs",
+ "tauri-build",
+ "tauri-macros",
+ "tauri-runtime",
+ "tauri-runtime-wry",
+ "tauri-utils",
+ "thiserror 2.0.18",
+ "tokio",
+ "tray-icon",
+ "url",
+ "webkit2gtk",
+ "webview2-com",
+ "window-vibrancy",
+ "windows",
+]
+
+[[package]]
+name = "tauri-build"
+version = "2.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632"
+dependencies = [
+ "anyhow",
+ "cargo_toml",
+ "dirs",
+ "glob",
+ "heck 0.5.0",
+ "json-patch",
+ "schemars 0.8.22",
+ "semver",
+ "serde",
+ "serde_json",
+ "tauri-utils",
+ "tauri-winres",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-codegen"
+version = "2.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5"
+dependencies = [
+ "base64 0.22.1",
+ "brotli",
+ "ico",
+ "json-patch",
+ "plist",
+ "png 0.17.16",
+ "proc-macro2",
+ "quote",
+ "semver",
+ "serde",
+ "serde_json",
+ "sha2",
+ "syn 2.0.118",
+ "tauri-utils",
+ "thiserror 2.0.18",
+ "time",
+ "url",
+ "uuid",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-macros"
+version = "2.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45"
+dependencies = [
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+ "tauri-codegen",
+ "tauri-utils",
+]
+
+[[package]]
+name = "tauri-plugin"
+version = "2.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020"
+dependencies = [
+ "anyhow",
+ "glob",
+ "plist",
+ "schemars 0.8.22",
+ "serde",
+ "serde_json",
+ "tauri-utils",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-plugin-dialog"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884"
+dependencies = [
+ "log",
+ "raw-window-handle",
+ "rfd",
+ "serde",
+ "serde_json",
+ "tauri",
+ "tauri-plugin",
+ "tauri-plugin-fs",
+ "thiserror 2.0.18",
+ "url",
+]
+
+[[package]]
+name = "tauri-plugin-fs"
+version = "2.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371"
+dependencies = [
+ "anyhow",
+ "dunce",
+ "glob",
+ "log",
+ "objc2-foundation",
+ "percent-encoding",
+ "schemars 0.8.22",
+ "serde",
+ "serde_json",
+ "serde_repr",
+ "tauri",
+ "tauri-plugin",
+ "tauri-utils",
+ "thiserror 2.0.18",
+ "toml 1.1.2+spec-1.1.0",
+ "url",
+]
+
+[[package]]
+name = "tauri-runtime"
+version = "2.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8"
+dependencies = [
+ "cookie",
+ "dpi",
+ "gtk",
+ "http",
+ "jni",
+ "objc2",
+ "objc2-ui-kit",
+ "objc2-web-kit",
+ "raw-window-handle",
+ "serde",
+ "serde_json",
+ "tauri-utils",
+ "thiserror 2.0.18",
+ "url",
+ "webkit2gtk",
+ "webview2-com",
+ "windows",
+]
+
+[[package]]
+name = "tauri-runtime-wry"
+version = "2.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f"
+dependencies = [
+ "gtk",
+ "http",
+ "jni",
+ "log",
+ "objc2",
+ "objc2-app-kit",
+ "once_cell",
+ "percent-encoding",
+ "raw-window-handle",
+ "softbuffer",
+ "tao",
+ "tauri-runtime",
+ "tauri-utils",
+ "url",
+ "webkit2gtk",
+ "webview2-com",
+ "windows",
+ "wry",
+]
+
+[[package]]
+name = "tauri-utils"
+version = "2.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887"
+dependencies = [
+ "anyhow",
+ "brotli",
+ "cargo_metadata",
+ "ctor",
+ "dom_query",
+ "dunce",
+ "glob",
+ "http",
+ "infer",
+ "json-patch",
+ "log",
+ "memchr",
+ "phf",
+ "plist",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "schemars 0.8.22",
+ "semver",
+ "serde",
+ "serde-untagged",
+ "serde_json",
+ "serde_with",
+ "swift-rs",
+ "thiserror 2.0.18",
+ "toml 1.1.2+spec-1.1.0",
+ "url",
+ "urlpattern",
+ "uuid",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-winres"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6"
+dependencies = [
+ "dunce",
+ "embed-resource",
+ "toml 1.1.2+spec-1.1.0",
+]
+
+[[package]]
+name = "tendril"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24"
+dependencies = [
+ "new_debug_unreachable",
+ "utf-8",
+]
+
+[[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.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl 2.0.18",
+]
+
+[[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.118",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "time"
+version = "0.3.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50"
+dependencies = [
+ "deranged",
+ "num-conv",
+ "powerfmt",
+ "serde_core",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
+
+[[package]]
+name = "time-macros"
+version = "0.2.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.52.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio",
+ "pin-project-lite",
+ "socket2",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "toml"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d"
+dependencies = [
+ "serde",
+ "serde_spanned 0.6.9",
+ "toml_datetime 0.6.3",
+ "toml_edit 0.20.2",
+]
+
+[[package]]
+name = "toml"
+version = "0.9.12+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
+dependencies = [
+ "indexmap 2.14.0",
+ "serde_core",
+ "serde_spanned 1.1.1",
+ "toml_datetime 0.7.5+spec-1.1.0",
+ "toml_parser",
+ "toml_writer",
+ "winnow 0.7.15",
+]
+
+[[package]]
+name = "toml"
+version = "1.1.2+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
+dependencies = [
+ "indexmap 2.14.0",
+ "serde_core",
+ "serde_spanned 1.1.1",
+ "toml_datetime 1.1.1+spec-1.1.0",
+ "toml_parser",
+ "toml_writer",
+ "winnow 1.0.3",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.7.5+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
+dependencies = [
+ "serde_core",
+]
+
+[[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.19.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
+dependencies = [
+ "indexmap 2.14.0",
+ "toml_datetime 0.6.3",
+ "winnow 0.5.40",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
+dependencies = [
+ "indexmap 2.14.0",
+ "serde",
+ "serde_spanned 0.6.9",
+ "toml_datetime 0.6.3",
+ "winnow 0.5.40",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.25.12+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7"
+dependencies = [
+ "indexmap 2.14.0",
+ "toml_datetime 1.1.1+spec-1.1.0",
+ "toml_parser",
+ "winnow 1.0.3",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.1.2+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
+dependencies = [
+ "winnow 1.0.3",
+]
+
+[[package]]
+name = "toml_writer"
+version = "1.1.1+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
+
+[[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-http"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
+dependencies = [
+ "bitflags 2.13.0",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "pin-project-lite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "url",
+]
+
+[[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 = [
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "tray-icon"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc"
+dependencies = [
+ "crossbeam-channel",
+ "dirs",
+ "libappindicator",
+ "muda",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-foundation",
+ "once_cell",
+ "png 0.18.1",
+ "serde",
+ "thiserror 2.0.18",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[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 = "unic-char-property"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221"
+dependencies = [
+ "unic-char-range",
+]
+
+[[package]]
+name = "unic-char-range"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc"
+
+[[package]]
+name = "unic-common"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
+
+[[package]]
+name = "unic-ucd-ident"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987"
+dependencies = [
+ "unic-char-property",
+ "unic-char-range",
+ "unic-ucd-version",
+]
+
+[[package]]
+name = "unic-ucd-version"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4"
+dependencies = [
+ "unic-common",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
+
+[[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",
+ "serde_derive",
+]
+
+[[package]]
+name = "urlpattern"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d"
+dependencies = [
+ "regex",
+ "serde",
+ "unic-ucd-ident",
+ "url",
+]
+
+[[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.23.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53"
+dependencies = [
+ "getrandom 0.4.3",
+ "js-sys",
+ "serde_core",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "version-compare"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e"
+
+[[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 = "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.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.76"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "wasm-streams"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141"
+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 = "web_atoms"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297"
+dependencies = [
+ "phf",
+ "phf_codegen",
+ "string_cache",
+ "string_cache_codegen",
+]
+
+[[package]]
+name = "webkit2gtk"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793"
+dependencies = [
+ "bitflags 1.3.2",
+ "cairo-rs",
+ "gdk",
+ "gdk-sys",
+ "gio",
+ "gio-sys",
+ "glib",
+ "glib-sys",
+ "gobject-sys",
+ "gtk",
+ "gtk-sys",
+ "javascriptcore-rs",
+ "libc",
+ "once_cell",
+ "soup3",
+ "webkit2gtk-sys",
+]
+
+[[package]]
+name = "webkit2gtk-sys"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5"
+dependencies = [
+ "bitflags 1.3.2",
+ "cairo-sys-rs",
+ "gdk-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "gtk-sys",
+ "javascriptcore-rs-sys",
+ "libc",
+ "pkg-config",
+ "soup3-sys",
+ "system-deps",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "webview2-com"
+version = "0.38.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a"
+dependencies = [
+ "webview2-com-macros",
+ "webview2-com-sys",
+ "windows",
+ "windows-core 0.61.2",
+ "windows-implement",
+ "windows-interface",
+]
+
+[[package]]
+name = "webview2-com-macros"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "webview2-com-sys"
+version = "0.38.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c"
+dependencies = [
+ "thiserror 2.0.18",
+ "windows",
+ "windows-core 0.61.2",
+]
+
+[[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 = "window-vibrancy"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c"
+dependencies = [
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "raw-window-handle",
+ "windows-sys 0.59.0",
+ "windows-version",
+]
+
+[[package]]
+name = "windows"
+version = "0.61.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
+dependencies = [
+ "windows-collections",
+ "windows-core 0.61.2",
+ "windows-future",
+ "windows-link 0.1.3",
+ "windows-numerics",
+]
+
+[[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-core"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "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",
+ "windows-interface",
+ "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",
+]
+
+[[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.118",
+]
+
+[[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.118",
+]
+
+[[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-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.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.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.2",
+]
+
+[[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.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.5",
+]
+
+[[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.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
+[[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-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.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
+[[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.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
+[[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.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
+[[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.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[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.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
+[[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.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
+[[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.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
+[[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.5.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "winnow"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
+
+[[package]]
+name = "winnow"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
+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 = "wit-bindgen"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+
+[[package]]
+name = "writeable"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
+
+[[package]]
+name = "wry"
+version = "0.55.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514"
+dependencies = [
+ "base64 0.22.1",
+ "block2",
+ "cookie",
+ "crossbeam-channel",
+ "dirs",
+ "dom_query",
+ "dpi",
+ "dunce",
+ "gdkx11",
+ "gtk",
+ "http",
+ "javascriptcore-rs",
+ "jni",
+ "libc",
+ "ndk",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "objc2-ui-kit",
+ "objc2-web-kit",
+ "once_cell",
+ "percent-encoding",
+ "raw-window-handle",
+ "sha2",
+ "soup3",
+ "tao-macros",
+ "thiserror 2.0.18",
+ "url",
+ "webkit2gtk",
+ "webkit2gtk-sys",
+ "webview2-com",
+ "windows",
+ "windows-core 0.61.2",
+ "windows-version",
+ "x11-dl",
+]
+
+[[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-dl"
+version = "2.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f"
+dependencies = [
+ "libc",
+ "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.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+ "synstructure",
+]
+
+[[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.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
+
+[[package]]
+name = "zerotrie"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.118",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
new file mode 100644
index 0000000..2b44b96
--- /dev/null
+++ b/src-tauri/Cargo.toml
@@ -0,0 +1,22 @@
+[package]
+name = "proxywarden"
+version = "0.1.0"
+description = "Standalone Windows desktop proxy management app for ProxyWarden."
+authors = ["ProxyWarden"]
+edition = "2021"
+
+[lib]
+name = "proxywarden_lib"
+crate-type = ["staticlib", "cdylib", "rlib"]
+
+[build-dependencies]
+tauri-build = { version = "2", features = [] }
+
+[dependencies]
+tauri = { version = "2", features = [] }
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+tauri-plugin-dialog = "2.7.1"
+base64 = "0.22"
+reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls"] }
+url = "2"
diff --git a/src-tauri/build.rs b/src-tauri/build.rs
new file mode 100644
index 0000000..261851f
--- /dev/null
+++ b/src-tauri/build.rs
@@ -0,0 +1,3 @@
+fn main() {
+ tauri_build::build();
+}
diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json
new file mode 100644
index 0000000..802d2a2
--- /dev/null
+++ b/src-tauri/capabilities/default.json
@@ -0,0 +1,7 @@
+{
+ "$schema": "../gen/schemas/desktop-schema.json",
+ "identifier": "default",
+ "description": "Default capability for the main ProxyWarden Windows shell. Task 8 keeps helper/install launch explicit: no shell or sidecar permission is granted here until a packaged helper is declared.",
+ "windows": ["main"],
+ "permissions": ["core:default", "dialog:allow-open"]
+}
diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png
new file mode 100644
index 0000000..90a975b
Binary files /dev/null and b/src-tauri/icons/128x128.png differ
diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png
new file mode 100644
index 0000000..16fce22
Binary files /dev/null and b/src-tauri/icons/128x128@2x.png differ
diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png
new file mode 100644
index 0000000..3c701b3
Binary files /dev/null and b/src-tauri/icons/32x32.png differ
diff --git a/src-tauri/icons/64x64.png b/src-tauri/icons/64x64.png
new file mode 100644
index 0000000..077f48d
Binary files /dev/null and b/src-tauri/icons/64x64.png differ
diff --git a/src-tauri/icons/Square107x107Logo.png b/src-tauri/icons/Square107x107Logo.png
new file mode 100644
index 0000000..e94ae22
Binary files /dev/null and b/src-tauri/icons/Square107x107Logo.png differ
diff --git a/src-tauri/icons/Square142x142Logo.png b/src-tauri/icons/Square142x142Logo.png
new file mode 100644
index 0000000..b3b886c
Binary files /dev/null and b/src-tauri/icons/Square142x142Logo.png differ
diff --git a/src-tauri/icons/Square150x150Logo.png b/src-tauri/icons/Square150x150Logo.png
new file mode 100644
index 0000000..844afa8
Binary files /dev/null and b/src-tauri/icons/Square150x150Logo.png differ
diff --git a/src-tauri/icons/Square284x284Logo.png b/src-tauri/icons/Square284x284Logo.png
new file mode 100644
index 0000000..392d54a
Binary files /dev/null and b/src-tauri/icons/Square284x284Logo.png differ
diff --git a/src-tauri/icons/Square30x30Logo.png b/src-tauri/icons/Square30x30Logo.png
new file mode 100644
index 0000000..1c289fb
Binary files /dev/null and b/src-tauri/icons/Square30x30Logo.png differ
diff --git a/src-tauri/icons/Square310x310Logo.png b/src-tauri/icons/Square310x310Logo.png
new file mode 100644
index 0000000..b7b750f
Binary files /dev/null and b/src-tauri/icons/Square310x310Logo.png differ
diff --git a/src-tauri/icons/Square44x44Logo.png b/src-tauri/icons/Square44x44Logo.png
new file mode 100644
index 0000000..89d7b89
Binary files /dev/null and b/src-tauri/icons/Square44x44Logo.png differ
diff --git a/src-tauri/icons/Square71x71Logo.png b/src-tauri/icons/Square71x71Logo.png
new file mode 100644
index 0000000..114e394
Binary files /dev/null and b/src-tauri/icons/Square71x71Logo.png differ
diff --git a/src-tauri/icons/Square89x89Logo.png b/src-tauri/icons/Square89x89Logo.png
new file mode 100644
index 0000000..39d0dac
Binary files /dev/null and b/src-tauri/icons/Square89x89Logo.png differ
diff --git a/src-tauri/icons/StoreLogo.png b/src-tauri/icons/StoreLogo.png
new file mode 100644
index 0000000..0434861
Binary files /dev/null and b/src-tauri/icons/StoreLogo.png differ
diff --git a/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml b/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..2ffbf24
--- /dev/null
+++ b/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..33e1845
Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png differ
diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..ef3edab
Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..2df7fcf
Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..9d3c42d
Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png differ
diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..05e9383
Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..51f37ec
Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..5ca8107
Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..b193555
Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..b1255c1
Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..816057d
Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..61a1100
Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..a6532c3
Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..3d2a501
Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..601a4d5
Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..8831810
Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/src-tauri/icons/android/values/ic_launcher_background.xml b/src-tauri/icons/android/values/ic_launcher_background.xml
new file mode 100644
index 0000000..ea9c223
--- /dev/null
+++ b/src-tauri/icons/android/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #fff
+
\ No newline at end of file
diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns
new file mode 100644
index 0000000..0af0872
Binary files /dev/null and b/src-tauri/icons/icon.icns differ
diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico
new file mode 100644
index 0000000..2211580
Binary files /dev/null and b/src-tauri/icons/icon.ico differ
diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png
new file mode 100644
index 0000000..6fb9a70
Binary files /dev/null and b/src-tauri/icons/icon.png differ
diff --git a/src-tauri/icons/ios/AppIcon-20x20@1x.png b/src-tauri/icons/ios/AppIcon-20x20@1x.png
new file mode 100644
index 0000000..102bf0c
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@1x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-20x20@2x-1.png b/src-tauri/icons/ios/AppIcon-20x20@2x-1.png
new file mode 100644
index 0000000..f2b425a
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@2x-1.png differ
diff --git a/src-tauri/icons/ios/AppIcon-20x20@2x.png b/src-tauri/icons/ios/AppIcon-20x20@2x.png
new file mode 100644
index 0000000..f2b425a
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@2x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-20x20@3x.png b/src-tauri/icons/ios/AppIcon-20x20@3x.png
new file mode 100644
index 0000000..b8e4f13
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@3x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-29x29@1x.png b/src-tauri/icons/ios/AppIcon-29x29@1x.png
new file mode 100644
index 0000000..1134b3d
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@1x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-29x29@2x-1.png b/src-tauri/icons/ios/AppIcon-29x29@2x-1.png
new file mode 100644
index 0000000..549b066
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@2x-1.png differ
diff --git a/src-tauri/icons/ios/AppIcon-29x29@2x.png b/src-tauri/icons/ios/AppIcon-29x29@2x.png
new file mode 100644
index 0000000..549b066
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@2x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-29x29@3x.png b/src-tauri/icons/ios/AppIcon-29x29@3x.png
new file mode 100644
index 0000000..3c5daf3
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@3x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-40x40@1x.png b/src-tauri/icons/ios/AppIcon-40x40@1x.png
new file mode 100644
index 0000000..f2b425a
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@1x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-40x40@2x-1.png b/src-tauri/icons/ios/AppIcon-40x40@2x-1.png
new file mode 100644
index 0000000..2036ec9
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@2x-1.png differ
diff --git a/src-tauri/icons/ios/AppIcon-40x40@2x.png b/src-tauri/icons/ios/AppIcon-40x40@2x.png
new file mode 100644
index 0000000..2036ec9
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@2x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-40x40@3x.png b/src-tauri/icons/ios/AppIcon-40x40@3x.png
new file mode 100644
index 0000000..abd0ca8
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@3x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-512@2x.png b/src-tauri/icons/ios/AppIcon-512@2x.png
new file mode 100644
index 0000000..023a8c2
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-512@2x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-60x60@2x.png b/src-tauri/icons/ios/AppIcon-60x60@2x.png
new file mode 100644
index 0000000..abd0ca8
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-60x60@2x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-60x60@3x.png b/src-tauri/icons/ios/AppIcon-60x60@3x.png
new file mode 100644
index 0000000..305f34c
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-60x60@3x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-76x76@1x.png b/src-tauri/icons/ios/AppIcon-76x76@1x.png
new file mode 100644
index 0000000..f7b45f2
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-76x76@1x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-76x76@2x.png b/src-tauri/icons/ios/AppIcon-76x76@2x.png
new file mode 100644
index 0000000..a8e8b4b
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-76x76@2x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png b/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
new file mode 100644
index 0000000..9dde93b
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png differ
diff --git a/src-tauri/src/activity.rs b/src-tauri/src/activity.rs
new file mode 100644
index 0000000..6c8cc28
--- /dev/null
+++ b/src-tauri/src/activity.rs
@@ -0,0 +1,23 @@
+use crate::models::ActivityEntry;
+
+pub const DEFAULT_ACTIVITY_LIMIT: usize = 200;
+
+pub fn sort_activity_desc(mut entries: Vec) -> Vec {
+ entries.sort_by(|left, right| right.at.cmp(&left.at).then_with(|| right.id.cmp(&left.id)));
+ entries
+}
+
+pub fn cap_activity(entries: Vec, limit: usize) -> Vec {
+ let mut entries = sort_activity_desc(entries);
+ entries.truncate(limit);
+ entries
+}
+
+pub fn append_activity(
+ mut entries: Vec,
+ entry: ActivityEntry,
+ limit: usize,
+) -> Vec {
+ entries.push(entry);
+ cap_activity(entries, limit)
+}
diff --git a/src-tauri/src/adapters/proxifyre.rs b/src-tauri/src/adapters/proxifyre.rs
new file mode 100644
index 0000000..2839bbc
--- /dev/null
+++ b/src-tauri/src/adapters/proxifyre.rs
@@ -0,0 +1,245 @@
+#[cfg(not(test))]
+use crate::adapters::proxy_router::{
+ ProxyRouterAdapter, ProxyRouterError, ProxyRouterErrorKind, ProxyRouterGeneratedConfig,
+ ProxyRouterRequest,
+};
+use crate::models::{
+ ComponentId, ComponentState, ComponentStatus, Profile, ProfileItemType, Protocol,
+ ProxyProtocol, Target,
+};
+#[cfg(test)]
+use crate::proxy_router::{
+ ProxyRouterAdapter, ProxyRouterError, ProxyRouterErrorKind, ProxyRouterGeneratedConfig,
+ ProxyRouterRequest,
+};
+use serde::{Deserialize, Serialize};
+
+pub const PROXIFYRE_ADAPTER_ID: &str = "proxifyre";
+pub const PROXIFYRE_OUTPUT_FILE: &str = "proxifyre-app-config.json";
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct ProxiFyreAdapter {
+ log_level: String,
+ bypass_lan: bool,
+}
+
+impl ProxiFyreAdapter {
+ pub fn new(log_level: impl Into, bypass_lan: bool) -> Self {
+ Self {
+ log_level: log_level.into(),
+ bypass_lan,
+ }
+ }
+
+ pub fn generate_proxifyre_config(
+ &self,
+ request: ProxyRouterRequest<'_>,
+ ) -> Result {
+ let mut proxies = Vec::new();
+
+ for profile in request.profiles.iter().filter(|profile| profile.enabled) {
+ let target = find_target(profile, request.targets)?;
+ ensure_target_supported(profile, target, request.components)?;
+
+ let app_names = app_names_for_profile(profile);
+ if app_names.is_empty() {
+ return Err(ProxyRouterError::new(
+ ProxyRouterErrorKind::EmptyProfileItems,
+ format!(
+ "В профиле '{}' нет приложений для маршрутизации",
+ profile.id
+ ),
+ ));
+ }
+
+ proxies.push(ProxiFyreProxy {
+ app_names,
+ socks5_proxy_endpoint: format!("{}:{}", target.host, target.port),
+ supported_protocols: protocols_for_profile(profile),
+ });
+ }
+
+ Ok(ProxiFyreConfig {
+ log_level: self.log_level.clone(),
+ bypass_lan: self.bypass_lan,
+ proxies,
+ })
+ }
+}
+
+impl Default for ProxiFyreAdapter {
+ fn default() -> Self {
+ Self::new("Info", true)
+ }
+}
+
+impl ProxyRouterAdapter for ProxiFyreAdapter {
+ fn id(&self) -> &'static str {
+ PROXIFYRE_ADAPTER_ID
+ }
+
+ fn output_file_name(&self) -> &'static str {
+ PROXIFYRE_OUTPUT_FILE
+ }
+
+ fn generate_config(
+ &self,
+ request: ProxyRouterRequest<'_>,
+ ) -> Result {
+ let config = self.generate_proxifyre_config(request)?;
+ let enabled_profiles = config.proxies.len();
+ let routed_apps = config
+ .proxies
+ .iter()
+ .map(|proxy| proxy.app_names.len())
+ .sum();
+ let contents = serde_json::to_string_pretty(&config).map_err(|error| {
+ ProxyRouterError::new(
+ ProxyRouterErrorKind::Serialization,
+ format!("Не удалось сериализовать конфиг ProxiFyre: {error}"),
+ )
+ })?;
+
+ Ok(ProxyRouterGeneratedConfig {
+ adapter_id: self.id().to_string(),
+ output_file_name: self.output_file_name().to_string(),
+ contents,
+ enabled_profiles,
+ routed_apps,
+ })
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct ProxiFyreConfig {
+ #[serde(rename = "logLevel")]
+ pub log_level: String,
+ #[serde(rename = "bypassLan")]
+ pub bypass_lan: bool,
+ pub proxies: Vec,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct ProxiFyreProxy {
+ #[serde(rename = "appNames")]
+ pub app_names: Vec,
+ #[serde(rename = "socks5ProxyEndpoint")]
+ pub socks5_proxy_endpoint: String,
+ #[serde(rename = "supportedProtocols")]
+ pub supported_protocols: Vec,
+}
+
+fn find_target<'a>(
+ profile: &Profile,
+ targets: &'a [Target],
+) -> Result<&'a Target, ProxyRouterError> {
+ targets
+ .iter()
+ .find(|target| target.id == profile.target_id)
+ .ok_or_else(|| {
+ ProxyRouterError::new(
+ ProxyRouterErrorKind::MissingTarget,
+ format!(
+ "Профиль '{}' ссылается на отсутствующую цель '{}'",
+ profile.id, profile.target_id
+ ),
+ )
+ })
+}
+
+fn ensure_target_supported(
+ profile: &Profile,
+ target: &Target,
+ components: &[ComponentStatus],
+) -> Result<(), ProxyRouterError> {
+ if target.protocol != ProxyProtocol::Socks5 {
+ return Err(ProxyRouterError::new(
+ ProxyRouterErrorKind::UnsupportedTargetProtocol,
+ format!(
+ "Цель '{}' использует HTTP, но ProxiFyre требует SOCKS5",
+ target.id
+ ),
+ ));
+ }
+
+ if let Some(required_component) = &target.requires_component {
+ let Some(status) = components
+ .iter()
+ .find(|component| &component.id == required_component)
+ else {
+ return Err(ProxyRouterError::new(
+ ProxyRouterErrorKind::MissingRequiredComponent,
+ format!(
+ "Цель '{}' профиля '{}' требует отсутствующий компонент '{}'",
+ target.id,
+ profile.id,
+ component_id_label(required_component)
+ ),
+ ));
+ };
+
+ if !component_is_running(status) {
+ return Err(ProxyRouterError::new(
+ ProxyRouterErrorKind::RequiredComponentNotRunning,
+ format!(
+ "Цель '{}' профиля '{}' требует запущенный компонент '{}'",
+ target.id,
+ profile.id,
+ component_id_label(required_component)
+ ),
+ ));
+ }
+ }
+
+ Ok(())
+}
+
+fn component_is_running(status: &ComponentStatus) -> bool {
+ status.installed && status.running && status.state == ComponentState::Running
+}
+
+fn app_names_for_profile(profile: &Profile) -> Vec {
+ let mut names = Vec::new();
+
+ for item in &profile.items {
+ let value = item.value.trim();
+ if value.is_empty() {
+ continue;
+ }
+
+ let app_name = match item.item_type {
+ ProfileItemType::Process | ProfileItemType::Folder | ProfileItemType::Exe => value,
+ };
+
+ if !names.iter().any(|existing| existing == app_name) {
+ names.push(app_name.to_string());
+ }
+ }
+
+ names
+}
+
+fn protocols_for_profile(profile: &Profile) -> Vec {
+ let mut protocols = Vec::new();
+
+ for protocol in &profile.protocols {
+ let value = match protocol {
+ Protocol::Tcp => "TCP",
+ Protocol::Udp => "UDP",
+ };
+
+ if !protocols.iter().any(|existing| existing == value) {
+ protocols.push(value.to_string());
+ }
+ }
+
+ protocols
+}
+
+fn component_id_label(component_id: &ComponentId) -> &'static str {
+ match component_id {
+ ComponentId::ControlApp => "control-app",
+ ComponentId::Proxyfier => "proxyfier",
+ ComponentId::Singbox => "singbox",
+ }
+}
diff --git a/src-tauri/src/adapters/proxy_router.rs b/src-tauri/src/adapters/proxy_router.rs
new file mode 100644
index 0000000..f9e0162
--- /dev/null
+++ b/src-tauri/src/adapters/proxy_router.rs
@@ -0,0 +1,67 @@
+use crate::models::{ComponentStatus, Profile, Target};
+
+#[derive(Debug, Clone, Copy)]
+pub struct ProxyRouterRequest<'a> {
+ pub profiles: &'a [Profile],
+ pub targets: &'a [Target],
+ pub components: &'a [ComponentStatus],
+}
+
+impl<'a> ProxyRouterRequest<'a> {
+ pub fn new(
+ profiles: &'a [Profile],
+ targets: &'a [Target],
+ components: &'a [ComponentStatus],
+ ) -> Self {
+ Self {
+ profiles,
+ targets,
+ components,
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct ProxyRouterGeneratedConfig {
+ pub adapter_id: String,
+ pub output_file_name: String,
+ pub contents: String,
+ pub enabled_profiles: usize,
+ pub routed_apps: usize,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct ProxyRouterError {
+ pub kind: ProxyRouterErrorKind,
+ pub message: String,
+}
+
+impl ProxyRouterError {
+ pub fn new(kind: ProxyRouterErrorKind, message: impl Into) -> Self {
+ Self {
+ kind,
+ message: message.into(),
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum ProxyRouterErrorKind {
+ EmptyProfileItems,
+ MissingTarget,
+ MissingRequiredComponent,
+ RequiredComponentNotRunning,
+ UnsupportedTargetProtocol,
+ Serialization,
+}
+
+pub trait ProxyRouterAdapter {
+ fn id(&self) -> &'static str;
+
+ fn output_file_name(&self) -> &'static str;
+
+ fn generate_config(
+ &self,
+ request: ProxyRouterRequest<'_>,
+ ) -> Result;
+}
diff --git a/src-tauri/src/adapters/singbox.rs b/src-tauri/src/adapters/singbox.rs
new file mode 100644
index 0000000..0bcc83b
--- /dev/null
+++ b/src-tauri/src/adapters/singbox.rs
@@ -0,0 +1,338 @@
+use crate::models::{LocalSingBoxConfig, SubscriptionCache};
+use serde::{Deserialize, Serialize};
+use serde_json::{json, Value};
+use std::{
+ env, fs,
+ path::Path,
+ process::Command,
+ time::{SystemTime, UNIX_EPOCH},
+};
+
+pub const SINGBOX_ADAPTER_ID: &str = "singbox";
+pub const SINGBOX_OUTPUT_FILE: &str = "sing-box-config.json";
+pub const DEFAULT_MIXED_INBOUND_TAG: &str = "proxywarden-mixed-in";
+pub const DEFAULT_VPN_OUTBOUND_TAG: &str = "vpn";
+pub const DEFAULT_DIRECT_OUTBOUND_TAG: &str = "direct";
+pub const DEFAULT_BLOCK_OUTBOUND_TAG: &str = "block";
+
+const SUPPORTED_PROXY_TYPES: &[&str] = &["vless", "vmess", "trojan", "shadowsocks", "hysteria2"];
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct SingBoxAdapter {
+ log_level: String,
+ inbound_tag: String,
+ vpn_outbound_tag: String,
+}
+
+impl SingBoxAdapter {
+ pub fn new(
+ log_level: impl Into,
+ inbound_tag: impl Into,
+ vpn_outbound_tag: impl Into,
+ ) -> Self {
+ Self {
+ log_level: log_level.into(),
+ inbound_tag: inbound_tag.into(),
+ vpn_outbound_tag: vpn_outbound_tag.into(),
+ }
+ }
+
+ pub fn generate_config(
+ &self,
+ request: SingBoxGenerationRequest<'_>,
+ checker: &C,
+ ) -> Result
+ where
+ C: SingBoxConfigChecker,
+ {
+ let selected_server_tag = request
+ .config
+ .selected_server_tag
+ .as_deref()
+ .map(str::trim)
+ .filter(|value| !value.is_empty())
+ .ok_or_else(|| {
+ SingBoxConfigError::new(
+ SingBoxConfigErrorKind::MissingSelectedServer,
+ "Сервер Local sing-box не выбран",
+ )
+ })?;
+ let vpn_outbound = selected_outbound(
+ &request.subscription_cache.config,
+ selected_server_tag,
+ &self.vpn_outbound_tag,
+ )?;
+ let generated_config = json!({
+ "log": {
+ "disabled": false,
+ "level": self.log_level,
+ "timestamp": true
+ },
+ "inbounds": [
+ {
+ "type": "mixed",
+ "tag": self.inbound_tag,
+ "listen": request.config.listen_host,
+ "listen_port": request.config.listen_port,
+ "users": [],
+ "set_system_proxy": false
+ }
+ ],
+ "outbounds": [
+ vpn_outbound,
+ { "type": "direct", "tag": DEFAULT_DIRECT_OUTBOUND_TAG },
+ { "type": "block", "tag": DEFAULT_BLOCK_OUTBOUND_TAG }
+ ],
+ "route": {
+ "rules": [
+ { "ip_is_private": true, "outbound": DEFAULT_DIRECT_OUTBOUND_TAG }
+ ],
+ "final": self.vpn_outbound_tag
+ }
+ });
+ let contents = serde_json::to_string_pretty(&generated_config).map_err(|error| {
+ SingBoxConfigError::new(
+ SingBoxConfigErrorKind::Serialization,
+ format!("Не удалось сериализовать конфиг sing-box: {error}"),
+ )
+ })?;
+ let check = match request.binary_path {
+ Some(binary_path) => Some(checker.check_config(binary_path, &contents)?),
+ None => None,
+ };
+
+ Ok(SingBoxGeneratedConfig {
+ adapter_id: SINGBOX_ADAPTER_ID.to_string(),
+ output_file_name: SINGBOX_OUTPUT_FILE.to_string(),
+ contents,
+ selected_server_tag: selected_server_tag.to_string(),
+ listen: request.config.listen_host.clone(),
+ listen_port: request.config.listen_port,
+ check,
+ })
+ }
+}
+
+impl Default for SingBoxAdapter {
+ fn default() -> Self {
+ Self::new("info", DEFAULT_MIXED_INBOUND_TAG, DEFAULT_VPN_OUTBOUND_TAG)
+ }
+}
+
+#[derive(Debug, Clone, Copy)]
+pub struct SingBoxGenerationRequest<'a> {
+ pub config: &'a LocalSingBoxConfig,
+ pub subscription_cache: &'a SubscriptionCache,
+ pub binary_path: Option<&'a Path>,
+}
+
+impl<'a> SingBoxGenerationRequest<'a> {
+ pub fn new(
+ config: &'a LocalSingBoxConfig,
+ subscription_cache: &'a SubscriptionCache,
+ binary_path: Option<&'a Path>,
+ ) -> Self {
+ Self {
+ config,
+ subscription_cache,
+ binary_path,
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct SingBoxGeneratedConfig {
+ pub adapter_id: String,
+ pub output_file_name: String,
+ pub contents: String,
+ pub selected_server_tag: String,
+ pub listen: String,
+ pub listen_port: u16,
+ pub check: Option,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct SingBoxCheckResult {
+ pub checked: bool,
+ pub success: bool,
+ pub message: String,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct SingBoxConfigError {
+ pub kind: SingBoxConfigErrorKind,
+ pub message: String,
+}
+
+impl SingBoxConfigError {
+ pub fn new(kind: SingBoxConfigErrorKind, message: impl Into) -> Self {
+ Self {
+ kind,
+ message: message.into(),
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum SingBoxConfigErrorKind {
+ MissingSelectedServer,
+ MissingSelectedOutbound,
+ UnsupportedSelectedOutbound,
+ Serialization,
+ CheckFailed,
+}
+
+pub trait SingBoxConfigChecker {
+ fn check_config(
+ &self,
+ binary_path: &Path,
+ config_json: &str,
+ ) -> Result;
+}
+
+#[derive(Debug, Clone, Copy, Default)]
+pub struct SingBoxCommandChecker;
+
+impl SingBoxConfigChecker for SingBoxCommandChecker {
+ fn check_config(
+ &self,
+ binary_path: &Path,
+ config_json: &str,
+ ) -> Result {
+ let config_path = env::temp_dir().join(format!(
+ "proxywarden-sing-box-{}-{}.json",
+ std::process::id(),
+ now_millis()
+ ));
+
+ fs::write(&config_path, config_json).map_err(|error| {
+ SingBoxConfigError::new(
+ SingBoxConfigErrorKind::CheckFailed,
+ format!(
+ "Не удалось записать временный конфиг sing-box '{}': {error}",
+ config_path.display()
+ ),
+ )
+ })?;
+
+ let output = Command::new(binary_path)
+ .arg("check")
+ .arg("-c")
+ .arg(&config_path)
+ .output()
+ .map_err(|error| {
+ let _ = fs::remove_file(&config_path);
+ SingBoxConfigError::new(
+ SingBoxConfigErrorKind::CheckFailed,
+ format!(
+ "Не удалось выполнить '{} check': {error}",
+ binary_path.display()
+ ),
+ )
+ })?;
+ let _ = fs::remove_file(&config_path);
+
+ let stdout = String::from_utf8_lossy(&output.stdout);
+ let stderr = String::from_utf8_lossy(&output.stderr);
+ let message = command_message(&stdout, &stderr);
+
+ if !output.status.success() {
+ return Err(SingBoxConfigError::new(
+ SingBoxConfigErrorKind::CheckFailed,
+ format!("Проверка sing-box не прошла: {message}"),
+ ));
+ }
+
+ Ok(SingBoxCheckResult {
+ checked: true,
+ success: true,
+ message: if message.is_empty() {
+ "Проверка sing-box прошла успешно".to_string()
+ } else {
+ message
+ },
+ })
+ }
+}
+
+fn selected_outbound(
+ subscription_config: &Value,
+ selected_server_tag: &str,
+ vpn_outbound_tag: &str,
+) -> Result {
+ let outbounds = subscription_config
+ .get("outbounds")
+ .and_then(Value::as_array)
+ .ok_or_else(|| {
+ SingBoxConfigError::new(
+ SingBoxConfigErrorKind::MissingSelectedOutbound,
+ "В cache подписки нет outbounds",
+ )
+ })?;
+ let outbound = outbounds
+ .iter()
+ .find(|outbound| {
+ outbound
+ .get("tag")
+ .and_then(Value::as_str)
+ .is_some_and(|tag| tag.trim() == selected_server_tag)
+ })
+ .ok_or_else(|| {
+ SingBoxConfigError::new(
+ SingBoxConfigErrorKind::MissingSelectedOutbound,
+ format!("Outbound не найден: {selected_server_tag}"),
+ )
+ })?;
+ let outbound_type = outbound
+ .get("type")
+ .and_then(Value::as_str)
+ .unwrap_or_default();
+
+ if !SUPPORTED_PROXY_TYPES.contains(&outbound_type) {
+ return Err(SingBoxConfigError::new(
+ SingBoxConfigErrorKind::UnsupportedSelectedOutbound,
+ format!(
+ "Outbound '{selected_server_tag}' имеет неподдерживаемый тип '{outbound_type}'"
+ ),
+ ));
+ }
+
+ let mut outbound = outbound.clone();
+ let object = outbound.as_object_mut().ok_or_else(|| {
+ SingBoxConfigError::new(
+ SingBoxConfigErrorKind::UnsupportedSelectedOutbound,
+ format!("Outbound '{selected_server_tag}' должен быть JSON-объектом"),
+ )
+ })?;
+ object.insert(
+ "tag".to_string(),
+ Value::String(vpn_outbound_tag.to_string()),
+ );
+ if outbound_type == "vless" && !object.contains_key("packet_encoding") {
+ object.insert(
+ "packet_encoding".to_string(),
+ Value::String("xudp".to_string()),
+ );
+ }
+
+ Ok(outbound)
+}
+
+fn now_millis() -> u128 {
+ SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .map(|duration| duration.as_millis())
+ .unwrap_or_default()
+}
+
+fn command_message(stdout: &str, stderr: &str) -> String {
+ let stdout = stdout.trim();
+ let stderr = stderr.trim();
+
+ match (stdout.is_empty(), stderr.is_empty()) {
+ (true, true) => String::new(),
+ (false, true) => stdout.to_string(),
+ (true, false) => stderr.to_string(),
+ (false, false) => format!("{stdout}\n{stderr}"),
+ }
+}
diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs
new file mode 100644
index 0000000..0f00896
--- /dev/null
+++ b/src-tauri/src/commands.rs
@@ -0,0 +1,3530 @@
+#[cfg(not(test))]
+use crate::adapters::proxifyre::ProxiFyreAdapter;
+#[cfg(not(test))]
+use crate::adapters::proxy_router::{
+ ProxyRouterAdapter, ProxyRouterError, ProxyRouterErrorKind, ProxyRouterGeneratedConfig,
+ ProxyRouterRequest,
+};
+#[cfg(not(test))]
+use crate::adapters::singbox::{
+ SingBoxAdapter, SingBoxCheckResult, SingBoxCommandChecker, SingBoxConfigChecker,
+ SingBoxConfigError, SingBoxConfigErrorKind, SingBoxGeneratedConfig, SingBoxGenerationRequest,
+};
+use crate::component_detection::{
+ detect_proxyfier_install, detect_proxyfier_install_with_host, detect_singbox_install,
+ proxyfier_component_from_detection, singbox_component_from_detection, DetectedProxyfier,
+ DetectedSingBox, ProxyfierDetectionHost, SystemProxyfierDetectionHost,
+};
+use crate::models::{
+ ActivityEntry, ActivityLevel, ComponentId, ComponentState, ComponentStatus, LocalSingBoxConfig,
+ Profile, ProfileInput, ProfileItem, ProfileItemInput, ProfileItemType, Protocol, ProxyProtocol,
+ SubscriptionCache, SubscriptionServer, Target, TargetInput, TargetKind,
+};
+#[cfg(test)]
+use crate::proxifyre::ProxiFyreAdapter;
+#[cfg(test)]
+use crate::proxy_router::{
+ ProxyRouterAdapter, ProxyRouterError, ProxyRouterErrorKind, ProxyRouterGeneratedConfig,
+ ProxyRouterRequest,
+};
+#[cfg(test)]
+use crate::singbox::{
+ SingBoxAdapter, SingBoxCheckResult, SingBoxCommandChecker, SingBoxConfigChecker,
+ SingBoxConfigError, SingBoxConfigErrorKind, SingBoxGeneratedConfig, SingBoxGenerationRequest,
+};
+use crate::singbox_service::{
+ build_singbox_setup_status, ensure_safe_singbox_install_dir,
+ parse_service_command_output as parse_singbox_service_command_output, service_control_script,
+ ServiceCommandOutput as SingBoxServiceCommandOutput, SingBoxServiceAction, SingBoxSetupStatus,
+};
+use crate::storage::{default_config_root, JsonStorage};
+use crate::subscription;
+use crate::validation::{normalize_profile, normalize_target, ValidationError};
+use serde::{Deserialize, Serialize};
+use std::env;
+use std::fs;
+use std::net::{IpAddr, TcpStream, ToSocketAddrs, UdpSocket};
+use std::path::{Path, PathBuf};
+use std::process::{Command, Output};
+use std::time::{Duration, Instant};
+use std::time::{SystemTime, UNIX_EPOCH};
+
+const PROXIFYRE_INSTALL_DIR: &str = r"C:\Tools\ProxiFyre";
+const PROXIFYRE_RELEASE_API_URL: &str =
+ "https://api.github.com/repos/wiresock/proxifyre/releases/latest";
+const NDISAPI_RELEASE_API_URL: &str =
+ "https://api.github.com/repos/wiresock/ndisapi/releases/latest";
+const VC_REDIST_X64_URL: &str = "https://aka.ms/vc14/vc_redist.x64.exe";
+const VC_REDIST_X86_URL: &str = "https://aka.ms/vc14/vc_redist.x86.exe";
+
+#[derive(Debug, Clone)]
+pub struct CommandState {
+ root: PathBuf,
+}
+
+impl CommandState {
+ pub fn new(root: impl Into) -> Self {
+ Self { root: root.into() }
+ }
+
+ pub fn storage(&self) -> JsonStorage {
+ JsonStorage::new(self.root.clone())
+ }
+}
+
+impl Default for CommandState {
+ fn default() -> Self {
+ Self::new(default_config_root())
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CommandError {
+ pub code: String,
+ pub message: String,
+ #[serde(default)]
+ pub details: Vec,
+}
+
+impl CommandError {
+ fn new(code: impl Into, message: impl Into) -> Self {
+ Self {
+ code: code.into(),
+ message: message.into(),
+ details: Vec::new(),
+ }
+ }
+
+ fn with_details(
+ code: impl Into,
+ message: impl Into,
+ details: Vec,
+ ) -> Self {
+ Self {
+ code: code.into(),
+ message: message.into(),
+ details,
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ValidationIssue {
+ pub field: String,
+ pub message: String,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct StatusResponse {
+ pub route_line: String,
+ pub active_profile_count: usize,
+ pub routed_app_count: usize,
+ pub active_target: Option,
+ pub components: Vec,
+ pub recent_activity: Vec,
+ pub generated_config_path: String,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SavedStateResponse {
+ pub profiles: Vec,
+ pub targets: Vec,
+ pub generated_config_path: String,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ProxiFyreSetupStatusDto {
+ pub ready: bool,
+ pub missing_count: usize,
+ pub items: Vec,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ProxiFyreSetupItemDto {
+ pub id: String,
+ pub name: String,
+ pub installed: bool,
+ pub version: Option,
+ pub details: String,
+}
+
+pub type SingBoxSetupStatusDto = SingBoxSetupStatus;
+
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct LocalSingBoxStatusResponse {
+ pub config: LocalSingBoxConfigDto,
+ pub cache: Option,
+ pub component: ComponentStatusDto,
+ pub generated_config_path: String,
+ pub lan_listen_host: Option,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct LocalSingBoxConfigDto {
+ pub subscription_display_url: Option,
+ pub has_subscription: bool,
+ pub selected_server_tag: Option,
+ pub listen_host: String,
+ pub listen_port: u16,
+ pub service_name: String,
+ pub install_root: String,
+ pub updated_at: Option,
+}
+
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SubscriptionCacheDto {
+ pub servers: Vec,
+ pub user_info: serde_json::Map,
+ pub fetched_at: String,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SubscriptionServerDto {
+ pub tag: String,
+ #[serde(rename = "type")]
+ pub server_type: String,
+ pub server: String,
+ pub server_port: u16,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SaveSingBoxSubscriptionInputDto {
+ pub subscription_url: String,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SelectSingBoxServerInputDto {
+ pub tag: String,
+ #[serde(default)]
+ pub server: Option,
+ #[serde(default)]
+ pub server_port: Option,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct PingSingBoxServerInputDto {
+ pub tag: String,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct PingProxyTargetInputDto {
+ pub host: String,
+ pub port: u16,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct PingServerResponse {
+ pub tag: String,
+ pub server: String,
+ pub server_port: u16,
+ pub ok: bool,
+ pub latency: Option,
+ pub error: Option,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct GenerateSingBoxConfigResponse {
+ pub success: bool,
+ pub message: String,
+ pub adapter_id: String,
+ pub generated_config_path: String,
+ pub selected_server_tag: String,
+ pub listen_host: String,
+ pub listen_port: u16,
+ pub check: Option,
+ pub activity: ActivityEntryDto,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ProfileInputDto {
+ pub id: Option,
+ pub name: String,
+ #[serde(default)]
+ pub enabled: Option,
+ #[serde(default)]
+ pub target_id: Option,
+ #[serde(default)]
+ pub protocols: Option>,
+ #[serde(default)]
+ pub items: Option>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct ProfileItemInputDto {
+ #[serde(rename = "type")]
+ pub item_type: String,
+ pub value: String,
+ #[serde(default)]
+ pub recursive: Option,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct TargetInputDto {
+ pub id: Option,
+ pub name: String,
+ #[serde(default)]
+ pub kind: Option,
+ #[serde(default)]
+ pub protocol: Option,
+ pub host: String,
+ pub port: u32,
+ #[serde(default)]
+ pub requires_component: Option,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ProfileDto {
+ pub id: String,
+ pub name: String,
+ pub enabled: bool,
+ pub target_id: String,
+ pub protocols: Vec,
+ pub items: Vec,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct ProfileItemDto {
+ #[serde(rename = "type")]
+ pub item_type: ProfileItemType,
+ pub value: String,
+ pub recursive: bool,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct TargetDto {
+ pub id: String,
+ pub name: String,
+ pub kind: TargetKind,
+ pub protocol: ProxyProtocol,
+ pub host: String,
+ pub port: u16,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub requires_component: Option,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ComponentStatusDto {
+ pub id: ComponentId,
+ pub name: String,
+ pub state: ComponentState,
+ pub installed: bool,
+ pub running: bool,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub version: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub path: Option,
+ pub problems: Vec,
+ pub actions: Vec,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ActivityEntryDto {
+ pub id: String,
+ pub at: String,
+ pub level: ActivityLevel,
+ pub title: String,
+ pub message: String,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ResolveProfilePreviewResponse {
+ pub profile_id: String,
+ pub apps: Vec,
+ pub warnings: Vec,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ResolvedAppDto {
+ pub source_type: ProfileItemType,
+ pub source_value: String,
+ pub app_name: String,
+ pub notes: Vec,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ApplyProfilesResponse {
+ pub success: bool,
+ pub changed: bool,
+ pub message: String,
+ pub adapter_id: String,
+ pub generated_config_path: String,
+ pub enabled_profiles: usize,
+ pub routed_apps: usize,
+ pub helper: HelperApplyResult,
+ pub activity: ActivityEntryDto,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct HelperApplyResult {
+ pub success: bool,
+ pub changed: bool,
+ pub action: String,
+ pub message: String,
+}
+
+pub struct HelperApplyRequest<'a> {
+ pub adapter_id: &'a str,
+ pub config_path: &'a Path,
+ pub config_contents: &'a str,
+}
+
+pub trait ProxyApplyHelper {
+ fn apply_proxy_config(
+ &self,
+ request: HelperApplyRequest<'_>,
+ ) -> Result;
+}
+
+pub trait SubscriptionFetcher {
+ fn fetch_subscription(
+ &self,
+ url: &str,
+ ) -> Result;
+}
+
+pub struct SystemSubscriptionFetcher;
+
+impl SubscriptionFetcher for SystemSubscriptionFetcher {
+ fn fetch_subscription(
+ &self,
+ url: &str,
+ ) -> Result {
+ subscription::fetch_subscription(url)
+ }
+}
+
+pub trait Clock {
+ fn now(&self) -> String;
+}
+
+pub struct SystemClock;
+
+impl Clock for SystemClock {
+ fn now(&self) -> String {
+ let seconds = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .map(|duration| duration.as_secs())
+ .unwrap_or(0);
+ format!("unix:{seconds}")
+ }
+}
+
+pub struct StagedApplyHelper;
+
+impl ProxyApplyHelper for StagedApplyHelper {
+ fn apply_proxy_config(
+ &self,
+ request: HelperApplyRequest<'_>,
+ ) -> Result {
+ Ok(HelperApplyResult {
+ success: true,
+ changed: true,
+ action: format!("{}.stage-generated-config", request.adapter_id),
+ message: format!(
+ "Сгенерированный конфиг подготовлен в {}; интеграция привилегированного помощника еще не подключена",
+ request.config_path.display()
+ ),
+ })
+ }
+}
+
+pub struct DetectedProxyApplyHelper {
+ host: H,
+}
+
+impl DetectedProxyApplyHelper {
+ pub fn system() -> Self {
+ Self {
+ host: SystemProxyfierDetectionHost,
+ }
+ }
+}
+
+impl DetectedProxyApplyHelper {
+ pub fn new(host: H) -> Self {
+ Self { host }
+ }
+}
+
+impl ProxyApplyHelper for DetectedProxyApplyHelper
+where
+ H: ProxyfierDetectionHost,
+{
+ fn apply_proxy_config(
+ &self,
+ request: HelperApplyRequest<'_>,
+ ) -> Result {
+ let Some(detected) = detect_proxyfier_install_with_host(&self.host) else {
+ return staged_apply_result(request);
+ };
+
+ apply_to_detected_proxyfier(request, &detected)
+ }
+}
+
+#[tauri::command]
+pub async fn get_status(
+ state: tauri::State<'_, CommandState>,
+) -> Result {
+ let storage = state.storage();
+ tauri::async_runtime::spawn_blocking(move || build_status(&storage))
+ .await
+ .map_err(background_task_error)?
+}
+
+#[tauri::command]
+pub fn get_saved_state(
+ state: tauri::State<'_, CommandState>,
+) -> Result {
+ read_saved_state(&state.storage())
+}
+
+#[tauri::command]
+pub fn get_profiles(
+ state: tauri::State<'_, CommandState>,
+) -> Result, CommandError> {
+ read_profiles(&state.storage())
+}
+
+#[tauri::command]
+pub fn save_profile(
+ state: tauri::State<'_, CommandState>,
+ input: ProfileInputDto,
+) -> Result {
+ save_profile_to_storage(&state.storage(), input)
+}
+
+#[tauri::command]
+pub fn get_targets(state: tauri::State<'_, CommandState>) -> Result, CommandError> {
+ read_targets(&state.storage())
+}
+
+#[tauri::command]
+pub fn save_target(
+ state: tauri::State<'_, CommandState>,
+ input: TargetInputDto,
+) -> Result {
+ save_target_to_storage(&state.storage(), input)
+}
+
+#[tauri::command]
+pub async fn get_components(
+ state: tauri::State<'_, CommandState>,
+) -> Result, CommandError> {
+ let storage = state.storage();
+ tauri::async_runtime::spawn_blocking(move || read_components(&storage))
+ .await
+ .map_err(background_task_error)?
+}
+
+#[tauri::command]
+pub async fn get_proxifyre_setup_status() -> Result {
+ tauri::async_runtime::spawn_blocking(build_proxifyre_setup_status)
+ .await
+ .map_err(background_task_error)
+}
+
+#[tauri::command]
+pub async fn get_singbox_status(
+ state: tauri::State<'_, CommandState>,
+) -> Result {
+ let storage = state.storage();
+ tauri::async_runtime::spawn_blocking(move || read_singbox_status(&storage))
+ .await
+ .map_err(background_task_error)?
+}
+
+#[tauri::command]
+pub async fn get_singbox_setup_status() -> Result {
+ tauri::async_runtime::spawn_blocking(|| {
+ build_singbox_setup_status(detect_singbox_install().as_ref())
+ })
+ .await
+ .map_err(background_task_error)
+}
+
+#[tauri::command]
+pub fn resolve_profile_preview(
+ input: ProfileInputDto,
+) -> Result {
+ resolve_preview(input)
+}
+
+#[tauri::command]
+pub fn save_singbox_subscription(
+ state: tauri::State<'_, CommandState>,
+ input: SaveSingBoxSubscriptionInputDto,
+) -> Result {
+ save_singbox_subscription_to_storage(&state.storage(), input, &SystemClock)
+}
+
+#[tauri::command]
+pub async fn fetch_singbox_subscription(
+ state: tauri::State<'_, CommandState>,
+) -> Result {
+ let storage = state.storage();
+ tauri::async_runtime::spawn_blocking(move || {
+ fetch_singbox_subscription_with_fetcher(&storage, &SystemSubscriptionFetcher, &SystemClock)
+ })
+ .await
+ .map_err(background_task_error)?
+}
+
+#[tauri::command]
+pub fn forget_singbox_subscription(
+ state: tauri::State<'_, CommandState>,
+) -> Result {
+ forget_singbox_subscription_in_storage(&state.storage(), &SystemClock)
+}
+
+#[tauri::command]
+pub fn select_singbox_server(
+ state: tauri::State<'_, CommandState>,
+ input: SelectSingBoxServerInputDto,
+) -> Result {
+ select_singbox_server_in_storage(&state.storage(), input, &SystemClock)
+}
+
+#[tauri::command]
+pub fn ping_singbox_server(
+ state: tauri::State<'_, CommandState>,
+ input: PingSingBoxServerInputDto,
+) -> Result {
+ ping_singbox_server_in_storage(&state.storage(), input)
+}
+
+#[tauri::command]
+pub fn ping_all_singbox_servers(
+ state: tauri::State<'_, CommandState>,
+) -> Result, CommandError> {
+ ping_all_singbox_servers_in_storage(&state.storage())
+}
+
+#[tauri::command]
+pub fn ping_proxy_target(input: PingProxyTargetInputDto) -> Result {
+ ping_proxy_target_endpoint(input)
+}
+
+#[tauri::command]
+pub fn generate_singbox_config(
+ state: tauri::State<'_, CommandState>,
+) -> Result {
+ let detected = detect_singbox_install();
+ let binary_path = detected
+ .as_ref()
+ .map(|detected| detected.executable_path.as_path());
+ generate_singbox_config_with_services(
+ &state.storage(),
+ &SingBoxAdapter::default(),
+ &SingBoxCommandChecker,
+ &SystemClock,
+ binary_path,
+ )
+}
+
+#[tauri::command]
+pub fn apply_profiles(
+ state: tauri::State<'_, CommandState>,
+) -> Result {
+ let storage = state.storage();
+ let adapter = ProxiFyreAdapter::default();
+ let helper = DetectedProxyApplyHelper::system();
+ let clock = SystemClock;
+
+ apply_profiles_with_services(&storage, &adapter, &helper, &clock)
+}
+
+#[tauri::command]
+pub fn get_logs(
+ state: tauri::State<'_, CommandState>,
+) -> Result, CommandError> {
+ read_activity(&state.storage())
+}
+
+#[tauri::command]
+pub fn open_config_location(state: tauri::State<'_, CommandState>) -> Result {
+ let storage = state.storage();
+ let generated_path = storage
+ .paths()
+ .generated_dir
+ .join("proxifyre-app-config.json");
+ let config_path = detect_proxyfier_install()
+ .and_then(|detected| detected.config_path)
+ .filter(|path| path.exists())
+ .or_else(|| generated_path.exists().then_some(generated_path.clone()));
+
+ let Some(config_path) = config_path else {
+ return open_folder(&storage.paths().generated_dir);
+ };
+
+ open_file_or_select(&config_path)?;
+ Ok(config_path.display().to_string())
+}
+
+#[tauri::command]
+pub async fn start_proxifyre_service() -> Result {
+ tauri::async_runtime::spawn_blocking(|| control_proxifyre_service(ServiceControlAction::Start))
+ .await
+ .map_err(background_task_error)?
+}
+
+#[tauri::command]
+pub async fn stop_proxifyre_service() -> Result {
+ tauri::async_runtime::spawn_blocking(|| control_proxifyre_service(ServiceControlAction::Stop))
+ .await
+ .map_err(background_task_error)?
+}
+
+#[tauri::command]
+pub async fn install_proxifyre(
+ state: tauri::State<'_, CommandState>,
+) -> Result {
+ let storage = state.storage();
+ tauri::async_runtime::spawn_blocking(move || install_proxifyre_component(&storage))
+ .await
+ .map_err(background_task_error)?
+}
+
+#[tauri::command]
+pub async fn uninstall_proxifyre() -> Result {
+ tauri::async_runtime::spawn_blocking(uninstall_proxifyre_component)
+ .await
+ .map_err(background_task_error)?
+}
+
+#[tauri::command]
+pub async fn start_singbox_service(
+ state: tauri::State<'_, CommandState>,
+) -> Result {
+ let storage = state.storage();
+ tauri::async_runtime::spawn_blocking(move || {
+ let detected = detect_singbox_install();
+ let binary_path = detected
+ .as_ref()
+ .map(|detected| detected.executable_path.as_path());
+ let generated = generate_singbox_config_with_services(
+ &storage,
+ &SingBoxAdapter::default(),
+ &SingBoxCommandChecker,
+ &SystemClock,
+ binary_path,
+ )?;
+ let generated_path = PathBuf::from(generated.generated_config_path);
+ control_singbox_service(SingBoxServiceAction::Start, Some(generated_path.as_path()))
+ })
+ .await
+ .map_err(background_task_error)?
+}
+
+#[tauri::command]
+pub async fn stop_singbox_service() -> Result {
+ tauri::async_runtime::spawn_blocking(|| control_singbox_service(SingBoxServiceAction::Stop, None))
+ .await
+ .map_err(background_task_error)?
+}
+
+#[tauri::command]
+pub async fn install_singbox(
+ state: tauri::State<'_, CommandState>,
+) -> Result {
+ let storage = state.storage();
+ tauri::async_runtime::spawn_blocking(move || install_singbox_component(&storage))
+ .await
+ .map_err(background_task_error)?
+}
+
+#[tauri::command]
+pub async fn uninstall_singbox() -> Result {
+ tauri::async_runtime::spawn_blocking(uninstall_singbox_component)
+ .await
+ .map_err(background_task_error)?
+}
+
+pub fn build_status(storage: &JsonStorage) -> Result {
+ let profiles = storage.read_profiles().map_err(storage_error)?;
+ let targets = storage.read_targets().map_err(storage_error)?;
+ let components = components_or_defaults(storage)?;
+ let activity = storage.read_activity().map_err(storage_error)?;
+ let active_profile_count = profiles.iter().filter(|profile| profile.enabled).count();
+ let routed_app_count = profiles
+ .iter()
+ .filter(|profile| profile.enabled)
+ .map(|profile| profile.items.len())
+ .sum();
+ let active_target = profiles
+ .iter()
+ .find(|profile| profile.enabled)
+ .and_then(|profile| targets.iter().find(|target| target.id == profile.target_id));
+ let route_line = route_line(active_target);
+
+ Ok(StatusResponse {
+ route_line,
+ active_profile_count,
+ routed_app_count,
+ active_target: active_target.map(TargetDto::from),
+ components: components.iter().map(ComponentStatusDto::from).collect(),
+ recent_activity: activity
+ .iter()
+ .take(10)
+ .map(ActivityEntryDto::from)
+ .collect(),
+ generated_config_path: storage
+ .paths()
+ .generated_dir
+ .join("proxifyre-app-config.json")
+ .display()
+ .to_string(),
+ })
+}
+
+pub fn read_profiles(storage: &JsonStorage) -> Result, CommandError> {
+ storage
+ .read_profiles()
+ .map_err(storage_error)
+ .map(|profiles| profiles.iter().map(ProfileDto::from).collect())
+}
+
+pub fn save_profile_to_storage(
+ storage: &JsonStorage,
+ input: ProfileInputDto,
+) -> Result {
+ let profile = normalize_profile(input.into()).map_err(validation_error)?;
+ let mut profiles = storage.read_profiles().map_err(storage_error)?;
+
+ match profiles
+ .iter()
+ .position(|existing| existing.id == profile.id)
+ {
+ Some(index) => profiles[index] = profile.clone(),
+ None => profiles.push(profile.clone()),
+ }
+
+ storage.write_profiles(&profiles).map_err(storage_error)?;
+ Ok(ProfileDto::from(&profile))
+}
+
+pub fn read_targets(storage: &JsonStorage) -> Result, CommandError> {
+ storage
+ .read_targets()
+ .map_err(storage_error)
+ .map(|targets| targets.iter().map(TargetDto::from).collect())
+}
+
+pub fn save_target_to_storage(
+ storage: &JsonStorage,
+ input: TargetInputDto,
+) -> Result {
+ let target = normalize_target(input.into()).map_err(validation_error)?;
+ let mut targets = storage.read_targets().map_err(storage_error)?;
+
+ match targets.iter().position(|existing| existing.id == target.id) {
+ Some(index) => targets[index] = target.clone(),
+ None => targets.push(target.clone()),
+ }
+
+ storage.write_targets(&targets).map_err(storage_error)?;
+ Ok(TargetDto::from(&target))
+}
+
+pub fn read_components(storage: &JsonStorage) -> Result, CommandError> {
+ components_or_defaults(storage).map(|components| {
+ components
+ .iter()
+ .map(ComponentStatusDto::from)
+ .collect::>()
+ })
+}
+
+pub fn read_activity(storage: &JsonStorage) -> Result, CommandError> {
+ storage
+ .read_activity()
+ .map_err(storage_error)
+ .map(|entries| entries.iter().map(ActivityEntryDto::from).collect())
+}
+
+pub fn read_saved_state(storage: &JsonStorage) -> Result {
+ Ok(SavedStateResponse {
+ profiles: read_profiles(storage)?,
+ targets: read_targets(storage)?,
+ generated_config_path: storage
+ .paths()
+ .generated_dir
+ .join("proxifyre-app-config.json")
+ .display()
+ .to_string(),
+ })
+}
+
+pub fn resolve_preview(
+ input: ProfileInputDto,
+) -> Result {
+ let profile = normalize_profile(input.into()).map_err(validation_error)?;
+ let mut warnings = Vec::new();
+ let apps = profile
+ .items
+ .iter()
+ .map(|item| resolved_app(item, &mut warnings))
+ .collect();
+
+ Ok(ResolveProfilePreviewResponse {
+ profile_id: profile.id,
+ apps,
+ warnings,
+ })
+}
+
+pub fn apply_profiles_with_services(
+ storage: &JsonStorage,
+ adapter: &impl ProxyRouterAdapter,
+ helper: &impl ProxyApplyHelper,
+ clock: &impl Clock,
+) -> Result {
+ let profiles = storage.read_profiles().map_err(storage_error)?;
+ let targets = storage.read_targets().map_err(storage_error)?;
+ let components = components_or_defaults(storage)?;
+ let generated =
+ match adapter.generate_config(ProxyRouterRequest::new(&profiles, &targets, &components)) {
+ Ok(generated) => generated,
+ Err(error) => {
+ let command_error = adapter_error(error);
+ let activity = activity_for_apply_error(clock, &command_error);
+ storage.append_activity(activity).map_err(storage_error)?;
+ return Err(command_error);
+ }
+ };
+
+ let generated_path = storage
+ .paths()
+ .generated_dir
+ .join(generated.output_file_name.as_str());
+ write_generated_config(&generated_path, &generated.contents)?;
+
+ let helper_result = helper.apply_proxy_config(HelperApplyRequest {
+ adapter_id: generated.adapter_id.as_str(),
+ config_path: &generated_path,
+ config_contents: generated.contents.as_str(),
+ })?;
+
+ let activity = activity_for_apply(clock, &generated, &generated_path, &helper_result);
+ storage
+ .append_activity(activity.clone())
+ .map_err(storage_error)?;
+
+ Ok(ApplyProfilesResponse {
+ success: helper_result.success,
+ changed: helper_result.changed,
+ message: helper_result.message.clone(),
+ adapter_id: generated.adapter_id,
+ generated_config_path: generated_path.display().to_string(),
+ enabled_profiles: generated.enabled_profiles,
+ routed_apps: generated.routed_apps,
+ helper: helper_result,
+ activity: ActivityEntryDto::from(&activity),
+ })
+}
+
+pub fn read_singbox_status(
+ storage: &JsonStorage,
+) -> Result {
+ let config = storage.read_local_singbox_config().map_err(storage_error)?;
+ let cache = storage
+ .read_singbox_subscription_cache()
+ .map_err(storage_error)?;
+ let detected = detect_singbox_install();
+ let component = singbox_component_from_detection(detected.as_ref());
+
+ Ok(LocalSingBoxStatusResponse {
+ config: LocalSingBoxConfigDto::from(&config),
+ cache: cache.as_ref().map(SubscriptionCacheDto::from),
+ component: ComponentStatusDto::from(&component),
+ generated_config_path: storage
+ .paths()
+ .generated_dir
+ .join("sing-box-config.json")
+ .display()
+ .to_string(),
+ lan_listen_host: local_lan_ipv4(),
+ })
+}
+
+pub fn save_singbox_subscription_to_storage(
+ storage: &JsonStorage,
+ input: SaveSingBoxSubscriptionInputDto,
+ clock: &impl Clock,
+) -> Result {
+ let subscription_url = input.subscription_url.trim().to_string();
+ validate_subscription_url(&subscription_url)?;
+
+ let mut config = storage.read_local_singbox_config().map_err(storage_error)?;
+ config.subscription_url = Some(subscription_url);
+ config.updated_at = Some(clock.now());
+ storage
+ .write_local_singbox_config(&config)
+ .map_err(storage_error)?;
+
+ read_singbox_status(storage)
+}
+
+pub fn fetch_singbox_subscription_with_fetcher(
+ storage: &JsonStorage,
+ fetcher: &impl SubscriptionFetcher,
+ clock: &impl Clock,
+) -> Result {
+ let mut config = storage.read_local_singbox_config().map_err(storage_error)?;
+ let subscription_url = config
+ .subscription_url
+ .as_deref()
+ .map(str::trim)
+ .filter(|value| !value.is_empty())
+ .map(str::to_string)
+ .ok_or_else(|| {
+ CommandError::new(
+ "singbox_subscription_missing",
+ "Ссылка на подписку Local sing-box не сохранена.",
+ )
+ })?;
+
+ let cache = fetcher
+ .fetch_subscription(&subscription_url)
+ .map_err(|error| CommandError::new("singbox_subscription_fetch_failed", error.message))?;
+ let selected_tag = config
+ .selected_server_tag
+ .as_deref()
+ .filter(|tag| cache.servers.iter().any(|server| server.tag == *tag))
+ .map(str::to_string)
+ .or_else(|| cache.servers.first().map(|server| server.tag.clone()));
+
+ config.selected_server_tag = selected_tag;
+ config.updated_at = Some(clock.now());
+ storage
+ .write_singbox_subscription_cache(&cache)
+ .map_err(storage_error)?;
+ storage
+ .write_local_singbox_config(&config)
+ .map_err(storage_error)?;
+ storage
+ .append_activity(ActivityEntry {
+ id: "singbox-subscription-fetched".to_string(),
+ at: clock.now(),
+ level: ActivityLevel::Success,
+ title: "Подписка Local sing-box обновлена".to_string(),
+ message: format!("Серверов найдено: {}", cache.servers.len()),
+ })
+ .map_err(storage_error)?;
+
+ read_singbox_status(storage)
+}
+
+pub fn forget_singbox_subscription_in_storage(
+ storage: &JsonStorage,
+ clock: &impl Clock,
+) -> Result {
+ let mut config = storage.read_local_singbox_config().map_err(storage_error)?;
+ config.subscription_url = None;
+ config.selected_server_tag = None;
+ config.updated_at = Some(clock.now());
+ storage
+ .write_local_singbox_config(&config)
+ .map_err(storage_error)?;
+ storage
+ .remove_singbox_subscription_cache()
+ .map_err(storage_error)?;
+
+ read_singbox_status(storage)
+}
+
+pub fn select_singbox_server_in_storage(
+ storage: &JsonStorage,
+ input: SelectSingBoxServerInputDto,
+ clock: &impl Clock,
+) -> Result {
+ let requested_tag = input.tag.trim().to_string();
+ if requested_tag.is_empty() {
+ return Err(CommandError::new(
+ "singbox_server_tag_missing",
+ "Сервер Local sing-box не выбран.",
+ ));
+ }
+
+ let cache = storage
+ .read_singbox_subscription_cache()
+ .map_err(storage_error)?
+ .ok_or_else(|| {
+ CommandError::new(
+ "singbox_subscription_cache_missing",
+ "Сначала нужно загрузить подписку Local sing-box.",
+ )
+ })?;
+ let Some(server) = find_subscription_server(
+ &cache,
+ &requested_tag,
+ input.server.as_deref(),
+ input.server_port,
+ ) else {
+ return Err(CommandError::new(
+ "singbox_server_not_found",
+ format!("Сервер Local sing-box '{requested_tag}' не найден в текущей подписке."),
+ ));
+ };
+ let selected_tag = server.tag.clone();
+
+ let mut config = storage.read_local_singbox_config().map_err(storage_error)?;
+ config.selected_server_tag = Some(selected_tag);
+ config.updated_at = Some(clock.now());
+ storage
+ .write_local_singbox_config(&config)
+ .map_err(storage_error)?;
+
+ read_singbox_status(storage)
+}
+
+pub fn ping_singbox_server_in_storage(
+ storage: &JsonStorage,
+ input: PingSingBoxServerInputDto,
+) -> Result {
+ let tag = input.tag.trim();
+ let cache = read_required_singbox_cache(storage)?;
+ let server = find_subscription_server(&cache, tag, None, None).ok_or_else(|| {
+ CommandError::new(
+ "singbox_server_not_found",
+ format!("Сервер Local sing-box '{tag}' не найден в текущей подписке."),
+ )
+ })?;
+
+ Ok(ping_subscription_server(server))
+}
+
+pub fn ping_all_singbox_servers_in_storage(
+ storage: &JsonStorage,
+) -> Result, CommandError> {
+ let cache = read_required_singbox_cache(storage)?;
+ Ok(cache.servers.iter().map(ping_subscription_server).collect())
+}
+
+pub fn ping_proxy_target_endpoint(
+ input: PingProxyTargetInputDto,
+) -> Result {
+ let host = input.host.trim();
+ if host.is_empty() {
+ return Err(CommandError::new(
+ "proxy_target_host_missing",
+ "Хост внешнего прокси не указан.",
+ ));
+ }
+
+ Ok(ping_endpoint("external-proxy", host, input.port))
+}
+
+pub fn generate_singbox_config_with_services(
+ storage: &JsonStorage,
+ adapter: &SingBoxAdapter,
+ checker: &C,
+ clock: &impl Clock,
+ binary_path: Option<&Path>,
+) -> Result
+where
+ C: SingBoxConfigChecker,
+{
+ let config = storage.read_local_singbox_config().map_err(storage_error)?;
+ let cache = read_required_singbox_cache(storage)?;
+ let generated = adapter
+ .generate_config(
+ SingBoxGenerationRequest::new(&config, &cache, binary_path),
+ checker,
+ )
+ .map_err(singbox_adapter_error)?;
+ let generated_path = storage
+ .paths()
+ .generated_dir
+ .join(generated.output_file_name.as_str());
+
+ write_generated_config(&generated_path, &generated.contents)?;
+ ensure_local_singbox_target(storage, &config)?;
+
+ let activity = activity_for_singbox_generate(clock, &generated, &generated_path);
+ storage
+ .append_activity(activity.clone())
+ .map_err(storage_error)?;
+
+ Ok(GenerateSingBoxConfigResponse {
+ success: true,
+ message: "Конфиг Local sing-box создан".to_string(),
+ adapter_id: generated.adapter_id,
+ generated_config_path: generated_path.display().to_string(),
+ selected_server_tag: generated.selected_server_tag,
+ listen_host: generated.listen,
+ listen_port: generated.listen_port,
+ check: generated.check,
+ activity: ActivityEntryDto::from(&activity),
+ })
+}
+
+fn read_required_singbox_cache(storage: &JsonStorage) -> Result {
+ storage
+ .read_singbox_subscription_cache()
+ .map_err(storage_error)?
+ .ok_or_else(|| {
+ CommandError::new(
+ "singbox_subscription_cache_missing",
+ "Сначала нужно загрузить подписку Local sing-box.",
+ )
+ })
+}
+
+fn validate_subscription_url(subscription_url: &str) -> Result<(), CommandError> {
+ if subscription_url.is_empty() {
+ return Err(CommandError::new(
+ "singbox_subscription_url_missing",
+ "Ссылка на подписку Local sing-box не указана.",
+ ));
+ }
+
+ let parsed = url::Url::parse(subscription_url).map_err(|_| {
+ CommandError::new(
+ "singbox_subscription_url_invalid",
+ "Ссылка на подписку Local sing-box должна быть корректным URL.",
+ )
+ })?;
+ if !matches!(parsed.scheme(), "http" | "https") {
+ return Err(CommandError::new(
+ "singbox_subscription_url_invalid",
+ "Ссылка на подписку Local sing-box должна начинаться с http:// или https://.",
+ ));
+ }
+
+ Ok(())
+}
+
+fn ping_subscription_server(server: &SubscriptionServer) -> PingServerResponse {
+ ping_endpoint(&server.tag, &server.server, server.server_port)
+}
+
+fn ping_endpoint(tag: &str, server: &str, server_port: u16) -> PingServerResponse {
+ let started = Instant::now();
+ let addresses = match (server, server_port).to_socket_addrs() {
+ Ok(addresses) => addresses.collect::>(),
+ Err(error) => {
+ return PingServerResponse {
+ tag: tag.to_string(),
+ server: server.to_string(),
+ server_port,
+ ok: false,
+ latency: None,
+ error: Some(format!("DNS/адрес недоступен: {error}")),
+ };
+ }
+ };
+
+ if addresses.is_empty() {
+ return PingServerResponse {
+ tag: tag.to_string(),
+ server: server.to_string(),
+ server_port,
+ ok: false,
+ latency: None,
+ error: Some("DNS не вернул адреса".to_string()),
+ };
+ }
+
+ let timeout = Duration::from_secs(2);
+ let mut last_error = None;
+ for address in addresses {
+ match TcpStream::connect_timeout(&address, timeout) {
+ Ok(_) => {
+ return PingServerResponse {
+ tag: tag.to_string(),
+ server: server.to_string(),
+ server_port,
+ ok: true,
+ latency: Some(started.elapsed().as_millis()),
+ error: None,
+ };
+ }
+ Err(error) => last_error = Some(error.to_string()),
+ }
+ }
+
+ PingServerResponse {
+ tag: tag.to_string(),
+ server: server.to_string(),
+ server_port,
+ ok: false,
+ latency: None,
+ error: last_error,
+ }
+}
+
+fn local_lan_ipv4() -> Option {
+ let socket = UdpSocket::bind("0.0.0.0:0").ok()?;
+ socket.connect("8.8.8.8:80").ok()?;
+ let IpAddr::V4(address) = socket.local_addr().ok()?.ip() else {
+ return None;
+ };
+ if address.is_loopback() || address.is_link_local() || address.is_unspecified() {
+ return None;
+ }
+ Some(address.to_string())
+}
+
+fn find_subscription_server<'a>(
+ cache: &'a SubscriptionCache,
+ requested_tag: &str,
+ requested_server: Option<&str>,
+ requested_port: Option,
+) -> Option<&'a SubscriptionServer> {
+ cache
+ .servers
+ .iter()
+ .find(|server| server.tag == requested_tag)
+ .or_else(|| {
+ let requested = comparable_server_tag(requested_tag);
+ cache
+ .servers
+ .iter()
+ .find(|server| comparable_server_tag(&server.tag) == requested)
+ })
+ .or_else(|| {
+ let server_name = requested_server?.trim();
+ let server_port = requested_port?;
+ cache.servers.iter().find(|server| {
+ server.server.eq_ignore_ascii_case(server_name) && server.server_port == server_port
+ })
+ })
+}
+
+fn comparable_server_tag(value: &str) -> String {
+ value
+ .chars()
+ .filter(|ch| !matches!(ch, '\u{fe0e}' | '\u{fe0f}' | '\u{200d}'))
+ .collect::()
+ .split_whitespace()
+ .collect::>()
+ .join(" ")
+}
+
+fn ensure_local_singbox_target(
+ storage: &JsonStorage,
+ config: &LocalSingBoxConfig,
+) -> Result<(), CommandError> {
+ let mut targets = storage.read_targets().map_err(storage_error)?;
+ let target = Target {
+ id: "local-singbox".to_string(),
+ name: "Локальный sing-box".to_string(),
+ kind: TargetKind::Local,
+ protocol: ProxyProtocol::Socks5,
+ host: config.listen_host.clone(),
+ port: config.listen_port,
+ requires_component: Some(ComponentId::Singbox),
+ };
+
+ match targets.iter().position(|existing| existing.id == target.id) {
+ Some(index) => targets[index] = target,
+ None => targets.push(target),
+ }
+
+ storage.write_targets(&targets).map_err(storage_error)
+}
+
+fn activity_for_singbox_generate(
+ clock: &impl Clock,
+ generated: &SingBoxGeneratedConfig,
+ generated_path: &Path,
+) -> ActivityEntry {
+ ActivityEntry {
+ id: "singbox-config-generated".to_string(),
+ at: clock.now(),
+ level: ActivityLevel::Success,
+ title: "Конфиг Local sing-box создан".to_string(),
+ message: format!(
+ "Сервер: {}, listen: {}:{}, конфиг: {}",
+ generated.selected_server_tag,
+ generated.listen,
+ generated.listen_port,
+ generated_path.display()
+ ),
+ }
+}
+
+fn singbox_adapter_error(error: SingBoxConfigError) -> CommandError {
+ let code = match error.kind {
+ SingBoxConfigErrorKind::MissingSelectedServer => "singbox_server_not_selected",
+ SingBoxConfigErrorKind::MissingSelectedOutbound => "singbox_selected_server_missing",
+ SingBoxConfigErrorKind::UnsupportedSelectedOutbound => {
+ "singbox_selected_server_unsupported"
+ }
+ SingBoxConfigErrorKind::Serialization => "serialization_error",
+ SingBoxConfigErrorKind::CheckFailed => "singbox_check_failed",
+ };
+
+ CommandError::new(code, error.message)
+}
+
+fn control_singbox_service(
+ action: SingBoxServiceAction,
+ config_source: Option<&Path>,
+) -> Result {
+ let Some(detected) = detect_singbox_install() else {
+ return Err(CommandError::new(
+ "singbox_not_found",
+ "Local sing-box не найден на компьютере.",
+ ));
+ };
+
+ let config_target = config_source.map(|_| detected.install_dir.join("config.json"));
+ let script = service_control_script(
+ action,
+ &detected.service_name,
+ config_source,
+ config_target.as_deref(),
+ );
+ let output = Command::new("powershell")
+ .args([
+ "-NoProfile",
+ "-NonInteractive",
+ "-ExecutionPolicy",
+ "Bypass",
+ "-Command",
+ script.as_str(),
+ ])
+ .output()
+ .map_err(|error| {
+ CommandError::new(
+ singbox_service_error_code(action),
+ format!(
+ "Не удалось {} службу Local sing-box: {error}",
+ action.label()
+ ),
+ )
+ })?;
+ let result = parse_singbox_service_command_output(&output.stdout).ok_or_else(|| {
+ CommandError::new(
+ singbox_service_error_code(action),
+ singbox_service_script_failed_message(action, output.status.code()),
+ )
+ })?;
+
+ if result.success {
+ let refreshed = detect_singbox_install();
+ let component = singbox_component_from_detection(refreshed.as_ref());
+ return Ok(ComponentStatusDto::from(&component));
+ }
+
+ if matches!(
+ result.code.as_str(),
+ "start_failed" | "stop_failed" | "config_sync_failed"
+ ) {
+ run_elevated_singbox_service_command(
+ action,
+ &detected.service_name,
+ config_source,
+ config_target.as_deref(),
+ &result,
+ )?;
+ let refreshed = detect_singbox_install();
+ let component = singbox_component_from_detection(refreshed.as_ref());
+ return Ok(ComponentStatusDto::from(&component));
+ }
+
+ Err(CommandError::new(
+ singbox_service_error_code(action),
+ singbox_service_command_failed_message(action, &result),
+ ))
+}
+
+fn run_elevated_singbox_service_command(
+ action: SingBoxServiceAction,
+ service_name: &str,
+ config_source: Option<&Path>,
+ config_target: Option<&Path>,
+ direct_result: &SingBoxServiceCommandOutput,
+) -> Result<(), CommandError> {
+ let script_path =
+ write_elevated_singbox_service_script(action, service_name, config_source, config_target)?;
+ let launch_script = format!(
+ "$p = Start-Process -FilePath 'powershell.exe' -Verb RunAs -Wait -PassThru -WindowStyle Hidden -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File','{}'); exit $p.ExitCode",
+ escape_powershell_single(&script_path.display().to_string())
+ );
+ let output = Command::new("powershell")
+ .args([
+ "-NoProfile",
+ "-NonInteractive",
+ "-ExecutionPolicy",
+ "Bypass",
+ "-Command",
+ launch_script.as_str(),
+ ])
+ .output();
+
+ let _ = fs::remove_file(&script_path);
+
+ match output {
+ Ok(output) if output.status.success() => Ok(()),
+ Ok(output) => Err(CommandError::new(
+ singbox_service_error_code(action),
+ elevated_singbox_service_failed_message(action, direct_result, output.status.code()),
+ )),
+ Err(error) => Err(CommandError::new(
+ singbox_service_error_code(action),
+ format!(
+ "Не удалось запросить права администратора, чтобы {} службу Local sing-box: {error}",
+ action.label()
+ ),
+ )),
+ }
+}
+
+fn write_elevated_singbox_service_script(
+ action: SingBoxServiceAction,
+ service_name: &str,
+ config_source: Option<&Path>,
+ config_target: Option<&Path>,
+) -> Result {
+ let nonce = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .map(|duration| duration.as_millis())
+ .unwrap_or(0);
+ let script_path = env::temp_dir().join(format!("proxywarden-singbox-service-{nonce}.ps1"));
+ let script = elevated_singbox_service_script(action, service_name, config_source, config_target);
+
+ write_powershell_script(&script_path, &script).map_err(|error| {
+ CommandError::new(
+ singbox_service_error_code(action),
+ format!(
+ "Не удалось подготовить временный скрипт для управления Local sing-box '{}': {error}",
+ script_path.display()
+ ),
+ )
+ })?;
+
+ Ok(script_path)
+}
+
+fn elevated_singbox_service_script(
+ action: SingBoxServiceAction,
+ service_name: &str,
+ config_source: Option<&Path>,
+ config_target: Option<&Path>,
+) -> String {
+ let action_name = action.action_name();
+ let escaped_service_name = escape_powershell_single(service_name);
+ let escaped_config_source = config_source
+ .map(|path| escape_powershell_single(&path.display().to_string()))
+ .unwrap_or_default();
+ let escaped_config_target = config_target
+ .map(|path| escape_powershell_single(&path.display().to_string()))
+ .unwrap_or_default();
+
+ format!(
+ r#"
+$ErrorActionPreference = 'SilentlyContinue'
+$serviceName = '{escaped_service_name}'
+$action = '{action_name}'
+$configSource = '{escaped_config_source}'
+$configTarget = '{escaped_config_target}'
+
+if ($action -eq 'start') {{
+ if (-not [string]::IsNullOrWhiteSpace($configSource)) {{
+ if (-not (Test-Path -LiteralPath $configSource)) {{ exit 5 }}
+ if (-not [string]::IsNullOrWhiteSpace($configTarget)) {{
+ try {{
+ Copy-Item -LiteralPath $configSource -Destination $configTarget -Force -ErrorAction Stop
+ }} catch {{
+ exit 6
+ }}
+ }}
+ }}
+
+ $service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue
+ if ($null -eq $service) {{ exit 2 }}
+ if ($service.Status -eq 'Running') {{ exit 0 }}
+
+ Start-Service -Name $serviceName -ErrorAction SilentlyContinue
+ $service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue
+ if ($null -ne $service) {{
+ try {{ $service.WaitForStatus('Running', [TimeSpan]::FromSeconds(15)) }} catch {{}}
+ if ($service.Status -eq 'Running') {{ exit 0 }}
+ }}
+
+ exit 3
+}}
+
+$service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue
+if ($null -eq $service) {{ exit 2 }}
+if ($service.Status -eq 'Stopped') {{ exit 0 }}
+
+Stop-Service -Name $serviceName -Force -ErrorAction SilentlyContinue
+$service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue
+if ($null -ne $service) {{
+ try {{ $service.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(15)) }} catch {{}}
+ if ($service.Status -eq 'Stopped') {{ exit 0 }}
+}}
+
+exit 4
+"#
+ )
+}
+
+fn install_singbox_component(storage: &JsonStorage) -> Result {
+ let generated_config_path = storage.paths().generated_dir.join("sing-box-config.json");
+ run_elevated_singbox_package_script(
+ SingBoxPackageAction::Install,
+ include_str!("../../scripts/install-singbox.ps1"),
+ vec![
+ "-ConfigSource".to_string(),
+ generated_config_path.display().to_string(),
+ ],
+ &storage.paths().state_dir,
+ )?;
+
+ let refreshed = detect_singbox_install();
+ let Some(detected) = refreshed.as_ref() else {
+ return Err(CommandError::new(
+ SingBoxPackageAction::Install.error_code(),
+ "Установка Local sing-box завершилась, но приложение не найдено после проверки.",
+ ));
+ };
+
+ Ok(ComponentStatusDto::from(&singbox_component_from_detection(
+ Some(detected),
+ )))
+}
+
+fn uninstall_singbox_component() -> Result {
+ let Some(detected) = detect_singbox_install() else {
+ let component = singbox_component_from_detection(None);
+ return Ok(ComponentStatusDto::from(&component));
+ };
+
+ ensure_safe_singbox_install_dir(&detected.install_dir).map_err(|message| {
+ CommandError::new(SingBoxPackageAction::Uninstall.error_code(), message)
+ })?;
+ let artifact_dir = default_config_root().join("state");
+ run_elevated_singbox_package_script(
+ SingBoxPackageAction::Uninstall,
+ include_str!("../../scripts/install-singbox.ps1"),
+ vec![
+ "-InstallRoot".to_string(),
+ detected.install_dir.display().to_string(),
+ "-ServiceName".to_string(),
+ detected.service_name,
+ "-Uninstall".to_string(),
+ ],
+ &artifact_dir,
+ )?;
+
+ let refreshed = detect_singbox_install();
+ if refreshed.is_some() {
+ return Err(CommandError::new(
+ SingBoxPackageAction::Uninstall.error_code(),
+ "Удаление Local sing-box завершилось, но приложение все еще найдено на компьютере.",
+ ));
+ }
+
+ let component = singbox_component_from_detection(None);
+ Ok(ComponentStatusDto::from(&component))
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum SingBoxPackageAction {
+ Install,
+ Uninstall,
+}
+
+impl SingBoxPackageAction {
+ fn error_code(self) -> &'static str {
+ match self {
+ SingBoxPackageAction::Install => "singbox_install_failed",
+ SingBoxPackageAction::Uninstall => "singbox_uninstall_failed",
+ }
+ }
+
+ fn label(self) -> &'static str {
+ match self {
+ SingBoxPackageAction::Install => "установить",
+ SingBoxPackageAction::Uninstall => "удалить",
+ }
+ }
+
+ fn file_label(self) -> &'static str {
+ match self {
+ SingBoxPackageAction::Install => "install",
+ SingBoxPackageAction::Uninstall => "uninstall",
+ }
+ }
+}
+
+fn run_elevated_singbox_package_script(
+ action: SingBoxPackageAction,
+ installer_body: &str,
+ installer_args: Vec,
+ artifact_dir: &Path,
+) -> Result<(), CommandError> {
+ let nonce = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .map(|duration| duration.as_millis())
+ .unwrap_or(0);
+ fs::create_dir_all(artifact_dir).map_err(|error| {
+ CommandError::new(
+ action.error_code(),
+ format!(
+ "Не удалось создать папку для временных файлов Local sing-box '{}': {error}",
+ artifact_dir.display()
+ ),
+ )
+ })?;
+
+ let installer_path = artifact_dir.join(format!(
+ "proxywarden-singbox-{}-{nonce}.ps1",
+ action.file_label()
+ ));
+ let runner_path = artifact_dir.join(format!(
+ "proxywarden-singbox-{}-{nonce}.runner.ps1",
+ action.file_label()
+ ));
+ let result_path = artifact_dir.join(format!(
+ "proxywarden-singbox-{}-{nonce}.log",
+ action.file_label()
+ ));
+
+ write_powershell_script(&installer_path, installer_body).map_err(|error| {
+ CommandError::new(
+ action.error_code(),
+ format!(
+ "Не удалось подготовить установщик Local sing-box '{}': {error}",
+ installer_path.display()
+ ),
+ )
+ })?;
+ write_powershell_script(
+ &runner_path,
+ &singbox_installer_runner_script(&installer_path, &result_path, &installer_args),
+ )
+ .map_err(|error| {
+ CommandError::new(
+ action.error_code(),
+ format!(
+ "Не удалось подготовить runner Local sing-box '{}': {error}",
+ runner_path.display()
+ ),
+ )
+ })?;
+
+ let launch_script = format!(
+ r#"
+$ErrorActionPreference = 'Stop'
+$resultPath = '{}'
+try {{
+ $p = Start-Process -FilePath 'powershell.exe' -Verb RunAs -Wait -PassThru -WindowStyle Hidden -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File','{}')
+ if ($null -eq $p) {{
+ Set-Content -LiteralPath $resultPath -Value 'Elevated PowerShell не был запущен.' -Encoding UTF8
+ exit 1
+ }}
+ exit $p.ExitCode
+}} catch {{
+ Set-Content -LiteralPath $resultPath -Value ($_ | Out-String) -Encoding UTF8
+ exit 1
+}}
+"#,
+ escape_powershell_single(&result_path.display().to_string()),
+ escape_powershell_single(&runner_path.display().to_string())
+ );
+ let output = Command::new("powershell")
+ .args([
+ "-NoProfile",
+ "-NonInteractive",
+ "-ExecutionPolicy",
+ "Bypass",
+ "-Command",
+ launch_script.as_str(),
+ ])
+ .output();
+
+ let _ = fs::remove_file(&installer_path);
+ let _ = fs::remove_file(&runner_path);
+
+ match output {
+ Ok(output) if output.status.success() => {
+ let _ = fs::remove_file(&result_path);
+ Ok(())
+ }
+ Ok(output) => {
+ let details = package_failure_details(&result_path, &output);
+ let _ = fs::remove_file(&result_path);
+ Err(CommandError::new(
+ action.error_code(),
+ format!(
+ "Не удалось {} Local sing-box. Код elevated-команды: {}. {details}",
+ action.label(),
+ output.status.code().unwrap_or(-1),
+ ),
+ ))
+ }
+ Err(error) => Err(CommandError::new(
+ action.error_code(),
+ format!(
+ "Не удалось запросить права администратора, чтобы {} Local sing-box: {error}",
+ action.label()
+ ),
+ )),
+ }
+}
+
+pub(crate) fn singbox_installer_runner_script(
+ installer_path: &Path,
+ result_path: &Path,
+ installer_args: &[String],
+) -> String {
+ let args = installer_args
+ .iter()
+ .map(|arg| format!("'{}'", escape_powershell_single(arg)))
+ .collect::>()
+ .join(", ");
+
+ format!(
+ r#"
+$ErrorActionPreference = 'Stop'
+$installerPath = '{}'
+$resultPath = '{}'
+$stdoutPath = "$resultPath.stdout.log"
+$stderrPath = "$resultPath.stderr.log"
+$installerArgs = @({args})
+try {{
+ $output = & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $installerPath @installerArgs 2>&1
+ $exitCode = $LASTEXITCODE
+ Set-Content -LiteralPath $stdoutPath -Value ($output | Out-String) -Encoding UTF8
+ if ($exitCode -ne 0) {{
+ $stdout = if (Test-Path -LiteralPath $stdoutPath) {{ Get-Content -LiteralPath $stdoutPath -Raw }} else {{ '' }}
+ $stderr = if (Test-Path -LiteralPath $stderrPath) {{ Get-Content -LiteralPath $stderrPath -Raw }} else {{ '' }}
+ throw "install-singbox.ps1 завершился с кодом $exitCode. stdout: $stdout stderr: $stderr"
+ }}
+ Set-Content -LiteralPath $resultPath -Value 'ok' -Encoding UTF8
+ exit 0
+}} catch {{
+ Set-Content -LiteralPath $resultPath -Value ($_ | Out-String) -Encoding UTF8
+ exit 1
+}} finally {{
+ Remove-Item -LiteralPath $stdoutPath, $stderrPath -Force -ErrorAction SilentlyContinue
+}}
+"#,
+ escape_powershell_single(&installer_path.display().to_string()),
+ escape_powershell_single(&result_path.display().to_string())
+ )
+}
+
+fn singbox_service_error_code(action: SingBoxServiceAction) -> &'static str {
+ match action {
+ SingBoxServiceAction::Start => "singbox_service_start_failed",
+ SingBoxServiceAction::Stop => "singbox_service_stop_failed",
+ }
+}
+
+fn singbox_service_script_failed_message(
+ action: SingBoxServiceAction,
+ exit_code: Option,
+) -> String {
+ let exit_code = exit_code
+ .map(|code| format!(" Код выхода PowerShell: {code}."))
+ .unwrap_or_default();
+
+ format!(
+ "Не удалось {} службу Local sing-box: команда управления службой не вернула корректный результат.{exit_code}",
+ action.label()
+ )
+}
+
+fn singbox_service_command_failed_message(
+ action: SingBoxServiceAction,
+ result: &SingBoxServiceCommandOutput,
+) -> String {
+ let service_name = result
+ .service_name
+ .as_deref()
+ .filter(|value| !value.trim().is_empty())
+ .unwrap_or("ProxyWardenSingBox");
+ let status = result
+ .status
+ .as_deref()
+ .filter(|value| !value.trim().is_empty())
+ .unwrap_or("неизвестен");
+ let pid = result
+ .process_id
+ .filter(|value| *value > 0)
+ .map(|value| format!(", PID: {value}"))
+ .unwrap_or_default();
+
+ match result.code.as_str() {
+ "service_not_found" => "Служба Local sing-box не найдена.".to_string(),
+ "config_source_missing" => {
+ "Сгенерированный конфиг Local sing-box не найден перед запуском службы.".to_string()
+ }
+ "config_sync_failed" => {
+ "Не удалось обновить config.json службы Local sing-box перед запуском. Попробуй запустить приложение от имени администратора.".to_string()
+ }
+ "start_failed" => format!(
+ "Не удалось запустить службу {service_name}. Текущий статус: {status}{pid}. Попробуй запустить приложение от имени администратора."
+ ),
+ "stop_failed" => format!(
+ "Не удалось остановить службу {service_name}. Текущий статус: {status}{pid}. Запусти приложение от имени администратора или останови службу вручную в services.msc."
+ ),
+ _ => format!(
+ "Не удалось {} службу {service_name}. Текущий статус: {status}{pid}.",
+ action.label()
+ ),
+ }
+}
+
+fn elevated_singbox_service_failed_message(
+ action: SingBoxServiceAction,
+ direct_result: &SingBoxServiceCommandOutput,
+ exit_code: Option,
+) -> String {
+ let exit_code = exit_code
+ .map(|code| format!(" Код выхода elevated PowerShell: {code}."))
+ .unwrap_or_default();
+ format!(
+ "{} Попытка с правами администратора тоже не сработала.{exit_code}",
+ singbox_service_command_failed_message(action, direct_result)
+ )
+}
+
+fn components_or_defaults(storage: &JsonStorage) -> Result, CommandError> {
+ let components = storage.read_components().map_err(storage_error)?;
+ Ok(resolve_component_statuses(
+ components,
+ detect_proxyfier_install(),
+ detect_singbox_install(),
+ ))
+}
+
+pub fn resolve_component_statuses(
+ stored_components: Vec,
+ detected_proxyfier: Option,
+ detected_singbox: Option,
+) -> Vec {
+ let mut components = default_components();
+
+ for component in stored_components {
+ upsert_component(&mut components, component);
+ }
+
+ if detected_proxyfier.is_some() {
+ upsert_component(
+ &mut components,
+ proxyfier_component_from_detection(detected_proxyfier.as_ref()),
+ );
+ }
+ if detected_singbox.is_some() {
+ upsert_component(
+ &mut components,
+ singbox_component_from_detection(detected_singbox.as_ref()),
+ );
+ }
+
+ components
+}
+
+fn default_components() -> Vec {
+ vec![
+ ComponentStatus {
+ id: ComponentId::ControlApp,
+ name: "Приложение управления".to_string(),
+ state: ComponentState::Running,
+ installed: true,
+ running: true,
+ version: None,
+ path: None,
+ problems: Vec::new(),
+ actions: vec![
+ "Открыть журнал".to_string(),
+ "Скопировать диагностику".to_string(),
+ ],
+ },
+ ComponentStatus {
+ id: ComponentId::Proxyfier,
+ name: "ProxiFyre".to_string(),
+ state: ComponentState::Missing,
+ installed: false,
+ running: false,
+ version: None,
+ path: None,
+ problems: vec!["ProxiFyre нужен для маршрутизации выбранных приложений".to_string()],
+ actions: vec!["Установить ProxiFyre".to_string()],
+ },
+ ComponentStatus {
+ id: ComponentId::Singbox,
+ name: "Локальный sing-box".to_string(),
+ state: ComponentState::Missing,
+ installed: false,
+ running: false,
+ version: None,
+ path: None,
+ problems: Vec::new(),
+ actions: vec!["Установить локальный sing-box".to_string()],
+ },
+ ]
+}
+
+fn upsert_component(components: &mut Vec, component: ComponentStatus) {
+ match components
+ .iter()
+ .position(|existing| existing.id == component.id)
+ {
+ Some(index) => components[index] = component,
+ None => components.push(component),
+ }
+}
+
+fn route_line(active_target: Option<&Target>) -> String {
+ match active_target {
+ Some(target) if target.id == "local-singbox" => {
+ format!(
+ "Выбранные приложения -> ProxiFyre -> локальный sing-box {}:{} -> VPN",
+ target.host, target.port
+ )
+ }
+ Some(target) => format!(
+ "Выбранные приложения -> ProxiFyre -> внешний прокси {}:{}",
+ target.host, target.port
+ ),
+ None => "Выбранные приложения -> ProxiFyre -> внешний прокси".to_string(),
+ }
+}
+
+fn resolved_app(item: &ProfileItem, warnings: &mut Vec) -> ResolvedAppDto {
+ let mut notes = Vec::new();
+ match item.item_type {
+ ProfileItemType::Process => notes.push("Имя процесса используется напрямую".to_string()),
+ ProfileItemType::Folder => {
+ let note = "Сканирование папок отложено; ProxiFyre получает путь к папке";
+ notes.push(note.to_string());
+ warnings.push(note.to_string());
+ }
+ ProfileItemType::Exe => {
+ notes.push("Путь к EXE сохраняется для сопоставления в ProxiFyre".to_string())
+ }
+ }
+
+ ResolvedAppDto {
+ source_type: item.item_type.clone(),
+ source_value: item.value.clone(),
+ app_name: item.value.clone(),
+ notes,
+ }
+}
+
+fn write_generated_config(path: &Path, contents: &str) -> Result<(), CommandError> {
+ if let Some(parent) = path.parent() {
+ fs::create_dir_all(parent).map_err(storage_error)?;
+ }
+ fs::write(path, contents).map_err(storage_error)
+}
+
+fn open_file_or_select(path: &Path) -> Result<(), CommandError> {
+ let status = Command::new("notepad.exe")
+ .arg(path)
+ .spawn()
+ .map_err(|error| {
+ CommandError::new(
+ "open_config_failed",
+ format!("Не удалось открыть конфиг '{}': {error}", path.display()),
+ )
+ })?;
+
+ drop(status);
+ Ok(())
+}
+
+fn open_folder(path: &Path) -> Result {
+ fs::create_dir_all(path).map_err(storage_error)?;
+ Command::new("explorer.exe")
+ .arg(path)
+ .spawn()
+ .map_err(|error| {
+ CommandError::new(
+ "open_config_failed",
+ format!("Не удалось открыть папку '{}': {error}", path.display()),
+ )
+ })?;
+
+ Ok(path.display().to_string())
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum ServiceControlAction {
+ Start,
+ Stop,
+}
+
+impl ServiceControlAction {
+ fn error_code(self) -> &'static str {
+ match self {
+ ServiceControlAction::Start => "proxifyre_service_start_failed",
+ ServiceControlAction::Stop => "proxifyre_service_stop_failed",
+ }
+ }
+
+ fn label(self) -> &'static str {
+ match self {
+ ServiceControlAction::Start => "запустить",
+ ServiceControlAction::Stop => "остановить",
+ }
+ }
+}
+
+fn control_proxifyre_service(
+ action: ServiceControlAction,
+) -> Result {
+ let Some(detected) = detect_proxyfier_install() else {
+ return Err(CommandError::new(
+ "proxifyre_not_found",
+ "ProxiFyre не найден на компьютере.",
+ ));
+ };
+
+ run_proxifyre_service_command(action, &service_name_candidates(&detected))?;
+ let refreshed = detect_proxyfier_install();
+ let component = proxyfier_component_from_detection(refreshed.as_ref());
+
+ Ok(ComponentStatusDto::from(&component))
+}
+
+fn service_name_candidates(detected: &DetectedProxyfier) -> Vec {
+ let mut names = Vec::new();
+
+ if let Some(service_name) = &detected.service_name {
+ names.push(service_name.clone());
+ }
+ for service_name in ["ProxiFyreService", "ProxiFyre"] {
+ if !names
+ .iter()
+ .any(|existing| existing.eq_ignore_ascii_case(service_name))
+ {
+ names.push(service_name.to_string());
+ }
+ }
+
+ names
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct ServiceCommandOutput {
+ success: bool,
+ code: String,
+ service_name: Option,
+ status: Option,
+ process_id: Option,
+}
+
+fn run_proxifyre_service_command(
+ action: ServiceControlAction,
+ service_names: &[String],
+) -> Result<(), CommandError> {
+ let names = service_names
+ .iter()
+ .map(|name| format!("'{}'", escape_powershell_single(name)))
+ .collect::>()
+ .join(", ");
+ let action_name = match action {
+ ServiceControlAction::Start => "start",
+ ServiceControlAction::Stop => "stop",
+ };
+ let script = format!(
+ r#"
+$ErrorActionPreference = 'Stop'
+$names = @({names})
+$action = '{action_name}'
+$service = $null
+
+function Find-ProxiFyreService {{
+ foreach ($name in $names) {{
+ $candidate = Get-Service -Name $name -ErrorAction SilentlyContinue
+ if ($null -ne $candidate) {{ return $candidate }}
+ }}
+
+ return Get-Service |
+ Where-Object {{ $_.Name -match 'ProxiFyre|Proxifyre' -or $_.DisplayName -match 'ProxiFyre|Proxifyre' }} |
+ Select-Object -First 1
+}}
+
+function Get-ServiceProcessId([string]$name) {{
+ $escapedName = $name.Replace("'", "''")
+ $record = Get-CimInstance Win32_Service -Filter "Name='$escapedName'" -ErrorAction SilentlyContinue
+ if ($null -eq $record) {{ return 0 }}
+ return [int]$record.ProcessId
+}}
+
+function Get-ServiceStatus([string]$name) {{
+ $current = Get-Service -Name $name -ErrorAction SilentlyContinue
+ if ($null -eq $current) {{ return $null }}
+ return $current.Status.ToString()
+}}
+
+function Write-ServiceResult([bool]$success, [string]$code, [string]$status, [int]$processId) {{
+ [PSCustomObject]@{{
+ success = $success
+ code = $code
+ serviceName = if ($null -ne $service) {{ $service.Name }} else {{ $null }}
+ status = $status
+ processId = $processId
+ }} | ConvertTo-Json -Compress
+ exit 0
+}}
+
+$service = Find-ProxiFyreService
+if ($null -eq $service) {{
+ Write-ServiceResult $false 'service_not_found' $null 0
+}}
+
+$status = $service.Status.ToString()
+$processId = Get-ServiceProcessId $service.Name
+
+if ($action -eq 'start') {{
+ if ($status -eq 'Running') {{
+ Write-ServiceResult $true 'already_running' $status $processId
+ }}
+
+ try {{
+ Start-Service -Name $service.Name -ErrorAction Stop
+ $service = Get-Service -Name $service.Name
+ $service.WaitForStatus('Running', [TimeSpan]::FromSeconds(15))
+ }} catch {{
+ Write-ServiceResult $false 'start_failed' (Get-ServiceStatus $service.Name) (Get-ServiceProcessId $service.Name)
+ }}
+
+ Write-ServiceResult ($service.Status -eq 'Running') 'started' $service.Status.ToString() (Get-ServiceProcessId $service.Name)
+}}
+
+if ($status -eq 'Stopped') {{
+ Write-ServiceResult $true 'already_stopped' $status $processId
+}}
+
+try {{
+ if ($service.CanStop) {{
+ Stop-Service -Name $service.Name -Force -ErrorAction Stop
+ }}
+}} catch {{}}
+
+try {{
+ $service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
+ if ($null -ne $service -and $service.Status -ne 'Stopped') {{
+ $null = & sc.exe stop $service.Name 2>$null
+ }}
+}} catch {{}}
+
+try {{
+ $service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
+ if ($null -ne $service -and $service.Status -ne 'Stopped') {{
+ $service.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(8))
+ }}
+}} catch {{}}
+
+$status = Get-ServiceStatus $service.Name
+$processId = Get-ServiceProcessId $service.Name
+if ($status -ne 'Stopped' -and $processId -gt 0) {{
+ try {{
+ $null = & taskkill.exe /PID $processId /F 2>$null
+ Start-Sleep -Milliseconds 700
+ $service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
+ if ($null -ne $service) {{
+ $service.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(8))
+ }}
+ }} catch {{}}
+}}
+
+$status = Get-ServiceStatus $service.Name
+$processId = Get-ServiceProcessId $service.Name
+if ($status -eq 'Stopped') {{
+ Write-ServiceResult $true 'stopped' $status $processId
+}}
+
+Write-ServiceResult $false 'stop_failed' $status $processId
+"#
+ );
+
+ let output = Command::new("powershell")
+ .args([
+ "-NoProfile",
+ "-NonInteractive",
+ "-ExecutionPolicy",
+ "Bypass",
+ "-Command",
+ script.as_str(),
+ ])
+ .output()
+ .map_err(|error| {
+ CommandError::new(
+ action.error_code(),
+ format!("Не удалось {} службу ProxiFyre: {error}", action.label()),
+ )
+ })?;
+
+ let result = parse_service_command_output(&output.stdout).ok_or_else(|| {
+ CommandError::new(
+ action.error_code(),
+ service_script_failed_message(action, output.status.code()),
+ )
+ })?;
+
+ if result.success {
+ return Ok(());
+ }
+
+ if matches!(result.code.as_str(), "start_failed" | "stop_failed") {
+ run_elevated_proxifyre_service_command(action, service_names, &result)?;
+ return Ok(());
+ }
+
+ Err(CommandError::new(
+ action.error_code(),
+ service_command_failed_message(action, &result),
+ ))
+}
+
+fn run_elevated_proxifyre_service_command(
+ action: ServiceControlAction,
+ service_names: &[String],
+ direct_result: &ServiceCommandOutput,
+) -> Result<(), CommandError> {
+ let script_path = write_elevated_service_script(action, service_names)?;
+ let launch_script = format!(
+ "$p = Start-Process -FilePath 'powershell.exe' -Verb RunAs -Wait -PassThru -WindowStyle Hidden -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File','{}'); exit $p.ExitCode",
+ escape_powershell_single(&script_path.display().to_string())
+ );
+ let output = Command::new("powershell")
+ .args([
+ "-NoProfile",
+ "-NonInteractive",
+ "-ExecutionPolicy",
+ "Bypass",
+ "-Command",
+ launch_script.as_str(),
+ ])
+ .output();
+
+ let _ = fs::remove_file(&script_path);
+
+ match output {
+ Ok(output) if output.status.success() => Ok(()),
+ Ok(output) => Err(CommandError::new(
+ action.error_code(),
+ elevated_service_failed_message(action, direct_result, output.status.code()),
+ )),
+ Err(error) => Err(CommandError::new(
+ action.error_code(),
+ format!(
+ "Не удалось запросить права администратора, чтобы {} службу ProxiFyre: {error}",
+ action.label()
+ ),
+ )),
+ }
+}
+
+fn write_elevated_service_script(
+ action: ServiceControlAction,
+ service_names: &[String],
+) -> Result {
+ let nonce = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .map(|duration| duration.as_millis())
+ .unwrap_or(0);
+ let script_path = env::temp_dir().join(format!("proxywarden-proxifyre-service-{nonce}.ps1"));
+ let script = elevated_service_script(action, service_names);
+
+ write_powershell_script(&script_path, &script).map_err(|error| {
+ CommandError::new(
+ action.error_code(),
+ format!(
+ "Не удалось подготовить временный скрипт для управления ProxiFyre '{}': {error}",
+ script_path.display()
+ ),
+ )
+ })?;
+
+ Ok(script_path)
+}
+
+fn elevated_service_script(action: ServiceControlAction, service_names: &[String]) -> String {
+ let names = service_names
+ .iter()
+ .map(|name| format!("'{}'", escape_powershell_single(name)))
+ .collect::>()
+ .join(", ");
+ let action_name = match action {
+ ServiceControlAction::Start => "start",
+ ServiceControlAction::Stop => "stop",
+ };
+
+ format!(
+ r#"
+$ErrorActionPreference = 'SilentlyContinue'
+$names = @({names})
+$action = '{action_name}'
+$service = $null
+
+foreach ($name in $names) {{
+ $service = Get-Service -Name $name -ErrorAction SilentlyContinue
+ if ($null -ne $service) {{ break }}
+}}
+
+if ($null -eq $service) {{
+ $service = Get-Service |
+ Where-Object {{ $_.Name -match 'ProxiFyre|Proxifyre' -or $_.DisplayName -match 'ProxiFyre|Proxifyre' }} |
+ Select-Object -First 1
+}}
+
+if ($null -eq $service) {{ exit 2 }}
+
+function Get-ServiceProcessId([string]$name) {{
+ $escapedName = $name.Replace("'", "''")
+ $record = Get-CimInstance Win32_Service -Filter "Name='$escapedName'" -ErrorAction SilentlyContinue
+ if ($null -eq $record) {{ return 0 }}
+ return [int]$record.ProcessId
+}}
+
+if ($action -eq 'start') {{
+ if ($service.Status -eq 'Running') {{ exit 0 }}
+ Start-Service -Name $service.Name -ErrorAction SilentlyContinue
+ $service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
+ if ($null -ne $service) {{
+ try {{ $service.WaitForStatus('Running', [TimeSpan]::FromSeconds(15)) }} catch {{}}
+ if ($service.Status -eq 'Running') {{ exit 0 }}
+ }}
+ exit 3
+}}
+
+if ($service.Status -eq 'Stopped') {{ exit 0 }}
+
+if ($service.CanStop) {{
+ Stop-Service -Name $service.Name -Force -ErrorAction SilentlyContinue
+}}
+
+$service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
+if ($null -ne $service -and $service.Status -ne 'Stopped') {{
+ $null = & sc.exe stop $service.Name 2>$null
+}}
+
+$service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
+if ($null -ne $service -and $service.Status -ne 'Stopped') {{
+ try {{ $service.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(8)) }} catch {{}}
+}}
+
+$service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
+if ($null -ne $service -and $service.Status -ne 'Stopped') {{
+ $processId = Get-ServiceProcessId $service.Name
+ if ($processId -gt 0) {{
+ $null = & taskkill.exe /PID $processId /F 2>$null
+ Start-Sleep -Milliseconds 700
+ $service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
+ if ($null -ne $service) {{
+ try {{ $service.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(8)) }} catch {{}}
+ }}
+ }}
+}}
+
+$service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
+if ($null -eq $service -or $service.Status -eq 'Stopped') {{ exit 0 }}
+exit 4
+"#
+ )
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum ProxiFyrePackageAction {
+ Install,
+ Uninstall,
+}
+
+impl ProxiFyrePackageAction {
+ fn error_code(self) -> &'static str {
+ match self {
+ ProxiFyrePackageAction::Install => "proxifyre_install_failed",
+ ProxiFyrePackageAction::Uninstall => "proxifyre_uninstall_failed",
+ }
+ }
+
+ fn label(self) -> &'static str {
+ match self {
+ ProxiFyrePackageAction::Install => "установить",
+ ProxiFyrePackageAction::Uninstall => "удалить",
+ }
+ }
+
+ fn file_label(self) -> &'static str {
+ match self {
+ ProxiFyrePackageAction::Install => "install",
+ ProxiFyrePackageAction::Uninstall => "uninstall",
+ }
+ }
+}
+
+fn install_proxifyre_component(storage: &JsonStorage) -> Result {
+ let generated_config_path = storage
+ .paths()
+ .generated_dir
+ .join("proxifyre-app-config.json");
+ let script = install_proxifyre_script(&generated_config_path);
+
+ run_elevated_package_script(
+ ProxiFyrePackageAction::Install,
+ script,
+ &storage.paths().state_dir,
+ )?;
+
+ let refreshed = detect_proxyfier_install();
+ let Some(detected) = refreshed.as_ref() else {
+ return Err(CommandError::new(
+ ProxiFyrePackageAction::Install.error_code(),
+ "Установка ProxiFyre завершилась, но приложение не найдено после проверки.",
+ ));
+ };
+
+ Ok(ComponentStatusDto::from(
+ &proxyfier_component_from_detection(Some(detected)),
+ ))
+}
+
+fn uninstall_proxifyre_component() -> Result {
+ let Some(detected) = detect_proxyfier_install() else {
+ let component = proxyfier_component_from_detection(None);
+ return Ok(ComponentStatusDto::from(&component));
+ };
+
+ ensure_safe_proxifyre_install_dir(&detected.install_dir)?;
+ let script = uninstall_proxifyre_script(&detected.install_dir, &detected.executable_path);
+
+ let artifact_dir = default_config_root().join("state");
+ run_elevated_package_script(ProxiFyrePackageAction::Uninstall, script, &artifact_dir)?;
+
+ let refreshed = detect_proxyfier_install();
+ if refreshed.is_some() {
+ return Err(CommandError::new(
+ ProxiFyrePackageAction::Uninstall.error_code(),
+ "Удаление ProxiFyre завершилось, но приложение все еще найдено на компьютере.",
+ ));
+ }
+
+ let component = proxyfier_component_from_detection(None);
+ Ok(ComponentStatusDto::from(&component))
+}
+
+fn build_proxifyre_setup_status() -> ProxiFyreSetupStatusDto {
+ let vc_runtime = detect_vc_runtime();
+ let packet_filter = detect_windows_packet_filter();
+ let proxifyre = detect_proxyfier_install();
+
+ let vc_runtime_item = setup_item_from_program(
+ "vc-runtime",
+ &format!("Microsoft Visual C++ Runtime ({})", runtime_arch_label()),
+ vc_runtime,
+ "Нужен для запуска ProxiFyre.exe. Установщик скачает официальный vc_redist от Microsoft.",
+ );
+ let packet_filter_item = setup_item_from_program(
+ "packet-filter",
+ "Windows Packet Filter",
+ packet_filter,
+ "Сетевой драйвер NT Kernel/WireSock, через который ProxiFyre перехватывает трафик приложений.",
+ );
+ let proxifyre_item = match proxifyre {
+ Some(detected) => ProxiFyreSetupItemDto {
+ id: "proxifyre".to_string(),
+ name: "ProxiFyre".to_string(),
+ installed: true,
+ version: Some(if detected.running {
+ "служба запущена".to_string()
+ } else {
+ "служба не запущена".to_string()
+ }),
+ details: detected.install_dir.display().to_string(),
+ },
+ None => ProxiFyreSetupItemDto {
+ id: "proxifyre".to_string(),
+ name: "ProxiFyre".to_string(),
+ installed: false,
+ version: None,
+ details: format!(
+ "Будет скачан с GitHub и распакован в {}.",
+ PROXIFYRE_INSTALL_DIR
+ ),
+ },
+ };
+
+ let items = vec![vc_runtime_item, packet_filter_item, proxifyre_item];
+ let missing_count = items.iter().filter(|item| !item.installed).count();
+
+ ProxiFyreSetupStatusDto {
+ ready: missing_count == 0,
+ missing_count,
+ items,
+ }
+}
+
+fn setup_item_from_program(
+ id: &str,
+ name: &str,
+ program: Option,
+ missing_details: &str,
+) -> ProxiFyreSetupItemDto {
+ match program {
+ Some(program) => ProxiFyreSetupItemDto {
+ id: id.to_string(),
+ name: name.to_string(),
+ installed: true,
+ version: program.display_version,
+ details: program.display_name,
+ },
+ None => ProxiFyreSetupItemDto {
+ id: id.to_string(),
+ name: name.to_string(),
+ installed: false,
+ version: None,
+ details: missing_details.to_string(),
+ },
+ }
+}
+
+#[derive(Debug, Clone)]
+struct InstalledProgram {
+ display_name: String,
+ display_version: Option,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "PascalCase")]
+struct InstalledProgramJson {
+ display_name: Option,
+ display_version: Option,
+}
+
+fn detect_vc_runtime() -> Option {
+ installed_program(&vc_runtime_registry_pattern())
+}
+
+fn detect_windows_packet_filter() -> Option {
+ installed_program("Windows Packet Filter|WinpkFilter|NDISAPI")
+}
+
+fn installed_program(pattern: &str) -> Option {
+ let script = format!(
+ r#"
+$paths = @(
+ 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*',
+ 'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*',
+ 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'
+)
+$program = Get-ItemProperty -Path $paths -ErrorAction SilentlyContinue |
+ Where-Object {{ $_.DisplayName -match '{}' }} |
+ Select-Object -First 1 DisplayName, DisplayVersion
+if ($null -ne $program) {{
+ $program | ConvertTo-Json -Compress
+}}
+"#,
+ escape_powershell_single(pattern)
+ );
+
+ let output = Command::new("powershell")
+ .args([
+ "-NoProfile",
+ "-NonInteractive",
+ "-ExecutionPolicy",
+ "Bypass",
+ "-Command",
+ script.as_str(),
+ ])
+ .output()
+ .ok()?;
+ if !output.status.success() {
+ return None;
+ }
+
+ let stdout = String::from_utf8_lossy(&output.stdout);
+ let payload = stdout.trim();
+ if payload.is_empty() || payload.eq_ignore_ascii_case("null") {
+ return None;
+ }
+
+ let parsed: InstalledProgramJson = serde_json::from_str(payload).ok()?;
+ let display_name = parsed.display_name?.trim().to_string();
+ if display_name.is_empty() {
+ return None;
+ }
+
+ Some(InstalledProgram {
+ display_name,
+ display_version: parsed
+ .display_version
+ .map(|version| version.trim().to_string())
+ .filter(|version| !version.is_empty()),
+ })
+}
+
+fn vc_runtime_registry_pattern() -> String {
+ let arch = runtime_arch_label();
+ if arch == "ARM64" {
+ return r"Microsoft Visual C\+\+.*Redistributable.*\((ARM64|x64)\)".to_string();
+ }
+
+ format!(r"Microsoft Visual C\+\+.*Redistributable.*\({arch}\)")
+}
+
+fn runtime_arch_label() -> &'static str {
+ if cfg!(target_arch = "aarch64") {
+ "ARM64"
+ } else if cfg!(target_arch = "x86") {
+ "x86"
+ } else {
+ "x64"
+ }
+}
+
+fn run_elevated_package_script(
+ action: ProxiFyrePackageAction,
+ body: String,
+ artifact_dir: &Path,
+) -> Result<(), CommandError> {
+ let nonce = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .map(|duration| duration.as_millis())
+ .unwrap_or(0);
+ fs::create_dir_all(artifact_dir).map_err(|error| {
+ CommandError::new(
+ action.error_code(),
+ format!(
+ "Не удалось создать папку для временных файлов ProxiFyre '{}': {error}",
+ artifact_dir.display()
+ ),
+ )
+ })?;
+ let script_path = artifact_dir.join(format!(
+ "proxywarden-proxifyre-{}-{nonce}.ps1",
+ action.file_label()
+ ));
+ let result_path = artifact_dir.join(format!(
+ "proxywarden-proxifyre-{}-{nonce}.log",
+ action.file_label()
+ ));
+ let script = wrap_elevated_package_script(&body, &result_path);
+
+ write_powershell_script(&script_path, &script).map_err(|error| {
+ CommandError::new(
+ action.error_code(),
+ format!(
+ "Не удалось подготовить временный скрипт, чтобы {} ProxiFyre '{}': {error}",
+ action.label(),
+ script_path.display()
+ ),
+ )
+ })?;
+
+ let launch_script = format!(
+ r#"
+$ErrorActionPreference = 'Stop'
+$resultPath = '{}'
+try {{
+ $p = Start-Process -FilePath 'powershell.exe' -Verb RunAs -Wait -PassThru -WindowStyle Hidden -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File','{}')
+ if ($null -eq $p) {{
+ Set-Content -LiteralPath $resultPath -Value 'Elevated PowerShell не был запущен.' -Encoding UTF8
+ exit 1
+ }}
+ exit $p.ExitCode
+}} catch {{
+ Set-Content -LiteralPath $resultPath -Value ($_ | Out-String) -Encoding UTF8
+ exit 1
+}}
+"#,
+ escape_powershell_single(&result_path.display().to_string()),
+ escape_powershell_single(&script_path.display().to_string())
+ );
+ let output = Command::new("powershell")
+ .args([
+ "-NoProfile",
+ "-NonInteractive",
+ "-ExecutionPolicy",
+ "Bypass",
+ "-Command",
+ launch_script.as_str(),
+ ])
+ .output();
+
+ let _ = fs::remove_file(&script_path);
+
+ match output {
+ Ok(output) if output.status.success() => {
+ let _ = fs::remove_file(&result_path);
+ Ok(())
+ }
+ Ok(output) => {
+ let details = package_failure_details(&result_path, &output);
+ let _ = fs::remove_file(&result_path);
+ Err(CommandError::new(
+ action.error_code(),
+ format!(
+ "Не удалось {} ProxiFyre. Код elevated-команды: {}. {details}",
+ action.label(),
+ output.status.code().unwrap_or(-1),
+ ),
+ ))
+ }
+ Err(error) => Err(CommandError::new(
+ action.error_code(),
+ format!(
+ "Не удалось запросить права администратора, чтобы {} ProxiFyre: {error}",
+ action.label()
+ ),
+ )),
+ }
+}
+
+pub(crate) fn wrap_elevated_package_script(body: &str, result_path: &Path) -> String {
+ let mut script = String::new();
+ script.push_str("$ErrorActionPreference = 'Stop'\n");
+ script.push_str(&format!(
+ "$resultPath = '{}'\n",
+ escape_powershell_single(&result_path.display().to_string())
+ ));
+ script.push_str("try {\n");
+ script.push_str(body);
+ script.push_str(
+ r#"
+ Set-Content -LiteralPath $resultPath -Value 'ok' -Encoding UTF8
+ exit 0
+} catch {
+ $message = ($_ | Out-String)
+ Set-Content -LiteralPath $resultPath -Value $message -Encoding UTF8
+ exit 1
+}
+"#,
+ );
+
+ script
+}
+
+fn write_powershell_script(path: &Path, script: &str) -> std::io::Result<()> {
+ let mut bytes = Vec::with_capacity(script.len() + 3);
+ bytes.extend_from_slice(&[0xEF, 0xBB, 0xBF]);
+ bytes.extend_from_slice(script.as_bytes());
+ fs::write(path, bytes)
+}
+
+pub(crate) fn install_proxifyre_script(generated_config_path: &Path) -> String {
+ let mut script = String::new();
+ script.push_str(&format!(
+ "$targetDir = '{}'\n",
+ escape_powershell_single(PROXIFYRE_INSTALL_DIR)
+ ));
+ script.push_str(&format!(
+ "$generatedConfigPath = '{}'\n",
+ escape_powershell_single(&generated_config_path.display().to_string())
+ ));
+ script.push_str(&format!(
+ "$proxifyreReleaseApi = '{}'\n",
+ escape_powershell_single(PROXIFYRE_RELEASE_API_URL)
+ ));
+ script.push_str(&format!(
+ "$ndisapiReleaseApi = '{}'\n",
+ escape_powershell_single(NDISAPI_RELEASE_API_URL)
+ ));
+ script.push_str(&format!(
+ "$vcRedistX64Url = '{}'\n",
+ escape_powershell_single(VC_REDIST_X64_URL)
+ ));
+ script.push_str(&format!(
+ "$vcRedistX86Url = '{}'\n",
+ escape_powershell_single(VC_REDIST_X86_URL)
+ ));
+ script.push_str(
+ r#"
+ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+
+ function Get-NativeArchitecture {
+ $processor = Get-CimInstance Win32_Processor | Select-Object -First 1
+ if ($null -ne $processor -and $processor.Architecture -eq 12) { return 'ARM64' }
+ if ([Environment]::Is64BitOperatingSystem) { return 'x64' }
+ return 'x86'
+ }
+
+ function Invoke-Download([string]$uri, [string]$path) {
+ Invoke-WebRequest -UseBasicParsing -Uri $uri -OutFile $path -Headers @{ 'User-Agent' = 'proxywarden' }
+ }
+
+ function Select-Asset($assets, [string]$pattern, [string]$label) {
+ $asset = $assets | Where-Object { $_.name -match $pattern } | Select-Object -First 1
+ if ($null -eq $asset) { throw "Не найден подходящий asset для $label ($pattern)." }
+ return $asset
+ }
+
+ function Verify-AssetHash([string]$path, $asset) {
+ if ($asset.digest -match '^sha256:(.+)$') {
+ $expected = $Matches[1].ToLowerInvariant()
+ $actual = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant()
+ if ($actual -ne $expected) {
+ throw "SHA256 не совпал для $($asset.name). Ожидалось $expected, получилось $actual."
+ }
+ }
+ }
+
+ function Assert-ExitCode($process, [string]$label) {
+ if ($process.ExitCode -ne 0 -and $process.ExitCode -ne 3010) {
+ throw "$label завершился с кодом $($process.ExitCode)."
+ }
+ }
+
+ function Get-InstalledProgram([string]$pattern) {
+ $paths = @(
+ 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*',
+ 'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*',
+ 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'
+ )
+ return Get-ItemProperty -Path $paths -ErrorAction SilentlyContinue |
+ Where-Object { $_.DisplayName -match $pattern } |
+ Select-Object -First 1
+ }
+
+ function Test-VcRuntime([string]$arch) {
+ $pattern = if ($arch -eq 'ARM64') {
+ 'Microsoft Visual C\+\+.*Redistributable.*\((ARM64|x64)\)'
+ } else {
+ "Microsoft Visual C\+\+.*Redistributable.*\($arch\)"
+ }
+
+ return $null -ne (Get-InstalledProgram $pattern)
+ }
+
+ function Test-WindowsPacketFilter {
+ return $null -ne (Get-InstalledProgram 'Windows Packet Filter|WinpkFilter|NDISAPI')
+ }
+
+ function Get-LogTail([string]$path) {
+ if (-not (Test-Path -LiteralPath $path)) { return '' }
+ return (Get-Content -LiteralPath $path -Tail 40 -ErrorAction SilentlyContinue) -join ' '
+ }
+
+ $arch = Get-NativeArchitecture
+ $workDir = Join-Path ([IO.Path]::GetTempPath()) 'proxywarden-proxifyre-install'
+ $extractDir = Join-Path $workDir 'proxifyre'
+ Remove-Item -LiteralPath $workDir -Recurse -Force -ErrorAction SilentlyContinue
+ New-Item -ItemType Directory -Force -Path $workDir, $extractDir, $targetDir | Out-Null
+
+ if (-not (Test-VcRuntime $arch)) {
+ $vcRedistPath = Join-Path $workDir 'vc_redist.exe'
+ $vcRedistUrl = if ($arch -eq 'x86') { $vcRedistX86Url } else { $vcRedistX64Url }
+ Invoke-Download $vcRedistUrl $vcRedistPath
+ $vcProcess = Start-Process -FilePath $vcRedistPath -ArgumentList @('/install', '/quiet', '/norestart') -Wait -PassThru -WindowStyle Hidden
+ if ($vcProcess.ExitCode -ne 0 -and $vcProcess.ExitCode -ne 3010 -and $vcProcess.ExitCode -ne 1638 -and -not (Test-VcRuntime $arch)) {
+ throw "Visual C++ Runtime завершился с кодом $($vcProcess.ExitCode)."
+ }
+ }
+
+ if (-not (Test-WindowsPacketFilter)) {
+ $ndisRelease = Invoke-RestMethod -Uri $ndisapiReleaseApi -Headers @{ 'User-Agent' = 'proxywarden' }
+ $ndisPattern = if ($arch -eq 'ARM64') { 'ARM64\.msi$' } elseif ($arch -eq 'x86') { 'x86\.msi$' } else { 'x64\.msi$' }
+ $ndisAsset = Select-Asset $ndisRelease.assets $ndisPattern 'Windows Packet Filter'
+ $ndisPath = Join-Path $workDir $ndisAsset.name
+ $ndisLogPath = Join-Path $workDir 'windows-packet-filter-install.log'
+ Invoke-Download $ndisAsset.browser_download_url $ndisPath
+ Verify-AssetHash $ndisPath $ndisAsset
+ $ndisProcess = Start-Process -FilePath 'msiexec.exe' -ArgumentList @('/i', $ndisPath, '/qn', '/norestart', '/L*v', $ndisLogPath) -Wait -PassThru -WindowStyle Hidden
+ if ($ndisProcess.ExitCode -ne 0 -and $ndisProcess.ExitCode -ne 3010 -and -not (Test-WindowsPacketFilter)) {
+ $ndisLogTail = Get-LogTail $ndisLogPath
+ throw "Windows Packet Filter завершился с кодом $($ndisProcess.ExitCode). MSI log: $ndisLogPath $ndisLogTail"
+ }
+ }
+
+ $proxifyreRelease = Invoke-RestMethod -Uri $proxifyreReleaseApi -Headers @{ 'User-Agent' = 'proxywarden' }
+ $proxifyrePattern = if ($arch -eq 'ARM64') { 'ARM64-signed\.zip$' } elseif ($arch -eq 'x86') { 'x86-signed\.zip$' } else { 'x64-signed\.zip$' }
+ $proxifyreAsset = Select-Asset $proxifyreRelease.assets $proxifyrePattern 'ProxiFyre'
+ $proxifyreZipPath = Join-Path $workDir $proxifyreAsset.name
+ Invoke-Download $proxifyreAsset.browser_download_url $proxifyreZipPath
+ Verify-AssetHash $proxifyreZipPath $proxifyreAsset
+
+ Expand-Archive -LiteralPath $proxifyreZipPath -DestinationPath $extractDir -Force
+ $proxifyreExe = Get-ChildItem -LiteralPath $extractDir -Recurse -Filter 'ProxiFyre.exe' | Select-Object -First 1
+ if ($null -eq $proxifyreExe) { throw 'В архиве ProxiFyre не найден ProxiFyre.exe.' }
+
+ Copy-Item -Path (Join-Path $proxifyreExe.Directory.FullName '*') -Destination $targetDir -Recurse -Force
+
+ $configTarget = Join-Path $targetDir 'app-config.json'
+ if (Test-Path -LiteralPath $generatedConfigPath) {
+ Copy-Item -LiteralPath $generatedConfigPath -Destination $configTarget -Force
+ } elseif (-not (Test-Path -LiteralPath $configTarget)) {
+ $emptyConfig = '{"logLevel":"Info","bypassLan":true,"proxies":[]}'
+ Set-Content -LiteralPath $configTarget -Value $emptyConfig -Encoding UTF8
+ }
+
+ Push-Location $targetDir
+ try {
+ & .\ProxiFyre.exe stop | Out-Null
+ & .\ProxiFyre.exe uninstall | Out-Null
+ & .\ProxiFyre.exe install
+ if ($LASTEXITCODE -ne 0) { throw "ProxiFyre.exe install завершился с кодом $LASTEXITCODE." }
+ & .\ProxiFyre.exe start
+ if ($LASTEXITCODE -ne 0) {
+ Start-Service -Name 'ProxiFyreService' -ErrorAction Stop
+ }
+ } finally {
+ Pop-Location
+ }
+"#,
+ );
+
+ script
+}
+
+fn uninstall_proxifyre_script(install_dir: &Path, executable_path: &Path) -> String {
+ let mut script = String::new();
+ script.push_str(&format!(
+ "$installDir = '{}'\n",
+ escape_powershell_single(&install_dir.display().to_string())
+ ));
+ script.push_str(&format!(
+ "$exePath = '{}'\n",
+ escape_powershell_single(&executable_path.display().to_string())
+ ));
+ script.push_str(
+ r#"
+ function Find-ProxiFyreService {
+ foreach ($name in @('ProxiFyreService', 'ProxiFyre')) {
+ $candidate = Get-Service -Name $name -ErrorAction SilentlyContinue
+ if ($null -ne $candidate) { return $candidate }
+ }
+
+ return Get-Service |
+ Where-Object { $_.Name -match 'ProxiFyre|Proxifyre' -or $_.DisplayName -match 'ProxiFyre|Proxifyre' } |
+ Select-Object -First 1
+ }
+
+ function Get-ServiceProcessId([string]$name) {
+ $escapedName = $name.Replace("'", "''")
+ $record = Get-CimInstance Win32_Service -Filter "Name='$escapedName'" -ErrorAction SilentlyContinue
+ if ($null -eq $record) { return 0 }
+ return [int]$record.ProcessId
+ }
+
+ $service = Find-ProxiFyreService
+ if ($null -ne $service -and $service.Status -ne 'Stopped') {
+ try {
+ if ($service.CanStop) { Stop-Service -Name $service.Name -Force -ErrorAction SilentlyContinue }
+ $service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
+ if ($null -ne $service) { $service.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(8)) }
+ } catch {}
+ }
+
+ $service = Find-ProxiFyreService
+ if ($null -ne $service -and $service.Status -ne 'Stopped') {
+ $processId = Get-ServiceProcessId $service.Name
+ if ($processId -gt 0) {
+ taskkill.exe /PID $processId /F | Out-Null
+ Start-Sleep -Milliseconds 700
+ }
+ }
+
+ if (Test-Path -LiteralPath $exePath) {
+ Push-Location (Split-Path -Parent $exePath)
+ try {
+ & $exePath uninstall | Out-Null
+ } finally {
+ Pop-Location
+ }
+ }
+
+ $service = Find-ProxiFyreService
+ if ($null -ne $service) {
+ sc.exe delete $service.Name | Out-Null
+ }
+
+ Get-Process -Name 'ProxiFyre' -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
+
+ if (Test-Path -LiteralPath $installDir) {
+ Remove-Item -LiteralPath $installDir -Recurse -Force
+ }
+"#,
+ );
+
+ script
+}
+
+fn ensure_safe_proxifyre_install_dir(path: &Path) -> Result<(), CommandError> {
+ let name = path
+ .file_name()
+ .and_then(|value| value.to_str())
+ .map(|value| value.to_ascii_lowercase())
+ .unwrap_or_default();
+
+ if path.parent().is_some() && name.contains("proxifyre") {
+ return Ok(());
+ }
+
+ Err(CommandError::new(
+ ProxiFyrePackageAction::Uninstall.error_code(),
+ format!(
+ "Отказываюсь рекурсивно удалять папку ProxiFyre с небезопасным путем: {}",
+ path.display()
+ ),
+ ))
+}
+
+fn apply_to_detected_proxyfier(
+ request: HelperApplyRequest<'_>,
+ detected: &DetectedProxyfier,
+) -> Result {
+ let Some(config_path) = &detected.config_path else {
+ return staged_apply_result(request);
+ };
+
+ if let Some(parent) = config_path.parent() {
+ fs::create_dir_all(parent).map_err(|error| {
+ CommandError::new(
+ "proxyfier_apply_failed",
+ format!(
+ "Не удалось создать папку конфига ProxiFyre '{}': {error}",
+ parent.display()
+ ),
+ )
+ })?;
+ }
+
+ if config_path.exists() {
+ let backup_path = config_path.with_file_name(format!(
+ "{}.bak",
+ config_path
+ .file_name()
+ .and_then(|value| value.to_str())
+ .unwrap_or("app-config.json")
+ ));
+ fs::copy(config_path, backup_path).map_err(|error| {
+ CommandError::new(
+ "proxyfier_apply_failed",
+ format!(
+ "Не удалось создать backup текущего конфига ProxiFyre '{}': {error}",
+ config_path.display()
+ ),
+ )
+ })?;
+ }
+
+ fs::write(config_path, request.config_contents).map_err(|error| {
+ CommandError::new(
+ "proxyfier_apply_failed",
+ format!(
+ "Не удалось записать конфиг ProxiFyre '{}': {error}",
+ config_path.display()
+ ),
+ )
+ })?;
+
+ Ok(HelperApplyResult {
+ success: true,
+ changed: true,
+ action: "proxifyre.apply-detected-config".to_string(),
+ message: format!(
+ "Сгенерированный конфиг записан в найденную установку ProxiFyre: {}",
+ config_path.display()
+ ),
+ })
+}
+
+fn staged_apply_result(request: HelperApplyRequest<'_>) -> Result {
+ Ok(HelperApplyResult {
+ success: true,
+ changed: true,
+ action: format!("{}.stage-generated-config", request.adapter_id),
+ message: format!(
+ "Сгенерированный конфиг подготовлен в {}; совместимая установка ProxiFyre не найдена",
+ request.config_path.display()
+ ),
+ })
+}
+
+fn activity_for_apply(
+ clock: &impl Clock,
+ generated: &ProxyRouterGeneratedConfig,
+ generated_path: &Path,
+ helper_result: &HelperApplyResult,
+) -> ActivityEntry {
+ let level = if helper_result.success {
+ ActivityLevel::Success
+ } else {
+ ActivityLevel::Error
+ };
+
+ ActivityEntry {
+ id: format!("apply-{}", generated.adapter_id),
+ at: clock.now(),
+ level,
+ title: "Конфиг ProxiFyre создан".to_string(),
+ message: format!(
+ "Профилей: {}, приложений: {}, конфиг: {}",
+ generated.enabled_profiles,
+ generated.routed_apps,
+ generated_path.display()
+ ),
+ }
+}
+
+fn activity_for_apply_error(clock: &impl Clock, error: &CommandError) -> ActivityEntry {
+ ActivityEntry {
+ id: format!("apply-error-{}", error.code),
+ at: clock.now(),
+ level: ActivityLevel::Error,
+ title: "Применение ProxiFyre заблокировано".to_string(),
+ message: error.message.clone(),
+ }
+}
+
+fn storage_error(error: std::io::Error) -> CommandError {
+ CommandError::new("storage_error", error.to_string())
+}
+
+fn background_task_error(error: impl std::fmt::Display) -> CommandError {
+ CommandError::new(
+ "background_task_failed",
+ format!("Фоновая проверка не завершилась: {error}"),
+ )
+}
+
+fn package_failure_details(result_path: &Path, output: &Output) -> String {
+ let mut parts = Vec::new();
+
+ if let Ok(contents) = fs::read_to_string(result_path) {
+ let details = compact_error_text(&contents);
+ if !details.is_empty() && !details.eq_ignore_ascii_case("ok") {
+ parts.push(details);
+ }
+ }
+
+ let stdout = String::from_utf8_lossy(&output.stdout);
+ let stdout = compact_error_text(&stdout);
+ if !stdout.is_empty() {
+ parts.push(format!("stdout: {stdout}"));
+ }
+
+ let stderr = String::from_utf8_lossy(&output.stderr);
+ let stderr = compact_error_text(&stderr);
+ if !stderr.is_empty() {
+ parts.push(format!("stderr: {stderr}"));
+ }
+
+ if parts.is_empty() {
+ parts.push(
+ "Лог elevated-скрипта не создан. Обычно это значит, что окно UAC было отменено или Windows не дала запустить elevated PowerShell."
+ .to_string(),
+ );
+ }
+
+ parts.join(" ")
+}
+
+fn compact_error_text(value: &str) -> String {
+ let text = value
+ .lines()
+ .map(str::trim)
+ .filter(|line| !line.is_empty())
+ .collect::>()
+ .join(" ");
+
+ let max_chars = 1400;
+ if text.chars().count() <= max_chars {
+ return text;
+ }
+
+ let truncated = text.chars().take(max_chars).collect::();
+ format!("{truncated}...")
+}
+
+fn parse_service_command_output(stdout: &[u8]) -> Option {
+ let stdout = String::from_utf8_lossy(stdout);
+ let payload = stdout
+ .lines()
+ .rev()
+ .map(str::trim)
+ .find(|line| line.starts_with('{') && line.ends_with('}'))?;
+
+ serde_json::from_str(payload).ok()
+}
+
+fn service_script_failed_message(action: ServiceControlAction, exit_code: Option) -> String {
+ let exit_code = exit_code
+ .map(|code| format!(" Код выхода PowerShell: {code}."))
+ .unwrap_or_default();
+
+ format!(
+ "Не удалось {} службу ProxiFyre: команда управления службой не вернула корректный результат.{exit_code}",
+ action.label()
+ )
+}
+
+fn service_command_failed_message(
+ action: ServiceControlAction,
+ result: &ServiceCommandOutput,
+) -> String {
+ let service_name = result
+ .service_name
+ .as_deref()
+ .filter(|value| !value.trim().is_empty())
+ .unwrap_or("ProxiFyre");
+ let status = result
+ .status
+ .as_deref()
+ .filter(|value| !value.trim().is_empty())
+ .unwrap_or("неизвестен");
+ let pid = result
+ .process_id
+ .filter(|value| *value > 0)
+ .map(|value| format!(", PID: {value}"))
+ .unwrap_or_default();
+
+ match result.code.as_str() {
+ "service_not_found" => "Служба ProxiFyre не найдена.".to_string(),
+ "start_failed" => format!(
+ "Не удалось запустить службу {service_name}. Текущий статус: {status}{pid}. Попробуй запустить приложение от имени администратора."
+ ),
+ "stop_failed" => format!(
+ "Не удалось остановить службу {service_name} даже после принудительной попытки. Текущий статус: {status}{pid}. Запусти приложение от имени администратора или останови службу вручную в services.msc."
+ ),
+ _ => format!(
+ "Не удалось {} службу {service_name}. Текущий статус: {status}{pid}.",
+ action.label()
+ ),
+ }
+}
+
+fn elevated_service_failed_message(
+ action: ServiceControlAction,
+ direct_result: &ServiceCommandOutput,
+ exit_code: Option,
+) -> String {
+ let service_name = direct_result
+ .service_name
+ .as_deref()
+ .filter(|value| !value.trim().is_empty())
+ .unwrap_or("ProxiFyre");
+ let status = direct_result
+ .status
+ .as_deref()
+ .filter(|value| !value.trim().is_empty())
+ .unwrap_or("неизвестен");
+ let pid = direct_result
+ .process_id
+ .filter(|value| *value > 0)
+ .map(|value| format!(", PID: {value}"))
+ .unwrap_or_default();
+ let exit_code = exit_code
+ .map(|code| format!(" Код elevated-команды: {code}."))
+ .unwrap_or_default();
+
+ format!(
+ "Не удалось {} службу {service_name} даже после запроса прав администратора. До запроса UAC статус был: {status}{pid}.{exit_code} Если появлялось окно UAC, проверь, что оно было подтверждено.",
+ action.label()
+ )
+}
+
+fn escape_powershell_single(value: &str) -> String {
+ value.replace('\'', "''")
+}
+
+fn validation_error(errors: Vec) -> CommandError {
+ CommandError::with_details(
+ "validation_error",
+ "Проверка введенных данных не прошла",
+ errors
+ .into_iter()
+ .map(|error| ValidationIssue {
+ field: error.field,
+ message: error.message,
+ })
+ .collect(),
+ )
+}
+
+fn adapter_error(error: ProxyRouterError) -> CommandError {
+ let code = match error.kind {
+ ProxyRouterErrorKind::EmptyProfileItems => "empty_profile_items",
+ ProxyRouterErrorKind::MissingTarget => "missing_target",
+ ProxyRouterErrorKind::MissingRequiredComponent => "missing_required_component",
+ ProxyRouterErrorKind::RequiredComponentNotRunning => "required_component_not_running",
+ ProxyRouterErrorKind::UnsupportedTargetProtocol => "unsupported_target_protocol",
+ ProxyRouterErrorKind::Serialization => "serialization_error",
+ };
+
+ CommandError::new(code, error.message)
+}
+
+impl From for ProfileInput {
+ fn from(input: ProfileInputDto) -> Self {
+ Self {
+ id: input.id,
+ name: input.name,
+ enabled: input.enabled.unwrap_or(true),
+ target_id: input
+ .target_id
+ .unwrap_or_else(|| "local-singbox".to_string()),
+ protocols: input
+ .protocols
+ .unwrap_or_else(|| vec!["TCP".to_string(), "UDP".to_string()]),
+ items: input
+ .items
+ .unwrap_or_default()
+ .into_iter()
+ .map(ProfileItemInput::from)
+ .collect(),
+ }
+ }
+}
+
+impl From for ProfileItemInput {
+ fn from(input: ProfileItemInputDto) -> Self {
+ Self {
+ item_type: input.item_type,
+ value: input.value,
+ recursive: input.recursive,
+ }
+ }
+}
+
+impl From for TargetInput {
+ fn from(input: TargetInputDto) -> Self {
+ Self {
+ id: input.id,
+ name: input.name,
+ kind: input.kind.unwrap_or_else(|| "external".to_string()),
+ protocol: input.protocol.unwrap_or_else(|| "socks5".to_string()),
+ host: input.host,
+ port: input.port,
+ requires_component: input.requires_component,
+ }
+ }
+}
+
+impl From<&Profile> for ProfileDto {
+ fn from(profile: &Profile) -> Self {
+ Self {
+ id: profile.id.clone(),
+ name: profile.name.clone(),
+ enabled: profile.enabled,
+ target_id: profile.target_id.clone(),
+ protocols: profile.protocols.clone(),
+ items: profile.items.iter().map(ProfileItemDto::from).collect(),
+ }
+ }
+}
+
+impl From<&ProfileItem> for ProfileItemDto {
+ fn from(item: &ProfileItem) -> Self {
+ Self {
+ item_type: item.item_type.clone(),
+ value: item.value.clone(),
+ recursive: item.recursive,
+ }
+ }
+}
+
+impl From<&Target> for TargetDto {
+ fn from(target: &Target) -> Self {
+ Self {
+ id: target.id.clone(),
+ name: target.name.clone(),
+ kind: target.kind.clone(),
+ protocol: target.protocol.clone(),
+ host: target.host.clone(),
+ port: target.port,
+ requires_component: target.requires_component.clone(),
+ }
+ }
+}
+
+impl From<&ComponentStatus> for ComponentStatusDto {
+ fn from(component: &ComponentStatus) -> Self {
+ Self {
+ id: component.id.clone(),
+ name: component.name.clone(),
+ state: component.state.clone(),
+ installed: component.installed,
+ running: component.running,
+ version: component.version.clone(),
+ path: component.path.clone(),
+ problems: component.problems.clone(),
+ actions: component.actions.clone(),
+ }
+ }
+}
+
+impl From<&ActivityEntry> for ActivityEntryDto {
+ fn from(entry: &ActivityEntry) -> Self {
+ Self {
+ id: entry.id.clone(),
+ at: entry.at.clone(),
+ level: entry.level.clone(),
+ title: entry.title.clone(),
+ message: entry.message.clone(),
+ }
+ }
+}
+
+impl From<&LocalSingBoxConfig> for LocalSingBoxConfigDto {
+ fn from(config: &LocalSingBoxConfig) -> Self {
+ Self {
+ subscription_display_url: config.subscription_display_url(),
+ has_subscription: config
+ .subscription_url
+ .as_deref()
+ .is_some_and(|value| !value.trim().is_empty()),
+ selected_server_tag: config.selected_server_tag.clone(),
+ listen_host: config.listen_host.clone(),
+ listen_port: config.listen_port,
+ service_name: config.service_name.clone(),
+ install_root: config.install_root.clone(),
+ updated_at: config.updated_at.clone(),
+ }
+ }
+}
+
+impl From<&SubscriptionCache> for SubscriptionCacheDto {
+ fn from(cache: &SubscriptionCache) -> Self {
+ Self {
+ servers: cache
+ .servers
+ .iter()
+ .map(SubscriptionServerDto::from)
+ .collect(),
+ user_info: cache.user_info.clone(),
+ fetched_at: cache.fetched_at.clone(),
+ }
+ }
+}
+
+impl From<&SubscriptionServer> for SubscriptionServerDto {
+ fn from(server: &SubscriptionServer) -> Self {
+ Self {
+ tag: server.tag.clone(),
+ server_type: server.server_type.clone(),
+ server: server.server.clone(),
+ server_port: server.server_port,
+ }
+ }
+}
diff --git a/src-tauri/src/component_detection.rs b/src-tauri/src/component_detection.rs
new file mode 100644
index 0000000..fa6b96f
--- /dev/null
+++ b/src-tauri/src/component_detection.rs
@@ -0,0 +1,564 @@
+use crate::models::{
+ ComponentId, ComponentState, ComponentStatus, DEFAULT_LOCAL_SINGBOX_INSTALL_ROOT,
+ DEFAULT_LOCAL_SINGBOX_SERVICE_NAME,
+};
+use serde::Deserialize;
+use std::{
+ env,
+ path::{Path, PathBuf},
+ process::Command,
+};
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum ProxyfierEngine {
+ ProxiFyre,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct DetectedProxyfier {
+ pub engine: ProxyfierEngine,
+ pub name: String,
+ pub install_dir: PathBuf,
+ pub executable_path: PathBuf,
+ pub config_path: Option,
+ pub running: bool,
+ pub service_name: Option,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct DetectedSingBox {
+ pub install_dir: PathBuf,
+ pub executable_path: PathBuf,
+ pub wrapper_path: PathBuf,
+ pub binary_exists: bool,
+ pub wrapper_exists: bool,
+ pub running: bool,
+ pub service_name: String,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RegistryInstallEntry {
+ pub display_name: String,
+ pub install_location: Option,
+ pub display_icon: Option,
+}
+
+pub trait ProxyfierDetectionHost {
+ fn env_var(&self, name: &str) -> Option;
+
+ fn path_exists(&self, path: &Path) -> bool;
+
+ fn process_running(&self, process_name: &str) -> bool;
+
+ fn service_running(&self, service_name: &str) -> bool;
+
+ fn registry_install_entries(&self) -> Vec;
+}
+
+#[derive(Debug, Clone, Copy, Default)]
+pub struct SystemProxyfierDetectionHost;
+
+impl ProxyfierDetectionHost for SystemProxyfierDetectionHost {
+ fn env_var(&self, name: &str) -> Option {
+ env::var(name).ok().filter(|value| !value.trim().is_empty())
+ }
+
+ fn path_exists(&self, path: &Path) -> bool {
+ path.exists()
+ }
+
+ fn process_running(&self, process_name: &str) -> bool {
+ let process_name = process_name.trim_end_matches(".exe");
+ let script = format!(
+ "if (Get-Process -Name '{}' -ErrorAction SilentlyContinue) {{ 'true' }} else {{ 'false' }}",
+ escape_powershell_single(process_name)
+ );
+
+ powershell_bool(&script)
+ }
+
+ fn service_running(&self, service_name: &str) -> bool {
+ let script = format!(
+ "$s = Get-Service -Name '{}' -ErrorAction SilentlyContinue; if ($s -and $s.Status -eq 'Running') {{ 'true' }} else {{ 'false' }}",
+ escape_powershell_single(service_name)
+ );
+
+ powershell_bool(&script)
+ }
+
+ fn registry_install_entries(&self) -> Vec {
+ read_registry_install_entries()
+ }
+}
+
+pub fn detect_proxyfier_install() -> Option {
+ detect_proxyfier_install_with_host(&SystemProxyfierDetectionHost)
+}
+
+pub fn detect_proxyfier_install_with_host(
+ host: &impl ProxyfierDetectionHost,
+) -> Option {
+ let proxifyre_running = host.process_running("ProxiFyre.exe")
+ || host.service_running("ProxiFyreService")
+ || host.service_running("ProxiFyre");
+
+ proxyfier_candidates(host)
+ .into_iter()
+ .filter_map(|candidate| candidate.into_detected(host, proxifyre_running))
+ .next()
+}
+
+pub fn proxyfier_component_from_detection(detected: Option<&DetectedProxyfier>) -> ComponentStatus {
+ match detected {
+ Some(proxyfier) => detected_proxyfier_component(proxyfier),
+ None => missing_proxyfier_component(),
+ }
+}
+
+pub fn detect_singbox_install() -> Option {
+ detect_singbox_install_with_host(&SystemProxyfierDetectionHost)
+}
+
+pub fn detect_singbox_install_with_host(
+ host: &impl ProxyfierDetectionHost,
+) -> Option {
+ let running = host.process_running("sing-box.exe")
+ || host.service_running(DEFAULT_LOCAL_SINGBOX_SERVICE_NAME);
+
+ singbox_candidates(host)
+ .into_iter()
+ .filter_map(|install_dir| detected_singbox_from_dir(host, install_dir, running))
+ .next()
+}
+
+pub fn singbox_component_from_detection(detected: Option<&DetectedSingBox>) -> ComponentStatus {
+ match detected {
+ Some(singbox) => detected_singbox_component(singbox),
+ None => missing_singbox_component(),
+ }
+}
+
+fn detected_proxyfier_component(proxyfier: &DetectedProxyfier) -> ComponentStatus {
+ let state = if proxyfier.running {
+ ComponentState::Running
+ } else {
+ ComponentState::Installed
+ };
+ let actions = match proxyfier.engine {
+ ProxyfierEngine::ProxiFyre => {
+ if proxyfier.running {
+ vec![
+ "Применить сгенерированный конфиг".to_string(),
+ "Открыть папку конфига".to_string(),
+ "Остановить".to_string(),
+ ]
+ } else {
+ vec![
+ "Применить сгенерированный конфиг".to_string(),
+ "Открыть папку конфига".to_string(),
+ "Запустить".to_string(),
+ ]
+ }
+ }
+ };
+
+ ComponentStatus {
+ id: ComponentId::Proxyfier,
+ name: "ProxiFyre".to_string(),
+ state,
+ installed: true,
+ running: proxyfier.running,
+ version: Some(match proxyfier.engine {
+ ProxyfierEngine::ProxiFyre => "ProxiFyre найден".to_string(),
+ }),
+ path: Some(proxyfier.install_dir.display().to_string()),
+ problems: Vec::new(),
+ actions,
+ }
+}
+
+fn missing_proxyfier_component() -> ComponentStatus {
+ ComponentStatus {
+ id: ComponentId::Proxyfier,
+ name: "ProxiFyre".to_string(),
+ state: ComponentState::Missing,
+ installed: false,
+ running: false,
+ version: None,
+ path: None,
+ problems: vec!["ProxiFyre нужен для маршрутизации выбранных приложений".to_string()],
+ actions: vec!["Установить ProxiFyre".to_string()],
+ }
+}
+
+fn detected_singbox_component(singbox: &DetectedSingBox) -> ComponentStatus {
+ let state = if singbox.running {
+ ComponentState::Running
+ } else {
+ ComponentState::Stopped
+ };
+ let actions = if singbox.running {
+ vec![
+ "Сгенерировать конфиг".to_string(),
+ "Остановить".to_string(),
+ "Открыть папку".to_string(),
+ ]
+ } else {
+ vec![
+ "Сгенерировать конфиг".to_string(),
+ "Запустить".to_string(),
+ "Открыть папку".to_string(),
+ ]
+ };
+ let problems = if singbox.running {
+ Vec::new()
+ } else {
+ vec!["Служба Local sing-box остановлена".to_string()]
+ };
+
+ ComponentStatus {
+ id: ComponentId::Singbox,
+ name: "Local sing-box".to_string(),
+ state,
+ installed: true,
+ running: singbox.running,
+ version: Some("sing-box найден".to_string()),
+ path: Some(singbox.executable_path.display().to_string()),
+ problems,
+ actions,
+ }
+}
+
+fn missing_singbox_component() -> ComponentStatus {
+ ComponentStatus {
+ id: ComponentId::Singbox,
+ name: "Local sing-box".to_string(),
+ state: ComponentState::Missing,
+ installed: false,
+ running: false,
+ version: None,
+ path: None,
+ problems: Vec::new(),
+ actions: vec!["Установить Local sing-box".to_string()],
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+struct ProxyfierCandidate {
+ engine: ProxyfierEngine,
+ name: String,
+ install_dir: PathBuf,
+}
+
+impl ProxyfierCandidate {
+ fn into_detected(
+ self,
+ host: &impl ProxyfierDetectionHost,
+ proxifyre_running: bool,
+ ) -> Option {
+ let executable_path = self.install_dir.join(executable_name(&self.engine));
+ let config_path = config_path(&self.engine, &self.install_dir);
+ let exists = host.path_exists(&self.install_dir)
+ || host.path_exists(&executable_path)
+ || config_path
+ .as_ref()
+ .is_some_and(|path| host.path_exists(path));
+
+ if !exists {
+ return None;
+ }
+
+ Some(DetectedProxyfier {
+ service_name: service_name(&self.engine).map(str::to_string),
+ engine: self.engine,
+ name: self.name,
+ install_dir: self.install_dir,
+ executable_path,
+ config_path,
+ running: proxifyre_running,
+ })
+ }
+}
+
+fn proxyfier_candidates(host: &impl ProxyfierDetectionHost) -> Vec {
+ let mut candidates = Vec::new();
+
+ push_env_candidate(
+ &mut candidates,
+ host,
+ ProxyfierEngine::ProxiFyre,
+ "ProxiFyre",
+ "PROXYWARDEN_PROXIFYRE_ROOT",
+ );
+ for entry in host.registry_install_entries() {
+ if let Some(engine) = engine_from_name(&entry.display_name) {
+ let install_dir = entry
+ .install_location
+ .or_else(|| entry.display_icon.and_then(|path| executable_parent(&path)));
+ if let Some(install_dir) = install_dir {
+ push_candidate(
+ &mut candidates,
+ ProxyfierCandidate {
+ name: entry.display_name,
+ engine,
+ install_dir,
+ },
+ );
+ }
+ }
+ }
+
+ for install_dir in common_install_dirs(host, "ProxiFyre") {
+ push_candidate(
+ &mut candidates,
+ ProxyfierCandidate {
+ engine: ProxyfierEngine::ProxiFyre,
+ name: "ProxiFyre".to_string(),
+ install_dir,
+ },
+ );
+ }
+ candidates
+}
+
+fn push_env_candidate(
+ candidates: &mut Vec,
+ host: &impl ProxyfierDetectionHost,
+ engine: ProxyfierEngine,
+ name: &str,
+ env_name: &str,
+) {
+ if let Some(path) = host.env_var(env_name) {
+ push_candidate(
+ candidates,
+ ProxyfierCandidate {
+ engine,
+ name: name.to_string(),
+ install_dir: PathBuf::from(path),
+ },
+ );
+ }
+}
+
+fn push_candidate(candidates: &mut Vec, candidate: ProxyfierCandidate) {
+ if !candidates.iter().any(|existing| {
+ existing.engine == candidate.engine
+ && same_path(&existing.install_dir, &candidate.install_dir)
+ }) {
+ candidates.push(candidate);
+ }
+}
+
+fn common_install_dirs(host: &impl ProxyfierDetectionHost, folder_name: &str) -> Vec {
+ let mut dirs = vec![PathBuf::from(format!(r"C:\Tools\{folder_name}"))];
+
+ for env_name in ["ProgramFiles", "ProgramFiles(x86)", "LOCALAPPDATA"] {
+ if let Some(root) = host.env_var(env_name) {
+ dirs.push(PathBuf::from(root).join(folder_name));
+ }
+ }
+
+ dirs
+}
+
+fn singbox_candidates(host: &impl ProxyfierDetectionHost) -> Vec {
+ let mut candidates = Vec::new();
+
+ if let Some(path) = host.env_var("PROXYWARDEN_SINGBOX_ROOT") {
+ push_path_candidate(&mut candidates, PathBuf::from(path));
+ }
+ push_path_candidate(
+ &mut candidates,
+ PathBuf::from(DEFAULT_LOCAL_SINGBOX_INSTALL_ROOT),
+ );
+ push_path_candidate(
+ &mut candidates,
+ PathBuf::from(r"C:\Tools\ProxyWarden\sing-box"),
+ );
+ for env_name in ["ProgramFiles", "ProgramFiles(x86)", "LOCALAPPDATA"] {
+ if let Some(root) = host.env_var(env_name) {
+ push_path_candidate(
+ &mut candidates,
+ PathBuf::from(&root).join("ProxyWarden").join("sing-box"),
+ );
+ push_path_candidate(&mut candidates, PathBuf::from(root).join("sing-box"));
+ }
+ }
+
+ candidates
+}
+
+fn push_path_candidate(candidates: &mut Vec, candidate: PathBuf) {
+ if !candidates
+ .iter()
+ .any(|existing| same_path(existing, &candidate))
+ {
+ candidates.push(candidate);
+ }
+}
+
+fn detected_singbox_from_dir(
+ host: &impl ProxyfierDetectionHost,
+ install_dir: PathBuf,
+ running: bool,
+) -> Option {
+ let executable_path = install_dir.join("sing-box.exe");
+ let wrapper_path = install_dir.join("ProxyWardenSingBox.exe");
+ let binary_exists = host.path_exists(&executable_path);
+ let wrapper_exists = host.path_exists(&wrapper_path);
+ let exists = host.path_exists(&install_dir) || binary_exists || wrapper_exists;
+
+ if !exists {
+ return None;
+ }
+
+ Some(DetectedSingBox {
+ install_dir,
+ executable_path,
+ wrapper_path,
+ binary_exists,
+ wrapper_exists,
+ running,
+ service_name: DEFAULT_LOCAL_SINGBOX_SERVICE_NAME.to_string(),
+ })
+}
+
+fn executable_name(engine: &ProxyfierEngine) -> &'static str {
+ match engine {
+ ProxyfierEngine::ProxiFyre => "ProxiFyre.exe",
+ }
+}
+
+fn config_path(engine: &ProxyfierEngine, install_dir: &Path) -> Option {
+ match engine {
+ ProxyfierEngine::ProxiFyre => Some(install_dir.join("app-config.json")),
+ }
+}
+
+fn service_name(engine: &ProxyfierEngine) -> Option<&'static str> {
+ match engine {
+ ProxyfierEngine::ProxiFyre => Some("ProxiFyreService"),
+ }
+}
+
+fn engine_from_name(name: &str) -> Option {
+ let normalized = name.to_ascii_lowercase();
+ if normalized.contains("proxifyre") {
+ Some(ProxyfierEngine::ProxiFyre)
+ } else {
+ None
+ }
+}
+
+fn executable_parent(path: &Path) -> Option {
+ path.parent().map(Path::to_path_buf)
+}
+
+fn same_path(left: &Path, right: &Path) -> bool {
+ left.to_string_lossy()
+ .eq_ignore_ascii_case(&right.to_string_lossy())
+}
+
+fn powershell_bool(script: &str) -> bool {
+ Command::new("powershell")
+ .args(["-NoProfile", "-NonInteractive", "-Command", script])
+ .output()
+ .ok()
+ .and_then(|output| String::from_utf8(output.stdout).ok())
+ .is_some_and(|stdout| stdout.trim().eq_ignore_ascii_case("true"))
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "PascalCase")]
+struct RegistryInstallJson {
+ display_name: Option,
+ install_location: Option,
+ display_icon: Option,
+}
+
+fn read_registry_install_entries() -> Vec {
+ let script = r#"
+$paths = @(
+ 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*',
+ 'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*',
+ 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'
+)
+$items = foreach ($path in $paths) {
+ Get-ItemProperty -Path $path -ErrorAction SilentlyContinue
+}
+$items |
+ Where-Object { $_.DisplayName -match 'ProxiFyre' } |
+ Select-Object DisplayName,InstallLocation,DisplayIcon |
+ ConvertTo-Json -Compress
+"#;
+
+ let Ok(output) = Command::new("powershell")
+ .args(["-NoProfile", "-NonInteractive", "-Command", script])
+ .output()
+ else {
+ return Vec::new();
+ };
+
+ if !output.status.success() {
+ return Vec::new();
+ }
+
+ let Ok(stdout) = String::from_utf8(output.stdout) else {
+ return Vec::new();
+ };
+ let stdout = stdout.trim();
+ if stdout.is_empty() {
+ return Vec::new();
+ }
+
+ parse_registry_json(stdout)
+}
+
+fn parse_registry_json(json: &str) -> Vec {
+ let Ok(value) = serde_json::from_str::(json) else {
+ return Vec::new();
+ };
+
+ match value {
+ serde_json::Value::Array(entries) => entries
+ .into_iter()
+ .filter_map(registry_entry_from_value)
+ .collect(),
+ entry => registry_entry_from_value(entry).into_iter().collect(),
+ }
+}
+
+fn registry_entry_from_value(value: serde_json::Value) -> Option {
+ let parsed = serde_json::from_value::(value).ok()?;
+ let display_name = parsed.display_name?;
+ Some(RegistryInstallEntry {
+ display_name,
+ install_location: parsed
+ .install_location
+ .filter(|value| !value.trim().is_empty())
+ .map(PathBuf::from),
+ display_icon: parsed
+ .display_icon
+ .and_then(|value| display_icon_path(&value)),
+ })
+}
+
+fn display_icon_path(value: &str) -> Option {
+ let trimmed = value.trim().trim_matches('"');
+ if trimmed.is_empty() {
+ return None;
+ }
+
+ let without_icon_index = trimmed
+ .split_once(',')
+ .map(|(path, _)| path)
+ .unwrap_or(trimmed)
+ .trim()
+ .trim_matches('"');
+
+ Some(PathBuf::from(without_icon_index))
+}
+
+fn escape_powershell_single(value: &str) -> String {
+ value.replace('\'', "''")
+}
diff --git a/src-tauri/src/helper.rs b/src-tauri/src/helper.rs
new file mode 100644
index 0000000..1334c29
--- /dev/null
+++ b/src-tauri/src/helper.rs
@@ -0,0 +1,184 @@
+use crate::models::ComponentId;
+use serde::{Deserialize, Serialize};
+use serde_json::{json, Value};
+use std::path::{Path, PathBuf};
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub enum HelperAction {
+ #[serde(rename = "install-control-app")]
+ InstallControlApp,
+ #[serde(rename = "install-proxyfier")]
+ InstallProxyfier,
+ #[serde(rename = "install-singbox")]
+ InstallSingbox,
+ #[serde(rename = "proxyfier.apply")]
+ ProxyfierApply,
+ #[serde(rename = "service.status")]
+ ServiceStatus,
+ #[serde(rename = "service.start")]
+ ServiceStart,
+ #[serde(rename = "service.stop")]
+ ServiceStop,
+ #[serde(rename = "service.restart")]
+ ServiceRestart,
+}
+
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct HelperRequest {
+ pub action: HelperAction,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub component: Option,
+ #[serde(default)]
+ pub payload: Value,
+}
+
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct HelperResponse {
+ pub success: bool,
+ pub action: HelperAction,
+ pub changed: bool,
+ pub message: String,
+ #[serde(default)]
+ pub details: Value,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct HelperCommandSpec {
+ pub program: PathBuf,
+ pub args: Vec,
+ pub stdin: String,
+ pub requires_elevation: bool,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct HelperCommandOutput {
+ pub status_code: i32,
+ pub stdout: String,
+ pub stderr: String,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct HelperError {
+ pub code: String,
+ pub message: String,
+}
+
+impl HelperError {
+ pub fn new(code: impl Into, message: impl Into) -> Self {
+ Self {
+ code: code.into(),
+ message: message.into(),
+ }
+ }
+}
+
+pub trait HelperCommandRunner {
+ fn run(&self, spec: &HelperCommandSpec) -> Result;
+}
+
+#[derive(Debug, Clone)]
+pub struct StructuredHelper {
+ helper_program: PathBuf,
+ runner: R,
+}
+
+impl StructuredHelper
+where
+ R: HelperCommandRunner,
+{
+ pub fn new(helper_program: impl Into, runner: R) -> Self {
+ Self {
+ helper_program: helper_program.into(),
+ runner,
+ }
+ }
+
+ pub fn runner(&self) -> &R {
+ &self.runner
+ }
+
+ pub fn execute(&self, request: &HelperRequest) -> Result {
+ let stdin = serde_json::to_string(request)
+ .map_err(|error| HelperError::new("helper_request_encode", error.to_string()))?;
+ let spec = HelperCommandSpec {
+ program: self.helper_program.clone(),
+ args: vec!["--json".to_string()],
+ stdin,
+ requires_elevation: helper_action_requires_elevation(&request.action),
+ };
+ let output = self.runner.run(&spec)?;
+
+ if output.status_code != 0 {
+ return Err(HelperError::new(
+ "helper_exit",
+ format!(
+ "Помощник завершился с кодом {}: {}",
+ output.status_code, output.stderr
+ ),
+ ));
+ }
+
+ parse_helper_response(&output.stdout)
+ }
+}
+
+pub fn parse_helper_response(stdout: &str) -> Result {
+ serde_json::from_str(stdout).map_err(|error| {
+ HelperError::new(
+ "helper_response_decode",
+ format!("Помощник вернул не JSON или некорректный JSON: {error}"),
+ )
+ })
+}
+
+pub fn install_request(component: ComponentId) -> HelperRequest {
+ let action = match component {
+ ComponentId::ControlApp => HelperAction::InstallControlApp,
+ ComponentId::Proxyfier => HelperAction::InstallProxyfier,
+ ComponentId::Singbox => HelperAction::InstallSingbox,
+ };
+
+ HelperRequest {
+ action,
+ component: Some(component),
+ payload: json!({}),
+ }
+}
+
+pub fn service_request(component: ComponentId, action: HelperAction) -> HelperRequest {
+ HelperRequest {
+ action,
+ component: Some(component),
+ payload: json!({}),
+ }
+}
+
+pub fn proxifyre_apply_request(
+ config_path: impl AsRef,
+ service_name: impl Into,
+) -> HelperRequest {
+ HelperRequest {
+ action: HelperAction::ProxyfierApply,
+ component: Some(ComponentId::Proxyfier),
+ payload: json!({
+ "configPath": config_path.as_ref().display().to_string(),
+ "serviceName": service_name.into(),
+ }),
+ }
+}
+
+pub fn helper_action_requires_elevation(action: &HelperAction) -> bool {
+ matches!(
+ action,
+ HelperAction::InstallControlApp
+ | HelperAction::InstallProxyfier
+ | HelperAction::InstallSingbox
+ | HelperAction::ProxyfierApply
+ | HelperAction::ServiceStart
+ | HelperAction::ServiceStop
+ | HelperAction::ServiceRestart
+ )
+}
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
new file mode 100644
index 0000000..008d525
--- /dev/null
+++ b/src-tauri/src/lib.rs
@@ -0,0 +1,5 @@
+pub fn run() {
+ tauri::Builder::default()
+ .run(tauri::generate_context!())
+ .expect("не удалось запустить клиент ProxyWarden");
+}
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs
new file mode 100644
index 0000000..f251a66
--- /dev/null
+++ b/src-tauri/src/main.rs
@@ -0,0 +1,71 @@
+#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
+
+mod activity;
+mod commands;
+mod component_detection;
+mod models;
+mod singbox_service;
+mod storage;
+mod subscription;
+mod validation;
+
+mod adapters {
+ pub mod proxifyre;
+ pub mod proxy_router;
+ pub mod singbox;
+}
+
+#[cfg(test)]
+pub(crate) mod proxifyre {
+ pub use crate::adapters::proxifyre::*;
+}
+
+#[cfg(test)]
+pub(crate) mod proxy_router {
+ pub use crate::adapters::proxy_router::*;
+}
+
+#[cfg(test)]
+pub(crate) mod singbox {
+ pub use crate::adapters::singbox::*;
+}
+
+fn main() {
+ tauri::Builder::default()
+ .plugin(tauri_plugin_dialog::init())
+ .manage(commands::CommandState::default())
+ .invoke_handler(tauri::generate_handler![
+ commands::get_status,
+ commands::get_profiles,
+ commands::get_saved_state,
+ commands::save_profile,
+ commands::get_targets,
+ commands::save_target,
+ commands::get_components,
+ commands::get_proxifyre_setup_status,
+ commands::get_singbox_status,
+ commands::get_singbox_setup_status,
+ commands::resolve_profile_preview,
+ commands::save_singbox_subscription,
+ commands::fetch_singbox_subscription,
+ commands::forget_singbox_subscription,
+ commands::select_singbox_server,
+ commands::ping_singbox_server,
+ commands::ping_all_singbox_servers,
+ commands::ping_proxy_target,
+ commands::generate_singbox_config,
+ commands::apply_profiles,
+ commands::get_logs,
+ commands::open_config_location,
+ commands::start_proxifyre_service,
+ commands::stop_proxifyre_service,
+ commands::install_proxifyre,
+ commands::uninstall_proxifyre,
+ commands::start_singbox_service,
+ commands::stop_singbox_service,
+ commands::install_singbox,
+ commands::uninstall_singbox
+ ])
+ .run(tauri::generate_context!())
+ .expect("не удалось запустить клиент ProxyWarden");
+}
diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs
new file mode 100644
index 0000000..8c0ea6e
--- /dev/null
+++ b/src-tauri/src/models.rs
@@ -0,0 +1,273 @@
+use serde::{Deserialize, Serialize};
+
+pub const DEFAULT_LOCAL_SINGBOX_LISTEN_HOST: &str = "127.0.0.1";
+pub const DEFAULT_LOCAL_SINGBOX_LISTEN_PORT: u16 = 1080;
+pub const DEFAULT_LOCAL_SINGBOX_SERVICE_NAME: &str = "ProxyWardenSingBox";
+pub const DEFAULT_LOCAL_SINGBOX_INSTALL_ROOT: &str = r"C:\Program Files\ProxyWarden\sing-box";
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
+pub enum Protocol {
+ Tcp,
+ Udp,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub enum ProfileItemType {
+ Process,
+ Folder,
+ Exe,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub enum TargetKind {
+ Local,
+ External,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub enum ProxyProtocol {
+ Socks5,
+ Http,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub enum ComponentId {
+ ControlApp,
+ Proxyfier,
+ Singbox,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub enum ComponentState {
+ Installed,
+ Missing,
+ Stopped,
+ Running,
+ Error,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct ProfileItemInput {
+ #[serde(rename = "type")]
+ pub item_type: String,
+ pub value: String,
+ #[serde(default)]
+ pub recursive: Option,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct ProfileInput {
+ pub id: Option,
+ pub name: String,
+ #[serde(default = "default_enabled")]
+ pub enabled: bool,
+ #[serde(default = "default_target_id")]
+ pub target_id: String,
+ #[serde(default = "default_protocols")]
+ pub protocols: Vec,
+ #[serde(default)]
+ pub items: Vec,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct ProfileItem {
+ #[serde(rename = "type")]
+ pub item_type: ProfileItemType,
+ pub value: String,
+ pub recursive: bool,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct Profile {
+ pub id: String,
+ pub name: String,
+ pub enabled: bool,
+ pub target_id: String,
+ pub protocols: Vec,
+ pub items: Vec,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct TargetInput {
+ pub id: Option,
+ pub name: String,
+ #[serde(default = "default_target_kind")]
+ pub kind: String,
+ #[serde(default = "default_proxy_protocol")]
+ pub protocol: String,
+ pub host: String,
+ pub port: u32,
+ #[serde(default)]
+ pub requires_component: Option,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct Target {
+ pub id: String,
+ pub name: String,
+ pub kind: TargetKind,
+ pub protocol: ProxyProtocol,
+ pub host: String,
+ pub port: u16,
+ pub requires_component: Option,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct ComponentStatus {
+ pub id: ComponentId,
+ pub name: String,
+ pub state: ComponentState,
+ pub installed: bool,
+ pub running: bool,
+ pub version: Option,
+ pub path: Option,
+ #[serde(default)]
+ pub problems: Vec,
+ #[serde(default)]
+ pub actions: Vec,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct LocalSingBoxConfig {
+ #[serde(default)]
+ pub subscription_url: Option,
+ #[serde(default)]
+ pub selected_server_tag: Option,
+ #[serde(default = "default_local_singbox_listen_host")]
+ pub listen_host: String,
+ #[serde(default = "default_local_singbox_listen_port")]
+ pub listen_port: u16,
+ #[serde(default = "default_local_singbox_service_name")]
+ pub service_name: String,
+ #[serde(default = "default_local_singbox_install_root")]
+ pub install_root: String,
+ #[serde(default)]
+ pub updated_at: Option,
+}
+
+impl LocalSingBoxConfig {
+ pub fn subscription_display_url(&self) -> Option {
+ self.subscription_url
+ .as_deref()
+ .map(redact_subscription_url)
+ }
+}
+
+impl Default for LocalSingBoxConfig {
+ fn default() -> Self {
+ Self {
+ subscription_url: None,
+ selected_server_tag: None,
+ listen_host: default_local_singbox_listen_host(),
+ listen_port: default_local_singbox_listen_port(),
+ service_name: default_local_singbox_service_name(),
+ install_root: default_local_singbox_install_root(),
+ updated_at: None,
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+pub struct SubscriptionCache {
+ pub config: serde_json::Value,
+ #[serde(default)]
+ pub servers: Vec,
+ #[serde(default)]
+ pub user_info: serde_json::Map,
+ pub fetched_at: String,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct SubscriptionServer {
+ pub tag: String,
+ #[serde(rename = "type")]
+ pub server_type: String,
+ pub server: String,
+ pub server_port: u16,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct ActivityEntry {
+ pub id: String,
+ pub at: String,
+ pub level: ActivityLevel,
+ pub title: String,
+ pub message: String,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub enum ActivityLevel {
+ Info,
+ Warning,
+ Error,
+ Success,
+}
+
+fn default_enabled() -> bool {
+ true
+}
+
+fn default_target_id() -> String {
+ "local-singbox".to_string()
+}
+
+fn default_protocols() -> Vec {
+ vec!["TCP".to_string(), "UDP".to_string()]
+}
+
+fn default_target_kind() -> String {
+ "external".to_string()
+}
+
+fn default_proxy_protocol() -> String {
+ "socks5".to_string()
+}
+
+fn default_local_singbox_listen_host() -> String {
+ DEFAULT_LOCAL_SINGBOX_LISTEN_HOST.to_string()
+}
+
+fn default_local_singbox_listen_port() -> u16 {
+ DEFAULT_LOCAL_SINGBOX_LISTEN_PORT
+}
+
+fn default_local_singbox_service_name() -> String {
+ DEFAULT_LOCAL_SINGBOX_SERVICE_NAME.to_string()
+}
+
+fn default_local_singbox_install_root() -> String {
+ DEFAULT_LOCAL_SINGBOX_INSTALL_ROOT.to_string()
+}
+
+pub fn redact_subscription_url(raw_url: &str) -> String {
+ let trimmed = raw_url.trim();
+ if trimmed.is_empty() {
+ return String::new();
+ }
+
+ match trimmed.split_once("://") {
+ Some((scheme, rest)) => {
+ let host = rest
+ .split(['/', '?', '#'])
+ .next()
+ .filter(|value| !value.is_empty())
+ .unwrap_or("subscription");
+ format!("{scheme}://{host}/...")
+ }
+ None => {
+ let visible = trimmed.chars().take(18).collect::();
+ if trimmed.chars().count() <= 18 {
+ "***".to_string()
+ } else {
+ format!("{visible}...")
+ }
+ }
+ }
+}
diff --git a/src-tauri/src/singbox_service.rs b/src-tauri/src/singbox_service.rs
new file mode 100644
index 0000000..0ca27da
--- /dev/null
+++ b/src-tauri/src/singbox_service.rs
@@ -0,0 +1,273 @@
+use crate::component_detection::DetectedSingBox;
+use crate::models::{DEFAULT_LOCAL_SINGBOX_INSTALL_ROOT, DEFAULT_LOCAL_SINGBOX_SERVICE_NAME};
+use serde::{Deserialize, Serialize};
+use std::path::Path;
+
+pub const SINGBOX_RELEASE_API_URL: &str =
+ "https://api.github.com/repos/SagerNet/sing-box/releases/latest";
+pub const WINSW_RELEASE_API_URL: &str = "https://api.github.com/repos/winsw/winsw/releases/latest";
+pub const WINSW_WRAPPER_FILE: &str = "ProxyWardenSingBox.exe";
+pub const SINGBOX_BINARY_FILE: &str = "sing-box.exe";
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum SingBoxServiceAction {
+ Start,
+ Stop,
+}
+
+impl SingBoxServiceAction {
+ pub fn action_name(self) -> &'static str {
+ match self {
+ SingBoxServiceAction::Start => "start",
+ SingBoxServiceAction::Stop => "stop",
+ }
+ }
+
+ pub fn label(self) -> &'static str {
+ match self {
+ SingBoxServiceAction::Start => "запустить",
+ SingBoxServiceAction::Stop => "остановить",
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SingBoxSetupStatus {
+ pub ready: bool,
+ pub missing_count: usize,
+ pub items: Vec,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SingBoxSetupItem {
+ pub id: String,
+ pub name: String,
+ pub installed: bool,
+ pub version: Option,
+ pub details: String,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ServiceCommandOutput {
+ pub success: bool,
+ pub code: String,
+ pub service_name: Option