MetaImGUI 1.0.0
ImGui Application Template for C++20
Loading...
Searching...
No Matches
ConfigManager.cpp
Go to the documentation of this file.
1/*
2 MetaImGUI
3 Copyright (C) 2026 A P Nicholson
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <https://www.gnu.org/licenses/>.
17*/
18
19#include "ConfigManager.h"
20
21#include "Logger.h"
22
23#include <nlohmann/json.hpp>
24
25#include <fstream>
26#include <mutex>
27#include <shared_mutex>
28
29#ifdef _WIN32
30#include <shlobj.h>
31#include <windows.h>
32#else
33#include <pwd.h>
34#include <sys/stat.h>
35#include <sys/types.h>
36#include <unistd.h>
37#endif
38
39using json = nlohmann::json;
40
41namespace MetaImGUI {
42
43// Pimpl implementation hides the JSON dependency and the synchronisation
44// primitive from every translation unit that includes ConfigManager.h.
45//
46// shared_mutex chosen over plain mutex because reads (Get*) far outnumber
47// writes (Set*/Load/Save/Reset) at runtime — most frames only read.
49 friend class ConfigManager;
50
51private:
52 json config;
53 std::filesystem::path configPath;
54 size_t maxRecentFiles = 10;
55 mutable std::shared_mutex mutex;
56
57 // Default values
58 static constexpr int DEFAULT_WINDOW_WIDTH = 1200;
59 static constexpr int DEFAULT_WINDOW_HEIGHT = 800;
60 static constexpr const char* DEFAULT_THEME = "Modern";
61
62 // Reset the config to defaults assuming the caller already holds the
63 // exclusive lock. Used by Reset() (which takes the lock) and by Load()
64 // on parse failure (which already holds it).
65 void ResetUnlocked() {
66 config = json::object();
67 config["window"]["width"] = DEFAULT_WINDOW_WIDTH;
68 config["window"]["height"] = DEFAULT_WINDOW_HEIGHT;
69 config["window"]["maximized"] = false;
70 config["theme"] = DEFAULT_THEME;
71 config["recentFiles"] = json::array();
72 config["settings"] = json::object();
73 }
74};
75
76ConfigManager::ConfigManager() : m_impl(std::make_unique<Impl>()) {
77 m_impl->configPath = GetConfigPath();
78 // No lock needed: the object isn't visible to any other thread yet.
79 m_impl->ResetUnlocked();
80}
81
82// Out-of-line so unique_ptr<Impl> can hold an incomplete type in the header.
84
86 const std::unique_lock lock(m_impl->mutex);
87 try {
88 if (!std::filesystem::exists(m_impl->configPath)) {
89 LOG_INFO("Config file not found, using defaults");
90 return false;
91 }
92
93 std::ifstream file(m_impl->configPath);
94 if (!file.is_open()) {
95 LOG_ERROR("Failed to open config file: {}", m_impl->configPath.string());
96 return false;
97 }
98
99 m_impl->config = json::parse(file);
100 LOG_INFO("Configuration loaded from: {}", m_impl->configPath.string());
101 return true;
102 } catch (const json::exception& e) {
103 LOG_ERROR("Failed to parse config file: {}", e.what());
104 m_impl->ResetUnlocked(); // Already hold the exclusive lock.
105 return false;
106 } catch (const std::exception& e) {
107 LOG_ERROR("Failed to load config: {}", e.what());
108 return false;
109 }
110}
111
113 // Snapshot under shared lock so concurrent readers aren't blocked on disk I/O.
114 json snapshot;
115 std::filesystem::path path;
116 {
117 const std::shared_lock lock(m_impl->mutex);
118 snapshot = m_impl->config;
119 path = m_impl->configPath;
120 }
121
122 try {
123 if (!EnsureConfigDirectoryExists()) {
124 LOG_ERROR("Failed to create config directory");
125 return false;
126 }
127
128 std::ofstream file(path);
129 if (!file.is_open()) {
130 LOG_ERROR("Failed to open config file for writing: {}", path.string());
131 return false;
132 }
133
134 file << snapshot.dump(2);
135 LOG_INFO("Configuration saved to: {}", path.string());
136 return true;
137 } catch (const std::exception& e) {
138 LOG_ERROR("Failed to save config: {}", e.what());
139 return false;
140 }
141}
142
144 const std::unique_lock lock(m_impl->mutex);
145 m_impl->ResetUnlocked();
146}
147
149 const std::shared_lock lock(m_impl->mutex);
150 return std::filesystem::exists(m_impl->configPath);
151}
152
153std::filesystem::path ConfigManager::GetConfigPath() const {
154 return GetConfigDirectory() / "config.json";
155}
156
157// Window settings
158
160 const std::unique_lock lock(m_impl->mutex);
161 m_impl->config["window"]["x"] = x;
162 m_impl->config["window"]["y"] = y;
163}
164
165void ConfigManager::SetWindowSize(int width, int height) {
166 const std::unique_lock lock(m_impl->mutex);
167 m_impl->config["window"]["width"] = width;
168 m_impl->config["window"]["height"] = height;
169}
170
171std::optional<std::pair<int, int>> ConfigManager::GetWindowPosition() const {
172 const std::shared_lock lock(m_impl->mutex);
173 try {
174 if (m_impl->config.contains("window") && m_impl->config["window"].contains("x") &&
175 m_impl->config["window"].contains("y")) {
176 return std::make_pair(m_impl->config["window"]["x"].get<int>(), m_impl->config["window"]["y"].get<int>());
177 }
178 } catch (const json::exception& e) {
179 LOG_WARNING("Failed to get window position from config: {}", e.what());
180 }
181 return std::nullopt;
182}
183
184std::optional<std::pair<int, int>> ConfigManager::GetWindowSize() const {
185 const std::shared_lock lock(m_impl->mutex);
186 try {
187 if (m_impl->config.contains("window") && m_impl->config["window"].contains("width") &&
188 m_impl->config["window"].contains("height")) {
189 return std::make_pair(m_impl->config["window"]["width"].get<int>(),
190 m_impl->config["window"]["height"].get<int>());
191 }
192 } catch (const json::exception& e) {
193 LOG_WARNING("Failed to get window size from config: {}", e.what());
194 }
195 return std::nullopt;
196}
197
199 const std::unique_lock lock(m_impl->mutex);
200 m_impl->config["window"]["maximized"] = maximized;
201}
202
204 const std::shared_lock lock(m_impl->mutex);
205 try {
206 if (m_impl->config.contains("window") && m_impl->config["window"].contains("maximized")) {
207 return m_impl->config["window"]["maximized"].get<bool>();
208 }
209 } catch (const json::exception& e) {
210 LOG_WARNING("Failed to get window maximized state from config: {}", e.what());
211 }
212 return false;
213}
214
215// Theme settings
216
217void ConfigManager::SetTheme(const std::string& theme) {
218 const std::unique_lock lock(m_impl->mutex);
219 m_impl->config["theme"] = theme;
220}
221
222std::string ConfigManager::GetTheme() const {
223 const std::shared_lock lock(m_impl->mutex);
224 try {
225 if (m_impl->config.contains("theme")) {
226 return m_impl->config["theme"].get<std::string>();
227 }
228 } catch (const json::exception& e) {
229 LOG_WARNING("Failed to get theme from config: {}", e.what());
230 }
231 return Impl::DEFAULT_THEME;
232}
233
234// Recent files
235
236void ConfigManager::AddRecentFile(const std::string& filepath) {
237 const std::unique_lock lock(m_impl->mutex);
238 if (!m_impl->config.contains("recentFiles")) {
239 m_impl->config["recentFiles"] = json::array();
240 }
241
242 auto& recentFiles = m_impl->config["recentFiles"];
243
244 // Remove if already exists (to move to front)
245 for (auto it = recentFiles.begin(); it != recentFiles.end(); ++it) {
246 if (*it == filepath) {
247 recentFiles.erase(it);
248 break;
249 }
250 }
251
252 // Add to front
253 recentFiles.insert(recentFiles.begin(), filepath);
254
255 // Limit size
256 while (recentFiles.size() > m_impl->maxRecentFiles) {
257 recentFiles.erase(recentFiles.end() - 1);
258 }
259}
260
261std::vector<std::string> ConfigManager::GetRecentFiles() const {
262 const std::shared_lock lock(m_impl->mutex);
263 std::vector<std::string> result;
264 try {
265 if (m_impl->config.contains("recentFiles")) {
266 for (const auto& file : m_impl->config["recentFiles"]) {
267 result.push_back(file.get<std::string>());
268 }
269 }
270 } catch (const json::exception& e) {
271 LOG_WARNING("Failed to get recent files from config: {}", e.what());
272 }
273 return result;
274}
275
277 const std::unique_lock lock(m_impl->mutex);
278 m_impl->config["recentFiles"] = json::array();
279}
280
282 const std::unique_lock lock(m_impl->mutex);
283 m_impl->maxRecentFiles = max;
284}
285
286// Generic settings
287
288void ConfigManager::SetString(const std::string& key, const std::string& value) {
289 const std::unique_lock lock(m_impl->mutex);
290 if (!m_impl->config.contains("settings")) {
291 m_impl->config["settings"] = json::object();
292 }
293 m_impl->config["settings"][key] = value;
294}
295
296std::optional<std::string> ConfigManager::GetString(const std::string& key) const {
297 const std::shared_lock lock(m_impl->mutex);
298 try {
299 if (m_impl->config.contains("settings") && m_impl->config["settings"].contains(key)) {
300 return m_impl->config["settings"][key].get<std::string>();
301 }
302 } catch (const json::exception& e) {
303 LOG_WARNING("Failed to get string '{}' from config: {}", key, e.what());
304 }
305 return std::nullopt;
306}
307
308void ConfigManager::SetInt(const std::string& key, int value) {
309 const std::unique_lock lock(m_impl->mutex);
310 if (!m_impl->config.contains("settings")) {
311 m_impl->config["settings"] = json::object();
312 }
313 m_impl->config["settings"][key] = value;
314}
315
316std::optional<int> ConfigManager::GetInt(const std::string& key) const {
317 const std::shared_lock lock(m_impl->mutex);
318 try {
319 if (m_impl->config.contains("settings") && m_impl->config["settings"].contains(key)) {
320 return m_impl->config["settings"][key].get<int>();
321 }
322 } catch (const json::exception& e) {
323 LOG_WARNING("Failed to get int '{}' from config: {}", key, e.what());
324 }
325 return std::nullopt;
326}
327
328void ConfigManager::SetBool(const std::string& key, bool value) {
329 const std::unique_lock lock(m_impl->mutex);
330 if (!m_impl->config.contains("settings")) {
331 m_impl->config["settings"] = json::object();
332 }
333 m_impl->config["settings"][key] = value;
334}
335
336std::optional<bool> ConfigManager::GetBool(const std::string& key) const {
337 const std::shared_lock lock(m_impl->mutex);
338 try {
339 if (m_impl->config.contains("settings") && m_impl->config["settings"].contains(key)) {
340 return m_impl->config["settings"][key].get<bool>();
341 }
342 } catch (const json::exception& e) {
343 LOG_WARNING("Failed to get bool '{}' from config: {}", key, e.what());
344 }
345 return std::nullopt;
346}
347
348void ConfigManager::SetFloat(const std::string& key, float value) {
349 const std::unique_lock lock(m_impl->mutex);
350 if (!m_impl->config.contains("settings")) {
351 m_impl->config["settings"] = json::object();
352 }
353 m_impl->config["settings"][key] = value;
354}
355
356std::optional<float> ConfigManager::GetFloat(const std::string& key) const {
357 const std::shared_lock lock(m_impl->mutex);
358 try {
359 if (m_impl->config.contains("settings") && m_impl->config["settings"].contains(key)) {
360 return m_impl->config["settings"][key].get<float>();
361 }
362 } catch (const json::exception& e) {
363 LOG_WARNING("Failed to get float '{}' from config: {}", key, e.what());
364 }
365 return std::nullopt;
366}
367
368bool ConfigManager::HasKey(const std::string& key) const {
369 const std::shared_lock lock(m_impl->mutex);
370 return m_impl->config.contains("settings") && m_impl->config["settings"].contains(key);
371}
372
373void ConfigManager::RemoveKey(const std::string& key) {
374 const std::unique_lock lock(m_impl->mutex);
375 if (m_impl->config.contains("settings")) {
376 m_impl->config["settings"].erase(key);
377 }
378}
379
380std::vector<std::string> ConfigManager::GetAllKeys() const {
381 const std::shared_lock lock(m_impl->mutex);
382 std::vector<std::string> keys;
383 try {
384 if (m_impl->config.contains("settings")) {
385 for (auto it = m_impl->config["settings"].begin(); it != m_impl->config["settings"].end(); ++it) {
386 keys.push_back(it.key());
387 }
388 }
389 } catch (const json::exception& e) {
390 LOG_WARNING("Failed to get all keys from config: {}", e.what());
391 }
392 return keys;
393}
394
395// Platform-specific helpers
396
397std::filesystem::path ConfigManager::GetConfigDirectory() {
398#ifdef _WIN32
399 // Windows: %APPDATA%/MetaImGUI
400 // Use SHGetKnownFolderPath for long path support (not limited to MAX_PATH)
401 PWSTR path = nullptr;
402 if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, NULL, &path))) {
403 std::filesystem::path result(path);
404 CoTaskMemFree(path); // Must free the allocated string
405 return result / "MetaImGUI";
406 }
407 return std::filesystem::path("./config");
408#elif defined(__APPLE__)
409 // macOS: ~/Library/Application Support/MetaImGUI
410 // NOLINTNEXTLINE(concurrency-mt-unsafe) - Safe: called during single-threaded initialization
411 const char* home = getenv("HOME");
412 if (home) {
413 return std::filesystem::path(home) / "Library" / "Application Support" / "MetaImGUI";
414 }
415 return std::filesystem::path("./config");
416#else
417 // Linux: ~/.config/MetaImGUI
418 // NOLINTNEXTLINE(concurrency-mt-unsafe) - Safe: called during single-threaded initialization
419 const char* xdgConfig = getenv("XDG_CONFIG_HOME");
420 if (xdgConfig != nullptr) {
421 return std::filesystem::path(xdgConfig) / "MetaImGUI";
422 }
423
424 // NOLINTNEXTLINE(concurrency-mt-unsafe) - Safe: called during single-threaded initialization
425 const char* home = getenv("HOME");
426 if (home != nullptr) {
427 return std::filesystem::path(home) / ".config" / "MetaImGUI";
428 }
429
430 return {"./config"};
431#endif
432}
433
434bool ConfigManager::EnsureConfigDirectoryExists() {
435 try {
436 const std::filesystem::path dir = GetConfigDirectory();
437 if (!std::filesystem::exists(dir)) {
438 return std::filesystem::create_directories(dir);
439 }
440 return true;
441 } catch (const std::filesystem::filesystem_error& e) {
442 LOG_ERROR("Failed to create config directory: {}", e.what());
443 return false;
444 }
445}
446
447} // namespace MetaImGUI
nlohmann::json json
#define LOG_INFO(...)
Definition Logger.h:166
#define LOG_ERROR(...)
Definition Logger.h:168
#define LOG_WARNING(...)
Definition Logger.h:167
Configuration manager for persistent application settings.
void Reset()
Reset configuration to defaults.
bool Save()
Save configuration to disk.
void SetWindowMaximized(bool maximized)
void SetBool(const std::string &key, bool value)
void AddRecentFile(const std::string &filepath)
std::optional< bool > GetBool(const std::string &key) const
std::optional< int > GetInt(const std::string &key) const
std::optional< float > GetFloat(const std::string &key) const
void SetString(const std::string &key, const std::string &value)
void SetFloat(const std::string &key, float value)
std::optional< std::pair< int, int > > GetWindowSize() const
std::vector< std::string > GetAllKeys() const
std::optional< std::string > GetString(const std::string &key) const
void RemoveKey(const std::string &key)
std::string GetTheme() const
bool ConfigFileExists() const
Check if configuration file exists.
void SetMaxRecentFiles(size_t max)
bool HasKey(const std::string &key) const
bool Load()
Load configuration from disk.
void SetTheme(const std::string &theme)
void SetWindowPosition(int x, int y)
void SetWindowSize(int width, int height)
std::vector< std::string > GetRecentFiles() const
void SetInt(const std::string &key, int value)
std::optional< std::pair< int, int > > GetWindowPosition() const
std::filesystem::path GetConfigPath() const
Get configuration file path.