1mod alacritty;
2mod registry;
3
4use chromasync_types::{
5 GeneratedArtifact, GenerationContext, HexColor, RenderTarget, SemanticTokens,
6};
7use thiserror::Error;
8
9pub use crate::registry::{
10 ArtifactSpec, CompiledTarget, ListedTarget, OutputRegistry, RendererRegistry, TargetRegistry,
11 TargetSource, TargetSpec, parse_target_file, user_targets_dir,
12};
13
14pub const BUILTIN_TARGETS: [RenderTarget; 17] = [
15 RenderTarget::Alacritty,
16 RenderTarget::Chromium,
17 RenderTarget::Ghostty,
18 RenderTarget::GoogleChrome,
19 RenderTarget::Gtk3,
20 RenderTarget::Gtk4,
21 RenderTarget::HeliumBrowser,
22 RenderTarget::Hyprland,
23 RenderTarget::HyprlandLua,
24 RenderTarget::KColorScheme,
25 RenderTarget::Kitty,
26 RenderTarget::Micro,
27 RenderTarget::Qt5,
28 RenderTarget::Qt6,
29 RenderTarget::VsCode,
30 RenderTarget::VsCodeInsiders,
31 RenderTarget::Zed,
32];
33
34pub(crate) const BUILTIN_DECLARATIVE_TARGETS: [(&str, &str, &str); 16] = [
35 (
36 "chromium",
37 "chromium.toml",
38 include_str!("../builtin-targets/chromium.toml"),
39 ),
40 (
41 "ghostty",
42 "ghostty.toml",
43 include_str!("../builtin-targets/ghostty.toml"),
44 ),
45 (
46 "google-chrome",
47 "google-chrome.toml",
48 include_str!("../builtin-targets/google-chrome.toml"),
49 ),
50 (
51 "gtk3",
52 "gtk3.toml",
53 include_str!("../builtin-targets/gtk3.toml"),
54 ),
55 (
56 "gtk4",
57 "gtk4.toml",
58 include_str!("../builtin-targets/gtk4.toml"),
59 ),
60 (
61 "helium-browser",
62 "helium-browser.toml",
63 include_str!("../builtin-targets/helium-browser.toml"),
64 ),
65 (
66 "hyprland",
67 "hyprland.toml",
68 include_str!("../builtin-targets/hyprland.toml"),
69 ),
70 (
71 "hyprland-lua",
72 "hyprland-lua.toml",
73 include_str!("../builtin-targets/hyprland-lua.toml"),
74 ),
75 (
76 "kcolorscheme",
77 "kcolorscheme.toml",
78 include_str!("../builtin-targets/kcolorscheme.toml"),
79 ),
80 (
81 "kitty",
82 "kitty.toml",
83 include_str!("../builtin-targets/kitty.toml"),
84 ),
85 (
86 "micro",
87 "micro.toml",
88 include_str!("../builtin-targets/micro.toml"),
89 ),
90 (
91 "qt5",
92 "qt5.toml",
93 include_str!("../builtin-targets/qt5.toml"),
94 ),
95 (
96 "qt6",
97 "qt6.toml",
98 include_str!("../builtin-targets/qt6.toml"),
99 ),
100 (
101 "vscode",
102 "vscode.toml",
103 include_str!("../builtin-targets/vscode.toml"),
104 ),
105 (
106 "vscode-insiders",
107 "vscode-insiders.toml",
108 include_str!("../builtin-targets/vscode-insiders.toml"),
109 ),
110 (
111 "zed",
112 "zed.toml",
113 include_str!("../builtin-targets/zed.toml"),
114 ),
115];
116
117pub trait ArtifactGenerator: Send + Sync {
118 fn name(&self) -> &str;
119 fn preferred_template(&self) -> Option<&str> {
120 None
121 }
122 fn chroma_strategy(&self) -> Option<chromasync_types::ChromaStrategy> {
123 None
124 }
125 fn generate(
126 &self,
127 tokens: &SemanticTokens,
128 ctx: &GenerationContext,
129 ) -> Result<Vec<GeneratedArtifact>, RendererError>;
130}
131
132pub trait Renderer: Send + Sync {
133 fn target(&self) -> RenderTarget;
134 fn name(&self) -> &'static str;
135
136 fn file_name(&self) -> &'static str {
137 self.target().file_name()
138 }
139
140 fn render_artifact(&self, tokens: &SemanticTokens) -> Result<GeneratedArtifact, RendererError>;
141}
142
143impl<T> ArtifactGenerator for T
144where
145 T: Renderer,
146{
147 fn name(&self) -> &str {
148 Renderer::name(self)
149 }
150
151 fn generate(
152 &self,
153 tokens: &SemanticTokens,
154 _ctx: &GenerationContext,
155 ) -> Result<Vec<GeneratedArtifact>, RendererError> {
156 Ok(vec![self.render_artifact(tokens)?])
157 }
158}
159
160#[derive(Debug, Error)]
161pub enum RendererError {
162 #[error("renderer '{target}' is not implemented yet")]
163 UnsupportedTarget { target: RenderTarget },
164 #[error(
165 "target '{target}' generates {count} artifacts; use render_targets for multi-artifact targets"
166 )]
167 MultiArtifactTarget { target: RenderTarget, count: usize },
168 #[error("target '{requested}' was not found")]
169 UnknownTarget { requested: String },
170 #[error("color '{value}' must use the #RRGGBB format")]
171 InvalidColorFormat { value: String },
172 #[error("color '{value}' contains invalid hexadecimal digits")]
173 InvalidHexDigits { value: String },
174 #[error("failed to read targets directory '{path}': {source}")]
175 ReadTargetsDir {
176 path: std::path::PathBuf,
177 #[source]
178 source: std::io::Error,
179 },
180 #[error("failed to read target file '{path}': {source}")]
181 ReadTargetFile {
182 path: std::path::PathBuf,
183 #[source]
184 source: std::io::Error,
185 },
186 #[error("failed to parse target '{name}' from {source}: {error}")]
187 ParseTarget {
188 name: String,
189 source: String,
190 #[source]
191 error: Box<toml::de::Error>,
192 },
193 #[error("target '{name}' uses an invalid name: {reason}")]
194 InvalidTargetName { name: String, reason: String },
195 #[error("target '{target}' has no artifacts after inheritance is resolved")]
196 TargetHasNoArtifacts { target: String },
197 #[error("target '{target}' artifact '{file_name}' is invalid: {reason}")]
198 InvalidArtifactFileName {
199 target: String,
200 file_name: String,
201 reason: String,
202 },
203 #[error("target '{target}' artifact '{file_name}' uses an invalid placeholder '{placeholder}'")]
204 InvalidPlaceholder {
205 target: String,
206 file_name: String,
207 placeholder: String,
208 },
209 #[error("target '{target}' artifact '{file_name}' contains an unterminated placeholder")]
210 UnterminatedPlaceholder { target: String, file_name: String },
211 #[error("target '{target}' references unknown base target '{base}'")]
212 UnknownBaseTarget { target: String, base: String },
213 #[error("target '{target}' cannot inherit from built-in renderer '{base}'")]
214 BuiltInTargetInheritance { target: String, base: String },
215 #[error("target inheritance cycle detected: {cycle}")]
216 TargetInheritanceCycle { cycle: String },
217 #[error("user target '{name}' collides with a built-in renderer name")]
218 TargetNameCollidesWithBuiltIn { name: String },
219 #[error(
220 "user target '{name}' is defined multiple times: first at {first_source}, second at {second_source}"
221 )]
222 DuplicateTargetName {
223 name: String,
224 first_source: String,
225 second_source: String,
226 },
227}
228
229pub fn built_in_targets() -> &'static [RenderTarget] {
230 &BUILTIN_TARGETS
231}
232
233pub fn render_target(
234 target: RenderTarget,
235 tokens: &SemanticTokens,
236) -> Result<GeneratedArtifact, RendererError> {
237 let registry = RendererRegistry::new();
238 let generator = registry
239 .get(target.as_str())
240 .ok_or(RendererError::UnsupportedTarget { target })?;
241 let context = GenerationContext::default();
242 let mut artifacts = generator.generate(tokens, &context)?;
243
244 if artifacts.len() > 1 {
245 return Err(RendererError::MultiArtifactTarget {
246 target,
247 count: artifacts.len(),
248 });
249 }
250
251 artifacts
252 .pop()
253 .ok_or(RendererError::UnsupportedTarget { target })
254}
255
256pub fn render_targets(
257 targets: &[RenderTarget],
258 tokens: &SemanticTokens,
259) -> Result<Vec<GeneratedArtifact>, RendererError> {
260 let registry = RendererRegistry::new();
261 let requested = if targets.is_empty() {
262 registry.built_in_names()
263 } else {
264 let mut seen = std::collections::BTreeSet::new();
265 let mut names = Vec::with_capacity(targets.len());
266
267 for target in targets {
268 let name = target.as_str().to_owned();
269
270 if seen.insert(name.clone()) {
271 names.push(name);
272 }
273 }
274
275 names
276 };
277
278 registry::OutputRegistry::default().generate(&requested, tokens, &GenerationContext::default())
279}
280
281fn parse_hex_color(value: &str) -> Result<[u8; 3], RendererError> {
282 let normalized = value.strip_prefix('#').unwrap_or(value);
283
284 if normalized.len() != 6 {
285 return Err(RendererError::InvalidColorFormat {
286 value: value.to_owned(),
287 });
288 }
289
290 let red =
291 u8::from_str_radix(&normalized[0..2], 16).map_err(|_| RendererError::InvalidHexDigits {
292 value: value.to_owned(),
293 })?;
294 let green =
295 u8::from_str_radix(&normalized[2..4], 16).map_err(|_| RendererError::InvalidHexDigits {
296 value: value.to_owned(),
297 })?;
298 let blue =
299 u8::from_str_radix(&normalized[4..6], 16).map_err(|_| RendererError::InvalidHexDigits {
300 value: value.to_owned(),
301 })?;
302
303 Ok([red, green, blue])
304}
305
306fn hyprland_rgba(value: &str, alpha: u8) -> Result<String, RendererError> {
307 let [red, green, blue] = parse_hex_color(value)?;
308
309 Ok(format!("rgba({red:02X}{green:02X}{blue:02X}{alpha:02X})"))
310}
311
312fn normalized_hex(value: &str) -> Result<String, RendererError> {
313 let [red, green, blue] = parse_hex_color(value)?;
314
315 Ok(format!("#{red:02X}{green:02X}{blue:02X}"))
316}
317
318fn normalized_hex_without_hash(value: &str) -> Result<String, RendererError> {
319 let [red, green, blue] = parse_hex_color(value)?;
320
321 Ok(format!("{red:02x}{green:02x}{blue:02x}"))
322}
323
324fn rgb_components(value: &str) -> Result<String, RendererError> {
325 let [red, green, blue] = parse_hex_color(value)?;
326
327 Ok(format!("{red}, {green}, {blue}"))
328}
329
330fn terminal_ansi_colors(tokens: &SemanticTokens) -> [HexColor; 16] {
331 [
332 tokens.bg_secondary.clone(),
333 tokens.error.clone(),
334 tokens.success.clone(),
335 tokens.warning.clone(),
336 tokens.link.clone(),
337 tokens.accent.clone(),
338 tokens.selection.clone(),
339 tokens.border_strong.clone(),
340 tokens.text_muted.clone(),
341 tokens.error.clone(),
342 tokens.success.clone(),
343 tokens.warning.clone(),
344 tokens.accent_hover.clone(),
345 tokens.accent_active.clone(),
346 tokens.border_strong.clone(),
347 tokens.text.clone(),
348 ]
349}