diff --git a/CLAUDE.md b/CLAUDE.md index 15301c4b..397f180d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -96,6 +96,51 @@ Perception is handled by `gently-perception` (separate repo: `pskeshu/gently-per The device layer runs as a separate process (`python start_device_layer.py`). It communicates with the agent via HTTP. Bluesky plans require ophyd device name kwargs (e.g. `xy_stage='xy_stage'`, `volume_scanner='volume_scanner'`) — these must match the device names registered in `device_factory.py`. +## Desktop App (Tauri) + +Gently can run as a Windows desktop app — a thin Tauri (WebView2) shell in +`desktop/` that OWNS the Python backend. It spawns `launch_gently.py --no-browser`, +shows a splash, then renders the live web UI (`http://localhost:8080`) in a native +window. The Python-served web UI stays the single source of truth — no app logic +lives in the shell. Full detail: `desktop/README.md`. + +### Build / run +``` +cd desktop +npm install # once — restores the Tauri CLI +npm run dev # dev: build + launch (spawns the backend for you) +npm run build # release: NSIS installer under src-tauri/target/release/bundle/ +``` +Prereqs: Rust (MSVC toolchain), the repo's uv `.venv`, WebView2 runtime (inbox on +Win 11). The **release** exe (`src-tauri/target/release/gently-desktop.exe`) is what +the Desktop shortcut points at; rebuild with `npm run build` to refresh it. + +### Key pieces +- `desktop/src-tauri/src/main.rs` — spawns the backend, waits for the server, + navigates the window to it, owns teardown. +- **No orphans:** the spawned Python (and its device-layer grandchild) run in a + Windows Job Object with `KILL_ON_JOB_CLOSE`, so quitting/crashing the shell reaps + the whole tree. +- **No console window:** the backend is spawned with `CREATE_NO_WINDOW` in release + (the device-layer supervisor does the same); `tauri dev` keeps the console so + logs stay visible while developing. + +### Editing code — what reflects +- **Web UI** (`gently/ui/web/templates`, `static/js`, `static/css`): refresh the + window (Ctrl+R) — served live by Python, no rebuild. +- **Python backend**: restart it, or run with `--reload` + (`launch_gently.py --reload`, or `GENTLY_LAUNCH_ARGS="--reload …"`) to auto-restart + on `gently/*.py` changes, then Ctrl+R. Whole-backend restart — not for live hardware. +- **Rust shell / `tauri.conf.json`**: `npm run dev` auto-rebuilds and relaunches. + +### Shell env config (read by `main.rs`) +`GENTLY_HOME` (repo dir), `GENTLY_PYTHON` (interpreter), `GENTLY_LAUNCH_ARGS` +(extra `launch_gently` args), `VIZ_PORT` (default 8080), `GENTLY_DEVICE_LAYER_SCRIPT`. + +### Deferred +Bundling the Python env (torch/anthropic/perception) for a redistributable +installer — today's build launches the repo's `.venv`, so it's single-machine. + ## Debugging Data Sources - **Agent logs**: `D:\Gently3\logs\gently_*.log` diff --git a/desktop/.gitignore b/desktop/.gitignore new file mode 100644 index 00000000..6b3daa59 --- /dev/null +++ b/desktop/.gitignore @@ -0,0 +1,14 @@ +# Node / Tauri CLI +node_modules/ + +# Rust / Tauri build output +src-tauri/target/ +src-tauri/gen/ + +# Generated mobile / MS-Store icon variants (regenerate with `tauri icon`). +# The desktop icons referenced by tauri.conf.json (32x32, 128x128, 128x128@2x, +# icon.ico, icon.icns, icon.png) ARE committed so a clean checkout builds. +src-tauri/icons/android/ +src-tauri/icons/ios/ +src-tauri/icons/Square*Logo.png +src-tauri/icons/StoreLogo.png diff --git a/desktop/README.md b/desktop/README.md new file mode 100644 index 00000000..52e8b0ae --- /dev/null +++ b/desktop/README.md @@ -0,0 +1,146 @@ +# Gently Desktop (Tauri shell) + +A thin [Tauri](https://v2.tauri.app) desktop wrapper that turns gently into a +double-click Windows app. It **owns** the Python backend: on launch it spawns +`launch_gently.py --no-browser`, shows a splash while the server boots, then +renders the existing web UI in a native WebView2 window. No application logic +lives here — the web UI served by Python stays the single source of truth. + +This is the desktop-packaging fold-in described in RFC #78 +(`docs/superpowers/specs/2026-07-02-unified-launcher-design.md`). + +## Why Tauri (and what it does / doesn't solve) + +- **Tiny footprint** — uses the OS WebView2 (already present on Win 11), not a + bundled Chromium. +- **Robust process ownership** — the shell puts the spawned Python into a + Windows **Job Object** with `KILL_ON_JOB_CLOSE`. Python's own device-layer + grandchild (via `DeviceLayerSupervisor`) inherits the job, so when the app + quits *or crashes* the OS reaps the **entire** process tree — no orphaned + device layer holding COM ports. +- **Not solved here:** bundling the Python environment (torch, anthropic, + perception) into a redistributable installer. This build launches the repo's + existing `.venv`. Shipping a self-contained `.msi`/`.exe` to another machine + needs an embeddable-Python / PyInstaller sidecar — see *Bundling* below. + +## Architecture + +``` +Tauri shell (Rust, WebView2 window) + └─ spawns: python launch_gently.py --no-browser [in a kill-on-close Job] + └─ spawns: start_device_layer.py (DeviceLayerSupervisor) + ├─ shows splash/index.html while uvicorn boots + └─ navigate() → http://localhost:8080 (the live gently UI) +``` + +- `src-tauri/src/main.rs` — spawn + Job Object teardown + wait-for-server + navigate. +- `splash/index.html` — boot splash (Rust updates its status line via `window.__gentlyStatus`). +- `src-tauri/tauri.conf.json` — one window, `frontendDist: ../splash`, NSIS bundle. + +## Prerequisites + +- **Rust** (MSVC toolchain) — `rustup default stable-x86_64-pc-windows-msvc` +- **MSVC C++ build tools** (Visual Studio 2022 / Build Tools) +- **WebView2 runtime** (inbox on Windows 11) +- **Node** (only for the Tauri CLI: `npm install` in this folder) +- A working gently checkout with its Python **`.venv`** at the repo root + +### Linux (dev machines) + +The shell is cross-platform — the Job-Object code is `#[cfg(windows)]`, so on +Linux teardown is the best-effort `child.kill()` only (fine for dev; the +kill-on-close guarantee is a Windows-production feature). Verified on Manjaro +(KDE/Wayland, webkit2gtk 2.52): + +- **Rust** — `rustup default stable` +- **WebKitGTK + GTK3** — `webkit2gtk-4.1` and `gtk3` dev packages + (Arch/Manjaro: `pacman -S webkit2gtk-4.1`; Debian/Ubuntu: + `apt install libwebkit2gtk-4.1-dev build-essential`) +- **Node** — as above +- The venv is found at **`.venv/bin/python`** (Unix layout) automatically; + `GENTLY_PYTHON` overrides as usual + +Same commands, bash syntax: + +```bash +cd desktop && npm install +GENTLY_LAUNCH_ARGS="--no-api --offline --no-auth" npm run dev +``` + +`npm run build` is Windows-only as configured (`bundle.targets: ["nsis"]`) — +on Linux use `npm run dev`, or override targets if a Linux bundle is ever needed. + +## Run (dev) + +```powershell +cd desktop +npm install # once — restores the Tauri CLI +npm run dev # build + launch the shell (spawns the backend for you) +``` + +`npm run dev` (= `tauri dev`) compiles the shell, opens the window, spawns +`launch_gently.py --no-browser`, and navigates to the live UI once it's up. + +UI-only (no hardware / API key / login) — handy for pure UI work: + +```powershell +$env:GENTLY_LAUNCH_ARGS="--no-api --offline --no-auth"; npm run dev +``` + +## Making code changes — what reflects, and how + +The window is just a WebView pointed at the Python server, so it splits in three: + +| You edit… | Reflects by… | Rebuild? | +|---|---|---| +| **Web UI** (`gently/ui/web/templates`, `static/js`, `static/css`) | **Refresh the window** (Ctrl+R) | no — served live by Python | +| **Python backend** (`gently/**/*.py`, `launch_gently.py`) | restart the backend — or run with **`--reload`** (below), then Ctrl+R | process restart | +| **Rust shell / config** (`src-tauri/`, `splash/`) | `npm run dev` **auto-rebuilds + relaunches** | automatic (watched) | + +**Python hot-reload.** Pass `--reload` and the backend auto-restarts whenever a +`gently/*.py` file changes (watchfiles) — then just Ctrl+R the window: + +```powershell +$env:GENTLY_LAUNCH_ARGS="--reload --no-api --offline --no-auth"; npm run dev +# or, iterating in a plain browser instead of the shell: +uv run python launch_gently.py --reload +``` + +It restarts the *whole* backend (a few seconds), so use it for UI / backend dev, +not live hardware sessions. + +## Build (installer) + +```powershell +cd desktop +npm run build # = tauri build — NSIS installer under src-tauri/target/release/bundle/ +``` + +Then Gently installs like any Windows app (Start-Menu entry, double-click — no +terminal). The built app launches the **repo's** `.venv` + `launch_gently.py` +(paths baked at compile time / overridable — see env vars); it's a single-machine +build until Python bundling lands. + +## Configuration (env vars read by the shell) + +| Var | Default | Purpose | +|---|---|---| +| `GENTLY_HOME` | compile-time repo root | Directory to run the backend from | +| `GENTLY_PYTHON` | `/.venv/Scripts/python.exe` | Python interpreter to launch | +| `GENTLY_LAUNCH_ARGS` | *(none)* | Extra args appended to `launch_gently.py --no-browser` | +| `VIZ_PORT` | `8080` | Port the shell waits on and navigates to | +| `GENTLY_DEVICE_LAYER_SCRIPT` | `start_device_layer.py` | Alternate device-layer entry point (read by `DeviceLayerSupervisor`) | + +## Bundling Python (the remaining long pole) + +`tauri build` today does **not** package Python. To make a redistributable app: + +1. Produce a self-contained backend (PyInstaller one-folder of `launch_gently`, + or an embeddable-Python tree with the deps installed). +2. Ship it as a Tauri **sidecar** (or under the app's resource dir) and point + `GENTLY_PYTHON` / `GENTLY_HOME` at it. +3. Expect a multi-GB artifact (torch/CUDA dominate) and plan auto-update + accordingly. + +The Job-Object ownership and boot/navigate flow are unchanged by bundling — only +*where Python comes from* changes. diff --git a/desktop/icons/source.png b/desktop/icons/source.png new file mode 100644 index 00000000..8f2af46c Binary files /dev/null and b/desktop/icons/source.png differ diff --git a/desktop/package-lock.json b/desktop/package-lock.json new file mode 100644 index 00000000..63621f9e --- /dev/null +++ b/desktop/package-lock.json @@ -0,0 +1,232 @@ +{ + "name": "gently-desktop", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gently-desktop", + "version": "0.1.0", + "devDependencies": { + "@tauri-apps/cli": "^2.11.4" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.4.tgz", + "integrity": "sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.11.4", + "@tauri-apps/cli-darwin-x64": "2.11.4", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.4", + "@tauri-apps/cli-linux-arm64-gnu": "2.11.4", + "@tauri-apps/cli-linux-arm64-musl": "2.11.4", + "@tauri-apps/cli-linux-riscv64-gnu": "2.11.4", + "@tauri-apps/cli-linux-x64-gnu": "2.11.4", + "@tauri-apps/cli-linux-x64-musl": "2.11.4", + "@tauri-apps/cli-win32-arm64-msvc": "2.11.4", + "@tauri-apps/cli-win32-ia32-msvc": "2.11.4", + "@tauri-apps/cli-win32-x64-msvc": "2.11.4" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.4.tgz", + "integrity": "sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.4.tgz", + "integrity": "sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.4.tgz", + "integrity": "sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.4.tgz", + "integrity": "sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.4.tgz", + "integrity": "sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.4.tgz", + "integrity": "sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.4.tgz", + "integrity": "sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.4.tgz", + "integrity": "sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.4.tgz", + "integrity": "sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.4.tgz", + "integrity": "sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.4.tgz", + "integrity": "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + } + } +} diff --git a/desktop/package.json b/desktop/package.json new file mode 100644 index 00000000..0173efb1 --- /dev/null +++ b/desktop/package.json @@ -0,0 +1,14 @@ +{ + "name": "gently-desktop", + "version": "0.1.0", + "private": true, + "description": "Tauri desktop shell for gently (owns the Python backend + device layer)", + "scripts": { + "tauri": "tauri", + "dev": "tauri dev", + "build": "tauri build" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.11.4" + } +} diff --git a/desktop/splash/index.html b/desktop/splash/index.html new file mode 100644 index 00000000..f7bb3357 --- /dev/null +++ b/desktop/splash/index.html @@ -0,0 +1,51 @@ + + + + + +Gently + + + + +
+
Gently
+
+
Starting the backend…
+
microscopy agent · desktop
+ + + diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock new file mode 100644 index 00000000..27a8fbb4 --- /dev/null +++ b/desktop/src-tauri/Cargo.lock @@ -0,0 +1,4433 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6aedf8ae72766347502cf3cb4f41cf5e9cc37d28bee90f1fdaaae15f9cf9424" + +[[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.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2d30e4173c4026932d51d31d6b0613b1fd3014bf3f9f8943d4ba139c437ba0" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.2.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" +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 = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.0", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.0", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "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.118", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.118", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dbus" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.118", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.0", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.2+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gently-desktop" +version = "0.1.0" +dependencies = [ + "tauri", + "tauri-build", + "windows 0.62.2", +] + +[[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.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.13.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +dependencies = [ + "libc", +] + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.0", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plist" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.12+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "regex" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db" +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 = "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 = "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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.118", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.13.0", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +dependencies = [ + "bitflags 2.13.0", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "667b20e2726d572dea2de7370da16e188eb06008faf9a92fab7cdc46791190b5" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows 0.61.3", +] + +[[package]] +name = "tauri-build" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.118", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows 0.61.3", +] + +[[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 0.61.3", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.2+spec-1.1.0", +] + +[[package]] +name = "tendril" +version = "0.5.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.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "time" +version = "0.3.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.3", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.0", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "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.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.118", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_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 0.61.3", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows 0.61.3", + "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 0.2.0", + "windows-core 0.61.2", + "windows-future 0.2.1", + "windows-link 0.1.3", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + +[[package]] +name = "windows-core" +version = "0.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 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-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.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", + "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-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.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_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_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_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[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_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_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_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 = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml new file mode 100644 index 00000000..6d85d560 --- /dev/null +++ b/desktop/src-tauri/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "gently-desktop" +version = "0.1.0" +edition = "2021" +description = "Gently desktop shell — owns the Python backend + device layer" +authors = ["gently"] +default-run = "gently-desktop" + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2" } + +# Windows Job Object: put the spawned Python (and its device-layer grandchild) +# in a kill-on-close job so the whole tree dies when the shell exits or crashes. +[target.'cfg(windows)'.dependencies] +windows = { version = "0.62", features = [ + "Win32_Foundation", + "Win32_System_JobObjects", + "Win32_System_Threading", + "Win32_Security", +] } + +[profile.release] +codegen-units = 1 +lto = true +opt-level = "s" +strip = true diff --git a/desktop/src-tauri/build.rs b/desktop/src-tauri/build.rs new file mode 100644 index 00000000..d860e1e6 --- /dev/null +++ b/desktop/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/desktop/src-tauri/capabilities/default.json b/desktop/src-tauri/capabilities/default.json new file mode 100644 index 00000000..c1da0cbb --- /dev/null +++ b/desktop/src-tauri/capabilities/default.json @@ -0,0 +1,7 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Thin shell — the UI is served by the Python backend and makes no Tauri IPC calls. Navigation is driven from Rust, so no window/shell permissions are needed.", + "windows": ["main"], + "permissions": ["core:default"] +} diff --git a/desktop/src-tauri/icons/128x128.png b/desktop/src-tauri/icons/128x128.png new file mode 100644 index 00000000..28f09f10 Binary files /dev/null and b/desktop/src-tauri/icons/128x128.png differ diff --git a/desktop/src-tauri/icons/128x128@2x.png b/desktop/src-tauri/icons/128x128@2x.png new file mode 100644 index 00000000..ceff7feb Binary files /dev/null and b/desktop/src-tauri/icons/128x128@2x.png differ diff --git a/desktop/src-tauri/icons/32x32.png b/desktop/src-tauri/icons/32x32.png new file mode 100644 index 00000000..777c4bb9 Binary files /dev/null and b/desktop/src-tauri/icons/32x32.png differ diff --git a/desktop/src-tauri/icons/64x64.png b/desktop/src-tauri/icons/64x64.png new file mode 100644 index 00000000..f05ad3ab Binary files /dev/null and b/desktop/src-tauri/icons/64x64.png differ diff --git a/desktop/src-tauri/icons/icon.icns b/desktop/src-tauri/icons/icon.icns new file mode 100644 index 00000000..f0a19c15 Binary files /dev/null and b/desktop/src-tauri/icons/icon.icns differ diff --git a/desktop/src-tauri/icons/icon.ico b/desktop/src-tauri/icons/icon.ico new file mode 100644 index 00000000..744ad499 Binary files /dev/null and b/desktop/src-tauri/icons/icon.ico differ diff --git a/desktop/src-tauri/icons/icon.png b/desktop/src-tauri/icons/icon.png new file mode 100644 index 00000000..22963015 Binary files /dev/null and b/desktop/src-tauri/icons/icon.png differ diff --git a/desktop/src-tauri/src/main.rs b/desktop/src-tauri/src/main.rs new file mode 100644 index 00000000..ff706c69 --- /dev/null +++ b/desktop/src-tauri/src/main.rs @@ -0,0 +1,247 @@ +// Gently desktop shell. +// +// A thin Tauri (WebView2) window that OWNS the Python backend. On launch it +// spawns `launch_gently.py --no-browser`, shows a splash while uvicorn boots, +// then navigates the window to the live UI (http://localhost:). The whole +// UI is served by Python — this shell holds no application logic. +// +// Process ownership: the Python child spawns its own device-layer grandchild +// (via DeviceLayerSupervisor). To guarantee no orphans, on Windows we put the +// spawned Python into a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; +// children inherit the job, so when this shell exits or crashes the OS reaps the +// entire tree. See docs/superpowers/specs/2026-07-02-unified-launcher-design.md. +// +// Deferred (documented): bundling the Python environment for a redistributable +// installer (embeddable Python / PyInstaller sidecar), and a graceful backend +// shutdown handshake before the job hard-kills the tree. + +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +use std::path::{Path, PathBuf}; +use std::process::{Child, Command}; +use std::sync::Mutex; +use std::time::{Duration, Instant}; + +use tauri::{Manager, RunEvent}; + +/// Backend process + (Windows) the job that owns its tree, held for the app's +/// whole lifetime as Tauri managed state. +struct Backend { + child: Mutex>, + #[cfg(windows)] + job: jobkill::Job, +} + +fn main() { + #[cfg(windows)] + let job = jobkill::create_kill_on_close().expect("failed to create Job Object"); + + let backend = Backend { + child: Mutex::new(None), + #[cfg(windows)] + job, + }; + + tauri::Builder::default() + .manage(backend) + .setup(|app| { + let handle = app.handle().clone(); + // Boot the backend off the UI thread so the splash paints immediately. + std::thread::spawn(move || boot_backend(handle)); + Ok(()) + }) + .build(tauri::generate_context!()) + .expect("error building the Gently desktop app") + .run(|app_handle, event| { + if let RunEvent::Exit = event { + let b = app_handle.state::(); + // Best-effort direct kill of the child we spawned... + // (trailing `;` so the MutexGuard temporary drops before `b` — + // without it this is the block's tail expression on non-Windows, + // where the cfg'd jobkill line below is compiled out: E0597) + if let Some(mut child) = b.child.lock().unwrap().take() { + let _ = child.kill(); + }; + // ...and close the job handle, which kill-on-close uses to reap + // the whole tree (python + device-layer grandchild). Correct even + // if the child kill above missed a grandchild. + #[cfg(windows)] + jobkill::close(&b.job); + } + }); +} + +/// Spawn the Python backend, wait for its server, then navigate the window to it. +fn boot_backend(app: tauri::AppHandle) { + let repo = repo_root(); + let python = python_exe(&repo); + + let mut args: Vec = vec!["launch_gently.py".into(), "--no-browser".into()]; + if let Ok(extra) = std::env::var("GENTLY_LAUNCH_ARGS") { + args.extend(extra.split_whitespace().map(str::to_string)); + } + + set_status(&app, "Starting the backend…"); + eprintln!("[gently-desktop] spawning: {} {:?} (cwd={})", python.display(), args, repo.display()); + + let mut cmd = Command::new(&python); + cmd.args(&args).current_dir(&repo); + // Release: run the console-subsystem Python backend WITHOUT a console window + // (a GUI parent would otherwise pop one). Debug (`tauri dev`) inherits the dev + // console so backend logs stay visible while developing. + #[cfg(all(windows, not(debug_assertions)))] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + cmd.creation_flags(CREATE_NO_WINDOW); + } + + let child = match cmd.spawn() { + Ok(c) => c, + Err(e) => { + set_status(&app, &format!("Failed to start backend: {e}")); + eprintln!("[gently-desktop] spawn failed: {e}"); + return; + } + }; + + // Put the child in our kill-on-close job so its tree can't outlive us. + #[cfg(windows)] + { + let b = app.state::(); + if let Err(e) = jobkill::assign(&b.job, &child) { + eprintln!("[gently-desktop] job assign failed (continuing): {e:?}"); + } + } + *app.state::().child.lock().unwrap() = Some(child); + + let port = viz_port(); + set_status(&app, "Waiting for the server…"); + if wait_for_port("127.0.0.1", port, 120) { + let url = format!("http://localhost:{port}"); + eprintln!("[gently-desktop] backend up — navigating to {url}"); + if let Some(win) = app.get_webview_window("main") { + match url.parse() { + Ok(u) => { let _ = win.navigate(u); } + Err(e) => set_status(&app, &format!("Bad backend URL: {e}")), + } + } + } else { + set_status(&app, "Backend did not become ready in time. Check the terminal log."); + eprintln!("[gently-desktop] timed out waiting for 127.0.0.1:{port}"); + } +} + +/// Repo root: `GENTLY_HOME` override, else the compile-time project root +/// (desktop/src-tauri/../..). The compile-time path is correct for `tauri dev` +/// and for a build run on this machine; a redistributable build should set +/// `GENTLY_HOME` (or bundle Python — see the module header). +fn repo_root() -> PathBuf { + if let Ok(p) = std::env::var("GENTLY_HOME") { + return PathBuf::from(p); + } + let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let root = manifest.join("..").join(".."); + std::fs::canonicalize(&root).unwrap_or(root) +} + +/// Python interpreter: `GENTLY_PYTHON` override, else the repo venv, else PATH. +fn python_exe(repo: &Path) -> PathBuf { + if let Ok(p) = std::env::var("GENTLY_PYTHON") { + return PathBuf::from(p); + } + let venv = if cfg!(windows) { + repo.join(".venv").join("Scripts").join("python.exe") + } else { + repo.join(".venv").join("bin").join("python") + }; + if venv.exists() { + venv + } else { + PathBuf::from("python") + } +} + +/// Viz port — mirrors the backend's `VIZ_PORT` env (default 8080). +fn viz_port() -> u16 { + std::env::var("VIZ_PORT") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(8080) +} + +/// Poll a TCP port until something accepts, or `secs` elapse. +fn wait_for_port(host: &str, port: u16, secs: u64) -> bool { + use std::net::TcpStream; + let addr: std::net::SocketAddr = match format!("{host}:{port}").parse() { + Ok(a) => a, + Err(_) => return false, + }; + let deadline = Instant::now() + Duration::from_secs(secs); + while Instant::now() < deadline { + if TcpStream::connect_timeout(&addr, Duration::from_millis(800)).is_ok() { + return true; + } + std::thread::sleep(Duration::from_millis(700)); + } + false +} + +/// Update the splash status line via the JS hook it exposes. +fn set_status(app: &tauri::AppHandle, msg: &str) { + if let Some(win) = app.get_webview_window("main") { + let safe = msg.replace('\\', "\\\\").replace('\'', "\\'"); + let js = format!("window.__gentlyStatus && window.__gentlyStatus('{safe}')"); + let _ = win.eval(js.as_str()); + } +} + +/// Windows Job Object helper — kill-on-close ownership of the backend tree. +#[cfg(windows)] +mod jobkill { + use std::ffi::c_void; + use std::os::windows::io::AsRawHandle; + use std::process::Child; + + use windows::Win32::Foundation::{CloseHandle, HANDLE}; + use windows::Win32::System::JobObjects::{ + AssignProcessToJobObject, CreateJobObjectW, SetInformationJobObject, + JobObjectExtendedLimitInformation, JOBOBJECT_EXTENDED_LIMIT_INFORMATION, + JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, + }; + + /// Owns a job HANDLE. `Send`/`Sync` so it can live in Tauri managed state; + /// the handle is only touched from create/assign/close. + pub struct Job(pub HANDLE); + unsafe impl Send for Job {} + unsafe impl Sync for Job {} + + pub fn create_kill_on_close() -> windows::core::Result { + unsafe { + let job = CreateJobObjectW(None, None)?; + let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default(); + info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + SetInformationJobObject( + job, + JobObjectExtendedLimitInformation, + &info as *const _ as *const c_void, + std::mem::size_of::() as u32, + )?; + Ok(Job(job)) + } + } + + /// Assign a freshly-spawned child to the job. Its own children inherit the + /// job (Windows default), so grandchildren are covered too. + pub fn assign(job: &Job, child: &Child) -> windows::core::Result<()> { + unsafe { AssignProcessToJobObject(job.0, HANDLE(child.as_raw_handle())) } + } + + /// Close the job handle. With kill-on-close, this terminates every process + /// still in the job. + pub fn close(job: &Job) { + unsafe { + let _ = CloseHandle(job.0); + } + } +} diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json new file mode 100644 index 00000000..d7dc638e --- /dev/null +++ b/desktop/src-tauri/tauri.conf.json @@ -0,0 +1,38 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "productName": "Gently", + "version": "0.1.0", + "identifier": "org.janelia.gently.desktop", + "build": { + "frontendDist": "../splash" + }, + "app": { + "windows": [ + { + "label": "main", + "title": "Gently", + "width": 1440, + "height": 920, + "minWidth": 900, + "minHeight": 600, + "resizable": true, + "visible": true, + "center": true + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": ["nsis"], + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + } +} diff --git a/docs/superpowers/mockups/2026-07-02-launcher-gate.html b/docs/superpowers/mockups/2026-07-02-launcher-gate.html new file mode 100644 index 00000000..18879dde --- /dev/null +++ b/docs/superpowers/mockups/2026-07-02-launcher-gate.html @@ -0,0 +1,82 @@ + + + + + +Launch Gently + + + +
+
Gently
+

What do you need today?

+ +
+
+ +
+
+
Microscope hardware
+
Start the device layer & connect to the rig · :60610
+
+ +
+ +
+
+ +
+
+
AI agent
+
Chat, perception & planning — uses your API key
+
+ +
+ + +
Advanced options · remembers your choice
+
+ + diff --git a/docs/superpowers/specs/2026-07-02-unified-launcher-design.md b/docs/superpowers/specs/2026-07-02-unified-launcher-design.md new file mode 100644 index 00000000..3e24228c --- /dev/null +++ b/docs/superpowers/specs/2026-07-02-unified-launcher-design.md @@ -0,0 +1,136 @@ +# Unified Launcher — single entry point + device-layer process management — Design + +Date: 2026-07-02 +Status: **Initial design (RFC) — PARKED.** A starting point to build on, not a finished +design; direction + mockup done, implementation deferred to a future session. +Idea credit: **Magdalena** — the single unified launcher. +Branch: `feature/unified-launcher` (off `development`). + +## Problem + +Starting gently today is two commands in two terminals: `python start_device_layer.py` +(the hardware server) then `python launch_gently.py` (agent + web). gently can't +start or stop the device layer — it only connects to it as an HTTP client — so there's +no in‑app way to bring hardware up/down. Goal: make `launch_gently.py` the **single** +entry point, with a dead‑simple launch gate and the ability to start/stop the device +layer from gently. + +## How the two processes relate (today) + +- `start_device_layer.py` boots the hardware control server (aiohttp) on **port 60610** + (`DEVICE_PORT`) and runs the configured hardware module's `create_device_layer()`. + Independent OS process. +- `launch_gently.py` boots the agent + web/viz server and creates an HTTP client + (`QueueServerClient → http://127.0.0.1:60610`); `client.is_connected` drives the + "Device: ○ offline" banner. The two only talk over HTTP. + +## The launch gate — BARE BASIC (the key decision) + +The launch screen answers exactly two questions, nothing more: + +1. **Microscope hardware — on/off.** On → gently starts + connects the device layer. + Off → software‑only (analysis, planning, reviewing saved data). +2. **AI agent (API) — on/off.** On → chat, perception, planning (uses the API key). + Off → UI‑only. + +Then **Let's go →**. That's the whole screen. Everything else — organism, hardware +module, device port, SAM device, session resume — is a **default** (config / last‑used), +reachable behind a muted **"Advanced options"** disclosure and in **Settings**. It is +NOT on the gate: if you're not using hardware, you should never be asked which hardware. + +Visual reference (dark, sharp, sleek): `docs/superpowers/mockups/2026-07-02-launcher-gate.html` +— two toggle cards (`Microscope hardware`, `AI agent`) + `Let's go →` + `Advanced +options · remembers your choice`. + +## Process model — managed child subprocess + +A **`DeviceLayerSupervisor`** in the gently process spawns `start_device_layer.py` as a +**child** (`subprocess.Popen([sys.executable, "start_device_layer.py", "--port", …, +"--sam-device", …])`), holds its handle, monitors liveness, captures its log, and stops +it. Ownership is what makes start/stop‑from‑UI and no‑orphans work. +- API: `start(config)`, `stop(force=False)`, `status()`, log tail, `atexit`/signal cleanup. +- **External device layer still supported:** if one is already running on 60610, gently + connects and shows it as "external (not managed)" — it just won't stop what it didn't start. +- Rejected alternatives: independent processes + a `/shutdown` endpoint (weaker ownership, + more parts); in‑process device layer (a hardware crash would take down the UI). + +## Boot flow (defer‑init) + +`launch_gently` boots a minimal web server → shows the **launch gate** (prefilled from +last choice) → on **Let's go**, it initializes per the two toggles: start the agent (if +API on) and start the device layer via the supervisor (if hardware on) → then the landing +("Good afternoon…") → dashboard. Deferring heavy init until the gate is submitted is what +lets a web screen control boot‑level behavior (agent on/off). + +## Runtime control + +A **device‑layer panel in the Devices tab** mirrors the gate's hardware block at runtime: +live status (running / stopped / external / crashed), Start / Stop, and a tail of the +device‑layer log — so you can bring hardware up/down without restarting gently. + +## Stop safety (graceful + mid‑run guard) + +Stop → if a plan/acquisition is active, warn + require explicit confirm ("hardware is +active — stop anyway?") → SIGTERM (clean shutdown) → SIGKILL fallback (~5 s). Reuses the +409 + `"blocked"` pattern from the thermalizer work. Startup failure (hardware off) +surfaces the existing plain‑language `_render_startup_failure` diagnosis, not a traceback. + +## Persistence + +`config/launch.local.json` (gitignored) remembers the two toggles (+ any advanced values) +so the gate is prefilled every boot. + +## Non‑goals (YAGNI) + +Auto‑restart‑on‑crash loops; multiple / remote device layers (mesh already covers +cross‑machine); managing an externally‑started device layer's lifecycle; putting +organism/hardware/port/SAM/session on the gate (they're defaults + Advanced/Settings). + +## Phasing (when resumed) + +1. `DeviceLayerSupervisor` + the runtime Devices start/stop panel + the **hardware toggle** + on a minimal gate. Delivers "no more separate `start_device_layer.py`" immediately. +2. Defer‑init boot refactor + the **agent toggle** + persistence + "Advanced options". + +## Open questions to fold in (this is initial work) + +This is a starting point — the following must be worked through as it grows: + +- **Background startup.** After "Let's go", the device‑layer startup should run in the + **background (non‑blocking)** while gently proceeds to the dashboard — not block the gate + until the device connects. (To confirm + design the progress signalling.) +- **Usable during startup.** gently must stay **usable while the device layer boots** — + software features (planning, review, analysis) available immediately; hardware‑dependent + actions gated behind a "device starting…" state until connected. +- **On‑demand startup‑sequence screen.** There must be a screen to watch the device + **startup sequence** on demand — a device console (in the Devices panel) streaming the + `start_device_layer` boot log + per‑stage connection progress. +- **Shutdown from that same screen.** That console is also the **shutdown control** — start + the sequence, watch it, and stop the device from one place. +- **Guided shutdown housekeeping.** Shutdown is a **sequence, not just a kill** — e.g. it + can prompt the operator to **reset the F drive** (post‑shutdown drive/housekeeping) as a + step in the flow. + +## Future direction — Windows desktop app (Electron) + +Bigger fold‑in to weigh: package the unified launcher as a **desktop app for Windows**, +potentially **Electron**, so gently is a double‑click application instead of terminal +commands. Electron would *be* the launcher shell — it owns and spawns the Python backend +(`launch_gently`) and the device‑layer child, renders the existing web UI in a native +window (the launch gate is the first screen), and gets native process lifecycle +(children killed on quit), a tray/menu, and a real installer + auto‑update. + +Tradeoffs for later: bundling/shipping the Python environment (embeddable Python / +PyInstaller), app size, auto‑update, and keeping the **web UI the single source of truth** +(Electron stays a thin shell — no UI logic moves into it). A lighter native wrapper +(**pywebview** or **Tauri**) is the fallback if Electron's footprint is too heavy. This is +a packaging/architecture decision, not required for the launcher's first cut — but the +managed‑child process model here is exactly what an Electron shell would take over. + +## Decisions log (from brainstorming) + +- Launcher model: **web startup screen + runtime panel**. +- Boot flow: **show the gate every boot, remember last choices** (one click to proceed). +- Stop: **graceful SIGTERM + mid‑run guard + SIGKILL fallback**. +- Scope: **fold in gently's own options** — but as a **bare‑basic 2‑question gate** + (hardware, agent), with the rest behind Advanced/Settings. diff --git a/gently/app/device_supervisor.py b/gently/app/device_supervisor.py new file mode 100644 index 00000000..17b70a01 --- /dev/null +++ b/gently/app/device_supervisor.py @@ -0,0 +1,373 @@ +"""Managed child subprocess for the device layer. + +Today gently only connects to ``start_device_layer.py`` as an HTTP client, so +there is no in-app way to bring hardware up or down — you run it yourself in a +second terminal. ``DeviceLayerSupervisor`` gives the gently process *ownership* +of that child: it spawns ``start_device_layer.py``, holds the handle, tails its +console output, reports liveness, and stops it (gracefully, then forcibly). That +ownership is what makes start/stop-from-UI and no-orphans-on-exit possible. + +Design: ``docs/superpowers/specs/2026-07-02-unified-launcher-design.md`` (RFC #78). + +Scope of this scaffold +---------------------- +- Real, working spawn / stop / status / log-tail + atexit cleanup. +- **External device layer is respected:** if something is already listening on + the port, we surface it as ``external`` and never try to manage (or stop) what + we did not start. + +Deliberately deferred (RFC "open questions"): +- Background/non-blocking startup with per-stage boot progress. +- Truly graceful shutdown on Windows — that needs ``start_device_layer.py`` to + handle ``SIGBREAK`` (see ``stop`` below). Until then Windows falls back to a + hard terminate after the grace period. +""" + +from __future__ import annotations + +import atexit +import json +import logging +import os +import signal +import socket +import subprocess +import sys +import threading +import time +from collections import deque +from pathlib import Path + +from gently.settings import settings + +logger = logging.getLogger(__name__) + +# Repo root: gently/app/device_supervisor.py -> parents[2] +_PROJECT_ROOT = Path(__file__).resolve().parents[2] +_DEVICE_SCRIPT = _PROJECT_ROOT / "start_device_layer.py" + +# How many console lines to retain for the log-tail shown in the Devices panel. +_LOG_TAIL_LINES = 500 +# Grace period (seconds) for a clean shutdown before we hard-kill. +_STOP_GRACE_SECONDS = 5.0 + + +class DeviceLayerSupervisor: + """Own the ``start_device_layer.py`` child process for one gently instance. + + Thread-safe: ``start``/``stop`` take an internal lock so overlapping calls + from async route handlers can't race on the process handle. A single reader + thread drains the child's stdout into a bounded ring buffer for the tail. + """ + + def __init__( + self, + port: int = settings.network.device_port, + host: str = settings.network.device_host, + sam_device: str = "cuda", + config_path: str = "config/config.yml", + ): + self.port = port + self.host = host + self.sam_device = sam_device + self.config_path = config_path + + self._proc: subprocess.Popen | None = None + self._lock = threading.Lock() + self._log: deque[str] = deque(maxlen=_LOG_TAIL_LINES) + self._reader: threading.Thread | None = None + self._started_at: float | None = None + # True once *we* asked the child to stop, so a resulting non-zero exit + # reads as "stopped" rather than "crashed". + self._stopping = False + + # Latest structured startup-progress / failure events parsed from the + # child's stdout (@@GENTLY_PROGRESS@@ lines). Single dict refs, swapped + # atomically by the reader thread and read without the lock (GIL). + self._progress: dict | None = None + self._failure: dict | None = None + + atexit.register(self._atexit_cleanup) + + # ── lifecycle ──────────────────────────────────────────────────────── + + def start(self, *, sam_device: str | None = None, config_path: str | None = None) -> dict: + """Spawn the device layer child if it isn't already up. + + Idempotent: if we already own a live child, returns its status. If an + *external* (unmanaged) device layer is already listening on the port, + refuses to start a duplicate and returns the external status. + """ + with self._lock: + if self._alive(): + assert self._proc is not None # _alive() guarantees this + logger.debug("Device layer already managed (pid=%s)", self._proc.pid) + return self._status_locked() + + if self._port_open(): + # Something is already listening we didn't start — don't stomp it. + logger.info( + "Device layer already running externally on %s:%s — not spawning a child", + self.host, + self.port, + ) + return self._status_locked() + + # GENTLY_DEVICE_LAYER_SCRIPT overrides the launcher — lets an operator + # point at an alternate device-layer entry point (and lets tests point + # at a harmless stand-in instead of touching real hardware). + script = Path(os.environ.get("GENTLY_DEVICE_LAYER_SCRIPT") or _DEVICE_SCRIPT) + if not script.exists(): + raise FileNotFoundError(f"device layer script not found: {script}") + + if sam_device is not None: + self.sam_device = sam_device + if config_path is not None: + self.config_path = config_path + + # -u = unbuffered stdout: CPython block-buffers stdout when it's a + # pipe, which would stall the [N/5] progress + @@GENTLY_PROGRESS@@ + # lines in the child until the buffer fills. Unbuffered makes the + # boot readout live. + cmd = [ + sys.executable, + "-u", + str(script), + "--port", + str(self.port), + "--sam-device", + self.sam_device, + "--config", + self.config_path, + ] + + # A new process group on Windows is what later lets us send a + # CTRL_BREAK for a (best-effort) clean shutdown instead of only a + # hard TerminateProcess. CREATE_NO_WINDOW keeps the device layer from + # popping its own console window when gently is launched as a desktop + # app — its output is still captured via the stdout pipe (below) and + # its own device_layer_*.log. + creationflags = 0 + if sys.platform == "win32": + creationflags = ( + subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore[attr-defined] + | subprocess.CREATE_NO_WINDOW # type: ignore[attr-defined] + ) + + logger.info("Spawning device layer: %s", " ".join(cmd)) + self._log.clear() + self._progress = None + self._failure = None + self._stopping = False + self._proc = subprocess.Popen( + cmd, + cwd=str(_PROJECT_ROOT), + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + encoding="utf-8", + errors="replace", + bufsize=1, + creationflags=creationflags, + ) + self._started_at = time.monotonic() + + self._reader = threading.Thread( + target=self._drain_output, + args=(self._proc,), + name="device-layer-log", + daemon=True, + ) + self._reader.start() + + return self._status_locked() + + def stop(self, *, force: bool = False, timeout: float = _STOP_GRACE_SECONDS) -> dict: + """Stop the managed child. Graceful first, hard-kill fallback. + + We only stop a child *we* spawned — an external device layer is left + alone (you can't cleanly stop what you don't own). + + ``force=True`` skips the grace period and hard-kills immediately. + """ + with self._lock: + if not self._alive(): + # Nothing of ours to stop (may still be external — status says so). + return self._status_locked() + + self._stopping = True + proc = self._proc + assert proc is not None + + if force: + self._hard_kill(proc) + return self._status_locked() + + # Graceful request. + try: + if sys.platform == "win32": + # Best-effort clean stop. NOTE: start_device_layer.py handles + # SIGINT/SIGTERM but not SIGBREAK, so today this usually falls + # through to the hard-kill below. Teaching the device layer to + # handle SIGBREAK (RFC follow-up) makes this a clean shutdown. + proc.send_signal(signal.CTRL_BREAK_EVENT) # type: ignore[attr-defined] + else: + proc.terminate() # SIGTERM -> device layer's clean shutdown path + except (ProcessLookupError, OSError) as e: + logger.debug("Signal to device layer failed (already exiting?): %s", e) + + try: + proc.wait(timeout=timeout) + logger.info("Device layer stopped cleanly (rc=%s)", proc.returncode) + except subprocess.TimeoutExpired: + logger.warning( + "Device layer didn't exit within %.1fs — forcing termination", timeout + ) + self._hard_kill(proc) + + return self._status_locked() + + def restart(self, **kwargs) -> dict: + """Stop (if running) then start — convenience for the Devices panel.""" + self.stop() + return self.start(**kwargs) + + # ── introspection ──────────────────────────────────────────────────── + + def status(self) -> dict: + """Current state of the device layer (see ``_status_locked``).""" + with self._lock: + return self._status_locked() + + def log_tail(self, limit: int = 200) -> list[str]: + """The most recent captured console lines (oldest → newest).""" + lines = list(self._log) + return lines[-limit:] if limit else lines + + # ── internals ──────────────────────────────────────────────────────── + + def _status_locked(self) -> dict: + """Compute status. Caller must hold ``_lock``. + + state (our managed child, in lifecycle order): + starting — spawned, no progress events yet, HTTP port not up + initializing — [N/5] progress flowing, port not up yet (carries progress) + ready — child alive AND port open (the server binds only after + initialize() finishes, so an open port == init done) + failed — child exited non-zero and we captured a structured + failure reason (carries failure summary + hints) + crashed — child exited non-zero with no diagnosis (unexpected) + external — port listening but not our child + stopped — nothing running (never started, or cleanly stopped) + """ + managed_alive = self._alive() + port_open = self._port_open() + progress = self._progress # atomic ref read (no lock needed) + failure = self._failure + + if managed_alive: + if port_open: + state = "ready" + elif progress is not None: + state = "initializing" + else: + state = "starting" + elif ( + self._proc is not None and self._proc.returncode not in (None, 0) and not self._stopping + ): + state = "failed" if failure else "crashed" + elif port_open: + state = "external" + else: + state = "stopped" + + pid = self._proc.pid if (self._proc and managed_alive) else None + rc = self._proc.returncode if self._proc else None + uptime = ( + round(time.monotonic() - self._started_at, 1) + if (managed_alive and self._started_at) + else None + ) + + return { + "state": state, + "managed": managed_alive, + "pid": pid, + "returncode": rc, + "host": self.host, + "port": self.port, + "port_open": port_open, + "sam_device": self.sam_device, + "uptime_seconds": uptime, + "progress": progress, + "failure": failure, + "log_tail": self.log_tail(50), + } + + def _alive(self) -> bool: + """True if we own a child that is still running.""" + return self._proc is not None and self._proc.poll() is None + + def _port_open(self, timeout: float = 0.25) -> bool: + """True if anything is accepting connections on the device port. + + Used to detect an externally-started device layer. ``host`` may be a + bind address like ``0.0.0.0``; probe loopback for that case. + """ + probe_host = "127.0.0.1" if self.host in ("0.0.0.0", "", None) else self.host + try: + with socket.create_connection((probe_host, self.port), timeout=timeout): + return True + except OSError: + return False + + def _hard_kill(self, proc: subprocess.Popen) -> None: + """Terminate a process forcibly and reap it. Caller holds ``_lock``.""" + try: + proc.kill() + proc.wait(timeout=3.0) + except subprocess.TimeoutExpired: + logger.error("Device layer pid=%s would not die after kill()", proc.pid) + except (ProcessLookupError, OSError): + pass + + # Sentinel prefix for machine-readable startup events on the child's stdout + # (emitted by gently.hardware.console_ui.progress_event). + _PROGRESS_PREFIX = "@@GENTLY_PROGRESS@@ " + + def _drain_output(self, proc: subprocess.Popen) -> None: + """Reader-thread body: copy child stdout into the ring buffer, peeling + off structured @@GENTLY_PROGRESS@@ events into _progress / _failure.""" + stream = proc.stdout + if stream is None: + return + try: + for raw in iter(stream.readline, ""): + line = raw.rstrip("\n") + if line.startswith(self._PROGRESS_PREFIX): + try: + ev = json.loads(line[len(self._PROGRESS_PREFIX) :]) + except ValueError: + self._log.append(line) # malformed — keep it visible + continue + # Atomic single-ref swaps; read lock-free in _status_locked. + if ev.get("status") == "failed": + self._failure = ev + else: + self._progress = ev + continue # keep the sentinel out of the human-visible tail + self._log.append(line) + except (ValueError, OSError): + # Stream closed underneath us during shutdown — fine. + pass + + def _atexit_cleanup(self) -> None: + """Kill an orphaned child when the gently process exits.""" + proc = self._proc + if proc is not None and proc.poll() is None: + logger.info("atexit: terminating managed device layer (pid=%s)", proc.pid) + self._stopping = True + try: + proc.kill() + except (ProcessLookupError, OSError): + pass diff --git a/gently/hardware/console_ui.py b/gently/hardware/console_ui.py index 60e8dfff..e99e3384 100644 --- a/gently/hardware/console_ui.py +++ b/gently/hardware/console_ui.py @@ -23,6 +23,7 @@ from __future__ import annotations +import json import os import sys @@ -145,14 +146,45 @@ def sub(label: str, value: str, label_w: int = 10) -> None: out(f" {c(label.ljust(label_w), 'grey')}{value}") +_last_step: tuple[int, int, str] | None = None + + +def progress_event(**payload) -> None: + """Emit one machine-readable startup-progress event on stdout. + + The DeviceLayerSupervisor drains the device layer's stdout pipe — the only + channel that crosses the process boundary before the device layer's HTTP + port opens — and parses these ``@@GENTLY_PROGRESS@@`` lines into a per-stage + progress readout for the UI. No-op on an interactive terminal (isatty), so a + human running the device layer directly never sees the sentinel noise; it + only fires when stdout is captured (piped) by the supervisor. + """ + try: + if sys.stdout.isatty(): + return + except (AttributeError, ValueError): + return + payload.setdefault("v", 1) + try: + print("@@GENTLY_PROGRESS@@ " + json.dumps(payload), flush=True) + except Exception: + pass + + def step(n: int, total: int, label: str) -> None: """A startup progress line: `` [2/5] Starting Micro-Manager core``""" + global _last_step + _last_step = (n, total, label) out(f" {c(f'[{n}/{total}]', 'cyan')} {label}") + progress_event(i=n, n=total, status="start", label=label) def step_done(detail: str = "ok") -> None: """A check-mark continuation under the most recent step.""" out(f" {c(_CHECK, 'green')} {c(detail, 'grey')}") + if _last_step is not None: + i, total, label = _last_step + progress_event(i=i, n=total, status="ok", label=label, detail=detail) def note(text: str, style: str = "grey") -> None: diff --git a/gently/ui/web/launch_prefs.py b/gently/ui/web/launch_prefs.py new file mode 100644 index 00000000..d6acfee4 --- /dev/null +++ b/gently/ui/web/launch_prefs.py @@ -0,0 +1,130 @@ +"""Persistence for the launch gate's two toggles (+ advanced defaults). + +The launch gate answers exactly two questions — *microscope hardware on/off* and +*AI agent on/off* — and everything else (device port, SAM device) is a remembered +default behind "Advanced options". Those choices live in +``config/launch.local.json`` (gitignored) so the gate is prefilled every boot. + +See ``docs/superpowers/specs/2026-07-02-unified-launcher-design.md`` (RFC #78). +""" + +from __future__ import annotations + +import json +import logging +from pathlib import Path + +from gently.settings import settings + +logger = logging.getLogger(__name__) + +# Repo root: gently/ui/web/launch_prefs.py -> parents[3] +_CONFIG_DIR = Path(__file__).resolve().parents[3] / "config" +PREFS_PATH = _CONFIG_DIR / "launch.local.json" + +# Only these keys are read from / written to disk. Advanced values fall back to +# settings so a fresh install has sensible defaults without a prefs file. +_DEFAULTS: dict = { + "hardware": True, # start + connect the device layer + "agent": True, # enable chat / perception / planning (needs API key) + "port": settings.network.device_port, + # "auto" is resolved to cuda/cpu at load time by GPU auto-detection — a + # biologist should never have to choose (RFC #78). Only a scope wrangler + # pins a concrete value (in Settings). + "sam_device": "auto", +} +_ALLOWED_KEYS = set(_DEFAULTS) + +_sam_device_cache: str | None = None + + +def detect_sam_device() -> str: + """Auto-detect the SAM inference device: ``cuda`` if an NVIDIA GPU is present, + else ``cpu``. Cached — detection is cheap once torch is loaded.""" + global _sam_device_cache + if _sam_device_cache is not None: + return _sam_device_cache + dev = "cpu" + try: + import torch + + if torch.cuda.is_available(): + dev = "cuda" + except Exception: + # torch missing/failed — fall back to a plain nvidia-smi presence check. + import shutil + import subprocess + + if shutil.which("nvidia-smi"): + try: + subprocess.run(["nvidia-smi"], capture_output=True, timeout=4, check=True) + dev = "cuda" + except Exception: + pass + _sam_device_cache = dev + logger.info("SAM device auto-detected: %s", dev) + return dev + + +def stored_prefs() -> dict: + """The raw persisted prefs (unresolved), or {} — for the Settings UI which + needs to show 'auto' vs a pinned SAM device.""" + try: + if PREFS_PATH.exists(): + data = json.loads(PREFS_PATH.read_text(encoding="utf-8")) + if isinstance(data, dict): + return data + except (OSError, ValueError): + pass + return {} + + +def load_prefs() -> dict: + """Return the persisted launch choices merged over defaults. + + Never raises — a missing or corrupt file just yields the defaults, so the + gate always renders. + """ + prefs = dict(_DEFAULTS) + try: + if PREFS_PATH.exists(): + stored = json.loads(PREFS_PATH.read_text(encoding="utf-8")) + if isinstance(stored, dict): + prefs.update({k: stored[k] for k in _ALLOWED_KEYS if k in stored}) + except (OSError, ValueError) as e: + logger.warning("launch prefs unreadable (%s) — using defaults", e) + # Resolve the SAM device unless a concrete value was pinned in Settings. + if prefs.get("sam_device") in (None, "", "auto"): + prefs["sam_device"] = detect_sam_device() + return prefs + + +def save_prefs(prefs: dict) -> dict: + """Persist a (partial) set of launch choices; returns the merged result. + + Unknown keys are ignored; known keys are coerced to the expected types. + """ + merged = load_prefs() + for key in _ALLOWED_KEYS & set(prefs): + merged[key] = _coerce(key, prefs[key]) + try: + _CONFIG_DIR.mkdir(parents=True, exist_ok=True) + PREFS_PATH.write_text(json.dumps(merged, indent=2), encoding="utf-8") + except OSError as e: + logger.error("could not persist launch prefs: %s", e) + return merged + + +def _coerce(key: str, value): + """Coerce a JSON-supplied value to the type its default implies.""" + default = _DEFAULTS[key] + if isinstance(default, bool): + if isinstance(value, str): + return value.strip().lower() in ("1", "true", "yes", "on") + return bool(value) + if isinstance(default, int): + try: + return int(value) + except (TypeError, ValueError): + return default + return value diff --git a/gently/ui/web/routes/__init__.py b/gently/ui/web/routes/__init__.py index a881a0bb..ef939435 100644 --- a/gently/ui/web/routes/__init__.py +++ b/gently/ui/web/routes/__init__.py @@ -12,6 +12,7 @@ from .chat import create_router as create_chat_router from .context import create_router as create_context_router from .data import create_router as create_data_router +from .device_layer import create_router as create_device_layer_router from .experiments import create_router as create_experiments_router from .images import create_router as create_images_router from .notebook import create_router as create_notebook_router @@ -30,6 +31,7 @@ def register_all_routes(server): for factory in ( create_pages_router, create_auth_router, + create_device_layer_router, create_sessions_router, create_campaigns_router, create_experiments_router, diff --git a/gently/ui/web/routes/device_layer.py b/gently/ui/web/routes/device_layer.py new file mode 100644 index 00000000..86d20f6e --- /dev/null +++ b/gently/ui/web/routes/device_layer.py @@ -0,0 +1,176 @@ +"""Device-layer supervision + launch gate routes. + +Two surfaces, both backed by ``DeviceLayerSupervisor``: + +- **Launch gate** (``GET /launch``) — the bare two-question start screen + (hardware on/off, agent on/off) with its persisted choices. +- **Devices panel** (``/api/device-layer/*``) — runtime status, log tail, and + Start / Stop controls that mirror the gate's hardware block. + +Control-mutating endpoints (start/stop) are gated behind ``require_control``, +matching the rest of the hardware routes. Stop reuses the 409 + ``"blocked"`` +mid-run guard pattern: if hardware looks active, the caller must confirm. + +See ``docs/superpowers/specs/2026-07-02-unified-launcher-design.md`` (RFC #78). +""" + +from __future__ import annotations + +import logging + +from fastapi import APIRouter, Depends, Request +from fastapi.responses import HTMLResponse, JSONResponse + +from gently.ui.web.auth import require_control +from gently.ui.web.launch_prefs import detect_sam_device, load_prefs, save_prefs, stored_prefs + +logger = logging.getLogger(__name__) + + +def get_supervisor(server): + """Return the server's DeviceLayerSupervisor, creating one on first use. + + ``launch_gently`` attaches a supervisor to the viz server at boot, but we + lazily construct one if it's absent so these routes work standalone (and so + the panel degrades gracefully rather than 500-ing). + """ + sup = getattr(server, "device_supervisor", None) + if sup is None: + from gently.app.device_supervisor import DeviceLayerSupervisor + + sup = DeviceLayerSupervisor() + server.device_supervisor = sup + return sup + + +def _hardware_active(server) -> bool: + """Best-effort: is an acquisition / timelapse currently running? + + Used by the stop guard. Conservative — only reports active when we can see a + clearly-running timelapse; unknown state reads as inactive so a stuck flag + can't wedge the Stop button. + """ + tracker = getattr(server, "timelapse_tracker", None) + status = getattr(tracker, "status", None) + return str(status).lower() in ("running", "acquiring", "active") + + +def create_router(server) -> APIRouter: + router = APIRouter() + + # ── Launch gate ────────────────────────────────────────────────────── + + @router.get("/launch", response_class=HTMLResponse) + async def launch_gate(request: Request): + """The bare two-question launch screen (prefilled from last choice).""" + return server.templates.TemplateResponse(request, "launch.html", {}) + + @router.get("/api/launch/prefs") + async def get_launch_prefs(): + """Persisted launch choices (hardware/agent toggles + advanced defaults). + + Adds `sam_detected` (auto-detected GPU/CPU) and `sam_device_raw` (the + stored 'auto'/'cuda'/'cpu' before resolution) for the Settings UI. + """ + prefs = load_prefs() + prefs["sam_detected"] = detect_sam_device() + prefs["sam_device_raw"] = stored_prefs().get("sam_device", "auto") + return prefs + + @router.post("/api/launch/prefs", dependencies=[Depends(require_control)]) + async def set_launch_prefs(request: Request): + """Persist launch choices; returns the merged result.""" + body = await request.json() + return save_prefs(body if isinstance(body, dict) else {}) + + @router.post("/api/launch/go", dependencies=[Depends(require_control)]) + async def launch_go(request: Request): + """Submit the launch gate: persist the toggles, mark the gate passed + (so / stops bouncing to /launch), and — if the microscope toggle is on — + start the device layer. + + Returns fast: start() only *spawns* the child. MMCore init and its + per-stage progress happen in the background and are polled via + /api/device-layer/status, so the UI can hand off to the dashboard + immediately and follow the boot there. + """ + body = await _safe_json(request) + prefs = save_prefs(body if isinstance(body, dict) else {}) + server.gate_passed = True + device = None + if prefs.get("hardware"): + try: + device = get_supervisor(server).start() + except (FileNotFoundError, OSError) as e: + logger.error("device layer start failed from launch gate: %s", e) + return {"ok": True, "hardware": True, "error": str(e)} + return { + "ok": True, + "hardware": bool(prefs.get("hardware")), + "agent": bool(prefs.get("agent")), + "device": device, + } + + # ── Devices panel: device-layer supervision ────────────────────────── + + @router.get("/api/device-layer/status") + async def device_layer_status(): + """Live status: running / stopped / external / crashed (+ short log tail).""" + return get_supervisor(server).status() + + @router.get("/api/device-layer/log") + async def device_layer_log(limit: int = 200): + """Recent captured console output (oldest → newest) for the console view.""" + return {"lines": get_supervisor(server).log_tail(limit)} + + @router.post("/api/device-layer/start", dependencies=[Depends(require_control)]) + async def device_layer_start(request: Request): + """Spawn (or adopt-if-external) the device layer per the request body. + + Body (all optional): ``{"sam_device": "cuda"|"cpu", "config_path": "..."}``. + """ + body = await _safe_json(request) + sup = get_supervisor(server) + try: + status = sup.start( + sam_device=body.get("sam_device"), + config_path=body.get("config_path"), + ) + except (FileNotFoundError, OSError) as e: + logger.error("device layer start failed: %s", e) + return JSONResponse({"error": str(e)}, status_code=500) + return status + + @router.post("/api/device-layer/stop", dependencies=[Depends(require_control)]) + async def device_layer_stop(request: Request): + """Stop the managed device layer, with a mid-run confirmation guard. + + If hardware looks active and the body doesn't carry ``{"confirm": true}``, + returns 409 ``{"blocked": true, ...}`` instead of stopping (same shape the + thermalizer stop-guard uses). ``{"force": true}`` skips the grace period. + """ + body = await _safe_json(request) + sup = get_supervisor(server) + + if _hardware_active(server) and not body.get("confirm"): + return JSONResponse( + { + "blocked": True, + "reason": "hardware is active — an acquisition is running", + "hint": 'resend with {"confirm": true} to stop anyway', + }, + status_code=409, + ) + + return sup.stop(force=bool(body.get("force"))) + + return router + + +async def _safe_json(request: Request) -> dict: + """Parse a JSON body, tolerating an empty/absent one (returns {}).""" + try: + body = await request.json() + except (ValueError, TypeError): + return {} + return body if isinstance(body, dict) else {} diff --git a/gently/ui/web/routes/pages.py b/gently/ui/web/routes/pages.py index 04a1a959..b3125add 100644 --- a/gently/ui/web/routes/pages.py +++ b/gently/ui/web/routes/pages.py @@ -16,7 +16,12 @@ async def index(request: Request): Viewing is open to everyone — the dashboard loads in view mode with no login. Signing in is an *elevation* to control (handled in-app via the chat window's "Sign in" affordance), not a gate on the page itself. + + The launch gate is the entry point: until it's submitted this session, + every visit to / bounces to /launch (RFC #78 defer-init boot). """ + if not getattr(server, "gate_passed", False): + return RedirectResponse("/launch", status_code=302) return server.templates.TemplateResponse( request, "index.html", diff --git a/gently/ui/web/server.py b/gently/ui/web/server.py index aa36ea0c..c96be89b 100644 --- a/gently/ui/web/server.py +++ b/gently/ui/web/server.py @@ -164,6 +164,10 @@ def __init__( # Wired in by launch_gently after construction (optional subsystems). self.agent_bridge: Any = None self.mesh_service: Any = None + self.device_supervisor: Any = None # DeviceLayerSupervisor (RFC #78) + # False until the launch gate is submitted this session; while False, / + # bounces to /launch so the gate is the entry point (RFC #78). + self.gate_passed: bool = False # Connection manager for WebSocket clients self.manager = ConnectionManager() diff --git a/gently/ui/web/static/css/boot-banner.css b/gently/ui/web/static/css/boot-banner.css new file mode 100644 index 00000000..fb9ef465 --- /dev/null +++ b/gently/ui/web/static/css/boot-banner.css @@ -0,0 +1,78 @@ +/* Device-layer boot banner (boot-banner.js) - non-modal, follows MMCore boot. + A compact bottom-center pill; the Devices panel is the full on-demand console. */ +.boot-banner { + position: fixed; + left: 50%; + bottom: 22px; + transform: translateX(-50%); + z-index: 10020; + display: flex; + align-items: center; + gap: 0.6rem; + max-width: min(560px, 92vw); + padding: 0.55rem 0.7rem 0.55rem 0.9rem; + border: 1px solid var(--border); + border-radius: 999px; + background: var(--bg-card); + color: var(--text); + box-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.45); + font-size: 0.85rem; +} +.boot-banner-dot { + width: 9px; + height: 9px; + border-radius: 50%; + flex: none; + background: var(--accent); +} +.boot-banner.booting .boot-banner-dot { + animation: boot-pulse 1.1s ease-in-out infinite; +} +@keyframes boot-pulse { + 0%, 100% { opacity: 0.35; } + 50% { opacity: 1; } +} +.boot-banner.ready { + border-color: var(--accent-green); +} +.boot-banner.ready .boot-banner-dot { + background: var(--accent-green); + animation: none; +} +.boot-banner.failed { + border-color: var(--accent-orange); +} +.boot-banner.failed .boot-banner-dot { + background: var(--accent-orange); + animation: none; +} +.boot-banner-text { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.boot-banner-btn { + flex: none; + border: 1px solid var(--border); + background: var(--bg-hover); + color: var(--text); + border-radius: 8px; + padding: 0.25rem 0.6rem; + font-size: 0.78rem; + cursor: pointer; +} +.boot-banner-btn:hover:not(:disabled) { background: var(--bg-card); } +.boot-banner-btn:disabled { opacity: 0.5; cursor: default; } +.boot-banner-x { + flex: none; + border: 0; + background: none; + color: var(--text-muted); + font-size: 1.1rem; + line-height: 1; + cursor: pointer; + padding: 0 0.15rem; +} +.boot-banner-x:hover { color: var(--text); } diff --git a/gently/ui/web/static/css/main.css b/gently/ui/web/static/css/main.css index 2c7751aa..01115260 100644 --- a/gently/ui/web/static/css/main.css +++ b/gently/ui/web/static/css/main.css @@ -9427,6 +9427,54 @@ body.modal-open { .devices-status-pill.stale { background: rgba(210, 153, 34, 0.2); color: var(--accent-orange, #d29922); } .devices-status-pill.error { background: rgba(248, 81, 73, 0.2); color: #f85149; } +/* Device-layer supervision card (device-layer.js) */ +.devices-layer-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 6px; + padding: 0.6rem 0.9rem; + margin: 0.75rem 0 0.25rem; +} +.devices-layer-head { + display: flex; + align-items: center; + gap: 0.6rem; + flex-wrap: wrap; +} +.devices-layer-head .devices-card-title { margin-bottom: 0; } +.devices-layer-meta { + font-family: 'SF Mono', 'Consolas', monospace; + font-size: 0.72rem; + color: var(--text-muted); + font-variant-numeric: tabular-nums; +} +.devices-layer-hint { + font-size: 0.72rem; + color: var(--text-muted); +} +.devices-layer-actions { + display: flex; + align-items: center; + gap: 0.4rem; + margin-top: 0.55rem; + flex-wrap: wrap; +} +.devices-layer-log { + margin: 0.55rem 0 0; + padding: 0.5rem 0.65rem; + max-height: 220px; + overflow: auto; + background: var(--bg-hover); + border: 1px solid var(--border); + border-radius: 5px; + font-family: 'SF Mono', 'Consolas', monospace; + font-size: 0.7rem; + line-height: 1.5; + color: var(--text-muted); + white-space: pre-wrap; + word-break: break-word; +} + .devices-positions { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); diff --git a/gently/ui/web/static/js/boot-banner.js b/gently/ui/web/static/js/boot-banner.js new file mode 100644 index 00000000..419b02f7 --- /dev/null +++ b/gently/ui/web/static/js/boot-banner.js @@ -0,0 +1,149 @@ +/** + * Boot banner — a small, always-visible, non-modal summary of the device-layer + * boot, so the operator can follow MMCore startup from anywhere on the dashboard + * (the Devices panel is the on-demand console; this is its compact form). + * + * Polls /api/device-layer/status globally — fast (1s) while booting, calm (6s) + * otherwise so it also catches a device layer started later from the Devices + * panel. Also publishes a readiness signal (window.gentlyDeviceReady + + * ClientEventBus 'DEVICE_LAYER_STATE') that hardware-only controls can gate on. + */ +const BootBanner = (function () { + const STAGE_TOTAL = 5; + + let _el, _text, _details, _retry, _close; + let _timer = null; + let _pollMs = 0; + let _lastState = null; + let _dom = false; + + function cacheDom() { + if (_dom) return; + _el = document.getElementById('gently-boot-banner'); + if (!_el) return; + _text = document.getElementById('boot-banner-text'); + _details = document.getElementById('boot-banner-details'); + _retry = document.getElementById('boot-banner-retry'); + _close = document.getElementById('boot-banner-close'); + _dom = true; + } + + function init() { + cacheDom(); + if (!_el) return; + _details.addEventListener('click', () => { + // The v2 landing overlay covers the workspace — dismiss it first, + // or the tab switch below happens invisibly behind it and the + // click feels dead (found via session replay: the operator + // clicked Details and saw nothing for over two minutes). + const landing = document.getElementById('v2-landing'); + if (landing && !landing.classList.contains('dismissed')) { + const skip = document.getElementById('v2-landing-skip'); + if (skip) skip.click(); else landing.classList.add('dismissed'); + } + if (typeof switchTab === 'function' && typeof TABS !== 'undefined') switchTab(TABS.DEVICES); + }); + _retry.addEventListener('click', onRetry); + _close.addEventListener('click', hide); + setPoll(1500); + } + + function setPoll(ms) { + if (ms === _pollMs && _timer) return; + _pollMs = ms; + if (_timer) clearInterval(_timer); + _timer = setInterval(poll, _pollMs); + } + + async function poll() { + try { + const r = await fetch('/api/device-layer/status'); + if (!r.ok) return; + render(await r.json()); + } catch (e) { + /* keep last-known UI */ + } + } + + function render(d) { + const state = d.state; + + // Publish a readiness signal for hardware-only controls to gate on. + const ready = state === 'ready' || state === 'external'; + window.gentlyDeviceReady = ready; + if (state !== _lastState && typeof ClientEventBus !== 'undefined') { + ClientEventBus.emit('DEVICE_LAYER_STATE', { state, ready }); + } + + if (state === 'starting' || state === 'initializing') { + show('booting'); + const p = d.progress || {}; + const step = p.i ? `step ${p.i}/${p.n || STAGE_TOTAL} · ` : ''; + _text.textContent = `Microscope warming up — ${step}${p.label || 'starting…'}`; + btns({ details: true, retry: false, close: false }); + setPoll(1000); + } else if (state === 'ready') { + setPoll(6000); + const justFinished = _lastState === 'starting' || _lastState === 'initializing'; + if (justFinished) { + // Flash "ready" briefly, then auto-dismiss. + show('ready'); + _text.textContent = 'Microscope ready'; + btns({ details: false, retry: false, close: false }); + setTimeout(() => { + if (_el.classList.contains('ready')) hide(); + }, 3500); + } else if (!(_el.classList.contains('ready') && !_el.hidden)) { + hide(); // already ready on load, or the ready-flash was dismissed + } + } else if (state === 'failed' || state === 'crashed') { + show('failed'); + _text.textContent = + (d.failure && d.failure.summary) || + (state === 'crashed' + ? 'The device layer stopped unexpectedly.' + : "The microscope didn't start."); + btns({ details: true, retry: true, close: true }); + setPoll(6000); + } else { + // stopped (software-only session) or external with nothing to add. + hide(); + setPoll(6000); + } + _lastState = state; + } + + function show(kind) { + _el.hidden = false; + _el.className = 'boot-banner ' + kind; + } + function hide() { + _el.hidden = true; + } + function btns({ details, retry, close }) { + _details.hidden = !details; + _retry.hidden = !retry; + _close.hidden = !close; + } + + async function onRetry() { + _retry.disabled = true; + try { + await fetch('/api/device-layer/start', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: '{}', + }); + } catch (e) { + /* the status poll will reflect the outcome */ + } finally { + _retry.disabled = false; + setTimeout(poll, 400); + poll(); + } + } + + return { init }; +})(); + +document.addEventListener('DOMContentLoaded', () => BootBanner.init()); diff --git a/gently/ui/web/static/js/device-layer.js b/gently/ui/web/static/js/device-layer.js new file mode 100644 index 00000000..6754d770 --- /dev/null +++ b/gently/ui/web/static/js/device-layer.js @@ -0,0 +1,271 @@ +/** + * Device-layer supervision card (Devices tab). + * + * The runtime mirror of the launch gate's hardware block: shows whether the + * device layer is running / stopped / external / crashed, lets an operator + * Start or Stop it from the UI, and tails its console. Consumes the routes + * added in gently/ui/web/routes/device_layer.py: + * + * GET /api/device-layer/status -> {state, managed, pid, port, port_open, + * sam_device, uptime_seconds, log_tail[]} + * GET /api/device-layer/log?limit=N -> {lines: [...]} + * POST /api/device-layer/start (control-gated) body {sam_device?, config_path?} + * POST /api/device-layer/stop (control-gated) body {confirm?, force?} + * -> 409 {blocked:true,...} mid-acquisition + * + * Self-contained IIFE (no devices.js internals). Polls only while the Devices + * tab is visible (ClientEventBus 'TAB_CHANGED'). Control auth is transparent: + * a same-origin fetch carries the gently_session cookie, and control-auth.js + * already toasts on a bare 403, so Start/Stop need no credential code. + */ +const DeviceLayerCard = (function () { + let _pollMs = 5000; // adaptive: 1s while booting, 5s otherwise + + // state -> [pill modifier class, label]. + const PILL = { + ready: ['live', 'ready'], + running: ['live', 'running'], // legacy alias + starting: ['paused', 'starting'], + initializing: ['paused', 'starting'], + external: ['paused', 'external'], + stopped: ['', 'stopped'], + crashed: ['error', 'crashed'], + failed: ['error', 'failed'], + }; + + let _card, _pill, _meta, _start, _stop, _logToggle, _log, _hint; + let _timer = null; + let _busy = false; // a start/stop is in flight + let _dom = false; + + function cacheDom() { + if (_dom) return; + _card = document.getElementById('devices-layer-card'); + if (!_card) return; // markup absent -> stay inert + _pill = document.getElementById('devices-layer-pill'); + _meta = document.getElementById('devices-layer-meta'); + _start = document.getElementById('devices-layer-start'); + _stop = document.getElementById('devices-layer-stop'); + _logToggle = document.getElementById('devices-layer-log-toggle'); + _log = document.getElementById('devices-layer-log'); + _hint = document.getElementById('devices-layer-hint'); + _dom = true; + } + + function init() { + cacheDom(); + if (!_card) return; + _start.addEventListener('click', onStart); + _stop.addEventListener('click', onStop); + _logToggle.addEventListener('click', toggleLog); + + // Poll only while the Devices tab is showing. switchTab() emits + // TAB_CHANGED with the *new* tab id (no leave signal), so compare + // against TABS.DEVICES to derive both enter and exit. + if (typeof ClientEventBus !== 'undefined') { + ClientEventBus.on('TAB_CHANGED', (tab) => { + if (tab === TABS.DEVICES) startPoll(); + else stopPoll(); + }); + } + // Seed if Devices is already the active tab (reload / deep link) — + // TAB_CHANGED does not fire for the landing tab. + const content = document.getElementById('devices-content'); + if (content && content.classList.contains('active')) startPoll(); + } + + // ── polling ────────────────────────────────────────────────────────── + + function startPoll() { + loadStatus(); // immediate + if (_timer) clearInterval(_timer); // clear-before-set + _timer = setInterval(loadStatus, _pollMs); + } + + function stopPoll() { + if (_timer) { clearInterval(_timer); _timer = null; } + } + + async function loadStatus() { + if (!_card) return; + try { + const res = await fetch('/api/device-layer/status'); + if (!res.ok) return; // keep last-known UI + apply(await res.json()); + } catch (e) { + console.debug('device-layer status poll failed', e); + } + } + + // ── render ─────────────────────────────────────────────────────────── + + function apply(d) { + _card.hidden = false; + const [mod, label] = PILL[d.state] || ['stale', d.state || 'unknown']; + _pill.className = 'devices-status-pill' + (mod ? ' ' + mod : ''); + _pill.textContent = label; + + const booting = d.state === 'starting' || d.state === 'initializing'; + + // Meta line: during boot show the current stage; otherwise pid/port/uptime. + if (booting && d.progress && d.progress.i && d.progress.n) { + _meta.textContent = + 'step ' + d.progress.i + '/' + d.progress.n + ' · ' + (d.progress.label || 'starting…'); + } else if (booting) { + _meta.textContent = 'starting…'; + } else { + const bits = []; + if (d.pid) bits.push('pid ' + d.pid); + if (d.port) bits.push('port ' + d.port + (d.port_open ? '' : ' · closed')); + if (d.sam_device) bits.push(d.sam_device); + if (typeof d.uptime_seconds === 'number') bits.push('up ' + fmtUptime(d.uptime_seconds)); + _meta.textContent = bits.join(' · '); + } + + // Hint line: failure reason, external note, or a reassurance during the + // slow MMCore step (step 2) so a long wait doesn't read as frozen. + if (d.state === 'failed' && d.failure) { + _hint.hidden = false; + const h = (d.failure.hints && d.failure.hints[0]) ? ' — ' + d.failure.hints[0] : ''; + _hint.textContent = (d.failure.summary || 'Startup failed') + h; + } else if (d.state === 'external') { + _hint.hidden = false; + _hint.textContent = 'running externally — not managed by gently'; + } else if (booting && d.progress && d.progress.i === 2) { + _hint.hidden = false; + _hint.textContent = 'Initializing Micro-Manager — this can take a minute.'; + } else { + _hint.hidden = true; + _hint.textContent = ''; + } + + // Enable/disable (a hint only; the server 403/409 is the real gate). + const canStart = d.state === 'stopped' || d.state === 'crashed' || d.state === 'failed'; + const canStop = + d.state === 'ready' || d.state === 'running' || booting; + _start.disabled = _busy || !canStart; + _stop.disabled = _busy || !canStop; + + // Adaptive cadence: poll fast (1s) while booting, calm (5s) otherwise. + const want = booting ? 1000 : 5000; + if (want !== _pollMs && _timer) { + _pollMs = want; + clearInterval(_timer); + _timer = setInterval(loadStatus, _pollMs); + } + + if (Array.isArray(d.log_tail) && d.log_tail.length && !_log.hidden) { + renderLog(d.log_tail); + } + } + + function fmtUptime(s) { + s = Math.max(0, Math.floor(s)); + if (s < 60) return s + 's'; + if (s < 3600) return Math.floor(s / 60) + 'm ' + (s % 60) + 's'; + return Math.floor(s / 3600) + 'h ' + Math.floor((s % 3600) / 60) + 'm'; + } + + // ── start / stop ───────────────────────────────────────────────────── + + async function onStart() { + if (_start.disabled) return; + _busy = true; + _start.disabled = true; + try { + await postJSON('/api/device-layer/start', {}); + toast('Device layer starting…'); + } catch (e) { + // 403 already surfaced by control-auth.js; don't double-toast. + if (e.status !== 403) toast('Start failed: ' + e.message); + } finally { + _busy = false; + // The layer takes a moment to bind its port; re-poll shortly. + setTimeout(loadStatus, 800); + loadStatus(); + } + } + + async function onStop() { + if (_stop.disabled) return; + if (!window.confirm('Stop the device layer?')) return; + _busy = true; + _stop.disabled = true; + try { + await postJSON('/api/device-layer/stop', { confirm: true }); + toast('Device layer stopping…'); + } catch (e) { + if (e.status === 409 && e.payload && e.payload.blocked) { + // Mid-acquisition soft block — offer an explicit force-stop. + if (window.confirm('A run is active. Force-stop the device layer anyway?')) { + try { + await postJSON('/api/device-layer/stop', { confirm: true, force: true }); + toast('Force-stopping…'); + } catch (e2) { + if (e2.status !== 403) toast('Force-stop failed: ' + e2.message); + } + } + } else if (e.status !== 403) { + toast('Stop failed: ' + e.message); + } + } finally { + _busy = false; + setTimeout(loadStatus, 500); + loadStatus(); + } + } + + // POST JSON; on !ok throw an Error carrying .status and parsed .payload so + // callers can branch on 409/403 (mirrors operate.js's postJSON convention). + async function postJSON(url, body) { + const res = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body || {}), + }); + if (!res.ok) { + let payload = {}; + try { payload = await res.json(); } catch (_) { /* non-JSON */ } + const err = new Error(payload.error || payload.reason || res.statusText); + err.status = res.status; + err.payload = payload; + throw err; + } + return res.json(); + } + + // ── log tail ───────────────────────────────────────────────────────── + + function toggleLog() { + const show = _log.hidden; + _log.hidden = !show; + _logToggle.setAttribute('aria-expanded', String(show)); + _logToggle.classList.toggle('active', show); + if (show) loadFullLog(); + } + + async function loadFullLog() { + try { + const res = await fetch('/api/device-layer/log?limit=200'); + if (!res.ok) return; + const d = await res.json(); + if (Array.isArray(d.lines)) renderLog(d.lines); + } catch (e) { + console.debug('device-layer log fetch failed', e); + } + } + + function renderLog(lines) { + if (!_log) return; + _log.textContent = lines.join('\n'); + _log.scrollTop = _log.scrollHeight; // pin to newest + } + + function toast(msg) { + if (typeof showGentlyToast === 'function') showGentlyToast(msg); + } + + return { init }; +})(); + +document.addEventListener('DOMContentLoaded', () => DeviceLayerCard.init()); diff --git a/gently/ui/web/templates/index.html b/gently/ui/web/templates/index.html index af177670..35374ae8 100644 --- a/gently/ui/web/templates/index.html +++ b/gently/ui/web/templates/index.html @@ -11,6 +11,7 @@ + @@ -484,6 +485,26 @@

Device + + +
Hardware
+ Device layer Thermalizer Effective config Advanced @@ -94,6 +95,31 @@

Alert Thresholds

+ +
+

Device layer

+

+ Install-time settings for the microscope control process. Most rigs never + change these — they're kept off the launch screen on purpose. +

+
+ +
+ +
+
+
+ +
+ + + +
+

+
+

+
+

Ambient Pulse

@@ -304,6 +330,49 @@

Advanced (restart require localStorage.setItem('gently-theme', next); }); } + + // Device-layer settings (port + SAM device) — persisted via /api/launch/prefs. + // Applies to the next device-layer start (a running one isn't reconfigured). + (function () { + const port = document.getElementById('dl-port'); + const detected = document.getElementById('dl-sam-detected'); + const status = document.getElementById('dl-status'); + if (!port) return; + let loaded = false; + fetch('/api/launch/prefs').then(r => (r.ok ? r.json() : null)).then(p => { + if (!p) return; + port.value = (p.port != null ? p.port : ''); + const sam = p.sam_device_raw || 'auto'; + const radio = document.querySelector('#dl-sam input[value="' + sam + '"]') + || document.querySelector('#dl-sam input[value="auto"]'); + if (radio) radio.checked = true; + if (detected) { + detected.textContent = 'Detected: ' + (p.sam_detected === 'cuda' + ? 'GPU (CUDA)' + : 'CPU — no GPU found, so image analysis will be slower'); + } + loaded = true; + }); + async function save() { + if (!loaded) return; + const sam = (document.querySelector('#dl-sam input:checked') || {}).value || 'auto'; + const body = { sam_device: sam }; + const pv = parseInt(port.value, 10); + if (pv) body.port = pv; + try { + const r = await fetch('/api/launch/prefs', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + if (status) status.textContent = r.ok + ? 'Saved — applies next time the device layer starts' + : (r.status === 403 ? 'Sign in to change device settings' : 'Failed to save'); + } catch (e) { if (status) status.textContent = 'Failed to save'; } + } + port.addEventListener('change', save); + document.querySelectorAll('#dl-sam input').forEach(r => r.addEventListener('change', save)); + })(); diff --git a/launch_gently.py b/launch_gently.py index 6afcbe86..d6930e5b 100644 --- a/launch_gently.py +++ b/launch_gently.py @@ -223,6 +223,74 @@ def run_ink_picker(tui_dist: Path, sessions_json: str) -> str | None: return None +# launch_gently runs as `__main__`, whose logger is not wired to the configured +# handlers — so operator-facing lifecycle events (reconnect, tool registration) +# use a `gently.*`-namespaced logger that reaches the console + file logs. +_reconnect_log = logging.getLogger("gently.app.launch") + + +async def _attach_microscope(client, store) -> int: + """Configure the device session + register microscope tools for a connected + client. Used both at boot and when reconnecting to a device layer that was + started from the launch gate / Devices panel. Returns the tool count.""" + try: + await client.configure_device_session(str(store.incoming_dir)) + logger.info("Device session configured: volume_dir=%s", store.incoming_dir) + except Exception as e: + logger.error("Failed to configure device session (volumes will be slow): %s", e) + try: + from gently.harness.microscope import register_microscope_tools + + n = register_microscope_tools(client) + if n: + logger.info("Registered %d microscope tools from device layer", n) + return n + except Exception as e: + logger.debug("Auto-tool registration skipped: %s", e) + return 0 + + +async def _reconnect_microscope_when_ready(client, viz_server, store) -> None: + """Background task (RFC #78 pragmatic reconnect): when a device layer started + from the launch gate (or the Devices panel) reaches 'ready', (re)connect the + agent's microscope client to it and register its tools — so hardware the gate + started becomes usable by the agent without a relaunch. Acts only while the + client is disconnected, and registers tools once.""" + if client is None: + return + # If already wired up at boot, the boot path registered tools already. + tools_registered = bool(client.is_connected) + while True: + await asyncio.sleep(2.0) + sup = getattr(viz_server, "device_supervisor", None) if viz_server else None + if sup is None: + continue + try: + # status() does a short socket probe — off the event loop. + state = (await asyncio.to_thread(sup.status)).get("state") + except Exception: + continue + if state != "ready": + continue + # Ensure the client is connected to the now-ready device layer. + if not client.is_connected: + try: + await client.disconnect() # drop any stale (failed-at-boot) session + except Exception: + pass + try: + await client.connect() + except Exception as e: + _reconnect_log.debug("Microscope reconnect attempt failed: %s", e) + # Register microscope tools once — whoever connected the client. + if client.is_connected and not tools_registered: + n = await _attach_microscope(client, store) + _reconnect_log.info( + "Microscope client reconnected to the device layer (%d tools registered)", n + ) + tools_registered = True + + async def main( offline: bool = False, resume_session: str | None = None, @@ -352,25 +420,12 @@ async def main( http_url, ) - # Configure device session for zero-copy volume transfer - if client and client.is_connected: - try: - incoming = str(store.incoming_dir) - await client.configure_device_session(incoming) - logger.info("Device session configured: volume_dir=%s", incoming) - except Exception as e: - logger.error("Failed to configure device session (volumes will be slow): %s", e) - - # Register auto-generated microscope tools from device layer plan schemas + # Configure the device session + register microscope tools if the client + # connected at boot (an already-running device layer). If the device layer is + # started later from the launch gate, _reconnect_microscope_when_ready does + # this once it reaches 'ready'. if client and client.is_connected: - try: - from gently.harness.microscope import register_microscope_tools - - n = register_microscope_tools(client) - if n: - logger.info("Registered %d microscope tools from device layer", n) - except Exception as e: - logger.debug("Auto-tool registration skipped: %s", e) + await _attach_microscope(client, store) # Create agent agent = MicroscopyAgent( @@ -552,6 +607,18 @@ def _status_provider(): if agent.viz_server is not None: agent.viz_server.agent_bridge = bridge agent.viz_server.set_context_store(context_store) + # Device-layer supervisor (managed child subprocess) — lets the launch + # gate and the Devices panel start/stop start_device_layer.py from the + # UI, and kills it on exit so it never orphans. It auto-detects an + # already-running (external) device layer and leaves it alone. RFC #78. + try: + from gently.app.device_supervisor import DeviceLayerSupervisor + + agent.viz_server.device_supervisor = DeviceLayerSupervisor( + port=settings.network.device_port, + ) + except Exception: + logger.debug("DeviceLayerSupervisor init skipped", exc_info=True) # If launched into an existing session, rehydrate its persisted # imagery so the galleries/filmstrips show data from the start. if session_to_resume: @@ -560,6 +627,11 @@ def _status_provider(): except Exception: logger.debug("Startup rehydrate failed", exc_info=True) + # Reconnect the microscope client if a device layer is started from the + # launch gate / Devices panel after boot (RFC #78 pragmatic reconnect). + if client is not None: + asyncio.create_task(_reconnect_microscope_when_ready(client, agent.viz_server, store)) + # ── Banner + serve ────────────────────────────────────────────── # The viz server runs in-process (uvicorn in a background task). With # the TUI retired, the launcher's job is to keep that server alive and @@ -633,6 +705,37 @@ def _sig(*_a): pass +def _serve(**main_kwargs) -> None: + """Run one backend lifetime — the reload child's entry point.""" + try: + asyncio.run(main(**main_kwargs)) + except (KeyboardInterrupt, RuntimeError, SystemExit): + pass + + +def _run_with_reload(main_kwargs: dict) -> None: + """Dev auto-restart: re-run the backend whenever a gently/*.py file changes. + + Uses watchfiles (the same watcher uvicorn --reload uses) to run the server in + a child process and restart it on any .py change under gently/ (or + launch_gently.py). This is a WHOLE-backend restart, not an in-place reload — + gently's app is built from runtime state (agent, store, mesh), so there is no + static import for uvicorn's native reloader to hot-swap. A running device + layer restarts too, so this is for UI / backend dev, not live hardware. + After a restart, refresh the browser / Tauri window (Ctrl+R) to see changes. + """ + from watchfiles import PythonFilter, run_process + + root = Path(__file__).resolve().parent + paths = [str(root / "gently"), str(root / "launch_gently.py")] + print( + "[reload] watching gently/ + launch_gently.py — edit a .py file and the " + "backend restarts (then refresh the page)", + flush=True, + ) + run_process(*paths, target=_serve, kwargs=main_kwargs, watch_filter=PythonFilter()) + + def cli_main(): """Sync entry point for ``gently`` console script (pyproject.toml).""" parser = argparse.ArgumentParser(description="Launch Microscopy Agent") @@ -665,10 +768,29 @@ def cli_main(): action="store_true", help="Do not auto-open the web UI in a browser", ) + parser.add_argument( + "--reload", + action="store_true", + help="Dev: auto-restart the backend when a gently/*.py file changes " + "(watchfiles). Restarts the whole backend — not for live hardware.", + ) args = parser.parse_args() + # Gate the agent + hardware by the launch gate's remembered choices (the gate + # persists them; they apply at boot). CLI flags can still force these OFF. + launch_no_api = args.no_api + launch_offline = args.offline + try: + from gently.ui.web.launch_prefs import load_prefs + + _lp = load_prefs() + launch_no_api = args.no_api or not _lp.get("agent", True) + launch_offline = args.offline or not _lp.get("hardware", True) + except Exception: + pass + # An API key is required unless running in UI-only mode. - if not args.no_api and not os.getenv("ANTHROPIC_API_KEY"): + if not launch_no_api and not os.getenv("ANTHROPIC_API_KEY"): print("Error: ANTHROPIC_API_KEY not set") if os.name == "nt": print("Set with: set ANTHROPIC_API_KEY=your-key") @@ -687,19 +809,25 @@ def cli_main(): pick_session = args.resume == "__PICK__" resume_id = args.resume if args.resume and args.resume != "__PICK__" else None + main_kwargs = dict( + offline=launch_offline, + show_sessions=args.sessions, + resume_session=resume_id, + pick_session=pick_session, + log_level=log_level, + no_browser=args.no_browser, + no_api=launch_no_api, + no_auth=args.no_auth, + ) + + # Dev: auto-restart the backend on gently/*.py changes (refresh the page to + # pick them up). Runs the server in a watchfiles-managed child process. + if args.reload: + _run_with_reload(main_kwargs) + return + try: - asyncio.run( - main( - offline=args.offline, - show_sessions=args.sessions, - resume_session=resume_id, - pick_session=pick_session, - log_level=log_level, - no_browser=args.no_browser, - no_api=args.no_api, - no_auth=args.no_auth, - ) - ) + asyncio.run(main(**main_kwargs)) except (KeyboardInterrupt, RuntimeError, SystemExit): pass diff --git a/start_device_layer.py b/start_device_layer.py index 9daae9c6..8d1eacfc 100644 --- a/start_device_layer.py +++ b/start_device_layer.py @@ -78,6 +78,16 @@ def _render_startup_failure(exc, log_file): "Then start the device layer again.", ] + # Structured failure event for the DeviceLayerSupervisor (rides the same + # stdout pipe as progress) so the UI can show a plain-language reason + hints + # instead of re-parsing the console panel. No-op on an interactive terminal. + cui.progress_event( + status="failed", + stage=(device or "startup"), + summary=summary, + detail=details, + hints=hints, + ) cui.error_panel("GENTLY DEVICE LAYER", summary, details, hints, log_file)