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