1use clap::{CommandFactory, Parser};
2use clap_complete::{generate, Shell};
3use std::io;
4use std::path::PathBuf;
5
6#[derive(Parser)]
7#[command(
8 name = "logwatcher",
9 about = "Real-time log file monitoring with pattern highlighting and desktop notifications",
10 version = env!("CARGO_PKG_VERSION"),
11 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."
12)]
13pub struct Args {
14 #[arg(short = 'f', long = "file", required_unless_present = "completions", num_args = 1..)]
16 pub files: Vec<PathBuf>,
17
18 #[arg(long = "completions", value_name = "SHELL")]
20 pub completions: Option<Shell>,
21
22 #[arg(short = 'p', long = "pattern", default_value = "ERROR,WARN")]
24 pub patterns: String,
25
26 #[arg(short = 'r', long = "regex")]
28 pub regex: bool,
29
30 #[arg(short = 'i', long = "case-insensitive")]
32 pub case_insensitive: bool,
33
34 #[arg(short = 'c', long = "color-map")]
36 pub color_map: Option<String>,
37
38 #[arg(short = 'n', long = "notify", default_value = "true")]
40 pub notify: bool,
41
42 #[arg(long = "notify-patterns")]
44 pub notify_patterns: Option<String>,
45
46 #[arg(long = "notify-throttle", default_value = "5")]
48 pub notify_throttle: u32,
49
50 #[arg(short = 'd', long = "dry-run")]
52 pub dry_run: bool,
53
54 #[arg(short = 'q', long = "quiet")]
56 pub quiet: bool,
57
58 #[arg(short = 'e', long = "exclude")]
60 pub exclude: Option<String>,
61
62 #[arg(long = "no-color")]
64 pub no_color: bool,
65
66 #[arg(long = "prefix-file")]
68 pub prefix_file: Option<bool>,
69
70 #[arg(long = "poll-interval", default_value = "100")]
72 pub poll_interval: u64,
73
74 #[arg(long = "buffer-size", default_value = "8192")]
76 pub buffer_size: usize,
77}
78
79impl Args {
80 pub fn files(&self) -> &[PathBuf] {
82 &self.files
83 }
84
85 pub fn patterns(&self) -> Vec<String> {
87 self.patterns
88 .split(',')
89 .map(|s| s.trim().to_string())
90 .filter(|s| !s.is_empty())
91 .collect()
92 }
93
94 pub fn notify_patterns(&self) -> Vec<String> {
96 if let Some(ref patterns) = self.notify_patterns {
97 patterns
98 .split(',')
99 .map(|s| s.trim().to_string())
100 .filter(|s| !s.is_empty())
101 .collect()
102 } else {
103 self.patterns()
104 }
105 }
106
107 pub fn color_mappings(&self) -> Vec<(String, String)> {
109 if let Some(ref color_map) = self.color_map {
110 color_map
111 .split(',')
112 .filter_map(|mapping| {
113 let parts: Vec<&str> = mapping.split(':').collect();
114 if parts.len() == 2 {
115 Some((parts[0].trim().to_string(), parts[1].trim().to_string()))
116 } else {
117 None
118 }
119 })
120 .collect()
121 } else {
122 vec![]
123 }
124 }
125
126 pub fn should_prefix_files(&self) -> bool {
128 if let Some(prefix) = self.prefix_file {
129 prefix
130 } else {
131 self.files.len() > 1
132 }
133 }
134
135 pub fn exclude_patterns(&self) -> Vec<String> {
137 if let Some(ref patterns) = self.exclude {
138 patterns
139 .split(',')
140 .map(|s| s.trim().to_string())
141 .filter(|s| !s.is_empty())
142 .collect()
143 } else {
144 vec![]
145 }
146 }
147
148 pub fn generate_completions(shell: Shell) {
150 let mut cmd = Args::command();
151 generate(shell, &mut cmd, "logwatcher", &mut io::stdout());
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn test_color_mappings_invalid_format() {
161 let args = Args {
162 files: vec![PathBuf::from("test.log")],
163 completions: None,
164 patterns: "ERROR".to_string(),
165 regex: false,
166 case_insensitive: false,
167 color_map: Some("invalid_format".to_string()),
168 notify: false,
169 notify_patterns: None,
170 quiet: false,
171 dry_run: false,
172 exclude: None,
173 prefix_file: Some(false),
174 poll_interval: 1000,
175 buffer_size: 8192,
176 no_color: false,
177 notify_throttle: 0,
178 };
179
180 let mappings = args.color_mappings();
181 assert_eq!(mappings.len(), 0); }
183
184 #[test]
185 fn test_exclude_patterns() {
186 let args = Args {
187 files: vec![PathBuf::from("test.log")],
188 completions: None,
189 patterns: "ERROR".to_string(),
190 regex: false,
191 case_insensitive: false,
192 color_map: None,
193 notify: false,
194 notify_patterns: None,
195 quiet: false,
196 dry_run: false,
197 exclude: Some("DEBUG,TRACE".to_string()),
198 prefix_file: Some(false),
199 poll_interval: 1000,
200 buffer_size: 8192,
201 no_color: false,
202 notify_throttle: 0,
203 };
204
205 let patterns = args.exclude_patterns();
206 assert_eq!(patterns.len(), 2);
207 assert!(patterns.contains(&"DEBUG".to_string()));
208 assert!(patterns.contains(&"TRACE".to_string()));
209 }
210
211 #[test]
212 fn test_exclude_patterns_empty() {
213 let args = Args {
214 files: vec![PathBuf::from("test.log")],
215 completions: None,
216 patterns: "ERROR".to_string(),
217 regex: false,
218 case_insensitive: false,
219 color_map: None,
220 notify: false,
221 notify_patterns: None,
222 quiet: false,
223 dry_run: false,
224 exclude: None,
225 prefix_file: Some(false),
226 poll_interval: 1000,
227 buffer_size: 8192,
228 no_color: false,
229 notify_throttle: 0,
230 };
231
232 let patterns = args.exclude_patterns();
233 assert!(patterns.is_empty());
234 }
235
236 #[test]
237 fn test_generate_completions() {
238 use clap_complete::Shell;
241 Args::generate_completions(Shell::Bash);
242 }
243}