use std::path::{Path, PathBuf};
pub const CONFIG_DIR_ENV: &str = "FLOW_CONFIG_DIR";
#[must_use]
pub fn resolve_config_dir(cli_override: Option<&Path>) -> PathBuf {
let (dir, source) = if let Some(override_path) = cli_override {
(override_path.to_path_buf(), "CLI --config flag")
} else if let Ok(env_val) = std::env::var(CONFIG_DIR_ENV) {
(PathBuf::from(&env_val), "FLOW_CONFIG_DIR env var")
} else {
(default_config_dir(), "default (USERPROFILE/.config/flow)")
};
if let Err(e) = std::fs::create_dir_all(&dir) {
log::warn!(
"could not create config dir {:?} (source: {source}): {e}",
dir
);
}
log::info!("config dir resolved from {source}: {:?}", dir);
dir
}
#[must_use]
pub fn config_dir() -> PathBuf {
resolve_config_dir(None)
}
#[must_use]
pub fn user_rules_path() -> PathBuf {
resolve_config_dir(None).join("flow-rules.toml")
}
#[must_use]
pub fn user_rules_path_in(dir: &Path) -> PathBuf {
dir.join("flow-rules.toml")
}
#[must_use]
pub fn history_rules_path() -> PathBuf {
resolve_config_dir(None).join("history-flow-rules.toml")
}
#[must_use]
pub fn history_rules_path_in(dir: &Path) -> PathBuf {
dir.join("history-flow-rules.toml")
}
#[must_use]
pub fn user_app_config_path_in(dir: &Path) -> PathBuf {
dir.join("flow.toml")
}
#[must_use]
pub fn user_app_config_path() -> PathBuf {
config_dir().join("flow.toml")
}
#[must_use]
pub fn logs_dir_in(dir: &Path) -> PathBuf {
dir.join("logs")
}
#[must_use]
pub fn logs_dir() -> PathBuf {
logs_dir_in(&resolve_config_dir(None))
}
#[must_use]
pub fn log_file_path_in(dir: &Path, date: &str) -> PathBuf {
logs_dir_in(dir).join(format!("flowd-{date}.log"))
}
fn default_config_dir() -> PathBuf {
if let Ok(userprofile) = std::env::var("USERPROFILE") {
return PathBuf::from(userprofile).join(".config").join("flow");
}
if let Ok(appdata) = std::env::var("APPDATA") {
let base = PathBuf::from(&appdata);
let stripped = base
.parent() .and_then(|p| p.parent()); let user_home = match stripped {
Some(p) => p.to_path_buf(),
None => base,
};
log::warn!(
"USERPROFILE not set; falling back to APPDATA-derived path {:?}",
user_home
);
return user_home.join(".config").join("flow");
}
log::warn!("neither USERPROFILE nor APPDATA is set; falling back to '.' for config dir");
PathBuf::from(".").join("flow")
}
#[cfg(test)]
mod tests {
use super::*;
static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[test]
fn user_rules_path_ends_with_flow_rules_toml() {
let path = user_rules_path();
assert!(path.ends_with("flow-rules.toml"), "path was: {path:?}");
}
#[test]
fn user_rules_path_is_under_flow_subdir() {
let path = user_rules_path();
assert!(
path.to_string_lossy().contains("flow"),
"path should contain 'flow' subdir: {path:?}"
);
}
#[test]
fn user_rules_path_parent_is_flow() {
let path = user_rules_path();
assert_eq!(
path.parent().and_then(|p| p.file_name()),
Some(std::ffi::OsStr::new("flow")),
"parent dir should be 'flow': {path:?}"
);
}
#[test]
fn user_rules_path_returns_pathbuf() {
let path: PathBuf = user_rules_path();
assert!(!path.as_os_str().is_empty(), "path should not be empty");
}
#[test]
fn user_rules_path_filename_is_flow_rules_toml() {
let path = user_rules_path();
assert_eq!(
path.file_name(),
Some(std::ffi::OsStr::new("flow-rules.toml")),
"filename should be flow-rules.toml: {path:?}"
);
}
#[test]
fn resolve_config_dir_with_override_uses_override() {
let custom = Path::new("C:\\my-custom-config");
let dir = resolve_config_dir(Some(custom));
assert_eq!(dir, custom, "CLI override should be used directly");
}
#[test]
fn resolve_config_dir_without_override_uses_env() {
let _guard = ENV_LOCK.lock().unwrap();
let original = std::env::var(CONFIG_DIR_ENV).ok();
unsafe { std::env::set_var(CONFIG_DIR_ENV, "C:\\env-config\\flow") };
let dir = resolve_config_dir(None);
match original {
Some(val) => unsafe { std::env::set_var(CONFIG_DIR_ENV, val) },
None => unsafe { std::env::remove_var(CONFIG_DIR_ENV) },
}
assert_eq!(
dir,
PathBuf::from("C:\\env-config\\flow"),
"env var should override default"
);
}
#[test]
fn resolve_config_dir_without_anything_uses_default() {
let _guard = ENV_LOCK.lock().unwrap();
let original = std::env::var(CONFIG_DIR_ENV).ok();
unsafe { std::env::remove_var(CONFIG_DIR_ENV) };
let dir = resolve_config_dir(None);
if let Some(val) = original {
unsafe { std::env::set_var(CONFIG_DIR_ENV, val) }
}
let path_str = dir.to_string_lossy();
assert!(
path_str.contains(".config") && path_str.ends_with("flow"),
"default path should be …/.config/flow: {dir:?}"
);
}
#[test]
fn resolve_config_dir_cli_override_beats_env_var() {
let _guard = ENV_LOCK.lock().unwrap();
let original = std::env::var(CONFIG_DIR_ENV).ok();
unsafe { std::env::set_var(CONFIG_DIR_ENV, "C:\\env-config\\flow") };
let cli_path = Path::new("C:\\cli-override\\flow");
let dir = resolve_config_dir(Some(cli_path));
match original {
Some(val) => unsafe { std::env::set_var(CONFIG_DIR_ENV, val) },
None => unsafe { std::env::remove_var(CONFIG_DIR_ENV) },
}
assert_eq!(
dir, cli_path,
"CLI override must win over FLOW_CONFIG_DIR env var"
);
assert_ne!(
dir,
PathBuf::from("C:\\env-config\\flow"),
"env var path must not be returned when CLI override is given"
);
}
#[test]
fn user_app_config_path_returns_flow_toml() {
let path = user_app_config_path();
assert!(path.ends_with("flow.toml"), "path was: {path:?}");
}
#[test]
fn user_rules_path_in_returns_correct_path() {
let dir = Path::new("C:\\test\\config");
let path = user_rules_path_in(dir);
assert_eq!(path, dir.join("flow-rules.toml"));
}
#[test]
fn user_app_config_path_in_returns_correct_path() {
let dir = Path::new("C:\\test\\config");
let path = user_app_config_path_in(dir);
assert_eq!(path, dir.join("flow.toml"));
}
#[test]
fn logs_dir_in_appends_logs_subdir() {
let dir = Path::new("C:\\test\\config");
let logs = logs_dir_in(dir);
assert_eq!(logs, dir.join("logs"));
}
#[test]
fn logs_dir_ends_with_logs() {
let logs = logs_dir();
assert!(logs.ends_with("logs"), "logs dir was: {logs:?}");
}
#[test]
fn log_file_path_in_produces_dated_filename() {
let dir = Path::new("C:\\test\\config");
let path = log_file_path_in(dir, "2026-06-17");
assert!(path.ends_with("flowd-2026-06-17.log"), "path was: {path:?}");
}
#[test]
fn log_file_path_in_nests_under_logs() {
let dir = Path::new("C:\\test\\config");
let path = log_file_path_in(dir, "2026-06-17");
assert_eq!(
path.parent(),
Some(logs_dir_in(dir).as_path()),
"log file should be directly under the logs dir"
);
}
#[test]
fn log_file_path_in_distinguishes_dates() {
let dir = Path::new("C:\\test\\config");
let a = log_file_path_in(dir, "2026-06-17");
let b = log_file_path_in(dir, "2026-06-18");
assert_ne!(a, b, "different dates must produce different paths");
}
#[test]
fn history_rules_path_ends_with_history_flow_rules_toml() {
let path = history_rules_path();
assert!(
path.ends_with("history-flow-rules.toml"),
"path was: {path:?}"
);
}
#[test]
fn history_rules_path_in_returns_correct_path() {
let dir = Path::new("C:\\test\\config");
let path = history_rules_path_in(dir);
assert_eq!(path, dir.join("history-flow-rules.toml"));
}
}