From 5c5b4d6c231304f0750387d75fa682bab026b90f Mon Sep 17 00:00:00 2001 From: Ali Date: Wed, 24 Jun 2026 17:19:40 +0300 Subject: [PATCH 1/2] add reset clock display toggle --- Cargo.toml | 1 + src/poller.rs | 75 ++++++++++++++++++++++++++++++-- src/window.rs | 117 +++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 184 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9cbc1c6..c193bf9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ features = [ "Win32_Globalization", "Win32_Graphics_Gdi", "Win32_System_LibraryLoader", + "Win32_System_Time", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging", "Win32_UI_Accessibility", diff --git a/src/poller.rs b/src/poller.rs index a29cd0d..8ba9b27 100644 --- a/src/poller.rs +++ b/src/poller.rs @@ -6,8 +6,10 @@ use std::path::PathBuf; use std::process::Command; use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use std::os::windows::process::CommandExt; +use windows::Win32::Foundation::{FILETIME, SYSTEMTIME}; +use windows::Win32::System::Time::{FileTimeToSystemTime, SystemTimeToTzSpecificLocalTime}; use crate::diagnose; use crate::localization::Strings; @@ -43,6 +45,23 @@ pub enum CredentialWatchMode { pub type CredentialWatchSnapshot = Vec; +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum ResetTimeDisplay { + #[default] + Relative, + Clock, +} + +impl ResetTimeDisplay { + pub fn toggled(self) -> Self { + match self { + Self::Relative => Self::Clock, + Self::Clock => Self::Relative, + } + } +} + #[derive(Deserialize)] struct UsageResponse { five_hour: Option, @@ -1522,9 +1541,13 @@ 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, + reset_time_display: ResetTimeDisplay, +) -> String { let pct = format!("{:.0}%", section.percentage); - let cd = format_countdown(section.resets_at, strings); + let cd = format_reset_time(section.resets_at, strings, reset_time_display); if cd.is_empty() { pct } else { @@ -1532,6 +1555,18 @@ pub fn format_line(section: &UsageSection, strings: Strings) -> String { } } +fn format_reset_time( + resets_at: Option, + strings: Strings, + reset_time_display: ResetTimeDisplay, +) -> String { + match reset_time_display { + ResetTimeDisplay::Relative => format_countdown(resets_at, strings), + ResetTimeDisplay::Clock => format_clock_time(resets_at) + .unwrap_or_else(|| format_countdown(resets_at, strings)), + } +} + fn format_countdown(resets_at: Option, strings: Strings) -> String { let reset = match resets_at { Some(t) => t, @@ -1546,6 +1581,40 @@ fn format_countdown(resets_at: Option, strings: Strings) -> String { format_countdown_from_secs(remaining.as_secs(), strings) } +fn format_clock_time(resets_at: Option) -> Option { + let reset = resets_at?; + reset.duration_since(SystemTime::now()).ok()?; + + let local_time = local_system_time(reset)?; + Some(format!("{}:{:02}", local_time.wHour, local_time.wMinute)) +} + +fn local_system_time(time: SystemTime) -> Option { + const WINDOWS_EPOCH_OFFSET_SECS: u64 = 11_644_473_600; + const WINDOWS_TICKS_PER_SEC: u64 = 10_000_000; + + let duration = time.duration_since(UNIX_EPOCH).ok()?; + let ticks = duration + .as_secs() + .checked_add(WINDOWS_EPOCH_OFFSET_SECS)? + .checked_mul(WINDOWS_TICKS_PER_SEC)? + .checked_add(u64::from(duration.subsec_nanos() / 100))?; + + let file_time = FILETIME { + dwLowDateTime: ticks as u32, + dwHighDateTime: (ticks >> 32) as u32, + }; + + let mut utc_time = SYSTEMTIME::default(); + let mut local_time = SYSTEMTIME::default(); + unsafe { + FileTimeToSystemTime(&file_time, &mut utc_time).ok()?; + SystemTimeToTzSpecificLocalTime(None, &utc_time, &mut local_time).ok()?; + } + + Some(local_time) +} + /// Calculate how long until the display text would change pub fn time_until_display_change(resets_at: Option) -> Option { let reset = resets_at?; diff --git a/src/window.rs b/src/window.rs index f6d261e..130def2 100644 --- a/src/window.rs +++ b/src/window.rs @@ -70,6 +70,7 @@ struct AppState { show_claude_code: bool, show_codex: bool, show_antigravity: bool, + reset_time_display: poller::ResetTimeDisplay, data: Option, @@ -316,6 +317,8 @@ struct SettingsFile { show_codex: bool, #[serde(default = "default_show_antigravity")] show_antigravity: bool, + #[serde(default)] + reset_time_display: poller::ResetTimeDisplay, } impl Default for SettingsFile { @@ -330,6 +333,7 @@ impl Default for SettingsFile { show_claude_code: true, show_codex: false, show_antigravity: false, + reset_time_display: poller::ResetTimeDisplay::default(), } } } @@ -391,6 +395,7 @@ fn save_state_settings() { show_claude_code: s.show_claude_code, show_codex: s.show_codex, show_antigravity: s.show_antigravity, + reset_time_display: s.reset_time_display, }); } } @@ -640,33 +645,36 @@ fn refresh_usage_texts(state: &mut AppState) { } let strings = state.language.strings(); + let reset_time_display = state.reset_time_display; 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); - state.weekly_text = poller::format_line(&claude_code.weekly, strings); + state.session_text = + poller::format_line(&claude_code.session, strings, reset_time_display); + state.weekly_text = poller::format_line(&claude_code.weekly, strings, reset_time_display); } 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, reset_time_display); + state.codex_weekly_text = poller::format_line(&codex.weekly, strings, reset_time_display); } else if state.show_codex { state.codex_session_text = "!".to_string(); state.codex_weekly_text = "!".to_string(); } if let Some(antigravity) = data.antigravity.as_ref() { - state.antigravity_session_text = poller::format_line(&antigravity.session, strings); + state.antigravity_session_text = + poller::format_line(&antigravity.session, strings, reset_time_display); state.antigravity_weekly_text = if antigravity.weekly.resets_at.is_none() && antigravity.weekly.percentage == 0.0 { "--".to_string() } else { - poller::format_line(&antigravity.weekly, strings) + poller::format_line(&antigravity.weekly, strings, reset_time_display) }; } else if state.show_antigravity { state.antigravity_session_text = "!".to_string(); @@ -1079,6 +1087,74 @@ fn cursor_is_on_drag_handle(hwnd: HWND) -> bool { } } +fn cursor_is_on_reset_time_toggle(hwnd: HWND) -> bool { + unsafe { + let mut pt = POINT::default(); + if GetCursorPos(&mut pt).is_err() || !ScreenToClient(hwnd, &mut pt).as_bool() { + return false; + } + + let state = lock_state(); + state + .as_ref() + .is_some_and(|s| is_reset_time_toggle_point(s, pt.x, pt.y)) + } +} + +fn is_reset_time_toggle_point(state: &AppState, client_x: i32, client_y: i32) -> bool { + let height = sc(WIDGET_HEIGHT); + let content_x = sc(LEFT_DIVIDER_W) + sc(DIVIDER_RIGHT_MARGIN); + let row2_y = height - sc(5) - sc(SEGMENT_H); + let row1_y = row2_y - sc(10) - sc(SEGMENT_H); + let active_models = active_model_count( + state.show_claude_code, + state.show_codex, + state.show_antigravity, + ); + let segment_count = row_bar_segment_count(active_models); + + reset_text_hit_row(state, content_x, row1_y, segment_count, client_x, client_y) + || reset_text_hit_row(state, content_x, row2_y, segment_count, client_x, client_y) +} + +fn reset_text_hit_row( + state: &AppState, + x: i32, + y: i32, + segment_count: i32, + client_x: i32, + client_y: i32, +) -> bool { + if client_y < y || client_y >= y + sc(SEGMENT_H) { + return false; + } + + let mut model_x = x + sc(LABEL_WIDTH) + sc(LABEL_RIGHT_MARGIN); + for visible in [ + state.show_claude_code, + state.show_codex, + state.show_antigravity, + ] { + if visible { + if reset_text_hit_model(model_x, segment_count, client_x) { + return true; + } + model_x += model_usage_width(segment_count) + sc(MODEL_RIGHT_MARGIN); + } + } + + false +} + +fn reset_text_hit_model(bar_x: i32, segment_count: i32, client_x: i32) -> bool { + let text_x = bar_x + + segment_count * (sc(SEGMENT_W) + sc(SEGMENT_GAP)) + - sc(SEGMENT_GAP) + + sc(BAR_RIGHT_MARGIN); + + client_x >= text_x && client_x < text_x + sc(TEXT_WIDTH) +} + fn active_model_count(show_claude_code: bool, show_codex: bool, show_antigravity: bool) -> i32 { (show_claude_code as i32 + show_codex as i32 + show_antigravity as i32).max(1) } @@ -1310,6 +1386,7 @@ pub fn run() { show_claude_code: settings.show_claude_code, show_codex: settings.show_codex, show_antigravity: settings.show_antigravity, + reset_time_display: settings.reset_time_display, data: None, poll_interval_ms: settings.poll_interval_ms, retry_count: 0, @@ -2338,6 +2415,11 @@ unsafe extern "system" fn wnd_proc( SetCursor(cursor); return LRESULT(1); } + if cursor_is_on_reset_time_toggle(hwnd) { + let cursor = LoadCursorW(HINSTANCE::default(), IDC_HAND).unwrap_or_default(); + SetCursor(cursor); + return LRESULT(1); + } DefWindowProcW(hwnd, msg, wparam, lparam) } WM_LBUTTONDOWN => { @@ -2456,6 +2538,29 @@ unsafe extern "system" fn wnd_proc( LRESULT(0) } WM_LBUTTONUP => { + let client_x = (lparam.0 & 0xFFFF) as i16 as i32; + let client_y = ((lparam.0 >> 16) & 0xFFFF) as i16 as i32; + let toggled_reset_time = { + let mut state = lock_state(); + if let Some(s) = state.as_mut() { + if !s.dragging && is_reset_time_toggle_point(s, client_x, client_y) { + s.reset_time_display = s.reset_time_display.toggled(); + refresh_usage_texts(s); + true + } else { + false + } + } else { + false + } + }; + if toggled_reset_time { + save_state_settings(); + schedule_countdown_timer(); + render_layered(); + return LRESULT(0); + } + let mut pt = POINT::default(); let _ = GetCursorPos(&mut pt); let drag_result = { From ec5bb57ab4b568c6ce6a7e56c45417b66f90b6e1 Mon Sep 17 00:00:00 2001 From: Ali Date: Thu, 25 Jun 2026 09:46:54 +0300 Subject: [PATCH 2/2] format reset clock as twelve-hour time --- src/poller.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/poller.rs b/src/poller.rs index 8ba9b27..60c3eb7 100644 --- a/src/poller.rs +++ b/src/poller.rs @@ -1586,7 +1586,15 @@ fn format_clock_time(resets_at: Option) -> Option { reset.duration_since(SystemTime::now()).ok()?; let local_time = local_system_time(reset)?; - Some(format!("{}:{:02}", local_time.wHour, local_time.wMinute)) + Some(format_clock_time_parts(local_time.wHour, local_time.wMinute)) +} + +fn format_clock_time_parts(hour_24: u16, minute: u16) -> String { + let hour_12 = match hour_24 % 12 { + 0 => 12, + hour => hour, + }; + format!("{hour_12}:{minute:02}") } fn local_system_time(time: SystemTime) -> Option { @@ -1683,6 +1691,13 @@ mod tests { } } + #[test] + fn clock_display_uses_twelve_hour_time_without_suffix() { + assert_eq!(format_clock_time_parts(14, 20), "2:20"); + assert_eq!(format_clock_time_parts(0, 20), "12:20"); + assert_eq!(format_clock_time_parts(12, 5), "12:05"); + } + #[test] fn claude_failure_does_not_block_codex_when_both_are_enabled() { let data = poll_with(