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 (json_theme, legacy_theme) = load_theme(args.theme)?;
28
29 let theme = convert_theme(json_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
92fn convert_theme(json_theme: JsonTheme, legacy_theme: LegacyTheme) -> Result<theme2::Theme> {
93 let transparent = hsla(0.0, 0.0, 0.0, 0.0);
94
95 let players: [PlayerTheme; 8] = [
96 PlayerThemeColors::new(&legacy_theme, 0).into(),
97 PlayerThemeColors::new(&legacy_theme, 1).into(),
98 PlayerThemeColors::new(&legacy_theme, 2).into(),
99 PlayerThemeColors::new(&legacy_theme, 3).into(),
100 PlayerThemeColors::new(&legacy_theme, 4).into(),
101 PlayerThemeColors::new(&legacy_theme, 5).into(),
102 PlayerThemeColors::new(&legacy_theme, 6).into(),
103 PlayerThemeColors::new(&legacy_theme, 7).into(),
104 ];
105
106 let theme = theme2::Theme {
107 metadata: theme2::ThemeMetadata {
108 name: legacy_theme.name.clone().into(),
109 is_light: legacy_theme.is_light,
110 },
111 transparent,
112 mac_os_traffic_light_red: rgb::<Hsla>(0xEC695E),
113 mac_os_traffic_light_yellow: rgb::<Hsla>(0xF4BF4F),
114 mac_os_traffic_light_green: rgb::<Hsla>(0x62C554),
115 border: legacy_theme.lowest.base.default.border,
116 border_variant: legacy_theme.lowest.variant.default.border,
117 border_focused: legacy_theme.lowest.accent.default.border,
118 border_transparent: transparent,
119 elevated_surface: legacy_theme.lowest.base.default.background,
120 surface: legacy_theme.middle.base.default.background,
121 background: legacy_theme.lowest.base.default.background,
122 filled_element: legacy_theme.lowest.base.default.background,
123 filled_element_hover: hsla(0.0, 0.0, 100.0, 0.12),
124 filled_element_active: hsla(0.0, 0.0, 100.0, 0.16),
125 filled_element_selected: legacy_theme.lowest.accent.default.background,
126 filled_element_disabled: transparent,
127 ghost_element: transparent,
128 ghost_element_hover: hsla(0.0, 0.0, 100.0, 0.08),
129 ghost_element_active: hsla(0.0, 0.0, 100.0, 0.12),
130 ghost_element_selected: legacy_theme.lowest.accent.default.background,
131 ghost_element_disabled: transparent,
132 text: legacy_theme.lowest.base.default.foreground,
133 text_muted: legacy_theme.lowest.variant.default.foreground,
134 /// TODO: map this to a real value
135 text_placeholder: legacy_theme.lowest.negative.default.foreground,
136 text_disabled: legacy_theme.lowest.base.disabled.foreground,
137 text_accent: legacy_theme.lowest.accent.default.foreground,
138 icon_muted: legacy_theme.lowest.variant.default.foreground,
139 syntax: SyntaxTheme {
140 highlights: json_theme
141 .editor
142 .syntax
143 .iter()
144 .map(|(token, style)| (token.clone(), style.color.clone().into()))
145 .collect(),
146 },
147 status_bar: legacy_theme.lowest.base.default.background,
148 title_bar: legacy_theme.lowest.base.default.background,
149 toolbar: legacy_theme.highest.base.default.background,
150 tab_bar: legacy_theme.middle.base.default.background,
151 editor: legacy_theme.highest.base.default.background,
152 editor_subheader: legacy_theme.middle.base.default.background,
153 terminal: legacy_theme.highest.base.default.background,
154 editor_active_line: legacy_theme.highest.on.default.background,
155 image_fallback_background: legacy_theme.lowest.base.default.background,
156
157 git_created: legacy_theme.lowest.positive.default.foreground,
158 git_modified: legacy_theme.lowest.accent.default.foreground,
159 git_deleted: legacy_theme.lowest.negative.default.foreground,
160 git_conflict: legacy_theme.lowest.warning.default.foreground,
161 git_ignored: legacy_theme.lowest.base.disabled.foreground,
162 git_renamed: legacy_theme.lowest.warning.default.foreground,
163
164 players,
165 };
166
167 Ok(theme)
168}
169
170#[derive(Deserialize)]
171struct JsonTheme {
172 pub editor: JsonEditorTheme,
173 pub base_theme: serde_json::Value,
174}
175
176#[derive(Deserialize)]
177struct JsonEditorTheme {
178 pub syntax: HashMap<String, JsonSyntaxStyle>,
179}
180
181#[derive(Deserialize)]
182struct JsonSyntaxStyle {
183 pub color: Hsla,
184}
185
186/// Loads the [`Theme`] with the given name.
187fn load_theme(name: String) -> Result<(JsonTheme, LegacyTheme)> {
188 let theme_contents = Assets::get(&format!("themes/{name}.json"))
189 .with_context(|| format!("theme file not found: '{name}'"))?;
190
191 let json_theme: JsonTheme = serde_json::from_str(std::str::from_utf8(&theme_contents.data)?)
192 .context("failed to parse legacy theme")?;
193
194 let legacy_theme: LegacyTheme = serde_json::from_value(json_theme.base_theme.clone())
195 .context("failed to parse `base_theme`")?;
196
197 Ok((json_theme, legacy_theme))
198}
199
200#[derive(Deserialize, Clone, Default, Debug)]
201pub struct LegacyTheme {
202 pub name: String,
203 pub is_light: bool,
204 pub lowest: Layer,
205 pub middle: Layer,
206 pub highest: Layer,
207 pub popover_shadow: Shadow,
208 pub modal_shadow: Shadow,
209 #[serde(deserialize_with = "deserialize_player_colors")]
210 pub players: Vec<PlayerColors>,
211 #[serde(deserialize_with = "deserialize_syntax_colors")]
212 pub syntax: HashMap<String, Hsla>,
213}
214
215#[derive(Deserialize, Clone, Default, Debug)]
216pub struct Layer {
217 pub base: StyleSet,
218 pub variant: StyleSet,
219 pub on: StyleSet,
220 pub accent: StyleSet,
221 pub positive: StyleSet,
222 pub warning: StyleSet,
223 pub negative: StyleSet,
224}
225
226#[derive(Deserialize, Clone, Default, Debug)]
227pub struct StyleSet {
228 #[serde(rename = "default")]
229 pub default: ContainerColors,
230 pub hovered: ContainerColors,
231 pub pressed: ContainerColors,
232 pub active: ContainerColors,
233 pub disabled: ContainerColors,
234 pub inverted: ContainerColors,
235}
236
237#[derive(Deserialize, Clone, Default, Debug)]
238pub struct ContainerColors {
239 pub background: Hsla,
240 pub foreground: Hsla,
241 pub border: Hsla,
242}
243
244#[derive(Deserialize, Clone, Default, Debug)]
245pub struct PlayerColors {
246 pub selection: Hsla,
247 pub cursor: Hsla,
248}
249
250#[derive(Deserialize, Clone, Default, Debug)]
251pub struct Shadow {
252 pub blur: u8,
253 pub color: Hsla,
254 pub offset: Vec<u8>,
255}
256
257fn deserialize_player_colors<'de, D>(deserializer: D) -> Result<Vec<PlayerColors>, D::Error>
258where
259 D: Deserializer<'de>,
260{
261 struct PlayerArrayVisitor;
262
263 impl<'de> Visitor<'de> for PlayerArrayVisitor {
264 type Value = Vec<PlayerColors>;
265
266 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
267 formatter.write_str("an object with integer keys")
268 }
269
270 fn visit_map<A: serde::de::MapAccess<'de>>(
271 self,
272 mut map: A,
273 ) -> Result<Self::Value, A::Error> {
274 let mut players = Vec::with_capacity(8);
275 while let Some((key, value)) = map.next_entry::<usize, PlayerColors>()? {
276 if key < 8 {
277 players.push(value);
278 } else {
279 return Err(serde::de::Error::invalid_value(
280 serde::de::Unexpected::Unsigned(key as u64),
281 &"a key in range 0..7",
282 ));
283 }
284 }
285 Ok(players)
286 }
287 }
288
289 deserializer.deserialize_map(PlayerArrayVisitor)
290}
291
292fn deserialize_syntax_colors<'de, D>(deserializer: D) -> Result<HashMap<String, Hsla>, D::Error>
293where
294 D: serde::Deserializer<'de>,
295{
296 #[derive(Deserialize)]
297 struct ColorWrapper {
298 color: Hsla,
299 }
300
301 struct SyntaxVisitor;
302
303 impl<'de> Visitor<'de> for SyntaxVisitor {
304 type Value = HashMap<String, Hsla>;
305
306 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
307 formatter.write_str("a map with keys and objects with a single color field as values")
308 }
309
310 fn visit_map<M>(self, mut map: M) -> Result<HashMap<String, Hsla>, M::Error>
311 where
312 M: serde::de::MapAccess<'de>,
313 {
314 let mut result = HashMap::new();
315 while let Some(key) = map.next_key()? {
316 let wrapper: ColorWrapper = map.next_value()?; // Deserialize values as Hsla
317 result.insert(key, wrapper.color);
318 }
319 Ok(result)
320 }
321 }
322 deserializer.deserialize_map(SyntaxVisitor)
323}
324
325pub struct ThemePrinter(theme2::Theme);
326
327struct HslaPrinter(Hsla);
328
329impl Debug for HslaPrinter {
330 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
331 write!(f, "{:?}", IntoPrinter(&Rgba::from(self.0)))
332 }
333}
334
335struct IntoPrinter<'a, D: Debug>(&'a D);
336
337impl<'a, D: Debug> Debug for IntoPrinter<'a, D> {
338 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
339 write!(f, "{:?}.into()", self.0)
340 }
341}
342
343impl Debug for ThemePrinter {
344 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
345 f.debug_struct("Theme")
346 .field("metadata", &ThemeMetadataPrinter(self.0.metadata.clone()))
347 .field("transparent", &HslaPrinter(self.0.transparent))
348 .field(
349 "mac_os_traffic_light_red",
350 &HslaPrinter(self.0.mac_os_traffic_light_red),
351 )
352 .field(
353 "mac_os_traffic_light_yellow",
354 &HslaPrinter(self.0.mac_os_traffic_light_yellow),
355 )
356 .field(
357 "mac_os_traffic_light_green",
358 &HslaPrinter(self.0.mac_os_traffic_light_green),
359 )
360 .field("border", &HslaPrinter(self.0.border))
361 .field("border_variant", &HslaPrinter(self.0.border_variant))
362 .field("border_focused", &HslaPrinter(self.0.border_focused))
363 .field(
364 "border_transparent",
365 &HslaPrinter(self.0.border_transparent),
366 )
367 .field("elevated_surface", &HslaPrinter(self.0.elevated_surface))
368 .field("surface", &HslaPrinter(self.0.surface))
369 .field("background", &HslaPrinter(self.0.background))
370 .field("filled_element", &HslaPrinter(self.0.filled_element))
371 .field(
372 "filled_element_hover",
373 &HslaPrinter(self.0.filled_element_hover),
374 )
375 .field(
376 "filled_element_active",
377 &HslaPrinter(self.0.filled_element_active),
378 )
379 .field(
380 "filled_element_selected",
381 &HslaPrinter(self.0.filled_element_selected),
382 )
383 .field(
384 "filled_element_disabled",
385 &HslaPrinter(self.0.filled_element_disabled),
386 )
387 .field("ghost_element", &HslaPrinter(self.0.ghost_element))
388 .field(
389 "ghost_element_hover",
390 &HslaPrinter(self.0.ghost_element_hover),
391 )
392 .field(
393 "ghost_element_active",
394 &HslaPrinter(self.0.ghost_element_active),
395 )
396 .field(
397 "ghost_element_selected",
398 &HslaPrinter(self.0.ghost_element_selected),
399 )
400 .field(
401 "ghost_element_disabled",
402 &HslaPrinter(self.0.ghost_element_disabled),
403 )
404 .field("text", &HslaPrinter(self.0.text))
405 .field("text_muted", &HslaPrinter(self.0.text_muted))
406 .field("text_placeholder", &HslaPrinter(self.0.text_placeholder))
407 .field("text_disabled", &HslaPrinter(self.0.text_disabled))
408 .field("text_accent", &HslaPrinter(self.0.text_accent))
409 .field("icon_muted", &HslaPrinter(self.0.icon_muted))
410 .field("syntax", &SyntaxThemePrinter(self.0.syntax.clone()))
411 .field("status_bar", &HslaPrinter(self.0.status_bar))
412 .field("title_bar", &HslaPrinter(self.0.title_bar))
413 .field("toolbar", &HslaPrinter(self.0.toolbar))
414 .field("tab_bar", &HslaPrinter(self.0.tab_bar))
415 .field("editor", &HslaPrinter(self.0.editor))
416 .field("editor_subheader", &HslaPrinter(self.0.editor_subheader))
417 .field(
418 "editor_active_line",
419 &HslaPrinter(self.0.editor_active_line),
420 )
421 .field("terminal", &HslaPrinter(self.0.terminal))
422 .field(
423 "image_fallback_background",
424 &HslaPrinter(self.0.image_fallback_background),
425 )
426 .field("git_created", &HslaPrinter(self.0.git_created))
427 .field("git_modified", &HslaPrinter(self.0.git_modified))
428 .field("git_deleted", &HslaPrinter(self.0.git_deleted))
429 .field("git_conflict", &HslaPrinter(self.0.git_conflict))
430 .field("git_ignored", &HslaPrinter(self.0.git_ignored))
431 .field("git_renamed", &HslaPrinter(self.0.git_renamed))
432 .field("players", &self.0.players.map(PlayerThemePrinter))
433 .finish()
434 }
435}
436
437pub struct ThemeMetadataPrinter(ThemeMetadata);
438
439impl Debug for ThemeMetadataPrinter {
440 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
441 f.debug_struct("ThemeMetadata")
442 .field("name", &IntoPrinter(&self.0.name))
443 .field("is_light", &self.0.is_light)
444 .finish()
445 }
446}
447
448pub struct SyntaxThemePrinter(SyntaxTheme);
449
450impl Debug for SyntaxThemePrinter {
451 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
452 f.debug_struct("SyntaxTheme")
453 .field(
454 "highlights",
455 &VecPrinter(
456 &self
457 .0
458 .highlights
459 .iter()
460 .map(|(token, highlight)| {
461 (IntoPrinter(token), HslaPrinter(highlight.color.unwrap()))
462 })
463 .collect(),
464 ),
465 )
466 .finish()
467 }
468}
469
470pub struct VecPrinter<'a, T>(&'a Vec<T>);
471
472impl<'a, T: Debug> Debug for VecPrinter<'a, T> {
473 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
474 write!(f, "vec!{:?}", &self.0)
475 }
476}
477
478pub struct PlayerThemePrinter(PlayerTheme);
479
480impl Debug for PlayerThemePrinter {
481 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
482 f.debug_struct("PlayerTheme")
483 .field("cursor", &HslaPrinter(self.0.cursor))
484 .field("selection", &HslaPrinter(self.0.selection))
485 .finish()
486 }
487}