display_tree/
display.rs

1use std::fmt;
2
3use super::DisplayTree;
4
5/// A helper struct for formatting a type that implements [`DisplayTree`].
6///
7/// [`AsTree`] stores a reference to the type to format. It implements
8/// [`std::fmt::Display`], so it can be used in [`println!`], [`format!`],
9/// etc...
10///
11/// # Styling
12///
13/// [`AsTree`] controls the way a tree is styled when it is formatted. The style
14/// can be customized using builder methods. See [`Style`] for the different
15/// aspects that can be customized.
16///
17/// **Note:** [`StyleBuilder`] must be in scope to use the builder methods.
18///
19/// **Note:** Some styling options use ANSI escape codes and therefore will only
20/// work where they are supported. See [`TextStyle`] for more information.
21///
22/// # Examples
23///
24/// ```
25/// use display_tree::{AsTree, DisplayTree};
26///
27/// #[derive(DisplayTree)]
28/// struct Tree {
29///     a: i32,
30///     b: i32,
31/// }
32///
33/// let tree = Tree { a: 1, b: 2 };
34///
35/// assert_eq!(
36///     format!("{}", AsTree::new(&tree)),
37///     "Tree\n\
38///      ├── 1\n\
39///      └── 2"
40/// );
41/// ```
42///
43/// Specifying a style:
44///
45/// ```
46/// use display_tree::{AsTree, CharSet, DisplayTree, StyleBuilder};
47///
48/// #[derive(DisplayTree)]
49/// struct Tree {
50///     a: i32,
51///     b: i32,
52/// }
53///
54/// let tree = Tree { a: 1, b: 2 };
55///
56/// assert_eq!(
57///     format!("{}", AsTree::new(&tree).char_set(CharSet::DOUBLE_LINE)),
58///     "Tree\n\
59///      ╠══ 1\n\
60///      ╚══ 2"
61/// );
62/// ```
63pub struct AsTree<'a, T: DisplayTree> {
64    tree: &'a T,
65    style: Style,
66}
67
68impl<'a, T: DisplayTree> AsTree<'a, T> {
69    /// Creates a wrapper around a type that implements [`DisplayTree`],
70    /// allowing it to be formatted.
71    ///
72    /// # Examples
73    ///
74    /// ```
75    /// use display_tree::{AsTree, DisplayTree};
76    ///
77    /// #[derive(DisplayTree)]
78    /// struct Tree;
79    ///
80    /// let as_tree = AsTree::new(&Tree);
81    /// ```
82    pub fn new(tree: &'a T) -> Self {
83        Self {
84            tree,
85            style: Style::default(),
86        }
87    }
88
89    /// Creates a wrapper around a type that implements [`DisplayTree`],
90    /// allowing it to be formatted with the given style.
91    ///
92    /// # Examples
93    ///
94    /// ```
95    /// use display_tree::{AsTree, DisplayTree, Style};
96    ///
97    /// #[derive(DisplayTree)]
98    /// struct Tree;
99    ///
100    /// let as_styled_tree = AsTree::with_style(&Tree, Style::default());
101    /// ```
102    pub fn with_style(tree: &'a T, style: Style) -> Self {
103        Self { tree, style }
104    }
105}
106
107impl<'a, T: DisplayTree> StyleBuilder for AsTree<'a, T> {
108    fn style_mut(&mut self) -> &mut Style {
109        &mut self.style
110    }
111}
112
113impl<'a, T: DisplayTree> fmt::Display for AsTree<'a, T> {
114    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115        self.tree.fmt(f, self.style)
116    }
117}
118
119/// A type that describes the way a type that implements [`DisplayTree`] should
120/// be formatted.
121///
122/// Prefer using builder methods, either on [`Style`] or [`AsTree`], over
123/// constructing an instance of [`Style`] manually.
124///
125/// **Note:** [`StyleBuilder`] must be in scope to use builder methods.
126///
127/// # Examples
128///
129/// ```
130/// use display_tree::{CharSet, Color, Style, StyleBuilder};
131///
132/// let style = Style::default()
133///     .leaf_color(Color::Blue)
134///     .branch_background_color(Color::Red)
135///     .indentation(4)
136///     .char_set(CharSet::SINGLE_LINE_CURVED);
137/// ```
138#[derive(Clone, Copy)]
139pub struct Style {
140    /// The [`CharSet`] making up the branches of the tree.
141    pub char_set: CharSet,
142    /// The indentation of each node.
143    pub indentation: u32,
144    /// The style of the leaves of the tree. See [`TextStyle`] for more
145    /// information.
146    pub leaf_style: TextStyle,
147    /// The style of the branches of the tree. See [`TextStyle`] for more
148    /// information.
149    pub branch_style: TextStyle,
150}
151
152impl StyleBuilder for Style {
153    fn style_mut(&mut self) -> &mut Style {
154        self
155    }
156}
157
158impl Default for Style {
159    /// The default [`Style`] used if none is specified.
160    ///
161    /// Default values:
162    /// - [`char_set`](Style::char_set): [`CharSet::SINGLE_LINE`]
163    /// - [`indentation`](Style::indentation): `2`
164    /// - [`leaf_style`](Style::leaf_style): [`TextStyle::default()`]
165    /// - [`branch_style`](Style::branch_style): [`TextStyle::default()`]
166    fn default() -> Self {
167        Self {
168            char_set: CharSet::SINGLE_LINE,
169            indentation: 2,
170            leaf_style: TextStyle::default(),
171            branch_style: TextStyle::default(),
172        }
173    }
174}
175
176/// A type that described how text will be rendered.
177///
178/// **Note:** [`TextStyle`] uses ANSI escape codes, so it should not be used
179/// anywhere they are not supported. Support for individual fields may also vary
180/// by terminal.
181#[derive(Clone, Copy, Default)]
182pub struct TextStyle {
183    /// The text color. [`None`] will not apply any color
184    pub text_color: Option<Color>,
185    /// The background color. [`None`] will not apply any background color.
186    pub background_color: Option<Color>,
187    /// Whether the text is bold. (Might be rendered as increased intensity.)
188    pub is_bold: bool,
189    /// Whether the text has decreased intensity.
190    pub is_faint: bool,
191    /// Whether the text is italicised.
192    pub is_italic: bool,
193    /// Whether the text is underlined.
194    pub is_underlined: bool,
195    /// Whether the text is crossed-out.
196    pub is_strikethrough: bool,
197}
198
199impl TextStyle {
200    /// Applies the style to a string.
201    ///
202    /// [`apply()`](TextStyle::apply()) should not be called unless you are
203    /// manually implementing [`DisplayTree`]. It is used in derived
204    /// [`DisplayTree`] implementations.
205    pub fn apply(&self, string: &str) -> String {
206        use std::borrow::Cow;
207
208        let mut ansi_codes: Vec<Cow<str>> = Vec::new();
209
210        if let Some(text_color) = self.text_color {
211            ansi_codes.push(match text_color {
212                Color::Black => "30".into(),
213                Color::Red => "31".into(),
214                Color::Green => "32".into(),
215                Color::Yellow => "33".into(),
216                Color::Blue => "34".into(),
217                Color::Magenta => "35".into(),
218                Color::Cyan => "36".into(),
219                Color::White => "37".into(),
220                Color::Rgb(r, g, b) => format!("38;2;{r};{g};{b}").into(),
221            })
222        }
223
224        if let Some(background_color) = self.background_color {
225            ansi_codes.push(match background_color {
226                Color::Black => "40".into(),
227                Color::Red => "41".into(),
228                Color::Green => "42".into(),
229                Color::Yellow => "43".into(),
230                Color::Blue => "44".into(),
231                Color::Magenta => "45".into(),
232                Color::Cyan => "46".into(),
233                Color::White => "47".into(),
234                Color::Rgb(r, g, b) => format!("48;2;{r};{g};{b}").into(),
235            })
236        }
237
238        if self.is_bold {
239            ansi_codes.push("1".into())
240        }
241
242        if self.is_faint {
243            ansi_codes.push("2".into())
244        }
245
246        if self.is_italic {
247            ansi_codes.push("3".into())
248        }
249
250        if self.is_underlined {
251            ansi_codes.push("4".into())
252        }
253
254        if self.is_strikethrough {
255            ansi_codes.push("9".into())
256        }
257
258        if !ansi_codes.is_empty() {
259            let escape_sequences = ansi_codes
260                .into_iter()
261                .map(|code| format!("\x1b[{code}m"))
262                .collect::<String>();
263            format!("{escape_sequences}{string}\x1b[0m")
264        } else {
265            string.to_owned()
266        }
267    }
268}
269
270/// An ANSI color that a tree can be styled with.
271#[derive(Clone, Copy)]
272pub enum Color {
273    /// ANSI color #0. Exact color depends on terminal.
274    Black,
275    /// ANSI color #1. Exact color depends on terminal.
276    Red,
277    /// ANSI color #2. Exact color depends on terminal.
278    Green,
279    /// ANSI color #3. Exact color depends on terminal.
280    Yellow,
281    /// ANSI color #4. Exact color depends on terminal.
282    Blue,
283    /// ANSI color #5. Exact color depends on terminal.
284    Magenta,
285    /// ANSI color #6. Exact color depends on terminal.
286    Cyan,
287    /// ANSI color #7. Exact color depends on terminal.
288    White,
289    /// A color with custom RGB values.
290    ///
291    /// **Note:** Truecolor support is required for this variant. [`Color::Rgb`]
292    /// will not work properly if Truecolor is not supported in your terminal.
293    /// In some cases it may be rendered as an 8-bit color if your terminal
294    /// supports 256 colors.
295    Rgb(u8, u8, u8),
296}
297
298/// A set of [`char`]s used for formatting a type that implements
299/// [`DisplayTree`].
300///
301/// These are the characters that make up the text that connects the nodes of
302/// the tree.
303///
304/// [`CharSet`] provides a few built-in sets via associated constants, but you
305/// can construct your own if needed.
306///
307/// # Examples
308///
309/// ```
310/// let char_set = display_tree::CharSet {
311///     horizontal: '─',
312///     vertical: '│',
313///     connector: '├',
314///     end_connector: '└',
315/// };
316/// ```
317#[derive(Clone, Copy)]
318pub struct CharSet {
319    /// The characters used in the horizontal portion of a branch.
320    ///
321    /// Should resemble a plain horizontal line, eg. '─'.
322    pub horizontal: char,
323    /// The character used in the space between branches in place of
324    /// [`connector`](CharSet::connector).
325    ///
326    /// Should resemble a plain vertical line, eg. '│'.
327    pub vertical: char,
328    /// The character connecting the vertical and horizontal portions of a
329    /// branch.
330    ///
331    /// Should resemble a vertical line with an offshoot on the right, eg. '├'.
332    pub connector: char,
333    /// The character connecting the vertical and horizontal portions of the
334    /// last branch under a node.
335    ///
336    /// Should resemble an "L" shape, eg. '└'.
337    pub end_connector: char,
338}
339
340impl CharSet {
341    /// Regular Unicode box-drawing characters.
342    pub const SINGLE_LINE: Self = Self {
343        horizontal: '─',
344        vertical: '│',
345        connector: '├',
346        end_connector: '└',
347    };
348
349    /// Bold Unicode box-drawing characters.
350    pub const SINGLE_LINE_BOLD: Self = Self {
351        horizontal: '━',
352        vertical: '┃',
353        connector: '┣',
354        end_connector: '┗',
355    };
356
357    /// Curved Unicode box-drawing characters.
358    pub const SINGLE_LINE_CURVED: Self = Self {
359        horizontal: '─',
360        vertical: '│',
361        connector: '├',
362        end_connector: '╰',
363    };
364
365    /// Double Unicode box-drawing characters.
366    pub const DOUBLE_LINE: Self = Self {
367        horizontal: '═',
368        vertical: '║',
369        connector: '╠',
370        end_connector: '╚',
371    };
372
373    /// ASCII characters.
374    pub const ASCII: Self = Self {
375        horizontal: '-',
376        vertical: '|',
377        connector: '|',
378        end_connector: '`',
379    };
380}
381
382/// A trait that provides builder methods for constructing an instance of
383/// [`Style`].
384///
385/// [`StyleBuilder`] is implemented for [`Style`] and [`AsTree`], so you can use
386/// those types to construct an instance of [`Style`].
387///
388/// Do not implement [`StyleBuilder`] for any new types.
389pub trait StyleBuilder: Sized {
390    #[doc(hidden)]
391    fn style_mut(&mut self) -> &mut Style;
392
393    /// Sets the [`CharSet`] making up the branches of the tree.
394    ///
395    /// See [`CharSet`] for more information.
396    ///
397    /// # Examples
398    ///
399    /// ```
400    /// use display_tree::{AsTree, CharSet, DisplayTree, StyleBuilder};
401    ///
402    /// #[derive(DisplayTree)]
403    /// struct Tree {
404    ///     a: i32,
405    ///     b: i32,
406    /// }
407    ///
408    /// let tree = Tree { a: 1, b: 2 };
409    ///
410    /// assert_eq!(
411    ///     format!(
412    ///         "{}",
413    ///         // Use ASCII characters instead of the default Unicode ones.
414    ///         AsTree::new(&tree).char_set(CharSet::ASCII),
415    ///     ),
416    ///     "Tree\n\
417    ///      |-- 1\n\
418    ///      `-- 2",
419    /// );
420    /// ```
421    fn char_set(mut self, char_set: CharSet) -> Self {
422        self.style_mut().char_set = char_set;
423        self
424    }
425
426    /// Sets the indentation of each node.
427    ///
428    /// More specifically, [`indentation()`](AsTree::indentation()) sets the
429    /// number of horizontal characters to use for each branch of the tree.
430    ///
431    /// # Examples
432    ///
433    /// ```
434    /// use display_tree::{AsTree, DisplayTree, StyleBuilder};
435    ///
436    /// #[derive(DisplayTree)]
437    /// struct Tree {
438    ///     a: i32,
439    ///     b: i32,
440    /// }
441    ///
442    /// let tree = Tree { a: 1, b: 2 };
443    ///
444    /// assert_eq!(
445    ///     format!("{}", AsTree::new(&tree).indentation(4),),
446    ///     "Tree\n\
447    ///      ├──── 1\n\
448    ///      └──── 2"
449    /// );
450    /// ```
451    fn indentation(mut self, indentation: u32) -> Self {
452        self.style_mut().indentation = indentation;
453        self
454    }
455
456    /// Sets the style of the leaves of the tree. See [`TextStyle`] for more
457    /// information.
458    fn leaf_style(mut self, style: TextStyle) -> Self {
459        self.style_mut().leaf_style = style;
460        self
461    }
462
463    /// Sets the style of the branches of the tre. See [`TextStyle`] for more
464    /// information.
465    fn branch_style(mut self, style: TextStyle) -> Self {
466        self.style_mut().branch_style = style;
467        self
468    }
469
470    /// Sets the color of the leaves of the tree. See [`Color`] for more
471    /// information.
472    fn leaf_color(mut self, color: Color) -> Self {
473        self.style_mut().leaf_style.text_color = Some(color);
474        self
475    }
476
477    /// Sets the background color of the leaves of the tree. See [`Color`] for
478    /// more information.
479    fn leaf_background_color(mut self, color: Color) -> Self {
480        self.style_mut().leaf_style.background_color = Some(color);
481        self
482    }
483
484    /// Renders the leaves as bold.
485    fn bold_leaves(mut self) -> Self {
486        self.style_mut().leaf_style.is_bold = true;
487        self
488    }
489
490    /// Decreases the intensity of the leaves.
491    fn faint_leaves(mut self) -> Self {
492        self.style_mut().leaf_style.is_faint = true;
493        self
494    }
495
496    /// Italicises the leaves.
497    fn italic_leaves(mut self) -> Self {
498        self.style_mut().leaf_style.is_italic = true;
499        self
500    }
501
502    /// Underlines the leaves.
503    fn underlined_leaves(mut self) -> Self {
504        self.style_mut().leaf_style.is_underlined = true;
505        self
506    }
507
508    /// Causes the leaves to be crossed-out.
509    fn strikethrough_leaves(mut self) -> Self {
510        self.style_mut().leaf_style.is_strikethrough = true;
511        self
512    }
513
514    /// Sets the color of the branches of the tree. See [`Color`] for more
515    /// information.
516    fn branch_color(mut self, color: Color) -> Self {
517        self.style_mut().branch_style.text_color = Some(color);
518        self
519    }
520
521    /// Sets the background color of the branches of the tree. See [`Color`] for
522    /// more information.
523    fn branch_background_color(mut self, color: Color) -> Self {
524        self.style_mut().branch_style.background_color = Some(color);
525        self
526    }
527
528    /// Renders the branches as bold.
529    fn bold_branches(mut self) -> Self {
530        self.style_mut().branch_style.is_bold = true;
531        self
532    }
533
534    /// Decreases the intensity of the branches.
535    fn faint_branches(mut self) -> Self {
536        self.style_mut().branch_style.is_faint = true;
537        self
538    }
539}
540
541#[cfg(test)]
542mod tests {
543    use super::*;
544
545    #[test]
546    fn plain() {
547        let style = TextStyle::default();
548        assert_eq!(style.apply("text"), "text")
549    }
550
551    #[test]
552    fn text_color() {
553        let style = TextStyle {
554            text_color: Some(Color::Red),
555            ..TextStyle::default()
556        };
557        assert_eq!(style.apply("text"), "\x1b[31mtext\x1b[0m")
558    }
559
560    #[test]
561    fn background_color() {
562        let style = TextStyle {
563            background_color: Some(Color::Red),
564            ..TextStyle::default()
565        };
566        assert_eq!(style.apply("text"), "\x1b[41mtext\x1b[0m")
567    }
568
569    #[test]
570    fn bold() {
571        let style = TextStyle {
572            is_bold: true,
573            ..TextStyle::default()
574        };
575        assert_eq!(style.apply("text"), "\x1b[1mtext\x1b[0m")
576    }
577
578    #[test]
579    fn faint() {
580        let style = TextStyle {
581            is_faint: true,
582            ..TextStyle::default()
583        };
584        assert_eq!(style.apply("text"), "\x1b[2mtext\x1b[0m")
585    }
586
587    #[test]
588    fn italic() {
589        let style = TextStyle {
590            is_italic: true,
591            ..TextStyle::default()
592        };
593        assert_eq!(style.apply("text"), "\x1b[3mtext\x1b[0m")
594    }
595
596    #[test]
597    fn underline() {
598        let style = TextStyle {
599            is_underlined: true,
600            ..TextStyle::default()
601        };
602        assert_eq!(style.apply("text"), "\x1b[4mtext\x1b[0m")
603    }
604
605    #[test]
606    fn strikethrough() {
607        let style = TextStyle {
608            is_strikethrough: true,
609            ..TextStyle::default()
610        };
611        assert_eq!(style.apply("text"), "\x1b[9mtext\x1b[0m")
612    }
613}