use std::env;
use std::path::PathBuf;
use crate::APP_NAME;
pub fn home_dir() -> Option<PathBuf> {
resolve_home(
non_empty("HOME"),
non_empty("USERPROFILE"),
non_empty("HOMEDRIVE").zip(non_empty("HOMEPATH")),
)
}
fn non_empty(name: &str) -> Option<String> {
env::var(name).ok().filter(|value| !value.is_empty())
}
fn resolve_home(
home: Option<String>,
profile: Option<String>,
drive_and_path: Option<(String, String)>,
) -> Option<PathBuf> {
if let Some(home) = home {
return Some(PathBuf::from(home));
}
if let Some(profile) = profile {
return Some(PathBuf::from(profile));
}
drive_and_path.map(|(drive, path)| PathBuf::from(format!("{drive}{path}")))
}
fn xdg_dir(env_var: &str, fallback: &str) -> PathBuf {
app_dir(non_empty(env_var), home_dir(), fallback)
}
fn app_dir(
base: Option<String>,
home: Option<PathBuf>,
fallback: &str,
) -> PathBuf {
match base {
Some(base) => PathBuf::from(base).join(APP_NAME),
None => home
.unwrap_or_else(|| PathBuf::from("."))
.join(fallback)
.join(APP_NAME),
}
}
pub fn config_dir() -> PathBuf {
xdg_dir("XDG_CONFIG_HOME", ".config")
}
pub fn state_dir() -> PathBuf {
xdg_dir("XDG_STATE_HOME", ".local/state")
}
pub fn config_file() -> PathBuf {
config_dir().join("config.toml")
}
pub fn state_file() -> PathBuf {
state_dir().join("state.toml")
}
pub fn log_file() -> PathBuf {
state_dir().join("calcli.log")
}
#[cfg(test)]
mod tests {
use super::*;
fn some(value: &str) -> Option<String> {
Some(value.to_string())
}
#[test]
fn home_wins_over_the_windows_variables() {
let home = resolve_home(
some("/home/ada"),
some("C:\\Users\\Ada"),
Some(("C:".into(), "\\Users\\Ada".into())),
);
assert_eq!(home, Some(PathBuf::from("/home/ada")));
}
#[test]
fn user_profile_stands_in_when_home_is_unset() {
let home = resolve_home(None, some("C:\\Users\\Ada"), None);
assert_eq!(home, Some(PathBuf::from("C:\\Users\\Ada")));
}
#[test]
fn the_drive_and_path_pair_is_the_last_fallback() {
let home = resolve_home(
None,
None,
Some(("C:".into(), "\\Users\\Ada".into())),
);
assert_eq!(home, Some(PathBuf::from("C:\\Users\\Ada")));
}
#[test]
fn no_variable_at_all_yields_no_home() {
assert_eq!(resolve_home(None, None, None), None);
}
#[test]
fn an_xdg_base_is_used_verbatim_under_the_app_name() {
let dir = app_dir(some("/xdg/config"), None, ".config");
assert_eq!(dir, PathBuf::from("/xdg/config").join(APP_NAME));
}
#[test]
fn an_absent_xdg_base_falls_back_to_the_home_relative_default() {
let dir = app_dir(None, Some(PathBuf::from("/home/ada")), ".config");
assert_eq!(dir, PathBuf::from("/home/ada/.config").join(APP_NAME));
}
#[test]
fn without_a_home_the_default_is_rooted_at_the_current_directory() {
let dir = app_dir(None, None, ".local/state");
assert_eq!(dir, PathBuf::from("./.local/state").join(APP_NAME));
}
#[test]
fn an_empty_variable_reads_as_unset() {
const VAR: &str = "CALCLI_PATHS_TEST_VAR";
unsafe {
env::set_var(VAR, "");
}
assert_eq!(non_empty(VAR), None);
unsafe {
env::set_var(VAR, "/somewhere");
}
assert_eq!(non_empty(VAR), some("/somewhere"));
unsafe {
env::remove_var(VAR);
}
assert_eq!(non_empty(VAR), None);
}
#[test]
fn the_file_paths_sit_inside_their_directories() {
assert_eq!(config_file(), config_dir().join("config.toml"));
assert_eq!(state_file(), state_dir().join("state.toml"));
assert_eq!(log_file(), state_dir().join("calcli.log"));
}
}