Skip to content

Commit 77bd36d

Browse files
committed
Prepare 1.2.11 changes on pr-6
1 parent 40d3d1f commit 77bd36d

File tree

7 files changed

+151
-64
lines changed

7 files changed

+151
-64
lines changed

.github/animation.gif

93.2 KB
Loading

.github/screenshot.png

-12 KB
Binary file not shown.

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
[package]
22
name = "claude-code-usage-monitor"
3-
version = "1.2.9"
3+
version = "1.2.11"
44
edition = "2021"
55
license = "MIT"
6-
description = "Windows taskbar widget for monitoring Claude Code usage and rate limits"
6+
description = "Claude Code Usage Monitor"
77
homepage = "https://codezeno.com.au"
88
repository = "https://github.com/CodeZeno/Claude-Code-Usage-Monitor"
99

1010
[package.metadata.winres]
1111
CompanyName = "Code Zeno Pty Ltd"
1212
ProductName = "Claude Code Usage Monitor"
13-
FileDescription = "Windows taskbar widget for monitoring Claude Code usage and rate limits"
13+
FileDescription = "Claude Code Usage Monitor"
1414
OriginalFilename = "claude-code-usage-monitor.exe"
1515
InternalName = "ClaudeCodeUsageMonitor"
1616
Comments = "https://codezeno.com.au"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ It sits in your taskbar and shows how much of your Claude Code usage window you
77
![Windows](https://img.shields.io/badge/platform-Windows-blue)
88
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
99

10-
![Screenshot](.github/screenshot.png)
10+
![Screenshot](.github/animation.gif)
1111

1212
## What You Get
1313

src/tray_icon.rs

Lines changed: 86 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
use windows::Win32::Foundation::*;
22
use windows::Win32::Graphics::Gdi::*;
3+
use windows::Win32::System::LibraryLoader::GetModuleFileNameW;
34
use windows::Win32::UI::Shell::{
4-
NIF_ICON, NIF_MESSAGE, NIF_TIP, NIM_ADD, NIM_DELETE, NIM_MODIFY, NOTIFYICONDATAW,
5-
Shell_NotifyIconW,
5+
ExtractIconExW, NIF_ICON, NIF_MESSAGE, NIF_TIP, NIM_ADD, NIM_DELETE, NIM_MODIFY,
6+
NOTIFYICONDATAW, Shell_NotifyIconW,
67
};
78
use windows::Win32::UI::WindowsAndMessaging::*;
89
use windows::core::PCWSTR;
@@ -21,45 +22,65 @@ pub enum TrayAction {
2122
ShowContextMenu,
2223
}
2324

25+
fn lerp_channel(start: u8, end: u8, t: f64) -> u8 {
26+
(start as f64 + (end as f64 - start as f64) * t.clamp(0.0, 1.0)).round() as u8
27+
}
28+
29+
fn lerp_color(start: Color, end: Color, t: f64) -> Color {
30+
Color::new(
31+
lerp_channel(start.r, end.r, t),
32+
lerp_channel(start.g, end.g, t),
33+
lerp_channel(start.b, end.b, t),
34+
)
35+
}
36+
37+
fn interpolated_fill(percent: f64) -> Color {
38+
if percent <= 50.0 {
39+
return Color::from_hex("#D97757");
40+
}
41+
42+
let stops = [
43+
(50.0, Color::from_hex("#D97757")),
44+
(70.0, Color::from_hex("#D08540")),
45+
(85.0, Color::from_hex("#CC8C20")),
46+
(95.0, Color::from_hex("#C45020")),
47+
(100.0, Color::from_hex("#B82020")),
48+
];
49+
50+
for pair in stops.windows(2) {
51+
let (start_pct, start_color) = pair[0];
52+
let (end_pct, end_color) = pair[1];
53+
if percent <= end_pct {
54+
let span = (end_pct - start_pct).max(f64::EPSILON);
55+
let t = (percent - start_pct) / span;
56+
return lerp_color(start_color, end_color, t);
57+
}
58+
}
59+
60+
stops[stops.len() - 1].1
61+
}
62+
2463
/// Create a rounded-rectangle tray icon badge showing the usage percentage.
25-
/// `percent` = None means "no data" (gray "?"), Some(p) is the usage level.
64+
/// `percent` = None means "no data/loading" and uses the embedded app icon.
2665
pub fn create_icon(percent: Option<f64>) -> HICON {
66+
if percent.is_none() {
67+
let app_icon = load_embedded_app_icon();
68+
if !app_icon.is_invalid() {
69+
return app_icon;
70+
}
71+
}
72+
2773
let size = 64_i32;
28-
let margin = 4_i32;
29-
let radius = 14_i32;
30-
let outline = 2_i32;
31-
32-
let (fill, outline_col, text_col) = match percent {
33-
None => (
34-
Color::from_hex("#6c757d"),
35-
Color::from_hex("#495057"),
36-
Color::from_hex("#FFFFFF"),
37-
),
38-
Some(p) if p < 50.0 => (
39-
Color::from_hex("#28a745"),
40-
Color::from_hex("#1e7e34"),
41-
Color::from_hex("#FFFFFF"),
42-
),
43-
Some(p) if p < 75.0 => (
44-
Color::from_hex("#ffc107"),
45-
Color::from_hex("#e0a800"),
46-
Color::from_hex("#1a1a1a"),
47-
),
48-
Some(p) if p < 90.0 => (
49-
Color::from_hex("#fd7e14"),
50-
Color::from_hex("#d9650a"),
51-
Color::from_hex("#FFFFFF"),
52-
),
53-
_ => (
54-
Color::from_hex("#dc3545"),
55-
Color::from_hex("#bd2130"),
56-
Color::from_hex("#FFFFFF"),
57-
),
58-
};
74+
let margin = 0_i32;
75+
let radius = 2_i32;
76+
let outline = 0_i32;
77+
78+
let fill = interpolated_fill(percent.unwrap_or(0.0));
79+
let text_col = Color::from_hex("#FFFFFF");
5980

6081
let display_text = match percent {
61-
None => "?".to_string(),
6282
Some(p) => format!("{}", p as u32),
83+
None => String::new(),
6384
};
6485

6586
let font_h = match display_text.len() {
@@ -108,22 +129,9 @@ pub fn create_icon(percent: Option<f64>) -> HICON {
108129
let null_pen = GetStockObject(NULL_PEN);
109130
let old_pen = SelectObject(mem_dc, null_pen);
110131

111-
// Outer rounded rect = outline colour
112-
let br_outline = CreateSolidBrush(COLORREF(outline_col.to_colorref()));
113-
let old_brush = SelectObject(mem_dc, br_outline);
114-
let _ = RoundRect(
115-
mem_dc,
116-
margin,
117-
margin,
118-
size - margin + 1,
119-
size - margin + 1,
120-
radius * 2,
121-
radius * 2,
122-
);
123-
124-
// Inner rounded rect = fill colour
132+
// Fill rounded rect
125133
let br_fill = CreateSolidBrush(COLORREF(fill.to_colorref()));
126-
SelectObject(mem_dc, br_fill);
134+
let old_brush = SelectObject(mem_dc, br_fill);
127135
let _ = RoundRect(
128136
mem_dc,
129137
margin + outline,
@@ -136,7 +144,6 @@ pub fn create_icon(percent: Option<f64>) -> HICON {
136144

137145
SelectObject(mem_dc, old_brush);
138146
SelectObject(mem_dc, old_pen);
139-
let _ = DeleteObject(br_outline);
140147
let _ = DeleteObject(br_fill);
141148

142149
// Draw centered percentage text
@@ -214,6 +221,34 @@ pub fn create_icon(percent: Option<f64>) -> HICON {
214221
}
215222
}
216223

224+
fn load_embedded_app_icon() -> HICON {
225+
unsafe {
226+
let mut exe_buf = [0u16; 260];
227+
let len = GetModuleFileNameW(None, &mut exe_buf) as usize;
228+
if len == 0 {
229+
return HICON::default();
230+
}
231+
232+
let mut small_icon = HICON::default();
233+
let mut large_icon = HICON::default();
234+
let extracted = ExtractIconExW(
235+
PCWSTR::from_raw(exe_buf.as_ptr()),
236+
0,
237+
Some(&mut large_icon),
238+
Some(&mut small_icon),
239+
1,
240+
);
241+
242+
if extracted == 0 {
243+
HICON::default()
244+
} else if !small_icon.is_invalid() {
245+
small_icon
246+
} else {
247+
large_icon
248+
}
249+
}
250+
}
251+
217252
/// Register the tray icon with the shell.
218253
pub fn add(hwnd: HWND, percent: Option<f64>, tooltip: &str) {
219254
let hicon = create_icon(percent);

src/window.rs

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use windows::Win32::System::Threading::CreateMutexW;
1313
use windows::Win32::UI::Accessibility::HWINEVENTHOOK;
1414
use windows::Win32::UI::HiDpi::*;
1515
use windows::Win32::UI::Input::KeyboardAndMouse::{ReleaseCapture, SetCapture};
16+
use windows::Win32::UI::Shell::ExtractIconExW;
1617
use windows::Win32::UI::WindowsAndMessaging::*;
1718

1819
use crate::diagnose;
@@ -136,6 +137,32 @@ fn refresh_dpi() {
136137
}
137138
}
138139

140+
fn load_embedded_app_icons() -> (HICON, HICON) {
141+
unsafe {
142+
let mut exe_buf = [0u16; 260];
143+
let len = GetModuleFileNameW(None, &mut exe_buf) as usize;
144+
if len == 0 {
145+
return (HICON::default(), HICON::default());
146+
}
147+
148+
let mut large_icon = HICON::default();
149+
let mut small_icon = HICON::default();
150+
let extracted = ExtractIconExW(
151+
PCWSTR::from_raw(exe_buf.as_ptr()),
152+
0,
153+
Some(&mut large_icon),
154+
Some(&mut small_icon),
155+
1,
156+
);
157+
158+
if extracted == 0 {
159+
(HICON::default(), HICON::default())
160+
} else {
161+
(large_icon, small_icon)
162+
}
163+
}
164+
}
165+
139166
unsafe impl Send for AppState {}
140167

141168
static STATE: Mutex<Option<AppState>> = Mutex::new(None);
@@ -742,12 +769,15 @@ pub fn run() {
742769

743770
unsafe {
744771
let hinstance = GetModuleHandleW(PCWSTR::null()).unwrap();
772+
let (large_icon, small_icon) = load_embedded_app_icons();
745773

746774
let wc = WNDCLASSEXW {
747775
cbSize: std::mem::size_of::<WNDCLASSEXW>() as u32,
748776
style: CS_HREDRAW | CS_VREDRAW,
749777
lpfnWndProc: Some(wnd_proc),
750778
hInstance: HINSTANCE(hinstance.0),
779+
hIcon: large_icon,
780+
hIconSm: small_icon,
751781
hCursor: LoadCursorW(HINSTANCE::default(), IDC_ARROW).unwrap_or_default(),
752782
hbrBackground: HBRUSH(std::ptr::null_mut()),
753783
lpszClassName: PCWSTR::from_raw(class_name.as_ptr()),
@@ -781,6 +811,24 @@ pub fn run() {
781811
None,
782812
)
783813
.unwrap();
814+
815+
if !large_icon.is_invalid() {
816+
let _ = SendMessageW(
817+
hwnd,
818+
WM_SETICON,
819+
WPARAM(ICON_BIG as usize),
820+
LPARAM(large_icon.0 as isize),
821+
);
822+
}
823+
if !small_icon.is_invalid() {
824+
let _ = SendMessageW(
825+
hwnd,
826+
WM_SETICON,
827+
WPARAM(ICON_SMALL as usize),
828+
LPARAM(small_icon.0 as isize),
829+
);
830+
}
831+
784832
diagnose::log(format!("main window created hwnd={:?}", hwnd));
785833

786834
let is_dark = theme::is_dark_mode();
@@ -1374,25 +1422,31 @@ fn position_at_taskbar() {
13741422
let widget_width = total_widget_width();
13751423

13761424
let widget_height = sc(WIDGET_HEIGHT);
1425+
let y = compute_anchor_y(anchor_top, anchor_height, widget_height);
13771426
if embedded {
13781427
// Child window: coordinates relative to parent (taskbar)
13791428
let x = tray_left - taskbar_rect.left - widget_width - tray_offset;
1380-
let y = (anchor_top - taskbar_rect.top) + (anchor_height - widget_height) / 2;
1381-
native_interop::move_window(hwnd, x, y, widget_width, widget_height);
1429+
native_interop::move_window(hwnd, x, y - taskbar_rect.top, widget_width, widget_height);
13821430
diagnose::log(format!(
1383-
"positioned embedded widget at x={x} y={y} w={widget_width} h={widget_height}"
1431+
"positioned embedded widget at x={x} y={} w={widget_width} h={widget_height}",
1432+
y - taskbar_rect.top
13841433
));
13851434
} else {
13861435
// Topmost popup: screen coordinates
13871436
let x = tray_left - widget_width - tray_offset;
1388-
let y = anchor_top + (anchor_height - widget_height) / 2;
13891437
native_interop::move_window(hwnd, x, y, widget_width, widget_height);
13901438
diagnose::log(format!(
13911439
"positioned fallback widget at x={x} y={y} w={widget_width} h={widget_height}"
13921440
));
13931441
}
13941442
}
13951443

1444+
fn compute_anchor_y(anchor_top: i32, anchor_height: i32, widget_height: i32) -> i32 {
1445+
let anchor_bottom = anchor_top + anchor_height;
1446+
let bottom_padding = (anchor_height - widget_height).clamp(0, sc(6));
1447+
(anchor_bottom - widget_height - bottom_padding).max(anchor_top)
1448+
}
1449+
13961450
/// WinEvent callback for tray icon location changes
13971451
unsafe extern "system" fn on_tray_location_changed(
13981452
_hook: HWINEVENTHOOK,
@@ -1627,20 +1681,18 @@ unsafe extern "system" fn wnd_proc(
16271681
}
16281682
let widget_width = total_widget_width();
16291683
let widget_height = sc(WIDGET_HEIGHT);
1684+
let y = compute_anchor_y(anchor_top, anchor_height, widget_height);
16301685
if s.embedded {
16311686
let x = tray_left - taskbar_rect.left - widget_width - new_offset;
1632-
let y =
1633-
(anchor_top - taskbar_rect.top) + (anchor_height - widget_height) / 2;
16341687
native_interop::move_window(
16351688
hwnd_val,
16361689
x,
1637-
y,
1690+
y - taskbar_rect.top,
16381691
widget_width,
16391692
widget_height,
16401693
);
16411694
} else {
16421695
let x = tray_left - widget_width - new_offset;
1643-
let y = anchor_top + (anchor_height - widget_height) / 2;
16441696
native_interop::move_window(
16451697
hwnd_val,
16461698
x,

0 commit comments

Comments
 (0)