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