ACAV f0ba4b7c9529
Abstract Syntax Tree (AST) visualization tool for C, C++, and Objective-C
Loading...
Searching...
No Matches
AppConfig.cpp
1/*$!{
2* Aurora Clang AST Viewer (ACAV)
3*
4* Copyright (c) 2026 Min Liu
5* Copyright (c) 2026 Michael David Adams
6*
7* SPDX-License-Identifier: GPL-2.0-or-later
8*
9* This program is free software; you can redistribute it and/or modify
10* it under the terms of the GNU General Public License as published by
11* the Free Software Foundation; either version 2 of the License, or
12* (at your option) any later version.
13*
14* This program is distributed in the hope that it will be useful,
15* but WITHOUT ANY WARRANTY; without even the implied warranty of
16* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17* GNU General Public License for more details.
18*
19* You should have received a copy of the GNU General Public License along
20* with this program; if not, see <https://www.gnu.org/licenses/>.
21}$!*/
22
23#include "core/AppConfig.h"
24#include <QCryptographicHash>
25#include <QDateTime>
26#include <QDebug>
27#include <QDir>
28#include <QFile>
29#include <QFileInfo>
30#include <QFontDatabase>
31#include <QJsonDocument>
32#include <QJsonObject>
33#include <QStandardPaths>
34#include <QTextStream>
35
36namespace acav {
37
38AppConfig *AppConfig::instance_ = nullptr;
39
40QString AppConfig::getDefaultConfigPath() {
41 QString homeDir =
42 QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
43 return homeDir + "/.acav";
44}
45
46void AppConfig::createDefaultConfigFile(const QString &path) {
47 QFileInfo fileInfo(path);
48 QDir dir = fileInfo.dir();
49 if (!dir.exists()) {
50 dir.mkpath(".");
51 }
52
53 QFile file(path);
54 if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
55 qWarning("Failed to create default config file: %s",
56 qPrintable(path));
57 return;
58 }
59
60 QTextStream out(&file);
61 out << "# ACAV (Clang AST Viewer) Configuration File\n";
62 out << "# Location: ~/.acav\n";
63 out << "# Edit this file to customize application behavior\n";
64 out << "# Use File > Reload Configuration to apply changes\n";
65 out << "\n";
66 out << "[cache]\n";
67 out << "# Directory for storing AST files and dependency analysis results\n";
68 out << "# Use absolute path or leave empty to use ~/.cache/acav/\n";
69 out << "# Default: (empty, uses ~/.cache/acav/)\n";
70 out << "root=\n";
71 out << "\n";
72 out << "[ui]\n";
73 out << "# Font family for UI (empty = system default)\n";
74 out << "# Example: fontFamily=Helvetica Neue\n";
75 out << "fontFamily=\n";
76 out << "\n";
77 out << "# Font size for tree views and source code editor\n";
78 out << "# Valid range: 8-32\n";
79 out << "# Default: 11\n";
80 out << "fontSize=11\n";
81 out << "\n";
82 out << "[ast]\n";
83 out << "# Extract and display comments as properties on Decl nodes\n";
84 out << "# When enabled, documentation comments are stored in the 'comment' property\n";
85 out << "# Default: true\n";
86 out << "commentExtraction=true\n";
87 out << "\n";
88 out << "[debug]\n";
89 out << "# Enable memory profiling checkpoints\n";
90 out << "# Shows peak memory usage at key points during processing\n";
91 out << "# Default: false\n";
92 out << "enableMemoryProfiling=false\n";
93 out << "\n";
94 out << "[performance]\n";
95 out << "# Number of CPU cores for parallel dependency analysis\n";
96 out << "# 0 = auto-detect (use all available cores)\n";
97 out << "# Default: 0\n";
98 out << "parallelProcessorCount=0\n";
99
100 file.close();
101}
102
103void AppConfig::initialize(const QString &configFilePath) {
104 if (instance_) {
105 return; // Already initialized
106 }
107
108 QString configPath = configFilePath.isEmpty()
109 ? getDefaultConfigPath()
110 : configFilePath;
111
112 // Create default config file if it doesn't exist
113 if (!QFile::exists(configPath)) {
114 createDefaultConfigFile(configPath);
115 }
116
117 instance_ = new AppConfig(configPath);
118}
119
121 if (!instance_) {
122 qFatal("AppConfig::initialize() must be called before instance()");
123 }
124 return *instance_;
125}
126
127AppConfig::AppConfig(const QString &configFilePath)
128 : configFilePath_(configFilePath),
129 settings_(configFilePath, QSettings::IniFormat) {
130 loadSettings();
131}
132
134 return configFilePath_;
135}
136
138 if (!QFile::exists(configFilePath_)) {
139 createDefaultConfigFile(configFilePath_);
140 }
141
142 settings_.sync();
143 loadSettings();
144 return QFile::exists(configFilePath_);
145}
146
147void AppConfig::loadSettings() {
148 // Check for config file errors
149 if (settings_.status() != QSettings::NoError) {
150 qWarning("Error reading config file '%s' (status=%d), using defaults",
151 qPrintable(configFilePath_), static_cast<int>(settings_.status()));
152 // Continue with defaults - don't return, as default values are set below
153 }
154
155 // Load cache root from settings, or use default
156 cacheRoot_ = settings_.value("cache/root", QString()).toString();
157 commentExtractionEnabled_ =
158 settings_.value("ast/commentExtraction", true).toBool();
159 memoryProfilingEnabled_ =
160 settings_.value("debug/enableMemoryProfiling", false).toBool();
161
162 // Load and validate font family - check if it exists on the system
163 QString requestedFontFamily = settings_.value("ui/fontFamily", QString()).toString();
164 if (!requestedFontFamily.isEmpty()) {
165 QStringList availableFamilies = QFontDatabase::families();
166 if (availableFamilies.contains(requestedFontFamily, Qt::CaseInsensitive)) {
167 fontFamily_ = requestedFontFamily;
168 } else {
169 qWarning("Font family '%s' not found on system, using system default. "
170 "Available fonts can be listed with 'fc-list' command.",
171 qPrintable(requestedFontFamily));
172 fontFamily_.clear();
173 }
174 } else {
175 fontFamily_.clear();
176 }
177
178 // Load and validate font size (valid range: 8-32)
179 constexpr int kDefaultFontSize = 11;
180 constexpr int kMinFontSize = 8;
181 constexpr int kMaxFontSize = 32;
182 fontSize_ = settings_.value("ui/fontSize", kDefaultFontSize).toInt();
183 if (fontSize_ < kMinFontSize || fontSize_ > kMaxFontSize) {
184 qWarning("Invalid fontSize=%d in config, using default %d (valid range: %d-%d)",
185 fontSize_, kDefaultFontSize, kMinFontSize, kMaxFontSize);
186 fontSize_ = kDefaultFontSize;
187 }
188
189 // Load and validate parallel processor count (must be >= 0)
190 parallelProcessorCount_ = settings_.value("performance/parallelProcessorCount", 0).toInt();
191 if (parallelProcessorCount_ < 0) {
192 qWarning("Invalid parallelProcessorCount=%d in config, using 0 (auto-detect)",
193 parallelProcessorCount_);
194 parallelProcessorCount_ = 0;
195 }
196
197 // Validate cache root - if specified, check if it's usable
198 QString defaultCacheRoot = QStandardPaths::writableLocation(QStandardPaths::HomeLocation)
199 + "/.cache/acav";
200 if (cacheRoot_.isEmpty()) {
201 cacheRoot_ = defaultCacheRoot;
202 } else {
203 // User specified a custom cache root - verify it's usable
204 QDir dir(cacheRoot_);
205 if (!dir.exists()) {
206 // Try to create it
207 if (!dir.mkpath(".")) {
208 qWarning("Cannot create cache directory '%s', using default '%s'",
209 qPrintable(cacheRoot_), qPrintable(defaultCacheRoot));
210 cacheRoot_ = defaultCacheRoot;
211 }
212 } else {
213 // Directory exists, check if writable by trying to create a test file
214 QFile testFile(cacheRoot_ + "/.acav_write_test");
215 if (testFile.open(QIODevice::WriteOnly)) {
216 testFile.close();
217 testFile.remove();
218 } else {
219 qWarning("Cache directory '%s' is not writable, using default '%s'",
220 qPrintable(cacheRoot_), qPrintable(defaultCacheRoot));
221 cacheRoot_ = defaultCacheRoot;
222 }
223 }
224 }
225}
226
227int AppConfig::getFontSize() const { return fontSize_; }
228
229QString AppConfig::getFontFamily() const { return fontFamily_; }
230
232 return parallelProcessorCount_;
233}
234
235QString AppConfig::hashPath(const QString &path) const {
236 // Create a hash of the absolute path to use as directory name
237 QFileInfo fileInfo(path);
238 QString absolutePath = fileInfo.absoluteFilePath();
239
240 QCryptographicHash hash(QCryptographicHash::Sha256);
241 hash.addData(absolutePath.toUtf8());
242 QString hashStr = QString::fromLatin1(hash.result().toHex());
243
244 // Use first 16 characters of hash for reasonable directory name
245 return hashStr.left(16);
246}
247
248QString
249AppConfig::getCacheDirectory(const QString &compilationDatabasePath) const {
250 // Create a subdirectory based on the compilation database path
251 // Structure: <cache_root>/<hash>/
252 QString pathHash = hashPath(compilationDatabasePath);
253 QString cacheDir = cacheRoot_ + "/" + pathHash;
254
255 // Ensure directory exists and write metadata
256 ensureCacheDirectory(compilationDatabasePath);
257
258 return cacheDir;
259}
260
262 const QString &compilationDatabasePath) const {
263 QString cacheDir = getCacheDirectory(compilationDatabasePath);
264 return cacheDir + "/dependencies.json";
265}
266
268 const QString &compilationDatabasePath) const {
269 QString cacheDir = getCacheDirectory(compilationDatabasePath);
270 return cacheDir + "/metadata.json";
271}
272
274 const QString &compilationDatabasePath) const {
275 QString metadataPath = getMetadataFilePath(compilationDatabasePath);
276 QFile metadataFile(metadataPath);
277
278 if (!metadataFile.exists() ||
279 !metadataFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
280 // Fallback: derive from compilation database path
281 QFileInfo info(compilationDatabasePath);
282 return info.absolutePath();
283 }
284
285 QByteArray data = metadataFile.readAll();
286 metadataFile.close();
287
288 QJsonDocument doc = QJsonDocument::fromJson(data);
289 if (doc.isNull() || !doc.isObject()) {
290 QFileInfo info(compilationDatabasePath);
291 return info.absolutePath();
292 }
293
294 QJsonObject metadata = doc.object();
295 return metadata["buildDirectory"].toString();
296}
297
298void AppConfig::ensureCacheDirectory(
299 const QString &compilationDatabasePath) const {
300 QString cacheDir = cacheRoot_ + "/" + hashPath(compilationDatabasePath);
301 QDir dir;
302
303 if (!dir.exists(cacheDir)) {
304 if (!dir.mkpath(cacheDir)) {
305 return;
306 }
307 }
308
309 // Also create a metadata file to remember which compilation database this is
310 QString metadataPath = cacheDir + "/metadata.json";
311 QFile metadataFile(metadataPath);
312 if (!metadataFile.exists()) {
313 if (metadataFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
314 QFileInfo info(compilationDatabasePath);
315 QJsonObject metadata;
316 metadata["compilationDatabasePath"] = info.absoluteFilePath();
317 metadata["buildDirectory"] = info.absolutePath();
318 metadata["createdAt"] = QDateTime::currentDateTime().toString(Qt::ISODate);
319
320 QJsonDocument doc(metadata);
321 metadataFile.write(doc.toJson(QJsonDocument::Indented));
322 metadataFile.close();
323 }
324 }
325}
326
327QString
328AppConfig::getAstCacheDirectory(const QString &compilationDatabasePath) const {
329 QString cacheDir = getCacheDirectory(compilationDatabasePath);
330 QString astDir = cacheDir + "/ast";
331
332 // Create directory if doesn't exist
333 QDir dir;
334 if (!dir.exists(astDir)) {
335 dir.mkpath(astDir);
336 }
337
338 return astDir;
339}
340
341QString AppConfig::getAstFilePath(const QString &compilationDatabasePath,
342 const QString &sourceFilePath) const {
343 QString astDir = getAstCacheDirectory(compilationDatabasePath);
344 QFileInfo sourceInfo(sourceFilePath);
345 QString fileName = sourceInfo.fileName(); // e.g., "main.cpp"
346
347 return astDir + "/" + fileName + ".ast";
348}
349
351 return commentExtractionEnabled_;
352}
353
355 return memoryProfilingEnabled_;
356}
357
358QString AppConfig::getCacheRoot() const { return cacheRoot_; }
359
360} // namespace acav
Application configuration and settings.
Application configuration manager.
Definition AppConfig.h:42
bool reload()
Reload settings from the config file on disk.
QString getAstCacheDirectory(const QString &compilationDatabasePath) const
Get the AST cache directory for a compilation database.
QString getDependenciesFilePath(const QString &compilationDatabasePath) const
Get the dependencies JSON file path for a compilation database.
QString getCacheDirectory(const QString &compilationDatabasePath) const
Get the cache directory for a compilation database.
QString getMetadataFilePath(const QString &compilationDatabasePath) const
Get the metadata JSON file path for a compilation database.
QString getAstFilePath(const QString &compilationDatabasePath, const QString &sourceFilePath) const
Get the AST cache file path for a source file.
QString getCacheRoot() const
Custom cache root directory (default: ~/.cache/acav/).
QString getBuildDirectory(const QString &compilationDatabasePath) const
Get the build directory from cached metadata.
static AppConfig & instance()
Get the singleton instance.
static void initialize(const QString &configFilePath=QString())
Initialize the singleton with a custom config file path.
bool getCommentExtractionEnabled() const
Whether to extract and display comments in AST (default: false).
bool getMemoryProfilingEnabled() const
Whether memory profiling checkpoints are enabled (default: false).
QString getFontFamily() const
Editor/UI font family (default: empty = system default).
int getParallelProcessorCount() const
Number of parallel processors for query-dependencies (default: 0 = auto) 0 means use all available CP...
QString getConfigFilePath() const
Get the path to the currently loaded configuration file.
int getFontSize() const
Editor/UI font size (default: 11).