use anyhow::Result;
use colored::*;
use std::env;
use std::fs;
use std::path::PathBuf;
use crate::templating::TemplateEngine;
use crate::commands::new::{create_ffi_web_crate, create_workspace_cargo_toml};
fn find_templates_dir() -> Result<PathBuf> {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let dev_templates = manifest_dir.join("templates");
if dev_templates.exists() {
return Ok(dev_templates);
}
let exe_path = env::current_exe()?;
if let Some(exe_dir) = exe_path.parent() {
let share_templates = exe_dir.join("../share/jffi/templates");
if share_templates.exists() {
return Ok(share_templates.canonicalize()?);
}
let local_templates = exe_dir.join("templates");
if local_templates.exists() {
return Ok(local_templates);
}
let sibling_templates = exe_dir.join("../templates");
if sibling_templates.exists() {
return Ok(sibling_templates.canonicalize()?);
}
}
Ok(dev_templates)
}
pub fn add_platform(platform: &str) -> Result<()> {
println!("{}", format!("➕ Adding {} platform...", platform).bright_green().bold());
println!();
let platform_dir = std::path::PathBuf::from("platforms").join(platform);
if platform_dir.exists() {
anyhow::bail!("Platform {} already exists", platform);
}
let mut config = crate::config::load_config()?;
let name = config.package.name.clone();
let templates_dir = find_templates_dir()?;
let engine = TemplateEngine::new(&templates_dir);
let template_name = "hello";
let template = engine.get_template(template_name)?
.ok_or_else(|| anyhow::anyhow!("Template '{}' not found", template_name))?;
if !template.manifest.platforms.supported.contains(&platform.to_string()) {
anyhow::bail!(
"Platform '{}' is not supported by template '{}'",
platform,
template_name
);
}
let platforms_dir = std::path::PathBuf::from("platforms");
fs::create_dir_all(&platforms_dir)?;
let mut context = std::collections::HashMap::new();
context.insert("name".to_string(), name.clone());
context.insert("name_pascal".to_string(), to_pascal_case(&name));
context.insert("name_snake".to_string(), name.replace("-", "_"));
context.insert("name_package".to_string(), name.replace("-", ""));
context.insert("greeting".to_string(), "Hello from JFFI".to_string());
use rand::Rng;
let mut rng = rand::thread_rng();
for i in 1..=30 {
let uuid = format!(
"{:08X}{:08X}{:08X}{:08X}",
rng.gen::<u32>(),
rng.gen::<u32>(),
rng.gen::<u32>(),
rng.gen::<u32>()
);
context.insert(format!("UUID{}", i), uuid);
}
for i in 1..=5 {
let guid = format!(
"{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
rng.gen::<u32>(),
rng.gen::<u16>(),
rng.gen::<u16>(),
rng.gen::<u16>(),
rng.gen::<u64>() & 0xFFFFFFFFFFFF
);
context.insert(format!("GUID{}", i), guid);
}
for (key, value) in &template.manifest.variables {
context.insert(key.clone(), value.clone());
}
let platform_template_dir = template.path.join("platforms").join(platform);
if !platform_template_dir.exists() {
anyhow::bail!(
"Platform '{}' not found in template '{}'",
platform,
template_name
);
}
let platform_dest_dir = platforms_dir.join(platform);
fs::create_dir_all(&platform_dest_dir)?;
copy_dir_with_render(&platform_template_dir, &platform_dest_dir, &context)?;
if platform == "web" {
let project_dir = PathBuf::from(".");
let all_platforms: Vec<&str> = config.platforms.enabled.iter().map(|s| s.as_str()).collect();
let mut updated_platforms = all_platforms.clone();
if !updated_platforms.contains(&"web") {
updated_platforms.push("web");
}
create_ffi_web_crate(&project_dir, &name)?;
create_workspace_cargo_toml(&project_dir, &updated_platforms)?;
println!(" {} ffi-web/", "✓".green());
}
if !config.platforms.enabled.contains(&platform.to_string()) {
config.platforms.enabled.push(platform.to_string());
crate::config::save_config(&config)?;
}
println!(" {} platforms/{}/", "✓".green(), platform);
println!();
println!("{}", format!("✅ {} platform added", platform).green());
println!();
println!("Next steps:");
println!(" {} - Compile the core and generate bindings", format!("jffi build --platform {}", platform).bright_cyan());
println!(" {} - Build and launch on simulator/device", format!("jffi run --platform {}", platform).bright_cyan());
println!(" {} - Watch mode (auto-rebuild on changes)", format!("jffi dev --platform {}", platform).bright_cyan());
Ok(())
}
fn copy_dir_with_render(
src: &PathBuf,
dst: &PathBuf,
context: &std::collections::HashMap<String, String>,
) -> Result<()> {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let path = entry.path();
let file_name = path.file_name().unwrap_or_default().to_string_lossy();
let rendered_name = render_template(&file_name, context);
let dest_path = dst.join(rendered_name);
if path.is_dir() {
copy_dir_with_render(&path, &dest_path, context)?;
} else {
match fs::read_to_string(&path) {
Ok(content) => {
let rendered = render_template(&content, context);
fs::write(&dest_path, rendered)?;
}
Err(_) => {
fs::copy(&path, &dest_path)?;
}
}
}
}
Ok(())
}
fn render_template(template: &str, context: &std::collections::HashMap<String, String>) -> String {
let mut result = template.to_string();
for (key, value) in context {
let placeholder = format!("{{{{{}}}}}", key);
result = result.replace(&placeholder, value);
}
result
}
fn to_pascal_case(s: &str) -> String {
s.split('-')
.map(|part| {
let mut chars = part.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
})
.collect()
}