Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/config/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,26 @@ pub struct OutputStyle {
pub name: String,
}

#[derive(Debug, Deserialize)]
pub struct RateLimitWindow {
pub used_percentage: Option<f64>,
pub resets_at: Option<u64>,
}

#[derive(Debug, Deserialize)]
pub struct RateLimits {
pub five_hour: Option<RateLimitWindow>,
pub seven_day: Option<RateLimitWindow>,
}

#[derive(Deserialize)]
pub struct InputData {
pub model: Model,
pub workspace: Workspace,
pub transcript_path: String,
pub cost: Option<Cost>,
pub output_style: Option<OutputStyle>,
pub rate_limits: Option<RateLimits>,
}

// OpenAI-style nested token details
Expand Down
69 changes: 67 additions & 2 deletions src/core/segments/usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,28 @@ impl UsageSegment {
"?".to_string()
}

fn format_reset_time_epoch(epoch: Option<u64>) -> 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<std::path::PathBuf> {
let home = dirs::home_dir()?;
Some(
Expand Down Expand Up @@ -181,10 +203,53 @@ impl UsageSegment {
}

impl Segment for UsageSegment {
fn collect(&self, _input: &InputData) -> Option<SegmentData> {
fn collect(&self, input: &InputData) -> Option<SegmentData> {
// 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);
Comment on lines +209 to +218

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Clamp or validate the utilization percentage before casting to u8 and formatting.

Because five_hour_util is external input, if it ever falls outside [0.0, 100.0] the round() as u8 cast can produce odd values like 255%. Clamping to a sane range (e.g., 0.0..=100.0) before rounding and casting would keep the displayed percentage within expected bounds.

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);

Expand Down