gardesk/garnotify / 712b68f

Browse files

notification: add types (Notification, Urgency, Hints, etc)

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
712b68f4715daa26352425bdadbcec269ad95263
Parents
38536ed
Tree
52b1220

1 changed file

StatusFile+-
A garnotify/src/notification/types.rs 322 0
garnotify/src/notification/types.rsadded
@@ -0,0 +1,322 @@
1
+//! Core notification types
2
+
3
+use serde::{Deserialize, Serialize};
4
+use std::collections::HashMap;
5
+use std::time::Instant;
6
+
7
+/// Notification urgency level
8
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
9
+#[serde(rename_all = "lowercase")]
10
+pub enum Urgency {
11
+    Low = 0,
12
+    #[default]
13
+    Normal = 1,
14
+    Critical = 2,
15
+}
16
+
17
+impl Urgency {
18
+    /// Convert from D-Bus byte value
19
+    pub fn from_byte(value: u8) -> Self {
20
+        match value {
21
+            0 => Urgency::Low,
22
+            2 => Urgency::Critical,
23
+            _ => Urgency::Normal,
24
+        }
25
+    }
26
+
27
+    /// Get string representation
28
+    pub fn as_str(&self) -> &'static str {
29
+        match self {
30
+            Urgency::Low => "low",
31
+            Urgency::Normal => "normal",
32
+            Urgency::Critical => "critical",
33
+        }
34
+    }
35
+}
36
+
37
+impl std::fmt::Display for Urgency {
38
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39
+        write!(f, "{}", self.as_str())
40
+    }
41
+}
42
+
43
+/// Raw image data from D-Bus hints
44
+#[derive(Debug, Clone)]
45
+pub struct ImageData {
46
+    pub width: i32,
47
+    pub height: i32,
48
+    pub rowstride: i32,
49
+    pub has_alpha: bool,
50
+    pub bits_per_sample: i32,
51
+    pub channels: i32,
52
+    pub data: Vec<u8>,
53
+}
54
+
55
+/// Notification action button
56
+#[derive(Debug, Clone, Serialize, Deserialize)]
57
+pub struct Action {
58
+    /// Action identifier (sent back in ActionInvoked signal)
59
+    pub key: String,
60
+    /// Human-readable label
61
+    pub label: String,
62
+}
63
+
64
+impl Action {
65
+    /// Parse actions from D-Bus array (alternating key, label pairs)
66
+    pub fn parse_from_dbus(actions: &[String]) -> Vec<Self> {
67
+        actions
68
+            .chunks(2)
69
+            .filter_map(|chunk| {
70
+                if chunk.len() == 2 {
71
+                    Some(Action {
72
+                        key: chunk[0].clone(),
73
+                        label: chunk[1].clone(),
74
+                    })
75
+                } else {
76
+                    None
77
+                }
78
+            })
79
+            .collect()
80
+    }
81
+}
82
+
83
+/// Parsed notification hints
84
+#[derive(Debug, Clone, Default)]
85
+pub struct Hints {
86
+    /// Urgency level (0=low, 1=normal, 2=critical)
87
+    pub urgency: Urgency,
88
+    /// Notification category (e.g., "email.arrived", "im.received")
89
+    pub category: Option<String>,
90
+    /// Desktop entry name of the calling application
91
+    pub desktop_entry: Option<String>,
92
+    /// Path to image file
93
+    pub image_path: Option<String>,
94
+    /// Raw image data
95
+    pub image_data: Option<ImageData>,
96
+    /// Path to sound file to play
97
+    pub sound_file: Option<String>,
98
+    /// Named sound from sound theme
99
+    pub sound_name: Option<String>,
100
+    /// Suppress notification sound
101
+    pub suppress_sound: bool,
102
+    /// Notification is transient (skip persistence)
103
+    pub transient: bool,
104
+    /// Keep notification after action invoked
105
+    pub resident: bool,
106
+    /// X coordinate for positioning
107
+    pub x: Option<i32>,
108
+    /// Y coordinate for positioning
109
+    pub y: Option<i32>,
110
+    /// Interpret action identifiers as icon names
111
+    pub action_icons: bool,
112
+}
113
+
114
+impl Hints {
115
+    /// Parse hints from D-Bus dictionary
116
+    pub fn parse_from_dbus(hints: &HashMap<String, zbus::zvariant::Value<'_>>) -> Self {
117
+        let mut result = Hints::default();
118
+
119
+        // Urgency
120
+        if let Some(v) = hints.get("urgency") {
121
+            if let Ok(u) = v.downcast_ref::<u8>() {
122
+                result.urgency = Urgency::from_byte(u);
123
+            }
124
+        }
125
+
126
+        // Category
127
+        if let Some(v) = hints.get("category") {
128
+            if let Ok(s) = v.downcast_ref::<&str>() {
129
+                result.category = Some(s.to_string());
130
+            }
131
+        }
132
+
133
+        // Desktop entry
134
+        if let Some(v) = hints.get("desktop-entry") {
135
+            if let Ok(s) = v.downcast_ref::<&str>() {
136
+                result.desktop_entry = Some(s.to_string());
137
+            }
138
+        }
139
+
140
+        // Image path (try both hyphen and underscore versions)
141
+        for key in &["image-path", "image_path"] {
142
+            if let Some(v) = hints.get(*key) {
143
+                if let Ok(s) = v.downcast_ref::<&str>() {
144
+                    result.image_path = Some(s.to_string());
145
+                    break;
146
+                }
147
+            }
148
+        }
149
+
150
+        // Sound file
151
+        if let Some(v) = hints.get("sound-file") {
152
+            if let Ok(s) = v.downcast_ref::<&str>() {
153
+                result.sound_file = Some(s.to_string());
154
+            }
155
+        }
156
+
157
+        // Sound name
158
+        if let Some(v) = hints.get("sound-name") {
159
+            if let Ok(s) = v.downcast_ref::<&str>() {
160
+                result.sound_name = Some(s.to_string());
161
+            }
162
+        }
163
+
164
+        // Suppress sound
165
+        if let Some(v) = hints.get("suppress-sound") {
166
+            if let Ok(b) = v.downcast_ref::<bool>() {
167
+                result.suppress_sound = b;
168
+            }
169
+        }
170
+
171
+        // Transient
172
+        if let Some(v) = hints.get("transient") {
173
+            if let Ok(b) = v.downcast_ref::<bool>() {
174
+                result.transient = b;
175
+            }
176
+        }
177
+
178
+        // Resident
179
+        if let Some(v) = hints.get("resident") {
180
+            if let Ok(b) = v.downcast_ref::<bool>() {
181
+                result.resident = b;
182
+            }
183
+        }
184
+
185
+        // X position
186
+        if let Some(v) = hints.get("x") {
187
+            if let Ok(x) = v.downcast_ref::<i32>() {
188
+                result.x = Some(x);
189
+            }
190
+        }
191
+
192
+        // Y position
193
+        if let Some(v) = hints.get("y") {
194
+            if let Ok(y) = v.downcast_ref::<i32>() {
195
+                result.y = Some(y);
196
+            }
197
+        }
198
+
199
+        // Action icons
200
+        if let Some(v) = hints.get("action-icons") {
201
+            if let Ok(b) = v.downcast_ref::<bool>() {
202
+                result.action_icons = b;
203
+            }
204
+        }
205
+
206
+        result
207
+    }
208
+}
209
+
210
+/// A desktop notification
211
+#[derive(Debug, Clone)]
212
+pub struct Notification {
213
+    /// Unique notification ID
214
+    pub id: u32,
215
+    /// Application name
216
+    pub app_name: String,
217
+    /// ID of notification this replaces (0 if none)
218
+    pub replaces_id: u32,
219
+    /// Icon name or path
220
+    pub app_icon: String,
221
+    /// Notification title/summary
222
+    pub summary: String,
223
+    /// Notification body text
224
+    pub body: String,
225
+    /// Action buttons
226
+    pub actions: Vec<Action>,
227
+    /// Parsed hints
228
+    pub hints: Hints,
229
+    /// Expiration timeout in milliseconds (-1 = default, 0 = never)
230
+    pub expire_timeout: i32,
231
+    /// When the notification was created
232
+    pub created_at: Instant,
233
+}
234
+
235
+impl Notification {
236
+    /// Create a new notification
237
+    pub fn new(
238
+        id: u32,
239
+        app_name: String,
240
+        replaces_id: u32,
241
+        app_icon: String,
242
+        summary: String,
243
+        body: String,
244
+        actions: Vec<Action>,
245
+        hints: Hints,
246
+        expire_timeout: i32,
247
+    ) -> Self {
248
+        Self {
249
+            id,
250
+            app_name,
251
+            replaces_id,
252
+            app_icon,
253
+            summary,
254
+            body,
255
+            actions,
256
+            hints,
257
+            expire_timeout,
258
+            created_at: Instant::now(),
259
+        }
260
+    }
261
+
262
+    /// Get the effective timeout in milliseconds
263
+    ///
264
+    /// Returns None if the notification should never expire
265
+    pub fn effective_timeout(&self, _default_timeout: i32, urgency_timeouts: &UrgencyTimeouts) -> Option<u64> {
266
+        let timeout = if self.expire_timeout == -1 {
267
+            // Use server default based on urgency
268
+            match self.hints.urgency {
269
+                Urgency::Low => urgency_timeouts.low,
270
+                Urgency::Normal => urgency_timeouts.normal,
271
+                Urgency::Critical => urgency_timeouts.critical,
272
+            }
273
+        } else {
274
+            self.expire_timeout
275
+        };
276
+
277
+        // 0 means never expire
278
+        if timeout <= 0 {
279
+            None
280
+        } else {
281
+            Some(timeout as u64)
282
+        }
283
+    }
284
+}
285
+
286
+/// Timeout configuration per urgency level
287
+#[derive(Debug, Clone)]
288
+pub struct UrgencyTimeouts {
289
+    pub low: i32,
290
+    pub normal: i32,
291
+    pub critical: i32,
292
+}
293
+
294
+impl Default for UrgencyTimeouts {
295
+    fn default() -> Self {
296
+        Self {
297
+            low: 10000,
298
+            normal: 5000,
299
+            critical: 0, // Never expire by default
300
+        }
301
+    }
302
+}
303
+
304
+/// Reason a notification was closed
305
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
306
+#[repr(u32)]
307
+pub enum CloseReason {
308
+    /// The notification expired
309
+    Expired = 1,
310
+    /// The notification was dismissed by the user
311
+    Dismissed = 2,
312
+    /// The notification was closed by CloseNotification call
313
+    Closed = 3,
314
+    /// Undefined/reserved reason
315
+    Undefined = 4,
316
+}
317
+
318
+impl CloseReason {
319
+    pub fn as_u32(self) -> u32 {
320
+        self as u32
321
+    }
322
+}