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