line_ui/
style.rs

1/*
2 * Copyright (c) 2025 Jasmine Tai. All rights reserved.
3 */
4
5use std::fmt;
6use std::ops::{Add, AddAssign};
7
8use termion::color::{AnsiValue, Bg, Fg};
9
10/// A text style, encompassing the color and other style options.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13pub struct Style {
14    /// The foreground color.
15    pub foreground: Option<u8>,
16    /// The background color.
17    pub background: Option<u8>,
18    /// Whether the text should be bold.
19    pub bold: Option<bool>,
20    /// Whether the text should be italicized.
21    pub italic: Option<bool>,
22    /// Whether the text should have its colors inverted.
23    pub invert: Option<bool>,
24}
25
26impl Style {
27    /// The empty style, with nothing specified. Equivalent to `Style::default()`.
28    pub const EMPTY: Style = Style {
29        foreground: None,
30        background: None,
31        bold: None,
32        italic: None,
33        invert: None,
34    };
35
36    /// Bold text.
37    pub const BOLD: Style = Style {
38        bold: Some(true),
39        ..Style::EMPTY
40    };
41
42    /// Italicized text.
43    pub const ITALIC: Style = Style {
44        italic: Some(true),
45        ..Style::EMPTY
46    };
47
48    /// Inverted colors.
49    pub const INVERT: Style = Style {
50        invert: Some(true),
51        ..Style::EMPTY
52    };
53
54    /// Creates a style with only the foreground specified.
55    pub fn fg(value: u8) -> Style {
56        Style {
57            foreground: Some(value),
58            ..Style::EMPTY
59        }
60    }
61
62    /// Creates a style with only the background specified.
63    pub fn bg(value: u8) -> Style {
64        Style {
65            background: Some(value),
66            ..Style::EMPTY
67        }
68    }
69
70    /// Merges two styles, with `other` taking precedence.
71    pub fn with(self, other: Style) -> Style {
72        other.or(self)
73    }
74
75    /// Merges two styles, with `self` taking precedence.
76    pub fn or(self, other: Style) -> Style {
77        Style {
78            foreground: self.foreground.or(other.foreground),
79            background: self.background.or(other.background),
80            bold: self.bold.or(other.bold),
81            italic: self.italic.or(other.italic),
82            invert: self.invert.or(other.invert),
83        }
84    }
85}
86
87impl Default for Style {
88    fn default() -> Self {
89        Self::EMPTY
90    }
91}
92
93impl Add for Style {
94    type Output = Style;
95
96    fn add(self, rhs: Self) -> Self::Output {
97        self.with(rhs)
98    }
99}
100
101impl AddAssign for Style {
102    fn add_assign(&mut self, rhs: Self) {
103        *self = *self + rhs;
104    }
105}
106
107impl fmt::Display for Style {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        if let Some(foreground) = self.foreground {
110            Fg(AnsiValue(foreground)).fmt(f)?;
111        }
112        if let Some(background) = self.background {
113            Bg(AnsiValue(background)).fmt(f)?;
114        }
115        if self.bold == Some(true) {
116            termion::style::Bold.fmt(f)?;
117        }
118        if self.italic == Some(true) {
119            termion::style::Italic.fmt(f)?;
120        }
121        if self.invert == Some(true) {
122            termion::style::Invert.fmt(f)?;
123        }
124        Ok(())
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use std::io::Write;
131
132    use super::*;
133
134    const STYLE_1: Style = Style {
135        foreground: Some(1),
136        bold: Some(true),
137        ..Style::EMPTY
138    };
139
140    const STYLE_2: Style = Style {
141        foreground: Some(2),
142        italic: Some(true),
143        ..Style::EMPTY
144    };
145
146    #[test]
147    fn with() {
148        let style = STYLE_1.with(STYLE_2);
149        assert_eq!(
150            style,
151            Style {
152                foreground: Some(2),
153                background: None,
154                bold: Some(true),
155                italic: Some(true),
156                invert: None
157            },
158        );
159    }
160
161    #[test]
162    fn or() {
163        let style = STYLE_1.or(STYLE_2);
164        assert_eq!(
165            style,
166            Style {
167                foreground: Some(1),
168                background: None,
169                bold: Some(true),
170                italic: Some(true),
171                invert: None
172            },
173        );
174    }
175
176    #[test]
177    fn plus() {
178        assert_eq!(STYLE_1.with(STYLE_2), STYLE_1 + STYLE_2);
179    }
180
181    #[test]
182    fn print_empty() {
183        let mut output = vec![];
184        write!(&mut output, "{}", Style::EMPTY).unwrap();
185        assert_eq!(output, b"");
186    }
187
188    #[test]
189    fn print_full() {
190        let mut output = vec![];
191        write!(
192            &mut output,
193            "{}",
194            Style {
195                foreground: Some(1),
196                background: Some(2),
197                bold: Some(true),
198                italic: Some(true),
199                invert: Some(true),
200            },
201        )
202        .unwrap();
203        assert_eq!(output, b"\x1b[38;5;1m\x1b[48;5;2m\x1b[1m\x1b[3m\x1b[7m");
204    }
205}