use ansi_to_tui::Error; use ansi_to_tui::IntoText; use ratatui::text::Line; use ratatui::text::Text; // Expand tabs in a best-effort way for transcript rendering. // Tabs can interact poorly with left-gutter prefixes in our TUI and CLI // transcript views (e.g., `nl` separates line numbers from content with a tab). // Replacing tabs with spaces avoids odd visual artifacts without changing // semantics for our use cases. fn expand_tabs(s: &str) -> std::borrow::Cow<'_, str> { if s.contains('\t') { // Keep it simple: replace each tab with 4 spaces. // We do not try to align to tab stops since most usages (like `nl`) // look acceptable with a fixed substitution and this avoids stateful math // across spans. std::borrow::Cow::Owned(s.replace('\t', " ")) } else { std::borrow::Cow::Borrowed(s) } } /// This function should be used when the contents of `s` are expected to match /// a single line. If multiple lines are found, a warning is logged and only the /// first line is returned. pub fn ansi_escape_line(s: &str) -> Line<'static> { // Normalize tabs to spaces to avoid odd gutter collisions in transcript mode. let s = expand_tabs(s); let text = ansi_escape(&s); match text.lines.as_slice() { [] => "".into(), [only] => only.clone(), [first, rest @ ..] => { tracing::warn!("ansi_escape_line: expected a single line, got {first:?} and {rest:?}"); first.clone() } } } pub fn ansi_escape(s: &str) -> Text<'static> { // to_text() claims to be faster, but introduces complex lifetime issues // such that it's not worth it. match s.into_text() { Ok(text) => text, Err(err) => match err { Error::NomError(message) => { tracing::error!( "ansi_to_tui NomError docs claim should never happen when parsing `{s}`: {message}" ); panic!(); } Error::Utf8Error(utf8error) => { tracing::error!("Utf8Error: {utf8error}"); panic!(); } }, } }