diff --git a/.forgejo/workflows/calamares.yml b/.forgejo/workflows/calamares.yml index 26b90b0..80637f9 100644 --- a/.forgejo/workflows/calamares.yml +++ b/.forgejo/workflows/calamares.yml @@ -24,12 +24,7 @@ jobs: kcoreaddons kpmcore libpwquality qt6-declarative qt6-svg yaml-cpp useradd -m builder git config --global --add safe.directory '*' - # Clone the branch/tag that triggered this run (not the default - # branch) — same as bibata.yml/powerlevel10k.yml/yay-bin.yml, so a - # push to a feature branch (or a release tag) builds and publishes - # from that ref, not whatever happens to be on the default branch. - git clone --depth 1 --branch "${GITHUB_REF_NAME}" \ - "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" /home/builder/src + git clone --depth 1 "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" /home/builder/src chown -R builder:builder /home/builder/src su builder -c "cd /home/builder/src/packaging/calamares && makepkg -f --noconfirm --nocheck" PKG=$(find /home/builder/src/packaging/calamares -name '*.pkg.tar.zst' | head -1) diff --git a/.forgejo/workflows/mirror.yml b/.forgejo/workflows/mirror.yml new file mode 100644 index 0000000..6a2f9ab --- /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.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..aaf7eb7 --- /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/arch/bos-settings-${VERSION}.tar.gz + SHA=$(sha256sum packaging/arch/bos-settings-${VERSION}.tar.gz | awk '{print $1}') + sed -i "s/^pkgver=.*/pkgver=${VERSION}/" packaging/arch/PKGBUILD + sed -i "s/^sha256sums=.*/sha256sums=('${SHA}')/" packaging/arch/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/arch && makepkg -f --noconfirm --nocheck" + PKG=$(find /home/builder/src/packaging/arch -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/python-pywal.yml b/.forgejo/workflows/python-pywal.yml deleted file mode 100644 index f5f7d61..0000000 --- a/.forgejo/workflows/python-pywal.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Build and publish python-pywal - -# python-pywal was dropped from Arch's [extra] repo (AUR-only now), but the ISO -# needs the `wal` binary (bread-theme extracts the wallpaper palette with it). -# BOS keeps an in-house PKGBUILD and publishes to the [breadway] repo — same -# pattern as calamares / bibata / powerlevel10k / yay-bin. -on: - push: - paths: - - 'packaging/python-pywal/**' - workflow_dispatch: - -jobs: - python-pywal: - runs-on: [self-hosted, hestia] - container: - image: archlinux:latest - steps: - - name: Build and publish - env: - PUBLISH_TOKEN: ${{ secrets.REGISTRY_TOKEN }} - run: | - set -euo pipefail - pacman -Syu --noconfirm base-devel git \ - python python-build python-installer python-wheel python-setuptools imagemagick - useradd -m builder - git config --global --add safe.directory '*' - # Clone the ref that triggered this run (not the default branch) — - # same as the other packaging workflows. - git clone --depth 1 --branch "${GITHUB_REF_NAME}" \ - "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" /home/builder/src - chown -R builder:builder /home/builder/src - su builder -c "cd /home/builder/src/packaging/python-pywal && makepkg -f --noconfirm" - PKG=$(find /home/builder/src/packaging/python-pywal -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/release-iso.yml b/.forgejo/workflows/release-iso.yml index 162fa16..27c2a34 100644 --- a/.forgejo/workflows/release-iso.yml +++ b/.forgejo/workflows/release-iso.yml @@ -1,21 +1,19 @@ name: Build and release ISO -# Builds the BOS ISO on the hestia self-hosted runner (native Arch container). -# Stages bakery desktop apps from the *minisign-verified* stable index at -# https://dl.breadway.dev/index.json (see iso/bread-lockfile.toml), then runs -# build-local.sh and uploads the ISO to a Forgejo release. A matching GitHub -# release is created best-effort and points at Forgejo for the download +# Builds the BOS ISO on the hestia self-hosted runner (native Arch container), +# downloads all bakery ecosystem binaries from their GitHub releases, compiles +# bread-theme from source, and uploads the resulting ISO to a Forgejo pre-release. +# A matching GitHub release is created that points to Forgejo for the download # (GitHub releases cannot host files larger than 2 GB). # # Required secrets: # RELEASE_TOKEN — Forgejo API token with write:repository scope -# MIRROR_TOKEN — GitHub personal access token with repo scope +# MIRROR_TOKEN — GitHub personal access token with repo scope (already used by mirror.yml) # GPG_PRIVATE_KEY — armoured secret key for the dedicated "BOS Release Signing" # identity (releases@breadway.dev); public half is committed -# at KEYS.asc. Signs ISO SHA256SUMS here; the same secret -# signs the [breadway] repo in signed-repo.yml. No passphrase -# (CI-only key, access controlled via the Forgejo secret -# store). +# at KEYS.asc for verification. No passphrase (CI-only key, +# access controlled via the Forgejo secret store, not a +# passphrase nobody could type non-interactively anyway). on: push: @@ -30,8 +28,6 @@ jobs: release-iso: runs-on: [self-hosted, hestia] container: - # Floating tag: this environment cannot pin a reproducible digest of - # archlinux:latest. Do not invent one. image: archlinux:latest # --privileged: mkarchiso needs CAP_SYS_ADMIN for loop mounts + mknod # --network=host: gives localhost:3002 access to Forgejo (avoids the @@ -41,10 +37,7 @@ jobs: steps: - name: Install build dependencies run: | - # grub is required by profiledef.sh bootmodes=('uefi.grub'): - # mkarchiso validates grub-install on the *builder*, not the image. - # archiso pulls syslinux/squashfs-tools/libisoburn; it does not pull grub. - pacman -Syu --noconfirm archiso grub curl python git minisign + pacman -Syu --noconfirm archiso curl python git rust - name: Determine tag and version id: vars @@ -62,17 +55,72 @@ jobs: git clone --branch "${{ steps.vars.outputs.tag }}" --depth 1 \ "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" /bos - - name: Stage bakery ecosystem from signed stable index + - name: Download bakery ecosystem binaries run: | set -euo pipefail - cd /bos - LAPTOP_HOME=/build-home python3 scripts/ci-stage-bakery.py + mkdir -p /build-home/.local/bin \ + /build-home/.local/state/bakery \ + /build-home/.cache/bakery - - name: Verify staged bakery bake inputs + # Fetch the canonical bakery index + curl -fsSL "https://dl.breadway.dev/index.json" \ + -o /build-home/.cache/bakery/index.json + + # Download each binary from dl.breadway.dev (canonical source; github_url + # is not always published for dev/patch releases) and generate the + # installed.json that bakery expects in ~/.local/state. + python3 << 'PYEOF' + import json, urllib.request, os + + with open('/build-home/.cache/bakery/index.json') as f: + idx = json.load(f) + + BIN_DIR = '/build-home/.local/bin' + installed = {} + + for pkg_name, pkg in idx['packages'].items(): + bins = [] + for b in pkg['binaries']: + dest_name = b['name'].removesuffix('-x86_64') + dest = os.path.join(BIN_DIR, dest_name) + url = b['dl_url'] + print(f' {dest_name} <- {url}', flush=True) + urllib.request.urlretrieve(url, dest) + os.chmod(dest, 0o755) + bins.append(dest_name) + + # installed.json services field is a flat list of unit-name strings + services = [ + (s['unit'] if isinstance(s, dict) else s) + for s in pkg.get('services', []) + ] + installed[pkg_name] = { + 'name': pkg_name, + 'version': pkg['version'], + 'binaries': bins, + 'services': services, + 'installed_at': '2024-01-01T00:00:00+00:00', + } + + with open('/build-home/.local/state/bakery/installed.json', 'w') as f: + json.dump({'packages': installed}, f, indent=2) + print('installed.json written', flush=True) + PYEOF + + - name: Build bread-theme from source run: | set -euo pipefail - cd /bos - LAPTOP_HOME=/build-home bash scripts/ci-verify-bake.sh + # bread-theme is not in the bakery index; build it at the tag pinned + # in bos-settings/Cargo.toml so the CLI matches the library version. + THEME_TAG=$(grep 'bread-theme.*tag' /bos/bos-settings/Cargo.toml \ + | grep -oP '"v[^"]+"' | tr -d '"') + echo "Building bread-theme @ $THEME_TAG" + git clone --branch "$THEME_TAG" --depth 1 \ + https://github.com/Breadway/bread-ecosystem /bread-ecosystem + cd /bread-ecosystem + cargo build --release -p bread-theme + install -m 755 target/release/bread-theme /build-home/.local/bin/bread-theme + echo "bread-theme built OK" - name: Build ISO run: | @@ -186,21 +234,6 @@ jobs: gh release create "${TAG}" \ --repo "Breadway/bos" \ --title "BOS ${TAG}" \ + \ --notes-file /tmp/gh-release-notes.md \ - || echo "skip: GitHub release failed (MIRROR_TOKEN historically broken)" - - # `stable` is a marker branch only — CI fast-forwards it to whatever - # commit the latest real (non-RC) release tag points at. Never merged - # into by hand, so unlike the old dev/beta/main model it can't rot: - # nobody has to remember to move it, a bot always does. Lets you - # `git diff stable..main` before a build to see what's new since the - # last release, without a human-maintained promotion step. - - name: Fast-forward stable branch to this tag - if: ${{ !contains(steps.vars.outputs.tag, '-rc.') }} - env: - FORGEJO_TOKEN: ${{ secrets.RELEASE_TOKEN }} - run: | - set -euo pipefail - cd /bos - git push "https://oauth2:${FORGEJO_TOKEN}@git.breadway.dev/${GITHUB_REPOSITORY}.git" \ - "HEAD:refs/heads/stable" --force + 2>/dev/null || echo "GitHub release already exists — skipping" diff --git a/.forgejo/workflows/signed-repo.yml b/.forgejo/workflows/signed-repo.yml deleted file mode 100644 index caa5d15..0000000 --- a/.forgejo/workflows/signed-repo.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: Publish signed [breadway] repo - -# Host job on hestia (no container:) so it can write /srv/breadway-dl, same -# as bakery releases. breadlock package.yml uses archlinux:latest and cannot -# see host /srv — do not add container: here. -# -# Collects breadlock + the ISO AUR republishes from the Forgejo Arch -# registry, detach-signs each .pkg.tar.zst, repo-add -s, publishes -# https://dl.breadway.dev/arch/x86_64/. Does not PUT to the registry -# (existing packaging workflows keep doing that). Does not flip ISO SigLevel. -# -# Required secret: GPG_PRIVATE_KEY (same BOS release key as release-iso.yml). - -on: - workflow_dispatch: - repository_dispatch: - types: [publish-signed-repo] - workflow_run: - workflows: - - Build and publish calamares - - Build and publish bibata-cursor-theme - - Build and publish powerlevel10k - - Build and publish yay-bin - - Build and publish python-pywal - types: [completed] - -concurrency: - group: signed-repo - cancel-in-progress: false - -jobs: - publish: - if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} - runs-on: [self-hosted, hestia] - steps: - - name: Clone repository - run: | - set -euo pipefail - REF="${GITHUB_REF_NAME:-main}" - rm -rf src - git clone --depth 1 --branch "$REF" \ - "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src - - - name: Sign packages and publish repo - env: - GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} - run: | - set -euo pipefail - if [ -z "${GPG_PRIVATE_KEY:-}" ]; then - echo "GPG_PRIVATE_KEY secret is missing; refusing to publish an unsigned [breadway] repo." >&2 - exit 1 - fi - bash src/scripts/ci-publish-signed-repo.sh diff --git a/.gitignore b/.gitignore index 6e38d88..25b3b3b 100644 --- a/.gitignore +++ b/.gitignore @@ -42,10 +42,3 @@ logs/ # Wallpaper source drop (baked copy lives in airootfs/usr/share/backgrounds) /Bread Background.png - -# Local hygiene notes (not for commit) -CLAUDE.md - -# Python -__pycache__/ -*.pyc diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 8019d5e..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,58 +0,0 @@ -# AGENTS.md — Repo hygiene - -Scope: this file covers *repo hygiene* — branching, remotes, CI, cleanup. It is not project documentation. Product shape is [README.md](README.md). - -## What this repo is - -ISO + Calamares + skel. **No Cargo workspace. No `bos-settings/` member.** -bos-settings (Tauri 2 + Svelte) and breadhelp are standalone bakery -products. breadlock is the only bread\* pacman package. - -Live skel is `iso/airootfs/etc/skel`. `dotfiles/` is stale. - -## Branch model - -Single-trunk: - -- `main` — integration + release trunk. Land work via short-lived - `feature/*` / `fix/*` branches, then merge. -- `stable` — marker only. CI fast-forwards it to the latest non-RC release - tag. Never merge into it by hand. - -There is no `dev` integration branch. - -## Remotes - -- `origin` — Forgejo (`ssh://git@100.66.238.26:2222/Breadway/bos.git`) — authoritative. -- `github` — GitHub (`https://github.com/Breadway/bos.git`) mirror. Push - origin (and github when mirroring). - -`origin` is **not** GitHub. - -## CI - -- `.forgejo/workflows/*.yml` trigger on `push: tags: ['v*']` (or - path-scoped packaging triggers) — ordinary pushes to `main` run nothing - except those path filters. Tag a release to build the ISO. -- `stable` is moved by the release-iso workflow, not by humans. -- No build/lint/test CI runs on ordinary commits or PRs — test locally - before merging to `main`. - -## Cleanup - -- Delete feature/fix branches (local + remote) once merged. Check with - `git branch --merged main`. -- Don't let merged branches accumulate. - -## Don't - -- Don't embed credentials in remote URLs — SSH or a credential helper only. -- Don't leave the default branch pointed at a feature branch on - GitHub/Forgejo. -- Don't bake an ISO (`sudo ./build-local.sh`) unless asked — lockfile/docs - work does not require it. -- Don't tell users to `snapper rollback` blindly; GRUB pins - `rootflags=subvol=@`. Recovery is grub-btrfs reboot. Bakery desktop - apps on BOS are system-prefix `/usr/local` (`/etc/bakery/config.toml`); - snapper `@` snapshots include them. Do not move those bits back to - `~/.local` on the image (hermes / default bakery stay user-layout). diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..bc9c049 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1039 @@ +# 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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "bos-settings" +version = "0.6.0" +dependencies = [ + "async-channel", + "bread-theme", + "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 = "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.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[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.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd42fdbbf48612c6e8f47c65fb92d2e8f39c25aecd6af047e83897c1a22d2a4e" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk4-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk4-sys" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d974ac4f15e67472c3a9728daf612590b4a5762a4b33f0edd298df0b80d043c" +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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3848bcba3a35cc0a71df8ba8ecfd799d6bfb862342a53a4a915fb62213aa4e6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "pin-project-lite", + "smallvec", +] + +[[package]] +name = "gio-sys" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64729ba2772c080448f9f966dba8f4456beeb100d8c28a865ef8a0f2ef4987e1" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "windows-sys 0.59.0", +] + +[[package]] +name = "glib" +version = "0.22.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c207e04e51605dcf7b2924c41591b3a10e1438eaac5bcf448fb91f325381104a" +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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7fbac234ed5bc2a28359b7bde8e1b9cdf1441cc2d7f068e4824672d7db9445" +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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d1b7881f96869f49808b6adfe906a93a57a34204952253444d68c3208d71f1" +dependencies = [ + "glib", + "graphene-sys", + "libc", +] + +[[package]] +name = "graphene-sys" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "517f062f3fd6b7fd3e57a3f038a74b3c23ca32f51199ff028aa704609943f79c" +dependencies = [ + "glib-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gsk4" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c912dfcbd28acace5fc99c40bb9f25e1dcb73efb1f2608327f66a99acdcb62" +dependencies = [ + "cairo-rs", + "gdk4", + "glib", + "graphene-rs", + "gsk4-sys", + "libc", + "pango", +] + +[[package]] +name = "gsk4-sys" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d54bbc7a9d8b6ffe4f0c95eede15ccfb365c8bf521275abe6bcfb57b18fb8a" +dependencies = [ + "cairo-sys-rs", + "gdk4-sys", + "glib-sys", + "gobject-sys", + "graphene-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk4" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7181b837f04cbe93f79441475f7a00560a92cba7a72e38cc1a68b6f8b78eaae2" +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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3581b242ba62fdff122ebb626ea641582ec326031622bd19d60f85029c804a87" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "gtk4-sys" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20ba8e695e2640455561274e65e45f0a151619e450746007667f4b23ceae4e1b" +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.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "251bdc6e6487b811be0e406a21e301e07e45c0aa8fa39e00c0c8e12a91752438" +dependencies = [ + "gio", + "glib", + "libc", + "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.12+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.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +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.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +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.2+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.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +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.3", +] + +[[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.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "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-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-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", + "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_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_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_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_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[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_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_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_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 = "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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..af9532d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +members = ["bos-settings"] +resolver = "2" diff --git a/DESIGN.md b/DESIGN.md index 6a075a8..d6d6585 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -1,26 +1,4 @@ -# BOS — historical design plan - -## Current architecture - -**Read [README.md](README.md) for how this repo actually ships.** This file -is the original plan. Several sections below are historical and must not be -taken as current: - -| Plan said | What the tree does now | -|-----------|------------------------| -| Cargo workspace with a `bos-settings/` member | This repo is ISO + Calamares + skel only. No Cargo workspace. | -| `bos-settings` as an in-tree GTK4 app | Standalone bakery product, **Tauri 2 + Svelte**. | -| bakery install in Calamares post-install | bakery binaries + breadhelp content are **baked into `/etc/skel` at ISO build time** from `iso/bread-lockfile.toml`. Missing bins fail the bake. | -| `dotfiles/` is the live skel | Live defaults are `iso/airootfs/etc/skel`. `dotfiles/` is stale. | -| A/B root swapping | **Future.** Today: btrfs + snapper + **grub-btrfs**. GRUB pins `rootflags=subvol=@`, so `snapper rollback` is not the user-facing recovery path. | -| Work on `dev`; origin = GitHub | Single-trunk `main`; `stable` is a CI marker. `origin` = Forgejo, `github` = GitHub. | -| `[breadway]` provides bakery/breadbar/bos-settings | `[breadway]` is breadlock + AUR republishes. Desktop apps are bakery. **Not shipped:** breadcast, breadarr. | -| NVIDIA / A/B / Secure Boot / LUKS2 | NVIDIA proprietary is **unsupported**. A/B root swapping is **not implemented**. Secure Boot is **Setup Mode only** (self-signed `sbctl`). Disk encryption is **LUKS1** because GRUB cannot unlock LUKS2+Argon2id. | -| `SigLevel = Required` on `[breadway]` | **Yes, as of the signed repo.** `[breadway]` points at `https://dl.breadway.dev/arch` where `scripts/ci-publish-signed-repo.sh` detach-signs every `.pkg.tar.zst` and the db with the BOS release key (`56203B86…`, `KEYS.asc`). That key is trusted in the pacman keyring at build time, on the live medium, and on the installed target. | - ---- - -# Original plan (kept for history) +# BOS — Bread Operating System Plan ## Context @@ -29,25 +7,51 @@ The bread ecosystem (bread, breadbar, breadbox, breadcrumbs, breadpad/breadman, Goals: - **Install and be done**: Calamares GUI installer → reboot → working Hyprland + full bread stack - **Rollback safety**: Btrfs subvolumes + snapper + snap-pac; every pacman transaction is snapshotted -- **Unified config**: `bos-settings` surfaces all app configs + snapshot management + bakery updates +- **Unified config**: `bos-settings` GTK4 app surfaces all app configs + snapshot management + bakery updates - **Future-compatible**: Btrfs layout is designed to allow A/B partition migration later (SteamOS model) --- ## Repo Structure -Single new repo: `Breadway/bos` — *planned as* a Cargo workspace. **That is -not what landed**; see Current architecture. +Single new repo: `Breadway/bos` — a Cargo workspace. ``` bos/ -├── Cargo.toml # Workspace (members: [bos-settings]) — NOT in tree -├── bos-settings/ # planned GTK4 app — now its own bakery repo -├── iso/ # archiso profile (this is the repo) +├── Cargo.toml # Workspace (members: [bos-settings]) +├── bos-settings/ # GTK4 unified settings app +│ ├── Cargo.toml +│ └── src/ +│ ├── main.rs +│ ├── state.rs +│ ├── theme.rs +│ ├── ui/ +│ │ ├── window.rs # Sidebar + content shell (port breadman pattern) +│ │ ├── sidebar.rs +│ │ └── views/ +│ │ ├── bread.rs +│ │ ├── breadbar.rs +│ │ ├── breadbox.rs +│ │ ├── breadcrumbs.rs +│ │ ├── breadpad.rs +│ │ ├── snapshots.rs +│ │ ├── packages.rs +│ │ └── hyprland.rs +│ └── config/ +│ └── mod.rs # Per-app config loaders +├── iso/ # archiso profile │ ├── profiledef.sh -│ ├── packages.x86_64 -│ └── airootfs/ -└── dotfiles/ # planned install-time configs — NOT the live skel +│ ├── packages.x86_64 # Live ISO + installed system package list +│ ├── airootfs/ # Files overlaid onto live ISO root +│ │ └── etc/ +│ │ ├── calamares/ # Calamares YAML configuration +│ │ └── skel/ # Default user dotfiles +└── dotfiles/ # Default configs deployed at install time + ├── hyprland/ # hyprland.conf, keybinds, autostart + ├── bread/ # breadd.toml, init.lua, devices.lua + ├── breadbar/ # (no config needed; zero-config by default) + ├── breadbox/ # config.toml with default context priorities + └── breadcrumbs/ # breadcrumbs.toml with default home profile ``` --- @@ -66,11 +70,7 @@ bos/ Mount options: `noatime,compress=zstd,space_cache=v2` on all subvolumes. -**A/B compatibility note (future):** The `@` subvolume is self-contained and -could be swapped atomically. This is a design property for a later upgrade -path. It is **not** implemented. Recovery today is reboot into a grub-btrfs -snapshot; GRUB's `rootflags=subvol=@` means a raw `snapper rollback` is the -wrong instruction to give users. +**A/B compatibility note:** The `@` subvolume is self-contained and can be swapped atomically — this is the design property needed for a future A/B upgrade path. The layout does not need to change to adopt it. ### Snapshot tooling (installed + configured during post-install) @@ -96,79 +96,100 @@ No user-facing CLI needed for this component — `bos-settings` is the interface ### archiso profile (`iso/`) - Derives from `/usr/share/archiso/configs/releng/` (the standard baseline) -- `packages.x86_64` is the live + installed pacman set (Hyprland, Calamares, - breadlock, WebKitGTK 4.1 for Tauri bos-settings, …). bakery apps are not - listed here. -- `airootfs/etc/skel/` contains the default user configs (this is the live - skel — not `dotfiles/`). +- `packages.x86_64` includes: base, linux, grub, btrfs-progs, snapper, snap-pac, grub-btrfs, hyprland, pipewire, wireplumber, networkmanager, gtk4, gtk4-layer-shell, iw, librsvg, libpulse, bluez, bluez-utils, calamares, calamares-qt6 +- `airootfs/etc/skel/` contains the default dotfiles (symlinked from `dotfiles/`) - Live session autologs into a `liveuser` and launches Calamares automatically ### Calamares modules (in order) -The historical list below included a post-install `bakery install` and -Calamares `bootloader`/`grubcfg` installing GRUB. What shipped instead: -binaries are already in skel; `post-install.sh` runs `grub-install` + -`grub-mkconfig` (Calamares' bootloader modules leave the ESP empty here). - 1. **welcome** — system checks (RAM ≥ 2GB, internet, disk space) 2. **locale** — timezone + locale selection 3. **keyboard** — layout selection 4. **partition** — custom `btrfs` mode: creates EFI partition + single btrfs pool with the subvolume layout above 5. **users** — create main user, set password 6. **packages** — install package list (reuses `packages.x86_64`) -7. **bootloader** — *planned*; actual GRUB install is in `post-install.sh` -8. **shellprocess (post-install)** — snapper, services, copy skel; does **not** run bakery +7. **bootloader** — install GRUB to EFI, `grub-mkconfig` with grub-btrfs hook +8. **shellprocess (post-install)** — runs `iso/post-install.sh`: + - Configures snapper root config + - Enables services: `NetworkManager`, `bluetooth`, `breadd` (user), `breadbox-sync` (user) + - Runs `bakery install bread breadbar breadbox breadcrumbs breadpad` (or `bakery install --all`) + - Copies `dotfiles/` into `/home/$USER/.config/` (skips any file that already exists) 9. **finished** — reboot prompt --- -## Component 3: `bos-settings` (planned as GTK4) - -### Tech choices (original) +## Component 3: `bos-settings` GTK4 App +### Tech choices - **gtk4-rs** (v0.11, v4_12 feature), no relm4 — plain GTK4 following breadman's pattern - -**What shipped:** Tauri 2 + Svelte in its own repo -(`git.breadway.dev/Breadway/bos-settings`), distributed by bakery. This -repo does not build it. +- **bread-theme** for palette + CSS (git dep: `github.com/Breadway/bread-ecosystem`) +- Reads/writes each tool's own config file directly (no unified intermediate config) +- Window: 960×640, sidebar 190px, `gtk4::Stack` for view switching — identical structure to breadman ### Sidebar sections + views -The panel list is still roughly accurate; see README. Snapshots recovery -should send users through **grub-btrfs reboot**, not `snapper rollback N`. +| Section | View | What it does | +|---------|------|--------------| +| **Apps** | bread | Edit `~/.config/bread/breadd.toml` | +| | breadbar | Edit `~/.config/breadbar/` (style.css override, no TOML needed) | +| | breadbox | Edit `~/.config/breadbox/config.toml` (context priority lists) | +| | breadcrumbs | Edit `~/.config/breadcrumbs/breadcrumbs.toml` (profiles, networks) | +| | breadpad | Edit `~/.config/breadpad/breadpad.toml` (model, reminders, calendar) | +| **System** | Snapshots | `snapper list` output; rollback button calls `snapper rollback N` | +| | Packages | `bakery list --installed`; update buttons call `bakery update ` | +| | Hyprland | "Open config in editor" + monitor list from `bread.state.monitors()` | + +### Config loading pattern + +Each view has a dedicated `load_config(path) -> Result` and `save_config(path, T) -> Result<()>` using `toml` crate. Config structs mirror each app's existing types (no duplication — import the `*-shared` crate where it exists, e.g. `breadpad-shared`). For apps without a shared crate (breadbox, breadcrumbs), define minimal local structs. + +### Snapshots view specifics + +- On open: runs `snapper list --output-cols number,date,description,pre-post` via `std::process::Command`, parses into table rows +- Rollback: confirmation dialog → `snapper rollback ` → notify user to reboot +- Delete: `snapper delete ` +- No write access to `/` needed for list/rollback since snapper is configured with `ALLOW_USERS` for the main user + +### Packages view specifics + +- On open: reads `~/.local/state/bakery/installed.json` directly (no network) +- "Check for updates": runs `bakery list` (triggers index refresh), compares versions +- "Update all": runs `bakery update --all` in a subprocess, streams stdout to a log TextView ### Distribution -`bos-settings` has its own `bakery.toml` and is installable via -`bakery install bos-settings` on any Arch/Hyprland system, not only as part -of a BOS install. +`bos-settings` gets a `bakery.toml` and is added to the `bread-ecosystem` registry — installable standalone on any Arch/Hyprland system via `bakery install bos-settings`, not only as part of a BOS install. --- ## Component 4: Default Dotfiles -Minimal but functional defaults. These live in `iso/airootfs/etc/skel` -(`hyprland.lua` + JSON binds, not `dotfiles/hyprland/*.conf`). +Minimal but functional defaults deployed at install time. These are opinionated starting points, not locked configs — users edit freely after install. -Zero-config bakery apps survive with no extra skel files. breadcrumbs -networks are user-filled after install — do not invent a full -`breadcrumbs.toml` in-tree. +| File | Key content | +|------|-------------| +| `dotfiles/hyprland/hyprland.conf` | Monitor auto-detect, default keybinds, `exec-once` for breadd/breadbar/breadbox-sync | +| `dotfiles/hyprland/keybinds.conf` | `$mod+Space` → breadbox, `$mod+N` → breadpad, `$mod+M` → breadman, `$mod+S` → bos-settings | +| `dotfiles/bread/breadd.toml` | All adapters enabled, log_level=info | +| `dotfiles/bread/init.lua` | Minimal: activates "default" profile on startup | +| `dotfiles/breadbox/config.toml` | Single default context with common apps | +| `dotfiles/breadcrumbs/breadcrumbs.toml` | Placeholder home profile (user fills in SSIDs) | --- ## Build Order -Historical. The ISO profile + skel + Calamares path is what this repo -iterates on. bos-settings is developed in its own repo. +1. **Dotfiles** — write default configs; these unblock installer testing immediately +2. **Btrfs + snapper config** — write `post-install.sh`; test in a VM with `archiso` livecdbase +3. **ISO profile** — archiso profiledef + package list + Calamares YAML; iterate in a VM +4. **bos-settings** — start with Snapshots and Packages views (highest value, no app-specific config parsing needed), then add per-app views one at a time --- ## Verification -- **ISO**: `sudo ./build-local.sh` (not a raw `mkarchiso iso/` — the bake - step is required). Boot in QEMU; complete install; confirm bakery bins and - `~/.local/share/breadhelp/content`. +- **ISO**: Build with `mkarchiso -v -w /tmp/bos-work -o /tmp/bos-out iso/`; boot in QEMU (`qemu-system-x86_64 -cdrom bos.iso -m 4G -enable-kvm`); complete install; reboot into installed system; confirm all services running and bakery packages present - **btrfs layout**: `btrfs subvolume list /` after install; confirm `@`, `@home`, `@snapshots`, `@log`, `@cache` exist - **snapper**: `snapper list`; run `pacman -Syu` and confirm two new snapshots appear - **grub-btrfs**: Reboot and confirm snapshot submenu in GRUB -- **bos-settings**: built and tested in the bos-settings repo, not here +- **bos-settings**: `cargo build --release`; launch; confirm each view loads its config file; edit a value, save, re-open and confirm persistence; test rollback button in Snapshots view diff --git a/README.md b/README.md index 22b0108..b736e5d 100644 --- a/README.md +++ b/README.md @@ -1,142 +1,72 @@ # BOS — Bread Operating System An Arch-based, Hyprland desktop distribution that ships the [bread -ecosystem](https://git.breadway.dev/Breadway) preconfigured. One Calamares install +ecosystem](https://github.com/Breadway) preconfigured. One Calamares install produces a themed, bootable Wayland desktop — no manual Arch bootstrap, no wiring up dotfiles, no per-tool bakery installs. -> This file is the product as the tree ships it. [DESIGN.md](DESIGN.md) is the -> original plan, kept as history — several of its sections (in-tree GTK -> bos-settings, bakery-at-post-install, A/B as if it were current) are not -> how the ISO works today. +> Design rationale and the btrfs/A-B roadmap live in [DESIGN.md](DESIGN.md). +> This file is the practical overview: what's in the image, how to build it, +> and how to test it. ## What you get - **Compositor**: Hyprland with a native-Lua config (`hyprland.lua`), curated keybinds, snappy animations, blur, and pywal-driven colours on a black base. -- **bread ecosystem**, baked into `/usr/local` from bakery-managed binaries - (no network needed at install time; per-user bakery state is seeded in - `/etc/skel`): the `bread`/`breadd` automation daemon - (`bread-emit` / `bread-module-host` when the stable bread release publishes - them), `breadbar` (status bar + notifications), `breadbox` (launcher), - `breadclip` (clipboard history), `breadcrumbs` (Wi-Fi profiles), - `breadpad`/`breadman` (notes), `breadpaper` (wallpaper + theme), - `breadsearch` (system search), `breadmon` (monitor layout TUI), - `breadshot` (screenshots), `bread-theme` (the shared palette engine), - `breadhelp` (onboarding + cheatsheet), `bos-settings` (control panel), - and the `bakery` package manager. Most of those apps are zero-config on - first boot; breadcrumbs networks are user-filled after install. See - [below](#the-bread-ecosystem). -- **breadlock** (lock screen + greeter) is the one bread\* app that ships as - **pacman**, not bakery — it needs a root-owned PAM service. -- **bos-settings**: a **Tauri 2 + Svelte** control panel (standalone bakery - product, not a member of this repo). Configures every bread\* app - non-destructively, plus snapshots, bakery/pacman updates, and day-to-day - machine administration. -- **Login**: greetd + breadgreet (under `cage`) → Hyprland session. +- **bread ecosystem**, baked into `/etc/skel` from bakery-managed binaries + (no network needed at install time): `bread`/`breadd`, `breadbar` (status bar + + notification daemon), `breadbox` (launcher), `breadcrumbs` (Wi-Fi profiles), + `breadpad` (notes/reminders), `breadman`, and the `bakery` package manager. +- **bos-settings**: a GTK4 control panel that configures every bread\* app's + config from a GUI (non-destructively), plus snapshot rollback and bakery + updates. See below. +- **Login**: greetd + breadgreet (bread-ecosystem's own greeter, under `cage`) + → Hyprland session. - **Boot splash**: Plymouth `bos` theme (logo + spinner, black background). - **Theming**: global dark across GTK3 (Adwaita-dark), GTK4/libadwaita (`color-scheme: prefer-dark`), and Qt (qt5ct/qt6ct Fusion dark); Papirus-Dark icons; Bibata cursor. - **Apps**: kitty, nautilus (+ gvfs), Zen browser, VLC, loupe, gnome-text-editor, gnome-calculator, file-roller, with file associations wired in `mimeapps.list`. - `yay` ships for AUR access beyond bakery + `[breadway]`. + `yay` ships for AUR access beyond bakery's bread ecosystem + `[breadway]`. - **Hardware**: pipewire audio, NetworkManager, BlueZ + blueman, CUPS printing with avahi mDNS discovery, TLP power management, fwupd firmware updates. - Mesa only — **NVIDIA proprietary drivers are not included** and NVIDIA is - unsupported out of the box (see [docs/hardware.md](docs/hardware.md)). - **Resilience**: btrfs + snapper + snap-pac + grub-btrfs snapshots on every - pacman transaction (**root `@` only** — snapper does not cover `@home`); - home backup is **Settings → Backup** (restic, local path or SFTP); zram - swap; ufw firewall (deny-incoming, mDNS allowed). A/B root swapping is - **not** implemented. Recovery is a grub-btrfs reboot, not - `snapper rollback` (GRUB pins `rootflags=subvol=@`). See - [docs/hardware.md](docs/hardware.md). -- **Security**: optional full-disk encryption is **LUKS1** (GRUB cannot unlock - LUKS2 + Argon2id). Secure Boot is **self-signed Setup Mode only** via - `sbctl` — not a Microsoft-signed shim; enrollment is skipped unless the - firmware is already in Setup Mode. - -## What ships vs what does not - -| Channel | What | -|---------|------| -| **Bakery, required** | `bakery`, `bread` / `breadd`, `breadbar`, `breadbox` / `breadbox-sync`, `breadcrumbs`, `breadpad` / `breadman`, `breadpaper`, `bread-theme`, `breadmon`, `breadsearch` / `breadmill`, `breadclip` / `breadclipd`, `breadshot`, `bos-settings`, `breadhelp` (+ breadhelp content under `/usr/local/share/breadhelp/`) | -| **Bakery, optional** | `bread-emit`, `bread-module-host` — baked when the verified stable index publishes them; skipped (not a failed bake) until bread ships them | -| **pacman (`packages.x86_64`)** | `breadlock`, plus the rest of the distro (Hyprland, Calamares, Zen, …) | -| **Not shipped** | `breadcast`, `breadarr` | - -The baked name list is [`iso/bread-lockfile.toml`](iso/bread-lockfile.toml) -(plus optional `[versions]` / `[[pin]]` so CI fetches -`https://dl.breadway.dev///...`). `build-local.sh` fails if any -**required** binary is missing on the builder. + pacman transaction; zram swap; ufw firewall (deny-incoming, mDNS allowed). +- **Security**: full-disk encryption (LUKS, via Calamares' built-in support — + cryptsetup + the matching mkinitcpio/GRUB wiring ship so an encrypted + install actually boots) and self-signed Secure Boot (via `sbctl`, enrolled + automatically at install time when the firmware is in Setup Mode). ## Repo layout -This is an **ISO + Calamares + skel** repo. There is no Cargo workspace and -no `bos-settings/` member — bos-settings and breadhelp live in their own -repos and arrive via bakery. - ``` bos/ +├── Cargo.toml # workspace (members: bos-settings) +├── bos-settings/ # GTK4 unified settings app (Rust) +│ └── src/ +│ ├── config/mod.rs # non-destructive toml_edit config layer +│ └── ui/{widgets,window,sidebar}.rs, ui/views/*.rs ├── iso/ # archiso profile -│ ├── bread-lockfile.toml # bakery bins + optional version pins │ ├── profiledef.sh -│ ├── packages.x86_64 # live + installed pacman set +│ ├── packages.x86_64 # live + installed package set │ └── airootfs/ # files overlaid onto the image │ └── etc/ -│ ├── skel/ # live user defaults (hypr, kitty, gtk, …) +│ ├── skel/ # default user dotfiles (hypr, kitty, gtk, …) │ └── calamares/ # installer config + post-install.sh ├── packaging/ # in-house PKGBUILDs for AUR-only deps +│ ├── arch/ # bos-settings │ ├── calamares/ -│ ├── bibata/ -│ ├── powerlevel10k/ -│ └── yay-bin/ -├── dotfiles/ # STALE — not the live skel; see its README -├── scripts/ -│ ├── ci-stage-bakery.py # CI: minisign-verified index → $LAPTOP_HOME -│ ├── ci-verify-bake.sh # CI: read-only checks before mkarchiso -│ ├── ci-publish-signed-repo.sh # CI: signed [breadway] repo → /srv/breadway-dl/arch -│ └── smoke-test.sh -├── docs/ -│ ├── hardware.md # Mesa only, NVIDIA, grub-btrfs recovery -│ └── signed-repo.md # dl.breadway.dev/arch signing -├── .forgejo/workflows/ # CI: AUR republish + signed repo + tagged ISO +│ └── bibata/ +├── .forgejo/workflows/ # CI: build + publish packages to [breadway] ├── build-local.sh # native ISO build for this machine -├── README.md -└── DESIGN.md # historical plan +└── DESIGN.md ``` -Live binds are `iso/airootfs/etc/skel/.config/hypr/binds.json` (`Super+L` → -`loginctl lock-session`, breadshot on `Super+Shift+S/C/P`, `Super+U` -breadpad). Do not treat `dotfiles/hypr/keybinds.conf` as current. - -## Branches and remotes - -Single-trunk: work on **`main`** via short-lived `feature/*` / `fix/*` -branches. **`stable`** is a marker branch CI fast-forwards to the latest -non-RC release tag — do not land work there by hand. - -Dual remotes: - -- **`origin`** — Forgejo (`ssh://git@100.66.238.26:2222/Breadway/bos.git`), - authoritative -- **`github`** — GitHub (`https://github.com/Breadway/bos.git`) mirror - -Push `origin` (and `github` when mirroring). Do not treat origin as GitHub. - ## Building the ISO -`build-local.sh` builds the image natively (no container) and copies this -machine's bakery-installed bread binaries + breadhelp content from the -builder's `~/.local` into the image at `/usr/local` (bins, share/data, -desktop files, licenses) and `/usr/lib/systemd/user` (units). Per-user -bakery state (`installed.json` + index cache) is seeded in `/etc/skel`. -User units are `systemctl --global enable`'d so a later `useradd -m` -starts them on first login. BOS opts in via `/etc/bakery/config.toml` -(`prefix = "/usr/local"`); default bakery without that file is still -`~/.local`. Snapper `@` snapshots include `/usr/local`; recovery is -still grub-btrfs, not `snapper rollback`. +`build-local.sh` builds the image natively (no container) and bakes this +machine's bakery-installed bread binaries into `/etc/skel`: ```sh sudo ./build-local.sh # release-quality (xz squashfs) @@ -144,24 +74,16 @@ sudo FAST_BUILD=1 ./build-local.sh # fast dev iteration (zstd squashfs) ``` The ISO lands in `out/bos--x86_64.iso`. The script pins -`SOURCE_DATE_EPOCH` (reproducible UUIDs), rewrites the `[breadway]` repo URL -to the Tailscale-reachable Forgejo registry for the build, and **exits -non-zero** if any **required** lockfile binary (or breadhelp content) is -missing. Optional bins are skipped with a warning. - -CI stages the builder from the **minisign-verified** stable bakery index -(`index.json` + `index.json.minisig`) and prefers lockfile `[versions]` -URLs (`https://dl.breadway.dev///...`) when set, so two bakes -of the same commit fetch the same bits. Local builds still snapshot the -builder. +`SOURCE_DATE_EPOCH` (reproducible UUIDs) and rewrites the `[breadway]` repo URL +to the Tailscale-reachable Forgejo registry for the build. ### Why some packages are in-house `calamares`, `zen-browser-bin`, `bibata-cursor-theme`, and `yay-bin` are -AUR-only. BOS keeps a PKGBUILD for each under `packaging/` and republishes -the built package to the `[breadway]` repo via a Forgejo Actions workflow -(built on the hestia self-hosted runner, published with a scoped registry -token). `[breadway]` is **not** where bakery/breadbar/bos-settings live. +AUR-only. BOS keeps a PKGBUILD for each under `packaging/` and republishes the +built package to the `[breadway]` repo via a Forgejo Actions workflow (built +on the hestia self-hosted runner, published with a scoped registry token). +`bos-settings` itself publishes the same way on a `v*` tag. ### Verifying a release @@ -174,11 +96,7 @@ dedicated release-signing key (not reused from anything else): 5620 3B86 A110 695A E7F3 1093 4AF3 323D 678E B5E2 ``` -The public half is committed at [`KEYS.asc`](KEYS.asc). The same key signs -the ISO checksums **and** the `[breadway]` pacman repo — every package and -the db at `https://dl.breadway.dev/arch` carry a `.sig` from it, and that -section is `SigLevel = Required` (see -[docs/signed-repo.md](docs/signed-repo.md)). To verify a download: +The public half is committed at [`KEYS.asc`](KEYS.asc). To verify a download: ```sh gpg --import KEYS.asc @@ -200,51 +118,19 @@ It uses KVM + `-cpu host`, 8 GiB / 8 vCPU, and `virtio-vga-gl` with Hyprland session in QEMU. The disk lives on NVMe (not the tmpfs `/tmp`) to avoid memory pressure. -Post-install, `scripts/smoke-test.sh` (run as the installed user) checks -subvolumes, services, bakery bins on PATH, breadhelp content under -`/usr/local/share/breadhelp/content`, and that bakery user units are -`--global` enabled (or the preset / wants files exist). - -## Second account - -Bakery desktop apps live in `/usr/local` — shared, already on PATH. A later -account does **not** get a private copy of those binaries. - -`/etc/default/useradd` keeps `SKEL=/etc/skel`. Stock `useradd -m` is enough: - -```sh -sudo useradd -m alice -sudo passwd alice -``` - -- **Apps**: `/usr/local/bin` (and `/usr/local/share`) — already there. -- **Session files**: `useradd -m` copies `/etc/skel` (Hyprland, bread - config, bakery `installed.json` + index cache) so first login has a - session. Skel does not contain bakery binaries. -- **Daemons**: `breadd`, `breadbox-sync`, `breadclipd`, `breadcrumbs`, - `breadmill`, … are `systemctl --global enable`'d at install (and on - the live image). Creating a user starts them on first login. -- **Login**: greetd/breadgreet lists any local user with a login shell - (`SHELL=/usr/bin/zsh` is the useradd default). - -`breadclipd` is WantedBy=`graphical-session.target`. BOS does not activate -that target (no uwsm), so Hyprland still `systemctl --user start`s it after -the compositor is up. `--global enable` still records it for every account. - -Rollback is still the GRUB snapshots submenu (grub-btrfs), not -`snapper rollback`. `/usr/local` rides the `@` snapshot. - ## bos-settings -Standalone bakery product: **Tauri 2 + Svelte**, not GTK4, and not built -from this repo. Install/update with `bakery`; the ISO just bakes whatever -binary the builder has. +A GTK4 settings app aiming for GNOME-Settings-style parity: not just editing +config files, but live system state and control, so day-to-day machine +administration doesn't require a terminal. -It aims for GNOME-Settings-style parity: live system state and control, so -day-to-day administration doesn't require a terminal. Bread-ecosystem -configs are edited **non-destructively** (comments and unmodeled keys stay). -Panels with a daemon (bread, breadbox, breadcrumbs, breadsearch, breadclip) -also get live systemd status + Start/Stop/Restart/Logs. +Bread-ecosystem TOML configs are edited **non-destructively**: `toml_edit` +parses the file, changes only the keys a view exposes, and writes it back — +preserving comments and any keys the UI doesn't model (calendar passwords, +saved-network passwords, model paths). Panels with a daemon behind them +(bread, breadbox, breadcrumbs, breadsearch, breadclip) also get live +systemd status + Start/Stop/Restart/Logs via a shared `service_control` +widget, not just the config file. | Panel | What it does | |-------|--------------| @@ -267,56 +153,27 @@ also get live systemd status + Start/Stop/Restart/Logs. | Packages | `bakery` installed list + updates, pacman system update | | AUR | Search via `yay`; installing opens a terminal (AUR build scripts need review) | | Firmware | `fwupd` device list + updates | -| Snapshots | `snapper` list (number / date / description); reboot to pick in GRUB (grub-btrfs); delete — **root (`@`) only** | -| Backup | restic of `$HOME` (`@home`) via Settings → Backup; snapper does not cover home | +| Snapshots | `snapper` list / boot-into (grub-btrfs) / delete | -Source and build live in the [bos-settings](https://git.breadway.dev/Breadway/bos-settings) -repo, not here. +Build standalone: -## The bread ecosystem +```sh +cargo build --release -p bos-settings +cargo test -p bos-settings # includes config round-trip tests +``` -Everything below is a separate bakery-distributed project with its own repo -and release cadence, baked into `/usr/local` at ISO build time so a fresh -install has them all with no network round-trip. Some ship more than one -binary from a single package — that's noted where it applies. Most have a -corresponding **bos-settings** panel; this table is about *using* the app -directly. - -**Desktop shell** +## The bread ecosystem at a glance | Tool | Role | Launch | |------|------|--------| -| `bread` / `breadd` | Reactive automation daemon — normalises hardware/compositor/power/network signals into events dispatched to Lua modules (`~/.config/bread/`). `bread-emit` is the fire-and-forget helper hooks/CLIs use; `bread-module-host` is the sandboxed out-of-process module runtime breadd spawns. Both extra bins are **optional** on the ISO until a stable bread release publishes them. | runs at login (`breadd.service`) | -| `breadbar` | Top status bar: workspaces, clock, system stats, tray, **and** the notification daemon — one process, not two | runs at login | -| `breadbox` | Application launcher (fuzzy search, per-context results via `breadbox-sync`) | `SUPER+Space` | -| `breadlock` | Idle lock screen. Also provides `breadgreet`, the login greeter hosted under `cage` via greetd — same project, two binaries, one visual identity from login to lock. **pacman**, not bakery. | `SUPER+L` (via `loginctl lock-session`, picked up by `hypridle`); `breadgreet` runs automatically at boot | -| `bread-theme` | The shared palette engine every bread app renders through: fixed dark base colors, with only the accent slots following the current wallpaper's pywal palette. `bread-theme generate` regenerates the stylesheet; hyprland.lua calls it automatically on wallpaper change. | invoked automatically, rarely run by hand | - -**Productivity** - -| Tool | Role | Launch | -|------|------|--------| -| `breadpad` | Quick-capture scratchpad/notes popup with AI classification and optional CalDAV calendar sync | `SUPER+U` | -| `breadman` | The fuller notes manager view (browse/organize) — ships from the same `breadpad` package as a second binary | `SUPER+M` | -| `breadclip` | Clipboard history. `breadclipd` is the background daemon that actually records history; `breadclip` is the GTK4 popup that browses it | `SUPER+V` / `SUPER+Shift+V` | -| `breadsearch` | Semantic system-wide search (indexes files/notes, embeds locally — CPU/ROCm/CUDA backend configurable). `breadmill` is its indexing daemon. | via breadbox, or BOS Settings → File Search | -| `breadhelp` | Onboarding + in-session help/cheatsheet. Content lives at `/usr/local/share/breadhelp/content` (bakery `content.tar.gz`, baked into the image). | `SUPER+/` | - -**System** - -| Tool | Role | Launch | -|------|------|--------| -| `breadcrumbs` | Location-aware Wi-Fi profile state machine, with optional Tailscale integration — switches network behavior based on which saved network you're on | CLI, or BOS Settings → Wi-Fi Profiles | -| `breadpaper` | Wallpaper manager — sets the wallpaper via `awww`, generates the pywal accent palette from it, and reloads every bread-theme app | BOS Settings → Wallpaper | -| `breadmon` | TUI monitor layout manager (resolution/position/scaling) — the interactive counterpart to BOS Settings' read-only Display panel | `breadmon` in a terminal | -| `breadshot` | Screenshot utility wrapping `grim`/`slurp`/`wl-copy` with Hyprland-aware geometry (multi-monitor-safe region select) | `breadshot`, or `SUPER+Shift+S/C/P` | - -**Tooling** - -| Tool | Role | Launch | -|------|------|--------| -| `bakery` | CLI package manager for the whole ecosystem — install/update/list, tracks installed binaries + versions independently of pacman | `bakery` | -| `bos-settings` | Unified Tauri 2 + Svelte control panel: live system state + control (network, power, firewall, users, packages, firmware, AUR, snapshots) plus non-destructive config editing for every app above | `SUPER+,` | +| `bread` / `breadd` | Reactive automation daemon — normalises hardware/compositor signals into events dispatched to Lua modules | runs at login | +| `breadbar` | Top status bar (workspaces, clock, stats, tray) **and** the notification daemon | runs at login | +| `breadbox` | Application launcher | `SUPER+Space` | +| `breadpad` | Notes & reminders (AI-classified, optional CalDAV sync) | `SUPER+U` | +| `breadman` | Package-manager UI | `SUPER+M` | +| `breadcrumbs` | Wi-Fi profile state machine (location-aware) | CLI / BOS Settings | +| `bakery` | CLI package manager for the ecosystem | `bakery` | +| `bos-settings` | Unified GTK4 control panel for all of the above + snapshots + updates | `SUPER+,` | ## Keyboard shortcuts @@ -328,16 +185,12 @@ cheatsheet in-session; first boot shows a short welcome (once). | `SUPER+Return` | Terminal (kitty) | | `SUPER+Space` | App launcher (breadbox) | | `SUPER+E` / `SUPER+B` | Files (nautilus) / Browser (zen) | -| `SUPER+U` / `SUPER+M` | Notes (breadpad) / notes manager (breadman) | +| `SUPER+U` / `SUPER+M` | breadpad / breadman | | `SUPER+,` / `SUPER+/` | BOS Settings / keybind cheatsheet | | `SUPER+L` / `SUPER+N` | Lock / log out | | `SUPER+Backspace` | Close window | -| `SUPER+F` | Fullscreen | -| `SUPER+I` | Toggle floating | -| `SUPER+P` | Toggle pseudotile | -| `SUPER+R` | Resize mode | -| `SUPER+T` | Toggle split direction | -| `SUPER+V` / `SUPER+Shift+V` | Clipboard history (breadclip) | +| `SUPER+F` / `SUPER+V` / `SUPER+T` | Fullscreen / float / toggle split | +| `SUPER+Shift+V` | Clipboard history | | `SUPER+Tab` | Last window | | `SUPER+Shift+S/C/P` | Screenshot region→file / region→clipboard / screen→file | | `SUPER+arrows` | Move focus | @@ -346,21 +199,13 @@ cheatsheet in-session; first boot shows a short welcome (once). | `SUPER+1..0` | Switch to workspace 1–10 | | `SUPER+Shift+1..0` | Move window to workspace | | `SUPER+[ / ]` | Previous / next workspace | -| `SUPER+Shift+[ / ]` | Move window to previous / next workspace | -| `SUPER+scroll` | Cycle workspaces | | `SUPER+left/right-drag` | Move / resize window with the mouse | -| Volume / brightness / play-pause / next / prev | Media keys — work even on the lock screen | -| Calculator key | Opens gnome-calculator | ## Known limitations -See [docs/hardware.md](docs/hardware.md) (GPUs, NVIDIA, recovery) and -[docs/signed-repo.md](docs/signed-repo.md) (`[breadway]` stays unsigned -until `dl.breadway.dev/arch` exists). - - **GPUs**: ships the generic Mesa stack — AMD and Intel work out of the box. - NVIDIA is **unsupported** (no proprietary driver, no NVIDIA firmware). See - [docs/hardware.md](docs/hardware.md). + The **NVIDIA proprietary driver is not included**; NVIDIA users must install + `nvidia`/`nvidia-utils` and set the usual Hyprland env vars after install. - **Virtual machines**: Hyprland needs GPU acceleration to be smooth. Use `virtio-vga-gl` + `-display gtk,gl=on` (virgl); plain software rendering is noticeably laggy. @@ -379,42 +224,26 @@ until `dl.breadway.dev/arch` exists). BOS ships the matching `cryptsetup`/mkinitcpio/GRUB wiring so an encrypted install actually boots (LUKS1, since GRUB doesn't support LUKS2 + Argon2id). - **Snapshots assume btrfs**: the snapper/grub-btrfs tooling expects the default - btrfs subvolume layout the installer creates. Recovery is the GRUB - snapshots submenu, not `snapper rollback` — [docs/hardware.md](docs/hardware.md). -- **`[breadway]` signatures**: `SigLevel = Required` — the signed repo at - `dl.breadway.dev/arch` is live (db + every package `.sig`ned with the BOS - release key). See [docs/signed-repo.md](docs/signed-repo.md). + btrfs subvolume layout the installer creates. ## Recovery -**An update broke something (system still boots):** reboot → **GRUB -“snapshots” submenu** (grub-btrfs), then boot that entry. - -BOS Settings → Snapshots lists each snapshot’s number, date, and -description so you know which GRUB entry to pick. It does not roll the -running root back in place. Snapper is root only. Home files are -**Settings → Backup** (restic restore into `~/bos-restore-`, not -over `$HOME`). - -Do **not** run `snapper rollback`. BOS GRUB pins `rootflags=subvol=@`, so -a snapper-swapped default subvolume is not what the installed grub.cfg -will boot next. Details: [docs/hardware.md](docs/hardware.md). - -A/B root swapping (SteamOS-style) is a **future** idea in DESIGN.md — it is -not shipped. +**An update broke something (system still boots):** open BOS Settings → +Snapshots and roll back, or pick a pre-update snapshot from the **GRUB +“snapshots” submenu** at boot, then run `snapper rollback` from the booted +snapshot. **The system won't boot (broken GRUB / lost EFI entry):** 1. Boot the BOS ISO and open a terminal (`SUPER+Return`). -2. Run `sudo bos-rescue`. It finds the installed btrfs `@` and the ESP, - prints the devices it will use, and asks `YES` before writing. It can - `arch-chroot` and/or reinstall GRUB with the same sequence the - installer uses (NVRAM + `--removable` + `grub-mkconfig`). -3. Manual equivalent, if you would rather type it: +2. Mount the installed root and EFI, then chroot: ```sh mount -o subvol=@ /dev/sdXN /mnt mount /dev/sdXP /mnt/boot/efi # the EFI partition arch-chroot /mnt + ``` +3. Reinstall the bootloader (the same sequence the installer uses): + ```sh grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=BOS --recheck grub-install --target=x86_64-efi --efi-directory=/boot/efi --removable --recheck grub-mkconfig -o /boot/grub/grub.cfg diff --git a/bakery.toml b/bakery.toml new file mode 100644 index 0000000..632de76 --- /dev/null +++ b/bakery.toml @@ -0,0 +1,12 @@ +name = "bos-settings" +description = "System settings app for Bread OS" +binaries = ["bos-settings"] +system_deps = ["gtk4", "glib2"] +optional_system_deps = ["snapper"] +bread_deps = [] + +[config] +dir = "~/.config" + +[install] +post_install = [] diff --git a/bos-settings/Cargo.toml b/bos-settings/Cargo.toml new file mode 100644 index 0000000..61bb665 --- /dev/null +++ b/bos-settings/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "bos-settings" +version = "0.6.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" diff --git a/bos-settings/src/config/mod.rs b/bos-settings/src/config/mod.rs new file mode 100644 index 0000000..4cc3266 --- /dev/null +++ b/bos-settings/src/config/mod.rs @@ -0,0 +1,213 @@ +//! Non-destructive config editing. +//! +//! 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, 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. + +use std::error::Error; +use std::path::{Path, PathBuf}; + +use toml_edit::{value, Array, DocumentMut, Item, Table, Value}; + +/// Load a TOML file into an editable document. A missing file yields an +/// empty document so the UI still renders with defaults — normal for a fresh +/// install. A file that *exists* but fails to parse is far more dangerous: +/// 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, +/// 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 { + let Ok(text) = std::fs::read_to_string(path) else { + return DocumentMut::default(); + }; + match text.parse::() { + Ok(doc) => doc, + Err(e) => { + let backup = PathBuf::from(format!("{}.bak", path.display())); + eprintln!( + "bos-settings: {} failed to parse ({e}); backed up to {} before falling back to defaults", + path.display(), + backup.display() + ); + let _ = std::fs::write(&backup, &text); + DocumentMut::default() + } + } +} + +/// Write the document back to disk, creating parent dirs as needed. +pub fn save_doc(path: &Path, doc: &DocumentMut) -> Result<(), Box> { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::write(path, doc.to_string())?; + Ok(()) +} + +pub fn config_dir() -> PathBuf { + // Honour XDG_CONFIG_HOME if set; otherwise fall back to $HOME/.config. + if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") { + let p = PathBuf::from(xdg); + if p.is_absolute() { + return p; + } + } + let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string()); + PathBuf::from(home).join(".config") +} + +// --- typed readers (walk a dotted path, return None if absent/wrong type) --- + +fn get<'a>(doc: &'a DocumentMut, path: &[&str]) -> Option<&'a Item> { + let mut tbl = doc.as_table(); + let (last, parents) = path.split_last()?; + for key in parents { + tbl = tbl.get(key)?.as_table()?; + } + tbl.get(last) +} + +pub fn get_bool(doc: &DocumentMut, path: &[&str]) -> Option { + get(doc, path)?.as_bool() +} +pub fn get_str(doc: &DocumentMut, path: &[&str]) -> Option { + get(doc, path)?.as_str().map(str::to_string) +} +pub fn get_i64(doc: &DocumentMut, path: &[&str]) -> Option { + get(doc, path)?.as_integer() +} +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)) +} +/// Read an array of strings (e.g. modules.disable, contexts[].priority). +pub fn get_str_list(doc: &DocumentMut, path: &[&str]) -> Vec { + match get(doc, path).and_then(Item::as_array) { + Some(arr) => arr + .iter() + .filter_map(|v| v.as_str().map(str::to_string)) + .collect(), + None => Vec::new(), + } +} + +// --- setters (auto-create intermediate tables, replace only the leaf) --- + +fn table_at_mut<'a>(doc: &'a mut DocumentMut, parents: &[&str]) -> &'a mut Table { + let mut tbl = doc.as_table_mut(); + for key in parents { + let entry = tbl.entry(key).or_insert_with(|| Item::Table(Table::new())); + if !entry.is_table() { + *entry = Item::Table(Table::new()); + } + tbl = entry.as_table_mut().expect("just ensured table"); + } + tbl +} + +fn set_item(doc: &mut DocumentMut, path: &[&str], item: Item) { + let Some((last, parents)) = path.split_last() else { + return; + }; + table_at_mut(doc, parents).insert(last, item); +} + +pub fn set_bool(doc: &mut DocumentMut, path: &[&str], v: bool) { + set_item(doc, path, value(v)); +} +pub fn set_str(doc: &mut DocumentMut, path: &[&str], v: &str) { + set_item(doc, path, value(v)); +} +pub fn set_i64(doc: &mut DocumentMut, path: &[&str], v: i64) { + set_item(doc, path, value(v)); +} +pub fn set_f64(doc: &mut DocumentMut, path: &[&str], v: f64) { + set_item(doc, path, value(v)); +} +pub fn set_str_list(doc: &mut DocumentMut, path: &[&str], items: &[String]) { + let mut arr = Array::new(); + for s in items { + arr.push(s.as_str()); + } + set_item(doc, path, Item::Value(Value::Array(arr))); +} + +/// Set a string key, or remove it entirely when the value is empty — keeps +/// optional fields out of the file rather than persisting `key = ""`. +pub fn set_str_or_remove(doc: &mut DocumentMut, path: &[&str], v: &str) { + if v.is_empty() { + remove(doc, path); + } else { + set_str(doc, path, v); + } +} + +pub fn remove(doc: &mut DocumentMut, path: &[&str]) { + if let Some((last, parents)) = path.split_last() { + let mut tbl = doc.as_table_mut(); + for key in parents { + match tbl.get_mut(key).and_then(Item::as_table_mut) { + Some(t) => tbl = t, + None => return, + } + } + tbl.remove(last); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn edits_preserve_unmodelled_keys_and_comments() { + let src = "\ +# a leading comment +[daemon] +log_level = \"info\" + +[calendar] +password = \"secret\" # keep me +"; + let mut doc: DocumentMut = src.parse().unwrap(); + // Modify a single modelled key. + set_str(&mut doc, &["daemon", "log_level"], "debug"); + // A key/section the UI never touches must survive untouched. + let out = doc.to_string(); + assert!(out.contains("log_level = \"debug\"")); + assert!(out.contains("password = \"secret\"")); + assert!(out.contains("# keep me")); + assert!(out.contains("# a leading comment")); + } + + #[test] + fn setters_create_missing_tables() { + 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_i64(&doc, &["adapters", "power", "poll_interval_secs"]), + Some(45) + ); + } + + #[test] + fn empty_string_removes_key() { + let mut doc: DocumentMut = "[calendar]\nurl = \"x\"\n".parse().unwrap(); + set_str_or_remove(&mut doc, &["calendar", "url"], ""); + assert_eq!(get_str(&doc, &["calendar", "url"]), None); + } + + #[test] + fn str_list_roundtrips() { + let mut doc = DocumentMut::new(); + let items = vec!["a".to_string(), "b".to_string()]; + set_str_list(&mut doc, &["modules", "disable"], &items); + assert_eq!(get_str_list(&doc, &["modules", "disable"]), items); + } +} diff --git a/bos-settings/src/main.rs b/bos-settings/src/main.rs new file mode 100644 index 0000000..a5e73fd --- /dev/null +++ b/bos-settings/src/main.rs @@ -0,0 +1,13 @@ +mod config; +mod theme; +mod ui; + +use gtk4::prelude::*; + +fn main() { + let app = gtk4::Application::builder() + .application_id("com.breadway.bos-settings") + .build(); + app.connect_activate(ui::window::build_ui); + app.run(); +} diff --git a/bos-settings/src/theme.rs b/bos-settings/src/theme.rs new file mode 100644 index 0000000..5b57ca8 --- /dev/null +++ b/bos-settings/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/bos-settings/src/ui/mod.rs b/bos-settings/src/ui/mod.rs new file mode 100644 index 0000000..3867fcd --- /dev/null +++ b/bos-settings/src/ui/mod.rs @@ -0,0 +1,4 @@ +pub mod sidebar; +pub mod views; +pub mod widgets; +pub mod window; diff --git a/bos-settings/src/ui/sidebar.rs b/bos-settings/src/ui/sidebar.rs new file mode 100644 index 0000000..5b2ef8e --- /dev/null +++ b/bos-settings/src/ui/sidebar.rs @@ -0,0 +1,141 @@ +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("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", "hyprland.lua", "video-display-symbolic"), + item("users", "Users", "system-users-symbolic"), +]; + +pub const PERSONALIZATION_ITEMS: &[SidebarItem] = &[ + 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, + } + } + + vbox.append(&list); + (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/bos-settings/src/ui/views/about.rs b/bos-settings/src/ui/views/about.rs new file mode 100644 index 0000000..8f4cdcb --- /dev/null +++ b/bos-settings/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/bos-settings/src/ui/views/aur.rs b/bos-settings/src/ui/views/aur.rs new file mode 100644 index 0000000..b526224 --- /dev/null +++ b/bos-settings/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