Commit Graph

36 Commits

Author SHA1 Message Date
tiennm99 f96d88074a chore: bump version to 0.1.12 v0.1.12 2026-05-23 11:23:14 +07:00
tiennm99 e243c589a8 refactor: fix latent bugs, invert bubble→app deps, unify color path
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).
2026-05-23 11:22:40 +07:00
tiennm99 a5dec52aaa feat(ui): unify usage colors, fix per-bar coloring, round panel corners
Phase 0 of UI/UX polish pass. Surgical changes, no substrate migration yet.

- Extract bar_fill_color + accent_color_for into new src/usage_color.rs so
  the bubble, panel, and tray badge agree on a single 4-band usage ramp.
- Panel: color each bar from its own percent (was using max(5h, 7d) for
  both rows, so a healthy 5h bar turned red whenever 7d was full).
- Light-mode amber #B47A20 (was #E0A040, failed WCAG AA at 2.4:1).
- Codex identity: switch from white/charcoal to OpenAI teal #10A37F
  across bubble, panel stripe, and tray sweep so the surfaces share one
  brand color and the tray badge stops reading as "loading spinner".
- Panel: drop WS_BORDER, add DwmSetWindowAttribute(DWMWCP_ROUND) for
  Win11 rounded corners. Idempotent re-apply on every show() so the
  attribute survives any future destroy/recreate path. Silently no-ops
  on Win10.
2026-05-23 09:18:42 +07:00
tiennm99 4e0f32591b chore: bump version to 0.1.11 v0.1.11 2026-05-21 16:51:46 +07:00
tiennm99 3e1af07ec2 feat(i18n): switch supported languages to en/ja/ko/vi/zh-TW
Drop nl/es/fr/de locales (no native-speaker maintenance) and add
Vietnamese. The supported set is now the languages with active
users we can support: English, Japanese, Korean, Vietnamese, and
Traditional Chinese.
2026-05-21 16:51:15 +07:00
tiennm99 27aa935a9b chore: bump version to 0.1.10 v0.1.10 2026-05-21 16:29:05 +07:00
tiennm99 1cd5b778f4 feat(update): replace cmd.exe handoff with native Win32 spawn
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).
2026-05-21 16:28:25 +07:00
tiennm99 1ba2883989 ci: switch back to windows-latest
Prefer staying on the floating tag — accept GitHub's auto-redirect to
windows-2025-vs2026 in mid-2026 rather than pin and chase.
2026-05-18 13:23:37 +07:00
tiennm99 858d7f1139 chore: bump version to 0.1.9 v0.1.9 2026-05-18 11:28:26 +07:00
tiennm99 5a2e4f1c60 fix(menu): repaint cached data and poll after Reset Position
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.
2026-05-18 11:28:26 +07:00
tiennm99 713eb5bbde ci: bump actions to latest, pin windows runner
- actions/checkout v4 -> v6 (Node 24, addresses Node 20 deprecation).
- runs-on windows-latest -> windows-2025 (current image; pin avoids
  silent surprise when GitHub redirects windows-latest to a newer
  image mid-2026).
- Swatinem/rust-cache@v2 retained (v2 floating tag still maintained;
  latest is v2.9.1 under that major).
2026-05-18 11:00:39 +07:00
tiennm99 bcce939f72 chore: bump version to 0.1.8 v0.1.8 2026-05-18 10:46:16 +07:00
tiennm99 e089a1b420 docs(reports): code-reviewer report for restart-button impl
DONE_WITH_CONCERNS — flagged M1 (match-arm vs IDM_LANG_BASE guard) and
L3 (lock-during-save). Both addressed in the feat commit.
2026-05-18 10:39:33 +07:00
tiennm99 457d5274da docs(plans): record menu-restart-button plan
Plan + phase-01. Plan-context decisions: placement above Exit, no confirm
dialog, cmd-handoff mechanism reused from update::install.
2026-05-18 10:39:29 +07:00
tiennm99 f1dfe15000 feat(menu): add Restart action between separator and Exit
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.
2026-05-18 10:39:19 +07:00
tiennm99 38ae4dff09 docs(reports): add reviews for offscreen-bubble fix
Code-reviewer flagged the clamp-before-render ordering nit; brainstormer
ranked the layered (validate + clamp) approach over topology-hash / per-monitor
pinning alternatives.
2026-05-18 09:43:35 +07:00
tiennm99 3c0878f6cc fix(bubble): recover off-screen position from disconnected monitor
Saved bubble_positions could land on a secondary monitor that was later
disconnected, leaving the bubble created off-screen with no visual feedback
on toggle-show.

- settings::load now drops any position whose 140px probe rect intersects
  no connected monitor (MonitorFromRect + MONITOR_DEFAULTTONULL).
- bubble::create calls clamp_into_work_area before the first render as a
  defense-in-depth catch for partial overflows or load/create monitor races.
- clamp_into_work_area preserves the Codex-above-Claude stagger from
  default_position when both bubbles get clamped to the same corner.
- Added info/warn log lines on create + clamp paths so future visibility
  bugs are diagnosable via --diagnose.
2026-05-18 09:43:27 +07:00
tiennm99 eca430ccc6 feat(menu): show current version on the Check-for-Updates entry
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.
v0.1.7
2026-05-16 14:10:02 +07:00
tiennm99 2791022e7a chore: drop dead ureq + native-tls deps
The legacy poller.rs and updater.rs modules they served were deleted
in the clean-room rewrite; everything now goes through net::winhttp.
Grep confirms no source references to either crate. Removing them
trims the dep graph noticeably.

Bumps version to 0.1.6.
v0.1.6
2026-05-16 13:59:18 +07:00
tiennm99 ed9b8b2042 feat: threshold balloons + dark/light auto-follow
- 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.
v0.1.5
2026-05-16 13:57:15 +07:00
tiennm99 8ad718d9c1 docs(reports): add code-reviewer / brainstormer / researcher reports
Advisory artifacts produced by the three agents spawned to audit the
project. Code-reviewer found the P0/P1 set fixed in 1ef1bfa;
brainstormer ranked feature ideas; researcher surveyed the
self-update / code-signing / distribution space.
v0.1.4
2026-05-16 13:51:25 +07:00
tiennm99 1ef1bfa7b2 fix: phase 2 — UI-freeze + GDI-leak + panic-on-GDI-exhaustion fixes
- 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.
2026-05-16 13:51:16 +07:00
tiennm99 0f3acd40d4 feat(update): SHA-256 verification + reject paths containing '%'
GitHub's Releases API exposes a `digest: "sha256:..."` field on every
asset since 2024. We now parse it, hash the downloaded bytes locally,
and abort with ChecksumMismatch if they disagree. Releases that predate
the field (none currently exist for this repo) skip verification rather
than fail, so v0.1.0 / v0.1.1 / v0.1.2 still update normally.

cmd.exe expands `%var%` even inside double-quoted arguments, which
would let a path like `C:\Users\%PATHEXT%\bubble.exe` substitute the
expansion. Real Windows paths with `%` are vanishingly rare, so we
fail fast with UnsafePath rather than ship a bespoke cmd-escape
implementation.

Bumps version to 0.1.3.
v0.1.3
2026-05-16 13:37:37 +07:00
tiennm99 a132c02711 fix(update): bypass Rust arg escaping in cmd.exe handoff
Rust's std::process::Command escapes inner double quotes as \" when
wrapping the args(["/c", &cmd]) array. cmd.exe does not understand the
\" escape, so the swap-and-restart command got mangled: `start ""
"PATH"` arrived as `start \"\" \"PATH\"`, which the cmd parser
collapsed into `start \ PATH` — producing the "Windows cannot find
'\'" dialog and aborting the update.

Switching to raw_arg lets us hand cmd.exe the literal command line it
expects. The two quote characters cmd needs to keep are the outer pair
wrapping the whole /c argument; cmd's "more than two quotes, special
chars present" branch then preserves the inner path quotes intact.

Bumps version to 0.1.2 since this is the first updater fix that ships
through the updater itself for any future v0.1.2+ user.
v0.1.2
2026-05-16 13:04:08 +07:00
tiennm99 ca9ab4ea97 chore: bump version to 0.1.1 v0.1.1 2026-05-16 12:58:18 +07:00
tiennm99 2ca5052915 feat: configurable auto-update check frequency
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.
2026-05-16 12:57:50 +07:00
tiennm99 5fc9d18c67 docs: link install to releases page, add release-process guide
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.
v0.1.0
2026-05-16 12:34:38 +07:00
tiennm99 60cde291bd ci: add windows release workflow 2026-05-16 12:34:26 +07:00
tiennm99 7f8ccf083b fix(bubble): repaint on un-hide so cached data shows immediately
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).
2026-05-16 11:58:41 +07:00
tiennm99 df45157316 fix(bubble): move percent out of bar, next to countdown
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).
2026-05-16 11:55:41 +07:00
tiennm99 6f5fb3fc80 fix(bubble): provider-tinted bar fill, tighten countdown gap
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.
2026-05-16 11:47:06 +07:00
tiennm99 be8efef068 fix(bubble): codex accent → white, percent rides fill edge
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.
2026-05-16 11:39:15 +07:00
tiennm99 5df75c901e feat(bubble): apply full UX review — labels, inline percent, accent, snaps
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
2026-05-16 11:35:02 +07:00
tiennm99 ee4e1c9b26 feat: redesign bubble as two-bar rounded rect, finish port wiring
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)
2026-05-16 11:11:34 +07:00
tiennm99 aa6217d2cf feat: clean-room rewrite — replace ported modules with original implementations
Every Rust module under src/ that previously contained upstream-derivative
code has been replaced by a from-scratch implementation:

  diag/    log + simplelog file appender (was: diagnose.rs)
  os/      color, dpi, registry, string, theme (was: theme.rs, native_interop.rs)
  net/     WinHTTP-based HTTP client (was: ureq + native-tls)
  i18n/    TOML-embedded locale tables (was: localization/*.rs)
  usage/   trait UsageProvider + ClaudeProvider + ChatGptProvider + refresh
           orchestrator + registry (was: poller.rs, models.rs)
  creds/   trait CredentialSource + local/WSL/Codex impls (was: poller.rs)
  tray/    stateless tray manager + tiny-skia anti-aliased badge renderer
           (was: tray_icon.rs)
  update/  release fetch + inline cmd /c handoff installer
           (was: updater.rs's helper-exe pattern)

Application files (app.rs, bubble.rs, panel.rs, settings.rs) migrated to
the new modules. main.rs declares only the new modules.

NOTICE deleted; LICENSE is plain Apache-2.0; README updated to credit
inspiration rather than claim derivation. Cargo.toml drops ureq + native-tls
+ winres in favour of log + simplelog + thiserror + toml + tiny-skia +
embed-resource. Build script swapped to embed-resource via res/icon.rc.

External contracts preserved unchanged: Anthropic + ChatGPT endpoints and
headers, ~/.claude/.credentials.json + Codex auth.json paths, WSL bridging
via wsl.exe, CLI-driven token refresh, GitHub Releases JSON shape, Windows
registry path for startup, single-instance mutex name.

Phase docs: plans/260516-0707-cleanroom-rewrite/.
2026-05-16 10:09:43 +07:00
tiennm99 c0f3e3f860 feat: initial port of claude-code-usage-monitor as a floating bubble
Windows-only floating, draggable circular bubble showing Claude Code
and Codex usage. Derivative of CodeZeno/Claude-Code-Usage-Monitor (MIT),
relicensed under Apache 2.0 with upstream attribution in NOTICE.

- Ported verbatim: poller, updater, tray_icon, theme, localization,
  diagnose, models (~2,700 LOC)
- Original: bubble (circular layered window, drag-anywhere via
  WM_NCHITTEST+HTCAPTION, snap-to-edge, Ctrl+Wheel resize, auto-hide
  on fullscreen), panel (expanded 5h/7d view), app (orchestrator,
  single-instance mutex, polling thread, context menu, dual-bubble
  lifecycle), settings (settings.json persistence)
- Cargo.toml features cover Win32 GDI, HiDpi, Registry, Threading,
  Shell, WindowsAndMessaging, and KeyboardAndMouse
2026-05-15 21:27:31 +07:00