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