envelope_cli/config/
paths.rs

1//! Path management for EnvelopeCLI
2//!
3//! Provides XDG-compliant path resolution for configuration, data, and backups.
4//!
5//! ## Path Resolution Order
6//!
7//! 1. `ENVELOPE_CLI_DATA_DIR` environment variable (if set)
8//! 2. Unix (Linux/macOS): `$XDG_CONFIG_HOME/envelope-cli` or `~/.config/envelope-cli`
9//! 3. Windows: `%APPDATA%\envelope-cli`
10
11use std::path::PathBuf;
12
13use crate::error::EnvelopeError;
14
15/// Manages all paths used by EnvelopeCLI
16#[derive(Debug, Clone)]
17pub struct EnvelopePaths {
18    /// Base directory for all EnvelopeCLI data
19    base_dir: PathBuf,
20}
21
22impl EnvelopePaths {
23    /// Create a new EnvelopePaths instance
24    ///
25    /// Path resolution:
26    /// 1. `ENVELOPE_CLI_DATA_DIR` env var (explicit override)
27    /// 2. Unix: `$XDG_CONFIG_HOME/envelope-cli` or `~/.config/envelope-cli`
28    /// 3. Windows: `%APPDATA%\envelope-cli`
29    ///
30    /// # Errors
31    ///
32    /// Returns an error if the home directory cannot be determined.
33    pub fn new() -> Result<Self, EnvelopeError> {
34        let base_dir = if let Ok(custom) = std::env::var("ENVELOPE_CLI_DATA_DIR") {
35            PathBuf::from(custom)
36        } else {
37            resolve_default_path()?
38        };
39
40        Ok(Self { base_dir })
41    }
42
43    /// Create EnvelopePaths with a custom base directory (useful for testing)
44    pub fn with_base_dir(base_dir: PathBuf) -> Self {
45        Self { base_dir }
46    }
47
48    /// Get the base directory (~/.config/envelope-cli/ or equivalent)
49    pub fn base_dir(&self) -> &PathBuf {
50        &self.base_dir
51    }
52
53    /// Get the config directory (same as base for simplicity)
54    pub fn config_dir(&self) -> PathBuf {
55        self.base_dir.clone()
56    }
57
58    /// Get the data directory (~/.config/envelope-cli/data/)
59    pub fn data_dir(&self) -> PathBuf {
60        self.base_dir.join("data")
61    }
62
63    /// Get the backup directory (~/.config/envelope-cli/backups/)
64    pub fn backup_dir(&self) -> PathBuf {
65        self.base_dir.join("backups")
66    }
67
68    /// Get the path to the settings file
69    pub fn settings_file(&self) -> PathBuf {
70        self.base_dir.join("config.json")
71    }
72
73    /// Get the path to the audit log
74    pub fn audit_log(&self) -> PathBuf {
75        self.base_dir.join("audit.log")
76    }
77
78    /// Get the path to accounts.json
79    pub fn accounts_file(&self) -> PathBuf {
80        self.data_dir().join("accounts.json")
81    }
82
83    /// Get the path to transactions.json
84    pub fn transactions_file(&self) -> PathBuf {
85        self.data_dir().join("transactions.json")
86    }
87
88    /// Get the path to budget.json (categories and groups)
89    pub fn budget_file(&self) -> PathBuf {
90        self.data_dir().join("budget.json")
91    }
92
93    /// Get the path to allocations.json (budget allocations per period)
94    pub fn allocations_file(&self) -> PathBuf {
95        self.data_dir().join("allocations.json")
96    }
97
98    /// Get the path to payees.json
99    pub fn payees_file(&self) -> PathBuf {
100        self.data_dir().join("payees.json")
101    }
102
103    /// Get the path to targets.json (budget targets)
104    pub fn targets_file(&self) -> PathBuf {
105        self.data_dir().join("targets.json")
106    }
107
108    /// Get the path to income.json (income expectations)
109    pub fn income_file(&self) -> PathBuf {
110        self.data_dir().join("income.json")
111    }
112
113    /// Ensure all required directories exist
114    ///
115    /// Creates:
116    /// - Base directory (~/.config/envelope-cli/)
117    /// - Data directory (~/.config/envelope-cli/data/)
118    /// - Backup directory (~/.config/envelope-cli/backups/)
119    pub fn ensure_directories(&self) -> Result<(), EnvelopeError> {
120        std::fs::create_dir_all(&self.base_dir)
121            .map_err(|e| EnvelopeError::Io(format!("Failed to create base directory: {}", e)))?;
122
123        std::fs::create_dir_all(self.data_dir())
124            .map_err(|e| EnvelopeError::Io(format!("Failed to create data directory: {}", e)))?;
125
126        std::fs::create_dir_all(self.backup_dir())
127            .map_err(|e| EnvelopeError::Io(format!("Failed to create backup directory: {}", e)))?;
128
129        Ok(())
130    }
131
132    /// Check if EnvelopeCLI has been initialized (config file exists)
133    pub fn is_initialized(&self) -> bool {
134        self.settings_file().exists()
135    }
136}
137
138/// Resolve the default data directory path based on platform
139#[cfg(not(windows))]
140fn resolve_default_path() -> Result<PathBuf, EnvelopeError> {
141    // Unix (Linux/macOS): Use XDG_CONFIG_HOME if set, otherwise ~/.config
142    let config_base = std::env::var("XDG_CONFIG_HOME")
143        .map(PathBuf::from)
144        .unwrap_or_else(|_| {
145            let home = std::env::var("HOME").expect("HOME environment variable not set");
146            PathBuf::from(home).join(".config")
147        });
148    Ok(config_base.join("envelope-cli"))
149}
150
151/// Resolve the default data directory path based on platform
152#[cfg(windows)]
153fn resolve_default_path() -> Result<PathBuf, EnvelopeError> {
154    // Windows: Use APPDATA
155    let appdata = std::env::var("APPDATA")
156        .map_err(|_| EnvelopeError::Config("Could not determine APPDATA directory".into()))?;
157    Ok(PathBuf::from(appdata).join("envelope-cli"))
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163    use std::env;
164    use tempfile::TempDir;
165
166    #[test]
167    fn test_custom_base_dir() {
168        let temp_dir = TempDir::new().unwrap();
169        let paths = EnvelopePaths::with_base_dir(temp_dir.path().to_path_buf());
170
171        assert_eq!(paths.base_dir(), temp_dir.path());
172        assert_eq!(paths.data_dir(), temp_dir.path().join("data"));
173        assert_eq!(paths.backup_dir(), temp_dir.path().join("backups"));
174    }
175
176    #[test]
177    fn test_env_var_override() {
178        let temp_dir = TempDir::new().unwrap();
179        let custom_path = temp_dir.path().to_str().unwrap();
180
181        // Set the env var
182        env::set_var("ENVELOPE_CLI_DATA_DIR", custom_path);
183
184        let paths = EnvelopePaths::new().unwrap();
185        assert_eq!(paths.base_dir(), temp_dir.path());
186
187        // Clean up
188        env::remove_var("ENVELOPE_CLI_DATA_DIR");
189    }
190
191    #[test]
192    fn test_ensure_directories() {
193        let temp_dir = TempDir::new().unwrap();
194        let paths = EnvelopePaths::with_base_dir(temp_dir.path().to_path_buf());
195
196        paths.ensure_directories().unwrap();
197
198        assert!(paths.data_dir().exists());
199        assert!(paths.backup_dir().exists());
200    }
201
202    #[test]
203    fn test_file_paths() {
204        let temp_dir = TempDir::new().unwrap();
205        let paths = EnvelopePaths::with_base_dir(temp_dir.path().to_path_buf());
206
207        assert_eq!(paths.settings_file(), temp_dir.path().join("config.json"));
208        assert_eq!(
209            paths.accounts_file(),
210            temp_dir.path().join("data").join("accounts.json")
211        );
212    }
213}