summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFabien Proriol <fabien.proriol@kazoe.org>2025-05-22 15:42:13 +0200
committerFabien Proriol <fabien.proriol@kazoe.org>2025-05-22 15:48:00 +0200
commitc842548fef050ac5f8b56a5fcb4f579820247434 (patch)
tree68bce34d0932db0157761635a010695c5e9616e3
Initial Commit
-rw-r--r--CMakeLists.txt71
-rw-r--r--LICENSE21
-rw-r--r--README.md181
-rw-r--r--cmake/CPack.cmake8
-rw-r--r--cmake/Config.cmake.in7
-rw-r--r--cmd/ksettings.cpp44
-rw-r--r--python/bindings.cpp65
-rw-r--r--src/changewatcher.cpp134
-rw-r--r--src/changewatcher.h39
-rw-r--r--src/settings.cpp285
-rw-r--r--src/settings.h180
11 files changed, 1035 insertions, 0 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..acb7c5d
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,71 @@
+cmake_minimum_required(VERSION 3.14)
+
+project(KaZoe-Settings VERSION 1.0.0 LANGUAGES CXX)
+
+option(WITH_PYTHON "Create python binding" ON)
+
+set(CMAKE_CXX_STANDARD 20)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+find_package (Threads REQUIRED)
+
+add_library(KaZoeSettings SHARED
+ src/settings.h src/settings.cpp
+ src/changewatcher.h src/changewatcher.cpp
+)
+set_target_properties(KaZoeSettings PROPERTIES VERSION ${CMAKE_PROJECT_VERSION} SOVERSION 1)
+set_target_properties(KaZoeSettings PROPERTIES PUBLIC_HEADER "src/settings.h")
+target_link_libraries(KaZoeSettings PRIVATE Threads::Threads)
+
+add_executable(ksettings cmd/ksettings.cpp)
+target_include_directories(ksettings PRIVATE src)
+target_link_libraries(ksettings KaZoeSettings)
+
+install(TARGETS KaZoeSettings
+ EXPORT "KaZoeSettingsTargets"
+ LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
+ RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
+ PUBLIC_HEADER DESTINATION "${CMAKE_INSTALL_PREFIX}/include/KaZoe/"
+)
+
+install(TARGETS ksettings
+ LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
+ RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
+)
+
+if(WITH_PYTHON)
+ message(STATUS "Build with python binding")
+ find_package(Python3 COMPONENTS Interpreter Development REQUIRED)
+ set(PYTHON_MODULE_EXTENSION ".so" CACHE INTERNAL "Cross python lib extension")
+ find_package(pybind11 REQUIRED)
+ include_directories(${Python3_INCLUDE_DIRS})
+ pybind11_add_module(PyKaZoeSettings python/bindings.cpp)
+ set_target_properties(PyKaZoeSettings PROPERTIES OUTPUT_NAME "KaZoeSettings")
+ target_link_libraries(PyKaZoeSettings PUBLIC KaZoeSettings ${PYTHON_LIBRARY})
+ install(TARGETS PyKaZoeSettings LIBRARY DESTINATION ${Python3_SITEARCH})
+endif(WITH_PYTHON)
+
+
+# CMake Module
+include(CMakePackageConfigHelpers)
+write_basic_package_version_file(
+ "${CMAKE_CURRENT_BINARY_DIR}/generated/KaZoeSettingsConfigVersion.cmake" COMPATIBILITY SameMajorVersion
+)
+
+configure_package_config_file(
+ "cmake/Config.cmake.in"
+ "${CMAKE_CURRENT_BINARY_DIR}/generated/KaZoeSettingsConfig.cmake"
+ INSTALL_DESTINATION "lib/cmake/${PROJECT_NAME}"
+)
+
+install(
+ FILES "${CMAKE_CURRENT_BINARY_DIR}/generated/KaZoeSettingsConfig.cmake" "${CMAKE_CURRENT_BINARY_DIR}/generated/KaZoeSettingsConfigVersion.cmake"
+ DESTINATION "lib/cmake/${PROJECT_NAME}"
+)
+
+install(
+ EXPORT "KaZoeSettingsTargets"
+ DESTINATION "lib/cmake/${PROJECT_NAME}"
+)
+
+include(cmake/CPack.cmake)
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..529eb62
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 Proriol Fabien
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..a84076d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,181 @@
+# KaZoe::Settings Class Documentation
+
+## Overview
+The `KaZoe::Settings` class provides a robust system-wide configuration management solution. It allows reading and writing settings from configuration files with category support, type safety, and change notifications.
+
+## Features
+- Read settings from multiple configuration files
+- Support for different value types (string, int, double, boolean)
+- Category-based organization of settings
+- File change monitoring
+- Permission-based write access
+- Change notification system
+
+## Configuration File Structure
+Settings are stored in configuration files with the `.conf` extension. The file format supports:
+- Categories in square brackets: `[CategoryName]`
+- Key-value pairs: `key = value`
+- Owner declarations: `@key = owner_id`
+
+### Value Types SettingValue
+- Strings: `key = "value"` or `key = value`
+- Numbers: `key = 42` or `key = 3.14`
+- Booleans: `key = true/false` or `key = on/off`
+
+## Usage
+
+### Initialization
+```cmake
+find_package(KaZoeSettings REQUIRED)
+include_directories(${LIBKAZOESETTINGS_INCLUDE_DIR})
+
+...
+
+target_link_libraries(myproject PUBLIC ${LIBKAZOESETTINGS_LIBRARIES})
+```
+
+```cpp
+#include <settings.h>
+
+KaZoe::Settings settings; // Creates instance and loads configuration
+```
+
+### Reading Settings
+```cpp
+// Get value with default
+auto value = settings.get("key", "category", defaultValue);
+
+// Get value without category
+auto value = settings.get("key");
+
+// Get value using category notation
+auto value = settings.get("[category]key");
+```
+
+### Writing Settings
+```cpp
+// Set value with category
+settings.set("key", value, "category");
+
+// Set value without category
+settings.set("key", value);
+```
+
+### Utility with SettingValue type
+
+KaZoe::makeValue: provide a way to convert std::string into SettingValue managing numeric, bool and string conversion
+
+```cpp
+#include <settings.h>
+
+SettingValue v1 = KaZoe::makeValue("42"); // Integer
+SettingValue v2 = KaZoe::makeValue("3.14"); // Double
+SettingValue v3 = KaZoe::makeValue("true"); // Boolean
+SettingValue v4 = KaZoe::makeValue("text"); // String
+```
+
+KaZoe::valueToStr: provide a way to convert SettingValue into std::string to display
+```cpp
+#include <settings.h>
+
+std::cout << KaZoe::valueToStr(v1) << std::endl;
+```
+
+### Change Notifications and print helper function
+```cpp
+// Set a notification handler
+settings.setNotifier([](const std::string& category, const std::string& key, SettingValue value) {
+ // Handle setting change
+ std::cout << category << " | " << key << " changed to " << KaZoe::valueToStr(value) << std::endl;
+});
+```
+
+## File Locations
+The system reads configuration from:
+
+ - /etc/kazoe.conf - System defaults (read-only)
+ - /etc/kazoe.conf.d/* - System configuration fragments (read-only, own by projects packages)
+ - /var/kazoe.conf.d/<appid>.conf - Persistant write data storage (set)
+
+## Permissions
+
+Settings must be declared writable using @key = owner_id
+The owner should be the binary filename of the application
+Only the declared owner can modify the setting if the program name is the good one
+
+
+The settings command-line tool can bypass ownership restrictions but must used ONLY for debug usage
+
+## Value Types
+The system supports these value types through the SettingValue variant:
+
+ - std::string
+ - int
+ - double
+ - bool
+
+# KaZoeSettings Python Module Documentation
+## Overview
+
+The KaZoeSettings module provides a Python interface for managing KaZoe settings across your application. It automatically uses the Python script name as a unique identifier.
+
+## Usage Class: KaZoeSettings
+
+```python
+from KaZoeSettings import KaZoeSettings
+```
+
+## Constructor
+```python
+settings = KaZoeSettings.KaZoeSettings()
+```
+
+Creates a new KaZoeSettings instance using the current Python script name as identifier.
+
+## Methods
+
+### get(key, category="", default=None)
+
+Retrieves a setting value.
+
+ Parameters:
+ key (str): The setting key
+ category (str, optional): Setting category
+ default: Default value if setting not found
+ Returns: The setting value or default if not found
+ Supported value types: str, int, float, bool
+
+```python
+value = settings.get("my_setting", "my_category", "default_value")
+```
+
+### set(key, value, category="")
+
+Sets a setting value.
+
+ Parameters:
+ key (str): The setting key
+ value: The value to set (str, int, float, or bool)
+ category (str, optional): Setting category
+ Returns: True if successful, False otherwise
+
+```python
+settings.set("my_setting", "new_value", "my_category")
+```
+
+## Full example usage
+
+```python
+from KaZoeSettings import KaZoeSettings
+
+# Create settings instance
+settings = KaZoeSettings()
+
+# Set values
+settings.set("port", 8080, "network")
+settings.set("debug", True, "app")
+
+# Get values
+port = settings.get("port", "network", 80) # returns 8080
+debug = settings.get("debug", "app", False) # returns True
+```
diff --git a/cmake/CPack.cmake b/cmake/CPack.cmake
new file mode 100644
index 0000000..33169b1
--- /dev/null
+++ b/cmake/CPack.cmake
@@ -0,0 +1,8 @@
+set(CPACK_GENERATOR "DEB")
+set(CPACK_PACKAGE_NAME "KaZoe-Settings")
+set(CPACK_DEBIAN_PACKAGE_DEPENDS "python3")
+set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT)
+set(CPACK_PACKAGE_VERSION_PATCH "${CMAKE_PROJECT_VERSION_PATCH}")
+set(CPACK_PACKAGE_DESCRIPTION "KaZoe library")
+set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Fabien Proriol <fabien.proriol@kazoe.org>")
+include(CPack)
diff --git a/cmake/Config.cmake.in b/cmake/Config.cmake.in
new file mode 100644
index 0000000..efbb7cc
--- /dev/null
+++ b/cmake/Config.cmake.in
@@ -0,0 +1,7 @@
+@PACKAGE_INIT@
+
+include("${CMAKE_CURRENT_LIST_DIR}/KaZoeSettingsTargets.cmake")
+set(LIBKAZOESETTINGS_INCLUDE_DIR "${PACKAGE_PREFIX_DIR}/include/@PROJECT_NAME@")
+set(LIBKAZOESETTINGS_LIBRARIES "KaZoeSettings" )
+
+check_required_components("@PROJECT_NAME@")
diff --git a/cmd/ksettings.cpp b/cmd/ksettings.cpp
new file mode 100644
index 0000000..0d29dab
--- /dev/null
+++ b/cmd/ksettings.cpp
@@ -0,0 +1,44 @@
+#include <iostream>
+#include <settings.h>
+
+using namespace std;
+
+int main(int argc, char *argv[])
+{
+ KaZoe::Settings settings;
+
+ if(argc < 2)
+ {
+ std::cout << "Current settings:" << std::endl;
+
+ settings.forEach([](const SettingKey& key, const SettingValue& value) {
+ std::string head;
+ if(!key.first.empty())
+ {
+ head = "[" + key.first + "]";
+ }
+ std::cout << "> " << head << key.second << " = " << KaZoe::valueToStr(value) << std::endl;
+ });
+ }
+ else
+ {
+ std::string category;
+ std::string gkey = argv[1];
+ if(gkey.starts_with("["))
+ {
+ category = gkey.substr(1, gkey.find(']') - 1);
+ gkey = gkey.substr(gkey.find(']') + 1);
+ }
+ if(argc == 2)
+ {
+ std::cout << KaZoe::valueToStr(settings.get(gkey, category)) << std::endl;
+ }
+ else if(argc == 3)
+ {
+ std::string value = argv[2];
+ settings.set(gkey, KaZoe::makeValue(value), category);
+ }
+ }
+
+ return 0;
+}
diff --git a/python/bindings.cpp b/python/bindings.cpp
new file mode 100644
index 0000000..5c816f6
--- /dev/null
+++ b/python/bindings.cpp
@@ -0,0 +1,65 @@
+#include <pybind11/pybind11.h>
+#include <pybind11/stl.h>
+#include "../src/settings.h"
+
+namespace py = pybind11;
+using namespace pybind11::literals;
+
+class PyKaZoeSettings {
+ KaZoe::Settings m_settings;
+
+public:
+ PyKaZoeSettings();
+ std::string __repr__();
+ static void add_python_binding(pybind11::module &m);
+ SettingValue get(std::string key, std::string category, SettingValue def);
+ SettingValue set(std::string key, SettingValue value, std::string category);
+};
+
+PyKaZoeSettings::PyKaZoeSettings() {
+ py::module sys = py::module::import("sys");
+ py::list argv = sys.attr("argv");
+ m_settings.setId(argv[0].cast<std::string>());
+}
+
+SettingValue PyKaZoeSettings::get(std::string key, std::string category, SettingValue def) {
+ return m_settings.get(key, category, def);
+}
+
+SettingValue PyKaZoeSettings::set(std::string key, SettingValue value, std::string category) {
+ return m_settings.set(key, value, category);
+}
+
+
+std::string PyKaZoeSettings::__repr__() {
+ std::string result = "KaZoeSettings";
+ return result;
+}
+void PyKaZoeSettings::add_python_binding(pybind11::module &m)
+{
+ py::class_<PyKaZoeSettings>(m, "KaZoeSettings")
+ .def(py::init<>())
+
+ .def("get", [] (PyKaZoeSettings &m, std::string key, std::string category = "", SettingValue def = SettingValue()) {
+ return m.get(key, category, def);
+ },
+ py::arg("key"),
+ py::arg("category") = "",
+ py::arg("default") = SettingValue())
+ .def("set", [] (PyKaZoeSettings &m, std::string key, SettingValue value, std::string category = "") {
+ return m.set(key, value, category);
+ },
+ py::arg("key"),
+ py::arg("value"),
+ py::arg("category") = "")
+ .def("__repr__", &PyKaZoeSettings::__repr__);
+}
+
+
+PYBIND11_MODULE(KaZoeSettings, m) {
+ m.doc() = R"pbdoc(
+ Python bindings for KaZoeSettings
+ )pbdoc";
+
+ PyKaZoeSettings::add_python_binding(m);
+}
diff --git a/src/changewatcher.cpp b/src/changewatcher.cpp
new file mode 100644
index 0000000..4762ebb
--- /dev/null
+++ b/src/changewatcher.cpp
@@ -0,0 +1,134 @@
+#include "changewatcher.h"
+#include <iostream>
+#include <filesystem>
+#include <sys/inotify.h>
+#include <sys/eventfd.h>
+#include <unistd.h>
+#include <algorithm>
+
+ChangeWatcher::ChangeWatcher()
+{
+}
+
+ChangeWatcher::~ChangeWatcher()
+{
+ setEnable(false);
+}
+
+void ChangeWatcher::watch(const std::string &newPath)
+{
+ if(std::find(m_watches.begin(), m_watches.end(), newPath) == m_watches.end())
+ {
+ m_watches.push_back(newPath);
+ if(m_enabled)
+ {
+ _addWatch(newPath);
+ }
+ }
+}
+
+void ChangeWatcher::setEnable(bool newVal)
+{
+ if(m_enabled == newVal)
+ return;
+
+ if(newVal)
+ {
+ // Start thread
+ m_fdnotify = inotify_init();
+ for(auto &newPath: m_watches)
+ {
+ _addWatch(newPath);
+ }
+ m_fdevent = eventfd(0, 0);
+ m_enabled = true;
+ m_thread = std::thread(_runningLoop, this);
+ }
+ else
+ {
+ // Stop thread
+ m_enabled = false;
+ uint64_t value = 1;
+ write(m_fdevent, &value, sizeof(value));
+ close(m_fdevent);
+ m_thread.join();
+ close(m_fdnotify);
+ }
+}
+
+bool ChangeWatcher::enabled() const
+{
+ return m_enabled;
+}
+
+void ChangeWatcher::setFileWrited(const std::function<void (const std::string &)> &callback)
+{
+ m_filewrited = callback;
+}
+
+void ChangeWatcher::setFileAdded(const std::function<void (const std::string &)> &callback)
+{
+ m_fileadded = callback;
+}
+
+void ChangeWatcher::setFileRemoved(const std::function<void (const std::string &)> &callback)
+{
+ m_fileremoved = callback;
+}
+
+void ChangeWatcher::_runningLoop(ChangeWatcher *m)
+{
+ const size_t event_size = sizeof(struct inotify_event);
+ const size_t buffer_size = 1024 * (event_size + 16);
+ char buffer[buffer_size];
+
+ while (m->m_enabled) {
+ fd_set rfds;
+ FD_ZERO(&rfds);
+ FD_SET(m->m_fdevent, &rfds);
+ FD_SET(m->m_fdnotify, &rfds);
+ int nfds = std::max(m->m_fdevent, m->m_fdnotify) + 1;
+
+ if (select(nfds, &rfds, nullptr, nullptr, nullptr) > 0) {
+ if (FD_ISSET(m->m_fdevent, &rfds)) {
+ break; // Signal d'arrêt reçu
+ }
+ if (FD_ISSET(m->m_fdnotify, &rfds)) {
+ int length = read(m->m_fdnotify, buffer, buffer_size);
+ if (length > 0) {
+ size_t i = 0;
+ while (i < length) {
+ auto* event = reinterpret_cast<struct inotify_event*>(&buffer[i]);
+ if((event->mask & IN_CLOSE_WRITE) && (m->m_filewrited != nullptr))
+ {
+ m->m_filewrited(m->m_wd[event->wd]);
+ }
+ if((event->mask & IN_CREATE) && (m->m_fileadded != nullptr))
+ {
+ m->m_fileadded(m->m_wd[event->wd]);
+ }
+ if((event->mask & IN_DELETE) && (m->m_fileremoved != nullptr))
+ {
+ m->m_fileremoved(m->m_wd[event->wd]);
+ }
+ i += event_size + event->len;
+ }
+ }
+ }
+ }
+ }
+}
+
+void ChangeWatcher::_addWatch(const std::string &newPath)
+{
+ int wd = -1;
+ if(std::filesystem::is_directory(newPath))
+ {
+ wd = inotify_add_watch(m_fdnotify, newPath.c_str(), IN_CREATE | IN_DELETE);
+ }
+ else
+ {
+ wd = inotify_add_watch(m_fdnotify, newPath.c_str(), IN_CLOSE_WRITE);
+ }
+ if(wd > -1) m_wd[wd] = newPath;
+}
diff --git a/src/changewatcher.h b/src/changewatcher.h
new file mode 100644
index 0000000..fde6ab8
--- /dev/null
+++ b/src/changewatcher.h
@@ -0,0 +1,39 @@
+#ifndef CHANGEWATCHER_H
+#define CHANGEWATCHER_H
+
+#include <functional>
+#include <string>
+#include <map>
+#include <thread>
+
+class ChangeWatcher
+{
+ int m_fdnotify;
+ int m_fdevent;
+ bool m_enabled {false};
+ std::thread m_thread;
+ std::vector<std::string> m_watches;
+ std::map<int, std::string> m_wd;
+
+ std::function<void (const std::string &)> m_filewrited {};
+ std::function<void (const std::string &)> m_fileadded;
+ std::function<void (const std::string &)> m_fileremoved;
+
+public:
+ explicit ChangeWatcher();
+ virtual ~ChangeWatcher();
+ void watch(const std::string &newPath);
+
+ void setEnable(bool newVal);
+ bool enabled() const;
+
+ void setFileWrited(const std::function<void(const std::string&)> &callback);
+ void setFileAdded(const std::function<void(const std::string&)> &callback);
+ void setFileRemoved(const std::function<void(const std::string&)> &callback);
+
+private:
+ static void _runningLoop(ChangeWatcher *m);
+ void _addWatch(const std::string& newPath);
+};
+
+#endif // CHANGEWATCHER_H
diff --git a/src/settings.cpp b/src/settings.cpp
new file mode 100644
index 0000000..addaa32
--- /dev/null
+++ b/src/settings.cpp
@@ -0,0 +1,285 @@
+#include "settings.h"
+#include <iostream>
+#include <fstream>
+#include <map>
+#include <vector>
+#include <filesystem>
+#include <climits>
+#include <mutex>
+#include <unistd.h>
+#include "changewatcher.h"
+
+using namespace KaZoe;
+
+static inline void trim(std::string & str)
+{
+ str.erase(str.begin(), std::find_if(str.begin(), str.end(), [](const unsigned char ch){return !std::isspace(ch);}));
+ str.erase(std::find_if(str.rbegin(), str.rend(), [](const unsigned char ch){return !std::isspace(ch);}).base(), str.end());
+}
+
+static inline bool list_contains(std::vector<std::string> list, const std::string & str)
+{
+ return (std::find(list.begin(), list.end(), str) != list.end());
+}
+
+namespace KaZoe {
+class SettingsPrivate {
+ friend class Settings;
+ Settings *m_parent;
+ std::map<SettingKey, SettingValue> m_data;
+ ChangeWatcher m_watcher;
+ std::map<SettingKey, std::string> m_owner;
+ std::string m_id;
+ std::mutex m_mutex_data;
+ std::mutex m_mutex_notifier;
+ bool m_bypass {false};
+ std::vector<std::function<void(const std::string&, const std::string&, SettingValue)>> m_notifier;
+
+public:
+ SettingsPrivate(Settings *parent)
+ : m_parent(parent) {
+ char result[PATH_MAX];
+ ssize_t count = readlink("/proc/self/exe", result, PATH_MAX);
+ std::string binary = std::string(result, (count > 0) ? count : 0);
+ std::filesystem::path bin = binary;
+ m_id = bin.filename();
+ if(m_id == "settings")
+ {
+ // Only "settings cli tools can bypass the owner protection"
+ m_bypass = true;
+ }
+
+ m_watcher.setFileAdded([this](const std::string path){ refreshDirectory(path); });
+ m_watcher.setFileRemoved([this](const std::string path){ refreshAll(); });
+ m_watcher.setFileWrited([this](const std::string path){ refreshFile(path); });
+ }
+
+ void parseFile(const std::string &filename, std::map<SettingKey, SettingValue> &data, bool watch = false)
+ {
+ if(!std::filesystem::exists(filename))
+ {
+ return;
+ }
+ std::filesystem::path pfile = filename;
+ if(pfile.filename().string().starts_with(".")) return;
+ if(pfile.extension() != ".conf") return;
+
+ if(watch)
+ {
+ m_watcher.watch(filename);
+ }
+
+ std::ifstream file(filename);
+ std::string line;
+ std::string category = "";
+
+ while (std::getline(file, line)) {
+ trim(line);
+
+ if(line.empty() || line.starts_with("#") || line.ends_with(";")) {
+ continue;
+ }
+ if(line.starts_with("[") && line.ends_with("]")) {
+ category = line.substr(1, line.length() - 2);
+ if(category == "General") category.clear();
+ continue;
+ }
+ size_t pos = line.find("=");
+ std::string key = line.substr(0, pos);
+ trim(key);
+ std::string value = line.substr(pos + 1);
+ trim(value);
+
+ if(key.starts_with("@"))
+ {
+ m_owner[SettingKey(category, key.substr(1))] = value;
+ continue;
+ }
+ if(key.ends_with("/owner"))
+ {
+ std::cerr << "WARNING: Legacy usage for owner: " << key << " should be @" << key.substr(0, key.size() - 6) << std::endl;
+ m_owner[SettingKey(category, key.substr(0, key.size() - 6))] = value;
+ continue;
+ }
+
+ data[SettingKey(category, key)] = makeValue(value);
+ }
+ }
+
+ void parseDir(const std::string &dconf, std::map<SettingKey, SettingValue> &data, bool watch)
+ {
+ if(std::filesystem::exists(dconf) && std::filesystem::is_directory(dconf))
+ {
+ for (const auto& entry : std::filesystem::directory_iterator(dconf))
+ {
+ if(entry.is_directory()) continue;
+ parseFile(std::filesystem::absolute(entry.path()), data, watch);
+ }
+ }
+ }
+
+ void parseConf(const std::string &fconf, std::map<SettingKey, SettingValue> &data, bool watch)
+ {
+ if(std::filesystem::exists(fconf))
+ {
+ parseFile(fconf, data, watch);
+ }
+ std::string dconf = fconf + ".d";
+ parseDir(dconf, data, watch);
+ if(watch)
+ {
+ m_watcher.watch(dconf);
+ }
+ }
+
+ void refreshData(const std::map<SettingKey, SettingValue> &cust_data)
+ {
+ std::lock_guard<std::mutex> lock(m_mutex_data);
+ std::lock_guard<std::mutex> lock_notifier(m_mutex_notifier);
+ for(const auto& [key, value] : cust_data) {
+ if(!m_data.contains(key) || m_data[key] != value)
+ {
+ // New ITEM OR Data changed
+ m_data[key] = value;
+ for(auto &notifier: m_notifier)
+ {
+ notifier(key.first, key.second, value);
+ }
+ continue;
+ }
+ }
+ }
+
+ void refreshFile(const std::string &file)
+ {
+ std::map<SettingKey, SettingValue> cust_data;
+ parseFile(file, cust_data);
+ refreshData(cust_data);
+ }
+
+ void refreshDirectory(const std::string &dirname)
+ {
+ std::map<SettingKey, SettingValue> cust_data;
+ parseDir(dirname, cust_data, true);
+ refreshData(cust_data);
+ }
+
+ void refreshAll()
+ {
+ std::map<SettingKey, SettingValue> cust_data;
+ parseConf("/etc/kazoe.conf", cust_data, false);
+ parseConf("/var/kazoe.conf", cust_data, true);
+ refreshData(cust_data);
+ }
+};
+};
+
+Settings::Settings()
+ : m_ptr(std::make_unique<SettingsPrivate>(this))
+{
+ m_ptr->parseConf("/etc/kazoe.conf", m_ptr->m_data, false);
+ m_ptr->parseConf("/var/kazoe.conf", m_ptr->m_data, true);
+}
+
+SettingValue Settings::get(const std::string &key, const std::string &category, SettingValue def) const
+{
+ std::lock_guard<std::mutex> lock(m_ptr->m_mutex_data);
+ std::string gkey = key;
+ std::string gcategory = category;
+ if(gcategory.empty() && gkey.starts_with("["))
+ {
+ gcategory = gkey.substr(1, gkey.find(']') - 1);
+ gkey = gkey.substr(gkey.find(']') + 1);
+ }
+
+ SettingKey pkey(gcategory, gkey);
+
+ if(m_ptr->m_data.count(pkey))
+ {
+ return m_ptr->m_data[pkey];
+ }
+ return def;
+}
+
+bool Settings::set(const std::string &key, SettingValue value, const std::string &category)
+{
+ // Check if variable is writable
+ std::lock_guard<std::mutex> lock(m_ptr->m_mutex_data);
+ SettingKey pkey(category, key);
+ std::string id;
+
+ if(!m_ptr->m_owner.count(pkey))
+ {
+ std::cerr << "Variable " << category << ">" << key << " is not writable" << std::endl;
+ return false;
+ }
+
+ if(m_ptr->m_bypass)
+ {
+ id = m_ptr->m_owner[pkey];
+ }
+ else
+ {
+ id = m_ptr->m_id;
+ if(m_ptr->m_owner[pkey] != id)
+ {
+ std::cerr << "Variable " << category << ">" << key << " is not own by " << m_ptr->m_id << std::endl;
+ return false;
+ }
+ }
+
+ // Permission is OK, get old custom variable
+ std::map<SettingKey, SettingValue> cust_data;
+ m_ptr->parseFile("/var/kazoe.conf.d/"+id+".conf", cust_data);
+ cust_data[pkey] = value;
+ std::string last_category = "";
+ std::ofstream file("/var/kazoe.conf.d/"+id+".conf");
+
+ for(const auto& [key, value] : cust_data) {
+ if(key.first != last_category)
+ {
+ last_category = key.first.empty() ? "General" : key.first;
+ file << "[" << last_category << "]\n";
+ }
+ file << key.second << " = " << KaZoe::valueToStr(value) << "\n";
+ }
+
+ m_ptr->m_data[pkey] = value;
+ return true;
+}
+
+std::string Settings::id() const
+{
+ return m_ptr->m_id;
+}
+
+void Settings::setId(const std::string &newid)
+{
+ m_ptr->m_id = newid;
+}
+
+void Settings::setNotifier(const std::function<void (const std::string &, const std::string &, SettingValue)> &callback)
+{
+ std::lock_guard<std::mutex> lock(m_ptr->m_mutex_notifier);
+ m_ptr->m_notifier.push_back(callback);
+ m_ptr->m_watcher.setEnable(true);
+}
+
+std::size_t Settings::size() const
+{
+ std::lock_guard<std::mutex> lock(m_ptr->m_mutex_data);
+ return m_ptr->m_data.size();
+}
+
+void Settings::forEach(const std::function<void (const SettingKey &, const SettingValue &)> &callback) const
+{
+ std::lock_guard<std::mutex> lock(m_ptr->m_mutex_data);
+ for (const auto& [key, value] : m_ptr->m_data) {
+ callback(key, value);
+ }
+}
+
+Settings::~Settings()
+{
+ m_ptr->m_watcher.setEnable(false);
+}
diff --git a/src/settings.h b/src/settings.h
new file mode 100644
index 0000000..5dfe90b
--- /dev/null
+++ b/src/settings.h
@@ -0,0 +1,180 @@
+#ifndef KAZOESETTINGS_H
+#define KAZOESETTINGS_H
+
+#include <functional>
+#include <memory>
+#include <variant>
+#include <string>
+#include <algorithm>
+
+/** @brief Key type for settings, composed of category and name */
+using SettingKey = std::pair<std::string, std::string>;
+
+/** @brief Value type that can hold string, int, double or boolean */
+using SettingValue = std::variant<std::string, int, double, bool>;
+
+
+namespace KaZoe {
+
+class SettingsPrivate;
+
+/**
+ * @brief Main class for managing KaZoe settings
+ *
+ * This class provides a thread-safe interface for storing and retrieving
+ * configuration settings with support for different value types.
+ */
+class Settings
+{
+public:
+ /** @brief Constructs a new Settings instance */
+ explicit Settings();
+
+ /** @brief Virtual destructor */
+ virtual ~Settings();
+
+ /**
+ * @brief Retrieves a setting value
+ * @param key The setting key
+ * @param category The setting category (optional)
+ * @param def Default value if setting not found (optional)
+ * @return The setting value or default if not found
+ */
+ SettingValue get(const std::string &key, const std::string &category = "", SettingValue def = SettingValue()) const;
+
+ /**
+ * @brief Sets a setting value
+ * @param key The setting key
+ * @param value The value to set
+ * @param category The setting category (optional)
+ * @return true if successful, false otherwise
+ */
+ bool set(const std::string &key, SettingValue value, const std::string &category = "");
+
+ /**
+ * @brief Gets the current instance identifier
+ * @return The instance ID string, by default, this is the excecutable filename
+ */
+ std::string id() const;
+
+ /**
+ * @brief Sets the instance identifier
+ * @param newid The new ID to set
+ */
+ void setId(const std::string &newid);
+
+ /**
+ * @brief Sets a callback for setting changes notification
+ * @param callback Function to call when settings change
+ */
+ void setNotifier(const std::function<void(const std::string&, const std::string&, SettingValue)>& callback);
+
+ /**
+ * @brief Gets the number of settings
+ * @return The total count of settings
+ */
+ std::size_t size() const;
+
+ /**
+ * @brief Iterates over all settings in a thread-safe manner
+ * @param callback Function called for each key-value pair
+ */
+ void forEach(const std::function<void(const SettingKey&, const SettingValue&)>& callback) const;
+
+private:
+ std::unique_ptr<SettingsPrivate> m_ptr; ///< Private implementation pointer
+};
+
+
+/**
+ * @brief Functor to convert SettingValue variants to string
+ *
+ * Provides string conversion for all supported setting value types
+ */
+struct settingValueFunctor {
+ std::string operator()(const std::string &x) const { return x; }
+ std::string operator()(int x) const { return std::to_string(x); }
+ std::string operator()(double x) const { return std::to_string(x); }
+ std::string operator()(bool x) const { return x ? "true" : "false"; }
+};
+
+/**
+ * @brief Convert a SettingValue into a string for display
+ * @param value Input SettingValue to convert
+ * @return std::string containing the converted value
+ */
+static inline std::string valueToStr(const SettingValue &value)
+{
+ return std::visit(settingValueFunctor(), value);
+}
+
+/**
+ * @brief Check if a string represents a valid numeric value
+ * @param str String to check
+ * @return true if string is numeric, false otherwise
+ */
+static inline bool is_numeric(const std::string& str) {
+ return !str.empty() &&
+ str.find_first_not_of("0123456789.-") == std::string::npos &&
+ (std::count(str.begin(), str.end(), '.') <= 1) &&
+ (std::count(str.begin(), str.end(), '-') <= 1) &&
+ (str[0] == '-' || std::isdigit(str[0]));
+}
+
+/**
+ * @brief Convert a string to appropriate SettingValue type
+ * @param value Input string to convert
+ * @return SettingValue containing the converted value
+ *
+ * Conversion rules:
+ * - Quoted strings -> string without quotes
+ * - Numeric values -> int or double
+ * - "true"/"on" -> boolean true
+ * - "false"/"off" -> boolean false
+ * - Other -> string
+ */
+static inline SettingValue makeValue(const std::string &value)
+{
+ if(value.starts_with('"') || value.starts_with('\''))
+ {
+ return value.substr(1, value.length() - 2);
+ }
+
+ if(is_numeric(value))
+ {
+ // double
+ if(value.find('.') != value.npos)
+ {
+ return std::stod(value);
+ }
+ else
+ {
+ return std::stoi(value);
+ }
+ }
+
+ // To Lower case
+ std::string lvalue = value;
+ std::transform(lvalue.begin(), lvalue.end(), lvalue.begin(),
+ [](unsigned char c){ return std::tolower(c); });
+
+
+ // bool true
+ if(lvalue == "true" || lvalue == "on")
+ {
+ return true;
+ }
+
+ // bool false
+ if(lvalue == "false" || lvalue == "off")
+ {
+ return false;
+ }
+
+ // else, string without quote
+ return value;
+}
+
+};
+
+#endif // KAZOESETTINGS_H