1use camino::{Utf8Component, Utf8Path, Utf8PathBuf};
5
6pub fn expand_tilde(s: &str) -> Utf8PathBuf {
17 match home_dir() {
18 Some(home) => expand_tilde_with(s, &home),
19 None => Utf8PathBuf::from(s),
20 }
21}
22
23pub fn expand_tilde_with(s: &str, home: &Utf8Path) -> Utf8PathBuf {
26 if let Some(rest) = s.strip_prefix("~/").or_else(|| s.strip_prefix("~\\")) {
27 home.join(rest)
28 } else if s == "~" {
29 home.to_path_buf()
30 } else {
31 Utf8PathBuf::from(s)
32 }
33}
34
35pub fn home_dir() -> Option<Utf8PathBuf> {
37 std::env::var("HOME")
38 .ok()
39 .or_else(|| std::env::var("USERPROFILE").ok())
40 .map(Utf8PathBuf::from)
41}
42
43pub fn load_yuiignore(source: &Utf8Path) -> crate::Result<ignore::gitignore::Gitignore> {
55 let path = source.join(".yuiignore");
56 if !path.is_file() {
57 return Ok(ignore::gitignore::Gitignore::empty());
58 }
59 let mut builder = ignore::gitignore::GitignoreBuilder::new(source);
60 if let Some(e) = builder.add(path.as_std_path()) {
61 return Err(crate::Error::Config(format!("parsing {path}: {e}")));
62 }
63 builder
64 .build()
65 .map_err(|e| crate::Error::Config(format!("building .yuiignore: {e}")))
66}
67
68pub fn is_ignored(
81 gi: &ignore::gitignore::Gitignore,
82 source: &Utf8Path,
83 path: &Utf8Path,
84 is_dir: bool,
85) -> bool {
86 let Ok(rel) = path.strip_prefix(source) else {
87 return false;
88 };
89 matches!(
90 gi.matched_path_or_any_parents(rel.as_std_path(), is_dir),
91 ignore::Match::Ignore(_)
92 )
93}
94
95pub fn source_walker(source: &Utf8Path) -> ignore::WalkBuilder {
104 let mut b = ignore::WalkBuilder::new(source);
105 b.hidden(false).git_ignore(false).ignore(false);
106 b.filter_entry(|entry| entry.file_name() != ".yui");
107 b
108}
109
110pub fn mirror_into_backup(backup_root: &Utf8Path, abs_target: &Utf8Path) -> Utf8PathBuf {
118 let mut out = backup_root.to_path_buf();
119 for component in abs_target.components() {
120 match component {
121 Utf8Component::Prefix(p) => {
122 let s = p.as_str().trim_end_matches(':');
123 if !s.is_empty() {
124 out.push(s);
125 }
126 }
127 Utf8Component::RootDir | Utf8Component::CurDir => {}
128 Utf8Component::ParentDir => {}
129 Utf8Component::Normal(s) => {
130 out.push(s);
131 }
132 }
133 }
134 out
135}
136
137pub fn append_timestamp(path: &Utf8Path, ts: &str) -> Utf8PathBuf {
145 let parent = path.parent().map(Utf8PathBuf::from).unwrap_or_default();
146 let file_name = path.file_name().unwrap_or("");
147
148 let (stem, ext) = match (path.file_stem(), path.extension()) {
149 (Some(stem), Some(ext)) if !file_name.starts_with('.') => (stem, Some(ext)),
150 _ => (file_name, None),
151 };
152
153 let new_name = match ext {
154 Some(ext) => format!("{stem}_{ts}.{ext}"),
155 None => format!("{stem}_{ts}"),
156 };
157 parent.join(new_name)
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn mirror_unix_absolute() {
166 let r = mirror_into_backup(
167 Utf8Path::new("/dotfiles/.yui/backup"),
168 Utf8Path::new("/home/u/.config/foo.toml"),
169 );
170 assert_eq!(
171 r,
172 Utf8PathBuf::from("/dotfiles/.yui/backup/home/u/.config/foo.toml")
173 );
174 }
175
176 #[test]
177 fn append_with_extension() {
178 let r = append_timestamp(Utf8Path::new("a/b.yml"), "20260429_143022123");
179 assert_eq!(r, Utf8PathBuf::from("a/b_20260429_143022123.yml"));
180 }
181
182 #[test]
183 fn append_no_extension() {
184 let r = append_timestamp(Utf8Path::new("a/b"), "20260429_143022123");
185 assert_eq!(r, Utf8PathBuf::from("a/b_20260429_143022123"));
186 }
187
188 #[test]
189 fn append_dotfile() {
190 let r = append_timestamp(Utf8Path::new(".gitconfig"), "20260429_143022123");
191 assert_eq!(r, Utf8PathBuf::from(".gitconfig_20260429_143022123"));
192 }
193
194 #[test]
195 fn tilde_slash_expands() {
196 let home = Utf8Path::new("/h/u");
197 assert_eq!(
198 expand_tilde_with("~/foo", home),
199 Utf8PathBuf::from("/h/u/foo")
200 );
201 assert_eq!(
202 expand_tilde_with("~/.config/nvim", home),
203 Utf8PathBuf::from("/h/u/.config/nvim")
204 );
205 }
206
207 #[test]
208 fn tilde_backslash_expands_for_windows_input() {
209 let home = Utf8Path::new("C:/Users/u");
211 assert_eq!(
212 expand_tilde_with("~\\foo", home),
213 Utf8PathBuf::from("C:/Users/u/foo")
214 );
215 }
216
217 #[test]
218 fn lone_tilde_is_home() {
219 let home = Utf8Path::new("/h/u");
220 assert_eq!(expand_tilde_with("~", home), Utf8PathBuf::from("/h/u"));
221 }
222
223 #[test]
224 fn tilde_user_form_is_untouched() {
225 let home = Utf8Path::new("/h/u");
226 assert_eq!(
229 expand_tilde_with("~root/foo", home),
230 Utf8PathBuf::from("~root/foo")
231 );
232 }
233
234 #[test]
235 fn no_tilde_unchanged() {
236 let home = Utf8Path::new("/h/u");
237 assert_eq!(
238 expand_tilde_with("/abs/path", home),
239 Utf8PathBuf::from("/abs/path")
240 );
241 assert_eq!(
242 expand_tilde_with("rel/path", home),
243 Utf8PathBuf::from("rel/path")
244 );
245 assert_eq!(
247 expand_tilde_with("/foo/~/bar", home),
248 Utf8PathBuf::from("/foo/~/bar")
249 );
250 }
251}