diff --git a/.changeset/calm-renderers-unite.md b/.changeset/calm-renderers-unite.md new file mode 100644 index 0000000000..ee914928b4 --- /dev/null +++ b/.changeset/calm-renderers-unite.md @@ -0,0 +1,5 @@ +--- +"@codegraphy-dev/graph-renderer": minor +--- + +Own shared Material icon matching, Node geometry, connection sizing, and force-control semantics in the renderer package. diff --git a/.changeset/quiet-desktop-graph.md b/.changeset/quiet-desktop-graph.md new file mode 100644 index 0000000000..eff7506385 --- /dev/null +++ b/.changeset/quiet-desktop-graph.md @@ -0,0 +1,5 @@ +--- +"@codegraphy-dev/core": patch +--- + +Expose a cached CodeGraphy Workspace Relationship Graph request for local interfaces, with a focused File and Folder projection that avoids hydrating unused analysis facts. diff --git a/.changeset/soft-graphs-resize.md b/.changeset/soft-graphs-resize.md new file mode 100644 index 0000000000..0b84baf8d2 --- /dev/null +++ b/.changeset/soft-graphs-resize.md @@ -0,0 +1,5 @@ +--- +"@codegraphy-dev/extension": patch +--- + +Keep Link Distance between 30 and 150 in the shared Graph View force contract and clamp stored values at the settings boundary. diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml new file mode 100644 index 0000000000..55a26cb6b1 --- /dev/null +++ b/.github/workflows/desktop-release.yml @@ -0,0 +1,174 @@ +name: Desktop release + +on: + workflow_dispatch: + inputs: + tag: + description: Exact desktop tag, for example desktop-v0.1.0 + required: true + type: string + +permissions: + contents: write + +concurrency: + group: desktop-release-${{ inputs.tag }} + cancel-in-progress: false + +jobs: + build-verify-draft: + name: Build, verify, and draft Apple Silicon release + runs-on: macos-26 + timeout-minutes: 60 + environment: desktop-release + env: + CODEGRAPHY_DESKTOP_TARGET: aarch64-apple-darwin + CODEGRAPHY_DESKTOP_REQUIRE_RELEASE_SIGNATURE: '1' + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Validate release request + env: + RELEASE_TAG: ${{ inputs.tag }} + shell: bash + run: | + set -euo pipefail + version="$(node -p "require('./apps/desktop/package.json').version")" + expected_tag="desktop-v${version}" + test "$GITHUB_REF" = "refs/heads/main" + test "$RELEASE_TAG" = "$expected_tag" + test "$(uname -m)" = "arm64" + xcodebuild -version | grep -q '^Xcode 26\.' + + - name: Setup pnpm + uses: pnpm/action-setup@v6.0.8 + with: + version: 10.32.0 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 22.23.2 + cache: pnpm + + - name: Setup Rust + uses: dtolnay/rust-toolchain@1.96.0 + with: + components: clippy,rustfmt + targets: aarch64-apple-darwin + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Test desktop release inputs + run: | + pnpm --filter @codegraphy-dev/desktop test + pnpm --filter @codegraphy-dev/desktop typecheck + cargo test --manifest-path apps/desktop/src-tauri/Cargo.toml --lib + + - name: Import Developer ID certificate + id: signing + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY_P8 }} + APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} + shell: bash + run: | + set -euo pipefail + keychain="$RUNNER_TEMP/codegraphy-signing.keychain-db" + certificate="$RUNNER_TEMP/codegraphy-developer-id.p12" + api_key_path="$RUNNER_TEMP/AuthKey_${APPLE_API_KEY}.p8" + keychain_password="$(openssl rand -hex 24)" + + printf '%s' "$APPLE_CERTIFICATE" | base64 --decode > "$certificate" + printf '%s' "$APPLE_API_KEY_CONTENT" > "$api_key_path" + chmod 600 "$certificate" "$api_key_path" + + security create-keychain -p "$keychain_password" "$keychain" + security set-keychain-settings -lut 21600 "$keychain" + security unlock-keychain -p "$keychain_password" "$keychain" + security import "$certificate" -k "$keychain" -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$keychain_password" "$keychain" + security list-keychains -d user -s "$keychain" + + signing_identity="$(security find-identity -v -p codesigning "$keychain" | sed -n 's/.*"\(Developer ID Application:.*\)"/\1/p' | head -n 1)" + test -n "$signing_identity" + printf 'APPLE_SIGNING_IDENTITY=%s\n' "$signing_identity" >> "$GITHUB_ENV" + printf 'APPLE_API_KEY_PATH=%s\n' "$api_key_path" >> "$GITHUB_ENV" + printf 'KEYCHAIN_PATH=%s\n' "$keychain" >> "$GITHUB_ENV" + + - name: Build signed and notarized app + env: + APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} + APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} + run: pnpm --filter @codegraphy-dev/desktop exec tauri build --target "$CODEGRAPHY_DESKTOP_TARGET" --bundles app,dmg + + - name: Sign, notarize, and staple final DMG + env: + APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} + APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} + shell: bash + run: | + set -euo pipefail + bundle_root="apps/desktop/src-tauri/target/${CODEGRAPHY_DESKTOP_TARGET}/release/bundle/dmg" + dmg="$(find "$bundle_root" -maxdepth 1 -type f -name '*.dmg' -print -quit)" + test -n "$dmg" + codesign --force --sign "$APPLE_SIGNING_IDENTITY" --timestamp "$dmg" + xcrun notarytool submit "$dmg" --key "$APPLE_API_KEY_PATH" --key-id "$APPLE_API_KEY" --issuer "$APPLE_API_ISSUER" --wait + xcrun stapler staple "$dmg" + + - name: Verify release bundle + run: pnpm --filter @codegraphy-dev/desktop check:bundle + + - name: Prepare release assets + id: assets + shell: bash + run: | + set -euo pipefail + version="$(node -p "require('./apps/desktop/package.json').version")" + bundle_root="apps/desktop/src-tauri/target/${CODEGRAPHY_DESKTOP_TARGET}/release/bundle/dmg" + dmg="$(find "$bundle_root" -maxdepth 1 -type f -name '*.dmg' -print -quit)" + test -n "$dmg" + asset="$RUNNER_TEMP/CodeGraphy_${version}_aarch64.dmg" + cp "$dmg" "$asset" + ( + cd "$RUNNER_TEMP" + shasum -a 256 "$(basename "$asset")" > "$(basename "$asset").sha256" + ) + printf 'dmg=%s\n' "$asset" >> "$GITHUB_OUTPUT" + printf 'checksum=%s.sha256\n' "$asset" >> "$GITHUB_OUTPUT" + + - name: Upload verified assets to draft release + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ inputs.tag }} + DMG_PATH: ${{ steps.assets.outputs.dmg }} + CHECKSUM_PATH: ${{ steps.assets.outputs.checksum }} + shell: bash + run: | + set -euo pipefail + version="$(node -p "require('./apps/desktop/package.json').version")" + notes="$RUNNER_TEMP/desktop-release-notes.md" + cat > "$notes" </dev/null)"; then + test "$(jq -r '.draft' <<< "$release_json")" = "true" + else + gh release create "$RELEASE_TAG" --draft --target "$GITHUB_SHA" --title "CodeGraphy ${version} for macOS" --notes-file "$notes" + fi + gh release upload "$RELEASE_TAG" "$DMG_PATH" "$CHECKSUM_PATH" --clobber + + - name: Remove signing keychain + if: always() && steps.signing.outcome != 'skipped' + shell: bash + run: | + if [[ -n "${KEYCHAIN_PATH:-}" && -f "$KEYCHAIN_PATH" ]]; then + security delete-keychain "$KEYCHAIN_PATH" + fi diff --git a/.gitignore b/.gitignore index e533556bef..b4c941d066 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,11 @@ dist/ .DS_Store dist-e2e/ /artifacts/ +apps/desktop/src-tauri/binaries/* +apps/desktop/src-tauri/gen/ +apps/desktop/src-tauri/runtime/ +apps/desktop/src-tauri/target/ +apps/desktop/public/material-icons/ .worktrees/ .playwright-mcp/ .playwright-cli/ diff --git a/CONTEXT.md b/CONTEXT.md index fefdb4dbf4..e27b68b446 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -127,6 +127,7 @@ Full and incremental updates acquire writer ownership before their final discove | Surface | Ownership | |---|---| | **Core Package** | `@codegraphy-dev/core` owns headless Indexing, Graph Cache storage, plugin processing, Graph Query, and the `codegraphy` CLI. | +| **macOS Desktop App** | `@codegraphy-dev/desktop` owns the Tauri window, File and Folder hierarchy, lightweight editor, safe File access, Core process lifecycle, and adapters over Core and the graph renderer. | | **VS Code Extension** | Owns VS Code lifecycle, the Graph View, editor actions, workspace settings UI, and adapters over Core and the renderer. | | **tldraw Interface** | `@codegraphy-dev/tldraw` owns its launcher, tldraw document lifecycle, native shapes, controls, and adapters over Core and renderer physics. | | **Graph Renderer** | `@codegraphy-dev/graph-renderer` owns WebGPU drawing and deterministic WebAssembly physics. It does not own product settings, persistence, or plugins. | @@ -185,10 +186,12 @@ Extension chrome inherits the active VS Code theme. Graph Data Color may encode ## Package Boundaries +- `apps/desktop` owns the macOS product surface and its local Core process boundary. - `packages/core` owns shared engine behavior and the CLI. - `packages/extension` owns the VS Code product surface. - `packages/tldraw` owns the tldraw offline product surface and launcher. - `packages/graph-renderer` owns graph drawing and physics. +- `packages/graph-renderer` owns WebGPU drawing, WebAssembly physics, and deterministic visual semantics shared by interfaces, including Material Icon Theme matching, graph sizing metrics, icon geometry, and renderer input defaults. Each interface still owns and resolves its host theme colors. - `packages/plugin-api` owns public Core plugin contracts. - `packages/extension-plugin-api` owns public VS Code Extension plugin contracts. - `packages/plugin-*` own optional Core or interface plugins. diff --git a/README.md b/README.md index 5f0c6d02d7..67ddee9766 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ CodeGraphy indexes a folder and projects its files and declarations into Nodes. It renders imports, calls, references, inheritance, containment, tests, and plugin-defined Relationships. -Explore the graph inside VS Code or as native shapes in tldraw offline. Search, Graph Scope, and persistent filters narrow the VS Code view. The Core engine also supports the terminal CLI and agent queries. +Explore the graph in the macOS desktop app, inside VS Code, or as native shapes in tldraw offline. Search, Graph Scope, and persistent filters narrow the VS Code view. The Core engine also supports the terminal CLI and agent queries. ![CodeGraphy Relationship Graph interaction demo](./docs/media/readme/relationship-graph-demo.gif) @@ -48,6 +48,7 @@ Join the [CodeGraphy Discord](https://discord.gg/Z75vbkt4Ry) for installation he | Capability | What it provides | |---|---| | Relationship Graph | File, folder, package, Symbol, and plugin-defined Nodes connected by typed Edges. | +| macOS desktop app | A focused File hierarchy, multi-language CodeMirror editor with Markdown preview, and WebGPU Relationship Graph backed by local Core. | | Search and filters | Temporary search plus workspace-local include and exclude rules. | | Graph Scope | One panel for Node Type and Edge Type visibility. | | Symbol Nodes | Functions, classes, interfaces, types, variables, constants, and language-specific declarations. | @@ -72,6 +73,12 @@ Join the [CodeGraphy Discord](https://discord.gg/Z75vbkt4Ry) for installation he ## Install +### macOS desktop app + +The Apple Silicon desktop app requires macOS 26 or later. The source, app bundle, DMG path, and release checks are in this repository, but there is no public download yet. Developer ID signing, notarization, and the installed-app acceptance check still gate the first release. + +Do not treat an ad-hoc development DMG as a production artifact. See the [desktop app guide](./apps/desktop/README.md) for current behavior and the exact release gate. + ### VS Code Extension 1. Install [CodeGraphy from the VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=codegraphy.codegraphy). @@ -199,16 +206,17 @@ A public `codegraphy/skills` repository will host the skill once published. `@codegraphy-dev/core` owns File Discovery, built-in analysis, Graph Cache watching, plugin activation, SQLite storage, Graph Query, and the CLI. It does not own rendering. -The VS Code extension connects Core to the editor lifecycle and React Graph View. It changes cached source facts only after an explicit Index or Re-index Workspace action. The tldraw interface connects Core data and shared physics to native tldraw shapes. +The macOS desktop app runs Core in a bundled local process and keeps File access and child-process control in Rust. Its webview combines a File hierarchy, CodeMirror, and the existing renderer. The VS Code extension connects Core to the editor lifecycle and React Graph View. The tldraw interface connects Core data and shared physics to native tldraw shapes. -`@codegraphy-dev/graph-renderer` owns WebGPU drawing and WebAssembly physics. Core plugins use `@codegraphy-dev/plugin-api`. VS Code Extension plugins use `@codegraphy-dev/extension-plugin-api`. +`@codegraphy-dev/graph-renderer` owns WebGPU drawing, WebAssembly physics, Node geometry, Material icon matching, connection sizing, and force-control semantics shared by the desktop app and VS Code extension. Interfaces still own host theme color resolution, settings persistence, and interaction policy. Core plugins use `@codegraphy-dev/plugin-api`. VS Code Extension plugins use `@codegraphy-dev/extension-plugin-api`. | Package | Role | |---|---| +| [`@codegraphy-dev/desktop`](./apps/desktop/README.md) | Tauri macOS app, local Core process boundary, File hierarchy, and editor. | | [`@codegraphy-dev/core`](./packages/core/README.md) | Shared indexing, cache, plugin, query, and CLI engine. | | [`@codegraphy-dev/extension`](./packages/extension/docs/README.md) | VS Code host and Graph View product integration. | | [`@codegraphy-dev/tldraw`](./packages/tldraw/README.md) | macOS launcher and native tldraw offline canvas integration. | -| [`@codegraphy-dev/graph-renderer`](./packages/graph-renderer/README.md) | WebGPU graph renderer and WebAssembly physics. | +| [`@codegraphy-dev/graph-renderer`](./packages/graph-renderer/README.md) | Shared graph rendering, visual geometry, Material icon matching, and WebAssembly physics. | | [`@codegraphy-dev/plugin-api`](./packages/plugin-api/README.md) | Public TypeScript contracts for Core plugins. | | [`@codegraphy-dev/extension-plugin-api`](./packages/extension-plugin-api/README.md) | Public TypeScript contracts for VS Code Extension plugins. | | `@codegraphy-dev/plugin-*` | Optional plugins for Core or an interface host. | diff --git a/apps/desktop/README.md b/apps/desktop/README.md new file mode 100644 index 0000000000..e0a21eb67d --- /dev/null +++ b/apps/desktop/README.md @@ -0,0 +1,80 @@ +# CodeGraphy for macOS + +CodeGraphy for macOS is a focused local code navigator. It puts the File and Folder hierarchy, a CodeMirror editor, and the Core-owned Relationship Graph in one window. + +There is no public download yet. The Apple Silicon app and DMG build locally, but the first public artifact still needs Developer ID signing, Apple notarization, and an installed-app acceptance check. Do not distribute an ad-hoc build as a release. + +## Requirements + +- Apple Silicon Mac +- macOS 26 or later +- WebGPU support in the system WKWebView + +Intel is not a supported release target yet. The graph renderer has no non-WebGPU implementation, so macOS 25 and earlier are not supported. + +## Current behavior + +- Open any local folder as a CodeGraphy Workspace. +- Switch workspaces from the toolbar or the native `File > Open Recent` menu. Missing recent folders stay visible as unavailable until the user clears the menu. +- Browse and filter a thin semantic File and Folder hierarchy. Arrow keys, Home, End, Enter, Space, type-ahead, `/`, and `Cmd+F` work while the hierarchy has focus. Up, Down, Home, End, and Right open each focused File without moving focus into the editor, so keyboard switching stays continuous. +- Keep rapid hierarchy navigation bounded to one active File request and one replaceable latest request. Repeated keys do not queue obsolete File reads or scroll frames after key release. +- Open UTF-8 text Files up to 5 MiB in CodeMirror with the maintained One Dark highlight theme and maintained language support for common source and data formats. Close File clears only the editor after the same dirty-edit confirmation used for switching Files. +- Edit Markdown as source, show a live rendered preview, or use a split source-and-preview view. Markdown preview code loads only for Markdown Files and stays independent of the Core plugin system. +- Save through an atomic replacement that preserves permissions and rejects an external edit conflict. +- Read or rebuild the workspace-owned `.codegraphy/graph.sqlite` Graph Cache through Core. +- Apply one-File incremental Indexing after a save. +- Show File and Folder Nodes with the extension's Material colors, icons, sizes, shapes, strokes, selection, labels, and Relationships through the existing WebGPU and WebAssembly graph renderer. The extension keeps its full Symbol support; the desktop Graph View is intentionally narrower for this release. +- Choose a File or Folder Node, drag any Node, pan the Graph Stage, zoom at the pointer, use Zoom In, Zoom Out, or Fit to Screen, and let the shared WebAssembly simulation settle after release. Clicking empty graph background clears graph selection without closing the editor File. +- Read the live ` Nodes ยท Relationships` count for the displayed File and Folder graph. +- Resize all three panes with pointer or keyboard separators. Pane proportions persist in local interface state and clamp to usable widths when the window changes. +- Hide the File hierarchy or Relationship Graph independently to focus on the open File. These interface preferences stay local and restore on the next launch. +- Show measured File-request latency only when Profile is enabled. The measurement starts before the Rust request and ends after two animation frames; it reports the latest, median, and p95 sample without claiming a CodeMirror-ready or GPU-frame-complete timestamp. +- Tune Repel Force, Center Force, and Link Distance from 30 to 150 live from Graph Settings. Reset restores the extension defaults. The desktop record persists in the workspace without restarting Core or Indexing. + +Source Files and the Graph Cache stay in the workspace. The app does not upload them. + +## Architecture + +The Tauri 2 Rust process owns the macOS window, folder picker, validated File reads and writes, and Core child-process lifecycle. The React webview owns the three-pane interface. A bundled Node 22.23.2 sidecar runs `@codegraphy-dev/core` over a JSON Lines request protocol. Tauri starts the sidecar during app setup so the first workspace does not also pay the process and Core import cost. + +Core still owns File Discovery, Tree-sitter Analysis, plugins, Indexing, Graph Cache storage, and graph queries. Rust does not reimplement graph behavior. `@codegraphy-dev/graph-renderer` owns WebGPU drawing, WebAssembly physics, Material icon matching, Node appearance, connection sizing, and the named force-control contract shared with the extension. Desktop pointer handling, host theme resolution, and settings persistence stay in the desktop interface. + +This boundary keeps the existing TypeScript Core and WebGPU renderer intact while the small Rust shell provides native macOS integration and safe local File access. + +## Development + +Run setup from the repository root: + +```bash +pnpm install --frozen-lockfile +``` + +Start the app with the desktop package as the working directory: + +```bash +pnpm --filter @codegraphy-dev/desktop dev +``` + +`build:sidecar` downloads the pinned Node archive, checks its SHA-256 digest, deploys the built Core runtime, removes development-only files, signs the staged native code, and imports Core plus every native parser before Tauri starts. + +Use the focused checks while changing the app: + +```bash +pnpm --filter @codegraphy-dev/desktop test +pnpm --filter @codegraphy-dev/desktop typecheck +cargo test --manifest-path apps/desktop/src-tauri/Cargo.toml --lib +``` + +## Local package check + +Use Xcode 26. Xcode 27 beta currently produces malformed release proc-macro binaries with Rust 1.96 on this project. + +```bash +DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer \ + pnpm --filter @codegraphy-dev/desktop build:bundle:ad-hoc +pnpm --filter @codegraphy-dev/desktop check:bundle +``` + +The ad-hoc configuration disables library validation because ad-hoc signatures have no shared Apple team. The production configuration does not have that entitlement. The verifier checks the app, the mounted DMG, the bundled Node and Core versions, all native modules, architecture, minimum macOS version, signatures, and a 220 MiB uncompressed runtime budget. + +Follow the [desktop release procedure](../../docs/RELEASING.md#macos-desktop-release) for production signing, notarization, draft assets, and the installed-app gate. diff --git a/apps/desktop/index.html b/apps/desktop/index.html new file mode 100644 index 0000000000..7038a60ad6 --- /dev/null +++ b/apps/desktop/index.html @@ -0,0 +1,13 @@ + + + + + + + CodeGraphy + + +
+ + + diff --git a/apps/desktop/package.json b/apps/desktop/package.json new file mode 100644 index 0000000000..9d2e7887c8 --- /dev/null +++ b/apps/desktop/package.json @@ -0,0 +1,50 @@ +{ + "name": "@codegraphy-dev/desktop", + "version": "0.1.0", + "private": true, + "description": "CodeGraphy desktop app for macOS", + "type": "module", + "engines": { + "node": "^22.14.0 || >=23.6.0" + }, + "scripts": { + "build": "pnpm run build:icons && vite build", + "build:bundle": "tauri build", + "build:bundle:ad-hoc": "tauri build --target aarch64-apple-darwin --bundles app,dmg --config src-tauri/tauri.ad-hoc.conf.json", + "build:icons": "node scripts/stage-material-icons.mjs", + "build:sidecar": "node scripts/stage-sidecar.mjs", + "check:bundle": "node scripts/verify-release-bundle.mjs", + "dev": "tauri dev --no-watch", + "lint": "eslint src scripts vite.config.ts vitest.config.ts", + "test": "vitest run --config vitest.config.ts", + "typecheck": "tsc --noEmit -p tsconfig.json" + }, + "dependencies": { + "@codegraphy-dev/core": "workspace:*", + "@codegraphy-dev/graph-renderer": "workspace:*", + "@codemirror/commands": "^6.10.4", + "@codemirror/language": "^6.12.4", + "@codemirror/language-data": "^6.5.2", + "@codemirror/search": "^6.7.1", + "@codemirror/state": "^6.7.1", + "@codemirror/theme-one-dark": "^6.1.3", + "@codemirror/view": "^6.43.8", + "@lezer/highlight": "^1.2.3", + "@tauri-apps/api": "^2.11.1", + "material-icon-theme": "5.33.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-markdown": "^10.1.0", + "remark-gfm": "^4.0.1" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.11.4", + "@types/react": "^18.2.48", + "@types/react-dom": "^18.2.18", + "@vitejs/plugin-react": "^4.2.1", + "jsdom": "^25.0.0", + "typescript": "^5.3.3", + "vite": "^6.2.0", + "vitest": "^3.0.0" + } +} diff --git a/apps/desktop/scripts/core-sidecar.mjs b/apps/desktop/scripts/core-sidecar.mjs new file mode 100644 index 0000000000..90acda256a --- /dev/null +++ b/apps/desktop/scripts/core-sidecar.mjs @@ -0,0 +1,185 @@ +#!/usr/bin/env node + +import { createInterface } from 'node:readline'; +import { pathToFileURL } from 'node:url'; +import { format } from 'node:util'; + +for (const method of ['debug', 'error', 'info', 'log', 'warn']) { + console[method] = (...values) => { + process.stderr.write(`${format(...values)}\n`); + }; +} + +const coreModuleUrl = process.env.CODEGRAPHY_DESKTOP_CORE_MODULE + ? pathToFileURL(process.env.CODEGRAPHY_DESKTOP_CORE_MODULE) + : new URL('./core/dist/index.js', import.meta.url); +const core = await import(coreModuleUrl.href); +const DESKTOP_INTERFACE_ID = 'codegraphy.desktop'; + +function write(message) { + process.stdout.write(`${JSON.stringify(message)}\n`); +} + +function isObject(value) { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function parseRequest(value) { + if (!isObject(value) || value.kind !== 'request' || !Number.isSafeInteger(value.id) || value.id < 0) { + throw new Error('Core request must have kind "request" and a non-negative safe integer id.'); + } + if (!['open', 'index', 'update', 'read-settings', 'write-settings'].includes(value.method) + || !isObject(value.params)) { + throw new Error('Core request method is not supported.'); + } + if (typeof value.params.workspaceRoot !== 'string' || value.params.workspaceRoot.length === 0) { + throw new Error('Core request requires params.workspaceRoot.'); + } + if (value.method === 'update' && ( + typeof value.params.relativePath !== 'string' || value.params.relativePath.length === 0 + )) { + throw new Error('Core update request requires params.relativePath.'); + } + if (value.method === 'write-settings' && !isObject(value.params.settings)) { + throw new Error('Core settings update requires params.settings.'); + } + return { + id: value.id, + method: value.method, + relativePath: value.params.relativePath, + settings: value.params.settings, + workspaceRoot: value.params.workspaceRoot, + }; +} + +let activeEngine; + +async function disposeActiveEngine() { + if (!activeEngine) return; + const previous = activeEngine; + activeEngine = undefined; + await previous.ready.catch(() => undefined); + previous.engine.dispose(); +} + +function startWorkspaceEngine(workspaceRoot) { + if (activeEngine?.workspaceRoot === workspaceRoot) return activeEngine; + if (activeEngine) throw new Error('The previous Core workspace engine is still active.'); + const engine = core.createCodeGraphyWorkspaceEngine({ workspaceRoot }); + const record = { engine, workspaceRoot, ready: engine.index() }; + activeEngine = record; + return record; +} + +function warmWorkspaceEngine(workspaceRoot) { + const record = startWorkspaceEngine(workspaceRoot); + void record.ready.catch(error => write({ + kind: 'event', + event: 'error', + message: error instanceof Error ? error.message : String(error), + workspaceRoot, + })); +} + +function graphRequest(request) { + return { + workspacePath: request.workspaceRoot, + projection: { nodeTypes: ['file', 'folder'] }, + }; +} + +function indexResult(result, graph) { + if (graph.kind !== 'ready') { + throw new Error('Core did not return the Graph Cache after Indexing.'); + } + return { + ...graph, + indexing: result.indexing, + discovery: { + indexedFiles: result.files.length, + totalFound: result.totalFound, + limitReached: result.limitReached, + }, + }; +} + +function readDesktopGraphSettings(workspaceRoot) { + const workspaceSettings = core.readCodeGraphyWorkspaceSettingsOrInitial(workspaceRoot); + return workspaceSettings.interfaces.find(entry => entry.id === DESKTOP_INTERFACE_ID)?.data ?? null; +} + +function writeDesktopGraphSettings(workspaceRoot, desktopSettings) { + const workspaceSettings = core.readCodeGraphyWorkspaceSettingsOrInitial(workspaceRoot); + core.writeCodeGraphyWorkspaceSettings(workspaceRoot, { + ...workspaceSettings, + interfaces: [ + ...workspaceSettings.interfaces.filter(entry => entry.id !== DESKTOP_INTERFACE_ID), + { id: DESKTOP_INTERFACE_ID, data: desktopSettings }, + ], + }); + return desktopSettings; +} + +async function runRequest(request) { + if (request.method === 'read-settings') { + return readDesktopGraphSettings(request.workspaceRoot); + } + if (request.method === 'write-settings') { + return writeDesktopGraphSettings(request.workspaceRoot, request.settings); + } + if (request.method === 'open') { + const cached = core.requestCodeGraphyWorkspaceGraph(graphRequest(request)); + if (cached.kind === 'ready') { + if (activeEngine?.workspaceRoot !== request.workspaceRoot) { + await disposeActiveEngine(); + warmWorkspaceEngine(request.workspaceRoot); + } + return cached; + } + if (cached.kind === 'unreadable') return cached; + } + + if (request.method === 'update') { + if (activeEngine?.workspaceRoot !== request.workspaceRoot) await disposeActiveEngine(); + const record = startWorkspaceEngine(request.workspaceRoot); + await record.ready; + write({ + kind: 'event', + event: 'indexing', + workspaceRoot: request.workspaceRoot, + filePaths: [request.relativePath], + }); + const result = await record.engine.applyChangedFiles([request.relativePath]); + return indexResult(result, core.requestCodeGraphyWorkspaceGraph(graphRequest(request))); + } + + await disposeActiveEngine(); + write({ + kind: 'event', + event: 'indexing', + workspaceRoot: request.workspaceRoot, + }); + const record = startWorkspaceEngine(request.workspaceRoot); + const result = await record.ready; + const response = indexResult(result, core.requestCodeGraphyWorkspaceGraph(graphRequest(request))); + return response; +} + +const lines = createInterface({ input: process.stdin, crlfDelay: Infinity }); +for await (const line of lines) { + let requestId = null; + try { + const request = parseRequest(JSON.parse(line)); + requestId = request.id; + const result = await runRequest(request); + write({ kind: 'response', id: request.id, outcome: 'success', result }); + } catch (error) { + write({ + kind: 'response', + id: requestId, + outcome: 'error', + error: error instanceof Error ? error.message : String(error), + }); + } +} +await disposeActiveEngine(); diff --git a/apps/desktop/scripts/prune-sidecar-runtime.d.mts b/apps/desktop/scripts/prune-sidecar-runtime.d.mts new file mode 100644 index 0000000000..52edd9f33f --- /dev/null +++ b/apps/desktop/scripts/prune-sidecar-runtime.d.mts @@ -0,0 +1,4 @@ +export declare function pruneDeployedRuntime( + runtimeRoot: string, + target: string, +): { directories: number; files: number }; diff --git a/apps/desktop/scripts/prune-sidecar-runtime.mjs b/apps/desktop/scripts/prune-sidecar-runtime.mjs new file mode 100644 index 0000000000..114c537b5d --- /dev/null +++ b/apps/desktop/scripts/prune-sidecar-runtime.mjs @@ -0,0 +1,78 @@ +import { lstatSync, readdirSync, rmSync } from 'node:fs'; +import path from 'node:path'; + +const developmentDirectories = new Set([ + '.github', + 'benchmark', + 'benchmarks', + 'docs', + 'example', + 'examples', + 'scripts', + 'src', + 'test', + 'tests', +]); + +const developmentFileExtensions = [ + '.d.ts', + '.d.ts.map', + '.map', + '.markdown', + '.md', + '.ts', + '.tsx', +]; + +const buildOnlyPackages = [ + 'node-addon-api', + 'npm-check-updates', + 'tree-sitter-cli', +]; + +function isDevelopmentFile(name) { + const lowerName = name.toLowerCase(); + return developmentFileExtensions.some(extension => lowerName.endsWith(extension)); +} + +function pruneDirectory(directory, nativePrebuildDirectory, counters) { + for (const entry of readdirSync(directory, { withFileTypes: true })) { + const entryPath = path.join(directory, entry.name); + if (entry.isSymbolicLink()) { + if (entry.name === '.bin') { + rmSync(entryPath, { force: true, recursive: true }); + counters.directories += 1; + } + continue; + } + if (entry.isDirectory()) { + const parentName = path.basename(directory); + const removePrebuild = parentName === 'prebuilds' + && !entry.name.startsWith(nativePrebuildDirectory); + if (developmentDirectories.has(entry.name) || entry.name === '.bin' || removePrebuild) { + rmSync(entryPath, { force: true, recursive: true }); + counters.directories += 1; + continue; + } + pruneDirectory(entryPath, nativePrebuildDirectory, counters); + continue; + } + if (entry.isFile() && isDevelopmentFile(entry.name)) { + rmSync(entryPath, { force: true }); + counters.files += 1; + } + } +} + +export function pruneDeployedRuntime(runtimeRoot, target) { + const nativePrebuildDirectory = target.startsWith('aarch64-') ? 'darwin-arm64' : 'darwin-x64'; + const counters = { directories: 0, files: 0 }; + if (!lstatSync(runtimeRoot).isDirectory()) throw new Error(`Runtime is not a directory: ${runtimeRoot}`); + pruneDirectory(runtimeRoot, nativePrebuildDirectory, counters); + for (const packageName of buildOnlyPackages) { + const packagePath = path.join(runtimeRoot, 'node_modules', packageName); + rmSync(packagePath, { force: true, recursive: true }); + counters.directories += 1; + } + return counters; +} diff --git a/apps/desktop/scripts/runtime-contract.d.mts b/apps/desktop/scripts/runtime-contract.d.mts new file mode 100644 index 0000000000..e4f9e4f3c4 --- /dev/null +++ b/apps/desktop/scripts/runtime-contract.d.mts @@ -0,0 +1,3 @@ +export const bundledNodeVersion: string; +export const nodeArchiveChecksums: Readonly>; +export const nativeRuntimeModules: readonly string[]; diff --git a/apps/desktop/scripts/runtime-contract.mjs b/apps/desktop/scripts/runtime-contract.mjs new file mode 100644 index 0000000000..b4203232e0 --- /dev/null +++ b/apps/desktop/scripts/runtime-contract.mjs @@ -0,0 +1,29 @@ +export const bundledNodeVersion = '22.23.2'; + +export const nodeArchiveChecksums = { + arm64: '61130f394c1630d211dd50aecc4353d379480f36d3ac913cd85dbba1aed585c6', + x64: '58e99022c2ff89395576cc7fd4d98cea24bb68081475d5f88b801ee8729fb026', +}; + +export const nativeRuntimeModules = [ + '@driftlog/tree-sitter-dart', + '@tree-sitter-grammars/tree-sitter-kotlin', + '@tree-sitter-grammars/tree-sitter-lua/bindings/node/index.js', + 'libsql', + 'tree-sitter', + 'tree-sitter-c', + 'tree-sitter-c-sharp', + 'tree-sitter-cpp', + 'tree-sitter-go', + 'tree-sitter-haskell', + 'tree-sitter-java', + 'tree-sitter-javascript', + 'tree-sitter-objc', + 'tree-sitter-php', + 'tree-sitter-python', + 'tree-sitter-ruby', + 'tree-sitter-rust', + 'tree-sitter-scala', + 'tree-sitter-swift', + 'tree-sitter-typescript', +]; diff --git a/apps/desktop/scripts/sign-native-runtime.d.mts b/apps/desktop/scripts/sign-native-runtime.d.mts new file mode 100644 index 0000000000..3c92e05807 --- /dev/null +++ b/apps/desktop/scripts/sign-native-runtime.d.mts @@ -0,0 +1,2 @@ +export function findNativeRuntimeCode(root: string): string[]; +export function signNativeRuntimeCode(root: string, identity: string): string[]; diff --git a/apps/desktop/scripts/sign-native-runtime.mjs b/apps/desktop/scripts/sign-native-runtime.mjs new file mode 100644 index 0000000000..d752e73c60 --- /dev/null +++ b/apps/desktop/scripts/sign-native-runtime.mjs @@ -0,0 +1,25 @@ +import { execFileSync } from 'node:child_process'; +import { readdirSync } from 'node:fs'; +import path from 'node:path'; + +export function findNativeRuntimeCode(root) { + const codePaths = []; + for (const entry of readdirSync(root, { withFileTypes: true })) { + const entryPath = path.join(root, entry.name); + if (entry.isDirectory()) codePaths.push(...findNativeRuntimeCode(entryPath)); + else if (entry.isFile() && (entry.name.endsWith('.node') || entry.name.endsWith('.dylib'))) { + codePaths.push(entryPath); + } + } + return codePaths.sort(); +} + +export function signNativeRuntimeCode(root, identity) { + const codePaths = findNativeRuntimeCode(root); + for (const codePath of codePaths) { + const timestamp = identity === '-' ? '--timestamp=none' : '--timestamp'; + execFileSync('codesign', ['--force', '--sign', identity, timestamp, codePath], { stdio: 'inherit' }); + execFileSync('codesign', ['--verify', '--strict', codePath], { stdio: 'inherit' }); + } + return codePaths; +} diff --git a/apps/desktop/scripts/stage-material-icons.mjs b/apps/desktop/scripts/stage-material-icons.mjs new file mode 100644 index 0000000000..42b79c79ba --- /dev/null +++ b/apps/desktop/scripts/stage-material-icons.mjs @@ -0,0 +1,49 @@ +import { cp, copyFile, mkdir, readdir, rm, stat } from 'node:fs/promises'; +import { createRequire } from 'node:module'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const require = createRequire(import.meta.url); +const scriptDirectory = dirname(fileURLToPath(import.meta.url)); +const desktopRoot = resolve(scriptDirectory, '..'); +const packageRoot = dirname(require.resolve('material-icon-theme/package.json')); +const sourceManifest = join(packageRoot, 'dist', 'material-icons.json'); +const sourceIcons = join(packageRoot, 'icons'); +const stagingRoot = join(desktopRoot, 'public', 'material-icons'); +const stagedManifest = join(stagingRoot, 'dist', 'material-icons.json'); +const stagedIcons = join(stagingRoot, 'icons'); + +await rm(stagingRoot, { force: true, recursive: true }); +await mkdir(dirname(stagedManifest), { recursive: true }); +await copyFile(sourceManifest, stagedManifest); +await cp(sourceIcons, stagedIcons, { recursive: true }); + +const staged = await measureFiles(stagingRoot); +console.log(`Staged Material Icon Theme: ${staged.count} files, ${formatBytes(staged.bytes)}.`); + +async function measureFiles(root) { + const entries = (await readdir(root, { withFileTypes: true })) + .sort((left, right) => left.name.localeCompare(right.name)); + let count = 0; + let bytes = 0; + + for (const entry of entries) { + const entryPath = join(root, entry.name); + if (entry.isDirectory()) { + const nested = await measureFiles(entryPath); + count += nested.count; + bytes += nested.bytes; + } else if (entry.isFile()) { + count += 1; + bytes += (await stat(entryPath)).size; + } + } + + return { count, bytes }; +} + +function formatBytes(bytes) { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`; +} diff --git a/apps/desktop/scripts/stage-sidecar.mjs b/apps/desktop/scripts/stage-sidecar.mjs new file mode 100644 index 0000000000..39bb11e82e --- /dev/null +++ b/apps/desktop/scripts/stage-sidecar.mjs @@ -0,0 +1,141 @@ +import { execFileSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { + chmodSync, + copyFileSync, + cpSync, + existsSync, + mkdirSync, + readFileSync, + rmSync, + writeFileSync, +} from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { pruneDeployedRuntime } from './prune-sidecar-runtime.mjs'; +import { + bundledNodeVersion, + nativeRuntimeModules, + nodeArchiveChecksums, +} from './runtime-contract.mjs'; +import { signNativeRuntimeCode } from './sign-native-runtime.mjs'; + +const appRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const repoRoot = path.resolve(appRoot, '../..'); +const runtimeRoot = path.join(appRoot, 'src-tauri', 'runtime'); +const binaryRoot = path.join(appRoot, 'src-tauri', 'binaries'); +const nodeVersion = bundledNodeVersion; +const hostTargetByArchitecture = { + arm64: 'aarch64-apple-darwin', + x64: 'x86_64-apple-darwin', +}; +const nodeDistributionArchitecture = { + arm64: 'arm64', + x64: 'x64', +}; + +if (process.platform !== 'darwin') { + throw new Error('CodeGraphy desktop sidecars must be staged on macOS.'); +} + +const hostTarget = hostTargetByArchitecture[process.arch]; +if (!hostTarget) throw new Error(`Unsupported macOS architecture: ${process.arch}`); +const target = process.env.CODEGRAPHY_DESKTOP_TARGET ?? hostTarget; +if (target !== hostTarget) { + throw new Error(`Cannot stage ${hostTarget} native Core modules for ${target}.`); +} + +const runtimeCache = process.env.CODEGRAPHY_DESKTOP_RUNTIME_CACHE + ?? path.join(os.homedir(), 'Library', 'Caches', 'CodeGraphy', 'desktop-runtime'); +const nodeArchiveName = `node-v${nodeVersion}-darwin-${nodeDistributionArchitecture[process.arch]}.tar.gz`; +const nodeDistributionRoot = path.join(runtimeCache, nodeArchiveName.replace(/\.tar\.gz$/u, '')); +const nodeExecutable = path.join(nodeDistributionRoot, 'bin', 'node'); +const expectedArchiveChecksum = nodeArchiveChecksums[process.arch]; +if (!expectedArchiveChecksum) throw new Error(`No Node checksum for architecture: ${process.arch}`); +const checksumMarker = path.join(nodeDistributionRoot, '.codegraphy-checksum'); +const cachedChecksum = existsSync(checksumMarker) ? readFileSync(checksumMarker, 'utf8').trim() : undefined; +if (!existsSync(nodeExecutable) || cachedChecksum !== expectedArchiveChecksum) { + mkdirSync(runtimeCache, { recursive: true }); + const archivePath = path.join(runtimeCache, nodeArchiveName); + if (!existsSync(archivePath)) { + execFileSync('curl', [ + '--fail', + '--location', + '--output', + archivePath, + `https://nodejs.org/dist/v${nodeVersion}/${nodeArchiveName}`, + ], { stdio: 'inherit' }); + } + const archiveChecksum = createHash('sha256').update(readFileSync(archivePath)).digest('hex'); + if (archiveChecksum !== expectedArchiveChecksum) { + throw new Error(`Node archive checksum mismatch for ${nodeArchiveName}.`); + } + rmSync(nodeDistributionRoot, { force: true, recursive: true }); + execFileSync('tar', ['-xzf', archivePath, '-C', runtimeCache], { stdio: 'inherit' }); + writeFileSync(checksumMarker, `${expectedArchiveChecksum}\n`); +} +execFileSync('codesign', ['--verify', '--strict', nodeExecutable], { stdio: 'inherit' }); +execFileSync(nodeExecutable, ['--version'], { stdio: 'inherit' }); + +execFileSync('pnpm', [ + '-w', + 'exec', + 'turbo', + 'run', + 'build', + '--filter=@codegraphy-dev/core...', +], { cwd: repoRoot, stdio: 'inherit' }); + +rmSync(runtimeRoot, { force: true, recursive: true }); +mkdirSync(runtimeRoot, { recursive: true }); +execFileSync('pnpm', [ + '--config.node-linker=hoisted', + '--filter', + '@codegraphy-dev/core', + 'deploy', + '--legacy', + '--prod', + path.join(runtimeRoot, 'core'), +], { cwd: repoRoot, stdio: 'inherit' }); +// Hoisted deploy can rewrite shared workspace linker metadata. Restore the +// checked-in lockfile layout before any repository build or test continues. +execFileSync('pnpm', ['install', '--frozen-lockfile'], { + cwd: repoRoot, + env: { ...process.env, CI: 'true' }, + stdio: 'inherit', +}); +const pruned = pruneDeployedRuntime(path.join(runtimeRoot, 'core'), target); +process.stdout.write(`Pruned ${pruned.directories} development directories and ${pruned.files} development files.\n`); +copyFileSync(path.join(appRoot, 'scripts', 'core-sidecar.mjs'), path.join(runtimeRoot, 'sidecar.mjs')); + +const signingIdentity = process.env.APPLE_SIGNING_IDENTITY ?? '-'; +const signedNativeCode = signNativeRuntimeCode(path.join(runtimeRoot, 'core'), signingIdentity); +process.stdout.write(`Signed ${signedNativeCode.length} native Core modules with the bundle identity.\n`); + +mkdirSync(binaryRoot, { recursive: true }); +const sidecarPath = path.join(binaryRoot, `codegraphy-core-${target}`); +copyFileSync(nodeExecutable, sidecarPath); +chmodSync(sidecarPath, 0o755); +execFileSync('strip', ['-x', sidecarPath], { stdio: 'inherit' }); +const sidecarSigningArguments = signingIdentity === '-' + ? ['--force', '--sign', '-', '--timestamp=none', sidecarPath] + : [ + '--force', + '--sign', signingIdentity, + '--timestamp', + '--options', 'runtime', + '--entitlements', path.join(appRoot, 'src-tauri', 'Entitlements.plist'), + sidecarPath, + ]; +execFileSync('codesign', sidecarSigningArguments, { stdio: 'inherit' }); +execFileSync(sidecarPath, ['--version'], { stdio: 'inherit' }); +const coreModuleUrl = pathToFileURL(path.join(runtimeRoot, 'core', 'dist', 'index.js')).href; +const runtimeProbe = `await Promise.all(${JSON.stringify(nativeRuntimeModules)}.map(module => import(module))); await import(${JSON.stringify(coreModuleUrl)});`; +execFileSync(sidecarPath, ['--input-type=module', '--eval', runtimeProbe], { + cwd: path.join(runtimeRoot, 'core'), + stdio: 'inherit', +}); + +const licensesSource = path.join(repoRoot, 'LICENSE'); +cpSync(licensesSource, path.join(runtimeRoot, 'LICENSE')); diff --git a/apps/desktop/scripts/verify-release-bundle.d.mts b/apps/desktop/scripts/verify-release-bundle.d.mts new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/apps/desktop/scripts/verify-release-bundle.d.mts @@ -0,0 +1 @@ +export {}; diff --git a/apps/desktop/scripts/verify-release-bundle.mjs b/apps/desktop/scripts/verify-release-bundle.mjs new file mode 100644 index 0000000000..7c0768c400 --- /dev/null +++ b/apps/desktop/scripts/verify-release-bundle.mjs @@ -0,0 +1,164 @@ +import { execFileSync, spawnSync } from 'node:child_process'; +import { + lstatSync, + mkdtempSync, + readdirSync, + readFileSync, + rmSync, + statSync, +} from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { bundledNodeVersion, nativeRuntimeModules } from './runtime-contract.mjs'; + +const appRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const packageManifest = JSON.parse(readFileSync(path.join(appRoot, 'package.json'), 'utf8')); +const target = process.env.CODEGRAPHY_DESKTOP_TARGET ?? 'aarch64-apple-darwin'; +const bundleRoot = process.env.CODEGRAPHY_DESKTOP_BUNDLE_ROOT + ?? path.join(appRoot, 'src-tauri', 'target', target, 'release', 'bundle'); +const appPath = path.join(bundleRoot, 'macos', 'CodeGraphy.app'); +const dmgDirectory = path.join(bundleRoot, 'dmg'); +const signedRelease = process.env.CODEGRAPHY_DESKTOP_REQUIRE_RELEASE_SIGNATURE === '1'; +const maxRuntimeBytes = 220 * 1024 * 1024; + +function assert(condition, message) { + if (!condition) throw new Error(message); +} + +function command(executable, args, options = {}) { + const output = execFileSync(executable, args, { encoding: 'utf8', ...options }); + return typeof output === 'string' ? output.trim() : ''; +} + +function walkFiles(root) { + const files = []; + for (const entry of readdirSync(root, { withFileTypes: true })) { + const entryPath = path.join(root, entry.name); + if (entry.isSymbolicLink()) { + throw new Error(`Release bundle contains a symbolic link: ${entryPath}`); + } + if (entry.isDirectory()) files.push(...walkFiles(entryPath)); + else if (entry.isFile()) files.push(entryPath); + } + return files; +} + +function directoryBytes(root) { + return walkFiles(root).reduce((total, file) => total + statSync(file).size, 0); +} + +function plistValue(infoPath, key) { + return command('/usr/libexec/PlistBuddy', ['-c', `Print :${key}`, infoPath]); +} + +function signatureDetails(targetPath) { + const result = spawnSync('codesign', ['-dv', '--verbose=4', targetPath], { encoding: 'utf8' }); + assert(result.status === 0, `Unable to inspect signature: ${targetPath}\n${result.stderr}`); + return result.stderr; +} + +function teamIdentifier(targetPath) { + return signatureDetails(targetPath).match(/^TeamIdentifier=(.+)$/mu)?.[1]; +} + +function verifySignature(targetPath, expectedTeam) { + command('codesign', ['--verify', '--strict', '--verbose=2', targetPath], { stdio: 'inherit' }); + if (expectedTeam) { + assert(teamIdentifier(targetPath) === expectedTeam, `Signing team mismatch: ${targetPath}`); + } +} + +function findDmg() { + const images = readdirSync(dmgDirectory) + .filter(name => name.endsWith('.dmg')) + .map(name => path.join(dmgDirectory, name)); + assert(images.length === 1, `Expected one DMG in ${dmgDirectory}; found ${images.length}.`); + return images[0]; +} + +function verifyInstalledImage(dmgPath, expectedTeam) { + const mountPoint = mkdtempSync(path.join(os.tmpdir(), 'codegraphy-dmg-')); + try { + command('diskutil', [ + 'image', + 'attach', + '--mountOptions', 'nobrowse', + '--readOnly', + '--mountPoint', mountPoint, + dmgPath, + ], { stdio: 'inherit' }); + const mountedApp = path.join(mountPoint, 'CodeGraphy.app'); + assert(lstatSync(mountedApp).isDirectory(), 'The DMG does not contain CodeGraphy.app.'); + command('codesign', ['--verify', '--deep', '--strict', '--verbose=2', mountedApp], { stdio: 'inherit' }); + if (expectedTeam) { + assert(teamIdentifier(mountedApp) === expectedTeam, 'The DMG app has the wrong signing team.'); + } + } finally { + try { + command('diskutil', ['eject', mountPoint], { stdio: 'inherit' }); + } finally { + rmSync(mountPoint, { force: true, recursive: true }); + } + } +} + +assert(process.platform === 'darwin', 'Desktop release verification must run on macOS.'); +assert(target === 'aarch64-apple-darwin', `Unsupported release target: ${target}`); +assert(lstatSync(appPath).isDirectory(), `Missing app bundle: ${appPath}`); + +const infoPath = path.join(appPath, 'Contents', 'Info.plist'); +const macOsRoot = path.join(appPath, 'Contents', 'MacOS'); +const resourcesRoot = path.join(appPath, 'Contents', 'Resources', 'runtime'); +const nodePath = path.join(macOsRoot, 'codegraphy-core'); +const appExecutable = path.join(macOsRoot, 'codegraphy-desktop'); +const coreRoot = path.join(resourcesRoot, 'core'); +const coreModule = path.join(coreRoot, 'dist', 'index.js'); +const sidecarScript = path.join(resourcesRoot, 'sidecar.mjs'); + +for (const requiredPath of [infoPath, nodePath, appExecutable, coreModule, sidecarScript]) { + assert(lstatSync(requiredPath).isFile(), `Missing release file: ${requiredPath}`); +} + +assert(plistValue(infoPath, 'CFBundleShortVersionString') === packageManifest.version, 'App version does not match package.json.'); +assert(plistValue(infoPath, 'LSMinimumSystemVersion') === '26.0', 'App minimum system version must be macOS 26.0.'); +assert(command('lipo', ['-archs', nodePath]) === 'arm64', 'Bundled Node must contain only arm64 code.'); +assert(command('lipo', ['-archs', appExecutable]) === 'arm64', 'Desktop executable must contain only arm64 code.'); +assert(command(nodePath, ['--version']) === `v${bundledNodeVersion}`, 'Bundled Node version is incorrect.'); + +const runtimeBytes = directoryBytes(coreRoot) + statSync(nodePath).size; +assert(runtimeBytes <= maxRuntimeBytes, `Bundled Core runtime exceeds 220 MiB: ${runtimeBytes} bytes.`); +const probe = `await Promise.all(${JSON.stringify(nativeRuntimeModules)}.map(module => import(module))); await import(${JSON.stringify(pathToFileURL(coreModule).href)});`; +command(nodePath, ['--input-type=module', '--eval', probe], { cwd: coreRoot, stdio: 'inherit' }); + +command('codesign', ['--verify', '--deep', '--strict', '--verbose=2', appPath], { stdio: 'inherit' }); +const dmgPath = findDmg(); +command('hdiutil', ['verify', dmgPath], { stdio: 'inherit' }); + +let signingTeam; +if (signedRelease) { + signingTeam = teamIdentifier(appPath); + assert(signingTeam && signingTeam !== 'not set', 'Release app does not have a Developer ID signing team.'); + const nestedCode = walkFiles(appPath).filter(file => ( + file === appExecutable + || file === nodePath + || file.endsWith('.dylib') + || file.endsWith('.node') + )); + for (const codePath of nestedCode) verifySignature(codePath, signingTeam); + verifySignature(dmgPath, signingTeam); + command('spctl', ['--assess', '--type', 'execute', '--verbose=4', appPath], { stdio: 'inherit' }); + command('spctl', [ + '--assess', + '--type', 'open', + '--context', 'context:primary-signature', + '--verbose=4', + dmgPath, + ], { stdio: 'inherit' }); + command('xcrun', ['stapler', 'validate', appPath], { stdio: 'inherit' }); + command('xcrun', ['stapler', 'validate', dmgPath], { stdio: 'inherit' }); +} + +verifyInstalledImage(dmgPath, signingTeam); +process.stdout.write(`Verified CodeGraphy ${packageManifest.version} for Apple Silicon (${Math.ceil(runtimeBytes / 1024 / 1024)} MiB Core runtime).\n`); +process.stdout.write(`${dmgPath}\n`); diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock new file mode 100644 index 0000000000..f8204dd523 --- /dev/null +++ b/apps/desktop/src-tauri/Cargo.lock @@ -0,0 +1,4688 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.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_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 = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "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 = "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.13.1", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.19", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "codegraphy-desktop" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "sha2", + "tauri", + "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-shell", + "tempfile", + "tokio", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.1", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.1", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[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.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.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 = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.1", + "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.3", +] + +[[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.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.4+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "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 = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[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.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-executor" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +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", + "libc", + "wasi", +] + +[[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", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.1", + "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 = "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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" + +[[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.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.13.1", + "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.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2026a5056764a10b2bf5d56488cba40da507f5493a6a429340e2004d9ed085fa" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "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 = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.19", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.1", + "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.5.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.13.1", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.1", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.1", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.1", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "open" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9cfef937e9c486488c7e3d949ae31c0f1d06bdacd75b99c086cb35356e30408" +dependencies = [ + "dunce", + "is-wsl", + "libc", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[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 = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.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 = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[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.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.13+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + +[[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 = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.19", +] + +[[package]] +name = "ref-cast" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[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.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.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.13.1", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[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.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.2", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0" +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", + "digest", +] + +[[package]] +name = "shared_child" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" +dependencies = [ + "libc", + "sigchld", + "windows-sys 0.60.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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" +dependencies = [ + "libc", + "os_pipe", + "signal-hook", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +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 = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.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 = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +dependencies = [ + "bitflags 2.13.1", + "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 = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "667b20e2726d572dea2de7370da16e188eb06008faf9a92fab7cdc46791190b5" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.19", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.119", + "tauri-utils", + "thiserror 2.0.19", + "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 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2d3c1dbe38037e7f590cdf2492594d5ceebe031e7bc7e827509b22a999d2940" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.19", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "objc2-foundation", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.19", + "toml 1.1.4+spec-1.1.0", + "url", +] + +[[package]] +name = "tauri-plugin-shell" +version = "2.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8457dbf9e2bab1edd8df22bb2c20857a59a9868e79cb3eac5ed639eec4d0c73b" +dependencies = [ + "encoding_rs", + "log", + "open", + "os_pipe", + "regex", + "schemars 0.8.22", + "serde", + "serde_json", + "shared_child", + "tauri", + "tauri-plugin", + "thiserror 2.0.19", + "tokio", +] + +[[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.19", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.19", + "toml 1.1.4+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.4+spec-1.1.0", +] + +[[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.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = [ + "thiserror-impl 2.0.19", +] + +[[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.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "time" +version = "0.3.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" + +[[package]] +name = "time-macros" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_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.13.1", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "045979e3f037cd18ad1cb2a419dfda133c5c29c9f3453370079f2255d46c257e" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.19", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "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.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "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.19", + "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.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.19", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml new file mode 100644 index 0000000000..202375f340 --- /dev/null +++ b/apps/desktop/src-tauri/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "codegraphy-desktop" +version = "0.1.0" +description = "CodeGraphy desktop app for macOS" +authors = ["Joe Soboleski"] +edition = "2024" +license = "MIT" +repository = "https://github.com/joesobo/CodeGraphyV4" +rust-version = "1.85" + +[lib] +name = "codegraphy_desktop_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2.5.4", features = [] } + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +sha2 = "0.10" +tauri = { version = "2.11.5", features = [] } +tauri-plugin-dialog = "2.7.2" +tauri-plugin-shell = "2.3.5" +tempfile = "3.23" +tokio = { version = "1.48", features = ["io-util", "process", "sync"] } + +[profile.release] +codegen-units = 1 +lto = true +opt-level = "s" +panic = "abort" +# Do not strip the complete Cargo profile. On macOS that also strips host +# proc-macro dylibs, which makes the next release build unable to load them. diff --git a/apps/desktop/src-tauri/Entitlements.ad-hoc.plist b/apps/desktop/src-tauri/Entitlements.ad-hoc.plist new file mode 100644 index 0000000000..f2eb2ecb0a --- /dev/null +++ b/apps/desktop/src-tauri/Entitlements.ad-hoc.plist @@ -0,0 +1,12 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + + diff --git a/apps/desktop/src-tauri/Entitlements.plist b/apps/desktop/src-tauri/Entitlements.plist new file mode 100644 index 0000000000..9ab52e13b3 --- /dev/null +++ b/apps/desktop/src-tauri/Entitlements.plist @@ -0,0 +1,10 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + + diff --git a/apps/desktop/src-tauri/build.rs b/apps/desktop/src-tauri/build.rs new file mode 100644 index 0000000000..d860e1e6a7 --- /dev/null +++ b/apps/desktop/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/apps/desktop/src-tauri/capabilities/default.json b/apps/desktop/src-tauri/capabilities/default.json new file mode 100644 index 0000000000..c3ce9af644 --- /dev/null +++ b/apps/desktop/src-tauri/capabilities/default.json @@ -0,0 +1,7 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "desktop", + "description": "CodeGraphy desktop window capabilities", + "windows": ["main"], + "permissions": ["core:default"] +} diff --git a/apps/desktop/src-tauri/icons/icon.icns b/apps/desktop/src-tauri/icons/icon.icns new file mode 100644 index 0000000000..c02930c7ef Binary files /dev/null and b/apps/desktop/src-tauri/icons/icon.icns differ diff --git a/apps/desktop/src-tauri/icons/icon.png b/apps/desktop/src-tauri/icons/icon.png new file mode 100644 index 0000000000..892a7dfbe1 Binary files /dev/null and b/apps/desktop/src-tauri/icons/icon.png differ diff --git a/apps/desktop/src-tauri/rust-toolchain.toml b/apps/desktop/src-tauri/rust-toolchain.toml new file mode 100644 index 0000000000..bf0cec7001 --- /dev/null +++ b/apps/desktop/src-tauri/rust-toolchain.toml @@ -0,0 +1,5 @@ +[toolchain] +channel = "1.96.0" +components = ["clippy", "rustfmt"] +profile = "minimal" +targets = ["aarch64-apple-darwin"] diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs new file mode 100644 index 0000000000..699137a063 --- /dev/null +++ b/apps/desktop/src-tauri/src/lib.rs @@ -0,0 +1,991 @@ +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value, json}; +use sha2::{Digest, Sha256}; +use std::fs::{self, File}; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; +use std::sync::RwLock; +use tauri::async_runtime::{Mutex, Receiver}; +use tauri::menu::{Menu, MenuItem, PredefinedMenuItem, Submenu}; +use tauri::{AppHandle, Emitter, Manager, State}; +use tauri_plugin_dialog::DialogExt; +use tauri_plugin_shell::ShellExt; +use tauri_plugin_shell::process::{CommandChild, CommandEvent}; + +const MAX_EDITABLE_FILE_BYTES: u64 = 5 * 1024 * 1024; +const MAX_RECENT_WORKSPACES: usize = 8; +const RECENT_WORKSPACES_FILE_NAME: &str = "recent-workspaces.json"; + +const MENU_OPEN_WORKSPACE: &str = "file.open-workspace"; +const MENU_OPEN_RECENT_PREFIX: &str = "file.open-recent."; +const MENU_CLEAR_RECENT: &str = "file.clear-recent"; +const MENU_CLOSE_FILE: &str = "file.close-file"; +const MENU_CLOSE_WORKSPACE: &str = "file.close-workspace"; +const MENU_SAVE: &str = "file.save"; + +const EVENT_OPEN_WORKSPACE: &str = "desktop-open-workspace"; +const EVENT_OPEN_RECENT_WORKSPACE: &str = "desktop-open-recent"; +const EVENT_RECENT_WORKSPACES_CHANGED: &str = "desktop-recent-workspaces-changed"; +const EVENT_CLOSE_FILE: &str = "desktop-close-file"; +const EVENT_CLOSE_WORKSPACE: &str = "desktop-close-workspace"; +const EVENT_SAVE: &str = "desktop-save"; + +#[derive(Default)] +struct WorkspaceState { + root: RwLock>, +} + +#[derive(Debug, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +struct RecentWorkspaceFile { + paths: Vec, +} + +struct RecentWorkspaceStore { + file_path: PathBuf, + paths: RwLock>, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +#[serde(rename_all = "camelCase")] +struct RecentWorkspace { + path: String, + name: String, + available: bool, +} + +impl RecentWorkspaceStore { + fn load(file_path: PathBuf) -> Result { + let paths = match fs::read(&file_path) { + Ok(bytes) => { + let stored: RecentWorkspaceFile = serde_json::from_slice(&bytes) + .map_err(|error| format!("Unable to read recent workspaces: {error}"))?; + normalize_recent_workspace_paths(stored.paths) + } + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Vec::new(), + Err(error) => return Err(format!("Unable to read recent workspaces: {error}")), + }; + Ok(Self { + file_path, + paths: RwLock::new(paths), + }) + } + + fn list(&self) -> Result, String> { + self.paths + .read() + .map_err(|_| "Recent workspace state is unavailable.".to_string())? + .iter() + .map(|path| { + let name = path + .file_name() + .and_then(|value| value.to_str()) + .filter(|value| !value.is_empty()) + .map(str::to_owned) + .unwrap_or_else(|| path.to_string_lossy().into_owned()); + Ok(RecentWorkspace { + path: path.to_string_lossy().into_owned(), + name, + available: path.is_dir(), + }) + }) + .collect() + } + + fn remember(&self, workspace_root: &Path) -> Result<(), String> { + let canonical = canonical_workspace_root(workspace_root)?; + let mut paths = self + .paths + .write() + .map_err(|_| "Recent workspace state is unavailable.".to_string())?; + paths.retain(|path| path != &canonical); + paths.insert(0, canonical); + paths.truncate(MAX_RECENT_WORKSPACES); + self.persist(&paths) + } + + fn clear(&self) -> Result<(), String> { + let mut paths = self + .paths + .write() + .map_err(|_| "Recent workspace state is unavailable.".to_string())?; + paths.clear(); + self.persist(&paths) + } + + fn persist(&self, paths: &[PathBuf]) -> Result<(), String> { + let parent = self + .file_path + .parent() + .ok_or_else(|| "Unable to resolve the recent workspace folder.".to_string())?; + fs::create_dir_all(parent) + .map_err(|error| format!("Unable to create the app configuration folder: {error}"))?; + let mut temporary = tempfile::NamedTempFile::new_in(parent) + .map_err(|error| format!("Unable to create recent workspace data: {error}"))?; + serde_json::to_writer_pretty( + temporary.as_file_mut(), + &RecentWorkspaceFile { + paths: paths.to_vec(), + }, + ) + .map_err(|error| format!("Unable to write recent workspaces: {error}"))?; + temporary + .write_all(b"\n") + .and_then(|_| temporary.flush()) + .and_then(|_| temporary.as_file().sync_all()) + .map_err(|error| format!("Unable to write recent workspaces: {error}"))?; + temporary + .persist(&self.file_path) + .map_err(|error| format!("Unable to replace recent workspaces: {}", error.error))?; + Ok(()) + } +} + +fn normalize_recent_workspace_paths(paths: Vec) -> Vec { + let mut normalized = Vec::new(); + for path in paths { + if !normalized.contains(&path) { + normalized.push(path); + } + if normalized.len() == MAX_RECENT_WORKSPACES { + break; + } + } + normalized +} + +fn build_app_menu( + app: &AppHandle, + recents: &RecentWorkspaceStore, +) -> tauri::Result> { + let codegraphy = Submenu::with_items( + app, + "CodeGraphy", + true, + &[ + &PredefinedMenuItem::about(app, None, None)?, + &PredefinedMenuItem::separator(app)?, + &PredefinedMenuItem::services(app, None)?, + &PredefinedMenuItem::separator(app)?, + &PredefinedMenuItem::hide(app, None)?, + &PredefinedMenuItem::hide_others(app, None)?, + &PredefinedMenuItem::show_all(app, None)?, + &PredefinedMenuItem::separator(app)?, + &PredefinedMenuItem::quit(app, None)?, + ], + )?; + + let recent_workspaces = recents.list().map_err(std::io::Error::other)?; + let recent_items = recent_workspaces + .iter() + .enumerate() + .map(|(index, workspace)| { + MenuItem::with_id( + app, + format!("{MENU_OPEN_RECENT_PREFIX}{index}"), + &workspace.name, + workspace.available, + None::<&str>, + ) + }) + .collect::>>()?; + let empty_recent = MenuItem::with_id( + app, + "file.no-recent-workspaces", + "No Recent Workspaces", + false, + None::<&str>, + )?; + let clear_recent = MenuItem::with_id( + app, + MENU_CLEAR_RECENT, + "Clear Menu", + !recent_workspaces.is_empty(), + None::<&str>, + )?; + let open_recent = Submenu::new(app, "Open Recent", true)?; + if recent_items.is_empty() { + open_recent.append(&empty_recent)?; + } else { + for item in &recent_items { + open_recent.append(item)?; + } + } + open_recent.append(&PredefinedMenuItem::separator(app)?)?; + open_recent.append(&clear_recent)?; + + let open_workspace = MenuItem::with_id( + app, + MENU_OPEN_WORKSPACE, + "Open Workspaceโ€ฆ", + true, + Some("CmdOrCtrl+O"), + )?; + let close_workspace = MenuItem::with_id( + app, + MENU_CLOSE_WORKSPACE, + "Close Workspace", + true, + None::<&str>, + )?; + let close_file = MenuItem::with_id(app, MENU_CLOSE_FILE, "Close File", true, None::<&str>)?; + let save = MenuItem::with_id(app, MENU_SAVE, "Save", true, Some("CmdOrCtrl+S"))?; + let file = Submenu::with_items( + app, + "File", + true, + &[ + &open_workspace, + &open_recent, + &PredefinedMenuItem::separator(app)?, + &save, + &PredefinedMenuItem::separator(app)?, + &close_file, + &close_workspace, + ], + )?; + let edit = Submenu::with_items( + app, + "Edit", + true, + &[ + &PredefinedMenuItem::undo(app, None)?, + &PredefinedMenuItem::redo(app, None)?, + &PredefinedMenuItem::separator(app)?, + &PredefinedMenuItem::cut(app, None)?, + &PredefinedMenuItem::copy(app, None)?, + &PredefinedMenuItem::paste(app, None)?, + &PredefinedMenuItem::select_all(app, None)?, + ], + )?; + let window = Submenu::with_items( + app, + "Window", + true, + &[ + &PredefinedMenuItem::minimize(app, None)?, + &PredefinedMenuItem::maximize(app, Some("Zoom"))?, + &PredefinedMenuItem::separator(app)?, + &PredefinedMenuItem::fullscreen(app, None)?, + &PredefinedMenuItem::bring_all_to_front(app, None)?, + ], + )?; + Menu::with_items(app, &[&codegraphy, &file, &edit, &window]) +} + +fn refresh_app_menu(app: &AppHandle, recents: &RecentWorkspaceStore) -> Result<(), String> { + let menu = build_app_menu(app, recents) + .map_err(|error| format!("Unable to build the app menu: {error}"))?; + app.set_menu(menu) + .map_err(|error| format!("Unable to update the app menu: {error}"))?; + Ok(()) +} + +fn handle_menu_event(app: &AppHandle, menu_id: &str) { + let emit = |event: &str| { + let _ = app.emit(event, ()); + }; + match menu_id { + MENU_OPEN_WORKSPACE => emit(EVENT_OPEN_WORKSPACE), + MENU_CLEAR_RECENT => { + let recents = app.state::(); + if recents.clear().is_ok() && refresh_app_menu(app, &recents).is_ok() { + emit(EVENT_RECENT_WORKSPACES_CHANGED); + } + } + MENU_CLOSE_FILE => emit(EVENT_CLOSE_FILE), + MENU_CLOSE_WORKSPACE => emit(EVENT_CLOSE_WORKSPACE), + MENU_SAVE => emit(EVENT_SAVE), + _ => { + let Some(index) = menu_id + .strip_prefix(MENU_OPEN_RECENT_PREFIX) + .and_then(|value| value.parse::().ok()) + else { + return; + }; + let recents = app.state::(); + let Ok(workspaces) = recents.list() else { + return; + }; + if let Some(workspace) = workspaces + .get(index) + .filter(|workspace| workspace.available) + { + let _ = app.emit(EVENT_OPEN_RECENT_WORKSPACE, workspace.path.clone()); + } + } + } +} + +#[derive(Default)] +struct CoreService { + inner: Mutex, +} + +#[derive(Default)] +struct CoreServiceState { + process: Option, + next_request_id: u64, +} + +struct CoreProcess { + child: Option, + events: Receiver, +} + +enum CoreRequestAction { + Open, + Index, + ReadSettings, + Update { relative_path: String }, + WriteSettings { settings: Value }, +} + +impl CoreRequestAction { + fn method(&self) -> &'static str { + match self { + Self::Open => "open", + Self::Index => "index", + Self::ReadSettings => "read-settings", + Self::Update { .. } => "update", + Self::WriteSettings { .. } => "write-settings", + } + } +} + +impl Drop for CoreProcess { + fn drop(&mut self) { + if let Some(child) = self.child.take() { + let _ = child.kill(); + } + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct FileDocument { + path: String, + content: String, + revision: String, +} + +#[derive(Deserialize)] +#[serde(tag = "kind", rename_all = "lowercase")] +enum CoreMessage { + Event { + event: String, + #[serde(flatten)] + details: Map, + }, + Response { + id: Option, + #[serde(flatten)] + outcome: CoreResponseOutcome, + }, +} + +#[derive(Deserialize)] +#[serde(tag = "outcome", rename_all = "lowercase")] +enum CoreResponseOutcome { + Success { result: Value }, + Error { error: String }, +} + +fn sha256_hex(bytes: &[u8]) -> String { + Sha256::digest(bytes) + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} + +fn canonical_workspace_root(path: &Path) -> Result { + let root = path + .canonicalize() + .map_err(|error| format!("Unable to open workspace: {error}"))?; + if !root.is_dir() { + return Err("The selected workspace is not a folder.".to_string()); + } + Ok(root) +} + +fn resolve_workspace_file(root: &Path, relative_path: &str) -> Result { + let relative = Path::new(relative_path); + if relative.as_os_str().is_empty() || relative.is_absolute() { + return Err("File path must be relative to the active workspace.".to_string()); + } + let canonical_root = root + .canonicalize() + .map_err(|error| format!("Unable to resolve workspace: {error}"))?; + let candidate = canonical_root + .join(relative) + .canonicalize() + .map_err(|error| format!("Unable to resolve File: {error}"))?; + if !candidate.starts_with(&canonical_root) { + return Err("File path leaves the active workspace.".to_string()); + } + if !candidate.is_file() { + return Err("The selected path is not a File.".to_string()); + } + Ok(candidate) +} + +fn active_workspace_root(state: &WorkspaceState) -> Result { + state + .root + .read() + .map_err(|_| "Workspace state is unavailable.".to_string())? + .clone() + .ok_or_else(|| "Open a workspace first.".to_string()) +} + +fn read_file_document(root: &Path, relative_path: &str) -> Result { + let file_path = resolve_workspace_file(root, relative_path)?; + let metadata = file_path + .metadata() + .map_err(|error| format!("Unable to inspect File: {error}"))?; + if metadata.len() > MAX_EDITABLE_FILE_BYTES { + return Err("This File is larger than the 5 MiB editor limit.".to_string()); + } + let mut bytes = Vec::with_capacity(metadata.len() as usize); + File::open(&file_path) + .and_then(|mut file| file.read_to_end(&mut bytes)) + .map_err(|error| format!("Unable to read File: {error}"))?; + if bytes.contains(&0) { + return Err("Binary Files cannot be opened in the text editor.".to_string()); + } + let content = String::from_utf8(bytes.clone()) + .map_err(|_| "This File is not valid UTF-8 text.".to_string())?; + Ok(FileDocument { + path: relative_path.to_string(), + content, + revision: sha256_hex(&bytes), + }) +} + +fn save_file_document( + root: &Path, + relative_path: &str, + content: &str, + expected_revision: &str, +) -> Result { + let file_path = resolve_workspace_file(root, relative_path)?; + let existing = fs::read(&file_path).map_err(|error| format!("Unable to read File: {error}"))?; + if sha256_hex(&existing) != expected_revision { + return Err("The File changed outside CodeGraphy. Reopen it before saving.".to_string()); + } + if content.len() as u64 > MAX_EDITABLE_FILE_BYTES { + return Err("This File is larger than the 5 MiB editor limit.".to_string()); + } + let parent = file_path + .parent() + .ok_or_else(|| "Unable to resolve the File folder.".to_string())?; + let permissions = file_path + .metadata() + .map_err(|error| format!("Unable to inspect File: {error}"))? + .permissions(); + let mut temporary = tempfile::NamedTempFile::new_in(parent) + .map_err(|error| format!("Unable to create a safe save File: {error}"))?; + temporary + .write_all(content.as_bytes()) + .and_then(|_| temporary.flush()) + .and_then(|_| temporary.as_file().sync_all()) + .map_err(|error| format!("Unable to write File: {error}"))?; + temporary + .as_file() + .set_permissions(permissions) + .map_err(|error| format!("Unable to preserve File permissions: {error}"))?; + temporary + .persist(&file_path) + .map_err(|error| format!("Unable to replace File safely: {}", error.error))?; + read_file_document(root, relative_path) +} + +fn spawn_core_process(app: &AppHandle) -> Result { + let script = app + .path() + .resource_dir() + .map_err(|error| format!("Unable to locate app resources: {error}"))? + .join("runtime") + .join("sidecar.mjs"); + let (events, child) = app + .shell() + .sidecar("codegraphy-core") + .map_err(|error| format!("Unable to locate Core service: {error}"))? + .arg(script) + .spawn() + .map_err(|error| format!("Unable to start Core service: {error}"))?; + Ok(CoreProcess { + child: Some(child), + events, + }) +} + +fn build_core_request(request_id: u64, action: &CoreRequestAction, workspace_root: &Path) -> Value { + let mut params = json!({ + "workspaceRoot": workspace_root, + }); + if let CoreRequestAction::Update { relative_path } = action { + params["relativePath"] = Value::String(relative_path.clone()); + } + if let CoreRequestAction::WriteSettings { settings } = action { + params["settings"] = settings.clone(); + } + json!({ + "kind": "request", + "id": request_id, + "method": action.method(), + "params": params, + }) +} + +async fn request_core( + app: &AppHandle, + service: &CoreService, + action: &CoreRequestAction, + workspace_root: &Path, +) -> Result { + let mut state = service.inner.lock().await; + state.next_request_id += 1; + let request_id = state.next_request_id; + if state.process.is_none() { + state.process = Some(spawn_core_process(app)?); + } + let process = state + .process + .as_mut() + .ok_or_else(|| "Core service is unavailable.".to_string())?; + let request = build_core_request(request_id, action, workspace_root); + let mut request_bytes = serde_json::to_vec(&request) + .map_err(|error| format!("Unable to encode Core request: {error}"))?; + request_bytes.push(b'\n'); + process + .child + .as_mut() + .ok_or_else(|| "Core service stopped.".to_string())? + .write(&request_bytes) + .map_err(|error| format!("Unable to send Core request: {error}"))?; + + while let Some(event) = process.events.recv().await { + match event { + CommandEvent::Stdout(line) => { + let message: CoreMessage = match serde_json::from_slice(&line) { + Ok(message) => message, + Err(error) => { + state.process = None; + return Err(format!("Core service returned invalid data: {error}")); + } + }; + match message { + CoreMessage::Event { event, mut details } => { + details.insert("event".to_string(), Value::String(event)); + app.emit("core-service-event", details) + .map_err(|error| format!("Unable to report Core progress: {error}"))?; + } + CoreMessage::Response { id, outcome } if id == Some(request_id) => { + return match outcome { + CoreResponseOutcome::Success { result } => Ok(result), + CoreResponseOutcome::Error { error } => Err(error), + }; + } + CoreMessage::Response { .. } => { + state.process = None; + return Err("Core service returned a mismatched response.".to_string()); + } + } + } + CommandEvent::Stderr(line) => { + let diagnostic = String::from_utf8_lossy(&line).trim().to_string(); + if !diagnostic.is_empty() { + let _ = app.emit("core-service-diagnostic", diagnostic); + } + } + CommandEvent::Error(error) => { + state.process = None; + return Err(format!("Core service failed: {error}")); + } + CommandEvent::Terminated(terminated) => { + state.process = None; + return Err(format!( + "Core service stopped with code {:?}.", + terminated.code + )); + } + _ => {} + } + } + state.process = None; + Err("Core service stopped before it returned a response.".to_string()) +} + +#[tauri::command] +async fn choose_workspace(app: AppHandle) -> Result, String> { + let selected = app.dialog().file().blocking_pick_folder(); + selected + .map(|file_path| { + file_path + .into_path() + .map_err(|error| format!("Unable to read selected workspace: {error}")) + .and_then(|path| canonical_workspace_root(&path)) + .map(|path| path.to_string_lossy().into_owned()) + }) + .transpose() +} + +#[tauri::command] +fn initial_workspace() -> Option { + std::env::var("CODEGRAPHY_DESKTOP_WORKSPACE") + .ok() + .filter(|value| !value.is_empty()) +} + +#[tauri::command] +async fn load_workspace_graph( + app: AppHandle, + core: State<'_, CoreService>, + recents: State<'_, RecentWorkspaceStore>, + workspace: State<'_, WorkspaceState>, + workspace_root: String, + reindex: bool, + changed_path: Option, +) -> Result { + let root = canonical_workspace_root(Path::new(&workspace_root))?; + let action = match (reindex, changed_path) { + (true, Some(_)) => { + return Err("A graph request cannot re-index and update one File.".to_string()); + } + (true, None) => CoreRequestAction::Index, + (false, Some(relative_path)) => { + resolve_workspace_file(&root, &relative_path)?; + CoreRequestAction::Update { relative_path } + } + (false, None) => CoreRequestAction::Open, + }; + let result = request_core(&app, &core, &action, &root).await?; + { + let mut active_root = workspace + .root + .write() + .map_err(|_| "Workspace state is unavailable.".to_string())?; + *active_root = Some(root.clone()); + } + recents.remember(&root)?; + refresh_app_menu(&app, &recents)?; + app.emit(EVENT_RECENT_WORKSPACES_CHANGED, ()) + .map_err(|error| format!("Unable to report recent workspace changes: {error}"))?; + Ok(result) +} + +#[tauri::command] +fn recent_workspaces( + recents: State<'_, RecentWorkspaceStore>, +) -> Result, String> { + recents.list() +} + +#[tauri::command] +fn clear_recent_workspaces( + app: AppHandle, + recents: State<'_, RecentWorkspaceStore>, +) -> Result<(), String> { + recents.clear()?; + refresh_app_menu(&app, &recents)?; + app.emit(EVENT_RECENT_WORKSPACES_CHANGED, ()) + .map_err(|error| format!("Unable to report recent workspace changes: {error}")) +} + +#[tauri::command] +async fn close_workspace( + core: State<'_, CoreService>, + workspace: State<'_, WorkspaceState>, +) -> Result<(), String> { + { + let mut active_root = workspace + .root + .write() + .map_err(|_| "Workspace state is unavailable.".to_string())?; + *active_root = None; + } + let process = { + let mut core_state = core.inner.lock().await; + core_state.process.take() + }; + drop(process); + Ok(()) +} + +#[tauri::command] +async fn read_workspace_file( + workspace: State<'_, WorkspaceState>, + relative_path: String, +) -> Result { + let root = active_workspace_root(&workspace)?; + tauri::async_runtime::spawn_blocking(move || read_file_document(&root, &relative_path)) + .await + .map_err(|error| format!("Unable to read File: {error}"))? +} + +#[tauri::command] +async fn save_workspace_file( + workspace: State<'_, WorkspaceState>, + relative_path: String, + content: String, + expected_revision: String, +) -> Result { + let root = active_workspace_root(&workspace)?; + tauri::async_runtime::spawn_blocking(move || { + save_file_document(&root, &relative_path, &content, &expected_revision) + }) + .await + .map_err(|error| format!("Unable to save File: {error}"))? +} + +#[tauri::command] +async fn read_graph_settings( + app: AppHandle, + core: State<'_, CoreService>, + workspace: State<'_, WorkspaceState>, +) -> Result { + let root = active_workspace_root(&workspace)?; + request_core(&app, &core, &CoreRequestAction::ReadSettings, &root).await +} + +#[tauri::command] +async fn write_graph_settings( + app: AppHandle, + core: State<'_, CoreService>, + workspace: State<'_, WorkspaceState>, + settings: Value, +) -> Result { + let root = active_workspace_root(&workspace)?; + request_core( + &app, + &core, + &CoreRequestAction::WriteSettings { settings }, + &root, + ) + .await +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .enable_macos_default_menu(false) + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_shell::init()) + .manage(CoreService::default()) + .manage(WorkspaceState::default()) + .setup(|app| { + let recent_workspaces_path = app + .path() + .app_config_dir()? + .join(RECENT_WORKSPACES_FILE_NAME); + let recents = RecentWorkspaceStore::load(recent_workspaces_path) + .map_err(std::io::Error::other)?; + refresh_app_menu(app.handle(), &recents).map_err(std::io::Error::other)?; + app.manage(recents); + let app_handle = app.handle().clone(); + tauri::async_runtime::spawn(async move { + let core = app_handle.state::(); + let mut state = core.inner.lock().await; + if state.process.is_none() { + state.process = spawn_core_process(&app_handle).ok(); + } + }); + Ok(()) + }) + .on_menu_event(|app, event| handle_menu_event(app, event.id().as_ref())) + .invoke_handler(tauri::generate_handler![ + choose_workspace, + clear_recent_workspaces, + close_workspace, + initial_workspace, + load_workspace_graph, + read_graph_settings, + read_workspace_file, + recent_workspaces, + save_workspace_file, + write_graph_settings, + ]) + .run(tauri::generate_context!()) + .expect("error while running CodeGraphy desktop"); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rejects_files_outside_the_active_workspace() { + let workspace = tempfile::tempdir().expect("workspace"); + let outside = tempfile::NamedTempFile::new().expect("outside File"); + let link = workspace.path().join("outside-link.ts"); + std::os::unix::fs::symlink(outside.path(), &link).expect("symlink"); + + let result = resolve_workspace_file(workspace.path(), "outside-link.ts"); + + assert_eq!( + result.unwrap_err(), + "File path leaves the active workspace." + ); + } + + #[test] + fn atomic_save_detects_external_changes() { + let workspace = tempfile::tempdir().expect("workspace"); + let file_path = workspace.path().join("entry.ts"); + fs::write(&file_path, "export const value = 1;\n").expect("write File"); + let opened = read_file_document(workspace.path(), "entry.ts").expect("open File"); + fs::write(&file_path, "export const value = 2;\n").expect("external edit"); + + let result = save_file_document( + workspace.path(), + "entry.ts", + "export const value = 3;\n", + &opened.revision, + ); + + assert_eq!( + result.unwrap_err(), + "The File changed outside CodeGraphy. Reopen it before saving." + ); + assert_eq!( + fs::read_to_string(file_path).expect("read File"), + "export const value = 2;\n" + ); + } + + #[test] + fn core_request_uses_the_fixed_file_and_folder_sidecar_contract() { + let request = build_core_request( + 7, + &CoreRequestAction::Update { + relative_path: "src/index.ts".to_string(), + }, + Path::new("/tmp/example"), + ); + + assert_eq!( + request, + json!({ + "id": 7, + "kind": "request", + "method": "update", + "params": { + "relativePath": "src/index.ts", + "workspaceRoot": "/tmp/example", + }, + }) + ); + } + + #[test] + fn core_settings_requests_keep_the_active_workspace_boundary() { + let settings = json!({ + "repelForce": 10, + "linkDistance": 80, + "linkForce": 1, + "damping": 0.4, + "centerForce": 0.1, + }); + + assert_eq!( + build_core_request( + 8, + &CoreRequestAction::WriteSettings { + settings: settings.clone(), + }, + Path::new("/tmp/example"), + ), + json!({ + "id": 8, + "kind": "request", + "method": "write-settings", + "params": { + "settings": settings, + "workspaceRoot": "/tmp/example", + }, + }) + ); + assert_eq!( + build_core_request( + 9, + &CoreRequestAction::ReadSettings, + Path::new("/tmp/example"), + ), + json!({ + "id": 9, + "kind": "request", + "method": "read-settings", + "params": { "workspaceRoot": "/tmp/example" }, + }) + ); + } + + #[test] + fn recent_workspaces_are_canonical_deduplicated_and_bounded() { + let config = tempfile::tempdir().expect("config folder"); + let workspaces = tempfile::tempdir().expect("workspace parent"); + let file_path = config.path().join(RECENT_WORKSPACES_FILE_NAME); + let store = RecentWorkspaceStore::load(file_path.clone()).expect("recent workspace store"); + let mut roots = Vec::new(); + for index in 0..=MAX_RECENT_WORKSPACES { + let root = workspaces.path().join(format!("workspace-{index}")); + fs::create_dir(&root).expect("create workspace"); + store.remember(&root).expect("remember workspace"); + roots.push(root.canonicalize().expect("canonical workspace")); + } + store + .remember(&roots[4]) + .expect("move existing workspace to front"); + + let recent = store.list().expect("list recent workspaces"); + assert_eq!(recent.len(), MAX_RECENT_WORKSPACES); + assert_eq!(recent[0].path, roots[4].to_string_lossy()); + assert_eq!(recent[1].path, roots[8].to_string_lossy()); + assert_eq!( + recent + .iter() + .filter(|workspace| workspace.path == roots[4].to_string_lossy()) + .count(), + 1 + ); + assert!(recent.iter().all(|workspace| workspace.available)); + + let stored: RecentWorkspaceFile = serde_json::from_slice( + &fs::read(&file_path).expect("read persisted recent workspaces"), + ) + .expect("parse persisted recent workspaces"); + assert_eq!(stored.paths[0], roots[4]); + assert_eq!(stored.paths[1], roots[8]); + assert_eq!(stored.paths.len(), MAX_RECENT_WORKSPACES); + + let reloaded = RecentWorkspaceStore::load(file_path).expect("reload recent workspaces"); + assert_eq!(reloaded.list().expect("list reloaded workspaces"), recent); + } + + #[test] + fn missing_recent_workspaces_remain_visible_and_clear_persists() { + let config = tempfile::tempdir().expect("config folder"); + let workspaces = tempfile::tempdir().expect("workspace parent"); + let file_path = config.path().join(RECENT_WORKSPACES_FILE_NAME); + let root = workspaces.path().join("moved-workspace"); + fs::create_dir(&root).expect("create workspace"); + let canonical = root.canonicalize().expect("canonical workspace"); + let store = RecentWorkspaceStore::load(file_path.clone()).expect("recent workspace store"); + store.remember(&root).expect("remember workspace"); + fs::remove_dir(&root).expect("remove workspace"); + + assert_eq!( + store.list().expect("list recent workspaces"), + vec![RecentWorkspace { + path: canonical.to_string_lossy().into_owned(), + name: "moved-workspace".to_string(), + available: false, + }] + ); + + store.clear().expect("clear recent workspaces"); + let reloaded = RecentWorkspaceStore::load(file_path).expect("reload recent workspaces"); + assert!(reloaded.list().expect("list cleared workspaces").is_empty()); + } +} diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs new file mode 100644 index 0000000000..17392c774e --- /dev/null +++ b/apps/desktop/src-tauri/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + codegraphy_desktop_lib::run(); +} diff --git a/apps/desktop/src-tauri/tauri.ad-hoc.conf.json b/apps/desktop/src-tauri/tauri.ad-hoc.conf.json new file mode 100644 index 0000000000..93a2cd4972 --- /dev/null +++ b/apps/desktop/src-tauri/tauri.ad-hoc.conf.json @@ -0,0 +1,8 @@ +{ + "bundle": { + "macOS": { + "entitlements": "Entitlements.ad-hoc.plist", + "signingIdentity": "-" + } + } +} diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json new file mode 100644 index 0000000000..325909235d --- /dev/null +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "CodeGraphy", + "version": "0.1.0", + "identifier": "dev.codegraphy.desktop", + "build": { + "beforeDevCommand": "pnpm run build:sidecar && pnpm run build:icons && pnpm exec vite", + "beforeBuildCommand": "pnpm run build:sidecar && pnpm run build", + "devUrl": "http://localhost:1420", + "frontendDist": "../dist" + }, + "app": { + "security": { + "csp": "default-src 'self'; connect-src 'self' ipc: http://ipc.localhost; img-src 'self' data: asset: http://asset.localhost; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'" + }, + "windows": [ + { + "title": "CodeGraphy", + "width": 1440, + "height": 900, + "minWidth": 1040, + "minHeight": 680, + "center": true, + "decorations": true, + "hiddenTitle": true, + "titleBarStyle": "Overlay" + } + ] + }, + "bundle": { + "active": true, + "category": "DeveloperTool", + "copyright": "Copyright ยฉ 2026 Joe Soboleski", + "externalBin": ["binaries/codegraphy-core"], + "icon": ["icons/icon.icns"], + "longDescription": "A fast local code navigator for Files, Folders, and their Relationships.", + "macOS": { + "entitlements": "Entitlements.plist", + "hardenedRuntime": true, + "minimumSystemVersion": "26.0" + }, + "resources": ["runtime/"], + "shortDescription": "Local Relationship Graph for code", + "targets": ["app", "dmg"] + } +} diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx new file mode 100644 index 0000000000..e6ef714b80 --- /dev/null +++ b/apps/desktop/src/App.tsx @@ -0,0 +1,228 @@ +import brandIconUrl from '../../../assets/icon-dark.svg?url'; +import { useState } from 'react'; +import { CodeEditor } from './components/CodeEditor'; +import { FileTree } from './components/FileTree'; +import { GraphPanel } from './components/GraphPanel'; +import { GraphSettingsPopover } from './components/GraphSettingsPopover'; +import { UnsavedFileDialog } from './components/UnsavedFileDialog'; +import { WorkspacePanes } from './components/WorkspacePanes'; +import { WorkspaceSwitcher } from './components/WorkspaceSwitcher'; +import { formatGraphCounts } from './model'; +import { + readDesktopInterfacePreferences, + saveDesktopInterfacePreferences, + type DesktopInterfacePreferences, +} from './interfacePreferences'; +import { useDesktopWorkspace } from './useDesktopWorkspace'; + +export function App(): React.ReactElement { + const workspace = useDesktopWorkspace(); + const [interfacePreferences, setInterfacePreferences] = useState(() => ( + readDesktopInterfacePreferences(window.localStorage) + )); + const updateInterfacePreference = ( + key: Key, + value: DesktopInterfacePreferences[Key], + ): void => { + const next = { ...interfacePreferences, [key]: value }; + try { + saveDesktopInterfacePreferences(window.localStorage, next); + } catch { + // Keep the current window usable when the webview denies local persistence. + } + setInterfacePreferences(next); + }; + + return ( +
+
+
+
+ + + CodeGraphy + Relationship Graph + +
+ void workspace.clearRecent()} + onOpenRecent={workspace.openRecentWorkspace} + onOpenWorkspace={workspace.openWorkspace} + recentWorkspaces={workspace.recentWorkspaces} + workspaceRoot={workspace.workspaceRoot} + /> +
+ + + + +
+
+ + {workspace.graph ? ( + +
Files{workspace.fileCount}
+ void workspace.selectFile(path)} + selectedPath={workspace.selectedPath} + /> + + )} + editorPane={( +
+
+ {workspace.document?.path ?? 'Editor'} +
+ + +
+
+ {workspace.document ? ( + void workspace.saveCurrentDocument()} /> + ) : ( +
+ +

Choose a File

+

Browse the workspace hierarchy, make a lightweight edit, and save it back to the source File.

+
+ )} +
+ )} + graphPane={( + + )} + graphVisible={interfacePreferences.graphPaneVisible} + /> + ) : ( +
+ +

MACOS ยท LOCAL ยท FAST

+

Trace the relationships
inside your workspace.

+

Open a local folder. CodeGraphy keeps the Graph Cache beside your source and puts Files, editing, and the Relationship Graph in one window.

+ +
+ )} + +
+
+ + {workspace.pendingFileAction && workspace.document ? ( + void workspace.finishPendingFileAction(false)} + onSave={() => void workspace.finishPendingFileAction(true)} + saving={workspace.saving} + /> + ) : null} + + {workspace.pendingWorkspaceAction && workspace.document ? ( + void workspace.finishPendingWorkspaceAction(false)} + onSave={() => void workspace.finishPendingWorkspaceAction(true)} + saving={workspace.saving} + /> + ) : null} +
+ ); +} diff --git a/apps/desktop/src/bridge.test.ts b/apps/desktop/src/bridge.test.ts new file mode 100644 index 0000000000..c70274f27c --- /dev/null +++ b/apps/desktop/src/bridge.test.ts @@ -0,0 +1,83 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const tauri = vi.hoisted(() => ({ invoke: vi.fn(), listen: vi.fn() })); + +vi.mock('@tauri-apps/api/core', () => ({ invoke: tauri.invoke })); +vi.mock('@tauri-apps/api/event', () => ({ listen: tauri.listen })); + +import { + parseDesktopGraphSettings, + listenToDesktopMenu, + readDesktopGraphSettings, + writeDesktopGraphSettings, +} from './bridge'; + +const settings = { + repelForce: 10, + linkDistance: 80, + linkForce: 1, + damping: 0.4, + centerForce: 0.1, +}; + +describe('desktop Graph Settings bridge', () => { + beforeEach(() => vi.clearAllMocks()); + + it('uses exact shared defaults when the workspace has no desktop record', () => { + expect(parseDesktopGraphSettings(null)).toEqual(settings); + }); + + it('clamps finite legacy values and rejects malformed interface data', () => { + expect(parseDesktopGraphSettings({ ...settings, repelForce: 21, linkDistance: 500 })) + .toEqual({ ...settings, repelForce: 20, linkDistance: 150 }); + expect(() => parseDesktopGraphSettings({ ...settings, linkDistance: '500' })) + .toThrow('Core returned invalid desktop Graph Settings.'); + expect(() => parseDesktopGraphSettings({ ...settings, futureSetting: true })) + .toThrow('Core returned invalid desktop Graph Settings.'); + }); + + it('reads and writes the exact settings record through the desktop host', async () => { + tauri.invoke.mockResolvedValue(settings); + + await expect(readDesktopGraphSettings()).resolves.toEqual(settings); + await expect(writeDesktopGraphSettings(settings)).resolves.toEqual(settings); + expect(tauri.invoke).toHaveBeenNthCalledWith(1, 'read_graph_settings'); + expect(tauri.invoke).toHaveBeenNthCalledWith(2, 'write_graph_settings', { settings }); + }); + + it('wires Close File without taking over the native macOS window-close shortcut', async () => { + const listeners = new Map void>(); + const unlisten = vi.fn(); + tauri.listen.mockImplementation(async (event: string, handler: (event: { payload: unknown }) => void) => { + listeners.set(event, handler); + return unlisten; + }); + const handlers = { + closeFile: vi.fn(), + closeWorkspace: vi.fn(), + openRecent: vi.fn(), + openWorkspace: vi.fn(), + recentWorkspacesChanged: vi.fn(), + save: vi.fn(), + }; + + const stop = await listenToDesktopMenu(handlers); + listeners.get('desktop-close-file')?.({ payload: undefined }); + listeners.get('desktop-open-recent')?.({ payload: '/workspace' }); + listeners.get('desktop-open-recent')?.({ payload: 42 }); + + expect(handlers.closeFile).toHaveBeenCalledOnce(); + expect(handlers.openRecent).toHaveBeenCalledOnce(); + expect(handlers.openRecent).toHaveBeenCalledWith('/workspace'); + expect([...listeners.keys()]).toEqual([ + 'desktop-open-workspace', + 'desktop-open-recent', + 'desktop-recent-workspaces-changed', + 'desktop-close-file', + 'desktop-close-workspace', + 'desktop-save', + ]); + stop(); + expect(unlisten).toHaveBeenCalledTimes(6); + }); +}); diff --git a/apps/desktop/src/bridge.ts b/apps/desktop/src/bridge.ts new file mode 100644 index 0000000000..8fc8e87156 --- /dev/null +++ b/apps/desktop/src/bridge.ts @@ -0,0 +1,153 @@ +import { invoke } from '@tauri-apps/api/core'; +import { listen, type UnlistenFn } from '@tauri-apps/api/event'; +import { + DEFAULT_GRAPH_PHYSICS_SETTINGS, + normalizeGraphPhysicsSettings, + type GraphPhysicsSettings, +} from '@codegraphy-dev/graph-renderer/visuals'; +import { parseWorkspaceGraphResult, type WorkspaceGraphResult } from './model'; + +export interface FileDocument { + path: string; + content: string; + revision: string; +} + +export interface RecentWorkspace { + path: string; + name: string; + available: boolean; +} + +export interface DesktopMenuHandlers { + closeFile(this: void): void; + closeWorkspace(this: void): void; + openRecent(this: void, path: string): void; + openWorkspace(this: void): void; + recentWorkspacesChanged(this: void): void; + save(this: void): void; +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function parseFileDocument(value: unknown): FileDocument { + if (!isRecord(value) + || typeof value.path !== 'string' + || typeof value.content !== 'string' + || typeof value.revision !== 'string') { + throw new Error('The desktop host returned an invalid File.'); + } + return { path: value.path, content: value.content, revision: value.revision }; +} + +function parseRecentWorkspace(value: unknown): RecentWorkspace { + if (!isRecord(value) + || typeof value.path !== 'string' + || typeof value.name !== 'string' + || typeof value.available !== 'boolean') { + throw new Error('The desktop host returned an invalid recent workspace.'); + } + return { path: value.path, name: value.name, available: value.available }; +} + +function isFiniteNumber(value: unknown): value is number { + return typeof value === 'number' && Number.isFinite(value); +} + +export function parseDesktopGraphSettings(value: unknown): GraphPhysicsSettings { + if (value === null) return { ...DEFAULT_GRAPH_PHYSICS_SETTINGS }; + const expectedKeys = ['centerForce', 'damping', 'linkDistance', 'linkForce', 'repelForce']; + if (!isRecord(value) + || Object.keys(value).sort().join(',') !== expectedKeys.join(',') + || !isFiniteNumber(value.repelForce) + || !isFiniteNumber(value.centerForce) + || !isFiniteNumber(value.linkDistance) + || !isFiniteNumber(value.linkForce) + || !isFiniteNumber(value.damping)) { + throw new Error('Core returned invalid desktop Graph Settings.'); + } + return normalizeGraphPhysicsSettings({ + repelForce: value.repelForce, + centerForce: value.centerForce, + linkDistance: value.linkDistance, + linkForce: value.linkForce, + damping: value.damping, + }); +} + +export async function chooseWorkspace(): Promise { + const result = await invoke('choose_workspace'); + if (result === null) return undefined; + if (typeof result !== 'string') throw new Error('The desktop host returned an invalid workspace.'); + return result; +} + +export async function initialWorkspace(): Promise { + const result = await invoke('initial_workspace'); + if (result === null) return undefined; + if (typeof result !== 'string') throw new Error('The desktop host returned an invalid workspace.'); + return result; +} + +export async function loadWorkspaceGraph(input: { + workspaceRoot: string; + reindex: boolean; + changedPath?: string; +}): Promise { + const result = await invoke('load_workspace_graph', input); + return parseWorkspaceGraphResult(result); +} + +export async function listRecentWorkspaces(): Promise { + const result = await invoke('recent_workspaces'); + if (!Array.isArray(result)) throw new Error('The desktop host returned invalid recent workspaces.'); + return result.map(parseRecentWorkspace); +} + +export async function clearRecentWorkspaces(): Promise { + await invoke('clear_recent_workspaces'); +} + +export async function closeWorkspace(): Promise { + await invoke('close_workspace'); +} + +export async function readDesktopGraphSettings(): Promise { + return parseDesktopGraphSettings(await invoke('read_graph_settings')); +} + +export async function writeDesktopGraphSettings( + settings: GraphPhysicsSettings, +): Promise { + return parseDesktopGraphSettings(await invoke('write_graph_settings', { settings })); +} + +export async function listenToDesktopMenu(handlers: DesktopMenuHandlers): Promise { + const unlisten = await Promise.all([ + listen('desktop-open-workspace', handlers.openWorkspace), + listen('desktop-open-recent', (event) => { + if (typeof event.payload === 'string') handlers.openRecent(event.payload); + }), + listen('desktop-recent-workspaces-changed', handlers.recentWorkspacesChanged), + listen('desktop-close-file', handlers.closeFile), + listen('desktop-close-workspace', handlers.closeWorkspace), + listen('desktop-save', handlers.save), + ]); + return () => { + for (const stop of unlisten) stop(); + }; +} + +export async function readWorkspaceFile(relativePath: string): Promise { + return parseFileDocument(await invoke('read_workspace_file', { relativePath })); +} + +export async function saveWorkspaceFile(input: { + relativePath: string; + content: string; + expectedRevision: string; +}): Promise { + return parseFileDocument(await invoke('save_workspace_file', input)); +} diff --git a/apps/desktop/src/components/CodeEditor.css b/apps/desktop/src/components/CodeEditor.css new file mode 100644 index 0000000000..0f6bd22b0c --- /dev/null +++ b/apps/desktop/src/components/CodeEditor.css @@ -0,0 +1,116 @@ +.markdown-editor { + display: grid; + min-height: 0; + height: 100%; + grid-template-rows: 31px minmax(0, 1fr); + background: var(--cg-editor); +} + +.markdown-mode-control { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 1px; + border-bottom: 1px solid var(--cg-divider); + background: var(--cg-editor-gutter); + padding: 4px 7px; +} + +.markdown-mode-control button { + min-height: 22px; + border: 1px solid transparent; + border-radius: 4px; + background: transparent; + padding: 0 8px; + color: var(--cg-text-muted); + font-size: 9px; +} + +.markdown-mode-control button:hover { + background: var(--cg-surface-hover); + color: var(--cg-text); +} + +.markdown-mode-control button.is-active { + border-color: color-mix(in srgb, var(--cg-accent) 36%, transparent); + background: var(--cg-accent-surface); + color: var(--cg-accent-hover); +} + +.markdown-mode-control button:focus-visible { + outline: 2px solid var(--cg-focus); + outline-offset: -2px; +} + +.markdown-workspace, +.markdown-source { + min-width: 0; + min-height: 0; + height: 100%; +} + +.markdown-editor--split .markdown-workspace { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); +} + +.markdown-editor--preview .markdown-workspace { + display: grid; + grid-template-columns: minmax(0, 1fr); +} + +.markdown-editor--split .markdown-source { + border-right: 1px solid var(--cg-divider); +} + +.markdown-preview { + height: 100%; + min-width: 0; + min-height: 0; + overflow: auto; + padding: 24px clamp(24px, 5vw, 52px) 64px; + color: var(--cg-text-secondary); + font-size: 13px; + line-height: 1.65; + overflow-wrap: anywhere; +} + +.markdown-preview > :first-child { margin-top: 0; } +.markdown-preview > :last-child { margin-bottom: 0; } +.markdown-preview h1, +.markdown-preview h2, +.markdown-preview h3 { + margin: 1.5em 0 0.55em; + color: var(--cg-text); + line-height: 1.25; +} +.markdown-preview h1 { padding-bottom: 0.3em; border-bottom: 1px solid var(--cg-divider); font-size: 1.8em; } +.markdown-preview h2 { font-size: 1.4em; } +.markdown-preview h3 { font-size: 1.15em; } +.markdown-preview-link { color: var(--cg-focus); text-decoration: underline; text-decoration-style: dotted; text-underline-offset: 2px; } +.markdown-preview-image-placeholder { display: inline-flex; min-height: 38px; align-items: center; border: 1px dashed var(--cg-border-strong); border-radius: 5px; padding: 7px 10px; color: var(--cg-text-muted); font-size: 0.92em; } +.markdown-preview blockquote { margin-left: 0; border-left: 3px solid var(--cg-accent); padding-left: 14px; color: var(--cg-text-muted); } +.markdown-preview code { border-radius: 3px; background: var(--cg-popover); padding: 0.12em 0.35em; color: #e5c07b; font-family: var(--cg-font-mono); font-size: 0.92em; } +.markdown-preview pre { overflow: auto; border: 1px solid var(--cg-border); border-radius: 6px; background: var(--cg-bg); padding: 13px 15px; } +.markdown-preview pre code { background: transparent; padding: 0; color: var(--cg-text-secondary); } +.markdown-preview table { width: 100%; border-collapse: collapse; } +.markdown-preview th, +.markdown-preview td { border: 1px solid var(--cg-border); padding: 5px 8px; text-align: left; } +.markdown-preview th { background: var(--cg-popover); color: var(--cg-text); } +.markdown-preview img { max-width: 100%; height: auto; } +.markdown-preview hr { border: 0; border-top: 1px solid var(--cg-divider); } + +.markdown-preview-limit { + display: grid; + min-height: 0; + place-content: center; + padding: 32px; + color: var(--cg-text-muted); + font-size: 11px; + line-height: 1.5; + text-align: center; +} + +@media (forced-colors: active) { + .markdown-mode-control button.is-active { border-color: Highlight; color: HighlightText; } +} diff --git a/apps/desktop/src/components/CodeEditor.test.tsx b/apps/desktop/src/components/CodeEditor.test.tsx new file mode 100644 index 0000000000..6cfae40b73 --- /dev/null +++ b/apps/desktop/src/components/CodeEditor.test.tsx @@ -0,0 +1,85 @@ +import { act } from 'react'; +import { createRoot } from 'react-dom/client'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import type { FileDocument } from '../bridge'; +import { CodeEditor } from './CodeEditor'; +import { createCodeEditor } from './createCodeEditor'; + +vi.mock('./createCodeEditor', () => ({ + createCodeEditor: vi.fn(async () => () => undefined), +})); + +const markdownDocument: FileDocument = { + content: '# Original', + path: 'README.md', + revision: 'revision-1', +}; + +describe('CodeEditor Markdown modes', () => { + beforeEach(() => vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)); + + afterEach(() => { + vi.clearAllMocks(); + vi.unstubAllGlobals(); + document.body.replaceChildren(); + }); + + it('offers accessible Edit, Split, and Preview modes while preserving draft changes', async () => { + const host = document.createElement('div'); + document.body.append(host); + const root = createRoot(host); + const onChange = vi.fn(); + await act(async () => root.render( + , + )); + await act(async () => undefined); + + const modeGroup = host.querySelector('[role="group"][aria-label="Markdown editor mode"]'); + const buttons = [...(modeGroup?.querySelectorAll('button') ?? [])]; + expect(buttons.map(button => [button.textContent, button.getAttribute('aria-pressed')])).toEqual([ + ['Edit', 'true'], + ['Split', 'false'], + ['Preview', 'false'], + ]); + const editorOptions = vi.mocked(createCodeEditor).mock.calls[0]?.[0]; + await act(async () => editorOptions?.onChange('# Updated\n\nPreview text')); + expect(onChange).toHaveBeenCalledWith('# Updated\n\nPreview text'); + + const split = buttons.find(button => button.textContent === 'Split'); + await act(async () => { + split?.click(); + await import('./MarkdownPreview'); + }); + expect(host.querySelector('.markdown-source')).not.toBeNull(); + expect(host.querySelector('article[aria-label="Markdown preview"] h1')?.textContent).toBe('Updated'); + expect(host.textContent).toContain('Preview text'); + + const preview = buttons.find(button => button.textContent === 'Preview'); + await act(async () => { + preview?.focus(); + preview?.click(); + }); + expect(host.querySelector('.markdown-source')?.hasAttribute('hidden')).toBe(true); + expect(document.activeElement).toBe(preview); + + await act(async () => root.unmount()); + }); + + it('does not add Markdown mode controls to another language', async () => { + const host = document.createElement('div'); + document.body.append(host); + const root = createRoot(host); + await act(async () => root.render( + , + )); + await act(async () => undefined); + + expect(host.querySelector('[aria-label="Markdown editor mode"]')).toBeNull(); + expect(host.querySelector('.editor-host')).not.toBeNull(); + await act(async () => root.unmount()); + }); +}); diff --git a/apps/desktop/src/components/CodeEditor.tsx b/apps/desktop/src/components/CodeEditor.tsx new file mode 100644 index 0000000000..0d1ebe47eb --- /dev/null +++ b/apps/desktop/src/components/CodeEditor.tsx @@ -0,0 +1,133 @@ +import { lazy, Suspense, useEffect, useRef, useState } from 'react'; +import type { FileDocument } from '../bridge'; +import { isMarkdownPath } from './markdownPath'; +import './CodeEditor.css'; + +type MarkdownMode = 'edit' | 'split' | 'preview'; + +const markdownModes: ReadonlyArray<{ label: string; mode: MarkdownMode }> = [ + { label: 'Edit', mode: 'edit' }, + { label: 'Split', mode: 'split' }, + { label: 'Preview', mode: 'preview' }, +]; + +const MarkdownPreview = lazy(async () => { + const module = await import('./MarkdownPreview'); + return { default: module.MarkdownPreview }; +}); + +export function CodeEditor({ + document, + onChange, + onSave, +}: { + document: FileDocument; + onChange: (content: string) => void; + onSave: () => void; +}): React.ReactElement { + const hostRef = useRef(null); + const onChangeRef = useRef(onChange); + const onSaveRef = useRef(onSave); + const [loadError, setLoadError] = useState(); + const documentKey = `${document.path}\0${document.revision}`; + const [markdownDraft, setMarkdownDraft] = useState({ + content: document.content, + documentKey, + }); + const [previewDraft, setPreviewDraft] = useState({ + content: document.content, + documentKey, + }); + const [markdownMode, setMarkdownMode] = useState('edit'); + const markdown = isMarkdownPath(document.path); + const markdownContent = markdownDraft.documentKey === documentKey + ? markdownDraft.content + : document.content; + + useEffect(() => { + onChangeRef.current = onChange; + }, [onChange]); + + useEffect(() => { + onSaveRef.current = onSave; + }, [onSave]); + + useEffect(() => { + if (!markdown || markdownMode === 'edit') return; + const timeout = window.setTimeout(() => { + setPreviewDraft({ content: markdownContent, documentKey }); + }, 150); + return () => window.clearTimeout(timeout); + }, [documentKey, markdown, markdownContent, markdownMode]); + + useEffect(() => { + const host = hostRef.current; + if (!host) return; + let active = true; + let destroy: (() => void) | undefined; + setLoadError(undefined); + void import('./createCodeEditor') + .then(({ createCodeEditor }) => createCodeEditor({ + content: document.content, + onChange: (content) => { + setMarkdownDraft({ content, documentKey }); + onChangeRef.current(content); + }, + onSave: () => onSaveRef.current(), + parent: host, + path: document.path, + })) + .then((created) => { + if (active) destroy = created; + else created(); + }) + .catch((error: unknown) => { + if (active) setLoadError(error instanceof Error ? error.message : String(error)); + }); + return () => { + active = false; + destroy?.(); + }; + }, [document.content, document.path, documentKey]); + + if (loadError) { + return
Editor unavailable: {loadError}
; + } + + if (!markdown) return
; + + const previewContent = previewDraft.documentKey === documentKey + ? previewDraft.content + : markdownContent; + + return ( +
+
+ {markdownModes.map(({ label, mode }) => ( + + ))} +
+
+ }> + + + ) : null} +
+
+ ); +} diff --git a/apps/desktop/src/components/FileTree.test.tsx b/apps/desktop/src/components/FileTree.test.tsx new file mode 100644 index 0000000000..6cfe8f9b2d --- /dev/null +++ b/apps/desktop/src/components/FileTree.test.tsx @@ -0,0 +1,165 @@ +import { act } from 'react'; +import { createRoot } from 'react-dom/client'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import type { FileTreeEntry } from '../model'; + +vi.mock('../materialIconTheme', () => ({ resolveMaterialIcon: vi.fn(async () => undefined) })); + +import { FileTree } from './FileTree'; + +const entries: FileTreeEntry[] = [{ + kind: 'folder', + name: 'src', + path: 'src', + children: [ + { kind: 'file', name: 'main.ts', path: 'src/main.ts' }, + { + kind: 'folder', + name: 'graph', + path: 'src/graph', + children: [{ kind: 'file', name: 'camera.ts', path: 'src/graph/camera.ts' }], + }, + ], +}]; + +function key(target: Element, value: string, options: KeyboardEventInit = {}): void { + target.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: value, ...options })); +} + +function keyUp(target: Element, value: string): void { + target.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, key: value })); +} + +function setInputValue(input: HTMLInputElement, value: string): void { + const descriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value'); + if (!descriptor?.set) throw new Error('The input value setter is unavailable.'); + descriptor.set.bind(input)(value); + input.dispatchEvent(new Event('input', { bubbles: true })); +} + +describe('File hierarchy keyboard and filter behavior', () => { + const animationFrames = new Map(); + let animationFrameId = 0; + + const flushAnimationFrames = (): void => { + const callbacks = [...animationFrames.values()]; + animationFrames.clear(); + callbacks.forEach(callback => callback(0)); + }; + + beforeEach(() => { + animationFrames.clear(); + animationFrameId = 0; + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); + vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => { + const id = ++animationFrameId; + animationFrames.set(id, callback); + return id; + }); + vi.stubGlobal('cancelAnimationFrame', (id: number) => animationFrames.delete(id)); + Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', { + configurable: true, + value: vi.fn(), + }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + document.body.replaceChildren(); + }); + + it('keeps a roving focus while opening Files and navigating visible rows', async () => { + const host = document.createElement('div'); + document.body.append(host); + const root = createRoot(host); + const onSelect = vi.fn(); + await act(async () => root.render( + , + )); + const main = host.querySelector('[data-tree-path="src/main.ts"]'); + await act(async () => main?.focus()); + await act(async () => main?.click()); + expect(onSelect).toHaveBeenCalledWith('src/main.ts'); + expect(document.activeElement).toBe( + host.querySelector('[data-tree-path="src/main.ts"]'), + ); + + await act(async () => { if (main) key(main, 'ArrowDown'); }); + await act(async () => flushAnimationFrames()); + const graphFolder = host.querySelector('[data-tree-path="src/graph"]'); + expect(document.activeElement).toBe(graphFolder); + await act(async () => { if (graphFolder) key(graphFolder, 'ArrowRight'); }); + await act(async () => flushAnimationFrames()); + expect(document.activeElement).toBe( + host.querySelector('[data-tree-path="src/graph/camera.ts"]'), + ); + expect(onSelect).toHaveBeenLastCalledWith('src/graph/camera.ts'); + expect(host.querySelectorAll('[role="treeitem"][tabindex="0"]')).toHaveLength(1); + await act(async () => root.unmount()); + }); + + it('coalesces rapid keyboard navigation without replaying focus after key release', async () => { + const host = document.createElement('div'); + document.body.append(host); + const root = createRoot(host); + const onSelect = vi.fn(); + await act(async () => root.render( + , + )); + const src = host.querySelector('[data-tree-path="src"]'); + await act(async () => src?.focus()); + + await act(async () => { + for (let index = 0; index < 3; index += 1) { + const activeItem = document.activeElement; + if (activeItem) key(activeItem, 'ArrowDown', { repeat: index > 0 }); + } + }); + + expect(document.activeElement).toBe( + host.querySelector('[data-tree-path="src/graph/camera.ts"]'), + ); + expect(onSelect).toHaveBeenLastCalledWith('src/graph/camera.ts'); + expect(animationFrames).toHaveLength(1); + await act(async () => flushAnimationFrames()); + expect(document.activeElement).toBe( + host.querySelector('[data-tree-path="src/graph/camera.ts"]'), + ); + await act(async () => root.unmount()); + }); + + it('filters relative paths, keeps ancestors, reports empty results, and Escape restores the tree', async () => { + const host = document.createElement('div'); + document.body.append(host); + const root = createRoot(host); + await act(async () => root.render( + undefined} selectedPath="src/main.ts" />, + )); + const main = host.querySelector('[data-tree-path="src/main.ts"]'); + await act(async () => main?.focus()); + await act(async () => { if (main) key(main, 'f', { metaKey: true }); }); + const filter = host.querySelector('[role="searchbox"]'); + expect(document.activeElement).toBe(filter); + + await act(async () => { + if (!filter) return; + setInputValue(filter, 'graph/cam'); + }); + expect([...host.querySelectorAll('[role="treeitem"]')].map(item => item.dataset.treePath)) + .toEqual(['src', 'src/graph', 'src/graph/camera.ts']); + + await act(async () => { + if (!filter) return; + setInputValue(filter, 'not-present'); + }); + expect(host.textContent).toContain('No Files or Folders match'); + await act(async () => { if (filter) keyUp(filter, 'Escape'); }); + await act(async () => flushAnimationFrames()); + expect(host.querySelectorAll('[role="treeitem"]')).toHaveLength(4); + expect(document.activeElement).toBe( + host.querySelector('[data-tree-path="src/main.ts"]'), + ); + await act(async () => root.unmount()); + }); +}); diff --git a/apps/desktop/src/components/FileTree.tsx b/apps/desktop/src/components/FileTree.tsx new file mode 100644 index 0000000000..f522fe4697 --- /dev/null +++ b/apps/desktop/src/components/FileTree.tsx @@ -0,0 +1,322 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; +import { + collectFolderPaths, + filterFileTree, + flattenVisibleFileTree, +} from '../fileTreeModel'; +import { resolveMaterialIcon, type MaterialIconData } from '../materialIconTheme'; +import type { FileTreeEntry } from '../model'; + +function MaterialIcon({ path, mode }: { path: string; mode: 'file' | 'folder' }): React.ReactElement { + const [icon, setIcon] = useState(); + + useEffect(() => { + let active = true; + void resolveMaterialIcon(path, mode) + .then(result => { if (active) setIcon(result); }) + .catch(() => { if (active) setIcon(undefined); }); + return () => { active = false; }; + }, [mode, path]); + + return icon + ? + :
handlePhysicsChange('centerForce', values[0])} onValueCommit={() => flushPhysicsSetting('centerForce')} @@ -100,9 +103,9 @@ export function ForcesSection(): React.ReactElement { handlePhysicsChange('linkDistance', values[0])} onValueCommit={() => flushPhysicsSetting('linkDistance')} @@ -115,9 +118,9 @@ export function ForcesSection(): React.ReactElement { handlePhysicsChange('linkForce', values[0])} onValueCommit={() => flushPhysicsSetting('linkForce')} diff --git a/packages/extension/src/webview/components/settingsPanel/forces/persistence.ts b/packages/extension/src/webview/components/settingsPanel/forces/persistence.ts index c0fd43828a..a9c077fa52 100644 --- a/packages/extension/src/webview/components/settingsPanel/forces/persistence.ts +++ b/packages/extension/src/webview/components/settingsPanel/forces/persistence.ts @@ -1,8 +1,8 @@ -import type { IPhysicsSettings } from '../../../../shared/settings/physics'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; -export type PendingPhysicsMap = Partial>; +export type PendingPhysicsMap = Partial>; export type PhysicsTimerMap = Partial< - Record> + Record> >; export function clearPhysicsTimerMap(timers: PhysicsTimerMap): void { @@ -16,8 +16,8 @@ export function clearPhysicsTimerMap(timers: PhysicsTimerMap): void { export function flushPendingPhysicsValue( pendingValues: PendingPhysicsMap, timers: PhysicsTimerMap, - key: keyof IPhysicsSettings, - emit: (key: keyof IPhysicsSettings, value: number) => void, + key: keyof GraphPhysicsSettings, + emit: (key: keyof GraphPhysicsSettings, value: number) => void, ): void { const pendingValue = pendingValues[key]; if (pendingValue === undefined) { @@ -37,10 +37,10 @@ export function flushPendingPhysicsValue( export function schedulePendingPhysicsValue( pendingValues: PendingPhysicsMap, timers: PhysicsTimerMap, - key: keyof IPhysicsSettings, + key: keyof GraphPhysicsSettings, value: number, delayMs: number, - flush: (key: keyof IPhysicsSettings) => void, + flush: (key: keyof GraphPhysicsSettings) => void, ): void { pendingValues[key] = value; diff --git a/packages/extension/src/webview/export/json/build/nodes.ts b/packages/extension/src/webview/export/json/build/nodes.ts index 68e5f7e1ee..b765e69d09 100644 --- a/packages/extension/src/webview/export/json/build/nodes.ts +++ b/packages/extension/src/webview/export/json/build/nodes.ts @@ -1,5 +1,5 @@ import type { IGraphData } from '../../../../shared/graph/contracts'; -import { DEFAULT_NODE_COLOR } from '../../../../shared/fileColors'; +import { DEFAULT_NODE_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import type { IGroup } from '../../../../shared/settings/groups'; import { globMatch } from '../../../globMatch'; diff --git a/packages/extension/src/webview/export/shared/context.ts b/packages/extension/src/webview/export/shared/context.ts index 38f4cbe17c..b76d28e79b 100644 --- a/packages/extension/src/webview/export/shared/context.ts +++ b/packages/extension/src/webview/export/shared/context.ts @@ -1,4 +1,4 @@ -import { DEFAULT_DIRECTION_COLOR } from '../../../shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; const EXPORT_BACKGROUND_COLOR = '#18181b'; diff --git a/packages/extension/src/webview/export/svg/link/element/build.ts b/packages/extension/src/webview/export/svg/link/element/build.ts index 6fffea7e19..8653793559 100644 --- a/packages/extension/src/webview/export/svg/link/element/build.ts +++ b/packages/extension/src/webview/export/svg/link/element/build.ts @@ -1,5 +1,5 @@ import type { SvgExportLink, SvgPosition } from '../../contracts'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../../shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; const CURVATURE_EPSILON = 0.001; diff --git a/packages/extension/src/webview/export/svg/node/image/overlay.ts b/packages/extension/src/webview/export/svg/node/image/overlay.ts index 73e4906e9c..405e59216b 100644 --- a/packages/extension/src/webview/export/svg/node/image/overlay.ts +++ b/packages/extension/src/webview/export/svg/node/image/overlay.ts @@ -2,6 +2,7 @@ import { getImage } from '../../../../components/graph/rendering/imageCache'; import { svgShapePath } from '../../shape/shapes'; import type { SvgExportNode, SvgPosition } from '../../contracts'; import type { NodeShape2D } from '../../../../../shared/settings/modes'; +import { fileIconSize } from '@codegraphy-dev/graph-renderer/visuals'; function buildClipShape(node: SvgExportNode, position: SvgPosition, shape: NodeShape2D): string { if (shape === 'circle') { @@ -28,7 +29,7 @@ export function appendNodeImageOverlay( } const clipId = `clip-${node.id.replace(/[^a-zA-Z0-9]/g, '_')}`; - const imageSize = node.size * 1.2; + const imageSize = fileIconSize(node.size); definitions.push(`${buildClipShape(node, position, shape)}`); const canvas = document.createElement('canvas'); diff --git a/packages/extension/src/webview/graphControls/filtering/nodes.ts b/packages/extension/src/webview/graphControls/filtering/nodes.ts index b8f70e0621..253f8e8c51 100644 --- a/packages/extension/src/webview/graphControls/filtering/nodes.ts +++ b/packages/extension/src/webview/graphControls/filtering/nodes.ts @@ -3,7 +3,7 @@ import { DEFAULT_FOLDER_NODE_COLOR, DEFAULT_NODE_COLOR, DEFAULT_PACKAGE_NODE_COLOR, -} from '../../../shared/fileColors'; +} from '@codegraphy-dev/graph-renderer/visuals'; import type { IGraphNodeTypeDefinition } from '../../../shared/graphControls/contracts'; import { CORE_GRAPH_NODE_TYPES } from '../../../shared/graphControls/defaults/nodeTypes'; import { symbolMatchesScopedDefinition } from '../../../shared/visibleGraph/scope/symbolMatch'; diff --git a/packages/extension/src/webview/search/filtering/rules/nodeLegend/apply.ts b/packages/extension/src/webview/search/filtering/rules/nodeLegend/apply.ts index 7d6bc88776..482ccdd0f7 100644 --- a/packages/extension/src/webview/search/filtering/rules/nodeLegend/apply.ts +++ b/packages/extension/src/webview/search/filtering/rules/nodeLegend/apply.ts @@ -1,4 +1,4 @@ -import { DEFAULT_NODE_COLOR } from '../../../../../shared/fileColors'; +import { DEFAULT_NODE_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import type { IGraphData } from '../../../../../shared/graph/contracts'; import { normalizeNodeLegendRules } from './compile'; import type { diff --git a/packages/extension/src/webview/store/defaults.ts b/packages/extension/src/webview/store/defaults.ts index b724dbddea..96046011a2 100644 --- a/packages/extension/src/webview/store/defaults.ts +++ b/packages/extension/src/webview/store/defaults.ts @@ -1,7 +1,4 @@ import type { SearchOptions } from '../components/searchBar/field/model'; -import { DEFAULT_PHYSICS_SETTINGS, type IPhysicsSettings } from '../../shared/settings/physics'; - -export const DEFAULT_PHYSICS: IPhysicsSettings = DEFAULT_PHYSICS_SETTINGS; export const DEFAULT_SEARCH_OPTIONS: SearchOptions = { matchCase: false, diff --git a/packages/extension/src/webview/store/initialState.ts b/packages/extension/src/webview/store/initialState.ts index 565091202d..c53c1c86f1 100644 --- a/packages/extension/src/webview/store/initialState.ts +++ b/packages/extension/src/webview/store/initialState.ts @@ -1,6 +1,9 @@ import type { GraphStateFields } from './state'; -import { DEFAULT_PHYSICS, DEFAULT_SEARCH_OPTIONS } from './defaults'; -import { DEFAULT_DIRECTION_COLOR } from '../../shared/fileColors'; +import { DEFAULT_SEARCH_OPTIONS } from './defaults'; +import { + DEFAULT_DIRECTION_COLOR, + DEFAULT_GRAPH_PHYSICS_SETTINGS, +} from '@codegraphy-dev/graph-renderer/visuals'; import { DEFAULT_MAX_FILES, DEFAULT_SHOW_MINIMAP } from '../../shared/settings/defaults'; export const INITIAL_STATE: GraphStateFields = { @@ -30,7 +33,7 @@ export const INITIAL_STATE: GraphStateFields = { cssSnippets: {}, graphViewportScale: null, nodeSizeMode: 'connections' as const, - physicsSettings: DEFAULT_PHYSICS, + physicsSettings: DEFAULT_GRAPH_PHYSICS_SETTINGS, depthMode: false, depthLimit: 1, maxDepthLimit: 10, diff --git a/packages/extension/src/webview/store/messageTypes.ts b/packages/extension/src/webview/store/messageTypes.ts index bdd8291ada..6cec132e31 100644 --- a/packages/extension/src/webview/store/messageTypes.ts +++ b/packages/extension/src/webview/store/messageTypes.ts @@ -11,7 +11,7 @@ import type { } from '../../shared/graphControls/contracts'; import type { IGroup } from '../../shared/settings/groups'; import type { BidirectionalEdgeMode, DirectionMode, NodeSizeMode } from '../../shared/settings/modes'; -import type { IPhysicsSettings } from '../../shared/settings/physics'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; import type { WorkspaceFilterAccounting } from '@codegraphy-dev/core'; import type { PendingGroupUpdates, @@ -46,7 +46,7 @@ export interface IStoreFields { cssSnippets: Record; graphViewportScale: number | null; nodeSizeMode: NodeSizeMode; - physicsSettings: IPhysicsSettings; + physicsSettings: GraphPhysicsSettings; depthMode: boolean; depthLimit: number; maxDepthLimit: number; diff --git a/packages/extension/src/webview/store/state.ts b/packages/extension/src/webview/store/state.ts index c68d50be13..f8d7603f77 100644 --- a/packages/extension/src/webview/store/state.ts +++ b/packages/extension/src/webview/store/state.ts @@ -16,7 +16,7 @@ import type { } from '../../shared/graphControls/contracts'; import type { IGroup } from '../../shared/settings/groups'; import type { BidirectionalEdgeMode, DirectionMode, NodeSizeMode } from '../../shared/settings/modes'; -import type { IPhysicsSettings } from '../../shared/settings/physics'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; import type { WorkspaceFilterAccounting } from '@codegraphy-dev/core'; import type { PendingGroupUpdates, @@ -50,7 +50,7 @@ export interface GraphState { cssSnippets: Record; graphViewportScale: number | null; nodeSizeMode: NodeSizeMode; - physicsSettings: IPhysicsSettings; + physicsSettings: GraphPhysicsSettings; depthMode: boolean; depthLimit: number; maxDepthLimit: number; @@ -83,7 +83,7 @@ export interface GraphState { setActivePanel: (panel: GraphState['activePanel']) => void; setGraphViewportScale: (scale: number | null) => void; setNodeSizeMode: (mode: NodeSizeMode) => void; - setPhysicsSettings: (settings: IPhysicsSettings) => void; + setPhysicsSettings: (settings: GraphPhysicsSettings) => void; setLegends: (legends: IGroup[]) => void; setOptimisticLegendUpdate: (legendId: string, updates: Partial) => void; setOptimisticLegendUpdates: (updatesByLegendId: Record>) => void; diff --git a/packages/extension/tests/extension/graphView/controls/definitions/definitions.test.ts b/packages/extension/tests/extension/graphView/controls/definitions/definitions.test.ts index 29cc2aae86..472148c74c 100644 --- a/packages/extension/tests/extension/graphView/controls/definitions/definitions.test.ts +++ b/packages/extension/tests/extension/graphView/controls/definitions/definitions.test.ts @@ -1,15 +1,11 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { mergeEdgeTypes, mergeNodeTypes } from '../../../../../src/extension/graphView/controls/send/definitions/merge'; import { STRUCTURAL_NESTS_EDGE_KIND } from '../../../../../src/shared/graphControls/defaults/definitions'; -import { normalizeHexColor } from '../../../../../src/shared/fileColors'; +import { normalizeHexColor } from '../../../../../src/shared/normalizeHexColor'; import { prettifyIdentifier } from '../../../../../src/extension/graphView/controls/send/definitions/identifiers'; -vi.mock('../../../../../src/shared/fileColors', async () => { - const actual = await vi.importActual('../../../../../src/shared/fileColors'); - return { - ...actual, - normalizeHexColor: vi.fn(), - }; +vi.mock('../../../../../src/shared/normalizeHexColor', () => { + return { normalizeHexColor: vi.fn() }; }); vi.mock('../../../../../src/extension/graphView/controls/send/definitions/identifiers', () => ({ diff --git a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/extensionMatch.test.ts b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/extensionMatch.test.ts index e887ec8719..d0c873c479 100644 --- a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/extensionMatch.test.ts +++ b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/extensionMatch.test.ts @@ -3,7 +3,7 @@ import { createMaterialExtensionMatcher, findLongestExtensionMatch, findLongestExtensionMatchWithMatcher, -} from '../../../../../../src/extension/graphView/groups/defaults/materialTheme/extensionMatch'; +} from '@codegraphy-dev/graph-renderer/visuals'; describe('graphView/materialTheme/extensionMatch', () => { it('matches bare extension filenames', () => { diff --git a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/fileExtension.test.ts b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/fileExtension.test.ts index e4f48e4f99..c22b2348dd 100644 --- a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/fileExtension.test.ts +++ b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/fileExtension.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { matchMaterialFileExtension } from '../../../../../../src/extension/graphView/groups/defaults/materialTheme/fileExtension'; +import { matchMaterialFileExtension } from '@codegraphy-dev/graph-renderer/visuals'; describe('graphView/materialTheme/fileExtension', () => { it('matches dotted suffixes and exact extension names', () => { diff --git a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/fileName.test.ts b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/fileName.test.ts index e9a88bdef5..008012c73d 100644 --- a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/fileName.test.ts +++ b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/fileName.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { matchMaterialFileName } from '../../../../../../src/extension/graphView/groups/defaults/materialTheme/fileName'; +import { matchMaterialFileName } from '@codegraphy-dev/graph-renderer/visuals'; describe('graphView/materialTheme/fileName', () => { it('matches basename and nested suffix rules', () => { diff --git a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/files.test.ts b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/files.test.ts index 3da07d46f0..8c158a8ed5 100644 --- a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/files.test.ts +++ b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/files.test.ts @@ -5,7 +5,7 @@ import { afterEach, describe, expect, it } from 'vitest'; import type { IGraphData } from '../../../../../../src/shared/graph/contracts'; import { collectMaterialFileGroups } from '../../../../../../src/extension/graphView/groups/defaults/materialTheme/files'; import type { MaterialThemeCacheEntry } from '../../../../../../src/extension/graphView/groups/defaults/materialTheme/model'; -import { createMaterialPathRuleMatcher } from '../../../../../../src/extension/graphView/groups/defaults/materialTheme/pathMatch'; +import { createMaterialPathRuleMatcher } from '@codegraphy-dev/graph-renderer/visuals'; const tempDirs: string[] = []; diff --git a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/folderName.test.ts b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/folderName.test.ts index de0e222c9b..111add3f35 100644 --- a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/folderName.test.ts +++ b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/folderName.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { matchMaterialFolderName } from '../../../../../../src/extension/graphView/groups/defaults/materialTheme/folderName'; +import { matchMaterialFolderName } from '@codegraphy-dev/graph-renderer/visuals'; describe('graphView/materialTheme/folderName', () => { it('matches basename folder rules across the tree', () => { diff --git a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/folders.test.ts b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/folders.test.ts index 95582b8786..d484ca76b3 100644 --- a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/folders.test.ts +++ b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/folders.test.ts @@ -6,7 +6,7 @@ import { afterEach, describe, expect, it } from 'vitest'; import type { IGraphData } from '../../../../../../src/shared/graph/contracts'; import { collectMaterialFolderGroups } from '../../../../../../src/extension/graphView/groups/defaults/materialTheme/folders'; import type { MaterialThemeCacheEntry } from '../../../../../../src/extension/graphView/groups/defaults/materialTheme/model'; -import { MATERIAL_TRANSPARENT_NODE_COLOR } from '../../../../../../src/extension/graphView/groups/defaults/materialTheme/groups'; +import { MATERIAL_TRANSPARENT_NODE_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; const tempDirs: string[] = []; diff --git a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/languageFallback.test.ts b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/languageFallback.test.ts index 10913e33cc..8db10d18a6 100644 --- a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/languageFallback.test.ts +++ b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/languageFallback.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { matchMaterialLanguageFallback } from '../../../../../../src/extension/graphView/groups/defaults/materialTheme/languageFallback'; +import { matchMaterialLanguageFallback } from '@codegraphy-dev/graph-renderer/visuals'; describe('graphView/materialTheme/languageFallback', () => { it('uses language fallback rules when the manifest defines the language id', () => { diff --git a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/pathMatch.test.ts b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/pathMatch.test.ts index 161dd4a6cc..02a474e907 100644 --- a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/pathMatch.test.ts +++ b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/pathMatch.test.ts @@ -3,7 +3,7 @@ import { createMaterialPathRuleMatcher, findLongestPathMatch, findLongestPathMatchWithMatcher, -} from '../../../../../../src/extension/graphView/groups/defaults/materialTheme/pathMatch'; +} from '@codegraphy-dev/graph-renderer/visuals'; describe('graphView/materialTheme/pathMatch', () => { it('matches basename rules case-insensitively', () => { diff --git a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/svg.test.ts b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/svg.test.ts index 53a978ee90..7fb7d9f54f 100644 --- a/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/svg.test.ts +++ b/packages/extension/tests/extension/graphView/groups/defaults/materialTheme/svg.test.ts @@ -1,6 +1,6 @@ import { Buffer } from 'node:buffer'; import { describe, expect, it } from 'vitest'; -import { extractPrimaryColor, toSvgDataUrl, toWhiteSvgDataUrl } from '../../../../../../src/extension/graphView/groups/defaults/materialTheme/svg'; +import { extractPrimaryColor, toSvgDataUrl, toWhiteSvgDataUrl } from '@codegraphy-dev/graph-renderer/visuals'; describe('graphView/materialTheme/svg', () => { it('extracts the most common hex color and falls back when none exist', () => { diff --git a/packages/extension/tests/extension/graphView/provider/physicsSettings.test.ts b/packages/extension/tests/extension/graphView/provider/physicsSettings.test.ts index e73ec1a128..d666763aed 100644 --- a/packages/extension/tests/extension/graphView/provider/physicsSettings.test.ts +++ b/packages/extension/tests/extension/graphView/provider/physicsSettings.test.ts @@ -1,10 +1,10 @@ import { describe, expect, it, vi } from 'vitest'; -import type { IPhysicsSettings } from '../../../../src/shared/settings/physics'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; import { createGraphViewProviderPhysicsSettingsMethods } from '../../../../src/extension/graphView/provider/physicsSettings'; describe('graphView/provider/physicsSettings', () => { it('reads and sends current physics settings through the provider message bridge', () => { - const readPhysicsSettings = vi.fn(() => ({ damping: 1 } as IPhysicsSettings)); + const readPhysicsSettings = vi.fn(() => ({ damping: 1 } as GraphPhysicsSettings)); const source = { _sendMessage: vi.fn() }; const configuration = { get: vi.fn((_, fallback) => fallback), @@ -16,7 +16,7 @@ describe('graphView/provider/physicsSettings', () => { readPhysicsSettings, updatePhysicsSetting: vi.fn(), resetPhysicsSettings: vi.fn(), - defaultPhysics: {} as IPhysicsSettings, + defaultPhysics: {} as GraphPhysicsSettings, }); expect(methods._getPhysicsSettings()).toEqual({ damping: 1 }); @@ -41,10 +41,10 @@ describe('graphView/provider/physicsSettings', () => { { _sendMessage: vi.fn() } as never, { getConfiguration, - readPhysicsSettings: vi.fn(() => ({ damping: 1 } as IPhysicsSettings)), + readPhysicsSettings: vi.fn(() => ({ damping: 1 } as GraphPhysicsSettings)), updatePhysicsSetting, resetPhysicsSettings: vi.fn(async () => undefined), - defaultPhysics: {} as IPhysicsSettings, + defaultPhysics: {} as GraphPhysicsSettings, }, ); @@ -73,10 +73,10 @@ describe('graphView/provider/physicsSettings', () => { { _sendMessage: vi.fn() } as never, { getConfiguration, - readPhysicsSettings: vi.fn(() => ({ damping: 1 } as IPhysicsSettings)), + readPhysicsSettings: vi.fn(() => ({ damping: 1 } as GraphPhysicsSettings)), updatePhysicsSetting: vi.fn(async () => undefined), resetPhysicsSettings, - defaultPhysics: {} as IPhysicsSettings, + defaultPhysics: {} as GraphPhysicsSettings, }, ); diff --git a/packages/extension/tests/extension/graphView/provider/settingsState.test.ts b/packages/extension/tests/extension/graphView/provider/settingsState.test.ts index 4f4e6eaa0b..2f772f4ce6 100644 --- a/packages/extension/tests/extension/graphView/provider/settingsState.test.ts +++ b/packages/extension/tests/extension/graphView/provider/settingsState.test.ts @@ -4,7 +4,7 @@ import path from 'path'; import * as vscode from 'vscode'; import { afterEach, describe, expect, it, vi } from 'vitest'; import type { IGraphData } from '../../../../src/shared/graph/contracts'; -import type { IPhysicsSettings } from '../../../../src/shared/settings/physics'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; import { createGraphViewProviderSettingsStateMethods, type GraphViewProviderSettingsStateMethodDependencies, @@ -39,7 +39,7 @@ function createSource( _computeMergedGroups: vi.fn(), _sendGroupsUpdated: vi.fn(), _sendMessage: vi.fn(), - _getPhysicsSettings: vi.fn(() => ({ damping: 1 } as IPhysicsSettings)), + _getPhysicsSettings: vi.fn(() => ({ damping: 1 } as GraphPhysicsSettings)), ...overrides, }; diff --git a/packages/extension/tests/extension/graphView/provider/settingsStateDefaultDependencies.test.ts b/packages/extension/tests/extension/graphView/provider/settingsStateDefaultDependencies.test.ts index a4fb744390..b3e2a900b6 100644 --- a/packages/extension/tests/extension/graphView/provider/settingsStateDefaultDependencies.test.ts +++ b/packages/extension/tests/extension/graphView/provider/settingsStateDefaultDependencies.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { WorkspaceFolder } from 'vscode'; import type { IGraphData } from '../../../../src/shared/graph/contracts'; -import type { IPhysicsSettings } from '../../../../src/shared/settings/physics'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; const mocks = vi.hoisted(() => { let workspaceFolders: WorkspaceFolder[] | undefined = undefined; @@ -96,7 +96,7 @@ function createSource( _computeMergedGroups: vi.fn(), _sendGroupsUpdated: vi.fn(), _sendMessage: vi.fn(), - _getPhysicsSettings: vi.fn(() => ({ damping: 1 } as IPhysicsSettings)), + _getPhysicsSettings: vi.fn(() => ({ damping: 1 } as GraphPhysicsSettings)), ...overrides, }; diff --git a/packages/extension/tests/extension/graphView/settings/physics/reader.test.ts b/packages/extension/tests/extension/graphView/settings/physics/reader.test.ts index 25c8fe9802..87dd6f8bb0 100644 --- a/packages/extension/tests/extension/graphView/settings/physics/reader.test.ts +++ b/packages/extension/tests/extension/graphView/settings/physics/reader.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it, vi } from 'vitest'; import type * as vscode from 'vscode'; -import type { IPhysicsSettings } from '../../../../../src/shared/settings/physics'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; import { readGraphViewPhysicsSettings } from '../../../../../src/extension/graphView/settings/physics/reader'; -const defaults: IPhysicsSettings = { +const defaults: GraphPhysicsSettings = { repelForce: 10, linkDistance: 80, linkForce: 0.15, @@ -20,8 +20,8 @@ describe('graphView/settings/physics/reader', () => { repelForce: 11, linkDistance: 81, linkForce: 1.15, - damping: 1.7, - centerForce: 1.1, + damping: 1, + centerForce: 1, }); expect(get.mock.calls.map(([key]) => key)).toEqual([ 'physics.repelForce', @@ -31,4 +31,12 @@ describe('graphView/settings/physics/reader', () => { 'physics.centerForce', ]); }); + + it('clamps a stored Link Distance through the shared settings boundary', () => { + const config = { + get: vi.fn((key: string, fallback: number) => key === 'physics.linkDistance' ? 500 : fallback), + } as unknown as vscode.WorkspaceConfiguration; + + expect(readGraphViewPhysicsSettings(config, defaults).linkDistance).toBe(150); + }); }); diff --git a/packages/extension/tests/extension/graphView/settings/physics/updates.test.ts b/packages/extension/tests/extension/graphView/settings/physics/updates.test.ts index 2c4a986e1d..b6435db5e4 100644 --- a/packages/extension/tests/extension/graphView/settings/physics/updates.test.ts +++ b/packages/extension/tests/extension/graphView/settings/physics/updates.test.ts @@ -12,7 +12,17 @@ describe('graph view physics update helpers', () => { getConfiguration: () => ({ update }), }); - expect(update).toHaveBeenCalledWith('physics.repelForce', 25); + expect(update).toHaveBeenCalledWith('physics.repelForce', 20); + }); + + it('clamps Link Distance before persistence', async () => { + const update = vi.fn(() => Promise.resolve()); + + await updateGraphViewPhysicsSetting('linkDistance', 500, { + getConfiguration: () => ({ update }), + }); + + expect(update).toHaveBeenCalledWith('physics.linkDistance', 150); }); it('updates a single physics setting at the requested configuration target', async () => { diff --git a/packages/extension/tests/extension/graphView/settings/reader.test.ts b/packages/extension/tests/extension/graphView/settings/reader.test.ts index 1abcda7fa1..df2aa8e745 100644 --- a/packages/extension/tests/extension/graphView/settings/reader.test.ts +++ b/packages/extension/tests/extension/graphView/settings/reader.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import * as vscode from 'vscode'; -import { DEFAULT_DIRECTION_COLOR, DEFAULT_FOLDER_NODE_COLOR } from '../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR, DEFAULT_FOLDER_NODE_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import { getGraphViewConfigTarget, normalizeDirectionColor, diff --git a/packages/extension/tests/extension/graphView/settings/snapshotMessages.test.ts b/packages/extension/tests/extension/graphView/settings/snapshotMessages.test.ts index ab40db3e2c..8d6a0c3668 100644 --- a/packages/extension/tests/extension/graphView/settings/snapshotMessages.test.ts +++ b/packages/extension/tests/extension/graphView/settings/snapshotMessages.test.ts @@ -4,7 +4,7 @@ import { buildGraphViewSettingsMessages, } from '../../../../src/extension/graphView/settings/messages'; import { captureGraphViewSettingsSnapshot } from '../../../../src/extension/graphView/settings/snapshot'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import { DEFAULT_MAX_FILES } from '../../../../src/shared/settings/defaults'; function createConfig(values: Record) { diff --git a/packages/extension/tests/extension/repoSettings/defaults.test.ts b/packages/extension/tests/extension/repoSettings/defaults.test.ts index 07faead617..636ceb9ab1 100644 --- a/packages/extension/tests/extension/repoSettings/defaults.test.ts +++ b/packages/extension/tests/extension/repoSettings/defaults.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import { CODEGRAPHY_MARKDOWN_PLUGIN_ID } from '@codegraphy-dev/core'; -import { DEFAULT_DIRECTION_COLOR } from '../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import { DEFAULT_MAX_FILES } from '../../../src/shared/settings/defaults'; import { createDefaultEdgeVisibility, diff --git a/packages/extension/tests/playwright/graphRenderer.package-resolution.ts b/packages/extension/tests/playwright/graphRenderer.package-resolution.ts new file mode 100644 index 0000000000..ce0bc56739 --- /dev/null +++ b/packages/extension/tests/playwright/graphRenderer.package-resolution.ts @@ -0,0 +1,21 @@ +import { + DEFAULT_DIRECTION_COLOR, + DEFAULT_NODE_COLOR, + DEFAULT_NODE_SIZE, + GRAPH_NODE_BORDER_WIDTH, + MAX_NODE_SIZE, + MIN_NODE_SIZE, + computeConnectionSizes, +} from '@codegraphy-dev/graph-renderer/visuals'; + +/** + * Keeps the workspace package surface under the extension's NodeNext resolution. + * The Playwright typecheck imports this file even when the package's own Bundler + * resolution accepts a source export that NodeNext cannot follow. + */ +export const graphRendererPackageResolutionProbe = { + colors: [DEFAULT_DIRECTION_COLOR, DEFAULT_NODE_COLOR], + connectionSizes: computeConnectionSizes([], []), + sizes: [MIN_NODE_SIZE, DEFAULT_NODE_SIZE, MAX_NODE_SIZE], + stroke: GRAPH_NODE_BORDER_WIDTH, +}; diff --git a/packages/extension/tests/shared/fileColors.test.ts b/packages/extension/tests/shared/fileColors.test.ts index 6d1b0d1083..86b2d89785 100644 --- a/packages/extension/tests/shared/fileColors.test.ts +++ b/packages/extension/tests/shared/fileColors.test.ts @@ -1,12 +1,12 @@ import { describe, it, expect } from 'vitest'; import { - normalizeHexColor, DEFAULT_NODE_COLOR, DEFAULT_FOLDER_NODE_COLOR, DEFAULT_DIRECTION_COLOR, FILE_TYPE_COLORS, getFileColor, -} from '../../src/shared/fileColors'; +} from '@codegraphy-dev/graph-renderer/visuals'; +import { normalizeHexColor } from '../../src/shared/normalizeHexColor'; describe('normalizeHexColor', () => { it('returns the default color when value is undefined', () => { diff --git a/packages/extension/tests/shared/graphControls/defaults/nodeTypes.test.ts b/packages/extension/tests/shared/graphControls/defaults/nodeTypes.test.ts index 151aabc4f9..248c32bab5 100644 --- a/packages/extension/tests/shared/graphControls/defaults/nodeTypes.test.ts +++ b/packages/extension/tests/shared/graphControls/defaults/nodeTypes.test.ts @@ -3,7 +3,7 @@ import { DEFAULT_FOLDER_NODE_COLOR, DEFAULT_NODE_COLOR, DEFAULT_PACKAGE_NODE_COLOR, -} from '../../../../src/shared/fileColors'; +} from '@codegraphy-dev/graph-renderer/visuals'; import { CORE_GRAPH_NODE_TYPES, createCoreGraphNodeTypes, diff --git a/packages/extension/tests/shared/mockData.test.ts b/packages/extension/tests/shared/mockData.test.ts index 529ce0accf..cadf261363 100644 --- a/packages/extension/tests/shared/mockData.test.ts +++ b/packages/extension/tests/shared/mockData.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; import { getMockGraphData, MOCK_FILE_DATA } from '../../src/shared/mockData'; -import { FILE_TYPE_COLORS, DEFAULT_NODE_COLOR } from '../../src/shared/fileColors'; +import { FILE_TYPE_COLORS, DEFAULT_NODE_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; describe('Mock Data', () => { describe('MOCK_FILE_DATA', () => { diff --git a/packages/extension/tests/webview/app/shell/behavior.keepseventopleftandtorendersthegraphwitheffective.test.tsx b/packages/extension/tests/webview/app/shell/behavior.keepseventopleftandtorendersthegraphwitheffective.test.tsx index 0e97ffc4cd..1da258e3db 100644 --- a/packages/extension/tests/webview/app/shell/behavior.keepseventopleftandtorendersthegraphwitheffective.test.tsx +++ b/packages/extension/tests/webview/app/shell/behavior.keepseventopleftandtorendersthegraphwitheffective.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import { graphStore } from '../../../../src/webview/store/state'; const harness = vi.hoisted(() => ({ diff --git a/packages/extension/tests/webview/app/shell/behavior.showstheloadingstatewhentodoesnotrendertheembedded.test.tsx b/packages/extension/tests/webview/app/shell/behavior.showstheloadingstatewhentodoesnotrendertheembedded.test.tsx index 55faba2246..29dfb59df4 100644 --- a/packages/extension/tests/webview/app/shell/behavior.showstheloadingstatewhentodoesnotrendertheembedded.test.tsx +++ b/packages/extension/tests/webview/app/shell/behavior.showstheloadingstatewhentodoesnotrendertheembedded.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import { graphStore } from '../../../../src/webview/store/state'; const harness = vi.hoisted(() => ({ diff --git a/packages/extension/tests/webview/app/shell/behavior/fixture.tsx b/packages/extension/tests/webview/app/shell/behavior/fixture.tsx index 3ca7bcb872..956d3042cf 100644 --- a/packages/extension/tests/webview/app/shell/behavior/fixture.tsx +++ b/packages/extension/tests/webview/app/shell/behavior/fixture.tsx @@ -1,5 +1,5 @@ import { vi } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import { graphStore } from '../../../../../src/webview/store/state'; const harness = vi.hoisted(() => ({ diff --git a/packages/extension/tests/webview/app/shell/mutations/fixture.tsx b/packages/extension/tests/webview/app/shell/mutations/fixture.tsx index 8d165d31d4..dc3418319f 100644 --- a/packages/extension/tests/webview/app/shell/mutations/fixture.tsx +++ b/packages/extension/tests/webview/app/shell/mutations/fixture.tsx @@ -1,5 +1,5 @@ import { vi } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import { graphStore } from '../../../../../src/webview/store/state'; const harness = vi.hoisted(() => ({ diff --git a/packages/extension/tests/webview/app/shell/runtime.mutations.test.tsx b/packages/extension/tests/webview/app/shell/runtime.mutations.test.tsx index d1ca32fcf1..19e71d22da 100644 --- a/packages/extension/tests/webview/app/shell/runtime.mutations.test.tsx +++ b/packages/extension/tests/webview/app/shell/runtime.mutations.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import { graphStore } from '../../../../src/webview/store/state'; const harness = vi.hoisted(() => ({ diff --git a/packages/extension/tests/webview/app/shell/view/fixture.ts b/packages/extension/tests/webview/app/shell/view/fixture.ts index 33df44f0bd..2ca96e836f 100644 --- a/packages/extension/tests/webview/app/shell/view/fixture.ts +++ b/packages/extension/tests/webview/app/shell/view/fixture.ts @@ -1,6 +1,6 @@ import { vi } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import { graphStore } from '../../../../../src/webview/store/state'; export const messageListeners: Array<(event: MessageEvent) => void> = []; diff --git a/packages/extension/tests/webview/export/shared/context.test.ts b/packages/extension/tests/webview/export/shared/context.test.ts index bfcb033399..9adeadcb1e 100644 --- a/packages/extension/tests/webview/export/shared/context.test.ts +++ b/packages/extension/tests/webview/export/shared/context.test.ts @@ -4,7 +4,7 @@ import { createImageExportDataUrl, resolveDirectionColor, } from '../../../../src/webview/export/shared/context'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; afterEach(() => { vi.restoreAllMocks(); diff --git a/packages/extension/tests/webview/export/svg/link/document.test.ts b/packages/extension/tests/webview/export/svg/link/document.test.ts index 5dcc86f570..c48bec8eeb 100644 --- a/packages/extension/tests/webview/export/svg/link/document.test.ts +++ b/packages/extension/tests/webview/export/svg/link/document.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import { assembleSvg, createBaseParts, createDefinitions, getPalette } from '../../../../../src/webview/export/svg/link/document'; import type { SvgExportOptions } from '../../../../../src/webview/export/svg/contracts'; diff --git a/packages/extension/tests/webview/export/svg/link/element/build.test.ts b/packages/extension/tests/webview/export/svg/link/element/build.test.ts index 3eb34887fa..1e8e59397a 100644 --- a/packages/extension/tests/webview/export/svg/link/element/build.test.ts +++ b/packages/extension/tests/webview/export/svg/link/element/build.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import { buildLinkElement } from '../../../../../../src/webview/export/svg/link/element/build'; import type { SvgExportLink } from '../../../../../../src/webview/export/svg/contracts'; diff --git a/packages/extension/tests/webview/export/svg/link/links.test.ts b/packages/extension/tests/webview/export/svg/link/links.test.ts index c7fa0fe59f..3ef07edf51 100644 --- a/packages/extension/tests/webview/export/svg/link/links.test.ts +++ b/packages/extension/tests/webview/export/svg/link/links.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import { appendLinkElements } from '../../../../../src/webview/export/svg/link/links'; import type { SvgExportLink } from '../../../../../src/webview/export/svg/contracts'; diff --git a/packages/extension/tests/webview/graph/drag.test.tsx b/packages/extension/tests/webview/graph/drag.test.tsx index f7f7c64117..232a827000 100644 --- a/packages/extension/tests/webview/graph/drag.test.tsx +++ b/packages/extension/tests/webview/graph/drag.test.tsx @@ -1,8 +1,8 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, act } from '@testing-library/react'; import Graph from '../../../src/webview/components/graph/view/component'; -import { toOwnedPhysicsConfig } from '../../../src/webview/components/graph/rendering/surface/owned2d/layout/runtime/model'; -import { DEFAULT_DIRECTION_COLOR } from '../../../src/shared/fileColors'; +import { toGraphPhysicsLayoutConfig } from '@codegraphy-dev/graph-renderer/visuals'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import type { IGraphData } from '../../../src/shared/graph/contracts'; import { graphStore } from '../../../src/webview/store/state'; import OwnedGraphSurface from '../../__mocks__/ownedGraphSurface'; @@ -134,7 +134,7 @@ describe('Graph: owned WebGPU rendering', () => { render(); const settings = OwnedGraphSurface.getLastProps().physicsSettings; expect(settings).toBeDefined(); - expect(toOwnedPhysicsConfig(settings!).centralGravity).toBe(1); + expect(toGraphPhysicsLayoutConfig(settings!).centralGravity).toBe(1); }); it('sends PHYSICS_STABILIZED when onEngineStop fires', () => { diff --git a/packages/extension/tests/webview/graph/marqueeSelection/view.test.tsx b/packages/extension/tests/webview/graph/marqueeSelection/view.test.tsx index 8346794a53..4a28b6e179 100644 --- a/packages/extension/tests/webview/graph/marqueeSelection/view.test.tsx +++ b/packages/extension/tests/webview/graph/marqueeSelection/view.test.tsx @@ -2,7 +2,7 @@ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest'; import OwnedGraphSurface from '../../../__mocks__/ownedGraphSurface'; import Graph from '../../../../src/webview/components/graph/view/component'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import type { IGraphData } from '../../../../src/shared/graph/contracts'; import { graphStore } from '../../../../src/webview/store/state'; diff --git a/packages/extension/tests/webview/graph/model/index.test.ts b/packages/extension/tests/webview/graph/model/index.test.ts index 2aadfe0074..000da164f1 100644 --- a/packages/extension/tests/webview/graph/model/index.test.ts +++ b/packages/extension/tests/webview/graph/model/index.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import type { IGraphData } from '../../../../src/shared/graph/contracts'; import { buildGraphData, diff --git a/packages/extension/tests/webview/graph/model/node/display.mutations.test.ts b/packages/extension/tests/webview/graph/model/node/display.mutations.test.ts index dce2567212..6c7d81b9e9 100644 --- a/packages/extension/tests/webview/graph/model/node/display.mutations.test.ts +++ b/packages/extension/tests/webview/graph/model/node/display.mutations.test.ts @@ -5,7 +5,7 @@ import { getDepthSizeMultiplier, getNodeType, } from '../../../../../src/webview/components/graph/model/node/display'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; describe('nodeDisplay (mutation targets)', () => { describe('resolveDirectionColor', () => { diff --git a/packages/extension/tests/webview/graph/model/node/display.mutations2.test.ts b/packages/extension/tests/webview/graph/model/node/display.mutations2.test.ts index 36b0805942..0ae89f0032 100644 --- a/packages/extension/tests/webview/graph/model/node/display.mutations2.test.ts +++ b/packages/extension/tests/webview/graph/model/node/display.mutations2.test.ts @@ -4,7 +4,7 @@ import { getDepthOpacity, getDepthSizeMultiplier, } from '../../../../../src/webview/components/graph/model/node/display'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; describe('nodeDisplay (mutation kill tests)', () => { /** diff --git a/packages/extension/tests/webview/graph/model/node/display.test.ts b/packages/extension/tests/webview/graph/model/node/display.test.ts index 18618f408f..fb6cca468a 100644 --- a/packages/extension/tests/webview/graph/model/node/display.test.ts +++ b/packages/extension/tests/webview/graph/model/node/display.test.ts @@ -1,7 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR, DEFAULT_NODE_SIZE } from '@codegraphy-dev/graph-renderer/visuals'; import { - DEFAULT_NODE_SIZE, FAVORITE_BORDER_COLOR, getDepthOpacity, getDepthSizeMultiplier, diff --git a/packages/extension/tests/webview/graph/model/sizing/calculations.mutations.test.ts b/packages/extension/tests/webview/graph/model/sizing/calculations.mutations.test.ts index f9b35cf4a1..45f1d91ecc 100644 --- a/packages/extension/tests/webview/graph/model/sizing/calculations.mutations.test.ts +++ b/packages/extension/tests/webview/graph/model/sizing/calculations.mutations.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { computeConnectionSizes } from '../../../../../src/webview/components/graph/model/sizing/calculations'; +import { computeConnectionSizes } from '@codegraphy-dev/graph-renderer/visuals'; function relatedGraph(relatedNodeCount: number) { const leaves = Array.from({ length: relatedNodeCount }, (_, index) => ({ diff --git a/packages/extension/tests/webview/graph/model/sizing/calculations.test.ts b/packages/extension/tests/webview/graph/model/sizing/calculations.test.ts index f20143293b..3f9e085e5a 100644 --- a/packages/extension/tests/webview/graph/model/sizing/calculations.test.ts +++ b/packages/extension/tests/webview/graph/model/sizing/calculations.test.ts @@ -3,7 +3,7 @@ import { MIN_NODE_SIZE, MAX_NODE_SIZE, computeConnectionSizes, -} from '../../../../../src/webview/components/graph/model/sizing/calculations'; +} from '@codegraphy-dev/graph-renderer/visuals'; describe('semantic node size range', () => { it('matches Obsidianโ€™s bounded radius domain', () => { diff --git a/packages/extension/tests/webview/graph/rendering/link/colors/model.usesthemutedlinkcolortonormalizesinvaliddirectioncolorsback.test.ts b/packages/extension/tests/webview/graph/rendering/link/colors/model.usesthemutedlinkcolortonormalizesinvaliddirectioncolorsback.test.ts index 0158c0bf90..9f70bc5417 100644 --- a/packages/extension/tests/webview/graph/rendering/link/colors/model.usesthemutedlinkcolortonormalizesinvaliddirectioncolorsback.test.ts +++ b/packages/extension/tests/webview/graph/rendering/link/colors/model.usesthemutedlinkcolortonormalizesinvaliddirectioncolorsback.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import type { EdgeDecorationPayload } from '../../../../../../src/shared/plugins/decorations'; import { DEFAULT_GRAPH_APPEARANCE } from '../../../../../../src/webview/components/graph/appearance/model'; import type { FGLink } from '../../../../../../src/webview/components/graph/model/build'; diff --git a/packages/extension/tests/webview/graph/rendering/links.test.ts b/packages/extension/tests/webview/graph/rendering/links.test.ts index ebf8b28e9c..adcbfea261 100644 --- a/packages/extension/tests/webview/graph/rendering/links.test.ts +++ b/packages/extension/tests/webview/graph/rendering/links.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { DEFAULT_DIRECTION_COLOR } from '../../../../src/shared/fileColors'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import type { EdgeDecorationPayload } from '../../../../src/shared/plugins/decorations'; import type { FGLink } from '../../../../src/webview/components/graph/model/build'; import { DEFAULT_GRAPH_APPEARANCE, type GraphAppearance } from '../../../../src/webview/components/graph/appearance/model'; diff --git a/packages/extension/tests/webview/graph/rendering/surface/owned2d/layout/runtime/model.test.ts b/packages/extension/tests/webview/graph/rendering/surface/owned2d/layout/runtime/model.test.ts index 013ee172cb..2b29fa9cce 100644 --- a/packages/extension/tests/webview/graph/rendering/surface/owned2d/layout/runtime/model.test.ts +++ b/packages/extension/tests/webview/graph/rendering/surface/owned2d/layout/runtime/model.test.ts @@ -1,10 +1,12 @@ import { describe, expect, it, vi } from 'vitest'; -import type { IPhysicsSettings } from '../../../../../../../../src/shared/settings/physics'; +import { + toGraphPhysicsLayoutConfig, + type GraphPhysicsSettings, +} from '@codegraphy-dev/graph-renderer/visuals'; import type { FGLink, FGNode } from '../../../../../../../../src/webview/components/graph/model/build'; import { ownedNodeCollisionRadius } from '../../../../../../../../src/webview/components/graph/rendering/surface/owned2d/layout/collision/radius'; import { createOwnedGraphLayout, - toOwnedPhysicsConfig, updateOwnedGraphLayout, } from '../../../../../../../../src/webview/components/graph/rendering/surface/owned2d/layout/runtime/model'; import { @@ -16,7 +18,7 @@ import { GraphNodeFlag, } from '@codegraphy-dev/graph-renderer'; -const DEFAULT_SETTINGS: IPhysicsSettings = { +const DEFAULT_SETTINGS: GraphPhysicsSettings = { centerForce: 0.1, damping: 0.4, linkDistance: 80, @@ -56,21 +58,21 @@ function run(engine: ReturnType, ticks = 240): v describe('owned graph layout settings', () => { it('maps every existing force setting to its semantic engine value', () => { - expect(toOwnedPhysicsConfig({ + expect(toGraphPhysicsLayoutConfig({ centerForce: 1, damping: 0.4, - linkDistance: 500, + linkDistance: 150, linkForce: 1, repelForce: 20, })).toEqual({ centralGravity: 1, chargeStrength: -500, - linkDistance: 500, + linkDistance: 150, linkStrength: 1, velocityDecay: 0.4, }); - expect(toOwnedPhysicsConfig({ + expect(toGraphPhysicsLayoutConfig({ centerForce: Number.POSITIVE_INFINITY, damping: -1, linkDistance: 1, @@ -87,19 +89,19 @@ describe('owned graph layout settings', () => { it('makes link distance and link force materially affect spring convergence', () => { const short = twoNodeEngine(300); - short.setConfig(toOwnedPhysicsConfig({ ...DEFAULT_SETTINGS, centerForce: 0, repelForce: 0, linkDistance: 30, linkForce: 1 })); + short.setConfig(toGraphPhysicsLayoutConfig({ ...DEFAULT_SETTINGS, centerForce: 0, repelForce: 0, linkDistance: 30, linkForce: 1 })); run(short); const long = twoNodeEngine(300); - long.setConfig(toOwnedPhysicsConfig({ ...DEFAULT_SETTINGS, centerForce: 0, repelForce: 0, linkDistance: 500, linkForce: 1 })); + long.setConfig(toGraphPhysicsLayoutConfig({ ...DEFAULT_SETTINGS, centerForce: 0, repelForce: 0, linkDistance: 150, linkForce: 1 })); run(long); const disabled = twoNodeEngine(300); - disabled.setConfig(toOwnedPhysicsConfig({ ...DEFAULT_SETTINGS, centerForce: 0, repelForce: 0, linkDistance: 30, linkForce: 0 })); + disabled.setConfig(toGraphPhysicsLayoutConfig({ ...DEFAULT_SETTINGS, centerForce: 0, repelForce: 0, linkDistance: 30, linkForce: 0 })); run(disabled, 60); const strong = twoNodeEngine(300); - strong.setConfig(toOwnedPhysicsConfig({ ...DEFAULT_SETTINGS, centerForce: 0, repelForce: 0, linkDistance: 30, linkForce: 1 })); + strong.setConfig(toGraphPhysicsLayoutConfig({ ...DEFAULT_SETTINGS, centerForce: 0, repelForce: 0, linkDistance: 30, linkForce: 1 })); run(strong, 60); const distance = (engine: ReturnType) => Math.abs(engine.x[1] - engine.x[0]); @@ -117,7 +119,7 @@ describe('owned graph layout settings', () => { edgeSources: new Uint32Array(), edgeTargets: new Uint32Array(), }); - engine.setConfig(toOwnedPhysicsConfig({ ...DEFAULT_SETTINGS, repelForce, centerForce })); + engine.setConfig(toGraphPhysicsLayoutConfig({ ...DEFAULT_SETTINGS, repelForce, centerForce })); run(engine, 120); return engine; }; diff --git a/packages/extension/tests/webview/graph/rendering/surface/owned2d/view/surface/fixture.ts b/packages/extension/tests/webview/graph/rendering/surface/owned2d/view/surface/fixture.ts index 674073e4c4..41662d3690 100644 --- a/packages/extension/tests/webview/graph/rendering/surface/owned2d/view/surface/fixture.ts +++ b/packages/extension/tests/webview/graph/rendering/surface/owned2d/view/surface/fixture.ts @@ -1,5 +1,5 @@ import { vi } from 'vitest'; -import { DEFAULT_PHYSICS_SETTINGS } from '../../../../../../../../src/shared/settings/physics'; +import { DEFAULT_GRAPH_PHYSICS_SETTINGS } from '@codegraphy-dev/graph-renderer/visuals'; import type { Surface2dProps } from '../../../../../../../../src/webview/components/graph/rendering/surface/owned2d/view/surface/contracts'; export function createDefaultSurfaceProps(): Surface2dProps { @@ -45,7 +45,7 @@ export function createDefaultSurfaceProps(): Surface2dProps { onRenderFramePost: vi.fn(), particleSize: 4, particleSpeed: 0.005, - physicsSettings: DEFAULT_PHYSICS_SETTINGS, + physicsSettings: DEFAULT_GRAPH_PHYSICS_SETTINGS, showFps: false, showMinimap: true, sharedProps: { diff --git a/packages/extension/tests/webview/graph/runtime/physics/root/settings.test.ts b/packages/extension/tests/webview/graph/runtime/physics/root/settings.test.ts index 9ee7c7166a..4a2d5ffbd0 100644 --- a/packages/extension/tests/webview/graph/runtime/physics/root/settings.test.ts +++ b/packages/extension/tests/webview/graph/runtime/physics/root/settings.test.ts @@ -1,11 +1,11 @@ import { describe, expect, it } from 'vitest'; -import { DEFAULT_PHYSICS_SETTINGS } from '../../../../../../src/shared/settings/physics'; +import { + DEFAULT_GRAPH_PHYSICS_SETTINGS, + applyGraphPhysicsSettings, + toGraphPhysicsLayoutConfig, +} from '@codegraphy-dev/graph-renderer/visuals'; import type { FGNode } from '../../../../../../src/webview/components/graph/model/build'; import { ownedNodeCollisionRadius } from '../../../../../../src/webview/components/graph/rendering/surface/owned2d/layout/collision/radius'; -import { - applyOwnedPhysicsSettings, - toOwnedPhysicsConfig, -} from '../../../../../../src/webview/components/graph/rendering/surface/owned2d/layout/runtime/model'; import { createGraphLayoutEngine } from '@codegraphy-dev/graph-renderer'; function engine() { @@ -21,7 +21,7 @@ function engine() { describe('owned physics settings', () => { it('maps every persisted force setting into the typed engine', () => { - expect(toOwnedPhysicsConfig(DEFAULT_PHYSICS_SETTINGS)).toEqual({ + expect(toGraphPhysicsLayoutConfig(DEFAULT_GRAPH_PHYSICS_SETTINGS)).toEqual({ centralGravity: 0.1, chargeStrength: -250, linkDistance: 80, @@ -37,7 +37,7 @@ describe('owned physics settings', () => { ['linkForce', { linkForce: 0.4 }, 'linkStrength', 0.4], ['damping', { damping: 0.2 }, 'velocityDecay', 0.2], ] as const)('maps changed %s values', (_field, patch, mappedField, expected) => { - expect(toOwnedPhysicsConfig({ ...DEFAULT_PHYSICS_SETTINGS, ...patch })[mappedField]).toBe(expected); + expect(toGraphPhysicsLayoutConfig({ ...DEFAULT_GRAPH_PHYSICS_SETTINGS, ...patch })[mappedField]).toBe(expected); }); it('reheats typed physics when settings are applied', () => { @@ -45,13 +45,13 @@ describe('owned physics settings', () => { for (let tick = 0; tick < 320; tick += 1) layout.tick(); expect(layout.settled).toBe(true); - applyOwnedPhysicsSettings(layout, { ...DEFAULT_PHYSICS_SETTINGS, centerForce: 1 }); + applyGraphPhysicsSettings(layout, { ...DEFAULT_GRAPH_PHYSICS_SETTINGS, centerForce: 1 }); expect(layout.settled).toBe(false); }); it('maps persisted damping to D3 velocity decay', () => { - expect(toOwnedPhysicsConfig({ ...DEFAULT_PHYSICS_SETTINGS, damping: 0.7 }).velocityDecay) + expect(toGraphPhysicsLayoutConfig({ ...DEFAULT_GRAPH_PHYSICS_SETTINGS, damping: 0.7 }).velocityDecay) .toBe(0.7); }); diff --git a/packages/extension/tests/webview/graph/runtime/tooltip/rect.test.ts b/packages/extension/tests/webview/graph/runtime/tooltip/rect.test.ts index c8f52ebe96..e4c25e6fdd 100644 --- a/packages/extension/tests/webview/graph/runtime/tooltip/rect.test.ts +++ b/packages/extension/tests/webview/graph/runtime/tooltip/rect.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import type { FGNode } from '../../../../../src/webview/components/graph/model/build'; -import { DEFAULT_NODE_SIZE } from '../../../../../src/webview/components/graph/model/build'; +import { DEFAULT_NODE_SIZE } from '@codegraphy-dev/graph-renderer/visuals'; import { getTooltipNodeRect } from '../../../../../src/webview/components/graph/runtime/tooltip/rect'; describe('getTooltipNodeRect', () => { diff --git a/packages/extension/tests/webview/graph/runtime/use/indicators/nodeAppearance.apply.test.ts b/packages/extension/tests/webview/graph/runtime/use/indicators/nodeAppearance.apply.test.ts index 8bf81faadf..7cf7fa05ab 100644 --- a/packages/extension/tests/webview/graph/runtime/use/indicators/nodeAppearance.apply.test.ts +++ b/packages/extension/tests/webview/graph/runtime/use/indicators/nodeAppearance.apply.test.ts @@ -1,8 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { - DEFAULT_NODE_SIZE, - FAVORITE_BORDER_COLOR, -} from '../../../../../../src/webview/components/graph/model/build'; +import { DEFAULT_NODE_SIZE } from '@codegraphy-dev/graph-renderer/visuals'; +import { FAVORITE_BORDER_COLOR } from '../../../../../../src/webview/components/graph/model/build'; import { applyNodeAppearance } from '../../../../../../src/webview/components/graph/runtime/use/indicators/nodeAppearance'; import { adjustColorForLightTheme } from '../../../../../../src/webview/theme/useTheme'; import { diff --git a/packages/extension/tests/webview/graph/runtime/use/indicators/nodeAppearance.fixture.ts b/packages/extension/tests/webview/graph/runtime/use/indicators/nodeAppearance.fixture.ts index 5950b4e6d8..7c831df701 100644 --- a/packages/extension/tests/webview/graph/runtime/use/indicators/nodeAppearance.fixture.ts +++ b/packages/extension/tests/webview/graph/runtime/use/indicators/nodeAppearance.fixture.ts @@ -1,9 +1,9 @@ import type { IGraphData } from '../../../../../../src/shared/graph/contracts'; import { DEFAULT_GRAPH_APPEARANCE } from '../../../../../../src/webview/components/graph/appearance/model'; import { - DEFAULT_NODE_SIZE, type FGNode, } from '../../../../../../src/webview/components/graph/model/build'; +import { DEFAULT_NODE_SIZE } from '@codegraphy-dev/graph-renderer/visuals'; export const DARK_APPEARANCE = { ...DEFAULT_GRAPH_APPEARANCE, diff --git a/packages/extension/tests/webview/graph/viewport/shellFixture.tsx b/packages/extension/tests/webview/graph/viewport/shellFixture.tsx index 718a00c1d0..ba3c79f33d 100644 --- a/packages/extension/tests/webview/graph/viewport/shellFixture.tsx +++ b/packages/extension/tests/webview/graph/viewport/shellFixture.tsx @@ -2,7 +2,7 @@ import React, { type ComponentProps, type ReactElement } from 'react'; import { render } from '@testing-library/react'; import { vi } from 'vitest'; import type { IGraphData } from '../../../../src/shared/graph/contracts'; -import type { IPhysicsSettings } from '../../../../src/shared/settings/physics'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; import type { GraphViewStoreState } from '../../../../src/webview/components/graph/view/store'; import type { UseGraphInteractionRuntimeResult } from '../../../../src/webview/components/graph/runtime/use/interaction'; import type { GraphRuntime } from '../../../../src/webview/components/graph/runtime/use/state'; @@ -219,7 +219,7 @@ export function createViewState(): Pick< GraphViewStoreState, 'bidirectionalMode' | 'depthMode' | 'directionMode' | 'favorites' | 'graphViewVisible' | 'nodeSizeMode' | 'particleSize' | 'particleSpeed' | 'physicsSettings' | 'showFps' | 'showLabels' | 'showMinimap' > { - const physicsSettings: IPhysicsSettings = { + const physicsSettings: GraphPhysicsSettings = { centerForce: 0.1, damping: 0.42, linkDistance: 120, diff --git a/packages/extension/tests/webview/graphControls/filtering/nodes.test.ts b/packages/extension/tests/webview/graphControls/filtering/nodes.test.ts index d4f021da84..7057a1fedc 100644 --- a/packages/extension/tests/webview/graphControls/filtering/nodes.test.ts +++ b/packages/extension/tests/webview/graphControls/filtering/nodes.test.ts @@ -11,7 +11,7 @@ import { DEFAULT_FOLDER_NODE_COLOR, DEFAULT_NODE_COLOR, DEFAULT_PACKAGE_NODE_COLOR, -} from '../../../../src/shared/fileColors'; +} from '@codegraphy-dev/graph-renderer/visuals'; function node(id: string, nodeType?: string, color = ''): IGraphNode { return { @@ -105,7 +105,7 @@ describe('webview/graphControls/filtering nodes', () => { it('uses node-type-specific fallback colors when no color is configured', async () => { vi.resetModules(); - vi.doMock('../../../../src/shared/fileColors', () => ({ + vi.doMock('@codegraphy-dev/graph-renderer/visuals', () => ({ DEFAULT_FOLDER_NODE_COLOR: '#folder', DEFAULT_NODE_COLOR: '#file', DEFAULT_PACKAGE_NODE_COLOR: '#package', @@ -126,7 +126,7 @@ describe('webview/graphControls/filtering nodes', () => { { ...node('a.ts'), color: '#file' }, ]); } finally { - vi.doUnmock('../../../../src/shared/fileColors'); + vi.doUnmock('@codegraphy-dev/graph-renderer/visuals'); vi.resetModules(); } }); diff --git a/packages/extension/tests/webview/search/filtering.test.ts b/packages/extension/tests/webview/search/filtering.test.ts index 915013cf60..d61a74dc51 100644 --- a/packages/extension/tests/webview/search/filtering.test.ts +++ b/packages/extension/tests/webview/search/filtering.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { DEFAULT_NODE_COLOR } from '../../../src/shared/fileColors'; +import { DEFAULT_NODE_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import type { IGraphData } from '../../../src/shared/graph/contracts'; import type { IGroup } from '../../../src/shared/settings/groups'; import { diff --git a/packages/extension/tests/webview/search/filtering/rules.test.ts b/packages/extension/tests/webview/search/filtering/rules.test.ts index 4aa8ebc027..54888c6328 100644 --- a/packages/extension/tests/webview/search/filtering/rules.test.ts +++ b/packages/extension/tests/webview/search/filtering/rules.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { DEFAULT_NODE_COLOR } from '../../../../src/shared/fileColors'; +import { DEFAULT_NODE_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import type { IGraphData } from '../../../../src/shared/graph/contracts'; import type { IGroup } from '../../../../src/shared/settings/groups'; import { applyLegendRules } from '../../../../src/webview/search/filtering/rules'; diff --git a/packages/extension/tests/webview/search/filtering/rules/nodes.test.ts b/packages/extension/tests/webview/search/filtering/rules/nodes.test.ts index 7a068c8ebd..0766dd6898 100644 --- a/packages/extension/tests/webview/search/filtering/rules/nodes.test.ts +++ b/packages/extension/tests/webview/search/filtering/rules/nodes.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { DEFAULT_NODE_COLOR } from '../../../../../src/shared/fileColors'; +import { DEFAULT_NODE_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import { applyNodeLegendRules, compileNodeLegendRules, diff --git a/packages/extension/tests/webview/settingsPanel/Drawer.test.tsx b/packages/extension/tests/webview/settingsPanel/Drawer.test.tsx index 84f5736529..bb8951b99a 100644 --- a/packages/extension/tests/webview/settingsPanel/Drawer.test.tsx +++ b/packages/extension/tests/webview/settingsPanel/Drawer.test.tsx @@ -1,7 +1,7 @@ import { act, fireEvent, render, screen } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import type { IPhysicsSettings } from '../../../src/shared/settings/physics'; -import { DEFAULT_DIRECTION_COLOR } from '../../../src/shared/fileColors'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; +import { DEFAULT_DIRECTION_COLOR } from '@codegraphy-dev/graph-renderer/visuals'; import SettingsPanel from '../../../src/webview/components/settingsPanel/Drawer'; import { graphStore } from '../../../src/webview/store/state'; @@ -11,7 +11,7 @@ vi.mock('../../../src/webview/vscodeApi', () => ({ vscode: { getState: () => undefined, setState: vi.fn() }, })); -const DEFAULT_PHYSICS: IPhysicsSettings = { +const DEFAULT_PHYSICS: GraphPhysicsSettings = { repelForce: 5, centerForce: 0.01, linkDistance: 100, diff --git a/packages/extension/tests/webview/settingsPanel/forces/Section.test.tsx b/packages/extension/tests/webview/settingsPanel/forces/Section.test.tsx index 522702f647..86c6798c83 100644 --- a/packages/extension/tests/webview/settingsPanel/forces/Section.test.tsx +++ b/packages/extension/tests/webview/settingsPanel/forces/Section.test.tsx @@ -2,11 +2,12 @@ import { act, fireEvent, render, screen } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { ForcesSection } from '../../../../src/webview/components/settingsPanel/forces/Section'; import { graphStore } from '../../../../src/webview/store/state'; -import type { IPhysicsSettings } from '../../../../src/shared/settings/physics'; +import type { GraphPhysicsSettings } from '@codegraphy-dev/graph-renderer/visuals'; vi.mock('../../../../src/webview/components/ui/controls/slider', () => ({ Slider: ({ 'data-testid': testId, + min, max, step, value, @@ -14,6 +15,7 @@ vi.mock('../../../../src/webview/components/ui/controls/slider', () => ({ onValueCommit, }: { 'data-testid'?: string; + min?: number; max?: number; step?: number; value?: number[]; @@ -26,7 +28,9 @@ vi.mock('../../../../src/webview/components/ui/controls/slider', () => ({