line_ui/
style.rs

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