memscope_rs/render_engine/dashboard/renderer/
template_registry.rs1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct DashboardTemplate {
18 pub id: String,
20 pub name: String,
22 pub description: String,
24 pub template_path: PathBuf,
26 #[serde(default)]
28 pub kind: TemplateKind,
29}
30
31#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
33pub enum TemplateKind {
34 #[default]
36 BuiltIn,
37 External,
39}
40
41pub struct TemplateRegistry {
43 templates: HashMap<String, DashboardTemplate>,
45 handlebars: Handlebars<'static>,
47 external_base: Option<PathBuf>,
49}
50
51impl TemplateRegistry {
52 pub fn new() -> Self {
54 let mut handlebars = Handlebars::new();
55 register_helpers(&mut handlebars);
61 Self {
62 templates: HashMap::new(),
63 handlebars,
64 external_base: None,
65 }
66 }
67
68 pub fn with_built_in_templates(
76 templates_dir: &Path,
77 ) -> Result<Self, Box<dyn std::error::Error>> {
78 let _ = templates_dir; 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 pub fn set_external_base(&mut self, path: PathBuf) {
105 self.external_base = Some(path);
106 }
107
108 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 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 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 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 pub fn templates(&self) -> &HashMap<String, DashboardTemplate> {
188 &self.templates
189 }
190
191 pub fn get_template(&self, id: &str) -> Option<&DashboardTemplate> {
193 self.templates.get(id)
194 }
195
196 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 pub fn template_ids(&self) -> Vec<String> {
223 self.templates.keys().cloned().collect()
224 }
225
226 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 }
256}