1use std::{collections::HashMap, str::FromStr};
2
3use anyhow::Result;
4use palette::{
5 FromColor, Lab, Mix, Srgb, Xyz,
6 chromatic_adaptation::AdaptIntoUnclamped,
7 white_point::{D50, D65},
8};
9use ratatui::style::Color;
10use serde::Deserialize;
11use syntect::highlighting::{StyleModifier, Theme, ThemeItem};
12
13#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, clap::ValueEnum)]
14pub enum Appearance {
15 #[default]
16 Dark,
17 Light,
18}
19
20impl Appearance {
21 pub fn toggle(self) -> Self {
22 if self == Self::Dark {
23 Self::Light
24 } else {
25 Self::Dark
26 }
27 }
28 pub fn name(self) -> &'static str {
29 if self == Self::Dark { "dark" } else { "light" }
30 }
31}
32
33#[derive(Clone)]
34pub struct Colors {
35 pub bg: Color,
36 pub fg: Color,
37 pub dim: Color,
38 pub chrome: Color,
39 pub separator: Color,
40 pub empty: Color,
41 pub context: Color,
42 pub added: Color,
43 pub deleted: Color,
44 pub accent: Color,
45 pub add_bg: Color,
46 pub del_bg: Color,
47 pub add_word: Color,
48 pub del_word: Color,
49}
50
51#[derive(Deserialize)]
52struct ThemeData {
53 colors: HashMap<String, String>,
54 #[serde(rename = "tokenColors")]
55 tokens: Vec<Token>,
56}
57
58#[derive(Deserialize)]
59struct Token {
60 scope: serde_json::Value,
61 settings: HashMap<String, String>,
62}
63
64fn rgb(value: &str) -> Result<Color> {
65 let value = value.trim_start_matches('#');
66 let n = u32::from_str_radix(value.get(..6).unwrap_or(value), 16)?;
67 Ok(Color::Rgb((n >> 16) as u8, (n >> 8) as u8, n as u8))
68}
69
70fn syntect_color(color: Color) -> syntect::highlighting::Color {
71 let Color::Rgb(r, g, b) = color else {
72 unreachable!("theme colors are RGB")
73 };
74 syntect::highlighting::Color { r, g, b, a: 255 }
75}
76
77pub fn load(appearance: Appearance) -> Result<(Colors, Theme)> {
78 let input = match appearance {
79 Appearance::Dark => include_str!("../assets/pierre/pierre-dark.json"),
80 Appearance::Light => include_str!("../assets/pierre/pierre-light.json"),
81 };
82 let data: ThemeData = serde_json::from_str(input)?;
83 let get = |key: &str| -> Result<Color> { rgb(&data.colors[key]) };
84 let bg = get("editor.background")?;
85 let fg = get("editor.foreground")?;
86 let added = get("gitDecoration.addedResourceForeground")?;
87 let deleted = get("gitDecoration.deletedResourceForeground")?;
88 let dark = appearance == Appearance::Dark;
89 let add_bg = lab_mix(bg, added, if dark { 0.20 } else { 0.12 });
90 let del_bg = lab_mix(bg, deleted, if dark { 0.20 } else { 0.12 });
91 let colors = Colors {
92 bg,
93 fg,
94 added,
95 deleted,
96 dim: get("editorLineNumber.foreground")?,
97 chrome: get("sideBar.background")?,
98 accent: get("gitDecoration.modifiedResourceForeground")?,
99 separator: lab_mix(bg, fg, if dark { 0.15 } else { 0.04 }),
100 empty: lab_mix(bg, fg, if dark { 0.18 } else { 0.10 }),
101 context: lab_mix(bg, fg, if dark { 0.075 } else { 0.015 }),
102 add_bg,
103 del_bg,
104 add_word: alpha_mix(add_bg, added, if dark { 0.20 } else { 0.15 }),
105 del_word: alpha_mix(del_bg, deleted, if dark { 0.20 } else { 0.15 }),
106 };
107 let mut syntax = Theme::default();
108 syntax.settings.foreground = Some(syntect_color(fg));
109 syntax.settings.background = Some(syntect_color(bg));
110 for token in data.tokens {
111 let scopes = match token.scope {
112 serde_json::Value::String(s) => s,
113 serde_json::Value::Array(items) => items
114 .iter()
115 .filter_map(|s| s.as_str())
116 .collect::<Vec<_>>()
117 .join(","),
118 _ => continue,
119 };
120 if let Some(fg) = token.settings.get("foreground") {
121 syntax.scopes.push(ThemeItem {
122 scope: FromStr::from_str(&scopes)?,
123 style: StyleModifier {
124 foreground: Some(syntect_color(rgb(fg)?)),
125 ..StyleModifier::default()
126 },
127 });
128 }
129 }
130 Ok((colors, syntax))
131}
132
133fn components(color: Color) -> (u8, u8, u8) {
134 let Color::Rgb(r, g, b) = color else {
135 unreachable!("theme colors are RGB")
136 };
137 (r, g, b)
138}
139
140fn lab_mix(a: Color, b: Color, amount: f32) -> Color {
141 let (r, g, blue) = components(a);
142 let a: Xyz<D65, f32> = Xyz::from_color(Srgb::new(r, g, blue).into_format::<f32>());
143 let a: Xyz<D50, f32> = a.adapt_into_unclamped();
144 let a = Lab::from_color(a);
145 let (r, g, blue) = components(b);
146 let b: Xyz<D65, f32> = Xyz::from_color(Srgb::new(r, g, blue).into_format::<f32>());
147 let b: Xyz<D50, f32> = b.adapt_into_unclamped();
148 let b = Lab::from_color(b);
149 let mixed: Xyz<D50, f32> = Xyz::from_color(a.mix(b, amount));
150 let adapted: Xyz<D65, f32> = mixed.adapt_into_unclamped();
151 let color: Srgb<u8> = Srgb::from_color(adapted).into_format();
152 Color::Rgb(color.red, color.green, color.blue)
153}
154
155fn alpha_mix(a: Color, b: Color, amount: f32) -> Color {
156 let a = components(a);
157 let b = components(b);
158 let blend = |a: u8, b: u8| (a as f32 * (1.0 - amount) + b as f32 * amount).round() as u8;
159 Color::Rgb(blend(a.0, b.0), blend(a.1, b.1), blend(a.2, b.2))
160}