mirror of
https://github.com/tiennm99/claude-code-usage-bubble.git
synced 2026-10-20 00:15:09 +00:00
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/.
11 KiB
11 KiB
phase, status, estimated_hours
| phase | status | estimated_hours |
|---|---|---|
| 1 | pending | 9 |
Phase 1 — Infrastructure
Context links
- Brainstorm:
../reports/brainstorm-260516-0707-cleanroom-reimplementation.md(axes 1, 2, 9, 10 + os/) - Source files to be REPLACED later:
src/diagnose.rs,src/theme.rs,src/native_interop.rs(do not delete this phase — Phase 2+ depends on them until then)
Overview
- Priority: Critical (every other phase depends on these primitives)
- Status: pending
- Brief: Stand up the foundation modules — logging, Win32 helpers, WinHTTP client, and the build-script swap — without touching any business logic. End of this phase:
cargo build --releasesucceeds with the new modules compiled in but not yet referenced byapp.rs(so behavior is unchanged).
Key insights from brainstorm
- WinHTTP via
windows-rs::Win32::Networking::WinHttpremovesureq+native-tls(~600 KB binary savings) and respects the system proxy automatically. - WinHTTP is verbose. Encapsulate in a single ~150 LOC
net::winhttp::Clientso the rest of the codebase stays clean. log+simplelogreplaces the bespokeOnceLock<Mutex<File>>logger. Standard ecosystem, same one-line ergonomics.embed-resourcereplaceswinres. Same purpose, different file shape (res/icon.rcinstead of builder API).os/directory consolidates color, wide-string, DPI, registry, theme helpers (currently scattered acrosstheme.rsandnative_interop.rs).
Requirements
Functional
diag::init(enabled: bool)writes a log file at%TEMP%\claude-code-usage-bubble.logwhen called withtrue; no-op otherwise.log::info!/log::warn!/log::error!macros work and route to the file.os::wide_str(&str) -> Vec<u16>returns a NUL-terminated UTF-16 vector.os::Colorprovides hex parsing + COLORREF conversion.os::dpi::for_window(hwnd)returns u32 DPI ≥ 96.os::registry::read_string(hkey, path, name)returns Option.os::registry::write_string(hkey, path, name, value)returns Result.os::registry::delete_value(hkey, path, name)returns Result.os::theme::is_dark()returns bool from registry.net::winhttp::Client::new()constructs a client with a user-agent string.client.get(url).header(k, v).send()returnsResult<Response, net::Error>.client.post(url).header(k, v).json_body(value).send()returnsResult<Response, net::Error>.Response::status() -> u32,Response::header(&str) -> Option<&str>,Response::text() -> Result<String, net::Error>,Response::json<T>() -> Result<T, net::Error>.- Build script (
build.rs) embedsres/icon.icoand version info viaembed-resource.
Non-functional
- Binary size after this phase: ≤ current (we add
tiny-skia/embed-resource/simplelog/thiserrordeps in later phases; this phase should not balloon). - No new behavior changes vs current build.
- All new modules pass
cargo clippy -- -W clippy::allwith zero new warnings.
Architecture
diag/mod.rs (~30 LOC)
use std::path::PathBuf;
use simplelog::{Config, LevelFilter, WriteLogger};
use std::fs::File;
pub fn init(enabled: bool) -> Result<Option<PathBuf>, std::io::Error> {
if !enabled { return Ok(None); }
let path = std::env::temp_dir().join("claude-code-usage-bubble.log");
WriteLogger::init(LevelFilter::Debug, Config::default(), File::create(&path)?)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
log::info!("diagnostic logging enabled");
Ok(Some(path))
}
Callers use log::info! etc directly — no diagnose::log(...) indirection.
os/ directory
| File | Responsibility | LOC |
|---|---|---|
mod.rs |
pub use re-exports + module declarations |
~10 |
color.rs |
Color { r, g, b }, from_hex, to_colorref() |
~30 |
string.rs |
wide_str(&str) -> Vec<u16> |
~5 |
dpi.rs |
for_window(hwnd), for_system(), scale(logical, dpi) |
~20 |
registry.rs |
typed wrapper over RegOpenKeyExW/RegQueryValueExW/RegSetValueExW/RegDeleteValueW |
~80 |
theme.rs |
is_dark() → reads SystemUsesLightTheme via os::registry |
~15 |
net/winhttp.rs (~150 LOC)
pub struct Client {
session: HINTERNET,
user_agent: Vec<u16>,
}
pub struct RequestBuilder<'a> {
client: &'a Client,
method: Method,
url: Url,
headers: Vec<(Vec<u16>, Vec<u16>)>,
body: Option<Vec<u8>>,
}
pub struct Response {
status: u32,
headers: HashMap<String, String>,
body: Vec<u8>,
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("WinHTTP error {code}: {context}")]
Win(u32, String),
#[error("HTTP {status}")]
Status(u32),
#[error("JSON parse: {0}")]
Json(#[from] serde_json::Error),
#[error("invalid URL: {0}")]
Url(String),
#[error("UTF-8 conversion failed")]
Utf8,
}
Internally chains: WinHttpOpen → WinHttpCrackUrl → WinHttpConnect → WinHttpOpenRequest → WinHttpSendRequest → WinHttpReceiveResponse → WinHttpQueryHeaders → WinHttpReadData.
Cargo.toml updates
[dependencies]
# REMOVED: ureq, native-tls
serde = { version = "1", features = ["derive"] }
serde_json = "1"
dirs = "6"
log = "0.4"
simplelog = "0.12"
thiserror = "2"
[dependencies.windows]
version = "0.58"
features = [
# existing features +
"Win32_Networking_WinHttp",
]
[build-dependencies]
# REMOVED: winres
embed-resource = "3"
build.rs
fn main() {
embed_resource::compile("res/icon.rc", embed_resource::NONE);
}
res/icon.rc (new file)
#include <winver.h>
1 ICON "..\\src\\icons\\icon.ico"
1 VERSIONINFO
FILEVERSION 0,1,0,0
PRODUCTVERSION 0,1,0,0
FILEOS 0x40004L
FILETYPE 0x1L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904E4"
BEGIN
VALUE "ProductName", "Claude Code Usage Bubble\0"
VALUE "FileDescription", "Claude Code Usage Bubble\0"
VALUE "OriginalFilename", "claude-code-usage-bubble.exe\0"
VALUE "InternalName", "ClaudeCodeUsageBubble\0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1252
END
END
Related code files
To create:
src/diag/mod.rssrc/os/mod.rssrc/os/color.rssrc/os/string.rssrc/os/dpi.rssrc/os/registry.rssrc/os/theme.rssrc/net/mod.rssrc/net/winhttp.rsres/icon.rc
To modify:
Cargo.toml(add new deps, removeureq+native-tls+winres, addWin32_Networking_WinHttp)build.rs(replace winres with embed-resource)src/main.rs(addmod diag; mod os; mod net;declarations — do NOT remove oldmod diagnose; mod theme; mod native_interop;yet)
To delete: nothing in this phase (Phase 2 removes theme.rs, etc.)
Implementation steps
- Add new dependencies to
Cargo.toml:log,simplelog,thiserror,embed-resource. AddWin32_Networking_WinHttptowindowsfeatures. Leaveureq,native-tls,winresin place for now. - Create
res/icon.rcreferencingsrc/icons/icon.ico. - Replace
build.rswithembed-resource::compile. cargo build --release— verify icon embedding works (PE has icon resource).- Create
src/os/string.rswithwide_str(). Trivial. - Create
src/os/color.rswithColorstruct +from_hex+to_colorref. - Create
src/os/registry.rswithread_string,read_u32,write_string,delete_value—unsafewrappers over Win32 registry APIs returningResult<T, RegistryError>. - Create
src/os/theme.rscallingregistry::read_u32(HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", "SystemUsesLightTheme")and inverting. - Create
src/os/dpi.rswithfor_window,for_system,scale. - Create
src/os/mod.rswithpub mod color; pub mod string; pub mod dpi; pub mod registry; pub mod theme;+ re-exports of common items (Color,wide_str). - Create
src/diag/mod.rswithinit(bool)that initialises simplelog. Addlog::set_max_levelif not handled by simplelog. - Create
src/net/winhttp.rsin 4 commits:- 12a.
Client::new,DropforWinHttpCloseHandle. - 12b.
RequestBuilder+ GET path. - 12c. POST + JSON body.
- 12d.
Response::header+Response::text+Response::json.
- 12a.
- Create
src/net/mod.rswithpub mod winhttp;+pub use winhttp::{Client, Error, Response};. - Wire modules into
src/main.rs:mod diag; mod net; mod os; // OLD ones still present for now: mod diagnose; mod theme; mod native_interop; cargo build --release— must compile clean. Binary should run identically to current build (we haven't replaced anything yet).cargo clippy --release— fix any clippy warnings on new modules.- Manual smoke test on Windows (or note as deferred to Phase 4 testing):
- Run
--diagnose, confirm log file exists at%TEMP%\claude-code-usage-bubble.log(but it's empty for now since nothing callslog::info!yet — that's OK). - Run a tiny dev-only test binary or
cargo testthat exercisesnet::winhttp::Client.get("https://api.github.com").send()to validate the HTTP wrapper end-to-end.
- Run
Todo checklist
- Cargo.toml deps updated
res/icon.rccreatedbuild.rsswapped to embed-resource- Build produces .exe with embedded icon
src/os/string.rssrc/os/color.rssrc/os/registry.rssrc/os/theme.rssrc/os/dpi.rssrc/os/mod.rssrc/diag/mod.rssrc/net/winhttp.rs(incremental, 4 sub-commits)src/net/mod.rssrc/main.rsdeclares new modulescargo build --releasecleancargo clippyclean- WinHTTP smoke test passes against
api.github.com
Success criteria
- Phase ends with a runnable binary that behaves exactly like the previous version (no business logic changes).
net::winhttp::Clientcan successfully GEThttps://api.github.com/repos/tiennm99/claude-code-usage-bubble/releases/latestand parse JSON.log::info!("test")from anywhere writes to%TEMP%\claude-code-usage-bubble.logwhen--diagnoseis passed.- Binary size: not larger than current (we've removed
ureq+native-tls, added smaller crates).
Risks + mitigations
| Risk | Likelihood | Mitigation |
|---|---|---|
| WinHTTP TLS handshake fails on older Windows | Low | Test on Win10/Win11; WinHTTP supports TLS 1.2+ since Win10 1607 |
WinHttpCrackUrl is awkward; URL parsing has edge cases |
Medium | Use url crate (~50 KB) for parsing, then pass components to WinHTTP |
embed-resource doesn't match winres's VERSIONINFO output exactly |
Low | Verify with mt /inspect output.exe |
log + simplelog collide with another logger init |
None | App owns the only init |
| Chunked transfer / compression auto-decode disabled | Medium | Set WINHTTP_OPTION_DECOMPRESSION flag |
Security considerations
- WinHTTP enforces certificate validation by default — don't disable.
registrymodule writes only toHKEY_CURRENT_USER(user-scoped). No admin escalation.- Log file path is
%TEMP%— user-scoped. No secrets logged (verify in Phase 4 when adding token-handling logs).
Next steps
→ Phase 2: replace models.rs + localization/* with usage/types.rs + i18n/ directory.