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<'a>(&'a self, color_ref: &'a str) -> Option<&'a str> {
270 if color_ref.starts_with('#') {
271 return Some(color_ref);
272 }
273
274 match color_ref.splitn(3, '.').collect::<Vec<_>>().as_slice() {
276 ["palette", ramp, idx_str] => {
277 let idx: usize = idx_str.parse().ok()?;
278 let value = self.palette.as_ref()?.get_ramp(ramp)?.get(idx)?;
279 self.resolve(value)
281 }
282 ["ansi", key] => Some(self.ansi.get(key)?),
283 ["base16", key] => {
284 let value = self.base16.as_ref()?.get(key)?;
285 self.resolve(value)
286 }
287 _ => None,
288 }
289 }
290
291 pub fn from_name(name: Option<&str>) -> Theme {
310 use crate::BuiltinTheme;
311 #[cfg(feature = "fs")]
312 {
313 use crate::util::load_theme_file;
314 use terminal_colorsaurus::{theme_mode, QueryOptions, ThemeMode};
315 name.and_then(|n| load_theme_file(n).ok())
317 .as_ref()
318 .and_then(|s| toml::from_str::<Theme>(s).ok())
319 .or_else(|| {
321 name.and_then(|n| {
322 let slug = heck::AsKebabCase(n).to_string();
323 slug.parse::<BuiltinTheme>().ok().map(|b| b.theme())
324 })
325 })
326 .or_else(|| {
329 crate::util::mode_aware_theme_name()
330 .and_then(|n| n.parse::<BuiltinTheme>().ok().map(|b| b.theme()))
331 })
332 .unwrap_or_else(|| match theme_mode(QueryOptions::default()).ok() {
334 Some(ThemeMode::Light) => BuiltinTheme::default_light().theme(),
335 _ => BuiltinTheme::default().theme(),
336 })
337 }
338 #[cfg(not(feature = "fs"))]
339 {
340 name.and_then(|n| n.parse::<BuiltinTheme>().ok().map(|b| b.theme()))
341 .unwrap_or_else(|| BuiltinTheme::default().theme())
342 }
343 }
344}
345
346pub fn hex_to_rgb(hex: &str) -> Result<(u8, u8, u8), HexColorError> {
366 let hex = hex.trim_start_matches('#');
367
368 if hex.len() != 6 {
369 return Err(HexColorError::InvalidLength(hex.len()));
370 }
371
372 let r = u8::from_str_radix(&hex[0..2], 16)?;
373 let g = u8::from_str_radix(&hex[2..4], 16)?;
374 let b = u8::from_str_radix(&hex[4..6], 16)?;
375 Ok((r, g, b))
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381
382 fn create_test_theme() -> Theme {
383 Theme {
384 meta: Meta {
385 name: "Test Theme".to_string(),
386 slug: Some("test".to_string()),
387 author: None,
388 version: None,
389 description: None,
390 dark: Some(true),
391 },
392 palette: Some(Palette(HashMap::from([
393 (
394 "neutral".into(),
395 vec![
396 "#1a1a1a".to_string(),
397 "#666666".to_string(),
398 "#fafafa".to_string(),
399 ],
400 ),
401 (
402 "red".into(),
403 vec!["#cc0000".to_string(), "ansi.bright_red".to_string()],
404 ),
405 ]))),
406 ansi: Ansi {
407 black: "#1a1a1a".to_string(),
408 red: "#cc0000".to_string(),
409 green: "#00ff00".to_string(),
410 yellow: "#ffff00".to_string(),
411 blue: "#0000ff".to_string(),
412 magenta: "#ff00ff".to_string(),
413 cyan: "#00ffff".to_string(),
414 white: "#fafafa".to_string(),
415 bright_black: "#666666".to_string(),
416 bright_red: "#ff5555".to_string(),
417 bright_green: "#00ff00".to_string(),
418 bright_yellow: "#ffff00".to_string(),
419 bright_blue: "#0000ff".to_string(),
420 bright_magenta: "#ff00ff".to_string(),
421 bright_cyan: "#00ffff".to_string(),
422 bright_white: "#ffffff".to_string(),
423 },
424 base16: None,
425 semantic: Semantic {
426 error: "palette.red.0".to_string(),
427 warning: "ansi.yellow".to_string(),
428 info: "ansi.blue".to_string(),
429 success: "ansi.green".to_string(),
430 highlight: "ansi.cyan".to_string(),
431 link: "palette.red.1".to_string(),
432 },
433 ui: Ui {
434 bg: UiBg {
435 primary: "palette.neutral.0".to_string(),
436 secondary: "palette.neutral.1".to_string(),
437 },
438 fg: UiFg {
439 primary: "palette.neutral.2".to_string(),
440 secondary: "palette.neutral.1".to_string(),
441 muted: "palette.neutral.1".to_string(),
442 },
443 border: UiBorder {
444 primary: "ansi.blue".to_string(),
445 muted: "palette.neutral.1".to_string(),
446 },
447 cursor: UiCursor {
448 primary: "ansi.white".to_string(),
449 muted: "palette.neutral.1".to_string(),
450 },
451 selection: UiSelection {
452 bg: "palette.neutral.1".to_string(),
453 fg: "palette.neutral.2".to_string(),
454 },
455 },
456 }
457 }
458
459 #[test]
460 fn test_resolve_hex_color() {
461 let theme = create_test_theme();
462 assert_eq!(theme.resolve("#ff0000"), Some("#ff0000"));
463 }
464
465 #[test]
466 fn test_resolve_palette_reference() {
467 let theme = create_test_theme();
468 assert_eq!(theme.resolve("palette.red.0"), Some("#cc0000"));
470 assert_eq!(theme.resolve("palette.red.1"), Some("#ff5555"));
471 assert_eq!(theme.resolve("palette.neutral.0"), Some("#1a1a1a"));
472 }
473
474 #[test]
475 fn test_resolve_ansi_reference() {
476 let theme = create_test_theme();
477 assert_eq!(theme.resolve("ansi.red"), Some("#cc0000"));
478 assert_eq!(theme.resolve("ansi.bright_black"), Some("#666666"));
479 }
480
481 #[test]
482 fn test_resolve_invalid() {
483 let theme = create_test_theme();
484 assert_eq!(theme.resolve("$nonexistent.3"), None);
485 assert_eq!(theme.resolve("invalid"), None);
486 assert_eq!(theme.resolve("palette.red.99"), None); }
488
489 #[test]
490 fn test_hex_to_rgb_with_hash() {
491 let (r, g, b) = hex_to_rgb("#ff5533").unwrap();
492 assert_eq!((r, g, b), (255, 85, 51));
493 }
494
495 #[test]
496 fn test_hex_to_rgb_without_hash() {
497 let (r, g, b) = hex_to_rgb("ff5533").unwrap();
498 assert_eq!((r, g, b), (255, 85, 51));
499 }
500
501 #[test]
502 fn test_hex_to_rgb_black() {
503 let (r, g, b) = hex_to_rgb("#000000").unwrap();
504 assert_eq!((r, g, b), (0, 0, 0));
505 }
506
507 #[test]
508 fn test_hex_to_rgb_white() {
509 let (r, g, b) = hex_to_rgb("#ffffff").unwrap();
510 assert_eq!((r, g, b), (255, 255, 255));
511 }
512
513 #[test]
514 fn test_hex_to_rgb_too_short() {
515 assert!(hex_to_rgb("#fff").is_err());
516 assert!(hex_to_rgb("abc").is_err());
517 }
518
519 #[test]
520 fn test_hex_to_rgb_too_long() {
521 assert!(hex_to_rgb("#ff5533aa").is_err());
522 }
523
524 #[test]
525 fn test_hex_to_rgb_invalid_chars() {
526 assert!(hex_to_rgb("#gggggg").is_err());
527 assert!(hex_to_rgb("#xyz123").is_err());
528 }
529
530 #[test]
531 fn test_hex_to_rgb_empty() {
532 assert!(hex_to_rgb("").is_err());
533 assert!(hex_to_rgb("#").is_err());
534 }
535}