1use std::path::PathBuf;
14
15#[must_use]
29pub fn config_dir(app: &str) -> Option<PathBuf> {
30 config_root(env_lookup).map(|root| root.join(app))
31}
32
33#[must_use]
47pub fn data_dir(app: &str) -> Option<PathBuf> {
48 data_root(env_lookup).map(|root| root.join(app))
49}
50
51#[must_use]
65pub fn state_dir(app: &str) -> Option<PathBuf> {
66 state_root(env_lookup).map(|root| root.join(app))
67}
68
69#[must_use]
81pub fn cache_dir(app: &str) -> Option<PathBuf> {
82 cache_root(env_lookup).map(|root| root.join(app))
83}
84
85pub(super) fn env_lookup(name: &str) -> Option<PathBuf> {
87 std::env::var_os(name).map(PathBuf::from)
88}
89
90pub(super) fn config_root(lookup: impl Fn(&str) -> Option<PathBuf>) -> Option<PathBuf> {
92 let non_empty = |name: &str| lookup(name).filter(|path| !path.as_os_str().is_empty());
93 if cfg!(windows) {
94 return non_empty("APPDATA");
95 }
96 if cfg!(target_os = "macos") {
97 return absolute(non_empty("HOME")).map(|home| home.join("Library").join("Application Support"));
98 }
99 absolute(non_empty("XDG_CONFIG_HOME")).or_else(|| absolute(non_empty("HOME")).map(|home| home.join(".config")))
100}
101
102fn data_root(lookup: impl Fn(&str) -> Option<PathBuf>) -> Option<PathBuf> {
104 let non_empty = |name: &str| lookup(name).filter(|path| !path.as_os_str().is_empty());
105 if cfg!(windows) {
106 return non_empty("LOCALAPPDATA");
107 }
108 if cfg!(target_os = "macos") {
109 return absolute(non_empty("HOME")).map(|home| home.join("Library").join("Application Support"));
110 }
111 absolute(non_empty("XDG_DATA_HOME"))
112 .or_else(|| absolute(non_empty("HOME")).map(|home| home.join(".local").join("share")))
113}
114
115pub(super) fn state_root(lookup: impl Fn(&str) -> Option<PathBuf>) -> Option<PathBuf> {
117 let non_empty = |name: &str| lookup(name).filter(|path| !path.as_os_str().is_empty());
118 if cfg!(windows) {
119 return non_empty("LOCALAPPDATA");
120 }
121 if cfg!(target_os = "macos") {
122 return absolute(non_empty("HOME")).map(|home| home.join("Library").join("Application Support"));
123 }
124 absolute(non_empty("XDG_STATE_HOME"))
125 .or_else(|| absolute(non_empty("HOME")).map(|home| home.join(".local").join("state")))
126}
127
128pub(super) fn cache_root(lookup: impl Fn(&str) -> Option<PathBuf>) -> Option<PathBuf> {
130 let non_empty = |name: &str| lookup(name).filter(|path| !path.as_os_str().is_empty());
131 if cfg!(windows) {
132 return non_empty("LOCALAPPDATA");
133 }
134 if cfg!(target_os = "macos") {
135 return absolute(non_empty("HOME")).map(|home| home.join("Library").join("Caches"));
136 }
137 absolute(non_empty("XDG_CACHE_HOME")).or_else(|| absolute(non_empty("HOME")).map(|home| home.join(".cache")))
138}
139
140pub(super) fn absolute(path: Option<PathBuf>) -> Option<PathBuf> {
144 path.filter(|path| path.is_absolute())
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 fn env(pairs: &'static [(&'static str, &'static str)]) -> impl Fn(&str) -> Option<PathBuf> {
153 move |name: &str| pairs.iter().find(|(key, _)| *key == name).map(|(_, value)| PathBuf::from(value))
154 }
155
156 #[test]
157 fn settings_and_data_are_two_folders_on_unix() {
158 if !cfg!(all(unix, not(target_os = "macos"))) {
159 return;
160 }
161 let home = env(&[("HOME", "/home/ada")]);
162 assert_eq!(config_root(&home), Some(PathBuf::from("/home/ada/.config")));
163 assert_eq!(data_root(&home), Some(PathBuf::from("/home/ada/.local/share")));
164
165 let xdg = env(&[("HOME", "/home/ada"), ("XDG_CONFIG_HOME", "/cfg"), ("XDG_DATA_HOME", "/dat")]);
166 assert_eq!(config_root(&xdg), Some(PathBuf::from("/cfg")));
167 assert_eq!(data_root(&xdg), Some(PathBuf::from("/dat")));
168
169 let relative = env(&[("HOME", "/home/ada"), ("XDG_CONFIG_HOME", "cfg"), ("XDG_DATA_HOME", "dat")]);
171 assert_eq!(config_root(&relative), Some(PathBuf::from("/home/ada/.config")));
172 assert_eq!(data_root(&relative), Some(PathBuf::from("/home/ada/.local/share")));
173
174 let empty = env(&[("HOME", "/home/ada"), ("XDG_DATA_HOME", "")]);
176 assert_eq!(data_root(&empty), Some(PathBuf::from("/home/ada/.local/share")));
177 }
178
179 #[test]
180 fn macos_keeps_both_in_application_support() {
181 if !cfg!(target_os = "macos") {
182 return;
183 }
184 let home = env(&[("HOME", "/Users/ada")]);
185 let support = PathBuf::from("/Users/ada/Library/Application Support");
186 assert_eq!(config_root(&home), Some(support.clone()));
187 assert_eq!(data_root(&home), Some(support));
188 }
189
190 #[test]
191 fn windows_roams_settings_and_keeps_data_local() {
192 if !cfg!(windows) {
193 return;
194 }
195 let both =
196 env(&[("APPDATA", r"C:\Users\ada\AppData\Roaming"), ("LOCALAPPDATA", r"C:\Users\ada\AppData\Local")]);
197 assert_eq!(config_root(&both), Some(PathBuf::from(r"C:\Users\ada\AppData\Roaming")));
198 assert_eq!(data_root(&both), Some(PathBuf::from(r"C:\Users\ada\AppData\Local")));
199 }
200
201 #[test]
202 fn a_relative_home_counts_as_missing() {
203 if cfg!(windows) {
204 return;
205 }
206 let relative = env(&[("HOME", "ada")]);
207 assert_eq!(config_root(&relative), None, "not a folder under the working directory");
208 assert_eq!(data_root(&relative), None);
209 if cfg!(not(target_os = "macos")) {
211 let xdg = env(&[("HOME", "ada"), ("XDG_CONFIG_HOME", "/cfg"), ("XDG_DATA_HOME", "/dat")]);
212 assert_eq!(config_root(&xdg), Some(PathBuf::from("/cfg")));
213 assert_eq!(data_root(&xdg), Some(PathBuf::from("/dat")));
214 }
215 }
216
217 #[test]
218 fn state_and_cache_follow_their_xdg_variables_on_unix() {
219 if !cfg!(all(unix, not(target_os = "macos"))) {
220 return;
221 }
222 let home = env(&[("HOME", "/home/ada")]);
223 assert_eq!(state_root(&home), Some(PathBuf::from("/home/ada/.local/state")));
224 assert_eq!(cache_root(&home), Some(PathBuf::from("/home/ada/.cache")));
225
226 let xdg = env(&[("HOME", "/home/ada"), ("XDG_STATE_HOME", "/st"), ("XDG_CACHE_HOME", "/ca")]);
227 assert_eq!(state_root(&xdg), Some(PathBuf::from("/st")));
228 assert_eq!(cache_root(&xdg), Some(PathBuf::from("/ca")));
229
230 let relative = env(&[("HOME", "/home/ada"), ("XDG_STATE_HOME", "st"), ("XDG_CACHE_HOME", "ca")]);
232 assert_eq!(state_root(&relative), Some(PathBuf::from("/home/ada/.local/state")));
233 assert_eq!(cache_root(&relative), Some(PathBuf::from("/home/ada/.cache")));
234
235 let empty = env(&[("HOME", "/home/ada"), ("XDG_STATE_HOME", ""), ("XDG_CACHE_HOME", "")]);
236 assert_eq!(state_root(&empty), Some(PathBuf::from("/home/ada/.local/state")));
237 assert_eq!(cache_root(&empty), Some(PathBuf::from("/home/ada/.cache")));
238
239 let bare = env(&[("HOME", "ada")]);
241 assert_eq!((state_root(&bare), cache_root(&bare)), (None, None));
242 let only_xdg = env(&[("HOME", "ada"), ("XDG_STATE_HOME", "/st"), ("XDG_CACHE_HOME", "/ca")]);
243 assert_eq!(state_root(&only_xdg), Some(PathBuf::from("/st")));
244 assert_eq!(cache_root(&only_xdg), Some(PathBuf::from("/ca")));
245 }
246
247 #[test]
248 fn macos_keeps_state_with_the_data_and_caches_in_library_caches() {
249 if !cfg!(target_os = "macos") {
250 return;
251 }
252 let home = env(&[("HOME", "/Users/ada")]);
253 assert_eq!(state_root(&home), Some(PathBuf::from("/Users/ada/Library/Application Support")));
254 assert_eq!(cache_root(&home), Some(PathBuf::from("/Users/ada/Library/Caches")));
255 }
256
257 #[test]
258 fn windows_keeps_state_and_cache_local() {
259 if !cfg!(windows) {
260 return;
261 }
262 let both =
263 env(&[("APPDATA", r"C:\Users\ada\AppData\Roaming"), ("LOCALAPPDATA", r"C:\Users\ada\AppData\Local")]);
264 assert_eq!(state_root(&both), Some(PathBuf::from(r"C:\Users\ada\AppData\Local")));
265 assert_eq!(cache_root(&both), Some(PathBuf::from(r"C:\Users\ada\AppData\Local")));
266 }
267
268 #[test]
269 fn nothing_in_the_environment_means_no_folder() {
270 assert_eq!(config_root(env(&[])), None);
271 assert_eq!(data_root(env(&[])), None);
272 assert_eq!(state_root(env(&[])), None);
273 assert_eq!(cache_root(env(&[])), None);
274 }
275
276 #[test]
277 fn the_application_name_is_the_last_segment() {
278 for dir in
280 [config_dir("qfocus"), data_dir("qfocus"), state_dir("qfocus"), cache_dir("qfocus")].into_iter().flatten()
281 {
282 assert_eq!(dir.file_name().and_then(|name| name.to_str()), Some("qfocus"), "{}", dir.display());
283 assert!(dir.is_absolute(), "{}", dir.display());
284 }
285 }
286}