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:
@ -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);
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -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");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user