/* GREX - A graphical frontend for creating and managing Rex project files. Copyright (C) 2026 SILO GROUP, LLC. Written by Chris Punches This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ #include "models/project.h" #include namespace grex { namespace fs = std::filesystem; void Project::report_status(const std::string& msg) { if (status_cb) status_cb(msg, status_cb_data); } fs::path Project::resolved_project_root() const { auto val = config.get("project_root"); if (val.empty() || !resolver.can_resolve(val)) return {}; return resolver.resolve(val); } fs::path Project::resolved_units_dir() const { auto root = resolved_project_root(); if (root.empty()) return {}; auto units = config.get("units_path"); if (units.empty()) return {}; return root / units; } fs::path Project::resolved_shells_path() const { auto root = resolved_project_root(); if (root.empty()) return {}; auto sp = config.get("shells_path"); if (sp.empty()) return {}; return root / sp; } Unit* Project::find_unit(const std::string& unit_name) { for (auto& uf : unit_files) { auto* u = uf.find_unit(unit_name); if (u) return u; } return nullptr; } UnitFile* Project::find_unit_file(const std::string& unit_name) { for (auto& uf : unit_files) { if (uf.find_unit(unit_name)) return &uf; } return nullptr; } bool Project::is_unit_name_taken(const std::string& name, const Unit* exclude) const { for (auto& uf : unit_files) { for (auto& u : uf.units) { if (u.name == name && &u != exclude) return true; } } return false; } void Project::load_all_units() { auto u_dir = resolved_units_dir(); if (u_dir.empty()) { report_status("Error: units path not resolved"); return; } if (!fs::is_directory(u_dir)) { report_status("Error: units path is not a directory: " + u_dir.string()); return; } std::vector loaded; int total_units = 0; int file_count = 0; int duplicates = 0; std::set seen_names; for (auto& entry : fs::directory_iterator(u_dir)) { if (entry.path().extension() == ".units") { try { auto uf = UnitFile::load(entry.path()); // Remove units with duplicate names auto it = uf.units.begin(); while (it != uf.units.end()) { if (seen_names.count(it->name)) { report_status("Warning: duplicate unit '" + it->name + "' in " + entry.path().filename().string() + " — skipped"); it = uf.units.erase(it); duplicates++; } else { seen_names.insert(it->name); ++it; } } total_units += (int)uf.units.size(); file_count++; loaded.push_back(std::move(uf)); } catch (const std::exception& e) { report_status("Error: failed to load " + entry.path().filename().string() + ": " + e.what()); } } } unit_files = std::move(loaded); auto msg = "Loaded " + std::to_string(total_units) + " units from " + std::to_string(file_count) + " files at '" + u_dir.string() + "'"; if (duplicates > 0) msg += " (" + std::to_string(duplicates) + " duplicates skipped)"; report_status(msg); } Project Project::load(const fs::path& cfg_path) { Project proj; proj.config_path = fs::canonical(cfg_path); proj.config = RexConfig::load(proj.config_path); // scan all config string values for variables, populate from environment proj.resolver.scan_and_populate(proj.config.all_string_values()); // auto-load shells if path resolves auto sp = proj.resolved_shells_path(); if (!sp.empty() && fs::exists(sp)) { try { proj.shells = ShellsFile::load(sp); } catch (const std::exception& e) { // no status_cb set yet at load time — caller handles } } // auto-load all units if path resolves proj.load_all_units(); return proj; } void Project::load_plan(const fs::path& plan_path) { plans.clear(); try { plans.push_back(Plan::load(plan_path)); report_status("Loaded plan: " + plan_path.filename().string()); } catch (const std::exception& e) { report_status("Error: failed to load plan: " + std::string(e.what())); throw; } } void Project::load_shells(const fs::path& shells_path) { shells = ShellsFile(); try { shells = ShellsFile::load(shells_path); report_status("Loaded shells: " + shells_path.filename().string()); } catch (const std::exception& e) { report_status("Error: failed to load shells: " + std::string(e.what())); throw; } } void Project::reload_shells() { auto sp = resolved_shells_path(); if (sp.empty()) { report_status("Error: shells path not resolved"); return; } if (!fs::exists(sp)) { report_status("Error: shells file not found: " + sp.string()); return; } try { shells = ShellsFile::load(sp); report_status("Loaded shells: " + sp.filename().string()); } catch (const std::exception& e) { report_status("Error: failed to load shells: " + std::string(e.what())); } } void Project::save_config() { config.save(config_path); report_status("Saved config: " + config_path.filename().string()); } void Project::save_plans() { for (auto& p : plans) { p.save(); report_status("Saved plan: " + p.name); } } void Project::save_shells() { if (shells.filepath.empty()) { report_status("Error: no shells loaded to save"); return; } shells.save(); report_status("Saved shells: " + shells.filepath.filename().string()); } void Project::open_config(const fs::path& new_config_path) { config_path = fs::canonical(new_config_path); config = RexConfig::load(config_path); resolver = VarResolver(); resolver.scan_and_populate(config.all_string_values()); // reload shells shells = ShellsFile(); auto sp = resolved_shells_path(); if (!sp.empty() && fs::exists(sp)) { try { shells = ShellsFile::load(sp); report_status("Loaded shells: " + sp.filename().string()); } catch (const std::exception& e) { report_status(std::string("Error loading shells: ") + e.what()); } } // reload units load_all_units(); // keep any loaded plans but note they may reference stale units now report_status("Opened config: " + config_path.filename().string()); } void Project::close_config() { config = RexConfig(); config_path.clear(); resolver = VarResolver(); plans.clear(); unit_files.clear(); shells = ShellsFile(); report_status("Config closed"); } }