Skip to main content

yt/
dirs.rs

1use std::path::PathBuf;
2
3use crate::env::Env;
4use crate::error::{Result, YtError};
5
6/// Where yt keeps its files: XDG locations on every OS, except caches on macOS.
7#[derive(Clone, Debug, PartialEq, Eq)]
8pub struct Dirs {
9    pub config: PathBuf,
10    pub cache: PathBuf,
11    pub state: PathBuf,
12}
13
14#[cfg(target_os = "macos")]
15const CACHE_FALLBACK: &str = "Library/Caches";
16#[cfg(not(target_os = "macos"))]
17const CACHE_FALLBACK: &str = ".cache";
18
19impl Dirs {
20    pub fn from_env(env: &Env) -> Result<Self> {
21        let home = env.path("HOME");
22        let base = |xdg: &str, fallback: &str| -> Result<PathBuf> {
23            env.path(xdg)
24                .or_else(|| home.as_ref().map(|home| home.join(fallback)))
25                .ok_or_else(|| YtError::NoHome.into())
26        };
27        let config = match env.path("YT_CONFIG_DIR") {
28            Some(path) => path,
29            None => base("XDG_CONFIG_HOME", ".config")?.join("yt"),
30        };
31        let cache = match env.path("YT_CACHE_DIR") {
32            Some(path) => path,
33            None => base("XDG_CACHE_HOME", CACHE_FALLBACK)?.join("yt"),
34        };
35        let state = match env.path("YT_STATE_DIR") {
36            Some(path) => path,
37            None => base("XDG_STATE_HOME", ".local/state")?.join("yt"),
38        };
39        Ok(Self {
40            config,
41            cache,
42            state,
43        })
44    }
45
46    pub fn config_file(&self) -> PathBuf {
47        self.config.join("config.toml")
48    }
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54
55    #[test]
56    fn defaults_follow_xdg_under_home() {
57        let dirs = Dirs::from_env(&Env::from_vars([("HOME", "/home/jane")])).unwrap();
58        assert_eq!(dirs.config, PathBuf::from("/home/jane/.config/yt"));
59        assert_eq!(dirs.state, PathBuf::from("/home/jane/.local/state/yt"));
60        assert_eq!(
61            dirs.config_file(),
62            PathBuf::from("/home/jane/.config/yt/config.toml")
63        );
64        #[cfg(target_os = "macos")]
65        assert_eq!(dirs.cache, PathBuf::from("/home/jane/Library/Caches/yt"));
66        #[cfg(not(target_os = "macos"))]
67        assert_eq!(dirs.cache, PathBuf::from("/home/jane/.cache/yt"));
68    }
69
70    #[test]
71    fn xdg_variables_win_over_home() {
72        let env = Env::from_vars([
73            ("HOME", "/home/jane"),
74            ("XDG_CONFIG_HOME", "/x/config"),
75            ("XDG_CACHE_HOME", "/x/cache"),
76            ("XDG_STATE_HOME", "/x/state"),
77        ]);
78        let dirs = Dirs::from_env(&env).unwrap();
79        assert_eq!(dirs.config, PathBuf::from("/x/config/yt"));
80        assert_eq!(dirs.cache, PathBuf::from("/x/cache/yt"));
81        assert_eq!(dirs.state, PathBuf::from("/x/state/yt"));
82    }
83
84    #[test]
85    fn yt_variables_win_over_everything() {
86        let env = Env::from_vars([
87            ("XDG_CONFIG_HOME", "/x/config"),
88            ("YT_CONFIG_DIR", "/y/config"),
89            ("YT_CACHE_DIR", "/y/cache"),
90            ("YT_STATE_DIR", "/y/state"),
91        ]);
92        let dirs = Dirs::from_env(&env).unwrap();
93        assert_eq!(dirs.config, PathBuf::from("/y/config"));
94        assert_eq!(dirs.cache, PathBuf::from("/y/cache"));
95        assert_eq!(dirs.state, PathBuf::from("/y/state"));
96    }
97
98    #[test]
99    fn missing_home_is_an_error() {
100        let error = Dirs::from_env(&Env::from_vars([("X", "y")])).unwrap_err();
101        assert!(matches!(
102            error.downcast_ref::<YtError>(),
103            Some(YtError::NoHome)
104        ));
105    }
106}