main.rs

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