f56a1eea3b
Full project snapshot migrated to new Gitea remote without history: engine, editor, physics, script, examples, tests, docs, and assets. Relicensed from GPLv3 to MIT and updated repo URLs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
173 lines
5.4 KiB
Rust
173 lines
5.4 KiB
Rust
//! 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<LogLine>,
|
|
}
|
|
|
|
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<Item = &LogLine> {
|
|
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<Arc<Mutex<LogBuffer>>> = OnceLock::new();
|
|
|
|
/// The shared capture buffer, if logging has been initialised.
|
|
pub fn log_buffer() -> Option<&'static Arc<Mutex<LogBuffer>>> {
|
|
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<String>) {
|
|
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<Mutex<LogBuffer>>,
|
|
}
|
|
|
|
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());
|
|
}
|
|
}
|