From 2fcd8ef29d8c3302c957b0cc5f4dccc71beea08d Mon Sep 17 00:00:00 2001 From: "Carlos Miguel C. Resurreccion" Date: Fri, 26 Jun 2026 10:17:51 +0800 Subject: [PATCH] feat: pace indicator on v1.4.8 --- .github/screenshots/pace-solid-green.png | Bin 0 -> 5265 bytes .github/screenshots/pace-solid-red.png | Bin 0 -> 5222 bytes .github/screenshots/pace-tick-green.png | Bin 0 -> 5234 bytes .github/screenshots/pace-tick-red.png | Bin 0 -> 5305 bytes README.md | 1 + src/localization/dutch.rs | 4 + src/localization/english.rs | 4 + src/localization/french.rs | 4 + src/localization/german.rs | 4 + src/localization/japanese.rs | 4 + src/localization/korean.rs | 4 + src/localization/mod.rs | 4 + src/localization/portuguese_brazil.rs | 4 + src/localization/russian.rs | 4 + src/localization/spanish.rs | 4 + src/localization/traditional_chinese.rs | 4 + src/window.rs | 491 +++++++++++++++++++++-- 17 files changed, 492 insertions(+), 44 deletions(-) create mode 100644 .github/screenshots/pace-solid-green.png create mode 100644 .github/screenshots/pace-solid-red.png create mode 100644 .github/screenshots/pace-tick-green.png create mode 100644 .github/screenshots/pace-tick-red.png diff --git a/.github/screenshots/pace-solid-green.png b/.github/screenshots/pace-solid-green.png new file mode 100644 index 0000000000000000000000000000000000000000..ccb954d9e847aab37f6c648dabf40a1ea304c5b7 GIT binary patch literal 5265 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 g(J&vhMG*lISL5Frp7p6!{6ocn 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 literal 0 HcmV?d00001 diff --git a/.github/screenshots/pace-tick-green.png b/.github/screenshots/pace-tick-green.png new file mode 100644 index 0000000000000000000000000000000000000000..8acb3b207c012d78b0d4e2d3fa031a449c688525 GIT binary patch literal 5234 zcmZXYdpy(a|HmhU97{zQsXKLda-8Kf9YlR=s@q*iMr1*)-AWgo&iokz;}R}~`%~1<2OH}Na5sbN57=|_+7wQ7H74IoX`UPV z7oQ^NRHOK}H}z9Dbgu}f$=2D~c{Z`gW{ICA zsV0x9v$c}-_Y>mn$=#~9!qT0sG)t3+>{tBTIA8YLi8`X5yEAQ{LvH+6lugr$e_P+@ zx7n#=17Qqnadtbx*sz`*!xHKp9v}4CRJCzw8VSZ) zhxUS<=WEp$djAz^Wzim7f&RP8$gF4KQKa?`vm-cR3qEQlD5@@&WlFUf@6*8^1%a0%|br%7L9MyxOM5U z$dZ%g`e*+YBi=y&ndyk@%1z9saxo7lojsYC@b@x@!`YKpFADP1zF#^LdHh6h_i{i4 zA-@2W$EPkzxjr~Yw*jxmmcgEGr34qM*qOpkIdG>_rKRAkckX!D2ov0tor!_RarTuO z1)?f3G>1vJJwpAXl4>cSq2jb!vfv-{hCA<1evY?n=7kico3Ku9%c#I;E4uJae)x<^ zkogaz!kvlNVru1v>5*UcRyOg?2XurP^+0{&#V?U5YTn=azQ(UWwywmymUcfsg9(w( z@|o%YcHn+W&+t;|k01983~**7;^rPE{qZsV5JM*4Ei>6mqZ5DU85vU3=-0+xyR|puOSwK&lg@idg=tSMiKPly~jjJw7X3?c+XQ zUI47MVkE{J0*QjM(g+8y#@dr$5`k?cE>{#Di7f(y7_MCkvMS#%STux;eR@R7G--|R*)bP2E6g}STbBRIW@b9n z1X=rNU{8wI#Jo2-=Ik+>L*svYhbG#v|!6R^cloum=2A)IYrwg{t;=;>BzQ^1{U6cbe9$(uaiZg z#iF5ckVZ+h3CD|!SD*0tOC+=%>NM)$D|`|+Z!klcCS5d+OrKb$&ocrKzGh|>`pmxl zSB+VO)b1Qs0H}~^d9bwe3tR&A!+lu+r*|-_3Tw6LuFA|63)oooV!m^ml_!1mg-yGu zwb;uA96Rry|MbE(K+E0tZ9JHR90Yby?C|)xtx6LD@7q5PwY(;@{$-<(|JtZzOwCR= zY6((J3EZXQ)z?9JbglUFA9TIC*tphf52LAQ@J+!WQ!slD;h3(c2Ht-KO0Bs0>ZQ+?MBEA?D`s~;3<1v0KR2P=A{nf&TCiPAg zkgT5-^=Fu_^U~tL2Xu(&LQ)Pw7@*&$g>#4A>AgORrB#K#%la)lfq-Oo8>+l+)32n& zdeIaUSk&FM{-K`QZ_k|w4j()boN+RB>b1#24FXEsWSB6!29i#ZK5u){f2cgTAJhZm z@jdsWwHYVTbVc;nKpY!Ww`=>~gCc0&z`$JAmslw-0Kkufn#?Y--&g0iVm+ zCTR2tCrrg%%g^hTIxtxw>jj{_JaX>F*~!eTc2s60b8S3)YHypv8>m=iXC+Y|OhTpV zKFXu^SLl|FJ@)n4ftm9VnyNtPwswzJFH zmVFEL&tX?cG+p<$rT~Y2@!-L}z7QZ2&LRm@4LOjvWASbxp}eWWjTpmp1fr4~tI;7G z{n4NH3Vqj$HpJF%k(9Y0#|3y7dKo_{8`3kJFVRblBc_5hVY6#6|CY_mv~-IjO1?eT zlfLb^7oh}*vZzmC3eAKadb6tQ!q<17A6D#ePYr5i3rYro`H~pZC`PcsE2SyT{aZxY zWAP#0KT__GcV7-jgr82`c(O#UtEP1sVtHqHc?N}ZwRQ_B?d+PhB`jk&qnJN@hFw81 z+Yx!-qj_giuZ_k`Sm3y?CdqybdGK3h%H~KW)R9pfNX=I1uJAuMb;W}mJe4KvDk7Sf z>$mXJ2cro3>$Xk8)RGN7pSo4nsttJZ(lCBXx65*~L9In_BiWnzy@jirgm^k89exq^ z@Rl}PWWpWqB4NF)sh(g~lC!w9iO*nJ3%c4OR?lW2F)$YrUApQGqSYsYvyO(*g145&>ec-on9 z;BSg64!vxe0Tf1;9h>U&H4r(IAren{V7<7@&!{i_O2`u13~ajlGM7p!iC(objQBlX zM=^NaU98LFHsI(zJB-k!JFzt3ZM7zUeXS?hig8{lPLxBjXA*N(6JMCWIsOLg{r;kU zbOO6fiJHo=FxY_bXFzOe%`lvoH!?`4? zFCci`itl@qR48VC|JtqHxpg%Sm5){>IMg$TC{*UaT1Ih&C&v%Q8CtL~9Gh z%m6WErZC#~Bwsg)4Y*jm(Gy_WrH=(uoJK+?okqA{pRheizd}SZ7Qq8~rt$d!h~Z+s zzy|-Z@k<@5zY2f-;OPy$r}Wxrx>hdRAj4}3|m6h7;WjYq`gFsF#>t&%#2XQFJN0>iweg9S|{y6{LfE_#bodi zvA-*8le+!w8T?e|grAq&XbLIkdm$dznnrZ}Yhkw|M@le)OJc#OsV24K<&Im?Ntgfa zAOo`~*&!1nlEL6ZZ~Va+8K_W#enl>x?<=jMoT8!=adCES?IqIMR8DuNPgIZ3)&rBy zLr~OJOYbYSQ{Sm0uSC_P$hfa{oV}$pqisv=;=&qWJ;RysJZk)kLek2Q5w|LR__Og} z_&N2^{MtPksQ_6(HJtOT{Q!Q(uPFCAVy~|16cSIrY zWyO0o)t2tptm^;l7jU}z1^)uDNIvAQ#V}LQT}Y}DhXOxE6mD?*eqMZg75mMY)$zoO z8ZSYWZ|bx&9Z}=LTXn{Mr!@-jRCsl$66#%w#EF#PaOk%v*NdbM%31Y~&=ez_>X^F4 zFD!aCzTExfh^`F38uZAD(li-kvSpM>*Ek>jW_6YR?Aa(z{1MtGJ?I0LS!VD3tk0fX z1pSz>e8jCOxa9CmNa*RidmRfH(pB7qQ)1-<`s91(Z_<2<%RNsVpf#agdbuEOpLoG} z8LQmN+Arf2hR@T$=!pDO8B%C7b83zDm!d36D}Oc_uem@ARz)_^qWpWcFKWSy8_3uZ z_)w^wJJ?GC%^mlUTY%mg;==hA#M>*Gs(Pk`{%kFAe)3;q9Y706_|FdYgQOLg1eeDv<;PWWM9W8ZVn+biD^=$TJ_iGa51_MfM16b%aMi&}?I@vbMj zEpIOkFwbiE&Q^Bp9rKGTSKdfwfYsg(QkRmdz)aMW37 zdn4TcR-uuMSk%*ggRi&A2XsbyO_Gek59kCZq<(;QSERw9o$4%7Yt#}MY-sdP;7r5p;P1myg^l|(gXmtg~euo{hiG^ z`?{Eh7D}3e+h6N6EyV`lD4E*O(h(w*%!qmto1E)Fo@p@KE5xvxE>Diyhq&{PWYu)d zG6ywhw1^S$2L9xh48=m9)uqG1!(oU@j8yU4N;|!G>MgMgO0!PG&6R?rtSquAWgh;3ShC^Vu`Q`tMdE0I-d_`LNYC!hKv_NH zwY}*u7bB5WkDDPS)SfWrWAubBZ?gf4e3X= iL;(zZJa`?&aZJxJ@ehIxJInqP0xq6+b*eq<``|y7(d2*t literal 0 HcmV?d00001 diff --git a/.github/screenshots/pace-tick-red.png b/.github/screenshots/pace-tick-red.png new file mode 100644 index 0000000000000000000000000000000000000000..9547716a7e984211fcbd1d282d27ea4db74e0ec3 GIT binary patch literal 5305 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 Mm)tH^Ir=~P9|3{oHUIzs literal 0 HcmV?d00001 diff --git a/README.md b/README.md index f8b3c2b..c625720 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,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 +- Under right-click `Settings` → `Show pace indicator`, pick a `Tick` or `Solid` style to mark where your usage should be for the time elapsed: green means you have headroom to ramp up, red means you are on a trajectory to exceed your quota before it resets (stop work or pay for extra usage); pick `Off` to turn it off ### Models diff --git a/src/localization/dutch.rs b/src/localization/dutch.rs index ed815bf..520e322 100644 --- a/src/localization/dutch.rs +++ b/src/localization/dutch.rs @@ -35,6 +35,10 @@ pub(super) const STRINGS: Strings = Strings { session_window: "5u", weekly_window: "7d", now: "nu", + show_pace_indicator: "Tempo-indicator tonen", + pace_style_off: "Uit (standaard)", + pace_style_tick: "Streepje", + pace_style_solid: "Gevuld", day_suffix: "d", hour_suffix: "u", minute_suffix: "m", diff --git a/src/localization/english.rs b/src/localization/english.rs index 0249730..8636dff 100644 --- a/src/localization/english.rs +++ b/src/localization/english.rs @@ -35,6 +35,10 @@ pub(super) const STRINGS: Strings = Strings { session_window: "5h", weekly_window: "7d", now: "now", + show_pace_indicator: "Show pace indicator", + pace_style_off: "Off (default)", + pace_style_tick: "Tick", + pace_style_solid: "Solid", day_suffix: "d", hour_suffix: "h", minute_suffix: "m", diff --git a/src/localization/french.rs b/src/localization/french.rs index 1850f41..67fcb4b 100644 --- a/src/localization/french.rs +++ b/src/localization/french.rs @@ -35,6 +35,10 @@ pub(super) const STRINGS: Strings = Strings { session_window: "5h", weekly_window: "7d", now: "maintenant", + show_pace_indicator: "Afficher l'indicateur de cadence", + pace_style_off: "Désactivé (par défaut)", + pace_style_tick: "Repère", + pace_style_solid: "Plein", day_suffix: "j", hour_suffix: "h", minute_suffix: "m", diff --git a/src/localization/german.rs b/src/localization/german.rs index 2b91a81..a8f34ca 100644 --- a/src/localization/german.rs +++ b/src/localization/german.rs @@ -35,6 +35,10 @@ pub(super) const STRINGS: Strings = Strings { session_window: "5h", weekly_window: "7d", now: "jetzt", + show_pace_indicator: "Tempo-Anzeige einblenden", + pace_style_off: "Aus (Standard)", + pace_style_tick: "Strich", + pace_style_solid: "Gefüllt", day_suffix: "T", hour_suffix: "h", minute_suffix: "m", diff --git a/src/localization/japanese.rs b/src/localization/japanese.rs index 2eec041..65c7156 100644 --- a/src/localization/japanese.rs +++ b/src/localization/japanese.rs @@ -35,6 +35,10 @@ pub(super) const STRINGS: Strings = Strings { session_window: "5h", weekly_window: "7d", now: "今", + show_pace_indicator: "ペース表示", + pace_style_off: "オフ(デフォルト)", + pace_style_tick: "目盛り", + pace_style_solid: "塗りつぶし", day_suffix: "日", hour_suffix: "時間", minute_suffix: "分", diff --git a/src/localization/korean.rs b/src/localization/korean.rs index 965687d..dba00c4 100644 --- a/src/localization/korean.rs +++ b/src/localization/korean.rs @@ -35,6 +35,10 @@ pub(super) const STRINGS: Strings = Strings { session_window: "5시간", weekly_window: "7일", now: "지금", + show_pace_indicator: "사용 속도 표시", + pace_style_off: "끄기(기본값)", + pace_style_tick: "눈금", + pace_style_solid: "채우기", day_suffix: "일", hour_suffix: "시간", minute_suffix: "분", diff --git a/src/localization/mod.rs b/src/localization/mod.rs index 2a06b04..d1e8507 100644 --- a/src/localization/mod.rs +++ b/src/localization/mod.rs @@ -169,6 +169,10 @@ pub struct Strings { pub session_window: &'static str, pub weekly_window: &'static str, pub now: &'static str, + pub show_pace_indicator: &'static str, + pub pace_style_off: &'static str, + pub pace_style_tick: &'static str, + pub pace_style_solid: &'static str, pub day_suffix: &'static str, pub hour_suffix: &'static str, pub minute_suffix: &'static str, diff --git a/src/localization/portuguese_brazil.rs b/src/localization/portuguese_brazil.rs index 56cf3bf..79d1fb4 100644 --- a/src/localization/portuguese_brazil.rs +++ b/src/localization/portuguese_brazil.rs @@ -35,6 +35,10 @@ pub(super) const STRINGS: Strings = Strings { session_window: "5h", weekly_window: "7d", now: "agora", + show_pace_indicator: "Mostrar indicador de ritmo", + pace_style_off: "Desligado (padrão)", + pace_style_tick: "Traço", + pace_style_solid: "Sólido", day_suffix: "d", hour_suffix: "h", minute_suffix: "m", diff --git a/src/localization/russian.rs b/src/localization/russian.rs index fc7e372..459c16a 100644 --- a/src/localization/russian.rs +++ b/src/localization/russian.rs @@ -35,6 +35,10 @@ pub(super) const STRINGS: Strings = Strings { session_window: "5ч", weekly_window: "7д", now: "сейчас", + show_pace_indicator: "Показывать индикатор темпа", + pace_style_off: "Выкл. (по умолчанию)", + pace_style_tick: "Штрих", + pace_style_solid: "Заливка", day_suffix: "д", hour_suffix: "ч", minute_suffix: "м", diff --git a/src/localization/spanish.rs b/src/localization/spanish.rs index e635771..9786ec3 100644 --- a/src/localization/spanish.rs +++ b/src/localization/spanish.rs @@ -35,6 +35,10 @@ pub(super) const STRINGS: Strings = Strings { session_window: "5h", weekly_window: "7d", now: "ahora", + show_pace_indicator: "Mostrar indicador de ritmo", + pace_style_off: "Desactivado (predeterminado)", + pace_style_tick: "Marca", + pace_style_solid: "Relleno", day_suffix: "d", hour_suffix: "h", minute_suffix: "m", diff --git a/src/localization/traditional_chinese.rs b/src/localization/traditional_chinese.rs index 3eb3514..d434f94 100644 --- a/src/localization/traditional_chinese.rs +++ b/src/localization/traditional_chinese.rs @@ -35,6 +35,10 @@ pub(super) const STRINGS: Strings = Strings { session_window: "5h", weekly_window: "7d", now: "現在", + show_pace_indicator: "顯示使用步調", + pace_style_off: "關閉(預設)", + pace_style_tick: "刻度線", + pace_style_solid: "實心", day_suffix: "天", hour_suffix: "時", minute_suffix: "分", diff --git a/src/window.rs b/src/window.rs index f6d261e..965db05 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}; @@ -68,6 +68,8 @@ struct AppState { antigravity_weekly_percent: f64, antigravity_weekly_text: String, show_claude_code: bool, + show_pace_indicator: bool, + pace_indicator_solid: bool, show_codex: bool, show_antigravity: bool, @@ -128,10 +130,18 @@ const IDM_LANG_KOREAN: u16 = 47; const IDM_LANG_TRADITIONAL_CHINESE: u16 = 48; const IDM_LANG_RUSSIAN: u16 = 49; const IDM_LANG_PORTUGUESE_BRAZIL: u16 = 50; +const IDM_PACE_STYLE_OFF: u16 = 71; +const IDM_PACE_STYLE_TICK: u16 = 72; +const IDM_PACE_STYLE_SOLID: u16 = 73; const IDM_MODEL_CLAUDE_CODE: u16 = 60; const IDM_MODEL_CODEX: u16 = 61; const IDM_MODEL_ANTIGRAVITY: u16 = 62; +// 5 hours and 7 days, in seconds. Used to compute "where pace says you +// should be" by comparing remaining time against the full window length. +const SESSION_WINDOW_SECS: u64 = 5 * 3600; +const WEEKLY_WINDOW_SECS: u64 = 7 * 86400; + const WM_DPICHANGED_MSG: u32 = 0x02E0; const WM_APP_UPDATE_CHECK_COMPLETE: u32 = WM_APP + 2; const TRAY_ICON_UPDATE_REPOSITION_SUPPRESS_MS: u64 = 750; @@ -312,6 +322,10 @@ struct SettingsFile { widget_visible: bool, #[serde(default = "default_show_claude_code")] show_claude_code: bool, + #[serde(default)] + show_pace_indicator: bool, + #[serde(default = "default_pace_indicator_solid")] + pace_indicator_solid: bool, #[serde(default = "default_show_codex")] show_codex: bool, #[serde(default = "default_show_antigravity")] @@ -328,6 +342,8 @@ impl Default for SettingsFile { last_update_check_unix: None, widget_visible: true, show_claude_code: true, + show_pace_indicator: false, + pace_indicator_solid: default_pace_indicator_solid(), show_codex: false, show_antigravity: false, } @@ -354,6 +370,10 @@ fn default_show_antigravity() -> bool { false } +fn default_pace_indicator_solid() -> bool { + true +} + fn load_settings() -> SettingsFile { let content = match std::fs::read_to_string(settings_path()) { Ok(c) => c, @@ -389,12 +409,60 @@ fn save_state_settings() { last_update_check_unix: s.last_update_check_unix, widget_visible: s.widget_visible, show_claude_code: s.show_claude_code, + show_pace_indicator: s.show_pace_indicator, + pace_indicator_solid: s.pace_indicator_solid, show_codex: s.show_codex, show_antigravity: s.show_antigravity, }); } } +/// Where pace says you should be, as a 0-100 percentage of the window +/// consumed by now. `None` if there is no reset timestamp yet, if the +/// window has already reset (data is stale), or if the remaining time +/// exceeds the window length (unexpected, but guarded against). +fn expected_pace_pct(resets_at: Option, window_secs: u64) -> Option { + let reset = resets_at?; + let remaining = reset.duration_since(SystemTime::now()).ok()?; + let remaining_secs = remaining.as_secs(); + if remaining_secs > window_secs { + return None; + } + let elapsed = window_secs - remaining_secs; + Some(elapsed as f64 / window_secs as f64 * 100.0) +} + +/// Pace values for the six usage cells, in the order +/// (claude session, claude weekly, codex session, codex weekly, +/// antigravity session, antigravity weekly). +/// Returns all `None` when the indicator is disabled, when there is no +/// usage data yet, or when individual reset timestamps are missing. +fn pace_values_from_state( + s: &AppState, +) -> ( + Option, + Option, + Option, + Option, + Option, + Option, +) { + if !s.show_pace_indicator { + return (None, None, None, None, None, None); + } + let claude = s.data.as_ref().and_then(|d| d.claude_code.as_ref()); + let codex = s.data.as_ref().and_then(|d| d.codex.as_ref()); + let antigravity = s.data.as_ref().and_then(|d| d.antigravity.as_ref()); + ( + claude.and_then(|c| expected_pace_pct(c.session.resets_at, SESSION_WINDOW_SECS)), + claude.and_then(|c| expected_pace_pct(c.weekly.resets_at, WEEKLY_WINDOW_SECS)), + codex.and_then(|c| expected_pace_pct(c.session.resets_at, SESSION_WINDOW_SECS)), + codex.and_then(|c| expected_pace_pct(c.weekly.resets_at, WEEKLY_WINDOW_SECS)), + antigravity.and_then(|c| expected_pace_pct(c.session.resets_at, SESSION_WINDOW_SECS)), + antigravity.and_then(|c| expected_pace_pct(c.weekly.resets_at, WEEKLY_WINDOW_SECS)), + ) +} + fn tray_icon_data_from_state() -> Vec { let state = lock_state(); match state.as_ref() { @@ -672,6 +740,8 @@ fn refresh_usage_texts(state: &mut AppState) { state.antigravity_session_text = "!".to_string(); state.antigravity_weekly_text = "!".to_string(); } + + update_measured_text_width(state); } fn set_window_title(hwnd: HWND, strings: Strings) { @@ -1083,6 +1153,76 @@ fn active_model_count(show_claude_code: bool, show_codex: bool, show_antigravity (show_claude_code as i32 + show_codex as i32 + show_antigravity as i32).max(1) } +/// Small trailing padding (device px, unscaled) added after measured text. +const TEXT_MEASURE_PAD: i32 = 6; + +/// Width (device px, already DPI-scaled) of the widest usage-cell text actually +/// shown, recomputed whenever the texts change. The cell column sizes to real +/// content (detailed time / ETD suffix only when present) instead of a fixed +/// worst-case reservation. Falls back to the base column before first measure. +static MEASURED_TEXT_WIDTH: AtomicI32 = AtomicI32::new(0); + +fn current_text_width() -> i32 { + MEASURED_TEXT_WIDTH.load(Ordering::Relaxed).max(sc(TEXT_WIDTH)) +} + +/// Measure a string's pixel width in the same font the widget renders with. +fn measure_text_px(text: &str) -> i32 { + if text.is_empty() { + return 0; + } + unsafe { + let hdc = GetDC(HWND::default()); + let mem = CreateCompatibleDC(hdc); + 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 = SelectObject(mem, font); + let wide: Vec = text.encode_utf16().collect(); + let mut size = SIZE::default(); + let _ = GetTextExtentPoint32W(mem, &wide, &mut size); + SelectObject(mem, old); + let _ = DeleteObject(font); + let _ = DeleteDC(mem); + ReleaseDC(HWND::default(), hdc); + size.cx + } +} + +/// Recompute the measured cell-text width from the currently-visible texts. +fn update_measured_text_width(state: &AppState) { + let mut max_w = 0; + if state.show_claude_code { + max_w = max_w.max(measure_text_px(&state.session_text)); + max_w = max_w.max(measure_text_px(&state.weekly_text)); + } + if state.show_codex { + max_w = max_w.max(measure_text_px(&state.codex_session_text)); + max_w = max_w.max(measure_text_px(&state.codex_weekly_text)); + } + if state.show_antigravity { + max_w = max_w.max(measure_text_px(&state.antigravity_session_text)); + max_w = max_w.max(measure_text_px(&state.antigravity_weekly_text)); + } + if max_w > 0 { + MEASURED_TEXT_WIDTH.store(max_w + sc(TEXT_MEASURE_PAD), Ordering::Relaxed); + } +} + fn row_bar_segment_count(active_models: i32) -> i32 { match active_models { 1 => SEGMENT_COUNT, @@ -1095,7 +1235,7 @@ 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); + + current_text_width(); sc(LEFT_DIVIDER_W) + sc(DIVIDER_RIGHT_MARGIN) @@ -1308,6 +1448,8 @@ pub fn run() { antigravity_weekly_percent: 0.0, antigravity_weekly_text: "--".to_string(), show_claude_code: settings.show_claude_code, + show_pace_indicator: settings.show_pace_indicator, + pace_indicator_solid: settings.pace_indicator_solid, show_codex: settings.show_codex, show_antigravity: settings.show_antigravity, data: None, @@ -1435,30 +1577,54 @@ fn render_layered() { show_claude_code, show_codex, show_antigravity, + session_pace_pct, + weekly_pace_pct, + codex_session_pace_pct, + codex_weekly_pace_pct, + antigravity_session_pace_pct, + antigravity_weekly_pace_pct, + pace_solid, ) = { let state = lock_state(); match state.as_ref() { - Some(s) => ( - s.hwnd, - s.is_dark, - s.embedded, - s.language.strings(), - s.session_percent, - s.session_text.clone(), - s.weekly_percent, - s.weekly_text.clone(), - s.codex_session_percent, - s.codex_session_text.clone(), - s.codex_weekly_percent, - s.codex_weekly_text.clone(), - s.antigravity_session_percent, - s.antigravity_session_text.clone(), - s.antigravity_weekly_percent, - s.antigravity_weekly_text.clone(), - s.show_claude_code, - s.show_codex, - s.show_antigravity, - ), + Some(s) => { + let ( + session_pace, + weekly_pace, + codex_session_pace, + codex_weekly_pace, + antigravity_session_pace, + antigravity_weekly_pace, + ) = pace_values_from_state(s); + ( + s.hwnd, + s.is_dark, + s.embedded, + s.language.strings(), + s.session_percent, + s.session_text.clone(), + s.weekly_percent, + s.weekly_text.clone(), + s.codex_session_percent, + s.codex_session_text.clone(), + s.codex_weekly_percent, + s.codex_weekly_text.clone(), + s.antigravity_session_percent, + s.antigravity_session_text.clone(), + s.antigravity_weekly_percent, + s.antigravity_weekly_text.clone(), + s.show_claude_code, + s.show_codex, + s.show_antigravity, + session_pace, + weekly_pace, + codex_session_pace, + codex_weekly_pace, + antigravity_session_pace, + antigravity_weekly_pace, + s.pace_indicator_solid, + ) + } None => return, } }; @@ -1555,6 +1721,13 @@ fn render_layered() { show_antigravity, &codex_accent, &antigravity_accent, + session_pace_pct, + weekly_pace_pct, + codex_session_pace_pct, + codex_weekly_pace_pct, + antigravity_session_pace_pct, + antigravity_weekly_pace_pct, + pace_solid, ); // Background pixels → alpha 1 (nearly invisible but still hittable for right-click). @@ -1631,6 +1804,13 @@ fn paint_content( show_antigravity: bool, codex_accent: &Color, antigravity_accent: &Color, + session_pace_pct: Option, + weekly_pace_pct: Option, + codex_session_pace_pct: Option, + codex_weekly_pace_pct: Option, + antigravity_session_pace_pct: Option, + antigravity_weekly_pace_pct: Option, + pace_solid: bool, ) { unsafe { let client_rect = RECT { @@ -1727,6 +1907,10 @@ fn paint_content( codex_accent, antigravity_accent, track, + session_pace_pct, + codex_session_pace_pct, + antigravity_session_pace_pct, + pace_solid, ); draw_row( hdc, @@ -1748,6 +1932,10 @@ fn paint_content( codex_accent, antigravity_accent, track, + weekly_pace_pct, + codex_weekly_pace_pct, + antigravity_weekly_pace_pct, + pace_solid, ); SelectObject(hdc, old_font); @@ -2576,6 +2764,21 @@ unsafe extern "system" fn wnd_proc( IDM_START_WITH_WINDOWS => { set_startup_enabled(!is_startup_enabled()); } + IDM_PACE_STYLE_OFF | IDM_PACE_STYLE_TICK | IDM_PACE_STYLE_SOLID => { + { + let mut state = lock_state(); + if let Some(s) = state.as_mut() { + if id == IDM_PACE_STYLE_OFF { + s.show_pace_indicator = false; + } else { + s.show_pace_indicator = true; + s.pace_indicator_solid = id == IDM_PACE_STYLE_SOLID; + } + } + } + save_state_settings(); + render_layered(); + } IDM_FREQ_1MIN | IDM_FREQ_5MIN | IDM_FREQ_15MIN | IDM_FREQ_1HOUR => { let new_interval = match id { IDM_FREQ_1MIN => POLL_1_MIN, @@ -2715,6 +2918,8 @@ fn show_context_menu(hwnd: HWND) { show_claude_code, show_codex, show_antigravity, + show_pace_indicator, + pace_indicator_solid, ) = { let state = lock_state(); match state.as_ref() { @@ -2729,6 +2934,8 @@ fn show_context_menu(hwnd: HWND) { s.show_claude_code, s.show_codex, s.show_antigravity, + s.show_pace_indicator, + s.pace_indicator_solid, ), None => ( POLL_15_MIN, @@ -2741,6 +2948,8 @@ fn show_context_menu(hwnd: HWND) { true, false, false, + false, + true, ), } }; @@ -2851,6 +3060,63 @@ fn show_context_menu(hwnd: HWND) { PCWSTR::from_raw(startup_str.as_ptr()), ); + // "Show pace indicator" is a submenu whose Off/Tick/Solid items form a + // radio group. The parent entry shows a check while a style is active; + // selecting Off clears that check and turns the indicator off. + let pace_menu = CreatePopupMenu().unwrap(); + + let pace_off_str = native_interop::wide_str(strings.pace_style_off); + let pace_off_flags = if show_pace_indicator { + MENU_ITEM_FLAGS(0) + } else { + MF_CHECKED + }; + let _ = AppendMenuW( + pace_menu, + pace_off_flags, + IDM_PACE_STYLE_OFF as usize, + PCWSTR::from_raw(pace_off_str.as_ptr()), + ); + + let pace_tick_str = native_interop::wide_str(strings.pace_style_tick); + let pace_tick_flags = if show_pace_indicator && !pace_indicator_solid { + MF_CHECKED + } else { + MENU_ITEM_FLAGS(0) + }; + let _ = AppendMenuW( + pace_menu, + pace_tick_flags, + IDM_PACE_STYLE_TICK as usize, + PCWSTR::from_raw(pace_tick_str.as_ptr()), + ); + + let pace_solid_str = native_interop::wide_str(strings.pace_style_solid); + let pace_solid_flags = if show_pace_indicator && pace_indicator_solid { + MF_CHECKED + } else { + MENU_ITEM_FLAGS(0) + }; + let _ = AppendMenuW( + pace_menu, + pace_solid_flags, + IDM_PACE_STYLE_SOLID as usize, + PCWSTR::from_raw(pace_solid_str.as_ptr()), + ); + + let pace_label = native_interop::wide_str(strings.show_pace_indicator); + let pace_parent_flags = if show_pace_indicator { + MF_POPUP | MF_CHECKED + } else { + MF_POPUP + }; + let _ = AppendMenuW( + settings_menu, + pace_parent_flags, + pace_menu.0 as usize, + PCWSTR::from_raw(pace_label.as_ptr()), + ); + let reset_pos_str = native_interop::wide_str(strings.reset_position); let _ = AppendMenuW( settings_menu, @@ -2987,28 +3253,52 @@ fn paint(hdc: HDC, hwnd: HWND) { show_claude_code, show_codex, show_antigravity, + session_pace_pct, + weekly_pace_pct, + codex_session_pace_pct, + codex_weekly_pace_pct, + antigravity_session_pace_pct, + antigravity_weekly_pace_pct, + pace_solid, ) = { let state = lock_state(); match state.as_ref() { - Some(s) => ( - s.is_dark, - s.language.strings(), - s.session_percent, - s.session_text.clone(), - s.weekly_percent, - s.weekly_text.clone(), - s.codex_session_percent, - s.codex_session_text.clone(), - s.codex_weekly_percent, - s.codex_weekly_text.clone(), - s.antigravity_session_percent, - s.antigravity_session_text.clone(), - s.antigravity_weekly_percent, - s.antigravity_weekly_text.clone(), - s.show_claude_code, - s.show_codex, - s.show_antigravity, - ), + Some(s) => { + let ( + session_pace, + weekly_pace, + codex_session_pace, + codex_weekly_pace, + antigravity_session_pace, + antigravity_weekly_pace, + ) = pace_values_from_state(s); + ( + s.is_dark, + s.language.strings(), + s.session_percent, + s.session_text.clone(), + s.weekly_percent, + s.weekly_text.clone(), + s.codex_session_percent, + s.codex_session_text.clone(), + s.codex_weekly_percent, + s.codex_weekly_text.clone(), + s.antigravity_session_percent, + s.antigravity_session_text.clone(), + s.antigravity_weekly_percent, + s.antigravity_weekly_text.clone(), + s.show_claude_code, + s.show_codex, + s.show_antigravity, + session_pace, + weekly_pace, + codex_session_pace, + codex_weekly_pace, + antigravity_session_pace, + antigravity_weekly_pace, + s.pace_indicator_solid, + ) + } None => return, } }; @@ -3073,6 +3363,13 @@ fn paint(hdc: HDC, hwnd: HWND) { show_antigravity, &codex_accent, &antigravity_accent, + session_pace_pct, + weekly_pace_pct, + codex_session_pace_pct, + codex_weekly_pace_pct, + antigravity_session_pace_pct, + antigravity_weekly_pace_pct, + pace_solid, ); let _ = BitBlt(hdc, 0, 0, width, height, mem_dc, 0, 0, SRCCOPY); @@ -3103,6 +3400,10 @@ fn draw_row( codex_accent: &Color, antigravity_accent: &Color, track: &Color, + claude_pace_pct: Option, + codex_pace_pct: Option, + antigravity_pace_pct: Option, + pace_solid: bool, ) { let seg_h = sc(SEGMENT_H); let active_models = active_model_count(show_claude_code, show_codex, show_antigravity); @@ -3152,6 +3453,9 @@ fn draw_row( claude_accent, track, &claude_value_color, + claude_pace_pct, + is_dark, + pace_solid, ); model_x += model_usage_width(segment_count) + sc(MODEL_RIGHT_MARGIN); } @@ -3166,6 +3470,9 @@ fn draw_row( codex_accent, track, &codex_value_color, + codex_pace_pct, + is_dark, + pace_solid, ); model_x += model_usage_width(segment_count) + sc(MODEL_RIGHT_MARGIN); } @@ -3180,6 +3487,9 @@ fn draw_row( antigravity_accent, track, &antigravity_value_color, + antigravity_pace_pct, + is_dark, + pace_solid, ); } } @@ -3188,7 +3498,7 @@ fn draw_row( 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) + + current_text_width() } fn draw_usage_bar( @@ -3201,6 +3511,9 @@ fn draw_usage_bar( accent: &Color, track: &Color, text_color: &Color, + pace_pct: Option, + is_dark: bool, + pace_solid: bool, ) { let seg_w = sc(SEGMENT_W); let seg_h = sc(SEGMENT_H); @@ -3256,12 +3569,102 @@ fn draw_usage_bar( } } + // Pace indicator. Two styles, selected by pace_solid: + // solid — a filled band spanning the gap between actual usage and + // where pace says the user should be (red overage over the + // orange accent, green headroom over the grey track). + // tick — a thick vertical bar at the expected-pace position. + // Green means usage is behind pace — headroom to ramp up. Red means + // usage is ahead of pace — on a trajectory to exhaust the quota early. + if let Some(pace) = pace_pct { + let expected = pace.clamp(0.0, 100.0); + let actual = percent_clamped; + let _ = is_dark; + let pace_color = if expected < actual { + Color::from_hex("#E53935") // red — ahead of pace, may exhaust quota early + } else { + Color::from_hex("#43A047") // green — behind pace, headroom to ramp up + }; + + if pace_solid { + if (actual - expected).abs() > 0.01 { + let (band_lo, band_hi) = if actual > expected { + (expected, actual) + } else { + (actual, expected) + }; + let band_brush = CreateSolidBrush(COLORREF(pace_color.to_colorref())); + 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 overlap_start = band_lo.max(seg_start); + let overlap_end = band_hi.min(seg_end); + if overlap_end <= overlap_start { + continue; + } + + let frac_start = (overlap_start - seg_start) / segment_percent; + let frac_end = (overlap_end - seg_start) / segment_percent; + let fill_left = seg_x + (seg_w as f64 * frac_start) as i32; + let fill_right = seg_x + (seg_w as f64 * frac_end) as i32; + if fill_right <= fill_left { + continue; + } + + let band_rect = RECT { + left: fill_left, + top: y, + right: fill_right, + bottom: y + seg_h, + }; + let rgn = CreateRoundRectRgn( + seg_x, + y, + seg_x + seg_w + 1, + y + seg_h + 1, + corner_r * 2, + corner_r * 2, + ); + let _ = SelectClipRgn(hdc, rgn); + FillRect(hdc, &band_rect, band_brush); + let _ = SelectClipRgn(hdc, HRGN::default()); + let _ = DeleteObject(rgn); + } + let _ = DeleteObject(band_brush); + } + } else { + let seg_idx = + ((expected / segment_percent).floor() as i32).clamp(0, segment_count - 1); + let frac_in_seg = + (expected - (seg_idx as f64) * segment_percent) / segment_percent; + let seg_x = bar_x + seg_idx * (seg_w + seg_gap); + let tick_center = seg_x + (seg_w as f64 * frac_in_seg).round() as i32; + + let bar_left = bar_x; + let bar_right = bar_x + segment_count * (seg_w + seg_gap) - seg_gap; + let tick_w = sc(3).max(2); + let tick_left = (tick_center - tick_w / 2).clamp(bar_left, bar_right - tick_w); + + let tick_rect = RECT { + left: tick_left, + top: y, + right: tick_left + tick_w, + bottom: y + seg_h, + }; + let tick_brush = CreateSolidBrush(COLORREF(pace_color.to_colorref())); + FillRect(hdc, &tick_rect, tick_brush); + let _ = DeleteObject(tick_brush); + } + } + let text_x = bar_x + segment_count * (seg_w + seg_gap) - seg_gap + sc(BAR_RIGHT_MARGIN); let mut text_wide: Vec = text.encode_utf16().collect(); let mut text_rect = RECT { left: text_x, top: y, - right: text_x + sc(TEXT_WIDTH), + right: text_x + current_text_width(), bottom: y + seg_h, }; let _ = SetTextColor(hdc, COLORREF(text_color.to_colorref()));