use crate::config::Config;
use crate::matcher::MatchResult;
use anyhow::Result;
use std::io::Write;
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
#[derive(Debug)]
pub struct Highlighter {
config: Config,
stdout: StandardStream,
stderr: StandardStream,
}
impl Highlighter {
pub fn new(config: Config) -> Self {
let color_choice = if config.no_color {
ColorChoice::Never
} else {
ColorChoice::Auto
};
Self {
config,
stdout: StandardStream::stdout(color_choice),
stderr: StandardStream::stderr(color_choice),
}
}
pub fn print_line(
&mut self,
line: &str,
filename: Option<&str>,
match_result: &MatchResult,
dry_run: bool,
) -> Result<()> {
if self.config.quiet && !match_result.matched {
return Ok(());
}
let mut output_line = String::new();
if dry_run && match_result.matched {
output_line.push_str("[DRY-RUN] ");
}
if self.config.prefix_files {
if let Some(filename) = filename {
output_line.push_str(&format!("[{}] ", filename));
}
}
output_line.push_str(line);
if let Some(color) = match_result.color {
self.print_colored(&output_line, color)?;
} else {
self.print_plain(&output_line)?;
}
Ok(())
}
fn print_colored(&mut self, text: &str, color: Color) -> Result<()> {
self.stdout
.set_color(ColorSpec::new().set_fg(Some(color)))?;
writeln!(self.stdout, "{}", text)?;
self.stdout.reset()?;
self.stdout.flush()?;
Ok(())
}
fn print_plain(&mut self, text: &str) -> Result<()> {
writeln!(self.stdout, "{}", text)?;
self.stdout.flush()?;
Ok(())
}
pub fn print_error(&mut self, message: &str) -> Result<()> {
self.stderr
.set_color(ColorSpec::new().set_fg(Some(Color::Red)))?;
writeln!(self.stderr, "Error: {}", message)?;
self.stderr.reset()?;
self.stderr.flush()?;
Ok(())
}
pub fn print_warning(&mut self, message: &str) -> Result<()> {
self.stderr
.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)))?;
writeln!(self.stderr, "Warning: {}", message)?;
self.stderr.reset()?;
self.stderr.flush()?;
Ok(())
}
pub fn print_info(&mut self, message: &str) -> Result<()> {
self.stderr
.set_color(ColorSpec::new().set_fg(Some(Color::Cyan)))?;
writeln!(self.stderr, "Info: {}", message)?;
self.stderr.reset()?;
self.stderr.flush()?;
Ok(())
}
pub fn print_dry_run_summary(&mut self, matches: &[(String, usize)]) -> Result<()> {
if matches.is_empty() {
self.print_info("No matching lines found")?;
return Ok(());
}
self.print_info("Dry-run summary:")?;
for (pattern, count) in matches {
self.print_plain(&format!(" {}: {} matches", pattern, count))?;
}
self.print_info("Dry-run complete. No notifications sent.")?;
Ok(())
}
pub fn print_startup_info(&mut self) -> Result<()> {
self.print_info(&format!("Watching {} file(s)", self.config.files.len()))?;
if !self.config.patterns.is_empty() {
self.print_info(&format!("Patterns: {}", self.config.patterns.join(", ")))?;
}
if self.config.notify_enabled {
self.print_info("Desktop notifications enabled")?;
}
if self.config.dry_run {
self.print_info("Dry-run mode: reading existing content only")?;
}
Ok(())
}
pub fn print_file_rotation(&mut self, filename: &str) -> Result<()> {
self.print_warning(&format!("File rotation detected for {}", filename))?;
Ok(())
}
pub fn print_file_reopened(&mut self, filename: &str) -> Result<()> {
self.print_info(&format!("Reopened file: {}", filename))?;
Ok(())
}
pub fn print_file_error(&mut self, filename: &str, error: &str) -> Result<()> {
self.print_error(&format!("Error watching {}: {}", filename, error))?;
Ok(())
}
pub fn print_shutdown_summary(&mut self, stats: &WatcherStats) -> Result<()> {
self.print_info("Shutdown summary:")?;
self.print_plain(&format!(" Files watched: {}", stats.files_watched))?;
self.print_plain(&format!(" Lines processed: {}", stats.lines_processed))?;
if stats.lines_excluded > 0 {
self.print_plain(&format!(" Lines excluded: {}", stats.lines_excluded))?;
}
self.print_plain(&format!(" Matches found: {}", stats.matches_found))?;
self.print_plain(&format!(
" Notifications sent: {}",
stats.notifications_sent
))?;
Ok(())
}
}
#[derive(Debug, Default)]
pub struct WatcherStats {
pub files_watched: usize,
pub lines_processed: usize,
pub lines_excluded: usize,
pub matches_found: usize,
pub notifications_sent: usize,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::Args;
use std::path::PathBuf;
fn create_test_config() -> Config {
let args = Args {
files: vec![PathBuf::from("test.log")],
completions: None,
patterns: "ERROR".to_string(),
regex: false,
case_insensitive: false,
color_map: None,
notify: true,
notify_patterns: None,
notify_throttle: 5,
dry_run: false,
quiet: false,
exclude: None,
no_color: true, prefix_file: None,
poll_interval: 100,
buffer_size: 8192,
};
Config::from_args(&args).unwrap()
}
#[test]
fn test_print_line_without_match() {
let config = create_test_config();
let mut highlighter = Highlighter::new(config);
let match_result = MatchResult {
matched: false,
pattern: None,
color: None,
should_notify: false,
};
highlighter
.print_line("Normal line", None, &match_result, false)
.unwrap();
}
#[test]
fn test_print_line_with_match() {
let config = create_test_config();
let mut highlighter = Highlighter::new(config);
let match_result = MatchResult {
matched: true,
pattern: Some("ERROR".to_string()),
color: Some(Color::Red),
should_notify: true,
};
highlighter
.print_line("ERROR: Something went wrong", None, &match_result, false)
.unwrap();
}
#[test]
fn test_dry_run_prefix() {
let config = create_test_config();
let mut highlighter = Highlighter::new(config);
let match_result = MatchResult {
matched: true,
pattern: Some("ERROR".to_string()),
color: Some(Color::Red),
should_notify: true,
};
highlighter
.print_line("ERROR: Something went wrong", None, &match_result, true)
.unwrap();
}
#[test]
fn test_print_file_error() {
let config = create_test_config();
let mut highlighter = Highlighter::new(config);
let result = highlighter.print_file_error("test.log", "Permission denied");
assert!(result.is_ok());
}
#[test]
fn test_print_shutdown_summary() {
let config = create_test_config();
let mut highlighter = Highlighter::new(config);
let stats = WatcherStats {
files_watched: 2,
lines_processed: 100,
lines_excluded: 10,
matches_found: 5,
notifications_sent: 3,
};
let result = highlighter.print_shutdown_summary(&stats);
assert!(result.is_ok());
}
#[test]
fn test_print_file_rotation() {
let config = create_test_config();
let mut highlighter = Highlighter::new(config);
let result = highlighter.print_file_rotation("test.log");
assert!(result.is_ok());
}
#[test]
fn test_print_file_reopened() {
let config = create_test_config();
let mut highlighter = Highlighter::new(config);
let result = highlighter.print_file_reopened("test.log");
assert!(result.is_ok());
}
#[test]
fn test_print_startup_info() {
let config = create_test_config();
let mut highlighter = Highlighter::new(config);
let result = highlighter.print_startup_info();
assert!(result.is_ok());
}
#[test]
fn test_print_colored_with_custom_color() {
let config = create_test_config();
let mut highlighter = Highlighter::new(config);
let result = highlighter.print_colored("Custom message", Color::Magenta);
assert!(result.is_ok());
}
#[test]
fn test_print_plain() {
let config = create_test_config();
let mut highlighter = Highlighter::new(config);
let result = highlighter.print_plain("Plain message");
assert!(result.is_ok());
}
#[test]
fn test_color_choice_never() {
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: true, notify_throttle: 0,
};
let config = Config::from_args(&args).unwrap();
let highlighter = Highlighter::new(config);
assert!(highlighter.config.no_color);
}
#[test]
fn test_quiet_mode_skip_non_matching() {
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: true, dry_run: false,
exclude: None,
prefix_file: Some(false),
poll_interval: 1000,
buffer_size: 8192,
no_color: false,
notify_throttle: 0,
};
let config = Config::from_args(&args).unwrap();
let mut highlighter = Highlighter::new(config);
let match_result = MatchResult {
matched: false,
pattern: None,
color: None,
should_notify: false,
};
let result = highlighter.print_line("Normal line", None, &match_result, false);
assert!(result.is_ok());
}
#[test]
fn test_print_dry_run_summary_empty() {
let config = create_test_config();
let mut highlighter = Highlighter::new(config);
let matches = vec![];
let result = highlighter.print_dry_run_summary(&matches);
assert!(result.is_ok());
}
#[test]
fn test_print_dry_run_summary_with_matches() {
let config = create_test_config();
let mut highlighter = Highlighter::new(config);
let matches = vec![("ERROR".to_string(), 5), ("WARN".to_string(), 3)];
let result = highlighter.print_dry_run_summary(&matches);
assert!(result.is_ok());
}
#[test]
fn test_print_dry_run_summary_coverage_line_116() {
let config = create_test_config();
let mut highlighter = Highlighter::new(config);
let matches = vec![("ERROR".to_string(), 2)];
let result = highlighter.print_dry_run_summary(&matches);
assert!(result.is_ok());
}
}