Skip to main content

lowfat_core/
config.rs

1use crate::level::Level;
2use crate::pipeline::{ConditionalPipelines, parse_conditional_pipeline};
3use std::collections::{HashMap, HashSet};
4use std::env;
5use std::fs;
6use std::path::PathBuf;
7
8/// Resolved lowfat configuration from env + .lowfat file.
9#[derive(Debug)]
10pub struct RunfConfig {
11    pub level: Level,
12    pub disabled: HashSet<String>,
13    /// Some = whitelist mode (only these filters active)
14    pub allowed: Option<HashSet<String>>,
15    pub data_dir: PathBuf,
16    pub plugin_dir: PathBuf,
17    pub home_dir: PathBuf,
18    /// Per-command conditional pipelines from .lowfat config.
19    /// Supports: pipeline.git = ..., pipeline.git.error = ..., pipeline.git.large = ...
20    pub pipelines: HashMap<String, ConditionalPipelines>,
21}
22
23impl RunfConfig {
24    /// Resolve configuration from environment and .lowfat config walking.
25    pub fn resolve() -> Self {
26        let home_dir = env::var("LOWFAT_HOME")
27            .map(PathBuf::from)
28            .unwrap_or_else(|_| {
29                dirs_home().join(".lowfat")
30            });
31
32        let data_dir = env::var("LOWFAT_DATA")
33            .map(PathBuf::from)
34            .unwrap_or_else(|_| {
35                env::var("XDG_DATA_HOME")
36                    .map(PathBuf::from)
37                    .unwrap_or_else(|_| dirs_home().join(".local/share"))
38                    .join("lowfat")
39            });
40
41        let plugin_dir = home_dir.join("plugins");
42
43        // Level: LOWFAT_LEVEL env > .lowfat config > default
44        let mut level = Level::Full;
45        let mut disabled = HashSet::new();
46        let mut allowed: Option<HashSet<String>> = None;
47        // Collect raw pipeline lines for post-processing into ConditionalPipelines
48        // Key: (command, condition) e.g., ("git", "") or ("git", "error")
49        let mut pipeline_lines: HashMap<String, Vec<(String, String)>> = HashMap::new();
50        let mut pipelines = HashMap::new();
51
52        // Parse .lowfat config (walk up from cwd)
53        if let Some(config_path) = find_config() {
54            if let Ok(content) = fs::read_to_string(&config_path) {
55                for line in content.lines() {
56                    let line = line.trim();
57                    if line.is_empty() || line.starts_with('#') {
58                        continue;
59                    }
60                    if let Some(val) = line.strip_prefix("level=") {
61                        if let Ok(l) = val.parse() {
62                            level = l;
63                        }
64                    } else if let Some(val) = line.strip_prefix("filters=") {
65                        allowed = Some(
66                            val.split(',').map(|s| s.trim().to_string()).collect(),
67                        );
68                    } else if let Some(val) = line.strip_prefix("disable=") {
69                        for name in val.split(',') {
70                            disabled.insert(name.trim().to_string());
71                        }
72                    } else if let Some(rest) = line.strip_prefix("pipeline.") {
73                        // pipeline.git = strip-ansi | git-compact | truncate
74                        // pipeline.git.error = strip-ansi | head
75                        // pipeline.git.large = git-compact | token-budget
76                        if let Some((key, spec)) = rest.split_once('=') {
77                            let key = key.trim();
78                            let spec = spec.trim().to_string();
79                            // Split "git.error" → cmd="git", condition="error"
80                            let (cmd, condition) = match key.split_once('.') {
81                                Some((c, cond)) => (c.to_string(), cond.to_string()),
82                                None => (key.to_string(), String::new()),
83                            };
84                            pipeline_lines
85                                .entry(cmd)
86                                .or_default()
87                                .push((condition, spec));
88                        }
89                    }
90                }
91            }
92        }
93
94        // Build ConditionalPipelines from collected lines
95        for (cmd, lines) in pipeline_lines {
96            pipelines.insert(cmd, parse_conditional_pipeline(&lines));
97        }
98
99        // LOWFAT_DISABLE env overrides
100        if let Ok(val) = env::var("LOWFAT_DISABLE") {
101            for name in val.split(',') {
102                disabled.insert(name.trim().to_string());
103            }
104        }
105
106        // LOWFAT_LEVEL env takes highest priority
107        if let Ok(val) = env::var("LOWFAT_LEVEL") {
108            if let Ok(l) = val.parse() {
109                level = l;
110            }
111        }
112
113        RunfConfig {
114            level,
115            disabled,
116            allowed,
117            data_dir,
118            plugin_dir,
119            home_dir,
120            pipelines,
121        }
122    }
123
124    /// Get the conditional pipelines for a command, if configured.
125    pub fn pipeline_for(&self, cmd: &str) -> Option<&ConditionalPipelines> {
126        self.pipelines.get(cmd)
127    }
128
129    /// Check if a filter name is enabled under current config.
130    pub fn is_enabled(&self, name: &str) -> bool {
131        if self.disabled.contains(name) {
132            return false;
133        }
134        if let Some(ref allowed) = self.allowed {
135            return allowed.contains(name);
136        }
137        true
138    }
139}
140
141/// Walk up from cwd to find nearest `.lowfat` config file.
142pub fn find_config() -> Option<PathBuf> {
143    let mut dir = env::current_dir().ok()?;
144    loop {
145        let candidate = dir.join(".lowfat");
146        if candidate.is_file() {
147            return Some(candidate);
148        }
149        if !dir.pop() {
150            return None;
151        }
152    }
153}
154
155fn dirs_home() -> PathBuf {
156    env::var("HOME")
157        .map(PathBuf::from)
158        .unwrap_or_else(|_| PathBuf::from("/tmp"))
159}
160
161/// Find the .lowfat config path (exposed for display purposes).
162pub fn find_config_display() -> Option<PathBuf> {
163    find_config()
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn is_enabled_default() {
172        let config = RunfConfig {
173            level: Level::Full,
174            disabled: HashSet::new(),
175            allowed: None,
176            data_dir: PathBuf::new(),
177            plugin_dir: PathBuf::new(),
178            home_dir: PathBuf::new(),
179            pipelines: HashMap::new(),
180        };
181        assert!(config.is_enabled("git"));
182        assert!(config.is_enabled("docker"));
183    }
184
185    #[test]
186    fn is_enabled_disabled() {
187        let mut disabled = HashSet::new();
188        disabled.insert("npm".to_string());
189        let config = RunfConfig {
190            level: Level::Full,
191            disabled,
192            allowed: None,
193            data_dir: PathBuf::new(),
194            plugin_dir: PathBuf::new(),
195            home_dir: PathBuf::new(),
196            pipelines: HashMap::new(),
197        };
198        assert!(!config.is_enabled("npm"));
199        assert!(config.is_enabled("git"));
200    }
201
202    #[test]
203    fn is_enabled_whitelist() {
204        let mut allowed = HashSet::new();
205        allowed.insert("git".to_string());
206        allowed.insert("docker".to_string());
207        let config = RunfConfig {
208            level: Level::Full,
209            disabled: HashSet::new(),
210            allowed: Some(allowed),
211            data_dir: PathBuf::new(),
212            plugin_dir: PathBuf::new(),
213            home_dir: PathBuf::new(),
214            pipelines: HashMap::new(),
215        };
216        assert!(config.is_enabled("git"));
217        assert!(!config.is_enabled("npm"));
218    }
219}