ureq is built without socks-proxy, so an ALL_PROXY=socks5h entry failed
every request and a socks5 entry silently connected directly, while also
suppressing the Windows system proxy. Pick the first HTTP or HTTPS proxy
from the environment ourselves, keep NO_PROXY on it, and fall back to the
system proxy when none is usable.
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.
Poll results, threshold crossings and the alert cooldown now return
values instead of calling the tray, so they run under unit tests. The
bubble and panel payloads carry every display string and colour from
Rust, and the context menu is a plain tree with string ids that parse
back into commands. The update-check schedule and countdown delay are
plain functions of the stored times.
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.
The bundler has no icon fallback, so an empty list breaks the AppImage
step and ships a deb without hicolor icons. Put icon.png first so it stays
the default window icon on Linux and macOS, and bundle the deb in the
Linux CI row to catch a broken list before release.
Windows and Ubuntu 22.04 run fmt, clippy, tests, lint, the JS tests and
a no-bundle release build. The Windows row checks the exe version
resource and uploads the exe as a 7-day artifact. macOS runs cargo check
as the compile gate.
@tauri-apps/cli is a local devDependency. ESLint runs the recommended
and JSDoc rules, with browser globals for ui/ and node globals for tests
and scripts. npm test runs node --test over tests/js.
Turn the package into a lib plus thin bin on Tauri v2 with a root-level
tauri.conf.json, capabilities, generated icons (keeping the hand-tuned
icon.ico) and a placeholder bubble page. The Win32 modules leave the
module tree until each one is ported; their sources stay on disk.
tauri-build now embeds the icon, a VERSIONINFO from the Cargo version and
a PerMonitorV2 manifest, replacing res/icon.rc and its stale 0.1.0.
Inner ring's consumed wedge now grows clockwise from 12 o'clock (mirroring
a clock-hand countdown) and the tail remaining-time bar shrinks toward the
right edge instead of the left. Usage ring and weekly usage bar unchanged.
- Lift inner time ring above WCAG 1.4.11 (track #303030 -> #404040,
stroke floor 1 -> 2 logical, ring gap 3 -> 4).
- Breathe tail bar/text with bar_text_gap=8 (was pad=6); right inset
12 -> 14 logical so text clears the stadium end-cap.
- Reweight typography: head percent FW_BOLD, "5H" tag and tail percent
FW_SEMIBOLD, tail countdown stays normal but takes muted color so
the percent reads as the headline.
- Tone down usage track (#3A3A3A/#D6D6D6 -> #2C2C2C/#E2E2E2) so fill
dominates at low percentages.
- Differentiate lane mass: usage bar 9%/5..12 -> 10%/6..12, time bar
5%/3..7 -> 4%/3..6, lane gap 5 -> 6. Time bar now reads as supporting
context, not a competing quota.
- Min-fill guard on weekly bar: sub-cap fills floor at one cap-diameter
so 1% renders as a recognizable dot.
- head_pad 4 -> 5; big-font ratio 26% -> 24% of head diameter (BOLD
compensates for the size cut).
v0.3.0 introduced a tail 7d% reading but the layout-collapse guard
fired at every common bubble configuration — at the default 200-logical
size on both 100% and 125% DPI, after reserving the CJK worst-case
countdown column ("999시간") and the "100%" text rect, the tail had
less than 20 logical of bar room left. The guard collapsed the % rect
to zero width and the paint code's `if rect.right > rect.left` skip
ran on every frame, so the feature was effectively dead on arrival
for the majority of users.
The 20-logical bar minimum was the pre-feature bar floor, used to
guarantee a readable bar at very small bubble sizes. It does not need
to apply when the % is shown — the % is the actual data and the bar
becomes secondary visual context. Split into two thresholds:
- `bar_min_with_pct = 8 logical` decides whether the % can fit. With
8 logical of bar room the bar still renders as a short pill.
- `bar_min = 20 logical` only applies on the fallback (140-logical
minimum bubble) path where the % has been dropped — preserving
the pre-feature readable-bar behavior at the smallest size.
The bar's render floor now follows the active path (`bar_render_min`)
so a thin bar in the pct-active case does not overlap the countdown.
The stadium bubble previously dropped the 5h reset countdown (only the
ring + percent were visible in the head) and never showed the 7d percent
as a number (only the tail bar fill suggested it). Two more glanceable
data points now live on the bubble face without reopening the panel.
Head: the small "5h" tag is replaced by the live 5h countdown (e.g.
"2h14m"). Falls back to the literal "5h" when no countdown is available
yet (cold start) or when the localized string would overflow the rect —
DT_NOCLIP would otherwise leak wide CJK glyphs ("4시간 32분") onto the
ring stroke at the 140-logical minimum width.
Tail: a new "X%" reading sits between the "7d" label and the bar
(layout reads "7d 62% ▰▰▰▰▰▱▱▱ 6d4h"). Foreground text color —
not the bar accent — because Codex teal #10A37F on the light theme
background only hits ~3.2:1 contrast, below WCAG AA for small text;
adjacency to the bar carries the visual grouping without hue. The text
brightens in sync with the bar fill when weekly_pct >= 95%.
compute_bubble_layout reserves room for a "100%"-sized rect between
label and bar; if that would push the bar below the 20-logical
minimum, the % rect collapses to zero width and the layout falls back
to the original label→bar→countdown geometry, so the 140-logical
bubble keeps its bar.
No new graphics dependencies; tiny-skia + GDI hybrid render path
unchanged. session_text plumbing in src/app.rs was already wired but
unused in the render — now consumed.
cargo check: clean. cargo test: 2/2. cargo clippy: 13 warnings
(unchanged baseline).
User report on v0.1.14: text appears semi-transparent, desktop wallpaper
bleeds through glyph pixels.
Root cause: GDI's DrawTextW writes only RGB into 32bpp BI_RGB DIBs — the
"reserved" alpha byte (byte 3) is not preserved per the BITMAPINFOHEADER
contract. When UpdateLayeredWindow later composites with AC_SRC_ALPHA, it
reads alpha=0 at every glyph pixel and shows them as fully transparent.
The pre-v0.1.13 pipeline worked around this with an apply_alpha_mask
post-pass that OR'd 0xFF000000 into every pixel inside the rounded rect.
The stadium-shape rewrite (526786b) removed it on the false assumption
that tiny-skia's per-pixel alpha would "stick" through subsequent GDI
writes — but GDI runs *after* tiny-skia in the pipeline, so any pixel
GDI text writes to loses the alpha that tiny-skia set.
Fix: re-stamp the alpha channel from the original Pixmap after the GDI
text overlay. This restores tiny-skia's exact alpha values (255 in the
stadium interior, partial on the AA curved perimeter, 0 outside),
including the AA fade at the stadium's rounded ends.
Implementation:
- new helper `restore_alpha_from_pixmap(pixmap, dst)` next to the
existing `copy_pixmap_to_dib`
- hoist `pixmap` out of the if-let arm in render() so it survives until
after `paint_bubble_text`
- call `restore_alpha_from_pixmap` post-text
Two parallel reviewers (debugger + code-reviewer) converged on the same
diagnosis; the debugger preferred this approach for its simplicity and
because it's robust to any GDI behavior (whether alpha is zeroed,
untouched, or scribbled on, we overwrite with the known-good value).
Build clean.
User feedback on v0.1.13: design works, but the 5h percent glyph in the
head crowds the ring at small bubble sizes (the "100%" string was wider
than the ring's inner clear at MIN_BUBBLE_SIZE).
Two parallel UI/UX reviewers converged on:
- big_font_px ratio: head_diameter × 26/100 (was 35/100), floor 11
- small_font_px ratio: big × 55/100 (was 45/100), floor 9
- head_pad: 4 logical px (was 6) — recovers 4px
of inner clear at small sizes
- ring_stroke_w: clamped to [2, 4] (was floor 2 only)
- label/glyph gap: big × 15/100, floor 2 (was implicit 0)
- tail_bar_h: 5 logical px (was 6) — restores
proportion against the 3-px head ring stroke
Worked example at MIN_BUBBLE_SIZE=140 (head_diameter=47):
before: "100%" glyph ≈ 32px wide vs 28px ring inner — overflow
after: "100%" glyph ≈ 23px wide vs 32px ring inner — comfortable
Worked example at MAX_BUBBLE_SIZE=360 (head_diameter=138):
glyph ≈ 70px wide in 124px inner clear (~57%) — confident not crowding
Deliberately not applying:
- drop "7d" label (one reviewer wanted it): rejected — symmetry with
"5h" matters for self-explanation at a glance, and the ~14px cost is
acceptable
- head_diameter bump to canvas_h × 1.08: rejected — only useful coupled
with the label drop
Build clean.