use std::path::PathBuf; use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::{Mutex, MutexGuard}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use serde::{Deserialize, Serialize}; use windows::core::PCWSTR; use windows::Win32::Foundation::*; use windows::Win32::Graphics::Gdi::*; use windows::Win32::System::LibraryLoader::{GetModuleFileNameW, GetModuleHandleW}; use windows::Win32::System::Registry::*; 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; use crate::localization::{self, LanguageId, Strings}; use crate::models::AppUsageData; use crate::native_interop::{ self, Color, TIMER_COUNTDOWN, TIMER_POLL, TIMER_RESET_POLL, TIMER_UPDATE_CHECK, WM_APP_TRAY, WM_APP_USAGE_UPDATED, }; use crate::poller; use crate::theme; use crate::tray_icon; use crate::updater::{self, InstallChannel, ReleaseDescriptor, UpdateCheckResult}; /// Wrapper to make HWND sendable across threads (safe for PostMessage usage) #[derive(Clone, Copy)] struct SendHwnd(isize); unsafe impl Send for SendHwnd {} impl SendHwnd { fn from_hwnd(hwnd: HWND) -> Self { Self(hwnd.0 as isize) } fn to_hwnd(self) -> HWND { HWND(self.0 as *mut _) } } /// Shared application state struct AppState { hwnd: SendHwnd, taskbar_hwnd: Option, tray_notify_hwnd: Option, win_event_hook: Option, is_dark: bool, embedded: bool, language_override: Option, language: LanguageId, install_channel: InstallChannel, session_percent: f64, session_text: String, weekly_percent: f64, weekly_text: String, codex_session_percent: f64, codex_session_text: String, codex_weekly_percent: f64, codex_weekly_text: String, antigravity_session_percent: f64, antigravity_session_text: String, antigravity_weekly_percent: f64, antigravity_weekly_text: String, show_claude_code: bool, show_codex: bool, show_antigravity: bool, data: Option, poll_interval_ms: u32, retry_count: u32, force_notify_auth_error: bool, auth_error_paused_polling: bool, auth_watch_mode: poller::CredentialWatchMode, auth_watch_snapshot: poller::CredentialWatchSnapshot, last_poll_ok: bool, update_status: UpdateStatus, last_update_check_unix: Option, tray_offset: i32, dragging: bool, drag_start_mouse_x: i32, drag_start_offset: i32, widget_visible: bool, } #[derive(Clone, Debug)] enum UpdateStatus { Idle, Checking, Applying, UpToDate, Available(ReleaseDescriptor), } const RETRY_BASE_MS: u32 = 30_000; // 30 seconds const POLL_1_MIN: u32 = 60_000; const POLL_5_MIN: u32 = 300_000; const POLL_15_MIN: u32 = 900_000; const POLL_1_HOUR: u32 = 3_600_000; // Menu item IDs for update frequency const IDM_FREQ_1MIN: u16 = 10; const IDM_FREQ_5MIN: u16 = 11; const IDM_FREQ_15MIN: u16 = 12; const IDM_FREQ_1HOUR: u16 = 13; const IDM_START_WITH_WINDOWS: u16 = 20; const IDM_RESET_POSITION: u16 = 30; const IDM_VERSION_ACTION: u16 = 31; const IDM_LANG_SYSTEM: u16 = 40; const IDM_LANG_ENGLISH: u16 = 41; const IDM_LANG_DUTCH: u16 = 42; const IDM_LANG_SPANISH: u16 = 43; const IDM_LANG_FRENCH: u16 = 44; const IDM_LANG_GERMAN: u16 = 45; const IDM_LANG_JAPANESE: u16 = 46; const IDM_LANG_KOREAN: u16 = 47; const IDM_LANG_TRADITIONAL_CHINESE: u16 = 48; const IDM_LANG_RUSSIAN: u16 = 49; const IDM_LANG_PORTUGUESE_BRAZIL: u16 = 50; const IDM_MODEL_CLAUDE_CODE: u16 = 60; const IDM_MODEL_CODEX: u16 = 61; const IDM_MODEL_ANTIGRAVITY: u16 = 62; const DIVIDER_HIT_ZONE: i32 = 13; // LEFT_DIVIDER_W + DIVIDER_RIGHT_MARGIN const WM_DPICHANGED_MSG: u32 = 0x02E0; const WM_APP_UPDATE_CHECK_COMPLETE: u32 = WM_APP + 2; const TRAY_ICON_UPDATE_REPOSITION_SUPPRESS_MS: u64 = 750; static SUPPRESS_TRAY_REPOSITION_UNTIL: Mutex> = Mutex::new(None); /// Current system DPI (96 = 100% scaling, 144 = 150%, 192 = 200%, etc.) static CURRENT_DPI: AtomicU32 = AtomicU32::new(96); /// Scale a base pixel value (designed at 96 DPI) to the current DPI. fn sc(px: i32) -> i32 { let dpi = CURRENT_DPI.load(Ordering::Relaxed); (px as f64 * dpi as f64 / 96.0).round() as i32 } /// Re-query the monitor DPI for our window and update the cached value. /// Uses GetDpiForWindow which returns the live DPI (unlike GetDpiForSystem /// which is cached at process startup and never changes). fn refresh_dpi() { let hwnd = { let state = lock_state(); state.as_ref().map(|s| s.hwnd.to_hwnd()) }; if let Some(hwnd) = hwnd { let dpi = unsafe { GetDpiForWindow(hwnd) }; if dpi > 0 { CURRENT_DPI.store(dpi, Ordering::Relaxed); } } } 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> = Mutex::new(None); /// Lock STATE safely, recovering from poisoned mutex fn lock_state() -> MutexGuard<'static, Option> { STATE.lock().unwrap_or_else(|e| e.into_inner()) } fn settings_path() -> PathBuf { let appdata = std::env::var("APPDATA").unwrap_or_else(|_| ".".to_string()); PathBuf::from(appdata) .join("ClaudeCodeUsageMonitor") .join("settings.json") } #[derive(Debug, Serialize, Deserialize)] struct SettingsFile { #[serde(default)] tray_offset: i32, #[serde(default = "default_poll_interval")] poll_interval_ms: u32, #[serde(default, skip_serializing_if = "Option::is_none")] language: Option, #[serde(default, skip_serializing_if = "Option::is_none")] last_update_check_unix: Option, #[serde(default = "default_widget_visible")] widget_visible: bool, #[serde(default = "default_show_claude_code")] show_claude_code: bool, #[serde(default = "default_show_codex")] show_codex: bool, #[serde(default = "default_show_antigravity")] show_antigravity: bool, } impl Default for SettingsFile { fn default() -> Self { Self { tray_offset: 0, poll_interval_ms: default_poll_interval(), language: None, last_update_check_unix: None, widget_visible: true, show_claude_code: true, show_codex: false, show_antigravity: false, } } } fn default_poll_interval() -> u32 { POLL_15_MIN } fn default_widget_visible() -> bool { true } fn default_show_claude_code() -> bool { true } fn default_show_codex() -> bool { false } fn default_show_antigravity() -> bool { false } fn load_settings() -> SettingsFile { let content = match std::fs::read_to_string(settings_path()) { Ok(c) => c, Err(_) => return SettingsFile::default(), }; let mut settings: SettingsFile = serde_json::from_str(&content).unwrap_or_default(); if !settings.show_claude_code && !settings.show_codex && !settings.show_antigravity { settings.show_claude_code = true; } settings } fn save_settings(settings: &SettingsFile) { let path = settings_path(); if let Some(parent) = path.parent() { let _ = std::fs::create_dir_all(parent); } if let Ok(json) = serde_json::to_string_pretty(settings) { let _ = std::fs::write(path, json); } } fn save_state_settings() { let state = lock_state(); if let Some(s) = state.as_ref() { save_settings(&SettingsFile { tray_offset: s.tray_offset, poll_interval_ms: s.poll_interval_ms, language: s .language_override .map(|language| language.code().to_string()), last_update_check_unix: s.last_update_check_unix, widget_visible: s.widget_visible, show_claude_code: s.show_claude_code, show_codex: s.show_codex, show_antigravity: s.show_antigravity, }); } } fn tray_icon_data_from_state() -> Vec { let state = lock_state(); match state.as_ref() { Some(s) if s.last_poll_ok => { let mut icons = Vec::new(); if s.show_claude_code { icons.push(tray_icon::TrayIconData { kind: tray_icon::TrayIconKind::Claude, percent: Some(s.session_percent), tooltip: format!( "{} 5h: {} | 7d: {}", s.language.strings().claude_code_model, s.session_text, s.weekly_text ), }); } if s.show_codex { icons.push(tray_icon::TrayIconData { kind: tray_icon::TrayIconKind::Codex, percent: Some(s.codex_session_percent), tooltip: format!( "{} 5h: {} | 7d: {}", s.language.strings().codex_model, s.codex_session_text, s.codex_weekly_text ), }); } if s.show_antigravity { icons.push(tray_icon::TrayIconData { kind: tray_icon::TrayIconKind::Antigravity, percent: Some(s.antigravity_session_percent), tooltip: format!( "{} 5h: {} | 7d: {}", s.language.strings().antigravity_model, s.antigravity_session_text, s.antigravity_weekly_text ), }); } icons } Some(s) => { let mut icons = Vec::new(); if s.show_claude_code { icons.push(tray_icon::TrayIconData { kind: tray_icon::TrayIconKind::Claude, percent: None, tooltip: s.language.strings().window_title.to_string(), }); } if s.show_codex { icons.push(tray_icon::TrayIconData { kind: tray_icon::TrayIconKind::Codex, percent: None, tooltip: s.language.strings().codex_window_title.to_string(), }); } if s.show_antigravity { icons.push(tray_icon::TrayIconData { kind: tray_icon::TrayIconKind::Antigravity, percent: None, tooltip: s.language.strings().antigravity_window_title.to_string(), }); } icons } None => Vec::new(), } } fn sync_tray_icons(hwnd: HWND) { let icons = tray_icon_data_from_state(); tray_icon::sync(hwnd, &icons); } fn toggle_widget_visibility(hwnd: HWND) { let new_visible = { let mut state = lock_state(); if let Some(s) = state.as_mut() { s.widget_visible = !s.widget_visible; s.widget_visible } else { return; } }; save_state_settings(); unsafe { if new_visible { position_at_taskbar(); let _ = ShowWindow(hwnd, SW_SHOWNOACTIVATE); render_layered(); } else { let _ = ShowWindow(hwnd, SW_HIDE); } } } fn now_unix_secs() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs() } fn update_check_interval() -> Duration { Duration::from_secs(24 * 60 * 60) } fn auto_update_check_due(last_update_check_unix: Option) -> bool { let Some(last_update_check_unix) = last_update_check_unix else { return true; }; now_unix_secs().saturating_sub(last_update_check_unix) >= update_check_interval().as_secs() } fn schedule_auto_update_check(hwnd: HWND) { let delay_ms = { let state = lock_state(); let Some(s) = state.as_ref() else { return; }; if auto_update_check_due(s.last_update_check_unix) { None } else { let elapsed = now_unix_secs().saturating_sub(s.last_update_check_unix.unwrap_or(0)); let remaining_secs = update_check_interval().as_secs().saturating_sub(elapsed); Some((remaining_secs.saturating_mul(1000)).min(u32::MAX as u64) as u32) } }; unsafe { let _ = KillTimer(hwnd, TIMER_UPDATE_CHECK); if let Some(delay_ms) = delay_ms { SetTimer(hwnd, TIMER_UPDATE_CHECK, delay_ms.max(1), None); } } } fn refresh_usage_texts(state: &mut AppState) { if !state.last_poll_ok { return; } let strings = state.language.strings(); let Some(data) = state.data.as_ref() else { return; }; if let Some(claude_code) = data.claude_code.as_ref() { state.session_text = poller::format_line(&claude_code.session, strings); state.weekly_text = poller::format_line(&claude_code.weekly, strings); } else if state.show_claude_code { state.session_text = "!".to_string(); state.weekly_text = "!".to_string(); } if let Some(codex) = data.codex.as_ref() { state.codex_session_text = poller::format_line(&codex.session, strings); state.codex_weekly_text = poller::format_line(&codex.weekly, strings); } else if state.show_codex { state.codex_session_text = "!".to_string(); state.codex_weekly_text = "!".to_string(); } if let Some(antigravity) = data.antigravity.as_ref() { state.antigravity_session_text = poller::format_line(&antigravity.session, strings); state.antigravity_weekly_text = if antigravity.weekly.resets_at.is_none() && antigravity.weekly.percentage == 0.0 { "--".to_string() } else { poller::format_line(&antigravity.weekly, strings) }; } else if state.show_antigravity { state.antigravity_session_text = "!".to_string(); state.antigravity_weekly_text = "!".to_string(); } } fn set_window_title(hwnd: HWND, strings: Strings) { unsafe { let title = native_interop::wide_str(strings.window_title); let _ = SetWindowTextW(hwnd, PCWSTR::from_raw(title.as_ptr())); } } fn show_info_message(hwnd: HWND, title: &str, message: &str) { unsafe { let title_wide = native_interop::wide_str(title); let message_wide = native_interop::wide_str(message); let _ = MessageBoxW( hwnd, PCWSTR::from_raw(message_wide.as_ptr()), PCWSTR::from_raw(title_wide.as_ptr()), MB_OK | MB_ICONINFORMATION, ); } } fn show_error_message(hwnd: HWND, title: &str, message: &str) { unsafe { let title_wide = native_interop::wide_str(title); let message_wide = native_interop::wide_str(message); let _ = MessageBoxW( hwnd, PCWSTR::from_raw(message_wide.as_ptr()), PCWSTR::from_raw(title_wide.as_ptr()), MB_OK | MB_ICONERROR, ); } } fn show_update_prompt(hwnd: HWND, strings: Strings, release: &ReleaseDescriptor) -> bool { let message = strings .update_prompt_now .replace("{version}", &release.latest_version); unsafe { let title_wide = native_interop::wide_str(strings.update_available); let message_wide = native_interop::wide_str(&message); MessageBoxW( hwnd, PCWSTR::from_raw(message_wide.as_ptr()), PCWSTR::from_raw(title_wide.as_ptr()), MB_YESNO | MB_ICONQUESTION, ) == IDYES } } fn apply_language_to_state(state: &mut AppState, language_override: Option) { state.language_override = language_override; state.language = localization::resolve_language(language_override); set_window_title(state.hwnd.to_hwnd(), state.language.strings()); refresh_usage_texts(state); } fn update_language_change() -> bool { let mut state = lock_state(); let Some(app_state) = state.as_mut() else { return false; }; if app_state.language_override.is_some() { return false; } let new_language = localization::detect_system_language(); if new_language == app_state.language { return false; } apply_language_to_state(app_state, None); true } fn version_action_label( strings: Strings, language: LanguageId, install_channel: InstallChannel, status: &UpdateStatus, ) -> String { let current = env!("CARGO_PKG_VERSION"); match status { UpdateStatus::Idle => format!("v{current} - {}", strings.check_for_updates), UpdateStatus::Checking => format!("v{current} - {}", strings.checking_for_updates), UpdateStatus::Applying => format!("v{current} - {}", strings.applying_update), UpdateStatus::UpToDate => format!("v{current} - {}", strings.up_to_date_short), UpdateStatus::Available(release) => match install_channel { InstallChannel::Portable => { format!( "v{current} - {} v{}", strings.update_to, release.latest_version ) } InstallChannel::Winget => format!( "v{current} - {} v{}", localization::update_via_winget(language), release.latest_version ), }, } } fn begin_update_check(hwnd: HWND, interactive: bool) { let send_hwnd = SendHwnd::from_hwnd(hwnd); let (strings, install_channel) = { let mut state = lock_state(); let Some(app_state) = state.as_mut() else { return; }; if matches!( app_state.update_status, UpdateStatus::Checking | UpdateStatus::Applying ) { if interactive { show_info_message( hwnd, app_state.language.strings().updates, app_state.language.strings().update_in_progress, ); } return; } app_state.update_status = UpdateStatus::Checking; (app_state.language.strings(), app_state.install_channel) }; std::thread::spawn(move || { let hwnd = send_hwnd.to_hwnd(); let checked_at = now_unix_secs(); match updater::check_for_updates() { Ok(UpdateCheckResult::UpToDate) => { { let mut state = lock_state(); if let Some(s) = state.as_mut() { s.update_status = UpdateStatus::UpToDate; s.last_update_check_unix = Some(checked_at); } } save_state_settings(); if interactive { show_info_message(hwnd, strings.updates, strings.up_to_date); } unsafe { let _ = PostMessageW(hwnd, WM_APP_UPDATE_CHECK_COMPLETE, WPARAM(0), LPARAM(0)); } } Ok(UpdateCheckResult::Available(release)) => { { let mut state = lock_state(); if let Some(s) = state.as_mut() { s.update_status = UpdateStatus::Available(release.clone()); s.last_update_check_unix = Some(checked_at); } } save_state_settings(); if interactive && show_update_prompt(hwnd, strings, &release) { match install_channel { InstallChannel::Portable => begin_update_apply(hwnd, release), InstallChannel::Winget => begin_winget_update(hwnd), } } unsafe { let _ = PostMessageW(hwnd, WM_APP_UPDATE_CHECK_COMPLETE, WPARAM(0), LPARAM(0)); } } Err(error) => { { let mut state = lock_state(); if let Some(s) = state.as_mut() { s.update_status = UpdateStatus::Idle; s.last_update_check_unix = Some(checked_at); } } save_state_settings(); if interactive { let message = format!("{}.\n\n{}", strings.update_failed, error); show_error_message(hwnd, strings.updates, &message); } unsafe { let _ = PostMessageW(hwnd, WM_APP_UPDATE_CHECK_COMPLETE, WPARAM(0), LPARAM(0)); } } } }); } fn begin_update_apply(hwnd: HWND, release: ReleaseDescriptor) { let send_hwnd = SendHwnd::from_hwnd(hwnd); let strings = { let mut state = lock_state(); let Some(app_state) = state.as_mut() else { return; }; if matches!( app_state.update_status, UpdateStatus::Checking | UpdateStatus::Applying ) { show_info_message( hwnd, app_state.language.strings().updates, app_state.language.strings().update_in_progress, ); return; } app_state.update_status = UpdateStatus::Applying; app_state.language.strings() }; std::thread::spawn(move || { let hwnd = send_hwnd.to_hwnd(); match updater::begin_self_update(&release) { Ok(()) => unsafe { let _ = PostMessageW(hwnd, WM_CLOSE, WPARAM(0), LPARAM(0)); }, Err(error) => { { let mut state = lock_state(); if let Some(s) = state.as_mut() { s.update_status = UpdateStatus::Available(release); } } let message = format!("{}.\n\n{}", strings.update_failed, error); show_error_message(hwnd, strings.updates, &message); unsafe { let _ = PostMessageW(hwnd, WM_APP_UPDATE_CHECK_COMPLETE, WPARAM(0), LPARAM(0)); } } } }); } fn begin_winget_update(hwnd: HWND) { let strings = { let state = lock_state(); state.as_ref().map(|s| s.language.strings()) } .unwrap_or(LanguageId::English.strings()); match updater::begin_winget_update() { Ok(()) => unsafe { let _ = PostMessageW(hwnd, WM_CLOSE, WPARAM(0), LPARAM(0)); }, Err(error) => { let message = format!("{}.\n\n{}", strings.update_failed, error); show_error_message(hwnd, strings.updates, &message); } } } const STARTUP_REGISTRY_PATH: &str = r"Software\Microsoft\Windows\CurrentVersion\Run"; const STARTUP_REGISTRY_KEY: &str = "ClaudeCodeUsageMonitor"; /// Returns true only if the startup registry value points to this executable. fn is_startup_enabled() -> bool { unsafe { let path = native_interop::wide_str(STARTUP_REGISTRY_PATH); let key_name = native_interop::wide_str(STARTUP_REGISTRY_KEY); let mut hkey = HKEY::default(); let result = RegOpenKeyExW( HKEY_CURRENT_USER, PCWSTR::from_raw(path.as_ptr()), 0, KEY_READ, &mut hkey, ); if result.is_err() { return false; } // Query the size of the value let mut data_size: u32 = 0; let result = RegQueryValueExW( hkey, PCWSTR::from_raw(key_name.as_ptr()), None, None, None, Some(&mut data_size), ); if result.is_err() || data_size == 0 { let _ = RegCloseKey(hkey); return false; } // Read the value let mut buf = vec![0u8; data_size as usize]; let result = RegQueryValueExW( hkey, PCWSTR::from_raw(key_name.as_ptr()), None, None, Some(buf.as_mut_ptr()), Some(&mut data_size), ); let _ = RegCloseKey(hkey); if result.is_err() { return false; } // Convert the registry value (UTF-16) to a string let wide_slice = std::slice::from_raw_parts(buf.as_ptr() as *const u16, data_size as usize / 2); let reg_value = String::from_utf16_lossy(wide_slice) .trim_end_matches('\0') .to_string(); // Get the current executable path let mut exe_buf = [0u16; 260]; let len = GetModuleFileNameW(None, &mut exe_buf) as usize; if len == 0 { return false; } let current_exe = String::from_utf16_lossy(&exe_buf[..len]); // Case-insensitive comparison (Windows paths are case-insensitive) reg_value.eq_ignore_ascii_case(¤t_exe) } } fn set_startup_enabled(enable: bool) { unsafe { let path = native_interop::wide_str(STARTUP_REGISTRY_PATH); let mut hkey = HKEY::default(); let result = RegOpenKeyExW( HKEY_CURRENT_USER, PCWSTR::from_raw(path.as_ptr()), 0, KEY_SET_VALUE, &mut hkey, ); if result.is_err() { return; } let key_name = native_interop::wide_str(STARTUP_REGISTRY_KEY); if enable { let mut exe_buf = [0u16; 260]; let len = GetModuleFileNameW(None, &mut exe_buf) as usize; if len > 0 { // Write the wide string including null terminator let byte_len = ((len + 1) * 2) as u32; let _ = RegSetValueExW( hkey, PCWSTR::from_raw(key_name.as_ptr()), 0, REG_SZ, Some(std::slice::from_raw_parts( exe_buf.as_ptr() as *const u8, byte_len as usize, )), ); } } else { let _ = RegDeleteValueW(hkey, PCWSTR::from_raw(key_name.as_ptr())); } let _ = RegCloseKey(hkey); } } // Dimensions matching the C# version const SEGMENT_W: i32 = 10; const SEGMENT_H: i32 = 13; const SEGMENT_GAP: i32 = 1; const SEGMENT_COUNT: i32 = 10; const CORNER_RADIUS: i32 = 2; const LEFT_DIVIDER_W: i32 = 3; const DIVIDER_RIGHT_MARGIN: i32 = 10; const LABEL_WIDTH: i32 = 18; const LABEL_RIGHT_MARGIN: i32 = 10; const BAR_RIGHT_MARGIN: i32 = 4; const TEXT_WIDTH: i32 = 62; const MODEL_RIGHT_MARGIN: i32 = 5; const RIGHT_MARGIN: i32 = 1; const WIDGET_HEIGHT: i32 = 46; fn active_model_count(show_claude_code: bool, show_codex: bool, show_antigravity: bool) -> i32 { (show_claude_code as i32 + show_codex as i32 + show_antigravity as i32).max(1) } fn row_bar_segment_count(active_models: i32) -> i32 { if active_models > 1 { 5 } else { SEGMENT_COUNT } } fn total_widget_width_for(active_models: i32) -> i32 { let bar_segments = row_bar_segment_count(active_models); let model_width = (sc(SEGMENT_W) + sc(SEGMENT_GAP)) * bar_segments - sc(SEGMENT_GAP) + sc(BAR_RIGHT_MARGIN) + sc(TEXT_WIDTH); sc(LEFT_DIVIDER_W) + sc(DIVIDER_RIGHT_MARGIN) + sc(LABEL_WIDTH) + sc(LABEL_RIGHT_MARGIN) + model_width * active_models + sc(MODEL_RIGHT_MARGIN) * (active_models - 1) + sc(RIGHT_MARGIN) } fn total_widget_width_for_state(state: &AppState) -> i32 { total_widget_width_for(active_model_count( state.show_claude_code, state.show_codex, state.show_antigravity, )) } fn total_widget_width() -> i32 { let active_models = { let state = lock_state(); state .as_ref() .map(|s| active_model_count(s.show_claude_code, s.show_codex, s.show_antigravity)) .unwrap_or(1) }; total_widget_width_for(active_models) } fn claude_accent_color() -> Color { Color::from_hex("#D97757") } fn codex_accent_color(is_dark: bool) -> Color { if is_dark { Color::from_hex("#F5F5F5") } else { Color::from_hex("#1F1F1F") } } fn antigravity_accent_color() -> Color { Color::from_hex("#34A853") } fn claude_usage_text_color(is_dark: bool) -> Color { if is_dark { Color::from_hex("#F09A7A") } else { Color::from_hex("#A94F32") } } fn codex_usage_text_color(is_dark: bool) -> Color { if is_dark { Color::from_hex("#F5F5F5") } else { Color::from_hex("#1F1F1F") } } fn antigravity_usage_text_color(is_dark: bool) -> Color { if is_dark { Color::from_hex("#6FD58D") } else { Color::from_hex("#1E7D41") } } pub fn run() { // Enable Per-Monitor DPI Awareness V2 for crisp rendering at any scale factor unsafe { let _ = SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); CURRENT_DPI.store(GetDpiForSystem(), Ordering::Relaxed); } diagnose::log("window::run started"); // Single-instance guard: silently exit if another instance is running let mutex_name = native_interop::wide_str("Global\\ClaudeCodeUsageMonitor"); let _mutex = unsafe { let handle = CreateMutexW(None, false, PCWSTR::from_raw(mutex_name.as_ptr())); match handle { Ok(h) => { if GetLastError() == ERROR_ALREADY_EXISTS { diagnose::log("startup aborted: another instance is already running"); return; } h } Err(error) => { diagnose::log_error( "startup aborted: unable to create single-instance mutex", error, ); return; } } }; let class_name = native_interop::wide_str("ClaudeCodeUsageMonitor"); unsafe { let hinstance = GetModuleHandleW(PCWSTR::null()).unwrap(); let (large_icon, small_icon) = load_embedded_app_icons(); let wc = WNDCLASSEXW { cbSize: std::mem::size_of::() 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()), ..Default::default() }; let atom = RegisterClassExW(&wc); if atom == 0 { diagnose::log("RegisterClassExW returned 0"); } let settings = load_settings(); let language_override = settings.language.as_deref().and_then(LanguageId::from_code); let language = localization::resolve_language(language_override); let install_channel = updater::current_install_channel(); // Create as layered popup (will be reparented into taskbar) let title = native_interop::wide_str(language.strings().window_title); let initial_model_count = active_model_count( settings.show_claude_code, settings.show_codex, settings.show_antigravity, ); let hwnd = CreateWindowExW( WS_EX_TOOLWINDOW | WS_EX_LAYERED | WS_EX_NOACTIVATE, PCWSTR::from_raw(class_name.as_ptr()), PCWSTR::from_raw(title.as_ptr()), WS_POPUP, 0, 0, total_widget_width_for(initial_model_count), sc(WIDGET_HEIGHT), HWND::default(), HMENU::default(), hinstance, 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(); let mut embedded = false; { let mut state = lock_state(); *state = Some(AppState { hwnd: SendHwnd::from_hwnd(hwnd), taskbar_hwnd: None, tray_notify_hwnd: None, win_event_hook: None, is_dark, embedded: false, language_override, language, install_channel, session_percent: 0.0, session_text: "--".to_string(), weekly_percent: 0.0, weekly_text: "--".to_string(), codex_session_percent: 0.0, codex_session_text: "--".to_string(), codex_weekly_percent: 0.0, codex_weekly_text: "--".to_string(), antigravity_session_percent: 0.0, antigravity_session_text: "--".to_string(), antigravity_weekly_percent: 0.0, antigravity_weekly_text: "--".to_string(), show_claude_code: settings.show_claude_code, show_codex: settings.show_codex, show_antigravity: settings.show_antigravity, data: None, poll_interval_ms: settings.poll_interval_ms, retry_count: 0, force_notify_auth_error: false, auth_error_paused_polling: false, auth_watch_mode: poller::CredentialWatchMode::ActiveSource, auth_watch_snapshot: Vec::new(), last_poll_ok: false, update_status: UpdateStatus::Idle, last_update_check_unix: settings.last_update_check_unix, tray_offset: settings.tray_offset, dragging: false, drag_start_mouse_x: 0, drag_start_offset: 0, widget_visible: settings.widget_visible, }); } // Try to embed in taskbar if let Some(taskbar_hwnd) = native_interop::find_taskbar() { diagnose::log(format!("taskbar found hwnd={:?}", taskbar_hwnd)); native_interop::embed_in_taskbar(hwnd, taskbar_hwnd); embedded = true; let mut state = lock_state(); let s = state.as_mut().unwrap(); s.taskbar_hwnd = Some(taskbar_hwnd); s.embedded = true; let tray_notify = native_interop::find_child_window(taskbar_hwnd, "TrayNotifyWnd"); s.tray_notify_hwnd = tray_notify; if tray_notify.is_some() { diagnose::log("TrayNotifyWnd found"); } else { diagnose::log("TrayNotifyWnd not found"); } if let Some(tray_hwnd) = tray_notify { let thread_id = native_interop::get_window_thread_id(tray_hwnd); let hook = native_interop::set_tray_event_hook(thread_id, on_tray_location_changed); s.win_event_hook = hook; if hook.is_some() { diagnose::log("tray event hook installed"); } else { diagnose::log("tray event hook could not be installed"); } } } else { diagnose::log("taskbar not found; using fallback popup window"); } // If not embedded, fall back to topmost popup with SetLayeredWindowAttributes if !embedded { let _ = SetLayeredWindowAttributes(hwnd, COLORREF(0), 255, LWA_ALPHA); let _ = SetWindowPos( hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE, ); } // Register system tray icon(s) sync_tray_icons(hwnd); // Position and show (only if widget_visible preference is true) position_at_taskbar(); if settings.widget_visible { let _ = ShowWindow(hwnd, SW_SHOWNOACTIVATE); } diagnose::log("window shown"); // Initial render via UpdateLayeredWindow (for embedded) or InvalidateRect (fallback) render_layered(); // Poll timer: 15 minutes let initial_poll_ms = { let state = lock_state(); state .as_ref() .map(|s| s.poll_interval_ms) .unwrap_or(POLL_15_MIN) }; SetTimer(hwnd, TIMER_POLL, initial_poll_ms, None); // Initial poll let send_hwnd = SendHwnd::from_hwnd(hwnd); std::thread::spawn(move || { diagnose::log("initial poll thread started"); do_poll(send_hwnd); }); schedule_auto_update_check(hwnd); let should_check_updates = { let state = lock_state(); state .as_ref() .map(|s| auto_update_check_due(s.last_update_check_unix)) .unwrap_or(false) }; if should_check_updates { begin_update_check(hwnd, false); } // Initial theme check check_theme_change(); // Message loop let mut msg = MSG::default(); while GetMessageW(&mut msg, HWND::default(), 0, 0).as_bool() { let _ = TranslateMessage(&msg); DispatchMessageW(&msg); } } } /// Render widget content and push to the layered window via UpdateLayeredWindow. /// Renders fully opaque with the actual taskbar background colour so that /// ClearType sub-pixel font rendering can be used for crisp, OS-native text. fn render_layered() { refresh_dpi(); let ( hwnd_val, is_dark, embedded, strings, session_pct, session_text, weekly_pct, weekly_text, codex_session_pct, codex_session_text, codex_weekly_pct, codex_weekly_text, antigravity_session_pct, antigravity_session_text, antigravity_weekly_pct, antigravity_weekly_text, show_claude_code, show_codex, show_antigravity, ) = { let state = lock_state(); match state.as_ref() { Some(s) => ( s.hwnd, s.is_dark, s.embedded, s.language.strings(), s.session_percent, s.session_text.clone(), s.weekly_percent, s.weekly_text.clone(), s.codex_session_percent, s.codex_session_text.clone(), s.codex_weekly_percent, s.codex_weekly_text.clone(), s.antigravity_session_percent, s.antigravity_session_text.clone(), s.antigravity_weekly_percent, s.antigravity_weekly_text.clone(), s.show_claude_code, s.show_codex, s.show_antigravity, ), None => return, } }; let hwnd = hwnd_val.to_hwnd(); // For non-embedded fallback, just invalidate and let WM_PAINT handle it if !embedded { unsafe { let _ = InvalidateRect(hwnd, None, false); } return; } let width = total_widget_width(); let height = sc(WIDGET_HEIGHT); let accent = claude_accent_color(); let codex_accent = codex_accent_color(is_dark); let antigravity_accent = antigravity_accent_color(); let track = if is_dark { Color::from_hex("#444444") } else { Color::from_hex("#AAAAAA") }; let text_color = if is_dark { Color::from_hex("#888888") } else { Color::from_hex("#404040") }; let bg_color = if is_dark { Color::from_hex("#1C1C1C") } else { Color::from_hex("#F3F3F3") }; unsafe { let screen_dc = GetDC(hwnd); let bmi = BITMAPINFO { bmiHeader: BITMAPINFOHEADER { biSize: std::mem::size_of::() as u32, biWidth: width, biHeight: -height, // top-down biPlanes: 1, biBitCount: 32, biCompression: 0, // BI_RGB ..Default::default() }, ..Default::default() }; let mut bits: *mut std::ffi::c_void = std::ptr::null_mut(); let mem_dc = CreateCompatibleDC(screen_dc); let dib = CreateDIBSection(mem_dc, &bmi, DIB_RGB_COLORS, &mut bits, None, 0).unwrap_or_default(); if dib.is_invalid() || bits.is_null() { let _ = DeleteDC(mem_dc); ReleaseDC(hwnd, screen_dc); return; } let old_bmp = SelectObject(mem_dc, dib); let pixel_count = (width * height) as usize; // Render once with the actual taskbar background colour. // Using an opaque background lets us use CLEARTYPE_QUALITY for // sub-pixel font rendering that matches the rest of the OS. paint_content( mem_dc, width, height, is_dark, &bg_color, &text_color, &accent, &track, strings, session_pct, &session_text, weekly_pct, &weekly_text, codex_session_pct, &codex_session_text, codex_weekly_pct, &codex_weekly_text, antigravity_session_pct, &antigravity_session_text, antigravity_weekly_pct, &antigravity_weekly_text, show_claude_code, show_codex, show_antigravity, &codex_accent, &antigravity_accent, ); // Background pixels → alpha 1 (nearly invisible but still hittable for right-click). // Content pixels → fully opaque (preserves ClearType sub-pixel rendering). let bg_bgr = bg_color.to_colorref(); let pixel_data = std::slice::from_raw_parts_mut(bits as *mut u32, pixel_count); for px in pixel_data.iter_mut() { let rgb = *px & 0x00FFFFFF; if rgb == bg_bgr { *px = 0x01000000; } else { *px = rgb | 0xFF000000; } } // Push to window via UpdateLayeredWindow let pt_src = POINT { x: 0, y: 0 }; let sz = SIZE { cx: width, cy: height, }; let blend = BLENDFUNCTION { BlendOp: 0, // AC_SRC_OVER BlendFlags: 0, SourceConstantAlpha: 255, AlphaFormat: 1, // AC_SRC_ALPHA }; let _ = UpdateLayeredWindow( hwnd, screen_dc, None, Some(&sz), mem_dc, Some(&pt_src), COLORREF(0), Some(&blend), ULW_ALPHA, ); // Cleanup SelectObject(mem_dc, old_bmp); let _ = DeleteObject(dib); let _ = DeleteDC(mem_dc); ReleaseDC(hwnd, screen_dc); } } /// Paint all widget content onto a DC with a given background color. fn paint_content( hdc: HDC, width: i32, height: i32, is_dark: bool, bg: &Color, text_color: &Color, accent: &Color, track: &Color, strings: Strings, session_pct: f64, session_text: &str, weekly_pct: f64, weekly_text: &str, codex_session_pct: f64, codex_session_text: &str, codex_weekly_pct: f64, codex_weekly_text: &str, antigravity_session_pct: f64, antigravity_session_text: &str, antigravity_weekly_pct: f64, antigravity_weekly_text: &str, show_claude_code: bool, show_codex: bool, show_antigravity: bool, codex_accent: &Color, antigravity_accent: &Color, ) { unsafe { let client_rect = RECT { left: 0, top: 0, right: width, bottom: height, }; let bg_brush = CreateSolidBrush(COLORREF(bg.to_colorref())); FillRect(hdc, &client_rect, bg_brush); let _ = DeleteObject(bg_brush); // Left divider let divider_h = sc(25); let divider_top = (height - divider_h) / 2; let divider_bottom = divider_top + divider_h; let (div_left, div_right) = if is_dark { ((80, 80, 80), (40, 40, 40)) } else { ((160, 160, 160), (230, 230, 230)) }; let left_brush = CreateSolidBrush(COLORREF(native_interop::colorref( div_left.0, div_left.1, div_left.2, ))); let left_rect = RECT { left: 0, top: divider_top, right: sc(2), bottom: divider_bottom, }; FillRect(hdc, &left_rect, left_brush); let _ = DeleteObject(left_brush); let right_brush = CreateSolidBrush(COLORREF(native_interop::colorref( div_right.0, div_right.1, div_right.2, ))); let right_rect = RECT { left: sc(2), top: divider_top, right: sc(3), bottom: divider_bottom, }; FillRect(hdc, &right_rect, right_brush); let _ = DeleteObject(right_brush); let content_x = sc(LEFT_DIVIDER_W) + sc(DIVIDER_RIGHT_MARGIN); 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())); let font_name = native_interop::wide_str("Segoe UI"); let font = CreateFontW( sc(-12), 0, 0, 0, FW_MEDIUM.0 as i32, 0, 0, 0, DEFAULT_CHARSET.0 as u32, OUT_TT_PRECIS.0 as u32, CLIP_DEFAULT_PRECIS.0 as u32, CLEARTYPE_QUALITY.0 as u32, (DEFAULT_PITCH.0 | FF_DONTCARE.0) as u32, PCWSTR::from_raw(font_name.as_ptr()), ); let old_font = SelectObject(hdc, font); draw_row( hdc, content_x, row1_y, is_dark, text_color, strings.session_window, session_pct, session_text, codex_session_pct, codex_session_text, antigravity_session_pct, antigravity_session_text, show_claude_code, show_codex, show_antigravity, accent, codex_accent, antigravity_accent, track, ); draw_row( hdc, content_x, row2_y, is_dark, text_color, strings.weekly_window, weekly_pct, weekly_text, codex_weekly_pct, codex_weekly_text, antigravity_weekly_pct, antigravity_weekly_text, show_claude_code, show_codex, show_antigravity, accent, codex_accent, antigravity_accent, track, ); SelectObject(hdc, old_font); let _ = DeleteObject(font); } } fn do_poll(send_hwnd: SendHwnd) { let hwnd = send_hwnd.to_hwnd(); let (show_claude_code, show_codex, show_antigravity) = { let state = lock_state(); state .as_ref() .map(|s| (s.show_claude_code, s.show_codex, s.show_antigravity)) .unwrap_or((true, false, false)) }; match poller::poll(show_claude_code, show_codex, show_antigravity) { Ok(data) => { let mut state = lock_state(); if let Some(s) = state.as_mut() { if let Some(claude_code) = data.claude_code.as_ref() { s.session_percent = claude_code.session.percentage; s.weekly_percent = claude_code.weekly.percentage; } else if s.show_claude_code { s.session_percent = 0.0; s.weekly_percent = 0.0; } if let Some(codex) = data.codex.as_ref() { s.codex_session_percent = codex.session.percentage; s.codex_weekly_percent = codex.weekly.percentage; } else if s.show_codex { s.codex_session_percent = 0.0; s.codex_weekly_percent = 0.0; } if let Some(antigravity) = data.antigravity.as_ref() { s.antigravity_session_percent = antigravity.session.percentage; s.antigravity_weekly_percent = antigravity.weekly.percentage; } else if s.show_antigravity { s.antigravity_session_percent = 0.0; s.antigravity_weekly_percent = 0.0; } // Stop fast-poll if reset data is now fresh if !poller::app_is_past_reset(&data) { unsafe { let _ = KillTimer(hwnd, TIMER_RESET_POLL); } } s.data = Some(data); s.last_poll_ok = true; refresh_usage_texts(s); // Recovered from errors — restore normal poll interval if s.retry_count > 0 { s.retry_count = 0; let interval = s.poll_interval_ms; unsafe { SetTimer(hwnd, TIMER_POLL, interval, None); } } s.force_notify_auth_error = false; s.auth_error_paused_polling = false; s.auth_watch_mode = poller::CredentialWatchMode::ActiveSource; s.auth_watch_snapshot.clear(); } unsafe { let _ = PostMessageW(hwnd, WM_APP_USAGE_UPDATED, WPARAM(0), LPARAM(0)); } } Err(e) => { let auth_watch = match e { poller::PollError::AuthRequired | poller::PollError::TokenExpired if show_antigravity && !show_claude_code && !show_codex => { Some(( poller::CredentialWatchMode::Antigravity, poller::credential_watch_snapshot(poller::CredentialWatchMode::Antigravity), )) } poller::PollError::AuthRequired | poller::PollError::TokenExpired => Some(( poller::CredentialWatchMode::ActiveSource, poller::credential_watch_snapshot(poller::CredentialWatchMode::ActiveSource), )), poller::PollError::NoCredentials => Some(( poller::CredentialWatchMode::AllSources, poller::credential_watch_snapshot(poller::CredentialWatchMode::AllSources), )), poller::PollError::RequestFailed => None, }; // Distinguish auth-required errors from transient errors. let notify_auth_error = { let mut state = lock_state(); let mut should_notify = false; if let Some(s) = state.as_mut() { s.last_poll_ok = false; match auth_watch { Some((watch_mode, watch_snapshot)) => { // Only show the balloon on the first failure so it doesn't spam. if s.retry_count == 0 || s.force_notify_auth_error { should_notify = true; } s.force_notify_auth_error = false; s.auth_error_paused_polling = true; s.auth_watch_mode = watch_mode; s.auth_watch_snapshot = watch_snapshot; s.session_text = "!".to_string(); s.weekly_text = "!".to_string(); s.codex_session_text = "!".to_string(); s.codex_weekly_text = "!".to_string(); s.antigravity_session_text = "!".to_string(); s.antigravity_weekly_text = "!".to_string(); s.retry_count = s.retry_count.saturating_add(1); unsafe { let _ = KillTimer(hwnd, TIMER_POLL); let _ = KillTimer(hwnd, TIMER_RESET_POLL); let _ = KillTimer(hwnd, TIMER_COUNTDOWN); SetTimer(hwnd, TIMER_POLL, s.poll_interval_ms, None); } } _ => { // Transient network / credential-missing errors: exponential backoff. s.force_notify_auth_error = false; s.auth_error_paused_polling = false; s.auth_watch_mode = poller::CredentialWatchMode::ActiveSource; s.auth_watch_snapshot.clear(); s.session_text = "...".to_string(); s.weekly_text = "...".to_string(); s.codex_session_text = "...".to_string(); s.codex_weekly_text = "...".to_string(); s.antigravity_session_text = "...".to_string(); s.antigravity_weekly_text = "...".to_string(); s.retry_count = s.retry_count.saturating_add(1); let backoff = RETRY_BASE_MS.saturating_mul( 1u32.checked_shl(s.retry_count - 1).unwrap_or(u32::MAX), ); let retry_ms = backoff.min(s.poll_interval_ms); unsafe { let _ = KillTimer(hwnd, TIMER_RESET_POLL); SetTimer(hwnd, TIMER_POLL, retry_ms, None); } } } } should_notify }; if notify_auth_error { let balloon = { let state = lock_state(); state.as_ref().map(|s| { if s.show_claude_code { ( s.language.strings(), tray_icon::TrayIconKind::Claude, s.language.strings().token_expired_title, s.language.strings().token_expired_body, ) } else if s.show_codex { ( s.language.strings(), tray_icon::TrayIconKind::Codex, s.language.strings().codex_token_expired_title, s.language.strings().codex_token_expired_body, ) } else { ( s.language.strings(), tray_icon::TrayIconKind::Antigravity, s.language.strings().antigravity_token_expired_title, s.language.strings().antigravity_token_expired_body, ) } }) }; if let Some((_strings, kind, title, body)) = balloon { tray_icon::notify_balloon(hwnd, kind, title, body); } } unsafe { let _ = PostMessageW(hwnd, WM_APP_USAGE_UPDATED, WPARAM(0), LPARAM(0)); } } } } fn schedule_countdown_timer() { let state = lock_state(); let s = match state.as_ref() { Some(s) => s, None => return, }; let hwnd = s.hwnd.to_hwnd(); if !s.last_poll_ok { unsafe { let _ = KillTimer(hwnd, TIMER_COUNTDOWN); let _ = KillTimer(hwnd, TIMER_RESET_POLL); } return; } let data = match &s.data { Some(d) => d, None => return, }; // If a reset time has passed, poll every 5s to pick up fresh data if poller::app_is_past_reset(data) { unsafe { SetTimer(hwnd, TIMER_RESET_POLL, 5_000, None); } } let delays = [ data.claude_code .as_ref() .and_then(|usage| poller::time_until_display_change(usage.session.resets_at)), data.claude_code .as_ref() .and_then(|usage| poller::time_until_display_change(usage.weekly.resets_at)), data.codex .as_ref() .and_then(|usage| poller::time_until_display_change(usage.session.resets_at)), data.codex .as_ref() .and_then(|usage| poller::time_until_display_change(usage.weekly.resets_at)), data.antigravity .as_ref() .and_then(|usage| poller::time_until_display_change(usage.session.resets_at)), data.antigravity .as_ref() .and_then(|usage| poller::time_until_display_change(usage.weekly.resets_at)), ]; let min_delay = delays.into_iter().flatten().min(); let ms = min_delay .unwrap_or(Duration::from_secs(60)) .as_millis() .max(1000) as u32; unsafe { SetTimer(hwnd, TIMER_COUNTDOWN, ms, None); } } fn check_theme_change() { let new_dark = theme::is_dark_mode(); let changed = { let mut state = lock_state(); if let Some(s) = state.as_mut() { if s.is_dark != new_dark { s.is_dark = new_dark; true } else { false } } else { false } }; if changed { render_layered(); } } fn check_language_change() { if update_language_change() { render_layered(); } } fn update_display() { let mut state = lock_state(); let s = match state.as_mut() { Some(s) => s, None => return, }; // Don't overwrite error text with stale cached data if !s.last_poll_ok { return; } refresh_usage_texts(s); } fn suppress_tray_reposition_for(duration: Duration) { let mut until = SUPPRESS_TRAY_REPOSITION_UNTIL .lock() .unwrap_or_else(|e| e.into_inner()); *until = Some(Instant::now() + duration); } fn tray_reposition_is_suppressed() -> bool { let now = Instant::now(); let mut until = SUPPRESS_TRAY_REPOSITION_UNTIL .lock() .unwrap_or_else(|e| e.into_inner()); match *until { Some(deadline) if now < deadline => true, Some(_) => { *until = None; false } None => false, } } fn position_at_taskbar() { refresh_dpi(); // Drop the app-state lock before any Win32 call that may synchronously // re-enter our window procedure. let (hwnd, embedded, tray_offset, taskbar_hwnd) = { let state = lock_state(); let s = match state.as_ref() { Some(s) => s, None => return, }; // Don't fight the user's drag if s.dragging { return; } let taskbar_hwnd = match s.taskbar_hwnd { Some(h) => h, None => { diagnose::log("position_at_taskbar skipped: no taskbar handle"); return; } }; (s.hwnd.to_hwnd(), s.embedded, s.tray_offset, taskbar_hwnd) }; let taskbar_rect = match native_interop::get_taskbar_rect(taskbar_hwnd) { Some(r) => r, None => { diagnose::log("position_at_taskbar skipped: unable to query taskbar rect"); return; } }; let taskbar_height = taskbar_rect.bottom - taskbar_rect.top; let mut tray_left = taskbar_rect.right; 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; } } 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; native_interop::move_window(hwnd, x, y - taskbar_rect.top, widget_width, widget_height); diagnose::log(format!( "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; 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}" )); } } fn compute_anchor_y(anchor_top: i32, anchor_height: i32, widget_height: i32) -> i32 { let anchor_bottom = anchor_top + anchor_height; (anchor_bottom - widget_height).max(anchor_top) } /// WinEvent callback for tray icon location changes unsafe extern "system" fn on_tray_location_changed( _hook: HWINEVENTHOOK, _event: u32, hwnd: HWND, _id_object: i32, _id_child: i32, _thread: u32, _time: u32, ) { static LAST_REPOSITION: Mutex> = Mutex::new(None); let is_tray = { let state = lock_state(); state .as_ref() .and_then(|s| s.tray_notify_hwnd) .map(|h| h == hwnd) .unwrap_or(false) }; if is_tray { if tray_reposition_is_suppressed() { return; } let should_reposition = { let mut last = LAST_REPOSITION.lock().unwrap_or_else(|e| e.into_inner()); let now = std::time::Instant::now(); if last .map(|t| now.duration_since(t).as_millis() > 500) .unwrap_or(true) { *last = Some(now); true } else { false } }; if should_reposition { position_at_taskbar(); render_layered(); } } } /// Main window procedure unsafe extern "system" fn wnd_proc( hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM, ) -> LRESULT { match msg { WM_PAINT => { // For non-embedded fallback, paint normally let embedded = { let state = lock_state(); state.as_ref().map(|s| s.embedded).unwrap_or(false) }; if embedded { // Layered windows don't use WM_PAINT; just validate the region let mut ps = PAINTSTRUCT::default(); let _ = BeginPaint(hwnd, &mut ps); let _ = EndPaint(hwnd, &ps); } else { let mut ps = PAINTSTRUCT::default(); let hdc = BeginPaint(hwnd, &mut ps); paint(hdc, hwnd); let _ = EndPaint(hwnd, &ps); } LRESULT(0) } WM_ERASEBKGND => LRESULT(1), WM_DISPLAYCHANGE | WM_DPICHANGED_MSG | WM_SETTINGCHANGE => { if msg == WM_DPICHANGED_MSG { let new_dpi = (wparam.0 & 0xFFFF) as u32; CURRENT_DPI.store(new_dpi, Ordering::Relaxed); } if msg == WM_SETTINGCHANGE { check_theme_change(); check_language_change(); } refresh_dpi(); position_at_taskbar(); render_layered(); LRESULT(0) } WM_TIMER => { let timer_id = wparam.0; match timer_id { TIMER_POLL => { let auth_watch = { let state = lock_state(); state.as_ref().map(|s| { ( s.auth_error_paused_polling, s.auth_watch_mode, s.auth_watch_snapshot.clone(), ) }) }; match auth_watch { Some((true, watch_mode, previous_snapshot)) => { let current_snapshot = poller::credential_watch_snapshot(watch_mode); if current_snapshot != previous_snapshot { let mut state = lock_state(); if let Some(s) = state.as_mut() { if s.auth_error_paused_polling && s.auth_watch_mode == watch_mode { s.auth_watch_snapshot = current_snapshot; } } drop(state); let sh = SendHwnd::from_hwnd(hwnd); std::thread::spawn(move || { do_poll(sh); }); } } Some((false, _, _)) => { let sh = SendHwnd::from_hwnd(hwnd); std::thread::spawn(move || { do_poll(sh); }); } None => {} } } TIMER_COUNTDOWN => { update_display(); render_layered(); schedule_countdown_timer(); } TIMER_RESET_POLL => { let should_poll = { let state = lock_state(); state .as_ref() .map(|s| !s.auth_error_paused_polling) .unwrap_or(false) }; if should_poll { let sh = SendHwnd::from_hwnd(hwnd); std::thread::spawn(move || { do_poll(sh); }); } } TIMER_UPDATE_CHECK => { begin_update_check(hwnd, false); } _ => {} } LRESULT(0) } WM_APP_USAGE_UPDATED => { check_theme_change(); check_language_change(); render_layered(); schedule_countdown_timer(); suppress_tray_reposition_for(Duration::from_millis( TRAY_ICON_UPDATE_REPOSITION_SUPPRESS_MS, )); sync_tray_icons(hwnd); LRESULT(0) } WM_APP_UPDATE_CHECK_COMPLETE => { schedule_auto_update_check(hwnd); LRESULT(0) } WM_SETCURSOR => { let is_dragging = { let state = lock_state(); state.as_ref().map(|s| s.dragging).unwrap_or(false) }; // Always show resize cursor while dragging or when hovering divider zone let hit_test = (lparam.0 & 0xFFFF) as u16; if is_dragging { let cursor = LoadCursorW(HINSTANCE::default(), IDC_SIZEWE).unwrap_or_default(); SetCursor(cursor); return LRESULT(1); } if hit_test == 1 { // HTCLIENT let mut pt = POINT::default(); let _ = GetCursorPos(&mut pt); let _ = ScreenToClient(hwnd, &mut pt); if pt.x < sc(DIVIDER_HIT_ZONE) { let cursor = LoadCursorW(HINSTANCE::default(), IDC_SIZEWE).unwrap_or_default(); SetCursor(cursor); return LRESULT(1); } } DefWindowProcW(hwnd, msg, wparam, lparam) } WM_LBUTTONDOWN => { let client_x = (lparam.0 & 0xFFFF) as i16 as i32; if client_x < sc(DIVIDER_HIT_ZONE) { let mut pt = POINT::default(); let _ = GetCursorPos(&mut pt); let mut state = lock_state(); if let Some(s) = state.as_mut() { s.dragging = true; s.drag_start_mouse_x = pt.x; s.drag_start_offset = s.tray_offset; } SetCapture(hwnd); } LRESULT(0) } WM_MOUSEMOVE => { let is_dragging = { let state = lock_state(); state.as_ref().map(|s| s.dragging).unwrap_or(false) }; if is_dragging { let mut pt = POINT::default(); let _ = GetCursorPos(&mut pt); let move_target = { let mut state = lock_state(); let s = match state.as_mut() { Some(s) => s, None => return LRESULT(0), }; // Moving mouse left = positive delta = larger offset (further left) let delta = s.drag_start_mouse_x - pt.x; let mut new_offset = s.drag_start_offset + delta; // Clamp: offset >= 0 (can't go right of default) if new_offset < 0 { new_offset = 0; } let taskbar_hwnd = s.taskbar_hwnd; let embedded = s.embedded; let hwnd_val = s.hwnd.to_hwnd(); // Clamp: don't go past left edge of taskbar if let Some(taskbar_hwnd) = taskbar_hwnd { if let Some(taskbar_rect) = native_interop::get_taskbar_rect(taskbar_hwnd) { let mut tray_left = taskbar_rect.right; 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; } } let widget_width = total_widget_width_for_state(s); let max_offset = (tray_left - taskbar_rect.left - widget_width).max(0); if new_offset > max_offset { new_offset = max_offset; } s.tray_offset = new_offset; let taskbar_height = taskbar_rect.bottom - taskbar_rect.top; let anchor_top = taskbar_rect.top; let anchor_height = taskbar_height; let widget_height = sc(WIDGET_HEIGHT); let y = compute_anchor_y(anchor_top, anchor_height, widget_height); let x = if embedded { tray_left - taskbar_rect.left - widget_width - new_offset } else { tray_left - widget_width - new_offset }; Some(( hwnd_val, embedded, x, y, taskbar_rect.top, widget_width, widget_height, )) } else { s.tray_offset = new_offset; None } } else { s.tray_offset = new_offset; None } }; if let Some((hwnd_val, embedded, x, y, taskbar_top, widget_width, widget_height)) = move_target { if embedded { native_interop::move_window( hwnd_val, x, y - taskbar_top, widget_width, widget_height, ); } else { native_interop::move_window(hwnd_val, x, y, widget_width, widget_height); } } } LRESULT(0) } WM_LBUTTONUP => { let was_dragging = { let mut state = lock_state(); if let Some(s) = state.as_mut() { if s.dragging { s.dragging = false; let offset = s.tray_offset; Some(offset) } else { None } } else { None } }; if was_dragging.is_some() { let _ = ReleaseCapture(); save_state_settings(); } LRESULT(0) } WM_RBUTTONUP => { show_context_menu(hwnd); LRESULT(0) } WM_COMMAND => { let id = wparam.0 as u16; match id { 1 => { { let mut state = lock_state(); if let Some(s) = state.as_mut() { s.session_text = "...".to_string(); s.weekly_text = "...".to_string(); s.codex_session_text = "...".to_string(); s.codex_weekly_text = "...".to_string(); s.force_notify_auth_error = true; } } render_layered(); let sh = SendHwnd::from_hwnd(hwnd); std::thread::spawn(move || { do_poll(sh); }); } IDM_VERSION_ACTION => { let (install_channel, release) = { let state = lock_state(); match state.as_ref() { Some(s) => ( s.install_channel, match &s.update_status { UpdateStatus::Available(release) => Some(release.clone()), _ => None, }, ), None => (InstallChannel::Portable, None), } }; match install_channel { InstallChannel::Winget => { if release.is_some() { begin_winget_update(hwnd); } else { begin_update_check(hwnd, true); } } InstallChannel::Portable => { if let Some(release) = release { begin_update_apply(hwnd, release); } else { begin_update_check(hwnd, true); } } } } 2 => { let hook = { let state = lock_state(); state.as_ref().and_then(|s| s.win_event_hook) }; if let Some(h) = hook { native_interop::unhook_win_event(h); } PostQuitMessage(0); } IDM_RESET_POSITION => { { let mut state = lock_state(); if let Some(s) = state.as_mut() { s.tray_offset = 0; } } save_state_settings(); position_at_taskbar(); } IDM_START_WITH_WINDOWS => { set_startup_enabled(!is_startup_enabled()); } IDM_FREQ_1MIN | IDM_FREQ_5MIN | IDM_FREQ_15MIN | IDM_FREQ_1HOUR => { let new_interval = match id { IDM_FREQ_1MIN => POLL_1_MIN, IDM_FREQ_5MIN => POLL_5_MIN, IDM_FREQ_15MIN => POLL_15_MIN, IDM_FREQ_1HOUR => POLL_1_HOUR, _ => POLL_15_MIN, }; { let mut state = lock_state(); if let Some(s) = state.as_mut() { s.poll_interval_ms = new_interval; } } save_state_settings(); // Reset the poll timer with the new interval SetTimer(hwnd, TIMER_POLL, new_interval, None); } IDM_MODEL_CLAUDE_CODE | IDM_MODEL_CODEX | IDM_MODEL_ANTIGRAVITY => { { let mut state = lock_state(); if let Some(s) = state.as_mut() { match id { IDM_MODEL_CLAUDE_CODE => { if s.show_codex || s.show_antigravity || !s.show_claude_code { s.show_claude_code = !s.show_claude_code; } } IDM_MODEL_CODEX => { if s.show_claude_code || s.show_antigravity || !s.show_codex { s.show_codex = !s.show_codex; } } IDM_MODEL_ANTIGRAVITY => { if s.show_claude_code || s.show_codex || !s.show_antigravity { s.show_antigravity = !s.show_antigravity; } } _ => {} } s.session_text = "...".to_string(); s.weekly_text = "...".to_string(); s.codex_session_text = "...".to_string(); s.codex_weekly_text = "...".to_string(); s.antigravity_session_text = "...".to_string(); s.antigravity_weekly_text = "...".to_string(); } } save_state_settings(); position_at_taskbar(); render_layered(); sync_tray_icons(hwnd); let sh = SendHwnd::from_hwnd(hwnd); std::thread::spawn(move || { do_poll(sh); }); } IDM_LANG_SYSTEM | IDM_LANG_ENGLISH | IDM_LANG_DUTCH | IDM_LANG_SPANISH | IDM_LANG_FRENCH | IDM_LANG_GERMAN | IDM_LANG_JAPANESE | IDM_LANG_KOREAN | IDM_LANG_TRADITIONAL_CHINESE | IDM_LANG_RUSSIAN | IDM_LANG_PORTUGUESE_BRAZIL => { let language_override = match id { IDM_LANG_SYSTEM => None, IDM_LANG_ENGLISH => Some(LanguageId::English), IDM_LANG_DUTCH => Some(LanguageId::Dutch), IDM_LANG_SPANISH => Some(LanguageId::Spanish), IDM_LANG_FRENCH => Some(LanguageId::French), IDM_LANG_GERMAN => Some(LanguageId::German), IDM_LANG_JAPANESE => Some(LanguageId::Japanese), IDM_LANG_KOREAN => Some(LanguageId::Korean), IDM_LANG_TRADITIONAL_CHINESE => Some(LanguageId::TraditionalChinese), IDM_LANG_RUSSIAN => Some(LanguageId::Russian), IDM_LANG_PORTUGUESE_BRAZIL => Some(LanguageId::PortugueseBrazil), _ => None, }; { let mut state = lock_state(); if let Some(s) = state.as_mut() { apply_language_to_state(s, language_override); } } save_state_settings(); render_layered(); } id if id == tray_icon::IDM_TOGGLE_WIDGET => { toggle_widget_visibility(hwnd); } _ => {} } LRESULT(0) } _ if msg == WM_APP_TRAY => { match tray_icon::handle_message(lparam) { tray_icon::TrayAction::ToggleWidget => { toggle_widget_visibility(hwnd); } tray_icon::TrayAction::ShowContextMenu => { show_context_menu(hwnd); } tray_icon::TrayAction::None => {} } LRESULT(0) } WM_DESTROY => { let hook = { let state = lock_state(); state.as_ref().and_then(|s| s.win_event_hook) }; if let Some(h) = hook { native_interop::unhook_win_event(h); } tray_icon::remove_all(hwnd); PostQuitMessage(0); LRESULT(0) } _ => DefWindowProcW(hwnd, msg, wparam, lparam), } } fn show_context_menu(hwnd: HWND) { unsafe { let ( current_interval, strings, language, language_override, install_channel, update_status, widget_visible, show_claude_code, show_codex, show_antigravity, ) = { let state = lock_state(); match state.as_ref() { Some(s) => ( s.poll_interval_ms, s.language.strings(), s.language, s.language_override, s.install_channel, s.update_status.clone(), s.widget_visible, s.show_claude_code, s.show_codex, s.show_antigravity, ), None => ( POLL_15_MIN, LanguageId::English.strings(), LanguageId::English, None, InstallChannel::Portable, UpdateStatus::Idle, true, true, false, false, ), } }; let menu = CreatePopupMenu().unwrap(); let refresh_str = native_interop::wide_str(strings.refresh); let _ = AppendMenuW( menu, MENU_ITEM_FLAGS(0), 1, PCWSTR::from_raw(refresh_str.as_ptr()), ); // Update Frequency submenu let freq_menu = CreatePopupMenu().unwrap(); let freq_items: [(u16, u32, &str); 4] = [ (IDM_FREQ_1MIN, POLL_1_MIN, strings.one_minute), (IDM_FREQ_5MIN, POLL_5_MIN, strings.five_minutes), (IDM_FREQ_15MIN, POLL_15_MIN, strings.fifteen_minutes), (IDM_FREQ_1HOUR, POLL_1_HOUR, strings.one_hour), ]; for (id, interval, label) in freq_items { let label_str = native_interop::wide_str(label); let flags = if interval == current_interval { MF_CHECKED } else { MENU_ITEM_FLAGS(0) }; let _ = AppendMenuW( freq_menu, flags, id as usize, PCWSTR::from_raw(label_str.as_ptr()), ); } let freq_label = native_interop::wide_str(strings.update_frequency); let _ = AppendMenuW( menu, MF_POPUP, freq_menu.0 as usize, PCWSTR::from_raw(freq_label.as_ptr()), ); // Models submenu let models_menu = CreatePopupMenu().unwrap(); let claude_model = native_interop::wide_str(strings.claude_code_model); let claude_flags = if show_claude_code { MF_CHECKED } else { MENU_ITEM_FLAGS(0) }; let _ = AppendMenuW( models_menu, claude_flags, IDM_MODEL_CLAUDE_CODE as usize, PCWSTR::from_raw(claude_model.as_ptr()), ); let codex_model = native_interop::wide_str(strings.codex_model); let codex_flags = if show_codex { MF_CHECKED } else { MENU_ITEM_FLAGS(0) }; let _ = AppendMenuW( models_menu, codex_flags, IDM_MODEL_CODEX as usize, PCWSTR::from_raw(codex_model.as_ptr()), ); let antigravity_model = native_interop::wide_str(strings.antigravity_model); let antigravity_flags = if show_antigravity { MF_CHECKED } else { MENU_ITEM_FLAGS(0) }; let _ = AppendMenuW( models_menu, antigravity_flags, IDM_MODEL_ANTIGRAVITY as usize, PCWSTR::from_raw(antigravity_model.as_ptr()), ); let models_label = native_interop::wide_str(strings.models); let _ = AppendMenuW( menu, MF_POPUP, models_menu.0 as usize, PCWSTR::from_raw(models_label.as_ptr()), ); // Settings submenu let settings_menu = CreatePopupMenu().unwrap(); let startup_str = native_interop::wide_str(strings.start_with_windows); let startup_flags = if is_startup_enabled() { MF_CHECKED } else { MENU_ITEM_FLAGS(0) }; let _ = AppendMenuW( settings_menu, startup_flags, IDM_START_WITH_WINDOWS as usize, PCWSTR::from_raw(startup_str.as_ptr()), ); let reset_pos_str = native_interop::wide_str(strings.reset_position); let _ = AppendMenuW( settings_menu, MENU_ITEM_FLAGS(0), IDM_RESET_POSITION as usize, PCWSTR::from_raw(reset_pos_str.as_ptr()), ); let language_menu = CreatePopupMenu().unwrap(); let system_label = native_interop::wide_str(strings.system_default); let system_flags = if language_override.is_none() { MF_CHECKED } else { MENU_ITEM_FLAGS(0) }; let _ = AppendMenuW( language_menu, system_flags, IDM_LANG_SYSTEM as usize, PCWSTR::from_raw(system_label.as_ptr()), ); for language in LanguageId::ALL { let id = match language { LanguageId::English => IDM_LANG_ENGLISH, LanguageId::Dutch => IDM_LANG_DUTCH, LanguageId::Spanish => IDM_LANG_SPANISH, LanguageId::French => IDM_LANG_FRENCH, LanguageId::German => IDM_LANG_GERMAN, LanguageId::Japanese => IDM_LANG_JAPANESE, LanguageId::Korean => IDM_LANG_KOREAN, LanguageId::TraditionalChinese => IDM_LANG_TRADITIONAL_CHINESE, LanguageId::Russian => IDM_LANG_RUSSIAN, LanguageId::PortugueseBrazil => IDM_LANG_PORTUGUESE_BRAZIL, }; let label_str = native_interop::wide_str(language.native_name()); let flags = if language_override == Some(language) { MF_CHECKED } else { MENU_ITEM_FLAGS(0) }; let _ = AppendMenuW( language_menu, flags, id as usize, PCWSTR::from_raw(label_str.as_ptr()), ); } let language_label = native_interop::wide_str(strings.language); let _ = AppendMenuW( settings_menu, MF_POPUP, language_menu.0 as usize, PCWSTR::from_raw(language_label.as_ptr()), ); let _ = AppendMenuW(settings_menu, MF_SEPARATOR, 0, PCWSTR::null()); let version_label = version_action_label(strings, language, install_channel, &update_status); let version_str = native_interop::wide_str(&version_label); let version_flags = if matches!( update_status, UpdateStatus::Checking | UpdateStatus::Applying ) { MF_GRAYED } else { MENU_ITEM_FLAGS(0) }; let _ = AppendMenuW( settings_menu, version_flags, IDM_VERSION_ACTION as usize, PCWSTR::from_raw(version_str.as_ptr()), ); let settings_label = native_interop::wide_str(strings.settings); let _ = AppendMenuW( menu, MF_POPUP, settings_menu.0 as usize, PCWSTR::from_raw(settings_label.as_ptr()), ); let widget_label = native_interop::wide_str(strings.show_widget); let widget_flags = if widget_visible { MF_CHECKED } else { MENU_ITEM_FLAGS(0) }; let _ = AppendMenuW( menu, widget_flags, tray_icon::IDM_TOGGLE_WIDGET as usize, PCWSTR::from_raw(widget_label.as_ptr()), ); let _ = AppendMenuW(menu, MF_SEPARATOR, 0, PCWSTR::null()); let exit_str = native_interop::wide_str(strings.exit); let _ = AppendMenuW( menu, MENU_ITEM_FLAGS(0), 2, PCWSTR::from_raw(exit_str.as_ptr()), ); let mut pt = POINT::default(); let _ = GetCursorPos(&mut pt); let _ = SetForegroundWindow(hwnd); let _ = TrackPopupMenu(menu, TPM_RIGHTBUTTON, pt.x, pt.y, 0, hwnd, None); let _ = DestroyMenu(menu); } } /// Paint for non-embedded fallback (normal WM_PAINT path) fn paint(hdc: HDC, hwnd: HWND) { let ( is_dark, strings, session_pct, session_text, weekly_pct, weekly_text, codex_session_pct, codex_session_text, codex_weekly_pct, codex_weekly_text, antigravity_session_pct, antigravity_session_text, antigravity_weekly_pct, antigravity_weekly_text, show_claude_code, show_codex, show_antigravity, ) = { let state = lock_state(); match state.as_ref() { Some(s) => ( s.is_dark, s.language.strings(), s.session_percent, s.session_text.clone(), s.weekly_percent, s.weekly_text.clone(), s.codex_session_percent, s.codex_session_text.clone(), s.codex_weekly_percent, s.codex_weekly_text.clone(), s.antigravity_session_percent, s.antigravity_session_text.clone(), s.antigravity_weekly_percent, s.antigravity_weekly_text.clone(), s.show_claude_code, s.show_codex, s.show_antigravity, ), None => return, } }; let accent = claude_accent_color(); let codex_accent = codex_accent_color(is_dark); let antigravity_accent = antigravity_accent_color(); let track = if is_dark { Color::from_hex("#444444") } else { Color::from_hex("#AAAAAA") }; let text_color = if is_dark { Color::from_hex("#888888") } else { Color::from_hex("#404040") }; let bg_color = if is_dark { Color::from_hex("#1C1C1C") } else { Color::from_hex("#F3F3F3") }; unsafe { let mut client_rect = RECT::default(); let _ = GetClientRect(hwnd, &mut client_rect); let width = client_rect.right - client_rect.left; let height = client_rect.bottom - client_rect.top; if width <= 0 || height <= 0 { return; } let mem_dc = CreateCompatibleDC(hdc); let mem_bmp = CreateCompatibleBitmap(hdc, width, height); let old_bmp = SelectObject(mem_dc, mem_bmp); paint_content( mem_dc, width, height, is_dark, &bg_color, &text_color, &accent, &track, strings, session_pct, &session_text, weekly_pct, &weekly_text, codex_session_pct, &codex_session_text, codex_weekly_pct, &codex_weekly_text, antigravity_session_pct, &antigravity_session_text, antigravity_weekly_pct, &antigravity_weekly_text, show_claude_code, show_codex, show_antigravity, &codex_accent, &antigravity_accent, ); let _ = BitBlt(hdc, 0, 0, width, height, mem_dc, 0, 0, SRCCOPY); SelectObject(mem_dc, old_bmp); let _ = DeleteObject(mem_bmp); let _ = DeleteDC(mem_dc); } } fn draw_row( hdc: HDC, x: i32, y: i32, is_dark: bool, text_color: &Color, label: &str, claude_percent: f64, claude_text: &str, codex_percent: f64, codex_text: &str, antigravity_percent: f64, antigravity_text: &str, show_claude_code: bool, show_codex: bool, show_antigravity: bool, claude_accent: &Color, codex_accent: &Color, antigravity_accent: &Color, track: &Color, ) { let seg_h = sc(SEGMENT_H); let active_models = active_model_count(show_claude_code, show_codex, show_antigravity); let segment_count = row_bar_segment_count(active_models); let use_model_text_colors = active_models > 1; let claude_value_color = if use_model_text_colors { claude_usage_text_color(is_dark) } else { *text_color }; let codex_value_color = if use_model_text_colors { codex_usage_text_color(is_dark) } else { *text_color }; let antigravity_value_color = if use_model_text_colors { antigravity_usage_text_color(is_dark) } else { *text_color }; unsafe { let _ = SetTextColor(hdc, COLORREF(text_color.to_colorref())); let mut label_wide: Vec = label.encode_utf16().collect(); let mut label_rect = RECT { left: x, top: y, right: x + sc(LABEL_WIDTH), bottom: y + seg_h, }; let _ = DrawTextW( hdc, &mut label_wide, &mut label_rect, DT_LEFT | DT_VCENTER | DT_SINGLELINE, ); let mut model_x = x + sc(LABEL_WIDTH) + sc(LABEL_RIGHT_MARGIN); if show_claude_code { draw_usage_bar( hdc, model_x, y, segment_count, claude_percent, claude_text, claude_accent, track, &claude_value_color, ); model_x += model_usage_width(segment_count) + sc(MODEL_RIGHT_MARGIN); } if show_codex { draw_usage_bar( hdc, model_x, y, segment_count, codex_percent, codex_text, codex_accent, track, &codex_value_color, ); model_x += model_usage_width(segment_count) + sc(MODEL_RIGHT_MARGIN); } if show_antigravity { draw_usage_bar( hdc, model_x, y, segment_count, antigravity_percent, antigravity_text, antigravity_accent, track, &antigravity_value_color, ); } } } fn model_usage_width(segment_count: i32) -> i32 { (sc(SEGMENT_W) + sc(SEGMENT_GAP)) * segment_count - sc(SEGMENT_GAP) + sc(BAR_RIGHT_MARGIN) + sc(TEXT_WIDTH) } fn draw_usage_bar( hdc: HDC, bar_x: i32, y: i32, segment_count: i32, percent: f64, text: &str, accent: &Color, track: &Color, text_color: &Color, ) { let seg_w = sc(SEGMENT_W); let seg_h = sc(SEGMENT_H); let seg_gap = sc(SEGMENT_GAP); let corner_r = sc(CORNER_RADIUS); unsafe { let percent_clamped = percent.clamp(0.0, 100.0); let segment_percent = 100.0 / segment_count as f64; for i in 0..segment_count { let seg_x = bar_x + i * (seg_w + seg_gap); let seg_start = (i as f64) * segment_percent; let seg_end = seg_start + segment_percent; let seg_rect = RECT { left: seg_x, top: y, right: seg_x + seg_w, bottom: y + seg_h, }; if percent_clamped >= seg_end { draw_rounded_rect(hdc, &seg_rect, accent, corner_r); } else if percent_clamped <= seg_start { draw_rounded_rect(hdc, &seg_rect, track, corner_r); } else { draw_rounded_rect(hdc, &seg_rect, track, corner_r); let fraction = (percent_clamped - seg_start) / segment_percent; let fill_width = (seg_w as f64 * fraction) as i32; if fill_width > 0 { let fill_rect = RECT { left: seg_x, top: y, right: seg_x + fill_width, bottom: y + seg_h, }; let rgn = CreateRoundRectRgn( seg_rect.left, seg_rect.top, seg_rect.right + 1, seg_rect.bottom + 1, corner_r * 2, corner_r * 2, ); let _ = SelectClipRgn(hdc, rgn); let brush = CreateSolidBrush(COLORREF(accent.to_colorref())); FillRect(hdc, &fill_rect, brush); let _ = DeleteObject(brush); let _ = SelectClipRgn(hdc, HRGN::default()); let _ = DeleteObject(rgn); } } } let text_x = bar_x + segment_count * (seg_w + seg_gap) - seg_gap + sc(BAR_RIGHT_MARGIN); let mut text_wide: Vec = text.encode_utf16().collect(); let mut text_rect = RECT { left: text_x, top: y, right: text_x + sc(TEXT_WIDTH), bottom: y + seg_h, }; let _ = SetTextColor(hdc, COLORREF(text_color.to_colorref())); let _ = DrawTextW( hdc, &mut text_wide, &mut text_rect, DT_LEFT | DT_VCENTER | DT_SINGLELINE, ); } } fn draw_rounded_rect(hdc: HDC, rect: &RECT, color: &Color, radius: i32) { unsafe { let brush = CreateSolidBrush(COLORREF(color.to_colorref())); let rgn = CreateRoundRectRgn( rect.left, rect.top, rect.right + 1, rect.bottom + 1, radius * 2, radius * 2, ); let _ = FillRgn(hdc, rgn, brush); let _ = DeleteObject(rgn); let _ = DeleteObject(brush); } }