Skip to main content

f00_format/
tree.rs

1use f00_core::{Entry, IndicatorStyle};
2
3use crate::color::Colorizer;
4use crate::icons::icon_prefix;
5use crate::perms::classify_suffix;
6
7/// Render a tree from a flat recursive listing (entries with depth).
8///
9/// Also works for a single non-recursive directory by treating all entries as depth 0.
10///
11/// Rendering is **O(n)** in the number of entries (precomputed last-sibling flags).
12pub fn format_tree(
13    entries: &[Entry],
14    colorizer: &Colorizer,
15    icons: bool,
16    indicator: IndicatorStyle,
17) -> String {
18    let items: Vec<&Entry> = entries.iter().filter(|e| !e.is_dir_header).collect();
19    if items.is_empty() {
20        return String::new();
21    }
22
23    let use_depth = items.iter().any(|e| e.depth > 0);
24    let mut out = String::with_capacity(items.len().saturating_mul(48));
25
26    if !use_depth {
27        for (i, entry) in items.iter().enumerate() {
28            let last = i + 1 == items.len();
29            out.push_str(if last { "└── " } else { "├── " });
30            push_name(&mut out, entry, colorizer, icons, indicator);
31            out.push('\n');
32        }
33        return out;
34    }
35
36    format_tree_by_depth(&items, colorizer, icons, indicator, &mut out);
37    out
38}
39
40fn push_name(
41    out: &mut String,
42    entry: &Entry,
43    colorizer: &Colorizer,
44    icons: bool,
45    indicator: IndicatorStyle,
46) {
47    let icon = icon_prefix(entry, icons);
48    let suffix = classify_suffix(entry, indicator);
49    let plain = format!("{icon}{}{suffix}", entry.name);
50    out.push_str(&colorizer.paint_name(entry, &plain));
51}
52
53/// Preorder depths (1 = root children). Compute `is_last` and draw connectors in O(n).
54fn format_tree_by_depth(
55    items: &[&Entry],
56    colorizer: &Colorizer,
57    icons: bool,
58    indicator: IndicatorStyle,
59    out: &mut String,
60) {
61    let n = items.len();
62    // Walkdir depths are typically 1+ for listed nodes; treat 0 as 1 for safety.
63    let depths: Vec<usize> = items.iter().map(|e| e.depth.max(1)).collect();
64
65    // is_last[i] == true if items[i] is the last among its siblings (same parent in preorder).
66    let is_last = precompute_is_last(&depths);
67
68    // stack of indices of ancestors (by depth level: stack[0] is depth-1 node, …)
69    let mut stack: Vec<usize> = Vec::with_capacity(16);
70
71    for i in 0..n {
72        let d = depths[i];
73        // Pop finished subtrees: keep only ancestors strictly above `d`.
74        while !stack.is_empty() && depths[*stack.last().unwrap()] >= d {
75            stack.pop();
76        }
77
78        // Vertical bars for each ancestor that still has more siblings after this node.
79        for &anc in &stack {
80            if is_last[anc] {
81                out.push_str("    ");
82            } else {
83                out.push_str("│   ");
84            }
85        }
86
87        out.push_str(if is_last[i] {
88            "└── "
89        } else {
90            "├── "
91        });
92        push_name(out, items[i], colorizer, icons, indicator);
93        out.push('\n');
94
95        stack.push(i);
96    }
97}
98
99/// For a preorder sequence of depths, mark each index as last among siblings.
100///
101/// Sibling group: consecutive nodes that share the same parent in the preorder walk
102/// (same depth, with no intervening shallower node).
103fn precompute_is_last(depths: &[usize]) -> Vec<bool> {
104    let n = depths.len();
105    let mut is_last = vec![true; n];
106    if n == 0 {
107        return is_last;
108    }
109
110    // last_at[d] = most recent index at depth d that may still get another sibling
111    let max_d = depths.iter().copied().max().unwrap_or(1);
112    let mut last_at: Vec<Option<usize>> = vec![None; max_d + 2];
113
114    for (i, &d) in depths.iter().enumerate() {
115        // Closing any open nodes deeper than d: they are last in their groups already.
116        for slot in last_at.iter_mut().skip(d + 1) {
117            *slot = None;
118        }
119        // Previous node at this depth (same parent, preorder) is not last — we are its sibling.
120        if let Some(prev) = last_at[d] {
121            is_last[prev] = false;
122        }
123        last_at[d] = Some(i);
124    }
125    is_last
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use f00_core::{Entry, EntryKind, GitStatus};
132    use std::path::PathBuf;
133
134    fn e_path(path: &str, depth: usize) -> Entry {
135        let p = PathBuf::from(path);
136        let name = p.file_name().unwrap().to_string_lossy().into_owned();
137        Entry {
138            path: p,
139            name,
140            kind: EntryKind::File,
141            size: 0,
142            modified: None,
143            created: None,
144            accessed: None,
145            changed: None,
146            mode: 0o644,
147            readonly: false,
148            symlink_target: None,
149            depth,
150            git_status: GitStatus::Clean,
151            is_dir_header: false,
152            nlink: 1,
153            uid: 0,
154            gid: 0,
155            inode: 0,
156            blocks: 0,
157            owner: String::new(),
158            group: String::new(),
159            author: String::new(),
160            context: String::new(),
161        }
162    }
163
164    #[test]
165    fn precompute_siblings() {
166        // a(1), b(2), g(3), f(2) — classic tree preorder
167        let d = vec![1, 2, 3, 2];
168        let last = precompute_is_last(&d);
169        assert!(last[0]); // a only root child
170        assert!(!last[1]); // b then f
171        assert!(last[2]); // g only child of b
172        assert!(last[3]); // f last under a
173    }
174
175    #[test]
176    fn tree_connectors_correct() {
177        let entries = vec![
178            e_path("/r/a", 1),
179            e_path("/r/a/b", 2),
180            e_path("/r/a/b/g", 3),
181            e_path("/r/a/f", 2),
182        ];
183        // force directory kinds for a,b
184        let mut entries = entries;
185        entries[0].kind = EntryKind::Directory;
186        entries[1].kind = EntryKind::Directory;
187
188        let colorizer = Colorizer::new(false);
189        let out = format_tree(&entries, &colorizer, false, IndicatorStyle::None);
190        // Expect classic tree:
191        // └── a
192        //     ├── b
193        //     │   └── g
194        //     └── f
195        assert!(out.contains("└── a\n"), "{out}");
196        assert!(out.contains("├── b\n"), "{out}");
197        assert!(out.contains("│   └── g\n"), "{out}");
198        assert!(out.contains("└── f\n"), "{out}");
199    }
200}