1mod assets;
2mod color;
3mod theme_printer;
4mod util;
5mod vscode;
6mod zed1;
7
8use std::collections::HashMap;
9use std::fs::{self, File};
10use std::io::Write;
11use std::path::PathBuf;
12use std::process::Command;
13use std::str::FromStr;
14
15use any_ascii::any_ascii;
16use anyhow::{anyhow, Context, Result};
17use clap::Parser;
18use convert_case::{Case, Casing};
19use indexmap::IndexMap;
20use indoc::formatdoc;
21use json_comments::StripComments;
22use log::LevelFilter;
23use serde::Deserialize;
24use simplelog::{TermLogger, TerminalMode};
25use theme::{Appearance, UserTheme, UserThemeFamily};
26
27use crate::theme_printer::UserThemeFamilyPrinter;
28use crate::vscode::VsCodeTheme;
29use crate::vscode::VsCodeThemeConverter;
30use crate::zed1::theme::Theme as Zed1Theme;
31use crate::zed1::{zed1_theme_licenses, Zed1ThemeConverter};
32
33#[derive(Debug, Deserialize)]
34struct FamilyMetadata {
35 pub name: String,
36 pub author: String,
37 pub themes: Vec<ThemeMetadata>,
38
39 /// Overrides for specific syntax tokens.
40 ///
41 /// Use this to ensure certain Zed syntax tokens are matched
42 /// to an exact set of scopes when it is not otherwise possible
43 /// to rely on the default mappings in the theme importer.
44 #[serde(default)]
45 pub syntax: IndexMap<String, Vec<String>>,
46}
47
48#[derive(Debug, Clone, Copy, Deserialize)]
49#[serde(rename_all = "snake_case")]
50pub enum ThemeAppearanceJson {
51 Light,
52 Dark,
53}
54
55impl From<ThemeAppearanceJson> for Appearance {
56 fn from(value: ThemeAppearanceJson) -> Self {
57 match value {
58 ThemeAppearanceJson::Light => Self::Light,
59 ThemeAppearanceJson::Dark => Self::Dark,
60 }
61 }
62}
63
64#[derive(Debug, Deserialize)]
65pub struct ThemeMetadata {
66 pub name: String,
67 pub file_name: String,
68 pub appearance: ThemeAppearanceJson,
69}
70
71#[derive(Parser)]
72#[command(author, version, about, long_about = None)]
73struct Args {
74 /// Whether to warn when values are missing from the theme.
75 #[arg(long)]
76 warn_on_missing: bool,
77}
78
79fn main() -> Result<()> {
80 const SOURCE_PATH: &str = "assets/themes/src/vscode";
81 const OUT_PATH: &str = "crates/theme/src/themes";
82
83 let args = Args::parse();
84
85 let log_config = {
86 let mut config = simplelog::ConfigBuilder::new();
87 config
88 .set_level_color(log::Level::Trace, simplelog::Color::Cyan)
89 .set_level_color(log::Level::Info, simplelog::Color::Blue)
90 .set_level_color(log::Level::Warn, simplelog::Color::Yellow)
91 .set_level_color(log::Level::Error, simplelog::Color::Red);
92
93 if !args.warn_on_missing {
94 config.add_filter_ignore_str("theme_printer");
95 }
96
97 config.build()
98 };
99
100 TermLogger::init(LevelFilter::Trace, log_config, TerminalMode::Mixed)
101 .expect("could not initialize logger");
102
103 let mut theme_families = Vec::new();
104
105 /// Whether VS Code themes should be imported.
106 const IMPORT_VS_CODE_THEMES: bool = false;
107
108 if IMPORT_VS_CODE_THEMES {
109 log::info!("Loading themes source...");
110 let vscode_themes_path = PathBuf::from_str(SOURCE_PATH)?;
111 if !vscode_themes_path.exists() {
112 return Err(anyhow!(format!(
113 "Couldn't find {}, make sure it exists",
114 SOURCE_PATH
115 )));
116 }
117
118 for theme_family_dir in fs::read_dir(&vscode_themes_path)? {
119 let theme_family_dir = theme_family_dir?;
120
121 if !theme_family_dir.file_type()?.is_dir() {
122 continue;
123 }
124
125 let theme_family_slug = theme_family_dir
126 .path()
127 .file_stem()
128 .ok_or(anyhow!("no file stem"))
129 .map(|stem| stem.to_string_lossy().to_string())?;
130
131 let family_metadata_file = File::open(theme_family_dir.path().join("family.json"))
132 .context(format!(
133 "no `family.json` found for '{}'",
134 theme_family_slug
135 ))?;
136
137 let license_file_path = theme_family_dir.path().join("LICENSE");
138
139 if !license_file_path.exists() {
140 log::info!("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);
141 continue;
142 }
143
144 let family_metadata: FamilyMetadata = serde_json::from_reader(family_metadata_file)
145 .context(format!(
146 "failed to parse `family.json` for '{theme_family_slug}'"
147 ))?;
148
149 let mut themes = Vec::new();
150
151 for theme_metadata in family_metadata.themes {
152 log::info!("Converting '{}' theme", &theme_metadata.name);
153
154 let theme_file_path = theme_family_dir.path().join(&theme_metadata.file_name);
155
156 let theme_file = match File::open(&theme_file_path) {
157 Ok(file) => file,
158 Err(_) => {
159 log::info!("Failed to open file at path: {:?}", theme_file_path);
160 continue;
161 }
162 };
163
164 let theme_without_comments = StripComments::new(theme_file);
165 let vscode_theme: VsCodeTheme = serde_json::from_reader(theme_without_comments)
166 .context(format!("failed to parse theme {theme_file_path:?}"))?;
167
168 let converter = VsCodeThemeConverter::new(
169 vscode_theme,
170 theme_metadata,
171 family_metadata.syntax.clone(),
172 );
173
174 let theme = converter.convert()?;
175
176 themes.push(theme);
177 }
178
179 let theme_family = UserThemeFamily {
180 name: family_metadata.name.into(),
181 author: family_metadata.author.into(),
182 themes,
183 };
184
185 theme_families.push(theme_family);
186 }
187 }
188
189 let zed1_themes_path = PathBuf::from_str("assets/themes")?;
190
191 let zed1_theme_families = [
192 "Andromeda",
193 "Atelier",
194 "Ayu",
195 "Gruvbox",
196 "One",
197 "Rosé Pine",
198 "Sandcastle",
199 "Solarized",
200 "Summercamp",
201 ];
202
203 let zed1_licenses_by_theme: HashMap<String, zed1::Zed1ThemeLicense> = HashMap::from_iter(
204 zed1_theme_licenses()
205 .into_iter()
206 .map(|theme_license| (theme_license.theme.clone(), theme_license)),
207 );
208
209 let mut zed1_themes_by_family: IndexMap<String, Vec<UserTheme>> = IndexMap::from_iter(
210 zed1_theme_families
211 .into_iter()
212 .map(|family| (family.to_string(), Vec::new())),
213 );
214
215 for entry in fs::read_dir(&zed1_themes_path)? {
216 let entry = entry?;
217
218 if entry.file_type()?.is_dir() {
219 continue;
220 }
221
222 match entry.path().extension() {
223 None => continue,
224 Some(extension) => {
225 if extension != "json" {
226 continue;
227 }
228 }
229 }
230
231 let theme_file_path = entry.path();
232
233 let theme_file = match File::open(&theme_file_path) {
234 Ok(file) => file,
235 Err(_) => {
236 log::info!("Failed to open file at path: {:?}", theme_file_path);
237 continue;
238 }
239 };
240
241 let theme_without_comments = StripComments::new(theme_file);
242
243 let zed1_theme: Zed1Theme = serde_json::from_reader(theme_without_comments)
244 .context(format!("failed to parse theme {theme_file_path:?}"))?;
245
246 let theme_name = zed1_theme.meta.name.clone();
247
248 let converter = Zed1ThemeConverter::new(zed1_theme);
249
250 let theme = converter.convert()?;
251
252 let Some((_, themes_for_family)) = zed1_themes_by_family
253 .iter_mut()
254 .find(|(family, _)| theme_name.starts_with(*family))
255 else {
256 log::warn!("No theme family found for '{}'.", theme_name);
257 continue;
258 };
259
260 themes_for_family.push(theme);
261 }
262
263 zed1_themes_by_family.sort_keys();
264
265 let mut licenses = Vec::new();
266
267 for (family, themes) in zed1_themes_by_family {
268 let mut theme_family = UserThemeFamily {
269 name: family,
270 author: "Zed Industries".to_string(),
271 themes,
272 };
273
274 theme_family
275 .themes
276 .sort_unstable_by_key(|theme| theme.name.clone());
277
278 for theme in &theme_family.themes {
279 let license = zed1_licenses_by_theme
280 .get(&theme.name)
281 .ok_or_else(|| anyhow!("missing license for theme: '{}'", theme.name))?;
282
283 let license_header = match license.license_url.as_ref() {
284 Some(license_url) => {
285 format!("[{theme_name}]({license_url})", theme_name = theme.name)
286 }
287 None => theme.name.clone(),
288 };
289
290 licenses.push(formatdoc!(
291 "
292 ## {license_header}
293
294 {license_text}
295 ********************************************************************************
296 ",
297 license_text = license.license_text
298 ));
299 }
300
301 theme_families.push(theme_family);
302 }
303
304 let themes_output_path = PathBuf::from_str(OUT_PATH)?;
305
306 if !themes_output_path.exists() {
307 log::info!("Creating directory: {:?}", themes_output_path);
308 fs::create_dir_all(&themes_output_path)?;
309 }
310
311 let mut mod_rs_file = File::create(themes_output_path.join(format!("mod.rs")))?;
312
313 let mut theme_modules = Vec::new();
314
315 for theme_family in theme_families {
316 let theme_family_slug = any_ascii(&theme_family.name)
317 .replace("(", "")
318 .replace(")", "")
319 .to_case(Case::Snake);
320
321 let mut output_file =
322 File::create(themes_output_path.join(format!("{theme_family_slug}.rs")))?;
323 log::info!(
324 "Creating file: {:?}",
325 themes_output_path.join(format!("{theme_family_slug}.rs"))
326 );
327
328 let theme_module = format!(
329 r#"
330 // This file was generated by the `theme_importer`.
331 // Be careful when modifying it by hand.
332
333 use gpui::rgba;
334
335 #[allow(unused)]
336 use crate::{{
337 Appearance, PlayerColor, PlayerColors, StatusColorsRefinement, ThemeColorsRefinement,
338 UserHighlightStyle, UserSyntaxTheme, UserTheme, UserThemeFamily, UserThemeStylesRefinement,
339 UserFontWeight, UserFontStyle
340 }};
341
342 pub fn {theme_family_slug}() -> UserThemeFamily {{
343 {theme_family_definition}
344 }}
345 "#,
346 theme_family_definition = format!("{:#?}", UserThemeFamilyPrinter::new(theme_family))
347 );
348
349 output_file.write_all(theme_module.as_bytes())?;
350
351 theme_modules.push(theme_family_slug);
352 }
353
354 theme_modules.sort();
355
356 let themes_vector_contents = format!(
357 r#"
358 use crate::UserThemeFamily;
359
360 pub(crate) fn all_user_themes() -> Vec<UserThemeFamily> {{
361 vec![{all_themes}]
362 }}
363 "#,
364 all_themes = theme_modules
365 .iter()
366 .map(|module| format!("{}()", module))
367 .collect::<Vec<_>>()
368 .join(", ")
369 );
370
371 let mod_rs_contents = format!(
372 r#"
373 // This file was generated by the `theme_importer`.
374 // Be careful when modifying it by hand.
375
376 {mod_statements}
377
378 {use_statements}
379
380 {themes_vector_contents}
381 "#,
382 mod_statements = theme_modules
383 .iter()
384 .map(|module| format!("mod {module};"))
385 .collect::<Vec<_>>()
386 .join("\n"),
387 use_statements = theme_modules
388 .iter()
389 .map(|module| format!("pub use {module}::*;"))
390 .collect::<Vec<_>>()
391 .join("\n"),
392 themes_vector_contents = themes_vector_contents
393 );
394
395 mod_rs_file.write_all(mod_rs_contents.as_bytes())?;
396
397 log::info!("Writing LICENSES file...");
398
399 let mut licenses_file = File::create(themes_output_path.join(format!("LICENSES")))?;
400
401 licenses_file.write_all(licenses.join("\n").as_bytes())?;
402
403 log::info!("Formatting themes...");
404
405 let format_result = format_themes_crate()
406 // We need to format a second time to catch all of the formatting issues.
407 .and_then(|_| format_themes_crate());
408
409 if let Err(err) = format_result {
410 log::error!("Failed to format themes: {}", err);
411 }
412
413 log::info!("Done!");
414
415 Ok(())
416}
417
418fn format_themes_crate() -> std::io::Result<std::process::Output> {
419 Command::new("cargo")
420 .args(["fmt", "--package", "theme"])
421 .output()
422}