From 0567ba7607c735f4d920d2d5e491adf4b305be49 Mon Sep 17 00:00:00 2001 From: John Mylchreest Date: Sun, 4 Jan 2026 12:38:00 +0000 Subject: [PATCH] core/parser: correctly match keyed special categories by value being set (#89) When parsing multiple special category blocks with different key values, the second block would incorrectly overwrite the first instead of creating a separate category entry. Root cause: When parsing a block like: wallpaper { monitor = path = /path/image.png } wallpaper { monitor = DP-1 path = /path/image.png } 1. After closing the first block, `currentSpecialKey` resets to "" 2. When parsing `monitor = DP-1` in the second block, the code looks for an existing category where key value == currentSpecialKey ("") 3. The first category's key value IS "", so it matches incorrectly 4. The second block overwrites the first, leaving only one category The fix: When looking for an existing category to reuse, check what field we're parsing: - If parsing the KEY field itself, match by the VALUE being set - If parsing other fields, match by currentSpecialKey (existing behavior) This ensures `monitor = DP-1` looks for a category with `monitor == "DP-1"`, not `monitor == ""`, allowing empty string keys to work correctly alongside non-empty keys. This bug affects any hyprlang consumer using keyed special categories where empty string is a valid key value (e.g., hyprpaper's wallpaper category with `monitor =` for default/wildcard). --- src/config.cpp | 18 ++++++++++++++++-- tests/config/config.conf | 6 ++++++ tests/parse/main.cpp | 13 +++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/config.cpp b/src/config.cpp index c727bec..97af421 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -361,8 +361,22 @@ std::pair CConfig::configSetValueSafe(const std::string& com if (!valueName.starts_with(sc->name + ":")) continue; - if (!sc->isStatic && std::string{std::any_cast(sc->values[sc->key].getValue())} != impl->currentSpecialKey) - continue; + if (!sc->isStatic) { + const auto fieldName = valueName.substr(sc->name.length() + 1); + const auto existingKeyValue = std::string{std::any_cast(sc->values[sc->key].getValue())}; + + // When parsing the key field itself, match by the value being set. + // Otherwise, match by currentSpecialKey. + // This ensures multiple blocks with different key values create separate categories, + // and correctly handles empty string keys. + if (fieldName == sc->key) { + if (existingKeyValue != value) + continue; + } else { + if (existingKeyValue != impl->currentSpecialKey) + continue; + } + } VALUEIT = sc->values.find(valueName.substr(sc->name.length() + 1)); impl->currentSpecialCategory = sc.get(); diff --git a/tests/config/config.conf b/tests/config/config.conf index 096bf09..7658591 100644 --- a/tests/config/config.conf +++ b/tests/config/config.conf @@ -108,6 +108,12 @@ special[b] { value = 2 } +# Test empty key value - should create separate entry from "a" and "b" +special { + key = + value = 100 +} + specialGeneric { one { value = 1 diff --git a/tests/parse/main.cpp b/tests/parse/main.cpp index 309040d..c3b0ef4 100644 --- a/tests/parse/main.cpp +++ b/tests/parse/main.cpp @@ -353,6 +353,19 @@ int main(int argc, char** argv, char** envp) { // test listing keys EXPECT(config.listKeysForSpecialCategory("special")[1], "b"); + // test empty key value - should be a separate entry from "a" and "b" + const auto SPECIAL_KEYS = config.listKeysForSpecialCategory("special"); + EXPECT(SPECIAL_KEYS.size(), 3); + // Find and verify the empty key entry + auto emptyKeyIt = std::find(SPECIAL_KEYS.begin(), SPECIAL_KEYS.end(), ""); + if (emptyKeyIt == SPECIAL_KEYS.end()) { + std::cout << Colors::RED << "Failed: " << Colors::RESET << "empty key not found in listKeysForSpecialCategory\n"; + ret = 1; + } else { + std::cout << Colors::GREEN << "Passed " << Colors::RESET << "empty key found in listKeysForSpecialCategory\n"; + EXPECT(std::any_cast(config.getSpecialConfigValue("special", "value", "")), (Hyprlang::INT)100); + } + // test anonymous EXPECT(config.listKeysForSpecialCategory("specialAnonymous").size(), 2); const auto KEYS = config.listKeysForSpecialCategory("specialAnonymous");