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:
commit
2cd2fbc7f6
5 changed files with 232 additions and 50 deletions
|
|
@ -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: >k4::Box, window: >k4::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: >k4::Box, window: >k4::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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue