owo_colors/
styled_list.rs

1use crate::{dyn_styles::StyleFlags, Style, Styled};
2use core::{
3    fmt::{self, Display},
4    marker::PhantomData,
5};
6
7#[cfg(feature = "alloc")]
8extern crate alloc;
9
10// Hidden trait for use in `StyledList` bounds
11mod sealed {
12    pub trait IsStyled {
13        type Inner: core::fmt::Display;
14
15        fn style(&self) -> &crate::Style;
16        fn inner(&self) -> &Self::Inner;
17    }
18}
19
20use sealed::IsStyled;
21
22impl<T: IsStyled> IsStyled for &T {
23    type Inner = T::Inner;
24
25    fn style(&self) -> &Style {
26        <T as IsStyled>::style(*self)
27    }
28
29    fn inner(&self) -> &Self::Inner {
30        <T as IsStyled>::inner(*self)
31    }
32}
33
34impl<T: Display> IsStyled for Styled<T> {
35    type Inner = T;
36
37    fn style(&self) -> &Style {
38        &self.style
39    }
40
41    fn inner(&self) -> &T {
42        &self.target
43    }
44}
45
46/// A collection of [`Styled`] items that are displayed in such a way as to minimize the amount of characters
47/// that are written when displayed.
48///
49/// ```rust
50/// use owo_colors::{Style, Styled, StyledList};
51///
52/// let styled_items = [
53///     Style::new().red().style("Hello "),
54///     Style::new().green().style("World"),
55///  ];
56///
57/// // 29 characters
58/// let normal_length = styled_items.iter().map(|item| format!("{}", item).len()).sum::<usize>();
59/// // 25 characters
60/// let styled_length = format!("{}", StyledList::from(styled_items)).len();
61///
62/// assert!(styled_length < normal_length);
63/// ```
64pub struct StyledList<T, U>(pub T, PhantomData<fn(U)>)
65where
66    T: AsRef<[U]>,
67    U: IsStyled;
68
69impl<T, U> From<T> for StyledList<T, U>
70where
71    T: AsRef<[U]>,
72    U: IsStyled,
73{
74    fn from(list: T) -> Self {
75        Self(list, PhantomData)
76    }
77}
78
79impl<T, U> Display for StyledList<T, U>
80where
81    T: AsRef<[U]>,
82    U: IsStyled,
83{
84    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85        // Handle first item manually
86        let first_item = match self.0.as_ref().first() {
87            Some(s) => s,
88            None => return Ok(()),
89        };
90
91        first_item.style().fmt_prefix(f)?;
92        write!(f, "{}", first_item.inner())?;
93
94        // Handle the rest
95        for window in self.0.as_ref().windows(2) {
96            let prev = &window[0];
97            let current = &window[1];
98
99            write!(
100                f,
101                "{}{}",
102                current.style().transition_from(prev.style()),
103                current.inner()
104            )?;
105        }
106
107        // Print final reset
108        // SAFETY: We know that the first item exists, thus a last item exists
109        self.0.as_ref().last().unwrap().style().fmt_suffix(f)
110    }
111}
112
113impl<'a> Style {
114    /// Retuns an enum that indicates how the transition from one style to this style should be printed
115    fn transition_from(&'a self, from: &Style) -> Transition<'a> {
116        if self == from {
117            return Transition::Noop;
118        }
119
120        // Use full reset if transitioning from colored to non-colored
121        // or if previous style contains properties that are not in this style
122        if (from.fg.is_some() && self.fg.is_none())
123            || (from.bg.is_some() && self.bg.is_none())
124            || (from.bold && !self.bold)
125            || (!self.style_flags.0 & from.style_flags.0) != 0
126        {
127            return Transition::FullReset(self);
128        }
129
130        // Build up a transition style, that does not require a full reset
131        // Contains all properties from `self` that are not in `from`
132        let fg = match (self.fg, from.fg) {
133            (Some(fg), Some(from_fg)) if fg != from_fg => Some(fg),
134            (Some(fg), None) => Some(fg),
135            _ => None,
136        };
137
138        let bg = match (self.bg, from.bg) {
139            (Some(bg), Some(from_bg)) if bg != from_bg => Some(bg),
140            (Some(bg), None) => Some(bg),
141            _ => None,
142        };
143
144        let new_style = Style {
145            fg,
146            bg,
147            bold: from.bold ^ self.bold,
148            style_flags: StyleFlags(self.style_flags.0 ^ from.style_flags.0),
149        };
150
151        Transition::Style(new_style)
152    }
153}
154
155/// How the transition between two styles should be printed
156#[cfg_attr(test, derive(Debug, PartialEq))]
157enum Transition<'a> {
158    Noop,
159    FullReset(&'a Style),
160    Style(Style),
161}
162
163impl fmt::Display for Transition<'_> {
164    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
165        match self {
166            // Styles are equal
167            Transition::Noop => Ok(()),
168            // Reset the style & print full prefix
169            Transition::FullReset(style) => {
170                write!(f, "\x1B[0m")?;
171                style.fmt_prefix(f)
172            }
173            // Print transition style without resetting the style
174            Transition::Style(style) => style.fmt_prefix(f),
175        }
176    }
177}
178
179/// A helper alias for [`StyledList`] for easier usage with [`alloc::vec::Vec`].
180#[cfg(feature = "alloc")]
181pub type StyledVec<T> = StyledList<alloc::vec::Vec<Styled<T>>, Styled<T>>;
182
183#[cfg(test)]
184mod test {
185    use super::*;
186
187    #[test]
188    fn test_styled_list() {
189        let list = &[
190            Style::new().red().style("red"),
191            Style::new().green().italic().style("green italic"),
192            Style::new().red().bold().style("red bold"),
193        ];
194
195        let list = StyledList::from(list);
196
197        assert_eq!(
198            format!("{}", list),
199            "\x1b[31mred\x1b[32;3mgreen italic\x1b[0m\x1b[31;1mred bold\x1b[0m"
200        );
201    }
202
203    #[test]
204    fn test_styled_final_plain() {
205        let list = &[
206            Style::new().red().style("red"),
207            Style::new().green().italic().style("green italic"),
208            Style::new().style("plain"),
209        ];
210
211        let list = StyledList::from(list);
212
213        assert_eq!(
214            format!("{}", list),
215            "\x1b[31mred\x1b[32;3mgreen italic\x1b[0mplain"
216        );
217    }
218
219    #[test]
220    fn test_transition_from_noop() {
221        let style_current = Style::new().italic().red();
222        let style_prev = Style::new().italic().red();
223
224        assert_eq!(style_current.transition_from(&style_prev), Transition::Noop);
225    }
226
227    #[test]
228    fn test_transition_from_full_reset() {
229        let style_current = Style::new().italic().red();
230        let style_prev = Style::new().italic().dimmed().red();
231
232        assert_eq!(
233            style_current.transition_from(&style_prev),
234            Transition::FullReset(&style_current)
235        );
236
237        let style_current = Style::new();
238        let style_prev = Style::new().red();
239        assert_eq!(
240            style_current.transition_from(&style_prev),
241            Transition::FullReset(&style_current)
242        );
243
244        let style_current = Style::new();
245        let style_prev = Style::new().bold();
246        assert_eq!(
247            style_current.transition_from(&style_prev),
248            Transition::FullReset(&style_current)
249        );
250    }
251
252    #[test]
253    fn test_transition_from_style() {
254        let style_current = Style::new().italic().dimmed().red();
255        let style_prev = Style::new().italic().red();
256
257        assert_eq!(
258            style_current.transition_from(&style_prev),
259            Transition::Style(Style::new().dimmed())
260        );
261
262        let style_current = Style::new().red().on_green();
263        let style_prev = Style::new().red().on_bright_cyan();
264        assert_eq!(
265            style_current.transition_from(&style_prev),
266            Transition::Style(Style::new().on_green())
267        );
268
269        let style_current = Style::new().bold().blue();
270        let style_prev = Style::new().bold();
271        assert_eq!(
272            style_current.transition_from(&style_prev),
273            Transition::Style(Style::new().blue())
274        );
275    }
276}