1mod 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 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 pub fn find_config_file(start_dir: &Path) -> Option<PathBuf> {
72 let mut current = start_dir.to_path_buf();
73
74 loop {
75 let chore_toml = current.join("chore.toml");
77 if chore_toml.exists() {
78 return Some(chore_toml);
79 }
80
81 let dot_chore_toml = current.join(".chore.toml");
83 if dot_chore_toml.exists() {
84 return Some(dot_chore_toml);
85 }
86
87 if !current.pop() {
89 break;
90 }
91 }
92
93 None
94 }
95
96 pub fn generate_init_config(project_dir: &Path, max_depth: usize) -> String {
98 let found_extensions = Self::scan_for_extensions(project_dir, max_depth);
100
101 let all_formats = default_formats();
103
104 let mut active_formats: Vec<(String, String)> = all_formats
106 .into_iter()
107 .filter(|(ext, _)| found_extensions.contains(ext))
108 .collect();
109
110 active_formats.sort_by(|a, b| a.0.cmp(&b.0));
112
113 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 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}