feat: config object migration path and add new options to settings
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

This commit is contained in:
2026-08-31 18:43:41 +02:00
parent 487615f482
commit dbb51ceadd
20 changed files with 329 additions and 110 deletions
+123
View File
@@ -0,0 +1,123 @@
#include "migration.hpp"
#include <QDebug>
namespace ZShell::config {
namespace {
// A stored value is "missing" if the path does not resolve to an entry.
// Note: default-constructed QJsonValue is Null, not Undefined.
bool isMissing(const QJsonValue& v) {
return v.isUndefined() || v.isNull();
}
QJsonValue valueAt(const QJsonObject& obj, const QStringList& path) {
QJsonObject cur = obj;
for (int i = 0; i < path.size() - 1; ++i) {
if (!cur.value(path.at(i)).isObject()) return QJsonValue::Undefined;
cur = cur.value(path.at(i)).toObject();
}
const QString key = path.last();
if (!cur.contains(key)) return QJsonValue::Undefined;
return cur.value(key);
}
QJsonObject placeAt(QJsonObject obj, QStringList path, const QJsonValue& value) {
const QString head = path.takeFirst();
if (path.isEmpty()) {
obj[head] = value;
return obj;
}
QJsonObject child = obj.value(head).isObject() ? obj.value(head).toObject()
: QJsonObject();
obj[head] = placeAt(child, path, value);
return obj;
}
// Removes `path` from `obj`. Returns true if a value was removed.
// Parent objects left empty are removed as well.
bool removeAt(QJsonObject& obj, const QStringList& path) {
const QString head = path.first();
if (path.size() == 1) {
if (!obj.contains(head)) return false;
obj.remove(head);
return true;
}
if (!obj.value(head).isObject()) return false;
QJsonObject child = obj.value(head).toObject();
const bool removed = removeAt(child, path.mid(1));
if (child.isEmpty())
obj.remove(head);
else
obj[head] = child;
return removed;
}
} // namespace
const QList<ConfigMigrationRule>& ConfigMigrations::rules() {
static const QList<ConfigMigrationRule> s_rules = {
migrate(
"general.color.scheduleHyprsunset", "display.nightlight.schedule"),
migrate(
"general.color.scheduleHyprsunsetStart",
"display.nightlight.scheduleStart"),
migrate(
"general.color.scheduleHyprsunsetEnd",
"display.nightlight.scheduleEnd"),
migrate("general.color.scheduleDark", "colors.scheduleDark"),
migrate("general.color.scheduleDarkStart", "colors.scheduleDarkStart"),
migrate("general.color.scheduleDarkEnd", "colors.scheduleDarkEnd"),
migrate("general.color.schemeGeneration", "colors.schemeGen"),
migrate(
"general.color.useNativeNightlight",
"display.nightlight.useNative"),
migrate(
"general.color.nativeFadeDuration",
"display.nightlight.fadeDuration"),
migrate("general.color.hyprsunsetTemp", "display.nightlight.temp"),
migrate("general.color.neovimColors", "colors.neovimColors"),
migrate("general.color.mode", "colors.mode"),
migrate("general.color.smart", "colors.smart"),
};
return s_rules;
}
QJsonObject ConfigMigrations::apply(const QJsonObject& json) {
QJsonObject obj = json;
for (const auto& rule : rules()) {
const QStringList from =
rule.from.split(QLatin1Char('.'), Qt::SkipEmptyParts);
const QStringList to =
rule.to.split(QLatin1Char('.'), Qt::SkipEmptyParts);
if (from.isEmpty() || to.isEmpty()) continue;
const QJsonValue oldValue = valueAt(obj, from);
if (isMissing(oldValue)) continue;
const bool targetExists = !isMissing(valueAt(obj, to));
if (!targetExists) obj = placeAt(obj, to, oldValue);
if (removeAt(obj, from)) {
qInfo() << "Config migration:" << rule.from << "->" << rule.to
<< (targetExists ? "(target already set, old value dropped)"
: "(value moved)");
}
}
return obj;
}
} // namespace ZShell::config