C++ / fmt (pull_request) Failing after 4s
C++ / build (pull_request) Failing after 12s
JS/TS / fmt (pull_request) Successful in 15s
JS/TS / lint (pull_request) Successful in 15s
C++ / clang-tidy (pull_request) Failing after 33s
Python / static (pull_request) Failing after 36s
Rust / fmt (pull_request) Successful in 1m14s
Rust / build (pull_request) Successful in 1m49s
Rust / clippy (pull_request) Successful in 1m33s
Python / verify (pull_request) Successful in 2m10s
298 lines
6.5 KiB
C++
298 lines
6.5 KiB
C++
#include "config.hpp"
|
|
|
|
#include "appearance.hpp"
|
|
#include "background.hpp"
|
|
#include "bar.hpp"
|
|
#include "clipboard.hpp"
|
|
#include "colors.hpp"
|
|
#include "dashboard.hpp"
|
|
#include "display.hpp"
|
|
#include "dock.hpp"
|
|
#include "general.hpp"
|
|
#include "launcher.hpp"
|
|
#include "llm.hpp"
|
|
#include "lock.hpp"
|
|
#include "migration.hpp"
|
|
#include "notifs.hpp"
|
|
#include "osd.hpp"
|
|
#include "screenshot.hpp"
|
|
#include "services.hpp"
|
|
#include "sidebar.hpp"
|
|
#include "utilities.hpp"
|
|
|
|
#include <QFile>
|
|
#include <QSaveFile>
|
|
#include <QJsonDocument>
|
|
#include <QStandardPaths>
|
|
#include <QDir>
|
|
#include <QDebug>
|
|
#include <QFutureWatcher>
|
|
#include <QFileInfo>
|
|
#include <QFileSystemWatcher>
|
|
#include <qcontainerfwd.h>
|
|
#include <qfileinfo.h>
|
|
#include <qfilesystemwatcher.h>
|
|
#include <qthreadpool.h>
|
|
|
|
namespace ZShell::config {
|
|
|
|
Config* Config::s_instance = nullptr;
|
|
|
|
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_display(new Display(this))
|
|
, m_dock(new Dock(this))
|
|
, m_general(new General(this))
|
|
, m_launcher(new Launcher(this))
|
|
, m_llm(new Llm(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)) {
|
|
s_instance = 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::instance() {
|
|
return s_instance;
|
|
}
|
|
|
|
Config* Config::create(QQmlEngine*, QJSEngine*) {
|
|
if (!s_instance)
|
|
s_instance = new Config();
|
|
return s_instance;
|
|
}
|
|
|
|
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());
|
|
QJsonObject before;
|
|
|
|
bool existed = false;
|
|
bool migrated = false;
|
|
|
|
if (f.open(QIODevice::ReadOnly)) {
|
|
const auto doc = QJsonDocument::fromJson(f.readAll());
|
|
if (doc.isObject()) {
|
|
const QJsonObject raw = doc.object();
|
|
before = ConfigMigrations::apply(raw);
|
|
migrated = before != raw;
|
|
existed = true;
|
|
} else {
|
|
qInfo() << "Config: existing config at" << filePath()
|
|
<< "is empty or not a valid JSON object - using defaults";
|
|
}
|
|
} else {
|
|
qInfo() << "Config: no existing config at" << filePath()
|
|
<< "- using defaults";
|
|
}
|
|
|
|
loadFromJson(QJsonValue(before));
|
|
m_loading = false;
|
|
|
|
const auto after = toJson().toObject();
|
|
if (!existed || migrated || after != before) saveNow();
|
|
}
|
|
|
|
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());
|
|
const bool valid = doc.isObject();
|
|
const QJsonObject raw = valid ? doc.object() : QJsonObject();
|
|
const QJsonObject before = ConfigMigrations::apply(raw);
|
|
const bool migrated = valid && before != raw;
|
|
|
|
QMetaObject::invokeMethod(
|
|
this,
|
|
[this, valid, migrated, before]() {
|
|
loadFromJson(QJsonValue(before));
|
|
m_loading = false;
|
|
|
|
const auto after = toJson().toObject();
|
|
if (!valid || migrated || after != before) saveNow();
|
|
|
|
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
|