oo-ide 0.0.4

∞ is a terminal IDE focused on low distraction, high usability.
Documentation
//! IDE-internal logging system.
//!
//! `ooLogger` hooks into the `log` crate and fans each record out to:
//!   1. A fixed-capacity in-memory ring buffer (`LogBuffer`).
//!   2. A `tokio::sync::broadcast` channel so the log view receives live entries.
//!
//! The file sink is handled by `simplelog::WriteLogger` via
//! `simplelog::CombinedLogger`; this module only manages the in-process buffer.

use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use std::time::SystemTime;

use log::{Level, LevelFilter, Metadata, Record};
use simplelog::{Config, SharedLogger};
use tokio::sync::broadcast;

// ---------------------------------------------------------------------------
// Public types
// ---------------------------------------------------------------------------

/// One captured log record.
#[derive(Clone, Debug)]
pub struct LogEntry {
    pub time: SystemTime,
    pub level: Level,
    /// Target reported by the `log` macro (e.g. `oo::lsp`).
    pub target: String,
    pub message: String,
}

/// Fixed-capacity FIFO that keeps the most recent N entries.
pub struct LogBuffer {
    capacity: usize,
    entries: VecDeque<LogEntry>,
}

impl LogBuffer {
    fn new(capacity: usize) -> Self {
        Self {
            capacity,
            entries: VecDeque::with_capacity(capacity),
        }
    }

    fn push(&mut self, entry: LogEntry) {
        if self.entries.len() == self.capacity {
            self.entries.pop_front();
        }
        self.entries.push_back(entry);
    }

    /// Snapshot for the log view to display on open.
    pub fn snapshot(&self) -> Vec<LogEntry> {
        self.entries.iter().cloned().collect()
    }
}

/// Shared state between the custom logger and any subscribers (e.g. `LogView`).
pub struct LogState {
    pub buffer: Mutex<LogBuffer>,
    pub tx: broadcast::Sender<LogEntry>,
}

impl LogState {
    fn new(capacity: usize) -> Arc<Self> {
        let (tx, _) = broadcast::channel(4096);
        Arc::new(Self {
            buffer: Mutex::new(LogBuffer::new(capacity)),
            tx,
        })
    }

    /// Subscribe to live log entries.
    pub fn subscribe(&self) -> broadcast::Receiver<LogEntry> {
        self.tx.subscribe()
    }
}

// ---------------------------------------------------------------------------
// In-memory logger — implements simplelog::SharedLogger
// ---------------------------------------------------------------------------

pub struct InMemoryLogger {
    state: Arc<LogState>,
    level: LevelFilter,
}

impl log::Log for InMemoryLogger {
    fn enabled(&self, metadata: &Metadata) -> bool {
        metadata.level() <= self.level
    }

    fn log(&self, record: &Record) {
        if !self.enabled(record.metadata()) {
            return;
        }
        let entry = LogEntry {
            time: SystemTime::now(),
            level: record.level(),
            target: record.target().to_owned(),
            message: record.args().to_string(),
        };
        if let Ok(mut buf) = self.state.buffer.lock() {
            buf.push(entry.clone());
        }
        // Ignore send errors — no subscribers is fine.
        let _ = self.state.tx.send(entry);
    }

    fn flush(&self) {}
}

impl SharedLogger for InMemoryLogger {
    fn level(&self) -> LevelFilter {
        self.level
    }

    fn config(&self) -> Option<&Config> {
        None
    }

    fn as_log(self: Box<Self>) -> Box<dyn log::Log> {
        self
    }
}

// ---------------------------------------------------------------------------
// Initialisation
// ---------------------------------------------------------------------------

/// Initialise a combined logger: `simplelog::WriteLogger` to `log_file` plus
/// the in-memory logger.  Returns the `LogState` handle to thread into the app.
///
/// # Panics
/// Panics if a global logger has already been set.
pub fn init_combined_logger(log_file: std::fs::File, level: LevelFilter) -> Arc<LogState> {
    let state = LogState::new(10_000);

    let file_logger = simplelog::WriteLogger::new(level, Config::default(), log_file);

    let mem_logger = Box::new(InMemoryLogger {
        state: state.clone(),
        level,
    });

    simplelog::CombinedLogger::init(vec![file_logger, mem_logger])
        .expect("logger already initialised");

    state
}