1use std::borrow::Cow;
16use std::path::{Path, PathBuf};
17
18const QUALIFIER: &str = "com";
19const ORG: &str = "whycorporation";
20const APP: &str = "whycodes";
21const PROJECT_DIR: &str = ".whycodes";
22const INSTANCE_DIR: &str = ".whycodes";
23
24pub fn whycodes_home() -> Option<PathBuf> {
26 let raw = std::env::var_os("WHYCODES_HOME")?;
27 if raw.is_empty() {
28 None
29 } else {
30 Some(PathBuf::from(raw))
31 }
32}
33
34pub fn user_home() -> Option<PathBuf> {
36 std::env::var_os("HOME")
37 .filter(|v| !v.is_empty())
38 .or_else(|| std::env::var_os("USERPROFILE").filter(|v| !v.is_empty()))
39 .map(PathBuf::from)
40}
41
42pub fn instance_root() -> PathBuf {
44 if let Some(home) = whycodes_home() {
45 return home;
46 }
47 or_dot(user_home().map(|h| h.join(INSTANCE_DIR)))
48}
49
50pub fn data_dir() -> PathBuf {
52 instance_root()
53}
54
55pub fn config_dir() -> PathBuf {
57 instance_root()
58}
59
60pub fn config_file() -> PathBuf {
62 config_dir().join("config.toml")
63}
64
65pub fn legacy_instance_roots() -> Vec<PathBuf> {
68 legacy_roots_from(
69 directories::ProjectDirs::from(QUALIFIER, ORG, APP).map(|d| {
70 (
71 d.config_dir().to_path_buf(),
72 d.data_local_dir().to_path_buf(),
73 )
74 }),
75 )
76}
77
78pub(crate) fn legacy_roots_from(dirs: Option<(PathBuf, PathBuf)>) -> Vec<PathBuf> {
79 match dirs {
80 Some((config, data)) => unique_legacy_roots(config, data),
81 None => Vec::new(),
82 }
83}
84
85pub(crate) fn unique_legacy_roots(config: PathBuf, data: PathBuf) -> Vec<PathBuf> {
86 if config == data {
87 vec![config]
88 } else {
89 vec![config, data]
90 }
91}
92
93pub(crate) fn or_dot(p: Option<PathBuf>) -> PathBuf {
94 p.unwrap_or_else(|| PathBuf::from("."))
95}
96
97pub fn project_dir(working_dir: &Path) -> PathBuf {
99 working_dir.join(PROJECT_DIR)
100}
101
102pub fn project_scratch_dir(working_dir: &Path) -> PathBuf {
104 project_dir(working_dir).join("scratch")
105}
106
107pub fn project_rule_dirs(working_dir: &Path) -> [(PathBuf, &'static str); 4] {
111 [
112 (project_dir(working_dir).join("rules"), "md"),
113 (working_dir.join(".cursor").join("rules"), "mdc"),
114 (working_dir.join(".clinerules"), "md"),
115 (working_dir.join(".github").join("instructions"), "md"),
116 ]
117}
118
119pub fn display_path(path: &Path) -> String {
126 strip_windows_verbatim_prefix(&path.to_string_lossy()).into_owned()
127}
128
129pub(crate) fn strip_windows_verbatim_prefix(s: &str) -> Cow<'_, str> {
132 const VERBATIM: &str = r"\\?\";
133 const UNC: &str = r"UNC\";
134 let Some(rest) = s.strip_prefix(VERBATIM) else {
135 return Cow::Borrowed(s);
136 };
137 if let Some(unc) = rest.strip_prefix(UNC) {
138 let mut out = String::from(r"\\");
139 out.push_str(unc);
140 return Cow::Owned(out);
141 }
142 let b = rest.as_bytes();
143 if b.len() >= 2 && b[0].is_ascii_alphabetic() && b[1] == b':' {
144 return Cow::Borrowed(rest);
145 }
146 Cow::Borrowed(s)
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
154 fn display_strip_project_dir_and_or_dot() {
155 assert_eq!(
156 strip_windows_verbatim_prefix(r"\\?\C:\foo").as_ref(),
157 r"C:\foo"
158 );
159 assert_eq!(
160 strip_windows_verbatim_prefix(r"\\?\d:\bar").as_ref(),
161 r"d:\bar"
162 );
163 assert_eq!(
164 strip_windows_verbatim_prefix(r"\\?\UNC\srv\share").as_ref(),
165 r"\\srv\share"
166 );
167 assert_eq!(
168 strip_windows_verbatim_prefix(r"\\?\pipe\name").as_ref(),
169 r"\\?\pipe\name"
170 );
171 assert_eq!(strip_windows_verbatim_prefix("/tmp/a").as_ref(), "/tmp/a");
172 assert_eq!(display_path(Path::new("/tmp/a")), "/tmp/a");
173 assert_eq!(project_dir(Path::new("/w")), PathBuf::from("/w/.whycodes"));
174 assert_eq!(
175 project_scratch_dir(Path::new("/w")),
176 PathBuf::from("/w/.whycodes/scratch")
177 );
178 let rules = project_rule_dirs(Path::new("/w"));
179 assert_eq!(rules[0], (PathBuf::from("/w/.whycodes/rules"), "md"));
180 assert_eq!(rules[1], (PathBuf::from("/w/.cursor/rules"), "mdc"));
181 assert_eq!(rules[2], (PathBuf::from("/w/.clinerules"), "md"));
182 assert_eq!(rules[3], (PathBuf::from("/w/.github/instructions"), "md"));
183 assert_eq!(or_dot(None), PathBuf::from("."));
184 assert_eq!(or_dot(Some(PathBuf::from("/x"))), PathBuf::from("/x"));
185 assert_eq!(config_file().file_name().unwrap(), "config.toml");
186 let roots = legacy_instance_roots();
187 assert!(
188 roots.len() <= 2,
189 "legacy roots are config and/or data: {roots:?}"
190 );
191 assert_eq!(
192 unique_legacy_roots(PathBuf::from("/a"), PathBuf::from("/a")),
193 vec![PathBuf::from("/a")]
194 );
195 assert_eq!(
196 unique_legacy_roots(PathBuf::from("/a"), PathBuf::from("/b")),
197 vec![PathBuf::from("/a"), PathBuf::from("/b")]
198 );
199 assert!(legacy_roots_from(None).is_empty());
200 }
201}