Compare commits
14 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa34aa1319 | ||
|
|
0893724e5f | ||
|
|
37b9a342e1 | ||
|
|
0cb27ec1c4 | ||
|
|
1959a86157 | ||
|
|
b098178058 | ||
|
|
ec24ed6371 | ||
|
|
ea441a2de3 | ||
|
|
289fc1c827 | ||
|
|
4b4d222784 | ||
|
|
7c23265eab | ||
|
|
00dbb1df5f | ||
|
|
50bb249b3a | ||
|
|
44a8d95887 |
13 changed files with 729 additions and 62 deletions
21
.forgejo/workflows/mirror.yml
Normal file
21
.forgejo/workflows/mirror.yml
Normal 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/*'
|
||||
40
.forgejo/workflows/package.yml
Normal file
40
.forgejo/workflows/package.yml
Normal 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"
|
||||
282
Cargo.lock
generated
282
Cargo.lock
generated
|
|
@ -2,12 +2,30 @@
|
|||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "async-broadcast"
|
||||
version = "0.7.2"
|
||||
|
|
@ -70,6 +88,18 @@ version = "1.5.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.0"
|
||||
|
|
@ -78,8 +108,8 @@ checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
|||
|
||||
[[package]]
|
||||
name = "bread-theme"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/Breadway/bread-ecosystem?tag=v0.1.0#6b5f4f475f66a645b08cb865e6dda8228d23679b"
|
||||
version = "0.2.3"
|
||||
source = "git+https://github.com/Breadway/bread-ecosystem?tag=v0.2.8#77417d552130281ff787e07d52541eb25e9d533b"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"gtk4",
|
||||
|
|
@ -89,7 +119,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "breadbar"
|
||||
version = "0.1.1"
|
||||
version = "0.1.7"
|
||||
dependencies = [
|
||||
"bread-theme",
|
||||
"futures-lite",
|
||||
|
|
@ -97,6 +127,7 @@ dependencies = [
|
|||
"gtk4-layer-shell",
|
||||
"hyprland",
|
||||
"relm4",
|
||||
"resvg",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
|
|
@ -109,6 +140,12 @@ version = "3.20.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.1"
|
||||
|
|
@ -121,7 +158,7 @@ version = "0.22.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5cc8d9aa793480744cd9a0524fef1a2e197d9eaa0f739cde19d16aba530dcb95"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.13.0",
|
||||
"cairo-sys-rs",
|
||||
"glib",
|
||||
"libc",
|
||||
|
|
@ -172,12 +209,27 @@ dependencies = [
|
|||
"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]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "data-url"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "2.1.1"
|
||||
|
|
@ -271,6 +323,15 @@ dependencies = [
|
|||
"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]]
|
||||
name = "event-listener"
|
||||
version = "5.4.1"
|
||||
|
|
@ -301,6 +362,15 @@ dependencies = [
|
|||
"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]]
|
||||
name = "field-offset"
|
||||
version = "0.3.6"
|
||||
|
|
@ -311,6 +381,22 @@ dependencies = [
|
|||
"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]]
|
||||
name = "flume"
|
||||
version = "0.12.0"
|
||||
|
|
@ -591,7 +677,7 @@ version = "0.22.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c207e04e51605dcf7b2924c41591b3a10e1438eaac5bcf448fb91f325381104a"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.13.0",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
|
|
@ -720,7 +806,7 @@ version = "0.8.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4069987ff4793699511a251028cc336b438e46565b463f111250148d574752a"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.13.0",
|
||||
"gdk4",
|
||||
"glib",
|
||||
"glib-sys",
|
||||
|
|
@ -835,6 +921,12 @@ version = "2.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||
|
||||
[[package]]
|
||||
name = "imagesize"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
|
|
@ -871,6 +963,17 @@ version = "3.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
|
|
@ -928,6 +1031,16 @@ dependencies = [
|
|||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.1"
|
||||
|
|
@ -939,6 +1052,15 @@ dependencies = [
|
|||
"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]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
|
|
@ -997,6 +1119,12 @@ version = "0.1.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
|
||||
|
||||
[[package]]
|
||||
name = "pico-args"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
|
|
@ -1009,6 +1137,19 @@ version = "0.3.33"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
|
|
@ -1103,6 +1244,35 @@ dependencies = [
|
|||
"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]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
|
|
@ -1118,7 +1288,7 @@ version = "1.1.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.13.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
|
|
@ -1216,6 +1386,27 @@ dependencies = [
|
|||
"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]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
|
|
@ -1247,6 +1438,25 @@ dependencies = [
|
|||
"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]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
|
|
@ -1310,6 +1520,32 @@ dependencies = [
|
|||
"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]]
|
||||
name = "tokio"
|
||||
version = "1.52.3"
|
||||
|
|
@ -1449,6 +1685,28 @@ version = "0.2.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "usvg"
|
||||
version = "0.44.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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"
|
||||
|
|
@ -1563,7 +1821,7 @@ version = "0.244.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.13.0",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap",
|
||||
"semver",
|
||||
|
|
@ -1723,7 +1981,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
"bitflags 2.13.0",
|
||||
"indexmap",
|
||||
"log",
|
||||
"serde",
|
||||
|
|
@ -1759,6 +2017,12 @@ version = "0.8.28"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
|
||||
|
||||
[[package]]
|
||||
name = "xmlwriter"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
|
||||
|
||||
[[package]]
|
||||
name = "zbus"
|
||||
version = "5.16.0"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "breadbar"
|
||||
version = "0.1.1"
|
||||
version = "0.1.7"
|
||||
edition = "2021"
|
||||
description = "Minimal status bar and notification daemon for Hyprland on Wayland"
|
||||
license = "MIT"
|
||||
|
|
@ -10,7 +10,7 @@ keywords = ["wayland", "hyprland", "bar", "status-bar", "gtk4"]
|
|||
categories = ["gui"]
|
||||
|
||||
[dependencies]
|
||||
bread-theme = { git = "https://github.com/Breadway/bread-ecosystem", tag = "v0.1.0", features = ["gtk"] }
|
||||
bread-theme = { git = "https://github.com/Breadway/bread-ecosystem", tag = "v0.2.8", features = ["gtk"] }
|
||||
gtk4 = { version = "0.11", features = ["v4_12"] }
|
||||
gtk4-layer-shell = "0.8"
|
||||
relm4 = { version = "0.11", features = ["macros"] }
|
||||
|
|
@ -20,6 +20,9 @@ zbus = { version = "5", default-features = false, features = ["tokio"] }
|
|||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "time", "process", "signal", "sync"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
Minimal status bar and notification daemon for [Hyprland](https://hyprland.org/) on Wayland.
|
||||
|
||||
A single Rust binary that provides a full-width top bar and a standards-compliant D-Bus notification daemon, with no system tray, no launcher, and no wallpaper logic.
|
||||
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
|
||||
|
||||
|
|
@ -10,7 +10,7 @@ A single Rust binary that provides a full-width top bar and a standards-complian
|
|||
|
||||
- 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
|
||||
- Right: CPU%, RAM, power draw (W), battery level + AC indicator, Bluetooth state, WiFi SSID with signal strength, system tray (SNI)
|
||||
|
||||
**Notification daemon**:
|
||||
|
||||
|
|
@ -105,6 +105,7 @@ Example — change the font size:
|
|||
| `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 |
|
||||
|
|
|
|||
36
packaging/arch/PKGBUILD
Normal file
36
packaging/arch/PKGBUILD
Normal 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"
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
pub mod clock;
|
||||
pub mod stats;
|
||||
pub mod tray;
|
||||
pub mod workspaces;
|
||||
|
|
|
|||
|
|
@ -15,22 +15,25 @@ static BT_CONN: AsyncOnce<zbus::Connection> = AsyncOnce::const_new();
|
|||
static BT_CACHE: LazyLock<Mutex<&'static str>> = LazyLock::new(|| Mutex::new(BT_OFF));
|
||||
static BT_TICK: AtomicU8 = AtomicU8::new(0);
|
||||
|
||||
pub const WIFI_STRONG: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/WiFi Strong.svg");
|
||||
pub const WIFI_MEDIUM: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/WiFi Medium.svg");
|
||||
pub const WIFI_WEAK: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/WiFi Weak.svg");
|
||||
pub const WIFI_OFF: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/WiFi Disconnect.svg");
|
||||
// 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 = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/Battery 3 Bars.svg");
|
||||
pub const BAT_MID: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/Battery 2 Bars.svg");
|
||||
pub const BAT_LOW: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/Battery 1 Bar.svg");
|
||||
pub const AC_POWER: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/AC Power.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 = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/Bluetooth Off.svg");
|
||||
pub const BT_ON: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/Bluetooth.svg");
|
||||
pub const BT_CONNECTED: &str = concat!(
|
||||
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)]
|
||||
pub struct Stats {
|
||||
|
|
|
|||
240
src/bar/tray.rs
Normal file
240
src/bar/tray.rs
Normal 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)
|
||||
}
|
||||
65
src/main.rs
65
src/main.rs
|
|
@ -1,6 +1,10 @@
|
|||
// 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 {
|
||||
($n:literal) => {
|
||||
concat!(env!("CARGO_MANIFEST_DIR"), "/assets/", $n)
|
||||
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/", $n))
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -34,6 +38,8 @@ pub struct App {
|
|||
wifi_img: gtk4::Image,
|
||||
// Pre-loaded textures indexed by constant pointer values.
|
||||
wifi_textures: std::collections::HashMap<usize, gtk4::gdk::Texture>,
|
||||
tray_box: gtk4::Box,
|
||||
tray_items: std::collections::HashMap<String, gtk4::Button>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -42,6 +48,7 @@ pub enum AppInput {
|
|||
ActiveWorkspace(WorkspaceId),
|
||||
ClockTick,
|
||||
StatsUpdate(bar::stats::Stats),
|
||||
TrayUpdate(bar::tray::TrayUpdate),
|
||||
}
|
||||
|
||||
#[relm4::component(pub)]
|
||||
|
|
@ -153,6 +160,8 @@ impl SimpleComponent for App {
|
|||
wifi_lbl: wifi_lbl.clone(),
|
||||
wifi_img: wifi_img.clone(),
|
||||
wifi_textures,
|
||||
tray_box: gtk4::Box::new(gtk4::Orientation::Horizontal, 4),
|
||||
tray_items: std::collections::HashMap::new(),
|
||||
};
|
||||
let widgets = view_output!();
|
||||
model.workspace_box = widgets.workspace_box.clone();
|
||||
|
|
@ -179,12 +188,15 @@ impl SimpleComponent for App {
|
|||
wifi_pair.append(&wifi_img);
|
||||
wifi_pair.append(&wifi_lbl);
|
||||
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));
|
||||
|
||||
theme::apply();
|
||||
bar::workspaces::spawn_watcher(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();
|
||||
osd::spawn();
|
||||
|
||||
|
|
@ -228,6 +240,26 @@ impl SimpleComponent for App {
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -246,24 +278,39 @@ impl App {
|
|||
}
|
||||
}
|
||||
|
||||
fn stat_pair(icon_path: &str, label: >k4::Label) -> gtk4::Box {
|
||||
fn stat_pair(icon_svg: &str, label: >k4::Label) -> gtk4::Box {
|
||||
let pair = gtk4::Box::new(gtk4::Orientation::Horizontal, 0);
|
||||
pair.add_css_class("stat-pair");
|
||||
let img = gtk4::Image::from_paintable(Some(&svg_texture(icon_path)));
|
||||
let img = gtk4::Image::from_paintable(Some(&svg_texture(icon_svg)));
|
||||
img.add_css_class("stat-icon");
|
||||
pair.append(&img);
|
||||
pair.append(label);
|
||||
pair
|
||||
}
|
||||
|
||||
fn svg_texture(path: &str) -> gtk4::gdk::Texture {
|
||||
// Rasterise an (embedded) SVG to a texture. Done in pure Rust with resvg
|
||||
// because librsvg dropped its gdk-pixbuf SVG loader, so gdk::Texture::from_bytes
|
||||
// can no longer decode SVG on a stock system.
|
||||
fn svg_texture(svg_src: &str) -> gtk4::gdk::Texture {
|
||||
use resvg::{tiny_skia, usvg};
|
||||
let fg = theme::fg_color();
|
||||
let svg = std::fs::read_to_string(path)
|
||||
.unwrap_or_default()
|
||||
let svg = svg_src
|
||||
.replace("currentColor", &fg)
|
||||
.replace(r#"width="24" height="24""#, r#"width="16" height="16""#);
|
||||
let bytes = gtk4::glib::Bytes::from_owned(svg.into_bytes());
|
||||
gtk4::gdk::Texture::from_bytes(&bytes).expect("svg load")
|
||||
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() -> gtk4::Label {
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ impl NotifServer {
|
|||
(
|
||||
"breadbar".into(),
|
||||
"breadway".into(),
|
||||
"0.1.0".into(),
|
||||
env!("CARGO_PKG_VERSION").into(),
|
||||
"1.2".into(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ fn volume_watcher(tx: mpsc::Sender<OsdEvent>) {
|
|||
let stdout = child.stdout.take().unwrap();
|
||||
let reader = BufReader::new(stdout);
|
||||
|
||||
for line in reader.lines().flatten() {
|
||||
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);
|
||||
|
|
|
|||
63
src/theme.rs
63
src/theme.rs
|
|
@ -1,57 +1,68 @@
|
|||
use bread_theme::{gtk as bgtk, hex_to_rgba, load_palette};
|
||||
use bread_theme::{gtk as bgtk, hex_to_rgba, ink_on, load_palette};
|
||||
use gtk4::CssProvider;
|
||||
use std::cell::RefCell;
|
||||
|
||||
thread_local! {
|
||||
static PROVIDER: RefCell<Option<CssProvider>> = const { RefCell::new(None) };
|
||||
static USER_PROVIDER: RefCell<Option<CssProvider>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
fn load_css() -> String {
|
||||
let p = load_palette();
|
||||
// breadbar-specific rules only — fonts, base colours, and generic widgets
|
||||
// come from the shared ecosystem stylesheet (applied first in `apply()`).
|
||||
// Colour is set on each surface (bar, active workspace pill, notification
|
||||
// card) and child labels inherit it, so text stays legible whatever lightness
|
||||
// pywal hands a given slot. `on_*` are luminance-picked ink (black/white) for
|
||||
// that background — the pywal hues themselves are untouched.
|
||||
format!(
|
||||
"* {{ font-family: 'Varela Round', sans-serif; font-size: 14px; }}\
|
||||
window.breadbar {{ background-color: {bg_rgba}; border-radius: 0; }}\
|
||||
label {{ color: {fg}; }}\
|
||||
.workspace-btn {{ background: transparent; color: {fg}; opacity: 0.45;\
|
||||
border-radius: 0 0 8px 8px; border: none; outline: none; box-shadow: none;\
|
||||
"window.breadbar {{ background-color: {bg_rgba}; color: {on_bg}; border-radius: 0; }}\
|
||||
.workspace-btn {{ background: transparent; opacity: 0.45;\
|
||||
border-radius: 0; border: none; outline: none; box-shadow: none;\
|
||||
min-width: 24px; padding: 4px 8px; }}\
|
||||
.workspace-btn:hover {{ opacity: 0.8; }}\
|
||||
.workspace-btn.active {{ background: {accent}; opacity: 1; }}\
|
||||
.workspace-btn.active {{ background: {accent}; color: {on_accent}; opacity: 1; }}\
|
||||
.stats-box {{ margin-right: 8px; }}\
|
||||
.stat-pair {{ margin-right: 12px; }}\
|
||||
.stat-icon {{ margin-right: 5px; }}\
|
||||
.bt-icon {{ margin-right: 12px; }}\
|
||||
window.breadbar-notification {{ background-color: alpha({bg_plain}, 0.95); }}\
|
||||
.notification-card {{ background: {surface}; border-radius: 8px;\
|
||||
window.breadbar-notification {{ background-color: alpha({bg_plain}, 0.95); color: {on_bg}; }}\
|
||||
.notification-card {{ background: {surface}; color: {on_surface}; border-radius: 8px;\
|
||||
padding: 12px; margin-bottom: 8px; }}\
|
||||
.notification-summary {{ font-weight: bold; color: {fg}; }}\
|
||||
.notification-app {{ color: {fg}; opacity: 0.6; }}\
|
||||
.notification-body {{ color: {fg}; }}\
|
||||
window.breadbar-osd {{ background-color: alpha({bg_plain}, 0.95); border-radius: 8px; }}\
|
||||
.osd-kind {{ color: {fg}; opacity: 0.75; font-size: 12px; }}\
|
||||
.osd-pct {{ color: {fg}; font-weight: bold; font-size: 12px; }}\
|
||||
.notification-summary {{ font-weight: bold; }}\
|
||||
.notification-app {{ opacity: 0.6; }}\
|
||||
window.breadbar-osd {{ background-color: alpha({bg_plain}, 0.95); color: {on_bg}; border-radius: 8px; }}\
|
||||
.osd-kind {{ opacity: 0.75; font-size: 12px; }}\
|
||||
.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,
|
||||
fg = p.foreground,
|
||||
accent = p.color4,
|
||||
trough = hex_to_rgba(&p.color4, 0.25),
|
||||
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),
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the current foreground colour (used for icon tinting in the stats bar).
|
||||
/// Returns the ink colour for icon tinting in the stats bar — the same
|
||||
/// luminance-picked colour the bar's text uses, so icons stay legible on the bar
|
||||
/// whatever lightness pywal gives the background.
|
||||
pub fn fg_color() -> String {
|
||||
load_palette().foreground
|
||||
ink_on(&load_palette().background).to_string()
|
||||
}
|
||||
|
||||
/// Apply (or reload) the theme CSS. Safe to call from `glib::MainContext::invoke`.
|
||||
pub fn apply() {
|
||||
let css = load_css();
|
||||
PROVIDER.with(|cell| bgtk::apply_css(&css, cell));
|
||||
// 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"));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue