diff --git a/Components/CustomFlickable.qml b/Components/CustomFlickable.qml index 796fa77..556d439 100644 --- a/Components/CustomFlickable.qml +++ b/Components/CustomFlickable.qml @@ -30,7 +30,6 @@ Flickable { onTriggered: { root.doneFakeFlick = false; - root.returnToBounds(); } } } diff --git a/Greeter/Services/Colors.qml b/Greeter/Services/Colors.qml index ee9d678..ac4dce6 100644 --- a/Greeter/Services/Colors.qml +++ b/Greeter/Services/Colors.qml @@ -13,21 +13,17 @@ import qs.Paths Singleton { id: root - readonly property M3Palette current: M3Palette { - } + readonly property M3Palette current: M3Palette {} property bool currentLight property string flavour readonly property bool light: showPreview ? previewLight : currentLight readonly property M3Palette palette: showPreview ? preview : current - readonly property M3Palette preview: M3Palette { - } + readonly property M3Palette preview: M3Palette {} property bool previewLight property string scheme property bool showPreview - readonly property M3TPalette tPalette: M3TPalette { - } - readonly property Transparency transparency: Transparency { - } + readonly property M3TPalette tPalette: M3TPalette {} + readonly property Transparency transparency: Transparency {} readonly property alias wallLuminance: analyser.luminance function alterColor(c: color, a: real, layer: int): color { @@ -112,8 +108,7 @@ Singleton { function setMode(mode: string): void { Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--mode", mode]); - Config.general.color.mode = mode; - Config.save(); + Config.colors.mode = mode; } function swapRG(c: color): color { diff --git a/Helpers/Hyprsunset.qml b/Helpers/Hyprsunset.qml index 540091d..29857f3 100644 --- a/Helpers/Hyprsunset.qml +++ b/Helpers/Hyprsunset.qml @@ -9,13 +9,13 @@ Singleton { id: root readonly property bool enabled: service.enabled - readonly property int end: Config.general.color.scheduleHyprsunsetEnd + readonly property int end: Config.display.nightlight.scheduleEnd property bool manualToggle: false - readonly property int start: Config.general.color.scheduleHyprsunsetStart - readonly property int temp: Config.general.color.hyprsunsetTemp + readonly property int start: Config.display.nightlight.scheduleStart + readonly property int temp: Config.display.nightlight.temp function checkStartup(): void { - if (!Config.general.color.scheduleHyprsunset) + if (!Config.display.nightlight.schedule) return; service.apply(); @@ -29,11 +29,11 @@ Singleton { NightlightManager { id: service - activeAuto: Config.general.color.scheduleHyprsunset + activeAuto: Config.display.nightlight.schedule endTime: root.end startTime: root.start temp: root.temp - fadeDuration: Config.general.color.nativeFadeDuration / 1000 - useNativeNightlight: Config.general.color.useNativeNightlight + fadeDuration: Config.display.nightlight.fadeDuration / 1000 + useNativeNightlight: Config.display.nightlight.useNative } } diff --git a/Helpers/ModeScheduler.qml b/Helpers/ModeScheduler.qml index a45e15a..14b6aa8 100644 --- a/Helpers/ModeScheduler.qml +++ b/Helpers/ModeScheduler.qml @@ -11,14 +11,14 @@ import qs.Services Singleton { id: root - readonly property int darkEnd: Config.general.color.scheduleDarkEnd - readonly property int darkStart: Config.general.color.scheduleDarkStart - readonly property bool enabled: Config.general.color.scheduleDark && Config.general.color.schemeGeneration + readonly property int darkEnd: Config.colors.scheduleDarkEnd + readonly property int darkStart: Config.colors.scheduleDarkStart + readonly property bool enabled: Config.colors.scheduleDark && Config.colors.schemeGen function applyDarkMode() { Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--mode", "dark"]); - Config.general.color.mode = "dark"; + Config.colors.mode = "dark"; Quickshell.execDetached(["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", "'prefer-dark'"]); @@ -30,13 +30,13 @@ Singleton { function applyLightMode() { Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--mode", "light"]); - Config.general.color.mode = "light"; + Config.colors.mode = "light"; Quickshell.execDetached(["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", "'prefer-light'"]); Quickshell.execDetached(["sh", "-c", `sed -i 's/color_scheme_path=\\(.*\\)Dark.colors/color_scheme_path=\\1Light.colors/' ${Paths.home}/.config/qt6ct/qt6ct.conf`]); - if (Config.general.color.neovimColors) + if (Config.colors.neovimColors) Quickshell.execDetached(["sed", "-i", "'s/\\(vim.cmd.colorscheme \\).*/\\1\"onelight\"/'", "~/.config/nvim/lua/config/load-colorscheme.lua"]); } diff --git a/Helpers/Wallpapers.qml b/Helpers/Wallpapers.qml index ba4b64c..d02b01a 100644 --- a/Helpers/Wallpapers.qml +++ b/Helpers/Wallpapers.qml @@ -38,7 +38,7 @@ Searcher { previewPath = path; showPreview = true; - if (Config.general.color.schemeGeneration) + if (Config.colors.schemeGen) previewColorsProc.running = true; } @@ -62,8 +62,8 @@ Searcher { WallpaperPath.currentWallpaperPath = path; Quickshell.screens.forEach(n => setCrop(n.name, Qt.rect(0, 0, 1, 1), 1.0)); Quickshell.execDetached(["zshell-cli", "wallpaper", "lockscreen", "--input-image", `${root.actualCurrent}`, "--output-path", `${Paths.state}/lockscreen_bg.png`, "--blur-amount", `${Config.lock.blurAmount}`]); - if (Config.general.color.schemeGeneration) - Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.general.color.mode}`]); + if (Config.colors.schemeGen) + Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.colors.mode}`]); } function stopPreview(): void { diff --git a/Modules/Settings/Pages.qml b/Modules/Settings/Pages.qml index ad148b1..4aacc6d 100644 --- a/Modules/Settings/Pages.qml +++ b/Modules/Settings/Pages.qml @@ -43,17 +43,17 @@ Item { Component.onCompleted: root.loadPage(root.sState.currentPageIdx) - CustomRect { - anchors.fill: parent - color: Qt.alpha(Colors.palette.m3shadow, 0.3) - opacity: root.sState.dimmed ? 1 : 0 - visible: opacity > 0 - z: 0 - - Behavior on opacity { - Anim {} - } - } + // CustomRect { + // anchors.fill: parent + // color: Qt.alpha(Colors.palette.m3shadow, 0.3) + // opacity: root.sState.dimmed ? 1 : 0 + // visible: opacity > 0 + // z: 0 + // + // Behavior on opacity { + // Anim {} + // } + // } } Connections { diff --git a/Modules/Settings/Pages/ServicesPage.qml b/Modules/Settings/Pages/ServicesPage.qml index e179635..8a7ffb7 100644 --- a/Modules/Settings/Pages/ServicesPage.qml +++ b/Modules/Settings/Pages/ServicesPage.qml @@ -202,12 +202,12 @@ PageBase { } ToggleRow { - checked: Config.general.color.smart + checked: Config.colors.smart settingAnchor: "services-smart-color-scheme" subtext: qsTr("Derive theme mode from the wallpaper") text: qsTr("Smart color scheme") - onToggled: Config.general.color.smart = checked + onToggled: Config.colors.smart = checked } SelectRow { diff --git a/Modules/Settings/Pages/Wallpaper/ColorsFonts.qml b/Modules/Settings/Pages/Wallpaper/ColorsFonts.qml index 44a490e..be09d14 100644 --- a/Modules/Settings/Pages/Wallpaper/ColorsFonts.qml +++ b/Modules/Settings/Pages/Wallpaper/ColorsFonts.qml @@ -73,15 +73,15 @@ PageBase { ToggleRow { text: qsTr("Dynamic colors") first: true - subtext: qsTr("Automatic color scheme based on wallpaper is %1").arg(Config.general.color.schemeGeneration ? "enabled" : "disabled") - checked: Config.general.color.schemeGeneration + subtext: qsTr("Automatic color scheme based on wallpaper is %1").arg(Config.colors.schemeGen ? "enabled" : "disabled") + checked: Config.colors.schemeGen settingAnchor: "colors-fonts-dynamic-colors" onToggled: { - Config.general.color.schemeGeneration = checked; + Config.colors.schemeGen = checked; if (checked) - Colors.setMode(Config.general.color.mode); + Colors.setMode(Config.colors.mode); } } @@ -89,7 +89,7 @@ PageBase { settingAnchor: "colors-fonts-preset-scheme" model: root.schemes acceptLabel: qsTr("Confirm") - enabled: !Config.general.color.schemeGeneration + enabled: !Config.colors.schemeGen header: qsTr("Select scheme") rowButton.activeItem: root.schemes.find(s => { var regex = new RegExp(Config.colors.presets.name, "i"); @@ -157,7 +157,7 @@ PageBase { } // qmlformat on acceptLabel: qsTr("Confirm") - enabled: !Config.general.color.schemeGeneration && model.length > 1 + enabled: !Config.colors.schemeGen && model.length > 1 header: qsTr("Select variant") rowButton.activeItem: { const v = Config.colors.presets.variant ?? ""; @@ -210,7 +210,7 @@ PageBase { } // qmlformat on acceptLabel: qsTr("Confirm") - enabled: !Config.general.color.schemeGeneration && model.length > 1 + enabled: !Config.colors.schemeGen && model.length > 1 header: qsTr("Select accent") rowButton.activeItem: { const a = Config.colors.presets.accent ?? ""; diff --git a/Modules/Settings/Pages/WallpaperPage.qml b/Modules/Settings/Pages/WallpaperPage.qml index 5e7035c..8c4b524 100644 --- a/Modules/Settings/Pages/WallpaperPage.qml +++ b/Modules/Settings/Pages/WallpaperPage.qml @@ -96,20 +96,21 @@ PageBase { TimeDialogSelect { readonly property string endTime: { var d = new Date(0, 0, 0, 0, 0, 0, 0); - d.setMinutes(Config.general.color.scheduleDarkEnd); + d.setMinutes(Config.colors.scheduleDarkEnd); return Qt.formatTime(d, "hh:mm AP"); } readonly property string startTime: { var d = new Date(0, 0, 0, 0, 0, 0, 0); - d.setMinutes(Config.general.color.scheduleDarkStart); + d.setMinutes(Config.colors.scheduleDarkStart); return Qt.formatTime(d, "hh:mm AP"); } - rowButton.checked: Config.general.color.scheduleDark - rowButton.onToggled: Config.general.color.scheduleDark = rowButton.checked - start: Config.general.color.scheduleDarkStart - end: Config.general.color.scheduleDarkEnd + rowButton.checked: Config.colors.scheduleDark + rowButton.onToggled: Config.colors.scheduleDark = rowButton.checked + start: Config.colors.scheduleDarkStart + end: Config.colors.scheduleDarkEnd first: true + last: true settingAnchor: "style-schedule-dark-mode" acceptLabel: qsTr("Apply") rootParent: root.flickable @@ -122,8 +123,8 @@ PageBase { } onAccepted: { - Config.general.color.scheduleDarkStart = startCurrent; - Config.general.color.scheduleDarkEnd = endCurrent; + Config.colors.scheduleDarkStart = startCurrent; + Config.colors.scheduleDarkEnd = endCurrent; ModeScheduler.checkStartup(); } } @@ -131,37 +132,77 @@ PageBase { TimeDialogSelect { readonly property string endTime: { var d = new Date(0, 0, 0, 0, 0, 0, 0); - d.setMinutes(Config.general.color.scheduleHyprsunsetEnd); + d.setMinutes(Config.display.nightlight.scheduleEnd); return Qt.formatTime(d, "hh:mm AP"); } readonly property string startTime: { var d = new Date(0, 0, 0, 0, 0, 0, 0); - d.setMinutes(Config.general.color.scheduleHyprsunsetStart); + d.setMinutes(Config.display.nightlight.scheduleStart); return Qt.formatTime(d, "hh:mm AP"); } - Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing - rowButton.checked: Config.general.color.scheduleHyprsunset - rowButton.onToggled: Config.general.color.scheduleHyprsunset = rowButton.checked - start: Config.general.color.scheduleHyprsunsetStart - end: Config.general.color.scheduleHyprsunsetEnd - last: true - settingAnchor: "style-schedule-hyprsunset" + rowButton.checked: Config.display.nightlight.schedule + rowButton.onToggled: Config.display.nightlight.schedule = rowButton.checked + start: Config.display.nightlight.scheduleStart + end: Config.display.nightlight.scheduleEnd + first: true + settingAnchor: "style-schedule-nightlight" acceptLabel: qsTr("Apply") rootParent: root.flickable header: qsTr("Select time") - subtext: qsTr("Hyprsunset will turn on at %1, and turn off at %2.").arg(startTime).arg(endTime) - label: qsTr("Schedule hyprsunset") + subtext: qsTr("Nightlight will turn on at %1, and turn off at %2.").arg(startTime).arg(endTime) + label: qsTr("Schedule nightlight") onOpenChanged: { root.sState.dimmed = open; } onAccepted: { - Config.general.color.scheduleHyprsunsetStart = startCurrent; - Config.general.color.scheduleHyprsunsetEnd = endCurrent; + Config.display.nightlight.scheduleStart = startCurrent; + Config.display.nightlight.scheduleEnd = endCurrent; Hyprsunset.checkStartup(); } } + + ToggleRow { + Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing + + checked: Config.display.nightlight.useNative + settingAnchor: "display-nightlight-useNative" + text: qsTr("Use native") + subtext: qsTr("Use native shader nightlight for more features") + + onToggled: Config.display.nightlight.useNative = checked + } + + SpinRow { + Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing + + from: 2000 + settingAnchor: "display-nightlight-temp" + stepSize: 100 + subtext: qsTr("How warm/orange the screen will be. Lower is warmer (Kelvin)") + text: qsTr("Temperature") + to: 6500 + value: Config.display.nightlight.temp + + onMoved: v => Config.display.nightlight.temp = v + } + + SpinRow { + Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing + + from: 0 + settingAnchor: "display-nightlight-fade-duration" + enabled: Config.display.nightlight.useNative + last: true + stepSize: 1 + subtext: qsTr("How long it takes for the gradual nightlight to finish (s)") + text: qsTr("Fade duration") + to: 60 * 5 + value: Config.display.nightlight.fadeDuration / 1000 + + onMoved: v => Config.display.nightlight.fadeDuration = v * 1000 + } } } diff --git a/Plugins/ZShell/Config/CMakeLists.txt b/Plugins/ZShell/Config/CMakeLists.txt index 55b3f7d..dfe9d59 100644 --- a/Plugins/ZShell/Config/CMakeLists.txt +++ b/Plugins/ZShell/Config/CMakeLists.txt @@ -5,6 +5,7 @@ qml_module(ZShell-config configobject.hpp configobject.cpp configlist.hpp configlist.cpp config.hpp config.cpp + migration.hpp migration.cpp anim.hpp anim.cpp tokens.hpp appearance.hpp @@ -13,6 +14,7 @@ qml_module(ZShell-config clipboard.hpp colors.hpp dashboard.hpp + display.hpp dock.hpp general.hpp launcher.hpp diff --git a/Plugins/ZShell/Config/colors.hpp b/Plugins/ZShell/Config/colors.hpp index 0554df6..20f8657 100644 --- a/Plugins/ZShell/Config/colors.hpp +++ b/Plugins/ZShell/Config/colors.hpp @@ -22,6 +22,13 @@ class Colors : public ConfigObject { CONFIG_SUBOBJECT(Presets, presets) CFG_PROPERTY(QString, schemeType, QStringLiteral("fidelity")) + CFG_PROPERTY(bool, scheduleDark, false) + CFG_PROPERTY(int, scheduleDarkEnd, 600) + CFG_PROPERTY(int, scheduleDarkStart, 1140) + CFG_PROPERTY(bool, schemeGen, true) + CFG_PROPERTY(bool, neovimColors, false) + CFG_PROPERTY(QString, mode, QStringLiteral("dark")) + CFG_PROPERTY(bool, smart, false) public: explicit Colors(QObject* parent = nullptr) diff --git a/Plugins/ZShell/Config/config.cpp b/Plugins/ZShell/Config/config.cpp index 8e0515d..6f3083f 100644 --- a/Plugins/ZShell/Config/config.cpp +++ b/Plugins/ZShell/Config/config.cpp @@ -6,11 +6,13 @@ #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" @@ -44,6 +46,7 @@ Config::Config(QObject* parent) , 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)) @@ -111,11 +114,14 @@ void Config::loadSync() { QJsonObject before; bool existed = false; + bool migrated = false; if (f.open(QIODevice::ReadOnly)) { const auto doc = QJsonDocument::fromJson(f.readAll()); if (doc.isObject()) { - before = doc.object(); + const QJsonObject raw = doc.object(); + before = ConfigMigrations::apply(raw); + migrated = before != raw; existed = true; } else { qInfo() << "Config: existing config at" << filePath() @@ -130,7 +136,7 @@ void Config::loadSync() { m_loading = false; const auto after = toJson().toObject(); - if (!existed || after != before) saveNow(); + if (!existed || migrated || after != before) saveNow(); } void Config::updateWatch() { @@ -176,16 +182,18 @@ 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(); + const QJsonObject raw = valid ? doc.object() : QJsonObject(); + const QJsonObject before = ConfigMigrations::apply(raw); + const bool migrated = valid && before != raw; QMetaObject::invokeMethod( this, - [this, valid, before]() { + [this, valid, migrated, before]() { loadFromJson(QJsonValue(before)); m_loading = false; const auto after = toJson().toObject(); - if (!valid || after != before) saveNow(); + if (!valid || migrated || after != before) saveNow(); if (m_reloadPending) m_reloadTimer.start(); }, diff --git a/Plugins/ZShell/Config/config.hpp b/Plugins/ZShell/Config/config.hpp index 6bbc3d3..2ad92ea 100644 --- a/Plugins/ZShell/Config/config.hpp +++ b/Plugins/ZShell/Config/config.hpp @@ -21,6 +21,7 @@ class Bar; class Clipboard; class Colors; class Dashboard; +class Display; class Dock; class General; class Launcher; @@ -44,6 +45,7 @@ class Config : public ConfigObject { Q_MOC_INCLUDE("clipboard.hpp") Q_MOC_INCLUDE("colors.hpp") Q_MOC_INCLUDE("dashboard.hpp") + Q_MOC_INCLUDE("display.hpp") Q_MOC_INCLUDE("dock.hpp") Q_MOC_INCLUDE("general.hpp") Q_MOC_INCLUDE("launcher.hpp") @@ -62,6 +64,7 @@ class Config : public ConfigObject { CONFIG_SUBOBJECT(Clipboard, clipboard) CONFIG_SUBOBJECT(Colors, colors) CONFIG_SUBOBJECT(Dashboard, dashboard) + CONFIG_SUBOBJECT(Display, display) CONFIG_SUBOBJECT(Dock, dock) CONFIG_SUBOBJECT(General, general) CONFIG_SUBOBJECT(Launcher, launcher) diff --git a/Plugins/ZShell/Config/display.hpp b/Plugins/ZShell/Config/display.hpp new file mode 100644 index 0000000..b22d1e8 --- /dev/null +++ b/Plugins/ZShell/Config/display.hpp @@ -0,0 +1,33 @@ +#pragma once +#include "configobject.hpp" +#include + +namespace ZShell::config { + +class Nightlight : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CFG_PROPERTY(bool, schedule, true) + CFG_PROPERTY(int, scheduleStart, 1200) + CFG_PROPERTY(int, scheduleEnd, 570) + CFG_PROPERTY(bool, useNative, true) + CFG_PROPERTY(int, fadeDuration, 2000) + CFG_PROPERTY(int, temp, 2600) + + public: + explicit Nightlight(QObject* parent = nullptr) : ConfigObject(parent) {} +}; + +class Display : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_SUBOBJECT(Nightlight, nightlight) + + public: + explicit Display(QObject* parent = nullptr) + : ConfigObject(parent), m_nightlight(new Nightlight(this)) {} +}; + +} // namespace ZShell::config diff --git a/Plugins/ZShell/Config/general.hpp b/Plugins/ZShell/Config/general.hpp index 9f6a70f..558ff82 100644 --- a/Plugins/ZShell/Config/general.hpp +++ b/Plugins/ZShell/Config/general.hpp @@ -91,28 +91,6 @@ class Battery : public ConfigObject { explicit Battery(QObject* parent = nullptr) : ConfigObject(parent) {} }; -class ColorSettings : public ConfigObject { - Q_OBJECT - QML_ANONYMOUS - - CFG_PROPERTY(int, hyprsunsetTemp, 2600) - CFG_PROPERTY(QString, mode, QStringLiteral("dark")) - CFG_PROPERTY(bool, neovimColors, false) - CFG_PROPERTY(bool, useNativeNightlight, true) - CFG_PROPERTY(int, nativeFadeDuration, 2000) - CFG_PROPERTY(bool, scheduleDark, false) - CFG_PROPERTY(int, scheduleDarkEnd, 600) - CFG_PROPERTY(int, scheduleDarkStart, 1140) - CFG_PROPERTY(bool, scheduleHyprsunset, true) - CFG_PROPERTY(int, scheduleHyprsunsetEnd, 570) - CFG_PROPERTY(int, scheduleHyprsunsetStart, 1200) - CFG_PROPERTY(bool, schemeGeneration, true) - CFG_PROPERTY(bool, smart, false) - - public: - explicit ColorSettings(QObject* parent = nullptr) : ConfigObject(parent) {} -}; - class Idle : public ConfigObject { Q_OBJECT QML_ANONYMOUS @@ -137,7 +115,6 @@ class General : public ConfigObject { CONFIG_SUBOBJECT(Apps, apps) CONFIG_SUBOBJECT(Battery, battery) - CONFIG_SUBOBJECT(ColorSettings, color) CFG_PROPERTY(QString, dateFormat, QStringLiteral("ddd d MMM - hh:mm:ss")) CFG_PROPERTY(bool, desktopIcons, true) CONFIG_SUBOBJECT(Idle, idle) @@ -153,7 +130,6 @@ class General : public ConfigObject { : ConfigObject(parent) , m_apps(new Apps(this)) , m_battery(new Battery(this)) - , m_color(new ColorSettings(this)) , m_idle(new Idle(this)) {} }; diff --git a/Plugins/ZShell/Config/migration.cpp b/Plugins/ZShell/Config/migration.cpp new file mode 100644 index 0000000..da78206 --- /dev/null +++ b/Plugins/ZShell/Config/migration.cpp @@ -0,0 +1,123 @@ +#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 diff --git a/Plugins/ZShell/Config/migration.hpp b/Plugins/ZShell/Config/migration.hpp new file mode 100644 index 0000000..1c03505 --- /dev/null +++ b/Plugins/ZShell/Config/migration.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +namespace ZShell::config { + +// A single migration rule: move the value stored at the dotted `from` path +// to the dotted `to` path, then delete the old key (and any parent objects +// left empty). +// +// Rules are applied to the raw JSON file on every load, before the schema +// loads it. They are idempotent: once the old key is gone the rule never +// fires again, so no version stamping is needed. Old rules can stay in the +// table forever. +// +// WARNING: the target path must exist in the schema (i.e. a ConfigObject +// property somewhere in the tree). If it does not, the migrated value is +// loaded as an unknown key and silently dropped on the next save. +struct ConfigMigrationRule { + QString from; + QString to; +}; + +// Helper for writing rules: migrate("general.color.foo", "display.bar.foo") +inline ConfigMigrationRule migrate(const QString& from, const QString& to) { + return {from, to}; +} + +class ConfigMigrations { + public: + [[nodiscard]] static const QList& rules(); + + // Applies all rules to `json`, returning the migrated object. + [[nodiscard]] static QJsonObject apply(const QJsonObject& json); +}; + +} // namespace ZShell::config diff --git a/Plugins/ZShell/Services/nightlightmanager.cpp b/Plugins/ZShell/Services/nightlightmanager.cpp index 78658d4..ea50b1d 100644 --- a/Plugins/ZShell/Services/nightlightmanager.cpp +++ b/Plugins/ZShell/Services/nightlightmanager.cpp @@ -10,7 +10,6 @@ namespace ZShell::services { namespace { constexpr const char* NL_EVENT = "zshell-nightlight"; -constexpr int NEUTRAL_KELVIN = 6500; constexpr int RECONNECT_MS = 2000; } // namespace @@ -404,7 +403,6 @@ void NightlightManager::handleEventLine(const QByteArray& line) { if (line.left(sep) != NL_EVENT) return; - // data = enabled,animating,last,current const auto fields = line.mid(sep + 2).split(','); if (fields.size() < 4) return; diff --git a/Services/Colors.qml b/Services/Colors.qml index 863c03f..970281d 100644 --- a/Services/Colors.qml +++ b/Services/Colors.qml @@ -13,21 +13,17 @@ import qs.Paths Singleton { id: root - readonly property M3Palette current: M3Palette { - } + readonly property M3Palette current: M3Palette {} property bool currentLight property string flavour readonly property bool light: showPreview ? previewLight : currentLight readonly property M3Palette palette: showPreview ? preview : current - readonly property M3Palette preview: M3Palette { - } + readonly property M3Palette preview: M3Palette {} property bool previewLight property string scheme property bool showPreview - readonly property M3TPalette tPalette: M3TPalette { - } - readonly property Transparency transparency: Transparency { - } + readonly property M3TPalette tPalette: M3TPalette {} + readonly property Transparency transparency: Transparency {} readonly property alias wallLuminance: analyser.luminance function alterColor(c: color, a: real, layer: int): color { @@ -112,7 +108,7 @@ Singleton { function setMode(mode: string): void { Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--mode", mode]); - Config.general.color.mode = mode; + Config.colors.mode = mode; } function swapRG(c: color): color { diff --git a/cli/src/zshell/subcommands/scheme.py b/cli/src/zshell/subcommands/scheme.py index 2a65612..f0c8e72 100644 --- a/cli/src/zshell/subcommands/scheme.py +++ b/cli/src/zshell/subcommands/scheme.py @@ -625,8 +625,8 @@ def generate( scheme_type = config["colors"].get("schemeType", "fruit-salad") scheme = scheme or scheme_type assert isinstance(scheme, str) - config_mode = config["general"]["color"]["mode"] - smart = bool(config["general"]["color"].get("smart", False)) + config_mode = config["colors"]["mode"] + smart = bool(config["colors"].get("smart", False)) scheme_class = get_scheme_class(scheme) p_variant = "default"