use std::env;
use std::io::{self, IsTerminal};
const BANNER_WIDTH: usize = 38;
pub fn use_terminal_style() -> bool {
io::stdout().is_terminal()
&& env::var_os("NO_COLOR").is_none()
&& env::var("TERM").map_or(true, |term| term != "dumb")
}
pub fn muted(text: &str) -> String {
if use_terminal_style() {
format!("\x1b[2m{text}\x1b[0m")
} else {
text.to_string()
}
}
pub fn color_status_text(status: &str, text: &str) -> String {
if use_terminal_style() {
let color = match status.to_lowercase().as_str() {
"pass" => "\x1b[32m", "warn" => "\x1b[33m", "fail" => "\x1b[31m", "skipped" | "skip" => "\x1b[36m", "info" => "\x1b[34m", _ => "",
};
if color.is_empty() {
text.to_string()
} else {
format!("{color}{text}\x1b[0m")
}
} else {
text.to_string()
}
}
pub fn colored_status_icon(status: &str) -> String {
let icon = status_icon(status);
color_status_text(status, icon)
}
pub fn colored_status_label(status: &str) -> String {
color_status_text(status, status)
}
pub fn status_icon(status: &str) -> &'static str {
match status {
"pass" => "✓",
"fail" => "✕",
"warn" => "⚠",
"skipped" | "skip" => "◌",
_ => "◌",
}
}
pub fn status_signal(status: &str, has_invalid: bool, has_missing: bool) -> &'static str {
match status {
"pass" => {
if has_invalid {
"invalid"
} else if has_missing {
"missing"
} else {
"clean"
}
}
"warn" => "attention",
"fail" => "blocked",
"skipped" | "skip" => "skipped",
_ => "unknown",
}
}
pub fn banner(title: &str) {
let header = format!("─ SDD {title} ");
let dashes = BANNER_WIDTH.saturating_sub(header.chars().count());
println!("╭{header}{}", "─".repeat(dashes));
}
pub fn banner_close() {
println!("╰{}", "─".repeat(BANNER_WIDTH));
}
pub fn status_line(status: &str, signal: &str) {
let icon = status_icon(status);
let upper = status.to_uppercase();
if use_terminal_style() {
let colored_icon = colored_status_icon(status);
let colored_upper = colored_status_label(&upper);
let colored_signal = color_status_text(status, signal);
println!("│ {colored_icon} {colored_upper:<4} {colored_signal}");
} else {
println!("│ {icon} {upper:<4} {signal}");
}
}
pub fn summary_line(label: &str, body: &str) {
println!("│ ◇ {label:<10} {body}");
}
pub fn section(title: &str, count: usize) {
println!();
println!("◇ {title} ({count})");
}
pub fn section_none() {
println!(" ✓ none");
}
pub fn item(icon: &str, text: &str) {
println!(" {icon} {text}");
}
#[allow(dead_code)]
pub fn item_labeled(icon: &str, id: &str, width: usize, body: &str) {
println!(" {icon} {id:<width$} {body}");
}
pub fn tree(details: &[String], preview_limit: Option<usize>) {
let visible = preview_limit
.map(|limit| details.len().min(limit))
.unwrap_or(details.len());
let hidden = details.len().saturating_sub(visible);
let total_lines = visible + usize::from(hidden > 0);
for (index, detail) in details.iter().take(visible).enumerate() {
let branch = if index + 1 == total_lines {
"└─"
} else {
"├─"
};
println!(" {branch} {detail}");
}
if hidden > 0 {
println!(" └─ +{hidden} more");
}
}
pub fn plural<'a>(count: usize, singular: &'a str, plural_str: &'a str) -> &'a str {
if count == 1 {
singular
} else {
plural_str
}
}
#[derive(Clone, Copy)]
pub enum RailMarker {
Hollow,
Filled,
}
impl RailMarker {
pub fn symbol(self) -> &'static str {
match self {
RailMarker::Hollow => "◇",
RailMarker::Filled => "◆",
}
}
}
pub fn rail_blank() {
println!("│");
}
pub fn rail_detail(detail: &str) {
println!("│ {}", muted(detail));
}
pub fn rail_node(marker: RailMarker, label: &str, detail: Option<&str>) {
println!("{} {label}", marker.symbol());
if let Some(detail) = detail {
rail_detail(detail);
}
rail_blank();
}
pub fn quick_start(steps: &[String], done: &str) {
println!("◇ Quick start ─────────────");
rail_blank();
for step in steps {
rail_detail(step);
}
rail_blank();
println!("└ {done}");
}