Files
oak-gpui/crates/theme_importer/src/main.rs
T
Marshall Bowers 0d95410634 Rework third-party themes (#3254)
This PR reworks the way we define our third-party themes to make them
work as overlays on top of a base theme.

We introduce the concept of a `UserThemeFamily` that contains
`UserTheme`s. Rather than being an entire theme definition on their own,
a `UserTheme` just contains optional overrides for the values in a
`Theme`.

When resolving a `UserTheme`, we apply it on top of the base theme. Any
values not overridden in the `UserTheme` will fall back to the `Theme`
defaults.

Right now we are just using `UserTheme` to model third-party themes that
we distribute with the Zed binary. However, this same structure can also
be used to import arbitrary user themes (such as from a theme registry,
or even a theme blob from the settings file).

Release Notes:

- N/A
2023-11-07 11:40:07 -05:00

213 lines
6.1 KiB
Rust

mod theme_printer;
mod util;
mod vscode;
use std::fs::{self, File};
use std::io::Write;
use std::path::PathBuf;
use std::str::FromStr;
use anyhow::{anyhow, Context, Result};
use convert_case::{Case, Casing};
use gpui::serde_json;
use log::LevelFilter;
use serde::Deserialize;
use simplelog::SimpleLogger;
use theme::{Appearance, UserThemeFamily};
use vscode::VsCodeThemeConverter;
use crate::theme_printer::UserThemeFamilyPrinter;
use crate::vscode::VsCodeTheme;
#[derive(Debug, Deserialize)]
struct FamilyMetadata {
pub name: String,
pub author: String,
pub themes: Vec<ThemeMetadata>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ThemeAppearanceJson {
Light,
Dark,
}
impl From<ThemeAppearanceJson> for Appearance {
fn from(value: ThemeAppearanceJson) -> Self {
match value {
ThemeAppearanceJson::Light => Self::Light,
ThemeAppearanceJson::Dark => Self::Dark,
}
}
}
#[derive(Debug, Deserialize)]
pub struct ThemeMetadata {
pub name: String,
pub file_name: String,
pub appearance: ThemeAppearanceJson,
}
fn main() -> Result<()> {
const SOURCE_PATH: &str = "assets/themes/src/vscode";
const OUT_PATH: &str = "crates/theme2/src/themes";
SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
println!("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
)));
}
let mut theme_families = Vec::new();
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;
}
let theme_family_slug = theme_family_dir
.path()
.file_stem()
.ok_or(anyhow!("no file stem"))
.map(|stem| stem.to_string_lossy().to_string())?;
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() {
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);
continue;
}
let family_metadata: FamilyMetadata = serde_json::from_reader(family_metadata_file)
.context(format!(
"failed to parse `family.json` for '{theme_family_slug}'"
))?;
let mut themes = Vec::new();
for theme_metadata in family_metadata.themes {
let theme_file_path = theme_family_dir.path().join(&theme_metadata.file_name);
let theme_file = match File::open(&theme_file_path) {
Ok(file) => file,
Err(_) => {
println!("Failed to open file at path: {:?}", theme_file_path);
continue;
}
};
let vscode_theme: VsCodeTheme = serde_json::from_reader(theme_file)
.context(format!("failed to parse theme {theme_file_path:?}"))?;
let converter = VsCodeThemeConverter::new(vscode_theme, theme_metadata);
let theme = converter.convert()?;
themes.push(theme);
}
let theme_family = UserThemeFamily {
name: family_metadata.name.into(),
author: family_metadata.author.into(),
themes,
};
theme_families.push(theme_family);
}
let themes_output_path = PathBuf::from_str(OUT_PATH)?;
if !themes_output_path.exists() {
println!("Creating directory: {:?}", themes_output_path);
fs::create_dir_all(&themes_output_path)?;
}
let mut mod_rs_file = File::create(themes_output_path.join(format!("mod.rs")))?;
let mut theme_modules = Vec::new();
for theme_family in theme_families {
let theme_family_slug = theme_family.name.to_string().to_case(Case::Snake);
let mut output_file =
File::create(themes_output_path.join(format!("{theme_family_slug}.rs")))?;
println!(
"Creating file: {:?}",
themes_output_path.join(format!("{theme_family_slug}.rs"))
);
let theme_module = format!(
r#"
use gpui::rgba;
use crate::{{
Appearance, ThemeColorsRefinement, UserTheme, UserThemeFamily, UserThemeStylesRefinement,
}};
pub fn {theme_family_slug}() -> UserThemeFamily {{
{theme_family_definition}
}}
"#,
theme_family_definition = format!("{:#?}", UserThemeFamilyPrinter::new(theme_family))
);
output_file.write_all(theme_module.as_bytes())?;
theme_modules.push(theme_family_slug);
}
let themes_vector_contents = format!(
r#"
use crate::UserThemeFamily;
pub(crate) fn all_imported_themes() -> Vec<UserThemeFamily> {{
vec![{all_themes}]
}}
"#,
all_themes = theme_modules
.iter()
.map(|module| format!("{}()", module))
.collect::<Vec<_>>()
.join(", ")
);
let mod_rs_contents = format!(
r#"
{mod_statements}
{use_statements}
{themes_vector_contents}
"#,
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
);
mod_rs_file.write_all(mod_rs_contents.as_bytes())?;
Ok(())
}