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.
Replaces the "build from source only" Install section with a
Releases-page download path plus a SmartScreen note, and adds
docs/release-process.md so the tag-bump-build flow is captured for
future maintainers.
When the bubble is hidden (manually via "Show widget" menu or
automatically by the fullscreen detector) and then re-shown, the
layered window's composited surface had already been dropped — and
ShowWindow(SW_SHOWNOACTIVATE) alone doesn't issue a new
UpdateLayeredWindow. The bubble reappeared blank and stayed that way
until the next poll cycle triggered a render.
The BubbleState's session/weekly pcts + texts never went anywhere, so
the fix is to call render() right after the show — both in
set_user_visible (manual toggle) and in check_fullscreen (auto-show
when leaving fullscreen).
Two issues reported on the inline-percent design:
1. "Same color as bar" — for Codex at safe levels the fill is white
(theme-adapted from the accent stripe colour) and the
luminance-based contrast switch wasn't landing reliably across the
fill/track boundary. White percent text disappeared into white fill.
2. "Not centered" — the percent anchored to the fill's trailing edge,
so its horizontal position changed with the bar's percentage. Read
more like a label on the fill than a centered numeric readout.
Fix: move the percent out of the bar entirely into its own column
between the bar and the countdown. Layout flow per row is now:
[accent] [5h] [bar ▓▓▓░░░░] [44%] [3h]
This puts the two numeric readouts ("44%" and "3h") side-by-side for
quick scanning while keeping the bar purely visual. The percent text
now renders on the bubble background — predictable, high-contrast in
both modes — and the entire luminance-contrast / fill-vs-track branch
is gone (`draw_inline_percent` replaced with a much shorter
`draw_percent`).
The percent column width is sized off the live font via
`GetTextExtentPoint32W("100%", ...)` so it always reserves exactly
enough room.
Bar usable width at default 200×66 drops from ~120 px to ~96 px
(stable across Claude vs Codex since the bar no longer holds text).
Two visible regressions from the previous pass:
1. Codex bars still rendered orange even though the accent stripe was
white — `ring_color_for_percent` only knew the percentage, not the
provider, so it returned the Claude-style orange/amber/red ramp for
both models. Replaced with `bar_fill_color(model, is_dark, pct)`:
- <60% → accent_color_for(model, is_dark) (Claude orange / Codex
white-on-dark / Codex charcoal-on-light)
- 60-80% → amber (#E0A040)
- 80-95% → red (#C45020)
- ≥95% → deep red (#A01818)
Alarm bands stay model-agnostic so an approaching limit always looks
the same. The "safe" band carries the provider's identity. Mirrored
into the panel via `bar_color_for(model, percent, is_dark)`.
2. Big visible gap between bar end and countdown text — the column
was sized for the old combined "100% · 23h" probe string (~9 chars)
but the bubble now renders countdown-only (~3 chars). Probe template
shrunk to "999d" so the right column is no longer ~70 px reserved
for ~14 px of text. Countdown also flipped from DT_RIGHT to DT_LEFT
so "3h" / "19h" sit immediately next to the bar with just the
`label_pad` gap, instead of floating at the bubble's far edge.
Inline-percent contrast calc uses `bar_fill_color` so the
luminance-based foreground colour responds to the new white/charcoal
Codex bars (black-on-white in safe band, white-on-amber/red in alarm).
Side effect: `ring_color_for_percent` is gone — it had no internal
callers outside the two replaced sites. `bar_fill_color` is now the
only public bar-color helper.
Codex stripe is white in dark mode and charcoal (#2A2A2A) in light mode
— a pure white would vanish into the light-mode #F3F3F3 background, so
the light-mode variant keeps the same monochrome-vs-orange identity
without disappearing. Mirrored into the expanded panel.
Inline percent now anchors to the fill's trailing edge instead of the
bar's far-right. Two modes:
- Fill is wide enough (fill_w >= text_w + 2*inset): right-align the
percent *inside* the fill at its trailing edge. Text sits on the
fill colour.
- Fill is too narrow: left-align the percent just past the fill on
the track. Text follows the fill's edge.
Previously the percent was always right-aligned to bar_right - inset,
which detached it from the fill at low percentages — e.g. at 42% the
"42%" floated near the bar's right end while the fill stopped at
~42% of bar width. Now the percent rides the fill so the visual
relationship between number and bar length is immediate.
Text width measured via GetTextExtentPoint32W against the currently
selected font so the anchoring decision uses the actual rendered
width, not a heuristic.
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)