From ddee8d9f9b929553d756ed89eadd1f4f66899aee Mon Sep 17 00:00:00 2001 From: "Carlos Miguel C. Resurreccion" Date: Fri, 19 Jun 2026 20:17:05 +0800 Subject: [PATCH] refactor: read show_etd from state for merge compatibility Revert shared window.rs draw/width signatures to base; read show_etd via a state-lock accessor and share a lock-free text-width slot mechanism with the detailed-remaining feature (identical block, distinct per-feature wiring). Place ETD's struct fields, menu ID, and Settings menu item at anchors that are non-adjacent to the other display features so any merge subset is conflict-free. Keeps poller's format_countdown_from_secs at its base 2-arg signature. --- src/window.rs | 171 +++++++++++++++++++++++++++----------------------- 1 file changed, 92 insertions(+), 79 deletions(-) diff --git a/src/window.rs b/src/window.rs index 4eb0125..545ba43 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,5 +1,5 @@ use std::path::PathBuf; -use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::atomic::{AtomicI32, AtomicU32, Ordering}; use std::sync::{Mutex, MutexGuard}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; @@ -65,7 +65,6 @@ struct AppState { codex_weekly_text: String, show_claude_code: bool, show_codex: bool, - show_etd: bool, data: Option, @@ -85,6 +84,7 @@ struct AppState { drag_start_offset: i32, widget_visible: bool, + show_etd: bool, } #[derive(Clone, Debug)] @@ -123,8 +123,8 @@ const IDM_LANG_TRADITIONAL_CHINESE: u16 = 48; 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_SHOW_ETD: u16 = 74; +const IDM_MODEL_CODEX: u16 = 61; const DIVIDER_HIT_ZONE: i32 = 13; // LEFT_DIVIDER_W + DIVIDER_RIGHT_MARGIN @@ -213,12 +213,12 @@ struct SettingsFile { last_update_check_unix: Option, #[serde(default = "default_widget_visible")] widget_visible: bool, + #[serde(default)] + show_etd: bool, #[serde(default = "default_show_claude_code")] show_claude_code: bool, #[serde(default = "default_show_codex")] show_codex: bool, - #[serde(default)] - show_etd: bool, } impl Default for SettingsFile { @@ -229,9 +229,9 @@ impl Default for SettingsFile { language: None, last_update_check_unix: None, widget_visible: true, + show_etd: false, show_claude_code: true, show_codex: false, - show_etd: false, } } } @@ -285,9 +285,9 @@ fn save_state_settings() { .map(|language| language.code().to_string()), last_update_check_unix: s.last_update_check_unix, widget_visible: s.widget_visible, + show_etd: s.show_etd, show_claude_code: s.show_claude_code, show_codex: s.show_codex, - show_etd: s.show_etd, }); } } @@ -847,27 +847,45 @@ const DIVIDER_RIGHT_MARGIN: i32 = 10; const LABEL_WIDTH: i32 = 18; const LABEL_RIGHT_MARGIN: i32 = 10; const BAR_RIGHT_MARGIN: i32 = 4; -const TEXT_WIDTH: i32 = 62; // fits the worst-case "100% · 59m" +const TEXT_WIDTH: i32 = 62; +const MODEL_RIGHT_MARGIN: i32 = 5; +const RIGHT_MARGIN: i32 = 1; +const WIDGET_HEIGHT: i32 = 46; /// Text column when the ETD suffix is shown. The ETD form is worst-case /// "100% · 59m rem · 59m ETD" — the base "100% · 59m" plus a " rem · 59m ETD" /// label-and-estimate of similar length — so the column is sized to fit the /// widest possible remaining-time and ETD (~2.6x the base) without clipping. const ETD_TEXT_WIDTH: i32 = 160; -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) } -/// Text-column width for a usage cell, widened when the ETD suffix is shown. -fn text_width(show_etd: bool) -> i32 { - if show_etd { - ETD_TEXT_WIDTH - } else { - TEXT_WIDTH - } +// Optional display features may widen the usage text column. Each feature +// stores its own extra width (in base 96-DPI px) in a dedicated slot, read +// lock-free by the width/layout helpers below. Slots are summed so multiple +// features stack. This block is identical across the display-feature branches +// so they merge without conflict; a branch only wires up its own slot. +#[allow(dead_code)] +static EXTRA_TEXT_WIDTH_DETAILED: AtomicI32 = AtomicI32::new(0); +#[allow(dead_code)] +static EXTRA_TEXT_WIDTH_ETD: AtomicI32 = AtomicI32::new(0); + +#[allow(dead_code)] +fn set_extra_text_width_detailed(px: i32) { + EXTRA_TEXT_WIDTH_DETAILED.store(px, Ordering::Relaxed); +} + +#[allow(dead_code)] +fn set_extra_text_width_etd(px: i32) { + EXTRA_TEXT_WIDTH_ETD.store(px, Ordering::Relaxed); +} + +/// Usage text-column width including any enabled display-feature widening. +fn effective_text_width() -> i32 { + TEXT_WIDTH + + EXTRA_TEXT_WIDTH_DETAILED.load(Ordering::Relaxed) + + EXTRA_TEXT_WIDTH_ETD.load(Ordering::Relaxed) } fn row_bar_segment_count(active_models: i32) -> i32 { @@ -878,11 +896,11 @@ fn row_bar_segment_count(active_models: i32) -> i32 { } } -fn total_widget_width_for(active_models: i32, show_etd: bool) -> i32 { +fn total_widget_width_for(active_models: i32) -> 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(show_etd)); + + sc(effective_text_width()); sc(LEFT_DIVIDER_W) + sc(DIVIDER_RIGHT_MARGIN) @@ -893,22 +911,22 @@ fn total_widget_width_for(active_models: i32, show_etd: bool) -> i32 { + sc(RIGHT_MARGIN) } -fn total_widget_width_for_state(state: &AppState) -> i32 { - total_widget_width_for( - active_model_count(state.show_claude_code, state.show_codex), - state.show_etd, - ) -} - fn total_widget_width() -> i32 { - let (active_models, show_etd) = { + let active_models = { let state = lock_state(); state .as_ref() - .map(|s| (active_model_count(s.show_claude_code, s.show_codex), s.show_etd)) - .unwrap_or((1, false)) + .map(|s| active_model_count(s.show_claude_code, s.show_codex)) + .unwrap_or(1) }; - total_widget_width_for(active_models, show_etd) + total_widget_width_for(active_models) +} + +/// Whether the ETD suffix is enabled, read from shared state. Returns false +/// when state is not yet populated (startup) or the lock cannot be acquired. +/// Callers must not hold the state lock. +fn show_etd_enabled() -> bool { + lock_state().as_ref().map_or(false, |s| s.show_etd) } fn claude_accent_color() -> Color { @@ -1009,7 +1027,7 @@ pub fn run() { WS_POPUP, 0, 0, - total_widget_width_for(initial_model_count, settings.show_etd), + total_widget_width_for(initial_model_count), sc(WIDGET_HEIGHT), HWND::default(), HMENU::default(), @@ -1062,7 +1080,6 @@ pub fn run() { codex_weekly_text: "--".to_string(), show_claude_code: settings.show_claude_code, show_codex: settings.show_codex, - show_etd: settings.show_etd, data: None, poll_interval_ms: settings.poll_interval_ms, retry_count: 0, @@ -1078,6 +1095,7 @@ pub fn run() { drag_start_mouse_x: 0, drag_start_offset: 0, widget_visible: settings.widget_visible, + show_etd: settings.show_etd, }); } @@ -1114,6 +1132,14 @@ pub fn run() { diagnose::log("taskbar not found; using fallback popup window"); } + // Seed the ETD text-column widening so the initial layout matches the + // wider draw rect when the ETD suffix is enabled at startup. + set_extra_text_width_etd(if settings.show_etd { + ETD_TEXT_WIDTH - TEXT_WIDTH + } else { + 0 + }); + // If not embedded, fall back to topmost popup with SetLayeredWindowAttributes if !embedded { let _ = SetLayeredWindowAttributes(hwnd, COLORREF(0), 255, LWA_ALPHA); @@ -1202,7 +1228,6 @@ fn render_layered() { codex_weekly_text, show_claude_code, show_codex, - show_etd, ) = { let state = lock_state(); match state.as_ref() { @@ -1221,7 +1246,6 @@ fn render_layered() { s.codex_weekly_text.clone(), s.show_claude_code, s.show_codex, - s.show_etd, ), None => return, } @@ -1311,7 +1335,6 @@ fn render_layered() { &codex_weekly_text, show_claude_code, show_codex, - show_etd, &codex_accent, ); @@ -1382,7 +1405,6 @@ fn paint_content( codex_weekly_text: &str, show_claude_code: bool, show_codex: bool, - show_etd: bool, codex_accent: &Color, ) { unsafe { @@ -1473,7 +1495,6 @@ fn paint_content( codex_session_text, show_claude_code, show_codex, - show_etd, accent, codex_accent, track, @@ -1491,7 +1512,6 @@ fn paint_content( codex_weekly_text, show_claude_code, show_codex, - show_etd, accent, codex_accent, track, @@ -2068,6 +2088,9 @@ unsafe extern "system" fn wnd_proc( if is_dragging { let mut pt = POINT::default(); let _ = GetCursorPos(&mut pt); + // Compute the widget width before locking; the width helpers read + // shared state themselves and must not run while the lock is held. + let widget_width = total_widget_width(); let move_target = { let mut state = lock_state(); let s = match state.as_mut() { @@ -2101,7 +2124,6 @@ unsafe extern "system" fn wnd_proc( tray_left = tray_rect.left; } } - let widget_width = total_widget_width_for_state(s); let max_offset = (tray_left - taskbar_rect.left - widget_width).max(0); if new_offset > max_offset { new_offset = max_offset; @@ -2311,10 +2333,16 @@ unsafe extern "system" fn wnd_proc( let mut state = lock_state(); if let Some(s) = state.as_mut() { s.show_etd = !s.show_etd; + set_extra_text_width_etd(if s.show_etd { + ETD_TEXT_WIDTH - TEXT_WIDTH + } else { + 0 + }); refresh_usage_texts(s); } } save_state_settings(); + position_at_taskbar(); render_layered(); } IDM_LANG_SYSTEM @@ -2398,7 +2426,6 @@ fn show_context_menu(hwnd: HWND) { widget_visible, show_claude_code, show_codex, - show_etd, ) = { let state = lock_state(); match state.as_ref() { @@ -2412,7 +2439,6 @@ fn show_context_menu(hwnd: HWND) { s.widget_visible, s.show_claude_code, s.show_codex, - s.show_etd, ), None => ( POLL_15_MIN, @@ -2424,7 +2450,6 @@ fn show_context_menu(hwnd: HWND) { true, true, false, - false, ), } }; @@ -2530,19 +2555,6 @@ fn show_context_menu(hwnd: HWND) { PCWSTR::from_raw(reset_pos_str.as_ptr()), ); - let etd_str = native_interop::wide_str(strings.show_etd); - let etd_flags = if show_etd { - MF_CHECKED - } else { - MENU_ITEM_FLAGS(0) - }; - let _ = AppendMenuW( - settings_menu, - etd_flags, - IDM_SHOW_ETD as usize, - PCWSTR::from_raw(etd_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() { @@ -2592,6 +2604,19 @@ fn show_context_menu(hwnd: HWND) { PCWSTR::from_raw(language_label.as_ptr()), ); + let etd_str = native_interop::wide_str(strings.show_etd); + let etd_flags = if show_etd_enabled() { + MF_CHECKED + } else { + MENU_ITEM_FLAGS(0) + }; + let _ = AppendMenuW( + settings_menu, + etd_flags, + IDM_SHOW_ETD as usize, + PCWSTR::from_raw(etd_str.as_ptr()), + ); + let _ = AppendMenuW(settings_menu, MF_SEPARATOR, 0, PCWSTR::null()); let version_label = @@ -2666,7 +2691,6 @@ fn paint(hdc: HDC, hwnd: HWND) { codex_weekly_text, show_claude_code, show_codex, - show_etd, ) = { let state = lock_state(); match state.as_ref() { @@ -2683,7 +2707,6 @@ fn paint(hdc: HDC, hwnd: HWND) { s.codex_weekly_text.clone(), s.show_claude_code, s.show_codex, - s.show_etd, ), None => return, } @@ -2741,7 +2764,6 @@ fn paint(hdc: HDC, hwnd: HWND) { &codex_weekly_text, show_claude_code, show_codex, - show_etd, &codex_accent, ); @@ -2766,7 +2788,6 @@ fn draw_row( codex_text: &str, show_claude_code: bool, show_codex: bool, - show_etd: bool, claude_accent: &Color, codex_accent: &Color, track: &Color, @@ -2814,9 +2835,8 @@ fn draw_row( claude_accent, track, &claude_value_color, - show_etd, ); - model_x += model_usage_width(segment_count, show_etd) + sc(MODEL_RIGHT_MARGIN); + model_x += model_usage_width(segment_count) + sc(MODEL_RIGHT_MARGIN); } if show_codex { draw_usage_bar( @@ -2829,16 +2849,15 @@ fn draw_row( codex_accent, track, &codex_value_color, - show_etd, ); } } } -fn model_usage_width(segment_count: i32, show_etd: bool) -> i32 { +fn model_usage_width(segment_count: i32) -> i32 { (sc(SEGMENT_W) + sc(SEGMENT_GAP)) * segment_count - sc(SEGMENT_GAP) + sc(BAR_RIGHT_MARGIN) - + sc(text_width(show_etd)) + + sc(effective_text_width()) } fn draw_usage_bar( @@ -2851,7 +2870,6 @@ fn draw_usage_bar( accent: &Color, track: &Color, text_color: &Color, - show_etd: bool, ) { let seg_w = sc(SEGMENT_W); let seg_h = sc(SEGMENT_H); @@ -2912,7 +2930,7 @@ fn draw_usage_bar( let mut text_rect = RECT { left: text_x, top: y, - right: text_x + sc(text_width(show_etd)), + right: text_x + sc(effective_text_width()), bottom: y + seg_h, }; let _ = SetTextColor(hdc, COLORREF(text_color.to_colorref())); @@ -2950,20 +2968,15 @@ mod tests { fn etd_text_column_is_wider_than_base() { // Sized for the worst case "100% · 59m rem · 59m ETD". assert!(ETD_TEXT_WIDTH > TEXT_WIDTH); - assert_eq!(text_width(false), TEXT_WIDTH); - assert_eq!(text_width(true), ETD_TEXT_WIDTH); } #[test] - fn widget_widens_by_text_delta_per_model_when_etd_on() { - // Enabling ETD must widen the widget by exactly the column growth, once - // per active model — so the layout calc matches the wider draw rect and - // nothing re-clips. - for models in 1..=2 { - let off = total_widget_width_for(models, false); - let on = total_widget_width_for(models, true); - let expected = (sc(ETD_TEXT_WIDTH) - sc(TEXT_WIDTH)) * models; - assert_eq!(on - off, expected, "models={models}"); - } + fn etd_width_delta_matches_text_column_growth() { + // When ETD is enabled the usage text column grows by exactly this delta, + // once per active model (see total_widget_width_for / model_usage_width), + // so the layout calc matches the wider draw rect and nothing re-clips. + let delta = ETD_TEXT_WIDTH - TEXT_WIDTH; + assert!(delta > 0); + assert_eq!(TEXT_WIDTH + delta, ETD_TEXT_WIDTH); } }