mirror of
https://github.com/CodeZeno/Claude-Code-Usage-Monitor.git
synced 2026-06-20 10:19:27 +08:00
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.
This commit is contained in:
parent
20c44dbb9e
commit
ddee8d9f9b
171
src/window.rs
171
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<AppUsageData>,
|
||||
|
||||
@ -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<u64>,
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user