use include_dir::{include_dir, Dir}; use std::path::Path; use anyhow::{Result, Context, bail}; use tera::Tera; use std::fs; use colored::*; // Embed template directories at compile time static BASIC_TEMPLATE: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates/basic"); static TESTING_TEMPLATE: Dir = include_dir!("$CARGO_MANIFEST_DIR/templates/testing"); pub struct TemplateManager { #[allow(dead_code)] tera: Tera, } #[allow(dead_code)] pub struct TemplateInfo { pub name: String, pub description: String, pub file_count: usize, pub line_count: usize, pub test_count: usize, pub is_default: bool, } pub struct TemplateContext { pub project_name: String, pub package_name: String, pub creation_date: String, pub weevil_version: String, pub template_name: String, } impl TemplateManager { pub fn new() -> Result { let tera = Tera::default(); Ok(Self { tera }) } pub fn template_exists(&self, name: &str) -> bool { matches!(name, "basic" | "testing") } pub fn list_templates(&self) -> Vec { vec![ " basic - Minimal FTC project (default)".to_string(), " testing - Testing showcase with examples".to_string(), ] } #[allow(dead_code)] pub fn get_template_info_all(&self) -> Result> { Ok(vec![ TemplateInfo { name: "basic".to_string(), description: "Minimal FTC project structure".to_string(), file_count: 10, line_count: 50, test_count: 0, is_default: true, }, TemplateInfo { name: "testing".to_string(), description: "Professional testing showcase with examples".to_string(), file_count: 30, line_count: 2500, test_count: 45, is_default: false, }, ]) } #[allow(dead_code)] pub fn show_template_info(&self, name: &str) -> Result<()> { let info = match name { "basic" => TemplateInfo { name: "basic".to_string(), description: "Minimal FTC project with clean structure".to_string(), file_count: 10, line_count: 50, test_count: 0, is_default: true, }, "testing" => TemplateInfo { name: "testing".to_string(), description: "Comprehensive testing showcase demonstrating professional robotics software engineering practices.".to_string(), file_count: 30, line_count: 2500, test_count: 45, is_default: false, }, _ => bail!("Unknown template: {}", name), }; println!("{}", format!("Template: {}", info.name).bright_cyan().bold()); println!(); println!("{}", "Description:".bright_white().bold()); println!(" {}", info.description); println!(); if info.name == "testing" { println!("{}", "Features:".bright_white().bold()); println!(" • Three complete subsystems with full test coverage"); println!(" • Hardware abstraction layer with mocks"); println!(" • 45 passing tests (unit, integration, system)"); println!(" • Comprehensive documentation (6 files)"); println!(" • Ready to use as learning material"); println!(); } println!("{}", "Files included:".bright_white().bold()); println!(" {} files", info.file_count); println!(" ~{} lines of code", info.line_count); if info.test_count > 0 { println!(" {} tests", info.test_count); } println!(); println!("{}", "Example usage:".bright_white().bold()); println!(" weevil new my-robot --template {}", info.name); Ok(()) } pub fn extract_template( &self, template_name: &str, output_dir: &Path, context: &TemplateContext, ) -> Result { let template_dir = match template_name { "basic" => &BASIC_TEMPLATE, "testing" => &TESTING_TEMPLATE, _ => bail!("Unknown template: {}", template_name), }; // Extract all files from template let file_count = self.extract_dir_recursively(template_dir, output_dir, "", context)?; Ok(file_count) } fn extract_dir_recursively( &self, source: &Dir, output_base: &Path, relative_path: &str, context: &TemplateContext, ) -> Result { let mut file_count = 0; // Process files in current directory for file in source.files() { let file_path = file.path(); let file_name = file_path.file_name().unwrap().to_string_lossy(); let output_path = if relative_path.is_empty() { output_base.join(&*file_name) } else { output_base.join(relative_path).join(&*file_name) }; // Create parent directory if needed if let Some(parent) = output_path.parent() { fs::create_dir_all(parent)?; } // Process file based on extension if file_name.ends_with(".template") { // Template file - process variables let contents = file.contents_utf8() .context("Template file must be UTF-8")?; let processed = self.process_template_string(contents, context)?; // Remove .template extension from output let output_name = file_name.trim_end_matches(".template"); let final_path = output_path.with_file_name(output_name); fs::write(final_path, processed)?; file_count += 1; } else { // Regular file - copy as-is fs::write(&output_path, file.contents())?; file_count += 1; } } // Process subdirectories for dir in source.dirs() { let dir_name = dir.path().file_name().unwrap().to_string_lossy(); let new_relative = if relative_path.is_empty() { dir_name.to_string() } else { format!("{}/{}", relative_path, dir_name) }; file_count += self.extract_dir_recursively(dir, output_base, &new_relative, context)?; } Ok(file_count) } fn process_template_string( &self, template: &str, context: &TemplateContext, ) -> Result { let processed = template .replace("{{PROJECT_NAME}}", &context.project_name) .replace("{{PACKAGE_NAME}}", &context.package_name) .replace("{{CREATION_DATE}}", &context.creation_date) .replace("{{WEEVIL_VERSION}}", &context.weevil_version) .replace("{{TEMPLATE_NAME}}", &context.template_name); Ok(processed) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_template_manager_creation() { let mgr = TemplateManager::new(); assert!(mgr.is_ok()); } #[test] fn test_template_exists() { let mgr = TemplateManager::new().unwrap(); assert!(mgr.template_exists("basic")); assert!(mgr.template_exists("testing")); assert!(!mgr.template_exists("nonexistent")); } #[test] fn test_list_templates() { let mgr = TemplateManager::new().unwrap(); let templates = mgr.list_templates(); assert_eq!(templates.len(), 2); } }