chromasync-renderers 0.6.0

Built-in renderers and target registry for Chromasync
Documentation
mod alacritty;
mod registry;

use chromasync_types::{
    GeneratedArtifact, GenerationContext, HexColor, RenderTarget, SemanticTokens,
};
use thiserror::Error;

pub use crate::registry::{
    ArtifactSpec, CompiledTarget, ListedTarget, OutputRegistry, RendererRegistry, TargetRegistry,
    TargetSource, TargetSpec, parse_target_file, user_targets_dir,
};

pub const BUILTIN_TARGETS: [RenderTarget; 17] = [
    RenderTarget::Alacritty,
    RenderTarget::Chromium,
    RenderTarget::Ghostty,
    RenderTarget::GoogleChrome,
    RenderTarget::Gtk3,
    RenderTarget::Gtk4,
    RenderTarget::HeliumBrowser,
    RenderTarget::Hyprland,
    RenderTarget::HyprlandLua,
    RenderTarget::KColorScheme,
    RenderTarget::Kitty,
    RenderTarget::Micro,
    RenderTarget::Qt5,
    RenderTarget::Qt6,
    RenderTarget::VsCode,
    RenderTarget::VsCodeInsiders,
    RenderTarget::Zed,
];

pub(crate) const BUILTIN_DECLARATIVE_TARGETS: [(&str, &str, &str); 16] = [
    (
        "chromium",
        "chromium.toml",
        include_str!("../builtin-targets/chromium.toml"),
    ),
    (
        "ghostty",
        "ghostty.toml",
        include_str!("../builtin-targets/ghostty.toml"),
    ),
    (
        "google-chrome",
        "google-chrome.toml",
        include_str!("../builtin-targets/google-chrome.toml"),
    ),
    (
        "gtk3",
        "gtk3.toml",
        include_str!("../builtin-targets/gtk3.toml"),
    ),
    (
        "gtk4",
        "gtk4.toml",
        include_str!("../builtin-targets/gtk4.toml"),
    ),
    (
        "helium-browser",
        "helium-browser.toml",
        include_str!("../builtin-targets/helium-browser.toml"),
    ),
    (
        "hyprland",
        "hyprland.toml",
        include_str!("../builtin-targets/hyprland.toml"),
    ),
    (
        "hyprland-lua",
        "hyprland-lua.toml",
        include_str!("../builtin-targets/hyprland-lua.toml"),
    ),
    (
        "kcolorscheme",
        "kcolorscheme.toml",
        include_str!("../builtin-targets/kcolorscheme.toml"),
    ),
    (
        "kitty",
        "kitty.toml",
        include_str!("../builtin-targets/kitty.toml"),
    ),
    (
        "micro",
        "micro.toml",
        include_str!("../builtin-targets/micro.toml"),
    ),
    (
        "qt5",
        "qt5.toml",
        include_str!("../builtin-targets/qt5.toml"),
    ),
    (
        "qt6",
        "qt6.toml",
        include_str!("../builtin-targets/qt6.toml"),
    ),
    (
        "vscode",
        "vscode.toml",
        include_str!("../builtin-targets/vscode.toml"),
    ),
    (
        "vscode-insiders",
        "vscode-insiders.toml",
        include_str!("../builtin-targets/vscode-insiders.toml"),
    ),
    (
        "zed",
        "zed.toml",
        include_str!("../builtin-targets/zed.toml"),
    ),
];

pub trait ArtifactGenerator: Send + Sync {
    fn name(&self) -> &str;
    fn preferred_template(&self) -> Option<&str> {
        None
    }
    fn chroma_strategy(&self) -> Option<chromasync_types::ChromaStrategy> {
        None
    }
    fn generate(
        &self,
        tokens: &SemanticTokens,
        ctx: &GenerationContext,
    ) -> Result<Vec<GeneratedArtifact>, RendererError>;
}

pub trait Renderer: Send + Sync {
    fn target(&self) -> RenderTarget;
    fn name(&self) -> &'static str;

    fn file_name(&self) -> &'static str {
        self.target().file_name()
    }

    fn render_artifact(&self, tokens: &SemanticTokens) -> Result<GeneratedArtifact, RendererError>;
}

impl<T> ArtifactGenerator for T
where
    T: Renderer,
{
    fn name(&self) -> &str {
        Renderer::name(self)
    }

    fn generate(
        &self,
        tokens: &SemanticTokens,
        _ctx: &GenerationContext,
    ) -> Result<Vec<GeneratedArtifact>, RendererError> {
        Ok(vec![self.render_artifact(tokens)?])
    }
}

#[derive(Debug, Error)]
pub enum RendererError {
    #[error("renderer '{target}' is not implemented yet")]
    UnsupportedTarget { target: RenderTarget },
    #[error(
        "target '{target}' generates {count} artifacts; use render_targets for multi-artifact targets"
    )]
    MultiArtifactTarget { target: RenderTarget, count: usize },
    #[error("target '{requested}' was not found")]
    UnknownTarget { requested: String },
    #[error("color '{value}' must use the #RRGGBB format")]
    InvalidColorFormat { value: String },
    #[error("color '{value}' contains invalid hexadecimal digits")]
    InvalidHexDigits { value: String },
    #[error("failed to read targets directory '{path}': {source}")]
    ReadTargetsDir {
        path: std::path::PathBuf,
        #[source]
        source: std::io::Error,
    },
    #[error("failed to read target file '{path}': {source}")]
    ReadTargetFile {
        path: std::path::PathBuf,
        #[source]
        source: std::io::Error,
    },
    #[error("failed to parse target '{name}' from {source}: {error}")]
    ParseTarget {
        name: String,
        source: String,
        #[source]
        error: Box<toml::de::Error>,
    },
    #[error("target '{name}' uses an invalid name: {reason}")]
    InvalidTargetName { name: String, reason: String },
    #[error("target '{target}' has no artifacts after inheritance is resolved")]
    TargetHasNoArtifacts { target: String },
    #[error("target '{target}' artifact '{file_name}' is invalid: {reason}")]
    InvalidArtifactFileName {
        target: String,
        file_name: String,
        reason: String,
    },
    #[error("target '{target}' artifact '{file_name}' uses an invalid placeholder '{placeholder}'")]
    InvalidPlaceholder {
        target: String,
        file_name: String,
        placeholder: String,
    },
    #[error("target '{target}' artifact '{file_name}' contains an unterminated placeholder")]
    UnterminatedPlaceholder { target: String, file_name: String },
    #[error("target '{target}' references unknown base target '{base}'")]
    UnknownBaseTarget { target: String, base: String },
    #[error("target '{target}' cannot inherit from built-in renderer '{base}'")]
    BuiltInTargetInheritance { target: String, base: String },
    #[error("target inheritance cycle detected: {cycle}")]
    TargetInheritanceCycle { cycle: String },
    #[error("user target '{name}' collides with a built-in renderer name")]
    TargetNameCollidesWithBuiltIn { name: String },
    #[error(
        "user target '{name}' is defined multiple times: first at {first_source}, second at {second_source}"
    )]
    DuplicateTargetName {
        name: String,
        first_source: String,
        second_source: String,
    },
}

pub fn built_in_targets() -> &'static [RenderTarget] {
    &BUILTIN_TARGETS
}

pub fn render_target(
    target: RenderTarget,
    tokens: &SemanticTokens,
) -> Result<GeneratedArtifact, RendererError> {
    let registry = RendererRegistry::new();
    let generator = registry
        .get(target.as_str())
        .ok_or(RendererError::UnsupportedTarget { target })?;
    let context = GenerationContext::default();
    let mut artifacts = generator.generate(tokens, &context)?;

    if artifacts.len() > 1 {
        return Err(RendererError::MultiArtifactTarget {
            target,
            count: artifacts.len(),
        });
    }

    artifacts
        .pop()
        .ok_or(RendererError::UnsupportedTarget { target })
}

pub fn render_targets(
    targets: &[RenderTarget],
    tokens: &SemanticTokens,
) -> Result<Vec<GeneratedArtifact>, RendererError> {
    let registry = RendererRegistry::new();
    let requested = if targets.is_empty() {
        registry.built_in_names()
    } else {
        let mut seen = std::collections::BTreeSet::new();
        let mut names = Vec::with_capacity(targets.len());

        for target in targets {
            let name = target.as_str().to_owned();

            if seen.insert(name.clone()) {
                names.push(name);
            }
        }

        names
    };

    registry::OutputRegistry::default().generate(&requested, tokens, &GenerationContext::default())
}

fn parse_hex_color(value: &str) -> Result<[u8; 3], RendererError> {
    let normalized = value.strip_prefix('#').unwrap_or(value);

    if normalized.len() != 6 {
        return Err(RendererError::InvalidColorFormat {
            value: value.to_owned(),
        });
    }

    let red =
        u8::from_str_radix(&normalized[0..2], 16).map_err(|_| RendererError::InvalidHexDigits {
            value: value.to_owned(),
        })?;
    let green =
        u8::from_str_radix(&normalized[2..4], 16).map_err(|_| RendererError::InvalidHexDigits {
            value: value.to_owned(),
        })?;
    let blue =
        u8::from_str_radix(&normalized[4..6], 16).map_err(|_| RendererError::InvalidHexDigits {
            value: value.to_owned(),
        })?;

    Ok([red, green, blue])
}

fn hyprland_rgba(value: &str, alpha: u8) -> Result<String, RendererError> {
    let [red, green, blue] = parse_hex_color(value)?;

    Ok(format!("rgba({red:02X}{green:02X}{blue:02X}{alpha:02X})"))
}

fn normalized_hex(value: &str) -> Result<String, RendererError> {
    let [red, green, blue] = parse_hex_color(value)?;

    Ok(format!("#{red:02X}{green:02X}{blue:02X}"))
}

fn normalized_hex_without_hash(value: &str) -> Result<String, RendererError> {
    let [red, green, blue] = parse_hex_color(value)?;

    Ok(format!("{red:02x}{green:02x}{blue:02x}"))
}

fn rgb_components(value: &str) -> Result<String, RendererError> {
    let [red, green, blue] = parse_hex_color(value)?;

    Ok(format!("{red}, {green}, {blue}"))
}

fn terminal_ansi_colors(tokens: &SemanticTokens) -> [HexColor; 16] {
    [
        tokens.bg_secondary.clone(),
        tokens.error.clone(),
        tokens.success.clone(),
        tokens.warning.clone(),
        tokens.link.clone(),
        tokens.accent.clone(),
        tokens.selection.clone(),
        tokens.border_strong.clone(),
        tokens.text_muted.clone(),
        tokens.error.clone(),
        tokens.success.clone(),
        tokens.warning.clone(),
        tokens.accent_hover.clone(),
        tokens.accent_active.clone(),
        tokens.border_strong.clone(),
        tokens.text.clone(),
    ]
}