use anyhow::{Context, Result};
use clap::Parser;
use colorgrad::Color;
use image::{ImageBuffer, Rgb};
use log::{debug, info};
use std::fs;
use std::path::PathBuf;
use std::time::Instant;
#[derive(Parser, Debug)]
#[command(
name = "theme-preview-generator",
about = "Generate preview images for ChromaCat themes",
version
)]
pub struct Args {
#[arg(short, long, default_value = "docs/theme-previews")]
pub output_dir: PathBuf,
#[arg(short = 'B', long, default_value_t = 30)]
pub blocks: u32,
#[arg(short = 'H', long, default_value_t = 50)]
pub height: u32,
#[arg(short = 'W', long)]
pub width: Option<u32>,
#[arg(long)]
pub skip_combined: bool,
#[arg(short, long)]
pub theme: Option<String>,
#[arg(short, long)]
pub category: Option<String>,
}
pub struct ThemePreviewGenerator {
args: Args,
}
impl ThemePreviewGenerator {
pub fn new(args: Args) -> Self {
Self { args }
}
pub fn generate(&self) -> Result<()> {
let start = Instant::now();
info!("Starting theme preview generation");
debug!("Configuration: {:#?}", self.args);
fs::create_dir_all(&self.args.output_dir).context("Failed to create output directory")?;
let themes = if let Some(theme_name) = &self.args.theme {
vec![chromacat::themes::get_theme(theme_name)?]
} else if let Some(category) = &self.args.category {
let names = chromacat::themes::list_category(category).unwrap_or_default();
names
.into_iter()
.filter_map(|name| chromacat::themes::get_theme(&name).ok())
.collect()
} else {
chromacat::themes::all_themes()
};
info!("Generating previews for {} themes", themes.len());
for theme in &themes {
self.generate_preview(theme).with_context(|| {
format!("Failed to generate preview for theme '{}'", theme.name)
})?;
}
if !self.args.skip_combined {
self.generate_category_previews()
.context("Failed to generate category previews")?;
}
info!(
"Preview generation completed in {:.2}s",
start.elapsed().as_secs_f64()
);
Ok(())
}
fn generate_preview(&self, theme: &chromacat::themes::ThemeDefinition) -> Result<()> {
debug!("Generating preview for theme: {}", theme.name);
let gradient = theme
.create_gradient()
.with_context(|| format!("Failed to create gradient for theme '{}'", theme.name))?;
let blocks = self.args.blocks;
let img_height = self.args.height;
let img_width = self.args.width.unwrap_or(blocks * 10); let block_width = img_width / blocks;
let mut img = ImageBuffer::new(img_width, img_height);
for i in 0..blocks {
let t = i as f32 / (blocks - 1) as f32;
let color = gradient.at(t);
let rgb = color_to_rgb(&color);
let x_start = i * block_width;
let x_end = if i == blocks - 1 {
img_width
} else {
(i + 1) * block_width
};
for x in x_start..x_end {
for y in 0..img_height {
img.put_pixel(x, y, rgb);
}
}
}
let category = self.get_theme_category(&theme.name)?;
let category_dir = self.args.output_dir.join(&category);
fs::create_dir_all(&category_dir).context("Failed to create category directory")?;
let filename = format!("{}.png", theme.name);
let path = category_dir.join(filename);
img.save(&path)
.with_context(|| format!("Failed to save preview to '{}'", path.display()))?;
debug!(
"Generated preview for '{}' at '{}'",
theme.name,
path.display()
);
Ok(())
}
fn generate_category_previews(&self) -> Result<()> {
info!("Generating combined category previews");
for category in chromacat::themes::list_categories() {
if let Some(requested) = &self.args.category {
if requested != &category {
continue;
}
}
if let Some(themes) = chromacat::themes::list_category(&category) {
let theme_count = themes.len();
if theme_count == 0 {
continue;
}
let blocks = self.args.blocks;
let img_height = self.args.height * theme_count as u32;
let img_width = self.args.width.unwrap_or(blocks * 10); let block_width = img_width / blocks;
let mut combined_img = ImageBuffer::new(img_width, img_height);
for (i, theme_name) in themes.iter().enumerate() {
let theme = chromacat::themes::get_theme(theme_name)?;
let gradient = theme.create_gradient()?;
let y_offset = i as u32 * self.args.height;
for j in 0..blocks {
let t = j as f32 / (blocks - 1) as f32;
let color = gradient.at(t);
let rgb = color_to_rgb(&color);
let x_start = j * block_width;
let x_end = if j == blocks - 1 {
img_width
} else {
(j + 1) * block_width
};
for x in x_start..x_end {
for y in y_offset..(y_offset + self.args.height) {
combined_img.put_pixel(x, y, rgb);
}
}
}
}
let filename = format!("{}_all.png", category);
let path = self.args.output_dir.join(&filename);
combined_img.save(&path).with_context(|| {
format!(
"Failed to save combined preview for category '{}'",
category
)
})?;
debug!("Generated combined preview for category '{}'", category);
}
}
Ok(())
}
fn get_theme_category(&self, theme_name: &str) -> Result<String> {
for category in chromacat::themes::list_categories() {
if let Some(themes) = chromacat::themes::list_category(&category) {
if themes.contains(&theme_name.to_string()) {
return Ok(category);
}
}
}
Ok("default".to_string())
}
}
fn color_to_rgb(color: &Color) -> Rgb<u8> {
Rgb([
(color.r * 255.0).clamp(0.0, 255.0) as u8,
(color.g * 255.0).clamp(0.0, 255.0) as u8,
(color.b * 255.0).clamp(0.0, 255.0) as u8,
])
}
fn main() -> Result<()> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
.format_timestamp(None)
.format_target(false)
.init();
let args = Args::parse();
let generator = ThemePreviewGenerator::new(args);
generator.generate()
}