Skip to main content

spell_framework/vault/
notification_manager.rs

1use crate::{
2    vault::{
3        BlockingNotification, DbusSignalEvent, Hint, NOTIFICATION_EVENT, Notification,
4        NotificationManager, Timeout, Urgency,
5    },
6    wayland_adapter::SpellWin,
7};
8use smithay_client_toolkit::reexports::calloop::channel::{self, Sender};
9use std::{cmp::Ordering, collections::HashMap};
10use tracing::{info, warn};
11use zbus::{fdo::Error as BusError, interface, object_server::SignalEmitter, zvariant::Value};
12
13/// It is an internal function used in the expansion of [`cast_spell`](crate::cast_spell) macro
14/// if the macro has a notification instance to run.
15pub fn set_notification(win: &SpellWin, ui: Box<dyn NotificationManager>) {
16    let (sender, rx) = channel::channel::<NotifyEvent>();
17    let (tx, dbus_rx) = tokio::sync::mpsc::unbounded_channel::<DbusSignalEvent>();
18
19    let layer_name = win.layer_name.clone();
20    let sender_cl = sender.clone();
21    std::thread::spawn(move || {
22        let rt = tokio::runtime::Builder::new_current_thread()
23            .enable_all()
24            .build()
25            .unwrap();
26        rt.block_on(async move {
27            //TODO handle and report the error here
28            let _ = notification_service_enter(sender_cl, layer_name, dbus_rx).await;
29        });
30    });
31
32    let _ = NOTIFICATION_EVENT.set(BlockingNotification::new(tx));
33    let _ = win
34        .get_handler()
35        .0
36        .insert_source(rx, move |event, _, _| match event {
37            channel::Event::Msg(msg) => match msg {
38                NotifyEvent::Noti(notification) => {
39                    if let Err(err) = ui.new_notification(notification) {
40                        warn!("{:?}", err);
41                    }
42                }
43                NotifyEvent::NotificationClosed(id) => {
44                    if let Err(err) = ui.close_notification(id) {
45                        warn!(" Error closing notification with id {} : {:?}", id, err);
46                    }
47                }
48            },
49            channel::Event::Closed => info!("Notification Channel to async thread is closed!"),
50        });
51}
52
53pub(crate) enum NotifyEvent {
54    Noti(Notification),
55    NotificationClosed(u32),
56}
57
58async fn notification_service_enter(
59    sender: Sender<NotifyEvent>,
60    layer_name: String,
61    mut rx: tokio::sync::mpsc::UnboundedReceiver<DbusSignalEvent>,
62) -> zbus::fdo::Result<()> {
63    let conn = zbus::Connection::session().await?;
64    conn.object_server()
65        .at(
66            "/org/freedesktop/Notifications",
67            NotificationHandler {
68                sender: sender.clone(),
69                layer_name,
70                next_id: 1,
71                notifications: Vec::new(),
72            },
73        )
74        .await?;
75    info!("Object server is setup");
76    if let Err(err) = conn.request_name("org.freedesktop.Notifications").await {
77        warn!("Error When creating notification crate {:?}", err);
78    }
79    info!("Notification service is live with the provided name");
80
81    while let Some(event) = rx.recv().await {
82        if let Ok(iface_ref) = conn
83            .object_server()
84            .interface::<_, NotificationHandler>("/org/freedesktop/Notifications")
85            .await
86        {
87            let emitter = iface_ref.signal_emitter();
88            match event {
89                DbusSignalEvent::ActionInvoked { id, action_key } => {
90                    let _ = NotificationHandler::action_invoked(emitter, id, &action_key).await;
91                }
92                DbusSignalEvent::NotificationClosed { id, reason } => {
93                    let _ = NotificationHandler::notification_closed(emitter, id, reason).await;
94                }
95            }
96        }
97    }
98
99    Ok(())
100}
101
102pub(crate) struct NotificationHandler {
103    pub(crate) sender: Sender<NotifyEvent>,
104    pub(crate) layer_name: String,
105    pub(crate) next_id: u32,
106    pub(crate) notifications: Vec<Notification>,
107}
108
109#[interface(name = "org.freedesktop.Notifications", proxy(gen_blocking = false,))]
110impl NotificationHandler {
111    async fn get_capabilities(&self) -> Result<Vec<String>, BusError> {
112        info!("capabilities called");
113        // body-markup will be implemented in the future maybe. icon-multi is not
114        // added since slint doen't yet support animated images.
115        Ok([
116            "actions",
117            "body",
118            "body-images",
119            "icon-static",
120            "persistence",
121        ]
122        .iter()
123        .map(|x| x.to_string())
124        .collect())
125    }
126
127    #[allow(clippy::too_many_arguments)]
128    async fn notify(
129        &mut self,
130        app_name: String,
131        replaces_id: u32,
132        app_icon: String,
133        summary: String,
134        body: String,
135        actions: Vec<String>,
136        hints: HashMap<String, zbus::zvariant::Value<'_>>,
137        expire_timeout: i32,
138    ) -> Result<u32, BusError> {
139        info!("Notifcation event received");
140        let notification = Notification {
141            id: replaces_id,
142            appname: app_name,
143            summary,
144            subtitle: None,
145            body,
146            icon: app_icon,
147            hints: hints
148                .into_iter()
149                .map(|(key, value)| {
150                    let val: Hint = match key.as_str() {
151                        "action-icons" => {
152                            if let Value::Bool(x) = value {
153                                Hint::ActionIcons(x)
154                            } else {
155                                Hint::Invalid
156                            }
157                        }
158                        "category" => {
159                            if let Value::Str(x) = value {
160                                Hint::Category(x.to_string())
161                            } else {
162                                Hint::Invalid
163                            }
164                        }
165                        "desktop-entry" => {
166                            if let Value::Str(x) = value {
167                                Hint::DesktopEntry(x.to_string())
168                            } else {
169                                Hint::Invalid
170                            }
171                        }
172                        "image-data" => Hint::Invalid,
173                        "image_data" => Hint::Invalid,
174                        "image-path" => {
175                            if let Value::Str(x) = value {
176                                Hint::ImagePath(x.to_string())
177                            } else {
178                                Hint::Invalid
179                            }
180                        }
181                        "image_path" => {
182                            if let Value::Str(x) = value {
183                                Hint::ImagePath(x.to_string())
184                            } else {
185                                Hint::Invalid
186                            }
187                        }
188                        "icon_data" => Hint::Invalid,
189                        "resident" => {
190                            if let Value::Bool(x) = value {
191                                Hint::Resident(x)
192                            } else {
193                                Hint::Invalid
194                            }
195                        }
196                        "sound-file" => {
197                            if let Value::Str(x) = value {
198                                Hint::SoundFile(x.to_string())
199                            } else {
200                                Hint::Invalid
201                            }
202                        }
203                        "sound-name" => {
204                            if let Value::Str(x) = value {
205                                Hint::SoundName(x.to_string())
206                            } else {
207                                Hint::Invalid
208                            }
209                        }
210                        "suppress-sound" => {
211                            if let Value::Bool(x) = value {
212                                Hint::SuppressSound(x)
213                            } else {
214                                Hint::Invalid
215                            }
216                        }
217                        "transient" => {
218                            if let Value::Bool(x) = value {
219                                Hint::Transient(x)
220                            } else {
221                                Hint::Invalid
222                            }
223                        }
224                        "x" => {
225                            if let Value::I32(x) = value {
226                                Hint::X(x)
227                            } else {
228                                Hint::Invalid
229                            }
230                        }
231                        "y" => {
232                            if let Value::I32(x) = value {
233                                Hint::Y(x)
234                            } else {
235                                Hint::Invalid
236                            }
237                        }
238                        "urgency" => {
239                            if let Value::U8(x) = value {
240                                Hint::Urgency(match x {
241                                    0 => Urgency::Low,
242                                    1 => Urgency::Normal,
243                                    2 => Urgency::Critical,
244                                    _ => Urgency::Normal,
245                                })
246                            } else {
247                                Hint::Invalid
248                            }
249                        }
250                        err => {
251                            warn!("Invalid hint passed with key: {}", err);
252                            Hint::Invalid
253                        }
254                    };
255                    val
256                })
257                .collect(),
258            actions,
259            timeout: match expire_timeout.cmp(&0) {
260                Ordering::Equal => Timeout::Never,
261                Ordering::Greater => Timeout::Milliseconds(expire_timeout),
262                Ordering::Less => Timeout::Default,
263            },
264        };
265        let _ = self
266            .sender
267            .clone()
268            .send(NotifyEvent::Noti(notification.clone()));
269        self.notifications.push(notification);
270        if replaces_id == 0 {
271            let id = self.next_id;
272            self.next_id = self.next_id.wrapping_add(1);
273            if self.next_id == 0 {
274                self.next_id = 1;
275            }
276            Ok(id)
277        } else {
278            Ok(replaces_id)
279        }
280    }
281
282    async fn close_notification(
283        &self,
284        #[zbus(signal_emitter)] emitter: SignalEmitter<'_>,
285        id: u32,
286    ) -> Result<(), BusError> {
287        emitter.notification_closed(id, 4).await?;
288        if let Err(err) = self
289            .sender
290            .clone()
291            .send(NotifyEvent::NotificationClosed(id))
292        {
293            warn!("Error calling CloseNotification: {err}")
294        }
295        Ok(())
296    }
297
298    async fn get_server_information(&self) -> Result<(String, String, String, String), BusError> {
299        Ok((
300            "SpellNC-".to_string() + self.layer_name.as_str(),
301            "VimYoung".to_string(),
302            "0.0.1".to_string(),
303            "1.3".to_string(),
304        ))
305    }
306
307    #[zbus(signal)]
308    async fn notification_closed(
309        emitter: &SignalEmitter<'_>,
310        id: u32,
311        reason: u32,
312    ) -> zbus::Result<()>;
313
314    #[zbus(signal)]
315    async fn action_invoked(
316        emitter: &SignalEmitter<'_>,
317        id: u32,
318        action_key: &str,
319    ) -> zbus::Result<()>;
320}