Merge branch 'fix/audit-findings' into dev

# Conflicts:
#	assets/icons-needed.txt
#	src/main.rs
#	src/notifications/mod.rs
#	src/notifications/popup.rs
This commit is contained in:
Breadway 2026-07-17 14:39:23 +08:00
commit 2cd2fbc7f6
5 changed files with 232 additions and 50 deletions

View file

@ -1,14 +1,29 @@
use std::{cell::RefCell, collections::HashMap, rc::Rc, time::Duration};
use std::{cell::RefCell, collections::HashMap, rc::Rc};
use gtk4::prelude::*;
use gtk4_layer_shell::{Edge, Layer, LayerShell};
use tokio::sync::mpsc::Receiver;
use super::{NotifEvent, Urgency};
use super::{Expire, NotifEvent, Urgency};
type Cards = Rc<RefCell<HashMap<u32, gtk4::Box>>>;
// Bumped every time an id gets a (re)placed card — an auto-dismiss timer
// scheduled for an earlier Show captures the generation it was scheduled
// under, and checks it's still current before dismissing. Without this, a
// notification that replaces an existing id (replaces_id) doesn't cancel
// the original's timer, so the *replacement* card gets dismissed on the
// *original*'s deadline instead of its own.
type Generations = Rc<RefCell<HashMap<u32, u64>>>;
pub async fn run(mut rx: Receiver<NotifEvent>) {
/// NotificationClosed reason codes per the freedesktop spec.
mod close_reason {
pub const EXPIRED: u32 = 1;
#[allow(dead_code)] // no in-app dismiss button exists yet (see make_card)
pub const DISMISSED_BY_USER: u32 = 2;
pub const CLOSE_NOTIFICATION_CALL: u32 = 3;
}
pub async fn run(mut rx: Receiver<NotifEvent>, conn: zbus::Connection) {
let window = create_window();
let cards_box = gtk4::Box::new(gtk4::Orientation::Vertical, 4);
cards_box.set_margin_top(8);
@ -18,6 +33,7 @@ pub async fn run(mut rx: Receiver<NotifEvent>) {
window.set_child(Some(&cards_box));
let cards: Cards = Rc::new(RefCell::new(HashMap::new()));
let generations: Generations = Rc::new(RefCell::new(HashMap::new()));
while let Some(event) = rx.recv().await {
match event {
@ -26,8 +42,8 @@ pub async fn run(mut rx: Receiver<NotifEvent>) {
app_name,
summary,
body,
timeout_ms,
urgency,
expire,
} => {
// Replace existing card with same id (replaces_id case)
if let Some(old) = cards.borrow_mut().remove(&id) {
@ -38,29 +54,75 @@ pub async fn run(mut rx: Receiver<NotifEvent>) {
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);
});
let my_generation = {
let mut gens = generations.borrow_mut();
let g = gens.entry(id).or_insert(0);
*g += 1;
*g
};
// `Expire::Never` (expire_timeout=0, or a critical-urgency
// notification with no explicit timeout) schedules no timer
// at all — it persists until an explicit CloseNotification.
if let Expire::After(duration) = expire {
let cards_clone = cards.clone();
let cards_box_clone = cards_box.clone();
let win_clone = window.clone();
let generations_clone = generations.clone();
let conn_clone = conn.clone();
relm4::spawn_local(async move {
gtk4::glib::timeout_future(duration).await;
let still_current =
generations_clone.borrow().get(&id) == Some(&my_generation);
if still_current
&& dismiss(&cards_box_clone, &win_clone, &cards_clone, id)
{
emit_closed(&conn_clone, id, close_reason::EXPIRED).await;
}
});
}
}
NotifEvent::Close(id) => {
dismiss(&cards_box, &window, &cards, id);
if dismiss(&cards_box, &window, &cards, id) {
emit_closed(&conn, id, close_reason::CLOSE_NOTIFICATION_CALL).await;
}
}
}
}
}
fn dismiss(cards_box: &gtk4::Box, window: &gtk4::Window, cards: &Cards, id: u32) {
if let Some(card) = cards.borrow_mut().remove(&id) {
cards_box.remove(&card);
}
/// Removes `id`'s card if present. Returns whether a card was actually
/// removed, so callers only emit `NotificationClosed` for a real dismissal
/// (not a no-op on an id that's already gone or was never shown).
fn dismiss(cards_box: &gtk4::Box, window: &gtk4::Window, cards: &Cards, id: u32) -> bool {
let removed = cards.borrow_mut().remove(&id);
let Some(card) = removed else {
return false;
};
cards_box.remove(&card);
if cards.borrow().is_empty() {
window.set_visible(false);
}
true
}
/// Emits the spec-mandated `NotificationClosed(id, reason)` signal. Sent
/// directly over the connection rather than through the zbus interface
/// macro's generated helper, since the dismiss decision happens here in the
/// popup task, not inside `NotifServer`'s own method bodies.
async fn emit_closed(conn: &zbus::Connection, id: u32, reason: u32) {
let result = conn
.emit_signal(
None::<&str>,
"/org/freedesktop/Notifications",
"org.freedesktop.Notifications",
"NotificationClosed",
&(id, reason),
)
.await;
if let Err(e) = result {
eprintln!("breadbar: failed to emit NotificationClosed for {id}: {e}");
}
}
fn create_window() -> gtk4::Window {