kanban-tui 0.7.0

Terminal user interface for the kanban project management tool
Documentation
use std::collections::VecDeque;
use std::sync::{Arc, Mutex};

pub const MAX_ENTRIES: usize = 200;

#[derive(Debug, Clone)]
pub enum LogLevel {
    Error,
    Warn,
}

#[derive(Debug, Clone)]
pub struct LogEntry {
    pub level: LogLevel,
    pub message: String,
    pub target: String,
    pub timestamp: chrono::DateTime<chrono::Local>,
}

#[derive(Debug, Default)]
pub struct ErrorLogState {
    pub entries: VecDeque<LogEntry>,
    pub has_unread_errors: bool,
    pub unread_count: usize,
    pub error_count: usize,
}

impl ErrorLogState {
    pub fn push(&mut self, message: String, target: String, level: LogLevel) {
        let is_error = matches!(level, LogLevel::Error);
        self.entries.push_back(LogEntry {
            level,
            message,
            target,
            timestamp: chrono::Local::now(),
        });
        if self.entries.len() > MAX_ENTRIES {
            self.entries.pop_front();
        }
        self.has_unread_errors = true;
        self.unread_count += 1;
        if is_error {
            self.error_count += 1;
        }
    }

    pub fn clear_unread(&mut self) {
        self.has_unread_errors = false;
        self.unread_count = 0;
    }
}

pub struct InMemoryLogLayer {
    pub state: Arc<Mutex<ErrorLogState>>,
}

impl InMemoryLogLayer {
    pub fn new(state: Arc<Mutex<ErrorLogState>>) -> Self {
        Self { state }
    }
}

impl<S: tracing::Subscriber> tracing_subscriber::Layer<S> for InMemoryLogLayer {
    fn on_event(
        &self,
        event: &tracing::Event<'_>,
        _ctx: tracing_subscriber::layer::Context<'_, S>,
    ) {
        let level = event.metadata().level();
        if *level > tracing::Level::WARN {
            return;
        }
        let log_level = if *level == tracing::Level::ERROR {
            LogLevel::Error
        } else {
            LogLevel::Warn
        };
        let target = event.metadata().target().to_string();
        let mut visitor = MessageVisitor(String::new());
        event.record(&mut visitor);
        if let Ok(mut state) = self.state.lock() {
            state.push(visitor.0, target, log_level);
        }
    }
}

struct MessageVisitor(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" {
            let formatted = format!("{value:?}");
            self.0 = formatted
                .strip_prefix('"')
                .and_then(|s| s.strip_suffix('"'))
                .unwrap_or(&formatted)
                .to_string();
        }
    }

    fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
        if field.name() == "message" {
            self.0 = value.to_string();
        }
    }
}