//! Design system de terminal do CLI SDD.
//!
//! Convenções visuais:
//! - **Banner em caixa** (`╭─ SDD <title> ──… ╰──`) abre comandos de inspeção
//! (`doctor`, `health`, `list`) e concentra status + contagens.
//! - **Rail de timeline** (`◇`/`◆` + `│`) orienta comandos de execução
//! (`init`, `sync`, `save`).
//! - **Seções** (`◇ Title (n)`) agrupam detalhamento com árvore `├─└─`.
//! - **Quick start** fecha comandos longos com próximos passos.
//!
//! Todas as funções respeitam `NO_COLOR`, `TERM=dumb` e redirecionamento
//! (não-TTY): quando o estilo está desativado, os glifos continuam (são
//! legíveis em pipe) mas o dim ANSI é suprimido.
use std::io::{self, IsTerminal};
use std::env;
/// Largura interna do banner (conta os `─` entre `╭`/`╰` e o conteúdo).
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()
}
}
/// `✓` para pass, `✕` para fail, `⚠` para warn, `◌` para skipped/unknown.
pub fn status_icon(status: &str) -> &'static str {
match status {
"pass" => "✓",
"fail" => "✕",
"warn" => "⚠",
"skipped" | "skip" => "◌",
_ => "◌",
}
}
/// Sinal humano para a linha de status: `clean`, `attention`, `blocked`,
/// `invalid`, `missing`, `skipped`.
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",
}
}
// ── Banner em caixa ──────────────────────────────────────────────────────
/// Abre `╭─ SDD <title> ────` com largura fixa.
pub fn banner(title: &str) {
let header = format!("─ SDD {title} ");
let dashes = BANNER_WIDTH.saturating_sub(header.chars().count());
println!("╭{header}{}", "─".repeat(dashes));
}
/// Fecha o banner: `╰──────`.
pub fn banner_close() {
println!("╰{}", "─".repeat(BANNER_WIDTH));
}
/// Linha de status dentro do banner: `│ ✓ PASS clean`.
pub fn status_line(status: &str, signal: &str) {
let icon = status_icon(status);
let upper = status.to_uppercase();
println!("│ {icon} {upper:<4} {signal}");
}
/// Linha de resumo dentro do banner: `│ ◇ Label body`.
pub fn summary_line(label: &str, body: &str) {
println!("│ ◇ {label:<10} {body}");
}
// ── Seções e árvore ──────────────────────────────────────────────────────
/// Abre uma seção de detalhamento: linha em branco + `◇ Title (count)`.
pub fn section(title: &str, count: usize) {
println!();
println!("◇ {title} ({count})");
}
/// Estado vazio de uma seção: ` ✓ none`.
pub fn section_none() {
println!(" ✓ none");
}
/// Item de seção: ` {icon} {text}`.
pub fn item(icon: &str, text: &str) {
println!(" {icon} {text}");
}
/// Item de seção com identificador alinhado: ` {icon} {id:<width} {body}`.
pub fn item_labeled(icon: &str, id: &str, width: usize, body: &str) {
println!(" {icon} {id:<width$} {body}");
}
/// Detalhes em árvore (`├─`/`└─`) com preview opcional e linha `+N more`.
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");
}
}
/// Pluralização simples: 1 → singular, demais → plural.
pub fn plural<'a>(count: usize, singular: &'a str, plural_str: &'a str) -> &'a str {
if count == 1 {
singular
} else {
plural_str
}
}
// ── Rail de timeline (execução) ──────────────────────────────────────────
#[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();
}
// ── Quick start ──────────────────────────────────────────────────────────
/// Fecha comandos longos com próximos passos.
pub fn quick_start(steps: &[String], done: &str) {
println!("◇ Quick start ─────────────");
rail_blank();
for step in steps {
rail_detail(step);
}
rail_blank();
println!("└ {done}");
}