tracing_human_layer/
textwrap.rs

1//! Extensions and utilities for the [`textwrap`] crate.
2
3use std::borrow::Cow;
4
5use textwrap::LineEnding;
6use textwrap::Options;
7use textwrap::WordSeparator;
8use textwrap::WordSplitter;
9use textwrap::WrapAlgorithm;
10
11#[cfg(doc)]
12use crate::HumanLayer;
13#[cfg(doc)]
14use crate::ProvideStyle;
15
16/// The width to wrap text at.
17#[derive(Debug, Clone, Copy)]
18enum TextWrapWidth {
19    /// Wrap text at the width of the terminal, or 80 columns by default.
20    TerminalWidth,
21    /// Wrap text at a given fixed width.
22    Fixed(usize),
23}
24
25/// Options for wrapping and filling text. Like [`textwrap::Options`], but owned.
26///
27/// We want to vary the [`textwrap::Options::initial_indent`] and
28/// [`textwrap::Options::subsequent_indent`] depending on the log level, so those fields are
29/// set in a [`HumanLayer`]'s [`ProvideStyle`] implementation instead.
30#[derive(Debug, Clone)]
31pub struct TextWrapOptionsOwned {
32    /// The width in columns at which the text will be wrapped.
33    width: TextWrapWidth,
34    /// Line ending used for breaking lines.
35    line_ending: LineEnding,
36    /// Allow long words to be broken if they cannot fit on a line.
37    /// When set to `false`, some lines may be longer than
38    /// `self.width`. See the [`Options::break_words`] method.
39    break_words: bool,
40    /// Wrapping algorithm to use.
41    wrap_algorithm: WrapAlgorithm,
42    /// The line breaking algorithm to use.
43    word_separator: WordSeparator,
44    /// The method for splitting words. This can be used to prohibit
45    /// splitting words on hyphens, or it can be used to implement
46    /// language-aware machine hyphenation.
47    word_splitter: WordSplitter,
48}
49
50impl TextWrapOptionsOwned {
51    /// Construct a new [`TextWrapOptionsOwned`]. This differs from [`textwrap::Options::new`]
52    /// in the following ways:
53    ///
54    /// - The `width` defaults to the terminal's width (except in tests, where the width is
55    ///   always 80 columns).
56    /// - The `word_separator` is set to [`WordSeparator::AsciiSpace`].
57    /// - The `word_splitter` is set to [`WordSplitter::NoHyphenation`].
58    pub fn new() -> Self {
59        Self {
60            // In tests, the terminal is always 80 characters wide.
61            width: if cfg!(test) {
62                TextWrapWidth::Fixed(80)
63            } else {
64                TextWrapWidth::TerminalWidth
65            },
66            line_ending: LineEnding::LF,
67            break_words: false,
68            wrap_algorithm: WrapAlgorithm::new(),
69            word_separator: WordSeparator::AsciiSpace,
70            word_splitter: WordSplitter::NoHyphenation,
71        }
72    }
73
74    /// Use a given fixed width. This corresponds to [`textwrap::Options::new`].
75    pub fn with_width(self, width: usize) -> Self {
76        Self {
77            width: TextWrapWidth::Fixed(width),
78            ..self
79        }
80    }
81
82    /// Use the width of the terminal, or 80 columns by default. This corresponds to
83    /// [`textwrap::Options::with_termwidth`]. Note that the terminal width is queried lazily,
84    /// as `tracing` records are formatted.
85    pub fn with_termwidth(self) -> Self {
86        Self {
87            width: TextWrapWidth::TerminalWidth,
88            ..self
89        }
90    }
91
92    /// Corresponds to [`textwrap::Options::line_ending`].
93    pub fn with_line_ending(self, line_ending: LineEnding) -> Self {
94        Self {
95            line_ending,
96            ..self
97        }
98    }
99
100    /// Corresponds to [`textwrap::Options::break_words`].
101    pub fn with_break_words(self, break_words: bool) -> Self {
102        Self {
103            break_words,
104            ..self
105        }
106    }
107
108    /// Corresponds to [`textwrap::Options::wrap_algorithm`].
109    pub fn with_wrap_algorithm(self, wrap_algorithm: WrapAlgorithm) -> Self {
110        Self {
111            wrap_algorithm,
112            ..self
113        }
114    }
115
116    /// Corresponds to [`textwrap::Options::word_separator`].
117    pub fn with_word_separator(self, word_separator: WordSeparator) -> Self {
118        Self {
119            word_separator,
120            ..self
121        }
122    }
123
124    /// Corresponds to [`textwrap::Options::word_splitter`].
125    pub fn with_word_splitter(self, word_splitter: WordSplitter) -> Self {
126        Self {
127            word_splitter,
128            ..self
129        }
130    }
131}
132
133impl Default for TextWrapOptionsOwned {
134    fn default() -> Self {
135        Self::new()
136    }
137}
138
139/// Note that this leaves the [`textwrap::Options::initial_indent`] and
140/// [`textwrap::Options::subsequent_indent`] fields empty.
141impl<'a> From<&'_ TextWrapOptionsOwned> for Options<'a> {
142    fn from(opts: &'_ TextWrapOptionsOwned) -> Self {
143        match opts.width {
144            TextWrapWidth::TerminalWidth => Options::with_termwidth(),
145            TextWrapWidth::Fixed(width) => Options::new(width),
146        }
147        .line_ending(opts.line_ending)
148        .break_words(opts.break_words)
149        .wrap_algorithm(opts.wrap_algorithm)
150        .word_separator(opts.word_separator)
151        .word_splitter(opts.word_splitter.clone())
152    }
153}
154
155/// Extension trait adding methods to [`textwrap::Options`]
156pub(crate) trait TextWrapOptionsExt {
157    /// Wrap the given text into lines.
158    fn wrap<'s>(&self, text: &'s str) -> Vec<Cow<'s, str>>;
159}
160
161impl<'a> TextWrapOptionsExt for Options<'a> {
162    fn wrap<'s>(&self, text: &'s str) -> Vec<Cow<'s, str>> {
163        textwrap::wrap(text, self)
164    }
165}
166
167/// A trivial implementation which does nothing when the [`Option`] is [`None`].
168impl<'a> TextWrapOptionsExt for Option<Options<'a>> {
169    fn wrap<'s>(&self, text: &'s str) -> Vec<Cow<'s, str>> {
170        match self {
171            Some(options) => textwrap::wrap(text, options),
172            None => {
173                vec![Cow::Borrowed(text)]
174            }
175        }
176    }
177}