3
0
mirror of https://github.com/hyprwm/Hyprland.git synced 2026-08-18 02:51:23 +00:00

bindings/dispatchers: Don't inherit key state modmask during key sym lookup (#15401)

* bindings/dispatchers: Don't inherit key state modmask during key sym lookup

Discussion: https://github.com/hyprwm/Hyprland/discussions/14772

In some cases, such as with bind -> sendshortcut, the key sym lookup
will fail resulting in a 'key not found' error when the cached xkb
state is used for lookup.

This can happen if, for example, the SHIFT key is bound in `hl.bind`.
Consider the following case, where SHIFT+1 is mapped to 2:

```lua
hl.bind("SHIFT + 1", hl.dsp.send_shortcut({mods = "", key = "2"}))
````

`resolveKeycode` will fail to lookup 2 because SHIFT is depressed in
the modmask.

Here, I've modified the lookup to use a fresh key state with no modmask.

After lookup, `ConfigActions::pass` will clear the modmask before
sending keys anyway, so an empty modmask can be used for lookup
in `resolveKeycode` safely.

* hyprtester/plugins: Update xkb state for test keyboard

* hyprtester/plugins: Add keybind_modmask plugin

This is different than keybind/keybind2 in that it takes a list of mods,
and applies the mods to the xkb state as well, which the other two do not.

* hyprtester/tests: Add set_shortbut bind tests involving SHIFT

---------

Co-authored-by: Max Asnaashari <max@masnax.org>
This commit is contained in:
Max Asnaashari
2026-08-01 04:27:09 -07:00
committed by GitHub
parent 1ea103b5e5
commit ac82a9c2e8
3 changed files with 116 additions and 15 deletions

View File

@ -125,16 +125,7 @@ class CTestKeyboard : public IKeyboard {
}
void setMods(uint32_t depressed, uint32_t latched, uint32_t locked, uint32_t group) {
m_modifiersState.depressed = depressed;
m_modifiersState.latched = latched;
m_modifiersState.locked = locked;
m_modifiersState.group = group;
m_keyboardEvents.modifiers.emit(IKeyboard::SModifiersEvent{
.depressed = depressed,
.latched = latched,
.locked = locked,
.group = group,
});
updateModifiers(depressed, latched, locked, group);
}
void destroy() {
@ -431,6 +422,28 @@ static SDispatchResult keybind2(std::string in) {
return {};
}
static SDispatchResult keybindModmask(std::string in) {
CVarList2 data(std::move(in));
// 0 = release, 1 = press
bool press;
// See src/devices/IKeyboard.hpp : eKeyboardModifiers for modifier bitmasks
// 0 = none, eKeyboardModifiers is shifted to start at 1
uint32_t modifierMask;
// keycode
uint32_t key;
try {
press = std::stoul(std::string{data[0]}) == 1;
modifierMask = std::stoul(std::string{data[1]});
key = std::stoul(std::string{data[2]}) - 8; // xkb offset
} catch (...) { return {.success = false, .error = "invalid input"}; }
g_pInputManager->m_lastMods = modifierMask;
g_keyboard->setMods(modifierMask, 0, 0, 0);
g_keyboard->sendKey(key, press);
return {};
}
static SDispatchResult setMods(std::string in) {
CVarList2 data(std::move(in));
try {
@ -693,6 +706,13 @@ static int luaKeybind2(lua_State* L) {
return luaResult(L, ::keybind2(std::format("{},{},{}", press, modifier, key)));
}
static int luaKeybindMask(lua_State* L) {
const auto press = (int)luaL_checkinteger(L, 1);
const auto modifierMask = (int)luaL_checkinteger(L, 2);
const auto key = (int)luaL_checkinteger(L, 3);
return luaResult(L, ::keybindModmask(std::format("{},{},{}", press, modifierMask, key)));
}
static int luaSetMods(lua_State* L) {
const auto kbIndex = (int)luaL_checkinteger(L, 1);
const auto depressed = (int)luaL_checkinteger(L, 2);
@ -767,6 +787,7 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) {
addLuaFn("click", ::luaClick);
addLuaFn("keybind", ::luaKeybind);
addLuaFn("keybind2", ::luaKeybind2);
addLuaFn("keybind_modmask", ::luaKeybindMask);
addLuaFn("set_mods", ::luaSetMods);
addLuaFn("nullfocus", ::luaNullfocus);
addLuaFn("clear_surface_focus", ::luaClearSurfaceFocus);

View File

@ -14,6 +14,14 @@ static std::string pluginKeybindCmd(bool pressed, uint32_t modifier, uint32_t ke
return "/eval hl.plugin.test.keybind(" + std::to_string(pressed ? 1 : 0) + ", " + std::to_string(modifier) + ", " + std::to_string(key) + ")";
}
static std::string pluginKeybindMaskCmd(bool pressed, const std::vector<uint8_t>& mods, uint32_t key) {
uint32_t mask = 0;
for (auto m : mods)
mask |= (1 << (m - 1));
return "/eval hl.plugin.test.keybind_modmask(" + std::to_string(pressed ? 1 : 0) + ", " + std::to_string(mask) + ", " + std::to_string(key) + ")";
}
static std::string pluginScrollCmd(int delta) {
return "/eval hl.plugin.test.scroll(" + std::to_string(delta) + ")";
}
@ -295,6 +303,8 @@ SUBTEST(shortcutBind) {
if (!kittyProc) {
FAIL_TEST("Could not spawn kitty");
}
// test SUPER.
EXPECT(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:keybinds_test' })"), "ok");
EXPECT(getFromSocket("/eval hl.bind('SUPER + Y', hl.dsp.send_shortcut({ mods = '', key = 'q', window = 'activewindow' }))"), "ok");
// press keybind
@ -303,10 +313,41 @@ SUBTEST(shortcutBind) {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
OK(getFromSocket(pluginKeybindCmd(false, 0, 29)));
std::this_thread::sleep_for(std::chrono::milliseconds(50));
const std::string output = readKittyOutput();
std::string output = readKittyOutput();
EXPECT_COUNT_STRING(output, "y", 0);
EXPECT(output.find("q") != std::string::npos, true);
EXPECT(getFromSocket("/eval hl.unbind('SUPER + Y')"), "ok");
// test SUPER + SHIFT (pick a different shortcut to avoid caching).
EXPECT(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:keybinds_test' })"), "ok");
EXPECT(getFromSocket("/eval hl.bind('SUPER + SHIFT + Y', hl.dsp.send_shortcut({ mods = '', key = 'w', window = 'activewindow' }))"), "ok");
// press keybind
OK(getFromSocket(pluginKeybindMaskCmd(true, {MOD_META, MOD_SHIFT}, 29)));
// release keybind
std::this_thread::sleep_for(std::chrono::milliseconds(50));
OK(getFromSocket(pluginKeybindMaskCmd(false, {}, 29)));
std::this_thread::sleep_for(std::chrono::milliseconds(50));
output = readKittyOutput();
EXPECT_COUNT_STRING(output, "y", 0);
EXPECT_COUNT_STRING(output, "Y", 0);
EXPECT(output.find("w") != std::string::npos, true);
EXPECT(getFromSocket("/eval hl.unbind('SUPER + SHIFT + Y')"), "ok");
// test SUPER + SHIFT (check numbers (1 -> ! is not captured by case sensitivity)).
EXPECT(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:keybinds_test' })"), "ok");
EXPECT(getFromSocket("/eval hl.bind('SUPER + SHIFT + Y', hl.dsp.send_shortcut({ mods = '', key = '1', window = 'activewindow' }))"), "ok");
// press keybind
OK(getFromSocket(pluginKeybindMaskCmd(true, {MOD_META, MOD_SHIFT}, 29)));
// release keybind
std::this_thread::sleep_for(std::chrono::milliseconds(50));
OK(getFromSocket(pluginKeybindMaskCmd(false, {}, 29)));
std::this_thread::sleep_for(std::chrono::milliseconds(50));
output = readKittyOutput();
EXPECT_COUNT_STRING(output, "y", 0);
EXPECT_COUNT_STRING(output, "Y", 0);
EXPECT(output.find("1") != std::string::npos, true);
EXPECT(getFromSocket("/eval hl.unbind('SUPER + SHIFT + Y')"), "ok");
Tests::killAllWindows();
}
@ -315,19 +356,54 @@ SUBTEST(shortcutBindKey) {
if (!kittyProc) {
FAIL_TEST("Could not spawn kitty");
}
// test lowercase Y.
EXPECT(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:keybinds_test' })"), "ok");
EXPECT(getFromSocket("/eval hl.bind('Y', hl.dsp.send_shortcut({ mods = '', key = 'q', window = 'activewindow' }))"), "ok");
EXPECT(getFromSocket("/eval hl.bind('Y', hl.dsp.send_shortcut({ mods = '', key = 'e', window = 'activewindow' }))"), "ok");
// press keybind
OK(getFromSocket(pluginKeybindCmd(true, 0, 29)));
// release keybind
std::this_thread::sleep_for(std::chrono::milliseconds(50));
OK(getFromSocket(pluginKeybindCmd(false, 0, 29)));
std::this_thread::sleep_for(std::chrono::milliseconds(50));
const std::string output = readKittyOutput();
std::string output = readKittyOutput();
EXPECT_COUNT_STRING(output, "y", 0);
// disabled: doesn't work in CI
// EXPECT_COUNT_STRING(output, "q", 1);
// EXPECT_COUNT_STRING(output, "e", 1);
EXPECT(getFromSocket("/eval hl.unbind('Y')"), "ok");
// test SHIFT + Y (pick a different shortcut to avoid caching).
EXPECT(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:keybinds_test' })"), "ok");
EXPECT(getFromSocket("/eval hl.bind('SHIFT + Y', hl.dsp.send_shortcut({ mods = '', key = 'r', window = 'activewindow' }))"), "ok");
// press keybind
OK(getFromSocket(pluginKeybindMaskCmd(true, {MOD_SHIFT}, 29)));
// release keybind
std::this_thread::sleep_for(std::chrono::milliseconds(50));
OK(getFromSocket(pluginKeybindMaskCmd(false, {}, 29)));
std::this_thread::sleep_for(std::chrono::milliseconds(50));
output = readKittyOutput();
EXPECT_COUNT_STRING(output, "y", 0);
EXPECT_COUNT_STRING(output, "Y", 0);
// disabled: doesn't work in CI
// EXPECT_COUNT_STRING(output, "r", 1);
EXPECT(getFromSocket("/eval hl.unbind('SHIFT + Y')"), "ok");
// test SHIFT + Y (check numbers (2 -> @ is not captured by case sensitivity)).
EXPECT(getFromSocket("/dispatch hl.dsp.focus({ window = 'class:keybinds_test' })"), "ok");
EXPECT(getFromSocket("/eval hl.bind('SHIFT + Y', hl.dsp.send_shortcut({ mods = '', key = '2', window = 'activewindow' }))"), "ok");
// press keybind
OK(getFromSocket(pluginKeybindMaskCmd(true, {MOD_SHIFT}, 29)));
// release keybind
std::this_thread::sleep_for(std::chrono::milliseconds(50));
OK(getFromSocket(pluginKeybindMaskCmd(false, {}, 29)));
std::this_thread::sleep_for(std::chrono::milliseconds(50));
output = readKittyOutput();
EXPECT_COUNT_STRING(output, "y", 0);
EXPECT_COUNT_STRING(output, "Y", 0);
// disabled: doesn't work in CI
// EXPECT_COUNT_STRING(output, "2", 1);
EXPECT(getFromSocket("/eval hl.unbind('SHIFT + Y')"), "ok");
Tests::killAllWindows();
}

View File

@ -376,11 +376,13 @@ static std::expected<uint32_t, std::string> resolveKeycode(const std::string& ke
return g_pKeybindManager->m_keyToCodeCache[KEYPAIRSTRING];
xkb_keymap* km = KB->m_xkbKeymap;
xkb_state* ks = KB->m_xkbState;
xkb_state* ks = xkb_state_new(km);
xkb_keycode_t keycode_min = xkb_keymap_min_keycode(km);
xkb_keycode_t keycode_max = xkb_keymap_max_keycode(km);
uint32_t keycode = 0;
xkb_state_update_mask(ks, 0, 0, 0, 0, 0, KB->m_modifiersState.group);
for (xkb_keycode_t kc = keycode_min; kc <= keycode_max; ++kc) {
xkb_keysym_t sym = xkb_state_key_get_one_sym(ks, kc);
if (sym == KEYSYM) {
@ -389,6 +391,8 @@ static std::expected<uint32_t, std::string> resolveKeycode(const std::string& ke
}
}
xkb_state_unref(ks);
if (!keycode)
return std::unexpected("key not found");