main.rs

  1mod assets;
  2mod color;
  3mod theme_printer;
  4mod util;
  5mod vscode;
  6mod zed1;
  7
  8use std::collections::HashMap;
  9use std::fs::{self, File};
 10use std::io::Write;
 11use std::path::PathBuf;
 12use std::process::Command;
 13use std::str::FromStr;
 14
 15use any_ascii::any_ascii;
 16use anyhow::{anyhow, Context, Result};
 17use clap::Parser;
 18use convert_case::{Case, Casing};
 19use gpui::Hsla;
 20use indexmap::IndexMap;
 21use indoc::formatdoc;
 22use json_comments::StripComments;
 23use log::LevelFilter;
 24use serde::Deserialize;
 25use simplelog::{TermLogger, TerminalMode};
 26use theme::{
 27    Appearance, FontWeightContent, HighlightStyleContent, PlayerColorContent, StatusColorsContent,
 28    ThemeColorsContent, ThemeContent, ThemeFamilyContent, ThemeStyleContent, UserTheme,
 29    UserThemeFamily,
 30};
 31
 32use crate::theme_printer::UserThemeFamilyPrinter;
 33use crate::vscode::VsCodeTheme;
 34use crate::vscode::VsCodeThemeConverter;
 35use crate::zed1::theme::Theme as Zed1Theme;
 36use crate::zed1::{zed1_theme_licenses, Zed1ThemeConverter};
 37
 38#[derive(Debug, Deserialize)]
 39struct FamilyMetadata {
 40    pub name: String,
 41    pub author: String,
 42    pub themes: Vec<ThemeMetadata>,
 43
 44    /// Overrides for specific syntax tokens.
 45    ///
 46    /// Use this to ensure certain Zed syntax tokens are matched
 47    /// to an exact set of scopes when it is not otherwise possible
 48    /// to rely on the default mappings in the theme importer.
 49    #[serde(default)]
 50    pub syntax: IndexMap<String, Vec<String>>,
 51}
 52
 53#[derive(Debug, Clone, Copy, Deserialize)]
 54#[serde(rename_all = "snake_case")]
 55pub enum ThemeAppearanceJson {
 56    Light,
 57    Dark,
 58}
 59
 60impl From<ThemeAppearanceJson> for Appearance {
 61    fn from(value: ThemeAppearanceJson) -> Self {
 62        match value {
 63            ThemeAppearanceJson::Light => Self::Light,
 64            ThemeAppearanceJson::Dark => Self::Dark,
 65        }
 66    }
 67}
 68
 69#[derive(Debug, Deserialize)]
 70pub struct ThemeMetadata {
 71    pub name: String,
 72    pub file_name: String,
 73    pub appearance: ThemeAppearanceJson,
 74}
 75
 76#[derive(Parser)]
 77#[command(author, version, about, long_about = None)]
 78struct Args {
 79    /// Whether to warn when values are missing from the theme.
 80    #[arg(long)]
 81    warn_on_missing: bool,
 82}
 83
 84fn main() -> Result<()> {
 85    const SOURCE_PATH: &str = "assets/themes/src/vscode";
 86    const OUT_PATH: &str = "crates/theme/src/themes";
 87
 88    let args = Args::parse();
 89
 90    let log_config = {
 91        let mut config = simplelog::ConfigBuilder::new();
 92        config
 93            .set_level_color(log::Level::Trace, simplelog::Color::Cyan)
 94            .set_level_color(log::Level::Info, simplelog::Color::Blue)
 95            .set_level_color(log::Level::Warn, simplelog::Color::Yellow)
 96            .set_level_color(log::Level::Error, simplelog::Color::Red);
 97
 98        if !args.warn_on_missing {
 99            config.add_filter_ignore_str("theme_printer");
100        }
101
102        config.build()
103    };
104
105    TermLogger::init(LevelFilter::Trace, log_config, TerminalMode::Mixed)
106        .expect("could not initialize logger");
107
108    if 1 < 2 {
109        let themes: Vec<UserThemeFamily> = Vec::new();
110        // Uncomment this line when you need to regenerate themes.
111        // let themes = theme::all_user_themes();
112
113        let mut families = Vec::new();
114
115        for family in themes {
116            families.push(convert_family(family));
117        }
118
119        for family in families {
120            let theme_family_slug = any_ascii(&family.name)
121                .replace("(", "")
122                .replace(")", "")
123                .to_case(Case::Snake);
124
125            let output_dir = PathBuf::from("assets/themes/").join(&theme_family_slug);
126
127            fs::create_dir_all(&output_dir)?;
128
129            let mut output_file =
130                File::create(output_dir.join(format!("{theme_family_slug}.json")))?;
131
132            let theme_json = serde_json::to_string_pretty(&family).unwrap();
133
134            output_file.write_all(format!("{theme_json}\n").as_bytes())?;
135        }
136
137        return Ok(());
138    }
139
140    let mut theme_families = Vec::new();
141
142    /// Whether VS Code themes should be imported.
143    const IMPORT_VS_CODE_THEMES: bool = false;
144
145    if IMPORT_VS_CODE_THEMES {
146        log::info!("Loading themes source...");
147        let vscode_themes_path = PathBuf::from_str(SOURCE_PATH)?;
148        if !vscode_themes_path.exists() {
149            return Err(anyhow!(format!(
150                "Couldn't find {}, make sure it exists",
151                SOURCE_PATH
152            )));
153        }
154
155        for theme_family_dir in fs::read_dir(&vscode_themes_path)? {
156            let theme_family_dir = theme_family_dir?;
157
158            if !theme_family_dir.file_type()?.is_dir() {
159                continue;
160            }
161
162            let theme_family_slug = theme_family_dir
163                .path()
164                .file_stem()
165                .ok_or(anyhow!("no file stem"))
166                .map(|stem| stem.to_string_lossy().to_string())?;
167
168            let family_metadata_file = File::open(theme_family_dir.path().join("family.json"))
169                .context(format!(
170                    "no `family.json` found for '{}'",
171                    theme_family_slug
172                ))?;
173
174            let license_file_path = theme_family_dir.path().join("LICENSE");
175
176            if !license_file_path.exists() {
177                log::info!("Skipping theme family '{}' because it does not have a LICENSE file. This theme will only be imported once a LICENSE file is provided.", theme_family_slug);
178                continue;
179            }
180
181            let family_metadata: FamilyMetadata = serde_json::from_reader(family_metadata_file)
182                .context(format!(
183                    "failed to parse `family.json` for '{theme_family_slug}'"
184                ))?;
185
186            let mut themes = Vec::new();
187
188            for theme_metadata in family_metadata.themes {
189                log::info!("Converting '{}' theme", &theme_metadata.name);
190
191                let theme_file_path = theme_family_dir.path().join(&theme_metadata.file_name);
192
193                let theme_file = match File::open(&theme_file_path) {
194                    Ok(file) => file,
195                    Err(_) => {
196                        log::info!("Failed to open file at path: {:?}", theme_file_path);
197                        continue;
198                    }
199                };
200
201                let theme_without_comments = StripComments::new(theme_file);
202                let vscode_theme: VsCodeTheme = serde_json::from_reader(theme_without_comments)
203                    .context(format!("failed to parse theme {theme_file_path:?}"))?;
204
205                let converter = VsCodeThemeConverter::new(
206                    vscode_theme,
207                    theme_metadata,
208                    family_metadata.syntax.clone(),
209                );
210
211                let theme = converter.convert()?;
212
213                themes.push(theme);
214            }
215
216            let theme_family = UserThemeFamily {
217                name: family_metadata.name.into(),
218                author: family_metadata.author.into(),
219                themes,
220            };
221
222            theme_families.push(theme_family);
223        }
224    }
225
226    let zed1_themes_path = PathBuf::from_str("assets/themes")?;
227
228    let zed1_theme_families = [
229        "Andromeda",
230        "Atelier",
231        "Ayu",
232        "Gruvbox",
233        "One",
234        "Rosé Pine",
235        "Sandcastle",
236        "Solarized",
237        "Summercamp",
238    ];
239
240    let zed1_licenses_by_theme: HashMap<String, zed1::Zed1ThemeLicense> = HashMap::from_iter(
241        zed1_theme_licenses()
242            .into_iter()
243            .map(|theme_license| (theme_license.theme.clone(), theme_license)),
244    );
245
246    let mut zed1_themes_by_family: IndexMap<String, Vec<UserTheme>> = IndexMap::from_iter(
247        zed1_theme_families
248            .into_iter()
249            .map(|family| (family.to_string(), Vec::new())),
250    );
251
252    for entry in fs::read_dir(&zed1_themes_path)? {
253        let entry = entry?;
254
255        if entry.file_type()?.is_dir() {
256            continue;
257        }
258
259        match entry.path().extension() {
260            None => continue,
261            Some(extension) => {
262                if extension != "json" {
263                    continue;
264                }
265            }
266        }
267
268        let theme_file_path = entry.path();
269
270        let theme_file = match File::open(&theme_file_path) {
271            Ok(file) => file,
272            Err(_) => {
273                log::info!("Failed to open file at path: {:?}", theme_file_path);
274                continue;
275            }
276        };
277
278        let theme_without_comments = StripComments::new(theme_file);
279
280        let zed1_theme: Zed1Theme = serde_json::from_reader(theme_without_comments)
281            .context(format!("failed to parse theme {theme_file_path:?}"))?;
282
283        let theme_name = zed1_theme.meta.name.clone();
284
285        let converter = Zed1ThemeConverter::new(zed1_theme);
286
287        let theme = converter.convert()?;
288
289        let Some((_, themes_for_family)) = zed1_themes_by_family
290            .iter_mut()
291            .find(|(family, _)| theme_name.starts_with(*family))
292        else {
293            log::warn!("No theme family found for '{}'.", theme_name);
294            continue;
295        };
296
297        themes_for_family.push(theme);
298    }
299
300    zed1_themes_by_family.sort_keys();
301
302    let mut licenses = Vec::new();
303
304    for (family, themes) in zed1_themes_by_family {
305        let mut theme_family = UserThemeFamily {
306            name: family,
307            author: "Zed Industries".to_string(),
308            themes,
309        };
310
311        theme_family
312            .themes
313            .sort_unstable_by_key(|theme| theme.name.clone());
314
315        for theme in &theme_family.themes {
316            let license = zed1_licenses_by_theme
317                .get(&theme.name)
318                .ok_or_else(|| anyhow!("missing license for theme: '{}'", theme.name))?;
319
320            let license_header = match license.license_url.as_ref() {
321                Some(license_url) => {
322                    format!("[{theme_name}]({license_url})", theme_name = theme.name)
323                }
324                None => theme.name.clone(),
325            };
326
327            licenses.push(formatdoc!(
328                "
329                ## {license_header}
330
331                {license_text}
332                ********************************************************************************
333                ",
334                license_text = license.license_text
335            ));
336        }
337
338        theme_families.push(theme_family);
339    }
340
341    let themes_output_path = PathBuf::from_str(OUT_PATH)?;
342
343    if !themes_output_path.exists() {
344        log::info!("Creating directory: {:?}", themes_output_path);
345        fs::create_dir_all(&themes_output_path)?;
346    }
347
348    let mut mod_rs_file = File::create(themes_output_path.join(format!("mod.rs")))?;
349
350    let mut theme_modules = Vec::new();
351
352    for theme_family in theme_families {
353        let theme_family_slug = any_ascii(&theme_family.name)
354            .replace("(", "")
355            .replace(")", "")
356            .to_case(Case::Snake);
357
358        let mut output_file =
359            File::create(themes_output_path.join(format!("{theme_family_slug}.rs")))?;
360        log::info!(
361            "Creating file: {:?}",
362            themes_output_path.join(format!("{theme_family_slug}.rs"))
363        );
364
365        let theme_module = format!(
366            r#"
367            // This file was generated by the `theme_importer`.
368            // Be careful when modifying it by hand.
369
370            use gpui::rgba;
371
372            #[allow(unused)]
373            use crate::{{
374                Appearance, PlayerColor, PlayerColors, StatusColorsRefinement, ThemeColorsRefinement,
375                UserHighlightStyle, UserSyntaxTheme, UserTheme, UserThemeFamily, UserThemeStylesRefinement,
376                UserFontWeight, UserFontStyle
377            }};
378
379            pub fn {theme_family_slug}() -> UserThemeFamily {{
380                {theme_family_definition}
381            }}
382            "#,
383            theme_family_definition = format!("{:#?}", UserThemeFamilyPrinter::new(theme_family))
384        );
385
386        output_file.write_all(theme_module.as_bytes())?;
387
388        theme_modules.push(theme_family_slug);
389    }
390
391    theme_modules.sort();
392
393    let themes_vector_contents = format!(
394        r#"
395        use crate::UserThemeFamily;
396
397        pub(crate) fn all_user_themes() -> Vec<UserThemeFamily> {{
398            vec![{all_themes}]
399        }}
400        "#,
401        all_themes = theme_modules
402            .iter()
403            .map(|module| format!("{}()", module))
404            .collect::<Vec<_>>()
405            .join(", ")
406    );
407
408    let mod_rs_contents = format!(
409        r#"
410        // This file was generated by the `theme_importer`.
411        // Be careful when modifying it by hand.
412
413        {mod_statements}
414
415        {use_statements}
416
417        {themes_vector_contents}
418        "#,
419        mod_statements = theme_modules
420            .iter()
421            .map(|module| format!("mod {module};"))
422            .collect::<Vec<_>>()
423            .join("\n"),
424        use_statements = theme_modules
425            .iter()
426            .map(|module| format!("pub use {module}::*;"))
427            .collect::<Vec<_>>()
428            .join("\n"),
429        themes_vector_contents = themes_vector_contents
430    );
431
432    mod_rs_file.write_all(mod_rs_contents.as_bytes())?;
433
434    log::info!("Writing LICENSES file...");
435
436    let mut licenses_file = File::create(themes_output_path.join(format!("LICENSES")))?;
437
438    licenses_file.write_all(licenses.join("\n").as_bytes())?;
439
440    log::info!("Formatting themes...");
441
442    let format_result = format_themes_crate()
443        // We need to format a second time to catch all of the formatting issues.
444        .and_then(|_| format_themes_crate());
445
446    if let Err(err) = format_result {
447        log::error!("Failed to format themes: {}", err);
448    }
449
450    log::info!("Done!");
451
452    Ok(())
453}
454
455fn format_themes_crate() -> std::io::Result<std::process::Output> {
456    Command::new("cargo")
457        .args(["fmt", "--package", "theme"])
458        .output()
459}
460
461fn convert_family(family: UserThemeFamily) -> ThemeFamilyContent {
462    ThemeFamilyContent {
463        name: family.name,
464        author: family.author,
465        themes: family.themes.into_iter().map(convert_theme).collect(),
466    }
467}
468
469fn convert_theme(theme: UserTheme) -> ThemeContent {
470    ThemeContent {
471        name: theme.name,
472        appearance: match theme.appearance {
473            Appearance::Light => theme::AppearanceContent::Light,
474            Appearance::Dark => theme::AppearanceContent::Dark,
475        },
476        style: convert_theme_styles(theme.styles),
477    }
478}
479
480fn serialize_color(color: Hsla) -> String {
481    let rgba = color.to_rgb();
482    format!("#{:08x}", u32::from(rgba))
483}
484
485fn convert_theme_styles(styles: theme::UserThemeStylesRefinement) -> ThemeStyleContent {
486    ThemeStyleContent {
487        colors: ThemeColorsContent {
488            border: styles.colors.border.map(serialize_color),
489            border_variant: styles.colors.border_variant.map(serialize_color),
490            border_focused: styles.colors.border_focused.map(serialize_color),
491            border_selected: styles.colors.border_selected.map(serialize_color),
492            border_transparent: styles.colors.border_transparent.map(serialize_color),
493            border_disabled: styles.colors.border_disabled.map(serialize_color),
494            elevated_surface_background: styles
495                .colors
496                .elevated_surface_background
497                .map(serialize_color),
498            surface_background: styles.colors.surface_background.map(serialize_color),
499            background: styles.colors.background.map(serialize_color),
500            element_background: styles.colors.element_background.map(serialize_color),
501            element_hover: styles.colors.element_hover.map(serialize_color),
502            element_active: styles.colors.element_active.map(serialize_color),
503            element_selected: styles.colors.element_selected.map(serialize_color),
504            element_disabled: styles.colors.element_disabled.map(serialize_color),
505            drop_target_background: styles.colors.drop_target_background.map(serialize_color),
506            ghost_element_background: styles.colors.ghost_element_background.map(serialize_color),
507            ghost_element_hover: styles.colors.ghost_element_hover.map(serialize_color),
508            ghost_element_active: styles.colors.ghost_element_active.map(serialize_color),
509            ghost_element_selected: styles.colors.ghost_element_selected.map(serialize_color),
510            ghost_element_disabled: styles.colors.ghost_element_disabled.map(serialize_color),
511            text: styles.colors.text.map(serialize_color),
512            text_muted: styles.colors.text_muted.map(serialize_color),
513            text_placeholder: styles.colors.text_placeholder.map(serialize_color),
514            text_disabled: styles.colors.text_disabled.map(serialize_color),
515            text_accent: styles.colors.text_accent.map(serialize_color),
516            icon: styles.colors.icon.map(serialize_color),
517            icon_muted: styles.colors.icon_muted.map(serialize_color),
518            icon_disabled: styles.colors.icon_disabled.map(serialize_color),
519            icon_placeholder: styles.colors.icon_placeholder.map(serialize_color),
520            icon_accent: styles.colors.icon_accent.map(serialize_color),
521            status_bar_background: styles.colors.status_bar_background.map(serialize_color),
522            title_bar_background: styles.colors.title_bar_background.map(serialize_color),
523            toolbar_background: styles.colors.toolbar_background.map(serialize_color),
524            tab_bar_background: styles.colors.tab_bar_background.map(serialize_color),
525            tab_inactive_background: styles.colors.tab_inactive_background.map(serialize_color),
526            tab_active_background: styles.colors.tab_active_background.map(serialize_color),
527            search_match_background: styles.colors.search_match_background.map(serialize_color),
528            panel_background: styles.colors.panel_background.map(serialize_color),
529            panel_focused_border: styles.colors.panel_focused_border.map(serialize_color),
530            pane_focused_border: styles.colors.pane_focused_border.map(serialize_color),
531            scrollbar_thumb_background: styles
532                .colors
533                .scrollbar_thumb_background
534                .map(serialize_color),
535            scrollbar_thumb_hover_background: styles
536                .colors
537                .scrollbar_thumb_hover_background
538                .map(serialize_color),
539            scrollbar_thumb_border: styles.colors.scrollbar_thumb_border.map(serialize_color),
540            scrollbar_track_background: styles
541                .colors
542                .scrollbar_track_background
543                .map(serialize_color),
544            scrollbar_track_border: styles.colors.scrollbar_track_border.map(serialize_color),
545            editor_foreground: styles.colors.editor_foreground.map(serialize_color),
546            editor_background: styles.colors.editor_background.map(serialize_color),
547            editor_gutter_background: styles.colors.editor_gutter_background.map(serialize_color),
548            editor_subheader_background: styles
549                .colors
550                .editor_subheader_background
551                .map(serialize_color),
552            editor_active_line_background: styles
553                .colors
554                .editor_active_line_background
555                .map(serialize_color),
556            editor_highlighted_line_background: styles
557                .colors
558                .editor_highlighted_line_background
559                .map(serialize_color),
560            editor_line_number: styles.colors.editor_line_number.map(serialize_color),
561            editor_active_line_number: styles.colors.editor_active_line_number.map(serialize_color),
562            editor_invisible: styles.colors.editor_invisible.map(serialize_color),
563            editor_wrap_guide: styles.colors.editor_wrap_guide.map(serialize_color),
564            editor_active_wrap_guide: styles.colors.editor_active_wrap_guide.map(serialize_color),
565            editor_document_highlight_read_background: styles
566                .colors
567                .editor_document_highlight_read_background
568                .map(serialize_color),
569            editor_document_highlight_write_background: styles
570                .colors
571                .editor_document_highlight_write_background
572                .map(serialize_color),
573            terminal_background: styles.colors.terminal_background.map(serialize_color),
574            terminal_foreground: styles.colors.terminal_foreground.map(serialize_color),
575            terminal_bright_foreground: styles
576                .colors
577                .terminal_bright_foreground
578                .map(serialize_color),
579            terminal_dim_foreground: styles.colors.terminal_dim_foreground.map(serialize_color),
580            terminal_ansi_black: styles.colors.terminal_ansi_black.map(serialize_color),
581            terminal_ansi_bright_black: styles
582                .colors
583                .terminal_ansi_bright_black
584                .map(serialize_color),
585            terminal_ansi_dim_black: styles.colors.terminal_ansi_dim_black.map(serialize_color),
586            terminal_ansi_red: styles.colors.terminal_ansi_red.map(serialize_color),
587            terminal_ansi_bright_red: styles.colors.terminal_ansi_bright_red.map(serialize_color),
588            terminal_ansi_dim_red: styles.colors.terminal_ansi_dim_red.map(serialize_color),
589            terminal_ansi_green: styles.colors.terminal_ansi_green.map(serialize_color),
590            terminal_ansi_bright_green: styles
591                .colors
592                .terminal_ansi_bright_green
593                .map(serialize_color),
594            terminal_ansi_dim_green: styles.colors.terminal_ansi_dim_green.map(serialize_color),
595            terminal_ansi_yellow: styles.colors.terminal_ansi_yellow.map(serialize_color),
596            terminal_ansi_bright_yellow: styles
597                .colors
598                .terminal_ansi_bright_yellow
599                .map(serialize_color),
600            terminal_ansi_dim_yellow: styles.colors.terminal_ansi_dim_yellow.map(serialize_color),
601            terminal_ansi_blue: styles.colors.terminal_ansi_blue.map(serialize_color),
602            terminal_ansi_bright_blue: styles.colors.terminal_ansi_bright_blue.map(serialize_color),
603            terminal_ansi_dim_blue: styles.colors.terminal_ansi_dim_blue.map(serialize_color),
604            terminal_ansi_magenta: styles.colors.terminal_ansi_magenta.map(serialize_color),
605            terminal_ansi_bright_magenta: styles
606                .colors
607                .terminal_ansi_bright_magenta
608                .map(serialize_color),
609            terminal_ansi_dim_magenta: styles.colors.terminal_ansi_dim_magenta.map(serialize_color),
610            terminal_ansi_cyan: styles.colors.terminal_ansi_cyan.map(serialize_color),
611            terminal_ansi_bright_cyan: styles.colors.terminal_ansi_bright_cyan.map(serialize_color),
612            terminal_ansi_dim_cyan: styles.colors.terminal_ansi_dim_cyan.map(serialize_color),
613            terminal_ansi_white: styles.colors.terminal_ansi_white.map(serialize_color),
614            terminal_ansi_bright_white: styles
615                .colors
616                .terminal_ansi_bright_white
617                .map(serialize_color),
618            terminal_ansi_dim_white: styles.colors.terminal_ansi_dim_white.map(serialize_color),
619            link_text_hover: styles.colors.link_text_hover.map(serialize_color),
620        },
621        status: StatusColorsContent {
622            conflict: styles.status.conflict.map(serialize_color),
623            conflict_background: styles.status.conflict_background.map(serialize_color),
624            conflict_border: styles.status.conflict_border.map(serialize_color),
625            created: styles.status.created.map(serialize_color),
626            created_background: styles.status.created_background.map(serialize_color),
627            created_border: styles.status.created_border.map(serialize_color),
628            deleted: styles.status.deleted.map(serialize_color),
629            deleted_background: styles.status.deleted_background.map(serialize_color),
630            deleted_border: styles.status.deleted_border.map(serialize_color),
631            error: styles.status.error.map(serialize_color),
632            error_background: styles.status.error_background.map(serialize_color),
633            error_border: styles.status.error_border.map(serialize_color),
634            hidden: styles.status.hidden.map(serialize_color),
635            hidden_background: styles.status.hidden_background.map(serialize_color),
636            hidden_border: styles.status.hidden_border.map(serialize_color),
637            hint: styles.status.hint.map(serialize_color),
638            hint_background: styles.status.hint_background.map(serialize_color),
639            hint_border: styles.status.hint_border.map(serialize_color),
640            ignored: styles.status.ignored.map(serialize_color),
641            ignored_background: styles.status.ignored_background.map(serialize_color),
642            ignored_border: styles.status.ignored_border.map(serialize_color),
643            info: styles.status.info.map(serialize_color),
644            info_background: styles.status.info_background.map(serialize_color),
645            info_border: styles.status.info_border.map(serialize_color),
646            modified: styles.status.modified.map(serialize_color),
647            modified_background: styles.status.modified_background.map(serialize_color),
648            modified_border: styles.status.modified_border.map(serialize_color),
649            predictive: styles.status.predictive.map(serialize_color),
650            predictive_background: styles.status.predictive_background.map(serialize_color),
651            predictive_border: styles.status.predictive_border.map(serialize_color),
652            renamed: styles.status.renamed.map(serialize_color),
653            renamed_background: styles.status.renamed_background.map(serialize_color),
654            renamed_border: styles.status.renamed_border.map(serialize_color),
655            success: styles.status.success.map(serialize_color),
656            success_background: styles.status.success_background.map(serialize_color),
657            success_border: styles.status.success_border.map(serialize_color),
658            unreachable: styles.status.unreachable.map(serialize_color),
659            unreachable_background: styles.status.unreachable_background.map(serialize_color),
660            unreachable_border: styles.status.unreachable_border.map(serialize_color),
661            warning: styles.status.warning.map(serialize_color),
662            warning_background: styles.status.warning_background.map(serialize_color),
663            warning_border: styles.status.warning_border.map(serialize_color),
664        },
665        players: styles
666            .player
667            .map(|players| {
668                players
669                    .0
670                    .into_iter()
671                    .map(|player_color| PlayerColorContent {
672                        cursor: Some(player_color.cursor).map(serialize_color),
673                        background: Some(player_color.background).map(serialize_color),
674                        selection: Some(player_color.selection).map(serialize_color),
675                    })
676                    .collect()
677            })
678            .unwrap_or_default(),
679        syntax: styles
680            .syntax
681            .map(|syntax| {
682                IndexMap::from_iter(syntax.highlights.into_iter().map(|(name, style)| {
683                    (
684                        name,
685                        HighlightStyleContent {
686                            color: style.color.map(serialize_color),
687                            font_style: style.font_style.map(|font_style| match font_style {
688                                theme::UserFontStyle::Normal => theme::FontStyleContent::Normal,
689                                theme::UserFontStyle::Italic => theme::FontStyleContent::Italic,
690                                theme::UserFontStyle::Oblique => theme::FontStyleContent::Oblique,
691                            }),
692                            font_weight: style.font_weight.map(|font_weight| match font_weight.0 {
693                                _ if font_weight.0 == 100.0 => FontWeightContent::Thin,
694                                _ if font_weight.0 == 200.0 => FontWeightContent::ExtraLight,
695                                _ if font_weight.0 == 300.0 => FontWeightContent::Light,
696                                _ if font_weight.0 == 400.0 => FontWeightContent::Normal,
697                                _ if font_weight.0 == 500.0 => FontWeightContent::Medium,
698                                _ if font_weight.0 == 600.0 => FontWeightContent::Semibold,
699                                _ if font_weight.0 == 700.0 => FontWeightContent::Bold,
700                                _ if font_weight.0 == 800.0 => FontWeightContent::ExtraBold,
701                                _ if font_weight.0 == 900.0 => FontWeightContent::Black,
702                                _ => unreachable!(),
703                            }),
704                        },
705                    )
706                }))
707            })
708            .unwrap_or_default(),
709    }
710}