From 34178a2c27f3caaa87e18b1a09adf9694d79b771 Mon Sep 17 00:00:00 2001 From: "Carlos Miguel C. Resurreccion" Date: Fri, 15 May 2026 15:08:51 +0800 Subject: [PATCH 1/4] feat: add "Show detailed remaining time" localization Adds the localized label that the upcoming Settings toggle will use. --- src/localization/dutch.rs | 1 + src/localization/english.rs | 1 + src/localization/french.rs | 1 + src/localization/german.rs | 1 + src/localization/japanese.rs | 1 + src/localization/korean.rs | 1 + src/localization/mod.rs | 1 + src/localization/spanish.rs | 1 + src/localization/traditional_chinese.rs | 1 + 9 files changed, 9 insertions(+) diff --git a/src/localization/dutch.rs b/src/localization/dutch.rs index 0eb486d..be4f3bd 100644 --- a/src/localization/dutch.rs +++ b/src/localization/dutch.rs @@ -16,6 +16,7 @@ pub(super) const STRINGS: Strings = Strings { settings: "Instellingen", start_with_windows: "Opstarten met Windows", reset_position: "Positie herstellen", + show_detailed_remaining: "Gedetailleerde resterende tijd tonen", language: "Taal", system_default: "Systeemstandaard", check_for_updates: "Controleren op updates", diff --git a/src/localization/english.rs b/src/localization/english.rs index 2b92f36..32c81a9 100644 --- a/src/localization/english.rs +++ b/src/localization/english.rs @@ -16,6 +16,7 @@ pub(super) const STRINGS: Strings = Strings { settings: "Settings", start_with_windows: "Start with Windows", reset_position: "Reset Position", + show_detailed_remaining: "Show detailed remaining time", language: "Language", system_default: "System Default", check_for_updates: "Check for Updates", diff --git a/src/localization/french.rs b/src/localization/french.rs index fa448fb..195b1a3 100644 --- a/src/localization/french.rs +++ b/src/localization/french.rs @@ -16,6 +16,7 @@ pub(super) const STRINGS: Strings = Strings { settings: "Paramètres", start_with_windows: "Démarrer avec Windows", reset_position: "Réinitialiser la position", + show_detailed_remaining: "Afficher le temps restant détaillé", language: "Langue", system_default: "Par défaut du système", check_for_updates: "Vérifier les mises à jour", diff --git a/src/localization/german.rs b/src/localization/german.rs index 5c7bb23..d86dc2c 100644 --- a/src/localization/german.rs +++ b/src/localization/german.rs @@ -16,6 +16,7 @@ pub(super) const STRINGS: Strings = Strings { settings: "Einstellungen", start_with_windows: "Mit Windows starten", reset_position: "Position zurücksetzen", + show_detailed_remaining: "Detaillierte Restzeit anzeigen", language: "Sprache", system_default: "Systemstandard", check_for_updates: "Nach Updates suchen", diff --git a/src/localization/japanese.rs b/src/localization/japanese.rs index 0020018..3b7b269 100644 --- a/src/localization/japanese.rs +++ b/src/localization/japanese.rs @@ -16,6 +16,7 @@ pub(super) const STRINGS: Strings = Strings { settings: "設定", start_with_windows: "Windows と同時に開始", reset_position: "位置をリセット", + show_detailed_remaining: "残り時間を詳細表示", language: "言語", system_default: "システム既定", check_for_updates: "更新を確認", diff --git a/src/localization/korean.rs b/src/localization/korean.rs index 59e3829..0fc9bb0 100644 --- a/src/localization/korean.rs +++ b/src/localization/korean.rs @@ -16,6 +16,7 @@ pub(super) const STRINGS: Strings = Strings { settings: "설정", start_with_windows: "Windows 시작 시 자동 실행", reset_position: "위치 초기화", + show_detailed_remaining: "남은 시간 상세 표시", language: "언어", system_default: "시스템 기본값", check_for_updates: "업데이트 확인", diff --git a/src/localization/mod.rs b/src/localization/mod.rs index 146b419..27d5cec 100644 --- a/src/localization/mod.rs +++ b/src/localization/mod.rs @@ -134,6 +134,7 @@ pub struct Strings { pub settings: &'static str, pub start_with_windows: &'static str, pub reset_position: &'static str, + pub show_detailed_remaining: &'static str, pub language: &'static str, pub system_default: &'static str, pub check_for_updates: &'static str, diff --git a/src/localization/spanish.rs b/src/localization/spanish.rs index 8e6513e..0538516 100644 --- a/src/localization/spanish.rs +++ b/src/localization/spanish.rs @@ -16,6 +16,7 @@ pub(super) const STRINGS: Strings = Strings { settings: "Configuración", start_with_windows: "Iniciar con Windows", reset_position: "Restablecer posición", + show_detailed_remaining: "Mostrar tiempo restante detallado", language: "Idioma", system_default: "Predeterminado del sistema", check_for_updates: "Buscar actualizaciones", diff --git a/src/localization/traditional_chinese.rs b/src/localization/traditional_chinese.rs index 809ebba..4f74537 100644 --- a/src/localization/traditional_chinese.rs +++ b/src/localization/traditional_chinese.rs @@ -16,6 +16,7 @@ pub(super) const STRINGS: Strings = Strings { settings: "設定", start_with_windows: "開機時啟動", reset_position: "重置位置", + show_detailed_remaining: "顯示詳細剩餘時間", language: "語言", system_default: "系統預設", check_for_updates: "檢查更新", From 7180fac8cd1476a7b1e8689332ee1649df3435ce Mon Sep 17 00:00:00 2001 From: "Carlos Miguel C. Resurreccion" Date: Fri, 15 May 2026 15:09:01 +0800 Subject: [PATCH 2/4] refactor: thread detailed flag through countdown formatter Adds a `detailed: bool` parameter to `format_line`, `format_countdown`, `format_countdown_from_secs`, `time_until_display_change`, and `time_until_display_change_from_secs`. When `true`, sub-units are surfaced (Xh Ym for hours, Xd Yh for days) and the display-change tick fires on minute/hour boundaries to match. All callers pass `false`, so behavior is unchanged. The toggle that flips this is added in a later commit. --- src/poller.rs | 54 +++++++++++++++++++++++++++++++++++++++------------ src/window.rs | 16 +++++++-------- 2 files changed, 50 insertions(+), 20 deletions(-) diff --git a/src/poller.rs b/src/poller.rs index 5bd02bc..d379722 100644 --- a/src/poller.rs +++ b/src/poller.rs @@ -1021,9 +1021,9 @@ fn is_leap(y: u64) -> bool { } /// Format a usage section as "X% · Yh" style text -pub fn format_line(section: &UsageSection, strings: Strings) -> String { +pub fn format_line(section: &UsageSection, strings: Strings, detailed: bool) -> String { let pct = format!("{:.0}%", section.percentage); - let cd = format_countdown(section.resets_at, strings); + let cd = format_countdown(section.resets_at, strings, detailed); if cd.is_empty() { pct } else { @@ -1031,7 +1031,7 @@ pub fn format_line(section: &UsageSection, strings: Strings) -> String { } } -fn format_countdown(resets_at: Option, strings: Strings) -> String { +fn format_countdown(resets_at: Option, strings: Strings, detailed: bool) -> String { let reset = match resets_at { Some(t) => t, None => return String::new(), @@ -1042,25 +1042,47 @@ fn format_countdown(resets_at: Option, strings: Strings) -> String { Err(_) => return strings.now.to_string(), }; - format_countdown_from_secs(remaining.as_secs(), strings) + format_countdown_from_secs(remaining.as_secs(), strings, detailed) } /// Calculate how long until the display text would change -pub fn time_until_display_change(resets_at: Option) -> Option { +pub fn time_until_display_change( + resets_at: Option, + detailed: bool, +) -> Option { let reset = resets_at?; let remaining = reset.duration_since(SystemTime::now()).ok()?; - Some(time_until_display_change_from_secs(remaining.as_secs())) + Some(time_until_display_change_from_secs( + remaining.as_secs(), + detailed, + )) } -fn format_countdown_from_secs(total_secs: u64, strings: Strings) -> String { +fn format_countdown_from_secs(total_secs: u64, strings: Strings, detailed: bool) -> String { let total_mins = total_secs / 60; let total_hours = total_secs / 3600; let total_days = total_secs / 86400; if total_days >= 1 { - format!("{total_days}{}", strings.day_suffix) + if detailed { + let hours = total_hours % 24; + format!( + "{total_days}{} {hours}{}", + strings.day_suffix, strings.hour_suffix + ) + } else { + format!("{total_days}{}", strings.day_suffix) + } } else if total_hours >= 1 { - format!("{total_hours}{}", strings.hour_suffix) + if detailed { + let mins = total_mins % 60; + format!( + "{total_hours}{} {mins}{}", + strings.hour_suffix, strings.minute_suffix + ) + } else { + format!("{total_hours}{}", strings.hour_suffix) + } } else if total_mins >= 1 { format!("{total_mins}{}", strings.minute_suffix) } else { @@ -1068,15 +1090,23 @@ fn format_countdown_from_secs(total_secs: u64, strings: Strings) -> String { } } -fn time_until_display_change_from_secs(total_secs: u64) -> Duration { +fn time_until_display_change_from_secs(total_secs: u64, detailed: bool) -> Duration { let total_mins = total_secs / 60; let total_hours = total_secs / 3600; let total_days = total_secs / 86400; let current_bucket_start = if total_days >= 1 { - total_days * 86400 + if detailed { + total_hours * 3600 + } else { + total_days * 86400 + } } else if total_hours >= 1 { - total_hours * 3600 + if detailed { + total_mins * 60 + } else { + total_hours * 3600 + } } else if total_mins >= 1 { total_mins * 60 } else { diff --git a/src/window.rs b/src/window.rs index 31955f5..246dd0a 100644 --- a/src/window.rs +++ b/src/window.rs @@ -418,16 +418,16 @@ fn refresh_usage_texts(state: &mut AppState) { }; if let Some(claude_code) = data.claude_code.as_ref() { - state.session_text = poller::format_line(&claude_code.session, strings); - state.weekly_text = poller::format_line(&claude_code.weekly, strings); + state.session_text = poller::format_line(&claude_code.session, strings, false); + state.weekly_text = poller::format_line(&claude_code.weekly, strings, false); } else if state.show_claude_code { state.session_text = "!".to_string(); state.weekly_text = "!".to_string(); } if let Some(codex) = data.codex.as_ref() { - state.codex_session_text = poller::format_line(&codex.session, strings); - state.codex_weekly_text = poller::format_line(&codex.weekly, strings); + state.codex_session_text = poller::format_line(&codex.session, strings, false); + state.codex_weekly_text = poller::format_line(&codex.weekly, strings, false); } else if state.show_codex { state.codex_session_text = "!".to_string(); state.codex_weekly_text = "!".to_string(); @@ -1632,16 +1632,16 @@ fn schedule_countdown_timer() { let delays = [ data.claude_code .as_ref() - .and_then(|usage| poller::time_until_display_change(usage.session.resets_at)), + .and_then(|usage| poller::time_until_display_change(usage.session.resets_at, false)), data.claude_code .as_ref() - .and_then(|usage| poller::time_until_display_change(usage.weekly.resets_at)), + .and_then(|usage| poller::time_until_display_change(usage.weekly.resets_at, false)), data.codex .as_ref() - .and_then(|usage| poller::time_until_display_change(usage.session.resets_at)), + .and_then(|usage| poller::time_until_display_change(usage.session.resets_at, false)), data.codex .as_ref() - .and_then(|usage| poller::time_until_display_change(usage.weekly.resets_at)), + .and_then(|usage| poller::time_until_display_change(usage.weekly.resets_at, false)), ]; let min_delay = delays.into_iter().flatten().min(); From 39a5e25572fec64cd3c04429c454cea957315b15 Mon Sep 17 00:00:00 2001 From: "Carlos Miguel C. Resurreccion" Date: Fri, 15 May 2026 15:10:54 +0800 Subject: [PATCH 3/4] refactor: thread detailed flag through widget width and rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `TEXT_WIDTH_DETAILED` (95 px, the narrowest value that fits the worst-case `100% · 23h 59m` weekly display) alongside the existing 62 px default, plus a `text_width_for(detailed)` helper. Threads a `detailed: bool` parameter through `total_widget_width_for`, `model_usage_width`, `draw_usage_bar`, `draw_row`, and `paint_content` so the widget can size and clip text appropriately. All callers pass `false`, so behavior is unchanged. The toggle that flips this is added in a later commit. --- src/window.rs | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/src/window.rs b/src/window.rs index 246dd0a..f55bdc6 100644 --- a/src/window.rs +++ b/src/window.rs @@ -816,6 +816,7 @@ const LABEL_WIDTH: i32 = 18; const LABEL_RIGHT_MARGIN: i32 = 10; const BAR_RIGHT_MARGIN: i32 = 4; const TEXT_WIDTH: i32 = 62; +const TEXT_WIDTH_DETAILED: i32 = 95; const MODEL_RIGHT_MARGIN: i32 = 5; const RIGHT_MARGIN: i32 = 1; const WIDGET_HEIGHT: i32 = 46; @@ -824,6 +825,14 @@ fn active_model_count(show_claude_code: bool, show_codex: bool) -> i32 { (show_claude_code as i32 + show_codex as i32).max(1) } +fn text_width_for(detailed: bool) -> i32 { + if detailed { + TEXT_WIDTH_DETAILED + } else { + TEXT_WIDTH + } +} + fn row_bar_segment_count(active_models: i32) -> i32 { if active_models > 1 { 5 @@ -832,11 +841,11 @@ fn row_bar_segment_count(active_models: i32) -> i32 { } } -fn total_widget_width_for(active_models: i32) -> i32 { +fn total_widget_width_for(active_models: i32, detailed: bool) -> i32 { let bar_segments = row_bar_segment_count(active_models); let model_width = (sc(SEGMENT_W) + sc(SEGMENT_GAP)) * bar_segments - sc(SEGMENT_GAP) + sc(BAR_RIGHT_MARGIN) - + sc(TEXT_WIDTH); + + sc(text_width_for(detailed)); sc(LEFT_DIVIDER_W) + sc(DIVIDER_RIGHT_MARGIN) @@ -848,7 +857,10 @@ 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), + false, + ) } fn total_widget_width() -> i32 { @@ -859,7 +871,7 @@ fn total_widget_width() -> i32 { .map(|s| active_model_count(s.show_claude_code, s.show_codex)) .unwrap_or(1) }; - total_widget_width_for(active_models) + total_widget_width_for(active_models, false) } fn claude_accent_color() -> Color { @@ -960,7 +972,7 @@ pub fn run() { WS_POPUP, 0, 0, - total_widget_width_for(initial_model_count), + total_widget_width_for(initial_model_count, false), sc(WIDGET_HEIGHT), HWND::default(), HMENU::default(), @@ -1260,6 +1272,7 @@ fn render_layered() { show_claude_code, show_codex, &codex_accent, + false, ); // Background pixels → alpha 1 (nearly invisible but still hittable for right-click). @@ -1330,6 +1343,7 @@ fn paint_content( show_claude_code: bool, show_codex: bool, codex_accent: &Color, + detailed: bool, ) { unsafe { let client_rect = RECT { @@ -1422,6 +1436,7 @@ fn paint_content( accent, codex_accent, track, + detailed, ); draw_row( hdc, @@ -1439,6 +1454,7 @@ fn paint_content( accent, codex_accent, track, + detailed, ); SelectObject(hdc, old_font); @@ -2651,6 +2667,7 @@ fn paint(hdc: HDC, hwnd: HWND) { show_claude_code, show_codex, &codex_accent, + false, ); let _ = BitBlt(hdc, 0, 0, width, height, mem_dc, 0, 0, SRCCOPY); @@ -2677,6 +2694,7 @@ fn draw_row( claude_accent: &Color, codex_accent: &Color, track: &Color, + detailed: bool, ) { let seg_h = sc(SEGMENT_H); let active_models = active_model_count(show_claude_code, show_codex); @@ -2721,8 +2739,9 @@ fn draw_row( claude_accent, track, &claude_value_color, + detailed, ); - model_x += model_usage_width(segment_count) + sc(MODEL_RIGHT_MARGIN); + model_x += model_usage_width(segment_count, detailed) + sc(MODEL_RIGHT_MARGIN); } if show_codex { draw_usage_bar( @@ -2735,15 +2754,16 @@ fn draw_row( codex_accent, track, &codex_value_color, + detailed, ); } } } -fn model_usage_width(segment_count: i32) -> i32 { +fn model_usage_width(segment_count: i32, detailed: bool) -> i32 { (sc(SEGMENT_W) + sc(SEGMENT_GAP)) * segment_count - sc(SEGMENT_GAP) + sc(BAR_RIGHT_MARGIN) - + sc(TEXT_WIDTH) + + sc(text_width_for(detailed)) } fn draw_usage_bar( @@ -2756,6 +2776,7 @@ fn draw_usage_bar( accent: &Color, track: &Color, text_color: &Color, + detailed: bool, ) { let seg_w = sc(SEGMENT_W); let seg_h = sc(SEGMENT_H); @@ -2816,7 +2837,7 @@ fn draw_usage_bar( let mut text_rect = RECT { left: text_x, top: y, - right: text_x + sc(TEXT_WIDTH), + right: text_x + sc(text_width_for(detailed)), bottom: y + seg_h, }; let _ = SetTextColor(hdc, COLORREF(text_color.to_colorref())); From 46d3712a00a6df19ece36664b071cea9514a8036 Mon Sep 17 00:00:00 2001 From: "Carlos Miguel C. Resurreccion" Date: Fri, 15 May 2026 15:14:17 +0800 Subject: [PATCH 4/4] feat: add Settings toggle to show detailed remaining time Adds a new "Show detailed remaining time" entry under the right-click Settings submenu, off by default. When on, the 5h session window shows minutes alongside hours (e.g. "4h 12m") and the 7d weekly window shows hours alongside days (e.g. "3d 5h"). The widget grows to TEXT_WIDTH_DETAILED (95 px per row) while toggled, and the countdown timer repaints on the finer-unit boundary so the displayed value is never stale. The preference is persisted in settings.json as `show_detailed_remaining` and is loaded on startup. Existing settings.json files without the field deserialize to false. --- README.md | 1 + src/window.rs | 79 ++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 64 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 08c8a28..eb20097 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ Once running, it will appear in your taskbar and as one or more tray icons in th - Right-click the taskbar widget or tray icon for refresh, displayed models, update frequency, Start with Windows, reset position, language, updates, and exit - Left-click the tray icon to toggle the taskbar widget on or off - Enable `Start with Windows` from the right-click menu if you want it to launch automatically when you sign in +- Enable `Show detailed remaining time` under right-click `Settings` to add minutes alongside hours (5h window) and hours alongside days (7d window) ### Models diff --git a/src/window.rs b/src/window.rs index f55bdc6..9a7ee5f 100644 --- a/src/window.rs +++ b/src/window.rs @@ -65,6 +65,7 @@ struct AppState { codex_weekly_text: String, show_claude_code: bool, show_codex: bool, + show_detailed_remaining: bool, data: Option, @@ -121,6 +122,7 @@ const IDM_LANG_KOREAN: u16 = 47; const IDM_LANG_TRADITIONAL_CHINESE: u16 = 48; const IDM_MODEL_CLAUDE_CODE: u16 = 60; const IDM_MODEL_CODEX: u16 = 61; +const IDM_SHOW_DETAILED_REMAINING: u16 = 70; const DIVIDER_HIT_ZONE: i32 = 13; // LEFT_DIVIDER_W + DIVIDER_RIGHT_MARGIN @@ -213,6 +215,8 @@ struct SettingsFile { show_claude_code: bool, #[serde(default = "default_show_codex")] show_codex: bool, + #[serde(default)] + show_detailed_remaining: bool, } impl Default for SettingsFile { @@ -225,6 +229,7 @@ impl Default for SettingsFile { widget_visible: true, show_claude_code: true, show_codex: false, + show_detailed_remaining: false, } } } @@ -280,6 +285,7 @@ fn save_state_settings() { widget_visible: s.widget_visible, show_claude_code: s.show_claude_code, show_codex: s.show_codex, + show_detailed_remaining: s.show_detailed_remaining, }); } } @@ -413,21 +419,22 @@ fn refresh_usage_texts(state: &mut AppState) { } let strings = state.language.strings(); + let detailed = state.show_detailed_remaining; let Some(data) = state.data.as_ref() else { return; }; if let Some(claude_code) = data.claude_code.as_ref() { - state.session_text = poller::format_line(&claude_code.session, strings, false); - state.weekly_text = poller::format_line(&claude_code.weekly, strings, false); + state.session_text = poller::format_line(&claude_code.session, strings, detailed); + state.weekly_text = poller::format_line(&claude_code.weekly, strings, detailed); } else if state.show_claude_code { state.session_text = "!".to_string(); state.weekly_text = "!".to_string(); } if let Some(codex) = data.codex.as_ref() { - state.codex_session_text = poller::format_line(&codex.session, strings, false); - state.codex_weekly_text = poller::format_line(&codex.weekly, strings, false); + state.codex_session_text = poller::format_line(&codex.session, strings, detailed); + state.codex_weekly_text = poller::format_line(&codex.weekly, strings, detailed); } else if state.show_codex { state.codex_session_text = "!".to_string(); state.codex_weekly_text = "!".to_string(); @@ -859,19 +866,24 @@ fn total_widget_width_for(active_models: i32, detailed: bool) -> i32 { fn total_widget_width_for_state(state: &AppState) -> i32 { total_widget_width_for( active_model_count(state.show_claude_code, state.show_codex), - false, + state.show_detailed_remaining, ) } fn total_widget_width() -> i32 { - let active_models = { + let (active_models, detailed) = { let state = lock_state(); state .as_ref() - .map(|s| active_model_count(s.show_claude_code, s.show_codex)) - .unwrap_or(1) + .map(|s| { + ( + active_model_count(s.show_claude_code, s.show_codex), + s.show_detailed_remaining, + ) + }) + .unwrap_or((1, false)) }; - total_widget_width_for(active_models, false) + total_widget_width_for(active_models, detailed) } fn claude_accent_color() -> Color { @@ -972,7 +984,7 @@ pub fn run() { WS_POPUP, 0, 0, - total_widget_width_for(initial_model_count, false), + total_widget_width_for(initial_model_count, settings.show_detailed_remaining), sc(WIDGET_HEIGHT), HWND::default(), HMENU::default(), @@ -1025,6 +1037,7 @@ pub fn run() { codex_weekly_text: "--".to_string(), show_claude_code: settings.show_claude_code, show_codex: settings.show_codex, + show_detailed_remaining: settings.show_detailed_remaining, data: None, poll_interval_ms: settings.poll_interval_ms, retry_count: 0, @@ -1164,6 +1177,7 @@ fn render_layered() { codex_weekly_text, show_claude_code, show_codex, + show_detailed_remaining, ) = { let state = lock_state(); match state.as_ref() { @@ -1182,6 +1196,7 @@ fn render_layered() { s.codex_weekly_text.clone(), s.show_claude_code, s.show_codex, + s.show_detailed_remaining, ), None => return, } @@ -1272,7 +1287,7 @@ fn render_layered() { show_claude_code, show_codex, &codex_accent, - false, + show_detailed_remaining, ); // Background pixels → alpha 1 (nearly invisible but still hittable for right-click). @@ -1645,19 +1660,20 @@ fn schedule_countdown_timer() { } } + let detailed = s.show_detailed_remaining; let delays = [ data.claude_code .as_ref() - .and_then(|usage| poller::time_until_display_change(usage.session.resets_at, false)), + .and_then(|usage| poller::time_until_display_change(usage.session.resets_at, detailed)), data.claude_code .as_ref() - .and_then(|usage| poller::time_until_display_change(usage.weekly.resets_at, false)), + .and_then(|usage| poller::time_until_display_change(usage.weekly.resets_at, detailed)), data.codex .as_ref() - .and_then(|usage| poller::time_until_display_change(usage.session.resets_at, false)), + .and_then(|usage| poller::time_until_display_change(usage.session.resets_at, detailed)), data.codex .as_ref() - .and_then(|usage| poller::time_until_display_change(usage.weekly.resets_at, false)), + .and_then(|usage| poller::time_until_display_change(usage.weekly.resets_at, detailed)), ]; let min_delay = delays.into_iter().flatten().min(); @@ -2234,6 +2250,19 @@ unsafe extern "system" fn wnd_proc( // Reset the poll timer with the new interval SetTimer(hwnd, TIMER_POLL, new_interval, None); } + IDM_SHOW_DETAILED_REMAINING => { + { + let mut state = lock_state(); + if let Some(s) = state.as_mut() { + s.show_detailed_remaining = !s.show_detailed_remaining; + refresh_usage_texts(s); + } + } + save_state_settings(); + position_at_taskbar(); + render_layered(); + schedule_countdown_timer(); + } IDM_MODEL_CLAUDE_CODE | IDM_MODEL_CODEX => { { let mut state = lock_state(); @@ -2343,6 +2372,7 @@ fn show_context_menu(hwnd: HWND) { widget_visible, show_claude_code, show_codex, + show_detailed_remaining, ) = { let state = lock_state(); match state.as_ref() { @@ -2356,6 +2386,7 @@ fn show_context_menu(hwnd: HWND) { s.widget_visible, s.show_claude_code, s.show_codex, + s.show_detailed_remaining, ), None => ( POLL_15_MIN, @@ -2367,6 +2398,7 @@ fn show_context_menu(hwnd: HWND) { true, true, false, + false, ), } }; @@ -2472,6 +2504,19 @@ fn show_context_menu(hwnd: HWND) { PCWSTR::from_raw(reset_pos_str.as_ptr()), ); + let detailed_str = native_interop::wide_str(strings.show_detailed_remaining); + let detailed_flags = if show_detailed_remaining { + MF_CHECKED + } else { + MENU_ITEM_FLAGS(0) + }; + let _ = AppendMenuW( + settings_menu, + detailed_flags, + IDM_SHOW_DETAILED_REMAINING as usize, + PCWSTR::from_raw(detailed_str.as_ptr()), + ); + let language_menu = CreatePopupMenu().unwrap(); let system_label = native_interop::wide_str(strings.system_default); let system_flags = if language_override.is_none() { @@ -2593,6 +2638,7 @@ fn paint(hdc: HDC, hwnd: HWND) { codex_weekly_text, show_claude_code, show_codex, + show_detailed_remaining, ) = { let state = lock_state(); match state.as_ref() { @@ -2609,6 +2655,7 @@ fn paint(hdc: HDC, hwnd: HWND) { s.codex_weekly_text.clone(), s.show_claude_code, s.show_codex, + s.show_detailed_remaining, ), None => return, } @@ -2667,7 +2714,7 @@ fn paint(hdc: HDC, hwnd: HWND) { show_claude_code, show_codex, &codex_accent, - false, + show_detailed_remaining, ); let _ = BitBlt(hdc, 0, 0, width, height, mem_dc, 0, 0, SRCCOPY);