use colored::{Color, Colorize};
use dissimilar::Chunk;
use log::{Level, Log, Metadata, Record};
use std::{
fmt::Write as _,
io::{self, StderrLock, StdoutLock, Write},
sync::atomic::{AtomicUsize, Ordering},
};
use super::DefmtRecord;
pub(crate) struct PrettyLogger {
always_include_location: bool,
should_log: Box<dyn Fn(&Metadata) -> bool + Sync + Send>,
timing_align: AtomicUsize,
}
impl Log for PrettyLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
(self.should_log)(metadata)
}
fn log(&self, record: &Record) {
if !self.enabled(record.metadata()) {
return;
}
match DefmtRecord::new(record) {
Some(record) => {
let sink = io::stdout().lock();
match record.level() {
Some(level) => self.print_defmt_record(record, level, sink),
None => Self::print_println_record(record, sink),
};
}
None => {
let sink = io::stderr().lock();
self.print_host_record(record, sink);
}
}
}
fn flush(&self) {}
}
impl PrettyLogger {
pub fn new(
always_include_location: bool,
should_log: impl Fn(&Metadata) -> bool + Sync + Send + 'static,
) -> Box<Self> {
Box::new(Self::new_unboxed(always_include_location, should_log))
}
pub fn new_unboxed(
always_include_location: bool,
should_log: impl Fn(&Metadata) -> bool + Sync + Send + 'static,
) -> Self {
Self {
always_include_location,
should_log: Box::new(should_log),
timing_align: AtomicUsize::new(0),
}
}
fn print_defmt_record(&self, record: DefmtRecord, level: Level, mut sink: StdoutLock) {
let len = record.timestamp().len();
self.timing_align.fetch_max(len, Ordering::Relaxed);
let min_timestamp_width = self.timing_align.load(Ordering::Relaxed);
Printer::new(&record, level)
.include_location(true) .min_timestamp_width(min_timestamp_width)
.print_colored(&mut sink)
.ok();
}
pub(super) fn print_host_record(&self, record: &Record, mut sink: StderrLock) {
let min_timestamp_width = self.timing_align.load(Ordering::Relaxed);
writeln!(
sink,
"{timestamp:>0$} {level:5} {args}",
min_timestamp_width,
timestamp = "(HOST)",
level = record
.level()
.to_string()
.color(color_for_log_level(record.level())),
args = record.args()
)
.ok();
if self.always_include_location {
print_location(
&mut sink,
record.file(),
record.line(),
record.module_path(),
)
.ok();
}
}
fn print_println_record(record: DefmtRecord, mut sink: StdoutLock) {
let timestamp = match record.timestamp().is_empty() {
true => record.timestamp().to_string(),
false => format!("{} ", record.timestamp()),
};
writeln!(&mut sink, "{timestamp}{}", record.args()).ok();
print_location(
&mut sink,
record.file(),
record.line(),
record.module_path(),
)
.ok();
}
}
pub struct Printer<'a> {
record: &'a DefmtRecord<'a>,
include_location: bool,
level: Level,
min_timestamp_width: usize,
}
impl<'a> Printer<'a> {
pub fn new(record: &'a DefmtRecord, level: Level) -> Self {
Self {
record,
include_location: false,
level,
min_timestamp_width: 0,
}
}
pub fn include_location(&mut self, include_location: bool) -> &mut Self {
self.include_location = include_location;
self
}
pub fn min_timestamp_width(&mut self, min_timestamp_width: usize) -> &mut Self {
self.min_timestamp_width = min_timestamp_width;
self
}
pub fn print_colored<W: io::Write>(&self, sink: &mut W) -> io::Result<()> {
writeln!(
sink,
"{timestamp:>0$}{spacing}{level:5} {args}",
self.min_timestamp_width,
timestamp = self.record.timestamp(),
spacing = if self.record.timestamp().is_empty() {
""
} else {
" "
},
level = self
.level
.to_string()
.color(color_for_log_level(self.level)),
args = color_diff(self.record.args().to_string()),
)?;
if self.include_location {
let log_record = self.record.log_record;
print_location(
sink,
log_record.file(),
log_record.line(),
log_record.module_path(),
)?;
}
Ok(())
}
}
fn color_diff(text: String) -> String {
let lines = text.lines().collect::<Vec<_>>();
let nlines = lines.len();
if nlines > 2 {
let left = lines[nlines - 2];
let right = lines[nlines - 1];
const LEFT_START: &str = " left: `";
const RIGHT_START: &str = "right: `";
const END: &str = "`";
if left.starts_with(LEFT_START)
&& left.ends_with(END)
&& right.starts_with(RIGHT_START)
&& right.ends_with(END)
{
let left = &left[LEFT_START.len()..left.len() - END.len()];
let right = &right[RIGHT_START.len()..right.len() - END.len()];
let mut buf = lines[..nlines - 2].join("\n").bold().to_string();
buf.push('\n');
let diffs = dissimilar::diff(left, right);
writeln!(
buf,
"{} {} / {}",
"diff".bold(),
"< left".red(),
"right >".green()
)
.ok();
write!(buf, "{}", "<".red()).ok();
for diff in &diffs {
match diff {
Chunk::Equal(s) => {
write!(buf, "{}", s.red()).ok();
}
Chunk::Insert(_) => continue,
Chunk::Delete(s) => {
write!(buf, "{}", s.red().bold()).ok();
}
}
}
buf.push('\n');
write!(buf, "{}", ">".green()).ok();
for diff in &diffs {
match diff {
Chunk::Equal(s) => {
write!(buf, "{}", s.green()).ok();
}
Chunk::Delete(_) => continue,
Chunk::Insert(s) => {
write!(buf, "{}", s.green().bold()).ok();
}
}
}
return buf;
}
}
text.bold().to_string()
}
fn color_for_log_level(level: Level) -> Color {
match level {
Level::Error => Color::Red,
Level::Warn => Color::Yellow,
Level::Info => Color::Green,
Level::Debug => Color::BrightWhite,
Level::Trace => Color::BrightBlack,
}
}
fn print_location<W: io::Write>(
sink: &mut W,
file: Option<&str>,
line: Option<u32>,
module_path: Option<&str>,
) -> io::Result<()> {
if let Some(file) = file {
let mod_path = module_path.unwrap();
let mut loc = file.to_string();
if let Some(line) = line {
let _ = write!(loc, ":{line}");
}
writeln!(sink, "{}", format!("└─ {mod_path} @ {loc}").dimmed())?;
}
Ok(())
}