use crate::output::format_duration_secs;
use super::types::{ AccountQuota, PreferStrategy };
pub( crate ) fn token_exp_label( expires_at_ms : u64 ) -> String
{
let now_ms = u64::try_from(
std::time::SystemTime::now()
.duration_since( std::time::UNIX_EPOCH )
.unwrap_or_default()
.as_millis()
).unwrap_or( u64::MAX );
if now_ms >= expires_at_ms
{
format!( "expired({} ago)", format_duration_secs( ( now_ms - expires_at_ms ) / 1000 ) )
}
else
{
format!( "valid({} left)", format_duration_secs( ( expires_at_ms - now_ms ) / 1000 ) )
}
}
pub( crate ) fn compute_expires_cell( expires_at_ms : u64, now_secs : u64 ) -> String
{
let remaining = ( expires_at_ms / 1000 ).saturating_sub( now_secs );
if remaining == 0
{
"EXPIRED".to_string()
}
else
{
format!( "in {}", format_duration_secs( remaining ) )
}
}
pub( crate ) fn unix_to_date( unix_secs : u64 ) -> ( u64, u64, u64 )
{
let is_leap = |y : u64| ( y % 4 == 0 && y % 100 != 0 ) || y % 400 == 0;
let mut days = unix_secs / 86_400;
let mut year = 1970_u64;
loop
{
let in_year = if is_leap( year ) { 366 } else { 365 };
if days < in_year { break; }
days -= in_year;
year += 1;
}
let feb = if is_leap( year ) { 29 } else { 28 };
let month_days : [ u64; 12 ] = [ 31, feb, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
let mut month = 0_u64;
for d in &month_days
{
if days < *d { break; }
days -= d;
month += 1;
}
( year, month + 1, days + 1 )
}
fn date_to_unix( year : u64, month : u64, day : u64 ) -> u64
{
let is_leap = |y : u64| ( y % 4 == 0 && y % 100 != 0 ) || y % 400 == 0;
let mut days = 0_u64;
for y in 1970..year { days += if is_leap( y ) { 366 } else { 365 }; }
let feb = if is_leap( year ) { 29 } else { 28 };
let month_days : [ u64; 12 ] = [ 31, feb, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
for &month_day in month_days.iter().take( usize::try_from( month - 1 ).unwrap_or( 0 ) ) { days += month_day; }
days += day - 1;
days * 86_400
}
fn parse_iso_secs( s : &str ) -> Option< u64 >
{
if s.len() < 19 { return None; }
let year : u64 = s[ 0..4 ].parse().ok()?;
let month : u64 = s[ 5..7 ].parse().ok()?;
let day : u64 = s[ 8..10 ].parse().ok()?;
let hour : u64 = s[ 11..13 ].parse().ok()?;
let min : u64 = s[ 14..16 ].parse().ok()?;
let sec : u64 = s[ 17..19 ].parse().ok()?;
if year < 1970 || month == 0 || month > 12 || day == 0 || day > 31 { return None; }
Some( date_to_unix( year, month, day ) + hour * 3_600 + min * 60 + sec )
}
pub( crate ) fn renewal_secs(
renewal_at_opt : Option< &str >,
org_created_at_opt : Option< &str >,
now_secs : u64,
) -> Option< ( u64, bool ) >
{
if let Some( renewal_at ) = renewal_at_opt
{
let mut ts = parse_iso_secs( renewal_at )?;
while ts < now_secs { ts = ts.saturating_add( 2_592_000 ); }
return Some( ( ts.saturating_sub( now_secs ), false ) );
}
if let Some( org_created_at ) = org_created_at_opt
{
if org_created_at.len() < 10 { return None; }
let billing_day : u64 = org_created_at[ 8..10 ].parse().ok()?;
if billing_day == 0 || billing_day > 31 { return None; }
let ( year, month, day ) = unix_to_date( now_secs );
let ( renewal_year, renewal_month ) = if billing_day > day
{
( year, month )
}
else if month == 12
{
( year + 1, 1 )
}
else
{
( year, month + 1 )
};
let renewal_ts = date_to_unix( renewal_year, renewal_month, billing_day );
return Some( ( renewal_ts.saturating_sub( now_secs ), true ) );
}
None
}
pub( crate ) fn renews_label(
renewal_at_opt : Option< &str >,
org_created_at_opt : Option< &str >,
now_secs : u64,
) -> String
{
if renewal_at_opt.is_none() && org_created_at_opt.is_none()
{
return "?".to_string();
}
match renewal_secs( renewal_at_opt, org_created_at_opt, now_secs )
{
None => "\u{2014}".to_string(),
Some( ( secs, false ) ) => format!( "in {}", format_duration_secs( secs ) ),
Some( ( secs, true ) ) => format!( "~in {}", format_duration_secs( secs ) ),
}
}
pub( crate ) fn next_event_raw(
seven_day_resets_secs : Option< u64 >,
renewal_secs_opt : Option< u64 >,
renewal_is_estimate : bool,
) -> Option< ( u64, &'static str, bool ) >
{
let consider = |current : Option< ( u64, &'static str, bool ) >,
secs : u64,
prefix : &'static str,
est : bool|
-> Option< ( u64, &'static str, bool ) >
{
if secs == 0 { return current; }
match current
{
None => Some( ( secs, prefix, est ) ),
Some( ( best, _, _ ) ) if secs < best => Some( ( secs, prefix, est ) ),
other => other,
}
};
let mut best = None;
if let Some( s ) = seven_day_resets_secs { best = consider( best, s, "+7d", false ); }
if let Some( s ) = renewal_secs_opt { best = consider( best, s, "$ren", renewal_is_estimate ); }
best
}
pub( crate ) fn next_event_label(
seven_day_resets_secs : Option< u64 >,
renewal_secs_opt : Option< u64 >,
renewal_is_estimate : bool,
) -> String
{
match next_event_raw( seven_day_resets_secs, renewal_secs_opt, renewal_is_estimate )
{
None => "\u{2014}".to_string(),
Some( ( secs, prefix, true ) ) => format!( "~in {} {prefix}", format_duration_secs( secs ) ),
Some( ( secs, prefix, false ) ) => format!( "in {} {prefix}", format_duration_secs( secs ) ),
}
}
pub( crate ) fn sub_label( account : Option< &claude_quota::OauthAccountData > ) -> &'static str
{
let Some( a ) = account else { return "?"; };
if a.billing_type == "none" { return "\u{2014}"; }
if a.has_max { return "max"; }
if a.billing_type == "stripe_subscription" { return "pro"; }
"?"
}
pub( crate ) fn shorten_error( reason : &str ) -> &str
{
if reason.starts_with( "HTTP transport error: HTTP 429" )
{
"rate limited (429)"
}
else if reason.starts_with( "HTTP transport error: HTTP 401" )
{
"auth expired (401)"
}
else if reason.starts_with( "HTTP transport error: HTTP 403" )
{
"auth forbidden (403)"
}
else if reason.starts_with( "rate-limit header missing:" )
{
"no header"
}
else
{
reason
}
}
pub( crate ) fn five_hour_left( aq : &AccountQuota ) -> f64
{
if let Ok( data ) = &aq.result
{
100.0 - data.five_hour.as_ref().map_or( 0.0, |p| p.utilization )
}
else
{
-1.0
}
}
pub( crate ) fn seven_day_left( aq : &AccountQuota ) -> f64
{
let Ok( ref data ) = aq.result else { return 0.0; };
100.0 - data.seven_day.as_ref().map_or( 0.0, |p| p.utilization )
}
pub( super ) fn relevant_quotas( aq : &AccountQuota, prefer : PreferStrategy ) -> ( f64, f64 )
{
let Ok( data ) = &aq.result else { return ( -1.0, 0.0 ); };
let five_h_left = 100.0 - data.five_hour.as_ref().map_or( 0.0, |p| p.utilization );
let left_7d = 100.0 - data.seven_day.as_ref().map_or( 0.0, |p| p.utilization );
let relevant_7d = match prefer
{
PreferStrategy::Opus => left_7d,
PreferStrategy::Sonnet =>
{
if let Some( ref son ) = data.seven_day_sonnet { 100.0 - son.utilization }
else { 0.0 }
}
PreferStrategy::Any =>
{
if let Some( ref son ) = data.seven_day_sonnet { left_7d.min( 100.0 - son.utilization ) }
else { left_7d }
}
};
( five_h_left, relevant_7d )
}
pub( crate ) fn prefer_weekly( aq : &AccountQuota, prefer : PreferStrategy ) -> f64
{
relevant_quotas( aq, prefer ).1
}
pub( super ) const OPUS_OVERRIDE_THRESHOLD : f64 = 15.0;
pub( crate ) fn recommended_model( aq : &AccountQuota ) -> &'static str
{
match &aq.result
{
Ok( data ) => match &data.seven_day_sonnet
{
Some( s ) if 100.0 - s.utilization < OPUS_OVERRIDE_THRESHOLD => "opus",
_ => "sonnet",
},
Err( _ ) => "sonnet",
}
}
pub( crate ) fn quota_text_cells( data : &claude_quota::OauthUsageData, now_secs : u64 ) -> [ String; 5 ]
{
let dash = "\u{2014}".to_string();
let pct_cell = |util : Option< f64 >| -> String
{
util.map_or_else( || dash.clone(), |u| format!( "{:.0}%", 100.0 - u ) )
};
let pct_emoji = |util : Option< f64 >, threshold : f64| -> String
{
util.map_or_else( || dash.clone(), |u|
{
let left = 100.0 - u;
let emoji = if left > threshold { "🟢" } else { "🟡" };
format!( "{emoji} {left:.0}%" )
} )
};
let reset_cell = |iso : Option< &str >| -> String
{
iso.and_then( claude_quota::iso_to_unix_secs )
.map_or_else( || dash.clone(), |t|
format!( "in {}", format_duration_secs( t.saturating_sub( now_secs ) ) )
)
};
[
pct_emoji( data.five_hour.as_ref().map( |p| p.utilization ), 15.0 ),
reset_cell( data.five_hour.as_ref().and_then( |p| p.resets_at.as_deref() ) ),
pct_emoji( data.seven_day.as_ref().map( |p| p.utilization ), 5.0 ),
pct_cell( data.seven_day_sonnet.as_ref().map( |p| p.utilization ) ),
reset_cell( data.seven_day.as_ref().and_then( |p| p.resets_at.as_deref() ) ),
]
}
pub( crate ) fn status_emoji( result : &Result< claude_quota::OauthUsageData, String > ) -> &'static str
{
match result
{
Err( _ ) => "🔴",
Ok( data ) =>
{
let h5_left = 100.0 - data.five_hour.as_ref().map_or( 0.0, |p| p.utilization );
let d7_left = 100.0 - data.seven_day.as_ref().map_or( 0.0, |p| p.utilization );
if h5_left > 15.0 && d7_left > 5.0 { "🟢" } else { "🟡" }
}
}
}
#[ cfg( test ) ]
#[ path = "format_tests.rs" ]
mod tests;