- Added a native system tray icon that shows current 5-hour usage as a percentage badge.

- Added tray icon tooltip support showing both 5h and 7d countdown/usage values.

- Added tray icon interactions: left-click toggles the taskbar widget, right-click opens the context menu.

- Anchored widget to the bottom of the taskbar

- Added embedded app icon loading so tray fallback uses the app's own icon.

- Updated countdown timer logic to stop cases where 61m is shown

- Updated README.md
This commit is contained in:
Craig Constable 2026-03-31 09:30:35 +10:00
parent 1d8ec6cad0
commit 643cc0a8b6
5 changed files with 43 additions and 63 deletions

2
Cargo.lock generated
View File

@ -38,7 +38,7 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "claude-code-usage-monitor"
version = "1.2.11"
version = "1.3.0"
dependencies = [
"dirs",
"native-tls",

View File

@ -1,6 +1,6 @@
[package]
name = "claude-code-usage-monitor"
version = "1.2.11"
version = "1.3.0"
edition = "2021"
license = "MIT"
description = "Claude Code Usage Monitor"

View File

@ -1,14 +1,14 @@
![Windows](https://img.shields.io/badge/platform-Windows-blue)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
# Claude Code Usage Monitor
![Screenshot](.github/animation.gif)
A lightweight Windows taskbar widget for people already using Claude Code.
It sits in your taskbar and shows how much of your Claude Code usage window you have left, without needing to open the terminal or the Claude site.
![Windows](https://img.shields.io/badge/platform-Windows-blue)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
![Screenshot](.github/animation.gif)
## What You Get
- A **5h** bar for your current 5-hour Claude usage window
@ -59,15 +59,7 @@ Once running, it will appear in your taskbar and as a tray icon in the notificat
### System Tray Icon
The tray icon shows your current 5-hour usage as a color-coded percentage badge:
| Color | Meaning |
|--------|-------------------|
| Green | Under 50% used |
| Yellow | 50–75% used |
| Orange | 75–90% used |
| Red | 90% or more used |
| Gray | No data available |
The tray icon shows your current 5-hour usage as a color-coded percentage badge.
Hovering over the tray icon shows a tooltip with both your 5h and 7d usage.

View File

@ -663,55 +663,48 @@ fn format_countdown(resets_at: Option<SystemTime>, strings: Strings) -> String {
Err(_) => return strings.now.to_string(),
};
let total_secs = remaining.as_secs();
let total_mins = total_secs / 60;
let total_hours = total_secs / 3600;
let total_days = total_secs / 86400;
if total_days >= 1 {
format!("{total_days}{}", strings.day_suffix)
} else if total_mins > 61 {
format!("{total_hours}{}", strings.hour_suffix)
} else if total_secs > 60 {
format!("{total_mins}{}", strings.minute_suffix)
} else {
format!("{total_secs}{}", strings.second_suffix)
}
format_countdown_from_secs(remaining.as_secs(), strings)
}
/// Calculate how long until the display text would change
pub fn time_until_display_change(resets_at: Option<SystemTime>) -> Option<Duration> {
let reset = resets_at?;
let remaining = reset.duration_since(SystemTime::now()).ok()?;
Some(time_until_display_change_from_secs(remaining.as_secs()))
}
let total_secs = remaining.as_secs();
fn format_countdown_from_secs(total_secs: u64, strings: Strings) -> String {
let total_mins = total_secs / 60;
let total_hours = total_secs / 3600;
let total_days = total_secs / 86400;
if total_secs <= 60 {
// Update every second during final countdown
return Some(Duration::from_secs(1));
}
let next_boundary = if total_days >= 1 {
Duration::from_secs(total_days * 86400)
} else if total_mins > 61 {
if total_hours > 1 {
Duration::from_secs(total_hours * 3600)
} else {
Duration::from_secs(61 * 60)
}
if total_days >= 1 {
format!("{total_days}{}", strings.day_suffix)
} else if total_hours >= 1 {
format!("{total_hours}{}", strings.hour_suffix)
} else if total_mins >= 1 {
format!("{total_mins}{}", strings.minute_suffix)
} else {
Duration::from_secs(total_mins * 60)
format!("{total_secs}{}", strings.second_suffix)
}
}
fn time_until_display_change_from_secs(total_secs: u64) -> Duration {
let total_mins = total_secs / 60;
let total_hours = total_secs / 3600;
let total_days = total_secs / 86400;
let current_bucket_start = if total_days >= 1 {
total_days * 86400
} else if total_hours >= 1 {
total_hours * 3600
} else if total_mins >= 1 {
total_mins * 60
} else {
total_secs
};
let delay = remaining.saturating_sub(next_boundary);
if delay > Duration::ZERO {
Some(delay + Duration::from_secs(1))
} else {
Some(Duration::from_secs(1))
}
Duration::from_secs(total_secs.saturating_sub(current_bucket_start) + 1)
}
/// Returns true if either section has reached "now" (reset time has passed).

View File

@ -1179,8 +1179,8 @@ fn paint_content(
let _ = DeleteObject(right_brush);
let content_x = sc(LEFT_DIVIDER_W) + sc(DIVIDER_RIGHT_MARGIN);
let row1_y = sc(5);
let row2_y = sc(5) + sc(SEGMENT_H) + sc(10);
let row2_y = height - sc(5) - sc(SEGMENT_H);
let row1_y = row2_y - sc(10) - sc(SEGMENT_H);
let _ = SetBkMode(hdc, TRANSPARENT);
let _ = SetTextColor(hdc, COLORREF(text_color.to_colorref()));
@ -1408,14 +1408,12 @@ fn position_at_taskbar() {
let taskbar_height = taskbar_rect.bottom - taskbar_rect.top;
let mut tray_left = taskbar_rect.right;
let mut anchor_top = taskbar_rect.top;
let mut anchor_height = taskbar_height;
let anchor_top = taskbar_rect.top;
let anchor_height = taskbar_height;
if let Some(tray_hwnd) = native_interop::find_child_window(taskbar_hwnd, "TrayNotifyWnd") {
if let Some(tray_rect) = native_interop::get_window_rect_safe(tray_hwnd) {
tray_left = tray_rect.left;
anchor_top = tray_rect.top;
anchor_height = tray_rect.bottom - tray_rect.top;
}
}
@ -1443,8 +1441,7 @@ fn position_at_taskbar() {
fn compute_anchor_y(anchor_top: i32, anchor_height: i32, widget_height: i32) -> i32 {
let anchor_bottom = anchor_top + anchor_height;
let bottom_padding = (anchor_height - widget_height).clamp(0, sc(6));
(anchor_bottom - widget_height - bottom_padding).max(anchor_top)
(anchor_bottom - widget_height).max(anchor_top)
}
/// WinEvent callback for tray icon location changes
@ -1667,16 +1664,14 @@ unsafe extern "system" fn wnd_proc(
if let Some(taskbar_rect) = native_interop::get_taskbar_rect(taskbar_hwnd) {
let taskbar_height = taskbar_rect.bottom - taskbar_rect.top;
let mut tray_left = taskbar_rect.right;
let mut anchor_top = taskbar_rect.top;
let mut anchor_height = taskbar_height;
let anchor_top = taskbar_rect.top;
let anchor_height = taskbar_height;
if let Some(tray_hwnd) =
native_interop::find_child_window(taskbar_hwnd, "TrayNotifyWnd")
{
if let Some(tray_rect) = native_interop::get_window_rect_safe(tray_hwnd)
{
tray_left = tray_rect.left;
anchor_top = tray_rect.top;
anchor_height = tray_rect.bottom - tray_rect.top;
}
}
let widget_width = total_widget_width();