1use serde::Deserialize;
29use std::path::{Path, PathBuf};
30
31#[derive(Debug, Clone, Deserialize)]
33#[serde(default)]
34pub struct ZshrsConfig {
35 pub worker_pool: WorkerPoolConfig,
36 pub completion: CompletionConfig,
37 pub history: HistoryConfig,
38 pub glob: GlobConfig,
39 pub log: LogConfig,
40}
41
42#[derive(Debug, Clone, Deserialize)]
43#[serde(default)]
44pub struct WorkerPoolConfig {
45 pub size: usize,
47}
48
49#[derive(Debug, Clone, Deserialize)]
50#[serde(default)]
51pub struct CompletionConfig {
52 pub max_matches: usize,
53 pub fts_enabled: bool,
54 pub ast_cache: bool,
55}
56
57#[derive(Debug, Clone, Deserialize)]
58#[serde(default)]
59pub struct HistoryConfig {
60 pub async_writes: bool,
61 pub max_entries: usize,
62}
63
64#[derive(Debug, Clone, Deserialize)]
65#[serde(default)]
66pub struct GlobConfig {
67 pub parallel_threshold: usize,
69 pub recursive_parallel: bool,
71}
72
73#[derive(Debug, Clone, Deserialize)]
74#[serde(default)]
75pub struct LogConfig {
76 pub level: String,
77}
78
79impl Default for ZshrsConfig {
82 fn default() -> Self {
83 Self {
84 worker_pool: WorkerPoolConfig::default(),
85 completion: CompletionConfig::default(),
86 history: HistoryConfig::default(),
87 glob: GlobConfig::default(),
88 log: LogConfig::default(),
89 }
90 }
91}
92
93impl Default for WorkerPoolConfig {
94 fn default() -> Self {
95 Self { size: 0 } }
97}
98
99impl Default for CompletionConfig {
100 fn default() -> Self {
101 Self {
102 max_matches: 1000,
103 fts_enabled: true,
104 ast_cache: true,
105 }
106 }
107}
108
109impl Default for HistoryConfig {
110 fn default() -> Self {
111 Self {
112 async_writes: true,
113 max_entries: 100_000,
114 }
115 }
116}
117
118impl Default for GlobConfig {
119 fn default() -> Self {
120 Self {
121 parallel_threshold: 32,
122 recursive_parallel: true,
123 }
124 }
125}
126
127impl Default for LogConfig {
128 fn default() -> Self {
129 Self {
130 level: "info".to_string(),
131 }
132 }
133}
134
135pub fn config_path() -> PathBuf {
139 dirs::config_dir()
140 .unwrap_or_else(|| PathBuf::from("/tmp"))
141 .join("zshrs")
142 .join("config.toml")
143}
144
145pub fn load() -> ZshrsConfig {
147 load_from(&config_path())
148}
149
150pub fn load_from(path: &Path) -> ZshrsConfig {
152 match std::fs::read_to_string(path) {
153 Ok(content) => match toml::from_str(&content) {
154 Ok(config) => {
155 tracing::info!(path = %path.display(), "config loaded");
156 config
157 }
158 Err(e) => {
159 tracing::warn!(
160 path = %path.display(),
161 error = %e,
162 "config parse error, using defaults"
163 );
164 ZshrsConfig::default()
165 }
166 },
167 Err(_) => {
168 ZshrsConfig::default()
170 }
171 }
172}
173
174pub fn resolve_pool_size(config: &WorkerPoolConfig) -> usize {
177 if config.size > 0 {
178 config.size.clamp(1, 64)
179 } else {
180 std::thread::available_parallelism()
181 .map(|n| n.get())
182 .unwrap_or(4)
183 .clamp(2, 18)
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 #[test]
192 fn test_default_config() {
193 let config = ZshrsConfig::default();
194 assert_eq!(config.worker_pool.size, 0);
195 assert_eq!(config.completion.max_matches, 1000);
196 assert!(config.completion.fts_enabled);
197 assert!(config.completion.ast_cache);
198 assert!(config.history.async_writes);
199 assert!(config.glob.recursive_parallel);
200 assert_eq!(config.glob.parallel_threshold, 32);
201 }
202
203 #[test]
204 fn test_parse_toml() {
205 let toml = r#"
206[worker_pool]
207size = 4
208
209[completion]
210max_matches = 500
211ast_cache = false
212
213[glob]
214parallel_threshold = 64
215"#;
216 let config: ZshrsConfig = toml::from_str(toml).unwrap();
217 assert_eq!(config.worker_pool.size, 4);
218 assert_eq!(config.completion.max_matches, 500);
219 assert!(!config.completion.ast_cache);
220 assert_eq!(config.glob.parallel_threshold, 64);
221 assert!(config.history.async_writes);
223 assert!(config.glob.recursive_parallel);
224 }
225
226 #[test]
227 fn test_resolve_pool_size() {
228 let auto = WorkerPoolConfig { size: 0 };
229 let resolved = resolve_pool_size(&auto);
230 assert!(resolved >= 2 && resolved <= 18);
231
232 let explicit = WorkerPoolConfig { size: 4 };
233 assert_eq!(resolve_pool_size(&explicit), 4);
234
235 let clamped = WorkerPoolConfig { size: 999 };
236 assert_eq!(resolve_pool_size(&clamped), 64);
237 }
238
239 #[test]
240 fn test_missing_file_returns_defaults() {
241 let config = load_from(Path::new("/nonexistent/config.toml"));
242 assert_eq!(config.worker_pool.size, 0);
243 }
244}