main.rs

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