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>
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
//! 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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user