use std::borrow::Cow;
use std::path::{Path, PathBuf};
const QUALIFIER: &str = "com";
const ORG: &str = "whycorporation";
const APP: &str = "whycodes";
const PROJECT_DIR: &str = ".whycodes";
const INSTANCE_DIR: &str = ".whycodes";
pub fn whycodes_home() -> Option<PathBuf> {
let raw = std::env::var_os("WHYCODES_HOME")?;
if raw.is_empty() {
None
} else {
Some(PathBuf::from(raw))
}
}
pub fn user_home() -> Option<PathBuf> {
std::env::var_os("HOME")
.filter(|v| !v.is_empty())
.or_else(|| std::env::var_os("USERPROFILE").filter(|v| !v.is_empty()))
.map(PathBuf::from)
}
pub fn instance_root() -> PathBuf {
if let Some(home) = whycodes_home() {
return home;
}
or_dot(user_home().map(|h| h.join(INSTANCE_DIR)))
}
pub fn data_dir() -> PathBuf {
instance_root()
}
pub fn config_dir() -> PathBuf {
instance_root()
}
pub fn config_file() -> PathBuf {
config_dir().join("config.toml")
}
pub fn legacy_instance_roots() -> Vec<PathBuf> {
legacy_roots_from(
directories::ProjectDirs::from(QUALIFIER, ORG, APP).map(|d| {
(
d.config_dir().to_path_buf(),
d.data_local_dir().to_path_buf(),
)
}),
)
}
pub(crate) fn legacy_roots_from(dirs: Option<(PathBuf, PathBuf)>) -> Vec<PathBuf> {
match dirs {
Some((config, data)) => unique_legacy_roots(config, data),
None => Vec::new(),
}
}
pub(crate) fn unique_legacy_roots(config: PathBuf, data: PathBuf) -> Vec<PathBuf> {
if config == data {
vec![config]
} else {
vec![config, data]
}
}
pub(crate) fn or_dot(p: Option<PathBuf>) -> PathBuf {
p.unwrap_or_else(|| PathBuf::from("."))
}
pub fn project_dir(working_dir: &Path) -> PathBuf {
working_dir.join(PROJECT_DIR)
}
pub fn project_scratch_dir(working_dir: &Path) -> PathBuf {
project_dir(working_dir).join("scratch")
}
pub fn project_rule_dirs(working_dir: &Path) -> [(PathBuf, &'static str); 4] {
[
(project_dir(working_dir).join("rules"), "md"),
(working_dir.join(".cursor").join("rules"), "mdc"),
(working_dir.join(".clinerules"), "md"),
(working_dir.join(".github").join("instructions"), "md"),
]
}
pub fn display_path(path: &Path) -> String {
strip_windows_verbatim_prefix(&path.to_string_lossy()).into_owned()
}
pub(crate) fn strip_windows_verbatim_prefix(s: &str) -> Cow<'_, str> {
const VERBATIM: &str = r"\\?\";
const UNC: &str = r"UNC\";
let Some(rest) = s.strip_prefix(VERBATIM) else {
return Cow::Borrowed(s);
};
if let Some(unc) = rest.strip_prefix(UNC) {
let mut out = String::from(r"\\");
out.push_str(unc);
return Cow::Owned(out);
}
let b = rest.as_bytes();
if b.len() >= 2 && b[0].is_ascii_alphabetic() && b[1] == b':' {
return Cow::Borrowed(rest);
}
Cow::Borrowed(s)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn display_strip_project_dir_and_or_dot() {
assert_eq!(
strip_windows_verbatim_prefix(r"\\?\C:\foo").as_ref(),
r"C:\foo"
);
assert_eq!(
strip_windows_verbatim_prefix(r"\\?\d:\bar").as_ref(),
r"d:\bar"
);
assert_eq!(
strip_windows_verbatim_prefix(r"\\?\UNC\srv\share").as_ref(),
r"\\srv\share"
);
assert_eq!(
strip_windows_verbatim_prefix(r"\\?\pipe\name").as_ref(),
r"\\?\pipe\name"
);
assert_eq!(strip_windows_verbatim_prefix("/tmp/a").as_ref(), "/tmp/a");
assert_eq!(display_path(Path::new("/tmp/a")), "/tmp/a");
assert_eq!(project_dir(Path::new("/w")), PathBuf::from("/w/.whycodes"));
assert_eq!(
project_scratch_dir(Path::new("/w")),
PathBuf::from("/w/.whycodes/scratch")
);
let rules = project_rule_dirs(Path::new("/w"));
assert_eq!(rules[0], (PathBuf::from("/w/.whycodes/rules"), "md"));
assert_eq!(rules[1], (PathBuf::from("/w/.cursor/rules"), "mdc"));
assert_eq!(rules[2], (PathBuf::from("/w/.clinerules"), "md"));
assert_eq!(rules[3], (PathBuf::from("/w/.github/instructions"), "md"));
assert_eq!(or_dot(None), PathBuf::from("."));
assert_eq!(or_dot(Some(PathBuf::from("/x"))), PathBuf::from("/x"));
assert_eq!(config_file().file_name().unwrap(), "config.toml");
let roots = legacy_instance_roots();
assert!(
roots.len() <= 2,
"legacy roots are config and/or data: {roots:?}"
);
assert_eq!(
unique_legacy_roots(PathBuf::from("/a"), PathBuf::from("/a")),
vec![PathBuf::from("/a")]
);
assert_eq!(
unique_legacy_roots(PathBuf::from("/a"), PathBuf::from("/b")),
vec![PathBuf::from("/a"), PathBuf::from("/b")]
);
assert!(legacy_roots_from(None).is_empty());
}
}