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