use camino::{Utf8Component, Utf8Path, Utf8PathBuf};
pub fn expand_tilde(s: &str) -> Utf8PathBuf {
match home_dir() {
Some(home) => expand_tilde_with(s, &home),
None => Utf8PathBuf::from(s),
}
}
pub fn expand_tilde_with(s: &str, home: &Utf8Path) -> Utf8PathBuf {
if let Some(rest) = s.strip_prefix("~/").or_else(|| s.strip_prefix("~\\")) {
home.join(rest)
} else if s == "~" {
home.to_path_buf()
} else {
Utf8PathBuf::from(s)
}
}
pub fn home_dir() -> Option<Utf8PathBuf> {
std::env::var("HOME")
.ok()
.or_else(|| std::env::var("USERPROFILE").ok())
.map(Utf8PathBuf::from)
}
pub fn source_walker(source: &Utf8Path) -> ignore::WalkBuilder {
let mut b = ignore::WalkBuilder::new(source);
b.hidden(false).git_ignore(false).ignore(false);
b.filter_entry(|entry| entry.file_name() != ".yui");
b
}
pub fn mirror_into_backup(backup_root: &Utf8Path, abs_target: &Utf8Path) -> Utf8PathBuf {
let mut out = backup_root.to_path_buf();
for component in abs_target.components() {
match component {
Utf8Component::Prefix(p) => {
let s = p.as_str().trim_end_matches(':');
if !s.is_empty() {
out.push(s);
}
}
Utf8Component::RootDir | Utf8Component::CurDir => {}
Utf8Component::ParentDir => {}
Utf8Component::Normal(s) => {
out.push(s);
}
}
}
out
}
pub fn append_timestamp(path: &Utf8Path, ts: &str) -> Utf8PathBuf {
let parent = path.parent().map(Utf8PathBuf::from).unwrap_or_default();
let file_name = path.file_name().unwrap_or("");
let (stem, ext) = match (path.file_stem(), path.extension()) {
(Some(stem), Some(ext)) if !file_name.starts_with('.') => (stem, Some(ext)),
_ => (file_name, None),
};
let new_name = match ext {
Some(ext) => format!("{stem}_{ts}.{ext}"),
None => format!("{stem}_{ts}"),
};
parent.join(new_name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mirror_unix_absolute() {
let r = mirror_into_backup(
Utf8Path::new("/dotfiles/.yui/backup"),
Utf8Path::new("/home/u/.config/foo.toml"),
);
assert_eq!(
r,
Utf8PathBuf::from("/dotfiles/.yui/backup/home/u/.config/foo.toml")
);
}
#[test]
fn append_with_extension() {
let r = append_timestamp(Utf8Path::new("a/b.yml"), "20260429_143022123");
assert_eq!(r, Utf8PathBuf::from("a/b_20260429_143022123.yml"));
}
#[test]
fn append_no_extension() {
let r = append_timestamp(Utf8Path::new("a/b"), "20260429_143022123");
assert_eq!(r, Utf8PathBuf::from("a/b_20260429_143022123"));
}
#[test]
fn append_dotfile() {
let r = append_timestamp(Utf8Path::new(".gitconfig"), "20260429_143022123");
assert_eq!(r, Utf8PathBuf::from(".gitconfig_20260429_143022123"));
}
#[test]
fn tilde_slash_expands() {
let home = Utf8Path::new("/h/u");
assert_eq!(
expand_tilde_with("~/foo", home),
Utf8PathBuf::from("/h/u/foo")
);
assert_eq!(
expand_tilde_with("~/.config/nvim", home),
Utf8PathBuf::from("/h/u/.config/nvim")
);
}
#[test]
fn tilde_backslash_expands_for_windows_input() {
let home = Utf8Path::new("C:/Users/u");
assert_eq!(
expand_tilde_with("~\\foo", home),
Utf8PathBuf::from("C:/Users/u/foo")
);
}
#[test]
fn lone_tilde_is_home() {
let home = Utf8Path::new("/h/u");
assert_eq!(expand_tilde_with("~", home), Utf8PathBuf::from("/h/u"));
}
#[test]
fn tilde_user_form_is_untouched() {
let home = Utf8Path::new("/h/u");
assert_eq!(
expand_tilde_with("~root/foo", home),
Utf8PathBuf::from("~root/foo")
);
}
#[test]
fn no_tilde_unchanged() {
let home = Utf8Path::new("/h/u");
assert_eq!(
expand_tilde_with("/abs/path", home),
Utf8PathBuf::from("/abs/path")
);
assert_eq!(
expand_tilde_with("rel/path", home),
Utf8PathBuf::from("rel/path")
);
assert_eq!(
expand_tilde_with("/foo/~/bar", home),
Utf8PathBuf::from("/foo/~/bar")
);
}
}