1use f00_core::{Entry, IndicatorStyle};
2
3use crate::color::Colorizer;
4use crate::icons::icon_prefix;
5use crate::perms::classify_suffix;
6
7pub 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
53fn 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 let depths: Vec<usize> = items.iter().map(|e| e.depth.max(1)).collect();
64
65 let is_last = precompute_is_last(&depths);
67
68 let mut stack: Vec<usize> = Vec::with_capacity(16);
70
71 for i in 0..n {
72 let d = depths[i];
73 while !stack.is_empty() && depths[*stack.last().unwrap()] >= d {
75 stack.pop();
76 }
77
78 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
99fn 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 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 for slot in last_at.iter_mut().skip(d + 1) {
117 *slot = None;
118 }
119 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 let d = vec![1, 2, 3, 2];
168 let last = precompute_is_last(&d);
169 assert!(last[0]); assert!(!last[1]); assert!(last[2]); assert!(last[3]); }
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 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 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}