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 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 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 let result1 = notifier
191 .send_notification("ERROR", "Test message 1", None)
192 .await;
193 let _ = result1;
195
196 let result2 = notifier
198 .send_notification("ERROR", "Test message 2", None)
199 .await;
200 let _ = result2;
201
202 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 let _ = result;
219 }
220
221 #[test]
222 fn test_get_notification_count() {
223 let config = create_test_config(true, 0);
224 let notifier = Notifier::new(config);
225
226 let count = notifier.get_notification_count();
227 let count_value = count.blocking_lock();
228 assert_eq!(*count_value, 0);
229 }
230
231 #[tokio::test]
232 async fn test_notification_with_file_info() {
233 let config = create_test_config(true, 0);
234 let notifier = Notifier::new(config);
235
236 let result = notifier
237 .send_notification("ERROR", "Test error", Some("test.log"))
238 .await;
239 let _ = result;
241 }
242}