use std::io::{self, Write};
use std::path::PathBuf;
use std::sync::{LazyLock, Mutex};
pub(crate) const RING_CAPACITY: usize = 64 * 1024;
pub(crate) struct CaptureRing {
buf: Vec<u8>,
pos: usize,
filled: usize,
}
impl CaptureRing {
pub(crate) fn with_capacity(cap: usize) -> Self {
Self {
buf: Vec::with_capacity(cap),
pos: 0,
filled: 0,
}
}
pub(crate) fn push(&mut self, bytes: &[u8]) {
let cap = self.buf.capacity().max(1);
for &b in bytes {
if self.buf.len() < cap {
self.buf.push(b);
} else {
self.buf[self.pos] = b;
}
self.pos = (self.pos + 1) % cap;
}
self.filled = self.filled.saturating_add(bytes.len()).min(cap);
}
pub(crate) fn snapshot(&self) -> Vec<u8> {
let cap = self.buf.capacity();
if self.buf.len() < cap || self.pos == 0 {
return self.buf.clone();
}
let mut out = Vec::with_capacity(self.buf.len());
out.extend_from_slice(&self.buf[self.pos..]);
out.extend_from_slice(&self.buf[..self.pos]);
out
}
}
static RING: LazyLock<Mutex<CaptureRing>> =
LazyLock::new(|| Mutex::new(CaptureRing::with_capacity(RING_CAPACITY)));
pub(crate) fn record(bytes: &[u8]) {
if let Ok(mut ring) = RING.lock() {
ring.push(bytes);
}
}
pub(crate) fn snapshot() -> Vec<u8> {
RING.lock().map(|ring| ring.snapshot()).unwrap_or_default()
}
fn capture_file_enabled() -> bool {
std::env::var("OPENCRABS_TUI_CAPTURE").is_ok_and(|v| !v.is_empty() && v != "0")
}
fn capture_file_path() -> PathBuf {
let home = std::env::var("HOME")
.unwrap_or_else(|_| std::env::temp_dir().to_string_lossy().into_owned());
PathBuf::from(home)
.join(".opencrabs")
.join("logs")
.join("tui-capture.bin")
}
fn append_capture_file(bytes: &[u8]) {
let path = capture_file_path();
if let Some(dir) = path.parent() {
let _ = std::fs::create_dir_all(dir);
}
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
{
let _ = f.write_all(bytes);
}
}
pub(crate) struct CaptureWriter<W: io::Write> {
inner: W,
}
impl<W: io::Write> CaptureWriter<W> {
pub(crate) fn new(inner: W) -> Self {
Self { inner }
}
}
impl<W: io::Write> Write for CaptureWriter<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
record(buf);
if capture_file_enabled() {
append_capture_file(buf);
}
self.inner.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.inner.flush()
}
}
pub(crate) fn escape_bytes(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 2);
for &b in bytes {
match b {
b'\n' => out.push_str("\\n"),
b'\r' => out.push_str("\\r"),
b'\t' => out.push_str("\\t"),
0x20..=0x7e => out.push(b as char),
_ => out.push_str(&format!("\\x{b:02x}")),
}
}
out
}
pub(crate) fn dump_tail_to_log(context: &str) {
const TAIL: usize = 2048;
let snap = snapshot();
let start = snap.len().saturating_sub(TAIL);
let tail = &snap[start..];
tracing::error!(
"[TUI] byte-stream tail ({} of {} bytes) {}: {}",
tail.len(),
snap.len(),
context,
escape_bytes(tail)
);
}