use inquire::ui::{Attributes, Color, RenderConfig, StyleSheet, Styled};
use crate::profile::Profile;
use crate::usage::{
FetchStatus, PlanInfo, UsageInfo, UsageWindow, iso_to_epoch_secs, now_epoch_secs,
};
pub(crate) const C_RESET: &str = "\x1b[0m";
pub(crate) const C_BOLD: &str = "\x1b[1m";
pub(crate) const C_NOBOLD: &str = "\x1b[22m"; pub(crate) const C_FG_OFF: &str = "\x1b[39m"; pub(crate) const C_ACCENT: &str = "\x1b[38;2;67;171;229m"; pub(crate) const C_ORANGE: &str = "\x1b[38;2;217;119;87m"; pub(crate) const C_WARNING: &str = "\x1b[38;2;249;226;175m";
pub(crate) const C_DANGER: &str = "\x1b[38;2;243;139;168m";
pub(crate) const C_DIM: &str = "\x1b[38;2;166;173;200m";
pub(crate) const C_FAINT: &str = "\x1b[38;2;127;132;156m";
pub(crate) const C_UL_WARNING: &str = "\x1b[4;58:2::249:226:175m";
pub(crate) const C_UL_DANGER: &str = "\x1b[4;58:2::243:139:168m";
pub(crate) const C_UL_OFF: &str = "\x1b[24;59m";
fn titlecase_words(s: &str) -> String {
s.split(['_', ' '])
.filter(|w| !w.is_empty())
.map(|w| {
let mut chars = w.chars();
match chars.next() {
None => String::new(),
Some(f) => f.to_uppercase().collect::<String>() + chars.as_str(),
}
})
.collect::<Vec<_>>()
.join(" ")
}
fn max_multiplier(tier: &str) -> Option<&str> {
let last = tier.rsplit('_').next()?;
last.strip_suffix('x')
.filter(|m| m.chars().all(|c| c.is_ascii_digit()))
}
fn plan_label(plan: &PlanInfo) -> String {
let org = plan.organization_type.as_deref().unwrap_or("");
let base = match org {
"claude_max" => "Max".to_string(),
"claude_pro" => "Pro".to_string(),
"claude_team" | "claude_teams" => "Team".to_string(),
"claude_enterprise" => "Enterprise".to_string(),
"claude_free" | "free" => "Free".to_string(),
"" => {
if plan.has_max {
"Max".to_string()
} else if plan.has_pro {
"Pro".to_string()
} else {
return "Claude".to_string();
}
}
other => titlecase_words(other.strip_prefix("claude_").unwrap_or(other)),
};
if base == "Max"
&& let Some(tier) = plan.rate_limit_tier.as_deref()
&& let Some(mult) = max_multiplier(tier)
{
return format!("Claude Max {mult}x");
}
format!("Claude {base}")
}
fn endpoint_label(profile: &Profile) -> String {
if let Some(url) = &profile.base_url {
return url.clone();
}
if let Some(plan) = profile.usage.as_ref().and_then(|u| u.plan.as_ref()) {
return plan_label(plan);
}
let sub = profile
.credentials
.as_ref()
.and_then(|c| c.claude_ai_oauth.as_ref())
.and_then(|o| o.subscription_type.as_deref())
.unwrap_or("pro");
format!("Claude {}", titlecase_words(sub))
}
fn format_reset(window: &UsageWindow) -> Option<String> {
let resets_at = window.resets_at.as_deref()?;
let target = iso_to_epoch_secs(resets_at)?;
let secs = target - now_epoch_secs();
Some(humanize_duration(secs))
}
fn humanize_duration(secs: i64) -> String {
if secs <= 0 {
return "now".to_string();
}
let mins = secs / 60;
let hours = mins / 60;
let days = hours / 24;
if days > 0 {
format!("{}d {}h", days, hours % 24)
} else if hours > 0 {
format!("{}h {}m", hours, mins % 60)
} else {
format!("{}m", mins.max(1))
}
}
fn bar_for(pct: f64) -> (String, &'static str) {
let pct = pct.clamp(0.0, 100.0);
let filled = ((pct / 100.0) * 10.0).round() as usize;
let bar = format!("{}{}", "█".repeat(filled), "░".repeat(10 - filled));
let color = if pct >= 80.0 {
C_DANGER
} else if pct >= 60.0 {
C_WARNING
} else {
C_DIM
};
(bar, color)
}
fn five_hour_chunk(info: &UsageInfo) -> String {
let Some(window) = &info.five_hour else {
return String::new();
};
let (bar, color) = bar_for(window.utilization);
let pct = window.utilization.clamp(0.0, 100.0);
let reset = format_reset(window)
.map(|r| format!("{C_FAINT} ({r}){C_RESET}"))
.unwrap_or_default();
format!(" {C_FAINT}5h{C_RESET} {color}[{bar}] {pct:.0}%{C_RESET}{reset}")
}
fn seven_day_chunk(info: &UsageInfo) -> String {
let Some(window) = &info.seven_day else {
return String::new();
};
let pct = window.utilization.clamp(0.0, 100.0);
let color = if pct >= 80.0 {
C_DANGER
} else if pct >= 60.0 {
C_WARNING
} else {
C_FAINT
};
let reset = format_reset(window)
.map(|r| format!(" in {r}"))
.unwrap_or_default();
format!("{C_FAINT} · {color}7d {pct:.0}%{C_FAINT}{reset}{C_RESET}")
}
fn weekly_window(info: &UsageInfo) -> Option<&UsageWindow> {
info.seven_day
.as_ref()
.or(info.seven_day_sonnet.as_ref())
.or(info.seven_day_opus.as_ref())
}
fn weekly_bar_chunk(info: &UsageInfo) -> String {
let Some(window) = weekly_window(info) else {
return String::new();
};
let (bar, color) = bar_for(window.utilization);
let pct = window.utilization.clamp(0.0, 100.0);
let reset = format_reset(window)
.map(|r| format!("{C_FAINT} ({r}){C_RESET}"))
.unwrap_or_default();
format!("{C_FAINT} · 7d{C_RESET} {color}[{bar}] {pct:.0}%{C_RESET}{reset}")
}
fn extra_usage_chunk(info: &UsageInfo) -> String {
let Some(extra) = &info.extra_usage else {
return String::new();
};
if !extra.is_enabled {
return String::new();
}
let used = extra.used_credits.unwrap_or(0.0);
let limit = extra.monthly_limit.unwrap_or(0.0);
let currency = extra.currency.as_deref().unwrap_or("");
let pct = extra.utilization.unwrap_or(0.0).clamp(0.0, 100.0);
let color = if pct >= 80.0 {
C_DANGER
} else if pct >= 60.0 {
C_WARNING
} else {
C_FAINT
};
let prefix = if currency.is_empty() { "" } else { " " };
format!(
"{C_FAINT} · {color}extra {used_div:.2}/{limit_div:.2}{prefix}{currency}{C_RESET}",
used_div = used / 100.0,
limit_div = limit / 100.0,
)
}
pub(crate) fn endpoint_visible_width(profile: &Profile) -> usize {
endpoint_label(profile).chars().count()
}
pub(crate) fn visible_width(s: &str) -> usize {
let mut count = 0;
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c != '\x1b' {
count += 1;
continue;
}
let Some(next) = chars.next() else { break };
if next != '[' {
continue;
}
for c in chars.by_ref() {
if matches!(c, '\x40'..='\x7e') {
break;
}
}
}
count
}
pub(crate) fn weekly_bar_visible_width(profile: &Profile) -> usize {
if !is_oauth_profile(profile) {
return 0;
}
match profile.usage.as_ref() {
Some(info) => visible_width(&weekly_bar_chunk(info)),
None => 0,
}
}
fn is_oauth_profile(profile: &Profile) -> bool {
profile.base_url.is_none()
}
fn name_underline_pair(profile: &Profile) -> (&'static str, &'static str) {
if !is_oauth_profile(profile) {
return ("", "");
}
match profile.fetch_status {
Some(FetchStatus::Cached) => (C_UL_WARNING, C_UL_OFF),
Some(FetchStatus::Failed) => (C_UL_DANGER, C_UL_OFF),
_ => ("", ""),
}
}
pub(crate) fn format_profile_entry(
profile: &Profile,
is_active: bool,
name_width: usize,
endpoint_width: usize,
show_weekly: bool,
) -> String {
let endpoint = endpoint_label(profile);
let usage = if is_oauth_profile(profile) {
profile.usage.as_ref()
} else {
None
};
let usage_hint = usage.map(five_hour_chunk).unwrap_or_default();
let weekly_hint = if show_weekly {
usage.map(weekly_bar_chunk).unwrap_or_default()
} else {
String::new()
};
let endpoint_pad = if usage_hint.is_empty() {
String::new()
} else {
" ".repeat(endpoint_width.saturating_sub(endpoint.chars().count()))
};
let key_hint = if profile.base_url.is_some() && profile.api_key.is_some() {
format!("{C_FAINT} · API key set{C_RESET}")
} else {
String::new()
};
let cred_warn = if profile.credentials.is_none() {
format!("{C_WARNING} · no credentials{C_RESET}")
} else {
String::new()
};
let name = &profile.name;
let (ul_on, ul_off) = name_underline_pair(profile);
let name_pad = " ".repeat(name_width.saturating_sub(name.chars().count()));
if is_active {
format!(
"{C_ACCENT}● {ul_on}{name}{ul_off}{name_pad}{C_NOBOLD} {C_DIM}{endpoint}{endpoint_pad}{C_RESET}{usage_hint}{weekly_hint}{key_hint}{cred_warn}"
)
} else {
format!(
" {ul_on}{name}{ul_off}{name_pad}{C_NOBOLD} {C_DIM}{endpoint}{endpoint_pad}{C_RESET}{usage_hint}{weekly_hint}{key_hint}{cred_warn}"
)
}
}
pub(crate) fn format_submenu_title(profile: &Profile) -> String {
let name = &profile.name;
let url = endpoint_label(profile);
let credentials = if profile.credentials.is_none() {
format!(" · {C_WARNING}no credentials")
} else {
String::new()
};
let usage = if is_oauth_profile(profile) {
profile.usage.as_ref()
} else {
None
};
let five_hour = usage.map(five_hour_chunk).unwrap_or_default();
let seven_day = usage.map(seven_day_chunk).unwrap_or_default();
let extra = usage.map(extra_usage_chunk).unwrap_or_default();
let (ul_on, ul_off) = name_underline_pair(profile);
format!(
"{C_BOLD}{ul_on}{name}{ul_off}{C_RESET}{C_FAINT} · {C_RESET}{C_DIM}{url}{C_FAINT}{credentials}{C_RESET}{five_hour}{seven_day}{extra}"
)
}
pub(crate) fn build_render_config() -> RenderConfig<'static> {
let orange = Color::Rgb {
r: 217,
g: 119,
b: 87,
};
let blue = Color::Rgb {
r: 67,
g: 171,
b: 229,
};
let faint = Color::Rgb {
r: 127,
g: 132,
b: 156,
};
RenderConfig::default()
.with_prompt_prefix(Styled::new("?").with_fg(blue))
.with_answered_prompt_prefix(Styled::new("?").with_fg(faint))
.with_highlighted_option_prefix(Styled::new("▶").with_fg(orange))
.with_selected_option(Some(StyleSheet::new().with_attr(Attributes::BOLD)))
.with_answer(StyleSheet::new().with_attr(Attributes::ITALIC))
.with_help_message(StyleSheet::new().with_fg(blue))
}