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