From 22175d9c39d195db6fed9e6c888a7fbe01e9f0c7 Mon Sep 17 00:00:00 2001 From: codex Date: Sun, 20 Sep 2026 20:11:32 +0900 Subject: [PATCH] feat(desktop): land the desktop application stack on dev Consolidates the desktop stack that was carried as a GitHub native stack on top of the macOS menu bar companion. #5196 landed as 38a5ab9fc4 by squash, which detached every child in the chain from its base, so the remaining work is applied here as one branch against the current dev instead of replayed through bases that no longer exist. Carries the standalone binary, the Tauri v2 cross-platform tray and webview shell, the GUI desktop shell integration, the WidgetKit appex bundle, and the signed desktop packaging for DMG, MSI, AppImage and deb. Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 92 +- .github/workflows/release.yml | 253 +- .gitignore | 5 + AGENTS.md | 6 +- README.md | 13 +- app/Info.plist | 42 - app/Package.swift | 23 +- app/Sources/IconProbe/main.swift | 32 - app/Sources/MenuBarApp/main.swift | 11 - app/Sources/MenuBarCore/Discovery.swift | 2 +- .../MenuBarCore/PollingCoordinator.swift | 2 +- app/Sources/MenuBarCore/ProxyClient.swift | 2 +- app/Sources/MenuBarCore/WidgetSnapshot.swift | 2 +- .../MenuBarCoreTests/TransportSuite.swift | 2 +- app/Sources/MenuBarUI/AppDelegate.swift | 254 - app/Sources/MenuBarUI/CompanionViews.swift | 78 - app/Sources/MenuBarUI/PopoverPanel.swift | 170 - .../MenuBarUI/PopoverViewController.swift | 384 -- app/Sources/MenuBarUI/ProviderListView.swift | 255 - app/Sources/MenuBarUI/StatusIcon.swift | 73 - app/Sources/MenuBarUI/Theme.swift | 103 - app/Sources/MenuBarUI/TimelineChartView.swift | 135 - app/Sources/MenuBarUI/Views.swift | 264 - app/Sources/MenuBarUITests/Harness.swift | 105 - app/Sources/MenuBarUITests/main.swift | 165 - app/Sources/OpenCodexWidget/Views.swift | 4 +- app/Sources/UIProbe/main.swift | 165 - app/Widget-Info.plist | 2 +- desktop/README.md | 69 + desktop/package.json | 13 + desktop/scripts/build-widget.sh | 76 + desktop/scripts/collect-release-assets.ts | 105 + desktop/scripts/prepare-sidecar.ts | 59 + desktop/scripts/updater-manifest.ts | 98 + desktop/src-tauri/Cargo.lock | 5654 +++++++++++++++++ desktop/src-tauri/Cargo.toml | 35 + desktop/src-tauri/build.rs | 3 + desktop/src-tauri/capabilities/default.json | 14 + desktop/src-tauri/icons/128x128.png | Bin 0 -> 2849 bytes desktop/src-tauri/icons/128x128@2x.png | Bin 0 -> 6300 bytes desktop/src-tauri/icons/32x32.png | Bin 0 -> 638 bytes desktop/src-tauri/icons/64x64.png | Bin 0 -> 1176 bytes desktop/src-tauri/icons/Square107x107Logo.png | Bin 0 -> 2268 bytes desktop/src-tauri/icons/Square142x142Logo.png | Bin 0 -> 3346 bytes desktop/src-tauri/icons/Square150x150Logo.png | Bin 0 -> 3655 bytes desktop/src-tauri/icons/Square284x284Logo.png | Bin 0 -> 7155 bytes desktop/src-tauri/icons/Square30x30Logo.png | Bin 0 -> 559 bytes desktop/src-tauri/icons/Square310x310Logo.png | Bin 0 -> 7822 bytes desktop/src-tauri/icons/Square44x44Logo.png | Bin 0 -> 845 bytes desktop/src-tauri/icons/Square71x71Logo.png | Bin 0 -> 1453 bytes desktop/src-tauri/icons/Square89x89Logo.png | Bin 0 -> 1716 bytes desktop/src-tauri/icons/StoreLogo.png | Bin 0 -> 894 bytes desktop/src-tauri/icons/icon.icns | Bin 0 -> 65470 bytes desktop/src-tauri/icons/icon.ico | Bin 0 -> 11591 bytes desktop/src-tauri/icons/icon.png | Bin 0 -> 13516 bytes desktop/src-tauri/icons/tray/icon.png | Bin 0 -> 3433 bytes desktop/src-tauri/src/auth.rs | 31 + desktop/src-tauri/src/discovery.rs | 116 + desktop/src-tauri/src/formatting.rs | 79 + desktop/src-tauri/src/lib.rs | 119 + desktop/src-tauri/src/logging.rs | 14 + desktop/src-tauri/src/main.rs | 3 + desktop/src-tauri/src/proxy.rs | 111 + desktop/src-tauri/src/sidecar.rs | 47 + desktop/src-tauri/src/tray.rs | 351 + desktop/src-tauri/src/updater.rs | 71 + desktop/src-tauri/src/widget.rs | 495 ++ desktop/src-tauri/src/window.rs | 88 + desktop/src-tauri/tauri.conf.json | 71 + desktop/ui/index.html | 29 + desktop/ui/main.js | 34 + docs-site/astro.config.mjs | 1 + .../docs/fr/reference/cli/lifecycle.md | 1 + .../docs/getting-started/installation.md | 13 + .../docs/getting-started/quickstart.md | 5 + .../src/content/docs/guides/desktop-app.md | 84 + .../src/content/docs/guides/macos-menu-bar.md | 32 +- .../docs/ja/getting-started/installation.md | 13 + .../docs/ja/getting-started/quickstart.md | 5 + .../content/docs/ja/guides/macos-menu-bar.md | 32 +- .../docs/ja/reference/cli/lifecycle.md | 1 + .../docs/ko/getting-started/installation.md | 13 + .../docs/ko/getting-started/quickstart.md | 5 + .../content/docs/ko/guides/macos-menu-bar.md | 31 +- .../docs/ko/reference/cli/lifecycle.md | 2 + .../content/docs/reference/cli/lifecycle.md | 2 + .../docs/ru/getting-started/installation.md | 13 + .../docs/ru/getting-started/quickstart.md | 5 + .../content/docs/ru/guides/macos-menu-bar.md | 32 +- .../docs/ru/reference/cli/lifecycle.md | 2 + .../docs/tr/reference/cli/lifecycle.md | 2 + .../zh-cn/getting-started/installation.md | 12 + .../docs/zh-cn/getting-started/quickstart.md | 5 + .../docs/zh-cn/guides/macos-menu-bar.md | 29 +- .../docs/zh-cn/reference/cli/lifecycle.md | 1 + .../docs/zh-tw/reference/cli/lifecycle.md | 1 + gui/src/App.tsx | 18 + gui/src/i18n/de.ts | 25 +- gui/src/i18n/en.ts | 25 +- gui/src/i18n/fr.ts | 25 +- gui/src/i18n/ja.ts | 25 +- gui/src/i18n/ko.ts | 25 +- gui/src/i18n/ru.ts | 25 +- gui/src/i18n/tr.ts | 25 +- gui/src/i18n/vi.ts | 25 +- gui/src/i18n/zh-TW.ts | 25 +- gui/src/i18n/zh.ts | 25 +- gui/src/lib/desktop-shell.ts | 32 + gui/src/pages/usage-companion-panel.tsx | 147 +- gui/src/pages/usage-companion-utils.ts | 1 + gui/tests/desktop-shell.test.ts | 37 + package.json | 5 +- readme/README.fr.md | 5 +- readme/README.ja.md | 4 +- readme/README.ko.md | 4 +- readme/README.ru.md | 5 +- readme/README.tr.md | 4 +- readme/README.zh-CN.md | 4 +- readme/README.zh-TW.md | 4 +- readme/i18n-manifest.json | 14 +- scripts/build-macos-app.sh | 268 - scripts/build-standalone.ts | 53 + scripts/package-macos-release.sh | 113 - scripts/test-layout/layout.json | 6 +- src/cli/help.ts | 10 +- src/cli/registry.ts | 3 +- src/client/machine-listener.ts | 7 +- src/lib/bun-runtime.ts | 8 +- src/lib/package-tree-integrity.ts | 3 +- src/lib/package-version.ts | 8 + src/lib/standalone.ts | 16 + src/lib/winsw.ts | 4 +- src/server/gui-static.ts | 14 +- src/server/management-api.ts | 11 +- src/server/management/companion-routes.ts | 31 +- src/server/management/route-registry.ts | 1 + src/server/responses/codex-ws-wire.ts | 10 +- src/service/health.ts | 5 +- src/service/launchd.ts | 2 +- src/service/state.ts | 11 +- src/service/windows-taskxml.ts | 21 +- src/tray/windows-tray.ps1 | 2 +- src/update/index.ts | 7 +- structure/INDEX.md | 3 + structure/desktop-shell.md | 50 + structure/gui-and-management-api.md | 5 + structure/manifest.json | 26 +- structure/ops/docs-and-release.md | 4 + structure/ops/service-and-sidecars.md | 5 + structure/overview.md | 8 +- tests/ci-workflows/ci-structure-gate.test.ts | 15 +- tests/ci-workflows/ci-workflows.test.ts | 1 + .../release-desktop-scripts.test.ts | 199 + tests/fixtures/test-layout-expected.json | 6 +- tests/gui/gui-desktop-sidecar-script.test.ts | 18 + tests/gui/gui-static.test.ts | 13 + tests/gui/macos-build-script.test.ts | 201 - tests/gui/standalone-build-script.test.ts | 18 + tests/lib/standalone.test.ts | 22 + tests/server/companion-settings.test.ts | 41 +- tests/service/standalone-service.test.ts | 26 + 161 files changed, 9441 insertions(+), 3139 deletions(-) delete mode 100644 app/Info.plist delete mode 100644 app/Sources/IconProbe/main.swift delete mode 100644 app/Sources/MenuBarApp/main.swift delete mode 100644 app/Sources/MenuBarUI/AppDelegate.swift delete mode 100644 app/Sources/MenuBarUI/CompanionViews.swift delete mode 100644 app/Sources/MenuBarUI/PopoverPanel.swift delete mode 100644 app/Sources/MenuBarUI/PopoverViewController.swift delete mode 100644 app/Sources/MenuBarUI/ProviderListView.swift delete mode 100644 app/Sources/MenuBarUI/StatusIcon.swift delete mode 100644 app/Sources/MenuBarUI/Theme.swift delete mode 100644 app/Sources/MenuBarUI/TimelineChartView.swift delete mode 100644 app/Sources/MenuBarUI/Views.swift delete mode 100644 app/Sources/MenuBarUITests/Harness.swift delete mode 100644 app/Sources/MenuBarUITests/main.swift delete mode 100644 app/Sources/UIProbe/main.swift create mode 100644 desktop/README.md create mode 100644 desktop/package.json create mode 100755 desktop/scripts/build-widget.sh create mode 100644 desktop/scripts/collect-release-assets.ts create mode 100644 desktop/scripts/prepare-sidecar.ts create mode 100644 desktop/scripts/updater-manifest.ts create mode 100644 desktop/src-tauri/Cargo.lock create mode 100644 desktop/src-tauri/Cargo.toml create mode 100644 desktop/src-tauri/build.rs create mode 100644 desktop/src-tauri/capabilities/default.json create mode 100644 desktop/src-tauri/icons/128x128.png create mode 100644 desktop/src-tauri/icons/128x128@2x.png create mode 100644 desktop/src-tauri/icons/32x32.png create mode 100644 desktop/src-tauri/icons/64x64.png create mode 100644 desktop/src-tauri/icons/Square107x107Logo.png create mode 100644 desktop/src-tauri/icons/Square142x142Logo.png create mode 100644 desktop/src-tauri/icons/Square150x150Logo.png create mode 100644 desktop/src-tauri/icons/Square284x284Logo.png create mode 100644 desktop/src-tauri/icons/Square30x30Logo.png create mode 100644 desktop/src-tauri/icons/Square310x310Logo.png create mode 100644 desktop/src-tauri/icons/Square44x44Logo.png create mode 100644 desktop/src-tauri/icons/Square71x71Logo.png create mode 100644 desktop/src-tauri/icons/Square89x89Logo.png create mode 100644 desktop/src-tauri/icons/StoreLogo.png create mode 100644 desktop/src-tauri/icons/icon.icns create mode 100644 desktop/src-tauri/icons/icon.ico create mode 100644 desktop/src-tauri/icons/icon.png create mode 100644 desktop/src-tauri/icons/tray/icon.png create mode 100644 desktop/src-tauri/src/auth.rs create mode 100644 desktop/src-tauri/src/discovery.rs create mode 100644 desktop/src-tauri/src/formatting.rs create mode 100644 desktop/src-tauri/src/lib.rs create mode 100644 desktop/src-tauri/src/logging.rs create mode 100644 desktop/src-tauri/src/main.rs create mode 100644 desktop/src-tauri/src/proxy.rs create mode 100644 desktop/src-tauri/src/sidecar.rs create mode 100644 desktop/src-tauri/src/tray.rs create mode 100644 desktop/src-tauri/src/updater.rs create mode 100644 desktop/src-tauri/src/widget.rs create mode 100644 desktop/src-tauri/src/window.rs create mode 100644 desktop/src-tauri/tauri.conf.json create mode 100644 desktop/ui/index.html create mode 100644 desktop/ui/main.js create mode 100644 docs-site/src/content/docs/guides/desktop-app.md create mode 100644 gui/src/lib/desktop-shell.ts create mode 100644 gui/tests/desktop-shell.test.ts delete mode 100755 scripts/build-macos-app.sh create mode 100644 scripts/build-standalone.ts delete mode 100755 scripts/package-macos-release.sh create mode 100644 src/lib/package-version.ts create mode 100644 src/lib/standalone.ts create mode 100644 structure/desktop-shell.md create mode 100644 tests/ci-workflows/release-desktop-scripts.test.ts create mode 100644 tests/gui/gui-desktop-sidecar-script.test.ts delete mode 100644 tests/gui/macos-build-script.test.ts create mode 100644 tests/gui/standalone-build-script.test.ts create mode 100644 tests/lib/standalone.test.ts create mode 100644 tests/service/standalone-service.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e5eacaa1e9..3dbb2f7ded 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,7 @@ on: - "tests/**" - "scripts/**" - "app/**" + - "desktop/**" - "gui/**" - "assets/**" - ".gitattributes" @@ -215,6 +216,7 @@ jobs: - 'tests/**' - 'scripts/**' - 'app/**' + - 'desktop/**' - 'gui/**' - 'assets/**' - '.gitattributes' @@ -1149,12 +1151,12 @@ jobs: # `if: always()` is load-bearing. Without it, a failed or skipped dependency # skips this job too — and GitHub reports a skipped job as success, so the gate # would go green precisely when something went wrong. - macos-app: - name: macos app + widget: + name: macos widget + bundle needs: [changes, gates] if: github.event_name != 'pull_request' || needs.changes.outputs.ci == 'true' runs-on: macos-latest - timeout-minutes: 20 + timeout-minutes: 30 steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 @@ -1167,13 +1169,80 @@ jobs: bun-version: 1.3.14 - name: Install dependencies - run: bun install --frozen-lockfile + run: | + bun install --frozen-lockfile + cd desktop + bun install --frozen-lockfile + + - name: Setup Rust + uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de # master + with: + toolchain: stable - - name: Test macOS menu bar app + - name: Test MenuBarCore run: bun run test:macos - - name: Build macOS menu bar app - run: bun run build:macos + - name: Build dashboard + run: bun run build:gui + + - name: Prepare desktop sidecar + run: bun desktop/scripts/prepare-sidecar.ts + + - name: Build WidgetKit appex + run: bash desktop/scripts/build-widget.sh + + - name: Build unsigned desktop app + working-directory: desktop + run: bunx tauri build --ci --bundles app + + - name: Verify WidgetKit appex and desktop app + run: | + app=desktop/src-tauri/target/release/bundle/macos/OpenCodex.app + test -x "$app/Contents/MacOS/OpenCodex" + test -x "$app/Contents/PlugIns/OpenCodexWidget.appex/Contents/MacOS/OpenCodexWidget" + test -x "$app/Contents/MacOS/ocx" + codesign -dv "$app/Contents/PlugIns/OpenCodexWidget.appex" + + desktop-shell: + name: desktop shell + needs: [changes, gates] + if: github.event_name != 'pull_request' || needs.changes.outputs.ci == 'true' + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + persist-credentials: false + + - name: Install Tauri Linux dependencies + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf + + - name: Setup Rust + uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de # master + with: + toolchain: stable + components: rustfmt, clippy + + - name: Prepare desktop check resources + run: | + set -euo pipefail + triple="$(rustc -vV | sed -n 's/^host: //p')" + mkdir -p desktop/src-tauri/binaries desktop/src-tauri/resources/gui/dist + : > "desktop/src-tauri/binaries/ocx-${triple}" + chmod +x "desktop/src-tauri/binaries/ocx-${triple}" + : > desktop/src-tauri/resources/gui/dist/.keep + + - name: Check Rust formatting + run: cargo fmt --manifest-path desktop/src-tauri/Cargo.toml --check + + - name: Run Rust clippy + run: cargo clippy --manifest-path desktop/src-tauri/Cargo.toml --all-targets -- -D warnings + + - name: Run Rust tests + run: cargo test --manifest-path desktop/src-tauri/Cargo.toml ci: name: ci @@ -1182,7 +1251,7 @@ jobs: # direct dependencies only, so a failing `select-windows-runner` would # otherwise reach this gate as nothing at all while its dependents report # `skipped`, which is the shape the step below is written to catch. - needs: [changes, select-windows-runner, test, storage-policy, api-usage, gates, platform-macos, macos-control, platform-windows, keyring-smoke, docker-smoke, docs-site-build, structure-gate, npm-global-smoke, macos-app] + needs: [changes, select-windows-runner, test, storage-policy, api-usage, gates, platform-macos, macos-control, platform-windows, keyring-smoke, docker-smoke, docs-site-build, structure-gate, npm-global-smoke, widget, desktop-shell] runs-on: ubuntu-latest timeout-minutes: 5 permissions: @@ -1248,12 +1317,15 @@ jobs: GATED_JOBS="changes select-windows-runner test storage-policy api-usage gates" GATED_JOBS="$GATED_JOBS platform-macos keyring-smoke docker-smoke npm-global-smoke" GATED_JOBS="$GATED_JOBS macos-control platform-windows docs-site-build" - GATED_JOBS="$GATED_JOBS structure-gate macos-app" + GATED_JOBS="$GATED_JOBS structure-gate widget" + GATED_JOBS="$GATED_JOBS desktop-shell" expected_for() { case "$1" in changes|select-windows-runner) echo requested ;; - test|storage-policy|api-usage|gates|platform-macos|keyring-smoke|docker-smoke|macos-app) + test|storage-policy|api-usage|gates|platform-macos|keyring-smoke|docker-smoke|widget) + echo "$scoped" ;; + desktop-shell) echo "$scoped" ;; npm-global-smoke) echo "$packaging" ;; docs-site-build) echo "$docs" ;; diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2c3c200258..dd63e98a51 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -69,61 +69,258 @@ jobs: process.exit(1); } NODE - package-macos: + package-standalone: needs: validate-dispatch - runs-on: macos-latest - timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + target: bun-linux-x64 + smoke: true + - os: macos-latest + target: bun-darwin-arm64 + smoke: true + - os: macos-latest + target: bun-darwin-x64 + smoke: false + - os: windows-latest + target: bun-windows-x64 + smoke: true + - os: ubuntu-latest + target: bun-linux-arm64 + smoke: false + runs-on: ${{ matrix.os }} + timeout-minutes: 25 permissions: contents: read - outputs: - archive_name: ${{ steps.package.outputs.archive_name }} - checksum_name: ${{ steps.package.outputs.checksum_name }} steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: persist-credentials: false - - name: Package the macOS companion - id: package + - name: Setup project Bun + uses: ./.github/actions/setup-project-bun + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Build dashboard + run: bun run build:gui + + - name: Build standalone binary + run: bun run build:standalone --target ${{ matrix.target }} + + - name: Smoke test standalone binary + if: matrix.smoke && runner.os != 'Windows' + shell: bash + run: | + set -euo pipefail + binary="dist/standalone/${{ matrix.target }}/ocx" + "$binary" --version + OPENCODEX_HOME="$RUNNER_TEMP/ocx-home" "$binary" start --port 10177 >"$RUNNER_TEMP/ocx.log" 2>&1 & + pid=$! + trap 'kill "$pid" 2>/dev/null || true' EXIT + for _ in $(seq 1 30); do curl -fsS http://127.0.0.1:10177/healthz && break || sleep 1; done + curl -fsS http://127.0.0.1:10177/healthz + test "$(curl -sS -o /dev/null -w '%{http_code}' http://127.0.0.1:10177/)" = 200 + + - name: Smoke test standalone binary (Windows) + if: matrix.smoke && runner.os == 'Windows' + shell: pwsh + run: | + $binary = "dist/standalone/${{ matrix.target }}/ocx.exe" + & $binary --version + $env:OPENCODEX_HOME = Join-Path $env:RUNNER_TEMP "ocx-home" + $process = Start-Process -FilePath $binary -ArgumentList "start", "--port", "10177" -PassThru + try { + for ($i = 0; $i -lt 30; $i++) { + try { Invoke-WebRequest -UseBasicParsing http://127.0.0.1:10177/healthz | Out-Null; break } catch { Start-Sleep -Seconds 1 } + } + Invoke-WebRequest -UseBasicParsing http://127.0.0.1:10177/healthz | Select-Object -ExpandProperty Content + Invoke-WebRequest -UseBasicParsing http://127.0.0.1:10177/ | Out-Null + } finally { Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue } + + - name: Archive standalone release + shell: bash env: RELEASE_VERSION: ${{ inputs.version }} - UNIVERSAL: "1" - # A monotonic numeric CFBundleVersion. Preview versions carry a suffix that - # Apple does not accept in that field, so the script uses the numeric core - # plus this run number. - MACOS_BUILD_NUMBER: ${{ github.run_number }} - # NOTE: intentionally no MACOS_SIGN_IDENTITY here. The build script honours - # it, but an identity NAME alone cannot sign on a hosted runner — the - # certificate and private key are never imported into a keychain, so codesign - # fails with "no identity found". Real Developer ID signing needs a protected - # P12 import, a temporary keychain, notarytool credentials, and stapling, all - # as one security-reviewed change. Until then the asset is ad-hoc signed and - # the docs carry the Gatekeeper first-launch path. - run: bash scripts/package-macos-release.sh - - - name: Upload the release asset + STANDALONE_TARGET: ${{ matrix.target }} + run: | + set -euo pipefail + cd "dist/standalone/$STANDALONE_TARGET" + if [[ "$RUNNER_OS" == "Windows" ]]; then + powershell -NoProfile -Command 'Compress-Archive -Path ocx.exe,gui -DestinationPath ("../../ocx-{0}-{1}.zip" -f $env:RELEASE_VERSION,$env:STANDALONE_TARGET) -Force' + else + tar -czf "../../ocx-${RELEASE_VERSION}-${STANDALONE_TARGET}.tar.gz" ocx gui + fi + cd ../../.. + if [[ "$RUNNER_OS" == "Windows" ]]; then sha256sum "dist/ocx-${RELEASE_VERSION}-${STANDALONE_TARGET}.zip" > "dist/ocx-${RELEASE_VERSION}-${STANDALONE_TARGET}.sha256" + else sha256sum "dist/ocx-${RELEASE_VERSION}-${STANDALONE_TARGET}.tar.gz" > "dist/ocx-${RELEASE_VERSION}-${STANDALONE_TARGET}.sha256" + fi + + - name: Upload standalone release uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: macos-release + name: standalone-${{ matrix.target }} + path: | + dist/ocx-*.tar.gz + dist/ocx-*.zip + dist/ocx-*.sha256 + if-no-files-found: error + retention-days: 7 + + package-desktop: + needs: validate-dispatch + strategy: + fail-fast: false + matrix: + include: + - os: macos-latest + target: universal-apple-darwin + bundles: app,dmg + sidecar-targets: macos + artifact-suffixes: macos.dmg,macos.app.tar.gz + - os: windows-latest + target: x86_64-pc-windows-msvc + bundles: msi + sidecar-targets: x86_64-pc-windows-msvc + artifact-suffixes: windows-x64.msi + - os: ubuntu-22.04 + target: x86_64-unknown-linux-gnu + bundles: appimage,deb + sidecar-targets: x86_64-unknown-linux-gnu + artifact-suffixes: linux-x86_64.AppImage,linux-amd64.deb + runs-on: ${{ matrix.os }} + timeout-minutes: 45 + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + persist-credentials: false + + - name: Setup project Bun + uses: ./.github/actions/setup-project-bun + + - name: Install project dependencies + run: bun install --frozen-lockfile + + - name: Build dashboard + run: bun run build:gui + + - name: Setup Rust + uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de # master + with: + toolchain: stable + + - name: Install Linux desktop dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libssl-dev + + - name: Install desktop dependencies + working-directory: desktop + run: bun install --frozen-lockfile + + - name: Prepare macOS sidecars + if: runner.os == 'macOS' + run: | + bun desktop/scripts/prepare-sidecar.ts --target aarch64-apple-darwin + bun desktop/scripts/prepare-sidecar.ts --target x86_64-apple-darwin + + - name: Prepare sidecar + if: runner.os != 'macOS' + run: bun desktop/scripts/prepare-sidecar.ts --target ${{ matrix.sidecar-targets }} + + - name: Build WidgetKit extension + if: runner.os == 'macOS' + run: bash desktop/scripts/build-widget.sh + + # Release signing is intentionally secret-gated. Developer ID, notarization, + # and updater signatures require maintainer-owned credentials; builds without + # those secrets remain useful for local validation but are not release assets. + - name: Build desktop bundles + working-directory: desktop + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + MACOS_SIGN_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + run: bunx tauri build --ci --target ${{ matrix.target }} --bundles ${{ matrix.bundles }} + + - name: Rename release assets + env: + RELEASE_VERSION: ${{ inputs.version }} + DESKTOP_TARGET: ${{ matrix.target }} + run: | + bun desktop/scripts/collect-release-assets.ts \ + --version "$RELEASE_VERSION" \ + --target "$DESKTOP_TARGET" \ + --out dist/release + + - name: Upload desktop release + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: desktop-${{ matrix.target }} path: dist/release/ if-no-files-found: error retention-days: 7 - attach-macos: + attach-release: runs-on: ubuntu-latest - needs: [publish, package-macos] + needs: [publish, package-standalone, package-desktop] if: ${{ inputs.dry-run != true }} + env: + UPDATER_SIGNING_CONFIGURED: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY != '' }} timeout-minutes: 10 permissions: contents: write steps: - - name: Download the packaged asset + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + persist-credentials: false + + - name: Setup project Bun + uses: ./.github/actions/setup-project-bun + + - name: Download standalone packaged assets uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: macos-release + pattern: standalone-* + merge-multiple: true path: dist/release + - name: Download desktop packaged assets + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: desktop-* + merge-multiple: true + path: dist/release + + # Generate latest.json only when the updater key is configured; then require + # signatures for all four updater platforms before publishing it. + - name: Generate updater manifest + if: env.UPDATER_SIGNING_CONFIGURED == 'true' + env: + RELEASE_VERSION: ${{ inputs.version }} + run: | + bun desktop/scripts/updater-manifest.ts \ + --version "$RELEASE_VERSION" \ + --dir dist/release \ + --repo lidge-jun/opencodex \ + --out dist/release/latest.json \ + --require-all + - name: Verify the checksum before uploading run: | cd dist/release diff --git a/.gitignore b/.gitignore index e6dd44882e..8f9c5f9fd5 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,8 @@ go/ native/**/target/ dist/macos/ dist/release/ +desktop/src-tauri/binaries/ +desktop/src-tauri/resources/ +desktop/src-tauri/widget/ +desktop/src-tauri/gen/ +desktop/src-tauri/target/ diff --git a/AGENTS.md b/AGENTS.md index 469912b5e3..5fc447e7c3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,9 +27,8 @@ Bun-native TypeScript with no separate server compile step. seeds in `layout.json` place a conventionally named file until then. History: `devlog/_fin/260905_test_modularization_and_windows/`. - `gui/` — React + Vite dashboard; packaged output is served from `gui/dist`. -- `app/` — native macOS menu bar companion (Swift + AppKit, no third-party - dependencies). `MenuBarCore` is the testable transport/model layer, - `MenuBarUI` the AppKit views, `MenuBarApp` the entry point. Its tests are +- `app/` — native macOS WidgetKit extension bundled into the Tauri desktop app; + `MenuBarCore` is its snapshot model/formatting layer. Its tests are executables, not XCTest bundles — Command Line Tools ships neither a usable XCTest module nor the swift-testing runtime. - `docs-site/` — public docs (Astro + Starlight), deployed to GitHub Pages. @@ -46,6 +45,7 @@ Bun-native TypeScript with no separate server compile step. gone, and on a new `src/` area nobody claimed. - `scripts/` — release and maintenance tooling; `scripts/release.ts` is the release authority. +- `desktop/` — Tauri v2 desktop shell, bootstrap UI, and compiled proxy sidecar preparation. - `devlog/` — planning and investigation notes, tracked in this repository. See "The `devlog` directory" below for what may and may not go there. diff --git a/README.md b/README.md index 88a6438b3d..d5a2b010cf 100644 --- a/README.md +++ b/README.md @@ -91,13 +91,16 @@ Open **http://localhost:10100** and configure everything in the web dashboard (40+ built-ins, or any OpenAI-compatible endpoint), pick models, manage accounts. `ocx gui` re-opens the dashboard at any time. -### macOS menu bar app +### macOS desktop app and widget -A native companion for proxy status, usage, and provider quotas without opening the -dashboard. The source lives in [`app/`](./app) (Swift + AppKit, no third-party -dependencies). Download it from the +Download the desktop app for macOS, Windows, or Linux from the +[latest releases](https://github.com/lidge-jun/opencodex/releases). + +A native desktop app and WidgetKit extension for proxy status, usage, and provider +quotas without opening the dashboard. The snapshot model lives in [`app/`](./app) +(`MenuBarCore`). Download it from the [releases page](https://github.com/lidge-jun/opencodex/releases) or build it locally with -`bun run build:macos`. +`bun run prepare-sidecar && bun run prepare-widget && bunx tauri build`. The first launch needs a right-click → Open, because the app is ad-hoc signed rather than notarized. See the [macOS Menu Bar App guide](https://lidge-jun.github.io/opencodex/guides/macos-menu-bar/) diff --git a/app/Info.plist b/app/Info.plist deleted file mode 100644 index 3d52873063..0000000000 --- a/app/Info.plist +++ /dev/null @@ -1,42 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - OpenCodexMenuBar - CFBundleIdentifier - com.opencodex.menubar - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - OpenCodex - CFBundleDisplayName - OpenCodex - CFBundlePackageType - APPL - CFBundleIconFile - OpenCodex - CFBundleShortVersionString - 0.0.0 - CFBundleVersion - 0.0.0 - LSUIElement - - LSMinimumSystemVersion - 13.0 - - NSAppTransportSecurity - - NSAllowsLocalNetworking - - - NSHumanReadableCopyright - MIT — opencodex contributors - - diff --git a/app/Package.swift b/app/Package.swift index 9e5f137275..0e14e3bd5b 100644 --- a/app/Package.swift +++ b/app/Package.swift @@ -2,26 +2,14 @@ import PackageDescription let package = Package( - name: "OpenCodexMenuBar", + name: "OpenCodexWidget", platforms: [.macOS(.v13)], products: [ - .executable(name: "OpenCodexMenuBar", targets: ["MenuBarApp"]), .executable(name: "OpenCodexWidget", targets: ["OpenCodexWidget"]), .executable(name: "MenuBarCoreTests", targets: ["MenuBarCoreTests"]), - .executable(name: "MenuBarUITests", targets: ["MenuBarUITests"]), - .executable(name: "UIProbe", targets: ["UIProbe"]), - .executable(name: "IconProbe", targets: ["IconProbe"]), ], targets: [ .target(name: "MenuBarCore", path: "Sources/MenuBarCore"), - // AppKit views live in a library so both the app and the visual-QA probe can - // build the same surface. An executable target cannot be imported. - .target(name: "MenuBarUI", dependencies: ["MenuBarCore"], path: "Sources/MenuBarUI"), - .executableTarget( - name: "MenuBarApp", - dependencies: ["MenuBarCore", "MenuBarUI"], - path: "Sources/MenuBarApp" - ), .executableTarget( name: "OpenCodexWidget", dependencies: ["MenuBarCore"], @@ -41,15 +29,6 @@ let package = Package( dependencies: ["MenuBarCore"], path: "Sources/MenuBarCoreTests" ), - // UI-layer tests need AppKit and an NSApplication, so they are a separate - // executable from the dependency-free core suite. - .executableTarget( - name: "MenuBarUITests", - dependencies: ["MenuBarCore", "MenuBarUI"], - path: "Sources/MenuBarUITests" - ), - .executableTarget(name: "UIProbe", dependencies: ["MenuBarCore", "MenuBarUI"], path: "Sources/UIProbe"), - .executableTarget(name: "IconProbe", dependencies: ["MenuBarCore", "MenuBarUI"], path: "Sources/IconProbe"), ], swiftLanguageVersions: [.v5] ) diff --git a/app/Sources/IconProbe/main.swift b/app/Sources/IconProbe/main.swift deleted file mode 100644 index f9a359114d..0000000000 --- a/app/Sources/IconProbe/main.swift +++ /dev/null @@ -1,32 +0,0 @@ -// Renders every menu bar glyph state to one sheet so the state signal can be verified -// visually. The notch previously did not render at all, which made protected and -// at-risk indistinguishable. -import AppKit -import MenuBarCore -import MenuBarUI - -let states: [(String, ProxyState)] = [ - ("protected", .running(StartupHealth(status: "protected"))), - ("at-risk", .running(StartupHealth(status: "at-risk"))), - ("loading", .loading), - ("stopped", .unreachable), -] - -let scale: CGFloat = 6 -let cell = NSSize(width: 17 * scale, height: 17 * scale) -let sheet = NSImage(size: NSSize(width: cell.width * CGFloat(states.count), height: cell.height)) -sheet.lockFocus() -NSColor.white.setFill() -NSRect(origin: .zero, size: sheet.size).fill() -for (i, entry) in states.enumerated() { - let img = StatusIcon.image(for: entry.1) - let rect = NSRect(x: CGFloat(i) * cell.width, y: 0, width: cell.width, height: cell.height) - NSGraphicsContext.current?.imageInterpolation = .none - img.draw(in: rect.insetBy(dx: 8, dy: 8)) -} -sheet.unlockFocus() -if let tiff = sheet.tiffRepresentation, let rep = NSBitmapImageRep(data: tiff), - let png = rep.representation(using: .png, properties: [:]) { - try? png.write(to: URL(fileURLWithPath: "/tmp/glyphs.png")) -} -print("wrote /tmp/glyphs.png:", states.map(\.0).joined(separator: ", ")) diff --git a/app/Sources/MenuBarApp/main.swift b/app/Sources/MenuBarApp/main.swift deleted file mode 100644 index 710ac8c51b..0000000000 --- a/app/Sources/MenuBarApp/main.swift +++ /dev/null @@ -1,11 +0,0 @@ -import AppKit -import MenuBarUI - -let app = NSApplication.shared -// .accessory keeps it out of the Dock; LSUIElement in Info.plist does the same for the -// packaged bundle, and this covers `swift run` during development. -app.setActivationPolicy(.accessory) - -let delegate = AppDelegate() -app.delegate = delegate -app.run() diff --git a/app/Sources/MenuBarCore/Discovery.swift b/app/Sources/MenuBarCore/Discovery.swift index 412a5ff5c0..23b34e1d0c 100644 --- a/app/Sources/MenuBarCore/Discovery.swift +++ b/app/Sources/MenuBarCore/Discovery.swift @@ -57,7 +57,7 @@ public enum ProxyDiscovery { /// Reads `runtime-port.json`, falling back to the default port on any problem. /// /// Every failure mode — missing file, malformed JSON, out-of-range port — resolves to - /// the default rather than throwing. A menu bar app that refuses to start because a + /// the default rather than throwing. A desktop app that refuses to start because a /// cache file is unreadable would be worse than one that probes the usual port. public static func resolve(configDirectory directory: URL) -> ProxyEndpoint { let file = directory.appendingPathComponent("runtime-port.json") diff --git a/app/Sources/MenuBarCore/PollingCoordinator.swift b/app/Sources/MenuBarCore/PollingCoordinator.swift index 4125657a62..5154fd7e83 100644 --- a/app/Sources/MenuBarCore/PollingCoordinator.swift +++ b/app/Sources/MenuBarCore/PollingCoordinator.swift @@ -2,7 +2,7 @@ import Foundation /// Owns the refresh schedule and turns transport results into a `ProxySnapshot`. /// -/// Polling is deliberately conservative. A menu bar app that hits a local server every +/// Polling is deliberately conservative. A desktop app that hits a local server every /// five seconds forever is a battery complaint waiting to happen, so heavy aggregation /// endpoints are fetched only while the popover is open, and repeated failures back the /// liveness tick off rather than hammering a proxy the user has stopped on purpose. diff --git a/app/Sources/MenuBarCore/ProxyClient.swift b/app/Sources/MenuBarCore/ProxyClient.swift index 5318827814..e9a3198424 100644 --- a/app/Sources/MenuBarCore/ProxyClient.swift +++ b/app/Sources/MenuBarCore/ProxyClient.swift @@ -275,7 +275,7 @@ public actor ProxyClient { request.httpMethod = method request.timeoutInterval = timeout ?? (method == "GET" ? 4 : 6) let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "dev" - request.setValue("OpenCodexMenuBar/\(version)", forHTTPHeaderField: "User-Agent") + request.setValue("OpenCodexWidget/\(version)", forHTTPHeaderField: "User-Agent") if let credential = key ?? apiKey { request.setValue(credential, forHTTPHeaderField: "x-opencodex-api-key") } diff --git a/app/Sources/MenuBarCore/WidgetSnapshot.swift b/app/Sources/MenuBarCore/WidgetSnapshot.swift index 3dc4f9cd98..6ea48c8978 100644 --- a/app/Sources/MenuBarCore/WidgetSnapshot.swift +++ b/app/Sources/MenuBarCore/WidgetSnapshot.swift @@ -127,7 +127,7 @@ public final class WidgetSnapshotStore: @unchecked Sendable { private var loggedFailures = Set() public init( - widgetBundleID: String = "com.opencodex.menubar.widget", + widgetBundleID: String = "com.opencodex.desktop.widget", fileManager: FileManager = .default, homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser ) { diff --git a/app/Sources/MenuBarCoreTests/TransportSuite.swift b/app/Sources/MenuBarCoreTests/TransportSuite.swift index 570e10bdea..d46a667e0f 100644 --- a/app/Sources/MenuBarCoreTests/TransportSuite.swift +++ b/app/Sources/MenuBarCoreTests/TransportSuite.swift @@ -283,7 +283,7 @@ enum TransportSuite { let url = StubProtocol.recorded.first?.url?.absoluteString ?? "" t.expect(url.contains("range=7d"), "expected range=7d in \(url)") t.expect(url.contains("/api/usage"), "expected /api/usage in \(url)") - t.equal(StubProtocol.recorded.first?.value(forHTTPHeaderField: "User-Agent"), "OpenCodexMenuBar/dev") + t.equal(StubProtocol.recorded.first?.value(forHTTPHeaderField: "User-Agent"), "OpenCodexWidget/dev") } t.test("requests: the provider patch sends exactly {\"disabled\":true}") { diff --git a/app/Sources/MenuBarUI/AppDelegate.swift b/app/Sources/MenuBarUI/AppDelegate.swift deleted file mode 100644 index 7ca5c1d8e2..0000000000 --- a/app/Sources/MenuBarUI/AppDelegate.swift +++ /dev/null @@ -1,254 +0,0 @@ -import AppKit -import MenuBarCore - -public final class AppDelegate: NSObject, NSApplicationDelegate { - private var statusItem: NSStatusItem? - /// A key-capable panel rather than `NSPopover`. - /// - /// This is the single most-tested decision in this file. `NSPopover` from an - /// accessory (`LSUIElement`) process creates a window that never appears in - /// `NSApp.windows` and reports `canBecomeKey == false`, so macOS will not route key - /// events to it no matter how the process is activated — Escape and the Tab path - /// simply never arrive. A `nonactivatingPanel` that overrides `canBecomeKey` - /// measures as `canBecomeKey=1 isKey=1` under the same conditions. - private let panel = PopoverPanel() - private let controller = PopoverViewController() - private var coordinator: PollingCoordinator? - private var actions: ActionCoordinator? - private var client: ProxyClient? - private let widgetStore = WidgetSnapshotStore() - /// The snapshot the UI is currently showing, for decisions that need context - /// (the start command to display, the default provider to protect). - private var latest: ProxySnapshot? - private var endpoint = ProxyEndpoint.default - private var pollTask: Task? - /// Fallback Escape handling for the case where the panel is visible but another - /// process holds focus. Installed on open, removed on close. - private var escapeMonitor: Any? - - public override init() { super.init() } - - public func applicationDidFinishLaunching(_ notification: Notification) { - endpoint = ProxyDiscovery.resolve() - let client = ProxyClient(endpoint: endpoint) - self.client = client - let coordinator = PollingCoordinator(client: client, endpoint: endpoint) - self.coordinator = coordinator - self.actions = ActionCoordinator(client: client) - - let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) - item.button?.image = StatusIcon.image(for: .loading) - item.button?.imagePosition = .imageOnly - item.button?.target = self - item.button?.action = #selector(togglePopover) - item.button?.setAccessibilityLabel("OpenCodex proxy status") - statusItem = item - - controller.onDashboard = { [weak self] in self?.openDashboard() } - controller.onCompanionSettings = { [weak self] in self?.openCompanionSettings() } - controller.onStop = { [weak self] in self?.stopProxy() } - controller.onRefresh = { [weak self] in self?.refreshNow() } - controller.onAddKey = { [weak self] in self?.openDashboard() } - controller.onRetry = { [weak self] in self?.refreshNow() } - controller.onToggleProvider = { [weak self] name, disable in - self?.toggleProvider(name, disable: disable) - } - controller.onQuit = { NSApp.terminate(nil) } - - panel.contentViewController = controller - panel.onDismiss = { [weak self] in self?.handlePanelClosed() } - - // The observer closure is `@Sendable` and crosses actor boundaries, so it must - // not capture the delegate. It hops to the main actor and looks the delegate up - // there instead. - Task { - await coordinator.observe { snapshot in - Task { @MainActor in - (NSApp.delegate as? AppDelegate)?.render(snapshot) - } - } - await MainActor.run { (NSApp.delegate as? AppDelegate)?.startPolling() } - } - } - - public func applicationWillTerminate(_ notification: Notification) { - pollTask?.cancel() - removeEscapeMonitor() - panel.dismiss() - } - - // MARK: - Polling - - @MainActor - fileprivate func startPolling() { - guard let coordinator else { return } - pollTask?.cancel() - pollTask = Task { - while !Task.isCancelled { - await coordinator.refresh() - let interval = await coordinator.currentInterval - try? await Task.sleep(nanoseconds: UInt64(interval * 1_000_000_000)) - } - } - } - - private func refreshNow() { - Task { [coordinator] in await coordinator?.refresh(includeHeavy: true) } - } - - @MainActor - fileprivate func render(_ snapshot: ProxySnapshot) { - latest = snapshot - let title = snapshot.menuBarTitle ?? "" - statusItem?.button?.title = title - statusItem?.button?.font = NSFont.monospacedDigitSystemFont(ofSize: 12, weight: .medium) - statusItem?.button?.imagePosition = title.isEmpty ? .imageOnly : .imageLeading - statusItem?.button?.image = StatusIcon.image(for: snapshot.state) - statusItem?.button?.toolTip = "OpenCodex — \(snapshot.state.title) (\(snapshot.endpoint.display))" - controller.apply(snapshot) - let widgetSnapshot = WidgetSnapshot.make(from: snapshot) - Task.detached { [widgetStore] in widgetStore.writeIfChanged(widgetSnapshot) } - } - - // MARK: - Actions - - #if DEBUG - /// Testing hook: drives the exact presentation path a status-item click uses, so a - /// harness can verify key focus and Escape without Accessibility permission. - /// Debug-only — it is not part of the shipped surface. - public func debugTogglePanel() { togglePopover() } - #endif - - @objc private func togglePopover() { - guard let button = statusItem?.button else { return } - if panel.isShown { - panel.dismiss() - } else { - panel.present(from: button) - installEscapeMonitor() - Task { [coordinator] in await coordinator?.setPopoverOpen(true) } - } - } - - /// Called by the panel whenever it closes, however it was dismissed. - private func handlePanelClosed() { - removeEscapeMonitor() - Task { [coordinator] in await coordinator?.setPopoverOpen(false) } - } - - /// The panel is key-capable, so `cancelOperation(_:)` handles Escape in the normal - /// case. This local monitor is belt-and-braces for the window where the panel is up - /// but focus sits elsewhere in this process, such as the confirmation sheet. - private func installEscapeMonitor() { - removeEscapeMonitor() - escapeMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in - // While a confirmation is up, Escape belongs to the alert: consuming it - // here dismissed the panel and stranded the alert with no way to cancel. - guard event.keyCode == 53, - self?.panel.isShown == true, - self?.panel.isPresentingModal == false - else { return event } - self?.panel.dismiss() - return nil - } - } - - - private func removeEscapeMonitor() { - if let monitor = escapeMonitor { NSEvent.removeMonitor(monitor) } - escapeMonitor = nil - } - - private func openDashboard() { - NSWorkspace.shared.open(endpoint.baseURL) - } - - private func openCompanionSettings() { - guard let url = URL(string: "\(endpoint.baseURL.absoluteString)/#/usage#usage-section-companion") else { return } - NSWorkspace.shared.open(url) - } - - /// Stopping is destructive: it interrupts in-flight requests and stops the launchd - /// service, so nothing restarts the proxy. It always confirms first. - private func stopProxy() { - let alert = NSAlert() - alert.messageText = "Stop the OpenCodex proxy?" - alert.informativeText = - "In-flight requests will be interrupted, and OpenCodex will not restart on its own." - alert.alertStyle = .warning - alert.addButton(withTitle: "Stop proxy") - alert.addButton(withTitle: "Cancel") - - // The alert takes key focus, which would otherwise trip resignKey and dismiss - // the panel behind it — leaving a user who chose Cancel with nothing. - panel.isPresentingModal = true - NSApp.activate(ignoringOtherApps: true) - let confirmed = alert.runModal() == .alertFirstButtonReturn - panel.isPresentingModal = false - - guard confirmed else { - panel.makeKeyAndOrderFront(nil) - return - } - - let startCommand = latest?.lastKnownStartCommand ?? "ocx start" - controller.showResult("Stopping…", isError: false) - - Task { [actions, coordinator] in - let outcome = await actions?.stop(startCommand: startCommand) ?? .failed("Unavailable.") - await coordinator?.refresh() - await MainActor.run { [weak self] in - switch outcome { - case .succeeded: - self?.controller.showResult("Proxy stopped.", isError: false) - case .requiresManualStart(let command): - // Not a failure — the API has no start endpoint by design. - self?.controller.showResult("Proxy stopped. Start it again with \(command)", isError: false) - case .stoppedWithRestoreFailure(let command): - // The proxy is down but native Codex still points at the dead port. - self?.controller.showResult( - "Proxy stopped, but restoring native Codex failed. Run `ocx restore`, then \(command)", - isError: true - ) - case .failed(let message): - self?.controller.showResult(message, isError: true) - } - } - } - } - - /// Optimistic toggle: the switch has already moved, so a rejection must move it back - /// rather than leave the UI showing a state the proxy refused. - private func toggleProvider(_ name: String, disable: Bool) { - let defaultProvider = latest?.defaultProvider - controller.setProviderBusy(name, true, intended: !disable) - - Task { [actions, coordinator] in - let outcome = await actions?.setProvider(name, disabled: disable, defaultProvider: defaultProvider) - ?? .failed("Unavailable.") - await MainActor.run { [weak self] in - switch outcome { - case .succeeded: - self?.controller.showResult( - disable ? "\(name) disabled." : "\(name) enabled.", - isError: false - ) - case .failed(let message): - self?.controller.revertProvider(name, to: !disable) - self?.controller.showResult(message, isError: true) - case .requiresManualStart, .stoppedWithRestoreFailure: - // Not reachable for a provider write. - break - } - } - // Re-read so the summary line and switch states match the proxy, not our - // optimistic guess. refreshAndWait rather than refresh: a coalesced refresh - // returns immediately, which would re-enable the switch against pre-write - // data. - await coordinator?.refreshAndWait() - await MainActor.run { [weak self] in - self?.controller.setProviderBusy(name, false) - } - } - } -} diff --git a/app/Sources/MenuBarUI/CompanionViews.swift b/app/Sources/MenuBarUI/CompanionViews.swift deleted file mode 100644 index a23c4bf42c..0000000000 --- a/app/Sources/MenuBarUI/CompanionViews.swift +++ /dev/null @@ -1,78 +0,0 @@ -import AppKit -import MenuBarCore - -final class ModelsListView: NSView { - private let stack = NSStackView() - private let caption = makeLabel("MODELS", font: Theme.micro, color: Theme.faint) - - init() { - super.init(frame: .zero) - stack.orientation = .vertical - stack.alignment = .leading - stack.spacing = Theme.tightGap - stack.addArrangedSubview(caption) - stack.translatesAutoresizingMaskIntoConstraints = false - addSubview(stack) - NSLayoutConstraint.activate([ - stack.topAnchor.constraint(equalTo: topAnchor), stack.leadingAnchor.constraint(equalTo: leadingAnchor), - stack.trailingAnchor.constraint(equalTo: trailingAnchor), stack.bottomAnchor.constraint(equalTo: bottomAnchor), - ]) - } - - required init?(coder: NSCoder) { nil } - - func apply(_ snapshot: ProxySnapshot) { - clearRows() - let rows = snapshot.todayRows.sorted { ($0.totalTokens ?? 0) > ($1.totalTokens ?? 0) }.prefix(5) - isHidden = !snapshot.settings.showModels || rows.isEmpty - for row in rows { - let model = [row.provider, row.model].compactMap { $0 }.joined(separator: "/") - let cost = snapshot.settings.showCost ? " · \(Format.cost(row.estimatedCostUsd))" : "" - stack.addArrangedSubview(makeLabel( - "\(model) · \(Format.count(row.requests)) · \(Format.tokens(row.totalTokens))\(cost)", - font: Theme.caption, color: Theme.text - )) - } - } - - private func clearRows() { - for view in stack.arrangedSubviews.dropFirst() { stack.removeArrangedSubview(view); view.removeFromSuperview() } - } -} - -final class AccountsListView: NSView { - private let stack = NSStackView() - private let caption = makeLabel("ACCOUNTS", font: Theme.micro, color: Theme.faint) - - init() { - super.init(frame: .zero) - stack.orientation = .vertical - stack.alignment = .leading - stack.spacing = Theme.tightGap - stack.addArrangedSubview(caption) - stack.translatesAutoresizingMaskIntoConstraints = false - addSubview(stack) - NSLayoutConstraint.activate([ - stack.topAnchor.constraint(equalTo: topAnchor), stack.leadingAnchor.constraint(equalTo: leadingAnchor), - stack.trailingAnchor.constraint(equalTo: trailingAnchor), stack.bottomAnchor.constraint(equalTo: bottomAnchor), - ]) - } - - required init?(coder: NSCoder) { nil } - - func apply(_ snapshot: ProxySnapshot) { - clearRows() - let rows = (snapshot.today?.accounts ?? []).sorted { ($0.totalTokens ?? 0) > ($1.totalTokens ?? 0) } - isHidden = !snapshot.settings.showAccounts || rows.isEmpty - for row in rows { - stack.addArrangedSubview(makeLabel( - "\(row.accountLogLabel ?? Format.unknown) · \(Format.count(row.requests)) · \(Format.tokens(row.totalTokens))", - font: Theme.caption, color: Theme.text - )) - } - } - - private func clearRows() { - for view in stack.arrangedSubviews.dropFirst() { stack.removeArrangedSubview(view); view.removeFromSuperview() } - } -} diff --git a/app/Sources/MenuBarUI/PopoverPanel.swift b/app/Sources/MenuBarUI/PopoverPanel.swift deleted file mode 100644 index 5c579f88be..0000000000 --- a/app/Sources/MenuBarUI/PopoverPanel.swift +++ /dev/null @@ -1,170 +0,0 @@ -import AppKit - -/// The popover surface. -/// -/// Deliberately a panel rather than `NSPopover`. Measured on macOS 27 from an accessory -/// (`LSUIElement`) process: the window `NSPopover` creates never appears in -/// `NSApp.windows` and reports `canBecomeKey == false`, so the OS refuses to route key -/// events to it — Escape and Tab never arrive regardless of how the process is -/// activated. The same probe against this panel reports `canBecomeKey=1 isKey=1`. -/// -/// `nonactivatingPanel` keeps the click-through feel of a menu bar popover: opening it -/// does not steal focus from the user's editor. -public final class PopoverPanel: NSPanel { - /// Invoked whenever the panel closes, however it was dismissed. - public var onDismiss: (() -> Void)? - - private var clickOutsideMonitor: Any? - - public init() { - super.init( - contentRect: NSRect(x: 0, y: 0, width: 340, height: 300), - styleMask: [.nonactivatingPanel, .fullSizeContentView, .borderless], - backing: .buffered, - defer: false - ) - isFloatingPanel = true - level = .statusBar - hidesOnDeactivate = false - becomesKeyOnlyIfNeeded = false - isOpaque = false - backgroundColor = .clear - hasShadow = true - isMovable = false - animationBehavior = .utilityWindow - } - - /// Wraps the content in a real popover material. - /// - /// A borderless panel has NO background of its own: without this the dashboard - /// composites straight onto whatever application is underneath, so labels collide - /// with the app behind it and contrast depends on that app's colours. `NSPopover` - /// supplies this surface automatically; a panel must build it. - public override var contentViewController: NSViewController? { - didSet { - guard let content = contentViewController?.view else { return } - contentView = PopoverSurface.make(content: content) - } - } - - /// Suspends resign-key dismissal, so presenting a modal sheet does not tear the - /// panel down behind it and strand a user who chose Cancel. - public var isPresentingModal = false - - public override var canBecomeKey: Bool { true } - /// Never main: this is chrome, not a document window. - public override var canBecomeMain: Bool { false } - - public var isShown: Bool { isVisible } - - /// Presents under a status item button, clamped to the visible screen. - public func present(from button: NSStatusBarButton) { - guard let buttonWindow = button.window else { return } - layoutContent() - - let size = contentViewController?.preferredContentSize ?? frame.size - setContentSize(size) - - let buttonRect = buttonWindow.convertToScreen(button.convert(button.bounds, to: nil)) - var origin = NSPoint( - x: buttonRect.midX - size.width / 2, - y: buttonRect.minY - size.height - 6 - ) - - if let screen = buttonWindow.screen ?? NSScreen.main { - let visible = screen.visibleFrame - origin.x = min(max(origin.x, visible.minX + 8), visible.maxX - size.width - 8) - origin.y = max(origin.y, visible.minY + 8) - } - - setFrameOrigin(origin) - makeKeyAndOrderFront(nil) - NSApp.activate(ignoringOtherApps: true) - installClickOutsideMonitor() - } - - public func dismiss() { - // Idempotent: a late monitor callback must not re-run teardown. - guard isVisible else { return } - removeClickOutsideMonitor() - orderOut(nil) - onDismiss?() - } - - /// Transient behaviour: clicking anywhere else dismisses, matching what a menu bar - /// popover trained the user to expect. - private func installClickOutsideMonitor() { - removeClickOutsideMonitor() - clickOutsideMonitor = NSEvent.addGlobalMonitorForEvents( - matching: [.leftMouseDown, .rightMouseDown] - ) { [weak self] _ in - self?.dismiss() - } - } - - private func removeClickOutsideMonitor() { - if let monitor = clickOutsideMonitor { NSEvent.removeMonitor(monitor) } - clickOutsideMonitor = nil - } - - public override func cancelOperation(_ sender: Any?) { dismiss() } - - public override func resignKey() { - super.resignKey() - // Losing key focus means the user moved on — unless we put the focus elsewhere - // ourselves by presenting a confirmation. - guard !isPresentingModal else { return } - if isVisible { dismiss() } - } - - private func layoutContent() { - contentViewController?.view.layoutSubtreeIfNeeded() - } -} - -private enum PopoverSurface { - static func make(content: NSView) -> NSView { - let surface: NSView -#if compiler(>=6.2) - if #available(macOS 26, *) { - let glass = NSGlassEffectView() - glass.cornerRadius = 16 - glass.style = .regular - glass.contentView = content - surface = glass - } else { - surface = makeMaterialSurface(content: content) - } -#else - surface = makeMaterialSurface(content: content) -#endif - - let host = NSView() - host.addSubview(surface) - surface.translatesAutoresizingMaskIntoConstraints = false - content.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - surface.topAnchor.constraint(equalTo: host.topAnchor), - surface.leadingAnchor.constraint(equalTo: host.leadingAnchor), - surface.trailingAnchor.constraint(equalTo: host.trailingAnchor), - surface.bottomAnchor.constraint(equalTo: host.bottomAnchor), - content.topAnchor.constraint(equalTo: surface.topAnchor), - content.leadingAnchor.constraint(equalTo: surface.leadingAnchor), - content.trailingAnchor.constraint(equalTo: surface.trailingAnchor), - content.bottomAnchor.constraint(equalTo: surface.bottomAnchor), - ]) - return host - } - - private static func makeMaterialSurface(content: NSView) -> NSView { - let effect = NSVisualEffectView() - effect.material = .popover - effect.blendingMode = .behindWindow - effect.state = .active - effect.wantsLayer = true - effect.layer?.cornerRadius = 10 - effect.layer?.masksToBounds = true - effect.addSubview(content) - return effect - } -} diff --git a/app/Sources/MenuBarUI/PopoverViewController.swift b/app/Sources/MenuBarUI/PopoverViewController.swift deleted file mode 100644 index 1cc8dd51ae..0000000000 --- a/app/Sources/MenuBarUI/PopoverViewController.swift +++ /dev/null @@ -1,384 +0,0 @@ -import AppKit -import MenuBarCore - -/// The popover body: one column ordered by urgency. -/// -/// Deliberately not a tab bar. A menu bar popover is a glance surface, and tabs would put -/// the answer to "is it fine?" one click away three times out of four. -/// -/// Fixed header and action row with a scrolling middle: the quota and provider sections -/// grow with the user's configuration, and an uncapped popover would eventually run off -/// the screen. -public final class PopoverViewController: NSViewController { - public override init(nibName: NSNib.Name?, bundle: Bundle?) { - super.init(nibName: nibName, bundle: bundle) - } - - public required init?(coder: NSCoder) { nil } - - /// The popover never grows past this; the variable middle scrolls instead. - private static let maxHeight: CGFloat = 480 - - // Fixed chrome - private let header = StatusHeaderView() - private let dashboardButton = NSButton() - private let stopButton = NSButton() - private let overflowButton = NSButton() - /// State-specific call to action: "Add key…" or "Retry". - private let primaryButton = NSButton() - - // Scrolling body - private let scrollView = NSScrollView() - private let body = NSStackView() - private let metrics = MetricsView() - private let timelineChart = TimelineChartView() - private let models = ModelsListView() - private let accounts = AccountsListView() - private let quotaStack = NSStackView() - private let quotaEmpty = makeLabel("No provider quota sources connected.", font: Theme.caption, color: Theme.muted) - private let providers = ProviderListView() - /// Transient result of the last write action. Actions that report nothing leave the - /// user guessing whether anything happened. - private let resultBanner = makeLabel("", font: Theme.caption, color: Theme.muted) - private let skeleton = SkeletonView() - private let guidanceLabel: NSTextField = { - let field = makeLabel("", font: Theme.caption, color: Theme.muted) - // Guidance is a sentence, not a stat: let it wrap instead of truncating away - // the half that explains what to do. - field.lineBreakMode = .byWordWrapping - field.maximumNumberOfLines = 3 - field.preferredMaxLayoutWidth = Theme.width - Theme.gutter * 2 - return field - }() - private let commandField = NSTextField(labelWithString: "") - private let metricsSeparator = makeSeparator() - private let quotaSeparator = makeSeparator() - - public var onDashboard: (() -> Void)? - public var onCompanionSettings: (() -> Void)? - public var onStop: (() -> Void)? - public var onQuit: (() -> Void)? - public var onRefresh: (() -> Void)? - /// `(provider, shouldDisable)`. - public var onToggleProvider: ((String, Bool) -> Void)? - /// Distinct callbacks: "Retry" must retry in place, while "Add key…" navigates to - /// the dashboard. Routing both through one handler made Retry open a browser. - public var onAddKey: (() -> Void)? - public var onRetry: (() -> Void)? - - private var snapshot: ProxySnapshot? - private var scrollHeight: NSLayoutConstraint? - /// Guards the banner's auto-hide so a newer result is not cleared by an older timer. - private var resultToken = 0 - - public override func loadView() { - configureControls() - resultBanner.isHidden = true - resultBanner.lineBreakMode = .byWordWrapping - resultBanner.maximumNumberOfLines = 3 - resultBanner.preferredMaxLayoutWidth = Theme.width - Theme.gutter * 2 - providers.onToggle = { [weak self] name, disable in - self?.onToggleProvider?(name, disable) - } - - body.orientation = .vertical - body.alignment = .leading - body.spacing = Theme.rowGap - body.setViews( - [skeleton, metrics, timelineChart, metricsSeparator, models, quotaStack, quotaEmpty, - accounts, providers, quotaSeparator, resultBanner, guidanceLabel, commandField], - in: .top - ) - body.translatesAutoresizingMaskIntoConstraints = false - - // A flipped clip view puts the scroll origin at the TOP. Without this, content - // that overflows opens scrolled to the bottom, hiding the status and metrics the - // urgency order exists to surface first. - scrollView.contentView = FlippedClipView() - scrollView.documentView = body - scrollView.hasVerticalScroller = true - scrollView.autohidesScrollers = true - scrollView.drawsBackground = false - scrollView.borderType = .noBorder - scrollView.translatesAutoresizingMaskIntoConstraints = false - - let actions = NSStackView(views: [dashboardButton, stopButton, primaryButton, NSView(), overflowButton]) - actions.orientation = .horizontal - actions.spacing = Theme.rowGap - actions.alignment = .centerY - - let column = NSStackView(views: [header, makeSeparator(), scrollView, actions]) - column.orientation = .vertical - column.alignment = .leading - column.spacing = Theme.rowGap - column.edgeInsets = NSEdgeInsets( - top: Theme.gutter, left: Theme.gutter, - bottom: Theme.gutter, right: Theme.gutter - ) - column.translatesAutoresizingMaskIntoConstraints = false - - let root = NSView(frame: NSRect(x: 0, y: 0, width: Theme.width, height: 300)) - root.addSubview(column) - - let contentWidth = Theme.width - Theme.gutter * 2 - NSLayoutConstraint.activate([ - column.topAnchor.constraint(equalTo: root.topAnchor), - column.leadingAnchor.constraint(equalTo: root.leadingAnchor), - column.trailingAnchor.constraint(equalTo: root.trailingAnchor), - column.bottomAnchor.constraint(equalTo: root.bottomAnchor), - root.widthAnchor.constraint(equalToConstant: Theme.width), - header.widthAnchor.constraint(equalToConstant: contentWidth), - actions.widthAnchor.constraint(equalToConstant: contentWidth), - scrollView.widthAnchor.constraint(equalToConstant: contentWidth), - body.widthAnchor.constraint(equalToConstant: contentWidth), - ]) - - let heightConstraint = scrollView.heightAnchor.constraint(equalToConstant: 120) - heightConstraint.isActive = true - scrollHeight = heightConstraint - - view = root - } - - private func configureControls() { - for (button, title) in [(dashboardButton, "Dashboard"), (stopButton, "Stop proxy")] { - button.title = title - button.bezelStyle = .rounded - button.controlSize = .small - button.font = Theme.caption - button.target = self - } - dashboardButton.action = #selector(dashboardTapped) - stopButton.action = #selector(stopTapped) - - primaryButton.bezelStyle = .rounded - primaryButton.controlSize = .small - primaryButton.font = Theme.caption - primaryButton.target = self - primaryButton.action = #selector(primaryTapped) - primaryButton.isHidden = true - - overflowButton.title = "···" - overflowButton.bezelStyle = .rounded - overflowButton.controlSize = .small - overflowButton.font = Theme.caption - overflowButton.target = self - overflowButton.action = #selector(overflowTapped) - overflowButton.setAccessibilityLabel("More actions") - - quotaStack.orientation = .vertical - quotaStack.alignment = .leading - quotaStack.spacing = Theme.tightGap - - commandField.font = Theme.numericSmall - commandField.textColor = Theme.text - commandField.isSelectable = true - commandField.isBordered = false - commandField.drawsBackground = false - } - - public func apply(_ snapshot: ProxySnapshot) { - self.snapshot = snapshot - header.apply(snapshot) - - let showsData = snapshot.showsData - let isLoading = !snapshot.hasEverLoaded && snapshot.state == .loading - - // Loading shows structure, not empty copy: the shape of the answer is already - // known, only the values are missing. - skeleton.isHidden = !isLoading - - metrics.isHidden = !showsData - metricsSeparator.isHidden = !showsData - quotaSeparator.isHidden = !showsData - if showsData { - metrics.apply(snapshot) - timelineChart.apply(snapshot) - models.apply(snapshot) - accounts.apply(snapshot) - applyQuotas(snapshot) - providers.apply(snapshot) - } else { - timelineChart.isHidden = true - models.isHidden = true - accounts.isHidden = true - quotaStack.isHidden = true - quotaEmpty.isHidden = true - providers.isHidden = true - } - - applyGuidance(snapshot) - applyActions(snapshot, isLoading: isLoading) - resize() - } - - private func applyQuotas(_ snapshot: ProxySnapshot) { - for view in quotaStack.arrangedSubviews { - quotaStack.removeArrangedSubview(view) - view.removeFromSuperview() - } - let rows = snapshot.quotaRows - quotaStack.isHidden = rows.isEmpty - // "Not fetched yet" and "the proxy reported none" are different facts. - quotaEmpty.isHidden = !(rows.isEmpty && snapshot.quotasLoaded) - for quota in rows { - let row = QuotaRowView(quota: quota) - row.translatesAutoresizingMaskIntoConstraints = false - quotaStack.addArrangedSubview(row) - row.widthAnchor.constraint(equalTo: quotaStack.widthAnchor).isActive = true - } - } - - /// Shows the outcome of a write action, then clears itself. A banner that never - /// leaves would become permanent furniture. - public func showResult(_ text: String, isError: Bool) { - resultBanner.stringValue = text - resultBanner.textColor = isError ? Theme.red : Theme.muted - resultBanner.isHidden = false - refreshSize() - - resultToken &+= 1 - let token = resultToken - DispatchQueue.main.asyncAfter(deadline: .now() + 6) { [weak self] in - guard let self, self.resultToken == token else { return } - self.resultBanner.isHidden = true - self.refreshSize() - } - } - - public func revertProvider(_ name: String, to enabled: Bool) { - providers.revert(name, to: enabled) - } - - public func setProviderBusy(_ name: String, _ busy: Bool, intended: Bool? = nil) { - providers.setBusy(name, busy, intended: intended) - } - - /// Re-measures after content changes height (disclosure, banner). - public func refreshSize() { resize() } - - /// Guidance text plus any command the user should run. Commands are shown as - /// selectable text; the app never executes them. - private func applyGuidance(_ snapshot: ProxySnapshot) { - var guidance: String? - var command: String? - - switch snapshot.nextAction { - case .none: - // A running-but-at-risk proxy still has advice worth surfacing. - if case .running = snapshot.state, let recommended = snapshot.recommendedCommand { - guidance = "Recommended:" - command = recommended - } - case .runCommand(let value): - guidance = "Start it again with:" - command = value - case .addAPIKey: - guidance = "This proxy is bound to a non-loopback address and needs a key." - case .retry: - guidance = snapshot.dataAge.map { "Showing data from \(Format.age($0)). Retrying automatically." } - ?? "Retrying automatically." - } - - guidanceLabel.isHidden = guidance == nil - guidanceLabel.stringValue = guidance ?? "" - commandField.isHidden = command == nil - commandField.stringValue = command ?? "" - if let command { - commandField.setAccessibilityLabel("Command to run: \(command)") - } - } - - private func applyActions(_ snapshot: ProxySnapshot, isLoading: Bool) { - // Nothing is actionable before the first read completes. - dashboardButton.isEnabled = !isLoading - overflowButton.isEnabled = !isLoading - stopButton.isEnabled = snapshot.state.isRunning - stopButton.isHidden = !snapshot.state.isRunning - - switch snapshot.nextAction { - case .addAPIKey: - primaryButton.isHidden = false - primaryButton.title = "Add key…" - primaryButton.keyEquivalent = "\r" - case .retry: - primaryButton.isHidden = false - primaryButton.title = "Retry" - primaryButton.keyEquivalent = "\r" - case .none, .runCommand: - primaryButton.isHidden = true - primaryButton.keyEquivalent = "" - } - } - - private func resize() { - view.layoutSubtreeIfNeeded() - let bodyHeight = ceil(body.fittingSize.height) - // Chrome is the header, separator, action row, and insets. - let chrome = ceil(header.fittingSize.height) + Theme.gutter * 2 + Theme.rowGap * 3 + 28 - let natural = chrome + bodyHeight - let capped = min(Self.maxHeight, natural) - // Scrollers appear only when the content genuinely overflows; a scroll bar on a - // three-line loading state reads as a broken layout. - let overflowing = natural > Self.maxHeight - scrollView.hasVerticalScroller = overflowing - scrollHeight?.constant = max(0, capped - chrome) - preferredContentSize = NSSize(width: Theme.width, height: max(96, capped)) - } - - // MARK: - Actions - - @objc private func dashboardTapped() { onDashboard?() } - @objc private func stopTapped() { onStop?() } - @objc private func primaryTapped() { - switch snapshot?.nextAction { - case .addAPIKey: onAddKey?() - case .retry: onRetry?() - default: break - } - } - @objc private func refreshTapped() { onRefresh?() } - @objc private func quitTapped() { onQuit?() } - - @objc private func overflowTapped() { - let menu = NSMenu() - menu.addItem(withTitle: "Refresh", action: #selector(refreshTapped), keyEquivalent: "r").target = self - menu.addItem(withTitle: "Open dashboard", action: #selector(dashboardTapped), keyEquivalent: "").target = self - menu.addItem(withTitle: "Companion settings…", action: #selector(companionSettingsTapped), keyEquivalent: "").target = self - menu.addItem(.separator()) - menu.addItem(withTitle: "Quit OpenCodex", action: #selector(quitTapped), keyEquivalent: "q").target = self - menu.popUp(positioning: nil, at: NSPoint(x: 0, y: overflowButton.bounds.height + 4), in: overflowButton) - } - @objc private func companionSettingsTapped() { onCompanionSettings?() } - - /// AppKit routes Escape here for the whole responder chain, which `keyDown` does not - /// reliably receive inside a popover. - public override func cancelOperation(_ sender: Any?) { - view.window?.performClose(nil) - } -} - -/// Top-anchored clip view. AppKit scroll views are bottom-origin by default. -final class FlippedClipView: NSClipView { - override var isFlipped: Bool { true } -} - -/// Loading structure: grey bars where values will appear, so the first paint shows the -/// shape of the answer instead of empty space or a spinner. -final class SkeletonView: NSView { - override var intrinsicContentSize: NSSize { - NSSize(width: NSView.noIntrinsicMetric, height: 84) - } - - override func draw(_ dirtyRect: NSRect) { - Theme.raised.setFill() - let widths: [CGFloat] = [72, 0, 96, 140, 120, 110] - var y = bounds.maxY - 12 - for width in widths { - guard width > 0 else { y -= 8; continue } - let rect = NSRect(x: 0, y: y, width: width, height: 9) - NSBezierPath(roundedRect: rect, xRadius: 3, yRadius: 3).fill() - y -= 15 - } - } -} diff --git a/app/Sources/MenuBarUI/ProviderListView.swift b/app/Sources/MenuBarUI/ProviderListView.swift deleted file mode 100644 index c0d5b6dc0b..0000000000 --- a/app/Sources/MenuBarUI/ProviderListView.swift +++ /dev/null @@ -1,255 +0,0 @@ -import AppKit -import MenuBarCore - -/// Collapsed provider list with per-provider enable/disable switches. -/// -/// Collapsed by default: reading status is frequent, toggling a provider is rare, and -/// the urgency order in `003` puts actions below information. -public final class ProviderListView: NSView { - private let disclosure = NSButton() - private let summary = makeLabel("", font: Theme.caption, color: Theme.muted) - private let rows = NSStackView() - private var expanded = false - private var snapshot: ProxySnapshot? - /// Providers with a write in flight, mapped to the state the USER chose. A poll can - /// still be carrying pre-write data, so the intended value — not the snapshot — is - /// what a rebuilt row must show. - private var pending: [String: Bool] = [:] - - /// `(provider, shouldDisable)`. - public var onToggle: ((String, Bool) -> Void)? - - public override init(frame: NSRect) { - super.init(frame: frame) - - disclosure.bezelStyle = .disclosure - disclosure.setButtonType(.onOff) - disclosure.title = "" - disclosure.target = self - disclosure.action = #selector(toggleExpanded) - disclosure.setAccessibilityLabel("Show providers") - - rows.orientation = .vertical - rows.alignment = .leading - rows.spacing = Theme.tightGap - rows.isHidden = true - - let header = NSStackView(views: [disclosure, summary]) - header.orientation = .horizontal - header.spacing = Theme.tightGap - header.alignment = .centerY - - let column = NSStackView(views: [header, rows]) - column.orientation = .vertical - column.alignment = .leading - column.spacing = Theme.tightGap - column.translatesAutoresizingMaskIntoConstraints = false - addSubview(column) - NSLayoutConstraint.activate([ - column.topAnchor.constraint(equalTo: topAnchor), - column.leadingAnchor.constraint(equalTo: leadingAnchor), - column.trailingAnchor.constraint(equalTo: trailingAnchor), - column.bottomAnchor.constraint(equalTo: bottomAnchor), - ]) - } - - public convenience init() { self.init(frame: .zero) } - - public required init?(coder: NSCoder) { nil } - - public func apply(_ snapshot: ProxySnapshot) { - self.snapshot = snapshot - - guard snapshot.providersLoaded else { - isHidden = true - return - } - isHidden = false - - if snapshot.providers.isEmpty { - summary.stringValue = "No providers configured." - disclosure.isHidden = true - rows.isHidden = true - return - } - - disclosure.isHidden = false - let enabled = snapshot.providers.filter(\.isEnabled).count - summary.stringValue = "\(enabled) of \(snapshot.providers.count) providers enabled" - rebuildRows(snapshot) - rows.isHidden = !expanded - } - - private func rebuildRows(_ snapshot: ProxySnapshot) { - for view in rows.arrangedSubviews { - rows.removeArrangedSubview(view) - view.removeFromSuperview() - } - - for provider in snapshot.visibleProviders.sorted(by: { $0.name < $1.name }) { - let isDefault = provider.name == snapshot.defaultProvider - let row = ProviderRowView( - provider: provider, - isDefault: isDefault - ) { [weak self] shouldDisable in - self?.onToggle?(provider.name, shouldDisable) - } - // A refresh that lands mid-write must not undo the optimistic state: apply - // the intended value first, then mark the row busy. - if let intended = pending[provider.name] { - row.setEnabled(intended) - row.setBusy(true) - } - row.translatesAutoresizingMaskIntoConstraints = false - rows.addArrangedSubview(row) - row.widthAnchor.constraint(equalTo: rows.widthAnchor).isActive = true - } - } - - /// Shared by the disclosure button and the test hook. - func setExpanded(_ value: Bool) { - expanded = value - disclosure.state = value ? .on : .off - rows.isHidden = !expanded - disclosure.setAccessibilityLabel(expanded ? "Hide providers" : "Show providers") - (window?.contentViewController as? PopoverViewController)?.refreshSize() - } - - var providerRows: [NSView] { rows.arrangedSubviews } - - @objc private func toggleExpanded() { - expanded = disclosure.state == .on - rows.isHidden = !expanded - disclosure.setAccessibilityLabel(expanded ? "Hide providers" : "Show providers") - // The popover has to grow or shrink with the disclosure. - (window?.contentViewController as? PopoverViewController)?.refreshSize() - } - - /// Reverts a switch after the proxy rejected the change. - public func revert(_ name: String, to enabled: Bool) { - pending[name] = nil - for case let row as ProviderRowView in rows.arrangedSubviews where row.providerName == name { - row.setEnabled(enabled) - row.setBusy(false) - } - } - - /// Marks a provider as having a write in flight. Its switch stays inert until the - /// authoritative refresh lands, so a poll cannot resurrect the pre-toggle state and - /// a second click cannot race the first. - /// `intended` is the state the user selected, retained so a poll landing mid-write - /// cannot snap the switch back. - public func setBusy(_ name: String, _ busy: Bool, intended: Bool? = nil) { - if busy { - pending[name] = intended ?? pending[name] ?? true - } else { - pending[name] = nil - } - for case let row as ProviderRowView in rows.arrangedSubviews where row.providerName == name { - if busy, let value = pending[name] { row.setEnabled(value) } - row.setBusy(busy) - } - } -} - -public final class ProviderRowView: NSView { - public let providerName: String - private let toggle = NSSwitch() - private let onToggle: (Bool) -> Void - private var baseEnabled = true - private var isBusy = false - - init(provider: ProviderSummary, isDefault: Bool, onToggle: @escaping (Bool) -> Void) { - self.providerName = provider.name - self.onToggle = onToggle - super.init(frame: .zero) - - let name = makeLabel(provider.name, font: Theme.caption, color: Theme.text) - let detail = makeLabel( - isDefault ? "default" : (provider.authMode ?? ""), - font: Theme.micro, - color: Theme.faint - ) - - let labels = NSStackView(views: [name, detail]) - labels.orientation = .vertical - labels.alignment = .leading - labels.spacing = 0 - - toggle.state = provider.isEnabled ? .on : .off - toggle.controlSize = .mini - toggle.target = self - toggle.action = #selector(switched) - - // The proxy rejects only DISABLING the default provider (`provider-routes.ts:178` - // guards on `rawBody.disabled && name === defaultProvider`). Enabling it is - // valid, so a default provider that is currently off must stay toggleable — - // otherwise the app strands the user in a state it cannot leave. - let wouldDisableDefault = isDefault && provider.isEnabled - toggle.isEnabled = !wouldDisableDefault - toggle.toolTip = wouldDisableDefault - ? "This is the default provider. Choose another default in the dashboard first." - : nil - baseEnabled = toggle.isEnabled - toggle.setAccessibilityLabel("\(provider.name) enabled") - - let row = NSStackView(views: [labels, NSView(), toggle]) - row.orientation = .horizontal - row.spacing = Theme.rowGap - row.alignment = .centerY - row.translatesAutoresizingMaskIntoConstraints = false - addSubview(row) - NSLayoutConstraint.activate([ - row.topAnchor.constraint(equalTo: topAnchor), - row.leadingAnchor.constraint(equalTo: leadingAnchor), - row.trailingAnchor.constraint(equalTo: trailingAnchor), - row.bottomAnchor.constraint(equalTo: bottomAnchor), - ]) - } - - required init?(coder: NSCoder) { nil } - - func setEnabled(_ enabled: Bool) { toggle.state = enabled ? .on : .off } - - var toggleState: Bool { toggle.state == .on } - var toggleIsEnabled: Bool { toggle.isEnabled } - - /// Inert while its write is in flight, so a second click cannot race the first. - func setBusy(_ busy: Bool) { - isBusy = busy - toggle.isEnabled = busy ? false : baseEnabled - alphaValue = busy ? 0.6 : 1 - } - - @objc private func switched() { - // Optimistic: the switch has already moved. The caller reverts on failure. - onToggle(toggle.state == .off) - } -} - - -// MARK: - Test inspection - -/// Read-only hooks so the UI suite can assert on rendered control state rather than on -/// the view's private bookkeeping. -package extension ProviderListView { - /// Expands the list without going through a click, so tests do not depend on - /// NSButton action dispatch. - func expandForTesting() { setExpanded(true) } - - func isToggleOn(_ name: String) -> Bool? { row(name)?.isOn } - func isToggleEnabled(_ name: String) -> Bool? { row(name)?.isToggleEnabled } - func hasProviderForTesting(_ name: String) -> Bool { row(name) != nil } - - private func row(_ name: String) -> ProviderRowView? { - for case let row as ProviderRowView in providerRows where row.providerName == name { - return row - } - return nil - } -} - -package extension ProviderRowView { - var isOn: Bool { toggleState } - var isToggleEnabled: Bool { toggleIsEnabled } -} diff --git a/app/Sources/MenuBarUI/StatusIcon.swift b/app/Sources/MenuBarUI/StatusIcon.swift deleted file mode 100644 index e7eb2951b1..0000000000 --- a/app/Sources/MenuBarUI/StatusIcon.swift +++ /dev/null @@ -1,73 +0,0 @@ -import AppKit -import MenuBarCore - -/// The menu bar glyph. -/// -/// Drawn as vector paths rather than shipped as PNGs, so it stays crisp at every scale -/// factor and inverts correctly as a template image. -/// -/// Colour is deliberately absent here. macOS menu bar items are monochrome by -/// convention, and a coloured dot up there is the tell of an app that does not respect -/// the platform. State is carried by fill and by a notch instead. The coloured dot lives -/// inside the popover, where it sits beside a word and so never encodes meaning by -/// colour alone. -public enum StatusIcon { - public static let size = NSSize(width: 17, height: 17) - - public static func image(for state: ProxyState) -> NSImage { - switch state { - case .running(let health) where health.isProtected: - return mark(filled: true, notched: false, alpha: 1) - case .running: - return mark(filled: true, notched: true, alpha: 1) - case .loading, .degraded: - return mark(filled: false, notched: false, alpha: 1) - case .unreachable, .unauthorized: - return mark(filled: false, notched: false, alpha: 0.4) - } - } - - /// A rounded mark reduced to menu bar scale. - /// - /// The notch is carved out of the geometry with an even-odd path rather than by - /// compositing. An earlier version stroked with `.clear` and `.clear` composite mode, - /// which silently did nothing — the rendered at-risk glyph was indistinguishable from - /// the protected one, so the state signal was invisible. - private static func mark(filled: Bool, notched: Bool, alpha: CGFloat) -> NSImage { - let image = NSImage(size: size, flipped: false) { rect in - let inset = rect.insetBy(dx: 2.5, dy: 2.5) - let path = NSBezierPath(roundedRect: inset, xRadius: 4, yRadius: 4) - - if notched { - // A slot carved out of the trailing edge, kept fully inside the mark so - // the silhouette stays clean. Even-odd winding turns the subpath into a - // hole rather than a second filled shape. - let notch = NSBezierPath( - roundedRect: NSRect( - x: inset.maxX - 4.2, - y: inset.midY - 1.1, - width: 3.0, - height: 2.2 - ), - xRadius: 1.1, - yRadius: 1.1 - ) - path.append(notch) - path.windingRule = .evenOdd - } - - NSColor.black.withAlphaComponent(alpha).setStroke() - NSColor.black.withAlphaComponent(alpha).setFill() - - if filled { - path.fill() - } else { - path.lineWidth = 1.6 - path.stroke() - } - return true - } - image.isTemplate = true - return image - } -} diff --git a/app/Sources/MenuBarUI/Theme.swift b/app/Sources/MenuBarUI/Theme.swift deleted file mode 100644 index b1e8b4241f..0000000000 --- a/app/Sources/MenuBarUI/Theme.swift +++ /dev/null @@ -1,103 +0,0 @@ -import AppKit - -/// Tokens derived from `gui/src/styles.css` so the companion and the dashboard agree on -/// what "healthy" looks like. -/// -/// For SURFACES, AppKit's semantic colours win over a hardcoded hex: they track -/// light/dark plus the increased-contrast and vibrancy accessibility settings, which a -/// literal cannot. -/// -/// The TEXT tiers are a deliberate exception. Measured against the popover material, -/// `tertiaryLabelColor` renders at 2.01:1 in light and 2.39:1 in dark — it is designed -/// for disabled affordances, not for information the user has to read. All four text and -/// mark tokens below are therefore calibrated against the rendered material and verified -/// numerically rather than trusted by name. -enum Theme { - // Surfaces - static let separator = NSColor.separatorColor - static let raised = NSColor.controlBackgroundColor - - // Text: --text / --muted / --faint - // - // `tertiaryLabelColor` measured 2.01:1 in light and 2.39:1 in dark against the - // popover material — well under the 4.5:1 required for normal text. AppKit's - // tertiary tier is intended for disabled affordances, not for information the user - // has to read, and every label using this tier here (range heading, metric captions, - // quota window labels) carries real meaning. Calibrated tokens replace it. - /// All three text tiers are calibrated against the RENDERED popover material, not - /// picked from AppKit's semantic palette. Measured backgrounds: light (220,219,218), - /// dark (102,101,101). - /// - /// The dark material constrains this hard — pure white measures only 5.81:1 against - /// it — so the tiers are packed into the band that remains while keeping every text - /// tier above 4.5:1 and preserving `text > muted > faint` in both appearances. - static let text = dynamic(light: 0x1A1A1A, dark: 0xFFFFFF) - static let muted = dynamic(light: 0x3D3D3D, dark: 0xF2F2F2) - /// Small supporting text that must still be legible: 10-11pt captions and labels. - static let faint = dynamic(light: 0x545454, dark: 0xEDEDED) - /// Graphical marks only, held to the 3:1 non-text threshold rather than 4.5:1. - static let graphMark = dynamic(light: 0x707070, dark: 0xD2D2D2) - - // State colours, taken verbatim from styles.css. - static let green = dynamic(light: 0x0A7D5C, dark: 0x4ECB9D) - static let amber = dynamic(light: 0x9A4A08, dark: 0xFBBF24) - static let red = dynamic(light: 0xB91C1C, dark: 0xF87171) - - // Type ladder: --text-micro / --text-caption / --text-label / --text-control. - static let micro = NSFont.systemFont(ofSize: 10, weight: .medium) - static let caption = NSFont.systemFont(ofSize: 11) - static let label = NSFont.systemFont(ofSize: 12, weight: .semibold) - /// Monospaced digits are the AppKit equivalent of `font-variant-numeric: tabular-nums`. - /// Without this, polling makes every digit jitter. - static let numeric = NSFont.monospacedDigitSystemFont(ofSize: 13, weight: .medium) - static let numericSmall = NSFont.monospacedDigitSystemFont(ofSize: 11, weight: .regular) - - // Geometry: --space-* and --radius-sm. - static let gutter: CGFloat = 12 - static let rowGap: CGFloat = 8 - static let tightGap: CGFloat = 4 - static let radius: CGFloat = 8 - static let width: CGFloat = 340 - - static func color(for tone: ProxyToneBridge) -> NSColor { - switch tone { - case .neutral: return muted - case .good: return green - case .warning: return amber - case .bad: return red - } - } - - /// Quota fill: green under 80, amber to 95, red above. The percentage is always - /// printed beside the bar, so colour is reinforcement rather than the only signal. - static func quotaColor(percent: Double?) -> NSColor { - guard let percent else { return faint } - if percent > 95 { return red } - if percent >= 80 { return amber } - return green - } - - /// `light-dark()` equivalent: resolves per appearance instead of at creation time. - private static func dynamic(light: Int, dark: Int) -> NSColor { - NSColor(name: nil) { appearance in - let isDark = appearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua - return NSColor(hex: isDark ? dark : light) - } - } -} - -/// Mirrors `ProxyState.Tone` without importing AppKit into the core module. -enum ProxyToneBridge { - case neutral, good, warning, bad -} - -extension NSColor { - convenience init(hex: Int) { - self.init( - srgbRed: CGFloat((hex >> 16) & 0xFF) / 255, - green: CGFloat((hex >> 8) & 0xFF) / 255, - blue: CGFloat(hex & 0xFF) / 255, - alpha: 1 - ) - } -} diff --git a/app/Sources/MenuBarUI/TimelineChartView.swift b/app/Sources/MenuBarUI/TimelineChartView.swift deleted file mode 100644 index 56a8a6288f..0000000000 --- a/app/Sources/MenuBarUI/TimelineChartView.swift +++ /dev/null @@ -1,135 +0,0 @@ -import AppKit -import MenuBarCore - -public final class TimelineChartView: NSView { - private var timeline: UsageTimeline? - private var settings = CompanionSettings.defaults - private let colors = [0x0A84FF, 0xFF9F0A, 0x30D158, 0xBF5AF2, 0xFF453A, 0x64D2FF] - - public override var intrinsicContentSize: NSSize { - NSSize(width: NSView.noIntrinsicMetric, height: 104) - } - - public func apply(_ snapshot: ProxySnapshot) { - settings = snapshot.settings - timeline = snapshot.timeline - isHidden = !settings.showChart || timeline == nil - setAccessibilityLabel("Usage timeline") - needsDisplay = true - } - - public override func draw(_ dirtyRect: NSRect) { - guard let timeline, !timeline.isEmpty else { - if settings.showChart { - drawText("No token usage in this window.", in: NSRect(x: 0, y: 36, width: bounds.width, height: 16), font: Theme.caption, color: Theme.muted) - } - return - } - let chartHeight: CGFloat = 72 - let maxValue = settings.chartStyle == .stackedBar ? timeline.stackedMax : timeline.maxPoint - drawText(Format.tokens(Int(maxValue.rounded())), in: NSRect(x: 0, y: chartHeight + 8, width: bounds.width, height: 14), font: Theme.micro, color: Theme.muted, alignment: .right) - let window = timeline.buckets * timeline.bucketSeconds / 3600 - let windowLabel: String - if window < 48 { - windowLabel = "\(window)h" - } else { - windowLabel = "\(window / 24)d" - } - drawText(windowLabel, in: NSRect(x: 0, y: chartHeight + 8, width: 40, height: 14), font: Theme.micro, color: Theme.muted) - - let plot = NSRect(x: 0, y: 20, width: bounds.width, height: chartHeight) - Theme.muted.setStroke() - let baseline = NSBezierPath() - baseline.move(to: NSPoint(x: plot.minX, y: plot.minY)) - baseline.line(to: NSPoint(x: plot.maxX, y: plot.minY)) - baseline.lineWidth = 0.5 - baseline.stroke() - - if settings.chartStyle == .stackedBar { - drawBars(timeline, in: plot, maxValue: maxValue) - } else { - drawLines(timeline, in: plot, maxValue: maxValue) - } - - drawLegend(timeline, in: NSRect(x: 0, y: 0, width: bounds.width, height: 14)) - } - - private func drawText( - _ text: String, in rect: NSRect, font: NSFont, color: NSColor, alignment: NSTextAlignment = .left - ) { - let style = NSMutableParagraphStyle() - style.alignment = alignment - NSAttributedString( - string: text, - attributes: [.font: font, .foregroundColor: color, .paragraphStyle: style] - ).draw(in: rect) - } - - private func drawLines(_ timeline: UsageTimeline, in plot: NSRect, maxValue: Double) { - guard timeline.buckets > 1, maxValue > 0 else { return } - for (seriesIndex, series) in timeline.series.enumerated() { - let path = NSBezierPath() - for (index, value) in series.points.enumerated() { - let x = plot.minX + plot.width * CGFloat(index) / CGFloat(max(timeline.buckets - 1, 1)) - let y = plot.minY + plot.height * CGFloat(value / maxValue) - if index == 0 { path.move(to: NSPoint(x: x, y: y)) } else { path.line(to: NSPoint(x: x, y: y)) } - } - NSColor(hex: colors[seriesIndex % colors.count]).setStroke() - path.lineWidth = 1.5 - path.stroke() - } - } - - private func drawBars(_ timeline: UsageTimeline, in plot: NSRect, maxValue: Double) { - guard timeline.buckets > 0, maxValue > 0 else { return } - let width = max(1, plot.width / CGFloat(timeline.buckets) - 1) - for bucket in 0.. 0 { - let extra = entries.count - visible - let suffixWidth = extra > 0 - ? NSAttributedString(string: "+\(extra) more", attributes: attributes).size().width + separator - : 0 - let entryWidth = entries.prefix(visible).reduce(CGFloat.zero) { width, entry in - width + dotSize + 4 + entry.1.size().width + separator - } - if entryWidth + suffixWidth <= rect.width || visible == 0 { break } - visible -= 1 - } - let extra = entries.count - visible - var x = rect.minX - for (index, text) in entries.prefix(visible) { - let dot = NSRect(x: x, y: rect.midY - dotSize / 2, width: dotSize, height: dotSize) - NSColor(hex: colors[index % colors.count]).setFill() - NSBezierPath(ovalIn: dot).fill() - x += dotSize + 4 - text.draw(at: NSPoint(x: x, y: rect.minY)) - x += text.size().width + separator - } - if extra > 0 { - NSAttributedString(string: "+\(extra) more", attributes: attributes) - .draw(at: NSPoint(x: x, y: rect.minY)) - } - } -} diff --git a/app/Sources/MenuBarUI/Views.swift b/app/Sources/MenuBarUI/Views.swift deleted file mode 100644 index 70c229b685..0000000000 --- a/app/Sources/MenuBarUI/Views.swift +++ /dev/null @@ -1,264 +0,0 @@ -import AppKit -import MenuBarCore - -// MARK: - Shared helpers - -func makeLabel(_ text: String, font: NSFont, color: NSColor) -> NSTextField { - let field = NSTextField(labelWithString: text) - field.font = font - field.textColor = color - field.lineBreakMode = .byTruncatingTail - return field -} - -func makeRow(_ views: [NSView], spacing: CGFloat = Theme.rowGap) -> NSStackView { - let stack = NSStackView(views: views) - stack.orientation = .horizontal - stack.spacing = spacing - stack.alignment = .firstBaseline - return stack -} - -func makeSeparator() -> NSView { - let line = NSView() - line.wantsLayer = true - line.layer?.backgroundColor = Theme.separator.cgColor - line.translatesAutoresizingMaskIntoConstraints = false - line.heightAnchor.constraint(equalToConstant: 1).isActive = true - return line -} - -// MARK: - Status header - -/// `● Running 127.0.0.1:10100` -/// -/// The dot never travels alone: the word beside it carries the same meaning, so the UI -/// stays readable without colour perception (WCAG 1.4.1). -final class StatusHeaderView: NSView { - private let dot = StatusDotView() - private let title = makeLabel("", font: Theme.label, color: Theme.text) - private let endpoint = makeLabel("", font: Theme.caption, color: Theme.muted) - private let detail = makeLabel("", font: Theme.caption, color: Theme.muted) - - init() { - super.init(frame: .zero) - let top = makeRow([dot, title, NSView(), endpoint], spacing: Theme.rowGap) - top.alignment = .centerY - top.distribution = .fill - endpoint.setContentHuggingPriority(.defaultHigh, for: .horizontal) - - let stack = NSStackView(views: [top, detail]) - stack.orientation = .vertical - stack.alignment = .leading - stack.spacing = 2 - stack.translatesAutoresizingMaskIntoConstraints = false - addSubview(stack) - NSLayoutConstraint.activate([ - stack.topAnchor.constraint(equalTo: topAnchor), - stack.leadingAnchor.constraint(equalTo: leadingAnchor), - stack.trailingAnchor.constraint(equalTo: trailingAnchor), - stack.bottomAnchor.constraint(equalTo: bottomAnchor), - ]) - } - - required init?(coder: NSCoder) { nil } - - func apply(_ snapshot: ProxySnapshot) { - let state = snapshot.state - title.stringValue = state.title - endpoint.stringValue = snapshot.endpoint.display - dot.tone = bridge(state.tone) - - if let text = state.detail { - detail.stringValue = text - detail.isHidden = false - } else { - detail.isHidden = true - } - - setAccessibilityLabel("Proxy \(state.title) at \(snapshot.endpoint.display)") - } - - private func bridge(_ tone: ProxyState.Tone) -> ProxyToneBridge { - switch tone { - case .neutral: return .neutral - case .good: return .good - case .warning: return .warning - case .bad: return .bad - } - } -} - -final class StatusDotView: NSView { - var tone: ProxyToneBridge = .neutral { - didSet { needsDisplay = true } - } - - override var intrinsicContentSize: NSSize { NSSize(width: 8, height: 8) } - - override func draw(_ dirtyRect: NSRect) { - let rect = NSRect(x: 0, y: (bounds.height - 8) / 2, width: 8, height: 8) - Theme.color(for: tone).setFill() - NSBezierPath(ovalIn: rect).fill() - } -} - -// MARK: - Metrics - -/// Three columns plus a range header that echoes the response, never the request. -final class MetricsView: NSView { - private let rangeLabel = makeLabel("USAGE", font: Theme.micro, color: Theme.faint) - private let columns: [(caption: NSTextField, value: NSTextField)] - private let emptyLabel = makeLabel("", font: Theme.caption, color: Theme.muted) - private let stack: NSStackView - private let columnsRow: NSStackView - - init() { - let captions = ["TOKENS", "REQUESTS", "COST"] - columns = captions.map { caption in - (makeLabel(caption, font: Theme.micro, color: Theme.faint), - makeLabel(Format.unknown, font: Theme.numeric, color: Theme.text)) - } - - let columnViews: [NSView] = columns.map { pair in - let column = NSStackView(views: [pair.caption, pair.value]) - column.orientation = .vertical - column.alignment = .leading - column.spacing = 1 - return column - } - columnsRow = NSStackView(views: columnViews) - columnsRow.orientation = .horizontal - columnsRow.distribution = .fillEqually - columnsRow.alignment = .top - - stack = NSStackView(views: [rangeLabel, columnsRow, emptyLabel]) - stack.orientation = .vertical - stack.alignment = .leading - stack.spacing = Theme.tightGap - - super.init(frame: .zero) - stack.translatesAutoresizingMaskIntoConstraints = false - addSubview(stack) - NSLayoutConstraint.activate([ - stack.topAnchor.constraint(equalTo: topAnchor), - stack.leadingAnchor.constraint(equalTo: leadingAnchor), - stack.trailingAnchor.constraint(equalTo: trailingAnchor), - stack.bottomAnchor.constraint(equalTo: bottomAnchor), - ]) - } - - required init?(coder: NSCoder) { nil } - - func apply(_ snapshot: ProxySnapshot) { - let usage = snapshot.today ?? snapshot.usage - isHidden = !snapshot.settings.showToday - rangeLabel.stringValue = usage?.rangeLabel ?? "USAGE" - columnsRow.arrangedSubviews[2].isHidden = !snapshot.settings.showCost - - // Three states: known-empty gets copy, unknown gets em dashes, data gets values. - switch snapshot.usageIsEmpty { - case .some(true): - columnsRow.isHidden = true - emptyLabel.isHidden = false - emptyLabel.stringValue = "No requests in this period." - default: - columnsRow.isHidden = false - emptyLabel.isHidden = true - let summary = usage?.summary - let requests = Format.count(summary?.requests) - columns[0].value.stringValue = Format.tokens(summary?.totalTokens) - columns[1].value.stringValue = (summary?.hasEstimates ?? false) ? requests + "~" : requests - columns[2].value.stringValue = Format.cost(summary?.estimatedCostUsd) - columns[0].value.setAccessibilityLabel( - "\(Format.tokens(summary?.totalTokens)) tokens" - ) - columns[1].value.setAccessibilityLabel( - (summary?.hasEstimates ?? false) - ? "\(requests) requests, partly estimated" - : "\(requests) requests" - ) - columns[2].value.setAccessibilityLabel( - "\(Format.cost(summary?.estimatedCostUsd)) estimated cost" - ) - } - } -} - -// MARK: - Quotas - -/// `OpenAI ▓▓▓▓▓░░░░░ 44%` -final class QuotaRowView: NSView { - init(quota: NormalizedQuota) { - super.init(frame: .zero) - - let name = makeLabel(quota.providerLabel, font: Theme.caption, color: Theme.text) - name.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - name.lineBreakMode = .byTruncatingTail - - // Which window a number belongs to is not decoration: 42% of an API-usage window - // and 42% of a month mean very different things. - let window = makeLabel( - quota.hasPercent ? quota.windowLabel : "", - font: Theme.micro, color: Theme.faint - ) - - let labels = NSStackView(views: [name, window]) - labels.orientation = .vertical - labels.alignment = .leading - labels.spacing = 0 - - let bar = QuotaBarView() - bar.percent = quota.percent - - let value = makeLabel(Format.percent(quota.percent), font: Theme.numericSmall, color: Theme.muted) - value.alignment = .right - - let row = NSStackView(views: [labels, bar, value]) - row.orientation = .horizontal - row.spacing = Theme.rowGap - row.alignment = .centerY - row.translatesAutoresizingMaskIntoConstraints = false - addSubview(row) - - NSLayoutConstraint.activate([ - row.topAnchor.constraint(equalTo: topAnchor), - row.leadingAnchor.constraint(equalTo: leadingAnchor), - row.trailingAnchor.constraint(equalTo: trailingAnchor), - row.bottomAnchor.constraint(equalTo: bottomAnchor), - labels.widthAnchor.constraint(equalToConstant: 132), - value.widthAnchor.constraint(equalToConstant: 36), - ]) - - // The percentage is spoken, not merely drawn as a filled width. - let reset = Format.resetsIn(quota.resetAt) - setAccessibilityLabel( - quota.hasPercent - ? "\(quota.providerLabel): \(Format.percent(quota.percent)) of \(quota.windowLabel) quota, resets in \(reset)" - : "\(quota.providerLabel): quota unknown" - ) - } - - required init?(coder: NSCoder) { nil } -} - -final class QuotaBarView: NSView { - var percent: Double? - - override var intrinsicContentSize: NSSize { NSSize(width: 110, height: 6) } - - override func draw(_ dirtyRect: NSRect) { - let track = NSRect(x: 0, y: (bounds.height - 6) / 2, width: bounds.width, height: 6) - Theme.raised.setFill() - NSBezierPath(roundedRect: track, xRadius: 3, yRadius: 3).fill() - - // A nil percent draws no fill at all — a zero-width bar would read as "0% used", - // which is a different fact from "unknown". - guard let percent else { return } - let clamped = max(0, min(100, percent)) - guard clamped > 0 else { return } - let fill = NSRect(x: 0, y: track.origin.y, width: track.width * CGFloat(clamped / 100), height: 6) - Theme.quotaColor(percent: percent).setFill() - NSBezierPath(roundedRect: fill, xRadius: 3, yRadius: 3).fill() - } -} diff --git a/app/Sources/MenuBarUITests/Harness.swift b/app/Sources/MenuBarUITests/Harness.swift deleted file mode 100644 index 0deb1d6ae4..0000000000 --- a/app/Sources/MenuBarUITests/Harness.swift +++ /dev/null @@ -1,105 +0,0 @@ -import Foundation - -/// A dependency-free assertion harness. -/// -/// Why not XCTest or swift-testing: neither ships a usable runtime in Xcode Command Line -/// Tools. `import XCTest` fails module resolution outright, and swift-testing compiles -/// but cannot `dlopen` `Testing.framework` at run time. Requiring a full Xcode install to -/// run the unit tests of a menu bar companion would put the tests out of reach for most -/// contributors and for any CI runner without Xcode selected. -/// -/// This harness is ~60 lines, runs as a plain executable, and prints TAP-ish output that -/// both a human and CI can read. If the package ever gains a full-Xcode requirement for -/// other reasons, migrating these cases to swift-testing is mechanical. -public struct TestFailure { - let test: String - let message: String - let file: String - let line: Int -} - -public final class TestRunner { - private(set) var passed = 0 - private(set) var failures: [TestFailure] = [] - private var current = "" - - public init() {} - - public func test(_ name: String, _ body: () throws -> Void) { - current = name - let failuresBefore = failures.count - do { - try body() - } catch { - failures.append(TestFailure(test: name, message: "threw \(error)", file: #file, line: #line)) - print("FAIL — \(name): threw \(error)") - return - } - // A case that recorded an expectation failure is not a pass, even though its - // body returned normally. - if failures.count == failuresBefore { - passed += 1 - print("ok — \(name)") - } - } - - public func expect( - _ condition: Bool, - _ message: @autoclosure () -> String, - file: String = #file, - line: Int = #line - ) { - guard !condition else { return } - let failure = TestFailure(test: current, message: message(), file: file, line: line) - failures.append(failure) - print("FAIL — \(current): \(failure.message) (\(URL(fileURLWithPath: file).lastPathComponent):\(line))") - } - - public func equal( - _ actual: T, - _ expected: T, - _ label: String = "", - file: String = #file, - line: Int = #line - ) { - expect( - actual == expected, - "\(label.isEmpty ? "" : label + ": ")expected \(expected), got \(actual)", - file: file, - line: line - ) - } - - public func notNil( - _ value: T?, - _ label: String, - file: String = #file, - line: Int = #line - ) -> T? { - expect(value != nil, "\(label) should not be nil", file: file, line: line) - return value - } - - public func isNil( - _ value: T?, - _ label: String, - file: String = #file, - line: Int = #line - ) { - expect(value == nil, "\(label) should be nil, got \(String(describing: value))", file: file, line: line) - } - - /// Prints the summary and returns the process exit code. - public func summarize() -> Int32 { - print("") - if failures.isEmpty { - print("\(passed) passed, 0 failed") - return 0 - } - print("\(passed) passed, \(failures.count) FAILED") - for failure in failures { - print(" - \(failure.test): \(failure.message)") - } - return 1 - } -} diff --git a/app/Sources/MenuBarUITests/main.swift b/app/Sources/MenuBarUITests/main.swift deleted file mode 100644 index daf7e872b2..0000000000 --- a/app/Sources/MenuBarUITests/main.swift +++ /dev/null @@ -1,165 +0,0 @@ -import AppKit -import MenuBarCore -import MenuBarUI - -// UI-layer tests. Separate from MenuBarCoreTests because these need AppKit and an -// NSApplication; the core suite deliberately has no UI dependency. -// -// These cover the Phase 3 behaviours that were defects in earlier review rounds: -// optimistic rollback, pending state surviving a poll, and the direction-sensitive -// default-provider guard. - -let app = NSApplication.shared -app.setActivationPolicy(.prohibited) - -let runner = TestRunner() - -func provider(_ name: String, enabled: Bool = true) -> ProviderSummary { - let json = #"{"name":"\#(name)","disabled":\#(enabled ? "false" : "true")}"# - return try! JSONDecoder().decode(ProviderSummary.self, from: Data(json.utf8)) -} - -func snapshot( - providers: [ProviderSummary], - defaultProvider: String? = "openai" -) -> ProxySnapshot { - ProxySnapshot( - state: .running(StartupHealth(status: "protected")), - endpoint: .default, - providers: providers, - defaultProvider: defaultProvider, - lastUpdated: Date(), - providersLoaded: true - ) -} - -// MARK: - Default-provider guard direction - -runner.test("ui: an enabled default provider cannot be switched off") { - let list = ProviderListView() - list.apply(snapshot(providers: [provider("openai"), provider("anthropic")])) - list.expandForTesting() - - runner.equal(list.isToggleEnabled("openai"), false, "enabled default is inert") - runner.equal(list.isToggleEnabled("anthropic"), true, "non-default is toggleable") -} - -// The proxy guard is `disabled && name === defaultProvider`, so ENABLING the default is -// valid. Making the control inert whenever isDefault stranded the user. -runner.test("ui: a disabled default provider can still be switched back on") { - let list = ProviderListView() - list.apply(snapshot(providers: [provider("openai", enabled: false)])) - list.expandForTesting() - - runner.equal(list.isToggleEnabled("openai"), true, "disabled default must be recoverable") -} - -// MARK: - Optimistic update and rollback - -runner.test("ui: a rejected write restores the switch it moved") { - let list = ProviderListView() - list.apply(snapshot(providers: [provider("anthropic")])) - list.expandForTesting() - - // User switches it off; the write is in flight. - list.setBusy("anthropic", true, intended: false) - runner.equal(list.isToggleOn("anthropic"), false, "optimistic state applied") - runner.equal(list.isToggleEnabled("anthropic"), false, "inert while in flight") - - // The proxy rejects it. - list.revert("anthropic", to: true) - runner.equal(list.isToggleOn("anthropic"), true, "reverted to the server's value") - runner.equal(list.isToggleEnabled("anthropic"), true, "interactive again") -} - -runner.test("ui: a successful write clears busy without reverting") { - let list = ProviderListView() - list.apply(snapshot(providers: [provider("anthropic")])) - list.expandForTesting() - - list.setBusy("anthropic", true, intended: false) - // The authoritative refresh now reports it disabled. - list.apply(snapshot(providers: [provider("anthropic", enabled: false)])) - list.setBusy("anthropic", false) - - runner.equal(list.isToggleOn("anthropic"), false, "server state retained") - runner.equal(list.isToggleEnabled("anthropic"), true, "interactive again") -} - -// MARK: - Pending state versus a stale poll - -// This is the defect a reviewer caught: rebuildRows initialised each switch from the -// snapshot, so a poll carrying pre-write data snapped the switch back mid-write. -runner.test("ui: a stale poll cannot undo an in-flight optimistic change") { - let list = ProviderListView() - list.apply(snapshot(providers: [provider("anthropic")])) - list.expandForTesting() - - list.setBusy("anthropic", true, intended: false) - runner.equal(list.isToggleOn("anthropic"), false, "optimistic state applied") - - // A poll that started before the write lands, still reporting the old value. - list.apply(snapshot(providers: [provider("anthropic", enabled: true)])) - - runner.equal(list.isToggleOn("anthropic"), false, "stale poll must not snap it back") - runner.equal(list.isToggleEnabled("anthropic"), false, "still inert while in flight") -} - -runner.test("ui: pending state is per provider and does not leak") { - let list = ProviderListView() - list.apply(snapshot(providers: [provider("anthropic"), provider("xai")])) - list.expandForTesting() - - list.setBusy("anthropic", true, intended: false) - runner.equal(list.isToggleEnabled("anthropic"), false, "target is inert") - runner.equal(list.isToggleEnabled("xai"), true, "sibling is unaffected") - runner.equal(list.isToggleOn("xai"), true, "sibling keeps its value") -} - -// MARK: - Empty and unloaded states - -runner.test("ui: providers are hidden until they have actually been read") { - let list = ProviderListView() - var unloaded = snapshot(providers: []) - unloaded.providersLoaded = false - list.apply(unloaded) - runner.equal(list.isHidden, true, "not fetched yet is not the same as none") - - list.apply(snapshot(providers: [])) - runner.equal(list.isHidden, false, "an empty result renders its own copy") -} - -runner.test("ui: hidden providers do not create rows") { - let list = ProviderListView() - var current = snapshot(providers: [provider("openai"), provider("anthropic")]) - current.settings = CompanionSettings(hiddenProviders: ["openai"]) - list.apply(current) - list.expandForTesting() - runner.equal(list.hasProviderForTesting("openai"), false) - runner.equal(list.hasProviderForTesting("anthropic"), true) -} - -runner.test("ui: chart setting hides the timeline view") { - let chart = TimelineChartView() - var current = snapshot(providers: []) - current.timeline = try! JSONDecoder().decode( - UsageTimeline.self, - from: Data(#"{"start":0,"end":1,"bucketSeconds":1,"buckets":1,"metric":"total","aggregation":"sum","grouping":"model","series":[],"availableModels":[],"missingMeasurements":0}"#.utf8) - ) - current.settings = CompanionSettings(showChart: false) - chart.apply(current) - runner.equal(chart.isHidden, true) -} - -runner.test("ui: menu title renders from a companion template") { - let report = try! JSONDecoder().decode( - UsageReport.self, - from: Data(#"{"range":"today","summary":{"requests":3}}"#.utf8) - ) - var current = snapshot(providers: []) - current.today = report - current.settings = CompanionSettings(menuBarTemplate: "req {requests}") - runner.equal(current.menuBarTitle, "req 3") -} - -exit(runner.summarize()) diff --git a/app/Sources/OpenCodexWidget/Views.swift b/app/Sources/OpenCodexWidget/Views.swift index b130d1fda5..edc91171f4 100644 --- a/app/Sources/OpenCodexWidget/Views.swift +++ b/app/Sources/OpenCodexWidget/Views.swift @@ -293,8 +293,8 @@ struct OpenCodexWidgetView: View { Image(systemName: failure == .missing ? "rectangle.on.rectangle" : "exclamationmark.triangle") .font(.title2) Text(failure == .missing - ? "Open the OpenCodex menu bar app to start sharing usage." - : "Snapshot unreadable — refresh from the menu bar app.") + ? "Open the OpenCodex desktop app to start sharing usage." + : "Snapshot unreadable — refresh from the desktop app.") .font(.caption) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) diff --git a/app/Sources/UIProbe/main.swift b/app/Sources/UIProbe/main.swift deleted file mode 100644 index 02eca1592b..0000000000 --- a/app/Sources/UIProbe/main.swift +++ /dev/null @@ -1,165 +0,0 @@ -// Visual-QA harness (not shipped). -// -// Presents the real PopoverPanel over a deliberately loud backdrop and captures it with -// CGWindowListCreateImage, so every UI state can be inspected without depending on free -// menu bar space. -// -// Two harness decisions are load-bearing, both learned the hard way: -// * Present through the REAL panel. An earlier version used a plain NSWindow, which -// supplied its own background and hid the fact that the panel had none at all. -// * Capture through the window server. cacheDisplay(in:to:) skips text rendering and -// produced screenshots with no labels. -// -// PROBE_STATE: live | stopped | unauthorized | loading | degraded | empty | overflow -// PROBE_TAG: output filename suffix -// PROBE_APPEARANCE: light | dark (forces appearance without touching system settings) - -import AppKit -import MenuBarCore -import MenuBarUI - -// Presents the real PopoverPanel over a contrasting backdrop and captures it through the -// window server, so the UI can be inspected without depending on menu bar space. -final class ProbeDelegate: NSObject, NSApplicationDelegate { - let controller = PopoverViewController() - var window: NSWindow? - - func applicationDidFinishLaunching(_ n: Notification) { - // Force an appearance for contrast measurement without touching system settings. - if let name = ProcessInfo.processInfo.environment["PROBE_APPEARANCE"] { - NSApp.appearance = NSAppearance(named: name == "dark" ? .darkAqua : .aqua) - } - let endpoint = ProxyDiscovery.resolve() - let client = ProxyClient(endpoint: endpoint) - let coordinator = PollingCoordinator(client: client, endpoint: endpoint) - - // A loud backdrop first: if the panel has no surface of its own, this shows - // straight through and the defect is unmissable. - let backdrop = NSWindow( - contentRect: NSRect(x: 0, y: 0, width: 520, height: 620), - styleMask: [.titled], backing: .buffered, defer: false) - backdrop.title = "backdrop" - let strip = NSView(frame: NSRect(x: 0, y: 0, width: 520, height: 620)) - strip.wantsLayer = true - strip.layer?.backgroundColor = NSColor.systemRed.cgColor - for i in 0..<14 { - let bar = NSView(frame: NSRect(x: 0, y: CGFloat(i) * 44, width: 520, height: 22)) - bar.wantsLayer = true - bar.layer?.backgroundColor = NSColor.systemYellow.cgColor - strip.addSubview(bar) - } - backdrop.contentView = strip - backdrop.center() - backdrop.makeKeyAndOrderFront(nil) - - // Present through the real panel so its surface (or absence of one) is captured. - let realPanel = PopoverPanel() - realPanel.contentViewController = controller - controller.view.layoutSubtreeIfNeeded() - let size = controller.preferredContentSize - realPanel.setContentSize(NSSize(width: 340, height: max(size.height, 200))) - realPanel.setFrameOrigin(NSPoint(x: backdrop.frame.midX - 170, y: backdrop.frame.midY - 150)) - realPanel.makeKeyAndOrderFront(nil) - window = realPanel - NSApp.activate(ignoringOtherApps: true) - - Task { - var snap: ProxySnapshot - let mode = ProcessInfo.processInfo.environment["PROBE_STATE"] ?? "live" - switch mode { - case "stopped": - snap = ProxySnapshot(state: .unreachable, endpoint: endpoint, - lastKnownStartCommand: "ocx service start") - case "unauthorized": - snap = ProxySnapshot(state: .unauthorized, endpoint: endpoint) - case "loading": - snap = ProxySnapshot(state: .loading, endpoint: endpoint) - case "degraded": - snap = ProxySnapshot(state: .degraded("The proxy returned an unexpected status (503)."), - endpoint: endpoint, lastUpdated: Date().addingTimeInterval(-120)) - case "overflow": - let many = (1...24).map { i in - #"{"provider":"p\#(i)","label":"Provider \#(i)","quota":{"weeklyPercent":\#(i * 3)}}"# - }.joined(separator: ",") - let quotas = (try? JSONDecoder().decode([QuotaReport].self, from: Data("[\(many)]".utf8))) ?? [] - let usage = try? JSONDecoder().decode( - UsageReport.self, - from: Data(#"{"range":"today","summary":{"requests":100,"totalTokens":1200},"models":[{"provider":"p","model":"m","requests":100,"totalTokens":1200}]}"#.utf8)) - let timeline = try? JSONDecoder().decode( - UsageTimeline.self, - from: Data(#"{"start":0,"end":3600,"bucketSeconds":900,"buckets":4,"metric":"total","aggregation":"sum","grouping":"model","series":[{"id":"p/m","provider":"p","model":"m","total":1200,"points":[100,200,300,600]}],"availableModels":["p/m"],"missingMeasurements":0}"#.utf8)) - snap = ProxySnapshot(state: .running(StartupHealth(status: "protected", protection: "service")), - endpoint: endpoint, usage: usage, settings: CompanionSettings(menuBarMetric: .tokens), - today: usage, timeline: timeline, - quotas: quotas, - quotasLoaded: true) - case "empty": - let usage = try? JSONDecoder().decode( - UsageReport.self, - from: Data(#"{"range":"today","summary":{"requests":0},"models":[],"accounts":[]}"#.utf8)) - snap = ProxySnapshot(state: .running(StartupHealth(status: "protected", protection: "service")), - endpoint: endpoint, usage: usage, today: usage, quotas: [], providers: [], - providersLoaded: true, quotasLoaded: true) - default: - await coordinator.setPopoverOpen(true) - snap = await coordinator.current - } - await MainActor.run { - self.controller.apply(snap) - // Expand the provider list so its toggles are visible in the capture. - if ProcessInfo.processInfo.environment["PROBE_EXPAND"] == "1" { - self.expandProviders(in: self.controller.view) - } - if ProcessInfo.processInfo.environment["PROBE_RESULT"] != nil { - self.controller.showResult( - ProcessInfo.processInfo.environment["PROBE_RESULT"]!, - isError: ProcessInfo.processInfo.environment["PROBE_RESULT_ERROR"] == "1") - } - self.controller.view.layoutSubtreeIfNeeded() - // Match the real popover: size to content instead of a fixed frame. - let h = self.controller.preferredContentSize.height - if h > 0, let w = self.window { - w.setContentSize(NSSize(width: 340, height: h)) - } - } - try? await Task.sleep(nanoseconds: 1_200_000_000) - await MainActor.run { self.capture() } - } - } - - @MainActor func expandProviders(in view: NSView) { - for sub in view.subviews { - if let button = sub as? NSButton, button.bezelStyle == .disclosure { - button.state = .on - if let target = button.target, let action = button.action { - _ = target.perform(action, with: button) - } - } - expandProviders(in: sub) - } - } - - @MainActor func capture() { - guard let w = window else { return } - let tag = ProcessInfo.processInfo.environment["PROBE_TAG"] ?? "light" - // CGWindowListCreateImage rather than shelling out to screencapture: nothing - // under app/ may construct a Process (030 security rule). The bitmap-rep path - // is not an option either — it skips text rendering entirely. - let id = CGWindowID(w.windowNumber) - if let cg = CGWindowListCreateImage( - .null, .optionIncludingWindow, id, [.boundsIgnoreFraming, .bestResolution] - ) { - let rep = NSBitmapImageRep(cgImage: cg) - if let png = rep.representation(using: .png, properties: [:]) { - try? png.write(to: URL(fileURLWithPath: "/tmp/popover-\(tag).png")) - } - } - NSApp.terminate(nil) - } -} - -let app = NSApplication.shared -app.setActivationPolicy(.regular) -let d = ProbeDelegate() -app.delegate = d -app.run() diff --git a/app/Widget-Info.plist b/app/Widget-Info.plist index 360a159103..568e5fce75 100644 --- a/app/Widget-Info.plist +++ b/app/Widget-Info.plist @@ -4,7 +4,7 @@ CFBundleDevelopmentRegionen CFBundleExecutableOpenCodexWidget - CFBundleIdentifiercom.opencodex.menubar.widget + CFBundleIdentifiercom.opencodex.desktop.widget CFBundleInfoDictionaryVersion6.0 CFBundleNameOpenCodex CFBundlePackageTypeXPC! diff --git a/desktop/README.md b/desktop/README.md new file mode 100644 index 0000000000..9106a50f67 --- /dev/null +++ b/desktop/README.md @@ -0,0 +1,69 @@ +# OpenCodex desktop shell + +The Tauri shell attaches to the local OpenCodex proxy and keeps the dashboard +in the proxy's loopback origin. During development: + +```sh +bun run prepare-sidecar +bun run prepare-widget +bunx tauri dev +``` + +The sidecar is generated from the repository's standalone binary build and is +not checked into git. + +The CI desktop-shell job performs Rust-only checks. It creates an empty +platform-named sidecar stub and a placeholder dashboard resource directory +solely for Tauri's external-binary and resource validation; it does not build +or run the standalone binary. + +For a macOS release build, prepare the sidecar and WidgetKit extension before invoking +Tauri: + +```sh +bun run prepare-sidecar +bun run prepare-widget +bunx tauri build +``` + +## Release packaging and updates + +The release workflow builds a macOS DMG, Windows MSI, Linux AppImage, and Debian package. +It collects the platform artifacts beside checksum files and creates `latest.json` for the +Tauri updater. The public updater key and endpoint live in `src-tauri/tauri.conf.json`; +the private key must never be committed. The manifest is generated only when the updater +key secret is configured and then requires all four platforms to be signed. + +To package locally: + +```sh +bun run build:gui +cd desktop +bun install --frozen-lockfile +bun run prepare-sidecar +bun run prepare-widget +bunx tauri build --ci --bundles app,dmg +``` + +Release signing is supplied through environment variables: + +```sh +export TAURI_SIGNING_PRIVATE_KEY="..." +export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="..." +export APPLE_CERTIFICATE="..." +export APPLE_CERTIFICATE_PASSWORD="..." +export APPLE_SIGNING_IDENTITY="Developer ID Application: Your Name (TEAMID)" +export APPLE_ID="..." +export APPLE_PASSWORD="..." +export APPLE_TEAM_ID="..." +export MACOS_SIGN_IDENTITY="$APPLE_SIGNING_IDENTITY" +``` + +Generate a Tauri updater key pair with: + +```sh +bunx tauri signer generate +``` + +Keep the private key in a local secret store. Windows SmartScreen signing is not wired +yet; the release workflow documents that installers may show an unsigned-publisher warning. diff --git a/desktop/package.json b/desktop/package.json new file mode 100644 index 0000000000..09668ce09b --- /dev/null +++ b/desktop/package.json @@ -0,0 +1,13 @@ +{ + "name": "@opencodex/desktop", + "private": true, + "scripts": { + "dev": "tauri dev", + "build": "tauri build", + "prepare-sidecar": "bun scripts/prepare-sidecar.ts", + "prepare-widget": "bash scripts/build-widget.sh" + }, + "devDependencies": { + "@tauri-apps/cli": "2.5.0" + } +} diff --git a/desktop/scripts/build-widget.sh b/desktop/scripts/build-widget.sh new file mode 100755 index 0000000000..59827dcea2 --- /dev/null +++ b/desktop/scripts/build-widget.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ "$(uname -s)" != "Darwin" ]]; then + echo "prepare-widget requires macOS." >&2 + exit 1 +fi + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +desktop_dir="$(cd "$script_dir/.." && pwd)" +repo_root="$(cd "$desktop_dir/.." && pwd)" +package_dir="$repo_root/app" +output_dir="$desktop_dir/src-tauri/widget/OpenCodexWidget.appex" +configuration="${CONFIGURATION:-release}" +universal="${UNIVERSAL:-1}" + +if [[ "$universal" != "0" && "$universal" != "1" ]]; then + echo "UNIVERSAL must be 0 or 1." >&2 + exit 1 +fi + +build_root="$(mktemp -d "${TMPDIR:-/tmp}/opencodex-widget.XXXXXX")" +cleanup() { rm -rf "$build_root"; } +trap cleanup EXIT + +build_widget() { + local arch="$1" + local scratch="$build_root/$arch" + swift build \ + --package-path "$package_dir" \ + --scratch-path "$scratch" \ + -c "$configuration" \ + --arch "$arch" \ + --product OpenCodexWidget + swift build \ + --package-path "$package_dir" \ + --scratch-path "$scratch" \ + -c "$configuration" \ + --arch "$arch" \ + --show-bin-path +} + +if [[ "$universal" == "1" ]]; then + arm64_bin="$(build_widget arm64 | tail -n 1)/OpenCodexWidget" + x86_64_bin="$(build_widget x86_64 | tail -n 1)/OpenCodexWidget" + executable="$build_root/OpenCodexWidget" + lipo -create "$arm64_bin" "$x86_64_bin" -output "$executable" +else + executable="$(build_widget "$(uname -m)" | tail -n 1)/OpenCodexWidget" +fi + +[[ -x "$executable" ]] || { echo "Swift build did not produce $executable" >&2; exit 1; } + +rm -rf "$output_dir" +mkdir -p "$output_dir/Contents/MacOS" +cp "$executable" "$output_dir/Contents/MacOS/OpenCodexWidget" +cp "$package_dir/Widget-Info.plist" "$output_dir/Contents/Info.plist" + +version="$(sed -n 's/^[[:space:]]*"version": "\([^"]*\)",/\1/p' "$desktop_dir/src-tauri/tauri.conf.json" | head -n 1)" +version_core="${version%%-*}" +[[ "$version_core" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { + echo "Invalid Tauri version: $version" >&2 + exit 1 +} +plutil -replace CFBundleShortVersionString -string "$version_core" "$output_dir/Contents/Info.plist" +plutil -replace CFBundleVersion -string "$version_core" "$output_dir/Contents/Info.plist" + +if [[ -n "${MACOS_SIGN_IDENTITY:-}" ]]; then + codesign --force --sign "$MACOS_SIGN_IDENTITY" --entitlements "$package_dir/Widget.entitlements" \ + --timestamp "$output_dir" +else + codesign --force --sign - --entitlements "$package_dir/Widget.entitlements" \ + --timestamp=none "$output_dir" +fi + +echo "$output_dir" diff --git a/desktop/scripts/collect-release-assets.ts b/desktop/scripts/collect-release-assets.ts new file mode 100644 index 0000000000..f946e1d4a9 --- /dev/null +++ b/desktop/scripts/collect-release-assets.ts @@ -0,0 +1,105 @@ +import { createHash } from "node:crypto"; +import { + copyFileSync, + existsSync, + mkdirSync, + readFileSync, + readdirSync, + writeFileSync, +} from "node:fs"; +import { join, resolve } from "node:path"; + +type BundleKind = "dmg" | "app.tar.gz" | "msi" | "appimage" | "deb"; + +interface BundleSpec { + kind: BundleKind; + dir: string; + name: string; +} + +const bundlesByTarget: Record = { + "universal-apple-darwin": [ + { kind: "dmg", dir: "dmg", name: "macos.dmg" }, + { kind: "app.tar.gz", dir: "macos", name: "macos.app.tar.gz" }, + ], + "aarch64-apple-darwin": [ + { kind: "dmg", dir: "dmg", name: "macos.dmg" }, + { kind: "app.tar.gz", dir: "macos", name: "macos.app.tar.gz" }, + ], + "x86_64-apple-darwin": [ + { kind: "dmg", dir: "dmg", name: "macos.dmg" }, + { kind: "app.tar.gz", dir: "macos", name: "macos.app.tar.gz" }, + ], + "x86_64-pc-windows-msvc": [{ kind: "msi", dir: "msi", name: "windows-x64.msi" }], + "x86_64-unknown-linux-gnu": [ + { kind: "appimage", dir: "appimage", name: "linux-x86_64.AppImage" }, + { kind: "deb", dir: "deb", name: "linux-amd64.deb" }, + ], +}; + +export interface CollectReleaseAssetsOptions { + version: string; + target: string; + out: string; + repoRoot?: string; +} + +function findBundle(directory: string, kind: BundleKind): string { + if (!existsSync(directory)) { + throw new Error(`Missing ${kind} bundle directory: ${directory}`); + } + const artifact = readdirSync(directory) + .filter(name => name.toLowerCase().endsWith(`.${kind.toLowerCase()}`)); + if (artifact.length === 0) throw new Error(`No ${kind} bundle found in ${directory}`); + if (artifact.length > 1) { + throw new Error(`Multiple ${kind} bundles found in ${directory}: ${artifact.join(", ")}`); + } + return join(directory, artifact[0]); +} + +export function collectReleaseAssets(options: CollectReleaseAssetsOptions): string[] { + const repoRoot = resolve(options.repoRoot ?? join(import.meta.dir, "../..")); + const bundles = bundlesByTarget[options.target]; + if (!bundles) throw new Error(`Unsupported desktop target: ${options.target}`); + + const output = resolve(options.out); + mkdirSync(output, { recursive: true }); + const written: string[] = []; + for (const bundle of bundles) { + const source = findBundle( + join(repoRoot, "desktop", "src-tauri", "target", options.target, "release", "bundle", bundle.dir), + bundle.kind, + ); + const destinationName = `OpenCodex-${options.version}-${bundle.name}`; + const destination = join(output, destinationName); + copyFileSync(source, destination); + written.push(destination); + + const signature = `${source}.sig`; + if (existsSync(signature)) { + copyFileSync(signature, `${destination}.sig`); + written.push(`${destination}.sig`); + } + + const digest = createHash("sha256").update(readFileSync(destination)).digest("hex"); + const checksum = `${destination}.sha256`; + writeFileSync(checksum, `${digest} ${destinationName}\n`); + written.push(checksum); + } + return written; +} + +function argument(name: string): string | undefined { + const index = Bun.argv.indexOf(name); + return index < 0 ? undefined : Bun.argv[index + 1]; +} + +if (import.meta.main) { + const version = argument("--version"); + const target = argument("--target"); + const out = argument("--out"); + if (!version || !target || !out) { + throw new Error("Usage: collect-release-assets.ts --version --target --out "); + } + for (const path of collectReleaseAssets({ version, target, out })) console.log(`Wrote ${path}`); +} diff --git a/desktop/scripts/prepare-sidecar.ts b/desktop/scripts/prepare-sidecar.ts new file mode 100644 index 0000000000..502127870d --- /dev/null +++ b/desktop/scripts/prepare-sidecar.ts @@ -0,0 +1,59 @@ +import { copyFileSync, cpSync, existsSync, mkdirSync } from "node:fs"; +import { join, resolve } from "node:path"; + +const targetByTriple: Record = { + "aarch64-apple-darwin": "bun-darwin-arm64", + "x86_64-apple-darwin": "bun-darwin-x64", + "x86_64-pc-windows-msvc": "bun-windows-x64", + "x86_64-unknown-linux-gnu": "bun-linux-x64", + "aarch64-unknown-linux-gnu": "bun-linux-arm64", +}; + +function argument(name: string): string | undefined { + const index = Bun.argv.indexOf(name); + return index < 0 ? undefined : Bun.argv[index + 1]; +} + +function hostTriple(): string | undefined { + const result = Bun.spawnSync(["rustc", "-vV"], { stdout: "pipe", stderr: "ignore" }); + if (result.exitCode !== 0) return undefined; + const host = result.stdout.toString().match(/^host:\s*(\S+)$/m)?.[1]; + return host; +} + +const repoRoot = resolve(import.meta.dir, "../.."); +const triple = + argument("--target") ?? + process.env.TARGET ?? + process.env.RUST_TARGET ?? + Bun.env.RUST_TARGET ?? + hostTriple(); +if (!triple || !targetByTriple[triple]) { + throw new Error( + `Unsupported Rust target ${triple ?? "(host unavailable)"}; pass --target ${Object.keys(targetByTriple).join("|")}`, + ); +} + +const target = targetByTriple[triple]; +const source = join(repoRoot, "dist", "standalone", target); +const executable = join(source, target.startsWith("bun-windows-") ? "ocx.exe" : "ocx"); +if (!existsSync(executable)) { + const result = Bun.spawnSync([ + process.execPath, + "run", + "build:standalone", + "--target", + target, + ], { cwd: repoRoot, stdout: "inherit", stderr: "inherit" }); + if (result.exitCode !== 0) process.exit(result.exitCode); +} + +const desktopRoot = resolve(import.meta.dir, ".."); +const binaries = join(desktopRoot, "src-tauri", "binaries"); +const resources = join(desktopRoot, "src-tauri", "resources", "gui", "dist"); +mkdirSync(binaries, { recursive: true }); +mkdirSync(resources, { recursive: true }); +const destination = join(binaries, `ocx-${triple}${target.startsWith("bun-windows-") ? ".exe" : ""}`); +copyFileSync(executable, destination); +cpSync(join(repoRoot, "gui", "dist"), resources, { recursive: true }); +console.log(`Prepared ${destination}`); diff --git a/desktop/scripts/updater-manifest.ts b/desktop/scripts/updater-manifest.ts new file mode 100644 index 0000000000..e67459d848 --- /dev/null +++ b/desktop/scripts/updater-manifest.ts @@ -0,0 +1,98 @@ +import { + existsSync, + readFileSync, + renameSync, + writeFileSync, +} from "node:fs"; +import { join, resolve } from "node:path"; + +export interface UpdaterManifestOptions { + version: string; + dir: string; + repo: string; + out: string; + warn?: (message: string) => void; + requireAll?: boolean; +} + +interface PlatformUpdate { + signature: string; + url: string; +} + +export interface UpdaterManifest { + version: string; + notes: string; + pub_date: string; + platforms: Record; +} + +const platformFiles: Record = { + "darwin-aarch64": "macos.app.tar.gz", + "darwin-x86_64": "macos.app.tar.gz", + "windows-x86_64": "windows-x64.msi", + "linux-x86_64": "linux-x86_64.AppImage", +}; + +export function buildUpdaterManifest(options: UpdaterManifestOptions): UpdaterManifest { + const dir = resolve(options.dir); + const warn = options.warn ?? console.warn; + const platforms: Record = {}; + const missing: string[] = []; + for (const [platform, suffix] of Object.entries(platformFiles)) { + const base = `OpenCodex-${options.version}-${suffix}`; + const signaturePath = join(dir, `${base}.sig`); + if (!existsSync(signaturePath)) { + missing.push(platform); + if (!options.requireAll) { + warn(`Skipping ${platform}: missing ${signaturePath}`); + } + continue; + } + platforms[platform] = { + signature: readFileSync(signaturePath, "utf8").trim(), + url: `https://github.com/${options.repo}/releases/download/v${options.version}/${base}`, + }; + } + if (options.requireAll && missing.length > 0) { + throw new Error(`Missing signed updater platforms: ${missing.join(", ")}`); + } + if (Object.keys(platforms).length === 0) { + throw new Error("No signed updater platforms remain"); + } + return { + version: options.version, + notes: `https://github.com/${options.repo}/releases/tag/v${options.version}`, + pub_date: new Date().toISOString(), + platforms, + }; +} + +export function writeUpdaterManifest(options: UpdaterManifestOptions): UpdaterManifest { + const manifest = buildUpdaterManifest(options); + const output = resolve(options.out); + const temporary = `${output}.${process.pid}.tmp`; + writeFileSync(temporary, `${JSON.stringify(manifest, null, 2)}\n`); + renameSync(temporary, output); + return manifest; +} + +function argument(name: string): string | undefined { + const index = Bun.argv.indexOf(name); + return index < 0 ? undefined : Bun.argv[index + 1]; +} + +if (import.meta.main) { + const version = argument("--version"); + const dir = argument("--dir"); + const repo = argument("--repo"); + const out = argument("--out"); + const requireAll = Bun.argv.includes("--require-all"); + if (!version || !dir || !repo || !out) { + throw new Error( + "Usage: updater-manifest.ts --version --dir --repo --out [--require-all]", + ); + } + writeUpdaterManifest({ version, dir, repo, out, requireAll }); + console.log(`Wrote ${out}`); +} diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock new file mode 100644 index 0000000000..95bc1a674a --- /dev/null +++ b/desktop/src-tauri/Cargo.lock @@ -0,0 +1,5654 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +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-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "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 = "auto-launch" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f012b8cc0c850f34117ec8252a44418f2e34a2cf501de89e29b241ae5f79471" +dependencies = [ + "dirs 4.0.0", + "thiserror 1.0.69", + "winreg 0.10.1", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide 0.8.9", + "object", + "rustc-demangle", + "windows-link 0.2.1", +] + +[[package]] +name = "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 = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[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.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +dependencies = [ + "serde", +] + +[[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 = "blocking" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "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 = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +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.9.4", + "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.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0b03af37dad7a14518b7691d81acb0f8222604ad3d1b02f6b4bed5188c0cd5" +dependencies = [ + "serde", +] + +[[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.20", +] + +[[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.5", +] + +[[package]] +name = "cc" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313" +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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core", +] + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.1.3", +] + +[[package]] +name = "combine" +version = "4.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "cookie" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" +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.9.4", + "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.9.4", + "core-foundation", + "libc", +] + +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "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.119", +] + +[[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.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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.119", +] + +[[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 = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys 0.3.7", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users 0.4.6", + "winapi", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.3", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.9.4", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "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.119", +] + +[[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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63a1d0de4f2249aa0ff5884d7080814f446bb241a559af6c170a41e878ed2d45" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 0.9.5", + "vswhom", + "winreg 0.55.0", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encoding_rs" +version = "0.8.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5ef0006ac9ab233c38522f5ae99cae3625151de8f706cacee1cba4b8e2832a" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7" +dependencies = [ + "serde", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[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 = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide 0.9.1", + "zlib-rs", +] + +[[package]] +name = "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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-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 = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[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.9.4", + "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.119", +] + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[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.119", +] + +[[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 = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "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.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "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 0.6.5", + "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.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "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 = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png 0.18.1", +] + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "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.119", +] + +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +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.9.4", + "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.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[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.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6480ccc157a1389bb2e4891b24751b0f798ba640d22386f23143fbcc89da195a" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lru-slab" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[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 = "minisign-verify" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "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.20", + "windows-sys 0.61.2", +] + +[[package]] +name = "multiversion" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ca4bea16ffc3f443cf7d866912118196bfef4c6a1556ca00f9f9b00bb43f7c" +dependencies = [ + "multiversion-macros", +] + +[[package]] +name = "multiversion-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d416831a7317ef4b08bee00b69cbbb9c8763da7959a7026244d6266869f9c83" +dependencies = [ + "proc-macro2", + "quote", + "rustversion", + "syn 3.0.6", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.9.4", + "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.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[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.4.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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.9.4", + "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.9.4", + "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.9.4", + "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.9.4", + "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.9.4", + "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.9.4", + "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.9.4", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-osa-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0" +dependencies = [ + "bitflags 2.9.4", + "objc2", + "objc2-app-kit", + "objc2-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.9.4", + "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.9.4", + "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.9.4", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "open" +version = "5.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa576c76302b7b808eecc68061e67336c47833ef9d22caa74dda10fa9675eebc" +dependencies = [ + "dunce", + "is-wsl", + "libc", +] + +[[package]] +name = "opencodex-desktop" +version = "2.61.0" +dependencies = [ + "reqwest 0.12.24", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-autostart", + "tauri-plugin-opener", + "tauri-plugin-process", + "tauri-plugin-shell", + "tauri-plugin-single-instance", + "tauri-plugin-updater", + "tokio", + "uuid", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "osakit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" +dependencies = [ + "objc2", + "objc2-foundation", + "objc2-osa-kit", + "serde", + "serde_json", + "thiserror 2.0.20", +] + +[[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" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "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.119", +] + +[[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 = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.2", + "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 0.8.9", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.9.4", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "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.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.4", +] + +[[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.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pxfm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2 0.6.5", + "thiserror 2.0.20", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9746dbde176634f4f2f1faf2404e30a31b2bc1e9cafb5329c95d8177a18c9fc" +dependencies = [ + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.20", + "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 0.6.5", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.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.9.4", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "redox_users" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" +dependencies = [ + "libredox", + "thiserror 2.0.20", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.12.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +dependencies = [ + "base64 0.22.1", + "bytes", + "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", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.4.2", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "reqwest" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" +dependencies = [ + "base64 0.23.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 0.5.0", + "web-sys", +] + +[[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-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags 2.9.4", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "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_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.119", +] + +[[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.9.4", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +dependencies = [ + "serde", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34836a629bcbc6f1afdf0907a744870039b1e14c0561cb26094fa683b158eff3" +dependencies = [ + "erased-serde", + "serde", + "typeid", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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.119", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" +dependencies = [ + "serde", +] + +[[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.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e47d95bc83ed33b2ecf84f4187ad1ab9685d18ff28db000c99deac8ce180e3" +dependencies = [ + "base64 0.21.7", + "chrono", + "hex", + "indexmap 1.9.3", + "serde", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea3cee93715c2e266b9338b7544da68a9f24e227722ba482bd1c024367c77c65" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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.119", +] + +[[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 = "shared_child" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "607549934f6cc26b89cfecfdc46fa90f1e5d1536a68349b0c3a4f9d1c0d37959" +dependencies = [ + "libc", + "sigchld", + "windows-sys 0.61.2", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "sigchld" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24f2b37f04360cd465089b87a9c3869c08220a2f3458463f0adf8badf5e77f2c" +dependencies = [ + "libc", + "os_pipe", + "signal-hook", +] + +[[package]] +name = "signal-hook" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a0c28ca5908dbdbcd52e6fdaa00358ab88637f8ab33e1f188dd510eb44b53d" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[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.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e45c444e496845d3f2a351146bff59aae4975b2280238df1dfaa0c7d1846f38e" +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.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "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.9.4", + "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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7eeb6d99155545da6150a1795945f16ac9c178deb2a5f2e74d776107bd5849" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fa5bacdb9bbad5954af3d1bd6cf6ae9192cab1b2e270f4a07f904610b9e85f4" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs 6.0.0", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "image", + "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.5", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.20", + "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 6.0.0", + "glob", + "heck 0.5.0", + "json-patch", + "schemars", + "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.119", + "tauri-utils", + "thiserror 2.0.20", + "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.119", + "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", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-autostart" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "062cdcd483d5e3148c9a64dabf8c574e239e2aa1193cf208d95cf89a676f87a5" +dependencies = [ + "auto-launch", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.20", +] + +[[package]] +name = "tauri-plugin-opener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc624469b06f59f5a29f874bbc61a2ed737c0f9c23ef09855a292c389c42e83f" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation", + "open", + "schemars", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.20", + "url", + "windows", + "zbus", +] + +[[package]] +name = "tauri-plugin-process" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7461c622a5ea00eb9cd9f7a08dbd3bf79484499fd5c21aa2964677f64ca651ab" +dependencies = [ + "tauri", + "tauri-plugin", +] + +[[package]] +name = "tauri-plugin-shell" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2c50a63e60fb8925956cc5b7569f4b750ac197a4d39f13b8dd46ea8e2bad79" +dependencies = [ + "encoding_rs", + "log", + "open", + "os_pipe", + "regex", + "schemars", + "serde", + "serde_json", + "shared_child", + "tauri", + "tauri-plugin", + "thiserror 2.0.20", + "tokio", +] + +[[package]] +name = "tauri-plugin-single-instance" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc61e4822b8f74d68278e09161d3e3fdd1b14b9eb781e24edccaabf10c420e8c" +dependencies = [ + "serde", + "serde_json", + "tauri", + "thiserror 2.0.20", + "tracing", + "windows-sys 0.60.2", + "zbus", +] + +[[package]] +name = "tauri-plugin-updater" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27cbc31740f4d507712550694749572ec0e43bdd66992db7599b89fbfd6b167b" +dependencies = [ + "base64 0.22.1", + "dirs 6.0.0", + "flate2", + "futures-util", + "http", + "infer", + "log", + "minisign-verify", + "osakit", + "percent-encoding", + "reqwest 0.12.24", + "semver", + "serde", + "serde_json", + "tar", + "tauri", + "tauri-plugin", + "tempfile", + "thiserror 2.0.20", + "time", + "tokio", + "url", + "windows-sys 0.60.2", + "zip", +] + +[[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.20", + "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", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.20", + "toml 0.9.5", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0" +dependencies = [ + "dunce", + "embed-resource", + "toml 0.9.5", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" +dependencies = [ + "new_debug_unreachable", +] + +[[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.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl 2.0.20", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2 0.5.10", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" +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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" +dependencies = [ + "indexmap 2.14.2", + "serde", + "serde_spanned 1.0.0", + "toml_datetime 0.7.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.2", + "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.2", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7211ff1b8f0d3adae1663b7da9ffe396eabe1ca25f0b0bee42b0da29a9ddce93" +dependencies = [ + "indexmap 2.14.2", + "toml_datetime 0.7.0", + "toml_parser", + "winnow 0.7.15", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.9.4", + "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-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "045979e3f037cd18ad1cb2a419dfda133c5c29c9f3453370079f2255d46c257e" +dependencies = [ + "crossbeam-channel", + "dirs 6.0.0", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.20", + "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 = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "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.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[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 = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "serde", + "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.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.6", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasm-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.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" +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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba8b815c1b593dc0baf78dd0f4fc8fdb2de53198fb1163738093e9a311c33fb3" +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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +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.119", +] + +[[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.20", + "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.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-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" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[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 6.0.0", + "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.20", + "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 = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.4", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 3.0.6", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" +dependencies = [ + "serde", + "winnow 1.0.4", + "zvariant", +] + +[[package]] +name = "zcheapstr" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473" +dependencies = [ + "serde", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "zip" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" +dependencies = [ + "arbitrary", + "crc32fast", + "indexmap 2.14.2", + "memchr", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + +[[package]] +name = "zvariant" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 1.0.4", + "zcheapstr", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 3.0.6", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 3.0.6", + "winnow 1.0.4", +] diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml new file mode 100644 index 0000000000..8683695613 --- /dev/null +++ b/desktop/src-tauri/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "opencodex-desktop" +version = "2.61.0" +description = "OpenCodex desktop shell" +authors = ["OpenCodex contributors"] +license = "MIT" +edition = "2021" +rust-version = "1.77" + +[lib] +name = "opencodex_desktop_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "=2.6.3", features = [] } + +[dependencies] +reqwest = { version = "=0.12.24", default-features = false, features = ["json", "rustls-tls"] } +serde = { version = "=1.0.219", features = ["derive"] } +serde_json = "=1.0.140" +uuid = { version = "=1.18.1", features = ["v4"] } +tauri = { version = "=2.11.6", features = ["tray-icon", "image-png"] } +tauri-plugin-autostart = "=2.5.0" +tauri-plugin-opener = "=2.5.3" +tauri-plugin-process = "=2.3.0" +tauri-plugin-shell = "=2.2.0" +tauri-plugin-single-instance = "=2.4.0" +tauri-plugin-updater = "=2.9.0" +tokio = { version = "=1.45.1", features = ["sync", "time"] } + +[profile.release] +codegen-units = 1 +lto = "thin" +opt-level = "s" +strip = "symbols" diff --git a/desktop/src-tauri/build.rs b/desktop/src-tauri/build.rs new file mode 100644 index 0000000000..d860e1e6a7 --- /dev/null +++ b/desktop/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/desktop/src-tauri/capabilities/default.json b/desktop/src-tauri/capabilities/default.json new file mode 100644 index 0000000000..622143b1fa --- /dev/null +++ b/desktop/src-tauri/capabilities/default.json @@ -0,0 +1,14 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Bootstrap-only shell permissions", + "windows": ["main"], + "permissions": [ + "core:default", + "core:window:allow-show", + "core:window:allow-hide", + "core:window:allow-set-title", + "opener:default", + "autostart:default" + ] +} diff --git a/desktop/src-tauri/icons/128x128.png b/desktop/src-tauri/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..e77df1e28fc9cfe97acd00d99810b17fd1fde004 GIT binary patch literal 2849 zcmZ{mXHXLg0)!((AV7cwG18@1gA{3kKunCNp(rIZArN{E9eETf2Ba!dlwJhX2NFek z2}pf(l-`LHL4*(x`*l{G zr@28$SZT*jk*lRib36$RV5R@jQ9!2!AxIfltmtQhy zSHZpQIJlGd8eOMXhro*GNXY{xlynx_KC-{M!!)yK{1tXq^Smm)iZ_3gFl`cI2cY2Hx{?x8ihEj|LFVDC&wqtC()-l9$8hYQJDvh zP{S!Kf!b}B*CvxJ3wI<#0Aj0Q;ZhlFF+nWMEDJ26UvFHSWWtM#?hHZl+O^dB|z3CddQT6~RXnC=1L5KmyV}#25{U2c&hNz0)xa zR&6;7=+moZQ6EN>`%Kfu7l(@BXM#&ky z_;p^=bY$)5#!qxmhp9eMJLi||2Z|%WIBgsojvwa$5WQ`qotRZ{0Xhk#(Z#2Ak|GsS zW-|SjBko!1T}K}F*G*8~`5aq>L{`m|@C@X`;zF>_`gVs_c;>cEU=0vKu%v0+*IvlX zd}F-4Y%AWx3dLu}6JJ_3y?K**F$uemNj#Is>6FH4$tY~X^&x6;tq`I>MPYPHhqO0W zc&6Bx!?@I)M?)p)?Gai2C(Uw+bJYVEpC>}*m-sSb&zwe4702iGt@XL|h^=H3KjeBH zK`63#OfcBUwsr`w??wr|*O2%Z71pGjRVUN<8c!;??f&2$M=8RxBb<6W-Wa@ z6G}{} z?jO{0M=Z55^ycL7tECVB#Fu>D^0y|lBdII##B!yWh%p}UB8^N_**F@jg`!sXos z4hpA$4Hm}Jq7x5O;%*Y@(1n?!0Abg&!3DG}lya0b*=*fL2%Z?73nRrTzXl%WbHEW& zU5c0ci;oK)02*`Bu_f3QE;{wh(Cyd3@|PN_?rRz66LE{{hiW%$0z!BbycSl+98HG( zJff(~9C&*t;TUS88(kQa(6s!0`Ft=2$K#PYlVPkkM4I}1p5Wtmd+k)h;qgtO?tMxk z*drZyiIvk%fEFG-AdnyLFfV&HkrL}=puxeFO=_!>WvHTNEWN1gTUa6LvRDx$AE32D+bA)mbOgm~s}YZ%j*l1sZ8?xQPfEWlP-nk;sLSujdyr9Ef<;92ed zb6Z{W5l^P4U&F2dv;0&fTRRYpsy;P(aD+KL&o_${Tk(NQW%K*=V8ZrS89P@)nE)Q< z8k7;k?05ahPI9(UmvK>IAKdTl78Z2tjsTkQWQ?LPqcf3?ySaQKUMDS`Q}oP@3dx(uA|>p%7`8la{X5gt4JNyTQa zJQ`r0xKJ2tq0Q%Iof0?nNa(Q6-c4Pg0lTPPpC@bit0^LnGM``ywfjo&o?w$0FV9W7 z%Pl*>CcG;o#fgYKPbi&RkxW=wuY8T}@b*mjdpLb_K#tTM3vC1W^!jclET>?E9O!D!h3AZ;xoxypy7DDW$_9=vC^FH?r>qw(N2Ky3`Xd zRn2M1!O4QrY{XHY;iEeL7HD{F*SWjXxS(6+@CRMJqGP(v@|Fq-gInh`50@!IFo6RU z8dIELtTWp7aTv2eS>B>l>6IlvfzVTl+D#x^VuSFJmv?&^gBNoxlP010mRicsXzoah z;Qo@WyPC)m0Tbal;#dkM3? zWA;n8EE)?E9#dZQXofR4Y>$s9B4@f>XX=F6r#hnr_I%K@hb~Ob{$+mEFMpGujQUeq zNU+m!BHbI|51kk%ZMh5bd+ZTe$tAGh-r`gol5quN)U$V~8~DKf+cBT*#@>oivF%K^ z`|+WjOjbY3{)#ll05lP3iD#ZQf1mV@Xtsy?=5prGLAh)5lMQl|sRx*X?n^IWzP;5p zg!J-raIu{}+ zvYKu!Zlj+VPdVUW9PaZ)Ua=$fnZi13nN0`9J)fI-iUp!K?zS^g)s{=4WU@M={C#ow zHU~XOstm1I^k!M4+RVx=?;4Nyq5x06R-|cjnJz<`a;IZ%Sc+w@MWNLfJVTGhiAX#8 zrEZs6O^;cRLE)!qP?{>XVKP$tk4;jVb2fI!`E!u(y2Zu@uDu&kRtg5DiU&vS1fGM+ zqkz#e_UA^ZqMDwwe=ZJpv3ktqeP$Y55o%bnp~7XhkrLUwjzL3;z7xmul~TS z%Pv;1;`GV2+U``$3YYaZN*$^)D7w^lOpDP_*-1!Hnz<)GTNojXd|8Y#O!M<5MvsvSed?o=5e ze9M;rc@EsC15fLRlApa3W=WYq$I^`3bx_}fvF1a+8eWW|=1)ONG2;0xw(mtbo>`K7E#Q-gl@H#kN)Mez`RJB-u8%H-H*9v9pau(Gi=n{13j; a=rBKLg=%#bB+u{b3(!XyAS)4eQU3xJL{iiM literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/128x128@2x.png b/desktop/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c6a3f4f3219f3b8a3f6c2745205598ef57cd2e77 GIT binary patch literal 6300 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 literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/32x32.png b/desktop/src-tauri/icons/32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..9299f3600476116e06ca002552e861f2bc32c65a GIT binary patch literal 638 zcmV-^0)hRBP)HOM*aq_;9&+|CAP089t&h!b z1;~!OVB?4TY!7S(iLiVG=};YmC5Bapya0-z;xkr_>jx>27E7YWo`7W^A76~PTys71 zS1~ihN5U2RszHFm&7#))MyNjk>miS#c?9>tny@v+J)wR13OEG| ze5YnUXGAA#e$joe7zx$Rz@nc3y)FsxAa?sC!yd)CDIz0If;t^zJffdIZLvF58yC(C zbZ)iaJjO8=Z4~$}&RkOWX&ZYXDGU@1aji7luqAz1ewk2axJra{-7M8t0(>-0#qDZV zJ8r|SU&sGXfY(MpURVKD@w>kbLOjEk>7y<{cPxqjrTDD^U|j+)Hux1IbkF#(yH6SJs@qwXuC{bo-7=-81?cIta7c?#W4cT zsaV&E4*Q!}Mnv~nh*UXd1w0F14FYtipTHoPVKFhy>-3(3^_}mBCkm1yI#)LOSdbiw zBK2>UWF0R`oq)5e*XR;lfFUpndLxwO=Unr{sAYN3he?73OGiFTI`(N6<<9Tg$LX)O Y57KR^4<>ipKEk+Ck0kqp4{QnGK1~3a~0LB5MC9F*oc?38B z>;YPUEkGN;513W-%>_cXv++Z5E)IUta{Q0v@vO6J`nHZg+#!r!|LJ@*)%By)i2jI~G>O~O^Q1XUy^NC!*N zY`;gdHg_` z2-!VBl|aBwoS%3Ez5`qXT)?#z)1{54(lJ_Jm}dpDpy}&LzcE&&p}anl7T857c8$gb zV~}DOp*1{uxg{h_#!GlaX1anZ=puB~MLEicC}7pfli#d0`xfiz)G} zW5}*tT|lz)b>8@G#G?H+{w5Oj50Y;iW#O`stn*Ah=|TW~K7$W7i4koD9(AIs$I>pY z#$1-bcvhJ}9jv;loY*jHm{g44?I82AX46k|1&Olk7!O1pLkC&r6lho&-*mV~)DaN5 zOmU75h4KM(gBrK6w>N<4y*-N+j~hBU5in|*^e}7Ennz;DWNy}IHw4(eZx5gav~#RH z@o8~SfG5Bc;0f>qcmg~Do&ZmPC!laXAj&9O4B8mU$Q)Vo0bvdSN0x`Au^u#7h%PJK zW0r@WqizT|uuR$*KnrMCHtV1a0k$Lqxy+pSKT|CFm=@zQDVOAp%E0xxy=$X z-|o|KO7b;X#W*x%-C*)1zBJ~T7LS>d>#SNt0z|m*T1vU9*E`=~72-+Ol_FRbXdPt_ zvWhoDUmgz%q@4BSzr{(@!7YPr>I~Iq@B_dnzz-%!l?@Wqbk)QzkQ*soA|%@i`hRZG zcIaDV>S~fYEKy6)u5^A-nxuxlP43h-N(LqB@d29LYy5kYT}eAeufM7YlC4iA@qo3; zK4F&EDN|Z@-4(@3(r?5`%i0wtERhi#FHDp{%&xYa5PkIC$7nB$`h3;z3k+?${Hb|i q!oM=(Uz!p=E$#{M1b705cl-ksSdIyd3tzqf000083QG zk_v*LcA?!U7-$pP)PT(*t|XLFh$X&xGoIm{OOBW4%$<32=jY8iaA^D9>&%?<-SgdZ z@7#N?Cs-OI9=m|SVqvftEEWcfg~4L5SQspp)`BH1UOQ^L1M-T4s+<^WSkiqZ@kvsCdZvL+xM20aFP1ax0b`qKFM?t3IZt5%fG#r^N5NWjN8c_@won2E7jY5+)3RmI2lUC>qfY>EB{NMnPWy zeGl|+&|eW$-*)yWEni!;xMz?;4&rwm3z=P+5f>mh0>O%@{GP@jR>l^#uB+1BxL>-1 zKy^qfY$8yq5&CFH2kp9pcB;PCX$lRxgbqFh{fLaAnha7>RvZ8M8YWHOAz*IN5hg7F zr8?T{Xh1*)y;+3LAV`b-S)^rxPL9Y>ED}@uH=sxR_@h=}ImEf&gT9P3;pYITF5|4M zz$trw0DYy8UuprCBrQ`#)IXq4WB@7BX!&>0CD3KibsXC~E|^**W_K}tO@K~=&Vo*9 zV?LPyG-Wr9Fj>TN%Yx}I?xQ})5K)vq1Wxr_MZi}on%0SZvL79N5~sXGMmksaU%=r? zA#^AU8b@_78~Or`GNG|9iKG~qbD&>o0C^?7#*iBQ(a(>Ao<#ul=T4*1`1#+$Sr6r> z%D{5TIO+~b*`;C2+s4_9Pc(o+t<$e+Fie4)$61RJL*|IcVES1y4(nD0mg3@HaJ*p> zRq8i!e{=%-qtfTuM85gVxL^Ac`?V~##slX=gZ5p{fJQ{f$p^`-gvrA%fIf#n`F-f8 zP7)BhU&rTOFl<~N891Lte|cp`mL!w1e}Fz|Kz|CT&v9O@&e3ENWuJk*YY<_|FAqVV z7CV!_QCTw>s$cm~^d@CHDc4dQzku^-qO6(ff+ZyV^0(MmPp8-~@%vr)?;nZ(tVfn= z72_CHyHlb{Wlu{;A%zYUMRn^$)bsy_eOOYKsQmK?qiZuybU6deC!Ru}f&`${KzAFH z36Zx|)iA;o^_@WblI8-{+~{ufTP{{}2Iz3-46dPQ?hHvL0x=vCkfh^`W^aNro2RX9I5*_0e?864D1|<$ z|701d`)!Pt|7%6+^EJ$vq83D!#94|-1HUZeQafvXmP6L{?Mne$wVFvtVe3|TiPQ>fv(0bLG*gqpox0$A!T!;DR^lpILm=8(LPT4~b4e=+#l z!xgkUsewkCAFa@!y2+$rD_bHI*aonaolAynGMTj@=K*VvVREu8+9}J1N!^~TV0k%s zc`OAi_4#^B!CE);&sf&}+{u{YkB|w;wBK&Fq`%UcXVZ{*cA-Cwhfq$A6xBCuIgeW! zS&t3<<7XYxcsZ9u56#xk3b(vEStljDVcmZ_M4<9+eZjl+Sr{x928+RBVX#;jEC!2( z!D6u31q>DogT-L6Fjy=M7K6nytYArYo2aA)iI?N;X2_rxVs~hJGhIMxIYcS1t4CW3 z)~KO>I$8H8o(E7_n**>90@Zc)mP{-c&_@roMN#h~^EDWxU?^rMxH3G|}(@g#wyeoI~V zq`?^@Vo1s?NVGS}ugYI{$-DbW>Rr#mlpwz&+G_@r>;eo_QE zsbI+}IfSNbyBv@T-75y1tLhV-$H8J}i0Dw&5}ZK$(&{mVn%_LGipEvZ`tD5M#M5rl z`Xd)xU~JGejS`v;)HSp#t=^*Y&qefGt_lWR$+HEOJk^75;ju)ol7X^FNjRPty?Rbx zV+-{gLW#8eUK~+xW+-axVwt<22C28m)u&rheOfPg7#ec1NBXc;lGX?0d#A$rM77D? z)1GS{pLf%NHa@{{qsUrya+UHYSa)_qOOABr|XoQ$CBNWoMD_DL+A)uI-NVniHisK- zNk+P)6+43fqtNTLPCgT&I9^K9eTpT!iix6kN5 z(lS9|F&h+GMkxC<jtwBUX%@h~3wa76S9)YoO4br%=6ERt98f=oL_ zwcaCS88m?>3Cv*K^GU4IJcc!8eX(=cSP}ORG1=L|_e>i&<#ij^_1*8o4Be^SRAmr; zQx~cM`U*Fq%lC;5U=_eaM3sbA1B}Nibx~LEsie=bne1nlM{dV@MOp!G#+`}^RZkW! z*TUmwgNC+pH*UFx%zReHd!6Out!do@2eUzkPJ4s5AC6b&BMi#8VAhhysMdO|hEf&wqF_<4cmW z7`KKEkH7Ic=0&z}5o&opv1FCCUv|jIsf2+ZkC!@>y_*kvULl zsGop!C~Tvkz!rHAvyHVya@d&o+vhmx10ijx^g6cl791kv8z@DRCyDV#5a`EVrEXV^ zsKre;&qx50prT^t0kXF@)drN_U?Ut0y$uR$}ygL>xkF`nDdhRKWAZ z8Or&y;tqD4;rXJy#G3v2DSgm`e%jb)p2WivKh)f)Zy&d(Eq?ms`@zPBD`~d1JiU_5 z+aIEp-kd`GGyXw5b~8mB#R6EaluVEF!GEw1+}3T2X)XW)ZawgET;e4fx*xIbGElgW zDl+AQFIFabkq3UU?*dD{If@S&g=KylAsg>qgaA!S+U>v~+iq|39TpW8Kabdga^tf# zw-Z8og8Xz7(;eV1M6HPGXFAB{|$x(SX z$^(Vt-cu@_77euQdFc>tHR4lcWF^Qmwg49ZcPaePQ1{QXE*u^=_A?N1Y!QBg#`lPs zIq@lhZz4@g(`XpcBFRi=%RDQsb4%&W9D$V0*hNdXVVgi?hqSi>ZGdYAG@frxhcXmpYJ0I!i&a8*`E80}#qoia!y^S}Gg!vdWB&TA-B)pbp=(bX9HhQ_P z^6rzexoU~tS3A50Kb0qLMfbwSrB==etIApDpc~x~-a<7fS9?F+p~(Ge8b>La z0MA2V4DyQL{u83fXD6W~Ww^V+b){cuF42Oaoum}5vWn_Z*#=qzdl>RGpj|&I%OfEg z8g%nhs4tw{5i>!>&ad-x3?7C{r;E)Ezoyf;AL6ED(5?+NGm*-PW0$<0#EuGL=T-K$ zaNIXN>3!Y)&l)lnuxkzMX1F=af+DI+?KI3gAxEW0*v}$=FMB)y^v+mNt`N;{9Af1{ z78@mfpq8?^TwY8itsay66PIUD=BsdWv%hvEu%i@7Qwq;yN4+C zqn*evE=&NOj@3^|R;Ey}%ml#)l7x@0a!Y^yCHN`uE-L8jw{f4wHp&_`MWyRlfh6{T zSHW}&TC%Ff{cb?1lSk--H`c0`q^Svl+nM9!3RT%JPgln5rp*o=`y~>t@T{y7%5b8N z24?Y5Do_!R3CoQqx~+|0AJzFzE!1ZC<9%y0$VJCo!H1dK6tE`lYK`Z8OdMvoay)W~ z_lLPRM9ycdX^pgik1kODe83$bu&KF}ODQIxUwOR{{W0Nj_qxiXmtBv0sdVG%>Nl$E z3rWaghS-BZz4ngKbWCJ{%en%>ziL&gFYz=~Ywm|^s#?{gw}m`O-C@4;qi)}NfbfsI zKuapVl9Ob&8P-{PgrmkkR`Oy?R87-tbVCqwDwWin=INaPKi8ZLy5wJZ%DTkh?~HAX zvX{_Vx2>WegFl7(wO@gV6_qHYXE^v{c0;5Bj<&IbFYJ5|L<|o%OKgJ3@|a2g z4%8Se)zaOOJ+9UnGvXh(nSpiCl=u$`zeV^j5`I(hPw*cq{?Cg4Wk|I#CAec-=b@-C z!Ea^SzeAvo>G6HDWoRS|Uj165;8Tsa6ibpvz@MIOuN(eSxW9rVlRbRik0x~qy9msr zf3uYDl2b(n~#@9AAP?lB&ByFYyFwXmk`K+mMP?YM$@T2_i`!=(qAvN zufieYP^AVydvhH*WCa7dWub3Y59i}pY|zGOPg!O+8*`&pBf9t9F=c4UjXtYC78O{o zppz*J)_LZ=`XZRGo-hna9mX}gwx>&;n-M{pZqYsC^))3olqRx5M1~|BD_GGaJEql; zqGE);s>-1b8p_>j#Y=Ju}q zCb6ZZ4~$-Y2ps2;Z`HCW)v$_Kn~bN!p*%)4w;NYi7gRG2t_L`XO&Jh_K0M!1BEYva zRTh+NTF8WVulu8Vbko-pH5Ot(MH_}5d8^3^OYbwt^2MQYjOLMEY? z6?-X;9rL9hTm_0~^uCj=b3Vl)e=Hls-khD2*`C^7MhX zU;-{+Vf_^gvPSwh6xAOm(vF3p%K2OhA!Y?sV(3Q=QhMJ!PeZr3tG3NoTRZ#e$-(kfhh&dm zC_1O0Ck1$5o*cJEy>rRjOs{B={O%Xb`ALdE8>Tp{;_kV_?Ikzh2$q=TAkF@XEUB2%+lEz`M}NJ>D{Uzk-ze@EWGp zQtOtfQE;PYva;UpfOI$Lp;BgWxP^F6nFctbu6(}2m$Kt@$gaMnObR)d&?RSg_V*&d zNny%Oc9Pv)n#Urp9WKmyWI^-f5GPzqXL_(2+y+! ziQ=^kytmxVS#OH9A{esBnh8-+`q_(GdS~c_=4^`3xd74&fOY0DC3WWON1k3&`}|^; zBdLsEXERSfWT_DH(ITz9vuYz)HuF#7wgoKBV z_x6~eHZ9^+7g}hRT!jYrb&T(gVxFdpsowu!7G5JTkt3(wNMxyQK4EJP)X{N}Vz%b6 zRVOl!wq_1C9Q3@PmAm?6+pb&Z$^DU}&?>b2R>Ov&`~pU3CHlS~;84HS2rLM8Zp*gC zH_FZ_)}uOYKDDg+wbbgi+HHA2o9ez0-66N?LhnpphFWe!Xs+8s2gHjnn7q9=Vu~(q zIKNkL%VC1pQw~GMAy}9Wsu`ZBcS9nWBbX5~CD+RKU^$e_QjCsbE$Yt?81q|h!>%7n zC8>Q+9UAd7f*1SjTvap|whT4Nwc`UMzB(8EHZ9Pk+69QpfsN}S@&)3v+DtFj>EFxK zXm?gx`I*^G((y&(q{|QX$&KSeE9@)gPa#hFW9U*dv2qFgny_%^_0no#mJ7cp;v(jIqsFl8 z71|8d@p7g-8<<$Qu3f@)mc24~8Y|C{Xt%lqLu_;ZgtzBeSzhi<$Paq1a7v-{WJ`X_ z*6`@wr}xDw(mxA3IG>??iMx{Ui;({&o`xF;3VJ zngmJ%zSCN6R0ky6*3i1pdI7y?yp}#Y3#u=%)jx{YIKP4Tqk0j?O{FDCA}=GaAwfuC zBu{Vx;mh|O$S$BCXb!ZGxENtXawT0MCG(dJtJ7+t*EVhJVdGzx`049_b!t*0JCohM zblQPT)C-jB2MC$8_FpS~{sNa1LMpX@7{E4w{p+7CQdc%T)gl#@xC*5(P_9xg zzDuMtkR;PvxW{#FYm(kYR0dxf0xka3Ls>hr?VLkOP-r!)UTTS4?LFnZou#IMdAi7i z0!#LWVSBLX`|Jh#LE0P2NG_xRYtpthS~k8}GFoaMio8L&uPIEvM`$4>wjZfz5HoAJezHG^K7bbMJ`kOVWrZ1$Jh8-{h;Mjq2tUcrE5F?) z;q+9M_1#q8?@^+q82@$yZ%N#hF~%Z~eI)~`5mTrnMHOl`kLrEzf+m)vHQaz8k$ zH0#nlHDX&$;LZDuHf= zp6OdZu$HI2 zC(?gJh>or5%{x2bPNZRBVWp`ptx`fvI~YY#0S~os;`njBVnV(A{#h%2wlqX@b5XTf zLYjvTufyTK9fq;lJ%wQc7TgNcec2P@p}W#@TvJiGabk$P(Z@qG0rF;TPIgU~74tD29?Y+GsDh>ga^& zy3R>gIWTT-R8&)!t9b4s=tn|OU)s-w#N+Nv`5*v}7uOyA9YRhmU7s)m5eX}Vnnd^T zH&AV{(>I?L^v?9xJc;7&M~|5Aaj0lK$w0o^Ge11OYp1dLaeT(pI=VB~J+Z!qnA3n_ z4qI1f6y}MtczJ6M$q@#z0bLs&sy8y`PDIDJOv+VL;;d~ zubW*gk>7Y(o|iELlqds%5ag>G;0rBRcXrxnL_?b`n8-$U{zjw|ISO4ZCl-qo;1m*y z^Om2;tn)CL%J;{u^?wDOzDj`eE}4`$?H)4)hG9Vh1`aYWgTVF zuY2pfe@1`cL-%Qzc*seTK%{>VGQ1m6tWzucO1$%4k^}X2aM1uCO>hD0o7(+f$$$2D zTAl?%ck4&(D7I5pd-p75u&4ZOS^n;Z_eU=Ygq#LCXt0(px$H)%U10GQ;#PKW#NH1^Eu$9nv+j=Z#Bnrbg zB#QwL|AFteh-L1bDIAXEiM`kB6e$1hJMvbb2Rv)5mO9w9zR-&{?llqOj__g$pmm~5 zmaCCl=Jw&~_eVUrVx>6T-~gL-s&&EcF=hS{3~iQ&5T7FR!zwitaHFprKbu@Tvix(Y z=`{ouQ)~g=_pjoBpMiT|D~VYW?NxAJ6{H**~8Bi?ja#-QgYl zs|X1tb6_!E1pG+kDIS9i;~L#wbdvDY^U0c(0l>NIyhE?QYvWWw;~1d*lnf{&8$_ z2-HvQo7sRf(*+hWHgmG=-yK_cyMZS4mH;fFFtdVKk|_HXm2s4picxz;*7YenhsQRB zAk~K#UL-082+hQ%d11K&eso^x2lj9*8q{N{ov|`VuF{kj37!_4BkYy1Me-{+4eDFu z&w19TyhWg8Cq^B|4}b=o)yssl-6K^mwM!=jpjobF3G#d(ou|dAm!JUuJLS*MdmLdh z;9Eeb<98C8aKre@malfVYuck-|L8gFICo;P&-?D!35EseWLcZ5cT*2jnuT1_>MNi0 z$WD*Khu*B-trTz&50`Q>q;*KIwqiSe!7#TdN}lZ2Odc}ajS+6W@nijR9G75)2c2^1 zKAoYc3VYLI-hj60$KDzBDTr3nYg71*HBik;sO@UY`(NT671%Y89&%NWe)pvtc#quY zh65|TQXfufyx(3!=~CC_0by^^!sWu9S8Jmc7a|mO`7$cmTslN#?Dm{@0-(5Ykr09C zvDRLz%;%Rpr>1|V3?ZE6pp0dqEwfEhoD87b23SV_7s7cqx7wum)1Np+YO3~lG?lGHYJ~_`+4>1BG%ZseVy*X|#q}nL zWCe^LXSJ|Vtyl!dh4VaP5+61gF`15py>{PNZkG+dRZyHve+oigb9&dawd++d)KyXE z{6~e7tf*|p*1Ix&@D$L9;>iwV$_Yz!b^?M$vu?BS2U*(+KKXrv)L2&&>RA*QaPvek zTflNJrhTEiN`=0sIyUK7aeH@V#gq;d!t(fj<7R56&a6sl0faMsTa`2ky_q03NGRv; zf5&tBC8RTTpYSWO*c1C^7IXTAuq^YF8{72($GJezd@#F-ui>;+kc~VQn7cP}x~Xvg z(S7l3h^pyHl;474vcAK9b*iG5UeT5X+;~II%r>6JXplh;lT29(u$hCETs&&a1aV1& z0}G&+;>J#4k=Q%b?GFeLqK}yG;jmNRbjvE~4>N@>TOcndMp7i8RL|qV$GFadWZjZY z)Lk|)M|_rRXpqHPKznJywHKtU9DD=A84jeOGtIr}YThd!?8uYDfhUZ$ycc-zP{9lkujmKJ38S?bc?^V|QI!BLgec7of3 z0AXojNObtX1tw-yBru58H)@haa}0j<$*K`an56h#I7Hl8q&DRxt&(7tMB|}llw#t- zCzcf_;}E|ma#gc-G8&SfGCY3o_$0}~m&()uVZ27&*No!>c-#G(%3J)2BV^Lgh3Tu8Tx)E% uIduR_{vpkO`59ua{XZ|%e__gQHQZigXp2LscIoef2+-EhS8q^5M*R<|4e5RW literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/Square284x284Logo.png b/desktop/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..7c578ed553729f8e81b1590f5594ab3b7581f523 GIT binary patch literal 7155 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 KMNUMnLSTZ@U?e*L literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/StoreLogo.png b/desktop/src-tauri/icons/StoreLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..03afa12a7b2cb1756b42fa2dcb4e78b3cdfeca90 GIT binary patch literal 894 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 _X}QEx%TW`Ef>q=eE? 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૕mya}XAM#^g0Y|`D`eg!LZ^i^L z5!lSdCc%2)WB|a%V0UTngf8*5tuCSuZdy^Tai*3`B94Rjq)M2lm?!Mi?Q@COOgeh` z3^lwCG?Sj8Z*}eN4FnsbUz-Y71|bCp+wuiF&%fGxOwgQk*ON^mQB$UM8hp0m9Y5nQ zN18618T(+our4zV_Kg^&iCaMy#z4vH#b~zOBjodjX;z%Z5m|3NK{0$gvH4RRP8+IL zf;|&Pl6yKXzmtw-G}QWhmlCdH&gmSOBGi#jo<|(hl^1mp3G0!OLm0_95}Vij3(u_V z-d(M)IcySLwB1#fg2%ErCymNf8tj=Ocn13z>D7;(_I4Kn4EKv<75C!uTR9%T^Aqz7 z#C?4$xj(!s$rUjrFHwfq4|%KrlM9V>|CKA($uJ!R2gEw6R3GRKX*Oh1^%0sXl00Be zu+SHpAu(Y!=(>D=?UjP1`pxh|@kRd(An`z2xdx@!r#=Hks2XY3blEPlVA`rYghC9z zbCfNCSdO2)=;^rwBV4y$Mmz}#wD4F5d8|dNdf+P7!tIHCHqY{y<;Rf>MdC8!WTwi& zp#kp55n$c0_8Hrn|2Q$<6?bAYx}K5br^T!Mz8F1PN<(s&V~d?4llE8|FCzia{k}DZ z^${y7coJszLdL$pZO_ess!t4-D9holjC^(5A)il-!X5}3P#1si$o<`_$S8-`YUoOs zRc|V%&?-r`0W-A`d+~d*Qf%?NGueL33r4Z!76pXya+1;eL&j@zxmV96x$PHepVa0A zDTyj4X?Hbc(U8zxNji0d@$lke?Tv1I0wsc$R0CD^V+y&$f(OQV>#ef=WD_QY(>@npqf&|{>TNIt`&5zeG9`@Ee&)SyXZA32Md1T z_fnQ6d(oq5IMjIY9tedWS`0`vwfma0Z1o>A<|r<-tGV;ydzvB%(W|ke^K*@(|`g~X9io; zB#G_8>QdM)grh^mbPAumTkm9Qz`fU-x=3T2^{Dt~pk6S^!~4jW;N5bTTeUkC`NB?( zvX%F#XvU&8=f2h#0>6*y@Uz5uoxEc$b6h-0!}5tG<&$K-L?RVm$h>hLvDPiD-94YW z_l_Uz&tV{RdlPF27s1dUx*?A*dxpqDW;O@;IwYKqXM-rf9OhC5|nk&0@P!V?g8Roe_ny zlcnWx;+o!Z&HKW!f?}BqAY^%u&(i}B(JuOtiX9i@>Y6B`L-phz=LYk7dJmLRY(KX) zdPu3OK)22a2Ob~g0ru&JNXw*FFKGC35`xAX67`)rKpa~Q@R3-HpR?cYwa5qJ$lbAJ zTD<5xg)8J%--+%MMGI>Lg5c`N?4(7((-~>|P=wmY-uv0UI0OO`>*cgFTU3w>?wHL1NqZia|23xWHYBB6NHmS;Pu#UrSeoa=a|@iV!o7MhsI?t#0Y1RcYqso1BaBnuJU?n+z=NeTNE;Za`0+BMex@W=dE!V;Mf+u
+

OpenCodex

+

Connecting to the OpenCodex proxy…

+ +
+ + + diff --git a/desktop/ui/main.js b/desktop/ui/main.js new file mode 100644 index 0000000000..85c4ea8732 --- /dev/null +++ b/desktop/ui/main.js @@ -0,0 +1,34 @@ +const params = new URLSearchParams(window.location.search); +const port = Number(params.get("port") || "10100"); +const origin = `http://127.0.0.1:${port}`; +const dashboardUrl = `${origin}/#/usage`; +const status = document.querySelector("#status"); +const retry = document.querySelector("#retry"); +let checking = false; + +async function check() { + if (checking) return; + checking = true; + status.textContent = `Connecting to OpenCodex proxy at 127.0.0.1:${port}…`; + retry.disabled = true; + try { + const response = await fetch(`${origin}/healthz`, { + cache: "no-store", + }); + if (response.ok) { + status.textContent = "Proxy is ready. Loading dashboard…"; + window.location.replace(dashboardUrl); + return; + } + throw new Error(`HTTP ${response.status}`); + } catch { + status.textContent = "The proxy is not reachable yet."; + } finally { + checking = false; + retry.disabled = false; + } +} + +retry.addEventListener("click", check); +check(); +setInterval(check, 1500); diff --git a/docs-site/astro.config.mjs b/docs-site/astro.config.mjs index 29a70bd919..2b68b5d457 100644 --- a/docs-site/astro.config.mjs +++ b/docs-site/astro.config.mjs @@ -97,6 +97,7 @@ export default defineConfig({ { label: "Codex Prompt Layers", translations: { fr: "Couches d'invite Codex", ko: "Codex 프롬프트 레이어", "zh-CN": "Codex 提示词层", "zh-TW": "Codex 提示詞層", ru: "Слои промпта Codex", ja: "Codex プロンプトレイヤー", tr: "Codex İstem Katmanları" }, slug: "guides/codex-prompt" }, { label: "Native Context Compatibility", translations: { ko: "네이티브 컨텍스트 호환성" }, slug: "guides/codex-native-context" }, { label: "macOS Menu Bar App", translations: { fr: "Application barre de menus macOS", ko: "macOS 메뉴바 앱", "zh-CN": "macOS 菜单栏应用", "zh-TW": "macOS 選單列 App", ru: "Приложение в строке меню macOS", ja: "macOS メニューバーアプリ", tr: "macOS Menü Çubuğu Uygulaması" }, slug: "guides/macos-menu-bar" }, + { label: "Desktop App", translations: { fr: "Application de bureau", ko: "데스크톱 앱", "zh-CN": "桌面应用", "zh-TW": "桌面 App", ru: "Настольное приложение", ja: "デスクトップアプリ", tr: "Masaüstü Uygulaması" }, slug: "guides/desktop-app" }, { label: "Model Ordering", translations: { fr: "Ordre des modèles", ko: "모델 정렬에 관하여", "zh-CN": "模型排序", "zh-TW": "模型排序", ru: "Сортировка моделей", ja: "モデルの並び順", tr: "Model Sıralaması" }, slug: "guides/model-ordering" }, { label: "Combos", translations: { fr: "Combinaisons", ko: "콤보", "zh-CN": "组合", "zh-TW": "組合", ru: "Комбо", ja: "コンボ", tr: "Kombolar" }, slug: "guides/combos" }, { label: "Claude Code", translations: { fr: "Claude Code", ko: "Claude Code", "zh-CN": "Claude Code", "zh-TW": "Claude Code", ru: "Claude Code", ja: "Claude Code", tr: "Claude Code" }, slug: "guides/claude-code" }, diff --git a/docs-site/src/content/docs/fr/reference/cli/lifecycle.md b/docs-site/src/content/docs/fr/reference/cli/lifecycle.md index c134193f42..f59ebe9916 100644 --- a/docs-site/src/content/docs/fr/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/fr/reference/cli/lifecycle.md @@ -303,6 +303,7 @@ Utilisez `ocx service` pour maintenir un proxy d’arrière-plan toujours actif, ### `ocx tray [--json] [--no-start]` Installe et contrôle l’icône OpenCodex dans la zone de notification Windows. Elle démarre à l’ouverture de session et fournit des commandes du proxy accessibles en un clic. `start` et `stop` contrôlent uniquement l’icône ; utilisez son menu pour contrôler le proxy. `--no-start` s’applique à `install` et installe l’icône sans la lancer immédiatement. +Obsolète : l’application OpenCodex fournit la zone de notification sous Windows, macOS et Linux ; `ocx tray` reste disponible pour les installations sans l’application de bureau. ## Tableau de bord diff --git a/docs-site/src/content/docs/getting-started/installation.md b/docs-site/src/content/docs/getting-started/installation.md index c5a7d36578..9ff7e5a72c 100644 --- a/docs-site/src/content/docs/getting-started/installation.md +++ b/docs-site/src/content/docs/getting-started/installation.md @@ -49,6 +49,19 @@ ocx --version opencodex --version ``` +## Standalone binary (no npm) + +Release downloads also include a standalone `ocx` binary for supported macOS, Linux, and Windows +targets. It includes the Bun runtime and dashboard, so npm, Node, and a separate Bun installation +are not required. Download the archive for your platform, extract it, and run: + +```bash +./ocx --version +./ocx start +``` + +The extracted `gui/dist` directory must stay beside the binary so `GET /` can serve the dashboard. + ### Release channels The stable `latest` channel already includes GPT-5.6 Sol/Terra/Luna catalog support for ChatGPT, diff --git a/docs-site/src/content/docs/getting-started/quickstart.md b/docs-site/src/content/docs/getting-started/quickstart.md index 042eb18304..7734c9f276 100644 --- a/docs-site/src/content/docs/getting-started/quickstart.md +++ b/docs-site/src/content/docs/getting-started/quickstart.md @@ -5,6 +5,11 @@ description: Configure your first provider and route OpenAI Codex through openco This guide takes you from a fresh install to running Codex against a non-OpenAI model. +## Standalone binary (no npm) + +You can also use a release archive containing the `ocx` binary and Bun runtime without npm. +Extract it with its `gui/dist` directory beside the binary, then run `./ocx start`. + ## 1. Run the setup wizard ```bash diff --git a/docs-site/src/content/docs/guides/desktop-app.md b/docs-site/src/content/docs/guides/desktop-app.md new file mode 100644 index 0000000000..c1405fc0c2 --- /dev/null +++ b/docs-site/src/content/docs/guides/desktop-app.md @@ -0,0 +1,84 @@ +--- +title: Desktop App +description: Install and use the OpenCodex desktop app on macOS, Windows, and Linux. +--- + +The OpenCodex desktop app combines a native tray with the web dashboard. It discovers an +existing local proxy, or starts the bundled `ocx` sidecar when no proxy is running. + +The dashboard remains available at [http://127.0.0.1:10100](http://127.0.0.1:10100). +The desktop app does not replace the proxy; it is a local shell around the dashboard and +its bundled runtime. + +## Install + +### macOS + +Download `OpenCodex--macos.dmg` from the +[latest release](https://github.com/lidge-jun/opencodex/releases). Open the DMG and drag +`OpenCodex.app` to Applications. + +On first launch, macOS Gatekeeper may warn that the developer cannot be verified. Right-click +the app, choose **Open**, and confirm **Open**. This build is signed for integrity but is not +yet notarized. + +### Windows + +Download `OpenCodex--windows-x64.msi` and run the installer. Windows SmartScreen may +warn because the installer is not yet code-signed; choose **More info → Run anyway** after +confirming that you downloaded it from the release page. + +### Linux + +Download `OpenCodex--linux-x86_64.AppImage` or +`OpenCodex--linux-amd64.deb` from the release page. + +For the AppImage: + +```bash +chmod +x OpenCodex--linux-x86_64.AppImage +./OpenCodex--linux-x86_64.AppImage +``` + +For Debian-based distributions: + +```bash +sudo apt install ./OpenCodex--linux-amd64.deb +``` + +The tray icon requires an AppIndicator-capable desktop environment. + +## First launch + +The app first looks for an existing `ocx` proxy on loopback, using the runtime port +metadata when available and falling back to port `10100`. If no proxy answers, it starts +the bundled sidecar. The dashboard is then opened inside the app's webview. + +Use the tray's **Open dashboard** or **Open in browser** action to move between the +embedded dashboard and your normal browser. The tray also provides update checks. + +## Updates + +Choose **Check for Updates…** in the tray menu to check immediately. Release builds also +check automatically after startup and every six hours. Updates are verified with the +project's signed updater public key before installation. On macOS, in-app updates download +`OpenCodex--macos.app.tar.gz`; the DMG is for the first installation. +The release manifest is generated only when the updater key secret is configured and then +requires all four platforms to be signed. + +## Widget + +The macOS app includes the OpenCodex WidgetKit extension. See the +[macOS Menu Bar App guide](/opencodex/guides/macos-menu-bar/) for widget setup and the +privacy-safe snapshot details. + +## Uninstall + +On macOS, drag `OpenCodex.app` from Applications to the Trash. On Windows, remove +OpenCodex from **Installed apps**. On Debian-based Linux systems, run: + +```bash +sudo apt remove opencodex +``` + +For an AppImage, delete the downloaded file. diff --git a/docs-site/src/content/docs/guides/macos-menu-bar.md b/docs-site/src/content/docs/guides/macos-menu-bar.md index 209efa7566..c95888a716 100644 --- a/docs-site/src/content/docs/guides/macos-menu-bar.md +++ b/docs-site/src/content/docs/guides/macos-menu-bar.md @@ -9,16 +9,24 @@ per-provider quota pressure, without opening the dashboard. It is a separate application from the proxy. `ocx` keeps running as it always has; the companion is a read-mostly client that talks to the local management API. -## Install +## Desktop app (Tauri) + +The same dashboard can run inside the OpenCodex desktop app. The Usage companion panel +uses the OS selector to show the matching macOS, Windows, or Linux installation steps. +While the dashboard is inside the desktop shell, choose **Open in browser** to open the +current dashboard view in your normal browser. -Download `OpenCodex--macos-universal.zip` from the -[latest release](https://github.com/lidge-jun/opencodex/releases), unzip it, and move -`OpenCodex.app` to your Applications folder. +## Install -Verify the download if you like — every release ships a checksum beside it: +Install the desktop app from the +[latest release](https://github.com/lidge-jun/opencodex/releases). On macOS, download +`OpenCodex--macos.dmg`, open it, and drag `OpenCodex.app` to Applications. +Windows users can run `OpenCodex--windows-x64.msi`; Linux users can use the +AppImage or `OpenCodex--linux-amd64.deb`. ```bash -shasum -a 256 -c OpenCodex--macos-universal.zip.sha256 +chmod +x OpenCodex--linux-x86_64.AppImage +sudo apt install ./OpenCodex--linux-amd64.deb ``` ## First launch: Gatekeeper @@ -104,7 +112,7 @@ Everything else — accounts, model configuration, storage — stays in the dash Add the widget from the desktop: right-click, choose **Edit Widgets**, then add **OpenCodex**. It shows proxy status, today's usage, quota pressure, and the same -privacy-safe usage snapshot as the menu bar app. The widget refreshes when the app polls. +privacy-safe usage snapshot as the desktop app. The widget refreshes when the app polls. It requires macOS 14 or later and reads only the privacy-safe snapshot written by the OpenCodex app; it does not receive API keys or raw account data. @@ -138,11 +146,13 @@ Requires macOS 13 or later, the Xcode Command Line Tools, and [Bun](https://bun. ```bash git clone https://github.com/lidge-jun/opencodex.git cd opencodex -bun run build:macos +bun run prepare-sidecar +bun run prepare-widget +bunx tauri build ``` -The bundle appears at `dist/macos/OpenCodex.app`. Without Bun you can run the script -directly: `bash scripts/build-macos-app.sh`. +The bundle appears in Tauri's release output, with the WidgetKit appex under +`OpenCodex.app/Contents/PlugIns/`. Building a universal binary (`UNIVERSAL=1`) needs the full Xcode toolchain — Command Line Tools ships only current-architecture Swift compatibility libraries, and the build @@ -152,7 +162,7 @@ If you have a Developer ID certificate in your keychain, set `MACOS_SIGN_IDENTIT sign with the hardened runtime instead of ad-hoc: ```bash -MACOS_SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" bun run build:macos +MACOS_SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" bun run prepare-widget ``` ## Uninstall diff --git a/docs-site/src/content/docs/ja/getting-started/installation.md b/docs-site/src/content/docs/ja/getting-started/installation.md index a62daede0a..49b50f7d37 100644 --- a/docs-site/src/content/docs/ja/getting-started/installation.md +++ b/docs-site/src/content/docs/ja/getting-started/installation.md @@ -43,6 +43,19 @@ ocx --version opencodex --version ``` +## スタンドアロンバイナリ(npm 不要) + +リリースには、対応する macOS、Linux、Windows 向けのスタンドアロン `ocx` バイナリも含まれます。 +Bun ランタイムとダッシュボードが含まれるため、npm、Node、別途の Bun インストールは必要ありません。 +お使いの環境向けのアーカイブをダウンロードして展開し、次のように実行します。 + +```bash +./ocx --version +./ocx start +``` + +ダッシュボードを提供するため、展開した `gui/dist` ディレクトリはバイナリの隣に置いたままにしてください。 + ### 配布チャネル 安定チャネルの `latest` にも ChatGPT、OpenAI API キー、OpenRouter、実験段階の Cursor 経路のための diff --git a/docs-site/src/content/docs/ja/getting-started/quickstart.md b/docs-site/src/content/docs/ja/getting-started/quickstart.md index f9184dfc5f..9c2d25a93e 100644 --- a/docs-site/src/content/docs/ja/getting-started/quickstart.md +++ b/docs-site/src/content/docs/ja/getting-started/quickstart.md @@ -5,6 +5,11 @@ description: 最初のプロバイダーを構成し、3 つのコマンドで O このガイドでは、新規インストールから非 OpenAI モデルに対して Codex を実行するまでを説明します。 +## スタンドアロンバイナリ(npm 不要) + +npm を使わず、Bun ランタイムを含むリリースアーカイブの `ocx` バイナリも利用できます。 +`gui/dist` ディレクトリをバイナリの隣に置いて展開し、`./ocx start` を実行してください。 + ## 1. セットアップウィザードを実行します ```bash diff --git a/docs-site/src/content/docs/ja/guides/macos-menu-bar.md b/docs-site/src/content/docs/ja/guides/macos-menu-bar.md index ff67a25ccc..bf429ac0dc 100644 --- a/docs-site/src/content/docs/ja/guides/macos-menu-bar.md +++ b/docs-site/src/content/docs/ja/guides/macos-menu-bar.md @@ -9,18 +9,28 @@ description: OpenCodex プロキシの状態、使用量、プロバイダーの プロキシとは別のアプリケーションです。`ocx` はこれまで通り動作し、メニューバーアプリは ローカルの管理 API に接続するクライアントとして動きます。 -## インストール +## デスクトップアプリ (Tauri) + +同じダッシュボードを OpenCodex デスクトップアプリ内で実行できます。Usage コンパニオン +パネルは OS に合ったインストール手順を表示し、デスクトップシェル内では **ブラウザーで開く** +を選ぶと現在の画面を通常のブラウザーで開けます。 -[リリースページ](https://github.com/lidge-jun/opencodex/releases)から -`OpenCodex--macos-universal.zip` をダウンロードし、展開して `OpenCodex.app` を -アプリケーションフォルダに移動します。 +## インストール -ダウンロードを検証する場合、リリースごとにチェックサムが添付されています。 +基本のインストール方法は OpenCodex デスクトップアプリです。[リリースページ](https://github.com/lidge-jun/opencodex/releases)から、macOS では +`OpenCodex--macos.dmg` をダウンロードし、DMG を開いて `OpenCodex.app` を +アプリケーションフォルダへドラッグします。Windows では +`OpenCodex--windows-x64.msi` を実行し、Linux では AppImage または +`OpenCodex--linux-amd64.deb` を使います。 ```bash -shasum -a 256 -c OpenCodex--macos-universal.zip.sha256 +chmod +x OpenCodex--linux-x86_64.AppImage +sudo apt install ./OpenCodex--linux-amd64.deb ``` +Windows SmartScreen や macOS Gatekeeper の警告が表示されることがあります。アプリは +既存の `ocx` に接続し、見つからなければ同梱のサイドカーを起動します。 + ## 初回起動: Gatekeeper **初回起動はブロックされます。** 次のメッセージが表示されます。 @@ -134,11 +144,13 @@ macOS 13 以降、Xcode Command Line Tools、および [Bun](https://bun.sh) が ```bash git clone https://github.com/lidge-jun/opencodex.git cd opencodex -bun run build:macos +bun run prepare-sidecar +bun run prepare-widget +bunx tauri build ``` -バンドルは `dist/macos/OpenCodex.app` に生成されます。Bun がない場合はスクリプトを直接 -実行できます: `bash scripts/build-macos-app.sh`。 +バンドルは Tauri のリリース出力に生成され、WidgetKit 拡張は +`OpenCodex.app/Contents/PlugIns/` に含まれます。 ユニバーサルバイナリ(`UNIVERSAL=1`)には完全な Xcode が必要です。Command Line Tools には 現在のアーキテクチャ用の Swift 互換ライブラリしか含まれないため、その場合はリンカーエラーでは @@ -148,7 +160,7 @@ bun run build:macos ではなく hardened runtime で署名できます。 ```bash -MACOS_SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" bun run build:macos +MACOS_SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" bun run prepare-widget ``` ## アンインストール diff --git a/docs-site/src/content/docs/ja/reference/cli/lifecycle.md b/docs-site/src/content/docs/ja/reference/cli/lifecycle.md index bf37bc911b..877954e975 100644 --- a/docs-site/src/content/docs/ja/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/ja/reference/cli/lifecycle.md @@ -272,6 +272,7 @@ OpenCodex の更新後、既存の Windows シムにこの動作を適用する ### `ocx tray [--json] [--no-start]` Windows ステータス トレイ アイコンをインストールして制御します。 Windows ログイン時に開始され、ワンクリックでプロキシ コントロールを提供します。 `start` および `stop` はアイコンのみを制御します。そのメニューを使用してプロキシを制御します。 `--no-start` は `install` に適用され、トレイをすぐに起動せずにインストールします。 +非推奨: OpenCodex デスクトップアプリは Windows、macOS、Linux のトレイを提供します。`ocx tray` はデスクトップアプリを使わないインストール向けに残っています。 ## ダッシュボード diff --git a/docs-site/src/content/docs/ko/getting-started/installation.md b/docs-site/src/content/docs/ko/getting-started/installation.md index 70a62f8609..146f25bb01 100644 --- a/docs-site/src/content/docs/ko/getting-started/installation.md +++ b/docs-site/src/content/docs/ko/getting-started/installation.md @@ -43,6 +43,19 @@ ocx --version opencodex --version ``` +## 독립 실행형 바이너리(npm 없음) + +릴리스에는 지원되는 macOS, Linux, Windows용 독립 실행형 `ocx` 바이너리도 포함됩니다. +Bun 런타임과 대시보드가 포함되어 있으므로 npm, Node 또는 별도의 Bun 설치가 필요하지 않습니다. +플랫폼에 맞는 아카이브를 다운로드해 압축을 풀고 다음과 같이 실행하세요. + +```bash +./ocx --version +./ocx start +``` + +대시보드를 제공하려면 압축을 푼 `gui/dist` 디렉터리를 바이너리 옆에 그대로 두어야 합니다. + ### 배포 채널 안정화 채널인 `latest`에도 ChatGPT, OpenAI API 키, OpenRouter, 실험 단계의 Cursor 경로를 위한 diff --git a/docs-site/src/content/docs/ko/getting-started/quickstart.md b/docs-site/src/content/docs/ko/getting-started/quickstart.md index f1a179649b..7e92511d56 100644 --- a/docs-site/src/content/docs/ko/getting-started/quickstart.md +++ b/docs-site/src/content/docs/ko/getting-started/quickstart.md @@ -5,6 +5,11 @@ description: 첫 프로바이더를 설정하고 명령어 세 개로 OpenAI Cod 이 가이드는 새로 설치한 상태에서 OpenAI가 아닌 모델로 Codex를 실행하기까지의 과정을 안내합니다. +## 독립 실행형 바이너리(npm 없음) + +npm 없이 Bun 런타임이 포함된 릴리스 아카이브의 `ocx` 바이너리를 사용할 수도 있습니다. +`gui/dist` 디렉터리를 바이너리 옆에 둔 채 압축을 풀고 `./ocx start`를 실행하세요. + ## 1. 설정 마법사 실행 ```bash diff --git a/docs-site/src/content/docs/ko/guides/macos-menu-bar.md b/docs-site/src/content/docs/ko/guides/macos-menu-bar.md index 53159b4589..6aeb3e7742 100644 --- a/docs-site/src/content/docs/ko/guides/macos-menu-bar.md +++ b/docs-site/src/content/docs/ko/guides/macos-menu-bar.md @@ -9,18 +9,27 @@ description: OpenCodex 프록시 상태와 사용량, 프로바이더 쿼터를 프록시와는 별개의 앱입니다. `ocx`는 지금까지처럼 그대로 돌아가고, 메뉴바 앱은 로컬 관리 API에 붙는 클라이언트입니다. -## 설치 +## 데스크톱 앱 (Tauri) + +같은 대시보드를 OpenCodex 데스크톱 앱에서 실행할 수 있습니다. 사용량 패널은 운영체제에 +맞는 설치 단계를 보여주며, 데스크톱 셸 안에서는 **브라우저에서 열기**를 선택해 현재 +대시보드 화면을 일반 브라우저로 열 수 있습니다. -[릴리스 페이지](https://github.com/lidge-jun/opencodex/releases)에서 -`OpenCodex-<버전>-macos-universal.zip`을 받아 압축을 풀고 `OpenCodex.app`을 응용 -프로그램 폴더로 옮기세요. +## 설치 -받은 파일을 검증하고 싶다면 릴리스마다 체크섬이 함께 올라갑니다. +기본 설치 경로는 OpenCodex 데스크톱 앱입니다. [릴리스 페이지](https://github.com/lidge-jun/opencodex/releases)에서 macOS용 +`OpenCodex-<버전>-macos.dmg`를 내려받아 DMG를 열고 `OpenCodex.app`을 응용 프로그램 +폴더로 드래그하세요. Windows에서는 `OpenCodex-<버전>-windows-x64.msi`를 실행하고, +Linux에서는 AppImage 또는 `OpenCodex-<버전>-linux-amd64.deb`를 사용하세요. ```bash -shasum -a 256 -c OpenCodex-<버전>-macos-universal.zip.sha256 +chmod +x OpenCodex-<버전>-linux-x86_64.AppImage +sudo apt install ./OpenCodex-<버전>-linux-amd64.deb ``` +Windows SmartScreen 또는 macOS Gatekeeper 경고가 표시될 수 있습니다. 앱은 기존 `ocx` +프록시에 연결하고, 찾지 못하면 포함된 사이드카를 시작합니다. + ## 첫 실행: Gatekeeper 차단 **처음 실행하면 macOS가 막습니다.** 이런 메시지가 뜹니다. @@ -130,11 +139,13 @@ macOS 13 이상, Xcode Command Line Tools, 그리고 [Bun](https://bun.sh)이 ```bash git clone https://github.com/lidge-jun/opencodex.git cd opencodex -bun run build:macos +bun run prepare-sidecar +bun run prepare-widget +bunx tauri build ``` -번들은 `dist/macos/OpenCodex.app`에 생깁니다. Bun 없이 쓰려면 스크립트를 직접 실행하세요: -`bash scripts/build-macos-app.sh`. +번들은 Tauri 릴리스 출력에 생성되며, WidgetKit 확장은 +`OpenCodex.app/Contents/PlugIns/` 아래에 포함됩니다. 유니버설 바이너리(`UNIVERSAL=1`)를 만들려면 전체 Xcode가 필요합니다. Command Line Tools 에는 현재 아키텍처용 Swift 호환 라이브러리만 들어 있어서, 이 경우 링커 오류 대신 그 이유를 @@ -144,7 +155,7 @@ bun run build:macos 런타임으로 서명할 수 있습니다. ```bash -MACOS_SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" bun run build:macos +MACOS_SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" bun run prepare-widget ``` ## 삭제 diff --git a/docs-site/src/content/docs/ko/reference/cli/lifecycle.md b/docs-site/src/content/docs/ko/reference/cli/lifecycle.md index e4f2913c6f..97048c3768 100644 --- a/docs-site/src/content/docs/ko/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/ko/reference/cli/lifecycle.md @@ -390,6 +390,8 @@ OpenCodex를 업데이트한 뒤 기존 Windows shim에 이 동작을 적용하 Windows 상태 트레이 아이콘을 설치하고 제어합니다. Windows 로그인 시 시작되며, 프록시를 원클릭으로 제어할 수 있습니다. `start`와 `stop`은 아이콘만 제어합니다. 프록시 제어는 메뉴를 사용하세요. `--no-start`는 `install`에 적용되며, 트레이를 바로 실행하지 않고 설치합니다. +지원 중단 예정: OpenCodex 데스크톱 앱이 Windows, macOS, Linux에서 트레이를 제공합니다. +`ocx tray`는 데스크톱 앱이 없는 설치를 위해 계속 사용할 수 있습니다. ## 대시보드 diff --git a/docs-site/src/content/docs/reference/cli/lifecycle.md b/docs-site/src/content/docs/reference/cli/lifecycle.md index e8380c73ca..07f6ca5853 100644 --- a/docs-site/src/content/docs/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/reference/cli/lifecycle.md @@ -611,6 +611,8 @@ file is not part of the injected `env_key` contract; the launching process must Install and control the Windows status tray icon. It starts at Windows login and provides one-click proxy controls. `start` and `stop` control the icon only; use its menu to control the proxy. `--no-start` applies to `install` and installs the tray without launching it immediately. +Deprecated: the OpenCodex desktop app provides the tray on Windows, macOS, and Linux; `ocx tray` +remains for installs without the desktop app. ## Dashboard diff --git a/docs-site/src/content/docs/ru/getting-started/installation.md b/docs-site/src/content/docs/ru/getting-started/installation.md index a1f3724a4b..ca0fe20b3e 100644 --- a/docs-site/src/content/docs/ru/getting-started/installation.md +++ b/docs-site/src/content/docs/ru/getting-started/installation.md @@ -45,6 +45,19 @@ ocx --version opencodex --version ``` +## Автономный бинарный файл (без npm) + +В релиз входят автономные бинарные файлы `ocx` для поддерживаемых macOS, Linux и Windows. +Они содержат рантайм Bun и дашборд, поэтому npm, Node и отдельная установка Bun не нужны. +Скачайте архив для своей платформы, распакуйте его и выполните: + +```bash +./ocx --version +./ocx start +``` + +Чтобы дашборд был доступен, оставьте распакованный каталог `gui/dist` рядом с бинарным файлом. + ### Каналы релизов Стабильный канал `latest` уже включает поддержку каталога GPT-5.6 Sol/Terra/Luna для маршрутов diff --git a/docs-site/src/content/docs/ru/getting-started/quickstart.md b/docs-site/src/content/docs/ru/getting-started/quickstart.md index 1b086c7db2..088843f0f9 100644 --- a/docs-site/src/content/docs/ru/getting-started/quickstart.md +++ b/docs-site/src/content/docs/ru/getting-started/quickstart.md @@ -5,6 +5,11 @@ description: Настройте первого провайдера и напр Это руководство проводит от чистой установки до запуска Codex с моделью не от OpenAI. +## Автономный бинарный файл (без npm) + +Можно также использовать архив с бинарным файлом `ocx` и рантаймом Bun без npm. +Распакуйте его, оставив каталог `gui/dist` рядом с бинарным файлом, и выполните `./ocx start`. + ## 1. Запустите мастер настройки ```bash diff --git a/docs-site/src/content/docs/ru/guides/macos-menu-bar.md b/docs-site/src/content/docs/ru/guides/macos-menu-bar.md index 30f54ad983..1d264a93c7 100644 --- a/docs-site/src/content/docs/ru/guides/macos-menu-bar.md +++ b/docs-site/src/content/docs/ru/guides/macos-menu-bar.md @@ -9,18 +9,28 @@ description: Нативное приложение, показывающее с Это отдельная программа. `ocx` работает как раньше, а приложение в строке меню — клиент, который обращается к локальному management API. -## Установка +## Настольное приложение (Tauri) + +Ту же панель можно открыть в настольном приложении OpenCodex. Панель компаньона показывает +шаги установки для выбранной ОС, а пункт **Открыть в браузере** открывает текущий экран +в обычном браузере, когда панель работает внутри desktop shell. -Скачайте `OpenCodex-<версия>-macos-universal.zip` со -[страницы релизов](https://github.com/lidge-jun/opencodex/releases), распакуйте и -переместите `OpenCodex.app` в папку «Программы». +## Установка -Если хотите проверить загрузку, к каждому релизу прилагается контрольная сумма: +Основной способ установки — настольное приложение OpenCodex. На +[странице релизов](https://github.com/lidge-jun/opencodex/releases) скачайте для macOS +`OpenCodex-<версия>-macos.dmg`, откройте DMG и перетащите `OpenCodex.app` в «Программы». +В Windows запустите `OpenCodex-<версия>-windows-x64.msi`, а в Linux используйте AppImage +или `OpenCodex-<версия>-linux-amd64.deb`. ```bash -shasum -a 256 -c OpenCodex-<версия>-macos-universal.zip.sha256 +chmod +x OpenCodex-<версия>-linux-x86_64.AppImage +sudo apt install ./OpenCodex-<версия>-linux-amd64.deb ``` +Windows SmartScreen и macOS Gatekeeper могут показать предупреждение. Приложение подключается +к существующему `ocx`, а если его нет — запускает встроенный sidecar. + ## Первый запуск: Gatekeeper **Первый запуск будет заблокирован.** macOS покажет: @@ -136,11 +146,13 @@ xattr -d com.apple.quarantine /Applications/OpenCodex.app ```bash git clone https://github.com/lidge-jun/opencodex.git cd opencodex -bun run build:macos +bun run prepare-sidecar +bun run prepare-widget +bunx tauri build ``` -Бандл появится в `dist/macos/OpenCodex.app`. Без Bun скрипт можно запустить напрямую: -`bash scripts/build-macos-app.sh`. +Бандл появится в выходных файлах Tauri, а расширение WidgetKit будет включено в +`OpenCodex.app/Contents/PlugIns/`. Для универсального бинарника (`UNIVERSAL=1`) нужен полный Xcode: в Command Line Tools есть только библиотеки совместимости Swift для текущей архитектуры, и сборка сообщит об этом @@ -150,7 +162,7 @@ bun run build:macos подписать с hardened runtime вместо ad-hoc: ```bash -MACOS_SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" bun run build:macos +MACOS_SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" bun run prepare-widget ``` ## Удаление diff --git a/docs-site/src/content/docs/ru/reference/cli/lifecycle.md b/docs-site/src/content/docs/ru/reference/cli/lifecycle.md index f338d75388..6719e9cf2c 100644 --- a/docs-site/src/content/docs/ru/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/ru/reference/cli/lifecycle.md @@ -395,6 +395,8 @@ ocx codex-shim uninstall one-click управление прокси. `start` и `stop` управляют только иконкой; самим прокси нужно управлять из её меню. `--no-start` применяется к `install` и устанавливает tray, не запуская её немедленно. +Устарело: приложение OpenCodex для рабочего стола предоставляет трей в Windows, macOS и Linux; +`ocx tray` остаётся для установок без приложения для рабочего стола. ## Дашборд diff --git a/docs-site/src/content/docs/tr/reference/cli/lifecycle.md b/docs-site/src/content/docs/tr/reference/cli/lifecycle.md index ffa5b19df5..f1c3039260 100644 --- a/docs-site/src/content/docs/tr/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/tr/reference/cli/lifecycle.md @@ -470,6 +470,8 @@ Windows durum tepsisi simgesini kurun ve kontrol edin. Windows oturum açılış başlar ve tek tıklamayla proxy kontrolleri sağlar. `start` ve `stop` yalnızca simgeyi kontrol eder; proxy'yi kontrol etmek için menüsünü kullanın. `--no-start`, `install` için geçerlidir ve tepsiyi hemen başlatmadan kurar. +Kullanımdan kaldırıldı: OpenCodex masaüstü uygulaması Windows, macOS ve Linux'ta tepsi sağlar; +`ocx tray`, masaüstü uygulaması olmayan kurulumlar için kullanılmaya devam eder. ## Kontrol Paneli diff --git a/docs-site/src/content/docs/zh-cn/getting-started/installation.md b/docs-site/src/content/docs/zh-cn/getting-started/installation.md index eb5b02ec94..335debd845 100644 --- a/docs-site/src/content/docs/zh-cn/getting-started/installation.md +++ b/docs-site/src/content/docs/zh-cn/getting-started/installation.md @@ -42,6 +42,18 @@ ocx --version opencodex --version ``` +## 独立二进制文件(无需 npm) + +发布包还包含适用于 macOS、Linux 和 Windows 的独立 `ocx` 二进制文件。 +它内置 Bun 运行时和仪表盘,因此无需安装 npm、Node 或单独的 Bun。下载适合你平台的压缩包,解压后运行: + +```bash +./ocx --version +./ocx start +``` + +为了让仪表盘可用,请将解压后的 `gui/dist` 目录保留在二进制文件旁边。 + ### 发布渠道 稳定的 `latest` 渠道已经包含 ChatGPT、OpenAI API key、OpenRouter 以及实验性 Cursor 路由所需的 diff --git a/docs-site/src/content/docs/zh-cn/getting-started/quickstart.md b/docs-site/src/content/docs/zh-cn/getting-started/quickstart.md index e965b159a6..91ac4ba047 100644 --- a/docs-site/src/content/docs/zh-cn/getting-started/quickstart.md +++ b/docs-site/src/content/docs/zh-cn/getting-started/quickstart.md @@ -5,6 +5,11 @@ description: 配置你的第一个 provider,并在三条命令内让 OpenAI Co 本指南将带你从全新安装,一路走到用一个非 OpenAI 模型运行 Codex。 +## 独立二进制文件(无需 npm) + +你也可以使用包含 Bun 运行时的发布压缩包中的 `ocx`,无需 npm。 +解压时将 `gui/dist` 目录保留在二进制文件旁边,然后运行 `./ocx start`。 + ## 1. 运行设置向导 ```bash diff --git a/docs-site/src/content/docs/zh-cn/guides/macos-menu-bar.md b/docs-site/src/content/docs/zh-cn/guides/macos-menu-bar.md index 50058524bf..2f3dc02616 100644 --- a/docs-site/src/content/docs/zh-cn/guides/macos-menu-bar.md +++ b/docs-site/src/content/docs/zh-cn/guides/macos-menu-bar.md @@ -7,17 +7,26 @@ description: 在菜单栏中查看 OpenCodex 代理状态、用量和各提供 它与代理是两个独立的程序。`ocx` 照常运行,菜单栏应用只是连接本地管理 API 的客户端。 -## 安装 +## 桌面应用(Tauri) + +同一个仪表板也可以在 OpenCodex 桌面应用中运行。用量面板会显示匹配操作系统的安装步骤; +在桌面壳中选择**在浏览器中打开**,即可在普通浏览器中打开当前页面。 -从[发布页面](https://github.com/lidge-jun/opencodex/releases)下载 -`OpenCodex--macos-universal.zip`,解压后把 `OpenCodex.app` 移到「应用程序」文件夹。 +## 安装 -如果需要校验下载文件,每个版本都附带校验和: +推荐使用 OpenCodex 桌面应用安装。从[发布页面](https://github.com/lidge-jun/opencodex/releases)下载 macOS 的 +`OpenCodex--macos.dmg`,打开 DMG 后将 `OpenCodex.app` 拖到「应用程序」文件夹。 +Windows 运行 `OpenCodex--windows-x64.msi`,Linux 使用 AppImage 或 +`OpenCodex--linux-amd64.deb`。 ```bash -shasum -a 256 -c OpenCodex--macos-universal.zip.sha256 +chmod +x OpenCodex--linux-x86_64.AppImage +sudo apt install ./OpenCodex--linux-amd64.deb ``` +Windows SmartScreen 或 macOS Gatekeeper 可能显示警告。应用会连接现有的 `ocx` 代理; +找不到代理时则启动内置 sidecar。 + ## 首次启动:Gatekeeper **首次启动会被阻止。** macOS 会提示: @@ -117,11 +126,13 @@ xattr -d com.apple.quarantine /Applications/OpenCodex.app ```bash git clone https://github.com/lidge-jun/opencodex.git cd opencodex -bun run build:macos +bun run prepare-sidecar +bun run prepare-widget +bunx tauri build ``` -程序包会生成在 `dist/macos/OpenCodex.app`。若没有 Bun,可以直接运行脚本: -`bash scripts/build-macos-app.sh`。 +程序包会生成在 Tauri 的发布输出中,WidgetKit 扩展位于 +`OpenCodex.app/Contents/PlugIns/`。 构建通用二进制(`UNIVERSAL=1`)需要完整的 Xcode。Command Line Tools 只包含当前架构的 Swift 兼容库,此时构建会给出说明信息,而不是抛出链接器错误。 @@ -130,7 +141,7 @@ bun run build:macos 替代 ad-hoc 签名: ```bash -MACOS_SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" bun run build:macos +MACOS_SIGN_IDENTITY="Developer ID Application: Your Name (TEAMID)" bun run prepare-widget ``` ## 卸载 diff --git a/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md b/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md index 30687e2b81..b1fdef059a 100644 --- a/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md @@ -259,6 +259,7 @@ ocx codex-shim uninstall ### `ocx tray [--json] [--no-start]` 安装并控制 Windows 状态托盘图标。它会在 Windows 登录时启动,并提供一键代理控制。`start` 和 `stop` 只控制图标本身;要控制代理,请使用其菜单。`--no-start` 适用于 `install`,会安装托盘但不会立即启动。 +已弃用:OpenCodex 桌面应用在 Windows、macOS 和 Linux 上提供托盘;没有桌面应用的安装仍可使用 `ocx tray`。 ## 仪表盘 diff --git a/docs-site/src/content/docs/zh-tw/reference/cli/lifecycle.md b/docs-site/src/content/docs/zh-tw/reference/cli/lifecycle.md index d9c104d3c9..4317ef37b3 100644 --- a/docs-site/src/content/docs/zh-tw/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/zh-tw/reference/cli/lifecycle.md @@ -244,6 +244,7 @@ ocx codex-shim uninstall ### `ocx tray [--json] [--no-start]` 安裝並控制 Windows 狀態列圖示。它在 Windows 登入時啟動並提供一鍵代理控制。`start` 與 `stop` 僅控制圖示;請用其選單控制代理。`--no-start` 適用於 `install`,並在不立即啟動它的情況下安裝 tray。 +已淘汰:OpenCodex 桌面應用程式在 Windows、macOS 與 Linux 提供系統匣;沒有桌面應用程式的安裝仍可使用 `ocx tray`。 ## 儀表板 diff --git a/gui/src/App.tsx b/gui/src/App.tsx index b7ec8f9db0..c53975f682 100644 --- a/gui/src/App.tsx +++ b/gui/src/App.tsx @@ -24,6 +24,7 @@ import { readModelsTab, type ModelsTab } from "./pages/models-tab"; import { useAppRouteState } from "./use-app-route-state"; import { requestProxyStop } from "./stop-proxy"; import { useCodexRestart } from "./use-codex-restart"; +import { isDesktopShell, isExternalLink } from "./lib/desktop-shell"; type Theme = "light" | "dark" | "system"; @@ -172,6 +173,23 @@ export default function App() { }; }, []); + useEffect(() => { + if (!isDesktopShell()) return; + const interceptExternalLinks = (event: MouseEvent) => { + const target = event.target; + if (!(target instanceof Element)) return; + const anchor = target.closest("a[href]"); + if (!(anchor instanceof HTMLAnchorElement)) return; + const href = anchor.href; + if (!isExternalLink(href)) return; + event.preventDefault(); + // Rust denies external HTTP(S) navigation and opens it in the system browser. + window.location.assign(href); + }; + document.addEventListener("click", interceptExternalLinks, true); + return () => document.removeEventListener("click", interceptExternalLinks, true); + }, []); + useEffect(() => { const el = document.documentElement; if (theme === "system") { el.removeAttribute("data-theme"); localStorage.removeItem(THEME_KEY); } diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 0ed0d69611..c09ccbbb5e 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -983,7 +983,7 @@ export const de: Record = { "usage.section.coverage": "Abdeckungs-Aufschlüsselung", "usage.section.companion": "Menüleiste & Widget", "usage.companion.title": "Menüleiste & Widget", - "usage.companion.description": "Diese Einstellungen steuern die OpenCodex-Menüleisten-App und ihr Widget.", + "usage.companion.description": "Diese Einstellungen steuern die OpenCodex Desktop-/Menüleisten-App und ihr Widget.", "usage.companion.installGuide": "Installationsanleitung", "usage.companion.loading": "Zeitachse wird geladen…", "usage.companion.timelineUnavailable": "Zeitachse nicht verfügbar", @@ -994,13 +994,26 @@ export const de: Record = { "usage.companion.corrupt": "Die Datei mit den Begleiteinstellungen ist beschädigt. Die Steuerelemente zeigen Standardwerte; das Speichern ist pausiert, bis Sie die Datei ersetzen.", "usage.companion.corruptReset": "Durch Standardwerte ersetzen", "usage.companion.connected": "Menüleisten-App verbunden · {age}", - "usage.companion.installTitle": "Menüleisten-App installieren", - "usage.companion.installStep1": "Laden Sie OpenCodex--macos-universal.zip aus der neuesten Veröffentlichung herunter und ziehen Sie OpenCodex.app in Programme.", - "usage.companion.installStep2": "Erster Start: Klicken Sie mit der rechten Maustaste auf OpenCodex.app → Öffnen (die App ist nur ad-hoc signiert, daher fragt Gatekeeper einmal).", - "usage.companion.installStep3": "Die App findet diesen Proxy selbst; das Widget erscheint in der Widget-Galerie, sobald die App ausgeführt wurde.", + "usage.companion.connectedDesktop": "Desktop-App verbunden · {age}", + "usage.companion.installTitle": "Desktop-App installieren", + "usage.companion.installOs": "Betriebssystem", + "usage.companion.osMac": "macOS", + "usage.companion.osWindows": "Windows", + "usage.companion.osLinux": "Linux", + "usage.companion.runningInDesktop": "Du bist in der OpenCodex-Desktop-App {version}", + "usage.companion.openInBrowser": "Im Browser öffnen", + "usage.companion.installMacStep1": "Laden Sie OpenCodex--macos.dmg aus der neuesten Veröffentlichung herunter und ziehen Sie OpenCodex.app in Programme.", + "usage.companion.installMacStep2": "Erster Start: Klicken Sie mit der rechten Maustaste auf OpenCodex.app → Öffnen (Gatekeeper fragt einmal, bis die App notariell signiert ist).", + "usage.companion.installMacStep3": "Die App findet diesen Proxy selbst; das Widget erscheint in der Widget-Galerie, sobald die App ausgeführt wurde.", + "usage.companion.installWinStep1": "Laden Sie OpenCodex--windows-x64.msi aus der neuesten Veröffentlichung herunter und führen Sie es aus.", + "usage.companion.installWinStep2": "Wenn SmartScreen warnt, wählen Sie Weitere Informationen → Trotzdem ausführen (der Installer ist noch nicht signiert).", + "usage.companion.installWinStep3": "OpenCodex erscheint in der Taskleiste und verbindet sich mit diesem Proxy oder startet den gebündelten Proxy.", + "usage.companion.installLinuxStep1": "Laden Sie OpenCodex--linux-x86_64.AppImage (oder die .deb-Datei) aus der neuesten Veröffentlichung herunter.", + "usage.companion.installLinuxStep2": "Führen Sie chmod +x für das AppImage aus und starten Sie es; ein Tray-Symbol benötigt einen AppIndicator-fähigen Desktop.", + "usage.companion.installLinuxStep3": "Die App verbindet sich mit diesem Proxy oder startet den gebündelten Proxy.", "usage.companion.notConnected": "Noch keine Menüleisten-App hat sich mit diesem Proxy verbunden.", "usage.companion.lastSeen": "Zuletzt gesehen {age}", - "usage.companion.installAnother": "Auf einem anderen Mac installieren", + "usage.companion.installAnother": "Auf einem anderen Gerät installieren", "usage.companion.saved": "Gespeichert · {time}", "usage.companion.saveFailed": "Speichern fehlgeschlagen: {error}", "usage.companion.reset": "Auf Standardwerte zurücksetzen", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 29202551e7..2c5b9f92a0 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -1036,7 +1036,7 @@ export const en = { "usage.section.coverage": "Coverage breakdown", "usage.section.companion": "Menu bar & widget", "usage.companion.title": "Menu bar & widget", - "usage.companion.description": "Settings here drive the OpenCodex menu bar app and its widget.", + "usage.companion.description": "Settings here drive the OpenCodex desktop/menu bar app and its widget.", "usage.companion.installGuide": "Install guide", "usage.companion.loading": "Loading timeline…", "usage.companion.timelineUnavailable": "Timeline unavailable", @@ -1047,13 +1047,26 @@ export const en = { "usage.companion.corrupt": "The companion settings file is corrupt. Controls show defaults; saving is paused until you replace the file.", "usage.companion.corruptReset": "Replace with defaults", "usage.companion.connected": "Menu bar app connected · {age}", - "usage.companion.installTitle": "Install the menu bar app", - "usage.companion.installStep1": "Download OpenCodex--macos-universal.zip from the latest release and drag OpenCodex.app to Applications.", - "usage.companion.installStep2": "First launch: right-click OpenCodex.app → Open (the app is ad-hoc signed, so Gatekeeper asks once).", - "usage.companion.installStep3": "The app finds this proxy on its own; the widget appears in the widget gallery once the app has run.", + "usage.companion.connectedDesktop": "Desktop app connected · {age}", + "usage.companion.installTitle": "Install the desktop app", + "usage.companion.installOs": "Operating system", + "usage.companion.osMac": "macOS", + "usage.companion.osWindows": "Windows", + "usage.companion.osLinux": "Linux", + "usage.companion.runningInDesktop": "You’re in the OpenCodex desktop app {version}", + "usage.companion.openInBrowser": "Open in browser", + "usage.companion.installMacStep1": "Download OpenCodex--macos.dmg from the latest release and drag OpenCodex.app to Applications.", + "usage.companion.installMacStep2": "First launch: right-click OpenCodex.app → Open (Gatekeeper asks once until the app is notarized).", + "usage.companion.installMacStep3": "The app finds this proxy on its own; the widget appears in the widget gallery once the app has run.", + "usage.companion.installWinStep1": "Download OpenCodex--windows-x64.msi from the latest release and run it.", + "usage.companion.installWinStep2": "If SmartScreen warns, choose More info → Run anyway (the installer is not yet code-signed).", + "usage.companion.installWinStep3": "OpenCodex appears in the system tray and attaches to this proxy, or starts its bundled one.", + "usage.companion.installLinuxStep1": "Download OpenCodex--linux-x86_64.AppImage (or the .deb) from the latest release.", + "usage.companion.installLinuxStep2": "chmod +x the AppImage and run it; a tray icon requires an AppIndicator-capable desktop.", + "usage.companion.installLinuxStep3": "The app attaches to this proxy, or starts its bundled one.", "usage.companion.notConnected": "No menu bar app has connected to this proxy yet.", "usage.companion.lastSeen": "Last seen {age}", - "usage.companion.installAnother": "Install on another Mac", + "usage.companion.installAnother": "Install on another device", "usage.companion.saved": "Saved · {time}", "usage.companion.saveFailed": "Couldn’t save: {error}", "usage.companion.reset": "Reset to defaults", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 715be09c70..348914135d 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -1013,7 +1013,7 @@ export const fr: Record = { "usage.section.coverage": "Répartition de la couverture", "usage.section.companion": "Barre des menus et widget", "usage.companion.title": "Barre des menus et widget", - "usage.companion.description": "Ces réglages contrôlent l’app OpenCodex de la barre des menus et son widget.", + "usage.companion.description": "Ces réglages pilotent l’application OpenCodex de bureau/barre des menus et son widget.", "usage.companion.installGuide": "Guide d’installation", "usage.companion.loading": "Chargement de la chronologie…", "usage.companion.timelineUnavailable": "Chronologie indisponible", @@ -1064,13 +1064,26 @@ export const fr: Record = { "usage.companion.placeholders": "Paramètres substituables :", "usage.companion.modelsOnChart": "Modèles du graphique", "usage.companion.connected": "App de barre des menus connectée · {age}", - "usage.companion.installTitle": "Installer l’app de barre des menus", - "usage.companion.installStep1": "Téléchargez OpenCodex--macos-universal.zip depuis la dernière version et faites glisser OpenCodex.app dans Applications.", - "usage.companion.installStep2": "Premier lancement : faites un clic droit sur OpenCodex.app → Ouvrir (l’app est signée ad hoc, Gatekeeper ne demande donc qu’une confirmation).", - "usage.companion.installStep3": "L’app trouve ce proxy automatiquement ; le widget apparaît dans la galerie de widgets après le lancement de l’app.", + "usage.companion.connectedDesktop": "Application de bureau connectée · {age}", + "usage.companion.installTitle": "Installer l’application de bureau", + "usage.companion.installOs": "Système d’exploitation", + "usage.companion.osMac": "Mac", + "usage.companion.osWindows": "Système Windows", + "usage.companion.osLinux": "Système Linux", + "usage.companion.runningInDesktop": "Vous êtes dans l’application de bureau OpenCodex {version}", + "usage.companion.openInBrowser": "Ouvrir dans le navigateur", + "usage.companion.installMacStep1": "Téléchargez OpenCodex--macos.dmg depuis la dernière version et faites glisser OpenCodex.app dans Applications.", + "usage.companion.installMacStep2": "Premier lancement : faites un clic droit sur OpenCodex.app → Ouvrir (Gatekeeper demande une confirmation jusqu’à la notarisation).", + "usage.companion.installMacStep3": "L’application trouve ce proxy automatiquement ; le widget apparaît dans la galerie après son lancement.", + "usage.companion.installWinStep1": "Téléchargez OpenCodex--windows-x64.msi depuis la dernière version et exécutez-le.", + "usage.companion.installWinStep2": "Si SmartScreen vous avertit, choisissez Informations supplémentaires → Exécuter quand même (l’installateur n’est pas encore signé).", + "usage.companion.installWinStep3": "OpenCodex apparaît dans la zone de notification et se connecte à ce proxy, ou démarre celui fourni.", + "usage.companion.installLinuxStep1": "Téléchargez OpenCodex--linux-x86_64.AppImage (ou le .deb) depuis la dernière version.", + "usage.companion.installLinuxStep2": "Exécutez chmod +x sur l’AppImage puis lancez-le ; une icône de zone de notification nécessite un bureau compatible AppIndicator.", + "usage.companion.installLinuxStep3": "L’application se connecte à ce proxy ou démarre celui fourni.", "usage.companion.notConnected": "Aucune app de barre des menus ne s’est encore connectée à ce proxy.", "usage.companion.lastSeen": "Dernière connexion {age}", - "usage.companion.installAnother": "Installer sur un autre Mac", + "usage.companion.installAnother": "Installer sur un autre appareil", "usage.companion.modelsCount": "{selected} sur {total} dans le graphique", "usage.companion.modelsShowAll": "Tout afficher", "usage.companion.hideProviders": "Masquer les fournisseurs", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index d4eef7d803..d5920d7015 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -948,7 +948,7 @@ export const ja: Record = { "usage.section.coverage": "カバレッジ内訳", "usage.section.companion": "メニューバーとウィジェット", "usage.companion.title": "メニューバーとウィジェット", - "usage.companion.description": "ここでの設定は OpenCodex のメニューバーアプリとウィジェットを制御します。", + "usage.companion.description": "ここでの設定は OpenCodex デスクトップ/メニューバーアプリとウィジェットを制御します。", "usage.companion.installGuide": "インストールガイド", "usage.companion.loading": "タイムラインを読み込み中…", "usage.companion.timelineUnavailable": "タイムラインを利用できません", @@ -999,13 +999,26 @@ export const ja: Record = { "usage.companion.placeholders": "プレースホルダー:", "usage.companion.modelsOnChart": "グラフのモデル", "usage.companion.connected": "メニューバーアプリ接続済み · {age}", - "usage.companion.installTitle": "メニューバーアプリをインストール", - "usage.companion.installStep1": "最新リリースから OpenCodex--macos-universal.zip をダウンロードし、OpenCodex.app をアプリケーションに移動します。", - "usage.companion.installStep2": "初回起動:OpenCodex.app を右クリックして「開く」を選択します(アドホック署名のため、Gatekeeper の確認は一度だけです)。", - "usage.companion.installStep3": "アプリはこのプロキシを自動検出します。アプリを一度起動するとウィジェットギャラリーに表示されます。", + "usage.companion.connectedDesktop": "デスクトップアプリ接続済み · {age}", + "usage.companion.installTitle": "デスクトップアプリをインストール", + "usage.companion.installOs": "オペレーティングシステム", + "usage.companion.osMac": "macOS", + "usage.companion.osWindows": "Windows", + "usage.companion.osLinux": "Linux", + "usage.companion.runningInDesktop": "OpenCodex デスクトップアプリ {version} を使用中です", + "usage.companion.openInBrowser": "ブラウザで開く", + "usage.companion.installMacStep1": "最新リリースから OpenCodex--macos.dmg をダウンロードし、OpenCodex.app をアプリケーションへ移動します。", + "usage.companion.installMacStep2": "初回起動:OpenCodex.app を右クリックして「開く」を選択します(公証されるまで Gatekeeper が一度確認します)。", + "usage.companion.installMacStep3": "アプリはこのプロキシを自動検出します。アプリを起動するとウィジェットギャラリーに表示されます。", + "usage.companion.installWinStep1": "最新リリースから OpenCodex--windows-x64.msi をダウンロードして実行します。", + "usage.companion.installWinStep2": "SmartScreen が警告したら「詳細情報」→「実行」を選択します(インストーラーはまだコード署名されていません)。", + "usage.companion.installWinStep3": "OpenCodex はシステムトレイに表示され、このプロキシに接続するか、同梱のプロキシを起動します。", + "usage.companion.installLinuxStep1": "最新リリースから OpenCodex--linux-x86_64.AppImage(または .deb)をダウンロードします。", + "usage.companion.installLinuxStep2": "AppImage に chmod +x を実行して起動します。トレイアイコンには AppIndicator 対応デスクトップが必要です。", + "usage.companion.installLinuxStep3": "アプリはこのプロキシに接続するか、同梱のプロキシを起動します。", "usage.companion.notConnected": "このプロキシに接続したメニューバーアプリはまだありません。", "usage.companion.lastSeen": "最終接続 {age}", - "usage.companion.installAnother": "別の Mac にインストール", + "usage.companion.installAnother": "別のデバイスにインストール", "usage.companion.modelsCount": "{selected} / {total} がグラフに表示中", "usage.companion.modelsShowAll": "すべて表示", "usage.companion.hideProviders": "プロバイダーを非表示", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index b2e8b94c50..6913c90317 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -1017,7 +1017,7 @@ export const ko: Record = { "usage.section.coverage": "커버리지 상세", "usage.section.companion": "메뉴 막대 및 위젯", "usage.companion.title": "메뉴 막대 및 위젯", - "usage.companion.description": "여기 설정은 OpenCodex 메뉴 막대 앱과 위젯을 제어합니다.", + "usage.companion.description": "이 설정은 OpenCodex 데스크톱/메뉴 막대 앱과 위젯을 제어합니다.", "usage.companion.installGuide": "설치 안내", "usage.companion.loading": "타임라인 로드 중…", "usage.companion.timelineUnavailable": "타임라인을 사용할 수 없습니다", @@ -1068,13 +1068,26 @@ export const ko: Record = { "usage.companion.placeholders": "자리표시자:", "usage.companion.modelsOnChart": "차트의 모델", "usage.companion.connected": "메뉴 막대 앱 연결됨 · {age}", - "usage.companion.installTitle": "메뉴 막대 앱 설치", - "usage.companion.installStep1": "최신 릴리스에서 OpenCodex--macos-universal.zip을 다운로드하고 OpenCodex.app을 응용 프로그램으로 드래그하세요.", - "usage.companion.installStep2": "첫 실행: OpenCodex.app을 마우스 오른쪽 버튼으로 클릭하고 열기를 선택하세요(앱이 애드혹 서명되어 Gatekeeper가 한 번 확인합니다).", - "usage.companion.installStep3": "앱이 이 프록시를 자동으로 찾습니다. 앱을 실행하면 위젯 갤러리에 위젯이 표시됩니다.", + "usage.companion.connectedDesktop": "데스크톱 앱 연결됨 · {age}", + "usage.companion.installTitle": "데스크톱 앱 설치", + "usage.companion.installOs": "운영 체제", + "usage.companion.osMac": "macOS", + "usage.companion.osWindows": "Windows", + "usage.companion.osLinux": "Linux", + "usage.companion.runningInDesktop": "OpenCodex 데스크톱 앱 {version}에서 실행 중입니다", + "usage.companion.openInBrowser": "브라우저에서 열기", + "usage.companion.installMacStep1": "최신 릴리스에서 OpenCodex--macos.dmg를 다운로드하고 OpenCodex.app을 응용 프로그램으로 드래그하세요.", + "usage.companion.installMacStep2": "첫 실행: OpenCodex.app을 마우스 오른쪽 버튼으로 클릭하고 열기를 선택하세요(공증될 때까지 Gatekeeper가 한 번 확인합니다).", + "usage.companion.installMacStep3": "앱이 이 프록시를 자동으로 찾으며, 앱을 실행하면 위젯 갤러리에 위젯이 표시됩니다.", + "usage.companion.installWinStep1": "최신 릴리스에서 OpenCodex--windows-x64.msi를 다운로드하고 실행하세요.", + "usage.companion.installWinStep2": "SmartScreen 경고가 표시되면 추가 정보 → 실행을 선택하세요(설치 프로그램은 아직 코드 서명되지 않았습니다).", + "usage.companion.installWinStep3": "OpenCodex가 시스템 트레이에 나타나 이 프록시에 연결하거나 번들 프록시를 시작합니다.", + "usage.companion.installLinuxStep1": "최신 릴리스에서 OpenCodex--linux-x86_64.AppImage(또는 .deb)를 다운로드하세요.", + "usage.companion.installLinuxStep2": "AppImage에 chmod +x를 실행하고 시작하세요. 트레이 아이콘에는 AppIndicator를 지원하는 데스크톱이 필요합니다.", + "usage.companion.installLinuxStep3": "앱이 이 프록시에 연결하거나 번들 프록시를 시작합니다.", "usage.companion.notConnected": "아직 이 프록시에 연결한 메뉴 막대 앱이 없습니다.", "usage.companion.lastSeen": "마지막 연결 {age}", - "usage.companion.installAnother": "다른 Mac에 설치", + "usage.companion.installAnother": "다른 기기에 설치", "usage.companion.modelsCount": "{selected} / {total}개가 차트에 표시됨", "usage.companion.modelsShowAll": "모두 표시", "usage.companion.hideProviders": "공급자 숨기기", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index af9681a2d8..c0002fc2ac 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -1004,7 +1004,7 @@ export const ru: Record = { "usage.section.coverage": "Детализация покрытия", "usage.section.companion": "Строка меню и виджет", "usage.companion.title": "Строка меню и виджет", - "usage.companion.description": "Эти настройки управляют приложением OpenCodex в строке меню и его виджетом.", + "usage.companion.description": "Эти настройки управляют настольным приложением/приложением в строке меню OpenCodex и его виджетом.", "usage.companion.installGuide": "Руководство по установке", "usage.companion.loading": "Загрузка временной шкалы…", "usage.companion.timelineUnavailable": "Временная шкала недоступна", @@ -1055,13 +1055,26 @@ export const ru: Record = { "usage.companion.placeholders": "Заполнители:", "usage.companion.modelsOnChart": "Модели на графике", "usage.companion.connected": "Приложение в строке меню подключено · {age}", - "usage.companion.installTitle": "Установить приложение в строке меню", - "usage.companion.installStep1": "Скачайте OpenCodex--macos-universal.zip из последнего релиза и перетащите OpenCodex.app в Программы.", - "usage.companion.installStep2": "Первый запуск: нажмите OpenCodex.app правой кнопкой и выберите «Открыть» (приложение подписано ad-hoc, поэтому Gatekeeper спросит один раз).", - "usage.companion.installStep3": "Приложение само найдёт этот прокси; виджет появится в галерее виджетов после запуска приложения.", + "usage.companion.connectedDesktop": "Настольное приложение подключено · {age}", + "usage.companion.installTitle": "Установить настольное приложение", + "usage.companion.installOs": "Операционная система", + "usage.companion.osMac": "macOS", + "usage.companion.osWindows": "Windows", + "usage.companion.osLinux": "Linux", + "usage.companion.runningInDesktop": "Вы находитесь в настольном приложении OpenCodex {version}", + "usage.companion.openInBrowser": "Открыть в браузере", + "usage.companion.installMacStep1": "Скачайте OpenCodex--macos.dmg из последнего релиза и перетащите OpenCodex.app в Программы.", + "usage.companion.installMacStep2": "Первый запуск: нажмите OpenCodex.app правой кнопкой и выберите «Открыть» (до нотариальной заверки Gatekeeper спросит один раз).", + "usage.companion.installMacStep3": "Приложение само найдёт этот прокси; после запуска приложение появится в галерее виджетов.", + "usage.companion.installWinStep1": "Скачайте OpenCodex--windows-x64.msi из последнего релиза и запустите его.", + "usage.companion.installWinStep2": "Если SmartScreen предупредит, выберите Подробнее → Всё равно запустить (установщик ещё не подписан).", + "usage.companion.installWinStep3": "OpenCodex появится в системном трее и подключится к этому прокси или запустит встроенный.", + "usage.companion.installLinuxStep1": "Скачайте OpenCodex--linux-x86_64.AppImage (или .deb) из последнего релиза.", + "usage.companion.installLinuxStep2": "Выполните chmod +x для AppImage и запустите его; для значка в трее нужен рабочий стол с поддержкой AppIndicator.", + "usage.companion.installLinuxStep3": "Приложение подключится к этому прокси или запустит встроенный.", "usage.companion.notConnected": "К этому прокси ещё не подключалось приложение из строки меню.", "usage.companion.lastSeen": "Последнее подключение: {age}", - "usage.companion.installAnother": "Установить на другом Mac", + "usage.companion.installAnother": "Установить на другом устройстве", "usage.companion.modelsCount": "{selected} из {total} на графике", "usage.companion.modelsShowAll": "Показать все", "usage.companion.hideProviders": "Скрыть провайдеров", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index db62526abb..528387f39d 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -1023,7 +1023,7 @@ export const tr: Record = { "usage.section.coverage": "Kapsam dağılımı", "usage.section.companion": "Menü çubuğu ve widget", "usage.companion.title": "Menü çubuğu ve widget", - "usage.companion.description": "Buradaki ayarlar OpenCodex menü çubuğu uygulamasını ve widget'ını yönetir.", + "usage.companion.description": "Buradaki ayarlar OpenCodex masaüstü/menü çubuğu uygulamasını ve widget’ını yönetir.", "usage.companion.installGuide": "Kurulum rehberi", "usage.companion.loading": "Zaman çizelgesi yükleniyor…", "usage.companion.timelineUnavailable": "Zaman çizelgesi kullanılamıyor", @@ -1074,13 +1074,26 @@ export const tr: Record = { "usage.companion.placeholders": "Yer tutucular:", "usage.companion.modelsOnChart": "Grafikteki modeller", "usage.companion.connected": "Menü çubuğu uygulaması bağlı · {age}", - "usage.companion.installTitle": "Menü çubuğu uygulamasını yükle", - "usage.companion.installStep1": "En son sürümden OpenCodex--macos-universal.zip dosyasını indirin ve OpenCodex.app'i Uygulamalar'a sürükleyin.", - "usage.companion.installStep2": "İlk çalıştırma: OpenCodex.app'e sağ tıklayıp Aç'ı seçin (uygulama ad-hoc imzalıdır; Gatekeeper bir kez sorar).", - "usage.companion.installStep3": "Uygulama bu proxy'yi kendisi bulur; uygulama çalıştıktan sonra widget, widget galerisinde görünür.", + "usage.companion.connectedDesktop": "Masaüstü uygulaması bağlı · {age}", + "usage.companion.installTitle": "Masaüstü uygulamasını yükle", + "usage.companion.installOs": "İşletim sistemi", + "usage.companion.osMac": "macOS", + "usage.companion.osWindows": "Windows", + "usage.companion.osLinux": "Linux", + "usage.companion.runningInDesktop": "OpenCodex masaüstü uygulaması {version} içindesiniz", + "usage.companion.openInBrowser": "Tarayıcıda aç", + "usage.companion.installMacStep1": "En son sürümden OpenCodex--macos.dmg dosyasını indirin ve OpenCodex.app’i Uygulamalar’a sürükleyin.", + "usage.companion.installMacStep2": "İlk çalıştırma: OpenCodex.app’e sağ tıklayıp Aç’ı seçin (uygulama not edilene kadar Gatekeeper bir kez sorar).", + "usage.companion.installMacStep3": "Uygulama bu proxy’yi kendisi bulur; uygulama çalıştıktan sonra widget galeride görünür.", + "usage.companion.installWinStep1": "En son sürümden OpenCodex--windows-x64.msi dosyasını indirin ve çalıştırın.", + "usage.companion.installWinStep2": "SmartScreen uyarırsa Daha fazla bilgi → Yine de çalıştır seçeneğini seçin (yükleyici henüz kod imzalı değil).", + "usage.companion.installWinStep3": "OpenCodex sistem tepsisinde görünür ve bu proxy’ye bağlanır veya paketlenmiş olanı başlatır.", + "usage.companion.installLinuxStep1": "En son sürümden OpenCodex--linux-x86_64.AppImage (veya .deb) dosyasını indirin.", + "usage.companion.installLinuxStep2": "AppImage için chmod +x çalıştırıp başlatın; tepsi simgesi AppIndicator destekli bir masaüstü gerektirir.", + "usage.companion.installLinuxStep3": "Uygulama bu proxy’ye bağlanır veya paketlenmiş olanı başlatır.", "usage.companion.notConnected": "Bu proxy'ye henüz hiçbir menü çubuğu uygulaması bağlanmadı.", "usage.companion.lastSeen": "Son görülme {age}", - "usage.companion.installAnother": "Başka bir Mac'e yükle", + "usage.companion.installAnother": "Başka bir cihaza yükle", "usage.companion.modelsCount": "Grafikte {selected}/{total}", "usage.companion.modelsShowAll": "Tümünü göster", "usage.companion.hideProviders": "Sağlayıcıları gizle", diff --git a/gui/src/i18n/vi.ts b/gui/src/i18n/vi.ts index 749fde103c..337335fdbd 100644 --- a/gui/src/i18n/vi.ts +++ b/gui/src/i18n/vi.ts @@ -1006,7 +1006,7 @@ export const vi: Record = { "usage.section.coverage": "Chi tiết độ phủ (Coverage breakdown)", "usage.section.companion": "Thanh menu và widget", "usage.companion.title": "Thanh menu và widget", - "usage.companion.description": "Các cài đặt ở đây điều khiển ứng dụng thanh menu OpenCodex và widget.", + "usage.companion.description": "Các cài đặt ở đây điều khiển ứng dụng máy tính/thanh menu OpenCodex và widget của ứng dụng.", "usage.companion.installGuide": "Hướng dẫn cài đặt", "usage.companion.loading": "Đang tải dòng thời gian…", "usage.companion.timelineUnavailable": "Không có dòng thời gian", @@ -1057,13 +1057,26 @@ export const vi: Record = { "usage.companion.placeholders": "Trình giữ chỗ:", "usage.companion.modelsOnChart": "Mô hình trên biểu đồ", "usage.companion.connected": "Ứng dụng trên thanh menu đã kết nối · {age}", - "usage.companion.installTitle": "Cài đặt ứng dụng trên thanh menu", - "usage.companion.installStep1": "Tải OpenCodex--macos-universal.zip từ bản phát hành mới nhất và kéo OpenCodex.app vào Applications.", - "usage.companion.installStep2": "Lần đầu mở: nhấp chuột phải vào OpenCodex.app → Mở (ứng dụng được ký ad-hoc nên Gatekeeper chỉ hỏi một lần).", - "usage.companion.installStep3": "Ứng dụng tự tìm proxy này; widget sẽ xuất hiện trong thư viện widget sau khi ứng dụng chạy.", + "usage.companion.connectedDesktop": "Ứng dụng máy tính đã kết nối · {age}", + "usage.companion.installTitle": "Cài đặt ứng dụng máy tính", + "usage.companion.installOs": "Hệ điều hành", + "usage.companion.osMac": "macOS", + "usage.companion.osWindows": "Windows", + "usage.companion.osLinux": "Linux", + "usage.companion.runningInDesktop": "Bạn đang ở trong ứng dụng máy tính OpenCodex {version}", + "usage.companion.openInBrowser": "Mở trong trình duyệt", + "usage.companion.installMacStep1": "Tải OpenCodex--macos.dmg từ bản phát hành mới nhất và kéo OpenCodex.app vào Applications.", + "usage.companion.installMacStep2": "Lần đầu mở: nhấp chuột phải vào OpenCodex.app → Mở (Gatekeeper sẽ hỏi một lần cho đến khi ứng dụng được công chứng).", + "usage.companion.installMacStep3": "Ứng dụng tự tìm proxy này; widget xuất hiện trong thư viện widget sau khi ứng dụng chạy.", + "usage.companion.installWinStep1": "Tải OpenCodex--windows-x64.msi từ bản phát hành mới nhất và chạy trình cài đặt.", + "usage.companion.installWinStep2": "Nếu SmartScreen cảnh báo, chọn Thông tin thêm → Vẫn chạy (trình cài đặt chưa được ký mã).", + "usage.companion.installWinStep3": "OpenCodex xuất hiện trong khay hệ thống và kết nối proxy này, hoặc khởi chạy proxy đi kèm.", + "usage.companion.installLinuxStep1": "Tải OpenCodex--linux-x86_64.AppImage (hoặc .deb) từ bản phát hành mới nhất.", + "usage.companion.installLinuxStep2": "Chạy chmod +x cho AppImage rồi mở; biểu tượng khay cần môi trường desktop hỗ trợ AppIndicator.", + "usage.companion.installLinuxStep3": "Ứng dụng kết nối proxy này hoặc khởi chạy proxy đi kèm.", "usage.companion.notConnected": "Chưa có ứng dụng trên thanh menu nào kết nối với proxy này.", "usage.companion.lastSeen": "Lần kết nối gần nhất {age}", - "usage.companion.installAnother": "Cài đặt trên máy Mac khác", + "usage.companion.installAnother": "Cài đặt trên thiết bị khác", "usage.companion.modelsCount": "{selected}/{total} trên biểu đồ", "usage.companion.modelsShowAll": "Hiện tất cả", "usage.companion.hideProviders": "Ẩn nhà cung cấp", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index 6201840100..28a7650bc1 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -814,7 +814,7 @@ export const zhTW: Record = { "usage.section.coverage": "覆蓋率明細", "usage.section.companion": "選單列與小工具", "usage.companion.title": "選單列與小工具", - "usage.companion.description": "這裡的設定會控制 OpenCodex 選單列 App 與其小工具。", + "usage.companion.description": "這裡的設定會控制 OpenCodex 桌面/選單列 App 與其小工具。", "usage.companion.installGuide": "安裝指南", "usage.companion.loading": "正在載入時間軸…", "usage.companion.timelineUnavailable": "時間軸無法使用", @@ -865,13 +865,26 @@ export const zhTW: Record = { "usage.companion.placeholders": "預留位置:", "usage.companion.modelsOnChart": "圖表中的模型", "usage.companion.connected": "選單列 App 已連線 · {age}", - "usage.companion.installTitle": "安裝選單列 App", - "usage.companion.installStep1": "從最新版本下載 OpenCodex--macos-universal.zip,並將 OpenCodex.app 拖到應用程式。", - "usage.companion.installStep2": "首次啟動:在 OpenCodex.app 上按右鍵並選擇「打開」(App 使用臨時簽章,因此 Gatekeeper 只會詢問一次)。", - "usage.companion.installStep3": "App 會自動找到此 Proxy;App 執行後,Widget 會出現在 Widget 圖庫中。", + "usage.companion.connectedDesktop": "桌面 App 已連線 · {age}", + "usage.companion.installTitle": "安裝桌面 App", + "usage.companion.installOs": "作業系統", + "usage.companion.osMac": "macOS 系統", + "usage.companion.osWindows": "Windows 系統", + "usage.companion.osLinux": "Linux 系統", + "usage.companion.runningInDesktop": "你正在使用 OpenCodex 桌面 App {version}", + "usage.companion.openInBrowser": "在瀏覽器中開啟", + "usage.companion.installMacStep1": "從最新版本下載 OpenCodex--macos.dmg,並將 OpenCodex.app 拖到應用程式。", + "usage.companion.installMacStep2": "首次啟動:在 OpenCodex.app 上按右鍵並選擇「開啟」(App 完成公證前,Gatekeeper 只會詢問一次)。", + "usage.companion.installMacStep3": "App 會自動找到此 Proxy;App 執行後,Widget 會出現在 Widget 圖庫中。", + "usage.companion.installWinStep1": "從最新版本下載 OpenCodex--windows-x64.msi 並執行。", + "usage.companion.installWinStep2": "如果 SmartScreen 發出警告,請選擇「更多資訊」→「仍要執行」(安裝程式尚未完成程式碼簽署)。", + "usage.companion.installWinStep3": "OpenCodex 會出現在系統匣並連線此 Proxy,或啟動內建 Proxy。", + "usage.companion.installLinuxStep1": "從最新版本下載 OpenCodex--linux-x86_64.AppImage(或 .deb)。", + "usage.companion.installLinuxStep2": "對 AppImage 執行 chmod +x 後啟動;系統匣圖示需要支援 AppIndicator 的桌面環境。", + "usage.companion.installLinuxStep3": "App 會連線此 Proxy,或啟動內建 Proxy。", "usage.companion.notConnected": "尚未有選單列 App 連線到此 Proxy。", "usage.companion.lastSeen": "上次連線 {age}", - "usage.companion.installAnother": "在另一台 Mac 上安裝", + "usage.companion.installAnother": "在另一台裝置上安裝", "usage.companion.modelsCount": "圖表顯示 {selected}/{total}", "usage.companion.modelsShowAll": "顯示全部", "usage.companion.hideProviders": "隱藏提供者", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 17a93d78de..7caef60437 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -998,7 +998,7 @@ export const zh: Record = { "usage.section.coverage": "覆盖率明细", "usage.section.companion": "菜单栏与小组件", "usage.companion.title": "菜单栏与小组件", - "usage.companion.description": "此处设置会控制 OpenCodex 菜单栏应用及其小组件。", + "usage.companion.description": "这里的设置会控制 OpenCodex 桌面/菜单栏应用及其小组件。", "usage.companion.installGuide": "安装指南", "usage.companion.loading": "正在加载时间线…", "usage.companion.timelineUnavailable": "时间线不可用", @@ -1049,13 +1049,26 @@ export const zh: Record = { "usage.companion.placeholders": "占位符:", "usage.companion.modelsOnChart": "图表中的模型", "usage.companion.connected": "菜单栏应用已连接 · {age}", - "usage.companion.installTitle": "安装菜单栏应用", - "usage.companion.installStep1": "从最新版本下载 OpenCodex--macos-universal.zip,并将 OpenCodex.app 拖到应用程序。", - "usage.companion.installStep2": "首次启动:右键点击 OpenCodex.app 并选择“打开”(应用使用临时签名,因此 Gatekeeper 只会询问一次)。", - "usage.companion.installStep3": "应用会自动找到此代理;应用运行后,小组件会出现在小组件图库中。", + "usage.companion.connectedDesktop": "桌面应用已连接 · {age}", + "usage.companion.installTitle": "安装桌面应用", + "usage.companion.installOs": "操作系统", + "usage.companion.osMac": "macOS", + "usage.companion.osWindows": "Windows", + "usage.companion.osLinux": "Linux", + "usage.companion.runningInDesktop": "你正在使用 OpenCodex 桌面应用 {version}", + "usage.companion.openInBrowser": "在浏览器中打开", + "usage.companion.installMacStep1": "从最新版本下载 OpenCodex--macos.dmg,并将 OpenCodex.app 拖到应用程序。", + "usage.companion.installMacStep2": "首次启动:右键点击 OpenCodex.app 并选择“打开”(应用完成公证前,Gatekeeper 只会询问一次)。", + "usage.companion.installMacStep3": "应用会自动找到此代理;应用运行后,小组件会出现在小组件图库中。", + "usage.companion.installWinStep1": "从最新版本下载 OpenCodex--windows-x64.msi 并运行。", + "usage.companion.installWinStep2": "如果 SmartScreen 发出警告,请选择“更多信息”→“仍要运行”(安装程序尚未经过代码签名)。", + "usage.companion.installWinStep3": "OpenCodex 会出现在系统托盘中并连接此代理,或启动内置代理。", + "usage.companion.installLinuxStep1": "从最新版本下载 OpenCodex--linux-x86_64.AppImage(或 .deb)。", + "usage.companion.installLinuxStep2": "对 AppImage 执行 chmod +x 后运行;托盘图标需要支持 AppIndicator 的桌面环境。", + "usage.companion.installLinuxStep3": "应用会连接此代理,或启动内置代理。", "usage.companion.notConnected": "尚未有菜单栏应用连接到此代理。", "usage.companion.lastSeen": "上次连接 {age}", - "usage.companion.installAnother": "在另一台 Mac 上安装", + "usage.companion.installAnother": "在另一台设备上安装", "usage.companion.modelsCount": "图表显示 {selected}/{total}", "usage.companion.modelsShowAll": "显示全部", "usage.companion.hideProviders": "隐藏提供商", diff --git a/gui/src/lib/desktop-shell.ts b/gui/src/lib/desktop-shell.ts new file mode 100644 index 0000000000..f6301ee119 --- /dev/null +++ b/gui/src/lib/desktop-shell.ts @@ -0,0 +1,32 @@ +export type HostOs = "macos" | "windows" | "linux" | "unknown"; + +function currentUserAgent(): string { + return typeof navigator === "undefined" ? "" : navigator.userAgent; +} + +export function desktopShellVersion(ua = currentUserAgent()): string | null { + return ua.match(/OpenCodexDesktop\/(\S+)/)?.[1] ?? null; +} + +export function isDesktopShell(ua = currentUserAgent()): boolean { + return desktopShellVersion(ua) !== null; +} + +export function hostOs(ua = currentUserAgent()): HostOs { + if (/Windows/i.test(ua)) return "windows"; + if (/Mac OS X|Macintosh/i.test(ua)) return "macos"; + if (/Linux|X11/i.test(ua) && !/Android/i.test(ua)) return "linux"; + return "unknown"; +} + +export function isExternalLink( + href: string, + origin = typeof location === "undefined" ? "" : location.origin, +): boolean { + if (!/^https?:\/\//i.test(href)) return false; + try { + return new URL(href).origin !== origin; + } catch { + return false; + } +} diff --git a/gui/src/pages/usage-companion-panel.tsx b/gui/src/pages/usage-companion-panel.tsx index f3af2082de..44dd4f2dff 100644 --- a/gui/src/pages/usage-companion-panel.tsx +++ b/gui/src/pages/usage-companion-panel.tsx @@ -1,8 +1,9 @@ import { useCallback, useEffect, useMemo, useRef, useState, type RefObject } from "react"; -import { useI18n } from "../i18n/shared"; +import { useI18n, type TFn, type TKey } from "../i18n/shared"; import { relativeTimeLabelsFromT, formatRelativeTime } from "../provider-workspace/usage"; import { Switch } from "../ui"; import { UsageCompanionChart } from "./usage-companion-chart"; +import { desktopShellVersion, hostOs, isDesktopShell, type HostOs } from "../lib/desktop-shell"; import { bucketMinutesForWindow, buildCompanionSettingsPatch, @@ -24,6 +25,50 @@ const CHART_STYLES = ["line", "stackedBar"] as const; const TOKEN_METRICS = ["total", "input", "output", "cached"] as const; const AGGREGATIONS = ["sum", "average", "max"] as const; const GROUPINGS = ["model", "modelAccount"] as const; +const MENU_METRIC_KEYS: Record<(typeof MENU_METRICS)[number], TKey> = { + requests: "usage.companion.menuRequests", + tokens: "usage.companion.menuTokens", + cost: "usage.companion.menuCost", + quota: "usage.companion.menuQuota", + none: "usage.companion.menuNone", +}; +const WINDOW_KEYS: Record<(typeof WINDOWS)[number], TKey> = { + 6: "usage.companion.window6", + 24: "usage.companion.window24", + 72: "usage.companion.window72", + 168: "usage.companion.window168", +}; +const TOKEN_METRIC_KEYS: Record<(typeof TOKEN_METRICS)[number], TKey> = { + total: "usage.companion.metricTotal", + input: "usage.companion.metricInput", + output: "usage.companion.metricOutput", + cached: "usage.companion.metricCached", +}; +const SECTION_OPTIONS = [ + ["showToday", "usage.companion.sectionToday"], + ["showChart", "usage.companion.sectionChart"], + ["showModels", "usage.companion.sectionModels"], + ["showCost", "usage.companion.sectionCost"], + ["showAccounts", "usage.companion.sectionAccounts"], +] as const; +const AGGREGATION_KEYS: Record<(typeof AGGREGATIONS)[number], TKey> = { + sum: "usage.companion.aggregationSum", + average: "usage.companion.aggregationAverage", + max: "usage.companion.aggregationMax", +}; +type InstallOs = Exclude; + +const INSTALL_STEP_KEYS: Record = { + macos: ["usage.companion.installMacStep1", "usage.companion.installMacStep2", "usage.companion.installMacStep3"], + windows: ["usage.companion.installWinStep1", "usage.companion.installWinStep2", "usage.companion.installWinStep3"], + linux: ["usage.companion.installLinuxStep1", "usage.companion.installLinuxStep2", "usage.companion.installLinuxStep3"], +}; + +const OS_LABEL_KEYS: Record = { + macos: "usage.companion.osMac", + windows: "usage.companion.osWindows", + linux: "usage.companion.osLinux", +}; function formatSaveTime(value: number, locale: string): string { return new Intl.DateTimeFormat(locale, { hour: "2-digit", minute: "2-digit" }).format(value); @@ -34,6 +79,26 @@ function errorMessage(value: unknown): string { return String(value); } +function OsSelector({ + value, + onChange, + t, +}: { + value: InstallOs; + onChange: (value: InstallOs) => void; + t: TFn; +}) { + return ( +
+ {(Object.keys(OS_LABEL_KEYS) as InstallOs[]).map(os => ( + + ))} +
+ ); +} + function Segment({ label, value, @@ -129,6 +194,10 @@ export default function UsageCompanionPanel({ const settingsRef = useRef(settings); const knownTotalsRef = useRef(new Map()); const [knownTotals, setKnownTotals] = useState>(new Map()); + const [installOs, setInstallOs] = useState(() => { + const detected = hostOs(); + return detected === "unknown" ? "macos" : detected; + }); useEffect(() => { saveStateRef.current = saveState; @@ -275,6 +344,20 @@ export default function UsageCompanionPanel({ } }, [apiBase, loadSettings]); + const openInBrowser = useCallback(async () => { + try { + const path = location.hash ? `/${location.hash}` : "/#/usage"; + const result = await fetch(`${apiBase}/api/companion/open-in-browser`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ path }), + }); + if (!result.ok) throw new Error(`${result.status} ${result.statusText}`.trim()); + } catch (error) { + setSaveError(errorMessage(error)); + } + }, [apiBase]); + if (settingsError) { return

{t("usage.companion.settingsUnavailable")}

; } @@ -307,29 +390,51 @@ export default function UsageCompanionPanel({ const lastSeenAt = response?.companion?.lastSeenAt ?? null; const connected = lastSeenAt !== null && fetchedAt !== null && fetchedAt - lastSeenAt <= 10 * 60 * 1000; const age = lastSeenAt === null || fetchedAt === null ? "" : formatRelativeTime(lastSeenAt, relativeTimeLabelsFromT(t), fetchedAt); + const shell = isDesktopShell(); + const shellVersion = desktopShellVersion() ?? __APP_VERSION__; + const stepKeys = INSTALL_STEP_KEYS[installOs]; const steps = (
    -
  1. {t("usage.companion.installStep1")} {t("common.github")}
  2. -
  3. {t("usage.companion.installStep2")}
  4. -
  5. {t("usage.companion.installStep3")}
  6. +
  7. {t(stepKeys[0])} {t("common.github")}
  8. +
  9. {t(stepKeys[1])}
  10. +
  11. {t(stepKeys[2])}
); + const installCommand = installOs === "macos" + ? xattr -d com.apple.quarantine /Applications/OpenCodex.app + : installOs === "linux" + ? chmod +x OpenCodex-*.AppImage + : null; + const installGuidance = ( +
+ {t("usage.companion.installAnother")} + + {steps} + {installCommand} +
+ ); + if (shell) { + return ( +
+

{t("usage.companion.runningInDesktop", { version: shellVersion })}

+ + {installGuidance} +
+ ); + } return connected ? (
-
-
- {t("usage.companion.installAnother")} - {steps} - xattr -d com.apple.quarantine /Applications/OpenCodex.app -
+
+ {installGuidance}
) : (
{t("usage.companion.installTitle")} {lastSeenAt !== null &&

{t("usage.companion.lastSeen", { age })}

} {lastSeenAt === null &&

{t("usage.companion.notConnected")}

} + {steps} - xattr -d com.apple.quarantine /Applications/OpenCodex.app + {installCommand}
); })()} @@ -378,30 +483,24 @@ export default function UsageCompanionPanel({ }
- t(`usage.companion.menu${value[0]!.toUpperCase()}${value.slice(1)}` as never)} onChange={value => updateSettings({ menuBarMetric: value })} /> - t(`usage.companion.window${value}` as never)} onChange={value => updateSettings({ chartHours: value, bucketMinutes: bucketMinutesForWindow(value) })} /> + t(MENU_METRIC_KEYS[value])} onChange={value => updateSettings({ menuBarMetric: value })} /> + t(WINDOW_KEYS[value])} onChange={value => updateSettings({ chartHours: value, bucketMinutes: bucketMinutesForWindow(value) })} /> value === "line" ? t("usage.companion.styleLine") : t("usage.companion.styleStacked")} onChange={value => updateSettings({ chartStyle: value })} /> - t(`usage.companion.metric${value[0]!.toUpperCase()}${value.slice(1)}` as never)} onChange={value => updateSettings({ tokenMetric: value })} /> + t(TOKEN_METRIC_KEYS[value])} onChange={value => updateSettings({ tokenMetric: value })} /> value === "model" ? t("usage.companion.groupModel") : t("usage.companion.groupAccount")} onChange={value => updateSettings({ chartGrouping: value })} />
{t("usage.companion.popoverSections")} - {([ - ["showToday", "today"], - ["showChart", "chart"], - ["showModels", "models"], - ["showCost", "cost"], - ["showAccounts", "accounts"], - ] as const).map(([key, label]) => ( + {SECTION_OPTIONS.map(([key, label]) => (
- {t(`usage.companion.section${label[0]!.toUpperCase()}${label.slice(1)}` as never)} - + {t(label)} +
))}
{t("usage.companion.advanced")}
- t(`usage.companion.aggregation${value[0]!.toUpperCase()}${value.slice(1)}` as never)} onChange={value => updateSettings({ aggregation: value })} /> + t(AGGREGATION_KEYS[value])} onChange={value => updateSettings({ aggregation: value })} />

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 B$)*4R literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/icon.ico b/desktop/src-tauri/icons/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..78c4c7833545da805e10394696bd5732dfdba926 GIT binary patch literal 11591 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 Z?Dq?7Sz|7_xbNx$`K*0rS$5A=W{{8C@=?7ir- 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 LNkvXXu0mjfF|b_g literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/src/auth.rs b/desktop/src-tauri/src/auth.rs new file mode 100644 index 0000000000..81a16467b0 --- /dev/null +++ b/desktop/src-tauri/src/auth.rs @@ -0,0 +1,31 @@ +use std::path::PathBuf; + +#[derive(Clone, Debug)] +pub struct Auth { + home: PathBuf, + environment_token: Option, +} + +impl Auth { + pub fn new(home: PathBuf) -> Self { + Self { + home, + environment_token: std::env::var("OPENCODEX_ADMIN_AUTH_TOKEN") + .ok() + .filter(|value| !value.is_empty()), + } + } + + pub fn token(&self) -> Option { + self.environment_token.clone().or_else(|| { + std::fs::read_to_string(self.home.join("admin-api-token")) + .ok() + .map(|value| value.trim().to_owned()) + .filter(|value| !value.is_empty()) + }) + } + + pub fn user_agent() -> &'static str { + concat!("OpenCodexDesktop/", env!("CARGO_PKG_VERSION")) + } +} diff --git a/desktop/src-tauri/src/discovery.rs b/desktop/src-tauri/src/discovery.rs new file mode 100644 index 0000000000..2eaded758d --- /dev/null +++ b/desktop/src-tauri/src/discovery.rs @@ -0,0 +1,116 @@ +use serde::Deserialize; +use std::path::{Path, PathBuf}; + +pub const DEFAULT_PORT: u16 = 10100; +const HOME_ENV: &str = "OPENCODEX_HOME"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ProxyEndpoint { + pub host: &'static str, + pub port: u16, +} + +impl ProxyEndpoint { + pub fn url(&self, path: &str) -> String { + format!("http://{}:{}{}", self.host, self.port, path) + } +} + +#[derive(Debug, Deserialize)] +struct RuntimePort { + port: u16, +} + +pub fn config_directory(environment: impl Fn(&str) -> Option, home: &Path) -> PathBuf { + environment(HOME_ENV) + .map(|value| value.trim().to_owned()) + .filter(|value| !value.is_empty()) + .map(|value| expand_tilde(PathBuf::from(value), home)) + .unwrap_or_else(|| home.join(".opencodex")) +} + +pub fn resolve(environment: impl Fn(&str) -> Option, home: &Path) -> ProxyEndpoint { + let directory = config_directory(environment, home); + let path = directory.join("runtime-port.json"); + let port = std::fs::read(&path) + .ok() + .and_then(|bytes| serde_json::from_slice::(&bytes).ok()) + .map(|record| record.port) + .filter(|port| (1..=u16::MAX).contains(port)) + .unwrap_or(DEFAULT_PORT); + ProxyEndpoint { + host: "127.0.0.1", + port, + } +} + +fn expand_tilde(path: PathBuf, home: &Path) -> PathBuf { + if path == Path::new("~") { + return home.to_path_buf(); + } + path.strip_prefix("~/") + .map(|rest| home.join(rest)) + .unwrap_or(path) +} + +pub fn current() -> (ProxyEndpoint, PathBuf) { + let home = dirs_home(); + let directory = config_directory(|key| std::env::var(key).ok(), &home); + let endpoint = resolve(|key| std::env::var(key).ok(), &home); + (endpoint, directory) +} + +fn dirs_home() -> PathBuf { + std::env::var_os("HOME") + .map(PathBuf::from) + .or_else(|| std::env::var_os("USERPROFILE").map(PathBuf::from)) + .unwrap_or_else(|| PathBuf::from(".")) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::{create_dir_all, write}; + + #[test] + fn resolves_home_override_and_runtime_port() { + let root = std::env::temp_dir().join(format!("ocx-discovery-{}", std::process::id())); + let home = root.join("home"); + let custom = home.join("custom"); + create_dir_all(&custom).unwrap(); + write( + custom.join("runtime-port.json"), + r#"{"pid":1,"port":12345}"#, + ) + .unwrap(); + let endpoint = resolve(|key| (key == HOME_ENV).then(|| "~/custom".into()), &home); + assert_eq!( + endpoint, + ProxyEndpoint { + host: "127.0.0.1", + port: 12345 + } + ); + let _ = std::fs::remove_dir_all(root); + } + + #[test] + fn any_failure_falls_back_to_default() { + let home = std::env::temp_dir().join("ocx-missing-home"); + let endpoint = resolve(|_| None, &home); + assert_eq!(endpoint.port, DEFAULT_PORT); + } + + #[test] + fn empty_override_and_invalid_port_use_default() { + let root = + std::env::temp_dir().join(format!("ocx-discovery-invalid-{}", std::process::id())); + let home = root.join("home"); + let directory = root.join("custom"); + create_dir_all(&directory).unwrap(); + write(directory.join("runtime-port.json"), r#"{"port":0}"#).unwrap(); + let endpoint = resolve(|key| (key == HOME_ENV).then(|| " ".into()), &home); + assert_eq!(endpoint.port, DEFAULT_PORT); + let _ = std::fs::remove_dir_all(root); + } +} diff --git a/desktop/src-tauri/src/formatting.rs b/desktop/src-tauri/src/formatting.rs new file mode 100644 index 0000000000..5f940b68fe --- /dev/null +++ b/desktop/src-tauri/src/formatting.rs @@ -0,0 +1,79 @@ +pub fn tokens(value: Option) -> String { + abbreviate(value, true) +} + +pub fn count(value: Option) -> String { + abbreviate(value, false) +} + +pub fn cost(value: Option) -> String { + let Some(value) = value else { + return "—".into(); + }; + if value < 1_000.0 { + return format!("${value:.2}"); + } + format!("${}", abbreviate_float(value, false)) +} + +fn abbreviate(value: Option, integer: bool) -> String { + let Some(value) = value else { + return "—".into(); + }; + if !integer && value < 10_000 { + return format!("{value}"); + } + if integer && value < 1_000 { + return format!("{value}"); + } + abbreviate_float(value as f64, integer) +} + +fn abbreviate_float(value: f64, integer: bool) -> String { + let units = [ + (1_000_000_000_000.0, "T"), + (1_000_000_000.0, "B"), + (1_000_000.0, "M"), + (1_000.0, "K"), + ]; + for (threshold, suffix) in units { + if value >= threshold * 0.9995 { + let scaled = value / threshold; + let decimals = if integer || scaled >= 100.0 { + 0 + } else if scaled >= 10.0 { + 1 + } else { + 2 + }; + let rendered = format!("{scaled:.decimals$}"); + return format!( + "{}{}", + rendered.trim_end_matches('0').trim_end_matches('.'), + suffix + ); + } + } + format!("{value:.0}") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn token_boundaries_match_swift_formatting() { + assert_eq!(tokens(Some(999_600)), "1M"); + assert_eq!(tokens(Some(1_234)), "1K"); + assert_eq!(tokens(Some(2_401_634_303)), "2B"); + assert_eq!(tokens(None), "—"); + } + + #[test] + fn counts_and_costs_have_expected_precision() { + assert_eq!(count(Some(9_999)), "9999"); + assert_eq!(count(Some(12_345)), "12.3K"); + assert_eq!(cost(Some(12.345)), "$12.35"); + assert_eq!(cost(Some(1_234.0)), "$1.23K"); + } +} diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs new file mode 100644 index 0000000000..aa688daca9 --- /dev/null +++ b/desktop/src-tauri/src/lib.rs @@ -0,0 +1,119 @@ +mod auth; +mod discovery; +mod formatting; +mod logging; +mod proxy; +mod sidecar; +mod tray; +mod updater; +mod widget; +mod window; + +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Mutex, +}; +use tauri::{Manager, WebviewUrl, WebviewWindowBuilder}; +use tauri_plugin_autostart::MacosLauncher; +use tauri_plugin_shell::process::CommandChild; + +pub struct AppState { + pub proxy: proxy::ProxyClient, + pub spawned_by_us: AtomicBool, + pub child: Mutex>, +} + +impl AppState { + pub fn shutdown_child(&self) { + if !self.spawned_by_us.swap(false, Ordering::AcqRel) { + return; + } + if let Ok(mut child) = self.child.lock() { + if let Some(child) = child.take() { + let _ = child.kill(); + } + } + } +} + +#[tauri::command] +fn show_dashboard(app: tauri::AppHandle) { + if let Some(window) = app.get_webview_window("main") { + window::show(&window); + } +} + +#[tauri::command] +fn hide_dashboard(app: tauri::AppHandle) { + if let Some(window) = app.get_webview_window("main") { + window::hide(&window); + } +} + +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| { + if let Some(window) = app.get_webview_window("main") { + window::show(&window); + } + })) + .plugin(tauri_plugin_opener::init()) + .plugin(tauri_plugin_process::init()) + .plugin(tauri_plugin_autostart::init( + MacosLauncher::LaunchAgent, + None, + )) + .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_updater::Builder::new().build()) + .invoke_handler(tauri::generate_handler![show_dashboard, hide_dashboard]) + .setup(|app| { + let (endpoint, home) = discovery::current(); + let proxy = proxy::ProxyClient::new(endpoint, auth::Auth::new(home)) + .map_err(|error| error.to_string())?; + let child = tauri::async_runtime::block_on(sidecar::ensure_proxy( + app.handle(), + &proxy, + endpoint, + )) + .map_err(std::io::Error::other)?; + app.manage(AppState { + proxy: proxy.clone(), + spawned_by_us: AtomicBool::new(child.is_some()), + child: Mutex::new(child), + }); + app.manage(updater::PendingUpdate(Mutex::new(None))); + app.manage(tray::TrayState::default()); + + let window = WebviewWindowBuilder::new( + app, + "main", + WebviewUrl::App(format!("index.html?port={}", endpoint.port).into()), + ) + .title("OpenCodex") + .inner_size(1100.0, 720.0) + .visible(false) + .user_agent(&window::webview_user_agent()) + .on_navigation(window::navigation_allowed(endpoint)) + .build()?; + window::configure(&window); + window::set_tray_policy(app.handle(), false); + let dashboard = endpoint.url("/#/usage"); + if tauri::async_runtime::block_on(proxy.is_alive()).is_ok() { + let _ = window.eval(format!("window.location.replace({dashboard:?})")); + } + tray::install(app.handle(), proxy)?; + if !cfg!(debug_assertions) { + updater::start_background_checks(app.handle().clone()); + } + Ok(()) + }) + .build(tauri::generate_context!()) + .expect("error while building OpenCodex desktop shell") + .run(|app, event| { + if matches!(event, tauri::RunEvent::Exit) { + if let Some(state) = app.try_state::() { + state.shutdown_child(); + } + } + }); +} diff --git a/desktop/src-tauri/src/logging.rs b/desktop/src-tauri/src/logging.rs new file mode 100644 index 0000000000..a3b71fb5f4 --- /dev/null +++ b/desktop/src-tauri/src/logging.rs @@ -0,0 +1,14 @@ +use std::{ + collections::HashSet, + sync::{Mutex, OnceLock}, +}; + +pub fn log_once(scope: &str, message: &str) { + static LOGGED: OnceLock>> = OnceLock::new(); + let logged = LOGGED.get_or_init(|| Mutex::new(HashSet::new())); + if let Ok(mut logged) = logged.lock() { + if logged.insert(format!("{scope}: {message}")) { + eprintln!("{scope}: {message}"); + } + } +} diff --git a/desktop/src-tauri/src/main.rs b/desktop/src-tauri/src/main.rs new file mode 100644 index 0000000000..c0e7716829 --- /dev/null +++ b/desktop/src-tauri/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + opencodex_desktop_lib::run(); +} diff --git a/desktop/src-tauri/src/proxy.rs b/desktop/src-tauri/src/proxy.rs new file mode 100644 index 0000000000..fc8dc71ec5 --- /dev/null +++ b/desktop/src-tauri/src/proxy.rs @@ -0,0 +1,111 @@ +use crate::{auth::Auth, discovery::ProxyEndpoint}; +use reqwest::{Client, Method, StatusCode}; +use serde_json::Value; +use std::time::Duration; + +#[derive(Clone)] +pub struct ProxyClient { + client: Client, + endpoint: ProxyEndpoint, + auth: Auth, +} + +#[derive(Debug)] +pub enum ProxyError { + Unreachable, + Unauthorized, + Http(StatusCode), + Decode(reqwest::Error), +} + +impl ProxyClient { + pub fn new(endpoint: ProxyEndpoint, auth: Auth) -> Result { + Ok(Self { + client: Client::builder() + .timeout(Duration::from_secs(4)) + .user_agent(Auth::user_agent()) + .build()?, + endpoint, + auth, + }) + } + + pub fn endpoint(&self) -> ProxyEndpoint { + self.endpoint + } + + pub async fn is_alive(&self) -> Result { + self.get("/healthz").await + } + + pub async fn companion_settings(&self) -> Result { + self.get("/api/companion/settings").await + } + + pub async fn usage_summary(&self) -> Result { + self.get("/api/usage?range=7d").await + } + + pub async fn usage_today(&self) -> Result { + self.get("/api/usage?range=today").await + } + + pub async fn startup_health(&self) -> Result { + self.get("/api/startup-health").await + } + + pub async fn quotas(&self) -> Result { + self.get("/api/provider-quotas").await + } + + pub async fn timeline(&self, query: &str) -> Result { + self.get(&format!("/api/usage/timeline?{query}")).await + } + + pub async fn stop(&self) -> Result { + self.request(Method::POST, "/api/stop").await + } + + async fn get(&self, path: &str) -> Result { + self.request(Method::GET, path).await + } + + async fn request(&self, method: Method, path: &str) -> Result { + let response = self.send(&method, path, None).await?; + if response.status() == StatusCode::UNAUTHORIZED { + let token = self.auth.token().ok_or(ProxyError::Unauthorized)?; + let response = self.send(&method, path, Some(token)).await?; + return decode(response).await; + } + decode(response).await + } + + async fn send( + &self, + method: &Method, + path: &str, + token: Option, + ) -> Result { + let mut request = self.client.request(method.clone(), self.endpoint.url(path)); + if let Some(value) = token { + request = request.header("X-OpenCodex-API-Key", value); + } + request.send().await.map_err(|error| { + if error.is_connect() { + ProxyError::Unreachable + } else { + ProxyError::Decode(error) + } + }) + } +} + +async fn decode(response: reqwest::Response) -> Result { + if response.status() == StatusCode::UNAUTHORIZED { + return Err(ProxyError::Unauthorized); + } + if !response.status().is_success() { + return Err(ProxyError::Http(response.status())); + } + response.json().await.map_err(ProxyError::Decode) +} diff --git a/desktop/src-tauri/src/sidecar.rs b/desktop/src-tauri/src/sidecar.rs new file mode 100644 index 0000000000..434c1d6288 --- /dev/null +++ b/desktop/src-tauri/src/sidecar.rs @@ -0,0 +1,47 @@ +use crate::{discovery::ProxyEndpoint, proxy::ProxyClient}; +use tauri::{AppHandle, Manager}; +use tauri_plugin_shell::{process::CommandChild, ShellExt}; +use tokio::time::{sleep, timeout, Duration, Instant}; + +pub async fn ensure_proxy( + app: &AppHandle, + proxy: &ProxyClient, + endpoint: ProxyEndpoint, +) -> Result, String> { + let deadline = Instant::now() + Duration::from_secs(2); + loop { + if matches!( + timeout(Duration::from_millis(250), proxy.is_alive()).await, + Ok(Ok(_)) + ) { + return Ok(None); + } + if Instant::now() >= deadline { + break; + } + sleep(Duration::from_millis(150)).await; + } + + let gui_dist = app + .path() + .resource_dir() + .map_err(|error| error.to_string())? + .join("gui") + .join("dist"); + let command = app + .shell() + .sidecar("ocx") + .map_err(|error| error.to_string())? + .args(["start", "--port", &endpoint.port.to_string()]) + .env("OPENCODEX_GUI_DIST", gui_dist); + let (_events, child) = command.spawn().map_err(|error| error.to_string())?; + + for _ in 0..20 { + tokio::time::sleep(std::time::Duration::from_millis(150)).await; + if proxy.is_alive().await.is_ok() { + return Ok(Some(child)); + } + } + let _ = child.kill(); + Err("the OpenCodex sidecar did not become healthy".into()) +} diff --git a/desktop/src-tauri/src/tray.rs b/desktop/src-tauri/src/tray.rs new file mode 100644 index 0000000000..56f2550645 --- /dev/null +++ b/desktop/src-tauri/src/tray.rs @@ -0,0 +1,351 @@ +use crate::{formatting, proxy::ProxyClient, updater, widget, window}; +use serde_json::Value; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Mutex, +}; +use tauri::{ + menu::{CheckMenuItem, Menu, MenuItem, PredefinedMenuItem}, + tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, + AppHandle, Manager, Wry, +}; +use tauri_plugin_autostart::ManagerExt; +use tauri_plugin_opener::OpenerExt; + +pub struct TrayState { + pub menu: Mutex>, + pub installing: AtomicBool, +} + +pub struct UpdateMenu { + check_updates: MenuItem, + install_update: MenuItem, +} + +impl Default for TrayState { + fn default() -> Self { + Self { + menu: Mutex::new(None), + installing: AtomicBool::new(false), + } + } +} + +pub fn install(app: &AppHandle, proxy: ProxyClient) -> tauri::Result<()> { + let open = MenuItem::with_id(app, "open-dashboard", "Open Dashboard", true, None::<&str>)?; + let browser = MenuItem::with_id(app, "open-browser", "Open in Browser", true, None::<&str>)?; + let login = CheckMenuItem::with_id( + app, + "start-at-login", + "Start at Login", + true, + app.autolaunch().is_enabled().unwrap_or(false), + None::<&str>, + )?; + let spawned_by_us = app + .state::() + .spawned_by_us + .load(Ordering::Relaxed); + let stop = MenuItem::with_id(app, "stop-proxy", "Stop proxy", spawned_by_us, None::<&str>)?; + let stop_item = stop.clone(); + let check_updates = MenuItem::with_id( + app, + "check-updates", + "Check for Updates…", + true, + None::<&str>, + )?; + let install_update = + MenuItem::with_id(app, "install-update", "Install update", false, None::<&str>)?; + let quit = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?; + let menu = Menu::with_items( + app, + &[ + &open, + &browser, + &PredefinedMenuItem::separator(app)?, + &login, + &stop, + &PredefinedMenuItem::separator(app)?, + &check_updates, + &install_update, + &PredefinedMenuItem::separator(app)?, + &quit, + ], + )?; + if let Ok(mut state) = app.state::().menu.lock() { + *state = Some(UpdateMenu { + check_updates: check_updates.clone(), + install_update: install_update.clone(), + }); + } + + let tray = TrayIconBuilder::with_id("main") + .icon(icon()) + .icon_as_template(true) + .menu(&menu) + .on_tray_icon_event(|tray, event| { + if let TrayIconEvent::Click { + button: MouseButton::Left, + button_state: MouseButtonState::Up, + .. + } = event + { + if let Some(window) = tray.app_handle().get_webview_window("main") { + window::show(&window); + } + } + }) + .on_menu_event(move |app, event| match event.id().as_ref() { + "open-dashboard" => { + if let Some(window) = app.get_webview_window("main") { + window::show(&window); + } + } + "open-browser" => { + let endpoint = app.state::().proxy.endpoint(); + let _ = app + .opener() + .open_url(format!("{}#/usage", endpoint.url("/")), None::); + } + "start-at-login" => { + let enabled = app.autolaunch().is_enabled().unwrap_or(false); + if enabled { + let _ = app.autolaunch().disable(); + } else { + let _ = app.autolaunch().enable(); + } + } + "stop-proxy" => { + if app + .state::() + .spawned_by_us + .load(Ordering::Relaxed) + { + let proxy = app.state::().proxy.clone(); + let app = app.clone(); + let stop_item = stop_item.clone(); + tauri::async_runtime::spawn(async move { + let stopped = proxy.stop().await.is_ok() || proxy.is_alive().await.is_err(); + if stopped { + app.state::().shutdown_child(); + let _ = stop_item.set_enabled(false); + } + }); + } + } + "check-updates" => { + let app = app.clone(); + tauri::async_runtime::spawn(async move { + updater::check_and_show(&app).await; + }); + } + "install-update" => { + let app = app.clone(); + tauri::async_runtime::spawn(async move { + let update = app + .state::() + .0 + .lock() + .ok() + .and_then(|mut pending| pending.take()); + let Some(update) = update else { + return; + }; + let version = update.version.clone(); + let retry_update = update.clone(); + set_installing(&app, &version); + if let Err(error) = updater::install(&app, update).await { + if let Ok(mut pending) = + app.state::().0.lock() + { + *pending = Some(retry_update); + } + set_install_failed(&app, &version); + crate::logging::log_once("updater install failed", &error); + } + }); + } + "quit" => app.exit(0), + _ => {} + }) + .build(app)?; + + refresh_title(&tray, &proxy); + widget::refresh(&proxy); + let tray = tray.clone(); + tauri::async_runtime::spawn(async move { + let mut tick = 0; + loop { + tokio::time::sleep(std::time::Duration::from_secs(60)).await; + refresh_title(&tray, &proxy); + tick += 1; + if tick % 5 == 0 { + widget::refresh(&proxy); + } + } + }); + Ok(()) +} + +pub fn show_update_available(app: &AppHandle, version: &str) { + if let Some(state) = app.try_state::() { + if let Ok(menu) = state.menu.lock() { + if let Some(menu) = menu.as_ref() { + let _ = menu.install_update.set_text(updater::update_label(version)); + let _ = menu.install_update.set_enabled(true); + let _ = menu.check_updates.set_enabled(true); + let _ = menu.check_updates.set_text("Check for Updates…"); + } + } + } +} + +pub fn show_up_to_date(app: &AppHandle) { + if let Some(state) = app.try_state::() { + if let Ok(menu) = state.menu.lock() { + if let Some(menu) = menu.as_ref() { + let _ = menu + .check_updates + .set_text(format!("Up to date (v{})", env!("CARGO_PKG_VERSION"))); + let _ = menu.check_updates.set_enabled(true); + let _ = menu.install_update.set_enabled(false); + } + } + } +} + +pub fn is_installing(app: &AppHandle) -> bool { + app.try_state::() + .is_some_and(|state| state.installing.load(Ordering::Acquire)) +} + +fn set_installing(app: &AppHandle, version: &str) { + if let Some(state) = app.try_state::() { + state.installing.store(true, Ordering::Release); + if let Ok(menu) = state.menu.lock() { + if let Some(menu) = menu.as_ref() { + let _ = menu + .install_update + .set_text(format!("Installing update v{version}…")); + let _ = menu.install_update.set_enabled(false); + let _ = menu.check_updates.set_enabled(false); + } + } + } +} + +fn set_install_failed(app: &AppHandle, version: &str) { + if let Some(state) = app.try_state::() { + state.installing.store(false, Ordering::Release); + } + show_update_available(app, version); +} + +fn refresh_title(tray: &tauri::tray::TrayIcon, proxy: &ProxyClient) { + let proxy = proxy.clone(); + let tray = tray.clone(); + tauri::async_runtime::spawn(async move { + let Ok(settings) = proxy.companion_settings().await else { + return; + }; + let Ok(usage) = proxy.usage_summary().await else { + return; + }; + let quotas = proxy.quotas().await.unwrap_or(Value::Null); + if let Some(title) = render_title(&settings, &usage, "as) { + let _ = tray.set_title(Some(&title)); + } + }); +} + +pub(crate) fn render_title(settings: &Value, usage: &Value, quotas: &Value) -> Option { + let metric = settings + .pointer("/settings/menuBarMetric") + .and_then(Value::as_str) + .unwrap_or("tokens"); + let summary = usage.get("summary").unwrap_or(usage); + let quota = quota_percent(quotas); + let value = match metric { + "requests" => formatting::count(summary.get("requests").and_then(Value::as_i64)), + "cost" => formatting::cost(summary.get("estimatedCostUsd").and_then(Value::as_f64)), + "quota" => format_percent(quota), + "none" => return None, + _ => formatting::tokens(summary.get("totalTokens").and_then(Value::as_i64)), + }; + let template = settings + .pointer("/settings/menuBarTemplate") + .and_then(Value::as_str) + .filter(|value| !value.trim().is_empty()); + let rendered = template + .map(|value| { + value + .replace( + "{requests}", + &formatting::count(summary.get("requests").and_then(Value::as_i64)), + ) + .replace( + "{totalTokens}", + &formatting::tokens(summary.get("totalTokens").and_then(Value::as_i64)), + ) + .replace( + "{costUsd}", + &formatting::cost(summary.get("estimatedCostUsd").and_then(Value::as_f64)), + ) + .replace( + "{inputTokens}", + &formatting::tokens(summary.get("inputTokens").and_then(Value::as_i64)), + ) + .replace( + "{outputTokens}", + &formatting::tokens(summary.get("outputTokens").and_then(Value::as_i64)), + ) + .replace("{quotaPercent}", &format_percent(quota)) + }) + .unwrap_or(value); + let rendered = rendered.trim(); + if rendered.is_empty() { + None + } else if rendered.chars().count() > 24 { + Some(format!( + "{}…", + rendered.chars().take(23).collect::() + )) + } else { + Some(rendered.to_owned()) + } +} + +fn quota_percent(value: &Value) -> Option { + let reports = value.get("reports")?.as_array()?; + let mut values = Vec::new(); + for report in reports { + let Some(quota) = report.get("quota") else { + continue; + }; + for key in ["weeklyPercent", "monthlyPercent", "fiveHourPercent"] { + if let Some(value) = quota.get(key).and_then(Value::as_f64) { + values.push(value); + } + } + if let Some(windows) = quota.get("customWindows").and_then(Value::as_array) { + values.extend( + windows + .iter() + .filter_map(|window| window.get("percent").and_then(Value::as_f64)), + ); + } + } + values.into_iter().reduce(f64::min) +} + +fn format_percent(value: Option) -> String { + value + .map(|value| format!("{}%", value.round() as i64)) + .unwrap_or_else(|| "—".into()) +} + +fn icon() -> tauri::image::Image<'static> { + tauri::image::Image::from_bytes(include_bytes!("../icons/tray/icon.png")) + .expect("valid tray icon") +} diff --git a/desktop/src-tauri/src/updater.rs b/desktop/src-tauri/src/updater.rs new file mode 100644 index 0000000000..4638c1565d --- /dev/null +++ b/desktop/src-tauri/src/updater.rs @@ -0,0 +1,71 @@ +use crate::{logging, tray}; +use std::sync::Mutex; +use tauri::{AppHandle, Manager}; +use tauri_plugin_updater::{Update, UpdaterExt}; + +pub struct PendingUpdate(pub Mutex>); + +pub async fn check(app: &AppHandle) -> Result, String> { + app.updater() + .map_err(|error| error.to_string())? + .check() + .await + .map_err(|error| error.to_string()) +} + +pub async fn install(app: &AppHandle, update: Update) -> Result<(), String> { + update + .download_and_install(|_, _| {}, || {}) + .await + .map_err(|error| error.to_string())?; + app.restart(); +} + +pub fn update_label(version: &str) -> String { + format!("Install update v{version}") +} + +pub fn start_background_checks(app: AppHandle) { + tauri::async_runtime::spawn(async move { + tokio::time::sleep(std::time::Duration::from_secs(30)).await; + loop { + check_and_show(&app).await; + tokio::time::sleep(std::time::Duration::from_secs(6 * 60 * 60)).await; + } + }); +} + +pub async fn check_and_show(app: &AppHandle) { + if tray::is_installing(app) { + return; + } + match check(app).await { + Ok(Some(update)) => { + if tray::is_installing(app) { + return; + } + let version = update.version.clone(); + if let Ok(mut pending) = app.state::().0.lock() { + *pending = Some(update); + } + tray::show_update_available(app, &version); + } + Ok(None) => { + if let Ok(mut pending) = app.state::().0.lock() { + *pending = None; + } + tray::show_up_to_date(app); + } + Err(error) => logging::log_once("updater check failed", &error), + } +} + +#[cfg(test)] +mod tests { + use super::update_label; + + #[test] + fn formats_update_menu_label() { + assert_eq!(update_label("2.62.0"), "Install update v2.62.0"); + } +} diff --git a/desktop/src-tauri/src/widget.rs b/desktop/src-tauri/src/widget.rs new file mode 100644 index 0000000000..4db3005f03 --- /dev/null +++ b/desktop/src-tauri/src/widget.rs @@ -0,0 +1,495 @@ +#[cfg(target_os = "macos")] +mod macos { + use crate::{ + proxy::{ProxyClient, ProxyError}, + tray, + }; + use serde::Serialize; + use serde_json::{json, Value}; + use std::{ + fs, + path::PathBuf, + time::{SystemTime, UNIX_EPOCH}, + }; + use uuid::Uuid; + + #[derive(Debug, Serialize, serde::Deserialize, Clone, PartialEq)] + #[serde(rename_all = "camelCase")] + struct Today { + requests: Option, + total_tokens: Option, + estimated_cost_usd: Option, + } + + #[derive(Debug, Serialize, serde::Deserialize, Clone, PartialEq)] + #[serde(rename_all = "camelCase")] + struct Quota { + provider_label: String, + window_label: String, + percent: Option, + reset_at: Option, + } + + #[derive(Debug, Serialize, serde::Deserialize, Clone, PartialEq)] + #[serde(rename_all = "camelCase")] + struct Series { + id: String, + points: Vec, + } + + #[derive(Debug, Serialize, serde::Deserialize, Clone, PartialEq)] + #[serde(rename_all = "camelCase")] + struct Chart { + start: f64, + bucket_seconds: i64, + style: String, + series: Vec, + } + + #[derive(Debug, Serialize, serde::Deserialize, Clone, PartialEq)] + #[serde(rename_all = "camelCase")] + struct Snapshot { + schema_version: i64, + state: String, + state_title: String, + detail: Option, + endpoint_display: String, + menu_title: Option, + today: Option, + quotas: Vec, + chart: Option, + last_updated: Option, + generated_at: f64, + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + enum ErrorKind { + Unreachable, + Unauthorized, + Http, + Decode, + } + + fn state_for_error( + kind: ErrorKind, + detail: Option, + ) -> (&'static str, &'static str, Option) { + match kind { + ErrorKind::Unreachable => ( + "unreachable", + "Stopped", + Some("The proxy is not running.".into()), + ), + ErrorKind::Unauthorized => ( + "unauthorized", + "Needs API key", + Some("This proxy requires an API key.".into()), + ), + ErrorKind::Http | ErrorKind::Decode => ("degraded", "Degraded", detail), + } + } + + fn proxy_error(error: &ProxyError) -> (ErrorKind, Option) { + match error { + ProxyError::Unreachable => (ErrorKind::Unreachable, None), + ProxyError::Unauthorized => (ErrorKind::Unauthorized, None), + ProxyError::Http(status) => (ErrorKind::Http, Some(format!("HTTP {status}"))), + ProxyError::Decode(error) => (ErrorKind::Decode, Some(error.to_string())), + } + } + + fn number(value: Option<&Value>) -> Option { + value.and_then(Value::as_f64) + } + + fn integer(value: Option<&Value>) -> Option { + value.and_then(Value::as_i64) + } + + fn reset_at(value: Option<&Value>) -> Option { + let value = number(value)?; + Some(if value >= 1_000_000_000_000.0 { + value / 1000.0 + } else { + value + }) + } + + fn quotas(value: &Value) -> Vec { + let Some(reports) = value.get("reports").and_then(Value::as_array) else { + return Vec::new(); + }; + let mut rows = Vec::new(); + for report in reports { + let provider_label = report + .get("label") + .or_else(|| report.get("provider")) + .and_then(Value::as_str) + .unwrap_or("unknown") + .to_owned(); + let Some(quota) = report.get("quota") else { + continue; + }; + let mut push = |percent: Option<&Value>, window_label: &str, reset: Option<&Value>| { + if percent.is_some() || reset.is_some() { + rows.push(Quota { + provider_label: provider_label.clone(), + window_label: window_label.to_owned(), + percent: number(percent), + reset_at: reset_at(reset), + }); + } + }; + push( + quota.get("fiveHourPercent"), + "5h", + quota.get("fiveHourResetAt"), + ); + push( + quota.get("weeklyPercent"), + "week", + quota.get("weeklyResetAt"), + ); + push( + quota.get("monthlyPercent"), + "month", + quota.get("monthlyResetAt"), + ); + if let Some(windows) = quota.get("customWindows").and_then(Value::as_array) { + for window in windows { + let label = window + .get("label") + .and_then(Value::as_str) + .unwrap_or("window"); + push(window.get("percent"), label, window.get("resetAt")); + } + } + } + rows + } + + fn chart(value: &Value, settings: &Value) -> Option { + let start = number(value.get("start"))?; + let bucket_seconds = integer(value.get("bucketSeconds"))?; + let settings = settings.get("settings").unwrap_or(settings); + let style = settings + .get("chartStyle") + .and_then(Value::as_str) + .unwrap_or("line") + .to_owned(); + let series = value + .get("series") + .and_then(Value::as_array)? + .iter() + .take(6) + .filter_map(|item| { + Some(Series { + id: item.get("id")?.as_str()?.to_owned(), + points: item + .get("points")? + .as_array()? + .iter() + .filter_map(Value::as_f64) + .collect(), + }) + }) + .collect(); + Some(Chart { + start, + bucket_seconds, + style, + series, + }) + } + + fn timeline_query(settings: &Value) -> String { + let settings = settings.get("settings").unwrap_or(settings); + let get = |key: &str, fallback: &str| { + settings + .get(key) + .and_then(Value::as_str) + .unwrap_or(fallback) + .to_owned() + }; + let hours = settings + .get("chartHours") + .and_then(Value::as_i64) + .unwrap_or(24); + let bucket_minutes = settings + .get("bucketMinutes") + .and_then(Value::as_i64) + .unwrap_or(60); + let metric = get("tokenMetric", "total"); + let aggregation = get("aggregation", "sum"); + let grouping = get("chartGrouping", "model"); + let mut query = format!( + "hours={hours}&bucketMinutes={bucket_minutes}&metric={metric}&aggregation={aggregation}&grouping={grouping}" + ); + if let Some(models) = settings.get("models").and_then(Value::as_array) { + let models = models + .iter() + .filter_map(Value::as_str) + .collect::>() + .join(","); + if !models.is_empty() { + query.push_str("&models="); + query.push_str(&models); + } + } + query + } + + fn snapshot_path() -> PathBuf { + let home = std::env::var_os("HOME") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from(".")); + home.join("Library/Containers/com.opencodex.desktop.widget/Data/Library/Application Support/OpenCodex/snapshot.json") + } + + fn now_seconds() -> f64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs_f64() + } + + fn without_generated_at(snapshot: &Snapshot) -> Snapshot { + let mut snapshot = snapshot.clone(); + snapshot.generated_at = 0.0; + snapshot + } + + fn write_if_changed( + path: &std::path::Path, + previous: Option<&Snapshot>, + snapshot: &Snapshot, + ) -> std::io::Result { + if previous.map(without_generated_at).as_ref() == Some(&without_generated_at(snapshot)) { + return Ok(false); + } + let Some(directory) = path.parent() else { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "snapshot path has no parent", + )); + }; + fs::create_dir_all(directory)?; + let bytes = serde_json::to_vec(snapshot).map_err(std::io::Error::other)?; + let temporary = directory.join(format!(".snapshot-{}.tmp", Uuid::new_v4())); + fs::write(&temporary, bytes)?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(&temporary, fs::Permissions::from_mode(0o600))?; + } + fs::rename(temporary, path)?; + Ok(true) + } + + fn make_snapshot( + proxy: &ProxyClient, + settings: &Value, + health: &Value, + today: Option<&Value>, + quota_value: Option<&Value>, + timeline_value: Option<&Value>, + ) -> Snapshot { + let endpoint = proxy.endpoint(); + let detail = { + let parts = [health.get("status"), health.get("protection")] + .into_iter() + .filter_map(|value| value.and_then(Value::as_str)) + .filter(|part| !part.is_empty() && *part != "none") + .collect::>(); + (!parts.is_empty()).then(|| parts.join(" · ")) + }; + let today_snapshot = today + .and_then(|value| value.get("summary").or(Some(value))) + .map(|summary| Today { + requests: integer(summary.get("requests")), + total_tokens: integer(summary.get("totalTokens")), + estimated_cost_usd: number(summary.get("estimatedCostUsd")), + }); + let quotas_value = quota_value.unwrap_or(&Value::Null); + let menu_title = tray::render_title(settings, today.unwrap_or(&Value::Null), quotas_value); + let chart = timeline_value.and_then(|value| chart(value, settings)); + Snapshot { + schema_version: 1, + state: "running".into(), + state_title: "Running".into(), + detail, + endpoint_display: format!("{}:{}", endpoint.host, endpoint.port), + menu_title, + today: today_snapshot, + quotas: quotas(quotas_value), + chart, + last_updated: timeline_value.map(|_| now_seconds()), + generated_at: now_seconds(), + } + } + + pub async fn write(proxy: ProxyClient) { + let health = match proxy.startup_health().await { + Ok(value) => value, + Err(error) => { + let (kind, detail) = proxy_error(&error); + let (state, state_title, detail) = state_for_error(kind, detail); + let snapshot = Snapshot { + schema_version: 1, + state: state.into(), + state_title: state_title.into(), + detail, + endpoint_display: format!( + "{}:{}", + proxy.endpoint().host, + proxy.endpoint().port + ), + menu_title: None, + today: None, + quotas: Vec::new(), + chart: None, + last_updated: None, + generated_at: now_seconds(), + }; + let path = snapshot_path(); + let previous = fs::read(&path) + .ok() + .and_then(|bytes| serde_json::from_slice::(&bytes).ok()); + if let Err(error) = write_if_changed(&path, previous.as_ref(), &snapshot) { + crate::logging::log_once("widget snapshot write failed", &error.to_string()); + } + crate::logging::log_once("widget snapshot health failed", state); + return; + } + }; + let settings = proxy + .companion_settings() + .await + .unwrap_or_else(|_| json!({ "settings": {} })); + let today = proxy.usage_today().await.ok(); + let quota_value = proxy.quotas().await.ok(); + let timeline_value = proxy.timeline(&timeline_query(&settings)).await.ok(); + let snapshot = make_snapshot( + &proxy, + &settings, + &health, + today.as_ref(), + quota_value.as_ref(), + timeline_value.as_ref(), + ); + let path = snapshot_path(); + let previous = fs::read(&path) + .ok() + .and_then(|bytes| serde_json::from_slice::(&bytes).ok()); + if let Err(error) = write_if_changed(&path, previous.as_ref(), &snapshot) { + crate::logging::log_once("widget snapshot write failed", &error.to_string()); + } + } + + pub fn refresh(proxy: &ProxyClient) { + let proxy = proxy.clone(); + tauri::async_runtime::spawn(async move { write(proxy).await }); + } + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn serialization_uses_swift_field_names() { + let snapshot = Snapshot { + schema_version: 1, + state: "running".into(), + state_title: "Running".into(), + detail: Some("ok".into()), + endpoint_display: "127.0.0.1:10100".into(), + menu_title: Some("2K".into()), + today: Some(Today { + requests: Some(2), + total_tokens: Some(1234), + estimated_cost_usd: Some(0.12), + }), + quotas: vec![Quota { + provider_label: "OpenAI".into(), + window_label: "week".into(), + percent: Some(10.0), + reset_at: Some(1.0), + }], + chart: Some(Chart { + start: 1.0, + bucket_seconds: 3600, + style: "line".into(), + series: vec![Series { + id: "openai/gpt".into(), + points: vec![1.0, 2.0], + }], + }), + last_updated: Some(2.0), + generated_at: 3.0, + }; + assert_eq!( + serde_json::to_string(&snapshot).unwrap(), + r#"{"schemaVersion":1,"state":"running","stateTitle":"Running","detail":"ok","endpointDisplay":"127.0.0.1:10100","menuTitle":"2K","today":{"requests":2,"totalTokens":1234,"estimatedCostUsd":0.12},"quotas":[{"providerLabel":"OpenAI","windowLabel":"week","percent":10.0,"resetAt":1.0}],"chart":{"start":1.0,"bucketSeconds":3600,"style":"line","series":[{"id":"openai/gpt","points":[1.0,2.0]}]},"lastUpdated":2.0,"generatedAt":3.0}"# + ); + } + + #[test] + fn error_state_mapping_covers_four_kinds() { + assert_eq!( + state_for_error(ErrorKind::Unreachable, None).0, + "unreachable" + ); + assert_eq!( + state_for_error(ErrorKind::Unauthorized, None).0, + "unauthorized" + ); + assert_eq!( + state_for_error(ErrorKind::Http, Some("HTTP 500".into())).0, + "degraded" + ); + assert_eq!( + state_for_error(ErrorKind::Decode, Some("bad".into())).0, + "degraded" + ); + } + + #[test] + fn write_if_changed_ignores_generated_at() { + let path = std::env::temp_dir().join(format!("ocx-widget-{}.json", std::process::id())); + let snapshot = Snapshot { + schema_version: 1, + state: "running".into(), + state_title: "Running".into(), + detail: None, + endpoint_display: "127.0.0.1:10100".into(), + menu_title: None, + today: None, + quotas: Vec::new(), + chart: None, + last_updated: None, + generated_at: 1.0, + }; + assert!(write_if_changed(&path, None, &snapshot).unwrap()); + let mut changed = snapshot.clone(); + changed.generated_at = 2.0; + assert!(!write_if_changed(&path, Some(&snapshot), &changed).unwrap()); + let _ = fs::remove_file(path); + } + + #[test] + fn chart_series_are_truncated_to_six() { + let series = (0..8) + .map(|index| json!({ "id": index.to_string(), "points": [1] })) + .collect::>(); + let value = json!({ "start": 1, "bucketSeconds": 60, "series": series }); + let result = chart(&value, &json!({ "settings": { "chartStyle": "line" } })).unwrap(); + assert_eq!(result.series.len(), 6); + } + } +} + +#[cfg(target_os = "macos")] +pub(crate) use macos::refresh; + +#[cfg(not(target_os = "macos"))] +pub(crate) fn refresh(_: &crate::proxy::ProxyClient) {} diff --git a/desktop/src-tauri/src/window.rs b/desktop/src-tauri/src/window.rs new file mode 100644 index 0000000000..f9550247ae --- /dev/null +++ b/desktop/src-tauri/src/window.rs @@ -0,0 +1,88 @@ +use crate::{auth::Auth, discovery::ProxyEndpoint}; +use tauri::{AppHandle, Manager, Url, WebviewWindow, WindowEvent}; + +pub fn webview_user_agent() -> String { + let platform = if cfg!(target_os = "macos") { + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko)" + } else if cfg!(target_os = "windows") { + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)" + } else { + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko)" + }; + format!("{platform} {}", Auth::user_agent()) +} + +pub fn configure(window: &WebviewWindow) { + let window_for_close = window.clone(); + window.on_window_event(move |event| { + if let WindowEvent::CloseRequested { api, .. } = event { + api.prevent_close(); + let _ = window_for_close.hide(); + apply_tray_policy(window_for_close.app_handle(), false); + } + }); +} + +pub fn navigation_allowed(endpoint: ProxyEndpoint) -> impl Fn(&Url) -> bool { + move |url| { + if url.scheme() == "tauri" { + return true; + } + if url.scheme() == "http" && url.host_str() == Some(endpoint.host) { + return url.port_or_known_default() == Some(endpoint.port); + } + if matches!(url.scheme(), "http" | "https") { + let _ = tauri_plugin_opener::open_url(url.as_str(), None::<&str>); + return false; + } + url.scheme() == "about" && url.as_str() == "about:blank" + } +} + +pub fn show(window: &WebviewWindow) { + let _ = window.show(); + let _ = window.set_focus(); + apply_tray_policy(window.app_handle(), true); +} + +pub fn hide(window: &WebviewWindow) { + let _ = window.hide(); + apply_tray_policy(window.app_handle(), false); +} + +#[cfg(target_os = "macos")] +fn apply_tray_policy(app: &AppHandle, visible: bool) { + let policy = if visible { + tauri::ActivationPolicy::Regular + } else { + tauri::ActivationPolicy::Accessory + }; + let _ = app.set_dock_visibility(visible); + let _ = app.set_activation_policy(policy); +} + +#[cfg(not(target_os = "macos"))] +fn apply_tray_policy(_app: &AppHandle, _visible: bool) {} + +pub fn set_tray_policy(app: &AppHandle, visible: bool) { + apply_tray_policy(app, visible); +} + +#[cfg(test)] +mod tests { + use super::webview_user_agent; + + #[test] + fn webview_user_agent_marks_the_desktop_shell() { + let user_agent = webview_user_agent(); + assert!(user_agent.starts_with("Mozilla/5.0 ")); + assert!(user_agent.contains("OpenCodexDesktop/")); + if cfg!(target_os = "macos") { + assert!(user_agent.contains("(Macintosh; Intel Mac OS X 10_15_7)")); + } else if cfg!(target_os = "windows") { + assert!(user_agent.contains("(Windows NT 10.0; Win64; x64)")); + } else { + assert!(user_agent.contains("(X11; Linux x86_64)")); + } + } +} diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json new file mode 100644 index 0000000000..6af54b5e1c --- /dev/null +++ b/desktop/src-tauri/tauri.conf.json @@ -0,0 +1,71 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "OpenCodex", + "version": "2.61.0", + "identifier": "com.opencodex.desktop", + "build": { + "frontendDist": "../ui", + "devUrl": "http://localhost:1420" + }, + "app": { + "security": { + "csp": "default-src 'self'; connect-src 'self' http://127.0.0.1:*; style-src 'self' 'unsafe-inline'; script-src 'self'" + } + }, + "bundle": { + "active": true, + "targets": "all", + "createUpdaterArtifacts": true, + "externalBin": [ + "binaries/ocx" + ], + "resources": { + "resources/gui/dist": "gui/dist" + }, + "icon": [ + "icons/icon.icns", + "icons/icon.ico", + "icons/icon.png" + ], + "macOS": { + "minimumSystemVersion": "13.0", + "files": { + "PlugIns/OpenCodexWidget.appex": "widget/OpenCodexWidget.appex" + }, + "dmg": { + "appPosition": { + "x": 180, + "y": 170 + }, + "applicationFolderPosition": { + "x": 480, + "y": 170 + } + } + }, + "windows": { + "webviewInstallMode": { + "type": "downloadBootstrapper" + }, + "wix": { + "language": "en-US" + } + }, + "linux": { + "deb": { + "depends": [] + }, + "appimage": { + "bundleMediaFramework": false + } + } + }, + "plugins": { + "updater": { + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEFDNzZCMDg0NkVCRUJGODEKUldTQnY3NXVoTEIyckhRUXJMOXJRUDR0aHQ2L3pLVHVweXFSc1lzS24vWDNiSUJ5MXJIZmpyb2sK", + "endpoints": [ + "https://github.com/lidge-jun/opencodex/releases/latest/download/latest.json" + ] + } + } +} diff --git a/desktop/ui/index.html b/desktop/ui/index.html new file mode 100644 index 0000000000..e4d5c9a7de --- /dev/null +++ b/desktop/ui/index.html @@ -0,0 +1,29 @@ + + + + + + OpenCodex + + + +