use crate::console::{Console, ConsoleOptions};
use crate::measure::Measurement;
use crate::protocol::Renderable;
use crate::segment::Segment;
use crate::style::Style;
use crate::text::Text;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
}
impl LogLevel {
fn styled(self) -> (&'static str, &'static str) {
match self {
LogLevel::Trace => ("TRACE", "dim"),
LogLevel::Debug => ("DEBUG", "green"),
LogLevel::Info => ("INFO", "blue"),
LogLevel::Warn => ("WARN", "yellow"),
LogLevel::Error => ("ERROR", "bold red"),
}
}
}
pub struct LogRender {
time: Option<String>,
level: LogLevel,
message: String,
path: Option<String>,
}
impl LogRender {
pub fn new(level: LogLevel, message: impl Into<String>) -> Self {
LogRender {
time: None,
level,
message: message.into(),
path: None,
}
}
pub fn time(mut self, time: impl Into<String>) -> Self {
self.time = Some(time.into());
self
}
pub fn path(mut self, path: impl Into<String>) -> Self {
self.path = Some(path.into());
self
}
fn content(&self) -> Text {
let mut text = Text::new("");
if let Some(time) = &self.time {
text.append(time, Style::parse("dim cyan").ok().map(Into::into));
text.append(" ", None);
}
let (name, spec) = self.level.styled();
text.append(
&format!("{name:<5}"),
Style::parse(spec).ok().map(Into::into),
);
text.append(" ", None);
text.append(&self.message, None);
if let Some(path) = &self.path {
text.append(
&format!(" {path}"),
Style::parse("dim").ok().map(Into::into),
);
}
text
}
}
impl Renderable for LogRender {
fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
self.content().rich_render(console, options)
}
fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
self.content().measure(console, options)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::color::ColorSystem;
fn render(record: &LogRender) -> String {
Console::builder()
.force_terminal(true)
.color_system(Some(ColorSystem::Truecolor))
.width(80)
.no_color(false)
.build()
.render_to_string(record)
}
#[test]
fn info_record_is_blue_level() {
let out = render(&LogRender::new(LogLevel::Info, "server started"));
assert!(out.contains("\x1b[34mINFO \x1b[0m"), "got {out:?}");
assert!(out.contains("server started"));
}
#[test]
fn error_level_is_bold_red() {
let out = render(&LogRender::new(LogLevel::Error, "boom"));
assert!(out.contains("\x1b[1;31mERROR\x1b[0m"), "got {out:?}");
}
#[test]
fn time_and_path_columns() {
let out = render(
&LogRender::new(LogLevel::Warn, "low disk")
.time("12:00:00")
.path("main.rs:42"),
);
assert!(out.contains("\x1b[2;36m12:00:00\x1b[0m"), "got {out:?}");
assert!(out.contains("main.rs:42"));
assert!(out.contains("\x1b[33mWARN \x1b[0m"));
}
}