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
27#ifdef _WIN32
28#include <shlobj.h>
29#include <windows.h>
30#else
31#include <pwd.h>
32#include <sys/stat.h>
33#include <sys/types.h>
34#include <unistd.h>
35#endif
36
37using json = nlohmann::json;
38
39namespace MetaImGUI {
40
41// Pimpl implementation to hide JSON dependency from header
44 std::filesystem::path configPath;
45 size_t maxRecentFiles = 10;
46
47 // Default values
48 static constexpr int DEFAULT_WINDOW_WIDTH = 1200;
49 static constexpr int DEFAULT_WINDOW_HEIGHT = 800;
50 static constexpr const char* DEFAULT_THEME = "Modern";
51};
52
53ConfigManager::ConfigManager() : m_impl(std::make_unique<Impl>()) {
54 m_impl->configPath = GetConfigPath();
55 Reset(); // Initialize with defaults
56}
57
59
61 try {
62 if (!ConfigFileExists()) {
63 LOG_INFO("Config file not found, using defaults");
64 return false;
65 }
66
67 std::ifstream file(m_impl->configPath);
68 if (!file.is_open()) {
69 LOG_ERROR("Failed to open config file: {}", m_impl->configPath.string());
70 return false;
71 }
72
73 m_impl->config = json::parse(file);
74 LOG_INFO("Configuration loaded from: {}", m_impl->configPath.string());
75 return true;
76 } catch (const json::exception& e) {
77 LOG_ERROR("Failed to parse config file: {}", e.what());
78 Reset(); // Reset to defaults on parse error
79 return false;
80 } catch (const std::exception& e) {
81 LOG_ERROR("Failed to load config: {}", e.what());
82 return false;
83 }
84}
85
87 try {
88 if (!EnsureConfigDirectoryExists()) {
89 LOG_ERROR("Failed to create config directory");
90 return false;
91 }
92
93 std::ofstream file(m_impl->configPath);
94 if (!file.is_open()) {
95 LOG_ERROR("Failed to open config file for writing: {}", m_impl->configPath.string());
96 return false;
97 }
98
99 file << m_impl->config.dump(2); // Pretty print with 2-space indent
100 LOG_INFO("Configuration saved to: {}", m_impl->configPath.string());
101 return true;
102 } catch (const std::exception& e) {
103 LOG_ERROR("Failed to save config: {}", e.what());
104 return false;
105 }
106}
107
109 m_impl->config = json::object();
110
111 // Set default values
112 m_impl->config["window"]["width"] = Impl::DEFAULT_WINDOW_WIDTH;
113 m_impl->config["window"]["height"] = Impl::DEFAULT_WINDOW_HEIGHT;
114 m_impl->config["window"]["maximized"] = false;
115 m_impl->config["theme"] = Impl::DEFAULT_THEME;
116 m_impl->config["recentFiles"] = json::array();
117 m_impl->config["settings"] = json::object();
118}
119
121 return std::filesystem::exists(m_impl->configPath);
122}
123
124std::filesystem::path ConfigManager::GetConfigPath() const {
125 return GetConfigDirectory() / "config.json";
126}
127
128// Window settings
129
131 m_impl->config["window"]["x"] = x;
132 m_impl->config["window"]["y"] = y;
133}
134
135void ConfigManager::SetWindowSize(int width, int height) {
136 m_impl->config["window"]["width"] = width;
137 m_impl->config["window"]["height"] = height;
138}
139
140std::optional<std::pair<int, int>> ConfigManager::GetWindowPosition() const {
141 try {
142 if (m_impl->config.contains("window") && m_impl->config["window"].contains("x") &&
143 m_impl->config["window"].contains("y")) {
144 return std::make_pair(m_impl->config["window"]["x"].get<int>(), m_impl->config["window"]["y"].get<int>());
145 }
146 } catch (const json::exception& e) {
147 LOG_WARNING("Failed to get window position from config: {}", e.what());
148 }
149 return std::nullopt;
150}
151
152std::optional<std::pair<int, int>> ConfigManager::GetWindowSize() const {
153 try {
154 if (m_impl->config.contains("window") && m_impl->config["window"].contains("width") &&
155 m_impl->config["window"].contains("height")) {
156 return std::make_pair(m_impl->config["window"]["width"].get<int>(),
157 m_impl->config["window"]["height"].get<int>());
158 }
159 } catch (const json::exception& e) {
160 LOG_WARNING("Failed to get window size from config: {}", e.what());
161 }
162 return std::nullopt;
163}
164
166 m_impl->config["window"]["maximized"] = maximized;
167}
168
170 try {
171 if (m_impl->config.contains("window") && m_impl->config["window"].contains("maximized")) {
172 return m_impl->config["window"]["maximized"].get<bool>();
173 }
174 } catch (const json::exception& e) {
175 LOG_WARNING("Failed to get window maximized state from config: {}", e.what());
176 }
177 return false;
178}
179
180// Theme settings
181
182void ConfigManager::SetTheme(const std::string& theme) {
183 m_impl->config["theme"] = theme;
184}
185
186std::string ConfigManager::GetTheme() const {
187 try {
188 if (m_impl->config.contains("theme")) {
189 return m_impl->config["theme"].get<std::string>();
190 }
191 } catch (const json::exception& e) {
192 LOG_WARNING("Failed to get theme from config: {}", e.what());
193 }
194 return Impl::DEFAULT_THEME;
195}
196
197// Recent files
198
199void ConfigManager::AddRecentFile(const std::string& filepath) {
200 if (!m_impl->config.contains("recentFiles")) {
201 m_impl->config["recentFiles"] = json::array();
202 }
203
204 auto& recentFiles = m_impl->config["recentFiles"];
205
206 // Remove if already exists (to move to front)
207 for (auto it = recentFiles.begin(); it != recentFiles.end(); ++it) {
208 if (*it == filepath) {
209 recentFiles.erase(it);
210 break;
211 }
212 }
213
214 // Add to front
215 recentFiles.insert(recentFiles.begin(), filepath);
216
217 // Limit size
218 while (recentFiles.size() > m_impl->maxRecentFiles) {
219 recentFiles.erase(recentFiles.end() - 1);
220 }
221}
222
223std::vector<std::string> ConfigManager::GetRecentFiles() const {
224 std::vector<std::string> result;
225 try {
226 if (m_impl->config.contains("recentFiles")) {
227 for (const auto& file : m_impl->config["recentFiles"]) {
228 result.push_back(file.get<std::string>());
229 }
230 }
231 } catch (const json::exception& e) {
232 LOG_WARNING("Failed to get recent files from config: {}", e.what());
233 }
234 return result;
235}
236
238 m_impl->config["recentFiles"] = json::array();
239}
240
242 m_impl->maxRecentFiles = max;
243}
244
245// Generic settings
246
247void ConfigManager::SetString(const std::string& key, const std::string& value) {
248 if (!m_impl->config.contains("settings")) {
249 m_impl->config["settings"] = json::object();
250 }
251 m_impl->config["settings"][key] = value;
252}
253
254std::optional<std::string> ConfigManager::GetString(const std::string& key) const {
255 try {
256 if (m_impl->config.contains("settings") && m_impl->config["settings"].contains(key)) {
257 return m_impl->config["settings"][key].get<std::string>();
258 }
259 } catch (const json::exception& e) {
260 LOG_WARNING("Failed to get string '{}' from config: {}", key, e.what());
261 }
262 return std::nullopt;
263}
264
265void ConfigManager::SetInt(const std::string& key, int value) {
266 if (!m_impl->config.contains("settings")) {
267 m_impl->config["settings"] = json::object();
268 }
269 m_impl->config["settings"][key] = value;
270}
271
272std::optional<int> ConfigManager::GetInt(const std::string& key) const {
273 try {
274 if (m_impl->config.contains("settings") && m_impl->config["settings"].contains(key)) {
275 return m_impl->config["settings"][key].get<int>();
276 }
277 } catch (const json::exception& e) {
278 LOG_WARNING("Failed to get int '{}' from config: {}", key, e.what());
279 }
280 return std::nullopt;
281}
282
283void ConfigManager::SetBool(const std::string& key, bool value) {
284 if (!m_impl->config.contains("settings")) {
285 m_impl->config["settings"] = json::object();
286 }
287 m_impl->config["settings"][key] = value;
288}
289
290std::optional<bool> ConfigManager::GetBool(const std::string& key) const {
291 try {
292 if (m_impl->config.contains("settings") && m_impl->config["settings"].contains(key)) {
293 return m_impl->config["settings"][key].get<bool>();
294 }
295 } catch (const json::exception& e) {
296 LOG_WARNING("Failed to get bool '{}' from config: {}", key, e.what());
297 }
298 return std::nullopt;
299}
300
301void ConfigManager::SetFloat(const std::string& key, float value) {
302 if (!m_impl->config.contains("settings")) {
303 m_impl->config["settings"] = json::object();
304 }
305 m_impl->config["settings"][key] = value;
306}
307
308std::optional<float> ConfigManager::GetFloat(const std::string& key) const {
309 try {
310 if (m_impl->config.contains("settings") && m_impl->config["settings"].contains(key)) {
311 return m_impl->config["settings"][key].get<float>();
312 }
313 } catch (const json::exception& e) {
314 LOG_WARNING("Failed to get float '{}' from config: {}", key, e.what());
315 }
316 return std::nullopt;
317}
318
319bool ConfigManager::HasKey(const std::string& key) const {
320 return m_impl->config.contains("settings") && m_impl->config["settings"].contains(key);
321}
322
323void ConfigManager::RemoveKey(const std::string& key) {
324 if (m_impl->config.contains("settings")) {
325 m_impl->config["settings"].erase(key);
326 }
327}
328
329std::vector<std::string> ConfigManager::GetAllKeys() const {
330 std::vector<std::string> keys;
331 try {
332 if (m_impl->config.contains("settings")) {
333 for (auto it = m_impl->config["settings"].begin(); it != m_impl->config["settings"].end(); ++it) {
334 keys.push_back(it.key());
335 }
336 }
337 } catch (const json::exception& e) {
338 LOG_WARNING("Failed to get all keys from config: {}", e.what());
339 }
340 return keys;
341}
342
343// Platform-specific helpers
344
345std::filesystem::path ConfigManager::GetConfigDirectory() {
346#ifdef _WIN32
347 // Windows: %APPDATA%/MetaImGUI
348 // Use SHGetKnownFolderPath for long path support (not limited to MAX_PATH)
349 PWSTR path = nullptr;
350 if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, NULL, &path))) {
351 std::filesystem::path result(path);
352 CoTaskMemFree(path); // Must free the allocated string
353 return result / "MetaImGUI";
354 }
355 return std::filesystem::path("./config");
356#elif defined(__APPLE__)
357 // macOS: ~/Library/Application Support/MetaImGUI
358 // NOLINTNEXTLINE(concurrency-mt-unsafe) - Safe: called during single-threaded initialization
359 const char* home = getenv("HOME");
360 if (home) {
361 return std::filesystem::path(home) / "Library" / "Application Support" / "MetaImGUI";
362 }
363 return std::filesystem::path("./config");
364#else
365 // Linux: ~/.config/MetaImGUI
366 // NOLINTNEXTLINE(concurrency-mt-unsafe) - Safe: called during single-threaded initialization
367 const char* xdgConfig = getenv("XDG_CONFIG_HOME");
368 if (xdgConfig != nullptr) {
369 return std::filesystem::path(xdgConfig) / "MetaImGUI";
370 }
371
372 // NOLINTNEXTLINE(concurrency-mt-unsafe) - Safe: called during single-threaded initialization
373 const char* home = getenv("HOME");
374 if (home != nullptr) {
375 return std::filesystem::path(home) / ".config" / "MetaImGUI";
376 }
377
378 return {"./config"};
379#endif
380}
381
382bool ConfigManager::EnsureConfigDirectoryExists() {
383 try {
384 const std::filesystem::path dir = GetConfigDirectory();
385 if (!std::filesystem::exists(dir)) {
386 return std::filesystem::create_directories(dir);
387 }
388 return true;
389 } catch (const std::filesystem::filesystem_error& e) {
390 LOG_ERROR("Failed to create config directory: {}", e.what());
391 return false;
392 }
393}
394
395} // namespace MetaImGUI
nlohmann::json json
#define LOG_INFO(...)
Definition Logger.h:218
#define LOG_ERROR(...)
Definition Logger.h:220
#define LOG_WARNING(...)
Definition Logger.h:219
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.
static constexpr const char * DEFAULT_THEME
static constexpr int DEFAULT_WINDOW_HEIGHT
std::filesystem::path configPath
static constexpr int DEFAULT_WINDOW_WIDTH