main.rs

  1use std::borrow::Cow;
  2use std::collections::HashMap;
  3use std::fmt;
  4
  5use anyhow::{anyhow, Context, Result};
  6use clap::Parser;
  7use gpui2::{hsla, rgb, serde_json, AssetSource, Hsla, SharedString};
  8use log::LevelFilter;
  9use rust_embed::RustEmbed;
 10use serde::de::Visitor;
 11use serde::{Deserialize, Deserializer};
 12use simplelog::SimpleLogger;
 13use theme2::{PlayerTheme, SyntaxTheme};
 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 std::fmt::Debug for SyntaxColor {
101    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102        f.debug_struct("SyntaxColor")
103            .field("comment", &self.comment.to_rgb().to_hex())
104            .field("string", &self.string.to_rgb().to_hex())
105            .field("function", &self.function.to_rgb().to_hex())
106            .field("keyword", &self.keyword.to_rgb().to_hex())
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
368impl std::fmt::Debug for ThemePrinter {
369    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
370        f.debug_struct("Theme")
371            .field("transparent", &self.0.transparent.to_rgb().to_hex())
372            .field(
373                "mac_os_traffic_light_red",
374                &self.0.mac_os_traffic_light_red.to_rgb().to_hex(),
375            )
376            .field(
377                "mac_os_traffic_light_yellow",
378                &self.0.mac_os_traffic_light_yellow.to_rgb().to_hex(),
379            )
380            .field(
381                "mac_os_traffic_light_green",
382                &self.0.mac_os_traffic_light_green.to_rgb().to_hex(),
383            )
384            .field("border", &self.0.border.to_rgb().to_hex())
385            .field("border_variant", &self.0.border_variant.to_rgb().to_hex())
386            .field("border_focused", &self.0.border_focused.to_rgb().to_hex())
387            .field(
388                "border_transparent",
389                &self.0.border_transparent.to_rgb().to_hex(),
390            )
391            .field(
392                "elevated_surface",
393                &self.0.elevated_surface.to_rgb().to_hex(),
394            )
395            .field("surface", &self.0.surface.to_rgb().to_hex())
396            .field("background", &self.0.background.to_rgb().to_hex())
397            .field("filled_element", &self.0.filled_element.to_rgb().to_hex())
398            .field(
399                "filled_element_hover",
400                &self.0.filled_element_hover.to_rgb().to_hex(),
401            )
402            .field(
403                "filled_element_active",
404                &self.0.filled_element_active.to_rgb().to_hex(),
405            )
406            .field(
407                "filled_element_selected",
408                &self.0.filled_element_selected.to_rgb().to_hex(),
409            )
410            .field(
411                "filled_element_disabled",
412                &self.0.filled_element_disabled.to_rgb().to_hex(),
413            )
414            .field("ghost_element", &self.0.ghost_element.to_rgb().to_hex())
415            .field(
416                "ghost_element_hover",
417                &self.0.ghost_element_hover.to_rgb().to_hex(),
418            )
419            .field(
420                "ghost_element_active",
421                &self.0.ghost_element_active.to_rgb().to_hex(),
422            )
423            .field(
424                "ghost_element_selected",
425                &self.0.ghost_element_selected.to_rgb().to_hex(),
426            )
427            .field(
428                "ghost_element_disabled",
429                &self.0.ghost_element_disabled.to_rgb().to_hex(),
430            )
431            .field("text", &self.0.text.to_rgb().to_hex())
432            .field("text_muted", &self.0.text_muted.to_rgb().to_hex())
433            .field(
434                "text_placeholder",
435                &self.0.text_placeholder.to_rgb().to_hex(),
436            )
437            .field("text_disabled", &self.0.text_disabled.to_rgb().to_hex())
438            .field("text_accent", &self.0.text_accent.to_rgb().to_hex())
439            .field("icon_muted", &self.0.icon_muted.to_rgb().to_hex())
440            .field("syntax", &SyntaxThemePrinter(self.0.syntax.clone()))
441            .field("status_bar", &self.0.status_bar.to_rgb().to_hex())
442            .field("title_bar", &self.0.title_bar.to_rgb().to_hex())
443            .field("toolbar", &self.0.toolbar.to_rgb().to_hex())
444            .field("tab_bar", &self.0.tab_bar.to_rgb().to_hex())
445            .field("editor", &self.0.editor.to_rgb().to_hex())
446            .field(
447                "editor_subheader",
448                &self.0.editor_subheader.to_rgb().to_hex(),
449            )
450            .field(
451                "editor_active_line",
452                &self.0.editor_active_line.to_rgb().to_hex(),
453            )
454            .field("terminal", &self.0.terminal.to_rgb().to_hex())
455            .field(
456                "image_fallback_background",
457                &self.0.image_fallback_background.to_rgb().to_hex(),
458            )
459            .field("git_created", &self.0.git_created.to_rgb().to_hex())
460            .field("git_modified", &self.0.git_modified.to_rgb().to_hex())
461            .field("git_deleted", &self.0.git_deleted.to_rgb().to_hex())
462            .field("git_conflict", &self.0.git_conflict.to_rgb().to_hex())
463            .field("git_ignored", &self.0.git_ignored.to_rgb().to_hex())
464            .field("git_renamed", &self.0.git_renamed.to_rgb().to_hex())
465            .field("player", &self.0.players.map(PlayerThemePrinter))
466            .finish()
467    }
468}
469
470pub struct SyntaxThemePrinter(SyntaxTheme);
471
472impl std::fmt::Debug for SyntaxThemePrinter {
473    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
474        f.debug_struct("SyntaxTheme")
475            .field("comment", &self.0.comment.to_rgb().to_hex())
476            .field("string", &self.0.string.to_rgb().to_hex())
477            .field("function", &self.0.function.to_rgb().to_hex())
478            .field("keyword", &self.0.keyword.to_rgb().to_hex())
479            .finish()
480    }
481}
482
483pub struct PlayerThemePrinter(PlayerTheme);
484
485impl std::fmt::Debug for PlayerThemePrinter {
486    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
487        f.debug_struct("PlayerTheme")
488            .field("cursor", &self.0.cursor.to_rgb().to_hex())
489            .field("selection", &self.0.selection.to_rgb().to_hex())
490            .finish()
491    }
492}