From a3fa7b49503d8eadd644d68c5bc452b600a11935 Mon Sep 17 00:00:00 2001 From: Vaxry <43317083+vaxerski@users.noreply.github.com> Date: Sat, 2 May 2026 19:18:44 +0100 Subject: [PATCH] keybinds: fix keycode matching on lua (#14254) --- src/config/legacy/ConfigManager.cpp | 8 +-- .../lua/bindings/LuaBindingsToplevel.cpp | 23 ++++++-- src/managers/KeybindManager.cpp | 56 +++++++++++++------ src/managers/KeybindManager.hpp | 12 ++-- 4 files changed, 66 insertions(+), 33 deletions(-) diff --git a/src/config/legacy/ConfigManager.cpp b/src/config/legacy/ConfigManager.cpp index 2705c3959..1b18b4a7d 100644 --- a/src/config/legacy/ConfigManager.cpp +++ b/src/config/legacy/ConfigManager.cpp @@ -1538,15 +1538,15 @@ std::optional CConfigManager::handleBind(const std::string& command else if ((ARGS.size() > sc(4) + DESCR_OFFSET + DEVICE_OFFSET && !mouse) || (ARGS.size() > sc(3) + DESCR_OFFSET + DEVICE_OFFSET && mouse)) return "bind: too many args"; - std::vector KEYSYMS; - std::vector MODS; + std::vector KEYSYMS; + std::vector MODS; if (multiKey) { for (const auto& splitKey : CVarList(ARGS[1], 8, '&')) { - KEYSYMS.emplace_back(xkb_keysym_from_name(splitKey.c_str(), XKB_KEYSYM_CASE_INSENSITIVE)); + KEYSYMS.emplace_back(xkb_keysym_from_name(splitKey.c_str(), XKB_KEYSYM_CASE_INSENSITIVE), 0); } for (const auto& splitMod : CVarList(ARGS[0], 8, '&')) { - MODS.emplace_back(xkb_keysym_from_name(splitMod.c_str(), XKB_KEYSYM_CASE_INSENSITIVE)); + MODS.emplace_back(xkb_keysym_from_name(splitMod.c_str(), XKB_KEYSYM_CASE_INSENSITIVE), 0); } } const auto MOD = g_pKeybindManager->stringToModMask(ARGS[0]); diff --git a/src/config/lua/bindings/LuaBindingsToplevel.cpp b/src/config/lua/bindings/LuaBindingsToplevel.cpp index 8bdd1c193..2d54a7b3c 100644 --- a/src/config/lua/bindings/LuaBindingsToplevel.cpp +++ b/src/config/lua/bindings/LuaBindingsToplevel.cpp @@ -9,6 +9,7 @@ #include "../../../devices/IKeyboard.hpp" #include "../../../managers/eventLoop/EventLoopManager.hpp" +#include #include #include @@ -46,12 +47,12 @@ static bool isSymSpecial(std::string_view sv) { } static std::expected parseKeyString(SKeybind& kb, std::string_view sv) { - bool modsEnded = false, specialSym = false; - CVarList2 vl(sv, 0, '+', true); + bool modsEnded = false, specialSym = false; + CVarList2 vl(sv, 0, '+', true); - uint32_t modMask = 0; - std::vector keysyms; - std::string lastKeyArg; + uint32_t modMask = 0; + std::vector> keysyms; + std::string lastKeyArg; if (sv == "catchall") { kb.catchAll = true; @@ -86,6 +87,16 @@ static std::expected parseKeyString(SKeybind& kb, std::string continue; } + if (arg.starts_with("code:") && isNumber(std::string{arg.substr(5)})) { + auto res = strToNumber(arg.substr(5)); + + if (!res) + return std::unexpected(std::format("Invalid keycode: \"{}\".", arg)); + + keysyms.emplace_back(XKB_KEY_NoSymbol, xkb_keycode_t{*res}); + continue; + } + auto sym = xkb_keysym_from_name(std::string{arg}.c_str(), XKB_KEYSYM_CASE_INSENSITIVE); if (sym == XKB_KEY_NoSymbol) { @@ -99,7 +110,7 @@ static std::expected parseKeyString(SKeybind& kb, std::string } lastKeyArg = arg; - keysyms.emplace_back(sym); + keysyms.emplace_back(sym, 0); } kb.modmask = modMask; diff --git a/src/managers/KeybindManager.cpp b/src/managers/KeybindManager.cpp index 4a5df4be7..2496a0a50 100644 --- a/src/managers/KeybindManager.cpp +++ b/src/managers/KeybindManager.cpp @@ -505,25 +505,44 @@ void CKeybindManager::onSwitchOffEvent(const std::string& switchName) { handleKeybinds(0, SPressedKeyWithMods{.keyName = "switch:off:" + switchName}, true, nullptr, nullptr); } -eMultiKeyCase CKeybindManager::mkKeysymSetMatches(const std::vector keybindKeysyms, const std::set pressedKeysyms) { - // Returns whether two sets of keysyms are equal, partially equal, or not - // matching. (Partially matching means that pressed is a subset of bound) +eMultiKeyCase CKeybindManager::mkKeysymSetMatches(const std::vector& keybindKeysyms, const std::set& pressedKeysyms) { + // Returns whether the bound and pressed keys match fully, partially, or not at all. + // KeybindKey stores {keysym, keycode}; either non-zero field matching is enough. - std::set boundKeysNotPressed; - std::set pressedKeysNotBound; + const auto MATCHES = [](const KeybindKey& lhs, const KeybindKey& rhs) { + return (lhs.first != 0 && rhs.first != 0 && lhs.first == rhs.first) || (lhs.second != 0 && rhs.second != 0 && lhs.second == rhs.second); + }; - std::set symsKb; - for (const auto& k : keybindKeysyms) { - symsKb.emplace(k); + std::vector pressed{pressedKeysyms.begin(), pressedKeysyms.end()}; + std::vector boundForPressed(pressed.size(), -1); + + const auto tryMatch = [&](auto&& self, const size_t boundIdx, std::vector& seen) -> bool { + for (size_t pressedIdx = 0; pressedIdx < pressed.size(); ++pressedIdx) { + if (seen[pressedIdx] || !MATCHES(keybindKeysyms[boundIdx], pressed[pressedIdx])) + continue; + + seen[pressedIdx] = true; + + if (boundForPressed[pressedIdx] == -1 || self(self, static_cast(boundForPressed[pressedIdx]), seen)) { + boundForPressed[pressedIdx] = static_cast(boundIdx); + return true; + } + } + + return false; + }; + + size_t matches = 0; + for (size_t boundIdx = 0; boundIdx < keybindKeysyms.size(); ++boundIdx) { + std::vector seen(pressed.size(), false); + if (tryMatch(tryMatch, boundIdx, seen)) + ++matches; } - std::ranges::set_difference(symsKb, pressedKeysyms, std::inserter(boundKeysNotPressed, boundKeysNotPressed.begin())); - std::ranges::set_difference(pressedKeysyms, symsKb, std::inserter(pressedKeysNotBound, pressedKeysNotBound.begin())); - - if (boundKeysNotPressed.empty() && pressedKeysNotBound.empty()) + if (matches == keybindKeysyms.size() && matches == pressed.size()) return MK_FULL_MATCH; - if (!boundKeysNotPressed.empty() && pressedKeysNotBound.empty()) + if (matches > 0 || (pressed.empty() && !keybindKeysyms.empty())) return MK_PARTIAL_MATCH; return MK_NO_MATCH; @@ -556,14 +575,14 @@ SDispatchResult CKeybindManager::handleKeybinds(const uint32_t modmask, const SP if (key.keysym != 0) { if (pressed) { if (keycodeToModifier(key.keycode)) - m_mkMods.insert(key.keysym); + m_mkMods.emplace(key.keysym, key.keycode); else - m_mkKeys.insert(key.keysym); + m_mkKeys.emplace(key.keysym, key.keycode); } else { if (keycodeToModifier(key.keycode)) - m_mkMods.erase(key.keysym); + std::erase_if(m_mkMods, [&key](const auto& e) { return e.first == key.keysym || e.second == key.keycode; }); else - m_mkKeys.erase(key.keysym); + std::erase_if(m_mkKeys, [&key](const auto& e) { return e.first == key.keysym || e.second == key.keycode; }); } } @@ -615,7 +634,8 @@ SDispatchResult CKeybindManager::handleKeybinds(const uint32_t modmask, const SP // check for just the one match // this is also needed for multi-key binds so that SUPER + A + K can't // be actuated by SUPER + K + A - if (key.keysym != k->sMkKeys.back()) + auto& back = k->sMkKeys.back(); + if (key.keysym != back.first && key.keycode != back.second) continue; } else if (!key.keyName.empty()) { if (key.keyName != k->key) diff --git a/src/managers/KeybindManager.hpp b/src/managers/KeybindManager.hpp index 87cae14af..8c8855801 100644 --- a/src/managers/KeybindManager.hpp +++ b/src/managers/KeybindManager.hpp @@ -25,13 +25,15 @@ struct SSubmap { } }; +using KeybindKey = std::pair; + struct SKeybind { std::string key = ""; - std::vector sMkKeys = {}; + std::vector sMkKeys = {}; uint32_t keycode = 0; bool catchAll = false; uint32_t modmask = 0; - std::vector sMkMods = {}; + std::vector sMkMods = {}; std::string handler = ""; std::string arg = ""; bool locked = false; @@ -156,10 +158,10 @@ class CKeybindManager { SDispatchResult handleKeybinds(const uint32_t, const SPressedKeyWithMods&, bool, SP, SP); - std::set m_mkKeys = {}; - std::set m_mkMods = {}; + std::set m_mkKeys = {}; + std::set m_mkMods = {}; eMultiKeyCase mkBindMatches(const SP); - eMultiKeyCase mkKeysymSetMatches(const std::vector, const std::set); + eMultiKeyCase mkKeysymSetMatches(const std::vector&, const std::set&); bool handleInternalKeybinds(xkb_keysym_t); bool handleVT(xkb_keysym_t);