Skip to main content

rich/
tree.rs

1//! Trees.
2//!
3//! Port of upstream `rich/tree.py`. A [`Tree`] renders a hierarchy with the
4//! familiar `├──`/`└──` guide lines: string (markup), `Text` or renderable
5//! labels, per-node `style`/`guide_style` (a bold guide style draws heavy
6//! guides, `underline2` double ones, and an ASCII-only console ASCII ones),
7//! collapsed (`expanded = false`) nodes and a hidden root.
8
9use crate::console::Justify;
10use crate::console::{Console, ConsoleOptions};
11use crate::measure::Measurement;
12use crate::protocol::Renderable;
13use crate::segment::Segment;
14use crate::style::{Style, StyleType};
15use crate::table::Cell;
16
17/// `Tree.ASCII_GUIDES`: the guides on a console that cannot encode Unicode.
18pub const ASCII_GUIDES: [&str; 4] = ["    ", "|   ", "+-- ", "`-- "];
19/// `Tree.TREE_GUIDES`: thin, heavy (bold) and double (underline2) guides.
20pub const TREE_GUIDES: [[&str; 4]; 3] = [
21    ["    ", "│   ", "├── ", "└── "],
22    ["    ", "┃   ", "┣━━ ", "┗━━ "],
23    ["    ", "║   ", "╠══ ", "╚══ "],
24];
25
26// Indexes into a guide set, as upstream's `SPACE, CONTINUE, FORK, END`.
27const SPACE: usize = 0;
28const CONTINUE: usize = 1;
29const FORK: usize = 2;
30const END: usize = 3;
31
32/// A node in a hierarchy. Mirrors `rich.tree.Tree`.
33pub struct Tree {
34    label: Cell,
35    style: StyleType,
36    guide_style: StyleType,
37    children: Vec<Tree>,
38    expanded: bool,
39    highlight: bool,
40    hide_root: bool,
41}
42
43impl Tree {
44    /// A new tree/subtree with the given label. A string label is console
45    /// markup, as upstream's `Tree("[b]root")` is; pass a
46    /// [`Text`](crate::text::Text) for a literal one.
47    pub fn new(label: impl Into<Cell>) -> Self {
48        Tree {
49            label: label.into(),
50            style: StyleType::Name("tree".to_string()),
51            guide_style: StyleType::Name("tree.line".to_string()),
52            children: Vec::new(),
53            expanded: true,
54            highlight: false,
55            hide_root: false,
56        }
57    }
58
59    /// Highlight string labels (upstream `Tree(highlight=…)`, default off).
60    /// Upstream renders every label with the root's setting.
61    pub fn highlight(mut self, highlight: bool) -> Self {
62        self.highlight = highlight;
63        self
64    }
65
66    /// The style of this node's label and, stacked, its descendants'
67    /// (upstream `Tree(style=…)`, default `"tree"`).
68    pub fn style(mut self, style: impl Into<StyleType>) -> Self {
69        self.style = style.into();
70        self
71    }
72
73    /// The style of the guide lines below this node (upstream
74    /// `Tree(guide_style=…)`, default `"tree.line"`). Bold selects the heavy
75    /// guides and `underline2` the double ones.
76    pub fn guide_style(mut self, style: impl Into<StyleType>) -> Self {
77        self.guide_style = style.into();
78        self
79    }
80
81    /// Whether this node's children are shown (upstream `expanded`, default
82    /// on).
83    pub fn expanded(mut self, expanded: bool) -> Self {
84        self.expanded = expanded;
85        self
86    }
87
88    /// Hide the root node, rendering its children as the top level (upstream
89    /// `hide_root`, default off; read from the root only).
90    pub fn hide_root(mut self, hide_root: bool) -> Self {
91        self.hide_root = hide_root;
92        self
93    }
94
95    /// Set [`style`](Self::style) in place.
96    pub fn set_style(&mut self, style: impl Into<StyleType>) -> &mut Self {
97        self.style = style.into();
98        self
99    }
100
101    /// Set [`guide_style`](Self::guide_style) in place.
102    pub fn set_guide_style(&mut self, style: impl Into<StyleType>) -> &mut Self {
103        self.guide_style = style.into();
104        self
105    }
106
107    /// Set [`expanded`](Self::expanded) in place (upstream assigns
108    /// `tree.expanded`).
109    pub fn set_expanded(&mut self, expanded: bool) -> &mut Self {
110        self.expanded = expanded;
111        self
112    }
113
114    /// Set [`hide_root`](Self::hide_root) in place.
115    pub fn set_hide_root(&mut self, hide_root: bool) -> &mut Self {
116        self.hide_root = hide_root;
117        self
118    }
119
120    /// Set [`highlight`](Self::highlight) in place.
121    pub fn set_highlight(&mut self, highlight: bool) -> &mut Self {
122        self.highlight = highlight;
123        self
124    }
125
126    /// Replace the label in place.
127    pub fn set_label(&mut self, label: impl Into<Cell>) -> &mut Self {
128        self.label = label.into();
129        self
130    }
131
132    /// The label.
133    pub fn label(&self) -> &Cell {
134        &self.label
135    }
136
137    /// The node's style (see [`style`](Self::style)).
138    pub fn get_style(&self) -> &StyleType {
139        &self.style
140    }
141
142    /// The node's guide style (see [`guide_style`](Self::guide_style)).
143    pub fn get_guide_style(&self) -> &StyleType {
144        &self.guide_style
145    }
146
147    /// Whether children are shown.
148    pub fn is_expanded(&self) -> bool {
149        self.expanded
150    }
151
152    /// Whether the root is hidden.
153    pub fn is_root_hidden(&self) -> bool {
154        self.hide_root
155    }
156
157    /// Whether string labels are highlighted.
158    pub fn is_highlighted(&self) -> bool {
159        self.highlight
160    }
161
162    /// The child nodes.
163    pub fn children(&self) -> &[Tree] {
164        &self.children
165    }
166
167    /// The child nodes, mutably.
168    pub fn children_mut(&mut self) -> &mut Vec<Tree> {
169        &mut self.children
170    }
171
172    /// Add a child with `label`, returning a mutable reference to it so further
173    /// descendants can be attached. Mirrors `Tree.add` with its defaults: the
174    /// child inherits this node's `style` and `guide_style` and is expanded.
175    pub fn add(&mut self, label: impl Into<Cell>) -> &mut Tree {
176        let mut child = Tree::new(label);
177        child.style = self.style.clone();
178        child.guide_style = self.guide_style.clone();
179        self.add_tree(child)
180    }
181
182    /// Attach an already built subtree as the last child, returning it. Use
183    /// it for upstream's `Tree.add(label, style=…, guide_style=…,
184    /// expanded=…)`.
185    pub fn add_tree(&mut self, child: Tree) -> &mut Tree {
186        self.children.push(child);
187        self.children.last_mut().expect("just pushed a child")
188    }
189
190    /// `make_guide`: the guide segment at `index` for a level in `style`.
191    fn make_guide(options: &ConsoleOptions, index: usize, style: Style) -> Segment {
192        let line = if options.ascii_only() {
193            ASCII_GUIDES[index]
194        } else {
195            let guide = if style.attr(BOLD) == Some(true) {
196                1
197            } else if style.attr(UNDERLINE2) == Some(true) {
198                2
199            } else {
200                0
201            };
202            TREE_GUIDES[if options.legacy_windows { 0 } else { guide }][index]
203        };
204        Segment::new(line, Some(style))
205    }
206}
207
208// Attribute indexes in `Style` (bold, underline2).
209const BOLD: usize = 0;
210const UNDERLINE2: usize = 9;
211
212/// `Styled(node.label, style)` for the render loop: the label rendered as
213/// upstream renders a `str`/`Text`/renderable, with `style` beneath it.
214struct Label<'a> {
215    label: &'a Cell,
216    style: &'a Style,
217    highlight: bool,
218}
219
220impl Renderable for Label<'_> {
221    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
222        let segments = match self.label {
223            Cell::Renderable(renderable) => renderable.rich_render(console, options),
224            cell => cell
225                .to_text(console, Some(self.highlight))
226                .unwrap_or_default()
227                .rich_render(console, options),
228        };
229        if self.style.is_null() {
230            segments
231        } else {
232            Segment::apply_style(&segments, self.style)
233        }
234    }
235}
236
237impl Tree {
238    /// Port of `Tree.__rich_console__`, as lines.
239    ///
240    /// Like upstream, this walks an explicit stack rather than recursing, so
241    /// a very deep tree cannot overflow the thread stack. The prefix (four
242    /// cells per level) is only copied for nodes that still have room to
243    /// render: materialising every prefix would cost quadratic memory on a
244    /// deep chain.
245    fn render_lines_into(&self, console: &Console, options: &ConsoleOptions) -> Vec<Vec<Segment>> {
246        let get_style = |style: &StyleType| console.get_style(style).unwrap_or_default();
247        let guide_style = get_style(&self.guide_style);
248        let mut levels: Vec<Segment> =
249            vec![Self::make_guide(options, CONTINUE, guide_style.clone())];
250        let mut stack: Vec<(&[Tree], usize)> = vec![(std::slice::from_ref(self), 0)];
251        let mut guide_style_stack = vec![guide_style];
252        let mut style_stack = vec![get_style(&self.style)];
253        let remove_guide_styles = Style::parse("not bold not underline2").unwrap_or_default();
254        let offset = if self.hide_root { 2 } else { 1 };
255        let pad = options.justify != Justify::Default;
256        let mut depth = 0usize;
257        let mut lines = Vec::new();
258        let level_style = |segment: &Segment| segment.style.clone().unwrap_or_default();
259
260        while let Some((siblings, next)) = stack.last_mut() {
261            let siblings: &[Tree] = siblings;
262            let Some(node) = siblings.get(*next) else {
263                stack.pop();
264                levels.pop();
265                if let Some(level) = levels.last_mut() {
266                    let style = level_style(level);
267                    *level = Self::make_guide(options, FORK, style);
268                    guide_style_stack.pop();
269                    style_stack.pop();
270                }
271                continue;
272            };
273            *next += 1;
274            let last = *next == siblings.len();
275            if last {
276                let level = levels.last_mut().expect("a level per open stack entry");
277                *level = Self::make_guide(options, END, level_style(level));
278            }
279
280            let current_guide = guide_style_stack.last().cloned().unwrap_or_default();
281            let current_style = style_stack.last().cloned().unwrap_or_default();
282            let node_guide_style = current_guide.combine(&get_style(&node.guide_style));
283            let style = current_style.combine(&get_style(&node.style));
284            let prefix_width = levels.len().saturating_sub(offset) * 4;
285
286            if !(depth == 0 && self.hide_root) && prefix_width < options.max_width {
287                let mut prefix: Vec<Segment> = levels[offset.min(levels.len())..].to_vec();
288                let mut label_options = options.update_width(options.max_width - prefix_width);
289                label_options.highlight = Some(self.highlight);
290                label_options.height = None;
291                let label = Label {
292                    label: &node.label,
293                    style: &style,
294                    highlight: self.highlight,
295                };
296                let background = Style::from_color(None, style.bgcolor().cloned());
297                for (index, label_line) in console
298                    .render_lines(&label, &label_options, pad)
299                    .into_iter()
300                    .enumerate()
301                {
302                    let mut line = Vec::new();
303                    if !prefix.is_empty() {
304                        line.extend(prefix.iter().map(|segment| {
305                            let own = segment.style.clone().unwrap_or_default();
306                            let own = if background.is_null() {
307                                own
308                            } else {
309                                background.combine(&own)
310                            };
311                            let style = if own.is_null() {
312                                remove_guide_styles.clone()
313                            } else {
314                                own.combine(&remove_guide_styles)
315                            };
316                            Segment::new(segment.text.clone(), Some(style))
317                        }));
318                    }
319                    line.extend(label_line);
320                    lines.push(line);
321                    if index == 0 && !prefix.is_empty() {
322                        let level = prefix.last_mut().expect("non-empty prefix");
323                        *level = Self::make_guide(
324                            options,
325                            if last { SPACE } else { CONTINUE },
326                            level_style(level),
327                        );
328                    }
329                }
330            }
331
332            if node.expanded && !node.children.is_empty() {
333                let level = levels.last_mut().expect("a level per open stack entry");
334                *level = Self::make_guide(
335                    options,
336                    if last { SPACE } else { CONTINUE },
337                    level_style(level),
338                );
339                levels.push(Self::make_guide(
340                    options,
341                    if node.children.len() == 1 { END } else { FORK },
342                    node_guide_style,
343                ));
344                style_stack.push(current_style.combine(&get_style(&node.style)));
345                guide_style_stack.push(current_guide.combine(&get_style(&node.guide_style)));
346                stack.push((&node.children, 0));
347                depth += 1;
348            }
349        }
350        lines
351    }
352}
353
354impl Drop for Tree {
355    /// Drop descendants from an explicit stack: the derived drop recurses
356    /// once per level and overflows the stack on a very deep tree.
357    fn drop(&mut self) {
358        let mut pending = std::mem::take(&mut self.children);
359        while let Some(mut child) = pending.pop() {
360            pending.append(&mut child.children);
361        }
362    }
363}
364
365impl Renderable for Tree {
366    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
367        let lines = self.render_lines_into(console, options);
368        let mut segments = Vec::new();
369        let last = lines.len().saturating_sub(1);
370        for (index, line) in lines.into_iter().enumerate() {
371            // Upstream ends every row with a newline, so an empty last row
372            // (`Tree("")`) is still a row; mark it with an empty segment.
373            if index == last && line.is_empty() {
374                segments.push(Segment::new("", None));
375            }
376            segments.extend(line);
377            if index != last {
378                segments.push(Segment::line());
379            }
380        }
381        segments
382    }
383
384    /// Port of `Tree.__rich_measure__`: the widest label plus its indent of
385    /// four cells per level, for both bounds.
386    fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
387        // Iterative, like the render: `(node, level)` pairs to visit.
388        let mut width = (0, 0);
389        let mut pending: Vec<(&Tree, usize)> = vec![(self, 0)];
390        while let Some((tree, level)) = pending.pop() {
391            let label = tree.label.measure_cell(console, options);
392            let indent = level * 4;
393            width.0 = width.0.max(label.minimum + indent);
394            width.1 = width.1.max(label.maximum + indent);
395            if tree.expanded {
396                pending.extend(tree.children.iter().map(|child| (child, level + 1)));
397            }
398        }
399        Measurement::new(width.0, width.1)
400    }
401}
402
403#[cfg(test)]
404mod tests {
405    use super::*;
406    use crate::color::ColorSystem;
407
408    fn console() -> Console {
409        Console::builder()
410            .force_terminal(true)
411            .color_system(Some(ColorSystem::Truecolor))
412            .width(40)
413            .build()
414    }
415
416    #[test]
417    fn nested_tree() {
418        let mut tree = Tree::new("root");
419        let a = tree.add("child A");
420        a.add("leaf A1");
421        a.add("leaf A2");
422        tree.add("child B");
423        let out = console().render_export(&tree);
424        let expected = concat!(
425            "root\n",
426            "├── child A\n",
427            "│   ├── leaf A1\n",
428            "│   └── leaf A2\n",
429            "└── child B\n",
430        );
431        assert_eq!(out, expected);
432    }
433
434    #[test]
435    fn deep_tree_renders_without_recursion() {
436        // Upstream renders from an explicit stack, so a 20 000-level chain is
437        // fine; a recursive render (or drop, or measure) overflows a normal
438        // 2 MiB thread stack long before that.
439        let handle = std::thread::Builder::new()
440            .stack_size(2 * 1024 * 1024)
441            .spawn(|| {
442                let mut tree = Tree::new("0");
443                let mut node = &mut tree;
444                for depth in 1..20_000 {
445                    node = node.add(depth.to_string());
446                }
447                let console = Console::builder().width(12).build();
448                let out = console.render_export(&tree);
449                let measured = Measurement::get(&console, &console.options(), &tree);
450                (out, measured)
451            })
452            .expect("spawn");
453        let (out, measured) = handle.join().expect("deep tree render overflowed");
454        // Labels render while the guides leave room (4 cells per level at
455        // width 12: depths 0-2); deeper nodes have no room and emit nothing.
456        assert_eq!(out, "0\n└── 1\n    └── 2\n");
457        // `Measurement.get` clamps the 80 001-cell measure to the width.
458        assert_eq!(measured, Measurement::new(12, 12));
459    }
460}