pub mod role;
pub use role::Role;
pub mod verbosity;
pub use verbosity::{OutputFormat, Verbosity};
pub mod theme;
pub use theme::Theme;
pub mod component;
pub use component::{Component, KvPair};
pub mod renderer;
pub mod printer;
pub use printer::{DocCapture, Printer, PromptAnswer};
pub mod section_guard;
pub use section_guard::SectionGuard;
pub mod status_builder;
pub use status_builder::StatusBuilder;
pub mod spinner;
pub use spinner::{ProgressBar, Spinner};
pub mod process;
pub use process::CommandOutput;
pub mod prompts;
pub mod raw;
pub mod doc;
pub use doc::{Doc, SectionBuilder, StatusFields};
pub fn strip_ansi(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '\u{1b}' && chars.peek() == Some(&'[') {
chars.next(); for inner in chars.by_ref() {
if inner == 'm' {
break;
}
}
} else {
out.push(c);
}
}
out
}
pub fn collapse_to_subject_line(err: impl std::fmt::Display) -> String {
let s = err.to_string();
let mut lines = s.lines().filter(|l| !l.trim().is_empty());
let first = match lines.next() {
Some(line) => line.trim().to_string(),
None => return String::new(),
};
let mut out = first;
for line in lines {
out.push_str(" — ");
out.push_str(line.trim());
}
out
}
pub fn error_doc(
name: &str,
error_kind: &str,
message: impl Into<String>,
extras: serde_json::Value,
) -> Doc {
let mut payload = serde_json::json!({
"error": error_kind,
"name": name,
});
if let serde_json::Value::Object(extra_map) = extras
&& let serde_json::Value::Object(payload_map) = &mut payload
{
for (k, v) in extra_map {
payload_map.insert(k, v);
}
}
Doc::new().status(Role::Fail, message).with_data(payload)
}
pub mod render_doc;
pub mod structured;
#[cfg(feature = "test-helpers")]
pub mod test_capture;
#[cfg(test)]
pub(crate) mod test_support;
#[cfg(test)]
mod tests;
#[cfg(test)]
mod collapse_tests {
use super::collapse_to_subject_line;
#[test]
fn single_line_passes_through_trimmed() {
assert_eq!(collapse_to_subject_line("simple error"), "simple error");
assert_eq!(
collapse_to_subject_line(" padded "),
"padded",
"outer whitespace must be trimmed"
);
}
#[test]
fn multi_line_joined_with_em_dash() {
let input = "Transport endpoint is not connected\n\
See system logs and 'systemctl status kubelet.service' for details.";
assert_eq!(
collapse_to_subject_line(input),
"Transport endpoint is not connected — \
See system logs and 'systemctl status kubelet.service' for details."
);
}
#[test]
fn leading_and_trailing_blank_lines_skipped() {
let input = "\n\n \nfirst real line\nsecond real line\n \n\n";
assert_eq!(
collapse_to_subject_line(input),
"first real line — second real line"
);
}
#[test]
fn interior_blank_lines_skipped_and_inner_lines_trimmed() {
let input = " head \n\n \n\t body \t\n";
assert_eq!(collapse_to_subject_line(input), "head — body");
}
#[test]
fn empty_input_returns_empty() {
assert_eq!(collapse_to_subject_line(""), "");
assert_eq!(collapse_to_subject_line(" "), "");
assert_eq!(collapse_to_subject_line("\n\n\n"), "");
}
#[test]
fn display_impl_consumed_not_just_strings() {
let err = std::io::Error::other("first line\nsecond line");
assert_eq!(
collapse_to_subject_line(&err),
"first line — second line",
"any Display value (e.g. io::Error) must work"
);
}
}
#[cfg(test)]
mod strip_ansi_tests {
use super::strip_ansi;
#[test]
fn plain_text_passes_through_unchanged() {
assert_eq!(strip_ansi("hello world"), "hello world");
assert_eq!(strip_ansi(""), "");
assert_eq!(strip_ansi("✓ ✗ — →"), "✓ ✗ — →");
}
#[test]
fn red_sgr_pair_stripped() {
assert_eq!(strip_ansi("\x1b[31mred\x1b[0m"), "red");
}
#[test]
fn compound_sgr_stripped() {
assert_eq!(strip_ansi("\x1b[1;31mbold-red\x1b[0m"), "bold-red");
}
#[test]
fn prefix_and_suffix_preserved_around_stripped_sgr() {
assert_eq!(
strip_ansi("prefix\x1b[31mcolored\x1b[0msuffix"),
"prefixcoloredsuffix"
);
}
#[test]
fn incomplete_escape_swallowed_to_eos() {
assert_eq!(strip_ansi("safe\x1b[31"), "safe");
assert_eq!(strip_ansi("\x1b[31"), "");
}
#[test]
fn bare_escape_without_bracket_passes_through() {
assert_eq!(strip_ansi("a\x1bX"), "a\x1bX");
}
}