From 82dbf08a627478a7af48e625a79705fe49753fdd Mon Sep 17 00:00:00 2001 From: NessZerra <90105158+Finesssee@users.noreply.github.com> Date: Sun, 13 Sep 2026 18:21:33 +0700 Subject: [PATCH 1/3] Port v0.59 tray pace colors (cherry picked from commit d39adafd2c61dae8d15610da5ddc97a712bd318c) --- .../src-tauri/src/commands/bridge.rs | 2 + .../src-tauri/src/commands/settings.rs | 32 ++++++++ .../src-tauri/src/tray_bridge.rs | 56 ++++++++++++-- apps/desktop-tauri/src/App.test.tsx | 1 + .../src/floatbar/FloatBar.test.tsx | 1 + apps/desktop-tauri/src/i18n/keys.ts | 2 + .../src/surfaces/TrayPanel.test.tsx | 1 + .../surfaces/settings/tabs/AboutTab.test.tsx | 1 + .../settings/tabs/AdvancedTab.test.tsx | 1 + .../settings/tabs/DisplayTab.test.tsx | 22 +++++- .../src/surfaces/settings/tabs/DisplayTab.tsx | 12 +++ .../settings/tabs/GeneralTab.test.tsx | 1 + apps/desktop-tauri/src/types/bridge.test.ts | 1 + apps/desktop-tauri/src/types/bridge.ts | 2 + rust/src/locale.rs | 2 + rust/src/locale/en-US.ftl | 2 + rust/src/settings.rs | 5 ++ rust/src/settings/raw.rs | 4 + rust/src/settings/tests.rs | 17 +++++ rust/src/tray/mod.rs | 5 +- rust/src/tray/render.rs | 75 ++++++++++++++++++- 21 files changed, 234 insertions(+), 11 deletions(-) diff --git a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs index ab5efbef8a..27978315d1 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs @@ -664,6 +664,7 @@ pub struct SettingsSnapshot { switcher_shows_icons: bool, menu_bar_shows_highest_usage: bool, menu_bar_shows_percent: bool, + menu_bar_color_pace: bool, show_as_used: bool, show_all_token_accounts_in_menu: bool, enable_animations: bool, @@ -786,6 +787,7 @@ impl From for SettingsSnapshot { switcher_shows_icons: settings.switcher_shows_icons, menu_bar_shows_highest_usage: settings.menu_bar_shows_highest_usage, menu_bar_shows_percent: settings.menu_bar_shows_percent, + menu_bar_color_pace: settings.menu_bar_color_pace, show_as_used: settings.show_as_used, show_all_token_accounts_in_menu: settings.show_all_token_accounts_in_menu, enable_animations: settings.enable_animations, diff --git a/apps/desktop-tauri/src-tauri/src/commands/settings.rs b/apps/desktop-tauri/src-tauri/src/commands/settings.rs index df0e6621a9..ab71f14b54 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/settings.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/settings.rs @@ -29,6 +29,7 @@ pub struct SettingsUpdate { pub switcher_shows_icons: Option, pub menu_bar_shows_highest_usage: Option, pub menu_bar_shows_percent: Option, + pub menu_bar_color_pace: Option, pub show_as_used: Option, pub show_all_token_accounts_in_menu: Option, pub enable_animations: Option, @@ -124,6 +125,7 @@ impl SettingsUpdate { || self.switcher_shows_icons.is_some() || self.menu_bar_shows_highest_usage.is_some() || self.menu_bar_shows_percent.is_some() + || self.menu_bar_color_pace.is_some() || self.show_as_used.is_some() || self.reset_time_relative.is_some() || self.menu_bar_display_mode.is_some() @@ -258,6 +260,9 @@ impl SettingsUpdate { if let Some(v) = self.menu_bar_shows_percent { settings.menu_bar_shows_percent = v; } + if let Some(v) = self.menu_bar_color_pace { + settings.menu_bar_color_pace = v; + } if let Some(v) = self.show_all_token_accounts_in_menu { settings.show_all_token_accounts_in_menu = v; } @@ -700,6 +705,33 @@ mod tests { } .refreshes_tray_presentation() ); + assert!( + SettingsUpdate { + menu_bar_color_pace: Some(true), + ..Default::default() + } + .refreshes_tray_presentation() + ); + } + + #[test] + fn apply_display_settings_updates_tray_pace_color() { + let mut settings = Settings::default(); + assert!(!settings.menu_bar_color_pace); + + SettingsUpdate { + menu_bar_color_pace: Some(true), + ..Default::default() + } + .apply_display_settings(&mut settings); + assert!(settings.menu_bar_color_pace); + + SettingsUpdate { + menu_bar_color_pace: Some(false), + ..Default::default() + } + .apply_display_settings(&mut settings); + assert!(!settings.menu_bar_color_pace); } #[test] diff --git a/apps/desktop-tauri/src-tauri/src/tray_bridge.rs b/apps/desktop-tauri/src-tauri/src/tray_bridge.rs index cb11675767..c69fe320b9 100644 --- a/apps/desktop-tauri/src-tauri/src/tray_bridge.rs +++ b/apps/desktop-tauri/src-tauri/src/tray_bridge.rs @@ -12,7 +12,9 @@ use tauri::menu::{CheckMenuItemBuilder, IsMenuItem, Menu, MenuItem, PredefinedMe use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}; use tauri::{AppHandle, Manager}; -use codexbar::tray::{render_bar_icon_rgba, render_percent_icon_rgba}; +use codexbar::tray::{ + TrayPaceColor, render_bar_icon_rgba_with_pace, render_percent_icon_rgba_with_pace, +}; use crate::shell; use crate::state::{AppState, TrayAnchor}; @@ -419,8 +421,10 @@ pub fn update_tray_icon_and_tooltip( None, ), }; + let pace = picked.and_then(|snapshot| snapshot.pace.as_ref()); - let (rgba, w, h) = render_tray_icon_for_settings(&settings, session_pct, weekly_pct, all_error); + let (rgba, w, h) = + render_tray_icon_for_settings(&settings, session_pct, weekly_pct, all_error, pace); let icon = Image::new_owned(rgba, w, h); let _ = tray.set_icon(Some(icon)); @@ -557,11 +561,17 @@ fn render_tray_icon_for_settings( session_pct: f64, weekly_pct: Option, all_error: bool, + pace: Option<&crate::commands::PaceSnapshot>, ) -> (Vec, u32, u32) { + let pace_color = if settings.menu_bar_color_pace { + pace.and_then(|snapshot| TrayPaceColor::from_delta(snapshot.delta_percent)) + } else { + None + }; if settings.menu_bar_shows_percent { - render_percent_icon_rgba(session_pct, all_error) + render_percent_icon_rgba_with_pace(session_pct, all_error, pace_color) } else { - render_bar_icon_rgba(session_pct, weekly_pct, all_error) + render_bar_icon_rgba_with_pace(session_pct, weekly_pct, all_error, pace_color) } } @@ -1081,14 +1091,48 @@ mod tests { }; let (bar, bar_w, bar_h) = - render_tray_icon_for_settings(&bar_settings, 72.0, Some(40.0), false); + render_tray_icon_for_settings(&bar_settings, 72.0, Some(40.0), false, None); let (percent, pct_w, pct_h) = - render_tray_icon_for_settings(&percent_settings, 72.0, Some(40.0), false); + render_tray_icon_for_settings(&percent_settings, 72.0, Some(40.0), false, None); assert_eq!((bar_w, bar_h), (pct_w, pct_h)); assert_ne!(bar, percent); } + #[test] + fn tray_pace_color_is_opt_in_and_follows_delta_direction() { + let pace_behind = crate::commands::PaceSnapshot { + stage: "behind".to_string(), + delta_percent: -8.0, + will_last_to_reset: true, + eta_seconds: None, + expected_used_percent: 40.0, + actual_used_percent: 32.0, + }; + let pace_ahead = crate::commands::PaceSnapshot { + delta_percent: 8.0, + stage: "ahead".to_string(), + ..pace_behind.clone() + }; + let default_settings = Settings::default(); + let colored_settings = Settings { + menu_bar_color_pace: true, + ..Settings::default() + }; + + let normal = render_tray_icon_for_settings(&default_settings, 50.0, None, false, None); + let disabled = + render_tray_icon_for_settings(&default_settings, 50.0, None, false, Some(&pace_behind)); + let behind = + render_tray_icon_for_settings(&colored_settings, 50.0, None, false, Some(&pace_behind)); + let ahead = + render_tray_icon_for_settings(&colored_settings, 50.0, None, false, Some(&pace_ahead)); + + assert_eq!(normal, disabled); + assert_ne!(normal, behind); + assert_ne!(behind, ahead); + } + #[test] fn tooltip_uses_compact_status_labels() { let mut claude = fake_snapshot("claude", "Claude", 13.0); diff --git a/apps/desktop-tauri/src/App.test.tsx b/apps/desktop-tauri/src/App.test.tsx index 10e46694eb..937f3cca18 100644 --- a/apps/desktop-tauri/src/App.test.tsx +++ b/apps/desktop-tauri/src/App.test.tsx @@ -89,6 +89,7 @@ function settings(overrides: Partial = {}): SettingsSnapshot { switcherShowsIcons: true, menuBarShowsHighestUsage: false, menuBarShowsPercent: false, + menuBarColorPace: false, showAsUsed: true, showAllTokenAccountsInMenu: false, enableAnimations: true, diff --git a/apps/desktop-tauri/src/floatbar/FloatBar.test.tsx b/apps/desktop-tauri/src/floatbar/FloatBar.test.tsx index 88860e087d..2919389dbe 100644 --- a/apps/desktop-tauri/src/floatbar/FloatBar.test.tsx +++ b/apps/desktop-tauri/src/floatbar/FloatBar.test.tsx @@ -153,6 +153,7 @@ function settings(overrides: Partial = {}): SettingsSnapshot { switcherShowsIcons: true, menuBarShowsHighestUsage: false, menuBarShowsPercent: false, + menuBarColorPace: false, showAsUsed: true, showAllTokenAccountsInMenu: false, enableAnimations: true, diff --git a/apps/desktop-tauri/src/i18n/keys.ts b/apps/desktop-tauri/src/i18n/keys.ts index 90c0984f49..2fb753d3df 100644 --- a/apps/desktop-tauri/src/i18n/keys.ts +++ b/apps/desktop-tauri/src/i18n/keys.ts @@ -546,6 +546,8 @@ export const ALL_LOCALE_KEYS = [ "PreferHighestUsageHelper", "ShowPercentInTray", "ShowPercentInTrayHelper", + "ColorPaceInTray", + "ColorPaceInTrayHelper", "DisplayModeLabel", "DisplayModeHelper", "DisplayModeDetailed", diff --git a/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx b/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx index 4655c3b58f..fd8a3ddfba 100644 --- a/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx +++ b/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx @@ -145,6 +145,7 @@ function settings(overrides: Partial = {}): SettingsSnapshot { switcherShowsIcons: true, menuBarShowsHighestUsage: false, menuBarShowsPercent: false, + menuBarColorPace: false, showAsUsed: true, showAllTokenAccountsInMenu: false, enableAnimations: true, diff --git a/apps/desktop-tauri/src/surfaces/settings/tabs/AboutTab.test.tsx b/apps/desktop-tauri/src/surfaces/settings/tabs/AboutTab.test.tsx index 1a818ed97f..38fcdd5a62 100644 --- a/apps/desktop-tauri/src/surfaces/settings/tabs/AboutTab.test.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/tabs/AboutTab.test.tsx @@ -64,6 +64,7 @@ const settings: SettingsSnapshot = { switcherShowsIcons: true, menuBarShowsHighestUsage: true, menuBarShowsPercent: true, + menuBarColorPace: false, showAsUsed: false, showAllTokenAccountsInMenu: true, enableAnimations: true, diff --git a/apps/desktop-tauri/src/surfaces/settings/tabs/AdvancedTab.test.tsx b/apps/desktop-tauri/src/surfaces/settings/tabs/AdvancedTab.test.tsx index 8d1d7f2148..693155d64e 100644 --- a/apps/desktop-tauri/src/surfaces/settings/tabs/AdvancedTab.test.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/tabs/AdvancedTab.test.tsx @@ -33,6 +33,7 @@ const settings: SettingsSnapshot = { switcherShowsIcons: true, menuBarShowsHighestUsage: true, menuBarShowsPercent: true, + menuBarColorPace: false, showAsUsed: false, showAllTokenAccountsInMenu: true, enableAnimations: true, diff --git a/apps/desktop-tauri/src/surfaces/settings/tabs/DisplayTab.test.tsx b/apps/desktop-tauri/src/surfaces/settings/tabs/DisplayTab.test.tsx index bff42a55b3..0cb8155cc5 100644 --- a/apps/desktop-tauri/src/surfaces/settings/tabs/DisplayTab.test.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/tabs/DisplayTab.test.tsx @@ -19,6 +19,7 @@ const baseSettings = { switcherShowsIcons: false, menuBarShowsHighestUsage: false, menuBarShowsPercent: false, + menuBarColorPace: false, menuBarDisplayMode: "detailed", overviewLayout: "detailed", windowScalePercent: 100, @@ -29,9 +30,17 @@ const baseSettings = { showPace: false, } as unknown as SettingsSnapshot; -function renderTab(set: (patch: Record) => void) { +function renderTab( + set: (patch: Record) => void, + mode: "menuBar" | "menu" = "menu", +) { return render( - , + , ); } @@ -83,4 +92,13 @@ describe("DisplayTab menu settings", () => { expect(set).toHaveBeenCalledWith({ trayPanelAlwaysOnTop: true }); }); + + it("updates the tray pace color preference", () => { + const set = vi.fn(); + renderTab(set, "menuBar"); + + fireEvent.click(screen.getByRole("checkbox", { name: "ColorPaceInTray" })); + + expect(set).toHaveBeenCalledWith({ menuBarColorPace: true }); + }); }); diff --git a/apps/desktop-tauri/src/surfaces/settings/tabs/DisplayTab.tsx b/apps/desktop-tauri/src/surfaces/settings/tabs/DisplayTab.tsx index 52588deb84..aac527ae83 100644 --- a/apps/desktop-tauri/src/surfaces/settings/tabs/DisplayTab.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/tabs/DisplayTab.tsx @@ -79,6 +79,18 @@ export default function DisplayTab({ onChange={(v) => set({ menuBarShowsPercent: v })} /> + + set({ menuBarColorPace: v })} + /> + { switcherShowsIcons: true, menuBarShowsHighestUsage: true, menuBarShowsPercent: true, + menuBarColorPace: false, showAsUsed: false, showAllTokenAccountsInMenu: true, enableAnimations: true, diff --git a/apps/desktop-tauri/src/types/bridge.ts b/apps/desktop-tauri/src/types/bridge.ts index 88754baa7d..296f52fb19 100644 --- a/apps/desktop-tauri/src/types/bridge.ts +++ b/apps/desktop-tauri/src/types/bridge.ts @@ -193,6 +193,7 @@ export interface SettingsSnapshot { switcherShowsIcons: boolean; menuBarShowsHighestUsage: boolean; menuBarShowsPercent: boolean; + menuBarColorPace: boolean; showAsUsed: boolean; showAllTokenAccountsInMenu: boolean; enableAnimations: boolean; @@ -302,6 +303,7 @@ export interface SettingsUpdate { switcherShowsIcons?: boolean; menuBarShowsHighestUsage?: boolean; menuBarShowsPercent?: boolean; + menuBarColorPace?: boolean; showAsUsed?: boolean; showAllTokenAccountsInMenu?: boolean; enableAnimations?: boolean; diff --git a/rust/src/locale.rs b/rust/src/locale.rs index 27f536ae93..df5e2b1aa7 100644 --- a/rust/src/locale.rs +++ b/rust/src/locale.rs @@ -812,6 +812,8 @@ locale_keys! { PreferHighestUsageHelper, ShowPercentInTray, ShowPercentInTrayHelper, + ColorPaceInTray, + ColorPaceInTrayHelper, DisplayModeLabel, DisplayModeHelper, DisplayModeDetailed, diff --git a/rust/src/locale/en-US.ftl b/rust/src/locale/en-US.ftl index a0865280aa..31bf616c22 100644 --- a/rust/src/locale/en-US.ftl +++ b/rust/src/locale/en-US.ftl @@ -483,6 +483,8 @@ PreferHighestUsage = Prefer highest usage PreferHighestUsageHelper = Show the provider closest to its limit in the merged tray display. ShowPercentInTray = Show percent in tray ShowPercentInTrayHelper = Replace usage bar with provider branding + percentage text. +ColorPaceInTray = Color tray usage by pace +ColorPaceInTrayHelper = Color the Windows tray usage indicator green when behind pace and red when ahead. DisplayModeLabel = Display mode DisplayModeHelper = Level of detail shown in the menu bar label. DisplayModeDetailed = Detailed diff --git a/rust/src/settings.rs b/rust/src/settings.rs index 9863f194ce..ff5b566784 100755 --- a/rust/src/settings.rs +++ b/rust/src/settings.rs @@ -148,6 +148,10 @@ pub struct Settings { #[serde(default)] pub menu_bar_shows_percent: bool, + /// Colour the Windows tray usage indicator by pace when enabled. + #[serde(default)] + pub menu_bar_color_pace: bool, + /// Show usage bars as "used" (true) or "remaining" (false) pub show_as_used: bool, @@ -544,6 +548,7 @@ impl Default for Settings { switcher_shows_icons: true, menu_bar_shows_highest_usage: false, menu_bar_shows_percent: false, + menu_bar_color_pace: false, show_as_used: true, // Show as "used" by default enable_animations: true, // Animations enabled by default reset_time_relative: true, // Show relative times by default diff --git a/rust/src/settings/raw.rs b/rust/src/settings/raw.rs index 850c90e960..a94fdc6177 100644 --- a/rust/src/settings/raw.rs +++ b/rust/src/settings/raw.rs @@ -39,6 +39,8 @@ pub(super) struct RawSettings { switcher_shows_icons: bool, menu_bar_shows_highest_usage: bool, menu_bar_shows_percent: bool, + #[serde(default)] + menu_bar_color_pace: bool, show_as_used: bool, enable_animations: bool, reset_time_relative: bool, @@ -209,6 +211,7 @@ impl Default for RawSettings { switcher_shows_icons: s.switcher_shows_icons, menu_bar_shows_highest_usage: s.menu_bar_shows_highest_usage, menu_bar_shows_percent: s.menu_bar_shows_percent, + menu_bar_color_pace: s.menu_bar_color_pace, show_as_used: s.show_as_used, enable_animations: s.enable_animations, reset_time_relative: s.reset_time_relative, @@ -536,6 +539,7 @@ impl From for Settings { switcher_shows_icons: raw.switcher_shows_icons, menu_bar_shows_highest_usage: raw.menu_bar_shows_highest_usage, menu_bar_shows_percent: raw.menu_bar_shows_percent, + menu_bar_color_pace: raw.menu_bar_color_pace, show_as_used: raw.show_as_used, enable_animations: raw.enable_animations, reset_time_relative: raw.reset_time_relative, diff --git a/rust/src/settings/tests.rs b/rust/src/settings/tests.rs index a6db4aa05b..2db7eb5124 100644 --- a/rust/src/settings/tests.rs +++ b/rust/src/settings/tests.rs @@ -115,6 +115,23 @@ fn open_codex_usage_logs_default_off_and_round_trip() { assert!(loaded.hide_native_codex_cost_when_open_codex_present); } +#[test] +fn tray_pace_color_defaults_off_and_round_trips() { + let defaulted: Settings = serde_json::from_str(r#"{ "enabled_providers": [] }"#) + .expect("missing tray pace color defaults false"); + assert!(!defaulted.menu_bar_color_pace); + + let enabled = Settings { + menu_bar_color_pace: true, + ..Settings::default() + }; + let json = serde_json::to_string(&enabled).expect("serialize tray pace color"); + assert!(json.contains(r#""menu_bar_color_pace":true"#)); + + let loaded: Settings = serde_json::from_str(&json).expect("deserialize tray pace color"); + assert!(loaded.menu_bar_color_pace); +} + #[test] fn notification_sound_paths_round_trip_and_default_for_existing_settings() { let settings = Settings { diff --git a/rust/src/tray/mod.rs b/rust/src/tray/mod.rs index bf4c52ef88..8afc577e5d 100755 --- a/rust/src/tray/mod.rs +++ b/rust/src/tray/mod.rs @@ -6,4 +6,7 @@ pub mod icon; pub mod render; pub use icon::LoadingPattern; -pub use render::{TRAY_ICON_SIZE, render_bar_icon_rgba, render_percent_icon_rgba}; +pub use render::{ + TRAY_ICON_SIZE, TrayPaceColor, render_bar_icon_rgba, render_bar_icon_rgba_with_pace, + render_percent_icon_rgba, render_percent_icon_rgba_with_pace, +}; diff --git a/rust/src/tray/render.rs b/rust/src/tray/render.rs index 5b486a5b6b..a22ac8f9cc 100644 --- a/rust/src/tray/render.rs +++ b/rust/src/tray/render.rs @@ -10,6 +10,38 @@ use super::icon::UsageLevel; /// Side length of the generated tray icon in pixels. pub const TRAY_ICON_SIZE: u32 = 32; +/// Optional pace tint for a tray icon's usage indicator. +/// +/// The upstream pace treatment uses green when usage is behind the expected +/// pace and red when it is ahead. On-track (zero) and unavailable deltas keep +/// the normal usage-level colour. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TrayPaceColor { + Behind, + Ahead, +} + +impl TrayPaceColor { + /// Map a finite, non-zero pace delta to its tray tint. + pub fn from_delta(delta_percent: f64) -> Option { + if !delta_percent.is_finite() || delta_percent == 0.0 { + return None; + } + if delta_percent < 0.0 { + Some(Self::Behind) + } else { + Some(Self::Ahead) + } + } + + fn rgb(self) -> (u8, u8, u8) { + match self { + Self::Behind => (76, 175, 80), + Self::Ahead => (244, 67, 54), + } + } +} + /// Render a usage-bar tray icon as raw RGBA bytes. /// /// - `session_percent`: primary bar fill (0–100), colour-coded by [`UsageLevel`] @@ -23,6 +55,16 @@ pub fn render_bar_icon_rgba( session_percent: f64, weekly_percent: Option, has_error: bool, +) -> (Vec, u32, u32) { + render_bar_icon_rgba_with_pace(session_percent, weekly_percent, has_error, None) +} + +/// Render a usage-bar tray icon with an optional pace tint. +pub fn render_bar_icon_rgba_with_pace( + session_percent: f64, + weekly_percent: Option, + has_error: bool, + pace_color: Option, ) -> (Vec, u32, u32) { const SZ: u32 = TRAY_ICON_SIZE; let mut img: RgbaImage = ImageBuffer::new(SZ, SZ); @@ -40,7 +82,9 @@ pub fn render_bar_icon_rgba( } let color_for = |percent: f64| -> (u8, u8, u8) { - let (r, g, b) = UsageLevel::from_percent(percent).color(); + let (r, g, b) = pace_color + .map(TrayPaceColor::rgb) + .unwrap_or_else(|| UsageLevel::from_percent(percent).color()); if has_error { // Average of three u8 colour channels: sum ≤ 765, so /3 ≤ 255 fits u8. #[allow( @@ -95,6 +139,15 @@ pub fn render_bar_icon_rgba( /// Render a compact numeric percent tray icon as raw RGBA bytes. pub fn render_percent_icon_rgba(percent: f64, has_error: bool) -> (Vec, u32, u32) { + render_percent_icon_rgba_with_pace(percent, has_error, None) +} + +/// Render a compact numeric percent tray icon with an optional pace tint. +pub fn render_percent_icon_rgba_with_pace( + percent: f64, + has_error: bool, + pace_color: Option, +) -> (Vec, u32, u32) { const SZ: u32 = TRAY_ICON_SIZE; let mut img: RgbaImage = ImageBuffer::new(SZ, SZ); @@ -134,7 +187,9 @@ pub fn render_percent_icon_rgba(percent: f64, has_error: bool) -> (Vec, u32, let start_x = (SZ.saturating_sub(text_width)) / 2; let start_y = (SZ.saturating_sub(text_height)) / 2; - let (r, g, b) = UsageLevel::from_percent(percent).color(); + let (r, g, b) = pace_color + .map(TrayPaceColor::rgb) + .unwrap_or_else(|| UsageLevel::from_percent(percent).color()); let color = if has_error { // Average of three u8 colour channels: sum ≤ 765, so /3 ≤ 255 fits u8. #[allow( @@ -304,4 +359,20 @@ mod tests { let (rgba, w, h) = render_percent_icon_rgba(125.0, false); assert_eq!(u32::try_from(rgba.len()).unwrap(), w * h * 4); } + + #[test] + fn pace_color_maps_direction_and_tints_bars() { + assert_eq!(TrayPaceColor::from_delta(-0.1), Some(TrayPaceColor::Behind)); + assert_eq!(TrayPaceColor::from_delta(0.1), Some(TrayPaceColor::Ahead)); + assert_eq!(TrayPaceColor::from_delta(0.0), None); + assert_eq!(TrayPaceColor::from_delta(f64::NAN), None); + + let (behind, _, _) = + render_bar_icon_rgba_with_pace(100.0, None, false, Some(TrayPaceColor::Behind)); + let (ahead, _, _) = + render_bar_icon_rgba_with_pace(100.0, None, false, Some(TrayPaceColor::Ahead)); + let idx = ((16 * TRAY_ICON_SIZE + 8) * 4) as usize; + assert_eq!(&behind[idx..idx + 3], &[76, 175, 80]); + assert_eq!(&ahead[idx..idx + 3], &[244, 67, 54]); + } } From 2e99806d7bc7f076bbfbcbbc1b8821311e2db13e Mon Sep 17 00:00:00 2001 From: NessZerra <90105158+Finesssee@users.noreply.github.com> Date: Mon, 14 Sep 2026 03:27:19 +0700 Subject: [PATCH 2/3] Use canonical pace stages for tray colors (cherry picked from commit 5435c41ffb01a3aefe3f70a15d96ae6e8155d61e) --- .../src-tauri/src/tray_bridge.rs | 17 +++++++-- rust/src/tray/render.rs | 35 ++++++++++--------- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/apps/desktop-tauri/src-tauri/src/tray_bridge.rs b/apps/desktop-tauri/src-tauri/src/tray_bridge.rs index c69fe320b9..fbc9b1d205 100644 --- a/apps/desktop-tauri/src-tauri/src/tray_bridge.rs +++ b/apps/desktop-tauri/src-tauri/src/tray_bridge.rs @@ -564,7 +564,7 @@ fn render_tray_icon_for_settings( pace: Option<&crate::commands::PaceSnapshot>, ) -> (Vec, u32, u32) { let pace_color = if settings.menu_bar_color_pace { - pace.and_then(|snapshot| TrayPaceColor::from_delta(snapshot.delta_percent)) + pace.and_then(|snapshot| TrayPaceColor::from_stage(&snapshot.stage)) } else { None }; @@ -1100,7 +1100,7 @@ mod tests { } #[test] - fn tray_pace_color_is_opt_in_and_follows_delta_direction() { + fn tray_pace_color_is_opt_in_and_follows_canonical_stage() { let pace_behind = crate::commands::PaceSnapshot { stage: "behind".to_string(), delta_percent: -8.0, @@ -1114,6 +1114,11 @@ mod tests { stage: "ahead".to_string(), ..pace_behind.clone() }; + let pace_on_track = crate::commands::PaceSnapshot { + delta_percent: 1.0, + stage: "on_track".to_string(), + ..pace_behind.clone() + }; let default_settings = Settings::default(); let colored_settings = Settings { menu_bar_color_pace: true, @@ -1127,8 +1132,16 @@ mod tests { render_tray_icon_for_settings(&colored_settings, 50.0, None, false, Some(&pace_behind)); let ahead = render_tray_icon_for_settings(&colored_settings, 50.0, None, false, Some(&pace_ahead)); + let on_track = render_tray_icon_for_settings( + &colored_settings, + 50.0, + None, + false, + Some(&pace_on_track), + ); assert_eq!(normal, disabled); + assert_eq!(normal, on_track); assert_ne!(normal, behind); assert_ne!(behind, ahead); } diff --git a/rust/src/tray/render.rs b/rust/src/tray/render.rs index a22ac8f9cc..12c300427f 100644 --- a/rust/src/tray/render.rs +++ b/rust/src/tray/render.rs @@ -13,8 +13,8 @@ pub const TRAY_ICON_SIZE: u32 = 32; /// Optional pace tint for a tray icon's usage indicator. /// /// The upstream pace treatment uses green when usage is behind the expected -/// pace and red when it is ahead. On-track (zero) and unavailable deltas keep -/// the normal usage-level colour. +/// pace and red when it is ahead. On-track and unavailable stages keep the +/// normal usage-level colour. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TrayPaceColor { Behind, @@ -22,15 +22,12 @@ pub enum TrayPaceColor { } impl TrayPaceColor { - /// Map a finite, non-zero pace delta to its tray tint. - pub fn from_delta(delta_percent: f64) -> Option { - if !delta_percent.is_finite() || delta_percent == 0.0 { - return None; - } - if delta_percent < 0.0 { - Some(Self::Behind) - } else { - Some(Self::Ahead) + /// Map the canonical bridge pace stage to its tray tint. + pub fn from_stage(stage: &str) -> Option { + match stage { + "slightly_behind" | "behind" | "far_behind" => Some(Self::Behind), + "slightly_ahead" | "ahead" | "far_ahead" => Some(Self::Ahead), + _ => None, } } @@ -361,11 +358,17 @@ mod tests { } #[test] - fn pace_color_maps_direction_and_tints_bars() { - assert_eq!(TrayPaceColor::from_delta(-0.1), Some(TrayPaceColor::Behind)); - assert_eq!(TrayPaceColor::from_delta(0.1), Some(TrayPaceColor::Ahead)); - assert_eq!(TrayPaceColor::from_delta(0.0), None); - assert_eq!(TrayPaceColor::from_delta(f64::NAN), None); + fn pace_color_maps_canonical_stage_and_tints_bars() { + assert_eq!( + TrayPaceColor::from_stage("slightly_behind"), + Some(TrayPaceColor::Behind) + ); + assert_eq!( + TrayPaceColor::from_stage("far_ahead"), + Some(TrayPaceColor::Ahead) + ); + assert_eq!(TrayPaceColor::from_stage("on_track"), None); + assert_eq!(TrayPaceColor::from_stage("unknown"), None); let (behind, _, _) = render_bar_icon_rgba_with_pace(100.0, None, false, Some(TrayPaceColor::Behind)); From 8c16d5d4aa5de5f7afbb04151cbd84592240cbe9 Mon Sep 17 00:00:00 2001 From: RCD <90105158+Finesssee@users.noreply.github.com> Date: Thu, 1 Oct 2026 04:23:51 +0700 Subject: [PATCH 3/3] Translate tray pace color labels in every locale The re-landed tray pace color toggle added two keys to en-US only. Add them to the other seven locale files so every .ftl stays aligned with locale.rs and keys.ts. --- rust/src/locale/es-MX.ftl | 2 ++ rust/src/locale/ja-JP.ftl | 2 ++ rust/src/locale/ko-KR.ftl | 2 ++ rust/src/locale/ru-RU.ftl | 2 ++ rust/src/locale/tr-TR.ftl | 2 ++ rust/src/locale/zh-CN.ftl | 2 ++ rust/src/locale/zh-TW.ftl | 2 ++ 7 files changed, 14 insertions(+) diff --git a/rust/src/locale/es-MX.ftl b/rust/src/locale/es-MX.ftl index 8195889ad1..1f2d1ad449 100644 --- a/rust/src/locale/es-MX.ftl +++ b/rust/src/locale/es-MX.ftl @@ -439,6 +439,8 @@ PreferHighestUsage = Preferir uso más alto PreferHighestUsageHelper = Mostrar el proveedor más cercano a su límite en la pantalla de bandeja combinada. ShowPercentInTray = Mostrar porcentaje en bandeja ShowPercentInTrayHelper = Reemplazar barra de uso con marca de proveedor + texto de porcentaje. +ColorPaceInTray = Colorear el uso en la bandeja según el ritmo +ColorPaceInTrayHelper = Colorea el indicador de uso de la bandeja de Windows en verde cuando vas por debajo del ritmo y en rojo cuando vas por encima. DisplayModeLabel = Modo de visualización DisplayModeHelper = Nivel de detalle mostrado en la etiqueta de barra de menú. DisplayModeDetailed = Detallado diff --git a/rust/src/locale/ja-JP.ftl b/rust/src/locale/ja-JP.ftl index 70e02dec96..39dbe0a973 100644 --- a/rust/src/locale/ja-JP.ftl +++ b/rust/src/locale/ja-JP.ftl @@ -421,6 +421,8 @@ PreferHighestUsage = 最も使用量が多いものを優先 PreferHighestUsageHelper = 統合トレイ表示で上限に最も近いプロバイダーを表示。 ShowPercentInTray = トレイに割合を表示 ShowPercentInTrayHelper = 使用量バーをプロバイダーのブランド + パーセント表示に置き換え。 +ColorPaceInTray = トレイの使用量をペースで色分け +ColorPaceInTrayHelper = ペースより遅いときは緑、速いときは赤で Windows トレイの使用量インジケーターを表示します。 DisplayModeLabel = 表示モード DisplayModeHelper = メニューバーラベルに表示する詳細度。 DisplayModeDetailed = 詳細 diff --git a/rust/src/locale/ko-KR.ftl b/rust/src/locale/ko-KR.ftl index 394bf2d3bd..8ba58f5586 100644 --- a/rust/src/locale/ko-KR.ftl +++ b/rust/src/locale/ko-KR.ftl @@ -426,6 +426,8 @@ PreferHighestUsage = 가장 높은 사용량 우선 PreferHighestUsageHelper = 병합된 트레이 아이콘에 사용 한도에 가장 가까운 제공업체를 표시합니다. ShowPercentInTray = 트레이에 백분율 표시 ShowPercentInTrayHelper = 사용량 표시줄을 제공업체 브랜드 및 백분율 텍스트로 대체합니다. +ColorPaceInTray = 사용 속도에 따라 트레이 사용량 색상 표시 +ColorPaceInTrayHelper = 사용 속도보다 느리면 녹색, 빠르면 빨간색으로 Windows 트레이 사용량 표시기를 표시합니다. DisplayModeLabel = 디스플레이 모드 DisplayModeHelper = 메뉴 바 라벨에 표시할 세부 정보 수준입니다. DisplayModeDetailed = 상세 diff --git a/rust/src/locale/ru-RU.ftl b/rust/src/locale/ru-RU.ftl index 31e8c9b0b3..0be0f8a7dd 100644 --- a/rust/src/locale/ru-RU.ftl +++ b/rust/src/locale/ru-RU.ftl @@ -405,6 +405,8 @@ PreferHighestUsage = Предпочитаю максимальное испол PreferHighestUsageHelper = Покажите провайдера, ближайшего к своему пределу, на объединенном лотке. ShowPercentInTray = Показывать проценты в трее ShowPercentInTrayHelper = Замените панель использования брендом провайдера + текстом в процентах. +ColorPaceInTray = Окрашивать использование в трее по темпу +ColorPaceInTrayHelper = Индикатор использования в трее Windows становится зелёным, когда вы отстаёте от темпа, и красным, когда опережаете его. DisplayModeLabel = Режим отображения DisplayModeHelper = Уровень детализации отображается в метке строки меню. DisplayModeDetailed = Подробный diff --git a/rust/src/locale/tr-TR.ftl b/rust/src/locale/tr-TR.ftl index 94fa3750d5..cd60c35423 100644 --- a/rust/src/locale/tr-TR.ftl +++ b/rust/src/locale/tr-TR.ftl @@ -442,6 +442,8 @@ PreferHighestUsage = En yüksek kullanımı tercih et PreferHighestUsageHelper = Birleşik tepsi görünümünde sınırına en yakın sağlayıcıyı göster. ShowPercentInTray = Tepside yüzdeyi göster ShowPercentInTrayHelper = Kullanım çubuğunu sağlayıcı markası ve yüzde metniyle değiştir. +ColorPaceInTray = Tepsi kullanımını hıza göre renklendir +ColorPaceInTrayHelper = Windows tepsi kullanım göstergesini hızın gerisindeyken yeşil, önündeyken kırmızı gösterir. DisplayModeLabel = Görünüm modu DisplayModeHelper = Menü çubuğu etiketinde gösterilen ayrıntı düzeyi. DisplayModeDetailed = Ayrıntılı diff --git a/rust/src/locale/zh-CN.ftl b/rust/src/locale/zh-CN.ftl index 71c6a6b2ea..c756e457d6 100644 --- a/rust/src/locale/zh-CN.ftl +++ b/rust/src/locale/zh-CN.ftl @@ -420,6 +420,8 @@ PreferHighestUsage = 优先显示最高用量 PreferHighestUsageHelper = 在合并托盘显示中优先展示最接近限额的服务商。 ShowPercentInTray = 在托盘中显示百分比 ShowPercentInTrayHelper = 使用服务商标识与百分比文字替代用量条。 +ColorPaceInTray = 按进度为托盘用量着色 +ColorPaceInTrayHelper = 用量进度落后时,Windows 托盘用量指示显示为绿色;超前时显示为红色。 DisplayModeLabel = 显示模式 DisplayModeHelper = 菜单栏标签显示的详细程度。 DisplayModeDetailed = 详细 diff --git a/rust/src/locale/zh-TW.ftl b/rust/src/locale/zh-TW.ftl index 9c04c3a347..121a70676c 100644 --- a/rust/src/locale/zh-TW.ftl +++ b/rust/src/locale/zh-TW.ftl @@ -420,6 +420,8 @@ PreferHighestUsage = 優先顯示最高用量 PreferHighestUsageHelper = 在合併系統匣顯示中優先展示最接近限額的提供者。 ShowPercentInTray = 在系統匣中顯示百分比 ShowPercentInTrayHelper = 使用提供者標識與百分比文字替代用量條。 +ColorPaceInTray = 依進度為系統匣用量著色 +ColorPaceInTrayHelper = 用量進度落後時,Windows 系統匣用量指示顯示為綠色;超前時顯示為紅色。 DisplayModeLabel = 顯示模式 DisplayModeHelper = 選單欄標籤顯示的詳細程度。 DisplayModeDetailed = 詳細