mirror of
https://github.com/tiennm99/claude-code-usage-bubble.git
synced 2026-06-06 20:12:56 +00:00
aa6217d2cf
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/.
112 lines
3.7 KiB
Rust
112 lines
3.7 KiB
Rust
// Pluggable credential discovery.
|
|
//
|
|
// Each `CredentialSource` knows how to read a single OAuth token from
|
|
// somewhere (a local JSON file, a WSL filesystem, …). The `Locator`
|
|
// holds a priority-ordered list and serves the first source that
|
|
// actually has a token. New sources drop in without touching the locator.
|
|
|
|
pub mod codex_auth;
|
|
pub mod local_fs;
|
|
pub mod wsl_bridge;
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct Token {
|
|
pub access_token: String,
|
|
/// Expiry timestamp in *milliseconds* since Unix epoch, matching the
|
|
/// format the Claude CLI writes. `None` means "the source didn't say".
|
|
pub expires_at_unix_ms: Option<i64>,
|
|
pub account_id: Option<String>,
|
|
}
|
|
|
|
/// Tells `RefreshOrchestrator` which CLI to spawn when the token rotates.
|
|
#[derive(Clone, Debug)]
|
|
pub enum RefreshHint {
|
|
/// `claude.cmd` / `claude.exe` on PATH.
|
|
LocalClaudeCli,
|
|
/// Run `claude -p .` inside a specific WSL distro.
|
|
WslClaudeCli { distro: String },
|
|
/// `codex` / `codex.cmd` / `codex.ps1` on PATH.
|
|
LocalCodexCli,
|
|
}
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum Error {
|
|
#[error("io: {0}")]
|
|
Io(#[from] std::io::Error),
|
|
#[error("invalid JSON: {0}")]
|
|
Json(#[from] serde_json::Error),
|
|
#[error("required field missing from credential JSON: {0}")]
|
|
MissingField(&'static str),
|
|
#[error("WSL command in {distro:?} failed: {detail}")]
|
|
WslCommand { distro: String, detail: String },
|
|
#[error("timeout while talking to WSL")]
|
|
WslTimeout,
|
|
#[error("credential source unavailable")]
|
|
Unavailable,
|
|
}
|
|
|
|
pub trait CredentialSource: Send + Sync {
|
|
/// Stable identifier used in logs and the locator's change-detection
|
|
/// signatures (e.g. `"local:C:\\Users\\me\\.claude\\.credentials.json"`).
|
|
fn id(&self) -> &str;
|
|
|
|
/// Read the current token. May spawn subprocesses (for WSL).
|
|
fn read(&self) -> Result<Token, Error>;
|
|
|
|
/// Cheap change-detection fingerprint. `None` means "source is missing".
|
|
fn signature(&self) -> Option<String>;
|
|
|
|
fn refresh_hint(&self) -> RefreshHint;
|
|
}
|
|
|
|
/// Ordered set of credential sources. The first source with a valid
|
|
/// `signature()` is treated as the "active" one.
|
|
pub struct Locator {
|
|
sources: Vec<Box<dyn CredentialSource>>,
|
|
}
|
|
|
|
impl Locator {
|
|
pub fn new(sources: Vec<Box<dyn CredentialSource>>) -> Self {
|
|
Self { sources }
|
|
}
|
|
|
|
/// Build a Claude locator with the standard search order: Windows
|
|
/// home directory first, then every installed WSL distro.
|
|
pub fn for_claude() -> Self {
|
|
let mut sources: Vec<Box<dyn CredentialSource>> = Vec::new();
|
|
if let Some(s) = local_fs::LocalClaudeCreds::detect() {
|
|
sources.push(Box::new(s));
|
|
}
|
|
for distro in wsl_bridge::list_distros() {
|
|
sources.push(Box::new(wsl_bridge::WslClaudeCreds::new(distro)));
|
|
}
|
|
Self { sources }
|
|
}
|
|
|
|
/// Build a ChatGPT/Codex locator with the standard search order.
|
|
pub fn for_chatgpt() -> Self {
|
|
let mut sources: Vec<Box<dyn CredentialSource>> = Vec::new();
|
|
if let Some(s) = codex_auth::LocalCodexCreds::detect() {
|
|
sources.push(Box::new(s));
|
|
}
|
|
Self { sources }
|
|
}
|
|
|
|
/// First source whose signature is currently non-None.
|
|
pub fn first_available(&self) -> Option<&dyn CredentialSource> {
|
|
self.sources
|
|
.iter()
|
|
.find(|s| s.signature().is_some())
|
|
.map(Box::as_ref)
|
|
}
|
|
|
|
/// Snapshot of fingerprints for every reachable source — used by the
|
|
/// app to detect credential changes (re-login) between poll cycles.
|
|
pub fn signatures(&self) -> Vec<String> {
|
|
let mut sigs: Vec<String> = self.sources.iter().filter_map(|s| s.signature()).collect();
|
|
sigs.sort();
|
|
sigs.dedup();
|
|
sigs
|
|
}
|
|
}
|