From 5c5b4d6c231304f0750387d75fa682bab026b90f Mon Sep 17 00:00:00 2001 From: Ali Date: Wed, 24 Jun 2026 17:19:40 +0300 Subject: [PATCH 1/9] 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/9] 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( From 9437313b7adf70b068ab82ea54e90cdbff6a40d4 Mon Sep 17 00:00:00 2001 From: Ali Date: Tue, 14 Jul 2026 12:25:40 +0300 Subject: [PATCH 3/9] refactor widget layout and styling for improved visual consistency --- src/window.rs | 182 +++++++++++++++++++------------------------------- 1 file changed, 70 insertions(+), 112 deletions(-) diff --git a/src/window.rs b/src/window.rs index 130def2..ec315d3 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1051,23 +1051,34 @@ fn set_startup_enabled(enable: bool) { } } -// Dimensions matching the C# version +// Compact, taskbar-friendly layout. The bar width still scales down when +// multiple providers are visible, but the meter itself is rendered as one +// continuous pill rather than a row of blocks. const SEGMENT_W: i32 = 10; -const SEGMENT_H: i32 = 13; +const SEGMENT_H: i32 = 9; const SEGMENT_GAP: i32 = 1; const SEGMENT_COUNT: i32 = 10; -const CORNER_RADIUS: i32 = 2; +const CORNER_RADIUS: i32 = 5; +const ROW_GAP: i32 = 7; const LEFT_DIVIDER_W: i32 = 3; -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; +const DIVIDER_RIGHT_MARGIN: i32 = 12; +const LABEL_WIDTH: i32 = 20; +const LABEL_RIGHT_MARGIN: i32 = 8; +const BAR_RIGHT_MARGIN: i32 = 8; +const TEXT_WIDTH: i32 = 64; +const TEXT_HEIGHT: i32 = 15; const MODEL_RIGHT_MARGIN: i32 = 3; const RIGHT_MARGIN: i32 = 1; const WIDGET_HEIGHT: i32 = 46; +fn usage_row_positions(height: i32) -> (i32, i32) { + let rows_height = sc(SEGMENT_H) * 2 + sc(ROW_GAP); + let row1_y = (height - rows_height) / 2; + let row2_y = row1_y + sc(SEGMENT_H) + sc(ROW_GAP); + (row1_y, row2_y) +} + fn is_drag_handle_point(client_x: i32, client_y: i32) -> bool { let divider_h = sc(25); let divider_top = (sc(WIDGET_HEIGHT) - divider_h) / 2; @@ -1104,8 +1115,7 @@ fn cursor_is_on_reset_time_toggle(hwnd: HWND) -> bool { 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 (row1_y, row2_y) = usage_row_positions(height); let active_models = active_model_count( state.show_claude_code, state.show_codex, @@ -1125,7 +1135,8 @@ fn reset_text_hit_row( client_x: i32, client_y: i32, ) -> bool { - if client_y < y || client_y >= y + sc(SEGMENT_H) { + let text_top = y + (sc(SEGMENT_H) - sc(TEXT_HEIGHT)) / 2; + if client_y < text_top || client_y >= text_top + sc(TEXT_HEIGHT) { return false; } @@ -1202,7 +1213,7 @@ fn total_widget_width() -> i32 { } fn claude_accent_color() -> Color { - Color::from_hex("#D97757") + Color::from_hex("#E18463") } fn codex_accent_color(is_dark: bool) -> Color { @@ -1219,7 +1230,7 @@ fn antigravity_accent_color() -> Color { fn claude_usage_text_color(is_dark: bool) -> Color { if is_dark { - Color::from_hex("#F09A7A") + Color::from_hex("#F0A184") } else { Color::from_hex("#A94F32") } @@ -1557,14 +1568,14 @@ fn render_layered() { let codex_accent = codex_accent_color(is_dark); let antigravity_accent = antigravity_accent_color(); let track = if is_dark { - Color::from_hex("#444444") + Color::from_hex("#343434") } else { - Color::from_hex("#AAAAAA") + Color::from_hex("#D4D4D4") }; let text_color = if is_dark { - Color::from_hex("#888888") + Color::from_hex("#A6A6A6") } else { - Color::from_hex("#404040") + Color::from_hex("#505050") }; let bg_color = if is_dark { Color::from_hex("#1C1C1C") @@ -1721,46 +1732,26 @@ fn paint_content( FillRect(hdc, &client_rect, bg_brush); let _ = DeleteObject(bg_brush); - // Left divider - let divider_h = sc(25); + // Subtle pill handle: visible enough to discover dragging without + // competing with the usage information. + let divider_h = sc(26); let divider_top = (height - divider_h) / 2; let divider_bottom = divider_top + divider_h; - - let (div_left, div_right) = if is_dark { - ((80, 80, 80), (40, 40, 40)) + let divider_color = if is_dark { + Color::from_hex("#5B5B5B") } else { - ((160, 160, 160), (230, 230, 230)) + Color::from_hex("#A6A6A6") }; - - let left_brush = CreateSolidBrush(COLORREF(native_interop::colorref( - div_left.0, div_left.1, div_left.2, - ))); - let left_rect = RECT { + let divider_rect = RECT { left: 0, top: divider_top, right: sc(2), bottom: divider_bottom, }; - FillRect(hdc, &left_rect, left_brush); - let _ = DeleteObject(left_brush); - - let right_brush = CreateSolidBrush(COLORREF(native_interop::colorref( - div_right.0, - div_right.1, - div_right.2, - ))); - let right_rect = RECT { - left: sc(2), - top: divider_top, - right: sc(3), - bottom: divider_bottom, - }; - FillRect(hdc, &right_rect, right_brush); - let _ = DeleteObject(right_brush); + draw_rounded_rect(hdc, ÷r_rect, ÷r_color, sc(1)); 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 (row1_y, row2_y) = usage_row_positions(height); let _ = SetBkMode(hdc, TRANSPARENT); let _ = SetTextColor(hdc, COLORREF(text_color.to_colorref())); @@ -3122,14 +3113,14 @@ fn paint(hdc: HDC, hwnd: HWND) { let codex_accent = codex_accent_color(is_dark); let antigravity_accent = antigravity_accent_color(); let track = if is_dark { - Color::from_hex("#444444") + Color::from_hex("#343434") } else { - Color::from_hex("#AAAAAA") + Color::from_hex("#D4D4D4") }; let text_color = if is_dark { - Color::from_hex("#888888") + Color::from_hex("#A6A6A6") } else { - Color::from_hex("#404040") + Color::from_hex("#505050") }; let bg_color = if is_dark { Color::from_hex("#1C1C1C") @@ -3210,33 +3201,22 @@ fn draw_row( track: &Color, ) { let seg_h = sc(SEGMENT_H); + let text_h = sc(TEXT_HEIGHT); + let text_top = y + (seg_h - text_h) / 2; let active_models = active_model_count(show_claude_code, show_codex, show_antigravity); let segment_count = row_bar_segment_count(active_models); - let use_model_text_colors = active_models > 1; - let claude_value_color = if use_model_text_colors { - claude_usage_text_color(is_dark) - } else { - *text_color - }; - let codex_value_color = if use_model_text_colors { - codex_usage_text_color(is_dark) - } else { - *text_color - }; - let antigravity_value_color = if use_model_text_colors { - antigravity_usage_text_color(is_dark) - } else { - *text_color - }; + let claude_value_color = claude_usage_text_color(is_dark); + let codex_value_color = codex_usage_text_color(is_dark); + let antigravity_value_color = antigravity_usage_text_color(is_dark); unsafe { let _ = SetTextColor(hdc, COLORREF(text_color.to_colorref())); let mut label_wide: Vec = label.encode_utf16().collect(); let mut label_rect = RECT { left: x, - top: y, + top: text_top, right: x + sc(LABEL_WIDTH), - bottom: y + seg_h, + bottom: text_top + text_h, }; let _ = DrawTextW( hdc, @@ -3311,63 +3291,41 @@ fn draw_usage_bar( let seg_h = sc(SEGMENT_H); let seg_gap = sc(SEGMENT_GAP); let corner_r = sc(CORNER_RADIUS); + let bar_width = segment_count * (seg_w + seg_gap) - seg_gap; unsafe { let percent_clamped = percent.clamp(0.0, 100.0); - let segment_percent = 100.0 / segment_count as f64; - - for i in 0..segment_count { - let seg_x = bar_x + i * (seg_w + seg_gap); - let seg_start = (i as f64) * segment_percent; - let seg_end = seg_start + segment_percent; + let track_rect = RECT { + left: bar_x, + top: y, + right: bar_x + bar_width, + bottom: y + seg_h, + }; + draw_rounded_rect(hdc, &track_rect, track, corner_r); - let seg_rect = RECT { - left: seg_x, + let fill_width = + ((bar_width as f64 * percent_clamped / 100.0).round() as i32).clamp(0, bar_width); + if fill_width > 0 { + let fill_rect = RECT { + left: bar_x, top: y, - right: seg_x + seg_w, + right: bar_x + fill_width, bottom: y + seg_h, }; - - if percent_clamped >= seg_end { - draw_rounded_rect(hdc, &seg_rect, accent, corner_r); - } else if percent_clamped <= seg_start { - draw_rounded_rect(hdc, &seg_rect, track, corner_r); - } else { - draw_rounded_rect(hdc, &seg_rect, track, corner_r); - let fraction = (percent_clamped - seg_start) / segment_percent; - let fill_width = (seg_w as f64 * fraction) as i32; - if fill_width > 0 { - let fill_rect = RECT { - left: seg_x, - top: y, - right: seg_x + fill_width, - bottom: y + seg_h, - }; - let rgn = CreateRoundRectRgn( - seg_rect.left, - seg_rect.top, - seg_rect.right + 1, - seg_rect.bottom + 1, - corner_r * 2, - corner_r * 2, - ); - let _ = SelectClipRgn(hdc, rgn); - let brush = CreateSolidBrush(COLORREF(accent.to_colorref())); - FillRect(hdc, &fill_rect, brush); - let _ = DeleteObject(brush); - let _ = SelectClipRgn(hdc, HRGN::default()); - let _ = DeleteObject(rgn); - } - } + // Keep very small values circular and larger values fully pill-shaped. + let fill_radius = corner_r.min(fill_width / 2).max(1); + draw_rounded_rect(hdc, &fill_rect, accent, fill_radius); } - let text_x = bar_x + segment_count * (seg_w + seg_gap) - seg_gap + sc(BAR_RIGHT_MARGIN); + let text_x = bar_x + bar_width + sc(BAR_RIGHT_MARGIN); + let text_h = sc(TEXT_HEIGHT); + let text_top = y + (seg_h - text_h) / 2; let mut text_wide: Vec = text.encode_utf16().collect(); let mut text_rect = RECT { left: text_x, - top: y, + top: text_top, right: text_x + sc(TEXT_WIDTH), - bottom: y + seg_h, + bottom: text_top + text_h, }; let _ = SetTextColor(hdc, COLORREF(text_color.to_colorref())); let _ = DrawTextW( From 86086f710555f32dbe2944ba7f5b32fd3fcb9469 Mon Sep 17 00:00:00 2001 From: Ali Date: Tue, 14 Jul 2026 12:27:37 +0300 Subject: [PATCH 4/9] add hide button functionality to toggle widget visibility --- src/window.rs | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/window.rs b/src/window.rs index ec315d3..9c18e91 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1071,6 +1071,8 @@ const TEXT_HEIGHT: i32 = 15; const MODEL_RIGHT_MARGIN: i32 = 3; const RIGHT_MARGIN: i32 = 1; const WIDGET_HEIGHT: i32 = 46; +const HIDE_BTN_W: i32 = 16; +const HIDE_BTN_MARGIN: i32 = 8; fn usage_row_positions(height: i32) -> (i32, i32) { let rows_height = sc(SEGMENT_H) * 2 + sc(ROW_GAP); @@ -1157,6 +1159,36 @@ fn reset_text_hit_row( false } +fn hide_button_rect(width: i32, height: i32) -> RECT { + let btn_w = sc(HIDE_BTN_W); + let right = width - sc(RIGHT_MARGIN); + let left = right - btn_w; + let top = (height - btn_w) / 2; + RECT { + left, + top, + right, + bottom: top + btn_w, + } +} + +fn is_hide_button_point(width: i32, height: i32, client_x: i32, client_y: i32) -> bool { + let r = hide_button_rect(width, height); + client_x >= r.left && client_x < r.right && client_y >= r.top && client_y < r.bottom +} + +fn cursor_is_on_hide_button(hwnd: HWND) -> bool { + unsafe { + let mut pt = POINT::default(); + if GetCursorPos(&mut pt).is_err() || !ScreenToClient(hwnd, &mut pt).as_bool() { + return false; + } + let mut rect = RECT::default(); + let _ = GetClientRect(hwnd, &mut rect); + is_hide_button_point(rect.right, rect.bottom, pt.x, pt.y) + } +} + 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)) @@ -1190,6 +1222,8 @@ fn total_widget_width_for(active_models: i32) -> i32 { + sc(LABEL_RIGHT_MARGIN) + model_width * active_models + sc(MODEL_RIGHT_MARGIN) * (active_models - 1) + + sc(HIDE_BTN_MARGIN) + + sc(HIDE_BTN_W) + sc(RIGHT_MARGIN) } @@ -2411,6 +2445,11 @@ unsafe extern "system" fn wnd_proc( SetCursor(cursor); return LRESULT(1); } + if cursor_is_on_hide_button(hwnd) { + let cursor = LoadCursorW(HINSTANCE::default(), IDC_HAND).unwrap_or_default(); + SetCursor(cursor); + return LRESULT(1); + } DefWindowProcW(hwnd, msg, wparam, lparam) } WM_LBUTTONDOWN => { @@ -2531,6 +2570,18 @@ unsafe extern "system" fn wnd_proc( WM_LBUTTONUP => { let client_x = (lparam.0 & 0xFFFF) as i16 as i32; let client_y = ((lparam.0 >> 16) & 0xFFFF) as i16 as i32; + let is_dragging = { + let state = lock_state(); + state.as_ref().map(|s| s.dragging).unwrap_or(false) + }; + if !is_dragging { + let mut rect = RECT::default(); + let _ = GetClientRect(hwnd, &mut rect); + if is_hide_button_point(rect.right, rect.bottom, client_x, client_y) { + toggle_widget_visibility(hwnd); + return LRESULT(0); + } + } let toggled_reset_time = { let mut state = lock_state(); if let Some(s) = state.as_mut() { From 6ef2c0d80b6d02c887fb6d7281f8c1c4f2d8a3f8 Mon Sep 17 00:00:00 2001 From: Ali Date: Tue, 14 Jul 2026 12:28:00 +0300 Subject: [PATCH 5/9] add hide button glyph to paint content function --- src/window.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/window.rs b/src/window.rs index 9c18e91..9ff7a29 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1852,6 +1852,16 @@ fn paint_content( track, ); + let mut hide_rect = hide_button_rect(width, height); + let mut hide_glyph: Vec = "\u{2715}".encode_utf16().collect(); + let _ = SetTextColor(hdc, COLORREF(text_color.to_colorref())); + let _ = DrawTextW( + hdc, + &mut hide_glyph, + &mut hide_rect, + DT_CENTER | DT_VCENTER | DT_SINGLELINE, + ); + SelectObject(hdc, old_font); let _ = DeleteObject(font); } From 1cd7fe8831eebfcbd8f29a5273d828c66c27faa9 Mon Sep 17 00:00:00 2001 From: Ali Date: Tue, 14 Jul 2026 12:35:52 +0300 Subject: [PATCH 6/9] add widget visibility toggle to paint content function --- src/window.rs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/window.rs b/src/window.rs index 9ff7a29..80ee5d1 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1557,6 +1557,7 @@ fn render_layered() { show_claude_code, show_codex, show_antigravity, + widget_visible, ) = { let state = lock_state(); match state.as_ref() { @@ -1580,6 +1581,7 @@ fn render_layered() { s.show_claude_code, s.show_codex, s.show_antigravity, + s.widget_visible, ), None => return, } @@ -1753,6 +1755,7 @@ fn paint_content( show_antigravity: bool, codex_accent: &Color, antigravity_accent: &Color, + visible: bool, ) { unsafe { let client_rect = RECT { @@ -1766,6 +1769,36 @@ fn paint_content( FillRect(hdc, &client_rect, bg_brush); let _ = DeleteObject(bg_brush); + if !visible { + // Collapsed: the whole widget IS the show button. + let font_name = native_interop::wide_str("Segoe UI"); + let font = CreateFontW( + sc(-12), + 0, + 0, + 0, + FW_MEDIUM.0 as i32, + 0, + 0, + 0, + DEFAULT_CHARSET.0 as u32, + OUT_TT_PRECIS.0 as u32, + CLIP_DEFAULT_PRECIS.0 as u32, + CLEARTYPE_QUALITY.0 as u32, + (DEFAULT_PITCH.0 | FF_DONTCARE.0) as u32, + PCWSTR::from_raw(font_name.as_ptr()), + ); + let old_font = SelectObject(hdc, font); + let _ = SetBkMode(hdc, TRANSPARENT); + let _ = SetTextColor(hdc, COLORREF(text_color.to_colorref())); + let mut glyph: Vec = "\u{00BB}".encode_utf16().collect(); + let mut r = client_rect; + let _ = DrawTextW(hdc, &mut glyph, &mut r, DT_CENTER | DT_VCENTER | DT_SINGLELINE); + SelectObject(hdc, old_font); + let _ = DeleteObject(font); + return; + } + // Subtle pill handle: visible enough to discover dragging without // competing with the usage information. let divider_h = sc(26); From 99c169bd4425f099e73a0bcda4870125159aefa7 Mon Sep 17 00:00:00 2001 From: Ali Date: Tue, 14 Jul 2026 12:38:54 +0300 Subject: [PATCH 7/9] add widget visibility to render and paint functions --- src/window.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/window.rs b/src/window.rs index 80ee5d1..aef200f 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1679,6 +1679,7 @@ fn render_layered() { show_antigravity, &codex_accent, &antigravity_accent, + widget_visible, ); // Background pixels → alpha 1 (nearly invisible but still hittable for right-click). @@ -3177,6 +3178,7 @@ fn paint(hdc: HDC, hwnd: HWND) { show_claude_code, show_codex, show_antigravity, + widget_visible, ) = { let state = lock_state(); match state.as_ref() { @@ -3198,6 +3200,7 @@ fn paint(hdc: HDC, hwnd: HWND) { s.show_claude_code, s.show_codex, s.show_antigravity, + s.widget_visible, ), None => return, } @@ -3263,6 +3266,7 @@ fn paint(hdc: HDC, hwnd: HWND) { show_antigravity, &codex_accent, &antigravity_accent, + widget_visible, ); let _ = BitBlt(hdc, 0, 0, width, height, mem_dc, 0, 0, SRCCOPY); From 1a138ca9769913839b8d1c42fb4c18ba22ba6776 Mon Sep 17 00:00:00 2001 From: Ali Date: Tue, 14 Jul 2026 12:41:43 +0300 Subject: [PATCH 8/9] refactor widget visibility handling for consistent display behavior --- src/window.rs | 42 +++++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/src/window.rs b/src/window.rs index aef200f..d00ec5b 100644 --- a/src/window.rs +++ b/src/window.rs @@ -478,24 +478,19 @@ fn sync_tray_icons(hwnd: HWND) { } fn toggle_widget_visibility(hwnd: HWND) { - let new_visible = { + { let mut state = lock_state(); if let Some(s) = state.as_mut() { s.widget_visible = !s.widget_visible; - s.widget_visible } else { return; } - }; + } save_state_settings(); unsafe { - if new_visible { - position_at_taskbar(); - let _ = ShowWindow(hwnd, SW_SHOWNOACTIVATE); - render_layered(); - } else { - let _ = ShowWindow(hwnd, SW_HIDE); - } + position_at_taskbar(); + let _ = ShowWindow(hwnd, SW_SHOWNOACTIVATE); + render_layered(); } } @@ -1227,7 +1222,15 @@ fn total_widget_width_for(active_models: i32) -> i32 { + sc(RIGHT_MARGIN) } +/// Collapsed width: just enough for the show-button glyph, tappable from the taskbar. +fn collapsed_widget_width() -> i32 { + sc(HIDE_BTN_MARGIN) + sc(HIDE_BTN_W) + sc(RIGHT_MARGIN) +} + fn total_widget_width_for_state(state: &AppState) -> i32 { + if !state.widget_visible { + return collapsed_widget_width(); + } total_widget_width_for(active_model_count( state.show_claude_code, state.show_codex, @@ -1236,6 +1239,13 @@ fn total_widget_width_for_state(state: &AppState) -> i32 { } fn total_widget_width() -> i32 { + let widget_visible = { + let state = lock_state(); + state.as_ref().map(|s| s.widget_visible).unwrap_or(true) + }; + if !widget_visible { + return collapsed_widget_width(); + } let active_models = { let state = lock_state(); state @@ -1474,11 +1484,9 @@ pub fn run() { // Register system tray icon(s) sync_tray_icons(hwnd); - // Position and show (only if widget_visible preference is true) + // Always show; collapses to a small show-button when widget_visible is false. position_at_taskbar(); - if settings.widget_visible { - let _ = ShowWindow(hwnd, SW_SHOWNOACTIVATE); - } + let _ = ShowWindow(hwnd, SW_SHOWNOACTIVATE); diagnose::log("window shown"); // Initial render via UpdateLayeredWindow (for embedded) or InvalidateRect (fallback) @@ -2499,7 +2507,11 @@ unsafe extern "system" fn wnd_proc( WM_LBUTTONDOWN => { let client_x = (lparam.0 & 0xFFFF) as i16 as i32; let client_y = ((lparam.0 >> 16) & 0xFFFF) as i16 as i32; - if !is_drag_handle_point(client_x, client_y) { + let widget_visible = { + let state = lock_state(); + state.as_ref().map(|s| s.widget_visible).unwrap_or(true) + }; + if !widget_visible || !is_drag_handle_point(client_x, client_y) { return LRESULT(0); } From 5528d7fda717eb1a24b0645d4fbebb54458587df Mon Sep 17 00:00:00 2001 From: Ali Date: Tue, 14 Jul 2026 12:41:55 +0300 Subject: [PATCH 9/9] add widget visibility check to cursor drag handle function --- src/window.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/window.rs b/src/window.rs index d00ec5b..3fc503e 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1091,7 +1091,11 @@ fn cursor_is_on_drag_handle(hwnd: HWND) -> bool { if GetCursorPos(&mut pt).is_err() || !ScreenToClient(hwnd, &mut pt).as_bool() { return false; } - is_drag_handle_point(pt.x, pt.y) + let widget_visible = { + let state = lock_state(); + state.as_ref().map(|s| s.widget_visible).unwrap_or(true) + }; + widget_visible && is_drag_handle_point(pt.x, pt.y) } }