Skip to main content

cdt_tree/
lib.rs

1//! Directory tree model: a flat `Vec<Entry>` where nesting is held in `depth`.
2//! Expanding splices children in, collapsing drains the nested run back out.
3use 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
25/// One directory's entries, directories first then files, case-insensitive.
26pub 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
43/// Number of entries right after `i` that are nested under it.
44pub 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    /// Expand or collapse the directory at `i`. No-op on files.
75    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    /// Index of the entry that `i` is nested under.
92    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    /// Re-root one directory up. False at the filesystem root.
101    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        // Own crate dir: contains src/ (a dir) and Cargo.toml.
136        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}