//! Editor console: captures `log` records into a ring buffer the Console panel //! renders. //! //! The engine and modules already speak through the `log` crate — in particular //! the scripting layer routes script `print`/`debug` and "script paused: …" //! errors to `target: "oxide_script"` (see `oxide-script`). This module installs //! a logger that mirrors every record into an in-memory ring buffer *and* still //! forwards it to `env_logger` for the terminal, so the editor's Console panel //! can show script output and errors without the engine knowing about the editor. //! //! The buffer is a process global (the `log` facade allows only one logger, set //! once at startup), reached by the panel through [`log_buffer`] — so wiring it //! in touches neither `Shell::new` nor its many test call sites. use std::collections::VecDeque; use std::sync::{Arc, Mutex, OnceLock}; use log::{Level, Log, Metadata, Record}; /// How many recent log lines the console keeps. Older lines are dropped. const CAPACITY: usize = 2000; /// One captured log record, flattened to what the panel renders. #[derive(Debug, Clone)] pub struct LogLine { /// Severity, used to colour the line. pub level: Level, /// The record's target (e.g. `oxide_script`), shown dimmed before the text. pub target: String, /// The formatted message. pub message: String, } /// A bounded ring buffer of the most recent [`LogLine`]s. #[derive(Default)] pub struct LogBuffer { lines: VecDeque, } impl LogBuffer { /// Appends a line, evicting the oldest if at capacity. fn push(&mut self, line: LogLine) { if self.lines.len() == CAPACITY { self.lines.pop_front(); } self.lines.push_back(line); } /// Iterates the buffered lines, oldest first. pub fn iter(&self) -> impl Iterator { self.lines.iter() } /// The number of buffered lines. pub fn len(&self) -> usize { self.lines.len() } /// Whether the buffer is empty. pub fn is_empty(&self) -> bool { self.lines.is_empty() } /// Drops all buffered lines (the panel's Clear button). pub fn clear(&mut self) { self.lines.clear(); } } /// The process-wide capture buffer, set by [`init`]. static LOG_BUFFER: OnceLock>> = OnceLock::new(); /// The shared capture buffer, if logging has been initialised. pub fn log_buffer() -> Option<&'static Arc>> { LOG_BUFFER.get() } /// Appends a line to the console from outside the `log` stream — used by the /// command terminal to echo commands and stream a process's output into the /// same panel. No-op if logging is not initialised. pub fn append(level: Level, target: &str, message: impl Into) { if let Some(buffer) = LOG_BUFFER.get() { if let Ok(mut buffer) = buffer.lock() { buffer.push(LogLine { level, target: target.to_string(), message: message.into(), }); } } } /// A logger that mirrors records into [`LOG_BUFFER`] and forwards them to an /// inner `env_logger` for the terminal. struct CaptureLogger { inner: env_logger::Logger, buffer: Arc>, } impl Log for CaptureLogger { fn enabled(&self, metadata: &Metadata) -> bool { self.inner.enabled(metadata) } fn log(&self, record: &Record) { // Honour the env filter for both the terminal and the buffer, so // RUST_LOG controls the console too. if !self.inner.enabled(record.metadata()) { return; } if let Ok(mut buffer) = self.buffer.lock() { buffer.push(LogLine { level: record.level(), target: record.target().to_string(), message: record.args().to_string(), }); } self.inner.log(record); } fn flush(&self) { self.inner.flush(); } } /// Installs the capturing logger and returns the shared buffer. Mirrors the old /// `env_logger` setup (honours `RUST_LOG`, default `info`) but also feeds the /// editor Console. Call once at startup, before any logging. pub fn init() { let inner = env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).build(); let max = inner.filter(); let buffer = Arc::new(Mutex::new(LogBuffer::default())); let _ = LOG_BUFFER.set(buffer.clone()); if log::set_boxed_logger(Box::new(CaptureLogger { inner, buffer })).is_ok() { log::set_max_level(max); } } #[cfg(test)] mod tests { use super::*; #[test] fn ring_buffer_evicts_oldest_past_capacity() { let mut buf = LogBuffer::default(); for i in 0..(CAPACITY + 10) { buf.push(LogLine { level: Level::Info, target: "t".into(), message: format!("line {i}"), }); } assert_eq!(buf.len(), CAPACITY); // The oldest 10 were evicted, so the first surviving line is "line 10". assert_eq!(buf.iter().next().unwrap().message, "line 10"); } #[test] fn clear_empties_the_buffer() { let mut buf = LogBuffer::default(); buf.push(LogLine { level: Level::Warn, target: "t".into(), message: "x".into(), }); assert!(!buf.is_empty()); buf.clear(); assert!(buf.is_empty()); } }