parse_style/
style.rs

1use super::attributes::{Attribute, AttributeSet};
2use super::color::Color;
3use std::fmt;
4use thiserror::Error;
5
6/// A terminal text style
7///
8/// `Style` stores two sets of [`Attribute`]s: those attributes that the style
9/// enables, plus those that the style explicitly disables/turns off.  The
10/// latter are relevant if you're applying a style in the middle of text with a
11/// different style; e.g., if a text styled with `"red bold"` contains a
12/// substring styled with `"blue not bold"`, you'd want to know that the bold
13/// effect should be disabled for that substring.
14#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
15pub struct Style {
16    /// The foreground color
17    foreground: Option<Color>,
18
19    /// The background color
20    background: Option<Color>,
21
22    /// Active/enabled attributes
23    enabled_attributes: AttributeSet,
24
25    /// Explicitly disabled attributes
26    ///
27    /// The individual `Style` methods must ensure that no attribute is ever in
28    /// both `enabled_attributes` and `disabled_attributes` at once.
29    disabled_attributes: AttributeSet,
30}
31
32impl Style {
33    /// Create a new, empty style
34    pub const fn new() -> Style {
35        Style {
36            foreground: None,
37            background: None,
38            enabled_attributes: AttributeSet::EMPTY,
39            disabled_attributes: AttributeSet::EMPTY,
40        }
41    }
42
43    /// Set or clear the foreground color.
44    ///
45    /// Note that setting the foreground to `None` is different from setting it
46    /// to [`Color::Default`]: if the style is applied in the middle of text
47    /// with a foreground color set, `None` will leave the foreground color
48    /// as-is while `Color::Default` will reset it.
49    pub const fn foreground(mut self, fg: Option<Color>) -> Style {
50        self.foreground = fg;
51        self
52    }
53
54    /// Set or clear the background color
55    ///
56    /// Note that setting the background to `None` is different from setting it
57    /// to [`Color::Default`]: if the style is applied in the middle of text
58    /// with a background color set, `None` will leave the background color
59    /// as-is while `Color::Default` will reset it.
60    pub const fn background(mut self, bg: Option<Color>) -> Style {
61        self.background = bg;
62        self
63    }
64
65    /// Set the enabled attributes
66    pub fn enabled_attributes<A: Into<AttributeSet>>(mut self, attrs: A) -> Style {
67        self.enabled_attributes = attrs.into();
68        self.disabled_attributes -= self.enabled_attributes;
69        self
70    }
71
72    /// Set the disabled attributes
73    pub fn disabled_attributes<A: Into<AttributeSet>>(mut self, attrs: A) -> Style {
74        self.disabled_attributes = attrs.into();
75        self.enabled_attributes -= self.disabled_attributes;
76        self
77    }
78
79    /// `true` if the style does not set any colors or attributes
80    pub fn is_empty(self) -> bool {
81        self.foreground.is_none()
82            && self.background.is_none()
83            && self.enabled_attributes.is_empty()
84            && self.disabled_attributes.is_empty()
85    }
86
87    /// `true` if `attr` is enabled
88    pub fn is_enabled(self, attr: Attribute) -> bool {
89        self.enabled_attributes.contains(attr) && !self.disabled_attributes.contains(attr)
90    }
91
92    /// `true` if `attr` is explicitly disabled
93    pub fn is_disabled(self, attr: Attribute) -> bool {
94        self.disabled_attributes.contains(attr) && !self.enabled_attributes.contains(attr)
95    }
96
97    /// Return the foreground color
98    pub const fn get_foreground(self) -> Option<Color> {
99        self.foreground
100    }
101
102    /// Return the background color
103    pub const fn get_background(self) -> Option<Color> {
104        self.background
105    }
106
107    /// Return the enabled attributes
108    pub const fn get_enabled_attributes(self) -> AttributeSet {
109        self.enabled_attributes
110    }
111
112    /// Return the disabled attributes
113    pub const fn get_disabled_attributes(self) -> AttributeSet {
114        self.disabled_attributes
115    }
116
117    /// Combine two styles, applying the effects of `other` after `self`
118    pub fn patch(self, other: Style) -> Style {
119        let foreground = self.foreground.or(other.foreground);
120        let background = self.background.or(other.background);
121        let enabled_attributes =
122            (self.enabled_attributes - other.disabled_attributes) | other.enabled_attributes;
123        let disabled_attributes =
124            (self.disabled_attributes - other.enabled_attributes) | other.disabled_attributes;
125        Style {
126            foreground,
127            background,
128            enabled_attributes,
129            disabled_attributes,
130        }
131    }
132
133    /// Enable the given attribute(s)
134    pub fn enable<A: Into<AttributeSet>>(mut self, attrs: A) -> Style {
135        let attrs = attrs.into();
136        self.enabled_attributes |= attrs;
137        self.disabled_attributes -= attrs;
138        self
139    }
140
141    /// Disable the given attribute(s)
142    pub fn disable<A: Into<AttributeSet>>(mut self, attrs: A) -> Style {
143        let attrs = attrs.into();
144        self.enabled_attributes -= attrs;
145        self.disabled_attributes |= attrs;
146        self
147    }
148
149    /// Enable bold text
150    pub fn bold(self) -> Style {
151        self.enable(Attribute::Bold)
152    }
153
154    /// Enable dim text
155    pub fn dim(self) -> Style {
156        self.enable(Attribute::Dim)
157    }
158
159    /// Enable italic text
160    pub fn italic(self) -> Style {
161        self.enable(Attribute::Italic)
162    }
163
164    /// Enable underlining
165    pub fn underline(self) -> Style {
166        self.enable(Attribute::Underline)
167    }
168
169    /// Enable blinking
170    pub fn blink(self) -> Style {
171        self.enable(Attribute::Blink)
172    }
173
174    /// Enable fast blinking
175    pub fn blink2(self) -> Style {
176        self.enable(Attribute::Blink2)
177    }
178
179    /// Enable reverse video
180    pub fn reverse(self) -> Style {
181        self.enable(Attribute::Reverse)
182    }
183
184    /// Enable concealed/hidden text
185    pub fn conceal(self) -> Style {
186        self.enable(Attribute::Conceal)
187    }
188
189    /// Enable strikethrough
190    pub fn strike(self) -> Style {
191        self.enable(Attribute::Strike)
192    }
193
194    /// Enable double-underlining
195    pub fn underline2(self) -> Style {
196        self.enable(Attribute::Underline2)
197    }
198
199    /// Enable framed text
200    pub fn frame(self) -> Style {
201        self.enable(Attribute::Frame)
202    }
203
204    /// Enable encircled text
205    pub fn encircle(self) -> Style {
206        self.enable(Attribute::Encircle)
207    }
208
209    /// Enable overlining
210    pub fn overline(self) -> Style {
211        self.enable(Attribute::Overline)
212    }
213
214    /// Disable bold text
215    pub fn not_bold(self) -> Style {
216        self.disable(Attribute::Bold)
217    }
218
219    /// Disable dim text
220    pub fn not_dim(self) -> Style {
221        self.disable(Attribute::Dim)
222    }
223
224    /// Disable italic text
225    pub fn not_italic(self) -> Style {
226        self.disable(Attribute::Italic)
227    }
228
229    /// Disable underlining
230    pub fn not_underline(self) -> Style {
231        self.disable(Attribute::Underline)
232    }
233
234    /// Disable blinking
235    pub fn not_blink(self) -> Style {
236        self.disable(Attribute::Blink)
237    }
238
239    /// Disable fast blinking
240    pub fn not_blink2(self) -> Style {
241        self.disable(Attribute::Blink2)
242    }
243
244    /// Disable reverse video
245    pub fn not_reverse(self) -> Style {
246        self.disable(Attribute::Reverse)
247    }
248
249    /// Disable concealed/hidden text
250    pub fn not_conceal(self) -> Style {
251        self.disable(Attribute::Conceal)
252    }
253
254    /// Disable strikethrough
255    pub fn not_strike(self) -> Style {
256        self.disable(Attribute::Strike)
257    }
258
259    /// Disable double-underlining
260    pub fn not_underline2(self) -> Style {
261        self.disable(Attribute::Underline2)
262    }
263
264    /// Disable framed text
265    pub fn not_frame(self) -> Style {
266        self.disable(Attribute::Frame)
267    }
268
269    /// Disable encircled text
270    pub fn not_encircle(self) -> Style {
271        self.disable(Attribute::Encircle)
272    }
273
274    /// Disable overlining
275    pub fn not_overline(self) -> Style {
276        self.disable(Attribute::Overline)
277    }
278}
279
280impl<C: Into<Color>> From<C> for Style {
281    /// Construct a new `Style` using the given color as the foreground color
282    fn from(value: C) -> Style {
283        Style::new().foreground(Some(value.into()))
284    }
285}
286
287impl From<Attribute> for Style {
288    /// Construct a new `Style` that enables the given attribute
289    fn from(value: Attribute) -> Style {
290        Style::new().enable(value)
291    }
292}
293
294impl From<AttributeSet> for Style {
295    /// Construct a new `Style` that enables the given attributes
296    fn from(value: AttributeSet) -> Style {
297        Style::new().enabled_attributes(value)
298    }
299}
300
301#[cfg(feature = "anstyle")]
302#[cfg_attr(docsrs, doc(cfg(feature = "anstyle")))]
303impl From<Style> for anstyle::Style {
304    /// Convert a `Style` to an [`anstyle::Style`]
305    ///
306    /// # Data Loss
307    ///
308    /// If the `Style`'s foreground or background color is [`Color::Default`],
309    /// it will be converted to `None`.
310    ///
311    /// The following attributes will be discarded during conversion:
312    ///     - [`Attribute::Blink2`]
313    ///     - [`Attribute::Frame`]
314    ///     - [`Attribute::Encircle`]
315    ///     - [`Attribute::Overline`]
316    ///
317    /// Disabled attributes are discarded during conversion.
318    fn from(value: Style) -> anstyle::Style {
319        anstyle::Style::new()
320            .fg_color(
321                value
322                    .get_foreground()
323                    .and_then(|c| anstyle::Color::try_from(c).ok()),
324            )
325            .bg_color(
326                value
327                    .get_background()
328                    .and_then(|c| anstyle::Color::try_from(c).ok()),
329            )
330            .effects(value.enabled_attributes.into())
331    }
332}
333
334#[cfg(feature = "anstyle")]
335#[cfg_attr(docsrs, doc(cfg(feature = "anstyle")))]
336impl From<anstyle::Style> for Style {
337    /// Convert an [`anstyle::Style`] to a `Style`
338    ///
339    /// # Data Loss
340    ///
341    /// Underline color is discarded during conversion.
342    ///
343    /// The following effects are discarded during conversion:
344    ///
345    /// - [`anstyle::Effects::CURLY_UNDERLINE`]
346    /// - [`anstyle::Effects::DOTTED_UNDERLINE`]
347    /// - [`anstyle::Effects::DASHED_UNDERLINE`]
348    fn from(value: anstyle::Style) -> Style {
349        Style::new()
350            .foreground(value.get_fg_color().map(Color::from))
351            .background(value.get_bg_color().map(Color::from))
352            .enabled_attributes(AttributeSet::from(value.get_effects()))
353    }
354}
355
356#[cfg(feature = "crossterm")]
357#[cfg_attr(docsrs, doc(cfg(feature = "crossterm")))]
358impl From<crossterm::style::Attributes> for Style {
359    /// Convert a [`crossterm::style::Attributes`] to a `Style`
360    ///
361    /// # Data Loss
362    ///
363    /// The following attributes are discarded during conversion:
364    ///
365    /// - [`crossterm::style::Attribute::Undercurled`]
366    /// - [`crossterm::style::Attribute::Underdotted`]
367    /// - [`crossterm::style::Attribute::Underdashed`]
368    /// - [`crossterm::style::Attribute::Fraktur`]
369    /// - [`crossterm::style::Attribute::NoBold`] (because it's dysfunctional)
370    fn from(value: crossterm::style::Attributes) -> Style {
371        use crossterm::style::Attribute as CrossAttrib;
372        let mut set = Style::new();
373        for attr in CrossAttrib::iterator().filter(|&attr| value.has(attr)) {
374            match attr {
375                CrossAttrib::Reset => set = Style::new(),
376                CrossAttrib::Bold => set = set.bold(),
377                CrossAttrib::Dim => set = set.dim(),
378                CrossAttrib::Italic => set = set.italic(),
379                CrossAttrib::Underlined => set = set.underline(),
380                CrossAttrib::DoubleUnderlined => set = set.underline2(),
381                CrossAttrib::Undercurled => (),
382                CrossAttrib::Underdotted => (),
383                CrossAttrib::Underdashed => (),
384                CrossAttrib::SlowBlink => set = set.blink(),
385                CrossAttrib::RapidBlink => set = set.blink2(),
386                CrossAttrib::Reverse => set = set.reverse(),
387                CrossAttrib::Hidden => set = set.conceal(),
388                CrossAttrib::CrossedOut => set = set.strike(),
389                CrossAttrib::Fraktur => (),
390                CrossAttrib::NoBold => (),
391                CrossAttrib::NormalIntensity => set = set.not_bold().not_dim(),
392                CrossAttrib::NoItalic => set = set.not_italic(),
393                CrossAttrib::NoUnderline => set = set.not_underline().not_underline2(),
394                CrossAttrib::NoBlink => set = set.not_blink().not_blink2(),
395                CrossAttrib::NoReverse => set = set.not_reverse(),
396                CrossAttrib::NoHidden => set = set.not_conceal(),
397                CrossAttrib::NotCrossedOut => set = set.not_strike(),
398                CrossAttrib::Framed => set = set.frame(),
399                CrossAttrib::Encircled => set = set.encircle(),
400                CrossAttrib::OverLined => set = set.overline(),
401                CrossAttrib::NotFramedOrEncircled => set = set.not_frame().not_encircle(),
402                CrossAttrib::NotOverLined => set = set.not_overline(),
403                _ => (), // non-exhaustive
404            }
405        }
406        set
407    }
408}
409
410#[cfg(feature = "crossterm")]
411#[cfg_attr(docsrs, doc(cfg(feature = "crossterm")))]
412impl From<Style> for crossterm::style::ContentStyle {
413    /// Convert a `Style` to a [`crossterm::style::ContentStyle`]
414    ///
415    /// # Data Loss
416    ///
417    /// Certain pairs of `parse-style` attributes are disabled by a single
418    /// shared `crossterm` attribute.  Thus, when one element of a pair occurs
419    /// in the disabled attributes, the resulting `ContentStyle` will disable
420    /// both elements of the pair.
421    ///
422    /// The pairs are as follows:
423    ///
424    /// - [`Attribute::Bold`] and [`Attribute::Dim`] — both disabled by
425    ///   [`crossterm::style::Attribute::NormalIntensity`]
426    ///
427    /// - [`Attribute::Blink`] and [`Attribute::Blink2`] — both disabled by
428    ///   [`crossterm::style::Attribute::NoBlink`]
429    ///
430    /// - [`Attribute::Underline`] and [`Attribute::Underline2`] — both
431    ///   disabled by [`crossterm::style::Attribute::NoUnderline`]
432    ///
433    /// - [`Attribute::Frame`] and [`Attribute::Encircle`] — both disabled by
434    ///   [`crossterm::style::Attribute::NotFramedOrEncircled`]
435    fn from(value: Style) -> crossterm::style::ContentStyle {
436        use crossterm::style::Attribute as CrossAttrib;
437        let foreground_color = value.foreground.map(crossterm::style::Color::from);
438        let background_color = value.background.map(crossterm::style::Color::from);
439        let mut attributes = crossterm::style::Attributes::from(value.enabled_attributes);
440        for attr in value.disabled_attributes {
441            match attr {
442                Attribute::Bold => attributes.set(CrossAttrib::NormalIntensity),
443                Attribute::Dim => attributes.set(CrossAttrib::NormalIntensity),
444                Attribute::Italic => attributes.set(CrossAttrib::NoItalic),
445                Attribute::Underline => attributes.set(CrossAttrib::NoUnderline),
446                Attribute::Blink => attributes.set(CrossAttrib::NoBlink),
447                Attribute::Blink2 => attributes.set(CrossAttrib::NoBlink),
448                Attribute::Reverse => attributes.set(CrossAttrib::NoReverse),
449                Attribute::Conceal => attributes.set(CrossAttrib::NoHidden),
450                Attribute::Strike => attributes.set(CrossAttrib::NotCrossedOut),
451                Attribute::Underline2 => attributes.set(CrossAttrib::NoUnderline),
452                Attribute::Frame => attributes.set(CrossAttrib::NotFramedOrEncircled),
453                Attribute::Encircle => attributes.set(CrossAttrib::NotFramedOrEncircled),
454                Attribute::Overline => attributes.set(CrossAttrib::NotOverLined),
455            }
456        }
457        crossterm::style::ContentStyle {
458            foreground_color,
459            background_color,
460            attributes,
461            underline_color: None,
462        }
463    }
464}
465
466#[cfg(feature = "crossterm")]
467#[cfg_attr(docsrs, doc(cfg(feature = "crossterm")))]
468impl From<crossterm::style::ContentStyle> for Style {
469    /// Convert a [`crossterm::style::ContentStyle`] to a `Style`
470    ///
471    /// # Data Loss
472    ///
473    /// Underline color is discarded during conversion.
474    ///
475    /// The following attributes are discarded during conversion:
476    ///
477    /// - [`crossterm::style::Attribute::Undercurled`]
478    /// - [`crossterm::style::Attribute::Underdotted`]
479    /// - [`crossterm::style::Attribute::Underdashed`]
480    /// - [`crossterm::style::Attribute::Fraktur`]
481    fn from(value: crossterm::style::ContentStyle) -> Style {
482        Style::from(value.attributes)
483            .foreground(value.foreground_color.map(Color::from))
484            .background(value.background_color.map(Color::from))
485    }
486}
487
488#[cfg(feature = "ratatui")]
489#[cfg_attr(docsrs, doc(cfg(feature = "ratatui")))]
490impl From<Style> for ratatui::style::Style {
491    /// Convert a `Style` to a [`ratatui::style::Style`]
492    ///
493    /// # Data Loss
494    ///
495    /// The following attributes are discarded during conversion:
496    ///
497    /// - [`Attribute::Underline2`]
498    /// - [`Attribute::Frame`]
499    /// - [`Attribute::Encircle`]
500    /// - [`Attribute::Overline`]
501    fn from(value: Style) -> ratatui::style::Style {
502        // Don't try to construct a ratatui Style using struct notation, as the
503        // `underline_color` field is feature-based.
504        let mut style = ratatui::style::Style::new();
505        if let Some(fg) = value.foreground.map(ratatui::style::Color::from) {
506            style = style.fg(fg);
507        }
508        if let Some(bg) = value.background.map(ratatui::style::Color::from) {
509            style = style.bg(bg);
510        }
511        style = style.add_modifier(value.enabled_attributes.into());
512        style = style.remove_modifier(value.disabled_attributes.into());
513        style
514    }
515}
516
517#[cfg(feature = "ratatui")]
518#[cfg_attr(docsrs, doc(cfg(feature = "ratatui")))]
519impl From<ratatui::style::Style> for Style {
520    /// Convert a [`ratatui::style::Style`] to a `Style`
521    ///
522    /// # Data Loss
523    ///
524    /// Underline color is discarded during conversion.
525    fn from(value: ratatui::style::Style) -> Style {
526        let foreground = value.fg.map(Color::from);
527        let background = value.bg.map(Color::from);
528        let enabled_attributes = AttributeSet::from(value.add_modifier);
529        let disabled_attributes = AttributeSet::from(value.sub_modifier);
530        Style {
531            foreground,
532            background,
533            enabled_attributes,
534            disabled_attributes,
535        }
536    }
537}
538
539impl fmt::Display for Style {
540    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
541        let mut first = true;
542        for attr in Attribute::iter() {
543            if self.is_enabled(attr) {
544                if !std::mem::replace(&mut first, false) {
545                    write!(f, " ")?;
546                }
547                write!(f, "{attr}")?;
548            } else if self.is_disabled(attr) {
549                if !std::mem::replace(&mut first, false) {
550                    write!(f, " ")?;
551                }
552                write!(f, "not {attr}")?;
553            }
554        }
555        if let Some(fg) = self.foreground {
556            if !std::mem::replace(&mut first, false) {
557                write!(f, " ")?;
558            }
559            write!(f, "{fg}")?;
560        }
561        if let Some(bg) = self.background {
562            if !std::mem::replace(&mut first, false) {
563                write!(f, " ")?;
564            }
565            write!(f, "on {bg}")?;
566        }
567        if first {
568            write!(f, "none")?;
569        }
570        Ok(())
571    }
572}
573
574impl std::str::FromStr for Style {
575    type Err = ParseStyleError;
576
577    fn from_str(s: &str) -> Result<Style, ParseStyleError> {
578        let mut style = Style::new();
579        if s.is_empty() || s.trim().eq_ignore_ascii_case("none") {
580            return Ok(style);
581        }
582        let mut words = s.split_whitespace();
583        while let Some(token) = words.next() {
584            if token.eq_ignore_ascii_case("on") {
585                let Some(bg) = words.next().and_then(|s| s.parse::<Color>().ok()) else {
586                    return Err(ParseStyleError::MissingBackground);
587                };
588                style.background = Some(bg);
589            } else if token.eq_ignore_ascii_case("not") {
590                let Some(attr) = words.next().and_then(|s| s.parse::<Attribute>().ok()) else {
591                    return Err(ParseStyleError::MissingAttribute);
592                };
593                style = style.disable(attr);
594            } else if let Ok(color) = token.parse::<Color>() {
595                style.foreground = Some(color);
596            } else if let Ok(attr) = token.parse::<Attribute>() {
597                style = style.enable(attr);
598            } else {
599                return Err(ParseStyleError::Token(token.to_owned()));
600            }
601        }
602        Ok(style)
603    }
604}
605
606#[cfg(feature = "serde")]
607#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
608impl serde::Serialize for Style {
609    fn serialize<S: serde::ser::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
610        serializer.collect_str(self)
611    }
612}
613
614#[cfg(feature = "serde")]
615#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
616impl<'de> serde::Deserialize<'de> for Style {
617    fn deserialize<D: serde::de::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
618        struct Visitor;
619
620        impl serde::de::Visitor<'_> for Visitor {
621            type Value = Style;
622
623            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
624                f.write_str("a style string")
625            }
626
627            fn visit_str<E>(self, input: &str) -> Result<Self::Value, E>
628            where
629                E: serde::de::Error,
630            {
631                input
632                    .parse::<Style>()
633                    .map_err(|_| E::invalid_value(serde::de::Unexpected::Str(input), &self))
634            }
635        }
636
637        deserializer.deserialize_str(Visitor)
638    }
639}
640
641/// Error returned when parsing a style fails
642#[derive(Clone, Debug, Eq, Error, PartialEq)]
643pub enum ParseStyleError {
644    /// An invalid/unexpected token was enountered
645    #[error("unexpected token in style string: {0:?}")]
646    Token(
647        /// The invalid token
648        String,
649    ),
650
651    /// `"on"` was not followed by a valid color word
652    #[error(r#""on" not followed by valid color word"#)]
653    MissingBackground,
654
655    /// `"not"` was not followed by a valid attribute name
656    #[error(r#""not" not followed by valid attribute name"#)]
657    MissingAttribute,
658}
659
660#[cfg(test)]
661mod test {
662    use super::*;
663
664    #[test]
665    fn test_new_is_default() {
666        assert_eq!(Style::new(), Style::default());
667    }
668
669    mod display {
670        use super::*;
671        use crate::Color256;
672
673        #[test]
674        fn none() {
675            assert_eq!(Style::new().to_string(), "none");
676        }
677
678        #[test]
679        fn fg_color() {
680            let style = Style::from(Color256::RED);
681            assert_eq!(style.to_string(), "red");
682        }
683
684        #[test]
685        fn bg_color() {
686            let style = Color256::RED.as_background();
687            assert_eq!(style.to_string(), "on red");
688        }
689
690        #[test]
691        fn fg_on_bg() {
692            let style = Color256::BLUE.on(Color256::RED);
693            assert_eq!(style.to_string(), "blue on red");
694        }
695
696        #[test]
697        fn attr() {
698            let style = Style::from(Attribute::Bold);
699            assert_eq!(style.to_string(), "bold");
700        }
701
702        #[test]
703        fn multiple_attrs() {
704            let style = Style::from(Attribute::Bold | Attribute::Reverse);
705            assert_eq!(style.to_string(), "bold reverse");
706        }
707
708        #[test]
709        fn not_attr() {
710            let style = Style::new().disable(Attribute::Bold);
711            assert_eq!(style.to_string(), "not bold");
712        }
713
714        #[test]
715        fn multiple_not_attrs() {
716            let style = Style::new()
717                .disable(Attribute::Bold)
718                .disable(Attribute::Reverse);
719            assert_eq!(style.to_string(), "not bold not reverse");
720        }
721
722        #[test]
723        fn attr_and_not_attr() {
724            let style = Style::from(Attribute::Bold).disable(Attribute::Blink);
725            assert_eq!(style.to_string(), "bold not blink");
726        }
727
728        #[test]
729        fn gamut() {
730            let style = Color256::YELLOW
731                .on(Color::Default)
732                .enable(Attribute::Italic)
733                .disable(Attribute::Bold);
734            assert_eq!(style.to_string(), "not bold italic yellow on default");
735        }
736
737        #[test]
738        fn all_attrs() {
739            let style = Style::from(AttributeSet::ALL);
740            assert_eq!(style.to_string(), "bold dim italic underline blink blink2 reverse conceal strike underline2 frame encircle overline");
741        }
742
743        #[test]
744        fn not_all_attrs() {
745            let style = Style::new().disabled_attributes(AttributeSet::ALL);
746            assert_eq!(style.to_string(), "not bold not dim not italic not underline not blink not blink2 not reverse not conceal not strike not underline2 not frame not encircle not overline");
747        }
748    }
749
750    mod parse {
751        use super::*;
752        use crate::Color256;
753        use rstest::rstest;
754
755        #[test]
756        fn none() {
757            assert_eq!("".parse::<Style>().unwrap(), Style::new());
758            assert_eq!("none".parse::<Style>().unwrap(), Style::new());
759            assert_eq!("NONE".parse::<Style>().unwrap(), Style::new());
760            assert_eq!(" none ".parse::<Style>().unwrap(), Style::new());
761        }
762
763        #[test]
764        fn fg() {
765            assert_eq!(
766                "green".parse::<Style>().unwrap(),
767                Style::from(Color256::GREEN)
768            );
769        }
770
771        #[test]
772        fn bg() {
773            assert_eq!(
774                "on green".parse::<Style>().unwrap(),
775                Color256::GREEN.as_background()
776            );
777            assert_eq!(
778                " on  green ".parse::<Style>().unwrap(),
779                Color256::GREEN.as_background()
780            );
781            assert_eq!(
782                " ON  GREEN ".parse::<Style>().unwrap(),
783                Color256::GREEN.as_background()
784            );
785        }
786
787        #[test]
788        fn fg_on_bg() {
789            assert_eq!(
790                "blue on white".parse::<Style>().unwrap(),
791                Color256::BLUE.on(Color256::WHITE)
792            );
793            assert_eq!(
794                "on white blue".parse::<Style>().unwrap(),
795                Color256::BLUE.on(Color256::WHITE)
796            );
797        }
798
799        #[test]
800        fn attr() {
801            assert_eq!(
802                "bold".parse::<Style>().unwrap(),
803                Style::from(Attribute::Bold)
804            );
805        }
806
807        #[test]
808        fn multiple_attr() {
809            assert_eq!(
810                "bold underline".parse::<Style>().unwrap(),
811                Style::from(Attribute::Bold | Attribute::Underline)
812            );
813            assert_eq!(
814                "underline bold".parse::<Style>().unwrap(),
815                Style::from(Attribute::Bold | Attribute::Underline)
816            );
817        }
818
819        #[test]
820        fn not_attr() {
821            assert_eq!(
822                "not bold".parse::<Style>().unwrap(),
823                Style::new().disable(Attribute::Bold)
824            );
825            assert_eq!(
826                " NOT  BOLD ".parse::<Style>().unwrap(),
827                Style::new().disable(Attribute::Bold)
828            );
829        }
830
831        #[test]
832        fn multiple_not_attrs() {
833            assert_eq!(
834                "not bold not s".parse::<Style>().unwrap(),
835                Style::new().disabled_attributes(Attribute::Bold | Attribute::Strike)
836            );
837            assert_eq!(
838                "not s not bold".parse::<Style>().unwrap(),
839                Style::new().disabled_attributes(Attribute::Bold | Attribute::Strike)
840            );
841        }
842
843        #[test]
844        fn attr_and_not_attr() {
845            assert_eq!(
846                "dim not blink2".parse::<Style>().unwrap(),
847                Style::new()
848                    .enable(Attribute::Dim)
849                    .disable(Attribute::Blink2)
850            );
851            assert_eq!(
852                "not blink2 dim".parse::<Style>().unwrap(),
853                Style::new()
854                    .enable(Attribute::Dim)
855                    .disable(Attribute::Blink2)
856            );
857        }
858
859        #[test]
860        fn gamut() {
861            for s in [
862                "bold not underline red on blue",
863                "not underline red on blue bold",
864                "on blue red not underline bold",
865            ] {
866                assert_eq!(
867                    s.parse::<Style>().unwrap(),
868                    Color256::RED.on(Color256::BLUE).bold().not_underline()
869                );
870            }
871        }
872
873        #[test]
874        fn multiple_fg() {
875            assert_eq!(
876                "red blue".parse::<Style>().unwrap(),
877                Style::from(Color256::BLUE)
878            );
879        }
880
881        #[test]
882        fn multiple_bg() {
883            assert_eq!(
884                "on red on blue".parse::<Style>().unwrap(),
885                Color256::BLUE.as_background()
886            );
887        }
888
889        #[test]
890        fn attr_on_and_off() {
891            assert_eq!(
892                "bold magenta not bold".parse::<Style>().unwrap(),
893                Style::from(Color256::MAGENTA).not_bold()
894            );
895        }
896
897        #[test]
898        fn attr_off_and_on() {
899            assert_eq!(
900                "not bold magenta bold".parse::<Style>().unwrap(),
901                Style::from(Color256::MAGENTA).bold()
902            );
903        }
904
905        #[rstest]
906        #[case("on bold")]
907        #[case("on foo")]
908        #[case("blue on")]
909        #[case("on")]
910        #[case("not blue")]
911        #[case("not foo")]
912        #[case("bold not")]
913        #[case("not not bold italic")]
914        #[case("not")]
915        #[case("none red")]
916        #[case("red none")]
917        #[case("foo")]
918        #[case("rgb(1, 2, 3)")]
919        #[case("bright blue")]
920        fn err(#[case] s: &str) {
921            assert!(s.parse::<Style>().is_err());
922        }
923    }
924}