1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
//! Index configuration persisted at `.greplm/config.toml`.
use serde::{Deserialize, Serialize};
use std::path::Path;
use crate::error::{Error, Result};
/// Which ingest read backend to use.
///
/// Only the portable backend exists today; the enum is kept so `config.toml`
/// stays forward-compatible if a batched-submission backend is added.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
pub enum Backend {
/// Portable rayon-based read pool (all platforms).
#[default]
Auto,
/// Force the portable backend.
Rayon,
/// Accepted only for compatibility with configs written before 0.5.0, when
/// this value selected a stub that did buffered reads anyway. Resolves to
/// [`Backend::Rayon`] with a warning, so an existing `config.toml` keeps
/// working instead of failing every command over a value that never did
/// anything.
#[serde(rename = "io-uring")]
IoUringRemoved,
}
/// Persistent project configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct Config {
/// Glob patterns to include (empty = all text files).
pub include: Vec<String>,
/// Extra glob patterns to exclude (on top of .gitignore).
pub exclude: Vec<String>,
/// Skip files larger than this many bytes. `0` disables the cap entirely
/// (grep parity: grep has no size limit).
pub max_file_size: u64,
/// Honor `.gitignore` / `.ignore` files during the walk.
pub respect_gitignore: bool,
/// Index hidden files and directories.
pub index_hidden: bool,
/// Index files containing NUL bytes (binary). Off by default; grep scans
/// such files (`grep -a`), so enabling this restores grep parity.
pub index_binary: bool,
/// Index empty (zero-byte) files. Off by default since they never match.
pub index_empty: bool,
/// Ingest read backend.
pub backend: Backend,
/// Merge segments automatically once this many accumulate.
pub merge_threshold: usize,
}
impl Default for Config {
fn default() -> Self {
Self {
include: Vec::new(),
exclude: vec![
"**/.git/**".to_string(),
"**/node_modules/**".to_string(),
"**/target/**".to_string(),
"**/.greplm/**".to_string(),
],
max_file_size: 4 * 1024 * 1024,
respect_gitignore: true,
index_hidden: false,
index_binary: false,
index_empty: false,
backend: Backend::Auto,
merge_threshold: 16,
}
}
}
impl Config {
pub fn load(path: &Path) -> Result<Config> {
let mut config = match std::fs::read_to_string(path) {
Ok(s) => toml::from_str(&s)?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Config::default(),
Err(e) => return Err(Error::io(path, e)),
};
config.apply_env_overrides();
Ok(config)
}
/// Overlay `GREPLM_*` environment variables on top of the loaded config.
/// Env wins over the file so a one-off `GREPLM_INDEX_BINARY=1 greplm index`
/// works without editing `config.toml`. Unset or unparseable vars are
/// ignored (the file/default value stands).
pub fn apply_env_overrides(&mut self) {
if let Some(v) = env_u64("GREPLM_MAX_FILE_SIZE") {
self.max_file_size = v;
}
if let Some(v) = env_bool("GREPLM_RESPECT_GITIGNORE") {
self.respect_gitignore = v;
}
if let Some(v) = env_bool("GREPLM_INDEX_HIDDEN") {
self.index_hidden = v;
}
if let Some(v) = env_bool("GREPLM_INDEX_BINARY") {
self.index_binary = v;
}
if let Some(v) = env_bool("GREPLM_INDEX_EMPTY") {
self.index_empty = v;
}
}
pub fn save(&self, path: &Path) -> Result<()> {
let s = toml::to_string_pretty(self)?;
std::fs::write(path, s).map_err(|e| Error::io(path, e))
}
}
/// Parse a boolean environment variable. Accepts `1/true/yes/on` and
/// `0/false/no/off` (case-insensitive); anything else (or unset) yields `None`.
fn env_bool(key: &str) -> Option<bool> {
let raw = std::env::var(key).ok()?;
match raw.trim().to_ascii_lowercase().as_str() {
"1" | "true" | "yes" | "on" => Some(true),
"0" | "false" | "no" | "off" => Some(false),
_ => None,
}
}
/// Parse an unsigned-integer environment variable; unset/unparseable yields
/// `None`.
fn env_u64(key: &str) -> Option<u64> {
std::env::var(key).ok()?.trim().parse().ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_are_grep_conservative() {
let c = Config::default();
assert!(!c.index_binary);
assert!(!c.index_empty);
assert_eq!(c.max_file_size, 4 * 1024 * 1024);
}
#[test]
fn env_bool_parses_common_forms() {
for (raw, want) in [
("1", true),
("TRUE", true),
("Yes", true),
("on", true),
("0", false),
("false", false),
("off", false),
] {
// SAFETY: single-threaded test; key is unique per case.
let key = format!("GREPLM_TEST_BOOL_{raw}");
std::env::set_var(&key, raw);
assert_eq!(env_bool(&key), Some(want), "raw={raw}");
std::env::remove_var(&key);
}
assert_eq!(env_bool("GREPLM_TEST_BOOL_UNSET_XYZ"), None);
}
}