1use crate::console::{Console, ConsoleOptions};
7use crate::renderables::Renderable;
8use crate::segment::Segment;
9use crate::style::Style;
10use crate::text::Text;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
14pub enum TreeGuides {
15 Ascii,
17 #[default]
19 Unicode,
20 Bold,
22 Double,
24 Rounded,
26}
27
28impl TreeGuides {
29 #[must_use]
31 pub const fn vertical(&self) -> &str {
32 match self {
33 Self::Ascii => "| ",
34 Self::Unicode | Self::Rounded => "\u{2502} ", Self::Bold => "\u{2503} ", Self::Double => "\u{2551} ", }
38 }
39
40 #[must_use]
42 pub const fn branch(&self) -> &str {
43 match self {
44 Self::Ascii => "+-- ",
45 Self::Unicode => "\u{251C}\u{2500}\u{2500} ", Self::Bold => "\u{2523}\u{2501}\u{2501} ", Self::Double => "\u{2560}\u{2550}\u{2550} ", Self::Rounded => "\u{251C}\u{2500}\u{2500} ", }
50 }
51
52 #[must_use]
54 pub const fn last(&self) -> &str {
55 match self {
56 Self::Ascii => "`-- ",
57 Self::Unicode => "\u{2514}\u{2500}\u{2500} ", Self::Bold => "\u{2517}\u{2501}\u{2501} ", Self::Double => "\u{255A}\u{2550}\u{2550} ", Self::Rounded => "\u{2570}\u{2500}\u{2500} ", }
62 }
63
64 #[must_use]
66 pub const fn space(&self) -> &'static str {
67 " "
68 }
69}
70
71#[derive(Debug, Clone)]
73pub struct TreeNode {
74 label: Text,
76 children: Vec<TreeNode>,
78 expanded: bool,
80 icon: Option<String>,
82 icon_style: Style,
84}
85
86impl TreeNode {
87 #[must_use]
93 pub fn new(label: impl Into<Text>) -> Self {
94 Self {
95 label: label.into(),
96 children: Vec::new(),
97 expanded: true,
98 icon: None,
99 icon_style: Style::new(),
100 }
101 }
102
103 #[must_use]
109 pub fn with_icon(icon: impl Into<String>, label: impl Into<Text>) -> Self {
110 Self {
111 label: label.into(),
112 children: Vec::new(),
113 expanded: true,
114 icon: Some(icon.into()),
115 icon_style: Style::new(),
116 }
117 }
118
119 #[must_use]
121 pub fn child(mut self, node: TreeNode) -> Self {
122 self.children.push(node);
123 self
124 }
125
126 #[must_use]
128 pub fn children(mut self, nodes: impl IntoIterator<Item = TreeNode>) -> Self {
129 self.children.extend(nodes);
130 self
131 }
132
133 #[must_use]
135 pub fn icon(mut self, icon: impl Into<String>) -> Self {
136 self.icon = Some(icon.into());
137 self
138 }
139
140 #[must_use]
142 pub fn icon_style(mut self, style: Style) -> Self {
143 self.icon_style = style;
144 self
145 }
146
147 #[must_use]
149 pub fn expanded(mut self, expanded: bool) -> Self {
150 self.expanded = expanded;
151 self
152 }
153
154 #[must_use]
156 pub fn collapsed(self) -> Self {
157 self.expanded(false)
158 }
159
160 #[must_use]
162 pub fn label(&self) -> &Text {
163 &self.label
164 }
165
166 #[must_use]
168 pub fn children_nodes(&self) -> &[TreeNode] {
169 &self.children
170 }
171
172 #[must_use]
174 pub fn has_children(&self) -> bool {
175 !self.children.is_empty()
176 }
177
178 #[must_use]
180 pub fn is_expanded(&self) -> bool {
181 self.expanded
182 }
183
184 #[must_use]
186 pub fn get_icon(&self) -> Option<&str> {
187 self.icon.as_deref()
188 }
189}
190
191#[derive(Debug, Clone)]
193pub struct Tree {
194 root: TreeNode,
196 guides: TreeGuides,
198 guide_style: Style,
200 show_root: bool,
202 highlight_style: Option<Style>,
204 max_depth: isize,
206}
207
208impl Default for Tree {
209 fn default() -> Self {
210 Self {
211 root: TreeNode::new("root"),
212 guides: TreeGuides::default(),
213 guide_style: Style::new(),
214 show_root: true,
215 highlight_style: None,
216 max_depth: -1,
217 }
218 }
219}
220
221impl Tree {
222 #[must_use]
224 pub fn new(root: TreeNode) -> Self {
225 Self {
226 root,
227 ..Self::default()
228 }
229 }
230
231 #[must_use]
237 pub fn with_label(label: impl Into<Text>) -> Self {
238 Self::new(TreeNode::new(label))
239 }
240
241 #[must_use]
243 pub fn guides(mut self, guides: TreeGuides) -> Self {
244 self.guides = guides;
245 self
246 }
247
248 #[must_use]
250 pub fn guide_style(mut self, style: Style) -> Self {
251 self.guide_style = style;
252 self
253 }
254
255 #[must_use]
257 pub fn show_root(mut self, show: bool) -> Self {
258 self.show_root = show;
259 self
260 }
261
262 #[must_use]
264 pub fn hide_root(self) -> Self {
265 self.show_root(false)
266 }
267
268 #[must_use]
270 pub fn highlight_style(mut self, style: Style) -> Self {
271 self.highlight_style = Some(style);
272 self
273 }
274
275 #[must_use]
277 pub fn max_depth(mut self, depth: isize) -> Self {
278 self.max_depth = depth;
279 self
280 }
281
282 #[must_use]
284 pub fn child(mut self, node: TreeNode) -> Self {
285 self.root.children.push(node);
286 self
287 }
288
289 #[must_use]
291 pub fn children(mut self, nodes: impl IntoIterator<Item = TreeNode>) -> Self {
292 self.root.children.extend(nodes);
293 self
294 }
295
296 #[must_use]
298 pub fn render(&self) -> Vec<Segment<'_>> {
299 let mut segments = Vec::new();
300 let prefix_stack: Vec<bool> = Vec::new();
301
302 if self.show_root {
303 self.render_node(&self.root, &mut segments, &prefix_stack, true, 0);
304 } else {
305 let children = &self.root.children;
307 for (i, child) in children.iter().enumerate() {
308 let is_last = i == children.len() - 1;
309 self.render_node(child, &mut segments, &prefix_stack, is_last, 0);
310 }
311 }
312
313 segments
314 }
315
316 fn sanitize_label(label: &Text) -> Text {
317 if !label.plain().contains('\n') {
318 return label.clone();
319 }
320
321 let mut sanitized = Text::new(label.plain().replace('\n', " "));
322 sanitized.set_style(label.style().clone());
323 sanitized.justify = label.justify;
324 sanitized.overflow = label.overflow;
325 sanitized.no_wrap = label.no_wrap;
326 sanitized.end.clone_from(&label.end);
327 sanitized.tab_size = label.tab_size;
328 for span in label.spans() {
329 sanitized.stylize(span.start, span.end, span.style.clone());
330 }
331 sanitized
332 }
333
334 #[expect(
336 clippy::cast_possible_wrap,
337 reason = "tree depth will never exceed isize::MAX"
338 )]
339 fn render_node<'a>(
340 &'a self,
341 node: &'a TreeNode,
342 segments: &mut Vec<Segment<'a>>,
343 prefix_stack: &[bool],
344 is_last: bool,
345 depth: usize,
346 ) {
347 if self.max_depth >= 0 && depth as isize > self.max_depth {
349 return;
350 }
351
352 for &has_more_siblings in prefix_stack {
354 let guide = if has_more_siblings {
355 self.guides.vertical()
356 } else {
357 self.guides.space()
358 };
359 segments.push(Segment::new(guide, Some(self.guide_style.clone())));
360 }
361
362 if depth > 0 || !self.show_root {
364 let guide = if is_last {
365 self.guides.last()
366 } else {
367 self.guides.branch()
368 };
369 segments.push(Segment::new(guide, Some(self.guide_style.clone())));
370 }
371
372 if let Some(icon) = node.get_icon() {
374 segments.push(Segment::new(
375 format!("{icon} "),
376 Some(node.icon_style.clone()),
377 ));
378 }
379
380 let label_text = Self::sanitize_label(&node.label);
382
383 let mut label_segments: Vec<Segment<'static>> = label_text
384 .render("")
385 .into_iter()
386 .map(Segment::into_owned)
387 .collect();
388 if let Some(ref highlight) = self.highlight_style {
389 for segment in &mut label_segments {
390 if !segment.is_control() {
391 segment.style = Some(match segment.style.take() {
392 Some(existing) => existing.combine(highlight),
393 None => highlight.clone(),
394 });
395 }
396 }
397 }
398 for segment in label_segments {
399 segments.push(segment);
400 }
401
402 if node.has_children() && !node.is_expanded() {
404 segments.push(Segment::new(" [...]", Some(self.guide_style.clone())));
405 }
406
407 segments.push(Segment::line());
408
409 if node.is_expanded() {
411 let children = &node.children;
412 let mut new_prefix_stack = prefix_stack.to_vec();
413 if !(self.show_root && depth == 0) {
414 new_prefix_stack.push(!is_last);
415 }
416
417 for (i, child) in children.iter().enumerate() {
418 let child_is_last = i == children.len() - 1;
419 self.render_node(child, segments, &new_prefix_stack, child_is_last, depth + 1);
420 }
421 }
422 }
423
424 #[must_use]
426 pub fn render_plain(&self) -> String {
427 self.render()
428 .into_iter()
429 .map(|seg| seg.text.into_owned())
430 .collect()
431 }
432}
433
434impl Renderable for Tree {
435 fn render<'a>(&'a self, _console: &Console, _options: &ConsoleOptions) -> Vec<Segment<'a>> {
436 self.render()
437 }
438}
439
440#[must_use]
444pub fn file_tree(root: &str, entries: &[(&str, bool)]) -> Tree {
445 let mut root_node = TreeNode::with_icon("📁", root);
446
447 for (path, is_dir) in entries {
448 let icon = if *is_dir { "📁" } else { "📄" };
449 root_node = root_node.child(TreeNode::with_icon(icon, *path));
450 }
451
452 Tree::new(root_node)
453}
454
455#[must_use]
457pub fn ascii_tree(root: TreeNode) -> Tree {
458 Tree::new(root).guides(TreeGuides::Ascii)
459}
460
461#[must_use]
463pub fn rounded_tree(root: TreeNode) -> Tree {
464 Tree::new(root).guides(TreeGuides::Rounded)
465}
466
467#[must_use]
469pub fn bold_tree(root: TreeNode) -> Tree {
470 Tree::new(root).guides(TreeGuides::Bold)
471}
472
473#[cfg(test)]
474mod tests {
475 use super::*;
476
477 #[test]
478 fn test_tree_node_new() {
479 let node = TreeNode::new("test");
480 assert_eq!(node.label().plain(), "test");
481 assert!(node.children_nodes().is_empty());
482 assert!(node.is_expanded());
483 }
484
485 #[test]
486 fn test_tree_node_with_icon() {
487 let node = TreeNode::with_icon("📁", "folder");
488 assert_eq!(node.label().plain(), "folder");
489 assert_eq!(node.get_icon(), Some("📁"));
490 }
491
492 #[test]
493 fn test_tree_node_children() {
494 let node = TreeNode::new("root")
495 .child(TreeNode::new("child1"))
496 .child(TreeNode::new("child2"));
497 assert_eq!(node.children_nodes().len(), 2);
498 assert!(node.has_children());
499 }
500
501 #[test]
502 fn test_tree_node_collapsed() {
503 let node = TreeNode::new("test").collapsed();
504 assert!(!node.is_expanded());
505 }
506
507 #[test]
508 fn test_tree_new() {
509 let tree = Tree::with_label("root");
510 assert!(tree.show_root);
511 assert_eq!(tree.guides, TreeGuides::Unicode);
512 }
513
514 #[test]
515 fn test_tree_guides_ascii() {
516 let guides = TreeGuides::Ascii;
517 assert_eq!(guides.vertical(), "| ");
518 assert_eq!(guides.branch(), "+-- ");
519 assert_eq!(guides.last(), "`-- ");
520 assert_eq!(guides.space(), " ");
521 }
522
523 #[test]
524 fn test_tree_guides_unicode() {
525 let guides = TreeGuides::Unicode;
526 assert!(guides.vertical().starts_with('\u{2502}')); assert!(guides.branch().starts_with('\u{251C}')); assert!(guides.last().starts_with('\u{2514}')); }
530
531 #[test]
532 fn test_tree_render_simple() {
533 let tree = Tree::with_label("root")
534 .child(TreeNode::new("child1"))
535 .child(TreeNode::new("child2"));
536
537 let segments = tree.render();
538 assert!(!segments.is_empty());
539
540 let plain = tree.render_plain();
541 assert!(plain.contains("root"));
542 assert!(plain.contains("child1"));
543 assert!(plain.contains("child2"));
544 }
545
546 #[test]
547 fn test_tree_render_preserves_spans() {
548 use crate::style::Attributes;
549
550 let mut label = Text::new("root");
551 label.stylize(0, 4, Style::new().bold());
552 let tree = Tree::new(TreeNode::new(label));
553
554 let segments = tree.render();
555 let has_bold = segments.iter().any(|seg| {
556 seg.text.contains("root")
557 && seg
558 .style
559 .as_ref()
560 .is_some_and(|style| style.attributes.contains(Attributes::BOLD))
561 });
562
563 assert!(has_bold);
564 }
565
566 #[test]
567 fn test_tree_render_preserves_spans_after_newline_sanitization() {
568 use crate::style::Attributes;
569
570 let mut label = Text::new("root\nnode");
571 label.stylize_all(Style::new().bold());
572 label.stylize(5, 9, Style::new().italic());
573 let tree = Tree::new(TreeNode::new(label));
574
575 let rendered = tree.render_plain();
576 assert!(rendered.contains("root node"));
577 assert!(!rendered.contains("root\nnode"));
578
579 let segments = tree.render();
580 let has_italic_node = segments.iter().any(|seg| {
581 seg.text.contains("node")
582 && seg
583 .style
584 .as_ref()
585 .is_some_and(|style| style.attributes.contains(Attributes::ITALIC))
586 });
587 assert!(has_italic_node);
588 }
589
590 #[test]
591 fn test_tree_render_nested() {
592 let tree =
593 Tree::with_label("root").child(TreeNode::new("parent").child(TreeNode::new("child")));
594
595 let plain = tree.render_plain();
596 assert!(plain.contains("root"));
597 assert!(plain.contains("parent"));
598 assert!(plain.contains("child"));
599 }
600
601 #[test]
602 fn test_tree_hide_root() {
603 let tree = Tree::with_label("root")
604 .hide_root()
605 .child(TreeNode::new("visible"));
606
607 let plain = tree.render_plain();
608 assert!(!plain.contains("root"));
609 assert!(plain.contains("visible"));
610 }
611
612 #[test]
613 fn test_tree_collapsed_node() {
614 let tree = Tree::with_label("root").child(
615 TreeNode::new("collapsed")
616 .collapsed()
617 .child(TreeNode::new("hidden")),
618 );
619
620 let plain = tree.render_plain();
621 assert!(plain.contains("collapsed"));
622 assert!(plain.contains("[...]"));
623 assert!(!plain.contains("hidden"));
624 }
625
626 #[test]
627 fn test_tree_max_depth() {
628 let tree = Tree::with_label("root")
629 .max_depth(1)
630 .child(TreeNode::new("level1").child(TreeNode::new("level2")));
631
632 let plain = tree.render_plain();
633 assert!(plain.contains("root"));
634 assert!(plain.contains("level1"));
635 assert!(!plain.contains("level2"));
636 }
637
638 #[test]
639 fn test_tree_ascii_style() {
640 let tree = ascii_tree(TreeNode::new("root").child(TreeNode::new("child")));
641
642 let plain = tree.render_plain();
643 assert!(plain.contains("+--") || plain.contains("`--"));
644 }
645
646 #[test]
647 fn test_tree_with_icons() {
648 let tree = Tree::with_label("project")
649 .child(TreeNode::with_icon("📁", "src"))
650 .child(TreeNode::with_icon("📄", "README.md"));
651
652 let plain = tree.render_plain();
653 assert!(plain.contains("📁"));
654 assert!(plain.contains("📄"));
655 assert!(plain.contains("src"));
656 assert!(plain.contains("README.md"));
657 }
658
659 #[test]
660 fn test_file_tree() {
661 let tree = file_tree("project", &[("src", true), ("Cargo.toml", false)]);
662
663 let plain = tree.render_plain();
664 assert!(plain.contains("project"));
665 assert!(plain.contains("src"));
666 assert!(plain.contains("Cargo.toml"));
667 }
668
669 #[test]
670 fn test_tree_complex_structure() {
671 let tree = Tree::with_label("root")
672 .child(
673 TreeNode::new("branch1")
674 .child(TreeNode::new("leaf1"))
675 .child(TreeNode::new("leaf2")),
676 )
677 .child(
678 TreeNode::new("branch2")
679 .child(TreeNode::new("sub-branch").child(TreeNode::new("deep-leaf"))),
680 )
681 .child(TreeNode::new("leaf3"));
682
683 let plain = tree.render_plain();
684
685 assert!(plain.contains("root"));
687 assert!(plain.contains("branch1"));
688 assert!(plain.contains("branch2"));
689 assert!(plain.contains("leaf1"));
690 assert!(plain.contains("leaf2"));
691 assert!(plain.contains("leaf3"));
692 assert!(plain.contains("sub-branch"));
693 assert!(plain.contains("deep-leaf"));
694 }
695
696 #[test]
697 fn test_tree_empty_root() {
698 let tree = Tree::with_label("");
700 let plain = tree.render_plain();
701 let _ = plain;
704 }
705
706 #[test]
707 fn test_tree_single_node() {
708 let tree = Tree::with_label("single");
709 let plain = tree.render_plain();
710 assert!(plain.contains("single"));
711 assert!(!plain.contains("├──"));
713 assert!(!plain.contains("└──"));
714 }
715
716 #[test]
717 fn test_tree_wide_unicode_labels() {
718 let tree = Tree::with_label("项目") .child(TreeNode::new("源代码")) .child(TreeNode::new("文档")); let plain = tree.render_plain();
724 assert!(plain.contains("项目"));
725 assert!(plain.contains("源代码"));
726 assert!(plain.contains("文档"));
727 }
728
729 #[test]
730 fn test_tree_emoji_labels() {
731 let tree = Tree::with_label("📁 Root")
732 .child(TreeNode::new("📄 File"))
733 .child(TreeNode::new("🔧 Config"));
734
735 let plain = tree.render_plain();
736 assert!(plain.contains("📁"));
737 assert!(plain.contains("📄"));
738 assert!(plain.contains("🔧"));
739 }
740
741 #[test]
742 fn test_tree_guides_bold() {
743 let guides = TreeGuides::Bold;
744 assert_eq!(guides.vertical(), "┃ ");
745 assert_eq!(guides.branch(), "┣━━ ");
746 assert_eq!(guides.last(), "┗━━ ");
747 assert_eq!(guides.space(), " ");
748 }
749
750 #[test]
751 fn test_tree_guides_double() {
752 let guides = TreeGuides::Double;
753 assert_eq!(guides.vertical(), "║ ");
754 assert_eq!(guides.branch(), "╠══ ");
755 assert_eq!(guides.last(), "╚══ ");
756 assert_eq!(guides.space(), " ");
757 }
758
759 #[test]
760 fn test_tree_guides_rounded() {
761 let guides = TreeGuides::Rounded;
762 assert_eq!(guides.vertical(), "│ ");
763 assert_eq!(guides.branch(), "├── ");
764 assert_eq!(guides.last(), "╰── "); assert_eq!(guides.space(), " ");
766 }
767}