1use super::attributes::{Attribute, AttributeSet};
2use super::color::Color;
3use std::fmt;
4use thiserror::Error;
5
6#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
15pub struct Style {
16 foreground: Option<Color>,
18
19 background: Option<Color>,
21
22 enabled_attributes: AttributeSet,
24
25 disabled_attributes: AttributeSet,
30}
31
32impl Style {
33 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 pub const fn foreground(mut self, fg: Option<Color>) -> Style {
50 self.foreground = fg;
51 self
52 }
53
54 pub const fn background(mut self, bg: Option<Color>) -> Style {
61 self.background = bg;
62 self
63 }
64
65 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 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 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 pub fn is_enabled(self, attr: Attribute) -> bool {
89 self.enabled_attributes.contains(attr) && !self.disabled_attributes.contains(attr)
90 }
91
92 pub fn is_disabled(self, attr: Attribute) -> bool {
94 self.disabled_attributes.contains(attr) && !self.enabled_attributes.contains(attr)
95 }
96
97 pub const fn get_foreground(self) -> Option<Color> {
99 self.foreground
100 }
101
102 pub const fn get_background(self) -> Option<Color> {
104 self.background
105 }
106
107 pub const fn get_enabled_attributes(self) -> AttributeSet {
109 self.enabled_attributes
110 }
111
112 pub const fn get_disabled_attributes(self) -> AttributeSet {
114 self.disabled_attributes
115 }
116
117 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 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 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 pub fn bold(self) -> Style {
151 self.enable(Attribute::Bold)
152 }
153
154 pub fn dim(self) -> Style {
156 self.enable(Attribute::Dim)
157 }
158
159 pub fn italic(self) -> Style {
161 self.enable(Attribute::Italic)
162 }
163
164 pub fn underline(self) -> Style {
166 self.enable(Attribute::Underline)
167 }
168
169 pub fn blink(self) -> Style {
171 self.enable(Attribute::Blink)
172 }
173
174 pub fn blink2(self) -> Style {
176 self.enable(Attribute::Blink2)
177 }
178
179 pub fn reverse(self) -> Style {
181 self.enable(Attribute::Reverse)
182 }
183
184 pub fn conceal(self) -> Style {
186 self.enable(Attribute::Conceal)
187 }
188
189 pub fn strike(self) -> Style {
191 self.enable(Attribute::Strike)
192 }
193
194 pub fn underline2(self) -> Style {
196 self.enable(Attribute::Underline2)
197 }
198
199 pub fn frame(self) -> Style {
201 self.enable(Attribute::Frame)
202 }
203
204 pub fn encircle(self) -> Style {
206 self.enable(Attribute::Encircle)
207 }
208
209 pub fn overline(self) -> Style {
211 self.enable(Attribute::Overline)
212 }
213
214 pub fn not_bold(self) -> Style {
216 self.disable(Attribute::Bold)
217 }
218
219 pub fn not_dim(self) -> Style {
221 self.disable(Attribute::Dim)
222 }
223
224 pub fn not_italic(self) -> Style {
226 self.disable(Attribute::Italic)
227 }
228
229 pub fn not_underline(self) -> Style {
231 self.disable(Attribute::Underline)
232 }
233
234 pub fn not_blink(self) -> Style {
236 self.disable(Attribute::Blink)
237 }
238
239 pub fn not_blink2(self) -> Style {
241 self.disable(Attribute::Blink2)
242 }
243
244 pub fn not_reverse(self) -> Style {
246 self.disable(Attribute::Reverse)
247 }
248
249 pub fn not_conceal(self) -> Style {
251 self.disable(Attribute::Conceal)
252 }
253
254 pub fn not_strike(self) -> Style {
256 self.disable(Attribute::Strike)
257 }
258
259 pub fn not_underline2(self) -> Style {
261 self.disable(Attribute::Underline2)
262 }
263
264 pub fn not_frame(self) -> Style {
266 self.disable(Attribute::Frame)
267 }
268
269 pub fn not_encircle(self) -> Style {
271 self.disable(Attribute::Encircle)
272 }
273
274 pub fn not_overline(self) -> Style {
276 self.disable(Attribute::Overline)
277 }
278}
279
280impl<C: Into<Color>> From<C> for Style {
281 fn from(value: C) -> Style {
283 Style::new().foreground(Some(value.into()))
284 }
285}
286
287impl From<Attribute> for Style {
288 fn from(value: Attribute) -> Style {
290 Style::new().enable(value)
291 }
292}
293
294impl From<AttributeSet> for Style {
295 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 fn from(value: Style) -> anstyle::Style {
320 anstyle::Style::new()
321 .fg_color(
322 value
323 .get_foreground()
324 .and_then(|c| anstyle::Color::try_from(c).ok()),
325 )
326 .bg_color(
327 value
328 .get_background()
329 .and_then(|c| anstyle::Color::try_from(c).ok()),
330 )
331 .effects(value.enabled_attributes.into())
332 }
333}
334
335#[cfg(feature = "anstyle")]
336#[cfg_attr(docsrs, doc(cfg(feature = "anstyle")))]
337impl From<anstyle::Style> for Style {
338 fn from(value: anstyle::Style) -> Style {
350 Style::new()
351 .foreground(value.get_fg_color().map(Color::from))
352 .background(value.get_bg_color().map(Color::from))
353 .enabled_attributes(AttributeSet::from(value.get_effects()))
354 }
355}
356
357#[cfg(feature = "crossterm")]
358#[cfg_attr(docsrs, doc(cfg(feature = "crossterm")))]
359impl From<crossterm::style::Attributes> for Style {
360 fn from(value: crossterm::style::Attributes) -> Style {
372 use crossterm::style::Attribute as CrossAttrib;
373 let mut set = Style::new();
374 for attr in CrossAttrib::iterator().filter(|&attr| value.has(attr)) {
375 match attr {
376 CrossAttrib::Reset => set = Style::new(),
377 CrossAttrib::Bold => set = set.bold(),
378 CrossAttrib::Dim => set = set.dim(),
379 CrossAttrib::Italic => set = set.italic(),
380 CrossAttrib::Underlined => set = set.underline(),
381 CrossAttrib::DoubleUnderlined => set = set.underline2(),
382 CrossAttrib::Undercurled => (),
383 CrossAttrib::Underdotted => (),
384 CrossAttrib::Underdashed => (),
385 CrossAttrib::SlowBlink => set = set.blink(),
386 CrossAttrib::RapidBlink => set = set.blink2(),
387 CrossAttrib::Reverse => set = set.reverse(),
388 CrossAttrib::Hidden => set = set.conceal(),
389 CrossAttrib::CrossedOut => set = set.strike(),
390 CrossAttrib::Fraktur => (),
391 CrossAttrib::NoBold => (),
392 CrossAttrib::NormalIntensity => set = set.not_bold().not_dim(),
393 CrossAttrib::NoItalic => set = set.not_italic(),
394 CrossAttrib::NoUnderline => set = set.not_underline().not_underline2(),
395 CrossAttrib::NoBlink => set = set.not_blink().not_blink2(),
396 CrossAttrib::NoReverse => set = set.not_reverse(),
397 CrossAttrib::NoHidden => set = set.not_conceal(),
398 CrossAttrib::NotCrossedOut => set = set.not_strike(),
399 CrossAttrib::Framed => set = set.frame(),
400 CrossAttrib::Encircled => set = set.encircle(),
401 CrossAttrib::OverLined => set = set.overline(),
402 CrossAttrib::NotFramedOrEncircled => set = set.not_frame().not_encircle(),
403 CrossAttrib::NotOverLined => set = set.not_overline(),
404 _ => (), }
406 }
407 set
408 }
409}
410
411#[cfg(feature = "crossterm")]
412#[cfg_attr(docsrs, doc(cfg(feature = "crossterm")))]
413impl From<Style> for crossterm::style::ContentStyle {
414 fn from(value: Style) -> crossterm::style::ContentStyle {
437 use crossterm::style::Attribute as CrossAttrib;
438 let foreground_color = value.foreground.map(crossterm::style::Color::from);
439 let background_color = value.background.map(crossterm::style::Color::from);
440 let mut attributes = crossterm::style::Attributes::from(value.enabled_attributes);
441 for attr in value.disabled_attributes {
442 match attr {
443 Attribute::Bold => attributes.set(CrossAttrib::NormalIntensity),
444 Attribute::Dim => attributes.set(CrossAttrib::NormalIntensity),
445 Attribute::Italic => attributes.set(CrossAttrib::NoItalic),
446 Attribute::Underline => attributes.set(CrossAttrib::NoUnderline),
447 Attribute::Blink => attributes.set(CrossAttrib::NoBlink),
448 Attribute::Blink2 => attributes.set(CrossAttrib::NoBlink),
449 Attribute::Reverse => attributes.set(CrossAttrib::NoReverse),
450 Attribute::Conceal => attributes.set(CrossAttrib::NoHidden),
451 Attribute::Strike => attributes.set(CrossAttrib::NotCrossedOut),
452 Attribute::Underline2 => attributes.set(CrossAttrib::NoUnderline),
453 Attribute::Frame => attributes.set(CrossAttrib::NotFramedOrEncircled),
454 Attribute::Encircle => attributes.set(CrossAttrib::NotFramedOrEncircled),
455 Attribute::Overline => attributes.set(CrossAttrib::NotOverLined),
456 }
457 }
458 crossterm::style::ContentStyle {
459 foreground_color,
460 background_color,
461 attributes,
462 underline_color: None,
463 }
464 }
465}
466
467#[cfg(feature = "crossterm")]
468#[cfg_attr(docsrs, doc(cfg(feature = "crossterm")))]
469impl From<crossterm::style::ContentStyle> for Style {
470 fn from(value: crossterm::style::ContentStyle) -> Style {
483 Style::from(value.attributes)
484 .foreground(value.foreground_color.map(Color::from))
485 .background(value.background_color.map(Color::from))
486 }
487}
488
489#[cfg(feature = "ratatui")]
490#[cfg_attr(docsrs, doc(cfg(feature = "ratatui")))]
491impl From<Style> for ratatui_core::style::Style {
492 fn from(value: Style) -> ratatui_core::style::Style {
503 let mut style = ratatui_core::style::Style::new();
506 if let Some(fg) = value.foreground.map(ratatui_core::style::Color::from) {
507 style = style.fg(fg);
508 }
509 if let Some(bg) = value.background.map(ratatui_core::style::Color::from) {
510 style = style.bg(bg);
511 }
512 style = style.add_modifier(value.enabled_attributes.into());
513 style = style.remove_modifier(value.disabled_attributes.into());
514 style
515 }
516}
517
518#[cfg(feature = "ratatui")]
519#[cfg_attr(docsrs, doc(cfg(feature = "ratatui")))]
520impl From<ratatui_core::style::Style> for Style {
521 fn from(value: ratatui_core::style::Style) -> Style {
527 let foreground = value.fg.map(Color::from);
528 let background = value.bg.map(Color::from);
529 let enabled_attributes = AttributeSet::from(value.add_modifier);
530 let disabled_attributes = AttributeSet::from(value.sub_modifier);
531 Style {
532 foreground,
533 background,
534 enabled_attributes,
535 disabled_attributes,
536 }
537 }
538}
539
540impl fmt::Display for Style {
541 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
542 let mut first = true;
543 for attr in Attribute::iter() {
544 if self.is_enabled(attr) {
545 if !std::mem::replace(&mut first, false) {
546 write!(f, " ")?;
547 }
548 if f.alternate() {
549 write!(f, "{attr:#}")?;
550 } else {
551 write!(f, "{attr}")?;
552 }
553 } else if self.is_disabled(attr) {
554 if !std::mem::replace(&mut first, false) {
555 write!(f, " ")?;
556 }
557 if f.alternate() {
558 write!(f, "not {attr:#}")?;
559 } else {
560 write!(f, "not {attr}")?;
561 }
562 }
563 }
564 if let Some(fg) = self.foreground {
565 if !std::mem::replace(&mut first, false) {
566 write!(f, " ")?;
567 }
568 write!(f, "{fg}")?;
569 }
570 if let Some(bg) = self.background {
571 if !std::mem::replace(&mut first, false) {
572 write!(f, " ")?;
573 }
574 write!(f, "on {bg}")?;
575 }
576 if first {
577 write!(f, "none")?;
578 }
579 Ok(())
580 }
581}
582
583impl std::str::FromStr for Style {
584 type Err = ParseStyleError;
585
586 fn from_str(s: &str) -> Result<Style, ParseStyleError> {
587 let mut style = Style::new();
588 if s.is_empty() || s.trim().eq_ignore_ascii_case("none") {
589 return Ok(style);
590 }
591 let mut words = s.split_whitespace();
592 while let Some(token) = words.next() {
593 if token.eq_ignore_ascii_case("on") {
594 let Some(bg) = words.next().and_then(|s| s.parse::<Color>().ok()) else {
595 return Err(ParseStyleError::MissingBackground);
596 };
597 style.background = Some(bg);
598 } else if token.eq_ignore_ascii_case("not") {
599 let Some(attr) = words.next().and_then(|s| s.parse::<Attribute>().ok()) else {
600 return Err(ParseStyleError::MissingAttribute);
601 };
602 style = style.disable(attr);
603 } else if let Ok(color) = token.parse::<Color>() {
604 style.foreground = Some(color);
605 } else if let Ok(attr) = token.parse::<Attribute>() {
606 style = style.enable(attr);
607 } else {
608 return Err(ParseStyleError::Token(token.to_owned()));
609 }
610 }
611 Ok(style)
612 }
613}
614
615#[cfg(feature = "serde")]
616#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
617impl serde::Serialize for Style {
618 fn serialize<S: serde::ser::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
619 serializer.collect_str(self)
620 }
621}
622
623#[cfg(feature = "serde")]
624#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
625impl<'de> serde::Deserialize<'de> for Style {
626 fn deserialize<D: serde::de::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
627 struct Visitor;
628
629 impl serde::de::Visitor<'_> for Visitor {
630 type Value = Style;
631
632 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
633 f.write_str("a style string")
634 }
635
636 fn visit_str<E>(self, input: &str) -> Result<Self::Value, E>
637 where
638 E: serde::de::Error,
639 {
640 input
641 .parse::<Style>()
642 .map_err(|_| E::invalid_value(serde::de::Unexpected::Str(input), &self))
643 }
644 }
645
646 deserializer.deserialize_str(Visitor)
647 }
648}
649
650#[derive(Clone, Debug, Eq, Error, PartialEq)]
652pub enum ParseStyleError {
653 #[error("unexpected token in style string: {0:?}")]
655 Token(
656 String,
658 ),
659
660 #[error(r#""on" not followed by valid color word"#)]
662 MissingBackground,
663
664 #[error(r#""not" not followed by valid attribute name"#)]
666 MissingAttribute,
667}
668
669#[cfg(test)]
670mod test {
671 use super::*;
672
673 #[test]
674 fn test_new_is_default() {
675 assert_eq!(Style::new(), Style::default());
676 }
677
678 mod display {
679 use super::*;
680 use crate::Color256;
681
682 #[test]
683 fn none() {
684 assert_eq!(Style::new().to_string(), "none");
685 }
686
687 #[test]
688 fn fg_color() {
689 let style = Style::from(Color256::RED);
690 assert_eq!(style.to_string(), "red");
691 }
692
693 #[test]
694 fn bg_color() {
695 let style = Color256::RED.as_background();
696 assert_eq!(style.to_string(), "on red");
697 }
698
699 #[test]
700 fn fg_on_bg() {
701 let style = Color256::BLUE.on(Color256::RED);
702 assert_eq!(style.to_string(), "blue on red");
703 }
704
705 #[test]
706 fn attr() {
707 let style = Style::from(Attribute::Bold);
708 assert_eq!(style.to_string(), "bold");
709 }
710
711 #[test]
712 fn alt_attr() {
713 let style = Style::from(Attribute::Bold);
714 assert_eq!(format!("{style:#}"), "b");
715 }
716
717 #[test]
718 fn multiple_attrs() {
719 let style = Style::from(Attribute::Bold | Attribute::Reverse);
720 assert_eq!(style.to_string(), "bold reverse");
721 }
722
723 #[test]
724 fn alt_multiple_attrs() {
725 let style = Style::from(Attribute::Bold | Attribute::Reverse);
726 assert_eq!(format!("{style:#}"), "b r");
727 }
728
729 #[test]
730 fn not_attr() {
731 let style = Style::new().disable(Attribute::Bold);
732 assert_eq!(style.to_string(), "not bold");
733 }
734
735 #[test]
736 fn alt_not_attr() {
737 let style = Style::new().disable(Attribute::Bold);
738 assert_eq!(format!("{style:#}"), "not b");
739 }
740
741 #[test]
742 fn multiple_not_attrs() {
743 let style = Style::new()
744 .disable(Attribute::Bold)
745 .disable(Attribute::Reverse);
746 assert_eq!(style.to_string(), "not bold not reverse");
747 }
748
749 #[test]
750 fn alt_multiple_not_attrs() {
751 let style = Style::new()
752 .disable(Attribute::Bold)
753 .disable(Attribute::Reverse);
754 assert_eq!(format!("{style:#}"), "not b not r");
755 }
756
757 #[test]
758 fn attr_and_not_attr() {
759 let style = Style::from(Attribute::Bold).disable(Attribute::Blink);
760 assert_eq!(style.to_string(), "bold not blink");
761 }
762
763 #[test]
764 fn gamut() {
765 let style = Color256::YELLOW
766 .on(Color::Default)
767 .enable(Attribute::Italic)
768 .disable(Attribute::Bold);
769 assert_eq!(style.to_string(), "not bold italic yellow on default");
770 }
771
772 #[test]
773 fn all_attrs() {
774 let style = Style::from(AttributeSet::ALL);
775 assert_eq!(
776 style.to_string(),
777 "bold dim italic underline blink blink2 reverse conceal strike underline2 frame encircle overline"
778 );
779 }
780
781 #[test]
782 fn not_all_attrs() {
783 let style = Style::new().disabled_attributes(AttributeSet::ALL);
784 assert_eq!(
785 style.to_string(),
786 "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"
787 );
788 }
789 }
790
791 mod parse {
792 use super::*;
793 use crate::Color256;
794 use rstest::rstest;
795
796 #[test]
797 fn none() {
798 assert_eq!("".parse::<Style>().unwrap(), Style::new());
799 assert_eq!("none".parse::<Style>().unwrap(), Style::new());
800 assert_eq!("NONE".parse::<Style>().unwrap(), Style::new());
801 assert_eq!(" none ".parse::<Style>().unwrap(), Style::new());
802 }
803
804 #[test]
805 fn fg() {
806 assert_eq!(
807 "green".parse::<Style>().unwrap(),
808 Style::from(Color256::GREEN)
809 );
810 }
811
812 #[test]
813 fn bg() {
814 assert_eq!(
815 "on green".parse::<Style>().unwrap(),
816 Color256::GREEN.as_background()
817 );
818 assert_eq!(
819 " on green ".parse::<Style>().unwrap(),
820 Color256::GREEN.as_background()
821 );
822 assert_eq!(
823 " ON GREEN ".parse::<Style>().unwrap(),
824 Color256::GREEN.as_background()
825 );
826 }
827
828 #[test]
829 fn fg_on_bg() {
830 assert_eq!(
831 "blue on white".parse::<Style>().unwrap(),
832 Color256::BLUE.on(Color256::WHITE)
833 );
834 assert_eq!(
835 "on white blue".parse::<Style>().unwrap(),
836 Color256::BLUE.on(Color256::WHITE)
837 );
838 }
839
840 #[test]
841 fn attr() {
842 assert_eq!(
843 "bold".parse::<Style>().unwrap(),
844 Style::from(Attribute::Bold)
845 );
846 }
847
848 #[test]
849 fn multiple_attr() {
850 assert_eq!(
851 "bold underline".parse::<Style>().unwrap(),
852 Style::from(Attribute::Bold | Attribute::Underline)
853 );
854 assert_eq!(
855 "underline bold".parse::<Style>().unwrap(),
856 Style::from(Attribute::Bold | Attribute::Underline)
857 );
858 }
859
860 #[test]
861 fn not_attr() {
862 assert_eq!(
863 "not bold".parse::<Style>().unwrap(),
864 Style::new().disable(Attribute::Bold)
865 );
866 assert_eq!(
867 " NOT BOLD ".parse::<Style>().unwrap(),
868 Style::new().disable(Attribute::Bold)
869 );
870 }
871
872 #[test]
873 fn multiple_not_attrs() {
874 assert_eq!(
875 "not bold not s".parse::<Style>().unwrap(),
876 Style::new().disabled_attributes(Attribute::Bold | Attribute::Strike)
877 );
878 assert_eq!(
879 "not s not bold".parse::<Style>().unwrap(),
880 Style::new().disabled_attributes(Attribute::Bold | Attribute::Strike)
881 );
882 }
883
884 #[test]
885 fn attr_and_not_attr() {
886 assert_eq!(
887 "dim not blink2".parse::<Style>().unwrap(),
888 Style::new()
889 .enable(Attribute::Dim)
890 .disable(Attribute::Blink2)
891 );
892 assert_eq!(
893 "not blink2 dim".parse::<Style>().unwrap(),
894 Style::new()
895 .enable(Attribute::Dim)
896 .disable(Attribute::Blink2)
897 );
898 }
899
900 #[test]
901 fn gamut() {
902 for s in [
903 "bold not underline red on blue",
904 "not underline red on blue bold",
905 "on blue red not underline bold",
906 ] {
907 assert_eq!(
908 s.parse::<Style>().unwrap(),
909 Color256::RED.on(Color256::BLUE).bold().not_underline()
910 );
911 }
912 }
913
914 #[test]
915 fn multiple_fg() {
916 assert_eq!(
917 "red blue".parse::<Style>().unwrap(),
918 Style::from(Color256::BLUE)
919 );
920 }
921
922 #[test]
923 fn multiple_bg() {
924 assert_eq!(
925 "on red on blue".parse::<Style>().unwrap(),
926 Color256::BLUE.as_background()
927 );
928 }
929
930 #[test]
931 fn attr_on_and_off() {
932 assert_eq!(
933 "bold magenta not bold".parse::<Style>().unwrap(),
934 Style::from(Color256::MAGENTA).not_bold()
935 );
936 }
937
938 #[test]
939 fn attr_off_and_on() {
940 assert_eq!(
941 "not bold magenta bold".parse::<Style>().unwrap(),
942 Style::from(Color256::MAGENTA).bold()
943 );
944 }
945
946 #[rstest]
947 #[case("on bold")]
948 #[case("on foo")]
949 #[case("blue on")]
950 #[case("on")]
951 #[case("not blue")]
952 #[case("not foo")]
953 #[case("bold not")]
954 #[case("not not bold italic")]
955 #[case("not")]
956 #[case("none red")]
957 #[case("red none")]
958 #[case("foo")]
959 #[case("rgb(1, 2, 3)")]
960 #[case("bright blue")]
961 fn err(#[case] s: &str) {
962 assert!(s.parse::<Style>().is_err());
963 }
964 }
965}