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
25pub fn children(dir: &Path, hidden: bool, depth: usize) -> Vec<Entry> {
27 let mut v: Vec<Entry> = std::fs::read_dir(dir)
28 .into_iter()
29 .flatten()
30 .flatten()
31 .filter(|e| hidden || !e.file_name().to_string_lossy().starts_with('.'))
32 .map(|e| Entry {
33 is_dir: e.file_type().map(|t| t.is_dir()).unwrap_or(false),
34 path: e.path(),
35 depth,
36 open: false,
37 })
38 .collect();
39 v.sort_by_key(|e| (!e.is_dir, e.name().to_lowercase()));
40 v
41}
42
43pub fn descendants(entries: &[Entry], i: usize) -> usize {
45 let d = entries[i].depth;
46 entries[i + 1..].iter().take_while(|e| e.depth > d).count()
47}
48
49impl Tree {
50 pub fn new(root: PathBuf, hidden: bool) -> Self {
51 Tree {
52 entries: children(&root, hidden, 0),
53 root,
54 hidden,
55 }
56 }
57
58 pub fn len(&self) -> usize {
59 self.entries.len()
60 }
61
62 pub fn is_empty(&self) -> bool {
63 self.entries.is_empty()
64 }
65
66 pub fn get(&self, i: usize) -> Option<&Entry> {
67 self.entries.get(i)
68 }
69
70 pub fn reload(&mut self) {
71 self.entries = children(&self.root, self.hidden, 0);
72 }
73
74 pub fn toggle(&mut self, i: usize) {
76 let Some(e) = self.entries.get(i) else { return };
77 if !e.is_dir {
78 return;
79 }
80 if e.open {
81 let n = descendants(&self.entries, i);
82 self.entries.drain(i + 1..=i + n);
83 self.entries[i].open = false;
84 } else {
85 let kids = children(&e.path, self.hidden, e.depth + 1);
86 self.entries[i].open = true;
87 self.entries.splice(i + 1..i + 1, kids);
88 }
89 }
90
91 pub fn parent_of(&self, i: usize) -> Option<usize> {
93 let d = self.entries.get(i)?.depth;
94 if d == 0 {
95 return None;
96 }
97 self.entries[..i].iter().rposition(|e| e.depth < d)
98 }
99
100 pub fn up(&mut self) -> bool {
102 let Some(parent) = self.root.parent().map(Path::to_path_buf) else {
103 return false;
104 };
105 self.root = parent;
106 self.reload();
107 true
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 fn e(depth: usize) -> Entry {
116 Entry {
117 path: PathBuf::from("x"),
118 depth,
119 is_dir: true,
120 open: false,
121 }
122 }
123
124 #[test]
125 fn descendants_counts_only_the_nested_run() {
126 let t = vec![e(0), e(1), e(2), e(1), e(0)];
127 assert_eq!(descendants(&t, 0), 3);
128 assert_eq!(descendants(&t, 1), 1);
129 assert_eq!(descendants(&t, 2), 0);
130 assert_eq!(descendants(&t, 4), 0);
131 }
132
133 #[test]
134 fn expand_then_collapse_restores_the_entry_list() {
135 let mut t = Tree::new(PathBuf::from(env!("CARGO_MANIFEST_DIR")), false);
137 let before = t.len();
138 let i = t
139 .entries
140 .iter()
141 .position(|e| e.is_dir)
142 .expect("src/ exists");
143 t.toggle(i);
144 assert!(t.len() > before && t.entries[i].open);
145 t.toggle(i);
146 assert_eq!(t.len(), before);
147 assert!(!t.entries[i].open);
148 }
149
150 #[test]
151 fn parent_of_walks_out_one_level() {
152 let t = Tree {
153 root: PathBuf::new(),
154 hidden: false,
155 entries: vec![e(0), e(1), e(2)],
156 };
157 assert_eq!(t.parent_of(0), None);
158 assert_eq!(t.parent_of(2), Some(1));
159 }
160}