#include "migration.hpp" #include 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& ConfigMigrations::rules() { static const QList 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