Skip to main content

remendo/config/
paths.rs

1//! XDG-compliant path resolution for application directories.
2
3use super::error::ConfigError;
4use std::path::PathBuf;
5
6/// Resolved filesystem paths for the application.
7///
8/// Uses the XDG Base Directory Specification via the `dirs` crate,
9/// with fallbacks to the current working directory when XDG paths
10/// are unavailable.
11#[derive(Debug, Clone)]
12pub struct AppPaths {
13    /// Path to the configuration file.
14    pub config_file: PathBuf,
15    /// Directory for cached API responses.
16    pub cache_dir: PathBuf,
17    /// Directory for persistent state (bookmarks, read-state).
18    pub state_dir: PathBuf,
19}
20
21impl AppPaths {
22    /// Resolve all application paths using the XDG spec.
23    ///
24    /// Falls back to CWD-relative paths if the user's home directory
25    /// cannot be determined.
26    ///
27    /// # Errors
28    ///
29    /// Returns [`ConfigError::Io`] if the current directory cannot be
30    /// determined during fallback resolution.
31    pub fn resolve() -> Result<Self, ConfigError> {
32        let config_file = dirs::config_dir().map_or_else(
33            || PathBuf::from("config.toml"),
34            |d| d.join("remendo").join("config.toml"),
35        );
36
37        let cache_dir = dirs::cache_dir().map_or_else(
38            || PathBuf::from(".cache").join("remendo"),
39            |d| d.join("remendo"),
40        );
41
42        let state_dir = dirs::state_dir().map_or_else(
43            || PathBuf::from(".local").join("state").join("remendo"),
44            |d| d.join("remendo"),
45        );
46
47        Ok(Self {
48            config_file,
49            cache_dir,
50            state_dir,
51        })
52    }
53}
54
55#[cfg(test)]
56#[allow(clippy::expect_used)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn resolve_produces_paths() {
62        let paths = AppPaths::resolve().expect("resolve paths");
63        assert!(
64            paths.config_file.to_string_lossy().ends_with("config.toml"),
65            "config_file should end with config.toml, got: {:?}",
66            paths.config_file
67        );
68    }
69
70    #[test]
71    fn resolve_config_under_xdg_dir() {
72        let paths = AppPaths::resolve().expect("resolve paths");
73        // On a system with a home directory, the config should be under
74        // a "remendo" subdirectory, not a bare "config.toml"
75        if dirs::config_dir().is_some() {
76            let path_str = paths.config_file.to_string_lossy();
77            assert!(
78                path_str.contains("remendo"),
79                "config path should contain 'remendo' subdir, got: {path_str}"
80            );
81        }
82    }
83
84    #[test]
85    fn resolve_cache_under_xdg_dir() {
86        let paths = AppPaths::resolve().expect("resolve paths");
87        if dirs::cache_dir().is_some() {
88            let path_str = paths.cache_dir.to_string_lossy();
89            assert!(
90                path_str.contains("remendo"),
91                "cache path should contain 'remendo' subdir, got: {path_str}"
92            );
93        }
94    }
95}