Merge branch 'main' into feat/wallpaper-picker-rework
C++ / fmt (pull_request) Successful in 9s
JS/TS / fmt (pull_request) Successful in 13s
JS/TS / lint (pull_request) Successful in 15s
Python / fmt (pull_request) Successful in 26s
Python / lint (pull_request) Successful in 24s
Python / test (pull_request) Successful in 55s
Python / typecheck (pull_request) Successful in 1m9s
Rust / fmt (pull_request) Successful in 41s
Python / buildcheck (pull_request) Successful in 2m36s
C++ / build (pull_request) Successful in 4m3s
Rust / build (pull_request) Successful in 1m53s
Rust / clippy (pull_request) Successful in 1m44s
C++ / clang-tidy (pull_request) Successful in 8m35s

This commit is contained in:
2026-08-16 12:50:58 +02:00
67 changed files with 1333 additions and 958 deletions
+3
View File
@@ -2,6 +2,8 @@
#include "configlist.hpp"
#include "configobject.hpp"
#include <qcontainerfwd.h>
#include <qhashfunctions.h>
#include <qqmlregistration.h>
namespace ZShell::config {
@@ -61,6 +63,7 @@ class Bar : public ConfigObject {
CFG_PROPERTY(int, revealDelay, 100)
CFG_PROPERTY(int, rounding, 14)
CFG_PROPERTY(int, smoothing, 32)
CFG_PROPERTY(QString, position, QStringLiteral("top"))
CONFIG_SUBOBJECT(Tray, tray)
CONFIG_LIST(
EntryList,
+40 -19
View File
@@ -61,6 +61,8 @@ Config::Config(QObject* parent)
m_reloadTimer.setInterval(50);
connect(&m_reloadTimer, &QTimer::timeout, this, &Config::reloadAsync);
connectAutoSave(this);
connect(
&m_watcher,
&QFileSystemWatcher::directoryChanged,
@@ -93,15 +95,31 @@ QString Config::filePath() const {
void Config::loadSync() {
m_loading = true;
QFile f(filePath());
QJsonObject before;
bool existed = false;
if (f.open(QIODevice::ReadOnly)) {
const auto doc = QJsonDocument::fromJson(f.readAll());
if (doc.isObject()) loadFromJson(QJsonValue(doc.object()));
if (doc.isObject()) {
before = doc.object();
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 || after != before) saveNow();
}
void Config::updateWatch() {
@@ -146,27 +164,21 @@ void Config::loadAsync() {
if (f.open(QIODevice::ReadOnly)) {
const auto doc = QJsonDocument::fromJson(f.readAll());
const bool valid = doc.isObject();
const QJsonObject before = valid ? doc.object() : QJsonObject();
if (doc.isObject()) {
QMetaObject::invokeMethod(
this,
[this, doc]() {
loadFromJson(QJsonValue(doc.object()));
m_loading = false;
QMetaObject::invokeMethod(
this,
[this, valid, before]() {
loadFromJson(QJsonValue(before));
m_loading = false;
if (m_reloadPending) m_reloadTimer.start();
},
Qt::QueuedConnection);
} else {
QMetaObject::invokeMethod(
this,
[this]() {
m_loading = false;
const auto after = toJson().toObject();
if (!valid || after != before) saveNow();
if (m_reloadPending) m_reloadTimer.start();
},
Qt::QueuedConnection);
}
if (m_reloadPending) m_reloadTimer.start();
},
Qt::QueuedConnection);
} else {
qInfo() << "Config: failed to reload from" << filePath()
<< "- using in-memory values";
@@ -232,6 +244,15 @@ void Config::saveNow() {
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());
+1
View File
@@ -90,6 +90,7 @@ class Config : public ConfigObject {
void updateWatch();
void loadSync();
void loadAsync();
void connectAutoSave(ConfigNode* node);
QTimer m_saveTimer;
QTimer m_reloadTimer;
+14 -5
View File
@@ -79,13 +79,14 @@ void ConfigList::loadFromJson(const QJsonValue& json) {
if (!json.isArray()) {
qCWarning(
lcConfig,
"Option '%s' must be a list, ignoring",
"Option '%s' must be a list, resetting to default",
qUtf8Printable(propertyPath()));
m_rejectedJson = json;
resetToDefaults();
materializeDefaults();
return;
}
m_rejectedJson = QJsonValue::Undefined;
populate(json.toArray());
m_loaded = true;
}
@@ -93,12 +94,11 @@ void ConfigList::loadFromJson(const QJsonValue& json) {
QJsonValue ConfigList::toJson() const {
if (m_loaded) return elementsToJson();
return m_rejectedJson;
return QJsonValue::Undefined;
}
void ConfigList::clearLoadedKeys() {
m_loaded = false;
m_rejectedJson = QJsonValue::Undefined;
}
QStringList ConfigList::unknownKeys() const {
@@ -113,6 +113,15 @@ QStringList ConfigList::unknownKeys() const {
return keys;
}
void ConfigList::materializeDefaults() {
for (auto* const item : m_items)
item->materializeDefaults();
if (m_global) return;
m_loaded = true;
}
void ConfigList::resyncFromGlobal() {
syncValuesFromGlobal();
}
+3 -5
View File
@@ -33,6 +33,7 @@ class ConfigList : public ConfigNode {
[[nodiscard]] QJsonValue toJson() const override;
void clearLoadedKeys() override;
[[nodiscard]] QStringList unknownKeys() const override;
void materializeDefaults() override;
void resyncFromGlobal() override;
signals:
@@ -71,7 +72,6 @@ class ConfigList : public ConfigNode {
QJsonArray m_defaults;
QList<ConfigObject*> m_items;
bool m_loaded = false;
QJsonValue m_rejectedJson = QJsonValue::Undefined;
};
} // namespace ZShell::config
@@ -103,7 +103,6 @@ class ConfigList : public ConfigNode {
} \
};
#define CONFIG_LIST(Type, name, ...) \
Q_PROPERTY(ZShell::config::Type* name READ name CONSTANT) \
\
@@ -115,6 +114,8 @@ class ConfigList : public ConfigNode {
private: \
Type* m_##name = new Type(this __VA_OPT__(, __VA_ARGS__));
#define LIST_ENTRY(id, enabled) \
vmap({{"id", QString::fromUtf8(#id)}, {"enabled", enabled}})
namespace ZShell::config {
@@ -136,6 +137,3 @@ class ListEntry : public ConfigObject {
CONFIG_LIST_TYPE(ListEntry, EntryList)
} // namespace ZShell::config
#define LIST_ENTRY(id, enabled) \
vmap({{"id", QString::fromUtf8(#id)}, {"enabled", enabled}})
+2
View File
@@ -26,6 +26,8 @@ class ConfigNode : public QObject {
[[nodiscard]] virtual QStringList unknownKeys() const = 0;
[[nodiscard]] virtual QList<ConfigNode*> childNodes() const;
virtual void materializeDefaults() = 0;
void syncFromGlobal(ConfigNode* global);
virtual void resyncFromGlobal() = 0;
+78 -4
View File
@@ -7,6 +7,51 @@
namespace ZShell::config {
namespace {
bool isStringArray(const QJsonArray& arr) {
for (const auto& v : arr) {
if (!v.isString()) return false;
}
return true;
}
bool jsonValueMatchesType(const QJsonValue& val, QMetaType type) {
switch (val.type()) {
case QJsonValue::Bool:
return type.id() == QMetaType::Bool;
case QJsonValue::Double:
switch (type.id()) {
case QMetaType::Int:
case QMetaType::UInt:
case QMetaType::LongLong:
case QMetaType::ULongLong:
case QMetaType::Double:
case QMetaType::Float:
return true;
default:
return false;
}
case QJsonValue::String:
return type.id() == QMetaType::QString;
case QJsonValue::Array:
if (type.id() == QMetaType::QStringList)
return isStringArray(val.toArray());
return type.id() == QMetaType::QVariantList;
case QJsonValue::Object:
case QJsonValue::Null:
case QJsonValue::Undefined:
default:
return false;
}
}
} // namespace
ConfigObject::ConfigObject(QObject* parent) : ConfigNode(parent) {}
void ConfigObject::loadFromJson(const QJsonValue& json) {
@@ -14,6 +59,7 @@ void ConfigObject::loadFromJson(const QJsonValue& json) {
const auto* meta = metaObject();
QSet<QString> known;
QSet<QString> invalid;
for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
auto prop = meta->property(i);
@@ -34,6 +80,15 @@ void ConfigObject::loadFromJson(const QJsonValue& json) {
if (!prop.isWritable()) continue;
if (!jsonValueMatchesType(jsonVal, prop.metaType())) {
qWarning() << "Config: type mismatch for" << key << "in"
<< meta->className() << "- expected"
<< prop.metaType().name() << "got value" << jsonVal
<< "- resetting to default";
invalid.insert(key);
continue;
}
if (prop.metaType().id() == QMetaType::QStringList) {
QStringList list;
const auto jsonArr = jsonVal.toArray();
@@ -56,10 +111,12 @@ void ConfigObject::loadFromJson(const QJsonValue& json) {
m_extras = {};
for (auto it = obj.begin(); it != obj.end(); ++it) {
if (!known.contains(it.key())) {
if (!known.contains(it.key()) || invalid.contains(it.key())) {
m_extras.insert(it.key(), it.value());
}
}
materializeDefaults();
}
QJsonValue ConfigObject::toJson() const {
@@ -104,9 +161,6 @@ QJsonValue ConfigObject::toJson() const {
obj.insert(key, QJsonValue::fromVariant(value));
}
for (auto it = m_extras.begin(); it != m_extras.end(); ++it)
obj.insert(it.key(), it.value());
if (obj.isEmpty()) return QJsonValue::Undefined;
return obj;
@@ -159,6 +213,26 @@ QList<ConfigNode*> ConfigObject::childNodes() const {
return nodes;
}
void ConfigObject::materializeDefaults() {
const auto* meta = metaObject();
for (int i = basePropertyOffset(); i < meta->propertyCount(); ++i) {
const auto prop = meta->property(i);
const auto key = QString::fromUtf8(prop.name());
if (auto* const node = prop.read(this).value<ConfigNode*>()) {
node->materializeDefaults();
continue;
}
if (!prop.isWritable()) continue;
if (m_global) continue;
m_loadedKeys.insert(key);
}
}
void ConfigObject::syncValuesFromGlobal() {
const auto* meta = metaObject();
+1
View File
@@ -62,6 +62,7 @@ class ConfigObject : public ConfigNode {
void clearLoadedKeys() override;
[[nodiscard]] QStringList unknownKeys() const override;
[[nodiscard]] QList<ConfigNode*> childNodes() const override;
void materializeDefaults() override;
void resyncFromGlobal() override;
[[nodiscard]] virtual QStringList identityKeys() const;
+15 -1
View File
@@ -3,7 +3,9 @@
#include "configobject.hpp"
#include <QVariantList>
#include <qobject.h>
#include <qqmlengine.h>
#include <qqmlintegration.h>
#include <qqmlregistration.h>
namespace ZShell::config {
@@ -195,6 +197,16 @@ class AnimTokens : public ConfigObject {
, m_durations(new AnimDurations(this)) {}
};
class BarTokens : public ConfigObject {
Q_OBJECT
QML_ANONYMOUS
CFG_PROPERTY(int, innerSize, 28)
public:
explicit BarTokens(QObject* parent = nullptr) : ConfigObject(parent) {}
};
class Tokens : public ConfigObject {
Q_OBJECT
QML_ELEMENT
@@ -205,6 +217,7 @@ class Tokens : public ConfigObject {
CONFIG_SUBOBJECT(AppearanceSpacing, spacing)
CONFIG_SUBOBJECT(FontTokens, font)
CONFIG_SUBOBJECT(AnimTokens, anim)
CONFIG_SUBOBJECT(BarTokens, bar)
public:
explicit Tokens(QObject* parent = nullptr)
@@ -213,7 +226,8 @@ class Tokens : public ConfigObject {
, m_padding(new AppearancePadding(this))
, m_spacing(new AppearanceSpacing(this))
, m_font(new FontTokens(this))
, m_anim(new AnimTokens(this)) {}
, m_anim(new AnimTokens(this))
, m_bar(new BarTokens(this)) {}
static Tokens* create(QQmlEngine*, QJSEngine*) { return new Tokens(); }
};