diff --git a/Cargo.lock b/Cargo.lock index 9db01b9e07..270f2971a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8537,6 +8537,20 @@ dependencies = [ "util", ] +[[package]] +name = "theme_converter" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap 4.4.4", + "gpui2", + "log", + "rust-embed", + "serde", + "simplelog", + "theme2", +] + [[package]] name = "theme_selector" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 030c20e99c..bb863fed56 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,6 +84,7 @@ members = [ "crates/text", "crates/theme", "crates/theme2", + "crates/theme_converter", "crates/theme_selector", "crates/ui2", "crates/util", diff --git a/crates/theme_converter/Cargo.toml b/crates/theme_converter/Cargo.toml new file mode 100644 index 0000000000..2a36dfce67 --- /dev/null +++ b/crates/theme_converter/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "theme_converter" +version = "0.1.0" +edition = "2021" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow.workspace = true +clap = { version = "4.4", features = ["derive", "string"] } +gpui2 = { path = "../gpui2" } +log.workspace = true +rust-embed.workspace = true +serde.workspace = true +simplelog = "0.9" +theme2 = { path = "../theme2" } diff --git a/crates/theme_converter/src/main.rs b/crates/theme_converter/src/main.rs new file mode 100644 index 0000000000..86d41beacf --- /dev/null +++ b/crates/theme_converter/src/main.rs @@ -0,0 +1,205 @@ +use std::borrow::Cow; +use std::collections::HashMap; +use std::fmt; + +use anyhow::{anyhow, Context, Result}; +use clap::Parser; +use gpui2::Hsla; +use gpui2::{serde_json, AssetSource, SharedString}; +use log::LevelFilter; +use rust_embed::RustEmbed; +use serde::de::Visitor; +use serde::{Deserialize, Deserializer}; +use simplelog::SimpleLogger; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +struct Args { + /// The name of the theme to convert. + theme: String, +} + +fn main() -> Result<()> { + SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger"); + + let args = Args::parse(); + + let theme = load_theme(args.theme)?; + + Ok(()) +} + +#[derive(RustEmbed)] +#[folder = "../../assets"] +#[include = "fonts/**/*"] +#[include = "icons/**/*"] +#[include = "themes/**/*"] +#[include = "sounds/**/*"] +#[include = "*.md"] +#[exclude = "*.DS_Store"] +pub struct Assets; + +impl AssetSource for Assets { + fn load(&self, path: &str) -> Result> { + Self::get(path) + .map(|f| f.data) + .ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path)) + } + + fn list(&self, path: &str) -> Result> { + Ok(Self::iter() + .filter(|p| p.starts_with(path)) + .map(SharedString::from) + .collect()) + } +} + +fn convert_theme(theme: LegacyTheme) -> Result { + let theme = theme2::Theme { + + } +} + +#[derive(Deserialize)] +struct JsonTheme { + pub base_theme: serde_json::Value, +} + +/// Loads the [`Theme`] with the given name. +pub fn load_theme(name: String) -> Result { + let theme_contents = Assets::get(&format!("themes/{name}.json")) + .with_context(|| format!("theme file not found: '{name}'"))?; + + let json_theme: JsonTheme = serde_json::from_str(std::str::from_utf8(&theme_contents.data)?) + .context("failed to parse legacy theme")?; + + let legacy_theme: LegacyTheme = serde_json::from_value(json_theme.base_theme.clone()) + .context("failed to parse `base_theme`")?; + + Ok(legacy_theme) +} + +#[derive(Deserialize, Clone, Default, Debug)] +pub struct LegacyTheme { + pub name: String, + pub is_light: bool, + pub lowest: Layer, + pub middle: Layer, + pub highest: Layer, + pub popover_shadow: Shadow, + pub modal_shadow: Shadow, + #[serde(deserialize_with = "deserialize_player_colors")] + pub players: Vec, + #[serde(deserialize_with = "deserialize_syntax_colors")] + pub syntax: HashMap, +} + +#[derive(Deserialize, Clone, Default, Debug)] +pub struct Layer { + pub base: StyleSet, + pub variant: StyleSet, + pub on: StyleSet, + pub accent: StyleSet, + pub positive: StyleSet, + pub warning: StyleSet, + pub negative: StyleSet, +} + +#[derive(Deserialize, Clone, Default, Debug)] +pub struct StyleSet { + #[serde(rename = "default")] + pub default: ContainerColors, + pub hovered: ContainerColors, + pub pressed: ContainerColors, + pub active: ContainerColors, + pub disabled: ContainerColors, + pub inverted: ContainerColors, +} + +#[derive(Deserialize, Clone, Default, Debug)] +pub struct ContainerColors { + pub background: Hsla, + pub foreground: Hsla, + pub border: Hsla, +} + +#[derive(Deserialize, Clone, Default, Debug)] +pub struct PlayerColors { + pub selection: Hsla, + pub cursor: Hsla, +} + +#[derive(Deserialize, Clone, Default, Debug)] +pub struct Shadow { + pub blur: u8, + pub color: Hsla, + pub offset: Vec, +} + +fn deserialize_player_colors<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + struct PlayerArrayVisitor; + + impl<'de> Visitor<'de> for PlayerArrayVisitor { + type Value = Vec; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("an object with integer keys") + } + + fn visit_map>( + self, + mut map: A, + ) -> Result { + let mut players = Vec::with_capacity(8); + while let Some((key, value)) = map.next_entry::()? { + if key < 8 { + players.push(value); + } else { + return Err(serde::de::Error::invalid_value( + serde::de::Unexpected::Unsigned(key as u64), + &"a key in range 0..7", + )); + } + } + Ok(players) + } + } + + deserializer.deserialize_map(PlayerArrayVisitor) +} + +fn deserialize_syntax_colors<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + #[derive(Deserialize)] + struct ColorWrapper { + color: Hsla, + } + + struct SyntaxVisitor; + + impl<'de> Visitor<'de> for SyntaxVisitor { + type Value = HashMap; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a map with keys and objects with a single color field as values") + } + + fn visit_map(self, mut map: M) -> Result, M::Error> + where + M: serde::de::MapAccess<'de>, + { + let mut result = HashMap::new(); + while let Some(key) = map.next_key()? { + let wrapper: ColorWrapper = map.next_value()?; // Deserialize values as Hsla + result.insert(key, wrapper.color); + } + Ok(result) + } + } + deserializer.deserialize_map(SyntaxVisitor) +}