log_watcher/
notifier.rs

1use crate::config::Config;
2use anyhow::Result;
3#[cfg(not(target_os = "windows"))]
4use notify_rust::Notification;
5use std::sync::Arc;
6use std::time::{Duration, Instant};
7use tokio::sync::Mutex;
8
9#[derive(Debug)]
10pub struct Notifier {
11    config: Config,
12    last_notification: Arc<Mutex<Instant>>,
13    notification_count: Arc<Mutex<u32>>,
14    throttle_window: Duration,
15}
16
17impl Notifier {
18    pub fn new(config: Config) -> Self {
19        Self {
20            config,
21            last_notification: Arc::new(Mutex::new(Instant::now())),
22            notification_count: Arc::new(Mutex::new(0)),
23            throttle_window: Duration::from_secs(1),
24        }
25    }
26
27    pub async fn send_notification(
28        &self,
29        pattern: &str,
30        line: &str,
31        filename: Option<&str>,
32    ) -> Result<()> {
33        if !self.config.notify_enabled {
34            return Ok(());
35        }
36
37        // Check if this pattern should trigger notifications
38        if !self.config.should_notify_for_pattern(pattern) {
39            return Ok(());
40        }
41
42        // Throttle notifications
43        if !self.should_send_notification().await {
44            return Ok(());
45        }
46
47        // Truncate long lines
48        let truncated_line = if line.len() > 200 {
49            format!("{}...", &line[..197])
50        } else {
51            line.to_string()
52        };
53
54        // Create notification title
55        let title = if let Some(filename) = filename {
56            format!("{} detected in {}", pattern, filename)
57        } else {
58            format!("{} detected", pattern)
59        };
60
61        // Send notification
62        self.send_desktop_notification(&title, &truncated_line)
63            .await?;
64
65        // Update throttling state
66        self.update_throttle_state().await;
67
68        Ok(())
69    }
70
71    async fn should_send_notification(&self) -> bool {
72        let mut count = self.notification_count.lock().await;
73        let mut last_time = self.last_notification.lock().await;
74
75        let now = Instant::now();
76
77        // Reset counter if we're in a new throttle window
78        if now.duration_since(*last_time) >= self.throttle_window {
79            *count = 0;
80            *last_time = now;
81        }
82
83        // Check if we're under the throttle limit
84        if *count < self.config.notify_throttle {
85            *count += 1;
86            true
87        } else {
88            false
89        }
90    }
91
92    async fn update_throttle_state(&self) {
93        let _count = self.notification_count.lock().await;
94        // The count was already updated in should_send_notification
95    }
96
97    async fn send_desktop_notification(&self, title: &str, body: &str) -> Result<()> {
98        #[cfg(not(target_os = "windows"))]
99        {
100            self.send_unix_notification(title, body).await
101        }
102
103        #[cfg(target_os = "windows")]
104        {
105            self.send_windows_notification(title, body).await
106        }
107    }
108
109    #[cfg(not(target_os = "windows"))]
110    async fn send_unix_notification(&self, title: &str, body: &str) -> Result<()> {
111        Notification::new()
112            .summary(title)
113            .body(body)
114            .icon("logwatcher")
115            .timeout(5000) // 5 seconds
116            .show()
117            .map_err(|e| anyhow::anyhow!("Failed to send notification: {}", e))?;
118
119        Ok(())
120    }
121
122    #[cfg(target_os = "windows")]
123    async fn send_windows_notification(&self, title: &str, body: &str) -> Result<()> {
124        use winrt_notification::Toast;
125
126        Toast::new(Toast::POWERSHELL_APP_ID)
127            .title(title)
128            .text1(body)
129            .duration(winrt_notification::Duration::Short)
130            .show()
131            .map_err(|e| anyhow::anyhow!("Failed to send Windows notification: {}", e))?;
132
133        Ok(())
134    }
135
136    pub async fn test_notification(&self) -> Result<()> {
137        self.send_notification("TEST", "LogWatcher notification test", Some("test.log"))
138            .await
139    }
140
141    pub fn get_notification_count(&self) -> Arc<Mutex<u32>> {
142        self.notification_count.clone()
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use crate::cli::Args;
150    use std::path::PathBuf;
151
152    fn create_test_config(notify_enabled: bool, throttle: u32) -> Config {
153        let args = Args {
154            files: vec![PathBuf::from("test.log")],
155            patterns: "ERROR".to_string(),
156            regex: false,
157            case_insensitive: false,
158            color_map: None,
159            notify: notify_enabled,
160            notify_patterns: None,
161            notify_throttle: throttle,
162            dry_run: false,
163            quiet: false,
164            no_color: false,
165            prefix_file: None,
166            poll_interval: 100,
167            buffer_size: 8192,
168        };
169        Config::from_args(&args).unwrap()
170    }
171
172    #[tokio::test]
173    async fn test_notification_disabled() {
174        let config = create_test_config(false, 5);
175        let notifier = Notifier::new(config);
176
177        let result = notifier
178            .send_notification("ERROR", "Test message", None)
179            .await;
180        // When notifications are disabled, this should always succeed
181        assert!(result.is_ok());
182    }
183
184    #[tokio::test]
185    async fn test_notification_throttling() {
186        let config = create_test_config(true, 2);
187        let notifier = Notifier::new(config);
188
189        // Send first notification
190        let result1 = notifier
191            .send_notification("ERROR", "Test message 1", None)
192            .await;
193        // In test environment, notifications might fail, so we just check it doesn't panic
194        let _ = result1;
195
196        // Send second notification
197        let result2 = notifier
198            .send_notification("ERROR", "Test message 2", None)
199            .await;
200        let _ = result2;
201
202        // Third notification should be throttled (but still return Ok)
203        let result3 = notifier
204            .send_notification("ERROR", "Test message 3", None)
205            .await;
206        let _ = result3;
207    }
208
209    #[tokio::test]
210    async fn test_line_truncation() {
211        let config = create_test_config(true, 5);
212        let notifier = Notifier::new(config);
213
214        let long_line = "a".repeat(250);
215        let result = notifier.send_notification("ERROR", &long_line, None).await;
216        // The notification might fail in test environment, so we just check it doesn't panic
217        // In a real environment, this would succeed and truncate the line
218        let _ = result;
219    }
220}