use crossterm::terminal;
use ratatui::prelude::Color;
use crate::tui::theme::Theme;
pub struct ExitSummary {
pub session_id: String,
pub tool_total: usize,
pub tool_success: usize,
pub tool_failure: usize,
pub wall_secs: u64,
pub total_tokens: u64,
pub api_calls: u32,
pub model_name: String,
pub theme: Theme,
}
pub fn print_goodbye(s: &ExitSummary) {
let term_width = terminal::size().map(|(w, _)| w as usize).unwrap_or(80);
let box_width = term_width.clamp(44, 72);
let inner = box_width - 2;
let c = ThemeAnsi::from(&s.theme);
let success_rate = if s.tool_total > 0 {
s.tool_success as f64 / s.tool_total as f64 * 100.0
} else {
0.0
};
let tokens_str = if s.total_tokens >= 1000 {
format!("{:.1}k", s.total_tokens as f64 / 1000.0)
} else {
s.total_tokens.to_string()
};
let session_short = if s.session_id.len() > 36 {
&s.session_id[..36]
} else {
&s.session_id
};
let top = format!(
"{border}╭{line}╮{r}",
border = c.border,
line = "─".repeat(inner),
r = c.reset,
);
let bot = format!(
"{border}╰{line}╯{r}",
border = c.border,
line = "─".repeat(inner),
r = c.reset,
);
println!();
println!("{top}");
row_text(
&c,
inner,
&format!(
"{}collet signing off — see you on the next run!{}",
c.accent, c.reset
),
);
row_blank(&c, inner);
row_section(&c, inner, "Interaction Summary");
row_kv(&c, inner, "Session ID:", session_short, &c.dim);
row_kv(
&c,
inner,
"Tool Calls:",
&format!(
"{} ( {}✓ {}{} {}✗ {}{} )",
s.tool_total, c.success, s.tool_success, c.reset, c.error, s.tool_failure, c.reset
),
&c.text,
);
row_kv(
&c,
inner,
"Success Rate:",
&format!("{:.1}%", success_rate),
&c.text,
);
row_blank(&c, inner);
row_section(&c, inner, "Performance");
row_kv(
&c,
inner,
"Wall Time:",
&format!("{:.1}s", s.wall_secs as f64),
&c.text,
);
row_kv(&c, inner, "Tokens Used:", &tokens_str, &c.text);
row_kv(&c, inner, "API Calls:", &s.api_calls.to_string(), &c.text);
row_kv(&c, inner, "Model:", &s.model_name, &c.text);
row_blank(&c, inner);
row_text(
&c,
inner,
&format!("{}Tip: Resume with: collet --continue{}", c.info, c.reset),
);
row_text(
&c,
inner,
&format!(
"{} or: collet --resume {}{}{}",
c.info, c.dim, session_short, c.reset
),
);
println!("{bot}");
println!();
}
fn row_blank(c: &ThemeAnsi, inner: usize) {
println!(
"{b}│{pad}│{r}",
b = c.border,
pad = " ".repeat(inner),
r = c.reset
);
}
fn row_text(c: &ThemeAnsi, inner: usize, text: &str) {
let vis = visible_width(text);
let pad = inner.saturating_sub(vis + 2);
println!(
"{b}│{r} {text}{pad} {b}│{r}",
b = c.border,
r = c.reset,
pad = " ".repeat(pad),
);
}
fn row_section(c: &ThemeAnsi, inner: usize, label: &str) {
let colored = format!(
"{bold}{accent}{label}{reset}",
bold = "\x1b[1m",
accent = c.accent,
reset = c.reset
);
let vis = visible_width(&colored);
let pad = inner.saturating_sub(vis + 2);
println!(
"{b}│{r} {colored}{pad} {b}│{r}",
b = c.border,
r = c.reset,
pad = " ".repeat(pad),
);
}
fn row_kv(c: &ThemeAnsi, inner: usize, key: &str, value: &str, val_color: &str) {
let key_width = 20;
let key_padded = format!("{:<width$}", key, width = key_width);
let line = format!(
"{dim}{key_padded}{reset}{vc}{value}{reset}",
dim = c.dim,
reset = c.reset,
vc = val_color,
);
let vis = visible_width(&line);
let pad = inner.saturating_sub(vis + 2);
println!(
"{b}│{r} {line}{pad} {b}│{r}",
b = c.border,
r = c.reset,
pad = " ".repeat(pad),
);
}
struct ThemeAnsi {
accent: String,
border: String,
text: String,
dim: String,
success: String,
error: String,
info: String,
reset: &'static str,
}
impl ThemeAnsi {
fn from(theme: &Theme) -> Self {
Self {
accent: color_to_ansi(theme.accent),
border: color_to_ansi(theme.border),
text: color_to_ansi(theme.text),
dim: color_to_ansi(theme.text_dim),
success: color_to_ansi(theme.success),
error: color_to_ansi(theme.error),
info: color_to_ansi(theme.info),
reset: "\x1b[0m",
}
}
}
fn color_to_ansi(c: Color) -> String {
match c {
Color::Rgb(r, g, b) => format!("\x1b[38;2;{r};{g};{b}m"),
Color::Black => "\x1b[30m".to_string(),
Color::Red => "\x1b[31m".to_string(),
Color::Green => "\x1b[32m".to_string(),
Color::Yellow => "\x1b[33m".to_string(),
Color::Blue => "\x1b[34m".to_string(),
Color::Magenta => "\x1b[35m".to_string(),
Color::Cyan => "\x1b[36m".to_string(),
Color::White => "\x1b[37m".to_string(),
Color::Gray => "\x1b[90m".to_string(),
Color::DarkGray => "\x1b[90m".to_string(),
Color::LightRed => "\x1b[91m".to_string(),
Color::LightGreen => "\x1b[92m".to_string(),
Color::LightYellow => "\x1b[93m".to_string(),
Color::LightBlue => "\x1b[94m".to_string(),
Color::LightMagenta => "\x1b[95m".to_string(),
Color::LightCyan => "\x1b[96m".to_string(),
_ => "\x1b[37m".to_string(), }
}
fn visible_width(s: &str) -> usize {
let mut width = 0;
let mut in_escape = false;
for ch in s.chars() {
if ch == '\x1b' {
in_escape = true;
} else if in_escape {
if ch.is_alphabetic() {
in_escape = false;
}
} else {
width += unicode_width::UnicodeWidthChar::width(ch).unwrap_or(0);
}
}
width
}