use crate::config::Config;
use anyhow::Result;
#[cfg(not(target_os = "windows"))]
use notify_rust::Notification;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::Mutex;
#[derive(Debug)]
pub struct Notifier {
config: Config,
last_notification: Arc<Mutex<Instant>>,
notification_count: Arc<Mutex<u32>>,
throttle_window: Duration,
}
impl Notifier {
pub fn new(config: Config) -> Self {
Self {
config,
last_notification: Arc::new(Mutex::new(Instant::now())),
notification_count: Arc::new(Mutex::new(0)),
throttle_window: Duration::from_secs(1),
}
}
pub async fn send_notification(
&self,
pattern: &str,
line: &str,
filename: Option<&str>,
) -> Result<()> {
if !self.config.notify_enabled {
return Ok(());
}
if !self.config.should_notify_for_pattern(pattern) {
return Ok(());
}
if !self.should_send_notification().await {
return Ok(());
}
let truncated_line = if line.len() > 200 {
format!("{}...", &line[..197])
} else {
line.to_string()
};
let title = if let Some(filename) = filename {
format!("{} detected in {}", pattern, filename)
} else {
format!("{} detected", pattern)
};
self.send_desktop_notification(&title, &truncated_line)
.await?;
self.update_throttle_state().await;
Ok(())
}
async fn should_send_notification(&self) -> bool {
let mut count = self.notification_count.lock().await;
let mut last_time = self.last_notification.lock().await;
let now = Instant::now();
if now.duration_since(*last_time) >= self.throttle_window {
*count = 0;
*last_time = now;
}
if *count < self.config.notify_throttle {
*count += 1;
true
} else {
false
}
}
async fn update_throttle_state(&self) {
let _count = self.notification_count.lock().await;
}
async fn send_desktop_notification(&self, title: &str, body: &str) -> Result<()> {
#[cfg(not(target_os = "windows"))]
{
self.send_unix_notification(title, body).await
}
#[cfg(target_os = "windows")]
{
self.send_windows_notification(title, body).await
}
}
#[cfg(not(target_os = "windows"))]
async fn send_unix_notification(&self, title: &str, body: &str) -> Result<()> {
Notification::new()
.summary(title)
.body(body)
.icon("logwatcher")
.timeout(5000) .show()
.map_err(|e| anyhow::anyhow!("Failed to send notification: {}", e))?;
Ok(())
}
#[cfg(target_os = "windows")]
async fn send_windows_notification(&self, title: &str, body: &str) -> Result<()> {
use winrt_notification::Toast;
Toast::new(Toast::POWERSHELL_APP_ID)
.title(title)
.text1(body)
.duration(winrt_notification::Duration::Short)
.show()
.map_err(|e| anyhow::anyhow!("Failed to send Windows notification: {}", e))?;
Ok(())
}
pub async fn test_notification(&self) -> Result<()> {
self.send_notification("TEST", "LogWatcher notification test", Some("test.log"))
.await
}
pub fn get_notification_count(&self) -> Arc<Mutex<u32>> {
self.notification_count.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::Args;
use std::path::PathBuf;
fn create_test_config(notify_enabled: bool, throttle: u32) -> Config {
let args = Args {
files: vec![PathBuf::from("test.log")],
completions: None,
patterns: "ERROR".to_string(),
regex: false,
case_insensitive: false,
color_map: None,
notify: notify_enabled,
notify_patterns: None,
notify_throttle: throttle,
dry_run: false,
quiet: false,
exclude: None,
no_color: false,
prefix_file: None,
poll_interval: 100,
buffer_size: 8192,
};
Config::from_args(&args).unwrap()
}
#[tokio::test]
async fn test_notification_disabled() {
let config = create_test_config(false, 5);
let notifier = Notifier::new(config);
let result = notifier
.send_notification("ERROR", "Test message", None)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_notification_throttling() {
let config = create_test_config(true, 2);
let notifier = Notifier::new(config);
let result1 = notifier
.send_notification("ERROR", "Test message 1", None)
.await;
let _ = result1;
let result2 = notifier
.send_notification("ERROR", "Test message 2", None)
.await;
let _ = result2;
let result3 = notifier
.send_notification("ERROR", "Test message 3", None)
.await;
let _ = result3;
}
#[tokio::test]
async fn test_line_truncation() {
let config = create_test_config(true, 5);
let notifier = Notifier::new(config);
let long_line = "a".repeat(250);
let result = notifier.send_notification("ERROR", &long_line, None).await;
let _ = result;
}
#[test]
fn test_get_notification_count() {
let config = create_test_config(true, 0);
let notifier = Notifier::new(config);
let count = notifier.get_notification_count();
let count_value = count.blocking_lock();
assert_eq!(*count_value, 0);
}
#[tokio::test]
async fn test_notification_with_file_info() {
let config = create_test_config(true, 0);
let notifier = Notifier::new(config);
let result = notifier
.send_notification("ERROR", "Test error", Some("test.log"))
.await;
let _ = result;
}
#[test]
fn test_should_notify_for_pattern_coverage_line_39() {
let config = create_test_config(true, 10);
let notifier = Notifier::new(config);
let result = notifier.send_notification("INFO", "Normal operation", Some("test.log"));
drop(result);
}
#[tokio::test]
async fn test_should_notify_for_pattern_early_return_coverage_line_39() {
let args = Args {
files: vec![PathBuf::from("test.log")],
completions: None,
patterns: "ERROR".to_string(),
regex: false,
case_insensitive: false,
color_map: None,
notify: true,
notify_patterns: Some("ERROR,WARN".to_string()),
quiet: false,
dry_run: false,
exclude: None,
poll_interval: 1000,
buffer_size: 1024,
notify_throttle: 5,
no_color: false,
prefix_file: None,
};
let config = Config::from_args(&args).unwrap();
let notifier = Notifier::new(config);
let result = notifier
.send_notification("INFO", "Normal operation", Some("test.log"))
.await;
drop(result);
}
#[test]
fn test_throttle_window_reset_coverage_lines_79_80() {
let config = create_test_config(true, 1);
let notifier = Notifier::new(config);
let count = notifier.get_notification_count();
let initial_count = *count.blocking_lock();
assert_eq!(initial_count, 0);
}
#[tokio::test]
async fn test_throttle_window_reset_logic_coverage_lines_79_80() {
let args = Args {
files: vec![PathBuf::from("test.log")],
completions: None,
patterns: "ERROR".to_string(),
regex: false,
case_insensitive: false,
color_map: None,
notify: true,
notify_patterns: None,
quiet: false,
dry_run: false,
exclude: None,
poll_interval: 1000,
buffer_size: 1024,
notify_throttle: 5,
no_color: false,
prefix_file: None,
};
let config = Config::from_args(&args).unwrap();
let notifier = Notifier::new(config);
let _ = notifier
.send_notification("ERROR", "Test error 1", Some("test.log"))
.await;
let _ = notifier
.send_notification("ERROR", "Test error 2", Some("test.log"))
.await;
let _ = notifier;
}
#[tokio::test]
async fn test_windows_notification_coverage_lines_136_138() {
let config = create_test_config(true, 10);
let notifier = Notifier::new(config);
let result = notifier
.send_notification("ERROR", "Test error", Some("test.log"))
.await;
let _ = result;
}
#[tokio::test]
async fn test_test_notification_method_coverage_lines_136_138() {
let config = create_test_config(true, 10);
let notifier = Notifier::new(config);
let result = notifier.test_notification().await;
let _ = result;
}
}