Skip to main content

claude_code_stats/
lib.rs

1pub mod source;
2pub mod types;
3
4use std::fs;
5use std::path::PathBuf;
6
7use anyhow::Result;
8use chrono::{DateTime, Utc};
9
10use source::cli_probe::CliProbeSource;
11use source::oauth::OauthSource;
12use source::web::WebSource;
13use source::{UsageSource, fetch_with_failover};
14use types::{
15    ActivePayload, ApiResponse, CachedResponse, CostData, DataSource, ExtraUsage, WidgetPayload,
16    parse_reset_time, to_usage_window,
17};
18
19pub const CACHE_MAX_AGE_SECS: i64 = 4 * 60;
20pub const CACHE_FILE_NAME: &str = "claude-code-stats-cache.json";
21
22pub fn collect_widget_payload() -> Result<WidgetPayload> {
23    let now = Utc::now();
24    let (usage, cached_at, data_source, cost_data) = get_usage_with_cache(now)?;
25
26    let data = ActivePayload {
27        five_hour: usage
28            .five_hour
29            .map(|w| to_usage_window(&w, now, Some(300.0))),
30        seven_day: usage
31            .seven_day
32            .map(|w| to_usage_window(&w, now, Some(10080.0))),
33        seven_day_sonnet: usage
34            .seven_day_sonnet
35            .map(|w| to_usage_window(&w, now, Some(10080.0))),
36        seven_day_opus: usage
37            .seven_day_opus
38            .map(|w| to_usage_window(&w, now, Some(10080.0))),
39        extra_usage: usage.extra_usage.map(|e| ExtraUsage {
40            is_enabled: e.is_enabled,
41            monthly_limit: e.monthly_limit,
42            used_credits: e.used_credits,
43            utilization: e.utilization,
44        }),
45        updated_at: cached_at.to_rfc3339(),
46        source: data_source,
47        cost_data,
48    };
49
50    Ok(WidgetPayload::Active {
51        data: Box::new(data),
52    })
53}
54
55pub fn collect_widget_payload_json() -> String {
56    let payload = match collect_widget_payload() {
57        Ok(payload) => payload,
58        Err(err) => {
59            eprintln!("claude-code-stats error: {err:?}");
60            WidgetPayload::Error {
61                title: "Claude usage error".to_string(),
62                message: err.to_string(),
63            }
64        }
65    };
66
67    match serde_json::to_string(&payload) {
68        Ok(json) => json,
69        Err(err) => {
70            eprintln!("claude-code-stats serialization error: {err:?}");
71            "{\"status\":\"error\",\"title\":\"claude-code-stats\",\"message\":\"serialization failure\"}".to_string()
72        }
73    }
74}
75
76fn get_cache_path() -> PathBuf {
77    dirs::cache_dir()
78        .unwrap_or_else(|| PathBuf::from("/tmp"))
79        .join(CACHE_FILE_NAME)
80}
81
82fn get_usage_with_cache(
83    now: DateTime<Utc>,
84) -> Result<(ApiResponse, DateTime<Utc>, DataSource, Option<CostData>)> {
85    let cache_path = get_cache_path();
86
87    if let Ok(contents) = fs::read_to_string(&cache_path) {
88        if let Ok(cached) = serde_json::from_str::<CachedResponse>(&contents) {
89            if let Some(cached_at) = parse_reset_time(&cached.cached_at) {
90                let age = now.signed_duration_since(cached_at).num_seconds();
91                if age < CACHE_MAX_AGE_SECS {
92                    return Ok((cached.response, cached_at, cached.source, cached.cost_data));
93                }
94            }
95        }
96    }
97
98    let sources: Vec<&dyn UsageSource> = vec![&OauthSource, &WebSource, &CliProbeSource];
99    let (response, source_name) = fetch_with_failover(&sources)?;
100
101    let data_source = match source_name {
102        "oauth_api" => DataSource::OauthApi,
103        "web_api" => DataSource::WebApi,
104        "cli_probe" => DataSource::CliProbe,
105        _ => DataSource::OauthApi,
106    };
107
108    let cost_data = source::cost_scanner::scan_cost_data();
109
110    let cached = CachedResponse {
111        cached_at: now.to_rfc3339(),
112        response: response.clone(),
113        source: data_source,
114        cost_data: cost_data.clone(),
115    };
116
117    if let Ok(json) = serde_json::to_string(&cached) {
118        let _ = fs::write(&cache_path, json);
119    }
120
121    Ok((response, now, data_source, cost_data))
122}