main.rs

  1mod color;
  2mod vscode;
  3
  4use std::fs::File;
  5use std::io::Write;
  6use std::path::PathBuf;
  7
  8use anyhow::{Context as _, Result};
  9use clap::Parser;
 10use indexmap::IndexMap;
 11use log::LevelFilter;
 12use serde::Deserialize;
 13use simplelog::ColorChoice;
 14use simplelog::{TermLogger, TerminalMode};
 15use theme::{Appearance, AppearanceContent};
 16
 17use crate::vscode::VsCodeTheme;
 18use crate::vscode::VsCodeThemeConverter;
 19
 20const ZED_THEME_SCHEMA_URL: &str = "https://zed.dev/schema/themes/v0.2.0.json";
 21
 22#[derive(Debug, Deserialize)]
 23struct FamilyMetadata {
 24    #[expect(
 25        unused,
 26        reason = "This field was found to be unused with serde library bump; it's left as is due to insufficient context on PO's side, but it *may* be fine to remove"
 27    )]
 28    pub name: String,
 29    #[expect(
 30        unused,
 31        reason = "This field was found to be unused with serde library bump; it's left as is due to insufficient context on PO's side, but it *may* be fine to remove"
 32    )]
 33    pub author: String,
 34    #[expect(
 35        unused,
 36        reason = "This field was found to be unused with serde library bump; it's left as is due to insufficient context on PO's side, but it *may* be fine to remove"
 37    )]
 38    pub themes: Vec<ThemeMetadata>,
 39
 40    /// Overrides for specific syntax tokens.
 41    ///
 42    /// Use this to ensure certain Zed syntax tokens are matched
 43    /// to an exact set of scopes when it is not otherwise possible
 44    /// to rely on the default mappings in the theme importer.
 45    #[serde(default)]
 46    #[expect(
 47        unused,
 48        reason = "This field was found to be unused with serde library bump; it's left as is due to insufficient context on PO's side, but it *may* be fine to remove"
 49    )]
 50    pub syntax: IndexMap<String, Vec<String>>,
 51}
 52
 53#[derive(Debug, Clone, Copy, Deserialize)]
 54#[serde(rename_all = "snake_case")]
 55pub enum ThemeAppearanceJson {
 56    Light,
 57    Dark,
 58}
 59
 60impl From<ThemeAppearanceJson> for AppearanceContent {
 61    fn from(value: ThemeAppearanceJson) -> Self {
 62        match value {
 63            ThemeAppearanceJson::Light => Self::Light,
 64            ThemeAppearanceJson::Dark => Self::Dark,
 65        }
 66    }
 67}
 68
 69impl From<ThemeAppearanceJson> for Appearance {
 70    fn from(value: ThemeAppearanceJson) -> Self {
 71        match value {
 72            ThemeAppearanceJson::Light => Self::Light,
 73            ThemeAppearanceJson::Dark => Self::Dark,
 74        }
 75    }
 76}
 77
 78#[derive(Debug, Deserialize)]
 79pub struct ThemeMetadata {
 80    pub name: String,
 81    pub file_name: String,
 82    pub appearance: ThemeAppearanceJson,
 83}
 84
 85#[derive(Parser)]
 86#[command(author, version, about, long_about = None)]
 87struct Args {
 88    /// The path to the theme to import.
 89    theme_path: PathBuf,
 90
 91    /// Whether to warn when values are missing from the theme.
 92    #[arg(long)]
 93    warn_on_missing: bool,
 94
 95    /// The path to write the output to.
 96    #[arg(long, short)]
 97    output: Option<PathBuf>,
 98}
 99
100fn main() -> Result<()> {
101    let args = Args::parse();
102
103    let log_config = {
104        let mut config = simplelog::ConfigBuilder::new();
105
106        if !args.warn_on_missing {
107            config.add_filter_ignore_str("theme_printer");
108        }
109
110        config.build()
111    };
112
113    TermLogger::init(
114        LevelFilter::Trace,
115        log_config,
116        TerminalMode::Stderr,
117        ColorChoice::Auto,
118    )
119    .expect("could not initialize logger");
120
121    let theme_file_path = args.theme_path;
122
123    let theme_file = match File::open(&theme_file_path) {
124        Ok(file) => file,
125        Err(err) => {
126            log::info!("Failed to open file at path: {:?}", theme_file_path);
127            return Err(err)?;
128        }
129    };
130
131    let vscode_theme: VsCodeTheme = serde_json_lenient::from_reader(theme_file)
132        .context(format!("failed to parse theme {theme_file_path:?}"))?;
133
134    let theme_metadata = ThemeMetadata {
135        name: vscode_theme.name.clone().unwrap_or("".to_string()),
136        appearance: ThemeAppearanceJson::Dark,
137        file_name: "".to_string(),
138    };
139
140    let converter = VsCodeThemeConverter::new(vscode_theme, theme_metadata, IndexMap::new());
141
142    let theme = converter.convert()?;
143    let mut theme = serde_json::to_value(theme).unwrap();
144    theme.as_object_mut().unwrap().insert(
145        "$schema".to_string(),
146        serde_json::Value::String(ZED_THEME_SCHEMA_URL.to_string()),
147    );
148    let theme_json = serde_json::to_string_pretty(&theme).unwrap();
149
150    if let Some(output) = args.output {
151        let mut file = File::create(output)?;
152        file.write_all(theme_json.as_bytes())?;
153    } else {
154        println!("{}", theme_json);
155    }
156
157    log::info!("Done!");
158
159    Ok(())
160}