//! Utilities for logging use crate::args::Args; use colored::{Color, Colorize}; use env_logger::Builder; use log::Level; use log::Level::{Debug, Error, Info, Trace, Warn}; use log::LevelFilter; use std::cmp::Reverse; use std::io::Write; use std::path::{Path, PathBuf}; use web_time::Instant; /// Holds a log entry #[derive(Debug)] pub struct Log { /// Log entry level pub level: Level, /// Time when the entry was logged pub time: Instant, /// File name associated with the entry pub file: PathBuf, /// Line number in the formatted file pub linum_new: Option, /// Line number in the original file pub linum_old: Option, /// Line content pub line: Option, /// Entry-specific message pub message: String, } /// Append a log to the logs list fn record_log( logs: &mut Vec, level: Level, file: &Path, linum_new: Option, linum_old: Option, line: Option, message: &str, ) { let log = Log { level, time: Instant::now(), file: file.to_path_buf(), linum_new, linum_old, line, message: message.to_string(), }; logs.push(log); } /// Append a file log to the logs list pub fn record_file_log( logs: &mut Vec, level: Level, file: &Path, message: &str, ) { record_log(logs, level, file, None, None, None, message); } /// Append a line log to the logs list pub fn record_line_log( logs: &mut Vec, level: Level, file: &Path, linum_new: usize, linum_old: usize, line: &str, message: &str, ) { record_log( logs, level, file, Some(linum_new), Some(linum_old), Some(line.to_string()), message, ); } /// Get the color of a log level const fn get_log_color(log_level: Level) -> Color { match log_level { Info => Color::Cyan, Warn => Color::Yellow, Error => Color::Red, Trace => Color::Green, Debug => panic!(), } } /// Start the logger pub fn init_logger(level_filter: LevelFilter) { Builder::new() .filter_level(level_filter) .format(|buf, record| { writeln!( buf, "{}: {}", record .level() .to_string() .color(get_log_color(record.level())) .bold(), record.args() ) }) .init(); } /// Sort and remove duplicates fn preprocess_logs(logs: &mut Vec) { logs.sort_by_key(|l| { ( l.level, l.linum_new, l.linum_old, l.message.clone(), Reverse(l.time), ) }); logs.dedup_by(|a, b| { ( a.level, &a.file, a.linum_new, a.linum_old, &a.line, &a.message, ) == ( b.level, &b.file, b.linum_new, b.linum_old, &b.line, &b.message, ) }); logs.sort_by_key(|l| l.time); } /// Format a log entry fn format_log(log: &Log) -> String { let linum_new = log .linum_new .map_or_else(String::new, |i| format!("Line {i} ")); let linum_old = log .linum_old .map_or_else(String::new, |i| format!("({i}). ")); let line = log .line .as_ref() .map_or_else(String::new, |l| l.trim_start().to_string()); let log_string = format!( "{}{}{} {}", linum_new.white().bold(), linum_old.white().bold(), log.message.yellow().bold(), line, ); log_string } /// Format all of the logs collected #[allow(clippy::similar_names)] pub fn format_logs(logs: &mut Vec, args: &Args) -> String { preprocess_logs(logs); let mut logs_string = String::new(); for log in logs { if log.level <= args.verbosity { let log_string = format_log(log); logs_string.push_str(&log_string); logs_string.push('\n'); } } logs_string } /// Print all of the logs collected /// /// # Panics /// /// This function panics if the file path does not exist pub fn print_logs(logs: &mut Vec) { preprocess_logs(logs); for log in logs { let log_string = format!( "{} {}: {}", "tex-fmt".magenta().bold(), log.file.to_str().unwrap().blue().bold(), format_log(log), ); match log.level { Error => log::error!("{log_string}"), Warn => log::warn!("{log_string}"), Info => log::info!("{log_string}"), Trace => log::trace!("{log_string}"), Debug => panic!(), } } }