Skip to main content

greplm_core/
config.rs

1//! Index configuration persisted at `.greplm/config.toml`.
2
3use serde::{Deserialize, Serialize};
4use std::path::Path;
5
6use crate::error::{Error, Result};
7
8/// Which ingest read backend to use.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
10#[serde(rename_all = "kebab-case")]
11pub enum Backend {
12    /// Portable rayon-based read pool (all platforms).
13    #[default]
14    Auto,
15    /// Force the portable backend.
16    Rayon,
17    /// Linux io_uring backend (requires the `io-uring` build feature).
18    IoUring,
19}
20
21/// Persistent project configuration.
22#[derive(Debug, Clone, Serialize, Deserialize)]
23#[serde(default)]
24pub struct Config {
25    /// Glob patterns to include (empty = all text files).
26    pub include: Vec<String>,
27    /// Extra glob patterns to exclude (on top of .gitignore).
28    pub exclude: Vec<String>,
29    /// Skip files larger than this many bytes. `0` disables the cap entirely
30    /// (grep parity: grep has no size limit).
31    pub max_file_size: u64,
32    /// Honor `.gitignore` / `.ignore` files during the walk.
33    pub respect_gitignore: bool,
34    /// Index hidden files and directories.
35    pub index_hidden: bool,
36    /// Index files containing NUL bytes (binary). Off by default; grep scans
37    /// such files (`grep -a`), so enabling this restores grep parity.
38    pub index_binary: bool,
39    /// Index empty (zero-byte) files. Off by default since they never match.
40    pub index_empty: bool,
41    /// Ingest read backend.
42    pub backend: Backend,
43    /// Merge segments automatically once this many accumulate.
44    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    /// Overlay `GREPLM_*` environment variables on top of the loaded config.
80    /// Env wins over the file so a one-off `GREPLM_INDEX_BINARY=1 greplm index`
81    /// works without editing `config.toml`. Unset or unparseable vars are
82    /// ignored (the file/default value stands).
83    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
107/// Parse a boolean environment variable. Accepts `1/true/yes/on` and
108/// `0/false/no/off` (case-insensitive); anything else (or unset) yields `None`.
109fn 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
118/// Parse an unsigned-integer environment variable; unset/unparseable yields
119/// `None`.
120fn 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            // SAFETY: single-threaded test; key is unique per case.
148            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}