use std::path::{Path, PathBuf};
use std::sync::mpsc;
use std::time::Duration;
const CLAUDE_MARKER: char = '\u{273b}';
pub(crate) fn claude_json_path() -> Option<PathBuf> {
claude_json_path_from(
std::env::var("CLAUDE_CONFIG_DIR").ok().as_deref(),
dirs::home_dir().as_deref(),
)
}
fn claude_json_path_from(config_dir: Option<&str>, home: Option<&Path>) -> Option<PathBuf> {
match config_dir {
Some(dir) if !dir.trim().is_empty() => Some(PathBuf::from(dir).join(".claude.json")),
_ => home.map(|h| h.join(".claude.json")),
}
}
#[derive(serde::Deserialize)]
struct ClaudeJson {
#[serde(default, rename = "oauthAccount")]
oauth_account: Option<OauthAccount>,
}
#[derive(serde::Deserialize)]
struct OauthAccount {
#[serde(default, rename = "emailAddress")]
email_address: String,
}
pub(crate) fn account_segment_from_path(path: &Path) -> Option<String> {
let raw = std::fs::read_to_string(path).ok()?;
let parsed: ClaudeJson = serde_json::from_str(&raw).ok()?;
let email = parsed.oauth_account?.email_address;
render_claude_account_segment(Some(&email))
}
fn render_claude_account_segment(email: Option<&str>) -> Option<String> {
let email = email?.trim();
if email.is_empty() {
return None;
}
Some(format!("{CLAUDE_MARKER}{email}"))
}
pub(crate) const ACCOUNT_PROBE_BUDGET: Duration = Duration::from_millis(100);
pub(crate) fn claude_account_segment_probe(
path: Option<&Path>,
budget: Duration,
) -> Option<String> {
let path = path?.to_path_buf();
bounded_probe(budget, move || account_segment_from_path(&path))
}
fn bounded_probe<F>(budget: Duration, read: F) -> Option<String>
where
F: FnOnce() -> Option<String> + Send + 'static,
{
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
let _ = tx.send(read());
});
rx.recv_timeout(budget).ok().flatten()
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write as _;
fn temp_json(body: &str) -> tempfile::NamedTempFile {
let mut f = tempfile::NamedTempFile::new().expect("temp file");
f.write_all(body.as_bytes()).expect("write");
f.flush().expect("flush");
f
}
#[test]
fn account_segment_renders_email_from_config() {
let f = temp_json(
r#"{
"numStartups": 412,
"oauthAccount": {
"accountUuid": "0000-1111",
"emailAddress": "someone@example.com",
"organizationName": "someone@example.com's Organization",
"organizationRole": "admin"
},
"projects": {}
}"#,
);
assert_eq!(
account_segment_from_path(f.path()).as_deref(),
Some("\u{273b}someone@example.com")
);
}
#[test]
fn account_segment_absent_when_file_missing() {
let dir = tempfile::tempdir().expect("temp dir");
let missing = dir.path().join("does-not-exist.json");
assert_eq!(account_segment_from_path(&missing), None);
}
#[test]
fn account_segment_absent_when_json_is_malformed() {
let f = temp_json(r#"{"oauthAccount": {"emailAddress": "trunc"#);
assert_eq!(account_segment_from_path(f.path()), None);
let f = temp_json(r#"{"numStartups": 1}"#);
assert_eq!(account_segment_from_path(f.path()), None);
}
#[test]
fn account_segment_absent_when_email_is_blank() {
let f = temp_json(r#"{"oauthAccount": {"emailAddress": " "}}"#);
assert_eq!(account_segment_from_path(f.path()), None);
let f = temp_json(r#"{"oauthAccount": {"organizationName": "Acme"}}"#);
assert_eq!(account_segment_from_path(f.path()), None);
}
#[test]
fn bounded_probe_omits_segment_when_read_outruns_budget() {
let (release_tx, release_rx) = mpsc::channel::<()>();
let (done_tx, done_rx) = mpsc::channel();
std::thread::spawn(move || {
let got = bounded_probe(Duration::from_millis(1), move || {
let _ = release_rx.recv();
Some("\u{273b}late@example.com".to_string())
});
let _ = done_tx.send(got);
});
let got = done_rx
.recv_timeout(Duration::from_secs(30))
.expect("probe must return when its budget lapses, not wait for the reader");
assert_eq!(got, None, "a timed-out read must omit the segment");
drop(release_tx);
}
#[test]
fn render_claude_account_segment_single() {
let seg = render_claude_account_segment(Some("someone@example.com")).expect("segment");
assert_eq!(seg, "\u{273b}someone@example.com");
assert!(!seg.starts_with('@'), "must not read as a gh login: {seg}");
}
#[test]
fn render_claude_account_segment_absent() {
assert_eq!(render_claude_account_segment(None), None);
assert_eq!(render_claude_account_segment(Some("")), None);
assert_eq!(render_claude_account_segment(Some(" \t ")), None);
}
#[test]
fn claude_json_path_prefers_config_dir() {
let resolved = claude_json_path_from(Some("/cfg"), Some(Path::new("/home/user")));
assert_eq!(resolved.as_deref(), Some(Path::new("/cfg/.claude.json")));
}
#[test]
fn claude_json_path_falls_back_to_home() {
let home = Path::new("/home/user");
for cfg in [None, Some(""), Some(" ")] {
assert_eq!(
claude_json_path_from(cfg, Some(home)).as_deref(),
Some(Path::new("/home/user/.claude.json")),
"cfg={cfg:?}"
);
}
assert_eq!(claude_json_path_from(None, None), None);
}
}