diff --git a/hyprtester/plugin/src/main.cpp b/hyprtester/plugin/src/main.cpp index eefbe6908..c130af9c7 100644 --- a/hyprtester/plugin/src/main.cpp +++ b/hyprtester/plugin/src/main.cpp @@ -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); diff --git a/hyprtester/src/tests/main/keybinds.cpp b/hyprtester/src/tests/main/keybinds.cpp index eeae5d3df..75a316f21 100644 --- a/hyprtester/src/tests/main/keybinds.cpp +++ b/hyprtester/src/tests/main/keybinds.cpp @@ -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& 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(); } diff --git a/src/config/lua/bindings/LuaBindingsDispatchers.cpp b/src/config/lua/bindings/LuaBindingsDispatchers.cpp index c9c5ef88d..748b641a4 100644 --- a/src/config/lua/bindings/LuaBindingsDispatchers.cpp +++ b/src/config/lua/bindings/LuaBindingsDispatchers.cpp @@ -376,11 +376,13 @@ static std::expected 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 resolveKeycode(const std::string& ke } } + xkb_state_unref(ks); + if (!keycode) return std::unexpected("key not found");