Prepare 1.2.11 changes on pr-6

This commit is contained in:
Craig Constable 2026-03-31 08:00:25 +10:00
parent 40d3d1f7e1
commit 77bd36d3e0
7 changed files with 153 additions and 66 deletions

BIN
.github/animation.gif vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

BIN
.github/screenshot.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

2
Cargo.lock generated
View File

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

View File

@ -1,16 +1,16 @@
[package]
name = "claude-code-usage-monitor"
version = "1.2.9"
version = "1.2.11"
edition = "2021"
license = "MIT"
description = "Windows taskbar widget for monitoring Claude Code usage and rate limits"
description = "Claude Code Usage Monitor"
homepage = "https://codezeno.com.au"
repository = "https://github.com/CodeZeno/Claude-Code-Usage-Monitor"
[package.metadata.winres]
CompanyName = "Code Zeno Pty Ltd"
ProductName = "Claude Code Usage Monitor"
FileDescription = "Windows taskbar widget for monitoring Claude Code usage and rate limits"
FileDescription = "Claude Code Usage Monitor"
OriginalFilename = "claude-code-usage-monitor.exe"
InternalName = "ClaudeCodeUsageMonitor"
Comments = "https://codezeno.com.au"

View File

@ -7,7 +7,7 @@ It sits in your taskbar and shows how much of your Claude Code usage window you
![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/screenshot.png)
![Screenshot](.github/animation.gif)
## What You Get

View File

@ -1,8 +1,9 @@
use windows::Win32::Foundation::*;
use windows::Win32::Graphics::Gdi::*;
use windows::Win32::System::LibraryLoader::GetModuleFileNameW;
use windows::Win32::UI::Shell::{
NIF_ICON, NIF_MESSAGE, NIF_TIP, NIM_ADD, NIM_DELETE, NIM_MODIFY, NOTIFYICONDATAW,
Shell_NotifyIconW,
ExtractIconExW, NIF_ICON, NIF_MESSAGE, NIF_TIP, NIM_ADD, NIM_DELETE, NIM_MODIFY,
NOTIFYICONDATAW, Shell_NotifyIconW,
};
use windows::Win32::UI::WindowsAndMessaging::*;
use windows::core::PCWSTR;
@ -21,45 +22,65 @@ pub enum TrayAction {
ShowContextMenu,
}
/// Create a rounded-rectangle tray icon badge showing the usage percentage.
/// `percent` = None means "no data" (gray "?"), Some(p) is the usage level.
pub fn create_icon(percent: Option<f64>) -> HICON {
let size = 64_i32;
let margin = 4_i32;
let radius = 14_i32;
let outline = 2_i32;
fn lerp_channel(start: u8, end: u8, t: f64) -> u8 {
(start as f64 + (end as f64 - start as f64) * t.clamp(0.0, 1.0)).round() as u8
}
let (fill, outline_col, text_col) = match percent {
None => (
Color::from_hex("#6c757d"),
Color::from_hex("#495057"),
Color::from_hex("#FFFFFF"),
),
Some(p) if p < 50.0 => (
Color::from_hex("#28a745"),
Color::from_hex("#1e7e34"),
Color::from_hex("#FFFFFF"),
),
Some(p) if p < 75.0 => (
Color::from_hex("#ffc107"),
Color::from_hex("#e0a800"),
Color::from_hex("#1a1a1a"),
),
Some(p) if p < 90.0 => (
Color::from_hex("#fd7e14"),
Color::from_hex("#d9650a"),
Color::from_hex("#FFFFFF"),
),
_ => (
Color::from_hex("#dc3545"),
Color::from_hex("#bd2130"),
Color::from_hex("#FFFFFF"),
),
};
fn lerp_color(start: Color, end: Color, t: f64) -> Color {
Color::new(
lerp_channel(start.r, end.r, t),
lerp_channel(start.g, end.g, t),
lerp_channel(start.b, end.b, t),
)
}
fn interpolated_fill(percent: f64) -> Color {
if percent <= 50.0 {
return Color::from_hex("#D97757");
}
let stops = [
(50.0, Color::from_hex("#D97757")),
(70.0, Color::from_hex("#D08540")),
(85.0, Color::from_hex("#CC8C20")),
(95.0, Color::from_hex("#C45020")),
(100.0, Color::from_hex("#B82020")),
];
for pair in stops.windows(2) {
let (start_pct, start_color) = pair[0];
let (end_pct, end_color) = pair[1];
if percent <= end_pct {
let span = (end_pct - start_pct).max(f64::EPSILON);
let t = (percent - start_pct) / span;
return lerp_color(start_color, end_color, t);
}
}
stops[stops.len() - 1].1
}
/// Create a rounded-rectangle tray icon badge showing the usage percentage.
/// `percent` = None means "no data/loading" and uses the embedded app icon.
pub fn create_icon(percent: Option<f64>) -> HICON {
if percent.is_none() {
let app_icon = load_embedded_app_icon();
if !app_icon.is_invalid() {
return app_icon;
}
}
let size = 64_i32;
let margin = 0_i32;
let radius = 2_i32;
let outline = 0_i32;
let fill = interpolated_fill(percent.unwrap_or(0.0));
let text_col = Color::from_hex("#FFFFFF");
let display_text = match percent {
None => "?".to_string(),
Some(p) => format!("{}", p as u32),
None => String::new(),
};
let font_h = match display_text.len() {
@ -108,22 +129,9 @@ pub fn create_icon(percent: Option<f64>) -> HICON {
let null_pen = GetStockObject(NULL_PEN);
let old_pen = SelectObject(mem_dc, null_pen);
// Outer rounded rect = outline colour
let br_outline = CreateSolidBrush(COLORREF(outline_col.to_colorref()));
let old_brush = SelectObject(mem_dc, br_outline);
let _ = RoundRect(
mem_dc,
margin,
margin,
size - margin + 1,
size - margin + 1,
radius * 2,
radius * 2,
);
// Inner rounded rect = fill colour
// Fill rounded rect
let br_fill = CreateSolidBrush(COLORREF(fill.to_colorref()));
SelectObject(mem_dc, br_fill);
let old_brush = SelectObject(mem_dc, br_fill);
let _ = RoundRect(
mem_dc,
margin + outline,
@ -136,7 +144,6 @@ pub fn create_icon(percent: Option<f64>) -> HICON {
SelectObject(mem_dc, old_brush);
SelectObject(mem_dc, old_pen);
let _ = DeleteObject(br_outline);
let _ = DeleteObject(br_fill);
// Draw centered percentage text
@ -214,6 +221,34 @@ pub fn create_icon(percent: Option<f64>) -> HICON {
}
}
fn load_embedded_app_icon() -> HICON {
unsafe {
let mut exe_buf = [0u16; 260];
let len = GetModuleFileNameW(None, &mut exe_buf) as usize;
if len == 0 {
return HICON::default();
}
let mut small_icon = HICON::default();
let mut large_icon = HICON::default();
let extracted = ExtractIconExW(
PCWSTR::from_raw(exe_buf.as_ptr()),
0,
Some(&mut large_icon),
Some(&mut small_icon),
1,
);
if extracted == 0 {
HICON::default()
} else if !small_icon.is_invalid() {
small_icon
} else {
large_icon
}
}
}
/// Register the tray icon with the shell.
pub fn add(hwnd: HWND, percent: Option<f64>, tooltip: &str) {
let hicon = create_icon(percent);

View File

@ -13,6 +13,7 @@ use windows::Win32::System::Threading::CreateMutexW;
use windows::Win32::UI::Accessibility::HWINEVENTHOOK;
use windows::Win32::UI::HiDpi::*;
use windows::Win32::UI::Input::KeyboardAndMouse::{ReleaseCapture, SetCapture};
use windows::Win32::UI::Shell::ExtractIconExW;
use windows::Win32::UI::WindowsAndMessaging::*;
use crate::diagnose;
@ -136,6 +137,32 @@ fn refresh_dpi() {
}
}
fn load_embedded_app_icons() -> (HICON, HICON) {
unsafe {
let mut exe_buf = [0u16; 260];
let len = GetModuleFileNameW(None, &mut exe_buf) as usize;
if len == 0 {
return (HICON::default(), HICON::default());
}
let mut large_icon = HICON::default();
let mut small_icon = HICON::default();
let extracted = ExtractIconExW(
PCWSTR::from_raw(exe_buf.as_ptr()),
0,
Some(&mut large_icon),
Some(&mut small_icon),
1,
);
if extracted == 0 {
(HICON::default(), HICON::default())
} else {
(large_icon, small_icon)
}
}
}
unsafe impl Send for AppState {}
static STATE: Mutex<Option<AppState>> = Mutex::new(None);
@ -742,12 +769,15 @@ pub fn run() {
unsafe {
let hinstance = GetModuleHandleW(PCWSTR::null()).unwrap();
let (large_icon, small_icon) = load_embedded_app_icons();
let wc = WNDCLASSEXW {
cbSize: std::mem::size_of::<WNDCLASSEXW>() as u32,
style: CS_HREDRAW | CS_VREDRAW,
lpfnWndProc: Some(wnd_proc),
hInstance: HINSTANCE(hinstance.0),
hIcon: large_icon,
hIconSm: small_icon,
hCursor: LoadCursorW(HINSTANCE::default(), IDC_ARROW).unwrap_or_default(),
hbrBackground: HBRUSH(std::ptr::null_mut()),
lpszClassName: PCWSTR::from_raw(class_name.as_ptr()),
@ -781,6 +811,24 @@ pub fn run() {
None,
)
.unwrap();
if !large_icon.is_invalid() {
let _ = SendMessageW(
hwnd,
WM_SETICON,
WPARAM(ICON_BIG as usize),
LPARAM(large_icon.0 as isize),
);
}
if !small_icon.is_invalid() {
let _ = SendMessageW(
hwnd,
WM_SETICON,
WPARAM(ICON_SMALL as usize),
LPARAM(small_icon.0 as isize),
);
}
diagnose::log(format!("main window created hwnd={:?}", hwnd));
let is_dark = theme::is_dark_mode();
@ -1374,18 +1422,18 @@ fn position_at_taskbar() {
let widget_width = total_widget_width();
let widget_height = sc(WIDGET_HEIGHT);
let y = compute_anchor_y(anchor_top, anchor_height, widget_height);
if embedded {
// Child window: coordinates relative to parent (taskbar)
let x = tray_left - taskbar_rect.left - widget_width - tray_offset;
let y = (anchor_top - taskbar_rect.top) + (anchor_height - widget_height) / 2;
native_interop::move_window(hwnd, x, y, widget_width, widget_height);
native_interop::move_window(hwnd, x, y - taskbar_rect.top, widget_width, widget_height);
diagnose::log(format!(
"positioned embedded widget at x={x} y={y} w={widget_width} h={widget_height}"
"positioned embedded widget at x={x} y={} w={widget_width} h={widget_height}",
y - taskbar_rect.top
));
} else {
// Topmost popup: screen coordinates
let x = tray_left - widget_width - tray_offset;
let y = anchor_top + (anchor_height - widget_height) / 2;
native_interop::move_window(hwnd, x, y, widget_width, widget_height);
diagnose::log(format!(
"positioned fallback widget at x={x} y={y} w={widget_width} h={widget_height}"
@ -1393,6 +1441,12 @@ 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)
}
/// WinEvent callback for tray icon location changes
unsafe extern "system" fn on_tray_location_changed(
_hook: HWINEVENTHOOK,
@ -1627,20 +1681,18 @@ unsafe extern "system" fn wnd_proc(
}
let widget_width = total_widget_width();
let widget_height = sc(WIDGET_HEIGHT);
let y = compute_anchor_y(anchor_top, anchor_height, widget_height);
if s.embedded {
let x = tray_left - taskbar_rect.left - widget_width - new_offset;
let y =
(anchor_top - taskbar_rect.top) + (anchor_height - widget_height) / 2;
native_interop::move_window(
hwnd_val,
x,
y,
y - taskbar_rect.top,
widget_width,
widget_height,
);
} else {
let x = tray_left - widget_width - new_offset;
let y = anchor_top + (anchor_height - widget_height) / 2;
native_interop::move_window(
hwnd_val,
x,