Skip to content

Commit a8be4f8

Browse files
committed
feat: display enterprise account credit and spend usage rows
For users on an enterprise Claude plan (no five-hour / seven-day rate-limit buckets), the widget now shows two dedicated rows: Cr 9%·20/6 — credit remaining (%) and expiry date in system locale format Sp $12/$50 — spend used / spend limit For personal (Pro/Free) accounts the rows continue to show the normal 5h/7d rate-limit bars. Implementation notes: - Parses `cinder_cove` and `spend` fields from the usage endpoint response - Locale-aware date format via GetLocaleInfoW (respects separator and D/M order) - Disk cache at %APPDATA%\ClaudeCodeUsageMonitor\account_cache.json survives widget restarts; cleared when plan has no enterprise fields (prevents stale enterprise rows appearing for Pro users after a 429) - Tray tooltip uses dynamic row labels ("Cr"/"Sp" vs "5h"/"7d") - tray_icon.rs: guard empty text before DrawTextW to avoid GDI crash
1 parent 9b29972 commit a8be4f8

5 files changed

Lines changed: 397 additions & 29 deletions

File tree

src/models.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::time::SystemTime;
44
pub struct UsageSection {
55
pub percentage: f64,
66
pub resets_at: Option<SystemTime>,
7+
pub has_bucket: bool,
78
}
89

910
#[derive(Clone, Debug, Default)]
@@ -12,9 +13,18 @@ pub struct UsageData {
1213
pub weekly: UsageSection,
1314
}
1415

16+
#[derive(Clone, Debug, Default)]
17+
pub struct AccountUsage {
18+
pub credit_pct: f64,
19+
pub credit_expiry: Option<SystemTime>,
20+
pub spend_used: f64,
21+
pub spend_limit: f64,
22+
}
23+
1524
#[derive(Clone, Debug, Default)]
1625
pub struct AppUsageData {
1726
pub claude_code: Option<UsageData>,
1827
pub codex: Option<UsageData>,
1928
pub antigravity: Option<UsageData>,
29+
pub account: Option<AccountUsage>,
2030
}

src/native_interop.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
use windows::core::PCWSTR;
22
use windows::Win32::Foundation::{BOOL, HWND, LPARAM, RECT};
3+
use windows::Win32::Globalization::GetLocaleInfoW;
34
use windows::Win32::UI::Accessibility::{SetWinEventHook, UnhookWinEvent, HWINEVENTHOOK};
45
use windows::Win32::UI::Shell::{SHAppBarMessage, ABM_GETTASKBARPOS, APPBARDATA};
56
use windows::Win32::UI::WindowsAndMessaging::*;
67

8+
const LOCALE_USER_DEFAULT: u32 = 0x0400;
9+
// Short date format pattern (e.g. "M/d/yyyy")
10+
const LOCALE_SSHORTDATE: u32 = 0x001F;
11+
712
// Window style constants
813
pub const WS_POPUP_STYLE: u32 = 0x80000000;
914
pub const WS_CHILD_STYLE: u32 = 0x40000000;
@@ -181,6 +186,41 @@ pub fn wide_str(s: &str) -> Vec<u16> {
181186
s.encode_utf16().chain(std::iter::once(0)).collect()
182187
}
183188

189+
/// Format a month/day pair respecting the Windows system locale
190+
/// (separator, and whether day or month comes first).
191+
/// Returns e.g. "9/15" (en-US), "15/9" (en-GB), "15.9" (de-DE).
192+
pub fn format_month_day_locale(month: u8, day: u8) -> String {
193+
if let Some(pattern) = locale_short_date_pattern() {
194+
let lower = pattern.to_lowercase();
195+
// Find the separator: first non-alphabetic, non-quote character
196+
let sep = lower
197+
.chars()
198+
.find(|c| !c.is_alphabetic() && *c != '\'')
199+
.unwrap_or('/');
200+
// day-first when 'd' appears before 'm' in the pattern (e.g. "dd/MM/yyyy")
201+
let d_pos = lower.find('d');
202+
let m_pos = lower.find('m');
203+
return match (d_pos, m_pos) {
204+
(Some(d), Some(m)) if d < m => format!("{}{}{}", day, sep, month),
205+
(Some(_), Some(_)) => format!("{}{}{}", month, sep, day),
206+
_ => format!("{}/{}", month, day), // malformed pattern — safe fallback
207+
};
208+
}
209+
format!("{}/{}", month, day)
210+
}
211+
212+
fn locale_short_date_pattern() -> Option<String> {
213+
unsafe {
214+
let mut buf = [0u16; 256];
215+
let len = GetLocaleInfoW(LOCALE_USER_DEFAULT, LOCALE_SSHORTDATE, Some(&mut buf));
216+
if len > 1 && (len as usize) <= buf.len() {
217+
Some(String::from_utf16_lossy(&buf[..len as usize - 1]).to_string())
218+
} else {
219+
None
220+
}
221+
}
222+
}
223+
184224
/// COLORREF wrapper (RGB packed into u32)
185225
pub fn colorref(r: u8, g: u8, b: u8) -> u32 {
186226
r as u32 | (g as u32) << 8 | (b as u32) << 16

0 commit comments

Comments
 (0)