step 8-10: notification daemon (zbus), popup window, SIGHUP theme reload
This commit is contained in:
parent
f1b471652b
commit
3100ee0591
4 changed files with 195 additions and 1 deletions
11
src/main.rs
11
src/main.rs
|
|
@ -99,6 +99,7 @@ impl SimpleComponent for App {
|
||||||
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);
|
||||||
|
notifications::spawn();
|
||||||
|
|
||||||
ComponentParts { model, widgets }
|
ComponentParts { model, widgets }
|
||||||
}
|
}
|
||||||
|
|
@ -138,6 +139,16 @@ impl App {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
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");
|
let app = RelmApp::new("sh.breadway.aster");
|
||||||
app.run::<App>(());
|
app.run::<App>(());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1,80 @@
|
||||||
pub mod popup;
|
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<NotifEvent>,
|
||||||
|
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<String>,
|
||||||
|
_hints: std::collections::HashMap<String, OwnedValue>,
|
||||||
|
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<String> {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<RefCell<HashMap<u32, gtk4::Box>>>;
|
||||||
|
|
||||||
|
pub async fn run(mut rx: Receiver<NotifEvent>) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,14 @@ fn load_css() -> String {
|
||||||
.workspace-btn {{ background: {surface}; color: {fg}; border-radius: 4px;\
|
.workspace-btn {{ background: {surface}; color: {fg}; border-radius: 4px;\
|
||||||
border: none; min-width: 24px; padding: 0 8px; }}\
|
border: none; min-width: 24px; padding: 0 8px; }}\
|
||||||
.workspace-btn:hover, .workspace-btn.active {{ background: {accent}; }}\
|
.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),
|
bg_rgba = hex_to_rgba(&bg, 0.92),
|
||||||
surface = surface,
|
surface = surface,
|
||||||
fg = fg,
|
fg = fg,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue