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).
This commit is contained in:
John Mylchreest 2026-01-04 12:38:00 +00:00 committed by GitHub
parent 0d00dc1189
commit 0567ba7607
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 35 additions and 2 deletions

View file

@ -361,8 +361,22 @@ std::pair<bool, CParseResult> CConfig::configSetValueSafe(const std::string& com
if (!valueName.starts_with(sc->name + ":"))
continue;
if (!sc->isStatic && std::string{std::any_cast<const char*>(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<const char*>(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();

View file

@ -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

View file

@ -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<int64_t>(config.getSpecialConfigValue("special", "value", "")), (Hyprlang::INT)100);
}
// test anonymous
EXPECT(config.listKeysForSpecialCategory("specialAnonymous").size(), 2);
const auto KEYS = config.listKeysForSpecialCategory("specialAnonymous");