diff --git a/Cargo.toml b/Cargo.toml index 9cbc1c6..50a9b3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ features = [ "Win32_Security", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_HiDpi", + "Win32_UI_Controls_Dialogs", ] [build-dependencies] diff --git a/src/localization/dutch.rs b/src/localization/dutch.rs index ed815bf..73a7cd6 100644 --- a/src/localization/dutch.rs +++ b/src/localization/dutch.rs @@ -14,6 +14,13 @@ pub(super) const STRINGS: Strings = Strings { claude_code_model: "Claude Code", codex_model: "Codex", antigravity_model: "Antigravity", + icon_colors: "Pictogramkleuren", + claude_code_color: "Kleur voor Claude Code...", + codex_color: "Kleur voor Codex...", + antigravity_color: "Kleur voor Antigravity...", + reset_icon_colors: "Standaardkleuren herstellen", + background_color: "Achtergrondkleur", + text_color: "Tekstkleur", settings: "Instellingen", start_with_windows: "Opstarten met Windows", reset_position: "Positie herstellen", diff --git a/src/localization/english.rs b/src/localization/english.rs index 0249730..6ef48f9 100644 --- a/src/localization/english.rs +++ b/src/localization/english.rs @@ -14,6 +14,13 @@ pub(super) const STRINGS: Strings = Strings { claude_code_model: "Claude Code", codex_model: "Codex", antigravity_model: "Antigravity", + icon_colors: "Icon Colors", + claude_code_color: "Claude Code Color...", + codex_color: "Codex Color...", + antigravity_color: "Antigravity Color...", + reset_icon_colors: "Reset to Default", + background_color: "Background Color", + text_color: "Text Color", settings: "Settings", start_with_windows: "Start with Windows", reset_position: "Reset Position", diff --git a/src/localization/french.rs b/src/localization/french.rs index 1850f41..ada96cd 100644 --- a/src/localization/french.rs +++ b/src/localization/french.rs @@ -14,6 +14,13 @@ pub(super) const STRINGS: Strings = Strings { claude_code_model: "Claude Code", codex_model: "Codex", antigravity_model: "Antigravity", + icon_colors: "Couleurs des icônes", + claude_code_color: "Couleur de Claude Code...", + codex_color: "Couleur de Codex...", + antigravity_color: "Couleur d'Antigravity...", + reset_icon_colors: "Réinitialiser les couleurs par défaut", + background_color: "Couleur d'arrière-plan", + text_color: "Couleur du texte", settings: "Paramètres", start_with_windows: "Démarrer avec Windows", reset_position: "Réinitialiser la position", diff --git a/src/localization/german.rs b/src/localization/german.rs index 2b91a81..b20fadf 100644 --- a/src/localization/german.rs +++ b/src/localization/german.rs @@ -14,6 +14,13 @@ pub(super) const STRINGS: Strings = Strings { claude_code_model: "Claude Code", codex_model: "Codex", antigravity_model: "Antigravity", + icon_colors: "Symbolfarben", + claude_code_color: "Farbe für Claude Code...", + codex_color: "Farbe für Codex...", + antigravity_color: "Farbe für Antigravity...", + reset_icon_colors: "Standardfarben zurücksetzen", + background_color: "Hintergrundfarbe", + text_color: "Textfarbe", settings: "Einstellungen", start_with_windows: "Mit Windows starten", reset_position: "Position zurücksetzen", diff --git a/src/localization/japanese.rs b/src/localization/japanese.rs index 2eec041..535fdd7 100644 --- a/src/localization/japanese.rs +++ b/src/localization/japanese.rs @@ -14,6 +14,13 @@ pub(super) const STRINGS: Strings = Strings { claude_code_model: "Claude Code", codex_model: "Codex", antigravity_model: "Antigravity", + icon_colors: "アイコンの色", + claude_code_color: "Claude Code の色...", + codex_color: "Codex の色...", + antigravity_color: "Antigravity の色...", + reset_icon_colors: "既定の色に戻す", + background_color: "背景色", + text_color: "文字色", settings: "設定", start_with_windows: "Windows と同時に開始", reset_position: "位置をリセット", diff --git a/src/localization/korean.rs b/src/localization/korean.rs index 965687d..8771b8b 100644 --- a/src/localization/korean.rs +++ b/src/localization/korean.rs @@ -14,6 +14,13 @@ pub(super) const STRINGS: Strings = Strings { claude_code_model: "Claude Code", codex_model: "Codex", antigravity_model: "Antigravity", + icon_colors: "아이콘 색상", + claude_code_color: "Claude Code 색상...", + codex_color: "Codex 색상...", + antigravity_color: "Antigravity 색상...", + reset_icon_colors: "기본 색상으로 재설정", + background_color: "배경 색상", + text_color: "텍스트 색상", settings: "설정", start_with_windows: "Windows 시작 시 자동 실행", reset_position: "위치 초기화", diff --git a/src/localization/mod.rs b/src/localization/mod.rs index 2a06b04..b189587 100644 --- a/src/localization/mod.rs +++ b/src/localization/mod.rs @@ -148,6 +148,13 @@ pub struct Strings { pub claude_code_model: &'static str, pub codex_model: &'static str, pub antigravity_model: &'static str, + pub icon_colors: &'static str, + pub claude_code_color: &'static str, + pub codex_color: &'static str, + pub antigravity_color: &'static str, + pub reset_icon_colors: &'static str, + pub background_color: &'static str, + pub text_color: &'static str, pub settings: &'static str, pub start_with_windows: &'static str, pub reset_position: &'static str, diff --git a/src/localization/portuguese_brazil.rs b/src/localization/portuguese_brazil.rs index 56cf3bf..2c19584 100644 --- a/src/localization/portuguese_brazil.rs +++ b/src/localization/portuguese_brazil.rs @@ -14,6 +14,13 @@ pub(super) const STRINGS: Strings = Strings { claude_code_model: "Claude Code", codex_model: "Codex", antigravity_model: "Antigravity", + icon_colors: "Cores do ícone", + claude_code_color: "Cor do Claude Code...", + codex_color: "Cor do Codex...", + antigravity_color: "Cor do Antigravity...", + reset_icon_colors: "Restaurar cores padrão", + background_color: "Cor de fundo", + text_color: "Cor do texto", settings: "Configurações", start_with_windows: "Iniciar com o Windows", reset_position: "Redefinir Posição", diff --git a/src/localization/russian.rs b/src/localization/russian.rs index fc7e372..1fa42c8 100644 --- a/src/localization/russian.rs +++ b/src/localization/russian.rs @@ -14,6 +14,13 @@ pub(super) const STRINGS: Strings = Strings { claude_code_model: "Claude Code", codex_model: "Codex", antigravity_model: "Antigravity", + icon_colors: "Цвета значков", + claude_code_color: "Цвет Claude Code...", + codex_color: "Цвет Codex...", + antigravity_color: "Цвет Antigravity...", + reset_icon_colors: "Сбросить цвета по умолчанию", + background_color: "Цвет фона", + text_color: "Цвет текста", settings: "Настройки", start_with_windows: "Запускать вместе с Windows", reset_position: "Сбросить позицию", diff --git a/src/localization/spanish.rs b/src/localization/spanish.rs index e635771..9fdfe34 100644 --- a/src/localization/spanish.rs +++ b/src/localization/spanish.rs @@ -14,6 +14,13 @@ pub(super) const STRINGS: Strings = Strings { claude_code_model: "Claude Code", codex_model: "Codex", antigravity_model: "Antigravity", + icon_colors: "Colores del icono", + claude_code_color: "Color de Claude Code...", + codex_color: "Color de Codex...", + antigravity_color: "Color de Antigravity...", + reset_icon_colors: "Restablecer colores predeterminados", + background_color: "Color de fondo", + text_color: "Color de texto", settings: "Configuración", start_with_windows: "Iniciar con Windows", reset_position: "Restablecer posición", diff --git a/src/localization/traditional_chinese.rs b/src/localization/traditional_chinese.rs index 3eb3514..3ceb614 100644 --- a/src/localization/traditional_chinese.rs +++ b/src/localization/traditional_chinese.rs @@ -14,6 +14,13 @@ pub(super) const STRINGS: Strings = Strings { claude_code_model: "Claude Code", codex_model: "Codex", antigravity_model: "Antigravity", + icon_colors: "圖示顏色", + claude_code_color: "Claude Code 顏色...", + codex_color: "Codex 顏色...", + antigravity_color: "Antigravity 顏色...", + reset_icon_colors: "重設為預設顏色", + background_color: "背景顏色", + text_color: "文字顏色", settings: "設定", start_with_windows: "開機時啟動", reset_position: "重置位置", diff --git a/src/native_interop.rs b/src/native_interop.rs index c745d08..6efd6f4 100644 --- a/src/native_interop.rs +++ b/src/native_interop.rs @@ -208,7 +208,19 @@ impl Color { Self { r, g, b } } + pub fn to_hex(self) -> String { + format!("#{:02X}{:02X}{:02X}", self.r, self.g, self.b) + } + pub fn to_colorref(self) -> u32 { colorref(self.r, self.g, self.b) } + + pub fn from_colorref(cr: u32) -> Self { + Self { + r: (cr & 0xFF) as u8, + g: ((cr >> 8) & 0xFF) as u8, + b: ((cr >> 16) & 0xFF) as u8, + } + } } diff --git a/src/tray_icon.rs b/src/tray_icon.rs index e2502e2..acb0c53 100644 --- a/src/tray_icon.rs +++ b/src/tray_icon.rs @@ -35,6 +35,15 @@ pub struct TrayIconData { pub kind: TrayIconKind, pub percent: Option, pub tooltip: String, + pub custom_color: Option, +} + +/// User-chosen background/text color that replaces the default usage-based +/// badge coloring for a given tray icon kind. +#[derive(Clone, Copy)] +pub struct IconColorOverride { + pub background: Color, + pub text: Color, } impl TrayIconKind { @@ -104,8 +113,12 @@ fn antigravity_fill(percent: f64) -> Color { /// 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 and Antigravity, `percent` = None uses a provider placeholder badge. -pub fn create_icon(kind: TrayIconKind, percent: Option) -> HICON { - if matches!(kind, TrayIconKind::Claude) && percent.is_none() { +pub fn create_icon( + kind: TrayIconKind, + percent: Option, + custom_color: Option, +) -> HICON { + if matches!(kind, TrayIconKind::Claude) && percent.is_none() && custom_color.is_none() { let app_icon = load_embedded_app_icon(); if !app_icon.is_invalid() { return app_icon; @@ -121,24 +134,37 @@ pub fn create_icon(kind: TrayIconKind, percent: Option) -> HICON { 0_i32 }; - 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 fill = match custom_color { + Some(over) => over.background, + None => 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("#1967D2"), - TrayIconKind::Antigravity => Color::from_hex("#FFFFFF"), + let text_col = match custom_color { + Some(over) => over.text, + None => 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("#1967D2") + } + 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("#1967D2"), - TrayIconKind::Antigravity => Color::from_hex("#FFFFFF"), + let outline_col = match custom_color { + Some(over) => over.background, + None => 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("#1967D2") + } + TrayIconKind::Antigravity => Color::from_hex("#FFFFFF"), + }, }; let display_text = match percent { @@ -249,19 +275,21 @@ pub fn create_icon(kind: TrayIconKind, percent: Option) -> HICON { let _ = SetBkMode(mem_dc, TRANSPARENT); let _ = SetTextColor(mem_dc, COLORREF(text_col.to_colorref())); - let mut text_rect = RECT { - left: margin, - top: margin, - right: size - margin, - bottom: size - margin, - }; - let mut text_wide: Vec = display_text.encode_utf16().collect(); - let _ = DrawTextW( - mem_dc, - &mut text_wide, - &mut text_rect, - DT_CENTER | DT_VCENTER | DT_SINGLELINE, - ); + if !display_text.is_empty() { + let mut text_rect = RECT { + left: margin, + top: margin, + right: size - margin, + bottom: size - margin, + }; + let mut text_wide: Vec = display_text.encode_utf16().collect(); + let _ = DrawTextW( + mem_dc, + &mut text_wide, + &mut text_rect, + DT_CENTER | DT_VCENTER | DT_SINGLELINE, + ); + } SelectObject(mem_dc, old_font); let _ = DeleteObject(font); @@ -360,8 +388,14 @@ fn copy_wide_256(s: &str, buf: &mut [u16; 256]) { } /// Register the tray icon with the shell. -pub fn add(hwnd: HWND, kind: TrayIconKind, percent: Option, tooltip: &str) { - let hicon = create_icon(kind, percent); +pub fn add( + hwnd: HWND, + kind: TrayIconKind, + percent: Option, + tooltip: &str, + custom_color: Option, +) { + let hicon = create_icon(kind, percent, custom_color); unsafe { let mut nid: NOTIFYICONDATAW = std::mem::zeroed(); nid.cbSize = std::mem::size_of::() as u32; @@ -379,8 +413,14 @@ pub fn add(hwnd: HWND, kind: TrayIconKind, percent: Option, tooltip: &str) } /// Update the tray icon colour and tooltip to reflect current usage. -pub fn update(hwnd: HWND, kind: TrayIconKind, percent: Option, tooltip: &str) { - let hicon = create_icon(kind, percent); +pub fn update( + hwnd: HWND, + kind: TrayIconKind, + percent: Option, + tooltip: &str, + custom_color: Option, +) { + let hicon = create_icon(kind, percent, custom_color); unsafe { let mut nid: NOTIFYICONDATAW = std::mem::zeroed(); nid.cbSize = std::mem::size_of::() as u32; @@ -419,22 +459,22 @@ pub fn sync(hwnd: HWND, icons: &[TrayIconData]) { .find(|icon| matches!(icon.kind, TrayIconKind::Antigravity)); if let Some(icon) = show_claude { - add(hwnd, icon.kind, icon.percent, &icon.tooltip); - update(hwnd, icon.kind, icon.percent, &icon.tooltip); + add(hwnd, icon.kind, icon.percent, &icon.tooltip, icon.custom_color); + update(hwnd, icon.kind, icon.percent, &icon.tooltip, icon.custom_color); } else { remove(hwnd, TrayIconKind::Claude); } if let Some(icon) = show_codex { - add(hwnd, icon.kind, icon.percent, &icon.tooltip); - update(hwnd, icon.kind, icon.percent, &icon.tooltip); + add(hwnd, icon.kind, icon.percent, &icon.tooltip, icon.custom_color); + update(hwnd, icon.kind, icon.percent, &icon.tooltip, icon.custom_color); } 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); + add(hwnd, icon.kind, icon.percent, &icon.tooltip, icon.custom_color); + update(hwnd, icon.kind, icon.percent, &icon.tooltip, icon.custom_color); } else { remove(hwnd, TrayIconKind::Antigravity); } diff --git a/src/window.rs b/src/window.rs index f6d261e..589d5e2 100644 --- a/src/window.rs +++ b/src/window.rs @@ -11,6 +11,9 @@ use windows::Win32::System::LibraryLoader::{GetModuleFileNameW, GetModuleHandleW use windows::Win32::System::Registry::*; use windows::Win32::System::Threading::{CreateMutexW, WaitForSingleObject}; use windows::Win32::UI::Accessibility::HWINEVENTHOOK; +use windows::Win32::UI::Controls::Dialogs::{ + ChooseColorW, CC_ENABLEHOOK, CC_FULLOPEN, CC_RGBINIT, CHOOSECOLORW, +}; use windows::Win32::UI::HiDpi::*; use windows::Win32::UI::Input::KeyboardAndMouse::{ReleaseCapture, SetCapture}; use windows::Win32::UI::Shell::ExtractIconExW; @@ -70,6 +73,9 @@ struct AppState { show_claude_code: bool, show_codex: bool, show_antigravity: bool, + icon_color_claude: Option, + icon_color_codex: Option, + icon_color_antigravity: Option, data: Option, @@ -131,6 +137,10 @@ 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 IDM_ICON_COLOR_CLAUDE: u16 = 80; +const IDM_ICON_COLOR_CODEX: u16 = 81; +const IDM_ICON_COLOR_ANTIGRAVITY: u16 = 82; +const IDM_ICON_COLOR_RESET: u16 = 83; const WM_DPICHANGED_MSG: u32 = 0x02E0; const WM_APP_UPDATE_CHECK_COMPLETE: u32 = WM_APP + 2; @@ -316,6 +326,35 @@ struct SettingsFile { show_codex: bool, #[serde(default = "default_show_antigravity")] show_antigravity: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + icon_color_claude: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + icon_color_codex: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + icon_color_antigravity: Option, +} + +/// Hex-encoded background/text color pair, as persisted in settings.json. +#[derive(Debug, Serialize, Deserialize, Clone)] +struct IconColorSetting { + background: String, + text: String, +} + +impl IconColorSetting { + fn from_override(over: tray_icon::IconColorOverride) -> Self { + Self { + background: over.background.to_hex(), + text: over.text.to_hex(), + } + } + + fn to_override(&self) -> tray_icon::IconColorOverride { + tray_icon::IconColorOverride { + background: Color::from_hex(&self.background), + text: Color::from_hex(&self.text), + } + } } impl Default for SettingsFile { @@ -330,6 +369,9 @@ impl Default for SettingsFile { show_claude_code: true, show_codex: false, show_antigravity: false, + icon_color_claude: None, + icon_color_codex: None, + icon_color_antigravity: None, } } } @@ -391,6 +433,11 @@ fn save_state_settings() { show_claude_code: s.show_claude_code, show_codex: s.show_codex, show_antigravity: s.show_antigravity, + icon_color_claude: s.icon_color_claude.map(IconColorSetting::from_override), + icon_color_codex: s.icon_color_codex.map(IconColorSetting::from_override), + icon_color_antigravity: s + .icon_color_antigravity + .map(IconColorSetting::from_override), }); } } @@ -410,6 +457,7 @@ fn tray_icon_data_from_state() -> Vec { s.session_text, s.weekly_text ), + custom_color: s.icon_color_claude, }); } if s.show_codex { @@ -422,6 +470,7 @@ fn tray_icon_data_from_state() -> Vec { s.codex_session_text, s.codex_weekly_text ), + custom_color: s.icon_color_codex, }); } if s.show_antigravity { @@ -434,6 +483,7 @@ fn tray_icon_data_from_state() -> Vec { s.antigravity_session_text, s.antigravity_weekly_text ), + custom_color: s.icon_color_antigravity, }); } icons @@ -445,6 +495,7 @@ fn tray_icon_data_from_state() -> Vec { kind: tray_icon::TrayIconKind::Claude, percent: None, tooltip: s.language.strings().window_title.to_string(), + custom_color: s.icon_color_claude, }); } if s.show_codex { @@ -452,6 +503,7 @@ fn tray_icon_data_from_state() -> Vec { kind: tray_icon::TrayIconKind::Codex, percent: None, tooltip: s.language.strings().codex_window_title.to_string(), + custom_color: s.icon_color_codex, }); } if s.show_antigravity { @@ -459,6 +511,7 @@ fn tray_icon_data_from_state() -> Vec { kind: tray_icon::TrayIconKind::Antigravity, percent: None, tooltip: s.language.strings().antigravity_window_title.to_string(), + custom_color: s.icon_color_antigravity, }); } icons @@ -472,6 +525,114 @@ fn sync_tray_icons(hwnd: HWND) { tray_icon::sync(hwnd, &icons); } +/// Custom-color swatches remembered across ChooseColorW invocations (Win32 requires +/// a caller-owned 16-entry buffer even when custom swatches aren't otherwise used). +static ICON_COLOR_CUSTOM_COLORS: Mutex<[u32; 16]> = Mutex::new([0x00FF_FFFF; 16]); + +/// Title for the next ChooseColorW dialog, staged here because `choose_color` +/// is fully modal on the UI thread (no re-entrancy) and reading it in the hook +/// this way avoids trusting the WM_INITDIALOG lParam's exact pointee type. +static PENDING_DIALOG_TITLE: Mutex> = Mutex::new(Vec::new()); + +/// WM_INITDIALOG hook for ChooseColorW: relabels the dialog's title bar so the +/// user can tell the background-color prompt apart from the text-color prompt. +unsafe extern "system" fn color_dialog_hook( + hdlg: HWND, + msg: u32, + _wparam: WPARAM, + _lparam: LPARAM, +) -> usize { + if msg == WM_INITDIALOG { + let title = PENDING_DIALOG_TITLE.lock().unwrap_or_else(|e| e.into_inner()); + if !title.is_empty() { + unsafe { + let _ = SetWindowTextW(hdlg, PCWSTR::from_raw(title.as_ptr())); + } + } + } + 0 +} + +/// Show the native Windows color picker, seeded with `initial` and titled with +/// `title` (so the user can tell which color they're editing). Returns `None` +/// if the user cancels. +fn choose_color(hwnd: HWND, initial: Color, title: &str) -> Option { + let mut custom_colors = ICON_COLOR_CUSTOM_COLORS + .lock() + .unwrap_or_else(|e| e.into_inner()); + { + let mut pending = PENDING_DIALOG_TITLE.lock().unwrap_or_else(|e| e.into_inner()); + *pending = native_interop::wide_str(title); + } + unsafe { + let mut cc: CHOOSECOLORW = std::mem::zeroed(); + cc.lStructSize = std::mem::size_of::() as u32; + cc.hwndOwner = hwnd; + cc.rgbResult = COLORREF(initial.to_colorref()); + cc.lpCustColors = custom_colors.as_mut_ptr() as *mut COLORREF; + cc.Flags = CC_RGBINIT | CC_FULLOPEN | CC_ENABLEHOOK; + cc.lpfnHook = Some(color_dialog_hook); + if ChooseColorW(&mut cc).as_bool() { + Some(Color::from_colorref(cc.rgbResult.0)) + } else { + None + } + } +} + +/// Prompt the user for a background color and then a text color for one tray icon +/// kind, applying both together only if neither dialog is cancelled. +fn prompt_icon_color(hwnd: HWND, kind: tray_icon::TrayIconKind) { + let (current, strings, model_name) = { + let state = lock_state(); + match state.as_ref() { + Some(s) => { + let over = match kind { + tray_icon::TrayIconKind::Claude => s.icon_color_claude, + tray_icon::TrayIconKind::Codex => s.icon_color_codex, + tray_icon::TrayIconKind::Antigravity => s.icon_color_antigravity, + }; + let strings = s.language.strings(); + let model_name = match kind { + tray_icon::TrayIconKind::Claude => strings.claude_code_model, + tray_icon::TrayIconKind::Codex => strings.codex_model, + tray_icon::TrayIconKind::Antigravity => strings.antigravity_model, + }; + (over, strings, model_name) + } + None => (None, LanguageId::English.strings(), ""), + } + }; + let (initial_bg, initial_text) = match current { + Some(over) => (over.background, over.text), + None => (Color::from_hex("#D97757"), Color::from_hex("#FFFFFF")), + }; + + let bg_title = format!("{} \u{2014} {}", model_name, strings.background_color); + let text_title = format!("{} \u{2014} {}", model_name, strings.text_color); + + let Some(background) = choose_color(hwnd, initial_bg, &bg_title) else { + return; + }; + let Some(text) = choose_color(hwnd, initial_text, &text_title) else { + return; + }; + + { + let mut state = lock_state(); + if let Some(s) = state.as_mut() { + let over = tray_icon::IconColorOverride { background, text }; + match kind { + tray_icon::TrayIconKind::Claude => s.icon_color_claude = Some(over), + tray_icon::TrayIconKind::Codex => s.icon_color_codex = Some(over), + tray_icon::TrayIconKind::Antigravity => s.icon_color_antigravity = Some(over), + } + } + } + save_state_settings(); + sync_tray_icons(hwnd); +} + fn toggle_widget_visibility(hwnd: HWND) { let new_visible = { let mut state = lock_state(); @@ -1310,6 +1471,18 @@ pub fn run() { show_claude_code: settings.show_claude_code, show_codex: settings.show_codex, show_antigravity: settings.show_antigravity, + icon_color_claude: settings + .icon_color_claude + .as_ref() + .map(IconColorSetting::to_override), + icon_color_codex: settings + .icon_color_codex + .as_ref() + .map(IconColorSetting::to_override), + icon_color_antigravity: settings + .icon_color_antigravity + .as_ref() + .map(IconColorSetting::to_override), data: None, poll_interval_ms: settings.poll_interval_ms, retry_count: 0, @@ -2633,6 +2806,27 @@ unsafe extern "system" fn wnd_proc( do_poll(sh); }); } + IDM_ICON_COLOR_CLAUDE => { + prompt_icon_color(hwnd, tray_icon::TrayIconKind::Claude); + } + IDM_ICON_COLOR_CODEX => { + prompt_icon_color(hwnd, tray_icon::TrayIconKind::Codex); + } + IDM_ICON_COLOR_ANTIGRAVITY => { + prompt_icon_color(hwnd, tray_icon::TrayIconKind::Antigravity); + } + IDM_ICON_COLOR_RESET => { + { + let mut state = lock_state(); + if let Some(s) = state.as_mut() { + s.icon_color_claude = None; + s.icon_color_codex = None; + s.icon_color_antigravity = None; + } + } + save_state_settings(); + sync_tray_icons(hwnd); + } IDM_LANG_SYSTEM | IDM_LANG_ENGLISH | IDM_LANG_DUTCH @@ -2835,6 +3029,55 @@ fn show_context_menu(hwnd: HWND) { PCWSTR::from_raw(models_label.as_ptr()), ); + // Icon Colors submenu + let icon_colors_menu = CreatePopupMenu().unwrap(); + + if show_claude_code { + let label = native_interop::wide_str(strings.claude_code_color); + let _ = AppendMenuW( + icon_colors_menu, + MENU_ITEM_FLAGS(0), + IDM_ICON_COLOR_CLAUDE as usize, + PCWSTR::from_raw(label.as_ptr()), + ); + } + if show_codex { + let label = native_interop::wide_str(strings.codex_color); + let _ = AppendMenuW( + icon_colors_menu, + MENU_ITEM_FLAGS(0), + IDM_ICON_COLOR_CODEX as usize, + PCWSTR::from_raw(label.as_ptr()), + ); + } + if show_antigravity { + let label = native_interop::wide_str(strings.antigravity_color); + let _ = AppendMenuW( + icon_colors_menu, + MENU_ITEM_FLAGS(0), + IDM_ICON_COLOR_ANTIGRAVITY as usize, + PCWSTR::from_raw(label.as_ptr()), + ); + } + + let _ = AppendMenuW(icon_colors_menu, MF_SEPARATOR, 0, PCWSTR::null()); + + let reset_colors_label = native_interop::wide_str(strings.reset_icon_colors); + let _ = AppendMenuW( + icon_colors_menu, + MENU_ITEM_FLAGS(0), + IDM_ICON_COLOR_RESET as usize, + PCWSTR::from_raw(reset_colors_label.as_ptr()), + ); + + let icon_colors_label = native_interop::wide_str(strings.icon_colors); + let _ = AppendMenuW( + menu, + MF_POPUP, + icon_colors_menu.0 as usize, + PCWSTR::from_raw(icon_colors_label.as_ptr()), + ); + // Settings submenu let settings_menu = CreatePopupMenu().unwrap();