use std::cell::RefCell;
use crate::console::{Console, ConsoleOptions, Overflow};
use crate::measure::Measurement;
use crate::protocol::Renderable;
use crate::segment::Segment;
use crate::style::{Style, StyleType};
use crate::table::Table;
use crate::text::Text;
pub struct LogRender {
show_time: bool,
show_level: bool,
show_path: bool,
omit_repeated_times: bool,
level_width: Option<usize>,
last_time: RefCell<Option<Text>>,
}
impl Default for LogRender {
fn default() -> Self {
LogRender {
show_time: true,
show_level: false,
show_path: true,
omit_repeated_times: true,
level_width: Some(8),
last_time: RefCell::new(None),
}
}
}
impl LogRender {
pub fn new() -> Self {
LogRender::default()
}
pub fn show_time(mut self, show: bool) -> Self {
self.show_time = show;
self
}
pub fn show_level(mut self, show: bool) -> Self {
self.show_level = show;
self
}
pub fn show_path(mut self, show: bool) -> Self {
self.show_path = show;
self
}
pub fn omit_repeated_times(mut self, omit: bool) -> Self {
self.omit_repeated_times = omit;
self
}
pub fn level_width(mut self, width: Option<usize>) -> Self {
self.level_width = width;
self
}
#[allow(clippy::too_many_arguments)]
pub fn render(
&self,
console: &Console,
message: Text,
time: Option<Text>,
level: Text,
path: Option<&str>,
line_no: Option<u32>,
link_path: Option<&str>,
) -> Table {
let style = |name: &str| {
console
.get_style(&StyleType::from(name))
.unwrap_or_default()
};
let mut output = Table::grid().padding(0, 1, 0, 1).expand(true);
if self.show_time {
output.add_column("").column_style(style("log.time"));
}
if self.show_level {
output.add_column("").column_style(style("log.level"));
if let Some(width) = self.level_width {
output.column_width(width);
}
}
output
.add_column("")
.column_ratio(1)
.column_style(style("log.message"))
.column_overflow(Overflow::Fold);
let path = path
.filter(|_| self.show_path)
.filter(|path| !path.is_empty());
if path.is_some() {
output.add_column("").column_style(style("log.path"));
}
let mut row = Vec::new();
if self.show_time {
let display = time.unwrap_or_default();
let mut last = self.last_time.borrow_mut();
let repeated = last.as_ref().is_some_and(|last| same_text(last, &display));
if repeated && self.omit_repeated_times {
row.push(Text::new(" ".repeat(display.plain().chars().count())));
} else {
row.push(display.clone());
*last = Some(display);
}
}
if self.show_level {
row.push(level);
}
row.push(message);
if let Some(path) = path {
let link = |target: String| Style::new().with_link(target);
let mut path_text = Text::new("");
path_text.append(
path,
link_path.map(|link_path| link(format!("file://{link_path}")).into()),
);
if let Some(line_no) = line_no.filter(|line| *line > 0) {
path_text.append(":", None);
path_text.append(
&line_no.to_string(),
link_path.map(|link_path| link(format!("file://{link_path}#{line_no}")).into()),
);
}
row.push(path_text);
}
output.add_row_text(row);
output
}
}
fn same_text(a: &Text, b: &Text) -> bool {
a.plain() == b.plain() && a.spans() == b.spans()
}
pub fn level_text(name: &str) -> Text {
Text::styled(
format!("{name:<8}"),
format!("logging.level.{}", name.to_lowercase()),
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
}
impl LogLevel {
pub fn name(self) -> &'static str {
match self {
LogLevel::Trace => "TRACE",
LogLevel::Debug => "DEBUG",
LogLevel::Info => "INFO",
LogLevel::Warn => "WARNING",
LogLevel::Error => "ERROR",
}
}
pub fn text(self) -> Text {
match self {
LogLevel::Trace => Text::styled(format!("{:<8}", "TRACE"), "logging.level.notset"),
level => level_text(level.name()),
}
}
}
pub struct LogRecord {
level: LogLevel,
message: String,
time: Option<String>,
path: Option<String>,
line_no: Option<u32>,
}
impl LogRecord {
pub fn new(level: LogLevel, message: impl Into<String>) -> Self {
LogRecord {
level,
message: message.into(),
time: None,
path: None,
line_no: 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
}
pub fn line(mut self, line: u32) -> Self {
self.line_no = Some(line);
self
}
fn table(&self, console: &Console) -> Table {
LogRender::new()
.show_level(true)
.show_time(self.time.is_some())
.render(
console,
Text::new(self.message.clone()),
self.time.as_deref().map(Text::new),
self.level.text(),
self.path.as_deref(),
self.line_no,
None,
)
}
}
impl Renderable for LogRecord {
fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
self.table(console).rich_render(console, options)
}
fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
self.table(console).measure(console, options)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::color::ColorSystem;
fn console() -> Console {
Console::builder()
.force_terminal(true)
.color_system(Some(ColorSystem::Truecolor))
.width(40)
.highlight(false)
.build()
}
#[test]
fn a_repeated_time_is_blanked() {
let console = console();
let render = LogRender::new();
let first = console.render_to_string(&render.render(
&console,
Text::new("one"),
Some(Text::new("[12:00]")),
Text::new(""),
None,
None,
None,
));
let second = console.render_to_string(&render.render(
&console,
Text::new("two"),
Some(Text::new("[12:00]")),
Text::new(""),
None,
None,
None,
));
assert!(first.contains("[12:00]"), "{first:?}");
assert!(!second.contains("[12:00]"), "{second:?}");
assert!(second.contains("two"));
}
#[test]
fn warn_uses_pythons_level_name() {
let out = console().render_to_string(&LogRecord::new(LogLevel::Warn, "low disk"));
assert!(out.contains("\x1b[33mWARNING \x1b[0m"), "{out:?}");
assert!(out.contains("low disk"));
}
}