77#include " config/config_migrations.h"
88#include " config/config_validate.h"
99#include " config/schema/config_schema.h"
10+ #include " config/schema/config_sections.h"
1011#include " config/schema/engine.h"
1112#include " config/widget_config.h"
1213#include " core/build_info.h"
@@ -667,6 +668,7 @@ void ConfigService::fireReloadCallbacks() {
667668 add (m_lastChange.theme , " theme" );
668669 add (m_lastChange.controlCenter , " controlCenter" );
669670 add (m_lastChange.plugins , " plugins" );
671+ add (m_lastChange.accessibility , " accessibility" );
670672 kLog .info (" reload: changed sections = [{}]" , changed.empty () ? " none" : changed);
671673 }
672674
@@ -1645,53 +1647,44 @@ void ConfigService::parseConfigTable(
16451647 }
16461648 }
16471649
1648- // Parse [shell]
1649- bool sessionActionsConfigured = false ;
1650- if (auto * shellTbl = tbl[" shell" ].as_table ()) {
1651- // Schema reads can't tell whether an empty actions list was explicit.
1652- sessionActionsConfigured = [&] {
1653- const auto * sessionTbl = (*shellTbl)[" session" ].as_table ();
1654- return sessionTbl != nullptr && (*sessionTbl)[" actions" ].as_array () != nullptr ;
1655- }();
1656- readConfigSection (*shellTbl, config.shell , schema::shellSchema (), " shell" , schemaDiag);
1650+ // Every schema-backed section is read through the section registry, so the loader
1651+ // cannot recognize a section that the validator and exporter do not.
1652+ for (const schema::SectionSpec& spec : schema::sections ()) {
1653+ const auto * sectionTbl = tbl[spec.name ].as_table ();
1654+ if (sectionTbl == nullptr ) {
1655+ continue ;
1656+ }
1657+ try {
1658+ spec.read (*sectionTbl, config, schemaDiag);
1659+ } catch (const std::exception& e) {
1660+ schemaDiag.error (std::string (spec.name ), e.what ());
1661+ kLog .warn (" {}: {}" , spec.name , e.what ());
1662+ }
16571663 }
1664+
1665+ // Default seeding must apply even when the section is absent, so it runs after the
1666+ // registry pass. A schema read can't tell an explicitly empty list from a missing
1667+ // one, so these probe the raw table for the list key.
1668+ const auto hasExplicitArray = [&tbl](std::string_view section, std::string_view key) {
1669+ const auto * sectionTbl = tbl[section].as_table ();
1670+ return sectionTbl != nullptr && (*sectionTbl)[key].as_array () != nullptr ;
1671+ };
1672+ const bool sessionActionsConfigured = [&tbl] {
1673+ const auto * shellTbl = tbl[" shell" ].as_table ();
1674+ const auto * sessionTbl = shellTbl != nullptr ? (*shellTbl)[" session" ].as_table () : nullptr ;
1675+ return sessionTbl != nullptr && (*sessionTbl)[" actions" ].as_array () != nullptr ;
1676+ }();
16581677 if (!sessionActionsConfigured && config.shell .session .actions .empty ()) {
16591678 config.shell .session .actions = defaultSessionPanelActions ();
16601679 }
1661-
1662- // Parse [theme]
1663- if (auto * themeTbl = tbl[" theme" ].as_table ()) {
1664- readConfigSection (*themeTbl, config.theme , schema::themeSchema (), " theme" , schemaDiag);
1665- }
1666-
1667- // Parse [wallpaper] (config keys only; app-managed state keys default/last/
1668- // monitors/favorite are handled separately by extractWallpaperFromTable).
1669- if (auto * wpTbl = tbl[" wallpaper" ].as_table ()) {
1670- readConfigSection (*wpTbl, config.wallpaper , schema::wallpaperSchema (), " wallpaper" , schemaDiag);
1671- }
1672-
1673- // Parse [backdrop]
1674- if (auto * ovTbl = tbl[" backdrop" ].as_table ()) {
1675- readConfigSection (*ovTbl, config.backdrop , schema::backdropSchema (), " backdrop" , schemaDiag);
1676- }
1677-
1678- // Parse [lockscreen]
1679- if (auto * lockTbl = tbl[" lockscreen" ].as_table ()) {
1680- readConfigSection (*lockTbl, config.lockscreen , schema::lockscreenSchema (), " lockscreen" , schemaDiag);
1681- }
1682-
1683- // Parse [osd]
1684- if (auto * osdTbl = tbl[" osd" ].as_table ()) {
1685- readConfigSection (*osdTbl, config.osd , schema::osdSchema (), " osd" , schemaDiag);
1680+ if (!hasExplicitArray (" control_center" , " shortcuts" ) && config.controlCenter .shortcuts .empty ()) {
1681+ config.controlCenter .shortcuts = defaultControlCenterShortcuts ();
16861682 }
1687-
1688- if (auto * notifTbl = tbl[" notification" ].as_table ()) {
1689- readConfigSection (*notifTbl, config.notification , schema::notificationSchema (), " notification" , schemaDiag);
1683+ if (!hasExplicitArray (" plugins" , " source" ) && config.plugins .sources .empty ()) {
1684+ config.plugins .sources = defaultPluginSources ();
16901685 }
1691-
1692- // Parse [dock]
1693- if (auto * dockTbl = tbl[" dock" ].as_table ()) {
1694- readConfigSection (*dockTbl, config.dock , schema::dockSchema (), " dock" , schemaDiag);
1686+ if (config.idle .behaviors .empty ()) {
1687+ config.idle .behaviors = defaultIdleBehaviors ();
16951688 }
16961689
16971690 // Parse [desktop_widgets]
@@ -1720,87 +1713,6 @@ void ConfigService::parseConfigTable(
17201713 );
17211714 }
17221715
1723- // Parse [hot_corners]
1724- if (auto * hotCornersTbl = tbl[" hot_corners" ].as_table ()) {
1725- readConfigSection (*hotCornersTbl, config.hotCorners , schema::hotCornersSchema (), " hot_corners" , schemaDiag);
1726- }
1727-
1728- // Parse [weather]
1729- if (auto * weatherTbl = tbl[" weather" ].as_table ()) {
1730- readConfigSection (*weatherTbl, config.weather , schema::weatherSchema (), " weather" , schemaDiag);
1731- }
1732-
1733- // Parse [calendar]
1734- if (auto * calendarTbl = tbl[" calendar" ].as_table ()) {
1735- readConfigSection (*calendarTbl, config.calendar , schema::calendarSchema (), " calendar" , schemaDiag);
1736- }
1737-
1738- // Parse [system]
1739- if (auto * systemTbl = tbl[" system" ].as_table ()) {
1740- readConfigSection (*systemTbl, config.system , schema::systemSchema (), " system" , schemaDiag);
1741- }
1742-
1743- // Parse [audio]
1744- if (auto * audioTbl = tbl[" audio" ].as_table ()) {
1745- readConfigSection (*audioTbl, config.audio , schema::audioSchema (), " audio" , schemaDiag);
1746- }
1747-
1748- // Parse [brightness]
1749- if (auto * brightnessTbl = tbl[" brightness" ].as_table ()) {
1750- readConfigSection (*brightnessTbl, config.brightness , schema::brightnessSchema (), " brightness" , schemaDiag);
1751- }
1752-
1753- // Parse [battery]
1754- if (auto * batteryTbl = tbl[" battery" ].as_table ()) {
1755- readConfigSection (*batteryTbl, config.battery , schema::batterySchema (), " battery" , schemaDiag);
1756- }
1757-
1758- // Parse [keybinds]
1759- if (auto * keybindsTbl = tbl[" keybinds" ].as_table ()) {
1760- readConfigSection (*keybindsTbl, config.keybinds , schema::keybindsSchema (), " keybinds" , schemaDiag);
1761- }
1762-
1763- // Parse [nightlight]
1764- if (auto * nightlightTbl = tbl[" nightlight" ].as_table ()) {
1765- readConfigSection (*nightlightTbl, config.nightlight , schema::nightlightSchema (), " nightlight" , schemaDiag);
1766- }
1767-
1768- // Parse [location]
1769- if (auto * locationTbl = tbl[" location" ].as_table ()) {
1770- readConfigSection (*locationTbl, config.location , schema::locationSchema (), " location" , schemaDiag);
1771- }
1772-
1773- // Parse [hooks]
1774- if (auto * hooksTbl = tbl[" hooks" ].as_table ()) {
1775- readConfigSection (*hooksTbl, config.hooks , schema::hooksSchema (), " hooks" , schemaDiag);
1776- }
1777-
1778- // Parse [control_center]. The default-shortcuts seeding stays here because it
1779- // must apply even when [control_center] (or its shortcuts array) is absent.
1780- bool controlCenterShortcutsConfigured = false ;
1781- if (auto * ccTbl = tbl[" control_center" ].as_table ()) {
1782- controlCenterShortcutsConfigured = (*ccTbl)[" shortcuts" ].as_array () != nullptr ;
1783- readConfigSection (*ccTbl, config.controlCenter , schema::controlCenterSchema (), " control_center" , schemaDiag);
1784- }
1785- if (!controlCenterShortcutsConfigured && config.controlCenter .shortcuts .empty ()) {
1786- config.controlCenter .shortcuts = defaultControlCenterShortcuts ();
1787- }
1788-
1789- // Parse [plugins]. Default-seeding stays here because it must apply even when
1790- // [plugins] (or its source array) is absent.
1791- bool pluginSourcesConfigured = false ;
1792- if (auto * pluginsTbl = tbl[" plugins" ].as_table ()) {
1793- pluginSourcesConfigured = (*pluginsTbl)[" source" ].as_array () != nullptr ;
1794- readConfigSection (*pluginsTbl, config.plugins , schema::pluginsSchema (), " plugins" , schemaDiag);
1795- }
1796- if (!pluginSourcesConfigured && config.plugins .sources .empty ()) {
1797- config.plugins .sources = defaultPluginSources ();
1798- }
1799-
1800- if (auto * accTbl = tbl[" accessibility" ].as_table ()) {
1801- readConfigSection (*accTbl, config.accessibility , schema::accessibilitySchema (), " accessibility" , schemaDiag);
1802- }
1803-
18041716 // Parse [plugin_settings."author/plugin"] — open-ended per-plugin setting maps,
18051717 // validated against the manifest schema (not the static pluginsSchema). Keys may
18061718 // contain '/', so this is a top-level table rather than nested under [plugins].
@@ -1819,15 +1731,6 @@ void ConfigService::parseConfigTable(
18191731 }
18201732 }
18211733
1822- // Parse [idle] and [idle.behavior.*]. Default-seeding stays here because it
1823- // must apply even when [idle] is absent.
1824- if (auto * idleTbl = tbl[" idle" ].as_table ()) {
1825- readConfigSection (*idleTbl, config.idle , schema::idleSchema (), " idle" , schemaDiag);
1826- }
1827- if (config.idle .behaviors .empty ()) {
1828- config.idle .behaviors = defaultIdleBehaviors ();
1829- }
1830-
18311734 if (config.bars .empty ()) {
18321735 if (logSummary) {
18331736 kLog .info (" no [bar.*] defined, using defaults" );
0 commit comments