Skip to main content

ytil_ext/
path_ext.rs

1//! Extensions for [`Path`] and [`std::path::PathBuf`]
2
3use std::path::Path;
4
5pub trait PathExt {
6    fn short_path(&self, home: &Self) -> String;
7}
8
9impl PathExt for Path {
10    fn short_path(&self, home: &Self) -> String {
11        if home != Self::new("/") {
12            if self == home {
13                return "~".into();
14            }
15            if let Ok(rel) = self.strip_prefix(home) {
16                let names = path_dir_names(rel);
17                return if names.is_empty() {
18                    "~".into()
19                } else {
20                    format!("~/{}", abbrev_path_dirs(&names))
21                };
22            }
23        }
24
25        let names = path_dir_names(self);
26        if names.is_empty() {
27            "/".into()
28        } else {
29            format!("/{}", abbrev_path_dirs(&names))
30        }
31    }
32}
33
34fn path_dir_names(path: &Path) -> Vec<String> {
35    path.components()
36        .filter_map(|component| match component {
37            std::path::Component::Normal(segment) => Some(segment.to_string_lossy().into_owned()),
38            std::path::Component::Prefix(_)
39            | std::path::Component::RootDir
40            | std::path::Component::CurDir
41            | std::path::Component::ParentDir => None,
42        })
43        .collect()
44}
45
46fn abbrev_path_dirs(names: &[String]) -> String {
47    match names.len() {
48        0 => String::new(),
49        1 => names.first().cloned().unwrap_or_default(),
50        total => {
51            let mut out = String::new();
52            for (idx, name) in names.iter().enumerate() {
53                if idx > 0 {
54                    out.push('/');
55                }
56                let is_last = idx == total.saturating_sub(1);
57                if is_last {
58                    out.push_str(name);
59                } else {
60                    out.push(name.chars().next().unwrap_or('ยท'));
61                }
62            }
63            out
64        }
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use test_that::prelude::*;
71
72    use super::*;
73
74    #[test]
75    fn test_short_path_under_home_abbreviates_parent_directories() {
76        let home = Path::new("/home/user");
77
78        assert_that!(
79            Path::new("/home/user/src/pkg/myproject").short_path(home),
80            eq("~/s/p/myproject")
81        );
82    }
83
84    #[test]
85    fn test_short_path_many_dirs_abbreviates_all_but_last() {
86        let home = Path::new("/home/user");
87
88        assert_that!(
89            Path::new("/home/user/one/two/three/four/five").short_path(home),
90            eq("~/o/t/t/f/five")
91        );
92    }
93
94    #[test]
95    fn test_short_path_outside_home_renders_absolute_abbrev() {
96        let home = Path::new("/home/user");
97
98        assert_that!(Path::new("/opt/pkg/foo").short_path(home), eq("/o/p/foo"));
99    }
100}