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}