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}