mirror of
https://github.com/CodeZeno/Claude-Code-Usage-Monitor.git
synced 2026-06-20 10:19:27 +08:00
Prepare 1.2.11 changes on pr-6
This commit is contained in:
parent
40d3d1f7e1
commit
77bd36d3e0
BIN
.github/animation.gif
vendored
Normal file
BIN
.github/animation.gif
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 93 KiB |
BIN
.github/screenshot.png
vendored
BIN
.github/screenshot.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -38,7 +38,7 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "claude-code-usage-monitor"
|
||||
version = "1.2.9"
|
||||
version = "1.2.11"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"native-tls",
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -7,7 +7,7 @@ It sits in your taskbar and shows how much of your Claude Code usage window you
|
||||

|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
||||

|
||||

|
||||
|
||||
## What You Get
|
||||
|
||||
|
||||
141
src/tray_icon.rs
141
src/tray_icon.rs
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user