1use regex::Regex;
4#[cfg(feature = "_gen")]
5use strum_macros::EnumIter;
6
7#[derive(Clone, Debug, Eq, PartialEq)]
12#[cfg_attr(feature = "_gen", derive(EnumIter))]
13pub enum ColourScheme {
14 #[cfg_attr(all(),
18 doc = ::embed_doc_image::embed_image!("scheme", "res/schemes/CLASSIC.png"),
19 )]
20 CLASSIC,
21 #[cfg_attr(all(),
25 doc = ::embed_doc_image::embed_image!("scheme", "res/schemes/MODERN.png"),
26 )]
27 MODERN,
28 #[cfg_attr(all(),
32 doc = ::embed_doc_image::embed_image!("scheme", "res/schemes/CATPPUCCIN.png"),
33 )]
34 CATPPUCCIN,
35 #[cfg_attr(all(),
39 doc = ::embed_doc_image::embed_image!("scheme", "res/schemes/DRACULA.png"),
40 )]
41 DRACULA,
42 #[cfg_attr(all(),
46 doc = ::embed_doc_image::embed_image!("scheme", "res/schemes/ROSEPINE.png"),
47 )]
48 ROSEPINE,
49 CUSTOM([[u8; 3]; 16]),
51}
52
53impl ColourScheme {
54 #[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 #[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 [
101 parse_hex(&c[1..3])?,
103 parse_hex(&c[3..5])?,
104 parse_hex(&c[5..7])?,
105 ],
106 [
107 parse_hex(&c[9..11])?,
109 parse_hex(&c[11..13])?,
110 parse_hex(&c[13..15])?,
111 ],
112 [
113 parse_hex(&c[17..19])?,
115 parse_hex(&c[19..21])?,
116 parse_hex(&c[21..23])?,
117 ],
118 [
119 parse_hex(&c[25..27])?,
121 parse_hex(&c[27..29])?,
122 parse_hex(&c[29..31])?,
123 ],
124 [
125 parse_hex(&c[33..35])?,
127 parse_hex(&c[35..37])?,
128 parse_hex(&c[37..39])?,
129 ],
130 [
131 parse_hex(&c[41..43])?,
133 parse_hex(&c[43..45])?,
134 parse_hex(&c[45..47])?,
135 ],
136 [
137 parse_hex(&c[49..51])?,
139 parse_hex(&c[51..53])?,
140 parse_hex(&c[53..55])?,
141 ],
142 [
143 parse_hex(&c[57..59])?,
145 parse_hex(&c[59..61])?,
146 parse_hex(&c[61..63])?,
147 ],
148 [
150 parse_hex(&c[65..67])?,
152 parse_hex(&c[67..69])?,
153 parse_hex(&c[69..71])?,
154 ],
155 [
156 parse_hex(&c[73..75])?,
158 parse_hex(&c[75..77])?,
159 parse_hex(&c[77..79])?,
160 ],
161 [
162 parse_hex(&c[81..83])?,
164 parse_hex(&c[83..85])?,
165 parse_hex(&c[85..87])?,
166 ],
167 [
168 parse_hex(&c[89..91])?,
170 parse_hex(&c[91..93])?,
171 parse_hex(&c[93..95])?,
172 ],
173 [
174 parse_hex(&c[97..99])?,
176 parse_hex(&c[99..101])?,
177 parse_hex(&c[101..103])?,
178 ],
179 [
180 parse_hex(&c[105..107])?,
182 parse_hex(&c[107..109])?,
183 parse_hex(&c[109..111])?,
184 ],
185 [
186 parse_hex(&c[113..115])?,
188 parse_hex(&c[115..117])?,
189 parse_hex(&c[117..119])?,
190 ],
191 [
192 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 #[must_use]
210 pub fn colours(&self) -> [[u8; 3]; 16] {
211 return match self {
212 ColourScheme::CLASSIC => [
213 [0x00, 0x00, 0x00], [0xAB, 0x00, 0x00], [0x00, 0xAB, 0x00], [0xAB, 0x57, 0x00], [0x00, 0x00, 0xAB], [0xAB, 0x00, 0xAB], [0x00, 0xAB, 0xAB], [0xAB, 0xAB, 0xAB], [0x57, 0x57, 0x57], [0xFF, 0x57, 0x57], [0x57, 0xFF, 0x57], [0xFF, 0xFF, 0x57], [0x57, 0x57, 0xFF], [0xFF, 0x57, 0xFF], [0x57, 0xFF, 0xFF], [0xFF, 0xFF, 0xFF], ],
232 ColourScheme::MODERN => [
233 [0x0A, 0x0A, 0x0A], [0x99, 0x4D, 0x4D], [0x8C, 0x99, 0x4D], [0xCC, 0x99, 0x66], [0x4D, 0x66, 0x99], [0xB3, 0x59, 0x86], [0x4D, 0x99, 0x99], [0x99, 0x99, 0x99], [0x4D, 0x4D, 0x4D], [0xCC, 0x7A, 0x7A], [0xBE, 0xCC, 0x7A], [0xFF, 0xCC, 0x99], [0x7A, 0x96, 0xCC], [0xE6, 0x8A, 0xB8], [0x7A, 0xCC, 0xCC], [0xE6, 0xE6, 0xE6], ],
252 ColourScheme::CATPPUCCIN => [
253 [0x23, 0x26, 0x34], [0xDB, 0x63, 0x63], [0x82, 0xBD, 0x64], [0xD4, 0xAA, 0x68], [0x6C, 0x8A, 0xE6], [0xE6, 0x93, 0xCD], [0x4E, 0xB5, 0xAB], [0xA5, 0xAD, 0xCE], [0x51, 0x57, 0x6D], [0xE7, 0x82, 0x84], [0xA6, 0xD1, 0x89], [0xE5, 0xC8, 0x90], [0x8C, 0xAA, 0xEE], [0xF4, 0xB8, 0xE4], [0x81, 0xC8, 0xBE], [0xC6, 0xD0, 0xF5], ],
272 ColourScheme::DRACULA => [
273 [0x21, 0x22, 0x2C], [0xFF, 0x55, 0x55], [0x50, 0xFA, 0x7B], [0xF1, 0xFA, 0x8C], [0xBD, 0x93, 0xF9], [0xFF, 0x79, 0xC6], [0x8B, 0xE9, 0xFD], [0xF8, 0xF8, 0xF2], [0x62, 0x72, 0xA4], [0xFF, 0x6E, 0x6E], [0x69, 0xFF, 0x94], [0xFF, 0xFF, 0xA5], [0xD6, 0xAC, 0xFF], [0xFF, 0x92, 0xDF], [0xA4, 0xFF, 0xFF], [0xFF, 0xFF, 0xFF], ],
292 ColourScheme::ROSEPINE => [
293 [0x19, 0x17, 0x24], [0xB4, 0x52, 0x6E], [0x8B, 0x95, 0x4D], [0xC4, 0x96, 0x56], [0x31, 0x74, 0x8F], [0x90, 0x7A, 0xA9], [0x56, 0x94, 0x9F], [0x90, 0x8C, 0xAA], [0x40, 0x3D, 0x52], [0xEB, 0x6F, 0x92], [0xB7, 0xC4, 0x6A], [0xF6, 0xC1, 0x77], [0x3E, 0x8F, 0xB0], [0xC4, 0xA7, 0xE7], [0x9C, 0xCF, 0xD8], [0xE0, 0xDE, 0xF4], ],
312 ColourScheme::CUSTOM(scheme) => *scheme,
313 };
314 }
315
316 #[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}