main.rs

  1mod theme_printer;
  2mod util;
  3mod vscode;
  4
  5use std::fs::{self, File};
  6use std::io::Write;
  7use std::path::PathBuf;
  8use std::process::Command;
  9use std::str::FromStr;
 10
 11use anyhow::{anyhow, Context, Result};
 12use convert_case::{Case, Casing};
 13use gpui::serde_json;
 14use json_comments::StripComments;
 15use log::LevelFilter;
 16use serde::Deserialize;
 17use simplelog::SimpleLogger;
 18use theme::{Appearance, UserThemeFamily};
 19
 20use crate::theme_printer::UserThemeFamilyPrinter;
 21use crate::vscode::VsCodeTheme;
 22use crate::vscode::VsCodeThemeConverter;
 23
 24#[derive(Debug, Deserialize)]
 25struct FamilyMetadata {
 26    pub name: String,
 27    pub author: String,
 28    pub themes: Vec<ThemeMetadata>,
 29}
 30
 31#[derive(Debug, Clone, Copy, Deserialize)]
 32#[serde(rename_all = "snake_case")]
 33pub enum ThemeAppearanceJson {
 34    Light,
 35    Dark,
 36}
 37
 38impl From<ThemeAppearanceJson> for Appearance {
 39    fn from(value: ThemeAppearanceJson) -> Self {
 40        match value {
 41            ThemeAppearanceJson::Light => Self::Light,
 42            ThemeAppearanceJson::Dark => Self::Dark,
 43        }
 44    }
 45}
 46
 47#[derive(Debug, Deserialize)]
 48pub struct ThemeMetadata {
 49    pub name: String,
 50    pub file_name: String,
 51    pub appearance: ThemeAppearanceJson,
 52}
 53
 54fn main() -> Result<()> {
 55    const SOURCE_PATH: &str = "assets/themes/src/vscode";
 56    const OUT_PATH: &str = "crates/theme2/src/themes";
 57
 58    SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
 59
 60    println!("Loading themes source...");
 61    let vscode_themes_path = PathBuf::from_str(SOURCE_PATH)?;
 62    if !vscode_themes_path.exists() {
 63        return Err(anyhow!(format!(
 64            "Couldn't find {}, make sure it exists",
 65            SOURCE_PATH
 66        )));
 67    }
 68
 69    let mut theme_families = Vec::new();
 70
 71    for theme_family_dir in fs::read_dir(&vscode_themes_path)? {
 72        let theme_family_dir = theme_family_dir?;
 73
 74        if !theme_family_dir.file_type()?.is_dir() {
 75            continue;
 76        }
 77
 78        let theme_family_slug = theme_family_dir
 79            .path()
 80            .file_stem()
 81            .ok_or(anyhow!("no file stem"))
 82            .map(|stem| stem.to_string_lossy().to_string())?;
 83
 84        let family_metadata_file = File::open(theme_family_dir.path().join("family.json"))
 85            .context(format!(
 86                "no `family.json` found for '{}'",
 87                theme_family_slug
 88            ))?;
 89
 90        let license_file_path = theme_family_dir.path().join("LICENSE");
 91
 92        if !license_file_path.exists() {
 93            println!("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);
 94            continue;
 95        }
 96
 97        let family_metadata: FamilyMetadata = serde_json::from_reader(family_metadata_file)
 98            .context(format!(
 99                "failed to parse `family.json` for '{theme_family_slug}'"
100            ))?;
101
102        let mut themes = Vec::new();
103
104        for theme_metadata in family_metadata.themes {
105            let theme_file_path = theme_family_dir.path().join(&theme_metadata.file_name);
106
107            let theme_file = match File::open(&theme_file_path) {
108                Ok(file) => file,
109                Err(_) => {
110                    println!("Failed to open file at path: {:?}", theme_file_path);
111                    continue;
112                }
113            };
114
115            let theme_without_comments = StripComments::new(theme_file);
116            let vscode_theme: VsCodeTheme = serde_json::from_reader(theme_without_comments)
117                .context(format!("failed to parse theme {theme_file_path:?}"))?;
118
119            let converter = VsCodeThemeConverter::new(vscode_theme, theme_metadata);
120
121            let theme = converter.convert()?;
122
123            themes.push(theme);
124        }
125
126        let theme_family = UserThemeFamily {
127            name: family_metadata.name.into(),
128            author: family_metadata.author.into(),
129            themes,
130        };
131
132        theme_families.push(theme_family);
133    }
134
135    let themes_output_path = PathBuf::from_str(OUT_PATH)?;
136
137    if !themes_output_path.exists() {
138        println!("Creating directory: {:?}", themes_output_path);
139        fs::create_dir_all(&themes_output_path)?;
140    }
141
142    let mut mod_rs_file = File::create(themes_output_path.join(format!("mod.rs")))?;
143
144    let mut theme_modules = Vec::new();
145
146    for theme_family in theme_families {
147        let theme_family_slug = theme_family.name.to_string().to_case(Case::Snake);
148
149        let mut output_file =
150            File::create(themes_output_path.join(format!("{theme_family_slug}.rs")))?;
151        println!(
152            "Creating file: {:?}",
153            themes_output_path.join(format!("{theme_family_slug}.rs"))
154        );
155
156        let theme_module = format!(
157            r#"
158            // This file was generated by the `theme_importer`.
159            // Be careful when modifying it by hand.
160
161            use gpui::rgba;
162
163            #[allow(unused)]
164            use crate::{{
165                Appearance, StatusColorsRefinement, ThemeColorsRefinement, UserHighlightStyle, UserSyntaxTheme,
166                UserTheme, UserThemeFamily, UserThemeStylesRefinement, UserFontWeight, UserFontStyle
167            }};
168
169            pub fn {theme_family_slug}() -> UserThemeFamily {{
170                {theme_family_definition}
171            }}
172            "#,
173            theme_family_definition = format!("{:#?}", UserThemeFamilyPrinter::new(theme_family))
174        );
175
176        output_file.write_all(theme_module.as_bytes())?;
177
178        theme_modules.push(theme_family_slug);
179    }
180
181    let themes_vector_contents = format!(
182        r#"
183        use crate::UserThemeFamily;
184
185        pub(crate) fn all_user_themes() -> Vec<UserThemeFamily> {{
186            vec![{all_themes}]
187        }}
188        "#,
189        all_themes = theme_modules
190            .iter()
191            .map(|module| format!("{}()", module))
192            .collect::<Vec<_>>()
193            .join(", ")
194    );
195
196    let mod_rs_contents = format!(
197        r#"
198        // This file was generated by the `theme_importer`.
199        // Be careful when modifying it by hand.
200
201        {mod_statements}
202
203        {use_statements}
204
205        {themes_vector_contents}
206        "#,
207        mod_statements = theme_modules
208            .iter()
209            .map(|module| format!("mod {module};"))
210            .collect::<Vec<_>>()
211            .join("\n"),
212        use_statements = theme_modules
213            .iter()
214            .map(|module| format!("pub use {module}::*;"))
215            .collect::<Vec<_>>()
216            .join("\n"),
217        themes_vector_contents = themes_vector_contents
218    );
219
220    mod_rs_file.write_all(mod_rs_contents.as_bytes())?;
221
222    println!("Formatting themes...");
223
224    let format_result = format_themes_crate()
225        // We need to format a second time to catch all of the formatting issues.
226        .and_then(|_| format_themes_crate());
227
228    if let Err(err) = format_result {
229        eprintln!("Failed to format themes: {}", err);
230    }
231
232    println!("Done!");
233
234    Ok(())
235}
236
237fn format_themes_crate() -> std::io::Result<std::process::Output> {
238    Command::new("cargo")
239        .args(["fmt", "--package", "theme2"])
240        .output()
241}