#include "config_manager.h" #include "yaml_parser.h" #include #include #include #include #include #include namespace radiantwave::config { // ─── Constructor & Destructor ────────────────────────────────────────── ConfigManager::ConfigManager() = default; ConfigManager::~ConfigManager() { shutdown(); } // ─── Path Resolution ─────────────────────────────────────────────────── std::string ConfigManager::resolvePath(const std::string& raw_path) const { std::string path = raw_path; if (path.empty()) { const char* home = std::getenv("HOME"); if (!home) { struct passwd* pw = getpwuid(getuid()); if (pw) { home = pw->pw_dir; } } if (home) { path = std::string(home) + "/.local/radiantwave/config.yaml"; } else { path = "/tmp/radiantwave/config.yaml"; } } else if (path.front() == '~') { const char* home = std::getenv("HOME"); if (!home) { struct passwd* pw = getpwuid(getuid()); if (pw) { home = pw->pw_dir; } } if (home) { path = std::string(home) + path.substr(1); } } return path; } // ─── Initialization ──────────────────────────────────────────────────── bool ConfigManager::initialize(const std::string& custom_path) { file_path_ = resolvePath(custom_path); // Create parent directories if they don't exist try { std::filesystem::path p(file_path_); if (p.has_parent_path()) { std::filesystem::create_directories(p.parent_path()); } } catch (const std::exception& e) { std::cerr << "[Config] Failed to create directories: " << e.what() << std::endl; return false; } // Load and merge defaults without overwriting existing settings loadAndMergeDefaults(); // Store initial modification timestamp try { if (std::filesystem::exists(file_path_)) { last_write_time_ = std::filesystem::last_write_time(file_path_); } } catch (...) {} // Start live hot-reload background watcher startWatcher(); return true; } void ConfigManager::shutdown() { stopWatcher(); } // ─── Read / Write Operations ─────────────────────────────────────────── AppConfig ConfigManager::get() const { std::lock_guard lock(mutex_); return config_; } bool ConfigManager::update(std::function modifier) { std::vector callbacks_to_notify; AppConfig updated_config; bool success = false; { std::lock_guard lock(mutex_); modifier(config_); updated_config = config_; success = saveUnlocked(); } if (success) { std::lock_guard cb_lock(callback_mutex_); callbacks_to_notify = callbacks_; } for (const auto& cb : callbacks_to_notify) { if (cb) cb(updated_config); } return success; } bool ConfigManager::save() { std::lock_guard lock(mutex_); return saveUnlocked(); } bool ConfigManager::saveUnlocked() { std::string content = serializeConfigToYaml(config_); std::ofstream file(file_path_, std::ios::out | std::ios::trunc); if (!file.is_open()) { std::cerr << "[Config] Failed to open file for writing: " << file_path_ << std::endl; return false; } file << content; file.close(); // Update timestamp so watcher doesn't trigger a duplicate hot-reload event try { if (std::filesystem::exists(file_path_)) { last_write_time_ = std::filesystem::last_write_time(file_path_); } } catch (...) {} return true; } bool ConfigManager::reload() { std::ifstream file(file_path_); if (!file.is_open()) { return false; } std::stringstream ss; ss << file.rdbuf(); file.close(); AppConfig loaded = parseConfigFromYaml(ss.str()); std::vector callbacks_to_notify; { std::lock_guard lock(mutex_); config_ = loaded; } { std::lock_guard cb_lock(callback_mutex_); callbacks_to_notify = callbacks_; } for (const auto& cb : callbacks_to_notify) { if (cb) cb(loaded); } return true; } void ConfigManager::loadAndMergeDefaults() { AppConfig defaults = AppConfig::defaults(); if (!std::filesystem::exists(file_path_)) { // File doesn't exist: write full defaults std::lock_guard lock(mutex_); config_ = defaults; saveUnlocked(); return; } // Read existing file std::ifstream file(file_path_); if (!file.is_open()) { std::lock_guard lock(mutex_); config_ = defaults; return; } std::stringstream ss; ss << file.rdbuf(); file.close(); std::string yaml_content = ss.str(); YamlNode root = parseYaml(yaml_content); AppConfig merged = defaults; // Preserving existing values if present if (root.hasChild("license_key") && !root.getChild("license_key").asString().empty()) { merged.license_key = root.getChild("license_key").asString(); } if (root.hasChild("email_address") && !root.getChild("email_address").asString().empty()) { merged.email_address = root.getChild("email_address").asString(); } if (root.hasChild("provisioning_status") && !root.getChild("provisioning_status").asString().empty()) { merged.provisioning_status = root.getChild("provisioning_status").asString(); } if (root.hasChild("timezone") && !root.getChild("timezone").asString().empty()) { merged.timezone = root.getChild("timezone").asString(); } if (root.hasChild("audio")) { const auto& audio_node = root.getChild("audio"); if (audio_node.hasChild("enabled")) { merged.audio.enabled = audio_node.getChild("enabled").asBool(defaults.audio.enabled); } if (audio_node.hasChild("volume")) { merged.audio.volume = audio_node.getChild("volume").asInt(defaults.audio.volume); } if (audio_node.hasChild("output_device")) { merged.audio.output_device = audio_node.getChild("output_device").asString(defaults.audio.output_device); } if (audio_node.hasChild("current_track")) { merged.audio.current_track = audio_node.getChild("current_track").asString(defaults.audio.current_track); } if (audio_node.hasChild("dynamic_audio")) { merged.audio.dynamic_audio = audio_node.getChild("dynamic_audio").asBool(defaults.audio.dynamic_audio); } } if (root.hasChild("proclamation")) { const auto& proc_node = root.getChild("proclamation"); if (proc_node.hasChild("scroll_speed")) { merged.proclamation.scroll_speed = proc_node.getChild("scroll_speed").asFloat(defaults.proclamation.scroll_speed); } if (proc_node.hasChild("font_size")) { merged.proclamation.font_size = proc_node.getChild("font_size").asInt(defaults.proclamation.font_size); } if (proc_node.hasChild("current_id")) { merged.proclamation.current_id = proc_node.getChild("current_id").asString(defaults.proclamation.current_id); } if (proc_node.hasChild("schedule_active")) { merged.proclamation.schedule_active = proc_node.getChild("schedule_active").asBool(defaults.proclamation.schedule_active); } if (proc_node.hasChild("schedule_enabled")) { merged.proclamation.schedule_enabled = proc_node.getChild("schedule_enabled").asBool(defaults.proclamation.schedule_enabled); } } { std::lock_guard lock(mutex_); config_ = merged; saveUnlocked(); } } // ─── Live Hot-Reloading Watcher ─────────────────────────────────────── void ConfigManager::onChanged(ConfigCallback callback) { std::lock_guard lock(callback_mutex_); callbacks_.push_back(std::move(callback)); } std::string ConfigManager::getFilePath() const { return file_path_; } void ConfigManager::startWatcher() { stopWatcher(); watcher_running_ = true; watcher_thread_ = std::thread([this]() { while (watcher_running_) { std::this_thread::sleep_for(std::chrono::milliseconds(500)); if (!watcher_running_) break; try { if (std::filesystem::exists(file_path_)) { auto current_time = std::filesystem::last_write_time(file_path_); if (current_time != last_write_time_) { last_write_time_ = current_time; std::cout << "[Config] Detected live change in " << file_path_ << ", hot-reloading..." << std::endl; reload(); } } } catch (...) {} } }); } void ConfigManager::stopWatcher() { if (watcher_running_) { watcher_running_ = false; if (watcher_thread_.joinable()) { watcher_thread_.join(); } } } } // namespace radiantwave::config