Skip to main content

farben_core/
ansi.rs

1use std::fmt::Write;
2
3use crate::lexer::EmphasisType;
4
5/// Whether a color applies to the foreground (text) or background.
6#[derive(Debug, PartialEq)]
7pub enum Ground {
8    /// Applies the color to the text itself (SGR 30-series / 38).
9    Foreground,
10    /// Applies the color to the cell background (SGR 40-series / 48).
11    Background,
12}
13
14/// A complete set of visual attributes for a span of text.
15#[derive(Default)]
16pub struct Style {
17    /// Foreground color. `None` leaves the terminal default unchanged.
18    pub fg: Option<Color>,
19    /// Background color. `None` leaves the terminal default unchanged.
20    pub bg: Option<Color>,
21    /// Bold text (SGR 1).
22    pub bold: bool,
23    /// Reduced intensity text (SGR 2).
24    pub dim: bool,
25    /// Italic text (SGR 3).
26    pub italic: bool,
27    /// Underlined text (SGR 4).
28    pub underline: bool,
29    /// Crossed-out text (SGR 9).
30    pub strikethrough: bool,
31    /// Blinking text (SGR 5). Terminal support varies.
32    pub blink: bool,
33}
34
35/// One of the eight standard ANSI named colors.
36#[derive(Debug, PartialEq)]
37pub enum NamedColor {
38    Black,
39    Red,
40    Green,
41    Yellow,
42    Blue,
43    Magenta,
44    Cyan,
45    White,
46}
47
48/// A terminal color, expressed as a named color, an ANSI 256-palette index, or an RGB triple.
49#[derive(Debug, PartialEq)]
50pub enum Color {
51    Named(NamedColor),
52    Ansi256(u8),
53    Rgb(u8, u8, u8),
54}
55
56impl NamedColor {
57    /// Parses a color name into a `NamedColor`.
58    ///
59    /// Returns `None` if the string does not match any of the eight standard names.
60    /// Matching is case-sensitive.
61    pub(crate) fn from_str(input: &str) -> Option<Self> {
62        match input {
63            "black" => Some(Self::Black),
64            "red" => Some(Self::Red),
65            "green" => Some(Self::Green),
66            "yellow" => Some(Self::Yellow),
67            "blue" => Some(Self::Blue),
68            "magenta" => Some(Self::Magenta),
69            "cyan" => Some(Self::Cyan),
70            "white" => Some(Self::White),
71            _ => None,
72        }
73    }
74}
75
76/// Joins a slice of SGR parameter bytes into a complete ANSI escape sequence.
77///
78/// Produces a string of the form `\x1b[n;n;...m`.
79fn vec_to_ansi_seq(vec: Vec<u8>) -> String {
80    let mut seq = String::from("\x1b[");
81
82    for (i, n) in vec.iter().enumerate() {
83        if i != 0 {
84            seq.push(';');
85        }
86        write!(seq, "{n}").unwrap();
87    }
88
89    seq.push('m');
90    seq
91}
92
93/// Appends the SGR parameter bytes for `color` onto `ansi`, using the correct base codes for
94/// foreground (30-series) or background (40-series) output.
95fn encode_color_sgr(ansi: &mut Vec<u8>, param: Ground, color: &Color) {
96    let addend: u8 = match param {
97        Ground::Background => 10,
98        Ground::Foreground => 0,
99    };
100    match color {
101        Color::Named(named) => {
102            ansi.push(match named {
103                NamedColor::Black => 30 + addend,
104                NamedColor::Red => 31 + addend,
105                NamedColor::Green => 32 + addend,
106                NamedColor::Yellow => 33 + addend,
107                NamedColor::Blue => 34 + addend,
108                NamedColor::Magenta => 35 + addend,
109                NamedColor::Cyan => 36 + addend,
110                NamedColor::White => 37 + addend,
111            });
112        }
113        Color::Ansi256(v) => {
114            ansi.extend_from_slice(&[38 + addend, 5, *v]);
115        }
116        Color::Rgb(r, g, b) => {
117            ansi.extend_from_slice(&[38 + addend, 2, *r, *g, *b]);
118        }
119    }
120}
121
122/// Converts a `Color` into a complete ANSI escape sequence for the given ground.
123///
124/// # Example
125/// ```ignore
126/// let seq = color_to_ansi(&Color::Named(NamedColor::Red), Ground::Foreground);
127/// assert_eq!(seq, "\x1b[31m");
128/// ```
129pub(crate) fn color_to_ansi(color: &Color, ground: Ground) -> String {
130    let mut ansi: Vec<u8> = Vec::new();
131    encode_color_sgr(&mut ansi, ground, color);
132
133    vec_to_ansi_seq(ansi)
134}
135
136/// Converts an `EmphasisType` into the corresponding SGR escape sequence.
137pub(crate) fn emphasis_to_ansi(emphasis: &EmphasisType) -> String {
138    let code = match emphasis {
139        EmphasisType::Bold => 1,
140        EmphasisType::Dim => 2,
141        EmphasisType::Italic => 3,
142        EmphasisType::Underline => 4,
143        EmphasisType::Blink => 5,
144        EmphasisType::Strikethrough => 9,
145    };
146    vec_to_ansi_seq(vec![code])
147}
148
149/// Converts a `Style` into a single combined SGR escape sequence.
150///
151/// All active attributes and colors are merged into one sequence. Returns an empty string
152/// if the style carries no active attributes and no colors.
153pub(crate) fn style_to_ansi(style: &Style) -> String {
154    let mut ansi: Vec<u8> = Vec::new();
155
156    for (enabled, code) in [
157        (style.bold, 1),
158        (style.dim, 2),
159        (style.italic, 3),
160        (style.underline, 4),
161        (style.blink, 5),
162        (style.strikethrough, 9),
163    ] {
164        if enabled {
165            ansi.push(code);
166        }
167    }
168
169    if let Some(fg) = &style.fg {
170        encode_color_sgr(&mut ansi, Ground::Foreground, fg);
171    }
172    if let Some(bg) = &style.bg {
173        encode_color_sgr(&mut ansi, Ground::Background, bg);
174    }
175
176    if ansi.is_empty() {
177        return String::new();
178    }
179
180    vec_to_ansi_seq(ansi)
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186    use crate::lexer::EmphasisType;
187
188    // --- NamedColor::from_str ---
189
190    #[test]
191    fn test_named_color_from_str_known_colors() {
192        assert_eq!(NamedColor::from_str("black"), Some(NamedColor::Black));
193        assert_eq!(NamedColor::from_str("red"), Some(NamedColor::Red));
194        assert_eq!(NamedColor::from_str("green"), Some(NamedColor::Green));
195        assert_eq!(NamedColor::from_str("yellow"), Some(NamedColor::Yellow));
196        assert_eq!(NamedColor::from_str("blue"), Some(NamedColor::Blue));
197        assert_eq!(NamedColor::from_str("magenta"), Some(NamedColor::Magenta));
198        assert_eq!(NamedColor::from_str("cyan"), Some(NamedColor::Cyan));
199        assert_eq!(NamedColor::from_str("white"), Some(NamedColor::White));
200    }
201
202    #[test]
203    fn test_named_color_from_str_unknown_returns_none() {
204        assert_eq!(NamedColor::from_str("purple"), None);
205    }
206
207    #[test]
208    fn test_named_color_from_str_case_sensitive() {
209        assert_eq!(NamedColor::from_str("Red"), None);
210        assert_eq!(NamedColor::from_str("RED"), None);
211    }
212
213    #[test]
214    fn test_named_color_from_str_empty_returns_none() {
215        assert_eq!(NamedColor::from_str(""), None);
216    }
217
218    // --- vec_to_ansi_seq ---
219
220    #[test]
221    fn test_vec_to_ansi_seq_single_param() {
222        let result = vec_to_ansi_seq(vec![1]);
223        assert_eq!(result, "\x1b[1m");
224    }
225
226    #[test]
227    fn test_vec_to_ansi_seq_multiple_params() {
228        let result = vec_to_ansi_seq(vec![1, 31]);
229        assert_eq!(result, "\x1b[1;31m");
230    }
231
232    #[test]
233    fn test_vec_to_ansi_seq_empty_produces_bare_sequence() {
234        let result = vec_to_ansi_seq(vec![]);
235        assert_eq!(result, "\x1b[m");
236    }
237
238    // --- color_to_ansi ---
239
240    #[test]
241    fn test_color_to_ansi_named_foreground() {
242        let result = color_to_ansi(&Color::Named(NamedColor::Red), Ground::Foreground);
243        assert_eq!(result, "\x1b[31m");
244    }
245
246    #[test]
247    fn test_color_to_ansi_named_background() {
248        let result = color_to_ansi(&Color::Named(NamedColor::Red), Ground::Background);
249        assert_eq!(result, "\x1b[41m");
250    }
251
252    #[test]
253    fn test_color_to_ansi_ansi256_foreground() {
254        let result = color_to_ansi(&Color::Ansi256(200), Ground::Foreground);
255        assert_eq!(result, "\x1b[38;5;200m");
256    }
257
258    #[test]
259    fn test_color_to_ansi_ansi256_background() {
260        let result = color_to_ansi(&Color::Ansi256(100), Ground::Background);
261        assert_eq!(result, "\x1b[48;5;100m");
262    }
263
264    #[test]
265    fn test_color_to_ansi_rgb_foreground() {
266        let result = color_to_ansi(&Color::Rgb(255, 128, 0), Ground::Foreground);
267        assert_eq!(result, "\x1b[38;2;255;128;0m");
268    }
269
270    #[test]
271    fn test_color_to_ansi_rgb_background() {
272        let result = color_to_ansi(&Color::Rgb(0, 0, 255), Ground::Background);
273        assert_eq!(result, "\x1b[48;2;0;0;255m");
274    }
275
276    #[test]
277    fn test_color_to_ansi_rgb_zero_values() {
278        let result = color_to_ansi(&Color::Rgb(0, 0, 0), Ground::Foreground);
279        assert_eq!(result, "\x1b[38;2;0;0;0m");
280    }
281
282    // --- emphasis_to_ansi ---
283
284    #[test]
285    fn test_emphasis_to_ansi_bold() {
286        assert_eq!(emphasis_to_ansi(&EmphasisType::Bold), "\x1b[1m");
287    }
288
289    #[test]
290    fn test_emphasis_to_ansi_dim() {
291        assert_eq!(emphasis_to_ansi(&EmphasisType::Dim), "\x1b[2m");
292    }
293
294    #[test]
295    fn test_emphasis_to_ansi_italic() {
296        assert_eq!(emphasis_to_ansi(&EmphasisType::Italic), "\x1b[3m");
297    }
298
299    #[test]
300    fn test_emphasis_to_ansi_underline() {
301        assert_eq!(emphasis_to_ansi(&EmphasisType::Underline), "\x1b[4m");
302    }
303
304    #[test]
305    fn test_emphasis_to_ansi_blink() {
306        assert_eq!(emphasis_to_ansi(&EmphasisType::Blink), "\x1b[5m");
307    }
308
309    #[test]
310    fn test_emphasis_to_ansi_strikethrough() {
311        assert_eq!(emphasis_to_ansi(&EmphasisType::Strikethrough), "\x1b[9m");
312    }
313
314    // --- style_to_ansi ---
315
316    #[test]
317    fn test_style_to_ansi_empty_style_returns_empty_string() {
318        let style = Style {
319            fg: None,
320            bg: None,
321            bold: false,
322            dim: false,
323            italic: false,
324            underline: false,
325            strikethrough: false,
326            blink: false,
327        };
328        assert_eq!(style_to_ansi(&style), "");
329    }
330
331    #[test]
332    fn test_style_to_ansi_bold_only() {
333        let style = Style {
334            fg: None,
335            bg: None,
336            bold: true,
337            dim: false,
338            italic: false,
339            underline: false,
340            strikethrough: false,
341            blink: false,
342        };
343        assert_eq!(style_to_ansi(&style), "\x1b[1m");
344    }
345
346    #[test]
347    fn test_style_to_ansi_bold_with_foreground_color() {
348        let style = Style {
349            fg: Some(Color::Named(NamedColor::Green)),
350            bg: None,
351            bold: true,
352            dim: false,
353            italic: false,
354            underline: false,
355            strikethrough: false,
356            blink: false,
357        };
358        assert_eq!(style_to_ansi(&style), "\x1b[1;32m");
359    }
360
361    #[test]
362    fn test_style_to_ansi_fg_and_bg() {
363        let style = Style {
364            fg: Some(Color::Named(NamedColor::White)),
365            bg: Some(Color::Named(NamedColor::Blue)),
366            bold: false,
367            dim: false,
368            italic: false,
369            underline: false,
370            strikethrough: false,
371            blink: false,
372        };
373        assert_eq!(style_to_ansi(&style), "\x1b[37;44m");
374    }
375
376    #[test]
377    fn test_style_to_ansi_all_emphasis_flags() {
378        let style = Style {
379            fg: None,
380            bg: None,
381            bold: true,
382            dim: true,
383            italic: true,
384            underline: true,
385            strikethrough: true,
386            blink: true,
387        };
388        assert_eq!(style_to_ansi(&style), "\x1b[1;2;3;4;5;9m");
389    }
390}
391
392// Skipped (side effects): none: all functions in ansi.rs are pure.