use std::env;
use std::path::{Path, PathBuf};
use crate::util::app_info::APP_NAME;
type EnvLookup<'a> = &'a dyn Fn(&str) -> Option<String>;
fn process_env(name: &str) -> Option<String> {
env::var(name).ok().filter(|value| !value.is_empty())
}
pub fn home_dir() -> Option<PathBuf> {
resolve_home(&process_env)
}
fn resolve_home(lookup: EnvLookup) -> Option<PathBuf> {
if let Some(home) = lookup("HOME") {
return Some(PathBuf::from(home));
}
if let Some(profile) = lookup("USERPROFILE") {
return Some(PathBuf::from(profile));
}
match (lookup("HOMEDRIVE"), lookup("HOMEPATH")) {
(Some(drive), Some(path)) => {
Some(PathBuf::from(format!("{drive}{path}")))
}
_ => None,
}
}
pub fn expand_tilde(path: &str) -> PathBuf {
expand_tilde_from(path, home_dir())
}
fn expand_tilde_from(path: &str, home: Option<PathBuf>) -> PathBuf {
if path == "~" {
return home.unwrap_or_else(|| PathBuf::from(path));
}
if let Some(rest) = path.strip_prefix("~/")
&& let Some(home) = home
{
return home.join(rest);
}
PathBuf::from(path)
}
fn xdg_dir(env_var: &str, fallback: &str) -> PathBuf {
resolve_xdg_dir(env_var, fallback, &process_env)
}
fn resolve_xdg_dir(
env_var: &str,
fallback: &str,
lookup: EnvLookup,
) -> PathBuf {
if let Some(base) = lookup(env_var)
&& Path::new(&base).is_absolute()
{
return PathBuf::from(base).join(APP_NAME);
}
let home = resolve_home(lookup).unwrap_or_else(|| PathBuf::from("."));
home.join(fallback).join(APP_NAME)
}
#[cfg(windows)]
fn windows_dir(env_var: &str) -> Option<PathBuf> {
env::var(env_var)
.ok()
.filter(|base| !base.is_empty())
.map(|base| PathBuf::from(base).join(APP_NAME))
}
pub fn config_dir() -> PathBuf {
#[cfg(windows)]
if let Some(dir) = windows_dir("APPDATA") {
return dir;
}
xdg_dir("XDG_CONFIG_HOME", ".config")
}
pub fn state_dir() -> PathBuf {
#[cfg(windows)]
if let Some(dir) = windows_dir("LOCALAPPDATA") {
return dir;
}
xdg_dir("XDG_STATE_HOME", ".local/state")
}
pub fn config_file() -> PathBuf {
config_dir().join("config.toml")
}
pub fn cache_file() -> PathBuf {
state_dir().join("git-info-cache.toml")
}
pub fn usage_file() -> PathBuf {
state_dir().join("usage.toml")
}
pub fn selected_repo_file() -> PathBuf {
state_dir().join("selected-repo.txt")
}
pub fn log_file() -> PathBuf {
state_dir().join("hop.log")
}
pub fn ui_state_file() -> PathBuf {
state_dir().join("ui-state.toml")
}
pub fn stats_file() -> PathBuf {
state_dir().join("stats-cache.toml")
}
#[cfg(test)]
mod tests {
use super::*;
fn env_of(pairs: &[(&str, &str)]) -> impl Fn(&str) -> Option<String> {
let owned: Vec<(String, String)> = pairs
.iter()
.map(|(k, v)| ((*k).to_string(), (*v).to_string()))
.collect();
move |name| {
owned
.iter()
.find(|(key, _)| key == name)
.map(|(_, value)| value.clone())
.filter(|value| !value.is_empty())
}
}
#[test]
fn home_prefers_the_unix_variable_then_the_windows_ones() {
let both = env_of(&[("HOME", "/home/c"), ("USERPROFILE", "C:\\Users")]);
assert_eq!(resolve_home(&both), Some(PathBuf::from("/home/c")));
let windows = env_of(&[("USERPROFILE", "C:\\Users\\c")]);
assert_eq!(resolve_home(&windows), Some(PathBuf::from("C:\\Users\\c")));
let split = env_of(&[("HOMEDRIVE", "C:"), ("HOMEPATH", "\\Users\\c")]);
assert_eq!(resolve_home(&split), Some(PathBuf::from("C:\\Users\\c")));
assert_eq!(resolve_home(&env_of(&[])), None);
}
#[test]
fn an_empty_home_counts_as_unset() {
assert_eq!(resolve_home(&env_of(&[("HOME", "")])), None);
}
#[test]
fn an_absolute_xdg_value_is_used() {
let env = env_of(&[("XDG_CONFIG_HOME", "/xdg/config")]);
assert_eq!(
resolve_xdg_dir("XDG_CONFIG_HOME", ".config", &env),
PathBuf::from("/xdg/config/hop")
);
}
#[test]
fn an_empty_or_relative_xdg_value_is_ignored_per_the_specification() {
for value in ["", "relative/path", "./state"] {
let pairs = [("XDG_STATE_HOME", value), ("HOME", "/home/c")];
let env = env_of(&pairs);
assert_eq!(
resolve_xdg_dir("XDG_STATE_HOME", ".local/state", &env),
PathBuf::from("/home/c/.local/state/hop"),
"{value:?} must fall back to the home default"
);
}
}
#[test]
fn a_missing_home_leaves_the_xdg_fallback_relative_to_the_cwd() {
assert_eq!(
resolve_xdg_dir("XDG_CONFIG_HOME", ".config", &env_of(&[])),
PathBuf::from("./.config/hop")
);
}
#[test]
fn tilde_expands_only_at_the_start_of_a_path() {
let home = Some(PathBuf::from("/home/c"));
assert_eq!(
expand_tilde_from("~", home.clone()),
PathBuf::from("/home/c")
);
assert_eq!(
expand_tilde_from("~/code/hop", home.clone()),
PathBuf::from("/home/c/code/hop")
);
assert_eq!(
expand_tilde_from("/tmp/~/x", home.clone()),
PathBuf::from("/tmp/~/x")
);
assert_eq!(
expand_tilde_from("~user/x", home),
PathBuf::from("~user/x")
);
}
#[test]
fn tilde_stays_verbatim_when_there_is_no_home() {
assert_eq!(expand_tilde_from("~", None), PathBuf::from("~"));
assert_eq!(expand_tilde_from("~/code", None), PathBuf::from("~/code"));
}
}