diff --git a/doc/user/lua-plugins.rst b/doc/user/lua-plugins.rst index b95fbdb8..0b3e4e7d 100644 --- a/doc/user/lua-plugins.rst +++ b/doc/user/lua-plugins.rst @@ -631,3 +631,22 @@ a device's capabilities before the device is processed by libinput. function is identical to ``prepend_frame()``. See ``prepend_frame()`` for more details. + +.. function:: EvdevDevice:disable_feature(feature_name) + + Disable the given libinput-internal feature for this device. This should be used + by plugins that replace that feature with a custom implementation for this device. + + libinput may have multiple internal implementations for any given feature, disabling + it via this API disables any and all of those implementations, causing the feature to + no longer work at all. It is up to the plugin implementation to re-implement that + feature to match the user's expectation. + + Version 1 of the plugin API supports the following features: + + - ``button-debouncing``: see :ref:`button_debouncing` + - ``touchpad-hysteresis``: see :ref:`touchpad_jitter` + - ``touchpad-jump-detection``: see :ref:`touchpad_jumping_cursor` + - ``touchpad-palm-detection``: see :ref:`palm_detection` + - ``wheel-debouncing``: some high-resolution mouse wheel movements inside libinput + are delayed and/or modified diff --git a/plugins/10-disable-feature.lua b/plugins/10-disable-feature.lua new file mode 100644 index 00000000..4ea16ec4 --- /dev/null +++ b/plugins/10-disable-feature.lua @@ -0,0 +1,16 @@ +-- SPDX-License-Identifier: MIT +-- +-- An example plugin to show how to disable an internal feature. +-- +-- Typically one would expect the plugin to re-implement the feature +-- in a more device-specific manner but that's not done here. + +-- UNCOMMENT THIS LINE TO ACTIVATE THE PLUGIN +-- libinput:register({1}) +libinput:connect("new-evdev-device", function(device) + local udev_info = device:udev_properties() + if udev_info["ID_INPUT_TOUCHPAD"] then + log.info("Disabling palm detection on " .. device:name()) + device:disable_feature("touchpad-palm-detection") + end +end) diff --git a/plugins/meson.build b/plugins/meson.build index 2be48f46..8464a014 100644 --- a/plugins/meson.build +++ b/plugins/meson.build @@ -5,6 +5,7 @@ plugins = [ '10-pointer-go-faster.lua', '10-pointer-go-slower.lua', '10-delay-motion.lua', + '10-disable-feature.lua', ] fs = import('fs') diff --git a/src/libinput-plugin-lua.c b/src/libinput-plugin-lua.c index 233e25f1..23673d9f 100644 --- a/src/libinput-plugin-lua.c +++ b/src/libinput-plugin-lua.c @@ -33,6 +33,7 @@ #include "util-strings.h" #include "evdev-frame.h" +#include "libinput-feature.h" #include "libinput-log.h" #include "libinput-plugin-lua.h" #include "libinput-plugin.h" @@ -95,6 +96,10 @@ typedef struct { int device_removed_refid; int frame_refid; + + /* Caches any disable_feature calls during device_new */ + bool was_added; + bitmask_t disabled_features; } EvdevDevice; struct libinput_lua_plugin { @@ -1011,6 +1016,44 @@ evdevdevice_append_frame(lua_State *L) return 0; } +static int +evdevdevice_disable_feature(lua_State *L) +{ + EvdevDevice *device = luaL_checkudata(L, 1, EVDEV_DEVICE_METATABLE); + luaL_argcheck(L, device != NULL, 1, EVDEV_DEVICE_METATABLE " expected"); + + const char *feature = luaL_checkstring(L, 2); + + /* No refid means we got removed, so quietly + * drop any call */ + if (device->refid == LUA_NOREF) + return 0; + + const struct { + const char *name; + enum libinput_feature feature; + } map[] = { + { "button-debouncing", LIBINPUT_FEATURE_BUTTON_DEBOUNCING }, + { "wheel-debouncing", LIBINPUT_FEATURE_WHEEL_DEBOUNCING }, + { "touchpad-jump-detection", LIBINPUT_FEATURE_TOUCHPAD_JUMP_DETECTION }, + { "touchpad-palm-detection", LIBINPUT_FEATURE_TOUCHPAD_PALM_DETECTION }, + { "touchpad-hysteresis", LIBINPUT_FEATURE_TOUCHPAD_HYSTERESIS }, + }; + + ARRAY_FOR_EACH(map, m) { + if (streq(feature, m->name)) { + struct libinput_lua_plugin *plugin = + lua_get_libinput_lua_plugin(L); + libinput_plugin_disable_device_feature(plugin->parent, + device->device, + m->feature); + return 0; + } + } + + return luaL_error(L, "Unknown feature: %s", feature); +} + static int evdevdevice_gc(lua_State *L) { @@ -1041,6 +1084,7 @@ static const struct luaL_Reg evdevdevice_vtable[] = { { "inject_frame", evdevdevice_inject_frame }, { "prepend_frame", evdevdevice_prepend_frame }, { "append_frame", evdevdevice_append_frame }, + { "disable_feature", evdevdevice_disable_feature }, { "__gc", evdevdevice_gc }, { NULL, NULL } }; diff --git a/test/test-plugins-lua.c b/test/test-plugins-lua.c index d1888661..076efdc8 100644 --- a/test/test-plugins-lua.c +++ b/test/test-plugins-lua.c @@ -1026,6 +1026,119 @@ START_TEST(lua_inject_frame) } END_TEST +enum when { + DEVICE_NEW, + FIRST_FRAME, +}; + +START_TEST(lua_disable_button_debounce) +{ + enum when when = litest_test_param_get_i32(test_env->params, "when"); + _destroy_(tmpdir) *tmpdir = tmpdir_create(NULL); + _autofree_ char *lua = strdup_printf( + "libinput:register({1})\n" + "function frame_handler(device, _, _)\n" + " device:disable_feature(\"button-debouncing\")\n" + "end\n" + "function new_device(device)\n" + " %s device:disable_feature(\"button-debouncing\")\n" + " %s device:connect(\"evdev-frame\", frame_handler)\n" + "end\n" + "libinput:connect(\"new-evdev-device\", new_device)\n", + when == DEVICE_NEW ? "" : "--", + when == FIRST_FRAME ? "" : "--"); + _autofree_ char *path = litest_write_plugin(tmpdir->path, lua); + etrace("%s", lua); + _litest_context_destroy_ struct libinput *li = + litest_create_context_with_plugindir(tmpdir->path); + + if (libinput_log_get_priority(li) > LIBINPUT_LOG_PRIORITY_DEBUG) + libinput_log_set_priority(li, LIBINPUT_LOG_PRIORITY_DEBUG); + + litest_with_logcapture(li, capture) { + libinput_plugin_system_load_plugins(li, LIBINPUT_PLUGIN_FLAG_NONE); + litest_drain_events(li); + + _destroy_(litest_device) *dev = litest_add_device(li, LITEST_MOUSE); + litest_drain_events(li); + + litest_disable_middleemu(dev); + + litest_event(dev, EV_KEY, BTN_LEFT, 1); + litest_event(dev, EV_SYN, SYN_REPORT, 0); + litest_event(dev, EV_KEY, BTN_LEFT, 0); + litest_event(dev, EV_SYN, SYN_REPORT, 0); + litest_event(dev, EV_KEY, BTN_LEFT, 1); + litest_event(dev, EV_SYN, SYN_REPORT, 0); + litest_event(dev, EV_KEY, BTN_LEFT, 0); + litest_event(dev, EV_SYN, SYN_REPORT, 0); + litest_timeout_debounce(li); + + litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_PRESSED); + litest_assert_button_event(li, + BTN_LEFT, + LIBINPUT_BUTTON_STATE_RELEASED); + litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_PRESSED); + litest_assert_button_event(li, + BTN_LEFT, + LIBINPUT_BUTTON_STATE_RELEASED); + litest_assert_empty_queue(li); + + litest_assert_strv_substring(capture->debugs, + "disabled button debouncing on request"); + } +} +END_TEST + +START_TEST(lua_disable_touchpad_jump_detection) +{ + enum when when = litest_test_param_get_i32(test_env->params, "when"); + _destroy_(tmpdir) *tmpdir = tmpdir_create(NULL); + _autofree_ char *lua = strdup_printf( + "libinput:register({1})\n" + "function frame_handler(dev, f, ts)\n" + " dev:disable_feature(\"touchpad-jump-detection\")\n" + "end\n" + "function new_device(device)\n" + " %sdevice:disable_feature(\"touchpad-jump-detection\")\n" + " %sdevice:connect(\"evdev-frame\", frame_handler)\n" + "end\n" + "libinput:connect(\"new-evdev-device\", new_device)\n", + when == DEVICE_NEW ? "" : "-- ", + when == FIRST_FRAME ? "" : "-- "); + + etrace("plugin data:\n%s", lua); + + _autofree_ char *path = litest_write_plugin(tmpdir->path, lua); + _litest_context_destroy_ struct libinput *li = + litest_create_context_with_plugindir(tmpdir->path); + + if (libinput_log_get_priority(li) > LIBINPUT_LOG_PRIORITY_DEBUG) + libinput_log_set_priority(li, LIBINPUT_LOG_PRIORITY_DEBUG); + + litest_with_logcapture(li, capture) { + libinput_plugin_system_load_plugins(li, LIBINPUT_PLUGIN_FLAG_NONE); + litest_drain_events(li); + + _destroy_(litest_device) *dev = + litest_add_device(li, LITEST_SYNAPTICS_RMI4); + litest_drain_events(li); + + litest_touch_down(dev, 0, 40, 50); + litest_touch_move(dev, 0, 80, 80); + litest_touch_move(dev, 0, 90, 90); + litest_touch_up(dev, 0); + + litest_assert_only_typed_events(li, LIBINPUT_EVENT_POINTER_MOTION); + litest_assert_empty_queue(li); + + litest_assert(!strv_find_substring(capture->infos, + "Touch jump detected and discarded", + NULL)); + } +} +END_TEST + TEST_COLLECTION(lua) { /* clang-format off */ @@ -1094,5 +1207,12 @@ TEST_COLLECTION(lua) litest_with_parameters(params, "in_timer", 'b') { litest_add_parametrized_no_device(lua_inject_frame, params); } + + litest_with_parameters(params, "when", 'I', 2, + litest_named_i32(DEVICE_NEW), + litest_named_i32(FIRST_FRAME)) { + litest_add_parametrized_no_device(lua_disable_button_debounce, params); + litest_add_parametrized_no_device(lua_disable_touchpad_jump_detection, params); + } /* clang-format on */ }