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