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)]
13#[serde(rename_all = "kebab-case")]
14pub enum Backend {
15 #[default]
17 Auto,
18 Rayon,
20 #[serde(rename = "io-uring")]
26 IoUringRemoved,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31#[serde(default)]
32pub struct Config {
33 pub include: Vec<String>,
35 pub exclude: Vec<String>,
37 pub max_file_size: u64,
40 pub respect_gitignore: bool,
42 pub index_hidden: bool,
44 pub index_binary: bool,
47 pub index_empty: bool,
49 pub backend: Backend,
51 pub merge_threshold: usize,
53}
54
55impl Default for Config {
56 fn default() -> Self {
57 Self {
58 include: Vec::new(),
59 exclude: vec![
60 "**/.git/**".to_string(),
61 "**/node_modules/**".to_string(),
62 "**/target/**".to_string(),
63 "**/.greplm/**".to_string(),
64 ],
65 max_file_size: 4 * 1024 * 1024,
66 respect_gitignore: true,
67 index_hidden: false,
68 index_binary: false,
69 index_empty: false,
70 backend: Backend::Auto,
71 merge_threshold: 16,
72 }
73 }
74}
75
76impl Config {
77 pub fn load(path: &Path) -> Result<Config> {
78 let mut config = match std::fs::read_to_string(path) {
79 Ok(s) => toml::from_str(&s)?,
80 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Config::default(),
81 Err(e) => return Err(Error::io(path, e)),
82 };
83 config.apply_env_overrides();
84 Ok(config)
85 }
86
87 pub fn apply_env_overrides(&mut self) {
92 if let Some(v) = env_u64("GREPLM_MAX_FILE_SIZE") {
93 self.max_file_size = v;
94 }
95 if let Some(v) = env_bool("GREPLM_RESPECT_GITIGNORE") {
96 self.respect_gitignore = v;
97 }
98 if let Some(v) = env_bool("GREPLM_INDEX_HIDDEN") {
99 self.index_hidden = v;
100 }
101 if let Some(v) = env_bool("GREPLM_INDEX_BINARY") {
102 self.index_binary = v;
103 }
104 if let Some(v) = env_bool("GREPLM_INDEX_EMPTY") {
105 self.index_empty = v;
106 }
107 }
108
109 pub fn save(&self, path: &Path) -> Result<()> {
110 let s = toml::to_string_pretty(self)?;
111 std::fs::write(path, s).map_err(|e| Error::io(path, e))
112 }
113}
114
115fn env_bool(key: &str) -> Option<bool> {
118 let raw = std::env::var(key).ok()?;
119 match raw.trim().to_ascii_lowercase().as_str() {
120 "1" | "true" | "yes" | "on" => Some(true),
121 "0" | "false" | "no" | "off" => Some(false),
122 _ => None,
123 }
124}
125
126fn env_u64(key: &str) -> Option<u64> {
129 std::env::var(key).ok()?.trim().parse().ok()
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn defaults_are_grep_conservative() {
138 let c = Config::default();
139 assert!(!c.index_binary);
140 assert!(!c.index_empty);
141 assert_eq!(c.max_file_size, 4 * 1024 * 1024);
142 }
143
144 #[test]
145 fn env_bool_parses_common_forms() {
146 for (raw, want) in [
147 ("1", true),
148 ("TRUE", true),
149 ("Yes", true),
150 ("on", true),
151 ("0", false),
152 ("false", false),
153 ("off", false),
154 ] {
155 let key = format!("GREPLM_TEST_BOOL_{raw}");
157 std::env::set_var(&key, raw);
158 assert_eq!(env_bool(&key), Some(want), "raw={raw}");
159 std::env::remove_var(&key);
160 }
161 assert_eq!(env_bool("GREPLM_TEST_BOOL_UNSET_XYZ"), None);
162 }
163}