Skip to main content

farben_core/
ansi.rs

1//! ANSI SGR escape sequence encoding for farben styles.
2//!
3//! Converts the typed color and style representations ([`Color`], [`Style`], [`NamedColor`])
4//! into raw terminal escape sequences. All functions in this module are pure: they take
5//! values and return strings with no side effects.
6//!
7//! Entry points for callers outside this module are [`color_to_ansi`], [`emphasis_to_ansi`],
8//! and [`style_to_ansi`]. [`Style::parse`] constructs a `Style` directly from a farben
9//! markup string.
10
11use std::fmt::Write;
12
13use crate::errors::LexError;
14use crate::lexer::{tokenize, EmphasisType, TagType, Token};
15
16/// Whether a color applies to the foreground (text) or background.
17#[derive(Debug, PartialEq, Clone)]
18pub enum Ground {
19    /// Applies the color to the text itself (SGR 30-series / 38).
20    Foreground,
21    /// Applies the color to the cell background (SGR 40-series / 48).
22    Background,
23}
24
25/// A complete set of visual attributes for a span of text.
26#[derive(Default, Clone, Debug)]
27pub struct Style {
28    /// Foreground color. `None` leaves the terminal default unchanged.
29    pub fg: Option<Color>,
30    /// Background color. `None` leaves the terminal default unchanged.
31    pub bg: Option<Color>,
32    /// Bold text (SGR 1). Increased intensity.
33    pub bold: bool,
34    /// Reduced intensity text (SGR 2). Lower intensity.
35    pub dim: bool,
36    /// Italic text (SGR 3). Slanted text.
37    pub italic: bool,
38    /// Underlined text (SGR 4). Single underline.
39    pub underline: bool,
40    /// Double-underlined text (SGR 21). Two lines.
41    pub double_underline: bool,
42    /// Crossed-out text (SGR 9). Strikethrough.
43    pub strikethrough: bool,
44    /// Blinking text (SGR 5). Slow blink.
45    pub blink: bool,
46    /// Overlined text (SGR 53).
47    pub overline: bool,
48    /// Invisible text (SGR 8). Text is hidden but selectable.
49    pub invisible: bool,
50    /// Reverse video (SGR 7). Swaps foreground and background.
51    pub reverse: bool,
52    /// Rapid blinking (SGR 6). Faster than Blink. Terminal support varies.
53    pub rapid_blink: bool,
54    /// Full reset. Enabling this option overrides all previous options.
55    pub reset: bool,
56    /// Optional prefix string prepended before the style's escape sequence.
57    pub prefix: Option<String>,
58}
59
60/// One of the eight standard ANSI named colors.
61#[derive(Debug, PartialEq, Clone)]
62pub enum NamedColor {
63    /// Black (30/40).
64    Black,
65    /// Red (31/41).
66    Red,
67    /// Green (32/42).
68    Green,
69    /// Yellow (33/43).
70    Yellow,
71    /// Blue (34/44).
72    Blue,
73    /// Magenta (35/45).
74    Magenta,
75    /// Cyan (36/46).
76    Cyan,
77    /// White (37/47).
78    White,
79    /// Bright black (90/100).
80    BrightBlack,
81    /// Bright red (91/101).
82    BrightRed,
83    /// Bright green (92/102).
84    BrightGreen,
85    /// Bright yellow (93/103).
86    BrightYellow,
87    /// Bright blue (94/104).
88    BrightBlue,
89    /// Bright magenta (95/105).
90    BrightMagenta,
91    /// Bright cyan (96/106).
92    BrightCyan,
93    /// Bright white (97/107).
94    BrightWhite,
95}
96
97/// A terminal color, expressed as a named color, an ANSI 256-palette index, or an RGB triple.
98#[derive(Debug, PartialEq, Clone)]
99pub enum Color {
100    /// A named ANSI color.
101    Named(NamedColor),
102    /// A 256-palette index (0-255).
103    Ansi256(u8),
104    /// An RGB triple (0-255 each).
105    Rgb(u8, u8, u8),
106}
107
108impl Style {
109    /// Parses a farben markup string into a `Style`.
110    ///
111    /// Tokenizes `markup` and folds the resulting tags into a single `Style` value.
112    /// Text tokens are ignored; only tag tokens affect the output.
113    ///
114    /// # Errors
115    ///
116    /// Returns a [`LexError`] if `markup` contains an unclosed tag, an unrecognized tag
117    /// name, or an invalid color argument.
118    ///
119    /// # Example
120    ///
121    /// ```ignore
122    /// let style = Style::parse("[bold red]")?;
123    /// assert!(style.bold);
124    /// assert_eq!(style.fg, Some(Color::Named(NamedColor::Red)));
125    /// ```
126    pub fn parse(markup: impl Into<String>) -> Result<Self, LexError> {
127        let mut res = Self {
128            ..Default::default()
129        };
130        for tok in tokenize(markup.into())? {
131            match tok {
132                Token::Text(_) => continue,
133                Token::Tag(tag) => match tag {
134                    TagType::ResetAll | TagType::ResetOne(_) => res.reset = true,
135                    TagType::Emphasis(emphasis) => match emphasis {
136                        EmphasisType::Dim => res.dim = true,
137                        EmphasisType::Blink => res.blink = true,
138                        EmphasisType::Bold => res.bold = true,
139                        EmphasisType::Italic => res.italic = true,
140                        EmphasisType::Strikethrough => res.strikethrough = true,
141                        EmphasisType::Underline => res.underline = true,
142                        EmphasisType::DoubleUnderline => res.double_underline = true,
143                        EmphasisType::Overline => res.overline = true,
144                        EmphasisType::Invisible => res.invisible = true,
145                        EmphasisType::Reverse => res.reverse = true,
146                        EmphasisType::RapidBlink => res.rapid_blink = true,
147                    },
148                    TagType::Color { color, ground } => match ground {
149                        Ground::Background => res.bg = Some(color),
150                        Ground::Foreground => res.fg = Some(color),
151                    },
152                    TagType::Prefix(_) => continue,
153                },
154            }
155        }
156
157        Ok(res)
158    }
159}
160
161impl NamedColor {
162    /// Parses a color name into a `NamedColor`.
163    ///
164    /// Returns `None` if the string does not match any of the eight standard names.
165    /// Matching is case-sensitive.
166    pub(crate) fn from_str(input: &str) -> Option<Self> {
167        match input {
168            "black" => Some(Self::Black),
169            "red" => Some(Self::Red),
170            "green" => Some(Self::Green),
171            "yellow" => Some(Self::Yellow),
172            "blue" => Some(Self::Blue),
173            "magenta" => Some(Self::Magenta),
174            "cyan" => Some(Self::Cyan),
175            "white" => Some(Self::White),
176            "bright-black" => Some(Self::BrightBlack),
177            "bright-red" => Some(Self::BrightRed),
178            "bright-green" => Some(Self::BrightGreen),
179            "bright-yellow" => Some(Self::BrightYellow),
180            "bright-blue" => Some(Self::BrightBlue),
181            "bright-magenta" => Some(Self::BrightMagenta),
182            "bright-cyan" => Some(Self::BrightCyan),
183            "bright-white" => Some(Self::BrightWhite),
184            _ => None,
185        }
186    }
187}
188
189/// Joins a slice of SGR parameter bytes into a complete ANSI escape sequence.
190///
191/// Produces a string of the form `\x1b[n;n;...m`. An empty `vec` produces `\x1b[m`.
192fn vec_to_ansi_seq(vec: Vec<u8>) -> String {
193    let mut seq = String::from("\x1b[");
194
195    for (i, n) in vec.iter().enumerate() {
196        if i != 0 {
197            seq.push(';');
198        }
199        write!(seq, "{n}").unwrap();
200    }
201
202    seq.push('m');
203    seq
204}
205
206/// Appends the SGR parameter bytes for `color` onto `ansi`, using the correct base codes for
207/// foreground (30-series) or background (40-series) output.
208fn encode_color_sgr(ansi: &mut Vec<u8>, param: Ground, color: &Color) {
209    let addend: u8 = match param {
210        Ground::Background => 10,
211        Ground::Foreground => 0,
212    };
213    match color {
214        Color::Named(named) => {
215            ansi.push(match named {
216                NamedColor::Black => 30 + addend,
217                NamedColor::Red => 31 + addend,
218                NamedColor::Green => 32 + addend,
219                NamedColor::Yellow => 33 + addend,
220                NamedColor::Blue => 34 + addend,
221                NamedColor::Magenta => 35 + addend,
222                NamedColor::Cyan => 36 + addend,
223                NamedColor::White => 37 + addend,
224                NamedColor::BrightBlack => 90 + addend,
225                NamedColor::BrightRed => 91 + addend,
226                NamedColor::BrightGreen => 92 + addend,
227                NamedColor::BrightYellow => 93 + addend,
228                NamedColor::BrightBlue => 94 + addend,
229                NamedColor::BrightMagenta => 95 + addend,
230                NamedColor::BrightCyan => 96 + addend,
231                NamedColor::BrightWhite => 97 + addend,
232            });
233        }
234        Color::Ansi256(v) => {
235            ansi.extend_from_slice(&[38 + addend, 5, *v]);
236        }
237        Color::Rgb(r, g, b) => {
238            ansi.extend_from_slice(&[38 + addend, 2, *r, *g, *b]);
239        }
240    }
241}
242
243/// Returns the base SGR code for a named color (foreground basis; add 10 for background).
244const fn named_sgr(color: &NamedColor) -> u8 {
245    match color {
246        NamedColor::Black => 30,
247        NamedColor::Red => 31,
248        NamedColor::Green => 32,
249        NamedColor::Yellow => 33,
250        NamedColor::Blue => 34,
251        NamedColor::Magenta => 35,
252        NamedColor::Cyan => 36,
253        NamedColor::White => 37,
254        NamedColor::BrightBlack => 90,
255        NamedColor::BrightRed => 91,
256        NamedColor::BrightGreen => 92,
257        NamedColor::BrightYellow => 93,
258        NamedColor::BrightBlue => 94,
259        NamedColor::BrightMagenta => 95,
260        NamedColor::BrightCyan => 96,
261        NamedColor::BrightWhite => 97,
262    }
263}
264
265/// Converts a `Color` into a complete ANSI escape sequence for the given ground.
266///
267/// Returns an ANSI escape code like `"\x1b[31m"` for red foreground.
268///
269/// # Example
270///
271/// ```ignore
272/// let seq = color_to_ansi(&Color::Named(NamedColor::Red), Ground::Foreground);
273/// assert_eq!(seq, "\x1b[31m");
274/// ```
275pub fn color_to_ansi(color: &Color, ground: Ground) -> String {
276    let add: u8 = match ground {
277        Ground::Background => 10,
278        Ground::Foreground => 0,
279    };
280    match color {
281        Color::Named(n) => format!("\x1b[{}m", named_sgr(n) + add),
282        Color::Ansi256(v) => format!("\x1b[{};5;{}m", 38 + add, v),
283        Color::Rgb(r, g, b) => format!("\x1b[{};2;{};{};{}m", 38 + add, r, g, b),
284    }
285}
286
287/// Converts an `EmphasisType` into the corresponding SGR escape sequence.
288///
289/// Returns an ANSI escape code like `"\x1b[1m"` for bold.
290pub fn emphasis_to_ansi(emphasis: &EmphasisType) -> String {
291    let code: u8 = match emphasis {
292        EmphasisType::Bold => 1,
293        EmphasisType::Dim => 2,
294        EmphasisType::Italic => 3,
295        EmphasisType::Underline => 4,
296        EmphasisType::DoubleUnderline => 21,
297        EmphasisType::Blink => 5,
298        EmphasisType::RapidBlink => 6,
299        EmphasisType::Reverse => 7,
300        EmphasisType::Invisible => 8,
301        EmphasisType::Strikethrough => 9,
302        EmphasisType::Overline => 53,
303    };
304    format!("\x1b[{}m", code)
305}
306
307/// Converts a `Style` into a single combined SGR escape sequence.
308///
309/// All active attributes and colors are merged into one sequence. Returns an empty string
310/// if the style carries no active attributes and no colors.
311///
312/// A `reset` style short-circuits to `\x1b[0m` regardless of any other fields.
313///
314/// # Example
315///
316/// ```
317/// use farben_core::ansi::{Style, Color, NamedColor, style_to_ansi};
318///
319/// let style = Style {
320///     bold: true,
321///     fg: Some(Color::Named(NamedColor::Red)),
322///     ..Default::default()
323/// };
324/// assert_eq!(style_to_ansi(&style), "\x1b[1;31m");
325/// ```
326pub fn style_to_ansi(style: &Style) -> String {
327    let mut ansi: Vec<u8> = Vec::new();
328
329    if style.reset {
330        return String::from("\x1b[0m");
331    }
332
333    for (enabled, code) in [
334        (style.bold, 1),
335        (style.dim, 2),
336        (style.italic, 3),
337        (style.underline, 4),
338        (style.double_underline, 21),
339        (style.blink, 5),
340        (style.rapid_blink, 6),
341        (style.reverse, 7),
342        (style.invisible, 8),
343        (style.strikethrough, 9),
344        (style.overline, 53),
345    ] {
346        if enabled {
347            ansi.push(code);
348        }
349    }
350
351    if let Some(fg) = &style.fg {
352        encode_color_sgr(&mut ansi, Ground::Foreground, fg);
353    }
354    if let Some(bg) = &style.bg {
355        encode_color_sgr(&mut ansi, Ground::Background, bg);
356    }
357
358    if ansi.is_empty() {
359        return String::new();
360    }
361
362    vec_to_ansi_seq(ansi)
363}
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368    use crate::lexer::EmphasisType;
369
370    // --- NamedColor::from_str ---
371
372    #[test]
373    fn test_named_color_from_str_known_colors() {
374        assert_eq!(NamedColor::from_str("black"), Some(NamedColor::Black));
375        assert_eq!(NamedColor::from_str("red"), Some(NamedColor::Red));
376        assert_eq!(NamedColor::from_str("green"), Some(NamedColor::Green));
377        assert_eq!(NamedColor::from_str("yellow"), Some(NamedColor::Yellow));
378        assert_eq!(NamedColor::from_str("blue"), Some(NamedColor::Blue));
379        assert_eq!(NamedColor::from_str("magenta"), Some(NamedColor::Magenta));
380        assert_eq!(NamedColor::from_str("cyan"), Some(NamedColor::Cyan));
381        assert_eq!(NamedColor::from_str("white"), Some(NamedColor::White));
382    }
383
384    #[test]
385    fn test_named_color_from_str_unknown_returns_none() {
386        assert_eq!(NamedColor::from_str("purple"), None);
387    }
388
389    #[test]
390    fn test_named_color_from_str_case_sensitive() {
391        assert_eq!(NamedColor::from_str("Red"), None);
392        assert_eq!(NamedColor::from_str("RED"), None);
393    }
394
395    #[test]
396    fn test_named_color_from_str_empty_returns_none() {
397        assert_eq!(NamedColor::from_str(""), None);
398    }
399
400    // --- vec_to_ansi_seq ---
401
402    #[test]
403    fn test_vec_to_ansi_seq_single_param() {
404        let result = vec_to_ansi_seq(vec![1]);
405        assert_eq!(result, "\x1b[1m");
406    }
407
408    #[test]
409    fn test_vec_to_ansi_seq_multiple_params() {
410        let result = vec_to_ansi_seq(vec![1, 31]);
411        assert_eq!(result, "\x1b[1;31m");
412    }
413
414    #[test]
415    fn test_vec_to_ansi_seq_empty_produces_bare_sequence() {
416        let result = vec_to_ansi_seq(vec![]);
417        assert_eq!(result, "\x1b[m");
418    }
419
420    // --- color_to_ansi ---
421
422    #[test]
423    fn test_color_to_ansi_named_foreground() {
424        let result = color_to_ansi(&Color::Named(NamedColor::Red), Ground::Foreground);
425        assert_eq!(result, "\x1b[31m");
426    }
427
428    #[test]
429    fn test_color_to_ansi_named_background() {
430        let result = color_to_ansi(&Color::Named(NamedColor::Red), Ground::Background);
431        assert_eq!(result, "\x1b[41m");
432    }
433
434    #[test]
435    fn test_color_to_ansi_ansi256_foreground() {
436        let result = color_to_ansi(&Color::Ansi256(200), Ground::Foreground);
437        assert_eq!(result, "\x1b[38;5;200m");
438    }
439
440    #[test]
441    fn test_color_to_ansi_ansi256_background() {
442        let result = color_to_ansi(&Color::Ansi256(100), Ground::Background);
443        assert_eq!(result, "\x1b[48;5;100m");
444    }
445
446    #[test]
447    fn test_color_to_ansi_rgb_foreground() {
448        let result = color_to_ansi(&Color::Rgb(255, 128, 0), Ground::Foreground);
449        assert_eq!(result, "\x1b[38;2;255;128;0m");
450    }
451
452    #[test]
453    fn test_color_to_ansi_rgb_background() {
454        let result = color_to_ansi(&Color::Rgb(0, 0, 255), Ground::Background);
455        assert_eq!(result, "\x1b[48;2;0;0;255m");
456    }
457
458    #[test]
459    fn test_color_to_ansi_rgb_zero_values() {
460        let result = color_to_ansi(&Color::Rgb(0, 0, 0), Ground::Foreground);
461        assert_eq!(result, "\x1b[38;2;0;0;0m");
462    }
463
464    // --- emphasis_to_ansi ---
465
466    #[test]
467    fn test_emphasis_to_ansi_bold() {
468        assert_eq!(emphasis_to_ansi(&EmphasisType::Bold), "\x1b[1m");
469    }
470
471    #[test]
472    fn test_emphasis_to_ansi_dim() {
473        assert_eq!(emphasis_to_ansi(&EmphasisType::Dim), "\x1b[2m");
474    }
475
476    #[test]
477    fn test_emphasis_to_ansi_italic() {
478        assert_eq!(emphasis_to_ansi(&EmphasisType::Italic), "\x1b[3m");
479    }
480
481    #[test]
482    fn test_emphasis_to_ansi_underline() {
483        assert_eq!(emphasis_to_ansi(&EmphasisType::Underline), "\x1b[4m");
484    }
485
486    #[test]
487    fn test_emphasis_to_ansi_blink() {
488        assert_eq!(emphasis_to_ansi(&EmphasisType::Blink), "\x1b[5m");
489    }
490
491    #[test]
492    fn test_emphasis_to_ansi_strikethrough() {
493        assert_eq!(emphasis_to_ansi(&EmphasisType::Strikethrough), "\x1b[9m");
494    }
495
496    // --- style_to_ansi ---
497
498    #[test]
499    fn test_style_to_ansi_empty_style_returns_empty_string() {
500        let style = Style {
501            fg: None,
502            bg: None,
503            bold: false,
504            dim: false,
505            italic: false,
506            underline: false,
507            strikethrough: false,
508            blink: false,
509            ..Default::default()
510        };
511        assert_eq!(style_to_ansi(&style), "");
512    }
513
514    #[test]
515    fn test_style_to_ansi_bold_only() {
516        let style = Style {
517            fg: None,
518            bg: None,
519            bold: true,
520            dim: false,
521            italic: false,
522            underline: false,
523            strikethrough: false,
524            blink: false,
525            ..Default::default()
526        };
527        assert_eq!(style_to_ansi(&style), "\x1b[1m");
528    }
529
530    #[test]
531    fn test_style_to_ansi_bold_with_foreground_color() {
532        let style = Style {
533            fg: Some(Color::Named(NamedColor::Green)),
534            bg: None,
535            bold: true,
536            dim: false,
537            italic: false,
538            underline: false,
539            strikethrough: false,
540            blink: false,
541            ..Default::default()
542        };
543        assert_eq!(style_to_ansi(&style), "\x1b[1;32m");
544    }
545
546    #[test]
547    fn test_style_to_ansi_fg_and_bg() {
548        let style = Style {
549            fg: Some(Color::Named(NamedColor::White)),
550            bg: Some(Color::Named(NamedColor::Blue)),
551            bold: false,
552            dim: false,
553            italic: false,
554            underline: false,
555            strikethrough: false,
556            blink: false,
557            ..Default::default()
558        };
559        assert_eq!(style_to_ansi(&style), "\x1b[37;44m");
560    }
561
562    #[test]
563    fn test_style_to_ansi_all_emphasis_flags() {
564        let style = Style {
565            fg: None,
566            bg: None,
567            bold: true,
568            dim: true,
569            italic: true,
570            underline: true,
571            strikethrough: true,
572            blink: true,
573            ..Default::default()
574        };
575        assert_eq!(style_to_ansi(&style), "\x1b[1;2;3;4;5;9m");
576    }
577}
578
579// Skipped (side effects): none: all functions in ansi.rs are pure.