1use crate::console::RenderContext;
6use crate::renderable::{Renderable, Segment};
7use crate::style::Style;
8use crate::text::{Span, Text};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12pub enum GuideStyle {
13 Ascii,
15 #[default]
17 Unicode,
18 Bold,
20 Double,
22}
23
24impl GuideStyle {
25 fn chars(&self) -> TreeGuideChars {
26 match self {
27 GuideStyle::Ascii => TreeGuideChars {
28 vertical: '|',
29 horizontal: '-',
30 branch: '+',
31 last_branch: '\\',
32 space: ' ',
33 },
34 GuideStyle::Unicode => TreeGuideChars {
35 vertical: '│',
36 horizontal: '─',
37 branch: '├',
38 last_branch: '└',
39 space: ' ',
40 },
41 GuideStyle::Bold => TreeGuideChars {
42 vertical: '┃',
43 horizontal: '━',
44 branch: '┣',
45 last_branch: '┗',
46 space: ' ',
47 },
48 GuideStyle::Double => TreeGuideChars {
49 vertical: '║',
50 horizontal: '═',
51 branch: '╠',
52 last_branch: '╚',
53 space: ' ',
54 },
55 }
56 }
57}
58
59#[derive(Debug, Clone, Copy)]
60struct TreeGuideChars {
61 vertical: char,
62 horizontal: char,
63 branch: char,
64 last_branch: char,
65 #[allow(dead_code)]
66 space: char,
67}
68
69#[derive(Debug, Clone)]
71pub struct TreeNode {
72 label: Text,
74 children: Vec<TreeNode>,
76 style: Style,
78 expanded: bool,
80}
81
82impl TreeNode {
83 pub fn new<T: Into<Text>>(label: T) -> Self {
85 TreeNode {
86 label: label.into(),
87 children: Vec::new(),
88 style: Style::new(),
89 expanded: true,
90 }
91 }
92
93 pub fn add<T: Into<TreeNode>>(&mut self, child: T) -> &mut Self {
95 self.children.push(child.into());
96 self
97 }
98
99 pub fn with_child<T: Into<TreeNode>>(mut self, child: T) -> Self {
101 self.children.push(child.into());
102 self
103 }
104
105 pub fn with_children<I, T>(mut self, children: I) -> Self
107 where
108 I: IntoIterator<Item = T>,
109 T: Into<TreeNode>,
110 {
111 for child in children {
112 self.children.push(child.into());
113 }
114 self
115 }
116
117 pub fn style(mut self, style: Style) -> Self {
119 self.style = style;
120 self
121 }
122
123 pub fn expanded(mut self, expanded: bool) -> Self {
125 self.expanded = expanded;
126 self
127 }
128
129 pub fn has_children(&self) -> bool {
131 !self.children.is_empty()
132 }
133}
134
135impl<T: Into<Text>> From<T> for TreeNode {
136 fn from(label: T) -> Self {
137 TreeNode::new(label)
138 }
139}
140
141#[derive(Debug, Clone)]
143pub struct Tree {
144 root: TreeNode,
146 guide_style: GuideStyle,
148 style: Style,
150 hide_root: bool,
152}
153
154impl Tree {
155 pub fn new<T: Into<TreeNode>>(root: T) -> Self {
157 Tree {
158 root: root.into(),
159 guide_style: GuideStyle::Unicode,
160 style: Style::new(),
161 hide_root: false,
162 }
163 }
164
165 pub fn guide_style(mut self, style: GuideStyle) -> Self {
167 self.guide_style = style;
168 self
169 }
170
171 pub fn style(mut self, style: Style) -> Self {
173 self.style = style;
174 self
175 }
176
177 pub fn hide_root(mut self, hide: bool) -> Self {
179 self.hide_root = hide;
180 self
181 }
182
183 pub fn add<T: Into<TreeNode>>(&mut self, child: T) -> &mut Self {
185 self.root.children.push(child.into());
186 self
187 }
188
189 fn render_node(
190 &self,
191 node: &TreeNode,
192 prefix: &str,
193 is_last: bool,
194 is_root: bool,
195 chars: &TreeGuideChars,
196 segments: &mut Vec<Segment>,
197 ) {
198 if !is_root || !self.hide_root {
199 let mut spans = Vec::new();
200
201 if !is_root {
202 spans.push(Span::styled(prefix.to_string(), self.style));
204
205 let branch_char = if is_last {
206 chars.last_branch
207 } else {
208 chars.branch
209 };
210
211 spans.push(Span::styled(
212 format!("{}{}{} ", branch_char, chars.horizontal, chars.horizontal),
213 self.style,
214 ));
215 }
216
217 for span in &node.label.spans {
219 let combined_style = node.style.combine(&span.style);
220 spans.push(Span::styled(span.text.to_string(), combined_style));
221 }
222
223 segments.push(Segment::line(spans));
224 }
225
226 if node.expanded {
228 let child_count = node.children.len();
229 for (i, child) in node.children.iter().enumerate() {
230 let is_last_child = i == child_count - 1;
231
232 let new_prefix = if is_root {
233 String::new()
234 } else {
235 let connector = if is_last {
236 " ".to_string()
237 } else {
238 format!("{} ", chars.vertical)
239 };
240 format!("{}{}", prefix, connector)
241 };
242
243 self.render_node(child, &new_prefix, is_last_child, false, chars, segments);
244 }
245 }
246 }
247}
248
249impl Renderable for Tree {
250 fn render(&self, _context: &RenderContext) -> Vec<Segment> {
251 let chars = self.guide_style.chars();
252 let mut segments = Vec::new();
253
254 self.render_node(&self.root, "", true, true, &chars, &mut segments);
255
256 segments
257 }
258}
259
260#[cfg(feature = "std")]
262pub fn from_directory(path: &std::path::Path) -> std::io::Result<Tree> {
263 fn build_node(path: &std::path::Path) -> std::io::Result<TreeNode> {
264 let name = path
265 .file_name()
266 .map(|s| s.to_string_lossy().to_string())
267 .unwrap_or_else(|| path.to_string_lossy().to_string());
268
269 let mut node = TreeNode::new(name);
270
271 if path.is_dir() {
272 let mut entries: Vec<_> = std::fs::read_dir(path)?.filter_map(|e| e.ok()).collect();
273
274 entries.sort_by_key(|e| e.file_name());
275
276 for entry in entries {
277 let child = build_node(&entry.path())?;
278 node.children.push(child);
279 }
280 }
281
282 Ok(node)
283 }
284
285 let root = build_node(path)?;
286 Ok(Tree::new(root))
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292
293 #[test]
294 fn test_tree_simple() {
295 let tree = Tree::new("root").guide_style(GuideStyle::Unicode);
296
297 let context = RenderContext {
298 width: 40,
299 height: None,
300 };
301 let segments = tree.render(&context);
302
303 assert_eq!(segments.len(), 1);
304 assert!(segments[0].plain_text().contains("root"));
305 }
306
307 #[test]
308 fn test_tree_with_children() {
309 let mut tree = Tree::new("root");
310 tree.add(TreeNode::new("child1"));
311 tree.add(TreeNode::new("child2"));
312
313 let context = RenderContext {
314 width: 40,
315 height: None,
316 };
317 let segments = tree.render(&context);
318
319 assert_eq!(segments.len(), 3);
320 }
321
322 #[test]
323 fn test_tree_nested() {
324 let child1 = TreeNode::new("child1")
325 .with_child("grandchild1")
326 .with_child("grandchild2");
327
328 let tree = Tree::new(TreeNode::new("root").with_child(child1));
329
330 let context = RenderContext {
331 width: 40,
332 height: None,
333 };
334 let segments = tree.render(&context);
335
336 assert_eq!(segments.len(), 4);
337
338 let text: String = segments.iter().map(|s| s.plain_text()).collect();
340 assert!(text.contains("├"));
341 assert!(text.contains("└"));
342 }
343
344 #[test]
345 fn test_tree_hide_root() {
346 let mut tree = Tree::new("root").hide_root(true);
347 tree.add("child1");
348 tree.add("child2");
349
350 let context = RenderContext {
351 width: 40,
352 height: None,
353 };
354 let segments = tree.render(&context);
355
356 let text: String = segments.iter().map(|s| s.plain_text()).collect();
358 assert!(!text.contains("root"));
359 assert!(text.contains("child1"));
360 }
361}