Whole-project review pass over the entire crate. No new features.
All function definitions preserved; layout and visibility reorganised.
Bug fixes:
- bubble: ExtractIconExW HICON pair was leaked per bubble toggle.
Extract once into a process-wide OnceLock, reuse forever (bounded).
- usage/anthropic: parse_iso8601 was stripping the UTC offset without
applying it — negative-offset users saw countdowns up to 14h wrong.
Now parses signed minutes and computes utc_secs = local - off*60.
Also rejects y<1970, mo∉[1,12], d∉[1,max_day(mo,y)] up front so
malformed API responses can't index DAYS_IN_MONTH out of bounds.
- usage: clamp utilization to [0,100] at all four Window construction
sites so a misbehaving server can't render "121%".
- bubble: GetDC and CreateCompatibleDC results weren't checked. Guard
both; release the screen DC on the CreateCompatibleDC failure path.
Refactor:
- Drop type TrayIconKind = ProviderId aliases (5 sites); use ProviderId
directly everywhere. Inline the identity-function kind_to_provider.
- Delete panel::bar_color_for shim (was just argument-reorder glue).
- Replace local scale_to_dpi fns in bubble.rs and panel.rs with
use crate::os::dpi::scale as scale_to_dpi (brings os::dpi into the
live import graph; was unused before).
- Delete dead PCWSTR import + #[allow(dead_code)] sentinel in
tray/badge.rs; fold the trailing `use BOOL` into the top imports.
- Inline app::primary_dpi() to crate::os::dpi::for_system().
app.rs:
- Add update_settings(|s: &mut AppState|) helper that locks state,
runs the closure, snapshots Settings, drops the lock, then saves
to disk. Convert four pure-mutate-then-save callsites.
Layering: bubble.rs no longer reaches upward into crate::app::.
Introduce bubble::Callbacks (fn-pointers), OnceLock<Callbacks>, and
bubble::install_callbacks(). The wnd_proc dispatches the six prior
upward calls via a private dispatch() helper. app::run installs
callbacks once at startup; the six on_bubble_* / recheck_theme fns
are demoted from pub fn to fn.
Resource-warning logs added: dispatch() warns on uninstalled
callbacks; app_icons() warns when ExtractIconExW returns nulls.
Build: cargo build --release clean; cargo clippy reports zero new
warnings (11 pre-existing, all in untouched code).
Both the in-app restart and the auto-update install previously
shelled out to cmd.exe so the new instance could wait for the old
one to release the singleton mutex and the locked exe file. On
some Windows configurations the `start ""` inside `cmd /c ...` can
flash a console window despite CREATE_NO_WINDOW + DETACHED_PROCESS
flags. The replacement spawns the child binary directly via
CreateProcessW; since the main exe is built with
windows_subsystem = "windows", no console is ever allocated.
- New `src/update/handoff.rs` exposes `spawn_detached`,
`wait_for_parent_exit`, and `cleanup_stale_old_exes`.
- New CLI flags `--wait-pid <pid>` and `--updated-to <version>`
parsed early in `main`; the child waits up to 5s on the parent
PID via OpenProcess+WaitForSingleObject before falling through
to a 3s mutex-acquisition retry.
- `restart_app` and `install::begin` both spawn detached children
using the new helper.
- Update install now uses MoveFileExW twice (rename running exe
sideways, then move staged exe into place with
MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED so portable
installs on non-system drives still work). Rollback restores
the backup if either the swap OR the post-swap detached spawn
fails, and a MessageBoxW modal surfaces the backup path if the
rollback itself fails.
- First launch after an auto-update shows a blue-info tray
balloon "Updated to vX.Y.Z" via a new `tray::notify_info` (the
existing `tray::notify` is split into `notify_warning` +
`notify_info` sharing a `notify_inner`).
- Startup sweeps stale `<exe>.old.<pid>` siblings left by past
in-place updates.
- Three new `LocaleStrings` fields translated across all 8
supported locales (en/nl/es/fr/de/ja/ko/zh-TW).
reset_positions() destroys and recreates the bubbles, which leaves them
displaying the spawn_bubble "…" placeholder until the next 5-minute
TIMER_POLL fires. Push the cached snapshot via propagate_to_ui() so the
last-known values appear immediately, and kick spawn_poll_thread() (idempotent
via POLL_IN_FLIGHT gate) so fresh data follows shortly after.
One-click relaunch of the running binary via a detached cmd.exe handoff:
timeout /t 1 /nobreak >/dev/null & start "" "<exe>" — the 1s wait outlives the
parent so the relaunched instance acquires Global\ClaudeCodeUsageBubble
without ERROR_ALREADY_EXISTS.
- New IDM_RESTART (33) wired into show_context_menu + on_menu_command.
- Match arm placed above the IDM_LANG_BASE guard so future ids in the
static band can't be swallowed by the dynamic-language catch-all.
- Settings flushed defensively before quit (clone-then-save to avoid
blocking the UI thread on disk I/O while holding lock_state).
- Rejects current_exe paths containing '%' (same defense as
update::install — cmd.exe expands %var% inside quotes).
- New 'restart' string in LocaleStrings + translation in all 8 locales.
Appends ' · v{CARGO_PKG_VERSION}' to every state of the version-action
menu item so users can see what they are running without opening an
About dialog. Reads as 'Check for updates · v0.1.7', 'Up to date ·
v0.1.7', etc. Winget channel decoration is preserved as a trailing
parenthetical.
Bumps version to 0.1.7.
- Threshold balloons fire the cycle utilization crosses 80% or 95%
on either provider, with the title showing "{Provider} · {N}%" and
body translated per shipped locale. Reuses the existing
BALLOON_COOLDOWN so notifications stay calm.
- Dark/light auto-follow: bubble's WM_SETTINGCHANGE handler now calls
app::recheck_theme(), which re-reads HKCU\…\Personalize, updates
state.is_dark if changed, and triggers a UI repaint + tray refresh.
Windows posts WM_SETTINGCHANGE to every top-level window when the
user toggles light/dark in Settings, so the bubble repaints in
near-real-time.
- Adds 2 new i18n keys (threshold_80_body, threshold_95_body) across
all eight shipped locales.
Bumps version to 0.1.5.
- P0: pull blocking HTTPS out from under the global mutex. AppState's
http and registry now live behind Arc<Client> and Arc<Mutex<Registry>>;
do_poll, attempt_refresh, and version_action's Apply branch clone
these out, drop lock_state, then do their I/O. Apply now spawns a
worker thread that posts WM_APP_UPDATE_APPLIED back to the message-
only window when the cmd handoff is launched, so the UI no longer
freezes for the duration of the download.
- P1: bubble.rs paint_text_layer saves and restores the DC's previous
HFONT before DeleteObject. The old code's DeleteObject on a still-
selected HFONT silently failed and leaked one handle per paint frame
(up to ~12/s under the ≥95% pulse animation).
- P1: replace 5x CreatePopupMenu().unwrap() with let-else early returns
that destroy any half-built menus and log. GDI exhaustion no longer
panics the UI thread.
- P1: at-most-one-in-flight gate (static AtomicBool) on the poll thread
so rapid Refresh clicks don't stack concurrent HTTPS calls.
- P1: token-expired balloon now picks the title/body for the provider
that actually failed, instead of always falling back to Claude when
show_claude_code is on.
- P1: panel place_near honors SM_XVIRTUALSCREEN / SM_YVIRTUALSCREEN so
multi-monitor setups with a secondary display left of the primary
no longer mis-clamp the panel position.
- P1: COUNTDOWN_TEMPLATE bumped from "999d" to "999시간" — Korean has
the widest suffix among shipped locales and was overflowing the
countdown column.
Bumps version to 0.1.4.
Adds a "Settings > Auto-update check" submenu with Disabled / Hourly /
Daily / Weekly. Hourly is the default; existing settings files pick it
up automatically via serde default. Manual "Check for updates" is
unchanged and still fires when auto is disabled.
The 24-hour hardcoded interval is replaced by reading
Settings.update_check_interval_secs in both the startup scheduler and
the post-check rearm path. None means auto is disabled and no timer is
armed.
Adds five new i18n keys across all eight locales.
Implements every recommendation from the UI/UX review of the two-bar
bubble: readability bump, faster glanceability, per-provider identity,
smarter resize curve, and a richer snap behavior set.
Readability + sizing:
- Default width 180 → 200, min 120 → 140 (Windows shell minimum legible
status-text floor is 12 px; previous breakpoint produced 11 px text)
- Discrete breakpoint table replaces the linear bar_h = h/4 formula —
140/200/280/360 widths map to (bar_h, font, row_gap) of
(12,11,4)/(16,13,6)/(20,15,8)/(24,17,10)
- Aspect tapers 3:1 → 2.8:1 → 2.6:1 as width grows so bars stay
proportionally chunky at large sizes
- Right-column width now derived from GetTextExtentPoint32W of
"100% · 23h" against the real font instead of the bar_h * 6 heuristic
Glanceability:
- ring_color_for_percent rewritten as a 4-band cliff: orange < 60,
amber 60-80, red 80-95, deep red ≥ 95 — gradient hid the 75 vs 85
difference at narrow bar widths
- Percent moved inside the bar (right-aligned with auto-contrast picked
from luminance of the underlying fill or track)
- Right column now shows only the countdown ("2h 14m"), not the
combined "X% · 2h 14m" string. i18n::format_countdown is now pub so
the bubble can request countdown-only; the panel still renders the
combined string via format_window
- "5h" / "7d" muted labels added in a new left column
- Rows whose bar is ≥ 95 % get a 15 %-red blush behind the entire row;
fill color also brightens via a slow triangle wave (TIMER_PULSE,
80 ms, only armed when at least one bar is in the alarm band)
Per-provider identity:
- 4-px vertical accent stripe at the left edge — Claude orange
(#D97757), Codex green (#10A37F) — disambiguates the two bubbles
when both are enabled. Mirrored into the expanded panel so the
identity carries across surfaces
Snap behavior:
- Corner snap: release within 32 px of a work-area corner slams the
bubble into the corner with a 12-px inset
- Taskbar-adjacency: SHAppBarMessage(ABM_GETTASKBARPOS) supplies the
taskbar rect/edge; bubble docks against the inner face with a
4-px gap on the docked edge
- Peer-Y alignment: when a second bubble exists within ±8 px on Y
at release time, the dragged bubble snaps to share its top edge
- WM_SETTINGCHANGE handler re-clamps bubbles into the new work area
when taskbar moves / auto-hides / DPI changes
Color packing:
- Direct DIB writes use rgb_to_dib (B,G,R,X memory order) instead of
into_colorref (R,G,B,X COLORREF order). Already corrected in the
previous redesign for the bar fills, but the accent stripe and blush
go through the same path now
Bubble shape changes from circle to rounded rectangle showing two stacked
horizontal bars — top: session (5h), bottom: weekly (7d) — each followed
by a right-aligned "X% · Yh Zm" string (percent + countdown).
Bubble surface:
- BubbleConfig/BubbleState carry session+weekly percents and texts (mirrors
PanelData); update_percentage renamed to update_data
- Aspect ratio fixed at 3:1; size_logical is interpreted as width with
height derived. Clamp is 120..360 (was 32..128 square)
- Hit-testing uses a rounded-rect predicate (point_in_rounded_rect) shared
with the alpha mask so paint and click area can't drift
- New rgb_to_dib helper for direct DIB writes — BI_RGB 32bpp stores B,G,R,X
in memory which is the opposite of COLORREF. The previous code wrote
COLORREF-packed u32 straight into DIB pixels; invisible while every color
was gray, but the new orange/red bar fills would have rendered blue
- bar_h capped at h/4 (range 6..18) so the text font derived from it stays
small enough that "100% · 23h" fits in right_text_w (= 6×bar_h, min 56);
the first iteration had a 19-px font in a 60-px column and ellipsized
away the countdown
- Initial session_text/weekly_text seeded with "…" so the bubble has
visible feedback during the first poll instead of two empty grey tracks
Compile + cleanup needed to make the port build at all:
- Color::from_hex added back as an infallible wrapper around parse_hex
(15 call sites in bubble.rs/panel.rs assumed the old infallible API)
- Color::to_colorref → into_colorref at 5 call sites
- GetModuleFileNameW added to the LibraryLoader import in bubble.rs
- usage::Error gains Creds(#[from] creds::Error) so `?` works in the
Anthropic and ChatGPT providers
- FlattenBoxed::flatten renamed to flatten_box — the std Option::flatten
was shadowing it and yielding Option<Box<T>> instead of Option<T>
- PanelState marked unsafe Send (HWND has *mut c_void; state is only
touched from the UI thread, Mutex is for OnceLock satisfaction)
- Crate-level #![allow(dead_code)] for in-progress port API surface
(creds, usage, update, os::dpi); unused pub-use re-exports removed
App wiring:
- propagate_to_ui now feeds both windows + their formatted texts into
update_data (was a single percent)