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