1mod theme_printer;
2
3use std::borrow::Cow;
4use std::collections::HashMap;
5use std::fmt::{self, Debug};
6
7use anyhow::{anyhow, Context, Result};
8use clap::Parser;
9use gpui2::{hsla, rgb, serde_json, AssetSource, Hsla, SharedString};
10use log::LevelFilter;
11use rust_embed::RustEmbed;
12use serde::de::Visitor;
13use serde::{Deserialize, Deserializer};
14use simplelog::SimpleLogger;
15use theme2::{PlayerTheme, SyntaxTheme};
16
17use crate::theme_printer::ThemePrinter;
18
19#[derive(Parser)]
20#[command(author, version, about, long_about = None)]
21struct Args {
22 /// The name of the theme to convert.
23 theme: String,
24}
25
26fn main() -> Result<()> {
27 SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
28
29 let args = Args::parse();
30
31 let (json_theme, legacy_theme) = load_theme(args.theme)?;
32
33 let theme = convert_theme(json_theme, legacy_theme)?;
34
35 println!("{:#?}", ThemePrinter::new(theme));
36
37 Ok(())
38}
39
40#[derive(RustEmbed)]
41#[folder = "../../assets"]
42#[include = "fonts/**/*"]
43#[include = "icons/**/*"]
44#[include = "themes/**/*"]
45#[include = "sounds/**/*"]
46#[include = "*.md"]
47#[exclude = "*.DS_Store"]
48pub struct Assets;
49
50impl AssetSource for Assets {
51 fn load(&self, path: &str) -> Result<Cow<[u8]>> {
52 Self::get(path)
53 .map(|f| f.data)
54 .ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
55 }
56
57 fn list(&self, path: &str) -> Result<Vec<SharedString>> {
58 Ok(Self::iter()
59 .filter(|p| p.starts_with(path))
60 .map(SharedString::from)
61 .collect())
62 }
63}
64
65#[derive(Clone, Copy)]
66pub struct PlayerThemeColors {
67 pub cursor: Hsla,
68 pub selection: Hsla,
69}
70
71impl PlayerThemeColors {
72 pub fn new(theme: &LegacyTheme, ix: usize) -> Self {
73 if ix < theme.players.len() {
74 Self {
75 cursor: theme.players[ix].cursor,
76 selection: theme.players[ix].selection,
77 }
78 } else {
79 Self {
80 cursor: rgb::<Hsla>(0xff00ff),
81 selection: rgb::<Hsla>(0xff00ff),
82 }
83 }
84 }
85}
86
87impl From<PlayerThemeColors> for PlayerTheme {
88 fn from(value: PlayerThemeColors) -> Self {
89 Self {
90 cursor: value.cursor,
91 selection: value.selection,
92 }
93 }
94}
95
96fn convert_theme(json_theme: JsonTheme, legacy_theme: LegacyTheme) -> Result<theme2::Theme> {
97 let transparent = hsla(0.0, 0.0, 0.0, 0.0);
98
99 let players: [PlayerTheme; 8] = [
100 PlayerThemeColors::new(&legacy_theme, 0).into(),
101 PlayerThemeColors::new(&legacy_theme, 1).into(),
102 PlayerThemeColors::new(&legacy_theme, 2).into(),
103 PlayerThemeColors::new(&legacy_theme, 3).into(),
104 PlayerThemeColors::new(&legacy_theme, 4).into(),
105 PlayerThemeColors::new(&legacy_theme, 5).into(),
106 PlayerThemeColors::new(&legacy_theme, 6).into(),
107 PlayerThemeColors::new(&legacy_theme, 7).into(),
108 ];
109
110 let theme = theme2::Theme {
111 metadata: theme2::ThemeMetadata {
112 name: legacy_theme.name.clone().into(),
113 is_light: legacy_theme.is_light,
114 },
115 transparent,
116 mac_os_traffic_light_red: rgb::<Hsla>(0xEC695E),
117 mac_os_traffic_light_yellow: rgb::<Hsla>(0xF4BF4F),
118 mac_os_traffic_light_green: rgb::<Hsla>(0x62C554),
119 border: legacy_theme.lowest.base.default.border,
120 border_variant: legacy_theme.lowest.variant.default.border,
121 border_focused: legacy_theme.lowest.accent.default.border,
122 border_transparent: transparent,
123 elevated_surface: legacy_theme.lowest.base.default.background,
124 surface: legacy_theme.middle.base.default.background,
125 background: legacy_theme.lowest.base.default.background,
126 filled_element: legacy_theme.lowest.base.default.background,
127 filled_element_hover: hsla(0.0, 0.0, 100.0, 0.12),
128 filled_element_active: hsla(0.0, 0.0, 100.0, 0.16),
129 filled_element_selected: legacy_theme.lowest.accent.default.background,
130 filled_element_disabled: transparent,
131 ghost_element: transparent,
132 ghost_element_hover: hsla(0.0, 0.0, 100.0, 0.08),
133 ghost_element_active: hsla(0.0, 0.0, 100.0, 0.12),
134 ghost_element_selected: legacy_theme.lowest.accent.default.background,
135 ghost_element_disabled: transparent,
136 text: legacy_theme.lowest.base.default.foreground,
137 text_muted: legacy_theme.lowest.variant.default.foreground,
138 /// TODO: map this to a real value
139 text_placeholder: legacy_theme.lowest.negative.default.foreground,
140 text_disabled: legacy_theme.lowest.base.disabled.foreground,
141 text_accent: legacy_theme.lowest.accent.default.foreground,
142 icon_muted: legacy_theme.lowest.variant.default.foreground,
143 syntax: SyntaxTheme {
144 highlights: json_theme
145 .editor
146 .syntax
147 .iter()
148 .map(|(token, style)| (token.clone(), style.color.clone().into()))
149 .collect(),
150 },
151 status_bar: legacy_theme.lowest.base.default.background,
152 title_bar: legacy_theme.lowest.base.default.background,
153 toolbar: legacy_theme.highest.base.default.background,
154 tab_bar: legacy_theme.middle.base.default.background,
155 editor: legacy_theme.highest.base.default.background,
156 editor_subheader: legacy_theme.middle.base.default.background,
157 terminal: legacy_theme.highest.base.default.background,
158 editor_active_line: legacy_theme.highest.on.default.background,
159 image_fallback_background: legacy_theme.lowest.base.default.background,
160
161 git_created: legacy_theme.lowest.positive.default.foreground,
162 git_modified: legacy_theme.lowest.accent.default.foreground,
163 git_deleted: legacy_theme.lowest.negative.default.foreground,
164 git_conflict: legacy_theme.lowest.warning.default.foreground,
165 git_ignored: legacy_theme.lowest.base.disabled.foreground,
166 git_renamed: legacy_theme.lowest.warning.default.foreground,
167
168 players,
169 };
170
171 Ok(theme)
172}
173
174#[derive(Deserialize)]
175struct JsonTheme {
176 pub editor: JsonEditorTheme,
177 pub base_theme: serde_json::Value,
178}
179
180#[derive(Deserialize)]
181struct JsonEditorTheme {
182 pub syntax: HashMap<String, JsonSyntaxStyle>,
183}
184
185#[derive(Deserialize)]
186struct JsonSyntaxStyle {
187 pub color: Hsla,
188}
189
190/// Loads the [`Theme`] with the given name.
191fn load_theme(name: String) -> Result<(JsonTheme, LegacyTheme)> {
192 let theme_contents = Assets::get(&format!("themes/{name}.json"))
193 .with_context(|| format!("theme file not found: '{name}'"))?;
194
195 let json_theme: JsonTheme = serde_json::from_str(std::str::from_utf8(&theme_contents.data)?)
196 .context("failed to parse legacy theme")?;
197
198 let legacy_theme: LegacyTheme = serde_json::from_value(json_theme.base_theme.clone())
199 .context("failed to parse `base_theme`")?;
200
201 Ok((json_theme, legacy_theme))
202}
203
204#[derive(Deserialize, Clone, Default, Debug)]
205pub struct LegacyTheme {
206 pub name: String,
207 pub is_light: bool,
208 pub lowest: Layer,
209 pub middle: Layer,
210 pub highest: Layer,
211 pub popover_shadow: Shadow,
212 pub modal_shadow: Shadow,
213 #[serde(deserialize_with = "deserialize_player_colors")]
214 pub players: Vec<PlayerColors>,
215 #[serde(deserialize_with = "deserialize_syntax_colors")]
216 pub syntax: HashMap<String, Hsla>,
217}
218
219#[derive(Deserialize, Clone, Default, Debug)]
220pub struct Layer {
221 pub base: StyleSet,
222 pub variant: StyleSet,
223 pub on: StyleSet,
224 pub accent: StyleSet,
225 pub positive: StyleSet,
226 pub warning: StyleSet,
227 pub negative: StyleSet,
228}
229
230#[derive(Deserialize, Clone, Default, Debug)]
231pub struct StyleSet {
232 #[serde(rename = "default")]
233 pub default: ContainerColors,
234 pub hovered: ContainerColors,
235 pub pressed: ContainerColors,
236 pub active: ContainerColors,
237 pub disabled: ContainerColors,
238 pub inverted: ContainerColors,
239}
240
241#[derive(Deserialize, Clone, Default, Debug)]
242pub struct ContainerColors {
243 pub background: Hsla,
244 pub foreground: Hsla,
245 pub border: Hsla,
246}
247
248#[derive(Deserialize, Clone, Default, Debug)]
249pub struct PlayerColors {
250 pub selection: Hsla,
251 pub cursor: Hsla,
252}
253
254#[derive(Deserialize, Clone, Default, Debug)]
255pub struct Shadow {
256 pub blur: u8,
257 pub color: Hsla,
258 pub offset: Vec<u8>,
259}
260
261fn deserialize_player_colors<'de, D>(deserializer: D) -> Result<Vec<PlayerColors>, D::Error>
262where
263 D: Deserializer<'de>,
264{
265 struct PlayerArrayVisitor;
266
267 impl<'de> Visitor<'de> for PlayerArrayVisitor {
268 type Value = Vec<PlayerColors>;
269
270 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
271 formatter.write_str("an object with integer keys")
272 }
273
274 fn visit_map<A: serde::de::MapAccess<'de>>(
275 self,
276 mut map: A,
277 ) -> Result<Self::Value, A::Error> {
278 let mut players = Vec::with_capacity(8);
279 while let Some((key, value)) = map.next_entry::<usize, PlayerColors>()? {
280 if key < 8 {
281 players.push(value);
282 } else {
283 return Err(serde::de::Error::invalid_value(
284 serde::de::Unexpected::Unsigned(key as u64),
285 &"a key in range 0..7",
286 ));
287 }
288 }
289 Ok(players)
290 }
291 }
292
293 deserializer.deserialize_map(PlayerArrayVisitor)
294}
295
296fn deserialize_syntax_colors<'de, D>(deserializer: D) -> Result<HashMap<String, Hsla>, D::Error>
297where
298 D: serde::Deserializer<'de>,
299{
300 #[derive(Deserialize)]
301 struct ColorWrapper {
302 color: Hsla,
303 }
304
305 struct SyntaxVisitor;
306
307 impl<'de> Visitor<'de> for SyntaxVisitor {
308 type Value = HashMap<String, Hsla>;
309
310 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
311 formatter.write_str("a map with keys and objects with a single color field as values")
312 }
313
314 fn visit_map<M>(self, mut map: M) -> Result<HashMap<String, Hsla>, M::Error>
315 where
316 M: serde::de::MapAccess<'de>,
317 {
318 let mut result = HashMap::new();
319 while let Some(key) = map.next_key()? {
320 let wrapper: ColorWrapper = map.next_value()?; // Deserialize values as Hsla
321 result.insert(key, wrapper.color);
322 }
323 Ok(result)
324 }
325 }
326 deserializer.deserialize_map(SyntaxVisitor)
327}