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 write!(f, "{attr}")?;
549 } else if self.is_disabled(attr) {
550 if !std::mem::replace(&mut first, false) {
551 write!(f, " ")?;
552 }
553 write!(f, "not {attr}")?;
554 }
555 }
556 if let Some(fg) = self.foreground {
557 if !std::mem::replace(&mut first, false) {
558 write!(f, " ")?;
559 }
560 write!(f, "{fg}")?;
561 }
562 if let Some(bg) = self.background {
563 if !std::mem::replace(&mut first, false) {
564 write!(f, " ")?;
565 }
566 write!(f, "on {bg}")?;
567 }
568 if first {
569 write!(f, "none")?;
570 }
571 Ok(())
572 }
573}
574
575impl std::str::FromStr for Style {
576 type Err = ParseStyleError;
577
578 fn from_str(s: &str) -> Result<Style, ParseStyleError> {
579 let mut style = Style::new();
580 if s.is_empty() || s.trim().eq_ignore_ascii_case("none") {
581 return Ok(style);
582 }
583 let mut words = s.split_whitespace();
584 while let Some(token) = words.next() {
585 if token.eq_ignore_ascii_case("on") {
586 let Some(bg) = words.next().and_then(|s| s.parse::<Color>().ok()) else {
587 return Err(ParseStyleError::MissingBackground);
588 };
589 style.background = Some(bg);
590 } else if token.eq_ignore_ascii_case("not") {
591 let Some(attr) = words.next().and_then(|s| s.parse::<Attribute>().ok()) else {
592 return Err(ParseStyleError::MissingAttribute);
593 };
594 style = style.disable(attr);
595 } else if let Ok(color) = token.parse::<Color>() {
596 style.foreground = Some(color);
597 } else if let Ok(attr) = token.parse::<Attribute>() {
598 style = style.enable(attr);
599 } else {
600 return Err(ParseStyleError::Token(token.to_owned()));
601 }
602 }
603 Ok(style)
604 }
605}
606
607#[cfg(feature = "serde")]
608#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
609impl serde::Serialize for Style {
610 fn serialize<S: serde::ser::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
611 serializer.collect_str(self)
612 }
613}
614
615#[cfg(feature = "serde")]
616#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
617impl<'de> serde::Deserialize<'de> for Style {
618 fn deserialize<D: serde::de::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
619 struct Visitor;
620
621 impl serde::de::Visitor<'_> for Visitor {
622 type Value = Style;
623
624 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
625 f.write_str("a style string")
626 }
627
628 fn visit_str<E>(self, input: &str) -> Result<Self::Value, E>
629 where
630 E: serde::de::Error,
631 {
632 input
633 .parse::<Style>()
634 .map_err(|_| E::invalid_value(serde::de::Unexpected::Str(input), &self))
635 }
636 }
637
638 deserializer.deserialize_str(Visitor)
639 }
640}
641
642#[derive(Clone, Debug, Eq, Error, PartialEq)]
644pub enum ParseStyleError {
645 #[error("unexpected token in style string: {0:?}")]
647 Token(
648 String,
650 ),
651
652 #[error(r#""on" not followed by valid color word"#)]
654 MissingBackground,
655
656 #[error(r#""not" not followed by valid attribute name"#)]
658 MissingAttribute,
659}
660
661#[cfg(test)]
662mod test {
663 use super::*;
664
665 #[test]
666 fn test_new_is_default() {
667 assert_eq!(Style::new(), Style::default());
668 }
669
670 mod display {
671 use super::*;
672 use crate::Color256;
673
674 #[test]
675 fn none() {
676 assert_eq!(Style::new().to_string(), "none");
677 }
678
679 #[test]
680 fn fg_color() {
681 let style = Style::from(Color256::RED);
682 assert_eq!(style.to_string(), "red");
683 }
684
685 #[test]
686 fn bg_color() {
687 let style = Color256::RED.as_background();
688 assert_eq!(style.to_string(), "on red");
689 }
690
691 #[test]
692 fn fg_on_bg() {
693 let style = Color256::BLUE.on(Color256::RED);
694 assert_eq!(style.to_string(), "blue on red");
695 }
696
697 #[test]
698 fn attr() {
699 let style = Style::from(Attribute::Bold);
700 assert_eq!(style.to_string(), "bold");
701 }
702
703 #[test]
704 fn multiple_attrs() {
705 let style = Style::from(Attribute::Bold | Attribute::Reverse);
706 assert_eq!(style.to_string(), "bold reverse");
707 }
708
709 #[test]
710 fn not_attr() {
711 let style = Style::new().disable(Attribute::Bold);
712 assert_eq!(style.to_string(), "not bold");
713 }
714
715 #[test]
716 fn multiple_not_attrs() {
717 let style = Style::new()
718 .disable(Attribute::Bold)
719 .disable(Attribute::Reverse);
720 assert_eq!(style.to_string(), "not bold not reverse");
721 }
722
723 #[test]
724 fn attr_and_not_attr() {
725 let style = Style::from(Attribute::Bold).disable(Attribute::Blink);
726 assert_eq!(style.to_string(), "bold not blink");
727 }
728
729 #[test]
730 fn gamut() {
731 let style = Color256::YELLOW
732 .on(Color::Default)
733 .enable(Attribute::Italic)
734 .disable(Attribute::Bold);
735 assert_eq!(style.to_string(), "not bold italic yellow on default");
736 }
737
738 #[test]
739 fn all_attrs() {
740 let style = Style::from(AttributeSet::ALL);
741 assert_eq!(
742 style.to_string(),
743 "bold dim italic underline blink blink2 reverse conceal strike underline2 frame encircle overline"
744 );
745 }
746
747 #[test]
748 fn not_all_attrs() {
749 let style = Style::new().disabled_attributes(AttributeSet::ALL);
750 assert_eq!(
751 style.to_string(),
752 "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"
753 );
754 }
755 }
756
757 mod parse {
758 use super::*;
759 use crate::Color256;
760 use rstest::rstest;
761
762 #[test]
763 fn none() {
764 assert_eq!("".parse::<Style>().unwrap(), Style::new());
765 assert_eq!("none".parse::<Style>().unwrap(), Style::new());
766 assert_eq!("NONE".parse::<Style>().unwrap(), Style::new());
767 assert_eq!(" none ".parse::<Style>().unwrap(), Style::new());
768 }
769
770 #[test]
771 fn fg() {
772 assert_eq!(
773 "green".parse::<Style>().unwrap(),
774 Style::from(Color256::GREEN)
775 );
776 }
777
778 #[test]
779 fn bg() {
780 assert_eq!(
781 "on green".parse::<Style>().unwrap(),
782 Color256::GREEN.as_background()
783 );
784 assert_eq!(
785 " on green ".parse::<Style>().unwrap(),
786 Color256::GREEN.as_background()
787 );
788 assert_eq!(
789 " ON GREEN ".parse::<Style>().unwrap(),
790 Color256::GREEN.as_background()
791 );
792 }
793
794 #[test]
795 fn fg_on_bg() {
796 assert_eq!(
797 "blue on white".parse::<Style>().unwrap(),
798 Color256::BLUE.on(Color256::WHITE)
799 );
800 assert_eq!(
801 "on white blue".parse::<Style>().unwrap(),
802 Color256::BLUE.on(Color256::WHITE)
803 );
804 }
805
806 #[test]
807 fn attr() {
808 assert_eq!(
809 "bold".parse::<Style>().unwrap(),
810 Style::from(Attribute::Bold)
811 );
812 }
813
814 #[test]
815 fn multiple_attr() {
816 assert_eq!(
817 "bold underline".parse::<Style>().unwrap(),
818 Style::from(Attribute::Bold | Attribute::Underline)
819 );
820 assert_eq!(
821 "underline bold".parse::<Style>().unwrap(),
822 Style::from(Attribute::Bold | Attribute::Underline)
823 );
824 }
825
826 #[test]
827 fn not_attr() {
828 assert_eq!(
829 "not bold".parse::<Style>().unwrap(),
830 Style::new().disable(Attribute::Bold)
831 );
832 assert_eq!(
833 " NOT BOLD ".parse::<Style>().unwrap(),
834 Style::new().disable(Attribute::Bold)
835 );
836 }
837
838 #[test]
839 fn multiple_not_attrs() {
840 assert_eq!(
841 "not bold not s".parse::<Style>().unwrap(),
842 Style::new().disabled_attributes(Attribute::Bold | Attribute::Strike)
843 );
844 assert_eq!(
845 "not s not bold".parse::<Style>().unwrap(),
846 Style::new().disabled_attributes(Attribute::Bold | Attribute::Strike)
847 );
848 }
849
850 #[test]
851 fn attr_and_not_attr() {
852 assert_eq!(
853 "dim not blink2".parse::<Style>().unwrap(),
854 Style::new()
855 .enable(Attribute::Dim)
856 .disable(Attribute::Blink2)
857 );
858 assert_eq!(
859 "not blink2 dim".parse::<Style>().unwrap(),
860 Style::new()
861 .enable(Attribute::Dim)
862 .disable(Attribute::Blink2)
863 );
864 }
865
866 #[test]
867 fn gamut() {
868 for s in [
869 "bold not underline red on blue",
870 "not underline red on blue bold",
871 "on blue red not underline bold",
872 ] {
873 assert_eq!(
874 s.parse::<Style>().unwrap(),
875 Color256::RED.on(Color256::BLUE).bold().not_underline()
876 );
877 }
878 }
879
880 #[test]
881 fn multiple_fg() {
882 assert_eq!(
883 "red blue".parse::<Style>().unwrap(),
884 Style::from(Color256::BLUE)
885 );
886 }
887
888 #[test]
889 fn multiple_bg() {
890 assert_eq!(
891 "on red on blue".parse::<Style>().unwrap(),
892 Color256::BLUE.as_background()
893 );
894 }
895
896 #[test]
897 fn attr_on_and_off() {
898 assert_eq!(
899 "bold magenta not bold".parse::<Style>().unwrap(),
900 Style::from(Color256::MAGENTA).not_bold()
901 );
902 }
903
904 #[test]
905 fn attr_off_and_on() {
906 assert_eq!(
907 "not bold magenta bold".parse::<Style>().unwrap(),
908 Style::from(Color256::MAGENTA).bold()
909 );
910 }
911
912 #[rstest]
913 #[case("on bold")]
914 #[case("on foo")]
915 #[case("blue on")]
916 #[case("on")]
917 #[case("not blue")]
918 #[case("not foo")]
919 #[case("bold not")]
920 #[case("not not bold italic")]
921 #[case("not")]
922 #[case("none red")]
923 #[case("red none")]
924 #[case("foo")]
925 #[case("rgb(1, 2, 3)")]
926 #[case("bright blue")]
927 fn err(#[case] s: &str) {
928 assert!(s.parse::<Style>().is_err());
929 }
930 }
931}