Compare commits
4 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c20a6d966c | ||
|
|
50efebe5df | ||
|
|
168a449edf | ||
|
|
1671af82ee |
29 changed files with 204 additions and 2558 deletions
|
|
@ -1,24 +0,0 @@
|
|||
name: check
|
||||
|
||||
# Fast-fail lint/test on short-lived work branches, before it ever reaches
|
||||
# main and triggers a dev-track release build.
|
||||
on:
|
||||
push:
|
||||
branches: ['feature/**', 'fix/**']
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: [self-hosted, hestia]
|
||||
steps:
|
||||
- name: checkout
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rm -rf src && mkdir src
|
||||
git clone --branch "${GITHUB_REF_NAME}" --depth 1 \
|
||||
"https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src
|
||||
|
||||
- name: clippy
|
||||
run: cd src && bash ci/build.sh cargo clippy --all-targets --locked -- -D warnings
|
||||
|
||||
- name: test
|
||||
run: cd src && bash ci/build.sh cargo test --release --locked
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
name: dev release
|
||||
|
||||
# Publishes a dev-track build on every push to `main` (the trunk
|
||||
# branch — there is no separate `dev` branch). See bread-ecosystem's
|
||||
# docs/release-channels.md for the release-track policy this is part of.
|
||||
on:
|
||||
push:
|
||||
branches: ['main']
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [self-hosted, hestia]
|
||||
steps:
|
||||
- name: checkout
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rm -rf src && mkdir src
|
||||
git clone --branch main --depth 1 \
|
||||
"https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src
|
||||
|
||||
- name: build
|
||||
run: cd src && bash ci/build.sh cargo build --release --locked
|
||||
|
||||
- name: test
|
||||
run: cd src && bash ci/build.sh cargo test --release --locked
|
||||
|
||||
- name: compute dev version
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd src
|
||||
# Base the dev version off the latest published stable tag,
|
||||
# not Cargo.toml — Cargo.toml can go stale relative to the last
|
||||
# real release (seen in practice: breadbox/breadpad/breadcrumbs/
|
||||
# breadpaper), which would make a dev build sort as OLDER than
|
||||
# what's already installed and bakery would correctly refuse it.
|
||||
LATEST_TAG="$(git ls-remote --tags --refs \
|
||||
"https://git.breadway.dev/${GITHUB_REPOSITORY}.git" 'v*' \
|
||||
| awk -F/ '{print $NF}' | sed 's/^v//' | (grep -v -- '-' || true) | sort -V | tail -1)"
|
||||
if [ -n "${LATEST_TAG}" ]; then
|
||||
CUR="${LATEST_TAG}"
|
||||
else
|
||||
CUR="$(grep -m1 '^version' Cargo.toml | sed -E 's/.*"(.*)".*/\1/')"
|
||||
fi
|
||||
IFS='.' read -r MA MI PA <<< "${CUR}"
|
||||
SHA="$(git rev-parse --short HEAD)"
|
||||
TS="$(date -u +%Y%m%d%H%M%S)"
|
||||
echo "VERSION=${MA}.${MI}.$((PA + 1))-dev.${TS}+${SHA}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: prepare artifacts
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PKG_DIR="/srv/breadway-dl/dev/breadpaper/${VERSION}"
|
||||
mkdir -p "${PKG_DIR}"
|
||||
cp "src/target/release/breadpaper" "${PKG_DIR}/breadpaper-x86_64"
|
||||
strip "${PKG_DIR}/breadpaper-x86_64"
|
||||
sha256sum "${PKG_DIR}/breadpaper-x86_64" | awk '{print $1}' \
|
||||
> "${PKG_DIR}/breadpaper-x86_64.sha256"
|
||||
cp src/LICENSE "${PKG_DIR}/"
|
||||
cp src/packaging/breadpaper.desktop "${PKG_DIR}/"
|
||||
cp src/bakery.toml "${PKG_DIR}/bakery.toml"
|
||||
ln -sfn "${VERSION}" "/srv/breadway-dl/dev/breadpaper/latest"
|
||||
|
||||
# No GitHub Release upload — dev, like the other non-stable track,
|
||||
# is only distributed via dl.breadway.dev/dev/.
|
||||
- name: regenerate dev index.json
|
||||
env:
|
||||
MINISIGN_SEC_KEY: ${{ secrets.BAKERY_MINISIGN_SEC_KEY_PATH }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${MINISIGN_SEC_KEY:-}" ]; then
|
||||
echo "::error::BAKERY_MINISIGN_SEC_KEY_PATH secret not set — refusing to regenerate dev index.json unsigned (would leave a stale signature mismatched against fresh content and break bakery for everyone on the dev track)"
|
||||
exit 1
|
||||
fi
|
||||
rm -rf /tmp/bread-ecosystem-ci-* 2>/dev/null || true
|
||||
# mktemp: a fixed clone path races when multiple repos' dev/beta
|
||||
# workflows run close together on the same self-hosted runner.
|
||||
ECOSYSTEM_CI_DIR="$(mktemp -d /tmp/bread-ecosystem-ci-XXXXXX)"
|
||||
git clone --branch main https://git.breadway.dev/Breadway/bread-ecosystem.git "${ECOSYSTEM_CI_DIR}"
|
||||
TRACK=dev bash "${ECOSYSTEM_CI_DIR}/scripts/gen-index.sh"
|
||||
rm -rf "${ECOSYSTEM_CI_DIR}"
|
||||
19
.forgejo/workflows/mirror.yml
Normal file
19
.forgejo/workflows/mirror.yml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
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
|
||||
git push --prune \
|
||||
"https://x-access-token:${{ secrets.MIRROR_TOKEN }}@github.com/Breadway/breadpaper.git" \
|
||||
'+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*'
|
||||
37
.forgejo/workflows/package.yml
Normal file
37
.forgejo/workflows/package.yml
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
name: Build and publish package
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
|
||||
jobs:
|
||||
package:
|
||||
runs-on: [self-hosted, hestia]
|
||||
container:
|
||||
image: archlinux:latest
|
||||
steps:
|
||||
- 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
|
||||
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="breadpaper-${VERSION}/" HEAD \
|
||||
> packaging/arch/breadpaper-${VERSION}.tar.gz
|
||||
SHA=$(sha256sum packaging/arch/breadpaper-${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
|
||||
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"
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
name: beta (rc) release
|
||||
|
||||
# Publishes a beta-track build for any `vX.Y.Z-rc.N` prerelease tag
|
||||
# pushed to `main` — there is no separate `beta` branch; "freezing" is
|
||||
# just pausing pushes to main while an RC gets tested. See
|
||||
# bread-ecosystem's docs/release-channels.md for the release-track policy.
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: ${{ contains(github.ref_name, '-rc.') }}
|
||||
runs-on: [self-hosted, hestia]
|
||||
steps:
|
||||
- name: checkout
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rm -rf src && mkdir src
|
||||
git clone --branch "${GITHUB_REF_NAME}" --depth 1 \
|
||||
"https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src
|
||||
|
||||
- name: build
|
||||
run: cd src && bash ci/build.sh cargo build --release --locked
|
||||
|
||||
- name: test
|
||||
run: cd src && bash ci/build.sh cargo test --release --locked
|
||||
|
||||
- name: prepare artifacts
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VERSION="${GITHUB_REF_NAME#v}"
|
||||
PKG_DIR="/srv/breadway-dl/beta/breadpaper/${VERSION}"
|
||||
mkdir -p "${PKG_DIR}"
|
||||
cp "src/target/release/breadpaper" "${PKG_DIR}/breadpaper-x86_64"
|
||||
strip "${PKG_DIR}/breadpaper-x86_64"
|
||||
sha256sum "${PKG_DIR}/breadpaper-x86_64" | awk '{print $1}' \
|
||||
> "${PKG_DIR}/breadpaper-x86_64.sha256"
|
||||
cp src/LICENSE "${PKG_DIR}/"
|
||||
cp src/packaging/breadpaper.desktop "${PKG_DIR}/"
|
||||
cp src/bakery.toml "${PKG_DIR}/bakery.toml"
|
||||
ln -sfn "${VERSION}" "/srv/breadway-dl/beta/breadpaper/latest"
|
||||
|
||||
# No GitHub Release upload — beta, like dev, is only distributed via
|
||||
# dl.breadway.dev/beta/.
|
||||
- name: regenerate beta index.json
|
||||
env:
|
||||
MINISIGN_SEC_KEY: ${{ secrets.BAKERY_MINISIGN_SEC_KEY_PATH }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${MINISIGN_SEC_KEY:-}" ]; then
|
||||
echo "::error::BAKERY_MINISIGN_SEC_KEY_PATH secret not set — refusing to regenerate beta index.json unsigned (would leave a stale signature mismatched against fresh content and break bakery for everyone on the beta track)"
|
||||
exit 1
|
||||
fi
|
||||
rm -rf /tmp/bread-ecosystem-ci-* 2>/dev/null || true
|
||||
# mktemp: a fixed clone path races when multiple repos' dev/beta
|
||||
# workflows run close together on the same self-hosted runner.
|
||||
ECOSYSTEM_CI_DIR="$(mktemp -d /tmp/bread-ecosystem-ci-XXXXXX)"
|
||||
git clone https://git.breadway.dev/Breadway/bread-ecosystem.git "${ECOSYSTEM_CI_DIR}"
|
||||
TRACK=beta bash "${ECOSYSTEM_CI_DIR}/scripts/gen-index.sh"
|
||||
rm -rf "${ECOSYSTEM_CI_DIR}"
|
||||
|
|
@ -4,57 +4,50 @@ on:
|
|||
push:
|
||||
tags: ["v*"]
|
||||
|
||||
env:
|
||||
DL_DIR: /srv/breadway-dl
|
||||
ECOSYSTEM_DIR: /tmp/bread-ecosystem-ci
|
||||
PATH: /home/breadway/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: ${{ !contains(github.ref_name, '-rc.') }}
|
||||
runs-on: [self-hosted, hestia]
|
||||
runs-on: hestia
|
||||
defaults:
|
||||
run:
|
||||
working-directory: /tmp/breadpaper-build
|
||||
|
||||
steps:
|
||||
- name: checkout
|
||||
working-directory: /tmp
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rm -rf src && mkdir src
|
||||
rm -rf /tmp/breadpaper-build
|
||||
git clone --branch "${GITHUB_REF_NAME}" --depth 1 \
|
||||
"https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src
|
||||
"https://git.breadway.dev/${GITHUB_REPOSITORY}.git" /tmp/breadpaper-build
|
||||
|
||||
- name: build
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ ! -f src/ci/build.sh ]; then
|
||||
echo "::error::ci/build.sh is missing — bakery release builds must go through the shared CI wrapper"
|
||||
exit 1
|
||||
fi
|
||||
cd src && bash ci/build.sh cargo build --release --locked || {
|
||||
echo "::error::cargo build --release --locked failed. If Cargo.lock drifted, update and commit it; do not drop --locked."
|
||||
exit 1
|
||||
}
|
||||
run: /home/breadway/.cargo/bin/cargo build --release --locked
|
||||
|
||||
- name: test
|
||||
run: cd src && bash ci/build.sh cargo test --release --locked
|
||||
run: /home/breadway/.cargo/bin/cargo test --release --locked
|
||||
|
||||
- name: prepare artifacts
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VERSION="${GITHUB_REF_NAME#v}"
|
||||
PKG_DIR="/srv/breadway-dl/breadpaper/${VERSION}"
|
||||
PKG_DIR="${DL_DIR}/breadpaper/${VERSION}"
|
||||
mkdir -p "${PKG_DIR}"
|
||||
cp src/target/release/breadpaper "${PKG_DIR}/breadpaper-x86_64"
|
||||
cp target/release/breadpaper "${PKG_DIR}/breadpaper-x86_64"
|
||||
strip "${PKG_DIR}/breadpaper-x86_64"
|
||||
sha256sum "${PKG_DIR}/breadpaper-x86_64" | awk '{print $1}' \
|
||||
> "${PKG_DIR}/breadpaper-x86_64.sha256"
|
||||
cp src/LICENSE "${PKG_DIR}/"
|
||||
cp src/packaging/breadpaper.desktop "${PKG_DIR}/"
|
||||
cp src/bakery.toml "${PKG_DIR}/bakery.toml"
|
||||
ln -sfn "${VERSION}" "/srv/breadway-dl/breadpaper/latest"
|
||||
cp bakery.toml "${PKG_DIR}/bakery.toml"
|
||||
ln -sfn "${VERSION}" "${DL_DIR}/breadpaper/latest"
|
||||
|
||||
- name: ensure bread-ecosystem
|
||||
working-directory: /tmp
|
||||
run: |
|
||||
rm -rf "${ECOSYSTEM_DIR}"
|
||||
git clone https://github.com/Breadway/bread-ecosystem.git "${ECOSYSTEM_DIR}"
|
||||
|
||||
- name: regenerate index.json
|
||||
env:
|
||||
MINISIGN_SEC_KEY: ${{ secrets.BAKERY_MINISIGN_SEC_KEY_PATH }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${MINISIGN_SEC_KEY:-}" ]; then
|
||||
echo "::error::BAKERY_MINISIGN_SEC_KEY_PATH secret not set — refusing to regenerate index.json unsigned (would leave a stale signature mismatched against fresh content and break bakery for everyone)"
|
||||
exit 1
|
||||
fi
|
||||
rm -rf /tmp/bread-ecosystem-ci
|
||||
git clone https://git.breadway.dev/Breadway/bread-ecosystem.git /tmp/bread-ecosystem-ci
|
||||
bash /tmp/bread-ecosystem-ci/scripts/gen-index.sh
|
||||
working-directory: /tmp
|
||||
run: bash "${ECOSYSTEM_DIR}/scripts/gen-index.sh"
|
||||
|
|
|
|||
38
.github/workflows/ci.yml
vendored
38
.github/workflows/ci.yml
vendored
|
|
@ -1,38 +0,0 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTFLAGS: "-D warnings"
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: fmt · clippy · test · build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Cache cargo
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Format check
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
- name: Clippy
|
||||
run: cargo clippy --all-targets -- -D warnings
|
||||
|
||||
- name: Test
|
||||
run: cargo test --all --verbose
|
||||
|
||||
- name: Release build
|
||||
run: cargo build --release --verbose
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,3 +1 @@
|
|||
/target
|
||||
|
||||
# Local hygiene notes (not for commit)
|
||||
|
|
|
|||
28
AGENTS.md
28
AGENTS.md
|
|
@ -1,28 +0,0 @@
|
|||
# AGENTS.md — Repo hygiene
|
||||
|
||||
This repo follows the branch/release workflow documented in `CONTRIBUTING.md`
|
||||
— read and follow it for any git, branch, or release work here (the
|
||||
single-trunk `main` model, `feature/x`/`fix/x` branch naming, RC-tag-driven
|
||||
beta releases, etc). Don't improvise a different workflow. There is no
|
||||
`dev` or `beta` branch — that three-branch model was retired.
|
||||
|
||||
## Remotes
|
||||
- `origin` — Forgejo (`git.breadway.dev` via Hestia, SSH) — authoritative.
|
||||
- `github` — GitHub mirror. Push both when publishing.
|
||||
|
||||
## CI
|
||||
- `check.yml` — clippy + test, triggers on push to `feature/**`/`fix/**`.
|
||||
- `dev-release.yml` — triggers on push to `main`.
|
||||
- `rc-release.yml` — triggers on `vX.Y.Z-rc.N` tag push.
|
||||
- `release.yml` — triggers on any other `v*` tag push.
|
||||
|
||||
All four run on a self-hosted runner (`hestia`) inside a pinned Arch
|
||||
container — not the host's native environment. The Containerfile/build
|
||||
script are shared across bread-ecosystem products and live in
|
||||
`bread-ecosystem/ci/`; this repo's `ci/build.sh` clones that repo at the
|
||||
sha in `ci/bread-ecosystem.rev` (deliberately pinned, not `main`) and
|
||||
delegates to it. Nothing runs automatically on plain commits or PRs
|
||||
beyond what's listed.
|
||||
|
||||
## Don't
|
||||
- Don't embed credentials in remote URLs — SSH or a credential helper only.
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
# Contributing
|
||||
|
||||
`breadpaper` — Wallpaper manager for the bread desktop.
|
||||
|
||||
Part of the bread ecosystem; this repo follows the same branch/release
|
||||
workflow as every other ecosystem product.
|
||||
|
||||
## Branches
|
||||
|
||||
There is one long-lived branch: **`main`**. All day-to-day work lands here.
|
||||
Every push to `main` automatically builds and publishes a **dev-track**
|
||||
build (see Tracks below) — a real install you can test before cutting
|
||||
anything more formal.
|
||||
|
||||
New work — features and bug fixes alike — goes on a short-lived branch:
|
||||
|
||||
```
|
||||
feature/<short-name>
|
||||
fix/<issue-number-or-short-name>
|
||||
```
|
||||
|
||||
Branch off `main`, open a PR/push back into `main` when ready. Short-lived
|
||||
branches get deleted on merge — they never accumulate the kind of drift a
|
||||
second long-lived branch does.
|
||||
|
||||
## The release cycle
|
||||
|
||||
There's no separate `beta` or release branch — "stable" and "beta" are both
|
||||
just **tags** on `main`, not branches that need to be kept in sync:
|
||||
|
||||
1. Work accumulates on `main` via `feature/x` / `fix/x` branches. Each push
|
||||
auto-publishes a dev build — install it with `bakery track set dev` and
|
||||
`bakery update --all`, then fix anything broken with another push.
|
||||
2. When you want to stabilize before a real release, tag a release
|
||||
candidate: `git tag vX.Y.Z-rc.1 && git push origin vX.Y.Z-rc.1` (push to
|
||||
both remotes). That tag alone triggers a beta-track build —
|
||||
"freezing" is just pausing pushes to `main` while you test it, not a
|
||||
branch operation. Cut `-rc.2`, `-rc.3`, etc. for further fixes.
|
||||
3. Once an RC has gone without issues, tag the real release:
|
||||
`git tag vX.Y.Z && git push origin vX.Y.Z` — that's what triggers the
|
||||
signed stable release build.
|
||||
|
||||
## Tracks, from a user's perspective
|
||||
|
||||
```
|
||||
bakery track show # what you're currently on (defaults to stable)
|
||||
bakery track set dev # or beta, or stable
|
||||
bakery update --all # pull the latest build on your current track
|
||||
```
|
||||
|
||||
| Track | What it is | Published from |
|
||||
|--------|-----------|-----------------|
|
||||
| `stable` | The last tagged release | a `vX.Y.Z` tag |
|
||||
| `beta` | Latest release candidate | a `vX.Y.Z-rc.N` tag |
|
||||
| `dev` | Bleeding edge | `main`, on every push |
|
||||
|
||||
Dev versions are auto-computed (`X.Y.Z-dev.<timestamp>+<sha>`) from the
|
||||
latest published stable tag, so they always sort as newer than what you
|
||||
have installed — no manual version bumping needed. Beta versions are just
|
||||
the RC tag itself (already valid semver, already sorts below the real
|
||||
release it's a candidate for).
|
||||
|
||||
## Local development
|
||||
|
||||
```sh
|
||||
cargo build --release
|
||||
cargo test --release
|
||||
```
|
||||
|
||||
## CI
|
||||
|
||||
- `dev-release.yml` — triggered on push to `main`.
|
||||
- `rc-release.yml` — triggered on any `vX.Y.Z-rc.N` tag push.
|
||||
- `release.yml` — triggered on any other `v*` tag push, cuts the actual
|
||||
stable release.
|
||||
|
||||
All CI runs on a self-hosted runner; nothing runs automatically on plain
|
||||
commits or PRs beyond the track builds above. See
|
||||
[bread-ecosystem's docs/release-channels.md](https://git.breadway.dev/Breadway/bread-ecosystem/src/branch/main/docs/release-channels.md)
|
||||
for the full policy, including how a new product gets wired onto these tracks.
|
||||
|
||||
## Questions
|
||||
|
||||
Open an issue on this repo's Forgejo tracker.
|
||||
716
Cargo.lock
generated
716
Cargo.lock
generated
|
|
@ -54,50 +54,15 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.104"
|
||||
version = "1.0.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
|
||||
|
||||
[[package]]
|
||||
name = "bread-shared"
|
||||
version = "0.7.0"
|
||||
source = "git+https://git.breadway.dev/Breadway/bread?tag=v0.7.0#22e34e2cf2202305d7960759dfccb54dc79f948b"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"toml 0.8.23",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bread-theme"
|
||||
version = "0.7.4"
|
||||
source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.4#fcba3760387e2523edb71350f8efea3bc851b21e"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"gtk4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
|
||||
|
||||
[[package]]
|
||||
name = "bread-utils"
|
||||
version = "0.7.2"
|
||||
source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.2#30517f161724132cdeb658c04cf5e490be07ee73"
|
||||
version = "0.3.0"
|
||||
source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.3.0#8e82d2d833e992ce939a5b836f910ee109f2e939"
|
||||
dependencies = [
|
||||
"bread-shared",
|
||||
"dirs",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
|
@ -105,49 +70,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "breadpaper"
|
||||
version = "0.1.14"
|
||||
version = "0.1.11"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bread-theme",
|
||||
"bread-utils",
|
||||
"clap",
|
||||
"gtk4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"toml 0.8.23",
|
||||
]
|
||||
|
||||
[[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]]
|
||||
|
|
@ -158,9 +85,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
|||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.6"
|
||||
version = "4.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
|
||||
checksum = "dd059f9da4f5c36b3787f65d38ccaab1cc315f07b01f89abc8359ee6a8205011"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
|
|
@ -168,9 +95,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.6"
|
||||
version = "4.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
|
||||
checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
|
|
@ -180,14 +107,14 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.6.4"
|
||||
version = "4.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
|
||||
checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.3",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -223,141 +150,6 @@ dependencies = [
|
|||
"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 = "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.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-macro",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gdk-pixbuf"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25f420376dbee041b2db374ce4573892a36222bb3f6c0c43e24f0d67eae9b646"
|
||||
dependencies = [
|
||||
"gdk-pixbuf-sys",
|
||||
"gio",
|
||||
"glib",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gdk-pixbuf-sys"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48f31b37b1fc4b48b54f6b91b7ef04c18e00b4585d98359dd7b998774bbd91fb"
|
||||
dependencies = [
|
||||
"gio-sys",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gdk4"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d81e2a6c6ecba2aab60633a98df1868b03fa0bfdce8105edc27c1bccf71f0e39"
|
||||
dependencies = [
|
||||
"cairo-rs",
|
||||
"gdk-pixbuf",
|
||||
"gdk4-sys",
|
||||
"gio",
|
||||
"glib",
|
||||
"libc",
|
||||
"pango",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gdk4-sys"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d8f608d8d7d229975c4d0d026f5d3071598c4ddab3c5262b0a31840fec78d13"
|
||||
dependencies = [
|
||||
"cairo-sys-rs",
|
||||
"gdk-pixbuf-sys",
|
||||
"gio-sys",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"pango-sys",
|
||||
"pkg-config",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
|
|
@ -369,216 +161,12 @@ dependencies = [
|
|||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gio"
|
||||
version = "0.22.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b3e1f669909c326b9413bde5a742097b8c90a7d78f45326db13668984769ded"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"gio-sys",
|
||||
"glib",
|
||||
"libc",
|
||||
"pin-project-lite",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gio-sys"
|
||||
version = "0.22.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "353fdc7da7cd16da916104b1e0e4e7de380ec9c8aaa20d4d742d66310ab4b0d5"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glib"
|
||||
version = "0.22.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddbcf514bd1881fc1b960e4e52b4e82873f4da3bceddbd58d42827b508888100"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
"gio-sys",
|
||||
"glib-macros",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"memchr",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glib-macros"
|
||||
version = "0.22.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "506d23499707c7142898429757e8d9a3871d965239a2cb66dfa05052be6d6f19"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glib-sys"
|
||||
version = "0.22.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "030967459f9f676851872c6304adea7825c6d462ec9b72554c733cf0c5952233"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gobject-sys"
|
||||
version = "0.22.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22a861859b887a79cf461359c192c97a57d8fb0229dd291232e57aa11f6fa72c"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "graphene-rs"
|
||||
version = "0.22.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb856b9c558971c3f13ab692358926da710b046932a4e087aedcc35b040d7dff"
|
||||
dependencies = [
|
||||
"glib",
|
||||
"graphene-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "graphene-sys"
|
||||
version = "0.22.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c7ffdfde88f3570d3705e0d8a2433e036d387a1f2930bbf47eafcb5f569fd04"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gsk4"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b867be1c5f14dcb8f552c0eff6e9a9b1da5f8b43943e8efc3a63c889d84952ff"
|
||||
dependencies = [
|
||||
"cairo-rs",
|
||||
"gdk4",
|
||||
"glib",
|
||||
"graphene-rs",
|
||||
"gsk4-sys",
|
||||
"libc",
|
||||
"pango",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gsk4-sys"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b7c7eb2e681ee896646cfb8872b431f24d09f53ba9283289d9b10caa6707088"
|
||||
dependencies = [
|
||||
"cairo-sys-rs",
|
||||
"gdk4-sys",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"graphene-sys",
|
||||
"libc",
|
||||
"pango-sys",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gtk4"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98a0a0466484f64b07b5b8184d43fa46be78eb0b8e04ae4e179af31d770b76d9"
|
||||
dependencies = [
|
||||
"cairo-rs",
|
||||
"field-offset",
|
||||
"futures-channel",
|
||||
"gdk-pixbuf",
|
||||
"gdk4",
|
||||
"gio",
|
||||
"glib",
|
||||
"graphene-rs",
|
||||
"gsk4",
|
||||
"gtk4-macros",
|
||||
"gtk4-sys",
|
||||
"libc",
|
||||
"pango",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gtk4-macros"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ac7179400a36a04de039c24206bb841c5596992b907b43b23ee8d5bdc40d00e"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gtk4-sys"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82b8f954786af0b1984425c4446b77f5ff6594346181316be3f850caab1c6f01"
|
||||
dependencies = [
|
||||
"cairo-sys-rs",
|
||||
"gdk-pixbuf-sys",
|
||||
"gdk4-sys",
|
||||
"gio-sys",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"graphene-sys",
|
||||
"gsk4-sys",
|
||||
"libc",
|
||||
"pango-sys",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
|
|
@ -593,15 +181,15 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
|||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.189"
|
||||
version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.20"
|
||||
version = "0.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28d0a00925a9f930d679b6789b721e3a7f9ed110f41b86d2497caa780c3a070a"
|
||||
checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
|
@ -612,15 +200,6 @@ version = "2.8.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
|
|
@ -633,64 +212,20 @@ version = "0.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "pango"
|
||||
version = "0.22.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d800d8d0de2ad5d0fb046f5344dbaba14a003cf3dd27cc21d85893d35ea316c"
|
||||
dependencies = [
|
||||
"gio",
|
||||
"glib",
|
||||
"pango-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pango-sys"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd111a20ca90fedf03e09c59783c679c00900f1d8491cca5399f5e33609d5d6"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "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.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
||||
dependencies = [
|
||||
"toml_edit 0.25.13+spec-1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
version = "1.0.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
|
@ -706,26 +241,11 @@ dependencies = [
|
|||
"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.229"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
|
|
@ -733,29 +253,29 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.229"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.229"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.3",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.151"
|
||||
version = "1.0.150"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
|
|
@ -764,36 +284,6 @@ dependencies = [
|
|||
"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 = "strsim"
|
||||
version = "0.11.1"
|
||||
|
|
@ -811,36 +301,6 @@ dependencies = [
|
|||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "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.4+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"
|
||||
|
|
@ -858,101 +318,9 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
"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.4+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde_core",
|
||||
"serde_spanned 1.1.1",
|
||||
"toml_datetime 1.1.1+spec-1.1.0",
|
||||
"toml_parser",
|
||||
"toml_writer",
|
||||
"winnow 1.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.1.1+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned 0.6.9",
|
||||
"toml_datetime 0.6.11",
|
||||
"toml_write",
|
||||
"winnow 0.7.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.25.13+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime 1.1.1+spec-1.1.0",
|
||||
"toml_parser",
|
||||
"winnow 1.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.1.3+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
|
||||
dependencies = [
|
||||
"winnow 1.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_write"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||
|
||||
[[package]]
|
||||
name = "toml_writer"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
|
|
@ -965,12 +333,6 @@ version = "0.2.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[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"
|
||||
|
|
@ -1058,24 +420,6 @@ version = "0.48.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "breadpaper"
|
||||
version = "0.1.14"
|
||||
version = "0.1.11"
|
||||
edition = "2024"
|
||||
description = "Wallpaper manager for the bread desktop"
|
||||
license = "MIT"
|
||||
|
|
@ -8,9 +8,4 @@ license = "MIT"
|
|||
[dependencies]
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
anyhow = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
toml = "0.8"
|
||||
gtk4 = { version = "0.11", features = ["v4_12"] }
|
||||
bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.4", features = ["gtk"] }
|
||||
bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2", features = ["bread-client"] }
|
||||
bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.3.0" }
|
||||
|
|
|
|||
70
EVENTS.md
70
EVENTS.md
|
|
@ -1,70 +0,0 @@
|
|||
# breadpaper — bread event integration
|
||||
|
||||
breadpaper is a wallpaper setter: it works exactly the same with or
|
||||
without `breadd` running. When breadd *is* present, a successful
|
||||
`breadpaper set` (or the bare-path shorthand) publishes `bread.paper.changed`
|
||||
into the shared bread automation fabric, and `breadpaper listen` honors
|
||||
`bread.command.paper.set` and `bread.command.paper.library`. See the parent `bread` repo's
|
||||
`Documentation.md` — specifically its "Namespaces" and "Integrating a
|
||||
bread\* app" sections — for the general convention this follows.
|
||||
|
||||
App id: **`paper`**. Transport: `bread-utils`'s `bread_client` module
|
||||
(feature `bread-client`) — the CLI links it directly. One-shot
|
||||
`set`/`get` use `BreadClient::connect("paper")` + `emit` only. The
|
||||
long-running `listen` subcommand holds a `subscribe` open.
|
||||
|
||||
`breadpaper listen` is fail-silent if breadd is down: `subscribe`
|
||||
reconnects with backoff and simply delivers nothing until the daemon
|
||||
comes back. It also honors `bread.monitor.connected` by re-applying
|
||||
`~/.config/breadpaper/current.json`. The one-shot `set`/`get`/`library`
|
||||
/`apply` path does not require `listen`. Modules that want to change
|
||||
the wallpaper without a listener can still shell out:
|
||||
|
||||
```lua
|
||||
bread.exec("breadpaper set /path/to/image.png")
|
||||
```
|
||||
|
||||
A workflow that publishes the command instead should wait for the
|
||||
confirmation, not assume the emit finished the set:
|
||||
|
||||
```lua
|
||||
bread.emit("bread.command.paper.set", { path = "/path/to/image.png" })
|
||||
bread.wait("bread.paper.set.done", { timeout = 10000 })
|
||||
```
|
||||
|
||||
## Events published (`bread.paper.*`)
|
||||
|
||||
| Event | Data | When |
|
||||
|-------|------|------|
|
||||
| `bread.paper.changed` | `{ "path": "<wallpaper>", "output"?: "<name>" }` | After a successful `set` / `set --output` (awww + palette + theme write), including when `listen` honors `bread.command.paper.set` or restores `current.json` after `bread.monitor.connected`. `path` is the canonical absolute path that was applied. `output` is the compositor output name when only one monitor was targeted; omit (or `null`) means every output. Not emitted on `get`, and not emitted if the set fails. |
|
||||
| `bread.paper.set.done` | `{ "path": "<wallpaper>", "output"?: "<name>" }` | `bread.command.paper.set` was received and `set()` / `set_on()` succeeded. `path` is the canonical absolute path that was applied. `output` is present when the command targeted one monitor. Not emitted by the one-shot CLI `set` — that path only publishes `changed`. |
|
||||
| `bread.paper.set.failed` | `{ "error": "<message>", "path"?: "<requested>", "output"?: "<name>" }` | `bread.command.paper.set` was received but `set()` / `set_on()` failed, or `data.path` was missing/not a string. `path` is the requested (not canonical) path when one was supplied. `output` is echoed when the command included one. |
|
||||
| `bread.paper.library.done` | `{}` | `bread.command.paper.library` was received and a `breadpaper library` process was started. Not emitted by the one-shot CLI `library` / `browse`. |
|
||||
| `bread.paper.library.failed` | `{ "error": "<message>" }` | `bread.command.paper.library` was received but the library process could not be spawned. |
|
||||
|
||||
## Commands honored (`bread.command.paper.*`)
|
||||
|
||||
Honored only while `breadpaper listen` is running. A
|
||||
`bread-emit bread.command.paper.set` with no listener is a silent no-op
|
||||
— that is the documented bread convention, not a breadpaper bug.
|
||||
|
||||
| Verb | Data | Effect |
|
||||
|------|------|--------|
|
||||
| `set` | `{ "path": "...", "output"?: "..." }` | Missing `output` calls `set()` (all live outputs, global `wal -i`, `bread-theme reload`). A string `output` calls `set_on()` (that monitor only; no global wal unless it is the focused Hyprland monitor). Emits `bread.paper.set.done` / `.failed`. A successful set also emits `bread.paper.changed`. |
|
||||
| `library` | `{}` | Spawns `breadpaper library` (GTK picker). Emits `bread.paper.library.done` once the process is started, or `bread.paper.library.failed` if the spawn fails. Clicking a thumbnail in that window applies to the picker's output and publishes `bread.paper.changed`. |
|
||||
|
||||
### Not implemented: slideshow / random / next
|
||||
|
||||
`breadpaper library` / `browse` is the in-app picker. Do not invent
|
||||
`bread.command.paper.next` / `.random` / `.cycle` (or matching events)
|
||||
ahead of a real slideshow feature. Unrecognized verbs are ignored.
|
||||
|
||||
## Fail-safe behavior
|
||||
|
||||
- If breadd isn't installed or isn't running, `emit` is a silent no-op
|
||||
(`BreadClient::emit` never blocks or errors the caller) — breadpaper
|
||||
still sets the wallpaper, generates the palette, and reloads themes.
|
||||
- `breadpaper listen` does not exit if breadd is down. The command
|
||||
subscription reconnects automatically (`BreadClient::subscribe`'s
|
||||
background thread has its own backoff loop); no restart of `listen`
|
||||
is needed once breadd returns.
|
||||
80
README.md
80
README.md
|
|
@ -1,88 +1,52 @@
|
|||
# breadpaper
|
||||
|
||||
Wallpaper setter for the bread desktop. One command sets the wallpaper
|
||||
via [awww](https://github.com/heywoodlh/awww), generates a palette with
|
||||
[pywal](https://github.com/dylanaraps/pywal) (`wal`), and runs
|
||||
`bread-theme reload`. Two monitors can keep different wallpapers (and
|
||||
per-output bread-theme files); the last path per output is stored in
|
||||
`~/.config/breadpaper/current.json`.
|
||||
|
||||
`set` / `get` / `apply` stay one-shot CLI. `breadpaper library` (alias
|
||||
`browse`) opens a GTK picker over the wallpaper directories. It is not
|
||||
a slideshow daemon.
|
||||
Wallpaper manager for the bread desktop. Sets a wallpaper via [awww](https://github.com/heywoodlh/awww), generates a colour palette with [pywal](https://github.com/dylanaraps/pywal), and reloads bread themes in one command.
|
||||
|
||||
## Dependencies
|
||||
|
||||
Must be on `$PATH` for `set`:
|
||||
Runtime:
|
||||
|
||||
- `awww` — Wayland wallpaper (`awww img`)
|
||||
- `wal` — palette generation (`python-pywal`)
|
||||
- `bread-theme` — theme reload (bakery package, not `breadd`)
|
||||
- `awww` — Wayland wallpaper daemon
|
||||
- `python-pywal` — colour palette generation
|
||||
- `bread-theme` — theme reload (part of the bread ecosystem)
|
||||
|
||||
`library` also needs GTK4 (the window loads `bread-theme`'s shared
|
||||
stylesheet and follows the monitor it sits on). `get` without
|
||||
`--output` still reads the path pywal stored at `~/.cache/wal/wal`.
|
||||
`get --output NAME` reads `~/.config/breadpaper/current.json`.
|
||||
Build:
|
||||
|
||||
- Rust 1.85+ (edition 2024) / cargo
|
||||
|
||||
## Install
|
||||
|
||||
**From the bakery (recommended on bread OS):**
|
||||
|
||||
```
|
||||
bakery install breadpaper
|
||||
```
|
||||
|
||||
From source:
|
||||
**From source:**
|
||||
|
||||
```
|
||||
cargo build --release
|
||||
install -Dm755 target/release/breadpaper ~/.local/bin/breadpaper
|
||||
```
|
||||
|
||||
**Arch Linux (PKGBUILD):**
|
||||
|
||||
```
|
||||
cd packaging/arch
|
||||
makepkg -si
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
breadpaper <path> # shorthand for `set` (all outputs)
|
||||
breadpaper <path> --output NAME # one output
|
||||
breadpaper set <path> # awww + wal + bread-theme reload (all)
|
||||
breadpaper set <path> --output NAME
|
||||
breadpaper get # print ~/.cache/wal/wal
|
||||
breadpaper get --output NAME # path from current.json
|
||||
breadpaper apply # restore current.json
|
||||
breadpaper library # GTK picker (alias: browse)
|
||||
breadpaper library --dir PATH # also scan PATH (repeatable)
|
||||
breadpaper listen # honor bread.command.paper.set / .library
|
||||
breadpaper <path> # set wallpaper (shorthand for `set`)
|
||||
breadpaper set <path> # set wallpaper, generate palette, reload themes
|
||||
breadpaper get # print the current wallpaper path
|
||||
```
|
||||
|
||||
Supported formats: `png`, `jpg`, `jpeg`, `webp`, `gif`, `bmp`.
|
||||
|
||||
## Library
|
||||
|
||||
`breadpaper library` scans these directories (missing ones are skipped):
|
||||
|
||||
1. `~/Pictures/Wallpapers`
|
||||
2. `/usr/share/backgrounds/bos`
|
||||
|
||||
Override the list in `~/.config/breadpaper/config.toml`:
|
||||
|
||||
```toml
|
||||
library_dirs = [
|
||||
"~/Pictures/Wallpapers",
|
||||
"/usr/share/backgrounds/bos",
|
||||
]
|
||||
```
|
||||
|
||||
`BREADPAPER_LIBRARY_DIRS` (colon-separated) overrides the file. `--dir`
|
||||
appends extra roots for that invocation. Clicking a thumbnail applies
|
||||
to the monitor the picker is on (`set --output`); if the output cannot
|
||||
be resolved it falls back to all outputs.
|
||||
|
||||
## Bread events
|
||||
|
||||
After a successful `set`, breadpaper emits `bread.paper.changed` if
|
||||
`breadd` is running (silent no-op if it isn't). `breadpaper listen` is
|
||||
the optional long-running subscriber for `bread.command.paper.set` and
|
||||
`bread.command.paper.library`; it does not start by itself. Lua modules
|
||||
can still `bread.exec("breadpaper set …")` or
|
||||
`bread.exec("breadpaper library")`. See [EVENTS.md](EVENTS.md).
|
||||
The current wallpaper path is stored by pywal at `~/.cache/wal/wal`.
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
name = "breadpaper"
|
||||
description = "Wallpaper manager for the bread desktop — sets awww wallpaper, generates pywal palette, reloads bread themes"
|
||||
binaries = ["breadpaper"]
|
||||
system_deps = ["awww", "python-pywal", "gtk4"]
|
||||
optional_system_deps = []
|
||||
bread_deps = ["bread-theme"]
|
||||
license_file = "LICENSE"
|
||||
desktop_file = "breadpaper.desktop"
|
||||
system_deps = ["python-pywal"]
|
||||
optional_system_deps = ["awww"]
|
||||
bread_deps = ["bread"]
|
||||
|
||||
[install]
|
||||
post_install = []
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
147cfbbf96ae4b171027defa1130d2caddb934b1
|
||||
21
ci/build.sh
21
ci/build.sh
|
|
@ -1,21 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# Delegates to bread-ecosystem's shared CI build image/script, pinned to
|
||||
# the commit in ci/bread-ecosystem.rev — not `main`. bread-ecosystem's CI
|
||||
# files now affect every product's release pipeline, so bumping the pin
|
||||
# is a deliberate act instead of silent drift (see the bread-theme test
|
||||
# that broke here for exactly that reason, before it was pinned by rev).
|
||||
#
|
||||
# Usage: ci/build.sh cargo build --release --locked
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
REV="$(cat "${ROOT}/ci/bread-ecosystem.rev")"
|
||||
|
||||
CACHE_DIR="/tmp/bread-ecosystem-ci-${REV}"
|
||||
if [ ! -d "$CACHE_DIR" ]; then
|
||||
rm -rf /tmp/bread-ecosystem-ci-*
|
||||
git clone https://git.breadway.dev/Breadway/bread-ecosystem.git "$CACHE_DIR"
|
||||
git -C "$CACHE_DIR" checkout --quiet "$REV"
|
||||
fi
|
||||
|
||||
bash "${CACHE_DIR}/ci/build.sh" breadpaper "$ROOT" "$@"
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
# ~/.config/breadpaper/config.toml
|
||||
# Missing directories are skipped. An empty list uses the built-in defaults.
|
||||
library_dirs = [
|
||||
"~/Pictures/Wallpapers",
|
||||
"/usr/share/backgrounds/bos",
|
||||
]
|
||||
34
packaging/arch/PKGBUILD
Normal file
34
packaging/arch/PKGBUILD
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# Maintainer: Breadway <plasticbread849@gmail.com>
|
||||
|
||||
pkgname=breadpaper
|
||||
pkgver=0.1.0
|
||||
pkgrel=1
|
||||
pkgdesc="Wallpaper manager for the bread desktop"
|
||||
arch=('x86_64')
|
||||
url="https://git.breadway.dev/Breadway/breadpaper"
|
||||
license=('MIT')
|
||||
options=(!lto !debug)
|
||||
depends=('glibc')
|
||||
optdepends=(
|
||||
'python-pywal: colour palette generation from wallpaper (AUR)'
|
||||
'awww: Wayland wallpaper daemon'
|
||||
)
|
||||
makedepends=('rust' 'cargo')
|
||||
source=("${pkgname}-${pkgver}.tar.gz")
|
||||
sha256sums=('SKIP')
|
||||
|
||||
build() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
cargo build --release --locked
|
||||
}
|
||||
|
||||
check() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
cargo test --release --locked
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
install -Dm755 target/release/breadpaper "${pkgdir}/usr/bin/breadpaper"
|
||||
install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
[Desktop Entry]
|
||||
Name=Wallpapers
|
||||
Comment=Browse and apply wallpapers on the Bread desktop
|
||||
Exec=breadpaper library
|
||||
Icon=preferences-desktop-wallpaper
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Settings;DesktopSettings;
|
||||
StartupWMClass=com.breadway.breadpaper
|
||||
252
src/config.rs
252
src/config.rs
|
|
@ -1,252 +0,0 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
/// User library directory used when no config file is present.
|
||||
pub const DEFAULT_USER_LIBRARY: &str = "Pictures/Wallpapers";
|
||||
|
||||
/// Packaged BOS backgrounds, scanned when the directory exists.
|
||||
pub const DEFAULT_SYSTEM_LIBRARY: &str = "/usr/share/backgrounds/bos";
|
||||
|
||||
/// Colon-separated override of [`Config::library_dirs`]. Empty means "use
|
||||
/// the config file / defaults".
|
||||
pub const LIBRARY_DIRS_ENV: &str = "BREADPAPER_LIBRARY_DIRS";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Config {
|
||||
pub library_dirs: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
struct ConfigFile {
|
||||
#[serde(default)]
|
||||
library_dirs: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
library_dirs: default_library_dirs(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn path() -> PathBuf {
|
||||
bread_utils::xdg::config_dir("breadpaper").join("config.toml")
|
||||
}
|
||||
|
||||
pub fn load() -> Self {
|
||||
Self::load_from(&Self::path())
|
||||
}
|
||||
|
||||
pub fn load_from(path: &Path) -> Self {
|
||||
let mut cfg = match std::fs::read_to_string(path) {
|
||||
Ok(text) => match toml::from_str::<ConfigFile>(&text) {
|
||||
Ok(parsed) if !parsed.library_dirs.is_empty() => Self {
|
||||
library_dirs: parsed.library_dirs,
|
||||
},
|
||||
Ok(_) => Self::default(),
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"breadpaper: {} failed to parse ({e}); using defaults",
|
||||
path.display()
|
||||
);
|
||||
Self::default()
|
||||
}
|
||||
},
|
||||
Err(_) => Self::default(),
|
||||
};
|
||||
|
||||
if let Some(dirs) = env_library_dirs() {
|
||||
cfg.library_dirs = dirs;
|
||||
}
|
||||
|
||||
cfg.library_dirs = cfg
|
||||
.library_dirs
|
||||
.into_iter()
|
||||
.map(expand_tilde)
|
||||
.filter(|p| !p.as_os_str().is_empty())
|
||||
.collect();
|
||||
cfg
|
||||
}
|
||||
|
||||
pub fn with_extra_dirs(mut self, extra: impl IntoIterator<Item = PathBuf>) -> Self {
|
||||
self.library_dirs
|
||||
.extend(extra.into_iter().map(expand_tilde));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_library_dirs() -> Vec<PathBuf> {
|
||||
vec![
|
||||
bread_utils::xdg::home_dir().join(DEFAULT_USER_LIBRARY),
|
||||
PathBuf::from(DEFAULT_SYSTEM_LIBRARY),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn expand_tilde(path: PathBuf) -> PathBuf {
|
||||
let Some(s) = path.to_str() else {
|
||||
return path;
|
||||
};
|
||||
if s == "~" {
|
||||
return bread_utils::xdg::home_dir();
|
||||
}
|
||||
if let Some(rest) = s.strip_prefix("~/") {
|
||||
return bread_utils::xdg::home_dir().join(rest);
|
||||
}
|
||||
path
|
||||
}
|
||||
|
||||
fn env_library_dirs() -> Option<Vec<PathBuf>> {
|
||||
let raw = std::env::var(LIBRARY_DIRS_ENV).ok()?;
|
||||
if raw.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let dirs: Vec<PathBuf> = raw
|
||||
.split(':')
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(PathBuf::from)
|
||||
.collect();
|
||||
if dirs.is_empty() { None } else { Some(dirs) }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
fn env_lock() -> std::sync::MutexGuard<'static, ()> {
|
||||
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
LOCK.get_or_init(|| Mutex::new(()))
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
fn tmp_dir(name: &str) -> PathBuf {
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"breadpaper-config-{name}-{}-{}",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos())
|
||||
.unwrap_or(0)
|
||||
));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
dir
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_dirs_are_pictures_wallpapers_and_bos_backgrounds() {
|
||||
let dirs = Config::default().library_dirs;
|
||||
assert!(
|
||||
dirs.iter().any(|d| d.ends_with(DEFAULT_USER_LIBRARY)),
|
||||
"missing ~/{DEFAULT_USER_LIBRARY} in {dirs:?}"
|
||||
);
|
||||
assert!(
|
||||
dirs.iter().any(|d| d == Path::new(DEFAULT_SYSTEM_LIBRARY)),
|
||||
"missing {DEFAULT_SYSTEM_LIBRARY} in {dirs:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_tilde_prefix() {
|
||||
let home = bread_utils::xdg::home_dir();
|
||||
assert_eq!(
|
||||
expand_tilde(PathBuf::from("~/Pictures/Wallpapers")),
|
||||
home.join("Pictures/Wallpapers")
|
||||
);
|
||||
assert_eq!(expand_tilde(PathBuf::from("~")), home);
|
||||
let abs = PathBuf::from("/usr/share/backgrounds/bos");
|
||||
assert_eq!(expand_tilde(abs.clone()), abs);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_from_missing_file_uses_defaults() {
|
||||
let _lock = env_lock();
|
||||
let prev = std::env::var_os(LIBRARY_DIRS_ENV);
|
||||
unsafe { std::env::remove_var(LIBRARY_DIRS_ENV) };
|
||||
let cfg = Config::load_from(&PathBuf::from("/no/such/breadpaper-config.toml"));
|
||||
if let Some(v) = prev {
|
||||
unsafe { std::env::set_var(LIBRARY_DIRS_ENV, v) };
|
||||
}
|
||||
assert_eq!(cfg.library_dirs, default_library_dirs());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_from_parses_library_dirs_and_expands_tilde() {
|
||||
let _lock = env_lock();
|
||||
let prev = std::env::var_os(LIBRARY_DIRS_ENV);
|
||||
unsafe { std::env::remove_var(LIBRARY_DIRS_ENV) };
|
||||
|
||||
let dir = tmp_dir("parse");
|
||||
let path = dir.join("config.toml");
|
||||
std::fs::write(
|
||||
&path,
|
||||
"library_dirs = [\"~/custom/walls\", \"/opt/walls\"]\n",
|
||||
)
|
||||
.unwrap();
|
||||
let cfg = Config::load_from(&path);
|
||||
if let Some(v) = prev {
|
||||
unsafe { std::env::set_var(LIBRARY_DIRS_ENV, v) };
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
assert_eq!(
|
||||
cfg.library_dirs,
|
||||
vec![
|
||||
bread_utils::xdg::home_dir().join("custom/walls"),
|
||||
PathBuf::from("/opt/walls"),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_library_dirs_key_falls_back_to_defaults() {
|
||||
let _lock = env_lock();
|
||||
let prev = std::env::var_os(LIBRARY_DIRS_ENV);
|
||||
unsafe { std::env::remove_var(LIBRARY_DIRS_ENV) };
|
||||
|
||||
let dir = tmp_dir("empty");
|
||||
let path = dir.join("config.toml");
|
||||
std::fs::write(&path, "library_dirs = []\n").unwrap();
|
||||
let cfg = Config::load_from(&path);
|
||||
if let Some(v) = prev {
|
||||
unsafe { std::env::set_var(LIBRARY_DIRS_ENV, v) };
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
assert_eq!(cfg.library_dirs, default_library_dirs());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_overrides_config_file() {
|
||||
let _lock = env_lock();
|
||||
let prev = std::env::var_os(LIBRARY_DIRS_ENV);
|
||||
unsafe { std::env::set_var(LIBRARY_DIRS_ENV, "/tmp/a:/tmp/b") };
|
||||
|
||||
let dir = tmp_dir("env");
|
||||
let path = dir.join("config.toml");
|
||||
std::fs::write(&path, "library_dirs = [\"/from/file\"]\n").unwrap();
|
||||
let cfg = Config::load_from(&path);
|
||||
match prev {
|
||||
Some(v) => unsafe { std::env::set_var(LIBRARY_DIRS_ENV, v) },
|
||||
None => unsafe { std::env::remove_var(LIBRARY_DIRS_ENV) },
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
assert_eq!(
|
||||
cfg.library_dirs,
|
||||
vec![PathBuf::from("/tmp/a"), PathBuf::from("/tmp/b")]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_extra_dirs_appends() {
|
||||
let cfg = Config {
|
||||
library_dirs: vec![PathBuf::from("/a")],
|
||||
}
|
||||
.with_extra_dirs([PathBuf::from("/b")]);
|
||||
assert_eq!(
|
||||
cfg.library_dirs,
|
||||
vec![PathBuf::from("/a"), PathBuf::from("/b")]
|
||||
);
|
||||
}
|
||||
}
|
||||
126
src/current.rs
126
src/current.rs
|
|
@ -1,126 +0,0 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Persisted wallpaper path per output (`~/.config/breadpaper/current.json`).
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Current {
|
||||
#[serde(default)]
|
||||
outputs: BTreeMap<String, PathBuf>,
|
||||
}
|
||||
|
||||
impl Current {
|
||||
pub fn path() -> PathBuf {
|
||||
bread_utils::xdg::config_dir("breadpaper").join("current.json")
|
||||
}
|
||||
|
||||
pub fn load() -> Self {
|
||||
Self::load_from(&Self::path())
|
||||
}
|
||||
|
||||
/// Missing or unreadable file => empty map.
|
||||
pub fn load_from(path: &Path) -> Self {
|
||||
let Ok(text) = std::fs::read_to_string(path) else {
|
||||
return Self::default();
|
||||
};
|
||||
serde_json::from_str(&text).unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn save(&self) -> Result<()> {
|
||||
self.save_to(&Self::path())
|
||||
}
|
||||
|
||||
pub fn save_to(&self, path: &Path) -> Result<()> {
|
||||
let text = serde_json::to_string_pretty(self).context("serialize current.json")?;
|
||||
let text = format!("{text}\n");
|
||||
bread_utils::atomic::write_atomic(path, &text, None)
|
||||
.with_context(|| format!("write {}", path.display()))
|
||||
}
|
||||
|
||||
pub fn set_output(&mut self, output: impl Into<String>, path: impl Into<PathBuf>) {
|
||||
self.outputs.insert(output.into(), path.into());
|
||||
}
|
||||
|
||||
pub fn get_output(&self, output: &str) -> Option<&Path> {
|
||||
self.outputs.get(output).map(PathBuf::as_path)
|
||||
}
|
||||
|
||||
pub fn all(&self) -> &BTreeMap<String, PathBuf> {
|
||||
&self.outputs
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn tmp_dir(name: &str) -> PathBuf {
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"breadpaper-current-{name}-{}-{}",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos())
|
||||
.unwrap_or(0)
|
||||
));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
dir
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_file_is_empty_map() {
|
||||
let dir = tmp_dir("missing");
|
||||
let path = dir.join("current.json");
|
||||
let cur = Current::load_from(&path);
|
||||
assert!(cur.all().is_empty());
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_pretty_json() {
|
||||
let dir = tmp_dir("roundtrip");
|
||||
let path = dir.join("nested").join("current.json");
|
||||
let mut cur = Current::default();
|
||||
cur.set_output("eDP-1", "/abs/path/a.png");
|
||||
cur.set_output("HDMI-A-1", "/abs/path/b.png");
|
||||
cur.save_to(&path).unwrap();
|
||||
|
||||
let text = std::fs::read_to_string(&path).unwrap();
|
||||
assert!(text.contains("\n \"outputs\""));
|
||||
assert!(text.contains("\n \"HDMI-A-1\""));
|
||||
assert!(text.contains("\n \"eDP-1\""));
|
||||
|
||||
let loaded = Current::load_from(&path);
|
||||
assert_eq!(
|
||||
loaded.get_output("eDP-1"),
|
||||
Some(Path::new("/abs/path/a.png"))
|
||||
);
|
||||
assert_eq!(
|
||||
loaded.get_output("HDMI-A-1"),
|
||||
Some(Path::new("/abs/path/b.png"))
|
||||
);
|
||||
assert_eq!(loaded.all().len(), 2);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_one_output_does_not_drop_others() {
|
||||
let dir = tmp_dir("keep");
|
||||
let path = dir.join("current.json");
|
||||
let mut cur = Current::default();
|
||||
cur.set_output("eDP-1", "/abs/a.png");
|
||||
cur.set_output("HDMI-A-1", "/abs/b.png");
|
||||
cur.save_to(&path).unwrap();
|
||||
|
||||
let mut cur = Current::load_from(&path);
|
||||
cur.set_output("eDP-1", "/abs/c.png");
|
||||
cur.save_to(&path).unwrap();
|
||||
|
||||
let loaded = Current::load_from(&path);
|
||||
assert_eq!(loaded.get_output("eDP-1"), Some(Path::new("/abs/c.png")));
|
||||
assert_eq!(loaded.get_output("HDMI-A-1"), Some(Path::new("/abs/b.png")));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
368
src/lib.rs
368
src/lib.rs
|
|
@ -1,249 +1,18 @@
|
|||
mod config;
|
||||
mod current;
|
||||
mod library;
|
||||
mod pywal;
|
||||
mod theme;
|
||||
mod ui;
|
||||
mod wallpaper;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
#[cfg(not(test))]
|
||||
use std::process::{Command, Stdio};
|
||||
use std::thread;
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use bread_utils::bread_client::{BreadClient, BreadEvent};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
pub use config::{Config, DEFAULT_SYSTEM_LIBRARY, DEFAULT_USER_LIBRARY};
|
||||
pub use library::{Wallpaper, scan};
|
||||
|
||||
/// App id in bread's sibling-app registry (`KNOWN_APPS`). Events publish as
|
||||
/// `bread.paper.*`. See `EVENTS.md`.
|
||||
const APP_ID: &str = "paper";
|
||||
|
||||
const IMAGE_EXTENSIONS: &[&str] = &["png", "jpg", "jpeg", "webp", "gif", "bmp"];
|
||||
|
||||
/// Open the GTK wallpaper library. Extra dirs are appended to the configured
|
||||
/// scan list (`~/.config/breadpaper/config.toml`, then defaults).
|
||||
pub fn library(extra_dirs: impl IntoIterator<Item = PathBuf>) -> Result<()> {
|
||||
let cfg = Config::load().with_extra_dirs(extra_dirs);
|
||||
ui::run(cfg.library_dirs)
|
||||
}
|
||||
|
||||
/// Set wallpaper + global pywal palette on every live output.
|
||||
pub fn set(path: &Path) -> Result<()> {
|
||||
let path = validate(path)?;
|
||||
apply_wallpaper(&path)?;
|
||||
generate_palette(&path)?;
|
||||
reload_theme()?;
|
||||
|
||||
let mut cur = current::Current::load();
|
||||
if cur.all().is_empty() {
|
||||
let live = live_outputs();
|
||||
if live.is_empty() {
|
||||
cur.set_output("*", path.clone());
|
||||
} else {
|
||||
for output in live {
|
||||
cur.set_output(output, path.clone());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let keys: Vec<String> = cur.all().keys().cloned().collect();
|
||||
for output in keys {
|
||||
cur.set_output(output, path.clone());
|
||||
}
|
||||
}
|
||||
cur.save()?;
|
||||
|
||||
for output in cur.all().keys() {
|
||||
if output != "*" {
|
||||
theme::generate_for_output(output, &path)?;
|
||||
}
|
||||
}
|
||||
|
||||
emit_changed(&path, None);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set wallpaper + per-output theme on a single compositor output.
|
||||
///
|
||||
/// Does not run global `wal -i`. If `output` is the focused Hyprland
|
||||
/// monitor, the shared stylesheet is updated from that output's palette
|
||||
/// so unbound apps match the focused screen.
|
||||
pub fn set_on(path: &Path, output: &str) -> Result<()> {
|
||||
if output.is_empty() {
|
||||
bail!("output name is empty");
|
||||
}
|
||||
let path = validate(path)?;
|
||||
wallpaper::apply_on(&path, output)?;
|
||||
let palette = theme::generate_for_output(output, &path)?;
|
||||
|
||||
let mut cur = current::Current::load();
|
||||
cur.set_output(output, path.clone());
|
||||
cur.save()?;
|
||||
|
||||
if is_focused_output(output) {
|
||||
theme::write_shared_from(&palette)?;
|
||||
}
|
||||
|
||||
emit_changed(&path, Some(output));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Re-apply every wallpaper + per-output theme stored in current.json.
|
||||
pub fn apply_saved() -> Result<()> {
|
||||
let cur = current::Current::load();
|
||||
let mut first_err = None;
|
||||
for (output, path) in cur.all() {
|
||||
let result = restore_one(output, path);
|
||||
match result {
|
||||
Ok(()) => {
|
||||
let name = (output != "*").then_some(output.as_str());
|
||||
emit_changed(path, name);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("breadpaper: apply {output}: {e:#}");
|
||||
if first_err.is_none() {
|
||||
first_err = Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
match first_err {
|
||||
Some(e) => Err(e),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn restore_one(output: &str, path: &Path) -> Result<()> {
|
||||
if output == "*" {
|
||||
wallpaper::apply(path)?;
|
||||
return Ok(());
|
||||
}
|
||||
wallpaper::apply_on(path, output)?;
|
||||
let palette = theme::generate_for_output(output, path)?;
|
||||
if is_focused_output(output) {
|
||||
theme::write_shared_from(&palette)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Honor `bread.command.paper.*` until killed. Subscribe reconnects with
|
||||
/// backoff if breadd is down or restarts — this never errors the caller.
|
||||
pub fn listen() -> Result<()> {
|
||||
let client = BreadClient::connect(APP_ID);
|
||||
let _subscription = client.subscribe("bread.command.paper.**", handle_command);
|
||||
let _monitors = client.subscribe("bread.monitor.connected", |_| {
|
||||
if let Err(e) = apply_saved() {
|
||||
eprintln!("breadpaper: apply_saved on monitor connect failed: {e:#}");
|
||||
}
|
||||
});
|
||||
loop {
|
||||
thread::park();
|
||||
}
|
||||
}
|
||||
|
||||
/// Fire-and-forget `bread.paper.changed`. Silent no-op if breadd is down
|
||||
/// (`BreadClient::emit` never blocks or errors the caller).
|
||||
///
|
||||
/// `output` is `None` when the wallpaper was applied to every output.
|
||||
fn emit_changed(path: &Path, output: Option<&str>) {
|
||||
let mut data = json!({ "path": path.to_string_lossy() });
|
||||
if let Some(name) = output {
|
||||
data["output"] = json!(name);
|
||||
}
|
||||
BreadClient::connect(APP_ID).emit("bread.paper.changed", data);
|
||||
}
|
||||
|
||||
fn handle_command(event: BreadEvent) {
|
||||
let Some(verb) = event.event.strip_prefix("bread.command.paper.") else {
|
||||
return;
|
||||
};
|
||||
match verb {
|
||||
"set" => handle_set(&event.data),
|
||||
"library" => handle_library(),
|
||||
other => {
|
||||
eprintln!("breadpaper: ignoring unrecognized command verb '{other}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_set(data: &Value) {
|
||||
let client = BreadClient::connect(APP_ID);
|
||||
let Some(path_str) = data.get("path").and_then(Value::as_str) else {
|
||||
client.emit(
|
||||
"bread.paper.set.failed",
|
||||
json!({ "error": "missing string \"path\"" }),
|
||||
);
|
||||
return;
|
||||
};
|
||||
let output = data.get("output").and_then(Value::as_str);
|
||||
let path = Path::new(path_str);
|
||||
let result = match output {
|
||||
Some(name) => set_on(path, name),
|
||||
None => set(path),
|
||||
};
|
||||
match result {
|
||||
Ok(()) => {
|
||||
let applied = path
|
||||
.canonicalize()
|
||||
.map(|p| p.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|_| path_str.to_string());
|
||||
let mut payload = json!({ "path": applied });
|
||||
if let Some(name) = output {
|
||||
payload["output"] = json!(name);
|
||||
}
|
||||
client.emit("bread.paper.set.done", payload);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("breadpaper: bread.command.paper.set failed: {e:#}");
|
||||
let mut payload = json!({ "error": format!("{e:#}"), "path": path_str });
|
||||
if let Some(name) = output {
|
||||
payload["output"] = json!(name);
|
||||
}
|
||||
client.emit("bread.paper.set.failed", payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_library() {
|
||||
let client = BreadClient::connect(APP_ID);
|
||||
match open_library() {
|
||||
Ok(()) => client.emit("bread.paper.library.done", json!({})),
|
||||
Err(e) => {
|
||||
eprintln!("breadpaper: bread.command.paper.library failed: {e:#}");
|
||||
client.emit(
|
||||
"bread.paper.library.failed",
|
||||
json!({ "error": format!("{e:#}") }),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawn a one-shot `breadpaper library` so the listen loop can stay a
|
||||
/// park() thread. GTK needs its own process (and argv) — mixing it into
|
||||
/// `listen` would steal the main thread.
|
||||
fn open_library() -> Result<()> {
|
||||
spawn_library()
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
fn spawn_library() -> Result<()> {
|
||||
let exe = std::env::current_exe().context("cannot resolve breadpaper executable")?;
|
||||
Command::new(exe)
|
||||
.arg("library")
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::inherit())
|
||||
.spawn()
|
||||
.context("failed to spawn breadpaper library")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn spawn_library() -> Result<()> {
|
||||
// cargo test's current_exe is the test harness, not breadpaper.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -257,14 +26,6 @@ pub fn get() -> Result<PathBuf> {
|
|||
Ok(PathBuf::from(contents.trim()))
|
||||
}
|
||||
|
||||
/// Wallpaper path last persisted for `output` in current.json.
|
||||
pub fn get_on(output: &str) -> Result<PathBuf> {
|
||||
current::Current::load()
|
||||
.get_output(output)
|
||||
.map(Path::to_path_buf)
|
||||
.with_context(|| format!("no wallpaper saved for output {output}"))
|
||||
}
|
||||
|
||||
pub fn apply_wallpaper(path: &Path) -> Result<()> {
|
||||
wallpaper::apply(path)
|
||||
}
|
||||
|
|
@ -298,132 +59,3 @@ fn validate(path: &Path) -> Result<PathBuf> {
|
|||
|
||||
Ok(canonical)
|
||||
}
|
||||
|
||||
fn live_outputs() -> Vec<String> {
|
||||
if let Some(names) = hypr_output_names() {
|
||||
return names;
|
||||
}
|
||||
wallpaper::query_outputs()
|
||||
}
|
||||
|
||||
fn hypr_output_names() -> Option<Vec<String>> {
|
||||
let v = bread_utils::hypr::request_json("j/monitors")?;
|
||||
let names: Vec<String> = v
|
||||
.as_array()?
|
||||
.iter()
|
||||
.filter_map(|m| m.get("name").and_then(|n| n.as_str()).map(str::to_string))
|
||||
.collect();
|
||||
if names.is_empty() { None } else { Some(names) }
|
||||
}
|
||||
|
||||
fn is_focused_output(output: &str) -> bool {
|
||||
bread_utils::hypr::focused_monitor()
|
||||
.map(|m| m.name == output)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn tmp_dir(name: &str) -> PathBuf {
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"breadpaper-lib-{name}-{}-{}",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos())
|
||||
.unwrap_or(0)
|
||||
));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
dir
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_changed_is_silent_without_breadd() {
|
||||
// BreadClient::emit must never panic or error just because the
|
||||
// socket is missing — this is the fail-silent contract.
|
||||
emit_changed(Path::new("/tmp/wallpaper.png"), None);
|
||||
emit_changed(Path::new("/tmp/wallpaper.png"), Some("eDP-1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subscribe_is_silent_without_breadd() {
|
||||
let client = BreadClient::connect(APP_ID);
|
||||
let sub = client.subscribe("bread.command.paper.**", |_| {});
|
||||
drop(sub);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handle_command_ignores_unrecognized_verb() {
|
||||
handle_command(BreadEvent {
|
||||
event: "bread.command.paper.next".into(),
|
||||
timestamp: 0,
|
||||
data: json!({}),
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handle_command_ignores_events_outside_its_own_command_namespace() {
|
||||
handle_command(BreadEvent {
|
||||
event: "bread.command.clip.clear".into(),
|
||||
timestamp: 0,
|
||||
data: json!({}),
|
||||
});
|
||||
handle_command(BreadEvent {
|
||||
event: "bread.paper.changed".into(),
|
||||
timestamp: 0,
|
||||
data: json!({ "path": "/tmp/wallpaper.png" }),
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handle_set_missing_path_is_silent_without_breadd() {
|
||||
handle_set(&json!({}));
|
||||
handle_set(&json!({ "path": 1 }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handle_set_with_output_vs_without_is_silent_without_breadd() {
|
||||
// Missing files fail in validate — never reaches awww/wal.
|
||||
handle_set(&json!({ "path": "/no/such/breadpaper-wallpaper.png" }));
|
||||
handle_set(&json!({
|
||||
"path": "/no/such/breadpaper-wallpaper.png",
|
||||
"output": "eDP-1"
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handle_command_library_is_silent_without_breadd() {
|
||||
handle_command(BreadEvent {
|
||||
event: "bread.command.paper.library".into(),
|
||||
timestamp: 0,
|
||||
data: json!({}),
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_bad_extensions() {
|
||||
let dir = tmp_dir("validate");
|
||||
let txt = dir.join("notes.txt");
|
||||
std::fs::write(&txt, b"x").unwrap();
|
||||
assert!(validate(&txt).is_err());
|
||||
|
||||
let png = dir.join("ok.png");
|
||||
std::fs::write(&png, b"x").unwrap();
|
||||
assert_eq!(validate(&png).unwrap(), png.canonicalize().unwrap());
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handle_set_bad_extension_with_output_is_silent_without_breadd() {
|
||||
let dir = tmp_dir("bad-ext");
|
||||
let txt = dir.join("notes.txt");
|
||||
std::fs::write(&txt, b"x").unwrap();
|
||||
handle_set(&json!({
|
||||
"path": txt.to_string_lossy(),
|
||||
"output": "HDMI-A-1"
|
||||
}));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
168
src/library.rs
168
src/library.rs
|
|
@ -1,168 +0,0 @@
|
|||
use std::collections::HashSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::IMAGE_EXTENSIONS;
|
||||
|
||||
/// Caps how many files the picker ever lists. The library is organized in
|
||||
/// subfolders (show/series), so the walk is recursive — without a bound a
|
||||
/// huge Pictures tree would stall the window.
|
||||
pub const MAX_LIBRARY_ITEMS: usize = 200;
|
||||
pub const MAX_SCAN_DEPTH: usize = 4;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Wallpaper {
|
||||
pub path: PathBuf,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
pub fn is_wallpaper_file(path: &Path) -> bool {
|
||||
path.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.map(|e| {
|
||||
IMAGE_EXTENSIONS
|
||||
.iter()
|
||||
.any(|ext| ext.eq_ignore_ascii_case(e))
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Recursively collect images under `dirs`. Missing directories are skipped.
|
||||
/// Results are sorted by filename (case-insensitive), then full path.
|
||||
pub fn scan(dirs: &[PathBuf]) -> Vec<Wallpaper> {
|
||||
let mut out = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
for dir in dirs {
|
||||
if !dir.is_dir() {
|
||||
continue;
|
||||
}
|
||||
walk(dir, MAX_SCAN_DEPTH, &mut out, &mut seen);
|
||||
if out.len() >= MAX_LIBRARY_ITEMS {
|
||||
break;
|
||||
}
|
||||
}
|
||||
out.sort_by(|a, b| {
|
||||
a.name
|
||||
.to_lowercase()
|
||||
.cmp(&b.name.to_lowercase())
|
||||
.then_with(|| a.path.cmp(&b.path))
|
||||
});
|
||||
out
|
||||
}
|
||||
|
||||
fn walk(dir: &Path, depth: usize, out: &mut Vec<Wallpaper>, seen: &mut HashSet<PathBuf>) {
|
||||
if depth == 0 || out.len() >= MAX_LIBRARY_ITEMS {
|
||||
return;
|
||||
}
|
||||
let Ok(entries) = std::fs::read_dir(dir) else {
|
||||
return;
|
||||
};
|
||||
let mut entries: Vec<_> = entries.flatten().collect();
|
||||
entries.sort_by_key(|e| e.file_name());
|
||||
for entry in entries {
|
||||
if out.len() >= MAX_LIBRARY_ITEMS {
|
||||
return;
|
||||
}
|
||||
let path = entry.path();
|
||||
let name = entry.file_name();
|
||||
if name.to_string_lossy().starts_with('.') {
|
||||
continue;
|
||||
}
|
||||
if path.is_dir() {
|
||||
walk(&path, depth - 1, out, seen);
|
||||
continue;
|
||||
}
|
||||
if !is_wallpaper_file(&path) {
|
||||
continue;
|
||||
}
|
||||
let canonical = path.canonicalize().unwrap_or_else(|_| path.clone());
|
||||
if !seen.insert(canonical.clone()) {
|
||||
continue;
|
||||
}
|
||||
out.push(Wallpaper {
|
||||
name: path
|
||||
.file_stem()
|
||||
.or_else(|| path.file_name())
|
||||
.map(|s| s.to_string_lossy().into_owned())
|
||||
.unwrap_or_default(),
|
||||
path: canonical,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn tmp_dir(name: &str) -> PathBuf {
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"breadpaper-scan-{name}-{}-{}",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos())
|
||||
.unwrap_or(0)
|
||||
));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
dir
|
||||
}
|
||||
|
||||
fn touch(path: &Path) {
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent).unwrap();
|
||||
}
|
||||
std::fs::write(path, []).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_wallpaper_file_accepts_known_extensions() {
|
||||
assert!(is_wallpaper_file(Path::new("a.PNG")));
|
||||
assert!(is_wallpaper_file(Path::new("b.jpeg")));
|
||||
assert!(is_wallpaper_file(Path::new("c.webp")));
|
||||
assert!(!is_wallpaper_file(Path::new("d.txt")));
|
||||
assert!(!is_wallpaper_file(Path::new("noext")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scan_skips_missing_dirs() {
|
||||
assert!(scan(&[PathBuf::from("/no/such/breadpaper-walls")]).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scan_finds_images_and_ignores_other_files() {
|
||||
let dir = tmp_dir("find");
|
||||
touch(&dir.join("keep.png"));
|
||||
touch(&dir.join("notes.txt"));
|
||||
touch(&dir.join(".hidden.jpg"));
|
||||
touch(&dir.join("nested").join("deep.jpg"));
|
||||
let found = scan(std::slice::from_ref(&dir));
|
||||
let names: Vec<_> = found.iter().map(|w| w.name.as_str()).collect();
|
||||
assert!(names.contains(&"keep"), "{names:?}");
|
||||
assert!(names.contains(&"deep"), "{names:?}");
|
||||
assert!(
|
||||
!names
|
||||
.iter()
|
||||
.any(|n| n.contains("notes") || n.contains("hidden"))
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scan_dedups_the_same_file_via_two_roots() {
|
||||
let dir = tmp_dir("dedup");
|
||||
touch(&dir.join("one.png"));
|
||||
let found = scan(&[dir.clone(), dir.clone()]);
|
||||
assert_eq!(found.len(), 1);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scan_respects_item_cap() {
|
||||
let dir = tmp_dir("cap");
|
||||
for i in 0..(MAX_LIBRARY_ITEMS + 10) {
|
||||
touch(&dir.join(format!("{i:04}.png")));
|
||||
}
|
||||
let found = scan(std::slice::from_ref(&dir));
|
||||
assert_eq!(found.len(), MAX_LIBRARY_ITEMS);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
30
src/main.rs
30
src/main.rs
|
|
@ -13,10 +13,6 @@ struct Cli {
|
|||
/// Image file to set as wallpaper (shorthand for `set`)
|
||||
path: Option<PathBuf>,
|
||||
|
||||
/// Restrict set/get to one compositor output
|
||||
#[arg(long, value_name = "NAME", global = true)]
|
||||
output: Option<String>,
|
||||
|
||||
#[command(subcommand)]
|
||||
command: Option<Command>,
|
||||
}
|
||||
|
|
@ -27,34 +23,16 @@ enum Command {
|
|||
Set { path: PathBuf },
|
||||
/// Print the current wallpaper path
|
||||
Get,
|
||||
/// Re-apply wallpapers and per-output themes from current.json
|
||||
Apply,
|
||||
/// Honor bread.command.paper.set / .library until killed
|
||||
Listen,
|
||||
/// Open the wallpaper library (alias: browse)
|
||||
#[command(visible_alias = "browse")]
|
||||
Library {
|
||||
/// Extra directory to scan (repeatable; added to configured dirs)
|
||||
#[arg(short, long = "dir", value_name = "DIR")]
|
||||
dirs: Vec<PathBuf>,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
|
||||
let result = match (cli.command, cli.path) {
|
||||
(Some(Command::Set { path }), _) | (None, Some(path)) => match cli.output.as_deref() {
|
||||
Some(output) => breadpaper::set_on(&path, output),
|
||||
None => breadpaper::set(&path),
|
||||
},
|
||||
(Some(Command::Listen), _) => breadpaper::listen(),
|
||||
(Some(Command::Library { dirs }), _) => breadpaper::library(dirs),
|
||||
(Some(Command::Apply), _) => breadpaper::apply_saved(),
|
||||
(Some(Command::Get), _) | (None, None) => match cli.output.as_deref() {
|
||||
Some(output) => breadpaper::get_on(output).map(|p| println!("{}", p.display())),
|
||||
None => breadpaper::get().map(|p| println!("{}", p.display())),
|
||||
},
|
||||
(Some(Command::Set { path }), _) | (None, Some(path)) => breadpaper::set(&path),
|
||||
(Some(Command::Get), _) | (None, None) => {
|
||||
breadpaper::get().map(|p| println!("{}", p.display()))
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = result {
|
||||
|
|
|
|||
23
src/pywal.rs
23
src/pywal.rs
|
|
@ -1,18 +1,19 @@
|
|||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use anyhow::{Result, bail};
|
||||
|
||||
pub fn generate(path: &Path) -> Result<()> {
|
||||
let status = Command::new("wal")
|
||||
.arg("-i")
|
||||
.arg(path)
|
||||
.arg("-n") // skip wal's own wallpaper backend; awww already set it
|
||||
.status()
|
||||
.context("failed to run wal — is python-pywal installed?")?;
|
||||
|
||||
if !status.success() {
|
||||
bail!("wal exited with {}", status);
|
||||
// Was a bare Command::new("wal").status() with no timeout — pywal can
|
||||
// hang on a corrupt/huge image; this used to be able to wedge the
|
||||
// caller indefinitely.
|
||||
let out = bread_utils::proc::run(
|
||||
"wal",
|
||||
&["-i", &path.to_string_lossy(), "-n"], // -n: skip wal's own wallpaper backend; awww already set it
|
||||
Duration::from_secs(30),
|
||||
);
|
||||
if !out.success {
|
||||
bail!("wal failed (is python-pywal installed?): {}", out.stderr.trim());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
30
src/theme.rs
30
src/theme.rs
|
|
@ -1,30 +1,12 @@
|
|||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use bread_theme::Palette;
|
||||
use anyhow::{Result, bail};
|
||||
|
||||
pub fn reload() -> Result<()> {
|
||||
let status = Command::new("bread-theme")
|
||||
.arg("reload")
|
||||
.status()
|
||||
.context("failed to run bread-theme — is it installed?")?;
|
||||
|
||||
if !status.success() {
|
||||
bail!("bread-theme reload exited with {}", status);
|
||||
// Was a bare Command::new("bread-theme").status() with no timeout.
|
||||
let out = bread_utils::proc::run("bread-theme", &["reload"], Duration::from_secs(5));
|
||||
if !out.success {
|
||||
bail!("bread-theme reload failed (is it installed?): {}", out.stderr.trim());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Per-output palette + bread-theme files. Does not run `wal -i`.
|
||||
pub fn generate_for_output(output: &str, path: &Path) -> Result<Palette> {
|
||||
bread_theme::generate_output(output, path)
|
||||
.with_context(|| format!("bread-theme generate_output({output}, {})", path.display()))?;
|
||||
Ok(bread_theme::load_palette_for(output))
|
||||
}
|
||||
|
||||
pub fn write_shared_from(palette: &Palette) -> Result<()> {
|
||||
bread_theme::write_shared_css_from(palette)
|
||||
.context("bread-theme write_shared_css_from")
|
||||
.map(|_| ())
|
||||
}
|
||||
|
|
|
|||
300
src/ui.rs
300
src/ui.rs
|
|
@ -1,300 +0,0 @@
|
|||
use std::cell::RefCell;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::rc::Rc;
|
||||
|
||||
use anyhow::Result;
|
||||
use gtk4::gdk_pixbuf::Pixbuf;
|
||||
use gtk4::gio::ApplicationFlags;
|
||||
use gtk4::prelude::*;
|
||||
use gtk4::{
|
||||
Align, Application, ApplicationWindow, Box as GBox, Button, ContentFit, CssProvider, FlowBox,
|
||||
FlowBoxChild, HeaderBar, Label, Orientation, Picture, PolicyType, ScrolledWindow,
|
||||
SelectionMode, Stack,
|
||||
};
|
||||
|
||||
use crate::library::{self, Wallpaper};
|
||||
|
||||
const APP_ID: &str = "com.breadway.breadpaper";
|
||||
const THUMB_W: i32 = 240;
|
||||
const THUMB_H: i32 = 135;
|
||||
|
||||
const APP_CSS: &str = "\
|
||||
headerbar {\
|
||||
background-color: @bg; color: @on-bg; box-shadow: none;\
|
||||
border-bottom: 1px solid alpha(@on-bg, 0.08);\
|
||||
}\n\
|
||||
.library-chrome { padding: 12px 16px 8px 16px; }\n\
|
||||
.library-grid { padding: 8px 12px 16px 12px; }\n\
|
||||
.library-empty { padding: 32px 24px; }\n\
|
||||
.wallpaper-tile {\
|
||||
padding: 0; background-color: @surface; color: @on-surface;\
|
||||
border-radius: 8px;\
|
||||
}\n\
|
||||
.wallpaper-tile:hover { background-color: alpha(@on-surface, 0.14); }\n\
|
||||
.wallpaper-tile.current { box-shadow: inset 0 0 0 2px @accent; }\n\
|
||||
.wallpaper-name { padding: 8px 10px; font-size: 12px; }\n\
|
||||
";
|
||||
|
||||
thread_local! {
|
||||
static APP_PROVIDER: RefCell<Option<CssProvider>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
pub fn run(dirs: Vec<PathBuf>) -> Result<()> {
|
||||
let app = Application::builder()
|
||||
.application_id(APP_ID)
|
||||
.flags(ApplicationFlags::empty())
|
||||
.build();
|
||||
|
||||
app.connect_activate(move |app| present(app, dirs.clone()));
|
||||
// Clap already consumed argv; do not let GApplication re-parse `library --dir`.
|
||||
let _ = app.run_with_args(&["breadpaper"]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn present(app: &Application, dirs: Vec<PathBuf>) {
|
||||
bread_theme::gtk::apply_shared();
|
||||
APP_PROVIDER.with(|cell| bread_theme::gtk::apply_css(APP_CSS, cell));
|
||||
|
||||
let window = ApplicationWindow::builder()
|
||||
.application(app)
|
||||
.title("Wallpapers")
|
||||
.default_width(960)
|
||||
.default_height(640)
|
||||
.build();
|
||||
|
||||
let header = HeaderBar::new();
|
||||
window.set_titlebar(Some(&header));
|
||||
|
||||
let refresh = Button::with_label("Refresh");
|
||||
header.pack_end(&refresh);
|
||||
|
||||
let root = GBox::new(Orientation::Vertical, 0);
|
||||
|
||||
let chrome = GBox::new(Orientation::Vertical, 6);
|
||||
chrome.add_css_class("library-chrome");
|
||||
|
||||
let summary = Label::new(None);
|
||||
summary.set_xalign(0.0);
|
||||
summary.set_wrap(true);
|
||||
summary.add_css_class("dim");
|
||||
chrome.append(&summary);
|
||||
|
||||
let status = Label::new(Some("Click a wallpaper to apply it."));
|
||||
status.set_xalign(0.0);
|
||||
status.set_wrap(true);
|
||||
chrome.append(&status);
|
||||
root.append(&chrome);
|
||||
|
||||
let flow = FlowBox::new();
|
||||
flow.set_selection_mode(SelectionMode::None);
|
||||
flow.set_homogeneous(true);
|
||||
flow.set_max_children_per_line(6);
|
||||
flow.set_min_children_per_line(2);
|
||||
flow.set_row_spacing(12);
|
||||
flow.set_column_spacing(12);
|
||||
flow.set_halign(Align::Fill);
|
||||
flow.add_css_class("library-grid");
|
||||
|
||||
let scrolled = ScrolledWindow::builder()
|
||||
.hscrollbar_policy(PolicyType::Never)
|
||||
.vscrollbar_policy(PolicyType::Automatic)
|
||||
.vexpand(true)
|
||||
.hexpand(true)
|
||||
.child(&flow)
|
||||
.build();
|
||||
|
||||
let empty = Label::new(None);
|
||||
empty.set_wrap(true);
|
||||
empty.set_justify(gtk4::Justification::Center);
|
||||
empty.add_css_class("dim");
|
||||
empty.add_css_class("library-empty");
|
||||
empty.set_hexpand(true);
|
||||
empty.set_vexpand(true);
|
||||
|
||||
let stack = Stack::new();
|
||||
stack.set_vexpand(true);
|
||||
stack.add_named(&scrolled, Some("grid"));
|
||||
stack.add_named(&empty, Some("empty"));
|
||||
root.append(&stack);
|
||||
|
||||
window.set_child(Some(&root));
|
||||
bread_theme::gtk::bind_window_auto(&window);
|
||||
|
||||
let dirs = Rc::new(dirs);
|
||||
let reload = {
|
||||
let dirs = dirs.clone();
|
||||
let flow = flow.clone();
|
||||
let summary = summary.clone();
|
||||
let status = status.clone();
|
||||
let stack = stack.clone();
|
||||
let empty = empty.clone();
|
||||
let window = window.clone();
|
||||
Rc::new(move || {
|
||||
let papers = library::scan(&dirs);
|
||||
summary.set_text(&dirs_summary(&dirs, papers.len()));
|
||||
empty.set_text(&empty_message(&dirs));
|
||||
if papers.is_empty() {
|
||||
stack.set_visible_child_name("empty");
|
||||
} else {
|
||||
stack.set_visible_child_name("grid");
|
||||
}
|
||||
fill_grid(&flow, &papers, &status, &window);
|
||||
})
|
||||
};
|
||||
|
||||
window.present();
|
||||
reload();
|
||||
{
|
||||
let reload = reload.clone();
|
||||
refresh.connect_clicked(move |_| reload());
|
||||
}
|
||||
}
|
||||
|
||||
fn fill_grid(flow: &FlowBox, papers: &[Wallpaper], status: &Label, host: &impl IsA<gtk4::Widget>) {
|
||||
while let Some(child) = flow.first_child() {
|
||||
flow.remove(&child);
|
||||
}
|
||||
let current = current_path_for(host);
|
||||
for paper in papers {
|
||||
let is_current = current.as_deref() == Some(paper.path.as_path());
|
||||
flow.insert(&tile(paper, is_current, flow, status), -1);
|
||||
}
|
||||
}
|
||||
|
||||
fn current_path_for(widget: &impl IsA<gtk4::Widget>) -> Option<PathBuf> {
|
||||
target_output(widget)
|
||||
.and_then(|output| {
|
||||
crate::current::Current::load()
|
||||
.get_output(&output)
|
||||
.map(Path::to_path_buf)
|
||||
})
|
||||
.or_else(|| crate::get().ok())
|
||||
}
|
||||
|
||||
fn target_output(widget: &impl IsA<gtk4::Widget>) -> Option<String> {
|
||||
bread_theme::gtk::output_for_widget(widget)
|
||||
.or_else(|| bread_utils::hypr::focused_monitor().map(|m| m.name))
|
||||
}
|
||||
|
||||
fn tile(paper: &Wallpaper, is_current: bool, flow: &FlowBox, status: &Label) -> Button {
|
||||
let btn = Button::new();
|
||||
btn.add_css_class("wallpaper-tile");
|
||||
btn.set_widget_name(&paper.path.to_string_lossy());
|
||||
btn.set_tooltip_text(Some(&paper.path.to_string_lossy()));
|
||||
if is_current {
|
||||
btn.add_css_class("current");
|
||||
}
|
||||
|
||||
let col = GBox::new(Orientation::Vertical, 0);
|
||||
col.append(&thumbnail(&paper.path));
|
||||
|
||||
let name = Label::new(Some(&paper.name));
|
||||
name.add_css_class("wallpaper-name");
|
||||
name.set_xalign(0.0);
|
||||
name.set_ellipsize(gtk4::pango::EllipsizeMode::End);
|
||||
name.set_max_width_chars(24);
|
||||
col.append(&name);
|
||||
btn.set_child(Some(&col));
|
||||
|
||||
let path = paper.path.clone();
|
||||
let pretty = paper.name.clone();
|
||||
let status = status.clone();
|
||||
let flow = flow.clone();
|
||||
btn.connect_clicked(move |clicked| {
|
||||
if !clicked.is_sensitive() {
|
||||
return;
|
||||
}
|
||||
clicked.set_sensitive(false);
|
||||
let output = target_output(clicked);
|
||||
status.set_text(&match output.as_deref() {
|
||||
Some(name) => format!("Applying {pretty} on {name}…"),
|
||||
None => format!("Applying {pretty}…"),
|
||||
});
|
||||
let path = path.clone();
|
||||
let pretty = pretty.clone();
|
||||
let status = status.clone();
|
||||
let flow = flow.clone();
|
||||
let clicked = clicked.clone();
|
||||
gtk4::glib::spawn_future_local(async move {
|
||||
let path_thread = path.clone();
|
||||
let output_thread = output.clone();
|
||||
let result = gtk4::gio::spawn_blocking(move || match output_thread.as_deref() {
|
||||
Some(name) => crate::set_on(&path_thread, name),
|
||||
None => crate::set(&path_thread),
|
||||
})
|
||||
.await;
|
||||
clicked.set_sensitive(true);
|
||||
match result {
|
||||
Ok(Ok(())) => {
|
||||
status.set_text(&match output.as_deref() {
|
||||
Some(name) => format!("Applied {pretty} on {name}"),
|
||||
None => format!("Applied {pretty}"),
|
||||
});
|
||||
mark_current(&flow, &path);
|
||||
}
|
||||
Ok(Err(e)) => status.set_text(&format!("{e:#}")),
|
||||
Err(_) => status.set_text("Failed to apply wallpaper"),
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
btn
|
||||
}
|
||||
|
||||
fn thumbnail(path: &Path) -> Picture {
|
||||
let picture = match Pixbuf::from_file_at_scale(path, THUMB_W, THUMB_H, true) {
|
||||
Ok(pb) => Picture::for_paintable(>k4::gdk::Texture::for_pixbuf(&pb)),
|
||||
Err(_) => Picture::for_filename(path),
|
||||
};
|
||||
picture.set_content_fit(ContentFit::Cover);
|
||||
picture.set_size_request(THUMB_W, THUMB_H);
|
||||
picture.set_can_shrink(true);
|
||||
picture.set_hexpand(true);
|
||||
picture
|
||||
}
|
||||
|
||||
fn mark_current(flow: &FlowBox, current: &Path) {
|
||||
let current = current.to_string_lossy();
|
||||
let mut i = 0;
|
||||
while let Some(wrapper) = flow.child_at_index(i) {
|
||||
if let Some(btn) = wrapper
|
||||
.downcast_ref::<FlowBoxChild>()
|
||||
.and_then(|c| c.child())
|
||||
.and_then(|w| w.downcast::<Button>().ok())
|
||||
{
|
||||
if btn.widget_name() == current.as_ref() {
|
||||
btn.add_css_class("current");
|
||||
} else {
|
||||
btn.remove_css_class("current");
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn dirs_summary(dirs: &[PathBuf], count: usize) -> String {
|
||||
let listed = dirs
|
||||
.iter()
|
||||
.map(|d| {
|
||||
if d.is_dir() {
|
||||
d.display().to_string()
|
||||
} else {
|
||||
format!("{} (missing)", d.display())
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(" · ");
|
||||
format!("{count} wallpaper(s) · {listed}")
|
||||
}
|
||||
|
||||
fn empty_message(dirs: &[PathBuf]) -> String {
|
||||
let listed = dirs
|
||||
.iter()
|
||||
.map(|d| d.display().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
format!(
|
||||
"No wallpapers found.\nAdd png/jpg/webp/gif/bmp files under:\n{listed}\n\nOr set library_dirs in {}",
|
||||
crate::config::Config::path().display()
|
||||
)
|
||||
}
|
||||
|
|
@ -1,76 +1,15 @@
|
|||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use anyhow::{Result, bail};
|
||||
|
||||
pub fn apply(path: &Path) -> Result<()> {
|
||||
run_awww(Command::new("awww").arg("img").arg(path))
|
||||
}
|
||||
|
||||
pub fn apply_on(path: &Path, output: &str) -> Result<()> {
|
||||
run_awww(
|
||||
Command::new("awww")
|
||||
.arg("img")
|
||||
.arg(path)
|
||||
.arg("--outputs")
|
||||
.arg(output),
|
||||
)
|
||||
}
|
||||
|
||||
/// Output names from `awww query`. Empty if the daemon isn't running.
|
||||
pub fn query_outputs() -> Vec<String> {
|
||||
let Ok(out) = Command::new("awww").arg("query").output() else {
|
||||
return Vec::new();
|
||||
};
|
||||
if !out.status.success() {
|
||||
return Vec::new();
|
||||
}
|
||||
parse_awww_query(&String::from_utf8_lossy(&out.stdout))
|
||||
}
|
||||
|
||||
fn run_awww(cmd: &mut Command) -> Result<()> {
|
||||
let status = cmd
|
||||
.status()
|
||||
.context("failed to run awww — is awww-daemon running?")?;
|
||||
if !status.success() {
|
||||
bail!("awww img exited with {}", status);
|
||||
// Was a bare Command::new("awww").status() with no timeout — a wedged
|
||||
// awww-daemon (compositor not ready yet, IPC socket stuck) used to be
|
||||
// able to hang this call indefinitely.
|
||||
let out = bread_utils::proc::run("awww", &["img", &path.to_string_lossy()], Duration::from_secs(10));
|
||||
if !out.success {
|
||||
bail!("awww img failed (is awww-daemon running?): {}", out.stderr.trim());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_awww_query(stdout: &str) -> Vec<String> {
|
||||
stdout.lines().filter_map(parse_awww_query_line).collect()
|
||||
}
|
||||
|
||||
/// `awww query` lines look like `: eDP-1: 1920x1080, scale: 1, ...`.
|
||||
fn parse_awww_query_line(line: &str) -> Option<String> {
|
||||
let rest = line.trim().strip_prefix(':').unwrap_or(line).trim();
|
||||
let name = rest.split(':').next()?.trim();
|
||||
if name.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(name.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_awww_query_names() {
|
||||
let sample = "\
|
||||
: eDP-1: 1920x1080, scale: 1, currently displaying: image: /a.png
|
||||
: HDMI-A-1: 2560x1440, scale: 1, currently displaying: image: /b.png
|
||||
";
|
||||
assert_eq!(
|
||||
parse_awww_query(sample),
|
||||
vec!["eDP-1".to_string(), "HDMI-A-1".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_awww_query_skips_blank() {
|
||||
assert!(parse_awww_query("\n \n").is_empty());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue