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