From f5fa50ed2420c83d30757144f04b7a1f7bd228c0 Mon Sep 17 00:00:00 2001 From: Kushal Shrestha Date: Tue, 28 Jul 2026 18:03:50 +0545 Subject: [PATCH 1/2] feat(widget): add placement controls and adaptive usage rows Add persistent left/right taskbar anchoring and direct monitor selection, including localized menu labels and drag behavior for embedded and fallback modes. Classify Codex rate-limit windows by duration so weekly-only quotas render on the weekly row, hide unavailable Codex bars, and point Antigravity reauthentication to the agy CLI. --- README.md | 13 +- src/localization/dutch.rs | 6 +- src/localization/english.rs | 6 +- src/localization/french.rs | 6 +- src/localization/german.rs | 6 +- src/localization/japanese.rs | 6 +- src/localization/korean.rs | 6 +- src/localization/mod.rs | 4 + src/localization/portuguese_brazil.rs | 6 +- src/localization/russian.rs | 6 +- src/localization/simplified_chinese.rs | 6 +- src/localization/spanish.rs | 6 +- src/localization/traditional_chinese.rs | 6 +- src/native_interop.rs | 38 +++- src/poller.rs | 44 ++++- src/window.rs | 236 +++++++++++++++++++++--- 16 files changed, 353 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index f8b3c2b..d8d318b 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ This app is for Windows users who already have **Claude Code (CLI or App) instal Codex support is optional. To show Codex usage, install and sign in to the Codex CLI, then enable Codex from the right-click **Models** menu. -Antigravity support is optional too. To show Antigravity usage, install and sign in to Google Antigravity, then enable the **Antigravity** model from the right-click **Models** menu. +Antigravity support is optional too. To show Antigravity usage, install Google Antigravity, run `agy` in a terminal and sign in there, then enable the **Antigravity** model from the right-click **Models** menu. It works best if you want a simple "how close am I to the limit?" display that is always visible. @@ -37,7 +37,7 @@ It works best if you want a simple "how close am I to the limit?" display that i - Windows 10 or Windows 11 - Claude Code (CLI or App) installed and authenticated - Optional: Codex CLI installed and authenticated, if you want Codex usage -- Optional: Google Antigravity installed and authenticated, if you want Antigravity usage +- Optional: Google Antigravity installed with its `agy` CLI authenticated, if you want Antigravity usage If you use Claude Code through WSL, that is supported too. The monitor can read your Claude Code credentials from Windows or from your WSL environment. @@ -62,8 +62,9 @@ claude-code-usage-monitor Once running, it will appear in your taskbar and as one or more tray icons in the notification area. - Drag the left divider to move the taskbar widget -- On multi-monitor setups, drag the widget onto another Windows taskbar to move it to that screen -- Right-click the taskbar widget or tray icon for refresh, displayed models, update frequency, Start with Windows, reset position, language, updates, and exit +- On multi-monitor setups, drag the widget onto another Windows taskbar to move it to that screen, or pick the screen directly via **Settings → Monitor** +- Right-click the taskbar widget or tray icon for refresh, displayed models, update frequency, Start with Windows, reset position, taskbar side, language, updates, and exit +- Use **Settings → Taskbar Side** to anchor the widget to the left or right side of the taskbar (right by default); dragging still fine-tunes the position from the chosen side - Left-click the tray icon to toggle the taskbar widget on or off - Enable `Start with Windows` from the right-click menu if you want it to launch automatically when you sign in @@ -139,7 +140,7 @@ What the app sends over the network: What the app stores locally: -- Widget position +- Widget position and taskbar side - Selected taskbar / screen - Widget visibility - Polling frequency @@ -159,7 +160,7 @@ Notes: - If your Claude Code token is expired, the app may ask the local Claude CLI to refresh it in the background - If your Codex token is expired, the app may ask the local Codex CLI to refresh it in the background. The monitor does not write `auth.json` itself; any credential update is handled by the Codex CLI. -- If your Antigravity token is expired, open Antigravity and sign in again. The monitor does not write Windows Credential Manager entries itself. +- If your Antigravity token is expired, run `agy` in a terminal and sign in again. The monitor does not write Windows Credential Manager entries itself. - Portable installs can update themselves by downloading the latest release from this repository - Proxies should be trusted because proxied usage requests include your OAuth bearer token inside the TLS connection diff --git a/src/localization/dutch.rs b/src/localization/dutch.rs index ed815bf..18bd7ef 100644 --- a/src/localization/dutch.rs +++ b/src/localization/dutch.rs @@ -17,6 +17,10 @@ pub(super) const STRINGS: Strings = Strings { settings: "Instellingen", start_with_windows: "Opstarten met Windows", reset_position: "Positie herstellen", + taskbar_side: "Taakbalkzijde", + taskbar_side_left: "Links", + taskbar_side_right: "Rechts", + monitor: "Monitor", language: "Taal", system_default: "Systeemstandaard", check_for_updates: "Controleren op updates", @@ -43,7 +47,7 @@ pub(super) const STRINGS: Strings = Strings { codex_token_expired_title: "Codex-authenticatiefout", codex_token_expired_body: "Voer 'codex' uit in een terminal en volg de aanmeldstappen. Ververs of herstart de app daarna.", antigravity_token_expired_title: "Antigravity-authenticatiefout", - antigravity_token_expired_body: "Open Antigravity en meld je opnieuw aan. Ververs of herstart de app daarna.", + antigravity_token_expired_body: "Voer 'agy' uit in een terminal en meld je opnieuw aan. Ververs of herstart de app daarna.", codex_window_title: "Codex-gebruiksmonitor", antigravity_window_title: "Antigravity-gebruiksmonitor", second_suffix: "s", diff --git a/src/localization/english.rs b/src/localization/english.rs index 0249730..1fd117f 100644 --- a/src/localization/english.rs +++ b/src/localization/english.rs @@ -17,6 +17,10 @@ pub(super) const STRINGS: Strings = Strings { settings: "Settings", start_with_windows: "Start with Windows", reset_position: "Reset Position", + taskbar_side: "Taskbar Side", + taskbar_side_left: "Left", + taskbar_side_right: "Right", + monitor: "Monitor", language: "Language", system_default: "System Default", check_for_updates: "Check for Updates", @@ -43,7 +47,7 @@ pub(super) const STRINGS: Strings = Strings { codex_token_expired_title: "Codex Auth Error", codex_token_expired_body: "Run 'codex' in a terminal and follow the sign-in prompts. After that, refresh or restart this app.", antigravity_token_expired_title: "Antigravity Auth Error", - antigravity_token_expired_body: "Open Antigravity and sign in again. After that, refresh or restart this app.", + antigravity_token_expired_body: "Run 'agy' in a terminal and sign in again. After that, refresh or restart this app.", codex_window_title: "Codex Usage Monitor", antigravity_window_title: "Antigravity Usage Monitor", second_suffix: "s", diff --git a/src/localization/french.rs b/src/localization/french.rs index 1850f41..0e2ea40 100644 --- a/src/localization/french.rs +++ b/src/localization/french.rs @@ -17,6 +17,10 @@ pub(super) const STRINGS: Strings = Strings { settings: "Paramètres", start_with_windows: "Démarrer avec Windows", reset_position: "Réinitialiser la position", + taskbar_side: "Côté de la barre des tâches", + taskbar_side_left: "Gauche", + taskbar_side_right: "Droite", + monitor: "Écran", language: "Langue", system_default: "Par défaut du système", check_for_updates: "Vérifier les mises à jour", @@ -43,7 +47,7 @@ pub(super) const STRINGS: Strings = Strings { codex_token_expired_title: "Erreur d'authentification Codex", codex_token_expired_body: "Executez 'codex' dans un terminal et suivez les instructions de connexion. Ensuite, actualisez ou redemarrez cette application.", antigravity_token_expired_title: "Erreur d'authentification Antigravity", - antigravity_token_expired_body: "Ouvrez Antigravity et reconnectez-vous. Ensuite, actualisez ou redemarrez cette application.", + antigravity_token_expired_body: "Executez 'agy' dans un terminal et reconnectez-vous. Ensuite, actualisez ou redemarrez cette application.", codex_window_title: "Moniteur d'utilisation Codex", antigravity_window_title: "Moniteur d'utilisation Antigravity", second_suffix: "s", diff --git a/src/localization/german.rs b/src/localization/german.rs index 2b91a81..2856d7a 100644 --- a/src/localization/german.rs +++ b/src/localization/german.rs @@ -17,6 +17,10 @@ pub(super) const STRINGS: Strings = Strings { settings: "Einstellungen", start_with_windows: "Mit Windows starten", reset_position: "Position zurücksetzen", + taskbar_side: "Taskleistenseite", + taskbar_side_left: "Links", + taskbar_side_right: "Rechts", + monitor: "Monitor", language: "Sprache", system_default: "Systemstandard", check_for_updates: "Nach Updates suchen", @@ -43,7 +47,7 @@ pub(super) const STRINGS: Strings = Strings { codex_token_expired_title: "Codex-Authentifizierungsfehler", codex_token_expired_body: "Fuhren Sie 'codex' in einem Terminal aus und folgen Sie den Anmeldeanweisungen. Aktualisieren oder starten Sie diese App anschliessend neu.", antigravity_token_expired_title: "Antigravity-Authentifizierungsfehler", - antigravity_token_expired_body: "Offnen Sie Antigravity und melden Sie sich erneut an. Aktualisieren oder starten Sie diese App anschliessend neu.", + antigravity_token_expired_body: "Fuhren Sie 'agy' in einem Terminal aus und melden Sie sich erneut an. Aktualisieren oder starten Sie diese App anschliessend neu.", codex_window_title: "Codex-Nutzungsmonitor", antigravity_window_title: "Antigravity-Nutzungsmonitor", second_suffix: "s", diff --git a/src/localization/japanese.rs b/src/localization/japanese.rs index 2eec041..f33c50b 100644 --- a/src/localization/japanese.rs +++ b/src/localization/japanese.rs @@ -17,6 +17,10 @@ pub(super) const STRINGS: Strings = Strings { settings: "設定", start_with_windows: "Windows と同時に開始", reset_position: "位置をリセット", + taskbar_side: "タスクバーの配置", + taskbar_side_left: "左", + taskbar_side_right: "右", + monitor: "モニター", language: "言語", system_default: "システム既定", check_for_updates: "更新を確認", @@ -43,7 +47,7 @@ pub(super) const STRINGS: Strings = Strings { codex_token_expired_title: "Codex 認証エラー", codex_token_expired_body: "ターミナルで 'codex' を実行し、サインインの案内に従ってください。その後、このアプリを更新または再起動してください。", antigravity_token_expired_title: "Antigravity 認証エラー", - antigravity_token_expired_body: "Antigravity を開いて再度サインインしてください。その後、このアプリを更新するか再起動してください。", + antigravity_token_expired_body: "ターミナルで 'agy' を実行し、再度サインインしてください。その後、このアプリを更新するか再起動してください。", codex_window_title: "Codex 使用量モニター", antigravity_window_title: "Antigravity 使用量モニター", second_suffix: "秒", diff --git a/src/localization/korean.rs b/src/localization/korean.rs index 965687d..cf55c84 100644 --- a/src/localization/korean.rs +++ b/src/localization/korean.rs @@ -17,6 +17,10 @@ pub(super) const STRINGS: Strings = Strings { settings: "설정", start_with_windows: "Windows 시작 시 자동 실행", reset_position: "위치 초기화", + taskbar_side: "작업 표시줄 위치", + taskbar_side_left: "왼쪽", + taskbar_side_right: "오른쪽", + monitor: "모니터", language: "언어", system_default: "시스템 기본값", check_for_updates: "업데이트 확인", @@ -43,7 +47,7 @@ pub(super) const STRINGS: Strings = Strings { codex_token_expired_title: "Codex 인증 오류", codex_token_expired_body: "터미널에서 'codex'를 실행하고 로그인 안내를 따르세요. 그런 다음 이 앱을 새로 고치거나 다시 시작하세요.", antigravity_token_expired_title: "Antigravity 인증 오류", - antigravity_token_expired_body: "Antigravity를 열고 다시 로그인하세요. 그런 다음 이 앱을 새로 고치거나 다시 시작하세요.", + antigravity_token_expired_body: "터미널에서 'agy'를 실행하고 다시 로그인하세요. 그런 다음 이 앱을 새로 고치거나 다시 시작하세요.", codex_window_title: "Codex 사용량 모니터", antigravity_window_title: "Antigravity 사용량 모니터", second_suffix: "초", diff --git a/src/localization/mod.rs b/src/localization/mod.rs index a11ba02..7056092 100644 --- a/src/localization/mod.rs +++ b/src/localization/mod.rs @@ -159,6 +159,10 @@ pub struct Strings { pub settings: &'static str, pub start_with_windows: &'static str, pub reset_position: &'static str, + pub taskbar_side: &'static str, + pub taskbar_side_left: &'static str, + pub taskbar_side_right: &'static str, + pub monitor: &'static str, pub language: &'static str, pub system_default: &'static str, pub check_for_updates: &'static str, diff --git a/src/localization/portuguese_brazil.rs b/src/localization/portuguese_brazil.rs index 56cf3bf..b1a0542 100644 --- a/src/localization/portuguese_brazil.rs +++ b/src/localization/portuguese_brazil.rs @@ -17,6 +17,10 @@ pub(super) const STRINGS: Strings = Strings { settings: "Configurações", start_with_windows: "Iniciar com o Windows", reset_position: "Redefinir Posição", + taskbar_side: "Lado da barra de tarefas", + taskbar_side_left: "Esquerda", + taskbar_side_right: "Direita", + monitor: "Monitor", language: "Idioma", system_default: "Padrão do Sistema", check_for_updates: "Busca atualizações", @@ -44,7 +48,7 @@ pub(super) const STRINGS: Strings = Strings { codex_token_expired_title: "Erro de Autenticação do Codex", codex_token_expired_body: "Execute 'codex' em um terminal e siga as instruções de login. Depois disso, atualize ou reinicie este aplicativo.", antigravity_token_expired_title: "Erro de Autenticação do Antigravity", - antigravity_token_expired_body: "Abra o Antigravity e entre novamente. Depois disso, atualize ou reinicie este aplicativo.", + antigravity_token_expired_body: "Execute 'agy' em um terminal e entre novamente. Depois disso, atualize ou reinicie este aplicativo.", codex_window_title: "Monitor de uso do Codex", antigravity_window_title: "Monitor de uso do Antigravity", }; diff --git a/src/localization/russian.rs b/src/localization/russian.rs index fc7e372..30255a7 100644 --- a/src/localization/russian.rs +++ b/src/localization/russian.rs @@ -17,6 +17,10 @@ pub(super) const STRINGS: Strings = Strings { settings: "Настройки", start_with_windows: "Запускать вместе с Windows", reset_position: "Сбросить позицию", + taskbar_side: "Сторона панели задач", + taskbar_side_left: "Слева", + taskbar_side_right: "Справа", + monitor: "Монитор", language: "Язык", system_default: "Системный по умолчанию", check_for_updates: "Проверить обновления", @@ -44,7 +48,7 @@ pub(super) const STRINGS: Strings = Strings { codex_token_expired_title: "Ошибка авторизации Codex", codex_token_expired_body: "Запустите 'codex' в терминале и следуйте инструкциям для входа. После этого обновите или перезапустите приложение.", antigravity_token_expired_title: "Ошибка авторизации Antigravity", - antigravity_token_expired_body: "Откройте Antigravity и войдите снова. После этого обновите или перезапустите приложение.", + antigravity_token_expired_body: "Запустите 'agy' в терминале и войдите снова. После этого обновите или перезапустите приложение.", codex_window_title: "Монитор использования Codex", antigravity_window_title: "Монитор использования Antigravity", }; diff --git a/src/localization/simplified_chinese.rs b/src/localization/simplified_chinese.rs index 8a62588..cddd905 100644 --- a/src/localization/simplified_chinese.rs +++ b/src/localization/simplified_chinese.rs @@ -17,6 +17,10 @@ pub(super) const STRINGS: Strings = Strings { settings: "设置", start_with_windows: "开机时启动", reset_position: "重置位置", + taskbar_side: "任务栏位置", + taskbar_side_left: "靠左", + taskbar_side_right: "靠右", + monitor: "显示器", language: "语言", system_default: "系统默认", check_for_updates: "检查更新", @@ -43,7 +47,7 @@ pub(super) const STRINGS: Strings = Strings { codex_token_expired_title: "Codex 验证错误", codex_token_expired_body: "请在终端中运行 'codex',并按照登录提示操作。完成后,请刷新或重新启动此应用程序。", antigravity_token_expired_title: "Antigravity 验证错误", - antigravity_token_expired_body: "请打开 Antigravity 并重新登录。完成后,请刷新或重新启动此应用程序。", + antigravity_token_expired_body: "请在终端中运行 'agy' 并重新登录。完成后,请刷新或重新启动此应用程序。", codex_window_title: "Codex 使用量监控", antigravity_window_title: "Antigravity 使用量监控", second_suffix: "秒", diff --git a/src/localization/spanish.rs b/src/localization/spanish.rs index e635771..9210d5f 100644 --- a/src/localization/spanish.rs +++ b/src/localization/spanish.rs @@ -17,6 +17,10 @@ pub(super) const STRINGS: Strings = Strings { settings: "Configuración", start_with_windows: "Iniciar con Windows", reset_position: "Restablecer posición", + taskbar_side: "Lado de la barra de tareas", + taskbar_side_left: "Izquierda", + taskbar_side_right: "Derecha", + monitor: "Monitor", language: "Idioma", system_default: "Predeterminado del sistema", check_for_updates: "Buscar actualizaciones", @@ -43,7 +47,7 @@ pub(super) const STRINGS: Strings = Strings { codex_token_expired_title: "Error de autenticacion de Codex", codex_token_expired_body: "Ejecuta 'codex' en una terminal y sigue las indicaciones de inicio de sesion. Despues, actualiza o reinicia esta aplicacion.", antigravity_token_expired_title: "Error de autenticacion de Antigravity", - antigravity_token_expired_body: "Abre Antigravity e inicia sesion otra vez. Despues, actualiza o reinicia esta aplicacion.", + antigravity_token_expired_body: "Ejecuta 'agy' en una terminal e inicia sesion otra vez. Despues, actualiza o reinicia esta aplicacion.", codex_window_title: "Monitor de uso de Codex", antigravity_window_title: "Monitor de uso de Antigravity", second_suffix: "s", diff --git a/src/localization/traditional_chinese.rs b/src/localization/traditional_chinese.rs index 3eb3514..017f5e4 100644 --- a/src/localization/traditional_chinese.rs +++ b/src/localization/traditional_chinese.rs @@ -17,6 +17,10 @@ pub(super) const STRINGS: Strings = Strings { settings: "設定", start_with_windows: "開機時啟動", reset_position: "重置位置", + taskbar_side: "工作列位置", + taskbar_side_left: "靠左", + taskbar_side_right: "靠右", + monitor: "顯示器", language: "語言", system_default: "系統預設", check_for_updates: "檢查更新", @@ -43,7 +47,7 @@ pub(super) const STRINGS: Strings = Strings { codex_token_expired_title: "Codex 驗證錯誤", codex_token_expired_body: "請在終端機中執行 'codex',並依照登入提示操作。完成後,請重新整理或重新啟動此應用程式。", antigravity_token_expired_title: "Antigravity 驗證錯誤", - antigravity_token_expired_body: "請開啟 Antigravity 並重新登入。完成後,請重新整理或重新啟動此應用程式。", + antigravity_token_expired_body: "請在終端機中執行 'agy' 並重新登入。完成後,請重新整理或重新啟動此應用程式。", codex_window_title: "Codex 使用量監控", antigravity_window_title: "Antigravity 使用量監控", second_suffix: "秒", diff --git a/src/native_interop.rs b/src/native_interop.rs index c745d08..7d59b52 100644 --- a/src/native_interop.rs +++ b/src/native_interop.rs @@ -1,5 +1,8 @@ use windows::core::PCWSTR; -use windows::Win32::Foundation::{BOOL, HWND, LPARAM, RECT}; +use windows::Win32::Foundation::{BOOL, HWND, LPARAM, POINT, RECT}; +use windows::Win32::Graphics::Gdi::{ + GetMonitorInfoW, MonitorFromPoint, MONITORINFO, MONITORINFOEXW, MONITOR_DEFAULTTONEAREST, +}; use windows::Win32::UI::Accessibility::{SetWinEventHook, UnhookWinEvent, HWINEVENTHOOK}; use windows::Win32::UI::Shell::{SHAppBarMessage, ABM_GETTASKBARPOS, APPBARDATA}; use windows::Win32::UI::WindowsAndMessaging::*; @@ -61,6 +64,39 @@ pub fn find_taskbars() -> Vec { taskbars } +/// Windows display device number (the `n` in `\\.\DISPLAYn`, matching the +/// numbering shown in display settings for typical setups) of the monitor +/// containing the given rect. +pub fn monitor_number_for_rect(rect: RECT) -> Option { + unsafe { + let center = POINT { + x: rect.left + (rect.right - rect.left) / 2, + y: rect.top + (rect.bottom - rect.top) / 2, + }; + let monitor = MonitorFromPoint(center, MONITOR_DEFAULTTONEAREST); + let mut info = MONITORINFOEXW::default(); + info.monitorInfo.cbSize = std::mem::size_of::() as u32; + if !GetMonitorInfoW( + monitor, + &mut info as *mut MONITORINFOEXW as *mut MONITORINFO, + ) + .as_bool() + { + return None; + } + let device_len = info + .szDevice + .iter() + .position(|unit| *unit == 0) + .unwrap_or(info.szDevice.len()); + let device = String::from_utf16_lossy(&info.szDevice[..device_len]); + device + .rsplit(|c: char| !c.is_ascii_digit()) + .next() + .and_then(|digits| digits.parse::().ok()) + } +} + /// Find a child window by class name pub fn find_child_window(parent: HWND, class_name: &str) -> Option { unsafe { diff --git a/src/poller.rs b/src/poller.rs index a29cd0d..11ffed8 100644 --- a/src/poller.rs +++ b/src/poller.rs @@ -81,6 +81,8 @@ struct CodexRateLimitDetails { struct CodexRateLimitWindow { used_percent: f64, reset_at: i64, + limit_window_seconds: Option, + window_minutes: Option, } #[derive(Deserialize)] @@ -841,16 +843,31 @@ fn codex_usage_from_response(response: CodexUsageResponse) -> Option let mut data = UsageData::default(); if let Some(window) = details.primary_window.flatten() { - data.session = codex_section_from_window(&window); + if codex_window_is_weekly(&window).unwrap_or(false) { + data.weekly = codex_section_from_window(&window); + } else { + data.session = codex_section_from_window(&window); + } } if let Some(window) = details.secondary_window.flatten() { - data.weekly = codex_section_from_window(&window); + if codex_window_is_weekly(&window).unwrap_or(true) { + data.weekly = codex_section_from_window(&window); + } else { + data.session = codex_section_from_window(&window); + } } Some(data) } +fn codex_window_is_weekly(window: &CodexRateLimitWindow) -> Option { + window + .limit_window_seconds + .map(|seconds| seconds >= 24 * 60 * 60) + .or_else(|| window.window_minutes.map(|minutes| minutes >= 24 * 60)) +} + fn codex_section_from_window(window: &CodexRateLimitWindow) -> UsageSection { UsageSection { percentage: window.used_percent, @@ -1614,6 +1631,29 @@ mod tests { } } + #[test] + fn codex_weekly_only_window_stays_in_weekly_slot() { + let response: CodexUsageResponse = serde_json::from_str( + r#"{ + "rate_limit": { + "primary_window": { + "used_percent": 32, + "reset_at": 2000000000, + "limit_window_seconds": 604800 + }, + "secondary_window": null + } + }"#, + ) + .unwrap(); + + let usage = codex_usage_from_response(response).unwrap(); + + assert!(usage.session.resets_at.is_none()); + assert_eq!(usage.weekly.percentage, 32.0); + assert!(usage.weekly.resets_at.is_some()); + } + #[test] fn claude_failure_does_not_block_codex_when_both_are_enabled() { let data = poll_with( diff --git a/src/window.rs b/src/window.rs index bf30f88..daf7b52 100644 --- a/src/window.rs +++ b/src/window.rs @@ -43,6 +43,17 @@ impl SendHwnd { } } +/// Which side of the taskbar the widget anchors to. `tray_offset` is measured +/// from the anchor side: rightwards from the taskbar's left edge when `Left`, +/// leftwards from the tray area when `Right`. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +enum TaskbarSide { + Left, + #[default] + Right, +} + /// Shared application state struct AppState { hwnd: SendHwnd, @@ -84,6 +95,7 @@ struct AppState { last_update_check_unix: Option, taskbar_index: usize, + taskbar_side: TaskbarSide, tray_offset: i32, dragging: bool, drag_start_mouse_x: i32, @@ -117,6 +129,8 @@ 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_SIDE_LEFT: u16 = 32; +const IDM_SIDE_RIGHT: u16 = 33; const IDM_LANG_SYSTEM: u16 = 40; const IDM_LANG_ENGLISH: u16 = 41; const IDM_LANG_DUTCH: u16 = 42; @@ -132,6 +146,9 @@ const IDM_LANG_SIMPLIFIED_CHINESE: u16 = 51; const IDM_MODEL_CLAUDE_CODE: u16 = 60; const IDM_MODEL_CODEX: u16 = 61; const IDM_MODEL_ANTIGRAVITY: u16 = 62; +// Dynamic range: IDM_MONITOR_BASE + taskbar index, one item per detected taskbar. +const IDM_MONITOR_BASE: u16 = 100; +const IDM_MONITOR_MAX: u16 = 131; const WM_DPICHANGED_MSG: u32 = 0x02E0; const WM_APP_UPDATE_CHECK_COMPLETE: u32 = WM_APP + 2; @@ -303,6 +320,8 @@ struct SettingsFile { tray_offset: i32, #[serde(default)] taskbar_index: usize, + #[serde(default)] + taskbar_side: TaskbarSide, #[serde(default = "default_poll_interval")] poll_interval_ms: u32, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -324,6 +343,7 @@ impl Default for SettingsFile { Self { tray_offset: 0, taskbar_index: 0, + taskbar_side: TaskbarSide::default(), poll_interval_ms: default_poll_interval(), language: None, last_update_check_unix: None, @@ -383,6 +403,7 @@ fn save_state_settings() { save_settings(&SettingsFile { tray_offset: s.tray_offset, taskbar_index: s.taskbar_index, + taskbar_side: s.taskbar_side, poll_interval_ms: s.poll_interval_ms, language: s .language_override @@ -585,10 +606,16 @@ fn offset_for_drop_point( taskbar_rect: RECT, pt: POINT, drag_start_client_x: i32, + taskbar_side: TaskbarSide, ) -> i32 { - let tray_left = tray_left_for_taskbar(taskbar_hwnd, taskbar_rect); let desired_left = pt.x - taskbar_rect.left - drag_start_client_x; - let offset = tray_left - taskbar_rect.left - total_widget_width() - desired_left; + let offset = match taskbar_side { + TaskbarSide::Right => { + let tray_left = tray_left_for_taskbar(taskbar_hwnd, taskbar_rect); + tray_left - taskbar_rect.left - total_widget_width() - desired_left + } + TaskbarSide::Left => desired_left, + }; clamp_offset_for_taskbar(taskbar_hwnd, taskbar_rect, offset) } @@ -654,8 +681,18 @@ fn refresh_usage_texts(state: &mut AppState) { } 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); + state.codex_session_text = + if codex.session.resets_at.is_none() && codex.session.percentage == 0.0 { + String::new() + } else { + poller::format_line(&codex.session, strings) + }; + state.codex_weekly_text = + if codex.weekly.resets_at.is_none() && codex.weekly.percentage == 0.0 { + String::new() + } else { + poller::format_line(&codex.weekly, strings) + }; } else if state.show_codex { state.codex_session_text = "!".to_string(); state.codex_weekly_text = "!".to_string(); @@ -1322,6 +1359,7 @@ pub fn run() { update_status: UpdateStatus::Idle, last_update_check_unix: settings.last_update_check_unix, taskbar_index: settings.taskbar_index, + taskbar_side: settings.taskbar_side, tray_offset: settings.tray_offset, dragging: false, drag_start_mouse_x: 0, @@ -2065,7 +2103,7 @@ 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 (hwnd, embedded, tray_offset, taskbar_hwnd, taskbar_side) = { let state = lock_state(); let s = match state.as_ref() { Some(s) => s, @@ -2085,7 +2123,13 @@ fn position_at_taskbar() { } }; - (s.hwnd.to_hwnd(), s.embedded, s.tray_offset, taskbar_hwnd) + ( + s.hwnd.to_hwnd(), + s.embedded, + s.tray_offset, + taskbar_hwnd, + s.taskbar_side, + ) }; let taskbar_rect = match native_interop::get_taskbar_rect(taskbar_hwnd) { @@ -2131,7 +2175,10 @@ fn position_at_taskbar() { let y = compute_anchor_y(anchor_top, anchor_height, widget_height); if embedded { // Child window: coordinates relative to parent (taskbar) - let x = tray_left - taskbar_rect.left - widget_width - tray_offset; + let x = match taskbar_side { + TaskbarSide::Right => tray_left - taskbar_rect.left - widget_width - tray_offset, + TaskbarSide::Left => 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}", @@ -2139,7 +2186,10 @@ fn position_at_taskbar() { )); } else { // Topmost popup: screen coordinates - let x = tray_left - widget_width - tray_offset; + let x = match taskbar_side { + TaskbarSide::Right => tray_left - widget_width - tray_offset, + TaskbarSide::Left => taskbar_rect.left + 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}" @@ -2375,11 +2425,15 @@ unsafe extern "system" fn wnd_proc( None => return LRESULT(0), }; - // Moving mouse left = positive delta = larger offset (further left) - let delta = s.drag_start_mouse_x - pt.x; + // Positive delta = further from the anchor side: moving the + // mouse left when right-anchored, right when left-anchored. + let delta = match s.taskbar_side { + TaskbarSide::Right => s.drag_start_mouse_x - pt.x, + TaskbarSide::Left => pt.x - s.drag_start_mouse_x, + }; let mut new_offset = s.drag_start_offset + delta; - // Clamp: offset >= 0 (can't go right of default) + // Clamp: offset >= 0 (can't go past the anchor side) if new_offset < 0 { new_offset = 0; } @@ -2414,10 +2468,21 @@ unsafe extern "system" fn wnd_proc( 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 + let x = match s.taskbar_side { + TaskbarSide::Right => { + if embedded { + tray_left - taskbar_rect.left - widget_width - new_offset + } else { + tray_left - widget_width - new_offset + } + } + TaskbarSide::Left => { + if embedded { + new_offset + } else { + taskbar_rect.left + new_offset + } + } }; Some(( hwnd_val, @@ -2464,7 +2529,7 @@ unsafe extern "system" fn wnd_proc( if let Some(s) = state.as_mut() { if s.dragging { s.dragging = false; - Some((s.taskbar_index, s.drag_start_client_x)) + Some((s.taskbar_index, s.drag_start_client_x, s.taskbar_side)) } else { None } @@ -2472,7 +2537,7 @@ unsafe extern "system" fn wnd_proc( None } }; - if let Some((current_taskbar_index, drag_start_client_x)) = drag_result { + if let Some((current_taskbar_index, drag_start_client_x, taskbar_side)) = drag_result { let _ = ReleaseCapture(); if let Some((target_index, target_taskbar)) = taskbar_at_point(pt) { if target_index != current_taskbar_index { @@ -2481,6 +2546,7 @@ unsafe extern "system" fn wnd_proc( target_taskbar.rect, pt, drag_start_client_x, + taskbar_side, ); { let mut state = lock_state(); @@ -2574,6 +2640,32 @@ unsafe extern "system" fn wnd_proc( save_state_settings(); position_at_taskbar(); } + IDM_SIDE_LEFT | IDM_SIDE_RIGHT => { + let new_side = if id == IDM_SIDE_LEFT { + TaskbarSide::Left + } else { + TaskbarSide::Right + }; + let changed = { + let mut state = lock_state(); + if let Some(s) = state.as_mut() { + if s.taskbar_side != new_side { + s.taskbar_side = new_side; + s.tray_offset = 0; + true + } else { + false + } + } else { + false + } + }; + if changed { + save_state_settings(); + position_at_taskbar(); + render_layered(); + } + } IDM_START_WITH_WINDOWS => { set_startup_enabled(!is_startup_enabled()); } @@ -2670,6 +2762,20 @@ unsafe extern "system" fn wnd_proc( save_state_settings(); render_layered(); } + id if (IDM_MONITOR_BASE..=IDM_MONITOR_MAX).contains(&id) => { + let target_index = (id - IDM_MONITOR_BASE) as usize; + let current_index = { + let state = lock_state(); + state.as_ref().map(|s| s.taskbar_index) + }; + if current_index.is_some_and(|current| current != target_index) + && attach_to_taskbar(hwnd, target_index) + { + save_state_settings(); + position_at_taskbar(); + render_layered(); + } + } id if id == tray_icon::IDM_TOGGLE_WIDGET => { toggle_widget_visibility(hwnd); } @@ -2718,6 +2824,8 @@ fn show_context_menu(hwnd: HWND) { show_claude_code, show_codex, show_antigravity, + taskbar_side, + taskbar_index, ) = { let state = lock_state(); match state.as_ref() { @@ -2732,6 +2840,8 @@ fn show_context_menu(hwnd: HWND) { s.show_claude_code, s.show_codex, s.show_antigravity, + s.taskbar_side, + s.taskbar_index, ), None => ( POLL_15_MIN, @@ -2744,6 +2854,8 @@ fn show_context_menu(hwnd: HWND) { true, false, false, + TaskbarSide::default(), + 0, ), } }; @@ -2862,6 +2974,72 @@ fn show_context_menu(hwnd: HWND) { PCWSTR::from_raw(reset_pos_str.as_ptr()), ); + // Taskbar side submenu + let side_menu = CreatePopupMenu().unwrap(); + let side_items: [(u16, TaskbarSide, &str); 2] = [ + (IDM_SIDE_LEFT, TaskbarSide::Left, strings.taskbar_side_left), + ( + IDM_SIDE_RIGHT, + TaskbarSide::Right, + strings.taskbar_side_right, + ), + ]; + for (id, side, label) in side_items { + let label_str = native_interop::wide_str(label); + let flags = if side == taskbar_side { + MF_CHECKED + } else { + MENU_ITEM_FLAGS(0) + }; + let _ = AppendMenuW( + side_menu, + flags, + id as usize, + PCWSTR::from_raw(label_str.as_ptr()), + ); + } + + let side_label = native_interop::wide_str(strings.taskbar_side); + let _ = AppendMenuW( + settings_menu, + MF_POPUP, + side_menu.0 as usize, + PCWSTR::from_raw(side_label.as_ptr()), + ); + + // Monitor submenu: one entry per detected taskbar (only shown when + // there is more than one) + let taskbars = native_interop::find_taskbars(); + if taskbars.len() > 1 { + let monitor_menu = CreatePopupMenu().unwrap(); + let max_items = (IDM_MONITOR_MAX - IDM_MONITOR_BASE + 1) as usize; + for (index, taskbar) in taskbars.iter().take(max_items).enumerate() { + let number = native_interop::monitor_number_for_rect(taskbar.rect) + .unwrap_or(index as u32 + 1); + let label = format!("{} {}", strings.monitor, number); + let label_str = native_interop::wide_str(&label); + let flags = if index == taskbar_index { + MF_CHECKED + } else { + MENU_ITEM_FLAGS(0) + }; + let _ = AppendMenuW( + monitor_menu, + flags, + IDM_MONITOR_BASE as usize + index, + PCWSTR::from_raw(label_str.as_ptr()), + ); + } + + let monitor_label = native_interop::wide_str(strings.monitor); + let _ = AppendMenuW( + settings_menu, + MF_POPUP, + monitor_menu.0 as usize, + PCWSTR::from_raw(monitor_label.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() { @@ -3160,17 +3338,19 @@ fn draw_row( 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, - ); + if !codex_text.is_empty() { + 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 { From 923436d5a337555de486c7ac188798864dab9aba Mon Sep 17 00:00:00 2001 From: Kushal Shrestha Date: Tue, 28 Jul 2026 18:11:25 +0545 Subject: [PATCH 2/2] Fix taskbar placement and usage handling --- working.md | 232 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 working.md diff --git a/working.md b/working.md new file mode 100644 index 0000000..87e4809 --- /dev/null +++ b/working.md @@ -0,0 +1,232 @@ +# Working State — Taskbar Side + Monitor Selection + +**Session date:** 2026-07-28 +**Repo:** `F:\Projects\Coding Projects\Claude-Code-Usage-Monitor` +**Branch:** `main` — HEAD is `7b108da v1.4.9` +**Status:** Two features implemented, compile-verified, **not committed**. A test exe was built for the user, who is currently testing it. + +This file is a handoff for another agent. It describes what was asked, what was changed, how it was verified, the environment landmines on this machine, and what is left to do. + +--- + +## 1. What the user asked for + +Three requests, in order: + +1. **Taskbar side option.** The widget only ever anchored to the right side of the taskbar (next to the tray), which on this user's machine overlapped their other app icons. They wanted an option to pick left or right so their widgets stack on the left. They run it with Claude Code and Codex usage both enabled. +2. **Monitor selection.** Multi-monitor setup, widget is on monitor 1. They wanted to select which monitor's taskbar hosts the widget (previously only possible by dragging the widget onto another taskbar). +3. **Build an exe** so they can test on Windows. Done — see §5. + +A fourth request is **pending** (see §7): after testing, add per-provider "sign in" menu items to the widget/tray menus. + +--- + +## 2. Architecture context (what you need to know before editing) + +The app is a Rust Win32 program (no framework). Key files: + +| File | Role | +|---|---| +| `src/window.rs` (~3300 lines) | Window creation, `AppState`, settings load/save, positioning, drag handling, `wnd_proc`, context menu | +| `src/native_interop.rs` | Thin Win32 wrappers — taskbar enumeration, window embedding, monitor lookup | +| `src/tray_icon.rs` | Notification-area icons. Owns `IDM_TOGGLE_WIDGET: u16 = 70` | +| `src/poller.rs` (~1735 lines) | Credential reading + usage polling for all three providers | +| `src/localization/` | `mod.rs` defines `struct Strings`; 11 per-language files each with a `STRINGS` const | + +**How the widget positions itself:** It is reparented as a **child window of the taskbar** (`Shell_TrayWnd` / `Shell_SecondaryTrayWnd`) via `native_interop::embed_in_taskbar`. There is a fallback "topmost popup" mode using screen coordinates when embedding fails — **every positioning change must handle both modes.** `position_at_taskbar()` in `window.rs` is the single source of truth for placement; the drag handler in `wnd_proc` (`WM_MOUSEMOVE`) duplicates the math for live feedback, so the two must stay in sync. + +**Important gotcha:** adding a field to `Strings` in `localization/mod.rs` breaks the build until **all 11** language files are updated, since each `STRINGS` is a struct literal. + +--- + +## 3. Feature 1 — Taskbar Side (left/right) + +### Design + +New enum in `window.rs:51`: + +```rust +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +enum TaskbarSide { Left, #[default] Right } +``` + +`Right` is the default, so **existing installs behave exactly as before**. The existing `tray_offset: i32` field was reinterpreted rather than replaced: it is now the distance **from the anchored side**, growing away from that side. In `Right` mode it is measured leftward from the tray area (original behavior, unchanged); in `Left` mode it is measured rightward from the taskbar's left edge. + +### Changes in `src/window.rs` + +- `window.rs:51` — `TaskbarSide` enum (above). +- `window.rs:98` — `taskbar_side: TaskbarSide` added to `AppState`. +- `window.rs:132-133` — `IDM_SIDE_LEFT: u16 = 32`, `IDM_SIDE_RIGHT: u16 = 33`. +- `window.rs:324, 346, 406, 1352` — `taskbar_side` threaded through `SettingsFile` (with `#[serde(default)]`), its `Default` impl, `save_state_settings()`, and `AppState` construction in `run()`. +- `window.rs:609-618` — `offset_for_drop_point()` gained a `taskbar_side` parameter; in `Left` mode the offset is simply `desired_left`, skipping the tray-relative math. +- `window.rs:2096-2124` — `position_at_taskbar()` now also reads `taskbar_side` out of the state lock. +- `window.rs:2168, 2179` — the two `x` computations (embedded child vs. fallback popup) became `match taskbar_side` blocks. Left mode uses `tray_offset` (child coords) or `taskbar_rect.left + tray_offset` (screen coords). +- `window.rs:2420` — drag delta is **mirrored** per side: `Right` uses `drag_start_mouse_x - pt.x`, `Left` uses `pt.x - drag_start_mouse_x`. Without this the widget would move opposite to the cursor in left mode. +- `window.rs:2461` — live-drag `x` computation became a nested `match` over side × embedded. +- `window.rs:2522-2539` — `WM_LBUTTONUP` drag_result tuple carries `taskbar_side` into `offset_for_drop_point`, so cross-monitor drops respect the side. +- `window.rs:2633-2657` — `IDM_SIDE_LEFT | IDM_SIDE_RIGHT` handler. On an actual change it sets the side, **resets `tray_offset` to 0** (so the widget snaps flush to the newly chosen side), saves, repositions, re-renders. +- `window.rs:2817, 2833` — `show_context_menu` reads `taskbar_side` for the checkmark. +- `window.rs:2966-2998` — builds the **Settings → Taskbar Side** submenu with `MF_CHECKED` on the active side. + +### Known minor behavior + +In Left mode, offset 0 sits flush against the far-left edge of the taskbar, which on Windows 11 is where the Widgets/weather button lives. The user was told to drag the widget right once if that's in the way; the offset persists. + +--- + +## 4. Feature 2 — Monitor selection + +### Design + +The infrastructure already existed: `AppState.taskbar_index` / `SettingsFile.taskbar_index` were already persisted and already used by the drag-to-another-taskbar feature, and `attach_to_taskbar(hwnd, index)` already handled re-parenting, re-hooking the tray WinEvent hook, and updating state. **This feature is just a menu in front of the existing code path** — that is why it is small. + +Menu IDs are a dynamic range so the item count can vary with the number of taskbars: + +```rust +const IDM_MONITOR_BASE: u16 = 100; +const IDM_MONITOR_MAX: u16 = 131; // 32 taskbars max +``` + +Range chosen to sit clear of all existing IDs (highest previously was `IDM_TOGGLE_WIDGET = 70` in `tray_icon.rs`). + +### Changes in `src/native_interop.rs` + +- Imports extended with `POINT`, and `Graphics::Gdi::{GetMonitorInfoW, MonitorFromPoint, MONITORINFO, MONITORINFOEXW, MONITOR_DEFAULTTONEAREST}`. (`Win32_Graphics_Gdi` was already in `Cargo.toml` features — no manifest change needed.) +- `native_interop.rs:70` — new `pub fn monitor_number_for_rect(rect: RECT) -> Option`. Takes the center point of a taskbar rect → `MonitorFromPoint(MONITOR_DEFAULTTONEAREST)` → `GetMonitorInfoW` into a `MONITORINFOEXW` → parses the trailing digits of the `\\.\DISPLAYn` device name. This gives labels that match what Windows display settings shows in typical setups. + +### Changes in `src/window.rs` + +- `window.rs:2755-2769` — handler for `id if (IDM_MONITOR_BASE..=IDM_MONITOR_MAX).contains(&id)`. Computes `target_index = id - IDM_MONITOR_BASE`, no-ops if it equals the current index, otherwise calls `attach_to_taskbar` → `save_state_settings` → `position_at_taskbar` → `render_layered`. + **Placement matters:** this arm is a guard pattern and must stay **before** the `id if id == tray_icon::IDM_TOGGLE_WIDGET` arm and after the literal-ID arms. +- `window.rs:2817, 2833` — `show_context_menu` also reads `taskbar_index`. +- `window.rs:3000-3033` — builds **Settings → Monitor** submenu. Calls `native_interop::find_taskbars()` fresh each time the menu opens (monitors can be hot-plugged). **The submenu is only appended when `taskbars.len() > 1`**, so single-monitor users see no change. Items are capped at `max_items` to stay inside the ID range. Label is `format!("{} {}", strings.monitor, number)`, falling back to `index + 1` if the display number can't be read. + +### Note for the user (already communicated) + +Windows only creates taskbars on secondary monitors when *"Show my taskbar on all displays"* is enabled in Windows taskbar settings. If it's off there is genuinely only one taskbar and the Monitor submenu correctly stays hidden. + +--- + +## 5. Localization + +`localization/mod.rs` `struct Strings` gained **four** fields: + +```rust +pub taskbar_side: &'static str, +pub taskbar_side_left: &'static str, +pub taskbar_side_right: &'static str, +pub monitor: &'static str, +``` + +All 11 language files were updated (en, nl, es, fr, de, ja, ko, zh-TW, zh-CN, ru, pt-BR). Sample values — English: `"Taskbar Side" / "Left" / "Right" / "Monitor"`; French uses `"Écran"` for monitor; Japanese `"モニター"`; Simplified Chinese `"显示器"`; Traditional Chinese `"顯示器"`; Korean `"모니터"`; Russian `"Монитор"`. Dutch/German/Spanish/Portuguese keep `"Monitor"`. + +--- + +## 6. Verification — and the two environment landmines + +### ⚠️ Landmine 1: Smart App Control blocks all native builds + +**`cargo build` cannot run natively on this machine.** Smart App Control (SAC) is **enabled** (confirmed: `HKLM:\SYSTEM\CurrentControlSet\Control\CI\Policy` → `VerifiedAndReputablePolicyState = 1`). It blocks execution of freshly compiled unsigned binaries, including **cargo's own build-script executables**. A native `cargo check` fails with: + +``` +could not execute process .../build-script-build (never executed) +Caused by: An Application Control policy has blocked this file. (os error 4551) +``` + +Disabling SAC is **irreversible** (cannot be re-enabled without resetting Windows), so it was **left enabled** — that is the user's call, not an agent's. Do not disable it without explicit instruction. + +Installed during this session anyway (harmless, may be useful if SAC is ever turned off): `Rustlang.Rustup` (rustc 1.97.1, msvc host) and `Microsoft.VisualStudio.2022.BuildTools` with the VC++ workload. Cargo lives at `C:\Users\kuu\.cargo\bin\cargo.exe` and is **not on PATH** — invoke by full path. + +### The working build path: cross-compile from WSL + +WSL Ubuntu is used as the build host, targeting `x86_64-pc-windows-gnu`. Set up during this session (persisted in the distro): + +- `apt-get install gcc mingw-w64 libc6-dev` (as root) +- rustup installed for the WSL **root** user, plus target `x86_64-pc-windows-gnu` +- `ln -sf /usr/bin/x86_64-w64-mingw32-windres /usr/local/bin/windres` — the `winres` build-dep invokes the unprefixed name +- Capitalized→lowercase import-lib symlinks in `/usr/x86_64-w64-mingw32/lib` (e.g. `libAdvapi32.a → libadvapi32.a`). The `windows` crate emits `-lAdvapi32` etc., which fails on a case-sensitive filesystem. Script kept at: + `C:\Users\kuu\AppData\Local\Temp\claude\F--Projects-Coding-Projects-Claude-Code-Usage-Monitor\fb1a92aa-896b-4516-9ca9-24859f4d1a06\scratchpad\fix-libs.sh` + +**Build recipe** (copies the tree into the WSL filesystem — building directly on `/mnt/f` is slow): + +```bash +wsl.exe -d Ubuntu -e bash -lc "rm -rf ~/ccum && mkdir -p ~/ccum && cd '/mnt/f/Projects/Coding Projects/Claude-Code-Usage-Monitor' && cp -r src build.rs Cargo.toml Cargo.lock ~/ccum/ && source ~/.cargo/env && cd ~/ccum && cargo build --release --target x86_64-pc-windows-gnu" +``` + +### ⚠️ Landmine 2: GNU ld drops the resource object + +With the GNU toolchain the icon/version resources compiled by `winres` land in `libresource.a` but get **garbage-collected out of the final binary** — the exe ends up with no `.rsrc` section (no icon, no version metadata). Fix is to force-link the object file directly: + +```bash +RUSTFLAGS='-C link-arg=/root/ccum/target/x86_64-pc-windows-gnu/release/build/claude-code-usage-monitor-/out/resource.o' \ + cargo build --release --target x86_64-pc-windows-gnu +``` + +The `` directory varies per build — locate it with `find ~/ccum/target -name resource.o`. Verify success with `objdump -h | grep -i rsrc` (expect a `.rsrc` section) and on the Windows side by reading `VersionInfo`. + +**None of this affects official releases** — CI (`.github/workflows/release.yml`) builds with MSVC on GitHub runners, where `build.rs`/`winres` work normally. These workarounds exist solely to produce a local test binary on a SAC-locked machine. + +### Verification actually performed + +- `cargo check --target x86_64-pc-windows-gnu` on the full tree after **both** features: **compiles clean, zero errors, zero warnings.** (For type-checking only, `build.rs` was stubbed to `fn main() {}` in the throwaway copy; the real `build.rs` is untouched in the repo and was used for the actual exe build.) +- Release exe built, `.rsrc` section confirmed present, `FileVersion 1.4.9 / Product "Claude Code Usage Monitor" / Company "Code Zeno Pty Ltd"` read back on Windows. +- **No runtime testing has been done by an agent.** SAC very likely prevents the unsigned test exe from launching at all. Runtime behavior of both features is **unverified** and rests with the user's testing. + +### Test exe location + +``` +F:\Projects\Coding Projects\Claude-Code-Usage-Monitor\target\claude-code-usage-monitor-test.exe +``` + +`target/` is gitignored, so it will not be committed. The user was told to exit the running instance (tray → Exit) before launching it, since two instances fight over the taskbar. + +--- + +## 7. Outstanding work + +### a) Commit the changes — **not yet done, awaiting user's word** + +15 modified files, no commits made. The user was asked whether to use one commit or one per feature and has **not answered**. Do not commit without confirmation. Note `working.md` (this file) is a new untracked file and is presumably a scratch/handoff artifact — confirm whether the user wants it tracked. + +### b) Pending feature request: in-menu auth shortcuts + +The user enabled all three providers and could not find any sign-in option. Explained: **the app never authenticates**; it only reads credentials that each tool stores itself. Diagnostic run this session found **all three credentials already present**: + +- Claude: `C:\Users\kuu\.claude\.credentials.json` ✔ (`claude` CLI on PATH ✔) +- Codex: `C:\Users\kuu\.codex\auth.json` ✔ — but **the `codex` CLI is NOT on PATH** ✖. This matters beyond login: `poller.rs:404 cli_refresh_codex_token()` shells out to `codex` to refresh an expired token, so auto-refresh is currently broken. Fix is `npm install -g @openai/codex`. +- Antigravity: Credential Manager target `gemini:antigravity` ✔ (sign-in happens in the Antigravity IDE; no CLI) + +Manual commands given to the user: `claude` then `/login`; `codex login`; for Antigravity, sign in inside the IDE. Then **Refresh** from the right-click menu. + +**The agreed next step**, to be done after the user finishes testing the current build: add per-provider "Sign in…" items to the widget right-click menu and the tray menus — launching a terminal running `claude` / `codex login`, and launching the Antigravity IDE for the third. Suggested implementation notes: +- Reuse the credential-path/CLI-resolution helpers already in `poller.rs` (`resolve_windows_codex_path`, `codex_auth_path`, `windows_credential_source`) rather than re-deriving paths. +- Menu IDs: pick a fresh block clear of `IDM_MONITOR_BASE..=IDM_MONITOR_MAX` (100–131) — e.g. 140+. +- Four new `Strings` fields will be needed (a submenu label plus three provider items) → **all 11 language files must be updated**. +- Codex's CLI may be absent; the menu item should degrade gracefully (surface a helpful message rather than silently failing). + +### c) Optional cleanup + +VS Build Tools (several GB) is unusable while SAC is on; the user may want to uninstall it. Rustup is small. The user's decision. + +--- + +## 8. Quick reference + +**Settings file:** `%APPDATA%\ClaudeCodeUsageMonitor\settings.json` — new key `taskbar_side` (`"left"` / `"right"`, defaults to `"right"`); `taskbar_index` (pre-existing) now also settable from the Monitor menu. + +**Menu ID map after this session:** + +| ID(s) | Meaning | +|---|---| +| 1, 2 | Refresh, Exit | +| 10–13 | Poll frequency | +| 20 | Start with Windows | +| 30, 31 | Reset Position, Version action | +| **32, 33** | **Taskbar Side left / right (new)** | +| 40–51 | Language | +| 60–62 | Models | +| 70 | Toggle widget (in `tray_icon.rs`) | +| **100–131** | **Monitor selection, dynamic (new)** | + +**Diagnostics:** `claude-code-usage-monitor --diagnose` → log at `%TEMP%\claude-code-usage-monitor.log`. `position_at_taskbar()` logs the computed x/y/w/h every time, which is the fastest way to debug placement problems.