initial commit
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace radiantwave::config {
|
||||
|
||||
struct AudioConfig {
|
||||
bool enabled = true; // audio master enabled toggle
|
||||
int volume = 80; // 0 - 100
|
||||
std::string output_device = ""; // empty = system default
|
||||
std::string current_track = "OriginalAudio.wav"; // active audio track filename
|
||||
bool dynamic_audio = true; // audio follows the displayed proclamation's solfeggio frequency
|
||||
};
|
||||
|
||||
struct ProclamationConfig {
|
||||
float scroll_speed = 1.0f; // scroll speed multiplier
|
||||
int font_size = 32; // font size in pt
|
||||
std::string current_id = ""; // currently active proclamation ID
|
||||
};
|
||||
|
||||
struct AppConfig {
|
||||
std::string license_key = "";
|
||||
std::string email_address = "";
|
||||
std::string provisioning_status = "FIRST_RUN";
|
||||
std::string timezone = "UTC"; // IANA timezone code (e.g. America/New_York, UTC)
|
||||
AudioConfig audio;
|
||||
ProclamationConfig proclamation;
|
||||
|
||||
// Single source of truth for default values
|
||||
static AppConfig defaults() {
|
||||
return AppConfig{};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace radiantwave::config
|
||||
@@ -0,0 +1,306 @@
|
||||
#include "config_manager.h"
|
||||
#include "yaml_parser.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <pwd.h>
|
||||
#include <sstream>
|
||||
#include <unistd.h>
|
||||
|
||||
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<std::mutex> lock(mutex_);
|
||||
return config_;
|
||||
}
|
||||
|
||||
bool ConfigManager::update(std::function<void(AppConfig& config)> modifier) {
|
||||
std::vector<ConfigCallback> callbacks_to_notify;
|
||||
AppConfig updated_config;
|
||||
bool success = false;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
modifier(config_);
|
||||
updated_config = config_;
|
||||
success = saveUnlocked();
|
||||
}
|
||||
|
||||
if (success) {
|
||||
std::lock_guard<std::mutex> 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<std::mutex> 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<ConfigCallback> callbacks_to_notify;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
config_ = loaded;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> 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<std::mutex> lock(mutex_);
|
||||
config_ = defaults;
|
||||
saveUnlocked();
|
||||
return;
|
||||
}
|
||||
|
||||
// Read existing file
|
||||
std::ifstream file(file_path_);
|
||||
if (!file.is_open()) {
|
||||
std::lock_guard<std::mutex> 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);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
config_ = merged;
|
||||
saveUnlocked();
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Live Hot-Reloading Watcher ───────────────────────────────────────
|
||||
|
||||
void ConfigManager::onChanged(ConfigCallback callback) {
|
||||
std::lock_guard<std::mutex> 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
|
||||
@@ -0,0 +1,70 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_config.h"
|
||||
#include <atomic>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace radiantwave::config {
|
||||
|
||||
using ConfigCallback = std::function<void(const AppConfig& new_config)>;
|
||||
|
||||
class ConfigManager {
|
||||
public:
|
||||
ConfigManager();
|
||||
~ConfigManager();
|
||||
|
||||
// Non-copyable, non-movable
|
||||
ConfigManager(const ConfigManager&) = delete;
|
||||
ConfigManager& operator=(const ConfigManager&) = delete;
|
||||
|
||||
// Initialize with default path (~/.local/radiantwave/config.yaml) or custom path
|
||||
bool initialize(const std::string& custom_path = "");
|
||||
|
||||
// Shutdown background watcher
|
||||
void shutdown();
|
||||
|
||||
// Access current config
|
||||
AppConfig get() const;
|
||||
|
||||
// Update config safely and write to disk
|
||||
bool update(std::function<void(AppConfig& config)> modifier);
|
||||
|
||||
// Save current config to disk
|
||||
bool save();
|
||||
|
||||
// Reload from disk immediately
|
||||
bool reload();
|
||||
|
||||
// Register callback for live config updates (hot-reloading)
|
||||
void onChanged(ConfigCallback callback);
|
||||
|
||||
// Get current config file path
|
||||
std::string getFilePath() const;
|
||||
|
||||
private:
|
||||
std::string resolvePath(const std::string& raw_path) const;
|
||||
void loadAndMergeDefaults();
|
||||
bool saveUnlocked();
|
||||
void startWatcher();
|
||||
void stopWatcher();
|
||||
|
||||
std::string file_path_;
|
||||
AppConfig config_;
|
||||
mutable std::mutex mutex_;
|
||||
|
||||
// Callbacks
|
||||
std::vector<ConfigCallback> callbacks_;
|
||||
std::mutex callback_mutex_;
|
||||
|
||||
// Watcher thread
|
||||
std::thread watcher_thread_;
|
||||
std::atomic<bool> watcher_running_{false};
|
||||
std::filesystem::file_time_type last_write_time_;
|
||||
};
|
||||
|
||||
} // namespace radiantwave::config
|
||||
@@ -0,0 +1,263 @@
|
||||
#include "yaml_parser.h"
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
|
||||
namespace radiantwave::config {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string trim(const std::string& str) {
|
||||
auto start = str.find_first_not_of(" \t\r\n");
|
||||
if (start == std::string::npos) return "";
|
||||
auto end = str.find_last_not_of(" \t\r\n");
|
||||
return str.substr(start, end - start + 1);
|
||||
}
|
||||
|
||||
std::string unquote(const std::string& str) {
|
||||
std::string s = trim(str);
|
||||
if (s.size() >= 2) {
|
||||
if ((s.front() == '"' && s.back() == '"') ||
|
||||
(s.front() == '\'' && s.back() == '\'')) {
|
||||
return s.substr(1, s.size() - 2);
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
std::string removeComments(const std::string& line) {
|
||||
bool in_single_quote = false;
|
||||
bool in_double_quote = false;
|
||||
|
||||
for (size_t i = 0; i < line.size(); ++i) {
|
||||
char c = line[i];
|
||||
if (c == '\'' && !in_double_quote) {
|
||||
in_single_quote = !in_single_quote;
|
||||
} else if (c == '"' && !in_single_quote) {
|
||||
in_double_quote = !in_double_quote;
|
||||
} else if (c == '#' && !in_single_quote && !in_double_quote) {
|
||||
return line.substr(0, i);
|
||||
}
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
int countIndent(const std::string& line) {
|
||||
int indent = 0;
|
||||
for (char c : line) {
|
||||
if (c == ' ') indent++;
|
||||
else if (c == '\t') indent += 2;
|
||||
else break;
|
||||
}
|
||||
return indent;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
// ─── YamlNode Implementation ───────────────────────────────────────────
|
||||
|
||||
std::string YamlNode::asString(const std::string& defaultValue) const {
|
||||
if (type == Type::Scalar && !scalar.empty()) {
|
||||
return scalar;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
int YamlNode::asInt(int defaultValue) const {
|
||||
if (type == Type::Scalar && !scalar.empty()) {
|
||||
try {
|
||||
return std::stoi(scalar);
|
||||
} catch (...) {}
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
float YamlNode::asFloat(float defaultValue) const {
|
||||
if (type == Type::Scalar && !scalar.empty()) {
|
||||
try {
|
||||
return std::stof(scalar);
|
||||
} catch (...) {}
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
bool YamlNode::asBool(bool defaultValue) const {
|
||||
if (type == Type::Scalar && !scalar.empty()) {
|
||||
std::string s = scalar;
|
||||
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c){ return std::tolower(c); });
|
||||
if (s == "true" || s == "yes" || s == "1" || s == "on") return true;
|
||||
if (s == "false" || s == "no" || s == "0" || s == "off") return false;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
bool YamlNode::hasChild(const std::string& key) const {
|
||||
if (type != Type::Mapping) return false;
|
||||
return children.find(key) != children.end();
|
||||
}
|
||||
|
||||
static const YamlNode EMPTY_NODE;
|
||||
|
||||
const YamlNode& YamlNode::getChild(const std::string& key) const {
|
||||
if (type == Type::Mapping) {
|
||||
auto it = children.find(key);
|
||||
if (it != children.end()) {
|
||||
return it->second;
|
||||
}
|
||||
}
|
||||
return EMPTY_NODE;
|
||||
}
|
||||
|
||||
// ─── Parser ─────────────────────────────────────────────────────────────
|
||||
|
||||
YamlNode parseYaml(const std::string& content) {
|
||||
YamlNode root;
|
||||
root.type = YamlNode::Type::Mapping;
|
||||
|
||||
std::stringstream ss(content);
|
||||
std::string raw_line;
|
||||
|
||||
std::string current_section = "";
|
||||
|
||||
while (std::getline(ss, raw_line)) {
|
||||
std::string line_no_comment = removeComments(raw_line);
|
||||
std::string line = trim(line_no_comment);
|
||||
|
||||
if (line.empty()) continue;
|
||||
|
||||
int indent = countIndent(line_no_comment);
|
||||
|
||||
auto colon_pos = line.find(':');
|
||||
if (colon_pos == std::string::npos) continue;
|
||||
|
||||
std::string key = trim(line.substr(0, colon_pos));
|
||||
std::string val = trim(line.substr(colon_pos + 1));
|
||||
|
||||
if (indent == 0) {
|
||||
// Top-level key
|
||||
if (val.empty()) {
|
||||
// Start a section mapping
|
||||
current_section = key;
|
||||
YamlNode section_node;
|
||||
section_node.type = YamlNode::Type::Mapping;
|
||||
root.children[key] = section_node;
|
||||
} else {
|
||||
current_section = "";
|
||||
YamlNode scalar_node;
|
||||
scalar_node.type = YamlNode::Type::Scalar;
|
||||
scalar_node.scalar = unquote(val);
|
||||
root.children[key] = scalar_node;
|
||||
}
|
||||
} else {
|
||||
// Indented key (child of current_section)
|
||||
if (!current_section.empty() && root.children.find(current_section) != root.children.end()) {
|
||||
YamlNode scalar_node;
|
||||
scalar_node.type = YamlNode::Type::Scalar;
|
||||
scalar_node.scalar = unquote(val);
|
||||
root.children[current_section].children[key] = scalar_node;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
std::string serializeYaml(const YamlNode& root) {
|
||||
std::stringstream ss;
|
||||
|
||||
if (root.type == YamlNode::Type::Mapping) {
|
||||
for (const auto& [key, node] : root.children) {
|
||||
if (node.isScalar()) {
|
||||
ss << key << ": \"" << node.scalar << "\"\n";
|
||||
} else if (node.isMapping()) {
|
||||
ss << "\n" << key << ":\n";
|
||||
for (const auto& [child_key, child_node] : node.children) {
|
||||
ss << " " << child_key << ": \"" << child_node.scalar << "\"\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
// ─── High-Level AppConfig Conversions ──────────────────────────────────
|
||||
|
||||
AppConfig parseConfigFromYaml(const std::string& yamlContent) {
|
||||
AppConfig config = AppConfig::defaults();
|
||||
YamlNode root = parseYaml(yamlContent);
|
||||
|
||||
if (root.hasChild("license_key")) {
|
||||
config.license_key = root.getChild("license_key").asString(config.license_key);
|
||||
}
|
||||
if (root.hasChild("email_address")) {
|
||||
config.email_address = root.getChild("email_address").asString(config.email_address);
|
||||
}
|
||||
if (root.hasChild("provisioning_status")) {
|
||||
config.provisioning_status = root.getChild("provisioning_status").asString(config.provisioning_status);
|
||||
}
|
||||
if (root.hasChild("timezone")) {
|
||||
config.timezone = root.getChild("timezone").asString(config.timezone);
|
||||
}
|
||||
|
||||
if (root.hasChild("audio")) {
|
||||
const auto& audio_node = root.getChild("audio");
|
||||
if (audio_node.hasChild("enabled")) {
|
||||
config.audio.enabled = audio_node.getChild("enabled").asBool(config.audio.enabled);
|
||||
}
|
||||
if (audio_node.hasChild("volume")) {
|
||||
config.audio.volume = audio_node.getChild("volume").asInt(config.audio.volume);
|
||||
}
|
||||
if (audio_node.hasChild("output_device")) {
|
||||
config.audio.output_device = audio_node.getChild("output_device").asString(config.audio.output_device);
|
||||
}
|
||||
if (audio_node.hasChild("current_track")) {
|
||||
config.audio.current_track = audio_node.getChild("current_track").asString(config.audio.current_track);
|
||||
}
|
||||
if (audio_node.hasChild("dynamic_audio")) {
|
||||
config.audio.dynamic_audio = audio_node.getChild("dynamic_audio").asBool(config.audio.dynamic_audio);
|
||||
}
|
||||
}
|
||||
|
||||
if (root.hasChild("proclamation")) {
|
||||
const auto& proc_node = root.getChild("proclamation");
|
||||
if (proc_node.hasChild("scroll_speed")) {
|
||||
config.proclamation.scroll_speed = proc_node.getChild("scroll_speed").asFloat(config.proclamation.scroll_speed);
|
||||
}
|
||||
if (proc_node.hasChild("font_size")) {
|
||||
config.proclamation.font_size = proc_node.getChild("font_size").asInt(config.proclamation.font_size);
|
||||
}
|
||||
if (proc_node.hasChild("current_id")) {
|
||||
config.proclamation.current_id = proc_node.getChild("current_id").asString(config.proclamation.current_id);
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
std::string serializeConfigToYaml(const AppConfig& config) {
|
||||
std::stringstream ss;
|
||||
|
||||
ss << "# RadiantWave Kiosk Configuration\n\n";
|
||||
ss << "license_key: \"" << config.license_key << "\"\n";
|
||||
ss << "email_address: \"" << config.email_address << "\"\n";
|
||||
ss << "provisioning_status: \"" << config.provisioning_status << "\"\n";
|
||||
ss << "timezone: \"" << config.timezone << "\"\n\n";
|
||||
|
||||
ss << "audio:\n";
|
||||
ss << " enabled: " << (config.audio.enabled ? "true" : "false") << "\n";
|
||||
ss << " volume: " << config.audio.volume << "\n";
|
||||
ss << " output_device: \"" << config.audio.output_device << "\"\n";
|
||||
ss << " current_track: \"" << config.audio.current_track << "\"\n";
|
||||
ss << " dynamic_audio: " << (config.audio.dynamic_audio ? "true" : "false") << "\n\n";
|
||||
|
||||
ss << "proclamation:\n";
|
||||
ss << " scroll_speed: " << config.proclamation.scroll_speed << "\n";
|
||||
ss << " font_size: " << config.proclamation.font_size << "\n";
|
||||
ss << " current_id: \"" << config.proclamation.current_id << "\"\n";
|
||||
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
} // namespace radiantwave::config
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include "app_config.h"
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
namespace radiantwave::config {
|
||||
|
||||
struct YamlNode {
|
||||
enum class Type { Scalar, Mapping };
|
||||
Type type = Type::Scalar;
|
||||
std::string scalar;
|
||||
std::map<std::string, YamlNode> children;
|
||||
|
||||
bool isScalar() const { return type == Type::Scalar; }
|
||||
bool isMapping() const { return type == Type::Mapping; }
|
||||
|
||||
std::string asString(const std::string& defaultValue = "") const;
|
||||
int asInt(int defaultValue = 0) const;
|
||||
float asFloat(float defaultValue = 0.0f) const;
|
||||
bool asBool(bool defaultValue = false) const;
|
||||
|
||||
bool hasChild(const std::string& key) const;
|
||||
const YamlNode& getChild(const std::string& key) const;
|
||||
};
|
||||
|
||||
// Core parser & serializer functions
|
||||
YamlNode parseYaml(const std::string& content);
|
||||
std::string serializeYaml(const YamlNode& root);
|
||||
|
||||
// High-level AppConfig conversions
|
||||
AppConfig parseConfigFromYaml(const std::string& yamlContent);
|
||||
std::string serializeConfigToYaml(const AppConfig& config);
|
||||
|
||||
} // namespace radiantwave::config
|
||||
Reference in New Issue
Block a user