1use crate::config::Config;
2use crate::matcher::MatchResult;
3use anyhow::Result;
4use std::io::Write;
5use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
6
7#[derive(Debug)]
8pub struct Highlighter {
9 config: Config,
10 stdout: StandardStream,
11 stderr: StandardStream,
12}
13
14impl Highlighter {
15 pub fn new(config: Config) -> Self {
16 let color_choice = if config.no_color {
17 ColorChoice::Never
18 } else {
19 ColorChoice::Auto
20 };
21
22 Self {
23 config,
24 stdout: StandardStream::stdout(color_choice),
25 stderr: StandardStream::stderr(color_choice),
26 }
27 }
28
29 pub fn print_line(
30 &mut self,
31 line: &str,
32 filename: Option<&str>,
33 match_result: &MatchResult,
34 dry_run: bool,
35 ) -> Result<()> {
36 if self.config.quiet && !match_result.matched {
38 return Ok(());
39 }
40
41 let mut output_line = String::new();
42
43 if dry_run && match_result.matched {
45 output_line.push_str("[DRY-RUN] ");
46 }
47
48 if self.config.prefix_files {
50 if let Some(filename) = filename {
51 output_line.push_str(&format!("[{}] ", filename));
52 }
53 }
54
55 output_line.push_str(line);
57
58 if let Some(color) = match_result.color {
60 self.print_colored(&output_line, color)?;
61 } else {
62 self.print_plain(&output_line)?;
63 }
64
65 Ok(())
66 }
67
68 fn print_colored(&mut self, text: &str, color: Color) -> Result<()> {
69 self.stdout
70 .set_color(ColorSpec::new().set_fg(Some(color)))?;
71 writeln!(self.stdout, "{}", text)?;
72 self.stdout.reset()?;
73 self.stdout.flush()?;
74 Ok(())
75 }
76
77 fn print_plain(&mut self, text: &str) -> Result<()> {
78 writeln!(self.stdout, "{}", text)?;
79 self.stdout.flush()?;
80 Ok(())
81 }
82
83 pub fn print_error(&mut self, message: &str) -> Result<()> {
84 self.stderr
85 .set_color(ColorSpec::new().set_fg(Some(Color::Red)))?;
86 writeln!(self.stderr, "Error: {}", message)?;
87 self.stderr.reset()?;
88 self.stderr.flush()?;
89 Ok(())
90 }
91
92 pub fn print_warning(&mut self, message: &str) -> Result<()> {
93 self.stderr
94 .set_color(ColorSpec::new().set_fg(Some(Color::Yellow)))?;
95 writeln!(self.stderr, "Warning: {}", message)?;
96 self.stderr.reset()?;
97 self.stderr.flush()?;
98 Ok(())
99 }
100
101 pub fn print_info(&mut self, message: &str) -> Result<()> {
102 self.stderr
103 .set_color(ColorSpec::new().set_fg(Some(Color::Cyan)))?;
104 writeln!(self.stderr, "Info: {}", message)?;
105 self.stderr.reset()?;
106 self.stderr.flush()?;
107 Ok(())
108 }
109
110 pub fn print_dry_run_summary(&mut self, matches: &[(String, usize)]) -> Result<()> {
111 if matches.is_empty() {
112 self.print_info("No matching lines found")?;
113 return Ok(());
114 }
115
116 self.print_info("Dry-run summary:")?;
117 for (pattern, count) in matches {
118 self.print_plain(&format!(" {}: {} matches", pattern, count))?;
119 }
120 self.print_info("Dry-run complete. No notifications sent.")?;
121 Ok(())
122 }
123
124 pub fn print_startup_info(&mut self) -> Result<()> {
125 self.print_info(&format!("Watching {} file(s)", self.config.files.len()))?;
126
127 if !self.config.patterns.is_empty() {
128 self.print_info(&format!("Patterns: {}", self.config.patterns.join(", ")))?;
129 }
130
131 if self.config.notify_enabled {
132 self.print_info("Desktop notifications enabled")?;
133 }
134
135 if self.config.dry_run {
136 self.print_info("Dry-run mode: reading existing content only")?;
137 }
138
139 Ok(())
140 }
141
142 pub fn print_file_rotation(&mut self, filename: &str) -> Result<()> {
143 self.print_warning(&format!("File rotation detected for {}", filename))?;
144 Ok(())
145 }
146
147 pub fn print_file_reopened(&mut self, filename: &str) -> Result<()> {
148 self.print_info(&format!("Reopened file: {}", filename))?;
149 Ok(())
150 }
151
152 pub fn print_file_error(&mut self, filename: &str, error: &str) -> Result<()> {
153 self.print_error(&format!("Error watching {}: {}", filename, error))?;
154 Ok(())
155 }
156
157 pub fn print_shutdown_summary(&mut self, stats: &WatcherStats) -> Result<()> {
158 self.print_info("Shutdown summary:")?;
159 self.print_plain(&format!(" Files watched: {}", stats.files_watched))?;
160 self.print_plain(&format!(" Lines processed: {}", stats.lines_processed))?;
161 if stats.lines_excluded > 0 {
162 self.print_plain(&format!(" Lines excluded: {}", stats.lines_excluded))?;
163 }
164 self.print_plain(&format!(" Matches found: {}", stats.matches_found))?;
165 self.print_plain(&format!(
166 " Notifications sent: {}",
167 stats.notifications_sent
168 ))?;
169 Ok(())
170 }
171}
172
173#[derive(Debug, Default)]
174pub struct WatcherStats {
175 pub files_watched: usize,
176 pub lines_processed: usize,
177 pub lines_excluded: usize,
178 pub matches_found: usize,
179 pub notifications_sent: usize,
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use crate::cli::Args;
186 use std::path::PathBuf;
187
188 fn create_test_config() -> Config {
189 let args = Args {
190 files: vec![PathBuf::from("test.log")],
191 completions: None,
192 patterns: "ERROR".to_string(),
193 regex: false,
194 case_insensitive: false,
195 color_map: None,
196 notify: true,
197 notify_patterns: None,
198 notify_throttle: 5,
199 dry_run: false,
200 quiet: false,
201 exclude: None,
202 no_color: true, prefix_file: None,
204 poll_interval: 100,
205 buffer_size: 8192,
206 };
207 Config::from_args(&args).unwrap()
208 }
209
210 #[test]
211 fn test_print_line_without_match() {
212 let config = create_test_config();
213 let mut highlighter = Highlighter::new(config);
214
215 let match_result = MatchResult {
216 matched: false,
217 pattern: None,
218 color: None,
219 should_notify: false,
220 };
221
222 highlighter
224 .print_line("Normal line", None, &match_result, false)
225 .unwrap();
226 }
227
228 #[test]
229 fn test_print_line_with_match() {
230 let config = create_test_config();
231 let mut highlighter = Highlighter::new(config);
232
233 let match_result = MatchResult {
234 matched: true,
235 pattern: Some("ERROR".to_string()),
236 color: Some(Color::Red),
237 should_notify: true,
238 };
239
240 highlighter
242 .print_line("ERROR: Something went wrong", None, &match_result, false)
243 .unwrap();
244 }
245
246 #[test]
247 fn test_dry_run_prefix() {
248 let config = create_test_config();
249 let mut highlighter = Highlighter::new(config);
250
251 let match_result = MatchResult {
252 matched: true,
253 pattern: Some("ERROR".to_string()),
254 color: Some(Color::Red),
255 should_notify: true,
256 };
257
258 highlighter
260 .print_line("ERROR: Something went wrong", None, &match_result, true)
261 .unwrap();
262 }
263
264 #[test]
265 fn test_print_file_error() {
266 let config = create_test_config();
267 let mut highlighter = Highlighter::new(config);
268 let result = highlighter.print_file_error("test.log", "Permission denied");
269 assert!(result.is_ok());
270 }
271
272 #[test]
273 fn test_print_shutdown_summary() {
274 let config = create_test_config();
275 let mut highlighter = Highlighter::new(config);
276 let stats = WatcherStats {
277 files_watched: 2,
278 lines_processed: 100,
279 lines_excluded: 10,
280 matches_found: 5,
281 notifications_sent: 3,
282 };
283 let result = highlighter.print_shutdown_summary(&stats);
284 assert!(result.is_ok());
285 }
286
287 #[test]
288 fn test_print_file_rotation() {
289 let config = create_test_config();
290 let mut highlighter = Highlighter::new(config);
291 let result = highlighter.print_file_rotation("test.log");
292 assert!(result.is_ok());
293 }
294
295 #[test]
296 fn test_print_file_reopened() {
297 let config = create_test_config();
298 let mut highlighter = Highlighter::new(config);
299 let result = highlighter.print_file_reopened("test.log");
300 assert!(result.is_ok());
301 }
302
303 #[test]
304 fn test_print_startup_info() {
305 let config = create_test_config();
306 let mut highlighter = Highlighter::new(config);
307 let result = highlighter.print_startup_info();
308 assert!(result.is_ok());
309 }
310
311 #[test]
312 fn test_print_colored_with_custom_color() {
313 let config = create_test_config();
314 let mut highlighter = Highlighter::new(config);
315 let result = highlighter.print_colored("Custom message", Color::Magenta);
316 assert!(result.is_ok());
317 }
318
319 #[test]
320 fn test_print_plain() {
321 let config = create_test_config();
322 let mut highlighter = Highlighter::new(config);
323 let result = highlighter.print_plain("Plain message");
324 assert!(result.is_ok());
325 }
326
327 #[test]
328 fn test_color_choice_never() {
329 let args = Args {
330 files: vec![PathBuf::from("test.log")],
331 completions: None,
332 patterns: "ERROR".to_string(),
333 regex: false,
334 case_insensitive: false,
335 color_map: None,
336 notify: false,
337 notify_patterns: None,
338 quiet: false,
339 dry_run: false,
340 exclude: None,
341 prefix_file: Some(false),
342 poll_interval: 1000,
343 buffer_size: 8192,
344 no_color: true, notify_throttle: 0,
346 };
347
348 let config = Config::from_args(&args).unwrap();
349 let highlighter = Highlighter::new(config);
350
351 assert!(highlighter.config.no_color);
353 }
354
355 #[test]
356 fn test_quiet_mode_skip_non_matching() {
357 let args = Args {
358 files: vec![PathBuf::from("test.log")],
359 completions: None,
360 patterns: "ERROR".to_string(),
361 regex: false,
362 case_insensitive: false,
363 color_map: None,
364 notify: false,
365 notify_patterns: None,
366 quiet: true, dry_run: false,
368 exclude: None,
369 prefix_file: Some(false),
370 poll_interval: 1000,
371 buffer_size: 8192,
372 no_color: false,
373 notify_throttle: 0,
374 };
375
376 let config = Config::from_args(&args).unwrap();
377 let mut highlighter = Highlighter::new(config);
378
379 let match_result = MatchResult {
381 matched: false,
382 pattern: None,
383 color: None,
384 should_notify: false,
385 };
386
387 let result = highlighter.print_line("Normal line", None, &match_result, false);
388 assert!(result.is_ok());
389 }
390
391 #[test]
392 fn test_print_dry_run_summary_empty() {
393 let config = create_test_config();
394 let mut highlighter = Highlighter::new(config);
395
396 let matches = vec![];
398 let result = highlighter.print_dry_run_summary(&matches);
399 assert!(result.is_ok());
400 }
401
402 #[test]
403 fn test_print_dry_run_summary_with_matches() {
404 let config = create_test_config();
405 let mut highlighter = Highlighter::new(config);
406
407 let matches = vec![("ERROR".to_string(), 5), ("WARN".to_string(), 3)];
409 let result = highlighter.print_dry_run_summary(&matches);
410 assert!(result.is_ok());
411 }
412
413 #[test]
414 fn test_print_dry_run_summary_coverage_line_116() {
415 let config = create_test_config();
416 let mut highlighter = Highlighter::new(config);
417
418 let matches = vec![("ERROR".to_string(), 2)];
420 let result = highlighter.print_dry_run_summary(&matches);
421 assert!(result.is_ok());
422 }
423}