1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
use std::cmp::PartialEq;
use chrono::{DateTime, Utc};
use derive_more::Debug;
use tokio::sync::broadcast;
use tracing::instrument;
use wayle_core::Property;
use zbus::Connection;
use super::{
controls::NotificationControls,
types::{Action, NotificationHints, NotificationProps},
};
use crate::{
error::Error,
events::NotificationEvent,
types::{Category, ClosedReason, Urgency},
};
/// A desktop notification.
///
/// Each notification displayed is allocated a unique ID by the server. This is unique
/// within the session. While the notification server is running, the ID will not be
/// recycled unless the capacity of a uint32 is exceeded.
#[derive(Clone, Debug)]
pub struct Notification {
#[debug(skip)]
zbus_connection: Connection,
#[debug(skip)]
notif_tx: broadcast::Sender<NotificationEvent>,
/// The ID of the notification
pub id: u32,
/// The optional name of the application sending the notification. This should be the
/// application's formal name, rather than some sort of ID. An example would be
/// "FredApp E-Mail Client," rather than "fredapp-email-client."
pub app_name: Property<Option<String>>,
/// An optional ID of an existing notification that this notification is intended to replace.
pub replaces_id: Property<Option<u32>>,
/// The notification icon.
pub app_icon: Property<Option<String>>,
/// This is a single line overview of the notification. For instance, "You have mail"
/// or "A friend has come online". It should generally not be longer than 40 characters,
/// though this is not a requirement, and server implementations should word wrap if
/// necessary. The summary must be encoded using UTF-8.
pub summary: Property<String>,
/// This is a multi-line body of text. Each line is a paragraph, server implementations
/// are free to word wrap them as they see fit.
///
/// The body may contain simple markup as specified in Markup. It must be encoded using UTF-8.
///
/// If the body is omitted, just the summary is displayed.
pub body: Property<Option<String>>,
/// Available actions for this notification.
///
/// Each action has an identifier and a human-readable label.
/// The "default" action is typically invoked when clicking the notification body.
pub actions: Property<Vec<Action>>,
/// The default action, triggered when clicking the notification body.
pub default_action: Property<Option<Action>>,
/// Hints are a way to provide extra data to a notification server that the server may
/// be able to make use of.
///
/// Neither clients nor notification servers are required to support any hints. Both
/// sides should assume that hints are not passed, and should ignore any hints they
/// do not understand.
pub hints: Property<Option<NotificationHints>>,
/// The timeout time in milliseconds since the display of the notification at which
/// the notification should automatically close.
///
/// `None` = server decides, `Some(0)` = never expires, `Some(ms)` = timeout in milliseconds.
pub expire_timeout: Property<Option<u32>>,
/// The urgency level.
pub urgency: Property<Urgency>,
/// The type of notification this is.
pub category: Property<Option<Category>>,
/// When the notification was created.
pub timestamp: Property<DateTime<Utc>>,
/// Path to an image file from hints.
pub image_path: Property<Option<String>>,
/// Desktop entry name of the application.
pub desktop_entry: Property<Option<String>>,
/// Whether the notification should be transient (not persisted).
pub is_transient: Property<bool>,
/// Whether the notification stays after action invocation.
pub is_resident: Property<bool>,
/// Path to a sound file to play when the notification pops up.
pub sound_file: Property<Option<String>>,
/// A themeable named sound to play when the notification pops up.
pub sound_name: Property<Option<String>>,
/// Whether to suppress playing sounds for this notification.
pub suppress_sound: Property<bool>,
/// X position hint for notification placement.
pub x: Property<Option<i32>>,
/// Y position hint for notification placement.
pub y: Property<Option<i32>>,
/// Whether action IDs should be interpreted as icon names.
pub action_icons: Property<bool>,
}
impl PartialEq for Notification {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Notification {
pub(crate) fn new(
props: NotificationProps,
connection: Connection,
notif_tx: broadcast::Sender<NotificationEvent>,
) -> Self {
Self::from_props(props, connection, notif_tx)
}
/// Dismisses the notification, removing it from history and emitting
/// the D-Bus NotificationClosed signal.
#[instrument(skip(self), fields(notification_id = %self.id))]
pub fn dismiss(&self) {
let _ = self.notif_tx.send(NotificationEvent::Remove(
self.id,
ClosedReason::DismissedByUser,
));
}
/// Invoke an action on the notification.
///
/// The notification is dismissed if it is not a resident.
///
/// # Errors
/// Returns error if the D-Bus signal emission fails.
#[instrument(skip(self), fields(notification_id = %self.id, action = %action_key), err)]
pub async fn invoke(&self, action_key: &str) -> Result<(), Error> {
NotificationControls::invoke(&self.zbus_connection, &self.id, action_key).await?;
if !self.is_resident.get() {
let _ = self
.notif_tx
.send(NotificationEvent::Remove(self.id, ClosedReason::Closed));
}
Ok(())
}
#[allow(clippy::too_many_lines)]
fn from_props(
props: NotificationProps,
connection: Connection,
notif_tx: broadcast::Sender<NotificationEvent>,
) -> Notification {
let app_name = if !props.app_name.is_empty() {
Some(props.app_name)
} else {
None
};
let app_icon = if !props.app_icon.is_empty() {
Some(props.app_icon)
} else {
None
};
let replaces_id = if props.replaces_id > 0 {
Some(props.replaces_id)
} else {
None
};
let body = if !props.body.is_empty() {
Some(props.body)
} else {
None
};
let urgency = &props
.hints
.get("urgency")
.and_then(|hint| hint.downcast_ref::<u8>().ok())
.map_or(Urgency::Normal, Urgency::from);
let category = props
.hints
.get("category")
.and_then(|hint| hint.downcast_ref::<String>().ok())
.and_then(|category| category.parse().ok());
let image_path = props
.hints
.get("image-path")
.and_then(|hint| hint.downcast_ref::<String>().ok());
let desktop_entry = props
.hints
.get("desktop-entry")
.and_then(|hint| hint.downcast_ref::<String>().ok());
let is_transient = props
.hints
.get("transient")
.and_then(|hint| hint.downcast_ref::<bool>().ok())
.unwrap_or(false);
let is_resident = props
.hints
.get("resident")
.and_then(|hint| hint.downcast_ref::<bool>().ok())
.unwrap_or(false);
let sound_file = props
.hints
.get("sound-file")
.and_then(|hint| hint.downcast_ref::<String>().ok());
let sound_name = props
.hints
.get("sound-name")
.and_then(|hint| hint.downcast_ref::<String>().ok());
let suppress_sound = props
.hints
.get("suppress-sound")
.and_then(|hint| hint.downcast_ref::<bool>().ok())
.unwrap_or(false);
let x = props
.hints
.get("x")
.and_then(|hint| hint.downcast_ref::<i32>().ok());
let y = props
.hints
.get("y")
.and_then(|hint| hint.downcast_ref::<i32>().ok());
let action_icons = props
.hints
.get("action-icons")
.and_then(|hint| hint.downcast_ref::<bool>().ok())
.unwrap_or(false);
let parsed_actions = Action::parse_dbus_actions(&props.actions);
let default_action = parsed_actions
.iter()
.find(|action| action.id == Action::DEFAULT_ID)
.cloned();
let hints = if !props.hints.is_empty() {
Some(props.hints)
} else {
None
};
let expire_timeout = match props.expire_timeout {
t if t > 0 => Some(t as u32),
0 => Some(0),
_ => None,
};
let id = props.id;
Self {
zbus_connection: connection.clone(),
notif_tx,
id,
app_name: Property::new(app_name),
app_icon: Property::new(app_icon),
replaces_id: Property::new(replaces_id),
summary: Property::new(props.summary),
actions: Property::new(parsed_actions),
default_action: Property::new(default_action),
body: Property::new(body),
hints: Property::new(hints),
expire_timeout: Property::new(expire_timeout),
urgency: Property::new(*urgency),
category: Property::new(category),
timestamp: Property::new(props.timestamp),
image_path: Property::new(image_path),
desktop_entry: Property::new(desktop_entry),
is_transient: Property::new(is_transient),
is_resident: Property::new(is_resident),
sound_file: Property::new(sound_file),
sound_name: Property::new(sound_name),
suppress_sound: Property::new(suppress_sound),
x: Property::new(x),
y: Property::new(y),
action_icons: Property::new(action_icons),
}
}
}