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