Skip to main content

chromasync_renderers/
lib.rs

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}