diff --git a/.forgejo/workflows/dev-release.yml b/.forgejo/workflows/dev-release.yml deleted file mode 100644 index 6dd98c8..0000000 --- a/.forgejo/workflows/dev-release.yml +++ /dev/null @@ -1,91 +0,0 @@ -name: dev release - -# Publishes a dev-track build on every push to `main` (the trunk -# branch — there is no separate `dev` branch). See bread-ecosystem's -# docs/release-channels.md for the release-track policy this is part of. -on: - push: - branches: ['main'] - -jobs: - build: - runs-on: [self-hosted, hestia] - steps: - - name: checkout - run: | - set -euo pipefail - rm -rf src && mkdir src - git clone --branch main --depth 1 \ - "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src - - # bos-settings is a Tauri app: Cargo.toml lives at src/src/Cargo.toml - # (checkout root is named "src" by convention above; the repo's own - # Rust backend is also in a dir called "src", not the usual - # src-tauri — hence src/src below), and the Rust build expects the - # frontend already built at frontend/build (tauri.conf.json's - # frontendDist) — that only happens automatically under `cargo - # tauri build`, so it's run explicitly here since this workflow - # just uses plain `cargo build`. - - name: build frontend - run: | - set -euo pipefail - cd src/frontend - npm ci - npm run build - - - name: build - run: cd src/src && cargo build --release --locked - - - name: compute dev version - run: | - set -euo pipefail - cd src - # Base the dev version off the latest published stable tag, - # not Cargo.toml — Cargo.toml can go stale relative to the last - # real release, which would make a dev build sort as OLDER than - # what's already installed and bakery would correctly refuse it. - LATEST_TAG="$(git ls-remote --tags --refs \ - "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" 'v*' \ - | awk -F/ '{print $NF}' | sed 's/^v//' | (grep -v -- '-' || true) | sort -V | tail -1)" - if [ -n "${LATEST_TAG}" ]; then - CUR="${LATEST_TAG}" - else - CUR="$(grep -m1 '^version' Cargo.toml | sed -E 's/.*"(.*)".*/\1/')" - fi - IFS='.' read -r MA MI PA <<< "${CUR}" - SHA="$(git rev-parse --short HEAD)" - TS="$(date -u +%Y%m%d%H%M%S)" - echo "VERSION=${MA}.${MI}.$((PA + 1))-dev.${TS}+${SHA}" >> "$GITHUB_ENV" - - - name: prepare artifacts - run: | - set -euo pipefail - PKG_DIR="/srv/breadway-dl/dev/bos-settings/${VERSION}" - mkdir -p "${PKG_DIR}" - cp "src/src/target/release/bos-settings" "${PKG_DIR}/bos-settings-x86_64" - strip "${PKG_DIR}/bos-settings-x86_64" - sha256sum "${PKG_DIR}/bos-settings-x86_64" | awk '{print $1}' \ - > "${PKG_DIR}/bos-settings-x86_64.sha256" - cp src/packaging/bos-settings.desktop "${PKG_DIR}/" - cp src/LICENSE "${PKG_DIR}/" - cp src/bakery.toml "${PKG_DIR}/bakery.toml" - ln -sfn "${VERSION}" "/srv/breadway-dl/dev/bos-settings/latest" - - # No GitHub Release upload — dev, like the other non-stable track, - # is only distributed via dl.breadway.dev/dev/. - - name: regenerate dev index.json - env: - MINISIGN_SEC_KEY: ${{ secrets.BAKERY_MINISIGN_SEC_KEY_PATH }} - run: | - set -euo pipefail - if [ -z "${MINISIGN_SEC_KEY:-}" ]; then - echo "::error::BAKERY_MINISIGN_SEC_KEY_PATH secret not set — refusing to regenerate dev index.json unsigned (would leave a stale signature mismatched against fresh content and break bakery for everyone on the dev track)" - exit 1 - fi - rm -rf /tmp/bread-ecosystem-ci-* 2>/dev/null || true - # mktemp: a fixed clone path races when multiple repos' dev/beta - # workflows run close together on the same self-hosted runner. - ECOSYSTEM_CI_DIR="$(mktemp -d /tmp/bread-ecosystem-ci-XXXXXX)" - git clone --branch main https://git.breadway.dev/Breadway/bread-ecosystem.git "${ECOSYSTEM_CI_DIR}" - TRACK=dev bash "${ECOSYSTEM_CI_DIR}/scripts/gen-index.sh" - rm -rf "${ECOSYSTEM_CI_DIR}" diff --git a/.forgejo/workflows/mirror.yml b/.forgejo/workflows/mirror.yml new file mode 100644 index 0000000..0fee1b9 --- /dev/null +++ b/.forgejo/workflows/mirror.yml @@ -0,0 +1,21 @@ +name: Mirror to GitHub + +on: + push: + branches: ['**'] + tags: ['**'] + +jobs: + mirror: + runs-on: [self-hosted, hestia] + steps: + - name: Mirror to GitHub + run: | + set -euo pipefail + git clone --mirror "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" repo.git + cd repo.git + # Mirror only branches and tags (not refs/pull/*, which GitHub rejects); + # --prune deletes GitHub refs that no longer exist on Forgejo. + git push --prune \ + "https://x-access-token:${{ secrets.MIRROR_TOKEN }}@github.com/Breadway/bos-settings.git" \ + '+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*' diff --git a/.forgejo/workflows/package.yml b/.forgejo/workflows/package.yml new file mode 100644 index 0000000..4ae3545 --- /dev/null +++ b/.forgejo/workflows/package.yml @@ -0,0 +1,40 @@ +name: Build and publish package + +on: + push: + tags: ['v*'] + +jobs: + package: + runs-on: [self-hosted, hestia] + container: + image: archlinux:latest + steps: + # Note: no actions/checkout — the archlinux image has no Node, which JS + # actions require. Everything runs as shell steps and clones manually. + - name: Build and publish + env: + PUBLISH_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + run: | + set -euo pipefail + VERSION="${GITHUB_REF_NAME#v}" + pacman -Syu --noconfirm base-devel git rust cargo gtk4 glib2 + useradd -m builder + git config --global --add safe.directory '*' + git clone --branch "${GITHUB_REF_NAME}" --depth 1 \ + "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" /home/builder/src + cd /home/builder/src + git archive --format=tar.gz --prefix="bos-settings-${VERSION}/" HEAD \ + > packaging/bos-settings-${VERSION}.tar.gz + SHA=$(sha256sum packaging/bos-settings-${VERSION}.tar.gz | awk '{print $1}') + sed -i "s/^pkgver=.*/pkgver=${VERSION}/" packaging/PKGBUILD + sed -i "s/^sha256sums=.*/sha256sums=('${SHA}')/" packaging/PKGBUILD + chown -R builder:builder /home/builder/src + # --nocheck: packaging builds the artifact; tests belong in a CI job. + su builder -c "cd /home/builder/src/packaging && makepkg -f --noconfirm --nocheck" + PKG=$(find /home/builder/src/packaging -name '*.pkg.tar.zst' | head -1) + curl -fsS -X PUT \ + -H "Authorization: token ${PUBLISH_TOKEN}" \ + -H "Content-Type: application/octet-stream" \ + --data-binary "@${PKG}" \ + "https://git.breadway.dev/api/packages/Breadway/arch/os" diff --git a/.forgejo/workflows/rc-release.yml b/.forgejo/workflows/rc-release.yml deleted file mode 100644 index ee123ec..0000000 --- a/.forgejo/workflows/rc-release.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: beta (rc) release - -# Publishes a beta-track build for any `vX.Y.Z-rc.N` prerelease tag -# pushed to `main` — there is no separate `beta` branch; "freezing" is -# just pausing pushes to main while an RC gets tested. See -# bread-ecosystem's docs/release-channels.md for the release-track policy. -on: - push: - tags: ['v*'] - -jobs: - build: - if: ${{ contains(github.ref_name, '-rc.') }} - runs-on: [self-hosted, hestia] - steps: - - name: checkout - run: | - set -euo pipefail - rm -rf src && mkdir src - git clone --branch "${GITHUB_REF_NAME}" --depth 1 \ - "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src - - - name: prepare artifacts - run: | - set -euo pipefail - VERSION="${GITHUB_REF_NAME#v}" - PKG_DIR="/srv/breadway-dl/beta/bos-settings/${VERSION}" - mkdir -p "${PKG_DIR}" - cp "src/src/target/release/bos-settings" "${PKG_DIR}/bos-settings-x86_64" - strip "${PKG_DIR}/bos-settings-x86_64" - sha256sum "${PKG_DIR}/bos-settings-x86_64" | awk '{print $1}' \ - > "${PKG_DIR}/bos-settings-x86_64.sha256" - cp src/packaging/bos-settings.desktop "${PKG_DIR}/" - cp src/LICENSE "${PKG_DIR}/" - cp src/bakery.toml "${PKG_DIR}/bakery.toml" - ln -sfn "${VERSION}" "/srv/breadway-dl/beta/bos-settings/latest" - - # No GitHub Release upload — beta, like dev, is only distributed via - # dl.breadway.dev/beta/. - - name: regenerate beta index.json - env: - MINISIGN_SEC_KEY: ${{ secrets.BAKERY_MINISIGN_SEC_KEY_PATH }} - run: | - set -euo pipefail - if [ -z "${MINISIGN_SEC_KEY:-}" ]; then - echo "::error::BAKERY_MINISIGN_SEC_KEY_PATH secret not set — refusing to regenerate beta index.json unsigned (would leave a stale signature mismatched against fresh content and break bakery for everyone on the beta track)" - exit 1 - fi - rm -rf /tmp/bread-ecosystem-ci-* 2>/dev/null || true - # mktemp: a fixed clone path races when multiple repos' dev/beta - # workflows run close together on the same self-hosted runner. - ECOSYSTEM_CI_DIR="$(mktemp -d /tmp/bread-ecosystem-ci-XXXXXX)" - git clone https://git.breadway.dev/Breadway/bread-ecosystem.git "${ECOSYSTEM_CI_DIR}" - TRACK=beta bash "${ECOSYSTEM_CI_DIR}/scripts/gen-index.sh" - rm -rf "${ECOSYSTEM_CI_DIR}" diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 46217eb..8fa9fad 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -6,7 +6,6 @@ on: jobs: build: - if: ${{ !contains(github.ref_name, '-rc.') }} runs-on: [self-hosted, hestia] steps: - name: checkout @@ -16,23 +15,8 @@ jobs: git clone --branch "${GITHUB_REF_NAME}" --depth 1 \ "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src - # bos-settings is a Tauri app: Cargo.toml lives at src/src/Cargo.toml - # (checkout root is named "src" by convention above; the repo's own - # Rust backend is also in a dir called "src", not the usual - # src-tauri — hence src/src below), and the Rust build expects the - # frontend already built at frontend/build (tauri.conf.json's - # frontendDist) — that only happens automatically under `cargo - # tauri build`, so it's run explicitly here since this workflow - # just uses plain `cargo build`. - - name: build frontend - run: | - set -euo pipefail - cd src/frontend - npm ci - npm run build - - name: build - run: cd src/src && cargo build --release --locked + run: cd src && cargo build --release --locked - name: prepare artifacts run: | @@ -40,29 +24,19 @@ jobs: VERSION="${GITHUB_REF_NAME#v}" PKG_DIR="/srv/breadway-dl/bos-settings/${VERSION}" mkdir -p "${PKG_DIR}" - cp "src/src/target/release/bos-settings" "${PKG_DIR}/bos-settings-x86_64" + cp "src/target/release/bos-settings" "${PKG_DIR}/bos-settings-x86_64" strip "${PKG_DIR}/bos-settings-x86_64" sha256sum "${PKG_DIR}/bos-settings-x86_64" | awk '{print $1}' \ > "${PKG_DIR}/bos-settings-x86_64.sha256" - cp src/packaging/bos-settings.desktop "${PKG_DIR}/" - cp src/LICENSE "${PKG_DIR}/" cp src/bakery.toml "${PKG_DIR}/bakery.toml" ln -sfn "${VERSION}" "/srv/breadway-dl/bos-settings/latest" - name: regenerate index.json - env: - MINISIGN_SEC_KEY: ${{ secrets.BAKERY_MINISIGN_SEC_KEY_PATH }} run: | set -euo pipefail - if [ -z "${MINISIGN_SEC_KEY:-}" ]; then - echo "::error::BAKERY_MINISIGN_SEC_KEY_PATH secret not set — refusing to regenerate index.json unsigned (would leave a stale signature mismatched against fresh content and break bakery for everyone)" - exit 1 - fi - rm -rf /tmp/bread-ecosystem-ci-* 2>/dev/null || true - ECOSYSTEM_CI_DIR="$(mktemp -d /tmp/bread-ecosystem-ci-XXXXXX)" - git clone https://git.breadway.dev/Breadway/bread-ecosystem.git "${ECOSYSTEM_CI_DIR}" - bash "${ECOSYSTEM_CI_DIR}/scripts/gen-index.sh" - rm -rf "${ECOSYSTEM_CI_DIR}" + rm -rf /tmp/bread-ecosystem-ci + git clone https://git.breadway.dev/Breadway/bread-ecosystem.git /tmp/bread-ecosystem-ci + bash /tmp/bread-ecosystem-ci/scripts/gen-index.sh - name: upload to GitHub Release env: diff --git a/.gitignore b/.gitignore index df7d56a..45cd856 100644 --- a/.gitignore +++ b/.gitignore @@ -32,8 +32,3 @@ logs/ # Claude Code local agent state .claude/ - -# .freebuff local tool state (not for commit) -.freebuff/ -# graphify knowledge-graph output (local tool cache, not for commit) -graphify-out/ diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index b61a76f..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,33 +0,0 @@ -# AGENTS.md — Repo hygiene - -Scope: this file covers *repo hygiene* — branching, remotes, CI, cleanup. It is not project documentation. - -This repo is a **Tauri 2 + Svelte 5** settings app, bakery-distributed. It follows the branch/release workflow in `CONTRIBUTING.md` — read and follow it for any git, branch, or release work here (the single-trunk model, `feature/x`/`fix/x` branch naming, how RC tags work, etc). Don't improvise a different workflow. The short version: there is one long-lived branch, `main` — no `dev` or `beta` branch exists. `main` auto-publishes a bakery **dev-track** build on every push. "Beta" and "stable" are both just tags, not branches: push a `vX.Y.Z-rc.N` tag to publish a beta-track build, push a plain `vX.Y.Z` tag to cut the signed stable release. "Freezing" for stabilization means pausing pushes to `main`, not moving a branch. - -This is not a GTK4 app. There is no `package.yml` pacman workflow here; bakery is the distribution channel. - -## Layout - -- `frontend/` — Svelte 5 + SvelteKit (static adapter) + TypeScript -- `src/` — Tauri 2 Rust crate (`bos-settings`). Commands live in `src/src/commands/`. -- Config edits are non-destructive (`toml_edit` / `bread_utils::tomlcfg`) except for JSON files that have no comments to preserve. - -## Remotes - -- `origin` — Forgejo (`git.breadway.dev` via Hestia, SSH) — authoritative. -- `github` — GitHub mirror. Push `origin` only; the github remote auto-mirrors. - -## CI - -- `dev-release.yml` — push to `main`. -- `rc-release.yml` — `vX.Y.Z-rc.N` tags. -- `release.yml` — other `v*` tags (signed stable). - -No build/lint/test CI runs on ordinary commits or PRs to `main` beyond the dev-track workflow above. - -## Don't - -- Don't commit `frontend/node_modules`. -- Don't embed credentials in remote URLs — SSH or a credential helper only. -- Don't write Wi-Fi passwords back into `breadcrumbs.toml`. Networks live in `~/.config/breadcrumbs/networks.toml` (0600). -- Don't expose a generic argv runner to the webview. Streaming updates are typed commands (`bakery_update`, `pacman_system_update`, `fwupd_*`). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 404880c..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,92 +0,0 @@ -# Contributing - -`bos-settings` — System settings app for Bread OS. - -Part of the bread ecosystem; this repo follows the same branch/release -workflow as every other ecosystem product. - -## Branches - -There is one long-lived branch: **`main`**. All day-to-day work lands here. -Every push to `main` automatically builds and publishes a **dev-track** -build (see Tracks below) — a real install you can test before cutting -anything more formal. - -New work — features and bug fixes alike — goes on a short-lived branch: - -``` -feature/ -fix/ -``` - -Branch off `main`, open a PR/push back into `main` when ready. Short-lived -branches get deleted on merge — they never accumulate the kind of drift a -second long-lived branch does. - -## The release cycle - -There's no separate `beta` or release branch — "stable" and "beta" are both -just **tags** on `main`, not branches that need to be kept in sync: - -1. Work accumulates on `main` via `feature/x` / `fix/x` branches. Each push - auto-publishes a dev build — install it with `bakery track set dev` and - `bakery update --all`, then fix anything broken with another push. -2. When you want to stabilize before a real release, tag a release - candidate: `git tag vX.Y.Z-rc.1 && git push origin vX.Y.Z-rc.1` (push to - both remotes). That tag alone triggers a beta-track build — - "freezing" is just pausing pushes to `main` while you test it, not a - branch operation. Cut `-rc.2`, `-rc.3`, etc. for further fixes. -3. Once an RC has gone without issues, tag the real release: - `git tag vX.Y.Z && git push origin vX.Y.Z` — that's what triggers the - signed stable release build. - -## Tracks, from a user's perspective - -``` -bakery track show # what you're currently on (defaults to stable) -bakery track set dev # or beta, or stable -bakery update --all # pull the latest build on your current track -``` - -| Track | What it is | Published from | -|--------|-----------|-----------------| -| `stable` | The last tagged release | a `vX.Y.Z` tag | -| `beta` | Latest release candidate | a `vX.Y.Z-rc.N` tag | -| `dev` | Bleeding edge | `main`, on every push | - -Dev versions are auto-computed (`X.Y.Z-dev.+`) from the -latest published stable tag, so they always sort as newer than what you -have installed — no manual version bumping needed. Beta versions are just -the RC tag itself (already valid semver, already sorts below the real -release it's a candidate for). - -## Local development - -`bos-settings` is a Tauri app: the Svelte frontend lives in `frontend/`, -the Rust backend in `src/src/` (the checkout root is conventionally named -`src`, and the backend's own crate is also called `src`, not `src-tauri`). -The frontend must be built before the Rust build — `tauri.conf.json`'s -`beforeBuildCommand` hook only fires under `cargo tauri build`, not plain -`cargo build`, so CI runs it explicitly: - -```sh -cd frontend && npm ci && npm run build -cd ../src/src && cargo build --release --locked -cargo test --release --locked -``` - -## CI - -- `dev-release.yml` — triggered on push to `main`. -- `rc-release.yml` — triggered on any `vX.Y.Z-rc.N` tag push. -- `release.yml` — triggered on any other `v*` tag push, cuts the actual - stable release. - -All CI runs on a self-hosted runner; nothing runs automatically on plain -commits or PRs beyond the track builds above. See -[bread-ecosystem's docs/release-channels.md](https://git.breadway.dev/Breadway/bread-ecosystem/src/branch/main/docs/release-channels.md) -for the full policy, including how a new product gets wired onto these tracks. - -## Questions - -Open an issue on this repo's Forgejo tracker. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..e1449b5 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,990 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[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 = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bos-settings" +version = "0.7.0" +dependencies = [ + "async-channel", + "bread-theme", + "bread-utils", + "glib", + "gtk4", + "serde", + "serde_json", + "toml 0.8.23", + "toml_edit 0.22.27", +] + +[[package]] +name = "bread-theme" +version = "0.2.3" +source = "git+https://github.com/Breadway/bread-ecosystem?tag=v0.2.10#17d1bb85801b9a8c195b64c02d288cd662c9c780" +dependencies = [ + "dirs", + "gtk4", + "serde", + "serde_json", +] + +[[package]] +name = "bread-utils" +version = "0.3.0" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.3.0#8e82d2d833e992ce939a5b836f910ee109f2e939" +dependencies = [ + "dirs", + "serde", + "serde_json", + "toml_edit 0.22.27", +] + +[[package]] +name = "cairo-rs" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc8d9aa793480744cd9a0524fef1a2e197d9eaa0f739cde19d16aba530dcb95" +dependencies = [ + "bitflags", + "cairo-sys-rs", + "glib", + "libc", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8b4985713047f5faee02b8db6a6ef32bbb50269ff53c1aee716d1d195b76d54" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "cfg-expr" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb693542bcafa528e198be0ebd9d3632ca5b7c93dbe7237460e199910835997c" +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 = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[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", + "windows-sys 0.48.0", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[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 = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-executor" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25f420376dbee041b2db374ce4573892a36222bb3f6c0c43e24f0d67eae9b646" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f31b37b1fc4b48b54f6b91b7ef04c18e00b4585d98359dd7b998774bbd91fb" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk4" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d81e2a6c6ecba2aab60633a98df1868b03fa0bfdce8105edc27c1bccf71f0e39" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk4-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk4-sys" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d8f608d8d7d229975c4d0d026f5d3071598c4ddab3c5262b0a31840fec78d13" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gio" +version = "0.22.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b3e1f669909c326b9413bde5a742097b8c90a7d78f45326db13668984769ded" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "pin-project-lite", + "smallvec", +] + +[[package]] +name = "gio-sys" +version = "0.22.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353fdc7da7cd16da916104b1e0e4e7de380ec9c8aaa20d4d742d66310ab4b0d5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "windows-sys 0.61.2", +] + +[[package]] +name = "glib" +version = "0.22.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddbcf514bd1881fc1b960e4e52b4e82873f4da3bceddbd58d42827b508888100" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "smallvec", +] + +[[package]] +name = "glib-macros" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "506d23499707c7142898429757e8d9a3871d965239a2cb66dfa05052be6d6f19" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "glib-sys" +version = "0.22.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "030967459f9f676851872c6304adea7825c6d462ec9b72554c733cf0c5952233" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "gobject-sys" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22a861859b887a79cf461359c192c97a57d8fb0229dd291232e57aa11f6fa72c" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "graphene-rs" +version = "0.22.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb856b9c558971c3f13ab692358926da710b046932a4e087aedcc35b040d7dff" +dependencies = [ + "glib", + "graphene-sys", +] + +[[package]] +name = "graphene-sys" +version = "0.22.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7ffdfde88f3570d3705e0d8a2433e036d387a1f2930bbf47eafcb5f569fd04" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gsk4" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b867be1c5f14dcb8f552c0eff6e9a9b1da5f8b43943e8efc3a63c889d84952ff" +dependencies = [ + "cairo-rs", + "gdk4", + "glib", + "graphene-rs", + "gsk4-sys", + "libc", + "pango", +] + +[[package]] +name = "gsk4-sys" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b7c7eb2e681ee896646cfb8872b431f24d09f53ba9283289d9b10caa6707088" +dependencies = [ + "cairo-sys-rs", + "gdk4-sys", + "glib-sys", + "gobject-sys", + "graphene-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk4" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98a0a0466484f64b07b5b8184d43fa46be78eb0b8e04ae4e179af31d770b76d9" +dependencies = [ + "cairo-rs", + "field-offset", + "futures-channel", + "gdk-pixbuf", + "gdk4", + "gio", + "glib", + "graphene-rs", + "gsk4", + "gtk4-macros", + "gtk4-sys", + "libc", + "pango", +] + +[[package]] +name = "gtk4-macros" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ac7179400a36a04de039c24206bb841c5596992b907b43b23ee8d5bdc40d00e" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "gtk4-sys" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f954786af0b1984425c4446b77f5ff6594346181316be3f850caab1c6f01" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk4-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "graphene-sys", + "gsk4-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libredox" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "pango" +version = "0.22.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d800d8d0de2ad5d0fb046f5344dbaba14a003cf3dd27cc21d85893d35ea316c" +dependencies = [ + "gio", + "glib", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd111a20ca90fedf03e09c59783c679c00900f1d8491cca5399f5e33609d5d6" +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 = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.13+spec-1.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[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_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", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "system-deps" +version = "7.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml 1.1.3+spec-1.1.0", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[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", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[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_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[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_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[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_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..e396c3f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "bos-settings" +version = "0.7.0" +edition = "2021" + +[dependencies] +gtk4 = { version = "0.11", features = ["v4_12"] } +glib = "0.22" +# Shared ecosystem theming — bos-settings loads the same generated stylesheet as +# breadbar/breadbox/breadpad so the whole desktop looks consistent. +bread-theme = { git = "https://github.com/Breadway/bread-ecosystem", tag = "v0.2.10", features = ["gtk"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +toml = "0.8" +# toml_edit drives non-destructive config editing: it preserves comments and +# any keys the UI doesn't model, so saving a single field never rewrites or +# drops the rest of the user's config file. +toml_edit = "0.22" +async-channel = "2" +# TODO(owner): switch to tag-pinned git dependency once bread-utils is merged and tagged, matching the bread-theme pattern +bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.3.0", features = ["toml"] } diff --git a/README.md b/README.md index a705ad7..08161fe 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,15 @@ # bos-settings -System settings app for [BOS (Bread Operating System)](https://git.breadway.dev/Breadway/bos) — Tauri 2 + Svelte 5. Configures every bread\* app's config plus core system settings (network, sound, power, users, firewall, snapshots, packages, AUR, firmware, Hyprland display/appearance/autostart) non-destructively. +System settings app for [BOS (Bread Operating System)](https://git.breadway.dev/Breadway/bos) — GTK4, configures every bread\* app's config plus core system settings (network, sound, power, users, firewall, snapshots, packages, AUR, firmware, Hyprland display/appearance/autostart) non-destructively. -Distributed via `bakery`. There is one long-lived branch, `main`; see `CONTRIBUTING.md` for the single-trunk / RC-tag release model shared across the bread ecosystem. +Split out of the `bos` repo into its own repo so a bos-settings release doesn't require a BOS ISO release, and vice versa. Still the only pacman-packaged (not bakery-managed) bread app — see `packaging/README.md`. ## Building -The Svelte frontend lives in `frontend/`, the Rust backend in `src/` (this repo's crate is not named `src-tauri`). `cargo tauri build` runs the frontend build hook; a plain `cargo build` does not. - ```bash -cd frontend && npm ci && npm run build -cd ../src && cargo build --release -``` - -Dev (Vite + `cargo tauri dev`): - -```bash -cd src && cargo tauri dev +cargo build --release ``` ## Packaging / releasing -Bump `src/Cargo.toml` (and `frontend/package.json`) version, then follow `CONTRIBUTING.md`: work lands on `main` via `feature/` / `fix/` branches (every push to `main` publishes a bakery **dev** build). Tag `vX.Y.Z-rc.N` for beta, `vX.Y.Z` for the signed stable release. Do not push to a `dev` branch — there isn't one. +See `packaging/README.md`. In short: bump `Cargo.toml`'s version, tag `vX.Y.Z`, push the tag to both remotes — `.forgejo/workflows/package.yml` builds and publishes to the `[breadway]` pacman repo automatically. diff --git a/bakery.toml b/bakery.toml index 9b83e9a..632de76 100644 --- a/bakery.toml +++ b/bakery.toml @@ -1,11 +1,9 @@ name = "bos-settings" description = "System settings app for Bread OS" binaries = ["bos-settings"] -system_deps = ["webkit2gtk-4.1", "gtk3", "libsoup3", "librsvg", "hicolor-icon-theme"] +system_deps = ["gtk4", "glib2"] optional_system_deps = ["snapper"] bread_deps = [] -license_file = "LICENSE" -desktop_file = "bos-settings.desktop" [config] dir = "~/.config" diff --git a/frontend/.gitignore b/frontend/.gitignore deleted file mode 100644 index 6635cf5..0000000 --- a/frontend/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -.DS_Store -node_modules -/build -/.svelte-kit -/package -.env -.env.* -!.env.example -vite.config.js.timestamp-* -vite.config.ts.timestamp-* diff --git a/frontend/README.md b/frontend/README.md deleted file mode 100644 index 858d179..0000000 --- a/frontend/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Tauri + SvelteKit + TypeScript - -This template should help get you started developing with Tauri, SvelteKit and TypeScript in Vite. - -## Recommended IDE Setup - -[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer). diff --git a/frontend/package-lock.json b/frontend/package-lock.json deleted file mode 100644 index 03950c8..0000000 --- a/frontend/package-lock.json +++ /dev/null @@ -1,1960 +0,0 @@ -{ - "name": "bos-settings-frontend", - "version": "0.8.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "bos-settings-frontend", - "version": "0.8.0", - "license": "MIT", - "dependencies": { - "@lucide/svelte": "^1.25.0", - "@tauri-apps/api": "^2", - "@tauri-apps/plugin-dialog": "^2.7.2", - "@tauri-apps/plugin-opener": "^2" - }, - "devDependencies": { - "@sveltejs/adapter-static": "^3.0.6", - "@sveltejs/kit": "^2.9.0", - "@sveltejs/vite-plugin-svelte": "^5.0.0", - "@tauri-apps/cli": "^2", - "@types/node": "^26.4.0", - "svelte": "^5.0.0", - "svelte-check": "^4.0.0", - "typescript": "~5.6.2", - "vite": "^6.0.3" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@lucide/svelte": { - "version": "1.25.0", - "resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-1.25.0.tgz", - "integrity": "sha512-v9m+dD68jxVnqkU3K59mG/RSRFlPGzmKCGSyMfnXcaGv9jODDQMyQkcp1CGvk3Y/cUj9v7f8rw1n//K0B53xGQ==", - "license": "ISC", - "peerDependencies": { - "svelte": "^5" - } - }, - "node_modules/@polka/url": { - "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", - "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", - "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", - "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", - "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", - "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", - "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", - "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", - "cpu": [ - "arm" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", - "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", - "cpu": [ - "arm" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", - "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", - "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", - "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", - "cpu": [ - "loong64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", - "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", - "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", - "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", - "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", - "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", - "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", - "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", - "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", - "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", - "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", - "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", - "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", - "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", - "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sveltejs/acorn-typescript": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.11.tgz", - "integrity": "sha512-LFuZUkjJ9iF7JZye/aG5XM0SFcQ5VyL0oVX4WJ9dc0Va3R3s0OauX1BESVCb+YN/ol8TAfqGDDAQsTG627Y5kw==", - "license": "MIT", - "peerDependencies": { - "acorn": "^8.9.0" - } - }, - "node_modules/@sveltejs/adapter-static": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.10.tgz", - "integrity": "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@sveltejs/kit": "^2.0.0" - } - }, - "node_modules/@sveltejs/kit": { - "version": "2.70.1", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.70.1.tgz", - "integrity": "sha512-nY9SPHGOZro3doud9vZXDBwl9tCZIouuJztjgSHs6PAIrv9M/z5O7eOhPV5xU7CgVHA976Jwu3BA1hIFvXztkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "@sveltejs/acorn-typescript": "^1.0.9", - "@types/cookie": "^0.6.0", - "acorn": "^8.16.0", - "cookie": "^0.6.0", - "devalue": "^5.8.1", - "esm-env": "^1.2.2", - "kleur": "^4.1.5", - "magic-string": "^0.30.5", - "mrmime": "^2.0.0", - "set-cookie-parser": "^3.0.0", - "sirv": "^3.0.0" - }, - "bin": { - "svelte-kit": "svelte-kit.js" - }, - "engines": { - "node": ">=18.13" - }, - "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 || ^7.0.0", - "svelte": "^4.0.0 || ^5.0.0-next.0", - "typescript": "^5.3.3 || ^6.0.0", - "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/@sveltejs/load-config": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@sveltejs/load-config/-/load-config-0.2.0.tgz", - "integrity": "sha512-1LgZ/qUqSoq+QorD83lk2hka79Px0wXNW2q5V1nZlxGhQgw1jrsIbVz5YiCeucVLo4XvFLjXukUaQjIiqowkcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 18.0.0" - } - }, - "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.1.tgz", - "integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", - "debug": "^4.4.1", - "deepmerge": "^4.3.1", - "kleur": "^4.1.5", - "magic-string": "^0.30.17", - "vitefu": "^1.0.6" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22" - }, - "peerDependencies": { - "svelte": "^5.0.0", - "vite": "^6.0.0" - } - }, - "node_modules/@sveltejs/vite-plugin-svelte-inspector": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", - "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.7" - }, - "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" - } - }, - "node_modules/@tauri-apps/api": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.1.tgz", - "integrity": "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==", - "license": "Apache-2.0 OR MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/tauri" - } - }, - "node_modules/@tauri-apps/cli": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.4.tgz", - "integrity": "sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==", - "dev": true, - "license": "Apache-2.0 OR MIT", - "bin": { - "tauri": "tauri.js" - }, - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/tauri" - }, - "optionalDependencies": { - "@tauri-apps/cli-darwin-arm64": "2.11.4", - "@tauri-apps/cli-darwin-x64": "2.11.4", - "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.4", - "@tauri-apps/cli-linux-arm64-gnu": "2.11.4", - "@tauri-apps/cli-linux-arm64-musl": "2.11.4", - "@tauri-apps/cli-linux-riscv64-gnu": "2.11.4", - "@tauri-apps/cli-linux-x64-gnu": "2.11.4", - "@tauri-apps/cli-linux-x64-musl": "2.11.4", - "@tauri-apps/cli-win32-arm64-msvc": "2.11.4", - "@tauri-apps/cli-win32-ia32-msvc": "2.11.4", - "@tauri-apps/cli-win32-x64-msvc": "2.11.4" - } - }, - "node_modules/@tauri-apps/cli-darwin-arm64": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.4.tgz", - "integrity": "sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-darwin-x64": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.4.tgz", - "integrity": "sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.4.tgz", - "integrity": "sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-arm64-gnu": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.4.tgz", - "integrity": "sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-arm64-musl": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.4.tgz", - "integrity": "sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.4.tgz", - "integrity": "sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-x64-gnu": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.4.tgz", - "integrity": "sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-linux-x64-musl": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.4.tgz", - "integrity": "sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-win32-arm64-msvc": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.4.tgz", - "integrity": "sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-win32-ia32-msvc": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.4.tgz", - "integrity": "sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/cli-win32-x64-msvc": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.4.tgz", - "integrity": "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0 OR MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tauri-apps/plugin-dialog": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.7.2.tgz", - "integrity": "sha512-pX0IGm1I3I6wc+zeKYcq1GSqogK6okCNX5fOdaNU5ab1AjGS6l1E5wFNjEb7meg7ZFSp0JUs+0jQGQNyOvLrsg==", - "license": "MIT OR Apache-2.0", - "dependencies": { - "@tauri-apps/api": "^2.11.0" - } - }, - "node_modules/@tauri-apps/plugin-opener": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.4.tgz", - "integrity": "sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ==", - "license": "MIT OR Apache-2.0", - "dependencies": { - "@tauri-apps/api": "^2.11.0" - } - }, - "node_modules/@types/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "26.4.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.0.tgz", - "integrity": "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~8.3.0" - } - }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT" - }, - "node_modules/acorn": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", - "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/aria-query": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", - "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/axobject-query": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/devalue": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.2.tgz", - "integrity": "sha512-DObPPAfdtFbXjxLqK8s2Xk9ZuWz5+ZoFEhC7J76es4GU/rEiXwHTmbImoCdyoCOcBH1UF3+Cz6Z2sYD4hyl5TA==", - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "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" - } - }, - "node_modules/esm-env": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", - "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", - "license": "MIT" - }, - "node_modules/esrap": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.3.0.tgz", - "integrity": "sha512-GQ/7RN8uOtEfNpzZzBMTzW9JBcX42oaSVtPzdF+6cEL8pqIL094iUpr9jzYGn4O4P/1S60dJ6izyT8F4LYARng==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "peerDependencies": { - "@typescript-eslint/types": "^8.2.0" - }, - "peerDependenciesMeta": { - "@typescript-eslint/types": { - "optional": true - } - } - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/is-reference": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", - "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.6" - } - }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/locate-character": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", - "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "license": "MIT" - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/mrmime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", - "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.21", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.21.tgz", - "integrity": "sha512-v4sDNP3fdNiWMfabO7OwOQdOX8TiQSztKyT1Wj0w+j7LDallJThJRBBBmzVGyYj0crMh7jlV4zepPkiNu9UwDQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.16", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/rollup": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", - "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.9" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.62.2", - "@rollup/rollup-android-arm64": "4.62.2", - "@rollup/rollup-darwin-arm64": "4.62.2", - "@rollup/rollup-darwin-x64": "4.62.2", - "@rollup/rollup-freebsd-arm64": "4.62.2", - "@rollup/rollup-freebsd-x64": "4.62.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", - "@rollup/rollup-linux-arm-musleabihf": "4.62.2", - "@rollup/rollup-linux-arm64-gnu": "4.62.2", - "@rollup/rollup-linux-arm64-musl": "4.62.2", - "@rollup/rollup-linux-loong64-gnu": "4.62.2", - "@rollup/rollup-linux-loong64-musl": "4.62.2", - "@rollup/rollup-linux-ppc64-gnu": "4.62.2", - "@rollup/rollup-linux-ppc64-musl": "4.62.2", - "@rollup/rollup-linux-riscv64-gnu": "4.62.2", - "@rollup/rollup-linux-riscv64-musl": "4.62.2", - "@rollup/rollup-linux-s390x-gnu": "4.62.2", - "@rollup/rollup-linux-x64-gnu": "4.62.2", - "@rollup/rollup-linux-x64-musl": "4.62.2", - "@rollup/rollup-openbsd-x64": "4.62.2", - "@rollup/rollup-openharmony-arm64": "4.62.2", - "@rollup/rollup-win32-arm64-msvc": "4.62.2", - "@rollup/rollup-win32-ia32-msvc": "4.62.2", - "@rollup/rollup-win32-x64-gnu": "4.62.2", - "@rollup/rollup-win32-x64-msvc": "4.62.2", - "fsevents": "~2.3.2" - } - }, - "node_modules/sade": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", - "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "mri": "^1.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/set-cookie-parser": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.2.tgz", - "integrity": "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==", - "dev": true, - "license": "MIT" - }, - "node_modules/sirv": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", - "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/svelte": { - "version": "5.56.7", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.7.tgz", - "integrity": "sha512-5qERUZX80oQj6XrDMUmD2Uhd/cIpCPDWWKBK3ZHmyRUC9apPyamWM8xMo31mbWsIQxwG2hVoSnOJ/EcnhVkkzQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.4", - "@jridgewell/sourcemap-codec": "^1.5.0", - "@sveltejs/acorn-typescript": "^1.0.10", - "@types/estree": "^1.0.5", - "@types/trusted-types": "^2.0.7", - "acorn": "^8.12.1", - "aria-query": "5.3.1", - "axobject-query": "^4.1.0", - "clsx": "^2.1.1", - "devalue": "^5.8.1", - "esm-env": "^1.2.1", - "esrap": "^2.2.12", - "is-reference": "^3.0.3", - "locate-character": "^3.0.0", - "magic-string": "^0.30.11", - "zimmerframe": "^1.1.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/svelte-check": { - "version": "4.7.3", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.7.3.tgz", - "integrity": "sha512-DHdTCGX62R0fCxBEaT+USdASAnoaRBaaNczkRJl0K7o3WyoCeVUbVxo6fKqpOll/B+WMWCsiFK0eFrJSNBKZIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "@sveltejs/load-config": "^0.2.0", - "chokidar": "^4.0.1", - "fdir": "^6.2.0", - "picocolors": "^1.0.0", - "sade": "^1.7.4" - }, - "bin": { - "svelte-check": "bin/svelte-check" - }, - "engines": { - "node": ">= 18.0.0" - }, - "peerDependencies": { - "svelte": "^4.0.0 || ^5.0.0-next.0", - "typescript": ">=5.0.0" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/typescript": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", - "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", - "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/vite": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", - "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "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 - } - } - }, - "node_modules/vitefu": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", - "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", - "dev": true, - "license": "MIT", - "workspaces": [ - "tests/deps/*", - "tests/projects/*", - "tests/projects/workspace/packages/*" - ], - "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "vite": { - "optional": true - } - } - }, - "node_modules/zimmerframe": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", - "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", - "license": "MIT" - } - } -} diff --git a/frontend/package.json b/frontend/package.json deleted file mode 100644 index 62b0eb1..0000000 --- a/frontend/package.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "bos-settings-frontend", - "version": "0.8.2", - "description": "Frontend for BOS Settings", - "type": "module", - "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": { - "@lucide/svelte": "^1.25.0", - "@tauri-apps/api": "^2", - "@tauri-apps/plugin-dialog": "^2.7.2", - "@tauri-apps/plugin-opener": "^2" - }, - "devDependencies": { - "@sveltejs/adapter-static": "^3.0.6", - "@sveltejs/kit": "^2.9.0", - "@sveltejs/vite-plugin-svelte": "^5.0.0", - "@tauri-apps/cli": "^2", - "@types/node": "^26.4.0", - "svelte": "^5.0.0", - "svelte-check": "^4.0.0", - "typescript": "~5.6.2", - "vite": "^6.0.3" - } -} diff --git a/frontend/src/app.html b/frontend/src/app.html deleted file mode 100644 index d367d13..0000000 --- a/frontend/src/app.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - BOS Settings - %sveltekit.head% - - -
%sveltekit.body%
- - diff --git a/frontend/src/lib/components/ChipPickerField.svelte b/frontend/src/lib/components/ChipPickerField.svelte deleted file mode 100644 index 0e790c0..0000000 --- a/frontend/src/lib/components/ChipPickerField.svelte +++ /dev/null @@ -1,126 +0,0 @@ - - -
-
- {label} -
-
- {#each value as name (name)} - - {name} - - - {/each} - {#if adding} - {#if available.length > 0} - - - {:else} - {emptyOptionsHint} - {/if} - {:else if available.length > 0} - - {/if} -
-
- - diff --git a/frontend/src/lib/components/CsvField.svelte b/frontend/src/lib/components/CsvField.svelte deleted file mode 100644 index b515002..0000000 --- a/frontend/src/lib/components/CsvField.svelte +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - diff --git a/frontend/src/lib/components/Embed.svelte b/frontend/src/lib/components/Embed.svelte deleted file mode 100644 index 40090e5..0000000 --- a/frontend/src/lib/components/Embed.svelte +++ /dev/null @@ -1,10 +0,0 @@ - - -{@render children()} diff --git a/frontend/src/lib/components/EmptyState.svelte b/frontend/src/lib/components/EmptyState.svelte deleted file mode 100644 index aba4339..0000000 --- a/frontend/src/lib/components/EmptyState.svelte +++ /dev/null @@ -1,32 +0,0 @@ - - -
- - {title} - {hint} -
- - diff --git a/frontend/src/lib/components/FileField.svelte b/frontend/src/lib/components/FileField.svelte deleted file mode 100644 index e320be6..0000000 --- a/frontend/src/lib/components/FileField.svelte +++ /dev/null @@ -1,67 +0,0 @@ - - - -
- - -
-
- - diff --git a/frontend/src/lib/components/Group.svelte b/frontend/src/lib/components/Group.svelte deleted file mode 100644 index fd11cb0..0000000 --- a/frontend/src/lib/components/Group.svelte +++ /dev/null @@ -1,62 +0,0 @@ - - -
-

{title}

- {#if hint}

{hint}

{/if} -
- {@render children()} -
-
- - diff --git a/frontend/src/lib/components/Hint.svelte b/frontend/src/lib/components/Hint.svelte deleted file mode 100644 index e86d531..0000000 --- a/frontend/src/lib/components/Hint.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - -

{text}

- - diff --git a/frontend/src/lib/components/Hub.svelte b/frontend/src/lib/components/Hub.svelte deleted file mode 100644 index dd404a0..0000000 --- a/frontend/src/lib/components/Hub.svelte +++ /dev/null @@ -1,51 +0,0 @@ - - - - {#if tabs.length > 1} - ({ id, label }))} bind:value={tab} /> - {/if} - - {#key tab} - {#if Active} - - {/if} - {/key} - - diff --git a/frontend/src/lib/components/HyprColorField.svelte b/frontend/src/lib/components/HyprColorField.svelte deleted file mode 100644 index cc9f430..0000000 --- a/frontend/src/lib/components/HyprColorField.svelte +++ /dev/null @@ -1,56 +0,0 @@ - - - -
- update(e.currentTarget.value, parsed.alpha)} /> - update(parsed.hex, Number(e.currentTarget.value))} - /> -
-
- - diff --git a/frontend/src/lib/components/InfoRow.svelte b/frontend/src/lib/components/InfoRow.svelte deleted file mode 100644 index f714f94..0000000 --- a/frontend/src/lib/components/InfoRow.svelte +++ /dev/null @@ -1,16 +0,0 @@ - - - - {value} - - - diff --git a/frontend/src/lib/components/LogView.svelte b/frontend/src/lib/components/LogView.svelte deleted file mode 100644 index a3206b5..0000000 --- a/frontend/src/lib/components/LogView.svelte +++ /dev/null @@ -1,24 +0,0 @@ - - -{#if lines.length > 0} -
{lines.join("\n")}
-{/if} - - diff --git a/frontend/src/lib/components/NumberField.svelte b/frontend/src/lib/components/NumberField.svelte deleted file mode 100644 index 14a2b31..0000000 --- a/frontend/src/lib/components/NumberField.svelte +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - diff --git a/frontend/src/lib/components/PasswordField.svelte b/frontend/src/lib/components/PasswordField.svelte deleted file mode 100644 index fb47eff..0000000 --- a/frontend/src/lib/components/PasswordField.svelte +++ /dev/null @@ -1,60 +0,0 @@ - - - -
- - -
-
- - diff --git a/frontend/src/lib/components/PathListField.svelte b/frontend/src/lib/components/PathListField.svelte deleted file mode 100644 index ac56678..0000000 --- a/frontend/src/lib/components/PathListField.svelte +++ /dev/null @@ -1,135 +0,0 @@ - - -
-
- {label} - -
- {#if hint}

{hint}

{/if} - {#if value.length === 0} -

None added.

- {:else} -
- {#each value as path (path)} - - {shorten(path)} - - - {/each} -
- {/if} -
- - diff --git a/frontend/src/lib/components/Placeholder.svelte b/frontend/src/lib/components/Placeholder.svelte deleted file mode 100644 index d0d939a..0000000 --- a/frontend/src/lib/components/Placeholder.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - -
-

Unknown page "{page}".

-
- - diff --git a/frontend/src/lib/components/Row.svelte b/frontend/src/lib/components/Row.svelte deleted file mode 100644 index 8247cb1..0000000 --- a/frontend/src/lib/components/Row.svelte +++ /dev/null @@ -1,51 +0,0 @@ - - -
-
- {label} - {#if hint}{hint}{/if} -
-
- {@render children()} -
-
- - diff --git a/frontend/src/lib/components/SaveButton.svelte b/frontend/src/lib/components/SaveButton.svelte deleted file mode 100644 index faadab1..0000000 --- a/frontend/src/lib/components/SaveButton.svelte +++ /dev/null @@ -1,38 +0,0 @@ - - -
- - {status} -
- - diff --git a/frontend/src/lib/components/SelectField.svelte b/frontend/src/lib/components/SelectField.svelte deleted file mode 100644 index feba79e..0000000 --- a/frontend/src/lib/components/SelectField.svelte +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - diff --git a/frontend/src/lib/components/ServiceControl.svelte b/frontend/src/lib/components/ServiceControl.svelte deleted file mode 100644 index 9f3e8c0..0000000 --- a/frontend/src/lib/components/ServiceControl.svelte +++ /dev/null @@ -1,93 +0,0 @@ - - - - - {active ? "Running" : "Stopped"} - - - {enabled ? "Yes" : "No"} - -
- - - -
- {#if hasConfig} - - {/if} -
- - diff --git a/frontend/src/lib/components/Sidebar.svelte b/frontend/src/lib/components/Sidebar.svelte deleted file mode 100644 index d7c67e8..0000000 --- a/frontend/src/lib/components/Sidebar.svelte +++ /dev/null @@ -1,160 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/SliderField.svelte b/frontend/src/lib/components/SliderField.svelte deleted file mode 100644 index e3ee9a9..0000000 --- a/frontend/src/lib/components/SliderField.svelte +++ /dev/null @@ -1,44 +0,0 @@ - - - - onChange?.()} /> - {shown}{suffix} - - - diff --git a/frontend/src/lib/components/Subnav.svelte b/frontend/src/lib/components/Subnav.svelte deleted file mode 100644 index acc782b..0000000 --- a/frontend/src/lib/components/Subnav.svelte +++ /dev/null @@ -1,40 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/Switch.svelte b/frontend/src/lib/components/Switch.svelte deleted file mode 100644 index c62300d..0000000 --- a/frontend/src/lib/components/Switch.svelte +++ /dev/null @@ -1,50 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/SwitchField.svelte b/frontend/src/lib/components/SwitchField.svelte deleted file mode 100644 index a8e2b19..0000000 --- a/frontend/src/lib/components/SwitchField.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - - - diff --git a/frontend/src/lib/components/TagsField.svelte b/frontend/src/lib/components/TagsField.svelte deleted file mode 100644 index ee7793d..0000000 --- a/frontend/src/lib/components/TagsField.svelte +++ /dev/null @@ -1,104 +0,0 @@ - - -
-
- {label} -
-
- {#each value as name (name)} - - {name} - - - {/each} - -
-
- - diff --git a/frontend/src/lib/components/TextField.svelte b/frontend/src/lib/components/TextField.svelte deleted file mode 100644 index 1711e56..0000000 --- a/frontend/src/lib/components/TextField.svelte +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - diff --git a/frontend/src/lib/components/Titlebar.svelte b/frontend/src/lib/components/Titlebar.svelte deleted file mode 100644 index 78fe93a..0000000 --- a/frontend/src/lib/components/Titlebar.svelte +++ /dev/null @@ -1,75 +0,0 @@ - - -
-
- - - -
-
Settings
-
- Ctrl K -
- - diff --git a/frontend/src/lib/components/ViewScaffold.svelte b/frontend/src/lib/components/ViewScaffold.svelte deleted file mode 100644 index 1eef417..0000000 --- a/frontend/src/lib/components/ViewScaffold.svelte +++ /dev/null @@ -1,64 +0,0 @@ - - -{#if embedded} -
- {@render children()} -
-{:else} -
-

{title}

- {#if lede} -

{lede}

- {/if} -
- {@render children()} -
-
-{/if} - - diff --git a/frontend/src/lib/debounce.ts b/frontend/src/lib/debounce.ts deleted file mode 100644 index 463b59b..0000000 --- a/frontend/src/lib/debounce.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function debounce(fn: () => void, ms = 280): () => void { - let t: ReturnType | undefined; - return () => { - clearTimeout(t); - t = setTimeout(fn, ms); - }; -} diff --git a/frontend/src/lib/embed.ts b/frontend/src/lib/embed.ts deleted file mode 100644 index 86ff3ab..0000000 --- a/frontend/src/lib/embed.ts +++ /dev/null @@ -1,2 +0,0 @@ -/** When set, ViewScaffold renders inner groups only - used by hub tab panes. */ -export const EMBEDDED_KEY = "bos-settings-embedded"; diff --git a/frontend/src/lib/nav.svelte.ts b/frontend/src/lib/nav.svelte.ts deleted file mode 100644 index b7382c9..0000000 --- a/frontend/src/lib/nav.svelte.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { DEFAULT_PAGE, resolvePage } from "$lib/sidebar"; - -/** Shared navigation - module `$state` so sidebar/search/hubs all see the same page. */ -export const nav = $state({ - page: DEFAULT_PAGE, - tab: null as string | null, - searchNonce: 0, -}); - -export function go(page: string, tab?: string) { - const resolved = resolvePage(page); - nav.tab = tab ?? resolved.tab ?? ""; - nav.page = resolved.page; -} - -export type Navigate = (page: string, tab?: string) => void; -export const NAVIGATE_KEY = "bos-settings-navigate"; diff --git a/frontend/src/lib/search.ts b/frontend/src/lib/search.ts deleted file mode 100644 index 2b5f4e6..0000000 --- a/frontend/src/lib/search.ts +++ /dev/null @@ -1,120 +0,0 @@ -export interface SearchHit { - page: string; - tab?: string; - label: string; - keywords: string; -} - -export const SEARCH_INDEX: SearchHit[] = [ - { page: "home", label: "Home", keywords: "search overview status" }, - { - page: "network", - label: "Wi-Fi", - keywords: "wifi ssid password scan radio ethernet hotspot dns", - }, - { - page: "network", - tab: "vpn", - label: "VPN / WireGuard", - keywords: "vpn wireguard tailscale tunnel import conf", - }, - { - page: "network", - tab: "breadcrumbs", - label: "Saved networks", - keywords: "wifi profiles breadcrumbs known ssid tailscale", - }, - { page: "bluetooth", label: "Bluetooth", keywords: "bluetooth headphones pair scan mouse keyboard" }, - { - page: "displays", - label: "Displays", - keywords: "monitor scale resolution arrange rotate vrr hdr hyprland", - }, - { - page: "displays", - tab: "nightlight", - label: "Night light", - keywords: "night light hyprsunset temperature warmth sunset", - }, - { - page: "displays", - tab: "breadmon", - label: "Live arrange", - keywords: "breadmon mirror profile arrange", - }, - { - page: "appearance", - label: "Wallpaper", - keywords: "wallpaper theme palette pywal breadpaper accent", - }, - { - page: "appearance", - tab: "appearance", - label: "Windows", - keywords: "gaps blur rounding border shadow tiling dwindle", - }, - { page: "sound", label: "Sound", keywords: "volume mute microphone output input speaker pavucontrol" }, - { - page: "power", - label: "Power & battery", - keywords: "battery brightness charge tlp suspend idle lid", - }, - { - page: "input", - label: "Keyboard & mouse", - keywords: "keybind shortcut layout touchpad tap natural scroll follow mouse", - }, - { page: "input", tab: "ime", label: "Input method", keywords: "fcitx5 ime cjk chinese japanese korean" }, - { - page: "input", - tab: "accessibility", - label: "Accessibility", - keywords: "orca screen reader zoom magnifier sticky keys", - }, - { - page: "desktop", - label: "Bar", - keywords: "breadbar modules clock tray workspaces", - }, - { page: "desktop", tab: "breadbox", label: "Launcher", keywords: "breadbox launcher apps" }, - { page: "desktop", tab: "breadlock", label: "Lock screen", keywords: "lock greet breadlock idle" }, - { page: "desktop", tab: "breadshot", label: "Screenshots", keywords: "screenshot grim slurp breadshot" }, - { page: "desktop", tab: "autostart", label: "Startup apps", keywords: "autostart login startup" }, - { page: "desktop", tab: "breadclip", label: "Clipboard", keywords: "clipboard history breadclip" }, - { page: "desktop", tab: "breadpad", label: "Notes", keywords: "breadpad notes calendar caldav" }, - { page: "desktop", tab: "breadsearch", label: "File search", keywords: "breadsearch breadmill index" }, - { page: "desktop", tab: "bread", label: "Daemon", keywords: "breadd daemon adapters lua" }, - { page: "apps", label: "Default apps", keywords: "browser terminal pdf mime default" }, - { page: "apps", tab: "optional", label: "Optional software", keywords: "steam flatpak libreoffice" }, - { page: "apps", tab: "printing", label: "Printing", keywords: "cups printer" }, - { page: "privacy", label: "Firewall", keywords: "firewall ufw port allow" }, - { page: "privacy", tab: "users", label: "Users", keywords: "users password account" }, - { page: "system", label: "Updates", keywords: "update bakery pacman firmware nvidia" }, - { page: "system", tab: "packages", label: "Packages", keywords: "packages bakery install" }, - { page: "system", tab: "aur", label: "AUR", keywords: "aur yay" }, - { page: "system", tab: "snapshots", label: "Snapshots", keywords: "snapper snapshot rollback grub" }, - { page: "system", tab: "backup", label: "Backup", keywords: "restic backup restore" }, - { page: "system", tab: "channel", label: "Bakery channel", keywords: "track stable beta dev bakery" }, - { page: "about", label: "About", keywords: "hostname kernel cpu gpu about machine" }, - { page: "about", tab: "datetime", label: "Date & time", keywords: "timezone ntp clock date time" }, - { page: "desktop", tab: "breadhelp", label: "Help", keywords: "help breadhelp onboarding" }, -]; - -export function searchSettings(query: string, limit = 8): SearchHit[] { - const q = query.trim().toLowerCase(); - if (q.length < 1) return []; - const scored = SEARCH_INDEX.map((hit) => { - const hay = `${hit.label} ${hit.keywords} ${hit.page}`.toLowerCase(); - let score = 0; - if (hit.label.toLowerCase().startsWith(q)) score += 8; - if (hit.label.toLowerCase().includes(q)) score += 4; - if (hay.includes(q)) score += 2; - for (const part of q.split(/\s+/)) { - if (hay.includes(part)) score += 1; - } - return { hit, score }; - }) - .filter((x) => x.score > 0) - .sort((a, b) => b.score - a.score); - return scored.slice(0, limit).map((x) => x.hit); -} diff --git a/frontend/src/lib/sidebar.ts b/frontend/src/lib/sidebar.ts deleted file mode 100644 index f7d9d50..0000000 --- a/frontend/src/lib/sidebar.ts +++ /dev/null @@ -1,112 +0,0 @@ -import type { Component } from "svelte"; -import House from "@lucide/svelte/icons/house"; -import Wifi from "@lucide/svelte/icons/wifi"; -import Bluetooth from "@lucide/svelte/icons/bluetooth"; -import Monitor from "@lucide/svelte/icons/monitor"; -import Volume2 from "@lucide/svelte/icons/volume-2"; -import BatteryFull from "@lucide/svelte/icons/battery-full"; -import Palette from "@lucide/svelte/icons/palette"; -import AppWindow from "@lucide/svelte/icons/app-window"; -import Keyboard from "@lucide/svelte/icons/keyboard"; -import LayoutGrid from "@lucide/svelte/icons/layout-grid"; -import Shield from "@lucide/svelte/icons/shield"; -import Download from "@lucide/svelte/icons/download"; -import Info from "@lucide/svelte/icons/info"; - -export interface SidebarItem { - id: string; - label: string; - icon: Component; -} - -export interface SidebarSection { - title: string | null; - items: SidebarItem[]; -} - -export const SIDEBAR_SECTIONS: SidebarSection[] = [ - { title: "Overview", items: [{ id: "home", label: "Home", icon: House }] }, - { - title: "Devices", - items: [ - { id: "network", label: "Wi-Fi & internet", icon: Wifi }, - { id: "bluetooth", label: "Bluetooth", icon: Bluetooth }, - { id: "displays", label: "Displays", icon: Monitor }, - { id: "sound", label: "Sound", icon: Volume2 }, - { id: "power", label: "Power & battery", icon: BatteryFull }, - ], - }, - { - title: "Desktop", - items: [ - { id: "appearance", label: "Appearance", icon: Palette }, - { id: "desktop", label: "Bar & apps", icon: AppWindow }, - { id: "input", label: "Keyboard & mouse", icon: Keyboard }, - { id: "apps", label: "Default apps", icon: LayoutGrid }, - ], - }, - { - title: "System", - items: [ - { id: "privacy", label: "Privacy & users", icon: Shield }, - { id: "system", label: "Updates & backup", icon: Download }, - { id: "about", label: "About", icon: Info }, - ], - }, -]; - -/** Old sidebar ids (screenshot CLI, in-app jumps) → hub page + optional tab. */ -export const PAGE_ALIASES: Record = { - home: { page: "home" }, - network: { page: "network" }, - vpn: { page: "network", tab: "vpn" }, - breadcrumbs: { page: "network", tab: "breadcrumbs" }, - bluetooth: { page: "bluetooth" }, - displays: { page: "displays" }, - hyprland: { page: "displays", tab: "hyprland" }, - nightlight: { page: "displays", tab: "nightlight" }, - breadmon: { page: "displays", tab: "breadmon" }, - sound: { page: "sound" }, - power: { page: "power" }, - appearance: { page: "appearance" }, - breadpaper: { page: "appearance", tab: "breadpaper" }, - desktop: { page: "desktop" }, - breadbar: { page: "desktop", tab: "breadbar" }, - breadbox: { page: "desktop", tab: "breadbox" }, - breadlock: { page: "desktop", tab: "breadlock" }, - breadshot: { page: "desktop", tab: "breadshot" }, - autostart: { page: "desktop", tab: "autostart" }, - breadclip: { page: "desktop", tab: "more" }, - breadpad: { page: "desktop", tab: "more" }, - breadsearch: { page: "desktop", tab: "more" }, - bread: { page: "desktop", tab: "more" }, - breadhelp: { page: "desktop", tab: "more" }, - more: { page: "desktop", tab: "more" }, - input: { page: "input" }, - keybinds: { page: "input", tab: "keybinds" }, - ime: { page: "input", tab: "ime" }, - accessibility: { page: "input", tab: "accessibility" }, - apps: { page: "apps" }, - defaults: { page: "apps" }, - optional: { page: "apps", tab: "optional" }, - printing: { page: "apps", tab: "printing" }, - privacy: { page: "privacy" }, - firewall: { page: "privacy" }, - users: { page: "privacy", tab: "users" }, - system: { page: "system" }, - updates: { page: "system" }, - packages: { page: "system", tab: "packages" }, - aur: { page: "system", tab: "aur" }, - firmware: { page: "system", tab: "firmware" }, - snapshots: { page: "system", tab: "snapshots" }, - backup: { page: "system", tab: "backup" }, - channel: { page: "system", tab: "channel" }, - about: { page: "about" }, - datetime: { page: "about", tab: "datetime" }, -}; - -export const DEFAULT_PAGE = "home"; - -export function resolvePage(id: string): { page: string; tab?: string } { - return PAGE_ALIASES[id] ?? { page: id }; -} diff --git a/frontend/src/lib/streaming.ts b/frontend/src/lib/streaming.ts deleted file mode 100644 index bf78f05..0000000 --- a/frontend/src/lib/streaming.ts +++ /dev/null @@ -1,25 +0,0 @@ -// Frontend half of the event-streaming command pattern (see -// src/src/commands/streaming.rs) - listens for `cmd-output` lines from a -// typed Tauri command that runs a hardcoded program, then resolves once -// the process exits. - -import { invoke } from "@tauri-apps/api/core"; -import { listen } from "@tauri-apps/api/event"; - -export async function runStreamed( - command: string, - args: Record, - onLine: (line: string) => void, -): Promise { - const sessionId = crypto.randomUUID(); - - const unlisten = await listen<{ session_id: string; line: string }>("cmd-output", (event) => { - if (event.payload.session_id === sessionId) onLine(event.payload.line); - }); - - try { - return await invoke(command, { sessionId, ...args }); - } finally { - unlisten(); - } -} diff --git a/frontend/src/lib/styles/app.css b/frontend/src/lib/styles/app.css deleted file mode 100644 index a626e8e..0000000 --- a/frontend/src/lib/styles/app.css +++ /dev/null @@ -1,121 +0,0 @@ -:root { - --bg-2: color-mix(in oklab, var(--bg) 82%, var(--surface)); - --surface-2: color-mix(in oklab, var(--surface) 72%, var(--overlay)); - --muted: color-mix(in oklab, var(--fg) 58%, transparent); - --faint: color-mix(in oklab, var(--fg) 38%, transparent); - --line: color-mix(in srgb, var(--fg) 8%, transparent); - --accent-soft: color-mix(in oklab, var(--accent) 18%, transparent); - --radius: 14px; - --radius-sm: 10px; -} - -* { - box-sizing: border-box; -} - -html, -body { - margin: 0; - border: none; - outline: none; - height: 100%; - color-scheme: dark; - background-color: var(--bg, #12161c); - color: var(--fg); - font-family: var(--font-family, sans-serif); - font-size: var(--font-size-base, 14px); -} - -#svelte { - height: 100%; -} - -button, -input, -select, -textarea { - font: inherit; - color: inherit; -} - -:focus { - outline: none; -} - -:focus-visible { - outline: 2px solid color-mix(in oklab, var(--accent) 70%, white); - outline-offset: 2px; -} - -input[type="range"] { - accent-color: var(--accent); -} - -.btn { - background: var(--surface-2, var(--surface)); - border: 1px solid var(--line); - padding: 8px 14px; - border-radius: var(--radius-sm, 10px); - font-size: 13px; - cursor: pointer; - color: var(--fg); -} - -.btn:hover { - background: color-mix(in oklab, var(--surface-2, var(--surface)) 80%, var(--fg)); -} - -.btn.primary { - background: var(--accent); - color: var(--on-accent); - border-color: transparent; - font-weight: 600; -} - -.btn.primary:hover { - filter: brightness(1.06); -} - -.btn.danger { - color: var(--red); -} - -.btn:disabled { - opacity: 0.5; - cursor: default; -} - -.kbd { - font-size: 11px; - letter-spacing: 0.04em; - background: color-mix(in srgb, var(--fg) 6%, transparent); - border: 1px solid var(--line); - padding: 2px 7px; - border-radius: 6px; - color: var(--muted); -} - -.pills { - display: flex; - flex-wrap: wrap; - gap: 6px; -} - -.pill { - display: inline-flex; - align-items: center; - padding: 6px 11px; - border-radius: 999px; - font-size: 12px; - background: color-mix(in srgb, var(--fg) 6%, transparent); - border: 1px solid var(--line); - color: var(--fg); - cursor: pointer; -} - -.pill.on { - background: var(--accent); - color: var(--on-accent); - border-color: transparent; - font-weight: 600; -} diff --git a/frontend/src/lib/theme/index.ts b/frontend/src/lib/theme/index.ts deleted file mode 100644 index 87e4b17..0000000 --- a/frontend/src/lib/theme/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Bridges bread-theme's pywal-derived palette into the webview. Mirrors -// bread_theme::gtk::apply_shared()'s two-phase pattern: fetch once at -// startup, then keep it live via a backend-pushed event - see -// src-tauri/src/commands/theme.rs for the file-watch side of this. - -import { invoke } from "@tauri-apps/api/core"; -import { listen } from "@tauri-apps/api/event"; - -const STYLE_ELEMENT_ID = "bread-theme"; - -function applyThemeCss(css: string) { - let style = document.getElementById(STYLE_ELEMENT_ID); - if (!style) { - style = document.createElement("style"); - style.id = STYLE_ELEMENT_ID; - document.head.appendChild(style); - } - style.textContent = css; -} - -/** Call once at startup (e.g. from a root `$effect`/`onMount`). */ -export async function initTheme(): Promise { - const css = await invoke("get_theme_css"); - applyThemeCss(css); - - await listen("theme-changed", (event) => { - applyThemeCss(event.payload); - }); -} diff --git a/frontend/src/lib/views/About.svelte b/frontend/src/lib/views/About.svelte deleted file mode 100644 index 43c0e56..0000000 --- a/frontend/src/lib/views/About.svelte +++ /dev/null @@ -1,132 +0,0 @@ - - - - {#if info} - - - - - - - - - - - -
- - -
- {#if status} - {status} - {/if} -
- {/if} -
- - diff --git a/frontend/src/lib/views/AboutHub.svelte b/frontend/src/lib/views/AboutHub.svelte deleted file mode 100644 index b4f7a2e..0000000 --- a/frontend/src/lib/views/AboutHub.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/frontend/src/lib/views/Accessibility.svelte b/frontend/src/lib/views/Accessibility.svelte deleted file mode 100644 index ed4094b..0000000 --- a/frontend/src/lib/views/Accessibility.svelte +++ /dev/null @@ -1,197 +0,0 @@ - - - - - {#if !st} - - {:else if !st.orca_installed} - - - {:else} -
- Orca - -
- {/if} -
- - - {#if st} - - - - {#if !st.kmag_installed} - - - {:else} - - {/if} - {/if} - - - - {#if st} -
- Sticky keys - -
-
- Slow keys - -
- - {/if} -
- {#if message}{/if} - -
- - diff --git a/frontend/src/lib/views/Appearance.svelte b/frontend/src/lib/views/Appearance.svelte deleted file mode 100644 index 2ef5fe2..0000000 --- a/frontend/src/lib/views/Appearance.svelte +++ /dev/null @@ -1,162 +0,0 @@ - - - - {#if cfg} - - - - - - - - - - - - - - {#if cfg.blur_enabled} - - - {/if} - - {#if cfg.shadow_enabled} - - {/if} - - - - - m.value)} - labels={Object.fromEntries(FOLLOW_MOUSE_MODES.map((m) => [m.value, m.label]))} - /> - - - - - {/if} - diff --git a/frontend/src/lib/views/AppearanceHub.svelte b/frontend/src/lib/views/AppearanceHub.svelte deleted file mode 100644 index 692f3ea..0000000 --- a/frontend/src/lib/views/AppearanceHub.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/frontend/src/lib/views/AppsHub.svelte b/frontend/src/lib/views/AppsHub.svelte deleted file mode 100644 index 5b5c900..0000000 --- a/frontend/src/lib/views/AppsHub.svelte +++ /dev/null @@ -1,16 +0,0 @@ - - - diff --git a/frontend/src/lib/views/Aur.svelte b/frontend/src/lib/views/Aur.svelte deleted file mode 100644 index ecf2ae0..0000000 --- a/frontend/src/lib/views/Aur.svelte +++ /dev/null @@ -1,151 +0,0 @@ - - - - -
- e.key === "Enter" && search()} /> - -
- - - -
- {#if results && results.length === 0} - - {:else if results} - {#each results as r (r.name)} -
-
- {r.name} - {r.version} - -
- {r.description} -
- {/each} - {/if} -
-
-
- - diff --git a/frontend/src/lib/views/Autostart.svelte b/frontend/src/lib/views/Autostart.svelte deleted file mode 100644 index 914bf12..0000000 --- a/frontend/src/lib/views/Autostart.svelte +++ /dev/null @@ -1,168 +0,0 @@ - - - - {#if entries} - - {#each entries as entry, i (i)} -
- -
- - -
- - -
- {/each} - - - -
- {/if} -
- - diff --git a/frontend/src/lib/views/Backup.svelte b/frontend/src/lib/views/Backup.svelte deleted file mode 100644 index 32cc11b..0000000 --- a/frontend/src/lib/views/Backup.svelte +++ /dev/null @@ -1,254 +0,0 @@ - - - - - {#if !st} - - {:else if !st.restic_installed} - - - {:else} - - - - - {/if} - - - -
- - - -
- {#if message}{/if} -
- - - -
- - -
-
- - - {#if snapshots.length === 0} - - {:else} -
- {#each snapshots as s (s.id)} - - {/each} -
- {/if} -
- -
- - diff --git a/frontend/src/lib/views/Bluetooth.svelte b/frontend/src/lib/views/Bluetooth.svelte deleted file mode 100644 index 73bad27..0000000 --- a/frontend/src/lib/views/Bluetooth.svelte +++ /dev/null @@ -1,195 +0,0 @@ - - - - {#if powered === null} - - {:else} - - powered ?? false, (v) => togglePower(v)} /> - - - -
- {#if paired === null} - - {:else if paired.length === 0} - - {:else} - {#each paired as dev (dev.address)} -
- {dev.name}{dev.connected ? " (connected)" : ""} - - -
- {/each} - {/if} -
-
- - -
- {#if scanResults === null} - - {:else if scanResults.length === 0} - - {:else} - {#each scanResults as dev (dev.address)} -
- {dev.name} - -
- {/each} - {/if} -
- -
- - - {/if} -
- - diff --git a/frontend/src/lib/views/Bread.svelte b/frontend/src/lib/views/Bread.svelte deleted file mode 100644 index a0de5cf..0000000 --- a/frontend/src/lib/views/Bread.svelte +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - {#if cfg} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {/if} - diff --git a/frontend/src/lib/views/Breadbar.svelte b/frontend/src/lib/views/Breadbar.svelte deleted file mode 100644 index 7748928..0000000 --- a/frontend/src/lib/views/Breadbar.svelte +++ /dev/null @@ -1,176 +0,0 @@ - - - - {#if style} - - - - - - - - - - - - - - - - - - - - - - - {/if} - - - - - {#if advancedOpen} - -
- - {cssStatus} -
- {/if} -
-
- - diff --git a/frontend/src/lib/views/Breadbox.svelte b/frontend/src/lib/views/Breadbox.svelte deleted file mode 100644 index 1cbc9a0..0000000 --- a/frontend/src/lib/views/Breadbox.svelte +++ /dev/null @@ -1,128 +0,0 @@ - - - - {#if contexts} - - {#if contexts.length === 0} -
No launcher contexts yet. Add one to control which apps/categories breadbox surfaces first.
- {/if} - {#each contexts as ctx, i (i)} -
- - setPriority(ctx, e.currentTarget.value)} - placeholder="firefox, code, Development, ..." - class="priority" - /> - -
- {/each} - -
- - - {/if} - - -
- - diff --git a/frontend/src/lib/views/Breadclip.svelte b/frontend/src/lib/views/Breadclip.svelte deleted file mode 100644 index 697ec75..0000000 --- a/frontend/src/lib/views/Breadclip.svelte +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - diff --git a/frontend/src/lib/views/Breadcrumbs.svelte b/frontend/src/lib/views/Breadcrumbs.svelte deleted file mode 100644 index 95c7c98..0000000 --- a/frontend/src/lib/views/Breadcrumbs.svelte +++ /dev/null @@ -1,303 +0,0 @@ - - - - - - {#if cfg} - - {#if profileNames.length > 0} - - {:else} - - {/if} - - - - - - - - - -
- {#each cfg.networks as net, i (i)} -
- - -
- - -
- -
- {/each} -
- -
- - - {#each cfg.profiles as profile, i (i)} -
-
- - -
- - - - - - -
- - -
-
- - -
-
- {/each} - -
- - - {/if} -
- - diff --git a/frontend/src/lib/views/Breadhelp.svelte b/frontend/src/lib/views/Breadhelp.svelte deleted file mode 100644 index 80b20d2..0000000 --- a/frontend/src/lib/views/Breadhelp.svelte +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - autostartOn, (v) => setAutostart(v)} /> - {#if !helpEntry} - - {/if} - {#if status} - - {/if} - - - - diff --git a/frontend/src/lib/views/Breadlock.svelte b/frontend/src/lib/views/Breadlock.svelte deleted file mode 100644 index edaf0da..0000000 --- a/frontend/src/lib/views/Breadlock.svelte +++ /dev/null @@ -1,89 +0,0 @@ - - - - - - - - - {#if cfg} - - - {#if cfg.background_mode === "image"} - - {/if} - - - - - - - - {/if} - - - - {#if examplePath} - - {:else} - - {/if} - - - - diff --git a/frontend/src/lib/views/Breadmon.svelte b/frontend/src/lib/views/Breadmon.svelte deleted file mode 100644 index d75c789..0000000 --- a/frontend/src/lib/views/Breadmon.svelte +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - diff --git a/frontend/src/lib/views/Breadpad.svelte b/frontend/src/lib/views/Breadpad.svelte deleted file mode 100644 index 6af69f0..0000000 --- a/frontend/src/lib/views/Breadpad.svelte +++ /dev/null @@ -1,138 +0,0 @@ - - - - {#if cfg} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {/if} - - - diff --git a/frontend/src/lib/views/Breadpaper.svelte b/frontend/src/lib/views/Breadpaper.svelte deleted file mode 100644 index d94cd39..0000000 --- a/frontend/src/lib/views/Breadpaper.svelte +++ /dev/null @@ -1,164 +0,0 @@ - - - - - {#if currentPath} -
- {currentPath.split("/").pop()} -
- {:else} -
No wallpaper
- {/if} -
- - {status} -
-
- - - {#if scanning && library === null} -
Loading {libraryDir}…
- {:else if library && library.length === 0} -
Nothing in {libraryDir}
- {:else if library} -
- {#each library as item (item.path)} - - {/each} -
- {/if} -
-
- - diff --git a/frontend/src/lib/views/Breadsearch.svelte b/frontend/src/lib/views/Breadsearch.svelte deleted file mode 100644 index 1ac6880..0000000 --- a/frontend/src/lib/views/Breadsearch.svelte +++ /dev/null @@ -1,105 +0,0 @@ - - - - - - {#if cfg} - - - - - - - - - - - - - - - - - - - - - - - - - {/if} - - - diff --git a/frontend/src/lib/views/Breadshot.svelte b/frontend/src/lib/views/Breadshot.svelte deleted file mode 100644 index 7354cf4..0000000 --- a/frontend/src/lib/views/Breadshot.svelte +++ /dev/null @@ -1,78 +0,0 @@ - - - - - {#if binds && binds.length > 0} - {#each binds as b (`${b.shortcut}:${b.command}`)} - - {/each} - {:else} - - {/if} - - - - {#if cfg} - - - - - - - - - - {/if} - - - diff --git a/frontend/src/lib/views/Channel.svelte b/frontend/src/lib/views/Channel.svelte deleted file mode 100644 index e3b3904..0000000 --- a/frontend/src/lib/views/Channel.svelte +++ /dev/null @@ -1,127 +0,0 @@ - - - - - {#if !track} - - {:else} -
- {#each track.tracks as name (name)} - - {/each} -
- - {/if} - - {#if message}{/if} -
- -
- - diff --git a/frontend/src/lib/views/DateTime.svelte b/frontend/src/lib/views/DateTime.svelte deleted file mode 100644 index fa92944..0000000 --- a/frontend/src/lib/views/DateTime.svelte +++ /dev/null @@ -1,121 +0,0 @@ - - - - {#if info} - - - - - - {#if info.timezones.length === 0} - - {:else} - - - - {#if tzStatus} - {tzStatus} - {/if} - {/if} - - - - - - - - {/if} - - - diff --git a/frontend/src/lib/views/Defaults.svelte b/frontend/src/lib/views/Defaults.svelte deleted file mode 100644 index 944ad8f..0000000 --- a/frontend/src/lib/views/Defaults.svelte +++ /dev/null @@ -1,75 +0,0 @@ - - - - - {#if st} - {#each CATEGORIES as cat (cat.id)} - - - - {/each} - - {:else} - - {/if} - - - - diff --git a/frontend/src/lib/views/DesktopHub.svelte b/frontend/src/lib/views/DesktopHub.svelte deleted file mode 100644 index a2f7aa0..0000000 --- a/frontend/src/lib/views/DesktopHub.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - - diff --git a/frontend/src/lib/views/DesktopMore.svelte b/frontend/src/lib/views/DesktopMore.svelte deleted file mode 100644 index 3c34441..0000000 --- a/frontend/src/lib/views/DesktopMore.svelte +++ /dev/null @@ -1,29 +0,0 @@ - - - ({ id, label }))} bind:value={tab} /> - - {#key tab} - {#if Active} - - {/if} - {/key} - diff --git a/frontend/src/lib/views/Display.svelte b/frontend/src/lib/views/Display.svelte deleted file mode 100644 index e121a6f..0000000 --- a/frontend/src/lib/views/Display.svelte +++ /dev/null @@ -1,370 +0,0 @@ - - - - - {#if monitors.length === 0} - - {:else} -
- {#each monitors as m, i (m.name)} - - {/each} -
- - {/if} -
- - {#if live} - - - {#if live.available_modes.length > 0} - - {:else} - {live.mode} - {/if} - - -
- {#each SCALES as s (s.value)} - - {/each} -
-
- -
- {#each TURNS as t (t.value)} - - {/each} -
-
- - {live.x}, {live.y} - - {#if message} - - {/if} -
- {/if} -
- - diff --git a/frontend/src/lib/views/DisplaysHub.svelte b/frontend/src/lib/views/DisplaysHub.svelte deleted file mode 100644 index 3e86d77..0000000 --- a/frontend/src/lib/views/DisplaysHub.svelte +++ /dev/null @@ -1,16 +0,0 @@ - - - diff --git a/frontend/src/lib/views/Firewall.svelte b/frontend/src/lib/views/Firewall.svelte deleted file mode 100644 index aba18bf..0000000 --- a/frontend/src/lib/views/Firewall.svelte +++ /dev/null @@ -1,219 +0,0 @@ - - - - - - - - - - -
- - -
-
- - -
- {#if status === "unloaded"} - - {:else if "error" in status} - - {:else if status.rules.length === 0} - - {:else} - {#each status.rules as rule (rule.number)} -
- {rule.text} - -
- {/each} - {/if} -
- - -
- - -
- - diff --git a/frontend/src/lib/views/Firmware.svelte b/frontend/src/lib/views/Firmware.svelte deleted file mode 100644 index db79195..0000000 --- a/frontend/src/lib/views/Firmware.svelte +++ /dev/null @@ -1,140 +0,0 @@ - - - - -
- {#if devices === null} - - {:else if devices.length === 0} - - {:else} - {#each devices as dev (dev.name)} -
- {dev.name} - {dev.version} -
- {/each} - {/if} -
- -
- - - -
-
- - -
- - diff --git a/frontend/src/lib/views/Home.svelte b/frontend/src/lib/views/Home.svelte deleted file mode 100644 index 77268c3..0000000 --- a/frontend/src/lib/views/Home.svelte +++ /dev/null @@ -1,315 +0,0 @@ - - -
-

Settings

-

Wi-Fi, lock screen, updates, keybinds.

- -
- - -
- - {#if hits.length > 0} -
- {#each hits as hit (`${hit.page}:${hit.tab ?? ""}:${hit.label}`)} - - {/each} -
- {:else} -
- - - -
- -
- {#each tiles as tile (tile.id + (tile.tab ?? ""))} - - {/each} -
- {/if} -
- - diff --git a/frontend/src/lib/views/InputHub.svelte b/frontend/src/lib/views/InputHub.svelte deleted file mode 100644 index e9a7664..0000000 --- a/frontend/src/lib/views/InputHub.svelte +++ /dev/null @@ -1,16 +0,0 @@ - - - diff --git a/frontend/src/lib/views/InputMethod.svelte b/frontend/src/lib/views/InputMethod.svelte deleted file mode 100644 index c485eb5..0000000 --- a/frontend/src/lib/views/InputMethod.svelte +++ /dev/null @@ -1,166 +0,0 @@ - - - - - {#if !st} - - {:else} -
- Enable fcitx5 for this session - -
- - - {/if} - {#if message}{/if} -
- - - {#if st} -
    - {#each st.packages as p (p.name)} -
  • {p.name}{p.installed ? "" : " (missing)"}
  • - {/each} -
- {/if} - {#if missing.length} - - {/if} -
- -
- - diff --git a/frontend/src/lib/views/Keybinds.svelte b/frontend/src/lib/views/Keybinds.svelte deleted file mode 100644 index 07b7b70..0000000 --- a/frontend/src/lib/views/Keybinds.svelte +++ /dev/null @@ -1,786 +0,0 @@ - - -{#snippet extraFields(row: EditRow)} - {#if row.action === "exec"} - setExtra(row, "command", e.currentTarget.value)} - /> - - - {:else if row.action === "focus" || row.action === "move"} - setExtra(row, "workspace", parseWorkspaceValue(e.currentTarget.value))} - /> - {:else if row.action === "move_dir"} - - {:else if row.action === "resize_dir"} - setExtra(row, "x", e.currentTarget.value === "" ? undefined : Number(e.currentTarget.value))} - /> - setExtra(row, "y", e.currentTarget.value === "" ? undefined : Number(e.currentTarget.value))} - /> - - {:else if row.action === "layout"} - setExtra(row, "layout", e.currentTarget.value)} - /> - {:else if row.action === "drag"} - Mouse-drag bind. - {:else if row.action === "close" || row.action === "fullscreen" || row.action === "float" || row.action === "pseudo" || row.action === "resize" || row.action === "focus_last" || row.action === "exit"} - No extra options for this action. - {:else} - onExtraInput(row, e.currentTarget.value)} - /> - {/if} - - {#if KNOWN_ACTIONS.includes(row.action as (typeof KNOWN_ACTIONS)[number]) && row.action !== "drag"} - - {/if} -{/snippet} - -{#snippet section(rows: EditRow[], onAdd: () => void, onRemove: (i: number) => void)} -
- {#each rows as row, i (i)} -
-
- {bindTitle(row)} - - -
-
- {@render extraFields(row)} -
- {#if row.advancedOpen && KNOWN_ACTIONS.includes(row.action as (typeof KNOWN_ACTIONS)[number]) && row.action !== "drag"} - onExtraInput(row, e.currentTarget.value)} - /> - {/if} -
- {/each} -
- -{/snippet} - - - {#if loadError} - - - - {/if} - {#if loaded} - {#if kind === "unknown"} - - - - {:else if kind === "flat"} - - - - - - {@render section( - bindingsRows, - () => bindingsRows.push(emptyRow()), - (i) => bindingsRows.splice(i, 1) - )} - - - - {:else} - - {#if layoutOrder.length > 0} - - {/if} - - - - - {@render section( - globalsRows, - () => globalsRows.push(emptyRow()), - (i) => globalsRows.splice(i, 1) - )} - - - - {@render section( - commonRows, - () => commonRows.push(emptyRow()), - (i) => commonRows.splice(i, 1) - )} - - - {#each layoutOrder as name (name)} - - - {@render section( - layoutRows[name] ?? [], - () => (layoutRows[name] ?? (layoutRows[name] = [])).push(emptyRow()), - (i) => layoutRows[name]?.splice(i, 1) - )} - - {/each} - - -
- - -
-
- - - {/if} - {/if} -
- - diff --git a/frontend/src/lib/views/Network.svelte b/frontend/src/lib/views/Network.svelte deleted file mode 100644 index b8e2f1b..0000000 --- a/frontend/src/lib/views/Network.svelte +++ /dev/null @@ -1,274 +0,0 @@ - - - - - radioEnabled, (v) => toggleRadio(v)} /> - {#if ethernetLabel} - - {/if} - - - {#if activeNet} - -
-
- {#each [1, 2, 3, 4] as n (n)} - = n}> - {/each} -
-
- {activeNet.ssid} - {activeNet.secured ? "Secured" : "Open"} · {activeNet.signal}% -
- Connected -
-
- {/if} - - -
- {#if scanning && networks === null} -
- - Scanning… -
- {:else if networks === null} -
- - No scan yet -
- {:else if nearby.length === 0 && !activeNet} -
- - No networks found -
- {:else} - {#each nearby as net (net.ssid)} -
-
- {#each [1, 2, 3, 4] as n (n)} - = n}> - {/each} -
-
- {net.ssid} - {net.known ? "Saved" : net.secured ? "Secured" : "Open"} -
- {#if net.secured}{/if} - -
- {/each} - {/if} -
- - {#if pendingSsid} -
- - -
- {/if} - {#if status} - {status} - {/if} - -
- - - - -
- - diff --git a/frontend/src/lib/views/NetworkHub.svelte b/frontend/src/lib/views/NetworkHub.svelte deleted file mode 100644 index da2b0fd..0000000 --- a/frontend/src/lib/views/NetworkHub.svelte +++ /dev/null @@ -1,16 +0,0 @@ - - - diff --git a/frontend/src/lib/views/NightLight.svelte b/frontend/src/lib/views/NightLight.svelte deleted file mode 100644 index 8f07737..0000000 --- a/frontend/src/lib/views/NightLight.svelte +++ /dev/null @@ -1,85 +0,0 @@ - - - - - {#if !st} - - {:else if !st.installed} - - - {:else} - st!.enabled, (v) => { st!.enabled = v; applyNow(); }} - /> - {#if st.enabled} - - {/if} - {/if} - {#if message}{/if} - - - diff --git a/frontend/src/lib/views/Optional.svelte b/frontend/src/lib/views/Optional.svelte deleted file mode 100644 index 882d537..0000000 --- a/frontend/src/lib/views/Optional.svelte +++ /dev/null @@ -1,149 +0,0 @@ - - - - - {#if !st} - - {:else} - {#each st.items as item (item.id)} -
-
- {item.title} -

{item.detail}

- {item.via}{item.installed ? " · installed" : ""} -
- {#if item.installed} - Installed - {:else} - - {/if} -
- {/each} - {/if} - {#if message}{/if} -
- -
- - diff --git a/frontend/src/lib/views/Packages.svelte b/frontend/src/lib/views/Packages.svelte deleted file mode 100644 index ec20f80..0000000 --- a/frontend/src/lib/views/Packages.svelte +++ /dev/null @@ -1,152 +0,0 @@ - - - - -
- {#if packages === null} - - {:else if packages.length === 0} - - {:else} - {#each packages as pkg (pkg.name)} -
- {pkg.name} - {pkg.version} - -
- {/each} - {/if} -
- -
- - -
-
- - - - - - -
- - diff --git a/frontend/src/lib/views/Power.svelte b/frontend/src/lib/views/Power.svelte deleted file mode 100644 index dcb50dd..0000000 --- a/frontend/src/lib/views/Power.svelte +++ /dev/null @@ -1,101 +0,0 @@ - - - - {#if info} - - {#each info.battery as [label, value] (label)} - - {/each} - - - - - {#if info.brightness_pct !== null} - - setBrightness(brightness)} /> - {brightness}% - - {:else} - - {/if} - - - {#if info.charge_start !== null && info.charge_end !== null} - - - setChargeThreshold("start", chargeStart)} /> - {chargeStart}% - - - setChargeThreshold("end", chargeEnd)} /> - {chargeEnd}% - - - {/if} - - - - - {/if} - - - diff --git a/frontend/src/lib/views/Printing.svelte b/frontend/src/lib/views/Printing.svelte deleted file mode 100644 index 2fc3ac5..0000000 --- a/frontend/src/lib/views/Printing.svelte +++ /dev/null @@ -1,147 +0,0 @@ - - - - - {#if !status} - - {:else if status.error} - - {:else if status.printers.length === 0} - - {:else} -
- {#each status.printers as p (p.name)} -
-
- {p.name}{p.is_default ? " (default)" : ""} - {p.enabled ? p.status : "disabled"} -
- -
- {/each} -
- {/if} -
- - -
- {#if message}{/if} -
- - - - - - -
- - diff --git a/frontend/src/lib/views/PrivacyHub.svelte b/frontend/src/lib/views/PrivacyHub.svelte deleted file mode 100644 index e0f7ef1..0000000 --- a/frontend/src/lib/views/PrivacyHub.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - diff --git a/frontend/src/lib/views/Snapshots.svelte b/frontend/src/lib/views/Snapshots.svelte deleted file mode 100644 index 592f82a..0000000 --- a/frontend/src/lib/views/Snapshots.svelte +++ /dev/null @@ -1,190 +0,0 @@ - - - - - -
- {#if snapshots === "loading"} - - {:else if errorHint} - - {:else if snapshots.length === 0} - - {:else} - - {#each snapshots as snap (snap.number)} - - {/each} - {/if} -
- -
- - - -
-
-
- - diff --git a/frontend/src/lib/views/Sound.svelte b/frontend/src/lib/views/Sound.svelte deleted file mode 100644 index 9c887a0..0000000 --- a/frontend/src/lib/views/Sound.svelte +++ /dev/null @@ -1,178 +0,0 @@ - - -{#snippet sectionCard(section: DeviceSection)} - - {#if section.devices.length === 0} - - {:else} - {#each section.devices as d, i (d.name + i)} - - {/each} - {#if current(section)} - - setVolume(section, Number(e.currentTarget.value))} - /> - {Math.round(current(section)!.percent)}% - - current(section)!.mute, (v) => setMute(section, v)} - /> - {/if} - {/if} - -{/snippet} - - - {#if output} - {@render sectionCard(output)} - {/if} - {#if input} - {@render sectionCard(input)} - {/if} - - - - - - - diff --git a/frontend/src/lib/views/SystemHub.svelte b/frontend/src/lib/views/SystemHub.svelte deleted file mode 100644 index b90060b..0000000 --- a/frontend/src/lib/views/SystemHub.svelte +++ /dev/null @@ -1,24 +0,0 @@ - - - diff --git a/frontend/src/lib/views/Updates.svelte b/frontend/src/lib/views/Updates.svelte deleted file mode 100644 index e566d55..0000000 --- a/frontend/src/lib/views/Updates.svelte +++ /dev/null @@ -1,250 +0,0 @@ - - - - {#if status?.nvidia} - -
- -
- {status.nvidia.gpu} -

{status.nvidia.reason}

- {#if status.nvidia.installed} - - {:else} - - {/if} -
- -
-
- {/if} - - - {#if !status} - - {:else if status.pacman_error} - - {:else if status.pacman.length === 0} - - {:else} -
- {#each status.pacman as pkg (pkg.name)} -
- {pkg.name} - {pkg.current} → {pkg.latest} -
- {/each} -
- {/if} -
- - -
-
- - - {#if !status} - - {:else if status.bakery_error} - - {:else if status.bakery.length === 0} - - {:else} -
- {#each status.bakery as pkg (pkg.name)} -
- {pkg.name} - {pkg.current ? `${pkg.current} → ` : ""}{pkg.latest} - -
- {/each} -
- {/if} -
- -
-
- - - {#if !status} - - {:else if status.firmware.length === 0} - - {:else} -
- {#each status.firmware as dev (dev.name)} -
- {dev.name} - {dev.version} -
- {/each} -
- {/if} -
- - -
-
- - - -
- -
-
- - -
- - diff --git a/frontend/src/lib/views/Users.svelte b/frontend/src/lib/views/Users.svelte deleted file mode 100644 index 5a74504..0000000 --- a/frontend/src/lib/views/Users.svelte +++ /dev/null @@ -1,196 +0,0 @@ - - - - -
- {#if accounts} - {#each accounts as acc (acc.username)} -
-
- {acc.username}{acc.full_name ? ` (${acc.full_name})` : ""} - - -
- {#if openPasswordFor === acc.username} -
- - -
- {/if} - {#if rowStatus[acc.username]} - {rowStatus[acc.username]} - {/if} -
- {/each} - {/if} -
-
- - - - - - - - - - - - - {#if addStatus} - {addStatus} - {/if} - -
- - diff --git a/frontend/src/lib/views/Vpn.svelte b/frontend/src/lib/views/Vpn.svelte deleted file mode 100644 index 1973575..0000000 --- a/frontend/src/lib/views/Vpn.svelte +++ /dev/null @@ -1,148 +0,0 @@ - - - - - {#if !status} - - {:else if status.error} - - {:else if status.connections.length === 0} - - {:else} -
- {#each status.connections as c (c.name)} -
-
- {c.name} - {c.kind}{c.active ? " · connected" : ""}{c.autoconnect ? " · autoconnect" : ""} -
- {#if c.active} - - {:else} - - {/if} -
- {/each} -
- {/if} - - {#if message}{/if} -
- - - - - -
- - diff --git a/frontend/src/lib/views/registry.ts b/frontend/src/lib/views/registry.ts deleted file mode 100644 index d36e136..0000000 --- a/frontend/src/lib/views/registry.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { Component } from "svelte"; -import AboutHub from "./AboutHub.svelte"; -import AppearanceHub from "./AppearanceHub.svelte"; -import AppsHub from "./AppsHub.svelte"; -import Bluetooth from "./Bluetooth.svelte"; -import DesktopHub from "./DesktopHub.svelte"; -import DisplaysHub from "./DisplaysHub.svelte"; -import Home from "./Home.svelte"; -import InputHub from "./InputHub.svelte"; -import NetworkHub from "./NetworkHub.svelte"; -import Power from "./Power.svelte"; -import PrivacyHub from "./PrivacyHub.svelte"; -import Sound from "./Sound.svelte"; -import SystemHub from "./SystemHub.svelte"; - -export const VIEWS: Record = { - home: Home, - network: NetworkHub, - bluetooth: Bluetooth, - displays: DisplaysHub, - sound: Sound, - power: Power, - appearance: AppearanceHub, - desktop: DesktopHub, - input: InputHub, - apps: AppsHub, - privacy: PrivacyHub, - system: SystemHub, - about: AboutHub, -}; diff --git a/frontend/src/routes/+layout.ts b/frontend/src/routes/+layout.ts deleted file mode 100644 index 9d24899..0000000 --- a/frontend/src/routes/+layout.ts +++ /dev/null @@ -1,5 +0,0 @@ -// Tauri doesn't have a Node.js server to do proper SSR -// so we use adapter-static with a fallback to index.html to put the site in SPA mode -// See: https://svelte.dev/docs/kit/single-page-apps -// See: https://v2.tauri.app/start/frontend/sveltekit/ for more info -export const ssr = false; diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte deleted file mode 100644 index 7916d15..0000000 --- a/frontend/src/routes/+page.svelte +++ /dev/null @@ -1,72 +0,0 @@ - - -
- -
- -
- {#key nav.page} - {#if ActiveView} - - {:else} - - {/if} - {/key} -
-
-
- - diff --git a/frontend/static/favicon.png b/frontend/static/favicon.png deleted file mode 100644 index 825b9e6..0000000 Binary files a/frontend/static/favicon.png and /dev/null differ diff --git a/frontend/svelte.config.js b/frontend/svelte.config.js deleted file mode 100644 index a7830ea..0000000 --- a/frontend/svelte.config.js +++ /dev/null @@ -1,18 +0,0 @@ -// Tauri doesn't have a Node.js server to do proper SSR -// so we use adapter-static with a fallback to index.html to put the site in SPA mode -// See: https://svelte.dev/docs/kit/single-page-apps -// See: https://v2.tauri.app/start/frontend/sveltekit/ for more info -import adapter from "@sveltejs/adapter-static"; -import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - preprocess: vitePreprocess(), - kit: { - adapter: adapter({ - fallback: "index.html", - }), - }, -}; - -export default config; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json deleted file mode 100644 index f4d0a0e..0000000 --- a/frontend/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "extends": "./.svelte-kit/tsconfig.json", - "compilerOptions": { - "allowJs": true, - "checkJs": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "sourceMap": true, - "strict": true, - "moduleResolution": "bundler" - } - // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias - // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files - // - // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes - // from the referenced tsconfig.json - TypeScript does not merge them in -} diff --git a/frontend/vite.config.js b/frontend/vite.config.js deleted file mode 100644 index 76b4712..0000000 --- a/frontend/vite.config.js +++ /dev/null @@ -1,39 +0,0 @@ -import { fileURLToPath } from "node:url"; -import path from "node:path"; -import { defineConfig } from "vite"; -import { sveltekit } from "@sveltejs/kit/vite"; - -const host = process.env.TAURI_DEV_HOST; - -// The Rust/Tauri project dir (../src relative to this file) — an absolute -// path, not a bare "**/src/**" glob, because this repo has two directories -// named `src`: this one (frontend/src, which must be watched for HMR) and -// the sibling Rust crate (../src, which must not be). -const rustProjectDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../src"); - -// https://vite.dev/config/ -export default defineConfig(async () => ({ - plugins: [sveltekit()], - - // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` - // - // 1. prevent Vite from obscuring rust errors - clearScreen: false, - // 2. tauri expects a fixed port, fail if that port is not available - server: { - port: 1420, - strictPort: true, - host: host || false, - hmr: host - ? { - protocol: "ws", - host, - port: 1421, - } - : undefined, - watch: { - // 3. tell Vite to ignore watching the Tauri/Rust project dir - ignored: [`${rustProjectDir}/**`], - }, - }, -})); diff --git a/packaging/PKGBUILD b/packaging/PKGBUILD new file mode 100644 index 0000000..805df59 --- /dev/null +++ b/packaging/PKGBUILD @@ -0,0 +1,38 @@ +# Maintainer: Breadway + +pkgname=bos-settings +pkgver=0.1.0 +pkgrel=1 +pkgdesc="System settings app for Bread OS" +arch=('x86_64') +url="https://git.breadway.dev/Breadway/bos-settings" +license=('MIT') +# Some Rust deps (ring/mlua) build vendored C/asm into static archives; makepkg's +# default -flto=auto emits GCC LTO bitcode the Rust (lld) link cannot read, +# causing undefined-symbol errors. Disable LTO. +options=(!lto !debug) +depends=('gtk4' 'glib2' 'hicolor-icon-theme') +optdepends=( + 'snapper: snapshot management view' +) +makedepends=('rust' 'cargo') +source=("${pkgname}-${pkgver}.tar.gz") +sha256sums=('SKIP') + +build() { + cd "${srcdir}/${pkgname}-${pkgver}" + cargo build --release --locked +} + +check() { + cd "${srcdir}/${pkgname}-${pkgver}" + cargo test --release --locked +} + +package() { + cd "${srcdir}/${pkgname}-${pkgver}" + install -Dm755 target/release/bos-settings "${pkgdir}/usr/bin/bos-settings" + install -Dm644 packaging/bos-settings.desktop \ + "${pkgdir}/usr/share/applications/bos-settings.desktop" + install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" +} diff --git a/packaging/README.md b/packaging/README.md new file mode 100644 index 0000000..af5acc0 --- /dev/null +++ b/packaging/README.md @@ -0,0 +1,25 @@ +Arch packaging +============== + +`PKGBUILD` builds and installs `bos-settings` from source. + +## Local build + +```bash +makepkg -si +``` + +## Before publishing to [breadway] repo + +1. Tag a release on GitHub. +2. Update `pkgver` to match the tag. +3. Update `source` to the release tarball URL. +4. Run `updpkgsums` (or manually set `sha256sums`). + +## Runtime dependencies + +| Package | Required | Notes | +|---------|----------|-------| +| `gtk4` | yes | UI toolkit | +| `glib2` | yes | always | +| `snapper` | optional | snapshot management view | diff --git a/src/.gitignore b/src/.gitignore deleted file mode 100644 index b21bd68..0000000 --- a/src/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# 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/src/Cargo.lock b/src/Cargo.lock deleted file mode 100644 index bf1adc3..0000000 --- a/src/Cargo.lock +++ /dev/null @@ -1,5424 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "aho-corasick" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" -dependencies = [ - "memchr", -] - -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" -dependencies = [ - "alloc-no-stdlib", -] - -[[package]] -name = "android_system_properties" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" -dependencies = [ - "libc", -] - -[[package]] -name = "anyhow" -version = "1.0.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" - -[[package]] -name = "async-broadcast" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" -dependencies = [ - "event-listener", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-executor" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "pin-project-lite", - "slab", -] - -[[package]] -name = "async-io" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" -dependencies = [ - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite", - "parking", - "polling", - "rustix", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-lock" -version = "3.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" -dependencies = [ - "event-listener", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-process" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" -dependencies = [ - "async-channel", - "async-io", - "async-lock", - "async-signal", - "async-task", - "blocking", - "cfg-if", - "event-listener", - "futures-lite", - "rustix", -] - -[[package]] -name = "async-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "async-signal" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" -dependencies = [ - "async-io", - "async-lock", - "atomic-waker", - "cfg-if", - "futures-core", - "futures-io", - "rustix", - "signal-hook-registry", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - -[[package]] -name = "async-trait" -version = "0.1.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "atk" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" -dependencies = [ - "atk-sys", - "glib", - "libc", -] - -[[package]] -name = "atk-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" -dependencies = [ - "serde_core", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "block2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" -dependencies = [ - "objc2", -] - -[[package]] -name = "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 = "bos-settings" -version = "0.8.2" -dependencies = [ - "anyhow", - "bread-theme", - "bread-utils", - "notify", - "regex", - "serde", - "serde_json", - "tauri", - "tauri-build", - "tauri-plugin-dialog", - "tauri-plugin-opener", - "tokio", - "toml_edit 0.22.27", -] - -[[package]] -name = "bread-theme" -version = "0.7.4" -source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.4#fcba3760387e2523edb71350f8efea3bc851b21e" -dependencies = [ - "dirs 5.0.1", - "serde", - "serde_json", -] - -[[package]] -name = "bread-utils" -version = "0.7.2" -source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.2#30517f161724132cdeb658c04cf5e490be07ee73" -dependencies = [ - "dirs 5.0.1", - "serde", - "serde_json", - "toml_edit 0.22.27", -] - -[[package]] -name = "brotli" -version = "8.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "5.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - -[[package]] -name = "bs58" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[package]] -name = "bytemuck" -version = "1.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" -dependencies = [ - "serde", -] - -[[package]] -name = "cairo-rs" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" -dependencies = [ - "bitflags 2.13.1", - "cairo-sys-rs", - "glib", - "libc", - "once_cell", - "thiserror 1.0.69", -] - -[[package]] -name = "cairo-sys-rs" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - -[[package]] -name = "camino" -version = "1.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d" -dependencies = [ - "serde_core", -] - -[[package]] -name = "cargo-platform" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" -dependencies = [ - "serde", -] - -[[package]] -name = "cargo_metadata" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" -dependencies = [ - "camino", - "cargo-platform", - "semver", - "serde", - "serde_json", - "thiserror 2.0.20", -] - -[[package]] -name = "cargo_toml" -version = "0.22.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" -dependencies = [ - "serde", - "toml 0.9.12+spec-1.1.0", -] - -[[package]] -name = "cc" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cfb" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" -dependencies = [ - "byteorder", - "fnv", - "uuid", -] - -[[package]] -name = "cfg-expr" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" -dependencies = [ - "smallvec", - "target-lexicon", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "chrono" -version = "0.4.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" -dependencies = [ - "iana-time-zone", - "num-traits", - "serde", - "windows-link 0.2.1", -] - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "cookie" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" -dependencies = [ - "time", - "version_check", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "core-graphics" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" -dependencies = [ - "bitflags 2.13.1", - "core-foundation", - "core-graphics-types", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" -dependencies = [ - "bitflags 2.13.1", - "core-foundation", - "libc", -] - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "cssparser" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" -dependencies = [ - "cssparser-macros", - "dtoa-short", - "itoa", - "phf", - "smallvec", -] - -[[package]] -name = "cssparser-macros" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" -dependencies = [ - "quote", - "syn 2.0.119", -] - -[[package]] -name = "ctor" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" -dependencies = [ - "ctor-proc-macro", - "dtor", -] - -[[package]] -name = "ctor-proc-macro" -version = "0.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" - -[[package]] -name = "darling" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" -dependencies = [ - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.119", -] - -[[package]] -name = "darling_macro" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "dbus" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" -dependencies = [ - "libc", - "libdbus-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "defmt" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" -dependencies = [ - "bitflags 1.3.2", - "defmt-macros", -] - -[[package]] -name = "defmt-macros" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" -dependencies = [ - "defmt-parser", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "defmt-parser" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" -dependencies = [ - "thiserror 2.0.20", -] - -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "serde_core", -] - -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.119", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "dirs" -version = "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 = "dispatch2" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" -dependencies = [ - "bitflags 2.13.1", - "block2", - "libc", - "objc2", -] - -[[package]] -name = "displaydoc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "dlopen2" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" -dependencies = [ - "dlopen2_derive", - "libc", - "once_cell", - "winapi", -] - -[[package]] -name = "dlopen2_derive" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "dom_query" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" -dependencies = [ - "bit-set", - "cssparser", - "foldhash", - "html5ever", - "precomputed-hash", - "selectors", - "tendril", -] - -[[package]] -name = "dpi" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" -dependencies = [ - "serde", -] - -[[package]] -name = "dtoa" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" - -[[package]] -name = "dtoa-short" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" -dependencies = [ - "dtoa", -] - -[[package]] -name = "dtor" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" -dependencies = [ - "dtor-proc-macro", -] - -[[package]] -name = "dtor-proc-macro" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - -[[package]] -name = "embed-resource" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd" -dependencies = [ - "cc", - "memchr", - "rustc_version", - "toml 1.1.4+spec-1.1.0", - "vswhom", - "winreg", -] - -[[package]] -name = "embed_plist" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" - -[[package]] -name = "endi" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" - -[[package]] -name = "enumflags2" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" -dependencies = [ - "enumflags2_derive", - "serde", -] - -[[package]] -name = "enumflags2_derive" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "erased-serde" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" -dependencies = [ - "serde", - "serde_core", - "typeid", -] - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "event-listener" -version = "5.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" -dependencies = [ - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener", - "pin-project-lite", -] - -[[package]] -name = "fastrand" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" - -[[package]] -name = "fdeflate" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "field-offset" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" -dependencies = [ - "memoffset", - "rustc_version", -] - -[[package]] -name = "filetime" -version = "0.2.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" -dependencies = [ - "cfg-if", - "libc", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" - -[[package]] -name = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "foreign-types" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" -dependencies = [ - "foreign-types-macros", - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "foreign-types-shared" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "fsevent-sys" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" -dependencies = [ - "libc", -] - -[[package]] -name = "futures-channel" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" -dependencies = [ - "futures-core", -] - -[[package]] -name = "futures-core" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" - -[[package]] -name = "futures-executor" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" - -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - -[[package]] -name = "futures-macro" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "futures-sink" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" - -[[package]] -name = "futures-task" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" - -[[package]] -name = "futures-util" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" -dependencies = [ - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", -] - -[[package]] -name = "gdk" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" -dependencies = [ - "cairo-rs", - "gdk-pixbuf", - "gdk-sys", - "gio", - "glib", - "libc", - "pango", -] - -[[package]] -name = "gdk-pixbuf" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" -dependencies = [ - "gdk-pixbuf-sys", - "gio", - "glib", - "libc", - "once_cell", -] - -[[package]] -name = "gdk-pixbuf-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" -dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gdk-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" -dependencies = [ - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "pkg-config", - "system-deps", -] - -[[package]] -name = "gdkwayland-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" -dependencies = [ - "gdk-sys", - "glib-sys", - "gobject-sys", - "libc", - "pkg-config", - "system-deps", -] - -[[package]] -name = "gdkx11" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" -dependencies = [ - "gdk", - "gdkx11-sys", - "gio", - "glib", - "libc", - "x11", -] - -[[package]] -name = "gdkx11-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" -dependencies = [ - "gdk-sys", - "glib-sys", - "libc", - "system-deps", - "x11", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi 5.3.0", - "wasip2", -] - -[[package]] -name = "getrandom" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", -] - -[[package]] -name = "gio" -version = "0.18.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "gio-sys", - "glib", - "libc", - "once_cell", - "pin-project-lite", - "smallvec", - "thiserror 1.0.69", -] - -[[package]] -name = "gio-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", - "winapi", -] - -[[package]] -name = "glib" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" -dependencies = [ - "bitflags 2.13.1", - "futures-channel", - "futures-core", - "futures-executor", - "futures-task", - "futures-util", - "gio-sys", - "glib-macros", - "glib-sys", - "gobject-sys", - "libc", - "memchr", - "once_cell", - "smallvec", - "thiserror 1.0.69", -] - -[[package]] -name = "glib-macros" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" -dependencies = [ - "heck 0.4.1", - "proc-macro-crate 2.0.0", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "glib-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" -dependencies = [ - "libc", - "system-deps", -] - -[[package]] -name = "glob" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" - -[[package]] -name = "gobject-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gtk" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" -dependencies = [ - "atk", - "cairo-rs", - "field-offset", - "futures-channel", - "gdk", - "gdk-pixbuf", - "gio", - "glib", - "gtk-sys", - "gtk3-macros", - "libc", - "pango", - "pkg-config", -] - -[[package]] -name = "gtk-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" -dependencies = [ - "atk-sys", - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "system-deps", -] - -[[package]] -name = "gtk3-macros" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" -dependencies = [ - "proc-macro-crate 1.3.1", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.5.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.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" -dependencies = [ - "log", - "markup5ever", -] - -[[package]] -name = "http" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "http-range" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "hyper" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "http", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-util" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core 0.62.2", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "ico" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" -dependencies = [ - "byteorder", - "png 0.17.16", -] - -[[package]] -name = "icu_collections" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" - -[[package]] -name = "icu_properties" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" - -[[package]] -name = "icu_provider" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92a7ed671a6aad807a8651a2e1782a6598fda9ce5185dd8158549e95a91c6428" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", - "serde", - "serde_core", -] - -[[package]] -name = "infer" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" -dependencies = [ - "cfb", -] - -[[package]] -name = "inotify" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc" -dependencies = [ - "bitflags 1.3.2", - "inotify-sys", - "libc", -] - -[[package]] -name = "inotify-sys" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d" -dependencies = [ - "libc", -] - -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "ipnet" -version = "2.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" - -[[package]] -name = "is-docker" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" -dependencies = [ - "once_cell", -] - -[[package]] -name = "is-wsl" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" -dependencies = [ - "is-docker", - "once_cell", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "javascriptcore-rs" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" -dependencies = [ - "bitflags 1.3.2", - "glib", - "javascriptcore-rs-sys", -] - -[[package]] -name = "javascriptcore-rs-sys" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "jiff" -version = "0.2.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" -dependencies = [ - "defmt", - "jiff-core", - "jiff-static", - "jiff-tzdb-platform", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", - "windows-link 0.2.1", -] - -[[package]] -name = "jiff-core" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" -dependencies = [ - "defmt", -] - -[[package]] -name = "jiff-static" -version = "0.2.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" -dependencies = [ - "jiff-core", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "jiff-tzdb" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" - -[[package]] -name = "jiff-tzdb-platform" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" -dependencies = [ - "jiff-tzdb", -] - -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys 0.3.1", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" -dependencies = [ - "jni-sys 0.4.1", -] - -[[package]] -name = "jni-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" -dependencies = [ - "jni-sys-macros", -] - -[[package]] -name = "jni-sys-macros" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" -dependencies = [ - "quote", - "syn 2.0.119", -] - -[[package]] -name = "js-sys" -version = "0.3.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" -dependencies = [ - "cfg-if", - "futures-util", - "wasm-bindgen", -] - -[[package]] -name = "json-patch" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" -dependencies = [ - "jsonptr", - "serde", - "serde_json", - "thiserror 1.0.69", -] - -[[package]] -name = "jsonptr" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "keyboard-types" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" -dependencies = [ - "bitflags 2.13.1", - "serde", - "unicode-segmentation", -] - -[[package]] -name = "kqueue" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d763e5b24120b4ddf50de6c92308156765aabfbbccebf401da7cff2d70a41ea" -dependencies = [ - "kqueue-sys", - "libc", -] - -[[package]] -name = "kqueue-sys" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" -dependencies = [ - "bitflags 2.13.1", - "libc", -] - -[[package]] -name = "libappindicator" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" -dependencies = [ - "glib", - "gtk", - "gtk-sys", - "libappindicator-sys", - "log", -] - -[[package]] -name = "libappindicator-sys" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" -dependencies = [ - "gtk-sys", - "libloading", - "once_cell", -] - -[[package]] -name = "libc" -version = "0.2.189" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" - -[[package]] -name = "libdbus-sys" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" -dependencies = [ - "pkg-config", -] - -[[package]] -name = "libloading" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" -dependencies = [ - "cfg-if", - "winapi", -] - -[[package]] -name = "libredox" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d0a00925a9f930d679b6789b721e3a7f9ed110f41b86d2497caa780c3a070a" -dependencies = [ - "libc", -] - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "litemap" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" - -[[package]] -name = "markup5ever" -version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" -dependencies = [ - "log", - "tendril", - "web_atoms", -] - -[[package]] -name = "memchr" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "mio" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "muda" -version = "0.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" -dependencies = [ - "crossbeam-channel", - "dpi", - "gtk", - "keyboard-types", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", - "once_cell", - "png 0.18.1", - "serde", - "thiserror 2.0.20", - "windows-sys 0.61.2", -] - -[[package]] -name = "ndk" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" -dependencies = [ - "bitflags 2.13.1", - "jni-sys 0.3.1", - "log", - "ndk-sys", - "num_enum", - "raw-window-handle", - "thiserror 1.0.69", -] - -[[package]] -name = "ndk-sys" -version = "0.6.0+11769913" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" -dependencies = [ - "jni-sys 0.3.1", -] - -[[package]] -name = "new_debug_unreachable" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" - -[[package]] -name = "notify" -version = "7.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009" -dependencies = [ - "bitflags 2.13.1", - "filetime", - "fsevent-sys", - "inotify", - "kqueue", - "libc", - "log", - "mio", - "notify-types", - "walkdir", - "windows-sys 0.52.0", -] - -[[package]] -name = "notify-types" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585d3cb5e12e01aed9e8a1f70d5c6b5e86fe2a6e48fc8cd0b3e0b8df6f6eb174" -dependencies = [ - "instant", -] - -[[package]] -name = "num-conv" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_enum" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" -dependencies = [ - "proc-macro-crate 3.5.0", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "objc2" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" -dependencies = [ - "objc2-encode", - "objc2-exception-helper", -] - -[[package]] -name = "objc2-app-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" -dependencies = [ - "bitflags 2.13.1", - "block2", - "objc2", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "objc2-cloud-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" -dependencies = [ - "bitflags 2.13.1", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-data" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags 2.13.1", - "dispatch2", - "objc2", -] - -[[package]] -name = "objc2-core-graphics" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" -dependencies = [ - "bitflags 2.13.1", - "dispatch2", - "objc2", - "objc2-core-foundation", - "objc2-io-surface", -] - -[[package]] -name = "objc2-core-image" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-location" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-text" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" -dependencies = [ - "bitflags 2.13.1", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "objc2-exception-helper" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" -dependencies = [ - "cc", -] - -[[package]] -name = "objc2-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" -dependencies = [ - "bitflags 2.13.1", - "block2", - "libc", - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-io-surface" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" -dependencies = [ - "bitflags 2.13.1", - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" -dependencies = [ - "bitflags 2.13.1", - "objc2", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "objc2-ui-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" -dependencies = [ - "bitflags 2.13.1", - "block2", - "objc2", - "objc2-cloud-kit", - "objc2-core-data", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-core-image", - "objc2-core-location", - "objc2-core-text", - "objc2-foundation", - "objc2-quartz-core", - "objc2-user-notifications", -] - -[[package]] -name = "objc2-user-notifications" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-web-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" -dependencies = [ - "bitflags 2.13.1", - "block2", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "open" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9cfef937e9c486488c7e3d949ae31c0f1d06bdacd75b99c086cb35356e30408" -dependencies = [ - "dunce", - "is-wsl", - "libc", -] - -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "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 = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "phf" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" -dependencies = [ - "phf_macros", - "phf_shared", - "serde", -] - -[[package]] -name = "phf_codegen" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" -dependencies = [ - "phf_generator", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" -dependencies = [ - "fastrand", - "phf_shared", -] - -[[package]] -name = "phf_macros" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "phf_shared" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "piper" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" -dependencies = [ - "atomic-waker", - "fastrand", - "futures-io", -] - -[[package]] -name = "pkg-config" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" - -[[package]] -name = "plist" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85" -dependencies = [ - "base64 0.22.1", - "indexmap 2.14.0", - "quick-xml", - "serde", - "time", -] - -[[package]] -name = "png" -version = "0.17.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" -dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "png" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" -dependencies = [ - "bitflags 2.13.1", - "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 = "portable-atomic" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" - -[[package]] -name = "portable-atomic-util" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "potential_utf" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "precomputed-hash" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" - -[[package]] -name = "proc-macro-crate" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" -dependencies = [ - "once_cell", - "toml_edit 0.19.15", -] - -[[package]] -name = "proc-macro-crate" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" -dependencies = [ - "toml_edit 0.20.7", -] - -[[package]] -name = "proc-macro-crate" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" -dependencies = [ - "toml_edit 0.25.13+spec-1.1.0", -] - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - -[[package]] -name = "proc-macro2" -version = "1.0.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quick-xml" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" -dependencies = [ - "memchr", -] - -[[package]] -name = "quote" -version = "1.0.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "raw-window-handle" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.13.1", -] - -[[package]] -name = "redox_users" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" -dependencies = [ - "getrandom 0.2.17", - "libredox", - "thiserror 1.0.69", -] - -[[package]] -name = "redox_users" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" -dependencies = [ - "getrandom 0.2.17", - "libredox", - "thiserror 2.0.20", -] - -[[package]] -name = "ref-cast" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "regex" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" - -[[package]] -name = "reqwest" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-core", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "serde", - "serde_json", - "sync_wrapper", - "tokio", - "tokio-util", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", -] - -[[package]] -name = "rfd" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" -dependencies = [ - "block2", - "dispatch2", - "glib-sys", - "gobject-sys", - "gtk-sys", - "js-sys", - "log", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", - "raw-window-handle", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows-sys 0.60.2", -] - -[[package]] -name = "rustc-hash" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags 2.13.1", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustversion" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "schemars" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" -dependencies = [ - "dyn-clone", - "indexmap 1.9.3", - "schemars_derive", - "serde", - "serde_json", - "url", - "uuid", -] - -[[package]] -name = "schemars" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schemars" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schemars_derive" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 2.0.119", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "selectors" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" -dependencies = [ - "bitflags 2.13.1", - "cssparser", - "derive_more", - "log", - "new_debug_unreachable", - "phf", - "phf_codegen", - "precomputed-hash", - "rustc-hash", - "servo_arc", - "smallvec", -] - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" -dependencies = [ - "serde", - "serde_core", -] - -[[package]] -name = "serde" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde-untagged" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" -dependencies = [ - "erased-serde", - "serde", - "serde_core", - "typeid", -] - -[[package]] -name = "serde_core" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "serde_derive_internals" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "serde_json" -version = "1.0.151" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_repr" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_spanned" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" -dependencies = [ - "serde_core", -] - -[[package]] -name = "serde_with" -version = "3.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee78f1fbe43ac4a0e47aadb3dbd357b69eb0d3793e948624cd03dd2750ab1c0a" -dependencies = [ - "base64 0.22.1", - "bs58", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.14.0", - "jiff", - "schemars 0.9.0", - "schemars 1.2.2", - "serde_core", - "serde_json", - "serde_with_macros", - "time", -] - -[[package]] -name = "serde_with_macros" -version = "3.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8705578779c2b6bd90d84d66eb2e206b708b1a4d7b9f17641b293545bf1c7e46" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "serialize-to-javascript" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" -dependencies = [ - "serde", - "serde_json", - "serialize-to-javascript-impl", -] - -[[package]] -name = "serialize-to-javascript-impl" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "servo_arc" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" -dependencies = [ - "stable_deref_trait", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[package]] -name = "simd-adler32" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" - -[[package]] -name = "siphasher" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "smallvec" -version = "1.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" - -[[package]] -name = "socket2" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "softbuffer" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" -dependencies = [ - "bytemuck", - "js-sys", - "ndk", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation", - "objc2-quartz-core", - "raw-window-handle", - "redox_syscall", - "tracing", - "wasm-bindgen", - "web-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "soup3" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" -dependencies = [ - "futures-channel", - "gio", - "glib", - "libc", - "soup3-sys", -] - -[[package]] -name = "soup3-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" -dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "string_cache" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" -dependencies = [ - "new_debug_unreachable", - "parking_lot", - "phf_shared", - "precomputed-hash", -] - -[[package]] -name = "string_cache_codegen" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "swift-rs" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" -dependencies = [ - "base64 0.21.7", - "serde", - "serde_json", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.119" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "system-deps" -version = "6.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" -dependencies = [ - "cfg-expr", - "heck 0.5.0", - "pkg-config", - "toml 0.8.23", - "version-compare", -] - -[[package]] -name = "tao" -version = "0.35.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" -dependencies = [ - "bitflags 2.13.1", - "block2", - "core-foundation", - "core-graphics", - "crossbeam-channel", - "dbus", - "dispatch2", - "dlopen2", - "dpi", - "gdkwayland-sys", - "gdkx11-sys", - "gtk", - "jni", - "libc", - "log", - "ndk", - "ndk-sys", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "objc2-ui-kit", - "once_cell", - "parking_lot", - "percent-encoding", - "raw-window-handle", - "tao-macros", - "unicode-segmentation", - "url", - "windows", - "windows-core 0.61.2", - "windows-version", - "x11-dl", -] - -[[package]] -name = "tao-macros" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7eeb6d99155545da6150a1795945f16ac9c178deb2a5f2e74d776107bd5849" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "target-lexicon" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" - -[[package]] -name = "tauri" -version = "2.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "667b20e2726d572dea2de7370da16e188eb06008faf9a92fab7cdc46791190b5" -dependencies = [ - "anyhow", - "bytes", - "cookie", - "dirs 6.0.0", - "dunce", - "embed_plist", - "getrandom 0.3.4", - "glob", - "gtk", - "heck 0.5.0", - "http", - "http-range", - "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.20", - "tokio", - "tray-icon", - "url", - "webkit2gtk", - "webview2-com", - "window-vibrancy", - "windows", -] - -[[package]] -name = "tauri-build" -version = "2.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" -dependencies = [ - "anyhow", - "cargo_toml", - "dirs 6.0.0", - "glob", - "heck 0.5.0", - "json-patch", - "schemars 0.8.22", - "semver", - "serde", - "serde_json", - "tauri-utils", - "tauri-winres", - "walkdir", -] - -[[package]] -name = "tauri-codegen" -version = "2.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" -dependencies = [ - "base64 0.22.1", - "brotli", - "ico", - "json-patch", - "plist", - "png 0.17.16", - "proc-macro2", - "quote", - "semver", - "serde", - "serde_json", - "sha2", - "syn 2.0.119", - "tauri-utils", - "thiserror 2.0.20", - "time", - "url", - "uuid", - "walkdir", -] - -[[package]] -name = "tauri-macros" -version = "2.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.119", - "tauri-codegen", - "tauri-utils", -] - -[[package]] -name = "tauri-plugin" -version = "2.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020" -dependencies = [ - "anyhow", - "glob", - "plist", - "schemars 0.8.22", - "serde", - "serde_json", - "tauri-utils", - "walkdir", -] - -[[package]] -name = "tauri-plugin-dialog" -version = "2.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2d3c1dbe38037e7f590cdf2492594d5ceebe031e7bc7e827509b22a999d2940" -dependencies = [ - "log", - "raw-window-handle", - "rfd", - "serde", - "serde_json", - "tauri", - "tauri-plugin", - "tauri-plugin-fs", - "thiserror 2.0.20", - "url", -] - -[[package]] -name = "tauri-plugin-fs" -version = "2.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371" -dependencies = [ - "anyhow", - "dunce", - "glob", - "log", - "objc2-foundation", - "percent-encoding", - "schemars 0.8.22", - "serde", - "serde_json", - "serde_repr", - "tauri", - "tauri-plugin", - "tauri-utils", - "thiserror 2.0.20", - "toml 1.1.4+spec-1.1.0", - "url", -] - -[[package]] -name = "tauri-plugin-opener" -version = "2.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17e1bea14edce6b793a04e2417e3fd924b9bc4faae83cdee7d714156cceeed29" -dependencies = [ - "dunce", - "glob", - "objc2-app-kit", - "objc2-foundation", - "open", - "schemars 0.8.22", - "serde", - "serde_json", - "tauri", - "tauri-plugin", - "thiserror 2.0.20", - "url", - "windows", - "zbus", -] - -[[package]] -name = "tauri-runtime" -version = "2.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" -dependencies = [ - "cookie", - "dpi", - "gtk", - "http", - "jni", - "objc2", - "objc2-ui-kit", - "objc2-web-kit", - "raw-window-handle", - "serde", - "serde_json", - "tauri-utils", - "thiserror 2.0.20", - "url", - "webkit2gtk", - "webview2-com", - "windows", -] - -[[package]] -name = "tauri-runtime-wry" -version = "2.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f" -dependencies = [ - "gtk", - "http", - "jni", - "log", - "objc2", - "objc2-app-kit", - "once_cell", - "percent-encoding", - "raw-window-handle", - "softbuffer", - "tao", - "tauri-runtime", - "tauri-utils", - "url", - "webkit2gtk", - "webview2-com", - "windows", - "wry", -] - -[[package]] -name = "tauri-utils" -version = "2.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" -dependencies = [ - "anyhow", - "brotli", - "cargo_metadata", - "ctor", - "dom_query", - "dunce", - "glob", - "http", - "infer", - "json-patch", - "log", - "memchr", - "phf", - "plist", - "proc-macro2", - "quote", - "regex", - "schemars 0.8.22", - "semver", - "serde", - "serde-untagged", - "serde_json", - "serde_with", - "swift-rs", - "thiserror 2.0.20", - "toml 1.1.4+spec-1.1.0", - "url", - "urlpattern", - "uuid", - "walkdir", -] - -[[package]] -name = "tauri-winres" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" -dependencies = [ - "dunce", - "embed-resource", - "toml 1.1.4+spec-1.1.0", -] - -[[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom 0.4.3", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "tendril" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" -dependencies = [ - "new_debug_unreachable", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" -dependencies = [ - "thiserror-impl 2.0.20", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "time" -version = "0.3.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" -dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" - -[[package]] -name = "time-macros" -version = "0.2.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tinystr" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" -dependencies = [ - "bytes", - "libc", - "mio", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "tokio-util" -version = "0.7.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_edit 0.22.27", -] - -[[package]] -name = "toml" -version = "0.9.12+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" -dependencies = [ - "indexmap 2.14.0", - "serde_core", - "serde_spanned 1.1.1", - "toml_datetime 0.7.5+spec-1.1.0", - "toml_parser", - "toml_writer", - "winnow 0.7.15", -] - -[[package]] -name = "toml" -version = "1.1.4+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" -dependencies = [ - "indexmap 2.14.0", - "serde_core", - "serde_spanned 1.1.1", - "toml_datetime 1.1.1+spec-1.1.0", - "toml_parser", - "toml_writer", - "winnow 1.0.4", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.19.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" -dependencies = [ - "indexmap 2.14.0", - "toml_datetime 0.6.11", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.20.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" -dependencies = [ - "indexmap 2.14.0", - "toml_datetime 0.6.11", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap 2.14.0", - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_write", - "winnow 0.7.15", -] - -[[package]] -name = "toml_edit" -version = "0.25.13+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" -dependencies = [ - "indexmap 2.14.0", - "toml_datetime 1.1.1+spec-1.1.0", - "toml_parser", - "winnow 1.0.4", -] - -[[package]] -name = "toml_parser" -version = "1.1.3+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" -dependencies = [ - "winnow 1.0.4", -] - -[[package]] -name = "toml_write" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" - -[[package]] -name = "toml_writer" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" - -[[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" -dependencies = [ - "bitflags 2.13.1", - "bytes", - "futures-util", - "http", - "http-body", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", - "url", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "tray-icon" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "045979e3f037cd18ad1cb2a419dfda133c5c29c9f3453370079f2255d46c257e" -dependencies = [ - "crossbeam-channel", - "dirs 6.0.0", - "libappindicator", - "muda", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation", - "once_cell", - "png 0.18.1", - "serde", - "thiserror 2.0.20", - "windows-sys 0.61.2", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "typeid" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" - -[[package]] -name = "typenum" -version = "1.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" - -[[package]] -name = "uds_windows" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" -dependencies = [ - "memoffset", - "tempfile", - "windows-sys 0.61.2", -] - -[[package]] -name = "unic-char-property" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" -dependencies = [ - "unic-char-range", -] - -[[package]] -name = "unic-char-range" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" - -[[package]] -name = "unic-common" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" - -[[package]] -name = "unic-ucd-ident" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" -dependencies = [ - "unic-char-property", - "unic-char-range", - "unic-ucd-version", -] - -[[package]] -name = "unic-ucd-version" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" -dependencies = [ - "unic-common", -] - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-segmentation" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", - "serde_derive", -] - -[[package]] -name = "urlpattern" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" -dependencies = [ - "regex", - "serde", - "unic-ucd-ident", - "url", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "uuid" -version = "1.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cefc03fd367c0c6d4305de1b312cf00248c4114f4a0418ce6a6af769e3b0bd9" -dependencies = [ - "getrandom 0.4.3", - "js-sys", - "serde_core", - "wasm-bindgen", -] - -[[package]] -name = "version-compare" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "vswhom" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" -dependencies = [ - "libc", - "vswhom-sys", -] - -[[package]] -name = "vswhom-sys" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.4+wasi-0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.127" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.127" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.127" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.119", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.127" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-streams" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "web-sys" -version = "0.3.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web_atoms" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba8b815c1b593dc0baf78dd0f4fc8fdb2de53198fb1163738093e9a311c33fb3" -dependencies = [ - "phf", - "phf_codegen", - "string_cache", - "string_cache_codegen", -] - -[[package]] -name = "webkit2gtk" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" -dependencies = [ - "bitflags 1.3.2", - "cairo-rs", - "gdk", - "gdk-sys", - "gio", - "gio-sys", - "glib", - "glib-sys", - "gobject-sys", - "gtk", - "gtk-sys", - "javascriptcore-rs", - "libc", - "once_cell", - "soup3", - "webkit2gtk-sys", -] - -[[package]] -name = "webkit2gtk-sys" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" -dependencies = [ - "bitflags 1.3.2", - "cairo-sys-rs", - "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "gtk-sys", - "javascriptcore-rs-sys", - "libc", - "pkg-config", - "soup3-sys", - "system-deps", -] - -[[package]] -name = "webview2-com" -version = "0.38.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" -dependencies = [ - "webview2-com-macros", - "webview2-com-sys", - "windows", - "windows-core 0.61.2", - "windows-implement", - "windows-interface", -] - -[[package]] -name = "webview2-com-macros" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "webview2-com-sys" -version = "0.38.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" -dependencies = [ - "thiserror 2.0.20", - "windows", - "windows-core 0.61.2", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "window-vibrancy" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" -dependencies = [ - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", - "raw-window-handle", - "windows-sys 0.59.0", - "windows-version", -] - -[[package]] -name = "windows" -version = "0.61.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" -dependencies = [ - "windows-collections", - "windows-core 0.61.2", - "windows-future", - "windows-link 0.1.3", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" -dependencies = [ - "windows-core 0.61.2", -] - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - -[[package]] -name = "windows-future" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", - "windows-threading", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-numerics" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", -] - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.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.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.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.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" -dependencies = [ - "memchr", -] - -[[package]] -name = "winnow" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" -dependencies = [ - "memchr", -] - -[[package]] -name = "winreg" -version = "0.55.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" -dependencies = [ - "cfg-if", - "windows-sys 0.59.0", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "writeable" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" - -[[package]] -name = "wry" -version = "0.55.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" -dependencies = [ - "base64 0.22.1", - "block2", - "cookie", - "crossbeam-channel", - "dirs 6.0.0", - "dom_query", - "dpi", - "dunce", - "gdkx11", - "gtk", - "http", - "javascriptcore-rs", - "jni", - "libc", - "ndk", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", - "objc2-ui-kit", - "objc2-web-kit", - "once_cell", - "percent-encoding", - "raw-window-handle", - "sha2", - "soup3", - "tao-macros", - "thiserror 2.0.20", - "url", - "webkit2gtk", - "webkit2gtk-sys", - "webview2-com", - "windows", - "windows-core 0.61.2", - "windows-version", - "x11-dl", -] - -[[package]] -name = "x11" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" -dependencies = [ - "libc", - "pkg-config", -] - -[[package]] -name = "x11-dl" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" -dependencies = [ - "libc", - "once_cell", - "pkg-config", -] - -[[package]] -name = "yoke" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", - "synstructure", -] - -[[package]] -name = "zbus" -version = "5.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907" -dependencies = [ - "async-broadcast", - "async-executor", - "async-io", - "async-lock", - "async-process", - "async-recursion", - "async-task", - "async-trait", - "blocking", - "enumflags2", - "event-listener", - "futures-core", - "futures-lite", - "hex", - "libc", - "ordered-stream", - "rustix", - "serde", - "serde_repr", - "tracing", - "uds_windows", - "uuid", - "windows-sys 0.61.2", - "winnow 1.0.4", - "zbus_macros", - "zbus_names", - "zvariant", -] - -[[package]] -name = "zbus_macros" -version = "5.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40" -dependencies = [ - "proc-macro-crate 3.5.0", - "proc-macro2", - "quote", - "syn 3.0.3", - "zbus_names", - "zvariant", - "zvariant_utils", -] - -[[package]] -name = "zbus_names" -version = "4.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" -dependencies = [ - "serde", - "winnow 1.0.4", - "zvariant", -] - -[[package]] -name = "zcheapstr" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473" -dependencies = [ - "serde", -] - -[[package]] -name = "zerofrom" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", - "synstructure", -] - -[[package]] -name = "zerotrie" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b5c6b5976d66c1d703c4fd17d3f5e43c8cedaacf604961b171adc7130896d8" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47402523226a02bfe5230160dc3ccc089aa6f6f19e7fcbb4e6f824bbb1b4aa62" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "zmij" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" - -[[package]] -name = "zvariant" -version = "5.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e28c25bd8bb8da5a1f3e7065d0c156b9ee9a7973adf78b0e35eaefdf3b1b5c" -dependencies = [ - "endi", - "enumflags2", - "serde", - "winnow 1.0.4", - "zcheapstr", - "zvariant_derive", - "zvariant_utils", -] - -[[package]] -name = "zvariant_derive" -version = "5.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d496a145685283b67e232bd9e47377f6b60ad9d51e3601b23867f77c42477f96" -dependencies = [ - "proc-macro-crate 3.5.0", - "proc-macro2", - "quote", - "syn 3.0.3", - "zvariant_utils", -] - -[[package]] -name = "zvariant_utils" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "629d80ece222cad20fe0e8741be493c4ab166acf3b85341bdc2cdbcfd8f3c2d6" -dependencies = [ - "proc-macro2", - "quote", - "serde", - "syn 3.0.3", - "winnow 1.0.4", -] diff --git a/src/Cargo.toml b/src/Cargo.toml deleted file mode 100644 index c74d506..0000000 --- a/src/Cargo.toml +++ /dev/null @@ -1,42 +0,0 @@ -[package] -name = "bos-settings" -version = "0.8.2" -description = "System settings app for BOS (Bread Operating System)" -authors = ["Breadway"] -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 = "bos_settings_lib" -crate-type = ["staticlib", "cdylib", "rlib"] - -[build-dependencies] -tauri-build = { version = "2", features = [] } - -[features] -# Default-on so a plain `cargo build --release` produces a standalone binary -# that embeds the built frontend instead of loading it from `devUrl` — -# `cargo tauri dev` strips this back off via `--no-default-features` so dev -# builds keep talking to the Vite dev server. See -# https://v2.tauri.app/reference/cargo-features/#custom-protocol -default = ["custom-protocol"] -custom-protocol = ["tauri/custom-protocol"] - -[dependencies] -tauri = { version = "2", features = ["protocol-asset"] } -tauri-plugin-opener = "2" -tauri-plugin-dialog = "2" -serde = { version = "1", features = ["derive"] } -serde_json = "1" -toml_edit = "0.22" -tokio = { version = "1", features = ["process", "io-util", "time", "macros"] } -notify = "7" -regex = "1" -bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.4" } -bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2", features = ["toml"] } -anyhow = "1" - diff --git a/src/build.rs b/src/build.rs deleted file mode 100644 index d860e1e..0000000 --- a/src/build.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - tauri_build::build() -} diff --git a/src/capabilities/default.json b/src/capabilities/default.json deleted file mode 100644 index 7039f81..0000000 --- a/src/capabilities/default.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$schema": "../gen/schemas/desktop-schema.json", - "identifier": "default", - "description": "Capability for the main window", - "windows": ["main"], - "permissions": [ - "core:default", - "core:window:allow-close", - "core:window:allow-minimize", - "core:window:allow-toggle-maximize", - "core:window:allow-start-dragging", - "dialog:default" - ] -} diff --git a/src/src/commands/config.rs b/src/config/mod.rs similarity index 90% rename from src/src/commands/config.rs rename to src/config/mod.rs index 31c140e..c1e4034 100644 --- a/src/src/commands/config.rs +++ b/src/config/mod.rs @@ -2,8 +2,7 @@ //! //! Every bread* app owns a TOML config that may contain keys, sections, and //! comments this settings app does not model (e.g. breadpad's calendar -//! credentials). Saved-network passwords live in breadcrumbs' separate -//! `networks.toml`, not in breadcrumbs.toml. To edit safely we parse +//! credentials, breadcrumbs' saved-network passwords). To edit safely we parse //! the file into a `toml_edit::DocumentMut`, mutate only the specific keys the //! UI exposes, and write the document back — preserving everything else, //! formatting and comments included. @@ -19,7 +18,7 @@ use toml_edit::{value, Array, DocumentMut, Item, Table, Value}; /// falling back to an empty document there means the next Save (see /// `save_doc`) overwrites it with only the UI-modelled keys, silently /// destroying anything else in the file (breadpad's calendar credentials, -/// unmodelled keys, ...). Back up the unparseable file +/// breadcrumbs' saved network passwords, ...). Back up the unparseable file /// once before falling back, so a bad edit is always recoverable. pub fn load_doc(path: &Path) -> DocumentMut { bread_utils::tomlcfg::load_doc("bos-settings", path) @@ -70,8 +69,7 @@ pub fn get_i64(doc: &DocumentMut, path: &[&str]) -> Option { } pub fn get_f64(doc: &DocumentMut, path: &[&str]) -> Option { let item = get(doc, path)?; - item.as_float() - .or_else(|| item.as_integer().map(|i| i as f64)) + item.as_float().or_else(|| item.as_integer().map(|i| i as f64)) } /// Read an array of strings (e.g. modules.disable, contexts[].priority). pub fn get_str_list(doc: &DocumentMut, path: &[&str]) -> Vec { @@ -178,10 +176,7 @@ password = \"secret\" # keep me let mut doc = DocumentMut::new(); set_bool(&mut doc, &["adapters", "power", "enabled"], false); set_i64(&mut doc, &["adapters", "power", "poll_interval_secs"], 45); - assert_eq!( - get_bool(&doc, &["adapters", "power", "enabled"]), - Some(false) - ); + assert_eq!(get_bool(&doc, &["adapters", "power", "enabled"]), Some(false)); assert_eq!( get_i64(&doc, &["adapters", "power", "poll_interval_secs"]), Some(45) @@ -205,20 +200,14 @@ password = \"secret\" # keep me #[test] fn atomic_write_backs_up_previous_contents_and_no_tmp_file_left_behind() { - let dir = std::env::temp_dir().join(format!( - "bos-settings-atomic-write-test-{}", - std::process::id() - )); + let dir = std::env::temp_dir().join(format!("bos-settings-atomic-write-test-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); let path = dir.join("config.toml"); let backup = dir.join("config.toml.bak"); atomic_write(&path, "first").unwrap(); assert_eq!(std::fs::read_to_string(&path).unwrap(), "first"); - assert!( - !backup.exists(), - "no backup should be made when there's nothing to back up yet" - ); + assert!(!backup.exists(), "no backup should be made when there's nothing to back up yet"); atomic_write(&path, "second").unwrap(); assert_eq!(std::fs::read_to_string(&path).unwrap(), "second"); @@ -230,10 +219,7 @@ password = \"secret\" # keep me .map(|e| e.file_name().to_string_lossy().into_owned()) .filter(|n| n.contains(".tmp.")) .collect(); - assert!( - leftover_tmp.is_empty(), - "temp file should be renamed away, not left behind: {leftover_tmp:?}" - ); + assert!(leftover_tmp.is_empty(), "temp file should be renamed away, not left behind: {leftover_tmp:?}"); let _ = std::fs::remove_dir_all(&dir); } diff --git a/src/icons/128x128.png b/src/icons/128x128.png deleted file mode 100644 index 6be5e50..0000000 Binary files a/src/icons/128x128.png and /dev/null differ diff --git a/src/icons/128x128@2x.png b/src/icons/128x128@2x.png deleted file mode 100644 index e81bece..0000000 Binary files a/src/icons/128x128@2x.png and /dev/null differ diff --git a/src/icons/32x32.png b/src/icons/32x32.png deleted file mode 100644 index a437dd5..0000000 Binary files a/src/icons/32x32.png and /dev/null differ diff --git a/src/icons/Square107x107Logo.png b/src/icons/Square107x107Logo.png deleted file mode 100644 index 0ca4f27..0000000 Binary files a/src/icons/Square107x107Logo.png and /dev/null differ diff --git a/src/icons/Square142x142Logo.png b/src/icons/Square142x142Logo.png deleted file mode 100644 index b81f820..0000000 Binary files a/src/icons/Square142x142Logo.png and /dev/null differ diff --git a/src/icons/Square150x150Logo.png b/src/icons/Square150x150Logo.png deleted file mode 100644 index 624c7bf..0000000 Binary files a/src/icons/Square150x150Logo.png and /dev/null differ diff --git a/src/icons/Square284x284Logo.png b/src/icons/Square284x284Logo.png deleted file mode 100644 index c021d2b..0000000 Binary files a/src/icons/Square284x284Logo.png and /dev/null differ diff --git a/src/icons/Square30x30Logo.png b/src/icons/Square30x30Logo.png deleted file mode 100644 index 6219700..0000000 Binary files a/src/icons/Square30x30Logo.png and /dev/null differ diff --git a/src/icons/Square310x310Logo.png b/src/icons/Square310x310Logo.png deleted file mode 100644 index f9bc048..0000000 Binary files a/src/icons/Square310x310Logo.png and /dev/null differ diff --git a/src/icons/Square44x44Logo.png b/src/icons/Square44x44Logo.png deleted file mode 100644 index d5fbfb2..0000000 Binary files a/src/icons/Square44x44Logo.png and /dev/null differ diff --git a/src/icons/Square71x71Logo.png b/src/icons/Square71x71Logo.png deleted file mode 100644 index 63440d7..0000000 Binary files a/src/icons/Square71x71Logo.png and /dev/null differ diff --git a/src/icons/Square89x89Logo.png b/src/icons/Square89x89Logo.png deleted file mode 100644 index f3f705a..0000000 Binary files a/src/icons/Square89x89Logo.png and /dev/null differ diff --git a/src/icons/StoreLogo.png b/src/icons/StoreLogo.png deleted file mode 100644 index 4556388..0000000 Binary files a/src/icons/StoreLogo.png and /dev/null differ diff --git a/src/icons/icon.icns b/src/icons/icon.icns deleted file mode 100644 index 12a5bce..0000000 Binary files a/src/icons/icon.icns and /dev/null differ diff --git a/src/icons/icon.ico b/src/icons/icon.ico deleted file mode 100644 index b3636e4..0000000 Binary files a/src/icons/icon.ico and /dev/null differ diff --git a/src/icons/icon.png b/src/icons/icon.png deleted file mode 100644 index e1cd261..0000000 Binary files a/src/icons/icon.png and /dev/null differ diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..1c28518 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,38 @@ +mod config; +mod theme; +mod ui; + +use gtk4::gio::ApplicationFlags; +use gtk4::prelude::*; + +fn main() { + let app = gtk4::Application::builder() + .application_id("com.breadway.bos-settings") + // HANDLES_COMMAND_LINE: without it, GApplication validates argv + // against registered GOptionEntries (none here) and would reject + // `--page ` with "unknown option" before `activate` ever runs. + .flags(ApplicationFlags::HANDLES_COMMAND_LINE) + .build(); + + app.connect_command_line(|app, cmdline| { + let page = parse_page_arg(&cmdline.arguments()); + ui::window::build_ui(app, page); + glib::ExitCode::SUCCESS + }); + + app.run(); +} + +/// Best-effort: like any non-command-line-aware launch of an already-running +/// GApplication, this is only seen by the launch that becomes primary — a +/// `--page` while bos-settings is already open just refocuses the existing +/// window on whatever page it was already showing. +fn parse_page_arg(args: &[std::ffi::OsString]) -> Option { + let mut it = args.iter().skip(1); + while let Some(arg) = it.next() { + if arg == "--page" { + return it.next().and_then(|s| s.to_str()).map(str::to_string); + } + } + None +} diff --git a/src/src/commands/a11y.rs b/src/src/commands/a11y.rs deleted file mode 100644 index 807bc1d..0000000 --- a/src/src/commands/a11y.rs +++ /dev/null @@ -1,116 +0,0 @@ -//! Accessibility toggles that actually do something on Hyprland. -//! Orca launches. Magnifier is Hyprland `cursor:zoom_factor`. Sticky/slow -//! keys are not exposed by Hyprland or xkeyboard-config rules — the UI -//! must show that honestly rather than a dead switch. - -use serde::Serialize; -use tokio::process::Command; - -use super::util::{command_exists, fail_output, pacman_installed}; - -#[derive(Serialize)] -pub struct A11yStatus { - orca_installed: bool, - orca_running: bool, - zoom_factor: f64, - sticky_keys_supported: bool, - slow_keys_supported: bool, - kmag_installed: bool, - note: String, -} - -async fn orca_running() -> bool { - Command::new("pgrep") - .args(["-x", "orca"]) - .status() - .await - .map(|s| s.success()) - .unwrap_or(false) -} - -async fn read_zoom() -> f64 { - let output = Command::new("hyprctl") - .args(["getoption", "cursor:zoom_factor", "-j"]) - .output() - .await; - let Ok(output) = output else { - return 1.0; - }; - let Ok(v) = serde_json::from_slice::(&output.stdout) else { - return 1.0; - }; - v.get("float") - .and_then(|x| x.as_f64()) - .or_else(|| v.get("int").and_then(|x| x.as_i64()).map(|i| i as f64)) - .unwrap_or(1.0) -} - -#[tauri::command] -pub async fn get_a11y_status() -> A11yStatus { - A11yStatus { - orca_installed: command_exists("orca") || pacman_installed("orca"), - orca_running: orca_running().await, - zoom_factor: read_zoom().await, - sticky_keys_supported: false, - slow_keys_supported: false, - kmag_installed: command_exists("kmag") || pacman_installed("kmag"), - note: "Hyprland does not expose XKB AccessX (sticky keys / slow keys). Those toggles stay off because they would not do anything.".into(), - } -} - -#[tauri::command] -pub async fn set_cursor_zoom(factor: f64) -> Result { - let factor = factor.clamp(1.0, 8.0); - let value = format!("{factor:.2}"); - let output = Command::new("hyprctl") - .args(["keyword", "cursor:zoom_factor", &value]) - .output() - .await - .map_err(|e| e.to_string())?; - if output.status.success() { - Ok(factor) - } else { - Err(fail_output(&output, "hyprctl keyword cursor:zoom_factor")) - } -} - -#[tauri::command] -pub async fn set_orca_running(running: bool) -> Result<(), String> { - if running { - if !command_exists("orca") { - return Err("orca is not installed".into()); - } - std::process::Command::new("orca") - .arg("--replace") - .stdin(std::process::Stdio::null()) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .spawn() - .map_err(|e| format!("couldn't start orca: {e}"))?; - Ok(()) - } else { - let _ = Command::new("pkill").args(["-x", "orca"]).status().await; - Ok(()) - } -} - -#[tauri::command] -pub fn open_kmag() -> Result<(), String> { - if !command_exists("kmag") { - return Err("kmag is not installed".into()); - } - std::process::Command::new("kmag") - .spawn() - .map_err(|e| format!("couldn't start kmag: {e}"))?; - Ok(()) -} - -#[cfg(test)] -mod tests { - #[test] - fn zoom_clamp_bounds() { - let f = 0.2_f64.clamp(1.0, 8.0); - assert_eq!(f, 1.0); - assert_eq!(12.0_f64.clamp(1.0, 8.0), 8.0); - } -} diff --git a/src/src/commands/about.rs b/src/src/commands/about.rs deleted file mode 100644 index b847a85..0000000 --- a/src/src/commands/about.rs +++ /dev/null @@ -1,208 +0,0 @@ -//! Read-only system info, plus the one thing worth making writable: hostname. -//! BOS is a rolling release (no fixed version number to show — `os-release` -//! ships `BUILD_ID=rolling` on purpose), so there's no "BOS 1.2.3" readout -//! here the way a point-release distro's About panel would have one. - -use serde::Serialize; -use std::fs; -use tokio::process::Command; - -#[derive(Serialize)] -pub struct SystemInfo { - os: String, - kernel: String, - cpu: String, - gpu: String, - memory: String, - disk: String, - uptime: String, - hostname: String, -} - -fn os_pretty_name() -> String { - fs::read_to_string("/etc/os-release") - .ok() - .and_then(|s| { - s.lines().find_map(|l| { - l.strip_prefix("PRETTY_NAME=") - .map(|v| v.trim_matches('"').to_string()) - }) - }) - .unwrap_or_else(|| "BOS".to_string()) -} - -fn hostname() -> String { - fs::read_to_string("/etc/hostname") - .map(|s| s.trim().to_string()) - .unwrap_or_else(|_| "unknown".to_string()) -} - -async fn kernel() -> String { - Command::new("uname") - .arg("-r") - .output() - .await - .ok() - .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) - .unwrap_or_else(|| "unknown".to_string()) -} - -fn cpu() -> String { - let model = fs::read_to_string("/proc/cpuinfo") - .ok() - .and_then(|s| { - s.lines().find_map(|l| { - l.strip_prefix("model name") - .map(|v| v.trim_start_matches([':', ' ', '\t']).to_string()) - }) - }) - .unwrap_or_else(|| "unknown".to_string()); - let cores = std::thread::available_parallelism() - .map(|n| n.get()) - .unwrap_or(0); - if cores > 0 { - format!("{model} ({cores} threads)") - } else { - model - } -} - -fn memory() -> String { - let kb = fs::read_to_string("/proc/meminfo").ok().and_then(|s| { - s.lines() - .find(|l| l.starts_with("MemTotal:")) - .and_then(|l| l.split_whitespace().nth(1)) - .and_then(|v| v.parse::().ok()) - }); - match kb { - Some(kb) => format!("{:.1} GiB", kb as f64 / 1024.0 / 1024.0), - None => "unknown".to_string(), - } -} - -async fn gpu() -> String { - let Ok(output) = Command::new("lspci").output().await else { - return "unknown".to_string(); - }; - let text = String::from_utf8_lossy(&output.stdout); - text.lines() - // "Display controller" covers integrated GPUs some laptop chipsets - // (this dev laptop's AMD Radeon 860M included) report under instead - // of "VGA compatible controller" — without it those show "unknown". - .find(|l| { - l.contains("VGA compatible controller") - || l.contains("3D controller") - || l.contains("Display controller") - }) - .and_then(|l| l.split(": ").nth(1)) - .unwrap_or("unknown") - .to_string() -} - -async fn disk_usage() -> String { - let Ok(output) = Command::new("df") - .args(["-h", "--output=used,size,pcent", "/"]) - .output() - .await - else { - return "unknown".to_string(); - }; - let text = String::from_utf8_lossy(&output.stdout); - text.lines() - .nth(1) - .map(|l| { - let cols: Vec<&str> = l.split_whitespace().collect(); - match cols.as_slice() { - [used, size, pcent] => format!("{used} of {size} used ({pcent})"), - _ => l.trim().to_string(), - } - }) - .unwrap_or_else(|| "unknown".to_string()) -} - -async fn uptime() -> String { - Command::new("uptime") - .arg("-p") - .output() - .await - .ok() - .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) - .unwrap_or_else(|| "unknown".to_string()) -} - -#[tauri::command] -pub async fn get_system_info() -> SystemInfo { - SystemInfo { - os: os_pretty_name(), - kernel: kernel().await, - cpu: cpu(), - gpu: gpu().await, - memory: memory(), - disk: disk_usage().await, - uptime: uptime().await, - hostname: hostname(), - } -} - -/// RFC 1123 labels (digit start allowed), no leading `-`. Linux static -/// hostnames are also capped at `HOST_NAME_MAX` (64). -fn valid_hostname(name: &str) -> bool { - let name = name.trim(); - if name.is_empty() || name.len() > 64 || name.starts_with('-') { - return false; - } - if name.contains('\n') || name.contains('\r') || name.contains('\0') { - return false; - } - name.split('.').all(valid_dns_label) -} - -fn valid_dns_label(label: &str) -> bool { - let b = label.as_bytes(); - if b.is_empty() || b.len() > 63 { - return false; - } - if !b[0].is_ascii_alphanumeric() || !b[b.len() - 1].is_ascii_alphanumeric() { - return false; - } - b.iter().all(|c| c.is_ascii_alphanumeric() || *c == b'-') -} - -#[tauri::command] -pub async fn set_hostname(name: String) -> Result<(), String> { - let name = name.trim(); - if !valid_hostname(name) { - return Err("invalid hostname".into()); - } - let output = Command::new("pkexec") - .args(["hostnamectl", "set-hostname", name]) - .output() - .await - .map_err(|e| e.to_string())?; - if output.status.success() { - Ok(()) - } else { - Err(String::from_utf8_lossy(&output.stderr).trim().to_string()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn hostname_rfc1123() { - assert!(valid_hostname("bos")); - assert!(valid_hostname("bos.local")); - assert!(valid_hostname("a1-b")); - assert!(valid_hostname("1host")); - assert!(!valid_hostname("")); - assert!(!valid_hostname("-bos")); - assert!(!valid_hostname("bos-")); - assert!(!valid_hostname("-foo.bar")); - assert!(!valid_hostname("foo_bar")); - assert!(!valid_hostname("bos\n-set-hostname evil")); - assert!(!valid_hostname("--help")); - assert!(!valid_hostname(&"a".repeat(65))); - } -} diff --git a/src/src/commands/appearance.rs b/src/src/commands/appearance.rs deleted file mode 100644 index a9b18ae..0000000 --- a/src/src/commands/appearance.rs +++ /dev/null @@ -1,75 +0,0 @@ -//! hypr/settings.json — Hyprland gaps/borders/blur/shadow/input, read by -//! `scripts/ui/settings.lua` on the Hyprland side. No comments to preserve, -//! so it's a plain typed struct round-tripped whole. -//! -//! `Default` here must stay in sync with `scripts/ui/settings.lua`'s -//! `DEFAULTS` table on the Hyprland side — two different languages/ -//! processes reading the same file, neither able to import the other's -//! defaults. - -use serde::{Deserialize, Serialize}; - -use super::config; - -#[derive(Clone, Serialize, Deserialize)] -#[serde(default)] -pub struct Appearance { - gaps_in: i64, - gaps_out: i64, - border_size: i64, - active_border: String, - inactive_border: String, - layout: String, - resize_on_border: bool, - rounding: i64, - blur_enabled: bool, - blur_size: i64, - blur_passes: i64, - shadow_enabled: bool, - shadow_range: i64, - shadow_render_power: i64, - kb_layout: String, - follow_mouse: i64, - natural_scroll: bool, -} - -impl Default for Appearance { - fn default() -> Self { - Self { - gaps_in: 5, - gaps_out: 10, - border_size: 2, - active_border: "rgba(88c0d0ff)".to_string(), - inactive_border: "rgba(4c566aff)".to_string(), - layout: "dwindle".to_string(), - resize_on_border: true, - rounding: 8, - blur_enabled: true, - blur_size: 6, - blur_passes: 2, - shadow_enabled: true, - shadow_range: 12, - shadow_render_power: 3, - kb_layout: "us".to_string(), - follow_mouse: 1, - natural_scroll: true, - } - } -} - -fn config_path() -> std::path::PathBuf { - config::config_dir().join("hypr/settings.json") -} - -#[tauri::command] -pub fn get_appearance() -> Appearance { - std::fs::read_to_string(config_path()).ok().and_then(|s| serde_json::from_str(&s).ok()).unwrap_or_default() -} - -#[tauri::command] -pub fn save_appearance(appearance: Appearance) -> Result<(), String> { - let json = serde_json::to_string_pretty(&appearance).map_err(|e| e.to_string())?; - config::atomic_write(&config_path(), &json).map_err(|e| e.to_string())?; - super::util::hypr_reload(); - Ok(()) -} diff --git a/src/src/commands/aur.rs b/src/src/commands/aur.rs deleted file mode 100644 index 6c72b64..0000000 --- a/src/src/commands/aur.rs +++ /dev/null @@ -1,66 +0,0 @@ -//! AUR search via yay — graphical discovery beyond bakery's bread ecosystem -//! and [breadway]'s own republished packages. -//! -//! Installing opens a terminal running `yay -S ` instead of a silent -//! `--noconfirm` install — deliberate, not a shortcut skipped. AUR packages -//! run arbitrary maintainer-supplied build scripts, and yay's interactive -//! PKGBUILD diff review (plus the sudo prompt) is the actual safety -//! mechanism against a malicious/compromised package; automating it away -//! would remove the one step that exists to catch that. - -use serde::Serialize; - -use super::util; - -#[derive(Serialize, Clone)] -pub struct AurResult { - name: String, - version: String, - description: String, -} - -#[tauri::command] -pub async fn search_aur(query: String) -> Vec { - let Ok(output) = tokio::process::Command::new("yay") - .args(["-Ss", "--aur", &query]) - .output() - .await - else { - return Vec::new(); - }; - let text = String::from_utf8_lossy(&output.stdout); - let mut results = Vec::new(); - let mut lines = text.lines().peekable(); - while let Some(header) = lines.next() { - // "aur/name version (+votes score) [Orphaned]" — name/version are - // always the first two whitespace-separated fields after "aur/". - let Some(rest) = header.strip_prefix("aur/") else { - continue; - }; - let mut parts = rest.split_whitespace(); - let Some(name) = parts.next() else { continue }; - let version = parts.next().unwrap_or("").to_string(); - let description = lines.next().unwrap_or("").trim().to_string(); - results.push(AurResult { - name: name.to_string(), - version, - description, - }); - if results.len() >= 50 { - break; - } - } - results -} - -#[tauri::command] -pub fn install_aur_package(pkg: String) -> Result<(), String> { - if !util::valid_pkg_name(&pkg) { - return Err(format!("refusing to install '{pkg}'")); - } - std::process::Command::new("kitty") - .args(["-e", "yay", "-S", &pkg]) - .spawn() - .map_err(|e| e.to_string())?; - Ok(()) -} diff --git a/src/src/commands/autostart.rs b/src/src/commands/autostart.rs deleted file mode 100644 index 47bcb26..0000000 --- a/src/src/commands/autostart.rs +++ /dev/null @@ -1,59 +0,0 @@ -//! hypr/autostart.json — the *extra*, user-toggleable autostart apps. The -//! core bootstrap sequence stays hardcoded in hyprland.lua on purpose (it's -//! timing/order-sensitive infrastructure, not something this panel exposes). - -use serde::{Deserialize, Serialize}; - -use super::config; - -#[derive(Clone, Serialize, Deserialize)] -pub struct AutostartEntry { - command: String, - #[serde(default)] - label: String, - #[serde(default = "default_true")] - enabled: bool, -} - -fn default_true() -> bool { - true -} - -#[derive(Serialize, Deserialize)] -struct AutostartFile { - #[serde(default)] - extra: Vec, -} - -fn default_extra() -> Vec { - vec![ - AutostartEntry { command: "breadbar".into(), label: "Bar (breadbar)".into(), enabled: true }, - AutostartEntry { command: "hypridle".into(), label: "Idle / lock daemon (hypridle)".into(), enabled: true }, - AutostartEntry { command: "bos-netcheck".into(), label: "Network connectivity check".into(), enabled: true }, - AutostartEntry { command: "breadhelp --autostart".into(), label: "BOS Help (first-run onboarding)".into(), enabled: true }, - ] -} - -fn config_path() -> std::path::PathBuf { - config::config_dir().join("hypr/autostart.json") -} - -#[tauri::command] -pub fn get_autostart_entries() -> Vec { - std::fs::read_to_string(config_path()) - .ok() - .and_then(|s| serde_json::from_str::(&s).ok()) - .map(|f| f.extra) - .unwrap_or_else(default_extra) -} - -/// Empty-command rows (still-being-typed "Add app" entries) are dropped on -/// save rather than written as a broken autostart.json entry the Lua loader -/// would otherwise have to reject. -#[tauri::command] -pub fn save_autostart_entries(entries: Vec) -> Result<(), String> { - let entries: Vec = entries.into_iter().filter(|e| !e.command.trim().is_empty()).collect(); - let file = AutostartFile { extra: entries }; - let json = serde_json::to_string_pretty(&file).map_err(|e| e.to_string())?; - config::atomic_write(&config_path(), &json).map_err(|e| e.to_string()) -} diff --git a/src/src/commands/backup.rs b/src/src/commands/backup.rs deleted file mode 100644 index 748e741..0000000 --- a/src/src/commands/backup.rs +++ /dev/null @@ -1,488 +0,0 @@ -//! restic backups of `$HOME`. Repo path + password live in -//! `~/.config/bos-settings/backup.toml` (0600). The password is write-only -//! to the webview — empty on save keeps the stored secret. - -use serde::{Deserialize, Serialize}; -use std::path::{Path, PathBuf}; -use tauri::AppHandle; -use tokio::process::Command; - -use super::config; -use super::streaming; -use super::util::{self, command_exists, fail_output}; - -fn home_dir() -> PathBuf { - std::env::var("HOME") - .map(PathBuf::from) - .unwrap_or_else(|_| PathBuf::from("/root")) -} - -fn backup_toml() -> PathBuf { - util::bos_settings_dir().join("backup.toml") -} - -#[derive(Clone)] -pub struct BackupSecrets { - pub repo: String, - pub password: Option, -} - -impl BackupSecrets { - fn empty() -> Self { - Self { - repo: String::new(), - password: None, - } - } -} - -pub fn load_secrets() -> BackupSecrets { - load_secrets_from(&backup_toml()) -} - -fn load_secrets_from(path: &Path) -> BackupSecrets { - let Ok(text) = std::fs::read_to_string(path) else { - return BackupSecrets::empty(); - }; - let doc = text.parse::().unwrap_or_default(); - BackupSecrets { - repo: config::get_str(&doc, &["repo"]).unwrap_or_default(), - password: config::get_str(&doc, &["password"]).filter(|s| !s.is_empty()), - } -} - -fn save_secrets_to(path: &Path, repo: &str, password: Option<&str>) -> Result<(), String> { - let existing = load_secrets_from(path); - let password = match password.map(str::trim).filter(|s| !s.is_empty()) { - Some(p) => Some(p.to_string()), - None => existing.password, - }; - let mut doc = toml_edit::DocumentMut::new(); - config::set_str(&mut doc, &["repo"], repo.trim()); - if let Some(p) = password.as_deref() { - config::set_str(&mut doc, &["password"], p); - } - util::write_secure(path, &doc.to_string()) -} - -#[derive(Serialize)] -pub struct BackupStatus { - restic_installed: bool, - repo: String, - has_password: bool, - snapshots: Vec, - error: Option, - home: String, -} - -#[derive(Serialize, Clone)] -pub struct ResticSnapshot { - id: String, - time: String, - paths: Vec, -} - -#[tauri::command] -pub fn get_backup_config() -> BackupStatus { - let s = load_secrets(); - BackupStatus { - restic_installed: command_exists("restic"), - repo: s.repo, - has_password: s.password.is_some(), - snapshots: Vec::new(), - error: None, - home: home_dir().to_string_lossy().into_owned(), - } -} - -#[derive(Deserialize)] -pub struct SaveBackupInput { - repo: String, - #[serde(default)] - password: Option, -} - -#[tauri::command] -pub fn save_backup_config(input: SaveBackupInput) -> Result<(), String> { - if !valid_repo(&input.repo) { - return Err("repo must be an absolute path or sftp:user@host:path".into()); - } - save_secrets_to(&backup_toml(), &input.repo, input.password.as_deref()) -} - -pub fn valid_repo(repo: &str) -> bool { - let repo = repo.trim(); - if repo.is_empty() || repo.len() > 512 || repo.contains('\n') || repo.contains('\0') { - return false; - } - if let Some(rest) = repo.strip_prefix("sftp:") { - return !rest.is_empty() && rest.contains('@') && rest.contains(':') && !rest.contains(' '); - } - std::path::Path::new(repo).is_absolute() -} - -fn require_ready() -> Result { - if !command_exists("restic") { - return Err("restic is not installed".into()); - } - let s = load_secrets(); - if !valid_repo(&s.repo) { - return Err("set a repository path first".into()); - } - if s.password.is_none() { - return Err("set a repository password first".into()); - } - Ok(s) -} - -fn restic_args<'a>(repo: &'a str, extra: &'a [&'a str]) -> Vec<&'a str> { - let mut args = vec!["--repo", repo]; - args.extend_from_slice(extra); - args -} - -#[tauri::command] -pub async fn restic_init(app: AppHandle, session_id: String) -> bool { - let Ok(s) = require_ready() else { - streaming::emit_line( - &app, - &session_id, - "Error: configure repo and password first", - ); - return false; - }; - let password = s.password.clone().unwrap_or_default(); - let extra = ["init"]; - let args = restic_args(&s.repo, &extra); - streaming::run_hardcoded_env( - app, - session_id, - "restic", - &args, - &[("RESTIC_PASSWORD", password)], - ) - .await -} - -fn exclude_args(home: &str) -> Vec { - let extras = [ - ".cache", - ".local/share/Trash", - ".local/share/Steam", - ".local/share/containers", - ".npm", - ".cargo/registry", - ".cargo/git", - ".rustup", - ".var/app", - ]; - let mut args = vec![ - "--exclude-caches".into(), - "--exclude".into(), - "node_modules".into(), - "--exclude".into(), - "target".into(), - "--exclude".into(), - ".git".into(), - ]; - for rel in extras { - args.push("--exclude".into()); - args.push(format!("{home}/{rel}")); - } - args -} - -#[tauri::command] -pub async fn restic_backup(app: AppHandle, session_id: String) -> bool { - let s = match require_ready() { - Ok(s) => s, - Err(e) => { - streaming::emit_line(&app, &session_id, &format!("Error: {e}")); - return false; - } - }; - let home = std::env::var("HOME").unwrap_or_else(|_| "/root".into()); - let password = s.password.clone().unwrap_or_default(); - let excludes = exclude_args(&home); - let mut args = vec!["--repo".to_string(), s.repo.clone()]; - args.extend(excludes); - args.push("backup".into()); - args.push(home); - let arg_refs: Vec<&str> = args.iter().map(String::as_str).collect(); - streaming::run_hardcoded_env( - app, - session_id, - "restic", - &arg_refs, - &[("RESTIC_PASSWORD", password)], - ) - .await -} - -/// `~/bos-restore-`. Never `$HOME` itself — restore writes into a new -/// directory so a bad snapshot cannot clobber the live home. -pub fn default_restore_dir(snapshot: &str) -> PathBuf { - home_dir().join(format!("bos-restore-{snapshot}")) -} - -fn normalize_abs(path: &Path) -> PathBuf { - path.components().collect() -} - -/// Absolute path, not `$HOME` and not `/`. Empty target means the default. -pub fn valid_restore_target(path: &Path) -> bool { - if !path.is_absolute() { - return false; - } - let s = path.to_string_lossy(); - if s.is_empty() || s.len() > 512 || s.contains('\n') || s.contains('\0') { - return false; - } - let normalized = normalize_abs(path); - if normalized == *"/" { - return false; - } - normalized != normalize_abs(&home_dir()) -} - -fn resolve_restore_target(snapshot: &str, target: Option<&str>) -> Result { - if !valid_snapshot_id(snapshot) { - return Err("invalid snapshot id".into()); - } - let dest = match target.map(str::trim).filter(|s| !s.is_empty()) { - Some(t) => PathBuf::from(t), - None => default_restore_dir(snapshot), - }; - if !valid_restore_target(&dest) { - return Err( - "restore target must be an absolute path that is not $HOME (default is ~/bos-restore-)" - .into(), - ); - } - Ok(dest) -} - -async fn run_restic_restore( - app: AppHandle, - session_id: String, - snapshot: String, - target: Option, - dry_run: bool, -) -> bool { - let s = match require_ready() { - Ok(s) => s, - Err(e) => { - streaming::emit_line(&app, &session_id, &format!("Error: {e}")); - return false; - } - }; - let snap = snapshot.trim(); - let dest = match resolve_restore_target(snap, target.as_deref()) { - Ok(p) => p, - Err(e) => { - streaming::emit_line(&app, &session_id, &format!("Error: {e}")); - return false; - } - }; - let dest_s = dest.to_string_lossy().into_owned(); - let password = s.password.clone().unwrap_or_default(); - let mut extra = vec![ - "restore".to_string(), - snap.to_string(), - "--target".into(), - dest_s.clone(), - ]; - if dry_run { - extra.push("--dry-run".into()); - } - streaming::emit_line( - &app, - &session_id, - &format!( - "{} {snap} → {dest_s}", - if dry_run { - "Dry-run restore" - } else { - "Restoring" - } - ), - ); - let extra_refs: Vec<&str> = extra.iter().map(String::as_str).collect(); - let args = restic_args(&s.repo, &extra_refs); - streaming::run_hardcoded_env( - app, - session_id, - "restic", - &args, - &[("RESTIC_PASSWORD", password)], - ) - .await -} - -#[tauri::command] -pub async fn restic_restore_dry_run( - app: AppHandle, - session_id: String, - snapshot: String, - target: Option, -) -> bool { - run_restic_restore(app, session_id, snapshot, target, true).await -} - -#[tauri::command] -pub async fn restic_restore( - app: AppHandle, - session_id: String, - snapshot: String, - target: Option, -) -> bool { - run_restic_restore(app, session_id, snapshot, target, false).await -} - -fn valid_snapshot_id(id: &str) -> bool { - if id == "latest" { - return true; - } - let bytes = id.as_bytes(); - !bytes.is_empty() && bytes.len() <= 64 && bytes.iter().all(|b| b.is_ascii_hexdigit()) -} - -#[tauri::command] -pub async fn list_restic_snapshots() -> Result, String> { - let s = require_ready()?; - let password = s.password.clone().unwrap_or_default(); - let output = Command::new("restic") - .args(["--repo", &s.repo, "snapshots", "--json"]) - .env("RESTIC_PASSWORD", password) - .output() - .await - .map_err(|e| e.to_string())?; - if !output.status.success() { - return Err(fail_output(&output, "restic snapshots")); - } - parse_snapshots(&output.stdout) -} - -fn parse_snapshots(bytes: &[u8]) -> Result, String> { - let v: serde_json::Value = - serde_json::from_slice(bytes).map_err(|e| format!("restic json: {e}"))?; - let Some(arr) = v.as_array() else { - return Ok(Vec::new()); - }; - Ok(arr - .iter() - .filter_map(|s| { - let id = s - .get("short_id") - .or_else(|| s.get("id")) - .and_then(|x| x.as_str())? - .to_string(); - let time = s - .get("time") - .and_then(|x| x.as_str()) - .unwrap_or("") - .to_string(); - let paths = s - .get("paths") - .and_then(|x| x.as_array()) - .map(|a| { - a.iter() - .filter_map(|p| p.as_str().map(str::to_string)) - .collect() - }) - .unwrap_or_default(); - Some(ResticSnapshot { id, time, paths }) - }) - .collect()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn repo_accepts_abs_and_sftp() { - assert!(valid_repo("/mnt/backup/bos")); - assert!(valid_repo("sftp:user@host:/backups/bos")); - assert!(!valid_repo("relative/path")); - assert!(!valid_repo("sftp:nocolon")); - assert!(!valid_repo("sftp:user host:/x")); - assert!(!valid_repo("")); - } - - #[test] - fn snapshot_id_hex_or_latest() { - assert!(valid_snapshot_id("latest")); - assert!(valid_snapshot_id("a1b2c3d4")); - assert!(!valid_snapshot_id("../x")); - assert!(!valid_snapshot_id("latest;rm")); - } - - #[test] - fn write_secure_is_0600_and_keeps_password() { - let dir = std::env::temp_dir().join(format!( - "bos-settings-backup-{}-{}", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_nanos()) - .unwrap_or(0) - )); - std::fs::create_dir_all(&dir).unwrap(); - let path = dir.join("backup.toml"); - save_secrets_to(&path, "/tmp/repo", Some("hunter2")).unwrap(); - let text = std::fs::read_to_string(&path).unwrap(); - assert!(text.contains("hunter2")); - assert!(text.contains("/tmp/repo")); - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777; - assert_eq!(mode, 0o600, "backup.toml must be 0600, got {mode:o}"); - } - save_secrets_to(&path, "/tmp/repo2", Some("")).unwrap(); - let text = std::fs::read_to_string(&path).unwrap(); - assert!(text.contains("hunter2"), "empty password keeps secret"); - assert!(text.contains("/tmp/repo2")); - let loaded = load_secrets_from(&path); - assert_eq!(loaded.password.as_deref(), Some("hunter2")); - let _ = std::fs::remove_dir_all(&dir); - } - - #[test] - fn parse_restic_json() { - let json = br#"[{"short_id":"abc123","time":"2026-08-15T01:00:00Z","paths":["/home/a"]}]"#; - let v = parse_snapshots(json).unwrap(); - assert_eq!(v[0].id, "abc123"); - assert_eq!(v[0].paths[0], "/home/a"); - } - - #[test] - fn restore_defaults_to_bos_restore_id_not_home() { - let dest = default_restore_dir("a1b2c3d4"); - let home = home_dir(); - assert_eq!(dest, home.join("bos-restore-a1b2c3d4")); - assert_ne!(dest, home); - assert!(valid_restore_target(&dest)); - assert!(!valid_restore_target(&home)); - assert!(!valid_restore_target(Path::new("/"))); - assert!(!valid_restore_target(Path::new("relative/path"))); - assert!(valid_restore_target(Path::new("/tmp/bos-restore-custom"))); - let resolved = resolve_restore_target("latest", None).unwrap(); - assert_eq!(resolved, home.join("bos-restore-latest")); - assert!(resolve_restore_target("latest", Some(home.to_str().unwrap())).is_err()); - } - - #[test] - fn exclude_covers_caches_and_containers() { - let args = exclude_args("/home/a"); - let joined = args.join(" "); - assert!(joined.contains("/home/a/.cache")); - assert!(joined.contains("/home/a/.local/share/Trash")); - assert!(joined.contains("/home/a/.local/share/Steam")); - assert!(joined.contains("/home/a/.local/share/containers")); - assert!(joined.contains("node_modules")); - assert!(joined.contains("target")); - assert!(joined.contains(".git")); - } -} diff --git a/src/src/commands/bluetooth.rs b/src/src/commands/bluetooth.rs deleted file mode 100644 index 4bb82f5..0000000 --- a/src/src/commands/bluetooth.rs +++ /dev/null @@ -1,123 +0,0 @@ -//! Bluetooth over `bluetoothctl`'s non-interactive mode — no D-Bus -//! dependency needed, same "shell out to the standard CLI" choice as -//! Network (nmcli). None of this needs `pkexec` — BlueZ's D-Bus policy -//! already allows the active session user. -//! -//! Pairing only covers "Just Works" Simple Secure Pairing — bluetoothd's -//! own built-in default agent auto-accepts that for most audio/HID -//! devices. A device that requires PIN/passkey confirmation isn't -//! supported (would need this app to register its own bluetoothd agent); -//! pairing such a device just fails, surfaced as an error. - -use serde::Serialize; -use std::collections::HashSet; -use tokio::process::Command; - -#[derive(Serialize, Clone)] -pub struct BtDevice { - address: String, - name: String, - connected: bool, -} - -#[tauri::command] -pub async fn get_adapter_powered() -> Option { - let output = Command::new("bluetoothctl").arg("show").output().await.ok()?; - if !output.status.success() { - return None; - } - let text = String::from_utf8_lossy(&output.stdout); - if !text.trim_start().starts_with("Controller") { - return None; - } - Some(text.lines().any(|l| l.trim() == "Powered: yes")) -} - -#[tauri::command] -pub async fn set_adapter_powered(on: bool) { - let val = if on { "on" } else { "off" }; - let _ = Command::new("bluetoothctl").args(["power", val]).status().await; -} - -fn parse_device_lines(text: &str) -> Vec { - text.lines() - .filter_map(|l| { - let rest = l.strip_prefix("Device ")?; - let (addr, name) = rest.split_once(' ')?; - Some(BtDevice { address: addr.trim().to_string(), name: name.trim().to_string(), connected: false }) - }) - .collect() -} - -async fn run_devices(filter: Option<&str>) -> Vec { - let mut args = vec!["devices"]; - if let Some(f) = filter { - args.push(f); - } - let Ok(out) = Command::new("bluetoothctl").args(&args).output().await else { - return Vec::new(); - }; - parse_device_lines(&String::from_utf8_lossy(&out.stdout)) -} - -#[tauri::command] -pub async fn get_paired_devices() -> Vec { - let mut devices = run_devices(Some("Paired")).await; - let connected: HashSet = run_devices(Some("Connected")).await.into_iter().map(|d| d.address).collect(); - for d in &mut devices { - d.connected = connected.contains(&d.address); - } - devices -} - -/// Scans for a few seconds and returns every device BlueZ has seen that -/// isn't already paired. -#[tauri::command] -pub async fn scan_bluetooth() -> Vec { - let _ = Command::new("bluetoothctl").args(["--timeout", "5", "scan", "on"]).output().await; - let paired: HashSet = run_devices(Some("Paired")).await.into_iter().map(|d| d.address).collect(); - run_devices(None).await.into_iter().filter(|d| !paired.contains(&d.address)).collect() -} - -#[tauri::command] -pub async fn bt_connect(address: String) -> Result<(), String> { - let out = Command::new("bluetoothctl").args(["connect", &address]).output().await.map_err(|e| e.to_string())?; - if out.status.success() { - Ok(()) - } else { - Err(String::from_utf8_lossy(&out.stdout).trim().to_string()) - } -} - -#[tauri::command] -pub async fn bt_disconnect(address: String) -> Result<(), String> { - let out = Command::new("bluetoothctl").args(["disconnect", &address]).output().await.map_err(|e| e.to_string())?; - if out.status.success() { - Ok(()) - } else { - Err(String::from_utf8_lossy(&out.stdout).trim().to_string()) - } -} - -#[tauri::command] -pub async fn bt_forget(address: String) -> Result<(), String> { - let out = Command::new("bluetoothctl").args(["remove", &address]).output().await.map_err(|e| e.to_string())?; - if out.status.success() { - Ok(()) - } else { - Err(String::from_utf8_lossy(&out.stdout).trim().to_string()) - } -} - -/// Pairs, then confirms it actually landed in the paired-device cache — -/// bluetoothctl's exit code alone isn't a reliable signal for pairing. -#[tauri::command] -pub async fn bt_pair(address: String) -> Result<(), String> { - let out = Command::new("bluetoothctl").args(["pair", &address]).output().await.map_err(|e| e.to_string())?; - let now_paired = run_devices(Some("Paired")).await.iter().any(|d| d.address == address); - if now_paired { - Ok(()) - } else { - Err(String::from_utf8_lossy(&out.stdout).trim().to_string()) - } -} diff --git a/src/src/commands/bread.rs b/src/src/commands/bread.rs deleted file mode 100644 index b7f1bb2..0000000 --- a/src/src/commands/bread.rs +++ /dev/null @@ -1,123 +0,0 @@ -//! breadd.toml — the bread daemon config. Schema mirrors -//! breadd/src/core/config.rs (daemon, lua, modules, adapters, events, -//! notifications). Edited non-destructively via `commands::config`. - -use serde::{Deserialize, Serialize}; - -use super::config; - -fn config_path() -> std::path::PathBuf { - config::config_dir().join("bread/breadd.toml") -} - -#[derive(Serialize, Deserialize)] -pub struct BreadConfig { - log_level: String, - socket_path: String, - lua_entry_point: String, - lua_module_path: String, - modules_builtin: bool, - modules_disable: Vec, - adapter_hyprland: bool, - adapter_udev: bool, - udev_subsystems: Vec, - adapter_power: bool, - power_poll_interval_secs: i64, - adapter_network: bool, - adapter_bluetooth: bool, - dedup_window_ms: i64, - notif_default_timeout_ms: i64, - notif_default_urgency: String, - notif_notify_send_path: String, -} - -#[tauri::command] -pub fn get_bread_config() -> BreadConfig { - let doc = config::load_doc(&config_path()); - BreadConfig { - log_level: config::get_str(&doc, &["daemon", "log_level"]).unwrap_or_else(|| "info".into()), - socket_path: config::get_str(&doc, &["daemon", "socket_path"]).unwrap_or_default(), - lua_entry_point: config::get_str(&doc, &["lua", "entry_point"]).unwrap_or_default(), - lua_module_path: config::get_str(&doc, &["lua", "module_path"]).unwrap_or_default(), - modules_builtin: config::get_bool(&doc, &["modules", "builtin"]).unwrap_or(true), - modules_disable: config::get_str_list(&doc, &["modules", "disable"]), - adapter_hyprland: config::get_bool(&doc, &["adapters", "hyprland", "enabled"]).unwrap_or(true), - adapter_udev: config::get_bool(&doc, &["adapters", "udev", "enabled"]).unwrap_or(true), - udev_subsystems: config::get_str_list(&doc, &["adapters", "udev", "subsystems"]), - adapter_power: config::get_bool(&doc, &["adapters", "power", "enabled"]).unwrap_or(true), - power_poll_interval_secs: config::get_i64(&doc, &["adapters", "power", "poll_interval_secs"]).unwrap_or(30), - adapter_network: config::get_bool(&doc, &["adapters", "network", "enabled"]).unwrap_or(true), - adapter_bluetooth: config::get_bool(&doc, &["adapters", "bluetooth", "enabled"]).unwrap_or(true), - dedup_window_ms: config::get_i64(&doc, &["events", "dedup_window_ms"]).unwrap_or(250), - notif_default_timeout_ms: config::get_i64(&doc, &["notifications", "default_timeout_ms"]).unwrap_or(5000), - notif_default_urgency: config::get_str(&doc, &["notifications", "default_urgency"]).unwrap_or_else(|| "normal".into()), - notif_notify_send_path: config::get_str(&doc, &["notifications", "notify_send_path"]).unwrap_or_default(), - } -} - -/// Real, pickable module names for the "Disabled modules" field: breadd's -/// four compiled-in modules (bread/breadd/src/lua/mod.rs's `BUILTIN_*` -/// registry — these aren't files on disk, so a directory scan alone would -/// miss them) plus every `.lua` file actually sitting in the configured -/// module directory (the user's own custom widgets/modules — the common -/// case in practice, going by real configs seen in the wild). -#[tauri::command] -pub fn list_bread_modules() -> Vec { - let mut modules = vec![ - "bread.monitors".to_string(), - "bread.devices".to_string(), - "bread.workspaces".to_string(), - "bread.binds".to_string(), - ]; - - let doc = config::load_doc(&config_path()); - let configured = config::get_str(&doc, &["lua", "module_path"]); - let module_dir = expand_home(configured.as_deref().filter(|s| !s.is_empty()).unwrap_or("~/.config/bread/modules")); - - if let Ok(entries) = std::fs::read_dir(&module_dir) { - let mut found: Vec = entries - .filter_map(|e| e.ok()) - .filter(|e| e.path().extension().and_then(|s| s.to_str()) == Some("lua")) - .filter_map(|e| e.file_name().into_string().ok()) - .collect(); - found.sort(); - modules.extend(found); - } - - modules -} - -/// Expands a leading `~/` against `$HOME` — breadd's own config resolution -/// (`Config::lua_module_path`) does the same for this exact field. -fn expand_home(path: &str) -> std::path::PathBuf { - if let Some(rest) = path.strip_prefix("~/") { - if let Some(home) = std::env::var_os("HOME") { - return std::path::PathBuf::from(home).join(rest); - } - } - std::path::PathBuf::from(path) -} - -#[tauri::command] -pub fn save_bread_config(cfg: BreadConfig) -> Result<(), String> { - let path = config_path(); - let mut doc = config::load_doc(&path); - config::set_str(&mut doc, &["daemon", "log_level"], &cfg.log_level); - config::set_str_or_remove(&mut doc, &["daemon", "socket_path"], &cfg.socket_path); - config::set_str_or_remove(&mut doc, &["lua", "entry_point"], &cfg.lua_entry_point); - config::set_str_or_remove(&mut doc, &["lua", "module_path"], &cfg.lua_module_path); - config::set_bool(&mut doc, &["modules", "builtin"], cfg.modules_builtin); - config::set_str_list(&mut doc, &["modules", "disable"], &cfg.modules_disable); - config::set_bool(&mut doc, &["adapters", "hyprland", "enabled"], cfg.adapter_hyprland); - config::set_bool(&mut doc, &["adapters", "udev", "enabled"], cfg.adapter_udev); - config::set_str_list(&mut doc, &["adapters", "udev", "subsystems"], &cfg.udev_subsystems); - config::set_bool(&mut doc, &["adapters", "power", "enabled"], cfg.adapter_power); - config::set_i64(&mut doc, &["adapters", "power", "poll_interval_secs"], cfg.power_poll_interval_secs); - config::set_bool(&mut doc, &["adapters", "network", "enabled"], cfg.adapter_network); - config::set_bool(&mut doc, &["adapters", "bluetooth", "enabled"], cfg.adapter_bluetooth); - config::set_i64(&mut doc, &["events", "dedup_window_ms"], cfg.dedup_window_ms); - config::set_i64(&mut doc, &["notifications", "default_timeout_ms"], cfg.notif_default_timeout_ms); - config::set_str(&mut doc, &["notifications", "default_urgency"], &cfg.notif_default_urgency); - config::set_str_or_remove(&mut doc, &["notifications", "notify_send_path"], &cfg.notif_notify_send_path); - config::save_doc(&path, &doc).map_err(|e| e.to_string()) -} diff --git a/src/src/commands/breadbar.rs b/src/src/commands/breadbar.rs deleted file mode 100644 index 8bdbecb..0000000 --- a/src/src/commands/breadbar.rs +++ /dev/null @@ -1,144 +0,0 @@ -//! breadbar/style.css — CSS overrides for the bar. No systemd unit (breadbar -//! is launched directly by hyprland.lua's exec-once); SIGHUP is its own -//! documented live-reload mechanism. -//! -//! The file has a fixed, hand-written structure (see the shipped template's -//! comments), so the common properties users actually tweak — font, bar -//! chrome, workspace indicator, spacing, tray/notification radii — are -//! exposed as a typed `BreadbarStyle` struct. `get`/`set_value` locate a -//! known `selector { ... }` block and rewrite just one declaration's value -//! inside it, leaving comments, ordering, and every unmodeled property -//! (font-weight, per-element opacities, notification padding, etc.) -//! untouched. Anything not modeled here stays reachable via the raw -//! `get`/`save_breadbar_css` pair, kept as an "Advanced" escape hatch. - -use regex::{Captures, Regex}; - -use super::config; - -fn css_path() -> std::path::PathBuf { - config::config_dir().join("breadbar/style.css") -} - -#[derive(serde::Serialize, serde::Deserialize)] -pub struct BreadbarStyle { - pub font_family: String, - pub font_size: u32, - pub bar_border_radius: u32, - pub bar_padding: u32, - pub workspace_inactive_opacity: f64, - pub workspace_font_size: u32, - pub stat_gap: u32, - pub tray_icon_size: u32, - pub notification_border_radius: u32, -} - -fn find_block(css: &str, selector: &str) -> Option<(usize, usize)> { - let pat = format!(r"(?m)^\s*{}\s*\{{", regex::escape(selector)); - let re = Regex::new(&pat).ok()?; - let m = re.find(css)?; - let body_start = m.end(); - let body_end = body_start + css[body_start..].find('}')?; - Some((body_start, body_end)) -} - -fn get_value(css: &str, selector: &str, property: &str) -> Option { - let (start, end) = find_block(css, selector)?; - let body = &css[start..end]; - let re = Regex::new(&format!(r"(?m)^\s*{}\s*:\s*([^;]+);", regex::escape(property))).ok()?; - Some(re.captures(body)?.get(1)?.as_str().trim().to_string()) -} - -fn set_value(css: &str, selector: &str, property: &str, new_value: &str) -> Option { - let (start, end) = find_block(css, selector)?; - let body = &css[start..end]; - let re = Regex::new(&format!(r"(?m)(^\s*{}\s*:\s*)([^;]+)(;)", regex::escape(property))).ok()?; - if !re.is_match(body) { - return None; - } - let new_body = re - .replace(body, |caps: &Captures| format!("{}{}{}", &caps[1], new_value, &caps[3])) - .into_owned(); - Some(format!("{}{}{}", &css[..start], new_body, &css[end..])) -} - -fn strip_px(v: &str) -> u32 { - v.trim().trim_end_matches("px").trim().parse().unwrap_or(0) -} - -#[tauri::command] -pub fn get_breadbar_css() -> String { - std::fs::read_to_string(css_path()).unwrap_or_default() -} - -/// Saves the CSS and sends breadbar SIGHUP to live-reload it. Returns -/// whether the reload signal was actually delivered (`false` just means -/// breadbar isn't running — the file is saved either way). -#[tauri::command] -pub fn save_breadbar_css(css: String) -> Result { - config::atomic_write(&css_path(), &css).map_err(|e| e.to_string())?; - reload_breadbar() -} - -#[tauri::command] -pub fn get_breadbar_style() -> BreadbarStyle { - let css = std::fs::read_to_string(css_path()).unwrap_or_default(); - BreadbarStyle { - font_family: get_value(&css, "*", "font-family") - .and_then(|v| v.split(',').next().map(|s| s.trim().trim_matches(['\'', '"']).to_string())) - .unwrap_or_else(|| "Varela Round".to_string()), - font_size: get_value(&css, "*", "font-size").map(|v| strip_px(&v)).unwrap_or(14), - bar_border_radius: get_value(&css, "window.breadbar", "border-radius") - .map(|v| strip_px(&v)) - .unwrap_or(0), - bar_padding: get_value(&css, "window.breadbar", "padding").map(|v| strip_px(&v)).unwrap_or(0), - workspace_inactive_opacity: get_value(&css, ".workspace-btn", "opacity") - .and_then(|v| v.trim().parse().ok()) - .unwrap_or(0.45), - workspace_font_size: get_value(&css, ".workspace-btn", "font-size") - .map(|v| strip_px(&v)) - .unwrap_or(20), - stat_gap: get_value(&css, ".stat-pair", "margin-right").map(|v| strip_px(&v)).unwrap_or(12), - tray_icon_size: get_value(&css, ".tray-btn image", "-gtk-icon-size") - .map(|v| strip_px(&v)) - .unwrap_or(16), - notification_border_radius: get_value(&css, "window.breadbar-notification", "border-radius") - .map(|v| strip_px(&v)) - .unwrap_or(6), - } -} - -#[tauri::command] -pub fn save_breadbar_style(style: BreadbarStyle) -> Result { - let mut css = std::fs::read_to_string(css_path()).unwrap_or_default(); - let px = |n: u32| format!("{n}px"); - - let edits: [(&str, &str, String); 9] = [ - ("*", "font-family", format!("'{}', sans-serif", style.font_family)), - ("*", "font-size", px(style.font_size)), - ("window.breadbar", "border-radius", px(style.bar_border_radius)), - ("window.breadbar", "padding", px(style.bar_padding)), - (".workspace-btn", "opacity", style.workspace_inactive_opacity.to_string()), - (".workspace-btn", "font-size", px(style.workspace_font_size)), - (".stat-pair", "margin-right", px(style.stat_gap)), - (".tray-btn image", "-gtk-icon-size", px(style.tray_icon_size)), - ("window.breadbar-notification", "border-radius", px(style.notification_border_radius)), - ]; - - for (selector, property, value) in &edits { - if let Some(updated) = set_value(&css, selector, property, value) { - css = updated; - } - } - - config::atomic_write(&css_path(), &css).map_err(|e| e.to_string())?; - reload_breadbar() -} - -fn reload_breadbar() -> Result { - Ok(std::process::Command::new("pkill") - .args(["-HUP", "-x", "breadbar"]) - .status() - .map(|s| s.success()) - .unwrap_or(false)) -} diff --git a/src/src/commands/breadbox.rs b/src/src/commands/breadbox.rs deleted file mode 100644 index 61ecbb8..0000000 --- a/src/src/commands/breadbox.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! breadbox config.toml — launcher contexts. Schema mirrors breadbox-shared -//! (`#[serde(rename = "context")]` — the TOML key is `[[context]]`, -//! singular, despite the Rust field being `contexts`), with `name` + -//! `priority`, an ordered list of app/category hints. The context array is -//! rewritten on save; any other top-level keys/comments are preserved. - -use serde::{Deserialize, Serialize}; -use toml_edit::{value, Array, ArrayOfTables, DocumentMut, Item, Table}; - -use super::config; - -fn config_path() -> std::path::PathBuf { - config::config_dir().join("breadbox/config.toml") -} - -#[derive(Serialize, Deserialize, Clone)] -pub struct Context { - name: String, - priority: Vec, -} - -fn read_contexts(doc: &DocumentMut) -> Vec { - let Some(aot) = doc.get("context").and_then(Item::as_array_of_tables) else { - return Vec::new(); - }; - aot.iter() - .map(|t| Context { - name: t.get("name").and_then(Item::as_str).unwrap_or("").to_string(), - priority: t - .get("priority") - .and_then(Item::as_array) - .map(|a| a.iter().filter_map(|v| v.as_str().map(String::from)).collect()) - .unwrap_or_default(), - }) - .collect() -} - -fn write_contexts(doc: &mut DocumentMut, ctxs: &[Context]) { - let mut aot = ArrayOfTables::new(); - for ctx in ctxs { - let mut t = Table::new(); - t.insert("name", value(&ctx.name)); - let mut arr = Array::new(); - for p in &ctx.priority { - arr.push(p.as_str()); - } - t.insert("priority", value(arr)); - aot.push(t); - } - doc.as_table_mut().insert("context", Item::ArrayOfTables(aot)); -} - -#[tauri::command] -pub fn get_breadbox_contexts() -> Vec { - read_contexts(&config::load_doc(&config_path())) -} - -#[tauri::command] -pub fn save_breadbox_contexts(contexts: Vec) -> Result<(), String> { - let path = config_path(); - let mut doc = config::load_doc(&path); - write_contexts(&mut doc, &contexts); - config::save_doc(&path, &doc).map_err(|e| e.to_string()) -} diff --git a/src/src/commands/breadclip.rs b/src/src/commands/breadclip.rs deleted file mode 100644 index 097c116..0000000 --- a/src/src/commands/breadclip.rs +++ /dev/null @@ -1,8 +0,0 @@ -//! breadclip has no config file to edit — this panel exists purely to make -//! its background daemon (breadclipd, via `service.rs`) and its -//! on-demand popup visible/controllable from Settings. - -#[tauri::command] -pub fn open_breadclip() { - let _ = std::process::Command::new("breadclip").spawn(); -} diff --git a/src/src/commands/breadcrumbs.rs b/src/src/commands/breadcrumbs.rs deleted file mode 100644 index 7ed5db5..0000000 --- a/src/src/commands/breadcrumbs.rs +++ /dev/null @@ -1,573 +0,0 @@ -//! breadcrumbs.toml — Wi-Fi profile state machine. Schema mirrors -//! breadcrumbs/src/config.rs: -//! [settings] scalar tunables (this file) -//! [profiles.] per-location profile (networks, tailscale, …) -//! -//! Saved networks (SSID + optional local password) live in a *separate* -//! `networks.toml` (0600) next to breadcrumbs.toml. breadcrumbs v2 stores -//! them there so a file people hand-edit / dotfile does not also carry -//! plaintext Wi-Fi credentials. After the first successful connect, -//! breadcrumbs clears the local password and NetworkManager owns the -//! secret; `None` means "NM already has it" or "open network". -//! -//! `[settings]` is edited in place via toml_edit; `profiles` are rewritten -//! from their editor on save. `[[networks]]` is never written back into -//! breadcrumbs.toml — leftover inline blocks from pre-split configs are -//! read once (only if `networks.toml` is missing) and migrated on the -//! next save. Other keys/comments in breadcrumbs.toml are preserved. - -use std::path::{Path, PathBuf}; - -use serde::{Deserialize, Serialize}; -use toml_edit::{value, Array, ArrayOfTables, DocumentMut, Item, Table}; - -use super::config; - -fn settings_path() -> PathBuf { - config::config_dir().join("breadcrumbs/breadcrumbs.toml") -} - -fn networks_path() -> PathBuf { - config::config_dir().join("breadcrumbs/networks.toml") -} - -#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq)] -pub struct Network { - ssid: String, - /// Write-only from the UI's point of view. `get_breadcrumbs_config` - /// never returns a stored PSK (always `None`). On save, `None` / empty - /// means "keep whatever is already in networks.toml, or omit — NM - /// remembers." A non-empty value is written only to networks.toml. - #[serde(default, skip_serializing_if = "Option::is_none")] - password: Option, - #[serde(default)] - hidden: bool, -} - -#[derive(Serialize, Deserialize, Clone, Default)] -pub struct Profile { - name: String, - networks: Vec, - detect_ssids: Vec, - bootstrap: String, - exit_node: String, - tailscale: bool, - include_all_known: bool, -} - -#[derive(Serialize, Deserialize)] -pub struct Settings { - default_profile: String, - dns: String, - exit_node: String, - ping_host: String, - connectivity_url: String, - nmcli_wait: i64, - watch_interval: i64, -} - -#[derive(Serialize)] -pub struct BreadcrumbsConfig { - settings: Settings, - networks: Vec, - profiles: Vec, -} - -fn read_networks(doc: &DocumentMut) -> Vec { - let Some(aot) = doc.get("networks").and_then(Item::as_array_of_tables) else { - return Vec::new(); - }; - aot.iter() - .map(|t| { - let password = t - .get("password") - .and_then(Item::as_str) - .map(str::trim) - .filter(|s| !s.is_empty()) - .map(str::to_string); - Network { - ssid: t - .get("ssid") - .and_then(Item::as_str) - .unwrap_or("") - .to_string(), - password, - hidden: t.get("hidden").and_then(Item::as_bool).unwrap_or(false), - } - }) - .collect() -} - -fn networks_document(nets: &[Network]) -> DocumentMut { - let mut doc = DocumentMut::new(); - let mut aot = ArrayOfTables::new(); - for n in nets { - if n.ssid.trim().is_empty() { - continue; - } - let mut t = Table::new(); - t.insert("ssid", value(&n.ssid)); - if let Some(pw) = n - .password - .as_deref() - .map(str::trim) - .filter(|s| !s.is_empty()) - { - t.insert("password", value(pw)); - } - t.insert("hidden", value(n.hidden)); - aot.push(t); - } - doc.as_table_mut() - .insert("networks", Item::ArrayOfTables(aot)); - doc -} - -/// Load saved networks. `networks.toml` wins when present; otherwise fall -/// back to a leftover inline `[[networks]]` block in breadcrumbs.toml so a -/// pre-split config still shows up until the next save migrates it. -fn load_networks(settings_doc: &DocumentMut, net_path: &Path) -> Vec { - if net_path.exists() { - let doc = config::load_doc(net_path); - return read_networks(&doc); - } - read_networks(settings_doc) -} - -fn redact_passwords(nets: Vec) -> Vec { - nets.into_iter() - .map(|n| Network { - password: None, - ..n - }) - .collect() -} - -fn incoming_password(n: &Network) -> Option { - n.password - .as_deref() - .map(str::trim) - .filter(|s| !s.is_empty()) - .map(str::to_string) -} - -/// Empty / omitted password from the UI means "keep the on-disk secret for -/// this SSID (if any), otherwise let NetworkManager remember." A typed -/// value replaces it. Matching is by SSID; a renamed SSID is a new -/// network and does not inherit the old password. -fn merge_network_passwords(incoming: Vec, existing: &[Network]) -> Vec { - incoming - .into_iter() - .filter(|n| !n.ssid.trim().is_empty()) - .map(|mut n| { - n.password = incoming_password(&n).or_else(|| { - existing - .iter() - .find(|e| e.ssid == n.ssid) - .and_then(|e| e.password.clone()) - }); - n - }) - .collect() -} - -/// Atomic write with mode 0600 set on the temp file *before* any bytes -/// land, so a secrets file is never briefly world-readable. Also re-applies -/// 0600 on the destination in case an older world-readable networks.toml -/// was being replaced (rename keeps the new inode's mode). -fn write_secure(path: &Path, contents: &str) -> Result<(), String> { - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent) - .map_err(|e| format!("creating {}: {e}", parent.display()))?; - } - bread_utils::atomic::write_atomic(path, contents, Some(0o600)) - .map_err(|e| format!("writing {}: {e}", path.display()))?; - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600)); - } - Ok(()) -} - -fn save_networks(path: &Path, nets: &[Network]) -> Result<(), String> { - write_secure(path, &networks_document(nets).to_string()) -} - -fn read_profiles(doc: &DocumentMut) -> Vec { - let Some(tbl) = doc.get("profiles").and_then(Item::as_table) else { - return Vec::new(); - }; - let str_list = |item: Option<&Item>| -> Vec { - item.and_then(Item::as_array) - .map(|a| { - a.iter() - .filter_map(|v| v.as_str().map(String::from)) - .collect() - }) - .unwrap_or_default() - }; - tbl.iter() - .filter_map(|(name, item)| { - let p = item.as_table()?; - Some(Profile { - name: name.to_string(), - networks: str_list(p.get("networks")), - detect_ssids: str_list(p.get("detect_ssids")), - bootstrap: p - .get("bootstrap") - .and_then(Item::as_str) - .unwrap_or("") - .to_string(), - exit_node: p - .get("exit_node") - .and_then(Item::as_str) - .unwrap_or("") - .to_string(), - tailscale: p.get("tailscale").and_then(Item::as_bool).unwrap_or(false), - include_all_known: p - .get("include_all_known") - .and_then(Item::as_bool) - .unwrap_or(false), - }) - }) - .collect() -} - -fn write_profiles(doc: &mut DocumentMut, profiles: &[Profile]) { - let mut tbl = Table::new(); - let to_arr = |items: &[String]| { - let mut a = Array::new(); - for s in items { - a.push(s.as_str()); - } - a - }; - for p in profiles { - if p.name.is_empty() { - continue; - } - let mut t = Table::new(); - t.insert("networks", value(to_arr(&p.networks))); - t.insert("tailscale", value(p.tailscale)); - t.insert("include_all_known", value(p.include_all_known)); - if !p.detect_ssids.is_empty() { - t.insert("detect_ssids", value(to_arr(&p.detect_ssids))); - } - if !p.bootstrap.is_empty() { - t.insert("bootstrap", value(&p.bootstrap)); - } - if !p.exit_node.is_empty() { - t.insert("exit_node", value(&p.exit_node)); - } - tbl.insert(&p.name, Item::Table(t)); - } - doc.as_table_mut().insert("profiles", Item::Table(tbl)); -} - -fn apply_settings(doc: &mut DocumentMut, settings: &Settings) { - config::set_str( - doc, - &["settings", "default_profile"], - &settings.default_profile, - ); - config::set_str(doc, &["settings", "dns"], &settings.dns); - config::set_str_or_remove(doc, &["settings", "exit_node"], &settings.exit_node); - config::set_str(doc, &["settings", "ping_host"], &settings.ping_host); - config::set_str( - doc, - &["settings", "connectivity_url"], - &settings.connectivity_url, - ); - config::set_i64(doc, &["settings", "nmcli_wait"], settings.nmcli_wait); - config::set_i64( - doc, - &["settings", "watch_interval"], - settings.watch_interval, - ); -} - -fn load_from(settings_path: &Path, net_path: &Path) -> BreadcrumbsConfig { - let doc = config::load_doc(settings_path); - BreadcrumbsConfig { - settings: Settings { - // breadcrumbs' own default_profile_name() is "away", not "home". - default_profile: config::get_str(&doc, &["settings", "default_profile"]) - .unwrap_or_else(|| "away".into()), - dns: config::get_str(&doc, &["settings", "dns"]).unwrap_or_else(|| "1.1.1.1".into()), - exit_node: config::get_str(&doc, &["settings", "exit_node"]).unwrap_or_default(), - ping_host: config::get_str(&doc, &["settings", "ping_host"]) - .unwrap_or_else(|| "1.1.1.1".into()), - connectivity_url: config::get_str(&doc, &["settings", "connectivity_url"]) - .unwrap_or_else(|| "http://connectivitycheck.gstatic.com/generate_204".into()), - nmcli_wait: config::get_i64(&doc, &["settings", "nmcli_wait"]).unwrap_or(8), - watch_interval: config::get_i64(&doc, &["settings", "watch_interval"]).unwrap_or(12), - }, - // Never ship a stored PSK to the webview — the password field is - // write-only (empty = keep existing / let NM remember). - networks: redact_passwords(load_networks(&doc, net_path)), - profiles: read_profiles(&doc), - } -} - -fn save_to( - settings_path: &Path, - net_path: &Path, - input: SaveBreadcrumbsInput, -) -> Result<(), String> { - let mut doc = config::load_doc(settings_path); - let existing = load_networks(&doc, net_path); - apply_settings(&mut doc, &input.settings); - write_profiles(&mut doc, &input.profiles); - // Completes the pre-split migration: leftover [[networks]] must not - // survive a save, even if the user only edited settings/profiles. - doc.as_table_mut().remove("networks"); - config::save_doc(settings_path, &doc).map_err(|e| e.to_string())?; - - let merged = merge_network_passwords(input.networks, &existing); - save_networks(net_path, &merged) -} - -#[tauri::command] -pub fn get_breadcrumbs_config() -> BreadcrumbsConfig { - load_from(&settings_path(), &networks_path()) -} - -#[derive(Deserialize)] -pub struct SaveBreadcrumbsInput { - settings: Settings, - networks: Vec, - profiles: Vec, -} - -#[tauri::command] -pub fn save_breadcrumbs_config(input: SaveBreadcrumbsInput) -> Result<(), String> { - save_to(&settings_path(), &networks_path(), input) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn tmp_dir(name: &str) -> PathBuf { - let dir = std::env::temp_dir().join(format!( - "bos-settings-breadcrumbs-{}-{}-{}", - name, - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_nanos()) - .unwrap_or(0) - )); - let _ = std::fs::remove_dir_all(&dir); - std::fs::create_dir_all(&dir).unwrap(); - dir - } - - fn sample_settings() -> Settings { - Settings { - default_profile: "away".into(), - dns: "1.1.1.1".into(), - exit_node: String::new(), - ping_host: "1.1.1.1".into(), - connectivity_url: "http://connectivitycheck.gstatic.com/generate_204".into(), - nmcli_wait: 8, - watch_interval: 12, - } - } - - #[test] - fn networks_document_omits_password_when_none() { - let nets = vec![Network { - ssid: "Cafe".into(), - password: None, - hidden: false, - }]; - let text = networks_document(&nets).to_string(); - assert!(text.contains("ssid")); - assert!(!text.contains("password"), "text: {text}"); - } - - #[test] - fn networks_document_writes_password_when_present() { - let nets = vec![Network { - ssid: "Cafe".into(), - password: Some("hunter2".into()), - hidden: true, - }]; - let text = networks_document(&nets).to_string(); - assert!(text.contains("hunter2")); - assert!(text.contains("hidden = true")); - let back = read_networks(&text.parse().unwrap()); - assert_eq!(back[0].password.as_deref(), Some("hunter2")); - assert!(back[0].hidden); - } - - #[test] - fn merge_keeps_existing_password_when_incoming_empty() { - let existing = vec![Network { - ssid: "Cafe".into(), - password: Some("hunter2".into()), - hidden: false, - }]; - let incoming = vec![Network { - ssid: "Cafe".into(), - password: Some(String::new()), - hidden: true, - }]; - let merged = merge_network_passwords(incoming, &existing); - assert_eq!(merged[0].password.as_deref(), Some("hunter2")); - assert!(merged[0].hidden); - } - - #[test] - fn merge_replaces_password_when_incoming_set() { - let existing = vec![Network { - ssid: "Cafe".into(), - password: Some("old".into()), - hidden: false, - }]; - let incoming = vec![Network { - ssid: "Cafe".into(), - password: Some("new".into()), - hidden: false, - }]; - let merged = merge_network_passwords(incoming, &existing); - assert_eq!(merged[0].password.as_deref(), Some("new")); - } - - #[test] - fn get_never_returns_stored_password() { - let dir = tmp_dir("redact"); - let settings = dir.join("breadcrumbs.toml"); - let nets = dir.join("networks.toml"); - std::fs::write(&settings, "[settings]\ndns = \"9.9.9.9\"\n").unwrap(); - std::fs::write( - &nets, - "[[networks]]\nssid = \"Cafe\"\npassword = \"hunter2\"\nhidden = false\n", - ) - .unwrap(); - - let cfg = load_from(&settings, &nets); - assert_eq!(cfg.settings.dns, "9.9.9.9"); - assert_eq!(cfg.networks.len(), 1); - assert_eq!(cfg.networks[0].ssid, "Cafe"); - assert_eq!(cfg.networks[0].password, None); - - let _ = std::fs::remove_dir_all(&dir); - } - - #[test] - fn save_writes_networks_toml_not_inline_and_uses_0600() { - let dir = tmp_dir("split"); - let settings = dir.join("breadcrumbs.toml"); - let nets = dir.join("networks.toml"); - std::fs::write( - &settings, - "# keep me\n[settings]\ndns = \"1.1.1.1\"\n\n[[networks]]\nssid = \"Old\"\npassword = \"legacy\"\n", - ) - .unwrap(); - - save_to( - &settings, - &nets, - SaveBreadcrumbsInput { - settings: sample_settings(), - networks: vec![Network { - ssid: "Cafe".into(), - password: Some("hunter2".into()), - hidden: false, - }], - profiles: vec![], - }, - ) - .unwrap(); - - let settings_text = std::fs::read_to_string(&settings).unwrap(); - assert!( - settings_text.contains("# keep me"), - "toml_edit must keep comments" - ); - assert!( - !settings_text.contains("[[networks]]"), - "inline networks must be gone" - ); - assert!( - !settings_text.contains("hunter2"), - "PSK must not land in breadcrumbs.toml" - ); - assert!(!settings_text.contains("legacy")); - - let nets_text = std::fs::read_to_string(&nets).unwrap(); - assert!(nets_text.contains("Cafe")); - assert!(nets_text.contains("hunter2")); - - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let mode = std::fs::metadata(&nets).unwrap().permissions().mode() & 0o777; - assert_eq!(mode, 0o600, "networks.toml must be 0600, got {mode:o}"); - } - - let _ = std::fs::remove_dir_all(&dir); - } - - #[test] - fn save_with_empty_password_keeps_existing_secret() { - let dir = tmp_dir("keep-secret"); - let settings = dir.join("breadcrumbs.toml"); - let nets = dir.join("networks.toml"); - std::fs::write(&settings, "[settings]\ndns = \"1.1.1.1\"\n").unwrap(); - std::fs::write( - &nets, - "[[networks]]\nssid = \"Cafe\"\npassword = \"hunter2\"\nhidden = false\n", - ) - .unwrap(); - - save_to( - &settings, - &nets, - SaveBreadcrumbsInput { - settings: sample_settings(), - networks: vec![Network { - ssid: "Cafe".into(), - password: None, - hidden: true, - }], - profiles: vec![], - }, - ) - .unwrap(); - - let nets_text = std::fs::read_to_string(&nets).unwrap(); - assert!( - nets_text.contains("hunter2"), - "empty password must keep existing secret" - ); - assert!(nets_text.contains("hidden = true")); - assert!(!std::fs::read_to_string(&settings) - .unwrap() - .contains("hunter2")); - - let _ = std::fs::remove_dir_all(&dir); - } - - #[test] - fn legacy_inline_networks_load_when_networks_toml_missing() { - let dir = tmp_dir("legacy"); - let settings = dir.join("breadcrumbs.toml"); - let nets = dir.join("networks.toml"); - std::fs::write( - &settings, - "[settings]\ndns = \"8.8.8.8\"\n\n[[networks]]\nssid = \"LegacyNet\"\npassword = \"secret\"\n", - ) - .unwrap(); - - let cfg = load_from(&settings, &nets); - assert_eq!(cfg.networks.len(), 1); - assert_eq!(cfg.networks[0].ssid, "LegacyNet"); - assert_eq!(cfg.networks[0].password, None, "still redacted to the UI"); - - let _ = std::fs::remove_dir_all(&dir); - } -} diff --git a/src/src/commands/breadhelp.rs b/src/src/commands/breadhelp.rs deleted file mode 100644 index efaa85d..0000000 --- a/src/src/commands/breadhelp.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! breadhelp is the onboarding / help center. This panel launches it; it -//! does not duplicate the help library, keybind tour, or troubleshoot -//! wizard. First-run autostart (`breadhelp --autostart` in -//! `hypr/autostart.json`) is toggled through the existing autostart -//! commands from the frontend. - -#[tauri::command] -pub fn open_breadhelp() { - let _ = std::process::Command::new("breadhelp").spawn(); -} diff --git a/src/src/commands/breadlock.rs b/src/src/commands/breadlock.rs deleted file mode 100644 index 6bad05f..0000000 --- a/src/src/commands/breadlock.rs +++ /dev/null @@ -1,132 +0,0 @@ -//! Lock screen (breadlock) and greeter (breadgreet). -//! -//! Super+L is `loginctl lock-session`; hypridle's lock_cmd / idle listener -//! then starts breadlock. This panel edits `~/.config/breadlock/breadlock.toml` -//! (appearance + fail timeout) — it does not, and cannot, configure PAM. -//! breadgreet is the greetd greeter; its live config is typically -//! `/etc/greetd/breadgreet.toml` (system, owned by the greeter user) and is -//! not written from here. - -use serde::{Deserialize, Serialize}; - -use super::config; - -fn config_path() -> std::path::PathBuf { - config::config_dir().join("breadlock/breadlock.toml") -} - -const EXAMPLE_CANDIDATES: &[&str] = &[ - "/usr/share/doc/breadlock/breadlock.example.toml", - "/usr/share/breadlock/breadlock.example.toml", - "/usr/share/doc/breadlock/examples/breadlock.example.toml", -]; - -#[derive(Serialize, Deserialize)] -pub struct BreadlockConfig { - background_mode: String, - background_path: String, - background_blur: bool, - clock_format: String, - font_family: String, - fail_timeout_ms: i64, -} - -impl Default for BreadlockConfig { - fn default() -> Self { - Self { - background_mode: "color".into(), - background_path: String::new(), - background_blur: false, - clock_format: "%H:%M".into(), - font_family: "Varela Round".into(), - fail_timeout_ms: 800, - } - } -} - -#[tauri::command] -pub fn get_breadlock_config() -> BreadlockConfig { - let doc = config::load_doc(&config_path()); - let mut cfg = BreadlockConfig::default(); - if let Some(mode) = config::get_str(&doc, &["background", "mode"]) { - cfg.background_mode = mode; - } - if let Some(path) = config::get_str(&doc, &["background", "path"]) { - cfg.background_path = path; - } - if let Some(blur) = config::get_bool(&doc, &["background", "blur"]) { - cfg.background_blur = blur; - } - if let Some(fmt) = config::get_str(&doc, &["clock", "format"]) { - cfg.clock_format = fmt; - } - if let Some(family) = config::get_str(&doc, &["font", "family"]) { - cfg.font_family = family; - } - if let Some(ms) = config::get_i64(&doc, &["input", "fail_timeout_ms"]) { - cfg.fail_timeout_ms = ms; - } - cfg -} - -#[tauri::command] -pub fn save_breadlock_config(cfg: BreadlockConfig) -> Result<(), String> { - let path = config_path(); - let mut doc = config::load_doc(&path); - let mode = if cfg.background_mode == "image" { - "image" - } else { - "color" - }; - config::set_str(&mut doc, &["background", "mode"], mode); - config::set_str_or_remove(&mut doc, &["background", "path"], &cfg.background_path); - config::set_bool(&mut doc, &["background", "blur"], cfg.background_blur); - config::set_str(&mut doc, &["clock", "format"], &cfg.clock_format); - config::set_str(&mut doc, &["font", "family"], &cfg.font_family); - config::set_i64( - &mut doc, - &["input", "fail_timeout_ms"], - cfg.fail_timeout_ms.max(0), - ); - config::save_doc(&path, &doc).map_err(|e| e.to_string()) -} - -/// First existing packaged example, if any — the panel links this rather -/// than pretending the in-app editor is the whole schema. -#[tauri::command] -pub fn breadlock_example_path() -> Option { - EXAMPLE_CANDIDATES - .iter() - .find(|p| std::path::Path::new(p).is_file()) - .map(|p| p.to_string()) -} - -fn open_in_editor(path: &std::path::Path) { - let editor = std::env::var("EDITOR").unwrap_or_else(|_| "nano".to_string()); - let _ = std::process::Command::new("kitty") - .args(["-e", &editor]) - .arg(path) - .spawn(); -} - -#[tauri::command] -pub fn open_breadlock_config() { - open_in_editor(&config_path()); -} - -#[tauri::command] -pub fn open_breadlock_example() { - if let Some(path) = breadlock_example_path() { - open_in_editor(std::path::Path::new(&path)); - } -} - -/// Super+L / hypridle path: `loginctl lock-session` → compositor lock -/// protocol → breadlock. Fire-and-forget; locking the live session is the -/// point of the button. -#[tauri::command] -pub fn lock_session() { - let _ = std::process::Command::new("loginctl") - .arg("lock-session") - .spawn(); -} diff --git a/src/src/commands/breadmon.rs b/src/src/commands/breadmon.rs deleted file mode 100644 index 06ea09d..0000000 --- a/src/src/commands/breadmon.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! breadmon is a TUI for live Hyprland monitor layout, mirroring, and -//! named profiles (`~/.config/breadmon/profiles/`). Display (this app) -//! edits `hypr/monitors.json` — the login-time layout Hyprland itself -//! reads. This module only launches the TUI; it does not write profiles. - -#[tauri::command] -pub fn open_breadmon() { - let _ = std::process::Command::new("breadmon").spawn(); -} diff --git a/src/src/commands/breadpad.rs b/src/src/commands/breadpad.rs deleted file mode 100644 index 19db123..0000000 --- a/src/src/commands/breadpad.rs +++ /dev/null @@ -1,148 +0,0 @@ -//! breadpad.toml — the breadpad notes/reminders config. Schema mirrors -//! breadpad-shared/src/config.rs (settings, model + model.ollama, reminders, -//! calendar). Edited non-destructively (calendar password + model paths -//! are preserved across saves). - -use serde::{Deserialize, Serialize}; - -use super::config; - -fn config_path() -> std::path::PathBuf { - config::config_dir().join("breadpad/breadpad.toml") -} - -#[derive(Serialize, Deserialize)] -pub struct BreadpadConfig { - default_type: String, - workspace_tag: bool, - snooze_options: Vec, - archive_after_days: i64, - model_path: String, - tokenizer_path: String, - ollama_enabled: bool, - ollama_endpoint: String, - ollama_model: String, - ollama_confidence_threshold: f64, - reminders_default_morning: String, - reminders_missed_grace_minutes: i64, - calendar_enabled: bool, - calendar_url: String, - calendar_username: String, - calendar_password: String, -} - -#[tauri::command] -pub fn get_breadpad_config() -> BreadpadConfig { - let doc = config::load_doc(&config_path()); - BreadpadConfig { - default_type: config::get_str(&doc, &["settings", "default_type"]) - .unwrap_or_else(|| "note".into()), - workspace_tag: config::get_bool(&doc, &["settings", "workspace_tag"]).unwrap_or(true), - snooze_options: config::get_str_list(&doc, &["settings", "snooze_options"]), - archive_after_days: config::get_i64(&doc, &["settings", "archive_after_days"]) - .unwrap_or(30), - model_path: config::get_str(&doc, &["model", "path"]).unwrap_or_default(), - tokenizer_path: config::get_str(&doc, &["model", "tokenizer"]).unwrap_or_default(), - ollama_enabled: config::get_bool(&doc, &["model", "ollama", "enabled"]).unwrap_or(true), - ollama_endpoint: config::get_str(&doc, &["model", "ollama", "endpoint"]) - .unwrap_or_default(), - ollama_model: config::get_str(&doc, &["model", "ollama", "model"]).unwrap_or_default(), - ollama_confidence_threshold: config::get_f64( - &doc, - &["model", "ollama", "confidence_threshold"], - ) - .unwrap_or(0.6), - reminders_default_morning: config::get_str(&doc, &["reminders", "default_morning"]) - .unwrap_or_else(|| "7:00".into()), - reminders_missed_grace_minutes: config::get_i64( - &doc, - &["reminders", "missed_grace_minutes"], - ) - .unwrap_or(60), - calendar_enabled: config::get_bool(&doc, &["calendar", "enabled"]).unwrap_or(false), - calendar_url: config::get_str(&doc, &["calendar", "url"]).unwrap_or_default(), - calendar_username: config::get_str(&doc, &["calendar", "username"]).unwrap_or_default(), - // Write-only to the webview, same as restic — never round-trip the secret. - calendar_password: String::new(), - } -} - -#[tauri::command] -pub fn save_breadpad_config(cfg: BreadpadConfig) -> Result<(), String> { - let path = config_path(); - let mut doc = config::load_doc(&path); - config::set_str(&mut doc, &["settings", "default_type"], &cfg.default_type); - config::set_bool(&mut doc, &["settings", "workspace_tag"], cfg.workspace_tag); - config::set_str_list( - &mut doc, - &["settings", "snooze_options"], - &cfg.snooze_options, - ); - config::set_i64( - &mut doc, - &["settings", "archive_after_days"], - cfg.archive_after_days, - ); - config::set_str_or_remove(&mut doc, &["model", "path"], &cfg.model_path); - config::set_str_or_remove(&mut doc, &["model", "tokenizer"], &cfg.tokenizer_path); - config::set_bool( - &mut doc, - &["model", "ollama", "enabled"], - cfg.ollama_enabled, - ); - config::set_str_or_remove( - &mut doc, - &["model", "ollama", "endpoint"], - &cfg.ollama_endpoint, - ); - config::set_str_or_remove(&mut doc, &["model", "ollama", "model"], &cfg.ollama_model); - config::set_f64( - &mut doc, - &["model", "ollama", "confidence_threshold"], - cfg.ollama_confidence_threshold, - ); - config::set_str_or_remove( - &mut doc, - &["reminders", "default_morning"], - &cfg.reminders_default_morning, - ); - config::set_i64( - &mut doc, - &["reminders", "missed_grace_minutes"], - cfg.reminders_missed_grace_minutes, - ); - config::set_bool(&mut doc, &["calendar", "enabled"], cfg.calendar_enabled); - config::set_str_or_remove(&mut doc, &["calendar", "url"], &cfg.calendar_url); - config::set_str_or_remove(&mut doc, &["calendar", "username"], &cfg.calendar_username); - apply_calendar_password(&mut doc, &cfg.calendar_password); - config::save_doc(&path, &doc).map_err(|e| e.to_string()) -} - -/// Empty incoming password keeps the existing secret (PasswordField is write-only). -fn apply_calendar_password(doc: &mut toml_edit::DocumentMut, incoming: &str) { - if incoming.is_empty() { - return; - } - config::set_str(doc, &["calendar", "password"], incoming); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn empty_password_keeps_existing_secret() { - let mut doc: toml_edit::DocumentMut = - "[calendar]\npassword = \"secret\"\n".parse().unwrap(); - apply_calendar_password(&mut doc, ""); - assert_eq!( - config::get_str(&doc, &["calendar", "password"]).as_deref(), - Some("secret") - ); - apply_calendar_password(&mut doc, "newpass"); - assert_eq!( - config::get_str(&doc, &["calendar", "password"]).as_deref(), - Some("newpass") - ); - } -} diff --git a/src/src/commands/breadpaper.rs b/src/src/commands/breadpaper.rs deleted file mode 100644 index 62a48f5..0000000 --- a/src/src/commands/breadpaper.rs +++ /dev/null @@ -1,111 +0,0 @@ -//! breadpaper — wallpaper manager. No config file to edit here; breadpaper -//! takes no persistent settings, just an image path via its CLI -//! (`breadpaper set ` / `breadpaper get`). These commands are a thin -//! backend for that CLI so wallpaper (and the pywal-driven theme it -//! generates) has a discoverable home in Settings. - -use serde::Serialize; -use std::path::{Path, PathBuf}; -use tokio::process::Command; - -/// Extensions breadpaper's own `validate()` accepts. -const WALLPAPER_EXTS: &[&str] = &["png", "jpg", "jpeg", "webp", "gif", "bmp"]; - -/// Caps how many thumbnails the library ever returns, and how deep the -/// recursive scan goes (the library is organized in subfolders, e.g. by -/// show/series, so a non-recursive scan would find nothing). -const MAX_LIBRARY_ITEMS: usize = 80; -const MAX_SCAN_DEPTH: usize = 4; - -pub fn wallpaper_library_dir() -> PathBuf { - let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string()); - PathBuf::from(home).join("Pictures/Backgrounds") -} - -fn is_wallpaper_file(path: &Path) -> bool { - path.extension() - .and_then(|e| e.to_str()) - .map(|e| WALLPAPER_EXTS.iter().any(|ext| ext.eq_ignore_ascii_case(e))) - .unwrap_or(false) -} - -fn scan_wallpapers(dir: &Path) -> Vec { - fn walk(dir: &Path, depth: usize, out: &mut Vec) { - if depth == 0 || out.len() >= MAX_LIBRARY_ITEMS { - return; - } - let Ok(entries) = std::fs::read_dir(dir) else { return }; - let mut entries: Vec<_> = entries.flatten().collect(); - entries.sort_by_key(|e| e.file_name()); - for entry in entries { - if out.len() >= MAX_LIBRARY_ITEMS { - return; - } - let path = entry.path(); - if path.is_dir() { - walk(&path, depth - 1, out); - } else if is_wallpaper_file(&path) { - out.push(path); - } - } - } - let mut out = Vec::new(); - walk(dir, MAX_SCAN_DEPTH, &mut out); - out -} - -#[tauri::command] -pub async fn get_current_wallpaper() -> Option { - let out = Command::new("breadpaper").arg("get").output().await.ok()?; - if !out.status.success() { - return None; - } - let s = String::from_utf8_lossy(&out.stdout).trim().to_string(); - if s.is_empty() { - None - } else { - Some(s) - } -} - -#[tauri::command] -pub async fn set_wallpaper(path: String) -> Result<(), String> { - let ok = Command::new("breadpaper") - .arg("set") - .arg(&path) - .status() - .await - .map(|s| s.success()) - .unwrap_or(false); - if ok { - Ok(()) - } else { - Err("breadpaper failed — see terminal/journal".into()) - } -} - -#[derive(Serialize)] -pub struct LibraryEntry { - path: String, - name: String, -} - -/// Lists wallpapers under the library dir. Bounded/depth-limited (see the -/// constants above) — this is gated behind an explicit "Browse" click on -/// the frontend, not run at app launch, same "costs real time, gated -/// behind a button" posture as the network view's Wi-Fi scan. -#[tauri::command] -pub fn list_wallpaper_library() -> Vec { - scan_wallpapers(&wallpaper_library_dir()) - .into_iter() - .map(|p| LibraryEntry { - name: p.file_name().map(|f| f.to_string_lossy().to_string()).unwrap_or_default(), - path: p.to_string_lossy().to_string(), - }) - .collect() -} - -#[tauri::command] -pub fn wallpaper_library_dir_display() -> String { - wallpaper_library_dir().to_string_lossy().to_string() -} diff --git a/src/src/commands/breadsearch.rs b/src/src/commands/breadsearch.rs deleted file mode 100644 index 3b1be38..0000000 --- a/src/src/commands/breadsearch.rs +++ /dev/null @@ -1,55 +0,0 @@ -//! breadsearch/config.toml — semantic search indexer (breadmill) + GUI. -//! Schema mirrors breadsearch-shared::Config ([index], [search], [model], [power]). - -use serde::{Deserialize, Serialize}; - -use super::config; - -fn config_path() -> std::path::PathBuf { - config::config_dir().join("breadsearch/config.toml") -} - -#[derive(Serialize, Deserialize)] -pub struct BreadsearchConfig { - power_enabled: bool, - run_on_battery: bool, - backend: String, - index_roots: Vec, - index_excludes: Vec, - index_extensions: Vec, - max_file_mb: f64, - search_limit: i64, - snippet_len: i64, -} - -#[tauri::command] -pub fn get_breadsearch_config() -> BreadsearchConfig { - let doc = config::load_doc(&config_path()); - BreadsearchConfig { - power_enabled: config::get_bool(&doc, &["power", "enabled"]).unwrap_or(true), - run_on_battery: config::get_bool(&doc, &["power", "run_on_battery"]).unwrap_or(false), - backend: config::get_str(&doc, &["model", "backend"]).unwrap_or_else(|| "cpu".into()), - index_roots: config::get_str_list(&doc, &["index", "roots"]), - index_excludes: config::get_str_list(&doc, &["index", "excludes"]), - index_extensions: config::get_str_list(&doc, &["index", "extensions"]), - max_file_mb: config::get_f64(&doc, &["index", "max_file_mb"]).unwrap_or(10.0), - search_limit: config::get_i64(&doc, &["search", "limit"]).unwrap_or(10), - snippet_len: config::get_i64(&doc, &["search", "snippet_len"]).unwrap_or(200), - } -} - -#[tauri::command] -pub fn save_breadsearch_config(cfg: BreadsearchConfig) -> Result<(), String> { - let path = config_path(); - let mut doc = config::load_doc(&path); - config::set_bool(&mut doc, &["power", "enabled"], cfg.power_enabled); - config::set_bool(&mut doc, &["power", "run_on_battery"], cfg.run_on_battery); - config::set_str(&mut doc, &["model", "backend"], &cfg.backend); - config::set_str_list(&mut doc, &["index", "roots"], &cfg.index_roots); - config::set_str_list(&mut doc, &["index", "excludes"], &cfg.index_excludes); - config::set_str_list(&mut doc, &["index", "extensions"], &cfg.index_extensions); - config::set_f64(&mut doc, &["index", "max_file_mb"], cfg.max_file_mb); - config::set_i64(&mut doc, &["search", "limit"], cfg.search_limit); - config::set_i64(&mut doc, &["search", "snippet_len"], cfg.snippet_len); - config::save_doc(&path, &doc).map_err(|e| e.to_string()) -} diff --git a/src/src/commands/breadshot.rs b/src/src/commands/breadshot.rs deleted file mode 100644 index 51f75be..0000000 --- a/src/src/commands/breadshot.rs +++ /dev/null @@ -1,139 +0,0 @@ -//! Screenshots (breadshot). Binds are read-only here — edit them on the -//! Keybinds panel. Config lives at `~/.config/breadshot/config.toml` and -//! matches breadshot's own `Config` (every field optional). - -use serde::{Deserialize, Serialize}; - -use super::config; -use super::keybinds; - -fn config_path() -> std::path::PathBuf { - config::config_dir().join("breadshot/config.toml") -} - -#[derive(Serialize, Deserialize)] -pub struct BreadshotConfig { - save_dir: String, - silent: bool, - freeze: bool, - notif_timeout: i64, - date_format: String, -} - -impl Default for BreadshotConfig { - fn default() -> Self { - Self { - save_dir: "~/Pictures/Screenshots".into(), - silent: false, - freeze: false, - notif_timeout: 5000, - date_format: "%Y-%m-%d-%H%M%S".into(), - } - } -} - -#[derive(Serialize)] -pub struct ShotBind { - shortcut: String, - command: String, -} - -#[tauri::command] -pub fn get_breadshot_config() -> BreadshotConfig { - let doc = config::load_doc(&config_path()); - let mut cfg = BreadshotConfig::default(); - if let Some(dir) = config::get_str(&doc, &["save_dir"]) { - cfg.save_dir = dir; - } - if let Some(v) = config::get_bool(&doc, &["silent"]) { - cfg.silent = v; - } - if let Some(v) = config::get_bool(&doc, &["freeze"]) { - cfg.freeze = v; - } - if let Some(v) = config::get_i64(&doc, &["notif_timeout"]) { - cfg.notif_timeout = v; - } - if let Some(v) = config::get_str(&doc, &["date_format"]) { - cfg.date_format = v; - } - cfg -} - -#[tauri::command] -pub fn save_breadshot_config(cfg: BreadshotConfig) -> Result<(), String> { - let path = config_path(); - let mut doc = config::load_doc(&path); - config::set_str(&mut doc, &["save_dir"], &cfg.save_dir); - config::set_bool(&mut doc, &["silent"], cfg.silent); - config::set_bool(&mut doc, &["freeze"], cfg.freeze); - config::set_i64(&mut doc, &["notif_timeout"], cfg.notif_timeout.max(0)); - config::set_str(&mut doc, &["date_format"], &cfg.date_format); - config::save_doc(&path, &doc).map_err(|e| e.to_string()) -} - -/// Documented BOS defaults (SUPER+Shift+S/C/P) used when binds.json has no -/// breadshot exec entries — still accurate as a cheatsheet even on a -/// machine whose binds were rewritten. -fn documented_defaults() -> Vec { - vec![ - ShotBind { - shortcut: "Super+Shift+S".into(), - command: "breadshot region".into(), - }, - ShotBind { - shortcut: "Super+Shift+C".into(), - command: "breadshot region --clipboard-only".into(), - }, - ShotBind { - shortcut: "Super+Shift+P".into(), - command: "breadshot active-output".into(), - }, - ] -} - -fn format_shortcut(mods: Option<&[String]>, key: Option<&str>, default_mods: &[String]) -> String { - let mods = mods.unwrap_or(default_mods); - let mut parts: Vec = mods - .iter() - .map(|m| match m.to_ascii_uppercase().as_str() { - "SUPER" | "MOD4" => "Super".into(), - "SHIFT" => "Shift".into(), - "CTRL" | "CONTROL" => "Ctrl".into(), - "ALT" | "MOD1" => "Alt".into(), - other => other.to_string(), - }) - .collect(); - if let Some(k) = key { - if !k.is_empty() { - parts.push(k.to_string()); - } - } - parts.join("+") -} - -/// Read-only: breadshot exec binds from binds.json, or the documented -/// Super+Shift+S/C/P cheatsheet if none are defined. -#[tauri::command] -pub fn get_breadshot_binds() -> Vec { - let from_file = keybinds::breadshot_binds(); - if from_file.is_empty() { - documented_defaults() - } else { - from_file - .into_iter() - .map(|b| ShotBind { - shortcut: format_shortcut(b.mods.as_deref(), b.key.as_deref(), &b.default_mods), - command: b.command, - }) - .collect() - } -} - -/// Interactive region capture, clipboard only — no file written. -#[tauri::command] -pub fn breadshot_region_clipboard() { - let _ = std::process::Command::new("breadshot") - .args(["region", "--clipboard-only"]) - .spawn(); -} diff --git a/src/src/commands/channel.rs b/src/src/commands/channel.rs deleted file mode 100644 index bc33a17..0000000 --- a/src/src/commands/channel.rs +++ /dev/null @@ -1,91 +0,0 @@ -//! Bakery track (stable / beta / dev). Preference only — `bakery update -//! --all` afterwards actually installs the new track's builds. - -use serde::Serialize; -use tokio::process::Command; - -use super::util::{fail_output, strip_ansi}; - -const TRACKS: &[&str] = &["stable", "beta", "dev"]; - -#[derive(Serialize)] -pub struct BakeryTrack { - current: String, - tracks: Vec, -} - -fn parse_track_show(text: &str) -> String { - let text = strip_ansi(text); - for line in text.lines() { - let line = line.trim(); - let lower = line.to_ascii_lowercase(); - if let Some(rest) = lower.strip_prefix("current track:") { - let raw = line[line.len() - rest.len()..].trim(); - return raw.to_ascii_lowercase(); - } - if TRACKS.contains(&line) { - return line.to_string(); - } - } - let lower = text.to_ascii_lowercase(); - for track in TRACKS { - if lower.contains(track) { - return (*track).to_string(); - } - } - "stable".into() -} - -#[tauri::command] -pub async fn get_bakery_track() -> Result { - let output = Command::new("bakery") - .args(["track", "show"]) - .output() - .await - .map_err(|e| format!("couldn't run bakery: {e}"))?; - if !output.status.success() { - return Err(fail_output(&output, "bakery track show")); - } - let text = String::from_utf8_lossy(&output.stdout); - Ok(BakeryTrack { - current: parse_track_show(&text), - tracks: TRACKS.iter().map(|s| (*s).to_string()).collect(), - }) -} - -#[tauri::command] -pub async fn set_bakery_track(track: String) -> Result { - let track = track.trim().to_ascii_lowercase(); - if !TRACKS.contains(&track.as_str()) { - return Err(format!( - "unknown track '{track}' — expected stable, beta, or dev" - )); - } - let output = Command::new("bakery") - .args(["track", "set", &track]) - .output() - .await - .map_err(|e| e.to_string())?; - if !output.status.success() { - return Err(fail_output(&output, "bakery track set")); - } - get_bakery_track().await -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parses_current_track_line() { - assert_eq!(parse_track_show("current track: dev\n"), "dev"); - assert_eq!(parse_track_show("current track: stable"), "stable"); - assert_eq!(parse_track_show("beta"), "beta"); - } - - #[test] - fn rejects_unknown_in_set_guard() { - assert!(!TRACKS.contains(&"nightly")); - assert!(TRACKS.contains(&"stable")); - } -} diff --git a/src/src/commands/datetime.rs b/src/src/commands/datetime.rs deleted file mode 100644 index 5413cc2..0000000 --- a/src/src/commands/datetime.rs +++ /dev/null @@ -1,131 +0,0 @@ -//! Timezone + NTP, over `timedatectl`. `systemd-timesyncd` is enabled by -//! default, so NTP sync is on out of the box — this is mostly for picking a -//! timezone and confirming sync is healthy. - -use serde::Serialize; -use tokio::process::Command; - -async fn show_property(prop: &str) -> String { - Command::new("timedatectl") - .args(["show", &format!("--property={prop}")]) - .output() - .await - .ok() - .and_then(|o| { - String::from_utf8_lossy(&o.stdout) - .trim() - .strip_prefix(&format!("{prop}=")) - .map(str::to_string) - }) - .unwrap_or_default() -} - -async fn list_timezones() -> Vec { - Command::new("timedatectl") - .arg("list-timezones") - .output() - .await - .ok() - .map(|o| { - String::from_utf8_lossy(&o.stdout) - .lines() - .map(str::to_string) - .collect() - }) - .unwrap_or_default() -} - -async fn current_time_label() -> String { - Command::new("date") - .arg("+%A, %d %B %Y %H:%M") - .output() - .await - .ok() - .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) - .unwrap_or_default() -} - -#[derive(Serialize)] -pub struct DateTimeInfo { - current_time: String, - timezones: Vec, - current_tz: String, - ntp_enabled: bool, - ntp_synced: bool, -} - -#[tauri::command] -pub async fn get_datetime_info() -> DateTimeInfo { - DateTimeInfo { - current_time: current_time_label().await, - timezones: list_timezones().await, - current_tz: show_property("Timezone").await, - ntp_enabled: show_property("NTP").await == "yes", - ntp_synced: show_property("NTPSynchronized").await == "yes", - } -} - -/// Reject flags, path traversal, and newlines before we ever exec. Charset -/// matches IANA names (`Area/City`, `UTC`, `Etc/GMT+6`). -fn timezone_looks_safe(tz: &str) -> bool { - let tz = tz.trim(); - if tz.is_empty() || tz.len() > 64 || tz.starts_with('-') { - return false; - } - if tz.contains('\n') || tz.contains('\r') || tz.contains('\0') || tz.contains("..") { - return false; - } - tz.chars() - .all(|c| c.is_ascii_alphanumeric() || matches!(c, '/' | '_' | '+' | '-')) -} - -#[tauri::command] -pub async fn set_timezone(tz: String) -> Result<(), String> { - let tz = tz.trim(); - if !timezone_looks_safe(tz) { - return Err("invalid timezone".into()); - } - let listed = list_timezones().await; - if !listed.is_empty() && !listed.iter().any(|t| t == tz) { - return Err("unknown timezone".into()); - } - let output = Command::new("pkexec") - .args(["timedatectl", "set-timezone", tz]) - .output() - .await - .map_err(|e| e.to_string())?; - if output.status.success() { - Ok(()) - } else { - Err("Error — check the timezone name".into()) - } -} - -#[tauri::command] -pub async fn set_ntp_enabled(enabled: bool) -> Result<(), String> { - let val = if enabled { "true" } else { "false" }; - Command::new("pkexec") - .args(["timedatectl", "set-ntp", val]) - .status() - .await - .map_err(|e| e.to_string())?; - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn timezone_rejects_flags_and_traversal() { - assert!(timezone_looks_safe("UTC")); - assert!(timezone_looks_safe("America/New_York")); - assert!(timezone_looks_safe("Etc/GMT+6")); - assert!(!timezone_looks_safe("")); - assert!(!timezone_looks_safe("-UTC")); - assert!(!timezone_looks_safe("--help")); - assert!(!timezone_looks_safe("America/../UTC")); - assert!(!timezone_looks_safe("UTC\n--adjust")); - assert!(!timezone_looks_safe("UTC;reboot")); - } -} diff --git a/src/src/commands/defaults.rs b/src/src/commands/defaults.rs deleted file mode 100644 index b6386a4..0000000 --- a/src/src/commands/defaults.rs +++ /dev/null @@ -1,431 +0,0 @@ -//! Default applications via `~/.config/mimeapps.list`. Categories cover -//! the associations BOS already ships in skel (browser, files, images, -//! PDF, editor) plus a terminal entry. - -use std::collections::{BTreeMap, HashMap}; -use std::path::PathBuf; - -use serde::Serialize; - -use super::config; - -const CATEGORIES: &[(&str, &[&str])] = &[ - ( - "browser", - &[ - "x-scheme-handler/http", - "x-scheme-handler/https", - "text/html", - ], - ), - ("files", &["inode/directory"]), - ("terminal", &["x-scheme-handler/terminal"]), - ( - "image", - &[ - "image/png", - "image/jpeg", - "image/webp", - "image/gif", - "image/svg+xml", - ], - ), - ("pdf", &["application/pdf"]), - ("editor", &["text/plain", "text/markdown"]), -]; - -#[derive(Serialize, Clone)] -pub struct DesktopApp { - id: String, - name: String, -} - -#[derive(Serialize)] -pub struct DefaultsStatus { - path: String, - current: HashMap, - options: HashMap>, -} - -fn mimeapps_path() -> PathBuf { - config::config_dir().join("mimeapps.list") -} - -fn xdg_terminals_path() -> PathBuf { - config::config_dir().join("xdg-terminals.list") -} - -fn applications_dirs() -> Vec { - let mut dirs = vec![ - PathBuf::from("/usr/share/applications"), - PathBuf::from("/usr/local/share/applications"), - ]; - if let Ok(home) = std::env::var("HOME") { - dirs.push(PathBuf::from(home).join(".local/share/applications")); - } - dirs -} - -#[derive(Clone)] -struct DesktopMeta { - id: String, - name: String, - mimes: Vec, - terminal: bool, -} - -fn parse_desktop(id: &str, text: &str) -> Option { - let mut in_entry = false; - let mut name = String::new(); - let mut mimes = Vec::new(); - let mut terminal = false; - let mut hidden = false; - for line in text.lines() { - let line = line.trim(); - if line.starts_with('[') { - in_entry = line.eq_ignore_ascii_case("[Desktop Entry]"); - continue; - } - if !in_entry { - continue; - } - if let Some(v) = line.strip_prefix("Name=") { - if name.is_empty() { - name = v.to_string(); - } - } else if let Some(v) = line.strip_prefix("MimeType=") { - mimes = v - .split(';') - .map(str::trim) - .filter(|s| !s.is_empty()) - .map(str::to_string) - .collect(); - } else if let Some(v) = line.strip_prefix("Categories=") { - terminal |= v.split(';').any(|c| c.trim() == "TerminalEmulator"); - } else if line == "Hidden=true" || line == "NoDisplay=true" { - hidden = true; - } - } - if hidden || name.is_empty() { - return None; - } - Some(DesktopMeta { - id: id.to_string(), - name, - mimes, - terminal, - }) -} - -fn scan_desktops() -> Vec { - let mut out = Vec::new(); - let mut seen = std::collections::HashSet::new(); - for dir in applications_dirs() { - let Ok(entries) = std::fs::read_dir(dir) else { - continue; - }; - for entry in entries.flatten() { - let path = entry.path(); - if path.extension().and_then(|e| e.to_str()) != Some("desktop") { - continue; - } - let Some(id) = path.file_name().and_then(|n| n.to_str()) else { - continue; - }; - if !seen.insert(id.to_string()) { - continue; - } - let Ok(text) = std::fs::read_to_string(&path) else { - continue; - }; - if let Some(meta) = parse_desktop(id, &text) { - out.push(meta); - } - } - } - out.sort_by_key(|a| a.name.to_lowercase()); - out -} - -fn parse_default_applications(text: &str) -> BTreeMap { - let mut map = BTreeMap::new(); - let mut in_defaults = false; - for line in text.lines() { - let t = line.trim(); - if t.starts_with('[') { - in_defaults = t.eq_ignore_ascii_case("[Default Applications]"); - continue; - } - if !in_defaults || t.is_empty() || t.starts_with('#') { - continue; - } - if let Some((k, v)) = t.split_once('=') { - let desktop = v.split(';').next().unwrap_or("").trim(); - if !desktop.is_empty() { - map.insert(k.trim().to_string(), desktop.to_string()); - } - } - } - map -} - -fn current_for_category(defaults: &BTreeMap, mimes: &[&str]) -> String { - for mime in mimes { - if let Some(v) = defaults.get(*mime) { - return v.clone(); - } - } - String::new() -} - -fn options_for( - apps: &[DesktopMeta], - category: &str, - mimes: &[&str], - current: &str, -) -> Vec { - let mut out = Vec::new(); - let mut seen = std::collections::HashSet::new(); - for app in apps { - let matches = if category == "terminal" { - app.terminal || app.mimes.iter().any(|m| mimes.contains(&m.as_str())) - } else { - app.mimes.iter().any(|m| mimes.contains(&m.as_str())) - }; - if matches && seen.insert(app.id.clone()) { - out.push(DesktopApp { - id: app.id.clone(), - name: app.name.clone(), - }); - } - } - if !current.is_empty() && !seen.contains(current) { - out.insert( - 0, - DesktopApp { - id: current.to_string(), - name: current.trim_end_matches(".desktop").to_string(), - }, - ); - } - out -} - -#[tauri::command] -pub fn get_default_apps() -> DefaultsStatus { - let path = mimeapps_path(); - let text = std::fs::read_to_string(&path).unwrap_or_default(); - let defaults = parse_default_applications(&text); - let apps = scan_desktops(); - let mut current = HashMap::new(); - let mut options = HashMap::new(); - for (cat, mimes) in CATEGORIES { - let cur = if *cat == "terminal" { - read_terminal_default(&defaults) - } else { - current_for_category(&defaults, mimes) - }; - options.insert((*cat).to_string(), options_for(&apps, cat, mimes, &cur)); - current.insert((*cat).to_string(), cur); - } - DefaultsStatus { - path: path.display().to_string(), - current, - options, - } -} - -fn read_terminal_default(defaults: &BTreeMap) -> String { - if let Ok(text) = std::fs::read_to_string(xdg_terminals_path()) { - if let Some(id) = text - .lines() - .map(str::trim) - .find(|l| !l.is_empty() && !l.starts_with('#')) - { - return id.to_string(); - } - } - current_for_category(defaults, &["x-scheme-handler/terminal"]) -} - -#[derive(serde::Deserialize)] -pub struct SaveDefaultsInput { - current: HashMap, -} - -#[tauri::command] -pub fn save_default_apps(input: SaveDefaultsInput) -> Result<(), String> { - let path = mimeapps_path(); - let existing = std::fs::read_to_string(&path).unwrap_or_default(); - let mut replacements = BTreeMap::new(); - for (cat, mimes) in CATEGORIES { - let Some(desktop) = input.current.get(*cat).map(|s| s.trim()) else { - continue; - }; - if desktop.is_empty() { - continue; - } - if !valid_desktop_id(desktop) { - return Err(format!("invalid desktop id '{desktop}'")); - } - for mime in *mimes { - replacements.insert((*mime).to_string(), desktop.to_string()); - } - if *cat == "terminal" { - write_terminal_list(desktop)?; - } - } - let text = upsert_defaults(&existing, &replacements); - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent).map_err(|e| e.to_string())?; - } - config::atomic_write(&path, &text).map_err(|e| e.to_string()) -} - -fn write_terminal_list(desktop: &str) -> Result<(), String> { - let path = xdg_terminals_path(); - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent).map_err(|e| e.to_string())?; - } - config::atomic_write(&path, &format!("{desktop}\n")).map_err(|e| e.to_string()) -} - -fn valid_desktop_id(id: &str) -> bool { - let bytes = id.as_bytes(); - bytes.ends_with(b".desktop") - && bytes.len() > ".desktop".len() - && bytes.len() <= 128 - && bytes - .iter() - .all(|b| b.is_ascii_alphanumeric() || matches!(*b, b'-' | b'_' | b'.' | b'+')) -} - -fn upsert_defaults(existing: &str, replacements: &BTreeMap) -> String { - if existing.trim().is_empty() { - let mut out = String::from("[Default Applications]\n"); - for (mime, desktop) in replacements { - out.push_str(&format!("{mime}={desktop}\n")); - } - return out; - } - let mut out = String::new(); - let mut in_defaults = false; - let mut seen = std::collections::HashSet::new(); - let mut wrote_header = false; - for line in existing.lines() { - let t = line.trim(); - if t.starts_with('[') { - if in_defaults { - for (mime, desktop) in replacements { - if seen.insert(mime.clone()) { - out.push_str(&format!("{mime}={desktop}\n")); - } - } - } - in_defaults = t.eq_ignore_ascii_case("[Default Applications]"); - if in_defaults { - wrote_header = true; - } - out.push_str(line); - out.push('\n'); - continue; - } - if in_defaults { - if let Some((k, _)) = t.split_once('=') { - let key = k.trim(); - if let Some(desktop) = replacements.get(key) { - out.push_str(&format!("{key}={desktop}\n")); - seen.insert(key.to_string()); - continue; - } - } - } - out.push_str(line); - out.push('\n'); - } - if in_defaults { - for (mime, desktop) in replacements { - if seen.insert(mime.clone()) { - out.push_str(&format!("{mime}={desktop}\n")); - } - } - } else if !wrote_header { - if !out.ends_with('\n') && !out.is_empty() { - out.push('\n'); - } - out.push_str("\n[Default Applications]\n"); - for (mime, desktop) in replacements { - out.push_str(&format!("{mime}={desktop}\n")); - } - } - out -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parse_skel_defaults() { - let text = "\ -[Default Applications] -text/html=zen.desktop -x-scheme-handler/http=zen.desktop -inode/directory=org.gnome.Nautilus.desktop -"; - let map = parse_default_applications(text); - assert_eq!(map.get("text/html").unwrap(), "zen.desktop"); - assert_eq!( - current_for_category(&map, &["x-scheme-handler/http", "text/html"]), - "zen.desktop" - ); - } - - #[test] - fn upsert_replaces_only_named_keys() { - let existing = "\ -# keep -[Default Applications] -text/html=old.desktop -image/png=org.gnome.Loupe.desktop - -[Added Associations] -text/html=extra.desktop; -"; - let mut rep = BTreeMap::new(); - rep.insert("text/html".into(), "zen.desktop".into()); - rep.insert("x-scheme-handler/http".into(), "zen.desktop".into()); - let out = upsert_defaults(existing, &rep); - assert!(out.contains("# keep")); - assert!(out.contains("text/html=zen.desktop")); - assert!(out.contains("x-scheme-handler/http=zen.desktop")); - assert!(out.contains("image/png=org.gnome.Loupe.desktop")); - assert!(out.contains("[Added Associations]")); - assert!(out.contains("text/html=extra.desktop;")); - assert_eq!(out.matches("text/html=zen.desktop").count(), 1); - } - - #[test] - fn desktop_id_check() { - assert!(valid_desktop_id("zen.desktop")); - assert!(valid_desktop_id("org.gnome.Nautilus.desktop")); - assert!(!valid_desktop_id("zen")); - assert!(!valid_desktop_id("../evil.desktop")); - } - - #[test] - fn parse_desktop_skips_hidden() { - let hidden = parse_desktop( - "x.desktop", - "[Desktop Entry]\nName=X\nNoDisplay=true\nMimeType=text/plain;\n", - ); - assert!(hidden.is_none()); - let ok = parse_desktop( - "ed.desktop", - "[Desktop Entry]\nName=Editor\nMimeType=text/plain;\nCategories=Utility;\n", - ) - .unwrap(); - assert_eq!(ok.name, "Editor"); - assert!(ok.mimes.contains(&"text/plain".into())); - } -} diff --git a/src/src/commands/firewall.rs b/src/src/commands/firewall.rs deleted file mode 100644 index bfc6471..0000000 --- a/src/src/commands/firewall.rs +++ /dev/null @@ -1,184 +0,0 @@ -//! ufw firewall rules. `ufw status` itself requires root (confirmed against -//! the installed ufw script), so unlike every other read-only panel, this -//! doesn't query eagerly — the frontend only calls `get_firewall_status` -//! when the user clicks Refresh, deferring the one unavoidable polkit -//! prompt to an explicit action instead of forcing it on app open. - -use serde::Serialize; -use tokio::process::Command; - -#[derive(Serialize, Clone)] -pub struct FirewallRule { - number: String, - text: String, -} - -#[derive(Serialize)] -pub struct FirewallStatus { - active: bool, - rules: Vec, -} - -/// One `pkexec ufw status numbered` call, parsed for both the active/ -/// inactive line and the numbered rules. -#[tauri::command] -pub async fn get_firewall_status() -> Result { - let output = Command::new("pkexec") - .args(["ufw", "status", "numbered"]) - .output() - .await - .map_err(|e| format!("couldn't run pkexec: {e}"))?; - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); - return Err(if stderr.is_empty() { - match output.status.code() { - Some(127) => { - "no polkit authentication agent is available in this session".to_string() - } - Some(code) => format!("pkexec exited with status {code}"), - None => "pkexec was terminated by a signal".to_string(), - } - } else { - stderr - }); - } - let text = String::from_utf8_lossy(&output.stdout); - let active = text - .lines() - .next() - .is_some_and(|l| l.trim() == "Status: active"); - let rules = text - .lines() - .filter_map(|l| { - let l = l.trim_start(); - if !l.starts_with('[') { - return None; - } - let (num, rest) = l.split_once(']')?; - let number = num.trim_start_matches('[').trim().to_string(); - Some(FirewallRule { - number, - text: rest.trim().to_string(), - }) - }) - .collect(); - Ok(FirewallStatus { active, rules }) -} - -#[tauri::command] -pub async fn set_firewall_enabled(enabled: bool) -> Result<(), String> { - let verb = if enabled { "enable" } else { "disable" }; - let output = Command::new("pkexec") - .args(["ufw", "--force", verb]) - .output() - .await - .map_err(|e| e.to_string())?; - if output.status.success() { - Ok(()) - } else { - Err(String::from_utf8_lossy(&output.stderr).trim().to_string()) - } -} - -/// Port, optional `/tcp`/`/udp`, or optional space-separated proto. No -/// service names, IPs, or flags — those become extra `ufw allow` operands. -fn valid_firewall_rule(rule: &str) -> bool { - let rule = rule.trim(); - if rule.is_empty() || rule.len() > 16 || rule.starts_with('-') { - return false; - } - if rule.contains('\n') || rule.contains('\r') || rule.contains('\0') { - return false; - } - let (port, proto) = if let Some((p, rest)) = rule.split_once('/') { - (p, Some(rest)) - } else if let Some((p, rest)) = rule.split_once(' ') { - (p, Some(rest.trim())) - } else { - (rule, None) - }; - let Ok(n) = port.parse::() else { - return false; - }; - if n == 0 { - return false; - } - match proto { - None => true, - Some(p) => p == "tcp" || p == "udp", - } -} - -fn valid_rule_number(number: &str) -> bool { - let t = number.trim(); - !t.is_empty() - && t.len() <= 8 - && t.bytes().all(|b| b.is_ascii_digit()) - && t.parse::().is_ok_and(|n| n > 0) -} - -#[tauri::command] -pub async fn add_firewall_rule(rule: String) -> Result<(), String> { - let rule = rule.trim(); - if !valid_firewall_rule(rule) { - return Err("invalid firewall rule".into()); - } - let output = Command::new("pkexec") - .args(["ufw", "allow", rule]) - .output() - .await - .map_err(|e| e.to_string())?; - if output.status.success() { - Ok(()) - } else { - Err(String::from_utf8_lossy(&output.stderr).trim().to_string()) - } -} - -#[tauri::command] -pub async fn remove_firewall_rule(number: String) -> Result<(), String> { - if !valid_rule_number(&number) { - return Err("invalid rule number".into()); - } - let output = Command::new("pkexec") - .args(["ufw", "--force", "delete", number.trim()]) - .output() - .await - .map_err(|e| e.to_string())?; - if output.status.success() { - Ok(()) - } else { - Err(String::from_utf8_lossy(&output.stderr).trim().to_string()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn firewall_rule_is_port_and_optional_proto() { - assert!(valid_firewall_rule("22")); - assert!(valid_firewall_rule("8080/tcp")); - assert!(valid_firewall_rule("53/udp")); - assert!(valid_firewall_rule("80 tcp")); - assert!(!valid_firewall_rule("OpenSSH")); - assert!(!valid_firewall_rule("-f")); - assert!(!valid_firewall_rule("22;id")); - assert!(!valid_firewall_rule("22/tcp\nallow 23")); - assert!(!valid_firewall_rule("0")); - assert!(!valid_firewall_rule("65536")); - assert!(!valid_firewall_rule("22/all")); - } - - #[test] - fn firewall_delete_is_positive_int() { - assert!(valid_rule_number("1")); - assert!(valid_rule_number("12")); - assert!(!valid_rule_number("0")); - assert!(!valid_rule_number("-1")); - assert!(!valid_rule_number("1;2")); - assert!(!valid_rule_number("1\n2")); - assert!(!valid_rule_number("")); - } -} diff --git a/src/src/commands/firmware.rs b/src/src/commands/firmware.rs deleted file mode 100644 index f3a5c87..0000000 --- a/src/src/commands/firmware.rs +++ /dev/null @@ -1,32 +0,0 @@ -use serde::Serialize; - -#[derive(Serialize, Clone)] -pub struct FwDevice { - name: String, - version: String, -} - -#[tauri::command] -pub async fn get_updatable_firmware() -> Vec { - let Ok(output) = tokio::process::Command::new("fwupdmgr").args(["get-devices", "--json"]).output().await else { - return Vec::new(); - }; - let Ok(root) = serde_json::from_slice::(&output.stdout) else { - return Vec::new(); - }; - let Some(devices) = root.get("Devices").and_then(|d| d.as_array()) else { - return Vec::new(); - }; - devices - .iter() - .filter(|d| { - d.get("Flags").and_then(|f| f.as_array()).is_some_and(|flags| flags.iter().any(|f| f.as_str() == Some("updatable"))) - }) - .filter_map(|d| { - Some(FwDevice { - name: d.get("Name")?.as_str()?.to_string(), - version: d.get("Version").and_then(|v| v.as_str()).unwrap_or("unknown").to_string(), - }) - }) - .collect() -} diff --git a/src/src/commands/hyprland.rs b/src/src/commands/hyprland.rs deleted file mode 100644 index c882479..0000000 --- a/src/src/commands/hyprland.rs +++ /dev/null @@ -1,218 +0,0 @@ -//! Display: live-connected-monitor readout (from `hyprctl monitors -j`) plus -//! an editor for `hypr/monitors.json` — the monitor *layout* Hyprland itself -//! reads at login. Like appearance.rs/autostart.rs, a plain typed struct -//! round-tripped whole (JSON has no comments to preserve). -//! -//! `Default` here (the single wildcard rule) must stay in sync with -//! `scripts/display/monitors.lua`'s own `DEFAULT_MONITORS` fallback. - -use serde::{Deserialize, Serialize}; - -use super::config; - -#[derive(Clone, Serialize, Deserialize)] -pub struct MonitorRule { - output: String, - #[serde(default = "default_mode")] - mode: String, - #[serde(default = "default_position")] - position: String, - #[serde(default = "default_scale")] - scale: String, -} - -fn default_mode() -> String { - "preferred".to_string() -} -fn default_position() -> String { - "auto".to_string() -} -fn default_scale() -> String { - "auto".to_string() -} - -impl Default for MonitorRule { - fn default() -> Self { - Self { output: String::new(), mode: default_mode(), position: default_position(), scale: default_scale() } - } -} - -#[derive(Serialize, Deserialize)] -struct MonitorsFile { - #[serde(default)] - monitors: Vec, -} - -fn config_path() -> std::path::PathBuf { - config::config_dir().join("hypr/monitors.json") -} - -fn hypr_path(name: &str) -> std::path::PathBuf { - config::config_dir().join("hypr").join(name) -} - -#[derive(Serialize, Deserialize, Clone)] -pub struct LiveMonitor { - pub name: String, - pub mode: String, - pub x: i32, - pub y: i32, - pub width: u32, - pub height: u32, - pub refresh: f64, - pub scale: f64, - pub transform: u32, - pub available_modes: Vec, -} - -fn json_i32(v: &serde_json::Value, key: &str) -> Option { - v.get(key)?.as_i64().map(|n| n as i32).or_else(|| v.get(key)?.as_f64().map(|n| n.round() as i32)) -} - -fn json_u32(v: &serde_json::Value, key: &str) -> Option { - v.get(key)?.as_u64().map(|n| n as u32).or_else(|| v.get(key)?.as_f64().map(|n| n.round() as u32)) -} - -fn json_f64(v: &serde_json::Value, key: &str) -> Option { - v.get(key)?.as_f64().or_else(|| v.get(key)?.as_u64().map(|n| n as f64)) -} - -#[tauri::command] -pub fn get_live_monitors() -> Vec { - let Some(value) = bread_utils::proc::run_json("hyprctl", &["monitors", "-j"], std::time::Duration::from_secs(3)) - else { - return Vec::new(); - }; - let Ok(monitors) = serde_json::from_value::>(value) else { - return Vec::new(); - }; - monitors - .iter() - .filter_map(|m| { - let name = m.get("name")?.as_str()?.to_string(); - let width = json_u32(m, "width")?; - let height = json_u32(m, "height")?; - let refresh = json_f64(m, "refreshRate").unwrap_or(60.0); - let x = json_i32(m, "x").unwrap_or(0); - let y = json_i32(m, "y").unwrap_or(0); - let scale = json_f64(m, "scale").unwrap_or(1.0).max(0.1); - let transform = json_u32(m, "transform").unwrap_or(0); - let available_modes = m - .get("availableModes") - .and_then(|v| v.as_array()) - .map(|arr| { - arr.iter() - .filter_map(|x| x.as_str().map(str::to_string)) - .collect::>() - }) - .unwrap_or_default(); - Some(LiveMonitor { - name, - mode: format!("{width}x{height} @ {refresh:.0}Hz"), - x, - y, - width, - height, - refresh, - scale, - transform, - available_modes, - }) - }) - .collect() -} - -fn write_monitor_rules(rules: &[MonitorRule]) -> Result<(), String> { - let file = MonitorsFile { monitors: rules.to_vec() }; - let json = serde_json::to_string_pretty(&file).map_err(|e| e.to_string())?; - config::atomic_write(&config_path(), &json).map_err(|e| e.to_string()) -} - -fn lua_ident(name: &str) -> Result<(), String> { - if name.is_empty() - || !name - .chars() - .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.') - { - return Err(format!("bad monitor name '{name}'")); - } - Ok(()) -} - -/// Live-apply a layout via BOS Hyprland `hl.monitor()`, then persist monitors.json. -#[tauri::command] -pub fn apply_monitor_layout(monitors: Vec) -> Result<(), String> { - if monitors.is_empty() { - return Err("no monitors".into()); - } - let mut stmts = Vec::new(); - let mut rules = Vec::new(); - for m in &monitors { - lua_ident(&m.name)?; - let refresh = if m.refresh > 1.0 { m.refresh.round() as u32 } else { 60 }; - let mode = format!("{}x{}@{refresh}", m.width.max(1), m.height.max(1)); - let position = format!("{}x{}", m.x, m.y); - let scale = if (m.scale - 1.0).abs() < 0.001 { - "1".to_string() - } else { - format!("{:.2}", m.scale) - }; - let transform = m.transform.min(7); - stmts.push(format!( - "hl.monitor({{ output = \"{}\", mode = \"{mode}\", position = \"{position}\", scale = \"{scale}\", transform = {transform}, vrr = false }})", - m.name - )); - rules.push(MonitorRule { - output: m.name.clone(), - mode, - position, - scale, - }); - } - let lua = stmts.join("; "); - let output = std::process::Command::new("hyprctl") - .args(["eval", &lua]) - .output() - .map_err(|e| e.to_string())?; - let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); - if stdout != "ok" { - let low = stdout.to_lowercase(); - if low.contains("unknown request") || low.contains("unknown command") || stdout.is_empty() { - return Err("Live layout needs BOS Hyprland (hyprctl eval / hl.monitor).".into()); - } - return Err(format!("hyprctl: {stdout}")); - } - write_monitor_rules(&rules)?; - Ok(()) -} - -#[tauri::command] -pub fn get_monitor_rules() -> Vec { - std::fs::read_to_string(config_path()) - .ok() - .and_then(|s| serde_json::from_str::(&s).ok()) - .filter(|f| !f.monitors.is_empty()) - .map(|f| f.monitors) - .unwrap_or_else(|| vec![MonitorRule::default()]) -} - -#[tauri::command] -pub fn save_monitor_rules(rules: Vec) -> Result<(), String> { - write_monitor_rules(&rules)?; - super::util::hypr_reload(); - Ok(()) -} - -/// Opens `hyprland.lua` in `$EDITOR` (nano if unset) inside a terminal — -/// spawning a TUI editor with no terminal to attach to is a silent no-op. -#[tauri::command] -pub fn open_hyprland_conf() { - let editor = std::env::var("EDITOR").unwrap_or_else(|_| "nano".to_string()); - let path = hypr_path("hyprland.lua"); - let _ = std::process::Command::new("kitty").args(["-e", &editor]).arg(path).spawn(); -} - -#[tauri::command] -pub fn open_keybinds_viewer() { - let _ = std::process::Command::new("breadhelp").spawn(); -} diff --git a/src/src/commands/ime.rs b/src/src/commands/ime.rs deleted file mode 100644 index d0c40e5..0000000 --- a/src/src/commands/ime.rs +++ /dev/null @@ -1,177 +0,0 @@ -//! fcitx5 input method for this session: environment.d + Hyprland env + -//! systemd --user / `fcitx5 -d`. Missing packages are offered via the -//! allowlisted pacman installer, not installed on page load. - -use serde::Serialize; -use tokio::process::Command; - -use super::config; -use super::util::{self, command_exists, pacman_installed}; - -const FRAGMENT: &str = "fcitx5.conf"; -const ENV_FILE: &str = "90-fcitx5.conf"; - -const ENV_LINES_SYSTEMD: &str = "\ -GTK_IM_MODULE=fcitx -QT_IM_MODULE=fcitx -XMODIFIERS=@im=fcitx -SDL_IM_MODULE=fcitx -"; - -const ENV_LINES_HYPR: &str = "\ -env = GTK_IM_MODULE,fcitx -env = QT_IM_MODULE,fcitx -env = XMODIFIERS,@im=fcitx -env = SDL_IM_MODULE,fcitx -exec-once = fcitx5 -d -"; - -#[derive(Serialize, Clone)] -pub struct ImePackage { - name: String, - installed: bool, -} - -#[derive(Serialize)] -pub struct ImeStatus { - enabled: bool, - running: bool, - packages: Vec, - error: Option, -} - -fn env_path() -> std::path::PathBuf { - config::config_dir().join("environment.d").join(ENV_FILE) -} - -fn wanted_packages() -> &'static [&'static str] { - &[ - "fcitx5", - "fcitx5-gtk", - "fcitx5-qt", - "fcitx5-configtool", - "fcitx5-chinese-addons", - ] -} - -fn packages_status() -> Vec { - wanted_packages() - .iter() - .map(|name| ImePackage { - name: (*name).to_string(), - installed: pacman_installed(name), - }) - .collect() -} - -fn env_file_present() -> bool { - env_path().is_file() -} - -async fn fcitx_running() -> bool { - Command::new("pgrep") - .args(["-x", "fcitx5"]) - .status() - .await - .map(|s| s.success()) - .unwrap_or(false) -} - -#[tauri::command] -pub async fn get_ime_status() -> ImeStatus { - ImeStatus { - enabled: env_file_present(), - running: fcitx_running().await, - packages: packages_status(), - error: None, - } -} - -#[tauri::command] -pub async fn set_ime_enabled(enabled: bool) -> Result { - if enabled { - enable_ime().await?; - } else { - disable_ime().await?; - } - Ok(ImeStatus { - enabled: env_file_present(), - running: fcitx_running().await, - packages: packages_status(), - error: None, - }) -} - -async fn enable_ime() -> Result<(), String> { - if !command_exists("fcitx5") { - return Err("fcitx5 is not installed".into()); - } - let env = env_path(); - if let Some(parent) = env.parent() { - std::fs::create_dir_all(parent).map_err(|e| e.to_string())?; - } - config::atomic_write(&env, ENV_LINES_SYSTEMD).map_err(|e| e.to_string())?; - - let hypr = util::hypr_dir().join(FRAGMENT); - std::fs::create_dir_all(util::hypr_dir()).map_err(|e| e.to_string())?; - config::atomic_write(&hypr, ENV_LINES_HYPR).map_err(|e| e.to_string())?; - util::ensure_hypr_source(FRAGMENT)?; - - let _ = Command::new("systemctl") - .args([ - "--user", - "import-environment", - "GTK_IM_MODULE", - "QT_IM_MODULE", - "XMODIFIERS", - "SDL_IM_MODULE", - ]) - .status() - .await; - - let enabled_unit = Command::new("systemctl") - .args(["--user", "enable", "--now", "fcitx5.service"]) - .status() - .await - .map(|s| s.success()) - .unwrap_or(false); - if !enabled_unit && !fcitx_running().await { - std::process::Command::new("fcitx5") - .arg("-d") - .stdin(std::process::Stdio::null()) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .spawn() - .map_err(|e| format!("couldn't start fcitx5: {e}"))?; - } - Ok(()) -} - -async fn disable_ime() -> Result<(), String> { - let _ = std::fs::remove_file(env_path()); - let _ = std::fs::remove_file(util::hypr_dir().join(FRAGMENT)); - util::remove_hypr_source(FRAGMENT)?; - let _ = Command::new("systemctl") - .args(["--user", "disable", "--now", "fcitx5.service"]) - .status() - .await; - let _ = Command::new("pkill").args(["-x", "fcitx5"]).status().await; - Ok(()) -} - -#[tauri::command] -pub fn open_fcitx_config() { - let _ = std::process::Command::new("fcitx5-configtool").spawn(); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn env_files_use_fcitx_module_name() { - assert!(ENV_LINES_SYSTEMD.contains("GTK_IM_MODULE=fcitx")); - assert!(ENV_LINES_HYPR.contains("XMODIFIERS,@im=fcitx")); - assert!(ENV_LINES_HYPR.contains("exec-once = fcitx5 -d")); - } -} diff --git a/src/src/commands/keybinds.rs b/src/src/commands/keybinds.rs deleted file mode 100644 index 6175259..0000000 --- a/src/src/commands/keybinds.rs +++ /dev/null @@ -1,427 +0,0 @@ -//! hypr/binds.json — Hyprland keybind editor, read by -//! `scripts/ui/binds.lua` on the Hyprland side (see hyprland.lua). -//! -//! This file has TWO real on-disk shapes, and which one applies depends on -//! the machine: -//! -//! - **Flat** (`default_mods` + a single `bindings` array) — what BOS itself -//! ships (`iso/airootfs/etc/skel/.config/hypr/binds.json`, read by the -//! BOS-shipped `scripts/input/binds.lua`). No layouts. Each bind carries -//! `label`/`category`/`demo_cmd` fields breadhelp depends on for its -//! cheatsheet and guided tour. -//! - **MultiLayout** (`globals`/`common`/one `layouts` entry per keyboard -//! layout) — a personal, per-machine schema some dev setups use instead, -//! read by a different, personal `binds.lua`. -//! -//! `SchemaKind` detects which shape is actually on disk (from the top-level -//! key set) and `save()` always emits that SAME shape back — see -//! `SchemaKind::detect` and `save_to`. Loading a real BOS (Flat) file under -//! the wrong assumption and saving it back would silently drop the -//! `bindings` key entirely — still valid JSON, so the Lua `pcall` -//! failsafes on the reading side would never catch it. -//! -//! Each bind's shape also varies by `action` (`exec` needs `command`, -//! `move_dir` needs `direction`, workspace-focus needs `workspace`, mouse -//! binds need `options.mouse`, ...). Rather than modelling every action's -//! field set as its own row layout — which would mean a combinatorial -//! explosion of widgets and silently dropping any action shape this editor -//! doesn't already know about — `action`/`key`/`mods` get real fields (the -//! ones every bind has) and everything else round-trips through -//! `#[serde(flatten)]` into a small inline-JSON column, same trade-off the -//! other Hyprland JSON editors (appearance.rs, hyprland.rs, autostart.rs) -//! already make: no comments to preserve, so this is a whole-file round -//! trip, not the `toml_edit`/`Doc` path-based pattern. - -use std::collections::BTreeMap; -use std::path::Path; - -use serde::{Deserialize, Serialize}; -use serde_json::{Map, Value}; - -use super::config; - -/// Which on-disk shape `binds.json` was loaded as. Detected once at load -/// time from the top-level key set present in the JSON, then pinned for the -/// lifetime of the editor session (round-tripped to the frontend and back -/// on save) so `save()` always writes back the same shape it read, -/// regardless of what the in-memory model happens to have populated. -#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum SchemaKind { - /// `{ "default_mods": [...], "bindings": [...] }` — BOS's real shipped - /// shape. No layout-switching UI applies; there's nothing to switch. - Flat, - /// `{ "active_layout", "default_mods", "globals", "common", "layouts" }` - /// — the personal, multi-keyboard-layout schema this editor was - /// originally built against. - MultiLayout, - /// Neither key set matched — an empty file, a totally different shape, - /// or unparsable JSON. Loading still renders (empty), but `save()` - /// refuses outright rather than guessing a shape and risking silently - /// destroying whatever the real file's actual schema was. - Unknown, -} - -impl SchemaKind { - fn detect(top_level: &Map) -> Self { - if top_level.contains_key("bindings") { - SchemaKind::Flat - } else if top_level.contains_key("globals") - || top_level.contains_key("common") - || top_level.contains_key("layouts") - { - SchemaKind::MultiLayout - } else { - SchemaKind::Unknown - } - } -} - -#[derive(Clone, Serialize, Deserialize, Default)] -#[serde(default)] -pub struct Bind { - action: String, - #[serde(skip_serializing_if = "Option::is_none")] - key: Option, - /// `None` (key omitted) means "fall back to `default_mods`"; `Some(_)` - /// — including `Some(vec![])` — means "use exactly this, even if that's - /// no modifiers at all." Real BOS binds rely on that distinction (e.g. - /// media keys pin `"mods": []` on purpose so they never inherit - /// `default_mods`), so this can't collapse both cases to "omit the - /// key" the way a bare `Vec` with `skip_serializing_if` would — - /// that would silently turn an explicit "no mods" into "use the - /// default" the next time this editor saves the file. - #[serde(skip_serializing_if = "Option::is_none")] - mods: Option>, - /// Everything else a bind can carry — `command`, `direction`, - /// `workspace`, `x`, `y`, `layout`, `options`, `label`, `category`, - /// `demo_cmd`, and any action shape not yet invented. Edited on the - /// frontend as compact inline JSON. This flatten is what keeps - /// breadhelp's `label`/`category`/`demo_cmd` fields — which this - /// editor's UI has no dedicated widgets for — alive across a full - /// load/save round trip instead of being silently dropped. - #[serde(flatten)] - extra: Map, -} - -#[derive(Serialize, Deserialize, Default)] -#[serde(default)] -pub struct BindsFile { - #[serde(skip_serializing_if = "String::is_empty")] - active_layout: String, - #[serde(skip_serializing_if = "Vec::is_empty")] - default_mods: Vec, - #[serde(skip_serializing_if = "Vec::is_empty")] - globals: Vec, - #[serde(skip_serializing_if = "Vec::is_empty")] - common: Vec, - /// A `BTreeMap` (alphabetical), not the file's original insertion order - /// — same "whole-file round trip, formatting not preserved" trade-off as - /// the rest of this file's JSON-config siblings. - #[serde(skip_serializing_if = "BTreeMap::is_empty")] - layouts: BTreeMap>, - /// Flat-schema bind list — BOS's real shipped shape. Only ever populated - /// when `SchemaKind::Flat` was detected at load time; stays empty (and - /// so omitted, see `to_json`) for a MultiLayout file. - #[serde(skip_serializing_if = "Vec::is_empty")] - bindings: Vec, -} - -/// What the frontend fetches once at load: which shape was detected, plus -/// the data itself. Sent back verbatim to `save_keybinds` so a stray code -/// path on either side can't accidentally save `file` without knowing which -/// shape it's supposed to come back out as. -#[derive(Serialize)] -pub struct BindsPayload { - kind: SchemaKind, - file: BindsFile, -} - -fn config_path() -> std::path::PathBuf { - config::config_dir().join("hypr/binds.json") -} - -fn load_from(path: &Path) -> (BindsFile, SchemaKind) { - let Ok(text) = std::fs::read_to_string(path) else { - // No file yet (fresh install/environment) — nothing on disk to - // misdetect or destroy. BOS itself ships the flat schema, so a new - // file defaults to Flat rather than the personal MultiLayout schema - // this editor originally assumed. - return (BindsFile::default(), SchemaKind::Flat); - }; - let kind = match serde_json::from_str::(&text) { - Ok(Value::Object(top_level)) => SchemaKind::detect(&top_level), - // Unparsable JSON, or valid JSON that isn't even an object — treat - // as Unknown so save() refuses rather than silently overwriting - // whatever this file actually was with an empty default. - _ => SchemaKind::Unknown, - }; - let file: BindsFile = serde_json::from_str(&text).unwrap_or_default(); - (file, kind) -} - -fn load() -> (BindsFile, SchemaKind) { - load_from(&config_path()) -} - -/// Serialize `f` in exactly the shape `kind` implies: -/// - `Flat` -> `{ "default_mods": [...], "bindings": [...] }`, nothing else -/// — no `active_layout`/`globals`/`common`/`layouts` keys, even if the -/// struct happens to carry empty values for them. -/// - `MultiLayout` -> today's existing shape (whatever fields are -/// non-empty), via `BindsFile`'s own `Serialize` impl. -fn to_json(f: &BindsFile, kind: SchemaKind) -> Value { - match kind { - SchemaKind::Flat => serde_json::json!({ - "default_mods": f.default_mods, - "bindings": f.bindings, - }), - SchemaKind::MultiLayout => serde_json::to_value(f).unwrap_or(Value::Null), - SchemaKind::Unknown => Value::Null, - } -} - -fn save_to(path: &Path, f: &BindsFile, kind: SchemaKind) -> std::io::Result<()> { - if kind == SchemaKind::Unknown { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "binds.json's schema wasn't recognized (expected a \"bindings\" key, or one of \ - \"globals\"/\"common\"/\"layouts\") — refusing to save so nothing gets silently \ - overwritten. Fix or remove the file, then reopen this panel.", - )); - } - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent)?; - } - let text = serde_json::to_string_pretty(&to_json(f, kind)).unwrap_or_default(); - config::atomic_write(path, &text) -} - -fn save(f: &BindsFile, kind: SchemaKind) -> std::io::Result<()> { - save_to(&config_path(), f, kind) -} - -/// One breadshot `exec` bind as binds.json stored it. Used by the -/// Screenshots panel (read-only); editing still happens here. -pub(crate) struct ShotBindRaw { - pub mods: Option>, - pub key: Option, - pub command: String, - pub default_mods: Vec, -} - -pub(crate) fn breadshot_binds() -> Vec { - let (file, _kind) = load(); - let default_mods = file.default_mods.clone(); - let mut out = Vec::new(); - let mut push = |binds: &[Bind]| { - for b in binds { - if b.action != "exec" { - continue; - } - let Some(cmd) = b.extra.get("command").and_then(|v| v.as_str()) else { - continue; - }; - if !cmd - .split_whitespace() - .next() - .is_some_and(|bin| bin == "breadshot" || bin.ends_with("/breadshot")) - { - continue; - } - out.push(ShotBindRaw { - mods: b.mods.clone(), - key: b.key.clone(), - command: cmd.to_string(), - default_mods: default_mods.clone(), - }); - } - }; - push(&file.bindings); - push(&file.globals); - push(&file.common); - for binds in file.layouts.values() { - push(binds); - } - out -} - -#[tauri::command] -pub fn get_keybinds() -> BindsPayload { - let (file, kind) = load(); - BindsPayload { kind, file } -} - -#[tauri::command] -pub fn save_keybinds(file: BindsFile, kind: SchemaKind) -> Result<(), String> { - save(&file, kind).map_err(|e| e.to_string())?; - super::util::hypr_reload(); - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - /// A representative slice of BOS's real shipped `binds.json` - /// (`iso/airootfs/etc/skel/.config/hypr/binds.json`, flat schema) — - /// chosen to exercise the extra-field variety breadhelp reads (`label`, - /// `category`, `demo_cmd`), an explicit `mods: []` override, a nested - /// `options` object, and both integer and string `workspace` values. - /// This is the fixture that would have caught the original GTK-editor - /// bug: mis-detecting this shape as MultiLayout and silently dropping - /// the whole `bindings` array on save. - const REAL_BOS_FLAT_FIXTURE: &str = r#"{ - "default_mods": ["SUPER"], - "bindings": [ - { "action": "exec", "command": "kitty", "key": "RETURN", "label": "Open a terminal", "category": "apps" }, - { "action": "close", "key": "BACKSPACE", "label": "Close the focused window", "category": "windows" }, - { "action": "exec", "command": "breadbox", "key": "SPACE", "label": "Open the app launcher (breadbox)", "category": "apps", "demo_cmd": "breadbox" }, - { "action": "exec", "command": "wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+", "key": "XF86AudioRaiseVolume", "mods": [], "options": { "locked": true, "repeating": true }, "label": "Volume up", "category": "media" }, - { "action": "focus", "workspace": 1, "key": "1", "label": "Switch to workspace 1", "category": "workspaces" }, - { "action": "focus", "workspace": "e+1", "key": "bracketright", "label": "Next workspace", "category": "workspaces" }, - { "action": "resize_dir", "x": 30, "y": 0, "key": "right", "mods": ["SUPER", "SHIFT"], "options": { "repeating": true }, "label": "Resize the focused window (grow right)", "category": "focus" }, - { "action": "drag", "key": "mouse:272", "options": { "mouse": true }, "label": "Move a window (drag)", "category": "mouse" } - ] -}"#; - - fn parse(text: &str) -> (BindsFile, SchemaKind) { - let kind = match serde_json::from_str::(text) { - Ok(Value::Object(top)) => SchemaKind::detect(&top), - _ => SchemaKind::Unknown, - }; - let file: BindsFile = serde_json::from_str(text).unwrap_or_default(); - (file, kind) - } - - #[test] - fn detects_flat_schema_from_real_bos_binds_json() { - let (_, kind) = parse(REAL_BOS_FLAT_FIXTURE); - assert_eq!(kind, SchemaKind::Flat); - } - - #[test] - fn round_trips_real_bos_flat_binds_json_through_load_and_save() { - let (file, kind) = parse(REAL_BOS_FLAT_FIXTURE); - assert_eq!(kind, SchemaKind::Flat); - - let original: Value = serde_json::from_str(REAL_BOS_FLAT_FIXTURE).unwrap(); - let saved = to_json(&file, kind); - - // Flat save must emit EXACTLY {default_mods, bindings} — no - // active_layout/globals/common/layouts keys leaking in. - let saved_obj = saved.as_object().expect("flat save must be a JSON object"); - assert_eq!( - saved_obj - .keys() - .cloned() - .collect::>(), - ["default_mods", "bindings"] - .into_iter() - .map(String::from) - .collect(), - "Flat schema must round-trip as exactly {{default_mods, bindings}}" - ); - - // The `bindings` array — and every per-bind extra field (label, - // category, demo_cmd, mods, options, integer vs string workspace, - // ...) — must survive the round trip semantically untouched. - assert_eq!(saved["bindings"], original["bindings"]); - assert_eq!(saved["default_mods"], original["default_mods"]); - } - - #[test] - fn round_trip_via_files_preserves_bindings_key_and_extras() { - let dir = - std::env::temp_dir().join(format!("bos-settings-keybinds-test-{}", std::process::id())); - std::fs::create_dir_all(&dir).unwrap(); - let path = dir.join("binds.json"); - std::fs::write(&path, REAL_BOS_FLAT_FIXTURE).unwrap(); - - let (file, kind) = load_from(&path); - assert_eq!(kind, SchemaKind::Flat); - save_to(&path, &file, kind).unwrap(); - - let saved_text = std::fs::read_to_string(&path).unwrap(); - let saved: Value = serde_json::from_str(&saved_text).unwrap(); - let original: Value = serde_json::from_str(REAL_BOS_FLAT_FIXTURE).unwrap(); - - assert!( - saved.get("bindings").is_some(), - "bindings key must survive a load -> save round trip" - ); - assert_eq!(saved["bindings"], original["bindings"]); - assert_eq!(saved["default_mods"], original["default_mods"]); - - // Backup safety net: a second save must leave `.bak` holding the - // prior contents. - save_to(&path, &file, kind).unwrap(); - let backup_path = dir.join("binds.json.bak"); - assert!(backup_path.exists(), "save must back up the previous file"); - let backup: Value = - serde_json::from_str(&std::fs::read_to_string(&backup_path).unwrap()).unwrap(); - assert_eq!(backup["bindings"], original["bindings"]); - - let _ = std::fs::remove_dir_all(&dir); - } - - #[test] - fn detects_and_round_trips_multi_layout_schema() { - let text = r#"{ - "active_layout": "qwerty", - "default_mods": ["SUPER"], - "globals": [{ "action": "exec", "command": "kitty", "key": "RETURN" }], - "common": [], - "layouts": { "qwerty": [{ "action": "close", "key": "BACKSPACE" }] } - }"#; - let (file, kind) = parse(text); - assert_eq!(kind, SchemaKind::MultiLayout); - - let saved = to_json(&file, kind); - assert!( - saved.get("bindings").is_none(), - "MultiLayout save must not emit a flat `bindings` key" - ); - assert_eq!(saved["active_layout"], "qwerty"); - assert_eq!(saved["layouts"]["qwerty"][0]["action"], "close"); - assert_eq!(saved["globals"][0]["command"], "kitty"); - } - - #[test] - fn unknown_schema_is_detected_and_refuses_to_save() { - let text = r#"{ "some_other_shape": true }"#; - let (file, kind) = parse(text); - assert_eq!(kind, SchemaKind::Unknown); - - let dir = std::env::temp_dir().join(format!( - "bos-settings-keybinds-unknown-test-{}", - std::process::id() - )); - std::fs::create_dir_all(&dir).unwrap(); - let path = dir.join("binds.json"); - - let result = save_to(&path, &file, kind); - assert!( - result.is_err(), - "save() must refuse when schema kind is Unknown" - ); - assert!( - !path.exists(), - "refusing to save must not create/touch the target file" - ); - - let _ = std::fs::remove_dir_all(&dir); - } - - #[test] - fn missing_file_defaults_to_flat_not_multi_layout() { - let dir = std::env::temp_dir().join(format!( - "bos-settings-keybinds-missing-test-{}", - std::process::id() - )); - // Don't create the file at all. - let path = dir.join("binds.json"); - let (_, kind) = load_from(&path); - assert_eq!(kind, SchemaKind::Flat); - } -} diff --git a/src/src/commands/network.rs b/src/src/commands/network.rs deleted file mode 100644 index 3f76413..0000000 --- a/src/src/commands/network.rs +++ /dev/null @@ -1,122 +0,0 @@ -//! Wi-Fi + Ethernet over `nmcli`. NetworkManager lets the active session -//! user manage connections via polkit already, so the common paths (scan, -//! connect, toggle radio) need no `pkexec`. VPN import, 802.1x, and other -//! edge cases are punted to `nm-connection-editor` via the Advanced button. - -use serde::Serialize; -use std::collections::{HashMap, HashSet}; -use tokio::process::Command; - -use super::util; - -#[derive(Serialize, Clone)] -pub struct WifiNetwork { - ssid: String, - signal: i32, - secured: bool, - active: bool, - known: bool, -} - -async fn radio_enabled() -> bool { - Command::new("nmcli") - .args(["radio", "wifi"]) - .output() - .await - .ok() - .map(|o| String::from_utf8_lossy(&o.stdout).trim() == "enabled") - .unwrap_or(false) -} - -async fn ethernet_status() -> Option { - let out = Command::new("nmcli").args(["-t", "-f", "DEVICE,TYPE,STATE"]).arg("dev").output().await.ok()?; - let text = String::from_utf8_lossy(&out.stdout); - text.lines().find_map(|l| { - let mut cols = l.splitn(3, ':'); - let (dev, ty, state) = (cols.next()?, cols.next()?, cols.next()?); - (ty == "ethernet").then(|| format!("{dev}: {state}")) - }) -} - -async fn known_connection_names() -> HashSet { - let Ok(out) = Command::new("nmcli").args(["-t", "-f", "NAME"]).arg("con").arg("show").output().await else { - return HashSet::new(); - }; - String::from_utf8_lossy(&out.stdout).lines().map(str::to_string).collect() -} - -#[derive(Serialize)] -pub struct NetworkInfo { - radio_enabled: bool, - ethernet: Option, -} - -#[tauri::command] -pub async fn get_network_info() -> NetworkInfo { - NetworkInfo { radio_enabled: radio_enabled().await, ethernet: ethernet_status().await } -} - -#[tauri::command] -pub async fn set_wifi_radio(enabled: bool) -> Result<(), String> { - let val = if enabled { "on" } else { "off" }; - Command::new("nmcli").args(["radio", "wifi", val]).status().await.map_err(|e| e.to_string())?; - Ok(()) -} - -/// Scan + list Wi-Fi networks, deduplicated by SSID (keeping the strongest -/// signal — the same AP shows once per band/BSSID otherwise). -#[tauri::command] -pub async fn scan_wifi() -> Vec { - let _ = Command::new("nmcli").args(["dev", "wifi", "rescan"]).output().await; - tokio::time::sleep(std::time::Duration::from_secs(2)).await; - let Ok(out) = Command::new("nmcli").args(["-t", "-f", "SSID,SIGNAL,SECURITY,IN-USE", "dev", "wifi", "list"]).output().await - else { - return Vec::new(); - }; - let known = known_connection_names().await; - let text = String::from_utf8_lossy(&out.stdout); - let mut by_ssid: HashMap = HashMap::new(); - for line in text.lines() { - let mut cols = line.splitn(4, ':'); - let (ssid, signal, security, in_use) = (cols.next(), cols.next(), cols.next(), cols.next()); - let Some(ssid) = ssid.filter(|s| !s.is_empty()) else { continue }; - let signal: i32 = signal.and_then(|s| s.parse().ok()).unwrap_or(0); - let net = WifiNetwork { - ssid: ssid.to_string(), - signal, - secured: security.map(|s| !s.is_empty()).unwrap_or(false), - active: in_use == Some("*"), - known: known.contains(ssid), - }; - by_ssid.entry(ssid.to_string()).and_modify(|existing| if net.signal > existing.signal { *existing = net.clone() }).or_insert(net); - } - let mut list: Vec<_> = by_ssid.into_values().collect(); - list.sort_by_key(|n| std::cmp::Reverse(n.signal)); - list -} - -#[tauri::command] -pub async fn connect_wifi(ssid: String, password: Option) -> Result<(), String> { - if !util::valid_nm_id(&ssid) { - return Err(format!("invalid SSID '{ssid}'")); - } - let known = known_connection_names().await; - let output = if let Some(password) = password { - Command::new("nmcli").args(["dev", "wifi", "connect", &ssid, "password", &password]).output().await - } else if known.contains(&ssid) { - Command::new("nmcli").args(["con", "up", &ssid]).output().await - } else { - Command::new("nmcli").args(["dev", "wifi", "connect", &ssid]).output().await - } - .map_err(|e| e.to_string())?; - if output.status.success() { - Ok(()) - } else { - Err(String::from_utf8_lossy(&output.stderr).trim().to_string()) - } -} - -#[tauri::command] -pub fn open_connection_editor() { - let _ = std::process::Command::new("nm-connection-editor").spawn(); -} diff --git a/src/src/commands/nightlight.rs b/src/src/commands/nightlight.rs deleted file mode 100644 index dc36bf5..0000000 --- a/src/src/commands/nightlight.rs +++ /dev/null @@ -1,216 +0,0 @@ -//! Night light via hyprsunset (Hyprland twilight IPC). The compositor -//! talks to a hyprsunset daemon socket; if the binary is missing we offer -//! a pacman install rather than pretending the toggle works. - -use serde::{Deserialize, Serialize}; -use tokio::process::Command; - -use super::config; -use super::util::{self, command_exists, fail_output}; - -const FRAGMENT: &str = "nightlight.conf"; -const DEFAULT_TEMP: u32 = 3500; -const MIN_TEMP: u32 = 2000; -const MAX_TEMP: u32 = 6500; - -#[derive(Serialize, Deserialize, Clone)] -pub struct NightlightConfig { - enabled: bool, - temperature: u32, -} - -impl Default for NightlightConfig { - fn default() -> Self { - Self { - enabled: false, - temperature: DEFAULT_TEMP, - } - } -} - -#[derive(Serialize)] -pub struct NightlightStatus { - installed: bool, - running: bool, - enabled: bool, - temperature: u32, - error: Option, -} - -fn persist_path() -> std::path::PathBuf { - util::bos_settings_dir().join("nightlight.toml") -} - -fn load_persist() -> NightlightConfig { - let Ok(text) = std::fs::read_to_string(persist_path()) else { - return NightlightConfig::default(); - }; - let doc = text.parse::().unwrap_or_default(); - NightlightConfig { - enabled: config::get_bool(&doc, &["enabled"]).unwrap_or(false), - temperature: config::get_i64(&doc, &["temperature"]) - .unwrap_or(DEFAULT_TEMP as i64) - .clamp(MIN_TEMP as i64, MAX_TEMP as i64) as u32, - } -} - -fn save_persist(cfg: &NightlightConfig) -> Result<(), String> { - let mut doc = toml_edit::DocumentMut::new(); - config::set_bool(&mut doc, &["enabled"], cfg.enabled); - config::set_i64(&mut doc, &["temperature"], cfg.temperature as i64); - let path = persist_path(); - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent).map_err(|e| e.to_string())?; - } - config::atomic_write(&path, &doc.to_string()).map_err(|e| e.to_string()) -} - -fn clamp_temp(t: u32) -> u32 { - t.clamp(MIN_TEMP, MAX_TEMP) -} - -async fn hyprsunset_running() -> bool { - Command::new("hyprctl") - .args(["hyprsunset", "gamma", "1.0"]) - .output() - .await - .map(|o| o.status.success()) - .unwrap_or(false) -} - -async fn start_daemon() -> Result<(), String> { - if hyprsunset_running().await { - return Ok(()); - } - if !command_exists("hyprsunset") { - return Err("hyprsunset is not installed".into()); - } - std::process::Command::new("hyprsunset") - .stdin(std::process::Stdio::null()) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .spawn() - .map_err(|e| format!("couldn't start hyprsunset: {e}"))?; - for _ in 0..15 { - tokio::time::sleep(std::time::Duration::from_millis(150)).await; - if hyprsunset_running().await { - return Ok(()); - } - } - Err("hyprsunset started but Hyprland twilight socket never came up".into()) -} - -async fn apply_temperature(temp: u32) -> Result<(), String> { - start_daemon().await?; - let t = clamp_temp(temp).to_string(); - let output = Command::new("hyprctl") - .args(["hyprsunset", "temperature", &t]) - .output() - .await - .map_err(|e| e.to_string())?; - if output.status.success() { - Ok(()) - } else { - Err(fail_output(&output, "hyprctl hyprsunset")) - } -} - -async fn apply_identity() -> Result<(), String> { - if !hyprsunset_running().await { - return Ok(()); - } - let output = Command::new("hyprctl") - .args(["hyprsunset", "identity"]) - .output() - .await - .map_err(|e| e.to_string())?; - if output.status.success() { - Ok(()) - } else { - Err(fail_output(&output, "hyprctl hyprsunset")) - } -} - -fn write_autostart() -> Result<(), String> { - let path = util::hypr_dir().join(FRAGMENT); - std::fs::create_dir_all(util::hypr_dir()).map_err(|e| e.to_string())?; - config::atomic_write(&path, "exec-once = hyprsunset\n").map_err(|e| e.to_string())?; - util::ensure_hypr_source(FRAGMENT) -} - -fn clear_autostart() -> Result<(), String> { - let path = util::hypr_dir().join(FRAGMENT); - let _ = std::fs::remove_file(path); - util::remove_hypr_source(FRAGMENT) -} - -#[tauri::command] -pub async fn get_nightlight() -> NightlightStatus { - let persist = load_persist(); - let installed = command_exists("hyprsunset"); - let running = if installed { - hyprsunset_running().await - } else { - false - }; - NightlightStatus { - installed, - running, - enabled: persist.enabled && running, - temperature: persist.temperature, - error: None, - } -} - -#[tauri::command] -pub async fn set_nightlight(enabled: bool, temperature: u32) -> Result { - if !command_exists("hyprsunset") { - return Ok(NightlightStatus { - installed: false, - running: false, - enabled: false, - temperature: clamp_temp(temperature), - error: Some("hyprsunset is not installed".into()), - }); - } - let mut cfg = NightlightConfig { - enabled, - temperature: clamp_temp(temperature), - }; - let mut error = None; - if enabled { - if let Err(e) = apply_temperature(cfg.temperature).await { - error = Some(e); - cfg.enabled = false; - } else if let Err(e) = write_autostart() { - error = Some(e); - } - } else { - if let Err(e) = apply_identity().await { - error = Some(e); - } - if let Err(e) = clear_autostart() { - error = Some(error.unwrap_or(e)); - } - } - save_persist(&cfg)?; - Ok(NightlightStatus { - installed: true, - running: hyprsunset_running().await, - enabled: cfg.enabled, - temperature: cfg.temperature, - error, - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn temp_clamps() { - assert_eq!(clamp_temp(100), MIN_TEMP); - assert_eq!(clamp_temp(9000), MAX_TEMP); - assert_eq!(clamp_temp(3500), 3500); - } -} diff --git a/src/src/commands/nvidia.rs b/src/src/commands/nvidia.rs deleted file mode 100644 index 37490ac..0000000 --- a/src/src/commands/nvidia.rs +++ /dev/null @@ -1,260 +0,0 @@ -//! NVIDIA driver offer. BOS writes a probe file when it sees a discrete -//! NVIDIA GPU; Settings only shows the card if that file exists and does -//! not install anything until the user clicks. - -use serde::Serialize; -use std::path::{Path, PathBuf}; -use tauri::AppHandle; - -use super::streaming; -use super::util::{self, command_exists, pacman_installed}; - -/// Same drop-in bos-nvidia-setup writes. hyprland.lua dofiles it only -/// when the file exists (Mesa machines have no file). -/// Hyprland 0.56 (Aquamarine): wiki requires LIBVA + GLX vendor. -/// NVD_BACKEND is the current VA-API hint. No WLR_* / GBM_BACKEND. -const NVIDIA_LUA: &str = "\ --- Written by bos-nvidia-setup. hyprland.lua dofiles this only when it exists. --- Hyprland 0.56 (Aquamarine) — no WLR_* variables. --- https://wiki.hypr.land/Nvidia/ -hl.env(\"LIBVA_DRIVER_NAME\", \"nvidia\") -hl.env(\"__GLX_VENDOR_LIBRARY_NAME\", \"nvidia\") -hl.env(\"NVD_BACKEND\", \"direct\") -"; - -const HYPR_INCLUDE: &str = "\ --- bos-nvidia-setup: optional proprietary env; no-op when the file is absent -do - local nvidia = (os.getenv(\"HOME\") or \"\") .. \"/.config/hypr/nvidia.lua\" - local f = io.open(nvidia, \"r\") - if f then - f:close() - pcall(dofile, nvidia) - end -end -"; - -const NVIDIA_PACKAGES: &[&str] = &["nvidia", "nvidia-utils"]; - -#[derive(Serialize, Clone)] -pub struct NvidiaOffer { - gpu: String, - reason: String, - packages: Vec, - installed: bool, -} - -fn offer_paths() -> Vec { - let home = std::env::var("HOME").unwrap_or_else(|_| "/root".into()); - let state = Path::new(&home).join(".local/state/bos"); - vec![ - state.join("nvidia-offer.json"), - state.join("nvidia-probe.json"), - ] -} - -fn nvidia_dropin_path() -> PathBuf { - util::hypr_dir().join("nvidia.lua") -} - -fn nvidia_ready() -> bool { - nvidia_dropin_path().is_file() && NVIDIA_PACKAGES.iter().all(|p| pacman_installed(p)) -} - -pub fn read_nvidia_offer() -> Option { - for path in offer_paths() { - if !path.is_file() { - continue; - } - let Ok(text) = std::fs::read_to_string(&path) else { - return Some(generic_offer()); - }; - if let Ok(v) = serde_json::from_str::(&text) { - if v.get("offer").and_then(|x| x.as_bool()) == Some(false) - || v.get("dismissed").and_then(|x| x.as_bool()) == Some(true) - { - return None; - } - let gpu = v - .get("gpu") - .or_else(|| v.get("name")) - .or_else(|| v.get("device")) - .and_then(|x| x.as_str()) - .unwrap_or("NVIDIA GPU") - .to_string(); - let reason = v - .get("reason") - .or_else(|| v.get("message")) - .and_then(|x| x.as_str()) - .unwrap_or("A discrete NVIDIA GPU was detected. The proprietary driver is not installed until you choose it.") - .to_string(); - let packages = v - .get("packages") - .and_then(|x| x.as_array()) - .map(|arr| { - arr.iter() - .filter_map(|x| x.as_str().map(str::to_string)) - .collect::>() - }) - .filter(|p| !p.is_empty()) - .unwrap_or_else(default_packages); - return Some(NvidiaOffer { - gpu, - reason, - packages, - installed: nvidia_ready(), - }); - } - return Some(generic_offer()); - } - None -} - -fn default_packages() -> Vec { - NVIDIA_PACKAGES.iter().map(|s| (*s).to_string()).collect() -} - -fn generic_offer() -> NvidiaOffer { - NvidiaOffer { - gpu: "NVIDIA GPU".into(), - reason: "BOS found an NVIDIA device. Install the proprietary driver only if you want it — nouveau stays otherwise.".into(), - packages: default_packages(), - installed: nvidia_ready(), - } -} - -#[tauri::command] -pub fn get_nvidia_offer() -> Option { - read_nvidia_offer() -} - -/// Install nvidia + nvidia-utils and write the Hyprland env drop-in. -/// Prefers `/usr/local/bin/bos-nvidia-setup` (ISO script) so package -/// install + env stay one path. Falls back to allowlisted pacman plus -/// the same drop-in when the script is not on this install yet. -#[tauri::command] -pub async fn nvidia_setup(app: AppHandle, session_id: String) -> bool { - let home = std::env::var("HOME").unwrap_or_default(); - if command_exists("bos-nvidia-setup") { - let ok = streaming::run_hardcoded( - app.clone(), - session_id.clone(), - "pkexec", - &["bos-nvidia-setup", "--home", &home], - ) - .await; - if ok { - streaming::emit_line(&app, &session_id, "reboot required"); - } - return ok; - } - - streaming::emit_line( - &app, - &session_id, - "bos-nvidia-setup not on PATH; installing via pacman and writing the env drop-in", - ); - let packages = default_packages(); - if !streaming::pacman_install(app.clone(), session_id.clone(), packages).await { - return false; - } - match write_nvidia_dropin() { - Ok(()) => { - streaming::emit_line(&app, &session_id, "wrote ~/.config/hypr/nvidia.lua"); - streaming::emit_line(&app, &session_id, "reboot required"); - true - } - Err(e) => { - streaming::emit_line(&app, &session_id, &format!("Error: {e}")); - false - } - } -} - -fn write_nvidia_dropin() -> Result<(), String> { - let dir = util::hypr_dir(); - std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?; - let path = dir.join("nvidia.lua"); - super::config::atomic_write(&path, NVIDIA_LUA).map_err(|e| e.to_string())?; - ensure_hyprland_include()?; - Ok(()) -} - -fn include_already_present(text: &str) -> bool { - text.contains("nvidia.lua") -} - -fn ensure_hyprland_include() -> Result<(), String> { - let path = util::hypr_dir().join("hyprland.lua"); - let existing = std::fs::read_to_string(&path).unwrap_or_default(); - if include_already_present(&existing) { - return Ok(()); - } - if existing.is_empty() { - return Ok(()); - } - let mut text = existing; - if !text.ends_with('\n') { - text.push('\n'); - } - text.push('\n'); - text.push_str(HYPR_INCLUDE); - if !text.ends_with('\n') { - text.push('\n'); - } - super::config::atomic_write(&path, &text).map_err(|e| e.to_string()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn missing_file_is_none() { - // This machine's real probe path is not something the unit test - // should depend on; the helper is covered via parse cases below. - let parsed = serde_json::from_str::("{\"offer\":false}").unwrap(); - assert_eq!(parsed["offer"], false); - } - - #[test] - fn dismissed_or_offer_false_hides() { - // Inlined copies of the hide conditions so a schema change is obvious. - let hide = |v: &str| { - let v: serde_json::Value = serde_json::from_str(v).unwrap(); - v.get("offer").and_then(|x| x.as_bool()) == Some(false) - || v.get("dismissed").and_then(|x| x.as_bool()) == Some(true) - }; - assert!(hide(r#"{"offer":false}"#)); - assert!(hide(r#"{"dismissed":true}"#)); - assert!(!hide(r#"{"gpu":"RTX 4060"}"#)); - } - - #[test] - fn dropin_has_current_wiki_env_and_no_obsolete_vars() { - assert!(NVIDIA_LUA.contains("LIBVA_DRIVER_NAME")); - assert!(NVIDIA_LUA.contains("__GLX_VENDOR_LIBRARY_NAME")); - assert!(NVIDIA_LUA.contains("NVD_BACKEND")); - assert!(!NVIDIA_LUA.contains("hl.env(\"WLR_")); - assert!(!NVIDIA_LUA.contains("hl.env(\"GBM_BACKEND")); - assert!(!NVIDIA_LUA.contains("cuda")); - } - - #[test] - fn include_snippet_is_conditional() { - assert!(HYPR_INCLUDE.contains("nvidia.lua")); - assert!(HYPR_INCLUDE.contains("io.open")); - assert!(HYPR_INCLUDE.contains("pcall(dofile")); - } - - #[test] - fn include_detects_existing_snippet() { - assert!(include_already_present("pcall(dofile, nvidia.lua)")); - assert!(!include_already_present("hl.env(\"XCURSOR_SIZE\", \"24\")")); - } - - #[test] - fn packages_are_nvidia_and_utils_only() { - assert_eq!(NVIDIA_PACKAGES, &["nvidia", "nvidia-utils"]); - } -} diff --git a/src/src/commands/optional.rs b/src/src/commands/optional.rs deleted file mode 100644 index 835ec8e..0000000 --- a/src/src/commands/optional.rs +++ /dev/null @@ -1,112 +0,0 @@ -//! Curated optional software. Not an AUR dump — four explicit offers, -//! each installed through a typed command (bakery or allowlisted pacman). - -use serde::Serialize; -use tokio::process::Command; - -use super::packages::get_installed_packages; -use super::util::{command_exists, fail_output, pacman_installed}; - -#[derive(Serialize, Clone)] -pub struct OptionalItem { - id: String, - title: String, - detail: String, - installed: bool, - via: String, -} - -#[derive(Serialize)] -pub struct OptionalStatus { - items: Vec, - flathub: bool, -} - -fn bakery_has(name: &str) -> bool { - get_installed_packages().iter().any(|p| p.name == name) -} - -fn flathub_enabled() -> bool { - if !command_exists("flatpak") { - return false; - } - std::process::Command::new("flatpak") - .args(["remotes"]) - .output() - .ok() - .map(|o| { - String::from_utf8_lossy(&o.stdout) - .to_ascii_lowercase() - .contains("flathub") - }) - .unwrap_or(false) -} - -#[tauri::command] -pub fn get_optional_software() -> OptionalStatus { - let breadcast = bakery_has("breadcast") || command_exists("breadcast"); - let flatpak = pacman_installed("flatpak") || command_exists("flatpak"); - let office = pacman_installed("libreoffice-fresh") - && (pacman_installed("papers") || pacman_installed("evince")); - let steam = pacman_installed("steam") || command_exists("steam"); - OptionalStatus { - items: vec![ - OptionalItem { - id: "breadcast".into(), - title: "breadcast".into(), - detail: - "Optional bread-ecosystem app. Installed through bakery — it is not on the ISO." - .into(), - installed: breadcast, - via: "bakery".into(), - }, - OptionalItem { - id: "flatpak".into(), - title: "Flatpak + Flathub".into(), - detail: "Enables the Flatpak runtime and the Flathub user remote.".into(), - installed: flatpak && flathub_enabled(), - via: "pacman".into(), - }, - OptionalItem { - id: "office".into(), - title: "LibreOffice + PDF".into(), - detail: "libreoffice-fresh and papers (GNOME document viewer).".into(), - installed: office, - via: "pacman".into(), - }, - OptionalItem { - id: "steam".into(), - title: "Steam".into(), - detail: "Valve Steam from the multilib repo.".into(), - installed: steam, - via: "pacman".into(), - }, - ], - flathub: flathub_enabled(), - } -} - -/// User Flathub remote — no root. Flatpak itself is installed separately -/// via the allowlisted pacman command when missing. -#[tauri::command] -pub async fn enable_flathub() -> Result<(), String> { - if !command_exists("flatpak") { - return Err("flatpak is not installed".into()); - } - let output = Command::new("flatpak") - .args([ - "remote-add", - "--if-not-exists", - "--user", - "flathub", - "https://dl.flathub.org/repo/flathub.flatpakrepo", - ]) - .output() - .await - .map_err(|e| e.to_string())?; - if output.status.success() { - Ok(()) - } else { - Err(fail_output(&output, "flatpak remote-add")) - } -} diff --git a/src/src/commands/packages.rs b/src/src/commands/packages.rs deleted file mode 100644 index 53556fb..0000000 --- a/src/src/commands/packages.rs +++ /dev/null @@ -1,44 +0,0 @@ -use serde::Serialize; -use std::collections::HashMap; - -#[derive(Serialize, Clone)] -pub struct InstalledPackage { - pub name: String, - version: String, -} - -#[tauri::command] -pub fn get_installed_packages() -> Vec { - let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string()); - let path = std::path::Path::new(&home).join(".local/state/bakery/installed.json"); - - let Ok(text) = std::fs::read_to_string(&path) else { - return Vec::new(); - }; - let Ok(mut parsed) = serde_json::from_str::(&text) else { - return Vec::new(); - }; - // installed.json is {"packages": {name: {version, binaries, services}}}, - // not a flat map of package name to metadata. - let Some(packages) = parsed.get_mut("packages").map(std::mem::take) else { - return Vec::new(); - }; - let Ok(packages) = serde_json::from_value::>(packages) - else { - return Vec::new(); - }; - - let mut list: Vec = packages - .into_iter() - .map(|(name, val)| { - let version = val - .get("version") - .and_then(|v| v.as_str()) - .unwrap_or("unknown") - .to_string(); - InstalledPackage { name, version } - }) - .collect(); - list.sort_by(|a, b| a.name.cmp(&b.name)); - list -} diff --git a/src/src/commands/power.rs b/src/src/commands/power.rs deleted file mode 100644 index ada019c..0000000 --- a/src/src/commands/power.rs +++ /dev/null @@ -1,212 +0,0 @@ -//! Battery/power status (upower), brightness (brightnessctl), and TLP's -//! current profile. Deliberately no AC/Battery/Performance *switcher* — TLP -//! automatically picks a profile by power source, so this only exposes -//! controls for things that are actually user choices: brightness, and -//! charge thresholds where the hardware supports them. - -use serde::Serialize; -use tokio::process::Command; - -use super::util; - -async fn upower_device(kind: &str) -> Option { - let out = Command::new("upower").arg("-e").output().await.ok()?; - String::from_utf8_lossy(&out.stdout) - .lines() - .find(|l| l.to_lowercase().contains(kind)) - .map(str::to_string) -} - -async fn upower_field(device: &str, field: &str) -> Option { - let out = Command::new("upower") - .args(["-i", device]) - .output() - .await - .ok()?; - let text = String::from_utf8_lossy(&out.stdout); - text.lines() - .find(|l| l.trim_start().starts_with(field)) - .and_then(|l| l.split(':').nth(1)) - .map(|v| v.trim().to_string()) -} - -async fn battery_summary() -> Vec<(String, String)> { - let Some(bat) = upower_device("bat").await else { - return vec![("Battery".to_string(), "No battery detected".to_string())]; - }; - let mut rows = Vec::new(); - if let Some(state) = upower_field(&bat, "state").await { - rows.push(("Status".to_string(), state)); - } - if let Some(pct) = upower_field(&bat, "percentage").await { - rows.push(("Charge".to_string(), pct)); - } - let t = match upower_field(&bat, "time to empty").await { - Some(t) => Some(t), - None => upower_field(&bat, "time to full").await, - }; - if let Some(t) = t { - rows.push(("Time remaining".to_string(), t)); - } - let full: Option = upower_field(&bat, "energy-full") - .await - .and_then(|v| v.split_whitespace().next()?.parse().ok()); - let design: Option = upower_field(&bat, "energy-full-design") - .await - .and_then(|v| v.split_whitespace().next()?.parse().ok()); - if let (Some(full), Some(design)) = (full, design) { - if design > 0.0 { - rows.push(( - "Battery health".to_string(), - format!("{:.0}% of design capacity", full / design * 100.0), - )); - } - } - rows -} - -async fn power_source() -> String { - match upower_device("ac").await { - Some(ac) => match upower_field(&ac, "online").await { - Some(v) if v == "yes" => "AC power".to_string(), - Some(_) => "Battery".to_string(), - None => "Unknown".to_string(), - }, - None => "Unknown".to_string(), - } -} - -async fn tlp_profile() -> Option { - let out = Command::new("tlp-stat").arg("-s").output().await.ok()?; - let text = String::from_utf8_lossy(&out.stdout); - text.lines() - .find(|l| l.trim_start().starts_with("TLP profile")) - .and_then(|l| l.split('=').nth(1)) - .map(|v| v.trim().to_string()) -} - -async fn brightness_device() -> Option { - let out = Command::new("brightnessctl").output().await.ok()?; - String::from_utf8_lossy(&out.stdout) - .lines() - .find(|l| l.starts_with("Device")) - .and_then(|l| l.split('\'').nth(1)) - .map(str::to_string) -} - -async fn brightness_pct() -> Option { - let out = Command::new("brightnessctl").output().await.ok()?; - let text = String::from_utf8_lossy(&out.stdout); - text.lines() - .find(|l| l.contains("Current brightness")) - .and_then(|l| l.split('(').nth(1)) - .and_then(|v| v.trim_end_matches("%)").parse().ok()) -} - -/// Charge-threshold sysfs paths, only Some when the running kernel driver -/// actually exposes them — genuinely hardware-dependent, not every install -/// will have this. -fn charge_threshold_paths() -> Option<(std::path::PathBuf, std::path::PathBuf)> { - let base = std::path::Path::new("/sys/class/power_supply"); - let entries = std::fs::read_dir(base).ok()?; - for entry in entries.flatten() { - let start = entry.path().join("charge_control_start_threshold"); - let end = entry.path().join("charge_control_end_threshold"); - if start.exists() && end.exists() { - return Some((start, end)); - } - } - None -} - -fn read_threshold(path: &std::path::Path) -> i64 { - std::fs::read_to_string(path) - .ok() - .and_then(|s| s.trim().parse().ok()) - .unwrap_or(100) -} - -#[derive(Serialize)] -pub struct PowerInfo { - battery: Vec<(String, String)>, - power_source: String, - brightness_pct: Option, - charge_start: Option, - charge_end: Option, - tlp_profile: Option, -} - -#[tauri::command] -pub async fn get_power_info() -> PowerInfo { - let charge = charge_threshold_paths(); - PowerInfo { - battery: battery_summary().await, - power_source: power_source().await, - brightness_pct: brightness_pct().await, - charge_start: charge.as_ref().map(|(s, _)| read_threshold(s)), - charge_end: charge.as_ref().map(|(_, e)| read_threshold(e)), - tlp_profile: tlp_profile().await, - } -} - -#[tauri::command] -pub async fn set_brightness(percent: i64) -> Result<(), String> { - let Some(device) = brightness_device().await else { - return Err("No controllable backlight found".into()); - }; - // Clamp before handing the value to brightnessctl. - let pct = format!("{}%", percent.clamp(0, 100)); - Command::new("brightnessctl") - .args(["--device", &device, "set", &pct]) - .status() - .await - .map_err(|e| e.to_string())?; - Ok(()) -} - -fn charge_threshold_write(which: &str, percent: i64) -> Result<(String, i64), String> { - if which != "start" && which != "end" { - return Err("threshold must be start or end".into()); - } - Ok((which.to_string(), percent.clamp(0, 100))) -} - -#[tauri::command] -pub async fn set_charge_threshold(which: String, percent: i64) -> Result<(), String> { - let (which, percent) = charge_threshold_write(&which, percent)?; - let Some((start, end)) = charge_threshold_paths() else { - return Err("No charge threshold support on this hardware".into()); - }; - let path = if which == "start" { start } else { end }; - // GNU tee writes stdin to its path operands — the percent must be piped, - // not passed as a second path argument. - let input = format!("{percent}\n"); - if util::run_with_stdin(&["pkexec", "tee", &path.display().to_string()], &input).await { - Ok(()) - } else { - Err("Failed to set charge threshold".into()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn charge_threshold_clamps_and_restricts_which() { - assert_eq!( - charge_threshold_write("start", 80).unwrap(), - ("start".into(), 80) - ); - assert_eq!( - charge_threshold_write("end", 150).unwrap(), - ("end".into(), 100) - ); - assert_eq!( - charge_threshold_write("start", -5).unwrap(), - ("start".into(), 0) - ); - assert!(charge_threshold_write("both", 50).is_err()); - assert!(charge_threshold_write("-start", 50).is_err()); - } -} diff --git a/src/src/commands/printing.rs b/src/src/commands/printing.rs deleted file mode 100644 index 0f2918b..0000000 --- a/src/src/commands/printing.rs +++ /dev/null @@ -1,204 +0,0 @@ -//! CUPS printers via lpstat / lpadmin. Adding a printer through the full -//! device wizard is `system-config-printer`; a simple IPP Everywhere queue -//! can be created here when the user has a URI. - -use serde::Serialize; -use tokio::process::Command; - -use super::util::{fail_output, valid_printer_name}; - -#[derive(Serialize, Clone)] -pub struct Printer { - name: String, - status: String, - enabled: bool, - is_default: bool, -} - -#[derive(Serialize)] -pub struct PrintingStatus { - printers: Vec, - default: Option, - cups_ok: bool, - error: Option, -} - -#[tauri::command] -pub async fn get_printers() -> PrintingStatus { - let output = match Command::new("lpstat").args(["-p", "-d"]).output().await { - Ok(o) => o, - Err(e) => { - return PrintingStatus { - printers: Vec::new(), - default: None, - cups_ok: false, - error: Some(format!("couldn't run lpstat: {e}")), - }; - } - }; - if !output.status.success() { - return PrintingStatus { - printers: Vec::new(), - default: None, - cups_ok: false, - error: Some(fail_output(&output, "lpstat")), - }; - } - let text = String::from_utf8_lossy(&output.stdout); - parse_lpstat(&text) -} - -fn parse_lpstat(text: &str) -> PrintingStatus { - let mut printers = Vec::new(); - let mut default = None; - for line in text.lines() { - let line = line.trim(); - if let Some(rest) = line.strip_prefix("printer ") { - let mut parts = rest.splitn(2, ' '); - let name = parts.next().unwrap_or("").to_string(); - let rest = parts.next().unwrap_or(""); - if name.is_empty() { - continue; - } - let enabled = !rest.contains("disabled"); - let status = rest - .strip_prefix("is ") - .unwrap_or(rest) - .split(". ") - .next() - .unwrap_or(rest) - .trim() - .to_string(); - printers.push(Printer { - name, - status, - enabled, - is_default: false, - }); - } else if let Some(name) = line.strip_prefix("system default destination: ") { - default = Some(name.trim().to_string()); - } else if line == "no system default destination" { - default = None; - } - } - if let Some(def) = default.as_deref() { - for p in &mut printers { - p.is_default = p.name == def; - } - } - PrintingStatus { - printers, - default, - cups_ok: true, - error: None, - } -} - -#[tauri::command] -pub async fn set_default_printer(name: String) -> Result<(), String> { - if !valid_printer_name(&name) { - return Err(format!("invalid printer name '{name}'")); - } - let output = Command::new("lpadmin") - .args(["-d", &name]) - .output() - .await - .map_err(|e| e.to_string())?; - if output.status.success() { - return Ok(()); - } - let output = Command::new("pkexec") - .args(["lpadmin", "-d", &name]) - .output() - .await - .map_err(|e| e.to_string())?; - if output.status.success() { - Ok(()) - } else { - Err(fail_output(&output, "lpadmin")) - } -} - -#[tauri::command] -pub async fn add_ipp_printer(name: String, uri: String) -> Result<(), String> { - if !valid_printer_name(&name) { - return Err(format!("invalid printer name '{name}'")); - } - if !valid_printer_uri(&uri) { - return Err("URI must be ipp://, ipps://, socket://, usb://, or dnssd://".into()); - } - let args_owned = [ - "-p".into(), - name.clone(), - "-E".into(), - "-v".into(), - uri, - "-m".into(), - "everywhere".into(), - ]; - let output = Command::new("lpadmin") - .args(&args_owned) - .output() - .await - .map_err(|e| e.to_string())?; - if output.status.success() { - return Ok(()); - } - let mut pk = vec!["lpadmin".to_string()]; - pk.extend(args_owned); - let output = Command::new("pkexec") - .args(&pk) - .output() - .await - .map_err(|e| e.to_string())?; - if output.status.success() { - Ok(()) - } else { - Err(fail_output(&output, "lpadmin")) - } -} - -fn valid_printer_uri(uri: &str) -> bool { - let u = uri.trim(); - !u.is_empty() - && u.len() <= 512 - && !u.contains(char::is_whitespace) - && (u.starts_with("ipp://") - || u.starts_with("ipps://") - || u.starts_with("socket://") - || u.starts_with("usb://") - || u.starts_with("dnssd://")) -} - -#[tauri::command] -pub fn open_printer_settings() { - let _ = std::process::Command::new("system-config-printer").spawn(); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn lpstat_parses_idle_and_default() { - let text = "\ -printer Canon-TS6360a is idle. enabled since Mon 10 Aug 2026 -printer Hall is disabled since yesterday -system default destination: Canon-TS6360a -"; - let st = parse_lpstat(text); - assert_eq!(st.printers.len(), 2); - assert!(st.printers[0].is_default); - assert!(st.printers[0].enabled); - assert!(!st.printers[1].enabled); - assert_eq!(st.default.as_deref(), Some("Canon-TS6360a")); - } - - #[test] - fn uri_schemes() { - assert!(valid_printer_uri("ipp://192.168.1.5/ipp/print")); - assert!(valid_printer_uri("ipps://printer.local/ipp")); - assert!(!valid_printer_uri("http://evil")); - assert!(!valid_printer_uri("ipp://x y")); - } -} diff --git a/src/src/commands/service.rs b/src/src/commands/service.rs deleted file mode 100644 index 17a1540..0000000 --- a/src/src/commands/service.rs +++ /dev/null @@ -1,119 +0,0 @@ -//! Live systemd `--user` unit status plus start/stop/restart/logs — every -//! bread-ecosystem panel whose app is actually a daemon (not just a config -//! file) gets this. Ported from `src/ui/widgets.rs`'s `service_control`; -//! the `critical`-unit confirm-before-stop behavior moves to the frontend -//! (a confirm step gating the call to `service_action`), since that's UI -//! policy, not something the command itself needs to know. - -use serde::{Deserialize, Serialize}; -use tokio::process::Command; - -#[derive(Serialize)] -pub struct ServiceStatus { - active: bool, - enabled: bool, -} - -#[derive(Deserialize)] -pub enum ServiceAction { - Start, - Stop, - Restart, -} - -/// Units the frontend already hardcodes in ServiceControl call sites. -const ALLOWED_UNITS: &[&str] = &[ - "breadd.service", - "breadclipd.service", - "breadcrumbs.service", - "breadmill.service", - "breadbox-sync.service", -]; - -fn allowed_unit(unit: &str) -> bool { - ALLOWED_UNITS.contains(&unit) -} - -async fn systemctl_active(unit: &str) -> bool { - Command::new("systemctl") - .args(["--user", "is-active", "--quiet", unit]) - .status() - .await - .map(|s| s.success()) - .unwrap_or(false) -} - -async fn systemctl_enabled(unit: &str) -> bool { - Command::new("systemctl") - .args(["--user", "is-enabled", "--quiet", unit]) - .status() - .await - .map(|s| s.success()) - .unwrap_or(false) -} - -#[tauri::command] -pub async fn get_service_status(unit: String) -> Result { - if !allowed_unit(&unit) { - return Err("unknown service".into()); - } - Ok(ServiceStatus { - active: systemctl_active(&unit).await, - enabled: systemctl_enabled(&unit).await, - }) -} - -#[tauri::command] -pub async fn service_action(unit: String, action: ServiceAction) -> Result<(), String> { - if !allowed_unit(&unit) { - return Err("unknown service".into()); - } - let verb = match action { - ServiceAction::Start => "start", - ServiceAction::Stop => "stop", - ServiceAction::Restart => "restart", - }; - let output = Command::new("systemctl") - .args(["--user", verb, &unit]) - .output() - .await - .map_err(|e| e.to_string())?; - if output.status.success() { - Ok(()) - } else { - Err(String::from_utf8_lossy(&output.stderr).trim().to_string()) - } -} - -/// Opens a terminal following the unit's journal — same as today's GTK -/// panel, no reason to pull an open-ended `journalctl -f` tail into the -/// webview. -#[tauri::command] -pub fn open_logs(unit: String) -> Result<(), String> { - if !allowed_unit(&unit) { - return Err("unknown service".into()); - } - std::process::Command::new("kitty") - .args(["-e", "journalctl", "--user", "-u", &unit, "-f"]) - .spawn() - .map_err(|e| e.to_string())?; - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn units_match_frontend_hardcoded_list() { - assert!(allowed_unit("breadd.service")); - assert!(allowed_unit("breadclipd.service")); - assert!(allowed_unit("breadcrumbs.service")); - assert!(allowed_unit("breadmill.service")); - assert!(allowed_unit("breadbox-sync.service")); - assert!(!allowed_unit("sshd.service")); - assert!(!allowed_unit("breadd.service;reboot")); - assert!(!allowed_unit("../sshd.service")); - assert!(!allowed_unit("-u sshd")); - } -} diff --git a/src/src/commands/snapshots.rs b/src/src/commands/snapshots.rs deleted file mode 100644 index 6e84d2a..0000000 --- a/src/src/commands/snapshots.rs +++ /dev/null @@ -1,67 +0,0 @@ -use serde::Serialize; -use tokio::process::Command; - -use super::util; - -#[derive(Serialize, Clone)] -pub struct SnapshotRow { - number: String, - date: String, - description: String, -} - -/// `Err` carries snapper's trimmed stderr — distinct from `Ok(vec![])` -/// (snapper works fine, there just aren't any snapshots yet). -#[tauri::command] -pub async fn get_snapshots() -> Result, String> { - // NOTE: the real flag is --columns, not --output-cols (snapper rejects - // that outright) — confirmed against snapper 0.13's own --help. - let output = Command::new("snapper") - .args(["list", "--columns", "number,date,description"]) - .output() - .await - .map_err(|e| e.to_string())?; - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); - eprintln!("bos-settings: snapper list failed: {stderr}"); - return Err(stderr); - } - - let text = String::from_utf8_lossy(&output.stdout); - Ok(text - .lines() - .skip(2) // header + separator - .filter_map(|line| { - let mut cols = line.splitn(3, '|'); - let number = cols.next()?.trim().to_string(); - // Snapshot 0 ("current") always exists, can't be rolled back to - // or deleted, and isn't a real snapshot. - if number == "0" { - return None; - } - Some(SnapshotRow { number, date: cols.next()?.trim().to_string(), description: cols.next()?.trim().to_string() }) - }) - .collect()) -} - -#[tauri::command] -pub async fn delete_snapshot(number: String) -> Result<(), String> { - if !util::valid_number_id(&number) { - return Err("invalid snapshot number".into()); - } - let output = Command::new("snapper").args(["delete", &number]).status().await.map_err(|e| e.to_string())?; - if output.success() { - Ok(()) - } else { - Err("snapper delete exited with an error — the snapshot wasn't removed.".into()) - } -} - -/// BOS boots with root pinned to a named subvolume (grub emits -/// rootflags=subvol=@), so `snapper rollback`'s usual mechanism has no -/// effect here. The real way back is grub-btrfs, which generates a GRUB -/// submenu entry per snapshot — this just reboots so the user can pick it. -#[tauri::command] -pub fn reboot_system() { - let _ = std::process::Command::new("systemctl").arg("reboot").spawn(); -} diff --git a/src/src/commands/sound.rs b/src/src/commands/sound.rs deleted file mode 100644 index 4cc2fb4..0000000 --- a/src/src/commands/sound.rs +++ /dev/null @@ -1,102 +0,0 @@ -//! Output/input volume and device selection over PipeWire's pulse -//! compatibility layer (`pactl`) — the same surface hyprland.lua's media -//! keys already use via `wpctl`. `pactl` is used here instead because it -//! can enumerate devices with human-readable descriptions and switch the -//! default in one command; `wpctl` cannot easily do either. - -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use tokio::process::Command; - -#[derive(Deserialize, Serialize, Clone)] -pub struct SoundDevice { - name: String, - description: String, - mute: bool, - percent: f64, -} - -#[derive(Deserialize)] -struct RawDevice { - name: String, - description: String, - mute: bool, - volume: HashMap, -} - -#[derive(Deserialize)] -struct RawVolumeChannel { - value_percent: String, -} - -impl RawDevice { - fn percent(&self) -> f64 { - self.volume - .values() - .next() - .and_then(|v| v.value_percent.trim_end_matches('%').trim().parse::().ok()) - .unwrap_or(0.0) - } -} - -async fn list_devices(kind: &str) -> Vec { - let Ok(output) = Command::new("pactl").args(["-f", "json", "list", kind]).output().await else { - return Vec::new(); - }; - let raw: Vec = serde_json::from_slice(&output.stdout).unwrap_or_default(); - raw.into_iter() - .map(|d| SoundDevice { name: d.name.clone(), description: d.description.clone(), mute: d.mute, percent: d.percent() }) - .collect() -} - -async fn default_device_name(kind: &str) -> Option { - let flag = if kind == "sinks" { "get-default-sink" } else { "get-default-source" }; - Command::new("pactl") - .arg(flag) - .output() - .await - .ok() - .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) - .filter(|s| !s.is_empty()) -} - -#[derive(Serialize)] -pub struct SoundSection { - devices: Vec, - default_name: Option, -} - -#[tauri::command] -pub async fn get_sound_section(kind: String) -> SoundSection { - let devices = list_devices(&kind).await; - let default_name = default_device_name(&kind).await; - SoundSection { devices, default_name } -} - -#[tauri::command] -pub async fn set_default_sound_device(kind: String, name: String) -> Result<(), String> { - let flag = if kind == "sinks" { "set-default-sink" } else { "set-default-source" }; - Command::new("pactl").args([flag, &name]).status().await.map_err(|e| e.to_string())?; - Ok(()) -} - -#[tauri::command] -pub async fn set_sound_volume(kind: String, name: String, percent: i64) -> Result<(), String> { - let flag = if kind == "sinks" { "set-sink-volume" } else { "set-source-volume" }; - let pct = format!("{percent}%"); - Command::new("pactl").args([flag, &name, &pct]).status().await.map_err(|e| e.to_string())?; - Ok(()) -} - -#[tauri::command] -pub async fn set_sound_mute(kind: String, name: String, mute: bool) -> Result<(), String> { - let flag = if kind == "sinks" { "set-sink-mute" } else { "set-source-mute" }; - let val = if mute { "1" } else { "0" }; - Command::new("pactl").args([flag, &name, val]).status().await.map_err(|e| e.to_string())?; - Ok(()) -} - -#[tauri::command] -pub fn open_mixer() { - let _ = std::process::Command::new("pavucontrol").spawn(); -} diff --git a/src/src/commands/streaming.rs b/src/src/commands/streaming.rs deleted file mode 100644 index 6295949..0000000 --- a/src/src/commands/streaming.rs +++ /dev/null @@ -1,222 +0,0 @@ -//! Shared event-streaming runner for the genuinely long-running operations -//! (package/firmware updates) where the GTK app treated output as "watch -//! the log scroll" — the Tauri-side analog of `stream_command_then`'s -//! async_channel → glib::spawn_future_local pipeline, using Tauri's event -//! bus instead of a GLib main-loop channel. -//! -//! The runner itself is *not* a Tauri command. A generic argv runner was -//! an arbitrary-command primitive; each public command below hardcodes the -//! program and the allowed argument shape. - -use serde::Serialize; -use std::process::Stdio; -use tauri::{AppHandle, Emitter}; -use tokio::io::{AsyncBufReadExt, BufReader}; -use tokio::process::Command; - -#[derive(Clone, Serialize)] -pub(crate) struct CmdOutputEvent { - session_id: String, - line: String, -} - -pub(crate) fn emit_line(app: &AppHandle, session_id: &str, line: &str) { - let _ = app.emit( - "cmd-output", - CmdOutputEvent { - session_id: session_id.to_string(), - line: line.to_string(), - }, - ); -} - -/// Runs a hardcoded `program args...`, emitting one `cmd-output` event per -/// line of stdout/stderr (tagged with `session_id` so the frontend can route -/// concurrent streams), and resolves to whether it exited successfully. -pub(crate) async fn run_hardcoded( - app: AppHandle, - session_id: String, - program: &str, - args: &[&str], -) -> bool { - run_hardcoded_env(app, session_id, program, args, &[]).await -} - -pub(crate) async fn run_hardcoded_env( - app: AppHandle, - session_id: String, - program: &str, - args: &[&str], - envs: &[(&str, String)], -) -> bool { - let mut cmd = Command::new(program); - cmd.args(args) - // Null stdin, never inherit: a child reading the app's inherited - // stdin (pkexec falling back to a tty password prompt, pacman's - // interactive confirm) would block forever and hang the install - // command. These commands are all non-interactive (--noconfirm/-y); - // authenticating goes through the polkit agent as a GUI dialog. - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .kill_on_drop(true); - for (k, v) in envs { - cmd.env(k, v); - } - let child = cmd.spawn(); - let mut child = match child { - Ok(c) => c, - Err(e) => { - let _ = app.emit( - "cmd-output", - CmdOutputEvent { - session_id, - line: format!("Error: {e}"), - }, - ); - return false; - } - }; - - let stdout = child.stdout.take().expect("stdout piped"); - let stderr = child.stderr.take().expect("stderr piped"); - - let read_stdout = async { - let mut lines = BufReader::new(stdout).lines(); - while let Ok(Some(line)) = lines.next_line().await { - let _ = app.emit( - "cmd-output", - CmdOutputEvent { - session_id: session_id.clone(), - line, - }, - ); - } - }; - let stderr_app = app.clone(); - let stderr_session = session_id.clone(); - let read_stderr = async move { - let mut lines = BufReader::new(stderr).lines(); - while let Ok(Some(line)) = lines.next_line().await { - let _ = stderr_app.emit( - "cmd-output", - CmdOutputEvent { - session_id: stderr_session.clone(), - line, - }, - ); - } - }; - - tokio::join!(read_stdout, read_stderr); - child.wait().await.map(|s| s.success()).unwrap_or(false) -} - -/// bakery package names are `foo`, `foo-bar`, `foo_bar` — reject flags, -/// paths, and anything else that would change `bakery update`'s shape. -fn valid_bakery_pkg(name: &str) -> bool { - let bytes = name.as_bytes(); - !bytes.is_empty() - && bytes.len() <= 128 - && bytes[0].is_ascii_alphanumeric() - && bytes - .iter() - .all(|b| b.is_ascii_alphanumeric() || *b == b'-' || *b == b'_') -} - -#[tauri::command] -pub async fn bakery_update(app: AppHandle, session_id: String, name: String) -> bool { - if !valid_bakery_pkg(&name) { - let _ = app.emit( - "cmd-output", - CmdOutputEvent { - session_id, - line: format!("Error: invalid bakery package name '{name}'"), - }, - ); - return false; - } - run_hardcoded(app, session_id, "bakery", &["update", &name]).await -} - -#[tauri::command] -pub async fn bakery_list(app: AppHandle, session_id: String) -> bool { - run_hardcoded(app, session_id, "bakery", &["list"]).await -} - -#[tauri::command] -pub async fn bakery_update_all(app: AppHandle, session_id: String) -> bool { - run_hardcoded(app, session_id, "bakery", &["update", "--all"]).await -} - -#[tauri::command] -pub async fn pacman_system_update(app: AppHandle, session_id: String) -> bool { - run_hardcoded( - app, - session_id, - "pkexec", - &["pacman", "-Syu", "--noconfirm"], - ) - .await -} - -#[tauri::command] -pub async fn fwupd_refresh(app: AppHandle, session_id: String) -> bool { - run_hardcoded(app, session_id, "fwupdmgr", &["refresh"]).await -} - -#[tauri::command] -pub async fn fwupd_update(app: AppHandle, session_id: String) -> bool { - run_hardcoded(app, session_id, "fwupdmgr", &["update", "-y"]).await -} - -#[tauri::command] -pub async fn bakery_install(app: AppHandle, session_id: String, name: String) -> bool { - if let Err(e) = super::util::allowed_bakery_install(&name) { - emit_line(&app, &session_id, &format!("Error: {e}")); - return false; - } - run_hardcoded(app, session_id, "bakery", &["-y", "install", &name]).await -} - -#[tauri::command] -pub async fn pacman_install(app: AppHandle, session_id: String, packages: Vec) -> bool { - let names = match super::util::allowed_pacman_packages(&packages) { - Ok(n) => n, - Err(e) => { - emit_line(&app, &session_id, &format!("Error: {e}")); - return false; - } - }; - let mut args: Vec = vec![ - "pacman".into(), - "-S".into(), - "--noconfirm".into(), - "--".into(), - ]; - args.extend(names); - let refs: Vec<&str> = args.iter().map(String::as_str).collect(); - run_hardcoded(app, session_id, "pkexec", &refs).await -} - -#[cfg(test)] -mod tests { - use super::valid_bakery_pkg; - - #[test] - fn bakery_pkg_accepts_real_names() { - assert!(valid_bakery_pkg("breadbar")); - assert!(valid_bakery_pkg("bos-settings")); - assert!(valid_bakery_pkg("bread_theme")); - } - - #[test] - fn bakery_pkg_rejects_flags_and_paths() { - assert!(!valid_bakery_pkg("")); - assert!(!valid_bakery_pkg("--all")); - assert!(!valid_bakery_pkg("-S")); - assert!(!valid_bakery_pkg("../evil")); - assert!(!valid_bakery_pkg("foo bar")); - assert!(!valid_bakery_pkg("foo;rm")); - } -} diff --git a/src/src/commands/theme.rs b/src/src/commands/theme.rs deleted file mode 100644 index b6a9116..0000000 --- a/src/src/commands/theme.rs +++ /dev/null @@ -1,148 +0,0 @@ -//! Bridges `bread-theme`'s pywal-derived palette into the webview as CSS -//! custom properties, and keeps it live: `bread-theme`'s generator rewrites -//! its shared stylesheet with a temp-then-rename (atomic replace), which -//! kills a direct file watch (inotify reports DELETE_SELF and never -//! re-arms) — so this watches the *parent directory* and filters by -//! filename instead, the same strategy `bread_theme::gtk::watch_theme_file` -//! uses for the GTK apps. - -use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; -use tauri::{AppHandle, Emitter, Manager}; - -/// Initial theme fetch — called once by the frontend at startup. -#[tauri::command] -pub fn get_theme_css(window: tauri::WebviewWindow) -> String { - render_theme_css(&palette_for_window(&window)) -} - -fn palette_for_window(window: &tauri::WebviewWindow) -> bread_theme::Palette { - window - .current_monitor() - .ok() - .flatten() - .and_then(|m| m.name().map(|s| s.to_string())) - .map(|name| bread_theme::load_palette_for(&name)) - .unwrap_or_else(bread_theme::load_palette) -} - -fn render_theme_css(palette: &bread_theme::Palette) -> String { - // bread-theme v0.7.1 exposes Palette + ink_on + tokens, but not the - // later css_custom_properties / css_tokens helpers (those landed after - // the tag). Emit the same :root custom-property names the Svelte app - // already uses so a tag pin doesn't require a web-side rename. - format!("{}\n{}", css_custom_properties(palette), css_tokens()) -} - -fn css_custom_properties(p: &bread_theme::Palette) -> String { - let pairs = [ - ("bg", p.background.as_str()), - ("fg", p.foreground.as_str()), - ("surface", p.color0.as_str()), - ("overlay", p.color7.as_str()), - ("accent", p.color1.as_str()), - ("red", p.color1.as_str()), - ("green", p.color2.as_str()), - ("yellow", p.color3.as_str()), - ("blue", p.color4.as_str()), - ("pink", p.color5.as_str()), - ("teal", p.color6.as_str()), - ("on-bg", bread_theme::ink_on(&p.background)), - ("on-surface", bread_theme::ink_on(&p.color0)), - ("on-accent", bread_theme::ink_on(&p.color1)), - ("on-red", bread_theme::ink_on(&p.color1)), - ("on-overlay", bread_theme::ink_on(&p.color7)), - ]; - let vars: String = pairs - .iter() - .map(|(name, value)| format!(" --{name}: {value};\n")) - .collect(); - format!(":root {{\n{vars}}}\n") -} - -fn css_tokens() -> String { - use bread_theme::tokens::*; - format!( - ":root {{\n\ - \x20\x20--font-family: '{font}';\n\ - \x20\x20--font-size-base: {base}px;\n\ - \x20\x20--font-size-secondary: {sec}px;\n\ - \x20\x20--space-xs: {xs}px;\n\ - \x20\x20--space-sm: {sm}px;\n\ - \x20\x20--space-md: {md}px;\n\ - \x20\x20--space-lg: {lg}px;\n\ - \x20\x20--space-xl: {xl}px;\n\ - \x20\x20--radius-primary: {r1}px;\n\ - \x20\x20--radius-secondary: {r2}px;\n\ - \x20\x20--radius-tertiary: {r3}px;\n\ - \x20\x20--radius-pill: {pill}px;\n\ - }}\n", - font = FONT_FAMILY, - base = FONT_SIZE_BASE, - sec = FONT_SIZE_SECONDARY, - xs = SPACE_XS, - sm = SPACE_SM, - md = SPACE_MD, - lg = SPACE_LG, - xl = SPACE_XL, - r1 = RADIUS_PRIMARY, - r2 = RADIUS_SECONDARY, - r3 = RADIUS_TERTIARY, - pill = RADIUS_PILL, - ) -} - -/// Start watching the shared theme file and emit `theme-changed` with the -/// freshly rendered CSS whenever it's rewritten (palette change from a new -/// wallpaper, or a manual `bread-theme reload`). Call once from `setup`. -pub fn watch_and_emit(app: &AppHandle) { - let target = bread_theme::shared_css_path(); - let Some(dir) = target.parent() else { return }; - let _ = std::fs::create_dir_all(dir); - - let app_for_watcher = app.clone(); - let target_for_watcher = target.clone(); - let mut watcher = match RecommendedWatcher::new( - move |res: notify::Result| { - let Ok(event) = res else { return }; - // Rewrites land as CREATE/MODIFY/RENAME events touching the - // stylesheet's path specifically — the directory watch also - // sees unrelated siblings, so filter to the target file. - let touches_target = matches!( - event.kind, - EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_) - ) && event.paths.iter().any(|p| p == &target_for_watcher); - if touches_target { - let css = app_for_watcher - .get_webview_window("main") - .map(|w| render_theme_css(&palette_for_window(&w))) - .unwrap_or_else(|| render_theme_css(&bread_theme::load_palette())); - let _ = app_for_watcher.emit("theme-changed", css); - } - }, - notify::Config::default(), - ) { - Ok(w) => w, - Err(e) => { - tracing_or_eprintln(&format!("theme watcher: failed to create: {e}")); - return; - } - }; - - if let Err(e) = watcher.watch(dir, RecursiveMode::NonRecursive) { - tracing_or_eprintln(&format!( - "theme watcher: failed to watch {}: {e}", - dir.display() - )); - return; - } - - // Leaked to stay alive for the process lifetime — this app has exactly - // one theme watcher, created once at startup, never torn down. - app.manage(WatcherHandle(watcher)); -} - -struct WatcherHandle(#[allow(dead_code)] RecommendedWatcher); - -fn tracing_or_eprintln(msg: &str) { - eprintln!("{msg}"); -} diff --git a/src/src/commands/updates.rs b/src/src/commands/updates.rs deleted file mode 100644 index 0b4da74..0000000 --- a/src/src/commands/updates.rs +++ /dev/null @@ -1,184 +0,0 @@ -//! Aggregated Updates page: pacman -Qu, bakery dry-run, fwupd devices. -//! Rollback is Snapshots / grub-btrfs — not `snapper rollback`. - -use serde::Serialize; -use tokio::process::Command; - -use super::firmware::{get_updatable_firmware, FwDevice}; -use super::nvidia::{read_nvidia_offer, NvidiaOffer}; -use super::util::strip_ansi; - -#[derive(Serialize, Clone)] -pub struct PendingUpdate { - name: String, - current: String, - latest: String, -} - -#[derive(Serialize)] -pub struct UpdatesStatus { - pacman: Vec, - pacman_error: Option, - bakery: Vec, - bakery_error: Option, - firmware: Vec, - nvidia: Option, -} - -#[tauri::command] -pub async fn get_updates_status() -> UpdatesStatus { - let (pacman, bakery, firmware) = tokio::join!( - list_pacman_upgrades(), - list_bakery_outdated(), - get_updatable_firmware() - ); - let (pacman, pacman_error) = match pacman { - Ok(v) => (v, None), - Err(e) => (Vec::new(), Some(e)), - }; - let (bakery, bakery_error) = match bakery { - Ok(v) => (v, None), - Err(e) => (Vec::new(), Some(e)), - }; - UpdatesStatus { - pacman, - pacman_error, - bakery, - bakery_error, - firmware, - nvidia: read_nvidia_offer(), - } -} - -async fn list_pacman_upgrades() -> Result, String> { - let output = Command::new("pacman") - .args(["-Qu"]) - .output() - .await - .map_err(|e| format!("couldn't run pacman: {e}"))?; - // pacman -Qu exits 1 when there is nothing to upgrade. - let text = String::from_utf8_lossy(&output.stdout); - Ok(parse_pacman_qu(&text)) -} - -fn parse_pacman_qu(text: &str) -> Vec { - text.lines() - .filter_map(|line| { - let line = line.trim(); - if line.is_empty() { - return None; - } - // "name old -> new" — extra fields after new are ignored. - let mut parts = line.split_whitespace(); - let name = parts.next()?.to_string(); - let current = parts.next()?.to_string(); - let arrow = parts.next()?; - if arrow != "->" { - return None; - } - let latest = parts.next()?.to_string(); - Some(PendingUpdate { - name, - current, - latest, - }) - }) - .collect() -} - -async fn list_bakery_outdated() -> Result, String> { - let output = Command::new("bakery") - .args(["--dry-run", "update", "--all"]) - .output() - .await - .map_err(|e| format!("couldn't run bakery: {e}"))?; - let text = strip_ansi(&String::from_utf8_lossy(&output.stdout)); - let err = strip_ansi(&String::from_utf8_lossy(&output.stderr)); - let combined = format!("{text}\n{err}"); - Ok(parse_bakery_outdated(&combined)) -} - -/// bakery has no `outdated` subcommand. `--dry-run update --all` is the -/// CLI's own preview of what a track-aware update would change. -fn parse_bakery_outdated(text: &str) -> Vec { - let mut out = Vec::new(); - for raw in text.lines() { - let line = raw.trim(); - if let Some(pkg) = parse_would_update(line).or_else(|| parse_updating_arrow(line)) { - if !out.iter().any(|p: &PendingUpdate| p.name == pkg.name) { - out.push(pkg); - } - } - } - out -} - -fn parse_would_update(line: &str) -> Option { - // "dry-run: would update bakery to 0.7.3-dev.…" - // "Would update bakery 0.7.3-dev.…" - let lower = line.to_ascii_lowercase(); - let i = lower.find("would update")?; - let rest = line[i + "would update".len()..].trim(); - let rest = rest.strip_prefix(':').unwrap_or(rest).trim(); - let rest = rest.strip_prefix("to ").unwrap_or(rest); - let mut parts = rest.split_whitespace(); - let name = parts.next()?.to_string(); - let mut latest = parts.next().unwrap_or("").to_string(); - if latest.eq_ignore_ascii_case("to") { - latest = parts.next().unwrap_or("").to_string(); - } - if name.is_empty() { - return None; - } - Some(PendingUpdate { - name, - current: String::new(), - latest, - }) -} - -fn parse_updating_arrow(line: &str) -> Option { - // "updating bakery 0.7.2 → 0.7.3" - let line = line.strip_prefix("updating ")?; - let (name, rest) = line.split_once(' ')?; - let (current, latest) = rest.split_once('→')?; - Some(PendingUpdate { - name: name.trim().to_string(), - current: current.trim().to_string(), - latest: latest.trim().to_string(), - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn pacman_qu_parses_arrow_lines() { - let text = - "linux 6.15.1-1 -> 6.15.2-1\nextra-note\nbos-settings 0.8.0-1 -> 0.8.1-1 [ignored]\n"; - let v = parse_pacman_qu(text); - assert_eq!(v.len(), 2); - assert_eq!(v[0].name, "linux"); - assert_eq!(v[0].current, "6.15.1-1"); - assert_eq!(v[0].latest, "6.15.2-1"); - assert_eq!(v[1].name, "bos-settings"); - } - - #[test] - fn bakery_dry_run_parses_would_update_and_arrow() { - let text = "\ - · breadbar is already at 0.3.2 -updating bakery 0.7.2-dev.1 → 0.7.3-dev.2 - dry-run: would update bakery to 0.7.3-dev.2 -Would update breadcast 1.2.3 -1 updated, 14 already up to date -"; - let v = parse_bakery_outdated(text); - assert_eq!(v.len(), 2); - assert_eq!(v[0].name, "bakery"); - assert_eq!(v[0].latest, "0.7.3-dev.2"); - assert_eq!(v[1].name, "breadcast"); - assert_eq!(v[1].latest, "1.2.3"); - } -} diff --git a/src/src/commands/users.rs b/src/src/commands/users.rs deleted file mode 100644 index a33023d..0000000 --- a/src/src/commands/users.rs +++ /dev/null @@ -1,216 +0,0 @@ -//! User account management — add/remove users, change passwords. Everything -//! here needs root (useradd/userdel/chpasswd), so every action goes through -//! `pkexec`. - -use serde::Serialize; -use tokio::process::Command; - -use super::util; - -#[derive(Serialize, Clone)] -pub struct Account { - username: String, - full_name: String, -} - -fn list_accounts() -> Vec { - let Ok(text) = std::fs::read_to_string("/etc/passwd") else { - return Vec::new(); - }; - text.lines() - .filter_map(|line| { - let f: Vec<&str> = line.split(':').collect(); - if f.len() < 7 { - return None; - } - let uid: u32 = f[2].parse().ok()?; - let shell = f[6]; - // Real human accounts: normal UID range, a real login shell - // (excludes system/service accounts like greeter, avahi, etc). - if !(1000..60000).contains(&uid) - || shell.ends_with("nologin") - || shell.ends_with("/false") - { - return None; - } - Some(Account { - username: f[0].to_string(), - full_name: f[4].split(',').next().unwrap_or("").to_string(), - }) - }) - .collect() -} - -#[derive(Serialize)] -pub struct UsersInfo { - accounts: Vec, - current_user: String, -} - -#[tauri::command] -pub fn get_users_info() -> UsersInfo { - UsersInfo { - accounts: list_accounts(), - current_user: std::env::var("USER").unwrap_or_default(), - } -} - -/// shadow-utils `USER_NAME_MAX` is 32; keep chpasswd/useradd operands inside it. -const USERNAME_MAX: usize = 32; - -/// `[a-z_][a-z0-9_-]*`, length-capped, no leading `-`. Also rejects `:`, -/// newlines, and other chpasswd field/line separators. -fn valid_username(name: &str) -> bool { - let bytes = name.as_bytes(); - if bytes.is_empty() || bytes.len() > USERNAME_MAX { - return false; - } - let first = bytes[0]; - if first != b'_' && !first.is_ascii_lowercase() { - return false; - } - bytes[1..] - .iter() - .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(*b, b'_' | b'-')) -} - -/// chpasswd reads `user:password` lines — a `:`, `\n`, or `\r` in either -/// field injects extra passwd entries or shifts columns. -fn valid_chpasswd_password(password: &str) -> bool { - !password.is_empty() - && password.len() <= 512 - && !password.contains('\n') - && !password.contains('\r') - && !password.contains(':') - && !password.contains('\0') -} - -fn chpasswd_input(username: &str, password: &str) -> Result { - if !valid_username(username) { - return Err("invalid username".into()); - } - if !valid_chpasswd_password(password) { - return Err("invalid password".into()); - } - Ok(format!("{username}:{password}\n")) -} - -fn may_delete_user(username: &str, current: &str) -> Result<(), String> { - if !valid_username(username) { - return Err("invalid username".into()); - } - if username == "root" { - return Err("refusing to remove root".into()); - } - if !current.is_empty() && username == current { - return Err("refusing to remove the current user".into()); - } - Ok(()) -} - -#[tauri::command] -pub async fn change_password(username: String, password: String) -> Result<(), String> { - // chpasswd_input validates the username *and* the password (rejects the - // newline / `:` that would let one entry smuggle another). - let input = chpasswd_input(&username, &password)?; - if util::run_with_stdin(&["pkexec", "chpasswd"], &input).await { - Ok(()) - } else { - Err("Failed to change password".into()) - } -} - -#[tauri::command] -pub async fn remove_user(username: String) -> Result<(), String> { - let current = std::env::var("USER").unwrap_or_default(); - // Validates the username and refuses `root` / the current user. - may_delete_user(&username, ¤t)?; - // `--` so a username can never be read as a userdel option. - let output = Command::new("pkexec") - .args(["userdel", "-r", "--", &username]) - .output() - .await - .map_err(|e| e.to_string())?; - if output.status.success() { - Ok(()) - } else { - Err(String::from_utf8_lossy(&output.stderr).trim().to_string()) - } -} - -#[tauri::command] -pub async fn add_user(username: String, full_name: String, password: String) -> Result<(), String> { - let username = username.trim(); - // Validates the username and the password (newline / `:` injection). - let input = chpasswd_input(username, &password)?; - // GECOS field is otherwise free text; strip control chars and the field - // separators so `-c` can't smuggle extra passwd fields or arguments. - let gecos: String = full_name - .chars() - .filter(|c| !matches!(c, '\n' | '\r' | '\0' | ',' | ':')) - .collect(); - let mut useradd_args = vec![ - "pkexec".to_string(), - "useradd".to_string(), - "-m".to_string(), - "-s".to_string(), - "/bin/bash".to_string(), - ]; - if !gecos.trim().is_empty() { - useradd_args.push("-c".to_string()); - useradd_args.push(gecos.trim().to_string()); - } - // `--` so the username can never be read as a useradd option. - useradd_args.push("--".to_string()); - useradd_args.push(username.to_string()); - let args_ref: Vec<&str> = useradd_args.iter().map(String::as_str).collect(); - let output = Command::new(args_ref[0]) - .args(&args_ref[1..]) - .output() - .await - .map_err(|e| e.to_string())?; - if !output.status.success() { - return Err(String::from_utf8_lossy(&output.stderr).trim().to_string()); - } - if util::run_with_stdin(&["pkexec", "chpasswd"], &input).await { - Ok(()) - } else { - Err("User created, but setting the password failed.".into()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn chpasswd_rejects_newline_injection() { - assert!(chpasswd_input("alice", "pw\nroot:evil").is_err()); - assert!(chpasswd_input("alice\nroot", "pw").is_err()); - assert!(chpasswd_input("alice\rroot", "pw").is_err()); - assert!(chpasswd_input("alice", "pw\rroot:x").is_err()); - assert!(chpasswd_input("al:ice", "pw").is_err()); - assert!(chpasswd_input("alice", "p:w").is_err()); - assert_eq!(chpasswd_input("alice", "secret").unwrap(), "alice:secret\n"); - } - - #[test] - fn username_grammar() { - assert!(valid_username("alice")); - assert!(valid_username("_svc")); - assert!(valid_username("a1-b_c")); - assert!(!valid_username("")); - assert!(!valid_username("-alice")); - assert!(!valid_username("Alice")); - assert!(!valid_username("root user")); - assert!(!valid_username(&"a".repeat(USERNAME_MAX + 1))); - } - - #[test] - fn remove_user_refuses_root_and_self() { - assert!(may_delete_user("root", "alice").is_err()); - assert!(may_delete_user("alice", "alice").is_err()); - assert!(may_delete_user("root\n", "alice").is_err()); - assert!(may_delete_user("bob", "alice").is_ok()); - } -} diff --git a/src/src/commands/util.rs b/src/src/commands/util.rs deleted file mode 100644 index 2eb76bb..0000000 --- a/src/src/commands/util.rs +++ /dev/null @@ -1,301 +0,0 @@ -//! Shared helpers for the OS-panel commands: PATH lookups, tight name -//! checks, 0600 writes, and the Hyprland `source =` fragment convention. - -use std::path::{Path, PathBuf}; - -use super::config; - -/// Pacman packages these panels may install. A generic `pacman -S` runner -/// is an arbitrary-package primitive; every name must be on this list. -pub const PACMAN_ALLOWLIST: &[&str] = &[ - "hyprsunset", - "fcitx5", - "fcitx5-configtool", - "fcitx5-gtk", - "fcitx5-qt", - "fcitx5-im", - "fcitx5-chinese-addons", - "fcitx5-table-extra", - "orca", - "kmag", - "restic", - "flatpak", - "libreoffice-fresh", - "papers", - "evince", - "steam", - "nvidia", - "nvidia-utils", -]; - -/// Bakery packages these panels may `bakery install`. breadcast is optional -/// software and is not on the ISO; do not add breadarr. -pub const BAKERY_INSTALL_ALLOWLIST: &[&str] = &["breadcast"]; - -pub fn command_exists(name: &str) -> bool { - let Some(paths) = std::env::var_os("PATH") else { - return false; - }; - std::env::split_paths(&paths).any(|dir| { - let candidate = dir.join(name); - candidate.is_file() - }) -} - -pub fn pacman_installed(pkg: &str) -> bool { - std::process::Command::new("pacman") - .args(["-Q", pkg]) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .status() - .map(|s| s.success()) - .unwrap_or(false) -} - -/// Arch package / bakery name: starts alphanumeric, then `[A-Za-z0-9+._-]`. -pub fn valid_pkg_name(name: &str) -> bool { - let bytes = name.as_bytes(); - !bytes.is_empty() - && bytes.len() <= 128 - && bytes[0].is_ascii_alphanumeric() - && bytes - .iter() - .all(|b| b.is_ascii_alphanumeric() || matches!(*b, b'-' | b'_' | b'+' | b'.')) -} - -pub fn allowed_pacman_packages(names: &[String]) -> Result, String> { - if names.is_empty() { - return Err("no packages given".into()); - } - let mut out = Vec::with_capacity(names.len()); - for name in names { - if !valid_pkg_name(name) || !PACMAN_ALLOWLIST.contains(&name.as_str()) { - return Err(format!("refusing to install '{name}'")); - } - if !out.iter().any(|e| e == name) { - out.push(name.clone()); - } - } - Ok(out) -} - -pub fn allowed_bakery_install(name: &str) -> Result<(), String> { - if !valid_pkg_name(name) || !BAKERY_INSTALL_ALLOWLIST.contains(&name) { - return Err(format!("refusing to bakery-install '{name}'")); - } - Ok(()) -} - -pub fn bos_settings_dir() -> PathBuf { - config::config_dir().join("bos-settings") -} - -/// Pipe `input` to a command's stdin (`pkexec` does not inherit a piped -/// stdin unless we set it). Used by chpasswd and `pkexec tee`. -pub async fn run_with_stdin(args: &[&str], input: &str) -> bool { - if args.is_empty() { - return false; - } - let Ok(mut child) = tokio::process::Command::new(args[0]) - .args(&args[1..]) - .stdin(std::process::Stdio::piped()) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .spawn() - else { - return false; - }; - if let Some(mut stdin) = child.stdin.take() { - use tokio::io::AsyncWriteExt; - if stdin.write_all(input.as_bytes()).await.is_err() { - return false; - } - } - child.wait().await.map(|s| s.success()).unwrap_or(false) -} - -/// Atomic write with mode 0600 set on the new inode before/after replace, -/// matching breadcrumbs' `networks.toml` care. -pub fn write_secure(path: &Path, contents: &str) -> Result<(), String> { - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent) - .map_err(|e| format!("creating {}: {e}", parent.display()))?; - } - bread_utils::atomic::write_atomic(path, contents, Some(0o600)) - .map_err(|e| format!("writing {}: {e}", path.display()))?; - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600)); - } - Ok(()) -} - -pub fn strip_ansi(s: &str) -> String { - let re = regex::Regex::new(r"\x1b\[[0-9;]*[A-Za-z]").expect("ansi regex"); - re.replace_all(s, "").into_owned() -} - -pub fn fail_output(output: &std::process::Output, what: &str) -> String { - let stderr = String::from_utf8_lossy(&output.stderr); - let stdout = String::from_utf8_lossy(&output.stdout); - let msg = stderr.trim(); - if !msg.is_empty() { - return msg.to_string(); - } - let msg = stdout.trim(); - if !msg.is_empty() { - return msg.to_string(); - } - format!("{what} failed") -} - -pub fn hypr_dir() -> PathBuf { - config::config_dir().join("hypr") -} - -pub fn hyprland_conf() -> PathBuf { - hypr_dir().join("hyprland.conf") -} - -/// Ensure `hyprland.conf` sources `~/.config/hypr/{fragment}`. Appends a -/// single source line when missing; does not rewrite the rest of the file. -pub fn ensure_hypr_source(fragment: &str) -> Result<(), String> { - if !valid_fragment(fragment) { - return Err(format!("invalid hypr fragment '{fragment}'")); - } - let dir = hypr_dir(); - std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?; - let path = hyprland_conf(); - let marker = format!("hypr/{fragment}"); - let existing = std::fs::read_to_string(&path).unwrap_or_default(); - if existing.lines().any(|l| l.contains(&marker)) { - return Ok(()); - } - let mut text = existing; - if !text.is_empty() && !text.ends_with('\n') { - text.push('\n'); - } - text.push_str(&format!("source = ~/.config/hypr/{fragment}\n")); - config::atomic_write(&path, &text).map_err(|e| e.to_string()) -} - -pub fn remove_hypr_source(fragment: &str) -> Result<(), String> { - if !valid_fragment(fragment) { - return Err(format!("invalid hypr fragment '{fragment}'")); - } - let path = hyprland_conf(); - let Ok(existing) = std::fs::read_to_string(&path) else { - return Ok(()); - }; - let marker = format!("hypr/{fragment}"); - let filtered: String = - existing - .lines() - .filter(|l| !l.contains(&marker)) - .fold(String::new(), |mut acc, l| { - acc.push_str(l); - acc.push('\n'); - acc - }); - if filtered != existing { - config::atomic_write(&path, &filtered).map_err(|e| e.to_string())?; - } - Ok(()) -} - -fn valid_fragment(name: &str) -> bool { - let bytes = name.as_bytes(); - !bytes.is_empty() - && bytes.len() <= 64 - && bytes[0].is_ascii_alphanumeric() - && bytes - .iter() - .all(|b| b.is_ascii_alphanumeric() || matches!(*b, b'-' | b'_' | b'.')) -} - -/// Connection / printer names: no flags, no newlines. Spaces are allowed -/// (NetworkManager connection ids often have them). -pub fn valid_nm_id(name: &str) -> bool { - let t = name.trim(); - !t.is_empty() - && t.len() <= 256 - && !t.starts_with('-') - && !t.contains('\n') - && !t.contains('\0') - && !t.contains(';') -} - -/// Reload Hyprland so settings.json / binds.json / monitors.json take effect now. -pub fn hypr_reload() { - let _ = std::process::Command::new("hyprctl").arg("reload").status(); -} - -pub fn valid_printer_name(name: &str) -> bool { - let bytes = name.as_bytes(); - !bytes.is_empty() - && bytes.len() <= 127 - && bytes[0].is_ascii_alphanumeric() - && bytes - .iter() - .all(|b| b.is_ascii_alphanumeric() || matches!(*b, b'-' | b'_' | b'.')) -} - -/// ufw / snapper numeric id — digits only. -pub fn valid_number_id(num: &str) -> bool { - !num.is_empty() && num.len() <= 12 && num.bytes().all(|b| b.is_ascii_digit()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn pkg_name_accepts_arch_names() { - assert!(valid_pkg_name("hyprsunset")); - assert!(valid_pkg_name("fcitx5-chinese-addons")); - assert!(valid_pkg_name("libreoffice-fresh")); - assert!(valid_pkg_name("nvidia-utils")); - } - - #[test] - fn pkg_name_rejects_flags() { - assert!(!valid_pkg_name("")); - assert!(!valid_pkg_name("-S")); - assert!(!valid_pkg_name("--noconfirm")); - assert!(!valid_pkg_name("foo;rm")); - assert!(!valid_pkg_name("foo bar")); - } - - #[test] - fn allowlist_rejects_unknown() { - assert!(allowed_pacman_packages(&["steam".into()]).is_ok()); - assert!(allowed_pacman_packages(&["evil".into()]).is_err()); - assert!(allowed_bakery_install("breadcast").is_ok()); - assert!(allowed_bakery_install("breadarr").is_err()); - } - - #[test] - fn nm_id_allows_spaces_not_flags() { - assert!(valid_nm_id("Home VPN")); - assert!(!valid_nm_id("-evil")); - assert!(!valid_nm_id("a\nb")); - assert!(!valid_nm_id("")); - } - - #[test] - fn printer_name_is_tight() { - assert!(valid_printer_name("Canon-TS6360a")); - assert!(!valid_printer_name("foo bar")); - assert!(!valid_printer_name("-d")); - } - - #[test] - fn number_id_is_digits_only() { - assert!(valid_number_id("42")); - assert!(!valid_number_id("")); - assert!(!valid_number_id("-1")); - assert!(!valid_number_id("12a")); - assert!(!valid_number_id("1 2")); - } -} diff --git a/src/src/commands/vpn.rs b/src/src/commands/vpn.rs deleted file mode 100644 index c09d039..0000000 --- a/src/src/commands/vpn.rs +++ /dev/null @@ -1,179 +0,0 @@ -//! NetworkManager VPN / WireGuard connections. breadcrumbs stays Wi-Fi -//! profiles; this panel only lists `vpn` and `wireguard` connection types. - -use serde::Serialize; -use tokio::process::Command; - -use super::util::{fail_output, valid_nm_id}; - -#[derive(Serialize, Clone)] -pub struct VpnConnection { - name: String, - kind: String, - active: bool, - autoconnect: bool, -} - -#[derive(Serialize)] -pub struct VpnStatus { - connections: Vec, - error: Option, -} - -#[tauri::command] -pub async fn get_vpn_connections() -> VpnStatus { - let output = match Command::new("nmcli") - .args([ - "-t", - "-f", - "NAME,TYPE,STATE,AUTOCONNECT", - "connection", - "show", - ]) - .output() - .await - { - Ok(o) => o, - Err(e) => { - return VpnStatus { - connections: Vec::new(), - error: Some(format!("couldn't run nmcli: {e}")), - }; - } - }; - if !output.status.success() { - return VpnStatus { - connections: Vec::new(), - error: Some(fail_output(&output, "nmcli")), - }; - } - let text = String::from_utf8_lossy(&output.stdout); - VpnStatus { - connections: parse_nm_connections(&text), - error: None, - } -} - -fn parse_nm_connections(text: &str) -> Vec { - text.lines() - .filter_map(|line| { - // nmcli -t escapes ":" in names as "\:". - let cols = split_nmcli(line); - if cols.len() < 3 { - return None; - } - let kind = cols[1].as_str(); - if kind != "vpn" && kind != "wireguard" { - return None; - } - let state = cols[2].as_str(); - let autoconnect = cols.get(3).map(|s| s == "yes").unwrap_or(false); - Some(VpnConnection { - name: cols[0].clone(), - kind: kind.to_string(), - active: state == "activated" || state == "activating", - autoconnect, - }) - }) - .collect() -} - -fn split_nmcli(line: &str) -> Vec { - let mut out = Vec::new(); - let mut cur = String::new(); - let mut chars = line.chars().peekable(); - while let Some(c) = chars.next() { - if c == '\\' { - if let Some(n) = chars.next() { - cur.push(n); - } - } else if c == ':' { - out.push(std::mem::take(&mut cur)); - } else { - cur.push(c); - } - } - out.push(cur); - out -} - -#[tauri::command] -pub async fn vpn_connect(name: String) -> Result<(), String> { - nmcli_con(&["connection", "up", "id", &checked_id(&name)?]).await -} - -#[tauri::command] -pub async fn vpn_disconnect(name: String) -> Result<(), String> { - nmcli_con(&["connection", "down", "id", &checked_id(&name)?]).await -} - -fn checked_id(name: &str) -> Result { - if !valid_nm_id(name) { - return Err(format!("invalid connection name '{name}'")); - } - Ok(name.trim().to_string()) -} - -async fn nmcli_con(args: &[&str]) -> Result<(), String> { - let output = Command::new("nmcli") - .args(args) - .output() - .await - .map_err(|e| e.to_string())?; - if output.status.success() { - Ok(()) - } else { - Err(fail_output(&output, "nmcli")) - } -} - -#[tauri::command] -pub async fn vpn_import(path: String) -> Result<(), String> { - let path = path.trim(); - if path.is_empty() || path.contains('\0') || path.contains('\n') { - return Err("invalid path".into()); - } - let p = std::path::Path::new(path); - if !p.is_absolute() || !p.is_file() { - return Err("pick an existing .conf or .ovpn file".into()); - } - let kind = match p - .extension() - .and_then(|e| e.to_str()) - .map(|s| s.to_ascii_lowercase()) - .as_deref() - { - Some("ovpn") => "openvpn", - Some("conf") => "wireguard", - _ => return Err("import a WireGuard .conf or OpenVPN .ovpn file".into()), - }; - nmcli_con(&["connection", "import", "type", kind, "file", path]).await -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parses_wireguard_and_skips_wifi() { - let text = "\ -Home WG:wireguard:activated:yes -Office:vpn: -NetComm:802-11-wireless:activated -tailscale0:tun:activated:yes -"; - let v = parse_nm_connections(text); - assert_eq!(v.len(), 2); - assert_eq!(v[0].name, "Home WG"); - assert!(v[0].active); - assert_eq!(v[1].kind, "vpn"); - assert!(!v[1].active); - } - - #[test] - fn unescapes_colon_in_name() { - let text = r"Work\:VPN:vpn:activated:no"; - let v = parse_nm_connections(text); - assert_eq!(v[0].name, "Work:VPN"); - } -} diff --git a/src/src/lib.rs b/src/src/lib.rs deleted file mode 100644 index fe4c347..0000000 --- a/src/src/lib.rs +++ /dev/null @@ -1,153 +0,0 @@ -mod commands; -mod screenshot; - -#[cfg_attr(mobile, tauri::mobile_entry_point)] -pub fn run() { - let args: Vec = std::env::args().collect(); - let screenshot_req = screenshot::parse(&args); - - tauri::Builder::default() - .plugin(tauri_plugin_opener::init()) - .plugin(tauri_plugin_dialog::init()) - .setup(move |app| { - commands::theme::watch_and_emit(app.handle()); - if let Some(req) = screenshot_req { - screenshot::dispatch(req, app.handle().clone()); - } - Ok(()) - }) - .invoke_handler(tauri::generate_handler![ - commands::theme::get_theme_css, - commands::about::get_system_info, - commands::about::set_hostname, - commands::service::get_service_status, - commands::service::service_action, - commands::service::open_logs, - commands::breadclip::open_breadclip, - commands::bread::get_bread_config, - commands::bread::save_bread_config, - commands::bread::list_bread_modules, - commands::breadpad::get_breadpad_config, - commands::breadpad::save_breadpad_config, - commands::breadsearch::get_breadsearch_config, - commands::breadsearch::save_breadsearch_config, - commands::breadbar::get_breadbar_css, - commands::breadbar::save_breadbar_css, - commands::breadbar::get_breadbar_style, - commands::breadbar::save_breadbar_style, - commands::breadbox::get_breadbox_contexts, - commands::breadbox::save_breadbox_contexts, - commands::breadcrumbs::get_breadcrumbs_config, - commands::breadcrumbs::save_breadcrumbs_config, - commands::breadpaper::get_current_wallpaper, - commands::breadpaper::set_wallpaper, - commands::breadpaper::list_wallpaper_library, - commands::breadpaper::wallpaper_library_dir_display, - commands::appearance::get_appearance, - commands::appearance::save_appearance, - commands::autostart::get_autostart_entries, - commands::autostart::save_autostart_entries, - commands::hyprland::get_live_monitors, - commands::hyprland::get_monitor_rules, - commands::hyprland::save_monitor_rules, - commands::hyprland::apply_monitor_layout, - commands::hyprland::open_hyprland_conf, - commands::hyprland::open_keybinds_viewer, - commands::keybinds::get_keybinds, - commands::keybinds::save_keybinds, - commands::sound::get_sound_section, - commands::sound::set_default_sound_device, - commands::sound::set_sound_volume, - commands::sound::set_sound_mute, - commands::sound::open_mixer, - commands::datetime::get_datetime_info, - commands::datetime::set_timezone, - commands::datetime::set_ntp_enabled, - commands::power::get_power_info, - commands::power::set_brightness, - commands::power::set_charge_threshold, - commands::network::get_network_info, - commands::network::set_wifi_radio, - commands::network::scan_wifi, - commands::network::connect_wifi, - commands::network::open_connection_editor, - commands::bluetooth::get_adapter_powered, - commands::bluetooth::set_adapter_powered, - commands::bluetooth::get_paired_devices, - commands::bluetooth::scan_bluetooth, - commands::bluetooth::bt_connect, - commands::bluetooth::bt_disconnect, - commands::bluetooth::bt_forget, - commands::bluetooth::bt_pair, - commands::firewall::get_firewall_status, - commands::firewall::set_firewall_enabled, - commands::firewall::add_firewall_rule, - commands::firewall::remove_firewall_rule, - commands::users::get_users_info, - commands::users::change_password, - commands::users::remove_user, - commands::users::add_user, - commands::streaming::bakery_update, - commands::streaming::bakery_list, - commands::streaming::bakery_update_all, - commands::streaming::bakery_install, - commands::streaming::pacman_system_update, - commands::streaming::pacman_install, - commands::streaming::fwupd_refresh, - commands::streaming::fwupd_update, - commands::packages::get_installed_packages, - commands::aur::search_aur, - commands::aur::install_aur_package, - commands::firmware::get_updatable_firmware, - commands::snapshots::get_snapshots, - commands::snapshots::delete_snapshot, - commands::snapshots::reboot_system, - commands::breadlock::get_breadlock_config, - commands::breadlock::save_breadlock_config, - commands::breadlock::breadlock_example_path, - commands::breadlock::open_breadlock_config, - commands::breadlock::open_breadlock_example, - commands::breadlock::lock_session, - commands::breadshot::get_breadshot_config, - commands::breadshot::save_breadshot_config, - commands::breadshot::get_breadshot_binds, - commands::breadshot::breadshot_region_clipboard, - commands::breadmon::open_breadmon, - commands::breadhelp::open_breadhelp, - commands::updates::get_updates_status, - commands::nvidia::get_nvidia_offer, - commands::nvidia::nvidia_setup, - commands::printing::get_printers, - commands::printing::set_default_printer, - commands::printing::add_ipp_printer, - commands::printing::open_printer_settings, - commands::vpn::get_vpn_connections, - commands::vpn::vpn_connect, - commands::vpn::vpn_disconnect, - commands::vpn::vpn_import, - commands::nightlight::get_nightlight, - commands::nightlight::set_nightlight, - commands::ime::get_ime_status, - commands::ime::set_ime_enabled, - commands::ime::open_fcitx_config, - commands::a11y::get_a11y_status, - commands::a11y::set_cursor_zoom, - commands::a11y::set_orca_running, - commands::a11y::open_kmag, - commands::defaults::get_default_apps, - commands::defaults::save_default_apps, - commands::channel::get_bakery_track, - commands::channel::set_bakery_track, - commands::backup::get_backup_config, - commands::backup::save_backup_config, - commands::backup::restic_init, - commands::backup::restic_backup, - commands::backup::restic_restore_dry_run, - commands::backup::restic_restore, - commands::backup::list_restic_snapshots, - commands::optional::get_optional_software, - commands::optional::enable_flathub, - ]) - .run(tauri::generate_context!()) - .expect("error while running tauri application"); -} diff --git a/src/src/main.rs b/src/src/main.rs deleted file mode 100644 index d138433..0000000 --- a/src/src/main.rs +++ /dev/null @@ -1,6 +0,0 @@ -// Prevents additional console window on Windows in release, DO NOT REMOVE!! -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] - -fn main() { - bos_settings_lib::run() -} diff --git a/src/src/screenshot.rs b/src/src/screenshot.rs deleted file mode 100644 index 36ee0bb..0000000 --- a/src/src/screenshot.rs +++ /dev/null @@ -1,191 +0,0 @@ -//! `--screenshot` CLI mode: switch the Svelte SPA to the named sidebar -//! section, capture it via grim, then exit — driven by -//! `bread-ecosystem`'s `bread-capture` orchestrator, or run standalone for -//! one-off captures. -//! -//! There's no `connect_map`/`glib` signal to hook here the way every other -//! bread-ecosystem app's screenshot mode does, since the window is owned by -//! tao/wry (Tauri's Linux backend), not gtk4-rs directly. Instead this -//! waits a fixed [`INITIAL_SETTLE_DELAY`] on Tauri's own async runtime after -//! `setup()` runs for the page's first paint (JS bundle parse + Svelte -//! mount) — longer than the native apps' settle delays, since a webview's -//! first paint is a full page load, not just GTK widget layout — then emits -//! a `screenshot-set-view` event the frontend listens for -//! (`+page.svelte`'s `onMount`) to switch `activePage` exactly like a real -//! sidebar click would, then waits [`VIEW_SETTLE_DELAY`] more for that -//! view's own data to load (each section fetches its own state over Tauri -//! commands on mount) before capturing. The window itself is a plain, -//! non-layer-shell toplevel (per `tauri.conf.json`'s fixed 960x640 size), -//! so — same reasoning as breadman/breadhelp — a full known-size canvas -//! capture is enough; no geometry to track. -//! -//! View names match `frontend/src/lib/sidebar.ts`'s item ids exactly (see -//! `KNOWN_VIEWS`) — every one of them has a real registered component (see -//! `frontend/src/lib/views/registry.ts`), no Placeholder fallbacks to skip. - -use bread_utils::screenshot_cli::{validate_pair, DEFAULT_HEIGHT, DEFAULT_WIDTH}; -use std::path::{Path, PathBuf}; -use std::time::Duration; -use tauri::Emitter; - -const INITIAL_SETTLE_DELAY: Duration = Duration::from_millis(2000); -/// Applied after switching views — shorter than the initial load (no full -/// page/JS reload, just a component swap + that view's own Tauri-command -/// data fetch), but the About page alone needed 2s for its fetch to land -/// (see the initial-pass commit), so this stays generous rather than -/// re-guessing per view. -const VIEW_SETTLE_DELAY: Duration = Duration::from_millis(2000); - -const KNOWN_VIEWS: &[&str] = &[ - "home", - "network", - "breadcrumbs", - "bluetooth", - "firewall", - "sound", - "power", - "datetime", - "hyprland", - "displays", - "keybinds", - "input", - "autostart", - "users", - "appearance", - "breadpaper", - "breadbar", - "breadbox", - "breadclip", - "breadpad", - "breadsearch", - "bread", - "desktop", - "packages", - "aur", - "firmware", - "snapshots", - "updates", - "system", - "printing", - "vpn", - "nightlight", - "ime", - "accessibility", - "defaults", - "apps", - "channel", - "backup", - "optional", - "breadlock", - "breadshot", - "breadmon", - "breadhelp", - "privacy", - "about", -]; - -pub struct ScreenshotRequest { - pub view: String, - pub output: PathBuf, - pub width: u32, - pub height: u32, -} - -/// `None` for a normal run. Exits the process with an error for an unknown -/// view, or if the `--screenshot` / `--output` pair is incomplete — before -/// any Tauri setup happens. -pub fn parse(args: &[String]) -> Option { - let mut view = None; - let mut output = None; - let mut width = DEFAULT_WIDTH; - let mut height = DEFAULT_HEIGHT; - let mut it = args.iter().skip(1); - while let Some(arg) = it.next() { - match arg.as_str() { - "--screenshot" => view = it.next().cloned(), - "--output" => output = it.next().cloned(), - "--width" => { - if let Some(v) = it.next().and_then(|s| s.parse().ok()) { - width = v; - } - } - "--height" => { - if let Some(v) = it.next().and_then(|s| s.parse().ok()) { - height = v; - } - } - _ => {} - } - } - if let Err(e) = validate_pair(view.as_deref(), output.as_deref().map(Path::new)) { - eprintln!("bos-settings: {e}"); - std::process::exit(1); - } - let view = view?; - if !KNOWN_VIEWS.contains(&view.as_str()) { - eprintln!( - "bos-settings: unknown screenshot view '{view}' (known: {})", - KNOWN_VIEWS.join(", ") - ); - std::process::exit(1); - } - Some(ScreenshotRequest { - view, - output: output?.into(), - width, - height, - }) -} - -/// Schedule the switch-view-then-capture-then-exit sequence. Called once -/// from `setup()`, which is also where `app` (needed to emit the -/// `screenshot-set-view` event) comes from. -pub fn dispatch(req: ScreenshotRequest, app: tauri::AppHandle) { - tauri::async_runtime::spawn(async move { - tokio::time::sleep(INITIAL_SETTLE_DELAY).await; - if let Err(e) = app.emit("screenshot-set-view", &req.view) { - eprintln!("bos-settings: failed to emit screenshot-set-view: {e}"); - std::process::exit(1); - } - tokio::time::sleep(VIEW_SETTLE_DELAY).await; - finish(capture_region( - 0, - 0, - req.width as i32, - req.height as i32, - &req.output, - )); - }); -} - -/// Same contract as bread-screenshots::capture_region. That crate is not -/// on bread-ecosystem v0.7.1 (it landed after the tag), so this stays a -/// local grim -g call rather than a branch-pinned git dep. -fn capture_region(x: i32, y: i32, w: i32, h: i32, out: &std::path::Path) -> anyhow::Result<()> { - if let Some(parent) = out.parent() { - std::fs::create_dir_all(parent)?; - } - let out_str = out - .to_str() - .ok_or_else(|| anyhow::anyhow!("output path is not valid UTF-8"))?; - let geometry = format!("{x},{y} {w}x{h}"); - let result = - bread_utils::proc::run("grim", &["-g", &geometry, out_str], Duration::from_secs(5)); - if !result.success { - anyhow::bail!( - "grim failed for geometry {geometry}: {}", - result.stderr.trim() - ); - } - Ok(()) -} - -fn finish(result: anyhow::Result<()>) { - match result { - Ok(()) => std::process::exit(0), - Err(e) => { - eprintln!("bos-settings: screenshot capture failed: {e}"); - std::process::exit(1); - } - } -} diff --git a/src/tauri.conf.json b/src/tauri.conf.json deleted file mode 100644 index 08f838e..0000000 --- a/src/tauri.conf.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "$schema": "https://schema.tauri.app/config/2", - "productName": "BOS Settings", - "version": "0.8.2", - "identifier": "dev.breadway.bos-settings", - "build": { - "beforeDevCommand": "npm --prefix frontend run dev", - "devUrl": "http://localhost:1420", - "beforeBuildCommand": "npm --prefix frontend run build", - "frontendDist": "../frontend/build" - }, - "app": { - "windows": [ - { - "title": "BOS Settings", - "width": 1280, - "height": 840, - "minWidth": 960, - "minHeight": 640, - "decorations": false, - "backgroundColor": "#12161c" - } - ], - "security": { - "csp": "default-src 'self'; connect-src ipc: http://ipc.localhost https://ipc.localhost; img-src 'self' asset: http://asset.localhost https://asset.localhost data: blob:; style-src 'self' 'unsafe-inline'; font-src 'self' data:; script-src 'self'; object-src 'none'; base-uri 'self'; frame-src 'none'", - "assetProtocol": { - "enable": true, - "scope": ["$HOME/Pictures/Backgrounds/**"] - } - } - }, - "bundle": { - "active": true, - "targets": "all", - "icon": [ - "icons/32x32.png", - "icons/128x128.png", - "icons/128x128@2x.png", - "icons/icon.icns", - "icons/icon.ico" - ] - } -} diff --git a/src/theme.rs b/src/theme.rs new file mode 100644 index 0000000..5b57ca8 --- /dev/null +++ b/src/theme.rs @@ -0,0 +1,56 @@ +//! Theming for bos-settings. +//! +//! bos-settings deliberately owns almost no styling: it loads the ecosystem's +//! shared stylesheet (the same one breadbar/breadbox/breadpad use, generated by +//! `bread-theme` from the pywal palette) and adds only the few layout rules +//! specific to this app's sidebar + content shell. This keeps it visually +//! identical to the rest of the bread desktop and live-recolouring for free. + +use gtk4::CssProvider; +use std::cell::RefCell; + +// App-specific layout only — everything visual (colours, buttons, entries, +// switches, sidebar/row styling, cards, scrollbars) comes from the shared sheet. +const APP_CSS: &str = "\ +.view-content { padding: 24px; }\n\ +.view-content > label.title { margin-bottom: 16px; }\n\ +/* Sidebar row sub-labels (the underlying binary/config name under a row's \ + human label) — smaller than the shared sheet's default dim-label size. */\n\ +.caption { font-size: 11px; }\n\ +/* bread-theme's shared sheet only overrides background-color on \ + suggested/destructive buttons, not background-image — so Adwaita's \ + built-in gradient (bright blue/red) paints over our flat colour \ + underneath it. Belongs upstream in bread-theme; patched locally here \ + until that's worth its own release. */\n\ +button.suggested-action, button.destructive-action { background-image: none; }\n\ +/* Same upstream gap for scale (Sound's volume sliders) — the shared sheet \ + has no `scale` rules at all, so they render in Adwaita's default blue. */\n\ +scale trough { background-color: alpha(@on-surface, 0.15); border-radius: 999px; min-height: 6px; background-image: none; }\n\ +scale highlight { background-color: @accent; background-image: none; border-radius: 999px; }\n\ +scale slider { background-color: @on-surface; border-radius: 999px; }\n\ +/* Destructive actions must not follow the wallpaper palette: @red is \ + pywal's color1, which can land on gold/yellow/anything depending on the \ + wallpaper (it did, this session) — making Delete/Remove look like a \ + primary action instead of a dangerous one. Fixed regardless of palette. */\n\ +button.destructive-action { background-color: #c0392b; color: #ffffff; }\n\ +button.destructive-action:hover { background-color: #d64535; }\n\ +/* Adwaita's default switch slider (the knob) carries a box-shadow used for \ + its 3D bevel look — bread-theme's override only sets background-color, \ + so that shadow still renders as a pale ring around the knob on top of \ + our flat colour. */\n\ +switch slider { box-shadow: none; outline: none; border: none; background-image: none; }\n\ +switch { box-shadow: none; outline: none; border: none; background-image: none; }\n\ +"; + +thread_local! { + static APP_PROVIDER: RefCell> = const { RefCell::new(None) }; +} + +pub fn load(_display: >k4::gdk::Display) { + // Shared ecosystem stylesheet (loads the generated file or a rendered + // fallback, and live-reloads when the palette changes). + bread_theme::gtk::apply_shared(); + + // bos-settings layout, layered on top at APPLICATION priority. + APP_PROVIDER.with(|cell| bread_theme::gtk::apply_css(APP_CSS, cell)); +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs new file mode 100644 index 0000000..3867fcd --- /dev/null +++ b/src/ui/mod.rs @@ -0,0 +1,4 @@ +pub mod sidebar; +pub mod views; +pub mod widgets; +pub mod window; diff --git a/src/ui/sidebar.rs b/src/ui/sidebar.rs new file mode 100644 index 0000000..b4859a7 --- /dev/null +++ b/src/ui/sidebar.rs @@ -0,0 +1,157 @@ +use gtk4::prelude::*; +use gtk4::{Box as GBox, Image, Label, ListBox, ListBoxRow, Orientation}; + +pub struct SidebarItem { + /// Must match the `Stack` page name registered in `window.rs`. + pub id: &'static str, + pub label: &'static str, + /// Dim second line — the underlying binary/config name, for items whose + /// human label doesn't already make that obvious. + pub sublabel: Option<&'static str>, + /// A `-symbolic` icon name from the system icon theme (Papirus-Dark ships + /// the full Adwaita-compatible symbolic set this app relies on). + pub icon: &'static str, +} + +const fn item(id: &'static str, label: &'static str, icon: &'static str) -> SidebarItem { + SidebarItem { id, label, sublabel: None, icon } +} + +const fn item_sub( + id: &'static str, + label: &'static str, + sublabel: &'static str, + icon: &'static str, +) -> SidebarItem { + SidebarItem { id, label, sublabel: Some(sublabel), icon } +} + +// Grouped by task, not by "app vs system internals" — a user thinks "I want +// to change my Wi-Fi" or "I want to change my wallpaper", not "which of +// these is a bread-ecosystem app". breadcrumbs (Wi-Fi profiles) moves out of +// the old "Apps" bucket into System for the same reason. +pub const SYSTEM_ITEMS: &[SidebarItem] = &[ + item("network", "Network", "network-wireless-symbolic"), + item_sub("breadcrumbs", "Wi-Fi Profiles", "breadcrumbs", "network-workgroup-symbolic"), + item("bluetooth", "Bluetooth", "bluetooth-symbolic"), + item("firewall", "Firewall", "security-high-symbolic"), + item("sound", "Sound", "audio-volume-high-symbolic"), + item("power", "Power", "battery-good-symbolic"), + item("datetime", "Date & Time", "preferences-system-time-symbolic"), + item_sub("hyprland", "Display", "monitors.json", "video-display-symbolic"), + item_sub("keybinds", "Keybinds", "binds.json", "input-keyboard-symbolic"), + item_sub("autostart", "Startup Apps", "autostart.json", "system-run-symbolic"), + item("users", "Users", "system-users-symbolic"), +]; + +pub const PERSONALIZATION_ITEMS: &[SidebarItem] = &[ + item_sub("appearance", "Appearance", "settings.json", "applications-graphics-symbolic"), + item_sub("breadpaper", "Wallpaper", "breadpaper", "preferences-desktop-wallpaper-symbolic"), + item_sub("breadbar", "Bar", "breadbar", "view-grid-symbolic"), + item_sub("breadbox", "Launcher", "breadbox", "view-app-grid-symbolic"), + item_sub("breadclip", "Clipboard", "breadclipd", "edit-paste-symbolic"), + item_sub("breadpad", "Notes", "breadpad", "text-editor-symbolic"), + item_sub("breadsearch", "File Search", "breadsearch", "system-search-symbolic"), + item_sub("bread", "Daemon", "breadd", "applications-system-symbolic"), +]; + +pub const MAINTENANCE_ITEMS: &[SidebarItem] = &[ + item("packages", "Packages", "package-x-generic-symbolic"), + item("aur", "AUR", "system-search-symbolic"), + item("firmware", "Firmware", "software-update-available-symbolic"), + item("snapshots", "Snapshots", "document-open-recent-symbolic"), +]; + +pub const ABOUT_ITEMS: &[SidebarItem] = &[item("about", "About", "help-about-symbolic")]; + +/// `default_id` must match whatever page `window.rs` sets as the `Stack`'s +/// initial visible child — previously these were two independent hardcoded +/// "about" literals in different files with no link between them, so +/// changing one without the other silently desynced the sidebar highlight +/// from the actually-displayed page. +pub fn build(default_id: &str) -> (GBox, ListBox) { + let vbox = GBox::new(Orientation::Vertical, 0); + vbox.add_css_class("sidebar"); + vbox.set_width_request(210); + + let list = ListBox::new(); + list.set_selection_mode(gtk4::SelectionMode::Single); + list.add_css_class("sidebar"); + + append_section(&list, "System", SYSTEM_ITEMS); + append_section(&list, "Personalization", PERSONALIZATION_ITEMS); + append_section(&list, "Maintenance", MAINTENANCE_ITEMS); + append_section(&list, None, ABOUT_ITEMS); + + let mut i = 0; + loop { + match list.row_at_index(i) { + None => break, + Some(row) if row.widget_name() == default_id => { + list.select_row(Some(&row)); + break; + } + _ => i += 1, + } + } + + // Unscrolled, this list's ~30 rows (4 sections' worth of items plus + // headers) force a minimum window height that exceeds the *logical* + // screen height at Hyprland scale factors above 1.0 (e.g. a 1200px-tall + // panel becomes 800 logical px at scale 1.5) — GTK can't shrink below a + // widget's natural size, so the window (and anything below the fold, + // like a panel's Save button) gets clipped by the compositor with no way + // to reach it. Scrolling the nav list independently of the content pane + // lets the window's minimum height drop to whatever a single row needs. + let scroll = gtk4::ScrolledWindow::new(); + scroll.set_vexpand(true); + scroll.set_hscrollbar_policy(gtk4::PolicyType::Never); + scroll.set_child(Some(&list)); + vbox.append(&scroll); + (vbox, list) +} + +fn append_section(list: &ListBox, title: impl Into>, items: &[SidebarItem]) { + if let Some(title) = title.into() { + let header_row = ListBoxRow::new(); + header_row.set_selectable(false); + header_row.set_activatable(false); + let header_lbl = Label::new(Some(title)); + header_lbl.add_css_class("section-header"); + header_lbl.set_xalign(0.0); + header_row.set_child(Some(&header_lbl)); + list.append(&header_row); + } + + for entry in items { + let row = ListBoxRow::new(); + row.set_widget_name(entry.id); + + let hbox = GBox::new(Orientation::Horizontal, 10); + hbox.set_margin_top(4); + hbox.set_margin_bottom(4); + + let icon = Image::from_icon_name(entry.icon); + icon.set_pixel_size(16); + hbox.append(&icon); + + let labels = GBox::new(Orientation::Vertical, 0); + let lbl = Label::new(Some(entry.label)); + lbl.set_xalign(0.0); + labels.append(&lbl); + if let Some(sub) = entry.sublabel { + let sub_lbl = Label::new(Some(sub)); + sub_lbl.add_css_class("dim-label"); + sub_lbl.set_xalign(0.0); + // Match the sidebar's smaller "section-header" scale rather than + // the shared sheet's default dim-label size, so it reads as a + // caption under the row label, not a second full-size label. + sub_lbl.add_css_class("caption"); + labels.append(&sub_lbl); + } + hbox.append(&labels); + + row.set_child(Some(&hbox)); + list.append(&row); + } +} diff --git a/src/ui/views/about.rs b/src/ui/views/about.rs new file mode 100644 index 0000000..8f4cdcb --- /dev/null +++ b/src/ui/views/about.rs @@ -0,0 +1,172 @@ +//! Read-only system info, plus the one thing worth making writable: hostname. +//! BOS is a rolling release (no fixed version number to show — `os-release` +//! ships `BUILD_ID=rolling` on purpose), so there's no "BOS 1.2.3" readout +//! here the way a point-release distro's About panel would have one. + +use gtk4::prelude::*; +use gtk4::{Box as GBox, Button, Entry, Label, Orientation}; +use std::fs; +use std::process::Command; + +use crate::ui::widgets as w; + +fn os_pretty_name() -> String { + fs::read_to_string("/etc/os-release") + .ok() + .and_then(|s| { + s.lines() + .find_map(|l| l.strip_prefix("PRETTY_NAME=").map(|v| v.trim_matches('"').to_string())) + }) + .unwrap_or_else(|| "BOS".to_string()) +} + +fn hostname() -> String { + fs::read_to_string("/etc/hostname") + .map(|s| s.trim().to_string()) + .unwrap_or_else(|_| "unknown".to_string()) +} + +fn kernel() -> String { + Command::new("uname") + .arg("-r") + .output() + .ok() + .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) + .unwrap_or_else(|| "unknown".to_string()) +} + +fn cpu() -> String { + let model = fs::read_to_string("/proc/cpuinfo") + .ok() + .and_then(|s| { + s.lines() + .find_map(|l| l.strip_prefix("model name").map(|v| v.trim_start_matches([':', ' ', '\t']).to_string())) + }) + .unwrap_or_else(|| "unknown".to_string()); + let cores = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(0); + if cores > 0 { + format!("{model} ({cores} threads)") + } else { + model + } +} + +fn memory() -> String { + let kb = fs::read_to_string("/proc/meminfo") + .ok() + .and_then(|s| { + s.lines() + .find(|l| l.starts_with("MemTotal:")) + .and_then(|l| l.split_whitespace().nth(1)) + .and_then(|v| v.parse::().ok()) + }); + match kb { + Some(kb) => format!("{:.1} GiB", kb as f64 / 1024.0 / 1024.0), + None => "unknown".to_string(), + } +} + +fn gpu() -> String { + let Ok(output) = Command::new("lspci").output() else { + return "unknown".to_string(); + }; + let text = String::from_utf8_lossy(&output.stdout); + text.lines() + // "Display controller" covers integrated GPUs some laptop chipsets + // (this dev laptop's AMD Radeon 860M included) report under instead + // of "VGA compatible controller" — without it those show "unknown". + .find(|l| { + l.contains("VGA compatible controller") + || l.contains("3D controller") + || l.contains("Display controller") + }) + .and_then(|l| l.split(": ").nth(1)) + .unwrap_or("unknown") + .to_string() +} + +fn disk_usage() -> String { + let Ok(output) = Command::new("df").args(["-h", "--output=used,size,pcent", "/"]).output() else { + return "unknown".to_string(); + }; + let text = String::from_utf8_lossy(&output.stdout); + text.lines() + .nth(1) + .map(|l| { + let cols: Vec<&str> = l.split_whitespace().collect(); + match cols.as_slice() { + [used, size, pcent] => format!("{used} of {size} used ({pcent})"), + _ => l.trim().to_string(), + } + }) + .unwrap_or_else(|| "unknown".to_string()) +} + +fn uptime() -> String { + Command::new("uptime") + .arg("-p") + .output() + .ok() + .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) + .unwrap_or_else(|| "unknown".to_string()) +} + +pub fn build() -> GBox { + let (outer, content) = w::view_scaffold("About"); + + content.append(&w::info_row("Operating system", &os_pretty_name())); + content.append(&w::info_row("Kernel", &kernel())); + content.append(&w::info_row("CPU", &cpu())); + content.append(&w::info_row("GPU", &gpu())); + content.append(&w::info_row("Memory", &memory())); + content.append(&w::info_row("Disk (/)", &disk_usage())); + content.append(&w::info_row("Uptime", &uptime())); + + content.append(&w::section("Hostname")); + content.append(&w::hint( + "Changes the machine's network name. Takes effect immediately; \ + needs your password (polkit).", + )); + + let hn_row = GBox::new(Orientation::Horizontal, 12); + let entry = Entry::new(); + entry.set_text(&hostname()); + entry.set_hexpand(true); + let apply_btn = Button::with_label("Apply"); + let status = Label::new(None); + status.add_css_class("dim-label"); + + { + let entry = entry.clone(); + let status = status.clone(); + apply_btn.connect_clicked(move |_| { + let name = entry.text().to_string(); + if name.trim().is_empty() { + status.set_text("Hostname can't be empty"); + return; + } + let log_buf = gtk4::TextBuffer::new(None); + let status2 = status.clone(); + status.set_text("Applying…"); + w::stream_command_then( + &["pkexec", "hostnamectl", "set-hostname", name.trim()], + log_buf.clone(), + move || { + let text = log_buf.text(&log_buf.start_iter(), &log_buf.end_iter(), false); + if text.trim().is_empty() { + status2.set_text("Applied"); + } else { + status2.set_text(&format!("Error: {}", text.trim())); + } + }, + ); + }); + } + + hn_row.append(&entry); + hn_row.append(&apply_btn); + content.append(&hn_row); + content.append(&status); + + outer +} diff --git a/src/ui/views/appearance.rs b/src/ui/views/appearance.rs new file mode 100644 index 0000000..419ac94 --- /dev/null +++ b/src/ui/views/appearance.rs @@ -0,0 +1,221 @@ +//! hypr/settings.json — Hyprland gaps/borders/blur/shadow/input, read by +//! `scripts/ui/settings.lua` on the Hyprland side (see hyprland.lua). Unlike +//! the TOML-backed views, this has no comments to preserve, so it's modeled +//! as a plain typed struct (round-tripped whole) rather than the +//! `toml_edit`/`Doc` path-based editor the other panels use. +//! +//! `Default` here must stay in sync with `scripts/ui/settings.lua`'s +//! `DEFAULTS` table on the Hyprland side — both independently define "what +//! BOS ships out of the box," the same duplication `binds.json`/ +//! `content/keybinds.rs` already accept for the same reason (two different +//! languages/processes reading the same file, neither able to import the +//! other's defaults). + +use std::cell::RefCell; +use std::rc::Rc; + +use gtk4::prelude::*; +use gtk4::{ + Adjustment, Box as GBox, ColorDialog, ColorDialogButton, DropDown, Entry, Expression, Orientation, + SpinButton, StringList, Switch, +}; +use serde::{Deserialize, Serialize}; + +use crate::ui::widgets as w; + +#[derive(Clone, Serialize, Deserialize)] +#[serde(default)] +struct Appearance { + gaps_in: i64, + gaps_out: i64, + border_size: i64, + active_border: String, + inactive_border: String, + layout: String, + resize_on_border: bool, + rounding: i64, + blur_enabled: bool, + blur_size: i64, + blur_passes: i64, + shadow_enabled: bool, + shadow_range: i64, + shadow_render_power: i64, + kb_layout: String, + follow_mouse: i64, + natural_scroll: bool, +} + +impl Default for Appearance { + fn default() -> Self { + Self { + gaps_in: 5, + gaps_out: 10, + border_size: 2, + active_border: "rgba(88c0d0ff)".to_string(), + inactive_border: "rgba(4c566aff)".to_string(), + layout: "dwindle".to_string(), + resize_on_border: true, + rounding: 8, + blur_enabled: true, + blur_size: 6, + blur_passes: 2, + shadow_enabled: true, + shadow_range: 12, + shadow_render_power: 3, + kb_layout: "us".to_string(), + follow_mouse: 1, + natural_scroll: true, + } + } +} + +fn config_path() -> std::path::PathBuf { + crate::config::config_dir().join("hypr/settings.json") +} + +/// A missing or malformed file yields defaults — same failsafe posture as +/// the Lua loader reading this same file on the Hyprland side. +fn load() -> Appearance { + std::fs::read_to_string(config_path()).ok().and_then(|s| serde_json::from_str(&s).ok()).unwrap_or_default() +} + +fn save(a: &Appearance) -> std::io::Result<()> { + let path = config_path(); + crate::config::atomic_write(&path, &serde_json::to_string_pretty(a).unwrap_or_default()) +} + +/// "rgba(RRGGBBAA)" (Hyprland's format) <-> gdk::RGBA, so the color fields +/// get a real color-picker button instead of a raw hex text field. +fn parse_hypr_color(s: &str) -> Option { + let inner = s.strip_prefix("rgba(")?.strip_suffix(')')?; + if inner.len() != 8 { + return None; + } + let r = u8::from_str_radix(&inner[0..2], 16).ok()?; + let g = u8::from_str_radix(&inner[2..4], 16).ok()?; + let b = u8::from_str_radix(&inner[4..6], 16).ok()?; + let a = u8::from_str_radix(&inner[6..8], 16).ok()?; + Some(gtk4::gdk::RGBA::new(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, a as f32 / 255.0)) +} + +fn to_hypr_color(c: >k4::gdk::RGBA) -> String { + let clamp = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u8; + format!("rgba({:02x}{:02x}{:02x}{:02x})", clamp(c.red()), clamp(c.green()), clamp(c.blue()), clamp(c.alpha())) +} + +fn color_row(label: &str, model: &Rc>, get: fn(&Appearance) -> &str, set: fn(&mut Appearance, String)) -> GBox { + let cur = parse_hypr_color(get(&model.borrow())).unwrap_or(gtk4::gdk::RGBA::new(0.5, 0.5, 0.5, 1.0)); + let btn = ColorDialogButton::new(Some(ColorDialog::builder().with_alpha(true).build())); + btn.set_rgba(&cur); + let model = model.clone(); + btn.connect_rgba_notify(move |b| { + set(&mut model.borrow_mut(), to_hypr_color(&b.rgba())); + }); + w::row(label, &btn) +} + +fn spin_row(label: &str, model: &Rc>, min: f64, max: f64, get: fn(&Appearance) -> i64, set: fn(&mut Appearance, i64)) -> GBox { + let cur = get(&model.borrow()); + let adj = Adjustment::new(cur as f64, min, max, 1.0, 1.0, 0.0); + let spin = SpinButton::new(Some(&adj), 1.0, 0); + let model = model.clone(); + spin.connect_value_changed(move |s| set(&mut model.borrow_mut(), s.value() as i64)); + w::row(label, &spin) +} + +fn switch_row(label: &str, model: &Rc>, get: fn(&Appearance) -> bool, set: fn(&mut Appearance, bool)) -> GBox { + let sw = Switch::new(); + sw.set_active(get(&model.borrow())); + let model = model.clone(); + sw.connect_active_notify(move |s| set(&mut model.borrow_mut(), s.is_active())); + w::row(label, &sw) +} + +fn entry_row(label: &str, model: &Rc>, get: fn(&Appearance) -> String, set: fn(&mut Appearance, String)) -> GBox { + let entry = Entry::new(); + entry.set_text(&get(&model.borrow())); + entry.set_hexpand(true); + entry.set_width_chars(16); + let model = model.clone(); + entry.connect_changed(move |e| set(&mut model.borrow_mut(), e.text().to_string())); + w::row(label, &entry) +} + +fn dropdown_row(label: &str, model: &Rc>, options: &[&str], get: fn(&Appearance) -> &str, set: fn(&mut Appearance, String)) -> GBox { + let cur = get(&model.borrow()).to_string(); + let dd = DropDown::new(Some(StringList::new(options)), Expression::NONE); + dd.set_selected(options.iter().position(|o| *o == cur).unwrap_or(0) as u32); + let owned: Vec = options.iter().map(|s| s.to_string()).collect(); + let model = model.clone(); + dd.connect_selected_notify(move |dd| { + if let Some(opt) = owned.get(dd.selected() as usize) { + set(&mut model.borrow_mut(), opt.clone()); + } + }); + w::row(label, &dd) +} + +pub fn build() -> GBox { + let (outer, content) = w::view_scaffold("Appearance"); + + content.append(&w::hint( + "Gaps, borders, blur, and input feel for Hyprland — the same \ + settings.json the compositor itself reads at login.", + )); + + let model = Rc::new(RefCell::new(load())); + + content.append(&w::section("Layout & borders")); + content.append(&spin_row("Gaps (inner)", &model, 0.0, 50.0, |a| a.gaps_in, |a, v| a.gaps_in = v)); + content.append(&spin_row("Gaps (outer)", &model, 0.0, 50.0, |a| a.gaps_out, |a, v| a.gaps_out = v)); + content.append(&spin_row("Border width", &model, 0.0, 10.0, |a| a.border_size, |a, v| a.border_size = v)); + content.append(&color_row("Active border color", &model, |a| &a.active_border, |a, v| a.active_border = v)); + content.append(&color_row("Inactive border color", &model, |a| &a.inactive_border, |a, v| a.inactive_border = v)); + content.append(&dropdown_row("Tiling layout", &model, &["dwindle", "master"], |a| &a.layout, |a, v| a.layout = v)); + content.append(&switch_row("Resize by dragging borders", &model, |a| a.resize_on_border, |a, v| a.resize_on_border = v)); + + content.append(&w::section("Effects")); + content.append(&spin_row("Corner rounding", &model, 0.0, 30.0, |a| a.rounding, |a, v| a.rounding = v)); + content.append(&switch_row("Blur", &model, |a| a.blur_enabled, |a, v| a.blur_enabled = v)); + content.append(&spin_row("Blur size", &model, 0.0, 20.0, |a| a.blur_size, |a, v| a.blur_size = v)); + content.append(&spin_row("Blur passes", &model, 1.0, 5.0, |a| a.blur_passes, |a, v| a.blur_passes = v)); + content.append(&switch_row("Window shadows", &model, |a| a.shadow_enabled, |a, v| a.shadow_enabled = v)); + content.append(&spin_row("Shadow range", &model, 0.0, 40.0, |a| a.shadow_range, |a, v| a.shadow_range = v)); + content.append(&spin_row("Shadow render power", &model, 1.0, 4.0, |a| a.shadow_render_power, |a, v| a.shadow_render_power = v)); + + content.append(&w::section("Input")); + content.append(&entry_row("Keyboard layout", &model, |a| a.kb_layout.clone(), |a, v| a.kb_layout = v)); + let follow_mouse_row = spin_row("Focus-follows-mouse mode", &model, 0.0, 3.0, |a| a.follow_mouse, |a, v| a.follow_mouse = v); + content.append(&follow_mouse_row); + content.append(&w::hint("0-3 — see the Hyprland wiki's follow_mouse setting for exact behavior of each value.")); + content.append(&switch_row("Natural scrolling (touchpad)", &model, |a| a.natural_scroll, |a, v| a.natural_scroll = v)); + + content.append(&w::hint("Applies on next login/Hyprland reload — this saves settings.json, it doesn't reload Hyprland live.")); + + let btn_row = GBox::new(Orientation::Horizontal, 12); + btn_row.set_margin_top(16); + let save_btn = gtk4::Button::with_label("Save"); + save_btn.add_css_class("suggested-action"); + let status = gtk4::Label::new(None); + status.add_css_class("dim-label"); + { + let model = model.clone(); + let status = status.clone(); + save_btn.connect_clicked(move |_| match save(&model.borrow()) { + Ok(()) => { + status.set_text("Saved"); + let lbl = status.clone(); + glib::timeout_add_seconds_local(3, move || { + lbl.set_text(""); + glib::ControlFlow::Break + }); + } + Err(e) => status.set_text(&format!("Error: {e}")), + }); + } + btn_row.append(&save_btn); + btn_row.append(&status); + outer.append(&btn_row); + + outer +} diff --git a/src/ui/views/aur.rs b/src/ui/views/aur.rs new file mode 100644 index 0000000..b526224 --- /dev/null +++ b/src/ui/views/aur.rs @@ -0,0 +1,169 @@ +//! AUR search via yay — graphical discovery for the wider AUR beyond +//! bakery's bread ecosystem and [breadway]'s own republished packages. +//! +//! Search and browsing are fully graphical; actually installing a package +//! opens a terminal running `yay -S ` instead of a silent `--noconfirm` +//! install. That's deliberate, not a shortcut we didn't get around to: +//! AUR packages run arbitrary maintainer-supplied build scripts, and yay's +//! interactive PKGBUILD diff review (plus the sudo password prompt) is the +//! actual safety mechanism against a malicious/compromised AUR package — +//! automating it away in the name of "no terminal" would remove the one +//! step that exists to catch that. + +use gtk4::prelude::*; +use gtk4::{Box as GBox, Button, Entry, Label, ListBox, ListBoxRow, Orientation, ScrolledWindow}; +use std::process::Command; + +use crate::ui::widgets as w; + +#[derive(Clone)] +struct AurResult { + name: String, + version: String, + description: String, +} + +fn search(query: &str) -> Vec { + let Ok(output) = Command::new("yay").args(["-Ss", "--aur", query]).output() else { + return Vec::new(); + }; + let text = String::from_utf8_lossy(&output.stdout); + let mut results = Vec::new(); + let mut lines = text.lines().peekable(); + while let Some(header) = lines.next() { + // "aur/name version (+votes score) [Orphaned]" — name/version are + // always the first two whitespace-separated fields after the repo/. + let Some(rest) = header.strip_prefix("aur/") else { continue }; + let mut parts = rest.split_whitespace(); + let Some(name) = parts.next() else { continue }; + let version = parts.next().unwrap_or("").to_string(); + let description = lines.next().unwrap_or("").trim().to_string(); + results.push(AurResult { name: name.to_string(), version, description }); + } + results +} + +fn install_in_terminal(pkg: &str) { + let _ = Command::new("kitty").args(["-e", "yay", "-S", pkg]).spawn(); +} + +pub fn build() -> GBox { + let (outer, content) = w::view_scaffold("AUR"); + content.append(&w::hint( + "Search the Arch User Repository via yay. Installing opens a \ + terminal — AUR packages run arbitrary build scripts, and reviewing \ + what yay is about to do (and entering your password) is a real \ + safety step, not just a formality.", + )); + + let search_row = GBox::new(Orientation::Horizontal, 8); + let search_entry = Entry::new(); + search_entry.set_hexpand(true); + search_entry.set_placeholder_text(Some("Search the AUR…")); + let search_btn = Button::with_label("Search"); + search_btn.add_css_class("suggested-action"); + search_row.append(&search_entry); + search_row.append(&search_btn); + content.append(&search_row); + + let status = w::hint("Search for a package to see results here."); + content.append(&status); + + let list = ListBox::new(); + list.set_selection_mode(gtk4::SelectionMode::None); + let scroll = ScrolledWindow::new(); + scroll.set_vexpand(true); + scroll.set_min_content_height(320); + scroll.set_child(Some(&list)); + content.append(&scroll); + + let run_search = { + let list = list.clone(); + let status = status.clone(); + let search_entry = search_entry.clone(); + move |btn: Option