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}