use crate::level::Level;
use crate::pipeline::{ConditionalPipelines, parse_conditional_pipeline};
use std::collections::{HashMap, HashSet};
use std::env;
use std::fs;
use std::path::PathBuf;
#[derive(Debug)]
pub struct RunfConfig {
pub level: Level,
pub disabled: HashSet<String>,
pub allowed: Option<HashSet<String>>,
pub data_dir: PathBuf,
pub plugin_dir: PathBuf,
pub home_dir: PathBuf,
pub pipelines: HashMap<String, ConditionalPipelines>,
}
impl RunfConfig {
pub fn resolve() -> Self {
let lowfat_home = env::var("LOWFAT_HOME").ok();
let xdg_config_home = env::var("XDG_CONFIG_HOME").ok();
let home = dirs_home();
let home_dir = resolve_home_dir(
lowfat_home.as_deref(),
xdg_config_home.as_deref(),
&home,
&|p| p.exists(),
);
let data_dir = env::var("LOWFAT_DATA")
.map(PathBuf::from)
.unwrap_or_else(|_| {
env::var("XDG_DATA_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| dirs_home().join(".local/share"))
.join("lowfat")
});
let plugin_dir = home_dir.join("plugins");
let mut level = Level::Full;
let mut disabled = HashSet::new();
let mut allowed: Option<HashSet<String>> = None;
let mut pipeline_lines: HashMap<String, Vec<(String, String)>> = HashMap::new();
let mut pipelines = HashMap::new();
if let Some(config_path) = find_config() {
if let Ok(content) = fs::read_to_string(&config_path) {
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some(val) = line.strip_prefix("level=") {
if let Ok(l) = val.parse() {
level = l;
}
} else if let Some(val) = line.strip_prefix("filters=") {
allowed = Some(
val.split(',').map(|s| s.trim().to_string()).collect(),
);
} else if let Some(val) = line.strip_prefix("disable=") {
for name in val.split(',') {
disabled.insert(name.trim().to_string());
}
} else if let Some(rest) = line.strip_prefix("pipeline.") {
if let Some((key, spec)) = rest.split_once('=') {
let key = key.trim();
let spec = spec.trim().to_string();
let (cmd, condition) = match key.split_once('.') {
Some((c, cond)) => (c.to_string(), cond.to_string()),
None => (key.to_string(), String::new()),
};
pipeline_lines
.entry(cmd)
.or_default()
.push((condition, spec));
}
}
}
}
}
for (cmd, lines) in pipeline_lines {
pipelines.insert(cmd, parse_conditional_pipeline(&lines));
}
if let Ok(val) = env::var("LOWFAT_DISABLE") {
for name in val.split(',') {
disabled.insert(name.trim().to_string());
}
}
if let Ok(val) = env::var("LOWFAT_LEVEL") {
if let Ok(l) = val.parse() {
level = l;
}
}
RunfConfig {
level,
disabled,
allowed,
data_dir,
plugin_dir,
home_dir,
pipelines,
}
}
pub fn pipeline_for(&self, cmd: &str) -> Option<&ConditionalPipelines> {
self.pipelines.get(cmd)
}
pub fn is_enabled(&self, name: &str) -> bool {
if self.disabled.contains(name) {
return false;
}
if let Some(ref allowed) = self.allowed {
return allowed.contains(name);
}
true
}
}
pub fn find_config() -> Option<PathBuf> {
let mut dir = env::current_dir().ok()?;
loop {
let candidate = dir.join(".lowfat");
if candidate.is_file() {
return Some(candidate);
}
if !dir.pop() {
return None;
}
}
}
fn dirs_home() -> PathBuf {
env::var("HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("/tmp"))
}
pub fn resolve_home_dir(
lowfat_home: Option<&str>,
xdg_config_home: Option<&str>,
home: &std::path::Path,
path_exists: &dyn Fn(&std::path::Path) -> bool,
) -> PathBuf {
if let Some(h) = lowfat_home {
return PathBuf::from(h);
}
let dot_lowfat = home.join(".lowfat");
if let Some(xdg) = xdg_config_home {
let xdg_path = PathBuf::from(xdg).join("lowfat");
warn_if_both_exist(&xdg_path, &dot_lowfat, path_exists);
return xdg_path;
}
let xdg_default = home.join(".config").join("lowfat");
if path_exists(&xdg_default) {
warn_if_both_exist(&xdg_default, &dot_lowfat, path_exists);
return xdg_default;
}
dot_lowfat
}
fn warn_if_both_exist(
chosen: &std::path::Path,
other: &std::path::Path,
path_exists: &dyn Fn(&std::path::Path) -> bool,
) {
if chosen != other && path_exists(chosen) && path_exists(other) {
eprintln!(
"[lowfat] warning: both {} and {} exist; using {}. Remove one to silence this.",
chosen.display(),
other.display(),
chosen.display(),
);
}
}
pub fn find_config_display() -> Option<PathBuf> {
find_config()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_enabled_default() {
let config = RunfConfig {
level: Level::Full,
disabled: HashSet::new(),
allowed: None,
data_dir: PathBuf::new(),
plugin_dir: PathBuf::new(),
home_dir: PathBuf::new(),
pipelines: HashMap::new(),
};
assert!(config.is_enabled("git"));
assert!(config.is_enabled("docker"));
}
#[test]
fn is_enabled_disabled() {
let mut disabled = HashSet::new();
disabled.insert("npm".to_string());
let config = RunfConfig {
level: Level::Full,
disabled,
allowed: None,
data_dir: PathBuf::new(),
plugin_dir: PathBuf::new(),
home_dir: PathBuf::new(),
pipelines: HashMap::new(),
};
assert!(!config.is_enabled("npm"));
assert!(config.is_enabled("git"));
}
#[test]
fn home_explicit_lowfat_home_wins() {
let r = resolve_home_dir(
Some("/custom/lf"),
Some("/wrong/.config"),
std::path::Path::new("/home/user"),
&|_| true,
);
assert_eq!(r, PathBuf::from("/custom/lf"));
}
#[test]
fn home_xdg_env_used_when_set_even_if_path_missing() {
let r = resolve_home_dir(
None,
Some("/explicit/.config"),
std::path::Path::new("/home/user"),
&|_| false,
);
assert_eq!(r, PathBuf::from("/explicit/.config/lowfat"));
}
#[test]
fn home_xdg_default_used_when_path_exists() {
let home = PathBuf::from("/home/user");
let xdg = home.join(".config/lowfat");
let r = resolve_home_dir(None, None, &home, &|p| p == xdg.as_path());
assert_eq!(r, xdg);
}
#[test]
fn home_dot_lowfat_used_when_neither_xdg_set_nor_exists() {
let r = resolve_home_dir(
None,
None,
std::path::Path::new("/home/user"),
&|_| false,
);
assert_eq!(r, PathBuf::from("/home/user/.lowfat"));
}
#[test]
fn home_dot_lowfat_used_when_only_it_exists() {
let home = PathBuf::from("/home/user");
let dot_lowfat = home.join(".lowfat");
let r = resolve_home_dir(None, None, &home, &|p| p == dot_lowfat.as_path());
assert_eq!(r, dot_lowfat);
}
#[test]
fn home_xdg_wins_when_both_exist() {
let home = PathBuf::from("/home/user");
let r = resolve_home_dir(None, None, &home, &|_| true);
assert_eq!(r, home.join(".config/lowfat"));
}
#[test]
fn is_enabled_whitelist() {
let mut allowed = HashSet::new();
allowed.insert("git".to_string());
allowed.insert("docker".to_string());
let config = RunfConfig {
level: Level::Full,
disabled: HashSet::new(),
allowed: Some(allowed),
data_dir: PathBuf::new(),
plugin_dir: PathBuf::new(),
home_dir: PathBuf::new(),
pipelines: HashMap::new(),
};
assert!(config.is_enabled("git"));
assert!(!config.is_enabled("npm"));
}
}