Parse VSCode theme files

Marshall Bowers created

Change summary

assets/themes/src/vscode/ayu/LICENSE          |   0 
assets/themes/src/vscode/ayu/ayu-dark.json    |   0 
assets/themes/src/vscode/ayu/ayu-light.json   |   0 
assets/themes/src/vscode/ayu/ayu-mirage.json  |   0 
assets/themes/src/vscode/ayu/family.json      |   0 
assets/themes/src/vscode/dracula/LICENSE      |   0 
assets/themes/src/vscode/dracula/dracula.json |   0 
assets/themes/src/vscode/dracula/family.json  |   0 
crates/theme2/src/theme2.rs                   |   3 
crates/theme_importer/src/main.rs             | 112 +++++++++++++-------
crates/theme_importer/src/vscode.rs           |  72 ++++++------
11 files changed, 110 insertions(+), 77 deletions(-)

Detailed changes

crates/theme2/src/theme2.rs 🔗

@@ -49,8 +49,7 @@ pub struct ThemeFamily {
 impl ThemeFamily {}
 
 pub struct ThemeVariant {
-    #[allow(dead_code)]
-    pub(crate) id: String,
+    pub id: String,
     pub name: SharedString,
     pub appearance: Appearance,
     pub styles: ThemeStyles,

crates/theme_importer/src/main.rs 🔗

@@ -1,15 +1,21 @@
+use std::borrow::Cow;
+use std::fs::{self, File};
 use std::path::PathBuf;
 use std::str::FromStr;
-use std::{borrow::Cow, fs::File};
 
 use anyhow::{anyhow, Context, Result};
 use convert_case::Case;
-use gpui::{AssetSource, SharedString};
+use gpui::{serde_json, AssetSource, SharedString};
 use log::LevelFilter;
 use rust_embed::RustEmbed;
 use serde::Deserialize;
 use simplelog::SimpleLogger;
-use theme::{default_color_scales, ThemeColorsRefinement, ThemeFamily};
+use theme::{
+    default_color_scales, Appearance, GitStatusColors, PlayerColors, StatusColors, SyntaxTheme,
+    SystemColors, ThemeColors, ThemeColorsRefinement, ThemeFamily, ThemeStyles, ThemeVariant,
+};
+
+use crate::vscode::VsCodeTheme;
 
 mod vscode;
 
@@ -23,20 +29,21 @@ pub(crate) fn new_theme_family(name: String, author: String) -> ThemeFamily {
     }
 }
 
-#[derive(Deserialize)]
+#[derive(Debug, Deserialize)]
 struct FamilyJson {
     pub name: String,
+    pub author: String,
     pub themes: Vec<ThemeVariantJson>,
 }
 
-#[derive(Deserialize)]
+#[derive(Debug, Deserialize)]
 #[serde(rename_all = "snake_case")]
 enum ThemeAppearanceJson {
     Light,
     Dark,
 }
 
-#[derive(Deserialize)]
+#[derive(Debug, Deserialize)]
 struct ThemeVariantJson {
     pub name: String,
     pub appearance: ThemeAppearanceJson,
@@ -69,54 +76,79 @@ fn main() -> Result<()> {
 
     let themes_path = PathBuf::from_str("crates/theme2/src/themes")?;
 
-    let mut theme_modules = Vec::new();
+    let vscode_themes_path = PathBuf::from_str("assets/themes/src/vscode/")?;
 
-    for theme_path in Assets.list("themes/src/vsc/")? {
-        let (_, theme_name) = theme_path.split_once("themes/").unwrap();
+    let mut theme_families = Vec::new();
 
-        if theme_name == ".gitkeep" {
-            continue;
-        }
+    for theme_family_dir in fs::read_dir(&vscode_themes_path)? {
+        let theme_family_dir = theme_family_dir?;
 
-        let theme_contents = Assets::get(&theme_path)
-            .with_context(|| format!("theme file not found: '{theme_path}'"))?;
+        let theme_family_slug = theme_family_dir
+            .path()
+            .file_stem()
+            .ok_or(anyhow!("no file stem"))
+            .map(|stem| stem.to_string_lossy().to_string())?;
 
-        // let json_theme: JsonTheme =
-        //     serde_json::from_str(std::str::from_utf8(&theme_contents.data)?)
-        //         .context("failed to parse legacy theme")?;
+        let family_metadata_file = File::open(theme_family_dir.path().join("family.json"))
+            .context(format!("no `family.json` found for '{theme_family_slug}'"))?;
 
-        // let (json_theme, legacy_theme) = load_theme(&theme_path)?;
+        let family_metadata: FamilyJson = serde_json::from_reader(family_metadata_file).context(
+            format!("failed to parse `family.json` for '{theme_family_slug}'"),
+        )?;
 
-        // let theme = convert_theme(json_theme, legacy_theme)?;
+        let mut themes = Vec::new();
 
-        // let theme_slug = theme
-        //     .metadata
-        //     .name
-        //     .as_ref()
-        //     .replace("é", "e")
-        //     .to_case(Case::Snake);
+        for theme_entry in fs::read_dir(vscode_themes_path.join(theme_family_slug))? {
+            let theme_entry = theme_entry?;
 
-        // let mut output_file = File::create(themes_path.join(format!("{theme_slug}.rs")))?;
+            let theme_file_path = theme_entry.path();
 
-        // let theme_module = format!(
-        //     r#"
-        //         use gpui2::rgba;
+            let file_name = theme_file_path
+                .file_name()
+                .ok_or(anyhow!("no file stem"))
+                .map(|file_name| file_name.to_string_lossy())?;
 
-        //         use crate::{{PlayerTheme, SyntaxTheme, Theme, ThemeMetadata}};
+            if !file_name.ends_with(".json") {
+                continue;
+            }
 
-        //         pub fn {theme_slug}() -> Theme {{
-        //             {theme_definition}
-        //         }}
-        //     "#,
-        //     theme_definition = format!("{:#?}", ThemePrinter::new(theme))
-        // );
+            if file_name == "family.json" {
+                continue;
+            }
 
-        // output_file.write_all(theme_module.as_bytes())?;
+            let theme_file = File::open(&theme_file_path)?;
 
-        theme_modules.push(theme_slug);
-    }
+            let theme: VsCodeTheme = serde_json::from_reader(theme_file)
+                .context(format!("failed to parse theme {theme_file_path:?}"))?;
 
-    println!("Hello, world!");
+            themes.push(theme);
+        }
+
+        let theme_family = ThemeFamily {
+            id: uuid::Uuid::new_v4().to_string(),
+            name: family_metadata.name.into(),
+            author: family_metadata.author.into(),
+            themes: themes
+                .into_iter()
+                .map(|theme| ThemeVariant {
+                    id: uuid::Uuid::new_v4().to_string(),
+                    name: "".into(),
+                    appearance: Appearance::Dark,
+                    styles: ThemeStyles {
+                        system: SystemColors::default(),
+                        colors: ThemeColors::default_dark(),
+                        status: StatusColors::default(),
+                        git: GitStatusColors::default(),
+                        player: PlayerColors::default(),
+                        syntax: SyntaxTheme::default_dark(),
+                    },
+                })
+                .collect(),
+            scales: default_color_scales(),
+        };
+
+        theme_families.push(theme_family);
+    }
 
     Ok(())
 }

crates/theme_importer/src/vscode.rs 🔗

@@ -5,21 +5,21 @@ use serde::Deserialize;
 use theme::{default_color_scales, ColorScales, ThemeFamily};
 
 #[derive(Deserialize, Debug)]
-pub struct VSCodeTheme {
+pub struct VsCodeTheme {
     #[serde(rename = "$schema")]
-    pub schema: String,
-    pub name: String,
-    pub author: String,
-    pub maintainers: Vec<String>,
+    pub schema: Option<String>,
+    pub name: Option<String>,
+    pub author: Option<String>,
+    pub maintainers: Option<Vec<String>>,
     #[serde(rename = "semanticClass")]
-    pub semantic_class: String,
+    pub semantic_class: Option<String>,
     #[serde(rename = "semanticHighlighting")]
-    pub semantic_highlighting: bool,
-    pub colors: VSCodeColors,
+    pub semantic_highlighting: Option<bool>,
+    pub colors: VsCodeColors,
 }
 
 #[derive(Debug, Deserialize)]
-pub struct VSCodeColors {
+pub struct VsCodeColors {
     #[serde(rename = "editor.foreground")]
     text: String,
     #[serde(rename = "editor.background")]
@@ -27,36 +27,38 @@ pub struct VSCodeColors {
 }
 
 pub(crate) fn new_theme_family_from_vsc(path: &Path) -> Result<ThemeFamily> {
-    let path_str = path.to_str().unwrap();
-    let family_name = path_str.split('/').last().unwrap();
+    todo!()
 
-    let mut json_files: Vec<String> = Vec::new();
+    // let path_str = path.to_str().unwrap();
+    // let family_name = path_str.split('/').last().unwrap();
 
-    if path.is_dir() {
-        for entry in std::fs::read_dir(path).unwrap() {
-            let entry = entry.unwrap();
-            let path = entry.path();
-            if path.is_file() {
-                if let Some(extension) = path.extension() {
-                    if extension == "json" {
-                        json_files.push(path.file_name().unwrap().to_str().unwrap().to_string());
-                    }
-                }
-            }
-        }
-    } else {
-        anyhow::bail!("Path is not a directory");
-    }
+    // let mut json_files: Vec<String> = Vec::new();
+
+    // if path.is_dir() {
+    //     for entry in std::fs::read_dir(path).unwrap() {
+    //         let entry = entry.unwrap();
+    //         let path = entry.path();
+    //         if path.is_file() {
+    //             if let Some(extension) = path.extension() {
+    //                 if extension == "json" {
+    //                     json_files.push(path.file_name().unwrap().to_str().unwrap().to_string());
+    //                 }
+    //             }
+    //         }
+    //     }
+    // } else {
+    //     anyhow::bail!("Path is not a directory");
+    // }
 
-    let mut theme_family = ThemeFamily {
-        id: uuid::Uuid::new_v4().to_string(),
-        name: family_name.into(),
-        author: "New Theme Family".into(),
-        themes: Vec::new(),
-        scales: default_color_scales(),
-    };
+    // let mut theme_family = ThemeFamily {
+    //     id: uuid::Uuid::new_v4().to_string(),
+    //     name: family_name.into(),
+    //     author: "New Theme Family".into(),
+    //     themes: Vec::new(),
+    //     scales: default_color_scales(),
+    // };
 
-    Ok(theme_family)
+    // Ok(theme_family)
 }
 
 #[cfg(test)]