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::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}