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