1use crate::color::Color;
4
5#[derive(Clone, Debug, Default, PartialEq, Eq)]
7pub struct Style {
8 pub fg: Option<Color>,
10 pub bg: Option<Color>,
12 pub bold: bool,
14 pub italic: bool,
16 pub underline: bool,
18 pub strikethrough: bool,
20 pub dim: bool,
22 pub reverse: bool,
24 pub link: Option<String>,
26}
27
28impl Style {
29 pub fn new() -> Self {
31 Self::default()
32 }
33
34 #[must_use]
36 pub fn fg(mut self, color: Color) -> Self {
37 self.fg = Some(color);
38 self
39 }
40
41 #[must_use]
43 pub fn bg(mut self, color: Color) -> Self {
44 self.bg = Some(color);
45 self
46 }
47
48 #[must_use]
50 pub fn bold(mut self, val: bool) -> Self {
51 self.bold = val;
52 self
53 }
54
55 #[must_use]
57 pub fn italic(mut self, val: bool) -> Self {
58 self.italic = val;
59 self
60 }
61
62 #[must_use]
64 pub fn underline(mut self, val: bool) -> Self {
65 self.underline = val;
66 self
67 }
68
69 #[must_use]
71 pub fn strikethrough(mut self, val: bool) -> Self {
72 self.strikethrough = val;
73 self
74 }
75
76 #[must_use]
78 pub fn dim(mut self, val: bool) -> Self {
79 self.dim = val;
80 self
81 }
82
83 #[must_use]
85 pub fn reverse(mut self, val: bool) -> Self {
86 self.reverse = val;
87 self
88 }
89
90 #[must_use]
92 pub fn link(mut self, url: impl Into<String>) -> Self {
93 self.link = Some(url.into());
94 self
95 }
96
97 #[must_use]
100 pub fn merge(&self, other: &Style) -> Style {
101 Style {
102 fg: other.fg.clone().or_else(|| self.fg.clone()),
103 bg: other.bg.clone().or_else(|| self.bg.clone()),
104 bold: if other.bold { true } else { self.bold },
105 italic: if other.italic { true } else { self.italic },
106 underline: if other.underline {
107 true
108 } else {
109 self.underline
110 },
111 strikethrough: if other.strikethrough {
112 true
113 } else {
114 self.strikethrough
115 },
116 dim: if other.dim { true } else { self.dim },
117 reverse: if other.reverse { true } else { self.reverse },
118 link: other.link.clone().or_else(|| self.link.clone()),
119 }
120 }
121
122 pub fn is_empty(&self) -> bool {
124 *self == Self::default()
125 }
126}
127
128impl From<&Style> for crossterm::style::ContentStyle {
129 fn from(style: &Style) -> Self {
130 use crossterm::style::{Attribute, ContentStyle};
131
132 let mut cs = ContentStyle::new();
133 if let Some(ref fg) = style.fg {
134 cs.foreground_color = Some(fg.into());
135 }
136 if let Some(ref bg) = style.bg {
137 cs.background_color = Some(bg.into());
138 }
139 if style.bold {
140 cs.attributes.set(Attribute::Bold);
141 }
142 if style.italic {
143 cs.attributes.set(Attribute::Italic);
144 }
145 if style.underline {
146 cs.attributes.set(Attribute::Underlined);
147 }
148 if style.strikethrough {
149 cs.attributes.set(Attribute::CrossedOut);
150 }
151 if style.dim {
152 cs.attributes.set(Attribute::Dim);
153 }
154 if style.reverse {
155 cs.attributes.set(Attribute::Reverse);
156 }
157 cs
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use crate::color::NamedColor;
165
166 #[test]
167 fn builder_pattern() {
168 let s = Style::new()
169 .fg(Color::Named(NamedColor::Red))
170 .bold(true)
171 .italic(true);
172 assert_eq!(s.fg, Some(Color::Named(NamedColor::Red)));
173 assert!(s.bold);
174 assert!(s.italic);
175 assert!(!s.underline);
176 }
177
178 #[test]
179 fn default_is_empty() {
180 assert!(Style::new().is_empty());
181 }
182
183 #[test]
184 fn non_empty_style() {
185 assert!(!Style::new().bold(true).is_empty());
186 }
187
188 #[test]
189 fn merge_fg_override() {
190 let base = Style::new().fg(Color::Named(NamedColor::Red));
191 let over = Style::new().fg(Color::Named(NamedColor::Blue));
192 let merged = base.merge(&over);
193 assert_eq!(merged.fg, Some(Color::Named(NamedColor::Blue)));
194 }
195
196 #[test]
197 fn merge_preserves_base() {
198 let base = Style::new().fg(Color::Named(NamedColor::Red)).bold(true);
199 let over = Style::new().italic(true);
200 let merged = base.merge(&over);
201 assert_eq!(merged.fg, Some(Color::Named(NamedColor::Red)));
202 assert!(merged.bold);
203 assert!(merged.italic);
204 }
205
206 #[test]
207 fn crossterm_conversion() {
208 let s = Style::new().fg(Color::Rgb { r: 1, g: 2, b: 3 }).bold(true);
209 let cs: crossterm::style::ContentStyle = (&s).into();
210 assert_eq!(
211 cs.foreground_color,
212 Some(crossterm::style::Color::Rgb { r: 1, g: 2, b: 3 })
213 );
214 }
215}