use owo_colors::{OwoColorize, Style};
use crate::api::models::LogLine;
use crate::render::theme;
#[derive(Clone, Copy)]
pub struct LineFormat {
pub stream: bool,
pub time: bool,
}
impl LineFormat {
pub fn with_stream() -> Self {
Self {
stream: true,
time: false,
}
}
pub fn plain() -> Self {
Self {
stream: false,
time: false,
}
}
pub fn showing_time(self, time: bool) -> Self {
Self { time, ..self }
}
}
pub fn render_line(line: &LogLine, format: LineFormat) -> String {
let mut out = String::new();
if format.time {
out.push_str(&format!(
"{} ",
clock_of(line.at.as_deref()).style(theme::muted())
));
}
if format.stream {
out.push_str(&format!(
"{} ",
pad_stream(&line.stream).style(stream_style(&line.stream))
));
}
out.push_str(&format!("{} ", level_mark(&line.level)));
if has_escapes(&line.message) {
out.push_str(&line.message);
} else {
out.push_str(&highlight(&line.message, &line.level));
}
out
}
fn clock_of(at: Option<&str>) -> String {
let Some(stamp) = at else {
return " ".repeat(8);
};
match stamp.split('T').nth(1) {
Some(time) if time.len() >= 8 => time[..8].to_owned(),
_ => format!("{stamp:<8.8}"),
}
}
fn has_escapes(message: &str) -> bool {
message.contains('\u{1b}')
}
fn pad_stream(stream: &str) -> String {
let shown = if stream.is_empty() { "—" } else { stream };
format!("{shown:<7}")
}
fn stream_style(stream: &str) -> Style {
match stream {
"build" => theme::accent(),
"deploy" => Style::new().blue(),
"runtime" => theme::success(),
_ => theme::muted(),
}
}
fn level_mark(level: &str) -> String {
match level {
"error" => format!("{}", "✖".style(theme::danger())),
"warn" => format!("{}", "!".style(theme::warning())),
"debug" => format!("{}", "·".style(theme::muted())),
_ => format!("{}", "│".style(theme::muted())),
}
}
fn highlight(message: &str, level: &str) -> String {
if level == "error" {
return format!("{}", message.style(theme::danger()));
}
if level == "warn" {
return format!("{}", message.style(theme::warning()));
}
let lowered = message.to_lowercase();
if lowered.contains("error") || lowered.contains("failed") || lowered.contains("exception") {
return format!("{}", message.style(theme::danger()));
}
if lowered.contains("warn") || lowered.contains("deprecated") {
return format!("{}", message.style(theme::warning()));
}
message.to_owned()
}
#[cfg(test)]
mod tests {
use super::*;
fn line(message: &str, level: &str) -> LogLine {
LogLine {
sequence: 1,
at: Some("2026-07-30T20:59:16.695Z".into()),
stream: "build".into(),
level: level.into(),
message: message.into(),
}
}
#[test]
fn it_should_keep_the_message_text_intact() {
assert!(render_line(&line("compiling", "info"), LineFormat::plain()).contains("compiling"));
}
#[test]
fn it_should_name_the_stream_when_asked() {
assert!(
render_line(&line("compiling", "info"), LineFormat::with_stream()).contains("build")
);
}
#[test]
fn it_should_leave_a_message_that_already_has_escapes_untouched() {
let coloured = "\u{1b}[32mok\u{1b}[0m";
assert!(render_line(&line(coloured, "info"), LineFormat::plain()).contains(coloured));
}
#[test]
fn it_should_show_the_wall_clock_when_asked() {
let shown = render_line(
&line("compiling", "info"),
LineFormat::plain().showing_time(true),
);
assert!(shown.contains("20:59:16"));
}
#[test]
fn it_should_leave_out_the_date_that_every_line_would_repeat() {
let shown = render_line(
&line("compiling", "info"),
LineFormat::plain().showing_time(true),
);
assert!(!shown.contains("2026-07-30"));
}
#[test]
fn it_should_hold_the_column_when_a_line_arrived_without_a_time() {
assert_eq!(clock_of(None).len(), 8);
}
#[test]
fn it_should_hold_the_column_when_the_time_is_not_a_stamp() {
assert_eq!(clock_of(Some("soon")).len(), 8);
}
#[test]
fn it_should_pad_a_missing_stream_rather_than_shifting_the_column() {
assert_eq!(pad_stream("").chars().count(), 7);
}
#[test]
fn it_should_pad_every_stream_to_the_same_width() {
assert_eq!(
pad_stream("build").chars().count(),
pad_stream("runtime").chars().count()
);
}
}