Files
Oxide/editor/src/console.rs
T
Homer Simpson 9eead719b0 Import Oxide engine (Stages 0–10) under MIT license
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>
2026-07-05 20:41:02 +02:00

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());
}
}