use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Default)]
pub struct Env {
pub os: Os,
pub home: Option<PathBuf>,
pub appdata: Option<PathBuf>,
pub local_appdata: Option<PathBuf>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Os {
Windows,
Mac,
#[default]
Other,
}
impl Env {
pub fn current() -> Env {
let os = if cfg!(windows) {
Os::Windows
} else if cfg!(target_os = "macos") {
Os::Mac
} else {
Os::Other
};
let var = |k: &str| std::env::var_os(k).map(PathBuf::from);
Env {
os,
home: var("HOME").or_else(|| var("USERPROFILE")),
appdata: var("APPDATA"),
local_appdata: var("LOCALAPPDATA"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Candidate {
pub path: PathBuf,
pub why: &'static str,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Place {
Fixed {
path: PathBuf,
marker: PathBuf,
why: &'static str,
},
Packaged {
parent: PathBuf,
prefix: &'static str,
suffix: &'static [&'static str],
why: &'static str,
},
}
pub fn resolve(places: &[Place]) -> Vec<Candidate> {
let mut out = Vec::new();
for place in places {
match place {
Place::Fixed { path, marker, why } => {
if marker.is_dir() {
out.push(Candidate {
path: path.clone(),
why,
});
}
}
Place::Packaged {
parent,
prefix,
suffix,
why,
} => {
let Ok(entries) = std::fs::read_dir(parent) else {
continue;
};
let mut found: Vec<PathBuf> = entries
.flatten()
.filter(|e| e.file_name().to_string_lossy().starts_with(*prefix))
.filter(|e| e.path().is_dir())
.map(|e| suffix.iter().fold(e.path(), |p, part| p.join(part)))
.collect();
found.sort();
out.extend(found.into_iter().map(|path| Candidate { path, why }));
}
}
}
out
}
pub fn claude_desktop(env: &Env) -> Vec<Place> {
const FILE: &str = "claude_desktop_config.json";
match env.os {
Os::Windows => {
let mut places = Vec::new();
if let Some(local) = &env.local_appdata {
places.push(Place::Packaged {
parent: local.join("Packages"),
prefix: "Claude_",
suffix: &["LocalCache", "Roaming", "Claude", FILE],
why: "the Microsoft Store build reads this one; the app's own Edit \
Config button does not open it",
});
}
if let Some(appdata) = &env.appdata {
let dir = appdata.join("Claude");
places.push(Place::Fixed {
path: dir.join(FILE),
marker: dir,
why: "the installer build reads this one, and it is what Edit Config \
opens on any build",
});
}
places
}
Os::Mac => {
let Some(home) = &env.home else {
return Vec::new();
};
let dir = home
.join("Library")
.join("Application Support")
.join("Claude");
vec![Place::Fixed {
path: dir.join(FILE),
marker: dir,
why: "where Claude Desktop keeps its settings on macOS",
}]
}
Os::Other => {
let Some(home) = &env.home else {
return Vec::new();
};
let dir = home.join(".config").join("Claude");
vec![Place::Fixed {
path: dir.join(FILE),
marker: dir,
why: "where a Linux build of Claude Desktop keeps its settings",
}]
}
}
}
pub fn codex_config(env: &Env) -> Option<PathBuf> {
let home = env.home.as_ref()?;
let dir = home.join(".codex");
dir.is_dir().then(|| dir.join("config.toml"))
}
pub fn claude_code(project: &Path) -> PathBuf {
project.join(".claude").join("settings.json")
}