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 basic tree from a flat recursive listing (headers + entries with depth).
8///
9/// Also works for a single non-recursive directory by treating all entries as depth 0.
10pub fn format_tree(
11    entries: &[Entry],
12    colorizer: &Colorizer,
13    icons: bool,
14    indicator: IndicatorStyle,
15) -> String {
16    // Collect non-header entries; use depth field when available.
17    let items: Vec<&Entry> = entries.iter().filter(|e| !e.is_dir_header).collect();
18    if items.is_empty() {
19        return String::new();
20    }
21
22    // If all depths are 0, build tree structure from paths when possible.
23    let use_depth = items.iter().any(|e| e.depth > 0);
24
25    let mut out = String::new();
26    if use_depth {
27        format_tree_by_depth(&items, colorizer, icons, indicator, &mut out);
28    } else {
29        // Flat tree of a single directory
30        for (i, entry) in items.iter().enumerate() {
31            let last = i + 1 == items.len();
32            let branch = if last { "└── " } else { "├── " };
33            let icon = icon_prefix(entry, icons);
34            let suffix = classify_suffix(entry, indicator);
35            let plain = format!("{icon}{}{suffix}", entry.name);
36            let name = colorizer.paint_name(entry, &plain);
37            out.push_str(branch);
38            out.push_str(&name);
39            out.push('\n');
40        }
41    }
42    out
43}
44
45fn format_tree_by_depth(
46    items: &[&Entry],
47    colorizer: &Colorizer,
48    icons: bool,
49    indicator: IndicatorStyle,
50    out: &mut String,
51) {
52    // Track which depths still have more siblings (for drawing │).
53    // Precompute for each index whether it is the last among siblings at its depth
54    // with the same parent path.
55    for (i, entry) in items.iter().enumerate() {
56        let depth = entry.depth.max(1); // root children are depth 1 in walkdir
57        let level = depth - 1;
58
59        // Determine if last among following items that share ancestor chain.
60        let is_last = is_last_sibling(items, i);
61
62        // Draw prefix for parent levels
63        for d in 0..level {
64            // Check if any ancestor at depth d+1 still has more siblings after us
65            let ancestor_has_more = ancestor_continues(items, i, d + 1);
66            if ancestor_has_more {
67                out.push_str("│   ");
68            } else {
69                out.push_str("    ");
70            }
71        }
72
73        out.push_str(if is_last { "└── " } else { "├── " });
74
75        let icon = icon_prefix(entry, icons);
76        let suffix = classify_suffix(entry, indicator);
77        let plain = format!("{icon}{}{suffix}", entry.name);
78        let name = colorizer.paint_name(entry, &plain);
79        out.push_str(&name);
80        out.push('\n');
81    }
82}
83
84fn is_last_sibling(items: &[&Entry], index: usize) -> bool {
85    let depth = items[index].depth;
86    let parent = items[index].path.parent().map(|p| p.to_path_buf());
87    for next in items.iter().skip(index + 1) {
88        if next.depth < depth {
89            return true;
90        }
91        if next.depth == depth {
92            let next_parent = next.path.parent().map(|p| p.to_path_buf());
93            if next_parent == parent {
94                return false;
95            }
96            // different parent at same depth means we've left the sibling group
97            if next.depth == depth {
98                return true;
99            }
100        }
101    }
102    true
103}
104
105fn ancestor_continues(items: &[&Entry], index: usize, ancestor_depth: usize) -> bool {
106    // Find the ancestor entry at `ancestor_depth` for items[index], then see if
107    // that ancestor has more siblings after this subtree.
108    let path = &items[index].path;
109    let ancestors: Vec<_> = path.ancestors().collect();
110    // path.ancestors: self, parent, grandparent...
111    // We need the path component at depth ancestor_depth from the walk root.
112    // Simpler heuristic: look ahead for another entry at `ancestor_depth`
113    // before any entry with depth < ancestor_depth.
114    for next in items.iter().skip(index + 1) {
115        if next.depth < ancestor_depth {
116            return false;
117        }
118        if next.depth == ancestor_depth {
119            return true;
120        }
121    }
122    let _ = ancestors;
123    false
124}