use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DataSource {
OauthApi,
WebApi,
CliProbe,
}
#[derive(Clone, Copy, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum UsageLevel {
Normal,
Warn,
Danger,
Over,
}
#[derive(Serialize)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum WidgetPayload {
Active { data: Box<ActivePayload> },
Error { title: String, message: String },
}
#[derive(Serialize)]
pub struct ActivePayload {
pub five_hour: Option<UsageWindow>,
pub seven_day: Option<UsageWindow>,
pub seven_day_sonnet: Option<UsageWindow>,
pub seven_day_opus: Option<UsageWindow>,
pub extra_usage: Option<ExtraUsage>,
pub updated_at: String,
pub source: DataSource,
pub cost_data: Option<CostData>,
}
#[derive(Serialize, Clone)]
pub struct UsageWindow {
pub utilization: f64,
pub resets_at: Option<String>,
pub resets_in_minutes: Option<f64>,
pub usage_level: UsageLevel,
pub pace: Option<PaceInfo>,
}
#[derive(Serialize, Clone)]
pub struct PaceInfo {
pub delta_percent: f64,
pub expected_percent: f64,
pub will_last_to_reset: bool,
pub eta_minutes: Option<f64>,
}
#[derive(Serialize, Clone)]
pub struct ExtraUsage {
pub is_enabled: bool,
pub monthly_limit: Option<f64>,
pub used_credits: Option<f64>,
pub utilization: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CostData {
pub last_30d_cost_usd: Option<f64>,
pub models: Vec<String>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ApiResponse {
pub five_hour: Option<ApiWindow>,
pub seven_day: Option<ApiWindow>,
pub seven_day_sonnet: Option<ApiWindow>,
pub seven_day_opus: Option<ApiWindow>,
pub extra_usage: Option<ApiExtraUsage>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct CachedResponse {
pub cached_at: String,
pub response: ApiResponse,
pub source: DataSource,
pub cost_data: Option<CostData>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ApiWindow {
pub utilization: Option<f64>,
pub resets_at: Option<String>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ApiExtraUsage {
pub is_enabled: bool,
pub monthly_limit: Option<f64>,
pub used_credits: Option<f64>,
pub utilization: Option<f64>,
}
#[derive(Debug, Deserialize)]
pub struct KeychainPayload {
#[serde(rename = "claudeAiOauth")]
pub claude_ai_oauth: Option<OAuthData>,
}
#[derive(Debug, Deserialize)]
pub struct OAuthData {
#[serde(rename = "accessToken")]
pub access_token: Option<String>,
#[serde(rename = "expiresAt")]
pub expires_at: Option<i64>,
}
#[derive(Debug, Deserialize)]
pub struct CredentialsFile {
#[serde(rename = "claudeAiOauth")]
pub claude_ai_oauth: Option<OAuthData>,
}
pub fn parse_reset_time(s: &str) -> Option<DateTime<Utc>> {
DateTime::parse_from_rfc3339(s)
.ok()
.map(|dt| dt.with_timezone(&Utc))
}
pub fn usage_level(percent: f64) -> UsageLevel {
if percent >= 100.0 {
UsageLevel::Over
} else if percent >= 80.0 {
UsageLevel::Danger
} else if percent >= 60.0 {
UsageLevel::Warn
} else {
UsageLevel::Normal
}
}
fn compute_pace(utilization: f64, resets_in_minutes: f64, window_minutes: f64) -> Option<PaceInfo> {
if window_minutes <= 0.0 || resets_in_minutes <= 0.0 || resets_in_minutes > window_minutes {
return None;
}
let elapsed_secs = (window_minutes - resets_in_minutes) * 60.0;
let duration_secs = window_minutes * 60.0;
let time_until_reset_secs = resets_in_minutes * 60.0;
let actual = utilization.clamp(0.0, 100.0);
let expected = ((elapsed_secs / duration_secs) * 100.0).clamp(0.0, 100.0);
if elapsed_secs == 0.0 && actual > 0.0 {
return None;
}
if expected < 3.0 {
return None;
}
let delta = actual - expected;
let (will_last_to_reset, eta_minutes) = if elapsed_secs > 0.0 && actual > 0.0 {
let rate = actual / elapsed_secs;
if rate > 0.0 {
let remaining = (100.0 - actual).max(0.0);
let candidate_secs = remaining / rate;
if candidate_secs >= time_until_reset_secs {
(true, None)
} else {
(false, Some(candidate_secs / 60.0))
}
} else {
(true, None)
}
} else if elapsed_secs > 0.0 {
(true, None)
} else {
return None;
};
Some(PaceInfo {
delta_percent: delta,
expected_percent: expected,
will_last_to_reset,
eta_minutes,
})
}
pub fn to_usage_window(
api: &ApiWindow,
now: DateTime<Utc>,
window_minutes: Option<f64>,
) -> UsageWindow {
let utilization = api.utilization.unwrap_or(0.0);
let reset_dt = api.resets_at.as_deref().and_then(parse_reset_time);
let resets_in_minutes = reset_dt.map(|reset| {
let delta = reset.signed_duration_since(now);
(delta.num_seconds() as f64 / 60.0).max(0.0)
});
let pace = match (window_minutes, resets_in_minutes) {
(Some(wm), Some(rm)) => compute_pace(utilization, rm, wm),
_ => None,
};
UsageWindow {
utilization,
resets_at: api.resets_at.clone(),
resets_in_minutes,
usage_level: usage_level(utilization),
pace,
}
}