tsk/
notifications.rs

1use notify_rust::{Notification, Timeout};
2use std::sync::Arc;
3
4/// Trait for sending desktop notifications
5pub trait NotificationClient: Send + Sync {
6    /// Notify when a task completes
7    fn notify_task_complete(&self, task_name: &str, success: bool, message: Option<&str>);
8
9    /// Notify when all tasks are complete
10    fn notify_all_tasks_complete(&self, total: usize, succeeded: usize, failed: usize);
11}
12
13/// Desktop notification client using notify-rust
14pub struct DesktopNotificationClient {
15    timeout_seconds: u32,
16}
17
18impl DesktopNotificationClient {
19    /// Create a new desktop notification client
20    pub fn new(timeout_seconds: u32) -> Self {
21        Self { timeout_seconds }
22    }
23}
24
25impl NotificationClient for DesktopNotificationClient {
26    fn notify_task_complete(&self, task_name: &str, success: bool, message: Option<&str>) {
27        let summary = if success {
28            "Task Completed"
29        } else {
30            "Task Failed"
31        };
32        let body = format!(
33            "Task '{}' has {}\n{}",
34            task_name,
35            if success {
36                "completed successfully"
37            } else {
38                "failed"
39            },
40            message.unwrap_or("")
41        );
42
43        let mut notification = Notification::new();
44        notification.summary(summary);
45        notification.body(&body);
46        notification.timeout(Timeout::Milliseconds(self.timeout_seconds * 1000));
47
48        if let Err(e) = notification.show() {
49            // Fall back to terminal output
50            eprintln!("TSK: {} - {}", summary, body.replace('\n', " "));
51            eprintln!("(Desktop notification failed: {e})");
52        }
53    }
54
55    fn notify_all_tasks_complete(&self, total: usize, succeeded: usize, failed: usize) {
56        let summary = if failed == 0 {
57            "All Tasks Completed"
58        } else {
59            "Tasks Completed with Failures"
60        };
61
62        let body = format!("Total: {total}\nSucceeded: {succeeded}\nFailed: {failed}");
63
64        let mut notification = Notification::new();
65        notification.summary(summary);
66        notification.body(&body);
67        notification.timeout(Timeout::Milliseconds(self.timeout_seconds * 1000));
68
69        if let Err(e) = notification.show() {
70            // Fall back to terminal output
71            eprintln!("TSK: {} - {}", summary, body.replace('\n', " "));
72            eprintln!("(Desktop notification failed: {e})");
73        }
74    }
75}
76
77/// No-op notification client for testing
78pub struct NoOpNotificationClient;
79
80impl NotificationClient for NoOpNotificationClient {
81    fn notify_task_complete(&self, _task_name: &str, _success: bool, _message: Option<&str>) {
82        // No-op
83    }
84
85    fn notify_all_tasks_complete(&self, _total: usize, _succeeded: usize, _failed: usize) {
86        // No-op
87    }
88}
89
90/// Create a notification client based on the environment
91pub fn create_notification_client() -> Arc<dyn NotificationClient> {
92    // Check if we're in a test environment or CI
93    if std::env::var("TSK_NO_NOTIFICATIONS").is_ok() || cfg!(test) {
94        Arc::new(NoOpNotificationClient)
95    } else {
96        Arc::new(DesktopNotificationClient::new(5))
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn test_noop_notification_client() {
106        let client = NoOpNotificationClient;
107        // Should not panic
108        client.notify_task_complete("test", true, None);
109        client.notify_task_complete("test", false, Some("error"));
110        client.notify_all_tasks_complete(5, 3, 2);
111    }
112
113    #[test]
114    fn test_create_notification_client_in_tests() {
115        let client = create_notification_client();
116        // Should return NoOpNotificationClient in tests
117        client.notify_task_complete("test", true, None);
118    }
119}