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