diff --git a/assets/translations/en.json b/assets/translations/en.json index 19ee1323f2..e1c370a1b8 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -3641,6 +3641,10 @@ "description": "Show placeholder text when no window is focused instead of hiding the widget", "label": "Show Empty Label" }, + "show-glyph": { + "description": "Show the widget glyph or custom image", + "label": "Show Glyph" + }, "show-icon": { "description": "Show this widget's icon", "label": "Show Icon" @@ -3665,6 +3669,10 @@ "description": "Show the status card above the login box for unlock hints and auth prompts. Errors and Caps Lock warnings still appear when this is off.", "label": "Unlock Hint" }, + "show-value": { + "description": "Show the current value next to the visualization", + "label": "Show Value" + }, "show-vpn-label": { "description": "Replaces network label with VPN label in replace mode; adds VPN label next to VPN icon in both mode", "label": "Show VPN Label" @@ -3722,6 +3730,10 @@ "description": "Color role used for urgent workspaces; fixed hex colors are also supported", "label": "Urgent Color" }, + "visualization": { + "description": "Gauge, graph, or no data visualization", + "label": "Visualization" + }, "vertical-format": { "description": "Optional format used on vertical bars", "label": "Vertical Format" diff --git a/src/config/config_migrations.cpp b/src/config/config_migrations.cpp index 9173878279..efb1f86689 100644 --- a/src/config/config_migrations.cpp +++ b/src/config/config_migrations.cpp @@ -21,6 +21,7 @@ namespace noctalia::config { constexpr int kCustomButtonCommandsMigrationVersion = 6; constexpr int kDeadZoneActionsMigrationVersion = 7; constexpr int kLockscreenLoginBoxDeprecatedSettingsMigrationVersion = 8; + constexpr int kSysmonPresentationMigrationVersion = 9; constexpr std::int64_t kMaxBarRadius = 500; constexpr std::array kBarRadiusKeys = { "radius", "radius_top_left", "radius_top_right", "radius_bottom_left", "radius_bottom_right", @@ -416,6 +417,66 @@ namespace noctalia::config { }); } + template void migrateSysmonPresentationSettings(toml::table& root, OnChanged&& onChanged) { + auto* widgets = root["widget"].as_table(); + if (widgets == nullptr) { + return; + } + + for (auto& [widgetName, widgetNode] : *widgets) { + auto* widget = widgetNode.as_table(); + if (widget == nullptr || (*widget)["type"].value_or(std::string(widgetName.str())) != "sysmon") { + continue; + } + + bool changed = false; + if (const auto showIcon = (*widget)["show_icon"].value(); showIcon.has_value()) { + if (!widget->contains("show_glyph")) { + widget->insert_or_assign("show_glyph", *showIcon); + } + widget->erase("show_icon"); + changed = true; + } + + const auto display = (*widget)["display"].value(); + if (display.has_value()) { + if (!widget->contains("visualization")) { + const std::string visualization = *display == "text" ? "none" : *display; + widget->insert_or_assign("visualization", visualization); + } + widget->erase("display"); + changed = true; + } + + if (const auto showLabel = (*widget)["show_label"].value(); showLabel.has_value()) { + if (!widget->contains("show_value")) { + bool showValue = *showLabel; + if (display == "text") { + showValue = true; + } else if (display == "none") { + showValue = false; + } + widget->insert_or_assign("show_value", showValue); + } + widget->erase("show_label"); + changed = true; + } else if (display.has_value() && !widget->contains("show_value")) { + widget->insert_or_assign("show_value", *display != "none"); + changed = true; + } + + if (changed) { + onChanged("widget." + std::string(widgetName.str())); + } + } + } + + void migrateSysmonPresentationSettingsSidecar(toml::table& root, schema::Diagnostics& diag) { + migrateSysmonPresentationSettings(root, [&diag](const std::string& path) { + diag.warn(path, "migrated sysmon presentation settings to their canonical names"); + }); + } + void migrateCustomButtonCommandsSidecar(toml::table& root, schema::Diagnostics& diag) { migrateCustomButtonCommands(root, [&diag](const std::string& path, std::string_view message) { diag.warn(path, std::string(message)); @@ -517,6 +578,11 @@ namespace noctalia::config { .summary = "lockscreen: drop removed login box show_password_hint setting", .apply = migrateLockscreenLoginBoxDeprecatedSettingsSidecar, }, + { + .toVersion = kSysmonPresentationMigrationVersion, + .summary = "widget: migrate sysmon presentation settings", + .apply = migrateSysmonPresentationSettingsSidecar, + }, }; return migrations; } @@ -625,6 +691,13 @@ namespace noctalia::config { .message = "removed deprecated show_password_hint", }); }); + migrateSysmonPresentationSettings(root, [&issues](const std::string& path) { + issues.push_back({ + .migrationVersion = kSysmonPresentationMigrationVersion, + .path = path, + .message = "sysmon display settings are now visualization, show_value, and show_glyph", + }); + }); } std::string legacyConfigIssueFingerprint(const LegacyConfigIssues& issues) { diff --git a/src/config/config_service.cpp b/src/config/config_service.cpp index c2851467c1..98fce20fcc 100644 --- a/src/config/config_service.cpp +++ b/src/config/config_service.cpp @@ -182,10 +182,26 @@ namespace { } } + void validateSysmonWidgetSettings(std::string_view widgetName, const WidgetConfig& widget) { + if (widget.type != "sysmon") { + return; + } + + const bool showGlyph = widget.getBool("show_glyph", true); + const bool showValue = widget.getBool("show_value", true); + const std::string visualization = widget.getString("visualization", "gauge"); + if (!showGlyph && !showValue && visualization == "none") { + throw std::runtime_error( + "widget." + std::string(widgetName) + ": show_glyph, show_value, and visualization cannot all be disabled" + ); + } + } + void validateWidgetSettings(std::string_view widgetName, const WidgetConfig& widget) { validateWidgetColorSettings(widgetName, widget); validateWidgetScaleSetting(widgetName, widget); validateKeyboardLayoutWidgetSettings(widgetName, widget); + validateSysmonWidgetSettings(widgetName, widget); } std::optional componentOwnerId(std::string_view ownerPath, std::string_view prefix) { diff --git a/src/shell/bar/widgets/sysmon_widget.cpp b/src/shell/bar/widgets/sysmon_widget.cpp index 514027fdac..2ec3d4b643 100644 --- a/src/shell/bar/widgets/sysmon_widget.cpp +++ b/src/shell/bar/widgets/sysmon_widget.cpp @@ -176,10 +176,9 @@ namespace { } // namespace SysmonWidget::SysmonWidget(SystemMonitorService* monitor, ConfigService& configService, Options options) - : m_monitor(monitor), m_stat(options.stat), m_displayMode(options.displayMode), - m_highlightColor(options.highlightColor), m_configService(configService), - m_showLabel(options.showLabel && options.displayMode != SysmonDisplayMode::None), - m_labelMinWidth(static_cast(options.labelMinWidth)), + : m_monitor(monitor), m_stat(options.stat), m_visualization(options.visualization), + m_highlightColor(options.highlightColor), m_configService(configService), m_showGlyph(options.showGlyph), + m_showValue(options.showValue), m_labelMinWidth(static_cast(options.labelMinWidth)), m_diskPath(FileUtils::expandUserPath(options.diskPath).string()), m_networkInterface(std::move(options.networkInterface)), m_networkSpeedUnit(options.networkSpeedUnit), m_networkSpeedLabelStyle( @@ -230,19 +229,21 @@ SysmonWidget::~SysmonWidget() { void SysmonWidget::create() { auto container = ui::inputArea({}); std::unique_ptr glyphNode; - if (m_customImage.enabled()) { - glyphNode = ui::image({.out = &m_image, .fit = ImageFit::Contain}); - } else { - glyphNode = ui::glyph({ - .out = &m_glyph, - .glyph = m_glyphOverride.empty() ? glyphName(m_stat) : m_glyphOverride, - .glyphSize = Style::baseGlyphSize * m_contentScale, - .color = widgetIconColorOr(colorSpecFromRole(ColorRole::OnSurface)), - }); + if (m_showGlyph) { + if (m_customImage.enabled()) { + glyphNode = ui::image({.out = &m_image, .fit = ImageFit::Contain}); + } else { + glyphNode = ui::glyph({ + .out = &m_glyph, + .glyph = m_glyphOverride.empty() ? glyphName(m_stat) : m_glyphOverride, + .glyphSize = Style::baseGlyphSize * m_contentScale, + .color = widgetIconColorOr(colorSpecFromRole(ColorRole::OnSurface)), + }); + } } std::unique_ptr graphOrGaugeNode; - if (m_displayMode == SysmonDisplayMode::Graph) { + if (m_visualization == SysmonVisualization::Graph) { graphOrGaugeNode = ui::box(); m_chartBg = static_cast(graphOrGaugeNode.get()); @@ -252,7 +253,7 @@ void SysmonWidget::create() { m_graph = static_cast(m_chartBg->addChild(std::move(graph))); } - if (m_displayMode == SysmonDisplayMode::Gauge) { + if (m_visualization == SysmonVisualization::Gauge) { const ColorSpec base = widgetForegroundOr(colorSpecFromRole(ColorRole::OnSurface)); graphOrGaugeNode = ui::progressBar({ .fill = base, @@ -263,7 +264,7 @@ void SysmonWidget::create() { } std::unique_ptr textNode; - if (m_displayMode == SysmonDisplayMode::Text || m_showLabel) { + if (m_showValue) { textNode = ui::label({ .out = &m_label, .fontSize = Style::fontSizeBody * m_contentScale, @@ -276,7 +277,9 @@ void SysmonWidget::create() { m_containerRow = static_cast(container->addChild(ui::row({.gap = Style::spaceXs * m_contentScale}))); if (m_glyphPosition == SysmonGlyphPosition::Before) { - m_containerRow->addChild(std::move(glyphNode)); + if (glyphNode) { + m_containerRow->addChild(std::move(glyphNode)); + } if (graphOrGaugeNode != nullptr) { m_containerRow->addChild(std::move(graphOrGaugeNode)); } @@ -290,7 +293,9 @@ void SysmonWidget::create() { if (graphOrGaugeNode != nullptr) { m_containerRow->addChild(std::move(graphOrGaugeNode)); } - m_containerRow->addChild(std::move(glyphNode)); + if (glyphNode) { + m_containerRow->addChild(std::move(glyphNode)); + } } setRoot(std::move(container)); @@ -497,7 +502,7 @@ void SysmonWidget::syncGaugeProgress(double normalized) { void SysmonWidget::doLayout(Renderer& renderer, float containerWidth, float containerHeight) { auto* rootNode = root(); - if ((m_glyph == nullptr && m_image == nullptr) || rootNode == nullptr) { + if ((m_showGlyph && m_glyph == nullptr && m_image == nullptr) || rootNode == nullptr) { return; } const bool isVerticalBar = containerHeight > containerWidth; @@ -508,9 +513,13 @@ void SysmonWidget::doLayout(Renderer& renderer, float containerWidth, float cont syncVisualPalette(); syncIcon(renderer); + const float iconW = iconWidth(); const float iconH = iconHeight(); const float gap = Style::spaceXs * m_contentScale; + const float iconWPlusGap = m_showGlyph ? iconW + gap : 0.0f; + const float iconHPlusGap = m_showGlyph ? iconH + gap : 0.0f; + const float baseSize = Style::fontSizeBody * m_contentScale; const bool verticalBar = m_isVerticalBar; if (m_label != nullptr) { @@ -523,9 +532,8 @@ void SysmonWidget::doLayout(Renderer& renderer, float containerWidth, float cont const float labelW = m_label != nullptr ? m_label->width() : 0.0f; const float labelH = m_label != nullptr ? m_label->height() : 0.0f; - if (m_displayMode == SysmonDisplayMode::Gauge && m_gauge != nullptr) { - const float baseSize = Style::fontSizeBody * m_contentScale; - const float gaugeStem = std::round(baseSize * 0.85f); + if (m_visualization == SysmonVisualization::Gauge && m_gauge != nullptr) { + const float gaugeStem = m_showGlyph ? std::round(baseSize * 0.85f) : std::round(baseSize * 1.2f); const float gaugeThickness = std::max(3.0f, roundf(baseSize * 0.3f)); if (verticalBar) { @@ -537,9 +545,9 @@ void SysmonWidget::doLayout(Renderer& renderer, float containerWidth, float cont if (m_label != nullptr) contentW = std::max(contentW, labelW); setIconPosition((contentW - iconW) * 0.5f, 0.0f); - m_gauge->setPosition(std::round((contentW - trackW) * 0.5f), iconH + gap); + m_gauge->setPosition(std::round((contentW - trackW) * 0.5f), iconHPlusGap); m_gauge->setSize(trackW, trackH); - float totalH = iconH + gap + trackH; + float totalH = iconHPlusGap + trackH; if (m_label != nullptr) { m_label->setPosition((contentW - labelW) * 0.5f, totalH + gap); totalH += gap + labelH; @@ -555,7 +563,7 @@ void SysmonWidget::doLayout(Renderer& renderer, float containerWidth, float cont contentH = std::max(contentH, labelH); const float gaugeY = std::round((contentH - gaugeH) * 0.5f); setIconPosition(0.0f, (contentH - iconH) * 0.5f); - m_gauge->setPosition(iconW + gap, gaugeY); + m_gauge->setPosition(iconWPlusGap, gaugeY); m_gauge->setSize(gaugeW, gaugeH); float totalW = m_gauge->x() + gaugeW; if (m_label != nullptr) { @@ -569,41 +577,42 @@ void SysmonWidget::doLayout(Renderer& renderer, float containerWidth, float cont return; } - if (m_displayMode == SysmonDisplayMode::Graph && m_chartBg != nullptr) { + if (m_visualization == SysmonVisualization::Graph && m_chartBg != nullptr) { const float chartW = verticalBar ? std::min(50.0f * m_contentScale, std::max(1.0f, containerWidth)) : 50.0f * m_contentScale; + const float chartH = m_showGlyph ? iconH : std::round(baseSize * 1.2f); if (verticalBar) { float contentW = std::max(iconW, chartW); if (m_label != nullptr) contentW = std::max(contentW, labelW); setIconPosition((contentW - iconW) * 0.5f, 0.0f); - const float chartY = iconH + gap; + const float chartY = iconHPlusGap; m_chartBg->setPosition(std::round((contentW - chartW) * 0.5f), chartY); - m_chartBg->setSize(chartW, iconH); + m_chartBg->setSize(chartW, chartH); if (m_graph != nullptr) { m_graph->setPosition(0.0f, 0.0f); - m_graph->setSize(chartW, iconH); + m_graph->setSize(chartW, chartH); } - float totalH = chartY + iconH; + float totalH = chartY + chartH; if (m_label != nullptr) { m_label->setPosition((contentW - labelW) * 0.5f, totalH + gap); totalH += gap + labelH; } rootNode->setSize(contentW, totalH); } else { - float contentH = iconH; + float contentH = chartH; if (m_label != nullptr) contentH = std::max(contentH, labelH); setIconPosition(0.0f, (contentH - iconH) * 0.5f); - m_chartBg->setPosition(iconW + gap, std::round((contentH - iconH) * 0.5f)); - m_chartBg->setSize(chartW, iconH); + m_chartBg->setPosition(iconWPlusGap, std::round((contentH - chartH) * 0.5f)); + m_chartBg->setSize(chartW, chartH); if (m_graph != nullptr) { m_graph->setPosition(0.0f, 0.0f); - m_graph->setSize(chartW, iconH); + m_graph->setSize(chartW, chartH); } float totalW = m_chartBg->x() + chartW; @@ -616,12 +625,12 @@ void SysmonWidget::doLayout(Renderer& renderer, float containerWidth, float cont } else if (m_label != nullptr && verticalBar) { const float contentW = std::max(iconW, labelW); setIconPosition((contentW - iconW) * 0.5f, 0.0f); - m_label->setPosition((contentW - labelW) * 0.5f, iconH + gap); - rootNode->setSize(contentW, iconH + gap + labelH); + m_label->setPosition((contentW - labelW) * 0.5f, iconHPlusGap); + rootNode->setSize(contentW, iconHPlusGap + labelH); } else if (m_label != nullptr) { const float contentH = std::max(iconH, labelH); setIconPosition(0.0f, (contentH - iconH) * 0.5f); - m_label->setPosition(iconW + gap, (contentH - labelH) * 0.5f); + m_label->setPosition(iconWPlusGap, (contentH - labelH) * 0.5f); rootNode->setSize(m_label->x() + labelW, contentH); } else { setIconPosition(0.0f, 0.0f); @@ -630,7 +639,7 @@ void SysmonWidget::doLayout(Renderer& renderer, float containerWidth, float cont } void SysmonWidget::doUpdate(Renderer& renderer) { - if (m_glyph == nullptr && m_image == nullptr) { + if (m_showGlyph && m_glyph == nullptr && m_image == nullptr) { return; } @@ -647,12 +656,12 @@ void SysmonWidget::doUpdate(Renderer& renderer) { static_cast(rootNode)->setTooltip(buildTooltipRows(value)); } - if (m_displayMode == SysmonDisplayMode::Gauge) { + if (m_visualization == SysmonVisualization::Gauge) { syncGaugeProgress(currentNormalized()); return; } - if (m_displayMode == SysmonDisplayMode::Graph) { + if (m_visualization == SysmonVisualization::Graph) { if (m_monitor != nullptr && m_monitor->isRunning()) { updateGraph(renderer); scheduleNextUpdate(m_monitor->latest().sampledAt); @@ -681,7 +690,7 @@ void SysmonWidget::onFrameTick(float deltaMs) { } bool SysmonWidget::needsFrameTick() const { - return m_displayMode == SysmonDisplayMode::Graph && m_scrollProgress < 1.0f; + return m_visualization == SysmonVisualization::Graph && m_scrollProgress < 1.0f; } void SysmonWidget::scheduleNextUpdate(std::chrono::steady_clock::time_point latestSampleAt) { diff --git a/src/shell/bar/widgets/sysmon_widget.h b/src/shell/bar/widgets/sysmon_widget.h index a1ffcb48bb..63ce413573 100644 --- a/src/shell/bar/widgets/sysmon_widget.h +++ b/src/shell/bar/widgets/sysmon_widget.h @@ -43,7 +43,7 @@ enum class SysmonStat { NetRx, NetTx }; -enum class SysmonDisplayMode { Text, Graph, Gauge, None }; +enum class SysmonVisualization { Graph, Gauge, None }; enum class SysmonGlyphPosition { Before, After }; class SysmonWidget : public Widget { @@ -57,9 +57,10 @@ class SysmonWidget : public Widget { std::string networkInterface; FormatUnits::DecimalByteRateUnit networkSpeedUnit = FormatUnits::DecimalByteRateUnit::Auto; bool networkSpeedCompact = false; - SysmonDisplayMode displayMode = SysmonDisplayMode::Gauge; + SysmonVisualization visualization = SysmonVisualization::Gauge; ColorSpec highlightColor = colorSpecFromRole(ColorRole::Error); - bool showLabel = true; + bool showGlyph = true; + bool showValue = true; int labelMinWidth = 0; bool showUnits = true; SysmonGlyphPosition glyphPosition = SysmonGlyphPosition::Before; @@ -102,10 +103,11 @@ class SysmonWidget : public Widget { SystemMonitorService* m_monitor; SysmonStat m_stat; - SysmonDisplayMode m_displayMode; + SysmonVisualization m_visualization; ColorSpec m_highlightColor = colorSpecFromRole(ColorRole::Error); ConfigService& m_configService; - bool m_showLabel; + bool m_showGlyph; + bool m_showValue; float m_labelMinWidth = 0.0f; std::string m_diskPath; std::string m_networkInterface; diff --git a/src/shell/bar/widgets/sysmon_widget_definition.cpp b/src/shell/bar/widgets/sysmon_widget_definition.cpp index 4a6554f32f..75bf868ec6 100644 --- a/src/shell/bar/widgets/sysmon_widget_definition.cpp +++ b/src/shell/bar/widgets/sysmon_widget_definition.cpp @@ -1,18 +1,5 @@ #include "shell/bar/widgets/sysmon_widget_definition.h" -namespace { - - settings::WidgetSettingVisibility labelMinWidthVisibility() { - settings::WidgetSettingVisibility visibility; - visibility.all = { - {"display", {"gauge", "graph", "text"}}, - {"show_label", {"true"}}, - }; - return visibility; - } - -} // namespace - const noctalia::bar::WidgetDefinition& sysmonWidgetDefinition() { using noctalia::bar::field; using Options = SysmonWidget::Options; @@ -21,8 +8,17 @@ const noctalia::bar::WidgetDefinition definition{ .type = "sysmon", @@ -103,20 +99,6 @@ const noctalia::bar::WidgetDefinition({ - .key = "glyph", - .control = settings::WidgetControlKind::Glyph, - .presentation = - settings::WidgetSettingPresentation{ - .descriptionKey = "settings.widgets.settings.glyph.sysmon-description", - }, - }), - field<&Options::customImage>({ - .key = "custom_image", - }), - field<&Options::customImageColorize>({ - .key = "custom_image_colorize", - }), field<&Options::diskPath>({ .key = "path", .presentation = @@ -163,44 +145,45 @@ const noctalia::bar::WidgetDefinition({ - .key = "display", + field<&Options::visualization>({ + .key = "visualization", .choices = { { - .value = SysmonDisplayMode::Gauge, + .value = SysmonVisualization::Gauge, .configValue = "gauge", .labelKey = "settings.widgets.options.gauge", }, { - .value = SysmonDisplayMode::Graph, + .value = SysmonVisualization::Graph, .configValue = "graph", .labelKey = "settings.widgets.options.graph", }, { - .value = SysmonDisplayMode::Text, - .configValue = "text", - .labelKey = "settings.widgets.options.text", - }, - { - .value = SysmonDisplayMode::None, + .value = SysmonVisualization::None, .configValue = "none", .labelKey = "settings.widgets.options.none", }, }, .presentation = settings::WidgetSettingPresentation{ + .group = "presentation", .segmented = true, }, }), - field<&Options::highlightColor>({ - .key = "highlight_color", + field<&Options::showValue>({ + .key = "show_value", + .presentation = + settings::WidgetSettingPresentation{ + .group = "presentation", + }, }), - field<&Options::showLabel>({ - .key = "show_label", + field<&Options::showUnits>({ + .key = "label_show_units", .presentation = settings::WidgetSettingPresentation{ - .visibleWhen = hasDisplay, + .group = "presentation", + .visibleWhen = showValue, }, }), field<&Options::labelMinWidth>({ @@ -210,14 +193,41 @@ const noctalia::bar::WidgetDefinition({ - .key = "label_show_units", + field<&Options::showGlyph>({ + .key = "show_glyph", + .presentation = + settings::WidgetSettingPresentation{ + .group = "presentation", + }, + }), + field<&Options::glyph>({ + .key = "glyph", + .control = settings::WidgetControlKind::Glyph, + .presentation = + settings::WidgetSettingPresentation{ + .descriptionKey = "settings.widgets.settings.glyph.sysmon-description", + .group = "presentation", + .visibleWhen = showGlyph, + }, + }), + field<&Options::customImage>({ + .key = "custom_image", + .presentation = + settings::WidgetSettingPresentation{ + .group = "presentation", + .visibleWhen = showGlyph, + }, + }), + field<&Options::customImageColorize>({ + .key = "custom_image_colorize", .presentation = settings::WidgetSettingPresentation{ - .visibleWhen = showLabel, + .group = "presentation", + .visibleWhen = showGlyph, }, }), field<&Options::glyphPosition>({ @@ -237,14 +247,23 @@ const noctalia::bar::WidgetDefinition({ + .key = "highlight_color", + .presentation = + settings::WidgetSettingPresentation{ + .group = "presentation", + .visibleWhen = hasVisualization, }, }), }, .finalize = [](Options& options, const SysmonWidgetDefinitionContext& context) { - if (context.verticalBar && options.displayMode == SysmonDisplayMode::Graph) { - options.displayMode = SysmonDisplayMode::Gauge; + if (context.verticalBar && options.visualization == SysmonVisualization::Graph) { + options.visualization = SysmonVisualization::Gauge; } }, }; diff --git a/tests/config_migration_test.cpp b/tests/config_migration_test.cpp index 37534ce0b5..58648fc515 100644 --- a/tests/config_migration_test.cpp +++ b/tests/config_migration_test.cpp @@ -454,6 +454,100 @@ middle_command = "" expect(secondPassIssues.empty(), "dead zone normalization was not idempotent"); } + void checkSysmonPresentationMigration() { + toml::table config = toml::parse(R"( +[widget.gauge] +type = "sysmon" +display = "gauge" +show_label = false +show_icon = false +label_show_units = false +label_min_width = 42 + +[widget.text] +type = "sysmon" +display = "text" +show_label = false + +[widget.none] +type = "sysmon" +display = "none" +show_label = true + +[widget.sysmon] +display = "graph" + +[widget.canonical] +type = "sysmon" +display = "text" +visualization = "graph" +show_label = true +show_value = false +show_icon = false +show_glyph = true + +[widget.clock] +display = "text" +show_label = false +)"); + noctalia::config::LegacyConfigIssues issues; + noctalia::config::normalizeLegacyConfig(config, issues); + + expect( + config["widget"]["gauge"]["visualization"].value() == std::optional{"gauge"}, + "gauge display was not migrated" + ); + expect( + config["widget"]["gauge"]["show_value"].value() == std::optional{false}, + "gauge show_label was not migrated" + ); + expect( + config["widget"]["gauge"]["show_glyph"].value() == std::optional{false}, + "show_icon was not migrated" + ); + expect( + config["widget"]["gauge"]["label_show_units"].value() == std::optional{false} + && config["widget"]["gauge"]["label_min_width"].value() == std::optional{42}, + "unchanged label detail settings were not preserved" + ); + expect( + config["widget"]["text"]["visualization"].value() == std::optional{"none"} + && config["widget"]["text"]["show_value"].value() == std::optional{true}, + "text display did not preserve its always-visible value" + ); + expect( + config["widget"]["none"]["visualization"].value() == std::optional{"none"} + && config["widget"]["none"]["show_value"].value() == std::optional{false}, + "none display did not preserve its hidden value" + ); + expect( + config["widget"]["sysmon"]["visualization"].value() == std::optional{"graph"} + && config["widget"]["sysmon"]["show_value"].value() == std::optional{true}, + "implicit sysmon defaults were not preserved" + ); + expect( + config["widget"]["canonical"]["visualization"].value() == std::optional{"graph"} + && config["widget"]["canonical"]["show_value"].value() == std::optional{false} + && config["widget"]["canonical"]["show_glyph"].value() == std::optional{true}, + "legacy sysmon settings overwrote canonical settings" + ); + expect( + !config["widget"]["canonical"].as_table()->contains("display") + && !config["widget"]["canonical"].as_table()->contains("show_label") + && !config["widget"]["canonical"].as_table()->contains("show_icon"), + "legacy sysmon keys were retained" + ); + expect( + config["widget"]["clock"]["display"].value() == std::optional{"text"}, + "another widget type was migrated as sysmon" + ); + expect(issues.size() == 5, "expected one migration issue per sysmon widget"); + + noctalia::config::LegacyConfigIssues secondPassIssues; + noctalia::config::normalizeLegacyConfig(config, secondPassIssues); + expect(secondPassIssues.empty(), "sysmon presentation normalization was not idempotent"); + } + void checkVersionGating() { toml::table legacy = toml::parse(R"( [bar.main] @@ -599,6 +693,7 @@ int main() { checkRemainingWidgetGesturesMigration(); checkCustomButtonCommandsMigration(); checkDeadZoneActionsMigration(); + checkSysmonPresentationMigration(); checkVersionGating(); checkReminderFingerprint(); checkRegistryOrdering();