use std::cell::RefCell;
use std::fmt;
use std::io::Write;
thread_local! {
static CAPTURE: RefCell<Option<Vec<u8>>> = const { RefCell::new(None) };
}
pub fn write_fmt(args: fmt::Arguments<'_>) {
let captured = CAPTURE.with(|slot| {
let mut slot = slot.borrow_mut();
let Some(buf) = slot.as_mut() else {
return false;
};
let _ = buf.write_fmt(args);
true
});
if !captured {
print!("{args}");
}
}
pub fn capturing() -> bool {
CAPTURE.with(|slot| slot.borrow().is_some())
}
macro_rules! out {
($($arg:tt)*) => {
$crate::output::write_fmt(format_args!($($arg)*))
};
}
macro_rules! outln {
() => {
$crate::output::write_fmt(format_args!("\n"))
};
($($arg:tt)*) => {
$crate::output::write_fmt(format_args!("{}\n", format_args!($($arg)*)))
};
}
struct Restore(Option<Vec<u8>>);
impl Drop for Restore {
fn drop(&mut self) {
CAPTURE.with(|slot| *slot.borrow_mut() = self.0.take());
}
}
pub fn capture<R>(f: impl FnOnce() -> R) -> (R, String) {
let previous = CAPTURE.with(|slot| slot.borrow_mut().replace(Vec::new()));
let restore = Restore(previous);
let result = f();
let buf = CAPTURE
.with(|slot| slot.borrow_mut().take())
.unwrap_or_default();
drop(restore);
(result, strip_ansi(&String::from_utf8_lossy(&buf)))
}
fn strip_ansi(text: &str) -> String {
let mut out = String::with_capacity(text.len());
let mut chars = text.chars().peekable();
while let Some(c) = chars.next() {
if c != '\x1b' {
out.push(c);
continue;
}
match chars.next() {
Some('[') => {
for c in chars.by_ref() {
if ('\x40'..='\x7e').contains(&c) {
break;
}
}
}
Some(']') => {
let mut prev = '\0';
for c in chars.by_ref() {
if c == '\x07' || (prev == '\x1b' && c == '\\') {
break;
}
prev = c;
}
}
Some(other) => out.push(other),
None => {}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use owo_colors::OwoColorize;
#[test]
fn capture_collects_and_strips_styling() {
let (value, text) = capture(|| {
outln!("{} {}", "bold".bold(), "red".bright_red());
out!("tail");
7
});
assert_eq!(value, 7);
assert_eq!(text, "bold red\ntail");
}
#[test]
fn captures_nest_and_restore() {
let (_, outer) = capture(|| {
outln!("before");
let (_, inner) = capture(|| outln!("inner"));
assert_eq!(inner, "inner\n");
outln!("after");
});
assert_eq!(outer, "before\nafter\n");
}
#[test]
fn strip_ansi_handles_osc_and_lone_escape() {
assert_eq!(strip_ansi("a\x1b]0;title\x07b"), "ab");
assert_eq!(strip_ansi("a\x1b]0;title\x1b\\b"), "ab");
assert_eq!(strip_ansi("a\x1b"), "a");
assert_eq!(strip_ansi("plain"), "plain");
}
}