log_watcher/
cli.rs

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    /// Path(s) to log file(s) to watch
13    #[arg(short = 'f', long = "file", required = true, num_args = 1..)]
14    pub files: Vec<PathBuf>,
15
16    /// Comma-separated patterns to match
17    #[arg(short = 'p', long = "pattern", default_value = "ERROR,WARN")]
18    pub patterns: String,
19
20    /// Treat patterns as regular expressions
21    #[arg(short = 'r', long = "regex")]
22    pub regex: bool,
23
24    /// Case-insensitive pattern matching
25    #[arg(short = 'i', long = "case-insensitive")]
26    pub case_insensitive: bool,
27
28    /// Custom pattern:color mappings (e.g., "ERROR:red,WARN:yellow")
29    #[arg(short = 'c', long = "color-map")]
30    pub color_map: Option<String>,
31
32    /// Enable desktop notifications
33    #[arg(short = 'n', long = "notify", default_value = "true")]
34    pub notify: bool,
35
36    /// Specific patterns that trigger notifications (default: all patterns)
37    #[arg(long = "notify-patterns")]
38    pub notify_patterns: Option<String>,
39
40    /// Maximum notifications per second
41    #[arg(long = "notify-throttle", default_value = "5")]
42    pub notify_throttle: u32,
43
44    /// Preview mode (no tailing, no notifications)
45    #[arg(short = 'd', long = "dry-run")]
46    pub dry_run: bool,
47
48    /// Suppress non-matching lines
49    #[arg(short = 'q', long = "quiet")]
50    pub quiet: bool,
51
52    /// Disable ANSI colors
53    #[arg(long = "no-color")]
54    pub no_color: bool,
55
56    /// Prefix lines with filename (auto: true for multiple files)
57    #[arg(long = "prefix-file")]
58    pub prefix_file: Option<bool>,
59
60    /// File polling interval in milliseconds
61    #[arg(long = "poll-interval", default_value = "100")]
62    pub poll_interval: u64,
63
64    /// Read buffer size in bytes
65    #[arg(long = "buffer-size", default_value = "8192")]
66    pub buffer_size: usize,
67}
68
69impl Args {
70    /// Get the list of files to watch
71    pub fn files(&self) -> &[PathBuf] {
72        &self.files
73    }
74
75    /// Get the patterns as a vector of strings
76    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    /// Get notification patterns as a vector of strings
85    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    /// Get color mappings as a vector of (pattern, color) tuples
98    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    /// Determine if filename prefixing should be enabled
117    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); // Should return empty map for invalid format
151    }
152}