From 7815e28414f96587db453e8a902d11f5ecea618d Mon Sep 17 00:00:00 2001 From: NSDont Date: Thu, 2 Apr 2026 21:20:58 +0800 Subject: [PATCH] feat(usage): prefer stdin rate_limits over API polling Claude Code >= 2.1.80 passes rate_limits in the statusLine JSON input, providing real-time usage data with zero network overhead. This change makes the Usage segment read from stdin first and only falls back to the API polling path for older Claude Code versions. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/config/types.rs | 13 +++++++ src/core/segments/usage.rs | 69 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/src/config/types.rs b/src/config/types.rs index e5a78dc1..d2c97dd3 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -110,6 +110,18 @@ pub struct OutputStyle { pub name: String, } +#[derive(Debug, Deserialize)] +pub struct RateLimitWindow { + pub used_percentage: Option, + pub resets_at: Option, +} + +#[derive(Debug, Deserialize)] +pub struct RateLimits { + pub five_hour: Option, + pub seven_day: Option, +} + #[derive(Deserialize)] pub struct InputData { pub model: Model, @@ -117,6 +129,7 @@ pub struct InputData { pub transcript_path: String, pub cost: Option, pub output_style: Option, + pub rate_limits: Option, } // OpenAI-style nested token details diff --git a/src/core/segments/usage.rs b/src/core/segments/usage.rs index d5dd9bde..d7f7a94e 100644 --- a/src/core/segments/usage.rs +++ b/src/core/segments/usage.rs @@ -65,6 +65,28 @@ impl UsageSegment { "?".to_string() } + fn format_reset_time_epoch(epoch: Option) -> String { + if let Some(ts) = epoch { + if ts == 0 { + return "?".to_string(); + } + let now = Utc::now().timestamp() as u64; + if ts <= now { + return "?".to_string(); + } + let remaining_mins = (ts - now) / 60; + if remaining_mins >= 1440 { + format!("{}d", remaining_mins / 1440) + } else if remaining_mins >= 60 { + format!("{}h", remaining_mins / 60) + } else { + format!("{}m", remaining_mins) + } + } else { + "?".to_string() + } + } + fn get_cache_path() -> Option { let home = dirs::home_dir()?; Some( @@ -181,10 +203,53 @@ impl UsageSegment { } impl Segment for UsageSegment { - fn collect(&self, _input: &InputData) -> Option { + fn collect(&self, input: &InputData) -> Option { + // Priority 1: Use rate_limits from stdin (Claude Code >= 2.1.80, zero network) + if let Some(rate_limits) = &input.rate_limits { + let five_hour_util = rate_limits + .five_hour + .as_ref() + .and_then(|w| w.used_percentage) + .unwrap_or(0.0); + let seven_day_util = rate_limits + .seven_day + .as_ref() + .and_then(|w| w.used_percentage) + .unwrap_or(0.0); + let resets_at_epoch = rate_limits + .five_hour + .as_ref() + .and_then(|w| w.resets_at); + + let dynamic_icon = Self::get_circle_icon(seven_day_util / 100.0); + let five_hour_percent = five_hour_util.round() as u8; + let primary = format!("{}%", five_hour_percent); + let secondary = format!( + "ยท {}", + Self::format_reset_time_epoch(resets_at_epoch) + ); + + let mut metadata = HashMap::new(); + metadata.insert("dynamic_icon".to_string(), dynamic_icon); + metadata.insert( + "five_hour_utilization".to_string(), + five_hour_util.to_string(), + ); + metadata.insert( + "seven_day_utilization".to_string(), + seven_day_util.to_string(), + ); + + return Some(SegmentData { + primary, + secondary, + metadata, + }); + } + + // Priority 2: API fallback (for older Claude Code versions) let token = credentials::get_oauth_token()?; - // Load config from file to get segment options let config = crate::config::Config::load().ok()?; let segment_config = config.segments.iter().find(|s| s.id == SegmentId::Usage);