#include "config.hpp" #include "appearance.hpp" #include "background.hpp" #include "bar.hpp" #include "clipboard.hpp" #include "colors.hpp" #include "dashboard.hpp" #include "dock.hpp" #include "general.hpp" #include "launcher.hpp" #include "lock.hpp" #include "notifs.hpp" #include "osd.hpp" #include "screenshot.hpp" #include "services.hpp" #include "sidebar.hpp" #include "utilities.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace ZShell::config { Config::Config(QObject* parent) : ConfigObject(parent) , m_appearance(new Appearance(this)) , m_background(new Background(this)) , m_bar(new Bar(this)) , m_clipboard(new Clipboard(this)) , m_colors(new Colors(this)) , m_dashboard(new Dashboard(this)) , m_dock(new Dock(this)) , m_general(new General(this)) , m_launcher(new Launcher(this)) , m_lock(new Lock(this)) , m_notifs(new Notifs(this)) , m_osd(new Osd(this)) , m_screenshot(new Screenshot(this)) , m_services(new Services(this)) , m_sidebar(new Sidebar(this)) , m_utilities(new Utilities(this)) { connect(this, &ConfigObject::propertiesChanged, this, &Config::scheduleSave); m_saveTimer.setSingleShot(true); m_saveTimer.setInterval(300); connect(&m_saveTimer, &QTimer::timeout, this, &Config::flushAsync); m_reloadTimer.setSingleShot(true); m_reloadTimer.setInterval(50); connect(&m_reloadTimer, &QTimer::timeout, this, &Config::reloadAsync); connectAutoSave(this); connect( &m_watcher, &QFileSystemWatcher::directoryChanged, this, &Config::onWatcherEvent); connect( &m_watcher, &QFileSystemWatcher::fileChanged, this, &Config::onWatcherEvent); loadSync(); updateWatch(); m_firstLoadDone = true; } Config* Config::create(QQmlEngine*, QJSEngine*) { return new Config(); } QString Config::filePath() const { const QString dir = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QStringLiteral("/zshell"); QDir().mkpath(dir); return dir + QStringLiteral("/config.json"); } void Config::loadSync() { m_loading = true; QFile f(filePath()); if (f.open(QIODevice::ReadOnly)) { const auto doc = QJsonDocument::fromJson(f.readAll()); if (doc.isObject()) loadFromJson(QJsonValue(doc.object())); } else { qInfo() << "Config: no existing config at" << filePath() << "- using defaults"; } m_loading = false; } void Config::updateWatch() { const QFileInfo info(filePath()); const QString dir = info.absolutePath(); const QString file = info.absoluteFilePath(); if (!m_watcher.directories().contains(dir)) m_watcher.addPath(dir); if (info.exists() && !m_watcher.files().contains(file)) m_watcher.addPath(file); } void Config::onWatcherEvent(const QString&) { updateWatch(); const QFileInfo info(filePath()); if (!info.exists()) return; QFile file(info.absoluteFilePath()); if (!file.open(QIODevice::ReadOnly)) return; const QByteArray hash = QCryptographicHash::hash(file.readAll(), QCryptographicHash::Sha256); { QMutexLocker lock(&m_writeMutex); if (hash == m_lastWriteHash) return; } m_reloadPending = true; if (!m_loading) m_reloadTimer.start(); } void Config::loadAsync() { m_loading = true; auto future = QtConcurrent::run([this]() { QFile f(filePath()); if (f.open(QIODevice::ReadOnly)) { const auto doc = QJsonDocument::fromJson(f.readAll()); if (doc.isObject()) { QMetaObject::invokeMethod( this, [this, doc]() { loadFromJson(QJsonValue(doc.object())); m_loading = false; if (m_reloadPending) m_reloadTimer.start(); }, Qt::QueuedConnection); } else { QMetaObject::invokeMethod( this, [this]() { m_loading = false; if (m_reloadPending) m_reloadTimer.start(); }, Qt::QueuedConnection); } } else { qInfo() << "Config: failed to reload from" << filePath() << "- using in-memory values"; QMetaObject::invokeMethod( this, [this]() { m_loading = false; if (m_reloadPending) m_reloadTimer.start(); }, Qt::QueuedConnection); } }); m_loadFuture = future; } void Config::load() { if (!m_firstLoadDone) { loadSync(); } else { loadAsync(); } } void Config::reloadAsync() { if (m_loading) return; m_reloadPending = false; loadAsync(); } void Config::scheduleSave() { if (m_loading) return; m_saveTimer.start(); } void Config::flushAsync() { const QByteArray data = QJsonDocument(toJson().toObject()).toJson(QJsonDocument::Indented); const QByteArray hash = QCryptographicHash::hash(data, QCryptographicHash::Sha256); QThreadPool::globalInstance()->start([this, data, hash]() { const bool success = writeAtomically(data); QMetaObject::invokeMethod( this, [this, success, hash]() { if (!success) return; QMutexLocker lock(&m_writeMutex); m_lastWriteHash = hash; }, Qt::QueuedConnection); }); } void Config::saveNow() { m_saveTimer.stop(); flushAsync(); } void Config::connectAutoSave(ConfigNode* node) { connect(node, &ConfigNode::propertiesChanged, this, [this] { scheduleSave(); }); for (auto* child : node->childNodes()) connectAutoSave(child); } bool Config::writeAtomically(const QByteArray& data) { QSaveFile f(filePath()); if (!f.open(QIODevice::WriteOnly)) { qWarning() << "Config: failed to open for atomic write:" << f.errorString(); return false; } if (f.write(data) != data.size()) { qWarning() << "Config: failed to write:" << f.errorString(); return false; } if (!f.commit()) { qWarning() << "Config: commit failed:" << f.errorString(); return false; } return true; } }; // namespace ZShell::config