use crate::colormap::ColorMap;
use crate::error::{ColorMapError, Result};
use serde::{Deserialize, Serialize};
use std::fs;
use std::io;
use std::path::PathBuf;
macro_rules! define_builtin_colormaps {
($($name:literal => $const_name:ident => $file:literal),* $(,)?) => {
$(
const $const_name: &str = include_str!($file);
)*
fn get_builtin_colormap_names() -> &'static [&'static str] {
&[$($name),*]
}
fn load_builtin_impl(name: &str) -> Option<&'static str> {
match name {
$($name => Some($const_name),)*
_ => None,
}
}
fn is_builtin_impl(name: &str) -> bool {
matches!(name, $($name)|*)
}
};
}
define_builtin_colormaps! {
"Default" => DEFAULT_COLORMAP_JSON => "colormaps/default.json",
"Fire" => FIRE_COLORMAP_JSON => "colormaps/fire.json",
"Ocean" => OCEAN_COLORMAP_JSON => "colormaps/ocean.json",
"Grayscale" => GRAYSCALE_COLORMAP_JSON => "colormaps/grayscale.json",
"Rainbow" => RAINBOW_COLORMAP_JSON => "colormaps/rainbow.json",
"Academic" => ACADEMIC_COLORMAP_JSON => "colormaps/academic.json",
"Twilight Garden" => TWILIGHT_GARDEN_COLORMAP_JSON => "colormaps/twilight_garden.json",
"Coral Sunset" => CORAL_SUNSET_COLORMAP_JSON => "colormaps/coral_sunset.json",
"Olive Symmetry" => OLIVE_SYMMETRY_COLORMAP_JSON => "colormaps/olive_symmetry.json",
"Orchid Garden" => ORCHID_GARDEN_COLORMAP_JSON => "colormaps/orchid_garden.json",
"Frozen Amaranth" => FROZEN_AMARANTH_COLORMAP_JSON => "colormaps/frozen_amaranth.json",
"Electric Neon" => ELECTRIC_NEON_COLORMAP_JSON => "colormaps/electric_neon.json",
"Cosmic Dawn" => COSMIC_DAWN_COLORMAP_JSON => "colormaps/cosmic_dawn.json",
"Vintage Lavender" => VINTAGE_LAVENDER_COLORMAP_JSON => "colormaps/vintage_lavender.json",
"Spring Meadow" => SPRING_MEADOW_COLORMAP_JSON => "colormaps/spring_meadow.json",
"Egyptian Echo" => EGYPTIAN_ECHO_COLORMAP_JSON => "colormaps/egyptian_echo.json",
"Copper Sheen" => COPPER_SHEEN_COLORMAP_JSON => "colormaps/copper_sheen.json",
"Electric Indigo" => ELECTRIC_INDIGO_COLORMAP_JSON => "colormaps/electric_indigo.json",
}
pub fn get_colormaps_directory() -> Result<PathBuf> {
let base_dir = directories::ProjectDirs::from("", "", "scala-chromatica")
.ok_or(ColorMapError::NoConfigDirectory)?;
let colormaps_dir = base_dir.config_dir().join("colormaps");
if !colormaps_dir.exists() {
fs::create_dir_all(&colormaps_dir)?;
}
Ok(colormaps_dir)
}
pub fn load_builtin_colormap(name: &str) -> Result<ColorMap> {
let json_str =
load_builtin_impl(name).ok_or_else(|| ColorMapError::NotFound(name.to_string()))?;
let colormap: ColorMap = serde_json::from_str(json_str)?;
Ok(colormap)
}
pub fn is_builtin_colormap(name: &str) -> bool {
is_builtin_impl(name)
}
pub fn save_colormap(colormap: &ColorMap) -> Result<PathBuf> {
let dir = get_colormaps_directory()?;
let filename = format!("{}.json", colormap.name);
let filepath = dir.join(&filename);
let json = serde_json::to_string_pretty(colormap)?;
fs::write(&filepath, json)?;
Ok(filepath)
}
pub fn load_custom_colormap(name: &str) -> Result<ColorMap> {
let dir = get_colormaps_directory()?;
let filename = format!("{}.json", name);
let filepath = dir.join(&filename);
if !filepath.exists() {
return Err(ColorMapError::NotFound(name.to_string()));
}
let json = fs::read_to_string(&filepath)?;
let colormap: ColorMap = serde_json::from_str(&json)?;
Ok(colormap)
}
pub fn load_colormap(name: &str) -> Result<ColorMap> {
if is_builtin_colormap(name) {
return load_builtin_colormap(name);
}
load_custom_colormap(name)
}
pub fn delete_custom_colormap(name: &str) -> Result<()> {
if is_builtin_colormap(name) {
return Err(ColorMapError::IoError(io::Error::new(
io::ErrorKind::PermissionDenied,
"Cannot delete built-in colormaps",
)));
}
let dir = get_colormaps_directory()?;
let filename = format!("{}.json", name);
let filepath = dir.join(&filename);
if !filepath.exists() {
return Err(ColorMapError::NotFound(name.to_string()));
}
fs::remove_file(&filepath)?;
Ok(())
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColorMapInfo {
pub name: String,
pub is_builtin: bool,
pub filepath: Option<PathBuf>,
}
pub fn list_available_colormaps() -> Result<Vec<ColorMapInfo>> {
let mut colormaps = Vec::new();
for name in get_builtin_colormap_names() {
colormaps.push(ColorMapInfo {
name: name.to_string(),
is_builtin: true,
filepath: None,
});
}
let dir = get_colormaps_directory()?;
if dir.exists() {
for entry in fs::read_dir(&dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("json") {
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
if !is_builtin_colormap(stem) {
colormaps.push(ColorMapInfo {
name: stem.to_string(),
is_builtin: false,
filepath: Some(path),
});
}
}
}
}
}
Ok(colormaps)
}
pub fn export_builtin_colormap(name: &str) -> Result<PathBuf> {
let colormap = load_builtin_colormap(name)?;
save_colormap(&colormap)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_load_builtin_colormaps() {
for name in &[
"Default",
"Fire",
"Ocean",
"Grayscale",
"Rainbow",
"Academic",
"Twilight Garden",
"Coral Sunset",
"Olive Symmetry",
"Orchid Garden",
] {
let result = load_builtin_colormap(name);
assert!(result.is_ok(), "Failed to load {}: {:?}", name, result);
let colormap = result.unwrap();
assert_eq!(colormap.name, *name);
assert!(!colormap.stops.is_empty());
}
}
#[test]
fn test_load_nonexistent_builtin() {
let result = load_builtin_colormap("NonExistent");
assert!(result.is_err());
}
#[test]
fn test_is_builtin_colormap() {
assert!(is_builtin_colormap("Fire"));
assert!(is_builtin_colormap("Ocean"));
assert!(is_builtin_colormap("Academic"));
assert!(is_builtin_colormap("Orchid Garden"));
assert!(!is_builtin_colormap("MyCustom"));
assert!(!is_builtin_colormap(""));
}
}