main.rs

  1mod theme_printer;
  2
  3use std::borrow::Cow;
  4use std::collections::HashMap;
  5use std::fmt::{self, Debug};
  6
  7use anyhow::{anyhow, Context, Result};
  8use clap::Parser;
  9use gpui2::{hsla, rgb, serde_json, AssetSource, Hsla, SharedString};
 10use log::LevelFilter;
 11use rust_embed::RustEmbed;
 12use serde::de::Visitor;
 13use serde::{Deserialize, Deserializer};
 14use simplelog::SimpleLogger;
 15use theme2::{PlayerTheme, SyntaxTheme};
 16
 17use crate::theme_printer::ThemePrinter;
 18
 19#[derive(Parser)]
 20#[command(author, version, about, long_about = None)]
 21struct Args {
 22    /// The name of the theme to convert.
 23    theme: String,
 24}
 25
 26fn main() -> Result<()> {
 27    SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
 28
 29    let args = Args::parse();
 30
 31    let (json_theme, legacy_theme) = load_theme(args.theme)?;
 32
 33    let theme = convert_theme(json_theme, legacy_theme)?;
 34
 35    println!("{:#?}", ThemePrinter::new(theme));
 36
 37    Ok(())
 38}
 39
 40#[derive(RustEmbed)]
 41#[folder = "../../assets"]
 42#[include = "fonts/**/*"]
 43#[include = "icons/**/*"]
 44#[include = "themes/**/*"]
 45#[include = "sounds/**/*"]
 46#[include = "*.md"]
 47#[exclude = "*.DS_Store"]
 48pub struct Assets;
 49
 50impl AssetSource for Assets {
 51    fn load(&self, path: &str) -> Result<Cow<[u8]>> {
 52        Self::get(path)
 53            .map(|f| f.data)
 54            .ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
 55    }
 56
 57    fn list(&self, path: &str) -> Result<Vec<SharedString>> {
 58        Ok(Self::iter()
 59            .filter(|p| p.starts_with(path))
 60            .map(SharedString::from)
 61            .collect())
 62    }
 63}
 64
 65#[derive(Clone, Copy)]
 66pub struct PlayerThemeColors {
 67    pub cursor: Hsla,
 68    pub selection: Hsla,
 69}
 70
 71impl PlayerThemeColors {
 72    pub fn new(theme: &LegacyTheme, ix: usize) -> Self {
 73        if ix < theme.players.len() {
 74            Self {
 75                cursor: theme.players[ix].cursor,
 76                selection: theme.players[ix].selection,
 77            }
 78        } else {
 79            Self {
 80                cursor: rgb::<Hsla>(0xff00ff),
 81                selection: rgb::<Hsla>(0xff00ff),
 82            }
 83        }
 84    }
 85}
 86
 87impl From<PlayerThemeColors> for PlayerTheme {
 88    fn from(value: PlayerThemeColors) -> Self {
 89        Self {
 90            cursor: value.cursor,
 91            selection: value.selection,
 92        }
 93    }
 94}
 95
 96fn convert_theme(json_theme: JsonTheme, legacy_theme: LegacyTheme) -> Result<theme2::Theme> {
 97    let transparent = hsla(0.0, 0.0, 0.0, 0.0);
 98
 99    let players: [PlayerTheme; 8] = [
100        PlayerThemeColors::new(&legacy_theme, 0).into(),
101        PlayerThemeColors::new(&legacy_theme, 1).into(),
102        PlayerThemeColors::new(&legacy_theme, 2).into(),
103        PlayerThemeColors::new(&legacy_theme, 3).into(),
104        PlayerThemeColors::new(&legacy_theme, 4).into(),
105        PlayerThemeColors::new(&legacy_theme, 5).into(),
106        PlayerThemeColors::new(&legacy_theme, 6).into(),
107        PlayerThemeColors::new(&legacy_theme, 7).into(),
108    ];
109
110    let theme = theme2::Theme {
111        metadata: theme2::ThemeMetadata {
112            name: legacy_theme.name.clone().into(),
113            is_light: legacy_theme.is_light,
114        },
115        transparent,
116        mac_os_traffic_light_red: rgb::<Hsla>(0xEC695E),
117        mac_os_traffic_light_yellow: rgb::<Hsla>(0xF4BF4F),
118        mac_os_traffic_light_green: rgb::<Hsla>(0x62C554),
119        border: legacy_theme.lowest.base.default.border,
120        border_variant: legacy_theme.lowest.variant.default.border,
121        border_focused: legacy_theme.lowest.accent.default.border,
122        border_transparent: transparent,
123        elevated_surface: legacy_theme.lowest.base.default.background,
124        surface: legacy_theme.middle.base.default.background,
125        background: legacy_theme.lowest.base.default.background,
126        filled_element: legacy_theme.lowest.base.default.background,
127        filled_element_hover: hsla(0.0, 0.0, 100.0, 0.12),
128        filled_element_active: hsla(0.0, 0.0, 100.0, 0.16),
129        filled_element_selected: legacy_theme.lowest.accent.default.background,
130        filled_element_disabled: transparent,
131        ghost_element: transparent,
132        ghost_element_hover: hsla(0.0, 0.0, 100.0, 0.08),
133        ghost_element_active: hsla(0.0, 0.0, 100.0, 0.12),
134        ghost_element_selected: legacy_theme.lowest.accent.default.background,
135        ghost_element_disabled: transparent,
136        text: legacy_theme.lowest.base.default.foreground,
137        text_muted: legacy_theme.lowest.variant.default.foreground,
138        /// TODO: map this to a real value
139        text_placeholder: legacy_theme.lowest.negative.default.foreground,
140        text_disabled: legacy_theme.lowest.base.disabled.foreground,
141        text_accent: legacy_theme.lowest.accent.default.foreground,
142        icon_muted: legacy_theme.lowest.variant.default.foreground,
143        syntax: SyntaxTheme {
144            highlights: json_theme
145                .editor
146                .syntax
147                .iter()
148                .map(|(token, style)| (token.clone(), style.color.clone().into()))
149                .collect(),
150        },
151        status_bar: legacy_theme.lowest.base.default.background,
152        title_bar: legacy_theme.lowest.base.default.background,
153        toolbar: legacy_theme.highest.base.default.background,
154        tab_bar: legacy_theme.middle.base.default.background,
155        editor: legacy_theme.highest.base.default.background,
156        editor_subheader: legacy_theme.middle.base.default.background,
157        terminal: legacy_theme.highest.base.default.background,
158        editor_active_line: legacy_theme.highest.on.default.background,
159        image_fallback_background: legacy_theme.lowest.base.default.background,
160
161        git_created: legacy_theme.lowest.positive.default.foreground,
162        git_modified: legacy_theme.lowest.accent.default.foreground,
163        git_deleted: legacy_theme.lowest.negative.default.foreground,
164        git_conflict: legacy_theme.lowest.warning.default.foreground,
165        git_ignored: legacy_theme.lowest.base.disabled.foreground,
166        git_renamed: legacy_theme.lowest.warning.default.foreground,
167
168        players,
169    };
170
171    Ok(theme)
172}
173
174#[derive(Deserialize)]
175struct JsonTheme {
176    pub editor: JsonEditorTheme,
177    pub base_theme: serde_json::Value,
178}
179
180#[derive(Deserialize)]
181struct JsonEditorTheme {
182    pub syntax: HashMap<String, JsonSyntaxStyle>,
183}
184
185#[derive(Deserialize)]
186struct JsonSyntaxStyle {
187    pub color: Hsla,
188}
189
190/// Loads the [`Theme`] with the given name.
191fn load_theme(name: String) -> Result<(JsonTheme, LegacyTheme)> {
192    let theme_contents = Assets::get(&format!("themes/{name}.json"))
193        .with_context(|| format!("theme file not found: '{name}'"))?;
194
195    let json_theme: JsonTheme = serde_json::from_str(std::str::from_utf8(&theme_contents.data)?)
196        .context("failed to parse legacy theme")?;
197
198    let legacy_theme: LegacyTheme = serde_json::from_value(json_theme.base_theme.clone())
199        .context("failed to parse `base_theme`")?;
200
201    Ok((json_theme, legacy_theme))
202}
203
204#[derive(Deserialize, Clone, Default, Debug)]
205pub struct LegacyTheme {
206    pub name: String,
207    pub is_light: bool,
208    pub lowest: Layer,
209    pub middle: Layer,
210    pub highest: Layer,
211    pub popover_shadow: Shadow,
212    pub modal_shadow: Shadow,
213    #[serde(deserialize_with = "deserialize_player_colors")]
214    pub players: Vec<PlayerColors>,
215    #[serde(deserialize_with = "deserialize_syntax_colors")]
216    pub syntax: HashMap<String, Hsla>,
217}
218
219#[derive(Deserialize, Clone, Default, Debug)]
220pub struct Layer {
221    pub base: StyleSet,
222    pub variant: StyleSet,
223    pub on: StyleSet,
224    pub accent: StyleSet,
225    pub positive: StyleSet,
226    pub warning: StyleSet,
227    pub negative: StyleSet,
228}
229
230#[derive(Deserialize, Clone, Default, Debug)]
231pub struct StyleSet {
232    #[serde(rename = "default")]
233    pub default: ContainerColors,
234    pub hovered: ContainerColors,
235    pub pressed: ContainerColors,
236    pub active: ContainerColors,
237    pub disabled: ContainerColors,
238    pub inverted: ContainerColors,
239}
240
241#[derive(Deserialize, Clone, Default, Debug)]
242pub struct ContainerColors {
243    pub background: Hsla,
244    pub foreground: Hsla,
245    pub border: Hsla,
246}
247
248#[derive(Deserialize, Clone, Default, Debug)]
249pub struct PlayerColors {
250    pub selection: Hsla,
251    pub cursor: Hsla,
252}
253
254#[derive(Deserialize, Clone, Default, Debug)]
255pub struct Shadow {
256    pub blur: u8,
257    pub color: Hsla,
258    pub offset: Vec<u8>,
259}
260
261fn deserialize_player_colors<'de, D>(deserializer: D) -> Result<Vec<PlayerColors>, D::Error>
262where
263    D: Deserializer<'de>,
264{
265    struct PlayerArrayVisitor;
266
267    impl<'de> Visitor<'de> for PlayerArrayVisitor {
268        type Value = Vec<PlayerColors>;
269
270        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
271            formatter.write_str("an object with integer keys")
272        }
273
274        fn visit_map<A: serde::de::MapAccess<'de>>(
275            self,
276            mut map: A,
277        ) -> Result<Self::Value, A::Error> {
278            let mut players = Vec::with_capacity(8);
279            while let Some((key, value)) = map.next_entry::<usize, PlayerColors>()? {
280                if key < 8 {
281                    players.push(value);
282                } else {
283                    return Err(serde::de::Error::invalid_value(
284                        serde::de::Unexpected::Unsigned(key as u64),
285                        &"a key in range 0..7",
286                    ));
287                }
288            }
289            Ok(players)
290        }
291    }
292
293    deserializer.deserialize_map(PlayerArrayVisitor)
294}
295
296fn deserialize_syntax_colors<'de, D>(deserializer: D) -> Result<HashMap<String, Hsla>, D::Error>
297where
298    D: serde::Deserializer<'de>,
299{
300    #[derive(Deserialize)]
301    struct ColorWrapper {
302        color: Hsla,
303    }
304
305    struct SyntaxVisitor;
306
307    impl<'de> Visitor<'de> for SyntaxVisitor {
308        type Value = HashMap<String, Hsla>;
309
310        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
311            formatter.write_str("a map with keys and objects with a single color field as values")
312        }
313
314        fn visit_map<M>(self, mut map: M) -> Result<HashMap<String, Hsla>, M::Error>
315        where
316            M: serde::de::MapAccess<'de>,
317        {
318            let mut result = HashMap::new();
319            while let Some(key) = map.next_key()? {
320                let wrapper: ColorWrapper = map.next_value()?; // Deserialize values as Hsla
321                result.insert(key, wrapper.color);
322            }
323            Ok(result)
324        }
325    }
326    deserializer.deserialize_map(SyntaxVisitor)
327}