1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
//! `anstyle_ls::parse` parses a color configuration string (in `LS_COLORS` syntax)
//! into an `anstyle::Style`:
//!
//! # Examples
//!
//! ```rust
//! let style = anstyle_ls::parse("34;03").unwrap();
//! assert_eq!(style, anstyle::AnsiColor::Blue.on_default() | anstyle::Effects::ITALIC);
//! ```

#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![warn(missing_docs)]
#![warn(clippy::print_stderr)]
#![warn(clippy::print_stdout)]

/// Parse a string in `LS_COLORS`'s color configuration syntax into an
/// `ansi_term::Style`.
pub fn parse(code: &str) -> Option<anstyle::Style> {
    if code.is_empty() || code == "0" || code == "00" {
        return None;
    }

    let mut parts: std::collections::VecDeque<u8> = code
        .split(';')
        .map(|c| c.parse::<u8>().ok())
        .collect::<Option<_>>()?;

    let mut effects = anstyle::Effects::new();
    let mut fg_color: Option<anstyle::Color> = None;
    let mut bg_color: Option<anstyle::Color> = None;
    let mut underline_color: Option<anstyle::Color> = None;

    while let Some(part) = parts.pop_front() {
        match part {
            0 => {
                effects = Default::default();
                fg_color = Default::default();
                bg_color = Default::default();
                underline_color = Default::default();
            }
            1 => effects |= anstyle::Effects::BOLD,
            2 => effects |= anstyle::Effects::DIMMED,
            3 => effects |= anstyle::Effects::ITALIC,
            4 => effects |= anstyle::Effects::UNDERLINE,
            5 => effects |= anstyle::Effects::BLINK,
            6 => effects |= anstyle::Effects::BLINK,
            7 => effects |= anstyle::Effects::INVERT,
            8 => effects |= anstyle::Effects::HIDDEN,
            9 => effects |= anstyle::Effects::STRIKETHROUGH,
            22 => {
                effects = effects
                    .remove(anstyle::Effects::BOLD)
                    .remove(anstyle::Effects::DIMMED);
            }
            23 => {
                effects = effects.remove(anstyle::Effects::ITALIC);
            }
            24 => {
                effects = effects.remove(anstyle::Effects::UNDERLINE);
            }
            25 => {
                effects = effects.remove(anstyle::Effects::BLINK);
            }
            27 => {
                effects = effects.remove(anstyle::Effects::INVERT);
            }
            28 => {
                effects = effects.remove(anstyle::Effects::HIDDEN);
            }
            29 => {
                effects = effects.remove(anstyle::Effects::STRIKETHROUGH);
            }
            30 => fg_color = Some(anstyle::AnsiColor::Black.into()),
            31 => fg_color = Some(anstyle::AnsiColor::Red.into()),
            32 => fg_color = Some(anstyle::AnsiColor::Green.into()),
            33 => fg_color = Some(anstyle::AnsiColor::Yellow.into()),
            34 => fg_color = Some(anstyle::AnsiColor::Blue.into()),
            35 => fg_color = Some(anstyle::AnsiColor::Magenta.into()),
            36 => fg_color = Some(anstyle::AnsiColor::Cyan.into()),
            37 => fg_color = Some(anstyle::AnsiColor::White.into()),
            38 => match (parts.pop_front(), parts.pop_front()) {
                (Some(5), Some(color)) => fg_color = Some(anstyle::Ansi256Color(color).into()),
                (Some(2), Some(red)) => match (parts.pop_front(), parts.pop_front()) {
                    (Some(green), Some(blue)) => {
                        fg_color = Some(anstyle::RgbColor(red, green, blue).into());
                    }
                    _ => {
                        break;
                    }
                },
                _ => {
                    break;
                }
            },
            39 => fg_color = None,
            40 => bg_color = Some(anstyle::AnsiColor::Black.into()),
            41 => bg_color = Some(anstyle::AnsiColor::Red.into()),
            42 => bg_color = Some(anstyle::AnsiColor::Green.into()),
            43 => bg_color = Some(anstyle::AnsiColor::Yellow.into()),
            44 => bg_color = Some(anstyle::AnsiColor::Blue.into()),
            45 => bg_color = Some(anstyle::AnsiColor::Magenta.into()),
            46 => bg_color = Some(anstyle::AnsiColor::Cyan.into()),
            47 => bg_color = Some(anstyle::AnsiColor::White.into()),
            48 => match (parts.pop_front(), parts.pop_front()) {
                (Some(5), Some(color)) => bg_color = Some(anstyle::Ansi256Color(color).into()),
                (Some(2), Some(red)) => match (parts.pop_front(), parts.pop_front()) {
                    (Some(green), Some(blue)) => {
                        bg_color = Some(anstyle::RgbColor(red, green, blue).into());
                    }
                    _ => {
                        break;
                    }
                },
                _ => {
                    break;
                }
            },
            49 => bg_color = None,
            58 => match (parts.pop_front(), parts.pop_front()) {
                (Some(5), Some(color)) => {
                    underline_color = Some(anstyle::Ansi256Color(color).into());
                }
                (Some(2), Some(red)) => match (parts.pop_front(), parts.pop_front()) {
                    (Some(green), Some(blue)) => {
                        underline_color = Some(anstyle::RgbColor(red, green, blue).into());
                    }
                    _ => {
                        break;
                    }
                },
                _ => {
                    break;
                }
            },
            59 => underline_color = None,
            90 => fg_color = Some(anstyle::AnsiColor::BrightBlack.into()),
            91 => fg_color = Some(anstyle::AnsiColor::BrightRed.into()),
            92 => fg_color = Some(anstyle::AnsiColor::BrightGreen.into()),
            93 => fg_color = Some(anstyle::AnsiColor::BrightYellow.into()),
            94 => fg_color = Some(anstyle::AnsiColor::BrightBlue.into()),
            95 => fg_color = Some(anstyle::AnsiColor::BrightMagenta.into()),
            96 => fg_color = Some(anstyle::AnsiColor::BrightCyan.into()),
            97 => fg_color = Some(anstyle::AnsiColor::BrightWhite.into()),
            100 => bg_color = Some(anstyle::AnsiColor::BrightBlack.into()),
            101 => bg_color = Some(anstyle::AnsiColor::BrightRed.into()),
            102 => bg_color = Some(anstyle::AnsiColor::BrightGreen.into()),
            103 => bg_color = Some(anstyle::AnsiColor::BrightYellow.into()),
            104 => bg_color = Some(anstyle::AnsiColor::BrightBlue.into()),
            105 => bg_color = Some(anstyle::AnsiColor::BrightMagenta.into()),
            106 => bg_color = Some(anstyle::AnsiColor::BrightCyan.into()),
            107 => bg_color = Some(anstyle::AnsiColor::BrightWhite.into()),
            _ => {
                continue;
            }
        }
    }

    Some(
        anstyle::Style::new()
            .fg_color(fg_color)
            .bg_color(bg_color)
            .underline_color(underline_color)
            .effects(effects),
    )
}

#[cfg(test)]
mod tests {
    use super::*;

    #[track_caller]
    fn assert_style(code: &str, expected: impl Into<anstyle::Style>) {
        let actual = parse(code).unwrap();
        let expected = expected.into();
        assert_eq!(actual, expected);
    }

    #[test]
    fn parse_simple() {
        assert_style("31", anstyle::AnsiColor::Red.on_default());
        assert_style(
            "47",
            anstyle::Style::new().bg_color(Some(anstyle::AnsiColor::White.into())),
        );
        assert_style("91", anstyle::AnsiColor::BrightRed.on_default());
        assert_style(
            "107",
            anstyle::Style::new().bg_color(Some(anstyle::AnsiColor::BrightWhite.into())),
        );
        assert_style(
            "32;40",
            anstyle::AnsiColor::Green.on(anstyle::AnsiColor::Black),
        );
    }

    #[test]
    fn parse_reject() {
        assert_eq!(None, parse("a"));
        assert_eq!(None, parse("1;"));
        assert_eq!(None, parse("33; 42"));
    }

    #[test]
    fn parse_font_style() {
        assert_style("00;31", anstyle::AnsiColor::Red.on_default());
        assert_style(
            "03;34",
            anstyle::AnsiColor::Blue.on_default() | anstyle::Effects::ITALIC,
        );
        assert_style(
            "06;34",
            anstyle::AnsiColor::Blue.on_default() | anstyle::Effects::BLINK,
        );
        assert_style(
            "01;36",
            anstyle::AnsiColor::Cyan.on_default() | anstyle::Effects::BOLD,
        );
        assert_style("01;03", anstyle::Effects::BOLD | anstyle::Effects::ITALIC);
    }

    #[test]
    fn ignore_unsupported_styles() {
        assert_style("14;31", anstyle::AnsiColor::Red.on_default());
    }

    #[test]
    fn support_reset_of_styles() {
        assert_style(
            "01;31",
            anstyle::AnsiColor::Red.on_default() | anstyle::Effects::BOLD,
        );
        assert_style("01;31;22", anstyle::AnsiColor::Red.on_default());
    }

    #[test]
    fn parse_font_style_backwards() {
        assert_style(
            "34;03",
            anstyle::AnsiColor::Blue.on_default() | anstyle::Effects::ITALIC,
        );
        assert_style(
            "36;01",
            anstyle::AnsiColor::Cyan.on_default() | anstyle::Effects::BOLD,
        );
        assert_style("31;00", anstyle::Style::new());
    }

    #[test]
    fn parse_8_bit_colors() {
        assert_style("38;5;115", anstyle::Ansi256Color(115).on_default());
        assert_style("00;38;5;115", anstyle::Ansi256Color(115).on_default());
        assert_style(
            "01;38;5;119",
            anstyle::Ansi256Color(119).on_default() | anstyle::Effects::BOLD,
        );
        assert_style(
            "38;5;119;01",
            anstyle::Ansi256Color(119).on_default() | anstyle::Effects::BOLD,
        );
        assert_style(
            "58;5;115",
            anstyle::Style::new().underline_color(Some(anstyle::Ansi256Color(115).into())),
        );
        assert_style(
            "00;58;5;115",
            anstyle::Style::new().underline_color(Some(anstyle::Ansi256Color(115).into())),
        );
        assert_style(
            "01;58;5;119",
            anstyle::Style::new().underline_color(Some(anstyle::Ansi256Color(119).into()))
                | anstyle::Effects::BOLD,
        );
    }

    #[test]
    fn parse_24_bit_colors() {
        assert_style(
            "38;2;115;3;100",
            anstyle::RgbColor(115, 3, 100).on_default(),
        );
        assert_style(
            "38;2;115;3;100;3",
            anstyle::RgbColor(115, 3, 100).on_default() | anstyle::Effects::ITALIC,
        );
        assert_style(
            "48;2;100;200;0;1;38;2;0;10;20",
            anstyle::RgbColor(0, 10, 20).on(anstyle::RgbColor(100, 200, 0))
                | anstyle::Effects::BOLD,
        );
        assert_style(
            "48;2;100;200;0;1;38;2;0;10;20;58;2;64;64;64",
            (anstyle::RgbColor(0, 10, 20).on(anstyle::RgbColor(100, 200, 0))
                | anstyle::Effects::BOLD)
                .underline_color(Some(anstyle::RgbColor(64, 64, 64).into())),
        );
    }
}