summaryrefslogtreecommitdiffstats
path: root/src/style/lsc.rs
blob: fee9b07103412317d9eaf9aec1ddba620f7f5574 (plain)
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
use std::ops::FnMut;

use ansi_term::{Colour, Style};
use ansi_term::Colour::*;


// Parsing the LS_COLORS environment variable into a map of names to Style values.
//
// This is sitting around undocumented at the moment because it’s a feature
// that should really be unnecessary! exa highlights its output by creating a
// theme of one Style value per part of the interface that can be coloured,
// then reading styles from that theme. The LS_COLORS variable, on the other
// hand, can contain arbitrary characters that ls is supposed to add to the
// output, without needing to know what they actually do. This puts exa in the
// annoying position of having to parse the ANSI escape codes _back_ into
// Style values before it’s able to use them. Doing this has a lot of
// downsides: if a new terminal feature is added with its own code, exa won’t
// be able to use this without explicit support for parsing the feature, while
// ls would not even need to know it existed. And there are some edge cases in
// ANSI codes, where terminals would accept codes exa is strict about it. It’s
// just not worth doing, and there should really be a way to just use slices
// of the LS_COLORS string without having to parse them.


pub struct LSColors<'var>(pub &'var str);

impl<'var> LSColors<'var> {
    pub fn each_pair<C>(&mut self, mut callback: C) where C: FnMut(Pair<'var>) -> () {
        for next in self.0.split(':') {
            let bits = next.split('=')
                           .take(3)
                           .collect::<Vec<_>>();

            if bits.len() == 2 && !bits[0].is_empty() && !bits[1].is_empty() {
                callback(Pair { key: bits[0], value: bits[1] });
            }
        }
    }
}

pub struct Pair<'var> {
    pub key: &'var str,
    pub value: &'var str,
}

use std::iter::Peekable;
fn parse_into_high_colour<'a, I>(iter: &mut Peekable<I>) -> Option<Colour>
where I: Iterator<Item=&'a str> {
    match iter.peek() {
        Some(&"5") => {
            let _ = iter.next();
            if let Some(byte) = iter.next() {
                if let Ok(num) = byte.parse() {
                    return Some(Fixed(num));
                }
            }
        }
        Some(&"2") => {
            let _ = iter.next();
            if let Some(hexes) = iter.next() {
                // Some terminals support R:G:B instead of R;G;B
                // but this clashes with splitting on ':' in each_pair above.
                /*if hexes.contains(':') {
                    let rgb = hexes.splitn(3, ':').collect::<Vec<_>>();
                    if rgb.len() != 3 {
                        return None;
                    }
                    else if let (Ok(r), Ok(g), Ok(b)) = (rgb[0].parse(), rgb[1].parse(), rgb[2].parse()) {
                        return Some(RGB(r, g, b));
                    }
                }*/

                if let (Some(r), Some(g), Some(b)) = (hexes.parse().ok(),
                                                           iter.next().and_then(|s| s.parse().ok()),
                                                           iter.next().and_then(|s| s.parse().ok())) {
                    return Some(RGB(r, g, b));
                }
            }
        }
        _ => {},
    }
    None
}

impl<'var> Pair<'var> {
    pub fn to_style(&self) -> Style {
        let mut style = Style::default();
        let mut iter = self.value.split(';').peekable();

        while let Some(num) = iter.next() {
            match num.trim_start_matches('0') {

                // Bold and italic
                "1" => style = style.bold(),
                "2" => style = style.dimmed(),
                "3" => style = style.italic(),
                "4" => style = style.underline(),
                "5" => style = style.blink(),
                // 6 is supposedly a faster blink
                "7" => style = style.reverse(),
                "8" => style = style.hidden(),
                "9" => style = style.strikethrough(),

                // Foreground colours
                "30" => style = style.fg(Black),
                "31" => style = style.fg(Red),
                "32" => style = style.fg(Green),
                "33" => style = style.fg(Yellow),
                "34" => style = style.fg(Blue),
                "35" => style = style.fg(Purple),
                "36" => style = style.fg(Cyan),
                "37" => style = style.fg(White),
                "38" => if let Some(c) = parse_into_high_colour(&mut iter) { style = style.fg(c) },

                // Background colours
                "40" => style = style.on(Black),
                "41" => style = style.on(Red),
                "42" => style = style.on(Green),
                "43" => style = style.on(Yellow),
                "44" => style = style.on(Blue),
                "45" => style = style.on(Purple),
                "46" => style = style.on(Cyan),
                "47" => style = style.on(White),
                "48" => if let Some(c) = parse_into_high_colour(&mut iter) { style = style.on(c) },

                 _    => {/* ignore the error and do nothing */},
            }
        }

        style
    }
}


#[cfg(test)]
mod ansi_test {
    use super::*;
    use ansi_term::Style;

    macro_rules! test {
        ($name:ident: $input:expr => $result:expr) => {
            #[test]
            fn $name() {
                assert_eq!(Pair { key: "", value: $input }.to_style(), $result);
            }
        };
    }

    // Styles
    test!(bold:  "1"         => Style::default().bold());
    test!(bold2: "01"        => Style::default().bold());
    test!(under: "4"         => Style::default().underline());
    test!(unde2: "04"        => Style::default().underline());
    test!(both:  "1;4"       => Style::default().bold().underline());
    test!(both2: "01;04"     => Style::default().bold().underline());
    test!(fg:    "31"        => Red.normal());
    test!(bg:    "43"        => Style::default().on(Yellow));
    test!(bfg:   "31;43"     => Red.on(Yellow));
    test!(bfg2:  "0031;0043" => Red.on(Yellow));
    test!(all:   "43;31;1;4" => Red.on(Yellow).bold().underline());
    test!(again: "1;1;1;1;1" => Style::default().bold());

    // Failure cases
    test!(empty: ""          => Style::default());
    test!(semis: ";;;;;;"    => Style::default());
    test!(nines: "99999999"  => Style::default());
    test!(word:  "GREEN"     => Style::default());

    // Higher colours
    test!(hifg:  "38;5;149"  => Fixed(149).normal());
    test!(hibg:  "48;5;1"    => Style::default().on(Fixed(1)));
    test!(hibo:  "48;5;1;1"  => Style::default().on(Fixed(1)).bold());
    test!(hiund: "4;48;5;1"  => Style::default().on(Fixed(1)).underline());

    test!(rgb:   "38;2;255;100;0"     => Style::default().fg(RGB(255, 100, 0)));
    test!(rgbi:  "38;2;255;100;0;3"   => Style::default().fg(RGB(255, 100, 0)).italic());
    test!(rgbbg: "48;2;255;100;0"     => Style::default().on(RGB(255, 100, 0)));
    test!(rgbbi: "48;2;255;100;0;3"   => Style::default().on(RGB(255, 100, 0)).italic());

    test!(fgbg:  "38;5;121;48;5;212"  => Fixed(121).on(Fixed(212)));
    test!(bgfg:  "48;5;121;38;5;212"  => Fixed(212).on(Fixed(121)));
    test!(toohi: "48;5;999"           => Style::default());
}



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

    macro_rules! test {
        ($name:ident: $input:expr => $result:expr) => {
            #[test]
            fn $name() {
                let mut lscs = Vec::new();
                LSColors($input).each_pair(|p| lscs.push( (p.key.clone(), p.to_style()) ));
                assert_eq!(lscs, $result.to_vec());
            }
        };
    }

    // Bad parses
    test!(empty:    ""       => []);
    test!(jibber:   "blah"   => []);

    test!(equals:     "="    => []);
    test!(starts:     "=di"  => []);
    test!(ends:     "id="    => []);

    // Foreground colours
    test!(green:   "cb=32"   => [ ("cb", Green.nor