From 3100ee059169e9b1001333c1d009a62497421154 Mon Sep 17 00:00:00 2001 From: Breadway Date: Sun, 17 May 2026 08:50:08 +0800 Subject: [PATCH] step 8-10: notification daemon (zbus), popup window, SIGHUP theme reload --- src/main.rs | 11 +++++ src/notifications/mod.rs | 79 +++++++++++++++++++++++++++++++ src/notifications/popup.rs | 97 ++++++++++++++++++++++++++++++++++++++ src/theme.rs | 9 +++- 4 files changed, 195 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 403ed91..f76d8d6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -99,6 +99,7 @@ impl SimpleComponent for App { bar::workspaces::spawn_watcher(sender.clone()); bar::clock::spawn_ticker(sender.clone()); bar::stats::spawn_poller(sender); + notifications::spawn(); ComponentParts { model, widgets } } @@ -138,6 +139,16 @@ impl App { } fn main() { + // Reload theme CSS on SIGHUP (e.g. after pywal runs). + relm4::spawn(async { + use tokio::signal::unix::{signal, SignalKind}; + let mut stream = signal(SignalKind::hangup()).expect("SIGHUP handler"); + loop { + stream.recv().await; + gtk4::glib::MainContext::default().invoke(theme::apply); + } + }); + let app = RelmApp::new("sh.breadway.aster"); app.run::(()); } diff --git a/src/notifications/mod.rs b/src/notifications/mod.rs index d9315e3..c6ccafd 100644 --- a/src/notifications/mod.rs +++ b/src/notifications/mod.rs @@ -1 +1,80 @@ pub mod popup; + +use std::sync::atomic::{AtomicU32, Ordering}; +use tokio::sync::mpsc; +use zbus::zvariant::OwnedValue; + +pub enum NotifEvent { + Show { id: u32, app_name: String, summary: String, body: String, timeout_ms: u32 }, + Close(u32), +} + +struct NotifServer { + tx: mpsc::Sender, + next_id: AtomicU32, +} + +#[zbus::interface(name = "org.freedesktop.Notifications")] +impl NotifServer { + async fn notify( + &self, + app_name: &str, + replaces_id: u32, + _app_icon: &str, + summary: &str, + body: &str, + _actions: Vec, + _hints: std::collections::HashMap, + expire_timeout: i32, + ) -> u32 { + let id = if replaces_id != 0 { + replaces_id + } else { + self.next_id.fetch_add(1, Ordering::Relaxed) + }; + let timeout_ms = if expire_timeout <= 0 { 5000 } else { expire_timeout as u32 }; + let _ = self + .tx + .send(NotifEvent::Show { + id, + app_name: app_name.to_string(), + summary: summary.to_string(), + body: body.to_string(), + timeout_ms, + }) + .await; + id + } + + async fn close_notification(&self, id: u32) { + let _ = self.tx.send(NotifEvent::Close(id)).await; + } + + fn get_capabilities(&self) -> Vec { + vec!["body".to_string()] + } + + fn get_server_information(&self) -> (String, String, String, String) { + ("aster".into(), "breadway".into(), "0.1.0".into(), "1.2".into()) + } +} + +pub fn spawn() { + let (tx, rx) = mpsc::channel(32); + + relm4::spawn(async move { + let server = NotifServer { tx, next_id: AtomicU32::new(1) }; + let _conn = zbus::connection::Builder::session() + .unwrap() + .name("org.freedesktop.Notifications") + .unwrap() + .serve_at("/org/freedesktop/Notifications", server) + .unwrap() + .build() + .await + .expect("failed to claim org.freedesktop.Notifications"); + std::future::pending::<()>().await + }); + + relm4::spawn_local(popup::run(rx)); +} diff --git a/src/notifications/popup.rs b/src/notifications/popup.rs index 8b13789..7340675 100644 --- a/src/notifications/popup.rs +++ b/src/notifications/popup.rs @@ -1 +1,98 @@ +use std::{cell::RefCell, collections::HashMap, rc::Rc, time::Duration}; +use gtk4::prelude::*; +use gtk4_layer_shell::{Edge, Layer, LayerShell}; +use tokio::sync::mpsc::Receiver; + +use super::NotifEvent; + +type Cards = Rc>>; + +pub async fn run(mut rx: Receiver) { + let window = create_window(); + let cards_box = gtk4::Box::new(gtk4::Orientation::Vertical, 4); + cards_box.set_margin_top(8); + cards_box.set_margin_bottom(8); + cards_box.set_margin_start(8); + cards_box.set_margin_end(8); + window.set_child(Some(&cards_box)); + + let cards: Cards = Rc::new(RefCell::new(HashMap::new())); + + while let Some(event) = rx.recv().await { + match event { + NotifEvent::Show { id, app_name, summary, body, timeout_ms } => { + // Replace existing card with same id (replaces_id case) + if let Some(old) = cards.borrow_mut().remove(&id) { + cards_box.remove(&old); + } + let card = make_card(&app_name, &summary, &body); + cards_box.prepend(&card); + cards.borrow_mut().insert(id, card.clone()); + window.set_visible(true); + + // Auto-dismiss via GLib-native timer (safe inside spawn_local) + let cards_clone = cards.clone(); + let cards_box_clone = cards_box.clone(); + let win_clone = window.clone(); + relm4::spawn_local(async move { + gtk4::glib::timeout_future(Duration::from_millis(timeout_ms as u64)).await; + dismiss(&cards_box_clone, &win_clone, &cards_clone, id); + }); + } + NotifEvent::Close(id) => { + dismiss(&cards_box, &window, &cards, id); + } + } + } +} + +fn dismiss(cards_box: >k4::Box, window: >k4::Window, cards: &Cards, id: u32) { + if let Some(card) = cards.borrow_mut().remove(&id) { + cards_box.remove(&card); + } + if cards.borrow().is_empty() { + window.set_visible(false); + } +} + +fn create_window() -> gtk4::Window { + let window = gtk4::Window::new(); + window.add_css_class("aster-notification"); + window.init_layer_shell(); + window.set_layer(Layer::Overlay); + window.set_anchor(Edge::Top, true); + window.set_anchor(Edge::Right, true); + window.set_margin(Edge::Top, 20); + window.set_margin(Edge::Right, 20); + window.set_default_width(320); + window +} + +fn make_card(app_name: &str, summary: &str, body: &str) -> gtk4::Box { + let card = gtk4::Box::new(gtk4::Orientation::Vertical, 4); + card.add_css_class("notification-card"); + + if !app_name.is_empty() { + let lbl = gtk4::Label::new(Some(app_name)); + lbl.add_css_class("notification-app"); + lbl.set_xalign(0.0); + card.append(&lbl); + } + + let summary_lbl = gtk4::Label::new(Some(summary)); + summary_lbl.add_css_class("notification-summary"); + summary_lbl.set_xalign(0.0); + summary_lbl.set_wrap(true); + card.append(&summary_lbl); + + if !body.is_empty() { + let body_lbl = gtk4::Label::new(Some(body)); + body_lbl.add_css_class("notification-body"); + body_lbl.set_xalign(0.0); + body_lbl.set_wrap(true); + card.append(&body_lbl); + } + + card +} diff --git a/src/theme.rs b/src/theme.rs index 460f594..c11abed 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -48,7 +48,14 @@ fn load_css() -> String { .workspace-btn {{ background: {surface}; color: {fg}; border-radius: 4px;\ border: none; min-width: 24px; padding: 0 8px; }}\ .workspace-btn:hover, .workspace-btn.active {{ background: {accent}; }}\ - label {{ color: {fg}; }}", + label {{ color: {fg}; }}\ + window.aster-notification {{ background-color: alpha({bg_plain}, 0.95); }}\ + .notification-card {{ background: {surface}; border-radius: 6px;\ + padding: 10px; margin-bottom: 4px; }}\ + .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,