use chrono::{DateTime, Utc};
use costroid_core::{LimitAvailability, LimitKind, ProviderId, ProviderStatus, ProviderStatusKind};
use crate::severity::Constraint;
pub const LIMIT_FRESHNESS_STAMP_MINUTES: i64 = 10;
pub fn provider_label(provider: ProviderId) -> &'static str {
match provider {
ProviderId::ClaudeCode => "claude code",
ProviderId::Codex => "codex",
ProviderId::Cursor => "cursor",
}
}
pub fn kind_label(kind: LimitKind) -> &'static str {
match kind {
LimitKind::FiveHour => "5h",
LimitKind::Weekly => "wk",
LimitKind::Daily => "1d",
LimitKind::Monthly => "mo",
LimitKind::BillingCycle => "cyc",
}
}
pub fn percent(fraction: f64) -> String {
format!("{:.0}%", (fraction * 100.0).round())
}
pub fn provider_status_word(kind: ProviderStatusKind) -> &'static str {
match kind {
ProviderStatusKind::Available => "available",
ProviderStatusKind::Detected => "detected",
ProviderStatusKind::Partial => "partial",
ProviderStatusKind::Missing => "missing",
ProviderStatusKind::Error => "error",
}
}
pub fn provider_state_word(status: Option<&ProviderStatus>) -> &'static str {
match status {
Some(status) => provider_status_word(status.status),
None => "not detected",
}
}
pub fn reset_countdown(seconds: i64) -> String {
if seconds <= 0 {
return "<1m".to_string();
}
let minutes = seconds / 60;
if minutes < 1 {
"<1m".to_string()
} else if minutes < 60 {
format!("{minutes}m")
} else {
let hours = minutes / 60;
let remaining_minutes = minutes % 60;
if hours < 24 {
if remaining_minutes == 0 {
format!("{hours}h")
} else {
format!("{hours}h {remaining_minutes}m")
}
} else {
let days = hours / 24;
let remaining_hours = hours % 24;
if remaining_hours == 0 {
format!("{days}d")
} else {
format!("{days}d {remaining_hours}h")
}
}
}
}
pub fn as_of(captured_at: DateTime<Utc>) -> String {
if captured_at.timestamp() == 0 {
"capture time unknown".to_string()
} else {
format!("as of {}", captured_at.format("%H:%M"))
}
}
pub fn freshness_stamp(captured_at: DateTime<Utc>, generated_at: DateTime<Utc>) -> String {
if captured_at.timestamp() == 0 {
return "capture time unknown".to_string();
}
if (generated_at - captured_at).num_minutes() >= LIMIT_FRESHNESS_STAMP_MINUTES {
format!("as of {}", captured_at.format("%H:%M"))
} else {
String::new()
}
}
pub fn with_thousands(value: &str) -> String {
let (sign, digits) = value
.strip_prefix('-')
.map(|digits| ("-", digits))
.unwrap_or(("", value));
let mut reversed = String::new();
for (index, ch) in digits.chars().rev().enumerate() {
if index > 0 && index % 3 == 0 {
reversed.push(',');
}
reversed.push(ch);
}
let grouped: String = reversed.chars().rev().collect();
format!("{sign}{grouped}")
}
pub fn tooltip(constraint: Option<&Constraint>) -> String {
match constraint {
Some(c) => constraint_line(c),
None => "costroid — no live quota reading".to_string(),
}
}
pub fn constraint_line(constraint: &Constraint) -> String {
let limit = &constraint.limit;
let tool = provider_label(limit.tool);
let kind = kind_label(limit.kind);
let pct = percent(constraint.fraction);
let stamp = as_of(limit.captured_at);
match &limit.availability {
LimitAvailability::Available {
reset_in_seconds, ..
} => format!(
"{tool} {kind} — {pct} used · resets in {} · {stamp}",
reset_countdown(*reset_in_seconds)
),
_ => format!("{tool} {kind} — {pct} used · {stamp}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
use costroid_core::{LimitMeasure, LimitSummary};
fn ts(secs: i64) -> DateTime<Utc> {
match DateTime::from_timestamp(secs, 0) {
Some(dt) => dt,
None => panic!("invalid test timestamp {secs}"),
}
}
#[test]
fn percent_rounds_like_the_cli() {
assert_eq!(percent(0.0), "0%");
assert_eq!(percent(0.925), "93%");
assert_eq!(percent(1.0), "100%");
}
#[test]
fn reset_countdown_uses_compact_two_units() {
assert_eq!(reset_countdown(0), "<1m");
assert_eq!(reset_countdown(30), "<1m");
assert_eq!(reset_countdown(41 * 60), "41m");
assert_eq!(reset_countdown(2 * 3600 + 14 * 60), "2h 14m");
assert_eq!(reset_countdown(3 * 86400 + 4 * 3600), "3d 4h");
assert_eq!(reset_countdown(3 * 3600), "3h");
}
#[test]
fn freshness_stamp_only_past_the_threshold() {
let captured = ts(8 * 3600);
assert_eq!(freshness_stamp(captured, ts(8 * 3600 + 8 * 60)), "");
assert_eq!(
freshness_stamp(captured, ts(8 * 3600 + 12 * 60)),
"as of 08:00"
);
assert_eq!(freshness_stamp(ts(0), ts(8 * 3600)), "capture time unknown");
}
#[test]
fn with_thousands_groups_in_threes() {
assert_eq!(with_thousands("0"), "0");
assert_eq!(with_thousands("999"), "999");
assert_eq!(with_thousands("1000"), "1,000");
assert_eq!(with_thousands("1234567"), "1,234,567");
assert_eq!(with_thousands("-123"), "-123");
assert_eq!(with_thousands("-1234"), "-1,234");
}
#[test]
fn as_of_handles_real_and_sentinel_times() {
assert_eq!(as_of(ts(55_500)), "as of 15:25");
assert_eq!(as_of(ts(0)), "capture time unknown");
}
#[test]
fn tooltip_idle_is_honest() {
assert_eq!(tooltip(None), "costroid — no live quota reading");
}
#[test]
fn tooltip_constraint_reads_like_the_brand() {
let constraint = Constraint {
limit: LimitSummary {
tool: ProviderId::ClaudeCode,
plan: None,
kind: LimitKind::FiveHour,
label: None,
captured_at: ts(55_500), availability: LimitAvailability::Available {
measure: LimitMeasure::TokenFraction(0.92),
resets_at: ts(55_500 + 41 * 60),
reset_in_seconds: 41 * 60,
},
},
fraction: 0.92,
};
assert_eq!(
tooltip(Some(&constraint)),
"claude code 5h — 92% used · resets in 41m · as of 15:25"
);
}
}