use crate::model::{Snapshot, Status, Window};
use chrono::{DateTime, Utc};
use std::collections::HashMap;
const BAR_WIDTH: usize = 20;
const GREEN: &str = "\x1b[32m";
const YELLOW: &str = "\x1b[33m";
const RED: &str = "\x1b[31m";
const RESET: &str = "\x1b[0m";
enum Entry {
Table {
header: String,
windows: Vec<Window>,
},
Line(String),
}
pub fn render(snapshots: &[Snapshot], color: bool) -> String {
let now = Utc::now();
let mut provider_counts: HashMap<&str, usize> = HashMap::new();
for s in snapshots {
if s.status != Status::NotDetected {
*provider_counts.entry(s.provider.as_str()).or_insert(0) += 1;
}
}
let mut entries: Vec<Entry> = Vec::new();
for s in snapshots {
match s.status {
Status::NotDetected => continue,
Status::AuthMissing => {
entries.push(Entry::Line(format!(
"{} \u{26A0} not logged in (run: {})",
s.provider, s.provider
)));
}
Status::Error => {
let msg = s.error.as_deref().unwrap_or("error");
entries.push(Entry::Line(format!("{} \u{2717} {}", s.provider, msg)));
}
Status::Ok | Status::Stale => {
if s.windows.is_empty() {
entries.push(Entry::Line(format!("{} (no usage data)", s.provider)));
continue;
}
let multi = provider_counts
.get(s.provider.as_str())
.copied()
.unwrap_or(0)
> 1;
let mut header = s.provider.clone();
if let Some(plan) = &s.plan {
header.push_str(" · ");
header.push_str(plan);
}
if multi {
let label = s.label.as_deref().unwrap_or(s.account.as_str());
header.push(' ');
header.push_str(label);
}
if s.status == Status::Stale {
header.push_str(&format!(" (stale {})", format_duration(now - s.fetched_at)));
}
entries.push(Entry::Table {
header,
windows: s.windows.clone(),
});
}
}
}
let mut header_width = 0usize;
let mut key_width = 0usize;
let mut pct_width = 0usize;
let mut count_width = 0usize;
for e in &entries {
if let Entry::Table { header, windows } = e {
header_width = header_width.max(header.chars().count());
for w in windows {
key_width = key_width.max(w.key.chars().count());
pct_width = pct_width.max(format_pct(w.used_pct).chars().count());
if let Some(count) = format_count(w) {
count_width = count_width.max(count.chars().count());
}
}
}
}
let mut lines: Vec<String> = Vec::new();
for e in entries {
match e {
Entry::Line(l) => lines.push(l),
Entry::Table { header, windows } => {
let mut first = true;
for w in windows {
let header_cell = if first {
format!("{:<hw$}", header, hw = header_width)
} else {
format!("{:<hw$}", "", hw = header_width)
};
first = false;
let key_cell = format!("{:<kw$}", w.key, kw = key_width);
let bar = render_bar(w.used_pct);
let pct_cell = format!("{:>pw$}", format_pct(w.used_pct), pw = pct_width);
let (bar_out, pct_out) = if color {
let c = color_for(w.used_pct);
(format!("{c}{bar}{RESET}"), format!("{c}{pct_cell}{RESET}"))
} else {
(bar, pct_cell)
};
let mut line = format!("{header_cell} {key_cell} {bar_out} {pct_out}");
if count_width > 0 {
let count = format_count(&w).unwrap_or_default();
line.push_str(&format!(" {count:<count_width$}"));
}
if let Some(resets) = format_resets(w.resets_at, now) {
line.push_str(" ");
line.push_str(&resets);
}
lines.push(line.trim_end().to_string());
}
}
}
}
lines.join("\n")
}
fn render_bar(used_pct: f64) -> String {
let clamped = used_pct.clamp(0.0, 100.0);
let filled = ((clamped / 100.0) * BAR_WIDTH as f64).round() as usize;
let filled = filled.min(BAR_WIDTH);
let empty = BAR_WIDTH - filled;
format!("{}{}", "█".repeat(filled), "░".repeat(empty))
}
fn format_count(w: &Window) -> Option<String> {
match (w.used, w.limit) {
(Some(u), Some(l)) => Some(format!("{}/{}", u as i64, l as i64)),
_ => None,
}
}
fn format_pct(pct: f64) -> String {
let rounded = (pct * 10.0).round() / 10.0;
if rounded.fract().abs() < 1e-9 {
format!("{}%", rounded as i64)
} else {
format!("{rounded:.1}%")
}
}
fn color_for(pct: f64) -> &'static str {
if pct >= 90.0 {
RED
} else if pct >= 70.0 {
YELLOW
} else {
GREEN
}
}
fn format_resets(resets_at: Option<DateTime<Utc>>, now: DateTime<Utc>) -> Option<String> {
let at = resets_at?;
let dur = at - now;
if dur <= chrono::Duration::zero() {
return None;
}
Some(format!("resets {}", format_duration(dur)))
}
fn format_duration(dur: chrono::Duration) -> String {
let total_secs = dur.num_seconds().max(0);
let days = total_secs / 86400;
let hours = (total_secs % 86400) / 3600;
let minutes = (total_secs % 3600) / 60;
if days >= 1 {
format!("{days}d {hours}h")
} else if hours >= 1 {
format!("{hours}h {minutes}m")
} else if minutes >= 1 {
format!("{minutes}m")
} else {
"<1m".to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{Unit, WindowKind};
fn window(key: &str, used_pct: f64, resets_at: Option<DateTime<Utc>>) -> Window {
Window {
key: key.to_string(),
kind: WindowKind::Rolling,
unit: Unit::Percent,
used_pct,
used: None,
limit: None,
unlimited: false,
resets_at,
}
}
fn ok_snapshot() -> Snapshot {
Snapshot {
provider: "claude".to_string(),
account: "default".to_string(),
label: None,
plan: Some("max".to_string()),
windows: vec![
window("5h", 19.0, Some(Utc::now() + chrono::Duration::hours(2))),
window("7d", 95.0, None),
],
fetched_at: Utc::now(),
status: Status::Ok,
error: None,
raw: None,
}
}
fn count_snapshot() -> Snapshot {
Snapshot {
provider: "copilot".to_string(),
account: "default".to_string(),
label: None,
plan: Some("enterprise".to_string()),
windows: vec![Window {
key: "monthly".to_string(),
kind: WindowKind::Calendar,
unit: Unit::Requests,
used_pct: 11.0,
used: Some(2729.0),
limit: Some(25000.0),
unlimited: false,
resets_at: None,
}],
fetched_at: Utc::now(),
status: Status::Ok,
error: None,
raw: None,
}
}
fn auth_missing_snapshot() -> Snapshot {
Snapshot {
provider: "codex".to_string(),
account: "default".to_string(),
label: None,
plan: None,
windows: vec![],
fetched_at: Utc::now(),
status: Status::AuthMissing,
error: None,
raw: None,
}
}
#[test]
fn render_plain_contains_expected_fragments() {
let snapshots = vec![ok_snapshot(), auth_missing_snapshot()];
let out = render(&snapshots, false);
assert!(out.contains("5h"), "missing window key: {out}");
assert!(out.contains('%'), "missing percent figure: {out}");
assert!(
out.contains("not logged in (run: codex)"),
"missing auth line: {out}"
);
assert!(!out.contains('\u{1b}'), "unexpected ANSI escape: {out}");
}
#[test]
fn render_color_emits_ansi_for_high_usage() {
let snapshots = vec![ok_snapshot(), auth_missing_snapshot()];
let out = render(&snapshots, true);
assert!(out.contains("\u{1b}[3"), "missing ANSI color escape: {out}");
}
#[test]
fn count_window_shows_used_over_limit() {
let out = render(&[count_snapshot()], false);
assert!(out.contains("2729/25000"), "missing count column: {out}");
}
#[test]
fn percent_only_rows_have_no_trailing_whitespace_with_counts() {
let out = render(&[ok_snapshot(), count_snapshot()], false);
for line in out.lines() {
assert_eq!(line, line.trim_end(), "trailing whitespace: {line:?}");
}
}
}