use clap::{CommandFactory, Parser};
use clap_complete::{generate, Shell};
use std::io;
use std::path::PathBuf;
#[derive(Parser)]
#[command(
name = "logwatcher",
about = "Real-time log file monitoring with pattern highlighting and desktop notifications",
version = env!("CARGO_PKG_VERSION"),
long_about = "LogWatcher is a CLI tool for monitoring log files in real-time. It provides pattern highlighting, desktop notifications, and handles file rotation automatically."
)]
pub struct Args {
#[arg(short = 'f', long = "file", required_unless_present = "completions", num_args = 1..)]
pub files: Vec<PathBuf>,
#[arg(long = "completions", value_name = "SHELL")]
pub completions: Option<Shell>,
#[arg(short = 'p', long = "pattern", default_value = "ERROR,WARN")]
pub patterns: String,
#[arg(short = 'r', long = "regex")]
pub regex: bool,
#[arg(short = 'i', long = "case-insensitive")]
pub case_insensitive: bool,
#[arg(short = 'c', long = "color-map")]
pub color_map: Option<String>,
#[arg(short = 'n', long = "notify", default_value = "true")]
pub notify: bool,
#[arg(long = "notify-patterns")]
pub notify_patterns: Option<String>,
#[arg(long = "notify-throttle", default_value = "5")]
pub notify_throttle: u32,
#[arg(short = 'd', long = "dry-run")]
pub dry_run: bool,
#[arg(short = 'q', long = "quiet")]
pub quiet: bool,
#[arg(short = 'e', long = "exclude")]
pub exclude: Option<String>,
#[arg(long = "no-color")]
pub no_color: bool,
#[arg(long = "prefix-file")]
pub prefix_file: Option<bool>,
#[arg(long = "poll-interval", default_value = "100")]
pub poll_interval: u64,
#[arg(long = "buffer-size", default_value = "8192")]
pub buffer_size: usize,
}
impl Args {
pub fn files(&self) -> &[PathBuf] {
&self.files
}
pub fn patterns(&self) -> Vec<String> {
self.patterns
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
}
pub fn notify_patterns(&self) -> Vec<String> {
if let Some(ref patterns) = self.notify_patterns {
patterns
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
} else {
self.patterns()
}
}
pub fn color_mappings(&self) -> Vec<(String, String)> {
if let Some(ref color_map) = self.color_map {
color_map
.split(',')
.filter_map(|mapping| {
let parts: Vec<&str> = mapping.split(':').collect();
if parts.len() == 2 {
Some((parts[0].trim().to_string(), parts[1].trim().to_string()))
} else {
None
}
})
.collect()
} else {
vec![]
}
}
pub fn should_prefix_files(&self) -> bool {
if let Some(prefix) = self.prefix_file {
prefix
} else {
self.files.len() > 1
}
}
pub fn exclude_patterns(&self) -> Vec<String> {
if let Some(ref patterns) = self.exclude {
patterns
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
} else {
vec![]
}
}
pub fn generate_completions(shell: Shell) {
let mut cmd = Args::command();
generate(shell, &mut cmd, "logwatcher", &mut io::stdout());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_mappings_invalid_format() {
let args = Args {
files: vec![PathBuf::from("test.log")],
completions: None,
patterns: "ERROR".to_string(),
regex: false,
case_insensitive: false,
color_map: Some("invalid_format".to_string()),
notify: false,
notify_patterns: None,
quiet: false,
dry_run: false,
exclude: None,
prefix_file: Some(false),
poll_interval: 1000,
buffer_size: 8192,
no_color: false,
notify_throttle: 0,
};
let mappings = args.color_mappings();
assert_eq!(mappings.len(), 0); }
#[test]
fn test_exclude_patterns() {
let args = Args {
files: vec![PathBuf::from("test.log")],
completions: None,
patterns: "ERROR".to_string(),
regex: false,
case_insensitive: false,
color_map: None,
notify: false,
notify_patterns: None,
quiet: false,
dry_run: false,
exclude: Some("DEBUG,TRACE".to_string()),
prefix_file: Some(false),
poll_interval: 1000,
buffer_size: 8192,
no_color: false,
notify_throttle: 0,
};
let patterns = args.exclude_patterns();
assert_eq!(patterns.len(), 2);
assert!(patterns.contains(&"DEBUG".to_string()));
assert!(patterns.contains(&"TRACE".to_string()));
}
#[test]
fn test_exclude_patterns_empty() {
let args = Args {
files: vec![PathBuf::from("test.log")],
completions: None,
patterns: "ERROR".to_string(),
regex: false,
case_insensitive: false,
color_map: None,
notify: false,
notify_patterns: None,
quiet: false,
dry_run: false,
exclude: None,
prefix_file: Some(false),
poll_interval: 1000,
buffer_size: 8192,
no_color: false,
notify_throttle: 0,
};
let patterns = args.exclude_patterns();
assert!(patterns.is_empty());
}
#[test]
fn test_generate_completions() {
use clap_complete::Shell;
Args::generate_completions(Shell::Bash);
}
}