Skip to main content

par_term_config/
shader_metadata.rs

1//! Shader metadata parsing and caching.
2//!
3//! Parses embedded YAML metadata from shader files in the format:
4//!
5//! ```glsl
6//! /*! par-term shader metadata
7//! name: "CRT Effect"
8//! author: "Timothy Lottes"
9//! description: "Classic CRT monitor simulation"
10//! version: "1.0.0"
11//!
12//! defaults:
13//!   animation_speed: 1.0
14//!   brightness: 0.85
15//!   channel0: "textures/noise.png"
16//! */
17//! ```
18
19use crate::types::ShaderMetadata;
20use std::collections::HashMap;
21use std::path::{Path, PathBuf};
22
23/// Marker string that identifies the start of shader metadata block
24const METADATA_MARKER: &str = "/*! par-term shader metadata";
25
26/// Parse shader metadata from GLSL source code.
27///
28/// Looks for a `/*! par-term shader metadata ... */` block at the top of the file
29/// and parses the YAML content within.
30///
31/// # Arguments
32/// * `source` - The GLSL shader source code
33///
34/// # Returns
35/// * `Some(ShaderMetadata)` if metadata was found and parsed successfully
36/// * `None` if no metadata block was found or parsing failed
37pub fn parse_shader_metadata(source: &str) -> Option<ShaderMetadata> {
38    // Find the metadata block marker
39    let start_marker = source.find(METADATA_MARKER)?;
40
41    // Find the start of YAML content (after the marker line)
42    let yaml_start = source[start_marker + METADATA_MARKER.len()..]
43        .find('\n')
44        .map(|i| start_marker + METADATA_MARKER.len() + i + 1)?;
45
46    // Find the closing */
47    let yaml_end = source[yaml_start..].find("*/")?;
48    let yaml_content = &source[yaml_start..yaml_start + yaml_end];
49
50    // Trim trailing whitespace from each line and the whole block
51    let yaml_trimmed = yaml_content.trim();
52
53    // Parse the YAML
54    match serde_yaml::from_str(yaml_trimmed) {
55        Ok(metadata) => {
56            log::debug!("Parsed shader metadata: {:?}", metadata);
57            Some(metadata)
58        }
59        Err(e) => {
60            log::warn!("Failed to parse shader metadata YAML: {}", e);
61            log::debug!("YAML content was:\n{}", yaml_trimmed);
62            None
63        }
64    }
65}
66
67/// Parse shader metadata from a file path.
68///
69/// # Arguments
70/// * `path` - Path to the shader file
71///
72/// # Returns
73/// * `Some(ShaderMetadata)` if the file was read and metadata was parsed successfully
74/// * `None` if reading failed or no metadata was found
75pub fn parse_shader_metadata_from_file(path: &Path) -> Option<ShaderMetadata> {
76    match std::fs::read_to_string(path) {
77        Ok(source) => parse_shader_metadata(&source),
78        Err(e) => {
79            log::warn!("Failed to read shader file '{}': {}", path.display(), e);
80            None
81        }
82    }
83}
84
85/// Serialize shader metadata to a YAML string (without the comment wrapper).
86///
87/// # Arguments
88/// * `metadata` - The metadata to serialize
89///
90/// # Returns
91/// The YAML representation of the metadata
92pub fn serialize_metadata_to_yaml(metadata: &ShaderMetadata) -> Result<String, String> {
93    serde_yaml::to_string(metadata).map_err(|e| format!("Failed to serialize metadata: {}", e))
94}
95
96/// Format shader metadata as a complete comment block ready to insert into a shader.
97///
98/// # Arguments
99/// * `metadata` - The metadata to format
100///
101/// # Returns
102/// The formatted metadata block including the `/*! par-term shader metadata ... */` wrapper
103pub fn format_metadata_block(metadata: &ShaderMetadata) -> Result<String, String> {
104    let yaml = serialize_metadata_to_yaml(metadata)?;
105    Ok(format!("{}\n{}\n*/", METADATA_MARKER, yaml.trim_end()))
106}
107
108/// Update or insert metadata in shader source code.
109///
110/// If the shader already has a metadata block, it will be replaced.
111/// If not, the metadata block will be inserted at the beginning of the file.
112///
113/// # Arguments
114/// * `source` - The original shader source code
115/// * `metadata` - The new metadata to insert/update
116///
117/// # Returns
118/// The updated shader source code
119pub fn update_shader_metadata(source: &str, metadata: &ShaderMetadata) -> Result<String, String> {
120    let new_block = format_metadata_block(metadata)?;
121
122    // Check if there's an existing metadata block
123    if let Some(start_pos) = source.find(METADATA_MARKER) {
124        // Find the end of the metadata block
125        if let Some(end_offset) = source[start_pos..].find("*/") {
126            let end_pos = start_pos + end_offset + 2; // Include the */
127            // Replace the existing block
128            let mut result = String::with_capacity(source.len());
129            result.push_str(&source[..start_pos]);
130            result.push_str(&new_block);
131            result.push_str(&source[end_pos..]);
132            return Ok(result);
133        }
134    }
135
136    // No existing block, insert at the beginning
137    Ok(format!("{}\n\n{}", new_block, source))
138}
139
140/// Update metadata in a shader file.
141///
142/// # Arguments
143/// * `path` - Path to the shader file
144/// * `metadata` - The new metadata to write
145///
146/// # Returns
147/// Ok(()) if successful, Err with error message otherwise
148pub fn update_shader_metadata_file(path: &Path, metadata: &ShaderMetadata) -> Result<(), String> {
149    // Read the current file content
150    let source = std::fs::read_to_string(path)
151        .map_err(|e| format!("Failed to read shader file '{}': {}", path.display(), e))?;
152
153    // Update the metadata
154    let updated_source = update_shader_metadata(&source, metadata)?;
155
156    // Write back to the file
157    std::fs::write(path, updated_source)
158        .map_err(|e| format!("Failed to write shader file '{}': {}", path.display(), e))?;
159
160    log::info!("Updated metadata in shader file: {}", path.display());
161    Ok(())
162}
163
164/// Cache for parsed shader metadata.
165///
166/// Avoids re-parsing shader files on every access while still allowing
167/// invalidation for hot reload scenarios.
168#[derive(Debug, Default)]
169pub struct ShaderMetadataCache {
170    /// Cached metadata by shader filename (not full path)
171    cache: HashMap<String, Option<ShaderMetadata>>,
172    /// The shaders directory path
173    shaders_dir: Option<PathBuf>,
174}
175
176impl ShaderMetadataCache {
177    /// Create a new empty metadata cache.
178    #[allow(dead_code)]
179    pub fn new() -> Self {
180        Self::default()
181    }
182
183    /// Create a new metadata cache with a specific shaders directory.
184    pub fn with_shaders_dir(shaders_dir: PathBuf) -> Self {
185        Self {
186            cache: HashMap::new(),
187            shaders_dir: Some(shaders_dir),
188        }
189    }
190
191    /// Set the shaders directory path.
192    #[allow(dead_code)]
193    pub fn set_shaders_dir(&mut self, shaders_dir: PathBuf) {
194        self.shaders_dir = Some(shaders_dir);
195    }
196
197    /// Get metadata for a shader, loading and caching if necessary.
198    ///
199    /// # Arguments
200    /// * `shader_name` - Filename of the shader (e.g., "crt.glsl")
201    ///
202    /// # Returns
203    /// * `Some(&ShaderMetadata)` if metadata was found
204    /// * `None` if no metadata was found or the shader couldn't be read
205    pub fn get(&mut self, shader_name: &str) -> Option<&ShaderMetadata> {
206        // Check if already cached
207        if self.cache.contains_key(shader_name) {
208            return self.cache.get(shader_name).and_then(|m| m.as_ref());
209        }
210
211        // Load and cache
212        let metadata = self.load_metadata(shader_name);
213        self.cache.insert(shader_name.to_string(), metadata);
214        self.cache.get(shader_name).and_then(|m| m.as_ref())
215    }
216
217    /// Get metadata without caching (always reads from disk).
218    ///
219    /// Useful for hot reload scenarios where you want fresh data.
220    #[allow(dead_code)]
221    pub fn get_fresh(&self, shader_name: &str) -> Option<ShaderMetadata> {
222        self.load_metadata(shader_name)
223    }
224
225    /// Load metadata from a shader file.
226    fn load_metadata(&self, shader_name: &str) -> Option<ShaderMetadata> {
227        let path = self.resolve_shader_path(shader_name)?;
228        parse_shader_metadata_from_file(&path)
229    }
230
231    /// Resolve a shader name to its full path.
232    fn resolve_shader_path(&self, shader_name: &str) -> Option<PathBuf> {
233        let shader_path = PathBuf::from(shader_name);
234
235        // If it's an absolute path, use it directly
236        if shader_path.is_absolute() && shader_path.exists() {
237            return Some(shader_path);
238        }
239
240        // Otherwise, resolve relative to shaders directory
241        if let Some(ref shaders_dir) = self.shaders_dir {
242            let full_path = shaders_dir.join(shader_name);
243            if full_path.exists() {
244                return Some(full_path);
245            }
246        }
247
248        // Try the default shaders directory
249        let default_path = crate::config::Config::shader_path(shader_name);
250        if default_path.exists() {
251            return Some(default_path);
252        }
253
254        None
255    }
256
257    /// Invalidate cached metadata for a specific shader.
258    ///
259    /// Call this when a shader file has been modified (hot reload).
260    #[allow(dead_code)]
261    pub fn invalidate(&mut self, shader_name: &str) {
262        self.cache.remove(shader_name);
263        log::debug!("Invalidated metadata cache for: {}", shader_name);
264    }
265
266    /// Invalidate all cached metadata.
267    ///
268    /// Call this when the shaders directory might have changed.
269    #[allow(dead_code)]
270    pub fn invalidate_all(&mut self) {
271        self.cache.clear();
272        log::debug!("Invalidated all metadata cache entries");
273    }
274
275    /// Check if metadata is cached for a shader.
276    #[allow(dead_code)]
277    pub fn is_cached(&self, shader_name: &str) -> bool {
278        self.cache.contains_key(shader_name)
279    }
280
281    /// Get the number of cached entries.
282    #[allow(dead_code)]
283    pub fn cache_size(&self) -> usize {
284        self.cache.len()
285    }
286}
287
288// ============================================================================
289// Cursor Shader Metadata Functions
290// ============================================================================
291
292use crate::types::CursorShaderMetadata;
293
294/// Parse cursor shader metadata from GLSL source code.
295///
296/// Uses the same `/*! par-term shader metadata ... */` format as background shaders,
297/// but deserializes to `CursorShaderMetadata` which includes cursor-specific settings.
298///
299/// # Arguments
300/// * `source` - The GLSL shader source code
301///
302/// # Returns
303/// * `Some(CursorShaderMetadata)` if metadata was found and parsed successfully
304/// * `None` if no metadata block was found or parsing failed
305pub fn parse_cursor_shader_metadata(source: &str) -> Option<CursorShaderMetadata> {
306    // Find the metadata block marker
307    let start_marker = source.find(METADATA_MARKER)?;
308
309    // Find the start of YAML content (after the marker line)
310    let yaml_start = source[start_marker + METADATA_MARKER.len()..]
311        .find('\n')
312        .map(|i| start_marker + METADATA_MARKER.len() + i + 1)?;
313
314    // Find the closing */
315    let yaml_end = source[yaml_start..].find("*/")?;
316    let yaml_content = &source[yaml_start..yaml_start + yaml_end];
317
318    // Trim trailing whitespace from each line and the whole block
319    let yaml_trimmed = yaml_content.trim();
320
321    // Parse the YAML as CursorShaderMetadata
322    match serde_yaml::from_str(yaml_trimmed) {
323        Ok(metadata) => {
324            log::debug!("Parsed cursor shader metadata: {:?}", metadata);
325            Some(metadata)
326        }
327        Err(e) => {
328            log::warn!("Failed to parse cursor shader metadata YAML: {}", e);
329            log::debug!("YAML content was:\n{}", yaml_trimmed);
330            None
331        }
332    }
333}
334
335/// Parse cursor shader metadata from a file path.
336///
337/// # Arguments
338/// * `path` - Path to the shader file
339///
340/// # Returns
341/// * `Some(CursorShaderMetadata)` if the file was read and metadata was parsed successfully
342/// * `None` if reading failed or no metadata was found
343pub fn parse_cursor_shader_metadata_from_file(path: &Path) -> Option<CursorShaderMetadata> {
344    match std::fs::read_to_string(path) {
345        Ok(source) => parse_cursor_shader_metadata(&source),
346        Err(e) => {
347            log::warn!(
348                "Failed to read cursor shader file '{}': {}",
349                path.display(),
350                e
351            );
352            None
353        }
354    }
355}
356
357/// Serialize cursor shader metadata to a YAML string (without the comment wrapper).
358///
359/// # Arguments
360/// * `metadata` - The metadata to serialize
361///
362/// # Returns
363/// The YAML representation of the metadata
364pub fn serialize_cursor_metadata_to_yaml(
365    metadata: &CursorShaderMetadata,
366) -> Result<String, String> {
367    serde_yaml::to_string(metadata).map_err(|e| format!("Failed to serialize metadata: {}", e))
368}
369
370/// Format cursor shader metadata as a complete comment block ready to insert into a shader.
371///
372/// # Arguments
373/// * `metadata` - The metadata to format
374///
375/// # Returns
376/// The formatted metadata block including the `/*! par-term shader metadata ... */` wrapper
377pub fn format_cursor_metadata_block(metadata: &CursorShaderMetadata) -> Result<String, String> {
378    let yaml = serialize_cursor_metadata_to_yaml(metadata)?;
379    Ok(format!("{}\n{}\n*/", METADATA_MARKER, yaml.trim_end()))
380}
381
382/// Update or insert cursor shader metadata in shader source code.
383///
384/// If the shader already has a metadata block, it will be replaced.
385/// If not, the metadata block will be inserted at the beginning of the file.
386///
387/// # Arguments
388/// * `source` - The original shader source code
389/// * `metadata` - The new metadata to insert/update
390///
391/// # Returns
392/// The updated shader source code
393pub fn update_cursor_shader_metadata(
394    source: &str,
395    metadata: &CursorShaderMetadata,
396) -> Result<String, String> {
397    let new_block = format_cursor_metadata_block(metadata)?;
398
399    // Check if there's an existing metadata block
400    if let Some(start_pos) = source.find(METADATA_MARKER) {
401        // Find the end of the metadata block
402        if let Some(end_offset) = source[start_pos..].find("*/") {
403            let end_pos = start_pos + end_offset + 2; // Include the */
404            // Replace the existing block
405            let mut result = String::with_capacity(source.len());
406            result.push_str(&source[..start_pos]);
407            result.push_str(&new_block);
408            result.push_str(&source[end_pos..]);
409            return Ok(result);
410        }
411    }
412
413    // No existing block, insert at the beginning
414    Ok(format!("{}\n\n{}", new_block, source))
415}
416
417/// Update cursor shader metadata in a shader file.
418///
419/// # Arguments
420/// * `path` - Path to the shader file
421/// * `metadata` - The new metadata to write
422///
423/// # Returns
424/// Ok(()) if successful, Err with error message otherwise
425pub fn update_cursor_shader_metadata_file(
426    path: &Path,
427    metadata: &CursorShaderMetadata,
428) -> Result<(), String> {
429    // Read the current file content
430    let source = std::fs::read_to_string(path)
431        .map_err(|e| format!("Failed to read shader file '{}': {}", path.display(), e))?;
432
433    // Update the metadata
434    let updated_source = update_cursor_shader_metadata(&source, metadata)?;
435
436    // Write back to the file
437    std::fs::write(path, updated_source)
438        .map_err(|e| format!("Failed to write shader file '{}': {}", path.display(), e))?;
439
440    log::info!("Updated cursor shader metadata in file: {}", path.display());
441    Ok(())
442}
443
444/// Cache for parsed cursor shader metadata.
445///
446/// Avoids re-parsing shader files on every access while still allowing
447/// invalidation for hot reload scenarios.
448#[derive(Debug, Default)]
449pub struct CursorShaderMetadataCache {
450    /// Cached metadata by shader filename (not full path)
451    cache: HashMap<String, Option<CursorShaderMetadata>>,
452    /// The shaders directory path
453    shaders_dir: Option<PathBuf>,
454}
455
456impl CursorShaderMetadataCache {
457    /// Create a new empty metadata cache.
458    #[allow(dead_code)]
459    pub fn new() -> Self {
460        Self::default()
461    }
462
463    /// Create a new metadata cache with a specific shaders directory.
464    pub fn with_shaders_dir(shaders_dir: PathBuf) -> Self {
465        Self {
466            cache: HashMap::new(),
467            shaders_dir: Some(shaders_dir),
468        }
469    }
470
471    /// Set the shaders directory path.
472    #[allow(dead_code)]
473    pub fn set_shaders_dir(&mut self, shaders_dir: PathBuf) {
474        self.shaders_dir = Some(shaders_dir);
475    }
476
477    /// Get metadata for a cursor shader, loading and caching if necessary.
478    ///
479    /// # Arguments
480    /// * `shader_name` - Filename of the shader (e.g., "cursor_glow.glsl")
481    ///
482    /// # Returns
483    /// * `Some(&CursorShaderMetadata)` if metadata was found
484    /// * `None` if no metadata was found or the shader couldn't be read
485    pub fn get(&mut self, shader_name: &str) -> Option<&CursorShaderMetadata> {
486        // Check if already cached
487        if self.cache.contains_key(shader_name) {
488            return self.cache.get(shader_name).and_then(|m| m.as_ref());
489        }
490
491        // Load and cache
492        let metadata = self.load_metadata(shader_name);
493        self.cache.insert(shader_name.to_string(), metadata);
494        self.cache.get(shader_name).and_then(|m| m.as_ref())
495    }
496
497    /// Get metadata without caching (always reads from disk).
498    ///
499    /// Useful for hot reload scenarios where you want fresh data.
500    #[allow(dead_code)]
501    pub fn get_fresh(&self, shader_name: &str) -> Option<CursorShaderMetadata> {
502        self.load_metadata(shader_name)
503    }
504
505    /// Load metadata from a shader file.
506    fn load_metadata(&self, shader_name: &str) -> Option<CursorShaderMetadata> {
507        let path = self.resolve_shader_path(shader_name)?;
508        parse_cursor_shader_metadata_from_file(&path)
509    }
510
511    /// Resolve a shader name to its full path.
512    fn resolve_shader_path(&self, shader_name: &str) -> Option<PathBuf> {
513        let shader_path = PathBuf::from(shader_name);
514
515        // If it's an absolute path, use it directly
516        if shader_path.is_absolute() && shader_path.exists() {
517            return Some(shader_path);
518        }
519
520        // Otherwise, resolve relative to shaders directory
521        if let Some(ref shaders_dir) = self.shaders_dir {
522            let full_path = shaders_dir.join(shader_name);
523            if full_path.exists() {
524                return Some(full_path);
525            }
526        }
527
528        // Try the default shaders directory
529        let default_path = crate::config::Config::shader_path(shader_name);
530        if default_path.exists() {
531            return Some(default_path);
532        }
533
534        None
535    }
536
537    /// Invalidate cached metadata for a specific shader.
538    ///
539    /// Call this when a shader file has been modified (hot reload).
540    pub fn invalidate(&mut self, shader_name: &str) {
541        self.cache.remove(shader_name);
542        log::debug!(
543            "Invalidated cursor shader metadata cache for: {}",
544            shader_name
545        );
546    }
547
548    /// Invalidate all cached metadata.
549    ///
550    /// Call this when the shaders directory might have changed.
551    #[allow(dead_code)]
552    pub fn invalidate_all(&mut self) {
553        self.cache.clear();
554        log::debug!("Invalidated all cursor shader metadata cache entries");
555    }
556
557    /// Check if metadata is cached for a shader.
558    #[allow(dead_code)]
559    pub fn is_cached(&self, shader_name: &str) -> bool {
560        self.cache.contains_key(shader_name)
561    }
562
563    /// Get the number of cached entries.
564    #[allow(dead_code)]
565    pub fn cache_size(&self) -> usize {
566        self.cache.len()
567    }
568}
569
570#[cfg(test)]
571mod tests {
572    use super::*;
573
574    #[test]
575    fn test_parse_metadata_basic() {
576        let source = r#"/*! par-term shader metadata
577name: "Test Shader"
578author: "Test Author"
579description: "A test shader"
580version: "1.0.0"
581*/
582
583void mainImage(out vec4 fragColor, in vec2 fragCoord) {
584    fragColor = vec4(1.0);
585}
586"#;
587
588        let metadata = parse_shader_metadata(source).expect("Should parse metadata");
589        assert_eq!(metadata.name, Some("Test Shader".to_string()));
590        assert_eq!(metadata.author, Some("Test Author".to_string()));
591        assert_eq!(metadata.description, Some("A test shader".to_string()));
592        assert_eq!(metadata.version, Some("1.0.0".to_string()));
593    }
594
595    #[test]
596    fn test_parse_metadata_with_defaults() {
597        let source = r#"/*! par-term shader metadata
598name: "CRT Effect"
599defaults:
600  animation_speed: 0.5
601  brightness: 0.85
602  full_content: true
603  channel0: "textures/noise.png"
604*/
605
606void mainImage(out vec4 fragColor, in vec2 fragCoord) {
607    fragColor = vec4(1.0);
608}
609"#;
610
611        let metadata = parse_shader_metadata(source).expect("Should parse metadata");
612        assert_eq!(metadata.name, Some("CRT Effect".to_string()));
613        assert_eq!(metadata.defaults.animation_speed, Some(0.5));
614        assert_eq!(metadata.defaults.brightness, Some(0.85));
615        assert_eq!(metadata.defaults.full_content, Some(true));
616        assert_eq!(
617            metadata.defaults.channel0,
618            Some("textures/noise.png".to_string())
619        );
620    }
621
622    #[test]
623    fn test_parse_metadata_not_found() {
624        let source = r#"// Regular shader without metadata
625void mainImage(out vec4 fragColor, in vec2 fragCoord) {
626    fragColor = vec4(1.0);
627}
628"#;
629
630        let metadata = parse_shader_metadata(source);
631        assert!(metadata.is_none());
632    }
633
634    #[test]
635    fn test_parse_metadata_partial() {
636        let source = r#"/*! par-term shader metadata
637name: "Minimal Shader"
638*/
639
640void mainImage(out vec4 fragColor, in vec2 fragCoord) {
641    fragColor = vec4(1.0);
642}
643"#;
644
645        let metadata = parse_shader_metadata(source).expect("Should parse metadata");
646        assert_eq!(metadata.name, Some("Minimal Shader".to_string()));
647        assert!(metadata.author.is_none());
648        assert!(metadata.description.is_none());
649        assert!(metadata.defaults.animation_speed.is_none());
650    }
651
652    #[test]
653    fn test_cache_basic() {
654        let mut cache = ShaderMetadataCache::new();
655
656        // Initially nothing is cached
657        assert!(!cache.is_cached("test.glsl"));
658        assert_eq!(cache.cache_size(), 0);
659
660        // After calling get (even if file doesn't exist), it gets cached as None
661        let _ = cache.get("nonexistent.glsl");
662        assert!(cache.is_cached("nonexistent.glsl"));
663        assert_eq!(cache.cache_size(), 1);
664
665        // Invalidate removes from cache
666        cache.invalidate("nonexistent.glsl");
667        assert!(!cache.is_cached("nonexistent.glsl"));
668        assert_eq!(cache.cache_size(), 0);
669    }
670
671    #[test]
672    fn test_update_metadata_existing_block() {
673        let source = r#"/*! par-term shader metadata
674name: "Old Name"
675version: "1.0.0"
676*/
677
678void mainImage(out vec4 fragColor, in vec2 fragCoord) {
679    fragColor = vec4(1.0);
680}
681"#;
682
683        let new_metadata = ShaderMetadata {
684            name: Some("New Name".to_string()),
685            author: Some("New Author".to_string()),
686            version: Some("2.0.0".to_string()),
687            ..Default::default()
688        };
689
690        let result = super::update_shader_metadata(source, &new_metadata).unwrap();
691
692        // Should contain the new metadata
693        assert!(result.contains("New Name"));
694        assert!(result.contains("New Author"));
695        assert!(result.contains("2.0.0"));
696        // Should NOT contain the old metadata
697        assert!(!result.contains("Old Name"));
698        // Should still contain the shader code
699        assert!(result.contains("void mainImage"));
700    }
701
702    #[test]
703    fn test_update_metadata_no_existing_block() {
704        let source = r#"// Simple shader without metadata
705void mainImage(out vec4 fragColor, in vec2 fragCoord) {
706    fragColor = vec4(1.0);
707}
708"#;
709
710        let new_metadata = ShaderMetadata {
711            name: Some("New Shader".to_string()),
712            version: Some("1.0.0".to_string()),
713            ..Default::default()
714        };
715
716        let result = super::update_shader_metadata(source, &new_metadata).unwrap();
717
718        // Should contain the new metadata at the beginning
719        assert!(result.starts_with("/*! par-term shader metadata"));
720        assert!(result.contains("New Shader"));
721        // Should still contain the shader code
722        assert!(result.contains("void mainImage"));
723        assert!(result.contains("// Simple shader without metadata"));
724    }
725
726    #[test]
727    fn test_format_metadata_block() {
728        let metadata = ShaderMetadata {
729            name: Some("Test Shader".to_string()),
730            author: Some("Test Author".to_string()),
731            description: Some("A test shader".to_string()),
732            version: Some("1.0.0".to_string()),
733            defaults: Default::default(),
734        };
735
736        let block = super::format_metadata_block(&metadata).unwrap();
737
738        assert!(block.starts_with("/*! par-term shader metadata"));
739        assert!(block.ends_with("*/"));
740        assert!(block.contains("Test Shader"));
741        assert!(block.contains("Test Author"));
742    }
743
744    // ========================================================================
745    // Cursor Shader Metadata Tests
746    // ========================================================================
747
748    #[test]
749    fn test_parse_cursor_metadata_basic() {
750        let source = r#"/*! par-term shader metadata
751name: "Cursor Glow"
752author: "Test Author"
753description: "A cursor glow effect"
754version: "1.0.0"
755*/
756
757void mainImage(out vec4 fragColor, in vec2 fragCoord) {
758    fragColor = vec4(1.0);
759}
760"#;
761
762        let metadata =
763            super::parse_cursor_shader_metadata(source).expect("Should parse cursor metadata");
764        assert_eq!(metadata.name, Some("Cursor Glow".to_string()));
765        assert_eq!(metadata.author, Some("Test Author".to_string()));
766        assert_eq!(
767            metadata.description,
768            Some("A cursor glow effect".to_string())
769        );
770        assert_eq!(metadata.version, Some("1.0.0".to_string()));
771    }
772
773    #[test]
774    fn test_parse_cursor_metadata_with_defaults() {
775        let source = r#"/*! par-term shader metadata
776name: "Cursor Trail"
777defaults:
778  animation_speed: 2.0
779  glow_radius: 100.0
780  glow_intensity: 0.5
781  trail_duration: 1.0
782  cursor_color: [255, 128, 0]
783*/
784
785void mainImage(out vec4 fragColor, in vec2 fragCoord) {
786    fragColor = vec4(1.0);
787}
788"#;
789
790        let metadata =
791            super::parse_cursor_shader_metadata(source).expect("Should parse cursor metadata");
792        assert_eq!(metadata.name, Some("Cursor Trail".to_string()));
793        assert_eq!(metadata.defaults.base.animation_speed, Some(2.0));
794        assert_eq!(metadata.defaults.glow_radius, Some(100.0));
795        assert_eq!(metadata.defaults.glow_intensity, Some(0.5));
796        assert_eq!(metadata.defaults.trail_duration, Some(1.0));
797        assert_eq!(metadata.defaults.cursor_color, Some([255, 128, 0]));
798    }
799
800    #[test]
801    fn test_cursor_shader_cache_basic() {
802        let mut cache = super::CursorShaderMetadataCache::new();
803
804        // Initially nothing is cached
805        assert!(!cache.is_cached("cursor_test.glsl"));
806        assert_eq!(cache.cache_size(), 0);
807
808        // After calling get (even if file doesn't exist), it gets cached as None
809        let _ = cache.get("nonexistent_cursor.glsl");
810        assert!(cache.is_cached("nonexistent_cursor.glsl"));
811        assert_eq!(cache.cache_size(), 1);
812
813        // Invalidate removes from cache
814        cache.invalidate("nonexistent_cursor.glsl");
815        assert!(!cache.is_cached("nonexistent_cursor.glsl"));
816        assert_eq!(cache.cache_size(), 0);
817    }
818
819    #[test]
820    fn test_update_cursor_metadata_existing_block() {
821        let source = r#"/*! par-term shader metadata
822name: "Old Cursor"
823version: "1.0.0"
824*/
825
826void mainImage(out vec4 fragColor, in vec2 fragCoord) {
827    fragColor = vec4(1.0);
828}
829"#;
830
831        let new_metadata = super::CursorShaderMetadata {
832            name: Some("New Cursor".to_string()),
833            author: Some("New Author".to_string()),
834            version: Some("2.0.0".to_string()),
835            ..Default::default()
836        };
837
838        let result = super::update_cursor_shader_metadata(source, &new_metadata).unwrap();
839
840        // Should contain the new metadata
841        assert!(result.contains("New Cursor"));
842        assert!(result.contains("New Author"));
843        assert!(result.contains("2.0.0"));
844        // Should NOT contain the old metadata
845        assert!(!result.contains("Old Cursor"));
846        // Should still contain the shader code
847        assert!(result.contains("void mainImage"));
848    }
849
850    #[test]
851    fn test_update_cursor_metadata_no_existing_block() {
852        let source = r#"// Cursor shader without metadata
853void mainImage(out vec4 fragColor, in vec2 fragCoord) {
854    fragColor = vec4(1.0);
855}
856"#;
857
858        let new_metadata = super::CursorShaderMetadata {
859            name: Some("New Cursor Shader".to_string()),
860            version: Some("1.0.0".to_string()),
861            ..Default::default()
862        };
863
864        let result = super::update_cursor_shader_metadata(source, &new_metadata).unwrap();
865
866        // Should contain the new metadata at the beginning
867        assert!(result.starts_with("/*! par-term shader metadata"));
868        assert!(result.contains("New Cursor Shader"));
869        // Should still contain the shader code
870        assert!(result.contains("void mainImage"));
871        assert!(result.contains("// Cursor shader without metadata"));
872    }
873
874    #[test]
875    fn test_format_cursor_metadata_block() {
876        use crate::CursorShaderConfig;
877
878        let metadata = super::CursorShaderMetadata {
879            name: Some("Test Cursor".to_string()),
880            author: Some("Test Author".to_string()),
881            description: Some("A test cursor shader".to_string()),
882            version: Some("1.0.0".to_string()),
883            defaults: CursorShaderConfig {
884                glow_radius: Some(50.0),
885                glow_intensity: Some(0.8),
886                ..Default::default()
887            },
888        };
889
890        let block = super::format_cursor_metadata_block(&metadata).unwrap();
891
892        assert!(block.starts_with("/*! par-term shader metadata"));
893        assert!(block.ends_with("*/"));
894        assert!(block.contains("Test Cursor"));
895        assert!(block.contains("Test Author"));
896        assert!(block.contains("glow_radius"));
897        assert!(block.contains("glow_intensity"));
898    }
899}