cp437_tools/libs/public/
colour.rs

1//! ANSI colour schemes.
2
3use regex::Regex;
4#[cfg(feature = "_gen")]
5use strum_macros::EnumIter;
6
7/// A collection of colour schemes.
8///
9/// Each entry is a list of 16 RGB values corresponding to the 4-bit colours
10/// used by CP437 files.
11#[derive(Clone, Debug, Eq, PartialEq)]
12#[cfg_attr(feature = "_gen", derive(EnumIter))]
13pub enum ColourScheme {
14    /// The classic scheme.
15    ///
16    /// ![CLASSIC scheme][scheme]
17    #[cfg_attr(all(),
18        doc = ::embed_doc_image::embed_image!("scheme", "res/schemes/CLASSIC.png"),
19    )]
20    CLASSIC,
21    /// A modern looking scheme.
22    ///
23    /// ![MODERN scheme][scheme]
24    #[cfg_attr(all(),
25        doc = ::embed_doc_image::embed_image!("scheme", "res/schemes/MODERN.png"),
26    )]
27    MODERN,
28    /// A [catppuccin](https://catppuccin.com/palette/)-based colour scheme.
29    ///
30    /// ![CATPPUCCIN scheme][scheme]
31    #[cfg_attr(all(),
32        doc = ::embed_doc_image::embed_image!("scheme", "res/schemes/CATPPUCCIN.png"),
33    )]
34    CATPPUCCIN,
35    /// A [dracula](https://draculatheme.com/contribute#color-palette)-based colour scheme.
36    ///
37    /// ![DRACULA scheme][scheme]
38    #[cfg_attr(all(),
39        doc = ::embed_doc_image::embed_image!("scheme", "res/schemes/DRACULA.png"),
40    )]
41    DRACULA,
42    /// A [rosé-pine](https://rosepinetheme.com/palette/)-based colour scheme.
43    ///
44    /// ![ROSEPINE scheme][scheme]
45    #[cfg_attr(all(),
46        doc = ::embed_doc_image::embed_image!("scheme", "res/schemes/ROSEPINE.png"),
47    )]
48    ROSEPINE,
49    /// A configurable scheme.
50    CUSTOM([[u8; 3]; 16]),
51}
52
53impl ColourScheme {
54    /// Get the string representation of a scheme.
55    #[must_use]
56    pub fn name(&self) -> String {
57        return match self {
58            ColourScheme::CLASSIC => String::from("CLASSIC"),
59            ColourScheme::MODERN => String::from("MODERN"),
60            ColourScheme::CATPPUCCIN => String::from("CATPPUCCIN"),
61            ColourScheme::DRACULA => String::from("DRACULA"),
62            ColourScheme::ROSEPINE => String::from("ROSEPINE"),
63            ColourScheme::CUSTOM(colours) => {
64                let codes = colours
65                    .iter()
66                    .map(|colour| {
67                        return format!("#{:02x}{:02x}{:02x}", colour[0], colour[1], colour[2]);
68                    })
69                    .fold(String::new(), |acc, x| return if acc.is_empty() { x } else { format!("{acc},{x}") });
70                format!("CUSTOM({codes})")
71            },
72        };
73    }
74
75    /// Get a colour scheme from a string.
76    ///
77    /// # Errors
78    ///
79    /// Fails when the theme is invalid.
80    ///
81    #[expect(clippy::too_many_lines, reason = "Not much that can be done")]
82    pub fn get(name: &String) -> Result<ColourScheme, String> {
83        let uppercase_name = name.to_uppercase();
84        return match uppercase_name.as_str() {
85            "CLASSIC" => Ok(ColourScheme::CLASSIC),
86            "MODERN" => Ok(ColourScheme::MODERN),
87            "CATPPUCCIN" => Ok(ColourScheme::CATPPUCCIN),
88            "DRACULA" => Ok(ColourScheme::DRACULA),
89            "ROSEPINE" => Ok(ColourScheme::ROSEPINE),
90            _ => {
91                if uppercase_name.starts_with("CUSTOM(") {
92                    if let Some(c) = Regex::new(r"^CUSTOM\(((?:#[0-9A-F]{6},){15}#[0-9A-F]{6})\)$")
93                        .expect("Valid regex")
94                        .captures(&uppercase_name)
95                    {
96                        let c = &c[1];
97
98                        Ok(ColourScheme::CUSTOM([
99                            // DARK
100                            [
101                                // BLACK
102                                parse_hex(&c[1..3])?,
103                                parse_hex(&c[3..5])?,
104                                parse_hex(&c[5..7])?,
105                            ],
106                            [
107                                // RED
108                                parse_hex(&c[9..11])?,
109                                parse_hex(&c[11..13])?,
110                                parse_hex(&c[13..15])?,
111                            ],
112                            [
113                                // GREEN
114                                parse_hex(&c[17..19])?,
115                                parse_hex(&c[19..21])?,
116                                parse_hex(&c[21..23])?,
117                            ],
118                            [
119                                // YELLOW
120                                parse_hex(&c[25..27])?,
121                                parse_hex(&c[27..29])?,
122                                parse_hex(&c[29..31])?,
123                            ],
124                            [
125                                // BLUE
126                                parse_hex(&c[33..35])?,
127                                parse_hex(&c[35..37])?,
128                                parse_hex(&c[37..39])?,
129                            ],
130                            [
131                                // MAGENTA
132                                parse_hex(&c[41..43])?,
133                                parse_hex(&c[43..45])?,
134                                parse_hex(&c[45..47])?,
135                            ],
136                            [
137                                // CYAN
138                                parse_hex(&c[49..51])?,
139                                parse_hex(&c[51..53])?,
140                                parse_hex(&c[53..55])?,
141                            ],
142                            [
143                                // WHITE
144                                parse_hex(&c[57..59])?,
145                                parse_hex(&c[59..61])?,
146                                parse_hex(&c[61..63])?,
147                            ],
148                            // BRIGHT
149                            [
150                                // BLACK
151                                parse_hex(&c[65..67])?,
152                                parse_hex(&c[67..69])?,
153                                parse_hex(&c[69..71])?,
154                            ],
155                            [
156                                // RED
157                                parse_hex(&c[73..75])?,
158                                parse_hex(&c[75..77])?,
159                                parse_hex(&c[77..79])?,
160                            ],
161                            [
162                                // GREEN
163                                parse_hex(&c[81..83])?,
164                                parse_hex(&c[83..85])?,
165                                parse_hex(&c[85..87])?,
166                            ],
167                            [
168                                // YELLOW
169                                parse_hex(&c[89..91])?,
170                                parse_hex(&c[91..93])?,
171                                parse_hex(&c[93..95])?,
172                            ],
173                            [
174                                // BLUE
175                                parse_hex(&c[97..99])?,
176                                parse_hex(&c[99..101])?,
177                                parse_hex(&c[101..103])?,
178                            ],
179                            [
180                                // MAGENTA
181                                parse_hex(&c[105..107])?,
182                                parse_hex(&c[107..109])?,
183                                parse_hex(&c[109..111])?,
184                            ],
185                            [
186                                // CYAN
187                                parse_hex(&c[113..115])?,
188                                parse_hex(&c[115..117])?,
189                                parse_hex(&c[117..119])?,
190                            ],
191                            [
192                                // WHITE
193                                parse_hex(&c[121..123])?,
194                                parse_hex(&c[123..125])?,
195                                parse_hex(&c[125..127])?,
196                            ],
197                        ]))
198                    } else {
199                        Err(format!("Unparseable colour scheme: {name}"))
200                    }
201                } else {
202                    Err(format!("Unknown scheme: {name}"))
203                }
204            },
205        };
206    }
207
208    /// Get this scheme's colours.
209    #[must_use]
210    pub fn colours(&self) -> [[u8; 3]; 16] {
211        return match self {
212            ColourScheme::CLASSIC => [
213                // DARK
214                [0x00, 0x00, 0x00], // BLACK
215                [0xAB, 0x00, 0x00], // RED
216                [0x00, 0xAB, 0x00], // GREEN
217                [0xAB, 0x57, 0x00], // YELLOW
218                [0x00, 0x00, 0xAB], // BLUE
219                [0xAB, 0x00, 0xAB], // MAGENTA
220                [0x00, 0xAB, 0xAB], // CYAN
221                [0xAB, 0xAB, 0xAB], // WHITE
222                // BRIGHT
223                [0x57, 0x57, 0x57], // BLACK
224                [0xFF, 0x57, 0x57], // RED
225                [0x57, 0xFF, 0x57], // GREEN
226                [0xFF, 0xFF, 0x57], // YELLOW
227                [0x57, 0x57, 0xFF], // BLUE
228                [0xFF, 0x57, 0xFF], // MAGENTA
229                [0x57, 0xFF, 0xFF], // CYAN
230                [0xFF, 0xFF, 0xFF], // WHITE
231            ],
232            ColourScheme::MODERN => [
233                // DARK
234                [0x0A, 0x0A, 0x0A], // BLACK
235                [0x99, 0x4D, 0x4D], // RED
236                [0x8C, 0x99, 0x4D], // GREEN
237                [0xCC, 0x99, 0x66], // YELLOW
238                [0x4D, 0x66, 0x99], // BLUE
239                [0xB3, 0x59, 0x86], // MAGENTA
240                [0x4D, 0x99, 0x99], // CYAN
241                [0x99, 0x99, 0x99], // WHITE
242                // BRIGHT
243                [0x4D, 0x4D, 0x4D], // BLACK
244                [0xCC, 0x7A, 0x7A], // RED
245                [0xBE, 0xCC, 0x7A], // GREEN
246                [0xFF, 0xCC, 0x99], // YELLOW
247                [0x7A, 0x96, 0xCC], // BLUE
248                [0xE6, 0x8A, 0xB8], // MAGENTA
249                [0x7A, 0xCC, 0xCC], // CYAN
250                [0xE6, 0xE6, 0xE6], // WHITE
251            ],
252            ColourScheme::CATPPUCCIN => [
253                // DARK
254                [0x23, 0x26, 0x34], // BLACK
255                [0xDB, 0x63, 0x63], // RED
256                [0x82, 0xBD, 0x64], // GREEN
257                [0xD4, 0xAA, 0x68], // YELLOW
258                [0x6C, 0x8A, 0xE6], // BLUE
259                [0xE6, 0x93, 0xCD], // MAGENTA
260                [0x4E, 0xB5, 0xAB], // CYAN
261                [0xA5, 0xAD, 0xCE], // WHITE
262                // BRIGHT
263                [0x51, 0x57, 0x6D], // BLACK
264                [0xE7, 0x82, 0x84], // RED
265                [0xA6, 0xD1, 0x89], // GREEN
266                [0xE5, 0xC8, 0x90], // YELLOW
267                [0x8C, 0xAA, 0xEE], // BLUE
268                [0xF4, 0xB8, 0xE4], // MAGENTA
269                [0x81, 0xC8, 0xBE], // CYAN
270                [0xC6, 0xD0, 0xF5], // WHITE
271            ],
272            ColourScheme::DRACULA => [
273                // DARK
274                [0x21, 0x22, 0x2C], // BLACK
275                [0xFF, 0x55, 0x55], // RED
276                [0x50, 0xFA, 0x7B], // GREEN
277                [0xF1, 0xFA, 0x8C], // YELLOW
278                [0xBD, 0x93, 0xF9], // BLUE
279                [0xFF, 0x79, 0xC6], // MAGENTA
280                [0x8B, 0xE9, 0xFD], // CYAN
281                [0xF8, 0xF8, 0xF2], // WHITE
282                // BRIGHT
283                [0x62, 0x72, 0xA4], // BLACK
284                [0xFF, 0x6E, 0x6E], // RED
285                [0x69, 0xFF, 0x94], // GREEN
286                [0xFF, 0xFF, 0xA5], // YELLOW
287                [0xD6, 0xAC, 0xFF], // BLUE
288                [0xFF, 0x92, 0xDF], // MAGENTA
289                [0xA4, 0xFF, 0xFF], // CYAN
290                [0xFF, 0xFF, 0xFF], // WHITE
291            ],
292            ColourScheme::ROSEPINE => [
293                // DARK
294                [0x19, 0x17, 0x24], // BLACK
295                [0xB4, 0x52, 0x6E], // RED
296                [0x8B, 0x95, 0x4D], // GREEN
297                [0xC4, 0x96, 0x56], // YELLOW
298                [0x31, 0x74, 0x8F], // BLUE
299                [0x90, 0x7A, 0xA9], // MAGENTA
300                [0x56, 0x94, 0x9F], // CYAN
301                [0x90, 0x8C, 0xAA], // WHITE
302                // BRIGHT
303                [0x40, 0x3D, 0x52], // BLACK
304                [0xEB, 0x6F, 0x92], // RED
305                [0xB7, 0xC4, 0x6A], // GREEN
306                [0xF6, 0xC1, 0x77], // YELLOW
307                [0x3E, 0x8F, 0xB0], // BLUE
308                [0xC4, 0xA7, 0xE7], // MAGENTA
309                [0x9C, 0xCF, 0xD8], // CYAN
310                [0xE0, 0xDE, 0xF4], // WHITE
311            ],
312            ColourScheme::CUSTOM(scheme) => *scheme,
313        };
314    }
315
316    /// Get a single colour from this scheme.
317    #[inline]
318    #[must_use]
319    pub fn colour(&self, index: u8) -> [u8; 3] {
320        return self.colours()[index as usize];
321    }
322}
323
324#[inline]
325fn parse_hex(hex: &str) -> Result<u8, String> {
326    return u8::from_str_radix(hex, 16).map_err(|err| return err.to_string());
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332
333    use pretty_assertions::assert_eq;
334    use rand::{rng, Rng};
335
336    #[test]
337    fn classic() -> Result<(), String> {
338        assert_eq!(ColourScheme::get(&String::from("ClAsSiC"))?, ColourScheme::CLASSIC);
339        for i in 0..16 {
340            assert_eq!(ColourScheme::CLASSIC.colours()[i], ColourScheme::CLASSIC.colour(i as u8));
341        }
342
343        return Ok(());
344    }
345
346    #[test]
347    fn modern() -> Result<(), String> {
348        assert_eq!(ColourScheme::get(&String::from("MoDeRn"))?, ColourScheme::MODERN);
349        for i in 0..16 {
350            assert_eq!(ColourScheme::MODERN.colours()[i], ColourScheme::MODERN.colour(i as u8));
351        }
352
353        return Ok(());
354    }
355
356    #[test]
357    fn dracula() -> Result<(), String> {
358        assert_eq!(ColourScheme::get(&String::from("DrAcUlA"))?, ColourScheme::DRACULA);
359        for i in 0..16 {
360            assert_eq!(ColourScheme::DRACULA.colours()[i], ColourScheme::DRACULA.colour(i as u8));
361        }
362
363        return Ok(());
364    }
365
366    #[test]
367    fn custom() -> Result<(), String> {
368        let colours = [
369            [rng().random(), rng().random(), rng().random()],
370            [rng().random(), rng().random(), rng().random()],
371            [rng().random(), rng().random(), rng().random()],
372            [rng().random(), rng().random(), rng().random()],
373            [rng().random(), rng().random(), rng().random()],
374            [rng().random(), rng().random(), rng().random()],
375            [rng().random(), rng().random(), rng().random()],
376            [rng().random(), rng().random(), rng().random()],
377            [rng().random(), rng().random(), rng().random()],
378            [rng().random(), rng().random(), rng().random()],
379            [rng().random(), rng().random(), rng().random()],
380            [rng().random(), rng().random(), rng().random()],
381            [rng().random(), rng().random(), rng().random()],
382            [rng().random(), rng().random(), rng().random()],
383            [rng().random(), rng().random(), rng().random()],
384            [rng().random(), rng().random(), rng().random()],
385        ];
386        let codes = colours
387            .iter()
388            .map(|colour| {
389                return format!("#{:02x}{:02x}{:02x}", colour[0], colour[1], colour[2]);
390            })
391            .fold(String::new(), |acc, x| if &acc == "" { x } else { format!("{},{}", acc, x) });
392        assert_eq!(ColourScheme::get(&format!("CuStOm({})", codes))?, ColourScheme::CUSTOM(colours));
393        assert_eq!(ColourScheme::get(&format!("CuStOm({})", codes))?.name(), format!("CUSTOM({})", codes));
394        assert_eq!(ColourScheme::CUSTOM(colours).colours(), colours);
395
396        return Ok(());
397    }
398
399    #[test]
400    fn custom_unparseable() {
401        let result = ColourScheme::get(&String::from("CuStOm()"));
402        assert!(result.is_err());
403        assert_eq!(result.unwrap_err(), "Unparseable colour scheme: CuStOm()");
404    }
405
406    #[test]
407    fn invalid() {
408        let result = ColourScheme::get(&String::from("x"));
409        assert!(result.is_err());
410        assert_eq!(result.unwrap_err(), "Unknown scheme: x");
411    }
412}