Skip to main content

claude_code_stats/
types.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
5#[serde(rename_all = "snake_case")]
6pub enum DataSource {
7    OauthApi,
8    WebApi,
9    CliProbe,
10}
11
12#[derive(Clone, Copy, Serialize)]
13#[serde(rename_all = "snake_case")]
14pub enum UsageLevel {
15    Normal,
16    Warn,
17    Danger,
18    Over,
19}
20
21#[derive(Serialize)]
22#[serde(tag = "status", rename_all = "snake_case")]
23pub enum WidgetPayload {
24    Active { data: Box<ActivePayload> },
25    Error { title: String, message: String },
26}
27
28#[derive(Serialize)]
29pub struct ActivePayload {
30    pub five_hour: Option<UsageWindow>,
31    pub seven_day: Option<UsageWindow>,
32    pub seven_day_sonnet: Option<UsageWindow>,
33    pub seven_day_opus: Option<UsageWindow>,
34    pub extra_usage: Option<ExtraUsage>,
35    pub updated_at: String,
36    pub source: DataSource,
37    pub cost_data: Option<CostData>,
38}
39
40#[derive(Serialize, Clone)]
41pub struct UsageWindow {
42    pub utilization: f64,
43    pub resets_at: Option<String>,
44    pub resets_in_minutes: Option<f64>,
45    pub usage_level: UsageLevel,
46    pub pace: Option<PaceInfo>,
47}
48
49#[derive(Serialize, Clone)]
50pub struct PaceInfo {
51    pub delta_percent: f64,
52    pub expected_percent: f64,
53    pub will_last_to_reset: bool,
54    pub eta_minutes: Option<f64>,
55}
56
57#[derive(Serialize, Clone)]
58pub struct ExtraUsage {
59    pub is_enabled: bool,
60    pub monthly_limit: Option<f64>,
61    pub used_credits: Option<f64>,
62    pub utilization: Option<f64>,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct CostData {
67    pub last_30d_cost_usd: Option<f64>,
68    pub models: Vec<String>,
69}
70
71#[derive(Debug, Deserialize, Serialize, Clone)]
72pub struct ApiResponse {
73    pub five_hour: Option<ApiWindow>,
74    pub seven_day: Option<ApiWindow>,
75    pub seven_day_sonnet: Option<ApiWindow>,
76    pub seven_day_opus: Option<ApiWindow>,
77    pub extra_usage: Option<ApiExtraUsage>,
78}
79
80#[derive(Debug, Deserialize, Serialize)]
81pub struct CachedResponse {
82    pub cached_at: String,
83    pub response: ApiResponse,
84    pub source: DataSource,
85    pub cost_data: Option<CostData>,
86}
87
88#[derive(Debug, Deserialize, Serialize, Clone)]
89pub struct ApiWindow {
90    pub utilization: Option<f64>,
91    pub resets_at: Option<String>,
92}
93
94#[derive(Debug, Deserialize, Serialize, Clone)]
95pub struct ApiExtraUsage {
96    pub is_enabled: bool,
97    pub monthly_limit: Option<f64>,
98    pub used_credits: Option<f64>,
99    pub utilization: Option<f64>,
100}
101
102#[derive(Debug, Deserialize)]
103pub struct KeychainPayload {
104    #[serde(rename = "claudeAiOauth")]
105    pub claude_ai_oauth: Option<OAuthData>,
106}
107
108#[derive(Debug, Deserialize)]
109pub struct OAuthData {
110    #[serde(rename = "accessToken")]
111    pub access_token: Option<String>,
112    #[serde(rename = "expiresAt")]
113    pub expires_at: Option<i64>,
114}
115
116#[derive(Debug, Deserialize)]
117pub struct CredentialsFile {
118    #[serde(rename = "claudeAiOauth")]
119    pub claude_ai_oauth: Option<OAuthData>,
120}
121
122pub fn parse_reset_time(s: &str) -> Option<DateTime<Utc>> {
123    DateTime::parse_from_rfc3339(s)
124        .ok()
125        .map(|dt| dt.with_timezone(&Utc))
126}
127
128pub fn usage_level(percent: f64) -> UsageLevel {
129    if percent >= 100.0 {
130        UsageLevel::Over
131    } else if percent >= 80.0 {
132        UsageLevel::Danger
133    } else if percent >= 60.0 {
134        UsageLevel::Warn
135    } else {
136        UsageLevel::Normal
137    }
138}
139
140fn compute_pace(utilization: f64, resets_in_minutes: f64, window_minutes: f64) -> Option<PaceInfo> {
141    if window_minutes <= 0.0 || resets_in_minutes <= 0.0 || resets_in_minutes > window_minutes {
142        return None;
143    }
144
145    let elapsed_secs = (window_minutes - resets_in_minutes) * 60.0;
146    let duration_secs = window_minutes * 60.0;
147    let time_until_reset_secs = resets_in_minutes * 60.0;
148
149    let actual = utilization.clamp(0.0, 100.0);
150    let expected = ((elapsed_secs / duration_secs) * 100.0).clamp(0.0, 100.0);
151
152    // CodexBar: skip when elapsed==0 but actual>0, or when expected < 3%
153    if elapsed_secs == 0.0 && actual > 0.0 {
154        return None;
155    }
156    if expected < 3.0 {
157        return None;
158    }
159
160    let delta = actual - expected;
161
162    let (will_last_to_reset, eta_minutes) = if elapsed_secs > 0.0 && actual > 0.0 {
163        let rate = actual / elapsed_secs;
164        if rate > 0.0 {
165            let remaining = (100.0 - actual).max(0.0);
166            let candidate_secs = remaining / rate;
167            if candidate_secs >= time_until_reset_secs {
168                (true, None)
169            } else {
170                (false, Some(candidate_secs / 60.0))
171            }
172        } else {
173            (true, None)
174        }
175    } else if elapsed_secs > 0.0 {
176        (true, None)
177    } else {
178        return None;
179    };
180
181    Some(PaceInfo {
182        delta_percent: delta,
183        expected_percent: expected,
184        will_last_to_reset,
185        eta_minutes,
186    })
187}
188
189pub fn to_usage_window(
190    api: &ApiWindow,
191    now: DateTime<Utc>,
192    window_minutes: Option<f64>,
193) -> UsageWindow {
194    let utilization = api.utilization.unwrap_or(0.0);
195    let reset_dt = api.resets_at.as_deref().and_then(parse_reset_time);
196    let resets_in_minutes = reset_dt.map(|reset| {
197        let delta = reset.signed_duration_since(now);
198        (delta.num_seconds() as f64 / 60.0).max(0.0)
199    });
200
201    let pace = match (window_minutes, resets_in_minutes) {
202        (Some(wm), Some(rm)) => compute_pace(utilization, rm, wm),
203        _ => None,
204    };
205
206    UsageWindow {
207        utilization,
208        resets_at: api.resets_at.clone(),
209        resets_in_minutes,
210        usage_level: usage_level(utilization),
211        pace,
212    }
213}