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
//! Configuration loading for ix.
//!
//! The daemon discovers `.ixd.toml` files to scope its watch and index
//! behaviour. Each config file specifies which subdirectories to watch
//! and which patterns to exclude.
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
/// ix runtime configuration, loaded from `.ixd.toml`.
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Config {
/// Root directories to watch for indexing.
#[serde(default)]
pub watch_roots: Vec<PathBuf>,
/// Glob patterns for paths to exclude from indexing.
#[serde(default)]
pub exclude_patterns: Vec<String>,
/// Debounce interval in milliseconds for file-watch events.
///
/// Minimum 50 ms, maximum 10000 ms (clamped by [`crate::watcher::Watcher::with_debounce`]).
/// `None` uses the default (500 ms).
///
/// # Merged-config precedence
///
/// When [`discover_under`](Self::discover_under) merges multiple `.ixd.toml`
/// files, the **root-level** `debounce_ms` takes precedence over subdirectory
/// configs. Subdirectory `debounce_ms` values are ignored to avoid conflicting
/// timer strategies within a single daemon instance. If you need different
/// debounce intervals per watched subtree, run separate `ixd` instances.
#[serde(default)]
pub debounce_ms: Option<u64>,
}
impl Default for Config {
fn default() -> Self {
Self {
watch_roots: Vec::new(),
// Default exclude patterns for the `.ixd.toml` daemon config.
// The daemon (daemon.rs) reads these and bridges them to both
// Builder (via with_exclude_patterns) and Watcher (via Watcher::new).
// See also: src/lib/builder.rs:226, src/lib/watcher.rs:40
exclude_patterns: vec![
".git".to_string(),
"node_modules".to_string(),
"target".to_string(),
],
debounce_ms: None,
}
}
}
impl Config {
/// Load configuration from a `.ixd.toml` file at the given path.
///
/// # Errors
///
/// Returns an error if the file cannot be read or parsed.
pub fn load(path: &Path) -> crate::error::Result<Self> {
let content = std::fs::read_to_string(path).map_err(|e| {
crate::error::Error::Config(format!("cannot read config file {}: {e}", path.display()))
})?;
toml::from_str(&content).map_err(|e| {
crate::error::Error::Config(format!("cannot parse config file {}: {e}", path.display()))
})
}
/// Discover `.ixd.toml` files under the given root directory by
/// walking up to two levels deep.
///
/// Returns the **merged** configuration: `exclude_patterns` from
/// the root-level config are applied globally; `watch_roots` from
/// each discovered file scope the daemon to those subdirectories.
///
/// # Errors
///
/// Returns an error only if a discovered file cannot be parsed.
/// Missing or absent config files are silently skipped.
pub fn discover_under(root: &Path) -> crate::error::Result<Self> {
let root_config_path = root.join(".ixd.toml");
let mut merged = if root_config_path.exists() {
Self::load(&root_config_path)?
} else {
Self::default()
};
// Walk one level of subdirectories looking for `.ixd.toml`
if let Ok(entries) = std::fs::read_dir(root) {
for entry in entries.flatten() {
let sub_path = entry.path();
if sub_path.is_dir() {
let config_path = sub_path.join(".ixd.toml");
if config_path.exists()
&& let Ok(sub_config) = Self::load(&config_path)
{
if !sub_config.watch_roots.is_empty() {
merged.watch_roots.extend(sub_config.watch_roots);
}
merged
.exclude_patterns
.extend(sub_config.exclude_patterns.clone());
}
}
}
}
// Deduplicate
merged.watch_roots.sort();
merged.watch_roots.dedup();
merged.exclude_patterns.sort();
merged.exclude_patterns.dedup();
Ok(merged)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_config_default() {
let config = Config::default();
assert_eq!(
config,
Config {
watch_roots: Vec::new(),
exclude_patterns: vec![
".git".to_string(),
"node_modules".to_string(),
"target".to_string(),
],
debounce_ms: None,
}
);
}
}