1#![warn(missing_docs)]
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10mod builtin;
11pub use builtin::BuiltinTheme;
12
13#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
15pub enum HexColorError {
16 #[error("hex color must be 6 characters (got {0})")]
18 InvalidLength(usize),
19 #[error("invalid hex digit: {0}")]
21 InvalidHex(#[from] std::num::ParseIntError),
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
26pub struct Theme {
27 #[serde(rename = "theme")]
29 pub meta: Meta,
30 pub ansi: Ansi,
32 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub palette: Option<Palette>,
35 #[serde(skip_serializing_if = "Option::is_none")]
37 pub base16: Option<Base16>,
38 pub semantic: Semantic,
40 pub ui: Ui,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
46pub struct Meta {
47 pub name: String,
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub slug: Option<String>,
52 #[serde(skip_serializing_if = "Option::is_none")]
54 pub author: Option<String>,
55 #[serde(skip_serializing_if = "Option::is_none")]
57 pub version: Option<String>,
58 #[serde(skip_serializing_if = "Option::is_none")]
60 pub description: Option<String>,
61 #[serde(skip_serializing_if = "Option::is_none")]
63 pub dark: Option<bool>,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
70pub struct Ansi {
71 pub black: String,
73 pub red: String,
75 pub green: String,
77 pub yellow: String,
79 pub blue: String,
81 pub magenta: String,
83 pub cyan: String,
85 pub white: String,
87 pub bright_black: String,
89 pub bright_red: String,
91 pub bright_green: String,
93 pub bright_yellow: String,
95 pub bright_blue: String,
97 pub bright_magenta: String,
99 pub bright_cyan: String,
101 pub bright_white: String,
103}
104
105impl Ansi {
106 pub fn get(&self, key: &str) -> Option<&str> {
110 match key {
111 "black" => Some(&self.black),
112 "red" => Some(&self.red),
113 "green" => Some(&self.green),
114 "yellow" => Some(&self.yellow),
115 "blue" => Some(&self.blue),
116 "magenta" => Some(&self.magenta),
117 "cyan" => Some(&self.cyan),
118 "white" => Some(&self.white),
119 "bright_black" => Some(&self.bright_black),
120 "bright_red" => Some(&self.bright_red),
121 "bright_green" => Some(&self.bright_green),
122 "bright_yellow" => Some(&self.bright_yellow),
123 "bright_blue" => Some(&self.bright_blue),
124 "bright_magenta" => Some(&self.bright_magenta),
125 "bright_cyan" => Some(&self.bright_cyan),
126 "bright_white" => Some(&self.bright_white),
127 _ => None,
128 }
129 }
130}
131
132#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
138pub struct Palette(HashMap<String, Vec<String>>);
139
140impl Palette {
141 pub fn new(map: HashMap<String, Vec<String>>) -> Self {
143 Self(map)
144 }
145
146 pub fn get_ramp(&self, name: &str) -> Option<&[String]> {
148 self.0.get(name).map(|v| &**v)
149 }
150
151 pub fn ramp_names(&self) -> Vec<&str> {
153 let mut names: Vec<&str> = self.0.keys().map(String::as_str).collect();
154 names.sort();
155 names
156 }
157
158 pub fn entries(&self) -> impl Iterator<Item = (&str, &Vec<String>)> {
160 self.0.iter().map(|(k, v)| (k.as_str(), v))
161 }
162}
163
164#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
166pub struct Base16(HashMap<String, String>);
167
168impl Base16 {
169 pub fn new(map: HashMap<String, String>) -> Self {
171 Self(map)
172 }
173
174 pub fn get(&self, key: &str) -> Option<&str> {
176 self.0.get(key).map(String::as_str)
177 }
178
179 pub fn entries(&self) -> impl Iterator<Item = (&str, &str)> {
181 self.0.iter().map(|(k, v)| (k.as_str(), v.as_str()))
182 }
183}
184
185#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
187pub struct Semantic {
188 pub error: String,
190 pub warning: String,
192 pub info: String,
194 pub success: String,
196 pub highlight: String,
198 pub link: String,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
204pub struct UiBg {
205 pub primary: String,
207 pub secondary: String,
209}
210
211#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
213pub struct UiFg {
214 pub primary: String,
216 pub secondary: String,
218 pub muted: String,
220}
221
222#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
224pub struct UiBorder {
225 pub primary: String,
227 pub muted: String,
229}
230
231#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
233pub struct UiCursor {
234 pub primary: String,
236 pub muted: String,
238}
239
240#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
242pub struct UiSelection {
243 pub bg: String,
245 pub fg: String,
247}
248
249#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
251pub struct Ui {
252 pub bg: UiBg,
254 pub fg: UiFg,
256 pub border: UiBorder,
258 pub cursor: UiCursor,
260 pub selection: UiSelection,
262}
263
264impl Theme {
265 pub fn resolve(&self, color_ref: &str) -> Option<String> {
273 if color_ref.starts_with('#') {
274 return Some(color_ref.to_string());
275 }
276
277 let parts: Vec<&str> = color_ref.splitn(3, '.').collect();
279
280 match parts.as_slice() {
281 ["palette", ramp, idx_str] => {
282 let idx: usize = idx_str.parse().ok()?;
283 let value = self.palette.as_ref()?.get_ramp(ramp)?.get(idx)?;
284 self.resolve(value)
286 }
287 ["ansi", key] => Some(self.ansi.get(key)?.to_string()),
288 ["base16", key] => {
289 let value = self.base16.as_ref()?.get(key)?;
290 self.resolve(value)
291 }
292 _ => None,
293 }
294 }
295}
296
297pub fn hex_to_rgb(hex: &str) -> Result<(u8, u8, u8), HexColorError> {
317 let hex = hex.trim_start_matches('#');
318
319 if hex.len() != 6 {
320 return Err(HexColorError::InvalidLength(hex.len()));
321 }
322
323 let r = u8::from_str_radix(&hex[0..2], 16)?;
324 let g = u8::from_str_radix(&hex[2..4], 16)?;
325 let b = u8::from_str_radix(&hex[4..6], 16)?;
326 Ok((r, g, b))
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 fn create_test_theme() -> Theme {
334 Theme {
335 meta: Meta {
336 name: "Test Theme".to_string(),
337 slug: Some("test".to_string()),
338 author: None,
339 version: None,
340 description: None,
341 dark: Some(true),
342 },
343 palette: Some(Palette(HashMap::from([
344 (
345 "neutral".into(),
346 vec![
347 "#1a1a1a".to_string(),
348 "#666666".to_string(),
349 "#fafafa".to_string(),
350 ],
351 ),
352 (
353 "red".into(),
354 vec!["#cc0000".to_string(), "ansi.bright_red".to_string()],
355 ),
356 ]))),
357 ansi: Ansi {
358 black: "#1a1a1a".to_string(),
359 red: "#cc0000".to_string(),
360 green: "#00ff00".to_string(),
361 yellow: "#ffff00".to_string(),
362 blue: "#0000ff".to_string(),
363 magenta: "#ff00ff".to_string(),
364 cyan: "#00ffff".to_string(),
365 white: "#fafafa".to_string(),
366 bright_black: "#666666".to_string(),
367 bright_red: "#ff5555".to_string(),
368 bright_green: "#00ff00".to_string(),
369 bright_yellow: "#ffff00".to_string(),
370 bright_blue: "#0000ff".to_string(),
371 bright_magenta: "#ff00ff".to_string(),
372 bright_cyan: "#00ffff".to_string(),
373 bright_white: "#ffffff".to_string(),
374 },
375 base16: None,
376 semantic: Semantic {
377 error: "palette.red.0".to_string(),
378 warning: "ansi.yellow".to_string(),
379 info: "ansi.blue".to_string(),
380 success: "ansi.green".to_string(),
381 highlight: "ansi.cyan".to_string(),
382 link: "palette.red.1".to_string(),
383 },
384 ui: Ui {
385 bg: UiBg {
386 primary: "palette.neutral.0".to_string(),
387 secondary: "palette.neutral.1".to_string(),
388 },
389 fg: UiFg {
390 primary: "palette.neutral.2".to_string(),
391 secondary: "palette.neutral.1".to_string(),
392 muted: "palette.neutral.1".to_string(),
393 },
394 border: UiBorder {
395 primary: "ansi.blue".to_string(),
396 muted: "palette.neutral.1".to_string(),
397 },
398 cursor: UiCursor {
399 primary: "ansi.white".to_string(),
400 muted: "palette.neutral.1".to_string(),
401 },
402 selection: UiSelection {
403 bg: "palette.neutral.1".to_string(),
404 fg: "palette.neutral.2".to_string(),
405 },
406 },
407 }
408 }
409
410 #[test]
411 fn test_resolve_hex_color() {
412 let theme = create_test_theme();
413 assert_eq!(theme.resolve("#ff0000"), Some("#ff0000".to_string()));
414 }
415
416 #[test]
417 fn test_resolve_palette_reference() {
418 let theme = create_test_theme();
419 assert_eq!(theme.resolve("palette.red.0"), Some("#cc0000".to_string()));
421 assert_eq!(theme.resolve("palette.red.1"), Some("#ff5555".to_string()));
422 assert_eq!(
423 theme.resolve("palette.neutral.0"),
424 Some("#1a1a1a".to_string())
425 );
426 }
427
428 #[test]
429 fn test_resolve_ansi_reference() {
430 let theme = create_test_theme();
431 assert_eq!(theme.resolve("ansi.red"), Some("#cc0000".to_string()));
432 assert_eq!(
433 theme.resolve("ansi.bright_black"),
434 Some("#666666".to_string())
435 );
436 }
437
438 #[test]
439 fn test_resolve_invalid() {
440 let theme = create_test_theme();
441 assert_eq!(theme.resolve("$nonexistent.3"), None);
442 assert_eq!(theme.resolve("invalid"), None);
443 assert_eq!(theme.resolve("palette.red.99"), None); }
445
446 #[test]
447 fn test_hex_to_rgb_with_hash() {
448 let (r, g, b) = hex_to_rgb("#ff5533").unwrap();
449 assert_eq!((r, g, b), (255, 85, 51));
450 }
451
452 #[test]
453 fn test_hex_to_rgb_without_hash() {
454 let (r, g, b) = hex_to_rgb("ff5533").unwrap();
455 assert_eq!((r, g, b), (255, 85, 51));
456 }
457
458 #[test]
459 fn test_hex_to_rgb_black() {
460 let (r, g, b) = hex_to_rgb("#000000").unwrap();
461 assert_eq!((r, g, b), (0, 0, 0));
462 }
463
464 #[test]
465 fn test_hex_to_rgb_white() {
466 let (r, g, b) = hex_to_rgb("#ffffff").unwrap();
467 assert_eq!((r, g, b), (255, 255, 255));
468 }
469
470 #[test]
471 fn test_hex_to_rgb_too_short() {
472 assert!(hex_to_rgb("#fff").is_err());
473 assert!(hex_to_rgb("abc").is_err());
474 }
475
476 #[test]
477 fn test_hex_to_rgb_too_long() {
478 assert!(hex_to_rgb("#ff5533aa").is_err());
479 }
480
481 #[test]
482 fn test_hex_to_rgb_invalid_chars() {
483 assert!(hex_to_rgb("#gggggg").is_err());
484 assert!(hex_to_rgb("#xyz123").is_err());
485 }
486
487 #[test]
488 fn test_hex_to_rgb_empty() {
489 assert!(hex_to_rgb("").is_err());
490 assert!(hex_to_rgb("#").is_err());
491 }
492}