Skip to main content

chore_cli/config/
mod.rs

1/* src/config/mod.rs */
2
3mod defaults;
4
5pub use defaults::default_formats;
6
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::fs;
10use std::path::{Path, PathBuf};
11
12#[derive(Debug, Serialize, Deserialize, Clone)]
13pub struct PathCommentConfig {
14    #[serde(default = "default_enabled")]
15    pub enabled: bool,
16
17    #[serde(default)]
18    pub formats: HashMap<String, String>,
19
20    #[serde(default)]
21    pub exclude: ExcludeConfig,
22}
23
24fn default_enabled() -> bool {
25    true
26}
27
28#[derive(Debug, Serialize, Deserialize, Clone, Default)]
29pub struct ExcludeConfig {
30    #[serde(default)]
31    pub dirs: Vec<String>,
32
33    #[serde(default)]
34    pub patterns: Vec<String>,
35}
36
37#[derive(Debug, Serialize, Deserialize, Clone)]
38pub struct Config {
39    pub path_comment: PathCommentConfig,
40}
41
42impl Default for Config {
43    fn default() -> Self {
44        Config {
45            path_comment: PathCommentConfig {
46                enabled: true,
47                formats: default_formats(),
48                exclude: ExcludeConfig::default(),
49            },
50        }
51    }
52}
53
54impl Config {
55    /// Load config from file, or return default config
56    pub fn load(config_path: Option<PathBuf>) -> Result<Self, String> {
57        if let Some(path) = config_path {
58            let content = fs::read_to_string(&path)
59                .map_err(|e| format!("Failed to read config file: {}", e))?;
60
61            let config: Config = toml::from_str(&content)
62                .map_err(|e| format!("Failed to parse config file: {}", e))?;
63
64            Ok(config)
65        } else {
66            Ok(Config::default())
67        }
68    }
69
70    /// Find config file in current or parent directories
71    pub fn find_config_file(start_dir: &Path) -> Option<PathBuf> {
72        let mut current = start_dir.to_path_buf();
73
74        loop {
75            // Check for chore.toml first
76            let chore_toml = current.join("chore.toml");
77            if chore_toml.exists() {
78                return Some(chore_toml);
79            }
80
81            // Then check for .chore.toml
82            let dot_chore_toml = current.join(".chore.toml");
83            if dot_chore_toml.exists() {
84                return Some(dot_chore_toml);
85            }
86
87            // Move to parent directory
88            if !current.pop() {
89                break;
90            }
91        }
92
93        None
94    }
95
96    /// Generate default config file content for --init
97    pub fn generate_init_config(project_dir: &Path, max_depth: usize) -> String {
98        // Scan project to find which file types exist
99        let found_extensions = Self::scan_for_extensions(project_dir, max_depth);
100
101        // Get default formats
102        let all_formats = default_formats();
103
104        // Filter to only include formats for extensions found in project
105        let mut active_formats: Vec<(String, String)> = all_formats
106            .into_iter()
107            .filter(|(ext, _)| found_extensions.contains(ext))
108            .collect();
109
110        // Sort for consistent output
111        active_formats.sort_by(|a, b| a.0.cmp(&b.0));
112
113        // Generate TOML content
114        let mut content =
115            String::from("[path_comment]\nenabled = true\n\n[path_comment.formats]\n");
116
117        for (ext, format) in active_formats {
118            content.push_str(&format!("\"{}\" = \"{}\"\n", ext, format));
119        }
120
121        content.push_str("\n[path_comment.exclude]\ndirs = []\npatterns = []\n");
122
123        content
124    }
125
126    /// Scan project directory to find which file extensions exist (up to max_depth levels)
127    fn scan_for_extensions(project_dir: &Path, max_depth: usize) -> Vec<String> {
128        use walkdir::WalkDir;
129
130        let mut extensions = Vec::new();
131        let default_formats = default_formats();
132
133        for entry in WalkDir::new(project_dir)
134            .max_depth(max_depth)
135            .follow_links(false)
136        {
137            if let Ok(entry) = entry {
138                if entry.file_type().is_file() {
139                    if let Some(ext) = entry.path().extension() {
140                        let ext_str = format!(".{}", ext.to_string_lossy());
141                        if default_formats.contains_key(&ext_str) && !extensions.contains(&ext_str)
142                        {
143                            extensions.push(ext_str);
144                        }
145                    }
146                }
147            }
148        }
149
150        extensions
151    }
152}