1use std::path::Path;
7
8#[cfg(not(target_arch = "wasm32"))]
9pub use interactive::*;
10
11#[cfg(not(target_arch = "wasm32"))]
12pub mod git_branch;
13#[cfg(not(target_arch = "wasm32"))]
14mod interactive;
15
16pub fn display_fixed_width(value: &str, max_chars: usize) -> String {
17 let normalized = value.split_whitespace().collect::<Vec<_>>().join(" ");
18 let chars: Vec<char> = normalized.chars().collect();
19
20 if chars.len() <= max_chars {
21 return normalized;
22 }
23
24 if max_chars == 0 {
25 return String::new();
26 }
27
28 if max_chars == 1 {
29 return "…".to_owned();
30 }
31
32 let mut trimmed: String = chars.into_iter().take(max_chars.saturating_sub(1)).collect();
33 trimmed.push('…');
34 trimmed
35}
36
37pub fn short_path(path: &Path, home: &Path) -> String {
38 if home != Path::new("/") {
39 if path == home {
40 return "~".into();
41 }
42 if let Ok(rel) = path.strip_prefix(home) {
43 let names = path_dir_names(rel);
44 return if names.is_empty() {
45 "~".into()
46 } else {
47 format!("~/{}", abbrev_path_dirs(&names))
48 };
49 }
50 }
51
52 let names = path_dir_names(path);
53 if names.is_empty() {
54 "/".into()
55 } else {
56 format!("/{}", abbrev_path_dirs(&names))
57 }
58}
59
60fn path_dir_names(path: &Path) -> Vec<String> {
61 path.components()
62 .filter_map(|component| match component {
63 std::path::Component::Normal(segment) => Some(segment.to_string_lossy().into_owned()),
64 std::path::Component::Prefix(_)
65 | std::path::Component::RootDir
66 | std::path::Component::CurDir
67 | std::path::Component::ParentDir => None,
68 })
69 .collect()
70}
71
72fn abbrev_path_dirs(names: &[String]) -> String {
73 match names.len() {
74 0 => String::new(),
75 1 => names.first().cloned().unwrap_or_default(),
76 total => {
77 let mut out = String::new();
78 for (idx, name) in names.iter().enumerate() {
79 if idx > 0 {
80 out.push('/');
81 }
82 let is_last = idx == total.saturating_sub(1);
83 if is_last {
84 out.push_str(name);
85 } else {
86 out.push(name.chars().next().unwrap_or('·'));
87 }
88 }
89 out
90 }
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use std::path::Path;
97
98 use super::*;
99
100 #[rstest::rstest]
101 #[case("hello world", 20, "hello world")]
102 #[case("abcdefghijklmnopqrstuvwxyz", 5, "abcd…")]
103 #[case("abc", 1, "…")]
104 #[case("abc", 0, "")]
105 fn display_fixed_width_trims_as_expected(#[case] value: &str, #[case] max_chars: usize, #[case] expected: &str) {
106 pretty_assertions::assert_eq!(display_fixed_width(value, max_chars), expected);
107 }
108
109 #[test]
110 fn test_short_path_under_home_abbreviates_parent_directories() {
111 let home = Path::new("/home/user");
112
113 pretty_assertions::assert_eq!(
114 short_path(Path::new("/home/user/src/pkg/myproject"), home),
115 "~/s/p/myproject"
116 );
117 }
118
119 #[test]
120 fn test_short_path_many_dirs_abbreviates_all_but_last() {
121 let home = Path::new("/home/user");
122
123 pretty_assertions::assert_eq!(
124 short_path(Path::new("/home/user/one/two/three/four/five"), home),
125 "~/o/t/t/f/five"
126 );
127 }
128
129 #[test]
130 fn test_short_path_outside_home_renders_absolute_abbrev() {
131 let home = Path::new("/home/user");
132
133 pretty_assertions::assert_eq!(short_path(Path::new("/opt/pkg/foo"), home), "/o/p/foo");
134 }
135}