Skip to main content

null_e/config/
mod.rs

1//! Configuration management
2//!
3//! Handles loading, saving, and merging configuration from multiple sources:
4//! - Default values
5//! - Config file (~/.config/devsweep/config.toml)
6//! - Environment variables
7//! - Command line arguments
8
9mod file;
10
11pub use file::*;
12
13use crate::git::ProtectionLevel;
14use crate::trash::DeleteMethod;
15use serde::{Deserialize, Serialize};
16use std::path::PathBuf;
17
18/// Main configuration structure
19#[derive(Debug, Clone, Serialize, Deserialize)]
20#[serde(default)]
21pub struct Config {
22    /// General settings
23    pub general: GeneralConfig,
24    /// Scan settings
25    pub scan: ScanSettings,
26    /// Clean settings
27    pub clean: CleanSettings,
28    /// UI settings
29    pub ui: UiSettings,
30    /// Plugin settings
31    pub plugins: PluginSettings,
32}
33
34impl Default for Config {
35    fn default() -> Self {
36        Self {
37            general: GeneralConfig::default(),
38            scan: ScanSettings::default(),
39            clean: CleanSettings::default(),
40            ui: UiSettings::default(),
41            plugins: PluginSettings::default(),
42        }
43    }
44}
45
46/// General settings
47#[derive(Debug, Clone, Serialize, Deserialize)]
48#[serde(default)]
49pub struct GeneralConfig {
50    /// Default directories to scan
51    pub default_paths: Vec<PathBuf>,
52    /// Paths to always exclude
53    pub exclude_paths: Vec<PathBuf>,
54    /// Log level (error, warn, info, debug, trace)
55    pub log_level: String,
56    /// Enable verbose output
57    pub verbose: bool,
58}
59
60impl Default for GeneralConfig {
61    fn default() -> Self {
62        let mut default_paths = Vec::new();
63
64        // Add common project directories
65        if let Some(home) = dirs::home_dir() {
66            default_paths.push(home.join("projects"));
67            default_paths.push(home.join("Projects"));
68            default_paths.push(home.join("code"));
69            default_paths.push(home.join("Code"));
70            default_paths.push(home.join("dev"));
71            default_paths.push(home.join("Developer"));
72            default_paths.push(home.join("src"));
73            default_paths.push(home.join("workspace"));
74        }
75
76        Self {
77            default_paths,
78            exclude_paths: vec![],
79            log_level: "info".into(),
80            verbose: false,
81        }
82    }
83}
84
85/// Scan settings
86#[derive(Debug, Clone, Serialize, Deserialize)]
87#[serde(default)]
88pub struct ScanSettings {
89    /// Maximum depth to scan
90    pub max_depth: Option<usize>,
91    /// Skip hidden directories
92    pub skip_hidden: bool,
93    /// Respect .gitignore files
94    pub respect_gitignore: bool,
95    /// Minimum artifact size to report (bytes)
96    pub min_size: Option<u64>,
97    /// Custom ignore patterns
98    pub ignore_patterns: Vec<String>,
99    /// Number of parallel threads (None = auto)
100    pub parallelism: Option<usize>,
101    /// Check git status for each project
102    pub check_git_status: bool,
103}
104
105impl Default for ScanSettings {
106    fn default() -> Self {
107        Self {
108            max_depth: None,
109            skip_hidden: true,
110            respect_gitignore: true,
111            min_size: None, // Could set to 1MB: Some(1_000_000)
112            ignore_patterns: vec![],
113            parallelism: None,
114            check_git_status: true,
115        }
116    }
117}
118
119/// Clean settings
120#[derive(Debug, Clone, Serialize, Deserialize)]
121#[serde(default)]
122pub struct CleanSettings {
123    /// Default delete method
124    #[serde(with = "delete_method_serde")]
125    pub delete_method: DeleteMethod,
126    /// Protection level for git repos
127    #[serde(with = "protection_level_serde")]
128    pub protection_level: ProtectionLevel,
129    /// Continue on errors
130    pub continue_on_error: bool,
131    /// Auto-confirm (no prompts)
132    pub auto_confirm: bool,
133    /// Dry run by default
134    pub dry_run: bool,
135}
136
137impl Default for CleanSettings {
138    fn default() -> Self {
139        Self {
140            delete_method: DeleteMethod::Trash,
141            protection_level: ProtectionLevel::Warn,
142            continue_on_error: true,
143            auto_confirm: false,
144            dry_run: false,
145        }
146    }
147}
148
149/// UI settings
150#[derive(Debug, Clone, Serialize, Deserialize)]
151#[serde(default)]
152pub struct UiSettings {
153    /// Color theme (dark, light, auto)
154    pub theme: String,
155    /// Show file counts
156    pub show_file_counts: bool,
157    /// Show last modified dates
158    pub show_dates: bool,
159    /// Sort by (size, name, date, kind)
160    pub sort_by: String,
161    /// Reverse sort order
162    pub sort_reverse: bool,
163    /// Use icons/emojis
164    pub use_icons: bool,
165}
166
167impl Default for UiSettings {
168    fn default() -> Self {
169        Self {
170            theme: "auto".into(),
171            show_file_counts: true,
172            show_dates: true,
173            sort_by: "size".into(),
174            sort_reverse: false,
175            use_icons: true,
176        }
177    }
178}
179
180/// Plugin settings
181#[derive(Debug, Clone, Serialize, Deserialize)]
182#[serde(default)]
183pub struct PluginSettings {
184    /// Enabled plugins (empty = all)
185    pub enabled: Vec<String>,
186    /// Disabled plugins
187    pub disabled: Vec<String>,
188}
189
190impl Default for PluginSettings {
191    fn default() -> Self {
192        Self {
193            enabled: vec![],
194            disabled: vec![],
195        }
196    }
197}
198
199// Custom serde implementations for enums
200
201mod delete_method_serde {
202    use super::*;
203    use serde::{Deserializer, Serializer};
204
205    pub fn serialize<S>(method: &DeleteMethod, serializer: S) -> Result<S::Ok, S::Error>
206    where
207        S: Serializer,
208    {
209        let s = match method {
210            DeleteMethod::Trash => "trash",
211            DeleteMethod::Permanent => "permanent",
212            DeleteMethod::DryRun => "dry-run",
213        };
214        serializer.serialize_str(s)
215    }
216
217    pub fn deserialize<'de, D>(deserializer: D) -> Result<DeleteMethod, D::Error>
218    where
219        D: Deserializer<'de>,
220    {
221        let s = String::deserialize(deserializer)?;
222        DeleteMethod::from_str(&s).ok_or_else(|| {
223            serde::de::Error::custom(format!("invalid delete method: {}", s))
224        })
225    }
226}
227
228mod protection_level_serde {
229    use super::*;
230    use serde::{Deserializer, Serializer};
231
232    pub fn serialize<S>(level: &ProtectionLevel, serializer: S) -> Result<S::Ok, S::Error>
233    where
234        S: Serializer,
235    {
236        serializer.serialize_str(level.as_str())
237    }
238
239    pub fn deserialize<'de, D>(deserializer: D) -> Result<ProtectionLevel, D::Error>
240    where
241        D: Deserializer<'de>,
242    {
243        let s = String::deserialize(deserializer)?;
244        ProtectionLevel::from_str(&s).ok_or_else(|| {
245            serde::de::Error::custom(format!("invalid protection level: {}", s))
246        })
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253
254    #[test]
255    fn test_default_config() {
256        let config = Config::default();
257        assert!(!config.general.default_paths.is_empty());
258        assert!(config.scan.skip_hidden);
259        assert_eq!(config.clean.delete_method, DeleteMethod::Trash);
260    }
261
262    #[test]
263    fn test_config_serialization() {
264        let config = Config::default();
265        let toml_str = toml::to_string_pretty(&config).unwrap();
266        assert!(toml_str.contains("[general]"));
267        assert!(toml_str.contains("[scan]"));
268        assert!(toml_str.contains("[clean]"));
269    }
270
271    #[test]
272    fn test_config_deserialization() {
273        let toml_str = r#"
274[general]
275verbose = true
276log_level = "debug"
277
278[scan]
279max_depth = 10
280skip_hidden = false
281
282[clean]
283delete_method = "permanent"
284protection_level = "block"
285"#;
286
287        let config: Config = toml::from_str(toml_str).unwrap();
288        assert!(config.general.verbose);
289        assert_eq!(config.general.log_level, "debug");
290        assert_eq!(config.scan.max_depth, Some(10));
291        assert!(!config.scan.skip_hidden);
292        assert_eq!(config.clean.delete_method, DeleteMethod::Permanent);
293        assert_eq!(config.clean.protection_level, ProtectionLevel::Block);
294    }
295}