clap_builder/builder/
styled_str.rs

1#![cfg_attr(not(feature = "usage"), allow(dead_code))]
2use std::borrow::Cow;
3
4/// Terminal-styling container
5///
6/// Styling may be encoded as [ANSI Escape Code](https://en.wikipedia.org/wiki/ANSI_escape_code)
7///
8/// # Examples
9///
10/// ```rust
11/// # use clap_builder as clap;
12/// // `cstr!` converts tags to ANSI codes
13/// let after_help: &'static str = color_print::cstr!(
14/// r#"<bold><underline>Examples</underline></bold>
15///
16///   <dim>$</dim> <bold>mybin --input file.toml</bold>
17/// "#);
18///
19/// let cmd = clap::Command::new("mybin")
20///     .after_help(after_help)  // The `&str` gets converted into a `StyledStr`
21///     // ...
22/// #   ;
23/// ```
24#[derive(Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord)]
25pub struct StyledStr(String);
26
27impl StyledStr {
28    /// Create an empty buffer
29    pub const fn new() -> Self {
30        Self(String::new())
31    }
32
33    /// Display using [ANSI Escape Code](https://en.wikipedia.org/wiki/ANSI_escape_code) styling
34    #[cfg(feature = "color")]
35    pub fn ansi(&self) -> impl std::fmt::Display + '_ {
36        self.0.as_str()
37    }
38
39    /// May allow the compiler to consolidate the `Drop`s for `msg`, reducing code size compared to
40    /// `styled.push_str(&msg)`
41    pub(crate) fn push_string(&mut self, msg: String) {
42        self.0.push_str(&msg);
43    }
44
45    /// Appends a given string slice onto the end of this `StyledStr`.
46    pub fn push_str(&mut self, msg: &str) {
47        self.0.push_str(msg);
48    }
49
50    pub(crate) fn trim_start_lines(&mut self) {
51        if let Some(pos) = self.0.find('\n') {
52            let (leading, help) = self.0.split_at(pos + 1);
53            if leading.trim().is_empty() {
54                self.0 = help.to_owned();
55            }
56        }
57    }
58
59    pub(crate) fn trim_end(&mut self) {
60        self.0 = self.0.trim_end().to_owned();
61    }
62
63    #[cfg(feature = "help")]
64    pub(crate) fn replace_newline_var(&mut self) {
65        self.0 = self.0.replace("{n}", "\n");
66    }
67
68    #[cfg(feature = "help")]
69    pub(crate) fn indent(&mut self, initial: &str, trailing: &str) {
70        self.0.insert_str(0, initial);
71
72        let mut line_sep = "\n".to_owned();
73        line_sep.push_str(trailing);
74        self.0 = self.0.replace('\n', &line_sep);
75    }
76
77    #[cfg(all(not(feature = "wrap_help"), feature = "help"))]
78    pub(crate) fn wrap(&mut self, _hard_width: usize) {}
79
80    #[cfg(feature = "wrap_help")]
81    pub(crate) fn wrap(&mut self, hard_width: usize) {
82        let mut new = String::with_capacity(self.0.len());
83
84        let mut last = 0;
85        let mut wrapper = crate::output::textwrap::wrap_algorithms::LineWrapper::new(hard_width);
86        for content in self.iter_text() {
87            // Preserve styling
88            let current = content.as_ptr() as usize - self.0.as_str().as_ptr() as usize;
89            if last != current {
90                new.push_str(&self.0.as_str()[last..current]);
91            }
92            last = current + content.len();
93
94            for (i, line) in content.split_inclusive('\n').enumerate() {
95                if 0 < i {
96                    // reset char count on newline, skipping the start as we might have carried
97                    // over from a prior block of styled text
98                    wrapper.reset();
99                }
100                let line = crate::output::textwrap::word_separators::find_words_ascii_space(line)
101                    .collect::<Vec<_>>();
102                new.extend(wrapper.wrap(line));
103            }
104        }
105        if last != self.0.len() {
106            new.push_str(&self.0.as_str()[last..]);
107        }
108        new = new.trim_end().to_owned();
109
110        self.0 = new;
111    }
112
113    #[inline(never)]
114    #[cfg(feature = "help")]
115    pub(crate) fn display_width(&self) -> usize {
116        let mut width = 0;
117        for c in self.iter_text() {
118            width += crate::output::display_width(c);
119        }
120        width
121    }
122
123    #[cfg(feature = "help")]
124    pub(crate) fn is_empty(&self) -> bool {
125        self.0.is_empty()
126    }
127
128    #[cfg(feature = "help")]
129    pub(crate) fn as_styled_str(&self) -> &str {
130        &self.0
131    }
132
133    #[cfg(feature = "color")]
134    pub(crate) fn iter_text(&self) -> impl Iterator<Item = &str> {
135        anstream::adapter::strip_str(&self.0)
136    }
137
138    #[cfg(not(feature = "color"))]
139    pub(crate) fn iter_text(&self) -> impl Iterator<Item = &str> {
140        [self.0.as_str()].into_iter()
141    }
142
143    pub(crate) fn push_styled(&mut self, other: &Self) {
144        self.0.push_str(&other.0);
145    }
146
147    pub(crate) fn write_to(&self, buffer: &mut dyn std::io::Write) -> std::io::Result<()> {
148        ok!(buffer.write_all(self.0.as_bytes()));
149
150        Ok(())
151    }
152}
153
154impl Default for &'_ StyledStr {
155    fn default() -> Self {
156        static DEFAULT: StyledStr = StyledStr::new();
157        &DEFAULT
158    }
159}
160
161impl From<String> for StyledStr {
162    fn from(name: String) -> Self {
163        StyledStr(name)
164    }
165}
166
167impl From<&'_ String> for StyledStr {
168    fn from(name: &'_ String) -> Self {
169        let mut styled = StyledStr::new();
170        styled.push_str(name);
171        styled
172    }
173}
174
175impl From<&'static str> for StyledStr {
176    fn from(name: &'static str) -> Self {
177        let mut styled = StyledStr::new();
178        styled.push_str(name);
179        styled
180    }
181}
182
183impl From<&'_ &'static str> for StyledStr {
184    fn from(name: &'_ &'static str) -> Self {
185        StyledStr::from(*name)
186    }
187}
188
189impl From<Cow<'static, str>> for StyledStr {
190    fn from(cow: Cow<'static, str>) -> Self {
191        match cow {
192            Cow::Borrowed(s) => StyledStr::from(s),
193            Cow::Owned(s) => StyledStr::from(s),
194        }
195    }
196}
197
198impl std::fmt::Write for StyledStr {
199    #[inline]
200    fn write_str(&mut self, s: &str) -> Result<(), std::fmt::Error> {
201        self.0.push_str(s);
202        Ok(())
203    }
204
205    #[inline]
206    fn write_char(&mut self, c: char) -> Result<(), std::fmt::Error> {
207        self.0.push(c);
208        Ok(())
209    }
210}
211
212/// Color-unaware printing. Never uses coloring.
213impl std::fmt::Display for StyledStr {
214    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215        for part in self.iter_text() {
216            part.fmt(f)?;
217        }
218
219        Ok(())
220    }
221}
222
223#[cfg(test)]
224#[cfg(feature = "wrap_help")]
225mod wrap_tests {
226    use super::*;
227
228    use snapbox::assert_data_eq;
229    use snapbox::str;
230
231    #[test]
232    #[cfg(feature = "wrap_help")]
233    fn wrap_unstyled() {
234        let style = anstyle::Style::new();
235        let input = format!("{style}12345{style:#} {style}12345{style:#} {style}12345{style:#} {style}12345{style:#}");
236        let mut actual = StyledStr::new();
237        actual.push_string(input);
238        actual.wrap(20);
239        assert_data_eq!(
240            actual.ansi().to_string(),
241            str![[r#"
24212345 12345 12345
24312345
244"#]]
245        );
246    }
247
248    #[test]
249    #[cfg(feature = "wrap_help")]
250    fn wrap_styled() {
251        let style = anstyle::Style::new().bold();
252        let input = format!("{style}12345{style:#} {style}12345{style:#} {style}12345{style:#} {style}12345{style:#}");
253        let mut actual = StyledStr::new();
254        actual.push_string(input);
255        actual.wrap(20);
256        assert_data_eq!(
257            actual.ansi().to_string(),
258            str![[r#"
25912345 12345 12345 
26012345
261"#]]
262        );
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269
270    #[test]
271    fn from_cow_borrowed() {
272        let cow = Cow::Borrowed("hello");
273        let styled = StyledStr::from(cow);
274        assert_eq!(styled, StyledStr::from("hello"));
275    }
276
277    #[test]
278    fn from_cow_owned() {
279        let cow = Cow::Owned("world".to_string());
280        let styled = StyledStr::from(cow);
281        assert_eq!(styled, StyledStr::from("world"));
282    }
283}