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