1#![warn(missing_docs)]
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
12pub enum HexColorError {
13 #[error("hex color must be 6 characters (got {0})")]
15 InvalidLength(usize),
16 #[error("invalid hex digit: {0}")]
18 InvalidHex(#[from] std::num::ParseIntError),
19}
20
21#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
23pub struct Theme {
24 #[serde(rename = "theme")]
26 pub meta: Meta,
27 pub ansi: Ansi,
29 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub palette: Option<Palette>,
32 #[serde(skip_serializing_if = "Option::is_none")]
34 pub base16: Option<Base16>,
35 pub semantic: Semantic,
37 pub ui: Ui,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
43pub struct Meta {
44 pub name: String,
46 #[serde(skip_serializing_if = "Option::is_none")]
48 pub slug: Option<String>,
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub author: Option<String>,
52 #[serde(skip_serializing_if = "Option::is_none")]
54 pub version: Option<String>,
55 #[serde(skip_serializing_if = "Option::is_none")]
57 pub description: Option<String>,
58 #[serde(skip_serializing_if = "Option::is_none")]
60 pub dark: Option<bool>,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
67pub struct Ansi {
68 pub black: String,
70 pub red: String,
72 pub green: String,
74 pub yellow: String,
76 pub blue: String,
78 pub magenta: String,
80 pub cyan: String,
82 pub white: String,
84 pub bright_black: String,
86 pub bright_red: String,
88 pub bright_green: String,
90 pub bright_yellow: String,
92 pub bright_blue: String,
94 pub bright_magenta: String,
96 pub bright_cyan: String,
98 pub bright_white: String,
100}
101
102impl Ansi {
103 pub fn get(&self, key: &str) -> Option<&str> {
107 match key {
108 "black" => Some(&self.black),
109 "red" => Some(&self.red),
110 "green" => Some(&self.green),
111 "yellow" => Some(&self.yellow),
112 "blue" => Some(&self.blue),
113 "magenta" => Some(&self.magenta),
114 "cyan" => Some(&self.cyan),
115 "white" => Some(&self.white),
116 "bright_black" => Some(&self.bright_black),
117 "bright_red" => Some(&self.bright_red),
118 "bright_green" => Some(&self.bright_green),
119 "bright_yellow" => Some(&self.bright_yellow),
120 "bright_blue" => Some(&self.bright_blue),
121 "bright_magenta" => Some(&self.bright_magenta),
122 "bright_cyan" => Some(&self.bright_cyan),
123 "bright_white" => Some(&self.bright_white),
124 _ => None,
125 }
126 }
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
135pub struct Palette(HashMap<String, Vec<String>>);
136
137impl Palette {
138 pub fn new(map: HashMap<String, Vec<String>>) -> Self {
140 Self(map)
141 }
142
143 pub fn get_ramp(&self, name: &str) -> Option<&[String]> {
145 self.0.get(name).map(|v| &**v)
146 }
147
148 pub fn ramp_names(&self) -> Vec<&str> {
150 let mut names: Vec<&str> = self.0.keys().map(String::as_str).collect();
151 names.sort();
152 names
153 }
154
155 pub fn entries(&self) -> impl Iterator<Item = (&str, &Vec<String>)> {
157 self.0.iter().map(|(k, v)| (k.as_str(), v))
158 }
159}
160
161#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
163pub struct Base16(HashMap<String, String>);
164
165impl Base16 {
166 pub fn new(map: HashMap<String, String>) -> Self {
168 Self(map)
169 }
170
171 pub fn get(&self, key: &str) -> Option<&str> {
173 self.0.get(key).map(String::as_str)
174 }
175
176 pub fn entries(&self) -> impl Iterator<Item = (&str, &str)> {
178 self.0.iter().map(|(k, v)| (k.as_str(), v.as_str()))
179 }
180}
181
182#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
184pub struct Semantic {
185 pub error: String,
187 pub warning: String,
189 pub info: String,
191 pub success: String,
193 pub highlight: String,
195 pub link: String,
197}
198
199#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
201pub struct UiBg {
202 pub primary: String,
204 pub secondary: String,
206}
207
208#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
210pub struct UiFg {
211 pub primary: String,
213 pub secondary: String,
215 pub muted: String,
217}
218
219#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
221pub struct UiBorder {
222 pub primary: String,
224 pub muted: String,
226}
227
228#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
230pub struct UiCursor {
231 pub primary: String,
233 pub muted: String,
235}
236
237#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
239pub struct UiSelection {
240 pub bg: String,
242 pub fg: String,
244}
245
246#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
248pub struct Ui {
249 pub bg: UiBg,
251 pub fg: UiFg,
253 pub border: UiBorder,
255 pub cursor: UiCursor,
257 pub selection: UiSelection,
259}
260
261impl Theme {
262 pub fn resolve(&self, color_ref: &str) -> Option<String> {
270 if color_ref.starts_with('#') {
271 return Some(color_ref.to_string());
272 }
273
274 let parts: Vec<&str> = color_ref.splitn(3, '.').collect();
276
277 match parts.as_slice() {
278 ["palette", ramp, idx_str] => {
279 let idx: usize = idx_str.parse().ok()?;
280 let value = self.palette.as_ref()?.get_ramp(ramp)?.get(idx)?;
281 self.resolve(value)
283 }
284 ["ansi", key] => Some(self.ansi.get(key)?.to_string()),
285 ["base16", key] => {
286 let value = self.base16.as_ref()?.get(key)?;
287 self.resolve(value)
288 }
289 _ => None,
290 }
291 }
292}
293
294pub fn hex_to_rgb(hex: &str) -> Result<(u8, u8, u8), HexColorError> {
314 let hex = hex.trim_start_matches('#');
315
316 if hex.len() != 6 {
317 return Err(HexColorError::InvalidLength(hex.len()));
318 }
319
320 let r = u8::from_str_radix(&hex[0..2], 16)?;
321 let g = u8::from_str_radix(&hex[2..4], 16)?;
322 let b = u8::from_str_radix(&hex[4..6], 16)?;
323 Ok((r, g, b))
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329
330 fn create_test_theme() -> Theme {
331 Theme {
332 meta: Meta {
333 name: "Test Theme".to_string(),
334 slug: Some("test".to_string()),
335 author: None,
336 version: None,
337 description: None,
338 dark: Some(true),
339 },
340 palette: Some(Palette(HashMap::from([
341 (
342 "neutral".into(),
343 vec![
344 "#1a1a1a".to_string(),
345 "#666666".to_string(),
346 "#fafafa".to_string(),
347 ],
348 ),
349 (
350 "red".into(),
351 vec!["#cc0000".to_string(), "ansi.bright_red".to_string()],
352 ),
353 ]))),
354 ansi: Ansi {
355 black: "#1a1a1a".to_string(),
356 red: "#cc0000".to_string(),
357 green: "#00ff00".to_string(),
358 yellow: "#ffff00".to_string(),
359 blue: "#0000ff".to_string(),
360 magenta: "#ff00ff".to_string(),
361 cyan: "#00ffff".to_string(),
362 white: "#fafafa".to_string(),
363 bright_black: "#666666".to_string(),
364 bright_red: "#ff5555".to_string(),
365 bright_green: "#00ff00".to_string(),
366 bright_yellow: "#ffff00".to_string(),
367 bright_blue: "#0000ff".to_string(),
368 bright_magenta: "#ff00ff".to_string(),
369 bright_cyan: "#00ffff".to_string(),
370 bright_white: "#ffffff".to_string(),
371 },
372 base16: None,
373 semantic: Semantic {
374 error: "palette.red.0".to_string(),
375 warning: "ansi.yellow".to_string(),
376 info: "ansi.blue".to_string(),
377 success: "ansi.green".to_string(),
378 highlight: "ansi.cyan".to_string(),
379 link: "palette.red.1".to_string(),
380 },
381 ui: Ui {
382 bg: UiBg {
383 primary: "palette.neutral.0".to_string(),
384 secondary: "palette.neutral.1".to_string(),
385 },
386 fg: UiFg {
387 primary: "palette.neutral.2".to_string(),
388 secondary: "palette.neutral.1".to_string(),
389 muted: "palette.neutral.1".to_string(),
390 },
391 border: UiBorder {
392 primary: "ansi.blue".to_string(),
393 muted: "palette.neutral.1".to_string(),
394 },
395 cursor: UiCursor {
396 primary: "ansi.white".to_string(),
397 muted: "palette.neutral.1".to_string(),
398 },
399 selection: UiSelection {
400 bg: "palette.neutral.1".to_string(),
401 fg: "palette.neutral.2".to_string(),
402 },
403 },
404 }
405 }
406
407 #[test]
408 fn test_resolve_hex_color() {
409 let theme = create_test_theme();
410 assert_eq!(theme.resolve("#ff0000"), Some("#ff0000".to_string()));
411 }
412
413 #[test]
414 fn test_resolve_palette_reference() {
415 let theme = create_test_theme();
416 assert_eq!(theme.resolve("palette.red.0"), Some("#cc0000".to_string()));
418 assert_eq!(theme.resolve("palette.red.1"), Some("#ff5555".to_string()));
419 assert_eq!(
420 theme.resolve("palette.neutral.0"),
421 Some("#1a1a1a".to_string())
422 );
423 }
424
425 #[test]
426 fn test_resolve_ansi_reference() {
427 let theme = create_test_theme();
428 assert_eq!(theme.resolve("ansi.red"), Some("#cc0000".to_string()));
429 assert_eq!(
430 theme.resolve("ansi.bright_black"),
431 Some("#666666".to_string())
432 );
433 }
434
435 #[test]
436 fn test_resolve_invalid() {
437 let theme = create_test_theme();
438 assert_eq!(theme.resolve("$nonexistent.3"), None);
439 assert_eq!(theme.resolve("invalid"), None);
440 assert_eq!(theme.resolve("palette.red.99"), None); }
442
443 #[test]
444 fn test_hex_to_rgb_with_hash() {
445 let (r, g, b) = hex_to_rgb("#ff5533").unwrap();
446 assert_eq!((r, g, b), (255, 85, 51));
447 }
448
449 #[test]
450 fn test_hex_to_rgb_without_hash() {
451 let (r, g, b) = hex_to_rgb("ff5533").unwrap();
452 assert_eq!((r, g, b), (255, 85, 51));
453 }
454
455 #[test]
456 fn test_hex_to_rgb_black() {
457 let (r, g, b) = hex_to_rgb("#000000").unwrap();
458 assert_eq!((r, g, b), (0, 0, 0));
459 }
460
461 #[test]
462 fn test_hex_to_rgb_white() {
463 let (r, g, b) = hex_to_rgb("#ffffff").unwrap();
464 assert_eq!((r, g, b), (255, 255, 255));
465 }
466
467 #[test]
468 fn test_hex_to_rgb_too_short() {
469 assert!(hex_to_rgb("#fff").is_err());
470 assert!(hex_to_rgb("abc").is_err());
471 }
472
473 #[test]
474 fn test_hex_to_rgb_too_long() {
475 assert!(hex_to_rgb("#ff5533aa").is_err());
476 }
477
478 #[test]
479 fn test_hex_to_rgb_invalid_chars() {
480 assert!(hex_to_rgb("#gggggg").is_err());
481 assert!(hex_to_rgb("#xyz123").is_err());
482 }
483
484 #[test]
485 fn test_hex_to_rgb_empty() {
486 assert!(hex_to_rgb("").is_err());
487 assert!(hex_to_rgb("#").is_err());
488 }
489}