1use crate::colormap::ColorMap;
32use crate::error::{ColorMapError, Result};
33use serde::{Deserialize, Serialize};
34use std::fs;
35use std::io;
36use std::path::PathBuf;
37
38macro_rules! define_builtin_colormaps {
40 ($($name:literal => $const_name:ident => $file:literal),* $(,)?) => {
41 $(
42 const $const_name: &str = include_str!($file);
43 )*
44
45 fn get_builtin_colormap_names() -> &'static [&'static str] {
47 &[$($name),*]
48 }
49
50 fn load_builtin_impl(name: &str) -> Option<&'static str> {
52 match name {
53 $($name => Some($const_name),)*
54 _ => None,
55 }
56 }
57
58 fn is_builtin_impl(name: &str) -> bool {
60 matches!(name, $($name)|*)
61 }
62 };
63}
64
65define_builtin_colormaps! {
67 "Default" => DEFAULT_COLORMAP_JSON => "colormaps/default.json",
68 "Fire" => FIRE_COLORMAP_JSON => "colormaps/fire.json",
69 "Ocean" => OCEAN_COLORMAP_JSON => "colormaps/ocean.json",
70 "Grayscale" => GRAYSCALE_COLORMAP_JSON => "colormaps/grayscale.json",
71 "Rainbow" => RAINBOW_COLORMAP_JSON => "colormaps/rainbow.json",
72 "Academic" => ACADEMIC_COLORMAP_JSON => "colormaps/academic.json",
73 "Twilight Garden" => TWILIGHT_GARDEN_COLORMAP_JSON => "colormaps/twilight_garden.json",
74 "Coral Sunset" => CORAL_SUNSET_COLORMAP_JSON => "colormaps/coral_sunset.json",
75 "Olive Symmetry" => OLIVE_SYMMETRY_COLORMAP_JSON => "colormaps/olive_symmetry.json",
76 "Orchid Garden" => ORCHID_GARDEN_COLORMAP_JSON => "colormaps/orchid_garden.json",
77 "Frozen Amaranth" => FROZEN_AMARANTH_COLORMAP_JSON => "colormaps/frozen_amaranth.json",
78 "Electric Neon" => ELECTRIC_NEON_COLORMAP_JSON => "colormaps/electric_neon.json",
79 "Cosmic Dawn" => COSMIC_DAWN_COLORMAP_JSON => "colormaps/cosmic_dawn.json",
80 "Vintage Lavender" => VINTAGE_LAVENDER_COLORMAP_JSON => "colormaps/vintage_lavender.json",
81 "Spring Meadow" => SPRING_MEADOW_COLORMAP_JSON => "colormaps/spring_meadow.json",
82 "Egyptian Echo" => EGYPTIAN_ECHO_COLORMAP_JSON => "colormaps/egyptian_echo.json",
83 "Copper Sheen" => COPPER_SHEEN_COLORMAP_JSON => "colormaps/copper_sheen.json",
84 "Electric Indigo" => ELECTRIC_INDIGO_COLORMAP_JSON => "colormaps/electric_indigo.json",
85}
86
87pub fn get_colormaps_directory() -> Result<PathBuf> {
93 let base_dir = directories::ProjectDirs::from("", "", "scala-chromatica")
94 .ok_or(ColorMapError::NoConfigDirectory)?;
95
96 let colormaps_dir = base_dir.config_dir().join("colormaps");
97
98 if !colormaps_dir.exists() {
100 fs::create_dir_all(&colormaps_dir)?;
101 }
102
103 Ok(colormaps_dir)
104}
105
106pub fn load_builtin_colormap(name: &str) -> Result<ColorMap> {
115 let json_str =
116 load_builtin_impl(name).ok_or_else(|| ColorMapError::NotFound(name.to_string()))?;
117
118 let colormap: ColorMap = serde_json::from_str(json_str)?;
119 Ok(colormap)
120}
121
122pub fn is_builtin_colormap(name: &str) -> bool {
124 is_builtin_impl(name)
125}
126
127pub fn save_colormap(colormap: &ColorMap) -> Result<PathBuf> {
132 let dir = get_colormaps_directory()?;
133 let filename = format!("{}.json", colormap.name);
134 let filepath = dir.join(&filename);
135
136 let json = serde_json::to_string_pretty(colormap)?;
137 fs::write(&filepath, json)?;
138
139 Ok(filepath)
140}
141
142pub fn load_custom_colormap(name: &str) -> Result<ColorMap> {
144 let dir = get_colormaps_directory()?;
145 let filename = format!("{}.json", name);
146 let filepath = dir.join(&filename);
147
148 if !filepath.exists() {
149 return Err(ColorMapError::NotFound(name.to_string()));
150 }
151
152 let json = fs::read_to_string(&filepath)?;
153 let colormap: ColorMap = serde_json::from_str(&json)?;
154
155 Ok(colormap)
156}
157
158pub fn load_colormap(name: &str) -> Result<ColorMap> {
160 if is_builtin_colormap(name) {
162 return load_builtin_colormap(name);
163 }
164
165 load_custom_colormap(name)
167}
168
169pub fn delete_custom_colormap(name: &str) -> Result<()> {
172 if is_builtin_colormap(name) {
173 return Err(ColorMapError::IoError(io::Error::new(
174 io::ErrorKind::PermissionDenied,
175 "Cannot delete built-in colormaps",
176 )));
177 }
178
179 let dir = get_colormaps_directory()?;
180 let filename = format!("{}.json", name);
181 let filepath = dir.join(&filename);
182
183 if !filepath.exists() {
184 return Err(ColorMapError::NotFound(name.to_string()));
185 }
186
187 fs::remove_file(&filepath)?;
188 Ok(())
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct ColorMapInfo {
194 pub name: String,
195 pub is_builtin: bool,
196 pub filepath: Option<PathBuf>,
197}
198
199pub fn list_available_colormaps() -> Result<Vec<ColorMapInfo>> {
201 let mut colormaps = Vec::new();
202
203 for name in get_builtin_colormap_names() {
205 colormaps.push(ColorMapInfo {
206 name: name.to_string(),
207 is_builtin: true,
208 filepath: None,
209 });
210 }
211
212 let dir = get_colormaps_directory()?;
214 if dir.exists() {
215 for entry in fs::read_dir(&dir)? {
216 let entry = entry?;
217 let path = entry.path();
218
219 if path.extension().and_then(|s| s.to_str()) == Some("json") {
220 if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
221 if !is_builtin_colormap(stem) {
223 colormaps.push(ColorMapInfo {
224 name: stem.to_string(),
225 is_builtin: false,
226 filepath: Some(path),
227 });
228 }
229 }
230 }
231 }
232 }
233
234 Ok(colormaps)
235}
236
237pub fn export_builtin_colormap(name: &str) -> Result<PathBuf> {
240 let colormap = load_builtin_colormap(name)?;
241 save_colormap(&colormap)
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 #[test]
249 fn test_load_builtin_colormaps() {
250 for name in &[
252 "Default",
253 "Fire",
254 "Ocean",
255 "Grayscale",
256 "Rainbow",
257 "Academic",
258 "Twilight Garden",
259 "Coral Sunset",
260 "Olive Symmetry",
261 "Orchid Garden",
262 ] {
263 let result = load_builtin_colormap(name);
264 assert!(result.is_ok(), "Failed to load {}: {:?}", name, result);
265
266 let colormap = result.unwrap();
267 assert_eq!(colormap.name, *name);
268 assert!(!colormap.stops.is_empty());
269 }
270 }
271
272 #[test]
273 fn test_load_nonexistent_builtin() {
274 let result = load_builtin_colormap("NonExistent");
275 assert!(result.is_err());
276 }
277
278 #[test]
279 fn test_is_builtin_colormap() {
280 assert!(is_builtin_colormap("Fire"));
281 assert!(is_builtin_colormap("Ocean"));
282 assert!(is_builtin_colormap("Academic"));
283 assert!(is_builtin_colormap("Orchid Garden"));
284 assert!(!is_builtin_colormap("MyCustom"));
285 assert!(!is_builtin_colormap(""));
286 }
287}