line_ui/
style.rs

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