main.rs

  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}