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 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_familes = [
192 "Andromeda",
193 "Atelier",
194 "Ayu",
195 "Gruvbox",
196 "One",
197 "Rosé Pine",
198 "Sandcastle",
199 "Solarized",
200 "Summercamp",
201 ];
202
203 let mut zed1_themes_by_family: HashMap<String, Vec<UserTheme>> = HashMap::from_iter(
204 zed1_theme_familes
205 .into_iter()
206 .map(|family| (family.to_string(), Vec::new())),
207 );
208
209 for entry in fs::read_dir(&zed1_themes_path)? {
210 let entry = entry?;
211
212 if entry.file_type()?.is_dir() {
213 continue;
214 }
215
216 match entry.path().extension() {
217 None => continue,
218 Some(extension) => {
219 if extension != "json" {
220 continue;
221 }
222 }
223 }
224
225 let theme_file_path = entry.path();
226
227 let theme_file = match File::open(&theme_file_path) {
228 Ok(file) => file,
229 Err(_) => {
230 log::info!("Failed to open file at path: {:?}", theme_file_path);
231 continue;
232 }
233 };
234
235 let theme_without_comments = StripComments::new(theme_file);
236
237 let zed1_theme: Zed1Theme = serde_json::from_reader(theme_without_comments)
238 .context(format!("failed to parse theme {theme_file_path:?}"))?;
239
240 let theme_name = zed1_theme.meta.name.clone();
241
242 let converter = Zed1ThemeConverter::new(zed1_theme);
243
244 let theme = converter.convert()?;
245
246 let Some((_, themes_for_family)) = zed1_themes_by_family
247 .iter_mut()
248 .find(|(family, _)| theme_name.starts_with(*family))
249 else {
250 log::warn!("No theme family found for '{}'.", theme_name);
251 continue;
252 };
253
254 themes_for_family.push(theme);
255 }
256
257 for (family, themes) in zed1_themes_by_family {
258 let theme_family = UserThemeFamily {
259 name: family,
260 author: "Zed Industries".to_string(),
261 themes,
262 };
263
264 theme_families.push(theme_family);
265 }
266
267 let themes_output_path = PathBuf::from_str(OUT_PATH)?;
268
269 if !themes_output_path.exists() {
270 log::info!("Creating directory: {:?}", themes_output_path);
271 fs::create_dir_all(&themes_output_path)?;
272 }
273
274 let mut mod_rs_file = File::create(themes_output_path.join(format!("mod.rs")))?;
275
276 let mut theme_modules = Vec::new();
277
278 for theme_family in theme_families {
279 let theme_family_slug = any_ascii(&theme_family.name)
280 .replace("(", "")
281 .replace(")", "")
282 .to_case(Case::Snake);
283
284 let mut output_file =
285 File::create(themes_output_path.join(format!("{theme_family_slug}.rs")))?;
286 log::info!(
287 "Creating file: {:?}",
288 themes_output_path.join(format!("{theme_family_slug}.rs"))
289 );
290
291 let theme_module = format!(
292 r#"
293 // This file was generated by the `theme_importer`.
294 // Be careful when modifying it by hand.
295
296 use gpui::rgba;
297
298 #[allow(unused)]
299 use crate::{{
300 Appearance, PlayerColor, PlayerColors, StatusColorsRefinement, ThemeColorsRefinement,
301 UserHighlightStyle, UserSyntaxTheme, UserTheme, UserThemeFamily, UserThemeStylesRefinement,
302 UserFontWeight, UserFontStyle
303 }};
304
305 pub fn {theme_family_slug}() -> UserThemeFamily {{
306 {theme_family_definition}
307 }}
308 "#,
309 theme_family_definition = format!("{:#?}", UserThemeFamilyPrinter::new(theme_family))
310 );
311
312 output_file.write_all(theme_module.as_bytes())?;
313
314 theme_modules.push(theme_family_slug);
315 }
316
317 theme_modules.sort();
318
319 let themes_vector_contents = format!(
320 r#"
321 use crate::UserThemeFamily;
322
323 pub(crate) fn all_user_themes() -> Vec<UserThemeFamily> {{
324 vec![{all_themes}]
325 }}
326 "#,
327 all_themes = theme_modules
328 .iter()
329 .map(|module| format!("{}()", module))
330 .collect::<Vec<_>>()
331 .join(", ")
332 );
333
334 let mod_rs_contents = format!(
335 r#"
336 // This file was generated by the `theme_importer`.
337 // Be careful when modifying it by hand.
338
339 {mod_statements}
340
341 {use_statements}
342
343 {themes_vector_contents}
344 "#,
345 mod_statements = theme_modules
346 .iter()
347 .map(|module| format!("mod {module};"))
348 .collect::<Vec<_>>()
349 .join("\n"),
350 use_statements = theme_modules
351 .iter()
352 .map(|module| format!("pub use {module}::*;"))
353 .collect::<Vec<_>>()
354 .join("\n"),
355 themes_vector_contents = themes_vector_contents
356 );
357
358 mod_rs_file.write_all(mod_rs_contents.as_bytes())?;
359
360 log::info!("Formatting themes...");
361
362 let format_result = format_themes_crate()
363 // We need to format a second time to catch all of the formatting issues.
364 .and_then(|_| format_themes_crate());
365
366 if let Err(err) = format_result {
367 log::error!("Failed to format themes: {}", err);
368 }
369
370 log::info!("Done!");
371
372 Ok(())
373}
374
375fn format_themes_crate() -> std::io::Result<std::process::Output> {
376 Command::new("cargo")
377 .args(["fmt", "--package", "theme"])
378 .output()
379}