Skip to main content

memscope_rs/render_engine/dashboard/renderer/
template_registry.rs

1//! Template registry - manages dashboard template loading and selection
2//!
3//! This module provides a centralized registry for dashboard templates,
4//! supporting both built-in templates and external template files from
5//! the templetes/ directory.
6
7use handlebars::Handlebars;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::fs;
11use std::path::{Path, PathBuf};
12
13use super::helpers::register_helpers;
14
15/// Registered dashboard template
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct DashboardTemplate {
18    /// Unique template identifier
19    pub id: String,
20    /// Human-readable display name
21    pub name: String,
22    /// Description of what this template visualizes
23    pub description: String,
24    /// File path to the Handlebars HTML template
25    pub template_path: PathBuf,
26    /// Whether this is a built-in or external template
27    #[serde(default)]
28    pub kind: TemplateKind,
29}
30
31/// Template source classification
32#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
33pub enum TemplateKind {
34    /// Built-in template bundled in the crate
35    #[default]
36    BuiltIn,
37    /// External template loaded from file system
38    External,
39}
40
41/// Template registry - holds all available dashboard templates
42pub struct TemplateRegistry {
43    /// Map of template ID to DashboardTemplate
44    templates: HashMap<String, DashboardTemplate>,
45    /// Handlebars instance with all templates registered
46    handlebars: Handlebars<'static>,
47    /// Base directory for external templates
48    external_base: Option<PathBuf>,
49}
50
51impl TemplateRegistry {
52    /// Create a new empty template registry
53    pub fn new() -> Self {
54        let mut handlebars = Handlebars::new();
55        // Register all Handlebars helpers (format_bytes, eq, len, risk_class, ...)
56        // so templates rendered through this registry can use them. Without this,
57        // helper calls like {{len thread_policies}} would be misinterpreted as
58        // field accesses and fail with "Cannot access array/vector with string
59        // index" errors.
60        register_helpers(&mut handlebars);
61        Self {
62            templates: HashMap::new(),
63            handlebars,
64            external_base: None,
65        }
66    }
67
68    /// Create a registry with the single built-in merged dashboard template pre-loaded.
69    ///
70    /// The merged template lives at
71    /// `src/render_engine/dashboard/templates/dashboard_unified.html` and bundles all
72    /// eight dashboard modes (Overview, Threads, Async, Task Graph, Variables,
73    /// Passports, FFI, Unsafe/Time) into one HTML file with a side-bar mode switcher.
74    /// The `templates_dir` argument is kept for API compatibility but no longer used.
75    pub fn with_built_in_templates(
76        templates_dir: &Path,
77    ) -> Result<Self, Box<dyn std::error::Error>> {
78        let _ = templates_dir; // unused: single merged template location is fixed
79        let mut registry = Self::new();
80
81        let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
82        let unified_path = manifest_dir
83            .join("src")
84            .join("render_engine")
85            .join("dashboard")
86            .join("templates")
87            .join("dashboard_unified.html");
88        if unified_path.exists() {
89            registry.register_template(DashboardTemplate {
90                id: "dashboard_unified".to_string(),
91                name: "Unified Dashboard".to_string(),
92                description: "Merged multi-mode dashboard (Overview, Threads, Async, Task Graph, Variables, Passports, FFI, Unsafe/Time)".to_string(),
93                template_path: unified_path,
94                kind: TemplateKind::BuiltIn,
95            })?;
96        } else {
97            tracing::warn!("Unified dashboard template not found: {:?}", unified_path);
98        }
99
100        Ok(registry)
101    }
102
103    /// Set the base directory for external templates
104    pub fn set_external_base(&mut self, path: PathBuf) {
105        self.external_base = Some(path);
106    }
107
108    /// Load external templates from the templetes/ directory
109    ///
110    /// Scans subdirectories for code.html files and registers them.
111    /// Each subdirectory becomes a template with the directory name as the ID.
112    pub fn load_external_templates(
113        &mut self,
114        templetes_dir: &Path,
115    ) -> Result<(), Box<dyn std::error::Error>> {
116        if !templetes_dir.exists() {
117            tracing::warn!("Templetes directory not found: {:?}", templetes_dir);
118            return Ok(());
119        }
120
121        let entries = fs::read_dir(templetes_dir)?;
122        for entry in entries {
123            let entry = entry?;
124            let dir_path = entry.path();
125
126            if !dir_path.is_dir() {
127                continue;
128            }
129
130            let code_html = dir_path.join("code.html");
131            if !code_html.exists() {
132                continue;
133            }
134
135            // Derive template ID from directory name (kebab-case → snake_case)
136            let dir_name = dir_path
137                .file_name()
138                .and_then(|n| n.to_str())
139                .unwrap_or("unknown");
140            let id = dir_name.replace('-', "_").replace(" ", "_");
141
142            // Derive display name from directory name
143            let name = dir_name
144                .split(['_', '-'])
145                .filter(|s| !s.is_empty())
146                .map(|s| {
147                    s.chars().next().unwrap_or(' ').to_uppercase().to_string()
148                        + &s[1..].to_lowercase()
149                })
150                .collect::<Vec<_>>()
151                .join(" ");
152
153            self.register_template(DashboardTemplate {
154                id: format!("ext_{}", id),
155                name: format!("Professional: {}", name),
156                description: format!("External template: {}", name),
157                template_path: code_html,
158                kind: TemplateKind::External,
159            })?;
160        }
161
162        tracing::info!(
163            "Loaded {} external templates from {:?}",
164            self.templates
165                .iter()
166                .filter(|(_, t)| t.kind == TemplateKind::External)
167                .count(),
168            templetes_dir
169        );
170
171        Ok(())
172    }
173
174    /// Register a single template
175    fn register_template(
176        &mut self,
177        template: DashboardTemplate,
178    ) -> Result<(), Box<dyn std::error::Error>> {
179        let id = template.id.clone();
180        self.handlebars
181            .register_template_file(&id, &template.template_path)?;
182        self.templates.insert(id, template);
183        Ok(())
184    }
185
186    /// Get all available templates
187    pub fn templates(&self) -> &HashMap<String, DashboardTemplate> {
188        &self.templates
189    }
190
191    /// Get a specific template by ID
192    pub fn get_template(&self, id: &str) -> Option<&DashboardTemplate> {
193        self.templates.get(id)
194    }
195
196    /// Render a dashboard using the specified template
197    pub fn render(
198        &self,
199        template_id: &str,
200        data: &serde_json::Value,
201    ) -> Result<String, Box<dyn std::error::Error>> {
202        let template = self.templates.get(template_id).ok_or_else(|| {
203            format!(
204                "Template '{}' not found. Available: {:?}",
205                template_id,
206                self.template_ids()
207            )
208        })?;
209
210        let result = self.handlebars.render(template_id, data);
211        match result {
212            Ok(html) => Ok(html),
213            Err(e) => Err(format!(
214                "Failed to render template '{}': {} ({:?})",
215                template.name, e, template.template_path
216            )
217            .into()),
218        }
219    }
220
221    /// List all available template IDs
222    pub fn template_ids(&self) -> Vec<String> {
223        self.templates.keys().cloned().collect()
224    }
225
226    /// Count of available templates
227    pub fn count(&self) -> usize {
228        self.templates.len()
229    }
230}
231
232impl Default for TemplateRegistry {
233    fn default() -> Self {
234        Self::new()
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    #[test]
243    fn test_registry_creation() {
244        let registry = TemplateRegistry::new();
245        assert_eq!(registry.count(), 0);
246        assert!(registry.template_ids().is_empty());
247    }
248
249    #[test]
250    fn test_register_and_retrieve() {
251        let registry = TemplateRegistry::new();
252        let templates = registry.templates();
253        assert!(templates.is_empty());
254        // Cannot fully test without a real file, but structure is verified
255    }
256}