use crate::ui::tui::log_buffer::LogBuffer;
use ratatui::{
layout::{Alignment, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph},
Frame,
};
use std::sync::{Arc, Mutex};
use tracing::Level;
pub const MIN_LOG_PANEL_HEIGHT: u16 = 3;
pub const MAX_LOG_PANEL_HEIGHT: u16 = 10;
pub const DEFAULT_LOG_PANEL_HEIGHT: u16 = 3;
fn level_color(level: Level) -> Color {
match level {
Level::ERROR => Color::Red,
Level::WARN => Color::Yellow,
Level::INFO => Color::White,
Level::DEBUG => Color::DarkGray,
Level::TRACE => Color::DarkGray,
}
}
fn level_span(level: Level) -> Span<'static> {
let (text, color) = match level {
Level::ERROR => ("ERROR", Color::Red),
Level::WARN => (" WARN", Color::Yellow),
Level::INFO => (" INFO", Color::White),
Level::DEBUG => ("DEBUG", Color::DarkGray),
Level::TRACE => ("TRACE", Color::DarkGray),
};
Span::styled(
text,
Style::default().fg(color).add_modifier(Modifier::BOLD),
)
}
pub fn render(
f: &mut Frame,
area: Rect,
buffer: &Arc<Mutex<LogBuffer>>,
scroll_offset: usize,
show_timestamps: bool,
) {
let available_lines = area.height.saturating_sub(2) as usize;
let (entries, total) = if let Ok(buffer) = buffer.lock() {
let total = buffer.len();
let entries: Vec<_> = buffer
.get_window(scroll_offset, available_lines)
.into_iter()
.cloned()
.collect();
(entries, total)
} else {
(Vec::new(), 0)
};
let lines: Vec<Line> = entries
.iter()
.map(|entry| {
let mut spans = Vec::new();
if show_timestamps {
spans.push(Span::styled(
format!("{} ", entry.timestamp.format("%H:%M:%S")),
Style::default().fg(Color::DarkGray),
));
}
spans.push(Span::raw("["));
spans.push(level_span(entry.level));
spans.push(Span::raw("] "));
let short_target = entry.target.rsplit("::").next().unwrap_or(&entry.target);
spans.push(Span::styled(
format!("{short_target}: "),
Style::default().fg(Color::Cyan),
));
spans.push(Span::styled(
entry.message.clone(),
Style::default().fg(level_color(entry.level)),
));
Line::from(spans)
})
.collect();
let left_title = if scroll_offset > 0 {
format!(" Logs ({} more below) ", scroll_offset)
} else if total > available_lines {
format!(" Logs ({} entries) ", total)
} else {
" Logs ".to_string()
};
let right_title = Line::from(Span::styled(
" j/k:scroll +/-:resize t:time ",
Style::default().fg(Color::DarkGray),
));
let mut display_lines = lines;
while display_lines.len() < available_lines {
display_lines.insert(0, Line::from(""));
}
let paragraph = Paragraph::new(display_lines).block(
Block::default()
.title_top(left_title)
.title_top(right_title.alignment(Alignment::Right))
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::DarkGray)),
);
f.render_widget(paragraph, area);
}
pub fn render_empty(f: &mut Frame, area: Rect) {
let paragraph = Paragraph::new(vec![Line::from(Span::styled(
"No logs captured",
Style::default().fg(Color::DarkGray),
))])
.block(
Block::default()
.title(" Logs ")
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::DarkGray)),
);
f.render_widget(paragraph, area);
}
pub fn calculate_layout(
total_area: Rect,
log_panel_height: u16,
log_panel_visible: bool,
) -> (Rect, Option<Rect>) {
if !log_panel_visible {
return (total_area, None);
}
let panel_height = log_panel_height.clamp(MIN_LOG_PANEL_HEIGHT, MAX_LOG_PANEL_HEIGHT);
if total_area.height <= panel_height + MIN_LOG_PANEL_HEIGHT {
return (total_area, None);
}
let main_height = total_area.height - panel_height;
let main_area = Rect {
x: total_area.x,
y: total_area.y,
width: total_area.width,
height: main_height,
};
let log_area = Rect {
x: total_area.x,
y: total_area.y + main_height,
width: total_area.width,
height: panel_height,
};
(main_area, Some(log_area))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_calculate_layout_visible() {
let total = Rect::new(0, 0, 80, 24);
let (main, log) = calculate_layout(total, 5, true);
assert_eq!(main.height, 19);
assert!(log.is_some());
let log = log.unwrap();
assert_eq!(log.height, 5);
assert_eq!(log.y, 19);
}
#[test]
fn test_calculate_layout_hidden() {
let total = Rect::new(0, 0, 80, 24);
let (main, log) = calculate_layout(total, 5, false);
assert_eq!(main.height, 24);
assert!(log.is_none());
}
#[test]
fn test_calculate_layout_too_small() {
let total = Rect::new(0, 0, 80, 5);
let (main, log) = calculate_layout(total, 5, true);
assert_eq!(main.height, 5);
assert!(log.is_none());
}
#[test]
fn test_level_color() {
assert_eq!(level_color(Level::ERROR), Color::Red);
assert_eq!(level_color(Level::WARN), Color::Yellow);
assert_eq!(level_color(Level::INFO), Color::White);
assert_eq!(level_color(Level::DEBUG), Color::DarkGray);
}
}