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