use std::path::PathBuf;
#[must_use]
pub fn config_dir(app: &str) -> Option<PathBuf> {
config_root(env_lookup).map(|root| root.join(app))
}
#[must_use]
pub fn data_dir(app: &str) -> Option<PathBuf> {
data_root(env_lookup).map(|root| root.join(app))
}
#[must_use]
pub fn state_dir(app: &str) -> Option<PathBuf> {
state_root(env_lookup).map(|root| root.join(app))
}
#[must_use]
pub fn cache_dir(app: &str) -> Option<PathBuf> {
cache_root(env_lookup).map(|root| root.join(app))
}
pub(super) fn env_lookup(name: &str) -> Option<PathBuf> {
std::env::var_os(name).map(PathBuf::from)
}
pub(super) fn config_root(lookup: impl Fn(&str) -> Option<PathBuf>) -> Option<PathBuf> {
let non_empty = |name: &str| lookup(name).filter(|path| !path.as_os_str().is_empty());
if cfg!(windows) {
return non_empty("APPDATA");
}
if cfg!(target_os = "macos") {
return absolute(non_empty("HOME")).map(|home| home.join("Library").join("Application Support"));
}
absolute(non_empty("XDG_CONFIG_HOME")).or_else(|| absolute(non_empty("HOME")).map(|home| home.join(".config")))
}
fn data_root(lookup: impl Fn(&str) -> Option<PathBuf>) -> Option<PathBuf> {
let non_empty = |name: &str| lookup(name).filter(|path| !path.as_os_str().is_empty());
if cfg!(windows) {
return non_empty("LOCALAPPDATA");
}
if cfg!(target_os = "macos") {
return absolute(non_empty("HOME")).map(|home| home.join("Library").join("Application Support"));
}
absolute(non_empty("XDG_DATA_HOME"))
.or_else(|| absolute(non_empty("HOME")).map(|home| home.join(".local").join("share")))
}
pub(super) fn state_root(lookup: impl Fn(&str) -> Option<PathBuf>) -> Option<PathBuf> {
let non_empty = |name: &str| lookup(name).filter(|path| !path.as_os_str().is_empty());
if cfg!(windows) {
return non_empty("LOCALAPPDATA");
}
if cfg!(target_os = "macos") {
return absolute(non_empty("HOME")).map(|home| home.join("Library").join("Application Support"));
}
absolute(non_empty("XDG_STATE_HOME"))
.or_else(|| absolute(non_empty("HOME")).map(|home| home.join(".local").join("state")))
}
pub(super) fn cache_root(lookup: impl Fn(&str) -> Option<PathBuf>) -> Option<PathBuf> {
let non_empty = |name: &str| lookup(name).filter(|path| !path.as_os_str().is_empty());
if cfg!(windows) {
return non_empty("LOCALAPPDATA");
}
if cfg!(target_os = "macos") {
return absolute(non_empty("HOME")).map(|home| home.join("Library").join("Caches"));
}
absolute(non_empty("XDG_CACHE_HOME")).or_else(|| absolute(non_empty("HOME")).map(|home| home.join(".cache")))
}
pub(super) fn absolute(path: Option<PathBuf>) -> Option<PathBuf> {
path.filter(|path| path.is_absolute())
}
#[cfg(test)]
mod tests {
use super::*;
fn env(pairs: &'static [(&'static str, &'static str)]) -> impl Fn(&str) -> Option<PathBuf> {
move |name: &str| pairs.iter().find(|(key, _)| *key == name).map(|(_, value)| PathBuf::from(value))
}
#[test]
fn settings_and_data_are_two_folders_on_unix() {
if !cfg!(all(unix, not(target_os = "macos"))) {
return;
}
let home = env(&[("HOME", "/home/ada")]);
assert_eq!(config_root(&home), Some(PathBuf::from("/home/ada/.config")));
assert_eq!(data_root(&home), Some(PathBuf::from("/home/ada/.local/share")));
let xdg = env(&[("HOME", "/home/ada"), ("XDG_CONFIG_HOME", "/cfg"), ("XDG_DATA_HOME", "/dat")]);
assert_eq!(config_root(&xdg), Some(PathBuf::from("/cfg")));
assert_eq!(data_root(&xdg), Some(PathBuf::from("/dat")));
let relative = env(&[("HOME", "/home/ada"), ("XDG_CONFIG_HOME", "cfg"), ("XDG_DATA_HOME", "dat")]);
assert_eq!(config_root(&relative), Some(PathBuf::from("/home/ada/.config")));
assert_eq!(data_root(&relative), Some(PathBuf::from("/home/ada/.local/share")));
let empty = env(&[("HOME", "/home/ada"), ("XDG_DATA_HOME", "")]);
assert_eq!(data_root(&empty), Some(PathBuf::from("/home/ada/.local/share")));
}
#[test]
fn macos_keeps_both_in_application_support() {
if !cfg!(target_os = "macos") {
return;
}
let home = env(&[("HOME", "/Users/ada")]);
let support = PathBuf::from("/Users/ada/Library/Application Support");
assert_eq!(config_root(&home), Some(support.clone()));
assert_eq!(data_root(&home), Some(support));
}
#[test]
fn windows_roams_settings_and_keeps_data_local() {
if !cfg!(windows) {
return;
}
let both =
env(&[("APPDATA", r"C:\Users\ada\AppData\Roaming"), ("LOCALAPPDATA", r"C:\Users\ada\AppData\Local")]);
assert_eq!(config_root(&both), Some(PathBuf::from(r"C:\Users\ada\AppData\Roaming")));
assert_eq!(data_root(&both), Some(PathBuf::from(r"C:\Users\ada\AppData\Local")));
}
#[test]
fn a_relative_home_counts_as_missing() {
if cfg!(windows) {
return;
}
let relative = env(&[("HOME", "ada")]);
assert_eq!(config_root(&relative), None, "not a folder under the working directory");
assert_eq!(data_root(&relative), None);
if cfg!(not(target_os = "macos")) {
let xdg = env(&[("HOME", "ada"), ("XDG_CONFIG_HOME", "/cfg"), ("XDG_DATA_HOME", "/dat")]);
assert_eq!(config_root(&xdg), Some(PathBuf::from("/cfg")));
assert_eq!(data_root(&xdg), Some(PathBuf::from("/dat")));
}
}
#[test]
fn state_and_cache_follow_their_xdg_variables_on_unix() {
if !cfg!(all(unix, not(target_os = "macos"))) {
return;
}
let home = env(&[("HOME", "/home/ada")]);
assert_eq!(state_root(&home), Some(PathBuf::from("/home/ada/.local/state")));
assert_eq!(cache_root(&home), Some(PathBuf::from("/home/ada/.cache")));
let xdg = env(&[("HOME", "/home/ada"), ("XDG_STATE_HOME", "/st"), ("XDG_CACHE_HOME", "/ca")]);
assert_eq!(state_root(&xdg), Some(PathBuf::from("/st")));
assert_eq!(cache_root(&xdg), Some(PathBuf::from("/ca")));
let relative = env(&[("HOME", "/home/ada"), ("XDG_STATE_HOME", "st"), ("XDG_CACHE_HOME", "ca")]);
assert_eq!(state_root(&relative), Some(PathBuf::from("/home/ada/.local/state")));
assert_eq!(cache_root(&relative), Some(PathBuf::from("/home/ada/.cache")));
let empty = env(&[("HOME", "/home/ada"), ("XDG_STATE_HOME", ""), ("XDG_CACHE_HOME", "")]);
assert_eq!(state_root(&empty), Some(PathBuf::from("/home/ada/.local/state")));
assert_eq!(cache_root(&empty), Some(PathBuf::from("/home/ada/.cache")));
let bare = env(&[("HOME", "ada")]);
assert_eq!((state_root(&bare), cache_root(&bare)), (None, None));
let only_xdg = env(&[("HOME", "ada"), ("XDG_STATE_HOME", "/st"), ("XDG_CACHE_HOME", "/ca")]);
assert_eq!(state_root(&only_xdg), Some(PathBuf::from("/st")));
assert_eq!(cache_root(&only_xdg), Some(PathBuf::from("/ca")));
}
#[test]
fn macos_keeps_state_with_the_data_and_caches_in_library_caches() {
if !cfg!(target_os = "macos") {
return;
}
let home = env(&[("HOME", "/Users/ada")]);
assert_eq!(state_root(&home), Some(PathBuf::from("/Users/ada/Library/Application Support")));
assert_eq!(cache_root(&home), Some(PathBuf::from("/Users/ada/Library/Caches")));
}
#[test]
fn windows_keeps_state_and_cache_local() {
if !cfg!(windows) {
return;
}
let both =
env(&[("APPDATA", r"C:\Users\ada\AppData\Roaming"), ("LOCALAPPDATA", r"C:\Users\ada\AppData\Local")]);
assert_eq!(state_root(&both), Some(PathBuf::from(r"C:\Users\ada\AppData\Local")));
assert_eq!(cache_root(&both), Some(PathBuf::from(r"C:\Users\ada\AppData\Local")));
}
#[test]
fn nothing_in_the_environment_means_no_folder() {
assert_eq!(config_root(env(&[])), None);
assert_eq!(data_root(env(&[])), None);
assert_eq!(state_root(env(&[])), None);
assert_eq!(cache_root(env(&[])), None);
}
#[test]
fn the_application_name_is_the_last_segment() {
for dir in
[config_dir("qfocus"), data_dir("qfocus"), state_dir("qfocus"), cache_dir("qfocus")].into_iter().flatten()
{
assert_eq!(dir.file_name().and_then(|name| name.to_str()), Some("qfocus"), "{}", dir.display());
assert!(dir.is_absolute(), "{}", dir.display());
}
}
}