wayle_notification/core/
notification.rs1use std::cmp::PartialEq;
2
3use chrono::{DateTime, Utc};
4use derive_more::Debug;
5use tokio::sync::broadcast;
6use tracing::instrument;
7use wayle_core::Property;
8use zbus::Connection;
9
10use super::{
11 controls::NotificationControls,
12 types::{Action, NotificationHints, NotificationProps},
13};
14use crate::{
15 error::Error,
16 events::NotificationEvent,
17 types::{Category, ClosedReason, Urgency},
18};
19
20#[derive(Clone, Debug)]
26pub struct Notification {
27 #[debug(skip)]
28 zbus_connection: Connection,
29 #[debug(skip)]
30 notif_tx: broadcast::Sender<NotificationEvent>,
31
32 pub id: u32,
34 pub app_name: Property<Option<String>>,
38 pub replaces_id: Property<Option<u32>>,
40 pub app_icon: Property<Option<String>>,
42 pub summary: Property<String>,
47 pub body: Property<Option<String>>,
54 pub actions: Property<Vec<Action>>,
59 pub default_action: Property<Option<Action>>,
61 pub hints: Property<Option<NotificationHints>>,
68 pub expire_timeout: Property<Option<u32>>,
73 pub urgency: Property<Urgency>,
75 pub category: Property<Option<Category>>,
77 pub timestamp: Property<DateTime<Utc>>,
79 pub image_path: Property<Option<String>>,
81 pub desktop_entry: Property<Option<String>>,
83 pub is_transient: Property<bool>,
85 pub is_resident: Property<bool>,
87 pub sound_file: Property<Option<String>>,
89 pub sound_name: Property<Option<String>>,
91 pub suppress_sound: Property<bool>,
93 pub x: Property<Option<i32>>,
95 pub y: Property<Option<i32>>,
97 pub action_icons: Property<bool>,
99}
100
101impl PartialEq for Notification {
102 fn eq(&self, other: &Self) -> bool {
103 self.id == other.id
104 }
105}
106
107impl Notification {
108 pub(crate) fn new(
109 props: NotificationProps,
110 connection: Connection,
111 notif_tx: broadcast::Sender<NotificationEvent>,
112 ) -> Self {
113 Self::from_props(props, connection, notif_tx)
114 }
115
116 #[instrument(skip(self), fields(notification_id = %self.id))]
119 pub fn dismiss(&self) {
120 let _ = self.notif_tx.send(NotificationEvent::Remove(
121 self.id,
122 ClosedReason::DismissedByUser,
123 ));
124 }
125
126 #[instrument(skip(self), fields(notification_id = %self.id, action = %action_key), err)]
133 pub async fn invoke(&self, action_key: &str) -> Result<(), Error> {
134 NotificationControls::invoke(&self.zbus_connection, &self.id, action_key).await?;
135 if !self.is_resident.get() {
136 let _ = self
137 .notif_tx
138 .send(NotificationEvent::Remove(self.id, ClosedReason::Closed));
139 }
140 Ok(())
141 }
142
143 #[allow(clippy::too_many_lines)]
144 fn from_props(
145 props: NotificationProps,
146 connection: Connection,
147 notif_tx: broadcast::Sender<NotificationEvent>,
148 ) -> Notification {
149 let app_name = if !props.app_name.is_empty() {
150 Some(props.app_name)
151 } else {
152 None
153 };
154
155 let app_icon = if !props.app_icon.is_empty() {
156 Some(props.app_icon)
157 } else {
158 None
159 };
160
161 let replaces_id = if props.replaces_id > 0 {
162 Some(props.replaces_id)
163 } else {
164 None
165 };
166
167 let body = if !props.body.is_empty() {
168 Some(props.body)
169 } else {
170 None
171 };
172
173 let urgency = &props
174 .hints
175 .get("urgency")
176 .and_then(|hint| hint.downcast_ref::<u8>().ok())
177 .map_or(Urgency::Normal, Urgency::from);
178
179 let category = props
180 .hints
181 .get("category")
182 .and_then(|hint| hint.downcast_ref::<String>().ok())
183 .and_then(|category| category.parse().ok());
184
185 let image_path = props
186 .hints
187 .get("image-path")
188 .and_then(|hint| hint.downcast_ref::<String>().ok());
189
190 let desktop_entry = props
191 .hints
192 .get("desktop-entry")
193 .and_then(|hint| hint.downcast_ref::<String>().ok());
194
195 let is_transient = props
196 .hints
197 .get("transient")
198 .and_then(|hint| hint.downcast_ref::<bool>().ok())
199 .unwrap_or(false);
200
201 let is_resident = props
202 .hints
203 .get("resident")
204 .and_then(|hint| hint.downcast_ref::<bool>().ok())
205 .unwrap_or(false);
206
207 let sound_file = props
208 .hints
209 .get("sound-file")
210 .and_then(|hint| hint.downcast_ref::<String>().ok());
211
212 let sound_name = props
213 .hints
214 .get("sound-name")
215 .and_then(|hint| hint.downcast_ref::<String>().ok());
216
217 let suppress_sound = props
218 .hints
219 .get("suppress-sound")
220 .and_then(|hint| hint.downcast_ref::<bool>().ok())
221 .unwrap_or(false);
222
223 let x = props
224 .hints
225 .get("x")
226 .and_then(|hint| hint.downcast_ref::<i32>().ok());
227
228 let y = props
229 .hints
230 .get("y")
231 .and_then(|hint| hint.downcast_ref::<i32>().ok());
232
233 let action_icons = props
234 .hints
235 .get("action-icons")
236 .and_then(|hint| hint.downcast_ref::<bool>().ok())
237 .unwrap_or(false);
238
239 let parsed_actions = Action::parse_dbus_actions(&props.actions);
240 let default_action = parsed_actions
241 .iter()
242 .find(|action| action.id == Action::DEFAULT_ID)
243 .cloned();
244
245 let hints = if !props.hints.is_empty() {
246 Some(props.hints)
247 } else {
248 None
249 };
250
251 let expire_timeout = match props.expire_timeout {
252 t if t > 0 => Some(t as u32),
253 0 => Some(0),
254 _ => None,
255 };
256
257 let id = props.id;
258
259 Self {
260 zbus_connection: connection.clone(),
261 notif_tx,
262 id,
263 app_name: Property::new(app_name),
264 app_icon: Property::new(app_icon),
265 replaces_id: Property::new(replaces_id),
266 summary: Property::new(props.summary),
267 actions: Property::new(parsed_actions),
268 default_action: Property::new(default_action),
269 body: Property::new(body),
270 hints: Property::new(hints),
271 expire_timeout: Property::new(expire_timeout),
272 urgency: Property::new(*urgency),
273 category: Property::new(category),
274 timestamp: Property::new(props.timestamp),
275 image_path: Property::new(image_path),
276 desktop_entry: Property::new(desktop_entry),
277 is_transient: Property::new(is_transient),
278 is_resident: Property::new(is_resident),
279 sound_file: Property::new(sound_file),
280 sound_name: Property::new(sound_name),
281 suppress_sound: Property::new(suppress_sound),
282 x: Property::new(x),
283 y: Property::new(y),
284 action_icons: Property::new(action_icons),
285 }
286 }
287}