use anyhow::{Context, Ok, Result};
use espforge_configuration::EspforgeConfiguration;
use espforge_esp32metadata::BoardDatabase;
use std::fs;
use std::path::Path;
pub fn copy_app_rust_to_src(project_dir: &Path, src_dir: &Path) -> Result<()> {
let app_rust_dir = project_dir.join("app").join("rust");
if !app_rust_dir.exists() {
return Ok(());
}
if !src_dir.exists() {
fs::create_dir_all(src_dir).context("Failed to create src directory")?;
}
for entry in fs::read_dir(&app_rust_dir).context("Failed to read app/rust directory")? {
let path = entry.context("Failed to read directory entry")?.path();
if path.extension().and_then(|s| s.to_str()) == Some("rs") {
let dest = src_dir.join(path.file_name().unwrap());
if !dest.exists() {
fs::copy(&path, &dest)
.with_context(|| format!("Failed to copy {} to src/", path.display()))?;
println!(
" ✓ Copied {} → src/",
path.file_name().unwrap().to_string_lossy()
);
}
}
}
Ok(())
}
pub fn generate_wokwi_config(project_dir: &Path, model: &EspforgeConfiguration) -> Result<()> {
let diagram_json = project_dir.join("diagram.json");
if !diagram_json.exists() {
return Ok(());
}
let db = BoardDatabase::load();
let chip = model.get_chip();
let board_id = db
.wokwi_board(chip)
.ok_or_else(|| anyhow::anyhow!("Unsupported chip variant for Wokwi: {}", chip))?;
let content =
fs::read_to_string(&diagram_json).context("Failed to read diagram.json template")?;
if !content.contains("PLACEHOLDER_WOKWI_BOARD") {
let dest_path = project_dir.join("diagram.json");
if diagram_json != dest_path {
fs::copy(&diagram_json, &dest_path)?;
}
return Ok(());
}
let mut processed = content.replace("PLACEHOLDER_WOKWI_BOARD", &board_id);
if let Some(gnd_tl) = db.gnd_top_left(chip) {
processed = processed.replace("PLACEHOLDER_GND_TOP_LEFT", &gnd_tl);
}
if let Some(gnd_tr) = db.gnd_top_right(chip) {
processed = processed.replace("PLACEHOLDER_GND_TOP_RIGHT", &gnd_tr);
}
if let Some(gnd_bl) = db.gnd_bottom_left(chip) {
processed = processed.replace("PLACEHOLDER_GND_BOTTOM_LEFT", &gnd_bl);
}
if let Some(gnd_br) = db.gnd_bottom_right(chip) {
processed = processed.replace("PLACEHOLDER_GND_BOTTOM_RIGHT", &gnd_br);
}
serde_json::from_str::<serde_json::Value>(&processed)
.context("Processed diagram.json is not valid JSON")?;
fs::write(project_dir.join("diagram.json"), processed)
.context("Failed to write processed diagram.json")?;
println!(" ✓ Processed diagram.json for {}", board_id);
Ok(())
}
pub fn copy_wokwi_files(project_dir: &Path) -> Result<()> {
let chip_wasm = project_dir.join("chip.wasm");
let chip_json = project_dir.join("chip.json");
if chip_wasm.exists() && chip_json.exists() {
update_wokwi_config_for_chip(project_dir)?;
}
Ok(())
}
fn update_wokwi_config_for_chip(project_dir: &Path) -> Result<()> {
let wokwi_path = project_dir.join("wokwi.toml");
if !wokwi_path.exists() {
return Ok(());
}
let content = fs::read_to_string(&wokwi_path).context("Failed to read wokwi.toml")?;
let mut doc = content
.parse::<toml_edit::DocumentMut>()
.context("Failed to parse wokwi.toml")?;
let chips = doc.entry("chip").or_insert(toml_edit::Item::ArrayOfTables(
toml_edit::ArrayOfTables::new(),
));
if let Some(arr) = chips.as_array_of_tables_mut() {
let exists = arr
.iter()
.any(|t| t.get("name").and_then(|s| s.as_str()) == Some("chip"));
if !exists {
let mut table = toml_edit::Table::new();
table.insert("name", toml_edit::value("chip"));
table.insert("binary", toml_edit::value("chip.wasm"));
arr.push(table);
fs::write(wokwi_path, doc.to_string())?;
println!(" Updated wokwi.toml to include chip.wasm");
}
}
Ok(())
}
pub fn inject_app_code(src_dir: &Path) -> Result<Vec<String>> {
let mut module_names = vec![];
if src_dir.exists() {
for entry in fs::read_dir(src_dir).context("Failed to read src directory")? {
let entry = entry.context("Failed to read directory entry")?;
let path = entry.path();
if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("rs") {
let file_name_str = path.file_name().unwrap().to_string_lossy().to_string();
if let Some(module_name) = file_name_str.strip_suffix(".rs")
&& module_name != "lib"
&& module_name != "generated"
&& module_name != "main"
&& !module_name.starts_with("bin")
{
module_names.push(module_name.to_string());
}
}
}
}
module_names.sort();
module_names.dedup();
Ok(module_names)
}
pub fn setup_wifi_env_config(project_dir: &Path, model: &EspforgeConfiguration) -> Result<()> {
let Some(wifi) = model.esp32.as_ref().and_then(|e| e.wifi.as_ref()) else {
return Ok(());
};
let config_path = project_dir.join(".cargo/config.toml");
if let Some(parent) = config_path.parent() {
fs::create_dir_all(parent).context("Failed to create .cargo directory")?;
}
let content = if config_path.exists() {
fs::read_to_string(&config_path).context("Failed to read .cargo/config.toml")?
} else {
String::new()
};
let mut doc = content
.parse::<toml_edit::DocumentMut>()
.context("Failed to parse .cargo/config.toml")?;
if !doc.contains_key("env") {
doc.insert("env", toml_edit::Item::Table(toml_edit::Table::new()));
}
let env_table = doc
.get_mut("env")
.and_then(|t| t.as_table_mut())
.context("Failed to get [env] table")?;
env_table.insert("WIFI_SSID", toml_edit::value(&*wifi.ssid));
env_table.insert("WIFI_PASSWORD", toml_edit::value(&*wifi.password));
fs::write(&config_path, doc.to_string()).context("Failed to write .cargo/config.toml")?;
println!(
" ✓ Injected WIFI_SSID / WIFI_PASSWORD into .cargo/config.toml (build-time env vars)"
);
Ok(())
}