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