1use crate::{displayed_item_tree::VisibleItemIndex, wave_data::WaveData};
3use camino::Utf8PathBuf;
4use egui::RichText;
5#[cfg(not(target_arch = "wasm32"))]
6use std::path::Path;
7
8#[must_use]
13pub(crate) fn uint_idx_to_alpha_idx(idx: VisibleItemIndex, nvariables: usize) -> String {
14 let width = usize::try_from(nvariables.ilog(16)).unwrap() + 1;
18 format!("{:0width$x}", idx.0)
19 .chars()
20 .map(|c| match c {
21 '0' => 'a',
22 '1' => 'b',
23 '2' => 'c',
24 '3' => 'd',
25 '4' => 'e',
26 '5' => 'f',
27 '6' => 'g',
28 '7' => 'h',
29 '8' => 'i',
30 '9' => 'j',
31 'a' => 'k',
32 'b' => 'l',
33 'c' => 'm',
34 'd' => 'n',
35 'e' => 'o',
36 'f' => 'p',
37 _ => '?',
38 })
39 .collect()
40}
41
42pub(crate) fn alpha_idx_to_uint_idx(idx: &str) -> Option<VisibleItemIndex> {
44 let mapped = idx
45 .chars()
46 .map(|c| match c {
47 'a' => '0',
48 'b' => '1',
49 'c' => '2',
50 'd' => '3',
51 'e' => '4',
52 'f' => '5',
53 'g' => '6',
54 'h' => '7',
55 'i' => '8',
56 'j' => '9',
57 'k' => 'a',
58 'l' => 'b',
59 'm' => 'c',
60 'n' => 'd',
61 'o' => 'e',
62 'p' => 'f',
63 _ => '?',
64 })
65 .collect::<String>();
66 usize::from_str_radix(&mapped, 16)
67 .ok()
68 .map(VisibleItemIndex)
69}
70
71#[must_use]
72pub(crate) fn get_alpha_focus_id(vidx: VisibleItemIndex, waves: &WaveData) -> RichText {
73 let alpha_id = uint_idx_to_alpha_idx(vidx, waves.displayed_items.len());
74
75 RichText::new(alpha_id).monospace()
76}
77
78#[cfg(not(target_arch = "wasm32"))]
84pub(crate) fn search_upward(
85 start: impl AsRef<Path>,
86 end: impl AsRef<Path>,
87 item: impl AsRef<Path>,
88) -> Vec<Utf8PathBuf> {
89 start
90 .as_ref()
91 .ancestors()
92 .take_while(|p| p.starts_with(end.as_ref()))
93 .map(|p| p.join(&item))
94 .filter(|p| p.try_exists().is_ok_and(std::convert::identity))
95 .filter_map(|p| Utf8PathBuf::from_path_buf(p).ok())
96 .collect()
97}
98
99fn get_multi_extension_from_filename(filename: &str) -> Option<String> {
100 filename
101 .find('.')
102 .map(|pos| filename[pos + 1..].to_string())
103}
104
105#[must_use]
109pub(crate) fn get_multi_extension(path: &Utf8PathBuf) -> Option<String> {
110 if let Some(filename) = path.file_name() {
112 return get_multi_extension_from_filename(filename);
113 }
114 None
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn test_uint_idx_to_alpha_idx_basic_width_1() {
123 assert_eq!(uint_idx_to_alpha_idx(VisibleItemIndex(0), 1), "a");
126 assert_eq!(uint_idx_to_alpha_idx(VisibleItemIndex(9), 1), "j");
127 assert_eq!(uint_idx_to_alpha_idx(VisibleItemIndex(15), 1), "p");
128 }
129
130 #[test]
131 fn test_uint_idx_to_alpha_idx_zero_padded_width_2() {
132 assert_eq!(uint_idx_to_alpha_idx(VisibleItemIndex(0x0), 16), "aa");
134 assert_eq!(uint_idx_to_alpha_idx(VisibleItemIndex(0x1), 16), "ab");
135 assert_eq!(uint_idx_to_alpha_idx(VisibleItemIndex(0xf), 16), "ap");
136 assert_eq!(uint_idx_to_alpha_idx(VisibleItemIndex(0x10), 16), "ba");
137 assert_eq!(uint_idx_to_alpha_idx(VisibleItemIndex(0x1f), 16), "bp");
138 }
139
140 #[test]
141 fn test_alpha_idx_to_uint_idx_roundtrip() {
142 let cases = [
144 (VisibleItemIndex(0x0), 1),
145 (VisibleItemIndex(0x9), 1),
146 (VisibleItemIndex(0xf), 1),
147 (VisibleItemIndex(0x10), 16),
148 (VisibleItemIndex(0x2a), 256),
149 (VisibleItemIndex(0xabc), 4096),
150 ];
151
152 for (vidx, nvars) in cases {
153 let s = uint_idx_to_alpha_idx(vidx, nvars);
154 let back = alpha_idx_to_uint_idx(&s).expect("should parse back");
155 assert_eq!(back, vidx);
156 }
157 }
158
159 #[test]
160 fn test_alpha_idx_to_uint_idx_invalid_input() {
161 assert!(alpha_idx_to_uint_idx("ar").is_none());
163 assert!(alpha_idx_to_uint_idx("").is_none());
165 assert!(alpha_idx_to_uint_idx("A").is_none());
167 assert!(alpha_idx_to_uint_idx("-").is_none());
168 }
169
170 #[test]
171 fn test_get_multi_extension_from_filename() {
172 assert_eq!(
173 get_multi_extension_from_filename("foo.tar.gz"),
174 Some("tar.gz".to_string())
175 );
176 assert_eq!(
177 get_multi_extension_from_filename("foo.txt"),
178 Some("txt".to_string())
179 );
180 assert_eq!(get_multi_extension_from_filename("foo"), None);
181 assert_eq!(
183 get_multi_extension_from_filename(".bashrc"),
184 Some("bashrc".to_string())
185 );
186 assert_eq!(
188 get_multi_extension_from_filename("foo."),
189 Some(String::new())
190 );
191 }
192
193 #[test]
194 fn test_get_multi_extension_from_path() {
195 let p = Utf8PathBuf::from("/tmp/foo/bar.tar.gz");
196 assert_eq!(get_multi_extension(&p), Some("tar.gz".to_string()));
197 let p = Utf8PathBuf::from("/tmp/foo/bar");
198 assert_eq!(get_multi_extension(&p), None);
199 }
200
201 #[test]
202 fn test_get_multi_extension_with_unicode() {
203 let name = "åäö.archive.tar.gz"; assert_eq!(
207 get_multi_extension_from_filename(name),
208 Some("archive.tar.gz".to_string())
209 );
210
211 let name2 = "ß.";
213 assert_eq!(
214 get_multi_extension_from_filename(name2),
215 Some(String::new())
216 );
217 }
218
219 #[cfg(not(target_arch = "wasm32"))]
220 #[test]
221 fn test_search_upward_finds_closest_first() {
222 use std::fs;
223 use std::io::Write;
224 use std::path::Path;
225
226 let tmp = tempfile::tempdir().expect("tempdir");
228 let root = tmp.path();
229 let a = root.join("a");
230 let b = a.join("b");
231 let c = b.join("c");
232 fs::create_dir_all(&c).expect("dirs");
233
234 let item_name = Path::new("target.txt");
236 let item_c = c.join(item_name);
237 let item_a = a.join(item_name);
238 {
239 let mut f = fs::File::create(&item_c).expect("create c");
240 writeln!(f, "hello").unwrap();
241 }
242 {
243 let mut f = fs::File::create(&item_a).expect("create a");
244 writeln!(f, "world").unwrap();
245 }
246
247 let found = search_upward(&c, root, item_name);
249 let expected =
251 [item_c, item_a].map(|p| Utf8PathBuf::from_path_buf(p).expect("valid UTF-8 temp path"));
252 assert_eq!(found, expected);
253 }
254}