line_ui/
style.rs

1/*
2 * Copyright (c) 2025 Jasmine Tai. All rights reserved.
3 */
4
5mod 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/// A text style, encompassing the color and other style options.
15///
16/// Each field of this struct is an [`Option`]. When the value is [`None`], then
17/// the particular field is *unspecified*. By default, this is the same as
18/// setting it to <code>[Some]\([Default::default]\())</code>. However, two
19/// style can be *merged* with either [`Style::with`] (or the `+` operator) or
20/// [`Style::or`]. This allows another style to override certain fields only if
21/// they are unspecified.
22///
23/// # Example
24///
25/// ```
26/// use line_ui::Style;
27///
28/// let style1 = Style::fg(1);
29/// let style2 = Style::fg(2) + Style::BOLD;
30///
31/// assert_eq!(style1 + style2, style2);
32/// assert_eq!(style1.with(style2), style2); // `with` is equivalent to `+`
33///
34/// assert_eq!(style2 + style1, Style::fg(1) + Style::BOLD);
35/// assert_eq!(style1.or(style2), Style::fg(1) + Style::BOLD);
36/// // `or` is equivalent to `+` with operands flipped
37/// ```
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40#[non_exhaustive]
41pub struct Style {
42    /// The foreground color.
43    pub foreground: Option<Color>,
44    /// The background color.
45    pub background: Option<Color>,
46    /// Whether the text should be bold.
47    pub bold: Option<bool>,
48    /// Whether the text should be italicized.
49    pub italic: Option<bool>,
50    /// Whether the text should be underlined.
51    pub underline: Option<bool>,
52    /// Whether the text should be blinking (not widely supported).
53    pub blink: Option<bool>,
54    /// Whether the text should have its colors inverted.
55    pub invert: Option<bool>,
56    /// Whether the text should be crossed out (not widely supported).
57    pub strikethrough: Option<bool>,
58}
59
60impl Style {
61    /// The empty style, with nothing specified. Equivalent to `Style::default()`.
62    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    /// Bold text.
74    pub const BOLD: Style = Style {
75        bold: Some(true),
76        ..Style::EMPTY
77    };
78
79    /// Italicized text.
80    pub const ITALIC: Style = Style {
81        italic: Some(true),
82        ..Style::EMPTY
83    };
84
85    /// Underlined text.
86    pub const UNDERLINE: Style = Style {
87        underline: Some(true),
88        ..Style::EMPTY
89    };
90
91    /// Blinking text (not widely supported).
92    pub const BLINK: Style = Style {
93        blink: Some(true),
94        ..Style::EMPTY
95    };
96
97    /// Inverted colors.
98    pub const INVERT: Style = Style {
99        invert: Some(true),
100        ..Style::EMPTY
101    };
102
103    /// Crossed-out text (not widely supported).
104    pub const STRIKETHROUGH: Style = Style {
105        strikethrough: Some(true),
106        ..Style::EMPTY
107    };
108
109    /// Creates a style with only the foreground specified.
110    pub fn fg(color: impl Into<Color>) -> Style {
111        Style {
112            foreground: Some(color.into()),
113            ..Style::EMPTY
114        }
115    }
116
117    /// Creates a style with only the background specified.
118    pub fn bg(color: impl Into<Color>) -> Style {
119        Style {
120            background: Some(color.into()),
121            ..Style::EMPTY
122        }
123    }
124
125    /// Merges two styles, with `other` taking precedence.
126    pub fn with(self, other: Style) -> Style {
127        other.or(self)
128    }
129
130    /// Merges two styles, with `self` taking precedence.
131    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}