OpenCode Go never had a usage source, so the provider list, its colours,
label, window lengths, default position and settings fields go. With it
goes the live/non-live split it introduced: every provider is now live,
and toggling still keeps at least one enabled. Saved settings that carry
the old OpenCode Go keys still load; the keys are dropped on next save.
The Claude and Codex fetches now take their endpoint URLs as arguments,
with poll supplying the real ones, so tests can point them at the local
one-shot server. The tests pin the current behaviour: usage auth
failures stop before the Messages fallback, server errors and unparsable
bodies fall back to Messages, partial bodies get their reset times
merged, Messages auth failures and missing rate-limit headers are
reported, Codex 401 and 500 map to auth-required and bad-response, and
the account header is sent only when an account id is set.
A GUI app starts with its working directory at the home folder, / or
System32, and `codex exec .` refuses to run there because it is not a
trusted directory or repository. Both CLIs now start from the temp
directory so neither runs inside a project or loads its instructions and
hooks, and Codex runs with --skip-git-repo-check in a read-only sandbox.
The orchestrator takes its spawn step as an argument internally, so its
refreshed, timeout and missing-CLI outcomes are now tested with a fake
credential source, along with the locator's source order and the Claude
credential file signature.
Replace the WinHTTP client with a ureq agent that keeps the same
Client/Response surface: status errors come back as responses, gzip,
60 s connect / 30 s receive / 120 s total timeouts, rustls with the
platform verifier and a per-request User-Agent override.
Environment proxies come from ureq. On Windows, when none is set, the
static Internet Settings proxy is read from HKCU and parsed by a pure
core module (per-scheme ProxyServer, ProxyOverride globs and <local>);
bypassed hosts go through a direct agent. PAC/WPAD is not evaluated.
The usage providers and credential sources compile again on every
platform. Refresh keeps the Windows candidates and CREATE_NO_WINDOW;
Unix spawns claude/codex from PATH plus the usual per-user install
directories and reaps the child on a detached thread. An empty
CODEX_HOME now counts as unset and a float expiresAt is truncated
instead of ignored.
Fixture tests cover the OAuth usage body, rate-limit headers (including
a 429 over loopback), the ChatGPT envelope, ISO 8601 edge cases,
credential JSON and proxy strings. An ignored live_poll test polls the
real providers.
Settings, the English strings, the colour ramp and the bubble geometry no
longer touch Win32 types, so they compile and test on every platform.
Snapping, clamping, default placement, panel placement and the monitor
probe take plain rects; settings loading is split into parsing and an
offscreen check fed with the monitor list. ProviderId now serialises as
its slug.
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).
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)