codex_cli/rate_limits/
render.rs1use chrono::{Local, TimeZone};
2use serde_json::Value;
3
4pub struct UsageData {
5 pub primary: Window,
6 pub secondary: Window,
7}
8
9pub struct Window {
10 pub limit_window_seconds: i64,
11 pub used_percent: f64,
12 pub reset_at: i64,
13}
14
15pub struct RenderValues {
16 pub primary_label: String,
17 pub secondary_label: String,
18 pub primary_remaining: i64,
19 pub secondary_remaining: i64,
20 pub primary_reset_epoch: i64,
21 pub secondary_reset_epoch: i64,
22}
23
24pub struct WeeklyValues {
25 pub weekly_remaining: i64,
26 pub weekly_reset_epoch: i64,
27 pub non_weekly_label: String,
28 pub non_weekly_remaining: i64,
29 pub non_weekly_reset_epoch: Option<i64>,
30}
31
32pub fn parse_usage(json: &Value) -> Option<UsageData> {
33 let rate_limit = json.get("rate_limit")?;
34 let primary = parse_window(rate_limit.get("primary_window")?)?;
35 let secondary = parse_window(rate_limit.get("secondary_window")?)?;
36 Some(UsageData { primary, secondary })
37}
38
39pub fn rate_limit_is_explicit_null(json: &Value) -> bool {
49 matches!(json.get("rate_limit"), Some(Value::Null))
50}
51
52fn parse_window(value: &Value) -> Option<Window> {
53 let limit_window_seconds = value.get("limit_window_seconds")?.as_i64()?;
54 let used_percent = value
55 .get("used_percent")
56 .and_then(|v| v.as_f64())
57 .unwrap_or(0.0);
58 let reset_at = value.get("reset_at")?.as_i64()?;
59 Some(Window {
60 limit_window_seconds,
61 used_percent,
62 reset_at,
63 })
64}
65
66pub fn render_values(data: &UsageData) -> RenderValues {
67 let primary_label = format_window_seconds(data.primary.limit_window_seconds)
68 .unwrap_or_else(|| "Primary".to_string());
69 let secondary_label = format_window_seconds(data.secondary.limit_window_seconds)
70 .unwrap_or_else(|| "Secondary".to_string());
71
72 let primary_remaining = remaining_percent(data.primary.used_percent);
73 let secondary_remaining = remaining_percent(data.secondary.used_percent);
74
75 RenderValues {
76 primary_label,
77 secondary_label,
78 primary_remaining,
79 secondary_remaining,
80 primary_reset_epoch: data.primary.reset_at,
81 secondary_reset_epoch: data.secondary.reset_at,
82 }
83}
84
85pub fn weekly_values(values: &RenderValues) -> WeeklyValues {
86 let (
87 weekly_remaining,
88 weekly_reset_epoch,
89 non_weekly_label,
90 non_weekly_remaining,
91 non_weekly_reset_epoch,
92 ) = if values.primary_label == "Weekly" {
93 (
94 values.primary_remaining,
95 values.primary_reset_epoch,
96 values.secondary_label.clone(),
97 values.secondary_remaining,
98 Some(values.secondary_reset_epoch),
99 )
100 } else {
101 (
102 values.secondary_remaining,
103 values.secondary_reset_epoch,
104 values.primary_label.clone(),
105 values.primary_remaining,
106 Some(values.primary_reset_epoch),
107 )
108 };
109
110 WeeklyValues {
111 weekly_remaining,
112 weekly_reset_epoch,
113 non_weekly_label,
114 non_weekly_remaining,
115 non_weekly_reset_epoch,
116 }
117}
118
119pub fn format_window_seconds(raw: i64) -> Option<String> {
120 if raw <= 0 {
121 return None;
122 }
123 if raw % 604_800 == 0 {
124 let weeks = raw / 604_800;
125 if weeks == 1 {
126 return Some("Weekly".to_string());
127 }
128 return Some(format!("{weeks}w"));
129 }
130 if raw % 86_400 == 0 {
131 return Some(format!("{}d", raw / 86_400));
132 }
133 if raw % 3_600 == 0 {
134 return Some(format!("{}h", raw / 3_600));
135 }
136 if raw % 60 == 0 {
137 return Some(format!("{}m", raw / 60));
138 }
139 Some(format!("{raw}s"))
140}
141
142pub fn format_epoch_local_datetime(epoch: i64) -> Option<String> {
143 let dt = Local.timestamp_opt(epoch, 0).single()?;
144 Some(dt.format("%m-%d %H:%M").to_string())
145}
146
147pub fn format_epoch_local_datetime_with_offset(epoch: i64) -> Option<String> {
148 let dt = Local.timestamp_opt(epoch, 0).single()?;
149 Some(dt.format("%m-%d %H:%M %:z").to_string())
150}
151
152pub fn format_epoch_local(epoch: i64, fmt: &str) -> Option<String> {
153 let dt = Local.timestamp_opt(epoch, 0).single()?;
154 Some(dt.format(fmt).to_string())
155}
156
157pub fn format_until_epoch_compact(target_epoch: i64, now_epoch: i64) -> Option<String> {
158 if target_epoch <= 0 || now_epoch <= 0 {
159 return None;
160 }
161 let remaining = target_epoch - now_epoch;
162 if remaining <= 0 {
163 return Some(format!("{:>2}h {:>2}m", 0, 0));
164 }
165
166 if remaining >= 86_400 {
167 let days = remaining / 86_400;
168 let hours = (remaining % 86_400) / 3_600;
169 return Some(format!("{:>2}d {:>2}h", days, hours));
170 }
171
172 let hours = remaining / 3_600;
173 let minutes = (remaining % 3_600) / 60;
174 Some(format!("{:>2}h {:>2}m", hours, minutes))
175}
176
177fn remaining_percent(used_percent: f64) -> i64 {
178 let remaining = 100.0 - used_percent;
179 remaining.round() as i64
180}