1use serde::{Deserialize, Serialize};
4use std::path::Path;
5
6use crate::error::{Error, Result};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
10#[serde(rename_all = "kebab-case")]
11pub enum Backend {
12 #[default]
14 Auto,
15 Rayon,
17 IoUring,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23#[serde(default)]
24pub struct Config {
25 pub include: Vec<String>,
27 pub exclude: Vec<String>,
29 pub max_file_size: u64,
32 pub respect_gitignore: bool,
34 pub index_hidden: bool,
36 pub index_binary: bool,
39 pub index_empty: bool,
41 pub backend: Backend,
43 pub merge_threshold: usize,
45}
46
47impl Default for Config {
48 fn default() -> Self {
49 Self {
50 include: Vec::new(),
51 exclude: vec![
52 "**/.git/**".to_string(),
53 "**/node_modules/**".to_string(),
54 "**/target/**".to_string(),
55 "**/.greplm/**".to_string(),
56 ],
57 max_file_size: 4 * 1024 * 1024,
58 respect_gitignore: true,
59 index_hidden: false,
60 index_binary: false,
61 index_empty: false,
62 backend: Backend::Auto,
63 merge_threshold: 16,
64 }
65 }
66}
67
68impl Config {
69 pub fn load(path: &Path) -> Result<Config> {
70 let mut config = match std::fs::read_to_string(path) {
71 Ok(s) => toml::from_str(&s)?,
72 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Config::default(),
73 Err(e) => return Err(Error::io(path, e)),
74 };
75 config.apply_env_overrides();
76 Ok(config)
77 }
78
79 pub fn apply_env_overrides(&mut self) {
84 if let Some(v) = env_u64("GREPLM_MAX_FILE_SIZE") {
85 self.max_file_size = v;
86 }
87 if let Some(v) = env_bool("GREPLM_RESPECT_GITIGNORE") {
88 self.respect_gitignore = v;
89 }
90 if let Some(v) = env_bool("GREPLM_INDEX_HIDDEN") {
91 self.index_hidden = v;
92 }
93 if let Some(v) = env_bool("GREPLM_INDEX_BINARY") {
94 self.index_binary = v;
95 }
96 if let Some(v) = env_bool("GREPLM_INDEX_EMPTY") {
97 self.index_empty = v;
98 }
99 }
100
101 pub fn save(&self, path: &Path) -> Result<()> {
102 let s = toml::to_string_pretty(self)?;
103 std::fs::write(path, s).map_err(|e| Error::io(path, e))
104 }
105}
106
107fn env_bool(key: &str) -> Option<bool> {
110 let raw = std::env::var(key).ok()?;
111 match raw.trim().to_ascii_lowercase().as_str() {
112 "1" | "true" | "yes" | "on" => Some(true),
113 "0" | "false" | "no" | "off" => Some(false),
114 _ => None,
115 }
116}
117
118fn env_u64(key: &str) -> Option<u64> {
121 std::env::var(key).ok()?.trim().parse().ok()
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn defaults_are_grep_conservative() {
130 let c = Config::default();
131 assert!(!c.index_binary);
132 assert!(!c.index_empty);
133 assert_eq!(c.max_file_size, 4 * 1024 * 1024);
134 }
135
136 #[test]
137 fn env_bool_parses_common_forms() {
138 for (raw, want) in [
139 ("1", true),
140 ("TRUE", true),
141 ("Yes", true),
142 ("on", true),
143 ("0", false),
144 ("false", false),
145 ("off", false),
146 ] {
147 let key = format!("GREPLM_TEST_BOOL_{raw}");
149 std::env::set_var(&key, raw);
150 assert_eq!(env_bool(&key), Some(want), "raw={raw}");
151 std::env::remove_var(&key);
152 }
153 assert_eq!(env_bool("GREPLM_TEST_BOOL_UNSET_XYZ"), None);
154 }
155}