1use std::collections::BTreeMap;
7
8use crate::walker::{format_size, FileEntry};
9
10#[derive(Debug)]
12enum TreeNode {
13 File {
14 size: u64,
15 },
16 Directory {
17 children: BTreeMap<String, TreeNode>,
18 },
19}
20
21impl TreeNode {
22 fn new_directory() -> Self {
23 TreeNode::Directory {
24 children: BTreeMap::new(),
25 }
26 }
27
28 fn new_file(size: u64) -> Self {
29 TreeNode::File { size }
30 }
31
32 fn get_or_create_dir(&mut self, name: &str) -> &mut TreeNode {
33 if let TreeNode::Directory { children } = self {
34 children
35 .entry(name.to_string())
36 .or_insert_with(TreeNode::new_directory)
37 } else {
38 panic!("Expected directory node");
39 }
40 }
41
42 fn insert_file(&mut self, name: &str, size: u64) {
43 if let TreeNode::Directory { children } = self {
44 children.insert(name.to_string(), TreeNode::new_file(size));
45 }
46 }
47}
48
49fn build_tree(entries: &[FileEntry]) -> TreeNode {
51 let mut root = TreeNode::new_directory();
52
53 for entry in entries {
54 let components: Vec<_> = entry.relative_path.components().collect();
55 let mut current = &mut root;
56
57 for (i, component) in components.iter().enumerate() {
58 let name = component.as_os_str().to_string_lossy().to_string();
59 let is_last = i == components.len() - 1;
60
61 if is_last {
62 current.insert_file(&name, entry.size);
63 } else {
64 current = current.get_or_create_dir(&name);
65 }
66 }
67 }
68
69 root
70}
71
72fn render_tree(node: &TreeNode, prefix: &str, is_root: bool, show_sizes: bool) -> String {
74 let mut output = String::new();
75
76 if let TreeNode::Directory { children } = node {
77 let entries: Vec<_> = children.iter().collect();
78 let total = entries.len();
79
80 for (i, (name, child)) in entries.iter().enumerate() {
81 let is_last_entry = i == total - 1;
82
83 let connector = if is_root {
85 ""
86 } else if is_last_entry {
87 "└── "
88 } else {
89 "├── "
90 };
91
92 match child {
94 TreeNode::File { size } => {
95 if show_sizes {
96 output.push_str(&format!(
97 "{}{}{} ({})\n",
98 prefix,
99 connector,
100 name,
101 format_size(*size)
102 ));
103 } else {
104 output.push_str(&format!("{}{}{}\n", prefix, connector, name));
105 }
106 }
107 TreeNode::Directory { .. } => {
108 output.push_str(&format!("{}{}{}/\n", prefix, connector, name));
109
110 let new_prefix = if is_root {
112 prefix.to_string()
113 } else if is_last_entry {
114 format!("{} ", prefix)
115 } else {
116 format!("{}│ ", prefix)
117 };
118
119 output.push_str(&render_tree(child, &new_prefix, false, show_sizes));
120 }
121 }
122 }
123 }
124
125 output
126}
127
128pub fn generate_tree(root_name: &str, entries: &[FileEntry], show_sizes: bool) -> String {
130 if entries.is_empty() {
131 return format!("{}/\n(empty)\n", root_name);
132 }
133
134 let tree = build_tree(entries);
135 let mut output = format!("{}/\n", root_name);
136 output.push_str(&render_tree(&tree, "", true, show_sizes));
137 output
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use std::path::PathBuf;
144
145 #[test]
146 fn test_simple_tree() {
147 let entries = vec![
148 FileEntry {
149 absolute_path: PathBuf::from("/project/src/main.rs"),
150 relative_path: PathBuf::from("src/main.rs"),
151 size: 100,
152 },
153 FileEntry {
154 absolute_path: PathBuf::from("/project/Cargo.toml"),
155 relative_path: PathBuf::from("Cargo.toml"),
156 size: 200,
157 },
158 ];
159
160 let tree = generate_tree("project", &entries, false);
161 assert!(tree.contains("project/"));
162 assert!(tree.contains("src/"));
163 assert!(tree.contains("main.rs"));
164 assert!(tree.contains("Cargo.toml"));
165 }
166
167 #[test]
168 fn test_tree_with_sizes() {
169 let entries = vec![FileEntry {
170 absolute_path: PathBuf::from("/project/file.rs"),
171 relative_path: PathBuf::from("file.rs"),
172 size: 1024,
173 }];
174
175 let tree = generate_tree("project", &entries, true);
176 assert!(tree.contains("1.0 KB"));
177 }
178}