gardesk/garnotify / 44101a5

Browse files

notification: add store with timeout scheduling

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
44101a5e213ae06b3b74a4ce0d35e19e4d93cc8d
Parents
712b68f
Tree
f329269

1 changed file

StatusFile+-
A garnotify/src/notification/store.rs 196 0
garnotify/src/notification/store.rsadded
@@ -0,0 +1,196 @@
1
+//! Notification storage and timeout management
2
+
3
+use std::collections::HashMap;
4
+use std::sync::atomic::{AtomicU32, Ordering};
5
+use std::sync::Arc;
6
+use tokio::runtime::Handle;
7
+use tokio::sync::Mutex;
8
+use tokio::task::JoinHandle;
9
+use tracing::{debug, info};
10
+
11
+use super::types::{CloseReason, Notification, UrgencyTimeouts};
12
+
13
+/// Counter for notification IDs
14
+static NEXT_ID: AtomicU32 = AtomicU32::new(1);
15
+
16
+/// Generate the next notification ID
17
+pub fn next_notification_id() -> u32 {
18
+    NEXT_ID.fetch_add(1, Ordering::SeqCst)
19
+}
20
+
21
+/// Event sent for notification lifecycle changes
22
+#[derive(Debug, Clone)]
23
+pub enum NotificationEvent {
24
+    /// A new notification was created
25
+    Created(Notification),
26
+    /// A notification was updated (replaced)
27
+    Updated(Notification),
28
+    /// A notification should be closed
29
+    Closed { id: u32, reason: CloseReason },
30
+}
31
+
32
+/// Notification storage with timeout management
33
+pub struct NotificationStore {
34
+    /// Active notifications by ID
35
+    notifications: HashMap<u32, Notification>,
36
+    /// Timeout handles by notification ID
37
+    timeouts: HashMap<u32, JoinHandle<()>>,
38
+    /// Channel to send expiration events
39
+    event_tx: tokio::sync::mpsc::Sender<NotificationEvent>,
40
+    /// Default timeout configuration
41
+    urgency_timeouts: UrgencyTimeouts,
42
+    /// Tokio runtime handle for spawning tasks
43
+    tokio_handle: Handle,
44
+}
45
+
46
+impl NotificationStore {
47
+    /// Create a new notification store
48
+    pub fn new(
49
+        event_tx: tokio::sync::mpsc::Sender<NotificationEvent>,
50
+        urgency_timeouts: UrgencyTimeouts,
51
+        tokio_handle: Handle,
52
+    ) -> Self {
53
+        Self {
54
+            notifications: HashMap::new(),
55
+            timeouts: HashMap::new(),
56
+            event_tx,
57
+            urgency_timeouts,
58
+            tokio_handle,
59
+        }
60
+    }
61
+
62
+    /// Add a notification to the store
63
+    ///
64
+    /// If replaces_id > 0 and exists, replaces that notification.
65
+    /// Returns the notification ID.
66
+    pub fn add(&mut self, mut notification: Notification) -> u32 {
67
+        // Handle replacement
68
+        let is_replacement = notification.replaces_id > 0
69
+            && self.notifications.contains_key(&notification.replaces_id);
70
+
71
+        let id = if is_replacement {
72
+            // Cancel existing timeout
73
+            if let Some(handle) = self.timeouts.remove(&notification.replaces_id) {
74
+                handle.abort();
75
+            }
76
+            notification.replaces_id
77
+        } else {
78
+            notification.id
79
+        };
80
+
81
+        // Update ID if we're replacing
82
+        notification.id = id;
83
+
84
+        info!(
85
+            "Storing notification: id={} app=\"{}\" summary=\"{}\" urgency={}",
86
+            id, notification.app_name, notification.summary, notification.hints.urgency
87
+        );
88
+
89
+        // Schedule timeout if needed
90
+        self.schedule_timeout(&notification);
91
+
92
+        // Send event for UI to show/update popup
93
+        let event = if is_replacement {
94
+            NotificationEvent::Updated(notification.clone())
95
+        } else {
96
+            NotificationEvent::Created(notification.clone())
97
+        };
98
+        let tx = self.event_tx.clone();
99
+        let _ = self.tokio_handle.spawn(async move {
100
+            let _ = tx.send(event).await;
101
+        });
102
+
103
+        // Store notification
104
+        self.notifications.insert(id, notification);
105
+
106
+        id
107
+    }
108
+
109
+    /// Schedule a timeout for a notification
110
+    fn schedule_timeout(&mut self, notification: &Notification) {
111
+        let timeout_ms = notification.effective_timeout(
112
+            self.urgency_timeouts.normal,
113
+            &self.urgency_timeouts,
114
+        );
115
+
116
+        if let Some(ms) = timeout_ms {
117
+            let id = notification.id;
118
+            let tx = self.event_tx.clone();
119
+
120
+            debug!("Scheduling timeout for notification {}: {}ms", id, ms);
121
+
122
+            // Use the stored tokio handle to spawn the timeout task
123
+            // This allows spawning from non-tokio contexts (like zbus's executor)
124
+            let handle = self.tokio_handle.spawn(async move {
125
+                tokio::time::sleep(tokio::time::Duration::from_millis(ms)).await;
126
+                let _ = tx
127
+                    .send(NotificationEvent::Closed {
128
+                        id,
129
+                        reason: CloseReason::Expired,
130
+                    })
131
+                    .await;
132
+            });
133
+
134
+            self.timeouts.insert(id, handle);
135
+        } else {
136
+            debug!(
137
+                "Notification {} has no timeout (will persist)",
138
+                notification.id
139
+            );
140
+        }
141
+    }
142
+
143
+    /// Get a notification by ID
144
+    pub fn get(&self, id: u32) -> Option<&Notification> {
145
+        self.notifications.get(&id)
146
+    }
147
+
148
+    /// Remove a notification from the store
149
+    ///
150
+    /// Returns the removed notification if it existed.
151
+    pub fn remove(&mut self, id: u32) -> Option<Notification> {
152
+        // Cancel timeout
153
+        if let Some(handle) = self.timeouts.remove(&id) {
154
+            handle.abort();
155
+        }
156
+
157
+        self.notifications.remove(&id)
158
+    }
159
+
160
+    /// Get all active notifications
161
+    pub fn list(&self) -> Vec<&Notification> {
162
+        self.notifications.values().collect()
163
+    }
164
+
165
+    /// Get count of active notifications
166
+    pub fn count(&self) -> usize {
167
+        self.notifications.len()
168
+    }
169
+
170
+    /// Check if a notification exists
171
+    pub fn contains(&self, id: u32) -> bool {
172
+        self.notifications.contains_key(&id)
173
+    }
174
+
175
+    /// Clear all notifications
176
+    pub fn clear(&mut self) -> Vec<Notification> {
177
+        // Cancel all timeouts
178
+        for (_, handle) in self.timeouts.drain() {
179
+            handle.abort();
180
+        }
181
+
182
+        self.notifications.drain().map(|(_, n)| n).collect()
183
+    }
184
+}
185
+
186
+/// Thread-safe wrapper around NotificationStore
187
+pub type SharedNotificationStore = Arc<Mutex<NotificationStore>>;
188
+
189
+/// Create a new shared notification store
190
+pub fn new_shared_store(
191
+    event_tx: tokio::sync::mpsc::Sender<NotificationEvent>,
192
+    urgency_timeouts: UrgencyTimeouts,
193
+    tokio_handle: Handle,
194
+) -> SharedNotificationStore {
195
+    Arc::new(Mutex::new(NotificationStore::new(event_tx, urgency_timeouts, tokio_handle)))
196
+}