use crate::{Rgb, RgbRange, Theme};
use colored::Colorize;
use log::Level;
use std::collections::HashMap;
use std::sync::Mutex;
use unicode_segmentation::UnicodeSegmentation;
pub enum ColorFormat {
Solid,
InlineGradient(usize),
MultiLineGradient(usize),
}
fn oscillate_dist(x: usize, n: usize) -> f32 {
let n = if n == 0 { 1 } else { n };
(x.wrapping_add(n) % n.wrapping_mul(2)).abs_diff(n) as f32 / (n as f32)
}
fn linear_gradient(range: &RgbRange, dist: f32) -> Rgb {
let dist = dist.clamp(0.0, 1.0);
let start = &range.start;
let end = &range.end;
let r_range = (end.r as f32) - (start.r as f32);
let g_range = (end.g as f32) - (start.g as f32);
let b_range = (end.b as f32) - (start.b as f32);
Rgb {
r: ((start.r as f32) + (dist * r_range)) as u8,
g: ((start.g as f32) + (dist * g_range)) as u8,
b: ((start.b as f32) + (dist * b_range)) as u8,
}
}
pub struct LogPainter {
lines_logged: Mutex<HashMap<Level, usize>>,
theme: Box<dyn Theme>,
color_format: Option<ColorFormat>,
}
impl LogPainter {
pub fn new(theme: Box<dyn Theme>, color_format: Option<ColorFormat>) -> LogPainter {
LogPainter {
lines_logged: Mutex::new(HashMap::new()),
theme,
color_format,
}
}
pub fn paint(&self, msg: String, level: Level) -> String {
if self.color_format.is_none() {
return msg;
}
let line = match self.color_format.as_ref().unwrap() {
ColorFormat::Solid => self.paint_solid(msg, level),
ColorFormat::InlineGradient(steps) => self.paint_inline_gradient(msg, level, *steps),
ColorFormat::MultiLineGradient(steps) => {
let l = self.paint_multi_line_gradient(msg, level, *steps);
self.lines_logged
.lock()
.unwrap()
.entry(level)
.and_modify(|e| *e = e.wrapping_add(1))
.or_insert(0);
return l;
}
};
return line;
}
fn paint_solid(&self, msg: String, level: Level) -> String {
let color = self.theme.solid(level);
msg.color(color).to_string()
}
fn paint_inline_gradient(&self, msg: String, level: Level, steps: usize) -> String {
msg.graphemes(true)
.enumerate()
.map(|(i, c)| {
let dist = oscillate_dist(i, steps);
let color = linear_gradient(&self.theme.range(level), dist);
c.color(color).to_string()
})
.collect::<Vec<String>>()
.join("")
}
fn paint_multi_line_gradient(&self, msg: String, level: Level, steps: usize) -> String {
let lines_logged = *self.lines_logged.lock().unwrap().entry(level).or_insert(0);
let dist = oscillate_dist(lines_logged, steps);
let color = linear_gradient(&self.theme.range(level), dist);
msg.color(color).to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::theme;
use num::NumCast;
fn assert_eq_with_eps<T: NumCast>(a: T, b: T, eps: T) {
let a: f64 = NumCast::from(a).unwrap();
let b: f64 = NumCast::from(b).unwrap();
let eps: f64 = NumCast::from(eps).unwrap();
if (a - b).abs() > eps {
panic!("{} and {} were not equal", a, b);
}
}
fn assert_rgb_eq(lhs: Rgb, rhs: Rgb, eps: Option<u8>) {
let eps = eps.unwrap_or(1);
assert_eq_with_eps(lhs.r, rhs.r, eps);
assert_eq_with_eps(lhs.g, rhs.g, eps);
assert_eq_with_eps(lhs.b, rhs.b, eps);
}
fn assert_logs_colored_by_level(f: &dyn Fn(&LogPainter, String, Level) -> String) {
let theme = Box::new(theme::Simple {});
let painter = LogPainter::new(theme, Some(ColorFormat::Solid));
let msg = "foo".to_string();
let lines = [
f(&painter, msg.clone(), Level::Trace),
f(&painter, msg.clone(), Level::Debug),
f(&painter, msg.clone(), Level::Info),
f(&painter, msg.clone(), Level::Warn),
f(&painter, msg.clone(), Level::Error),
];
for (i, line) in lines.iter().enumerate() {
for line1 in lines.iter().skip(i + 1) {
if line == line1 {
panic!("\"{}\" and \"{}\" had different levels but generated the same formatted line", line, line1);
}
}
}
}
#[test]
fn linear_gradient_calculates_correct_color() {
let r = RgbRange {
start: Rgb { r: 0, g: 0, b: 0 },
end: Rgb {
r: 255,
g: 255,
b: 255,
},
};
assert_rgb_eq(linear_gradient(&r, 0.0), Rgb { r: 0, g: 0, b: 0 }, None);
assert_rgb_eq(
linear_gradient(&r, 0.25),
Rgb {
r: 64,
g: 64,
b: 64,
},
None,
);
assert_rgb_eq(
linear_gradient(&r, 0.5),
Rgb {
r: 128,
g: 128,
b: 128,
},
None,
);
assert_rgb_eq(
linear_gradient(&r, 0.75),
Rgb {
r: 190,
g: 190,
b: 190,
},
None,
);
assert_rgb_eq(
linear_gradient(&r, 1.0),
Rgb {
r: 255,
g: 255,
b: 255,
},
None,
);
}
#[test]
fn linear_gradient_clamps_dist() {
let r = RgbRange {
start: Rgb { r: 0, g: 0, b: 0 },
end: Rgb {
r: 255,
g: 255,
b: 255,
},
};
let expected = Rgb { r: 0, g: 0, b: 0 };
assert_rgb_eq(linear_gradient(&r, -1.0), expected, None);
let expected = Rgb {
r: 255,
g: 255,
b: 255,
};
assert_rgb_eq(linear_gradient(&r, 100.0), expected, None);
}
#[test]
fn oscillate_dist_oscillates() {
assert_eq_with_eps(oscillate_dist(0, 255), 0.0, 1e-2);
assert_eq_with_eps(oscillate_dist(128, 255), 0.5, 1e-2);
assert_eq_with_eps(oscillate_dist(255, 255), 1.0, 1e-2);
assert_eq_with_eps(oscillate_dist(256, 255), 1.0 - (1.0 / 255.0), 1e-2);
assert_eq_with_eps(oscillate_dist(383, 255), 0.5, 1e-2);
assert_eq_with_eps(oscillate_dist(510, 255), 0.0, 1e-2);
assert_eq_with_eps(oscillate_dist(638, 255), 0.5, 1e-2);
assert_eq_with_eps(oscillate_dist(765, 255), 1.0, 1e-2);
assert_eq_with_eps(
oscillate_dist(usize::max_value(), 255),
oscillate_dist(usize::max_value() - 255, 255),
1e-2,
);
assert_eq_with_eps(oscillate_dist(12, usize::max_value()), 1.0, 1e-2);
assert_eq_with_eps(oscillate_dist(257, usize::max_value()), 1.0, 1e-2);
assert_eq_with_eps(
oscillate_dist(usize::max_value(), usize::max_value()),
1.0,
1e-2,
);
}
#[test]
fn oscillate_dist_handles_0_n() {
oscillate_dist(0, 0);
}
#[test]
fn paint_solid_colors_by_level() {
assert_logs_colored_by_level(&LogPainter::paint_solid);
}
#[test]
fn paint_inline_gradient_colors_by_level() {
let color_fn = |painter: &LogPainter, msg: String, level: Level| -> String {
painter.paint_inline_gradient(msg, level, 20)
};
assert_logs_colored_by_level(&color_fn);
}
#[test]
fn paint_multi_line_gradient_colors_by_level() {
let color_fn = |painter: &LogPainter, msg: String, level: Level| -> String {
painter.paint_multi_line_gradient(msg, level, 20)
};
assert_logs_colored_by_level(&color_fn);
}
#[test]
fn paint_fns_handle_empty_msg() {
let theme = Box::new(theme::Simple {});
let color_format = Some(ColorFormat::Solid);
let painter = LogPainter::new(theme, color_format);
painter.paint_solid("".to_string(), Level::Warn);
painter.paint_inline_gradient("".to_string(), Level::Warn, 10);
painter.paint_multi_line_gradient("".to_string(), Level::Warn, 10);
}
#[test]
fn paint_with_none_format_returns_orig() {
let theme = Box::new(theme::Simple {});
let color_format = None;
let painter = LogPainter::new(theme, color_format);
let msg = "foo".to_string();
assert_eq!(painter.paint(msg.clone(), Level::Info), msg);
}
#[test]
fn paint_log_with_inline_gradient_uses_steps_arg() {
let theme = Box::new(theme::Simple {});
let color_format = Some(ColorFormat::InlineGradient(2));
let painter = LogPainter::new(theme, color_format);
let msgs = vec!["0000000000".to_string(), "नमस्तेनमस्तेनमस्तेनमस्तेनमस्ते".to_string()];
for msg in msgs {
let msg_colored = painter.paint(msg.clone(), Level::Info);
let words = msg_colored.unicode_words().collect::<Vec<&str>>();
let num_words = words.len();
let graphemes = msg.graphemes(true).collect::<Vec<&str>>().len();
let escape_seqs = words
.into_iter()
.step_by(num_words / graphemes)
.collect::<Vec<&str>>();
assert_ne!(escape_seqs[0], escape_seqs[1]);
assert_eq!(escape_seqs[0], escape_seqs[4]);
assert_eq!(escape_seqs[1], escape_seqs[5]);
assert_eq!(escape_seqs[2], escape_seqs[6]);
assert_eq!(escape_seqs[3], escape_seqs[7]);
}
}
#[test]
fn paint_log_with_multi_line_gradient_changes_color_within_level() {
let theme = Box::new(theme::Simple {});
let color_format = Some(ColorFormat::MultiLineGradient(20));
let painter = LogPainter::new(theme, color_format);
let msg = "foo".to_string();
let assert_color_changes_within_level = |level: Level| {
let mut last_logged = "".to_string();
for _ in 0..10 {
let l = painter.paint(msg.clone(), level);
assert_ne!(last_logged, l);
last_logged = l;
}
};
assert_color_changes_within_level(Level::Trace);
assert_color_changes_within_level(Level::Debug);
assert_color_changes_within_level(Level::Info);
assert_color_changes_within_level(Level::Warn);
assert_color_changes_within_level(Level::Error);
}
#[test]
fn paint_log_with_multi_line_gradient_uses_steps_arg() {
let steps: usize = 2;
let theme = Box::new(theme::Simple {});
let color_format = Some(ColorFormat::MultiLineGradient(steps));
let painter = LogPainter::new(theme, color_format);
let msg = "foo".to_string();
let lines = vec![
painter.paint(msg.clone(), Level::Info),
painter.paint(msg.clone(), Level::Info),
painter.paint(msg.clone(), Level::Info),
painter.paint(msg.clone(), Level::Info),
painter.paint(msg.clone(), Level::Info),
painter.paint(msg.clone(), Level::Info),
painter.paint(msg.clone(), Level::Info),
painter.paint(msg.clone(), Level::Info),
];
assert_ne!(lines[0], lines[1]);
assert_eq!(lines[0], lines[4]);
assert_eq!(lines[1], lines[5]);
assert_eq!(lines[2], lines[6]);
assert_eq!(lines[3], lines[7]);
}
}