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 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::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 ///
107 /// For the initial release of Zed2, we will just be using the Zed1 themes ported to Zed2.
108 const IMPORT_VS_CODE_THEMES: bool = false;
109
110 if IMPORT_VS_CODE_THEMES {
111 log::info!("Loading themes source...");
112 let vscode_themes_path = PathBuf::from_str(SOURCE_PATH)?;
113 if !vscode_themes_path.exists() {
114 return Err(anyhow!(format!(
115 "Couldn't find {}, make sure it exists",
116 SOURCE_PATH
117 )));
118 }
119
120 for theme_family_dir in fs::read_dir(&vscode_themes_path)? {
121 let theme_family_dir = theme_family_dir?;
122
123 if !theme_family_dir.file_type()?.is_dir() {
124 continue;
125 }
126
127 let theme_family_slug = theme_family_dir
128 .path()
129 .file_stem()
130 .ok_or(anyhow!("no file stem"))
131 .map(|stem| stem.to_string_lossy().to_string())?;
132
133 let family_metadata_file = File::open(theme_family_dir.path().join("family.json"))
134 .context(format!(
135 "no `family.json` found for '{}'",
136 theme_family_slug
137 ))?;
138
139 let license_file_path = theme_family_dir.path().join("LICENSE");
140
141 if !license_file_path.exists() {
142 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);
143 continue;
144 }
145
146 let family_metadata: FamilyMetadata = serde_json::from_reader(family_metadata_file)
147 .context(format!(
148 "failed to parse `family.json` for '{theme_family_slug}'"
149 ))?;
150
151 let mut themes = Vec::new();
152
153 for theme_metadata in family_metadata.themes {
154 log::info!("Converting '{}' theme", &theme_metadata.name);
155
156 let theme_file_path = theme_family_dir.path().join(&theme_metadata.file_name);
157
158 let theme_file = match File::open(&theme_file_path) {
159 Ok(file) => file,
160 Err(_) => {
161 log::info!("Failed to open file at path: {:?}", theme_file_path);
162 continue;
163 }
164 };
165
166 let theme_without_comments = StripComments::new(theme_file);
167 let vscode_theme: VsCodeTheme = serde_json::from_reader(theme_without_comments)
168 .context(format!("failed to parse theme {theme_file_path:?}"))?;
169
170 let converter = VsCodeThemeConverter::new(
171 vscode_theme,
172 theme_metadata,
173 family_metadata.syntax.clone(),
174 );
175
176 let theme = converter.convert()?;
177
178 themes.push(theme);
179 }
180
181 let theme_family = UserThemeFamily {
182 name: family_metadata.name.into(),
183 author: family_metadata.author.into(),
184 themes,
185 };
186
187 theme_families.push(theme_family);
188 }
189 }
190
191 let zed1_themes_path = PathBuf::from_str("assets/themes")?;
192
193 let zed1_theme_familes = [
194 "Andromeda",
195 "Atelier",
196 "Ayu",
197 "Gruvbox",
198 "One",
199 "Rosé Pine",
200 "Sandcastle",
201 "Solarized",
202 "Summercamp",
203 ];
204
205 let mut zed1_themes_by_family: HashMap<String, Vec<UserTheme>> = HashMap::from_iter(
206 zed1_theme_familes
207 .into_iter()
208 .map(|family| (family.to_string(), Vec::new())),
209 );
210
211 for entry in fs::read_dir(&zed1_themes_path)? {
212 let entry = entry?;
213
214 if entry.file_type()?.is_dir() {
215 continue;
216 }
217
218 match entry.path().extension() {
219 None => continue,
220 Some(extension) => {
221 if extension != "json" {
222 continue;
223 }
224 }
225 }
226
227 let theme_file_path = entry.path();
228
229 let theme_file = match File::open(&theme_file_path) {
230 Ok(file) => file,
231 Err(_) => {
232 log::info!("Failed to open file at path: {:?}", theme_file_path);
233 continue;
234 }
235 };
236
237 let theme_without_comments = StripComments::new(theme_file);
238
239 let zed1_theme: Zed1Theme = serde_json::from_reader(theme_without_comments)
240 .context(format!("failed to parse theme {theme_file_path:?}"))?;
241
242 let theme_name = zed1_theme.meta.name.clone();
243
244 let converter = Zed1ThemeConverter::new(zed1_theme);
245
246 let theme = converter.convert()?;
247
248 let Some((_, themes_for_family)) = zed1_themes_by_family
249 .iter_mut()
250 .find(|(family, _)| theme_name.starts_with(*family))
251 else {
252 log::warn!("No theme family found for '{}'.", theme_name);
253 continue;
254 };
255
256 themes_for_family.push(theme);
257 }
258
259 for (family, themes) in zed1_themes_by_family {
260 let theme_family = UserThemeFamily {
261 name: family,
262 author: "Zed Industries".to_string(),
263 themes,
264 };
265
266 theme_families.push(theme_family);
267 }
268
269 let themes_output_path = PathBuf::from_str(OUT_PATH)?;
270
271 if !themes_output_path.exists() {
272 log::info!("Creating directory: {:?}", themes_output_path);
273 fs::create_dir_all(&themes_output_path)?;
274 }
275
276 let mut mod_rs_file = File::create(themes_output_path.join(format!("mod.rs")))?;
277
278 let mut theme_modules = Vec::new();
279
280 for theme_family in theme_families {
281 let theme_family_slug = any_ascii(&theme_family.name)
282 .replace("(", "")
283 .replace(")", "")
284 .to_case(Case::Snake);
285
286 let mut output_file =
287 File::create(themes_output_path.join(format!("{theme_family_slug}.rs")))?;
288 log::info!(
289 "Creating file: {:?}",
290 themes_output_path.join(format!("{theme_family_slug}.rs"))
291 );
292
293 let theme_module = format!(
294 r#"
295 // This file was generated by the `theme_importer`.
296 // Be careful when modifying it by hand.
297
298 use gpui::rgba;
299
300 #[allow(unused)]
301 use crate::{{
302 Appearance, PlayerColor, PlayerColors, StatusColorsRefinement, ThemeColorsRefinement,
303 UserHighlightStyle, UserSyntaxTheme, UserTheme, UserThemeFamily, UserThemeStylesRefinement,
304 UserFontWeight, UserFontStyle
305 }};
306
307 pub fn {theme_family_slug}() -> UserThemeFamily {{
308 {theme_family_definition}
309 }}
310 "#,
311 theme_family_definition = format!("{:#?}", UserThemeFamilyPrinter::new(theme_family))
312 );
313
314 output_file.write_all(theme_module.as_bytes())?;
315
316 theme_modules.push(theme_family_slug);
317 }
318
319 theme_modules.sort();
320
321 let themes_vector_contents = format!(
322 r#"
323 use crate::UserThemeFamily;
324
325 pub(crate) fn all_user_themes() -> Vec<UserThemeFamily> {{
326 vec![{all_themes}]
327 }}
328 "#,
329 all_themes = theme_modules
330 .iter()
331 .map(|module| format!("{}()", module))
332 .collect::<Vec<_>>()
333 .join(", ")
334 );
335
336 let mod_rs_contents = format!(
337 r#"
338 // This file was generated by the `theme_importer`.
339 // Be careful when modifying it by hand.
340
341 {mod_statements}
342
343 {use_statements}
344
345 {themes_vector_contents}
346 "#,
347 mod_statements = theme_modules
348 .iter()
349 .map(|module| format!("mod {module};"))
350 .collect::<Vec<_>>()
351 .join("\n"),
352 use_statements = theme_modules
353 .iter()
354 .map(|module| format!("pub use {module}::*;"))
355 .collect::<Vec<_>>()
356 .join("\n"),
357 themes_vector_contents = themes_vector_contents
358 );
359
360 mod_rs_file.write_all(mod_rs_contents.as_bytes())?;
361
362 log::info!("Formatting themes...");
363
364 let format_result = format_themes_crate()
365 // We need to format a second time to catch all of the formatting issues.
366 .and_then(|_| format_themes_crate());
367
368 if let Err(err) = format_result {
369 log::error!("Failed to format themes: {}", err);
370 }
371
372 log::info!("Done!");
373
374 Ok(())
375}
376
377fn format_themes_crate() -> std::io::Result<std::process::Output> {
378 Command::new("cargo")
379 .args(["fmt", "--package", "theme"])
380 .output()
381}