1use std::borrow::Cow;
2use std::collections::HashMap;
3use std::fmt::{self, Debug};
4
5use anyhow::{anyhow, Context, Result};
6use clap::Parser;
7use gpui2::{hsla, rgb, serde_json, AssetSource, Hsla, Rgba, SharedString};
8use log::LevelFilter;
9use rust_embed::RustEmbed;
10use serde::de::Visitor;
11use serde::{Deserialize, Deserializer};
12use simplelog::SimpleLogger;
13use theme2::{PlayerTheme, SyntaxTheme, ThemeMetadata};
14
15#[derive(Parser)]
16#[command(author, version, about, long_about = None)]
17struct Args {
18 /// The name of the theme to convert.
19 theme: String,
20}
21
22fn main() -> Result<()> {
23 SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
24
25 let args = Args::parse();
26
27 let legacy_theme = load_theme(args.theme)?;
28
29 let theme = convert_theme(legacy_theme)?;
30
31 println!("{:#?}", ThemePrinter(theme));
32
33 Ok(())
34}
35
36#[derive(RustEmbed)]
37#[folder = "../../assets"]
38#[include = "fonts/**/*"]
39#[include = "icons/**/*"]
40#[include = "themes/**/*"]
41#[include = "sounds/**/*"]
42#[include = "*.md"]
43#[exclude = "*.DS_Store"]
44pub struct Assets;
45
46impl AssetSource for Assets {
47 fn load(&self, path: &str) -> Result<Cow<[u8]>> {
48 Self::get(path)
49 .map(|f| f.data)
50 .ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
51 }
52
53 fn list(&self, path: &str) -> Result<Vec<SharedString>> {
54 Ok(Self::iter()
55 .filter(|p| p.starts_with(path))
56 .map(SharedString::from)
57 .collect())
58 }
59}
60
61#[derive(Clone, Copy)]
62pub struct PlayerThemeColors {
63 pub cursor: Hsla,
64 pub selection: Hsla,
65}
66
67impl PlayerThemeColors {
68 pub fn new(theme: &LegacyTheme, ix: usize) -> Self {
69 if ix < theme.players.len() {
70 Self {
71 cursor: theme.players[ix].cursor,
72 selection: theme.players[ix].selection,
73 }
74 } else {
75 Self {
76 cursor: rgb::<Hsla>(0xff00ff),
77 selection: rgb::<Hsla>(0xff00ff),
78 }
79 }
80 }
81}
82
83impl From<PlayerThemeColors> for PlayerTheme {
84 fn from(value: PlayerThemeColors) -> Self {
85 Self {
86 cursor: value.cursor,
87 selection: value.selection,
88 }
89 }
90}
91
92#[derive(Clone, Copy)]
93pub struct SyntaxColor {
94 pub comment: Hsla,
95 pub string: Hsla,
96 pub function: Hsla,
97 pub keyword: Hsla,
98}
99
100impl Debug for SyntaxColor {
101 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102 f.debug_struct("SyntaxColor")
103 .field("comment", &HslaPrinter(self.comment))
104 .field("string", &HslaPrinter(self.string))
105 .field("function", &HslaPrinter(self.function))
106 .field("keyword", &HslaPrinter(self.keyword))
107 .finish()
108 }
109}
110
111impl SyntaxColor {
112 pub fn new(theme: &LegacyTheme) -> Self {
113 Self {
114 comment: theme
115 .syntax
116 .get("comment")
117 .cloned()
118 .unwrap_or_else(|| rgb::<Hsla>(0xff00ff)),
119 string: theme
120 .syntax
121 .get("string")
122 .cloned()
123 .unwrap_or_else(|| rgb::<Hsla>(0xff00ff)),
124 function: theme
125 .syntax
126 .get("function")
127 .cloned()
128 .unwrap_or_else(|| rgb::<Hsla>(0xff00ff)),
129 keyword: theme
130 .syntax
131 .get("keyword")
132 .cloned()
133 .unwrap_or_else(|| rgb::<Hsla>(0xff00ff)),
134 }
135 }
136}
137
138impl From<SyntaxColor> for SyntaxTheme {
139 fn from(value: SyntaxColor) -> Self {
140 Self {
141 comment: value.comment,
142 string: value.string,
143 keyword: value.keyword,
144 function: value.function,
145 highlights: Vec::new(),
146 }
147 }
148}
149
150fn convert_theme(theme: LegacyTheme) -> Result<theme2::Theme> {
151 let transparent = hsla(0.0, 0.0, 0.0, 0.0);
152
153 let players: [PlayerTheme; 8] = [
154 PlayerThemeColors::new(&theme, 0).into(),
155 PlayerThemeColors::new(&theme, 1).into(),
156 PlayerThemeColors::new(&theme, 2).into(),
157 PlayerThemeColors::new(&theme, 3).into(),
158 PlayerThemeColors::new(&theme, 4).into(),
159 PlayerThemeColors::new(&theme, 5).into(),
160 PlayerThemeColors::new(&theme, 6).into(),
161 PlayerThemeColors::new(&theme, 7).into(),
162 ];
163
164 let theme = theme2::Theme {
165 metadata: theme2::ThemeMetadata {
166 name: theme.name.clone().into(),
167 is_light: theme.is_light,
168 },
169 transparent,
170 mac_os_traffic_light_red: rgb::<Hsla>(0xEC695E),
171 mac_os_traffic_light_yellow: rgb::<Hsla>(0xF4BF4F),
172 mac_os_traffic_light_green: rgb::<Hsla>(0x62C554),
173 border: theme.lowest.base.default.border,
174 border_variant: theme.lowest.variant.default.border,
175 border_focused: theme.lowest.accent.default.border,
176 border_transparent: transparent,
177 elevated_surface: theme.lowest.base.default.background,
178 surface: theme.middle.base.default.background,
179 background: theme.lowest.base.default.background,
180 filled_element: theme.lowest.base.default.background,
181 filled_element_hover: hsla(0.0, 0.0, 100.0, 0.12),
182 filled_element_active: hsla(0.0, 0.0, 100.0, 0.16),
183 filled_element_selected: theme.lowest.accent.default.background,
184 filled_element_disabled: transparent,
185 ghost_element: transparent,
186 ghost_element_hover: hsla(0.0, 0.0, 100.0, 0.08),
187 ghost_element_active: hsla(0.0, 0.0, 100.0, 0.12),
188 ghost_element_selected: theme.lowest.accent.default.background,
189 ghost_element_disabled: transparent,
190 text: theme.lowest.base.default.foreground,
191 text_muted: theme.lowest.variant.default.foreground,
192 /// TODO: map this to a real value
193 text_placeholder: theme.lowest.negative.default.foreground,
194 text_disabled: theme.lowest.base.disabled.foreground,
195 text_accent: theme.lowest.accent.default.foreground,
196 icon_muted: theme.lowest.variant.default.foreground,
197 syntax: SyntaxColor::new(&theme).into(),
198
199 status_bar: theme.lowest.base.default.background,
200 title_bar: theme.lowest.base.default.background,
201 toolbar: theme.highest.base.default.background,
202 tab_bar: theme.middle.base.default.background,
203 editor: theme.highest.base.default.background,
204 editor_subheader: theme.middle.base.default.background,
205 terminal: theme.highest.base.default.background,
206 editor_active_line: theme.highest.on.default.background,
207 image_fallback_background: theme.lowest.base.default.background,
208
209 git_created: theme.lowest.positive.default.foreground,
210 git_modified: theme.lowest.accent.default.foreground,
211 git_deleted: theme.lowest.negative.default.foreground,
212 git_conflict: theme.lowest.warning.default.foreground,
213 git_ignored: theme.lowest.base.disabled.foreground,
214 git_renamed: theme.lowest.warning.default.foreground,
215
216 players,
217 };
218
219 Ok(theme)
220}
221
222#[derive(Deserialize)]
223struct JsonTheme {
224 pub base_theme: serde_json::Value,
225}
226
227/// Loads the [`Theme`] with the given name.
228pub fn load_theme(name: String) -> Result<LegacyTheme> {
229 let theme_contents = Assets::get(&format!("themes/{name}.json"))
230 .with_context(|| format!("theme file not found: '{name}'"))?;
231
232 let json_theme: JsonTheme = serde_json::from_str(std::str::from_utf8(&theme_contents.data)?)
233 .context("failed to parse legacy theme")?;
234
235 let legacy_theme: LegacyTheme = serde_json::from_value(json_theme.base_theme.clone())
236 .context("failed to parse `base_theme`")?;
237
238 Ok(legacy_theme)
239}
240
241#[derive(Deserialize, Clone, Default, Debug)]
242pub struct LegacyTheme {
243 pub name: String,
244 pub is_light: bool,
245 pub lowest: Layer,
246 pub middle: Layer,
247 pub highest: Layer,
248 pub popover_shadow: Shadow,
249 pub modal_shadow: Shadow,
250 #[serde(deserialize_with = "deserialize_player_colors")]
251 pub players: Vec<PlayerColors>,
252 #[serde(deserialize_with = "deserialize_syntax_colors")]
253 pub syntax: HashMap<String, Hsla>,
254}
255
256#[derive(Deserialize, Clone, Default, Debug)]
257pub struct Layer {
258 pub base: StyleSet,
259 pub variant: StyleSet,
260 pub on: StyleSet,
261 pub accent: StyleSet,
262 pub positive: StyleSet,
263 pub warning: StyleSet,
264 pub negative: StyleSet,
265}
266
267#[derive(Deserialize, Clone, Default, Debug)]
268pub struct StyleSet {
269 #[serde(rename = "default")]
270 pub default: ContainerColors,
271 pub hovered: ContainerColors,
272 pub pressed: ContainerColors,
273 pub active: ContainerColors,
274 pub disabled: ContainerColors,
275 pub inverted: ContainerColors,
276}
277
278#[derive(Deserialize, Clone, Default, Debug)]
279pub struct ContainerColors {
280 pub background: Hsla,
281 pub foreground: Hsla,
282 pub border: Hsla,
283}
284
285#[derive(Deserialize, Clone, Default, Debug)]
286pub struct PlayerColors {
287 pub selection: Hsla,
288 pub cursor: Hsla,
289}
290
291#[derive(Deserialize, Clone, Default, Debug)]
292pub struct Shadow {
293 pub blur: u8,
294 pub color: Hsla,
295 pub offset: Vec<u8>,
296}
297
298fn deserialize_player_colors<'de, D>(deserializer: D) -> Result<Vec<PlayerColors>, D::Error>
299where
300 D: Deserializer<'de>,
301{
302 struct PlayerArrayVisitor;
303
304 impl<'de> Visitor<'de> for PlayerArrayVisitor {
305 type Value = Vec<PlayerColors>;
306
307 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
308 formatter.write_str("an object with integer keys")
309 }
310
311 fn visit_map<A: serde::de::MapAccess<'de>>(
312 self,
313 mut map: A,
314 ) -> Result<Self::Value, A::Error> {
315 let mut players = Vec::with_capacity(8);
316 while let Some((key, value)) = map.next_entry::<usize, PlayerColors>()? {
317 if key < 8 {
318 players.push(value);
319 } else {
320 return Err(serde::de::Error::invalid_value(
321 serde::de::Unexpected::Unsigned(key as u64),
322 &"a key in range 0..7",
323 ));
324 }
325 }
326 Ok(players)
327 }
328 }
329
330 deserializer.deserialize_map(PlayerArrayVisitor)
331}
332
333fn deserialize_syntax_colors<'de, D>(deserializer: D) -> Result<HashMap<String, Hsla>, D::Error>
334where
335 D: serde::Deserializer<'de>,
336{
337 #[derive(Deserialize)]
338 struct ColorWrapper {
339 color: Hsla,
340 }
341
342 struct SyntaxVisitor;
343
344 impl<'de> Visitor<'de> for SyntaxVisitor {
345 type Value = HashMap<String, Hsla>;
346
347 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
348 formatter.write_str("a map with keys and objects with a single color field as values")
349 }
350
351 fn visit_map<M>(self, mut map: M) -> Result<HashMap<String, Hsla>, M::Error>
352 where
353 M: serde::de::MapAccess<'de>,
354 {
355 let mut result = HashMap::new();
356 while let Some(key) = map.next_key()? {
357 let wrapper: ColorWrapper = map.next_value()?; // Deserialize values as Hsla
358 result.insert(key, wrapper.color);
359 }
360 Ok(result)
361 }
362 }
363 deserializer.deserialize_map(SyntaxVisitor)
364}
365
366pub struct ThemePrinter(theme2::Theme);
367
368struct HslaPrinter(Hsla);
369
370impl Debug for HslaPrinter {
371 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
372 write!(f, "{:?}", IntoPrinter(&Rgba::from(self.0)))
373 }
374}
375
376struct IntoPrinter<'a, D: Debug>(&'a D);
377
378impl<'a, D: Debug> Debug for IntoPrinter<'a, D> {
379 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
380 write!(f, "{:?}.into()", self.0)
381 }
382}
383
384impl Debug for ThemePrinter {
385 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
386 f.debug_struct("Theme")
387 .field("metadata", &ThemeMetadataPrinter(self.0.metadata.clone()))
388 .field("transparent", &HslaPrinter(self.0.transparent))
389 .field(
390 "mac_os_traffic_light_red",
391 &HslaPrinter(self.0.mac_os_traffic_light_red),
392 )
393 .field(
394 "mac_os_traffic_light_yellow",
395 &HslaPrinter(self.0.mac_os_traffic_light_yellow),
396 )
397 .field(
398 "mac_os_traffic_light_green",
399 &HslaPrinter(self.0.mac_os_traffic_light_green),
400 )
401 .field("border", &HslaPrinter(self.0.border))
402 .field("border_variant", &HslaPrinter(self.0.border_variant))
403 .field("border_focused", &HslaPrinter(self.0.border_focused))
404 .field(
405 "border_transparent",
406 &HslaPrinter(self.0.border_transparent),
407 )
408 .field("elevated_surface", &HslaPrinter(self.0.elevated_surface))
409 .field("surface", &HslaPrinter(self.0.surface))
410 .field("background", &HslaPrinter(self.0.background))
411 .field("filled_element", &HslaPrinter(self.0.filled_element))
412 .field(
413 "filled_element_hover",
414 &HslaPrinter(self.0.filled_element_hover),
415 )
416 .field(
417 "filled_element_active",
418 &HslaPrinter(self.0.filled_element_active),
419 )
420 .field(
421 "filled_element_selected",
422 &HslaPrinter(self.0.filled_element_selected),
423 )
424 .field(
425 "filled_element_disabled",
426 &HslaPrinter(self.0.filled_element_disabled),
427 )
428 .field("ghost_element", &HslaPrinter(self.0.ghost_element))
429 .field(
430 "ghost_element_hover",
431 &HslaPrinter(self.0.ghost_element_hover),
432 )
433 .field(
434 "ghost_element_active",
435 &HslaPrinter(self.0.ghost_element_active),
436 )
437 .field(
438 "ghost_element_selected",
439 &HslaPrinter(self.0.ghost_element_selected),
440 )
441 .field(
442 "ghost_element_disabled",
443 &HslaPrinter(self.0.ghost_element_disabled),
444 )
445 .field("text", &HslaPrinter(self.0.text))
446 .field("text_muted", &HslaPrinter(self.0.text_muted))
447 .field("text_placeholder", &HslaPrinter(self.0.text_placeholder))
448 .field("text_disabled", &HslaPrinter(self.0.text_disabled))
449 .field("text_accent", &HslaPrinter(self.0.text_accent))
450 .field("icon_muted", &HslaPrinter(self.0.icon_muted))
451 .field("syntax", &SyntaxThemePrinter(self.0.syntax.clone()))
452 .field("status_bar", &HslaPrinter(self.0.status_bar))
453 .field("title_bar", &HslaPrinter(self.0.title_bar))
454 .field("toolbar", &HslaPrinter(self.0.toolbar))
455 .field("tab_bar", &HslaPrinter(self.0.tab_bar))
456 .field("editor", &HslaPrinter(self.0.editor))
457 .field("editor_subheader", &HslaPrinter(self.0.editor_subheader))
458 .field(
459 "editor_active_line",
460 &HslaPrinter(self.0.editor_active_line),
461 )
462 .field("terminal", &HslaPrinter(self.0.terminal))
463 .field(
464 "image_fallback_background",
465 &HslaPrinter(self.0.image_fallback_background),
466 )
467 .field("git_created", &HslaPrinter(self.0.git_created))
468 .field("git_modified", &HslaPrinter(self.0.git_modified))
469 .field("git_deleted", &HslaPrinter(self.0.git_deleted))
470 .field("git_conflict", &HslaPrinter(self.0.git_conflict))
471 .field("git_ignored", &HslaPrinter(self.0.git_ignored))
472 .field("git_renamed", &HslaPrinter(self.0.git_renamed))
473 .field("players", &self.0.players.map(PlayerThemePrinter))
474 .finish()
475 }
476}
477
478pub struct ThemeMetadataPrinter(ThemeMetadata);
479
480impl Debug for ThemeMetadataPrinter {
481 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
482 f.debug_struct("ThemeMetadata")
483 .field("name", &IntoPrinter(&self.0.name))
484 .field("is_light", &self.0.is_light)
485 .finish()
486 }
487}
488
489pub struct SyntaxThemePrinter(SyntaxTheme);
490
491impl Debug for SyntaxThemePrinter {
492 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
493 f.debug_struct("SyntaxTheme")
494 .field("comment", &HslaPrinter(self.0.comment))
495 .field("string", &HslaPrinter(self.0.string))
496 .field("function", &HslaPrinter(self.0.function))
497 .field("keyword", &HslaPrinter(self.0.keyword))
498 .field("highlights", &VecPrinter(&self.0.highlights))
499 .finish()
500 }
501}
502
503pub struct VecPrinter<'a, T>(&'a Vec<T>);
504
505impl<'a, T: Debug> Debug for VecPrinter<'a, T> {
506 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
507 write!(f, "vec!{:?}", &self.0)
508 }
509}
510
511pub struct PlayerThemePrinter(PlayerTheme);
512
513impl Debug for PlayerThemePrinter {
514 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
515 f.debug_struct("PlayerTheme")
516 .field("cursor", &HslaPrinter(self.0.cursor))
517 .field("selection", &HslaPrinter(self.0.selection))
518 .finish()
519 }
520}