ratatui_toolkit/primitives/termtui/
attrs.rs

1//! Terminal text attributes (colors and styles)
2
3use ratatui::style::{Color as RatatuiColor, Modifier, Style};
4use termwiz::color::ColorSpec;
5
6/// Terminal color
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
8pub enum Color {
9    /// Default terminal color
10    #[default]
11    Default,
12    /// Indexed color (0-255)
13    Indexed(u8),
14    /// RGB true color
15    Rgb(u8, u8, u8),
16}
17
18impl Color {
19    /// Convert to ratatui color
20    pub fn to_ratatui(self) -> Option<RatatuiColor> {
21        match self {
22            Color::Default => None,
23            Color::Indexed(idx) => Some(match idx {
24                0 => RatatuiColor::Black,
25                1 => RatatuiColor::Red,
26                2 => RatatuiColor::Green,
27                3 => RatatuiColor::Yellow,
28                4 => RatatuiColor::Blue,
29                5 => RatatuiColor::Magenta,
30                6 => RatatuiColor::Cyan,
31                7 => RatatuiColor::Gray,
32                8 => RatatuiColor::DarkGray,
33                9 => RatatuiColor::LightRed,
34                10 => RatatuiColor::LightGreen,
35                11 => RatatuiColor::LightYellow,
36                12 => RatatuiColor::LightBlue,
37                13 => RatatuiColor::LightMagenta,
38                14 => RatatuiColor::LightCyan,
39                15 => RatatuiColor::White,
40                _ => RatatuiColor::Indexed(idx),
41            }),
42            Color::Rgb(r, g, b) => Some(RatatuiColor::Rgb(r, g, b)),
43        }
44    }
45}
46
47impl From<ColorSpec> for Color {
48    fn from(spec: ColorSpec) -> Self {
49        match spec {
50            ColorSpec::Default => Color::Default,
51            ColorSpec::PaletteIndex(idx) => Color::Indexed(idx),
52            ColorSpec::TrueColor(srgba) => Color::Rgb(
53                (srgba.0 * 255.0) as u8,
54                (srgba.1 * 255.0) as u8,
55                (srgba.2 * 255.0) as u8,
56            ),
57        }
58    }
59}
60
61// Text mode bit flags
62const TEXT_MODE_BOLD: u8 = 1 << 0;
63const TEXT_MODE_ITALIC: u8 = 1 << 1;
64const TEXT_MODE_UNDERLINE: u8 = 1 << 2;
65const TEXT_MODE_INVERSE: u8 = 1 << 3;
66const TEXT_MODE_STRIKETHROUGH: u8 = 1 << 4;
67
68/// Terminal cell attributes
69#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
70pub struct Attrs {
71    /// Foreground color
72    pub fg: Color,
73    /// Background color
74    pub bg: Color,
75    /// Text mode flags
76    mode: u8,
77}
78
79impl Attrs {
80    /// Create new attributes with default colors
81    pub fn new() -> Self {
82        Self::default()
83    }
84
85    /// Check if bold
86    pub fn bold(&self) -> bool {
87        self.mode & TEXT_MODE_BOLD != 0
88    }
89
90    /// Set bold
91    pub fn set_bold(&mut self, on: bool) {
92        if on {
93            self.mode |= TEXT_MODE_BOLD;
94        } else {
95            self.mode &= !TEXT_MODE_BOLD;
96        }
97    }
98
99    /// Check if italic
100    pub fn italic(&self) -> bool {
101        self.mode & TEXT_MODE_ITALIC != 0
102    }
103
104    /// Set italic
105    pub fn set_italic(&mut self, on: bool) {
106        if on {
107            self.mode |= TEXT_MODE_ITALIC;
108        } else {
109            self.mode &= !TEXT_MODE_ITALIC;
110        }
111    }
112
113    /// Check if underline
114    pub fn underline(&self) -> bool {
115        self.mode & TEXT_MODE_UNDERLINE != 0
116    }
117
118    /// Set underline
119    pub fn set_underline(&mut self, on: bool) {
120        if on {
121            self.mode |= TEXT_MODE_UNDERLINE;
122        } else {
123            self.mode &= !TEXT_MODE_UNDERLINE;
124        }
125    }
126
127    /// Check if inverse (swap fg/bg)
128    pub fn inverse(&self) -> bool {
129        self.mode & TEXT_MODE_INVERSE != 0
130    }
131
132    /// Set inverse
133    pub fn set_inverse(&mut self, on: bool) {
134        if on {
135            self.mode |= TEXT_MODE_INVERSE;
136        } else {
137            self.mode &= !TEXT_MODE_INVERSE;
138        }
139    }
140
141    /// Check if strikethrough
142    pub fn strikethrough(&self) -> bool {
143        self.mode & TEXT_MODE_STRIKETHROUGH != 0
144    }
145
146    /// Set strikethrough
147    pub fn set_strikethrough(&mut self, on: bool) {
148        if on {
149            self.mode |= TEXT_MODE_STRIKETHROUGH;
150        } else {
151            self.mode &= !TEXT_MODE_STRIKETHROUGH;
152        }
153    }
154
155    /// Reset all attributes to default
156    pub fn reset(&mut self) {
157        *self = Self::default();
158    }
159
160    /// Convert to ratatui style
161    pub fn to_ratatui(&self) -> Style {
162        let mut style = Style::default();
163
164        // Apply colors (handle inverse)
165        let (fg, bg) = if self.inverse() {
166            (self.bg, self.fg)
167        } else {
168            (self.fg, self.bg)
169        };
170
171        if let Some(color) = fg.to_ratatui() {
172            style = style.fg(color);
173        }
174        if let Some(color) = bg.to_ratatui() {
175            style = style.bg(color);
176        }
177
178        // Apply modifiers
179        let mut modifiers = Modifier::empty();
180        if self.bold() {
181            modifiers |= Modifier::BOLD;
182        }
183        if self.italic() {
184            modifiers |= Modifier::ITALIC;
185        }
186        if self.underline() {
187            modifiers |= Modifier::UNDERLINED;
188        }
189        if self.strikethrough() {
190            modifiers |= Modifier::CROSSED_OUT;
191        }
192
193        if !modifiers.is_empty() {
194            style = style.add_modifier(modifiers);
195        }
196
197        style
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn test_color_default() {
207        let color = Color::Default;
208        assert_eq!(color.to_ratatui(), None);
209    }
210
211    #[test]
212    fn test_color_indexed() {
213        assert_eq!(Color::Indexed(0).to_ratatui(), Some(RatatuiColor::Black));
214        assert_eq!(Color::Indexed(1).to_ratatui(), Some(RatatuiColor::Red));
215        assert_eq!(
216            Color::Indexed(100).to_ratatui(),
217            Some(RatatuiColor::Indexed(100))
218        );
219    }
220
221    #[test]
222    fn test_color_rgb() {
223        assert_eq!(
224            Color::Rgb(255, 128, 64).to_ratatui(),
225            Some(RatatuiColor::Rgb(255, 128, 64))
226        );
227    }
228
229    #[test]
230    fn test_attrs_default() {
231        let attrs = Attrs::default();
232        assert!(!attrs.bold());
233        assert!(!attrs.italic());
234        assert!(!attrs.underline());
235        assert!(!attrs.inverse());
236    }
237
238    #[test]
239    fn test_attrs_set_modes() {
240        let mut attrs = Attrs::new();
241
242        attrs.set_bold(true);
243        assert!(attrs.bold());
244
245        attrs.set_italic(true);
246        assert!(attrs.italic());
247
248        attrs.set_underline(true);
249        assert!(attrs.underline());
250
251        attrs.set_inverse(true);
252        assert!(attrs.inverse());
253
254        attrs.reset();
255        assert!(!attrs.bold());
256        assert!(!attrs.italic());
257    }
258}