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)]
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 fmt::Display for Style {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        if let Some(foreground) = self.foreground {
104            Fg(AnsiValue(foreground)).fmt(f)?;
105        }
106        if let Some(background) = self.background {
107            Bg(AnsiValue(background)).fmt(f)?;
108        }
109        if self.bold == Some(true) {
110            termion::style::Bold.fmt(f)?;
111        }
112        if self.italic == Some(true) {
113            termion::style::Italic.fmt(f)?;
114        }
115        if self.invert == Some(true) {
116            termion::style::Invert.fmt(f)?;
117        }
118        Ok(())
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use std::io::Write;
125
126    use super::*;
127
128    const STYLE_1: Style = Style {
129        foreground: Some(1),
130        bold: Some(true),
131        ..Style::EMPTY
132    };
133
134    const STYLE_2: Style = Style {
135        foreground: Some(2),
136        italic: Some(true),
137        ..Style::EMPTY
138    };
139
140    #[test]
141    fn with() {
142        let style = STYLE_1.with(STYLE_2);
143        assert_eq!(
144            style,
145            Style {
146                foreground: Some(2),
147                background: None,
148                bold: Some(true),
149                italic: Some(true),
150                invert: None
151            },
152        );
153    }
154
155    #[test]
156    fn or() {
157        let style = STYLE_1.or(STYLE_2);
158        assert_eq!(
159            style,
160            Style {
161                foreground: Some(1),
162                background: None,
163                bold: Some(true),
164                italic: Some(true),
165                invert: None
166            },
167        );
168    }
169
170    #[test]
171    fn plus() {
172        assert_eq!(STYLE_1.with(STYLE_2), STYLE_1 + STYLE_2);
173    }
174
175    #[test]
176    fn print_empty() {
177        let mut output = vec![];
178        write!(&mut output, "{}", Style::EMPTY).unwrap();
179        assert_eq!(output, b"");
180    }
181
182    #[test]
183    fn print_full() {
184        let mut output = vec![];
185        write!(
186            &mut output,
187            "{}",
188            Style {
189                foreground: Some(1),
190                background: Some(2),
191                bold: Some(true),
192                italic: Some(true),
193                invert: Some(true),
194            },
195        )
196        .unwrap();
197        assert_eq!(output, b"\x1b[38;5;1m\x1b[48;5;2m\x1b[1m\x1b[3m\x1b[7m");
198    }
199}