Skip to main content

ansi/
style.rs

1//! SGR styles as data: build escape sequences from a [`Style`], and apply
2//! parsed SGR parameters back onto one: the two directions used by the
3//! screen renderer and by anything interpreting terminal output.
4
5/// A terminal color: the terminal's default, one of the 256 indexed colors
6/// (0–7 normal, 8–15 bright, 16–255 the extended cube/grays), or 24-bit RGB.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
8pub enum Color {
9    #[default]
10    Default,
11    Indexed(u8),
12    Rgb(u8, u8, u8),
13}
14
15/// A complete character style: foreground, background, and attributes.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
17pub struct Style {
18    pub fg: Color,
19    pub bg: Color,
20    pub bold: bool,
21    pub dim: bool,
22    pub italic: bool,
23    pub underline: bool,
24    pub reverse: bool,
25    pub strike: bool,
26}
27
28impl Style {
29    /// The absolute SGR sequence for this style: a reset followed by every
30    /// set attribute, so it renders identically regardless of prior state.
31    /// The default style renders as `ESC[0m`.
32    pub fn sgr(&self) -> String {
33        let mut params = String::from("0");
34        self.push_set_params(&mut params);
35        format!("\x1b[{params}m")
36    }
37
38    /// The minimal SGR sequence that changes a terminal currently displaying
39    /// `self` to display `to`. Returns `""` when the styles are equal. When
40    /// `to` only *adds* attributes, only the additions are emitted; when
41    /// anything must be turned off, the absolute form of `to` is emitted
42    /// (reset + set), which is always correct.
43    pub fn transition_to(&self, to: &Style) -> String {
44        if self == to {
45            return String::new();
46        }
47        let removes = (self.bold && !to.bold)
48            || (self.dim && !to.dim)
49            || (self.italic && !to.italic)
50            || (self.underline && !to.underline)
51            || (self.reverse && !to.reverse)
52            || (self.strike && !to.strike)
53            || (self.fg != to.fg && to.fg == Color::Default)
54            || (self.bg != to.bg && to.bg == Color::Default);
55        if removes {
56            return to.sgr();
57        }
58        let mut params = String::new();
59        let diff = Style {
60            fg: if self.fg != to.fg {
61                to.fg
62            } else {
63                Color::Default
64            },
65            bg: if self.bg != to.bg {
66                to.bg
67            } else {
68                Color::Default
69            },
70            bold: to.bold && !self.bold,
71            dim: to.dim && !self.dim,
72            italic: to.italic && !self.italic,
73            underline: to.underline && !self.underline,
74            reverse: to.reverse && !self.reverse,
75            strike: to.strike && !self.strike,
76        };
77        diff.push_set_params(&mut params);
78        debug_assert!(!params.is_empty());
79        format!("\x1b[{}m", params.trim_start_matches(';'))
80    }
81
82    /// Append the SGR parameters for every *set* attribute (`;`-prefixed).
83    fn push_set_params(&self, params: &mut String) {
84        use std::fmt::Write;
85        if self.bold {
86            params.push_str(";1");
87        }
88        if self.dim {
89            params.push_str(";2");
90        }
91        if self.italic {
92            params.push_str(";3");
93        }
94        if self.underline {
95            params.push_str(";4");
96        }
97        if self.reverse {
98            params.push_str(";7");
99        }
100        if self.strike {
101            params.push_str(";9");
102        }
103        match self.fg {
104            Color::Default => {}
105            Color::Indexed(n @ 0..=7) => write!(params, ";{}", 30 + n as u16).unwrap(),
106            Color::Indexed(n @ 8..=15) => write!(params, ";{}", 90 + (n - 8) as u16).unwrap(),
107            Color::Indexed(n) => write!(params, ";38;5;{n}").unwrap(),
108            Color::Rgb(r, g, b) => write!(params, ";38;2;{r};{g};{b}").unwrap(),
109        }
110        match self.bg {
111            Color::Default => {}
112            Color::Indexed(n @ 0..=7) => write!(params, ";{}", 40 + n as u16).unwrap(),
113            Color::Indexed(n @ 8..=15) => write!(params, ";{}", 100 + (n - 8) as u16).unwrap(),
114            Color::Indexed(n) => write!(params, ";48;5;{n}").unwrap(),
115            Color::Rgb(r, g, b) => write!(params, ";48;2;{r};{g};{b}").unwrap(),
116        }
117    }
118
119    /// Apply parsed SGR parameters (the numbers from `ESC[...m`) to this
120    /// style, as a terminal would. An empty parameter list means reset.
121    /// Unknown parameters are ignored; malformed 38/48 extended-color runs
122    /// consume what they can and stop.
123    pub fn apply_sgr(&mut self, params: &[u16]) {
124        if params.is_empty() {
125            *self = Style::default();
126            return;
127        }
128        let mut i = 0;
129        while i < params.len() {
130            match params[i] {
131                0 => *self = Style::default(),
132                1 => self.bold = true,
133                2 => self.dim = true,
134                3 => self.italic = true,
135                4 => self.underline = true,
136                7 => self.reverse = true,
137                9 => self.strike = true,
138                22 => {
139                    self.bold = false;
140                    self.dim = false;
141                }
142                23 => self.italic = false,
143                24 => self.underline = false,
144                27 => self.reverse = false,
145                29 => self.strike = false,
146                30..=37 => self.fg = Color::Indexed((params[i] - 30) as u8),
147                39 => self.fg = Color::Default,
148                90..=97 => self.fg = Color::Indexed((params[i] - 90 + 8) as u8),
149                40..=47 => self.bg = Color::Indexed((params[i] - 40) as u8),
150                49 => self.bg = Color::Default,
151                100..=107 => self.bg = Color::Indexed((params[i] - 100 + 8) as u8),
152                38 | 48 => {
153                    let target_fg = params[i] == 38;
154                    let color = match params.get(i + 1) {
155                        Some(5) => {
156                            let Some(&n) = params.get(i + 2) else { return };
157                            i += 2;
158                            Color::Indexed(n.min(255) as u8)
159                        }
160                        Some(2) => {
161                            let (Some(&r), Some(&g), Some(&b)) =
162                                (params.get(i + 2), params.get(i + 3), params.get(i + 4))
163                            else {
164                                return;
165                            };
166                            i += 4;
167                            Color::Rgb(r.min(255) as u8, g.min(255) as u8, b.min(255) as u8)
168                        }
169                        _ => return,
170                    };
171                    if target_fg {
172                        self.fg = color;
173                    } else {
174                        self.bg = color;
175                    }
176                }
177                _ => {}
178            }
179            i += 1;
180        }
181    }
182}