1mod color;
6
7use std::fmt;
8use std::ops::{Add, AddAssign};
9
10use termion::color::{AnsiValue, Bg, Fg, Reset, Rgb};
11
12pub use color::Color;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40#[non_exhaustive]
41pub struct Style {
42 pub foreground: Option<Color>,
44 pub background: Option<Color>,
46 pub bold: Option<bool>,
48 pub italic: Option<bool>,
50 pub underline: Option<bool>,
52 pub blink: Option<bool>,
54 pub invert: Option<bool>,
56 pub strikethrough: Option<bool>,
58}
59
60impl Style {
61 pub const EMPTY: Style = Style {
63 foreground: None,
64 background: None,
65 bold: None,
66 italic: None,
67 underline: None,
68 blink: None,
69 invert: None,
70 strikethrough: None,
71 };
72
73 pub const BOLD: Style = Style {
75 bold: Some(true),
76 ..Style::EMPTY
77 };
78
79 pub const ITALIC: Style = Style {
81 italic: Some(true),
82 ..Style::EMPTY
83 };
84
85 pub const UNDERLINE: Style = Style {
87 underline: Some(true),
88 ..Style::EMPTY
89 };
90
91 pub const BLINK: Style = Style {
93 blink: Some(true),
94 ..Style::EMPTY
95 };
96
97 pub const INVERT: Style = Style {
99 invert: Some(true),
100 ..Style::EMPTY
101 };
102
103 pub const STRIKETHROUGH: Style = Style {
105 strikethrough: Some(true),
106 ..Style::EMPTY
107 };
108
109 pub fn fg(color: impl Into<Color>) -> Style {
111 Style {
112 foreground: Some(color.into()),
113 ..Style::EMPTY
114 }
115 }
116
117 pub fn bg(color: impl Into<Color>) -> Style {
119 Style {
120 background: Some(color.into()),
121 ..Style::EMPTY
122 }
123 }
124
125 pub fn with(self, other: Style) -> Style {
127 other.or(self)
128 }
129
130 pub fn or(self, other: Style) -> Style {
132 Style {
133 foreground: self.foreground.or(other.foreground),
134 background: self.background.or(other.background),
135 bold: self.bold.or(other.bold),
136 italic: self.italic.or(other.italic),
137 underline: self.underline.or(other.underline),
138 blink: self.blink.or(other.blink),
139 invert: self.invert.or(other.invert),
140 strikethrough: self.strikethrough.or(other.strikethrough),
141 }
142 }
143}
144
145impl Default for Style {
146 fn default() -> Self {
147 Self::EMPTY
148 }
149}
150
151impl Add for Style {
152 type Output = Style;
153
154 fn add(self, rhs: Self) -> Self::Output {
155 self.with(rhs)
156 }
157}
158
159impl AddAssign for Style {
160 fn add_assign(&mut self, rhs: Self) {
161 *self = self.with(rhs);
162 }
163}
164
165impl fmt::Display for Style {
166 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167 if let Some(foreground) = self.foreground {
168 match foreground {
169 Color::Default => Fg(Reset).fmt(f),
170 Color::Ansi(value) => Fg(AnsiValue(value)).fmt(f),
171 Color::Rgb(r, g, b) => Fg(Rgb(r, g, b)).fmt(f),
172 }?;
173 }
174 if let Some(background) = self.background {
175 match background {
176 Color::Default => Bg(Reset).fmt(f),
177 Color::Ansi(value) => Bg(AnsiValue(value)).fmt(f),
178 Color::Rgb(r, g, b) => Bg(Rgb(r, g, b)).fmt(f),
179 }?;
180 }
181 if self.bold == Some(true) {
182 termion::style::Bold.fmt(f)?;
183 }
184 if self.italic == Some(true) {
185 termion::style::Italic.fmt(f)?;
186 }
187 if self.underline == Some(true) {
188 termion::style::Underline.fmt(f)?;
189 }
190 if self.blink == Some(true) {
191 termion::style::Blink.fmt(f)?;
192 }
193 if self.invert == Some(true) {
194 termion::style::Invert.fmt(f)?;
195 }
196 if self.strikethrough == Some(true) {
197 termion::style::CrossedOut.fmt(f)?;
198 }
199 Ok(())
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use std::io::Write;
206
207 use super::*;
208
209 const STYLE_1: Style = Style {
210 foreground: Some(Color::Ansi(1)),
211 bold: Some(true),
212 ..Style::EMPTY
213 };
214
215 const STYLE_2: Style = Style {
216 foreground: Some(Color::Ansi(2)),
217 italic: Some(true),
218 ..Style::EMPTY
219 };
220
221 #[test]
222 fn with() {
223 let style = STYLE_1.with(STYLE_2);
224 assert_eq!(
225 style,
226 Style {
227 foreground: Some(2.into()),
228 bold: Some(true),
229 italic: Some(true),
230 ..Style::EMPTY
231 },
232 );
233 }
234
235 #[test]
236 fn or() {
237 let style = STYLE_1.or(STYLE_2);
238 assert_eq!(
239 style,
240 Style {
241 foreground: Some(1.into()),
242 bold: Some(true),
243 italic: Some(true),
244 ..Style::EMPTY
245 },
246 );
247 }
248
249 #[test]
250 fn plus() {
251 assert_eq!(STYLE_1.with(STYLE_2), STYLE_1 + STYLE_2);
252 }
253
254 #[test]
255 fn print_empty() {
256 let mut output = vec![];
257 write!(&mut output, "{}", Style::EMPTY).unwrap();
258 assert_eq!(output, b"");
259 }
260
261 #[test]
262 fn print_full() {
263 let mut output = vec![];
264 write!(
265 &mut output,
266 "{}",
267 Style {
268 foreground: Some(1.into()),
269 background: Some(2.into()),
270 bold: Some(true),
271 italic: Some(true),
272 underline: Some(true),
273 blink: Some(true),
274 invert: Some(true),
275 strikethrough: Some(true),
276 },
277 )
278 .unwrap();
279 assert_eq!(
280 output,
281 b"\x1b[38;5;1m\x1b[48;5;2m\x1b[1m\x1b[3m\x1b[4m\x1b[5m\x1b[7m\x1b[9m",
282 );
283 }
284
285 #[test]
286 fn print_default_and_rgb() {
287 let mut output = vec![];
288 write!(
289 &mut output,
290 "{}",
291 Style::fg((1, 2, 3)) + Style::bg(Color::Default),
292 )
293 .unwrap();
294 assert_eq!(output, b"\x1b[38;2;1;2;3m\x1b[49m");
295 }
296}