Compare commits

...

26 commits

Author SHA1 Message Date
Breadway
47dba6e34a Bump bread-theme to v0.2.8 (live-reload fix)
All checks were successful
Mirror to GitHub / mirror (push) Successful in 51s
Build and publish package / package (push) Successful in 3m15s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 12:55:47 +08:00
Breadway
d2ee6e325f Fix illegible text on light pywal palettes + hot-reload
All checks were successful
Mirror to GitHub / mirror (push) Successful in 31s
Use bread-theme 0.2.7's luminance-picked ink (@on-*) for text on coloured
backgrounds: the active workspace pill and notification cards previously kept the
pywal foreground, which vanished when those slots came out light. Drop the
blanket label colour rule (it overrode the per-surface ink on child labels).

Switch to bread_theme::gtk::apply_app_css so the bar recolours live on
`bread-theme reload` instead of only at startup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 12:40:52 +08:00
Breadway
2b6ad92082 Release 0.1.7: shared bread-theme stylesheet
All checks were successful
Mirror to GitHub / mirror (push) Successful in 55s
Build and publish package / package (push) Successful in 3m7s
Pin bread-theme v0.2.6 and load the shared ecosystem stylesheet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 18:31:18 +08:00
Breadway
901b3a3d9f theme: load the shared bread-theme stylesheet
All checks were successful
Mirror to GitHub / mirror (push) Successful in 55s
Call bread_theme::gtk::apply_shared() before breadbar's own rules so fonts,
palette, and generic widgets come from the one ecosystem stylesheet (and
recolour live). Keep only breadbar-specific CSS (bar window, workspace
buttons, stats, notifications, OSD). Bump bread-theme dep to v0.2.6.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 16:56:52 +08:00
Breadway
f9fa83e8a2 Default workspace-button rounding to 0
Some checks failed
Mirror to GitHub / mirror (push) Failing after 3s
Build and publish package / package (push) Successful in 2m55s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 18:51:42 +08:00
Breadway
ae39fb3ce6 Embed SVG assets and rasterise with resvg
All checks were successful
Mirror to GitHub / mirror (push) Successful in 1m28s
Build and publish package / package (push) Successful in 2m40s
The packaged binary panicked on startup ("svg load: Unrecognized image file
format"): asset SVGs were referenced by their build-time CARGO_MANIFEST_DIR
path (absent on an installed system, so read_to_string returned empty bytes),
and gdk::Texture::from_bytes can no longer decode SVG since librsvg dropped its
gdk-pixbuf loader.

- include_str! the SVGs into the binary (no runtime asset files)
- rasterise via resvg/tiny-skia into a gdk::MemoryTexture (no system loader)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 19:36:38 +08:00
Breadway
60fef9751e Disable debug package so the main package publishes correctly
All checks were successful
Mirror to GitHub / mirror (push) Successful in 2m3s
Build and publish package / package (push) Successful in 2m56s
makepkg's debug split produced a -debug pkg; the upload's head -1 could
grab it instead of the main package. !debug yields a single package.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 23:00:49 +08:00
Breadway
fb52d67f21 Use REGISTRY_TOKEN (scoped write:package) for registry publish
All checks were successful
Mirror to GitHub / mirror (push) Successful in 1m32s
Build and publish package / package (push) Successful in 3m22s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 22:55:40 +08:00
Breadway
22cc84ce90 Disable LTO in PKGBUILD (vendored ring/mlua static libs vs makepkg -flto)
All checks were successful
Mirror to GitHub / mirror (push) Successful in 1m31s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 17:06:54 +08:00
Breadway
b528706ef1 Clone from public URL, not GITHUB_SERVER_URL (resolves to localhost in runner)
All checks were successful
Mirror to GitHub / mirror (push) Successful in 1m25s
The Forgejo runner injects GITHUB_SERVER_URL as http://localhost:3002, which
is unreachable from inside the job container. Use the public URL instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 16:14:13 +08:00
Breadway
482348e3cf Rename mirror secret to MIRROR_TOKEN (GITHUB_ prefix is reserved)
Some checks failed
Mirror to GitHub / mirror (push) Failing after 1s
Forgejo/gitea rejects user secret names starting with GITHUB_.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 16:10:49 +08:00
Breadway
f2de616522 Fix Forgejo workflows for the actual server capabilities
Some checks failed
Mirror to GitHub / mirror (push) Failing after 1s
- package.yml: correct Arch registry upload (octet-stream + binary body),
  drop --privileged, manual shell clone (archlinux image has no Node),
  built-in Actions token, --nocheck
- mirror.yml: clone --mirror + explicit refs push with --prune

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 16:02:07 +08:00
Breadway
be74c6f0a5 Add packaging/arch PKGBUILD and Forgejo Actions workflows
Some checks failed
Mirror to GitHub / mirror (push) Failing after 34s
- packaging/arch/PKGBUILD: builds and publishes breadbar to [breadway] repo
- .forgejo/workflows/mirror.yml: mirrors every push/tag to GitHub
- .forgejo/workflows/package.yml: builds on tag, publishes to Forgejo registry

Requires FORGEJO_TOKEN and GITHUB_MIRROR_TOKEN secrets in Forgejo.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 12:12:41 +08:00
Breadway
e270cde5da feat: add system tray (StatusNotifierWatcher / SNI)
Some checks failed
release / build (push) Failing after 45s
Implements org.kde.StatusNotifierWatcher as a D-Bus service so apps
like Nextcloud can register their tray icons. Icons are rendered from
SNI ARGB pixmaps (falling back to icon-name theme lookup), click calls
Activate(0,0), and NameOwnerChanged cleans up ghost icons when an app
exits. Styling follows the Bread Design System (4px tertiary radius,
xs/sm spacing, opacity transitions).

Also fixes a latent infinite-loop risk in osd.rs (.flatten → .map_while)
and syncs the notifications server version string to CARGO_PKG_VERSION.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 22:31:25 +08:00
Breadway
4b262fce9e chore: update Cargo.lock for v0.1.1
Some checks failed
release / build (push) Failing after 57s
2026-06-11 14:27:58 +08:00
Breadway
b8d2b09fa5 chore: bump version to 0.1.1 2026-06-11 14:21:28 +08:00
Breadway
250d6dd5d3 fix: add missing libpulse dep, add optional_system_deps
libpulse (pactl) was missing — breadbar shells pactl 3x for volume.
Optional: hyprland (workspace display, not a linked dep).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 13:37:40 +08:00
Breadway
8097330bf4 fix: use relative symlink for latest to work inside Docker containers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 09:02:38 +08:00
Breadway
2e23a4f5ce fix: add contents: write permission for GitHub Release creation
Some checks failed
release / build (push) Failing after 37s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 00:00:47 +08:00
Breadway
1d583b58af fix: create GitHub Release before uploading artifacts 2026-06-06 23:52:39 +08:00
Breadway
691d458a70 fix: switch bread-theme to git dep (v0.1.0) for CI 2026-06-06 23:26:20 +08:00
Breadway
d152af02f6 fix: add missing build deps for hestia (Ubuntu) runner 2026-06-06 23:19:53 +08:00
Breadway
65274d0dd2 fix: use apt-get on hestia runner (Ubuntu, not Arch) 2026-06-06 22:47:41 +08:00
Breadway
9e829d3663 Refactor theme onto bread-theme; add bakery.toml and release workflow
- Cargo.toml: depend on bread-theme (path dep for local dev, git dep for
  production) with gtk feature; remove local theme dependencies
- src/theme.rs: replace local pywal/Catppuccin impl with bread_theme::gtk
  helpers; local bar-specific CSS is preserved
- bakery.toml: describes breadbar for bakery install
- release.yml: builds on hestia self-hosted runner, publishes binary to
  dl.breadway.dev and GitHub Releases on v* tags

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 22:31:10 +08:00
Breadway
9b9705520e Fixes 2026-05-19 12:31:26 +08:00
Breadway
9ed275b6c5 Prepare repo for public GitHub release
Add README, MIT LICENSE, expanded .gitignore, and updated SVG icon set
to make the repository presentable for open-source publication.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 12:30:12 +08:00
29 changed files with 1696 additions and 239 deletions

View file

@ -0,0 +1,21 @@
name: Mirror to GitHub
on:
push:
branches: ['**']
tags: ['**']
jobs:
mirror:
runs-on: [self-hosted, hestia]
steps:
- name: Mirror to GitHub
run: |
set -euo pipefail
git clone --mirror "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" repo.git
cd repo.git
# Mirror only branches and tags (not refs/pull/*, which GitHub rejects);
# --prune deletes GitHub refs that no longer exist on Forgejo.
git push --prune \
"https://x-access-token:${{ secrets.MIRROR_TOKEN }}@github.com/Breadway/breadbar.git" \
'+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*'

View file

@ -0,0 +1,40 @@
name: Build and publish package
on:
push:
tags: ['v*']
jobs:
package:
runs-on: [self-hosted, hestia]
container:
image: archlinux:latest
steps:
# Note: no actions/checkout — the archlinux image has no Node, which JS
# actions require. Everything runs as shell steps and clones manually.
- name: Build and publish
env:
PUBLISH_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
set -euo pipefail
VERSION="${GITHUB_REF_NAME#v}"
pacman -Syu --noconfirm base-devel git rust cargo gtk4 gtk4-layer-shell libpulse iw
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="breadbar-${VERSION}/" HEAD \
> packaging/arch/breadbar-${VERSION}.tar.gz
SHA=$(sha256sum packaging/arch/breadbar-${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"

61
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,61 @@
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 libdbus-1-dev pkg-config iw 2>/dev/null || true
- name: build
run: cargo build --release --locked
- name: prepare artifacts
run: |
VERSION="${GITHUB_REF_NAME#v}"
PKG_DIR="${DL_DIR}/breadbar/${VERSION}"
mkdir -p "${PKG_DIR}"
cp target/release/breadbar "${PKG_DIR}/breadbar-x86_64"
strip "${PKG_DIR}/breadbar-x86_64"
sha256sum "${PKG_DIR}/breadbar-x86_64" | awk '{print $1}' \
> "${PKG_DIR}/breadbar-x86_64.sha256"
cp bakery.toml "${PKG_DIR}/bakery.toml"
ln -sfn "${VERSION}" "${DL_DIR}/breadbar/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}/breadbar/${VERSION}"
gh release create "${GITHUB_REF_NAME}" \
--title "breadbar v${VERSION}" --generate-notes 2>/dev/null || true
gh release upload "${GITHUB_REF_NAME}" \
"${PKG_DIR}/breadbar-x86_64" \
"${PKG_DIR}/breadbar-x86_64.sha256" \
--clobber

37
.gitignore vendored
View file

@ -1,2 +1,37 @@
/target # Build artifacts
/target/
# Editor files
.vscode/
.idea/
*.swp
*.swo
*~
# OS artifacts
.DS_Store
Thumbs.db
desktop.ini
# Environment / secrets
.env
.env.*
*.env
*.pem
*.key
*.p12
secrets/
# Log files
*.log
logs/
# Runtime files
*.sock
*.pid
# Claude Code session data
.claude/
# Internal design documents (not for distribution)
aster-brief.md aster-brief.md

579
Cargo.lock generated
View file

@ -2,12 +2,30 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 version = 4
[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.102" version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "arrayref"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]] [[package]]
name = "async-broadcast" name = "async-broadcast"
version = "0.7.2" version = "0.7.2"
@ -66,25 +84,50 @@ dependencies = [
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.5.0" version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.11.1" version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
[[package]]
name = "bread-theme"
version = "0.2.3"
source = "git+https://github.com/Breadway/bread-ecosystem?tag=v0.2.8#77417d552130281ff787e07d52541eb25e9d533b"
dependencies = [
"dirs",
"gtk4",
"serde",
"serde_json",
]
[[package]] [[package]]
name = "breadbar" name = "breadbar"
version = "0.1.0" version = "0.1.7"
dependencies = [ dependencies = [
"bread-theme",
"futures-lite", "futures-lite",
"gtk4", "gtk4",
"gtk4-layer-shell", "gtk4-layer-shell",
"hyprland", "hyprland",
"relm4", "relm4",
"resvg",
"serde", "serde",
"serde_json", "serde_json",
"tokio", "tokio",
@ -93,9 +136,15 @@ dependencies = [
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.20.2" version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
name = "bytemuck"
version = "1.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
[[package]] [[package]]
name = "bytes" name = "bytes"
@ -109,7 +158,7 @@ version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cc8d9aa793480744cd9a0524fef1a2e197d9eaa0f739cde19d16aba530dcb95" checksum = "5cc8d9aa793480744cd9a0524fef1a2e197d9eaa0f739cde19d16aba530dcb95"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.13.0",
"cairo-sys-rs", "cairo-sys-rs",
"glib", "glib",
"libc", "libc",
@ -128,9 +177,9 @@ dependencies = [
[[package]] [[package]]
name = "cfg-expr" name = "cfg-expr"
version = "0.20.7" version = "0.20.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c6b04e07d8080154ed4ac03546d9a2b303cc2fe1901ba0b35b301516e289368" checksum = "fb693542bcafa528e198be0ebd9d3632ca5b7c93dbe7237460e199910835997c"
dependencies = [ dependencies = [
"smallvec", "smallvec",
"target-lexicon", "target-lexicon",
@ -160,12 +209,27 @@ dependencies = [
"unicode-segmentation", "unicode-segmentation",
] ]
[[package]]
name = "crc32fast"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.21" version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "data-url"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
[[package]] [[package]]
name = "derive_more" name = "derive_more"
version = "2.1.1" version = "2.1.1"
@ -190,10 +254,31 @@ dependencies = [
] ]
[[package]] [[package]]
name = "either" name = "dirs"
version = "1.15.0" version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-sys"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.48.0",
]
[[package]]
name = "either"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
[[package]] [[package]]
name = "endi" name = "endi"
@ -235,7 +320,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys", "windows-sys 0.61.2",
]
[[package]]
name = "euclid"
version = "0.22.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06"
dependencies = [
"num-traits",
] ]
[[package]] [[package]]
@ -268,6 +362,15 @@ dependencies = [
"getrandom 0.3.4", "getrandom 0.3.4",
] ]
[[package]]
name = "fdeflate"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
dependencies = [
"simd-adler32",
]
[[package]] [[package]]
name = "field-offset" name = "field-offset"
version = "0.3.6" version = "0.3.6"
@ -278,6 +381,22 @@ dependencies = [
"rustc_version", "rustc_version",
] ]
[[package]]
name = "flate2"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "float-cmp"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
[[package]] [[package]]
name = "flume" name = "flume"
version = "0.12.0" version = "0.12.0"
@ -464,6 +583,17 @@ dependencies = [
"system-deps", "system-deps",
] ]
[[package]]
name = "getrandom"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.3.4" version = "0.3.4"
@ -518,7 +648,7 @@ dependencies = [
"gobject-sys", "gobject-sys",
"libc", "libc",
"system-deps", "system-deps",
"windows-sys", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@ -547,7 +677,7 @@ version = "0.22.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c207e04e51605dcf7b2924c41591b3a10e1438eaac5bcf448fb91f325381104a" checksum = "c207e04e51605dcf7b2924c41591b3a10e1438eaac5bcf448fb91f325381104a"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.13.0",
"futures-channel", "futures-channel",
"futures-core", "futures-core",
"futures-executor", "futures-executor",
@ -676,7 +806,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4069987ff4793699511a251028cc336b438e46565b463f111250148d574752a" checksum = "a4069987ff4793699511a251028cc336b438e46565b463f111250148d574752a"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.13.0",
"gdk4", "gdk4",
"glib", "glib",
"glib-sys", "glib-sys",
@ -791,6 +921,12 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
[[package]]
name = "imagesize"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285"
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.14.0" version = "2.14.0"
@ -811,9 +947,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.98" version = "0.3.99"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"futures-util", "futures-util",
@ -827,6 +963,17 @@ version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
[[package]]
name = "kurbo"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62"
dependencies = [
"arrayvec",
"euclid",
"smallvec",
]
[[package]] [[package]]
name = "leb128fmt" name = "leb128fmt"
version = "0.1.0" version = "0.1.0"
@ -839,6 +986,15 @@ version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "libredox"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.12.1" version = "0.12.1"
@ -856,15 +1012,15 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.29" version = "0.4.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.8.0" version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
[[package]] [[package]]
name = "memoffset" name = "memoffset"
@ -876,14 +1032,33 @@ dependencies = [
] ]
[[package]] [[package]]
name = "mio" name = "miniz_oxide"
version = "1.2.0" version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
"simd-adler32",
]
[[package]]
name = "mio"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
dependencies = [ dependencies = [
"libc", "libc",
"wasi", "wasi",
"windows-sys", "windows-sys 0.61.2",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
] ]
[[package]] [[package]]
@ -892,6 +1067,12 @@ version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]] [[package]]
name = "ordered-stream" name = "ordered-stream"
version = "0.2.0" version = "0.2.0"
@ -932,35 +1113,18 @@ version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
[[package]]
name = "parking_lot"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-link",
]
[[package]] [[package]]
name = "pastey" name = "pastey"
version = "0.1.1" version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
[[package]]
name = "pico-args"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.17" version = "0.2.17"
@ -973,6 +1137,19 @@ version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
[[package]]
name = "png"
version = "0.17.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
dependencies = [
"bitflags 1.3.2",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide",
]
[[package]] [[package]]
name = "prettyplease" name = "prettyplease"
version = "0.2.37" version = "0.2.37"
@ -1023,12 +1200,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]] [[package]]
name = "redox_syscall" name = "redox_users"
version = "0.5.18" version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
dependencies = [ dependencies = [
"bitflags", "getrandom 0.2.17",
"libredox",
"thiserror",
] ]
[[package]] [[package]]
@ -1065,6 +1244,35 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "resvg"
version = "0.44.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a325d5e8d1cebddd070b13f44cec8071594ab67d1012797c121f27a669b7958"
dependencies = [
"log",
"pico-args",
"rgb",
"svgtypes",
"tiny-skia",
"usvg",
]
[[package]]
name = "rgb"
version = "0.8.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
dependencies = [
"bytemuck",
]
[[package]]
name = "roxmltree"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
[[package]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.4.1" version = "0.4.1"
@ -1080,11 +1288,11 @@ version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.13.0",
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
"windows-sys", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@ -1137,9 +1345,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.149" version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@ -1178,6 +1386,27 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "simd-adler32"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
[[package]]
name = "simplecss"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c"
dependencies = [
"log",
]
[[package]]
name = "siphasher"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.12" version = "0.4.12"
@ -1192,12 +1421,12 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.6.3" version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@ -1209,6 +1438,25 @@ dependencies = [
"lock_api", "lock_api",
] ]
[[package]]
name = "strict-num"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
dependencies = [
"float-cmp",
]
[[package]]
name = "svgtypes"
version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc"
dependencies = [
"kurbo",
"siphasher",
]
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.117" version = "2.0.117"
@ -1235,9 +1483,9 @@ dependencies = [
[[package]] [[package]]
name = "target-lexicon" name = "target-lexicon"
version = "0.13.3" version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
[[package]] [[package]]
name = "tempfile" name = "tempfile"
@ -1249,7 +1497,53 @@ dependencies = [
"getrandom 0.4.2", "getrandom 0.4.2",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys", "windows-sys 0.61.2",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tiny-skia"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab"
dependencies = [
"arrayref",
"arrayvec",
"bytemuck",
"cfg-if",
"log",
"png",
"tiny-skia-path",
]
[[package]]
name = "tiny-skia-path"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93"
dependencies = [
"arrayref",
"bytemuck",
"strict-num",
] ]
[[package]] [[package]]
@ -1261,13 +1555,12 @@ dependencies = [
"bytes", "bytes",
"libc", "libc",
"mio", "mio",
"parking_lot",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry", "signal-hook-registry",
"socket2", "socket2",
"tokio-macros", "tokio-macros",
"tracing", "tracing",
"windows-sys", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@ -1307,9 +1600,9 @@ dependencies = [
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.25.11+spec-1.1.0" version = "0.25.12+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"toml_datetime", "toml_datetime",
@ -1371,7 +1664,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [ dependencies = [
"memoffset", "memoffset",
"tempfile", "tempfile",
"windows-sys", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@ -1382,9 +1675,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]] [[package]]
name = "unicode-segmentation" name = "unicode-segmentation"
version = "1.13.2" version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
[[package]] [[package]]
name = "unicode-xid" name = "unicode-xid"
@ -1393,10 +1686,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]] [[package]]
name = "uuid" name = "usvg"
version = "1.23.1" version = "0.44.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" checksum = "7447e703d7223b067607655e625e0dbca80822880248937da65966194c4864e6"
dependencies = [
"base64",
"data-url",
"flate2",
"imagesize",
"kurbo",
"log",
"pico-args",
"roxmltree",
"simplecss",
"siphasher",
"strict-num",
"svgtypes",
"tiny-skia-path",
"xmlwriter",
]
[[package]]
name = "uuid"
version = "1.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"serde_core", "serde_core",
@ -1435,9 +1750,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.121" version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
@ -1448,9 +1763,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.121" version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@ -1458,9 +1773,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.121" version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"proc-macro2", "proc-macro2",
@ -1471,9 +1786,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.121" version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -1506,7 +1821,7 @@ version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.13.0",
"hashbrown 0.15.5", "hashbrown 0.15.5",
"indexmap", "indexmap",
"semver", "semver",
@ -1518,6 +1833,15 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.61.2" version = "0.61.2"
@ -1527,6 +1851,63 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "1.0.3" version = "1.0.3"
@ -1600,7 +1981,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bitflags", "bitflags 2.13.0",
"indexmap", "indexmap",
"log", "log",
"serde", "serde",
@ -1637,10 +2018,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
[[package]] [[package]]
name = "zbus" name = "xmlwriter"
version = "5.15.0" version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3bcbf15c8708d7fc1be0c993622e0a5cbd5e8b52bfa40afa4c3e0cd8d724ac1" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
[[package]]
name = "zbus"
version = "5.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285"
dependencies = [ dependencies = [
"async-broadcast", "async-broadcast",
"async-recursion", "async-recursion",
@ -1659,7 +2046,7 @@ dependencies = [
"tracing", "tracing",
"uds_windows", "uds_windows",
"uuid", "uuid",
"windows-sys", "windows-sys 0.61.2",
"winnow", "winnow",
"zbus_macros", "zbus_macros",
"zbus_names", "zbus_names",
@ -1668,9 +2055,9 @@ dependencies = [
[[package]] [[package]]
name = "zbus_macros" name = "zbus_macros"
version = "5.15.0" version = "5.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51fa5406ad9175a8c825a931f8cf347116b531b3634fcb0b627c290f1f2516ff" checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6"
dependencies = [ dependencies = [
"proc-macro-crate", "proc-macro-crate",
"proc-macro2", "proc-macro2",
@ -1700,9 +2087,9 @@ checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
[[package]] [[package]]
name = "zvariant" name = "zvariant"
version = "5.11.0" version = "5.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c1567a6ec68df868cbbfde844cfc6d81649fe5109a62b116b19fabd53e618ee" checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0"
dependencies = [ dependencies = [
"endi", "endi",
"enumflags2", "enumflags2",
@ -1714,9 +2101,9 @@ dependencies = [
[[package]] [[package]]
name = "zvariant_derive" name = "zvariant_derive"
version = "5.11.0" version = "5.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7d5b780599bbde114e39d9a0799577fad1ced5105d38515745f7b3099d8ceda" checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737"
dependencies = [ dependencies = [
"proc-macro-crate", "proc-macro-crate",
"proc-macro2", "proc-macro2",
@ -1727,9 +2114,9 @@ dependencies = [
[[package]] [[package]]
name = "zvariant_utils" name = "zvariant_utils"
version = "3.3.1" version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d464f5733ffa07a3164d656f18533caace9d0638596721355d73256a410d691" checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View file

@ -1,15 +1,30 @@
[package] [package]
name = "breadbar" name = "breadbar"
version = "0.1.0" version = "0.1.7"
edition = "2021" edition = "2021"
description = "Minimal status bar and notification daemon for Hyprland on Wayland"
license = "MIT"
authors = ["Breadway <rileyhorsham@gmail.com>"]
repository = "https://github.com/breadway/breadbar"
keywords = ["wayland", "hyprland", "bar", "status-bar", "gtk4"]
categories = ["gui"]
[dependencies] [dependencies]
bread-theme = { git = "https://github.com/Breadway/bread-ecosystem", tag = "v0.2.8", features = ["gtk"] }
gtk4 = { version = "0.11", features = ["v4_12"] } gtk4 = { version = "0.11", features = ["v4_12"] }
gtk4-layer-shell = "0.8" gtk4-layer-shell = "0.8"
relm4 = { version = "0.11", features = ["macros"] } relm4 = { version = "0.11", features = ["macros"] }
hyprland = { version = "0.4.0-beta.3", features = ["tokio"] } hyprland = { version = "0.4.0-beta.3", features = ["tokio"] }
futures-lite = "2" futures-lite = "2"
zbus = { version = "5", default-features = false, features = ["tokio"] } zbus = { version = "5", default-features = false, features = ["tokio"] }
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["rt-multi-thread", "macros", "time", "process", "signal", "sync"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
# Pure-Rust SVG rasteriser (default features off → no text/font deps; the icons
# are vector-only). Needed because librsvg dropped its gdk-pixbuf SVG loader.
resvg = { version = "0.44", default-features = false }
[profile.release]
lto = "thin"
codegen-units = 1
strip = "symbols"

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Riley Horsham
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

117
README.md Normal file
View file

@ -0,0 +1,117 @@
# breadbar
Minimal status bar and notification daemon for [Hyprland](https://hyprland.org/) on Wayland.
A single Rust binary that provides a full-width top bar, a system tray, and a standards-compliant D-Bus notification daemon. No launcher, no wallpaper logic.
## Features
**Status bar** (anchored to the top of every monitor via `gtk4-layer-shell`):
- Left: live workspace buttons sourced from Hyprland IPC, active workspace highlighted
- Centre: clock (`HH:MM`, updates at the top of each minute)
- Right: CPU%, RAM, power draw (W), battery level + AC indicator, Bluetooth state, WiFi SSID with signal strength, system tray (SNI)
**Notification daemon**:
- Implements `org.freedesktop.Notifications` (D-Bus) — works with any standard notification sender (`notify-send`, etc.)
- Popups appear top-right, stack vertically, auto-dismiss after the sender-specified timeout (default 5 s)
- Supports `CloseNotification`
**Theming**:
- Reads `~/.cache/wal/colors.json` (pywal) on startup for a palette that matches your wallpaper
- Falls back to a Catppuccin Mocha palette if pywal is not present
- User CSS override: `~/.config/breadbar/style.css`
- Send `SIGHUP` to reload the theme at runtime (integrates with wallpaper-change hooks)
## Dependencies
Runtime:
- GTK4 (≥ 4.12)
- `gtk4-layer-shell`
- `iw` — for WiFi SSID/signal (`iw dev <iface> link`)
- A running Hyprland compositor
- D-Bus session bus
Bluetooth status is read from `/sys/class/rfkill` and BlueZ D-Bus; it degrades gracefully if unavailable.
## Building
```sh
cargo build --release
```
The binary is at `target/release/breadbar`.
Requirements: Rust 1.77+ (uses `LazyLock`), a GTK4 development environment (`libgtk-4-dev` / `gtk4` package).
On Arch Linux:
```sh
sudo pacman -S gtk4 gtk4-layer-shell iw
cargo build --release
```
## Running
```sh
./target/release/breadbar
```
Typically launched from your Hyprland config:
```
exec-once = /path/to/breadbar
```
breadbar claims `org.freedesktop.Notifications` on the session D-Bus on startup. If another notification daemon is already running, startup will fail — stop the other daemon first.
## Theming
### pywal integration
breadbar reads `~/.cache/wal/colors.json` automatically. To reload after a wallpaper change:
```sh
pkill -HUP breadbar
```
Or hook it into your wallpaper script:
```sh
wal -i /path/to/wallpaper.jpg
pkill -HUP breadbar
```
### Custom CSS
Drop a `~/.config/breadbar/style.css` file and send `SIGHUP` to reload. This CSS is applied at a higher priority than the pywal palette so you can override anything.
Example — change the font size:
```css
* {
font-size: 13px;
}
```
## Architecture
| Module | Responsibility |
|---|---|
| `src/main.rs` | GTK4 app entry point, widget tree, `relm4` component |
| `src/bar/workspaces.rs` | Hyprland IPC event stream, workspace buttons |
| `src/bar/clock.rs` | Minute-tick clock |
| `src/bar/stats.rs` | Polling loop: CPU, RAM, power, battery, Bluetooth, WiFi |
| `src/bar/tray.rs` | `org.kde.StatusNotifierWatcher` D-Bus service, SNI item rendering |
| `src/notifications/mod.rs` | `org.freedesktop.Notifications` zbus service |
| `src/notifications/popup.rs` | Layer-shell popup window and card stack |
| `src/theme.rs` | pywal reader, GTK CSS provider injection |
Stats are polled every 2 seconds. Bluetooth and WiFi are sampled every 16 seconds and cached in between to avoid hammering D-Bus and `iw`.
## License
MIT

1
assets/AC Power.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-plug-zap-icon lucide-plug-zap"><path d="M6.3 20.3a2.4 2.4 0 0 0 3.4 0L12 18l-6-6-2.3 2.3a2.4 2.4 0 0 0 0 3.4Z"/><path d="m2 22 3-3"/><path d="M7.5 13.5 10 11"/><path d="M10.5 16.5 13 14"/><path d="m18 3-4 4h6l-4 4"/></svg>

After

Width:  |  Height:  |  Size: 424 B

1
assets/Battery 1 Bar.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-battery-low-icon lucide-battery-low"><path d="M22 14v-4"/><path d="M6 14v-4"/><rect x="2" y="6" width="16" height="12" rx="2"/></svg>

After

Width:  |  Height:  |  Size: 335 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-battery-medium-icon lucide-battery-medium"><path d="M10 14v-4"/><path d="M22 14v-4"/><path d="M6 14v-4"/><rect x="2" y="6" width="16" height="12" rx="2"/></svg>

After

Width:  |  Height:  |  Size: 362 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-battery-full-icon lucide-battery-full"><path d="M10 10v4"/><path d="M14 10v4"/><path d="M22 14v-4"/><path d="M6 10v4"/><rect x="2" y="6" width="16" height="12" rx="2"/></svg>

After

Width:  |  Height:  |  Size: 376 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bluetooth-connected-icon lucide-bluetooth-connected"><path d="m7 7 10 10-5 5V2l5 5L7 17"/><line x1="18" x2="21" y1="12" y2="12"/><line x1="3" x2="6" y1="12" y2="12"/></svg>

After

Width:  |  Height:  |  Size: 374 B

1
assets/Bluetooth Off.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bluetooth-off-icon lucide-bluetooth-off"><path d="m17 17-5 5V12l-5 5"/><path d="m2 2 20 20"/><path d="M14.5 9.5 17 7l-5-5v4.5"/></svg>

After

Width:  |  Height:  |  Size: 336 B

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-battery-icon lucide-battery"><path d="M 22 14 L 22 10"/><rect x="2" y="6" width="16" height="12" rx="2"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bluetooth-icon lucide-bluetooth"><path d="m7 7 10 10-5 5V2l5 5L7 17"/></svg>

Before

Width:  |  Height:  |  Size: 313 B

After

Width:  |  Height:  |  Size: 278 B

Before After
Before After

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-wifi-off-icon lucide-wifi-off"><path d="M12 20h.01"/><path d="M8.5 16.429a5 5 0 0 1 7 0"/><path d="M5 12.859a10 10 0 0 1 5.17-2.69"/><path d="M19 12.859a10 10 0 0 0-2.007-1.523"/><path d="M2 8.82a15 15 0 0 1 4.177-2.643"/><path d="M22 8.82a15 15 0 0 0-11.288-3.764"/><path d="m2 2 20 20"/></svg>

After

Width:  |  Height:  |  Size: 497 B

12
bakery.toml Normal file
View file

@ -0,0 +1,12 @@
name = "breadbar"
description = "Minimal status bar and notification daemon for Hyprland"
binaries = ["breadbar"]
system_deps = ["gtk4", "gtk4-layer-shell", "iw", "libpulse"]
optional_system_deps = ["hyprland"]
bread_deps = []
[config]
dir = "~/.config/breadbar"
[install]
post_install = []

36
packaging/arch/PKGBUILD Normal file
View file

@ -0,0 +1,36 @@
# Maintainer: Breadway <rileyhorsham@gmail.com>
pkgname=breadbar
pkgver=0.1.0
pkgrel=1
pkgdesc="Minimal status bar and notification daemon for Hyprland"
arch=('x86_64')
url="https://github.com/Breadway/breadbar"
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' 'libpulse' 'iw')
optdepends=(
'hyprland: workspace and window data 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
}
package() {
cd "${srcdir}/${pkgname}-${pkgver}"
install -Dm755 target/release/breadbar "${pkgdir}/usr/bin/breadbar"
install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
}

View file

@ -3,7 +3,9 @@ use relm4::ComponentSender;
pub fn current() -> String { pub fn current() -> String {
let dt = gtk4::glib::DateTime::now_local().expect("local time"); let dt = gtk4::glib::DateTime::now_local().expect("local time");
format!("{:02}:{:02}", dt.hour(), dt.minute()) let date = dt.format("%a %d/%m").expect("date format");
let time = format!("{:02}:{:02}", dt.hour(), dt.minute());
format!("{} {}", date, time)
} }
pub fn spawn_ticker(sender: ComponentSender<App>) { pub fn spawn_ticker(sender: ComponentSender<App>) {
@ -11,8 +13,7 @@ pub fn spawn_ticker(sender: ComponentSender<App>) {
loop { loop {
sender.input(AppInput::ClockTick); sender.input(AppInput::ClockTick);
// Sleep until the top of the next minute — display is HH:MM only. // Sleep until the top of the next minute — display is HH:MM only.
let secs = gtk4::glib::DateTime::now_local() let secs = gtk4::glib::DateTime::now_local().map_or(0, |dt| dt.second());
.map_or(0, |dt| dt.second());
let wait = (60 - secs.rem_euclid(60)) as u64; let wait = (60 - secs.rem_euclid(60)) as u64;
tokio::time::sleep(std::time::Duration::from_secs(wait.max(1))).await; tokio::time::sleep(std::time::Duration::from_secs(wait.max(1))).await;
} }

View file

@ -1,3 +1,4 @@
pub mod clock; pub mod clock;
pub mod stats; pub mod stats;
pub mod tray;
pub mod workspaces; pub mod workspaces;

View file

@ -8,11 +8,32 @@ use std::{
LazyLock, Mutex, OnceLock, LazyLock, Mutex, OnceLock,
}, },
}; };
use tokio::sync::OnceCell as AsyncOnce;
pub const WIFI_STRONG: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/WiFi Strong.svg"); static WIFI_IFACE: OnceLock<Option<String>> = OnceLock::new();
pub const WIFI_MEDIUM: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/WiFi Medium.svg"); static BT_CONN: AsyncOnce<zbus::Connection> = AsyncOnce::const_new();
pub const WIFI_WEAK: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/WiFi Weak.svg"); static BT_CACHE: LazyLock<Mutex<&'static str>> = LazyLock::new(|| Mutex::new(BT_OFF));
pub const WIFI_OFF: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/WiFi Connecting.svg"); static BT_TICK: AtomicU8 = AtomicU8::new(0);
// Embedded SVG contents (not paths). These &str constants double as stable
// HashMap keys via their .as_ptr(); include_str! keeps each one a single
// 'static literal, so pointer identity still holds.
pub const WIFI_STRONG: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/WiFi Strong.svg"));
pub const WIFI_MEDIUM: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/WiFi Medium.svg"));
pub const WIFI_WEAK: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/WiFi Weak.svg"));
pub const WIFI_OFF: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/WiFi Disconnect.svg"));
pub const BAT_HIGH: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/Battery 3 Bars.svg"));
pub const BAT_MID: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/Battery 2 Bars.svg"));
pub const BAT_LOW: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/Battery 1 Bar.svg"));
pub const AC_POWER: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/AC Power.svg"));
pub const BT_OFF: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/Bluetooth Off.svg"));
pub const BT_ON: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/Bluetooth.svg"));
pub const BT_CONNECTED: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/assets/Bluetooth Connected.svg"
));
#[derive(Debug)] #[derive(Debug)]
pub struct Stats { pub struct Stats {
@ -20,6 +41,9 @@ pub struct Stats {
pub mem: String, pub mem: String,
pub power: String, pub power: String,
pub bat: String, pub bat: String,
pub bat_icon: &'static str,
pub ac_connected: bool,
pub bt_icon: &'static str,
pub wifi_ssid: String, pub wifi_ssid: String,
pub wifi_icon: &'static str, pub wifi_icon: &'static str,
} }
@ -31,6 +55,7 @@ struct CpuSnapshot {
static PREV_CPU: OnceLock<Mutex<CpuSnapshot>> = OnceLock::new(); static PREV_CPU: OnceLock<Mutex<CpuSnapshot>> = OnceLock::new();
static BAT_PATH: OnceLock<Option<PathBuf>> = OnceLock::new(); static BAT_PATH: OnceLock<Option<PathBuf>> = OnceLock::new();
static AC_PATH: OnceLock<Option<PathBuf>> = OnceLock::new();
static WIFI_CACHE: LazyLock<Mutex<(String, &'static str)>> = static WIFI_CACHE: LazyLock<Mutex<(String, &'static str)>> =
LazyLock::new(|| Mutex::new(("".to_string(), WIFI_OFF))); LazyLock::new(|| Mutex::new(("".to_string(), WIFI_OFF)));
static WIFI_TICK: AtomicU8 = AtomicU8::new(0); static WIFI_TICK: AtomicU8 = AtomicU8::new(0);
@ -38,16 +63,21 @@ static WIFI_TICK: AtomicU8 = AtomicU8::new(0);
fn read_cpu() -> f32 { fn read_cpu() -> f32 {
let text = fs::read_to_string("/proc/stat").unwrap_or_default(); let text = fs::read_to_string("/proc/stat").unwrap_or_default();
let line = text.lines().next().unwrap_or_default(); let line = text.lines().next().unwrap_or_default();
let vals: Vec<u64> = line let mut total = 0u64;
.split_whitespace() let mut idle = 0u64;
.skip(1) let mut count = 0usize;
.filter_map(|s| s.parse().ok()) for (i, s) in line.split_whitespace().skip(1).enumerate() {
.collect(); if let Ok(v) = s.parse::<u64>() {
if vals.len() < 5 { total += v;
if i == 3 || i == 4 {
idle += v;
}
count += 1;
}
}
if count < 5 {
return 0.0; return 0.0;
} }
let idle = vals[3] + vals.get(4).copied().unwrap_or(0);
let total: u64 = vals.iter().sum();
let state = PREV_CPU.get_or_init(|| Mutex::new(CpuSnapshot { total, idle })); let state = PREV_CPU.get_or_init(|| Mutex::new(CpuSnapshot { total, idle }));
let mut prev = state.lock().unwrap(); let mut prev = state.lock().unwrap();
@ -65,13 +95,23 @@ fn read_ram() -> u64 {
let text = fs::read_to_string("/proc/meminfo").unwrap_or_default(); let text = fs::read_to_string("/proc/meminfo").unwrap_or_default();
let mut total = 0u64; let mut total = 0u64;
let mut avail = 0u64; let mut avail = 0u64;
let mut found = 0u8;
for line in text.lines() { for line in text.lines() {
let mut parts = line.split_whitespace(); let mut parts = line.split_whitespace();
match parts.next() { match parts.next() {
Some("MemTotal:") => total = parts.next().and_then(|s| s.parse().ok()).unwrap_or(0), Some("MemTotal:") => {
Some("MemAvailable:") => avail = parts.next().and_then(|s| s.parse().ok()).unwrap_or(0), total = parts.next().and_then(|s| s.parse().ok()).unwrap_or(0);
found += 1;
}
Some("MemAvailable:") => {
avail = parts.next().and_then(|s| s.parse().ok()).unwrap_or(0);
found += 1;
}
_ => {} _ => {}
} }
if found == 2 {
break;
}
} }
total.saturating_sub(avail) total.saturating_sub(avail)
} }
@ -83,7 +123,10 @@ fn bat_path() -> Option<&'static PathBuf> {
.ok()? .ok()?
.filter_map(|e| e.ok()) .filter_map(|e| e.ok())
.map(|e| e.path()) .map(|e| e.path())
.find(|p| p.file_name().map_or(false, |n| n.to_string_lossy().starts_with("BAT"))) .find(|p| {
p.file_name()
.is_some_and(|n| n.to_string_lossy().starts_with("BAT"))
})
}) })
.as_ref() .as_ref()
} }
@ -116,26 +159,103 @@ fn read_battery() -> Option<u8> {
.ok() .ok()
} }
async fn read_wifi() -> (String, &'static str) { fn bat_level_icon(pct: u8) -> &'static str {
let dev_out = tokio::process::Command::new("iw") if pct >= 67 {
.arg("dev") BAT_HIGH
.output() } else if pct >= 34 {
.await BAT_MID
.ok(); } else {
let dev_stdout = match dev_out { BAT_LOW
Some(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).into_owned(), }
_ => return ("".into(), WIFI_OFF), }
};
let iface = dev_stdout fn read_ac() -> bool {
.lines() AC_PATH
.find_map(|l| l.trim().strip_prefix("Interface ").map(str::to_string)); .get_or_init(|| {
let Some(iface) = iface else { fs::read_dir("/sys/class/power_supply")
.ok()?
.filter_map(|e| e.ok())
.map(|e| e.path())
.find(|p| {
fs::read_to_string(p.join("type"))
.map(|t| t.trim() == "Mains")
.unwrap_or(false)
})
})
.as_ref()
.and_then(|p| fs::read_to_string(p.join("online")).ok())
.map(|s| s.trim() == "1")
.unwrap_or(false)
}
fn bt_rfkill_on() -> bool {
fs::read_dir("/sys/class/rfkill")
.into_iter()
.flatten()
.filter_map(|e| e.ok())
.any(|e| {
let p = e.path();
fs::read_to_string(p.join("type"))
.map(|t| t.trim() == "bluetooth")
.unwrap_or(false)
&& fs::read_to_string(p.join("state"))
.map(|s| s.trim() == "1")
.unwrap_or(false)
})
}
async fn read_bt() -> &'static str {
if !bt_rfkill_on() {
return BT_OFF;
}
bt_connected_icon().await.unwrap_or(BT_ON)
}
async fn bt_connected_icon() -> Option<&'static str> {
let conn = BT_CONN
.get_or_try_init(zbus::Connection::system)
.await
.ok()?;
let mgr = zbus::fdo::ObjectManagerProxy::builder(conn)
.destination("org.bluez")
.ok()?
.path("/")
.ok()?
.build()
.await
.ok()?;
let objects = mgr.get_managed_objects().await.ok()?;
let connected = objects
.values()
.filter_map(|ifaces| ifaces.get("org.bluez.Device1"))
.any(|props| {
props
.get("Connected")
.and_then(|v| bool::try_from(v.clone()).ok())
.unwrap_or(false)
});
Some(if connected { BT_CONNECTED } else { BT_ON })
}
fn wifi_iface() -> Option<&'static str> {
WIFI_IFACE
.get_or_init(|| {
fs::read_dir("/sys/class/net")
.ok()?
.filter_map(|e| e.ok())
.find(|e| e.path().join("wireless").is_dir())
.map(|e| e.file_name().to_string_lossy().into_owned())
})
.as_deref()
}
async fn read_wifi() -> (String, &'static str) {
let Some(iface) = wifi_iface() else {
return ("".into(), WIFI_OFF); return ("".into(), WIFI_OFF);
}; };
let link_out = tokio::process::Command::new("iw") let link_out = tokio::process::Command::new("iw")
.args(["dev", &iface, "link"]) .args(["dev", iface, "link"])
.output() .output()
.await .await
.ok(); .ok();
@ -172,11 +292,24 @@ pub async fn poll() -> Stats {
let cpu = read_cpu(); let cpu = read_cpu();
let mem = read_ram(); let mem = read_ram();
let power = read_power().map_or_else(|| " —W".into(), |w| format!("{w:4.1}W")); let power = read_power().map_or_else(|| " —W".into(), |w| format!("{w:4.1}W"));
let bat = read_battery().map_or_else(|| "".into(), |p| format!("{p:3}%")); let pct = read_battery();
// Refresh WiFi every 8 cycles (~16 s); cache the result in between. let bat = pct.map_or_else(|| "".into(), |p| format!("{p:3}%"));
let bat_icon = pct.map_or(BAT_MID, bat_level_icon);
let ac_connected = read_ac();
// BT and WiFi both refresh every 8 cycles (~16 s); cache in between.
let bt_icon = {
let tick = BT_TICK.fetch_add(1, Ordering::Relaxed);
if tick.is_multiple_of(8) {
let fresh = read_bt().await;
*BT_CACHE.lock().unwrap() = fresh;
fresh
} else {
*BT_CACHE.lock().unwrap()
}
};
let (wifi_ssid, wifi_icon) = { let (wifi_ssid, wifi_icon) = {
let tick = WIFI_TICK.fetch_add(1, Ordering::Relaxed); let tick = WIFI_TICK.fetch_add(1, Ordering::Relaxed);
if tick % 8 == 0 { if tick.is_multiple_of(8) {
let fresh = read_wifi().await; let fresh = read_wifi().await;
*WIFI_CACHE.lock().unwrap() = fresh.clone(); *WIFI_CACHE.lock().unwrap() = fresh.clone();
fresh fresh
@ -193,6 +326,9 @@ pub async fn poll() -> Stats {
}, },
power, power,
bat, bat,
bat_icon,
ac_connected,
bt_icon,
wifi_ssid, wifi_ssid,
wifi_icon, wifi_icon,
} }

240
src/bar/tray.rs Normal file
View file

@ -0,0 +1,240 @@
use crate::{App, AppInput};
use futures_lite::StreamExt;
use gtk4::prelude::Cast;
use relm4::ComponentSender;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use zbus::{interface, object_server::SignalEmitter};
#[derive(Debug)]
pub enum TrayIconData {
Pixels { width: i32, height: i32, data: Vec<u8> },
Name(String),
}
#[derive(Debug)]
pub enum TrayUpdate {
Add { id: String, icon: Option<TrayIconData>, title: String },
Remove { id: String },
}
struct WatcherState {
items: Vec<String>,
}
struct Watcher {
state: Arc<Mutex<WatcherState>>,
tx: tokio::sync::mpsc::UnboundedSender<(String, String)>,
}
#[interface(name = "org.kde.StatusNotifierWatcher")]
impl Watcher {
async fn register_status_notifier_item(
&self,
service: String,
#[zbus(header)] header: zbus::message::Header<'_>,
#[zbus(signal_emitter)] ctx: SignalEmitter<'_>,
) {
let sender_name = header.sender().map(|s| s.to_string()).unwrap_or_default();
let (bus, path) = parse_service(&service, &sender_name);
let full = format!("{}{}", bus, path);
{
let mut state = self.state.lock().unwrap();
if !state.items.contains(&full) {
state.items.push(full.clone());
}
}
let _ = Self::status_notifier_item_registered(&ctx, &full).await;
let _ = self.tx.send((bus, path));
}
async fn register_status_notifier_host(
&self,
_service: String,
#[zbus(signal_emitter)] ctx: SignalEmitter<'_>,
) {
let _ = Self::status_notifier_host_registered(&ctx).await;
}
#[zbus(property)]
fn registered_status_notifier_items(&self) -> Vec<String> {
self.state.lock().unwrap().items.clone()
}
#[zbus(property)]
fn is_status_notifier_host_registered(&self) -> bool {
true
}
#[zbus(property)]
fn protocol_version(&self) -> i32 {
0
}
#[zbus(signal)]
async fn status_notifier_item_registered(
ctx: &SignalEmitter<'_>,
service: &str,
) -> zbus::Result<()>;
#[zbus(signal)]
async fn status_notifier_item_unregistered(
ctx: &SignalEmitter<'_>,
service: &str,
) -> zbus::Result<()>;
#[zbus(signal)]
async fn status_notifier_host_registered(ctx: &SignalEmitter<'_>) -> zbus::Result<()>;
}
fn parse_service(service: &str, sender: &str) -> (String, String) {
if service.starts_with('/') {
return (sender.to_string(), service.to_string());
}
match service.find('/') {
Some(slash) => (service[..slash].to_string(), service[slash..].to_string()),
None => (service.to_string(), "/StatusNotifierItem".to_string()),
}
}
async fn read_item(
conn: &zbus::Connection,
bus: &str,
path: &str,
) -> (Option<TrayIconData>, String) {
let Ok(proxy) = zbus::Proxy::new(conn, bus, path, "org.kde.StatusNotifierItem").await else {
return (None, String::new());
};
let icon = read_icon(&proxy).await;
let title = proxy.get_property::<String>("Title").await.unwrap_or_default();
(icon, title)
}
async fn read_icon(proxy: &zbus::Proxy<'_>) -> Option<TrayIconData> {
let pixmaps: Vec<(i32, i32, Vec<u8>)> =
proxy.get_property("IconPixmap").await.unwrap_or_default();
if !pixmaps.is_empty() {
return pixmaps
.into_iter()
.filter(|(w, h, _)| *w > 0 && *h > 0)
.min_by_key(|(w, h, _)| (w.max(h) - 22).abs())
.map(|(width, height, data)| TrayIconData::Pixels { width, height, data });
}
let name: String = proxy.get_property("IconName").await.ok()?;
if name.is_empty() {
return None;
}
Some(TrayIconData::Name(name))
}
/// Call `Activate(0, 0)` on the SNI item identified by `id` (`{bus}{path}`).
pub fn spawn_activate(id: String) {
relm4::spawn(async move {
let (bus, path) = match id.find('/') {
Some(slash) => (id[..slash].to_string(), id[slash..].to_string()),
None => (id, "/StatusNotifierItem".to_string()),
};
let Ok(conn) = zbus::Connection::session().await else { return };
let Ok(proxy) = zbus::Proxy::new(&conn, bus.as_str(), path.as_str(), "org.kde.StatusNotifierItem").await else { return };
let _ = proxy.call_method("Activate", &(0i32, 0i32)).await;
});
}
pub fn spawn_watcher(sender: ComponentSender<App>) {
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<(String, String)>();
// Maps bus name → item ids, shared between registration and cleanup tasks.
let bus_map: Arc<Mutex<HashMap<String, Vec<String>>>> = Arc::new(Mutex::new(HashMap::new()));
let bus_map_cleanup = bus_map.clone();
let sender_cleanup = sender.clone();
// Registration task — owns the watcher service and processes new items.
relm4::spawn(async move {
let watcher = Watcher {
state: Arc::new(Mutex::new(WatcherState { items: Vec::new() })),
tx,
};
// Builder steps fail only on invalid static strings — safe to unwrap.
let conn = zbus::connection::Builder::session()
.unwrap()
.name("org.kde.StatusNotifierWatcher")
.unwrap()
.serve_at("/StatusNotifierWatcher", watcher)
.unwrap()
.build()
.await
.expect("failed to register org.kde.StatusNotifierWatcher");
while let Some((bus, path)) = rx.recv().await {
let (icon, title) = read_item(&conn, &bus, &path).await;
let id = format!("{}{}", bus, path);
bus_map.lock().unwrap().entry(bus).or_default().push(id.clone());
sender.input(AppInput::TrayUpdate(TrayUpdate::Add { id, icon, title }));
}
});
// Cleanup task — watches NameOwnerChanged and removes items when their owner exits.
relm4::spawn(async move {
let Ok(conn) = zbus::Connection::session().await else { return };
let Ok(proxy) = zbus::fdo::DBusProxy::new(&conn).await else { return };
let Ok(mut stream) = proxy.receive_name_owner_changed().await else { return };
while let Some(signal) = stream.next().await {
let Ok(args) = signal.args() else { continue };
if args.new_owner().is_none() {
let gone = args.name().to_string();
if let Some(ids) = bus_map_cleanup.lock().unwrap().remove(&gone) {
for id in ids {
sender_cleanup.input(AppInput::TrayUpdate(TrayUpdate::Remove { id }));
}
}
}
}
});
}
/// Convert SNI ARGB pixel data (network byte order) to a GTK4 `Image`.
/// Falls back to an icon-name lookup or a placeholder on failure.
pub fn make_tray_image(icon: Option<&TrayIconData>) -> gtk4::Image {
let img = match icon {
Some(TrayIconData::Pixels { width, height, data }) => pixels_to_image(*width, *height, data),
Some(TrayIconData::Name(name)) => {
let img = gtk4::Image::from_icon_name(name);
img.set_pixel_size(16);
Some(img)
}
None => None,
};
img.unwrap_or_else(|| {
let img = gtk4::Image::from_icon_name("image-missing");
img.set_pixel_size(16);
img
})
}
fn pixels_to_image(width: i32, height: i32, data: &[u8]) -> Option<gtk4::Image> {
if data.len() != (width * height * 4) as usize {
return None;
}
// SNI delivers ARGB big-endian: bytes are [A, R, G, B] per pixel.
// GTK4 R8g8b8a8 expects [R, G, B, A] per pixel.
let mut rgba = Vec::with_capacity(data.len());
for chunk in data.chunks_exact(4) {
rgba.push(chunk[1]);
rgba.push(chunk[2]);
rgba.push(chunk[3]);
rgba.push(chunk[0]);
}
let bytes = gtk4::glib::Bytes::from_owned(rgba);
let tex = gtk4::gdk::MemoryTexture::new(
width,
height,
gtk4::gdk::MemoryFormat::R8g8b8a8,
&bytes,
(width * 4) as usize,
);
let tex: gtk4::gdk::Texture = tex.upcast();
let img = gtk4::Image::from_paintable(Some(&tex));
img.set_pixel_size(16);
Some(img)
}

View file

@ -44,7 +44,9 @@ pub fn make_button(id: WorkspaceId, name: &str, active: WorkspaceId) -> gtk4::Bu
} }
btn.connect_clicked(move |_| { btn.connect_clicked(move |_| {
use hyprland::dispatch::{Dispatch, DispatchType, WorkspaceIdentifierWithSpecial}; use hyprland::dispatch::{Dispatch, DispatchType, WorkspaceIdentifierWithSpecial};
let _ = Dispatch::call(DispatchType::Workspace(WorkspaceIdentifierWithSpecial::Id(id))); let _ = Dispatch::call(DispatchType::Workspace(WorkspaceIdentifierWithSpecial::Id(
id,
)));
}); });
btn btn
} }

View file

@ -1,11 +1,16 @@
// Embed asset SVGs into the binary at compile time. Previously these were
// referenced by their build-time filesystem path (CARGO_MANIFEST_DIR), which
// does not exist on an installed system — so the packaged binary loaded empty
// bytes and panicked. include_str! bakes the contents in instead.
macro_rules! asset { macro_rules! asset {
($n:literal) => { ($n:literal) => {
concat!(env!("CARGO_MANIFEST_DIR"), "/assets/", $n) include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/", $n))
}; };
} }
mod bar; mod bar;
mod notifications; mod notifications;
mod osd;
mod theme; mod theme;
use gtk4::prelude::*; use gtk4::prelude::*;
@ -19,14 +24,22 @@ pub struct App {
active_ws: WorkspaceId, active_ws: WorkspaceId,
time_str: String, time_str: String,
workspace_box: gtk4::Box, workspace_box: gtk4::Box,
button_map: std::collections::HashMap<WorkspaceId, gtk4::Button>,
cpu_lbl: gtk4::Label, cpu_lbl: gtk4::Label,
mem_lbl: gtk4::Label, mem_lbl: gtk4::Label,
pwr_lbl: gtk4::Label, pwr_lbl: gtk4::Label,
bat_lbl: gtk4::Label, bat_lbl: gtk4::Label,
bat_img: gtk4::Image,
bat_textures: std::collections::HashMap<usize, gtk4::gdk::Texture>,
ac_img: gtk4::Image,
bt_img: gtk4::Image,
bt_textures: std::collections::HashMap<usize, gtk4::gdk::Texture>,
wifi_lbl: gtk4::Label, wifi_lbl: gtk4::Label,
wifi_img: gtk4::Image, wifi_img: gtk4::Image,
// Pre-loaded textures indexed by the WIFI_* constant pointer values. // Pre-loaded textures indexed by constant pointer values.
wifi_textures: std::collections::HashMap<usize, gtk4::gdk::Texture>, wifi_textures: std::collections::HashMap<usize, gtk4::gdk::Texture>,
tray_box: gtk4::Box,
tray_items: std::collections::HashMap<String, gtk4::Button>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -35,6 +48,7 @@ pub enum AppInput {
ActiveWorkspace(WorkspaceId), ActiveWorkspace(WorkspaceId),
ClockTick, ClockTick,
StatsUpdate(bar::stats::Stats), StatsUpdate(bar::stats::Stats),
TrayUpdate(bar::tray::TrayUpdate),
} }
#[relm4::component(pub)] #[relm4::component(pub)]
@ -84,16 +98,45 @@ impl SimpleComponent for App {
root.set_anchor(Edge::Right, true); root.set_anchor(Edge::Right, true);
root.set_exclusive_zone(32); root.set_exclusive_zone(32);
let cpu_lbl = stat_label(4); let cpu_lbl = stat_label();
let mem_lbl = stat_label(4); let mem_lbl = stat_label();
let pwr_lbl = stat_label(5); let pwr_lbl = stat_label();
let bat_lbl = stat_label(4); let bat_lbl = stat_label();
let wifi_lbl = gtk4::Label::new(None); let wifi_lbl = gtk4::Label::new(None);
wifi_lbl.add_css_class("stat-label");
wifi_lbl.add_css_class("wifi-label");
wifi_lbl.set_ellipsize(gtk4::pango::EllipsizeMode::End); wifi_lbl.set_ellipsize(gtk4::pango::EllipsizeMode::End);
wifi_lbl.set_max_width_chars(12); wifi_lbl.set_max_width_chars(22);
let wifi_img = gtk4::Image::from_paintable(Some(&svg_texture(asset!("WiFi Connecting.svg")))); wifi_lbl.set_xalign(0.0);
let wifi_img =
gtk4::Image::from_paintable(Some(&svg_texture(asset!("WiFi Connecting.svg"))));
use bar::stats::{
AC_POWER, BAT_HIGH, BAT_LOW, BAT_MID, BT_CONNECTED, BT_OFF, BT_ON, WIFI_MEDIUM,
WIFI_OFF, WIFI_STRONG, WIFI_WEAK,
};
let bat_textures: std::collections::HashMap<usize, gtk4::gdk::Texture> =
[BAT_HIGH, BAT_MID, BAT_LOW]
.into_iter()
.map(|p| (p.as_ptr() as usize, svg_texture(p)))
.collect();
// BAT_MID was just inserted into bat_textures above — key is always present.
let bat_img = gtk4::Image::from_paintable(Some(
bat_textures.get(&(BAT_MID.as_ptr() as usize)).unwrap(),
));
let ac_img = gtk4::Image::from_paintable(Some(&svg_texture(AC_POWER)));
ac_img.set_visible(false);
let bt_textures: std::collections::HashMap<usize, gtk4::gdk::Texture> =
[BT_OFF, BT_ON, BT_CONNECTED]
.into_iter()
.map(|p| (p.as_ptr() as usize, svg_texture(p)))
.collect();
// BT_OFF was just inserted into bt_textures above — key is always present.
let bt_img = gtk4::Image::from_paintable(Some(
bt_textures.get(&(BT_OFF.as_ptr() as usize)).unwrap(),
));
use bar::stats::{WIFI_OFF, WIFI_STRONG, WIFI_MEDIUM, WIFI_WEAK};
let wifi_textures = [WIFI_STRONG, WIFI_MEDIUM, WIFI_WEAK, WIFI_OFF] let wifi_textures = [WIFI_STRONG, WIFI_MEDIUM, WIFI_WEAK, WIFI_OFF]
.into_iter() .into_iter()
.map(|p| (p.as_ptr() as usize, svg_texture(p))) .map(|p| (p.as_ptr() as usize, svg_texture(p)))
@ -104,34 +147,58 @@ impl SimpleComponent for App {
active_ws: 1, active_ws: 1,
time_str: bar::clock::current(), time_str: bar::clock::current(),
workspace_box: gtk4::Box::new(gtk4::Orientation::Horizontal, 4), workspace_box: gtk4::Box::new(gtk4::Orientation::Horizontal, 4),
button_map: std::collections::HashMap::new(),
cpu_lbl: cpu_lbl.clone(), cpu_lbl: cpu_lbl.clone(),
mem_lbl: mem_lbl.clone(), mem_lbl: mem_lbl.clone(),
pwr_lbl: pwr_lbl.clone(), pwr_lbl: pwr_lbl.clone(),
bat_lbl: bat_lbl.clone(), bat_lbl: bat_lbl.clone(),
bat_img: bat_img.clone(),
bat_textures,
ac_img: ac_img.clone(),
bt_img: bt_img.clone(),
bt_textures,
wifi_lbl: wifi_lbl.clone(), wifi_lbl: wifi_lbl.clone(),
wifi_img: wifi_img.clone(), wifi_img: wifi_img.clone(),
wifi_textures, wifi_textures,
tray_box: gtk4::Box::new(gtk4::Orientation::Horizontal, 4),
tray_items: std::collections::HashMap::new(),
}; };
let widgets = view_output!(); let widgets = view_output!();
model.workspace_box = widgets.workspace_box.clone(); model.workspace_box = widgets.workspace_box.clone();
let stats_box = gtk4::Box::new(gtk4::Orientation::Horizontal, 8); let stats_box = gtk4::Box::new(gtk4::Orientation::Horizontal, 0);
stats_box.set_margin_end(8); stats_box.add_css_class("stats-box");
stats_box.append(&stat_pair(asset!("CPU.svg"), &cpu_lbl)); stats_box.append(&stat_pair(asset!("CPU.svg"), &cpu_lbl));
stats_box.append(&stat_pair(asset!("RAM Usage.svg"), &mem_lbl)); stats_box.append(&stat_pair(asset!("RAM Usage.svg"), &mem_lbl));
stats_box.append(&stat_pair(asset!("Power Draw.svg"), &pwr_lbl)); stats_box.append(&stat_pair(asset!("Power Draw.svg"), &pwr_lbl));
stats_box.append(&stat_pair(asset!("Battery.svg"), &bat_lbl)); let bat_box = gtk4::Box::new(gtk4::Orientation::Horizontal, 0);
let wifi_pair = gtk4::Box::new(gtk4::Orientation::Horizontal, 4); bat_box.add_css_class("stat-pair");
bat_img.add_css_class("stat-icon");
bat_lbl.add_css_class("stat-label");
ac_img.add_css_class("stat-icon");
bat_box.append(&bat_img);
bat_box.append(&bat_lbl);
bat_box.append(&ac_img);
stats_box.append(&bat_box);
bt_img.add_css_class("bt-icon");
stats_box.append(&bt_img);
let wifi_pair = gtk4::Box::new(gtk4::Orientation::Horizontal, 0);
wifi_pair.add_css_class("stat-pair");
wifi_img.add_css_class("stat-icon");
wifi_pair.append(&wifi_img); wifi_pair.append(&wifi_img);
wifi_pair.append(&wifi_lbl); wifi_pair.append(&wifi_lbl);
stats_box.append(&wifi_pair); stats_box.append(&wifi_pair);
model.tray_box.add_css_class("tray-box");
stats_box.append(&model.tray_box);
widgets.center_box.set_end_widget(Some(&stats_box)); widgets.center_box.set_end_widget(Some(&stats_box));
theme::apply(); theme::apply();
bar::workspaces::spawn_watcher(sender.clone()); bar::workspaces::spawn_watcher(sender.clone());
bar::clock::spawn_ticker(sender.clone()); bar::clock::spawn_ticker(sender.clone());
bar::stats::spawn_poller(sender); bar::stats::spawn_poller(sender.clone());
bar::tray::spawn_watcher(sender.clone());
notifications::spawn(); notifications::spawn();
osd::spawn();
ComponentParts { model, widgets } ComponentParts { model, widgets }
} }
@ -145,8 +212,13 @@ impl SimpleComponent for App {
self.rebuild_buttons(); self.rebuild_buttons();
} }
AppInput::ActiveWorkspace(id) => { AppInput::ActiveWorkspace(id) => {
if let Some(old) = self.button_map.get(&self.active_ws) {
old.remove_css_class("active");
}
self.active_ws = id; self.active_ws = id;
self.rebuild_buttons(); if let Some(btn) = self.button_map.get(&self.active_ws) {
btn.add_css_class("active");
}
} }
AppInput::ClockTick => { AppInput::ClockTick => {
self.time_str = bar::clock::current(); self.time_str = bar::clock::current();
@ -156,46 +228,95 @@ impl SimpleComponent for App {
self.mem_lbl.set_label(&stats.mem); self.mem_lbl.set_label(&stats.mem);
self.pwr_lbl.set_label(&stats.power); self.pwr_lbl.set_label(&stats.power);
self.bat_lbl.set_label(&stats.bat); self.bat_lbl.set_label(&stats.bat);
if let Some(tex) = self.bat_textures.get(&(stats.bat_icon.as_ptr() as usize)) {
self.bat_img.set_paintable(Some(tex));
}
self.ac_img.set_visible(stats.ac_connected);
if let Some(tex) = self.bt_textures.get(&(stats.bt_icon.as_ptr() as usize)) {
self.bt_img.set_paintable(Some(tex));
}
self.wifi_lbl.set_label(&stats.wifi_ssid); self.wifi_lbl.set_label(&stats.wifi_ssid);
if let Some(tex) = self.wifi_textures.get(&(stats.wifi_icon.as_ptr() as usize)) { if let Some(tex) = self.wifi_textures.get(&(stats.wifi_icon.as_ptr() as usize)) {
self.wifi_img.set_paintable(Some(tex)); self.wifi_img.set_paintable(Some(tex));
} }
} }
AppInput::TrayUpdate(bar::tray::TrayUpdate::Add { id, icon, title }) => {
if self.tray_items.contains_key(&id) {
return;
}
let btn = gtk4::Button::new();
btn.add_css_class("tray-btn");
btn.set_child(Some(&bar::tray::make_tray_image(icon.as_ref())));
if !title.is_empty() {
btn.set_tooltip_text(Some(&title));
}
let id_click = id.clone();
btn.connect_clicked(move |_| bar::tray::spawn_activate(id_click.clone()));
self.tray_box.append(&btn);
self.tray_items.insert(id, btn);
}
AppInput::TrayUpdate(bar::tray::TrayUpdate::Remove { id }) => {
if let Some(btn) = self.tray_items.remove(&id) {
self.tray_box.remove(&btn);
}
}
} }
} }
} }
impl App { impl App {
fn rebuild_buttons(&self) { fn rebuild_buttons(&mut self) {
while let Some(child) = self.workspace_box.first_child() { while let Some(child) = self.workspace_box.first_child() {
self.workspace_box.remove(&child); self.workspace_box.remove(&child);
} }
self.button_map.clear();
for ws in &self.workspaces { for ws in &self.workspaces {
self.workspace_box let btn = bar::workspaces::make_button(ws.id, &ws.name, self.active_ws);
.append(&bar::workspaces::make_button(ws.id, &ws.name, self.active_ws)); self.workspace_box.append(&btn);
self.button_map.insert(ws.id, btn);
} }
} }
} }
fn stat_pair(icon_path: &str, label: &gtk4::Label) -> gtk4::Box { fn stat_pair(icon_svg: &str, label: &gtk4::Label) -> gtk4::Box {
let pair = gtk4::Box::new(gtk4::Orientation::Horizontal, 4); let pair = gtk4::Box::new(gtk4::Orientation::Horizontal, 0);
pair.append(&gtk4::Image::from_paintable(Some(&svg_texture(icon_path)))); pair.add_css_class("stat-pair");
let img = gtk4::Image::from_paintable(Some(&svg_texture(icon_svg)));
img.add_css_class("stat-icon");
pair.append(&img);
pair.append(label); pair.append(label);
pair pair
} }
fn svg_texture(path: &str) -> gtk4::gdk::Texture { // Rasterise an (embedded) SVG to a texture. Done in pure Rust with resvg
let svg = std::fs::read_to_string(path) // because librsvg dropped its gdk-pixbuf SVG loader, so gdk::Texture::from_bytes
.unwrap_or_default() // can no longer decode SVG on a stock system.
.replace("currentColor", "white"); fn svg_texture(svg_src: &str) -> gtk4::gdk::Texture {
let bytes = gtk4::glib::Bytes::from_owned(svg.into_bytes()); use resvg::{tiny_skia, usvg};
gtk4::gdk::Texture::from_bytes(&bytes).expect("svg load") let fg = theme::fg_color();
let svg = svg_src
.replace("currentColor", &fg)
.replace(r#"width="24" height="24""#, r#"width="16" height="16""#);
let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()).expect("parse svg");
let size = tree.size().to_int_size();
let (w, h) = (size.width(), size.height());
let mut pixmap = tiny_skia::Pixmap::new(w, h).expect("alloc pixmap");
resvg::render(&tree, tiny_skia::Transform::identity(), &mut pixmap.as_mut());
let bytes = gtk4::glib::Bytes::from_owned(pixmap.take());
gtk4::gdk::MemoryTexture::new(
w as i32,
h as i32,
gtk4::gdk::MemoryFormat::R8g8b8a8Premultiplied,
&bytes,
(w * 4) as usize,
)
.upcast()
} }
fn stat_label(width_chars: i32) -> gtk4::Label { fn stat_label() -> gtk4::Label {
let lbl = gtk4::Label::new(None); let lbl = gtk4::Label::new(None);
lbl.set_width_chars(width_chars); lbl.add_css_class("stat-label");
lbl.set_xalign(1.0); lbl.set_xalign(0.0);
lbl lbl
} }

View file

@ -5,7 +5,13 @@ use tokio::sync::mpsc;
use zbus::zvariant::OwnedValue; use zbus::zvariant::OwnedValue;
pub enum NotifEvent { pub enum NotifEvent {
Show { id: u32, app_name: String, summary: String, body: String, timeout_ms: u32 }, Show {
id: u32,
app_name: String,
summary: String,
body: String,
timeout_ms: u32,
},
Close(u32), Close(u32),
} }
@ -16,6 +22,8 @@ struct NotifServer {
#[zbus::interface(name = "org.freedesktop.Notifications")] #[zbus::interface(name = "org.freedesktop.Notifications")]
impl NotifServer { impl NotifServer {
// The org.freedesktop.Notifications spec mandates exactly these 8 parameters.
#[allow(clippy::too_many_arguments)]
async fn notify( async fn notify(
&self, &self,
app_name: &str, app_name: &str,
@ -32,7 +40,11 @@ impl NotifServer {
} else { } else {
self.next_id.fetch_add(1, Ordering::Relaxed) self.next_id.fetch_add(1, Ordering::Relaxed)
}; };
let timeout_ms = if expire_timeout <= 0 { 5000 } else { expire_timeout as u32 }; let timeout_ms = if expire_timeout <= 0 {
5000
} else {
expire_timeout as u32
};
let _ = self let _ = self
.tx .tx
.send(NotifEvent::Show { .send(NotifEvent::Show {
@ -55,7 +67,12 @@ impl NotifServer {
} }
fn get_server_information(&self) -> (String, String, String, String) { fn get_server_information(&self) -> (String, String, String, String) {
("breadbar".into(), "breadway".into(), "0.1.0".into(), "1.2".into()) (
"breadbar".into(),
"breadway".into(),
env!("CARGO_PKG_VERSION").into(),
"1.2".into(),
)
} }
} }
@ -63,7 +80,11 @@ pub fn spawn() {
let (tx, rx) = mpsc::channel(32); let (tx, rx) = mpsc::channel(32);
relm4::spawn(async move { relm4::spawn(async move {
let server = NotifServer { tx, next_id: AtomicU32::new(1) }; let server = NotifServer {
tx,
next_id: AtomicU32::new(1),
};
// Builder failures here would only occur with invalid static strings — safe to unwrap.
let _conn = zbus::connection::Builder::session() let _conn = zbus::connection::Builder::session()
.unwrap() .unwrap()
.name("org.freedesktop.Notifications") .name("org.freedesktop.Notifications")
@ -72,7 +93,7 @@ pub fn spawn() {
.unwrap() .unwrap()
.build() .build()
.await .await
.expect("failed to claim org.freedesktop.Notifications"); .expect("failed to claim org.freedesktop.Notifications on D-Bus session bus");
std::future::pending::<()>().await std::future::pending::<()>().await
}); });

View file

@ -21,7 +21,13 @@ pub async fn run(mut rx: Receiver<NotifEvent>) {
while let Some(event) = rx.recv().await { while let Some(event) = rx.recv().await {
match event { match event {
NotifEvent::Show { id, app_name, summary, body, timeout_ms } => { NotifEvent::Show {
id,
app_name,
summary,
body,
timeout_ms,
} => {
// Replace existing card with same id (replaces_id case) // Replace existing card with same id (replaces_id case)
if let Some(old) = cards.borrow_mut().remove(&id) { if let Some(old) = cards.borrow_mut().remove(&id) {
cards_box.remove(&old); cards_box.remove(&old);

184
src/osd.rs Normal file
View file

@ -0,0 +1,184 @@
use std::{cell::Cell, rc::Rc, time::Duration};
use gtk4::prelude::*;
use gtk4_layer_shell::{Edge, Layer, LayerShell};
use tokio::sync::mpsc;
enum OsdEvent {
Volume { pct: u8, muted: bool },
Brightness { pct: u8 },
}
pub fn spawn() {
let (tx, rx) = mpsc::channel::<OsdEvent>(8);
let tx1 = tx.clone();
std::thread::spawn(move || volume_watcher(tx1));
std::thread::spawn(move || brightness_watcher(tx));
relm4::spawn_local(run_osd(rx));
}
fn volume_watcher(tx: mpsc::Sender<OsdEvent>) {
use std::io::{BufRead, BufReader};
use std::process::{Command, Stdio};
let Ok(mut child) = Command::new("pactl")
.args(["subscribe"])
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
else {
return;
};
let stdout = child.stdout.take().unwrap();
let reader = BufReader::new(stdout);
for line in reader.lines().map_while(Result::ok) {
if line.contains("'change' on sink") {
if let Some(evt) = query_volume() {
let _ = tx.blocking_send(evt);
}
}
}
}
fn query_volume() -> Option<OsdEvent> {
use std::process::Command;
let vol = Command::new("pactl")
.args(["get-sink-volume", "@DEFAULT_SINK@"])
.output()
.ok()?;
let mute = Command::new("pactl")
.args(["get-sink-mute", "@DEFAULT_SINK@"])
.output()
.ok()?;
let vol_str = String::from_utf8_lossy(&vol.stdout);
let mute_str = String::from_utf8_lossy(&mute.stdout);
// "Volume: front-left: 45875 / 70% / -8.58 dB, ..."
let pct: u8 = vol_str
.split('/')
.nth(1)?
.trim()
.trim_end_matches('%')
.trim()
.parse()
.ok()?;
let muted = mute_str.contains(": yes");
Some(OsdEvent::Volume { pct, muted })
}
fn brightness_watcher(tx: mpsc::Sender<OsdEvent>) {
use std::fs;
let base = match fs::read_dir("/sys/class/backlight")
.ok()
.and_then(|mut d| d.next())
.and_then(|e| e.ok())
.map(|e| e.path())
{
Some(p) => p,
None => return,
};
let bright_path = base.join("brightness");
let max_path = base.join("max_brightness");
let max: u64 = match fs::read_to_string(&max_path)
.ok()
.and_then(|s| s.trim().parse().ok())
{
Some(v) if v > 0 => v,
_ => return,
};
// Initialize to current value so startup doesn't trigger OSD.
let mut last: u64 = fs::read_to_string(&bright_path)
.ok()
.and_then(|s| s.trim().parse().ok())
.unwrap_or(u64::MAX);
loop {
if let Some(val) = fs::read_to_string(&bright_path)
.ok()
.and_then(|s| s.trim().parse::<u64>().ok())
{
if val != last {
last = val;
let pct = ((val * 100) / max).min(100) as u8;
let _ = tx.blocking_send(OsdEvent::Brightness { pct });
}
}
std::thread::sleep(Duration::from_millis(200));
}
}
async fn run_osd(mut rx: mpsc::Receiver<OsdEvent>) {
let window = create_window();
let container = gtk4::Box::new(gtk4::Orientation::Vertical, 6);
container.set_margin_top(12);
container.set_margin_bottom(12);
container.set_margin_start(16);
container.set_margin_end(16);
window.set_child(Some(&container));
let header = gtk4::Box::new(gtk4::Orientation::Horizontal, 0);
let kind_lbl = gtk4::Label::new(Some("Volume"));
kind_lbl.add_css_class("osd-kind");
kind_lbl.set_hexpand(true);
kind_lbl.set_xalign(0.0);
let pct_lbl = gtk4::Label::new(Some("0%"));
pct_lbl.add_css_class("osd-pct");
header.append(&kind_lbl);
header.append(&pct_lbl);
container.append(&header);
let pbar = gtk4::ProgressBar::new();
pbar.add_css_class("osd-bar");
container.append(&pbar);
let dismiss_token = Rc::new(Cell::new(0u32));
while let Some(event) = rx.recv().await {
let (kind, pct) = match event {
OsdEvent::Volume { pct, muted } => {
(if muted { "Volume (Muted)" } else { "Volume" }, pct)
}
OsdEvent::Brightness { pct } => ("Brightness", pct),
};
kind_lbl.set_label(kind);
pct_lbl.set_label(&format!("{pct}%"));
pbar.set_fraction(pct as f64 / 100.0);
window.set_visible(true);
let token = dismiss_token.get().wrapping_add(1);
dismiss_token.set(token);
let dtok = dismiss_token.clone();
let win = window.clone();
relm4::spawn_local(async move {
gtk4::glib::timeout_future(Duration::from_millis(2000)).await;
if dtok.get() == token {
win.set_visible(false);
}
});
}
}
fn create_window() -> gtk4::Window {
let window = gtk4::Window::new();
window.add_css_class("breadbar-osd");
window.init_layer_shell();
window.set_layer(Layer::Overlay);
window.set_anchor(Edge::Bottom, true);
window.set_margin(Edge::Bottom, 80);
window.set_default_width(280);
window
}

View file

@ -1,77 +1,70 @@
use bread_theme::{gtk as bgtk, hex_to_rgba, ink_on, load_palette};
use gtk4::CssProvider; use gtk4::CssProvider;
use serde::Deserialize; use std::cell::RefCell;
#[derive(Deserialize)] thread_local! {
struct WalColors { static USER_PROVIDER: RefCell<Option<CssProvider>> = const { RefCell::new(None) };
special: Special,
colors: Colors,
}
#[derive(Deserialize)]
struct Special {
background: String,
}
#[derive(Deserialize)]
struct Colors {
color0: String,
color1: String,
color15: String,
}
fn hex_to_rgba(hex: &str, alpha: f32) -> String {
let h = hex.trim_start_matches('#');
let r = u8::from_str_radix(&h[0..2], 16).unwrap_or(0);
let g = u8::from_str_radix(&h[2..4], 16).unwrap_or(0);
let b = u8::from_str_radix(&h[4..6], 16).unwrap_or(0);
format!("rgba({r},{g},{b},{alpha})")
} }
fn load_css() -> String { fn load_css() -> String {
let home = std::env::var("HOME").unwrap_or_default(); let p = load_palette();
let text = std::fs::read_to_string(format!("{home}/.cache/wal/colors.json")) // breadbar-specific rules only — fonts, base colours, and generic widgets
.unwrap_or_default(); // come from the shared ecosystem stylesheet (applied first in `apply()`).
// Colour is set on each surface (bar, active workspace pill, notification
let (bg, surface, fg, accent) = if let Ok(wal) = serde_json::from_str::<WalColors>(&text) { // card) and child labels inherit it, so text stays legible whatever lightness
(wal.special.background, wal.colors.color0, wal.colors.color15, wal.colors.color1) // pywal hands a given slot. `on_*` are luminance-picked ink (black/white) for
} else { // that background — the pywal hues themselves are untouched.
(
"#1e1e2e".to_string(),
"#181825".to_string(),
"#cdd6f4".to_string(),
"#89b4fa".to_string(),
)
};
format!( format!(
"* {{ font-family: 'JetBrainsMono Nerd Font Mono', monospace; font-size: 12px; }}\ "window.breadbar {{ background-color: {bg_rgba}; color: {on_bg}; border-radius: 0; }}\
window.breadbar {{ background-color: {bg_rgba}; border-radius: 0; }}\ .workspace-btn {{ background: transparent; opacity: 0.45;\
.workspace-btn {{ background: transparent; color: {fg}; opacity: 0.45;\ border-radius: 0; border: none; outline: none; box-shadow: none;\
border-radius: 0 0 8px 8px; border: none; outline: none; box-shadow: none;\ min-width: 24px; padding: 4px 8px; }}\
min-width: 24px; padding: 2px 8px; }}\
.workspace-btn:hover {{ opacity: 0.8; }}\ .workspace-btn:hover {{ opacity: 0.8; }}\
.workspace-btn.active {{ background: {accent}; opacity: 1; }}\ .workspace-btn.active {{ background: {accent}; color: {on_accent}; opacity: 1; }}\
label {{ color: {fg}; }}\ .stats-box {{ margin-right: 8px; }}\
window.breadbar-notification {{ background-color: alpha({bg_plain}, 0.95); }}\ .stat-pair {{ margin-right: 12px; }}\
.notification-card {{ background: {surface}; border-radius: 6px;\ .stat-icon {{ margin-right: 5px; }}\
padding: 10px; margin-bottom: 4px; }}\ .bt-icon {{ margin-right: 12px; }}\
.notification-summary {{ font-weight: bold; color: {fg}; }}\ window.breadbar-notification {{ background-color: alpha({bg_plain}, 0.95); color: {on_bg}; }}\
.notification-app {{ color: {fg}; opacity: 0.6; }}\ .notification-card {{ background: {surface}; color: {on_surface}; border-radius: 8px;\
.notification-body {{ color: {fg}; }}", padding: 12px; margin-bottom: 8px; }}\
bg_plain = bg, .notification-summary {{ font-weight: bold; }}\
bg_rgba = hex_to_rgba(&bg, 0.92), .notification-app {{ opacity: 0.6; }}\
surface = surface, window.breadbar-osd {{ background-color: alpha({bg_plain}, 0.95); color: {on_bg}; border-radius: 8px; }}\
fg = fg, .osd-kind {{ opacity: 0.75; font-size: 12px; }}\
accent = accent, .osd-pct {{ font-weight: bold; font-size: 12px; }}\
progressbar.osd-bar {{ min-height: 8px; }}\
progressbar.osd-bar trough {{ background-image: none; background-color: {trough}; border-radius: 4px; min-height: 8px; }}\
progressbar.osd-bar trough progress {{ background-image: none; background-color: {accent}; border-radius: 4px; min-height: 8px; }}",
bg_plain = p.background,
bg_rgba = hex_to_rgba(&p.background, 0.92),
surface = p.color0,
accent = p.color4,
on_bg = ink_on(&p.background),
on_surface = ink_on(&p.color0),
on_accent = ink_on(&p.color4),
trough = hex_to_rgba(&p.color4, 0.25),
) )
} }
pub fn apply() { /// Returns the ink colour for icon tinting in the stats bar — the same
let provider = CssProvider::new(); /// luminance-picked colour the bar's text uses, so icons stay legible on the bar
provider.load_from_string(&load_css()); /// whatever lightness pywal gives the background.
gtk4::style_context_add_provider_for_display( pub fn fg_color() -> String {
&gtk4::gdk::Display::default().expect("no display"), ink_on(&load_palette().background).to_string()
&provider, }
gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION,
); /// Apply (or reload) the theme CSS. Safe to call from `glib::MainContext::invoke`.
pub fn apply() {
// Shared ecosystem base (fonts, palette, generic widgets) — applied first
// (and self-reloading) so breadbar's own rules below layer on top.
bgtk::apply_shared();
// breadbar's own rules, hot-reloaded on `bread-theme reload`: the closure
// re-reads the pywal palette each time so the bar recolours without restart.
bgtk::apply_app_css(load_css);
let home = std::env::var("HOME").unwrap_or_default();
let user_path = std::path::PathBuf::from(format!("{home}/.config/breadbar/style.css"));
USER_PROVIDER.with(|cell| bgtk::apply_user_css(&user_path, cell));
} }