tex_fmt/
logging.rs

1//! Utilities for logging
2
3use crate::args::Args;
4use colored::{Color, Colorize};
5use env_logger::Builder;
6use log::Level;
7use log::Level::{Debug, Error, Info, Trace, Warn};
8use log::LevelFilter;
9use std::cmp::Reverse;
10use std::io::Write;
11use std::path::{Path, PathBuf};
12use web_time::Instant;
13
14/// Holds a log entry
15#[derive(Debug)]
16pub struct Log {
17    /// Log entry level
18    pub level: Level,
19    /// Time when the entry was logged
20    pub time: Instant,
21    /// File name associated with the entry
22    pub file: PathBuf,
23    /// Line number in the formatted file
24    pub linum_new: Option<usize>,
25    /// Line number in the original file
26    pub linum_old: Option<usize>,
27    /// Line content
28    pub line: Option<String>,
29    /// Entry-specific message
30    pub message: String,
31}
32
33/// Append a log to the logs list
34fn record_log(
35    logs: &mut Vec<Log>,
36    level: Level,
37    file: &Path,
38    linum_new: Option<usize>,
39    linum_old: Option<usize>,
40    line: Option<String>,
41    message: &str,
42) {
43    let log = Log {
44        level,
45        time: Instant::now(),
46        file: file.to_path_buf(),
47        linum_new,
48        linum_old,
49        line,
50        message: message.to_string(),
51    };
52    logs.push(log);
53}
54
55/// Append a file log to the logs list
56pub fn record_file_log(
57    logs: &mut Vec<Log>,
58    level: Level,
59    file: &Path,
60    message: &str,
61) {
62    record_log(logs, level, file, None, None, None, message);
63}
64
65/// Append a line log to the logs list
66pub fn record_line_log(
67    logs: &mut Vec<Log>,
68    level: Level,
69    file: &Path,
70    linum_new: usize,
71    linum_old: usize,
72    line: &str,
73    message: &str,
74) {
75    record_log(
76        logs,
77        level,
78        file,
79        Some(linum_new),
80        Some(linum_old),
81        Some(line.to_string()),
82        message,
83    );
84}
85
86/// Get the color of a log level
87const fn get_log_color(log_level: Level) -> Color {
88    match log_level {
89        Info => Color::Cyan,
90        Warn => Color::Yellow,
91        Error => Color::Red,
92        Trace => Color::Green,
93        Debug => panic!(),
94    }
95}
96
97/// Start the logger
98pub fn init_logger(level_filter: LevelFilter) {
99    Builder::new()
100        .filter_level(level_filter)
101        .format(|buf, record| {
102            writeln!(
103                buf,
104                "{}: {}",
105                record
106                    .level()
107                    .to_string()
108                    .color(get_log_color(record.level()))
109                    .bold(),
110                record.args()
111            )
112        })
113        .init();
114}
115
116/// Sort and remove duplicates
117fn preprocess_logs(logs: &mut Vec<Log>) {
118    logs.sort_by_key(|l| {
119        (
120            l.level,
121            l.linum_new,
122            l.linum_old,
123            l.message.clone(),
124            Reverse(l.time),
125        )
126    });
127    logs.dedup_by(|a, b| {
128        (
129            a.level,
130            &a.file,
131            a.linum_new,
132            a.linum_old,
133            &a.line,
134            &a.message,
135        ) == (
136            b.level,
137            &b.file,
138            b.linum_new,
139            b.linum_old,
140            &b.line,
141            &b.message,
142        )
143    });
144    logs.sort_by_key(|l| l.time);
145}
146
147/// Format a log entry
148fn format_log(log: &Log) -> String {
149    let linum_new = log
150        .linum_new
151        .map_or_else(String::new, |i| format!("Line {i} "));
152
153    let linum_old = log
154        .linum_old
155        .map_or_else(String::new, |i| format!("({i}). "));
156
157    let line = log
158        .line
159        .as_ref()
160        .map_or_else(String::new, |l| l.trim_start().to_string());
161
162    let log_string = format!(
163        "{}{}{} {}",
164        linum_new.white().bold(),
165        linum_old.white().bold(),
166        log.message.yellow().bold(),
167        line,
168    );
169    log_string
170}
171
172/// Format all of the logs collected
173#[allow(clippy::similar_names)]
174pub fn format_logs(logs: &mut Vec<Log>, args: &Args) -> String {
175    preprocess_logs(logs);
176    let mut logs_string = String::new();
177    for log in logs {
178        if log.level <= args.verbosity {
179            let log_string = format_log(log);
180            logs_string.push_str(&log_string);
181            logs_string.push('\n');
182        }
183    }
184    logs_string
185}
186
187/// Print all of the logs collected
188///
189/// # Panics
190///
191/// This function panics if the file path does not exist
192pub fn print_logs(logs: &mut Vec<Log>) {
193    preprocess_logs(logs);
194    for log in logs {
195        let log_string = format!(
196            "{} {}: {}",
197            "tex-fmt".magenta().bold(),
198            log.file.to_str().unwrap().blue().bold(),
199            format_log(log),
200        );
201
202        match log.level {
203            Error => log::error!("{log_string}"),
204            Warn => log::warn!("{log_string}"),
205            Info => log::info!("{log_string}"),
206            Trace => log::trace!("{log_string}"),
207            Debug => panic!(),
208        }
209    }
210}