tooters 0.1.0

A Rust TUI Mastodon App
use core::fmt;
use parking_lot::Mutex;
use ratatui::{
    buffer::Buffer,
    layout::{Alignment, Rect},
    style::{Color, Modifier, Style},
    text::{Line, Span},
    widgets::{Block, Borders, Paragraph, Widget, Wrap},
};
use std::sync::Arc;
use tracing::{
    field::{Field, Visit},
    Event, Subscriber,
};
use tracing_subscriber::{layer::Context, registry::LookupSpan, Layer};

#[derive(Default)]
pub struct LogCollector {
    logs: Arc<Mutex<Vec<LogMessage>>>,
}

pub struct LogMessage {
    pub level: String,
    pub target: String,
    pub message: String,
}

impl LogCollector {
    #[must_use]
    pub fn logs(&self) -> Arc<Mutex<Vec<LogMessage>>> {
        self.logs.clone()
    }
}

impl<S> Layer<S> for LogCollector
where
    S: Subscriber + for<'a> LookupSpan<'a>,
{
    fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
        let mut logs = self.logs.lock();
        let metadata = event.metadata();
        let level = metadata.level().to_string();
        let target = metadata.target();
        let mut visitor = MessageVisitor::default();
        event.record(&mut visitor);
        let message = visitor.message;
        let log = LogMessage {
            level,
            target: target.to_string(),
            message,
        };
        logs.push(log);
    }
}

#[derive(Default)]
struct MessageVisitor {
    message: String,
}

impl Visit for MessageVisitor {
    fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
        if field.name() == "message" {
            self.message = format!("{value:?}");
        }
    }
}

pub struct LogWidget {
    logs: Arc<Mutex<Vec<LogMessage>>>,
}

impl LogWidget {
    pub fn new(logs: Arc<Mutex<Vec<LogMessage>>>) -> Self {
        Self { logs }
    }
}

impl Widget for LogWidget {
    fn render(self, area: Rect, buf: &mut Buffer) {
        let logs = self.logs.lock();
        let max_lines = area.height as usize;
        let start_index = if logs.len() > max_lines {
            logs.len() - max_lines
        } else {
            0
        };

        let text = logs[start_index..]
            .iter()
            .map(|log| {
                let level_color = match log.level.as_str() {
                    "ERROR" => Color::Red,
                    "WARN" => Color::Yellow,
                    "INFO" => Color::Green,
                    "DEBUG" => Color::Blue,
                    "TRACE" => Color::Cyan,
                    _ => Color::White,
                };
                Line::from(vec![
                    Span::styled(&log.level, Style::default().fg(level_color)),
                    Span::raw(" "),
                    Span::styled(&log.target, Style::default().add_modifier(Modifier::DIM)),
                    Span::styled(": ", Style::default().add_modifier(Modifier::DIM)),
                    Span::styled(&log.message, Style::default()),
                ])
            })
            .collect::<Vec<_>>();

        let paragraph = Paragraph::new(text)
            .block(Block::default().borders(Borders::ALL).title("Logs"))
            .alignment(Alignment::Left)
            .wrap(Wrap { trim: true });

        paragraph.render(area, buf);
    }
}