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 {
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 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 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 _ => (), }
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 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 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 fn from(value: Style) -> ratatui::style::Style {
502 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 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#[derive(Clone, Debug, Eq, Error, PartialEq)]
643pub enum ParseStyleError {
644 #[error("unexpected token in style string: {0:?}")]
646 Token(
647 String,
649 ),
650
651 #[error(r#""on" not followed by valid color word"#)]
653 MissingBackground,
654
655 #[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}