diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..77de25a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,56 @@ +name: release + +on: + push: + tags: ["v*"] + +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 system deps + run: sudo pacman -S --noconfirm gtk4 gtk4-layer-shell 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 "${PKG_DIR}" "${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 upload "${GITHUB_REF_NAME}" \ + "${PKG_DIR}/breadbar-x86_64" \ + "${PKG_DIR}/breadbar-x86_64.sha256" \ + --clobber diff --git a/Cargo.lock b/Cargo.lock index 067087b..b3b2530 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,10 +76,21 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +[[package]] +name = "bread-theme" +version = "0.1.0" +dependencies = [ + "dirs", + "gtk4", + "serde", + "serde_json", +] + [[package]] name = "breadbar" version = "0.1.0" dependencies = [ + "bread-theme", "futures-lite", "gtk4", "gtk4-layer-shell", @@ -189,6 +200,27 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "either" version = "1.15.0" @@ -235,7 +267,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -464,6 +496,17 @@ dependencies = [ "system-deps", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -518,7 +561,7 @@ dependencies = [ "gobject-sys", "libc", "system-deps", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -839,6 +882,15 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "libc", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -883,7 +935,7 @@ checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -892,6 +944,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "ordered-stream" version = "0.2.0" @@ -999,6 +1057,17 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror", +] + [[package]] name = "relm4" version = "0.11.0" @@ -1052,7 +1121,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1165,7 +1234,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1217,7 +1286,27 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "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]] @@ -1234,7 +1323,7 @@ dependencies = [ "socket2", "tokio-macros", "tracing", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1338,7 +1427,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1485,6 +1574,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "windows-sys" version = "0.61.2" @@ -1494,6 +1592,63 @@ dependencies = [ "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]] name = "winnow" version = "1.0.3" @@ -1626,7 +1781,7 @@ dependencies = [ "tracing", "uds_windows", "uuid", - "windows-sys", + "windows-sys 0.61.2", "winnow", "zbus_macros", "zbus_names", diff --git a/Cargo.toml b/Cargo.toml index 0096670..30b6e81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,9 @@ keywords = ["wayland", "hyprland", "bar", "status-bar", "gtk4"] categories = ["gui"] [dependencies] +# Path dep for local dev; replace with git dep on first tag: +# bread-theme = { git = "https://github.com/Breadway/bread-ecosystem", tag = "theme-v0.1.0", features = ["gtk"] } +bread-theme = { path = "../bread-ecosystem/bread-theme", features = ["gtk"] } gtk4 = { version = "0.11", features = ["v4_12"] } gtk4-layer-shell = "0.8" relm4 = { version = "0.11", features = ["macros"] } diff --git a/bakery.toml b/bakery.toml new file mode 100644 index 0000000..a334ea4 --- /dev/null +++ b/bakery.toml @@ -0,0 +1,11 @@ +name = "breadbar" +description = "Minimal status bar and notification daemon for Hyprland" +binaries = ["breadbar"] +system_deps = ["gtk4", "gtk4-layer-shell", "iw"] +bread_deps = [] + +[config] +dir = "~/.config/breadbar" + +[install] +post_install = [] diff --git a/src/bar/clock.rs b/src/bar/clock.rs index 76639d5..4501fde 100644 --- a/src/bar/clock.rs +++ b/src/bar/clock.rs @@ -3,7 +3,9 @@ use relm4::ComponentSender; pub fn current() -> String { 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) { diff --git a/src/main.rs b/src/main.rs index 8abaa65..10cca56 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ macro_rules! asset { mod bar; mod notifications; +mod osd; mod theme; use gtk4::prelude::*; @@ -185,6 +186,7 @@ impl SimpleComponent for App { bar::clock::spawn_ticker(sender.clone()); bar::stats::spawn_poller(sender); notifications::spawn(); + osd::spawn(); ComponentParts { model, widgets } } @@ -255,9 +257,10 @@ fn stat_pair(icon_path: &str, label: >k4::Label) -> gtk4::Box { } fn svg_texture(path: &str) -> gtk4::gdk::Texture { + let fg = theme::fg_color(); let svg = std::fs::read_to_string(path) .unwrap_or_default() - .replace("currentColor", "white") + .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") diff --git a/src/osd.rs b/src/osd.rs new file mode 100644 index 0000000..61f7ea4 --- /dev/null +++ b/src/osd.rs @@ -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::(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) { + 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().flatten() { + if line.contains("'change' on sink") { + if let Some(evt) = query_volume() { + let _ = tx.blocking_send(evt); + } + } + } +} + +fn query_volume() -> Option { + 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) { + 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::().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) { + 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 +} diff --git a/src/theme.rs b/src/theme.rs index 534e903..62ce751 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -1,5 +1,5 @@ +use bread_theme::{gtk as bgtk, hex_to_rgba, load_palette}; use gtk4::CssProvider; -use serde::Deserialize; use std::cell::RefCell; thread_local! { @@ -7,125 +7,53 @@ thread_local! { static USER_PROVIDER: RefCell> = const { RefCell::new(None) }; } -#[derive(Deserialize)] -struct WalColors { - 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 { - let home = std::env::var("HOME").unwrap_or_default(); - let text = - std::fs::read_to_string(format!("{home}/.cache/wal/colors.json")).unwrap_or_default(); - - let (bg, surface, fg, accent) = if let Ok(wal) = serde_json::from_str::(&text) { - ( - wal.special.background, - wal.colors.color0, - wal.colors.color15, - wal.colors.color1, - ) - } else { - ( - "#1e1e2e".to_string(), - "#181825".to_string(), - "#cdd6f4".to_string(), - "#89b4fa".to_string(), - ) - }; - + let p = load_palette(); format!( - "* {{ font-family: 'JetBrainsMono Nerd Font Mono', monospace; font-size: 14px; }}\ + "* {{ 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;\ - min-width: 24px; padding: 2px 8px; }}\ + min-width: 24px; padding: 4px 8px; }}\ .workspace-btn:hover {{ opacity: 0.8; }}\ .workspace-btn.active {{ background: {accent}; opacity: 1; }}\ .stats-box {{ margin-right: 8px; }}\ - .stat-pair {{ margin-right: 8px; }}\ - .stat-icon {{ margin-right: 3px; }}\ - .bt-icon {{ 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: 6px;\ - padding: 10px; margin-bottom: 4px; }}\ + .notification-card {{ background: {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}; }}", - bg_plain = bg, - bg_rgba = hex_to_rgba(&bg, 0.92), - surface = surface, - fg = fg, - accent = accent, + .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; }}\ + 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), ) } +/// Returns the current foreground colour (used for icon tinting in the stats bar). +pub fn fg_color() -> String { + load_palette().foreground +} + +/// Apply (or reload) the theme CSS. Safe to call from `glib::MainContext::invoke`. pub fn apply() { let css = load_css(); - let display = gtk4::gdk::Display::default().expect("no display"); + PROVIDER.with(|cell| bgtk::apply_css(&css, cell)); - PROVIDER.with(|cell| { - let mut guard = cell.borrow_mut(); - if let Some(p) = guard.as_ref() { - p.load_from_string(&css); - } else { - let p = CssProvider::new(); - p.load_from_string(&css); - gtk4::style_context_add_provider_for_display( - &display, - &p, - gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION, - ); - *guard = Some(p); - } - }); - - // User override: ~/.config/breadbar/style.css — send SIGHUP to reload. let home = std::env::var("HOME").unwrap_or_default(); - let user_path = format!("{home}/.config/breadbar/style.css"); - USER_PROVIDER.with(|cell| { - let mut guard = cell.borrow_mut(); - match std::fs::read_to_string(&user_path) { - Ok(user_css) => { - if let Some(p) = guard.as_ref() { - p.load_from_string(&user_css); - } else { - let p = CssProvider::new(); - p.load_from_string(&user_css); - gtk4::style_context_add_provider_for_display( - &display, - &p, - gtk4::STYLE_PROVIDER_PRIORITY_USER, - ); - *guard = Some(p); - } - } - Err(_) => { - if let Some(p) = guard.as_ref() { - p.load_from_string(""); - } - } - } - }); + let user_path = std::path::PathBuf::from(format!("{home}/.config/breadbar/style.css")); + USER_PROVIDER.with(|cell| bgtk::apply_user_css(&user_path, cell)); }