From 7ece4fd7626d7c7b880ea2172dd7a3bea27a6c73 Mon Sep 17 00:00:00 2001 From: Breadway Date: Fri, 19 Jun 2026 08:38:38 +0800 Subject: [PATCH 01/30] CI: use /tmp for ecosystem clone, avoid permissions conflict --- .github/workflows/release.yml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 71c0ca3..12f8fac 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ permissions: env: DL_DIR: /srv/breadway-dl - ECOSYSTEM_DIR: /home/breadway/Projects/bread-ecosystem + ECOSYSTEM_DIR: /tmp/bread-ecosystem-ci jobs: build: @@ -41,12 +41,8 @@ jobs: - name: ensure bread-ecosystem run: | - if [[ -d "${ECOSYSTEM_DIR}/.git" ]]; then - git -C "${ECOSYSTEM_DIR}" pull --ff-only - else - mkdir -p "$(dirname "${ECOSYSTEM_DIR}")" - git clone https://github.com/Breadway/bread-ecosystem.git "${ECOSYSTEM_DIR}" - fi + rm -rf "${ECOSYSTEM_DIR}" + git clone https://github.com/Breadway/bread-ecosystem.git "${ECOSYSTEM_DIR}" - name: regenerate index.json run: bash "${ECOSYSTEM_DIR}/scripts/gen-index.sh" From 6385a080f159df60b48740091ddeea0cc4582e50 Mon Sep 17 00:00:00 2001 From: Breadway Date: Fri, 3 Jul 2026 14:10:02 +0800 Subject: [PATCH 02/30] CI: migrate release workflow from GitHub Actions to Forgejo Actions GitHub Actions self-hosted runners need per-repo registration on a personal account; Forgejo Actions' runner already serves every repo with zero setup. Moves release publishing there (dl.breadway.dev stays the primary bakery target; GitHub release upload is kept as the fallback via an explicit token, since Forgejo Actions has no ambient GITHUB_TOKEN) and adds a mirror workflow to keep GitHub in sync automatically. --- .forgejo/workflows/mirror.yml | 2 -- .forgejo/workflows/release.yml | 59 +++++++++++++++++++++++++++++++ .github/workflows/release.yml | 63 ---------------------------------- README.md | 10 +++--- 4 files changed, 64 insertions(+), 70 deletions(-) create mode 100644 .forgejo/workflows/release.yml delete mode 100644 .github/workflows/release.yml diff --git a/.forgejo/workflows/mirror.yml b/.forgejo/workflows/mirror.yml index 6f40c04..4c186c9 100644 --- a/.forgejo/workflows/mirror.yml +++ b/.forgejo/workflows/mirror.yml @@ -14,8 +14,6 @@ jobs: set -euo pipefail git clone --mirror "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" repo.git cd repo.git - # Mirror only branches and tags (not refs/pull/*, which GitHub rejects); - # --prune deletes GitHub refs that no longer exist on Forgejo. git push --prune \ "https://x-access-token:${{ secrets.MIRROR_TOKEN }}@github.com/Breadway/breadbox.git" \ '+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*' diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml new file mode 100644 index 0000000..de309ae --- /dev/null +++ b/.forgejo/workflows/release.yml @@ -0,0 +1,59 @@ +name: release + +on: + push: + tags: ["v*"] + +jobs: + build: + 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 && cargo build --release --locked + + - name: prepare artifacts + run: | + set -euo pipefail + VERSION="${GITHUB_REF_NAME#v}" + PKG_DIR="/srv/breadway-dl/breadbox/${VERSION}" + mkdir -p "${PKG_DIR}" + for bin in breadbox breadbox-sync; do + cp "src/target/release/${bin}" "${PKG_DIR}/${bin}-x86_64" + strip "${PKG_DIR}/${bin}-x86_64" + sha256sum "${PKG_DIR}/${bin}-x86_64" | awk '{print $1}' \ + > "${PKG_DIR}/${bin}-x86_64.sha256" + done + cp src/packaging/breadbox-sync.service "${PKG_DIR}/" + cp src/config.example.toml "${PKG_DIR}/" + cp src/bakery.toml "${PKG_DIR}/bakery.toml" + ln -sfn "${VERSION}" "/srv/breadway-dl/breadbox/latest" + + - name: regenerate index.json + run: | + set -euo pipefail + rm -rf /tmp/bread-ecosystem-ci + git clone https://git.breadway.dev/Breadway/bread-ecosystem.git /tmp/bread-ecosystem-ci + bash /tmp/bread-ecosystem-ci/scripts/gen-index.sh + + - name: upload to GitHub Release + env: + GH_TOKEN: ${{ secrets.GH_RELEASE_TOKEN }} + run: | + set -euo pipefail + VERSION="${GITHUB_REF_NAME#v}" + PKG_DIR="/srv/breadway-dl/breadbox/${VERSION}" + gh release create "${GITHUB_REF_NAME}" --repo Breadway/breadbox \ + --title "breadbox v${VERSION}" --generate-notes 2>/dev/null || true + gh release upload "${GITHUB_REF_NAME}" --repo Breadway/breadbox \ + "${PKG_DIR}/breadbox-x86_64" \ + "${PKG_DIR}/breadbox-sync-x86_64" \ + "${PKG_DIR}/breadbox-x86_64.sha256" \ + "${PKG_DIR}/breadbox-sync-x86_64.sha256" \ + --clobber diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 12f8fac..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: release - -on: - push: - tags: ["v*"] - -permissions: - contents: write - -env: - DL_DIR: /srv/breadway-dl - ECOSYSTEM_DIR: /tmp/bread-ecosystem-ci - -jobs: - build: - runs-on: [self-hosted, hestia] - steps: - - uses: actions/checkout@v4 - - - name: install build deps - run: sudo apt-get install -y libgtk-4-dev librsvg2-dev libdbus-1-dev pkg-config 2>/dev/null || true - - - name: build - run: cargo build --release --locked - - - name: prepare artifacts - run: | - VERSION="${GITHUB_REF_NAME#v}" - PKG_DIR="${DL_DIR}/breadbox/${VERSION}" - mkdir -p "${PKG_DIR}" - for bin in breadbox breadbox-sync; do - cp "target/release/${bin}" "${PKG_DIR}/${bin}-x86_64" - strip "${PKG_DIR}/${bin}-x86_64" - sha256sum "${PKG_DIR}/${bin}-x86_64" | awk '{print $1}' \ - > "${PKG_DIR}/${bin}-x86_64.sha256" - done - cp packaging/breadbox-sync.service "${PKG_DIR}/" - cp config.example.toml "${PKG_DIR}/" - cp bakery.toml "${PKG_DIR}/bakery.toml" - ln -sfn "${VERSION}" "${DL_DIR}/breadbox/latest" - - - name: ensure bread-ecosystem - run: | - rm -rf "${ECOSYSTEM_DIR}" - git clone https://github.com/Breadway/bread-ecosystem.git "${ECOSYSTEM_DIR}" - - - name: regenerate index.json - run: bash "${ECOSYSTEM_DIR}/scripts/gen-index.sh" - - - name: upload to GitHub Release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - VERSION="${GITHUB_REF_NAME#v}" - PKG_DIR="${DL_DIR}/breadbox/${VERSION}" - gh release create "${GITHUB_REF_NAME}" \ - --title "breadbox v${VERSION}" --generate-notes 2>/dev/null || true - gh release upload "${GITHUB_REF_NAME}" \ - "${PKG_DIR}/breadbox-x86_64" \ - "${PKG_DIR}/breadbox-sync-x86_64" \ - "${PKG_DIR}/breadbox-x86_64.sha256" \ - "${PKG_DIR}/breadbox-sync-x86_64.sha256" \ - --clobber diff --git a/README.md b/README.md index 2b6ce13..b829c6c 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,8 @@ breadbox GTK4 layer-shell launcher - Layer-shell window, centered 600 px wide, keyboard-exclusive - Reads the active Hyprland workspace and sorts apps by context priority -- Fuzzy filtering as you type; Enter launches, Escape closes +- Launch history: non-priority apps sort by most-launched first, then alphabetically +- Fuzzy filtering as you type; Enter or click to launch, Escape or click outside to close - App icons loaded from the resolved icon cache (see `breadbox-sync`) - pywal palette auto-detected from `~/.cache/wal/colors.json`, falls back to Catppuccin Mocha - User CSS override at `~/.config/breadbox/style.css` @@ -88,10 +89,9 @@ breadbox-sync ``` Icon resolution order: -1. System icon theme (`~/.local/share/icons`, `/usr/share/icons`, `/usr/share/pixmaps`) — 64 px > 48 px PNG, then SVG -2. Flathub media server — for reverse-DNS app IDs (e.g. `org.gnome.Gedit`) -3. icon.horse — downloaded and cached -4. `application-x-executable` fallback from system theme +1. System icon theme (`~/.local/share/icons`, `/usr/share/icons`, `/usr/share/pixmaps`) — 64 px > 48 px > 128 px > 32 px > 256 px PNG, then SVG +2. Flathub appstream CDN — for reverse-DNS app IDs (e.g. `org.gnome.Gedit`) +3. `application-x-executable` fallback from system theme ### Systemd service (run on login) From eaf3fdc93f14579adb1374af4cd151c9d97bfeb2 Mon Sep 17 00:00:00 2001 From: Breadway Date: Sun, 5 Jul 2026 09:15:42 +0800 Subject: [PATCH 03/30] 0.2.6: search placeholder text, tighten fuzzy-match noise Search entry's placeholder read "breadbox" -- the app's own name -- easily mistaken for an already-typed query. Now reads "Search apps...". Bare-subsequence matching (e.g. "zen" matching "Avahi Zeroconf Browser" via z...e...n) surfaced alongside real name-based hits like "Zen Browser". The filter now hides subsequence-only rows whenever a stronger, name-based match exists elsewhere in the results -- that kind of noise now only shows up when it's the best any entry can do. Note: bumping to 0.2.6, not 0.2.5 -- Cargo.toml had drifted one release behind the actual latest tag (v0.2.5 already existed). --- Cargo.lock | 6 ++-- breadbox-shared/Cargo.toml | 2 +- breadbox-sync/Cargo.toml | 2 +- breadbox/Cargo.toml | 2 +- breadbox/src/main.rs | 64 +++++++++++++++++++++++++++++++------- 5 files changed, 59 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4f3f9c2..a82d64b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,7 +39,7 @@ dependencies = [ [[package]] name = "breadbox" -version = "0.2.4" +version = "0.2.6" dependencies = [ "bread-theme", "breadbox-shared", @@ -50,7 +50,7 @@ dependencies = [ [[package]] name = "breadbox-shared" -version = "0.2.4" +version = "0.2.6" dependencies = [ "serde", "serde_json", @@ -59,7 +59,7 @@ dependencies = [ [[package]] name = "breadbox-sync" -version = "0.2.4" +version = "0.2.6" dependencies = [ "breadbox-shared", "serde_json", diff --git a/breadbox-shared/Cargo.toml b/breadbox-shared/Cargo.toml index ec62ed1..f69d7a1 100644 --- a/breadbox-shared/Cargo.toml +++ b/breadbox-shared/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "breadbox-shared" -version = "0.2.4" +version = "0.2.6" edition = "2021" license = "MIT" diff --git a/breadbox-sync/Cargo.toml b/breadbox-sync/Cargo.toml index bdcab16..d85f1dc 100644 --- a/breadbox-sync/Cargo.toml +++ b/breadbox-sync/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "breadbox-sync" -version = "0.2.4" +version = "0.2.6" edition = "2021" license = "MIT" diff --git a/breadbox/Cargo.toml b/breadbox/Cargo.toml index 9189abf..9d3f28b 100644 --- a/breadbox/Cargo.toml +++ b/breadbox/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "breadbox" -version = "0.2.4" +version = "0.2.6" edition = "2021" license = "MIT" diff --git a/breadbox/src/main.rs b/breadbox/src/main.rs index 17f72ac..e5e5840 100644 --- a/breadbox/src/main.rs +++ b/breadbox/src/main.rs @@ -252,6 +252,42 @@ fn fuzzy_score(query: &str, entry: &DesktopEntry) -> u32 { 4 // subsequence match } +/// Same tiers as `fuzzy_score`, but `None` when nothing matches at all +/// (rather than falling through to a bare-subsequence score), and folds in +/// `exec` as a tier-4 (weakest) match too — used for filtering, not sorting. +/// Tier 4 is loose enough that e.g. querying "zen" matches "Avahi Zeroconf +/// Browser" (z…e…n as a subsequence) alongside the real "Zen Browser" hit; +/// the filter hides tier-4-only rows whenever a tier ≤2 (name-based) match +/// exists elsewhere in the list, so that kind of noise only shows up when +/// it's the best any entry can do. +fn match_tier(query: &str, entry: &DesktopEntry) -> Option { + if query.is_empty() { + return Some(0); + } + let q = query.to_lowercase(); + let name = entry.name.to_lowercase(); + let wm = entry.wm_class.as_deref().unwrap_or("").to_lowercase(); + if name == q || wm == q { + return Some(0); + } + if name.starts_with(&q) { + return Some(1); + } + if name.contains(&q) { + return Some(2); + } + if wm.starts_with(&q) || wm.contains(&q) { + return Some(3); + } + if fuzzy_matches(query, &entry.name) + || entry.wm_class.as_deref().is_some_and(|w| fuzzy_matches(query, w)) + || fuzzy_matches(query, &entry.exec) + { + return Some(4); + } + None +} + // ---- PID file toggle -------------------------------------------------------- fn pid_file() -> PathBuf { @@ -344,7 +380,7 @@ fn run_ui(entries: Vec, history: LaunchHistory) { vbox.set_size_request(600, -1); let search = SearchEntry::new(); - search.set_placeholder_text(Some("breadbox")); + search.set_placeholder_text(Some("Search apps…")); vbox.append(&search); let scroll = ScrolledWindow::new(); @@ -425,20 +461,26 @@ fn run_ui(entries: Vec, history: LaunchHistory) { let text = entry.text(); let query = text.as_str(); *filter_query.borrow_mut() = query.to_string(); + + // Two passes: first collect each row's match tier, then decide + // visibility — a tier-4 (bare subsequence) row only gets hidden + // once we know whether some *other* row has a real tier ≤2 hit. + let mut rows = Vec::new(); let mut i = 0i32; while let Some(row) = list_f.row_at_index(i) { - let vis = get_row_entry(&row) - .map(|e| { - fuzzy_matches(query, &e.name) - || e.wm_class - .as_deref() - .is_some_and(|w| fuzzy_matches(query, w)) - || fuzzy_matches(query, &e.exec) - }) - .unwrap_or(false); - row.set_visible(vis); + let tier = get_row_entry(&row).and_then(|e| match_tier(query, &e)); + rows.push((row, tier)); i += 1; } + let has_direct_hit = rows.iter().any(|(_, t)| matches!(t, Some(0..=2))); + for (row, tier) in &rows { + let vis = match tier { + None => false, + Some(4) if has_direct_hit => false, + Some(_) => true, + }; + row.set_visible(vis); + } list_f.invalidate_sort(); let first_vis = (0i32..).find_map(|j| { list_f.row_at_index(j).filter(|r| r.is_visible()) From 1dc7030237a866bf28f7ac1eb357fea32866d930 Mon Sep 17 00:00:00 2001 From: Breadway Date: Wed, 22 Jul 2026 09:58:10 +0800 Subject: [PATCH 04/30] ci: add dev/beta build track workflows Adds dev-release.yml (publishes on every push to dev) and beta-release.yml (publishes on a beta-v* tag), mirroring the pattern landing in bread-ecosystem/bread. See bread-ecosystem/docs/release-channels.md for the three-track policy. --- .forgejo/workflows/beta-release.yml | 57 +++++++++++++++++++++++++ .forgejo/workflows/dev-release.yml | 66 +++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 .forgejo/workflows/beta-release.yml create mode 100644 .forgejo/workflows/dev-release.yml diff --git a/.forgejo/workflows/beta-release.yml b/.forgejo/workflows/beta-release.yml new file mode 100644 index 0000000..357cee3 --- /dev/null +++ b/.forgejo/workflows/beta-release.yml @@ -0,0 +1,57 @@ +name: beta release + +# Publishes a beta-track build when a `beta-v*` tag is pushed — +# separate from release.yml's tag-triggered stable releases. See +# bread-ecosystem's docs/release-channels.md for the three-track policy +# this is part of. +on: + push: + tags: ['beta-v*'] + +jobs: + build: + 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 && cargo build --release --locked + + - name: prepare artifacts + run: | + set -euo pipefail + VERSION="${GITHUB_REF_NAME#beta-v}" + PKG_DIR="/srv/breadway-dl/beta/breadbox/${VERSION}" + mkdir -p "${PKG_DIR}" + for bin in breadbox breadbox-sync; do + cp "src/target/release/${bin}" "${PKG_DIR}/${bin}-x86_64" + strip "${PKG_DIR}/${bin}-x86_64" + sha256sum "${PKG_DIR}/${bin}-x86_64" | awk '{print $1}' \ + > "${PKG_DIR}/${bin}-x86_64.sha256" + done + cp src/packaging/breadbox-sync.service "${PKG_DIR}/" + cp src/config.example.toml "${PKG_DIR}/" + cp src/bakery.toml "${PKG_DIR}/bakery.toml" + ln -sfn "${VERSION}" "/srv/breadway-dl/beta/breadbox/latest" + + # No GitHub Release upload — beta, like the other non-stable track, + # 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 + # --branch dev: the TRACK-aware gen-index.sh isn't merged to + # bread-ecosystem's main yet. + git clone --branch dev https://git.breadway.dev/Breadway/bread-ecosystem.git /tmp/bread-ecosystem-ci + TRACK=beta bash /tmp/bread-ecosystem-ci/scripts/gen-index.sh diff --git a/.forgejo/workflows/dev-release.yml b/.forgejo/workflows/dev-release.yml new file mode 100644 index 0000000..5a50b5a --- /dev/null +++ b/.forgejo/workflows/dev-release.yml @@ -0,0 +1,66 @@ +name: dev release + +# Publishes a dev-track build on every push to `dev` — +# separate from release.yml's tag-triggered stable releases. See +# bread-ecosystem's docs/release-channels.md for the three-track policy +# this is part of. +on: + push: + branches: ['dev'] + +jobs: + build: + runs-on: [self-hosted, hestia] + steps: + - name: checkout + run: | + set -euo pipefail + rm -rf src && mkdir src + git clone --branch dev --depth 1 \ + "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src + + - name: build + run: cd src && cargo build --release --locked + + - name: compute dev version + run: | + set -euo pipefail + cd src + CUR="$(grep -m1 '^version' Cargo.toml | sed -E 's/.*"(.*)".*/\1/')" + 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/breadbox/${VERSION}" + mkdir -p "${PKG_DIR}" + for bin in breadbox breadbox-sync; do + cp "src/target/release/${bin}" "${PKG_DIR}/${bin}-x86_64" + strip "${PKG_DIR}/${bin}-x86_64" + sha256sum "${PKG_DIR}/${bin}-x86_64" | awk '{print $1}' \ + > "${PKG_DIR}/${bin}-x86_64.sha256" + done + cp src/packaging/breadbox-sync.service "${PKG_DIR}/" + cp src/config.example.toml "${PKG_DIR}/" + cp src/bakery.toml "${PKG_DIR}/bakery.toml" + ln -sfn "${VERSION}" "/srv/breadway-dl/dev/breadbox/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 + # --branch dev: the TRACK-aware gen-index.sh isn't merged to + # bread-ecosystem's main yet. + git clone --branch dev https://git.breadway.dev/Breadway/bread-ecosystem.git /tmp/bread-ecosystem-ci + TRACK=dev bash /tmp/bread-ecosystem-ci/scripts/gen-index.sh From 4df124e7e84008f2b8f5b76cf8982fdbf1a20397 Mon Sep 17 00:00:00 2001 From: Breadway Date: Wed, 22 Jul 2026 10:10:03 +0800 Subject: [PATCH 05/30] ci: retrigger dev-track build now that BAKERY_MINISIGN_SEC_KEY_PATH is set From c0d1728bc09a27ed90f3b1d33267cdc5e3af729f Mon Sep 17 00:00:00 2001 From: Breadway Date: Wed, 22 Jul 2026 10:24:18 +0800 Subject: [PATCH 06/30] ci: use a unique temp dir for the bread-ecosystem clone in dev/beta CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fixed /tmp/bread-ecosystem-ci path races when multiple repos' dev/beta workflows run close together on the same self-hosted runner — one job's rm -rf/clone can stomp another's in-progress checkout, causing the regenerate-index step to fail intermittently. Switch to mktemp -d. --- .forgejo/workflows/beta-release.yml | 12 +++++++----- .forgejo/workflows/dev-release.yml | 14 ++++++++------ 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/.forgejo/workflows/beta-release.yml b/.forgejo/workflows/beta-release.yml index 357cee3..25cbbc2 100644 --- a/.forgejo/workflows/beta-release.yml +++ b/.forgejo/workflows/beta-release.yml @@ -50,8 +50,10 @@ jobs: 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 - # --branch dev: the TRACK-aware gen-index.sh isn't merged to - # bread-ecosystem's main yet. - git clone --branch dev https://git.breadway.dev/Breadway/bread-ecosystem.git /tmp/bread-ecosystem-ci - TRACK=beta bash /tmp/bread-ecosystem-ci/scripts/gen-index.sh + 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 dev https://git.breadway.dev/Breadway/bread-ecosystem.git "${ECOSYSTEM_CI_DIR}" + TRACK=beta bash "${ECOSYSTEM_CI_DIR}/scripts/gen-index.sh" + rm -rf "${ECOSYSTEM_CI_DIR}" diff --git a/.forgejo/workflows/dev-release.yml b/.forgejo/workflows/dev-release.yml index 5a50b5a..b128412 100644 --- a/.forgejo/workflows/dev-release.yml +++ b/.forgejo/workflows/dev-release.yml @@ -26,7 +26,7 @@ jobs: run: | set -euo pipefail cd src - CUR="$(grep -m1 '^version' Cargo.toml | sed -E 's/.*"(.*)".*/\1/')" + CUR="$(grep -m1 '^version' breadbox/Cargo.toml | sed -E 's/.*"(.*)".*/\1/')" IFS='.' read -r MA MI PA <<< "${CUR}" SHA="$(git rev-parse --short HEAD)" TS="$(date -u +%Y%m%d%H%M%S)" @@ -59,8 +59,10 @@ jobs: 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 - # --branch dev: the TRACK-aware gen-index.sh isn't merged to - # bread-ecosystem's main yet. - git clone --branch dev https://git.breadway.dev/Breadway/bread-ecosystem.git /tmp/bread-ecosystem-ci - TRACK=dev bash /tmp/bread-ecosystem-ci/scripts/gen-index.sh + 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 dev 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}" From e20ad51def986c33062cdae4da4001dd48d3d1f1 Mon Sep 17 00:00:00 2001 From: Breadway Date: Wed, 22 Jul 2026 11:47:11 +0800 Subject: [PATCH 07/30] random commit message, read it yourself --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index f3bd9af..d6fe8f9 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,6 @@ Thumbs.db # Claude Code session data .claude/ + +# Local hygiene notes (not for commit) +CLAUDE.md From d27d6ef0b4b0bcf92e60468d7a06d904ee4d587c Mon Sep 17 00:00:00 2001 From: Breadway Date: Wed, 22 Jul 2026 13:51:58 +0800 Subject: [PATCH 08/30] ci: base dev version on the latest published tag, not Cargo.toml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cargo.toml can drift stale relative to the actual last release (observed on breadbox/breadpad/breadcrumbs/breadpaper), which made the auto-bumped dev version sort as OLDER than what's already installed — bakery's semver check correctly refused those "updates". Deriving the base version from git ls-remote --tags instead is self-healing regardless of Cargo.toml drift, with a Cargo.toml fallback only for a repo with no tags yet. --- .forgejo/workflows/dev-release.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.forgejo/workflows/dev-release.yml b/.forgejo/workflows/dev-release.yml index b128412..e914fb2 100644 --- a/.forgejo/workflows/dev-release.yml +++ b/.forgejo/workflows/dev-release.yml @@ -26,7 +26,19 @@ jobs: run: | set -euo pipefail cd src - CUR="$(grep -m1 '^version' breadbox/Cargo.toml | sed -E 's/.*"(.*)".*/\1/')" + # 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//' | sort -V | tail -1)" + if [ -n "${LATEST_TAG}" ]; then + CUR="${LATEST_TAG}" + else + CUR="$(grep -m1 '^version' breadbox/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)" From 88d5c86dc9ace2108d78be0ef90f3856d379cbe3 Mon Sep 17 00:00:00 2001 From: Breadway Date: Wed, 22 Jul 2026 18:37:22 +0800 Subject: [PATCH 09/30] ci: make beta a branch-triggered freeze track, not a one-off tag Beta is now a real stabilization branch: publishes on every push to `beta` (mirroring dev's model, auto-versioned X.Y.Z-beta.+, base version from the latest published tag) instead of a manual beta-v* tag. Fixes made during the freeze land via fix/ branches merged into `beta` directly. The gen-index.sh clone for beta pulls bread-ecosystem's default branch (main) rather than pinning to dev, since beta is the more stable track and main now carries the TRACK-aware script. --- .forgejo/workflows/beta-release.yml | 41 ++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/.forgejo/workflows/beta-release.yml b/.forgejo/workflows/beta-release.yml index 25cbbc2..1973738 100644 --- a/.forgejo/workflows/beta-release.yml +++ b/.forgejo/workflows/beta-release.yml @@ -1,12 +1,12 @@ name: beta release -# Publishes a beta-track build when a `beta-v*` tag is pushed — -# separate from release.yml's tag-triggered stable releases. See -# bread-ecosystem's docs/release-channels.md for the three-track policy -# this is part of. +# Publishes a beta-track build on every push to `beta` — a frozen +# stabilization branch cut from `dev` when ready to stabilize; only +# fix/ branches merged into `beta` should land here afterward. +# See bread-ecosystem's docs/release-channels.md for the three-track policy. on: push: - tags: ['beta-v*'] + branches: ['beta'] jobs: build: @@ -16,16 +16,37 @@ jobs: run: | set -euo pipefail rm -rf src && mkdir src - git clone --branch "${GITHUB_REF_NAME}" --depth 1 \ + git clone --branch beta --depth 1 \ "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src - name: build run: cd src && cargo build --release --locked + - name: compute beta version + run: | + set -euo pipefail + cd src + # Base the beta 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 beta 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//' | sort -V | tail -1)" + if [ -n "${LATEST_TAG}" ]; then + CUR="${LATEST_TAG}" + else + CUR="$(grep -m1 '^version' breadbox/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))-beta.${TS}+${SHA}" >> "$GITHUB_ENV" + - name: prepare artifacts run: | set -euo pipefail - VERSION="${GITHUB_REF_NAME#beta-v}" PKG_DIR="/srv/breadway-dl/beta/breadbox/${VERSION}" mkdir -p "${PKG_DIR}" for bin in breadbox breadbox-sync; do @@ -39,8 +60,8 @@ jobs: cp src/bakery.toml "${PKG_DIR}/bakery.toml" ln -sfn "${VERSION}" "/srv/breadway-dl/beta/breadbox/latest" - # No GitHub Release upload — beta, like the other non-stable track, - # is only distributed via dl.breadway.dev/beta/. + # 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 }} @@ -54,6 +75,6 @@ jobs: # 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 dev https://git.breadway.dev/Breadway/bread-ecosystem.git "${ECOSYSTEM_CI_DIR}" + 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}" From 57a6c9f20c290f9e1229a854a2939fc8d79de8de Mon Sep 17 00:00:00 2001 From: Breadway Date: Wed, 22 Jul 2026 19:40:36 +0800 Subject: [PATCH 10/30] docs: add CONTRIBUTING.md Documents the dev/beta/main branch and release-track workflow shared across the bread ecosystem. See bread-ecosystem's docs/release-channels.md for the full policy this implements. --- CONTRIBUTING.md | 91 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a55a583 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,91 @@ +# Contributing + +`breadbox` — App launcher for Hyprland / Wayland. + +Part of the bread ecosystem; this repo follows the same branch/release +workflow as every other ecosystem product. + +## Branches + +- **`main`** — release branch, always tag-ready. Nothing is committed to it + directly; it only moves forward via a `beta` merge (see below). +- **`dev`** — integration branch. All day-to-day work lands here first. + Every push to `dev` automatically builds and publishes a **dev-track** + build (see Tracks below) — use this to test your change in a real install + before it goes any further. +- **`beta`** — a frozen stabilization branch, cut from `dev` periodically. + Every push to `beta` automatically builds and publishes a **beta-track** + build. While a freeze is active, only fixes for issues found *in that + freeze* should land on `beta`. + +New work — features and bug fixes alike — goes on a short-lived branch: + +``` +feature/ +fix/ +``` + +Branch off `dev`, open a PR/push back into `dev` when ready. If you're fixing +something reported against an active `beta` freeze, branch off `beta` +instead, merge the fix there to unblock testers, and also forward the same +fix into `dev` so it doesn't quietly reappear next cycle. + +## The release cycle + +1. Work accumulates on `dev` 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 report or fix anything broken with another + push to `dev`. +2. Once `dev` has gone roughly **a week** without new issues, `beta` is cut + fresh from `dev`'s current tip. This freezes it as the stabilization + target — `dev` keeps moving independently starting the next cycle. +3. `beta` is open for anyone to test: `bakery track set beta` and + `bakery update --all`. **File issues against anything you find on this + repo's Forgejo issue tracker.** Fixes land via `fix/` branches + merged into `beta`. +4. Once `beta` has gone roughly **a month** without new issues, it's merged + into `main` and tagged `vX.Y.Z` — that tag is what actually triggers the + stable release build. `beta` is then reset from `dev` to start the next + cycle. + +## 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 | `main`, on a `vX.Y.Z` tag push | +| `beta` | Current stabilization freeze | `beta`, on every push | +| `dev` | Bleeding edge | `dev`, on every push | + +Dev/beta versions are auto-computed (`X.Y.Z-dev.+` / +`-beta.…`) from the latest published stable tag, so they always sort as +newer than what you have installed — no manual version bumping needed when +pushing to `dev` or `beta`. + +## Local development + +```sh +cargo build --release --workspace +cargo test --release --workspace +``` + +## CI + +- `dev-release.yml` — triggered on push to `dev`. +- `beta-release.yml` — triggered on push to `beta`. +- `release.yml` — triggered on a `v*` tag push, cuts the actual stable release. +- `package.yml` — publishes to the `[breadway]` pacman repo, also tag-triggered. + +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. From a47fd5852ca7221e7a7ae9259c4a79ef0acf7a4c Mon Sep 17 00:00:00 2001 From: Breadway Date: Thu, 23 Jul 2026 10:25:12 +0800 Subject: [PATCH 11/30] Drop pacman packaging, bakery-only distribution bakery already fully covers what the PKGBUILD provided (binary, systemd --user service where applicable, dependency declarations) except a LICENSE copy, which bakery.toml's new license_file field now closes. Removes packaging/arch/ and .forgejo/workflows/package.yml; adds the LICENSE artifact to each release/dev-release/beta-release workflow's prepare step. Not pacman-installed inside BOS today (BOS already consumes these apps exclusively via build-local.sh's skel-staging), so this only removes the option to `pacman -S` outside of BOS/bakery. --- .forgejo/workflows/beta-release.yml | 1 + .forgejo/workflows/dev-release.yml | 1 + .forgejo/workflows/package.yml | 40 ------------------- .forgejo/workflows/release.yml | 60 +++++++++++++++++++++++++++++ bakery.toml | 1 + packaging/arch/PKGBUILD | 39 ------------------- 6 files changed, 63 insertions(+), 79 deletions(-) delete mode 100644 .forgejo/workflows/package.yml create mode 100644 .forgejo/workflows/release.yml delete mode 100644 packaging/arch/PKGBUILD diff --git a/.forgejo/workflows/beta-release.yml b/.forgejo/workflows/beta-release.yml index 1973738..9e10fd5 100644 --- a/.forgejo/workflows/beta-release.yml +++ b/.forgejo/workflows/beta-release.yml @@ -57,6 +57,7 @@ jobs: done cp src/packaging/breadbox-sync.service "${PKG_DIR}/" cp src/config.example.toml "${PKG_DIR}/" + cp src/LICENSE "${PKG_DIR}/" cp src/bakery.toml "${PKG_DIR}/bakery.toml" ln -sfn "${VERSION}" "/srv/breadway-dl/beta/breadbox/latest" diff --git a/.forgejo/workflows/dev-release.yml b/.forgejo/workflows/dev-release.yml index e914fb2..ac58902 100644 --- a/.forgejo/workflows/dev-release.yml +++ b/.forgejo/workflows/dev-release.yml @@ -57,6 +57,7 @@ jobs: done cp src/packaging/breadbox-sync.service "${PKG_DIR}/" cp src/config.example.toml "${PKG_DIR}/" + cp src/LICENSE "${PKG_DIR}/" cp src/bakery.toml "${PKG_DIR}/bakery.toml" ln -sfn "${VERSION}" "/srv/breadway-dl/dev/breadbox/latest" diff --git a/.forgejo/workflows/package.yml b/.forgejo/workflows/package.yml deleted file mode 100644 index fecfe9d..0000000 --- a/.forgejo/workflows/package.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Build and publish package - -on: - push: - tags: ['v*'] - -jobs: - package: - runs-on: [self-hosted, hestia] - container: - image: archlinux:latest - steps: - # Note: no actions/checkout — the archlinux image has no Node, which JS - # actions require. Everything runs as shell steps and clones manually. - - name: Build and publish - env: - PUBLISH_TOKEN: ${{ secrets.REGISTRY_TOKEN }} - run: | - set -euo pipefail - VERSION="${GITHUB_REF_NAME#v}" - pacman -Syu --noconfirm base-devel git rust cargo gtk4 gtk4-layer-shell librsvg - 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="breadbox-${VERSION}/" HEAD \ - > packaging/arch/breadbox-${VERSION}.tar.gz - SHA=$(sha256sum packaging/arch/breadbox-${VERSION}.tar.gz | awk '{print $1}') - sed -i "s/^pkgver=.*/pkgver=${VERSION}/" packaging/arch/PKGBUILD - sed -i "s/^sha256sums=.*/sha256sums=('${SHA}')/" packaging/arch/PKGBUILD - chown -R builder:builder /home/builder/src - # --nocheck: packaging builds the artifact; tests belong in a CI job. - su builder -c "cd /home/builder/src/packaging/arch && makepkg -f --noconfirm --nocheck" - PKG=$(find /home/builder/src/packaging/arch -name '*.pkg.tar.zst' | head -1) - curl -fsS -X PUT \ - -H "Authorization: token ${PUBLISH_TOKEN}" \ - -H "Content-Type: application/octet-stream" \ - --data-binary "@${PKG}" \ - "https://git.breadway.dev/api/packages/Breadway/arch/os" diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml new file mode 100644 index 0000000..998d491 --- /dev/null +++ b/.forgejo/workflows/release.yml @@ -0,0 +1,60 @@ +name: release + +on: + push: + tags: ["v*"] + +jobs: + build: + 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 && cargo build --release --locked + + - name: prepare artifacts + run: | + set -euo pipefail + VERSION="${GITHUB_REF_NAME#v}" + PKG_DIR="/srv/breadway-dl/breadbox/${VERSION}" + mkdir -p "${PKG_DIR}" + for bin in breadbox breadbox-sync; do + cp "src/target/release/${bin}" "${PKG_DIR}/${bin}-x86_64" + strip "${PKG_DIR}/${bin}-x86_64" + sha256sum "${PKG_DIR}/${bin}-x86_64" | awk '{print $1}' \ + > "${PKG_DIR}/${bin}-x86_64.sha256" + done + cp src/packaging/breadbox-sync.service "${PKG_DIR}/" + cp src/config.example.toml "${PKG_DIR}/" + cp src/LICENSE "${PKG_DIR}/" + cp src/bakery.toml "${PKG_DIR}/bakery.toml" + ln -sfn "${VERSION}" "/srv/breadway-dl/breadbox/latest" + + - name: regenerate index.json + run: | + set -euo pipefail + rm -rf /tmp/bread-ecosystem-ci + git clone https://git.breadway.dev/Breadway/bread-ecosystem.git /tmp/bread-ecosystem-ci + bash /tmp/bread-ecosystem-ci/scripts/gen-index.sh + + - name: upload to GitHub Release + env: + GH_TOKEN: ${{ secrets.GH_RELEASE_TOKEN }} + run: | + set -euo pipefail + VERSION="${GITHUB_REF_NAME#v}" + PKG_DIR="/srv/breadway-dl/breadbox/${VERSION}" + gh release create "${GITHUB_REF_NAME}" --repo Breadway/breadbox \ + --title "breadbox v${VERSION}" --generate-notes 2>/dev/null || true + gh release upload "${GITHUB_REF_NAME}" --repo Breadway/breadbox \ + "${PKG_DIR}/breadbox-x86_64" \ + "${PKG_DIR}/breadbox-sync-x86_64" \ + "${PKG_DIR}/breadbox-x86_64.sha256" \ + "${PKG_DIR}/breadbox-sync-x86_64.sha256" \ + --clobber diff --git a/bakery.toml b/bakery.toml index f2abae4..6ad1675 100644 --- a/bakery.toml +++ b/bakery.toml @@ -4,6 +4,7 @@ binaries = ["breadbox", "breadbox-sync"] system_deps = ["gtk4", "gtk4-layer-shell", "librsvg"] optional_system_deps = ["hyprland"] bread_deps = [] +license_file = "LICENSE" [[service]] unit = "breadbox-sync.service" diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD deleted file mode 100644 index 1df5ba5..0000000 --- a/packaging/arch/PKGBUILD +++ /dev/null @@ -1,39 +0,0 @@ -# Maintainer: Breadway - -pkgname=breadbox -pkgver=0.1.0 -pkgrel=1 -pkgdesc="App launcher for Hyprland / Wayland" -arch=('x86_64') -url="https://github.com/Breadway/breadbox" -license=('MIT') -# Some Rust deps (ring/mlua) build vendored C/asm into static archives; makepkg's -# default -flto=auto emits GCC LTO bitcode the Rust (lld) link cannot read, -# causing undefined-symbol errors. Disable LTO. -options=(!lto !debug) -depends=('gtk4' 'gtk4-layer-shell' 'librsvg') -optdepends=( - 'hyprland: window and workspace integration' -) -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 --workspace -} - -package() { - cd "${srcdir}/${pkgname}-${pkgver}" - install -Dm755 target/release/breadbox "${pkgdir}/usr/bin/breadbox" - install -Dm755 target/release/breadbox-sync "${pkgdir}/usr/bin/breadbox-sync" - install -Dm644 packaging/breadbox-sync.service \ - "${pkgdir}/usr/lib/systemd/user/breadbox-sync.service" - install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" -} From 8d6573590b3c1a0a1ed47b609a64abde21f6591c Mon Sep 17 00:00:00 2001 From: Breadway Date: Wed, 29 Jul 2026 11:32:37 +0800 Subject: [PATCH 12/30] breadbox: add --screenshot CLI mode for automated capture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same pattern as breadbar's screenshot mode (see bread-ecosystem's bread-capture orchestrator): render the launcher panel, capture it via bread-screenshots, then exit. Only one view worth capturing here ("launcher") since breadbox has no separate layer surfaces or popovers — just the search box + app list over a full-screen transparent overlay — so a full known-size canvas capture is enough, no geometry tracking needed. Also fixes a real footgun in the existing PID-file toggle: it kills whatever process is holding breadbox.pid, which on this machine is typically the real, already-running breadbox. A screenshot run now skips toggle_or_continue() entirely instead of fighting over (and killing) the operator's real instance. --- Cargo.lock | 211 +++++++++++++++++++++++++++++++++++-- breadbox/Cargo.toml | 4 + breadbox/src/main.rs | 48 +++++++-- breadbox/src/screenshot.rs | 96 +++++++++++++++++ 4 files changed, 342 insertions(+), 17 deletions(-) create mode 100644 breadbox/src/screenshot.rs diff --git a/Cargo.lock b/Cargo.lock index 4f3f9c2..eba1794 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,62 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + [[package]] name = "autocfg" version = "1.5.1" @@ -26,6 +82,16 @@ version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +[[package]] +name = "bread-screenshots" +version = "0.3.1" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=dev#27b3b17c58b81f947a40cc0d3f9cd0862e7885ff" +dependencies = [ + "anyhow", + "bread-utils", + "tracing", +] + [[package]] name = "bread-theme" version = "0.2.3" @@ -37,12 +103,25 @@ dependencies = [ "serde_json", ] +[[package]] +name = "bread-utils" +version = "0.3.1" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=dev#27b3b17c58b81f947a40cc0d3f9cd0862e7885ff" +dependencies = [ + "dirs", + "serde", + "serde_json", +] + [[package]] name = "breadbox" version = "0.2.4" dependencies = [ + "anyhow", + "bread-screenshots", "bread-theme", "breadbox-shared", + "clap", "gtk4", "gtk4-layer-shell", "serde_json", @@ -115,6 +194,52 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "clap" +version = "4.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "crc32fast" version = "1.5.0" @@ -153,7 +278,7 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -237,7 +362,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -408,7 +533,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -544,7 +669,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -691,6 +816,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itoa" version = "1.0.18" @@ -761,6 +892,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "option-ext" version = "0.2.0" @@ -947,7 +1084,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1011,6 +1148,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -1028,6 +1171,17 @@ 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 = "synstructure" version = "0.13.2" @@ -1036,7 +1190,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1075,7 +1229,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1180,6 +1334,37 @@ version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + [[package]] name = "unicode-ident" version = "1.0.24" @@ -1226,6 +1411,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "version-compare" version = "0.2.1" @@ -1459,7 +1650,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -1480,7 +1671,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", "synstructure", ] @@ -1520,7 +1711,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] diff --git a/breadbox/Cargo.toml b/breadbox/Cargo.toml index 9189abf..85d6286 100644 --- a/breadbox/Cargo.toml +++ b/breadbox/Cargo.toml @@ -10,7 +10,11 @@ path = "src/main.rs" [dependencies] bread-theme = { git = "https://github.com/Breadway/bread-ecosystem", tag = "v0.2.8", features = ["gtk"] } +# Capture primitives for `--screenshot` mode — see src/screenshot.rs. +bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", branch = "dev" } breadbox-shared = { path = "../breadbox-shared" } gtk4 = { version = "0.11", features = ["v4_12"] } gtk4-layer-shell = "0.8" serde_json = "1" +clap = { version = "4", features = ["derive"] } +anyhow = "1" diff --git a/breadbox/src/main.rs b/breadbox/src/main.rs index 17f72ac..785ccf9 100644 --- a/breadbox/src/main.rs +++ b/breadbox/src/main.rs @@ -23,6 +23,8 @@ use gtk4::{ }; use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell}; +mod screenshot; + // ---- Hyprland IPC ----------------------------------------------------------- fn get_active_workspace() -> Option { @@ -295,13 +297,24 @@ fn get_row_entry(row: >k4::ListBoxRow) -> Option { } } -fn run_ui(entries: Vec, history: LaunchHistory) { - let app = Application::builder() - .application_id("com.breadway.breadbox") - .build(); +fn run_ui( + entries: Vec, + history: LaunchHistory, + screenshot_req: Option, +) { + let mut builder = Application::builder().application_id("com.breadway.breadbox"); + if screenshot_req.is_some() { + // GApplication is single-instance by default; this machine typically + // already has a real breadbox instance, so without this a screenshot + // run would just message the *existing* instance instead of + // starting a fresh one that ever sees `screenshot_req`. + builder = builder.flags(gtk4::gio::ApplicationFlags::NON_UNIQUE); + } + let app = builder.build(); let history_rc = Rc::new(RefCell::new(history)); let query_rc: Rc> = Rc::new(RefCell::new(String::new())); + let is_screenshot_run = screenshot_req.is_some(); app.connect_activate(move |app| { // Shared ecosystem base (fonts, palette, generic widgets) first, then @@ -539,17 +552,38 @@ fn run_ui(entries: Vec, history: LaunchHistory) { window.add_controller(outside_click); window.connect_destroy(|_| cleanup_pid()); + + if let Some(req) = screenshot_req.clone() { + screenshot::dispatch(&window, req); + } + window.present(); search.grab_focus(); }); - app.run(); + if is_screenshot_run { + // GLib's own option parser otherwise rejects --screenshot/--output + // before clap ever sees them (`Cli::parse()` already ran in `main`, + // over the real argv). + app.run_with_args(&[] as &[&str]); + } else { + app.run(); + } } // ---- Main ------------------------------------------------------------------- fn main() { - if !toggle_or_continue() { + use clap::Parser; + let cli = screenshot::Cli::parse(); + let screenshot_req = cli.screenshot_request(); + + // The PID-file toggle kills whatever's holding the file — a real, + // already-running breadbox instance included. A screenshot run must + // never touch it: it's a separate, disposable instance by design (same + // reasoning as breadbar's `allow_multiple_instances`), not a toggle of + // the operator's real launcher. + if screenshot_req.is_none() && !toggle_or_continue() { return; } @@ -564,5 +598,5 @@ fn main() { let manifest = load_manifest(); let entries = load_sorted_entries(&manifest, &priority, &history); - run_ui(entries, history); + run_ui(entries, history, screenshot_req); } diff --git a/breadbox/src/screenshot.rs b/breadbox/src/screenshot.rs new file mode 100644 index 0000000..59104ee --- /dev/null +++ b/breadbox/src/screenshot.rs @@ -0,0 +1,96 @@ +//! `--screenshot` CLI mode: render breadbox's launcher panel, capture it via +//! `bread-screenshots`, then exit — driven by `bread-ecosystem`'s +//! `bread-capture` orchestrator, or run standalone for one-off captures. +//! +//! breadbox has only one view worth capturing: the launcher panel itself +//! (search box + app list). It's a `halign: Center` panel over a full-screen +//! transparent overlay window, not its own layer surface, so — same +//! reasoning as breadbar's control-panel view — the simplest reliable +//! capture is the whole known-size canvas, not a hand-tracked panel +//! geometry. + +use clap::Parser; +use gtk4::prelude::*; +use std::path::PathBuf; +use std::time::Duration; + +/// Extra settle time after `map` for the first frame to actually paint +/// before grim runs — `map` fires once the surface exists, not once +/// anything has been drawn into it. +const SETTLE_DELAY: Duration = Duration::from_millis(300); + +#[derive(Parser)] +#[command(name = "breadbox")] +pub struct Cli { + /// Render the named view, capture it, then exit instead of running + /// normally. Known views: "launcher". + #[arg(long)] + pub screenshot: Option, + + /// PNG path to write the capture to. Required together with --screenshot. + #[arg(long)] + pub output: Option, + + /// Capture canvas width — matches the isolated compositor's output width + /// (`bread-capture --isolate-width`). + #[arg(long, default_value_t = 1920)] + pub width: u32, + + /// Capture canvas height — see `width`. + #[arg(long, default_value_t = 1080)] + pub height: u32, +} + +#[derive(Clone)] +pub struct ScreenshotRequest { + pub view: String, + pub output: PathBuf, + pub width: u32, + pub height: u32, +} + +impl Cli { + /// `None` for a normal run. Exits the process with an error if + /// `--screenshot` was given without `--output`, before any GTK setup + /// happens. + pub fn screenshot_request(&self) -> Option { + let view = self.screenshot.clone()?; + let Some(output) = self.output.clone() else { + eprintln!("breadbox: --screenshot requires --output"); + std::process::exit(1); + }; + Some(ScreenshotRequest { view, output, width: self.width, height: self.height }) + } +} + +/// Wire up the given view's screenshot sequence against an already-built, +/// not-yet-presented window. Every path here ends by exiting the process — +/// it never returns control to the normal launcher UI. +pub fn dispatch(window: >k4::ApplicationWindow, req: ScreenshotRequest) { + match req.view.as_str() { + "launcher" => { + let output = req.output; + let (width, height) = (req.width as i32, req.height as i32); + window.connect_map(move |_| { + let output = output.clone(); + gtk4::glib::timeout_add_local_once(SETTLE_DELAY, move || { + finish(bread_screenshots::capture_region(0, 0, width, height, &output)); + }); + }); + } + other => { + eprintln!("breadbox: unknown screenshot view '{other}' (known: launcher)"); + std::process::exit(1); + } + } +} + +fn finish(result: anyhow::Result<()>) { + match result { + Ok(()) => std::process::exit(0), + Err(e) => { + eprintln!("breadbox: screenshot capture failed: {e}"); + std::process::exit(1); + } + } +} From 7907b1192b68b05200f464decbc47d2b6251bfd9 Mon Sep 17 00:00:00 2001 From: Breadway Date: Fri, 31 Jul 2026 11:05:29 +0800 Subject: [PATCH 13/30] =?UTF-8?q?CI:=20single-trunk=20model=20=E2=80=94=20?= =?UTF-8?q?dev=20triggers=20on=20main,=20beta=20becomes=20RC-tag-triggered?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the dev/beta branch split with one trunk (main): dev-track builds still publish on every push, but the beta track now publishes from a vX.Y.Z-rc.N prerelease tag instead of a separately-maintained beta branch. Removes the branch nobody reliably kept in sync. --- .forgejo/workflows/dev-release.yml | 15 ++++---- .forgejo/workflows/mirror.yml | 21 ---------- .../{beta-release.yml => rc-release.yml} | 38 +++++-------------- .forgejo/workflows/release.yml | 1 + 4 files changed, 17 insertions(+), 58 deletions(-) delete mode 100644 .forgejo/workflows/mirror.yml rename .forgejo/workflows/{beta-release.yml => rc-release.yml} (59%) diff --git a/.forgejo/workflows/dev-release.yml b/.forgejo/workflows/dev-release.yml index ac58902..a7c6b7f 100644 --- a/.forgejo/workflows/dev-release.yml +++ b/.forgejo/workflows/dev-release.yml @@ -1,12 +1,11 @@ name: dev release -# Publishes a dev-track build on every push to `dev` — -# separate from release.yml's tag-triggered stable releases. See -# bread-ecosystem's docs/release-channels.md for the three-track policy -# this is part of. +# 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: ['dev'] + branches: ['main'] jobs: build: @@ -16,7 +15,7 @@ jobs: run: | set -euo pipefail rm -rf src && mkdir src - git clone --branch dev --depth 1 \ + git clone --branch main --depth 1 \ "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src - name: build @@ -33,7 +32,7 @@ jobs: # 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//' | sort -V | tail -1)" + | awk -F/ '{print $NF}' | sed 's/^v//' | (grep -v -- '-' || true) | sort -V | tail -1)" if [ -n "${LATEST_TAG}" ]; then CUR="${LATEST_TAG}" else @@ -76,6 +75,6 @@ jobs: # 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 dev https://git.breadway.dev/Breadway/bread-ecosystem.git "${ECOSYSTEM_CI_DIR}" + git clone --branch main https://git.breadway.dev/Breadway/bread-ecosystem.git "${ECOSYSTEM_CI_DIR}" TRACK=dev bash "${ECOSYSTEM_CI_DIR}/scripts/gen-index.sh" rm -rf "${ECOSYSTEM_CI_DIR}" diff --git a/.forgejo/workflows/mirror.yml b/.forgejo/workflows/mirror.yml deleted file mode 100644 index 6f40c04..0000000 --- a/.forgejo/workflows/mirror.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Mirror to GitHub - -on: - push: - branches: ['**'] - tags: ['**'] - -jobs: - mirror: - runs-on: [self-hosted, hestia] - steps: - - name: Mirror to GitHub - run: | - set -euo pipefail - git clone --mirror "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" repo.git - cd repo.git - # Mirror only branches and tags (not refs/pull/*, which GitHub rejects); - # --prune deletes GitHub refs that no longer exist on Forgejo. - git push --prune \ - "https://x-access-token:${{ secrets.MIRROR_TOKEN }}@github.com/Breadway/breadbox.git" \ - '+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*' diff --git a/.forgejo/workflows/beta-release.yml b/.forgejo/workflows/rc-release.yml similarity index 59% rename from .forgejo/workflows/beta-release.yml rename to .forgejo/workflows/rc-release.yml index 9e10fd5..39a302e 100644 --- a/.forgejo/workflows/beta-release.yml +++ b/.forgejo/workflows/rc-release.yml @@ -1,52 +1,32 @@ -name: beta release +name: beta (rc) release -# Publishes a beta-track build on every push to `beta` — a frozen -# stabilization branch cut from `dev` when ready to stabilize; only -# fix/ branches merged into `beta` should land here afterward. -# See bread-ecosystem's docs/release-channels.md for the three-track policy. +# 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: - branches: ['beta'] + 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 beta --depth 1 \ + git clone --branch "${GITHUB_REF_NAME}" --depth 1 \ "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src - name: build run: cd src && cargo build --release --locked - - name: compute beta version - run: | - set -euo pipefail - cd src - # Base the beta 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 beta 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//' | sort -V | tail -1)" - if [ -n "${LATEST_TAG}" ]; then - CUR="${LATEST_TAG}" - else - CUR="$(grep -m1 '^version' breadbox/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))-beta.${TS}+${SHA}" >> "$GITHUB_ENV" - - name: prepare artifacts run: | set -euo pipefail + VERSION="${GITHUB_REF_NAME#v}" PKG_DIR="/srv/breadway-dl/beta/breadbox/${VERSION}" mkdir -p "${PKG_DIR}" for bin in breadbox breadbox-sync; do diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 998d491..23b2a47 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -6,6 +6,7 @@ on: jobs: build: + if: ${{ !contains(github.ref_name, '-rc.') }} runs-on: [self-hosted, hestia] steps: - name: checkout From 0347d6deaebdd6794a500d3ba5c097702eaaa1e9 Mon Sep 17 00:00:00 2001 From: Breadway Date: Fri, 31 Jul 2026 11:08:41 +0800 Subject: [PATCH 14/30] CONTRIBUTING.md: document single-trunk + RC-tag release model --- CONTRIBUTING.md | 71 ++++++++++++++++++++++--------------------------- 1 file changed, 32 insertions(+), 39 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a55a583..5ae0190 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,16 +7,10 @@ workflow as every other ecosystem product. ## Branches -- **`main`** — release branch, always tag-ready. Nothing is committed to it - directly; it only moves forward via a `beta` merge (see below). -- **`dev`** — integration branch. All day-to-day work lands here first. - Every push to `dev` automatically builds and publishes a **dev-track** - build (see Tracks below) — use this to test your change in a real install - before it goes any further. -- **`beta`** — a frozen stabilization branch, cut from `dev` periodically. - Every push to `beta` automatically builds and publishes a **beta-track** - build. While a freeze is active, only fixes for issues found *in that - freeze* should land on `beta`. +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: @@ -25,28 +19,26 @@ feature/ fix/ ``` -Branch off `dev`, open a PR/push back into `dev` when ready. If you're fixing -something reported against an active `beta` freeze, branch off `beta` -instead, merge the fix there to unblock testers, and also forward the same -fix into `dev` so it doesn't quietly reappear next cycle. +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 -1. Work accumulates on `dev` via `feature/x` / `fix/x` branches. Each push +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 report or fix anything broken with another - push to `dev`. -2. Once `dev` has gone roughly **a week** without new issues, `beta` is cut - fresh from `dev`'s current tip. This freezes it as the stabilization - target — `dev` keeps moving independently starting the next cycle. -3. `beta` is open for anyone to test: `bakery track set beta` and - `bakery update --all`. **File issues against anything you find on this - repo's Forgejo issue tracker.** Fixes land via `fix/` branches - merged into `beta`. -4. Once `beta` has gone roughly **a month** without new issues, it's merged - into `main` and tagged `vX.Y.Z` — that tag is what actually triggers the - stable release build. `beta` is then reset from `dev` to start the next - cycle. + `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 @@ -58,14 +50,15 @@ bakery update --all # pull the latest build on your current track | Track | What it is | Published from | |--------|-----------|-----------------| -| `stable` | The last tagged release | `main`, on a `vX.Y.Z` tag push | -| `beta` | Current stabilization freeze | `beta`, on every push | -| `dev` | Bleeding edge | `dev`, on every push | +| `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/beta versions are auto-computed (`X.Y.Z-dev.+` / -`-beta.…`) from the latest published stable tag, so they always sort as -newer than what you have installed — no manual version bumping needed when -pushing to `dev` or `beta`. +Dev versions are auto-computed (`X.Y.Z-dev.+`) from the +latest published stable tag, so they always sort as newer than what you +have installed — no manual version bumping needed. Beta versions are just +the RC tag itself (already valid semver, already sorts below the real +release it's a candidate for). ## Local development @@ -76,10 +69,10 @@ cargo test --release --workspace ## CI -- `dev-release.yml` — triggered on push to `dev`. -- `beta-release.yml` — triggered on push to `beta`. -- `release.yml` — triggered on a `v*` tag push, cuts the actual stable release. -- `package.yml` — publishes to the `[breadway]` pacman repo, also tag-triggered. +- `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 From 1b3193855e1efb3e9131bf1200a87f48fcb98257 Mon Sep 17 00:00:00 2001 From: Breadway Date: Fri, 31 Jul 2026 14:30:01 +0800 Subject: [PATCH 15/30] Repoint bread-ecosystem git deps from dev to main branch --- Cargo.lock | 4 ++-- breadbox/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eba1794..fbbe7cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -85,7 +85,7 @@ checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "bread-screenshots" version = "0.3.1" -source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=dev#27b3b17c58b81f947a40cc0d3f9cd0862e7885ff" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=main#f86e299f4a0ea73ff485cd84923b986ddcc8242e" dependencies = [ "anyhow", "bread-utils", @@ -106,7 +106,7 @@ dependencies = [ [[package]] name = "bread-utils" version = "0.3.1" -source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=dev#27b3b17c58b81f947a40cc0d3f9cd0862e7885ff" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=main#f86e299f4a0ea73ff485cd84923b986ddcc8242e" dependencies = [ "dirs", "serde", diff --git a/breadbox/Cargo.toml b/breadbox/Cargo.toml index 85d6286..c2d859e 100644 --- a/breadbox/Cargo.toml +++ b/breadbox/Cargo.toml @@ -11,7 +11,7 @@ path = "src/main.rs" [dependencies] bread-theme = { git = "https://github.com/Breadway/bread-ecosystem", tag = "v0.2.8", features = ["gtk"] } # Capture primitives for `--screenshot` mode — see src/screenshot.rs. -bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", branch = "dev" } +bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", branch = "main" } breadbox-shared = { path = "../breadbox-shared" } gtk4 = { version = "0.11", features = ["v4_12"] } gtk4-layer-shell = "0.8" From 73b3c24a3cb60554c1378998357719a99c324c65 Mon Sep 17 00:00:00 2001 From: Breadway Date: Wed, 5 Aug 2026 19:12:32 +0800 Subject: [PATCH 16/30] CI: build on bread-ecosystem's shared Arch container Ports breadbox onto the shared, pinned CI build image (following the pattern proven in breadpad): ci/build.sh delegates to bread-ecosystem's own ci/build.sh, pinned by commit sha in ci/bread-ecosystem.rev, instead of building directly on the bare self-hosted runner. Also adds check.yml to run clippy/test on feature/fix branches ahead of the dev-track release build. No ci/deps.txt: breadbox's Cargo.toml pulls in nothing beyond what the shared Containerfile already installs (gtk4, gtk4-layer-shell). The librsvg system dep in bakery.toml is a runtime gdk-pixbuf loader for SVG icons, not a Cargo build dependency. --- .forgejo/workflows/check.yml | 24 ++++++++++++++++++++++++ .forgejo/workflows/dev-release.yml | 2 +- .forgejo/workflows/rc-release.yml | 2 +- .forgejo/workflows/release.yml | 2 +- ci/bread-ecosystem.rev | 1 + ci/build.sh | 21 +++++++++++++++++++++ 6 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 .forgejo/workflows/check.yml create mode 100644 ci/bread-ecosystem.rev create mode 100755 ci/build.sh diff --git a/.forgejo/workflows/check.yml b/.forgejo/workflows/check.yml new file mode 100644 index 0000000..b547c34 --- /dev/null +++ b/.forgejo/workflows/check.yml @@ -0,0 +1,24 @@ +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 --workspace --all-targets --locked -- -D warnings + + - name: test + run: cd src && bash ci/build.sh cargo test --workspace --locked diff --git a/.forgejo/workflows/dev-release.yml b/.forgejo/workflows/dev-release.yml index a7c6b7f..5d6839e 100644 --- a/.forgejo/workflows/dev-release.yml +++ b/.forgejo/workflows/dev-release.yml @@ -19,7 +19,7 @@ jobs: "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src - name: build - run: cd src && cargo build --release --locked + run: cd src && bash ci/build.sh cargo build --release --locked - name: compute dev version run: | diff --git a/.forgejo/workflows/rc-release.yml b/.forgejo/workflows/rc-release.yml index 39a302e..5ab74d0 100644 --- a/.forgejo/workflows/rc-release.yml +++ b/.forgejo/workflows/rc-release.yml @@ -21,7 +21,7 @@ jobs: "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src - name: build - run: cd src && cargo build --release --locked + run: cd src && bash ci/build.sh cargo build --release --locked - name: prepare artifacts run: | diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 23b2a47..2d08cd2 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -17,7 +17,7 @@ jobs: "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src - name: build - run: cd src && cargo build --release --locked + run: cd src && bash ci/build.sh cargo build --release --locked - name: prepare artifacts run: | diff --git a/ci/bread-ecosystem.rev b/ci/bread-ecosystem.rev new file mode 100644 index 0000000..34e7aa9 --- /dev/null +++ b/ci/bread-ecosystem.rev @@ -0,0 +1 @@ +147cfbbf96ae4b171027defa1130d2caddb934b1 diff --git a/ci/build.sh b/ci/build.sh new file mode 100755 index 0000000..a877acf --- /dev/null +++ b/ci/build.sh @@ -0,0 +1,21 @@ +#!/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" breadbox "$ROOT" "$@" From e5ba33d81024a855d36f3d1bfc21f7935c236016 Mon Sep 17 00:00:00 2001 From: Breadway Date: Sat, 15 Aug 2026 21:40:03 +0800 Subject: [PATCH 17/30] Pin ecosystem crates and switch to bread-utils::singleton Repoint bread-theme and bread-utils at git.breadway.dev tag v0.7.1 (no github.com). bread-screenshots is not on that tag yet, so rev-pin it instead of tracking branch=main. Replace the homegrown PID-file toggle with bread-utils::singleton so the launcher still dismisses on a second keybind without the TOCTOU window. Screenshot mode still skips the lock. README now documents the fixed BOS dark fallback (not Catppuccin Mocha). Drop the unused GitHub Actions release workflow; Forgejo is canonical. CONTRIBUTING already documents single-trunk. --- .github/README.md | 1 + .github/workflows/release.yml | 67 ----------------------------------- Cargo.lock | 23 ++++++++---- README.md | 2 +- breadbox/Cargo.toml | 7 ++-- breadbox/src/main.rs | 60 ++++++++++--------------------- 6 files changed, 42 insertions(+), 118 deletions(-) create mode 100644 .github/README.md delete mode 100644 .github/workflows/release.yml diff --git a/.github/README.md b/.github/README.md new file mode 100644 index 0000000..4cb7ee5 --- /dev/null +++ b/.github/README.md @@ -0,0 +1 @@ +Forgejo (`.forgejo/workflows`) is the canonical CI. This `.github` tree is unused. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 71c0ca3..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: release - -on: - push: - tags: ["v*"] - -permissions: - contents: write - -env: - DL_DIR: /srv/breadway-dl - ECOSYSTEM_DIR: /home/breadway/Projects/bread-ecosystem - -jobs: - build: - runs-on: [self-hosted, hestia] - steps: - - uses: actions/checkout@v4 - - - name: install build deps - run: sudo apt-get install -y libgtk-4-dev librsvg2-dev libdbus-1-dev pkg-config 2>/dev/null || true - - - name: build - run: cargo build --release --locked - - - name: prepare artifacts - run: | - VERSION="${GITHUB_REF_NAME#v}" - PKG_DIR="${DL_DIR}/breadbox/${VERSION}" - mkdir -p "${PKG_DIR}" - for bin in breadbox breadbox-sync; do - cp "target/release/${bin}" "${PKG_DIR}/${bin}-x86_64" - strip "${PKG_DIR}/${bin}-x86_64" - sha256sum "${PKG_DIR}/${bin}-x86_64" | awk '{print $1}' \ - > "${PKG_DIR}/${bin}-x86_64.sha256" - done - cp packaging/breadbox-sync.service "${PKG_DIR}/" - cp config.example.toml "${PKG_DIR}/" - cp bakery.toml "${PKG_DIR}/bakery.toml" - ln -sfn "${VERSION}" "${DL_DIR}/breadbox/latest" - - - name: ensure bread-ecosystem - run: | - if [[ -d "${ECOSYSTEM_DIR}/.git" ]]; then - git -C "${ECOSYSTEM_DIR}" pull --ff-only - else - mkdir -p "$(dirname "${ECOSYSTEM_DIR}")" - git clone https://github.com/Breadway/bread-ecosystem.git "${ECOSYSTEM_DIR}" - fi - - - name: regenerate index.json - run: bash "${ECOSYSTEM_DIR}/scripts/gen-index.sh" - - - name: upload to GitHub Release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - VERSION="${GITHUB_REF_NAME#v}" - PKG_DIR="${DL_DIR}/breadbox/${VERSION}" - gh release create "${GITHUB_REF_NAME}" \ - --title "breadbox v${VERSION}" --generate-notes 2>/dev/null || true - gh release upload "${GITHUB_REF_NAME}" \ - "${PKG_DIR}/breadbox-x86_64" \ - "${PKG_DIR}/breadbox-sync-x86_64" \ - "${PKG_DIR}/breadbox-x86_64.sha256" \ - "${PKG_DIR}/breadbox-sync-x86_64.sha256" \ - --clobber diff --git a/Cargo.lock b/Cargo.lock index fbbe7cd..d05d5ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -85,17 +85,17 @@ checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "bread-screenshots" version = "0.3.1" -source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=main#f86e299f4a0ea73ff485cd84923b986ddcc8242e" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?rev=69ce2d67a8de8a09c064aa9d7ff99b46656f0b1d#69ce2d67a8de8a09c064aa9d7ff99b46656f0b1d" dependencies = [ "anyhow", - "bread-utils", + "bread-utils 0.3.1 (git+https://git.breadway.dev/Breadway/bread-ecosystem?rev=69ce2d67a8de8a09c064aa9d7ff99b46656f0b1d)", "tracing", ] [[package]] name = "bread-theme" -version = "0.2.3" -source = "git+https://github.com/Breadway/bread-ecosystem?tag=v0.2.8#77417d552130281ff787e07d52541eb25e9d533b" +version = "0.3.1" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.1#db2fa3c4b4c1e6933bc5cf62a236d05972fdc886" dependencies = [ "dirs", "gtk4", @@ -106,7 +106,17 @@ dependencies = [ [[package]] name = "bread-utils" version = "0.3.1" -source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?branch=main#f86e299f4a0ea73ff485cd84923b986ddcc8242e" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.1#db2fa3c4b4c1e6933bc5cf62a236d05972fdc886" +dependencies = [ + "dirs", + "serde", + "serde_json", +] + +[[package]] +name = "bread-utils" +version = "0.3.1" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?rev=69ce2d67a8de8a09c064aa9d7ff99b46656f0b1d#69ce2d67a8de8a09c064aa9d7ff99b46656f0b1d" dependencies = [ "dirs", "serde", @@ -120,6 +130,7 @@ dependencies = [ "anyhow", "bread-screenshots", "bread-theme", + "bread-utils 0.3.1 (git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.1)", "breadbox-shared", "clap", "gtk4", @@ -480,7 +491,7 @@ dependencies = [ "gobject-sys", "libc", "system-deps", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] diff --git a/README.md b/README.md index 2b6ce13..b2d4f32 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ breadbox GTK4 layer-shell launcher - Reads the active Hyprland workspace and sorts apps by context priority - Fuzzy filtering as you type; Enter launches, Escape closes - App icons loaded from the resolved icon cache (see `breadbox-sync`) -- pywal palette auto-detected from `~/.cache/wal/colors.json`, falls back to Catppuccin Mocha +- pywal accents from `~/.cache/wal/colors.json`; background/surface/overlay/foreground stay fixed BOS dark - User CSS override at `~/.config/breadbox/style.css` - Toggle/dismiss: running a second instance kills the first diff --git a/breadbox/Cargo.toml b/breadbox/Cargo.toml index c2d859e..acc4146 100644 --- a/breadbox/Cargo.toml +++ b/breadbox/Cargo.toml @@ -9,9 +9,12 @@ name = "breadbox" path = "src/main.rs" [dependencies] -bread-theme = { git = "https://github.com/Breadway/bread-ecosystem", tag = "v0.2.8", features = ["gtk"] } +bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.1", features = ["gtk"] } +bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.1" } # Capture primitives for `--screenshot` mode — see src/screenshot.rs. -bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", branch = "main" } +# Not on tag v0.7.1 (crate landed after that tag); rev-pin so this is not +# `branch = "main"`. +bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", rev = "69ce2d67a8de8a09c064aa9d7ff99b46656f0b1d" } breadbox-shared = { path = "../breadbox-shared" } gtk4 = { version = "0.11", features = ["v4_12"] } gtk4-layer-shell = "0.8" diff --git a/breadbox/src/main.rs b/breadbox/src/main.rs index 785ccf9..2de4682 100644 --- a/breadbox/src/main.rs +++ b/breadbox/src/main.rs @@ -254,40 +254,6 @@ fn fuzzy_score(query: &str, entry: &DesktopEntry) -> u32 { 4 // subsequence match } -// ---- PID file toggle -------------------------------------------------------- - -fn pid_file() -> PathBuf { - env::var("XDG_RUNTIME_DIR") - .map(PathBuf::from) - .unwrap_or_else(|_| PathBuf::from("/tmp")) - .join("breadbox.pid") -} - -fn is_breadbox_pid(pid: u32) -> bool { - fs::read_to_string(format!("/proc/{}/comm", pid)) - .map(|s| s.trim() == "breadbox") - .unwrap_or(false) -} - -// Returns false if an existing instance was killed (caller should exit). -fn toggle_or_continue() -> bool { - let pf = pid_file(); - if let Ok(content) = fs::read_to_string(&pf) { - if let Ok(pid) = content.trim().parse::() { - if is_breadbox_pid(pid) { - let _ = Command::new("kill").arg(pid.to_string()).status(); - return false; - } - } - } - let _ = fs::write(&pf, std::process::id().to_string()); - true -} - -fn cleanup_pid() { - let _ = fs::remove_file(pid_file()); -} - // ---- UI --------------------------------------------------------------------- fn get_row_entry(row: >k4::ListBoxRow) -> Option { @@ -344,7 +310,6 @@ fn run_ui( let close_all: Rc = Rc::new({ let w = window.clone(); move || { - cleanup_pid(); w.close(); } }); @@ -551,8 +516,6 @@ fn run_ui( }); window.add_controller(outside_click); - window.connect_destroy(|_| cleanup_pid()); - if let Some(req) = screenshot_req.clone() { screenshot::dispatch(&window, req); } @@ -578,14 +541,27 @@ fn main() { let cli = screenshot::Cli::parse(); let screenshot_req = cli.screenshot_request(); - // The PID-file toggle kills whatever's holding the file — a real, - // already-running breadbox instance included. A screenshot run must + // `toggle_or_kill` kills whatever's holding the single-instance lock — + // a real, already-running breadbox included. A screenshot run must // never touch it: it's a separate, disposable instance by design (same // reasoning as breadbar's `allow_multiple_instances`), not a toggle of // the operator's real launcher. - if screenshot_req.is_none() && !toggle_or_continue() { - return; - } + // + // Kept alive for the rest of `main` — dropping it releases the + // single-instance lock and removes the pid file, which happens + // naturally once `run_ui` returns (after the window closes). + let _singleton_guard = if screenshot_req.is_some() { + None + } else { + match bread_utils::singleton::toggle_or_kill("breadbox") { + Ok(bread_utils::singleton::Toggle::Started(guard)) => Some(guard), + Ok(bread_utils::singleton::Toggle::KilledExisting) => return, + Err(e) => { + eprintln!("breadbox: single-instance lock unavailable ({e}); continuing without it"); + None + } + } + }; let config = Config::load(); let workspace = get_active_workspace().unwrap_or_default(); From e5ce91a9c9329b03939e88a58470b09f17c2bb83 Mon Sep 17 00:00:00 2001 From: Breadway Date: Sat, 15 Aug 2026 22:03:30 +0800 Subject: [PATCH 18/30] Track AGENTS.md --- .gitignore | 1 - AGENTS.md | 13 +++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 AGENTS.md diff --git a/.gitignore b/.gitignore index d6fe8f9..37a962f 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,3 @@ Thumbs.db .claude/ # Local hygiene notes (not for commit) -CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..578d722 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,13 @@ +# AGENTS.md — Repo hygiene + +Follow [`CONTRIBUTING.md`](CONTRIBUTING.md). Single-trunk: `main` plus short-lived `feature/` / `fix/` branches. `dev`/`beta` are bakery tracks (tags / main), not git branches. + +## Remotes +- `origin` — Forgejo (`git.breadway.dev`) — authoritative. +- `github` — mirror. Day-to-day push `origin` only. + +## Product +GTK4 app launcher + `breadbox-sync` icon cache. Theme via `bread-theme` (pin by tag on `git.breadway.dev`). Toggle uses `bread-utils::singleton`, not a homegrown PID file. + +## Distribution +Bakery (`bakery.toml`). Forgejo `.forgejo/workflows/` is canonical; do not re-add a GitHub Actions release workflow. From 1b02cec0a613fbf6f61608ab81f5d7a571ef0f6e Mon Sep 17 00:00:00 2001 From: Breadway Date: Sat, 15 Aug 2026 22:14:42 +0800 Subject: [PATCH 19/30] Emit bread.box.launched after a successful app launch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Link bread-utils bread-client (tag v0.7.1) from the breadbox crate. Fire-and-forget via BreadClient; silent if breadd is down. No command verbs — breadbox is not a daemon. EVENTS.md is the contract (app id box). --- AGENTS.md | 2 +- Cargo.lock | 12 +++++++++++ EVENTS.md | 41 ++++++++++++++++++++++++++++++++++++++ breadbox-shared/src/lib.rs | 10 ++++++++++ breadbox/Cargo.toml | 2 +- breadbox/src/main.rs | 33 +++++++++++++++++++++++++----- 6 files changed, 93 insertions(+), 7 deletions(-) create mode 100644 EVENTS.md diff --git a/AGENTS.md b/AGENTS.md index 578d722..a48505f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ Follow [`CONTRIBUTING.md`](CONTRIBUTING.md). Single-trunk: `main` plus short-liv - `github` — mirror. Day-to-day push `origin` only. ## Product -GTK4 app launcher + `breadbox-sync` icon cache. Theme via `bread-theme` (pin by tag on `git.breadway.dev`). Toggle uses `bread-utils::singleton`, not a homegrown PID file. +GTK4 app launcher + `breadbox-sync` icon cache. Theme via `bread-theme` (pin by tag on `git.breadway.dev`). Toggle uses `bread-utils::singleton`, not a homegrown PID file. `EVENTS.md` is the bread-event contract (app id `box`); emit `bread.box.launched` after a successful launch. No command verbs. ## Distribution Bakery (`bakery.toml`). Forgejo `.forgejo/workflows/` is canonical; do not re-add a GitHub Actions release workflow. diff --git a/Cargo.lock b/Cargo.lock index d05d5ff..142e3f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -92,6 +92,17 @@ dependencies = [ "tracing", ] +[[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.3.1" @@ -108,6 +119,7 @@ name = "bread-utils" version = "0.3.1" source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.1#db2fa3c4b4c1e6933bc5cf62a236d05972fdc886" dependencies = [ + "bread-shared", "dirs", "serde", "serde_json", diff --git a/EVENTS.md b/EVENTS.md new file mode 100644 index 0000000..6e76638 --- /dev/null +++ b/EVENTS.md @@ -0,0 +1,41 @@ +# breadbox — bread event integration + +breadbox is a standalone app launcher: it works exactly the same with or +without `breadd` running. When breadd *is* present, the GTK launcher +publishes a single event into the shared bread automation fabric after a +successful launch. 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: **`box`**. Transport: `bread-utils`'s `bread_client` module +(feature `bread-client`) — `breadbox` links it directly. breadbox is a +short-lived process (it exits when the launcher closes), so each `emit` +is its own short-lived connection. There is no long-running daemon and +therefore no command subscription. + +## Events published (`bread.box.*`) + +| Event | Data | When | +|-------|------|------| +| `bread.box.launched` | `{ "id": "", "name": "" }` | The user launched an app (Enter / keypad Enter on the selected row, or activating a row) **and** the spawn succeeded. Not emitted if `Command::spawn` fails (missing terminal, `exec` that cannot start). `id` is the desktop-file id (the `.desktop` filename, e.g. `firefox.desktop`), falling back to the stripped `Exec=` line when that id is empty. `name` is the desktop-entry display name. | + +Launch history is local to breadbox (`~/.cache/breadbox/history.json`); +the event bus is a notification that a launch happened, not a channel +for the exec line's arguments or the resulting process. + +## Commands honored (`bread.command.box.*`) + +None. breadbox is not a daemon — it is not running (and not subscribed) +except while the launcher overlay is open. There is no existing +"launch this desktop id from the bus" product surface, and inventing +`bread.command.box.launch` (or similar) without that surface would be a +stub. If/when breadbox grows a long-running piece that can honor a +verb, the corresponding `bread.command.box.*` command should be added +at the same time, not stubbed out ahead of it. + +## 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) — launching, + history, theming, and the singleton toggle are entirely unaffected. +- Closing the launcher without launching anything emits nothing. diff --git a/breadbox-shared/src/lib.rs b/breadbox-shared/src/lib.rs index 5286729..92c573b 100644 --- a/breadbox-shared/src/lib.rs +++ b/breadbox-shared/src/lib.rs @@ -54,6 +54,9 @@ pub fn app_dirs() -> Vec { #[derive(Debug, Clone)] pub struct DesktopEntry { + /// Desktop file id (the `.desktop` filename, e.g. `firefox.desktop`). + /// Empty only if the path had no file name; callers fall back to `exec`. + pub id: String, pub name: String, pub exec: String, pub icon_name: String, @@ -155,7 +158,14 @@ pub fn parse_desktop(path: &Path) -> Option { .map(|s| s.to_string()) .collect(); + let id = path + .file_name() + .map(|n| n.to_string_lossy().into_owned()) + .filter(|s| !s.is_empty()) + .unwrap_or_default(); + Some(DesktopEntry { + id, name, exec, icon_name, diff --git a/breadbox/Cargo.toml b/breadbox/Cargo.toml index acc4146..8846db8 100644 --- a/breadbox/Cargo.toml +++ b/breadbox/Cargo.toml @@ -10,7 +10,7 @@ path = "src/main.rs" [dependencies] bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.1", features = ["gtk"] } -bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.1" } +bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.1", features = ["bread-client"] } # Capture primitives for `--screenshot` mode — see src/screenshot.rs. # Not on tag v0.7.1 (crate landed after that tag); rev-pin so this is not # `branch = "main"`. diff --git a/breadbox/src/main.rs b/breadbox/src/main.rs index 2de4682..449b1ed 100644 --- a/breadbox/src/main.rs +++ b/breadbox/src/main.rs @@ -1,4 +1,5 @@ use bread_theme::{hex_to_rgba, ink_on, load_palette, Palette}; +use bread_utils::bread_client::BreadClient; use std::{ cell::RefCell, collections::HashMap, @@ -11,6 +12,10 @@ use std::{ rc::Rc, }; +/// This app's id in bread's sibling-app namespace registry +/// (`bread_shared::apps::KNOWN_APPS`) — events publish as `bread.box.*`. +const APP_ID: &str = "box"; + use breadbox_shared::{ config_dir, load_all_desktop_entries, Config, DesktopEntry, IconCache, LaunchHistory, }; @@ -206,24 +211,42 @@ fn pick_terminal() -> String { fn do_launch(entry: &DesktopEntry) { let cmd = entry.exec.trim(); - if entry.terminal { + let spawned = if entry.terminal { let term = pick_terminal(); - let _ = Command::new(&term) + Command::new(&term) .args(["-e", "bash", "-c", cmd]) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) - .spawn(); + .spawn() } else { - let _ = Command::new("bash") + Command::new("bash") .args(["-c", cmd]) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) - .spawn(); + .spawn() + }; + if spawned.is_ok() { + emit_launched(entry); } } +/// Publishes `bread.box.launched` after a successful spawn. Fire-and-forget +/// and non-fatal (`BreadClient::emit` never blocks or errors this caller) — +/// breadd being absent must never affect launching itself. +fn emit_launched(entry: &DesktopEntry) { + let id = if entry.id.is_empty() { + entry.exec.as_str() + } else { + entry.id.as_str() + }; + BreadClient::connect(APP_ID).emit( + "bread.box.launched", + serde_json::json!({ "id": id, "name": entry.name }), + ); +} + // ---- Fuzzy matching --------------------------------------------------------- fn fuzzy_matches(pattern: &str, text: &str) -> bool { From 2a157bdf2cc67ab14f7ef2d15ea3ee57602601d7 Mon Sep 17 00:00:00 2001 From: Breadway Date: Sat, 15 Aug 2026 22:32:37 +0800 Subject: [PATCH 20/30] gitignore: exclude graphify-out local cache --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 37a962f..ec10cb0 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,7 @@ Thumbs.db .claude/ # Local hygiene notes (not for commit) +CLAUDE.md + +# graphify knowledge-graph output (local tool cache, not for commit) +graphify-out/ From d42249a586e7f4aff8afd0ebd624428e8261d99d Mon Sep 17 00:00:00 2001 From: Breadway Date: Sat, 15 Aug 2026 22:53:46 +0800 Subject: [PATCH 21/30] Pin bread-ecosystem crates to v0.7.2 Repoint bread-theme, bread-utils, and bread-screenshots at tag v0.7.2 on git.breadway.dev. Drop the 69ce2d67 rev pin now that screenshots is on a tagged release. --- Cargo.lock | 26 ++++++++------------------ breadbox/Cargo.toml | 8 +++----- 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 142e3f8..eebe944 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,11 +84,11 @@ checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "bread-screenshots" -version = "0.3.1" -source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?rev=69ce2d67a8de8a09c064aa9d7ff99b46656f0b1d#69ce2d67a8de8a09c064aa9d7ff99b46656f0b1d" +version = "0.7.2" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.2#30517f161724132cdeb658c04cf5e490be07ee73" dependencies = [ "anyhow", - "bread-utils 0.3.1 (git+https://git.breadway.dev/Breadway/bread-ecosystem?rev=69ce2d67a8de8a09c064aa9d7ff99b46656f0b1d)", + "bread-utils", "tracing", ] @@ -105,8 +105,8 @@ dependencies = [ [[package]] name = "bread-theme" -version = "0.3.1" -source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.1#db2fa3c4b4c1e6933bc5cf62a236d05972fdc886" +version = "0.7.2" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.2#30517f161724132cdeb658c04cf5e490be07ee73" dependencies = [ "dirs", "gtk4", @@ -116,8 +116,8 @@ dependencies = [ [[package]] name = "bread-utils" -version = "0.3.1" -source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.1#db2fa3c4b4c1e6933bc5cf62a236d05972fdc886" +version = "0.7.2" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.2#30517f161724132cdeb658c04cf5e490be07ee73" dependencies = [ "bread-shared", "dirs", @@ -125,16 +125,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "bread-utils" -version = "0.3.1" -source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?rev=69ce2d67a8de8a09c064aa9d7ff99b46656f0b1d#69ce2d67a8de8a09c064aa9d7ff99b46656f0b1d" -dependencies = [ - "dirs", - "serde", - "serde_json", -] - [[package]] name = "breadbox" version = "0.2.4" @@ -142,7 +132,7 @@ dependencies = [ "anyhow", "bread-screenshots", "bread-theme", - "bread-utils 0.3.1 (git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.1)", + "bread-utils", "breadbox-shared", "clap", "gtk4", diff --git a/breadbox/Cargo.toml b/breadbox/Cargo.toml index 8846db8..d5849bc 100644 --- a/breadbox/Cargo.toml +++ b/breadbox/Cargo.toml @@ -9,12 +9,10 @@ name = "breadbox" path = "src/main.rs" [dependencies] -bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.1", features = ["gtk"] } -bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.1", features = ["bread-client"] } +bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2", features = ["gtk"] } +bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2", features = ["bread-client"] } # Capture primitives for `--screenshot` mode — see src/screenshot.rs. -# Not on tag v0.7.1 (crate landed after that tag); rev-pin so this is not -# `branch = "main"`. -bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", rev = "69ce2d67a8de8a09c064aa9d7ff99b46656f0b1d" } +bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2" } breadbox-shared = { path = "../breadbox-shared" } gtk4 = { version = "0.11", features = ["v4_12"] } gtk4-layer-shell = "0.8" From fbd73c198410a3e3fb1c6ba5baf2ccdad97b6f0f Mon Sep 17 00:00:00 2001 From: Breadway Date: Sat, 15 Aug 2026 23:05:47 +0800 Subject: [PATCH 22/30] Bump version to v0.3.1 --- Cargo.lock | 6 +++--- breadbox-shared/Cargo.toml | 2 +- breadbox-sync/Cargo.toml | 2 +- breadbox/Cargo.toml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eebe944..e1b3550 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -127,7 +127,7 @@ dependencies = [ [[package]] name = "breadbox" -version = "0.2.4" +version = "0.3.1" dependencies = [ "anyhow", "bread-screenshots", @@ -142,7 +142,7 @@ dependencies = [ [[package]] name = "breadbox-shared" -version = "0.2.4" +version = "0.3.1" dependencies = [ "serde", "serde_json", @@ -151,7 +151,7 @@ dependencies = [ [[package]] name = "breadbox-sync" -version = "0.2.4" +version = "0.3.1" dependencies = [ "breadbox-shared", "serde_json", diff --git a/breadbox-shared/Cargo.toml b/breadbox-shared/Cargo.toml index ec62ed1..3bb9e54 100644 --- a/breadbox-shared/Cargo.toml +++ b/breadbox-shared/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "breadbox-shared" -version = "0.2.4" +version = "0.3.1" edition = "2021" license = "MIT" diff --git a/breadbox-sync/Cargo.toml b/breadbox-sync/Cargo.toml index bdcab16..0d25925 100644 --- a/breadbox-sync/Cargo.toml +++ b/breadbox-sync/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "breadbox-sync" -version = "0.2.4" +version = "0.3.1" edition = "2021" license = "MIT" diff --git a/breadbox/Cargo.toml b/breadbox/Cargo.toml index d5849bc..9c40453 100644 --- a/breadbox/Cargo.toml +++ b/breadbox/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "breadbox" -version = "0.2.4" +version = "0.3.1" edition = "2021" license = "MIT" From 4d0c0288b7edd677517944ec335b967ad2b6fb3b Mon Sep 17 00:00:00 2001 From: Breadway Date: Sat, 15 Aug 2026 23:11:28 +0800 Subject: [PATCH 23/30] Honor bread.command.box.open via breadbox listen --- AGENTS.md | 2 +- EVENTS.md | 48 ++++++++++++++++++------ breadbox/src/listen.rs | 84 ++++++++++++++++++++++++++++++++++++++++++ breadbox/src/main.rs | 6 +++ 4 files changed, 127 insertions(+), 13 deletions(-) create mode 100644 breadbox/src/listen.rs diff --git a/AGENTS.md b/AGENTS.md index a48505f..6911f08 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ Follow [`CONTRIBUTING.md`](CONTRIBUTING.md). Single-trunk: `main` plus short-liv - `github` — mirror. Day-to-day push `origin` only. ## Product -GTK4 app launcher + `breadbox-sync` icon cache. Theme via `bread-theme` (pin by tag on `git.breadway.dev`). Toggle uses `bread-utils::singleton`, not a homegrown PID file. `EVENTS.md` is the bread-event contract (app id `box`); emit `bread.box.launched` after a successful launch. No command verbs. +GTK4 app launcher + `breadbox-sync` icon cache. Theme via `bread-theme` (pin by tag on `git.breadway.dev`). Toggle uses `bread-utils::singleton`, not a homegrown PID file. `EVENTS.md` is the bread-event contract (app id `box`); emit `bread.box.launched` after a successful launch. `breadbox listen` honors `bread.command.box.open`. ## Distribution Bakery (`bakery.toml`). Forgejo `.forgejo/workflows/` is canonical; do not re-add a GitHub Actions release workflow. diff --git a/EVENTS.md b/EVENTS.md index 6e76638..3e7064a 100644 --- a/EVENTS.md +++ b/EVENTS.md @@ -8,16 +8,19 @@ specifically its "Namespaces" and "Integrating a bread\* app" sections — for the general convention this follows. App id: **`box`**. Transport: `bread-utils`'s `bread_client` module -(feature `bread-client`) — `breadbox` links it directly. breadbox is a -short-lived process (it exits when the launcher closes), so each `emit` -is its own short-lived connection. There is no long-running daemon and -therefore no command subscription. +(feature `bread-client`) — `breadbox` links it directly. One-shot +launcher invocations each `emit` on their own fire-and-forget +connection. Command verbs are only received while `breadbox listen` is +running — that process holds the `bread.command.box.**` subscription +open. ## Events published (`bread.box.*`) | Event | Data | When | |-------|------|------| | `bread.box.launched` | `{ "id": "", "name": "" }` | The user launched an app (Enter / keypad Enter on the selected row, or activating a row) **and** the spawn succeeded. Not emitted if `Command::spawn` fails (missing terminal, `exec` that cannot start). `id` is the desktop-file id (the `.desktop` filename, e.g. `firefox.desktop`), falling back to the stripped `Exec=` line when that id is empty. `name` is the desktop-entry display name. | +| `bread.box.open.done` | `{}` | `bread.command.box.open` was received and `breadbox` was spawned. This is the command confirmation, not proof the overlay mapped — the spawned process is the same toggle as a keybind. | +| `bread.box.open.failed` | `{ "error": "" }` | `bread.command.box.open` was received but this binary could not be started. | Launch history is local to breadbox (`~/.cache/breadbox/history.json`); the event bus is a notification that a launch happened, not a channel @@ -25,17 +28,38 @@ for the exec line's arguments or the resulting process. ## Commands honored (`bread.command.box.*`) -None. breadbox is not a daemon — it is not running (and not subscribed) -except while the launcher overlay is open. There is no existing -"launch this desktop id from the bus" product surface, and inventing -`bread.command.box.launch` (or similar) without that surface would be a -stub. If/when breadbox grows a long-running piece that can honor a -verb, the corresponding `bread.command.box.*` command should be added -at the same time, not stubbed out ahead of it. +These are only received while `breadbox listen` is running. Publishing a +command with no subscriber is a silent no-op — that is the documented +bread convention, not a breadbox bug. + +| Verb | Data | Effect | +|------|------|--------| +| `open` | none | Same as running `breadbox` (toggle the launcher overlay via the existing singleton). Emits `bread.box.open.done` / `.failed`. | + +```lua +bread.spawn(function() + bread.emit("bread.command.box.open") + bread.wait("bread.box.open.done", { timeout = 5000 }) +end) +``` + +### Not implemented: extra verbs + +There is no `launch` / `close` / `query` command verb. Picking a desktop +id from the bus would be a new product surface. If/when that exists, add +the corresponding `bread.command.box.*` verb at the same time, not +stubbed as a no-op ahead of it. ## 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) — launching, + (`BreadClient::emit` never blocks or errors the caller) and the + command subscription simply never receives anything — launching, history, theming, and the singleton toggle are entirely unaffected. +- If breadd restarts, the command subscription reconnects automatically + (`BreadClient::subscribe`'s background thread has its own backoff + loop); no restart of `breadbox listen` is needed. +- If `breadbox listen` is not running, commands are a graceful no-op at + the bus (no subscriber). The CLI still works, and one-shot invocations + still emit `bread.box.launched` on their own short-lived connection. - Closing the launcher without launching anything emits nothing. diff --git a/breadbox/src/listen.rs b/breadbox/src/listen.rs new file mode 100644 index 0000000..9aeb367 --- /dev/null +++ b/breadbox/src/listen.rs @@ -0,0 +1,84 @@ +//! Long-running command subscription for `bread.command.box.*`. +//! +//! `breadbox` is still a one-shot toggle overlay by default. `breadbox listen` +//! is the optional persistent process that can honor bus commands. See +//! `EVENTS.md`. + +use bread_utils::bread_client::{BreadClient, BreadEvent}; + +/// Sibling-app id in `bread_shared::apps::KNOWN_APPS`. +const APP_ID: &str = "box"; + +/// Subscribe to `bread.command.box.**` and block until the process is killed. +/// +/// breadd being absent is not an error: [`BreadClient::subscribe`] reconnects +/// with backoff, and `on_event` simply isn't called until the daemon is up. +pub fn run() { + let client = BreadClient::connect(APP_ID); + if client.health().is_none() { + eprintln!("breadbox: breadd unreachable; command subscription will connect when it comes back"); + } + + let _commands = client.subscribe("bread.command.box.**", |event| { + handle_command(&event); + }); + + eprintln!("breadbox: listening for bread.command.box.**"); + loop { + std::thread::park(); + } +} + +/// Reacts to `bread.command.box.*` verbs. Only `open` is honored today — +/// other verbs are ignored, not stubbed as no-ops that pretend to succeed. +fn handle_command(event: &BreadEvent) { + let Some(verb) = command_verb(&event.event) else { + return; + }; + match verb { + "open" => handle_open(), + other => { + eprintln!("breadbox: ignoring unrecognized bread.command.box.{other}"); + } + } +} + +fn handle_open() { + // Same as running `breadbox` from a keybind: toggle the overlay via the + // existing singleton. Spawn success is the command confirmation — we do + // not wait for the GTK window to map. + let result = spawn_self(); + let client = BreadClient::connect(APP_ID); + match result { + Ok(_) => client.emit("bread.box.open.done", serde_json::json!({})), + Err(e) => { + eprintln!("breadbox: bread.command.box.open failed: {e}"); + client.emit( + "bread.box.open.failed", + serde_json::json!({ "error": e.to_string() }), + ); + } + } +} + +fn spawn_self() -> std::io::Result { + let exe = std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("breadbox")); + std::process::Command::new(exe).spawn() +} + +fn command_verb(event_name: &str) -> Option<&str> { + event_name.strip_prefix("bread.command.box.") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn command_verb_strips_box_prefix() { + assert_eq!(command_verb("bread.command.box.open"), Some("open")); + assert_eq!(command_verb("bread.command.box.launch"), Some("launch")); + assert_eq!(command_verb("bread.command.clip.clear"), None); + assert_eq!(command_verb("bread.box.launched"), None); + } +} diff --git a/breadbox/src/main.rs b/breadbox/src/main.rs index 449b1ed..b207b97 100644 --- a/breadbox/src/main.rs +++ b/breadbox/src/main.rs @@ -28,6 +28,7 @@ use gtk4::{ }; use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell}; +mod listen; mod screenshot; // ---- Hyprland IPC ----------------------------------------------------------- @@ -560,6 +561,11 @@ fn run_ui( // ---- Main ------------------------------------------------------------------- fn main() { + if std::env::args().nth(1).as_deref() == Some("listen") { + listen::run(); + return; + } + use clap::Parser; let cli = screenshot::Cli::parse(); let screenshot_req = cli.screenshot_request(); From c359fd74be34806a4c230ec2c9d81c51c789ba39 Mon Sep 17 00:00:00 2001 From: Breadway Date: Sun, 16 Aug 2026 00:26:06 +0800 Subject: [PATCH 24/30] Adopt bread_utils::screenshot_cli for --screenshot flags Replace the local settle delay, canvas defaults, and pair-validation error path with bread-utils v0.7.2. Clap parsing stays in-tree. --- breadbox/src/screenshot.rs | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/breadbox/src/screenshot.rs b/breadbox/src/screenshot.rs index 59104ee..a1b4edc 100644 --- a/breadbox/src/screenshot.rs +++ b/breadbox/src/screenshot.rs @@ -9,15 +9,10 @@ //! capture is the whole known-size canvas, not a hand-tracked panel //! geometry. +use bread_utils::screenshot_cli::{validate_pair, DEFAULT_HEIGHT, DEFAULT_WIDTH, SETTLE_DELAY}; use clap::Parser; use gtk4::prelude::*; use std::path::PathBuf; -use std::time::Duration; - -/// Extra settle time after `map` for the first frame to actually paint -/// before grim runs — `map` fires once the surface exists, not once -/// anything has been drawn into it. -const SETTLE_DELAY: Duration = Duration::from_millis(300); #[derive(Parser)] #[command(name = "breadbox")] @@ -33,11 +28,11 @@ pub struct Cli { /// Capture canvas width — matches the isolated compositor's output width /// (`bread-capture --isolate-width`). - #[arg(long, default_value_t = 1920)] + #[arg(long, default_value_t = DEFAULT_WIDTH)] pub width: u32, /// Capture canvas height — see `width`. - #[arg(long, default_value_t = 1080)] + #[arg(long, default_value_t = DEFAULT_HEIGHT)] pub height: u32, } @@ -50,16 +45,20 @@ pub struct ScreenshotRequest { } impl Cli { - /// `None` for a normal run. Exits the process with an error if - /// `--screenshot` was given without `--output`, before any GTK setup + /// `None` for a normal run. Exits the process with an error if the + /// `--screenshot` / `--output` pair is incomplete, before any GTK setup /// happens. pub fn screenshot_request(&self) -> Option { - let view = self.screenshot.clone()?; - let Some(output) = self.output.clone() else { - eprintln!("breadbox: --screenshot requires --output"); + if let Err(e) = validate_pair(self.screenshot.as_deref(), self.output.as_deref()) { + eprintln!("breadbox: {e}"); std::process::exit(1); - }; - Some(ScreenshotRequest { view, output, width: self.width, height: self.height }) + } + Some(ScreenshotRequest { + view: self.screenshot.clone()?, + output: self.output.clone()?, + width: self.width, + height: self.height, + }) } } From 6330ad195157db3e232c8474b4e9cd77d25aa13f Mon Sep 17 00:00:00 2001 From: Breadway Date: Sun, 16 Aug 2026 00:50:22 +0800 Subject: [PATCH 25/30] CI: refuse unsigned bakery index on stable tag releases --- .forgejo/workflows/release.yml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 2d08cd2..8f8c6dd 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -17,7 +17,16 @@ jobs: "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src - name: build - run: cd src && bash ci/build.sh cargo build --release --locked + 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 + } - name: prepare artifacts run: | @@ -38,8 +47,14 @@ jobs: ln -sfn "${VERSION}" "/srv/breadway-dl/breadbox/latest" - name: regenerate index.json + env: + MINISIGN_SEC_KEY: ${{ secrets.BAKERY_MINISIGN_SEC_KEY_PATH }} run: | set -euo pipefail + if [ -z "${MINISIGN_SEC_KEY:-}" ]; then + echo "::error::BAKERY_MINISIGN_SEC_KEY_PATH secret not set — refusing to regenerate index.json unsigned (would leave a stale signature mismatched against fresh content and break bakery for everyone)" + exit 1 + fi rm -rf /tmp/bread-ecosystem-ci git clone https://git.breadway.dev/Breadway/bread-ecosystem.git /tmp/bread-ecosystem-ci bash /tmp/bread-ecosystem-ci/scripts/gen-index.sh From 06015b262795eeb4031b71d231b4fa3ee93a2eb7 Mon Sep 17 00:00:00 2001 From: Breadway Date: Sun, 16 Aug 2026 13:25:53 +0800 Subject: [PATCH 26/30] Bind the launcher to the current monitor's bread-theme palette Pin bread-theme to v0.7.4. --- Cargo.lock | 355 ++++++++++++++++++++++--------------------- breadbox/Cargo.toml | 2 +- breadbox/src/main.rs | 1 + 3 files changed, 185 insertions(+), 173 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e1b3550..12ae2fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,9 +78,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.13.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "bread-screenshots" @@ -105,8 +105,8 @@ dependencies = [ [[package]] name = "bread-theme" -version = "0.7.2" -source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.2#30517f161724132cdeb658c04cf5e490be07ee73" +version = "0.7.4" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.4#fcba3760387e2523edb71350f8efea3bc851b21e" dependencies = [ "dirs", "gtk4", @@ -178,14 +178,14 @@ checksum = "f8b4985713047f5faee02b8db6a6ef32bbb50269ff53c1aee716d1d195b76d54" dependencies = [ "glib-sys", "libc", - "system-deps", + "system-deps 7.0.8", ] [[package]] name = "cc" -version = "1.2.63" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" dependencies = [ "find-msvc-tools", "shlex", @@ -209,9 +209,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "clap" -version = "4.6.4" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" dependencies = [ "clap_builder", "clap_derive", @@ -219,9 +219,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.2" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" dependencies = [ "anstream", "anstyle", @@ -285,13 +285,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] @@ -312,9 +312,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" [[package]] name = "flate2" @@ -337,24 +337,24 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" dependencies = [ "futures-core", "futures-task", @@ -363,32 +363,32 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-core", "futures-macro", @@ -419,14 +419,14 @@ dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps", + "system-deps 7.0.8", ] [[package]] name = "gdk4" -version = "0.11.2" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd42fdbbf48612c6e8f47c65fb92d2e8f39c25aecd6af047e83897c1a22d2a4e" +checksum = "d81e2a6c6ecba2aab60633a98df1868b03fa0bfdce8105edc27c1bccf71f0e39" dependencies = [ "cairo-rs", "gdk-pixbuf", @@ -440,9 +440,9 @@ dependencies = [ [[package]] name = "gdk4-sys" -version = "0.11.2" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d974ac4f15e67472c3a9728daf612590b4a5762a4b33f0edd298df0b80d043c" +checksum = "3d8f608d8d7d229975c4d0d026f5d3071598c4ddab3c5262b0a31840fec78d13" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -452,7 +452,7 @@ dependencies = [ "libc", "pango-sys", "pkg-config", - "system-deps", + "system-deps 7.0.8", ] [[package]] @@ -468,9 +468,9 @@ dependencies = [ [[package]] name = "gio" -version = "0.22.6" +version = "0.22.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3848bcba3a35cc0a71df8ba8ecfd799d6bfb862342a53a4a915fb62213aa4e6" +checksum = "8b3e1f669909c326b9413bde5a742097b8c90a7d78f45326db13668984769ded" dependencies = [ "futures-channel", "futures-core", @@ -485,15 +485,15 @@ dependencies = [ [[package]] name = "gio-sys" -version = "0.22.0" +version = "0.22.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64729ba2772c080448f9f966dba8f4456beeb100d8c28a865ef8a0f2ef4987e1" +checksum = "353fdc7da7cd16da916104b1e0e4e7de380ec9c8aaa20d4d742d66310ab4b0d5" dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps", - "windows-sys 0.52.0", + "system-deps 7.0.8", + "windows-sys 0.61.2", ] [[package]] @@ -518,9 +518,9 @@ dependencies = [ [[package]] name = "glib" -version = "0.22.7" +version = "0.22.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c207e04e51605dcf7b2924c41591b3a10e1438eaac5bcf448fb91f325381104a" +checksum = "ddbcf514bd1881fc1b960e4e52b4e82873f4da3bceddbd58d42827b508888100" dependencies = [ "bitflags", "futures-channel", @@ -546,17 +546,17 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "glib-sys" -version = "0.22.6" +version = "0.22.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7fbac234ed5bc2a28359b7bde8e1b9cdf1441cc2d7f068e4824672d7db9445" +checksum = "030967459f9f676851872c6304adea7825c6d462ec9b72554c733cf0c5952233" dependencies = [ "libc", - "system-deps", + "system-deps 7.0.8", ] [[package]] @@ -567,37 +567,35 @@ checksum = "22a861859b887a79cf461359c192c97a57d8fb0229dd291232e57aa11f6fa72c" dependencies = [ "glib-sys", "libc", - "system-deps", + "system-deps 7.0.8", ] [[package]] name = "graphene-rs" -version = "0.22.0" +version = "0.22.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7d1b7881f96869f49808b6adfe906a93a57a34204952253444d68c3208d71f1" +checksum = "eb856b9c558971c3f13ab692358926da710b046932a4e087aedcc35b040d7dff" dependencies = [ "glib", "graphene-sys", - "libc", ] [[package]] name = "graphene-sys" -version = "0.22.0" +version = "0.22.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "517f062f3fd6b7fd3e57a3f038a74b3c23ca32f51199ff028aa704609943f79c" +checksum = "5c7ffdfde88f3570d3705e0d8a2433e036d387a1f2930bbf47eafcb5f569fd04" dependencies = [ "glib-sys", "libc", - "pkg-config", - "system-deps", + "system-deps 7.0.8", ] [[package]] name = "gsk4" -version = "0.11.1" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c912dfcbd28acace5fc99c40bb9f25e1dcb73efb1f2608327f66a99acdcb62" +checksum = "b867be1c5f14dcb8f552c0eff6e9a9b1da5f8b43943e8efc3a63c889d84952ff" dependencies = [ "cairo-rs", "gdk4", @@ -610,9 +608,9 @@ dependencies = [ [[package]] name = "gsk4-sys" -version = "0.11.1" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7d54bbc7a9d8b6ffe4f0c95eede15ccfb365c8bf521275abe6bcfb57b18fb8a" +checksum = "5b7c7eb2e681ee896646cfb8872b431f24d09f53ba9283289d9b10caa6707088" dependencies = [ "cairo-sys-rs", "gdk4-sys", @@ -621,14 +619,14 @@ dependencies = [ "graphene-sys", "libc", "pango-sys", - "system-deps", + "system-deps 7.0.8", ] [[package]] name = "gtk4" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7181b837f04cbe93f79441475f7a00560a92cba7a72e38cc1a68b6f8b78eaae2" +checksum = "98a0a0466484f64b07b5b8184d43fa46be78eb0b8e04ae4e179af31d770b76d9" dependencies = [ "cairo-rs", "field-offset", @@ -647,9 +645,9 @@ dependencies = [ [[package]] name = "gtk4-layer-shell" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4069987ff4793699511a251028cc336b438e46565b463f111250148d574752a" +checksum = "17c28ea0f4676fdaaae7ff2413a24d0d35c8657424f84856c1103c73454c9da4" dependencies = [ "bitflags", "gdk4", @@ -662,34 +660,34 @@ dependencies = [ [[package]] name = "gtk4-layer-shell-sys" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f566a5ec5bcc454e7fcf2ab76930887ced5365afce12c1e5201bb296b95f1b9" +checksum = "bcf19bb884ef0ef55b9e6b2b369c39b4fcc0c41e3a0c1cbc8c267720338b690b" dependencies = [ "gdk4-sys", "glib-sys", "gtk4-sys", "libc", - "system-deps", + "system-deps 8.0.0", ] [[package]] name = "gtk4-macros" -version = "0.11.0" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3581b242ba62fdff122ebb626ea641582ec326031622bd19d60f85029c804a87" +checksum = "5ac7179400a36a04de039c24206bb841c5596992b907b43b23ee8d5bdc40d00e" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "gtk4-sys" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20ba8e695e2640455561274e65e45f0a151619e450746007667f4b23ceae4e1b" +checksum = "82b8f954786af0b1984425c4446b77f5ff6594346181316be3f850caab1c6f01" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -701,7 +699,7 @@ dependencies = [ "gsk4-sys", "libc", "pango-sys", - "system-deps", + "system-deps 7.0.8", ] [[package]] @@ -718,9 +716,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "icu_collections" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" dependencies = [ "displaydoc", "potential_utf", @@ -732,9 +730,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" dependencies = [ "displaydoc", "litemap", @@ -745,9 +743,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -759,16 +757,17 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" [[package]] name = "icu_properties" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" dependencies = [ + "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", @@ -779,15 +778,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" [[package]] name = "icu_provider" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +checksum = "92a7ed671a6aad807a8651a2e1782a6598fda9ce5185dd8158549e95a91c6428" dependencies = [ "displaydoc", "icu_locale_core", @@ -849,36 +848,36 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "libc" -version = "0.2.186" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libredox" -version = "0.1.17" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +checksum = "28d0a00925a9f930d679b6789b721e3a7f9ed110f41b86d2497caa780c3a070a" dependencies = [ "libc", ] [[package]] name = "litemap" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" [[package]] name = "log" -version = "0.4.32" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "memchr" -version = "2.8.1" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memoffset" @@ -919,13 +918,12 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "pango" -version = "0.22.6" +version = "0.22.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "251bdc6e6487b811be0e406a21e301e07e45c0aa8fa39e00c0c8e12a91752438" +checksum = "5d800d8d0de2ad5d0fb046f5344dbaba14a003cf3dd27cc21d85893d35ea316c" dependencies = [ "gio", "glib", - "libc", "pango-sys", ] @@ -938,7 +936,7 @@ dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps", + "system-deps 7.0.8", ] [[package]] @@ -955,15 +953,15 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkg-config" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" [[package]] name = "potential_utf" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" dependencies = [ "zerovec", ] @@ -974,23 +972,23 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.25.12+spec-1.1.0", + "toml_edit 0.25.13+spec-1.1.0", ] [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.45" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -1031,9 +1029,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ "log", "once_cell", @@ -1046,18 +1044,18 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.1" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a" dependencies = [ "ring", "rustls-pki-types", @@ -1072,9 +1070,9 @@ checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -1082,29 +1080,29 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -1139,9 +1137,9 @@ checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "simd-adler32" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" [[package]] name = "slab" @@ -1151,9 +1149,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "stable_deref_trait" @@ -1175,9 +1173,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.117" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" dependencies = [ "proc-macro2", "quote", @@ -1203,7 +1201,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -1215,7 +1213,20 @@ dependencies = [ "cfg-expr", "heck", "pkg-config", - "toml 1.1.2+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", + "version-compare", +] + +[[package]] +name = "system-deps" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83779a5c956bcb6ba627a4ecf0a9d7625db47d7537e0892d97f712ac995648a3" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml 1.1.4+spec-1.1.0", "version-compare", ] @@ -1242,14 +1253,14 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "tinystr" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" dependencies = [ "displaydoc", "zerovec", @@ -1269,9 +1280,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.2+spec-1.1.0" +version = "1.1.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" dependencies = [ "indexmap", "serde_core", @@ -1279,7 +1290,7 @@ dependencies = [ "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "toml_writer", - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] @@ -1316,23 +1327,23 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.12+spec-1.1.0" +version = "0.25.13+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" dependencies = [ "indexmap", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] name = "toml_parser" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] @@ -1343,9 +1354,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "toml_writer" -version = "1.1.1+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "tracing" @@ -1366,7 +1377,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -1448,14 +1459,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.7", + "webpki-roots 1.0.9", ] [[package]] name = "webpki-roots" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" dependencies = [ "rustls-pki-types", ] @@ -1625,24 +1636,24 @@ dependencies = [ [[package]] name = "winnow" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" dependencies = [ "memchr", ] [[package]] name = "writeable" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "xml-rs" -version = "0.8.28" +version = "0.8.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" +checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" [[package]] name = "yoke" @@ -1663,7 +1674,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", "synstructure", ] @@ -1684,21 +1695,21 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerotrie" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" dependencies = [ "displaydoc", "yoke", @@ -1707,9 +1718,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.6" +version = "0.11.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +checksum = "94b5c6b5976d66c1d703c4fd17d3f5e43c8cedaacf604961b171adc7130896d8" dependencies = [ "yoke", "zerofrom", @@ -1718,17 +1729,17 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +checksum = "47402523226a02bfe5230160dc3ccc089aa6f6f19e7fcbb4e6f824bbb1b4aa62" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/breadbox/Cargo.toml b/breadbox/Cargo.toml index 9c40453..c1a40cc 100644 --- a/breadbox/Cargo.toml +++ b/breadbox/Cargo.toml @@ -9,7 +9,7 @@ name = "breadbox" path = "src/main.rs" [dependencies] -bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2", features = ["gtk"] } +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"] } # Capture primitives for `--screenshot` mode — see src/screenshot.rs. bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2" } diff --git a/breadbox/src/main.rs b/breadbox/src/main.rs index b207b97..3570852 100644 --- a/breadbox/src/main.rs +++ b/breadbox/src/main.rs @@ -330,6 +330,7 @@ fn run_ui( window.set_anchor(edge, true); } window.set_exclusive_zone(0); + bread_theme::gtk::bind_window_auto(&window); let close_all: Rc = Rc::new({ let w = window.clone(); From b9abafe5724a3d03110675289f1be5e46a08930a Mon Sep 17 00:00:00 2001 From: Breadway Date: Sun, 16 Aug 2026 14:09:13 +0800 Subject: [PATCH 27/30] Bump version to v0.3.3 --- Cargo.lock | 6 +++--- breadbox-shared/Cargo.toml | 2 +- breadbox-sync/Cargo.toml | 2 +- breadbox/Cargo.toml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 12ae2fd..c9780a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -127,7 +127,7 @@ dependencies = [ [[package]] name = "breadbox" -version = "0.3.1" +version = "0.3.3" dependencies = [ "anyhow", "bread-screenshots", @@ -142,7 +142,7 @@ dependencies = [ [[package]] name = "breadbox-shared" -version = "0.3.1" +version = "0.3.3" dependencies = [ "serde", "serde_json", @@ -151,7 +151,7 @@ dependencies = [ [[package]] name = "breadbox-sync" -version = "0.3.1" +version = "0.3.3" dependencies = [ "breadbox-shared", "serde_json", diff --git a/breadbox-shared/Cargo.toml b/breadbox-shared/Cargo.toml index 3bb9e54..2c583be 100644 --- a/breadbox-shared/Cargo.toml +++ b/breadbox-shared/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "breadbox-shared" -version = "0.3.1" +version = "0.3.3" edition = "2021" license = "MIT" diff --git a/breadbox-sync/Cargo.toml b/breadbox-sync/Cargo.toml index 0d25925..218db09 100644 --- a/breadbox-sync/Cargo.toml +++ b/breadbox-sync/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "breadbox-sync" -version = "0.3.1" +version = "0.3.3" edition = "2021" license = "MIT" diff --git a/breadbox/Cargo.toml b/breadbox/Cargo.toml index c1a40cc..0977c1e 100644 --- a/breadbox/Cargo.toml +++ b/breadbox/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "breadbox" -version = "0.3.1" +version = "0.3.3" edition = "2021" license = "MIT" From 28ff4bc98298e900572f1a5031a9a39cd9f71019 Mon Sep 17 00:00:00 2001 From: Breadway Date: Sun, 16 Aug 2026 14:29:24 +0800 Subject: [PATCH 28/30] can't be bothered writing a commit message --- Cargo.lock | 355 ++++++++++++++++++++++--------------------- breadbox/Cargo.toml | 2 +- breadbox/src/main.rs | 318 ++++++++++++++++++++++++++++++-------- 3 files changed, 441 insertions(+), 234 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e1b3550..12ae2fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,9 +78,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.13.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "bread-screenshots" @@ -105,8 +105,8 @@ dependencies = [ [[package]] name = "bread-theme" -version = "0.7.2" -source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.2#30517f161724132cdeb658c04cf5e490be07ee73" +version = "0.7.4" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.4#fcba3760387e2523edb71350f8efea3bc851b21e" dependencies = [ "dirs", "gtk4", @@ -178,14 +178,14 @@ checksum = "f8b4985713047f5faee02b8db6a6ef32bbb50269ff53c1aee716d1d195b76d54" dependencies = [ "glib-sys", "libc", - "system-deps", + "system-deps 7.0.8", ] [[package]] name = "cc" -version = "1.2.63" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" dependencies = [ "find-msvc-tools", "shlex", @@ -209,9 +209,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "clap" -version = "4.6.4" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" dependencies = [ "clap_builder", "clap_derive", @@ -219,9 +219,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.2" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" dependencies = [ "anstream", "anstyle", @@ -285,13 +285,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] @@ -312,9 +312,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" [[package]] name = "flate2" @@ -337,24 +337,24 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" dependencies = [ "futures-core", "futures-task", @@ -363,32 +363,32 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-core", "futures-macro", @@ -419,14 +419,14 @@ dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps", + "system-deps 7.0.8", ] [[package]] name = "gdk4" -version = "0.11.2" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd42fdbbf48612c6e8f47c65fb92d2e8f39c25aecd6af047e83897c1a22d2a4e" +checksum = "d81e2a6c6ecba2aab60633a98df1868b03fa0bfdce8105edc27c1bccf71f0e39" dependencies = [ "cairo-rs", "gdk-pixbuf", @@ -440,9 +440,9 @@ dependencies = [ [[package]] name = "gdk4-sys" -version = "0.11.2" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d974ac4f15e67472c3a9728daf612590b4a5762a4b33f0edd298df0b80d043c" +checksum = "3d8f608d8d7d229975c4d0d026f5d3071598c4ddab3c5262b0a31840fec78d13" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -452,7 +452,7 @@ dependencies = [ "libc", "pango-sys", "pkg-config", - "system-deps", + "system-deps 7.0.8", ] [[package]] @@ -468,9 +468,9 @@ dependencies = [ [[package]] name = "gio" -version = "0.22.6" +version = "0.22.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3848bcba3a35cc0a71df8ba8ecfd799d6bfb862342a53a4a915fb62213aa4e6" +checksum = "8b3e1f669909c326b9413bde5a742097b8c90a7d78f45326db13668984769ded" dependencies = [ "futures-channel", "futures-core", @@ -485,15 +485,15 @@ dependencies = [ [[package]] name = "gio-sys" -version = "0.22.0" +version = "0.22.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64729ba2772c080448f9f966dba8f4456beeb100d8c28a865ef8a0f2ef4987e1" +checksum = "353fdc7da7cd16da916104b1e0e4e7de380ec9c8aaa20d4d742d66310ab4b0d5" dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps", - "windows-sys 0.52.0", + "system-deps 7.0.8", + "windows-sys 0.61.2", ] [[package]] @@ -518,9 +518,9 @@ dependencies = [ [[package]] name = "glib" -version = "0.22.7" +version = "0.22.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c207e04e51605dcf7b2924c41591b3a10e1438eaac5bcf448fb91f325381104a" +checksum = "ddbcf514bd1881fc1b960e4e52b4e82873f4da3bceddbd58d42827b508888100" dependencies = [ "bitflags", "futures-channel", @@ -546,17 +546,17 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "glib-sys" -version = "0.22.6" +version = "0.22.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7fbac234ed5bc2a28359b7bde8e1b9cdf1441cc2d7f068e4824672d7db9445" +checksum = "030967459f9f676851872c6304adea7825c6d462ec9b72554c733cf0c5952233" dependencies = [ "libc", - "system-deps", + "system-deps 7.0.8", ] [[package]] @@ -567,37 +567,35 @@ checksum = "22a861859b887a79cf461359c192c97a57d8fb0229dd291232e57aa11f6fa72c" dependencies = [ "glib-sys", "libc", - "system-deps", + "system-deps 7.0.8", ] [[package]] name = "graphene-rs" -version = "0.22.0" +version = "0.22.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7d1b7881f96869f49808b6adfe906a93a57a34204952253444d68c3208d71f1" +checksum = "eb856b9c558971c3f13ab692358926da710b046932a4e087aedcc35b040d7dff" dependencies = [ "glib", "graphene-sys", - "libc", ] [[package]] name = "graphene-sys" -version = "0.22.0" +version = "0.22.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "517f062f3fd6b7fd3e57a3f038a74b3c23ca32f51199ff028aa704609943f79c" +checksum = "5c7ffdfde88f3570d3705e0d8a2433e036d387a1f2930bbf47eafcb5f569fd04" dependencies = [ "glib-sys", "libc", - "pkg-config", - "system-deps", + "system-deps 7.0.8", ] [[package]] name = "gsk4" -version = "0.11.1" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c912dfcbd28acace5fc99c40bb9f25e1dcb73efb1f2608327f66a99acdcb62" +checksum = "b867be1c5f14dcb8f552c0eff6e9a9b1da5f8b43943e8efc3a63c889d84952ff" dependencies = [ "cairo-rs", "gdk4", @@ -610,9 +608,9 @@ dependencies = [ [[package]] name = "gsk4-sys" -version = "0.11.1" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7d54bbc7a9d8b6ffe4f0c95eede15ccfb365c8bf521275abe6bcfb57b18fb8a" +checksum = "5b7c7eb2e681ee896646cfb8872b431f24d09f53ba9283289d9b10caa6707088" dependencies = [ "cairo-sys-rs", "gdk4-sys", @@ -621,14 +619,14 @@ dependencies = [ "graphene-sys", "libc", "pango-sys", - "system-deps", + "system-deps 7.0.8", ] [[package]] name = "gtk4" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7181b837f04cbe93f79441475f7a00560a92cba7a72e38cc1a68b6f8b78eaae2" +checksum = "98a0a0466484f64b07b5b8184d43fa46be78eb0b8e04ae4e179af31d770b76d9" dependencies = [ "cairo-rs", "field-offset", @@ -647,9 +645,9 @@ dependencies = [ [[package]] name = "gtk4-layer-shell" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4069987ff4793699511a251028cc336b438e46565b463f111250148d574752a" +checksum = "17c28ea0f4676fdaaae7ff2413a24d0d35c8657424f84856c1103c73454c9da4" dependencies = [ "bitflags", "gdk4", @@ -662,34 +660,34 @@ dependencies = [ [[package]] name = "gtk4-layer-shell-sys" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f566a5ec5bcc454e7fcf2ab76930887ced5365afce12c1e5201bb296b95f1b9" +checksum = "bcf19bb884ef0ef55b9e6b2b369c39b4fcc0c41e3a0c1cbc8c267720338b690b" dependencies = [ "gdk4-sys", "glib-sys", "gtk4-sys", "libc", - "system-deps", + "system-deps 8.0.0", ] [[package]] name = "gtk4-macros" -version = "0.11.0" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3581b242ba62fdff122ebb626ea641582ec326031622bd19d60f85029c804a87" +checksum = "5ac7179400a36a04de039c24206bb841c5596992b907b43b23ee8d5bdc40d00e" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "gtk4-sys" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20ba8e695e2640455561274e65e45f0a151619e450746007667f4b23ceae4e1b" +checksum = "82b8f954786af0b1984425c4446b77f5ff6594346181316be3f850caab1c6f01" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -701,7 +699,7 @@ dependencies = [ "gsk4-sys", "libc", "pango-sys", - "system-deps", + "system-deps 7.0.8", ] [[package]] @@ -718,9 +716,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "icu_collections" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" dependencies = [ "displaydoc", "potential_utf", @@ -732,9 +730,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" dependencies = [ "displaydoc", "litemap", @@ -745,9 +743,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -759,16 +757,17 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" [[package]] name = "icu_properties" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" dependencies = [ + "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", @@ -779,15 +778,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" [[package]] name = "icu_provider" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +checksum = "92a7ed671a6aad807a8651a2e1782a6598fda9ce5185dd8158549e95a91c6428" dependencies = [ "displaydoc", "icu_locale_core", @@ -849,36 +848,36 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "libc" -version = "0.2.186" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libredox" -version = "0.1.17" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +checksum = "28d0a00925a9f930d679b6789b721e3a7f9ed110f41b86d2497caa780c3a070a" dependencies = [ "libc", ] [[package]] name = "litemap" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" [[package]] name = "log" -version = "0.4.32" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "memchr" -version = "2.8.1" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memoffset" @@ -919,13 +918,12 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "pango" -version = "0.22.6" +version = "0.22.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "251bdc6e6487b811be0e406a21e301e07e45c0aa8fa39e00c0c8e12a91752438" +checksum = "5d800d8d0de2ad5d0fb046f5344dbaba14a003cf3dd27cc21d85893d35ea316c" dependencies = [ "gio", "glib", - "libc", "pango-sys", ] @@ -938,7 +936,7 @@ dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps", + "system-deps 7.0.8", ] [[package]] @@ -955,15 +953,15 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkg-config" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" [[package]] name = "potential_utf" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" dependencies = [ "zerovec", ] @@ -974,23 +972,23 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.25.12+spec-1.1.0", + "toml_edit 0.25.13+spec-1.1.0", ] [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.45" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -1031,9 +1029,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ "log", "once_cell", @@ -1046,18 +1044,18 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.1" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a" dependencies = [ "ring", "rustls-pki-types", @@ -1072,9 +1070,9 @@ checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -1082,29 +1080,29 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -1139,9 +1137,9 @@ checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "simd-adler32" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" [[package]] name = "slab" @@ -1151,9 +1149,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "stable_deref_trait" @@ -1175,9 +1173,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.117" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" dependencies = [ "proc-macro2", "quote", @@ -1203,7 +1201,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -1215,7 +1213,20 @@ dependencies = [ "cfg-expr", "heck", "pkg-config", - "toml 1.1.2+spec-1.1.0", + "toml 1.1.4+spec-1.1.0", + "version-compare", +] + +[[package]] +name = "system-deps" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83779a5c956bcb6ba627a4ecf0a9d7625db47d7537e0892d97f712ac995648a3" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml 1.1.4+spec-1.1.0", "version-compare", ] @@ -1242,14 +1253,14 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] name = "tinystr" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" dependencies = [ "displaydoc", "zerovec", @@ -1269,9 +1280,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.2+spec-1.1.0" +version = "1.1.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" dependencies = [ "indexmap", "serde_core", @@ -1279,7 +1290,7 @@ dependencies = [ "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "toml_writer", - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] @@ -1316,23 +1327,23 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.12+spec-1.1.0" +version = "0.25.13+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" dependencies = [ "indexmap", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] name = "toml_parser" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] @@ -1343,9 +1354,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "toml_writer" -version = "1.1.1+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "tracing" @@ -1366,7 +1377,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", ] [[package]] @@ -1448,14 +1459,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.7", + "webpki-roots 1.0.9", ] [[package]] name = "webpki-roots" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" dependencies = [ "rustls-pki-types", ] @@ -1625,24 +1636,24 @@ dependencies = [ [[package]] name = "winnow" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" dependencies = [ "memchr", ] [[package]] name = "writeable" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "xml-rs" -version = "0.8.28" +version = "0.8.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" +checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" [[package]] name = "yoke" @@ -1663,7 +1674,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", "synstructure", ] @@ -1684,21 +1695,21 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.119", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerotrie" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" dependencies = [ "displaydoc", "yoke", @@ -1707,9 +1718,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.6" +version = "0.11.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +checksum = "94b5c6b5976d66c1d703c4fd17d3f5e43c8cedaacf604961b171adc7130896d8" dependencies = [ "yoke", "zerofrom", @@ -1718,17 +1729,17 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +checksum = "47402523226a02bfe5230160dc3ccc089aa6f6f19e7fcbb4e6f824bbb1b4aa62" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 3.0.3", ] [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/breadbox/Cargo.toml b/breadbox/Cargo.toml index 9c40453..c1a40cc 100644 --- a/breadbox/Cargo.toml +++ b/breadbox/Cargo.toml @@ -9,7 +9,7 @@ name = "breadbox" path = "src/main.rs" [dependencies] -bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2", features = ["gtk"] } +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"] } # Capture primitives for `--screenshot` mode — see src/screenshot.rs. bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2" } diff --git a/breadbox/src/main.rs b/breadbox/src/main.rs index b207b97..3506706 100644 --- a/breadbox/src/main.rs +++ b/breadbox/src/main.rs @@ -1,7 +1,7 @@ use bread_theme::{hex_to_rgba, ink_on, load_palette, Palette}; use bread_utils::bread_client::BreadClient; use std::{ - cell::RefCell, + cell::{Cell, RefCell}, collections::HashMap, env, fs, @@ -10,6 +10,7 @@ use std::{ path::{Path, PathBuf}, process::{Command, Stdio}, rc::Rc, + time::Duration, }; /// This app's id in bread's sibling-app namespace registry @@ -23,8 +24,8 @@ use gtk4::{ glib, pango::EllipsizeMode, prelude::*, - Application, ApplicationWindow, Box as GBox, CssProvider, EventControllerKey, Label, - ListBox, Orientation, PolicyType, ScrolledWindow, SearchEntry, SelectionMode, + Application, ApplicationWindow, Box as GBox, CssProvider, Entry, EventControllerKey, Label, + ListBox, Orientation, PolicyType, ScrolledWindow, SelectionMode, }; use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell}; @@ -138,58 +139,171 @@ fn matches_term(field: &str, term: &str) -> bool { // ---- Theming ---------------------------------------------------------------- +const STAGGER_ROWS: usize = 12; + fn build_css(p: &Palette) -> String { - let bg_panel = hex_to_rgba(&p.background, 0.60); + let bg_panel = hex_to_rgba(&p.background, 0.68); // breadbox-specific rules only — fonts, palette, and generic widgets come // from the shared ecosystem stylesheet (applied first in connect_activate). - // Colour is set on each surface (panel, search box, hovered/selected row) so + // Colour is set on each surface (panel, search, hovered/selected row) so // child labels inherit the legible ink for that background. `on_*` are - // luminance-picked black/white — the pywal hues are untouched. Without this a - // light `surface` slot makes the selected row's text vanish. + // luminance-picked black/white — the pywal hues are untouched. + // + // GTK4 ListBox's node is `list`, not `listbox`. These `list row:selected` + // rules beat the shared sheet's solid accent fill + on-accent ink so the + // glass card keeps a tinted selection and a left inset hairline. + let stagger = (0..STAGGER_ROWS) + .map(|i| { + format!( + ".launcher-bg.just-opened list row.stagger-{i} {{ animation-delay: {}ms; }}", + i * 28 + ) + }) + .collect::>() + .join(""); format!( - "window {{ background-color: transparent; }}\ - .launcher-bg {{ background-color: {bg_panel}; color: {on_bg}; border-radius: 8px;\ - box-shadow: 0 8px 32px rgba(0,0,0,0.6); }}\ - searchentry {{ background-color: {surface}; color: {on_surface}; caret-color: {accent};\ - border: none; outline: none; box-shadow: none;\ - padding: 12px 16px; border-radius: 6px 6px 0 0; }}\ - listbox {{ background-color: transparent; padding: 4px; }}\ - row {{ padding: 8px 12px; color: {on_bg}; background-color: transparent;\ - border-radius: 6px; }}\ - row:hover {{ background-color: {surface}; color: {on_surface}; }}\ - row:selected {{ background-color: {surface}; color: {on_surface}; }}\ - .app-name {{ font-size: 14px; }}\ - .app-muted {{ opacity: 0.6; font-size: 12px; }}\ - image {{ margin-right: 8px; }}", - bg_panel = bg_panel, - surface = p.color0, - accent = p.color4, - on_bg = ink_on(&p.background), - on_surface = ink_on(&p.color0), + "\ +window {{ background-color: rgba(0, 0, 0, 0.28); animation: scrim-in 0.28s ease both; }}\ +@keyframes scrim-in {{\ + from {{ background-color: rgba(0, 0, 0, 0); }}\ + to {{ background-color: rgba(0, 0, 0, 0.28); }}\ +}}\ +.launcher-bg {{\ + background-color: {bg_panel}; color: {on_bg}; border-radius: 20px;\ + border: 1px solid alpha({on_bg}, 0.14);\ + box-shadow: 0 24px 64px rgba(0, 0, 0, 0.50);\ + animation: card-in 0.42s cubic-bezier(0.22, 1, 0.36, 1) both;\ +}}\ +@keyframes card-in {{\ + from {{ opacity: 0; margin-top: 112px; }}\ + to {{ opacity: 1; margin-top: 88px; }}\ +}}\ +.launcher-bg entry {{\ + background-color: transparent; color: {on_bg}; caret-color: {accent};\ + border: none; outline: none; box-shadow: none;\ + padding: 20px 22px 14px; border-radius: 20px 20px 0 0;\ + font-size: 17px; min-height: 28px;\ +}}\ +.launcher-bg entry:focus, .launcher-bg entry:focus-within {{\ + border: none; outline: none; box-shadow: none; background-color: transparent;\ +}}\ +entry > text {{ background: transparent; }}\ +entry image {{ opacity: 0; min-width: 0; margin: 0; padding: 0; }}\ +.launcher-caret {{\ + min-height: 2px; max-height: 2px; margin: 0 20px; border-radius: 2px;\ + background-color: {accent};\ + background-image: linear-gradient(90deg, {accent}, {accent2});\ +}}\ +.launcher-bg.just-opened .launcher-caret {{\ + animation: caret-draw 0.45s cubic-bezier(0.22, 1, 0.36, 1) both;\ +}}\ +@keyframes caret-draw {{\ + from {{ margin-right: 600px; opacity: 0.25; }}\ + to {{ margin-right: 20px; opacity: 1; }}\ +}}\ +scrolledwindow {{ background: transparent; }}\ +list {{ background-color: transparent; padding: 8px 8px 4px; }}\ +list row {{\ + padding: 6px 8px; color: {on_bg}; background-color: transparent;\ + border-radius: 14px; margin: 1px 10px; outline: none;\ +}}\ +list row:hover {{ background-color: alpha({on_bg}, 0.07); color: {on_bg}; }}\ +row:selected, list row:selected, list row:selected:focus,\ +list row:selected:hover, list row:selected:focus:hover {{\ + background-color: alpha({accent}, 0.22); color: {on_bg};\ + outline: none; box-shadow: none;\ +}}\ +list row:selected label, list row:selected .app-name, list row:selected .app-muted {{\ + color: {on_bg};\ +}}\ +.app-row {{ min-height: 48px; }}\ +.app-icon-well {{\ + min-width: 38px; min-height: 38px; margin-right: 12px;\ + border-radius: 999px; background-color: alpha({on_bg}, 0.08);\ +}}\ +.app-icon {{ color: {on_bg}; opacity: 0.88; }}\ +.app-name {{ font-size: 14px; font-weight: bold; }}\ +.app-muted {{ opacity: 0.48; font-size: 11px; }}\ +.launcher-footer {{\ + padding: 8px 18px 12px; font-size: 11px; opacity: 0.40;\ + letter-spacing: 0.08em; text-transform: uppercase;\ +}}\ +.launcher-bg.just-opened list row {{\ + animation: row-in 0.32s cubic-bezier(0.22, 1, 0.36, 1) both;\ +}}\ +@keyframes row-in {{\ + from {{ opacity: 0; }}\ + to {{ opacity: 1; }}\ +}}\ +{stagger}\ +list.reflow row {{ animation: row-fade 0.16s ease both; }}\ +@keyframes row-fade {{\ + from {{ opacity: 0.40; }}\ + to {{ opacity: 1; }}\ +}}\ +.no-motion, .no-motion * {{ animation: none; transition: none; }}", + bg_panel = bg_panel, + accent = p.color4, + accent2 = p.color5, + on_bg = ink_on(&p.background), + stagger = stagger, ) } +fn category_label(entry: &DesktopEntry) -> &'static str { + let has = |needle: &str| { + entry + .categories + .iter() + .any(|c| c.eq_ignore_ascii_case(needle) || c.to_ascii_lowercase().contains(needle)) + }; + if entry.terminal || has("terminalemulator") { + "Terminal" + } else if has("webbrowser") { + "Browser" + } else if has("game") { + "Games" + } else if has("instantmessaging") || has("chat") || has("ircclient") { + "Chat" + } else if has("settings") || has("desktopsettings") || has("system") { + "System" + } else if has("ide") || has("development") { + "IDE" + } else if has("office") || has("wordprocessor") || has("texteditor") || has("notes") { + "Notes" + } else if has("audio") || has("player") || has("audiovideo") { + "Music" + } else if has("graphics") || has("photography") || has("camera") { + "Capture" + } else if has("filemanager") { + "Files" + } else { + "App" + } +} + +fn symbolic_icon(entry: &DesktopEntry) -> &'static str { + match category_label(entry) { + "Browser" => "web-browser-symbolic", + "Terminal" => "utilities-terminal-symbolic", + "Notes" => "accessories-text-editor-symbolic", + "System" => "emblem-system-symbolic", + "Chat" => "user-available-symbolic", + "Games" => "applications-games-symbolic", + "Music" => "audio-x-generic-symbolic", + "Capture" => "camera-photo-symbolic", + "IDE" => "applications-engineering-symbolic", + "Files" => "folder-symbolic", + _ => "application-x-executable-symbolic", + } +} + // ---- Icon loading ----------------------------------------------------------- -fn make_icon(icon_name: &str, icon_path: Option<&Path>) -> gtk4::Image { - // Try loading from resolved cached path via gio::File - if let Some(path) = icon_path { - let gio_file = gtk4::gio::File::for_path(path); - if let Ok(texture) = gtk4::gdk::Texture::from_file(&gio_file) { - let img = gtk4::Image::new(); - img.set_paintable(Some(&texture)); - img.set_pixel_size(32); - return img; - } - } - // Fall back to GTK icon theme lookup by name - let name = if icon_name.is_empty() { - "application-x-executable" - } else { - icon_name - }; - let img = gtk4::Image::from_icon_name(name); - img.set_pixel_size(32); +fn make_icon(entry: &DesktopEntry) -> gtk4::Image { + let img = gtk4::Image::from_icon_name(symbolic_icon(entry)); + img.set_pixel_size(18); + img.add_css_class("app-icon"); img } @@ -287,6 +401,26 @@ fn get_row_entry(row: >k4::ListBoxRow) -> Option { } } +fn visible_row_count(list: &ListBox) -> u32 { + let mut n = 0; + let mut i = 0; + while let Some(row) = list.row_at_index(i) { + if row.is_visible() { + n += 1; + } + i += 1; + } + n +} + +fn set_footer_count(footer: &Label, n: u32) { + match n { + 0 => footer.set_text("no match"), + 1 => footer.set_text("1 app"), + n => footer.set_text(&format!("{n} apps")), + } +} + fn run_ui( entries: Vec, history: LaunchHistory, @@ -330,6 +464,7 @@ fn run_ui( window.set_anchor(edge, true); } window.set_exclusive_zone(0); + bread_theme::gtk::bind_window_auto(&window); let close_all: Rc = Rc::new({ let w = window.clone(); @@ -342,13 +477,25 @@ fn run_ui( vbox.add_css_class("launcher-bg"); vbox.set_halign(gtk4::Align::Center); vbox.set_valign(gtk4::Align::Start); - vbox.set_margin_top(120); + vbox.set_margin_top(88); vbox.set_size_request(600, -1); + if is_screenshot_run { + window.add_css_class("no-motion"); + vbox.add_css_class("no-motion"); + } else { + vbox.add_css_class("just-opened"); + } - let search = SearchEntry::new(); - search.set_placeholder_text(Some("breadbox")); + let search = Entry::new(); + search.set_placeholder_text(Some("Search")); + search.set_has_frame(false); vbox.append(&search); + let caret = GBox::new(Orientation::Horizontal, 0); + caret.add_css_class("launcher-caret"); + caret.set_hexpand(true); + vbox.append(&caret); + let scroll = ScrolledWindow::new(); scroll.set_policy(PolicyType::Never, PolicyType::Automatic); scroll.set_max_content_height(480); @@ -359,28 +506,44 @@ fn run_ui( for (idx, entry) in entries.iter().enumerate() { let row = gtk4::ListBoxRow::new(); + if idx < STAGGER_ROWS { + row.add_css_class(&format!("stagger-{idx}")); + } let hbox = GBox::new(Orientation::Horizontal, 0); - hbox.set_margin_start(6); - hbox.set_margin_end(6); + hbox.add_css_class("app-row"); hbox.set_valign(gtk4::Align::Center); - let icon = make_icon(&entry.icon_name, entry.icon_path.as_deref()); - hbox.append(&icon); + let well = GBox::new(Orientation::Horizontal, 0); + well.add_css_class("app-icon-well"); + well.set_size_request(38, 38); + well.set_halign(gtk4::Align::Center); + well.set_valign(gtk4::Align::Center); + well.set_hexpand(false); + let icon = make_icon(entry); + icon.set_halign(gtk4::Align::Center); + icon.set_valign(gtk4::Align::Center); + icon.set_hexpand(true); + well.append(&icon); + hbox.append(&well); + + let text = GBox::new(Orientation::Vertical, 1); + text.add_css_class("app-text"); + text.set_hexpand(true); + text.set_valign(gtk4::Align::Center); let name_lbl = Label::new(Some(&entry.name)); name_lbl.add_css_class("app-name"); name_lbl.set_xalign(0.0); - name_lbl.set_hexpand(true); name_lbl.set_ellipsize(EllipsizeMode::End); - hbox.append(&name_lbl); + text.append(&name_lbl); - if let Some(ref wm) = entry.wm_class { - let wm_lbl = Label::new(Some(wm)); - wm_lbl.add_css_class("app-muted"); - wm_lbl.set_xalign(1.0); - hbox.append(&wm_lbl); - } + let sub_lbl = Label::new(Some(category_label(entry))); + sub_lbl.add_css_class("app-muted"); + sub_lbl.set_xalign(0.0); + sub_lbl.set_ellipsize(EllipsizeMode::End); + text.append(&sub_lbl); + hbox.append(&text); row.set_child(Some(&hbox)); unsafe { row.set_data("entry", entry.clone()) }; unsafe { row.set_data("initial_order", idx as u32) }; @@ -418,11 +581,29 @@ fn run_ui( scroll.set_child(Some(&list)); vbox.append(&scroll); + + let footer = Label::new(None); + footer.add_css_class("launcher-footer"); + footer.set_xalign(0.0); + set_footer_count(&footer, visible_row_count(&list)); + vbox.append(&footer); + window.set_child(Some(&vbox)); - // Filter on keystroke + if !is_screenshot_run { + let vbox_open = vbox.clone(); + glib::timeout_add_local_once(Duration::from_millis(520), move || { + vbox_open.remove_css_class("just-opened"); + }); + } + + // Filter on keystroke. ListBox keeps row identity across sort, so the + // reorder is already a cheap FLIP analog; a short CSS fade is the extra. let list_f = list.clone(); + let footer_f = footer.clone(); + let vbox_f = vbox.clone(); let filter_query = Rc::clone(&query_rc); + let reflow_gen = Rc::new(Cell::new(0u32)); search.connect_changed(move |entry| { let text = entry.text(); let query = text.as_str(); @@ -432,6 +613,7 @@ fn run_ui( let vis = get_row_entry(&row) .map(|e| { fuzzy_matches(query, &e.name) + || fuzzy_matches(query, category_label(&e)) || e.wm_class .as_deref() .is_some_and(|w| fuzzy_matches(query, w)) @@ -446,6 +628,20 @@ fn run_ui( list_f.row_at_index(j).filter(|r| r.is_visible()) }); list_f.select_row(first_vis.as_ref()); + set_footer_count(&footer_f, visible_row_count(&list_f)); + if !vbox_f.has_css_class("just-opened") { + list_f.remove_css_class("reflow"); + list_f.add_css_class("reflow"); + let gen = reflow_gen.get().wrapping_add(1); + reflow_gen.set(gen); + let list_fade = list_f.clone(); + let reflow_gen = Rc::clone(&reflow_gen); + glib::timeout_add_local_once(Duration::from_millis(180), move || { + if reflow_gen.get() == gen { + list_fade.remove_css_class("reflow"); + } + }); + } }); // Keyboard handling — capture phase on window From 7c55ea4756ecf047a722af2644732423cce97339 Mon Sep 17 00:00:00 2001 From: Breadway Date: Sun, 23 Aug 2026 14:39:11 +0800 Subject: [PATCH 29/30] Adopt bread_utils::gtk_popup for the launcher overlay Replace the hand-rolled layer-shell window, visible-row Up/Down navigation, and click-outside-to-close gesture with the shared helpers already used by breadclip. Keep singleton::toggle_or_kill and bind_window_auto. --- Cargo.lock | 2 + breadbox/Cargo.toml | 2 +- breadbox/src/main.rs | 113 ++++++++++++++++--------------------------- 3 files changed, 44 insertions(+), 73 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c9780a0..b47dd23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -121,6 +121,8 @@ source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.2#30517 dependencies = [ "bread-shared", "dirs", + "gtk4", + "gtk4-layer-shell", "serde", "serde_json", ] diff --git a/breadbox/Cargo.toml b/breadbox/Cargo.toml index 0977c1e..fde186f 100644 --- a/breadbox/Cargo.toml +++ b/breadbox/Cargo.toml @@ -10,7 +10,7 @@ path = "src/main.rs" [dependencies] 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.7.2", features = ["bread-client", "gtk"] } # Capture primitives for `--screenshot` mode — see src/screenshot.rs. bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2" } breadbox-shared = { path = "../breadbox-shared" } diff --git a/breadbox/src/main.rs b/breadbox/src/main.rs index 3506706..b29b80c 100644 --- a/breadbox/src/main.rs +++ b/breadbox/src/main.rs @@ -3,8 +3,7 @@ use bread_utils::bread_client::BreadClient; use std::{ cell::{Cell, RefCell}, collections::HashMap, - env, - fs, + env, fs, io::{Read, Write}, os::unix::net::UnixStream, path::{Path, PathBuf}, @@ -21,13 +20,9 @@ use breadbox_shared::{ config_dir, load_all_desktop_entries, Config, DesktopEntry, IconCache, LaunchHistory, }; use gtk4::{ - glib, - pango::EllipsizeMode, - prelude::*, - Application, ApplicationWindow, Box as GBox, CssProvider, Entry, EventControllerKey, Label, - ListBox, Orientation, PolicyType, ScrolledWindow, SelectionMode, + glib, pango::EllipsizeMode, prelude::*, Application, Box as GBox, CssProvider, Entry, + EventControllerKey, Label, ListBox, Orientation, PolicyType, ScrolledWindow, SelectionMode, }; -use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell}; mod listen; mod screenshot; @@ -91,7 +86,9 @@ fn load_sorted_entries( (None, Some(_)) => std::cmp::Ordering::Greater, (None, None) => { // Most-launched first, then alphabetical - history.count(&b.name).cmp(&history.count(&a.name)) + history + .count(&b.name) + .cmp(&history.count(&a.name)) .then(a.name.to_lowercase().cmp(&b.name.to_lowercase())) } } @@ -385,10 +382,18 @@ fn fuzzy_score(query: &str, entry: &DesktopEntry) -> u32 { let q = query.to_lowercase(); let name = entry.name.to_lowercase(); let wm = entry.wm_class.as_deref().unwrap_or("").to_lowercase(); - if name == q || wm == q { return 0; } - if name.starts_with(&q) { return 1; } - if name.contains(&q) { return 2; } - if wm.starts_with(&q) || wm.contains(&q) { return 3; } + if name == q || wm == q { + return 0; + } + if name.starts_with(&q) { + return 1; + } + if name.contains(&q) { + return 2; + } + if wm.starts_with(&q) || wm.contains(&q) { + return 3; + } 4 // subsequence match } @@ -454,16 +459,8 @@ fn run_ui( bread_theme::gtk::apply_user_css(&user_css_path, &user_cell); } - // Full-screen transparent window; clicks outside the launcher panel close it. - let window = ApplicationWindow::builder().application(app).build(); - window.init_layer_shell(); - window.set_namespace(Some("breadbox")); - window.set_layer(Layer::Overlay); - window.set_keyboard_mode(KeyboardMode::Exclusive); - for edge in [Edge::Top, Edge::Bottom, Edge::Left, Edge::Right] { - window.set_anchor(edge, true); - } - window.set_exclusive_zone(0); + // Full-screen transparent overlay; panel widget is positioned inside it. + let window = bread_utils::gtk_popup::new_overlay_window(app, "breadbox"); bread_theme::gtk::bind_window_auto(&window); let close_all: Rc = Rc::new({ @@ -557,8 +554,16 @@ fn run_ui( list.set_sort_func(move |row_a, row_b| { let query = sort_query.borrow(); if query.is_empty() { - let oa = unsafe { row_a.data::("initial_order").map_or(u32::MAX, |p| *p.as_ref()) }; - let ob = unsafe { row_b.data::("initial_order").map_or(u32::MAX, |p| *p.as_ref()) }; + let oa = unsafe { + row_a + .data::("initial_order") + .map_or(u32::MAX, |p| *p.as_ref()) + }; + let ob = unsafe { + row_b + .data::("initial_order") + .map_or(u32::MAX, |p| *p.as_ref()) + }; return oa.cmp(&ob).into(); } let (Some(ea), Some(eb)) = (get_row_entry(row_a), get_row_entry(row_b)) else { @@ -624,9 +629,8 @@ fn run_ui( i += 1; } list_f.invalidate_sort(); - let first_vis = (0i32..).find_map(|j| { - list_f.row_at_index(j).filter(|r| r.is_visible()) - }); + let first_vis = + (0i32..).find_map(|j| list_f.row_at_index(j).filter(|r| r.is_visible())); list_f.select_row(first_vis.as_ref()); set_footer_count(&footer_f, visible_row_count(&list_f)); if !vbox_f.has_css_class("just-opened") { @@ -669,36 +673,11 @@ fn run_ui( glib::Propagation::Stop } Key::Down => { - let cur = list_k.selected_row().map(|r| r.index()).unwrap_or(-1); - let mut i = cur + 1; - loop { - match list_k.row_at_index(i) { - Some(r) if r.is_visible() => { - list_k.select_row(Some(&r)); - break; - } - Some(_) => i += 1, - None => break, - } - } + bread_utils::gtk_popup::select_next_visible(&list_k); glib::Propagation::Stop } Key::Up => { - let cur = list_k.selected_row().map(|r| r.index()).unwrap_or(0); - let mut i = cur - 1; - loop { - if i < 0 { - break; - } - match list_k.row_at_index(i) { - Some(r) if r.is_visible() => { - list_k.select_row(Some(&r)); - break; - } - Some(_) => i -= 1, - None => break, - } - } + bread_utils::gtk_popup::select_prev_visible(&list_k); glib::Propagation::Stop } _ => glib::Propagation::Proceed, @@ -719,22 +698,10 @@ fn run_ui( }); // Click outside launcher panel → close - let close_outside = Rc::clone(&close_all); - let vbox_ref = vbox.clone(); - let win_ref = window.clone(); - let outside_click = gtk4::GestureClick::new(); - outside_click.connect_pressed(move |_, _, x, y| { - if let Some(b) = vbox_ref.compute_bounds(&win_ref) { - if x < b.x() as f64 - || x > (b.x() + b.width()) as f64 - || y < b.y() as f64 - || y > (b.y() + b.height()) as f64 - { - close_outside(); - } - } - }); - window.add_controller(outside_click); + { + let close_outside = Rc::clone(&close_all); + bread_utils::gtk_popup::close_on_outside_click(&window, &vbox, move || close_outside()); + } if let Some(req) = screenshot_req.clone() { screenshot::dispatch(&window, req); @@ -782,7 +749,9 @@ fn main() { Ok(bread_utils::singleton::Toggle::Started(guard)) => Some(guard), Ok(bread_utils::singleton::Toggle::KilledExisting) => return, Err(e) => { - eprintln!("breadbox: single-instance lock unavailable ({e}); continuing without it"); + eprintln!( + "breadbox: single-instance lock unavailable ({e}); continuing without it" + ); None } } From 09530e236d24a0d3dedd1272611b3570bdd33c44 Mon Sep 17 00:00:00 2001 From: Breadway Date: Mon, 31 Aug 2026 15:59:32 +0800 Subject: [PATCH 30/30] Adopt bread-launcher: shared matching/ranking/rows + manifest theming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit breadbox's launcher is now a thin host over the `bread-launcher` crate (bread-ecosystem v0.7.5) — the same core breadbar's embedded capsule uses — instead of a private implementation: - `breadbox-shared` becomes a re-export shim over `bread_launcher` (`DesktopEntry`, `LaunchHistory`, `config_dir`, `LAUNCHER_APP`); the local fuzzy-match / ranking / desktop-file parsing is deleted. - `run_ui` builds `bread_launcher::gtk::ResultsList` for the results list and calls `bread_launcher::do_launch` / `load_sorted_entries`; the hand-rolled row/icon/score code is gone. - Launcher geometry and style come from the active shell-theme manifest (`bread_theme::shell`, via the new `src/theme.rs` accessor mirroring breadbar's) — width, radius, panel alpha, top margin, icon size, footer wording. The Daylight / liquid-motion look is the manifest's job now, not hardcoded CSS. - Keeps `main`'s recent improvements on top of the branch work: the overlay window + outside-click come from `bread_utils::gtk_popup` (`new_overlay_window` / `bind_window_auto` / `close_on_outside_click`), replacing the manual layer-shell + GestureClick boilerplate. - Embedded-open redirect: when the active theme embeds the launcher (spotlight capsule), breadbox emits `bread.box.open_requested` on the bus instead of mapping its own window; falls back to its own overlay when breadd is unreachable. - Pins: bread-theme / bread-utils / bread-screenshots / bread-launcher → v0.7.5 (bread-launcher and `bread_theme::shell` first ship there). `.cargo/` gitignored — the local sibling-checkout source override (`fa85fc1`) is no longer needed now that v0.7.5 is tagged. Reconciles the long-lived `feature/launcher-core` (13 commits, incl. `feature/launcher-theming`) with `main`'s independent launcher redesign (`feature/liquid-motion` + `bread_utils::gtk_popup` adoption). Applied as one squashed reconciliation rather than a 13-commit rebase because the two lines rewrote the same ~200-line `run_ui`/`build_css` region in incompatible directions. cargo build --locked / test --locked / clippy --all-targets --locked -D warnings all clean. --- .gitignore | 3 + Cargo.lock | 198 ++++++++- EVENTS.md | 57 ++- breadbox-shared/Cargo.toml | 5 + breadbox-shared/src/lib.rs | 283 ++----------- breadbox-sync/src/main.rs | 4 +- breadbox/Cargo.toml | 9 +- breadbox/src/listen.rs | 39 +- breadbox/src/main.rs | 832 +++++++++++++------------------------ breadbox/src/theme.rs | 98 +++++ 10 files changed, 722 insertions(+), 806 deletions(-) create mode 100644 breadbox/src/theme.rs diff --git a/.gitignore b/.gitignore index ec10cb0..0b68375 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ CLAUDE.md # graphify knowledge-graph output (local tool cache, not for commit) graphify-out/ + +# Local-only source overrides (see .cargo/config.toml). +.cargo/ diff --git a/Cargo.lock b/Cargo.lock index b47dd23..d46553a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,10 +82,20 @@ version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +[[package]] +name = "bread-launcher" +version = "0.7.5" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.5#46b886b7bc168e73945269e143b18ef8a0d987f8" +dependencies = [ + "bread-utils", + "gtk4", + "serde_json", +] + [[package]] name = "bread-screenshots" -version = "0.7.2" -source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.2#30517f161724132cdeb658c04cf5e490be07ee73" +version = "0.7.5" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.5#46b886b7bc168e73945269e143b18ef8a0d987f8" dependencies = [ "anyhow", "bread-utils", @@ -94,33 +104,37 @@ dependencies = [ [[package]] name = "bread-shared" -version = "0.7.0" -source = "git+https://git.breadway.dev/Breadway/bread?tag=v0.7.0#22e34e2cf2202305d7960759dfccb54dc79f948b" +version = "0.8.0" +source = "git+https://git.breadway.dev/Breadway/bread?tag=v0.8.0#cdd5de8f58e437b3fc6d9b9087eb7b3d0fd09704" dependencies = [ - "dirs", + "dirs 6.0.0", "serde", "serde_json", "toml 0.8.23", + "uuid", ] [[package]] name = "bread-theme" -version = "0.7.4" -source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.4#fcba3760387e2523edb71350f8efea3bc851b21e" +version = "0.7.5" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.5#46b886b7bc168e73945269e143b18ef8a0d987f8" dependencies = [ - "dirs", + "anyhow", + "dirs 5.0.1", "gtk4", "serde", "serde_json", + "toml 0.8.23", + "tracing", ] [[package]] name = "bread-utils" -version = "0.7.2" -source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.2#30517f161724132cdeb658c04cf5e490be07ee73" +version = "0.7.5" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.5#46b886b7bc168e73945269e143b18ef8a0d987f8" dependencies = [ "bread-shared", - "dirs", + "dirs 5.0.1", "gtk4", "gtk4-layer-shell", "serde", @@ -132,6 +146,7 @@ name = "breadbox" version = "0.3.3" dependencies = [ "anyhow", + "bread-launcher", "bread-screenshots", "bread-theme", "bread-utils", @@ -146,6 +161,7 @@ dependencies = [ name = "breadbox-shared" version = "0.3.3" dependencies = [ + "bread-launcher", "serde", "serde_json", "toml 0.8.23", @@ -160,6 +176,12 @@ dependencies = [ "ureq", ] +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + [[package]] name = "cairo-rs" version = "0.22.0" @@ -270,7 +292,16 @@ version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" dependencies = [ - "dirs-sys", + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", ] [[package]] @@ -281,10 +312,22 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.4.6", "windows-sys 0.48.0", ] +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.2", + "windows-sys 0.61.2", +] + [[package]] name = "displaydoc" version = "0.2.7" @@ -468,6 +511,17 @@ dependencies = [ "wasi", ] +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + [[package]] name = "gio" version = "0.22.8" @@ -842,6 +896,17 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + [[package]] name = "khronos_api" version = "3.1.0" @@ -995,15 +1060,32 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "redox_users" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom", + "getrandom 0.2.17", "libredox", - "thiserror", + "thiserror 1.0.69", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.20", ] [[package]] @@ -1014,7 +1096,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -1064,6 +1146,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + [[package]] name = "semver" version = "1.0.28" @@ -1244,7 +1332,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl 2.0.20", ] [[package]] @@ -1258,6 +1355,17 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "tinystr" version = "0.8.4" @@ -1443,6 +1551,17 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "version-compare" version = "0.2.1" @@ -1455,6 +1574,51 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] + [[package]] name = "webpki-roots" version = "0.26.11" diff --git a/EVENTS.md b/EVENTS.md index 3e7064a..c180c75 100644 --- a/EVENTS.md +++ b/EVENTS.md @@ -14,13 +14,21 @@ connection. Command verbs are only received while `breadbox listen` is running — that process holds the `bread.command.box.**` subscription open. +Under an `[launcher] mode = "embedded"` theme (spotlight), breadbar's +own bar-drawer capsule *is* the launcher UI, and breadbox redirects +instead of mapping its own overlay window — see the "Embedded launcher +theme" section below. Everything else on this page describes the +default (`overlay`-mode) behavior. + ## Events published (`bread.box.*`) | Event | Data | When | |-------|------|------| | `bread.box.launched` | `{ "id": "", "name": "" }` | The user launched an app (Enter / keypad Enter on the selected row, or activating a row) **and** the spawn succeeded. Not emitted if `Command::spawn` fails (missing terminal, `exec` that cannot start). `id` is the desktop-file id (the `.desktop` filename, e.g. `firefox.desktop`), falling back to the stripped `Exec=` line when that id is empty. `name` is the desktop-entry display name. | -| `bread.box.open.done` | `{}` | `bread.command.box.open` was received and `breadbox` was spawned. This is the command confirmation, not proof the overlay mapped — the spawned process is the same toggle as a keybind. | -| `bread.box.open.failed` | `{ "error": "" }` | `bread.command.box.open` was received but this binary could not be started. | +| `bread.box.open.done` | `{}` | `bread.command.box.open` was received (via `breadbox listen`) **and this binary was spawned** — non-embedded themes only. Not proof the overlay mapped, just that the process was started (same toggle as a keybind). Never emitted under an embedded theme — see below. | +| `bread.box.open.failed` | `{ "error": "" }` | `bread.command.box.open` was received but this binary could not be started — non-embedded themes only. Unreachable under an embedded theme, since that branch returns before ever attempting to spawn. | +| `bread.box.open_requested` | `{}` | Emitted by the plain `breadbox` binary itself (not `breadbox listen`) instead of mapping its own overlay window, only when the active theme's `[launcher].mode` is `embedded`. breadbar's capsule (`launcher_command.rs`) subscribes to this and opens itself. Own-namespace event, not a command — see `dispatch_embedded_open`'s doc comment for why `bread.command.box.open` would be the wrong shape here. | +| `bread.box.open.redirected` | `{}` | Emitted by `breadbox listen`'s `handle_open`, only when `bread.command.box.open` is received while an embedded theme is active. Replaces `.done`/`.failed` in that case: this process never spawns anything (breadbar's capsule is the intended handler) and has no way to confirm breadbar actually received or handled the event — pub/sub here is one-way with no ack. This event means "redirect happened, outcome unknown", not "succeeded". | Launch history is local to breadbox (`~/.cache/breadbox/history.json`); the event bus is a notification that a launch happened, not a channel @@ -34,15 +42,21 @@ bread convention, not a breadbox bug. | Verb | Data | Effect | |------|------|--------| -| `open` | none | Same as running `breadbox` (toggle the launcher overlay via the existing singleton). Emits `bread.box.open.done` / `.failed`. | +| `open` | none | Under a non-embedded theme: same as running `breadbox` (toggle the launcher overlay via the existing singleton). Emits `bread.box.open.done` / `.failed`. Under an embedded theme: never spawns; emits `bread.box.open.redirected` instead — see "Embedded launcher theme" below. | ```lua +-- Non-embedded themes: `.done` is a real completion signal. bread.spawn(function() bread.emit("bread.command.box.open") bread.wait("bread.box.open.done", { timeout = 5000 }) end) ``` +Under an embedded theme this `.done` wait will time out — `.done` is +never emitted there. A workflow that needs to work under every theme +should wait on `bread.box.open.redirected` too (or treat a timeout as +"probably fine, embedded themes have no ack" rather than a failure). + ### Not implemented: extra verbs There is no `launch` / `close` / `query` command verb. Picking a desktop @@ -50,12 +64,44 @@ id from the bus would be a new product surface. If/when that exists, add the corresponding `bread.command.box.*` verb at the same time, not stubbed as a no-op ahead of it. +## Embedded launcher theme (spotlight) + +When the active shell theme's `[launcher].mode` is `embedded`, running +`breadbox` directly (e.g. from a keybind) does **not** map this binary's +overlay window — that would stack a second launcher on top of +breadbar's own capsule. Instead: + +1. `breadbox`'s `main` checks `BreadClient::health()` — a real, + bounded-timeout round trip to breadd, not just "did the socket + exist". +2. If breadd **is** reachable, it emits `bread.box.open_requested` and + returns. breadbar's capsule (subscribed only while its own active + theme is also `embedded`) is expected to open itself in response. + There is no ack for this — if breadbar isn't running, or is running + under a different theme and therefore never subscribed, this is + still a silent no-op on the bus with nothing further to fall back + to. (`BreadClient` has no "is anyone subscribed" query.) +3. If breadd is **not** reachable, `breadbox` logs that to stderr and + falls back to mapping its own overlay window anyway — a keybind + press always opens *something*, rather than the historical fully + silent no-op when the bus was down. + +`breadbox listen`'s `handle_open` (the `bread.command.box.open` +handler) applies the same "never map a second launcher" rule but from +the command side: under an embedded theme it never spawns, logs to +stderr, and emits `bread.box.open.redirected` instead of `.done` — see +the events table above for why `.done` would be a false claim here. + ## 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) and the command subscription simply never receives anything — launching, history, theming, and the singleton toggle are entirely unaffected. + Under an embedded theme specifically, a direct `breadbox` invocation + additionally checks reachability up front and falls back to mapping + its own overlay window when breadd is down (see above) — so this + invariant now holds there too, not just for every other theme. - If breadd restarts, the command subscription reconnects automatically (`BreadClient::subscribe`'s background thread has its own backoff loop); no restart of `breadbox listen` is needed. @@ -63,3 +109,8 @@ stubbed as a no-op ahead of it. the bus (no subscriber). The CLI still works, and one-shot invocations still emit `bread.box.launched` on their own short-lived connection. - Closing the launcher without launching anything emits nothing. +- What is **not** covered: under an embedded theme, if breadd is + reachable but breadbar itself isn't running (or isn't subscribed), + neither the direct-keybind path nor the command path can detect that + or fall back further — both are fire-and-forget with no ack. This is + a known limitation of the current transport, not an oversight. diff --git a/breadbox-shared/Cargo.toml b/breadbox-shared/Cargo.toml index 2c583be..7bb5868 100644 --- a/breadbox-shared/Cargo.toml +++ b/breadbox-shared/Cargo.toml @@ -8,3 +8,8 @@ license = "MIT" serde = { version = "1", features = ["derive"] } serde_json = "1" toml = "0.8" +# The desktop-entry/icon-cache/launch-history substance this crate used to +# own has moved to bread-ecosystem's bread-launcher (feature/launcher-core) +# so breadbar can share it too — see src/lib.rs. No `gtk` feature: this +# shim only re-exports the headless half. +bread-launcher = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.5" } diff --git a/breadbox-shared/src/lib.rs b/breadbox-shared/src/lib.rs index 92c573b..4b10510 100644 --- a/breadbox-shared/src/lib.rs +++ b/breadbox-shared/src/lib.rs @@ -1,264 +1,59 @@ -use std::{ - collections::HashMap, - env, - fs::{self, File}, - io::{BufRead, BufReader}, - path::{Path, PathBuf}, -}; +//! **Compatibility shim.** This crate used to own desktop-entry parsing, +//! icon caching, and launch history; that substance now lives in +//! `bread-launcher` (`bread-ecosystem`, feature/launcher-core) so breadbar's +//! future embedded capsule can share it with breadbox's overlay window — +//! one launcher implementation, two hosts (see +//! `bos-ui-demos/THEME_SYSTEM_PLAN.md` §3, §7). +//! +//! Everything below is either a direct re-export of `bread_launcher`, or a +//! thin wrapper that just bakes in `"breadbox"` as the app name +//! `bread_launcher`'s path/cache functions now take explicitly (so more +//! than one host can use that crate without colliding on +//! `~/.cache/`). This exists purely so `breadbox` and `breadbox-sync` +//! keep compiling with minimal churn — plan to remove it once both callers +//! depend on `bread-launcher` directly instead. +//! +//! `Config`/`Context` are NOT part of that move: they're breadbox's own +//! per-workspace launch-priority config format, not launcher substance, so +//! they stay here. + +use std::{fs, path::PathBuf}; use serde::{Deserialize, Serialize}; -// ---- XDG path helpers ------------------------------------------------------- +pub use bread_launcher::{ + app_dirs, home_dir, load_all_desktop_entries, parse_desktop, strip_exec_codes, DesktopEntry, + IconCache, LaunchHistory, +}; -pub fn home_dir() -> PathBuf { - PathBuf::from(env::var("HOME").unwrap_or_else(|_| "/tmp".into())) -} +/// The launcher's shared identity (`bread_launcher::LAUNCHER_APP`, currently +/// `"breadbox"`) — NOT a breadbox-specific literal. breadbar's embedded +/// capsule (theme 04/spotlight) reads/writes this exact same on-disk cache +/// and history, so the two surfaces stay one launcher with one ranking +/// instead of forking into two (see `LAUNCHER_APP`'s own doc comment). +const APP: &str = bread_launcher::LAUNCHER_APP; pub fn cache_dir() -> PathBuf { - env::var("XDG_CACHE_HOME") - .map(PathBuf::from) - .unwrap_or_else(|_| home_dir().join(".cache")) - .join("breadbox") + bread_launcher::cache_dir(APP) } pub fn config_dir() -> PathBuf { - env::var("XDG_CONFIG_HOME") - .map(PathBuf::from) - .unwrap_or_else(|_| home_dir().join(".config")) - .join("breadbox") + bread_launcher::config_dir(APP) } -pub fn app_dirs() -> Vec { - let home = home_dir(); - let mut dirs = vec![PathBuf::from("/usr/share/applications")]; - - let xdg_data_dirs = env::var("XDG_DATA_DIRS") - .unwrap_or_else(|_| "/usr/local/share:/usr/share".into()); - for d in xdg_data_dirs.split(':') { - let p = PathBuf::from(d).join("applications"); - if p != dirs[0] { - dirs.push(p); - } - } - - dirs.push( - env::var("XDG_DATA_HOME") - .map(PathBuf::from) - .unwrap_or_else(|_| home.join(".local/share")) - .join("applications"), - ); - dirs +pub fn icon_cache() -> IconCache { + IconCache::new(APP) } -// ---- Desktop entry ---------------------------------------------------------- - -#[derive(Debug, Clone)] -pub struct DesktopEntry { - /// Desktop file id (the `.desktop` filename, e.g. `firefox.desktop`). - /// Empty only if the path had no file name; callers fall back to `exec`. - pub id: String, - pub name: String, - pub exec: String, - pub icon_name: String, - pub icon_path: Option, // resolved by caller from manifest - pub categories: Vec, - pub wm_class: Option, - pub terminal: bool, +pub fn icon_manifest_path() -> PathBuf { + IconCache::manifest_path(APP) } -pub fn strip_exec_codes(exec: &str) -> String { - let mut out = String::with_capacity(exec.len()); - let mut chars = exec.chars().peekable(); - while let Some(c) = chars.next() { - if c == '%' { - match chars.peek().copied() { - Some('%') => { - chars.next(); - out.push('%'); - } - Some(n) if n.is_ascii_alphabetic() => { - chars.next(); - } - _ => out.push(c), - } - } else { - out.push(c); - } - } - out +pub fn launch_history() -> LaunchHistory { + LaunchHistory::load(APP) } -/// Returns `None` for entries that should not be shown (hidden, NoDisplay, non-Application type). -pub fn parse_desktop(path: &Path) -> Option { - let file = File::open(path).ok()?; - let mut in_entry = false; - let mut name: Option = None; - let mut exec: Option = None; - let mut icon: Option = None; - let mut categories: Option = None; - let mut wm_class: Option = None; - let mut app_type: Option = None; - let mut no_display = false; - let mut hidden = false; - let mut terminal = false; - - for line in BufReader::new(file).lines() { - let Ok(raw) = line else { continue }; - let s = raw.trim(); - if s.starts_with('#') || s.is_empty() { - continue; - } - if s.starts_with('[') { - in_entry = s == "[Desktop Entry]"; - continue; - } - if !in_entry { - continue; - } - - if let Some(v) = s.strip_prefix("Name=") { - name.get_or_insert_with(|| v.to_string()); - } else if let Some(v) = s.strip_prefix("Exec=") { - exec.get_or_insert_with(|| v.to_string()); - } else if let Some(v) = s.strip_prefix("Icon=") { - icon.get_or_insert_with(|| v.to_string()); - } else if let Some(v) = s.strip_prefix("Categories=") { - categories.get_or_insert_with(|| v.to_string()); - } else if let Some(v) = s.strip_prefix("StartupWMClass=") { - wm_class.get_or_insert_with(|| v.to_string()); - } else if let Some(v) = s.strip_prefix("Type=") { - app_type.get_or_insert_with(|| v.to_string()); - } else if let Some(v) = s.strip_prefix("NoDisplay=") { - no_display = v == "true"; - } else if let Some(v) = s.strip_prefix("Hidden=") { - hidden = v == "true"; - } else if let Some(v) = s.strip_prefix("Terminal=") { - terminal = v == "true" || v == "1"; - } - } - - if no_display || hidden { - return None; - } - if app_type.as_deref() != Some("Application") { - return None; - } - - let name = name?.trim().to_string(); - let exec = strip_exec_codes(exec?.trim()).trim().to_string(); - if name.is_empty() || exec.is_empty() { - return None; - } - - let icon_name = icon.unwrap_or_default().trim().to_string(); - let cats = categories - .unwrap_or_default() - .split(';') - .filter(|s| !s.is_empty()) - .map(|s| s.to_string()) - .collect(); - - let id = path - .file_name() - .map(|n| n.to_string_lossy().into_owned()) - .filter(|s| !s.is_empty()) - .unwrap_or_default(); - - Some(DesktopEntry { - id, - name, - exec, - icon_name, - icon_path: None, - categories: cats, - wm_class: wm_class.map(|s| s.trim().to_string()).filter(|s| !s.is_empty()), - terminal, - }) -} - -/// Walk all configured application directories and return deduplicated entries. -/// Entries from later directories (user-local) override those from earlier ones. -pub fn load_all_desktop_entries() -> Vec { - let mut seen: std::collections::HashMap = std::collections::HashMap::new(); - for dir in app_dirs() { - let Ok(entries) = fs::read_dir(&dir) else { continue }; - for entry in entries.flatten() { - let path = entry.path(); - if path.extension().and_then(|e| e.to_str()) != Some("desktop") { - continue; - } - let key = entry.file_name().to_string_lossy().into_owned(); - if let Some(app) = parse_desktop(&path) { - seen.insert(key, app); - } - } - } - seen.into_values().collect() -} - -// ---- Icon cache ------------------------------------------------------------- - -pub struct IconCache { - pub dir: PathBuf, -} - -impl IconCache { - pub fn new() -> Self { - IconCache { dir: cache_dir().join("icons") } - } - - pub fn path_for(&self, icon_name: &str) -> PathBuf { - self.dir.join(format!("{}.png", icon_name)) - } - - pub fn manifest_path() -> PathBuf { - cache_dir().join("manifest.json") - } - - pub fn ensure_dir(&self) -> std::io::Result<()> { - fs::create_dir_all(&self.dir) - } -} - -impl Default for IconCache { - fn default() -> Self { - Self::new() - } -} - -// ---- Launch history --------------------------------------------------------- - -pub struct LaunchHistory { - counts: HashMap, - path: PathBuf, -} - -impl LaunchHistory { - pub fn load() -> Self { - let path = cache_dir().join("history.json"); - let counts = fs::read_to_string(&path) - .ok() - .and_then(|s| serde_json::from_str(&s).ok()) - .unwrap_or_default(); - LaunchHistory { counts, path } - } - - pub fn count(&self, name: &str) -> u32 { - self.counts.get(name).copied().unwrap_or(0) - } - - pub fn increment(&mut self, name: &str) { - *self.counts.entry(name.to_string()).or_insert(0) += 1; - } - - pub fn save(&self) { - if let Ok(json) = serde_json::to_string(&self.counts) { - let _ = fs::write(&self.path, json); - } - } -} - -// ---- Config ----------------------------------------------------------------- +// ---- Config (breadbox-specific, not launcher substance) -------------------- #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Config { diff --git a/breadbox-sync/src/main.rs b/breadbox-sync/src/main.rs index 8c919bf..6bf73ee 100644 --- a/breadbox-sync/src/main.rs +++ b/breadbox-sync/src/main.rs @@ -205,7 +205,7 @@ fn main() { } fn run() -> Result<(), Box> { - let icon_cache = IconCache::new(); + let icon_cache = breadbox_shared::icon_cache(); icon_cache.ensure_dir()?; let theme = current_icon_theme(); @@ -266,7 +266,7 @@ fn run() -> Result<(), Box> { } } - let manifest_path = IconCache::manifest_path(); + let manifest_path = breadbox_shared::icon_manifest_path(); let json = serde_json::to_string_pretty(&manifest)?; let tmp = manifest_path.with_extension("tmp"); fs::write(&tmp, &json)?; diff --git a/breadbox/Cargo.toml b/breadbox/Cargo.toml index fde186f..966fcc8 100644 --- a/breadbox/Cargo.toml +++ b/breadbox/Cargo.toml @@ -9,10 +9,13 @@ name = "breadbox" path = "src/main.rs" [dependencies] -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", "gtk"] } +bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.5", features = ["gtk"] } +bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.5", features = ["bread-client", "gtk"] } # Capture primitives for `--screenshot` mode — see src/screenshot.rs. -bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2" } +bread-screenshots = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.5" } +# Matching/ranking/launching plus the results-list widget (see src/main.rs +# run_ui) — the same core breadbar's embedded capsule will use next phase. +bread-launcher = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.5", features = ["gtk"] } breadbox-shared = { path = "../breadbox-shared" } gtk4 = { version = "0.11", features = ["v4_12"] } gtk4-layer-shell = "0.8" diff --git a/breadbox/src/listen.rs b/breadbox/src/listen.rs index 9aeb367..e626532 100644 --- a/breadbox/src/listen.rs +++ b/breadbox/src/listen.rs @@ -44,11 +44,48 @@ fn handle_command(event: &BreadEvent) { } fn handle_open() { + let client = BreadClient::connect(APP_ID); + + // Under an embedded-launcher theme (spotlight), breadbar's own capsule + // subscribes to both this command and `bread.box.open_requested` (see + // `main.rs`'s `dispatch_embedded_open` doc comment) — it, not this + // process, is the thing that should react. Spawning `breadbox` here + // would just re-run that same binary's own embedded-mode redirect + // (`main`'s `dispatch_embedded_open`), emitting + // `bread.box.open_requested` back onto the bus; breadbar would open the + // capsule again and this still-running subscription would keep spawning + // on every command it sees. So this branch never spawns — that part is + // correct and stays. + // + // It used to also emit `bread.box.open.done` here unconditionally, which + // was a false-positive success report: `.done`'s documented meaning + // (EVENTS.md) is "breadbox was spawned", which is literally untrue in + // this branch, and there is no ack from breadbar on this one-way + // pub/sub bus — `BreadClient` has no way to ask "did anything actually + // pick this up" (same limitation `main.rs`'s `dispatch_embedded_open` + // documents). Claiming `.done` reported a completion this process + // cannot observe — the same shape of bug as bug #6 (a namespace + // violation making a false claim), just an over-eager "done" instead of + // an under-eager warning. Since confirming the handoff isn't possible + // with the current transport, this does the honest thing instead: log + // locally (so the failure mode is diagnosable from `breadbox listen`'s + // own output) and emit a distinct, explicitly-unconfirmed event so a bus + // observer can tell "redirected, outcome unknown" apart from "breadbox + // spawned" rather than being told a specific untrue thing. + if crate::theme::shell_theme().launcher().mode == bread_theme::shell::LauncherMode::Embedded { + eprintln!( + "breadbox: bread.command.box.open received under an embedded launcher \ + theme; breadbar's capsule is the intended handler and this process \ + cannot confirm it received the event" + ); + client.emit("bread.box.open.redirected", serde_json::json!({})); + return; + } + // Same as running `breadbox` from a keybind: toggle the overlay via the // existing singleton. Spawn success is the command confirmation — we do // not wait for the GTK window to map. let result = spawn_self(); - let client = BreadClient::connect(APP_ID); match result { Ok(_) => client.emit("bread.box.open.done", serde_json::json!({})), Err(e) => { diff --git a/breadbox/src/main.rs b/breadbox/src/main.rs index b29b80c..c8a639d 100644 --- a/breadbox/src/main.rs +++ b/breadbox/src/main.rs @@ -1,31 +1,37 @@ +use bread_launcher::gtk::{row_entry, ResultsList}; use bread_theme::{hex_to_rgba, ink_on, load_palette, Palette}; -use bread_utils::bread_client::BreadClient; use std::{ - cell::{Cell, RefCell}, + cell::RefCell, collections::HashMap, env, fs, io::{Read, Write}, os::unix::net::UnixStream, - path::{Path, PathBuf}, - process::{Command, Stdio}, + path::PathBuf, rc::Rc, - time::Duration, }; /// This app's id in bread's sibling-app namespace registry /// (`bread_shared::apps::KNOWN_APPS`) — events publish as `bread.box.*`. const APP_ID: &str = "box"; -use breadbox_shared::{ - config_dir, load_all_desktop_entries, Config, DesktopEntry, IconCache, LaunchHistory, -}; +/// Emitted instead of mapping breadbox's own window when the active shell +/// theme's launcher is embedded (theme 04's capsule). Must stay inside +/// `bread.box.*` — see [`dispatch_embedded_open`] for why. +pub const EMBEDDED_OPEN_EVENT: &str = "bread.box.open_requested"; + +/// Published via `bread_launcher::do_launch` after a successful launch — +/// see `EVENTS.md`. +const LAUNCHED_EVENT: &str = "bread.box.launched"; + +use breadbox_shared::{config_dir, Config, DesktopEntry, LaunchHistory}; use gtk4::{ - glib, pango::EllipsizeMode, prelude::*, Application, Box as GBox, CssProvider, Entry, - EventControllerKey, Label, ListBox, Orientation, PolicyType, ScrolledWindow, SelectionMode, + glib, prelude::*, Application, Box as GBox, CssProvider, + EventControllerKey, Label, Orientation, SearchEntry, }; mod listen; mod screenshot; +mod theme; // ---- Hyprland IPC ----------------------------------------------------------- @@ -48,7 +54,7 @@ fn get_active_workspace() -> Option { // ---- Manifest --------------------------------------------------------------- fn load_manifest() -> HashMap { - let path = IconCache::manifest_path(); + let path = breadbox_shared::icon_manifest_path(); let content = fs::read_to_string(&path).unwrap_or_default(); serde_json::from_str::>(&content) .unwrap_or_default() @@ -57,360 +63,145 @@ fn load_manifest() -> HashMap { .collect() } -// ---- Entry loading and sorting ---------------------------------------------- - -fn load_sorted_entries( - manifest: &HashMap, - priority: &[String], - history: &LaunchHistory, -) -> Vec { - let mut entries = load_all_desktop_entries(); - - // Populate icon_path from manifest - for entry in &mut entries { - if let Some(path) = manifest.get(&entry.icon_name) { - if path.exists() { - entry.icon_path = Some(path.clone()); - } - } - } - - let priority_lower: Vec = priority.iter().map(|s| s.to_lowercase()).collect(); - - entries.sort_by(|a, b| { - let ai = priority_rank(a, &priority_lower); - let bi = priority_rank(b, &priority_lower); - match (ai, bi) { - (Some(i), Some(j)) => i.cmp(&j), - (Some(_), None) => std::cmp::Ordering::Less, - (None, Some(_)) => std::cmp::Ordering::Greater, - (None, None) => { - // Most-launched first, then alphabetical - history - .count(&b.name) - .cmp(&history.count(&a.name)) - .then(a.name.to_lowercase().cmp(&b.name.to_lowercase())) - } - } - }); - - entries -} - -fn priority_rank(entry: &DesktopEntry, priority_lower: &[String]) -> Option { - let name_l = entry.name.to_lowercase(); - let wm_l = entry.wm_class.as_deref().unwrap_or("").to_lowercase(); - priority_lower - .iter() - .position(|p| matches_term(&name_l, p) || matches_term(&wm_l, p)) -} - -/// Whole-word / exact match of `term` within `field` (both lowercase). Avoids -/// "code" matching "vscodium" while still matching "Code", "code-oss", and -/// "Visual Studio Code". -fn matches_term(field: &str, term: &str) -> bool { - if term.is_empty() || field.is_empty() { - return false; - } - if field == term { - return true; - } - let bytes = field.as_bytes(); - let tlen = term.len(); - let mut start = 0; - while let Some(pos) = field[start..].find(term) { - let i = start + pos; - let before_ok = i == 0 || !bytes[i - 1].is_ascii_alphanumeric(); - let after = i + tlen; - let after_ok = after >= bytes.len() || !bytes[after].is_ascii_alphanumeric(); - if before_ok && after_ok { - return true; - } - start = i + 1; - if start >= field.len() { - break; - } - } - false -} - // ---- Theming ---------------------------------------------------------------- -const STAGGER_ROWS: usize = 12; - -fn build_css(p: &Palette) -> String { - let bg_panel = hex_to_rgba(&p.background, 0.68); - // breadbox-specific rules only — fonts, palette, and generic widgets come - // from the shared ecosystem stylesheet (applied first in connect_activate). - // Colour is set on each surface (panel, search, hovered/selected row) so - // child labels inherit the legible ink for that background. `on_*` are - // luminance-picked black/white — the pywal hues are untouched. +fn build_css( + p: &Palette, + launcher: &bread_theme::shell::Launcher, + tokens: &bread_theme::shell::Tokens, +) -> String { + // Panel opacity comes from the theme, not a hardcoded constant. 0.60 made + // the launcher wash out over a bright wallpaper and its text hard to read — + // a bar can be that translucent because it covers a thin strip, a full + // panel cannot. The approved reference uses 0.95/0.93 per theme. + let bg_panel = hex_to_rgba(&p.background, launcher.panel_alpha as f32); + let radius = format!("{}px", launcher.radius); + let on_bg = ink_on(&p.background); + // breadbox-specific rules only — the generic widget baseline (buttons, + // switches, plain `entry`/`spinbutton`) comes from the shared ecosystem + // stylesheet, applied first (lower priority) in connect_activate via + // `apply_shared`. Colour is set on each surface (panel, search box, + // hovered/selected row) so child labels inherit the legible ink for that + // background. `on_*` are luminance-picked black/white — the pywal hues + // are untouched. Without this a light `surface` slot makes the selected + // row's text vanish. // - // GTK4 ListBox's node is `list`, not `listbox`. These `list row:selected` - // rules beat the shared sheet's solid accent fill + on-accent ink so the - // glass card keeps a tinted selection and a left inset hairline. - let stagger = (0..STAGGER_ROWS) - .map(|i| { - format!( - ".launcher-bg.just-opened list row.stagger-{i} {{ animation-delay: {}ms; }}", - i * 28 - ) - }) - .collect::>() - .join(""); + // Selectors matter here: GTK4's `GtkSearchEntry` CSS node is `entry` + // carrying a `.search` style class (confirmed against + // `/usr/share/gir-1.0/Gtk-4.0.gir`'s `SearchEntry` "## CSS Nodes" doc, + // `entry.search ╰── text`) — NOT a node literally named `searchentry`, + // and `GtkListBox`'s node is `list`, NOT `listbox`. Both bogus selectors + // shipped here previously and silently matched nothing, which is why + // this rule block's `border`/`outline` suppression never actually beat + // the shared stylesheet's `entry:focus-within { border-color: @accent }` + // rule (`bread_theme::stylesheet`, still layered in underneath at + // APPLICATION priority) — the accent-coloured focus "outline" the user + // was seeing was that shared rule showing through unopposed. `entry` + // alone (no `.search`) would work too since this window has only one + // entry, but `.search` documents which GtkSearchEntry state this targets + // and is what actually renders. + // + // Per-theme geometry now comes entirely from `[launcher]`/`[tokens]` — + // liquid-motion and glass-workbench set every one of these to different + // values (see their `assets/shell/*/theme.toml`), which is what makes + // the two themes read as different instruments rather than one + // launcher recoloured. format!( - "\ -window {{ background-color: rgba(0, 0, 0, 0.28); animation: scrim-in 0.28s ease both; }}\ -@keyframes scrim-in {{\ - from {{ background-color: rgba(0, 0, 0, 0); }}\ - to {{ background-color: rgba(0, 0, 0, 0.28); }}\ -}}\ -.launcher-bg {{\ - background-color: {bg_panel}; color: {on_bg}; border-radius: 20px;\ - border: 1px solid alpha({on_bg}, 0.14);\ - box-shadow: 0 24px 64px rgba(0, 0, 0, 0.50);\ - animation: card-in 0.42s cubic-bezier(0.22, 1, 0.36, 1) both;\ -}}\ -@keyframes card-in {{\ - from {{ opacity: 0; margin-top: 112px; }}\ - to {{ opacity: 1; margin-top: 88px; }}\ -}}\ -.launcher-bg entry {{\ - background-color: transparent; color: {on_bg}; caret-color: {accent};\ - border: none; outline: none; box-shadow: none;\ - padding: 20px 22px 14px; border-radius: 20px 20px 0 0;\ - font-size: 17px; min-height: 28px;\ -}}\ -.launcher-bg entry:focus, .launcher-bg entry:focus-within {{\ - border: none; outline: none; box-shadow: none; background-color: transparent;\ -}}\ -entry > text {{ background: transparent; }}\ -entry image {{ opacity: 0; min-width: 0; margin: 0; padding: 0; }}\ -.launcher-caret {{\ - min-height: 2px; max-height: 2px; margin: 0 20px; border-radius: 2px;\ - background-color: {accent};\ - background-image: linear-gradient(90deg, {accent}, {accent2});\ -}}\ -.launcher-bg.just-opened .launcher-caret {{\ - animation: caret-draw 0.45s cubic-bezier(0.22, 1, 0.36, 1) both;\ -}}\ -@keyframes caret-draw {{\ - from {{ margin-right: 600px; opacity: 0.25; }}\ - to {{ margin-right: 20px; opacity: 1; }}\ -}}\ -scrolledwindow {{ background: transparent; }}\ -list {{ background-color: transparent; padding: 8px 8px 4px; }}\ -list row {{\ - padding: 6px 8px; color: {on_bg}; background-color: transparent;\ - border-radius: 14px; margin: 1px 10px; outline: none;\ -}}\ -list row:hover {{ background-color: alpha({on_bg}, 0.07); color: {on_bg}; }}\ -row:selected, list row:selected, list row:selected:focus,\ -list row:selected:hover, list row:selected:focus:hover {{\ - background-color: alpha({accent}, 0.22); color: {on_bg};\ - outline: none; box-shadow: none;\ -}}\ -list row:selected label, list row:selected .app-name, list row:selected .app-muted {{\ - color: {on_bg};\ -}}\ -.app-row {{ min-height: 48px; }}\ -.app-icon-well {{\ - min-width: 38px; min-height: 38px; margin-right: 12px;\ - border-radius: 999px; background-color: alpha({on_bg}, 0.08);\ -}}\ -.app-icon {{ color: {on_bg}; opacity: 0.88; }}\ -.app-name {{ font-size: 14px; font-weight: bold; }}\ -.app-muted {{ opacity: 0.48; font-size: 11px; }}\ -.launcher-footer {{\ - padding: 8px 18px 12px; font-size: 11px; opacity: 0.40;\ - letter-spacing: 0.08em; text-transform: uppercase;\ -}}\ -.launcher-bg.just-opened list row {{\ - animation: row-in 0.32s cubic-bezier(0.22, 1, 0.36, 1) both;\ -}}\ -@keyframes row-in {{\ - from {{ opacity: 0; }}\ - to {{ opacity: 1; }}\ -}}\ -{stagger}\ -list.reflow row {{ animation: row-fade 0.16s ease both; }}\ -@keyframes row-fade {{\ - from {{ opacity: 0.40; }}\ - to {{ opacity: 1; }}\ -}}\ -.no-motion, .no-motion * {{ animation: none; transition: none; }}", - bg_panel = bg_panel, - accent = p.color4, - accent2 = p.color5, - on_bg = ink_on(&p.background), - stagger = stagger, + "window {{ background-color: transparent; }}\ + .launcher-bg {{ background-color: {bg_panel}; color: {on_bg}; border-radius: {radius};\ + /* NO drop shadow. In a browser `backdrop-filter` blurs only the\ + element's own box, so the demo's shadow is free. Hyprland\ + blurs any surface pixel above `ignore_alpha` (0.2), and a\ + 32px shadow at 0.6 alpha is far above it — so the compositor\ + blurred the shadow as well, painting a rectangular blurred\ + halo around the rounded panel. That halo is the \"blur\ + backdrop whose radius doesn't match the content\" problem.\ + The blur behind a 0.95-opaque panel already gives plenty of\ + separation; the shadow only fought it. */\ + font-family: \"{font_family}\", {font_fallback}; }}\ + entry.search {{ background-color: transparent; color: {on_bg}; caret-color: {accent};\ + border: none; outline: none; box-shadow: none; border-radius: 0;\ + border-bottom: 1px solid {hairline};\ + padding: {search_pv}px {search_ph}px; font-size: {search_fs}px; }}\ + entry.search:focus, entry.search:focus-within {{\ + outline: none; box-shadow: none; border-color: transparent;\ + border-bottom: 1px solid {hairline}; }}\ + list {{ background-color: transparent; padding: 4px 0; }}\ + row {{ padding: {row_pv}px {row_ph}px; margin: 0 {row_inset}px; color: {on_bg};\ + background-color: transparent; border-radius: {row_radius}px;\ + font-size: {row_fs}px; }}\ + row:hover, row:selected {{ background-color: {selection_bg}; color: {on_bg}; }}\ + .app-muted {{ opacity: 0.4; font-size: 11px; }}\ + /* No background tile: these are real app icons, not the demo's\ + placeholder .ico boxes. A tint behind a real icon reads as a\ + failed/unloaded image. */\ + image {{ margin-right: 8px; border-radius: {icon_radius}px; }}\ + /* \"Recent\"/\"Apps\" headers (`[launcher].sections`, liquid-motion\ + only — glass-workbench's flat list never builds these rows). */\ + .section-header-label {{ font-size: 10px; letter-spacing: 0.14em;\ + text-transform: uppercase; font-weight: 600; opacity: 0.45; }}\ + .bread-drawer-section-header {{ padding: 11px 16px 5px; }}\ + .launcher-footer {{ padding: 8px 16px 12px; font-size: 10px; opacity: 0.4;\ + {footer_case} }}", + bg_panel = bg_panel, + accent = hex_to_rgba(&p.color4, 0.9), + on_bg = on_bg, + hairline = hex_to_rgba(on_bg, 0.08), + selection_bg = hex_to_rgba(&p.color4, launcher.selection_alpha as f32), + radius = radius, + font_family = tokens.font_family(), + font_fallback = tokens.font_fallback(), + row_fs = tokens.font_size_base(), + row_pv = launcher.row_padding_v, + row_ph = launcher.row_padding_h, + row_inset = launcher.row_inset, + row_radius = launcher.row_radius, + icon_radius = launcher.icon_radius, + search_pv = launcher.search_padding_v, + search_ph = launcher.search_padding_h, + search_fs = launcher.search_font_size, + // Section headers and an uppercase, letter-spaced footer travel + // together in both demos (liquid-motion has both; glass-workbench's + // flat list has neither) — reusing `sections` here instead of a + // dedicated schema key for this one footer detail. + footer_case = if launcher.sections { + "text-transform: uppercase; letter-spacing: 0.12em;" + } else { + "" + }, ) } -fn category_label(entry: &DesktopEntry) -> &'static str { - let has = |needle: &str| { - entry - .categories - .iter() - .any(|c| c.eq_ignore_ascii_case(needle) || c.to_ascii_lowercase().contains(needle)) - }; - if entry.terminal || has("terminalemulator") { - "Terminal" - } else if has("webbrowser") { - "Browser" - } else if has("game") { - "Games" - } else if has("instantmessaging") || has("chat") || has("ircclient") { - "Chat" - } else if has("settings") || has("desktopsettings") || has("system") { - "System" - } else if has("ide") || has("development") { - "IDE" - } else if has("office") || has("wordprocessor") || has("texteditor") || has("notes") { - "Notes" - } else if has("audio") || has("player") || has("audiovideo") { - "Music" - } else if has("graphics") || has("photography") || has("camera") { - "Capture" - } else if has("filemanager") { - "Files" +/// `[launcher].footer`'s noun: `"count_apps"` → "applications", anything +/// else (`"count_results"` included — every other current/future value +/// falls back to this rather than a hard error, matching the "never fails +/// to start over a theme string" rule the rest of this schema follows) → +/// "results". `n`'s plural/singular form is picked here rather than baked +/// into `[launcher].footer` itself, since neither built-in theme's value is +/// singular-aware. +fn footer_text(footer_kind: &str, n: usize) -> String { + let noun = if footer_kind == "count_apps" { + "application" } else { - "App" - } -} - -fn symbolic_icon(entry: &DesktopEntry) -> &'static str { - match category_label(entry) { - "Browser" => "web-browser-symbolic", - "Terminal" => "utilities-terminal-symbolic", - "Notes" => "accessories-text-editor-symbolic", - "System" => "emblem-system-symbolic", - "Chat" => "user-available-symbolic", - "Games" => "applications-games-symbolic", - "Music" => "audio-x-generic-symbolic", - "Capture" => "camera-photo-symbolic", - "IDE" => "applications-engineering-symbolic", - "Files" => "folder-symbolic", - _ => "application-x-executable-symbolic", - } -} - -// ---- Icon loading ----------------------------------------------------------- - -fn make_icon(entry: &DesktopEntry) -> gtk4::Image { - let img = gtk4::Image::from_icon_name(symbolic_icon(entry)); - img.set_pixel_size(18); - img.add_css_class("app-icon"); - img -} - -// ---- Launch ----------------------------------------------------------------- - -fn pick_terminal() -> String { - if let Ok(t) = env::var("TERMINAL") { - if !t.is_empty() { - return t; - } - } - let path_var = env::var("PATH").unwrap_or_default(); - for t in ["foot", "kitty", "alacritty", "wezterm", "ghostty", "xterm"] { - if path_var.split(':').any(|d| Path::new(d).join(t).exists()) { - return t.to_string(); - } - } - "xterm".to_string() -} - -fn do_launch(entry: &DesktopEntry) { - let cmd = entry.exec.trim(); - let spawned = if entry.terminal { - let term = pick_terminal(); - Command::new(&term) - .args(["-e", "bash", "-c", cmd]) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn() - } else { - Command::new("bash") - .args(["-c", cmd]) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn() + "result" }; - if spawned.is_ok() { - emit_launched(entry); - } -} - -/// Publishes `bread.box.launched` after a successful spawn. Fire-and-forget -/// and non-fatal (`BreadClient::emit` never blocks or errors this caller) — -/// breadd being absent must never affect launching itself. -fn emit_launched(entry: &DesktopEntry) { - let id = if entry.id.is_empty() { - entry.exec.as_str() + if n == 1 { + format!("1 {noun}") } else { - entry.id.as_str() - }; - BreadClient::connect(APP_ID).emit( - "bread.box.launched", - serde_json::json!({ "id": id, "name": entry.name }), - ); -} - -// ---- Fuzzy matching --------------------------------------------------------- - -fn fuzzy_matches(pattern: &str, text: &str) -> bool { - if pattern.is_empty() { - return true; - } - let mut chars = text.chars(); - for pc in pattern.chars() { - let pl = pc.to_lowercase().next().unwrap_or(pc); - if !chars - .by_ref() - .any(|tc| tc.to_lowercase().next().unwrap_or(tc) == pl) - { - return false; - } - } - true -} - -fn fuzzy_score(query: &str, entry: &DesktopEntry) -> u32 { - let q = query.to_lowercase(); - let name = entry.name.to_lowercase(); - let wm = entry.wm_class.as_deref().unwrap_or("").to_lowercase(); - if name == q || wm == q { - return 0; - } - if name.starts_with(&q) { - return 1; - } - if name.contains(&q) { - return 2; - } - if wm.starts_with(&q) || wm.contains(&q) { - return 3; - } - 4 // subsequence match -} - -// ---- UI --------------------------------------------------------------------- - -fn get_row_entry(row: >k4::ListBoxRow) -> Option { - unsafe { - row.data::("entry") - .map(|p| p.as_ref().clone()) + format!("{n} {noun}s") } } -fn visible_row_count(list: &ListBox) -> u32 { - let mut n = 0; - let mut i = 0; +/// Counts the rows currently shown as real (non-header) app matches — the +/// same "visible AND carries a `DesktopEntry`" test `ResultsList` itself +/// uses for keyboard selection (`select_next`/`select_prev` in +/// `bread_launcher::gtk`), reimplemented here read-only since `ResultsList` +/// doesn't expose a count of its own. +fn visible_app_count(list: >k4::ListBox) -> usize { + let mut i = 0i32; + let mut n = 0usize; while let Some(row) = list.row_at_index(i) { - if row.is_visible() { + if row.is_visible() && bread_launcher::gtk::row_entry(&row).is_some() { n += 1; } i += 1; @@ -418,13 +209,7 @@ fn visible_row_count(list: &ListBox) -> u32 { n } -fn set_footer_count(footer: &Label, n: u32) { - match n { - 0 => footer.set_text("no match"), - 1 => footer.set_text("1 app"), - n => footer.set_text(&format!("{n} apps")), - } -} +// ---- UI --------------------------------------------------------------------- fn run_ui( entries: Vec, @@ -442,15 +227,24 @@ fn run_ui( let app = builder.build(); let history_rc = Rc::new(RefCell::new(history)); - let query_rc: Rc> = Rc::new(RefCell::new(String::new())); let is_screenshot_run = screenshot_req.is_some(); app.connect_activate(move |app| { + // Shell theme, loaded once per process and cached (see src/theme.rs) + // — the source of the launcher's geometry and style values below, + // per THEME_SYSTEM_PLAN.md §4/§11 Phase 4. + let shell_theme = theme::shell_theme(); + let launcher = shell_theme.launcher().clone(); + // Shared ecosystem base (fonts, palette, generic widgets) first, then // breadbox-specific CSS layered on top — both hot-reload on // `bread-theme reload` (the closure re-reads the pywal palette). bread_theme::gtk::apply_shared(); - bread_theme::gtk::apply_app_css(|| build_css(&load_palette())); + { + let launcher = launcher.clone(); + let tokens = shell_theme.tokens().clone(); + bread_theme::gtk::apply_app_css(move || build_css(&load_palette(), &launcher, &tokens)); + } // User CSS override { @@ -474,186 +268,69 @@ fn run_ui( vbox.add_css_class("launcher-bg"); vbox.set_halign(gtk4::Align::Center); vbox.set_valign(gtk4::Align::Start); - vbox.set_margin_top(88); - vbox.set_size_request(600, -1); - if is_screenshot_run { - window.add_css_class("no-motion"); - vbox.add_css_class("no-motion"); - } else { - vbox.add_css_class("just-opened"); - } + vbox.set_margin_top(theme::top_margin_px(&launcher.top)); + // `set_size_request` only pins a MINIMUM, not a maximum — the same + // class of bug that made breadbar's capsule stretch to its widest + // row (`surface.rs`/`main.rs`'s `set_size_request` pin comment). + // This is safe here because `results.scroller` below is the + // `bread_launcher::gtk::ResultsList` widget, which already calls + // `scroller.set_propagate_natural_width(false)` specifically so its + // `ListBox`'s widest row (icon + full app name + wm-class) can never + // push a host wider than what the host requests — see that widget's + // own comment in `bread-launcher/src/gtk.rs`. `search` (a plain + // `SearchEntry`) has no unbounded natural width either. So nothing + // under `vbox` can grow past `launcher.width`, and this minimum-only + // request is effectively exact. + vbox.set_size_request(launcher.width, -1); - let search = Entry::new(); + let search = SearchEntry::new(); + // "Search", not the app's own name — matches both design demos' + // `` (bos-ui-demos/proposed/ + // {liquid-motion,glass-workbench}.html); no user-visible copy here + // should read like an internal identifier. search.set_placeholder_text(Some("Search")); - search.set_has_frame(false); vbox.append(&search); - let caret = GBox::new(Orientation::Horizontal, 0); - caret.add_css_class("launcher-caret"); - caret.set_hexpand(true); - vbox.append(&caret); + // Row building, fuzzy filtering, match/history sorting, and + // keyboard-style selection movement all live in bread-launcher's + // `ResultsList` — the widget breadbar's embedded capsule also uses + // (THEME_SYSTEM_PLAN.md §7). `[launcher].sections`: liquid-motion + // groups the idle view into "Recent"/"Apps" headers, glass-workbench + // stays the flat, ungrouped list — no longer hardcoded to `false`. + let results = ResultsList::new( + &entries, + launcher.icon_px, + Rc::clone(&history_rc), + launcher.sections, + ); - let scroll = ScrolledWindow::new(); - scroll.set_policy(PolicyType::Never, PolicyType::Automatic); - scroll.set_max_content_height(480); - scroll.set_propagate_natural_height(true); - - let list = ListBox::new(); - list.set_selection_mode(SelectionMode::Browse); - - for (idx, entry) in entries.iter().enumerate() { - let row = gtk4::ListBoxRow::new(); - if idx < STAGGER_ROWS { - row.add_css_class(&format!("stagger-{idx}")); - } - let hbox = GBox::new(Orientation::Horizontal, 0); - hbox.add_css_class("app-row"); - hbox.set_valign(gtk4::Align::Center); - - let well = GBox::new(Orientation::Horizontal, 0); - well.add_css_class("app-icon-well"); - well.set_size_request(38, 38); - well.set_halign(gtk4::Align::Center); - well.set_valign(gtk4::Align::Center); - well.set_hexpand(false); - let icon = make_icon(entry); - icon.set_halign(gtk4::Align::Center); - icon.set_valign(gtk4::Align::Center); - icon.set_hexpand(true); - well.append(&icon); - hbox.append(&well); - - let text = GBox::new(Orientation::Vertical, 1); - text.add_css_class("app-text"); - text.set_hexpand(true); - text.set_valign(gtk4::Align::Center); - - let name_lbl = Label::new(Some(&entry.name)); - name_lbl.add_css_class("app-name"); - name_lbl.set_xalign(0.0); - name_lbl.set_ellipsize(EllipsizeMode::End); - text.append(&name_lbl); - - let sub_lbl = Label::new(Some(category_label(entry))); - sub_lbl.add_css_class("app-muted"); - sub_lbl.set_xalign(0.0); - sub_lbl.set_ellipsize(EllipsizeMode::End); - text.append(&sub_lbl); - - hbox.append(&text); - row.set_child(Some(&hbox)); - unsafe { row.set_data("entry", entry.clone()) }; - unsafe { row.set_data("initial_order", idx as u32) }; - list.append(&row); - } - - // Sort by match quality + launch count when a query is active; - // fall back to insertion order (priority + launch frequency) when empty. - let sort_query = Rc::clone(&query_rc); - let sort_history = Rc::clone(&history_rc); - list.set_sort_func(move |row_a, row_b| { - let query = sort_query.borrow(); - if query.is_empty() { - let oa = unsafe { - row_a - .data::("initial_order") - .map_or(u32::MAX, |p| *p.as_ref()) - }; - let ob = unsafe { - row_b - .data::("initial_order") - .map_or(u32::MAX, |p| *p.as_ref()) - }; - return oa.cmp(&ob).into(); - } - let (Some(ea), Some(eb)) = (get_row_entry(row_a), get_row_entry(row_b)) else { - return std::cmp::Ordering::Equal.into(); - }; - let sa = fuzzy_score(&query, &ea); - let sb = fuzzy_score(&query, &eb); - let history = sort_history.borrow(); - let ca = history.count(&ea.name); - let cb = history.count(&eb.name); - sa.cmp(&sb) - .then(cb.cmp(&ca)) - .then(ea.name.to_lowercase().cmp(&eb.name.to_lowercase())) - .into() - }); - - if let Some(first) = list.row_at_index(0) { - list.select_row(Some(&first)); - } - - scroll.set_child(Some(&list)); - vbox.append(&scroll); + vbox.append(&results.scroller); + // Footer: "N applications" (liquid-motion) / "N results" + // (glass-workbench) — `[launcher].footer` selects the noun (see + // `footer_text`), updated alongside the query on every keystroke. let footer = Label::new(None); footer.add_css_class("launcher-footer"); footer.set_xalign(0.0); - set_footer_count(&footer, visible_row_count(&list)); + footer.set_label(&footer_text(&launcher.footer, visible_app_count(&results.list))); vbox.append(&footer); window.set_child(Some(&vbox)); - if !is_screenshot_run { - let vbox_open = vbox.clone(); - glib::timeout_add_local_once(Duration::from_millis(520), move || { - vbox_open.remove_css_class("just-opened"); - }); - } - - // Filter on keystroke. ListBox keeps row identity across sort, so the - // reorder is already a cheap FLIP analog; a short CSS fade is the extra. - let list_f = list.clone(); + // Filter on keystroke + let results_f = results.clone(); let footer_f = footer.clone(); - let vbox_f = vbox.clone(); - let filter_query = Rc::clone(&query_rc); - let reflow_gen = Rc::new(Cell::new(0u32)); + let footer_kind = launcher.footer.clone(); search.connect_changed(move |entry| { - let text = entry.text(); - let query = text.as_str(); - *filter_query.borrow_mut() = query.to_string(); - let mut i = 0i32; - while let Some(row) = list_f.row_at_index(i) { - let vis = get_row_entry(&row) - .map(|e| { - fuzzy_matches(query, &e.name) - || fuzzy_matches(query, category_label(&e)) - || e.wm_class - .as_deref() - .is_some_and(|w| fuzzy_matches(query, w)) - || fuzzy_matches(query, &e.exec) - }) - .unwrap_or(false); - row.set_visible(vis); - i += 1; - } - list_f.invalidate_sort(); - let first_vis = - (0i32..).find_map(|j| list_f.row_at_index(j).filter(|r| r.is_visible())); - list_f.select_row(first_vis.as_ref()); - set_footer_count(&footer_f, visible_row_count(&list_f)); - if !vbox_f.has_css_class("just-opened") { - list_f.remove_css_class("reflow"); - list_f.add_css_class("reflow"); - let gen = reflow_gen.get().wrapping_add(1); - reflow_gen.set(gen); - let list_fade = list_f.clone(); - let reflow_gen = Rc::clone(&reflow_gen); - glib::timeout_add_local_once(Duration::from_millis(180), move || { - if reflow_gen.get() == gen { - list_fade.remove_css_class("reflow"); - } - }); - } + results_f.set_query(entry.text().as_str()); + footer_f.set_label(&footer_text(&footer_kind, visible_app_count(&results_f.list))); }); // Keyboard handling — capture phase on window let key_ctrl = EventControllerKey::new(); key_ctrl.set_propagation_phase(gtk4::PropagationPhase::Capture); let close_k = Rc::clone(&close_all); - let list_k = list.clone(); - let history_k = Rc::clone(&history_rc); + let results_k = results.clone(); key_ctrl.connect_key_pressed(move |_, key, _, _| { use gtk4::gdk::Key; match key { @@ -662,22 +339,19 @@ fn run_ui( glib::Propagation::Stop } Key::Return | Key::KP_Enter => { - if let Some(row) = list_k.selected_row() { - if let Some(entry) = get_row_entry(&row) { - history_k.borrow_mut().increment(&entry.name); - history_k.borrow().save(); - do_launch(&entry); - close_k(); - } + if let Some(entry) = results_k.selected_entry() { + results_k.record_launch(&entry); + bread_launcher::do_launch(&entry, APP_ID, LAUNCHED_EVENT); + close_k(); } glib::Propagation::Stop } Key::Down => { - bread_utils::gtk_popup::select_next_visible(&list_k); + results_k.select_next(); glib::Propagation::Stop } Key::Up => { - bread_utils::gtk_popup::select_prev_visible(&list_k); + results_k.select_prev(); glib::Propagation::Stop } _ => glib::Propagation::Proceed, @@ -687,12 +361,11 @@ fn run_ui( // Row click launches let close_a = Rc::clone(&close_all); - let history_a = Rc::clone(&history_rc); - list.connect_row_activated(move |_, row| { - if let Some(entry) = get_row_entry(row) { - history_a.borrow_mut().increment(&entry.name); - history_a.borrow().save(); - do_launch(&entry); + let results_a = results.clone(); + results.list.connect_row_activated(move |_, row| { + if let Some(entry) = row_entry(row) { + results_a.record_launch(&entry); + bread_launcher::do_launch(&entry, APP_ID, LAUNCHED_EVENT); close_a(); } }); @@ -733,6 +406,32 @@ fn main() { let cli = screenshot::Cli::parse(); let screenshot_req = cli.screenshot_request(); + // Under an `[launcher] mode = "embedded"` theme (spotlight, + // THEME_SYSTEM_PLAN.md §7 phase 6c), breadbar's own bar-drawer capsule + // IS the launcher — mapping this binary's overlay window on top of it + // would stack a second launcher over the capsule, exactly the bug a + // keybind that still directly execs `breadbox` (see the CLAUDE.md task + // notes: `bash -c breadbox` in ~/.config/hypr/binds.json) would hit + // every time. `--screenshot` runs are exempt — those exist to capture + // THIS binary's own overlay for its own screenshot views regardless of + // whatever theme happens to be active on the machine running them. + if screenshot_req.is_none() + && crate::theme::shell_theme().launcher().mode + == bread_theme::shell::LauncherMode::Embedded + { + // `dispatch_embedded_open` only returns `true` once it has confirmed + // breadd itself is reachable (`BreadClient::health`) and emitted the + // redirect — see its doc comment for why that is the strongest + // guarantee this transport can give (there is no ack from breadbar). + // When breadd is unreachable, fall through to mapping breadbox's own + // overlay below instead of returning: a keybind press must always + // open *something*, even under an embedded theme, rather than + // silently doing nothing because the bus happens to be down. + if dispatch_embedded_open() { + return; + } + } + // `toggle_or_kill` kills whatever's holding the single-instance lock — // a real, already-running breadbox included. A screenshot run must // never touch it: it's a separate, disposable instance by design (same @@ -764,9 +463,70 @@ fn main() { .map(|c| c.priority.clone()) .unwrap_or_default(); - let history = LaunchHistory::load(); + let history = breadbox_shared::launch_history(); let manifest = load_manifest(); - let entries = load_sorted_entries(&manifest, &priority, &history); + let entries = bread_launcher::load_sorted_entries(&manifest, &priority, &history); run_ui(entries, history, screenshot_req); } + +/// Redirects an embedded-theme launch to the bus instead of mapping a +/// window (see the `main` call site). breadbar's capsule (spotlight) +/// subscribes to this and focuses/opens itself on receipt — see +/// `breadbar/src/launcher_command.rs`. +/// +/// Emits `bread.box.open_requested`, NOT `bread.command.box.open`. Two +/// reasons, one mechanical and one semantic: +/// +/// `BreadClient::emit` enforces that an app may only publish within its own +/// `bread..*` namespace (`bread_utils::bread_client`'s +/// `validate_app_namespace`). breadbox's app id is `box`, so a +/// `bread.command.*` event is refused outright and the redirect silently did +/// nothing but print a warning. +/// +/// The guard is right, and the original name had the direction backwards. +/// `bread.command..*` is a command addressed TO an app by an outside +/// trigger — it is what breadbox's own `listen` subscribes to. An app +/// emitting a command at itself would mean breadbox both sends and receives +/// the same verb, which is also how the respawn loop `listen.rs` guards +/// against arises. What actually happened here is an event: breadbox was +/// asked to open and is reporting that, so it belongs in breadbox's own +/// namespace as a past-tense fact. +/// +/// Fire-and-forget, same as every other `BreadClient::emit` here — breadd +/// being unreachable must never turn a keybind press into an error dialog or +/// a hung process. But "nobody was listening" used to also mean "the +/// keybind silently does nothing at all", which is the worst failure mode +/// available for a launcher: no window, no error, nothing on stderr. This +/// now checks reachability first (`BreadClient::health`, a real round trip +/// with a bounded timeout) so the `main` call site can fall back to mapping +/// breadbox's own overlay window when breadd itself is down — see its call +/// site. +/// +/// That fallback only covers "breadd is unreachable", not "breadd is up but +/// nobody is subscribed" (breadbar isn't running, or is running under a +/// non-Embedded theme and therefore never subscribed — +/// `launcher_command.rs::spawn`). `BreadClient`'s API has no way to ask "is +/// anything actually subscribed to this event" — `emit` has no ack and +/// `health` only reports breadd's own liveness — so that narrower gap can't +/// be closed without a bus-level ack protocol, which is out of scope here. +/// Returns `true` iff breadd was reachable and the redirect was sent. +fn dispatch_embedded_open() -> bool { + let client = bread_utils::bread_client::BreadClient::connect(APP_ID); + if client.health().is_none() { + eprintln!( + "breadbox: embedded launcher theme active but breadd is unreachable \ + (health check failed); falling back to breadbox's own overlay window \ + instead of a silent no-op" + ); + return false; + } + client.emit(EMBEDDED_OPEN_EVENT, serde_json::json!({})); + eprintln!( + "breadbox: embedded launcher theme active; redirected the open request to \ + '{EMBEDDED_OPEN_EVENT}' for breadbar's capsule to handle (breadd is \ + reachable, but this is fire-and-forget with no ack — if breadbar isn't \ + running or isn't subscribed, this is still a silent no-op on the bus)" + ); + true +} diff --git a/breadbox/src/theme.rs b/breadbox/src/theme.rs new file mode 100644 index 0000000..283eb72 --- /dev/null +++ b/breadbox/src/theme.rs @@ -0,0 +1,98 @@ +//! The active shell theme, loaded once per process (plan §5/§6, Phase 4b-i). +//! +//! Mirrors breadbar's `src/theme.rs::shell_theme()` accessor: the underlying +//! `bread_theme::shell::load()` call happens at most once per process (via +//! the thread-local cache below), not once per call site — every place in +//! this crate that needs launcher geometry/style reads through this shared +//! instance instead of calling `bread_theme::shell::load()` itself. +//! +//! Only `ShellTheme::launcher()` is consumed today; window-spec/tokens/slots +//! accessors exist on the type but breadbox has no bar/workspace/clock +//! chrome to drive from them. + +use bread_theme::shell::ShellTheme; +use std::cell::RefCell; +use std::rc::Rc; + +thread_local! { + static SHELL_THEME: RefCell> = + RefCell::new(Rc::new(bread_theme::shell::load())); +} + +/// The active shell theme. Loaded at most once per process; subsequent calls +/// just clone the cached `Rc`. +pub fn shell_theme() -> Rc { + SHELL_THEME.with(|cell| cell.borrow().clone()) +} + +/// Fallback margin (pixels) for any `[launcher] top` value this function +/// can't make sense of — breadbox's pre-theme hardcoded default. +const DEFAULT_TOP_MARGIN_PX: i32 = 120; + +/// Parses a `[launcher] top` value (e.g. `"120px"`) into whole pixels for +/// `Widget::set_margin_top`. Falls back to [`DEFAULT_TOP_MARGIN_PX`] — with a +/// logged reason — for any form this Phase 4b-i doesn't understand yet (a +/// bare percentage like theme 04's `"16%"`, or a malformed value in a +/// hand-edited theme) — per the manifest's "never fails to start" rule (plan +/// §4), a launcher window is not worth refusing to open over an unparseable +/// margin. +/// +/// A value that parses but is negative (e.g. `"-50px"`) is clamped to `0` +/// rather than passed straight through: `Widget::set_margin_top` requires a +/// non-negative margin, and passing it a negative one silently does nothing +/// useful (a `g_return_if_fail` on the GTK side) rather than the "push the +/// panel up off-screen" a theme author might expect — 0 (flush with the top +/// edge) is the closest sane interpretation. +pub fn top_margin_px(top: &str) -> i32 { + let trimmed = top.trim().trim_end_matches("px"); + match trimmed.parse::() { + Ok(px) if px < 0 => { + eprintln!( + "breadbox: [launcher] top = {top:?} parsed to a negative margin \ + ({px}px); clamping to 0" + ); + 0 + } + Ok(px) => px, + Err(_) => { + eprintln!( + "breadbox: [launcher] top = {top:?} is not a plain pixel value \ + (e.g. \"120px\"); falling back to the default \ + {DEFAULT_TOP_MARGIN_PX}px margin" + ); + DEFAULT_TOP_MARGIN_PX + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn plain_pixel_value() { + assert_eq!(top_margin_px("120px"), 120); + } + + #[test] + fn bare_number_without_unit_suffix() { + assert_eq!(top_margin_px("120"), 120); + } + + #[test] + fn percentage_form_falls_back_to_default() { + // The doc comment's own anticipated edge case (theme 04's original + // "16%" idea) — not parseable as a plain pixel count today. + assert_eq!(top_margin_px("16%"), DEFAULT_TOP_MARGIN_PX); + } + + #[test] + fn garbage_falls_back_to_default() { + assert_eq!(top_margin_px("not-a-length"), DEFAULT_TOP_MARGIN_PX); + } + + #[test] + fn negative_value_clamps_to_zero() { + assert_eq!(top_margin_px("-50px"), 0); + } +}