Skip to main content

ytil_tui/
lib.rs

1//! Provide minimal TUI selection & prompt helpers built on [`skim`].
2//!
3//! Offer uniform, cancellable single / multi select prompts with fuzzy filtering and helpers
4//! to derive a value from CLI args or fallback to an interactive selector.
5
6use 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}