use log::Level;
use once_cell::sync::Lazy;
use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph, Widget},
};
use std::fmt::Write;
use std::sync::Mutex;
static UI_LOG_BUFFER: Lazy<Mutex<Vec<(String, String, Level)>>> =
Lazy::new(|| Mutex::new(Vec::with_capacity(BUFFER_LIMIT)));
const BUFFER_LIMIT: usize = 100;
#[allow(dead_code)]
pub struct LogMessage {
pub timestamp: String,
pub message: String,
pub level: LogLevel,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LogLevel {
Error,
Warn,
Info,
Debug,
Trace,
}
static LEVEL_STR: [&str; 5] = ["ERROR", "WARN ", "INFO ", "DEBUG", "TRACE"];
static LEVEL_COLOR: [Color; 5] = [
Color::Red,
Color::Yellow,
Color::Green,
Color::Blue,
Color::DarkGray,
];
const LEVEL_MAP: [Level; 5] = [
Level::Error,
Level::Warn,
Level::Info,
Level::Debug,
Level::Trace,
];
#[inline]
fn to_sys_level(l: LogLevel) -> Level {
LEVEL_MAP[l as usize]
}
impl From<Level> for LogLevel {
fn from(level: Level) -> Self {
match level {
Level::Error => LogLevel::Error,
Level::Warn => LogLevel::Warn,
Level::Info => LogLevel::Info,
Level::Debug => LogLevel::Debug,
Level::Trace => LogLevel::Trace,
}
}
}
pub fn make_timestamp() -> String {
let now = chrono::Local::now();
let mut s = String::with_capacity(16);
write!(s, "{}", now.format("%H:%M:%S%.3f")).unwrap();
s
}
pub fn debug<S: AsRef<str>>(message: S) {
let message_ref = message.as_ref();
log_message(message_ref, LogLevel::Debug);
}
pub fn info<S: AsRef<str>>(message: S) {
let message_ref = message.as_ref();
log_message(message_ref, LogLevel::Info);
}
#[allow(dead_code)]
pub fn warn<S: AsRef<str>>(message: S) {
let message_ref = message.as_ref();
log_message(message_ref, LogLevel::Warn);
}
pub fn error<S: AsRef<str>>(message: S) {
let message_ref = message.as_ref();
log_message(message_ref, LogLevel::Error);
}
pub fn trace<S: AsRef<str>>(message: S) {
let message_ref = message.as_ref();
log_message(message_ref, LogLevel::Trace);
}
fn log_message(message: &str, level: LogLevel) {
let timestamp = make_timestamp();
if let Ok(mut buffer) = UI_LOG_BUFFER.lock() {
if buffer.len() == BUFFER_LIMIT {
buffer.pop();
}
let obj = (timestamp, message.to_string(), to_sys_level(level));
buffer.insert(0, obj);
}
}
pub struct DebugOverlay {}
impl DebugOverlay {
pub fn new() -> Self {
Self {}
}
}
impl Widget for DebugOverlay {
fn render(self, area: Rect, buf: &mut Buffer) {
let log_area_height = area.height.saturating_mul(2) / 3;
let log_area = Rect {
x: area.x,
y: area.height.saturating_sub(log_area_height),
width: area.width,
height: log_area_height,
};
let debug_block = Block::default()
.title(" Debug Overlay [d to toggle] ")
.borders(Borders::ALL)
.style(
Style::default()
.bg(Color::Black)
.fg(Color::White)
.add_modifier(Modifier::BOLD),
);
let inner_area = debug_block.inner(log_area);
debug_block.render(log_area, buf);
let messages = {
if let Ok(buffer) = UI_LOG_BUFFER.lock() {
let take = inner_area.height as usize;
let len = buffer.len();
let start = len.saturating_sub(take);
buffer[start..len].to_vec()
} else {
Vec::with_capacity(BUFFER_LIMIT)
}
};
let mut text = Vec::with_capacity(inner_area.height as usize);
for msg in messages.into_iter().rev() {
let (timestamp, message, level) = msg;
let log_level = LogLevel::from(level);
let i = log_level as usize;
let line = Line::from(vec![
Span::styled(
format!("{} ", timestamp),
Style::default().fg(Color::DarkGray),
),
Span::styled(LEVEL_STR[i], Style::default().fg(LEVEL_COLOR[i])),
Span::raw(message),
]);
text.push(line);
}
let paragraph =
Paragraph::new(text).style(Style::default().bg(Color::Black).fg(Color::White));
paragraph.render(inner_area, buf);
}
}