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 if !self.config.should_notify_for_pattern(pattern) {
39 return Ok(());
40 }
41
42 if !self.should_send_notification().await {
44 return Ok(());
45 }
46
47 let truncated_line = if line.len() > 200 {
49 format!("{}...", &line[..197])
50 } else {
51 line.to_string()
52 };
53
54 let title = if let Some(filename) = filename {
56 format!("{} detected in {}", pattern, filename)
57 } else {
58 format!("{} detected", pattern)
59 };
60
61 self.send_desktop_notification(&title, &truncated_line)
63 .await?;
64
65 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 if now.duration_since(*last_time) >= self.throttle_window {
79 *count = 0;
80 *last_time = now;
81 }
82
83 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 }
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) .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 completions: None,
156 patterns: "ERROR".to_string(),
157 regex: false,
158 case_insensitive: false,
159 color_map: None,
160 notify: notify_enabled,
161 notify_patterns: None,
162 notify_throttle: throttle,
163 dry_run: false,
164 quiet: false,
165 exclude: None,
166 no_color: false,
167 prefix_file: None,
168 poll_interval: 100,
169 buffer_size: 8192,
170 };
171 Config::from_args(&args).unwrap()
172 }
173
174 #[tokio::test]
175 async fn test_notification_disabled() {
176 let config = create_test_config(false, 5);
177 let notifier = Notifier::new(config);
178
179 let result = notifier
180 .send_notification("ERROR", "Test message", None)
181 .await;
182 assert!(result.is_ok());
184 }
185
186 #[tokio::test]
187 async fn test_notification_throttling() {
188 let config = create_test_config(true, 2);
189 let notifier = Notifier::new(config);
190
191 let result1 = notifier
193 .send_notification("ERROR", "Test message 1", None)
194 .await;
195 let _ = result1;
197
198 let result2 = notifier
200 .send_notification("ERROR", "Test message 2", None)
201 .await;
202 let _ = result2;
203
204 let result3 = notifier
206 .send_notification("ERROR", "Test message 3", None)
207 .await;
208 let _ = result3;
209 }
210
211 #[tokio::test]
212 async fn test_line_truncation() {
213 let config = create_test_config(true, 5);
214 let notifier = Notifier::new(config);
215
216 let long_line = "a".repeat(250);
217 let result = notifier.send_notification("ERROR", &long_line, None).await;
218 let _ = result;
221 }
222
223 #[test]
224 fn test_get_notification_count() {
225 let config = create_test_config(true, 0);
226 let notifier = Notifier::new(config);
227
228 let count = notifier.get_notification_count();
229 let count_value = count.blocking_lock();
230 assert_eq!(*count_value, 0);
231 }
232
233 #[tokio::test]
234 async fn test_notification_with_file_info() {
235 let config = create_test_config(true, 0);
236 let notifier = Notifier::new(config);
237
238 let result = notifier
239 .send_notification("ERROR", "Test error", Some("test.log"))
240 .await;
241 let _ = result;
243 }
244
245 #[test]
246 fn test_should_notify_for_pattern_coverage_line_39() {
247 let config = create_test_config(true, 10);
248 let notifier = Notifier::new(config);
249
250 let result = notifier.send_notification("INFO", "Normal operation", Some("test.log"));
253 drop(result);
256 }
257
258 #[tokio::test]
259 async fn test_should_notify_for_pattern_early_return_coverage_line_39() {
260 let args = Args {
262 files: vec![PathBuf::from("test.log")],
263 completions: None,
264 patterns: "ERROR".to_string(),
265 regex: false,
266 case_insensitive: false,
267 color_map: None,
268 notify: true,
269 notify_patterns: Some("ERROR,WARN".to_string()),
270 quiet: false,
271 dry_run: false,
272 exclude: None,
273 poll_interval: 1000,
274 buffer_size: 1024,
275 notify_throttle: 5,
276 no_color: false,
277 prefix_file: None,
278 };
279 let config = Config::from_args(&args).unwrap();
280 let notifier = Notifier::new(config);
281
282 let result = notifier
284 .send_notification("INFO", "Normal operation", Some("test.log"))
285 .await;
286 drop(result);
288 }
289
290 #[test]
291 fn test_throttle_window_reset_coverage_lines_79_80() {
292 let config = create_test_config(true, 1);
293 let notifier = Notifier::new(config);
294
295 let count = notifier.get_notification_count();
298 let initial_count = *count.blocking_lock();
299
300 assert_eq!(initial_count, 0);
302 }
303
304 #[tokio::test]
305 async fn test_throttle_window_reset_logic_coverage_lines_79_80() {
306 let args = Args {
308 files: vec![PathBuf::from("test.log")],
309 completions: None,
310 patterns: "ERROR".to_string(),
311 regex: false,
312 case_insensitive: false,
313 color_map: None,
314 notify: true,
315 notify_patterns: None,
316 quiet: false,
317 dry_run: false,
318 exclude: None,
319 poll_interval: 1000,
320 buffer_size: 1024,
321 notify_throttle: 5,
322 no_color: false,
323 prefix_file: None,
324 };
325 let config = Config::from_args(&args).unwrap();
326 let notifier = Notifier::new(config);
327
328 let _ = notifier
330 .send_notification("ERROR", "Test error 1", Some("test.log"))
331 .await;
332 let _ = notifier
333 .send_notification("ERROR", "Test error 2", Some("test.log"))
334 .await;
335
336 let _ = notifier;
340 }
341
342 #[tokio::test]
343 async fn test_windows_notification_coverage_lines_136_138() {
344 let config = create_test_config(true, 10);
345 let notifier = Notifier::new(config);
346
347 let result = notifier
350 .send_notification("ERROR", "Test error", Some("test.log"))
351 .await;
352 let _ = result;
354 }
355
356 #[tokio::test]
357 async fn test_test_notification_method_coverage_lines_136_138() {
358 let config = create_test_config(true, 10);
359 let notifier = Notifier::new(config);
360
361 let result = notifier.test_notification().await;
363 let _ = result;
366 }
367}