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}