use super::group::{Group, GroupLike};
use super::shared::Shared;
use super::terminal_widget::TerminalWidget;
use super::window::{Window, WindowLike, WindowPaletteType};
use crate::core::geometry::Rect;
use crate::core::palette::{Attr, TvColor};
use crate::terminal::Terminal;
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::mpsc;
struct LogEntry {
text: String,
attr: Attr,
}
pub struct LogSubscriber {
sender: mpsc::Sender<LogEntry>,
min_level: tracing::Level,
}
impl tracing::Subscriber for LogSubscriber {
fn enabled(&self, metadata: &tracing::Metadata<'_>) -> bool {
metadata.level() <= &self.min_level
}
fn new_span(&self, _attrs: &tracing::span::Attributes<'_>) -> tracing::span::Id {
static NEXT: AtomicUsize = AtomicUsize::new(1);
tracing::span::Id::from_u64(NEXT.fetch_add(1, Ordering::Relaxed) as u64)
}
fn record(&self, _span: &tracing::span::Id, _values: &tracing::span::Record<'_>) {}
fn record_follows_from(&self, _span: &tracing::span::Id, _follows: &tracing::span::Id) {}
fn event(&self, event: &tracing::Event<'_>) {
let metadata = event.metadata();
let level = *metadata.level();
let now = chrono::Local::now();
let timestamp = now.format("%H:%M:%S");
let level_str = match level {
tracing::Level::ERROR => "ERROR",
tracing::Level::WARN => "WARN ",
tracing::Level::INFO => "INFO ",
tracing::Level::DEBUG => "DEBUG",
tracing::Level::TRACE => "TRACE",
};
let mut visitor = MessageVisitor::default();
event.record(&mut visitor);
let message = visitor.message;
let text = format!("{timestamp} {level_str} {message}");
let attr = level_attr(level);
let _ = self.sender.send(LogEntry { text, attr });
}
fn enter(&self, _span: &tracing::span::Id) {}
fn exit(&self, _span: &tracing::span::Id) {}
}
#[derive(Default)]
struct MessageVisitor {
message: String,
}
impl tracing::field::Visit for MessageVisitor {
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
if field.name() == "message" {
self.message = format!("{value:?}");
} else if self.message.is_empty() {
self.message = format!("{}: {value:?}", field.name());
} else {
self.message
.push_str(&format!(" {}={value:?}", field.name()));
}
}
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
if field.name() == "message" {
self.message = value.to_string();
} else if self.message.is_empty() {
self.message = format!("{}: {value}", field.name());
} else {
self.message.push_str(&format!(" {}={value}", field.name()));
}
}
}
fn level_attr(level: tracing::Level) -> Attr {
match level {
tracing::Level::ERROR => Attr::new(TvColor::LightRed, TvColor::Black),
tracing::Level::WARN => Attr::new(TvColor::Yellow, TvColor::Black),
tracing::Level::INFO => Attr::new(TvColor::White, TvColor::Black),
tracing::Level::DEBUG => Attr::new(TvColor::LightGray, TvColor::Black),
tracing::Level::TRACE => Attr::new(TvColor::DarkGray, TvColor::Black),
}
}
pub struct LogWindow {
window: Window,
widget: Rc<RefCell<TerminalWidget>>,
receiver: mpsc::Receiver<LogEntry>,
}
impl LogWindow {
fn drain_logs(&mut self) {
while let Ok(entry) = self.receiver.try_recv() {
self.widget
.borrow_mut()
.append_line_colored(entry.text, entry.attr);
}
}
pub fn log(&mut self, level: tracing::Level, message: &str) {
let now = chrono::Local::now();
let timestamp = now.format("%H:%M:%S");
let level_str = match level {
tracing::Level::ERROR => "ERROR",
tracing::Level::WARN => "WARN ",
tracing::Level::INFO => "INFO ",
tracing::Level::DEBUG => "DEBUG",
tracing::Level::TRACE => "TRACE",
};
let text = format!("{timestamp} {level_str} {message}");
let attr = level_attr(level);
self.widget.borrow_mut().append_line_colored(text, attr);
}
pub fn clear(&mut self) {
self.widget.borrow_mut().clear();
}
}
impl GroupLike for LogWindow {
fn group(&self) -> &Group {
self.window.group()
}
fn group_mut(&mut self) -> &mut Group {
self.window.group_mut()
}
}
impl WindowLike for LogWindow {
fn window(&self) -> &Window {
&self.window
}
fn window_mut(&mut self) -> &mut Window {
&mut self.window
}
}
crate::impl_view_for_window!(LogWindow {
fn draw(&mut self, terminal: &mut Terminal) {
self.drain_logs();
self.window_draw(terminal);
}
});
pub struct LogWindowBuilder {
bounds: Option<Rect>,
title: Option<String>,
min_level: tracing::Level,
max_lines: usize,
}
impl LogWindowBuilder {
pub fn new() -> Self {
Self {
bounds: None,
title: None,
min_level: tracing::Level::TRACE,
max_lines: 10000,
}
}
#[must_use]
pub fn bounds(mut self, bounds: Rect) -> Self {
self.bounds = Some(bounds);
self
}
#[must_use]
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
#[must_use]
pub fn min_level(mut self, level: tracing::Level) -> Self {
self.min_level = level;
self
}
#[must_use]
pub fn max_lines(mut self, max_lines: usize) -> Self {
self.max_lines = max_lines;
self
}
pub fn build(self) -> LogWindow {
let bounds = self.bounds.expect("LogWindow bounds must be set");
let title = self.title.unwrap_or_else(|| "Log".to_string());
let mut window = Window::new_with_type(bounds, &title, WindowPaletteType::Blue);
window.set_custom_palette(vec![
97, 98, 99, 100, 101, 102, 103, 104, ]);
let interior_width = bounds.width() - 2;
let interior_height = bounds.height() - 2;
let widget_bounds = Rect::new(0, 0, interior_width, interior_height);
let mut widget = TerminalWidget::new(widget_bounds);
widget = widget.with_scrollbar();
widget.set_max_lines(self.max_lines);
widget.set_auto_scroll(true);
let widget = Rc::new(RefCell::new(widget));
window.add(Shared::new(Rc::clone(&widget)));
let (sender, receiver) = mpsc::channel();
let subscriber = LogSubscriber {
sender,
min_level: self.min_level,
};
let _ = tracing::subscriber::set_global_default(subscriber);
LogWindow {
window,
widget,
receiver,
}
}
pub fn build_boxed(self) -> Box<LogWindow> {
Box::new(self.build())
}
}
impl Default for LogWindowBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::views::View;
#[test]
fn test_level_attr_colors() {
let error = level_attr(tracing::Level::ERROR);
assert_eq!(error, Attr::new(TvColor::LightRed, TvColor::Black));
let warn = level_attr(tracing::Level::WARN);
assert_eq!(warn, Attr::new(TvColor::Yellow, TvColor::Black));
let info = level_attr(tracing::Level::INFO);
assert_eq!(info, Attr::new(TvColor::White, TvColor::Black));
let debug = level_attr(tracing::Level::DEBUG);
assert_eq!(debug, Attr::new(TvColor::LightGray, TvColor::Black));
let trace = level_attr(tracing::Level::TRACE);
assert_eq!(trace, Attr::new(TvColor::DarkGray, TvColor::Black));
}
#[test]
fn test_log_window_creation() {
let log_window = LogWindowBuilder::new()
.bounds(Rect::new(0, 0, 80, 15))
.title("Test Log")
.min_level(tracing::Level::DEBUG)
.max_lines(500)
.build();
assert_eq!(log_window.bounds(), Rect::new(0, 0, 80, 15));
}
#[test]
fn test_log_window_manual_log() {
let mut log_window = LogWindowBuilder::new()
.bounds(Rect::new(0, 0, 80, 15))
.title("Test Log")
.build();
log_window.log(tracing::Level::INFO, "test message");
assert_eq!(log_window.widget.borrow().line_count(), 1);
log_window.log(tracing::Level::ERROR, "error message");
assert_eq!(log_window.widget.borrow().line_count(), 2);
log_window.clear();
assert_eq!(log_window.widget.borrow().line_count(), 0);
}
}