zed/crates/theme_importer/src/main.rs

286 lines
8.4 KiB
Rust
Raw Normal View History

mod color;
2023-11-02 22:57:13 +00:00
mod theme_printer;
mod util;
2023-11-02 22:57:13 +00:00
mod vscode;
2023-11-02 22:00:55 +00:00
use std::fs::{self, File};
2023-11-02 23:14:51 +00:00
use std::io::Write;
use std::path::PathBuf;
use std::process::Command;
use std::str::FromStr;
use any_ascii::any_ascii;
use anyhow::{anyhow, Context, Result};
use clap::Parser;
2023-11-02 23:14:51 +00:00
use convert_case::{Case, Casing};
2023-11-02 22:24:38 +00:00
use gpui::serde_json;
use indexmap::IndexMap;
use json_comments::StripComments;
use log::LevelFilter;
use serde::Deserialize;
use simplelog::{TermLogger, TerminalMode};
use theme::{Appearance, UserThemeFamily};
2023-11-02 22:00:55 +00:00
use crate::theme_printer::UserThemeFamilyPrinter;
2023-11-02 22:00:55 +00:00
use crate::vscode::VsCodeTheme;
2023-11-09 18:07:32 +00:00
use crate::vscode::VsCodeThemeConverter;
2023-11-02 22:00:55 +00:00
#[derive(Debug, Deserialize)]
2023-11-02 22:24:38 +00:00
struct FamilyMetadata {
pub name: String,
2023-11-02 22:00:55 +00:00
pub author: String,
2023-11-02 22:24:38 +00:00
pub themes: Vec<ThemeMetadata>,
/// Overrides for specific syntax tokens.
///
/// Use this to ensure certain Zed syntax tokens are matched
/// to an exact set of scopes when it is not otherwise possible
/// to rely on the default mappings in the theme importer.
#[serde(default)]
pub syntax: IndexMap<String, Vec<String>>,
}
#[derive(Debug, Clone, Copy, Deserialize)]
#[serde(rename_all = "snake_case")]
2023-11-02 22:57:13 +00:00
pub enum ThemeAppearanceJson {
Light,
Dark,
}
2023-11-02 22:24:38 +00:00
impl From<ThemeAppearanceJson> for Appearance {
fn from(value: ThemeAppearanceJson) -> Self {
match value {
ThemeAppearanceJson::Light => Self::Light,
ThemeAppearanceJson::Dark => Self::Dark,
}
}
}
2023-11-02 22:24:38 +00:00
#[derive(Debug, Deserialize)]
2023-11-02 22:57:13 +00:00
pub struct ThemeMetadata {
pub name: String,
2023-11-02 22:24:38 +00:00
pub file_name: String,
pub appearance: ThemeAppearanceJson,
}
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Args {
/// Whether to warn when values are missing from the theme.
#[arg(long)]
warn_on_missing: bool,
}
fn main() -> Result<()> {
const SOURCE_PATH: &str = "assets/themes/src/vscode";
const OUT_PATH: &str = "crates/theme2/src/themes";
let args = Args::parse();
let log_config = {
let mut config = simplelog::ConfigBuilder::new();
config
.set_level_color(log::Level::Trace, simplelog::Color::Cyan)
.set_level_color(log::Level::Info, simplelog::Color::Blue)
.set_level_color(log::Level::Warn, simplelog::Color::Yellow)
.set_level_color(log::Level::Error, simplelog::Color::Red);
if !args.warn_on_missing {
config.add_filter_ignore_str("theme_printer");
}
config.build()
};
TermLogger::init(LevelFilter::Trace, log_config, TerminalMode::Mixed)
.expect("could not initialize logger");
log::info!("Loading themes source...");
let vscode_themes_path = PathBuf::from_str(SOURCE_PATH)?;
if !vscode_themes_path.exists() {
return Err(anyhow!(format!(
"Couldn't find {}, make sure it exists",
SOURCE_PATH
)));
}
2023-11-02 22:00:55 +00:00
let mut theme_families = Vec::new();
2023-11-02 22:00:55 +00:00
for theme_family_dir in fs::read_dir(&vscode_themes_path)? {
let theme_family_dir = theme_family_dir?;
if !theme_family_dir.file_type()?.is_dir() {
continue;
}
2023-11-02 22:00:55 +00:00
let theme_family_slug = theme_family_dir
.path()
.file_stem()
.ok_or(anyhow!("no file stem"))
.map(|stem| stem.to_string_lossy().to_string())?;
2023-11-02 22:00:55 +00:00
let family_metadata_file = File::open(theme_family_dir.path().join("family.json"))
.context(format!(
"no `family.json` found for '{}'",
theme_family_slug
))?;
let license_file_path = theme_family_dir.path().join("LICENSE");
if !license_file_path.exists() {
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);
continue;
}
2023-11-02 22:24:38 +00:00
let family_metadata: FamilyMetadata = serde_json::from_reader(family_metadata_file)
.context(format!(
"failed to parse `family.json` for '{theme_family_slug}'"
))?;
2023-11-02 22:00:55 +00:00
let mut themes = Vec::new();
2023-11-02 22:24:38 +00:00
for theme_metadata in family_metadata.themes {
log::info!("Converting '{}' theme", &theme_metadata.name);
2023-11-02 22:24:38 +00:00
let theme_file_path = theme_family_dir.path().join(&theme_metadata.file_name);
2023-11-06 17:43:10 +00:00
let theme_file = match File::open(&theme_file_path) {
Ok(file) => file,
Err(_) => {
log::info!("Failed to open file at path: {:?}", theme_file_path);
2023-11-06 17:43:10 +00:00
continue;
}
};
let theme_without_comments = StripComments::new(theme_file);
let vscode_theme: VsCodeTheme = serde_json::from_reader(theme_without_comments)
2023-11-02 22:00:55 +00:00
.context(format!("failed to parse theme {theme_file_path:?}"))?;
let converter = VsCodeThemeConverter::new(
vscode_theme,
theme_metadata,
family_metadata.syntax.clone(),
);
2023-11-02 22:24:38 +00:00
let theme = converter.convert()?;
2023-11-02 22:00:55 +00:00
themes.push(theme);
}
let theme_family = UserThemeFamily {
2023-11-02 22:00:55 +00:00
name: family_metadata.name.into(),
author: family_metadata.author.into(),
2023-11-02 22:24:38 +00:00
themes,
2023-11-02 22:00:55 +00:00
};
theme_families.push(theme_family);
}
2023-11-06 20:24:36 +00:00
let themes_output_path = PathBuf::from_str(OUT_PATH)?;
if !themes_output_path.exists() {
log::info!("Creating directory: {:?}", themes_output_path);
2023-11-06 20:24:36 +00:00
fs::create_dir_all(&themes_output_path)?;
}
let mut mod_rs_file = File::create(themes_output_path.join(format!("mod.rs")))?;
2023-11-02 23:14:51 +00:00
let mut theme_modules = Vec::new();
2023-11-02 22:57:13 +00:00
for theme_family in theme_families {
let theme_family_slug = any_ascii(&theme_family.name).to_case(Case::Snake);
2023-11-02 23:14:51 +00:00
let mut output_file =
File::create(themes_output_path.join(format!("{theme_family_slug}.rs")))?;
log::info!(
2023-11-06 17:43:10 +00:00
"Creating file: {:?}",
themes_output_path.join(format!("{theme_family_slug}.rs"))
);
2023-11-02 23:14:51 +00:00
let theme_module = format!(
r#"
// This file was generated by the `theme_importer`.
// Be careful when modifying it by hand.
2023-11-03 14:58:06 +00:00
use gpui::rgba;
2023-11-02 23:14:51 +00:00
#[allow(unused)]
2023-11-02 23:14:51 +00:00
use crate::{{
2023-11-09 17:59:20 +00:00
Appearance, StatusColorsRefinement, ThemeColorsRefinement, UserHighlightStyle, UserSyntaxTheme,
UserTheme, UserThemeFamily, UserThemeStylesRefinement, UserFontWeight, UserFontStyle
2023-11-02 23:14:51 +00:00
}};
pub fn {theme_family_slug}() -> UserThemeFamily {{
2023-11-02 23:14:51 +00:00
{theme_family_definition}
}}
"#,
theme_family_definition = format!("{:#?}", UserThemeFamilyPrinter::new(theme_family))
2023-11-02 23:14:51 +00:00
);
output_file.write_all(theme_module.as_bytes())?;
theme_modules.push(theme_family_slug);
2023-11-02 22:57:13 +00:00
}
let themes_vector_contents = format!(
r#"
use crate::UserThemeFamily;
pub(crate) fn all_user_themes() -> Vec<UserThemeFamily> {{
vec![{all_themes}]
}}
"#,
all_themes = theme_modules
.iter()
.map(|module| format!("{}()", module))
.collect::<Vec<_>>()
.join(", ")
2023-11-06 17:43:10 +00:00
);
2023-11-02 23:14:51 +00:00
let mod_rs_contents = format!(
r#"
// This file was generated by the `theme_importer`.
// Be careful when modifying it by hand.
2023-11-02 23:14:51 +00:00
{mod_statements}
{use_statements}
{themes_vector_contents}
2023-11-02 23:14:51 +00:00
"#,
mod_statements = theme_modules
.iter()
.map(|module| format!("mod {module};"))
.collect::<Vec<_>>()
.join("\n"),
use_statements = theme_modules
.iter()
.map(|module| format!("pub use {module}::*;"))
.collect::<Vec<_>>()
.join("\n"),
themes_vector_contents = themes_vector_contents
2023-11-02 23:14:51 +00:00
);
mod_rs_file.write_all(mod_rs_contents.as_bytes())?;
log::info!("Formatting themes...");
let format_result = format_themes_crate()
// We need to format a second time to catch all of the formatting issues.
.and_then(|_| format_themes_crate());
if let Err(err) = format_result {
log::error!("Failed to format themes: {}", err);
}
log::info!("Done!");
Ok(())
}
fn format_themes_crate() -> std::io::Result<std::process::Output> {
Command::new("cargo")
.args(["fmt", "--package", "theme2"])
.output()
}