tauri_plugin_notifications/
desktop.rs1use serde::de::DeserializeOwned;
2use tauri::{
3 plugin::{PermissionState, PluginApi},
4 AppHandle, Runtime,
5};
6
7use crate::NotificationsBuilder;
8
9#[cfg(target_os = "linux")]
20struct ActiveEntry {
21 caller_id: i32,
22 handle: notify_rust::NotificationHandle,
23 title: Option<String>,
24 body: Option<String>,
25}
26
27#[allow(clippy::unnecessary_wraps)]
29pub fn init<R: Runtime, C: DeserializeOwned>(
30 app: &AppHandle<R>,
31 _api: PluginApi<R, C>,
32) -> crate::Result<Notifications<R>> {
33 Ok(Notifications {
34 app: app.clone(),
35 #[cfg(target_os = "linux")]
36 active: std::sync::Mutex::new(std::collections::HashMap::new()),
37 #[cfg(target_os = "linux")]
38 active_counter: std::sync::atomic::AtomicU64::new(0),
39 #[cfg(all(target_os = "linux", feature = "push-notifications"))]
40 unifiedpush: tokio::sync::OnceCell::new(),
41 })
42}
43
44pub struct Notifications<R: Runtime> {
48 app: AppHandle<R>,
49 #[cfg(target_os = "linux")]
57 active: std::sync::Mutex<std::collections::HashMap<u64, ActiveEntry>>,
58 #[cfg(target_os = "linux")]
59 active_counter: std::sync::atomic::AtomicU64,
60 #[cfg(all(target_os = "linux", feature = "push-notifications"))]
61 unifiedpush: tokio::sync::OnceCell<std::sync::Arc<crate::unifiedpush::UnifiedPushState>>,
62}
63
64#[cfg(target_os = "linux")]
65fn active_lock_err(e: impl std::fmt::Display) -> crate::Error {
66 crate::Error::Io(std::io::Error::other(format!(
67 "active notifications mutex poisoned: {e}"
68 )))
69}
70
71#[cfg(target_os = "linux")]
72impl<R: Runtime> Notifications<R> {
73 fn close_by_caller_ids(&self, caller_ids: &[i32]) -> crate::Result<()> {
77 let mut to_close: Vec<ActiveEntry> = Vec::new();
78 {
79 let mut active = self.active.lock().map_err(active_lock_err)?;
80 let kept: std::collections::HashMap<u64, ActiveEntry> = std::mem::take(&mut *active)
86 .into_iter()
87 .filter_map(|(k, entry)| {
88 if caller_ids.contains(&entry.caller_id) {
89 to_close.push(entry);
90 None
91 } else {
92 Some((k, entry))
93 }
94 })
95 .collect();
96 *active = kept;
97 }
98 for entry in to_close {
99 tauri::async_runtime::spawn_blocking(move || entry.handle.close());
100 }
101 Ok(())
102 }
103}
104
105#[cfg(all(target_os = "linux", feature = "push-notifications"))]
106impl<R: Runtime> Notifications<R> {
107 async fn unifiedpush_state(
108 &self,
109 ) -> crate::Result<&std::sync::Arc<crate::unifiedpush::UnifiedPushState>> {
110 self.unifiedpush
111 .get_or_try_init(|| {
112 let displayer = Self::build_push_displayer(self.app.clone());
113 crate::unifiedpush::UnifiedPushState::new(&self.app, Some(displayer))
114 })
115 .await
116 }
117
118 fn build_push_displayer(app: AppHandle<R>) -> crate::unifiedpush::PushDisplayer {
128 std::sync::Arc::new(move |title: Option<String>, body: Option<String>| {
129 let app = app.clone();
130 let identifier = app.config().identifier.clone();
131 tauri::async_runtime::spawn_blocking(move || {
132 let notification = match imp::build_notification(
133 title.as_deref(),
134 body.as_deref(),
135 None,
136 &identifier,
137 ) {
138 Ok(n) => n,
139 Err(e) => {
140 log::warn!("Failed to build push notification: {e}");
141 return;
142 }
143 };
144 match notification.show() {
145 Ok(handle) => {
146 use std::sync::atomic::Ordering;
147 use tauri::Manager;
148 let state = app.state::<Self>();
149 let entry_id = state.active_counter.fetch_add(1, Ordering::Relaxed);
150 let entry = ActiveEntry {
151 caller_id: 0,
152 handle,
153 title,
154 body,
155 };
156 let lock = state.active.lock();
157 match lock {
158 Ok(mut active) => {
159 active.insert(entry_id, entry);
160 }
161 Err(poisoned) => {
162 log::warn!("active notifications mutex was poisoned; recovering");
163 poisoned.into_inner().insert(entry_id, entry);
164 }
165 }
166 }
167 Err(e) => log::warn!("Failed to show push notification toast: {e}"),
168 }
169 });
170 })
171 }
172}
173
174impl<R: Runtime> crate::NotificationsBuilder<R> {
176 pub async fn show(self) -> crate::Result<()> {
177 let caller_id = self.data.id;
178 let title = self
179 .data
180 .title
181 .or_else(|| self.app.config().product_name.clone());
182 let body = self.data.body;
183 let icon = self.data.icon;
184 let identifier = self.app.config().identifier.clone();
185 let app = self.app.clone();
186
187 let notification = imp::build_notification(
188 title.as_deref(),
189 body.as_deref(),
190 icon.as_deref(),
191 &identifier,
192 )?;
193
194 let join_result = tauri::async_runtime::spawn_blocking(move || notification.show())
201 .await
202 .map_err(|e| {
203 crate::Error::Io(std::io::Error::other(format!(
204 "notification spawn_blocking join error: {e}"
205 )))
206 })?;
207
208 match join_result {
209 #[cfg(target_os = "linux")]
210 Ok(handle) => {
211 use std::sync::atomic::Ordering;
212 use tauri::Manager;
213 let state = app.state::<Notifications<R>>();
214 let entry_id = state.active_counter.fetch_add(1, Ordering::Relaxed);
215 let entry = ActiveEntry {
216 caller_id,
217 handle,
218 title,
219 body,
220 };
221 let lock_result = state.active.lock();
224 match lock_result {
225 Ok(mut active) => {
226 active.insert(entry_id, entry);
227 }
228 Err(poisoned) => {
229 log::warn!("active notifications mutex was poisoned; recovering");
230 poisoned.into_inner().insert(entry_id, entry);
231 }
232 }
233 }
234 #[cfg(target_os = "macos")]
238 Ok(_) => {
239 let _ = (caller_id, title, body, app);
240 }
241 #[cfg(target_os = "windows")]
245 Ok(()) => {
246 let _ = (caller_id, title, body, app);
247 }
248 Err(e) => {
253 return Err(crate::Error::Io(std::io::Error::other(format!(
254 "Failed to show notification: {e}"
255 ))));
256 }
257 }
258
259 Ok(())
260 }
261}
262
263#[allow(clippy::unused_async)]
265impl<R: Runtime> Notifications<R> {
266 pub fn builder(&self) -> NotificationsBuilder<R> {
267 NotificationsBuilder::new(self.app.clone())
268 }
269
270 pub async fn request_permission(&self) -> crate::Result<PermissionState> {
271 Ok(PermissionState::Granted)
272 }
273
274 pub async fn register_for_push_notifications(&self) -> crate::Result<String> {
279 #[cfg(all(target_os = "linux", feature = "push-notifications"))]
280 {
281 let state = self.unifiedpush_state().await?;
282 state.register().await
283 }
284 #[cfg(not(all(target_os = "linux", feature = "push-notifications")))]
285 {
286 Err(crate::Error::Io(std::io::Error::other(
287 "Push notifications are not supported on desktop platforms",
288 )))
289 }
290 }
291
292 pub fn unregister_for_push_notifications(&self) -> crate::Result<()> {
296 Err(crate::Error::Io(std::io::Error::other(
297 "Push notifications are not supported on desktop platforms",
298 )))
299 }
300
301 pub async fn unregister_for_push_notifications_async(&self) -> crate::Result<()> {
306 #[cfg(all(target_os = "linux", feature = "push-notifications"))]
307 {
308 if let Some(state) = self.unifiedpush.get() {
309 state.unregister().await?;
310 }
311 Ok(())
312 }
313 #[cfg(not(all(target_os = "linux", feature = "push-notifications")))]
314 {
315 Err(crate::Error::Io(std::io::Error::other(
316 "Push notifications are not supported on desktop platforms",
317 )))
318 }
319 }
320
321 #[cfg(all(target_os = "linux", feature = "push-notifications"))]
323 pub async fn list_distributors(&self) -> crate::Result<Vec<String>> {
324 let state = self.unifiedpush_state().await?;
325 state.list_distributors().await
326 }
327
328 #[cfg(all(target_os = "linux", feature = "push-notifications"))]
330 pub async fn set_distributor(&self, name: String) -> crate::Result<()> {
331 let state = self.unifiedpush_state().await?;
332 state.set_distributor(name).await
333 }
334
335 #[cfg(all(target_os = "linux", feature = "push-notifications"))]
339 pub async fn set_token(&self, token: String) -> crate::Result<()> {
340 let state = self.unifiedpush_state().await?;
341 state.set_token(token).await
342 }
343
344 pub async fn permission_state(&self) -> crate::Result<PermissionState> {
345 Ok(PermissionState::Granted)
346 }
347
348 pub async fn pending(&self) -> crate::Result<Vec<crate::PendingNotification>> {
349 Err(crate::Error::Io(std::io::Error::other(
350 "Pending notifications are not supported with notify-rust",
351 )))
352 }
353
354 pub async fn active(&self) -> crate::Result<Vec<crate::ActiveNotification>> {
362 #[cfg(target_os = "linux")]
363 {
364 let active = self.active.lock().map_err(active_lock_err)?;
365 Ok(active
366 .values()
367 .map(|entry| {
368 crate::ActiveNotification::new(
369 entry.caller_id,
370 entry.title.clone(),
371 entry.body.clone(),
372 )
373 })
374 .collect())
375 }
376 #[cfg(not(target_os = "linux"))]
377 {
378 Err(crate::Error::Io(std::io::Error::other(
379 "Active notifications are not supported with notify-rust",
380 )))
381 }
382 }
383
384 pub fn set_click_listener_active(&self, _active: bool) -> crate::Result<()> {
385 Err(crate::Error::Io(std::io::Error::other(
386 "Click listeners are not supported with notify-rust",
387 )))
388 }
389
390 #[allow(clippy::needless_pass_by_value)]
395 pub fn remove_active(&self, ids: Vec<i32>) -> crate::Result<()> {
396 #[cfg(target_os = "linux")]
397 {
398 self.close_by_caller_ids(&ids)
399 }
400 #[cfg(not(target_os = "linux"))]
401 {
402 let _ = ids;
403 Err(crate::Error::Io(std::io::Error::other(
404 "Removing active notifications is not supported with notify-rust",
405 )))
406 }
407 }
408
409 pub fn remove_all_active(&self) -> crate::Result<()> {
410 Err(crate::Error::Io(std::io::Error::other(
411 "Removing active notifications is not supported with notify-rust",
412 )))
413 }
414
415 #[allow(clippy::needless_pass_by_value)]
419 pub fn cancel(&self, notifications: Vec<i32>) -> crate::Result<()> {
420 #[cfg(target_os = "linux")]
421 {
422 self.close_by_caller_ids(¬ifications)
423 }
424 #[cfg(not(target_os = "linux"))]
425 {
426 let _ = notifications;
427 Err(crate::Error::Io(std::io::Error::other(
428 "Canceling notifications is not supported with notify-rust",
429 )))
430 }
431 }
432
433 pub fn cancel_all(&self) -> crate::Result<()> {
436 #[cfg(target_os = "linux")]
437 {
438 let drained: Vec<ActiveEntry> = {
439 let mut active = self.active.lock().map_err(active_lock_err)?;
440 active.drain().map(|(_, v)| v).collect()
441 };
442 for entry in drained {
443 tauri::async_runtime::spawn_blocking(move || entry.handle.close());
446 }
447 Ok(())
448 }
449 #[cfg(not(target_os = "linux"))]
450 {
451 Err(crate::Error::Io(std::io::Error::other(
452 "Canceling notifications is not supported with notify-rust",
453 )))
454 }
455 }
456
457 pub fn register_action_types(&self, _types: Vec<crate::ActionType>) -> crate::Result<()> {
458 Err(crate::Error::Io(std::io::Error::other(
459 "Action types are not supported with notify-rust",
460 )))
461 }
462
463 pub fn create_channel(&self, _channel: crate::Channel) -> crate::Result<()> {
464 Err(crate::Error::Io(std::io::Error::other(
465 "Notification channels are not supported with notify-rust",
466 )))
467 }
468
469 pub fn delete_channel(&self, _id: impl Into<String>) -> crate::Result<()> {
470 Err(crate::Error::Io(std::io::Error::other(
471 "Notification channels are not supported with notify-rust",
472 )))
473 }
474
475 pub fn list_channels(&self) -> crate::Result<Vec<crate::Channel>> {
476 Err(crate::Error::Io(std::io::Error::other(
477 "Notification channels are not supported with notify-rust",
478 )))
479 }
480}
481
482mod imp {
483 #[cfg(windows)]
487 use std::path::MAIN_SEPARATOR as SEP;
488
489 #[allow(clippy::unnecessary_wraps)]
494 pub fn build_notification(
495 title: Option<&str>,
496 body: Option<&str>,
497 icon: Option<&str>,
498 identifier: &str,
499 ) -> crate::Result<notify_rust::Notification> {
500 let mut notification = notify_rust::Notification::new();
501 if let Some(body) = body {
502 notification.body(body);
503 }
504 if let Some(title) = title {
505 notification.summary(title);
506 }
507 if let Some(icon) = icon {
508 notification.icon(icon);
509 } else {
510 notification.auto_icon();
511 }
512
513 #[cfg(windows)]
514 {
515 let exe = tauri::utils::platform::current_exe()?;
516 let exe_dir = exe.parent().expect("failed to get exe directory");
517 let curr_dir = exe_dir.display().to_string();
518 if !(curr_dir.ends_with(format!("{SEP}target{SEP}debug").as_str())
521 || curr_dir.ends_with(format!("{SEP}target{SEP}release").as_str()))
522 {
523 notification.app_id(identifier);
524 }
525 }
526 #[cfg(target_os = "macos")]
527 {
528 let _ = notify_rust::set_application(if tauri::is_dev() {
529 "com.apple.Terminal"
530 } else {
531 identifier
532 });
533 }
534 #[cfg(target_os = "linux")]
537 let _ = identifier;
538
539 Ok(notification)
540 }
541}