//! Custom `tracing_subscriber` layer that forwards every formatted log event to the //! TUI so the status indicator can display the *latest* log line while a task is //! running. //! //! The layer is intentionally extremely small: we implement `on_event()` only and //! ignore spans/metadata because we only care about the already‑formatted output //! that the default `fmt` layer would print. We therefore borrow the same //! formatter (`tracing_subscriber::fmt::format::FmtSpan`) used by the default //! fmt layer so the text matches what is written to the log file. use std::fmt::Write as _; use tokio::sync::mpsc::UnboundedSender; use tracing::Event; use tracing::Subscriber; use tracing::field::Field; use tracing::field::Visit; use tracing_subscriber::Layer; use tracing_subscriber::layer::Context; use tracing_subscriber::registry::LookupSpan; /// Maximum characters forwarded to the TUI. Longer messages are truncated so the /// single‑line status indicator cannot overflow the viewport. #[allow(dead_code)] const _DEFAULT_MAX_LEN: usize = 120; pub struct TuiLogLayer { tx: UnboundedSender, max_len: usize, } impl TuiLogLayer { pub fn new(tx: UnboundedSender, max_len: usize) -> Self { Self { tx, max_len: max_len.max(8), } } } impl Layer for TuiLogLayer where S: Subscriber + for<'a> LookupSpan<'a>, { fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) { // Build a terse line like `[TRACE core::session] message …` by visiting // fields into a buffer. This avoids pulling in the heavyweight // formatter machinery. struct Visitor<'a> { buf: &'a mut String, } impl Visit for Visitor<'_> { fn record_debug(&mut self, _field: &Field, value: &dyn std::fmt::Debug) { let _ = write!(self.buf, " {:?}", value); } } let mut buf = String::new(); let _ = write!( buf, "[{} {}]", event.metadata().level(), event.metadata().target() ); event.record(&mut Visitor { buf: &mut buf }); // `String::truncate` operates on UTF‑8 code‑point boundaries and will // panic if the provided index is not one. Because we limit the log // line by its **byte** length we can not guarantee that the index we // want to cut at happens to be on a boundary. Therefore we fall back // to a simple, boundary‑safe loop that pops complete characters until // the string is within the designated size. if buf.len() > self.max_len { // Attempt direct truncate at the byte index. If that is not a // valid boundary we advance to the next one ( ≤3 bytes away ). if buf.is_char_boundary(self.max_len) { buf.truncate(self.max_len); } else { let mut idx = self.max_len; while idx < buf.len() && !buf.is_char_boundary(idx) { idx += 1; } buf.truncate(idx); } } let sanitized = buf.replace(['\n', '\r'], " "); let _ = self.tx.send(sanitized); } }