From c4b5c5d4382aea4595dd0b9a712e27b7160b5a66 Mon Sep 17 00:00:00 2001 From: Lemmy Date: Wed, 6 May 2026 10:54:57 -0400 Subject: [PATCH] config/settings: effective override, aka discard override when the value is identical --- src/config/config_overrides.cpp | 487 ++++++++++++++++++++++- src/config/config_service.cpp | 97 ++--- src/config/config_service.h | 7 + src/shell/settings/bar_widget_editor.cpp | 26 +- src/shell/settings/settings_content.cpp | 15 +- src/shell/settings/settings_window.cpp | 2 +- 6 files changed, 555 insertions(+), 79 deletions(-) diff --git a/src/config/config_overrides.cpp b/src/config/config_overrides.cpp index ecd686487..411e9d50f 100644 --- a/src/config/config_overrides.cpp +++ b/src/config/config_overrides.cpp @@ -3,9 +3,12 @@ #include "util/file_utils.h" #include +#include +#include #include #include #include +#include #include #include @@ -13,6 +16,340 @@ namespace { constexpr Logger kLog("config"); constexpr const char* kInternalStateTable = "noctalia_state"; constexpr const char* kSetupWizardCompletedKey = "setup_wizard_completed"; + constexpr double kConfigFloatEpsilon = 1.0e-5; + + std::string overrideCacheKey(const std::vector& path) { + std::string key; + for (const auto& part : path) { + if (!key.empty()) { + key.push_back('.'); + } + key += part; + } + return key; + } + + bool nearlyEqual(double a, double b) noexcept { return std::abs(a - b) <= kConfigFloatEpsilon; } + + bool colorEqual(const Color& a, const Color& b) noexcept { + return nearlyEqual(a.r, b.r) && nearlyEqual(a.g, b.g) && nearlyEqual(a.b, b.b) && nearlyEqual(a.a, b.a); + } + + bool colorSpecEqual(const ColorSpec& a, const ColorSpec& b) noexcept { + return a.role == b.role && colorEqual(a.fixed, b.fixed) && nearlyEqual(a.alpha, b.alpha); + } + + bool optionalDoubleEqual(const std::optional& a, const std::optional& b) noexcept { + if (a.has_value() != b.has_value()) { + return false; + } + return !a.has_value() || nearlyEqual(*a, *b); + } + + bool optionalColorSpecEqual(const std::optional& a, const std::optional& b) noexcept { + if (a.has_value() != b.has_value()) { + return false; + } + return !a.has_value() || colorSpecEqual(*a, *b); + } + + template + bool vectorEqual(const std::vector& a, const std::vector& b, Equal equal) { + if (a.size() != b.size()) { + return false; + } + for (std::size_t i = 0; i < a.size(); ++i) { + if (!equal(a[i], b[i])) { + return false; + } + } + return true; + } + + std::optional numericWidgetSetting(const WidgetSettingValue& value) { + if (const auto* i = std::get_if(&value)) { + return static_cast(*i); + } + if (const auto* d = std::get_if(&value)) { + return *d; + } + return std::nullopt; + } + + bool widgetSettingEqual(const WidgetSettingValue& a, const WidgetSettingValue& b) { + const auto aNum = numericWidgetSetting(a); + const auto bNum = numericWidgetSetting(b); + if (aNum.has_value() || bNum.has_value()) { + return aNum.has_value() && bNum.has_value() && nearlyEqual(*aNum, *bNum); + } + if (a.index() != b.index()) { + return false; + } + return std::visit( + [&](const auto& av) { + using T = std::decay_t; + const auto* bv = std::get_if(&b); + return bv != nullptr && av == *bv; + }, + a); + } + + bool widgetSettingsEqual(const std::unordered_map& a, + const std::unordered_map& b) { + if (a.size() != b.size()) { + return false; + } + for (const auto& [key, value] : a) { + const auto it = b.find(key); + if (it == b.end() || !widgetSettingEqual(value, it->second)) { + return false; + } + } + return true; + } + + bool barBaseConfigEqual(const BarConfig& a, const BarConfig& b) { + return a.name == b.name && a.position == b.position && a.enabled == b.enabled && a.autoHide == b.autoHide && + a.reserveSpace == b.reserveSpace && a.thickness == b.thickness && + nearlyEqual(a.backgroundOpacity, b.backgroundOpacity) && a.radius == b.radius && + a.radiusTopLeft == b.radiusTopLeft && a.radiusTopRight == b.radiusTopRight && + a.radiusBottomLeft == b.radiusBottomLeft && a.radiusBottomRight == b.radiusBottomRight && + a.marginEnds == b.marginEnds && a.marginEdge == b.marginEdge && a.padding == b.padding && + a.widgetSpacing == b.widgetSpacing && a.shadow == b.shadow && a.contactShadow == b.contactShadow && + a.attachPanels == b.attachPanels && nearlyEqual(a.scale, b.scale) && a.startWidgets == b.startWidgets && + a.centerWidgets == b.centerWidgets && a.endWidgets == b.endWidgets && + a.widgetCapsuleDefault == b.widgetCapsuleDefault && + colorSpecEqual(a.widgetCapsuleFill, b.widgetCapsuleFill) && + optionalColorSpecEqual(a.widgetCapsuleForeground, b.widgetCapsuleForeground) && + optionalColorSpecEqual(a.widgetColor, b.widgetColor) && a.widgetCapsuleGroups == b.widgetCapsuleGroups && + nearlyEqual(a.widgetCapsulePadding, b.widgetCapsulePadding) && + nearlyEqual(a.widgetCapsuleOpacity, b.widgetCapsuleOpacity) && + a.widgetCapsuleBorderSpecified == b.widgetCapsuleBorderSpecified && + optionalColorSpecEqual(a.widgetCapsuleBorder, b.widgetCapsuleBorder); + } + + BarConfig applyMonitorOverrideForComparison(const BarConfig& base, const BarMonitorOverride& ovr) { + BarConfig resolved = base; + resolved.monitorOverrides.clear(); + if (ovr.enabled) { + resolved.enabled = *ovr.enabled; + } + if (ovr.autoHide) { + resolved.autoHide = *ovr.autoHide; + } + if (ovr.reserveSpace) { + resolved.reserveSpace = *ovr.reserveSpace; + } + if (ovr.thickness) { + resolved.thickness = *ovr.thickness; + } + if (ovr.backgroundOpacity) { + resolved.backgroundOpacity = *ovr.backgroundOpacity; + } + if (ovr.radius) { + resolved.radius = *ovr.radius; + resolved.radiusTopLeft = *ovr.radius; + resolved.radiusTopRight = *ovr.radius; + resolved.radiusBottomLeft = *ovr.radius; + resolved.radiusBottomRight = *ovr.radius; + } + if (ovr.radiusTopLeft) { + resolved.radiusTopLeft = *ovr.radiusTopLeft; + } + if (ovr.radiusTopRight) { + resolved.radiusTopRight = *ovr.radiusTopRight; + } + if (ovr.radiusBottomLeft) { + resolved.radiusBottomLeft = *ovr.radiusBottomLeft; + } + if (ovr.radiusBottomRight) { + resolved.radiusBottomRight = *ovr.radiusBottomRight; + } + if (ovr.marginEnds) { + resolved.marginEnds = *ovr.marginEnds; + } + if (ovr.marginEdge) { + resolved.marginEdge = *ovr.marginEdge; + } + if (ovr.padding) { + resolved.padding = *ovr.padding; + } + if (ovr.widgetSpacing) { + resolved.widgetSpacing = *ovr.widgetSpacing; + } + if (ovr.shadow) { + resolved.shadow = *ovr.shadow; + } + if (ovr.contactShadow) { + resolved.contactShadow = *ovr.contactShadow; + } + if (ovr.attachPanels) { + resolved.attachPanels = *ovr.attachPanels; + } + if (ovr.startWidgets) { + resolved.startWidgets = *ovr.startWidgets; + } + if (ovr.centerWidgets) { + resolved.centerWidgets = *ovr.centerWidgets; + } + if (ovr.endWidgets) { + resolved.endWidgets = *ovr.endWidgets; + } + if (ovr.scale) { + resolved.scale = *ovr.scale; + } + if (ovr.widgetCapsuleDefault) { + resolved.widgetCapsuleDefault = *ovr.widgetCapsuleDefault; + } + if (ovr.widgetCapsuleFill) { + resolved.widgetCapsuleFill = *ovr.widgetCapsuleFill; + } + if (ovr.widgetCapsuleBorderSpecified) { + resolved.widgetCapsuleBorderSpecified = true; + resolved.widgetCapsuleBorder = ovr.widgetCapsuleBorder; + } + if (ovr.widgetCapsuleForeground) { + resolved.widgetCapsuleForeground = *ovr.widgetCapsuleForeground; + } + if (ovr.widgetColor) { + resolved.widgetColor = *ovr.widgetColor; + } + if (ovr.widgetCapsuleGroups) { + resolved.widgetCapsuleGroups = *ovr.widgetCapsuleGroups; + } + if (ovr.widgetCapsulePadding) { + resolved.widgetCapsulePadding = std::clamp(static_cast(*ovr.widgetCapsulePadding), 0.0f, 48.0f); + } + if (ovr.widgetCapsuleOpacity) { + resolved.widgetCapsuleOpacity = std::clamp(static_cast(*ovr.widgetCapsuleOpacity), 0.0f, 1.0f); + } + return resolved; + } + + bool barMonitorOverrideEqual(const BarConfig& base, const BarMonitorOverride& a, const BarMonitorOverride& b) { + return a.match == b.match && + barBaseConfigEqual(applyMonitorOverrideForComparison(base, a), applyMonitorOverrideForComparison(base, b)); + } + + bool barConfigEqual(const BarConfig& a, const BarConfig& b) { + return barBaseConfigEqual(a, b) && vectorEqual(a.monitorOverrides, b.monitorOverrides, + [&a](const BarMonitorOverride& lhs, const BarMonitorOverride& rhs) { + return barMonitorOverrideEqual(a, lhs, rhs); + }); + } + + bool widgetConfigEqual(const WidgetConfig& a, const WidgetConfig& b) { + return a.type == b.type && widgetSettingsEqual(a.settings, b.settings); + } + + bool widgetMapEqual(const std::unordered_map& a, + const std::unordered_map& b) { + if (a.size() != b.size()) { + return false; + } + for (const auto& [key, value] : a) { + const auto it = b.find(key); + if (it == b.end() || !widgetConfigEqual(value, it->second)) { + return false; + } + } + return true; + } + + bool wallpaperMonitorOverrideEqual(const WallpaperMonitorOverride& a, const WallpaperMonitorOverride& b) { + return a.match == b.match && a.enabled == b.enabled && optionalColorSpecEqual(a.fillColor, b.fillColor) && + a.directory == b.directory && a.directoryLight == b.directoryLight && a.directoryDark == b.directoryDark; + } + + bool wallpaperConfigEqual(const WallpaperConfig& a, const WallpaperConfig& b) { + return a.enabled == b.enabled && a.fillMode == b.fillMode && optionalColorSpecEqual(a.fillColor, b.fillColor) && + a.transitions == b.transitions && nearlyEqual(a.transitionDurationMs, b.transitionDurationMs) && + nearlyEqual(a.edgeSmoothness, b.edgeSmoothness) && a.directory == b.directory && + a.directoryLight == b.directoryLight && a.directoryDark == b.directoryDark && + a.automation.enabled == b.automation.enabled && + a.automation.intervalMinutes == b.automation.intervalMinutes && a.automation.order == b.automation.order && + a.automation.recursive == b.automation.recursive && + vectorEqual(a.monitorOverrides, b.monitorOverrides, wallpaperMonitorOverrideEqual); + } + + bool dockConfigEqual(const DockConfig& a, const DockConfig& b) { + return a.enabled == b.enabled && a.position == b.position && a.activeMonitorOnly == b.activeMonitorOnly && + a.iconSize == b.iconSize && a.padding == b.padding && a.itemSpacing == b.itemSpacing && + nearlyEqual(a.backgroundOpacity, b.backgroundOpacity) && a.radius == b.radius && + a.marginEnds == b.marginEnds && a.marginEdge == b.marginEdge && a.shadow == b.shadow && + a.showRunning == b.showRunning && a.autoHide == b.autoHide && a.reserveSpace == b.reserveSpace && + nearlyEqual(a.activeScale, b.activeScale) && nearlyEqual(a.inactiveScale, b.inactiveScale) && + nearlyEqual(a.activeOpacity, b.activeOpacity) && nearlyEqual(a.inactiveOpacity, b.inactiveOpacity) && + a.showInstanceCount == b.showInstanceCount && a.pinned == b.pinned; + } + + bool shellConfigEqual(const ShellConfig& a, const ShellConfig& b) { + return nearlyEqual(a.uiScale, b.uiScale) && a.fontFamily == b.fontFamily && a.lang == b.lang && + a.offlineMode == b.offlineMode && a.telemetryEnabled == b.telemetryEnabled && + a.polkitAgent == b.polkitAgent && a.passwordMaskStyle == b.passwordMaskStyle && + a.animation.enabled == b.animation.enabled && nearlyEqual(a.animation.speed, b.animation.speed) && + a.avatarPath == b.avatarPath && a.settingsShowAdvanced == b.settingsShowAdvanced && + a.showLocation == b.showLocation && a.clipboardAutoPaste == b.clipboardAutoPaste && + a.shadow.blur == b.shadow.blur && a.shadow.offsetX == b.shadow.offsetX && + a.shadow.offsetY == b.shadow.offsetY && nearlyEqual(a.shadow.alpha, b.shadow.alpha) && + a.panel.backgroundBlur == b.panel.backgroundBlur && a.screenCorners.enabled == b.screenCorners.enabled && + a.screenCorners.size == b.screenCorners.size && a.mpris.blacklist == b.mpris.blacklist; + } + + bool notificationConfigEqual(const NotificationConfig& a, const NotificationConfig& b) { + return a.enableDaemon == b.enableDaemon && a.position == b.position && a.layer == b.layer && + nearlyEqual(a.backgroundOpacity, b.backgroundOpacity) && a.monitors == b.monitors; + } + + bool audioConfigEqual(const AudioConfig& a, const AudioConfig& b) { + return a.enableOverdrive == b.enableOverdrive && a.enableSounds == b.enableSounds && + nearlyEqual(a.soundVolume, b.soundVolume) && a.volumeChangeSound == b.volumeChangeSound && + a.notificationSound == b.notificationSound; + } + + bool nightLightConfigEqual(const NightLightConfig& a, const NightLightConfig& b) { + return a.enabled == b.enabled && a.force == b.force && a.useWeatherLocation == b.useWeatherLocation && + a.startTime == b.startTime && a.stopTime == b.stopTime && optionalDoubleEqual(a.latitude, b.latitude) && + optionalDoubleEqual(a.longitude, b.longitude) && a.dayTemperature == b.dayTemperature && + a.nightTemperature == b.nightTemperature; + } + + bool idleConfigEqual(const IdleConfig& a, const IdleConfig& b) { + return vectorEqual(a.behaviors, b.behaviors, [](const IdleBehaviorConfig& lhs, const IdleBehaviorConfig& rhs) { + return lhs.name == rhs.name && lhs.enabled == rhs.enabled && lhs.timeoutSeconds == rhs.timeoutSeconds && + lhs.command == rhs.command && lhs.resumeCommand == rhs.resumeCommand; + }); + } + + bool themeConfigEqual(const ThemeConfig& a, const ThemeConfig& b) { + return a.source == b.source && a.builtinPalette == b.builtinPalette && a.communityPalette == b.communityPalette && + a.wallpaperScheme == b.wallpaperScheme && a.mode == b.mode && + a.templates.enableBuiltinTemplates == b.templates.enableBuiltinTemplates && + a.templates.builtinIds == b.templates.builtinIds && + a.templates.enableCommunityTemplates == b.templates.enableCommunityTemplates && + a.templates.communityIds == b.templates.communityIds && + a.templates.enableUserTemplates == b.templates.enableUserTemplates && + a.templates.userConfig == b.templates.userConfig; + } + + bool configEqual(const Config& a, const Config& b) { + return vectorEqual(a.bars, b.bars, barConfigEqual) && widgetMapEqual(a.widgets, b.widgets) && + wallpaperConfigEqual(a.wallpaper, b.wallpaper) && a.backdrop.enabled == b.backdrop.enabled && + nearlyEqual(a.backdrop.blurIntensity, b.backdrop.blurIntensity) && + nearlyEqual(a.backdrop.tintIntensity, b.backdrop.tintIntensity) && dockConfigEqual(a.dock, b.dock) && + a.desktopWidgets == b.desktopWidgets && shellConfigEqual(a.shell, b.shell) && + a.osd.position == b.osd.position && notificationConfigEqual(a.notification, b.notification) && + a.weather.enabled == b.weather.enabled && a.weather.autoLocate == b.weather.autoLocate && + a.weather.effects == b.weather.effects && a.weather.address == b.weather.address && + a.weather.refreshMinutes == b.weather.refreshMinutes && a.weather.unit == b.weather.unit && + a.system.monitor.enabled == b.system.monitor.enabled && audioConfigEqual(a.audio, b.audio) && + a.brightness == b.brightness && a.keybinds.validate == b.keybinds.validate && + a.keybinds.cancel == b.keybinds.cancel && a.keybinds.left == b.keybinds.left && + a.keybinds.right == b.keybinds.right && a.keybinds.up == b.keybinds.up && + a.keybinds.down == b.keybinds.down && nightLightConfigEqual(a.nightlight, b.nightlight) && + idleConfigEqual(a.idle, b.idle) && a.hooks == b.hooks && themeConfigEqual(a.theme, b.theme) && + a.controlCenter == b.controlCenter; + } toml::table* ensureTable(toml::table& parent, std::string_view key) { if (auto* existing = parent.get_as(key)) { @@ -99,6 +436,46 @@ namespace { parent->erase(changedPath[depth - 1]); } } + + bool eraseOverridePath(toml::table& root, const std::vector& path, std::size_t preserveDepth = 0) { + if (path.empty()) { + return false; + } + + toml::table* table = &root; + for (std::size_t i = 0; i + 1 < path.size(); ++i) { + auto* next = table->get_as(path[i]); + if (next == nullptr) { + return false; + } + table = next; + } + + if (table->erase(path.back()) == 0) { + return false; + } + pruneEmptyOverrideTables(root, path, preserveDepth); + return true; + } + + std::vector sortedConfigTomlFiles(std::string_view configDir) { + std::vector files; + if (configDir.empty()) { + return files; + } + + std::error_code ec; + if (!std::filesystem::is_directory(configDir, ec) || ec) { + return files; + } + for (const auto& entry : std::filesystem::directory_iterator(configDir, ec)) { + if (entry.is_regular_file() && entry.path().extension() == ".toml") { + files.push_back(entry.path()); + } + } + std::sort(files.begin(), files.end()); + return files; + } } // namespace void ConfigService::setThemeMode(ThemeMode mode) { @@ -168,6 +545,94 @@ bool ConfigService::hasOverride(const std::vector& path) const { return findOverrideNode(m_overridesTable, path) != nullptr; } +bool ConfigService::hasEffectiveOverride(const std::vector& path) const { + if (path.empty() || findOverrideNode(m_overridesTable, path) == nullptr) { + return false; + } + + const std::string key = overrideCacheKey(path); + if (const auto it = m_effectiveOverrideCache.find(key); it != m_effectiveOverrideCache.end()) { + return it->second; + } + + const bool effective = overridePathEffectiveInTable(path, m_overridesTable, &m_config); + m_effectiveOverrideCache[key] = effective; + return effective; +} + +std::size_t ConfigService::overridePreserveDepthForPath(const std::vector& path) const { + if (path.size() > 4 && path[0] == "bar" && path[2] == "monitor" && isOverrideOnlyMonitorOverride(path[1], path[3])) { + return 4; + } + if (path.size() > 2 && path[0] == "bar" && isOverrideOnlyBar(path[1])) { + return 2; + } + return 0; +} + +std::optional ConfigService::configForOverrides(const toml::table& overrides) const { + Config parsed; + seedBuiltinWidgets(parsed); + + const auto files = sortedConfigTomlFiles(m_configDir); + toml::table merged; + for (const auto& path : files) { + try { + auto tbl = toml::parse_file(path.string()); + deepMerge(merged, tbl); + } catch (const toml::parse_error& e) { + kLog.warn("skipping parse error in effective override comparison {}: {}", path.filename().string(), + e.description()); + } + } + + deepMerge(merged, overrides); + if (files.empty() && overrides.empty()) { + parsed.idle.behaviors.push_back(IdleBehaviorConfig{ + .name = "lock", + .enabled = false, + .timeoutSeconds = 660, + .command = "noctalia:screen-lock", + .resumeCommand = "", + }); + parsed.bars.push_back(BarConfig{}); + return parsed; + } + + try { + parseTableInto(merged, parsed, false); + } catch (const std::exception& e) { + kLog.warn("effective override comparison parse failed: {}", e.what()); + return std::nullopt; + } + return parsed; +} + +bool ConfigService::overridePathEffectiveInTable(const std::vector& path, const toml::table& overrides, + const Config* parsedWith) const { + if (path.empty() || findOverrideNode(overrides, path) == nullptr) { + return false; + } + + std::optional ownedWithOverride; + if (parsedWith == nullptr) { + ownedWithOverride = configForOverrides(overrides); + if (!ownedWithOverride.has_value()) { + return true; + } + parsedWith = &*ownedWithOverride; + } + + toml::table withoutTable = overrides; + eraseOverridePath(withoutTable, path, overridePreserveDepthForPath(path)); + auto withoutOverride = configForOverrides(withoutTable); + if (!withoutOverride.has_value()) { + return true; + } + + return !configEqual(*parsedWith, *withoutOverride); +} + bool ConfigService::isOverrideOnlyBar(std::string_view name) const { if (name.empty() || !hasOverride({"bar", std::string(name)})) { return false; @@ -412,6 +877,9 @@ bool ConfigService::setOverride(const std::vector& path, ConfigOver } insertOverrideValue(*table, path.back(), value); + if (!overridePathEffectiveInTable(path, m_overridesTable)) { + eraseOverridePath(m_overridesTable, path, overridePreserveDepthForPath(path)); + } if (!writeOverridesToFile()) { kLog.warn("failed to write {}", m_overridesPath); @@ -430,26 +898,9 @@ bool ConfigService::clearOverride(const std::vector& path) { return false; } - std::size_t preserveDepth = 0; - if (path.size() > 4 && path[0] == "bar" && path[2] == "monitor" && isOverrideOnlyMonitorOverride(path[1], path[3])) { - preserveDepth = 4; - } else if (path.size() > 2 && path[0] == "bar" && isOverrideOnlyBar(path[1])) { - preserveDepth = 2; - } - - toml::table* table = &m_overridesTable; - for (std::size_t i = 0; i + 1 < path.size(); ++i) { - auto* next = table->get_as(path[i]); - if (next == nullptr) { - return false; - } - table = next; - } - - if (table->erase(path.back()) == 0) { + if (!eraseOverridePath(m_overridesTable, path, overridePreserveDepthForPath(path))) { return false; } - pruneEmptyOverrideTables(m_overridesTable, path, preserveDepth); if (!writeOverridesToFile()) { kLog.warn("failed to write {}", m_overridesPath); diff --git a/src/config/config_service.cpp b/src/config/config_service.cpp index 46fc48a24..32e2216cb 100644 --- a/src/config/config_service.cpp +++ b/src/config/config_service.cpp @@ -770,6 +770,7 @@ void ConfigService::seedBuiltinWidgets(Config& config) { } void ConfigService::loadAll() { + m_effectiveOverrideCache.clear(); m_config = Config{}; seedBuiltinWidgets(m_config); @@ -866,7 +867,9 @@ void ConfigService::loadAll() { } } -void ConfigService::parseTable(const toml::table& tbl) { +void ConfigService::parseTable(const toml::table& tbl) { parseTableInto(tbl, m_config, true); } + +void ConfigService::parseTableInto(const toml::table& tbl, Config& config, bool logSummary) const { // Parse [bar.*] named subtables if (auto* barTblMap = tbl["bar"].as_table()) { std::vector parsedBars; @@ -1064,7 +1067,7 @@ void ConfigService::parseTable(const toml::table& tbl) { for (std::size_t i = 0; i < parsedBars.size(); ++i) { if (!used[i] && parsedBars[i].name == orderedName) { used[i] = true; - m_config.bars.push_back(std::move(parsedBars[i])); + config.bars.push_back(std::move(parsedBars[i])); break; } } @@ -1072,7 +1075,7 @@ void ConfigService::parseTable(const toml::table& tbl) { for (std::size_t i = 0; i < parsedBars.size(); ++i) { if (!used[i]) { - m_config.bars.push_back(std::move(parsedBars[i])); + config.bars.push_back(std::move(parsedBars[i])); } } } @@ -1090,10 +1093,10 @@ void ConfigService::parseTable(const toml::table& tbl) { if (auto v = (*entryTbl)["type"].value()) { wc.type = *v; - if (auto it = m_config.widgets.find(widgetName); it != m_config.widgets.end() && it->second.type == wc.type) { + if (auto it = config.widgets.find(widgetName); it != config.widgets.end() && it->second.type == wc.type) { wc.settings = it->second.settings; } - } else if (auto it = m_config.widgets.find(widgetName); it != m_config.widgets.end()) { + } else if (auto it = config.widgets.find(widgetName); it != config.widgets.end()) { wc = it->second; } else { wc.type = widgetName; @@ -1123,13 +1126,13 @@ void ConfigService::parseTable(const toml::table& tbl) { } } - m_config.widgets[widgetName] = std::move(wc); + config.widgets[widgetName] = std::move(wc); } } // Parse [shell] if (auto* shellTbl = tbl["shell"].as_table()) { - auto& shell = m_config.shell; + auto& shell = config.shell; if (auto v = (*shellTbl)["ui_scale"].value()) { shell.uiScale = std::clamp(static_cast(*v), 0.5f, 4.0f); } @@ -1214,7 +1217,7 @@ void ConfigService::parseTable(const toml::table& tbl) { // Parse [theme] if (auto* themeTbl = tbl["theme"].as_table()) { - auto& theme = m_config.theme; + auto& theme = config.theme; if (auto v = (*themeTbl)["source"].value()) { if (auto parsed = enumFromKey(kThemeSources, *v)) { theme.source = *parsed; @@ -1264,7 +1267,7 @@ void ConfigService::parseTable(const toml::table& tbl) { // Parse [wallpaper] if (auto* wpTbl = tbl["wallpaper"].as_table()) { - auto& wp = m_config.wallpaper; + auto& wp = config.wallpaper; if (auto v = (*wpTbl)["enabled"].value()) wp.enabled = *v; if (auto v = (*wpTbl)["fill_mode"].value()) { @@ -1354,7 +1357,7 @@ void ConfigService::parseTable(const toml::table& tbl) { // Parse [backdrop] if (auto* ovTbl = tbl["backdrop"].as_table()) { - auto& ov = m_config.backdrop; + auto& ov = config.backdrop; if (auto v = (*ovTbl)["enabled"].value()) ov.enabled = *v; if (auto v = (*ovTbl)["blur_intensity"].value()) @@ -1365,13 +1368,13 @@ void ConfigService::parseTable(const toml::table& tbl) { // Parse [osd] if (auto* osdTbl = tbl["osd"].as_table()) { - auto& osd = m_config.osd; + auto& osd = config.osd; if (auto v = (*osdTbl)["position"].value()) osd.position = *v; } - auto parseNotificationTable = [this](const toml::table& notifTable) { - auto& notif = m_config.notification; + auto parseNotificationTable = [&config](const toml::table& notifTable) { + auto& notif = config.notification; if (auto v = notifTable["enable_daemon"].value()) notif.enableDaemon = *v; if (auto v = notifTable["position"].value()) @@ -1395,7 +1398,7 @@ void ConfigService::parseTable(const toml::table& tbl) { // Parse [dock] if (auto* dockTbl = tbl["dock"].as_table()) { - auto& dock = m_config.dock; + auto& dock = config.dock; if (auto v = (*dockTbl)["enabled"].value()) dock.enabled = *v; if (auto v = (*dockTbl)["active_monitor_only"].value()) @@ -1440,7 +1443,7 @@ void ConfigService::parseTable(const toml::table& tbl) { // Parse [desktop_widgets] if (auto* desktopWidgetsTbl = tbl["desktop_widgets"].as_table()) { - auto& desktopWidgets = m_config.desktopWidgets; + auto& desktopWidgets = config.desktopWidgets; if (auto v = (*desktopWidgetsTbl)["enabled"].value()) { desktopWidgets.enabled = *v; } @@ -1448,7 +1451,7 @@ void ConfigService::parseTable(const toml::table& tbl) { // Parse [weather] if (auto* weatherTbl = tbl["weather"].as_table()) { - auto& weather = m_config.weather; + auto& weather = config.weather; if (auto v = (*weatherTbl)["enabled"].value()) weather.enabled = *v; if (auto v = (*weatherTbl)["auto_locate"].value()) @@ -1465,7 +1468,7 @@ void ConfigService::parseTable(const toml::table& tbl) { // Parse [system] if (auto* systemTbl = tbl["system"].as_table()) { - auto& system = m_config.system; + auto& system = config.system; if (const auto* monitorTbl = (*systemTbl)["monitor"].as_table()) { if (auto v = (*monitorTbl)["enabled"].value()) { system.monitor.enabled = *v; @@ -1475,7 +1478,7 @@ void ConfigService::parseTable(const toml::table& tbl) { // Parse [audio] if (auto* audioTbl = tbl["audio"].as_table()) { - auto& audio = m_config.audio; + auto& audio = config.audio; if (auto v = (*audioTbl)["enable_overdrive"].value()) { audio.enableOverdrive = *v; } @@ -1495,7 +1498,7 @@ void ConfigService::parseTable(const toml::table& tbl) { // Parse [brightness] if (auto* brightnessTbl = tbl["brightness"].as_table()) { - auto& brightness = m_config.brightness; + auto& brightness = config.brightness; if (auto v = (*brightnessTbl)["enable_ddcutil"].value()) { brightness.enableDdcutil = *v; } @@ -1535,7 +1538,7 @@ void ConfigService::parseTable(const toml::table& tbl) { // Parse [keybinds] if (auto* keybindsTbl = tbl["keybinds"].as_table()) { - auto& keybinds = m_config.keybinds; + auto& keybinds = config.keybinds; auto parseAction = [&](std::string_view key, std::vector& out) { out.clear(); @@ -1580,7 +1583,7 @@ void ConfigService::parseTable(const toml::table& tbl) { // Parse [nightlight] if (auto* nightlightTbl = tbl["nightlight"].as_table()) { - auto& nightlight = m_config.nightlight; + auto& nightlight = config.nightlight; if (auto v = (*nightlightTbl)["enabled"].value()) { nightlight.enabled = *v; } @@ -1624,7 +1627,7 @@ void ConfigService::parseTable(const toml::table& tbl) { // Parse [hooks] if (auto* hooksTbl = tbl["hooks"].as_table()) { - auto& hooks = m_config.hooks; + auto& hooks = config.hooks; for (const auto& [name, node] : *hooksTbl) { const std::string_view keyView{name.str()}; if (keyView == "battery_low_percent_threshold") { @@ -1643,7 +1646,7 @@ void ConfigService::parseTable(const toml::table& tbl) { // Parse [[control_center.shortcuts]] if (auto* ccTbl = tbl["control_center"].as_table()) { if (auto* shortcutsArr = (*ccTbl)["shortcuts"].as_array()) { - m_config.controlCenter.shortcuts.clear(); + config.controlCenter.shortcuts.clear(); for (const auto& entry : *shortcutsArr) { auto* entryTbl = entry.as_table(); if (entryTbl == nullptr) { @@ -1660,13 +1663,13 @@ void ConfigService::parseTable(const toml::table& tbl) { sc.icon = *v; } if (!sc.type.empty()) { - m_config.controlCenter.shortcuts.push_back(std::move(sc)); + config.controlCenter.shortcuts.push_back(std::move(sc)); } } } } - if (m_config.controlCenter.shortcuts.empty()) { - m_config.controlCenter.shortcuts = { + if (config.controlCenter.shortcuts.empty()) { + config.controlCenter.shortcuts = { {"wifi", {}, {}}, {"bluetooth", {}, {}}, {"caffeine", {}, {}}, {"nightlight", {}, {}}, {"notification", {}, {}}, {"power_profile", {}, {}}, }; @@ -1697,34 +1700,38 @@ void ConfigService::parseTable(const toml::table& tbl) { behavior.resumeCommand = *v; } - m_config.idle.behaviors.push_back(std::move(behavior)); + config.idle.behaviors.push_back(std::move(behavior)); } } } - if (m_config.bars.empty()) { - kLog.info("no [bar.*] defined, using defaults"); - m_config.bars.push_back(BarConfig{}); + if (config.bars.empty()) { + if (logSummary) { + kLog.info("no [bar.*] defined, using defaults"); + } + config.bars.push_back(BarConfig{}); } - std::string barOrder; - for (const auto& bar : m_config.bars) { - if (!barOrder.empty()) { - barOrder += ", "; + if (logSummary) { + std::string barOrder; + for (const auto& bar : config.bars) { + if (!barOrder.empty()) { + barOrder += ", "; + } + barOrder += bar.name; } - barOrder += bar.name; - } - kLog.info("{} bar(s) defined", m_config.bars.size()); - kLog.info("bar order: {}", barOrder); - kLog.info("idle behaviors={}", m_config.idle.behaviors.size()); - std::size_t hookKindsUsed = 0; - for (const auto& cmds : m_config.hooks.commands) { - if (!cmds.empty()) { - ++hookKindsUsed; + kLog.info("{} bar(s) defined", config.bars.size()); + kLog.info("bar order: {}", barOrder); + kLog.info("idle behaviors={}", config.idle.behaviors.size()); + std::size_t hookKindsUsed = 0; + for (const auto& cmds : config.hooks.commands) { + if (!cmds.empty()) { + ++hookKindsUsed; + } } + kLog.info("hooks kinds with commands={} battery_low_threshold={}%", hookKindsUsed, + config.hooks.batteryLowPercentThreshold); } - kLog.info("hooks kinds with commands={} battery_low_threshold={}%", hookKindsUsed, - m_config.hooks.batteryLowPercentThreshold); } bool ConfigService::matchesKeybind(KeybindAction action, std::uint32_t sym, std::uint32_t modifiers) const { diff --git a/src/config/config_service.h b/src/config/config_service.h index 73ef0119d..8d101cba1 100644 --- a/src/config/config_service.h +++ b/src/config/config_service.h @@ -66,6 +66,7 @@ public: void setDockEnabled(bool enabled); bool markSetupWizardCompleted(); [[nodiscard]] bool hasOverride(const std::vector& path) const; + [[nodiscard]] bool hasEffectiveOverride(const std::vector& path) const; [[nodiscard]] bool isOverrideOnlyBar(std::string_view name) const; [[nodiscard]] bool canMoveBarOverride(std::string_view name, int direction) const; [[nodiscard]] bool canDeleteBarOverride(std::string_view name) const; @@ -88,6 +89,11 @@ private: static void deepMerge(toml::table& base, const toml::table& overlay); void loadAll(); void parseTable(const toml::table& tbl); + void parseTableInto(const toml::table& tbl, Config& config, bool logSummary) const; + [[nodiscard]] std::optional configForOverrides(const toml::table& overrides) const; + [[nodiscard]] bool overridePathEffectiveInTable(const std::vector& path, const toml::table& overrides, + const Config* parsedWith = nullptr) const; + [[nodiscard]] std::size_t overridePreserveDepthForPath(const std::vector& path) const; void setupWatch(); void fireReloadCallbacks(); void loadOverridesFromFile(); @@ -108,6 +114,7 @@ private: std::string m_defaultWallpaperPath; std::unordered_map m_monitorWallpaperPaths; bool m_setupWizardCompleted = false; + mutable std::unordered_map m_effectiveOverrideCache; std::string m_pendingError; // parse error from initial load, sent as notification once manager is wired up uint32_t m_configErrorNotificationId = 0; // ID of the active config-error notification, 0 if none diff --git a/src/shell/settings/bar_widget_editor.cpp b/src/shell/settings/bar_widget_editor.cpp index c9fa180d6..0d1d45fb7 100644 --- a/src/shell/settings/bar_widget_editor.cpp +++ b/src/shell/settings/bar_widget_editor.cpp @@ -691,7 +691,7 @@ namespace settings { continue; } const auto path = widgetSettingPath(std::string(widgetName), key); - const bool overridden = ctx.configService != nullptr && ctx.configService->hasOverride(path); + const bool overridden = ctx.configService != nullptr && ctx.configService->hasEffectiveOverride(path); if (ctx.showOverriddenOnly && !overridden) { continue; } @@ -722,7 +722,7 @@ namespace settings { } const auto path = widgetSettingPath(std::string(widgetName), key); const std::string deleteKey = pathKey(path); - const bool overridden = ctx.configService != nullptr && ctx.configService->hasOverride(path); + const bool overridden = ctx.configService != nullptr && ctx.configService->hasEffectiveOverride(path); const bool pendingDelete = ctx.pendingDeleteWidgetSettingPath == deleteKey; auto row = std::make_unique(); @@ -781,7 +781,7 @@ namespace settings { } auto path = widgetSettingPath(std::string(widgetName), "type"); - const bool overridden = ctx.configService != nullptr && ctx.configService->hasOverride(path); + const bool overridden = ctx.configService != nullptr && ctx.configService->hasEffectiveOverride(path); if (ctx.showOverriddenOnly && !overridden) { return; } @@ -856,7 +856,7 @@ namespace settings { continue; } const auto path = widgetSettingPath(widgetName, spec.key); - const bool overridden = ctx.configService != nullptr && ctx.configService->hasOverride(path); + const bool overridden = ctx.configService != nullptr && ctx.configService->hasEffectiveOverride(path); if (ctx.showOverriddenOnly && !overridden) { continue; } @@ -1065,8 +1065,14 @@ namespace settings { currentLaneKey = std::string(laneKey); currentLanePath = std::move(p); currentLaneItems = std::move(items); - currentLaneInherited = isMonitorWidgetListPath(currentLanePath) && - !monitorWidgetListHasExplicitValue(ctx.config, currentLanePath); + const bool currentLaneOverridden = + ctx.configService != nullptr && ctx.configService->hasEffectiveOverride(currentLanePath); + const bool currentLaneRedundantGuiOverride = ctx.configService != nullptr && + ctx.configService->hasOverride(currentLanePath) && + !currentLaneOverridden; + currentLaneInherited = + isMonitorWidgetListPath(currentLanePath) && + (!monitorWidgetListHasExplicitValue(ctx.config, currentLanePath) || currentLaneRedundantGuiOverride); break; } } @@ -1580,9 +1586,11 @@ namespace settings { for (const auto laneKey : kLaneKeys) { auto lanePath = pathWithLastSegment(entry.path, std::string(laneKey)); const auto laneItems = barWidgetItemsForPath(ctx.config, lanePath); - const bool overridden = ctx.configService != nullptr && ctx.configService->hasOverride(lanePath); - const bool inherited = - isMonitorWidgetListPath(lanePath) && !monitorWidgetListHasExplicitValue(ctx.config, lanePath); + const bool overridden = ctx.configService != nullptr && ctx.configService->hasEffectiveOverride(lanePath); + const bool redundantGuiOverride = + ctx.configService != nullptr && ctx.configService->hasOverride(lanePath) && !overridden; + const bool inherited = isMonitorWidgetListPath(lanePath) && + (!monitorWidgetListHasExplicitValue(ctx.config, lanePath) || redundantGuiOverride); auto lane = std::make_unique(); lane->setDirection(FlexDirection::Vertical); diff --git a/src/shell/settings/settings_content.cpp b/src/shell/settings/settings_content.cpp index c3eb20187..17a4b9e99 100644 --- a/src/shell/settings/settings_content.cpp +++ b/src/shell/settings/settings_content.cpp @@ -426,9 +426,11 @@ namespace settings { }; const auto makeRow = [&](Flex& section, const SettingEntry& entry, std::unique_ptr control) { - const bool overridden = (ctx.configService != nullptr && ctx.configService->hasOverride(entry.path)); + const bool overridden = (ctx.configService != nullptr && ctx.configService->hasEffectiveOverride(entry.path)); + const bool redundantGuiOverride = + ctx.configService != nullptr && ctx.configService->hasOverride(entry.path) && !overridden; const bool monitorSetting = isMonitorOverrideSettingPath(entry.path); - const bool monitorExplicit = monitorOverrideHasExplicitValue(cfg, entry.path); + const bool monitorExplicit = monitorOverrideHasExplicitValue(cfg, entry.path) && !redundantGuiOverride; const bool monitorInherited = monitorSetting && !monitorExplicit; auto row = std::make_unique(); @@ -792,7 +794,7 @@ namespace settings { const auto makeSearchPickerBlock = [&](Flex& section, const SettingEntry& entry, const SearchPickerSetting& setting) { - const bool overridden = (ctx.configService != nullptr && ctx.configService->hasOverride(entry.path)); + const bool overridden = (ctx.configService != nullptr && ctx.configService->hasEffectiveOverride(entry.path)); const std::string pickerPath = pathKey(entry.path); auto block = std::make_unique(); @@ -912,7 +914,7 @@ namespace settings { }; const auto makeMultiSelectBlock = [&](Flex& section, const SettingEntry& entry, const MultiSelectSetting& setting) { - const bool overridden = (ctx.configService != nullptr && ctx.configService->hasOverride(entry.path)); + const bool overridden = (ctx.configService != nullptr && ctx.configService->hasEffectiveOverride(entry.path)); auto block = std::make_unique(); block->setDirection(FlexDirection::Vertical); @@ -1004,7 +1006,7 @@ namespace settings { }; const auto makeListBlock = [&](Flex& section, const SettingEntry& entry, const ListSetting& list) { - const bool overridden = (ctx.configService != nullptr && ctx.configService->hasOverride(entry.path)); + const bool overridden = (ctx.configService != nullptr && ctx.configService->hasEffectiveOverride(entry.path)); auto block = std::make_unique(); block->setDirection(FlexDirection::Vertical); @@ -1176,7 +1178,8 @@ namespace settings { if (!ctx.showAdvanced && entry.advanced) { continue; } - if (ctx.showOverriddenOnly && ctx.configService != nullptr && !ctx.configService->hasOverride(entry.path)) { + if (ctx.showOverriddenOnly && ctx.configService != nullptr && + !ctx.configService->hasEffectiveOverride(entry.path)) { continue; } if (!matchesNormalizedSettingQuery(entry, normalizedSearchQuery)) { diff --git a/src/shell/settings/settings_window.cpp b/src/shell/settings/settings_window.cpp index fa42c2486..b21651ce1 100644 --- a/src/shell/settings/settings_window.cpp +++ b/src/shell/settings/settings_window.cpp @@ -1014,7 +1014,7 @@ void SettingsWindow::buildScene(std::uint32_t width, std::uint32_t height) { if (m_config != nullptr) { for (const auto& entry : m_settingsRegistry) { if (settingEntryBelongsToPage(entry, m_selectedSection, m_selectedBarName, m_selectedMonitorOverride) && - m_config->hasOverride(entry.path) && !containsPath(resetPagePaths, entry.path)) { + m_config->hasEffectiveOverride(entry.path) && !containsPath(resetPagePaths, entry.path)) { resetPagePaths.push_back(entry.path); } }