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 indexmap::IndexMap;
 20use indoc::formatdoc;
 21use json_comments::StripComments;
 22use log::LevelFilter;
 23use serde::Deserialize;
 24use simplelog::{TermLogger, TerminalMode};
 25use theme::{Appearance, UserTheme, UserThemeFamily};
 26
 27use crate::theme_printer::UserThemeFamilyPrinter;
 28use crate::vscode::VsCodeTheme;
 29use crate::vscode::VsCodeThemeConverter;
 30use crate::zed1::theme::Theme as Zed1Theme;
 31use crate::zed1::{zed1_theme_licenses, Zed1ThemeConverter};
 32
 33#[derive(Debug, Deserialize)]
 34struct FamilyMetadata {
 35    pub name: String,
 36    pub author: String,
 37    pub themes: Vec<ThemeMetadata>,
 38
 39    /// Overrides for specific syntax tokens.
 40    ///
 41    /// Use this to ensure certain Zed syntax tokens are matched
 42    /// to an exact set of scopes when it is not otherwise possible
 43    /// to rely on the default mappings in the theme importer.
 44    #[serde(default)]
 45    pub syntax: IndexMap<String, Vec<String>>,
 46}
 47
 48#[derive(Debug, Clone, Copy, Deserialize)]
 49#[serde(rename_all = "snake_case")]
 50pub enum ThemeAppearanceJson {
 51    Light,
 52    Dark,
 53}
 54
 55impl From<ThemeAppearanceJson> for Appearance {
 56    fn from(value: ThemeAppearanceJson) -> Self {
 57        match value {
 58            ThemeAppearanceJson::Light => Self::Light,
 59            ThemeAppearanceJson::Dark => Self::Dark,
 60        }
 61    }
 62}
 63
 64#[derive(Debug, Deserialize)]
 65pub struct ThemeMetadata {
 66    pub name: String,
 67    pub file_name: String,
 68    pub appearance: ThemeAppearanceJson,
 69}
 70
 71#[derive(Parser)]
 72#[command(author, version, about, long_about = None)]
 73struct Args {
 74    /// Whether to warn when values are missing from the theme.
 75    #[arg(long)]
 76    warn_on_missing: bool,
 77}
 78
 79fn main() -> Result<()> {
 80    const SOURCE_PATH: &str = "assets/themes/src/vscode";
 81    const OUT_PATH: &str = "crates/theme/src/themes";
 82
 83    let args = Args::parse();
 84
 85    let log_config = {
 86        let mut config = simplelog::ConfigBuilder::new();
 87        config
 88            .set_level_color(log::Level::Trace, simplelog::Color::Cyan)
 89            .set_level_color(log::Level::Info, simplelog::Color::Blue)
 90            .set_level_color(log::Level::Warn, simplelog::Color::Yellow)
 91            .set_level_color(log::Level::Error, simplelog::Color::Red);
 92
 93        if !args.warn_on_missing {
 94            config.add_filter_ignore_str("theme_printer");
 95        }
 96
 97        config.build()
 98    };
 99
100    TermLogger::init(LevelFilter::Trace, log_config, TerminalMode::Mixed)
101        .expect("could not initialize logger");
102
103    let mut theme_families = Vec::new();
104
105    /// Whether VS Code themes should be imported.
106    const IMPORT_VS_CODE_THEMES: bool = false;
107
108    if IMPORT_VS_CODE_THEMES {
109        log::info!("Loading themes source...");
110        let vscode_themes_path = PathBuf::from_str(SOURCE_PATH)?;
111        if !vscode_themes_path.exists() {
112            return Err(anyhow!(format!(
113                "Couldn't find {}, make sure it exists",
114                SOURCE_PATH
115            )));
116        }
117
118        for theme_family_dir in fs::read_dir(&vscode_themes_path)? {
119            let theme_family_dir = theme_family_dir?;
120
121            if !theme_family_dir.file_type()?.is_dir() {
122                continue;
123            }
124
125            let theme_family_slug = theme_family_dir
126                .path()
127                .file_stem()
128                .ok_or(anyhow!("no file stem"))
129                .map(|stem| stem.to_string_lossy().to_string())?;
130
131            let family_metadata_file = File::open(theme_family_dir.path().join("family.json"))
132                .context(format!(
133                    "no `family.json` found for '{}'",
134                    theme_family_slug
135                ))?;
136
137            let license_file_path = theme_family_dir.path().join("LICENSE");
138
139            if !license_file_path.exists() {
140                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);
141                continue;
142            }
143
144            let family_metadata: FamilyMetadata = serde_json::from_reader(family_metadata_file)
145                .context(format!(
146                    "failed to parse `family.json` for '{theme_family_slug}'"
147                ))?;
148
149            let mut themes = Vec::new();
150
151            for theme_metadata in family_metadata.themes {
152                log::info!("Converting '{}' theme", &theme_metadata.name);
153
154                let theme_file_path = theme_family_dir.path().join(&theme_metadata.file_name);
155
156                let theme_file = match File::open(&theme_file_path) {
157                    Ok(file) => file,
158                    Err(_) => {
159                        log::info!("Failed to open file at path: {:?}", theme_file_path);
160                        continue;
161                    }
162                };
163
164                let theme_without_comments = StripComments::new(theme_file);
165                let vscode_theme: VsCodeTheme = serde_json::from_reader(theme_without_comments)
166                    .context(format!("failed to parse theme {theme_file_path:?}"))?;
167
168                let converter = VsCodeThemeConverter::new(
169                    vscode_theme,
170                    theme_metadata,
171                    family_metadata.syntax.clone(),
172                );
173
174                let theme = converter.convert()?;
175
176                themes.push(theme);
177            }
178
179            let theme_family = UserThemeFamily {
180                name: family_metadata.name.into(),
181                author: family_metadata.author.into(),
182                themes,
183            };
184
185            theme_families.push(theme_family);
186        }
187    }
188
189    let zed1_themes_path = PathBuf::from_str("assets/themes")?;
190
191    let zed1_theme_families = [
192        "Andromeda",
193        "Atelier",
194        "Ayu",
195        "Gruvbox",
196        "One",
197        "Rosé Pine",
198        "Sandcastle",
199        "Solarized",
200        "Summercamp",
201    ];
202
203    let zed1_licenses_by_theme: HashMap<String, zed1::Zed1ThemeLicense> = HashMap::from_iter(
204        zed1_theme_licenses()
205            .into_iter()
206            .map(|theme_license| (theme_license.theme.clone(), theme_license)),
207    );
208
209    let mut zed1_themes_by_family: IndexMap<String, Vec<UserTheme>> = IndexMap::from_iter(
210        zed1_theme_families
211            .into_iter()
212            .map(|family| (family.to_string(), Vec::new())),
213    );
214
215    for entry in fs::read_dir(&zed1_themes_path)? {
216        let entry = entry?;
217
218        if entry.file_type()?.is_dir() {
219            continue;
220        }
221
222        match entry.path().extension() {
223            None => continue,
224            Some(extension) => {
225                if extension != "json" {
226                    continue;
227                }
228            }
229        }
230
231        let theme_file_path = entry.path();
232
233        let theme_file = match File::open(&theme_file_path) {
234            Ok(file) => file,
235            Err(_) => {
236                log::info!("Failed to open file at path: {:?}", theme_file_path);
237                continue;
238            }
239        };
240
241        let theme_without_comments = StripComments::new(theme_file);
242
243        let zed1_theme: Zed1Theme = serde_json::from_reader(theme_without_comments)
244            .context(format!("failed to parse theme {theme_file_path:?}"))?;
245
246        let theme_name = zed1_theme.meta.name.clone();
247
248        let converter = Zed1ThemeConverter::new(zed1_theme);
249
250        let theme = converter.convert()?;
251
252        let Some((_, themes_for_family)) = zed1_themes_by_family
253            .iter_mut()
254            .find(|(family, _)| theme_name.starts_with(*family))
255        else {
256            log::warn!("No theme family found for '{}'.", theme_name);
257            continue;
258        };
259
260        themes_for_family.push(theme);
261    }
262
263    zed1_themes_by_family.sort_keys();
264
265    let mut licenses = Vec::new();
266
267    for (family, themes) in zed1_themes_by_family {
268        let mut theme_family = UserThemeFamily {
269            name: family,
270            author: "Zed Industries".to_string(),
271            themes,
272        };
273
274        theme_family
275            .themes
276            .sort_unstable_by_key(|theme| theme.name.clone());
277
278        for theme in &theme_family.themes {
279            let license = zed1_licenses_by_theme
280                .get(&theme.name)
281                .ok_or_else(|| anyhow!("missing license for theme: '{}'", theme.name))?;
282
283            let license_header = match license.license_url.as_ref() {
284                Some(license_url) => {
285                    format!("[{theme_name}]({license_url})", theme_name = theme.name)
286                }
287                None => theme.name.clone(),
288            };
289
290            licenses.push(formatdoc!(
291                "
292                ## {license_header}
293
294                {license_text}
295                ********************************************************************************
296                ",
297                license_text = license.license_text
298            ));
299        }
300
301        theme_families.push(theme_family);
302    }
303
304    let themes_output_path = PathBuf::from_str(OUT_PATH)?;
305
306    if !themes_output_path.exists() {
307        log::info!("Creating directory: {:?}", themes_output_path);
308        fs::create_dir_all(&themes_output_path)?;
309    }
310
311    let mut mod_rs_file = File::create(themes_output_path.join(format!("mod.rs")))?;
312
313    let mut theme_modules = Vec::new();
314
315    for theme_family in theme_families {
316        let theme_family_slug = any_ascii(&theme_family.name)
317            .replace("(", "")
318            .replace(")", "")
319            .to_case(Case::Snake);
320
321        let mut output_file =
322            File::create(themes_output_path.join(format!("{theme_family_slug}.rs")))?;
323        log::info!(
324            "Creating file: {:?}",
325            themes_output_path.join(format!("{theme_family_slug}.rs"))
326        );
327
328        let theme_module = format!(
329            r#"
330            // This file was generated by the `theme_importer`.
331            // Be careful when modifying it by hand.
332
333            use gpui::rgba;
334
335            #[allow(unused)]
336            use crate::{{
337                Appearance, PlayerColor, PlayerColors, StatusColorsRefinement, ThemeColorsRefinement,
338                UserHighlightStyle, UserSyntaxTheme, UserTheme, UserThemeFamily, UserThemeStylesRefinement,
339                UserFontWeight, UserFontStyle
340            }};
341
342            pub fn {theme_family_slug}() -> UserThemeFamily {{
343                {theme_family_definition}
344            }}
345            "#,
346            theme_family_definition = format!("{:#?}", UserThemeFamilyPrinter::new(theme_family))
347        );
348
349        output_file.write_all(theme_module.as_bytes())?;
350
351        theme_modules.push(theme_family_slug);
352    }
353
354    theme_modules.sort();
355
356    let themes_vector_contents = format!(
357        r#"
358        use crate::UserThemeFamily;
359
360        pub(crate) fn all_user_themes() -> Vec<UserThemeFamily> {{
361            vec![{all_themes}]
362        }}
363        "#,
364        all_themes = theme_modules
365            .iter()
366            .map(|module| format!("{}()", module))
367            .collect::<Vec<_>>()
368            .join(", ")
369    );
370
371    let mod_rs_contents = format!(
372        r#"
373        // This file was generated by the `theme_importer`.
374        // Be careful when modifying it by hand.
375
376        {mod_statements}
377
378        {use_statements}
379
380        {themes_vector_contents}
381        "#,
382        mod_statements = theme_modules
383            .iter()
384            .map(|module| format!("mod {module};"))
385            .collect::<Vec<_>>()
386            .join("\n"),
387        use_statements = theme_modules
388            .iter()
389            .map(|module| format!("pub use {module}::*;"))
390            .collect::<Vec<_>>()
391            .join("\n"),
392        themes_vector_contents = themes_vector_contents
393    );
394
395    mod_rs_file.write_all(mod_rs_contents.as_bytes())?;
396
397    log::info!("Writing LICENSES file...");
398
399    let mut licenses_file = File::create(themes_output_path.join(format!("LICENSES")))?;
400
401    licenses_file.write_all(licenses.join("\n").as_bytes())?;
402
403    log::info!("Formatting themes...");
404
405    let format_result = format_themes_crate()
406        // We need to format a second time to catch all of the formatting issues.
407        .and_then(|_| format_themes_crate());
408
409    if let Err(err) = format_result {
410        log::error!("Failed to format themes: {}", err);
411    }
412
413    log::info!("Done!");
414
415    Ok(())
416}
417
418fn format_themes_crate() -> std::io::Result<std::process::Output> {
419    Command::new("cargo")
420        .args(["fmt", "--package", "theme"])
421        .output()
422}