main.rs

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