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: &&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) => 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}
277