mirror of
https://github.com/CodeZeno/Claude-Code-Usage-Monitor.git
synced 2026-06-20 10:19:27 +08:00
Merge 14273a7095c83676df311d58a47fccae938e66c2 into 7af314996d19265de3b93f7afc4b7a5c173c488c
This commit is contained in:
commit
fbd5904953
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -38,7 +38,7 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "claude-code-usage-monitor"
|
||||
version = "1.4.3"
|
||||
version = "1.4.4"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"native-tls",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "claude-code-usage-monitor"
|
||||
version = "1.4.3"
|
||||
version = "1.4.4"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
description = "Claude Code Usage Monitor"
|
||||
|
||||
20
README.md
20
README.md
@ -5,15 +5,16 @@
|
||||
|
||||

|
||||
|
||||
A lightweight Windows taskbar widget for people already using Claude Code, with optional Codex usage display.
|
||||
A lightweight Windows taskbar widget for people already using Claude Code, with optional Codex and Google Antigravity usage display.
|
||||
|
||||
It sits in your taskbar and shows how much of your Claude Code and/or Codex usage window you have left, without needing to open the terminal or the provider site.
|
||||
It sits in your taskbar and shows how much of your Claude Code, Codex, and/or Antigravity usage window you have left, without needing to open the terminal or the provider site.
|
||||
|
||||
## What You Get
|
||||
|
||||
- A **5h** bar for your current 5-hour Claude usage window
|
||||
- A **7d** bar for your current 7-day window
|
||||
- Optional Codex usage bars alongside Claude Code
|
||||
- Optional Antigravity usage bars for Google's 5-hour and broader user quota windows
|
||||
- A live countdown until each limit resets
|
||||
- A small native widget that lives directly in the Windows taskbar
|
||||
- System tray icon badges showing your enabled model usage percentage
|
||||
@ -26,6 +27,8 @@ 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 Antigravity 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.
|
||||
|
||||
## Requirements
|
||||
@ -33,6 +36,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
|
||||
|
||||
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.
|
||||
|
||||
@ -67,16 +71,17 @@ Use the right-click **Models** menu to choose what the widget displays:
|
||||
|
||||
- **Claude Code** is enabled by default
|
||||
- **Codex** can be enabled alongside Claude Code or shown by itself
|
||||
- **Antigravity** can be enabled alongside the other providers or shown by itself
|
||||
|
||||
When both models are shown, each model has its own usage bar and matching usage text color.
|
||||
When multiple models are shown, each model has its own usage bar and matching usage text color.
|
||||
|
||||
### System Tray Icon
|
||||
|
||||
The tray icon shows your current 5-hour usage as a percentage badge.
|
||||
|
||||
If both Claude Code and Codex are enabled, the app shows two tray icons: one for Claude Code and one for Codex. If only one model is enabled, it shows one tray icon.
|
||||
If multiple providers are enabled, the app shows one tray icon per provider. If only one model is enabled, it shows one tray icon.
|
||||
|
||||
The Claude Code tray icon uses the same warm usage colors as the Claude bar. The Codex tray icon uses a black and white badge style.
|
||||
The Claude Code tray icon uses the same warm usage colors as the Claude bar. The Codex tray icon uses a black and white badge style. The Antigravity tray icon uses a green badge style.
|
||||
|
||||
Hovering over a tray icon shows the usage values for that model.
|
||||
|
||||
@ -120,11 +125,13 @@ What the app reads:
|
||||
- Your local Claude Code OAuth credentials from `~/.claude/.credentials.json`
|
||||
- If needed, the same credentials file inside an installed WSL distro
|
||||
- If Codex is enabled, your local Codex credentials from `$CODEX_HOME/auth.json` or `~/.codex/auth.json`
|
||||
- If Antigravity is enabled, your local Antigravity OAuth token from Windows Credential Manager target `gemini:antigravity`
|
||||
|
||||
What the app sends over the network:
|
||||
|
||||
- Requests to Anthropic's Claude endpoints to read your usage and rate-limit information
|
||||
- Requests to ChatGPT's Codex usage endpoint to read your Codex usage and rate-limit information, if Codex is enabled
|
||||
- Requests to Google's Cloud Code / Antigravity endpoints to read your Antigravity quota information, if Antigravity is enabled
|
||||
- Requests to GitHub only if you use the app's update check / self-update feature
|
||||
- If proxy environment variables such as `HTTPS_PROXY`, `HTTP_PROXY`, or `ALL_PROXY` are set, those outbound requests may use that proxy
|
||||
|
||||
@ -148,6 +155,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.
|
||||
- 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
|
||||
|
||||
@ -156,7 +164,7 @@ Notes:
|
||||
The monitor:
|
||||
|
||||
1. Finds your enabled model login credentials
|
||||
2. Reads your current usage from Anthropic and/or ChatGPT
|
||||
2. Reads your current usage from Anthropic, ChatGPT, and/or Google's Antigravity endpoints
|
||||
3. Shows the result directly in the Windows taskbar
|
||||
4. Refreshes periodically in the background
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ pub(super) const STRINGS: Strings = Strings {
|
||||
models: "Modellen",
|
||||
claude_code_model: "Claude Code",
|
||||
codex_model: "Codex",
|
||||
antigravity_model: "Antigravity",
|
||||
settings: "Instellingen",
|
||||
start_with_windows: "Opstarten met Windows",
|
||||
reset_position: "Positie herstellen",
|
||||
@ -41,6 +42,9 @@ pub(super) const STRINGS: Strings = Strings {
|
||||
token_expired_body: "Voer 'claude' uit in een terminal, gebruik daarna '/login' en volg de stappen. Ververs of herstart de app daarna.",
|
||||
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.",
|
||||
codex_window_title: "Codex-gebruiksmonitor",
|
||||
antigravity_window_title: "Antigravity-gebruiksmonitor",
|
||||
second_suffix: "s",
|
||||
};
|
||||
|
||||
@ -13,6 +13,7 @@ pub(super) const STRINGS: Strings = Strings {
|
||||
models: "Models",
|
||||
claude_code_model: "Claude Code",
|
||||
codex_model: "Codex",
|
||||
antigravity_model: "Antigravity",
|
||||
settings: "Settings",
|
||||
start_with_windows: "Start with Windows",
|
||||
reset_position: "Reset Position",
|
||||
@ -41,6 +42,9 @@ pub(super) const STRINGS: Strings = Strings {
|
||||
token_expired_body: "Run 'claude' in a terminal, then use '/login' and follow the prompts. After that, refresh or restart this app.",
|
||||
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.",
|
||||
codex_window_title: "Codex Usage Monitor",
|
||||
antigravity_window_title: "Antigravity Usage Monitor",
|
||||
second_suffix: "s",
|
||||
};
|
||||
|
||||
@ -13,6 +13,7 @@ pub(super) const STRINGS: Strings = Strings {
|
||||
models: "Modeles",
|
||||
claude_code_model: "Claude Code",
|
||||
codex_model: "Codex",
|
||||
antigravity_model: "Antigravity",
|
||||
settings: "Paramètres",
|
||||
start_with_windows: "Démarrer avec Windows",
|
||||
reset_position: "Réinitialiser la position",
|
||||
@ -41,6 +42,9 @@ pub(super) const STRINGS: Strings = Strings {
|
||||
token_expired_body: "Exécutez 'claude' dans un terminal, puis utilisez '/login' et suivez les instructions. Ensuite, actualisez ou redémarrez cette application.",
|
||||
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.",
|
||||
codex_window_title: "Moniteur d'utilisation Codex",
|
||||
antigravity_window_title: "Moniteur d'utilisation Antigravity",
|
||||
second_suffix: "s",
|
||||
};
|
||||
|
||||
@ -13,6 +13,7 @@ pub(super) const STRINGS: Strings = Strings {
|
||||
models: "Modelle",
|
||||
claude_code_model: "Claude Code",
|
||||
codex_model: "Codex",
|
||||
antigravity_model: "Antigravity",
|
||||
settings: "Einstellungen",
|
||||
start_with_windows: "Mit Windows starten",
|
||||
reset_position: "Position zurücksetzen",
|
||||
@ -41,6 +42,9 @@ pub(super) const STRINGS: Strings = Strings {
|
||||
token_expired_body: "Führen Sie 'claude' in einem Terminal aus, verwenden Sie dann '/login' und folgen Sie den Anweisungen. Aktualisieren oder starten Sie diese App anschließend neu.",
|
||||
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.",
|
||||
codex_window_title: "Codex-Nutzungsmonitor",
|
||||
antigravity_window_title: "Antigravity-Nutzungsmonitor",
|
||||
second_suffix: "s",
|
||||
};
|
||||
|
||||
@ -13,6 +13,7 @@ pub(super) const STRINGS: Strings = Strings {
|
||||
models: "モデル",
|
||||
claude_code_model: "Claude Code",
|
||||
codex_model: "Codex",
|
||||
antigravity_model: "Antigravity",
|
||||
settings: "設定",
|
||||
start_with_windows: "Windows と同時に開始",
|
||||
reset_position: "位置をリセット",
|
||||
@ -41,6 +42,9 @@ pub(super) const STRINGS: Strings = Strings {
|
||||
token_expired_body: "ターミナルで 'claude' を実行し、'/login' を使って案内に従ってください。その後、このアプリを更新するか再起動してください。",
|
||||
codex_token_expired_title: "Codex 認証エラー",
|
||||
codex_token_expired_body: "ターミナルで 'codex' を実行し、サインインの案内に従ってください。その後、このアプリを更新または再起動してください。",
|
||||
antigravity_token_expired_title: "Antigravity 認証エラー",
|
||||
antigravity_token_expired_body: "Antigravity を開いて再度サインインしてください。その後、このアプリを更新するか再起動してください。",
|
||||
codex_window_title: "Codex 使用量モニター",
|
||||
antigravity_window_title: "Antigravity 使用量モニター",
|
||||
second_suffix: "秒",
|
||||
};
|
||||
|
||||
@ -13,6 +13,7 @@ pub(super) const STRINGS: Strings = Strings {
|
||||
models: "모델",
|
||||
claude_code_model: "Claude Code",
|
||||
codex_model: "Codex",
|
||||
antigravity_model: "Antigravity",
|
||||
settings: "설정",
|
||||
start_with_windows: "Windows 시작 시 자동 실행",
|
||||
reset_position: "위치 초기화",
|
||||
@ -41,6 +42,9 @@ pub(super) const STRINGS: Strings = Strings {
|
||||
token_expired_body: "터미널에서 'claude'를 실행한 다음 '/login'을 사용하고 안내에 따라 진행하세요. 그런 다음 이 앱을 새로 고치거나 다시 시작하세요.",
|
||||
codex_token_expired_title: "Codex 인증 오류",
|
||||
codex_token_expired_body: "터미널에서 'codex'를 실행하고 로그인 안내를 따르세요. 그런 다음 이 앱을 새로 고치거나 다시 시작하세요.",
|
||||
antigravity_token_expired_title: "Antigravity 인증 오류",
|
||||
antigravity_token_expired_body: "Antigravity를 열고 다시 로그인하세요. 그런 다음 이 앱을 새로 고치거나 다시 시작하세요.",
|
||||
codex_window_title: "Codex 사용량 모니터",
|
||||
antigravity_window_title: "Antigravity 사용량 모니터",
|
||||
second_suffix: "초",
|
||||
};
|
||||
|
||||
@ -4,10 +4,10 @@ mod french;
|
||||
mod german;
|
||||
mod japanese;
|
||||
mod korean;
|
||||
mod portuguese_brazil;
|
||||
mod russian;
|
||||
mod spanish;
|
||||
mod traditional_chinese;
|
||||
mod russian;
|
||||
mod portuguese_brazil;
|
||||
|
||||
use windows::core::PWSTR;
|
||||
use windows::Win32::Globalization::{
|
||||
@ -147,6 +147,7 @@ pub struct Strings {
|
||||
pub models: &'static str,
|
||||
pub claude_code_model: &'static str,
|
||||
pub codex_model: &'static str,
|
||||
pub antigravity_model: &'static str,
|
||||
pub settings: &'static str,
|
||||
pub start_with_windows: &'static str,
|
||||
pub reset_position: &'static str,
|
||||
@ -176,7 +177,10 @@ pub struct Strings {
|
||||
pub token_expired_body: &'static str,
|
||||
pub codex_token_expired_title: &'static str,
|
||||
pub codex_token_expired_body: &'static str,
|
||||
pub antigravity_token_expired_title: &'static str,
|
||||
pub antigravity_token_expired_body: &'static str,
|
||||
pub codex_window_title: &'static str,
|
||||
pub antigravity_window_title: &'static str,
|
||||
}
|
||||
|
||||
pub fn resolve_language(language_override: Option<LanguageId>) -> LanguageId {
|
||||
|
||||
@ -13,6 +13,7 @@ pub(super) const STRINGS: Strings = Strings {
|
||||
models: "Modelos",
|
||||
claude_code_model: "Claude Code",
|
||||
codex_model: "Codex",
|
||||
antigravity_model: "Antigravity",
|
||||
settings: "Configurações",
|
||||
start_with_windows: "Iniciar com o Windows",
|
||||
reset_position: "Redefinir Posição",
|
||||
@ -42,5 +43,8 @@ pub(super) const STRINGS: Strings = Strings {
|
||||
token_expired_body: "Execute 'claude' em um terminal, use '/login' e siga as instruções. Depois disso, atualize ou reinicie este aplicativo.",
|
||||
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.",
|
||||
codex_window_title: "Monitor de uso do Codex",
|
||||
antigravity_window_title: "Monitor de uso do Antigravity",
|
||||
};
|
||||
|
||||
@ -13,6 +13,7 @@ pub(super) const STRINGS: Strings = Strings {
|
||||
models: "Модели",
|
||||
claude_code_model: "Claude Code",
|
||||
codex_model: "Codex",
|
||||
antigravity_model: "Antigravity",
|
||||
settings: "Настройки",
|
||||
start_with_windows: "Запускать вместе с Windows",
|
||||
reset_position: "Сбросить позицию",
|
||||
@ -42,5 +43,8 @@ pub(super) const STRINGS: Strings = Strings {
|
||||
token_expired_body: "Запустите 'claude' в терминале, затем используйте '/login' и следуйте инструкциям. После этого обновите или перезапустите приложение.",
|
||||
codex_token_expired_title: "Ошибка авторизации Codex",
|
||||
codex_token_expired_body: "Запустите 'codex' в терминале и следуйте инструкциям для входа. После этого обновите или перезапустите приложение.",
|
||||
antigravity_token_expired_title: "Ошибка авторизации Antigravity",
|
||||
antigravity_token_expired_body: "Откройте Antigravity и войдите снова. После этого обновите или перезапустите приложение.",
|
||||
codex_window_title: "Монитор использования Codex",
|
||||
};
|
||||
antigravity_window_title: "Монитор использования Antigravity",
|
||||
};
|
||||
|
||||
@ -13,6 +13,7 @@ pub(super) const STRINGS: Strings = Strings {
|
||||
models: "Modelos",
|
||||
claude_code_model: "Claude Code",
|
||||
codex_model: "Codex",
|
||||
antigravity_model: "Antigravity",
|
||||
settings: "Configuración",
|
||||
start_with_windows: "Iniciar con Windows",
|
||||
reset_position: "Restablecer posición",
|
||||
@ -41,6 +42,9 @@ pub(super) const STRINGS: Strings = Strings {
|
||||
token_expired_body: "Ejecuta 'claude' en una terminal, luego usa '/login' y sigue las indicaciones. Después, actualiza o reinicia esta aplicación.",
|
||||
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.",
|
||||
codex_window_title: "Monitor de uso de Codex",
|
||||
antigravity_window_title: "Monitor de uso de Antigravity",
|
||||
second_suffix: "s",
|
||||
};
|
||||
|
||||
@ -13,6 +13,7 @@ pub(super) const STRINGS: Strings = Strings {
|
||||
models: "模型",
|
||||
claude_code_model: "Claude Code",
|
||||
codex_model: "Codex",
|
||||
antigravity_model: "Antigravity",
|
||||
settings: "設定",
|
||||
start_with_windows: "開機時啟動",
|
||||
reset_position: "重置位置",
|
||||
@ -41,6 +42,9 @@ pub(super) const STRINGS: Strings = Strings {
|
||||
token_expired_body: "請在終端機中執行 'claude',然後使用 '/login' 並依照提示操作。完成後,請重新整理或重新啟動此應用程式。",
|
||||
codex_token_expired_title: "Codex 驗證錯誤",
|
||||
codex_token_expired_body: "請在終端機中執行 'codex',並依照登入提示操作。完成後,請重新整理或重新啟動此應用程式。",
|
||||
antigravity_token_expired_title: "Antigravity 驗證錯誤",
|
||||
antigravity_token_expired_body: "請開啟 Antigravity 並重新登入。完成後,請重新整理或重新啟動此應用程式。",
|
||||
codex_window_title: "Codex 使用量監控",
|
||||
antigravity_window_title: "Antigravity 使用量監控",
|
||||
second_suffix: "秒",
|
||||
};
|
||||
|
||||
@ -15,10 +15,7 @@ fn main() {
|
||||
let diagnose_enabled = args.iter().any(|arg| arg == "--diagnose");
|
||||
if diagnose_enabled {
|
||||
match diagnose::init() {
|
||||
Ok(path) => diagnose::log(format!(
|
||||
"startup args={args:?} log_path={}",
|
||||
path.display()
|
||||
)),
|
||||
Ok(path) => diagnose::log(format!("startup args={args:?} log_path={}", path.display())),
|
||||
Err(error) => {
|
||||
// Logging may not be available yet, but keep startup behavior unchanged.
|
||||
let _ = error;
|
||||
|
||||
@ -16,4 +16,5 @@ pub struct UsageData {
|
||||
pub struct AppUsageData {
|
||||
pub claude_code: Option<UsageData>,
|
||||
pub codex: Option<UsageData>,
|
||||
pub antigravity: Option<UsageData>,
|
||||
}
|
||||
|
||||
533
src/poller.rs
533
src/poller.rs
@ -1,3 +1,7 @@
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::c_void;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
@ -12,6 +16,12 @@ use crate::models::{AppUsageData, UsageData, UsageSection};
|
||||
const USAGE_URL: &str = "https://api.anthropic.com/api/oauth/usage";
|
||||
const MESSAGES_URL: &str = "https://api.anthropic.com/v1/messages";
|
||||
const CODEX_USAGE_URL: &str = "https://chatgpt.com/backend-api/wham/usage";
|
||||
const ANTIGRAVITY_CREDENTIAL_TARGET: &str = "gemini:antigravity";
|
||||
const ANTIGRAVITY_ENDPOINTS: &[&str] = &[
|
||||
"https://daily-cloudcode-pa.googleapis.com",
|
||||
"https://daily-cloudcode-pa.sandbox.googleapis.com",
|
||||
"https://cloudcode-pa.googleapis.com",
|
||||
];
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
|
||||
const MODEL_FALLBACK_CHAIN: &[&str] = &["claude-3-haiku-20240307", "claude-haiku-4-5-20251001"];
|
||||
@ -28,6 +38,7 @@ pub enum PollError {
|
||||
pub enum CredentialWatchMode {
|
||||
ActiveSource,
|
||||
AllSources,
|
||||
Antigravity,
|
||||
}
|
||||
|
||||
pub type CredentialWatchSnapshot = Vec<String>;
|
||||
@ -72,7 +83,99 @@ struct CodexRateLimitWindow {
|
||||
reset_at: i64,
|
||||
}
|
||||
|
||||
pub fn poll(show_claude_code: bool, show_codex: bool) -> Result<AppUsageData, PollError> {
|
||||
#[derive(Deserialize)]
|
||||
struct AntigravityAuthFile {
|
||||
token: AntigravityTokenData,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AntigravityTokenData {
|
||||
access_token: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AntigravityLoadResponse {
|
||||
#[serde(rename = "cloudaicompanionProject")]
|
||||
project: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AntigravityModelsResponse {
|
||||
models: HashMap<String, AntigravityModelInfo>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AntigravityModelInfo {
|
||||
#[serde(rename = "quotaInfo")]
|
||||
quota_info: Option<AntigravityQuotaInfo>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AntigravityQuotaInfo {
|
||||
#[serde(rename = "remainingFraction")]
|
||||
remaining_fraction: Option<f64>,
|
||||
#[serde(rename = "resetTime")]
|
||||
reset_time: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AntigravityQuotaSummaryResponse {
|
||||
groups: Option<Vec<AntigravityQuotaSummaryGroup>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AntigravityQuotaSummaryGroup {
|
||||
#[serde(rename = "displayName")]
|
||||
display_name: Option<String>,
|
||||
description: Option<String>,
|
||||
buckets: Option<Vec<AntigravityQuotaSummaryBucket>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
struct AntigravityQuotaSummaryBucket {
|
||||
#[serde(rename = "bucketId")]
|
||||
bucket_id: Option<String>,
|
||||
#[serde(rename = "displayName")]
|
||||
display_name: Option<String>,
|
||||
window: Option<String>,
|
||||
#[serde(rename = "remainingFraction")]
|
||||
remaining_fraction: Option<f64>,
|
||||
#[serde(rename = "resetTime")]
|
||||
reset_time: Option<String>,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
struct CredentialW {
|
||||
flags: u32,
|
||||
type_: u32,
|
||||
target_name: *mut u16,
|
||||
comment: *mut u16,
|
||||
last_written: u64,
|
||||
credential_blob_size: u32,
|
||||
credential_blob: *mut u8,
|
||||
persist: u32,
|
||||
attribute_count: u32,
|
||||
attributes: *mut c_void,
|
||||
target_alias: *mut u16,
|
||||
user_name: *mut u16,
|
||||
}
|
||||
|
||||
#[link(name = "Advapi32")]
|
||||
extern "system" {
|
||||
fn CredReadW(
|
||||
target_name: *const u16,
|
||||
type_: u32,
|
||||
reserved_flags: u32,
|
||||
credential: *mut *mut CredentialW,
|
||||
) -> i32;
|
||||
fn CredFree(buffer: *mut c_void);
|
||||
}
|
||||
|
||||
pub fn poll(
|
||||
show_claude_code: bool,
|
||||
show_codex: bool,
|
||||
show_antigravity: bool,
|
||||
) -> Result<AppUsageData, PollError> {
|
||||
let mut data = AppUsageData::default();
|
||||
|
||||
if show_claude_code {
|
||||
@ -82,12 +185,20 @@ pub fn poll(show_claude_code: bool, show_codex: bool) -> Result<AppUsageData, Po
|
||||
if show_codex {
|
||||
match poll_codex() {
|
||||
Ok(codex) => data.codex = Some(codex),
|
||||
Err(error) if !show_claude_code => return Err(error),
|
||||
Err(error) if !show_claude_code && !show_antigravity => return Err(error),
|
||||
Err(error) => diagnose::log(format!("Codex usage poll failed: {error:?}")),
|
||||
}
|
||||
}
|
||||
|
||||
if data.claude_code.is_none() && data.codex.is_none() {
|
||||
if show_antigravity {
|
||||
match poll_antigravity() {
|
||||
Ok(antigravity) => data.antigravity = Some(antigravity),
|
||||
Err(error) if !show_claude_code && !show_codex => return Err(error),
|
||||
Err(error) => diagnose::log(format!("Antigravity usage poll failed: {error:?}")),
|
||||
}
|
||||
}
|
||||
|
||||
if data.claude_code.is_none() && data.codex.is_none() && data.antigravity.is_none() {
|
||||
Err(PollError::RequestFailed)
|
||||
} else {
|
||||
Ok(data)
|
||||
@ -128,6 +239,18 @@ fn poll_codex() -> Result<UsageData, PollError> {
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_antigravity() -> Result<UsageData, PollError> {
|
||||
let creds = match read_antigravity_credentials() {
|
||||
Some(creds) => creds,
|
||||
None => {
|
||||
diagnose::log("Antigravity usage poll failed: no Antigravity credentials found");
|
||||
return Err(PollError::NoCredentials);
|
||||
}
|
||||
};
|
||||
|
||||
fetch_antigravity_usage(&creds.access_token)
|
||||
}
|
||||
|
||||
fn refresh_or_fallback(mut creds: Credentials) -> Result<Credentials, PollError> {
|
||||
loop {
|
||||
if !is_token_expired(creds.expires_at) {
|
||||
@ -405,11 +528,16 @@ fn build_agent() -> Result<ureq::Agent, PollError> {
|
||||
}
|
||||
|
||||
pub fn credential_watch_snapshot(mode: CredentialWatchMode) -> CredentialWatchSnapshot {
|
||||
if mode == CredentialWatchMode::Antigravity {
|
||||
return vec![antigravity_credential_watch_signature()];
|
||||
}
|
||||
|
||||
let sources = match mode {
|
||||
CredentialWatchMode::ActiveSource => read_first_credentials()
|
||||
.map(|creds| vec![creds.source])
|
||||
.unwrap_or_else(all_known_credential_sources),
|
||||
CredentialWatchMode::AllSources => all_known_credential_sources(),
|
||||
CredentialWatchMode::Antigravity => unreachable!(),
|
||||
};
|
||||
|
||||
let mut snapshot: CredentialWatchSnapshot = sources
|
||||
@ -694,6 +822,295 @@ fn codex_section_from_window(window: &CodexRateLimitWindow) -> UsageSection {
|
||||
}
|
||||
}
|
||||
|
||||
fn antigravity_credential_watch_signature() -> String {
|
||||
let Some(content) = read_windows_generic_credential(ANTIGRAVITY_CREDENTIAL_TARGET) else {
|
||||
return format!("{ANTIGRAVITY_CREDENTIAL_TARGET}|missing");
|
||||
};
|
||||
|
||||
let mut hasher = DefaultHasher::new();
|
||||
content.hash(&mut hasher);
|
||||
format!(
|
||||
"{ANTIGRAVITY_CREDENTIAL_TARGET}|present|{}|{}",
|
||||
content.len(),
|
||||
hasher.finish()
|
||||
)
|
||||
}
|
||||
|
||||
fn fetch_antigravity_usage(token: &str) -> Result<UsageData, PollError> {
|
||||
let mut auth_error = false;
|
||||
let mut last_error = PollError::RequestFailed;
|
||||
|
||||
for base_url in ANTIGRAVITY_ENDPOINTS {
|
||||
match fetch_antigravity_usage_from_endpoint(base_url, token) {
|
||||
Ok(data) => return Ok(data),
|
||||
Err(PollError::AuthRequired) => auth_error = true,
|
||||
Err(error) => last_error = error,
|
||||
}
|
||||
}
|
||||
|
||||
if auth_error {
|
||||
Err(PollError::AuthRequired)
|
||||
} else {
|
||||
Err(last_error)
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_antigravity_usage_from_endpoint(
|
||||
base_url: &str,
|
||||
token: &str,
|
||||
) -> Result<UsageData, PollError> {
|
||||
let project = fetch_antigravity_project(base_url, token)?;
|
||||
if let Some(project) = project.as_deref() {
|
||||
match fetch_antigravity_quota_summary(base_url, token, project) {
|
||||
Ok(data) => return Ok(data),
|
||||
Err(PollError::AuthRequired) => return Err(PollError::AuthRequired),
|
||||
Err(error) => diagnose::log(format!(
|
||||
"Antigravity retrieveUserQuotaSummary failed, falling back to model quota: {error:?}"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
let session = fetch_antigravity_model_quota(base_url, token, project.as_deref())?;
|
||||
let weekly = UsageSection::default();
|
||||
|
||||
Ok(UsageData { session, weekly })
|
||||
}
|
||||
|
||||
fn fetch_antigravity_project(base_url: &str, token: &str) -> Result<Option<String>, PollError> {
|
||||
let agent = build_agent()?;
|
||||
let body = serde_json::json!({
|
||||
"metadata": {
|
||||
"ideType": "ANTIGRAVITY"
|
||||
}
|
||||
});
|
||||
|
||||
let resp = match agent
|
||||
.post(&format!("{base_url}/v1internal:loadCodeAssist"))
|
||||
.set("Authorization", &format!("Bearer {token}"))
|
||||
.set("Content-Type", "application/json")
|
||||
.set("User-Agent", "antigravity")
|
||||
.send_json(&body)
|
||||
{
|
||||
Ok(resp) => resp,
|
||||
Err(ureq::Error::Status(code, _)) if code == 401 || code == 403 => {
|
||||
diagnose::log(format!(
|
||||
"Antigravity loadCodeAssist returned auth error status {code}"
|
||||
));
|
||||
return Err(PollError::AuthRequired);
|
||||
}
|
||||
Err(error) => {
|
||||
diagnose::log_error("Antigravity loadCodeAssist request failed", error);
|
||||
return Err(PollError::RequestFailed);
|
||||
}
|
||||
};
|
||||
|
||||
let response: AntigravityLoadResponse = match resp.into_json() {
|
||||
Ok(response) => response,
|
||||
Err(error) => {
|
||||
diagnose::log_error("unable to parse Antigravity loadCodeAssist response", error);
|
||||
return Err(PollError::RequestFailed);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(response.project.filter(|project| !project.is_empty()))
|
||||
}
|
||||
|
||||
fn fetch_antigravity_model_quota(
|
||||
base_url: &str,
|
||||
token: &str,
|
||||
project: Option<&str>,
|
||||
) -> Result<UsageSection, PollError> {
|
||||
let agent = build_agent()?;
|
||||
let body = match project {
|
||||
Some(project) => serde_json::json!({ "project": project }),
|
||||
None => serde_json::json!({}),
|
||||
};
|
||||
|
||||
let resp = match agent
|
||||
.post(&format!("{base_url}/v1internal:fetchAvailableModels"))
|
||||
.set("Authorization", &format!("Bearer {token}"))
|
||||
.set("Content-Type", "application/json")
|
||||
.set("User-Agent", "antigravity")
|
||||
.send_json(&body)
|
||||
{
|
||||
Ok(resp) => resp,
|
||||
Err(ureq::Error::Status(code, _)) if code == 401 || code == 403 => {
|
||||
diagnose::log(format!(
|
||||
"Antigravity fetchAvailableModels returned auth error status {code}"
|
||||
));
|
||||
return Err(PollError::AuthRequired);
|
||||
}
|
||||
Err(error) => {
|
||||
diagnose::log_error("Antigravity fetchAvailableModels request failed", error);
|
||||
return Err(PollError::RequestFailed);
|
||||
}
|
||||
};
|
||||
|
||||
let response: AntigravityModelsResponse = match resp.into_json() {
|
||||
Ok(response) => response,
|
||||
Err(error) => {
|
||||
diagnose::log_error(
|
||||
"unable to parse Antigravity fetchAvailableModels response",
|
||||
error,
|
||||
);
|
||||
return Err(PollError::RequestFailed);
|
||||
}
|
||||
};
|
||||
|
||||
best_antigravity_section(response.models.into_iter().filter_map(|(model, info)| {
|
||||
let quota = info.quota_info?;
|
||||
if !is_antigravity_display_model(&model) {
|
||||
return None;
|
||||
}
|
||||
antigravity_section_from_quota(quota)
|
||||
}))
|
||||
.ok_or(PollError::RequestFailed)
|
||||
}
|
||||
|
||||
fn fetch_antigravity_quota_summary(
|
||||
base_url: &str,
|
||||
token: &str,
|
||||
project: &str,
|
||||
) -> Result<UsageData, PollError> {
|
||||
let agent = build_agent()?;
|
||||
let body = serde_json::json!({ "project": project });
|
||||
|
||||
let resp = match agent
|
||||
.post(&format!("{base_url}/v1internal:retrieveUserQuotaSummary"))
|
||||
.set("Authorization", &format!("Bearer {token}"))
|
||||
.set("Content-Type", "application/json")
|
||||
.set("User-Agent", "antigravity")
|
||||
.send_json(&body)
|
||||
{
|
||||
Ok(resp) => resp,
|
||||
Err(ureq::Error::Status(code, _)) if code == 401 || code == 403 => {
|
||||
return Err(PollError::AuthRequired);
|
||||
}
|
||||
Err(error) => {
|
||||
diagnose::log_error("Antigravity retrieveUserQuotaSummary request failed", error);
|
||||
return Err(PollError::RequestFailed);
|
||||
}
|
||||
};
|
||||
|
||||
let response: AntigravityQuotaSummaryResponse = match resp.into_json() {
|
||||
Ok(response) => response,
|
||||
Err(error) => {
|
||||
diagnose::log_error(
|
||||
"unable to parse Antigravity retrieveUserQuotaSummary response",
|
||||
error,
|
||||
);
|
||||
return Err(PollError::RequestFailed);
|
||||
}
|
||||
};
|
||||
|
||||
antigravity_usage_from_summary(response).ok_or(PollError::RequestFailed)
|
||||
}
|
||||
|
||||
fn antigravity_section_from_quota(quota: AntigravityQuotaInfo) -> Option<UsageSection> {
|
||||
let remaining = quota.remaining_fraction?.clamp(0.0, 1.0);
|
||||
Some(UsageSection {
|
||||
percentage: (1.0 - remaining) * 100.0,
|
||||
resets_at: parse_iso8601(quota.reset_time.as_deref()),
|
||||
})
|
||||
}
|
||||
|
||||
fn antigravity_section_from_summary_bucket(
|
||||
bucket: &AntigravityQuotaSummaryBucket,
|
||||
) -> Option<UsageSection> {
|
||||
let remaining = bucket.remaining_fraction?.clamp(0.0, 1.0);
|
||||
Some(UsageSection {
|
||||
percentage: (1.0 - remaining) * 100.0,
|
||||
resets_at: parse_iso8601(bucket.reset_time.as_deref()),
|
||||
})
|
||||
}
|
||||
|
||||
fn antigravity_usage_from_summary(response: AntigravityQuotaSummaryResponse) -> Option<UsageData> {
|
||||
let mut fallback = None;
|
||||
|
||||
for group in response.groups.unwrap_or_default() {
|
||||
let is_gemini = is_antigravity_gemini_summary_group(&group);
|
||||
let usage = antigravity_usage_from_summary_group(group);
|
||||
|
||||
if is_gemini && usage.is_some() {
|
||||
return usage;
|
||||
}
|
||||
|
||||
if fallback.is_none() {
|
||||
fallback = usage;
|
||||
}
|
||||
}
|
||||
|
||||
fallback
|
||||
}
|
||||
|
||||
fn antigravity_usage_from_summary_group(group: AntigravityQuotaSummaryGroup) -> Option<UsageData> {
|
||||
let mut data = UsageData::default();
|
||||
let mut has_quota = false;
|
||||
|
||||
for bucket in group.buckets.unwrap_or_default() {
|
||||
let Some(section) = antigravity_section_from_summary_bucket(&bucket) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
match bucket.window.as_deref() {
|
||||
Some(window) if window.eq_ignore_ascii_case("5h") => {
|
||||
data.session = section;
|
||||
has_quota = true;
|
||||
}
|
||||
Some(window) if window.eq_ignore_ascii_case("weekly") => {
|
||||
data.weekly = section;
|
||||
has_quota = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
has_quota.then_some(data)
|
||||
}
|
||||
|
||||
fn is_antigravity_gemini_summary_group(group: &AntigravityQuotaSummaryGroup) -> bool {
|
||||
group
|
||||
.display_name
|
||||
.as_deref()
|
||||
.is_some_and(|name| name.to_ascii_lowercase().contains("gemini"))
|
||||
|| group
|
||||
.description
|
||||
.as_deref()
|
||||
.is_some_and(|description| description.to_ascii_lowercase().contains("gemini"))
|
||||
|| group.buckets.as_ref().is_some_and(|buckets| {
|
||||
buckets.iter().any(|bucket| {
|
||||
bucket
|
||||
.bucket_id
|
||||
.as_deref()
|
||||
.is_some_and(|id| id.to_ascii_lowercase().starts_with("gemini-"))
|
||||
|| bucket
|
||||
.display_name
|
||||
.as_deref()
|
||||
.is_some_and(|name| name.to_ascii_lowercase().contains("gemini"))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn best_antigravity_section<I>(sections: I) -> Option<UsageSection>
|
||||
where
|
||||
I: IntoIterator<Item = UsageSection>,
|
||||
{
|
||||
sections.into_iter().max_by(|a, b| {
|
||||
a.percentage
|
||||
.partial_cmp(&b.percentage)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
.then_with(|| a.resets_at.cmp(&b.resets_at))
|
||||
})
|
||||
}
|
||||
|
||||
fn is_antigravity_display_model(model: &str) -> bool {
|
||||
model.starts_with("gemini")
|
||||
|| model.starts_with("claude")
|
||||
|| model.starts_with("gpt")
|
||||
|| model.starts_with("image")
|
||||
|| model.starts_with("imagen")
|
||||
}
|
||||
|
||||
fn get_header_f64(response: &ureq::Response, name: &str) -> f64 {
|
||||
response
|
||||
.header(name)
|
||||
@ -799,6 +1216,54 @@ fn read_codex_credentials() -> Option<CodexTokenData> {
|
||||
auth.tokens.filter(|tokens| !tokens.access_token.is_empty())
|
||||
}
|
||||
|
||||
fn read_antigravity_credentials() -> Option<AntigravityTokenData> {
|
||||
let content = read_windows_generic_credential(ANTIGRAVITY_CREDENTIAL_TARGET)?;
|
||||
let auth: AntigravityAuthFile = serde_json::from_str(&content).ok()?;
|
||||
if auth.token.access_token.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(auth.token)
|
||||
}
|
||||
}
|
||||
|
||||
fn read_windows_generic_credential(target: &str) -> Option<String> {
|
||||
const CRED_TYPE_GENERIC: u32 = 1;
|
||||
|
||||
let mut target_wide: Vec<u16> = target.encode_utf16().chain(std::iter::once(0)).collect();
|
||||
let mut credential: *mut CredentialW = std::ptr::null_mut();
|
||||
|
||||
let ok = unsafe {
|
||||
CredReadW(
|
||||
target_wide.as_mut_ptr(),
|
||||
CRED_TYPE_GENERIC,
|
||||
0,
|
||||
&mut credential,
|
||||
)
|
||||
};
|
||||
|
||||
if ok == 0 || credential.is_null() {
|
||||
diagnose::log(format!(
|
||||
"unable to read Windows generic credential target {target}"
|
||||
));
|
||||
return None;
|
||||
}
|
||||
|
||||
let result = unsafe {
|
||||
let cred = &*credential;
|
||||
if cred.credential_blob_size == 0 || cred.credential_blob.is_null() {
|
||||
CredFree(credential as *mut c_void);
|
||||
return None;
|
||||
}
|
||||
let bytes =
|
||||
std::slice::from_raw_parts(cred.credential_blob, cred.credential_blob_size as usize);
|
||||
let text = String::from_utf8(bytes.to_vec()).ok();
|
||||
CredFree(credential as *mut c_void);
|
||||
text
|
||||
};
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn read_wsl_credentials(distro: &str) -> Option<Credentials> {
|
||||
let output = run_with_timeout(
|
||||
Command::new("wsl.exe")
|
||||
@ -1096,4 +1561,66 @@ pub fn is_past_reset(data: &UsageData) -> bool {
|
||||
pub fn app_is_past_reset(data: &AppUsageData) -> bool {
|
||||
data.claude_code.as_ref().is_some_and(is_past_reset)
|
||||
|| data.codex.as_ref().is_some_and(is_past_reset)
|
||||
|| data.antigravity.as_ref().is_some_and(is_past_reset)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn antigravity_summary_prefers_gemini_group() {
|
||||
let response: AntigravityQuotaSummaryResponse = serde_json::from_str(
|
||||
r#"{
|
||||
"groups": [
|
||||
{
|
||||
"displayName": "Claude and GPT models",
|
||||
"buckets": [
|
||||
{
|
||||
"bucketId": "3p-weekly",
|
||||
"window": "weekly",
|
||||
"resetTime": "2026-06-20T18:32:02Z",
|
||||
"remainingFraction": 1
|
||||
},
|
||||
{
|
||||
"bucketId": "3p-5h",
|
||||
"window": "5h",
|
||||
"resetTime": "2026-06-13T23:32:02Z",
|
||||
"remainingFraction": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"displayName": "Gemini Models",
|
||||
"description": "Models within this group: Gemini Flash, Gemini Pro",
|
||||
"buckets": [
|
||||
{
|
||||
"bucketId": "gemini-weekly",
|
||||
"displayName": "Weekly Limit",
|
||||
"window": "weekly",
|
||||
"resetTime": "2026-06-20T17:08:54Z",
|
||||
"remainingFraction": 0.99304295
|
||||
},
|
||||
{
|
||||
"bucketId": "gemini-5h",
|
||||
"displayName": "Five Hour Limit",
|
||||
"window": "5h",
|
||||
"resetTime": "2026-06-13T22:08:54Z",
|
||||
"remainingFraction": 0.9582575
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}"#,
|
||||
)
|
||||
.expect("summary response should deserialize");
|
||||
|
||||
let usage =
|
||||
antigravity_usage_from_summary(response).expect("Gemini quota should be selected");
|
||||
|
||||
assert!((usage.weekly.percentage - 0.695705).abs() < 0.000001);
|
||||
assert!((usage.session.percentage - 4.17425).abs() < 0.000001);
|
||||
assert!(usage.weekly.resets_at.is_some());
|
||||
assert!(usage.session.resets_at.is_some());
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@ use crate::native_interop::{self, Color, WM_APP_TRAY};
|
||||
|
||||
const CLAUDE_TRAY_ICON_ID: u32 = 1;
|
||||
const CODEX_TRAY_ICON_ID: u32 = 2;
|
||||
const ANTIGRAVITY_TRAY_ICON_ID: u32 = 3;
|
||||
|
||||
/// Menu item ID for toggling widget visibility (used by window.rs context menu).
|
||||
pub const IDM_TOGGLE_WIDGET: u16 = 50;
|
||||
@ -27,6 +28,7 @@ pub enum TrayAction {
|
||||
pub enum TrayIconKind {
|
||||
Claude,
|
||||
Codex,
|
||||
Antigravity,
|
||||
}
|
||||
|
||||
pub struct TrayIconData {
|
||||
@ -40,6 +42,7 @@ impl TrayIconKind {
|
||||
match self {
|
||||
Self::Claude => CLAUDE_TRAY_ICON_ID,
|
||||
Self::Codex => CODEX_TRAY_ICON_ID,
|
||||
Self::Antigravity => ANTIGRAVITY_TRAY_ICON_ID,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -90,9 +93,17 @@ fn codex_fill(percent: f64) -> Color {
|
||||
}
|
||||
}
|
||||
|
||||
fn antigravity_fill(percent: f64) -> Color {
|
||||
if percent >= 90.0 {
|
||||
Color::from_hex("#FFFFFF")
|
||||
} else {
|
||||
Color::from_hex("#34A853")
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a rounded-rectangle tray icon badge showing the usage percentage.
|
||||
/// For Claude, `percent` = None uses the embedded app icon as the loading state.
|
||||
/// For Codex, `percent` = None uses a black/white Codex placeholder badge.
|
||||
/// For Codex and Antigravity, `percent` = None uses a provider placeholder badge.
|
||||
pub fn create_icon(kind: TrayIconKind, percent: Option<f64>) -> HICON {
|
||||
if matches!(kind, TrayIconKind::Claude) && percent.is_none() {
|
||||
let app_icon = load_embedded_app_icon();
|
||||
@ -104,7 +115,7 @@ pub fn create_icon(kind: TrayIconKind, percent: Option<f64>) -> HICON {
|
||||
let size = 64_i32;
|
||||
let margin = 0_i32;
|
||||
let radius = 2_i32;
|
||||
let outline = if matches!(kind, TrayIconKind::Codex) {
|
||||
let outline = if matches!(kind, TrayIconKind::Codex | TrayIconKind::Antigravity) {
|
||||
3_i32
|
||||
} else {
|
||||
0_i32
|
||||
@ -113,16 +124,21 @@ pub fn create_icon(kind: TrayIconKind, percent: Option<f64>) -> HICON {
|
||||
let fill = match kind {
|
||||
TrayIconKind::Claude => interpolated_fill(percent.unwrap_or(0.0)),
|
||||
TrayIconKind::Codex => codex_fill(percent.unwrap_or(0.0)),
|
||||
TrayIconKind::Antigravity => antigravity_fill(percent.unwrap_or(0.0)),
|
||||
};
|
||||
let text_col = match kind {
|
||||
TrayIconKind::Claude => Color::from_hex("#FFFFFF"),
|
||||
TrayIconKind::Codex if percent.unwrap_or(0.0) >= 90.0 => Color::from_hex("#111111"),
|
||||
TrayIconKind::Codex => Color::from_hex("#FFFFFF"),
|
||||
TrayIconKind::Antigravity if percent.unwrap_or(0.0) >= 90.0 => Color::from_hex("#1E7D41"),
|
||||
TrayIconKind::Antigravity => Color::from_hex("#FFFFFF"),
|
||||
};
|
||||
let outline_col = match kind {
|
||||
TrayIconKind::Claude => fill,
|
||||
TrayIconKind::Codex if percent.unwrap_or(0.0) >= 90.0 => Color::from_hex("#111111"),
|
||||
TrayIconKind::Codex => Color::from_hex("#FFFFFF"),
|
||||
TrayIconKind::Antigravity if percent.unwrap_or(0.0) >= 90.0 => Color::from_hex("#1E7D41"),
|
||||
TrayIconKind::Antigravity => Color::from_hex("#FFFFFF"),
|
||||
};
|
||||
|
||||
let display_text = match percent {
|
||||
@ -130,6 +146,7 @@ pub fn create_icon(kind: TrayIconKind, percent: Option<f64>) -> HICON {
|
||||
None => match kind {
|
||||
TrayIconKind::Claude => String::new(),
|
||||
TrayIconKind::Codex => "C".to_string(),
|
||||
TrayIconKind::Antigravity => "A".to_string(),
|
||||
},
|
||||
};
|
||||
|
||||
@ -397,6 +414,9 @@ pub fn sync(hwnd: HWND, icons: &[TrayIconData]) {
|
||||
let show_codex = icons
|
||||
.iter()
|
||||
.find(|icon| matches!(icon.kind, TrayIconKind::Codex));
|
||||
let show_antigravity = icons
|
||||
.iter()
|
||||
.find(|icon| matches!(icon.kind, TrayIconKind::Antigravity));
|
||||
|
||||
if let Some(icon) = show_claude {
|
||||
add(hwnd, icon.kind, icon.percent, &icon.tooltip);
|
||||
@ -411,11 +431,19 @@ pub fn sync(hwnd: HWND, icons: &[TrayIconData]) {
|
||||
} else {
|
||||
remove(hwnd, TrayIconKind::Codex);
|
||||
}
|
||||
|
||||
if let Some(icon) = show_antigravity {
|
||||
add(hwnd, icon.kind, icon.percent, &icon.tooltip);
|
||||
update(hwnd, icon.kind, icon.percent, &icon.tooltip);
|
||||
} else {
|
||||
remove(hwnd, TrayIconKind::Antigravity);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_all(hwnd: HWND) {
|
||||
remove(hwnd, TrayIconKind::Claude);
|
||||
remove(hwnd, TrayIconKind::Codex);
|
||||
remove(hwnd, TrayIconKind::Antigravity);
|
||||
}
|
||||
|
||||
/// Interpret a tray callback message and return the action to take.
|
||||
|
||||
230
src/window.rs
230
src/window.rs
@ -63,8 +63,13 @@ struct AppState {
|
||||
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>,
|
||||
|
||||
@ -123,6 +128,7 @@ 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
|
||||
|
||||
@ -215,6 +221,8 @@ struct SettingsFile {
|
||||
show_claude_code: bool,
|
||||
#[serde(default = "default_show_codex")]
|
||||
show_codex: bool,
|
||||
#[serde(default = "default_show_antigravity")]
|
||||
show_antigravity: bool,
|
||||
}
|
||||
|
||||
impl Default for SettingsFile {
|
||||
@ -227,6 +235,7 @@ impl Default for SettingsFile {
|
||||
widget_visible: true,
|
||||
show_claude_code: true,
|
||||
show_codex: false,
|
||||
show_antigravity: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -247,13 +256,17 @@ 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 {
|
||||
if !settings.show_claude_code && !settings.show_codex && !settings.show_antigravity {
|
||||
settings.show_claude_code = true;
|
||||
}
|
||||
settings
|
||||
@ -282,6 +295,7 @@ fn save_state_settings() {
|
||||
widget_visible: s.widget_visible,
|
||||
show_claude_code: s.show_claude_code,
|
||||
show_codex: s.show_codex,
|
||||
show_antigravity: s.show_antigravity,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -315,6 +329,18 @@ fn tray_icon_data_from_state() -> Vec<tray_icon::TrayIconData> {
|
||||
),
|
||||
});
|
||||
}
|
||||
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) => {
|
||||
@ -333,6 +359,13 @@ fn tray_icon_data_from_state() -> Vec<tray_icon::TrayIconData> {
|
||||
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(),
|
||||
@ -434,6 +467,19 @@ fn refresh_usage_texts(state: &mut AppState) {
|
||||
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) {
|
||||
@ -822,8 +868,8 @@ 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) -> i32 {
|
||||
(show_claude_code as i32 + show_codex as i32).max(1)
|
||||
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 {
|
||||
@ -850,7 +896,11 @@ fn total_widget_width_for(active_models: i32) -> i32 {
|
||||
}
|
||||
|
||||
fn total_widget_width_for_state(state: &AppState) -> i32 {
|
||||
total_widget_width_for(active_model_count(state.show_claude_code, state.show_codex))
|
||||
total_widget_width_for(active_model_count(
|
||||
state.show_claude_code,
|
||||
state.show_codex,
|
||||
state.show_antigravity,
|
||||
))
|
||||
}
|
||||
|
||||
fn total_widget_width() -> i32 {
|
||||
@ -858,7 +908,7 @@ fn total_widget_width() -> i32 {
|
||||
let state = lock_state();
|
||||
state
|
||||
.as_ref()
|
||||
.map(|s| active_model_count(s.show_claude_code, s.show_codex))
|
||||
.map(|s| active_model_count(s.show_claude_code, s.show_codex, s.show_antigravity))
|
||||
.unwrap_or(1)
|
||||
};
|
||||
total_widget_width_for(active_models)
|
||||
@ -876,6 +926,10 @@ fn codex_accent_color(is_dark: bool) -> Color {
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
@ -892,6 +946,14 @@ fn codex_usage_text_color(is_dark: bool) -> Color {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -953,8 +1015,11 @@ pub fn run() {
|
||||
|
||||
// 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);
|
||||
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()),
|
||||
@ -1013,8 +1078,13 @@ pub fn run() {
|
||||
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,
|
||||
@ -1152,8 +1222,13 @@ fn render_layered() {
|
||||
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() {
|
||||
@ -1170,8 +1245,13 @@ fn render_layered() {
|
||||
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,
|
||||
}
|
||||
@ -1192,6 +1272,7 @@ fn render_layered() {
|
||||
|
||||
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 {
|
||||
@ -1259,9 +1340,15 @@ fn render_layered() {
|
||||
&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).
|
||||
@ -1329,9 +1416,15 @@ fn paint_content(
|
||||
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 {
|
||||
@ -1419,10 +1512,14 @@ fn paint_content(
|
||||
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(
|
||||
@ -1436,10 +1533,14 @@ fn paint_content(
|
||||
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,
|
||||
);
|
||||
|
||||
@ -1450,15 +1551,15 @@ fn paint_content(
|
||||
|
||||
fn do_poll(send_hwnd: SendHwnd) {
|
||||
let hwnd = send_hwnd.to_hwnd();
|
||||
let (show_claude_code, show_codex) = {
|
||||
let (show_claude_code, show_codex, show_antigravity) = {
|
||||
let state = lock_state();
|
||||
state
|
||||
.as_ref()
|
||||
.map(|s| (s.show_claude_code, s.show_codex))
|
||||
.unwrap_or((true, false))
|
||||
.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) {
|
||||
match poller::poll(show_claude_code, show_codex, show_antigravity) {
|
||||
Ok(data) => {
|
||||
let mut state = lock_state();
|
||||
if let Some(s) = state.as_mut() {
|
||||
@ -1476,6 +1577,13 @@ fn do_poll(send_hwnd: SendHwnd) {
|
||||
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 {
|
||||
@ -1507,6 +1615,14 @@ fn do_poll(send_hwnd: SendHwnd) {
|
||||
}
|
||||
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),
|
||||
@ -1537,6 +1653,8 @@ fn do_poll(send_hwnd: SendHwnd) {
|
||||
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);
|
||||
@ -1555,6 +1673,8 @@ fn do_poll(send_hwnd: SendHwnd) {
|
||||
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),
|
||||
@ -1581,13 +1701,20 @@ fn do_poll(send_hwnd: SendHwnd) {
|
||||
s.language.strings().token_expired_title,
|
||||
s.language.strings().token_expired_body,
|
||||
)
|
||||
} else {
|
||||
} 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,
|
||||
)
|
||||
}
|
||||
})
|
||||
};
|
||||
@ -1644,6 +1771,12 @@ fn schedule_countdown_timer() {
|
||||
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();
|
||||
|
||||
@ -2220,27 +2353,34 @@ unsafe extern "system" fn wnd_proc(
|
||||
// Reset the poll timer with the new interval
|
||||
SetTimer(hwnd, TIMER_POLL, new_interval, None);
|
||||
}
|
||||
IDM_MODEL_CLAUDE_CODE | IDM_MODEL_CODEX => {
|
||||
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_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_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();
|
||||
@ -2333,6 +2473,7 @@ fn show_context_menu(hwnd: HWND) {
|
||||
widget_visible,
|
||||
show_claude_code,
|
||||
show_codex,
|
||||
show_antigravity,
|
||||
) = {
|
||||
let state = lock_state();
|
||||
match state.as_ref() {
|
||||
@ -2346,6 +2487,7 @@ fn show_context_menu(hwnd: HWND) {
|
||||
s.widget_visible,
|
||||
s.show_claude_code,
|
||||
s.show_codex,
|
||||
s.show_antigravity,
|
||||
),
|
||||
None => (
|
||||
POLL_15_MIN,
|
||||
@ -2357,6 +2499,7 @@ fn show_context_menu(hwnd: HWND) {
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
}
|
||||
};
|
||||
@ -2430,6 +2573,19 @@ fn show_context_menu(hwnd: HWND) {
|
||||
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,
|
||||
@ -2583,8 +2739,13 @@ fn paint(hdc: HDC, hwnd: HWND) {
|
||||
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() {
|
||||
@ -2599,8 +2760,13 @@ fn paint(hdc: HDC, hwnd: HWND) {
|
||||
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,
|
||||
}
|
||||
@ -2608,6 +2774,7 @@ fn paint(hdc: HDC, hwnd: HWND) {
|
||||
|
||||
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 {
|
||||
@ -2656,9 +2823,15 @@ fn paint(hdc: HDC, hwnd: HWND) {
|
||||
&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);
|
||||
@ -2680,16 +2853,20 @@ fn draw_row(
|
||||
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);
|
||||
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 = show_claude_code && show_codex;
|
||||
let use_model_text_colors = active_models > 1;
|
||||
let claude_value_color = if use_model_text_colors {
|
||||
claude_usage_text_color(is_dark)
|
||||
} else {
|
||||
@ -2700,6 +2877,11 @@ fn draw_row(
|
||||
} 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()));
|
||||
@ -2744,6 +2926,20 @@ fn draw_row(
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user