1mod color;
2mod theme_printer;
3mod util;
4mod vscode;
5
6use std::fs::{self, File};
7use std::io::Write;
8use std::path::PathBuf;
9use std::process::Command;
10use std::str::FromStr;
11
12use any_ascii::any_ascii;
13use anyhow::{anyhow, Context, Result};
14use clap::Parser;
15use convert_case::{Case, Casing};
16use gpui::serde_json;
17use indexmap::IndexMap;
18use json_comments::StripComments;
19use log::LevelFilter;
20use serde::Deserialize;
21use simplelog::{TermLogger, TerminalMode};
22use theme::{Appearance, UserThemeFamily};
23
24use crate::theme_printer::UserThemeFamilyPrinter;
25use crate::vscode::VsCodeTheme;
26use crate::vscode::VsCodeThemeConverter;
27
28#[derive(Debug, Deserialize)]
29struct FamilyMetadata {
30 pub name: String,
31 pub author: String,
32 pub themes: Vec<ThemeMetadata>,
33
34 /// Overrides for specific syntax tokens.
35 ///
36 /// Use this to ensure certain Zed syntax tokens are matched
37 /// to an exact set of scopes when it is not otherwise possible
38 /// to rely on the default mappings in the theme importer.
39 #[serde(default)]
40 pub syntax: IndexMap<String, Vec<String>>,
41}
42
43#[derive(Debug, Clone, Copy, Deserialize)]
44#[serde(rename_all = "snake_case")]
45pub enum ThemeAppearanceJson {
46 Light,
47 Dark,
48}
49
50impl From<ThemeAppearanceJson> for Appearance {
51 fn from(value: ThemeAppearanceJson) -> Self {
52 match value {
53 ThemeAppearanceJson::Light => Self::Light,
54 ThemeAppearanceJson::Dark => Self::Dark,
55 }
56 }
57}
58
59#[derive(Debug, Deserialize)]
60pub struct ThemeMetadata {
61 pub name: String,
62 pub file_name: String,
63 pub appearance: ThemeAppearanceJson,
64}
65
66#[derive(Parser)]
67#[command(author, version, about, long_about = None)]
68struct Args {
69 /// Whether to warn when values are missing from the theme.
70 #[arg(long)]
71 warn_on_missing: bool,
72}
73
74fn main() -> Result<()> {
75 const SOURCE_PATH: &str = "assets/themes/src/vscode";
76 const OUT_PATH: &str = "crates/theme2/src/themes";
77
78 let args = Args::parse();
79
80 let log_config = {
81 let mut config = simplelog::ConfigBuilder::new();
82 config
83 .set_level_color(log::Level::Trace, simplelog::Color::Cyan)
84 .set_level_color(log::Level::Info, simplelog::Color::Blue)
85 .set_level_color(log::Level::Warn, simplelog::Color::Yellow)
86 .set_level_color(log::Level::Error, simplelog::Color::Red);
87
88 if !args.warn_on_missing {
89 config.add_filter_ignore_str("theme_printer");
90 }
91
92 config.build()
93 };
94
95 TermLogger::init(LevelFilter::Trace, log_config, TerminalMode::Mixed)
96 .expect("could not initialize logger");
97
98 log::info!("Loading themes source...");
99 let vscode_themes_path = PathBuf::from_str(SOURCE_PATH)?;
100 if !vscode_themes_path.exists() {
101 return Err(anyhow!(format!(
102 "Couldn't find {}, make sure it exists",
103 SOURCE_PATH
104 )));
105 }
106
107 let mut theme_families = Vec::new();
108
109 for theme_family_dir in fs::read_dir(&vscode_themes_path)? {
110 let theme_family_dir = theme_family_dir?;
111
112 if !theme_family_dir.file_type()?.is_dir() {
113 continue;
114 }
115
116 let theme_family_slug = theme_family_dir
117 .path()
118 .file_stem()
119 .ok_or(anyhow!("no file stem"))
120 .map(|stem| stem.to_string_lossy().to_string())?;
121
122 let family_metadata_file = File::open(theme_family_dir.path().join("family.json"))
123 .context(format!(
124 "no `family.json` found for '{}'",
125 theme_family_slug
126 ))?;
127
128 let license_file_path = theme_family_dir.path().join("LICENSE");
129
130 if !license_file_path.exists() {
131 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);
132 continue;
133 }
134
135 let family_metadata: FamilyMetadata = serde_json::from_reader(family_metadata_file)
136 .context(format!(
137 "failed to parse `family.json` for '{theme_family_slug}'"
138 ))?;
139
140 let mut themes = Vec::new();
141
142 for theme_metadata in family_metadata.themes {
143 log::info!("Converting '{}' theme", &theme_metadata.name);
144
145 let theme_file_path = theme_family_dir.path().join(&theme_metadata.file_name);
146
147 let theme_file = match File::open(&theme_file_path) {
148 Ok(file) => file,
149 Err(_) => {
150 log::info!("Failed to open file at path: {:?}", theme_file_path);
151 continue;
152 }
153 };
154
155 let theme_without_comments = StripComments::new(theme_file);
156 let vscode_theme: VsCodeTheme = serde_json::from_reader(theme_without_comments)
157 .context(format!("failed to parse theme {theme_file_path:?}"))?;
158
159 let converter = VsCodeThemeConverter::new(
160 vscode_theme,
161 theme_metadata,
162 family_metadata.syntax.clone(),
163 );
164
165 let theme = converter.convert()?;
166
167 themes.push(theme);
168 }
169
170 let theme_family = UserThemeFamily {
171 name: family_metadata.name.into(),
172 author: family_metadata.author.into(),
173 themes,
174 };
175
176 theme_families.push(theme_family);
177 }
178
179 let themes_output_path = PathBuf::from_str(OUT_PATH)?;
180
181 if !themes_output_path.exists() {
182 log::info!("Creating directory: {:?}", themes_output_path);
183 fs::create_dir_all(&themes_output_path)?;
184 }
185
186 let mut mod_rs_file = File::create(themes_output_path.join(format!("mod.rs")))?;
187
188 let mut theme_modules = Vec::new();
189
190 for theme_family in theme_families {
191 let theme_family_slug = any_ascii(&theme_family.name).to_case(Case::Snake);
192
193 let mut output_file =
194 File::create(themes_output_path.join(format!("{theme_family_slug}.rs")))?;
195 log::info!(
196 "Creating file: {:?}",
197 themes_output_path.join(format!("{theme_family_slug}.rs"))
198 );
199
200 let theme_module = format!(
201 r#"
202 // This file was generated by the `theme_importer`.
203 // Be careful when modifying it by hand.
204
205 use gpui::rgba;
206
207 #[allow(unused)]
208 use crate::{{
209 Appearance, StatusColorsRefinement, ThemeColorsRefinement, UserHighlightStyle, UserSyntaxTheme,
210 UserTheme, UserThemeFamily, UserThemeStylesRefinement, UserFontWeight, UserFontStyle
211 }};
212
213 pub fn {theme_family_slug}() -> UserThemeFamily {{
214 {theme_family_definition}
215 }}
216 "#,
217 theme_family_definition = format!("{:#?}", UserThemeFamilyPrinter::new(theme_family))
218 );
219
220 output_file.write_all(theme_module.as_bytes())?;
221
222 theme_modules.push(theme_family_slug);
223 }
224
225 let themes_vector_contents = format!(
226 r#"
227 use crate::UserThemeFamily;
228
229 pub(crate) fn all_user_themes() -> Vec<UserThemeFamily> {{
230 vec![{all_themes}]
231 }}
232 "#,
233 all_themes = theme_modules
234 .iter()
235 .map(|module| format!("{}()", module))
236 .collect::<Vec<_>>()
237 .join(", ")
238 );
239
240 let mod_rs_contents = format!(
241 r#"
242 // This file was generated by the `theme_importer`.
243 // Be careful when modifying it by hand.
244
245 {mod_statements}
246
247 {use_statements}
248
249 {themes_vector_contents}
250 "#,
251 mod_statements = theme_modules
252 .iter()
253 .map(|module| format!("mod {module};"))
254 .collect::<Vec<_>>()
255 .join("\n"),
256 use_statements = theme_modules
257 .iter()
258 .map(|module| format!("pub use {module}::*;"))
259 .collect::<Vec<_>>()
260 .join("\n"),
261 themes_vector_contents = themes_vector_contents
262 );
263
264 mod_rs_file.write_all(mod_rs_contents.as_bytes())?;
265
266 log::info!("Formatting themes...");
267
268 let format_result = format_themes_crate()
269 // We need to format a second time to catch all of the formatting issues.
270 .and_then(|_| format_themes_crate());
271
272 if let Err(err) = format_result {
273 log::error!("Failed to format themes: {}", err);
274 }
275
276 log::info!("Done!");
277
278 Ok(())
279}
280
281fn format_themes_crate() -> std::io::Result<std::process::Output> {
282 Command::new("cargo")
283 .args(["fmt", "--package", "theme2"])
284 .output()
285}