From 0b0d7ede2192ae515638890037890bdecca6eba2 Mon Sep 17 00:00:00 2001 From: Gwilherm Folliot Date: Wed, 15 Jul 2026 23:56:05 +0200 Subject: [PATCH] input-capture: impl protocol (#7919) Co-authored-by: Gerben Meijer Co-authored-by: Emerson Matos Co-authored-by: Mihai Fufezan Co-authored-by: Ashley Stonham Co-authored-by: extra-salad <87635414+extra-salad@users.noreply.github.com> --- .github/actions/setup_base/action.yml | 1 + CMakeLists.txt | 8 +- meta/generateLuaStubs.py | 1 + nix/default.nix | 2 + src/config/ConfigManager.cpp | 2 +- src/config/legacy/ConfigManager.cpp | 41 +- src/config/legacy/DispatcherTranslator.cpp | 5 + .../lua/bindings/LuaBindingsDispatchers.cpp | 12 + .../lua/bindings/LuaBindingsToplevel.cpp | 1 + src/config/lua/objects/LuaKeybind.cpp | 2 + src/config/shared/actions/ConfigActions.cpp | 6 + src/config/shared/actions/ConfigActions.hpp | 2 + src/config/values/ConfigValues.cpp | 6 + src/debug/HyprCtl.cpp | 9 +- src/i18n/Engine.cpp | 2 + src/i18n/Engine.hpp | 3 +- src/managers/KeybindManager.cpp | 9 +- src/managers/KeybindManager.hpp | 58 +- src/managers/ProtocolManager.cpp | 3 + src/managers/SeatManager.cpp | 2 + src/managers/input/Eis.cpp | 335 ++++++++++++ src/managers/input/Eis.hpp | 62 +++ src/managers/input/InputManager.cpp | 38 +- src/managers/input/InputManager.hpp | 1 + .../permissions/DynamicPermissionManager.cpp | 2 + .../permissions/DynamicPermissionManager.hpp | 1 + src/pointer/PointerManager.cpp | 7 + src/protocols/InputCapture.cpp | 496 ++++++++++++++++++ src/protocols/InputCapture.hpp | 122 +++++ src/protocols/core/Output.cpp | 4 +- src/render/Renderer.cpp | 3 +- subprojects/hyprland-protocols | 2 +- tests/config/lua/LuaObjectsBasic.cpp | 50 +- 33 files changed, 1210 insertions(+), 88 deletions(-) create mode 100644 src/managers/input/Eis.cpp create mode 100644 src/managers/input/Eis.hpp create mode 100644 src/protocols/InputCapture.cpp create mode 100644 src/protocols/InputCapture.hpp diff --git a/.github/actions/setup_base/action.yml b/.github/actions/setup_base/action.yml index 355699e96..06af3a447 100644 --- a/.github/actions/setup_base/action.yml +++ b/.github/actions/setup_base/action.yml @@ -31,6 +31,7 @@ runs: libc++ \ libdisplay-info \ libdrm \ + libei \ libepoxy \ libfontenc \ libglvnd \ diff --git a/CMakeLists.txt b/CMakeLists.txt index b201b6a4e..bf9c3fc3d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -252,8 +252,7 @@ set(WAYLAND_SERVER_MINIMUM_VERSION 1.22.91) set(WAYLAND_SERVER_PROTOCOLS_MINIMUM_VERSION 1.49) set(LIBINPUT_MINIMUM_VERSION 1.29) -pkg_check_modules( - deps +pkg_check_modules(deps REQUIRED IMPORTED_TARGET GLOBAL xkbcommon>=${XKBCOMMON_MINIMUM_VERSION} @@ -267,6 +266,7 @@ pkg_check_modules( xcursor libdrm libinput>=${LIBINPUT_MINIMUM_VERSION} + libeis-1.0 gbm gio-2.0 re2 @@ -514,7 +514,7 @@ else() target_link_libraries(hyprland_lib PUBLIC OpenGL::EGL OpenGL::GLES3 glslang::glslang glslang::glslang-default-resource-limits glslang::SPIRV Threads::Threads) endif() -pkg_check_modules(hyprland_protocols_dep hyprland-protocols>=0.6.4) +pkg_check_modules(hyprland_protocols_dep hyprland-protocols>=0.7.0) if(hyprland_protocols_dep_FOUND) pkg_get_variable(HYPRLAND_PROTOCOLS hyprland-protocols pkgdatadir) message(STATUS "hyprland-protocols dependency set to ${HYPRLAND_PROTOCOLS}") @@ -593,6 +593,8 @@ protocolnew("staging/ext-image-capture-source" "ext-image-capture-source-v1" fal protocolnew("staging/ext-image-copy-capture" "ext-image-copy-capture-v1" false) protocolnew("staging/ext-background-effect" "ext-background-effect-v1" false) +protocolnew("${HYPRLAND_PROTOCOLS}/protocols" "hyprland-input-capture-v1" + true) protocolwayland() # tools diff --git a/meta/generateLuaStubs.py b/meta/generateLuaStubs.py index a77a2e5e6..da3c900a8 100644 --- a/meta/generateLuaStubs.py +++ b/meta/generateLuaStubs.py @@ -667,6 +667,7 @@ def generate_stub(root: Path) -> str: ("description", "string", True), ("desc", "string", True), ("device", "{inclusive?: boolean, list?: string[]}", True), + ("allow_input_capture", "boolean", True), ], ) ) diff --git a/nix/default.nix b/nix/default.nix index 80647d685..dea2c4e2e 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -25,6 +25,7 @@ lcms2, libGL, libdrm, + libei, libexecinfo, libinput, libxcb, @@ -191,6 +192,7 @@ customStdenv.mkDerivation (finalAttrs: { libdrm libgbm libGL + libei libinput libuuid libxcursor diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp index 297e9c04f..74010df87 100644 --- a/src/config/ConfigManager.cpp +++ b/src/config/ConfigManager.cpp @@ -80,4 +80,4 @@ const char* Config::typeToString(eConfigManagerType t) { case CONFIG_LEGACY: return "hyprlang"; default: return "error"; } -} +} \ No newline at end of file diff --git a/src/config/legacy/ConfigManager.cpp b/src/config/legacy/ConfigManager.cpp index 10041fd14..4f1b11f25 100644 --- a/src/config/legacy/ConfigManager.cpp +++ b/src/config/legacy/ConfigManager.cpp @@ -1494,23 +1494,24 @@ std::optional CConfigManager::handleBind(const std::string& command // bind[fl]=SUPER,G,exec,dmenu_run // flags - bool locked = false; - bool release = false; - bool repeat = false; - bool mouse = false; - bool nonConsuming = false; - bool autoConsuming = false; - bool transparent = false; - bool ignoreMods = false; - bool multiKey = false; - bool longPress = false; - bool hasDescription = false; - bool dontInhibit = false; - bool click = false; - bool drag = false; - bool submapUniversal = false; - bool isPerDevice = false; - const auto BINDARGS = command.substr(4); + bool locked = false; + bool release = false; + bool repeat = false; + bool mouse = false; + bool nonConsuming = false; + bool autoConsuming = false; + bool transparent = false; + bool ignoreMods = false; + bool multiKey = false; + bool longPress = false; + bool hasDescription = false; + bool dontInhibit = false; + bool click = false; + bool drag = false; + bool submapUniversal = false; + bool isPerDevice = false; + bool allowInputCapture = false; + const auto BINDARGS = command.substr(4); for (auto const& arg : BINDARGS) { switch (arg) { @@ -1536,6 +1537,7 @@ std::optional CConfigManager::handleBind(const std::string& command break; case 'u': submapUniversal = true; break; case 'k': isPerDevice = true; break; + case 'x': allowInputCapture = true; break; default: return "bind: invalid flag"; } } @@ -1645,7 +1647,8 @@ std::optional CConfigManager::handleBind(const std::string& command drag, submapUniversal, deviceInclusive, - devices}); + devices, + allowInputCapture}); } return {}; @@ -1904,6 +1907,8 @@ std::optional CConfigManager::handlePermission(const std::string& c type = PERMISSION_TYPE_PLUGIN; else if (data[1] == "keyboard" || data[1] == "keeb") type = PERMISSION_TYPE_KEYBOARD; + else if (data[1] == "input-capture") + type = PERMISSION_TYPE_INPUT_CAPTURE; if (data[2] == "ask") mode = PERMISSION_RULE_ALLOW_MODE_ASK; diff --git a/src/config/legacy/DispatcherTranslator.cpp b/src/config/legacy/DispatcherTranslator.cpp index e34510723..b534fec35 100644 --- a/src/config/legacy/DispatcherTranslator.cpp +++ b/src/config/legacy/DispatcherTranslator.cpp @@ -789,6 +789,10 @@ static SDispatchResult forceidle(const std::string& args) { return wrap(Actions::forceIdle(duration.value())); } +static SDispatchResult releaseInputCapture(const std::string& args) { + return wrap(Actions::releaseInputCapture()); +} + CDispatcherTranslator::CDispatcherTranslator() { m_dispMap["exec"] = ::exec; m_dispMap["execr"] = ::execr; @@ -860,4 +864,5 @@ CDispatcherTranslator::CDispatcherTranslator() { m_dispMap["global"] = ::globalDispatcher; m_dispMap["setprop"] = ::setprop; m_dispMap["forceidle"] = ::forceidle; + m_dispMap["releaseinputcapture"] = ::releaseInputCapture; } diff --git a/src/config/lua/bindings/LuaBindingsDispatchers.cpp b/src/config/lua/bindings/LuaBindingsDispatchers.cpp index 3e0e14d36..c9c5ef88d 100644 --- a/src/config/lua/bindings/LuaBindingsDispatchers.cpp +++ b/src/config/lua/bindings/LuaBindingsDispatchers.cpp @@ -1,6 +1,7 @@ #include "LuaBindingsInternal.hpp" #include +#include #include "Check.hpp" @@ -12,6 +13,7 @@ #include "../../../state/WorkspaceState.hpp" #include "../../../devices/IKeyboard.hpp" #include "../../../desktop/rule/windowRule/WindowRule.hpp" +#include "config/shared/actions/ConfigActions.hpp" using namespace Config; using namespace Config::Lua; @@ -224,6 +226,10 @@ static int dsp_forceIdle(lua_State* L) { return Internal::checkResult(L, CA::forceIdle((float)lua_tonumber(L, lua_upvalueindex(1)))); } +static int dsp_releaseInputCapture(lua_State* L) { + return Internal::checkResult(L, CA::releaseInputCapture()); +} + static int hlExecCmd(lua_State* L) { const auto proc = Check::string(L, 1); @@ -339,6 +345,11 @@ static int hlForceIdle(lua_State* L) { return 1; } +static int hlReleaseInputCapture(lua_State* L) { + lua_pushcclosure(L, dsp_releaseInputCapture, 1); + return 1; +} + static std::expected resolveKeycode(const std::string& key) { if (isNumber(key) && std::stoi(key) > 9) return (uint32_t)std::stoi(key); @@ -1394,6 +1405,7 @@ void Internal::registerDispatcherBindings(lua_State* L) { Internal::setFn(L, "global", hlGlobal); Internal::setFn(L, "force_renderer_reload", hlForceRendererReload); Internal::setFn(L, "force_idle", hlForceIdle); + Internal::setFn(L, "release_input_capture", hlReleaseInputCapture); Internal::setFn(L, "focus", hlFocus); Internal::setFn(L, "no_op", hlNoop); } diff --git a/src/config/lua/bindings/LuaBindingsToplevel.cpp b/src/config/lua/bindings/LuaBindingsToplevel.cpp index 41a66a985..9bacdeffe 100644 --- a/src/config/lua/bindings/LuaBindingsToplevel.cpp +++ b/src/config/lua/bindings/LuaBindingsToplevel.cpp @@ -246,6 +246,7 @@ static int hlBind(lua_State* L) { } lua_pop(L, 1); } + kb.allowInputCapture = getBool("allow_input_capture"); lua_pop(L, 1); } diff --git a/src/config/lua/objects/LuaKeybind.cpp b/src/config/lua/objects/LuaKeybind.cpp index ea2ce43d8..c3de0d5b5 100644 --- a/src/config/lua/objects/LuaKeybind.cpp +++ b/src/config/lua/objects/LuaKeybind.cpp @@ -165,6 +165,8 @@ static int keybindIndex(lua_State* L) { lua_pushboolean(L, (*keybind)->deviceInclusive); else if (key == "devices") pushDeviceList(L, **keybind); + else if (key == "allow_input_capture") + lua_pushboolean(L, (*keybind)->allowInputCapture); else lua_pushnil(L); diff --git a/src/config/shared/actions/ConfigActions.cpp b/src/config/shared/actions/ConfigActions.cpp index 6aeaa9718..fe1b9bc2a 100644 --- a/src/config/shared/actions/ConfigActions.cpp +++ b/src/config/shared/actions/ConfigActions.cpp @@ -22,6 +22,7 @@ #include "../../../config/shared/monitor/MonitorRuleManager.hpp" #include "../../../protocols/IdleNotify.hpp" #include "../../../protocols/GlobalShortcuts.hpp" +#include "../../../protocols/InputCapture.hpp" #include "../../../event/EventBus.hpp" #include "../../../managers/XWaylandManager.hpp" #include "../../../layout/algorithm/Algorithm.hpp" @@ -1761,3 +1762,8 @@ ActionResult Actions::moveIntoOrCreateGroup(Math::eDirection dir, std::optional< return {}; } + +ActionResult Actions::releaseInputCapture() { + PROTO::inputCapture->forceRelease(); + return {}; +} diff --git a/src/config/shared/actions/ConfigActions.hpp b/src/config/shared/actions/ConfigActions.hpp index e0dda23e5..951924581 100644 --- a/src/config/shared/actions/ConfigActions.hpp +++ b/src/config/shared/actions/ConfigActions.hpp @@ -104,6 +104,8 @@ namespace Config::Actions { ActionResult denyWindowFromGroup(eTogglableAction action); ActionResult moveIntoOrCreateGroup(Math::eDirection dir, std::optional window = std::nullopt /* Active */); + ActionResult releaseInputCapture(); + class CActionState { public: CActionState() = default; diff --git a/src/config/values/ConfigValues.cpp b/src/config/values/ConfigValues.cpp index 06e4b7cff..8b2f65182 100644 --- a/src/config/values/ConfigValues.cpp +++ b/src/config/values/ConfigValues.cpp @@ -713,6 +713,12 @@ std::vector> Values::getConfigValues() { MS("experimental:wp_cm_1_2", "Allow wp-cm-v1 version 2", true), + /* + * input_capture: + */ + MS("input-capture:capture_modifiers", "If enabled, modifiers are also captured and sent to the program", false), + MS("input-capture:enforce_barriers", "If enabled, throw a wayland error when a invalid barrier is received", true), + /* * quirks: */ diff --git a/src/debug/HyprCtl.cpp b/src/debug/HyprCtl.cpp index faa51efc0..b05bf3db6 100644 --- a/src/debug/HyprCtl.cpp +++ b/src/debug/HyprCtl.cpp @@ -1041,6 +1041,8 @@ static std::string bindsRequest(eHyprCtlOutputFormat format, std::string request ret += "a"; if (kb->hasDescription) ret += "d"; + if (kb->allowInputCapture) + ret += "x"; ret += std::format("\n\tmodmask: {}\n\tsubmap: {}\n\tkey: {}\n\tkeycode: {}\n\tcatchall: {}\n\tdescription: {}\n\tdispatcher: {}\n\targ: {}\n\n", kb->modmask, kb->submap.name, kb->key.empty() ? kb->displayKey : kb->key, kb->keycode, kb->catchAll, kb->description, kb->handler, kb->arg); @@ -1067,13 +1069,14 @@ static std::string bindsRequest(eHyprCtlOutputFormat format, std::string request "keycode": {}, "catch_all": {}, "description": "{}", + "allow_input_capture": {}, "dispatcher": "{}", "arg": "{}" }},)#", kb->locked ? "true" : "false", kb->mouse ? "true" : "false", kb->release ? "true" : "false", kb->repeat ? "true" : "false", kb->longPress ? "true" : "false", - kb->nonConsuming ? "true" : "false", kb->autoConsuming ? "true" : "false", kb->hasDescription ? "true" : "false", kb->modmask, escapeJSONStrings(kb->submap.name), - kb->submapUniversal, escapeJSONStrings(kb->key), kb->keycode, kb->catchAll ? "true" : "false", escapeJSONStrings(kb->description), escapeJSONStrings(kb->handler), - escapeJSONStrings(kb->arg)); + kb->nonConsuming ? "true" : "false", kb->autoConsuming ? "true" : "false", kb->allowInputCapture ? "true" : "false", kb->hasDescription ? "true" : "false", + kb->modmask, escapeJSONStrings(kb->submap.name), kb->submapUniversal, escapeJSONStrings(kb->key), kb->keycode, kb->catchAll ? "true" : "false", + escapeJSONStrings(kb->description), escapeJSONStrings(kb->handler), escapeJSONStrings(kb->arg)); } trimTrailingComma(ret); ret += "]"; diff --git a/src/i18n/Engine.cpp b/src/i18n/Engine.cpp index c737095c1..6bbb38b61 100644 --- a/src/i18n/Engine.cpp +++ b/src/i18n/Engine.cpp @@ -221,6 +221,8 @@ I18n::CI18nEngine::CI18nEngine() { registerEntry("en_US", TXT_KEY_PERMISSION_REQUEST_CURSOR_POS, "An application {app} is trying to capture your cursor position.\n\nDo you want to allow it to?"); registerEntry("en_US", TXT_KEY_PERMISSION_REQUEST_PLUGIN, "An application {app} is trying to load a plugin: {plugin}.\n\nDo you want to allow it to?"); registerEntry("en_US", TXT_KEY_PERMISSION_REQUEST_KEYBOARD, "A new keyboard has been detected: {keyboard}.\n\nDo you want to allow it to operate?"); + huEngine->registerEntry("en_US", TXT_KEY_PERMISSION_REQUEST_INPUT_CAPTURE, + "An application {app} is trying to capture your inputs.\n\nDo you want to allow it to do so?"); registerEntry("en_US", TXT_KEY_PERMISSION_UNKNOWN_NAME, "(unknown)"); registerEntry("en_US", TXT_KEY_PERMISSION_TITLE, "Permission request"); registerEntry("en_US", TXT_KEY_PERMISSION_PERSISTENCE_HINT, "Hint: you can set persistent rules for these in the Hyprland config file."); diff --git a/src/i18n/Engine.hpp b/src/i18n/Engine.hpp index 79ec86f89..73140ddb4 100644 --- a/src/i18n/Engine.hpp +++ b/src/i18n/Engine.hpp @@ -19,6 +19,7 @@ namespace I18n { TXT_KEY_PERMISSION_REQUEST_CURSOR_POS, TXT_KEY_PERMISSION_REQUEST_PLUGIN, TXT_KEY_PERMISSION_REQUEST_KEYBOARD, + TXT_KEY_PERMISSION_REQUEST_INPUT_CAPTURE, TXT_KEY_PERMISSION_UNKNOWN_NAME, TXT_KEY_PERMISSION_TITLE, TXT_KEY_PERMISSION_PERSISTENCE_HINT, @@ -55,4 +56,4 @@ namespace I18n { }; SP i18nEngine(); -}; +}; \ No newline at end of file diff --git a/src/managers/KeybindManager.cpp b/src/managers/KeybindManager.cpp index 65fc1e048..4ab407ecd 100644 --- a/src/managers/KeybindManager.cpp +++ b/src/managers/KeybindManager.cpp @@ -5,6 +5,7 @@ #include "../devices/IKeyboard.hpp" #include "../managers/SeatManager.hpp" #include "../managers/fullscreen/FullscreenController.hpp" +#include "../protocols/InputCapture.hpp" #include "../protocols/ShortcutsInhibit.hpp" #include "../protocols/Hotkey.hpp" #include "../protocols/core/DataDevice.hpp" @@ -107,7 +108,8 @@ CKeybindManager::CKeybindManager() { "event", "global", "setprop", - "forceidle"}) { + "forceidle", + "releaseinputcapture"}) { m_dispatchers[name] = [n = std::string(name)](std::string args) -> SDispatchResult { return Config::Legacy::translator()->run(n, args); }; } @@ -372,7 +374,7 @@ bool CKeybindManager::onKeyEvent(std::any event, SP pKeyboard) { // handleInternalKeybinds returns true when the key should be suppressed, // while this function returns true when the key event should be sent - if (handleInternalKeybinds(internalKeysym)) + if (!PROTO::inputCapture->isCaptured() && handleInternalKeybinds(internalKeysym)) return false; const auto MODS = g_pInputManager->getModsFromAllKBs(); @@ -630,6 +632,9 @@ SDispatchResult CKeybindManager::handleKeybinds(const uint32_t modmask, const SP std::vector> bindsHit; for (auto& k : m_keybinds) { + if (PROTO::inputCapture->isCaptured() && !k->allowInputCapture) + continue; + const bool SPECIALDISPATCHER = k->handler == "global" || k->handler == "pass" || k->handler == "sendshortcut" || k->handler == "mouse" || k->releasePending; const bool SPECIALTRIGGERED = std::ranges::find_if(m_pressedSpecialBinds, [&](const auto& other) { return other == k; }) != m_pressedSpecialBinds.end(); const bool IGNORECONDITIONS = diff --git a/src/managers/KeybindManager.hpp b/src/managers/KeybindManager.hpp index a8bdb47f5..7aa01121c 100644 --- a/src/managers/KeybindManager.hpp +++ b/src/managers/KeybindManager.hpp @@ -32,34 +32,35 @@ struct SSubmap { using KeybindKey = std::pair; struct SKeybind { - std::string key = ""; - std::vector sMkKeys = {}; - uint32_t keycode = 0; - bool catchAll = false; - uint32_t modmask = 0; - std::vector sMkMods = {}; - std::string handler = ""; - std::string arg = ""; - bool locked = false; - SSubmap submap = {}; - std::string description = ""; - bool release = false; - bool repeat = false; - bool longPress = false; - bool mouse = false; - bool nonConsuming = false; - bool autoConsuming = false; - bool transparent = false; - bool ignoreMods = false; - bool multiKey = false; - bool hasDescription = false; - bool dontInhibit = false; - bool click = false; - bool drag = false; - bool submapUniversal = false; - bool deviceInclusive = false; - std::unordered_set devices = {}; - bool enabled = true; + std::string key = ""; + std::vector sMkKeys = {}; + uint32_t keycode = 0; + bool catchAll = false; + uint32_t modmask = 0; + std::vector sMkMods = {}; + std::string handler = ""; + std::string arg = ""; + bool locked = false; + SSubmap submap = {}; + std::string description = ""; + bool release = false; + bool repeat = false; + bool longPress = false; + bool mouse = false; + bool nonConsuming = false; + bool autoConsuming = false; + bool transparent = false; + bool ignoreMods = false; + bool multiKey = false; + bool hasDescription = false; + bool dontInhibit = false; + bool click = false; + bool drag = false; + bool submapUniversal = false; + bool deviceInclusive = false; + std::unordered_set devices = {}; + bool allowInputCapture = false; + bool enabled = true; std::string displayKey = ""; @@ -177,6 +178,7 @@ class CKeybindManager { void updateXKBTranslationState(); bool ensureMouseBindState(); + static SDispatchResult releaseInputCapture(std::string); friend class CCompositor; friend class CInputManager; diff --git a/src/managers/ProtocolManager.cpp b/src/managers/ProtocolManager.cpp index 442e30489..bf20269ca 100644 --- a/src/managers/ProtocolManager.cpp +++ b/src/managers/ProtocolManager.cpp @@ -49,6 +49,7 @@ #include "../protocols/SinglePixel.hpp" #include "../protocols/SecurityContext.hpp" #include "../protocols/CTMControl.hpp" +#include "../protocols/InputCapture.hpp" #include "../protocols/HyprlandSurface.hpp" #include "../protocols/ImageCaptureSource.hpp" #include "../protocols/ImageCopyCapture.hpp" @@ -207,6 +208,7 @@ CProtocolManager::CProtocolManager() { PROTO::singlePixel = makeUnique(&wp_single_pixel_buffer_manager_v1_interface, 1, "SinglePixel"); PROTO::securityContext = makeUnique(&wp_security_context_manager_v1_interface, 1, "SecurityContext"); PROTO::ctm = makeUnique(&hyprland_ctm_control_manager_v1_interface, 2, "CTMControl"); + PROTO::inputCapture = makeUnique(&hyprland_input_capture_manager_v1_interface, 1, "InputCapture"); PROTO::hyprlandSurface = makeUnique(&hyprland_surface_manager_v1_interface, 2, "HyprlandSurface"); PROTO::contentType = makeUnique(&wp_content_type_manager_v1_interface, 1, "ContentType"); PROTO::xdgTag = makeUnique(&xdg_toplevel_tag_manager_v1_interface, 1, "XDGTag"); @@ -318,6 +320,7 @@ CProtocolManager::~CProtocolManager() { PROTO::singlePixel.reset(); PROTO::securityContext.reset(); PROTO::ctm.reset(); + PROTO::inputCapture.reset(); PROTO::hyprlandSurface.reset(); PROTO::contentType.reset(); PROTO::colorManagement.reset(); diff --git a/src/managers/SeatManager.cpp b/src/managers/SeatManager.cpp index ba6aeb270..f1557bad1 100644 --- a/src/managers/SeatManager.cpp +++ b/src/managers/SeatManager.cpp @@ -6,6 +6,7 @@ #include "../protocols/PrimarySelection.hpp" #include "../protocols/core/Compositor.hpp" #include "../protocols/LayerShell.hpp" +#include "../protocols/InputCapture.hpp" #include "../Compositor.hpp" #include "../desktop/state/FocusState.hpp" #include "../devices/IKeyboard.hpp" @@ -168,6 +169,7 @@ void CSeatManager::updateActiveKeyboardData() { if (m_keyboard) PROTO::seat->updateRepeatInfo(m_keyboard->m_repeatRate, m_keyboard->m_repeatDelay); PROTO::seat->updateKeymap(); + PROTO::inputCapture->updateKeymap(); } void CSeatManager::setKeyboardFocus(SP surf) { diff --git a/src/managers/input/Eis.cpp b/src/managers/input/Eis.cpp new file mode 100644 index 000000000..d6b2d16ee --- /dev/null +++ b/src/managers/input/Eis.cpp @@ -0,0 +1,335 @@ +#include "Eis.hpp" + +#include "Compositor.hpp" +#include "devices/IKeyboard.hpp" +#include "managers/SeatManager.hpp" +#include "state/MonitorState.hpp" +#include "output/Monitor.hpp" +#include +#include +#include +#include +#include +#include +#include + +CEis::CEis(std::string socketName) { + Log::logger->log(Log::INFO, "[EIS] Init socket: {}", socketName); + + const char* xdg = getenv("XDG_RUNTIME_DIR"); + if (xdg) + m_socketPath = std::string(xdg) + "/" + socketName; + + if (m_socketPath.empty()) { + Log::logger->log(Log::ERR, "[EIS] Socket path is empty"); + return; + } + + m_eisCtx = eis_new(nullptr); + if (!m_eisCtx) { + Log::logger->log(Log::ERR, "[EIS] Cannot create eis context"); + return; + } + + if (eis_setup_backend_socket(m_eisCtx, m_socketPath.c_str())) { + Log::logger->log(Log::ERR, "[EIS] Cannot init eis socket on {}", m_socketPath); + return; + } + Log::logger->log(Log::INFO, "[EIS] Listening on {}", m_socketPath); + + m_eventSource = wl_event_loop_add_fd( + g_pCompositor->m_wlEventLoop, eis_get_fd(m_eisCtx), WL_EVENT_READABLE, [](int fd, uint32_t mask, void* data) { return ((CEis*)data)->pollEvents(); }, this); +} + +CEis::~CEis() { + if (m_eventSource) + wl_event_source_remove(m_eventSource); + + if (m_eisCtx) { + Log::logger->log(Log::INFO, "[EIS] Server fd {} destroyed", eis_get_fd(m_eisCtx)); + eis_unref(m_eisCtx); + } +} + +int CEis::pollEvents() { + if (!m_eisCtx) + return 0; + + eis_dispatch(m_eisCtx); + + //Pull every available events + while (true) { + eis_event* e = eis_get_event(m_eisCtx); + + if (!e) { + eis_event_unref(e); + break; + } + + int rc = onEvent(e); + eis_event_unref(e); + if (rc != 0) + break; + } + + return 0; +} + +int CEis::onEvent(eis_event* e) { + eis_client* eisClient = nullptr; + eis_seat* seat = nullptr; + eis_device* device = nullptr; + + switch (eis_event_get_type(e)) { + case EIS_EVENT_CLIENT_CONNECT: + eisClient = eis_event_get_client(e); + Log::logger->log(Log::INFO, "[EIS] {} client connected: {}", eis_client_is_sender(eisClient) ? "Sender" : "Receiver", eis_client_get_name(eisClient)); + + if (eis_client_is_sender(eisClient)) { + Log::logger->log(Log::WARN, "[EIS] Unexpected sender client {} connected to input capture session", eis_client_get_name(eisClient)); + eis_client_disconnect(eisClient); + return 0; + } + + if (m_client.m_handle) { + Log::logger->log(Log::WARN, "[EIS] Unexpected additional client {} connected to input capture session", eis_client_get_name(eisClient)); + eis_client_disconnect(eisClient); + return 0; + } + + m_client.m_handle = eisClient; + + eis_client_connect(eisClient); + Log::logger->log(Log::INFO, "[EIS] Creating new default seat"); + seat = eis_client_new_seat(eisClient, "default"); + + eis_seat_configure_capability(seat, EIS_DEVICE_CAP_POINTER); + eis_seat_configure_capability(seat, EIS_DEVICE_CAP_BUTTON); + eis_seat_configure_capability(seat, EIS_DEVICE_CAP_SCROLL); + eis_seat_configure_capability(seat, EIS_DEVICE_CAP_KEYBOARD); + eis_seat_add(seat); + m_client.m_seat = seat; + break; + case EIS_EVENT_CLIENT_DISCONNECT: + eisClient = eis_event_get_client(e); + Log::logger->log(Log::INFO, "[EIS] {} disconnected", eis_client_get_name(eisClient)); + eis_client_disconnect(eisClient); + + eis_seat_unref(m_client.m_seat); + clearPointer(); + clearKeyboard(); + m_client.m_handle = nullptr; + break; + case EIS_EVENT_SEAT_BIND: + Log::logger->log(Log::INFO, "[EIS] Binding seats..."); + + if (eis_event_seat_has_capability(e, EIS_DEVICE_CAP_POINTER) && eis_event_seat_has_capability(e, EIS_DEVICE_CAP_BUTTON) && + eis_event_seat_has_capability(e, EIS_DEVICE_CAP_SCROLL)) + ensurePointer(); + else + clearPointer(); + + if (eis_event_seat_has_capability(e, EIS_DEVICE_CAP_KEYBOARD)) + ensureKeyboard(); + else + clearKeyboard(); + break; + case EIS_EVENT_DEVICE_CLOSED: + device = eis_event_get_device(e); + if (device == m_client.m_pointer) + clearPointer(); + else if (device == m_client.m_keyboard) { + clearKeyboard(); + } else + Log::logger->log(Log::WARN, "[EIS] Unknown device to close"); + break; + default: return 0; + } + return 0; +} + +void CEis::ensurePointer() { + if (m_client.m_pointer) + return; + if (!m_client.m_seat) + return; + + Log::logger->log(Log::INFO, "[EIS] Creating pointer"); + eis_device* pointer = eis_seat_new_device(m_client.m_seat); + eis_device_configure_name(pointer, "captured relative pointer"); + eis_device_configure_capability(pointer, EIS_DEVICE_CAP_POINTER); + eis_device_configure_capability(pointer, EIS_DEVICE_CAP_BUTTON); + eis_device_configure_capability(pointer, EIS_DEVICE_CAP_SCROLL); + + for (auto const& o : State::monitorState()->monitors()) { + eis_region* r = eis_device_new_region(pointer); + + eis_region_set_offset(r, o->m_position.x, o->m_position.y); + eis_region_set_size(r, o->m_pixelSize.x, o->m_pixelSize.y); + eis_region_set_physical_scale(r, o->m_scale); + eis_region_add(r); + eis_region_unref(r); + } + + eis_device_add(pointer); + eis_device_resume(pointer); + + m_client.m_pointer = pointer; +} + +void CEis::ensureKeyboard() { + if (m_client.m_keyboard) + return; + if (!m_client.m_seat) + return; + + Log::logger->log(Log::INFO, "[EIS] Creating keyboard"); + eis_device* keyboard = eis_seat_new_device(m_client.m_seat); + eis_device_configure_name(keyboard, "captured keyboard"); + eis_device_configure_capability(keyboard, EIS_DEVICE_CAP_KEYBOARD); + + SKeymap _keymap = getKeymap(); + if (_keymap.m_fd != -1) { + Log::logger->log(Log::INFO, "[EIS] Using keymap {}", _keymap.m_fd, _keymap.m_size); + eis_keymap* eis_keymap = eis_device_new_keymap(keyboard, EIS_KEYMAP_TYPE_XKB, _keymap.m_fd, _keymap.m_size); + if (eis_keymap) { + eis_keymap_add(eis_keymap); + eis_keymap_unref(eis_keymap); + } else { + Log::logger->log(Log::INFO, "[EIS] Cannot open keymap"); + } + } + + eis_device_add(keyboard); + eis_device_resume(keyboard); + + m_client.m_keyboard = keyboard; +} + +CEis::SKeymap CEis::getKeymap() { + SKeymap _keymap = {.m_fd = -1, .m_size = -1}; + + SP keyboard = g_pSeatManager->m_keyboard.lock(); + if (!keyboard) + return _keymap; + + int32_t size = keyboard->m_xkbKeymapString.length() + 1; + int fd = keyboard->m_xkbKeymapFD.get(); + _keymap.m_size = size; + _keymap.m_fd = fd; + return _keymap; +} + +void CEis::clearPointer() { + if (!m_client.m_pointer) + return; + Log::logger->log(Log::INFO, "[EIS] Clearing pointer"); + + eis_device_remove(m_client.m_pointer); + eis_device_unref(m_client.m_pointer); + m_client.m_pointer = nullptr; +} + +void CEis::clearKeyboard() { + if (!m_client.m_keyboard) + return; + Log::logger->log(Log::INFO, "[EIS] Clearing keyboard"); + + eis_device_remove(m_client.m_keyboard); + eis_device_unref(m_client.m_keyboard); + m_client.m_keyboard = nullptr; +} + +int CEis::getFileDescriptor() { + if (!m_eisCtx) + return -1; + + return eis_backend_fd_add_client(m_eisCtx); +} + +void CEis::startEmulating(int sequence) { + Log::logger->log(Log::INFO, "[EIS] Start Emulating"); + + if (m_client.m_pointer) + eis_device_start_emulating(m_client.m_pointer, sequence); + + if (m_client.m_keyboard) + eis_device_start_emulating(m_client.m_keyboard, sequence); +} + +void CEis::stopEmulating() { + Log::logger->log(Log::INFO, "[EIS] Stop Emulating"); + + if (m_client.m_pointer) + eis_device_stop_emulating(m_client.m_pointer); + + if (m_client.m_keyboard) + eis_device_stop_emulating(m_client.m_keyboard); +} + +void CEis::resetKeyboard() { + if (!m_client.m_keyboard) //We don't re-create the keyboard if it doesn't exist + return; + clearKeyboard(); + ensureKeyboard(); +} + +void CEis::resetPointer() { + if (!m_client.m_pointer) //We don't re-create the pointer if it doesn't exist + return; + clearPointer(); + ensurePointer(); +} + +void CEis::sendMotion(double x, double y) { + if (!m_client.m_pointer) + return; + eis_device_pointer_motion(m_client.m_pointer, x, y); +} + +void CEis::sendKey(uint32_t key, bool pressed) { + if (!m_client.m_keyboard) + return; + uint64_t now = eis_now(m_eisCtx); + eis_device_keyboard_key(m_client.m_keyboard, key, pressed); + eis_device_frame(m_client.m_keyboard, now); +} + +void CEis::sendModifiers(uint32_t modsDepressed, uint32_t modsLatched, uint32_t modsLocked, uint32_t group) { + if (!m_client.m_keyboard) + return; + uint64_t now = eis_now(m_eisCtx); + eis_device_keyboard_send_xkb_modifiers(m_client.m_keyboard, modsDepressed, modsLatched, modsLocked, group); + eis_device_frame(m_client.m_keyboard, now); +} + +void CEis::sendButton(uint32_t button, bool pressed) { + if (!m_client.m_pointer) + return; + eis_device_button_button(m_client.m_pointer, button, pressed); +} + +void CEis::sendScrollDiscrete(int32_t x, int32_t y) { + if (!m_client.m_pointer) + return; + eis_device_scroll_discrete(m_client.m_pointer, x, y); +} + +void CEis::sendScrollDelta(double x, double y) { + if (!m_client.m_pointer) + return; + eis_device_scroll_delta(m_client.m_pointer, x, y); +} + +void CEis::sendScrollStop(bool x, bool y) { + if (!m_client.m_pointer) + return; + eis_device_scroll_stop(m_client.m_pointer, x, y); +} + +void CEis::sendPointerFrame() { + if (!m_client.m_pointer) + return; + uint64_t now = eis_now(m_eisCtx); + eis_device_frame(m_client.m_pointer, now); +} diff --git a/src/managers/input/Eis.hpp b/src/managers/input/Eis.hpp new file mode 100644 index 000000000..5832e81e2 --- /dev/null +++ b/src/managers/input/Eis.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include +#include +#include + +/* + * Responsible to creating a socket for input communication + */ +class CEis { + public: + CEis(std::string socketPath); + ~CEis(); + + void startEmulating(int activationId); + void stopEmulating(); + + void resetKeyboard(); + void resetPointer(); + + void sendMotion(double x, double y); + void sendKey(uint32_t key, bool pressed); + void sendModifiers(uint32_t modsDepressed, uint32_t modsLatched, uint32_t modsLocked, uint32_t group); + void sendButton(uint32_t button, bool pressed); + void sendScrollDelta(double x, double y); + void sendScrollDiscrete(int32_t x, int32_t y); + void sendScrollStop(bool stopX, bool stopY); + void sendPointerFrame(); + + int getFileDescriptor(); + + // + + std::string m_socketPath; + + private: + struct SKeymap { + int32_t m_fd = 0; + uint32_t m_size = 0; + }; + + struct SClient { + eis_client* m_handle = nullptr; + eis_seat* m_seat = nullptr; + + eis_device* m_pointer = nullptr; + eis_device* m_keyboard = nullptr; + } m_client; + + int onEvent(eis_event* e); + int pollEvents(); + void ensurePointer(); + void ensureKeyboard(); + SKeymap getKeymap(); + void clearPointer(); + void clearKeyboard(); + + bool m_stop = false; + eis* m_eisCtx = nullptr; + + wl_event_source* m_eventSource = nullptr; +}; diff --git a/src/managers/input/InputManager.cpp b/src/managers/input/InputManager.cpp index 0ae09bb8d..b08d5daa3 100644 --- a/src/managers/input/InputManager.cpp +++ b/src/managers/input/InputManager.cpp @@ -27,6 +27,7 @@ #include "../../protocols/core/DataDevice.hpp" #include "../../protocols/core/Compositor.hpp" #include "../../protocols/XDGShell.hpp" +#include "../../protocols/InputCapture.hpp" #include "../../devices/Mouse.hpp" #include "../../devices/VirtualPointer.hpp" @@ -153,6 +154,9 @@ void CInputManager::onMouseMoved(IPointer::SMotionEvent e) { PROTO::relativePointer->sendRelativeMotion(sc(e.timeMs) * 1000, delta, unaccel); Pointer::mgr()->move(DELTA); + if (PROTO::inputCapture->isCaptured()) + return; + mouseMoveUnified(e.timeMs, false, e.mouse); m_lastCursorMovement.reset(); @@ -744,6 +748,11 @@ void CInputManager::onMouseButton(IPointer::SButtonEvent e, SP mouse) if (e.mouse) recheckMouseWarpOnMouseInput(); + PROTO::inputCapture->button(e.button, e.state); + + if (PROTO::inputCapture->isCaptured()) + return; + m_lastCursorMovement.reset(); if (e.state == WL_POINTER_BUTTON_STATE_PRESSED) { @@ -976,7 +985,14 @@ void CInputManager::onMouseWheel(IPointer::SAxisEvent e, SP pointer) { if (e.mouse) recheckMouseWarpOnMouseInput(); - bool passEvent = g_pKeybindManager->onAxisEvent(e, pointer); + PROTO::inputCapture->axis(e.axis, e.delta); + if (e.source == 0) + PROTO::inputCapture->axisValue120(e.axis, e.delta); + else if (e.delta == 0) + PROTO::inputCapture->axisStop(e.axis); + PROTO::inputCapture->frame(); + + bool passEvent = !PROTO::inputCapture->isCaptured() && g_pKeybindManager->onAxisEvent(e, pointer); if (!passEvent) return; @@ -1070,6 +1086,11 @@ void CInputManager::onMouseWheel(IPointer::SAxisEvent e, SP pointer) { } void CInputManager::onPointerFrame() { + PROTO::inputCapture->frame(); + + if (PROTO::inputCapture->isCaptured()) + return; + if (!m_pointerAxisFramePending) return; @@ -1604,10 +1625,12 @@ void CInputManager::onKeyboardKey(const IKeyboard::SKeyEvent& event, SPkey(event.keycode, event.state); + + bool passEvent = DISALLOWACTION && !PROTO::inputCapture->isCaptured(); if (!DISALLOWACTION) - passEvent = g_pKeybindManager->onKeyEvent(event, pKeyboard); + passEvent = g_pKeybindManager->onKeyEvent(event, pKeyboard) && !PROTO::inputCapture->isCaptured(); if (passEvent) { auto state = event.state; @@ -1654,6 +1677,7 @@ void CInputManager::onKeyboardKey(const IKeyboard::SKeyEvent& event, SP pKeyboard) { + static auto PSENDMOD = CConfigValue("input-capture:capture_modifiers"); if (!pKeyboard->m_enabled) return; @@ -1662,8 +1686,14 @@ void CInputManager::onKeyboardMod(SP pKeyboard) { const auto IME = m_relay.m_inputMethod.lock(); const bool HASIME = IME && IME->hasGrab(); const bool USEIME = HASIME && !DISALLOWACTION; + auto MODS = pKeyboard->m_modifiersState; - auto MODS = pKeyboard->m_modifiersState; + if (*PSENDMOD) { + PROTO::inputCapture->modifiers(MODS.depressed, MODS.latched, MODS.locked, MODS.group); + + if (PROTO::inputCapture->isCaptured()) + return; + } // use merged mods states when sending to ime or when sending to seat with no ime // if passing from ime, send mods directly without merging diff --git a/src/managers/input/InputManager.hpp b/src/managers/input/InputManager.hpp index 52bbd5cca..9c32988a6 100644 --- a/src/managers/input/InputManager.hpp +++ b/src/managers/input/InputManager.hpp @@ -94,6 +94,7 @@ class CInputManager { void onMouseWheel(IPointer::SAxisEvent, SP pointer = nullptr); void onPointerFrame(); void onKeyboardKey(const IKeyboard::SKeyEvent&, SP); + void onMouseFrame(); void onKeyboardMod(SP); void newKeyboard(SP); diff --git a/src/managers/permissions/DynamicPermissionManager.cpp b/src/managers/permissions/DynamicPermissionManager.cpp index 4f299db12..0f9283782 100644 --- a/src/managers/permissions/DynamicPermissionManager.cpp +++ b/src/managers/permissions/DynamicPermissionManager.cpp @@ -54,6 +54,7 @@ static const char* permissionToString(eDynamicPermissionType type) { case PERMISSION_TYPE_PLUGIN: return "PERMISSION_TYPE_PLUGIN"; case PERMISSION_TYPE_KEYBOARD: return "PERMISSION_TYPE_KEYBOARD"; case PERMISSION_TYPE_CURSOR_POS: return "PERMISSION_TYPE_CURSOR_POS"; + case PERMISSION_TYPE_INPUT_CAPTURE: return "PERMISSION_TYPE_INPUT_CAPTURE"; } return "error"; @@ -256,6 +257,7 @@ void CDynamicPermissionManager::askForPermission(wl_client* client, const std::s case PERMISSION_TYPE_PLUGIN: description = I18n::i18nEngine()->localize(I18n::TXT_KEY_PERMISSION_REQUEST_PLUGIN, {{"app", appName}, {"plugin", binaryPath}}); break; case PERMISSION_TYPE_KEYBOARD: description = I18n::i18nEngine()->localize(I18n::TXT_KEY_PERMISSION_REQUEST_KEYBOARD, {{"keyboard", binaryPath}}); break; case PERMISSION_TYPE_UNKNOWN: description = I18n::i18nEngine()->localize(I18n::TXT_KEY_PERMISSION_REQUEST_UNKNOWN, {{"app", appName}}); break; + case PERMISSION_TYPE_INPUT_CAPTURE: description = I18n::i18nEngine()->localize(I18n::TXT_KEY_PERMISSION_REQUEST_INPUT_CAPTURE, {{"app", appName}}); break; } std::vector options; diff --git a/src/managers/permissions/DynamicPermissionManager.hpp b/src/managers/permissions/DynamicPermissionManager.hpp index 423596c39..1d676c4c2 100644 --- a/src/managers/permissions/DynamicPermissionManager.hpp +++ b/src/managers/permissions/DynamicPermissionManager.hpp @@ -19,6 +19,7 @@ enum eDynamicPermissionType : uint8_t { PERMISSION_TYPE_PLUGIN, PERMISSION_TYPE_KEYBOARD, PERMISSION_TYPE_CURSOR_POS, + PERMISSION_TYPE_INPUT_CAPTURE, }; enum eDynamicPermissionRuleSource : uint8_t { diff --git a/src/pointer/PointerManager.cpp b/src/pointer/PointerManager.cpp index ced8d90fa..d907f42d6 100644 --- a/src/pointer/PointerManager.cpp +++ b/src/pointer/PointerManager.cpp @@ -8,6 +8,7 @@ #include "../protocols/IdleNotify.hpp" #include "../protocols/core/Compositor.hpp" #include "../protocols/core/Seat.hpp" +#include "../protocols/InputCapture.hpp" #include "debug/log/Logger.hpp" #include "../managers/eventLoop/EventLoopManager.hpp" #include "../render/pass/ClearPassElement.hpp" @@ -831,6 +832,12 @@ void CPointerManager::move(const Vector2D& deltaLogical) { const auto oldPos = m_pointerPos; auto newPos = oldPos + Vector2D{std::isnan(deltaLogical.x) ? 0.0 : deltaLogical.x, std::isnan(deltaLogical.y) ? 0.0 : deltaLogical.y}; + if (!g_pInputManager->isLocked()) + PROTO::inputCapture->motion(newPos, deltaLogical); + + if (PROTO::inputCapture->isCaptured()) + return; + warpTo(newPos); } diff --git a/src/protocols/InputCapture.cpp b/src/protocols/InputCapture.cpp new file mode 100644 index 000000000..fe9b31159 --- /dev/null +++ b/src/protocols/InputCapture.cpp @@ -0,0 +1,496 @@ +#include "InputCapture.hpp" + +#include "Compositor.hpp" +#include "config/ConfigValue.hpp" +#include "debug/log/Logger.hpp" +#include "devices/IKeyboard.hpp" +#include "event/EventBus.hpp" +#include "hyprland-input-capture-v1.hpp" +#include "pointer/PointerManager.hpp" +#include "managers/SeatManager.hpp" +#include "managers/eventLoop/EventLoopManager.hpp" +#include "managers/permissions/DynamicPermissionManager.hpp" +#include "protocols/WaylandProtocol.hpp" +#include "render/Renderer.hpp" +#include "state/MonitorState.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static int eisCounter = 0; + +CInputCaptureResource::CInputCaptureResource(SP resource_, std::string handle) : m_sessionId(handle), m_resource(resource_) { + if UNLIKELY (!good()) + return; + Log::logger->log(Log::INFO, "[input-capture]({}) new session", m_sessionId.c_str()); + + m_resource->setOnDestroy([this](CHyprlandInputCaptureV1* r) { PROTO::inputCapture->destroyResource(this); }); //Remove & free this session + + m_resource->setEnable([this](CHyprlandInputCaptureV1* r) { onEnable(); }); + m_resource->setAddBarrier( + [this](CHyprlandInputCaptureV1* r, uint32_t zoneSet, uint32_t id, uint32_t x1, uint32_t y1, uint32_t x2, uint32_t y2) { onAddBarrier(zoneSet, id, x1, y1, x2, y2); }); + m_resource->setDisable([this](CHyprlandInputCaptureV1* r) { disable(); }); + m_resource->setRelease([this](CHyprlandInputCaptureV1* r, uint32_t activationId, double x, double y) { onRelease(activationId, x, y); }); + m_resource->setClearBarriers([this](CHyprlandInputCaptureV1* r) { onClearBarriers(); }); + + m_eis = makeUnique("eis-" + std::to_string(eisCounter++)); + + const int EISFD = m_eis->getFileDescriptor(); + if (EISFD >= 0) + m_resource->sendEisFd(EISFD); + else + Log::logger->log(Log::ERR, "[input-capture]({}) failed to create EIS client fd", m_sessionId.c_str()); + + m_keyRepeatTimer = makeShared( + std::nullopt, + [this](SP self, void* data) { + if (!m_keyRepeat.active || m_status != CLIENT_STATUS_ACTIVATED || !m_eis) + return; + + m_eis->sendKey(m_keyRepeat.key, false); + m_eis->sendKey(m_keyRepeat.key, true); + + if (m_keyRepeat.rate > 0) + self->updateTimeout(std::chrono::milliseconds(1000 / m_keyRepeat.rate)); + }, + nullptr); + + if (g_pEventLoopManager) + g_pEventLoopManager->addTimer(m_keyRepeatTimer); + + m_monitorCallback = Event::bus()->m_events.monitor.layoutChanged.listen([this] { + onClearBarriers(); + disable(); + m_eis->resetPointer(); + }); +} + +CInputCaptureResource::~CInputCaptureResource() { + stopKeyRepeat(); + + if (m_keyRepeatTimer && g_pEventLoopManager) { + g_pEventLoopManager->removeTimer(m_keyRepeatTimer); + m_keyRepeatTimer.reset(); + } + + if (m_status == CLIENT_STATUS_ACTIVATED) + PROTO::inputCapture->forceRelease(); + + Log::logger->log(Log::INFO, "[input-capture]({}) session destroyed", m_sessionId.c_str()); + PROTO::inputCapture->clearBarriers(m_sessionId); +}; + +bool CInputCaptureResource::enabled() { + return m_status == CLIENT_STATUS_ENABLED; +} + +bool CInputCaptureResource::good() { + return m_resource->resource(); +} + +void CInputCaptureResource::onEnable() { + Log::logger->log(Log::INFO, "[input-capture]({}) session enabled", m_sessionId.c_str()); + m_status = CLIENT_STATUS_ENABLED; +} + +enum eValidResult : uint8_t { + INVALID, + PARTIAL, + VALID +}; + +static eValidResult isBarrierValidAgainstMonitor(int x1, int y1, int x2, int y2, PHLMONITOR monitor) { + int mx1 = monitor->m_position.x; + int my1 = monitor->m_position.y; + + const int width = static_cast(monitor->m_size.x); + const int height = static_cast(monitor->m_size.y); + + int mx2 = mx1 + width - 1; + int my2 = my1 + height - 1; + + if (x1 == x2) { //If zone is vertical + if (x1 != mx1 && x1 != mx2 + 1) //If the zone don't touch the left or right side + return INVALID; + + if (y1 != my1 || y2 != my2) { //If the zone is shorter than the height of the screen + if ((my1 <= y1 && y1 <= my2) || (my1 <= y2 && y2 <= my2)) //Maybe the segments are overlapping + return PARTIAL; + return INVALID; + } + } else { + if (y1 != my1 && y1 != my2 + 1) //If the zone don't touch the bottom or top side + return INVALID; + + if (x1 != mx1 || x2 != mx2) { //If the zone is shorter than the height of the screen + if ((mx1 <= x1 && x1 <= mx2) || (mx1 <= x2 && x2 <= mx2)) //Maybe the segments are overlapping + return PARTIAL; + return INVALID; + } + } + + return VALID; +} + +static bool isBarrierValid(int x1, int y1, int x2, int y2) { + if (x1 != x2 && y1 != y2) //At least one axis should be aligned + return false; + + if (x1 == x2 && y1 == y2) //The barrier should have non-null area + return false; + + if (x1 > x2) + std::swap(x1, x2); + + if (y1 > y2) + std::swap(y1, y2); + + int valid = 0; + int partial = 0; //Used to detect if a barrier is placed on the side of two monitors + for (auto& o : State::monitorState()->monitors()) { + switch (isBarrierValidAgainstMonitor(x1, y1, x2, y2, o)) { + case VALID: valid++; break; + case PARTIAL: partial++; break; + case INVALID: break; + } + } + + return valid == 1 && partial == 0; +} + +void CInputCaptureResource::onAddBarrier(uint32_t zoneSet, uint32_t id, uint32_t x1, uint32_t y1, uint32_t x2, uint32_t y2) { + static auto PENFORCEBARRIERS = CConfigValue("input-capture:enforce_barriers"); + // Protocol coordinates are transported as uint32_t. Negative global-space + // coordinates are encoded via two's complement and must be reinterpreted. + const int32_t sx1 = static_cast(x1); + const int32_t sy1 = static_cast(y1); + const int32_t sx2 = static_cast(x2); + const int32_t sy2 = static_cast(y2); + + bool valid = isBarrierValid(sx1, sy1, sx2, sy2); + + if (!valid) { + Log::logger->log(Log::INFO, "[input-capture]({}) Barrier {} is invalid [{}, {}], [{}, {}]", m_sessionId.c_str(), id, sx1, sy1, sx2, sy2); + + if (*PENFORCEBARRIERS) { + m_resource->error(HYPRLAND_INPUT_CAPTURE_V1_ERROR_INVALID_BARRIER, "The barrier id " + std::to_string(id) + " is invalid"); + return; + } + } + + Log::logger->log(Log::INFO, "[input-capture]({}) Barrier {} [{}, {}], [{}, {}] added", m_sessionId.c_str(), id, sx1, sy1, sx2, sy2); + + PROTO::inputCapture->addBarrier({.sessionId = m_sessionId, .id = id, .x1 = sx1, .y1 = sy1, .x2 = sx2, .y2 = sy2}); +} + +void CInputCaptureResource::onDisable() { + disable(); +} + +void CInputCaptureResource::onRelease(uint32_t activationId_, double x, double y) { + if (activationId_ != m_activationId) { // If id is invalid we still want to release the mouse to avoid any issue + Log::logger->log(Log::WARN, "[input-capture]({}) Invalid activation id {} expected {}", m_sessionId.c_str(), activationId_, m_activationId); + } + + deactivate(); + + if (x != -1 && y != -1) + Pointer::mgr()->warpTo({x, y}); + g_pHyprRenderer->ensureCursorRenderingMode(); +} + +void CInputCaptureResource::onClearBarriers() { + PROTO::inputCapture->clearBarriers(m_sessionId); +} + +bool CInputCaptureResource::activate(double x, double y, uint32_t borderId) { + if (m_status != CLIENT_STATUS_ENABLED) + return false; + + const auto PERM = g_pDynamicPermissionManager->clientPermissionMode(m_resource->client(), PERMISSION_TYPE_INPUT_CAPTURE); + if (PERM != PERMISSION_RULE_ALLOW_MODE_ALLOW) + return false; + + m_activationId += 5; + m_status = CLIENT_STATUS_ACTIVATED; + Log::logger->log(Log::INFO, "[input-capture]({}) Input captured, activationId: {}, borderId: {}, x: {}, y: {}", m_sessionId.c_str(), m_activationId, borderId, x, y); + m_eis->startEmulating(m_activationId); + g_pHyprRenderer->ensureCursorRenderingMode(); + m_resource->sendActivated(m_activationId, x, y, borderId); + + return true; +} + +void CInputCaptureResource::deactivate() { + if (m_status != CLIENT_STATUS_ACTIVATED) + return; + + Log::logger->log(Log::INFO, "[input-capture]({}) Input released", m_sessionId.c_str()); + stopKeyRepeat(); + m_status = CLIENT_STATUS_ENABLED; + m_eis->stopEmulating(); + PROTO::inputCapture->release(); + + m_resource->sendDeactivated(m_activationId); +} + +void CInputCaptureResource::disable() { + stopKeyRepeat(); + + if (m_status == CLIENT_STATUS_ACTIVATED) + deactivate(); + if (m_status != CLIENT_STATUS_ENABLED) + return; + m_status = CLIENT_STATUS_CREATED; + + m_resource->sendDisabled(); +} + +void CInputCaptureResource::motion(double dx, double dy) { + m_eis->sendMotion(dx, dy); +} + +void CInputCaptureResource::key(uint32_t key, bool pressed) { + m_eis->sendKey(key, pressed); + + if (pressed) + startKeyRepeat(key); + else if (m_keyRepeat.active && m_keyRepeat.key == key) + stopKeyRepeat(); +} + +void CInputCaptureResource::modifiers(uint32_t modsDepressed, uint32_t modsLatched, uint32_t modsLocked, uint32_t group) { + m_eis->sendModifiers(modsDepressed, modsLatched, modsLocked, group); +} + +void CInputCaptureResource::button(uint32_t button, bool pressed) { + m_eis->sendButton(button, pressed); +} + +void CInputCaptureResource::axis(bool axis, double value) { + int32_t x = 0; + int32_t y = 0; + + if (axis) + x = value; + else + y = value; + + m_eis->sendScrollDelta(x, y); +} + +void CInputCaptureResource::axisValue120(bool axis, int32_t value120) { + int32_t x = 0; + int32_t y = 0; + + if (axis) + x = value120; + else + y = value120; + + m_eis->sendScrollDiscrete(x, y); +} + +void CInputCaptureResource::axisStop(bool axis) { + m_eis->sendScrollStop(axis, !axis); +} + +void CInputCaptureResource::frame() { + m_eis->sendPointerFrame(); +} + +static bool isLineIntersect(double p0X, double p0Y, double p1X, double p1Y, double p2X, double p2Y, double p3X, double p3Y) { + float s1X = p1X - p0X; + float s1Y = p1Y - p0Y; + float s2X = p3X - p2X; + float s2Y = p3Y - p2Y; + float s = (-s1Y * (p0X - p2X) + s1X * (p0Y - p2Y)) / (-s2X * s1Y + s1X * s2Y); + float t = (s2X * (p0Y - p2Y) - s2Y * (p0X - p2X)) / (-s2X * s1Y + s1X * s2Y); + + return s >= 0 && s <= 1 && t >= 0 && t <= 1; +} + +std::optional CInputCaptureProtocol::isColliding(double px, double py, double nx, double ny) { + for (const auto& barrier : barriers) { + auto session = getSession(barrier.sessionId); + if (!session.has_value()) + continue; + if (!session.value()->enabled()) + continue; + + if (isLineIntersect(barrier.x1, barrier.y1, barrier.x2, barrier.y2, px, py, nx, ny)) + return std::optional(barrier); + } + + return std::nullopt; +} + +CInputCaptureProtocol::CInputCaptureProtocol(const wl_interface* iface, const int& ver, const std::string& name) : IWaylandProtocol(iface, ver, name) { + ; +} + +void CInputCaptureProtocol::bindManager(wl_client* client, void* data, uint32_t ver, uint32_t id) { + const auto& RESOURCE = m_vManagers.emplace_back(makeUnique(client, ver, id)); + + RESOURCE->setOnDestroy([this](CHyprlandInputCaptureManagerV1* p) { std::erase_if(m_vManagers, [&](const auto& other) { return other->resource() == p->resource(); }); }); + + RESOURCE->setCreateSession([this](CHyprlandInputCaptureManagerV1* pMgr, uint32_t id, std::string handle) { onCreateSession(pMgr, id, handle); }); +} + +void CInputCaptureProtocol::onCreateSession(CHyprlandInputCaptureManagerV1* pMgr, uint32_t id, std::string handle) { + const auto RESOURCE = m_Sessions.emplace_back(makeShared(makeShared(pMgr->client(), pMgr->version(), id), handle)); + + if UNLIKELY (!RESOURCE->good()) { + pMgr->noMemory(); + m_Sessions.pop_back(); + return; + } + + Log::logger->log(Log::INFO, "New InputCapture at id {}", id); +} + +void CInputCaptureProtocol::destroyResource(CInputCaptureResource* resource) { + std::erase_if(m_Sessions, [resource](const auto& other) { return other->m_sessionId == resource->m_sessionId; }); +} + +bool CInputCaptureProtocol::isCaptured() { + return active != nullptr; +} + +void CInputCaptureProtocol::updateKeymap() { + for (auto& session : m_Sessions) { + session->updateKeymap(); + } +} + +void CInputCaptureProtocol::motion(const Vector2D& absolutePosition, const Vector2D& delta) { + auto [x, y] = absolutePosition; + auto [dx, dy] = delta; + + auto matched = isColliding(x, y, x - dx, y - dy); + if (matched.has_value()) { + auto session = getSession(matched->sessionId); + if (!session.has_value()) { + Log::logger->log(Log::ERR, "Cannot find session {} for triggering barrier {}", matched.value().id, matched.value().sessionId); + return; + } + if (session.value()->activate(x, y, matched.value().id)) + active = session.value(); + } + + if (active) + active->motion(dx, dy); +} + +std::optional> CInputCaptureProtocol::getSession(std::string sessionId) { + auto result = std::ranges::find_if(m_Sessions, [sessionId](SP elem) { return sessionId == elem->m_sessionId; }); + if (result == m_Sessions.end()) + return std::nullopt; + + return std::optional(*result); +} + +void CInputCaptureProtocol::addBarrier(SBarrier barrier) { + barriers.emplace_back(barrier); +} + +void CInputCaptureProtocol::clearBarriers(std::string sessionId) { + std::erase_if(barriers, [sessionId](SBarrier b) { return b.sessionId == sessionId; }); + Log::logger->log(Log::INFO, "[input-capture]({}) Barriers cleared", sessionId.c_str()); +} + +void CInputCaptureProtocol::release() { + active = nullptr; +} + +void CInputCaptureProtocol::forceRelease() { + Log::logger->log(Log::INFO, "[input-capture] Force release input capture"); + if (active) { + auto cpy = active; //Because deactivate will put active to nullptr + cpy->deactivate(); + cpy->disable(); + } + release(); +} + +void CInputCaptureProtocol::key(uint32_t keyCode, wl_keyboard_key_state state) { + if (active) + active->key(keyCode, state); +} + +void CInputCaptureProtocol::modifiers(uint32_t mods_depressed, uint32_t mods_latched, uint32_t mods_locked, uint32_t group) { + if (active) + active->modifiers(mods_depressed, mods_latched, mods_locked, group); +} + +void CInputCaptureProtocol::button(uint32_t button, wl_pointer_button_state state) { + if (active) + active->button(button, state); +} + +void CInputCaptureProtocol::axis(wl_pointer_axis axis, double value) { + if (active) + active->axis(axis, value); +} + +void CInputCaptureProtocol::axisValue120(wl_pointer_axis axis, int32_t value120) { + if (active) + active->axisValue120(axis, value120); +} + +void CInputCaptureProtocol::axisStop(wl_pointer_axis axis) { + if (active) + active->axisStop(axis); +} + +void CInputCaptureProtocol::frame() { + if (active) + active->frame(); +} + +void CInputCaptureResource::updateKeymap() { + Log::logger->log(Log::INFO, "[input-capture] Got new keymap, reseting keyboard"); + stopKeyRepeat(); + m_eis->resetKeyboard(); +} + +bool CInputCaptureResource::keyRepeats(uint32_t key) { + const auto KEYBOARD = g_pSeatManager->m_keyboard.lock(); + if (!KEYBOARD || !KEYBOARD->m_xkbKeymap || KEYBOARD->m_repeatRate <= 0) + return false; + + return xkb_keymap_key_repeats(KEYBOARD->m_xkbKeymap, key + 8); +} + +void CInputCaptureResource::startKeyRepeat(uint32_t key) { + const auto KEYBOARD = g_pSeatManager->m_keyboard.lock(); + if (!KEYBOARD || KEYBOARD->m_repeatRate <= 0) { + stopKeyRepeat(); + return; + } + + if (!keyRepeats(key)) + return; + + m_keyRepeat.active = true; + m_keyRepeat.key = key; + m_keyRepeat.rate = KEYBOARD->m_repeatRate; + + if (m_keyRepeatTimer) + m_keyRepeatTimer->updateTimeout(std::chrono::milliseconds(std::max(0, KEYBOARD->m_repeatDelay))); +} + +void CInputCaptureResource::stopKeyRepeat() { + m_keyRepeat.active = false; + + if (m_keyRepeatTimer) + m_keyRepeatTimer->updateTimeout(std::nullopt); +} diff --git a/src/protocols/InputCapture.hpp b/src/protocols/InputCapture.hpp new file mode 100644 index 000000000..aed15d5d2 --- /dev/null +++ b/src/protocols/InputCapture.hpp @@ -0,0 +1,122 @@ +#pragma once + +#include "managers/input/Eis.hpp" +#include "helpers/memory/Memory.hpp" +#include "helpers/signal/Signal.hpp" +#include "hyprland-input-capture-v1.hpp" +#include "../protocols/WaylandProtocol.hpp" +#include +#include +#include +#include +#include + +class CEventLoopTimer; + +class CInputCaptureResource { + public: + CInputCaptureResource(SP resource_, std::string handle); + ~CInputCaptureResource(); + + void motion(double dx, double dy); + void key(uint32_t key, bool pressed); + void modifiers(uint32_t modsDepressed, uint32_t modsLatched, uint32_t modsLocked, uint32_t group); + void button(uint32_t button, bool pressed); + void axis(bool axis, double value); + void axisValue120(bool axis, int32_t value120); + void axisStop(bool axis); + void frame(); + void updateKeymap(); + + bool activate(double x, double y, uint32_t borderId); + void disable(); + void deactivate(); + + bool enabled(); + bool good(); + + // + std::string m_sessionId; + + private: + enum eClientStatus : uint8_t { + CLIENT_STATUS_CREATED, //Is ready to be activated + CLIENT_STATUS_ENABLED, //Is ready for receiving inputs + CLIENT_STATUS_ACTIVATED, //Currently receiving inputs + CLIENT_STATUS_STOPPED //Can no longer be activated + }; + + void onEnable(); + void onAddBarrier(uint32_t zoneSet, uint32_t id, uint32_t x1, uint32_t y1, uint32_t x2, uint32_t y2); + void onDisable(); + void onRelease(uint32_t activationId, double x, double y); + void onClearBarriers(); + void startKeyRepeat(uint32_t key); + void stopKeyRepeat(); + bool keyRepeats(uint32_t key); + + // + SP m_resource; + UP m_eis; + SP m_keyRepeatTimer; + + uint32_t m_activationId = 0; + eClientStatus m_status = eClientStatus::CLIENT_STATUS_CREATED; + CHyprSignalListener m_monitorCallback; + + struct { + bool active = false; + uint32_t key = 0; + int rate = 0; + } m_keyRepeat; +}; + +class CInputCaptureProtocol : public IWaylandProtocol { + public: + struct SBarrier { + std::string sessionId = ""; + uint id = 0; + int x1 = 0; + int y1 = 0; + int x2 = 0; + int y2 = 0; + }; + + CInputCaptureProtocol(const wl_interface* iface, const int& ver, const std::string& name); + void bindManager(wl_client* client, void* data, uint32_t ver, uint32_t id); + void destroyResource(CInputCaptureResource* pointer); + + void addBarrier(SBarrier barrier); + void clearBarriers(std::string sessionId); + + void updateKeymap(); + + bool isCaptured(); + + void motion(const Vector2D& absolutePosition, const Vector2D& delta); + void key(uint32_t keyCode, wl_keyboard_key_state state); + void modifiers(uint32_t mods_depressed, uint32_t mods_latched, uint32_t mods_locked, uint32_t group); + void button(uint32_t button, wl_pointer_button_state state); + void axis(wl_pointer_axis axis, double value); + void axisValue120(wl_pointer_axis axis, int32_t value120); + void axisStop(wl_pointer_axis axis); + void frame(); + void forceRelease(); + + void release(); + + private: + std::vector> m_vManagers; + std::vector> m_Sessions; + SP active = nullptr; + std::vector barriers; + + // + void onCreateSession(CHyprlandInputCaptureManagerV1* pMgr, uint32_t id, std::string handle); + std::optional isColliding(double px, double py, double nx, double ny); + std::optional> getSession(std::string sessionId); +}; + +namespace PROTO { + inline UP inputCapture; +} diff --git a/src/protocols/core/Output.cpp b/src/protocols/core/Output.cpp index 18b2e99d0..4039cc656 100644 --- a/src/protocols/core/Output.cpp +++ b/src/protocols/core/Output.cpp @@ -81,8 +81,8 @@ void CWLOutputResource::updateState() { m_resource->sendMode(WL_OUTPUT_MODE_CURRENT, m_monitor->m_pixelSize.x, m_monitor->m_pixelSize.y, m_monitor->m_refreshRate * 1000.0); - m_resource->sendGeometry(0, 0, m_monitor->m_output->physicalSize.x, m_monitor->m_output->physicalSize.y, m_monitor->m_output->subpixel, m_monitor->m_output->make.c_str(), - m_monitor->m_output->model.c_str(), m_monitor->m_transform); + m_resource->sendGeometry(m_monitor->m_position.x, m_monitor->m_position.y, m_monitor->m_output->physicalSize.x, m_monitor->m_output->physicalSize.y, + m_monitor->m_output->subpixel, m_monitor->m_output->make.c_str(), m_monitor->m_output->model.c_str(), m_monitor->m_transform); if (m_resource->version() >= 2) m_resource->sendDone(); diff --git a/src/render/Renderer.cpp b/src/render/Renderer.cpp index 3276f64f2..38dcd6a62 100644 --- a/src/render/Renderer.cpp +++ b/src/render/Renderer.cpp @@ -25,6 +25,7 @@ #include "../protocols/core/Compositor.hpp" #include "../protocols/DRMSyncobj.hpp" #include "../protocols/LinuxDMABUF.hpp" +#include "../protocols/InputCapture.hpp" #include "../errorOverlay/Overlay.hpp" #include "../debug/Overlay.hpp" #include "../notification/NotificationOverlay.hpp" @@ -2895,7 +2896,7 @@ void IHyprRenderer::ensureCursorRenderingMode() { m_cursorHiddenByCondition = m_cursorHiddenConditions.hiddenOnTimeout || m_cursorHiddenConditions.hiddenOnTouch || m_cursorHiddenConditions.hiddenOnTablet || m_cursorHiddenConditions.hiddenOnKeyboard; - const bool HIDE = m_cursorHiddenByCondition || (*PINVISIBLE != 0); + const bool HIDE = m_cursorHiddenByCondition || (*PINVISIBLE != 0) || PROTO::inputCapture->isCaptured(); if (HIDE == m_cursorHidden) return; diff --git a/subprojects/hyprland-protocols b/subprojects/hyprland-protocols index 3a5c2bda1..bd153e76f 160000 --- a/subprojects/hyprland-protocols +++ b/subprojects/hyprland-protocols @@ -1 +1 @@ -Subproject commit 3a5c2bda1c1a4e55cc1330c782547695a93f05b2 +Subproject commit bd153e76f751f150a09328dbdeb5e4fab9d23622 diff --git a/tests/config/lua/LuaObjectsBasic.cpp b/tests/config/lua/LuaObjectsBasic.cpp index 1b4a87426..939e5d356 100644 --- a/tests/config/lua/LuaObjectsBasic.cpp +++ b/tests/config/lua/LuaObjectsBasic.cpp @@ -108,30 +108,31 @@ TEST(ConfigLuaObjects, keybindExposesMetadataAndRemoveMethods) { Objects::CLuaKeybind{}.setup(L); - auto keybind = makeShared(); - keybind->enabled = true; - keybind->description = "Close active window"; - keybind->hasDescription = true; - keybind->displayKey = "SUPER + Q"; - keybind->submap.name = "default"; - keybind->handler = "exec"; - keybind->arg = "kitty"; - keybind->modmask = HL_MODIFIER_META; - keybind->key = "Q"; - keybind->keycode = 24; - keybind->repeat = true; - keybind->locked = true; - keybind->release = false; - keybind->nonConsuming = true; - keybind->transparent = false; - keybind->ignoreMods = false; - keybind->longPress = false; - keybind->dontInhibit = true; - keybind->click = false; - keybind->drag = false; - keybind->submapUniversal = false; - keybind->deviceInclusive = true; - keybind->devices = {"kbd-a", "kbd-b"}; + auto keybind = makeShared(); + keybind->enabled = true; + keybind->description = "Close active window"; + keybind->hasDescription = true; + keybind->displayKey = "SUPER + Q"; + keybind->submap.name = "default"; + keybind->handler = "exec"; + keybind->arg = "kitty"; + keybind->modmask = HL_MODIFIER_META; + keybind->key = "Q"; + keybind->keycode = 24; + keybind->repeat = true; + keybind->locked = true; + keybind->release = false; + keybind->nonConsuming = true; + keybind->transparent = false; + keybind->ignoreMods = false; + keybind->longPress = false; + keybind->dontInhibit = true; + keybind->click = false; + keybind->drag = false; + keybind->submapUniversal = false; + keybind->deviceInclusive = true; + keybind->devices = {"kbd-a", "kbd-b"}; + keybind->allowInputCapture = true; Objects::CLuaKeybind::push(L, keybind); lua_setglobal(L, "kb"); @@ -151,6 +152,7 @@ TEST(ConfigLuaObjects, keybindExposesMetadataAndRemoveMethods) { assert(kb.non_consuming == true) assert(kb.dont_inhibit == true) assert(type(kb.devices) == "table") + assert(kb.allow_input_capture == true) kb:remove() kb:unbind()