Remove old themes using UserTheme constructs (#6897)

This PR removes the themes defined using the `UserTheme` types, as we're
now loading the themes via JSON.

The `theme_importer` has also been reworked to read in a VS Code theme
and output a new JSON theme.

Release Notes:

- N/A
This commit is contained in:
Marshall Bowers 2024-01-27 16:55:37 -05:00 committed by GitHub
parent 5f1dcb76fe
commit 9b453d2cbc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 142 additions and 23067 deletions

View file

@ -1,39 +1,22 @@
mod assets;
mod color;
mod theme_printer;
mod util;
mod vscode;
mod zed1;
use std::collections::HashMap;
use std::fs::{self, File};
use std::io::Write;
use std::fs::File;
use std::path::PathBuf;
use std::process::Command;
use std::str::FromStr;
use any_ascii::any_ascii;
use anyhow::{anyhow, Context, Result};
use anyhow::{Context, Result};
use clap::Parser;
use convert_case::{Case, Casing};
use gpui::Hsla;
use indexmap::IndexMap;
use indoc::formatdoc;
use json_comments::StripComments;
use log::LevelFilter;
use serde::Deserialize;
use simplelog::{TermLogger, TerminalMode};
use theme::{
Appearance, FontWeightContent, HighlightStyleContent, PlayerColorContent, StatusColorsContent,
ThemeColorsContent, ThemeContent, ThemeFamilyContent, ThemeStyleContent, UserTheme,
UserThemeFamily,
};
use theme::{Appearance, AppearanceContent};
use crate::theme_printer::UserThemeFamilyPrinter;
use crate::vscode::VsCodeTheme;
use crate::vscode::VsCodeThemeConverter;
use crate::zed1::theme::Theme as Zed1Theme;
use crate::zed1::{zed1_theme_licenses, Zed1ThemeConverter};
#[derive(Debug, Deserialize)]
struct FamilyMetadata {
@ -57,6 +40,15 @@ pub enum ThemeAppearanceJson {
Dark,
}
impl From<ThemeAppearanceJson> for AppearanceContent {
fn from(value: ThemeAppearanceJson) -> Self {
match value {
ThemeAppearanceJson::Light => Self::Light,
ThemeAppearanceJson::Dark => Self::Dark,
}
}
}
impl From<ThemeAppearanceJson> for Appearance {
fn from(value: ThemeAppearanceJson) -> Self {
match value {
@ -76,15 +68,15 @@ pub struct ThemeMetadata {
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Args {
/// The path to the theme to import.
theme_path: PathBuf,
/// 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/theme/src/themes";
let args = Args::parse();
let log_config = {
@ -105,606 +97,35 @@ fn main() -> Result<()> {
TermLogger::init(LevelFilter::Trace, log_config, TerminalMode::Mixed)
.expect("could not initialize logger");
if 1 < 2 {
let themes: Vec<UserThemeFamily> = Vec::new();
// Uncomment this line when you need to regenerate themes.
// let themes = theme::all_user_themes();
let theme_file_path = args.theme_path;
let mut families = Vec::new();
for family in themes {
families.push(convert_family(family));
let theme_file = match File::open(&theme_file_path) {
Ok(file) => file,
Err(err) => {
log::info!("Failed to open file at path: {:?}", theme_file_path);
return Err(err)?;
}
};
for family in families {
let theme_family_slug = any_ascii(&family.name)
.replace("(", "")
.replace(")", "")
.to_case(Case::Snake);
let theme_without_comments = StripComments::new(theme_file);
let vscode_theme: VsCodeTheme = serde_json::from_reader(theme_without_comments)
.context(format!("failed to parse theme {theme_file_path:?}"))?;
let output_dir = PathBuf::from("assets/themes/").join(&theme_family_slug);
let theme_metadata = ThemeMetadata {
name: "".to_string(),
appearance: ThemeAppearanceJson::Dark,
file_name: "".to_string(),
};
fs::create_dir_all(&output_dir)?;
let converter = VsCodeThemeConverter::new(vscode_theme, theme_metadata, IndexMap::new());
let mut output_file =
File::create(output_dir.join(format!("{theme_family_slug}.json")))?;
let theme = converter.convert()?;
let theme_json = serde_json::to_string_pretty(&family).unwrap();
let theme_json = serde_json::to_string_pretty(&theme).unwrap();
output_file.write_all(format!("{theme_json}\n").as_bytes())?;
}
return Ok(());
}
let mut theme_families = Vec::new();
/// Whether VS Code themes should be imported.
const IMPORT_VS_CODE_THEMES: bool = false;
if IMPORT_VS_CODE_THEMES {
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
)));
}
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() {
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;
}
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 {
log::info!("Converting '{}' theme", &theme_metadata.name);
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(_) => {
log::info!("Failed to open file at path: {:?}", theme_file_path);
continue;
}
};
let theme_without_comments = StripComments::new(theme_file);
let vscode_theme: VsCodeTheme = serde_json::from_reader(theme_without_comments)
.context(format!("failed to parse theme {theme_file_path:?}"))?;
let converter = VsCodeThemeConverter::new(
vscode_theme,
theme_metadata,
family_metadata.syntax.clone(),
);
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 zed1_themes_path = PathBuf::from_str("assets/themes")?;
let zed1_theme_families = [
"Andromeda",
"Atelier",
"Ayu",
"Gruvbox",
"One",
"Rosé Pine",
"Sandcastle",
"Solarized",
"Summercamp",
];
let zed1_licenses_by_theme: HashMap<String, zed1::Zed1ThemeLicense> = HashMap::from_iter(
zed1_theme_licenses()
.into_iter()
.map(|theme_license| (theme_license.theme.clone(), theme_license)),
);
let mut zed1_themes_by_family: IndexMap<String, Vec<UserTheme>> = IndexMap::from_iter(
zed1_theme_families
.into_iter()
.map(|family| (family.to_string(), Vec::new())),
);
for entry in fs::read_dir(&zed1_themes_path)? {
let entry = entry?;
if entry.file_type()?.is_dir() {
continue;
}
match entry.path().extension() {
None => continue,
Some(extension) => {
if extension != "json" {
continue;
}
}
}
let theme_file_path = entry.path();
let theme_file = match File::open(&theme_file_path) {
Ok(file) => file,
Err(_) => {
log::info!("Failed to open file at path: {:?}", theme_file_path);
continue;
}
};
let theme_without_comments = StripComments::new(theme_file);
let zed1_theme: Zed1Theme = serde_json::from_reader(theme_without_comments)
.context(format!("failed to parse theme {theme_file_path:?}"))?;
let theme_name = zed1_theme.meta.name.clone();
let converter = Zed1ThemeConverter::new(zed1_theme);
let theme = converter.convert()?;
let Some((_, themes_for_family)) = zed1_themes_by_family
.iter_mut()
.find(|(family, _)| theme_name.starts_with(*family))
else {
log::warn!("No theme family found for '{}'.", theme_name);
continue;
};
themes_for_family.push(theme);
}
zed1_themes_by_family.sort_keys();
let mut licenses = Vec::new();
for (family, themes) in zed1_themes_by_family {
let mut theme_family = UserThemeFamily {
name: family,
author: "Zed Industries".to_string(),
themes,
};
theme_family
.themes
.sort_unstable_by_key(|theme| theme.name.clone());
for theme in &theme_family.themes {
let license = zed1_licenses_by_theme
.get(&theme.name)
.ok_or_else(|| anyhow!("missing license for theme: '{}'", theme.name))?;
let license_header = match license.license_url.as_ref() {
Some(license_url) => {
format!("[{theme_name}]({license_url})", theme_name = theme.name)
}
None => theme.name.clone(),
};
licenses.push(formatdoc!(
"
## {license_header}
{license_text}
********************************************************************************
",
license_text = license.license_text
));
}
theme_families.push(theme_family);
}
let themes_output_path = PathBuf::from_str(OUT_PATH)?;
if !themes_output_path.exists() {
log::info!("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 = any_ascii(&theme_family.name)
.replace("(", "")
.replace(")", "")
.to_case(Case::Snake);
let mut output_file =
File::create(themes_output_path.join(format!("{theme_family_slug}.rs")))?;
log::info!(
"Creating file: {:?}",
themes_output_path.join(format!("{theme_family_slug}.rs"))
);
let theme_module = format!(
r#"
// This file was generated by the `theme_importer`.
// Be careful when modifying it by hand.
use gpui::rgba;
#[allow(unused)]
use crate::{{
Appearance, PlayerColor, PlayerColors, StatusColorsRefinement, ThemeColorsRefinement,
UserHighlightStyle, UserSyntaxTheme, UserTheme, UserThemeFamily, UserThemeStylesRefinement,
UserFontWeight, UserFontStyle
}};
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);
}
theme_modules.sort();
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(", ")
);
let mod_rs_contents = format!(
r#"
// This file was generated by the `theme_importer`.
// Be careful when modifying it by hand.
{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())?;
log::info!("Writing LICENSES file...");
let mut licenses_file = File::create(themes_output_path.join(format!("LICENSES")))?;
licenses_file.write_all(licenses.join("\n").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);
}
println!("{}", theme_json);
log::info!("Done!");
Ok(())
}
fn format_themes_crate() -> std::io::Result<std::process::Output> {
Command::new("cargo")
.args(["fmt", "--package", "theme"])
.output()
}
fn convert_family(family: UserThemeFamily) -> ThemeFamilyContent {
ThemeFamilyContent {
name: family.name,
author: family.author,
themes: family.themes.into_iter().map(convert_theme).collect(),
}
}
fn convert_theme(theme: UserTheme) -> ThemeContent {
ThemeContent {
name: theme.name,
appearance: match theme.appearance {
Appearance::Light => theme::AppearanceContent::Light,
Appearance::Dark => theme::AppearanceContent::Dark,
},
style: convert_theme_styles(theme.styles),
}
}
fn serialize_color(color: Hsla) -> String {
let rgba = color.to_rgb();
format!("#{:08x}", u32::from(rgba))
}
fn convert_theme_styles(styles: theme::UserThemeStylesRefinement) -> ThemeStyleContent {
ThemeStyleContent {
colors: ThemeColorsContent {
border: styles.colors.border.map(serialize_color),
border_variant: styles.colors.border_variant.map(serialize_color),
border_focused: styles.colors.border_focused.map(serialize_color),
border_selected: styles.colors.border_selected.map(serialize_color),
border_transparent: styles.colors.border_transparent.map(serialize_color),
border_disabled: styles.colors.border_disabled.map(serialize_color),
elevated_surface_background: styles
.colors
.elevated_surface_background
.map(serialize_color),
surface_background: styles.colors.surface_background.map(serialize_color),
background: styles.colors.background.map(serialize_color),
element_background: styles.colors.element_background.map(serialize_color),
element_hover: styles.colors.element_hover.map(serialize_color),
element_active: styles.colors.element_active.map(serialize_color),
element_selected: styles.colors.element_selected.map(serialize_color),
element_disabled: styles.colors.element_disabled.map(serialize_color),
drop_target_background: styles.colors.drop_target_background.map(serialize_color),
ghost_element_background: styles.colors.ghost_element_background.map(serialize_color),
ghost_element_hover: styles.colors.ghost_element_hover.map(serialize_color),
ghost_element_active: styles.colors.ghost_element_active.map(serialize_color),
ghost_element_selected: styles.colors.ghost_element_selected.map(serialize_color),
ghost_element_disabled: styles.colors.ghost_element_disabled.map(serialize_color),
text: styles.colors.text.map(serialize_color),
text_muted: styles.colors.text_muted.map(serialize_color),
text_placeholder: styles.colors.text_placeholder.map(serialize_color),
text_disabled: styles.colors.text_disabled.map(serialize_color),
text_accent: styles.colors.text_accent.map(serialize_color),
icon: styles.colors.icon.map(serialize_color),
icon_muted: styles.colors.icon_muted.map(serialize_color),
icon_disabled: styles.colors.icon_disabled.map(serialize_color),
icon_placeholder: styles.colors.icon_placeholder.map(serialize_color),
icon_accent: styles.colors.icon_accent.map(serialize_color),
status_bar_background: styles.colors.status_bar_background.map(serialize_color),
title_bar_background: styles.colors.title_bar_background.map(serialize_color),
toolbar_background: styles.colors.toolbar_background.map(serialize_color),
tab_bar_background: styles.colors.tab_bar_background.map(serialize_color),
tab_inactive_background: styles.colors.tab_inactive_background.map(serialize_color),
tab_active_background: styles.colors.tab_active_background.map(serialize_color),
search_match_background: styles.colors.search_match_background.map(serialize_color),
panel_background: styles.colors.panel_background.map(serialize_color),
panel_focused_border: styles.colors.panel_focused_border.map(serialize_color),
pane_focused_border: styles.colors.pane_focused_border.map(serialize_color),
scrollbar_thumb_background: styles
.colors
.scrollbar_thumb_background
.map(serialize_color),
scrollbar_thumb_hover_background: styles
.colors
.scrollbar_thumb_hover_background
.map(serialize_color),
scrollbar_thumb_border: styles.colors.scrollbar_thumb_border.map(serialize_color),
scrollbar_track_background: styles
.colors
.scrollbar_track_background
.map(serialize_color),
scrollbar_track_border: styles.colors.scrollbar_track_border.map(serialize_color),
editor_foreground: styles.colors.editor_foreground.map(serialize_color),
editor_background: styles.colors.editor_background.map(serialize_color),
editor_gutter_background: styles.colors.editor_gutter_background.map(serialize_color),
editor_subheader_background: styles
.colors
.editor_subheader_background
.map(serialize_color),
editor_active_line_background: styles
.colors
.editor_active_line_background
.map(serialize_color),
editor_highlighted_line_background: styles
.colors
.editor_highlighted_line_background
.map(serialize_color),
editor_line_number: styles.colors.editor_line_number.map(serialize_color),
editor_active_line_number: styles.colors.editor_active_line_number.map(serialize_color),
editor_invisible: styles.colors.editor_invisible.map(serialize_color),
editor_wrap_guide: styles.colors.editor_wrap_guide.map(serialize_color),
editor_active_wrap_guide: styles.colors.editor_active_wrap_guide.map(serialize_color),
editor_document_highlight_read_background: styles
.colors
.editor_document_highlight_read_background
.map(serialize_color),
editor_document_highlight_write_background: styles
.colors
.editor_document_highlight_write_background
.map(serialize_color),
terminal_background: styles.colors.terminal_background.map(serialize_color),
terminal_foreground: styles.colors.terminal_foreground.map(serialize_color),
terminal_bright_foreground: styles
.colors
.terminal_bright_foreground
.map(serialize_color),
terminal_dim_foreground: styles.colors.terminal_dim_foreground.map(serialize_color),
terminal_ansi_black: styles.colors.terminal_ansi_black.map(serialize_color),
terminal_ansi_bright_black: styles
.colors
.terminal_ansi_bright_black
.map(serialize_color),
terminal_ansi_dim_black: styles.colors.terminal_ansi_dim_black.map(serialize_color),
terminal_ansi_red: styles.colors.terminal_ansi_red.map(serialize_color),
terminal_ansi_bright_red: styles.colors.terminal_ansi_bright_red.map(serialize_color),
terminal_ansi_dim_red: styles.colors.terminal_ansi_dim_red.map(serialize_color),
terminal_ansi_green: styles.colors.terminal_ansi_green.map(serialize_color),
terminal_ansi_bright_green: styles
.colors
.terminal_ansi_bright_green
.map(serialize_color),
terminal_ansi_dim_green: styles.colors.terminal_ansi_dim_green.map(serialize_color),
terminal_ansi_yellow: styles.colors.terminal_ansi_yellow.map(serialize_color),
terminal_ansi_bright_yellow: styles
.colors
.terminal_ansi_bright_yellow
.map(serialize_color),
terminal_ansi_dim_yellow: styles.colors.terminal_ansi_dim_yellow.map(serialize_color),
terminal_ansi_blue: styles.colors.terminal_ansi_blue.map(serialize_color),
terminal_ansi_bright_blue: styles.colors.terminal_ansi_bright_blue.map(serialize_color),
terminal_ansi_dim_blue: styles.colors.terminal_ansi_dim_blue.map(serialize_color),
terminal_ansi_magenta: styles.colors.terminal_ansi_magenta.map(serialize_color),
terminal_ansi_bright_magenta: styles
.colors
.terminal_ansi_bright_magenta
.map(serialize_color),
terminal_ansi_dim_magenta: styles.colors.terminal_ansi_dim_magenta.map(serialize_color),
terminal_ansi_cyan: styles.colors.terminal_ansi_cyan.map(serialize_color),
terminal_ansi_bright_cyan: styles.colors.terminal_ansi_bright_cyan.map(serialize_color),
terminal_ansi_dim_cyan: styles.colors.terminal_ansi_dim_cyan.map(serialize_color),
terminal_ansi_white: styles.colors.terminal_ansi_white.map(serialize_color),
terminal_ansi_bright_white: styles
.colors
.terminal_ansi_bright_white
.map(serialize_color),
terminal_ansi_dim_white: styles.colors.terminal_ansi_dim_white.map(serialize_color),
link_text_hover: styles.colors.link_text_hover.map(serialize_color),
},
status: StatusColorsContent {
conflict: styles.status.conflict.map(serialize_color),
conflict_background: styles.status.conflict_background.map(serialize_color),
conflict_border: styles.status.conflict_border.map(serialize_color),
created: styles.status.created.map(serialize_color),
created_background: styles.status.created_background.map(serialize_color),
created_border: styles.status.created_border.map(serialize_color),
deleted: styles.status.deleted.map(serialize_color),
deleted_background: styles.status.deleted_background.map(serialize_color),
deleted_border: styles.status.deleted_border.map(serialize_color),
error: styles.status.error.map(serialize_color),
error_background: styles.status.error_background.map(serialize_color),
error_border: styles.status.error_border.map(serialize_color),
hidden: styles.status.hidden.map(serialize_color),
hidden_background: styles.status.hidden_background.map(serialize_color),
hidden_border: styles.status.hidden_border.map(serialize_color),
hint: styles.status.hint.map(serialize_color),
hint_background: styles.status.hint_background.map(serialize_color),
hint_border: styles.status.hint_border.map(serialize_color),
ignored: styles.status.ignored.map(serialize_color),
ignored_background: styles.status.ignored_background.map(serialize_color),
ignored_border: styles.status.ignored_border.map(serialize_color),
info: styles.status.info.map(serialize_color),
info_background: styles.status.info_background.map(serialize_color),
info_border: styles.status.info_border.map(serialize_color),
modified: styles.status.modified.map(serialize_color),
modified_background: styles.status.modified_background.map(serialize_color),
modified_border: styles.status.modified_border.map(serialize_color),
predictive: styles.status.predictive.map(serialize_color),
predictive_background: styles.status.predictive_background.map(serialize_color),
predictive_border: styles.status.predictive_border.map(serialize_color),
renamed: styles.status.renamed.map(serialize_color),
renamed_background: styles.status.renamed_background.map(serialize_color),
renamed_border: styles.status.renamed_border.map(serialize_color),
success: styles.status.success.map(serialize_color),
success_background: styles.status.success_background.map(serialize_color),
success_border: styles.status.success_border.map(serialize_color),
unreachable: styles.status.unreachable.map(serialize_color),
unreachable_background: styles.status.unreachable_background.map(serialize_color),
unreachable_border: styles.status.unreachable_border.map(serialize_color),
warning: styles.status.warning.map(serialize_color),
warning_background: styles.status.warning_background.map(serialize_color),
warning_border: styles.status.warning_border.map(serialize_color),
},
players: styles
.player
.map(|players| {
players
.0
.into_iter()
.map(|player_color| PlayerColorContent {
cursor: Some(player_color.cursor).map(serialize_color),
background: Some(player_color.background).map(serialize_color),
selection: Some(player_color.selection).map(serialize_color),
})
.collect()
})
.unwrap_or_default(),
syntax: styles
.syntax
.map(|syntax| {
IndexMap::from_iter(syntax.highlights.into_iter().map(|(name, style)| {
(
name,
HighlightStyleContent {
color: style.color.map(serialize_color),
font_style: style.font_style.map(|font_style| match font_style {
theme::UserFontStyle::Normal => theme::FontStyleContent::Normal,
theme::UserFontStyle::Italic => theme::FontStyleContent::Italic,
theme::UserFontStyle::Oblique => theme::FontStyleContent::Oblique,
}),
font_weight: style.font_weight.map(|font_weight| match font_weight.0 {
_ if font_weight.0 == 100.0 => FontWeightContent::Thin,
_ if font_weight.0 == 200.0 => FontWeightContent::ExtraLight,
_ if font_weight.0 == 300.0 => FontWeightContent::Light,
_ if font_weight.0 == 400.0 => FontWeightContent::Normal,
_ if font_weight.0 == 500.0 => FontWeightContent::Medium,
_ if font_weight.0 == 600.0 => FontWeightContent::Semibold,
_ if font_weight.0 == 700.0 => FontWeightContent::Bold,
_ if font_weight.0 == 800.0 => FontWeightContent::ExtraBold,
_ if font_weight.0 == 900.0 => FontWeightContent::Black,
_ => unreachable!(),
}),
},
)
}))
})
.unwrap_or_default(),
}
}