null_e/config/
file.rs

1//! Configuration file loading and saving
2
3use super::Config;
4use crate::error::{DevSweepError, Result};
5use std::path::{Path, PathBuf};
6
7/// Get the default config file path
8pub fn default_config_path() -> Result<PathBuf> {
9    let config_dir = dirs::config_dir().ok_or_else(|| {
10        DevSweepError::Config("Cannot determine config directory".into())
11    })?;
12
13    Ok(config_dir.join("devsweep").join("config.toml"))
14}
15
16/// Load configuration from file
17pub fn load_config(path: &Path) -> Result<Config> {
18    if !path.exists() {
19        return Ok(Config::default());
20    }
21
22    let content = std::fs::read_to_string(path).map_err(|e| {
23        DevSweepError::ConfigParse {
24            path: path.to_path_buf(),
25            reason: e.to_string(),
26        }
27    })?;
28
29    let config: Config = toml::from_str(&content).map_err(|e| {
30        DevSweepError::ConfigParse {
31            path: path.to_path_buf(),
32            reason: e.to_string(),
33        }
34    })?;
35
36    Ok(config)
37}
38
39/// Load configuration from default location
40pub fn load_default_config() -> Result<Config> {
41    let path = default_config_path()?;
42    load_config(&path)
43}
44
45/// Save configuration to file
46pub fn save_config(config: &Config, path: &Path) -> Result<()> {
47    // Ensure parent directory exists
48    if let Some(parent) = path.parent() {
49        std::fs::create_dir_all(parent)?;
50    }
51
52    let content = toml::to_string_pretty(config)?;
53    std::fs::write(path, content)?;
54
55    Ok(())
56}
57
58/// Save configuration to default location
59pub fn save_default_config(config: &Config) -> Result<()> {
60    let path = default_config_path()?;
61    save_config(config, &path)
62}
63
64/// Generate a sample configuration file
65pub fn generate_sample_config() -> String {
66    r#"# DevSweep Configuration
67# Location: ~/.config/devsweep/config.toml
68
69[general]
70# Default directories to scan (leave empty to require explicit path)
71default_paths = [
72    "~/projects",
73    "~/code",
74]
75
76# Paths to always exclude
77exclude_paths = [
78    "~/projects/important-project",
79]
80
81# Log level: error, warn, info, debug, trace
82log_level = "info"
83
84# Enable verbose output
85verbose = false
86
87[scan]
88# Maximum depth to scan (null = unlimited)
89# max_depth = 10
90
91# Skip hidden directories (starting with .)
92skip_hidden = true
93
94# Respect .gitignore files
95respect_gitignore = true
96
97# Minimum artifact size to report in bytes (null = no minimum)
98# min_size = 1000000  # 1 MB
99
100# Custom ignore patterns (glob syntax)
101ignore_patterns = []
102
103# Number of parallel threads (null = auto based on CPU)
104# parallelism = 4
105
106# Check git status for each project
107check_git_status = true
108
109[clean]
110# Delete method: trash, permanent, dry-run
111delete_method = "trash"
112
113# Protection level: none, warn, block, paranoid
114protection_level = "warn"
115
116# Continue on errors
117continue_on_error = true
118
119# Auto-confirm without prompts (use with caution!)
120auto_confirm = false
121
122# Dry run by default
123dry_run = false
124
125[ui]
126# Color theme: dark, light, auto
127theme = "auto"
128
129# Show file counts for artifacts
130show_file_counts = true
131
132# Show last modified dates
133show_dates = true
134
135# Sort results by: size, name, date, kind
136sort_by = "size"
137
138# Reverse sort order
139sort_reverse = false
140
141# Use icons/emojis
142use_icons = true
143
144[plugins]
145# Enabled plugins (empty = all)
146enabled = []
147
148# Disabled plugins
149disabled = []
150"#.to_string()
151}
152
153/// Initialize config directory with sample config
154pub fn init_config() -> Result<PathBuf> {
155    let path = default_config_path()?;
156
157    if path.exists() {
158        return Err(DevSweepError::Config(format!(
159            "Config file already exists at {}",
160            path.display()
161        )));
162    }
163
164    // Create parent directory
165    if let Some(parent) = path.parent() {
166        std::fs::create_dir_all(parent)?;
167    }
168
169    // Write sample config
170    let sample = generate_sample_config();
171    std::fs::write(&path, sample)?;
172
173    Ok(path)
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179    use tempfile::TempDir;
180
181    #[test]
182    fn test_load_nonexistent_config() {
183        let temp = TempDir::new().unwrap();
184        let path = temp.path().join("nonexistent.toml");
185
186        let config = load_config(&path).unwrap();
187        // Should return default config
188        assert_eq!(config.clean.delete_method, crate::trash::DeleteMethod::Trash);
189    }
190
191    #[test]
192    fn test_save_and_load_config() {
193        let temp = TempDir::new().unwrap();
194        let path = temp.path().join("config.toml");
195
196        let mut config = Config::default();
197        config.general.verbose = true;
198        config.scan.max_depth = Some(5);
199
200        save_config(&config, &path).unwrap();
201
202        let loaded = load_config(&path).unwrap();
203        assert!(loaded.general.verbose);
204        assert_eq!(loaded.scan.max_depth, Some(5));
205    }
206
207    #[test]
208    fn test_sample_config_is_valid() {
209        let sample = generate_sample_config();
210        let result: std::result::Result<Config, _> = toml::from_str(&sample);
211        assert!(result.is_ok(), "Sample config should be valid TOML");
212    }
213}