config/settings: effective override, aka discard override when the value is identical

This commit is contained in:
Lemmy
2026-05-06 10:54:57 -04:00
parent 7556f23a58
commit c4b5c5d438
6 changed files with 555 additions and 79 deletions
+469 -18
View File
@@ -3,9 +3,12 @@
#include "util/file_utils.h"
#include <algorithm>
#include <cmath>
#include <exception>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <optional>
#include <type_traits>
#include <vector>
@@ -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<std::string>& 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<double>& a, const std::optional<double>& b) noexcept {
if (a.has_value() != b.has_value()) {
return false;
}
return !a.has_value() || nearlyEqual(*a, *b);
}
bool optionalColorSpecEqual(const std::optional<ColorSpec>& a, const std::optional<ColorSpec>& b) noexcept {
if (a.has_value() != b.has_value()) {
return false;
}
return !a.has_value() || colorSpecEqual(*a, *b);
}
template <typename T, typename Equal>
bool vectorEqual(const std::vector<T>& a, const std::vector<T>& 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<double> numericWidgetSetting(const WidgetSettingValue& value) {
if (const auto* i = std::get_if<std::int64_t>(&value)) {
return static_cast<double>(*i);
}
if (const auto* d = std::get_if<double>(&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<decltype(av)>;
const auto* bv = std::get_if<T>(&b);
return bv != nullptr && av == *bv;
},
a);
}
bool widgetSettingsEqual(const std::unordered_map<std::string, WidgetSettingValue>& a,
const std::unordered_map<std::string, WidgetSettingValue>& 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<float>(*ovr.widgetCapsulePadding), 0.0f, 48.0f);
}
if (ovr.widgetCapsuleOpacity) {
resolved.widgetCapsuleOpacity = std::clamp(static_cast<float>(*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<std::string, WidgetConfig>& a,
const std::unordered_map<std::string, WidgetConfig>& 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<toml::table>(key)) {
@@ -99,6 +436,46 @@ namespace {
parent->erase(changedPath[depth - 1]);
}
}
bool eraseOverridePath(toml::table& root, const std::vector<std::string>& 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<toml::table>(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<std::filesystem::path> sortedConfigTomlFiles(std::string_view configDir) {
std::vector<std::filesystem::path> 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<std::string>& path) const {
return findOverrideNode(m_overridesTable, path) != nullptr;
}
bool ConfigService::hasEffectiveOverride(const std::vector<std::string>& 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<std::string>& 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<Config> 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<std::string>& path, const toml::table& overrides,
const Config* parsedWith) const {
if (path.empty() || findOverrideNode(overrides, path) == nullptr) {
return false;
}
std::optional<Config> 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<std::string>& 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<std::string>& 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<toml::table>(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);
+52 -45
View File
@@ -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<BarConfig> 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<std::string>()) {
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<double>()) {
shell.uiScale = std::clamp(static_cast<float>(*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<std::string>()) {
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<bool>())
wp.enabled = *v;
if (auto v = (*wpTbl)["fill_mode"].value<std::string>()) {
@@ -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<bool>())
ov.enabled = *v;
if (auto v = (*ovTbl)["blur_intensity"].value<double>())
@@ -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<std::string>())
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<bool>())
notif.enableDaemon = *v;
if (auto v = notifTable["position"].value<std::string>())
@@ -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<bool>())
dock.enabled = *v;
if (auto v = (*dockTbl)["active_monitor_only"].value<bool>())
@@ -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<bool>()) {
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<bool>())
weather.enabled = *v;
if (auto v = (*weatherTbl)["auto_locate"].value<bool>())
@@ -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<bool>()) {
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<bool>()) {
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<bool>()) {
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<KeyChord>& 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<bool>()) {
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 {
+7
View File
@@ -66,6 +66,7 @@ public:
void setDockEnabled(bool enabled);
bool markSetupWizardCompleted();
[[nodiscard]] bool hasOverride(const std::vector<std::string>& path) const;
[[nodiscard]] bool hasEffectiveOverride(const std::vector<std::string>& 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<Config> configForOverrides(const toml::table& overrides) const;
[[nodiscard]] bool overridePathEffectiveInTable(const std::vector<std::string>& path, const toml::table& overrides,
const Config* parsedWith = nullptr) const;
[[nodiscard]] std::size_t overridePreserveDepthForPath(const std::vector<std::string>& path) const;
void setupWatch();
void fireReloadCallbacks();
void loadOverridesFromFile();
@@ -108,6 +114,7 @@ private:
std::string m_defaultWallpaperPath;
std::unordered_map<std::string, std::string> m_monitorWallpaperPaths;
bool m_setupWizardCompleted = false;
mutable std::unordered_map<std::string, bool> 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
+17 -9
View File
@@ -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<Flex>();
@@ -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<Flex>();
lane->setDirection(FlexDirection::Vertical);
+9 -6
View File
@@ -426,9 +426,11 @@ namespace settings {
};
const auto makeRow = [&](Flex& section, const SettingEntry& entry, std::unique_ptr<Node> 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<Flex>();
@@ -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<Flex>();
@@ -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<Flex>();
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<Flex>();
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)) {
+1 -1
View File
@@ -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);
}
}