1use clap::Parser;
2use std::path::PathBuf;
3
4#[derive(Parser)]
5#[command(
6 name = "logwatcher",
7 about = "Real-time log file monitoring with pattern highlighting and desktop notifications",
8 version = "0.1.0",
9 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."
10)]
11pub struct Args {
12 #[arg(short = 'f', long = "file", required = true, num_args = 1..)]
14 pub files: Vec<PathBuf>,
15
16 #[arg(short = 'p', long = "pattern", default_value = "ERROR,WARN")]
18 pub patterns: String,
19
20 #[arg(short = 'r', long = "regex")]
22 pub regex: bool,
23
24 #[arg(short = 'i', long = "case-insensitive")]
26 pub case_insensitive: bool,
27
28 #[arg(short = 'c', long = "color-map")]
30 pub color_map: Option<String>,
31
32 #[arg(short = 'n', long = "notify", default_value = "true")]
34 pub notify: bool,
35
36 #[arg(long = "notify-patterns")]
38 pub notify_patterns: Option<String>,
39
40 #[arg(long = "notify-throttle", default_value = "5")]
42 pub notify_throttle: u32,
43
44 #[arg(short = 'd', long = "dry-run")]
46 pub dry_run: bool,
47
48 #[arg(short = 'q', long = "quiet")]
50 pub quiet: bool,
51
52 #[arg(long = "no-color")]
54 pub no_color: bool,
55
56 #[arg(long = "prefix-file")]
58 pub prefix_file: Option<bool>,
59
60 #[arg(long = "poll-interval", default_value = "100")]
62 pub poll_interval: u64,
63
64 #[arg(long = "buffer-size", default_value = "8192")]
66 pub buffer_size: usize,
67}
68
69impl Args {
70 pub fn files(&self) -> &[PathBuf] {
72 &self.files
73 }
74
75 pub fn patterns(&self) -> Vec<String> {
77 self.patterns
78 .split(',')
79 .map(|s| s.trim().to_string())
80 .filter(|s| !s.is_empty())
81 .collect()
82 }
83
84 pub fn notify_patterns(&self) -> Vec<String> {
86 if let Some(ref patterns) = self.notify_patterns {
87 patterns
88 .split(',')
89 .map(|s| s.trim().to_string())
90 .filter(|s| !s.is_empty())
91 .collect()
92 } else {
93 self.patterns()
94 }
95 }
96
97 pub fn color_mappings(&self) -> Vec<(String, String)> {
99 if let Some(ref color_map) = self.color_map {
100 color_map
101 .split(',')
102 .filter_map(|mapping| {
103 let parts: Vec<&str> = mapping.split(':').collect();
104 if parts.len() == 2 {
105 Some((parts[0].trim().to_string(), parts[1].trim().to_string()))
106 } else {
107 None
108 }
109 })
110 .collect()
111 } else {
112 vec![]
113 }
114 }
115
116 pub fn should_prefix_files(&self) -> bool {
118 if let Some(prefix) = self.prefix_file {
119 prefix
120 } else {
121 self.files.len() > 1
122 }
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn test_color_mappings_invalid_format() {
132 let args = Args {
133 files: vec![PathBuf::from("test.log")],
134 patterns: "ERROR".to_string(),
135 regex: false,
136 case_insensitive: false,
137 color_map: Some("invalid_format".to_string()),
138 notify: false,
139 notify_patterns: None,
140 quiet: false,
141 dry_run: false,
142 prefix_file: Some(false),
143 poll_interval: 1000,
144 buffer_size: 8192,
145 no_color: false,
146 notify_throttle: 0,
147 };
148
149 let mappings = args.color_mappings();
150 assert_eq!(mappings.len(), 0); }
152}