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
13pub 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 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 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}