2026-06-13 15:31:42 -05:00

3052 lines
101 KiB
Rust

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<HWND>,
tray_notify_hwnd: Option<HWND>,
win_event_hook: Option<HWINEVENTHOOK>,
is_dark: bool,
embedded: bool,
language_override: Option<LanguageId>,
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<AppUsageData>,
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<u64>,
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<Option<Instant>> = 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<Option<AppState>> = Mutex::new(None);
/// Lock STATE safely, recovering from poisoned mutex
fn lock_state() -> MutexGuard<'static, Option<AppState>> {
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<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
last_update_check_unix: Option<u64>,
#[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<tray_icon::TrayIconData> {
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<u64>) -> 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<LanguageId>) {
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(&current_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::<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()),
..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::<BITMAPINFOHEADER>() 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<Option<std::time::Instant>> = 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<u16> = 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<u16> = 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);
}
}