use std::io::IsTerminal;
use std::sync::LazyLock;
static COLOR_ENABLED: LazyLock<bool> = LazyLock::new(|| {
if std::env::var_os("NO_COLOR").is_some() {
return false;
}
if std::env::var("TERM").is_ok_and(|t| t == "dumb") {
return false;
}
std::io::stdout().is_terminal()
});
fn color_enabled() -> bool {
*COLOR_ENABLED
}
fn wrap(s: &str, code: &str) -> String {
if color_enabled() {
format!("\x1b[{code}m{s}\x1b[0m")
} else {
s.to_string()
}
}
pub fn bold(s: &str) -> String {
wrap(s, "1")
}
pub fn dim(s: &str) -> String {
wrap(s, "2")
}
pub fn cyan(s: &str) -> String {
wrap(s, "36")
}
pub fn green(s: &str) -> String {
wrap(s, "32")
}
pub fn yellow(s: &str) -> String {
wrap(s, "33")
}
pub fn magenta(s: &str) -> String {
wrap(s, "35")
}
pub fn blue(s: &str) -> String {
wrap(s, "34")
}
pub fn red(s: &str) -> String {
wrap(s, "31")
}
pub fn bold_cyan(s: &str) -> String {
wrap(s, "1;36")
}
pub fn bold_green(s: &str) -> String {
wrap(s, "1;32")
}
pub fn bold_white(s: &str) -> String {
wrap(s, "1;37")
}
pub fn bold_red(s: &str) -> String {
wrap(s, "1;31")
}
fn ansi_byte_count(s: &str) -> usize {
let bytes = s.as_bytes();
let mut i = 0;
let mut n = 0;
while i < bytes.len() {
if bytes[i] == 0x1b && i + 1 < bytes.len() && bytes[i + 1] == b'[' {
let start = i;
i += 2;
while i < bytes.len() && bytes[i] != b'm' {
i += 1;
}
if i < bytes.len() {
i += 1;
}
n += i - start;
} else {
i += 1;
}
}
n
}
pub fn visible_len(s: &str) -> usize {
s.chars().count() - ansi_byte_count(s)
}
pub fn pad_right(s: &str, width: usize) -> String {
let vl = visible_len(s);
if vl >= width {
s.to_string()
} else {
format!("{}{}", s, " ".repeat(width - vl))
}
}
pub fn pad_left(s: &str, width: usize) -> String {
let vl = visible_len(s);
if vl >= width {
s.to_string()
} else {
format!("{}{}", " ".repeat(width - vl), s)
}
}
pub fn bar(pct: f64, width: usize) -> String {
let p = pct.clamp(0.0, 1.0);
let filled = ((p * width as f64).round() as usize).min(width);
let empty = width - filled;
format!("{}{}", cyan(&"â–ˆ".repeat(filled)), dim(&"â–‘".repeat(empty)))
}
pub fn bar_threshold(pct: f64, width: usize) -> String {
let p = pct.clamp(0.0, 1.0);
let filled = ((p * width as f64).round() as usize).min(width);
let empty = width - filled;
let fill = "â–ˆ".repeat(filled);
let painted = if p >= 0.85 {
red(&fill)
} else if p >= 0.60 {
yellow(&fill)
} else {
green(&fill)
};
format!("{}{}", painted, dim(&"â–‘".repeat(empty)))
}
pub fn box_top(title: &str, inner_width: usize) -> String {
let total = inner_width + 4;
let dashes = total.saturating_sub(visible_len(title) + 5);
format!(
"{}{}{}",
dim("â•─ "),
bold_cyan(title),
dim(&format!(" {}╮", "─".repeat(dashes)))
)
}
pub fn box_mid(title: &str, inner_width: usize) -> String {
let total = inner_width + 4;
let dashes = total.saturating_sub(visible_len(title) + 5);
format!(
"{}{}{}",
dim("├─ "),
bold_cyan(title),
dim(&format!(" {}┤", "─".repeat(dashes)))
)
}
pub fn box_bottom(inner_width: usize) -> String {
dim(&format!("╰{}╯", "─".repeat(inner_width + 2)))
}
pub fn box_row(content: &str, inner_width: usize) -> String {
format!(
"{} {} {}",
dim("│"),
pad_right(content, inner_width),
dim("│")
)
}
pub fn box_blank(inner_width: usize) -> String {
box_row("", inner_width)
}
pub enum Align {
Left,
Right,
}
pub struct Table {
pub headers: Vec<String>,
pub aligns: Vec<Align>,
pub rows: Vec<Vec<String>>,
pub totals: Option<Vec<String>>,
}
impl Table {
pub fn new(headers: Vec<&str>, aligns: Vec<Align>) -> Self {
Self {
headers: headers.into_iter().map(String::from).collect(),
aligns,
rows: Vec::new(),
totals: None,
}
}
pub fn push(&mut self, row: Vec<String>) {
self.rows.push(row);
}
pub fn with_totals(&mut self, totals: Vec<String>) {
self.totals = Some(totals);
}
pub fn render(&self) -> String {
let cols = self.headers.len();
let mut widths = vec![0usize; cols];
for (i, h) in self.headers.iter().enumerate() {
widths[i] = widths[i].max(visible_len(h));
}
for row in &self.rows {
for (i, cell) in row.iter().enumerate() {
widths[i] = widths[i].max(visible_len(cell));
}
}
if let Some(t) = &self.totals {
for (i, cell) in t.iter().enumerate() {
if i < widths.len() {
widths[i] = widths[i].max(visible_len(cell));
}
}
}
let render_row = |cells: &[String], stylize: bool| -> String {
let mut out = String::new();
for (i, cell) in cells.iter().enumerate() {
let styled = if stylize {
bold_white(cell)
} else {
cell.clone()
};
let padded = match self.aligns.get(i).unwrap_or(&Align::Left) {
Align::Left => pad_right(&styled, widths[i]),
Align::Right => pad_left(&styled, widths[i]),
};
out.push_str(&padded);
if i + 1 < cells.len() {
out.push_str(" ");
}
}
out
};
let rule: Vec<String> = widths.iter().map(|w| dim(&"─".repeat(*w))).collect();
let mut out = String::new();
out.push_str(&render_row(&self.headers, true));
out.push('\n');
out.push_str(&render_row(&rule, false));
out.push('\n');
for row in &self.rows {
out.push_str(&render_row(row, false));
out.push('\n');
}
if let Some(t) = &self.totals {
out.push_str(&render_row(&rule, false));
out.push('\n');
let styled: Vec<String> = t.iter().map(|c| bold_green(c)).collect();
out.push_str(&render_row(&styled, false));
out.push('\n');
}
out
}
}
pub fn fmt_int(n: u64) -> String {
let s = n.to_string();
let mut out = String::with_capacity(s.len() + s.len() / 3);
for (i, c) in s.chars().rev().enumerate() {
if i > 0 && i % 3 == 0 {
out.push(',');
}
out.push(c);
}
out.chars().rev().collect()
}
pub fn fmt_compact(n: u64) -> String {
let f = n as f64;
if f < 1e3 {
n.to_string()
} else if f < 1e6 {
format!("{:.1}K", f / 1e3)
} else if f < 1e9 {
format!("{:.1}M", f / 1e6)
} else if f < 1e12 {
format!("{:.2}B", f / 1e9)
} else {
format!("{:.2}T", f / 1e12)
}
}
pub fn fmt_cost(v: f64) -> String {
let negative = v < 0.0;
let cents = (v.abs() * 100.0).round() as u64;
let whole = fmt_int(cents / 100);
let core = format!("${}.{:02}", whole, cents % 100);
if negative { format!("-{core}") } else { core }
}
pub fn truncate(s: &str, max: usize) -> String {
if s.chars().count() <= max {
s.to_string()
} else {
let head: String = s.chars().take(max.saturating_sub(1)).collect();
format!("{head}…")
}
}
pub fn short_model(s: &str) -> String {
let s = s.strip_prefix("claude-").unwrap_or(s);
let without_date = match s.rsplit_once('-') {
Some((head, tail)) if tail.len() == 8 && tail.chars().all(|c| c.is_ascii_digit()) => head,
_ => s,
};
if let Some((head, tail)) = without_date.rsplit_once('-') {
let digit =
|x: &str| !x.is_empty() && x.len() <= 2 && x.chars().all(|c| c.is_ascii_digit());
if digit(tail) {
if let Some((_, major)) = head.rsplit_once('-') {
if digit(major) {
return format!("{head}.{tail}");
}
}
}
}
without_date.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn short_model_anthropic_modern() {
assert_eq!(short_model("claude-opus-4-7"), "opus-4.7");
assert_eq!(short_model("claude-haiku-4-5"), "haiku-4.5");
assert_eq!(short_model("claude-sonnet-4-6"), "sonnet-4.6");
}
#[test]
fn short_model_strips_date_suffix() {
assert_eq!(short_model("claude-haiku-4-5-20251001"), "haiku-4.5");
assert_eq!(short_model("claude-sonnet-4-5-20250929"), "sonnet-4.5");
}
#[test]
fn short_model_leaves_non_versioned_ids_alone() {
assert_eq!(short_model("gpt-5.3-codex"), "gpt-5.3-codex");
assert_eq!(short_model("gpt-5.4"), "gpt-5.4");
assert_eq!(short_model("<synthetic>"), "<synthetic>");
assert_eq!(short_model("__unknown__"), "__unknown__");
}
#[test]
fn fmt_int_adds_thousands_separators() {
assert_eq!(fmt_int(0), "0");
assert_eq!(fmt_int(42), "42");
assert_eq!(fmt_int(1_000), "1,000");
assert_eq!(fmt_int(1_234_567), "1,234,567");
}
#[test]
fn fmt_compact_buckets() {
assert_eq!(fmt_compact(0), "0");
assert_eq!(fmt_compact(999), "999");
assert_eq!(fmt_compact(1_500), "1.5K");
assert_eq!(fmt_compact(2_500_000), "2.5M");
assert_eq!(fmt_compact(8_030_000_000), "8.03B");
}
#[test]
fn fmt_cost_rounds_and_separates() {
assert_eq!(fmt_cost(0.0), "$0.00");
assert_eq!(fmt_cost(0.005), "$0.01");
assert_eq!(fmt_cost(1_234.5), "$1,234.50");
assert_eq!(fmt_cost(-42.1), "-$42.10");
}
#[test]
fn visible_len_ignores_ansi() {
let plain = "hello";
let painted = "\x1b[1;32mhello\x1b[0m";
assert_eq!(visible_len(plain), 5);
assert_eq!(visible_len(painted), 5);
}
}