use std::path::{Path, PathBuf};
use crate::error::{Error, Result};
pub fn home_dir() -> Result<PathBuf> {
#[cfg(unix)]
{
std::env::var_os("HOME").map(PathBuf::from).ok_or_else(|| {
Error::User("could not determine home directory ($HOME is unset)".into())
})
}
#[cfg(windows)]
{
std::env::var_os("USERPROFILE")
.map(PathBuf::from)
.ok_or_else(|| {
Error::User("could not determine home directory (%USERPROFILE% is unset)".into())
})
}
}
pub fn expand_tilde(path: &Path) -> Result<PathBuf> {
let s = path.to_string_lossy();
if s == "~" {
home_dir()
} else if let Some(rest) = s.strip_prefix("~/") {
Ok(home_dir()?.join(rest))
} else {
Ok(path.to_path_buf())
}
}
pub fn collapse_tilde(path: &Path) -> PathBuf {
if let Ok(home) = home_dir() {
if let Ok(rest) = path.strip_prefix(&home) {
return PathBuf::from("~").join(rest);
}
}
path.to_path_buf()
}
pub fn relative_to(target: &Path, base: &Path) -> Option<PathBuf> {
if !target.is_absolute() || !base.is_absolute() {
return None;
}
let mut target_parts = target.components().peekable();
let mut base_parts = base.components().peekable();
while let (Some(a), Some(b)) = (target_parts.peek(), base_parts.peek()) {
if a != b {
break;
}
target_parts.next();
base_parts.next();
}
let mut result = PathBuf::new();
for _ in base_parts {
result.push("..");
}
for part in target_parts {
result.push(part);
}
Some(result)
}
const CATEGORY_RULES: &[(&str, &[&str])] = &[
(
"shell",
&[
".zshrc",
".zshenv",
".zprofile",
".bashrc",
".bash_profile",
".profile",
".fishrc",
],
),
("git", &[".gitconfig", ".gitignore_global"]),
("vim", &[".vimrc", ".gvimrc"]),
("tmux", &[".tmux.conf"]),
("ssh", &[".ssh"]),
("gnupg", &[".gnupg"]),
];
pub fn map_to_repo(home_path: &Path) -> Result<PathBuf> {
let home = home_dir()?;
let rel = home_path.strip_prefix(&home).map_err(|_| {
Error::User(format!(
"`{}` is not inside the home directory",
home_path.display()
))
})?;
let rel_str = rel.to_string_lossy();
if let Some(rest) = rel_str.strip_prefix(".config/") {
return Ok(PathBuf::from("config").join(rest));
}
if rel_str == ".config" {
return Ok(PathBuf::from("config"));
}
let file_name = rel
.file_name()
.map(|f| f.to_string_lossy())
.unwrap_or_default();
for &(category, patterns) in CATEGORY_RULES {
for &pattern in patterns {
if rel_str == pattern.trim_start_matches('.') || file_name == pattern {
let clean_name = file_name.trim_start_matches('.');
return Ok(PathBuf::from(category).join(clean_name));
}
}
}
let clean = rel_str.trim_start_matches('.');
Ok(PathBuf::from("home").join(clean))
}
pub fn resolve(path: &Path) -> Result<PathBuf> {
let expanded = expand_tilde(path)?;
if expanded.is_absolute() {
Ok(normalize(&expanded))
} else {
let cwd =
std::env::current_dir().map_err(|e| Error::io(".", "get current directory", e))?;
Ok(normalize(&cwd.join(&expanded)))
}
}
fn normalize(path: &Path) -> PathBuf {
let mut parts = Vec::new();
for component in path.components() {
match component {
std::path::Component::ParentDir => {
parts.pop();
}
std::path::Component::CurDir => {}
other => parts.push(other),
}
}
parts.iter().collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tilde_expansion() {
let expanded = expand_tilde(Path::new("~/test")).unwrap();
assert!(expanded.is_absolute());
assert!(expanded.ends_with("test"));
}
#[test]
fn tilde_collapse() {
let home = home_dir().unwrap();
let path = home.join("Documents/file.txt");
assert_eq!(collapse_tilde(&path), PathBuf::from("~/Documents/file.txt"));
}
#[test]
fn relative_path() {
let r = relative_to(Path::new("/a/b/c"), Path::new("/a/d")).unwrap();
assert_eq!(r, PathBuf::from("../b/c"));
}
#[test]
fn config_mapping() {
let home = home_dir().unwrap();
let p = home.join(".config/nvim/init.lua");
assert_eq!(
map_to_repo(&p).unwrap(),
PathBuf::from("config/nvim/init.lua")
);
}
#[test]
fn shell_mapping() {
let home = home_dir().unwrap();
let p = home.join(".zshrc");
assert_eq!(map_to_repo(&p).unwrap(), PathBuf::from("shell/zshrc"));
}
#[test]
fn default_mapping() {
let home = home_dir().unwrap();
let p = home.join(".some_random_rc");
assert_eq!(
map_to_repo(&p).unwrap(),
PathBuf::from("home/some_random_rc")
);
}
#[test]
fn normalize_dots() {
let p = normalize(Path::new("/a/b/../c/./d"));
assert_eq!(p, PathBuf::from("/a/c/d"));
}
}