pub const RESET: &str = "\x1b[0m";
const BOLD: &str = "\x1b[1m";
pub const DIM: &str = "\x1b[2m";
pub const ITALIC: &str = "\x1b[3m";
#[allow(dead_code)]
const UNDERLINE: &str = "\x1b[4m";
const CLEAR_LINE: &str = "\x1b[2K\r";
pub const GREEN: &str = "\x1b[38;5;77m"; const DARK_GREEN: &str = "\x1b[38;5;28m";
const TEAL: &str = "\x1b[38;5;37m"; const AMBER: &str = "\x1b[38;5;214m"; const ROSE: &str = "\x1b[38;5;204m"; const SKY: &str = "\x1b[38;5;117m"; const SILVER: &str = "\x1b[38;5;252m"; const LAVENDER: &str = "\x1b[38;5;183m"; const PEACH: &str = "\x1b[38;5;216m"; const RED: &str = "\x1b[38;5;203m";
const WHITE: &str = "\x1b[38;5;255m";
pub fn print_banner() {
let version = env!("CARGO_PKG_VERSION");
let banner = format!(
r#"
{g} ┌───┐ ╻ {dg}╻ ╻ {t}┏━━━┓
{g} │ │ ┃ {dg}┃ ┃ {t}┃ ┃ {a}🌿 {w}C L A W G A R D E N
{g} │ │ ┃ {dg}┃ ┃ {t}┃ ┃ {dim}Multi-Agent Garden · v{ver}
{g} └───┘ ╹ {dg}╹ ╹ {t}┗━━━┛
{reset}"#,
g = GREEN,
dg = DARK_GREEN,
t = TEAL,
a = AMBER,
w = WHITE,
dim = DIM,
ver = version,
reset = RESET,
);
print!("{banner}");
}
pub fn section_header(step: usize, _total: usize, icon: &str, title: &str) {
let inner = format!(" {} Step {} · {}", icon, step, title);
let width = strip_ansi(&inner).len().max(42);
let top = "┌".to_string() + &"─".repeat(width + 2) + "┐";
let bot = "└".to_string() + &"─".repeat(width + 2) + "┘";
let mid = format!(
"│ {}{}│",
inner,
" ".repeat(width - strip_ansi(&inner).len() + 1)
);
println!("\n{teal}{top}", teal = TEAL);
println!("{green}{mid}", green = GREEN);
println!("{teal}{bot}{reset}", teal = TEAL, reset = RESET);
println!();
}
pub fn section_header_no_step(icon: &str, title: &str) {
let inner = format!(" {} {}", icon, title);
let width = strip_ansi(&inner).len().max(42);
let top = "┌".to_string() + &"─".repeat(width + 2) + "┐";
let bot = "└".to_string() + &"─".repeat(width + 2) + "┘";
let mid = format!(
"│ {}{}│",
inner,
" ".repeat(width - strip_ansi(&inner).len() + 1)
);
println!("\n{teal}{top}", teal = TEAL);
println!("{green}{mid}", green = GREEN);
println!("{teal}{bot}{reset}", teal = TEAL, reset = RESET);
println!();
}
pub fn progress_bar(current: usize, total: usize) {
let width = 30;
let filled = if total > 0 {
(current * width) / total
} else {
0
};
let empty = width - filled;
let bar: String = "█".repeat(filled);
let bg: String = "░".repeat(empty);
let pct = if total > 0 {
(current * 100) / total
} else {
0
};
println!(
"{dim} {bar}{bg} {pct}% ({cur}/{tot}){reset}",
dim = DIM,
bar = format!("{green}{bar}{reset}", green = GREEN, reset = RESET),
bg = format!("{dark}{bg}{reset}", dark = "\x1b[38;5;236m", reset = RESET),
pct = pct,
cur = current,
tot = total,
reset = RESET,
);
println!();
}
pub fn hint(text: &str) {
println!(
" {dim}{italic}{text}{reset}",
dim = DIM,
italic = ITALIC,
text = text,
reset = RESET
);
}
pub fn tip(text: &str) {
println!(
" {amber}💡 Tip:{reset} {dim}{text}{reset}",
amber = AMBER,
dim = DIM,
text = text,
reset = RESET
);
}
pub fn success(text: &str) {
println!(
" {green}✔{reset} {text}",
green = GREEN,
reset = RESET,
text = text
);
}
pub fn warn(text: &str) {
println!(
" {amber}⚠{reset} {text}",
amber = AMBER,
reset = RESET,
text = text
);
}
pub fn error(text: &str) {
println!(
" {red}✘{reset} {text}",
red = RED,
reset = RESET,
text = text
);
}
pub fn divider() {
println!(" {dim}{}{reset}", "─".repeat(50), dim = DIM, reset = RESET);
}
#[allow(dead_code)]
pub fn kv(indent: usize, key: &str, value: &str) {
let pad = " ".repeat(indent);
println!(
"{pad}{dim}{key}:{reset} {value}",
pad = pad,
dim = DIM,
key = key,
reset = RESET,
value = value,
);
}
pub fn flower_separator() {
println!(
"\n {dg}· • {g}✿ {dg}• · {dim}{} {dg}· • {g}✿ {dg}• ·{reset}",
"─".repeat(26),
dg = DARK_GREEN,
g = GREEN,
dim = DIM,
reset = RESET,
);
println!();
}
pub fn summary_box(title: &str, rows: &[(String, String, String)]) {
let label_width = rows.iter().map(|r| r.1.len()).max().unwrap_or(10).max(10);
let detail_display_width = rows
.iter()
.map(|r| strip_ansi(&r.2).len())
.max()
.unwrap_or(10);
let inner = label_width + detail_display_width + 8;
let full = inner.max(strip_ansi(title).len() + 4);
let top = format!(" ╔{}╗", "═".repeat(full + 2));
let bot = format!(" ╚{}╝", "═".repeat(full + 2));
let title_line = format!(
" ║ {bold}{t}{}{reset} ║",
title,
bold = BOLD,
t = WHITE,
reset = RESET,
);
let title_inner_len = strip_ansi(&title_line).len();
let title_pad = if (full + 4) > title_inner_len {
" ".repeat(full + 4 - title_inner_len)
} else {
"".to_string()
};
let title_line = title_line.replace(" ║", &format!("{}║", title_pad));
println!("\n{teal}{top}", teal = TEAL);
println!("{title_line}");
println!(" ╠{}╣", "─".repeat(full + 2));
for (icon, label, detail) in rows {
let detail_visible = strip_ansi(detail).len();
let pad = full.saturating_sub(label_width + detail_visible + 4);
println!(
" ║ {icon} {dim}{label:<lw$}{reset} {detail}{pad_spaces}║",
icon = icon,
dim = DIM,
label = label,
lw = label_width,
reset = RESET,
detail = detail,
pad_spaces = " ".repeat(pad),
);
}
println!("{teal}{bot}{reset}", teal = TEAL, reset = RESET);
}
pub fn celebration(garden_name: &str) {
let name_display = format!("{w}{garden_name}{g}", w = WHITE, g = GREEN);
println!();
println!(
"{grn}{bld}
🌿 · · · · · · · · · · · · · · · · · 🌿
║ ║
║ 🎉 Garden Created! 🎉 ║
║ ║
║ '{name}' is ready to bloom. ║
║ ║
🌿 · · · · · · · · · · · · · · · · · 🌿
{reset}",
grn = GREEN,
bld = BOLD,
name = name_display,
reset = RESET,
);
}
pub fn next_steps(garden_name: &str) {
println!(
"\n {bold}{sky}▶ Next Steps{reset}",
bold = BOLD,
sky = SKY,
reset = RESET
);
println!();
println!(
" {dim}1.{reset} Start your garden:",
dim = DIM,
reset = RESET
);
println!(
" {green}garden up --name {name}{reset}",
green = GREEN,
name = garden_name,
reset = RESET
);
println!();
println!(
" {dim}2.{reset} Check if it's healthy:",
dim = DIM,
reset = RESET
);
println!(
" {green}garden logs --name {name} --follow{reset}",
green = GREEN,
name = garden_name,
reset = RESET
);
println!();
println!(
" {dim}3.{reset} Modify configuration:",
dim = DIM,
reset = RESET
);
println!(
" {green}garden config --name {name}{reset}",
green = GREEN,
name = garden_name,
reset = RESET
);
println!();
println!(
" {dim}4.{reset} Add more agents later:",
dim = DIM,
reset = RESET
);
println!(
" {green}garden config --name {gname}{reset} {dim}→ Add an agent{reset}",
green = GREEN,
gname = garden_name,
dim = DIM,
reset = RESET
);
println!();
}
pub fn spinner(text: &str, ms: u64) {
let frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
let iterations = (ms / 80).max(1);
for (i, frame) in frames.iter().cycle().take(iterations as usize).enumerate() {
if i > 0 {
eprint!("{}", CLEAR_LINE);
}
eprint!(
" {green}{frame} {text}{reset}",
green = GREEN,
frame = frame,
text = text,
reset = RESET
);
std::thread::sleep(std::time::Duration::from_millis(80));
}
eprint!("{}\n", CLEAR_LINE);
}
#[allow(dead_code)]
pub fn typewriter(text: &str, ms_per_char: u64) {
for ch in text.chars() {
eprint!("{}", ch);
std::thread::sleep(std::time::Duration::from_millis(ms_per_char));
}
eprint!("\n");
}
fn strip_ansi(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut in_escape = false;
for ch in s.chars() {
if ch == '\x1b' {
in_escape = true;
} else if in_escape {
if ch.is_ascii_alphabetic() {
in_escape = false;
}
} else {
out.push(ch);
}
}
out
}
pub fn role_badge(role: &str) -> String {
let color = match role.to_uppercase().as_str() {
"PM" => AMBER,
"DEV" => GREEN,
"CRITIC" => ROSE,
"DESIGNER" => LAVENDER,
"RESEARCHER" => SKY,
"TESTER" => TEAL,
"OPS" => PEACH,
"ANALYST" => SILVER,
_ => WHITE,
};
format!(
"{color}[{role}]{reset}",
color = color,
role = role,
reset = RESET
)
}
#[allow(dead_code)]
pub fn role_description(role: &str) -> &'static str {
match role.to_uppercase().as_str() {
"PM" => "Coordinates tasks & keeps the team on track",
"DEV" => "Writes and reviews code, implements features",
"CRITIC" => "Reviews output, catches issues & blind spots",
"DESIGNER" => "UI/UX design, system architecture thinking",
"RESEARCHER" => "Investigates, documents, and gathers context",
"TESTER" => "Quality assurance, edge-case explorer",
"OPS" => "Deployment, DevOps, infrastructure management",
"ANALYST" => "Data analysis, metrics, insights",
"OTHER" => "Custom role — define your own specialty",
_ => "Unknown role",
}
}