1use std::path::{Path, PathBuf};
4
5#[derive(Debug)]
6pub struct Entry {
7 pub path: PathBuf,
8 pub depth: usize,
9 pub is_dir: bool,
10 pub open: bool,
11}
12
13impl Entry {
14 pub fn name(&self) -> std::borrow::Cow<'_, str> {
15 self.path.file_name().unwrap_or_default().to_string_lossy()
16 }
17}
18
19pub struct Tree {
20 pub root: PathBuf,
21 pub entries: Vec<Entry>,
22 pub hidden: bool,
23}
24
25fn is_dir(e: &std::fs::DirEntry) -> bool {
30 match e.file_type() {
31 Ok(t) if t.is_dir() => true,
32 Ok(t) if t.is_symlink() => e.path().is_dir(),
33 _ => false,
34 }
35}
36
37pub fn children(dir: &Path, hidden: bool, depth: usize) -> Vec<Entry> {
39 let mut v: Vec<Entry> = std::fs::read_dir(dir)
40 .into_iter()
41 .flatten()
42 .flatten()
43 .filter(|e| hidden || !e.file_name().to_string_lossy().starts_with('.'))
44 .map(|e| Entry {
45 is_dir: is_dir(&e),
46 path: e.path(),
47 depth,
48 open: false,
49 })
50 .collect();
51 v.sort_by_key(|e| (!e.is_dir, e.name().to_lowercase()));
52 v
53}
54
55pub fn descendants(entries: &[Entry], i: usize) -> usize {
57 let d = entries[i].depth;
58 entries[i + 1..].iter().take_while(|e| e.depth > d).count()
59}
60
61impl Tree {
62 pub fn new(root: PathBuf, hidden: bool) -> Self {
63 Tree {
64 entries: children(&root, hidden, 0),
65 root,
66 hidden,
67 }
68 }
69
70 pub fn len(&self) -> usize {
71 self.entries.len()
72 }
73
74 pub fn is_empty(&self) -> bool {
75 self.entries.is_empty()
76 }
77
78 pub fn get(&self, i: usize) -> Option<&Entry> {
79 self.entries.get(i)
80 }
81
82 pub fn reload(&mut self) {
83 self.entries = children(&self.root, self.hidden, 0);
84 }
85
86 pub fn toggle(&mut self, i: usize) {
88 let Some(e) = self.entries.get(i) else { return };
89 if !e.is_dir {
90 return;
91 }
92 if e.open {
93 let n = descendants(&self.entries, i);
94 self.entries.drain(i + 1..=i + n);
95 self.entries[i].open = false;
96 } else {
97 let kids = children(&e.path, self.hidden, e.depth + 1);
98 self.entries[i].open = true;
99 self.entries.splice(i + 1..i + 1, kids);
100 }
101 }
102
103 pub fn parent_of(&self, i: usize) -> Option<usize> {
105 let d = self.entries.get(i)?.depth;
106 if d == 0 {
107 return None;
108 }
109 self.entries[..i].iter().rposition(|e| e.depth < d)
110 }
111
112 pub fn up(&mut self) -> bool {
114 let Some(parent) = self.root.parent().map(Path::to_path_buf) else {
115 return false;
116 };
117 self.root = parent;
118 self.reload();
119 true
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 fn e(depth: usize) -> Entry {
128 Entry {
129 path: PathBuf::from("x"),
130 depth,
131 is_dir: true,
132 open: false,
133 }
134 }
135
136 #[test]
137 fn descendants_counts_only_the_nested_run() {
138 let t = vec![e(0), e(1), e(2), e(1), e(0)];
139 assert_eq!(descendants(&t, 0), 3);
140 assert_eq!(descendants(&t, 1), 1);
141 assert_eq!(descendants(&t, 2), 0);
142 assert_eq!(descendants(&t, 4), 0);
143 }
144
145 #[test]
146 fn expand_then_collapse_restores_the_entry_list() {
147 let mut t = Tree::new(PathBuf::from(env!("CARGO_MANIFEST_DIR")), false);
149 let before = t.len();
150 let i = t
151 .entries
152 .iter()
153 .position(|e| e.is_dir)
154 .expect("src/ exists");
155 t.toggle(i);
156 assert!(t.len() > before && t.entries[i].open);
157 t.toggle(i);
158 assert_eq!(t.len(), before);
159 assert!(!t.entries[i].open);
160 }
161
162 #[test]
163 fn a_link_to_a_directory_counts_as_a_directory() {
164 let base = std::env::temp_dir().join(format!("cdt-tree-{}", std::process::id()));
165 let real = base.join("real");
166 std::fs::create_dir_all(&real).expect("temp dir");
167 let link = base.join("link");
168
169 #[cfg(windows)]
170 let made = std::os::windows::fs::symlink_dir(&real, &link).is_ok();
171 #[cfg(unix)]
172 let made = std::os::unix::fs::symlink(&real, &link).is_ok();
173
174 if made {
177 let kids = children(&base, false, 0);
178 let l = kids
179 .iter()
180 .find(|e| e.name() == "link")
181 .expect("the link is listed");
182 assert!(l.is_dir, "a link to a directory must expand like one");
183 }
184 let _ = std::fs::remove_dir_all(&base);
185 }
186
187 #[test]
188 fn parent_of_walks_out_one_level() {
189 let t = Tree {
190 root: PathBuf::new(),
191 hidden: false,
192 entries: vec![e(0), e(1), e(2)],
193 };
194 assert_eq!(t.parent_of(0), None);
195 assert_eq!(t.parent_of(2), Some(1));
196 }
197}