diff --git a/.github/workflows/desktop-ci.yml b/.github/workflows/desktop-ci.yml
new file mode 100644
index 0000000..8b6f47a
--- /dev/null
+++ b/.github/workflows/desktop-ci.yml
@@ -0,0 +1,88 @@
+name: desktop-ci
+
+on:
+ pull_request:
+ branches:
+ - main
+ - dev
+ paths:
+ - apps/desktop/**
+ - crates/**
+ - Cargo.toml
+ - Cargo.lock
+ - mise.toml
+ - .github/workflows/**
+ push:
+ branches:
+ - main
+ - dev
+ paths:
+ - apps/desktop/**
+ - crates/**
+ - Cargo.toml
+ - Cargo.lock
+ - mise.toml
+ - .github/workflows/**
+
+concurrency:
+ group: desktop-ci-${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ validate:
+ name: validate (${{ matrix.platform }})
+ runs-on: ${{ matrix.platform }}
+ strategy:
+ fail-fast: false
+ matrix:
+ platform:
+ - ubuntu-22.04
+ - windows-latest
+ defaults:
+ run:
+ shell: bash
+
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v6
+
+ - name: Set up pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 10.27.0
+ run_install: false
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: 22
+ cache: pnpm
+ cache-dependency-path: apps/desktop/pnpm-lock.yaml
+
+ - name: Set up Python
+ uses: actions/setup-python@v6
+ with:
+ python-version: "3.13"
+
+ - name: Set up Rust
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ toolchain: 1.92.0
+
+ - name: Install Linux system dependencies
+ if: matrix.platform == 'ubuntu-22.04'
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
+
+ - name: Install frontend dependencies
+ run: pnpm --dir apps/desktop install --frozen-lockfile
+
+ - name: Check Svelte and TypeScript
+ run: pnpm --dir apps/desktop check
+
+ - name: Check Rust workspace crate
+ run: cargo check --manifest-path apps/desktop/src-tauri/Cargo.toml
+
+ - name: Check integrated Tauri build
+ run: pnpm --dir apps/desktop tauri build --no-bundle --ci
diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml
new file mode 100644
index 0000000..359f15d
--- /dev/null
+++ b/.github/workflows/desktop-release.yml
@@ -0,0 +1,190 @@
+name: desktop-release
+
+on:
+ push:
+ tags:
+ - "v*"
+ workflow_dispatch:
+ inputs:
+ tag_name:
+ description: "Semver tag to release, for example v0.2.0"
+ required: true
+ type: string
+
+permissions:
+ contents: write
+
+concurrency:
+ group: desktop-release-${{ github.ref || inputs.tag_name }}
+ cancel-in-progress: false
+
+jobs:
+ preflight:
+ name: preflight
+ runs-on: ubuntu-22.04
+ defaults:
+ run:
+ shell: bash
+ outputs:
+ tag_name: ${{ steps.resolve_tag.outputs.tag_name }}
+ version: ${{ steps.validate.outputs.version }}
+
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+
+ - name: Set up Python
+ uses: actions/setup-python@v6
+ with:
+ python-version: "3.13"
+
+ - name: Resolve release tag
+ id: resolve_tag
+ env:
+ INPUT_TAG_NAME: ${{ inputs.tag_name }}
+ run: |
+ if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
+ tag_name="${GITHUB_REF_NAME}"
+ else
+ tag_name="${INPUT_TAG_NAME}"
+ fi
+
+ if [[ -z "${tag_name}" ]]; then
+ echo "Release tag is required." >&2
+ exit 1
+ fi
+
+ echo "tag_name=${tag_name}" >> "${GITHUB_OUTPUT}"
+
+ - name: Validate version lockstep and tag
+ id: validate
+ env:
+ RELEASE_TAG: ${{ steps.resolve_tag.outputs.tag_name }}
+ run: |
+ python - <<'PY'
+ import json
+ import os
+ import pathlib
+ import re
+ import sys
+ import tomllib
+
+ root = pathlib.Path(".")
+ release_tag = os.environ["RELEASE_TAG"]
+
+ if not re.fullmatch(r"v\d+\.\d+\.\d+", release_tag):
+ print(
+ f"Release tag '{release_tag}' is invalid. Expected format vX.Y.Z.",
+ file=sys.stderr,
+ )
+ sys.exit(1)
+
+ versions = {
+ "apps/desktop/package.json": json.loads(
+ (root / "apps/desktop/package.json").read_text(encoding="utf-8")
+ )["version"],
+ "apps/desktop/src-tauri/Cargo.toml": tomllib.loads(
+ (root / "apps/desktop/src-tauri/Cargo.toml").read_text(encoding="utf-8")
+ )["package"]["version"],
+ "apps/desktop/src-tauri/tauri.conf.json": json.loads(
+ (root / "apps/desktop/src-tauri/tauri.conf.json").read_text(
+ encoding="utf-8"
+ )
+ )["version"],
+ "crates/supervisor/Cargo.toml": tomllib.loads(
+ (root / "crates/supervisor/Cargo.toml").read_text(encoding="utf-8")
+ )["package"]["version"],
+ }
+
+ unique_versions = sorted(set(versions.values()))
+ if len(unique_versions) != 1:
+ print("Version mismatch detected across release manifests:", file=sys.stderr)
+ for path, version in versions.items():
+ print(f" - {path}: {version}", file=sys.stderr)
+ sys.exit(1)
+
+ version = unique_versions[0]
+ expected_tag = f"v{version}"
+ if release_tag != expected_tag:
+ print(
+ f"Tag '{release_tag}' does not match manifest version '{version}'. "
+ f"Expected '{expected_tag}'.",
+ file=sys.stderr,
+ )
+ sys.exit(1)
+
+ print(f"Validated release version {version} for tag {release_tag}.")
+ output_path = pathlib.Path(os.environ["GITHUB_OUTPUT"])
+ with output_path.open("a", encoding="utf-8") as output:
+ output.write(f"version={version}\n")
+ PY
+
+ publish:
+ name: publish (${{ matrix.platform }})
+ runs-on: ${{ matrix.platform }}
+ needs: preflight
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - platform: ubuntu-22.04
+ tauri_args: --ci --no-sign --bundles appimage
+ - platform: windows-latest
+ tauri_args: --ci --no-sign
+ defaults:
+ run:
+ shell: bash
+
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v6
+
+ - name: Set up pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 10.27.0
+ run_install: false
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: 22
+ cache: pnpm
+ cache-dependency-path: apps/desktop/pnpm-lock.yaml
+
+ - name: Set up Python
+ uses: actions/setup-python@v6
+ with:
+ python-version: "3.13"
+
+ - name: Set up Rust
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ toolchain: 1.92.0
+
+ - name: Install Linux system dependencies
+ if: matrix.platform == 'ubuntu-22.04'
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
+
+ - name: Install frontend dependencies
+ run: pnpm --dir apps/desktop install --frozen-lockfile
+
+ - name: Build and publish draft release
+ uses: tauri-apps/tauri-action@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ projectPath: apps/desktop
+ tauriScript: pnpm tauri
+ tagName: ${{ needs.preflight.outputs.tag_name }}
+ releaseName: Senamby ${{ needs.preflight.outputs.tag_name }}
+ releaseBody: |
+ Automated draft release for ${{ needs.preflight.outputs.tag_name }}.
+ releaseDraft: true
+ prerelease: false
+ releaseCommitish: ${{ github.sha }}
+ args: ${{ matrix.tauri_args }}
diff --git a/.gitignore b/.gitignore
index 3efccbb..aaffcbe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,167 +1,39 @@
-# Byte-compiled / optimized / DLL files
-__pycache__/
-*.py[cod]
-*$py.class
+.DS_Store
+Thumbs.db
+Desktop.ini
+.directory
-# C extensions
-*.so
+**/*.log
+pnpm-debug.log*
-# Distribution / packaging
-.Python
-build/
-develop-eggs/
+node_modules/
+.pnpm-store/
dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-wheels/
-share/python-wheels/
-*.egg-info/
-.installed.cfg
-*.egg
-MANIFEST
-
-# PyInstaller
-# Usually these files are written by a python script from a template
-# before PyInstaller builds the exe, so as to inject date/other infos into it.
-*.manifest
-*.spec
-
-# Installer logs
-pip-log.txt
-pip-delete-this-directory.txt
-
-# Unit test / coverage reports
-htmlcov/
-.tox/
-.nox/
-.coverage
-.coverage.*
-.cache
-nosetests.xml
-coverage.xml
-*.cover
-*.py,cover
-.hypothesis/
-.pytest_cache/
-cover/
-
-# Translations
-*.mo
-*.pot
-
-# Django stuff:
-*.log
-local_settings.py
-db.sqlite3
-db.sqlite3-journal
-
-# Flask stuff:
-instance/
-.webassets-cache
-
-# Scrapy stuff:
-.scrapy
-
-# Sphinx documentation
-docs/_build/
+build/
+.svelte-kit/
+.output/
-# PyBuilder
-.pybuilder/
target/
+docs-dev
+docs-dev/**
-# Jupyter Notebook
-.ipynb_checkpoints
-
-# IPython
-profile_default/
-ipython_config.py
-
-# pyenv
-# For a library or package, you might want to ignore these files since the code is
-# intended to run in multiple environments; otherwise, check them in:
-# .python-version
-
-# pipenv
-# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
-# However, in case of collaboration, if having platform-specific dependencies or dependencies
-# having no cross-platform support, pipenv may install dependencies that don't work, or not
-# install all needed dependencies.
-#Pipfile.lock
-
-# poetry
-# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
-# This is especially recommended for binary packages to ensure reproducibility, and is more
-# commonly ignored for libraries.
-# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
-#poetry.lock
+apps/desktop/src-tauri/gen/
+apps/desktop/src-tauri/binaries/
+apps/desktop/src-tauri/bin/
-# pdm
-# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
-#pdm.lock
-# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
-# in version control.
-# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
-.pdm.toml
-.pdm-python
-.pdm-build/
-
-# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
-__pypackages__/
-
-# Celery stuff
-celerybeat-schedule
-celerybeat.pid
-
-# SageMath parsed files
-*.sage.py
-
-# Environments
.env
-.venv
-env/
-venv/
-ENV/
-env.bak/
-venv.bak/
-
-# Spyder project settings
-.spyderproject
-.spyproject
-
-# Rope project settings
-.ropeproject
-
-# mkdocs documentation
-/site
+.env.*
+!.env.example
+!.env.test
-# mypy
-.mypy_cache/
-.dmypy.json
-dmypy.json
-
-# Pyre type checker
-.pyre/
-
-# pytype static type analyzer
-.pytype/
-
-# Cython debug symbols
-cython_debug/
-
-# PyCharm
-# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
-# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
-# and can be added to the global gitignore or merged into this file. For a more nuclear
-# option (not recommended) you can uncomment the following to ignore the entire idea folder.
-#.idea/
-
-temp_logs/
-.idea/
+__pycache__/
+*.py[cod]
+.venv/
+venv/
-.vscode/
\ No newline at end of file
+**/.vscode/
+!**/.vscode/*
+!**/.vscode/extensions.json
+!**/.vscode/settings.json
+!**/.vscode/tasks.json
+!**/.vscode/launch.json
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..aa1749e
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,5376 @@
+# 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.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
+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.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
+
+[[package]]
+name = "async-broadcast"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-channel"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
+dependencies = [
+ "concurrent-queue",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8"
+dependencies = [
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "async-io"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-io",
+ "futures-lite",
+ "parking",
+ "polling",
+ "rustix",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-lock"
+version = "3.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-process"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
+dependencies = [
+ "async-channel",
+ "async-io",
+ "async-lock",
+ "async-signal",
+ "async-task",
+ "blocking",
+ "cfg-if",
+ "event-listener",
+ "futures-lite",
+ "rustix",
+]
+
+[[package]]
+name = "async-recursion"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.114",
+]
+
+[[package]]
+name = "async-signal"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c"
+dependencies = [
+ "async-io",
+ "async-lock",
+ "atomic-waker",
+ "cfg-if",
+ "futures-core",
+ "futures-io",
+ "rustix",
+ "signal-hook-registry",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-task"
+version = "4.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
+
+[[package]]
+name = "async-trait"
+version = "0.1.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.114",
+]
+
+[[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.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[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 = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
+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 = "blocking"
+version = "1.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
+dependencies = [
+ "async-channel",
+ "async-task",
+ "futures-io",
+ "futures-lite",
+ "piper",
+]
+
+[[package]]
+name = "brotli"
+version = "8.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+ "brotli-decompressor",
+]
+
+[[package]]
+name = "brotli-decompressor"
+version = "5.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.19.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
+
+[[package]]
+name = "bytemuck"
+version = "1.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
+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.10.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.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48"
+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.17",
+]
+
+[[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.10+spec-1.1.0",
+]
+
+[[package]]
+name = "cc"
+version = "1.2.51"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cesu8"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+
+[[package]]
+name = "cfb"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f"
+dependencies = [
+ "byteorder",
+ "fnv",
+ "uuid",
+]
+
+[[package]]
+name = "cfg-expr"
+version = "0.15.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
+dependencies = [
+ "smallvec",
+ "target-lexicon",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "chrono"
+version = "0.4.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
+dependencies = [
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "serde",
+ "wasm-bindgen",
+ "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 = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "convert_case"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
+
+[[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.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
+dependencies = [
+ "bitflags 2.10.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.10.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.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "cssparser"
+version = "0.29.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa"
+dependencies = [
+ "cssparser-macros",
+ "dtoa-short",
+ "itoa",
+ "matches",
+ "phf 0.10.1",
+ "proc-macro2",
+ "quote",
+ "smallvec",
+ "syn 1.0.109",
+]
+
+[[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.114",
+]
+
+[[package]]
+name = "ctor"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
+dependencies = [
+ "quote",
+ "syn 2.0.114",
+]
+
+[[package]]
+name = "darling"
+version = "0.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn 2.0.114",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn 2.0.114",
+]
+
+[[package]]
+name = "deranged"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
+dependencies = [
+ "powerfmt",
+ "serde_core",
+]
+
+[[package]]
+name = "derive_more"
+version = "0.99.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f"
+dependencies = [
+ "convert_case",
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn 2.0.114",
+]
+
+[[package]]
+name = "diff"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
+
+[[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 = "5.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
+dependencies = [
+ "dirs-sys 0.4.1",
+]
+
+[[package]]
+name = "dirs"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
+dependencies = [
+ "dirs-sys 0.5.0",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users 0.4.6",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users 0.5.2",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "dispatch"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
+
+[[package]]
+name = "dispatch2"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
+dependencies = [
+ "bitflags 2.10.0",
+ "block2",
+ "libc",
+ "objc2",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.114",
+]
+
+[[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.114",
+]
+
+[[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 = "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.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55a075fc573c64510038d7ee9abc7990635863992f83ebc52c8b433b8411a02e"
+dependencies = [
+ "cc",
+ "memchr",
+ "rustc_version",
+ "toml 0.9.10+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 = "endi"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
+
+[[package]]
+name = "enumflags2"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
+dependencies = [
+ "enumflags2_derive",
+ "serde",
+]
+
+[[package]]
+name = "enumflags2_derive"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.114",
+]
+
+[[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.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3"
+dependencies = [
+ "serde",
+ "serde_core",
+ "typeid",
+]
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "event-listener"
+version = "5.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
+dependencies = [
+ "event-listener",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[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.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff"
+
+[[package]]
+name = "flate2"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb"
+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 = "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.114",
+]
+
+[[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 = "futf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843"
+dependencies = [
+ "mac",
+ "new_debug_unreachable",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
+
+[[package]]
+name = "futures-lite"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "futures-macro"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.114",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
+
+[[package]]
+name = "futures-task"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
+
+[[package]]
+name = "futures-util"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
+dependencies = [
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "fxhash"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "gdk"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691"
+dependencies = [
+ "cairo-rs",
+ "gdk-pixbuf",
+ "gdk-sys",
+ "gio",
+ "glib",
+ "libc",
+ "pango",
+]
+
+[[package]]
+name = "gdk-pixbuf"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec"
+dependencies = [
+ "gdk-pixbuf-sys",
+ "gio",
+ "glib",
+ "libc",
+ "once_cell",
+]
+
+[[package]]
+name = "gdk-pixbuf-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7"
+dependencies = [
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gdk-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7"
+dependencies = [
+ "cairo-sys-rs",
+ "gdk-pixbuf-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pango-sys",
+ "pkg-config",
+ "system-deps",
+]
+
+[[package]]
+name = "gdkwayland-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69"
+dependencies = [
+ "gdk-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pkg-config",
+ "system-deps",
+]
+
+[[package]]
+name = "gdkx11"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe"
+dependencies = [
+ "gdk",
+ "gdkx11-sys",
+ "gio",
+ "glib",
+ "libc",
+ "x11",
+]
+
+[[package]]
+name = "gdkx11-sys"
+version = "0.18.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d"
+dependencies = [
+ "gdk-sys",
+ "glib-sys",
+ "libc",
+ "system-deps",
+ "x11",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.9.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.11.1+wasi-snapshot-preview1",
+]
+
+[[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",
+ "wasip2",
+]
+
+[[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.10.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.114",
+]
+
+[[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.114",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "html5ever"
+version = "0.29.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c"
+dependencies = [
+ "log",
+ "mac",
+ "markup5ever",
+ "match_token",
+]
+
+[[package]]
+name = "http"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
+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.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "http",
+ "http-body",
+ "httparse",
+ "itoa",
+ "pin-project-lite",
+ "pin-utils",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "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.64"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
+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.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98"
+dependencies = [
+ "byteorder",
+ "png",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
+
+[[package]]
+name = "icu_properties"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af"
+
+[[package]]
+name = "icu_provider"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown 0.12.3",
+ "serde",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.16.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.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
+
+[[package]]
+name = "iri-string"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
+[[package]]
+name = "is-docker"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "is-wsl"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
+dependencies = [
+ "is-docker",
+ "once_cell",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
+
+[[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",
+ "log",
+ "thiserror 1.0.69",
+ "walkdir",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
+
+[[package]]
+name = "js-sys"
+version = "0.3.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
+dependencies = [
+ "once_cell",
+ "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.10.0",
+ "serde",
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "kuchikiki"
+version = "0.8.8-speedreader"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2"
+dependencies = [
+ "cssparser",
+ "html5ever",
+ "indexmap 2.12.1",
+ "selectors",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[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.179"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f"
+
+[[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.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
+dependencies = [
+ "bitflags 2.10.0",
+ "libc",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
+
+[[package]]
+name = "litemap"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
+
+[[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.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+
+[[package]]
+name = "mac"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
+
+[[package]]
+name = "markup5ever"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18"
+dependencies = [
+ "log",
+ "phf 0.11.3",
+ "phf_codegen 0.11.3",
+ "string_cache",
+ "string_cache_codegen",
+ "tendril",
+]
+
+[[package]]
+name = "match_token"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.114",
+]
+
+[[package]]
+name = "matches"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
+
+[[package]]
+name = "memchr"
+version = "2.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
+
+[[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.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
+dependencies = [
+ "libc",
+ "wasi 0.11.1+wasi-snapshot-preview1",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "muda"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a"
+dependencies = [
+ "crossbeam-channel",
+ "dpi",
+ "gtk",
+ "keyboard-types",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "once_cell",
+ "png",
+ "serde",
+ "thiserror 2.0.17",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "ndk"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
+dependencies = [
+ "bitflags 2.10.0",
+ "jni-sys",
+ "log",
+ "ndk-sys",
+ "num_enum",
+ "raw-window-handle",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "ndk-context"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
+
+[[package]]
+name = "ndk-sys"
+version = "0.6.0+11769913"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873"
+dependencies = [
+ "jni-sys",
+]
+
+[[package]]
+name = "new_debug_unreachable"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
+
+[[package]]
+name = "nix"
+version = "0.30.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
+dependencies = [
+ "bitflags 2.10.0",
+ "cfg-if",
+ "cfg_aliases",
+ "libc",
+ "memoffset",
+]
+
+[[package]]
+name = "nodrop"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
+
+[[package]]
+name = "num-conv"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_enum"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c"
+dependencies = [
+ "num_enum_derive",
+ "rustversion",
+]
+
+[[package]]
+name = "num_enum_derive"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7"
+dependencies = [
+ "proc-macro-crate 3.4.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.114",
+]
+
+[[package]]
+name = "objc2"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05"
+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.10.0",
+ "block2",
+ "libc",
+ "objc2",
+ "objc2-cloud-kit",
+ "objc2-core-data",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-core-image",
+ "objc2-core-text",
+ "objc2-core-video",
+ "objc2-foundation",
+ "objc2-quartz-core",
+]
+
+[[package]]
+name = "objc2-cloud-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c"
+dependencies = [
+ "bitflags 2.10.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 = [
+ "bitflags 2.10.0",
+ "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.10.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.10.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-text"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d"
+dependencies = [
+ "bitflags 2.10.0",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+]
+
+[[package]]
+name = "objc2-core-video"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6"
+dependencies = [
+ "bitflags 2.10.0",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-io-surface",
+]
+
+[[package]]
+name = "objc2-encode"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
+
+[[package]]
+name = "objc2-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.10.0",
+ "block2",
+ "libc",
+ "objc2",
+ "objc2-core-foundation",
+]
+
+[[package]]
+name = "objc2-io-surface"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
+dependencies = [
+ "bitflags 2.10.0",
+ "objc2",
+ "objc2-core-foundation",
+]
+
+[[package]]
+name = "objc2-javascript-core"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586"
+dependencies = [
+ "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.10.0",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-foundation",
+]
+
+[[package]]
+name = "objc2-security"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a"
+dependencies = [
+ "bitflags 2.10.0",
+ "objc2",
+ "objc2-core-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.10.0",
+ "objc2",
+ "objc2-core-foundation",
+ "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.10.0",
+ "block2",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "objc2-javascript-core",
+ "objc2-security",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "open"
+version = "5.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc"
+dependencies = [
+ "dunce",
+ "is-wsl",
+ "libc",
+ "pathdiff",
+]
+
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
+name = "ordered-stream"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "pango"
+version = "0.18.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4"
+dependencies = [
+ "gio",
+ "glib",
+ "libc",
+ "once_cell",
+ "pango-sys",
+]
+
+[[package]]
+name = "pango-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "pathdiff"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
+
+[[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.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
+dependencies = [
+ "phf_shared 0.8.0",
+]
+
+[[package]]
+name = "phf"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259"
+dependencies = [
+ "phf_macros 0.10.0",
+ "phf_shared 0.10.0",
+ "proc-macro-hack",
+]
+
+[[package]]
+name = "phf"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
+dependencies = [
+ "phf_macros 0.11.3",
+ "phf_shared 0.11.3",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815"
+dependencies = [
+ "phf_generator 0.8.0",
+ "phf_shared 0.8.0",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
+dependencies = [
+ "phf_generator 0.11.3",
+ "phf_shared 0.11.3",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526"
+dependencies = [
+ "phf_shared 0.8.0",
+ "rand 0.7.3",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
+dependencies = [
+ "phf_shared 0.10.0",
+ "rand 0.8.5",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
+dependencies = [
+ "phf_shared 0.11.3",
+ "rand 0.8.5",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0"
+dependencies = [
+ "phf_generator 0.10.0",
+ "phf_shared 0.10.0",
+ "proc-macro-hack",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
+dependencies = [
+ "phf_generator 0.11.3",
+ "phf_shared 0.11.3",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.114",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7"
+dependencies = [
+ "siphasher 0.3.11",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
+dependencies = [
+ "siphasher 0.3.11",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
+dependencies = [
+ "siphasher 1.0.1",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "piper"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066"
+dependencies = [
+ "atomic-waker",
+ "fastrand",
+ "futures-io",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+
+[[package]]
+name = "plist"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07"
+dependencies = [
+ "base64 0.22.1",
+ "indexmap 2.12.1",
+ "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 = "polling"
+version = "3.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "hermit-abi",
+ "pin-project-lite",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "potential_utf"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "precomputed-hash"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
+
+[[package]]
+name = "pretty_assertions"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
+dependencies = [
+ "diff",
+ "yansi",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
+dependencies = [
+ "once_cell",
+ "toml_edit 0.19.15",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24"
+dependencies = [
+ "toml_datetime 0.6.3",
+ "toml_edit 0.20.2",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983"
+dependencies = [
+ "toml_edit 0.23.10+spec-1.0.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-macro-hack"
+version = "0.5.20+deprecated"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.105"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quick-xml"
+version = "0.38.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
+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 = "rand"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
+dependencies = [
+ "getrandom 0.1.16",
+ "libc",
+ "rand_chacha 0.2.2",
+ "rand_core 0.5.1",
+ "rand_hc",
+ "rand_pcg",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
+dependencies = [
+ "getrandom 0.1.16",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.16",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
+dependencies = [
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rand_pcg"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429"
+dependencies = [
+ "rand_core 0.5.1",
+]
+
+[[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.10.0",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
+dependencies = [
+ "getrandom 0.2.16",
+ "libredox",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
+dependencies = [
+ "getrandom 0.2.16",
+ "libredox",
+ "thiserror 2.0.17",
+]
+
+[[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.114",
+]
+
+[[package]]
+name = "regex"
+version = "1.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
+
+[[package]]
+name = "reqwest"
+version = "0.12.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-util",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams",
+ "web-sys",
+]
+
+[[package]]
+name = "rfd"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
+dependencies = [
+ "block2",
+ "dispatch2",
+ "glib-sys",
+ "gobject-sys",
+ "gtk-sys",
+ "js-sys",
+ "log",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "raw-window-handle",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustix"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
+dependencies = [
+ "bitflags 2.10.0",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "ryu"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
+
+[[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.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2"
+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.114",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "selectors"
+version = "0.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416"
+dependencies = [
+ "bitflags 1.3.2",
+ "cssparser",
+ "derive_more",
+ "fxhash",
+ "log",
+ "phf 0.8.0",
+ "phf_codegen 0.8.0",
+ "precomputed-hash",
+ "servo_arc",
+ "smallvec",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
+dependencies = [
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "senamby"
+version = "0.1.0"
+dependencies = [
+ "chrono",
+ "dirs 5.0.1",
+ "parking_lot",
+ "serde",
+ "serde_json",
+ "tauri",
+ "tauri-build",
+ "tauri-plugin-dialog",
+ "tauri-plugin-opener",
+ "thiserror 1.0.69",
+ "uuid",
+]
+
+[[package]]
+name = "senamby-supervisor"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "pretty_assertions",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.17",
+ "tracing",
+]
+
+[[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.114",
+]
+
+[[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.114",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.149"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+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.114",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_with"
+version = "3.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7"
+dependencies = [
+ "base64 0.22.1",
+ "chrono",
+ "hex",
+ "indexmap 1.9.3",
+ "indexmap 2.12.1",
+ "schemars 0.9.0",
+ "schemars 1.2.0",
+ "serde_core",
+ "serde_json",
+ "serde_with_macros",
+ "time",
+]
+
+[[package]]
+name = "serde_with_macros"
+version = "3.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.114",
+]
+
+[[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.114",
+]
+
+[[package]]
+name = "servo_arc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741"
+dependencies = [
+ "nodrop",
+ "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 = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
+dependencies = [
+ "errno",
+ "libc",
+]
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
+
+[[package]]
+name = "siphasher"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
+
+[[package]]
+name = "siphasher"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
+
+[[package]]
+name = "slab"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "socket2"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
+dependencies = [
+ "libc",
+ "windows-sys 0.60.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 = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "string_cache"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f"
+dependencies = [
+ "new_debug_unreachable",
+ "parking_lot",
+ "phf_shared 0.11.3",
+ "precomputed-hash",
+ "serde",
+]
+
+[[package]]
+name = "string_cache_codegen"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0"
+dependencies = [
+ "phf_generator 0.11.3",
+ "phf_shared 0.11.3",
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "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",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.114"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
+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.114",
+]
+
+[[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.34.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7"
+dependencies = [
+ "bitflags 2.10.0",
+ "block2",
+ "core-foundation",
+ "core-graphics",
+ "crossbeam-channel",
+ "dispatch",
+ "dlopen2",
+ "dpi",
+ "gdkwayland-sys",
+ "gdkx11-sys",
+ "gtk",
+ "jni",
+ "lazy_static",
+ "libc",
+ "log",
+ "ndk",
+ "ndk-context",
+ "ndk-sys",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-foundation",
+ "once_cell",
+ "parking_lot",
+ "raw-window-handle",
+ "scopeguard",
+ "tao-macros",
+ "unicode-segmentation",
+ "url",
+ "windows",
+ "windows-core 0.61.2",
+ "windows-version",
+ "x11-dl",
+]
+
+[[package]]
+name = "tao-macros"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.114",
+]
+
+[[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.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a3868da5508446a7cd08956d523ac3edf0a8bc20bf7e4038f9a95c2800d2033"
+dependencies = [
+ "anyhow",
+ "bytes",
+ "cookie",
+ "dirs 6.0.0",
+ "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.17",
+ "tokio",
+ "tray-icon",
+ "url",
+ "webkit2gtk",
+ "webview2-com",
+ "window-vibrancy",
+ "windows",
+]
+
+[[package]]
+name = "tauri-build"
+version = "2.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17fcb8819fd16463512a12f531d44826ce566f486d7ccd211c9c8cebdaec4e08"
+dependencies = [
+ "anyhow",
+ "cargo_toml",
+ "dirs 6.0.0",
+ "glob",
+ "heck 0.5.0",
+ "json-patch",
+ "schemars 0.8.22",
+ "semver",
+ "serde",
+ "serde_json",
+ "tauri-utils",
+ "tauri-winres",
+ "toml 0.9.10+spec-1.1.0",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-codegen"
+version = "2.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa9844cefcf99554a16e0a278156ae73b0d8680bbc0e2ad1e4287aadd8489cf"
+dependencies = [
+ "base64 0.22.1",
+ "brotli",
+ "ico",
+ "json-patch",
+ "plist",
+ "png",
+ "proc-macro2",
+ "quote",
+ "semver",
+ "serde",
+ "serde_json",
+ "sha2",
+ "syn 2.0.114",
+ "tauri-utils",
+ "thiserror 2.0.17",
+ "time",
+ "url",
+ "uuid",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-macros"
+version = "2.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3764a12f886d8245e66b7ee9b43ccc47883399be2019a61d80cf0f4117446fde"
+dependencies = [
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.114",
+ "tauri-codegen",
+ "tauri-utils",
+]
+
+[[package]]
+name = "tauri-plugin"
+version = "2.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e1d0a4860b7ff570c891e1d2a586bf1ede205ff858fbc305e0b5ae5d14c1377"
+dependencies = [
+ "anyhow",
+ "glob",
+ "plist",
+ "schemars 0.8.22",
+ "serde",
+ "serde_json",
+ "tauri-utils",
+ "toml 0.9.10+spec-1.1.0",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-plugin-dialog"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9204b425d9be8d12aa60c2a83a289cf7d1caae40f57f336ed1155b3a5c0e359b"
+dependencies = [
+ "log",
+ "raw-window-handle",
+ "rfd",
+ "serde",
+ "serde_json",
+ "tauri",
+ "tauri-plugin",
+ "tauri-plugin-fs",
+ "thiserror 2.0.17",
+ "url",
+]
+
+[[package]]
+name = "tauri-plugin-fs"
+version = "2.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed390cc669f937afeb8b28032ce837bac8ea023d975a2e207375ec05afaf1804"
+dependencies = [
+ "anyhow",
+ "dunce",
+ "glob",
+ "percent-encoding",
+ "schemars 0.8.22",
+ "serde",
+ "serde_json",
+ "serde_repr",
+ "tauri",
+ "tauri-plugin",
+ "tauri-utils",
+ "thiserror 2.0.17",
+ "toml 0.9.10+spec-1.1.0",
+ "url",
+]
+
+[[package]]
+name = "tauri-plugin-opener"
+version = "2.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c26b72571d25dee25667940027114e60f569fc3974f8cefbe50c2cbc5fd65e3b"
+dependencies = [
+ "dunce",
+ "glob",
+ "objc2-app-kit",
+ "objc2-foundation",
+ "open",
+ "schemars 0.8.22",
+ "serde",
+ "serde_json",
+ "tauri",
+ "tauri-plugin",
+ "thiserror 2.0.17",
+ "url",
+ "windows",
+ "zbus",
+]
+
+[[package]]
+name = "tauri-runtime"
+version = "2.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87f766fe9f3d1efc4b59b17e7a891ad5ed195fa8d23582abb02e6c9a01137892"
+dependencies = [
+ "cookie",
+ "dpi",
+ "gtk",
+ "http",
+ "jni",
+ "objc2",
+ "objc2-ui-kit",
+ "objc2-web-kit",
+ "raw-window-handle",
+ "serde",
+ "serde_json",
+ "tauri-utils",
+ "thiserror 2.0.17",
+ "url",
+ "webkit2gtk",
+ "webview2-com",
+ "windows",
+]
+
+[[package]]
+name = "tauri-runtime-wry"
+version = "2.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "187a3f26f681bdf028f796ccf57cf478c1ee422c50128e5a0a6ebeb3f5910065"
+dependencies = [
+ "gtk",
+ "http",
+ "jni",
+ "log",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-foundation",
+ "once_cell",
+ "percent-encoding",
+ "raw-window-handle",
+ "softbuffer",
+ "tao",
+ "tauri-runtime",
+ "tauri-utils",
+ "url",
+ "webkit2gtk",
+ "webview2-com",
+ "windows",
+ "wry",
+]
+
+[[package]]
+name = "tauri-utils"
+version = "2.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76a423c51176eb3616ee9b516a9fa67fed5f0e78baaba680e44eb5dd2cc37490"
+dependencies = [
+ "anyhow",
+ "brotli",
+ "cargo_metadata",
+ "ctor",
+ "dunce",
+ "glob",
+ "html5ever",
+ "http",
+ "infer",
+ "json-patch",
+ "kuchikiki",
+ "log",
+ "memchr",
+ "phf 0.11.3",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "schemars 0.8.22",
+ "semver",
+ "serde",
+ "serde-untagged",
+ "serde_json",
+ "serde_with",
+ "swift-rs",
+ "thiserror 2.0.17",
+ "toml 0.9.10+spec-1.1.0",
+ "url",
+ "urlpattern",
+ "uuid",
+ "walkdir",
+]
+
+[[package]]
+name = "tauri-winres"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0"
+dependencies = [
+ "dunce",
+ "embed-resource",
+ "toml 0.9.10+spec-1.1.0",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
+dependencies = [
+ "fastrand",
+ "getrandom 0.3.4",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tendril"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0"
+dependencies = [
+ "futf",
+ "mac",
+ "utf-8",
+]
+
+[[package]]
+name = "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.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
+dependencies = [
+ "thiserror-impl 2.0.17",
+]
+
+[[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.114",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.114",
+]
+
+[[package]]
+name = "time"
+version = "0.3.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
+dependencies = [
+ "deranged",
+ "itoa",
+ "num-conv",
+ "powerfmt",
+ "serde",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
+
+[[package]]
+name = "time-macros"
+version = "0.2.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tokio"
+version = "1.49.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
+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.10+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48"
+dependencies = [
+ "indexmap 2.12.1",
+ "serde_core",
+ "serde_spanned 1.0.4",
+ "toml_datetime 0.7.5+spec-1.1.0",
+ "toml_parser",
+ "toml_writer",
+ "winnow 0.7.14",
+]
+
+[[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_edit"
+version = "0.19.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
+dependencies = [
+ "indexmap 2.12.1",
+ "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.12.1",
+ "serde",
+ "serde_spanned 0.6.9",
+ "toml_datetime 0.6.3",
+ "winnow 0.5.40",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.23.10+spec-1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269"
+dependencies = [
+ "indexmap 2.12.1",
+ "toml_datetime 0.7.5+spec-1.1.0",
+ "toml_parser",
+ "winnow 0.7.14",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.0.6+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44"
+dependencies = [
+ "winnow 0.7.14",
+]
+
+[[package]]
+name = "toml_writer"
+version = "1.0.6+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
+
+[[package]]
+name = "tower"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
+dependencies = [
+ "bitflags 2.10.0",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "iri-string",
+ "pin-project-lite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.114",
+]
+
+[[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.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c"
+dependencies = [
+ "crossbeam-channel",
+ "dirs 6.0.0",
+ "libappindicator",
+ "muda",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-core-graphics",
+ "objc2-foundation",
+ "once_cell",
+ "png",
+ "serde",
+ "thiserror 2.0.17",
+ "windows-sys 0.60.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.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
+
+[[package]]
+name = "uds_windows"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9"
+dependencies = [
+ "memoffset",
+ "tempfile",
+ "winapi",
+]
+
+[[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.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
+
+[[package]]
+name = "url"
+version = "2.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+ "serde_derive",
+]
+
+[[package]]
+name = "urlpattern"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d"
+dependencies = [
+ "regex",
+ "serde",
+ "unic-ucd-ident",
+ "url",
+]
+
+[[package]]
+name = "utf-8"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "uuid"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a"
+dependencies = [
+ "getrandom 0.3.4",
+ "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.9.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.1+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.56"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "once_cell",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.114",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "wasm-streams"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webkit2gtk"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a"
+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.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c"
+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.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4ba622a989277ef3886dd5afb3e280e3dd6d974b766118950a08f8f678ad6a4"
+dependencies = [
+ "webview2-com-macros",
+ "webview2-com-sys",
+ "windows",
+ "windows-core 0.61.2",
+ "windows-implement",
+ "windows-interface",
+]
+
+[[package]]
+name = "webview2-com-macros"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.114",
+]
+
+[[package]]
+name = "webview2-com-sys"
+version = "0.38.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c"
+dependencies = [
+ "thiserror 2.0.17",
+ "windows",
+ "windows-core 0.61.2",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "window-vibrancy"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c"
+dependencies = [
+ "objc2",
+ "objc2-app-kit",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "raw-window-handle",
+ "windows-sys 0.59.0",
+ "windows-version",
+]
+
+[[package]]
+name = "windows"
+version = "0.61.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
+dependencies = [
+ "windows-collections",
+ "windows-core 0.61.2",
+ "windows-future",
+ "windows-link 0.1.3",
+ "windows-numerics",
+]
+
+[[package]]
+name = "windows-collections"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
+dependencies = [
+ "windows-core 0.61.2",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link 0.1.3",
+ "windows-result 0.3.4",
+ "windows-strings 0.4.2",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link 0.2.1",
+ "windows-result 0.4.1",
+ "windows-strings 0.5.1",
+]
+
+[[package]]
+name = "windows-future"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
+dependencies = [
+ "windows-core 0.61.2",
+ "windows-link 0.1.3",
+ "windows-threading",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.114",
+]
+
+[[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.114",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-numerics"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
+dependencies = [
+ "windows-core 0.61.2",
+ "windows-link 0.1.3",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
+dependencies = [
+ "windows-link 0.1.3",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
+dependencies = [
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
+dependencies = [
+ "windows-link 0.1.3",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
+dependencies = [
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.2",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm 0.52.6",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
+dependencies = [
+ "windows-link 0.2.1",
+ "windows_aarch64_gnullvm 0.53.1",
+ "windows_aarch64_msvc 0.53.1",
+ "windows_i686_gnu 0.53.1",
+ "windows_i686_gnullvm 0.53.1",
+ "windows_i686_msvc 0.53.1",
+ "windows_x86_64_gnu 0.53.1",
+ "windows_x86_64_gnullvm 0.53.1",
+ "windows_x86_64_msvc 0.53.1",
+]
+
+[[package]]
+name = "windows-threading"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
+dependencies = [
+ "windows-link 0.1.3",
+]
+
+[[package]]
+name = "windows-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.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
+
+[[package]]
+name = "winnow"
+version = "0.5.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "winnow"
+version = "0.7.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "winreg"
+version = "0.55.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97"
+dependencies = [
+ "cfg-if",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
+
+[[package]]
+name = "writeable"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
+
+[[package]]
+name = "wry"
+version = "0.53.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728b7d4c8ec8d81cab295e0b5b8a4c263c0d41a785fb8f8c4df284e5411140a2"
+dependencies = [
+ "base64 0.22.1",
+ "block2",
+ "cookie",
+ "crossbeam-channel",
+ "dirs 6.0.0",
+ "dpi",
+ "dunce",
+ "gdkx11",
+ "gtk",
+ "html5ever",
+ "http",
+ "javascriptcore-rs",
+ "jni",
+ "kuchikiki",
+ "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.17",
+ "url",
+ "webkit2gtk",
+ "webkit2gtk-sys",
+ "webview2-com",
+ "windows",
+ "windows-core 0.61.2",
+ "windows-version",
+ "x11-dl",
+]
+
+[[package]]
+name = "x11"
+version = "2.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e"
+dependencies = [
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "x11-dl"
+version = "2.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f"
+dependencies = [
+ "libc",
+ "once_cell",
+ "pkg-config",
+]
+
+[[package]]
+name = "yansi"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
+
+[[package]]
+name = "yoke"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.114",
+ "synstructure",
+]
+
+[[package]]
+name = "zbus"
+version = "5.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91"
+dependencies = [
+ "async-broadcast",
+ "async-executor",
+ "async-io",
+ "async-lock",
+ "async-process",
+ "async-recursion",
+ "async-task",
+ "async-trait",
+ "blocking",
+ "enumflags2",
+ "event-listener",
+ "futures-core",
+ "futures-lite",
+ "hex",
+ "nix",
+ "ordered-stream",
+ "serde",
+ "serde_repr",
+ "tracing",
+ "uds_windows",
+ "uuid",
+ "windows-sys 0.61.2",
+ "winnow 0.7.14",
+ "zbus_macros",
+ "zbus_names",
+ "zvariant",
+]
+
+[[package]]
+name = "zbus_macros"
+version = "5.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cdb94821ca8a87ca9c298b5d1cbd80e2a8b67115d99f6e4551ac49e42b6a314"
+dependencies = [
+ "proc-macro-crate 3.4.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.114",
+ "zbus_names",
+ "zvariant",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zbus_names"
+version = "4.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97"
+dependencies = [
+ "serde",
+ "static_assertions",
+ "winnow 0.7.14",
+ "zvariant",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fabae64378cb18147bb18bca364e63bdbe72a0ffe4adf0addfec8aa166b2c56"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9c2d862265a8bb4471d87e033e730f536e2a285cc7cb05dbce09a2a97075f90"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.114",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.114",
+ "synstructure",
+]
+
+[[package]]
+name = "zerotrie"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.114",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8"
+
+[[package]]
+name = "zvariant"
+version = "5.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2be61892e4f2b1772727be11630a62664a1826b62efa43a6fe7449521cb8744c"
+dependencies = [
+ "endi",
+ "enumflags2",
+ "serde",
+ "winnow 0.7.14",
+ "zvariant_derive",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_derive"
+version = "5.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da58575a1b2b20766513b1ec59d8e2e68db2745379f961f86650655e862d2006"
+dependencies = [
+ "proc-macro-crate 3.4.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.114",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_utils"
+version = "3.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde",
+ "syn 2.0.114",
+ "winnow 0.7.14",
+]
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..91e408b
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,3 @@
+[workspace]
+resolver = "2"
+members = ["apps/desktop/src-tauri", "crates/*"]
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index f288702..0000000
--- a/LICENSE
+++ /dev/null
@@ -1,674 +0,0 @@
- GNU GENERAL PUBLIC LICENSE
- Version 3, 29 June 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc.
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
- Preamble
-
- The GNU General Public License is a free, copyleft license for
-software and other kinds of works.
-
- The licenses for most software and other practical works are designed
-to take away your freedom to share and change the works. By contrast,
-the GNU General Public License is intended to guarantee your freedom to
-share and change all versions of a program--to make sure it remains free
-software for all its users. We, the Free Software Foundation, use the
-GNU General Public License for most of our software; it applies also to
-any other work released this way by its authors. You can apply it to
-your programs, too.
-
- When we speak of free software, we are referring to freedom, not
-price. Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-them if you wish), that you receive source code or can get it if you
-want it, that you can change the software or use pieces of it in new
-free programs, and that you know you can do these things.
-
- To protect your rights, we need to prevent others from denying you
-these rights or asking you to surrender the rights. Therefore, you have
-certain responsibilities if you distribute copies of the software, or if
-you modify it: responsibilities to respect the freedom of others.
-
- For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must pass on to the recipients the same
-freedoms that you received. You must make sure that they, too, receive
-or can get the source code. And you must show them these terms so they
-know their rights.
-
- Developers that use the GNU GPL protect your rights with two steps:
-(1) assert copyright on the software, and (2) offer you this License
-giving you legal permission to copy, distribute and/or modify it.
-
- For the developers' and authors' protection, the GPL clearly explains
-that there is no warranty for this free software. For both users' and
-authors' sake, the GPL requires that modified versions be marked as
-changed, so that their problems will not be attributed erroneously to
-authors of previous versions.
-
- Some devices are designed to deny users access to install or run
-modified versions of the software inside them, although the manufacturer
-can do so. This is fundamentally incompatible with the aim of
-protecting users' freedom to change the software. The systematic
-pattern of such abuse occurs in the area of products for individuals to
-use, which is precisely where it is most unacceptable. Therefore, we
-have designed this version of the GPL to prohibit the practice for those
-products. If such problems arise substantially in other domains, we
-stand ready to extend this provision to those domains in future versions
-of the GPL, as needed to protect the freedom of users.
-
- Finally, every program is threatened constantly by software patents.
-States should not allow patents to restrict development and use of
-software on general-purpose computers, but in those that do, we wish to
-avoid the special danger that patents applied to a free program could
-make it effectively proprietary. To prevent this, the GPL assures that
-patents cannot be used to render the program non-free.
-
- The precise terms and conditions for copying, distribution and
-modification follow.
-
- TERMS AND CONDITIONS
-
- 0. Definitions.
-
- "This License" refers to version 3 of the GNU General Public License.
-
- "Copyright" also means copyright-like laws that apply to other kinds of
-works, such as semiconductor masks.
-
- "The Program" refers to any copyrightable work licensed under this
-License. Each licensee is addressed as "you". "Licensees" and
-"recipients" may be individuals or organizations.
-
- To "modify" a work means to copy from or adapt all or part of the work
-in a fashion requiring copyright permission, other than the making of an
-exact copy. The resulting work is called a "modified version" of the
-earlier work or a work "based on" the earlier work.
-
- A "covered work" means either the unmodified Program or a work based
-on the Program.
-
- To "propagate" a work means to do anything with it that, without
-permission, would make you directly or secondarily liable for
-infringement under applicable copyright law, except executing it on a
-computer or modifying a private copy. Propagation includes copying,
-distribution (with or without modification), making available to the
-public, and in some countries other activities as well.
-
- To "convey" a work means any kind of propagation that enables other
-parties to make or receive copies. Mere interaction with a user through
-a computer network, with no transfer of a copy, is not conveying.
-
- An interactive user interface displays "Appropriate Legal Notices"
-to the extent that it includes a convenient and prominently visible
-feature that (1) displays an appropriate copyright notice, and (2)
-tells the user that there is no warranty for the work (except to the
-extent that warranties are provided), that licensees may convey the
-work under this License, and how to view a copy of this License. If
-the interface presents a list of user commands or options, such as a
-menu, a prominent item in the list meets this criterion.
-
- 1. Source Code.
-
- The "source code" for a work means the preferred form of the work
-for making modifications to it. "Object code" means any non-source
-form of a work.
-
- A "Standard Interface" means an interface that either is an official
-standard defined by a recognized standards body, or, in the case of
-interfaces specified for a particular programming language, one that
-is widely used among developers working in that language.
-
- The "System Libraries" of an executable work include anything, other
-than the work as a whole, that (a) is included in the normal form of
-packaging a Major Component, but which is not part of that Major
-Component, and (b) serves only to enable use of the work with that
-Major Component, or to implement a Standard Interface for which an
-implementation is available to the public in source code form. A
-"Major Component", in this context, means a major essential component
-(kernel, window system, and so on) of the specific operating system
-(if any) on which the executable work runs, or a compiler used to
-produce the work, or an object code interpreter used to run it.
-
- The "Corresponding Source" for a work in object code form means all
-the source code needed to generate, install, and (for an executable
-work) run the object code and to modify the work, including scripts to
-control those activities. However, it does not include the work's
-System Libraries, or general-purpose tools or generally available free
-programs which are used unmodified in performing those activities but
-which are not part of the work. For example, Corresponding Source
-includes interface definition files associated with source files for
-the work, and the source code for shared libraries and dynamically
-linked subprograms that the work is specifically designed to require,
-such as by intimate data communication or control flow between those
-subprograms and other parts of the work.
-
- The Corresponding Source need not include anything that users
-can regenerate automatically from other parts of the Corresponding
-Source.
-
- The Corresponding Source for a work in source code form is that
-same work.
-
- 2. Basic Permissions.
-
- All rights granted under this License are granted for the term of
-copyright on the Program, and are irrevocable provided the stated
-conditions are met. This License explicitly affirms your unlimited
-permission to run the unmodified Program. The output from running a
-covered work is covered by this License only if the output, given its
-content, constitutes a covered work. This License acknowledges your
-rights of fair use or other equivalent, as provided by copyright law.
-
- You may make, run and propagate covered works that you do not
-convey, without conditions so long as your license otherwise remains
-in force. You may convey covered works to others for the sole purpose
-of having them make modifications exclusively for you, or provide you
-with facilities for running those works, provided that you comply with
-the terms of this License in conveying all material for which you do
-not control copyright. Those thus making or running the covered works
-for you must do so exclusively on your behalf, under your direction
-and control, on terms that prohibit them from making any copies of
-your copyrighted material outside their relationship with you.
-
- Conveying under any other circumstances is permitted solely under
-the conditions stated below. Sublicensing is not allowed; section 10
-makes it unnecessary.
-
- 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
-
- No covered work shall be deemed part of an effective technological
-measure under any applicable law fulfilling obligations under article
-11 of the WIPO copyright treaty adopted on 20 December 1996, or
-similar laws prohibiting or restricting circumvention of such
-measures.
-
- When you convey a covered work, you waive any legal power to forbid
-circumvention of technological measures to the extent such circumvention
-is effected by exercising rights under this License with respect to
-the covered work, and you disclaim any intention to limit operation or
-modification of the work as a means of enforcing, against the work's
-users, your or third parties' legal rights to forbid circumvention of
-technological measures.
-
- 4. Conveying Verbatim Copies.
-
- You may convey verbatim copies of the Program's source code as you
-receive it, in any medium, provided that you conspicuously and
-appropriately publish on each copy an appropriate copyright notice;
-keep intact all notices stating that this License and any
-non-permissive terms added in accord with section 7 apply to the code;
-keep intact all notices of the absence of any warranty; and give all
-recipients a copy of this License along with the Program.
-
- You may charge any price or no price for each copy that you convey,
-and you may offer support or warranty protection for a fee.
-
- 5. Conveying Modified Source Versions.
-
- You may convey a work based on the Program, or the modifications to
-produce it from the Program, in the form of source code under the
-terms of section 4, provided that you also meet all of these conditions:
-
- a) The work must carry prominent notices stating that you modified
- it, and giving a relevant date.
-
- b) The work must carry prominent notices stating that it is
- released under this License and any conditions added under section
- 7. This requirement modifies the requirement in section 4 to
- "keep intact all notices".
-
- c) You must license the entire work, as a whole, under this
- License to anyone who comes into possession of a copy. This
- License will therefore apply, along with any applicable section 7
- additional terms, to the whole of the work, and all its parts,
- regardless of how they are packaged. This License gives no
- permission to license the work in any other way, but it does not
- invalidate such permission if you have separately received it.
-
- d) If the work has interactive user interfaces, each must display
- Appropriate Legal Notices; however, if the Program has interactive
- interfaces that do not display Appropriate Legal Notices, your
- work need not make them do so.
-
- A compilation of a covered work with other separate and independent
-works, which are not by their nature extensions of the covered work,
-and which are not combined with it such as to form a larger program,
-in or on a volume of a storage or distribution medium, is called an
-"aggregate" if the compilation and its resulting copyright are not
-used to limit the access or legal rights of the compilation's users
-beyond what the individual works permit. Inclusion of a covered work
-in an aggregate does not cause this License to apply to the other
-parts of the aggregate.
-
- 6. Conveying Non-Source Forms.
-
- You may convey a covered work in object code form under the terms
-of sections 4 and 5, provided that you also convey the
-machine-readable Corresponding Source under the terms of this License,
-in one of these ways:
-
- a) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by the
- Corresponding Source fixed on a durable physical medium
- customarily used for software interchange.
-
- b) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by a
- written offer, valid for at least three years and valid for as
- long as you offer spare parts or customer support for that product
- model, to give anyone who possesses the object code either (1) a
- copy of the Corresponding Source for all the software in the
- product that is covered by this License, on a durable physical
- medium customarily used for software interchange, for a price no
- more than your reasonable cost of physically performing this
- conveying of source, or (2) access to copy the
- Corresponding Source from a network server at no charge.
-
- c) Convey individual copies of the object code with a copy of the
- written offer to provide the Corresponding Source. This
- alternative is allowed only occasionally and noncommercially, and
- only if you received the object code with such an offer, in accord
- with subsection 6b.
-
- d) Convey the object code by offering access from a designated
- place (gratis or for a charge), and offer equivalent access to the
- Corresponding Source in the same way through the same place at no
- further charge. You need not require recipients to copy the
- Corresponding Source along with the object code. If the place to
- copy the object code is a network server, the Corresponding Source
- may be on a different server (operated by you or a third party)
- that supports equivalent copying facilities, provided you maintain
- clear directions next to the object code saying where to find the
- Corresponding Source. Regardless of what server hosts the
- Corresponding Source, you remain obligated to ensure that it is
- available for as long as needed to satisfy these requirements.
-
- e) Convey the object code using peer-to-peer transmission, provided
- you inform other peers where the object code and Corresponding
- Source of the work are being offered to the general public at no
- charge under subsection 6d.
-
- A separable portion of the object code, whose source code is excluded
-from the Corresponding Source as a System Library, need not be
-included in conveying the object code work.
-
- A "User Product" is either (1) a "consumer product", which means any
-tangible personal property which is normally used for personal, family,
-or household purposes, or (2) anything designed or sold for incorporation
-into a dwelling. In determining whether a product is a consumer product,
-doubtful cases shall be resolved in favor of coverage. For a particular
-product received by a particular user, "normally used" refers to a
-typical or common use of that class of product, regardless of the status
-of the particular user or of the way in which the particular user
-actually uses, or expects or is expected to use, the product. A product
-is a consumer product regardless of whether the product has substantial
-commercial, industrial or non-consumer uses, unless such uses represent
-the only significant mode of use of the product.
-
- "Installation Information" for a User Product means any methods,
-procedures, authorization keys, or other information required to install
-and execute modified versions of a covered work in that User Product from
-a modified version of its Corresponding Source. The information must
-suffice to ensure that the continued functioning of the modified object
-code is in no case prevented or interfered with solely because
-modification has been made.
-
- If you convey an object code work under this section in, or with, or
-specifically for use in, a User Product, and the conveying occurs as
-part of a transaction in which the right of possession and use of the
-User Product is transferred to the recipient in perpetuity or for a
-fixed term (regardless of how the transaction is characterized), the
-Corresponding Source conveyed under this section must be accompanied
-by the Installation Information. But this requirement does not apply
-if neither you nor any third party retains the ability to install
-modified object code on the User Product (for example, the work has
-been installed in ROM).
-
- The requirement to provide Installation Information does not include a
-requirement to continue to provide support service, warranty, or updates
-for a work that has been modified or installed by the recipient, or for
-the User Product in which it has been modified or installed. Access to a
-network may be denied when the modification itself materially and
-adversely affects the operation of the network or violates the rules and
-protocols for communication across the network.
-
- Corresponding Source conveyed, and Installation Information provided,
-in accord with this section must be in a format that is publicly
-documented (and with an implementation available to the public in
-source code form), and must require no special password or key for
-unpacking, reading or copying.
-
- 7. Additional Terms.
-
- "Additional permissions" are terms that supplement the terms of this
-License by making exceptions from one or more of its conditions.
-Additional permissions that are applicable to the entire Program shall
-be treated as though they were included in this License, to the extent
-that they are valid under applicable law. If additional permissions
-apply only to part of the Program, that part may be used separately
-under those permissions, but the entire Program remains governed by
-this License without regard to the additional permissions.
-
- When you convey a copy of a covered work, you may at your option
-remove any additional permissions from that copy, or from any part of
-it. (Additional permissions may be written to require their own
-removal in certain cases when you modify the work.) You may place
-additional permissions on material, added by you to a covered work,
-for which you have or can give appropriate copyright permission.
-
- Notwithstanding any other provision of this License, for material you
-add to a covered work, you may (if authorized by the copyright holders of
-that material) supplement the terms of this License with terms:
-
- a) Disclaiming warranty or limiting liability differently from the
- terms of sections 15 and 16 of this License; or
-
- b) Requiring preservation of specified reasonable legal notices or
- author attributions in that material or in the Appropriate Legal
- Notices displayed by works containing it; or
-
- c) Prohibiting misrepresentation of the origin of that material, or
- requiring that modified versions of such material be marked in
- reasonable ways as different from the original version; or
-
- d) Limiting the use for publicity purposes of names of licensors or
- authors of the material; or
-
- e) Declining to grant rights under trademark law for use of some
- trade names, trademarks, or service marks; or
-
- f) Requiring indemnification of licensors and authors of that
- material by anyone who conveys the material (or modified versions of
- it) with contractual assumptions of liability to the recipient, for
- any liability that these contractual assumptions directly impose on
- those licensors and authors.
-
- All other non-permissive additional terms are considered "further
-restrictions" within the meaning of section 10. If the Program as you
-received it, or any part of it, contains a notice stating that it is
-governed by this License along with a term that is a further
-restriction, you may remove that term. If a license document contains
-a further restriction but permits relicensing or conveying under this
-License, you may add to a covered work material governed by the terms
-of that license document, provided that the further restriction does
-not survive such relicensing or conveying.
-
- If you add terms to a covered work in accord with this section, you
-must place, in the relevant source files, a statement of the
-additional terms that apply to those files, or a notice indicating
-where to find the applicable terms.
-
- Additional terms, permissive or non-permissive, may be stated in the
-form of a separately written license, or stated as exceptions;
-the above requirements apply either way.
-
- 8. Termination.
-
- You may not propagate or modify a covered work except as expressly
-provided under this License. Any attempt otherwise to propagate or
-modify it is void, and will automatically terminate your rights under
-this License (including any patent licenses granted under the third
-paragraph of section 11).
-
- However, if you cease all violation of this License, then your
-license from a particular copyright holder is reinstated (a)
-provisionally, unless and until the copyright holder explicitly and
-finally terminates your license, and (b) permanently, if the copyright
-holder fails to notify you of the violation by some reasonable means
-prior to 60 days after the cessation.
-
- Moreover, your license from a particular copyright holder is
-reinstated permanently if the copyright holder notifies you of the
-violation by some reasonable means, this is the first time you have
-received notice of violation of this License (for any work) from that
-copyright holder, and you cure the violation prior to 30 days after
-your receipt of the notice.
-
- Termination of your rights under this section does not terminate the
-licenses of parties who have received copies or rights from you under
-this License. If your rights have been terminated and not permanently
-reinstated, you do not qualify to receive new licenses for the same
-material under section 10.
-
- 9. Acceptance Not Required for Having Copies.
-
- You are not required to accept this License in order to receive or
-run a copy of the Program. Ancillary propagation of a covered work
-occurring solely as a consequence of using peer-to-peer transmission
-to receive a copy likewise does not require acceptance. However,
-nothing other than this License grants you permission to propagate or
-modify any covered work. These actions infringe copyright if you do
-not accept this License. Therefore, by modifying or propagating a
-covered work, you indicate your acceptance of this License to do so.
-
- 10. Automatic Licensing of Downstream Recipients.
-
- Each time you convey a covered work, the recipient automatically
-receives a license from the original licensors, to run, modify and
-propagate that work, subject to this License. You are not responsible
-for enforcing compliance by third parties with this License.
-
- An "entity transaction" is a transaction transferring control of an
-organization, or substantially all assets of one, or subdividing an
-organization, or merging organizations. If propagation of a covered
-work results from an entity transaction, each party to that
-transaction who receives a copy of the work also receives whatever
-licenses to the work the party's predecessor in interest had or could
-give under the previous paragraph, plus a right to possession of the
-Corresponding Source of the work from the predecessor in interest, if
-the predecessor has it or can get it with reasonable efforts.
-
- You may not impose any further restrictions on the exercise of the
-rights granted or affirmed under this License. For example, you may
-not impose a license fee, royalty, or other charge for exercise of
-rights granted under this License, and you may not initiate litigation
-(including a cross-claim or counterclaim in a lawsuit) alleging that
-any patent claim is infringed by making, using, selling, offering for
-sale, or importing the Program or any portion of it.
-
- 11. Patents.
-
- A "contributor" is a copyright holder who authorizes use under this
-License of the Program or a work on which the Program is based. The
-work thus licensed is called the contributor's "contributor version".
-
- A contributor's "essential patent claims" are all patent claims
-owned or controlled by the contributor, whether already acquired or
-hereafter acquired, that would be infringed by some manner, permitted
-by this License, of making, using, or selling its contributor version,
-but do not include claims that would be infringed only as a
-consequence of further modification of the contributor version. For
-purposes of this definition, "control" includes the right to grant
-patent sublicenses in a manner consistent with the requirements of
-this License.
-
- Each contributor grants you a non-exclusive, worldwide, royalty-free
-patent license under the contributor's essential patent claims, to
-make, use, sell, offer for sale, import and otherwise run, modify and
-propagate the contents of its contributor version.
-
- In the following three paragraphs, a "patent license" is any express
-agreement or commitment, however denominated, not to enforce a patent
-(such as an express permission to practice a patent or covenant not to
-sue for patent infringement). To "grant" such a patent license to a
-party means to make such an agreement or commitment not to enforce a
-patent against the party.
-
- If you convey a covered work, knowingly relying on a patent license,
-and the Corresponding Source of the work is not available for anyone
-to copy, free of charge and under the terms of this License, through a
-publicly available network server or other readily accessible means,
-then you must either (1) cause the Corresponding Source to be so
-available, or (2) arrange to deprive yourself of the benefit of the
-patent license for this particular work, or (3) arrange, in a manner
-consistent with the requirements of this License, to extend the patent
-license to downstream recipients. "Knowingly relying" means you have
-actual knowledge that, but for the patent license, your conveying the
-covered work in a country, or your recipient's use of the covered work
-in a country, would infringe one or more identifiable patents in that
-country that you have reason to believe are valid.
-
- If, pursuant to or in connection with a single transaction or
-arrangement, you convey, or propagate by procuring conveyance of, a
-covered work, and grant a patent license to some of the parties
-receiving the covered work authorizing them to use, propagate, modify
-or convey a specific copy of the covered work, then the patent license
-you grant is automatically extended to all recipients of the covered
-work and works based on it.
-
- A patent license is "discriminatory" if it does not include within
-the scope of its coverage, prohibits the exercise of, or is
-conditioned on the non-exercise of one or more of the rights that are
-specifically granted under this License. You may not convey a covered
-work if you are a party to an arrangement with a third party that is
-in the business of distributing software, under which you make payment
-to the third party based on the extent of your activity of conveying
-the work, and under which the third party grants, to any of the
-parties who would receive the covered work from you, a discriminatory
-patent license (a) in connection with copies of the covered work
-conveyed by you (or copies made from those copies), or (b) primarily
-for and in connection with specific products or compilations that
-contain the covered work, unless you entered into that arrangement,
-or that patent license was granted, prior to 28 March 2007.
-
- Nothing in this License shall be construed as excluding or limiting
-any implied license or other defenses to infringement that may
-otherwise be available to you under applicable patent law.
-
- 12. No Surrender of Others' Freedom.
-
- If conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License. If you cannot convey a
-covered work so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you may
-not convey it at all. For example, if you agree to terms that obligate you
-to collect a royalty for further conveying from those to whom you convey
-the Program, the only way you could satisfy both those terms and this
-License would be to refrain entirely from conveying the Program.
-
- 13. Use with the GNU Affero General Public License.
-
- Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
-under version 3 of the GNU Affero General Public License into a single
-combined work, and to convey the resulting work. The terms of this
-License will continue to apply to the part which is the covered work,
-but the special requirements of the GNU Affero General Public License,
-section 13, concerning interaction through a network will apply to the
-combination as such.
-
- 14. Revised Versions of this License.
-
- The Free Software Foundation may publish revised and/or new versions of
-the GNU General Public License from time to time. Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
- Each version is given a distinguishing version number. If the
-Program specifies that a certain numbered version of the GNU General
-Public License "or any later version" applies to it, you have the
-option of following the terms and conditions either of that numbered
-version or of any later version published by the Free Software
-Foundation. If the Program does not specify a version number of the
-GNU General Public License, you may choose any version ever published
-by the Free Software Foundation.
-
- If the Program specifies that a proxy can decide which future
-versions of the GNU General Public License can be used, that proxy's
-public statement of acceptance of a version permanently authorizes you
-to choose that version for the Program.
-
- Later license versions may give you additional or different
-permissions. However, no additional obligations are imposed on any
-author or copyright holder as a result of your choosing to follow a
-later version.
-
- 15. Disclaimer of Warranty.
-
- THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
-APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
-HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
-OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
-IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
-ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
- 16. Limitation of Liability.
-
- IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
-THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
-GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
-USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
-DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
-PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
-EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
-SUCH DAMAGES.
-
- 17. Interpretation of Sections 15 and 16.
-
- If the disclaimer of warranty and limitation of liability provided
-above cannot be given local legal effect according to their terms,
-reviewing courts shall apply local law that most closely approximates
-an absolute waiver of all civil liability in connection with the
-Program, unless a warranty or assumption of liability accompanies a
-copy of the Program in return for a fee.
-
- END OF TERMS AND CONDITIONS
-
- How to Apply These Terms to Your New Programs
-
- If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
- To do so, attach the following notices to the program. It is safest
-to attach them to the start of each source file to most effectively
-state the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
-
- Copyright (C)
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see .
-
-Also add information on how to contact you by electronic and paper mail.
-
- If the program does terminal interaction, make it output a short
-notice like this when it starts in an interactive mode:
-
- Copyright (C)
- This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
- This is free software, and you are welcome to redistribute it
- under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License. Of course, your program's commands
-might be different; for a GUI interface, you would use an "about box".
-
- You should also get your employer (if you work as a programmer) or school,
-if any, to sign a "copyright disclaimer" for the program, if necessary.
-For more information on this, and how to apply and follow the GNU GPL, see
-.
-
- The GNU General Public License does not permit incorporating your program
-into proprietary programs. If your program is a subroutine library, you
-may consider it more useful to permit linking proprietary applications with
-the library. If this is what you want to do, use the GNU Lesser General
-Public License instead of this License. But first, please read
-.
diff --git a/README.md b/README.md
index 6254215..a1d3902 100644
--- a/README.md
+++ b/README.md
@@ -1,96 +1,40 @@
-# splot - Visualizador e Analisador de Controle Térmico
+# Senamby
-O **splot** é uma ferramenta para visualizar e analisar o comportamento de um sistema de controle térmico. Ele permite **selecionar controladores ativos**, ajustar suas configurações em tempo real e visualizar a resposta do sistema. Além disso, é possível alternar para a aba **Analyzer** e **analisar logs de execuções anteriores**.
+[](docs/en/index.md)
+[](docs/pt-BR/index.md)
-### Instalação
+Senamby is a desktop workspace for creating, running, and analyzing plants driven by reusable drivers and controllers. It combines a Svelte/Tauri desktop UI, a Rust backend, and a Python runtime for plant plugins.
-Garanta que você tem Python 3.7 ou mais recente instalado:
+## What You Can Do
-```shell
-python --version
-```
+- Create plants with sensors and actuators
+- Register reusable driver and controller plugins
+- Connect a plant to a live runtime
+- Plot sensor and actuator behavior in real time
+- Import plants from JSON files and preview them before loading
+- Configure controllers, bindings, and setpoints from the UI
-> [!NOTE]
-> [Opcional] Crie um ambiente virtual e o ative para instalação do script.
+## Documentation
-Para instalar o plotter:
+- English: [docs/en/index.md](docs/en/index.md)
+- Português (Brasil): [docs/pt-BR/index.md](docs/pt-BR/index.md)
-```shell
-cd serial-plotter
-pip install -e .
-```
+## Quick Start
-## Como usar
+If you are running the app from source:
-Para executar o `splot`, utilize o seguinte comando:
+1. Install the frontend dependencies inside `apps/desktop`
+2. Start the desktop app with Tauri
+3. Create or import plugins
+4. Create or import a plant
+5. Connect the plant and monitor the charts
-```sh
-cd examples
-python main.py
-```
+The current frontend scripts live in `apps/desktop/package.json`, including `pnpm --dir apps/desktop tauri dev`.
-## Modos
+## Documentation Guide
-### Plotter (Execução em tempo real)
-
-- Exibe os dados do controlador e da planta em tempo real.
-- Permite selecionar e configurar diferentes controladores.
-- Mostra a resposta da planta ao longo do tempo.
-
-### Analyzer (Análise de logs)
-
-- Permite carregar **logs de execuções anteriores**.
-- Plota **temperatura e derivada dT/dt** em gráficos interativos.
-- Indica **pontos críticos** (ex.: ponto de maior derivada).
-- Possibilita comparar diferentes execuções.
-
-## Funcionalidades do Plotter
-
-- Selecionar o controlador ativo e editá-lo em tempo real.
-- Ajustar parâmetros do controlador, como **Setpoint, Kp, Ki, Kd**.
-- Visualizar a resposta da planta em gráficos interativos.
-- Alternar entre os modos de exibição para melhor visualização.
-
-## Funcionalidades do Analyzer
-- Carregar e analisar logs de execuções anteriores.
-- Fazer analise em malha aberta para calcular parâmetros de sintonia
-- Fazer analise em malha fechada par analisar a resposta do sistema
-
-## Navegação
-
-- Space → Alterna entre as visões (`Plotter` e `Analyzer`).
-- Escape → Finaliza o programa.
-- **Campo de entrada** → Define a temperatura desejada (`Setpoint`) e a envia ao controlador.
-
-## Estrutura do Projeto
-
-```sh
-serial-plotter/
-├── controller_framework/
-│ ├── core/ # Lógica principal do framework
-│ ├── gui/ # Interface gráfica (Plotter e Analyzer)
-│ ├── __init__.py
-│ └── ...
-├── examples/
-│ ├── main.py # Arquivo principal para rodar o splot
-│ ├── temp_logs/ # Pasta com logs de execuções anteriores
-├── pyproject.toml # Configuração do pacote
-```
-
-## Exemplo de Uso
-
-1. **Executar o `splot`**
- ```sh
- cd examples
- python main.py
- ```
-2. **Selecionar o controlador ativo** no menu lateral.
-3. **Editar os parâmetros** (Setpoint, Kp, Ki, Kd).
-4. **Visualizar a resposta da planta** no gráfico.
-5. **Alternar para `Analyzer`** para carregar logs anteriores.
-
-## Planta utilizada
-
-
-
-[Link para o modelo 3D](https://cad.onshape.com/documents/2719c8d20779534c7559f55d/w/e520d6a9af3b32d2f18ef8f3/e/bb6b8d18dfe883fe6632567b).
+- Start with [Getting Started](docs/en/getting-started.md)
+- Learn the vocabulary in [Core Concepts](docs/en/core-concepts.md)
+- Use [Plants](docs/en/plants.md) for plant lifecycle and runtime actions
+- Use [Drivers and Controllers](docs/en/drivers-and-controllers.md) to understand plugins and live control
+- Use [Plugin File Format](docs/en/plugin-file-format.md) for JSON and Python basics
diff --git a/apps/desktop/example_data.csv b/apps/desktop/example_data.csv
new file mode 100644
index 0000000..138ff1d
--- /dev/null
+++ b/apps/desktop/example_data.csv
@@ -0,0 +1,52 @@
+seconds,sensor_0,actuator_0,target_0,sensor_1,actuator_1,target_1
+0.0,20.1,45.2,20.0,30.5,55.1,30.0
+0.1,20.3,46.8,20.0,30.8,56.3,30.0
+0.2,20.5,48.1,20.0,31.2,57.8,30.0
+0.3,20.8,49.5,20.0,31.5,59.2,30.0
+0.4,21.1,50.8,20.0,31.9,60.5,30.0
+0.5,21.4,52.0,20.0,32.2,61.7,30.0
+0.6,21.7,53.1,20.0,32.6,62.9,30.0
+0.7,22.0,54.1,20.0,32.9,63.9,30.0
+0.8,22.2,54.9,20.0,33.2,64.8,30.0
+0.9,22.5,55.6,20.0,33.5,65.6,30.0
+1.0,22.7,56.2,20.0,33.7,66.2,30.0
+1.1,22.9,56.6,20.0,33.9,66.7,30.0
+1.2,23.1,56.9,20.0,34.1,67.1,30.0
+1.3,23.2,57.1,20.0,34.2,67.3,30.0
+1.4,23.3,57.2,20.0,34.3,67.4,30.0
+1.5,23.4,57.2,20.0,34.4,67.4,30.0
+1.6,23.5,57.1,20.0,34.4,67.3,30.0
+1.7,23.5,56.9,20.0,34.4,67.0,30.0
+1.8,23.5,56.6,20.0,34.4,66.7,30.0
+1.9,23.5,56.2,20.0,34.3,66.2,30.0
+2.0,23.4,55.8,20.0,34.2,65.7,30.0
+2.1,23.3,55.2,20.0,34.1,65.1,30.0
+2.2,23.2,54.6,20.0,33.9,64.4,30.0
+2.3,23.1,53.9,20.0,33.7,63.6,30.0
+2.4,22.9,53.2,20.0,33.5,62.8,30.0
+2.5,22.7,52.4,20.0,33.2,61.9,30.0
+2.6,22.5,51.6,20.0,32.9,60.9,30.0
+2.7,22.3,50.7,20.0,32.6,59.9,30.0
+2.8,22.0,49.8,20.0,32.3,58.9,30.0
+2.9,21.8,48.9,20.0,31.9,57.8,30.0
+3.0,21.5,47.9,20.0,31.6,56.7,30.0
+3.1,21.2,46.9,20.0,31.2,55.6,30.0
+3.2,20.9,45.9,20.0,30.8,54.5,30.0
+3.3,20.6,44.9,20.0,30.4,53.4,30.0
+3.4,20.3,43.9,20.0,30.0,52.3,30.0
+3.5,20.0,42.9,20.0,29.6,51.2,30.0
+3.6,19.7,41.9,20.0,29.2,50.1,30.0
+3.7,19.4,40.9,20.0,28.8,49.1,30.0
+3.8,19.1,40.0,20.0,28.4,48.1,30.0
+3.9,18.8,39.1,20.0,28.0,47.1,30.0
+4.0,18.6,38.3,20.0,27.6,46.2,30.0
+4.1,18.3,37.5,20.0,27.2,45.3,30.0
+4.2,18.1,36.8,20.0,26.8,44.5,30.0
+4.3,17.9,36.1,20.0,26.5,43.7,30.0
+4.4,17.7,35.5,20.0,26.1,43.0,30.0
+4.5,17.5,34.9,20.0,25.8,42.3,30.0
+4.6,17.3,34.4,20.0,25.5,41.7,30.0
+4.7,17.2,34.0,20.0,25.2,41.2,30.0
+4.8,17.1,33.6,20.0,24.9,40.7,30.0
+4.9,17.0,33.3,20.0,24.7,40.3,30.0
+5.0,16.9,33.0,20.0,24.5,40.0,30.0
diff --git a/apps/desktop/package.json b/apps/desktop/package.json
new file mode 100644
index 0000000..4105b28
--- /dev/null
+++ b/apps/desktop/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "senamby",
+ "version": "0.1.0",
+ "description": "Senamby desktop plotter workspace",
+ "type": "module",
+ "packageManager": "pnpm@10.27.0",
+ "scripts": {
+ "dev": "vite dev",
+ "build": "vite build",
+ "preview": "vite preview",
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
+ "tauri": "tauri"
+ },
+ "license": "MIT",
+ "dependencies": {
+ "@tauri-apps/api": "^2",
+ "@tauri-apps/plugin-dialog": "^2.6.0",
+ "@tauri-apps/plugin-opener": "^2",
+ "highlight.js": "^11.11.1",
+ "lucide-svelte": "^0.562.0",
+ "uplot": "^1.6.32"
+ },
+ "devDependencies": {
+ "@sveltejs/adapter-static": "^3.0.6",
+ "@sveltejs/kit": "^2.9.0",
+ "@sveltejs/vite-plugin-svelte": "^5.0.0",
+ "@tailwindcss/forms": "^0.5.10",
+ "@tailwindcss/vite": "^4.1.17",
+ "@tauri-apps/cli": "^2",
+ "svelte": "^5.0.0",
+ "svelte-check": "^4.0.0",
+ "tailwindcss": "^4.1.17",
+ "typescript": "^5",
+ "vite": "^6.0.3"
+ }
+}
diff --git a/apps/desktop/pnpm-lock.yaml b/apps/desktop/pnpm-lock.yaml
new file mode 100644
index 0000000..cbcf14c
--- /dev/null
+++ b/apps/desktop/pnpm-lock.yaml
@@ -0,0 +1,1539 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ '@tauri-apps/api':
+ specifier: ^2
+ version: 2.9.1
+ '@tauri-apps/plugin-dialog':
+ specifier: ^2.6.0
+ version: 2.6.0
+ '@tauri-apps/plugin-opener':
+ specifier: ^2
+ version: 2.5.2
+ highlight.js:
+ specifier: ^11.11.1
+ version: 11.11.1
+ lucide-svelte:
+ specifier: ^0.562.0
+ version: 0.562.0(svelte@5.46.1)
+ uplot:
+ specifier: ^1.6.32
+ version: 1.6.32
+ devDependencies:
+ '@sveltejs/adapter-static':
+ specifier: ^3.0.6
+ version: 3.0.10(@sveltejs/kit@2.49.3(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.46.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.46.1)(typescript@5.6.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)))
+ '@sveltejs/kit':
+ specifier: ^2.9.0
+ version: 2.49.3(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.46.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.46.1)(typescript@5.6.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2))
+ '@sveltejs/vite-plugin-svelte':
+ specifier: ^5.0.0
+ version: 5.1.1(svelte@5.46.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2))
+ '@tailwindcss/forms':
+ specifier: ^0.5.10
+ version: 0.5.11(tailwindcss@4.1.18)
+ '@tailwindcss/vite':
+ specifier: ^4.1.17
+ version: 4.1.18(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2))
+ '@tauri-apps/cli':
+ specifier: ^2
+ version: 2.9.6
+ svelte:
+ specifier: ^5.0.0
+ version: 5.46.1
+ svelte-check:
+ specifier: ^4.0.0
+ version: 4.3.5(picomatch@4.0.3)(svelte@5.46.1)(typescript@5.6.3)
+ tailwindcss:
+ specifier: ^4.1.17
+ version: 4.1.18
+ typescript:
+ specifier: ^5
+ version: 5.6.3
+ vite:
+ specifier: ^6.0.3
+ version: 6.4.1(jiti@2.6.1)(lightningcss@1.30.2)
+
+packages:
+
+ '@esbuild/aix-ppc64@0.25.12':
+ resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/android-arm64@0.25.12':
+ resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm@0.25.12':
+ resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.25.12':
+ resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.25.12':
+ resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.25.12':
+ resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.25.12':
+ resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.25.12':
+ resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/linux-arm64@0.25.12':
+ resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.25.12':
+ resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.25.12':
+ resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.25.12':
+ resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.25.12':
+ resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.25.12':
+ resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.25.12':
+ resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.25.12':
+ resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.25.12':
+ resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/netbsd-arm64@0.25.12':
+ resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.25.12':
+ resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/openbsd-arm64@0.25.12':
+ resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.25.12':
+ resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/openharmony-arm64@0.25.12':
+ resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@esbuild/sunos-x64@0.25.12':
+ resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.25.12':
+ resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.25.12':
+ resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.25.12':
+ resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
+ '@jridgewell/gen-mapping@0.3.13':
+ resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
+
+ '@jridgewell/remapping@2.3.5':
+ resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
+
+ '@jridgewell/resolve-uri@3.1.2':
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/sourcemap-codec@1.5.5':
+ resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+
+ '@polka/url@1.0.0-next.29':
+ resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
+
+ '@rollup/rollup-android-arm-eabi@4.55.1':
+ resolution: {integrity: sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==}
+ cpu: [arm]
+ os: [android]
+
+ '@rollup/rollup-android-arm64@4.55.1':
+ resolution: {integrity: sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==}
+ cpu: [arm64]
+ os: [android]
+
+ '@rollup/rollup-darwin-arm64@4.55.1':
+ resolution: {integrity: sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rollup/rollup-darwin-x64@4.55.1':
+ resolution: {integrity: sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rollup/rollup-freebsd-arm64@4.55.1':
+ resolution: {integrity: sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@rollup/rollup-freebsd-x64@4.55.1':
+ resolution: {integrity: sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.55.1':
+ resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==}
+ cpu: [arm]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm-musleabihf@4.55.1':
+ resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==}
+ cpu: [arm]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm64-gnu@4.55.1':
+ resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-arm64-musl@4.55.1':
+ resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rollup/rollup-linux-loong64-gnu@4.55.1':
+ resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==}
+ cpu: [loong64]
+ os: [linux]
+
+ '@rollup/rollup-linux-loong64-musl@4.55.1':
+ resolution: {integrity: sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==}
+ cpu: [loong64]
+ os: [linux]
+
+ '@rollup/rollup-linux-ppc64-gnu@4.55.1':
+ resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@rollup/rollup-linux-ppc64-musl@4.55.1':
+ resolution: {integrity: sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@rollup/rollup-linux-riscv64-gnu@4.55.1':
+ resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@rollup/rollup-linux-riscv64-musl@4.55.1':
+ resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@rollup/rollup-linux-s390x-gnu@4.55.1':
+ resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==}
+ cpu: [s390x]
+ os: [linux]
+
+ '@rollup/rollup-linux-x64-gnu@4.55.1':
+ resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==}
+ cpu: [x64]
+ os: [linux]
+
+ '@rollup/rollup-linux-x64-musl@4.55.1':
+ resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==}
+ cpu: [x64]
+ os: [linux]
+
+ '@rollup/rollup-openbsd-x64@4.55.1':
+ resolution: {integrity: sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@rollup/rollup-openharmony-arm64@4.55.1':
+ resolution: {integrity: sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rollup/rollup-win32-arm64-msvc@4.55.1':
+ resolution: {integrity: sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rollup/rollup-win32-ia32-msvc@4.55.1':
+ resolution: {integrity: sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==}
+ cpu: [ia32]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-gnu@4.55.1':
+ resolution: {integrity: sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==}
+ cpu: [x64]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-msvc@4.55.1':
+ resolution: {integrity: sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==}
+ cpu: [x64]
+ os: [win32]
+
+ '@standard-schema/spec@1.1.0':
+ resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
+
+ '@sveltejs/acorn-typescript@1.0.8':
+ resolution: {integrity: sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA==}
+ peerDependencies:
+ acorn: ^8.9.0
+
+ '@sveltejs/adapter-static@3.0.10':
+ resolution: {integrity: sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==}
+ peerDependencies:
+ '@sveltejs/kit': ^2.0.0
+
+ '@sveltejs/kit@2.49.3':
+ resolution: {integrity: sha512-luTmE2Isk9GRJnitqanLoByKBiyLdfLpV2qV9a25JMxjbQt919TVqG8pibJDkxTvX9+w2k/9IL7o+/RtG++3QA==}
+ engines: {node: '>=18.13'}
+ hasBin: true
+ peerDependencies:
+ '@opentelemetry/api': ^1.0.0
+ '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0
+ svelte: ^4.0.0 || ^5.0.0-next.0
+ typescript: ^5.3.3
+ vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0
+ peerDependenciesMeta:
+ '@opentelemetry/api':
+ optional: true
+ typescript:
+ optional: true
+
+ '@sveltejs/vite-plugin-svelte-inspector@4.0.1':
+ resolution: {integrity: sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==}
+ engines: {node: ^18.0.0 || ^20.0.0 || >=22}
+ peerDependencies:
+ '@sveltejs/vite-plugin-svelte': ^5.0.0
+ svelte: ^5.0.0
+ vite: ^6.0.0
+
+ '@sveltejs/vite-plugin-svelte@5.1.1':
+ resolution: {integrity: sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==}
+ engines: {node: ^18.0.0 || ^20.0.0 || >=22}
+ peerDependencies:
+ svelte: ^5.0.0
+ vite: ^6.0.0
+
+ '@tailwindcss/forms@0.5.11':
+ resolution: {integrity: sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA==}
+ peerDependencies:
+ tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1'
+
+ '@tailwindcss/node@4.1.18':
+ resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==}
+
+ '@tailwindcss/oxide-android-arm64@4.1.18':
+ resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [android]
+
+ '@tailwindcss/oxide-darwin-arm64@4.1.18':
+ resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-darwin-x64@4.1.18':
+ resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@tailwindcss/oxide-freebsd-x64@4.1.18':
+ resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18':
+ resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==}
+ engines: {node: '>= 10'}
+ cpu: [arm]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.1.18':
+ resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.1.18':
+ resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.1.18':
+ resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@tailwindcss/oxide-linux-x64-musl@4.1.18':
+ resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@tailwindcss/oxide-wasm32-wasi@4.1.18':
+ resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==}
+ engines: {node: '>=14.0.0'}
+ cpu: [wasm32]
+ bundledDependencies:
+ - '@napi-rs/wasm-runtime'
+ - '@emnapi/core'
+ - '@emnapi/runtime'
+ - '@tybys/wasm-util'
+ - '@emnapi/wasi-threads'
+ - tslib
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.1.18':
+ resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.1.18':
+ resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [win32]
+
+ '@tailwindcss/oxide@4.1.18':
+ resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==}
+ engines: {node: '>= 10'}
+
+ '@tailwindcss/vite@4.1.18':
+ resolution: {integrity: sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==}
+ peerDependencies:
+ vite: ^5.2.0 || ^6 || ^7
+
+ '@tauri-apps/api@2.9.1':
+ resolution: {integrity: sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw==}
+
+ '@tauri-apps/cli-darwin-arm64@2.9.6':
+ resolution: {integrity: sha512-gf5no6N9FCk1qMrti4lfwP77JHP5haASZgVbBgpZG7BUepB3fhiLCXGUK8LvuOjP36HivXewjg72LTnPDScnQQ==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@tauri-apps/cli-darwin-x64@2.9.6':
+ resolution: {integrity: sha512-oWh74WmqbERwwrwcueJyY6HYhgCksUc6NT7WKeXyrlY/FPmNgdyQAgcLuTSkhRFuQ6zh4Np1HZpOqCTpeZBDcw==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@tauri-apps/cli-linux-arm-gnueabihf@2.9.6':
+ resolution: {integrity: sha512-/zde3bFroFsNXOHN204DC2qUxAcAanUjVXXSdEGmhwMUZeAQalNj5cz2Qli2elsRjKN/hVbZOJj0gQ5zaYUjSg==}
+ engines: {node: '>= 10'}
+ cpu: [arm]
+ os: [linux]
+
+ '@tauri-apps/cli-linux-arm64-gnu@2.9.6':
+ resolution: {integrity: sha512-pvbljdhp9VOo4RnID5ywSxgBs7qiylTPlK56cTk7InR3kYSTJKYMqv/4Q/4rGo/mG8cVppesKIeBMH42fw6wjg==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@tauri-apps/cli-linux-arm64-musl@2.9.6':
+ resolution: {integrity: sha512-02TKUndpodXBCR0oP//6dZWGYcc22Upf2eP27NvC6z0DIqvkBBFziQUcvi2n6SrwTRL0yGgQjkm9K5NIn8s6jw==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@tauri-apps/cli-linux-riscv64-gnu@2.9.6':
+ resolution: {integrity: sha512-fmp1hnulbqzl1GkXl4aTX9fV+ubHw2LqlLH1PE3BxZ11EQk+l/TmiEongjnxF0ie4kV8DQfDNJ1KGiIdWe1GvQ==}
+ engines: {node: '>= 10'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@tauri-apps/cli-linux-x64-gnu@2.9.6':
+ resolution: {integrity: sha512-vY0le8ad2KaV1PJr+jCd8fUF9VOjwwQP/uBuTJvhvKTloEwxYA/kAjKK9OpIslGA9m/zcnSo74czI6bBrm2sYA==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@tauri-apps/cli-linux-x64-musl@2.9.6':
+ resolution: {integrity: sha512-TOEuB8YCFZTWVDzsO2yW0+zGcoMiPPwcUgdnW1ODnmgfwccpnihDRoks+ABT1e3fHb1ol8QQWsHSCovb3o2ENQ==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
+ '@tauri-apps/cli-win32-arm64-msvc@2.9.6':
+ resolution: {integrity: sha512-ujmDGMRc4qRLAnj8nNG26Rlz9klJ0I0jmZs2BPpmNNf0gM/rcVHhqbEkAaHPTBVIrtUdf7bGvQAD2pyIiUrBHQ==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@tauri-apps/cli-win32-ia32-msvc@2.9.6':
+ resolution: {integrity: sha512-S4pT0yAJgFX8QRCyKA1iKjZ9Q/oPjCZf66A/VlG5Yw54Nnr88J1uBpmenINbXxzyhduWrIXBaUbEY1K80ZbpMg==}
+ engines: {node: '>= 10'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@tauri-apps/cli-win32-x64-msvc@2.9.6':
+ resolution: {integrity: sha512-ldWuWSSkWbKOPjQMJoYVj9wLHcOniv7diyI5UAJ4XsBdtaFB0pKHQsqw/ItUma0VXGC7vB4E9fZjivmxur60aw==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [win32]
+
+ '@tauri-apps/cli@2.9.6':
+ resolution: {integrity: sha512-3xDdXL5omQ3sPfBfdC8fCtDKcnyV7OqyzQgfyT5P3+zY6lcPqIYKQBvUasNvppi21RSdfhy44ttvJmftb0PCDw==}
+ engines: {node: '>= 10'}
+ hasBin: true
+
+ '@tauri-apps/plugin-dialog@2.6.0':
+ resolution: {integrity: sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg==}
+
+ '@tauri-apps/plugin-opener@2.5.2':
+ resolution: {integrity: sha512-ei/yRRoCklWHImwpCcDK3VhNXx+QXM9793aQ64YxpqVF0BDuuIlXhZgiAkc15wnPVav+IbkYhmDJIv5R326Mew==}
+
+ '@types/cookie@0.6.0':
+ resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
+
+ '@types/estree@1.0.8':
+ resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
+
+ acorn@8.15.0:
+ resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ aria-query@5.3.2:
+ resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
+ engines: {node: '>= 0.4'}
+
+ axobject-query@4.1.0:
+ resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
+ engines: {node: '>= 0.4'}
+
+ chokidar@4.0.3:
+ resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
+ engines: {node: '>= 14.16.0'}
+
+ clsx@2.1.1:
+ resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
+ engines: {node: '>=6'}
+
+ cookie@0.6.0:
+ resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
+ engines: {node: '>= 0.6'}
+
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ deepmerge@4.3.1:
+ resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
+ engines: {node: '>=0.10.0'}
+
+ detect-libc@2.1.2:
+ resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
+ engines: {node: '>=8'}
+
+ devalue@5.6.1:
+ resolution: {integrity: sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A==}
+
+ enhanced-resolve@5.18.4:
+ resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==}
+ engines: {node: '>=10.13.0'}
+
+ esbuild@0.25.12:
+ resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ esm-env@1.2.2:
+ resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==}
+
+ esrap@2.2.1:
+ resolution: {integrity: sha512-GiYWG34AN/4CUyaWAgunGt0Rxvr1PTMlGC0vvEov/uOQYWne2bpN03Um+k8jT+q3op33mKouP2zeJ6OlM+qeUg==}
+
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ graceful-fs@4.2.11:
+ resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+
+ highlight.js@11.11.1:
+ resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
+ engines: {node: '>=12.0.0'}
+
+ is-reference@3.0.3:
+ resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==}
+
+ jiti@2.6.1:
+ resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
+ hasBin: true
+
+ kleur@4.1.5:
+ resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
+ engines: {node: '>=6'}
+
+ lightningcss-android-arm64@1.30.2:
+ resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [android]
+
+ lightningcss-darwin-arm64@1.30.2:
+ resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ lightningcss-darwin-x64@1.30.2:
+ resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ lightningcss-freebsd-x64@1.30.2:
+ resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ lightningcss-linux-arm-gnueabihf@1.30.2:
+ resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ lightningcss-linux-arm64-gnu@1.30.2:
+ resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ lightningcss-linux-arm64-musl@1.30.2:
+ resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ lightningcss-linux-x64-gnu@1.30.2:
+ resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ lightningcss-linux-x64-musl@1.30.2:
+ resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ lightningcss-win32-arm64-msvc@1.30.2:
+ resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ lightningcss-win32-x64-msvc@1.30.2:
+ resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ lightningcss@1.30.2:
+ resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==}
+ engines: {node: '>= 12.0.0'}
+
+ locate-character@3.0.0:
+ resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
+
+ lucide-svelte@0.562.0:
+ resolution: {integrity: sha512-kSJDH/55lf0mun/o4nqWBXOcq0fWYzPeIjbTD97ywoeumAB9kWxtM06gC7oynqjtK3XhAljWSz5RafIzPEYIQA==}
+ peerDependencies:
+ svelte: ^3 || ^4 || ^5.0.0-next.42
+
+ magic-string@0.30.21:
+ resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+
+ mini-svg-data-uri@1.4.4:
+ resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==}
+ hasBin: true
+
+ mri@1.2.0:
+ resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
+ engines: {node: '>=4'}
+
+ mrmime@2.0.1:
+ resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
+ engines: {node: '>=10'}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ nanoid@3.3.11:
+ resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@4.0.3:
+ resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
+ engines: {node: '>=12'}
+
+ postcss@8.5.6:
+ resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ readdirp@4.1.2:
+ resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
+ engines: {node: '>= 14.18.0'}
+
+ rollup@4.55.1:
+ resolution: {integrity: sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==}
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+ hasBin: true
+
+ sade@1.8.1:
+ resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
+ engines: {node: '>=6'}
+
+ set-cookie-parser@2.7.2:
+ resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==}
+
+ sirv@3.0.2:
+ resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==}
+ engines: {node: '>=18'}
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ svelte-check@4.3.5:
+ resolution: {integrity: sha512-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q==}
+ engines: {node: '>= 18.0.0'}
+ hasBin: true
+ peerDependencies:
+ svelte: ^4.0.0 || ^5.0.0-next.0
+ typescript: '>=5.0.0'
+
+ svelte@5.46.1:
+ resolution: {integrity: sha512-ynjfCHD3nP2el70kN5Pmg37sSi0EjOm9FgHYQdC4giWG/hzO3AatzXXJJgP305uIhGQxSufJLuYWtkY8uK/8RA==}
+ engines: {node: '>=18'}
+
+ tailwindcss@4.1.18:
+ resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==}
+
+ tapable@2.3.0:
+ resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
+ engines: {node: '>=6'}
+
+ tinyglobby@0.2.15:
+ resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
+ engines: {node: '>=12.0.0'}
+
+ totalist@3.0.1:
+ resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
+ engines: {node: '>=6'}
+
+ typescript@5.6.3:
+ resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ uplot@1.6.32:
+ resolution: {integrity: sha512-KIMVnG68zvu5XXUbC4LQEPnhwOxBuLyW1AHtpm6IKTXImkbLgkMy+jabjLgSLMasNuGGzQm/ep3tOkyTxpiQIw==}
+
+ vite@6.4.1:
+ resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==}
+ engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
+ jiti: '>=1.21.0'
+ less: '*'
+ lightningcss: ^1.21.0
+ sass: '*'
+ sass-embedded: '*'
+ stylus: '*'
+ sugarss: '*'
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ lightningcss:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+ vitefu@1.1.1:
+ resolution: {integrity: sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==}
+ peerDependencies:
+ vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0
+ peerDependenciesMeta:
+ vite:
+ optional: true
+
+ zimmerframe@1.1.4:
+ resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==}
+
+snapshots:
+
+ '@esbuild/aix-ppc64@0.25.12':
+ optional: true
+
+ '@esbuild/android-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/android-arm@0.25.12':
+ optional: true
+
+ '@esbuild/android-x64@0.25.12':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/darwin-x64@0.25.12':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-arm@0.25.12':
+ optional: true
+
+ '@esbuild/linux-ia32@0.25.12':
+ optional: true
+
+ '@esbuild/linux-loong64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.25.12':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.25.12':
+ optional: true
+
+ '@esbuild/linux-s390x@0.25.12':
+ optional: true
+
+ '@esbuild/linux-x64@0.25.12':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.25.12':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.25.12':
+ optional: true
+
+ '@esbuild/openharmony-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/sunos-x64@0.25.12':
+ optional: true
+
+ '@esbuild/win32-arm64@0.25.12':
+ optional: true
+
+ '@esbuild/win32-ia32@0.25.12':
+ optional: true
+
+ '@esbuild/win32-x64@0.25.12':
+ optional: true
+
+ '@jridgewell/gen-mapping@0.3.13':
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/remapping@2.3.5':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/resolve-uri@3.1.2': {}
+
+ '@jridgewell/sourcemap-codec@1.5.5': {}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ '@polka/url@1.0.0-next.29': {}
+
+ '@rollup/rollup-android-arm-eabi@4.55.1':
+ optional: true
+
+ '@rollup/rollup-android-arm64@4.55.1':
+ optional: true
+
+ '@rollup/rollup-darwin-arm64@4.55.1':
+ optional: true
+
+ '@rollup/rollup-darwin-x64@4.55.1':
+ optional: true
+
+ '@rollup/rollup-freebsd-arm64@4.55.1':
+ optional: true
+
+ '@rollup/rollup-freebsd-x64@4.55.1':
+ optional: true
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.55.1':
+ optional: true
+
+ '@rollup/rollup-linux-arm-musleabihf@4.55.1':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-gnu@4.55.1':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-musl@4.55.1':
+ optional: true
+
+ '@rollup/rollup-linux-loong64-gnu@4.55.1':
+ optional: true
+
+ '@rollup/rollup-linux-loong64-musl@4.55.1':
+ optional: true
+
+ '@rollup/rollup-linux-ppc64-gnu@4.55.1':
+ optional: true
+
+ '@rollup/rollup-linux-ppc64-musl@4.55.1':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-gnu@4.55.1':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-musl@4.55.1':
+ optional: true
+
+ '@rollup/rollup-linux-s390x-gnu@4.55.1':
+ optional: true
+
+ '@rollup/rollup-linux-x64-gnu@4.55.1':
+ optional: true
+
+ '@rollup/rollup-linux-x64-musl@4.55.1':
+ optional: true
+
+ '@rollup/rollup-openbsd-x64@4.55.1':
+ optional: true
+
+ '@rollup/rollup-openharmony-arm64@4.55.1':
+ optional: true
+
+ '@rollup/rollup-win32-arm64-msvc@4.55.1':
+ optional: true
+
+ '@rollup/rollup-win32-ia32-msvc@4.55.1':
+ optional: true
+
+ '@rollup/rollup-win32-x64-gnu@4.55.1':
+ optional: true
+
+ '@rollup/rollup-win32-x64-msvc@4.55.1':
+ optional: true
+
+ '@standard-schema/spec@1.1.0': {}
+
+ '@sveltejs/acorn-typescript@1.0.8(acorn@8.15.0)':
+ dependencies:
+ acorn: 8.15.0
+
+ '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.49.3(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.46.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.46.1)(typescript@5.6.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)))':
+ dependencies:
+ '@sveltejs/kit': 2.49.3(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.46.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.46.1)(typescript@5.6.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2))
+
+ '@sveltejs/kit@2.49.3(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.46.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.46.1)(typescript@5.6.3)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2))':
+ dependencies:
+ '@standard-schema/spec': 1.1.0
+ '@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0)
+ '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.46.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2))
+ '@types/cookie': 0.6.0
+ acorn: 8.15.0
+ cookie: 0.6.0
+ devalue: 5.6.1
+ esm-env: 1.2.2
+ kleur: 4.1.5
+ magic-string: 0.30.21
+ mrmime: 2.0.1
+ sade: 1.8.1
+ set-cookie-parser: 2.7.2
+ sirv: 3.0.2
+ svelte: 5.46.1
+ vite: 6.4.1(jiti@2.6.1)(lightningcss@1.30.2)
+ optionalDependencies:
+ typescript: 5.6.3
+
+ '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.46.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.46.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2))':
+ dependencies:
+ '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.46.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2))
+ debug: 4.4.3
+ svelte: 5.46.1
+ vite: 6.4.1(jiti@2.6.1)(lightningcss@1.30.2)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.46.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2))':
+ dependencies:
+ '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.46.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.46.1)(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2))
+ debug: 4.4.3
+ deepmerge: 4.3.1
+ kleur: 4.1.5
+ magic-string: 0.30.21
+ svelte: 5.46.1
+ vite: 6.4.1(jiti@2.6.1)(lightningcss@1.30.2)
+ vitefu: 1.1.1(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2))
+ transitivePeerDependencies:
+ - supports-color
+
+ '@tailwindcss/forms@0.5.11(tailwindcss@4.1.18)':
+ dependencies:
+ mini-svg-data-uri: 1.4.4
+ tailwindcss: 4.1.18
+
+ '@tailwindcss/node@4.1.18':
+ dependencies:
+ '@jridgewell/remapping': 2.3.5
+ enhanced-resolve: 5.18.4
+ jiti: 2.6.1
+ lightningcss: 1.30.2
+ magic-string: 0.30.21
+ source-map-js: 1.2.1
+ tailwindcss: 4.1.18
+
+ '@tailwindcss/oxide-android-arm64@4.1.18':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-arm64@4.1.18':
+ optional: true
+
+ '@tailwindcss/oxide-darwin-x64@4.1.18':
+ optional: true
+
+ '@tailwindcss/oxide-freebsd-x64@4.1.18':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-gnu@4.1.18':
+ optional: true
+
+ '@tailwindcss/oxide-linux-arm64-musl@4.1.18':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-gnu@4.1.18':
+ optional: true
+
+ '@tailwindcss/oxide-linux-x64-musl@4.1.18':
+ optional: true
+
+ '@tailwindcss/oxide-wasm32-wasi@4.1.18':
+ optional: true
+
+ '@tailwindcss/oxide-win32-arm64-msvc@4.1.18':
+ optional: true
+
+ '@tailwindcss/oxide-win32-x64-msvc@4.1.18':
+ optional: true
+
+ '@tailwindcss/oxide@4.1.18':
+ optionalDependencies:
+ '@tailwindcss/oxide-android-arm64': 4.1.18
+ '@tailwindcss/oxide-darwin-arm64': 4.1.18
+ '@tailwindcss/oxide-darwin-x64': 4.1.18
+ '@tailwindcss/oxide-freebsd-x64': 4.1.18
+ '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18
+ '@tailwindcss/oxide-linux-arm64-gnu': 4.1.18
+ '@tailwindcss/oxide-linux-arm64-musl': 4.1.18
+ '@tailwindcss/oxide-linux-x64-gnu': 4.1.18
+ '@tailwindcss/oxide-linux-x64-musl': 4.1.18
+ '@tailwindcss/oxide-wasm32-wasi': 4.1.18
+ '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18
+ '@tailwindcss/oxide-win32-x64-msvc': 4.1.18
+
+ '@tailwindcss/vite@4.1.18(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2))':
+ dependencies:
+ '@tailwindcss/node': 4.1.18
+ '@tailwindcss/oxide': 4.1.18
+ tailwindcss: 4.1.18
+ vite: 6.4.1(jiti@2.6.1)(lightningcss@1.30.2)
+
+ '@tauri-apps/api@2.9.1': {}
+
+ '@tauri-apps/cli-darwin-arm64@2.9.6':
+ optional: true
+
+ '@tauri-apps/cli-darwin-x64@2.9.6':
+ optional: true
+
+ '@tauri-apps/cli-linux-arm-gnueabihf@2.9.6':
+ optional: true
+
+ '@tauri-apps/cli-linux-arm64-gnu@2.9.6':
+ optional: true
+
+ '@tauri-apps/cli-linux-arm64-musl@2.9.6':
+ optional: true
+
+ '@tauri-apps/cli-linux-riscv64-gnu@2.9.6':
+ optional: true
+
+ '@tauri-apps/cli-linux-x64-gnu@2.9.6':
+ optional: true
+
+ '@tauri-apps/cli-linux-x64-musl@2.9.6':
+ optional: true
+
+ '@tauri-apps/cli-win32-arm64-msvc@2.9.6':
+ optional: true
+
+ '@tauri-apps/cli-win32-ia32-msvc@2.9.6':
+ optional: true
+
+ '@tauri-apps/cli-win32-x64-msvc@2.9.6':
+ optional: true
+
+ '@tauri-apps/cli@2.9.6':
+ optionalDependencies:
+ '@tauri-apps/cli-darwin-arm64': 2.9.6
+ '@tauri-apps/cli-darwin-x64': 2.9.6
+ '@tauri-apps/cli-linux-arm-gnueabihf': 2.9.6
+ '@tauri-apps/cli-linux-arm64-gnu': 2.9.6
+ '@tauri-apps/cli-linux-arm64-musl': 2.9.6
+ '@tauri-apps/cli-linux-riscv64-gnu': 2.9.6
+ '@tauri-apps/cli-linux-x64-gnu': 2.9.6
+ '@tauri-apps/cli-linux-x64-musl': 2.9.6
+ '@tauri-apps/cli-win32-arm64-msvc': 2.9.6
+ '@tauri-apps/cli-win32-ia32-msvc': 2.9.6
+ '@tauri-apps/cli-win32-x64-msvc': 2.9.6
+
+ '@tauri-apps/plugin-dialog@2.6.0':
+ dependencies:
+ '@tauri-apps/api': 2.9.1
+
+ '@tauri-apps/plugin-opener@2.5.2':
+ dependencies:
+ '@tauri-apps/api': 2.9.1
+
+ '@types/cookie@0.6.0': {}
+
+ '@types/estree@1.0.8': {}
+
+ acorn@8.15.0: {}
+
+ aria-query@5.3.2: {}
+
+ axobject-query@4.1.0: {}
+
+ chokidar@4.0.3:
+ dependencies:
+ readdirp: 4.1.2
+
+ clsx@2.1.1: {}
+
+ cookie@0.6.0: {}
+
+ debug@4.4.3:
+ dependencies:
+ ms: 2.1.3
+
+ deepmerge@4.3.1: {}
+
+ detect-libc@2.1.2: {}
+
+ devalue@5.6.1: {}
+
+ enhanced-resolve@5.18.4:
+ dependencies:
+ graceful-fs: 4.2.11
+ tapable: 2.3.0
+
+ esbuild@0.25.12:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.25.12
+ '@esbuild/android-arm': 0.25.12
+ '@esbuild/android-arm64': 0.25.12
+ '@esbuild/android-x64': 0.25.12
+ '@esbuild/darwin-arm64': 0.25.12
+ '@esbuild/darwin-x64': 0.25.12
+ '@esbuild/freebsd-arm64': 0.25.12
+ '@esbuild/freebsd-x64': 0.25.12
+ '@esbuild/linux-arm': 0.25.12
+ '@esbuild/linux-arm64': 0.25.12
+ '@esbuild/linux-ia32': 0.25.12
+ '@esbuild/linux-loong64': 0.25.12
+ '@esbuild/linux-mips64el': 0.25.12
+ '@esbuild/linux-ppc64': 0.25.12
+ '@esbuild/linux-riscv64': 0.25.12
+ '@esbuild/linux-s390x': 0.25.12
+ '@esbuild/linux-x64': 0.25.12
+ '@esbuild/netbsd-arm64': 0.25.12
+ '@esbuild/netbsd-x64': 0.25.12
+ '@esbuild/openbsd-arm64': 0.25.12
+ '@esbuild/openbsd-x64': 0.25.12
+ '@esbuild/openharmony-arm64': 0.25.12
+ '@esbuild/sunos-x64': 0.25.12
+ '@esbuild/win32-arm64': 0.25.12
+ '@esbuild/win32-ia32': 0.25.12
+ '@esbuild/win32-x64': 0.25.12
+
+ esm-env@1.2.2: {}
+
+ esrap@2.2.1:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ fdir@6.5.0(picomatch@4.0.3):
+ optionalDependencies:
+ picomatch: 4.0.3
+
+ fsevents@2.3.3:
+ optional: true
+
+ graceful-fs@4.2.11: {}
+
+ highlight.js@11.11.1: {}
+
+ is-reference@3.0.3:
+ dependencies:
+ '@types/estree': 1.0.8
+
+ jiti@2.6.1: {}
+
+ kleur@4.1.5: {}
+
+ lightningcss-android-arm64@1.30.2:
+ optional: true
+
+ lightningcss-darwin-arm64@1.30.2:
+ optional: true
+
+ lightningcss-darwin-x64@1.30.2:
+ optional: true
+
+ lightningcss-freebsd-x64@1.30.2:
+ optional: true
+
+ lightningcss-linux-arm-gnueabihf@1.30.2:
+ optional: true
+
+ lightningcss-linux-arm64-gnu@1.30.2:
+ optional: true
+
+ lightningcss-linux-arm64-musl@1.30.2:
+ optional: true
+
+ lightningcss-linux-x64-gnu@1.30.2:
+ optional: true
+
+ lightningcss-linux-x64-musl@1.30.2:
+ optional: true
+
+ lightningcss-win32-arm64-msvc@1.30.2:
+ optional: true
+
+ lightningcss-win32-x64-msvc@1.30.2:
+ optional: true
+
+ lightningcss@1.30.2:
+ dependencies:
+ detect-libc: 2.1.2
+ optionalDependencies:
+ lightningcss-android-arm64: 1.30.2
+ lightningcss-darwin-arm64: 1.30.2
+ lightningcss-darwin-x64: 1.30.2
+ lightningcss-freebsd-x64: 1.30.2
+ lightningcss-linux-arm-gnueabihf: 1.30.2
+ lightningcss-linux-arm64-gnu: 1.30.2
+ lightningcss-linux-arm64-musl: 1.30.2
+ lightningcss-linux-x64-gnu: 1.30.2
+ lightningcss-linux-x64-musl: 1.30.2
+ lightningcss-win32-arm64-msvc: 1.30.2
+ lightningcss-win32-x64-msvc: 1.30.2
+
+ locate-character@3.0.0: {}
+
+ lucide-svelte@0.562.0(svelte@5.46.1):
+ dependencies:
+ svelte: 5.46.1
+
+ magic-string@0.30.21:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ mini-svg-data-uri@1.4.4: {}
+
+ mri@1.2.0: {}
+
+ mrmime@2.0.1: {}
+
+ ms@2.1.3: {}
+
+ nanoid@3.3.11: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@4.0.3: {}
+
+ postcss@8.5.6:
+ dependencies:
+ nanoid: 3.3.11
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ readdirp@4.1.2: {}
+
+ rollup@4.55.1:
+ dependencies:
+ '@types/estree': 1.0.8
+ optionalDependencies:
+ '@rollup/rollup-android-arm-eabi': 4.55.1
+ '@rollup/rollup-android-arm64': 4.55.1
+ '@rollup/rollup-darwin-arm64': 4.55.1
+ '@rollup/rollup-darwin-x64': 4.55.1
+ '@rollup/rollup-freebsd-arm64': 4.55.1
+ '@rollup/rollup-freebsd-x64': 4.55.1
+ '@rollup/rollup-linux-arm-gnueabihf': 4.55.1
+ '@rollup/rollup-linux-arm-musleabihf': 4.55.1
+ '@rollup/rollup-linux-arm64-gnu': 4.55.1
+ '@rollup/rollup-linux-arm64-musl': 4.55.1
+ '@rollup/rollup-linux-loong64-gnu': 4.55.1
+ '@rollup/rollup-linux-loong64-musl': 4.55.1
+ '@rollup/rollup-linux-ppc64-gnu': 4.55.1
+ '@rollup/rollup-linux-ppc64-musl': 4.55.1
+ '@rollup/rollup-linux-riscv64-gnu': 4.55.1
+ '@rollup/rollup-linux-riscv64-musl': 4.55.1
+ '@rollup/rollup-linux-s390x-gnu': 4.55.1
+ '@rollup/rollup-linux-x64-gnu': 4.55.1
+ '@rollup/rollup-linux-x64-musl': 4.55.1
+ '@rollup/rollup-openbsd-x64': 4.55.1
+ '@rollup/rollup-openharmony-arm64': 4.55.1
+ '@rollup/rollup-win32-arm64-msvc': 4.55.1
+ '@rollup/rollup-win32-ia32-msvc': 4.55.1
+ '@rollup/rollup-win32-x64-gnu': 4.55.1
+ '@rollup/rollup-win32-x64-msvc': 4.55.1
+ fsevents: 2.3.3
+
+ sade@1.8.1:
+ dependencies:
+ mri: 1.2.0
+
+ set-cookie-parser@2.7.2: {}
+
+ sirv@3.0.2:
+ dependencies:
+ '@polka/url': 1.0.0-next.29
+ mrmime: 2.0.1
+ totalist: 3.0.1
+
+ source-map-js@1.2.1: {}
+
+ svelte-check@4.3.5(picomatch@4.0.3)(svelte@5.46.1)(typescript@5.6.3):
+ dependencies:
+ '@jridgewell/trace-mapping': 0.3.31
+ chokidar: 4.0.3
+ fdir: 6.5.0(picomatch@4.0.3)
+ picocolors: 1.1.1
+ sade: 1.8.1
+ svelte: 5.46.1
+ typescript: 5.6.3
+ transitivePeerDependencies:
+ - picomatch
+
+ svelte@5.46.1:
+ dependencies:
+ '@jridgewell/remapping': 2.3.5
+ '@jridgewell/sourcemap-codec': 1.5.5
+ '@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0)
+ '@types/estree': 1.0.8
+ acorn: 8.15.0
+ aria-query: 5.3.2
+ axobject-query: 4.1.0
+ clsx: 2.1.1
+ devalue: 5.6.1
+ esm-env: 1.2.2
+ esrap: 2.2.1
+ is-reference: 3.0.3
+ locate-character: 3.0.0
+ magic-string: 0.30.21
+ zimmerframe: 1.1.4
+
+ tailwindcss@4.1.18: {}
+
+ tapable@2.3.0: {}
+
+ tinyglobby@0.2.15:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.3)
+ picomatch: 4.0.3
+
+ totalist@3.0.1: {}
+
+ typescript@5.6.3: {}
+
+ uplot@1.6.32: {}
+
+ vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2):
+ dependencies:
+ esbuild: 0.25.12
+ fdir: 6.5.0(picomatch@4.0.3)
+ picomatch: 4.0.3
+ postcss: 8.5.6
+ rollup: 4.55.1
+ tinyglobby: 0.2.15
+ optionalDependencies:
+ fsevents: 2.3.3
+ jiti: 2.6.1
+ lightningcss: 1.30.2
+
+ vitefu@1.1.1(vite@6.4.1(jiti@2.6.1)(lightningcss@1.30.2)):
+ optionalDependencies:
+ vite: 6.4.1(jiti@2.6.1)(lightningcss@1.30.2)
+
+ zimmerframe@1.1.4: {}
diff --git a/apps/desktop/pnpm-workspace.yaml b/apps/desktop/pnpm-workspace.yaml
new file mode 100644
index 0000000..044a857
--- /dev/null
+++ b/apps/desktop/pnpm-workspace.yaml
@@ -0,0 +1,5 @@
+ignoredBuiltDependencies:
+ - esbuild
+onlyBuiltDependencies:
+ - esbuild
+ - '@tailwindcss/oxide'
diff --git a/apps/desktop/src-tauri/.gitignore b/apps/desktop/src-tauri/.gitignore
new file mode 100644
index 0000000..b21bd68
--- /dev/null
+++ b/apps/desktop/src-tauri/.gitignore
@@ -0,0 +1,7 @@
+# Generated by Cargo
+# will have compiled files and executables
+/target/
+
+# Generated by Tauri
+# will have schema files for capabilities auto-completion
+/gen/schemas
diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml
new file mode 100644
index 0000000..982d50f
--- /dev/null
+++ b/apps/desktop/src-tauri/Cargo.toml
@@ -0,0 +1,33 @@
+[package]
+name = "senamby"
+version = "0.1.0"
+description = "Senamby desktop runtime and workspace application"
+authors = ["Senamby"]
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[lib]
+# The `_lib` suffix may seem redundant but it is necessary
+# to make the lib name unique and wouldn't conflict with the bin name.
+# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
+name = "senamby_lib"
+crate-type = ["staticlib", "cdylib", "rlib"]
+
+[build-dependencies]
+tauri-build = { version = "2", features = [] }
+
+[dependencies]
+tauri = { version = "2", features = [] }
+tauri-plugin-dialog = "2"
+tauri-plugin-opener = "2"
+thiserror = "1"
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+uuid = { version = "1", features = ["v4", "serde"] }
+parking_lot = "0.12"
+chrono = { version = "0.4", features = ["serde"] }
+dirs = "5"
+
+[dev-dependencies]
+tauri = { version = "2", features = ["test"] }
diff --git a/apps/desktop/src-tauri/build.rs b/apps/desktop/src-tauri/build.rs
new file mode 100644
index 0000000..261851f
--- /dev/null
+++ b/apps/desktop/src-tauri/build.rs
@@ -0,0 +1,3 @@
+fn main() {
+ tauri_build::build();
+}
diff --git a/apps/desktop/src-tauri/capabilities/default.json b/apps/desktop/src-tauri/capabilities/default.json
new file mode 100644
index 0000000..01a4101
--- /dev/null
+++ b/apps/desktop/src-tauri/capabilities/default.json
@@ -0,0 +1,11 @@
+{
+ "$schema": "../gen/schemas/desktop-schema.json",
+ "identifier": "default",
+ "description": "Capability for the main window",
+ "windows": ["main"],
+ "permissions": [
+ "core:default",
+ "dialog:default",
+ "opener:default"
+ ]
+}
diff --git a/apps/desktop/src-tauri/icons/128x128.png b/apps/desktop/src-tauri/icons/128x128.png
new file mode 100644
index 0000000..6be5e50
Binary files /dev/null and b/apps/desktop/src-tauri/icons/128x128.png differ
diff --git a/apps/desktop/src-tauri/icons/128x128@2x.png b/apps/desktop/src-tauri/icons/128x128@2x.png
new file mode 100644
index 0000000..e81bece
Binary files /dev/null and b/apps/desktop/src-tauri/icons/128x128@2x.png differ
diff --git a/apps/desktop/src-tauri/icons/32x32.png b/apps/desktop/src-tauri/icons/32x32.png
new file mode 100644
index 0000000..a437dd5
Binary files /dev/null and b/apps/desktop/src-tauri/icons/32x32.png differ
diff --git a/apps/desktop/src-tauri/icons/Square107x107Logo.png b/apps/desktop/src-tauri/icons/Square107x107Logo.png
new file mode 100644
index 0000000..0ca4f27
Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square107x107Logo.png differ
diff --git a/apps/desktop/src-tauri/icons/Square142x142Logo.png b/apps/desktop/src-tauri/icons/Square142x142Logo.png
new file mode 100644
index 0000000..b81f820
Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square142x142Logo.png differ
diff --git a/apps/desktop/src-tauri/icons/Square150x150Logo.png b/apps/desktop/src-tauri/icons/Square150x150Logo.png
new file mode 100644
index 0000000..624c7bf
Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square150x150Logo.png differ
diff --git a/apps/desktop/src-tauri/icons/Square284x284Logo.png b/apps/desktop/src-tauri/icons/Square284x284Logo.png
new file mode 100644
index 0000000..c021d2b
Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square284x284Logo.png differ
diff --git a/apps/desktop/src-tauri/icons/Square30x30Logo.png b/apps/desktop/src-tauri/icons/Square30x30Logo.png
new file mode 100644
index 0000000..6219700
Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square30x30Logo.png differ
diff --git a/apps/desktop/src-tauri/icons/Square310x310Logo.png b/apps/desktop/src-tauri/icons/Square310x310Logo.png
new file mode 100644
index 0000000..f9bc048
Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square310x310Logo.png differ
diff --git a/apps/desktop/src-tauri/icons/Square44x44Logo.png b/apps/desktop/src-tauri/icons/Square44x44Logo.png
new file mode 100644
index 0000000..d5fbfb2
Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square44x44Logo.png differ
diff --git a/apps/desktop/src-tauri/icons/Square71x71Logo.png b/apps/desktop/src-tauri/icons/Square71x71Logo.png
new file mode 100644
index 0000000..63440d7
Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square71x71Logo.png differ
diff --git a/apps/desktop/src-tauri/icons/Square89x89Logo.png b/apps/desktop/src-tauri/icons/Square89x89Logo.png
new file mode 100644
index 0000000..f3f705a
Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square89x89Logo.png differ
diff --git a/apps/desktop/src-tauri/icons/StoreLogo.png b/apps/desktop/src-tauri/icons/StoreLogo.png
new file mode 100644
index 0000000..4556388
Binary files /dev/null and b/apps/desktop/src-tauri/icons/StoreLogo.png differ
diff --git a/apps/desktop/src-tauri/icons/icon.icns b/apps/desktop/src-tauri/icons/icon.icns
new file mode 100644
index 0000000..12a5bce
Binary files /dev/null and b/apps/desktop/src-tauri/icons/icon.icns differ
diff --git a/apps/desktop/src-tauri/icons/icon.ico b/apps/desktop/src-tauri/icons/icon.ico
new file mode 100644
index 0000000..06c23c8
Binary files /dev/null and b/apps/desktop/src-tauri/icons/icon.ico differ
diff --git a/apps/desktop/src-tauri/icons/icon.png b/apps/desktop/src-tauri/icons/icon.png
new file mode 100644
index 0000000..e1cd261
Binary files /dev/null and b/apps/desktop/src-tauri/icons/icon.png differ
diff --git a/apps/desktop/src-tauri/runtime/python/runner.py b/apps/desktop/src-tauri/runtime/python/runner.py
new file mode 100644
index 0000000..077de0e
--- /dev/null
+++ b/apps/desktop/src-tauri/runtime/python/runner.py
@@ -0,0 +1,1267 @@
+#!/usr/bin/env python3
+from __future__ import annotations
+
+import argparse
+import copy
+import importlib.util
+import inspect
+import json
+import queue
+import sys
+import threading
+import time
+import traceback
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import Any, Dict, List, Optional, Protocol, TypeAlias, cast
+
+JSONScalar: TypeAlias = str | int | float | bool | None
+JSONValue: TypeAlias = JSONScalar | List["JSONValue"] | Dict[str, "JSONValue"]
+JsonObject: TypeAlias = Dict[str, Any]
+SensorPayload: TypeAlias = Dict[str, float]
+ActuatorPayload: TypeAlias = Dict[str, float]
+ControllerOutputPayload: TypeAlias = Dict[str, float]
+PROTOCOL_STDOUT = sys.stdout
+
+DRIVER_REQUIRED_METHODS = ("connect", "stop", "read")
+DRIVER_WRITE_METHOD = "write"
+CONTROLLER_REQUIRED_METHODS = ("compute",)
+
+
+@dataclass
+class VariableSpec:
+ id: str
+ name: str
+ type: str
+ unit: str
+ setpoint: float
+ pv_min: float
+ pv_max: float
+ linked_sensor_ids: List[str]
+
+
+@dataclass
+class IOGroup:
+ ids: List[str]
+ count: int
+ variables: List[VariableSpec]
+ variables_by_id: Dict[str, VariableSpec]
+
+
+@dataclass
+class PlantContext:
+ id: str
+ name: str
+ variables: List[VariableSpec]
+ variables_by_id: Dict[str, VariableSpec]
+ sensors: IOGroup
+ actuators: IOGroup
+ setpoints: Dict[str, float]
+
+ def apply_setpoints(self, next_setpoints: Dict[str, float]) -> None:
+ self.setpoints = dict(next_setpoints)
+ for variable_id, variable in self.variables_by_id.items():
+ if variable_id in self.setpoints:
+ variable.setpoint = self.setpoints[variable_id]
+ for variable in self.variables:
+ if variable.id in self.setpoints:
+ variable.setpoint = self.setpoints[variable.id]
+
+
+@dataclass(frozen=True)
+class RuntimeTiming:
+ owner: str
+ clock: str
+ strategy: str
+ sample_time_ms: int
+
+
+@dataclass(frozen=True)
+class RuntimeSupervision:
+ owner: str
+ startup_timeout_ms: int
+ shutdown_timeout_ms: int
+
+
+@dataclass(frozen=True)
+class RuntimePaths:
+ runtime_dir: str
+ venv_python_path: str
+ runner_path: str
+ bootstrap_path: str
+
+
+@dataclass(frozen=True)
+class RuntimeContext:
+ id: str
+ timing: RuntimeTiming
+ supervision: RuntimeSupervision
+ paths: RuntimePaths
+
+
+@dataclass(frozen=True)
+class DriverMetadata:
+ plugin_id: str
+ plugin_name: str
+ plugin_dir: str
+ source_file: str
+ class_name: str
+ config: Dict[str, JSONValue]
+
+
+@dataclass
+class ControllerParamSpec:
+ key: str
+ type: str
+ value: JSONValue
+ label: str
+
+
+@dataclass
+class ControllerMetadata:
+ id: str
+ plugin_id: str
+ plugin_name: str
+ plugin_dir: str
+ source_file: str
+ class_name: str
+ name: str
+ controller_type: str
+ active: bool
+ input_variable_ids: List[str]
+ output_variable_ids: List[str]
+ params: Dict[str, ControllerParamSpec]
+
+
+@dataclass(frozen=True)
+class ControllerPublicMetadata:
+ id: str
+ name: str
+ controller_type: str
+ input_variable_ids: List[str]
+ output_variable_ids: List[str]
+ params: Dict[str, ControllerParamSpec]
+
+ def serialize(self) -> Dict[str, Any]:
+ return {
+ "id": self.id,
+ "name": self.name,
+ "controller_type": self.controller_type,
+ "input_variable_ids": list(self.input_variable_ids),
+ "output_variable_ids": list(self.output_variable_ids),
+ "params": serialize_controller_params(self.params),
+ }
+
+
+@dataclass(frozen=True)
+class DriverPluginContext:
+ config: Dict[str, JSONValue]
+ plant: PlantContext
+
+
+@dataclass(frozen=True)
+class ControllerPluginContext:
+ controller: ControllerPublicMetadata
+ plant: PlantContext
+
+
+@dataclass(frozen=True)
+class RuntimeBootstrap:
+ driver: DriverMetadata
+ controllers: List[ControllerMetadata]
+ plant: PlantContext
+ runtime: RuntimeContext
+
+
+@dataclass(frozen=True)
+class CycleDurations:
+ read_duration_ms: float = 0.0
+ control_duration_ms: float = 0.0
+ write_duration_ms: float = 0.0
+ publish_duration_ms: float = 0.0
+ controller_durations_ms: Dict[str, float] = field(default_factory=dict)
+
+
+class DriverProtocol(Protocol):
+ def connect(self) -> bool: ...
+
+ def stop(self) -> bool: ...
+
+ def read(self) -> Dict[str, Dict[str, float]]: ...
+
+ def write(self, outputs: Dict[str, float]) -> bool | None: ...
+
+
+class ControllerProtocol(Protocol):
+ def compute(self, snapshot: Dict[str, Any]) -> Dict[str, float]: ...
+
+
+@dataclass
+class LoadedController:
+ metadata: ControllerMetadata
+ public_metadata: Dict[str, Any]
+ instance: ControllerProtocol
+
+
+class PlantRuntimeEngine:
+ def __init__(self, bootstrap: RuntimeBootstrap) -> None:
+ self.bootstrap = bootstrap
+ self.runtime_id = bootstrap.runtime.id
+ self.plant_id = bootstrap.plant.id
+ self.sample_time_ms = bootstrap.runtime.timing.sample_time_ms
+ self.driver_instance: Optional[DriverProtocol] = None
+ self.controllers: List[LoadedController] = []
+ self.running = False
+ self.paused = False
+ self.should_exit = False
+ self.cycle_id = 0
+ self.runtime_started_at: Optional[float] = None
+ self.first_cycle_started_at: Optional[float] = None
+ self.last_cycle_started_at: Optional[float] = None
+ self.next_cycle_deadline: Optional[float] = None
+ self.paused_started_at: Optional[float] = None
+ self.paused_duration_s = 0.0
+
+ def apply_init(self, bootstrap: RuntimeBootstrap) -> None:
+ self.bootstrap = bootstrap
+ self.runtime_id = bootstrap.runtime.id
+ self.plant_id = bootstrap.plant.id
+ self.sample_time_ms = bootstrap.runtime.timing.sample_time_ms
+ self.driver_instance = None
+ self.controllers = []
+ self.running = False
+ self.paused = False
+ self.should_exit = False
+ self.cycle_id = 0
+ self.runtime_started_at = None
+ self.first_cycle_started_at = None
+ self.last_cycle_started_at = None
+ self.next_cycle_deadline = None
+ self.paused_started_at = None
+ self.paused_duration_s = 0.0
+
+ def start(self) -> None:
+ if self.driver_instance is None:
+ driver_cls = load_plugin_class(
+ Path(self.bootstrap.driver.plugin_dir),
+ self.bootstrap.driver.source_file,
+ self.bootstrap.driver.class_name,
+ DRIVER_REQUIRED_METHODS,
+ "driver",
+ )
+ driver_context = build_driver_plugin_context(self.bootstrap)
+ self.driver_instance = instantiate_plugin(
+ driver_cls,
+ driver_context,
+ "driver",
+ )
+
+ if self.bootstrap.controllers and not callable(
+ getattr(self.driver_instance, DRIVER_WRITE_METHOD, None)
+ ):
+ raise RuntimeError(
+ "Driver precisa implementar write(outputs) quando houver controladores ativos"
+ )
+
+ try:
+ connected_result = self.driver_instance.connect()
+ except Exception as exc: # noqa: BLE001
+ raise RuntimeError(
+ f"Falha ao conectar driver '{self.bootstrap.driver.plugin_name}': {format_exception_message(exc)}"
+ ) from exc
+
+ connected = coerce_required_bool("connect", connected_result)
+ if not connected:
+ raise RuntimeError("Driver retornou False em connect()")
+
+ self._replace_controllers(self.bootstrap.controllers)
+
+ self.running = True
+ self.paused = False
+ now = time.monotonic()
+ if self.runtime_started_at is None:
+ self.runtime_started_at = now
+ self.first_cycle_started_at = None
+ self.next_cycle_deadline = now
+ self.last_cycle_started_at = None
+
+ def _load_controllers(self) -> List[LoadedController]:
+ loaded: List[LoadedController] = []
+ for controller_meta in self.bootstrap.controllers:
+ controller_cls = load_plugin_class(
+ Path(controller_meta.plugin_dir),
+ controller_meta.source_file,
+ controller_meta.class_name,
+ CONTROLLER_REQUIRED_METHODS,
+ f"controlador '{controller_meta.name}'",
+ )
+ context = build_controller_plugin_context(
+ controller_meta,
+ self.bootstrap.plant,
+ )
+ instance = instantiate_plugin(
+ controller_cls,
+ context,
+ f"controlador '{controller_meta.name}'",
+ )
+ loaded.append(
+ LoadedController(
+ metadata=controller_meta,
+ public_metadata=build_public_controller_metadata(controller_meta).serialize(),
+ instance=cast(ControllerProtocol, instance),
+ )
+ )
+ return loaded
+
+ def pause(self) -> None:
+ if not self.paused:
+ self.paused_started_at = time.monotonic()
+ self.paused = True
+ self.next_cycle_deadline = None
+ self.last_cycle_started_at = None
+
+ def resume(self) -> None:
+ if self.paused_started_at is not None:
+ paused_elapsed = max(0.0, time.monotonic() - self.paused_started_at)
+ self.paused_duration_s += paused_elapsed
+ if self.first_cycle_started_at is not None:
+ self.first_cycle_started_at += paused_elapsed
+ self.paused_started_at = None
+ self.paused = False
+ self.next_cycle_deadline = time.monotonic() + (self.sample_time_ms / 1000.0)
+ self.last_cycle_started_at = None
+
+ def update_setpoints(self, setpoints: Dict[str, float]) -> None:
+ self.bootstrap.plant.apply_setpoints(setpoints)
+
+ def update_controllers(self, controllers: List[ControllerMetadata]) -> None:
+ self._replace_controllers(controllers)
+
+ def request_shutdown(self) -> None:
+ self.should_exit = True
+ self.running = False
+
+ def next_wait_timeout(self) -> Optional[float]:
+ if self.should_exit:
+ return 0.0
+ if not self.running or self.paused:
+ return None
+ if self.next_cycle_deadline is None:
+ return 0.0
+ return max(0.0, self.next_cycle_deadline - time.monotonic())
+
+ def run_cycle(self) -> None:
+ if not self.running or self.paused:
+ return
+
+ if self.next_cycle_deadline is None:
+ self.next_cycle_deadline = time.monotonic()
+
+ now = time.monotonic()
+ if now < self.next_cycle_deadline:
+ time.sleep(self.next_cycle_deadline - now)
+
+ cycle_started_at = time.monotonic()
+ self.cycle_id += 1
+ if self.first_cycle_started_at is None:
+ self.first_cycle_started_at = cycle_started_at
+ effective_dt_ms = self._resolve_effective_dt_ms(cycle_started_at)
+
+ sensors, actuators_read, durations, controller_outputs, written_outputs = self._execute_cycle(
+ cycle_started_at,
+ effective_dt_ms,
+ )
+
+ cycle_finished_at = time.monotonic()
+ cycle_duration_ms = (cycle_finished_at - cycle_started_at) * 1000.0
+
+ sample_step = self.sample_time_ms / 1000.0
+ planned_next_deadline = (self.next_cycle_deadline or cycle_started_at) + sample_step
+ late_by_ms = max(0.0, (cycle_finished_at - planned_next_deadline) * 1000.0)
+ cycle_late = late_by_ms > 0.0
+
+ publish_started_at = time.monotonic()
+ telemetry_payload = {
+ "timestamp": time.time(),
+ "cycle_id": self.cycle_id,
+ "configured_sample_time_ms": self.sample_time_ms,
+ "effective_dt_ms": effective_dt_ms,
+ "cycle_duration_ms": cycle_duration_ms,
+ "read_duration_ms": durations.read_duration_ms,
+ "control_duration_ms": durations.control_duration_ms,
+ "write_duration_ms": durations.write_duration_ms,
+ "publish_duration_ms": max(0.0, (time.monotonic() - publish_started_at) * 1000.0),
+ "cycle_late": cycle_late,
+ "late_by_ms": late_by_ms,
+ "phase": "publish_telemetry",
+ "uptime_s": self._resolve_uptime_s(cycle_started_at),
+ "sensors": sensors,
+ "actuators": written_outputs or actuators_read,
+ "actuators_read": actuators_read,
+ "setpoints": self.bootstrap.plant.setpoints,
+ "controller_outputs": controller_outputs,
+ "written_outputs": written_outputs,
+ "controller_durations_ms": durations.controller_durations_ms,
+ }
+ emit("telemetry", telemetry_payload)
+
+ if cycle_late:
+ emit(
+ "cycle_overrun",
+ {
+ "cycle_id": self.cycle_id,
+ "configured_sample_time_ms": self.sample_time_ms,
+ "cycle_duration_ms": cycle_duration_ms,
+ "late_by_ms": late_by_ms,
+ "phase": "publish_telemetry",
+ },
+ )
+
+ self.next_cycle_deadline = planned_next_deadline
+ while self.next_cycle_deadline < time.monotonic():
+ self.next_cycle_deadline += sample_step
+
+ self.last_cycle_started_at = cycle_started_at
+
+ def _execute_cycle(
+ self,
+ cycle_started_at: float,
+ effective_dt_ms: float,
+ ) -> tuple[SensorPayload, ActuatorPayload, CycleDurations, ControllerOutputPayload, ActuatorPayload]:
+ sensors: SensorPayload = {}
+ actuators_read: ActuatorPayload = {}
+ controller_outputs: ControllerOutputPayload = {}
+ written_outputs: ActuatorPayload = {}
+ controller_durations: Dict[str, float] = {}
+
+ read_started_at = time.monotonic()
+ try:
+ if self.driver_instance is not None:
+ sensors, actuators_read = normalize_read_snapshot(
+ self.driver_instance.read(),
+ self.bootstrap.plant,
+ )
+ except Exception as exc: # noqa: BLE001
+ log_error(traceback.format_exc())
+ emit("warning", {"message": f"Falha em leitura de driver: {exc}"})
+ read_duration_ms = (time.monotonic() - read_started_at) * 1000.0
+
+ control_started_at = time.monotonic()
+ for controller in self.controllers:
+ compute_started_at = time.monotonic()
+ try:
+ snapshot = build_controller_snapshot(
+ cycle_id=self.cycle_id,
+ cycle_started_at=cycle_started_at,
+ dt_ms=effective_dt_ms,
+ plant=self.bootstrap.plant,
+ controller_public_metadata=controller.public_metadata,
+ sensors=sensors,
+ actuators=actuators_read,
+ )
+ outputs = normalize_controller_outputs(
+ controller.instance.compute(snapshot),
+ controller.metadata.output_variable_ids,
+ controller.metadata.name,
+ )
+ for variable_id, value in outputs.items():
+ if variable_id in controller_outputs:
+ raise RuntimeError(
+ f"Saída '{variable_id}' recebeu mais de um valor no mesmo ciclo"
+ )
+ controller_outputs[variable_id] = value
+ except Exception as exc: # noqa: BLE001
+ log_error(traceback.format_exc())
+ emit(
+ "warning",
+ {
+ "message": f"Falha no controlador '{controller.metadata.name}': {exc}",
+ },
+ )
+ finally:
+ controller_durations[controller.metadata.id] = (
+ time.monotonic() - compute_started_at
+ ) * 1000.0
+ control_duration_ms = (time.monotonic() - control_started_at) * 1000.0
+
+ write_started_at = time.monotonic()
+ if controller_outputs and self.driver_instance is not None:
+ try:
+ write_status = self.driver_instance.write(dict(controller_outputs))
+ coerce_optional_bool(
+ "write",
+ write_status,
+ "Driver retornou False em write(outputs)",
+ )
+ written_outputs = dict(controller_outputs)
+ except Exception as exc: # noqa: BLE001
+ log_error(traceback.format_exc())
+ emit("warning", {"message": f"Falha em escrita de driver: {exc}"})
+ write_duration_ms = (time.monotonic() - write_started_at) * 1000.0
+
+ return (
+ sensors,
+ actuators_read,
+ CycleDurations(
+ read_duration_ms=read_duration_ms,
+ control_duration_ms=control_duration_ms,
+ write_duration_ms=write_duration_ms,
+ controller_durations_ms=controller_durations,
+ ),
+ controller_outputs,
+ written_outputs,
+ )
+
+ def _resolve_effective_dt_ms(self, cycle_started_at: float) -> float:
+ if self.last_cycle_started_at is None:
+ return float(self.sample_time_ms)
+ return max(0.0, (cycle_started_at - self.last_cycle_started_at) * 1000.0)
+
+ def _resolve_uptime_s(self, cycle_started_at: float) -> float:
+ if self.cycle_id == 1:
+ return 0.0
+ first_cycle_started_at = self.first_cycle_started_at or cycle_started_at
+ return max(0.0, cycle_started_at - first_cycle_started_at)
+
+ def stop(self) -> None:
+ for controller in self.controllers:
+ maybe_call_optional_stop(controller.instance, controller.metadata.name)
+ self.controllers = []
+ if self.driver_instance is not None:
+ try:
+ stopped = coerce_required_bool("stop", self.driver_instance.stop())
+ if not stopped:
+ emit("warning", {"message": "Driver retornou False em stop()"})
+ except Exception as exc: # noqa: BLE001
+ log_error(f"Falha ao finalizar driver: {exc}")
+
+ def _replace_controllers(self, controllers: List[ControllerMetadata]) -> None:
+ if controllers and self.driver_instance is not None and not callable(
+ getattr(self.driver_instance, DRIVER_WRITE_METHOD, None)
+ ):
+ raise RuntimeError(
+ "Driver precisa implementar write(outputs) quando houver controladores ativos"
+ )
+
+ for controller in self.controllers:
+ maybe_call_optional_stop(controller.instance, controller.metadata.name)
+
+ self.bootstrap = RuntimeBootstrap(
+ driver=self.bootstrap.driver,
+ controllers=list(controllers),
+ plant=self.bootstrap.plant,
+ runtime=self.bootstrap.runtime,
+ )
+ self.controllers = self._load_controllers()
+ for controller in self.controllers:
+ maybe_call_optional_connect(controller.instance, controller.metadata.name)
+
+
+def emit(msg_type: str, payload: Optional[Dict[str, Any]] = None) -> None:
+ envelope: Dict[str, Any] = {"type": msg_type}
+ if payload is not None:
+ envelope["payload"] = payload
+ PROTOCOL_STDOUT.write(json.dumps(envelope, ensure_ascii=False) + "\n")
+ PROTOCOL_STDOUT.flush()
+
+
+def log_error(message: str) -> None:
+ sys.stderr.write(message + "\n")
+ sys.stderr.flush()
+
+
+def format_exception_message(exc: BaseException) -> str:
+ message = str(exc).strip()
+ return message or exc.__class__.__name__
+
+
+def log_exception(exc: BaseException) -> None:
+ if isinstance(exc, RuntimeError):
+ log_error(str(exc))
+ return
+ log_error(traceback.format_exc())
+
+
+def expect_dict(raw_value: Any, context: str) -> JsonObject:
+ if not isinstance(raw_value, dict):
+ raise RuntimeError(f"{context} deve ser um objeto JSON")
+ return cast(JsonObject, raw_value)
+
+
+def normalize_string(raw_value: Any, context: str) -> str:
+ if not isinstance(raw_value, str) or not raw_value.strip():
+ raise RuntimeError(f"{context} deve ser uma string não vazia")
+ return raw_value.strip()
+
+
+def normalize_non_negative_int(raw_value: Any, context: str, default: int = 0) -> int:
+ if raw_value is None:
+ return default
+ resolved = int(raw_value)
+ if resolved < 0:
+ raise RuntimeError(f"{context} não pode ser negativo")
+ return resolved
+
+
+def normalize_positive_int(raw_value: Any, context: str, default: int = 1) -> int:
+ resolved = normalize_non_negative_int(raw_value, context, default)
+ if resolved <= 0:
+ raise RuntimeError(f"{context} deve ser maior que zero")
+ return resolved
+
+
+def normalize_string_list(raw_value: Any, context: str) -> List[str]:
+ if raw_value is None:
+ return []
+ if not isinstance(raw_value, list):
+ raise RuntimeError(f"{context} deve ser um array")
+ return [str(value) for value in raw_value]
+
+
+def normalize_json_map(raw_value: Any, context: str) -> Dict[str, JSONValue]:
+ if raw_value is None:
+ return {}
+ if not isinstance(raw_value, dict):
+ raise RuntimeError(f"{context} deve ser um objeto JSON")
+ return {str(key): cast(JSONValue, value) for key, value in raw_value.items()}
+
+
+def normalize_float_map(
+ raw_value: Any,
+ context: str,
+ allowed_keys: Optional[set[str]] = None,
+) -> Dict[str, float]:
+ if raw_value is None:
+ return {}
+ if not isinstance(raw_value, dict):
+ raise RuntimeError(f"{context} deve ser um objeto JSON")
+
+ normalized: Dict[str, float] = {}
+ for key, value in raw_value.items():
+ key_str = str(key)
+ if allowed_keys is not None and key_str not in allowed_keys:
+ continue
+ try:
+ numeric_value = float(value)
+ except Exception as exc: # noqa: BLE001
+ raise RuntimeError(f"{context}.{key_str} deve ser numérico") from exc
+ if numeric_value != numeric_value or numeric_value in (float("inf"), float("-inf")):
+ raise RuntimeError(f"{context}.{key_str} deve ser finito")
+ normalized[key_str] = numeric_value
+ return normalized
+
+
+def normalize_variable(raw_value: Any, context: str) -> VariableSpec:
+ raw = expect_dict(raw_value, context)
+ linked_sensor_ids_raw = raw.get("linked_sensor_ids")
+ linked_sensor_ids = (
+ normalize_string_list(linked_sensor_ids_raw, f"{context}.linked_sensor_ids")
+ if linked_sensor_ids_raw is not None
+ else []
+ )
+ return VariableSpec(
+ id=normalize_string(raw.get("id"), f"{context}.id"),
+ name=normalize_string(raw.get("name"), f"{context}.name"),
+ type=normalize_string(raw.get("type"), f"{context}.type"),
+ unit=normalize_string(raw.get("unit"), f"{context}.unit"),
+ setpoint=float(raw.get("setpoint", 0.0) or 0.0),
+ pv_min=float(raw.get("pv_min", 0.0) or 0.0),
+ pv_max=float(raw.get("pv_max", 0.0) or 0.0),
+ linked_sensor_ids=linked_sensor_ids,
+ )
+
+
+def normalize_variable_list(raw_value: Any, context: str) -> List[VariableSpec]:
+ if raw_value is None:
+ return []
+ if not isinstance(raw_value, list):
+ raise RuntimeError(f"{context} deve ser um array")
+ return [
+ normalize_variable(item, f"{context}[{index}]")
+ for index, item in enumerate(raw_value)
+ ]
+
+
+def normalize_variable_map(raw_value: Any, context: str) -> Dict[str, VariableSpec]:
+ if raw_value is None:
+ return {}
+ if not isinstance(raw_value, dict):
+ raise RuntimeError(f"{context} deve ser um objeto JSON")
+
+ normalized: Dict[str, VariableSpec] = {}
+ for key, value in raw_value.items():
+ variable = normalize_variable(value, f"{context}.{key}")
+ normalized[variable.id] = variable
+ return normalized
+
+
+def build_variable_map(variables: List[VariableSpec]) -> Dict[str, VariableSpec]:
+ return {variable.id: variable for variable in variables}
+
+
+def normalize_io_group(raw_value: Any, context: str) -> IOGroup:
+ raw = expect_dict(raw_value, context)
+ variables = normalize_variable_list(raw.get("variables"), f"{context}.variables")
+ variables_by_id = normalize_variable_map(raw.get("variables_by_id"), f"{context}.variables_by_id")
+ if not variables_by_id:
+ variables_by_id = build_variable_map(variables)
+ if not variables:
+ variables = list(variables_by_id.values())
+
+ ids = normalize_string_list(raw.get("ids"), f"{context}.ids")
+ if not ids:
+ ids = [variable.id for variable in variables]
+
+ count = normalize_non_negative_int(raw.get("count"), f"{context}.count", len(ids))
+ return IOGroup(ids=ids, count=count, variables=variables, variables_by_id=variables_by_id)
+
+
+def normalize_plant_context(raw_value: Any) -> PlantContext:
+ raw = expect_dict(raw_value, "bootstrap.plant")
+ variables = normalize_variable_list(raw.get("variables"), "bootstrap.plant.variables")
+ if not variables:
+ raise RuntimeError("bootstrap.plant.variables deve conter pelo menos uma variável")
+ variables_by_id = build_variable_map(variables)
+ sensor_ids = normalize_string_list(raw.get("sensor_ids"), "bootstrap.plant.sensor_ids")
+ actuator_ids = normalize_string_list(raw.get("actuator_ids"), "bootstrap.plant.actuator_ids")
+ if not sensor_ids:
+ sensor_ids = [variable.id for variable in variables if variable.type == "sensor"]
+ if not actuator_ids:
+ actuator_ids = [variable.id for variable in variables if variable.type == "atuador"]
+
+ sensor_variables = [variables_by_id[variable_id] for variable_id in sensor_ids if variable_id in variables_by_id]
+ actuator_variables = [
+ variables_by_id[variable_id] for variable_id in actuator_ids if variable_id in variables_by_id
+ ]
+
+ return PlantContext(
+ id=normalize_string(raw.get("id"), "bootstrap.plant.id"),
+ name=normalize_string(raw.get("name"), "bootstrap.plant.name"),
+ variables=variables,
+ variables_by_id=variables_by_id,
+ sensors=IOGroup(
+ ids=sensor_ids,
+ count=len(sensor_variables),
+ variables=sensor_variables,
+ variables_by_id=build_variable_map(sensor_variables),
+ ),
+ actuators=IOGroup(
+ ids=actuator_ids,
+ count=len(actuator_variables),
+ variables=actuator_variables,
+ variables_by_id=build_variable_map(actuator_variables),
+ ),
+ setpoints=normalize_float_map(raw.get("setpoints"), "bootstrap.plant.setpoints"),
+ )
+
+
+def normalize_runtime_context(raw_value: Any) -> RuntimeContext:
+ raw = expect_dict(raw_value, "bootstrap.runtime")
+ timing_raw = expect_dict(raw.get("timing"), "bootstrap.runtime.timing")
+ supervision_raw = expect_dict(raw.get("supervision"), "bootstrap.runtime.supervision")
+ paths_raw = expect_dict(raw.get("paths"), "bootstrap.runtime.paths")
+
+ return RuntimeContext(
+ id=normalize_string(raw.get("id"), "bootstrap.runtime.id"),
+ timing=RuntimeTiming(
+ owner=normalize_string(timing_raw.get("owner"), "bootstrap.runtime.timing.owner"),
+ clock=normalize_string(timing_raw.get("clock"), "bootstrap.runtime.timing.clock"),
+ strategy=normalize_string(timing_raw.get("strategy"), "bootstrap.runtime.timing.strategy"),
+ sample_time_ms=normalize_positive_int(
+ timing_raw.get("sample_time_ms"),
+ "bootstrap.runtime.timing.sample_time_ms",
+ 100,
+ ),
+ ),
+ supervision=RuntimeSupervision(
+ owner=normalize_string(
+ supervision_raw.get("owner"),
+ "bootstrap.runtime.supervision.owner",
+ ),
+ startup_timeout_ms=normalize_positive_int(
+ supervision_raw.get("startup_timeout_ms"),
+ "bootstrap.runtime.supervision.startup_timeout_ms",
+ 1000,
+ ),
+ shutdown_timeout_ms=normalize_positive_int(
+ supervision_raw.get("shutdown_timeout_ms"),
+ "bootstrap.runtime.supervision.shutdown_timeout_ms",
+ 1000,
+ ),
+ ),
+ paths=RuntimePaths(
+ runtime_dir=normalize_string(
+ paths_raw.get("runtime_dir"),
+ "bootstrap.runtime.paths.runtime_dir",
+ ),
+ venv_python_path=normalize_string(
+ paths_raw.get("venv_python_path"),
+ "bootstrap.runtime.paths.venv_python_path",
+ ),
+ runner_path=normalize_string(
+ paths_raw.get("runner_path"),
+ "bootstrap.runtime.paths.runner_path",
+ ),
+ bootstrap_path=normalize_string(
+ paths_raw.get("bootstrap_path"),
+ "bootstrap.runtime.paths.bootstrap_path",
+ ),
+ ),
+ )
+
+
+def normalize_driver_metadata(raw_value: Any) -> DriverMetadata:
+ raw = expect_dict(raw_value, "bootstrap.driver")
+ return DriverMetadata(
+ plugin_id=normalize_string(raw.get("plugin_id"), "bootstrap.driver.plugin_id"),
+ plugin_name=normalize_string(raw.get("plugin_name"), "bootstrap.driver.plugin_name"),
+ plugin_dir=normalize_string(raw.get("plugin_dir"), "bootstrap.driver.plugin_dir"),
+ source_file=normalize_string(raw.get("source_file"), "bootstrap.driver.source_file"),
+ class_name=normalize_string(raw.get("class_name"), "bootstrap.driver.class_name"),
+ config=normalize_json_map(raw.get("config"), "bootstrap.driver.config"),
+ )
+
+
+def normalize_controller_param(raw_value: Any, context: str, key: str) -> ControllerParamSpec:
+ raw = expect_dict(raw_value, context)
+ return ControllerParamSpec(
+ key=key,
+ type=normalize_string(raw.get("type"), f"{context}.type"),
+ value=cast(JSONValue, raw.get("value")),
+ label=normalize_string(raw.get("label"), f"{context}.label"),
+ )
+
+
+def normalize_controller_metadata(raw_value: Any, index: int) -> ControllerMetadata:
+ context = f"bootstrap.controllers[{index}]"
+ raw = expect_dict(raw_value, context)
+ params_raw = expect_dict(raw.get("params") or {}, f"{context}.params")
+
+ return ControllerMetadata(
+ id=normalize_string(raw.get("id"), f"{context}.id"),
+ plugin_id=normalize_string(raw.get("plugin_id"), f"{context}.plugin_id"),
+ plugin_name=normalize_string(raw.get("plugin_name"), f"{context}.plugin_name"),
+ plugin_dir=normalize_string(raw.get("plugin_dir"), f"{context}.plugin_dir"),
+ source_file=normalize_string(raw.get("source_file"), f"{context}.source_file"),
+ class_name=normalize_string(raw.get("class_name"), f"{context}.class_name"),
+ name=normalize_string(raw.get("name"), f"{context}.name"),
+ controller_type=normalize_string(raw.get("controller_type"), f"{context}.controller_type"),
+ active=bool(raw.get("active", True)),
+ input_variable_ids=normalize_string_list(raw.get("input_variable_ids"), f"{context}.input_variable_ids"),
+ output_variable_ids=normalize_string_list(raw.get("output_variable_ids"), f"{context}.output_variable_ids"),
+ params={
+ str(key): normalize_controller_param(value, f"{context}.params.{key}", str(key))
+ for key, value in params_raw.items()
+ },
+ )
+
+
+def normalize_bootstrap(raw_value: Any) -> RuntimeBootstrap:
+ raw = expect_dict(raw_value, "bootstrap")
+ controllers_raw = raw.get("controllers")
+ if controllers_raw is None:
+ controllers: List[ControllerMetadata] = []
+ elif not isinstance(controllers_raw, list):
+ raise RuntimeError("bootstrap.controllers deve ser um array")
+ else:
+ controllers = [
+ normalize_controller_metadata(controller_raw, index)
+ for index, controller_raw in enumerate(controllers_raw)
+ ]
+
+ return RuntimeBootstrap(
+ driver=normalize_driver_metadata(raw.get("driver")),
+ controllers=controllers,
+ plant=normalize_plant_context(raw.get("plant")),
+ runtime=normalize_runtime_context(raw.get("runtime")),
+ )
+
+
+def load_plugin_class(
+ plugin_dir: Path,
+ source_file: str,
+ expected_class_name: str,
+ required_methods: tuple[str, ...],
+ component_label: str,
+) -> type[Any]:
+ source_path = plugin_dir / source_file
+ if not source_path.exists():
+ raise RuntimeError(f"{source_file} não encontrado em '{source_path}'")
+
+ spec = importlib.util.spec_from_file_location(
+ f"runtime_plugin_{expected_class_name.lower()}",
+ str(source_path),
+ )
+ if spec is None or spec.loader is None:
+ raise RuntimeError(f"Falha ao criar spec do módulo do {component_label}")
+
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+
+ candidate = getattr(module, expected_class_name, None)
+ if candidate is None or not inspect.isclass(candidate):
+ raise RuntimeError(
+ f"Classe '{expected_class_name}' não encontrada em {source_file} para o {component_label}"
+ )
+ if candidate.__module__ != module.__name__:
+ raise RuntimeError(
+ f"Classe '{expected_class_name}' precisa ser definida em {source_file}"
+ )
+
+ missing = [
+ method
+ for method in required_methods
+ if not callable(getattr(candidate, method, None))
+ ]
+ if missing:
+ raise RuntimeError(
+ f"Classe '{expected_class_name}' inválida para o {component_label}. Métodos ausentes: {', '.join(missing)}"
+ )
+
+ return candidate
+
+
+def instantiate_plugin(plugin_cls: type[Any], context: Any, component_label: str) -> Any:
+ try:
+ return plugin_cls(context)
+ except TypeError as exc:
+ raise RuntimeError(
+ f"Construtor do {component_label} deve seguir o contrato __init__(self, context)"
+ ) from exc
+
+
+def coerce_required_bool(method_name: str, result: Any) -> bool:
+ if not isinstance(result, bool):
+ raise RuntimeError(
+ f"Método '{method_name}' deve retornar bool, recebeu {type(result).__name__}"
+ )
+ return result
+
+
+def coerce_optional_bool(method_name: str, result: Any, false_message: str) -> None:
+ if result is None:
+ return
+ if not isinstance(result, bool):
+ raise RuntimeError(
+ f"Método '{method_name}' deve retornar bool ou None, recebeu {type(result).__name__}"
+ )
+ if not result:
+ emit("warning", {"message": false_message})
+
+
+def maybe_call_optional_connect(instance: Any, component_name: str) -> None:
+ connect = getattr(instance, "connect", None)
+ if not callable(connect):
+ return
+ result = connect()
+ coerce_optional_bool(
+ "connect",
+ result,
+ f"Componente '{component_name}' retornou False em connect()",
+ )
+
+
+def maybe_call_optional_stop(instance: Any, component_name: str) -> None:
+ stop = getattr(instance, "stop", None)
+ if not callable(stop):
+ return
+ try:
+ result = stop()
+ coerce_optional_bool(
+ "stop",
+ result,
+ f"Componente '{component_name}' retornou False em stop()",
+ )
+ except Exception as exc: # noqa: BLE001
+ log_error(f"Falha ao finalizar componente '{component_name}': {exc}")
+
+
+def normalize_read_snapshot(
+ raw_value: Any,
+ plant: PlantContext,
+) -> tuple[SensorPayload, ActuatorPayload]:
+ if raw_value is None:
+ return {}, {}
+ if not isinstance(raw_value, dict):
+ raise RuntimeError(
+ "read() deve retornar um objeto JSON no formato {'sensors': {...}, 'actuators': {...}}"
+ )
+
+ sensors = normalize_float_map(raw_value.get("sensors"), "read().sensors", set(plant.sensors.ids))
+ actuators = normalize_float_map(raw_value.get("actuators"), "read().actuators", set(plant.actuators.ids))
+ return sensors, actuators
+
+
+def normalize_controller_outputs(
+ raw_value: Any,
+ allowed_output_ids: List[str],
+ controller_name: str,
+) -> ControllerOutputPayload:
+ return normalize_float_map(
+ raw_value,
+ f"compute().outputs[{controller_name}]",
+ set(allowed_output_ids),
+ )
+
+
+def clone_controller_params(
+ params: Dict[str, ControllerParamSpec],
+) -> Dict[str, ControllerParamSpec]:
+ return {
+ key: ControllerParamSpec(
+ key=param.key,
+ type=param.type,
+ value=cast(JSONValue, copy.deepcopy(param.value)),
+ label=param.label,
+ )
+ for key, param in params.items()
+ }
+
+
+def serialize_controller_params(
+ params: Dict[str, ControllerParamSpec],
+) -> Dict[str, Dict[str, JSONValue | str]]:
+ return {
+ key: {
+ "type": param.type,
+ "value": cast(JSONValue, copy.deepcopy(param.value)),
+ "label": param.label,
+ }
+ for key, param in params.items()
+ }
+
+
+def build_public_controller_metadata(
+ controller: ControllerMetadata,
+) -> ControllerPublicMetadata:
+ return ControllerPublicMetadata(
+ id=controller.id,
+ name=controller.name,
+ controller_type=controller.controller_type,
+ input_variable_ids=list(controller.input_variable_ids),
+ output_variable_ids=list(controller.output_variable_ids),
+ params=clone_controller_params(controller.params),
+ )
+
+
+def build_driver_plugin_context(bootstrap: RuntimeBootstrap) -> DriverPluginContext:
+ return DriverPluginContext(
+ config=cast(Dict[str, JSONValue], copy.deepcopy(bootstrap.driver.config)),
+ plant=bootstrap.plant,
+ )
+
+
+def build_controller_plugin_context(
+ controller: ControllerMetadata,
+ plant: PlantContext,
+) -> ControllerPluginContext:
+ return ControllerPluginContext(
+ controller=build_public_controller_metadata(controller),
+ plant=plant,
+ )
+
+
+def build_controller_snapshot(
+ cycle_id: int,
+ cycle_started_at: float,
+ dt_ms: float,
+ plant: PlantContext,
+ controller_public_metadata: Dict[str, Any],
+ sensors: SensorPayload,
+ actuators: ActuatorPayload,
+) -> Dict[str, Any]:
+ return {
+ "cycle_id": cycle_id,
+ "timestamp": cycle_started_at,
+ "dt_s": max(0.0, dt_ms / 1000.0),
+ "plant": {
+ "id": plant.id,
+ "name": plant.name,
+ },
+ "setpoints": dict(plant.setpoints),
+ "sensors": dict(sensors),
+ "actuators": dict(actuators),
+ "variables_by_id": {
+ variable_id: {
+ "id": variable.id,
+ "name": variable.name,
+ "type": variable.type,
+ "unit": variable.unit,
+ "setpoint": variable.setpoint,
+ "pv_min": variable.pv_min,
+ "pv_max": variable.pv_max,
+ "linked_sensor_ids": list(variable.linked_sensor_ids),
+ }
+ for variable_id, variable in plant.variables_by_id.items()
+ },
+ "controller": copy.deepcopy(controller_public_metadata),
+ }
+
+
+def spawn_command_reader(command_queue: "queue.Queue[Dict[str, Any]]") -> None:
+ def _reader() -> None:
+ for raw_line in sys.stdin:
+ line = raw_line.strip()
+ if not line:
+ continue
+ try:
+ payload = json.loads(line)
+ except Exception as exc: # noqa: BLE001
+ emit("error", {"message": f"Comando JSON inválido: {exc}"})
+ continue
+
+ if not isinstance(payload, dict):
+ emit("error", {"message": "Comando recebido deve ser um objeto JSON"})
+ continue
+
+ command_queue.put(cast(Dict[str, Any], payload))
+
+ thread = threading.Thread(target=_reader, daemon=True, name="stdin-command-reader")
+ thread.start()
+
+
+def bootstrap_from_file(bootstrap_path: Path) -> RuntimeBootstrap:
+ with bootstrap_path.open("r", encoding="utf-8") as handle:
+ return normalize_bootstrap(json.load(handle))
+
+
+def handle_command(command: Dict[str, Any], engine: PlantRuntimeEngine) -> None:
+ msg_type = str(command.get("type", "")).strip()
+ payload = command.get("payload")
+
+ if msg_type == "init":
+ engine.apply_init(normalize_bootstrap(payload))
+ return
+
+ if msg_type == "start":
+ engine.start()
+ emit(
+ "connected",
+ {"runtime_id": engine.runtime_id, "plant_id": engine.plant_id},
+ )
+ return
+
+ if msg_type == "pause":
+ engine.pause()
+ return
+
+ if msg_type == "resume":
+ engine.resume()
+ return
+
+ if msg_type == "update_setpoints":
+ raw_payload = expect_dict(payload, "update_setpoints.payload")
+ setpoints = normalize_float_map(
+ raw_payload.get("setpoints"),
+ "update_setpoints.payload.setpoints",
+ )
+ engine.update_setpoints(setpoints)
+ return
+
+ if msg_type == "update_controllers":
+ raw_payload = expect_dict(payload, "update_controllers.payload")
+ controllers_raw = raw_payload.get("controllers")
+ if controllers_raw is None:
+ controllers: List[ControllerMetadata] = []
+ elif not isinstance(controllers_raw, list):
+ raise RuntimeError("update_controllers.payload.controllers deve ser um array")
+ else:
+ controllers = [
+ normalize_controller_metadata(controller_raw, index)
+ for index, controller_raw in enumerate(controllers_raw)
+ ]
+ try:
+ engine.update_controllers(controllers)
+ except Exception as exc: # noqa: BLE001
+ log_exception(exc)
+ emit("error", {"message": f"Falha ao atualizar controladores: {exc}"})
+ return
+
+ if msg_type in ("stop", "shutdown"):
+ engine.request_shutdown()
+ return
+
+ if msg_type == "write_outputs":
+ emit("warning", {"message": "Comando write_outputs não é suportado nesta fase"})
+ return
+
+
+def run() -> int:
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--runtime-dir", required=True)
+ parser.add_argument("--bootstrap", required=True)
+ args = parser.parse_args()
+
+ runtime_dir = Path(args.runtime_dir)
+ bootstrap_path = Path(args.bootstrap)
+
+ # Keep stdout reserved for the JSON protocol. Any plugin/library print()
+ # should flow to stderr so it never corrupts the IPC stream.
+ sys.stdout = sys.stderr
+
+ if not bootstrap_path.exists():
+ emit("error", {"message": f"bootstrap.json não encontrado em '{bootstrap_path}'"})
+ return 1
+
+ bootstrap = bootstrap_from_file(bootstrap_path)
+ engine = PlantRuntimeEngine(bootstrap)
+ command_queue: "queue.Queue[Dict[str, Any]]" = queue.Queue()
+ spawn_command_reader(command_queue)
+
+ emit(
+ "ready",
+ {
+ "runtime_id": engine.runtime_id,
+ "plant_id": engine.plant_id,
+ "driver": engine.bootstrap.driver.plugin_name,
+ "runtime_dir": str(runtime_dir),
+ },
+ )
+
+ try:
+ while not engine.should_exit:
+ wait_timeout = engine.next_wait_timeout()
+ try:
+ command = command_queue.get(timeout=0.5 if wait_timeout is None else wait_timeout)
+ try:
+ handle_command(command, engine)
+ except Exception as exc: # noqa: BLE001
+ log_exception(exc)
+ emit("error", {"message": f"Falha ao processar comando '{command.get('type', '')}': {exc}"})
+ engine.request_shutdown()
+ continue
+ except queue.Empty:
+ pass
+
+ while not engine.should_exit:
+ try:
+ command = command_queue.get_nowait()
+ except queue.Empty:
+ break
+
+ try:
+ handle_command(command, engine)
+ except Exception as exc: # noqa: BLE001
+ log_exception(exc)
+ emit("error", {"message": f"Falha ao processar comando '{command.get('type', '')}': {exc}"})
+ engine.request_shutdown()
+ break
+
+ if engine.should_exit:
+ break
+
+ engine.run_cycle()
+ finally:
+ engine.stop()
+
+ emit("stopped", {"runtime_id": engine.runtime_id, "plant_id": engine.plant_id})
+ return 0
+
+
+if __name__ == "__main__":
+ try:
+ raise SystemExit(run())
+ except Exception as exc: # noqa: BLE001
+ log_exception(exc)
+ emit("error", {"message": f"Runner Python falhou: {exc}"})
+ raise
diff --git a/apps/desktop/src-tauri/runtime/python/test_runner_contract.py b/apps/desktop/src-tauri/runtime/python/test_runner_contract.py
new file mode 100644
index 0000000..42bb3ce
--- /dev/null
+++ b/apps/desktop/src-tauri/runtime/python/test_runner_contract.py
@@ -0,0 +1,346 @@
+from __future__ import annotations
+
+import importlib.util
+import sys
+import tempfile
+import textwrap
+import unittest
+from unittest.mock import patch
+from pathlib import Path
+from types import ModuleType
+from typing import Any
+
+
+def load_runner_module() -> ModuleType:
+ runner_path = Path(__file__).with_name("runner.py")
+ spec = importlib.util.spec_from_file_location("senamby_runtime_runner", runner_path)
+ if spec is None or spec.loader is None:
+ raise RuntimeError("Falha ao carregar runner.py para testes")
+ module = importlib.util.module_from_spec(spec)
+ sys.modules[spec.name] = module
+ spec.loader.exec_module(module)
+ return module
+
+
+runner = load_runner_module()
+
+
+class RunnerContractTests(unittest.TestCase):
+ def build_bootstrap(self, root: Path) -> Any:
+ plant_variables = [
+ runner.VariableSpec(
+ id="sensor_1",
+ name="Sensor 1",
+ type="sensor",
+ unit="C",
+ setpoint=42.0,
+ pv_min=0.0,
+ pv_max=100.0,
+ linked_sensor_ids=[],
+ ),
+ runner.VariableSpec(
+ id="actuator_1",
+ name="Actuator 1",
+ type="actuator",
+ unit="%",
+ setpoint=0.0,
+ pv_min=0.0,
+ pv_max=100.0,
+ linked_sensor_ids=["sensor_1"],
+ ),
+ ]
+
+ variables_by_id = {variable.id: variable for variable in plant_variables}
+ plant = runner.PlantContext(
+ id="plant_1",
+ name="Plant 1",
+ variables=plant_variables,
+ variables_by_id=variables_by_id,
+ sensors=runner.IOGroup(
+ ids=["sensor_1"],
+ count=1,
+ variables=[variables_by_id["sensor_1"]],
+ variables_by_id={"sensor_1": variables_by_id["sensor_1"]},
+ ),
+ actuators=runner.IOGroup(
+ ids=["actuator_1"],
+ count=1,
+ variables=[variables_by_id["actuator_1"]],
+ variables_by_id={"actuator_1": variables_by_id["actuator_1"]},
+ ),
+ setpoints={"sensor_1": 42.0, "actuator_1": 0.0},
+ )
+
+ runtime = runner.RuntimeContext(
+ id="rt_1",
+ timing=runner.RuntimeTiming(
+ owner="runtime",
+ clock="monotonic",
+ strategy="deadline",
+ sample_time_ms=100,
+ ),
+ supervision=runner.RuntimeSupervision(
+ owner="rust",
+ startup_timeout_ms=12000,
+ shutdown_timeout_ms=4000,
+ ),
+ paths=runner.RuntimePaths(
+ runtime_dir=str(root / "runtime"),
+ venv_python_path=str(root / ".venv" / "bin" / "python"),
+ runner_path=str(root / "runtime" / "runner.py"),
+ bootstrap_path=str(root / "runtime" / "bootstrap.json"),
+ ),
+ )
+
+ driver_dir = root / "driver_plugin"
+ driver_dir.mkdir()
+ (driver_dir / "main.py").write_text(
+ textwrap.dedent(
+ """
+ from typing import Any, Dict
+
+ class ContractDriver:
+ def __init__(self, context: Any) -> None:
+ if hasattr(context, "runtime"):
+ raise RuntimeError("driver context leaked runtime")
+ self.context = context
+ self.context_keys = set(vars(context).keys())
+
+ def connect(self) -> bool:
+ return True
+
+ def stop(self) -> bool:
+ return True
+
+ def read(self) -> Dict[str, Dict[str, float]]:
+ return {
+ "sensors": {"sensor_1": 1.0},
+ "actuators": {"actuator_1": 0.0},
+ }
+
+ def write(self, outputs: Dict[str, float]) -> bool:
+ return True
+ """
+ ).strip()
+ + "\n",
+ encoding="utf-8",
+ )
+
+ controller_dir = root / "controller_plugin"
+ controller_dir.mkdir()
+ (controller_dir / "main.py").write_text(
+ textwrap.dedent(
+ """
+ from typing import Any, Dict
+
+ class ContractController:
+ def __init__(self, context: Any) -> None:
+ if hasattr(context, "runtime"):
+ raise RuntimeError("controller context leaked runtime")
+ self.context = context
+ self.context_keys = set(vars(context).keys())
+ self.controller_keys = set(vars(context.controller).keys())
+
+ def connect(self) -> bool:
+ return True
+
+ def stop(self) -> bool:
+ return True
+
+ def compute(self, snapshot: Dict[str, Any]) -> Dict[str, float]:
+ return {self.context.controller.output_variable_ids[0]: 0.0}
+ """
+ ).strip()
+ + "\n",
+ encoding="utf-8",
+ )
+
+ return runner.RuntimeBootstrap(
+ driver=runner.DriverMetadata(
+ plugin_id="driver_plugin",
+ plugin_name="Driver Plugin",
+ plugin_dir=str(driver_dir),
+ source_file="main.py",
+ class_name="ContractDriver",
+ config={"port": "COM1"},
+ ),
+ controllers=[
+ runner.ControllerMetadata(
+ id="ctrl_1",
+ plugin_id="controller_plugin",
+ plugin_name="Controller Plugin",
+ plugin_dir=str(controller_dir),
+ source_file="main.py",
+ class_name="ContractController",
+ name="Controller 1",
+ controller_type="PID",
+ active=True,
+ input_variable_ids=["sensor_1"],
+ output_variable_ids=["actuator_1"],
+ params={
+ "kp": runner.ControllerParamSpec(
+ key="kp",
+ type="number",
+ value=1.2,
+ label="Kp",
+ )
+ },
+ )
+ ],
+ plant=plant,
+ runtime=runtime,
+ )
+
+ def test_driver_context_exposes_only_config_and_plant(self) -> None:
+ with tempfile.TemporaryDirectory() as tmp_dir:
+ bootstrap = self.build_bootstrap(Path(tmp_dir))
+ context = runner.build_driver_plugin_context(bootstrap)
+
+ self.assertEqual(set(vars(context).keys()), {"config", "plant"})
+ self.assertFalse(hasattr(context, "runtime"))
+
+ def test_controller_context_uses_minimum_public_shape(self) -> None:
+ with tempfile.TemporaryDirectory() as tmp_dir:
+ bootstrap = self.build_bootstrap(Path(tmp_dir))
+ context = runner.build_controller_plugin_context(
+ bootstrap.controllers[0],
+ bootstrap.plant,
+ )
+
+ self.assertEqual(set(vars(context).keys()), {"controller", "plant"})
+ self.assertFalse(hasattr(context, "runtime"))
+ self.assertEqual(
+ set(vars(context.controller).keys()),
+ {
+ "id",
+ "name",
+ "controller_type",
+ "input_variable_ids",
+ "output_variable_ids",
+ "params",
+ },
+ )
+
+ def test_snapshot_controller_omits_internal_loader_fields(self) -> None:
+ with tempfile.TemporaryDirectory() as tmp_dir:
+ bootstrap = self.build_bootstrap(Path(tmp_dir))
+ snapshot = runner.build_controller_snapshot(
+ cycle_id=1,
+ cycle_started_at=123.456,
+ dt_ms=100.0,
+ plant=bootstrap.plant,
+ controller_public_metadata=runner.build_public_controller_metadata(
+ bootstrap.controllers[0]
+ ).serialize(),
+ sensors={"sensor_1": 40.0},
+ actuators={"actuator_1": 10.0},
+ )
+
+ self.assertEqual(
+ set(snapshot["controller"].keys()),
+ {
+ "id",
+ "name",
+ "controller_type",
+ "input_variable_ids",
+ "output_variable_ids",
+ "params",
+ },
+ )
+ self.assertNotIn("plugin_id", snapshot["controller"])
+ self.assertNotIn("plugin_name", snapshot["controller"])
+ self.assertNotIn("active", snapshot["controller"])
+
+ def test_engine_loads_plugins_with_internal_bootstrap_and_public_context(self) -> None:
+ with tempfile.TemporaryDirectory() as tmp_dir:
+ bootstrap = self.build_bootstrap(Path(tmp_dir))
+ engine = runner.PlantRuntimeEngine(bootstrap)
+ try:
+ engine.start()
+
+ driver_instance = engine.driver_instance
+ self.assertIsNotNone(driver_instance)
+ self.assertEqual(driver_instance.context_keys, {"config", "plant"})
+ self.assertFalse(hasattr(driver_instance.context, "runtime"))
+
+ self.assertEqual(len(engine.controllers), 1)
+ controller_instance = engine.controllers[0].instance
+ self.assertEqual(controller_instance.context_keys, {"controller", "plant"})
+ self.assertFalse(hasattr(controller_instance.context, "runtime"))
+ self.assertEqual(
+ controller_instance.controller_keys,
+ {
+ "id",
+ "name",
+ "controller_type",
+ "input_variable_ids",
+ "output_variable_ids",
+ "params",
+ },
+ )
+ finally:
+ engine.stop()
+
+ def test_engine_uptime_progresses_from_first_cycle_start(self) -> None:
+ class FakeClock:
+ def __init__(self) -> None:
+ self.monotonic_now = 1000.0
+ self.wall_now = 1700000000.0
+
+ def monotonic(self) -> float:
+ return self.monotonic_now
+
+ def time(self) -> float:
+ return self.wall_now + (self.monotonic_now - 1000.0)
+
+ def sleep(self, duration: float) -> None:
+ self.monotonic_now += max(0.0, duration)
+
+ with tempfile.TemporaryDirectory() as tmp_dir:
+ original_bootstrap = self.build_bootstrap(Path(tmp_dir))
+ bootstrap = runner.RuntimeBootstrap(
+ driver=original_bootstrap.driver,
+ controllers=original_bootstrap.controllers,
+ plant=original_bootstrap.plant,
+ runtime=runner.RuntimeContext(
+ id=original_bootstrap.runtime.id,
+ timing=runner.RuntimeTiming(
+ owner=original_bootstrap.runtime.timing.owner,
+ clock=original_bootstrap.runtime.timing.clock,
+ strategy=original_bootstrap.runtime.timing.strategy,
+ sample_time_ms=1000,
+ ),
+ supervision=original_bootstrap.runtime.supervision,
+ paths=original_bootstrap.runtime.paths,
+ ),
+ )
+ engine = runner.PlantRuntimeEngine(bootstrap)
+ fake_clock = FakeClock()
+ telemetry_payloads: list[dict[str, Any]] = []
+
+ def capture_emit(msg_type: str, payload: dict[str, Any] | None = None) -> None:
+ if msg_type == "telemetry" and payload is not None:
+ telemetry_payloads.append(payload)
+
+ with (
+ patch.object(runner.time, "monotonic", fake_clock.monotonic),
+ patch.object(runner.time, "time", fake_clock.time),
+ patch.object(runner.time, "sleep", fake_clock.sleep),
+ patch.object(runner, "emit", capture_emit),
+ ):
+ try:
+ engine.start()
+ engine.run_cycle()
+ engine.run_cycle()
+ engine.run_cycle()
+ finally:
+ engine.stop()
+
+ self.assertEqual(len(telemetry_payloads), 3)
+ self.assertAlmostEqual(telemetry_payloads[0]["uptime_s"], 0.0, places=6)
+ self.assertAlmostEqual(telemetry_payloads[1]["uptime_s"], 1.0, places=6)
+ self.assertAlmostEqual(telemetry_payloads[2]["uptime_s"], 2.0, places=6)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/apps/desktop/src-tauri/src/commands/mod.rs b/apps/desktop/src-tauri/src/commands/mod.rs
new file mode 100644
index 0000000..5e0cc2d
--- /dev/null
+++ b/apps/desktop/src-tauri/src/commands/mod.rs
@@ -0,0 +1,2 @@
+pub mod plants;
+pub mod plugins;
diff --git a/apps/desktop/src-tauri/src/commands/plants.rs b/apps/desktop/src-tauri/src/commands/plants.rs
new file mode 100644
index 0000000..c2b2496
--- /dev/null
+++ b/apps/desktop/src-tauri/src/commands/plants.rs
@@ -0,0 +1,220 @@
+#![allow(clippy::needless_pass_by_value)]
+
+use crate::core::error::{AppError, ErrorDto};
+use crate::core::models::plant::{
+ CreatePlantRequest, Plant, PlantResponse, RemovePlantControllerRequest,
+ SavePlantControllerConfigRequest, SavePlantSetpointRequest, UpdatePlantRequest,
+};
+use crate::core::services::plant::PlantService;
+use crate::core::services::plant_import::{
+ ImportPlantFileResponse, OpenPlantFileResponse, PlantImportFileRequest, PlantImportService,
+};
+use crate::core::services::runtime::{DriverRuntimeService, PlantRuntimeManager};
+use crate::state::AppState;
+use serde::Deserialize;
+use tauri::{AppHandle, State};
+
+#[derive(Debug, Deserialize)]
+pub struct ImportFileRequest {
+ #[serde(rename = "fileName")]
+ pub file_name: String,
+ pub content: String,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct SaveExportFileRequest {
+ pub path: String,
+ pub content: String,
+}
+
+impl From for PlantImportFileRequest {
+ fn from(value: ImportFileRequest) -> Self {
+ Self {
+ file_name: value.file_name,
+ content: value.content,
+ }
+ }
+}
+
+fn into_plant_response(
+ runtimes: &PlantRuntimeManager,
+ result: crate::core::error::AppResult,
+) -> Result {
+ result
+ .map(|plant| PlantResponse::from(runtimes.apply_live_stats(plant)))
+ .map_err(ErrorDto::from)
+}
+
+#[tauri::command]
+pub fn create_plant(
+ state: State<'_, AppState>,
+ request: CreatePlantRequest,
+) -> Result {
+ into_plant_response(
+ state.runtimes(),
+ PlantService::create(state.plants(), state.plugins(), request),
+ )
+}
+
+#[tauri::command]
+pub fn update_plant(
+ state: State<'_, AppState>,
+ request: UpdatePlantRequest,
+) -> Result {
+ into_plant_response(
+ state.runtimes(),
+ PlantService::update(state.plants(), state.plugins(), request),
+ )
+}
+
+#[tauri::command]
+pub fn list_plants(state: State<'_, AppState>) -> Vec {
+ state
+ .runtimes()
+ .apply_live_stats_batch(PlantService::list(state.plants()))
+ .into_iter()
+ .map(PlantResponse::from)
+ .collect()
+}
+
+#[tauri::command]
+pub fn get_plant(state: State<'_, AppState>, id: String) -> Result {
+ into_plant_response(state.runtimes(), PlantService::get(state.plants(), &id))
+}
+
+#[tauri::command]
+pub fn close_plant(
+ app: AppHandle,
+ state: State<'_, AppState>,
+ id: String,
+) -> Result {
+ into_plant_response(
+ state.runtimes(),
+ DriverRuntimeService::close(&app, state.plants(), state.runtimes(), &id),
+ )
+}
+
+#[tauri::command]
+pub fn remove_plant(
+ app: AppHandle,
+ state: State<'_, AppState>,
+ id: String,
+) -> Result {
+ into_plant_response(
+ state.runtimes(),
+ DriverRuntimeService::remove(&app, state.plants(), state.runtimes(), &id),
+ )
+}
+
+#[tauri::command]
+pub fn connect_plant(
+ app: AppHandle,
+ state: State<'_, AppState>,
+ id: String,
+) -> Result {
+ into_plant_response(
+ state.runtimes(),
+ DriverRuntimeService::connect(&app, state.plants(), state.plugins(), state.runtimes(), &id),
+ )
+}
+
+#[tauri::command]
+pub fn disconnect_plant(
+ app: AppHandle,
+ state: State<'_, AppState>,
+ id: String,
+) -> Result {
+ into_plant_response(
+ state.runtimes(),
+ DriverRuntimeService::disconnect(&app, state.plants(), state.runtimes(), &id),
+ )
+}
+
+#[tauri::command]
+pub fn pause_plant(state: State<'_, AppState>, id: String) -> Result {
+ into_plant_response(
+ state.runtimes(),
+ DriverRuntimeService::pause(state.plants(), state.runtimes(), &id),
+ )
+}
+
+#[tauri::command]
+pub fn resume_plant(state: State<'_, AppState>, id: String) -> Result {
+ into_plant_response(
+ state.runtimes(),
+ DriverRuntimeService::resume(state.plants(), state.runtimes(), &id),
+ )
+}
+
+#[tauri::command]
+pub fn save_controller(
+ state: State<'_, AppState>,
+ request: SavePlantControllerConfigRequest,
+) -> Result {
+ into_plant_response(
+ state.runtimes(),
+ DriverRuntimeService::save_controller_config(
+ state.plants(),
+ state.plugins(),
+ state.runtimes(),
+ request,
+ ),
+ )
+}
+
+#[tauri::command]
+pub fn remove_controller(
+ state: State<'_, AppState>,
+ request: RemovePlantControllerRequest,
+) -> Result {
+ into_plant_response(
+ state.runtimes(),
+ DriverRuntimeService::remove_controller(
+ state.plants(),
+ state.plugins(),
+ state.runtimes(),
+ &request,
+ ),
+ )
+}
+
+#[tauri::command]
+pub fn save_setpoint(
+ state: State<'_, AppState>,
+ request: SavePlantSetpointRequest,
+) -> Result {
+ into_plant_response(
+ state.runtimes(),
+ DriverRuntimeService::save_setpoint(state.plants(), state.runtimes(), &request),
+ )
+}
+
+#[tauri::command]
+pub fn open_plant_file(request: ImportFileRequest) -> Result {
+ PlantImportService::open_file(request.into()).map_err(ErrorDto::from)
+}
+
+#[tauri::command]
+pub fn import_plant_file(
+ state: State<'_, AppState>,
+ request: ImportFileRequest,
+) -> Result {
+ PlantImportService::import_file(state.plants(), state.plugins(), request.into())
+ .map_err(ErrorDto::from)
+}
+
+#[tauri::command]
+pub fn save_export_file(request: SaveExportFileRequest) -> Result<(), ErrorDto> {
+ let path = request.path.trim();
+ if path.is_empty() {
+ return Err(ErrorDto::from(AppError::InvalidArgument(
+ "Caminho do arquivo é obrigatório".into(),
+ )));
+ }
+
+ std::fs::write(path, request.content).map_err(|error| {
+ ErrorDto::from(AppError::IoError(format!(
+ "Falha ao salvar arquivo \"{path}\": {error}"
+ )))
+ })
+}
diff --git a/apps/desktop/src-tauri/src/commands/plugins.rs b/apps/desktop/src-tauri/src/commands/plugins.rs
new file mode 100644
index 0000000..2f8c8fb
--- /dev/null
+++ b/apps/desktop/src-tauri/src/commands/plugins.rs
@@ -0,0 +1,73 @@
+#![allow(clippy::needless_pass_by_value)]
+
+use tauri::State;
+
+use crate::core::error::ErrorDto;
+use crate::core::models::plugin::{
+ CreatePluginRequest, PluginRegistry, PluginType, UpdatePluginRequest,
+};
+use crate::core::services::plugin::PluginService;
+use crate::core::services::plugin_import::PluginImportService;
+use crate::state::AppState;
+use serde::Deserialize;
+
+#[derive(Debug, Deserialize)]
+pub struct ImportPluginFileRequest {
+ pub content: String,
+}
+
+fn into_plugin_result(
+ result: crate::core::error::AppResult,
+) -> Result {
+ result.map_err(ErrorDto::from)
+}
+
+#[tauri::command]
+pub fn create_plugin(
+ state: State<'_, AppState>,
+ request: CreatePluginRequest,
+) -> Result {
+ into_plugin_result(PluginService::create(state.plugins(), request))
+}
+
+#[tauri::command]
+pub fn get_plugin(state: State<'_, AppState>, id: String) -> Result {
+ into_plugin_result(PluginService::get(state.plugins(), &id))
+}
+
+#[tauri::command]
+pub fn update_plugin(
+ state: State<'_, AppState>,
+ request: UpdatePluginRequest,
+) -> Result {
+ into_plugin_result(PluginService::update(state.plugins(), request))
+}
+
+#[tauri::command]
+pub fn list_plugins(state: State<'_, AppState>) -> Vec {
+ PluginService::list(state.plugins())
+}
+
+#[tauri::command]
+pub fn load_plugins(state: State<'_, AppState>) -> Result, ErrorDto> {
+ PluginService::load_all(state.plugins()).map_err(ErrorDto::from)
+}
+
+#[tauri::command]
+pub fn delete_plugin(state: State<'_, AppState>, id: String) -> Result {
+ into_plugin_result(PluginService::remove(state.plugins(), &id))
+}
+
+#[tauri::command]
+pub fn import_plugin_file(request: ImportPluginFileRequest) -> Result {
+ into_plugin_result(PluginImportService::parse_file(&request.content))
+}
+
+#[tauri::command]
+#[allow(non_snake_case)]
+pub fn list_plugins_by_type(
+ state: State<'_, AppState>,
+ pluginType: PluginType,
+) -> Vec {
+ PluginService::list_by_type(state.plugins(), pluginType)
+}
diff --git a/apps/desktop/src-tauri/src/core/error.rs b/apps/desktop/src-tauri/src/core/error.rs
new file mode 100644
index 0000000..c307a77
--- /dev/null
+++ b/apps/desktop/src-tauri/src/core/error.rs
@@ -0,0 +1,42 @@
+use serde::Serialize;
+use thiserror::Error;
+
+pub type AppResult = Result;
+
+#[derive(Debug, Error)]
+pub enum AppError {
+ #[error("invalid argument: {0}")]
+ InvalidArgument(String),
+
+ #[error("not found: {0}")]
+ NotFound(String),
+
+ #[error("io error: {0}")]
+ IoError(String),
+
+ #[allow(dead_code)]
+ #[error("internal error")]
+ InternalError,
+}
+
+#[derive(Debug, Serialize)]
+pub struct ErrorDto {
+ pub code: String,
+ pub message: String,
+}
+
+impl From for ErrorDto {
+ fn from(err: AppError) -> Self {
+ let (code, message) = match err {
+ AppError::InvalidArgument(msg) => ("INVALID_ARGUMENT", msg),
+ AppError::NotFound(msg) => ("NOT_FOUND", msg),
+ AppError::IoError(msg) => ("IO_ERROR", msg),
+ AppError::InternalError => ("INTERNAL_ERROR", "An internal error occurred".to_string()),
+ };
+
+ Self {
+ code: code.to_string(),
+ message,
+ }
+ }
+}
diff --git a/apps/desktop/src-tauri/src/core/mod.rs b/apps/desktop/src-tauri/src/core/mod.rs
new file mode 100644
index 0000000..2ecb516
--- /dev/null
+++ b/apps/desktop/src-tauri/src/core/mod.rs
@@ -0,0 +1,3 @@
+pub mod error;
+pub mod models;
+pub mod services;
diff --git a/apps/desktop/src-tauri/src/core/models/mod.rs b/apps/desktop/src-tauri/src/core/models/mod.rs
new file mode 100644
index 0000000..e7712d2
--- /dev/null
+++ b/apps/desktop/src-tauri/src/core/models/mod.rs
@@ -0,0 +1,2 @@
+pub mod plant;
+pub mod plugin;
diff --git a/apps/desktop/src-tauri/src/core/models/plant.rs b/apps/desktop/src-tauri/src/core/models/plant.rs
new file mode 100644
index 0000000..5bb9e48
--- /dev/null
+++ b/apps/desktop/src-tauri/src/core/models/plant.rs
@@ -0,0 +1,261 @@
+use crate::core::models::plugin::{PluginRuntime, SchemaFieldValue};
+use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
+
+fn default_sample_time_ms() -> u64 {
+ 100
+}
+
+fn default_controller_active() -> bool {
+ false
+}
+
+fn default_controller_runtime_status() -> ControllerRuntimeStatus {
+ ControllerRuntimeStatus::Synced
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub enum VariableType {
+ Sensor,
+ Atuador,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct PlantVariable {
+ pub id: String,
+ pub name: String,
+
+ #[serde(rename = "type")]
+ pub var_type: VariableType,
+
+ pub unit: String,
+ pub setpoint: f64,
+ pub pv_min: f64,
+ pub pv_max: f64,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub linked_sensor_ids: Option>,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct CreatePlantRequest {
+ pub name: String,
+ #[serde(default = "default_sample_time_ms")]
+ pub sample_time_ms: u64,
+ pub variables: Vec,
+ pub driver: CreatePlantDriverRequest,
+ #[serde(default)]
+ pub controllers: Vec,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct UpdatePlantRequest {
+ pub id: String,
+ pub name: String,
+ #[serde(default = "default_sample_time_ms")]
+ pub sample_time_ms: u64,
+ pub variables: Vec,
+ pub driver: CreatePlantDriverRequest,
+ #[serde(default)]
+ pub controllers: Vec,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct CreatePlantVariableRequest {
+ pub name: String,
+
+ #[serde(rename = "type")]
+ pub var_type: VariableType,
+
+ pub unit: String,
+ pub setpoint: f64,
+ pub pv_min: f64,
+ pub pv_max: f64,
+
+ #[serde(default)]
+ pub linked_sensor_ids: Option>,
+}
+
+#[derive(Debug, Clone, Default, Serialize, Deserialize)]
+pub struct PlantStats {
+ pub dt: f64,
+ pub uptime: u64,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct CreatePlantDriverRequest {
+ pub plugin_id: String,
+ #[serde(default)]
+ pub config: HashMap,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub enum ControllerParamType {
+ Number,
+ Boolean,
+ String,
+}
+
+#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum ControllerRuntimeStatus {
+ #[default]
+ Synced,
+ PendingRestart,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ControllerParam {
+ #[serde(rename = "type")]
+ pub param_type: ControllerParamType,
+ pub value: SchemaFieldValue,
+ pub label: String,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct CreatePlantControllerRequest {
+ #[serde(default)]
+ pub id: Option,
+ pub plugin_id: String,
+ pub name: String,
+ pub controller_type: String,
+ #[allow(dead_code)]
+ #[serde(default = "default_controller_active")]
+ pub active: bool,
+ #[serde(default)]
+ pub input_variable_ids: Vec,
+ #[serde(default)]
+ pub output_variable_ids: Vec,
+ #[serde(default)]
+ pub params: HashMap,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct PlantDriver {
+ pub plugin_id: String,
+ pub plugin_name: String,
+ pub runtime: PluginRuntime,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub source_file: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub source_code: Option,
+ #[serde(default)]
+ pub config: HashMap,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct PlantController {
+ pub id: String,
+ pub plugin_id: String,
+ #[serde(default)]
+ pub plugin_name: String,
+ pub name: String,
+ pub controller_type: String,
+ pub active: bool,
+ #[serde(default)]
+ pub input_variable_ids: Vec,
+ #[serde(default)]
+ pub output_variable_ids: Vec,
+ #[serde(default)]
+ pub params: HashMap,
+ #[serde(default = "default_controller_runtime_status")]
+ pub runtime_status: ControllerRuntimeStatus,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct SavePlantControllerConfigRequest {
+ pub plant_id: String,
+ pub controller_id: String,
+ #[serde(default)]
+ pub plugin_id: Option,
+ pub name: String,
+ pub controller_type: String,
+ pub active: bool,
+ #[serde(default)]
+ pub input_variable_ids: Vec,
+ #[serde(default)]
+ pub output_variable_ids: Vec,
+ #[serde(default)]
+ pub params: Vec,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct SavePlantControllerParamRequest {
+ pub key: String,
+ #[serde(rename = "type")]
+ pub param_type: ControllerParamType,
+ pub value: SchemaFieldValue,
+ pub label: String,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct SavePlantSetpointRequest {
+ pub plant_id: String,
+ pub variable_id: String,
+ pub setpoint: f64,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct RemovePlantControllerRequest {
+ pub plant_id: String,
+ pub controller_id: String,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Plant {
+ pub id: String,
+ pub name: String,
+ #[serde(default = "default_sample_time_ms")]
+ pub sample_time_ms: u64,
+ pub variables: Vec,
+ pub driver: PlantDriver,
+ #[serde(default)]
+ pub controllers: Vec,
+
+ #[serde(skip, default)]
+ pub connected: bool,
+
+ #[serde(skip, default)]
+ pub paused: bool,
+
+ #[serde(skip, default = "PlantStats::default")]
+ pub stats: PlantStats,
+}
+
+#[derive(Debug, Clone, Serialize)]
+pub struct PlantResponse {
+ pub id: String,
+ pub name: String,
+ pub sample_time_ms: u64,
+ pub connected: bool,
+ pub paused: bool,
+ pub variables: Vec,
+ pub stats: PlantStats,
+ pub driver: PlantDriver,
+ #[serde(default)]
+ pub controllers: Vec,
+}
+
+impl From<&Plant> for PlantResponse {
+ fn from(plant: &Plant) -> Self {
+ Self::from(plant.clone())
+ }
+}
+
+impl From for PlantResponse {
+ fn from(plant: Plant) -> Self {
+ Self {
+ id: plant.id,
+ name: plant.name,
+ sample_time_ms: plant.sample_time_ms,
+ connected: plant.connected,
+ paused: plant.paused,
+ variables: plant.variables,
+ stats: plant.stats,
+ driver: plant.driver,
+ controllers: plant.controllers,
+ }
+ }
+}
diff --git a/apps/desktop/src-tauri/src/core/models/plugin.rs b/apps/desktop/src-tauri/src/core/models/plugin.rs
new file mode 100644
index 0000000..64635a3
--- /dev/null
+++ b/apps/desktop/src-tauri/src/core/models/plugin.rs
@@ -0,0 +1,189 @@
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum PluginType {
+ Driver,
+ Controller,
+}
+
+impl PluginType {
+ pub fn as_label(self) -> &'static str {
+ match self {
+ Self::Driver => "driver",
+ Self::Controller => "controller",
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum PluginRuntime {
+ Python,
+ RustNative,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum SchemaFieldType {
+ Bool,
+ Int,
+ Float,
+ String,
+ List,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(untagged)]
+pub enum SchemaFieldValue {
+ Bool(bool),
+ Int(i64),
+ Float(f64),
+ String(String),
+ List(Vec),
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct PluginSchemaField {
+ pub name: String,
+
+ #[serde(rename = "type")]
+ pub field_type: SchemaFieldType,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub default_value: Option,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub description: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct PluginDependency {
+ pub name: String,
+ pub version: String,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct PluginRegistry {
+ pub id: String,
+ pub name: String,
+
+ #[serde(rename = "type")]
+ pub plugin_type: PluginType,
+
+ pub runtime: PluginRuntime,
+
+ #[serde(default, skip_serializing_if = "String::is_empty")]
+ pub entry_class: String,
+
+ #[serde(default)]
+ pub schema: Vec,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub source_file: Option,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub source_code: Option,
+
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ pub dependencies: Vec,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub description: Option,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub version: Option,
+
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub author: Option,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct CreatePluginRequest {
+ pub name: String,
+
+ #[serde(rename = "type")]
+ pub plugin_type: PluginType,
+
+ pub runtime: PluginRuntime,
+
+ #[serde(default)]
+ pub entry_class: Option,
+
+ #[serde(default)]
+ pub schema: Vec,
+
+ #[serde(default)]
+ pub source_file: Option,
+
+ #[serde(default)]
+ pub source_code: Option,
+
+ #[serde(default)]
+ pub dependencies: Vec,
+
+ #[serde(default)]
+ pub description: Option,
+
+ #[serde(default)]
+ pub version: Option,
+
+ #[serde(default)]
+ pub author: Option,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct UpdatePluginRequest {
+ pub id: String,
+ pub name: String,
+
+ #[serde(rename = "type")]
+ pub plugin_type: PluginType,
+
+ pub runtime: PluginRuntime,
+
+ #[serde(default)]
+ pub entry_class: Option,
+
+ #[serde(default)]
+ pub schema: Vec,
+
+ #[serde(default)]
+ pub source_file: Option,
+
+ #[serde(default)]
+ pub source_code: Option,
+
+ #[serde(default)]
+ pub dependencies: Vec,
+
+ #[serde(default)]
+ pub description: Option,
+
+ #[serde(default)]
+ pub version: Option,
+
+ #[serde(default)]
+ pub author: Option,
+}
+
+#[allow(dead_code)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum PluginInstanceStatus {
+ Idle,
+ Running,
+ Stopped,
+ Error,
+}
+
+#[allow(dead_code)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct PluginInstance {
+ pub id: String,
+ pub plugin_id: String,
+ pub plugin_name: String,
+ pub plugin_type: PluginType,
+ pub status: PluginInstanceStatus,
+ pub config: std::collections::HashMap,
+}
diff --git a/apps/desktop/src-tauri/src/core/services/mod.rs b/apps/desktop/src-tauri/src/core/services/mod.rs
new file mode 100644
index 0000000..b01b569
--- /dev/null
+++ b/apps/desktop/src-tauri/src/core/services/mod.rs
@@ -0,0 +1,6 @@
+pub mod plant;
+pub mod plant_import;
+pub mod plugin;
+pub mod plugin_import;
+pub mod runtime;
+pub mod workspace;
diff --git a/apps/desktop/src-tauri/src/core/services/plant.rs b/apps/desktop/src-tauri/src/core/services/plant.rs
new file mode 100644
index 0000000..606ca3e
--- /dev/null
+++ b/apps/desktop/src-tauri/src/core/services/plant.rs
@@ -0,0 +1,353 @@
+mod builders;
+mod controller_params;
+mod validation;
+
+use self::builders::{build_plant, build_updated_plant, PlantRuntimeSnapshot};
+use self::validation::{
+ resolve_plugin, validate_active_controller_conflicts, validate_controller, validate_payload,
+};
+use crate::core::error::{AppError, AppResult};
+use crate::core::models::plant::{
+ ControllerParam, ControllerRuntimeStatus, CreatePlantControllerRequest, CreatePlantRequest,
+ CreatePlantVariableRequest, Plant, PlantController, RemovePlantControllerRequest,
+ SavePlantControllerConfigRequest, SavePlantSetpointRequest, UpdatePlantRequest, VariableType,
+};
+use crate::core::models::plugin::{PluginRegistry, PluginType};
+use crate::core::services::workspace::WorkspaceService;
+use crate::state::{PlantStore, PluginStore};
+
+pub struct PlantService;
+
+impl PlantService {
+ pub fn create(
+ store: &PlantStore,
+ plugins: &PluginStore,
+ request: CreatePlantRequest,
+ ) -> AppResult {
+ validate_payload(
+ None,
+ &request.name,
+ request.sample_time_ms,
+ &request.variables,
+ &request.driver,
+ &request.controllers,
+ store,
+ plugins,
+ )?;
+
+ let plant = build_plant(request, plugins)?;
+ WorkspaceService::save_plant_registry(&plant)?;
+
+ if let Err(error) = store.insert(plant.clone()) {
+ let _ = WorkspaceService::delete_plant_registry(&plant.name);
+ return Err(error);
+ }
+
+ Ok(plant)
+ }
+
+ pub fn update(
+ store: &PlantStore,
+ plugins: &PluginStore,
+ request: UpdatePlantRequest,
+ ) -> AppResult {
+ validate_payload(
+ Some(request.id.as_str()),
+ &request.name,
+ request.sample_time_ms,
+ &request.variables,
+ &request.driver,
+ &request.controllers,
+ store,
+ plugins,
+ )?;
+
+ let runtime = store.read(&request.id, |existing| PlantRuntimeSnapshot {
+ previous_name: existing.name.clone(),
+ previous_sample_time_ms: existing.sample_time_ms,
+ connected: existing.connected,
+ paused: existing.paused,
+ stats: existing.stats.clone(),
+ })?;
+
+ let previous_name = runtime.previous_name.clone();
+ let updated_plant = build_updated_plant(request, plugins, runtime)?;
+ WorkspaceService::update_plant_registry(&updated_plant, &previous_name)?;
+ store.replace(&updated_plant.id, updated_plant.clone())?;
+
+ Ok(updated_plant)
+ }
+
+ pub(crate) fn fill_missing_controller_params(
+ params: &mut std::collections::HashMap,
+ plugin: &PluginRegistry,
+ ) -> bool {
+ controller_params::fill_missing_controller_params(params, plugin)
+ }
+
+ pub fn get(store: &PlantStore, id: &str) -> AppResult {
+ store.get(id)
+ }
+
+ pub fn list(store: &PlantStore) -> Vec {
+ store.list()
+ }
+
+ pub fn remove(store: &PlantStore, id: &str) -> AppResult {
+ let plant_name = store.read(id, |plant| plant.name.clone())?;
+ WorkspaceService::delete_plant_registry(&plant_name)?;
+ store.remove(id)
+ }
+
+ pub fn close(store: &PlantStore, id: &str) -> AppResult {
+ let closed = store.update(id, |plant| {
+ for controller in &mut plant.controllers {
+ controller.active = false;
+ controller.runtime_status = ControllerRuntimeStatus::Synced;
+ }
+ })?;
+ WorkspaceService::update_plant_registry(&closed, &closed.name)?;
+ store.remove(id)
+ }
+
+ #[allow(clippy::too_many_lines)]
+ pub fn save_controller_config(
+ store: &PlantStore,
+ plugins: &PluginStore,
+ request: SavePlantControllerConfigRequest,
+ ) -> AppResult {
+ let plant_snapshot = store.read(&request.plant_id, |plant| {
+ let controller_index = plant
+ .controllers
+ .iter()
+ .position(|controller| controller.id == request.controller_id);
+ let existing_controller =
+ controller_index.map(|index| plant.controllers[index].clone());
+
+ (
+ plant.connected,
+ map_current_variables(&plant.variables),
+ plant.controllers.clone(),
+ controller_index,
+ existing_controller,
+ )
+ })?;
+ let (
+ plant_connected,
+ current_variables,
+ existing_controllers,
+ controller_index,
+ existing_controller,
+ ) = plant_snapshot;
+
+ let plugin_id = request
+ .plugin_id
+ .as_deref()
+ .filter(|value| !value.trim().is_empty())
+ .or_else(|| {
+ existing_controller
+ .as_ref()
+ .map(|controller| controller.plugin_id.as_str())
+ })
+ .ok_or_else(|| {
+ AppError::InvalidArgument("Plugin do controlador é obrigatório".into())
+ })?;
+ let plugin = resolve_plugin(plugins, plugin_id, PluginType::Controller)?;
+
+ if plant_connected
+ && existing_controller
+ .as_ref()
+ .is_some_and(|controller| controller.plugin_id != plugin.id)
+ {
+ return Err(AppError::InvalidArgument(
+ "Não é permitido trocar o plugin do controlador com a planta ligada".into(),
+ ));
+ }
+
+ let mut controller_request = CreatePlantControllerRequest {
+ id: Some(request.controller_id.clone()),
+ plugin_id: plugin.id.clone(),
+ name: request.name.clone(),
+ controller_type: request.controller_type.clone(),
+ active: request.active,
+ input_variable_ids: request.input_variable_ids.clone(),
+ output_variable_ids: request.output_variable_ids.clone(),
+ params: request
+ .params
+ .into_iter()
+ .map(|param| {
+ (
+ param.key,
+ ControllerParam {
+ param_type: param.param_type,
+ value: param.value,
+ label: param.label,
+ },
+ )
+ })
+ .collect(),
+ };
+ Self::fill_missing_controller_params(&mut controller_request.params, &plugin);
+
+ validate_controller(&controller_request, ¤t_variables, plugins)?;
+
+ let mut conflict_requests = existing_controllers
+ .iter()
+ .enumerate()
+ .map(|(index, controller)| {
+ if controller_index == Some(index) {
+ controller_request.clone()
+ } else {
+ CreatePlantControllerRequest {
+ id: Some(controller.id.clone()),
+ plugin_id: controller.plugin_id.clone(),
+ name: controller.name.clone(),
+ controller_type: controller.controller_type.clone(),
+ active: controller.active,
+ input_variable_ids: controller.input_variable_ids.clone(),
+ output_variable_ids: controller.output_variable_ids.clone(),
+ params: controller.params.clone(),
+ }
+ }
+ })
+ .collect::>();
+
+ if controller_index.is_none() {
+ conflict_requests.push(controller_request.clone());
+ }
+
+ validate_active_controller_conflicts(&conflict_requests)?;
+
+ let updated = store.update(&request.plant_id, |plant| {
+ if let Some(controller) = plant
+ .controllers
+ .iter_mut()
+ .find(|controller| controller.id == request.controller_id)
+ {
+ controller.plugin_id.clone_from(&plugin.id);
+ controller.plugin_name.clone_from(&plugin.name);
+ controller.name = request.name.trim().to_string();
+ controller.active = request.active;
+ controller
+ .input_variable_ids
+ .clone_from(&request.input_variable_ids);
+ controller
+ .output_variable_ids
+ .clone_from(&request.output_variable_ids);
+ controller.controller_type = request.controller_type.trim().to_string();
+ controller.params.clone_from(&controller_request.params);
+ return;
+ }
+
+ plant.controllers.push(PlantController {
+ id: request.controller_id.clone(),
+ plugin_id: plugin.id.clone(),
+ plugin_name: plugin.name.clone(),
+ name: request.name.trim().to_string(),
+ controller_type: request.controller_type.trim().to_string(),
+ active: request.active,
+ input_variable_ids: request.input_variable_ids.clone(),
+ output_variable_ids: request.output_variable_ids.clone(),
+ params: controller_request.params.clone(),
+ runtime_status: ControllerRuntimeStatus::Synced,
+ });
+ })?;
+
+ WorkspaceService::update_plant_registry(&updated, &updated.name)?;
+ Ok(updated)
+ }
+
+ pub fn remove_controller(
+ store: &PlantStore,
+ request: &RemovePlantControllerRequest,
+ ) -> AppResult {
+ store.read(&request.plant_id, |plant| {
+ if plant
+ .controllers
+ .iter()
+ .any(|controller| controller.id == request.controller_id)
+ {
+ Ok(())
+ } else {
+ Err(AppError::NotFound(format!(
+ "Controlador '{}' não encontrado na planta '{}'",
+ request.controller_id, plant.name
+ )))
+ }
+ })??;
+
+ let updated = store.update(&request.plant_id, |plant| {
+ plant
+ .controllers
+ .retain(|controller| controller.id != request.controller_id);
+ })?;
+
+ WorkspaceService::update_plant_registry(&updated, &updated.name)?;
+ Ok(updated)
+ }
+
+ pub fn save_setpoint(
+ store: &PlantStore,
+ request: &SavePlantSetpointRequest,
+ ) -> AppResult {
+ store.read(&request.plant_id, |plant| {
+ let variable = plant
+ .variables
+ .iter()
+ .find(|variable| variable.id == request.variable_id)
+ .ok_or_else(|| {
+ AppError::NotFound(format!(
+ "Variável '{}' não encontrada na planta '{}'",
+ request.variable_id, plant.name
+ ))
+ })?;
+
+ if variable.var_type != VariableType::Sensor {
+ return Err(AppError::InvalidArgument(
+ "Apenas sensores podem ter setpoint editado".into(),
+ ));
+ }
+
+ if request.setpoint < variable.pv_min || request.setpoint > variable.pv_max {
+ return Err(AppError::InvalidArgument(
+ "Setpoint deve estar entre pv_min e pv_max".into(),
+ ));
+ }
+
+ Ok(())
+ })??;
+
+ let updated = store.update(&request.plant_id, |plant| {
+ if let Some(variable) = plant
+ .variables
+ .iter_mut()
+ .find(|variable| variable.id == request.variable_id)
+ {
+ variable.setpoint = request.setpoint;
+ }
+ })?;
+
+ WorkspaceService::update_plant_registry(&updated, &updated.name)?;
+ Ok(updated)
+ }
+}
+
+fn map_current_variables(
+ variables: &[crate::core::models::plant::PlantVariable],
+) -> Vec {
+ variables
+ .iter()
+ .map(|variable| CreatePlantVariableRequest {
+ name: variable.name.clone(),
+ var_type: variable.var_type,
+ unit: variable.unit.clone(),
+ setpoint: variable.setpoint,
+ pv_min: variable.pv_min,
+ pv_max: variable.pv_max,
+ linked_sensor_ids: variable.linked_sensor_ids.clone(),
+ })
+ .collect()
+}
+
+#[cfg(test)]
+mod tests;
diff --git a/apps/desktop/src-tauri/src/core/services/plant/builders.rs b/apps/desktop/src-tauri/src/core/services/plant/builders.rs
new file mode 100644
index 0000000..e128fb0
--- /dev/null
+++ b/apps/desktop/src-tauri/src/core/services/plant/builders.rs
@@ -0,0 +1,140 @@
+use super::controller_params::fill_missing_controller_params;
+use super::validation::resolve_plugin;
+use crate::core::error::AppResult;
+use crate::core::models::plant::{
+ ControllerRuntimeStatus, CreatePlantControllerRequest, CreatePlantDriverRequest,
+ CreatePlantRequest, CreatePlantVariableRequest, Plant, PlantController, PlantDriver,
+ PlantStats, PlantVariable,
+};
+use crate::core::models::plugin::{PluginRegistry, PluginType};
+use crate::state::PluginStore;
+use uuid::Uuid;
+
+#[derive(Debug, Clone)]
+pub(super) struct PlantRuntimeSnapshot {
+ pub previous_name: String,
+ pub previous_sample_time_ms: u64,
+ pub connected: bool,
+ pub paused: bool,
+ pub stats: PlantStats,
+}
+
+#[allow(clippy::cast_precision_loss)]
+fn sample_time_seconds(sample_time_ms: u64) -> f64 {
+ sample_time_ms as f64 / 1000.0
+}
+
+fn same_sample_time(dt: f64, sample_time_ms: u64) -> bool {
+ (dt - sample_time_seconds(sample_time_ms)).abs() < f64::EPSILON
+}
+
+pub(super) fn build_plant(request: CreatePlantRequest, plugins: &PluginStore) -> AppResult {
+ let plant_id = format!("plant_{}", Uuid::new_v4());
+ let sample_time_ms = request.sample_time_ms;
+ let driver_plugin = resolve_plugin(plugins, &request.driver.plugin_id, PluginType::Driver)?;
+ let variables = build_variables(request.variables);
+ let controllers = request
+ .controllers
+ .into_iter()
+ .map(|controller| build_controller(controller, plugins))
+ .collect::>>()?;
+
+ Ok(Plant {
+ id: plant_id,
+ name: request.name.trim().to_string(),
+ sample_time_ms,
+ variables,
+ driver: build_driver(request.driver, &driver_plugin),
+ controllers,
+ connected: false,
+ paused: false,
+ stats: PlantStats {
+ dt: sample_time_seconds(sample_time_ms),
+ uptime: 0,
+ },
+ })
+}
+
+pub(super) fn build_updated_plant(
+ request: crate::core::models::plant::UpdatePlantRequest,
+ plugins: &PluginStore,
+ runtime: PlantRuntimeSnapshot,
+) -> AppResult {
+ let driver_plugin = resolve_plugin(plugins, &request.driver.plugin_id, PluginType::Driver)?;
+ let variables = build_variables(request.variables);
+ let controllers = request
+ .controllers
+ .into_iter()
+ .map(|controller| build_controller(controller, plugins))
+ .collect::>>()?;
+
+ let mut stats = runtime.stats;
+ if !runtime.connected || same_sample_time(stats.dt, runtime.previous_sample_time_ms) {
+ stats.dt = sample_time_seconds(request.sample_time_ms);
+ }
+
+ Ok(Plant {
+ id: request.id,
+ name: request.name.trim().to_string(),
+ sample_time_ms: request.sample_time_ms,
+ variables,
+ driver: build_driver(request.driver, &driver_plugin),
+ controllers,
+ connected: runtime.connected,
+ paused: runtime.paused,
+ stats,
+ })
+}
+
+fn build_variables(variables: Vec) -> Vec {
+ variables
+ .into_iter()
+ .enumerate()
+ .map(|(idx, var)| PlantVariable {
+ id: format!("var_{idx}"),
+ name: var.name,
+ var_type: var.var_type,
+ unit: var.unit,
+ setpoint: var.setpoint,
+ pv_min: var.pv_min,
+ pv_max: var.pv_max,
+ linked_sensor_ids: var.linked_sensor_ids,
+ })
+ .collect()
+}
+
+fn build_driver(request: CreatePlantDriverRequest, plugin: &PluginRegistry) -> PlantDriver {
+ PlantDriver {
+ plugin_id: plugin.id.clone(),
+ plugin_name: plugin.name.clone(),
+ runtime: plugin.runtime,
+ source_file: plugin.source_file.clone(),
+ source_code: None,
+ config: request.config,
+ }
+}
+
+fn build_controller(
+ request: CreatePlantControllerRequest,
+ plugins: &PluginStore,
+) -> AppResult {
+ let plugin = resolve_plugin(plugins, &request.plugin_id, PluginType::Controller)?;
+ let mut params = request.params;
+ fill_missing_controller_params(&mut params, &plugin);
+
+ Ok(PlantController {
+ id: request
+ .id
+ .filter(|value| !value.trim().is_empty())
+ .unwrap_or_else(|| format!("ctrl_{}", Uuid::new_v4().simple())),
+ plugin_id: plugin.id,
+ plugin_name: plugin.name,
+ name: request.name.trim().to_string(),
+ controller_type: request.controller_type.trim().to_string(),
+ active: request.active,
+ input_variable_ids: request.input_variable_ids,
+ output_variable_ids: request.output_variable_ids,
+ params,
+ runtime_status: ControllerRuntimeStatus::Synced,
+ })
+}
diff --git a/apps/desktop/src-tauri/src/core/services/plant/controller_params.rs b/apps/desktop/src-tauri/src/core/services/plant/controller_params.rs
new file mode 100644
index 0000000..951af36
--- /dev/null
+++ b/apps/desktop/src-tauri/src/core/services/plant/controller_params.rs
@@ -0,0 +1,93 @@
+use crate::core::models::plant::{ControllerParam, ControllerParamType};
+use crate::core::models::plugin::{
+ PluginRegistry, PluginSchemaField, SchemaFieldType, SchemaFieldValue,
+};
+use std::collections::HashMap;
+
+pub(super) fn fill_missing_controller_params(
+ params: &mut HashMap,
+ plugin: &PluginRegistry,
+) -> bool {
+ let mut changed = false;
+
+ for field in &plugin.schema {
+ if params.contains_key(&field.name) {
+ continue;
+ }
+
+ params.insert(field.name.clone(), controller_param_from_schema(field));
+ changed = true;
+ }
+
+ changed
+}
+
+fn controller_param_from_schema(field: &PluginSchemaField) -> ControllerParam {
+ let label = field
+ .description
+ .as_deref()
+ .map(str::trim)
+ .filter(|value| !value.is_empty())
+ .unwrap_or(field.name.as_str())
+ .to_string();
+
+ match field.field_type {
+ SchemaFieldType::Bool => ControllerParam {
+ param_type: ControllerParamType::Boolean,
+ value: match field.default_value.clone() {
+ Some(SchemaFieldValue::Bool(value)) => SchemaFieldValue::Bool(value),
+ _ => SchemaFieldValue::Bool(false),
+ },
+ label,
+ },
+ SchemaFieldType::Int => ControllerParam {
+ param_type: ControllerParamType::Number,
+ value: match field.default_value.clone() {
+ Some(SchemaFieldValue::Int(value)) => SchemaFieldValue::Int(value),
+ Some(SchemaFieldValue::Float(value)) => SchemaFieldValue::Float(value),
+ _ => SchemaFieldValue::Int(0),
+ },
+ label,
+ },
+ SchemaFieldType::Float => ControllerParam {
+ param_type: ControllerParamType::Number,
+ value: match field.default_value.clone() {
+ Some(SchemaFieldValue::Float(value)) => SchemaFieldValue::Float(value),
+ Some(SchemaFieldValue::Int(value)) => SchemaFieldValue::Int(value),
+ _ => SchemaFieldValue::Float(0.0),
+ },
+ label,
+ },
+ SchemaFieldType::String => ControllerParam {
+ param_type: ControllerParamType::String,
+ value: match field.default_value.clone() {
+ Some(SchemaFieldValue::String(value)) => SchemaFieldValue::String(value),
+ Some(other) => SchemaFieldValue::String(stringify_schema_value(&other)),
+ None => SchemaFieldValue::String(String::new()),
+ },
+ label,
+ },
+ SchemaFieldType::List => ControllerParam {
+ param_type: ControllerParamType::String,
+ value: match field.default_value.clone() {
+ Some(other) => SchemaFieldValue::String(stringify_schema_value(&other)),
+ None => SchemaFieldValue::String(String::new()),
+ },
+ label,
+ },
+ }
+}
+
+fn stringify_schema_value(value: &SchemaFieldValue) -> String {
+ match value {
+ SchemaFieldValue::Bool(value) => value.to_string(),
+ SchemaFieldValue::Int(value) => value.to_string(),
+ SchemaFieldValue::Float(value) => value.to_string(),
+ SchemaFieldValue::String(value) => value.clone(),
+ SchemaFieldValue::List(values) => values
+ .iter()
+ .map(stringify_schema_value)
+ .collect::>()
+ .join(", "),
+ }
+}
diff --git a/apps/desktop/src-tauri/src/core/services/plant/tests.rs b/apps/desktop/src-tauri/src/core/services/plant/tests.rs
new file mode 100644
index 0000000..a69c329
--- /dev/null
+++ b/apps/desktop/src-tauri/src/core/services/plant/tests.rs
@@ -0,0 +1,504 @@
+use super::*;
+use crate::core::models::plant::{ControllerParam, ControllerParamType, VariableType};
+use crate::core::models::plugin::{
+ PluginRuntime, PluginSchemaField, SchemaFieldType, SchemaFieldValue,
+};
+use crate::core::services::workspace::test_workspace_root;
+use crate::state::PluginStore;
+use std::collections::HashMap;
+use std::path::PathBuf;
+
+fn create_test_variable(name: &str) -> CreatePlantVariableRequest {
+ CreatePlantVariableRequest {
+ name: name.to_string(),
+ var_type: VariableType::Sensor,
+ unit: "C".to_string(),
+ setpoint: 50.0,
+ pv_min: 0.0,
+ pv_max: 100.0,
+ linked_sensor_ids: None,
+ }
+}
+
+fn create_plugin_store() -> PluginStore {
+ let store = PluginStore::new();
+
+ store
+ .insert(PluginRegistry {
+ id: "driver_plugin".to_string(),
+ name: "Driver Python".to_string(),
+ plugin_type: PluginType::Driver,
+ runtime: PluginRuntime::Python,
+ entry_class: "Driver".to_string(),
+ schema: vec![],
+ source_file: Some("driver.py".to_string()),
+ source_code: Some("class Driver:\n pass".to_string()),
+ dependencies: vec![],
+ description: None,
+ version: None,
+ author: None,
+ })
+ .unwrap();
+
+ store
+ .insert(PluginRegistry {
+ id: "controller_plugin".to_string(),
+ name: "PID".to_string(),
+ plugin_type: PluginType::Controller,
+ runtime: PluginRuntime::Python,
+ entry_class: "Controller".to_string(),
+ schema: vec![],
+ source_file: Some("controller.py".to_string()),
+ source_code: Some("class Controller:\n pass".to_string()),
+ dependencies: vec![],
+ description: None,
+ version: None,
+ author: None,
+ })
+ .unwrap();
+
+ store
+}
+
+#[test]
+fn test_fill_missing_controller_params_uses_schema_defaults() {
+ let plugin = PluginRegistry {
+ id: "controller_plugin".to_string(),
+ name: "TCLAB Controller".to_string(),
+ plugin_type: PluginType::Controller,
+ runtime: PluginRuntime::Python,
+ entry_class: "TclabController".to_string(),
+ schema: vec![PluginSchemaField {
+ name: "open_duty_1".to_string(),
+ field_type: SchemaFieldType::Float,
+ default_value: Some(SchemaFieldValue::Float(37.5)),
+ description: Some("Open duty 1".to_string()),
+ }],
+ source_file: Some("main.py".to_string()),
+ source_code: None,
+ dependencies: vec![],
+ description: None,
+ version: None,
+ author: None,
+ };
+ let mut params = HashMap::new();
+
+ let changed = PlantService::fill_missing_controller_params(&mut params, &plugin);
+
+ assert!(changed);
+ let param = params.get("open_duty_1").expect("parametro ausente");
+ assert_eq!(param.param_type, ControllerParamType::Number);
+ assert_eq!(param.label, "Open duty 1");
+ match ¶m.value {
+ SchemaFieldValue::Float(value) => assert!((*value - 37.5).abs() < f64::EPSILON),
+ other => panic!("valor inesperado para parametro default: {other:?}"),
+ }
+}
+
+fn create_valid_request(name: &str) -> CreatePlantRequest {
+ CreatePlantRequest {
+ name: name.to_string(),
+ sample_time_ms: 100,
+ variables: vec![create_test_variable("Temperatura")],
+ driver: crate::core::models::plant::CreatePlantDriverRequest {
+ plugin_id: "driver_plugin".to_string(),
+ config: HashMap::new(),
+ },
+ controllers: vec![],
+ }
+}
+
+fn plant_registry_path(name: &str) -> PathBuf {
+ test_workspace_root()
+ .join("plants")
+ .join(name)
+ .join("registry.json")
+}
+
+#[test]
+fn test_create_plant_success() {
+ let store = PlantStore::new();
+ let plugins = create_plugin_store();
+ let request = create_valid_request("Planta 1");
+
+ let result = PlantService::create(&store, &plugins, request);
+ assert!(result.is_ok());
+
+ let plant = result.unwrap();
+ assert_eq!(plant.name, "Planta 1");
+ assert_eq!(plant.sample_time_ms, 100);
+ assert_eq!(plant.variables.len(), 1);
+ assert_eq!(plant.driver.plugin_id, "driver_plugin");
+ assert!(!plant.connected);
+ assert!(!plant.paused);
+ assert!(store.exists(&plant.id));
+}
+
+#[test]
+fn test_update_plant_success() {
+ let store = PlantStore::new();
+ let plugins = create_plugin_store();
+ let created = PlantService::create(&store, &plugins, create_valid_request("Planta 1")).unwrap();
+
+ let updated = PlantService::update(
+ &store,
+ &plugins,
+ UpdatePlantRequest {
+ id: created.id.clone(),
+ name: "Planta Atualizada".to_string(),
+ sample_time_ms: 200,
+ variables: vec![create_test_variable("Nova Variável")],
+ driver: crate::core::models::plant::CreatePlantDriverRequest {
+ plugin_id: "driver_plugin".to_string(),
+ config: HashMap::new(),
+ },
+ controllers: vec![],
+ },
+ )
+ .unwrap();
+
+ assert_eq!(updated.name, "Planta Atualizada");
+ assert_eq!(updated.sample_time_ms, 200);
+ assert_eq!(updated.variables[0].name, "Nova Variável");
+}
+
+#[test]
+fn test_create_plant_empty_name() {
+ let store = PlantStore::new();
+ let plugins = create_plugin_store();
+ let request = CreatePlantRequest {
+ name: String::new(),
+ sample_time_ms: 100,
+ variables: vec![create_test_variable("Temperatura")],
+ driver: crate::core::models::plant::CreatePlantDriverRequest {
+ plugin_id: "driver_plugin".to_string(),
+ config: HashMap::new(),
+ },
+ controllers: vec![],
+ };
+
+ let result = PlantService::create(&store, &plugins, request);
+ assert!(result.is_err());
+ assert_eq!(store.count(), 0);
+}
+
+#[test]
+fn test_create_plant_whitespace_name() {
+ let store = PlantStore::new();
+ let plugins = create_plugin_store();
+ let request = CreatePlantRequest {
+ name: " ".to_string(),
+ sample_time_ms: 100,
+ variables: vec![create_test_variable("Temperatura")],
+ driver: crate::core::models::plant::CreatePlantDriverRequest {
+ plugin_id: "driver_plugin".to_string(),
+ config: HashMap::new(),
+ },
+ controllers: vec![],
+ };
+
+ let result = PlantService::create(&store, &plugins, request);
+ assert!(result.is_err());
+}
+
+#[test]
+fn test_create_plant_no_variables() {
+ let store = PlantStore::new();
+ let plugins = create_plugin_store();
+ let request = CreatePlantRequest {
+ name: "Planta 1".to_string(),
+ sample_time_ms: 100,
+ variables: vec![],
+ driver: crate::core::models::plant::CreatePlantDriverRequest {
+ plugin_id: "driver_plugin".to_string(),
+ config: HashMap::new(),
+ },
+ controllers: vec![],
+ };
+
+ let result = PlantService::create(&store, &plugins, request);
+ assert!(result.is_err());
+}
+
+#[test]
+fn test_create_plant_invalid_pv_range() {
+ let store = PlantStore::new();
+ let plugins = create_plugin_store();
+ let mut var = create_test_variable("Temp");
+ var.pv_min = 100.0;
+ var.pv_max = 0.0;
+
+ let request = CreatePlantRequest {
+ name: "Planta 1".to_string(),
+ sample_time_ms: 100,
+ variables: vec![var],
+ driver: crate::core::models::plant::CreatePlantDriverRequest {
+ plugin_id: "driver_plugin".to_string(),
+ config: HashMap::new(),
+ },
+ controllers: vec![],
+ };
+
+ let result = PlantService::create(&store, &plugins, request);
+ assert!(result.is_err());
+}
+
+#[test]
+fn test_create_plant_invalid_setpoint() {
+ let store = PlantStore::new();
+ let plugins = create_plugin_store();
+ let mut var = create_test_variable("Temp");
+ var.setpoint = 150.0;
+
+ let request = CreatePlantRequest {
+ name: "Planta 1".to_string(),
+ sample_time_ms: 100,
+ variables: vec![var],
+ driver: crate::core::models::plant::CreatePlantDriverRequest {
+ plugin_id: "driver_plugin".to_string(),
+ config: HashMap::new(),
+ },
+ controllers: vec![],
+ };
+
+ let result = PlantService::create(&store, &plugins, request);
+ assert!(result.is_err());
+}
+
+#[test]
+fn test_create_plant_multiple_variables() {
+ let store = PlantStore::new();
+ let plugins = create_plugin_store();
+ let var1 = create_test_variable("Temperatura");
+ let mut var2 = create_test_variable("Umidade");
+ var2.unit = "%".to_string();
+ var2.var_type = VariableType::Atuador;
+ var2.setpoint = 0.0;
+
+ let request = CreatePlantRequest {
+ name: "Planta Complexa".to_string(),
+ sample_time_ms: 250,
+ variables: vec![var1, var2],
+ driver: crate::core::models::plant::CreatePlantDriverRequest {
+ plugin_id: "driver_plugin".to_string(),
+ config: HashMap::new(),
+ },
+ controllers: vec![CreatePlantControllerRequest {
+ id: Some("ctrl_1".to_string()),
+ plugin_id: "controller_plugin".to_string(),
+ name: "PID 1".to_string(),
+ controller_type: "PID".to_string(),
+ active: true,
+ input_variable_ids: vec!["var_0".to_string()],
+ output_variable_ids: vec!["var_1".to_string()],
+ params: HashMap::from([(
+ "kp".to_string(),
+ ControllerParam {
+ param_type: ControllerParamType::Number,
+ value: SchemaFieldValue::Float(1.0),
+ label: "Kp".to_string(),
+ },
+ )]),
+ }],
+ };
+
+ let result = PlantService::create(&store, &plugins, request);
+ assert!(result.is_ok());
+
+ let plant = result.unwrap();
+ assert_eq!(plant.variables.len(), 2);
+ assert_eq!(plant.variables[0].id, "var_0");
+ assert_eq!(plant.variables[1].id, "var_1");
+ assert_eq!(plant.sample_time_ms, 250);
+ assert_eq!(plant.driver.plugin_id, "driver_plugin");
+ assert_eq!(plant.controllers.len(), 1);
+ assert!(plant.controllers[0].active);
+}
+
+#[test]
+fn test_create_plant_preserves_controller_active_flag() {
+ let store = PlantStore::new();
+ let plugins = create_plugin_store();
+ let var1 = create_test_variable("Temperatura");
+ let mut var2 = create_test_variable("Valvula A");
+ var2.var_type = VariableType::Atuador;
+ var2.setpoint = 0.0;
+
+ let request = CreatePlantRequest {
+ name: "Planta Com Conflito".to_string(),
+ sample_time_ms: 250,
+ variables: vec![var1, var2],
+ driver: crate::core::models::plant::CreatePlantDriverRequest {
+ plugin_id: "driver_plugin".to_string(),
+ config: HashMap::new(),
+ },
+ controllers: vec![
+ CreatePlantControllerRequest {
+ id: Some("ctrl_1".to_string()),
+ plugin_id: "controller_plugin".to_string(),
+ name: "PID 1".to_string(),
+ controller_type: "PID".to_string(),
+ active: true,
+ input_variable_ids: vec!["var_0".to_string()],
+ output_variable_ids: vec!["var_1".to_string()],
+ params: HashMap::new(),
+ },
+ CreatePlantControllerRequest {
+ id: Some("ctrl_2".to_string()),
+ plugin_id: "controller_plugin".to_string(),
+ name: "PID 2".to_string(),
+ controller_type: "PID".to_string(),
+ active: true,
+ input_variable_ids: vec!["var_0".to_string()],
+ output_variable_ids: vec!["var_1".to_string()],
+ params: HashMap::new(),
+ },
+ ],
+ };
+
+ let result = PlantService::create(&store, &plugins, request);
+ assert!(result.is_ok());
+
+ let plant = result.unwrap();
+ assert_eq!(plant.controllers.len(), 2);
+ assert!(plant.controllers.iter().all(|controller| controller.active));
+}
+
+#[test]
+fn test_create_plant_duplicate_name() {
+ let store = PlantStore::new();
+ let plugins = create_plugin_store();
+
+ let request1 = create_valid_request("Mesma Planta");
+ PlantService::create(&store, &plugins, request1).unwrap();
+
+ let request2 = create_valid_request("Mesma Planta");
+ let result = PlantService::create(&store, &plugins, request2);
+ assert!(result.is_err());
+ assert_eq!(store.count(), 1);
+}
+
+#[test]
+fn test_create_plant_invalid_sample_time() {
+ let store = PlantStore::new();
+ let plugins = create_plugin_store();
+ let request = CreatePlantRequest {
+ name: "Planta 1".to_string(),
+ sample_time_ms: 0,
+ variables: vec![create_test_variable("Temperatura")],
+ driver: crate::core::models::plant::CreatePlantDriverRequest {
+ plugin_id: "driver_plugin".to_string(),
+ config: HashMap::new(),
+ },
+ controllers: vec![],
+ };
+
+ let result = PlantService::create(&store, &plugins, request);
+ assert!(result.is_err());
+}
+
+#[test]
+fn test_get_plant() {
+ let store = PlantStore::new();
+ let plugins = create_plugin_store();
+ let request = create_valid_request("Test Get");
+ let created = PlantService::create(&store, &plugins, request).unwrap();
+
+ let found = PlantService::get(&store, &created.id).unwrap();
+ assert_eq!(found.name, "Test Get");
+}
+
+#[test]
+fn test_get_plant_not_found() {
+ let store = PlantStore::new();
+ let result = PlantService::get(&store, "invalid_id");
+ assert!(result.is_err());
+}
+
+#[test]
+fn test_list_plants() {
+ let store = PlantStore::new();
+ let plugins = create_plugin_store();
+
+ PlantService::create(&store, &plugins, create_valid_request("Plant A")).unwrap();
+ PlantService::create(&store, &plugins, create_valid_request("Plant B")).unwrap();
+
+ let plants = PlantService::list(&store);
+ assert_eq!(plants.len(), 2);
+}
+
+#[test]
+fn test_remove_plant() {
+ let store = PlantStore::new();
+ let plugins = create_plugin_store();
+ let request = create_valid_request("To Remove");
+ let plant = PlantService::create(&store, &plugins, request).unwrap();
+ let registry_path = plant_registry_path(&plant.name);
+
+ assert_eq!(store.count(), 1);
+ assert!(registry_path.exists());
+
+ let removed = PlantService::remove(&store, &plant.id).unwrap();
+ assert_eq!(removed.name, "To Remove");
+ assert_eq!(store.count(), 0);
+ assert!(!registry_path.exists());
+}
+
+#[test]
+fn test_close_plant_unloads_but_preserves_registry() {
+ let store = PlantStore::new();
+ let plugins = create_plugin_store();
+ let request = create_valid_request("To Close");
+ let plant = PlantService::create(&store, &plugins, request).unwrap();
+ let plant = store
+ .update(&plant.id, |plant| {
+ plant.controllers.push(PlantController {
+ id: "ctrl_close".to_string(),
+ plugin_id: "controller_plugin".to_string(),
+ plugin_name: "PID".to_string(),
+ name: "Controller Close".to_string(),
+ controller_type: "PID".to_string(),
+ active: true,
+ input_variable_ids: vec!["var_0".to_string()],
+ output_variable_ids: vec!["var_0".to_string()],
+ params: HashMap::new(),
+ runtime_status: ControllerRuntimeStatus::Synced,
+ });
+ })
+ .unwrap();
+ WorkspaceService::update_plant_registry(&plant, &plant.name).unwrap();
+ let registry_path = plant_registry_path(&plant.name);
+
+ assert!(registry_path.exists());
+
+ let closed = PlantService::close(&store, &plant.id).unwrap();
+ assert_eq!(closed.name, "To Close");
+ assert!(closed
+ .controllers
+ .iter()
+ .all(|controller| !controller.active));
+ assert_eq!(store.count(), 0);
+ assert!(registry_path.exists());
+
+ let registry_contents = std::fs::read_to_string(®istry_path).unwrap();
+ let persisted: serde_json::Value = serde_json::from_str(®istry_contents).unwrap();
+ let controllers = persisted
+ .get("controllers")
+ .and_then(serde_json::Value::as_array)
+ .cloned()
+ .unwrap_or_default();
+ assert_eq!(controllers.len(), 1);
+ assert_eq!(
+ controllers[0]
+ .get("active")
+ .and_then(serde_json::Value::as_bool),
+ Some(false)
+ );
+}
+
+#[test]
+fn test_remove_plant_not_found() {
+ let store = PlantStore::new();
+ let result = PlantService::remove(&store, "invalid_id");
+ assert!(result.is_err());
+}
diff --git a/apps/desktop/src-tauri/src/core/services/plant/validation.rs b/apps/desktop/src-tauri/src/core/services/plant/validation.rs
new file mode 100644
index 0000000..cb2aaba
--- /dev/null
+++ b/apps/desktop/src-tauri/src/core/services/plant/validation.rs
@@ -0,0 +1,215 @@
+use crate::core::error::{AppError, AppResult};
+use crate::core::models::plant::{
+ CreatePlantControllerRequest, CreatePlantDriverRequest, CreatePlantVariableRequest,
+ VariableType,
+};
+use crate::core::models::plugin::{PluginRegistry, PluginType};
+use crate::state::{PlantStore, PluginStore};
+use std::collections::HashMap;
+
+#[allow(clippy::too_many_arguments)]
+pub(super) fn validate_payload(
+ current_id: Option<&str>,
+ name: &str,
+ sample_time_ms: u64,
+ variables: &[CreatePlantVariableRequest],
+ driver: &CreatePlantDriverRequest,
+ controllers: &[CreatePlantControllerRequest],
+ store: &PlantStore,
+ plugins: &PluginStore,
+) -> AppResult<()> {
+ if name.trim().is_empty() {
+ return Err(AppError::InvalidArgument(
+ "Nome da planta é obrigatório".into(),
+ ));
+ }
+
+ let has_duplicate_name = current_id.map_or_else(
+ || store.exists_by_name(name),
+ |id| store.exists_by_name_except(id, name),
+ );
+
+ if has_duplicate_name {
+ return Err(AppError::InvalidArgument(format!(
+ "Planta com NOME '{name}' já existe"
+ )));
+ }
+
+ if variables.is_empty() {
+ return Err(AppError::InvalidArgument(
+ "Pelo menos uma variável deve ser definida".into(),
+ ));
+ }
+
+ if sample_time_ms == 0 {
+ return Err(AppError::InvalidArgument(
+ "Tempo de amostragem deve ser maior que 0 ms".into(),
+ ));
+ }
+
+ if driver.plugin_id.trim().is_empty() {
+ return Err(AppError::InvalidArgument(
+ "Um driver de comunicação é obrigatório".into(),
+ ));
+ }
+
+ resolve_plugin(plugins, &driver.plugin_id, PluginType::Driver)?;
+
+ for (idx, var) in variables.iter().enumerate() {
+ validate_variable(var).map_err(|error| {
+ AppError::InvalidArgument(format!("Variável {} inválida: {}", idx + 1, error))
+ })?;
+ }
+
+ for (idx, controller) in controllers.iter().enumerate() {
+ validate_controller(controller, variables, plugins).map_err(|error| {
+ AppError::InvalidArgument(format!("Controlador {} inválido: {}", idx + 1, error))
+ })?;
+ }
+
+ Ok(())
+}
+
+pub(super) fn validate_variable(var: &CreatePlantVariableRequest) -> AppResult<()> {
+ if var.name.trim().is_empty() {
+ return Err(AppError::InvalidArgument(
+ "Nome da variável é obrigatório".into(),
+ ));
+ }
+
+ if var.pv_min >= var.pv_max {
+ return Err(AppError::InvalidArgument(
+ "pv_min deve ser menor que pv_max".into(),
+ ));
+ }
+
+ if var.setpoint < var.pv_min || var.setpoint > var.pv_max {
+ return Err(AppError::InvalidArgument(
+ "setpoint deve estar entre pv_min e pv_max".into(),
+ ));
+ }
+
+ Ok(())
+}
+
+pub(super) fn validate_controller(
+ controller: &CreatePlantControllerRequest,
+ variables: &[CreatePlantVariableRequest],
+ plugins: &PluginStore,
+) -> AppResult<()> {
+ if controller.plugin_id.trim().is_empty() {
+ return Err(AppError::InvalidArgument(
+ "Plugin do controlador é obrigatório".into(),
+ ));
+ }
+
+ if controller.name.trim().is_empty() {
+ return Err(AppError::InvalidArgument(
+ "Nome do controlador é obrigatório".into(),
+ ));
+ }
+
+ if controller.controller_type.trim().is_empty() {
+ return Err(AppError::InvalidArgument(
+ "Tipo do controlador é obrigatório".into(),
+ ));
+ }
+
+ if controller.input_variable_ids.is_empty() {
+ return Err(AppError::InvalidArgument(
+ "O controlador precisa de pelo menos uma variável de entrada".into(),
+ ));
+ }
+
+ if controller.output_variable_ids.is_empty() {
+ return Err(AppError::InvalidArgument(
+ "O controlador precisa de pelo menos uma variável de saída".into(),
+ ));
+ }
+
+ let variable_types = build_variable_type_map(variables);
+
+ for input_id in &controller.input_variable_ids {
+ match variable_types.get(input_id) {
+ Some(VariableType::Sensor) => {}
+ Some(VariableType::Atuador) => {
+ return Err(AppError::InvalidArgument(format!(
+ "A variável '{input_id}' não pode ser usada como entrada"
+ )));
+ }
+ None => {
+ return Err(AppError::InvalidArgument(format!(
+ "Variável de entrada '{input_id}' não existe"
+ )));
+ }
+ }
+ }
+
+ for output_id in &controller.output_variable_ids {
+ match variable_types.get(output_id) {
+ Some(VariableType::Atuador) => {}
+ Some(VariableType::Sensor) => {
+ return Err(AppError::InvalidArgument(format!(
+ "A variável '{output_id}' não pode ser usada como saída"
+ )));
+ }
+ None => {
+ return Err(AppError::InvalidArgument(format!(
+ "Variável de saída '{output_id}' não existe"
+ )));
+ }
+ }
+ }
+
+ resolve_plugin(plugins, &controller.plugin_id, PluginType::Controller)?;
+ Ok(())
+}
+
+pub(super) fn validate_active_controller_conflicts(
+ controllers: &[CreatePlantControllerRequest],
+) -> AppResult<()> {
+ let mut ownership: HashMap<&str, &str> = HashMap::new();
+
+ for controller in controllers.iter().filter(|controller| controller.active) {
+ for output_id in &controller.output_variable_ids {
+ if let Some(existing_controller) =
+ ownership.insert(output_id.as_str(), controller.name.trim())
+ {
+ return Err(AppError::InvalidArgument(format!(
+ "A saída '{}' não pode ser controlada ao mesmo tempo por '{}' e '{}'",
+ output_id, existing_controller, controller.name
+ )));
+ }
+ }
+ }
+
+ Ok(())
+}
+
+pub(super) fn resolve_plugin(
+ plugins: &PluginStore,
+ plugin_id: &str,
+ expected_type: PluginType,
+) -> AppResult {
+ plugins.read(plugin_id, |plugin| {
+ if plugin.plugin_type == expected_type {
+ Ok(plugin.clone())
+ } else {
+ Err(AppError::InvalidArgument(format!(
+ "Plugin '{}' não é do tipo {}",
+ plugin.name,
+ expected_type.as_label()
+ )))
+ }
+ })?
+}
+
+fn build_variable_type_map(
+ variables: &[CreatePlantVariableRequest],
+) -> HashMap {
+ variables
+ .iter()
+ .enumerate()
+ .map(|(idx, variable)| (format!("var_{idx}"), variable.var_type))
+ .collect()
+}
diff --git a/apps/desktop/src-tauri/src/core/services/plant_import.rs b/apps/desktop/src-tauri/src/core/services/plant_import.rs
new file mode 100644
index 0000000..7a565bf
--- /dev/null
+++ b/apps/desktop/src-tauri/src/core/services/plant_import.rs
@@ -0,0 +1,1218 @@
+use crate::core::error::{AppError, AppResult};
+use crate::core::models::plant::{
+ ControllerParam, CreatePlantControllerRequest, CreatePlantDriverRequest, CreatePlantRequest,
+ CreatePlantVariableRequest, PlantResponse, PlantStats, PlantVariable, VariableType,
+};
+use crate::core::models::plugin::{PluginType, SchemaFieldValue};
+use crate::core::services::plant::PlantService;
+use crate::core::services::plugin::PluginService;
+use crate::state::{PlantStore, PluginStore};
+use serde::Serialize;
+use serde_json::Value;
+use std::collections::HashMap;
+
+#[derive(Debug, Clone)]
+pub struct PlantImportFileRequest {
+ pub file_name: String,
+ pub content: String,
+}
+
+#[derive(Debug, Clone, Serialize)]
+pub struct ImportedVariableStatsResponse {
+ pub error_avg: f64,
+ pub stability: f64,
+ pub ripple: f64,
+}
+
+#[derive(Debug, Clone, Serialize)]
+pub struct ImportedSeriesDescriptorResponse {
+ pub key: String,
+ pub label: String,
+ pub role: String,
+}
+
+#[derive(Debug, Clone, Serialize)]
+pub struct ImportedSeriesCatalogResponse {
+ pub plant_id: String,
+ pub series: Vec,
+}
+
+#[derive(Debug, Clone, Serialize)]
+pub struct ImportedWorkspaceDriverResponse {
+ pub plugin_id: String,
+ pub plugin_name: String,
+ pub config: HashMap,
+}
+
+#[derive(Debug, Clone, Serialize)]
+pub struct ImportedWorkspaceControllerResponse {
+ pub id: String,
+ pub plugin_id: String,
+ pub plugin_name: String,
+ pub name: String,
+ pub controller_type: String,
+ pub active: bool,
+ pub input_variable_ids: Vec,
+ pub output_variable_ids: Vec,
+ pub params: HashMap,
+}
+
+#[derive(Debug, Clone, Serialize)]
+pub struct ImportedWorkspacePlantResponse {
+ pub id: String,
+ pub name: String,
+ pub sample_time_ms: u64,
+ pub connected: bool,
+ pub paused: bool,
+ pub variables: Vec,
+ pub stats: PlantStats,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub driver: Option,
+ #[serde(default)]
+ pub controllers: Vec,
+}
+
+#[derive(Debug, Clone, Serialize)]
+pub struct OpenPlantFileResponse {
+ pub plant: ImportedWorkspacePlantResponse,
+ pub data: Vec>,
+ pub stats: PlantStats,
+ pub variable_stats: Vec,
+ pub series_catalog: ImportedSeriesCatalogResponse,
+}
+
+#[derive(Debug, Clone, Serialize)]
+pub struct ImportPlantFileResponse {
+ pub plant: PlantResponse,
+ pub data: Vec>,
+ pub stats: PlantStats,
+ pub variable_stats: Vec,
+ pub series_catalog: ImportedSeriesCatalogResponse,
+}
+
+pub struct PlantImportService;
+
+impl PlantImportService {
+ #[allow(clippy::needless_pass_by_value, clippy::too_many_lines)]
+ pub fn open_file(request: PlantImportFileRequest) -> AppResult {
+ let parsed: Value = serde_json::from_str(&request.content)
+ .map_err(|error| invalid_argument(format!("JSON inválido: {error}")))?;
+
+ let root = expect_object(&parsed, "Arquivo")?;
+ if root.get("variables").is_some() {
+ return open_registry_plant_file(root, &request);
+ }
+
+ let meta = resolve_meta(root)?;
+ let sensors = expect_array(
+ root.get("sensors")
+ .ok_or_else(|| invalid_argument("Campo \"sensors\" ausente"))?,
+ "sensors",
+ )?;
+ let actuators = expect_array(
+ root.get("actuators")
+ .ok_or_else(|| invalid_argument("Campo \"actuators\" ausente"))?,
+ "actuators",
+ )?;
+ let setpoints = expect_array(
+ root.get("setpoints")
+ .ok_or_else(|| invalid_argument("Campo \"setpoints\" ausente"))?,
+ "setpoints",
+ )?;
+ let data = expect_array(
+ root.get("data")
+ .ok_or_else(|| invalid_argument("Campo \"data\" ausente"))?,
+ "data",
+ )?;
+
+ if data.is_empty() {
+ return Err(invalid_argument("Campo \"data\" está vazio"));
+ }
+
+ let mut variables = Vec::new();
+ let mut sensor_index_by_export_id = HashMap::new();
+
+ for (index, sensor) in sensors.iter().enumerate() {
+ let sensor_obj = expect_object(sensor, &format!("sensors[{index}]"))?;
+ let sensor_id = expect_string(sensor_obj.get("id"), &format!("sensors[{index}].id"))?;
+ let name = expect_string(sensor_obj.get("name"), &format!("sensors[{index}].name"))?;
+ let unit = sensor_obj
+ .get("unit")
+ .and_then(Value::as_str)
+ .unwrap_or("%")
+ .to_string();
+
+ sensor_index_by_export_id.insert(sensor_id, index);
+ variables.push(PlantVariable {
+ id: format!("var_{index}"),
+ name,
+ var_type: VariableType::Sensor,
+ unit,
+ setpoint: 0.0,
+ pv_min: 0.0,
+ pv_max: 100.0,
+ linked_sensor_ids: None,
+ });
+ }
+
+ let actuators_offset = variables.len();
+ let mut actuator_index_by_export_id = HashMap::new();
+
+ for (index, actuator) in actuators.iter().enumerate() {
+ let actuator_obj = expect_object(actuator, &format!("actuators[{index}]"))?;
+ let actuator_id =
+ expect_string(actuator_obj.get("id"), &format!("actuators[{index}].id"))?;
+ let name = expect_string(
+ actuator_obj.get("name"),
+ &format!("actuators[{index}].name"),
+ )?;
+ let unit = actuator_obj
+ .get("unit")
+ .and_then(Value::as_str)
+ .unwrap_or("%")
+ .to_string();
+ let linked_sensor_ids = actuator_obj
+ .get("linkedSensorIds")
+ .and_then(Value::as_array)
+ .map(|items| {
+ items
+ .iter()
+ .filter_map(Value::as_str)
+ .map(|sensor_id| {
+ sensor_index_by_export_id.get(sensor_id).map_or_else(
+ || sensor_id.to_string(),
+ |sensor_index| format!("var_{sensor_index}"),
+ )
+ })
+ .collect::>()
+ });
+
+ let variable_index = actuators_offset + index;
+ actuator_index_by_export_id.insert(actuator_id, variable_index);
+ variables.push(PlantVariable {
+ id: format!("var_{variable_index}"),
+ name,
+ var_type: VariableType::Atuador,
+ unit,
+ setpoint: 0.0,
+ pv_min: 0.0,
+ pv_max: 100.0,
+ linked_sensor_ids,
+ });
+ }
+
+ let mut setpoint_sensor_map = HashMap::new();
+ for (index, setpoint) in setpoints.iter().enumerate() {
+ let setpoint_obj = expect_object(setpoint, &format!("setpoints[{index}]"))?;
+ let setpoint_id =
+ expect_string(setpoint_obj.get("id"), &format!("setpoints[{index}].id"))?;
+ let sensor_id = expect_string(
+ setpoint_obj.get("sensorId"),
+ &format!("setpoints[{index}].sensorId"),
+ )?;
+ setpoint_sensor_map.insert(setpoint_id, sensor_id);
+ }
+
+ let mut points = Vec::with_capacity(data.len());
+ for (sample_index, sample) in data.iter().enumerate() {
+ let sample_obj = expect_object(sample, &format!("data[{sample_index}]"))?;
+ let mut point = HashMap::new();
+ point.insert(
+ "time".into(),
+ expect_number(
+ sample_obj.get("time"),
+ &format!("data[{sample_index}].time"),
+ )?,
+ );
+
+ let sensors_record = expect_object(
+ sample_obj.get("sensors").ok_or_else(|| {
+ invalid_argument(format!("data[{sample_index}].sensors ausente"))
+ })?,
+ &format!("data[{sample_index}].sensors"),
+ )?;
+ for (sensor_id, value) in sensors_record {
+ if let Some(variable_index) = sensor_index_by_export_id.get(sensor_id) {
+ point.insert(
+ format!("var_{variable_index}_pv"),
+ value.as_f64().unwrap_or(0.0),
+ );
+ }
+ }
+
+ let setpoints_record = expect_object(
+ sample_obj.get("setpoints").ok_or_else(|| {
+ invalid_argument(format!("data[{sample_index}].setpoints ausente"))
+ })?,
+ &format!("data[{sample_index}].setpoints"),
+ )?;
+ for (setpoint_id, value) in setpoints_record {
+ let Some(sensor_id) = setpoint_sensor_map.get(setpoint_id) else {
+ continue;
+ };
+ let Some(variable_index) = sensor_index_by_export_id.get(sensor_id) else {
+ continue;
+ };
+ point.insert(
+ format!("var_{variable_index}_sp"),
+ value.as_f64().unwrap_or(0.0),
+ );
+ }
+
+ let actuators_record = expect_object(
+ sample_obj.get("actuators").ok_or_else(|| {
+ invalid_argument(format!("data[{sample_index}].actuators ausente"))
+ })?,
+ &format!("data[{sample_index}].actuators"),
+ )?;
+ for (actuator_id, value) in actuators_record {
+ if let Some(variable_index) = actuator_index_by_export_id.get(actuator_id) {
+ point.insert(
+ format!("var_{variable_index}_pv"),
+ value.as_f64().unwrap_or(0.0),
+ );
+ }
+ }
+
+ points.push(point);
+ }
+
+ let stats = compute_imported_plant_stats(&points);
+ let name = meta
+ .get("name")
+ .and_then(Value::as_str)
+ .filter(|value| !value.trim().is_empty())
+ .unwrap_or(request.file_name.as_str())
+ .to_string();
+ let sample_time_ms = meta
+ .get("sampleTimeMs")
+ .and_then(Value::as_u64)
+ .unwrap_or_else(|| {
+ if stats.dt > 0.0 {
+ rounded_non_negative_to_u64(stats.dt * 1000.0)
+ } else {
+ 100
+ }
+ });
+ let plant_id = format!("imported_{}", uuid::Uuid::new_v4().simple());
+ let variable_stats = variables
+ .iter()
+ .enumerate()
+ .map(|(index, variable)| compute_imported_variable_stats(&points, index, variable))
+ .collect::>();
+ let series_catalog = build_imported_series_catalog(&plant_id, &variables);
+
+ Ok(OpenPlantFileResponse {
+ plant: ImportedWorkspacePlantResponse {
+ id: plant_id,
+ name,
+ sample_time_ms,
+ connected: false,
+ paused: false,
+ variables,
+ stats: stats.clone(),
+ driver: None,
+ controllers: vec![],
+ },
+ data: points,
+ stats,
+ variable_stats,
+ series_catalog,
+ })
+ }
+
+ pub fn import_file(
+ plants: &PlantStore,
+ plugins: &PluginStore,
+ request: PlantImportFileRequest,
+ ) -> AppResult {
+ let OpenPlantFileResponse {
+ plant: imported_plant,
+ data,
+ stats,
+ variable_stats,
+ series_catalog,
+ } = Self::open_file(request)?;
+
+ PluginService::load_all(plugins)?;
+
+ let driver = resolve_imported_driver_request(plugins, imported_plant.driver.as_ref())?;
+ let controllers =
+ resolve_imported_controller_requests(plugins, &imported_plant.controllers)?;
+ let variables = imported_plant
+ .variables
+ .iter()
+ .map(map_imported_variable_to_create_request)
+ .collect::>();
+
+ let created = PlantService::create(
+ plants,
+ plugins,
+ CreatePlantRequest {
+ name: imported_plant.name,
+ sample_time_ms: imported_plant.sample_time_ms,
+ variables,
+ driver,
+ controllers,
+ },
+ )?;
+
+ Ok(ImportPlantFileResponse {
+ plant: created.into(),
+ data,
+ stats,
+ variable_stats,
+ series_catalog,
+ })
+ }
+}
+
+fn invalid_argument(message: impl Into) -> AppError {
+ AppError::InvalidArgument(message.into())
+}
+
+fn expect_object<'a>(
+ value: &'a Value,
+ context: &str,
+) -> AppResult<&'a serde_json::Map> {
+ value
+ .as_object()
+ .ok_or_else(|| invalid_argument(format!("{context} deve ser um objeto")))
+}
+
+fn expect_array<'a>(value: &'a Value, context: &str) -> AppResult<&'a Vec> {
+ value
+ .as_array()
+ .ok_or_else(|| invalid_argument(format!("{context} deve ser um array")))
+}
+
+fn resolve_meta(
+ root: &serde_json::Map,
+) -> AppResult<&serde_json::Map> {
+ match root.get("meta") {
+ Some(value) => expect_object(value, "meta"),
+ None => Ok(root),
+ }
+}
+
+fn expect_string(value: Option<&Value>, context: &str) -> AppResult {
+ value
+ .and_then(Value::as_str)
+ .map(str::to_string)
+ .ok_or_else(|| invalid_argument(format!("{context} deve ser uma string")))
+}
+
+fn expect_number(value: Option<&Value>, context: &str) -> AppResult {
+ value
+ .and_then(Value::as_f64)
+ .ok_or_else(|| invalid_argument(format!("{context} deve ser um número")))
+}
+
+fn get_value_by_keys<'a>(
+ object: &'a serde_json::Map,
+ keys: &[&str],
+) -> Option<&'a Value> {
+ keys.iter().find_map(|key| object.get(*key))
+}
+
+fn parse_variable_type(value: &str) -> AppResult {
+ match value.trim().to_lowercase().as_str() {
+ "sensor" => Ok(VariableType::Sensor),
+ "atuador" | "actuator" => Ok(VariableType::Atuador),
+ _ => Err(invalid_argument(
+ "variables.type deve ser \"sensor\" ou \"atuador\"",
+ )),
+ }
+}
+
+fn parse_registry_variable(value: &Value, index: usize) -> AppResult {
+ let variable_obj = expect_object(value, &format!("variables[{index}]"))?;
+ let id = variable_obj
+ .get("id")
+ .and_then(Value::as_str)
+ .map(str::trim)
+ .filter(|entry| !entry.is_empty())
+ .map_or_else(|| format!("var_{index}"), str::to_string);
+ let name = expect_string(
+ variable_obj.get("name"),
+ &format!("variables[{index}].name"),
+ )?;
+ let type_label = variable_obj
+ .get("type")
+ .and_then(Value::as_str)
+ .ok_or_else(|| invalid_argument(format!("variables[{index}].type deve ser string")))?;
+ let var_type = parse_variable_type(type_label)?;
+ let unit = variable_obj
+ .get("unit")
+ .and_then(Value::as_str)
+ .unwrap_or("%")
+ .to_string();
+ let setpoint = variable_obj
+ .get("setpoint")
+ .and_then(Value::as_f64)
+ .unwrap_or(0.0);
+ let pv_min = get_value_by_keys(variable_obj, &["pv_min", "pvMin"])
+ .and_then(Value::as_f64)
+ .unwrap_or(0.0);
+ let pv_max = get_value_by_keys(variable_obj, &["pv_max", "pvMax"])
+ .and_then(Value::as_f64)
+ .unwrap_or(100.0);
+ let linked_sensor_ids =
+ get_value_by_keys(variable_obj, &["linked_sensor_ids", "linkedSensorIds"])
+ .and_then(Value::as_array)
+ .map(|items| {
+ items
+ .iter()
+ .filter_map(Value::as_str)
+ .map(str::to_string)
+ .collect::>()
+ });
+
+ Ok(PlantVariable {
+ id,
+ name,
+ var_type,
+ unit,
+ setpoint,
+ pv_min,
+ pv_max,
+ linked_sensor_ids,
+ })
+}
+
+fn parse_registry_driver(
+ root: &serde_json::Map,
+) -> AppResult