Skip to main content

scala_chromatica/
io.rs

1//! ColorMap Save/Load System
2//!
3//! This module handles persistent storage of ColorMaps using JSON files.
4//! It supports both built-in default colormaps (embedded at compile time)
5//! and user-created custom colormaps (stored in platform-specific directories).
6//!
7//! # Architecture
8//! - Built-in colormaps are embedded in the binary using `include_str!`
9//! - Custom colormaps are stored in OS-appropriate config directories
10//! - Automatic directory creation and error handling
11//!
12//! # Usage
13//! ```
14//! use scala_chromatica::io;
15//! use scala_chromatica::ColorMap;
16//!
17//! // Load a built-in colormap
18//! let fire = io::load_builtin_colormap("Fire").unwrap();
19//!
20//! // Create and save a custom colormap
21//! let custom = ColorMap::new("MyCustom");
22//! // io::save_colormap(&custom).unwrap(); // Commented - would write to disk
23//!
24//! // Load a custom colormap
25//! // let custom = io::load_custom_colormap("MyCustom").unwrap();
26//!
27//! // List all available colormaps
28//! let all = io::list_available_colormaps().unwrap();
29//! ```
30
31use crate::colormap::ColorMap;
32use crate::error::{ColorMapError, Result};
33use serde::{Deserialize, Serialize};
34use std::fs;
35use std::io;
36use std::path::PathBuf;
37
38/// Macro to define builtin colormaps with automatic list generation
39macro_rules! define_builtin_colormaps {
40    ($($name:literal => $const_name:ident => $file:literal),* $(,)?) => {
41        $(
42            const $const_name: &str = include_str!($file);
43        )*
44
45        /// Get list of all builtin colormap names
46        fn get_builtin_colormap_names() -> &'static [&'static str] {
47            &[$($name),*]
48        }
49
50        /// Load a builtin colormap by name
51        fn load_builtin_impl(name: &str) -> Option<&'static str> {
52            match name {
53                $($name => Some($const_name),)*
54                _ => None,
55            }
56        }
57
58        /// Check if a colormap name is builtin
59        fn is_builtin_impl(name: &str) -> bool {
60            matches!(name, $($name)|*)
61        }
62    };
63}
64
65// Define all builtin colormaps in one place
66define_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
87/// Get the directory where custom colormaps are stored
88/// Returns platform-specific config directory:
89/// - Windows: %APPDATA%\scala-chromatica\colormaps\
90/// - Linux: ~/.config/scala-chromatica/colormaps/
91/// - macOS: ~/Library/Application Support/scala-chromatica/colormaps/
92pub 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    // Create directory if it doesn't exist
99    if !colormaps_dir.exists() {
100        fs::create_dir_all(&colormaps_dir)?;
101    }
102
103    Ok(colormaps_dir)
104}
105
106/// Load a built-in colormap by name
107///
108/// Available built-in colormaps:
109/// - Default, Fire, Ocean, Grayscale, Rainbow
110/// - Academic, Twilight Garden, Coral Sunset
111/// - Olive Symmetry, Orchid Garden, Frozen Amaranth
112/// - Electric Neon, Cosmic Dawn, Vintage Lavender
113/// - Spring Meadow, Egyptian Echo, Copper Sheen, Electric Indigo
114pub 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
122/// Check if a colormap is a built-in default
123pub fn is_builtin_colormap(name: &str) -> bool {
124    is_builtin_impl(name)
125}
126
127/// Save a colormap to the custom colormaps directory
128/// This will create a JSON file named "{colormap.name}.json"
129///
130/// Returns the path to the saved file
131pub 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
142/// Load a custom colormap from the colormaps directory
143pub 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
158/// Load a colormap by name, checking built-ins first, then custom colormaps
159pub fn load_colormap(name: &str) -> Result<ColorMap> {
160    // Try built-in first
161    if is_builtin_colormap(name) {
162        return load_builtin_colormap(name);
163    }
164
165    // Try custom
166    load_custom_colormap(name)
167}
168
169/// Delete a custom colormap
170/// Note: Built-in colormaps cannot be deleted
171pub 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/// Information about an available colormap
192#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct ColorMapInfo {
194    pub name: String,
195    pub is_builtin: bool,
196    pub filepath: Option<PathBuf>,
197}
198
199/// List all available colormaps (built-in + custom)
200pub fn list_available_colormaps() -> Result<Vec<ColorMapInfo>> {
201    let mut colormaps = Vec::new();
202
203    // Add built-in colormaps
204    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    // Add custom colormaps
213    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                    // Skip if it has the same name as a built-in (built-ins take precedence)
222                    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
237/// Export a built-in colormap to the custom colormaps directory
238/// This allows users to create modified versions of built-in colormaps
239pub 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        // Test loading each built-in colormap
251        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}