From 9e37bc0cfa4d975291e5a2899e148fb83526d4a2 Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Tue, 22 Apr 2025 10:25:55 +1000 Subject: [PATCH] plugins: add support for lua plugins to change evdev event streams This patch adds support for Lua scripts to modify evdev devices and event frames before libinput sees those events. Plugins in Lua are sandboxed and restricted in what they can do: no IO, no network, not much of anything else. A plugin is notified about a new device before libinput handles it and it can thus modify that device (changes are passed through to our libevdev context). A plugin can then also connect an evdev frame handler which gives it access to the evdev frames before libinput processes them. The example plugin included shows how to swap left/right mouse buttons: libinput:register({1}) function frame(device, frame) for _, v in ipairs(frame.events) do if v.usage == evdev.BTN_RIGHT then v.usage = evdev.BTN_LEFT elseif v.usage == evdev.BTN_LEFT then v.usage = evdev.BTN_RIGHT end end return frame end function device_new(plugin, device) local usages = device:usages() if usages[evdev.BTN_LEFT] and usages[evdev.BTN_RIGHT] then device:connect("evdev-frame", frame) end end libinput:connect("new-evdev-device", device_new) A few other example plugins are included in this patch Part-of: --- .gitlab-ci.yml | 64 +- .gitlab-ci/ci.template | 15 + .gitlab-ci/config.yml | 10 +- doc/user/index.rst | 1 + doc/user/lua-plugins.rst | 633 ++++++++ doc/user/meson.build | 1 + meson.build | 37 +- meson_options.txt | 4 + plugins/10-delay-motion.lua | 61 + plugins/10-dwt.lua | 40 + plugins/10-example.lua | 87 ++ .../10-logitech-mx-master-horiz-scroll.lua | 32 + plugins/10-pointer-go-faster.lua | 22 + plugins/10-pointer-go-slower.lua | 53 + plugins/meson.build | 13 + src/libinput-plugin-lua.c | 1361 +++++++++++++++++ src/libinput-plugin-lua.h | 32 + src/libinput-plugin.c | 13 + test/test-plugins-lua.c | 1098 +++++++++++++ 19 files changed, 3562 insertions(+), 15 deletions(-) create mode 100644 doc/user/lua-plugins.rst create mode 100644 plugins/10-delay-motion.lua create mode 100644 plugins/10-dwt.lua create mode 100644 plugins/10-example.lua create mode 100644 plugins/10-logitech-mx-master-horiz-scroll.lua create mode 100644 plugins/10-pointer-go-faster.lua create mode 100644 plugins/10-pointer-go-slower.lua create mode 100644 plugins/meson.build create mode 100644 src/libinput-plugin-lua.c create mode 100644 src/libinput-plugin-lua.h create mode 100644 test/test-plugins-lua.c diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 64928065..59cb4bfb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -98,11 +98,11 @@ variables: # See the documentation here: # # https://wayland.freedesktop.org/libinput/doc/latest/building.html # ############################################################################### - FEDORA_PACKAGES: 'git-core gcc gcc-c++ pkgconf-pkg-config meson check-devel libudev-devel libevdev-devel doxygen graphviz python3-sphinx python3-recommonmark python3-sphinx_rtd_theme python3-pytest-xdist libwacom-devel cairo-devel gtk4-devel glib2-devel mtdev-devel diffutils wayland-protocols-devel black clang clang-tools-extra jq rpmdevtools valgrind systemd-udev qemu-img qemu-system-x86-core qemu-system-aarch64-core jq python3-click python3-rich virtme-ng' - DEBIAN_PACKAGES: 'git gcc g++ pkg-config meson check libudev-dev libevdev-dev doxygen graphviz python3-sphinx python3-recommonmark python3-sphinx-rtd-theme python3-pytest-xdist libwacom-dev libcairo2-dev libgtk-3-dev libglib2.0-dev libmtdev-dev curl' - UBUNTU_PACKAGES: 'git gcc g++ pkg-config meson check libudev-dev libevdev-dev doxygen graphviz python3-sphinx python3-recommonmark python3-sphinx-rtd-theme python3-pytest-xdist libwacom-dev libcairo2-dev libgtk-3-dev libglib2.0-dev libmtdev-dev' - ARCH_PACKAGES: 'git gcc pkgconfig meson check libsystemd libevdev python-pytest-xdist libwacom gtk4 mtdev diffutils' - ALPINE_PACKAGES: 'git gcc build-base pkgconfig meson check-dev eudev-dev libevdev-dev libwacom-dev cairo-dev gtk4.0-dev mtdev-dev bash' + FEDORA_PACKAGES: 'git-core gcc gcc-c++ pkgconf-pkg-config meson check-devel libudev-devel libevdev-devel doxygen graphviz python3-sphinx python3-recommonmark python3-sphinx_rtd_theme python3-pytest-xdist libwacom-devel cairo-devel gtk4-devel glib2-devel mtdev-devel diffutils wayland-protocols-devel black clang clang-tools-extra jq rpmdevtools valgrind systemd-udev qemu-img qemu-system-x86-core qemu-system-aarch64-core jq python3-click python3-rich virtme-ng lua-devel' + DEBIAN_PACKAGES: 'git gcc g++ pkg-config meson check libudev-dev libevdev-dev doxygen graphviz python3-sphinx python3-recommonmark python3-sphinx-rtd-theme python3-pytest-xdist libwacom-dev libcairo2-dev libgtk-3-dev libglib2.0-dev libmtdev-dev curl lua5.4-dev' + UBUNTU_PACKAGES: 'git gcc g++ pkg-config meson check libudev-dev libevdev-dev doxygen graphviz python3-sphinx python3-recommonmark python3-sphinx-rtd-theme python3-pytest-xdist libwacom-dev libcairo2-dev libgtk-3-dev libglib2.0-dev libmtdev-dev lua5.4-dev' + ARCH_PACKAGES: 'git gcc pkgconfig meson check libsystemd libevdev python-pytest-xdist libwacom gtk4 mtdev diffutils lua' + ALPINE_PACKAGES: 'git gcc build-base pkgconfig meson check-dev eudev-dev libevdev-dev libwacom-dev cairo-dev gtk4.0-dev mtdev-dev bash lua5.4-dev' FREEBSD_PACKAGES: 'git pkgconf meson libepoll-shim libudev-devd libevdev libwacom gtk3 libmtdev bash wayland' ############################ end of package lists ############################# @@ -110,12 +110,12 @@ variables: # changing these will force rebuilding the associated image # Note: these tags have no meaning and are not tied to a particular # libinput version - FEDORA_TAG: '2025-05-19.0' - DEBIAN_TAG: '2025-05-19.0' - UBUNTU_TAG: '2025-05-19.0' - ARCH_TAG: '2025-05-19.0' - ALPINE_TAG: '2025-05-19.0' - FREEBSD_TAG: '2025-05-19.0' + FEDORA_TAG: '2025-05-20.0' + DEBIAN_TAG: '2025-05-20.0' + UBUNTU_TAG: '2025-05-20.0' + ARCH_TAG: '2025-05-20.0' + ALPINE_TAG: '2025-05-20.0' + FREEBSD_TAG: '2025-05-20.0' FDO_UPSTREAM_REPO: libinput/libinput @@ -796,6 +796,19 @@ vm-pointer-no-libwacom: variables: MESON_ARGS: '-Dlibwacom=false' +vm-lua: + extends: + - .fedora:42@test-suite-vm + variables: + SUITE_NAMES: 'lua' + +vm-lua-no-libwacom: + extends: + - vm-lua + stage: test-suite-no-libwacom + variables: + MESON_ARGS: '-Dlibwacom=false' + vm-valgrind-touchpad: stage: valgrind @@ -1005,6 +1018,20 @@ vm-valgrind-pointer: rules: - if: $GITLAB_USER_LOGIN != "marge-bot" +vm-valgrind-lua: + stage: valgrind + extends: + - vm-lua + - .policy-retry-on-failure + variables: + MESON_TEST_ARGS: '--setup=valgrind' + LITEST_JOBS: 0 + retry: + max: 2 + rules: + - if: $GITLAB_USER_LOGIN != "marge-bot" + + .fedora-build@template: extends: @@ -1067,6 +1094,20 @@ build-no-mtdev-nodeps@fedora:42: before_script: - dnf remove -y mtdev mtdev-devel +build-no-lua@fedora:42: + extends: + - .fedora-build@template + variables: + MESON_ARGS: "-Dlua-plugins=disabled" + +build-no-lua-nodeps@fedora:42: + extends: + - .fedora-build@template + variables: + MESON_ARGS: "-Dlua-plugins=disabled" + before_script: + - dnf remove -y lua lua-devel + build-docs@fedora:42: extends: - .fedora-build@template @@ -1163,6 +1204,7 @@ check-test-suites: libinput-test-suite-totem libinput-test-suite-touch libinput-test-suite-pointer + libinput-test-suite-lua EOF - sort -o ci-testsuites ci-testsuites - diff -u8 -w ci-testsuites meson-testsuites || (echo "Some test suites are not run in the CI" && false) diff --git a/.gitlab-ci/ci.template b/.gitlab-ci/ci.template index 7190f264..007bc1c6 100644 --- a/.gitlab-ci/ci.template +++ b/.gitlab-ci/ci.template @@ -492,6 +492,7 @@ vm-valgrind-{{suite.name}}: - if: $GITLAB_USER_LOGIN != "marge-bot" {% endfor %} + {% endfor %}{# for if distro.use_for_qemu_tests #} {% for distro in distributions if distro.use_for_custom_build_tests %} @@ -557,6 +558,20 @@ build-no-mtdev-nodeps@{{distro.name}}:{{version}}: before_script: - dnf remove -y mtdev mtdev-devel +build-no-lua@{{distro.name}}:{{version}}: + extends: + - .{{distro.name}}-build@template + variables: + MESON_ARGS: "-Dlua-plugins=disabled" + +build-no-lua-nodeps@{{distro.name}}:{{version}}: + extends: + - .{{distro.name}}-build@template + variables: + MESON_ARGS: "-Dlua-plugins=disabled" + before_script: + - dnf remove -y lua lua-devel + build-docs@{{distro.name}}:{{version}}: extends: - .{{distro.name}}-build@template diff --git a/.gitlab-ci/config.yml b/.gitlab-ci/config.yml index 1ff1df6c..bcdf0d6f 100644 --- a/.gitlab-ci/config.yml +++ b/.gitlab-ci/config.yml @@ -3,7 +3,7 @@ # # We're happy to rebuild all containers when one changes. -.default_tag: &default_tag '2025-05-19.0' +.default_tag: &default_tag '2025-05-20.0' distributions: - name: fedora @@ -50,6 +50,7 @@ distributions: - python3-click - python3-rich - virtme-ng + - lua-devel - name: debian tag: *default_tag versions: @@ -75,6 +76,7 @@ distributions: - libglib2.0-dev - libmtdev-dev - curl # for the coverity job + - lua5.4-dev - name: ubuntu tag: *default_tag versions: @@ -99,6 +101,7 @@ distributions: - libgtk-3-dev - libglib2.0-dev - libmtdev-dev + - lua5.4-dev - name: arch tag: *default_tag versions: @@ -116,6 +119,7 @@ distributions: - gtk4 - mtdev - diffutils + - lua build: extra_variables: - "MESON_ARGS: '-Ddocumentation=false'" # python-recommonmark is no longer in the repos @@ -136,6 +140,7 @@ distributions: - gtk4.0-dev - mtdev-dev - bash + - lua5.4-dev build: extra_variables: - "MESON_ARGS: '-Ddocumentation=false' # alpine does not have python-recommonmark" @@ -227,6 +232,9 @@ test_suites: - name: pointer suites: - pointer + - name: lua + suites: + - lua vng: kernel: https://gitlab.freedesktop.org/api/v4/projects/libevdev%2Fhid-tools/packages/generic/kernel-x86_64/v6.14/bzImage diff --git a/doc/user/index.rst b/doc/user/index.rst index 4bf8c32b..40b40c9c 100644 --- a/doc/user/index.rst +++ b/doc/user/index.rst @@ -12,6 +12,7 @@ troubleshooting contributing development + lua-plugins API documentation <@HTTP_DOC_LINK@/api/> diff --git a/doc/user/lua-plugins.rst b/doc/user/lua-plugins.rst new file mode 100644 index 00000000..6e581c49 --- /dev/null +++ b/doc/user/lua-plugins.rst @@ -0,0 +1,633 @@ +.. _lua_plugins: + +============================================================================== +Lua Plugins +============================================================================== + +libinput provides a plugin system that allows users to modify the behavior +of devices. For example, a plugin may add or remove axes and/or buttons on a +device and/or modify the event stream seen by this device before it is passed +to libinput. + +Plugins are implemented in `Lua `_ (version 5.4 or later) +and are typically loaded from ``/usr/lib{64}/libinput/plugins`` and +``/etc/libinput/plugins``. Plugins are loaded in alphabetical order and where +multiple plugins share the same file name, the one in the highest precedence +directory is used. Plugins in ``/etc`` take precedence over +plugins in ``/usr``. + +Plugins are run sequentially in ascending sort-order (i.e. ``00-foo.lua`` runs +before ``10-bar.lua``) and each plugin sees the state left by any previous +plugins. + +See the `Lua Reference manual `_ for +details on the Lua language. + +.. note:: Plugins are **not** loaded by default, it is up to the compositor + whether to allow plugins. An explicit call to + ``libinput_plugin_system_load_plugins()`` is required. + +------------------------------------------------------------------------------ +Limitations +------------------------------------------------------------------------------ + +Each script runs in its own sandbox and cannot communicate or share state with +other scripts. + +Tables that hold API methods are not writable, i.e. it is not possible +to overwrite the default functionality of those APIs. + +The Lua API available to plugins is limited to the following calls:: + + assert error ipairs next pairs tonumber + pcall select print tostring type xpcall + table string math + +It is not possible to e.g. use the ``io`` module from a script. + +To use methods on instantiated objects, the method call syntax must be used. +For example: + +.. code-block:: lua + + libinput:register() + libinput.register() -- this will fail + +------------------------------------------------------------------------------ +When to use plugins +------------------------------------------------------------------------------ + +libinput plugins are a relatively niche use-case that typically need to +address either once-off issues (e.g. those caused by worn-out hardware) or +user preferences that libinput does not and will not cater for. + +Plugins should not be used for issues that can be fixed generically, for +example via :ref:`device-quirks`. + +As a rule of thumb: a plugin should be a once-off that only works for one +user's hardware. If a plugin can be shared with many users then the plugin +implements functionality that should be integrated into libinput proper. + +------------------------------------------------------------------------------ +Testing plugins +------------------------------------------------------------------------------ + +Our :ref:`tools` support plugins if passed the ``--enable-plugins`` commandline +option. For implementing and testing plugins the easiest commands to test are + +- ``libinput debug-events --enable-plugins`` (see :ref:`libinput-debug-events` docs) +- ``libinput debug-gui --enable-plugins`` (see :ref:`libinput-debug-gui` docs) + +Where libinput is built and run from git, the tools will also look for plugins +in the meson build directory. See the ``plugins/meson.build`` file for details. + +.. _plugins_api_lua: + +-------------------------------------------------------------------------------- +Lua Plugin API +-------------------------------------------------------------------------------- + +Lua plugins sit effectively below libinput and the API is not a +representation of the libinput API. The API revolves around two types: +``libinput`` and ``EvdevDevice``. The former is used to register a +plugin from a script, the latter represents one device that is present +in the system (but may not have yet been added by libinput). + +Typically a script does the following steps: + +- register with libinput via ``libinput:register(version)`` +- connect to the ``"new-evdev-device"`` event +- receive an ``EvdevDevice`` object in the ``"new-evdev-device"`` callback + + - check and/or modify the evdev event codes on the device + - connect to the device's ``"evdev-frame"`` event + +- receive an :ref:`evdev frame ` in the device's + ``"evdev-frame"`` callback + + - check and/or modify the events in that frame + +Where multiple plugins are active, the evdev frame passed to the callback is +the combined frame as processed by all previous plugins in ascending sort order. +For example, if one plugin discards all button events subsequent plugins will +never see those button events in the frame. + +.. _plugins_api_version_stability: + +.............................................................................. +Plugin version stability +.............................................................................. + +Plugin API version stability is provided on a best effort basis. We aim to provide +stable plugin versions for as long as feasible but may need to retire some older +versions over time. For this reason a plugin can select multiple versions it +implements, libinput will pick one supported version and adjust the plugin +behavior to match that version. See the ``libinput:register()`` call for details. + +-------------------------------------------------------------------------------- +Lua Plugin API Reference +-------------------------------------------------------------------------------- + + +libinput provides the following globals and types: + +.. _plugins_api_evdev_usage: + +................................................................................ +Evdev Usages +................................................................................ + +Evdev usages are a libinput-specific wrapper around the ``linux/input-event-codes.h`` +evdev types and codes. They are used by libinput internally and are a 32-bit +combination of ``type << 16 | code``. Each usage carries the type and code and +is thus simpler to pass around and less prone to type confusion. + +For the case where the :ref:`evdev global ` does not +provide a named constant the value can be crafted manually: + +.. code-block:: lua + + type = 0x3 -- EV_REL + code = 0x1 -- REL_Y + usage = (type << 16) | code + +.. _plugins_api_evdev_global: + +................................................................................ +The ``evdev`` global +................................................................................ + +The ``evdev`` global represents all known :ref:`plugins_api_evdev_usage`, +effectively in the form: + +.. code-block:: lua + + evdev = { + ABS_X = (3 << 16) | 0, + ABS_Y = (3 << 16) | 1, + ... + REL_X = (2 << 16) | 0, + REL_Y = (2 << 16) | 1, + ... + } + + +This global is provided for convenience to improve readability in the code. +Note that the name uses the event code name only but the value is an +:ref:`Evdev Usage ` (type and code). + +See the ``linux/input-event-codes.h`` header file provided by your kernel +for a list of all evdev types and codes. + +The evdev global also provides the bus type constants, e.g. ``evdev.BUS_USB``. +See the ``linux/input.h`` header file provided by your kernel +for a list of bus types. + + +.. _plugins_api_evdev_frame: + +................................................................................ +Evdev frames +................................................................................ + +Evdev frames represent a single frame of evdev events for a device. A frame +is a group of events that occured at the same time. The frame usually only +contains state that has changed compared to the previous frame. + +In our API a frame is exposed as a nested table with the following structure: + +.. code-block:: lua + + frame1 = { + { usage = evdev.ABS_X, value = 123 }, + { usage = evdev.ABS_Y, value = 456 }, + { usage = evdev.BTN_LEFT, value = 1 }, + } + frame2 = { + { sage = evdev.ABS_Y, value = 457 }, + } + frame3 = { + { sage = evdev.ABS_X, value = 124 }, + { usage = evdev.BTN_LEFT, value = 0 }, + } + +.. note:: This API does not use ``SYN_REPORT`` events, it is implied at the + end of the table. Where a plugin writes a ``SYN_REPORT`` into the + list of events, that ``SYN_REPORT`` terminates the event frame + (similar to writing a ``\0`` into the middle of a C string). + A frame containing only a ``SYN_REPORT`` is functionally equivalent + to an empty frame. + +Events or frames do not have a timestamp. Where a timestamp is required, that +timestamp is passed as additional argument to the function or return value. + +See :ref:`plugins_api_evdev_global` for a list of known usages. + +.. warning:: Evdev frames have an implementation-defined size limit of how many + events can be added to a single frame. This limit should never be + hit by valid plugins. + +.. _plugins_api_logglobal: + +................................................................................ +The ``log`` global +................................................................................ + +The ``log`` global is used to log messages from the plugin through libinput. +Whether a message is displayed in the log depends on libinput's log priority, +set by the caller. + +.. function:: log.debug(message) + + Log a debug message. + +.. function:: log.info(message) + + Log an info message. + +.. function:: log.error(message) + + Log an error message. + +A compositor may disable stdout and stderr. Log messages should be preferred +over Lua's ``print()`` function to ensure the messages end up in the same +location as other libinput log messages and are not discarded. + +.. _plugins_api_libinputglobal: + +................................................................................ +The ``libinput`` global object +................................................................................ + +The core of our plugin's API is the ``libinput`` global object. A script must +immediately ``register()`` to be active, otherwise it is unloaded immediately. + +All libinput-specific APIs can be accessed through the ``libinput`` object. + +.. function:: libinput:register({1, 2, ...}) + + Register this plugin with the given table of supported version numbers and + returns the version number selected by libinput for this plugin. See + :ref:`plugins_api_version_stability` for details. + + .. code-block:: lua + + -- this plugin can support versions 1, 4 and 5 + version = libinput:register({1, 4, 5}) + if version == 1: + .... + + This function must be the first function called. + If the plugin calls any other functions before ``register()``, those functions + return ``nil``, 0, an empty table, etc. + + If the plugin does not call ``register()`` it will be removed immediately. + Once registered, any connected callbacks will be invoked whenever libinput + detects new devices, removes devices, etc. + + This function must only be called once. + +.. function:: libinput:unregister() + + Unregister this plugin. This removes the plugin from libinput and releases + any resources. This call must be the last call in your plugin, it is + effectively equivalent to Lua's + `os.exit() `_. + +.. function:: libinput:now() + + Returns the current time in microseconds in ``CLOCK_MONOTONIC``. This is + the timestamp libinput uses internally. This timestamp cannot be mapped + to any particular time of day, see the + `clock_gettime() man page `_ + for details. + +.. function:: libinput:version() + + Returns the agreed-on version of the plugin, see ``libinput:register()``. + If called before ``libinput:register()`` this function returns 0. + +.. function:: libinput:connect(name, function) + + Set the callback to the given event name. Only one callback + may be set for an event name at any time, subsequent callbacks + will replace any earlier callbacks for the same name. + + Version 1 of the plugin API supports the following events and callback arguments: + + - ``"new-evdev-device"``: A new :ref:`EvdevDevice ` + has been seen by libinput but not yet added. + + .. code-block:: lua + + libinput:connect("new-evdev-device", function (device) ... end) + + - ``"timer-expired"``: The timer for this plugin has expired. This event is + only sent if the plugin has set a timer with ``timer_set()``. + + .. code-block:: lua + + libinput:connect("timer-expired", function (plugin, now) ... end) + + The ``now`` argument is the current time in microseconds in + ``CLOCK_MONOTONIC`` (see ``libinput.now()``). + +.. function:: libinput:timer_cancel() + + Cancel the timer for this plugin. This is a no-op if the timer + has not been set or has already expired. + +.. function:: libinput:timer_set_absolute(time) + + Set a timer for this plugin, with the given time in microseconds. + The timeout specifies an absolute time in microseconds (see + ``libinput.now()``) The timer will expire once and then call the + ``"timer-expired"`` event handler (if any). + + See ``libinput:timer_set_relative()`` for a relative timer. + + The following two lines of code are equivalent: + + .. code-block:: lua + + libinput:timer_set_relative(1000000) -- 1 second from now + libinput:timer_set_absolute(libinput.now() + 1000000) -- 1 second from now + + Calling this function will cancel any existing (relative or absolute) timer. + +.. function:: libinput:timer_set_relative(timeout) + + Set a timer for this plugin, with the given timeout in microseconds from + the current time. The timer will expire once and then call the + ``"timer-expired"`` event handler (if any). + + See ``libinput:timer_set_absolute()`` for a relative timer. + + The following two lines of code are equivalent: + + .. code-block:: lua + + libinput:timer_set_relative(1000000) -- 1 second from now + libinput:timer_set_absolute(libinput.now() + 1000000) -- 1 second from now + + Calling this function will cancel any existing (relative or absolute) timer. + +.. _plugins_api_evdevdevice: + +................................................................................ +The ``EvdevDevice`` type +................................................................................ + +The ``EvdevDevice`` type represents a device available in the system +but not (yet) added by libinput. This device may be used to modify +a device's capabilities before the device is processed by libinput. + +.. function:: EvdevDevice:info() + + A table containing static information about the device, e.g. + + .. code-block:: lua + + { + bustype = evdev.BUS_USB, + vid = 0x1234, + pid = 0x5678, + } + + A plugin must ignore keys it does not know about. + + Version 1 of the plugin API supports the following keys and values: + + - ``bustype``: The numeric bustype of the device. See the + ``BUS_*`` defines in ``linux/input.h`` for the list of possible values. + - ``vid``: The 16-bit vendor ID of the device + - ``pid``: The 16-bit product ID of the device + +.. function:: EvdevDevice:name() + + The device name as set by the kernel + +.. function:: EvdevDevice:usages() + + Returns a nested table of all usages that are currently enabled for this + device. Any type that exists on the device has a table assigned and in this + table any code that exists on the device is a boolean true. + For example: + + .. code-block:: lua + + { + evdev.REL_X = true, + evdev.REL_Y = true, + evdev.BTN_LEFT = true, + } + + All other usage ``nil``, so that the following code is possible: + + .. code-block:: lua + + if code[evdev.REL_X] then + -- do something + end + + + If the device has since been discarded by libinput, this function returns an + empty table. + +.. function:: EvdevDevice:absinfos() + + Returns a table of all ``EV_ABS`` codes that are currently enabled for this device. + The event code is the key, each value is a table containing the following keys: + ``minimum``, ``maximum``, ``fuzz``, ``flat``, ``resolution``. + + .. code-block:: lua + + { + evdev.ABS_X = { + minimum = 0, + maximum = 1234, + fuzz = 0, + flat = 0, + resolution = 45, + }, + } + + If the device has since been discarded by libinput, this function returns an + empty table. + +.. function:: EvdevDevice:udev_properties() + + Returns a table containing a filtered list of udev properties available on this device + in the form ``{ property_name = property_value, ... }``. + udev properties used as a boolean (e.g. ``ID_INPUT``) are only present if their + value is a logical true. + + Version 1 of the plugin API supports the following udev properties: + + - ``ID_INPUT`` and all of ``ID_INPUT_*`` that denote the device type as assigned + by udev. This information is usually used by libinput to determine a + device type. Note that for historical reasons these properties have + varying rules - some properties may be mutually exclusive, others are + independent, others may only be set if another property is set. Refer to + the udev documentation (if any) for details. ``ID_INPUT_WIDTH_MM`` and + ``ID_INPUT_HEIGHT_MM`` are excluded from this set. + + If the device has since been discarded by libinput, this function returns an + empty table. + +.. function:: EvdevDevice:enable_evdev_usage(usage) + + Enable the given :ref:`evdev usage ` for this device. + Use :ref:`plugins_api_evdev_global` for better readability, + e.g. ``device:enable_evdev_usage(evdev.REL_X)``. + This function must not be used for ``ABS_*`` events, use ``set_absinfo()`` instead. + + If the device has since been discarded by libinput, this function does nothing. + +.. function:: EvdevDevice:disable_evdev_usage(usage) + + Disable the given :ref:`evdev usage ` for this device. + Use :ref:`plugins_api_evdev_global` for better readability, + e.g. ``device:disable_evdev_usage(evdev.REL_X)``. + + If the device has since been discarded by libinput, this function does nothing. + +.. function:: EvdevDevice:set_absinfo(usage, absinfo) + + Set the absolute axis information for the given :ref:`evdev usage ` + if it does not yet exist on the device. The ``absinfo`` argument is a table + containing zero or more of the following keys: ``min``, ``max``, ``fuzz``, + ``flat``, ``resolution``. Any missing key defaults the corresponding + value from the device if the device already has this event code or zero otherwise. + In other words the following code is enough to change the resolution but leave + everything else as-is: + + .. code-block:: lua + + local absinfo = { + resolution = 40, + } + device:set_absinfo(evdev.ABS_X, absinfo) + device:set_absinfo(evdev.ABS_Y, absinfo) + + Use :ref:`plugins_api_evdev_global` for better readability as shown in the + example above. + + If the device has since been discarded by libinput, this function does nothing. + + .. note:: Overriding the absinfo values often indicates buggy firmware. This should + typically be fixed with an entry in the + `60-evdev.hwdb `_ + or :ref:`device-quirks` instead of a plugin so all users of that + device can benefit from the fix. + +.. function:: EvdevDevice:connect(name, function) + + Set the callback to the given event name. Only one callback + may be set for an event name at any time, subsequent callbacks + will overwrite any earlier callbacks for the same name. + + If the device has since been discarded by libinput, this function does nothing. + + Version 1 of the plugin API supports the following events and callback arguments: + + - ``"evdev-frame"``: A new :ref:`evdev frame ` has + started for this device. If the callback returns a value other than + ``nil`` or an empty table, that value is the frame with any modified + events. + + .. code-block:: lua + + device:connect("evdev-frame", function (device, frame, timestamp) + -- change any event into a movement left by 1 pixel + move_left = { + { usage = evdev.EV_REL, code = evdev.REL_X, value = -1, }, + } + return move_left + end + + The timestamp of an event frame is in microseconds in ``CLOCK_MONOTONIC``, see + ``libinput.now()`` for details. + + For performance reasons plugins that do not modify the event frame should + return ``nil`` (or nothing) instead of the event frame given as argument. + + - ``"device-removed"``: This device was removed by libinput. This may happen + without the device ever becoming a libinput device as seen by libinput's + public API (e.g. if the device does not meet the requirements to be + added). Once this callback is invoked, the plugin should remove any + references to this device and stop using it. + + .. code-block:: lua + + device:connect("new-evdev-device", function (device) ... end) + + Functions to query the device's capabilities (e.g. ``usages()``) will + return an empty table. + +.. function:: EvdevDevice:disconnect(name) + + Disconnect the existing callback (if any) for the given event name. See + ``EvdevDevice:connect()`` for a list of supported names. + +.. function:: EvdevDevice:inject_frame(frame) + + .. warning:: This function is only available from inside a timer callback. + + Inject an :ref:`evdev frame ` into the event stream + for this device. This emulates that same event frame being sent by the kernel + immediately with the current time. + + Assuming three plugins P1, P2 and P3, if P2 injects a frame the frame is + seen by P1, P2 and P3. + + This is rarely the right API to use. Injecting frames at the lowest level + may make other plugins behave unexpectedly. Use ``prepend_frame`` or + ``append_frame`` instead. + + .. warning:: The injected frame will be seen by all plugins, including the + injecting frame. Ensure a guard is in place to prevent recursion. + +.. function:: EvdevDevice:prepend_frame(frame) + + Prepend an :ref:`evdev frame ` for this device + **before** the current frame (if any). This function can only be called from + within a device's ``frame()`` handler or from within the plugin's timer + callback function. + + Assuming three plugins P1, P2 and P3, if P2 injects a frame the frame is + seen only by P3. + + For example, to change a single event into a drag, prepend a button + down and append a button up before each event: + + .. code:: lua + + function frame_handler(device, frame, timestamp) + device:prepend_frame({ + { usage = evdev.BTN_LEFT, value = 1} + }) + device:append_frame({ + { usage = evdev.BTN_LEFT, value = 0} + }) + return nil -- return the frame unmodified + + -- this results in the event sequence + -- button down, frame, button up + -- to be passed to the next plugin + end + + If called from within the plugin's timer there is no current frame and this + function is identical to ``append_frame()``. + +.. function:: EvdevDevice:append_frame(frame) + + Appends an :ref:`evdev frame ` for this device + **after** the current frame (if any). This function can only be called from + within a device's ``frame()`` handler or from within the plugin's timer + callback function. + + If called from within the plugin's timer there is no current frame and this + function is identical to ``prepend_frame()``. + + See ``prepend_frame()`` for more details. diff --git a/doc/user/meson.build b/doc/user/meson.build index 53d8fb3b..9ca63186 100644 --- a/doc/user/meson.build +++ b/doc/user/meson.build @@ -155,6 +155,7 @@ src_rst = files( 'middle-button-emulation.rst', 'normalization-of-relative-motion.rst', 'palm-detection.rst', + 'lua-plugins.rst', 'pointer-acceleration.rst', 'reporting-bugs.rst', 'scrolling.rst', diff --git a/meson.build b/meson.build index 3da428cb..057da9fa 100644 --- a/meson.build +++ b/meson.build @@ -2,7 +2,7 @@ project('libinput', 'c', version : '1.29.0', license : 'MIT/Expat', default_options : [ 'c_std=gnu99', 'warning_level=2' ], - meson_version : '>= 0.56.0') + meson_version : '>= 0.64.0') libinput_version = meson.project_version().split('.') @@ -168,7 +168,21 @@ dep_libevdev = dependency('libevdev', version: '>= 1.10.0') dep_lm = cc.find_library('m', required : false) dep_rt = cc.find_library('rt', required : false) -config_h.set10('HAVE_PLUGINS', true) +dep_lua = dependency('lua-5.4', 'lua5.4', 'lua', + version : '>= 5.4', + required : get_option('lua-plugins')) +have_lua = dep_lua.found() +config_h.set10('HAVE_LUA', have_lua) + +have_plugins = dep_lua.found() +config_h.set10('HAVE_PLUGINS', have_plugins) + +summary({ + 'Plugins enabled' : have_plugins, + 'Lua Plugin support' : have_lua, + }, + section : 'Plugins', + bool_yn : true) # Include directories includes_include = include_directories('include') @@ -417,10 +431,17 @@ src_libinput = src_libfilter + [ 'src/timer.c', 'src/util-libinput.c', ] + if have_mtdev src_libinput += ['src/libinput-plugin-mtdev.c'] endif +if dep_lua.found() + src_libinput += [ + 'src/libinput-plugin-lua.c', + ] +endif + deps_libinput = [ dep_mtdev, dep_udev, @@ -430,7 +451,8 @@ deps_libinput = [ dep_rt, dep_libwacom, dep_libinput_util, - dep_libquirks + dep_libquirks, + dep_lua, ] libinput_version_h_config = configuration_data() @@ -479,6 +501,8 @@ git_version_h = vcs_tag(command : ['git', 'describe'], input : 'src/libinput-git-version.h.in', output :'libinput-git-version.h') +subdir('plugins') + ############ documentation ############ if get_option('documentation') @@ -1024,6 +1048,10 @@ if get_option('tests') 'test/test-switch.c', 'test/test-quirks.c', ] + if have_plugins and have_lua + tests_sources += ['test/test-plugins-lua.c'] + endif + libinput_test_runner_sources = litest_sources + tests_sources libinput_test_runner = executable('libinput-test-suite', libinput_test_runner_sources, @@ -1065,6 +1093,9 @@ if get_option('tests') 'trackpoint', 'udev', ] + if have_plugins and have_lua + collections += ['lua'] + endif foreach group : collections test('libinput-test-suite-@0@'.format(group), diff --git a/meson_options.txt b/meson_options.txt index d872f331..784ab58d 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -42,3 +42,7 @@ option('internal-event-debugging', type: 'boolean', value: false, description: 'Enable additional internal event debug tracing. This will print key values to the logs and thus must never be enabled in a release build') +option('lua-plugins', + type: 'feature', + value: 'auto', + description: 'Enable support for Lua plugins') diff --git a/plugins/10-delay-motion.lua b/plugins/10-delay-motion.lua new file mode 100644 index 00000000..ddc122e3 --- /dev/null +++ b/plugins/10-delay-motion.lua @@ -0,0 +1,61 @@ +-- SPDX-License-Identifier: MIT +-- +-- This is an example libinput plugin +-- +-- This plugin delays any event with relative motion by the given DELAY +-- by storing it in a table and replaying it via a timer callback later. + +-- UNCOMMENT THIS LINE TO ACTIVATE THE PLUGIN +-- libinput:register({1}) + +DELAY = 1500 * 1000 -- 1.5s +next_timer_expiry = 0 +devices = {} + +function timer_expired(time_in_microseconds) + next_timer_expiry = 0 + for device, frames in pairs(devices) do + while #frames > 0 and frames[1].time <= time_in_microseconds do + --- we don't have a current frame so it doesn't matter + --- whether we prepend or append + device:prepend_frame(frames[1].frame) + table.remove(frames, 1) + end + local next_frame = frames[1] + if next_frame and (next_timer_expiry == 0 or next_frame.time < next_timer_expiry) then + next_timer_expiry = next_frame.time + end + end + if next_timer_expiry ~= 0 then + libinput:timer_set_absolute(next_timer_expiry) + end +end + +function frame(device, frame, timestamp) + for _, v in ipairs(frame) do + if v.usage == evdev.REL_X or v.usage == evdev.REL_Y then + next_time = timestamp + DELAY + table.insert(devices[device], { + time = next_time, + frame = frame + }) + if next_timer_expiry == 0 then + next_timer_expiry = next_time + libinput:timer_set_absolute(next_timer_expiry) + end + return {} -- discard frame + end + end + return nil +end + +function device_new(device) + local usages = device:usages() + if usages[evdev.REL_X] then + devices[device] = {} + device:connect("evdev-frame", frame) + end +end + +libinput:connect("new-evdev-device", device_new) +libinput:connect("timer-expired", timer_expired) diff --git a/plugins/10-dwt.lua b/plugins/10-dwt.lua new file mode 100644 index 00000000..79a3b584 --- /dev/null +++ b/plugins/10-dwt.lua @@ -0,0 +1,40 @@ +-- SPDX-License-Identifier: MIT +-- +-- This plugin implements a very simple version of disable-while-typing. +-- It monitors all keyboard devices and if any of them send an event, +-- any touchpad device is disabled for 2 seconds. +-- And "disabled" means any event from that touchpad is simply +-- discarded. +-- +-- Install this file in /etc/libinput/plugins and +-- +-- UNCOMMENT THIS LINE TO ACTIVATE THE PLUGIN +-- libinput:register({1}) + +tp_enabled = true + +libinput:connect("timer-expired", function(_, now) + log.debug("touchpad enabled") + tp_enabled = true +end) + +libinput:connect("new-evdev-device", function (_, device) + local props = device:udev_properties() + if props.ID_INPUT_KEYBOARD then + device:connect("evdev-frame", function (_, _, _) + libinput:timer_set_relative(2000000) + if tp_enabled then + log.debug("touchpad disabled") + tp_enabled = false + end + end) + elseif props.ID_INPUT_TOUCHPAD then + log.debug("Touchpad detected: " .. device:name()) + device:connect("evdev-frame", function (_, frame, _) + if not tp_enabled then + -- Returning an empty table discards the event. + return {} + end + end) + end +end) diff --git a/plugins/10-example.lua b/plugins/10-example.lua new file mode 100644 index 00000000..9fe7c6ba --- /dev/null +++ b/plugins/10-example.lua @@ -0,0 +1,87 @@ +-- SPDX-License-Identifier: MIT +-- +-- This is an example libinput plugin +-- +-- This plugin swaps left and right buttons on any device that has both buttons. + +-- Let's create a plugin. A single Lua script may create more than one +-- plugin instance but that's a bit of a nice use-case. Most scripts +-- should be a single plugin. + +-- A plugin needs to be registered to activate. If it isn't, it is +-- cleaned up immediately. In the register call we supply +-- the list of plugin versions we support. Currently we only +-- have version 1. +-- +-- UNCOMMENT THIS LINE TO ACTIVATE THE PLUGIN +-- libinput:register({1}) + +-- Note to the reader: it will be easier to understand this example +-- if you read it bottom-up from here on. + +-- The callback for our "evdev-frame" signal. +-- These frames are sent *before* libinput gets to handle them so +-- any modifications will affect libinput. +function frame(device, frame, time_in_microseconds) + -- Frame is a table in the form + -- { { usage: 123, value: 3 }, ... } + -- Let's use the evdev module to make it more readable, evdev.KEY_A + -- is simply the value (0x1 << 16) | 0x1 (see linux/input-event-codes.h) + for _, v in ipairs(frame) do + -- If we get a right button event, change it to left, and + -- vice versa. Because this happens before libinput (or the next + -- plugin in the precedence order) sees the + -- frame it doesn't know that the swap happend. + if v.usage == evdev.BTN_RIGHT then + v.usage = evdev.BTN_LEFT + elseif v.usage == evdev.BTN_LEFT then + v.usage = evdev.BTN_RIGHT + end + end + -- We changed the frame, let's return it. If we return nil + -- the original frame is being used as-is. + return frame +end + +-- This is a timer callback. It is invoked after one second +-- (see below) but only once - re-set the timer so +-- it goes off every second. +function timer_expired(time_in_microseconds) + libinput:timer_set_absolute(time_in_microseconds + 1000000) +end + +-- Callback for the "new-evdev-device" signal, see below +-- The argument is the EvdevDevice object, see the documentation. +function device_new(device) + -- A table of evdev usages available on our device. + -- Using the evdev module makes it more readable but you can + -- use numbers (which is what evdev.EV_KEY and + -- friends resolve to anyway). + local usages = device:usages() + if usages[evdev.BTN_LEFT] and usages[evdev.BTN_RIGHT] then + -- The "evdev-frame" callback is invoked whenever the device + -- provided us with one evdev frame, i.e. a bunch of events up + -- to excluding EV_SYN SYN_REPORT. + device:connect("evdev-frame", frame) + end + + -- The device has udev information, let's print it out. Right + -- now all we get are the ID_INPUT_ bits. + -- If this is empty we know libinput will ignore this device anyway + local udev_info = device:udev_properties() + for k, v in pairs(udev_info) do + log.debug(k .. "=" .. v) + end +end + +-- Let's connect to the "new-evdev-device" signal. This function +-- is invoked when libinput detects a new evdev device (but before +-- that device is actually available to libinput as libinput device). +-- This allows us to e.g. change properties on the device. +libinput:connect("new-evdev-device", device_new) + +-- Set our timer to expire 1s from now (in microseconds). +-- Timers are absolute, so they need to be added to the +-- current time +libinput:connect("timer-expired", timer_expired) +libinput:timer_set_relative(1000000) diff --git a/plugins/10-logitech-mx-master-horiz-scroll.lua b/plugins/10-logitech-mx-master-horiz-scroll.lua new file mode 100644 index 00000000..d182d631 --- /dev/null +++ b/plugins/10-logitech-mx-master-horiz-scroll.lua @@ -0,0 +1,32 @@ +-- SPDX-License-Identifier: MIT +-- +-- This plugin inverts the horizontal scroll direction of +-- the Logitech MX Master mouse. OOTB the mouse scrolls +-- in the opposite direction to all other mice out there. +-- +-- This plugin is only needed when the mouse is connected +-- to the Logitech Bolt receiver - on that receiver we cannot +-- tell which device is connected. +-- +-- For the Logitech Unifying receiver and Bluetooth please +-- add the ModelInvertHorizontalScrolling=1 quirk +-- in quirks/30-vendor-logitech.quirks. +-- +-- +-- Install this file in /etc/libinput/plugins and +-- +-- UNCOMMENT THIS LINE TO ACTIVATE THE PLUGIN +-- libinput:register({1}) +libinput:connect("new-evdev-device", function (_, device) + local info = device:info() + if info.vid == 0x046D and info.pid == 0xC548 then + device:connect("evdev-frame", function (_, frame, timestamp) + for _, event in ipairs(frame) do + if event.usage == evdev.REL_HWHEEL or event.usage == evdev.REL_HWHEEL_HI_RES then + event.value = -event.value + end + end + return frame + end) + end +end) diff --git a/plugins/10-pointer-go-faster.lua b/plugins/10-pointer-go-faster.lua new file mode 100644 index 00000000..23e8b4be --- /dev/null +++ b/plugins/10-pointer-go-faster.lua @@ -0,0 +1,22 @@ +-- SPDX-License-Identifier: MIT +-- +-- An example plugin to make the pointer go three times as fast +-- +-- Install this file in /etc/libinput/plugins and +-- +-- UNCOMMENT THIS LINE TO ACTIVATE THE PLUGIN +-- libinput:register({1}) +libinput:connect("new-evdev-device", function(_, device) + local usages = device:usages() + if usages[evdev.REL_X] then + device:connect("evdev-frame", function(_, frame, timestamp) + for _, v in ipairs(frame) do + if v.usage == evdev.REL_X or v.usage == evdev.REL_Y then + -- Multiply the relative motion by 3 + v.value = v.value * 3 + end + end + return frame + end) + end +end) diff --git a/plugins/10-pointer-go-slower.lua b/plugins/10-pointer-go-slower.lua new file mode 100644 index 00000000..56bdea2b --- /dev/null +++ b/plugins/10-pointer-go-slower.lua @@ -0,0 +1,53 @@ +-- SPDX-License-Identifier: MIT +-- +-- An example plugin to make the pointer go three times as slow +-- +-- Install this file in /etc/libinput/plugins and +-- +-- UNCOMMENT THIS LINE TO ACTIVATE THE PLUGIN +-- libinput:register({1}) +remainders = {} + +function split(v) + if math.abs(v) >= 1.0 then + local i = math.floor(math.abs(v)) + local r = math.abs(v) % 1.0 + if v < 0.0 then + i = -i + r = -r + end + return i, r + else + return 0, v + end +end + +function decelerate(device, x, y) + local remainder = remainders[device] + local rx, ry = 0, 0 + if x ~= 0.0 then + rx, remainder.x = split(remainder.x + x/3.0) + end + if y ~= 0.0 then + ry, remainder.y = split(remainder.y + y/3.0) + end + + return rx, ry +end + +libinput:connect("new-evdev-device", function(device) + local usages = device:usages() + if usages[evdev.REL_X] then + remainders[device] = { x = 0.0, y = 0.0 } + device:connect("evdev-frame", function(_, frame, timestamp) + for _, v in ipairs(frame) do + if v.usage == evdev.REL_X then + v.value, _ = decelerate(device, v.value, 0.0) + elseif v.usage == evdev.REL_Y then + _, v.value = decelerate(device, 0.0, v.value) + end + end + return frame + end) + end +end) diff --git a/plugins/meson.build b/plugins/meson.build new file mode 100644 index 00000000..2be48f46 --- /dev/null +++ b/plugins/meson.build @@ -0,0 +1,13 @@ +plugins = [ + '10-example.lua', + '10-dwt.lua', + '10-logitech-mx-master-horiz-scroll.lua', + '10-pointer-go-faster.lua', + '10-pointer-go-slower.lua', + '10-delay-motion.lua', +] + +fs = import('fs') +foreach plugin : plugins + fs.copyfile(plugin) +endforeach diff --git a/src/libinput-plugin-lua.c b/src/libinput-plugin-lua.c new file mode 100644 index 00000000..00130165 --- /dev/null +++ b/src/libinput-plugin-lua.c @@ -0,0 +1,1361 @@ +/* + * Copyright © 2025 Red Hat, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "config.h" + +#include +#include +#include +#include +#include + +#include "util-mem.h" +#include "util-strings.h" + +#include "evdev-frame.h" +#include "libinput-log.h" +#include "libinput-plugin-lua.h" +#include "libinput-plugin.h" +#include "libinput-util.h" +#include "timer.h" + +const uint32_t LIBINPUT_PLUGIN_VERSION = 1U; + +#define PLUGIN_METATABLE "LibinputPlugin" +#define EVDEV_DEVICE_METATABLE "EvdevDevice" + +static const char libinput_lua_plugin_key = 'p'; /* key to lua registry */ +static const char libinput_key = 'l'; /* key to lua registry */ + +DEFINE_TRIVIAL_CLEANUP_FUNC(lua_State *, lua_close); + +struct udev_property { + struct list link; + char *key; + char *value; +}; + +static inline struct udev_property * +udev_property_new(const char *key, const char *value) +{ + struct udev_property *prop = zalloc(sizeof(*prop)); + prop->key = safe_strdup(key); + prop->value = safe_strdup(value); + return prop; +} + +static inline void +udev_property_destroy(struct udev_property *prop) +{ + list_remove(&prop->link); + free(prop->key); + free(prop->value); + free(prop); +} + +/* A thin wrapper struct that just needs to exist, all + * the actual logic is struct libinput_lua_plugin */ +typedef struct { +} LibinputPlugin; + +typedef struct { + struct list link; + int refid; + + struct libinput_device *device; + + unsigned int id; + unsigned int bustype; + unsigned int vid; + unsigned int pid; + char *name; + struct list udev_properties_list; + + struct libevdev *evdev; + + int device_removed_refid; + int frame_refid; +} EvdevDevice; + +struct libinput_lua_plugin { + struct libinput_plugin *parent; + lua_State *L; + int sandbox_table_idx; + bool register_called; + + struct list evdev_devices; /* EvdevDevice */ + + size_t version; + int device_new_refid; + int timer_expired_refid; + + struct libinput_plugin_timer *timer; + bool in_timer_func; + struct list timer_injected_events; +}; + +static struct libinput_lua_plugin * +lua_get_libinput_lua_plugin(lua_State *L) +{ + struct libinput_lua_plugin *plugin = NULL; + + lua_pushlightuserdata(L, (void *)&libinput_lua_plugin_key); + lua_gettable(L, LUA_REGISTRYINDEX); + plugin = lua_touserdata(L, -1); + lua_pop(L, 1); + + return plugin; +} + +/* Prints the current stack "layout" with a message */ +#define lua_show_stack(L, ...) { \ + etrace(__VA_ARGS__); \ + etrace("pcall: stack has: %d", lua_gettop(L)); \ + for (int i = -1; i >= -lua_gettop(L); i--) \ + etrace(" stack %d: %s", i, lua_typename(L, lua_type(L, i))); \ +} + +static struct libinput * +lua_get_libinput(lua_State *L) +{ + struct libinput *libinput = NULL; + + lua_pushlightuserdata(L, (void *)&libinput_key); + lua_gettable(L, LUA_REGISTRYINDEX); + libinput = lua_touserdata(L, -1); + lua_pop(L, 1); + + return libinput; +} + +static void +lua_push_evdev_device(lua_State *L, + struct libinput_lua_plugin *plugin, + struct libinput_device *device, + struct libevdev *evdev, + struct udev_device *udev_device) +{ + EvdevDevice *lua_device = lua_newuserdata(L, sizeof(*lua_device)); + memset(lua_device, 0, sizeof(*lua_device)); + lua_device->device = libinput_device_ref(device); + lua_device->evdev = evdev; + lua_device->bustype = libinput_device_get_id_bustype(device); + lua_device->vid = libinput_device_get_id_vendor(device); + lua_device->pid = libinput_device_get_id_product(device); + lua_device->name = strdup(libinput_device_get_name(device)); + lua_device->device_removed_refid = LUA_NOREF; + lua_device->frame_refid = LUA_NOREF; + list_init(&lua_device->udev_properties_list); + + struct udev_list_entry *e = udev_device_get_properties_list_entry(udev_device); + while (e) { + const char *key = udev_list_entry_get_name(e); + if (strstartswith(key, "ID_INPUT_") && + !streq(key, "ID_INPUT_WIDTH_MM") && + !streq(key, "ID_INPUT_HEIGHT_MM")) { + const char *value = udev_list_entry_get_value(e); + if (!streq(value, "0")) { + struct udev_property *prop = + udev_property_new(key, value); + list_insert(&lua_device->udev_properties_list, + &prop->link); + } + } + e = udev_list_entry_get_next(e); + } + + list_insert(&plugin->evdev_devices, &lua_device->link); + + lua_pushvalue(L, -1); /* Copy to top */ + lua_device->refid = luaL_ref(L, LUA_REGISTRYINDEX); /* ref to device */ + + luaL_getmetatable(L, EVDEV_DEVICE_METATABLE); + lua_setmetatable(L, -2); +} + +static void +lua_push_evdev_frame(lua_State *L, struct evdev_frame *frame) +{ + size_t nevents; + struct evdev_event *events = evdev_frame_get_events(frame, &nevents); + + lua_newtable(L); + for (size_t i = 0; i < nevents; i++) { + struct evdev_event *e = &events[i]; + + if (evdev_usage_eq(e->usage, EVDEV_SYN_REPORT)) + break; + + lua_newtable(L); + lua_pushinteger(L, evdev_usage_as_uint32_t(e->usage)); + lua_setfield(L, -2, "usage"); + lua_pushinteger(L, e->value); + lua_setfield(L, -2, "value"); + lua_rawseti(L, -2, i + 1); + } +} + +static void +lua_pop_evdev_frame(struct libinput_lua_plugin *plugin, struct evdev_frame *frame_out) +{ + lua_State *L = plugin->L; + + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + return; + } + + if (!lua_istable(L, -1)) { + plugin_log_bug(plugin->parent, + "expected table like `{ events = { ... } }`, got %s", + lua_typename(L, lua_type(L, -1))); + return; + } + + struct evdev_event events[64] = { 0 }; + size_t nevents = 0; + + lua_pushnil(L); + while (lua_next(L, -2) != 0 && nevents < ARRAY_LENGTH(events)) { + + /* -2 is the index, -1 our { usage = ... } table */ + if (!lua_istable(L, -1)) { + plugin_log_bug( + plugin->parent, + "expected table like `{ type = ..., code = ...}`, got %s", + lua_typename(L, lua_type(L, -1))); + lua_pop(L, 1); + return; + } + + lua_getfield(L, -1, "usage"); + uint32_t usage = luaL_checkinteger(L, -1); + lua_pop(L, 1); + + lua_getfield(L, -1, "value"); + int32_t value = luaL_checkinteger(L, -1); + lua_pop(L, 1); + + lua_pop(L, 1); /* pop { usage = ..., value = ...} */ + + struct evdev_event *e = &events[nevents++]; + e->usage = evdev_usage_from_uint32_t(usage); + e->value = value; + + if (evdev_usage_eq(e->usage, EVDEV_SYN_REPORT)) { + lua_pop(L, 1); /* force-pop the nil */ + break; + } + } + + if (nevents == 0) { + events[0].usage = evdev_usage_from_uint32_t(EVDEV_SYN_REPORT); + events[0].value = 0; + nevents++; + } + + if (evdev_frame_set(frame_out, events, nevents) == -ENOMEM) { + plugin_log_bug(plugin->parent, "too many events in frame"); + } +} + +static bool +libinput_lua_pcall(struct libinput_lua_plugin *plugin, int narg, int nres) +{ + lua_State *L = plugin->L; + + int rc = lua_pcall(L, narg, nres, 0); + if (rc != LUA_OK) { + auto libinput_plugin = plugin->parent; + const char *errormsg = lua_tostring(L, -1); + if (strstr(errormsg, "@@unregistering@@") == NULL) { + plugin_log_bug(libinput_plugin, + "unloading after error: %s\n", + errormsg); + } + lua_pop(L, 1); /* pop error message */ + + if (plugin->timer) + libinput_plugin_timer_cancel(plugin->timer); + libinput_plugin_unregister(libinput_plugin); + /* plugin system will destroy the plugin later */ + } + return rc == LUA_OK; +} + +static void +libinput_lua_plugin_device_new(struct libinput_plugin *libinput_plugin, + struct libinput_device *device, + struct libevdev *evdev, + struct udev_device *udev_device) +{ + struct libinput_lua_plugin *plugin = + libinput_plugin_get_user_data(libinput_plugin); + + lua_rawgeti(plugin->L, LUA_REGISTRYINDEX, plugin->device_new_refid); + lua_push_evdev_device(plugin->L, plugin, device, evdev, udev_device); + + libinput_lua_pcall(plugin, 1, 0); +} + +static void +remove_device(struct libinput_lua_plugin *plugin, EvdevDevice *evdev) +{ + /* Don't allow access to the libevdev context during remove */ + evdev->evdev = NULL; + if (evdev->device_removed_refid != LUA_NOREF) { + lua_rawgeti(plugin->L, LUA_REGISTRYINDEX, evdev->device_removed_refid); + lua_rawgeti(plugin->L, LUA_REGISTRYINDEX, evdev->refid); + + if (!libinput_lua_pcall(plugin, 1, 0)) + return; + } + luaL_unref(plugin->L, evdev->refid, LUA_REGISTRYINDEX); + evdev->refid = LUA_NOREF; + list_remove(&evdev->link); + list_init(&evdev->link); /* so we can list_remove in _gc */ + + struct udev_property *prop; + list_for_each_safe(prop, &evdev->udev_properties_list, link) { + udev_property_destroy(prop); + } + free(evdev->name); + evdev->name = NULL; + evdev->device = libinput_device_unref(evdev->device); + + /* This device no longer exists but our lua code may have a + * reference to it */ +} + +static void +libinput_lua_plugin_device_ignored(struct libinput_plugin *libinput_plugin, + struct libinput_device *device) +{ + struct libinput_lua_plugin *plugin = + libinput_plugin_get_user_data(libinput_plugin); + + EvdevDevice *evdev; + list_for_each_safe(evdev, &plugin->evdev_devices, link) { + if (evdev->device != device) + continue; + remove_device(plugin, evdev); + } +} + +static void +libinput_lua_plugin_device_removed(struct libinput_plugin *libinput_plugin, + struct libinput_device *device) +{ + struct libinput_lua_plugin *plugin = + libinput_plugin_get_user_data(libinput_plugin); + + EvdevDevice *evdev; + list_for_each_safe(evdev, &plugin->evdev_devices, link) { + if (evdev->device != device) + continue; + remove_device(plugin, evdev); + } +} + +static void +libinput_lua_plugin_evdev_frame(struct libinput_plugin *libinput_plugin, + struct libinput_device *device, + struct evdev_frame *frame) +{ + struct libinput_lua_plugin *plugin = + libinput_plugin_get_user_data(libinput_plugin); + + EvdevDevice *evdev; + list_for_each_safe(evdev, &plugin->evdev_devices, link) { + if (evdev->device != device) + continue; + + if (evdev->frame_refid == LUA_NOREF) + continue; + + lua_rawgeti(plugin->L, LUA_REGISTRYINDEX, evdev->frame_refid); + lua_rawgeti(plugin->L, LUA_REGISTRYINDEX, evdev->refid); + lua_push_evdev_frame(plugin->L, frame); + lua_pushinteger(plugin->L, evdev_frame_get_time(frame)); + + if (!libinput_lua_pcall(plugin, 3, 1)) + return; + lua_pop_evdev_frame(plugin, frame); + } +} + +static void +register_func(struct lua_State *L, int stack_index, int *refid) +{ + if (*refid != LUA_NOREF) + luaL_unref(L, LUA_REGISTRYINDEX, *refid); + lua_pushvalue(L, stack_index); /* Copy function to top */ + *refid = luaL_ref(L, LUA_REGISTRYINDEX); /* ref to function */ +} + +static void +unregister_func(struct lua_State *L, int *refid) +{ + if (*refid != LUA_NOREF) { + luaL_unref(L, LUA_REGISTRYINDEX, *refid); + *refid = LUA_NOREF; + } +} + +static int +libinputplugin_connect(lua_State *L) +{ + LibinputPlugin *p = luaL_checkudata(L, 1, PLUGIN_METATABLE); + luaL_argcheck(L, p != NULL, 1, PLUGIN_METATABLE " expected"); + + const char *name = luaL_checkstring(L, 2); + luaL_checktype(L, 3, LUA_TFUNCTION); + + struct libinput_lua_plugin *plugin = lua_get_libinput_lua_plugin(L); + + /* Version 1 signals */ + if (streq(name, "new-evdev-device")) { + register_func(L, 3, &plugin->device_new_refid); + } else if (streq(name, "timer-expired")) { + register_func(L, 3, &plugin->timer_expired_refid); + } else { + return luaL_error(L, "Unknown name: %s", name); + } + + return 0; +} + +static int +libinputplugin_now(lua_State *L) +{ + LibinputPlugin *p = luaL_checkudata(L, 1, PLUGIN_METATABLE); + luaL_argcheck(L, p != NULL, 1, PLUGIN_METATABLE " expected"); + + struct libinput *libinput = lua_get_libinput(L); + uint64_t now = libinput_now(libinput); + + lua_pushinteger(L, now); + + return 1; +} + +static int +libinputplugin_version(lua_State *L) +{ + LibinputPlugin *p = luaL_checkudata(L, 1, PLUGIN_METATABLE); + luaL_argcheck(L, p != NULL, 1, PLUGIN_METATABLE " expected"); + + struct libinput_lua_plugin *plugin = lua_get_libinput_lua_plugin(L); + lua_pushinteger(L, plugin->version); + return 1; +} + +static int +libinputplugin_register(lua_State *L) +{ + LibinputPlugin *p = luaL_checkudata(L, 1, PLUGIN_METATABLE); + luaL_argcheck(L, p != NULL, 1, PLUGIN_METATABLE " expected"); + + struct libinput_lua_plugin *plugin = lua_get_libinput_lua_plugin(L); + if (plugin->register_called) { + return luaL_error(L, "plugin already registered"); + } + + uint32_t versions[16] = { 0 }; + size_t idx = 0; + + luaL_checktype(L, 2, LUA_TTABLE); + lua_pushnil(L); + while (idx < ARRAY_LENGTH(versions) && lua_next(L, -2) != 0) { + int version = luaL_checkinteger(L, -1); + lua_pop(L, 1); + if (version <= 0) { + return luaL_error(L, "Invalid version number"); + } + versions[idx++] = version; + } + + ARRAY_FOR_EACH(versions, v) { + if (*v == 0) + break; + if (*v == LIBINPUT_PLUGIN_VERSION) { + plugin->version = *v; + plugin->register_called = true; + + lua_pushinteger(L, plugin->version); + + return 1; + } + } + + return luaL_error(L, "None of this plugin's versions are supported"); +} + +static int +libinputplugin_unregister(lua_State *L) +{ + LibinputPlugin *p = luaL_checkudata(L, 1, PLUGIN_METATABLE); + luaL_argcheck(L, p != NULL, 1, PLUGIN_METATABLE " expected"); + + /* Bit of a hack: unregister should work like os.exit(1) + * but we're in a lua context here so the easiest way + * to handle this is pretend we have an error, let + * our error handler unwind and just search for this + * magic string to *not* print log message */ + return luaL_error(L, "@@unregistering@@"); +} + +static int +libinputplugin_gc(lua_State *L) +{ + LibinputPlugin *p = luaL_checkudata(L, 1, PLUGIN_METATABLE); + luaL_argcheck(L, p != NULL, 1, PLUGIN_METATABLE " expected"); + + struct libinput_lua_plugin *plugin = lua_get_libinput_lua_plugin(L); + if (plugin->timer) + libinput_plugin_timer_cancel(plugin->timer); + + /* We're about to destroy the plugin so the timer is the only + * thing we need to stop, the rest will be cleaned up + * when we destroy the plugin */ + + return 0; +} + +struct timer_injected_event { + struct list link; + struct evdev_frame *frame; + int device_refid; +}; + +static void +plugin_timer_func(struct libinput_plugin *libinput_plugin, uint64_t now, void *data) +{ + struct libinput_lua_plugin *plugin = data; + struct lua_State *L = plugin->L; + + lua_rawgeti(L, LUA_REGISTRYINDEX, plugin->timer_expired_refid); + lua_pushinteger(L, now); + + /* To allow for injecting events */ + plugin->in_timer_func = true; + libinput_lua_pcall(plugin, 1, 0); + plugin->in_timer_func = false; + + struct timer_injected_event *injected_event; + list_for_each_safe(injected_event, &plugin->timer_injected_events, link) { + EvdevDevice *device; + list_for_each(device, &plugin->evdev_devices, link) { + if (device->refid == injected_event->device_refid) { + libinput_plugin_inject_evdev_frame( + plugin->parent, + device->device, + injected_event->frame); + break; + } + } + + list_remove(&injected_event->link); + evdev_frame_unref(injected_event->frame); + free(injected_event); + } +} + +static int +libinputplugin_timer_set(lua_State *L, uint64_t offset) +{ + LibinputPlugin *p = luaL_checkudata(L, 1, PLUGIN_METATABLE); + luaL_argcheck(L, p != NULL, 1, PLUGIN_METATABLE " expected"); + + struct libinput_lua_plugin *plugin = lua_get_libinput_lua_plugin(L); + uint64_t timeout = luaL_checkinteger(L, 2); + + if (!plugin->timer) { + plugin->timer = libinput_plugin_timer_new( + plugin->parent, + libinput_plugin_get_name(plugin->parent), + plugin_timer_func, + plugin); + } + + libinput_plugin_timer_set(plugin->timer, offset + timeout); + + return 0; +} + +static int +libinputplugin_timer_set_absolute(lua_State *L) +{ + return libinputplugin_timer_set(L, 0); +} + +static int +libinputplugin_timer_set_relative(lua_State *L) +{ + auto libinput = lua_get_libinput(L); + return libinputplugin_timer_set(L, libinput_now(libinput)); +} + +static int +libinputplugin_timer_cancel(lua_State *L) +{ + LibinputPlugin *p = luaL_checkudata(L, 1, PLUGIN_METATABLE); + luaL_argcheck(L, p != NULL, 1, PLUGIN_METATABLE " expected"); + + struct libinput_lua_plugin *plugin = lua_get_libinput_lua_plugin(L); + if (plugin->timer) + libinput_plugin_timer_cancel(plugin->timer); + + return 0; +} + +static const struct luaL_Reg libinputplugin_vtable[] = { + { "now", libinputplugin_now }, + { "version", libinputplugin_version }, + { "connect", libinputplugin_connect }, + { "register", libinputplugin_register }, + { "unregister", libinputplugin_unregister }, + { "timer_cancel", libinputplugin_timer_cancel }, + { "timer_set_absolute", libinputplugin_timer_set_absolute }, + { "timer_set_relative", libinputplugin_timer_set_relative }, + { "__gc", libinputplugin_gc }, + { NULL, NULL } +}; + +static void +libinputplugin_init(lua_State *L) +{ + luaL_newmetatable(L, PLUGIN_METATABLE); + lua_pushstring(L, "__index"); + lua_pushvalue(L, -2); /* push metatable */ + lua_settable(L, -3); /* metatable.__index = metatable */ + luaL_setfuncs(L, libinputplugin_vtable, 0); +} + +static int +evdevdevice_info(lua_State *L) +{ + EvdevDevice *device = luaL_checkudata(L, 1, EVDEV_DEVICE_METATABLE); + luaL_argcheck(L, device != NULL, 1, EVDEV_DEVICE_METATABLE " expected"); + + lua_newtable(L); /* { bustype: ..., vid: ..., pid: ..., name: ... } */ + + lua_pushinteger(L, device->bustype); + lua_setfield(L, -2, "bustype"); + lua_pushinteger(L, device->vid); + lua_setfield(L, -2, "vid"); + lua_pushinteger(L, device->pid); + lua_setfield(L, -2, "pid"); + + return 1; +} + +static int +evdevdevice_name(lua_State *L) +{ + EvdevDevice *device = luaL_checkudata(L, 1, EVDEV_DEVICE_METATABLE); + luaL_argcheck(L, device != NULL, 1, EVDEV_DEVICE_METATABLE "expected"); + + lua_pushstring(L, device->name); + + return 1; +} + +static int +evdevdevice_usages(lua_State *L) +{ + EvdevDevice *device = luaL_checkudata(L, 1, EVDEV_DEVICE_METATABLE); + luaL_argcheck(L, device != NULL, 1, EVDEV_DEVICE_METATABLE "expected"); + + lua_newtable(L); /* { evdev.REL_X: ... } */ + + if (device->evdev == NULL) + return 1; + + for (unsigned int t = 0; t < EV_MAX; t++) { + if (!libevdev_has_event_type(device->evdev, t)) + continue; + + int max = libevdev_event_type_get_max(t); + for (unsigned int code = 0; (int)code < max; code++) { + if (!libevdev_has_event_code(device->evdev, t, code)) + continue; + + evdev_usage_t usage = evdev_usage_from_code(t, code); + lua_pushboolean(L, true); + lua_rawseti(L, -2, evdev_usage_as_uint32_t(usage)); + } + } + + return 1; +} + +static int +evdevdevice_absinfos(lua_State *L) +{ + EvdevDevice *device = luaL_checkudata(L, 1, EVDEV_DEVICE_METATABLE); + luaL_argcheck(L, device != NULL, 1, EVDEV_DEVICE_METATABLE "expected"); + + lua_newtable(L); /* { ABS_X: { min: 1, max: 2, ... }, ... } */ + + if (device->evdev == NULL) + return 1; + + for (unsigned int code = 0; code < ABS_MAX; code++) { + const struct input_absinfo *abs = + libevdev_get_abs_info(device->evdev, code); + if (!abs) + continue; + + lua_newtable(L); + lua_pushinteger(L, abs->minimum); + lua_setfield(L, -2, "minimum"); + lua_pushinteger(L, abs->maximum); + lua_setfield(L, -2, "maximum"); + lua_pushinteger(L, abs->fuzz); + lua_setfield(L, -2, "fuzz"); + lua_pushinteger(L, abs->flat); + lua_setfield(L, -2, "flat"); + lua_pushinteger(L, abs->resolution); + lua_setfield(L, -2, "resolution"); + + evdev_usage_t usage = evdev_usage_from_code(EV_ABS, code); + lua_rawseti( + L, + -2, + evdev_usage_as_uint32_t(usage)); /* Assign to top-level table */ + } + + return 1; +} + +static int +evdevdevice_udev_properties(lua_State *L) +{ + EvdevDevice *device = luaL_checkudata(L, 1, EVDEV_DEVICE_METATABLE); + luaL_argcheck(L, device != NULL, 1, EVDEV_DEVICE_METATABLE "expected"); + + lua_newtable(L); /* { ID_INPUT: { ... } , ... } */ + + if (device->evdev == NULL) + return 1; + + struct udev_property *prop; + list_for_each(prop, &device->udev_properties_list, link) { + lua_pushstring(L, prop->value); + lua_setfield(L, -2, prop->key); /* Assign to top-level table */ + } + + return 1; +} + +static int +evdevdevice_enable_evdev_usage(lua_State *L) +{ + EvdevDevice *device = luaL_checkudata(L, 1, EVDEV_DEVICE_METATABLE); + luaL_argcheck(L, device != NULL, 1, EVDEV_DEVICE_METATABLE "expected"); + + struct libinput_lua_plugin *plugin = lua_get_libinput_lua_plugin(L); + + evdev_usage_t usage = evdev_usage_from_uint32_t(luaL_checkinteger(L, 2)); + uint16_t type = evdev_usage_type(usage); + uint16_t code = evdev_usage_code(usage); + if (type > EV_MAX) { + plugin_log_bug(plugin->parent, + , + "Ignoring invalid evdev usage %#x\n", + evdev_usage_as_uint32_t(usage)); + return 0; + } + + if (device->evdev == NULL || type == EV_ABS) + return 0; + + libevdev_enable_event_code(device->evdev, type, code, NULL); + + return 0; +} + +static int +evdevdevice_disable_evdev_usage(lua_State *L) +{ + EvdevDevice *device = luaL_checkudata(L, 1, EVDEV_DEVICE_METATABLE); + luaL_argcheck(L, device != NULL, 1, EVDEV_DEVICE_METATABLE "expected"); + + evdev_usage_t usage = evdev_usage_from_uint32_t(luaL_checkinteger(L, 2)); + uint16_t type = evdev_usage_type(usage); + uint16_t code = evdev_usage_code(usage); + + if (device->evdev == NULL || type > EV_MAX) + return 0; + + libevdev_disable_event_code(device->evdev, type, code); + + return 0; +} + +static int +evdevdevice_set_absinfo(lua_State *L) +{ + EvdevDevice *device = luaL_checkudata(L, 1, EVDEV_DEVICE_METATABLE); + luaL_argcheck(L, device != NULL, 1, EVDEV_DEVICE_METATABLE "expected"); + + evdev_usage_t usage = evdev_usage_from_uint32_t(luaL_checkinteger(L, 2)); + luaL_checktype(L, 3, LUA_TTABLE); + + if (evdev_usage_type(usage) != EV_ABS) + return 0; + + if (!device->evdev) + return 0; + + uint16_t code = evdev_usage_code(usage); + const struct input_absinfo *absinfo = + libevdev_get_abs_info(device->evdev, code); + struct input_absinfo abs = {}; + if (absinfo) + abs = *absinfo; + + lua_getfield(L, 3, "minimum"); + if (lua_isnumber(L, -1)) + abs.minimum = luaL_checkinteger(L, -1); + lua_getfield(L, 3, "maximum"); + if (lua_isnumber(L, -1)) + abs.maximum = luaL_checkinteger(L, -1); + lua_getfield(L, 3, "resolution"); + if (lua_isnumber(L, -1)) + abs.resolution = luaL_checkinteger(L, -1); + lua_getfield(L, 3, "fuzz"); + if (lua_isnumber(L, -1)) + abs.fuzz = luaL_checkinteger(L, -1); + lua_getfield(L, 3, "flat"); + if (lua_isnumber(L, -1)) + abs.flat = luaL_checkinteger(L, -1); + + libevdev_enable_event_code(device->evdev, EV_ABS, code, &abs); + + return 0; +} + +static int +evdevdevice_connect(lua_State *L) +{ + EvdevDevice *device = luaL_checkudata(L, 1, EVDEV_DEVICE_METATABLE); + luaL_argcheck(L, device != NULL, 1, EVDEV_DEVICE_METATABLE " expected"); + + const char *name = luaL_checkstring(L, 2); + luaL_checktype(L, 3, LUA_TFUNCTION); + + /* No refid means we got removed, so quietly + * drop any connect call */ + if (device->refid == LUA_NOREF) + return 0; + + if (streq(name, "device-removed")) { + register_func(L, 3, &device->device_removed_refid); + } else if (streq(name, "evdev-frame")) { + struct libinput_lua_plugin *plugin = lua_get_libinput_lua_plugin(L); + libinput_plugin_enable_device_event_frame(plugin->parent, + device->device, + true); + register_func(L, 3, &device->frame_refid); + } else { + return luaL_error(L, "Unknown name: %s", name); + } + + return 0; +} + +static int +evdevdevice_disconnect(lua_State *L) +{ + EvdevDevice *device = luaL_checkudata(L, 1, EVDEV_DEVICE_METATABLE); + luaL_argcheck(L, device != NULL, 1, EVDEV_DEVICE_METATABLE " expected"); + + const char *name = luaL_checkstring(L, 2); + + /* No refid means we got removed, so quietly + * drop any disconnect call */ + if (device->refid == LUA_NOREF) + return 0; + + if (streq(name, "device-removed")) { + unregister_func(L, &device->device_removed_refid); + } else if (streq(name, "evdev-frame")) { + struct libinput_lua_plugin *plugin = lua_get_libinput_lua_plugin(L); + libinput_plugin_enable_device_event_frame(plugin->parent, + device->device, + false); + unregister_func(L, &device->frame_refid); + } else { + return luaL_error(L, "Unknown name: %s", name); + } + + return 0; +} + +static struct evdev_frame * +evdevdevice_frame(lua_State *L, struct libinput_lua_plugin *plugin) +{ + auto frame = evdev_frame_new(64); + lua_pop_evdev_frame(plugin, frame); + + struct libinput *libinput = lua_get_libinput(L); + uint64_t now = libinput_now(libinput); + evdev_frame_set_time(frame, now); + + return frame; +} + +static int +evdevdevice_inject_frame(lua_State *L) +{ + EvdevDevice *device = luaL_checkudata(L, 1, EVDEV_DEVICE_METATABLE); + luaL_argcheck(L, device != NULL, 1, EVDEV_DEVICE_METATABLE " expected"); + + luaL_checktype(L, 2, LUA_TTABLE); + + /* No refid means we got removed, so quietly + * drop any disconnect call */ + if (device->refid == LUA_NOREF) + return 0; + + struct libinput_lua_plugin *plugin = lua_get_libinput_lua_plugin(L); + if (!plugin->in_timer_func) { + return luaL_error(L, "Injecting events only possible in a timer func"); + } + _unref_(evdev_frame) *frame = evdevdevice_frame(L, plugin); + + /* Lua is unhappy if we inject an event which calls into our lua state + * immediately so we need to queue this for later when we're out of the timer + * func */ + struct timer_injected_event *event = zalloc(sizeof(*event)); + event->device_refid = device->refid; + event->frame = steal(&frame); + list_insert(&plugin->timer_injected_events, &event->link); + + return 0; +} + +static int +evdevdevice_prepend_frame(lua_State *L) +{ + EvdevDevice *device = luaL_checkudata(L, 1, EVDEV_DEVICE_METATABLE); + luaL_argcheck(L, device != NULL, 1, EVDEV_DEVICE_METATABLE " expected"); + + luaL_checktype(L, 2, LUA_TTABLE); + + /* No refid means we got removed, so quietly + * drop any disconnect call */ + if (device->refid == LUA_NOREF) + return 0; + + struct libinput_lua_plugin *plugin = lua_get_libinput_lua_plugin(L); + _unref_(evdev_frame) *frame = evdevdevice_frame(L, plugin); + /* FIXME: need to really ensure that the device can never be dangling */ + libinput_plugin_prepend_evdev_frame(plugin->parent, device->device, frame); + + return 0; +} + +static int +evdevdevice_append_frame(lua_State *L) +{ + EvdevDevice *device = luaL_checkudata(L, 1, EVDEV_DEVICE_METATABLE); + luaL_argcheck(L, device != NULL, 1, EVDEV_DEVICE_METATABLE " expected"); + + luaL_checktype(L, 2, LUA_TTABLE); + + /* No refid means we got removed, so quietly + * drop any disconnect call */ + if (device->refid == LUA_NOREF) + return 0; + + struct libinput_lua_plugin *plugin = lua_get_libinput_lua_plugin(L); + _unref_(evdev_frame) *frame = evdevdevice_frame(L, plugin); + + /* FIXME: need to really ensure that the device can never be dangling */ + libinput_plugin_append_evdev_frame(plugin->parent, device->device, frame); + + return 0; +} + +static int +evdevdevice_gc(lua_State *L) +{ + EvdevDevice *device = luaL_checkudata(L, 1, EVDEV_DEVICE_METATABLE); + luaL_argcheck(L, device != NULL, 1, EVDEV_DEVICE_METATABLE "expected"); + + list_remove(&device->link); + struct udev_property *prop; + list_for_each_safe(prop, &device->udev_properties_list, link) { + udev_property_destroy(prop); + } + free(device->name); + + return 0; +} + +static const struct luaL_Reg evdevdevice_vtable[] = { + { "info", evdevdevice_info }, + { "name", evdevdevice_name }, + { "usages", evdevdevice_usages }, + { "absinfos", evdevdevice_absinfos }, + { "udev_properties", evdevdevice_udev_properties }, + { "enable_evdev_usage", evdevdevice_enable_evdev_usage }, + { "disable_evdev_usage", evdevdevice_disable_evdev_usage }, + { "set_absinfo", evdevdevice_set_absinfo }, + { "connect", evdevdevice_connect }, + { "disconnect", evdevdevice_disconnect }, + { "inject_frame", evdevdevice_inject_frame }, + { "prepend_frame", evdevdevice_prepend_frame }, + { "append_frame", evdevdevice_append_frame }, + { "__gc", evdevdevice_gc }, + { NULL, NULL } +}; + +static void +evdevdevice_init(lua_State *L) +{ + luaL_newmetatable(L, EVDEV_DEVICE_METATABLE); + lua_pushstring(L, "__index"); + lua_pushvalue(L, -2); /* push metatable */ + lua_settable(L, -3); /* metatable.__index = metatable */ + luaL_setfuncs(L, evdevdevice_vtable, 0); +} + +static int +logfunc(lua_State *L, enum libinput_log_priority pri) +{ + auto plugin = lua_get_libinput_lua_plugin(L); + + const char *message = luaL_checkstring(L, 1); + + plugin_log_msg(plugin->parent, pri, "%s\n", message); + + return 0; +} + +static int +log_lua_error(lua_State *L) +{ + return logfunc(L, LIBINPUT_LOG_PRIORITY_ERROR); +} + +static int +log_lua_info(lua_State *L) +{ + return logfunc(L, LIBINPUT_LOG_PRIORITY_INFO); +} + +static int +log_lua_debug(lua_State *L) +{ + return logfunc(L, LIBINPUT_LOG_PRIORITY_DEBUG); +} + +/* Exposes log.debug, log.info, log.error() */ +static const struct luaL_Reg log_funcs[] = { { "debug", log_lua_debug }, + { "info", log_lua_info }, + { "error", log_lua_error }, + { NULL, NULL } }; + +static void +libinput_lua_plugin_destroy(struct libinput_lua_plugin *plugin) +{ + if (plugin->timer) + libinput_plugin_timer_cancel(plugin->timer); + + EvdevDevice *evdev; + list_for_each_safe(evdev, &plugin->evdev_devices, link) { + remove_device(plugin, evdev); + } + + if (plugin->timer) + plugin->timer = libinput_plugin_timer_unref(plugin->timer); + if (plugin->L) + lua_close(plugin->L); + free(plugin); +} + +DEFINE_DESTROY_CLEANUP_FUNC(libinput_lua_plugin); + +static void +libinput_plugin_destroy(struct libinput_plugin *libinput_plugin) +{ + struct libinput_lua_plugin *plugin = + libinput_plugin_get_user_data(libinput_plugin); + if (plugin) + libinput_lua_plugin_destroy(plugin); +} + +static void +libinput_lua_plugin_run(struct libinput_plugin *libinput_plugin) +{ + struct libinput_lua_plugin *plugin = + libinput_plugin_get_user_data(libinput_plugin); + struct lua_State *L = plugin->L; + + /* Main entry point into the plugin, so we need to push our sandbox + * as _ENV. This only needs to be done here because all other entry + * points into the Lua script are callbacks that are set up during + * this run (and thus share the _ENV) + */ + lua_pushvalue(L, plugin->sandbox_table_idx); + const char *upval = lua_setupvalue(L, -2, 1); + if (!upval || !streq(upval, "_ENV")) { + plugin_log_bug_libinput(libinput_plugin, "Failed to set up sandbox\n"); + libinput_plugin_unregister(libinput_plugin); + return; + } + + if (libinput_lua_pcall(plugin, 0, 0) && !plugin->register_called) { + plugin_log_bug(libinput_plugin, + "plugin never registered, unloading plugin\n"); + libinput_plugin_unregister(libinput_plugin); + /* plugin system will destroy the plugin later */ + } +} + +static void +libinput_lua_init_evdev_global(lua_State *L, int sandbox_table_idx) +{ + lua_newtable(L); + for (unsigned int t = 0; t < EV_MAX; t++) { + const char *typename = libevdev_event_type_get_name(t); + if (!typename) + continue; + + int max = libevdev_event_type_get_max(t); + if (max < 0) + continue; + + for (int i = 0; i < max; i++) { + const char *name = libevdev_event_code_get_name(t, i); + if (!name) + continue; + + evdev_usage_t usage = evdev_usage_from_code(t, i); + lua_pushinteger(L, evdev_usage_as_uint32_t(usage)); + lua_setfield(L, -2, name); + } + } + +#define pushbus(name, value) do { \ + lua_pushinteger(L, value); \ + lua_setfield(L, -2, #name); \ + } while (0) + + pushbus(BUS_PCI, 0x01); + pushbus(BUS_ISAPNP, 0x02); + pushbus(BUS_USB, 0x03); + pushbus(BUS_HIL, 0x04); + pushbus(BUS_BLUETOOTH, 0x05); + pushbus(BUS_VIRTUAL, 0x06); + + pushbus(BUS_ISA, 0x10); + pushbus(BUS_I8042, 0x11); + pushbus(BUS_XTKBD, 0x12); + pushbus(BUS_RS232, 0x13); + pushbus(BUS_GAMEPORT, 0x14); + pushbus(BUS_PARPORT, 0x15); + pushbus(BUS_AMIGA, 0x16); + pushbus(BUS_ADB, 0x17); + pushbus(BUS_I2C, 0x18); + pushbus(BUS_HOST, 0x19); + pushbus(BUS_GSC, 0x1A); + pushbus(BUS_ATARI, 0x1B); + pushbus(BUS_SPI, 0x1C); + pushbus(BUS_RMI, 0x1D); + pushbus(BUS_CEC, 0x1E); + pushbus(BUS_INTEL_ISHTP, 0x1F); + pushbus(BUS_AMD_SFH, 0x20); + +#undef pushbus + + lua_setfield(L, sandbox_table_idx, "evdev"); +} + +static const struct libinput_plugin_interface interface = { + .run = libinput_lua_plugin_run, + .destroy = libinput_plugin_destroy, + .device_new = libinput_lua_plugin_device_new, + .device_ignored = libinput_lua_plugin_device_ignored, + .device_added = NULL, + .device_removed = libinput_lua_plugin_device_removed, + .evdev_frame = libinput_lua_plugin_evdev_frame, +}; + +static lua_State * +libinput_lua_plugin_init_lua(struct libinput *libinput, + struct libinput_lua_plugin *plugin) +{ + lua_State *L = luaL_newstate(); + if (!L) + return NULL; + + /* This will be our _ENV later, see libinput_lua_pcall */ + lua_newtable(L); + int sandbox_table_idx = lua_gettop(L); + plugin->sandbox_table_idx = sandbox_table_idx; + + /* Load the modules we want to (partially) expose. + * An (outdated?) list of safe function is here: + * http://lua-users.org/wiki/SandBoxes + * + * Math, String and Table seem to be safe given that our plugins + * all have their own individual sandbox. + */ + + luaL_requiref(L, LUA_GNAME, luaopen_base, 0); + static const char *allowed_funcs[] = { + "assert", "error", "ipairs", "next", "pcall", "pairs", "pairs", + "print", "warn", "tonumber", "tostring", "type", "xpcall", + }; + ARRAY_FOR_EACH(allowed_funcs, func) { + lua_getfield(L, -1, *func); + lua_setfield(L, sandbox_table_idx, *func); + } + + /* Math is fine as a whole */ + luaL_requiref(L, LUA_MATHLIBNAME, luaopen_math, 0); + lua_setfield(L, sandbox_table_idx, "math"); + + /* Table is fine as a whole */ + luaL_requiref(L, LUA_TABLIBNAME, luaopen_table, 0); + lua_setfield(L, sandbox_table_idx, "table"); + + /* Table is fine as a whole */ + luaL_requiref(L, LUA_STRLIBNAME, luaopen_table, 0); + lua_setfield(L, sandbox_table_idx, "string"); + + /* Override metatable to prevent access to unregistered globals */ + lua_pushvalue(L, sandbox_table_idx); + lua_newtable(L); + lua_setfield(L, -2, "__index"); + lua_setmetatable(L, sandbox_table_idx); + + /* Our objects */ + libinputplugin_init(L); + evdevdevice_init(L); + + /* Our globals */ + luaL_newlib(L, log_funcs); + lua_setfield(L, sandbox_table_idx, "log"); + libinput_lua_init_evdev_global(L, sandbox_table_idx); + + /* The libinput global object */ + lua_newuserdata(L, sizeof(LibinputPlugin)); + luaL_getmetatable(L, PLUGIN_METATABLE); + lua_setmetatable(L, -2); + lua_setfield(L, sandbox_table_idx, "libinput"); + + /* Make struct libinput available in our callbacks */ + lua_pushlightuserdata(L, (void *)&libinput_key); + lua_pushlightuserdata(L, libinput); + lua_settable(L, LUA_REGISTRYINDEX); + + /* Make struct libinput_lua_plugin available in our callbacks */ + lua_pushlightuserdata(L, (void *)&libinput_lua_plugin_key); + lua_pushlightuserdata(L, plugin); + lua_settable(L, LUA_REGISTRYINDEX); + + return L; +} + +struct libinput_plugin * +libinput_lua_plugin_new_from_path(struct libinput *libinput, const char *path) +{ + _destroy_(libinput_lua_plugin) *plugin = zalloc(sizeof(*plugin)); + _autofree_ char *name = safe_strdup(safe_basename(path)); + + /* libinput's plugin system keeps a ref, we don't need + * a separate ref here, the plugin system will outlast us. + */ + _unref_(libinput_plugin) *p = + libinput_plugin_new(libinput, name, &interface, NULL); + + plugin->parent = p; + plugin->register_called = false; + plugin->version = LIBINPUT_PLUGIN_VERSION; + plugin->device_new_refid = LUA_NOREF; + plugin->timer_expired_refid = LUA_NOREF; + list_init(&plugin->evdev_devices); + list_init(&plugin->timer_injected_events); + + _cleanup_(lua_closep) lua_State *L = + libinput_lua_plugin_init_lua(libinput, plugin); + if (!L) { + plugin_log_bug(plugin->parent, + "Failed to create lua state for %s\n", + name); + libinput_plugin_unregister(p); + return NULL; + } + + int ret = luaL_loadfile(L, path); + if (ret == LUA_OK) { + plugin->L = steal(&L); + + libinput_plugin_set_user_data(p, steal(&plugin)); + return p; + } else { + const char *lua_error = lua_tostring(L, -1); + const char *error = lua_error; + if (!error) { + switch (ret) { + case LUA_ERRMEM: + error = "out of memory"; + break; + case LUA_ERRFILE: + error = "file not found or not readable"; + break; + case LUA_ERRSYNTAX: + error = "syntax error"; + break; + default: + break; + } + } + + if (ret == LUA_ERRSYNTAX && + log_is_logged(libinput, LIBINPUT_LOG_PRIORITY_DEBUG)) { + luaL_traceback(L, L, NULL, 1); + for (int i = -1; i > -4; i--) { + const char *msg = lua_tostring(L, i); + if (!msg) + break; + log_debug(libinput, "%s %s\n", name, msg); + } + lua_pop(L, 1); /* traceback */ + } + + plugin_log_bug(plugin->parent, "Failed to load %s: %s\n", path, error); + + lua_pop(L, 1); /* the lua_error message */ + + libinput_plugin_unregister(p); + + return NULL; + } +} diff --git a/src/libinput-plugin-lua.h b/src/libinput-plugin-lua.h new file mode 100644 index 00000000..b8995f66 --- /dev/null +++ b/src/libinput-plugin-lua.h @@ -0,0 +1,32 @@ +/* + * Copyright © 2025 Red Hat, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#pragma once + +#include "config.h" + +#include "libinput-plugin.h" +#include "libinput.h" + +struct libinput_plugin * +libinput_lua_plugin_new_from_path(struct libinput *libinput, const char *path); diff --git a/src/libinput-plugin.c b/src/libinput-plugin.c index 181b8cf7..72d969fe 100644 --- a/src/libinput-plugin.c +++ b/src/libinput-plugin.c @@ -31,6 +31,7 @@ #include "evdev-frame.h" #include "evdev-plugin.h" #include "libinput-plugin-button-debounce.h" +#include "libinput-plugin-lua.h" #include "libinput-plugin-mouse-wheel-lowres.h" #include "libinput-plugin-mouse-wheel.h" #include "libinput-plugin-mtdev.h" @@ -363,6 +364,18 @@ libinput_plugin_system_load_plugins(struct libinput *libinput, return 0; } +#if HAVE_LUA + _autostrvfree_ char **directories = steal(&libinput->plugin_system.directories); + size_t nfiles = 0; + _autostrvfree_ char **plugin_files = + list_files((const char **)directories, ".lua", &nfiles); + for (size_t i = 0; i < nfiles; i++) { + char *path = plugin_files[i]; + log_debug(libinput, "Loading plugin from %s\n", path); + libinput_lua_plugin_new_from_path(libinput, path); + } +#endif + libinput_plugin_system_load_internal_plugins(libinput, &libinput->plugin_system); libinput->plugin_system.loaded = true; diff --git a/test/test-plugins-lua.c b/test/test-plugins-lua.c new file mode 100644 index 00000000..d1888661 --- /dev/null +++ b/test/test-plugins-lua.c @@ -0,0 +1,1098 @@ +/* + * Copyright © 2025 Red Hat, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "config.h" + +#include +#include + +#include "util-files.h" +#include "util-strings.h" +#include "util-time.h" + +#include "libinput.h" +#include "litest.h" + +static char * +_litest_write_plugin(const char *tmpdir, const char *filename, const char *content) +{ + static int counter = 0; + counter += 10; + + char *path = strdup_printf("%s/%d-%s.lua", tmpdir, counter, filename); + _autoclose_ int fd = open(path, O_WRONLY | O_CREAT, 0644); + litest_assert_errno_success(fd); + + if (content) { + write(fd, content, strlen(content)); + fsync(fd); + } + + return path; +} + +#define litest_write_plugin(tmpdir_, content_) \ + _litest_write_plugin(tmpdir_, __func__, content_) + +START_TEST(lua_load_failure) +{ + _destroy_(tmpdir) *tmpdir = tmpdir_create(NULL); + const char *lua = "asfasdk1298'..asdfasdf'123@2;asd"; /* invalid lua */ + _autofree_ char *path = litest_write_plugin(tmpdir->path, lua); + _litest_context_destroy_ struct libinput *li = + litest_create_context_with_plugindir(tmpdir->path); + + litest_with_logcapture(li, capture) { + libinput_plugin_system_load_plugins(li, LIBINPUT_PLUGIN_FLAG_NONE); + litest_drain_events(li); + + size_t index = 0; + litest_assert( + strv_find_substring(capture->errors, "Failed to load", &index)); + litest_assert_str_in(path, capture->errors[index]); + } +} +END_TEST + +enum content { + EMPTY, + NOTHING, + BASIC, + COMMENT, + DUPLICATE_CALL, + VERSION_NOT_A_TABLE, + MISSING_REGISTER, +}; + +START_TEST(lua_load_success_but_no_register) +{ + enum content content = litest_test_param_get_i32(test_env->params, "content"); + + _destroy_(tmpdir) *tmpdir = tmpdir_create(NULL); + + const char *lua = NULL; + switch (content) { + case DUPLICATE_CALL: + lua = "v1 = libinput:register({1})\np2 = libinput:register({2})\n"; + break; + case VERSION_NOT_A_TABLE: + lua = "v1 = libinput:register(1)\n"; + break; + case MISSING_REGISTER: + lua = "libinput:connect(\"new-evdev-device\", function(device) assert(false) end)\n"; + break; + case BASIC: + lua = "a = 1 + 10"; + break; + case COMMENT: + lua = "-- near-empty file"; + break; + case NOTHING: + lua = ""; + break; + case EMPTY: + break; + } + + _autofree_ char *path = litest_write_plugin(tmpdir->path, lua); + _litest_context_destroy_ struct libinput *li = + litest_create_context_with_plugindir(tmpdir->path); + + litest_with_logcapture(li, capture) { + libinput_plugin_system_load_plugins(li, LIBINPUT_PLUGIN_FLAG_NONE); + switch (content) { + case VERSION_NOT_A_TABLE: + litest_assert_strv_substring(capture->errors, + "unloading after error"); + break; + case DUPLICATE_CALL: + litest_assert_strv_substring(capture->errors, + "plugin already registered"); + break; + default: + litest_assert_strv_substring(capture->errors, + "plugin never registered"); + break; + } + } +} +END_TEST + +START_TEST(lua_register_noop) +{ + _destroy_(tmpdir) *tmpdir = tmpdir_create(NULL); + const char *lua = "libinput:register({1})"; + _autofree_ char *path = litest_write_plugin(tmpdir->path, lua); + _litest_context_destroy_ struct libinput *li = + litest_create_context_with_plugindir(tmpdir->path); + litest_with_logcapture(li, capture) { + libinput_plugin_system_load_plugins(li, LIBINPUT_PLUGIN_FLAG_NONE); + litest_drain_events(li); + litest_assert_logcapture_no_errors(capture); + } +} +END_TEST + +START_TEST(lua_unregister_is_last) +{ + const char *when = litest_test_param_get_string(test_env->params, "when"); + + _destroy_(tmpdir) *tmpdir = tmpdir_create(NULL); + _autofree_ char *lua = strdup_printf( + "libinput:register({1})\n" + "libinput:connect(\"new-evdev-device\", function(device)\n %s\n log.error(\"abort abort\")\nend)\n" + "%slog.error(\"must not happen\")", + streq(when, "connect") ? "libinput:unregister()" : "", + streq(when, "run") ? "libinput:unregister()\n" : "--"); + _autofree_ char *path = litest_write_plugin(tmpdir->path, lua); + + _litest_context_destroy_ struct libinput *li = + litest_create_context_with_plugindir(tmpdir->path); + litest_with_logcapture(li, capture) { + libinput_plugin_system_load_plugins(li, LIBINPUT_PLUGIN_FLAG_NONE); + _destroy_(litest_device) *device = litest_add_device(li, LITEST_MOUSE); + litest_drain_events(li); + litest_assert_logcapture_no_errors(capture); + } +} +END_TEST + +START_TEST(lua_test_log_global) +{ + _destroy_(tmpdir) *tmpdir = tmpdir_create(NULL); + + enum libinput_log_priority priority = + litest_test_param_get_i32(test_env->params, "priority"); + + const char *lua = NULL; + switch (priority) { + case LIBINPUT_LOG_PRIORITY_DEBUG: + lua = "log.debug(\"deb-ug\");"; + break; + case LIBINPUT_LOG_PRIORITY_INFO: + lua = "log.info(\"inf-o\");"; + break; + case LIBINPUT_LOG_PRIORITY_ERROR: + lua = "log.error(\"err-or\");"; + break; + default: + litest_assert_not_reached(); + break; + } + + _autofree_ char *path = litest_write_plugin(tmpdir->path, lua); + _litest_context_destroy_ struct libinput *li = + litest_create_context_with_plugindir(tmpdir->path); + libinput_log_set_priority(li, priority); + + litest_with_logcapture(li, capture) { + libinput_plugin_system_load_plugins(li, LIBINPUT_PLUGIN_FLAG_NONE); + litest_drain_events(li); + + switch (priority) { + case LIBINPUT_LOG_PRIORITY_DEBUG: + litest_assert( + strv_find_substring(capture->debugs, "deb-ug", NULL)); + litest_assert( + !strv_find_substring(capture->infos, "inf-o", NULL)); + litest_assert( + !strv_find_substring(capture->errors, "err-or", NULL)); + break; + case LIBINPUT_LOG_PRIORITY_INFO: + litest_assert( + !strv_find_substring(capture->debugs, "deb-ug", NULL)); + litest_assert( + strv_find_substring(capture->infos, "inf-o", NULL)); + litest_assert( + !strv_find_substring(capture->errors, "err-or", NULL)); + break; + case LIBINPUT_LOG_PRIORITY_ERROR: + litest_assert( + !strv_find_substring(capture->debugs, "deb-ug", NULL)); + litest_assert( + !strv_find_substring(capture->infos, "inf-o", NULL)); + litest_assert( + strv_find_substring(capture->errors, "err-or", NULL)); + break; + } + } +} +END_TEST + +START_TEST(lua_test_evdev_global) +{ + _destroy_(tmpdir) *tmpdir = tmpdir_create(NULL); + + /* This is generated, if a few of them work the + * rest should work too */ + const char *lua = + "libinput:register({1}); a = evdev.ABS_X\nb = evdev.REL_X\nc = evdev.KEY_A\nd = evdev.EV_SYN\n"; + _autofree_ char *path = litest_write_plugin(tmpdir->path, lua); + _litest_context_destroy_ struct libinput *li = + litest_create_context_with_plugindir(tmpdir->path); + litest_with_logcapture(li, capture) { + libinput_plugin_system_load_plugins(li, LIBINPUT_PLUGIN_FLAG_NONE); + litest_drain_events(li); + + litest_assert_logcapture_no_errors(capture); + } +} +END_TEST + +START_TEST(lua_test_libinput_now) +{ + _destroy_(tmpdir) *tmpdir = tmpdir_create(NULL); + const char *lua = "log.error(\">>>\" .. libinput:now())"; + _autofree_ char *path = litest_write_plugin(tmpdir->path, lua); + _litest_context_destroy_ struct libinput *li = + litest_create_context_with_plugindir(tmpdir->path); + litest_with_logcapture(li, capture) { + libinput_plugin_system_load_plugins(li, LIBINPUT_PLUGIN_FLAG_NONE); + litest_drain_events(li); + + uint64_t test_now; + int rc = now_in_us(&test_now); + litest_assert_neg_errno_success(rc); + + size_t index = 0; + litest_assert(strv_find_substring(capture->errors, ">>>", &index)); + size_t nelem = 0; + _autostrvfree_ char **tokens = + strv_from_string(capture->errors[index], ">>>", &nelem); + litest_assert_int_eq(nelem, 2U); + + uint64_t plugin_now = strtoull(tokens[1], NULL, 10); + + litest_assert_int_le(plugin_now, test_now); + /* Even a slow test runner hopefully doesn't take >300ms to get to the + * log print */ + litest_assert_int_gt(plugin_now, test_now - ms2us(300)); + } +} +END_TEST + +START_TEST(lua_test_libinput_timer) +{ + _destroy_(tmpdir) *tmpdir = tmpdir_create(NULL); + + const char *mode = litest_test_param_get_string(test_env->params, "mode"); + bool reschedule = litest_test_param_get_bool(test_env->params, "reschedule"); + + _autofree_ char *timeout = + strdup_printf("%s%" PRIu64, + streq(mode, "absolute") ? "libinput:now() + " : "", + ms2us(100)); + _autofree_ char *reschedule_timeout = + strdup_printf("libinput:timer_set_%s(%s%" PRIu64 ")\n", + mode, + streq(mode, "absolute") ? "t + " : "", + ms2us(100)); + _autofree_ char *lua = strdup_printf( + "libinput:register({1})\n" + "libinput:connect(\"timer-expired\",\n" + " function(t)\n" + " log.error(\">>>\" .. t)\n" + " %s\n" + " end)\n" + "libinput:timer_set_%s(%s)\n", + reschedule ? reschedule_timeout : "", + mode, + timeout); + + _autofree_ char *path = litest_write_plugin(tmpdir->path, lua); + _litest_context_destroy_ struct libinput *li = + litest_create_context_with_plugindir(tmpdir->path); + + litest_with_logcapture(li, capture) { + libinput_plugin_system_load_plugins(li, LIBINPUT_PLUGIN_FLAG_NONE); + litest_drain_events(li); + + litest_assert_logcapture_no_errors(capture); + + size_t nloops = reschedule ? 4 : 1; + for (size_t i = 0; i < nloops; i++) { + libinput_dispatch(li); + msleep(100); + libinput_dispatch(li); + + uint64_t test_now; + int rc = now_in_us(&test_now); + litest_assert_neg_errno_success(rc); + + _autostrvfree_ char **msg = steal(&capture->errors); + litest_assert_ptr_notnull(msg); + size_t index; + litest_assert(strv_find_substring(msg, ">>>", &index)); + + size_t nelem = 0; + _autostrvfree_ char **tokens = + strv_from_string(msg[index], ">>>", &nelem); + litest_assert_int_eq(nelem, 2U); + + uint64_t plugin_now = strtoull(tokens[1], NULL, 10); + litest_assert_int_le(plugin_now, test_now); + /* Even a slow test runner hopefully doesn't take >300ms between + * dispatch and now_in_us */ + litest_assert_int_gt(plugin_now, test_now - ms2us(300)); + } + + if (!reschedule) { + libinput_dispatch(li); + msleep(120); + libinput_dispatch(li); + } + + litest_assert_logcapture_no_errors(capture); + } +} +END_TEST + +enum connect_error { + BAD_TYPE, + TOO_FEW_ARGS, + TOO_MANY_ARGS, +}; + +START_TEST(lua_bad_connect) +{ + _destroy_(tmpdir) *tmpdir = tmpdir_create(NULL); + + const char *handler = litest_test_param_get_string(test_env->params, "handler"); + enum connect_error error = litest_test_param_get_i32(test_env->params, "error"); + + const char *func = NULL; + switch (error) { + case BAD_TYPE: + func = "a"; + break; + case TOO_FEW_ARGS: + func = "function(p) log.debug(\"few\"); end"; + break; + case TOO_MANY_ARGS: + func = "function(p, a, b) log.debug(\"many\"); end"; + break; + } + + _autofree_ char *lua = strdup_printf( + "libinput:register({1})\n" + "a = 10\n" + "libinput:connect(\"%s\", %s)\n", + handler, + func); + + _autofree_ char *path = litest_write_plugin(tmpdir->path, lua); + _litest_context_destroy_ struct libinput *li = + litest_create_context_with_plugindir(tmpdir->path); + litest_with_logcapture(li, capture) { + libinput_plugin_system_load_plugins(li, LIBINPUT_PLUGIN_FLAG_NONE); + litest_drain_events(li); + + switch (error) { + /* These don't trigger a lua erro so we just test they don't segfault us + */ + case TOO_FEW_ARGS: + case TOO_MANY_ARGS: + litest_assert_logcapture_no_errors(capture); + break; + case BAD_TYPE: + litest_assert_strv_substring(capture->errors, + "bad argument #2 to 'connect'"); + break; + } + } +} +END_TEST + +START_TEST(lua_register_multiversions) +{ + + _destroy_(tmpdir) *tmpdir = tmpdir_create(NULL); + const char *lua = + "v = libinput:register({1, 3, 4, 10, 15})\nlog.info(\"VERSION:\" .. v)\n"; + _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_INFO) + libinput_log_set_priority(li, LIBINPUT_LOG_PRIORITY_INFO); + + litest_with_logcapture(li, capture) { + libinput_plugin_system_load_plugins(li, LIBINPUT_PLUGIN_FLAG_NONE); + litest_drain_events(li); + + litest_assert_logcapture_no_errors(capture); + litest_assert_strv_substring(capture->infos, "VERSION:1"); + } +} +END_TEST + +START_TEST(lua_allowed_functions) +{ + _destroy_(tmpdir) *tmpdir = tmpdir_create(NULL); + /* This tests on the assumption that if some of these work, + * then the others we allow will work too. */ + const char *lua = + "\n" + "libinput:register({1})\n" + "a = {10, 20}\n" + "for _, v in ipairs(a) do\n" + " v = v + 1\n" + "end\n" + "b = {foo = 1}" + "for k, v in pairs(a) do\n" + " v = v + 1\n" + "end\n" + "print(math.maxinteger)\n" + "table.sort({10, 2, 4})\n" + "assert(true)\n" + ""; + _autofree_ char *path = litest_write_plugin(tmpdir->path, lua); + _litest_context_destroy_ struct libinput *li = + litest_create_context_with_plugindir(tmpdir->path); + litest_with_logcapture(li, capture) { + libinput_plugin_system_load_plugins(li, LIBINPUT_PLUGIN_FLAG_NONE); + litest_drain_events(li); + + litest_assert_logcapture_no_errors(capture); + } +} +END_TEST + +START_TEST(lua_disallowed_functions) +{ + _destroy_(tmpdir) *tmpdir = tmpdir_create(NULL); + /* This tests on the assumption that if some of these work, + * then the others we allow will work too. */ + const char *lua = + "\n" + "libinput:register({1})\n" + "assert(io == nil)\n" + "assert(require == nil)\n" + "assert(rawget == nil)\n" + "assert(rawset == nil)\n" + "assert(setfenv == nil)\n" + "assert(getmetatable == nil)\n" + "assert(setmetatable == nil)\n" + "assert(package == nil)\n" + "assert(os == nil)\n" + "assert(debug == nil)\n" + ""; + _autofree_ char *path = litest_write_plugin(tmpdir->path, lua); + _litest_context_destroy_ struct libinput *li = + litest_create_context_with_plugindir(tmpdir->path); + litest_with_logcapture(li, capture) { + libinput_plugin_system_load_plugins(li, LIBINPUT_PLUGIN_FLAG_NONE); + litest_drain_events(li); + + litest_assert_logcapture_no_errors(capture); + } +} +END_TEST + +START_TEST(lua_frame_handler) +{ + _destroy_(tmpdir) *tmpdir = tmpdir_create(NULL); + const char *lua = + "libinput:register({1})\n" + "function frame_handler(_, frame, timestamp)\n" + " log.info(\"T:\" .. timestamp)\n" + " for _, e in ipairs(frame) do\n" + " log.info(\"E:\" .. e.usage .. \":\" .. e.value)\n" + " end\n" + "end\n" + "libinput:connect(\"new-evdev-device\", function(device) device:connect(\"evdev-frame\", frame_handler) end)\n"; + + _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_INFO) + libinput_log_set_priority(li, LIBINPUT_LOG_PRIORITY_INFO); + + litest_with_logcapture(li, capture) { + libinput_plugin_system_load_plugins(li, LIBINPUT_PLUGIN_FLAG_NONE); + litest_drain_events(li); + + _destroy_(litest_device) *device = litest_add_device(li, LITEST_MOUSE); + litest_drain_events(li); + + uint64_t before, after; + now_in_us(&before); + msleep(1); + litest_button_click_debounced(device, li, BTN_LEFT, 1); + litest_button_click_debounced(device, li, BTN_LEFT, 0); + litest_assert_logcapture_no_errors(capture); + msleep(1); + now_in_us(&after); + + /* EV_KEY << 16 | BTN_LEFT -> 65808 */ + + litest_assert_strv_substring(capture->infos, "E:65808:1"); + litest_assert_strv_substring(capture->infos, "E:65808:0"); + /* SYN_REPORT shouldn't show up in the frame */ + litest_assert(!strv_find_substring(capture->infos, "E:0:0", NULL)); + + size_t idx; + litest_assert(strv_find_substring(capture->infos, "T:", &idx)); + + _autofree_ char *str = safe_strdup(capture->infos[idx]); + for (size_t i = 0; str[i]; i++) { + if (str[i] == '\n') { + str[i] = '\0'; + break; + } + } + + size_t nelems; + _autostrvfree_ char **split = strv_from_string(str, ":", &nelems); + litest_assert_int_gt(nelems, 1U); + char *strtime = split[nelems - 1]; + uint64_t timestamp = 0; + litest_assert(safe_atou64(strtime, ×tamp)); + litest_assert_int_gt(timestamp, before); + litest_assert_int_lt(timestamp, after); + } +} +END_TEST + +START_TEST(lua_device_info) +{ + _destroy_(tmpdir) *tmpdir = tmpdir_create(NULL); + const char *lua = + "libinput:register({1})\n" + "function info_printer(device)\n" + " local info = device:info()\n" + " log.info(\"BUS:\" .. info.bustype)\n" + " log.info(\"VID:\" .. info.vid)\n" + " log.info(\"PID:\" .. info.pid)\n" + "end\n" + "libinput:connect(\"new-evdev-device\", info_printer)\n"; + + _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_INFO) + libinput_log_set_priority(li, LIBINPUT_LOG_PRIORITY_INFO); + + litest_with_logcapture(li, capture) { + libinput_plugin_system_load_plugins(li, LIBINPUT_PLUGIN_FLAG_NONE); + litest_drain_events(li); + + _destroy_(litest_device) *device = litest_add_device(li, LITEST_MOUSE); + litest_drain_events(li); + + litest_assert_strv_substring(capture->infos, "BUS:3"); + litest_assert_strv_substring(capture->infos, "VID:6127"); + litest_assert_strv_substring(capture->infos, "PID:24601"); + } +} +END_TEST + +START_TEST(lua_set_absinfo) +{ + _destroy_(tmpdir) *tmpdir = tmpdir_create(NULL); + const char *lua = + "libinput:register({1})\n" + "function absinfo_setter(device)\n" + " local absinfos = device:absinfos()\n" + " for u, a in pairs(absinfos) do\n" + " log.info(\"A:\" .. u .. \":\" .. a.minimum .. \":\" .. a.maximum .. \":\" .. a.resolution .. \":\" .. a.fuzz .. \":\" .. a.flat)\n" + " end\n" + " device:set_absinfo(evdev.ABS_X, { minimum = 0, maximum = 1000, resolution = 100 })\n" + " device:set_absinfo(evdev.ABS_Y, { minimum = 0, maximum = 200, resolution = 10 })\n" + " device:set_absinfo(evdev.ABS_MT_POSITION_X, { minimum = 0, maximum = 1000, resolution = 100 })\n" + " device:set_absinfo(evdev.ABS_MT_POSITION_Y, { minimum = 0, maximum = 200, resolution = 10 })\n" + "end\n" + "libinput:connect(\"new-evdev-device\", absinfo_setter)\n"; + + _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_INFO) + libinput_log_set_priority(li, LIBINPUT_LOG_PRIORITY_INFO); + + litest_with_logcapture(li, capture) { + libinput_plugin_system_load_plugins(li, LIBINPUT_PLUGIN_FLAG_NONE); + litest_drain_events(li); + + _destroy_(litest_device) *device = + litest_add_device(li, LITEST_GENERIC_MULTITOUCH_SCREEN); + litest_drain_events(li); + + litest_assert_logcapture_no_errors(capture); + + for (int code = 0; code <= ABS_MAX; code++) { + if (!libevdev_has_event_code(device->evdev, EV_ABS, code)) { + _autofree_ char *prefix = + strdup_printf("A:%u", (EV_ABS << 16) | code); + litest_assert(!strv_find_substring(capture->infos, + prefix, + NULL)); + continue; + } + + const struct input_absinfo *absinfo = + libevdev_get_abs_info(device->evdev, code); + _autofree_ char *message = strdup_printf("A:%u:%d:%d:%d:%d:%d", + (EV_ABS << 16) | code, + absinfo->minimum, + absinfo->maximum, + absinfo->resolution, + absinfo->fuzz, + absinfo->flat); + litest_assert_strv_substring(capture->infos, message); + } + + /* If the absinfo worked, our device is 10x20mm big */ + double w, h; + libinput_device_get_size(device->libinput_device, &w, &h); + litest_assert_double_eq(w, 10.0); + litest_assert_double_eq(h, 20.0); + } +} +END_TEST + +START_TEST(lua_enable_disable_evdev_usage) +{ + _destroy_(tmpdir) *tmpdir = tmpdir_create(NULL); + /* We have two plugins here, one that enables codes and one that prints + * the frame. + * + * The first plugin also inserts a REL_Z event into the frame since we + * can't send that through the kernel. + */ + const char *lua1 = + "libinput:register({1})\n" + "function frame_handler(_, frame, timestamp)\n" + " table.insert(frame, { usage = evdev.REL_Z, value = 3 })\n" + " return frame\n" + "end\n" + "function enabler(device)\n" + " device:enable_evdev_usage(evdev.REL_Z)\n" + " device:enable_evdev_usage(evdev.BTN_STYLUS2)\n" + " device:disable_evdev_usage(evdev.REL_WHEEL)\n" + " device:connect(\"evdev-frame\", frame_handler)\n" + "end\n" + "libinput:connect(\"new-evdev-device\", enabler)\n"; + + const char *lua2 = + "libinput:register({1})\n" + "function frame_handler(_, frame, timestamp)\n" + " log.info(\"frame\")\n" + " for _, e in ipairs(frame) do\n" + " log.info(\"E:\" .. e.usage .. \":\" .. e.value)\n" + " end\n" + "end\n" + "function f(device)\n" + " log.info(\"F: \" .. device:name())\n" + " device:connect(\"evdev-frame\", frame_handler)\n" + "end\n" + "libinput:connect(\"new-evdev-device\", f)\n"; + + _autofree_ char *p1 = litest_write_plugin(tmpdir->path, lua1); + _autofree_ char *p2 = litest_write_plugin(tmpdir->path, lua2); + _litest_context_destroy_ struct libinput *li = + litest_create_context_with_plugindir(tmpdir->path); + if (libinput_log_get_priority(li) > LIBINPUT_LOG_PRIORITY_INFO) + libinput_log_set_priority(li, LIBINPUT_LOG_PRIORITY_INFO); + + litest_with_logcapture(li, capture) { + libinput_plugin_system_load_plugins(li, LIBINPUT_PLUGIN_FLAG_NONE); + litest_drain_events(li); + + _destroy_(litest_device) *device = litest_add_device(li, LITEST_MOUSE); + litest_drain_events(li); + + /* We enabled that one ourselves */ + litest_assert( + libinput_device_pointer_has_button(device->libinput_device, + BTN_STYLUS2)); + + litest_assert_logcapture_no_errors(capture); + + litest_event(device, EV_REL, REL_X, 1); + litest_event(device, EV_REL, REL_Y, 2); + litest_event(device, EV_REL, REL_WHEEL, -1); + litest_event(device, EV_SYN, SYN_REPORT, 0); + litest_dispatch(li); + + litest_assert_logcapture_no_errors(capture); + + litest_assert_strv_substring(capture->infos, "E:131072:1"); + litest_assert_strv_substring(capture->infos, "E:131073:2"); + litest_assert_strv_substring(capture->infos, "E:131074:3"); + litest_assert(!strv_find_substring(capture->infos, "E:131080", NULL)); + } +} +END_TEST + +START_TEST(lua_udev_properties) +{ + _destroy_(tmpdir) *tmpdir = tmpdir_create(NULL); + const char *lua = + "libinput:register({1})\n" + "function prop_printer(device)\n" + " local properties = device:udev_properties()\n" + " for k, v in pairs(properties) do\n" + " log.info(k .. \"=\" .. v)\n" + " end\n" + "end\n" + "libinput:connect(\"new-evdev-device\", prop_printer)\n"; + + _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_INFO) + libinput_log_set_priority(li, LIBINPUT_LOG_PRIORITY_INFO); + + litest_with_logcapture(li, capture) { + libinput_plugin_system_load_plugins(li, LIBINPUT_PLUGIN_FLAG_NONE); + litest_drain_events(li); + + enum litest_device_type which = + litest_test_param_get_i32(test_env->params, "which"); + _destroy_(litest_device) *device = litest_add_device(li, which); + litest_drain_events(li); + + litest_assert_logcapture_no_errors(capture); + + switch (which) { + case LITEST_TRACKPOINT: + litest_assert_strv_substring(capture->infos, + "ID_INPUT_POINTINGSTICK=1"); + _fallthrough_; + case LITEST_MOUSE: + litest_assert_strv_substring(capture->infos, + "ID_INPUT_MOUSE=1"); + break; + case LITEST_GENERIC_MULTITOUCH_SCREEN: + litest_assert_strv_substring(capture->infos, + "ID_INPUT_TOUCHSCREEN=1"); + break; + default: + litest_assert_not_reached(); + break; + } + litest_assert(!strv_find_substring(capture->infos, + "ID_INPUT_WIDTH_MM", + NULL)); + litest_assert(!strv_find_substring(capture->infos, + "ID_INPUT_WIDTH_MM", + NULL)); + } +} +END_TEST + +START_TEST(lua_append_prepend_frame) +{ + bool append = litest_test_param_get_bool(test_env->params, "append"); + bool in_timer = litest_test_param_get_bool(test_env->params, "in_timer"); + _destroy_(tmpdir) *tmpdir = tmpdir_create(NULL); + _autofree_ char *lua = strdup_printf( + "libinput:register({1})\n" + "mydev = nil\n" + "function frame_handler(device, frame, timestamp)\n" + " device:%s_frame({{ usage = evdev.BTN_LEFT, value = 1}})\n" /* commented + out + if + !in_timer + */ + " return nil\n" + "end\n" + "libinput:connect(\"new-evdev-device\", function(device)\n" + " mydev = device\n" + " %sdevice:connect(\"evdev-frame\", frame_handler)\n" + " %slibinput:timer_set_relative(200000)\n" /* commented out if + !in_timer */ + "end)\n" + "function timer_expired(t)\n" + " mydev:%s_frame({{ usage = evdev.BTN_LEFT, value = 1 }})\n" + "end\n" + "libinput:connect(\"timer-expired\", timer_expired)\n", + append ? "append" : "prepend", + in_timer ? "-- " : "", + in_timer ? "" : "-- ", + append ? "append" : "prepend"); + _autofree_ char *path = litest_write_plugin(tmpdir->path, lua); + _litest_context_destroy_ struct libinput *li = + litest_create_context_with_plugindir(tmpdir->path); + libinput_plugin_system_load_plugins(li, LIBINPUT_PLUGIN_FLAG_NONE); + litest_drain_events(li); + + _destroy_(litest_device) *device = litest_add_device(li, LITEST_MOUSE); + litest_drain_events(li); + msleep(10); /* trigger the timer, if any */ + litest_dispatch(li); + + if (in_timer) { + litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_PRESSED); + } + + litest_event(device, EV_REL, REL_X, 1); + litest_event(device, EV_REL, REL_Y, 2); + litest_event(device, EV_SYN, SYN_REPORT, 0); + litest_dispatch(li); + litest_timeout_debounce(li); + litest_dispatch(li); + + if (!in_timer && !append) { + litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_PRESSED); + } + + _destroy_(libinput_event) *ev = libinput_get_event(li); + litest_is_motion_event(ev); + + if (!in_timer && append) { + litest_assert_button_event(li, BTN_LEFT, LIBINPUT_BUTTON_STATE_PRESSED); + } + + litest_assert_empty_queue(li); +} +END_TEST + +START_TEST(lua_inject_frame) +{ + bool in_timer = litest_test_param_get_bool(test_env->params, "in_timer"); + _destroy_(tmpdir) *tmpdir = tmpdir_create(NULL); + + /* Plugin 1 swaps left to right */ + const char *lua1 = + "libinput:register({1})\n" + "function frame_handler(device, frame, timestamp)\n" + " for _, e in ipairs(frame) do\n" + " if e.usage == evdev.BTN_LEFT then\n" + " e.usage = evdev.BTN_RIGHT\n" + " end\n" + " end\n" + " return frame\n" + "end\n" + "libinput:connect(\"new-evdev-device\", function(device)\n" + " device:connect(\"evdev-frame\", frame_handler)\n" + "end)\n"; + + /* Plugin 2 injects a left button if a middle button is pressed */ + _autofree_ char *lua2 = strdup_printf( + "libinput:register({1})\n" + "mydev = nil\n" + "function frame_handler(device, frame, timestamp)\n" + " for _, e in ipairs(frame) do\n" + " if e.usage == evdev.BTN_SIDE then\n" + " e.usage = evdev.BTN_EXTRA\n" + " end\n" + " if e.usage == evdev.BTN_MIDDLE then\n" + " log.debug(\"Injecting frame BTN_LEFT value \" .. e.value)\n" + " %sdevice:inject_frame({{ usage = evdev.BTN_LEFT, value = e.value }})\n" + " %slibinput:timer_set_relative(200000)\n" /* commented out + if !in_timer */ + " end\n" + " end\n" + " return frame\n" + "end\n" + "function timer_expired(t)\n" + " log.debug(\"Injecting timer BTN_LEFT\")\n" + " mydev:inject_frame({{ usage = evdev.BTN_LEFT, value = 1 }})\n" + "end\n" + "libinput:connect(\"new-evdev-device\", function(device)\n" + " mydev = device\n" + " device:connect(\"evdev-frame\", frame_handler)\n" + "end)\n" + "libinput:connect(\"timer-expired\", timer_expired)\n", + in_timer ? "-- " : "", + in_timer ? "" : "-- "); + + _autofree_ char *p1 = litest_write_plugin(tmpdir->path, lua1); + _autofree_ char *p2 = litest_write_plugin(tmpdir->path, lua2); + _litest_context_destroy_ struct libinput *li = + litest_create_context_with_plugindir(tmpdir->path); + libinput_plugin_system_load_plugins(li, LIBINPUT_PLUGIN_FLAG_NONE); + litest_drain_events(li); + + _destroy_(litest_device) *device = litest_add_device(li, LITEST_CYBORG_RAT); + litest_drain_events(li); + litest_dispatch(li); + + litest_log_group("P1 should swap left to right") { + litest_button_click_debounced(device, li, BTN_LEFT, 1); + litest_button_click_debounced(device, li, BTN_LEFT, 0); + litest_dispatch(li); + + litest_assert_button_event(li, + BTN_RIGHT, + LIBINPUT_BUTTON_STATE_PRESSED); + litest_assert_button_event(li, + BTN_RIGHT, + LIBINPUT_BUTTON_STATE_RELEASED); + litest_drain_events(li); + } + + litest_log_group("P1/P2 should leave BTN_EXTRA untouched") { + litest_button_click_debounced(device, li, BTN_EXTRA, 1); + litest_button_click_debounced(device, li, BTN_EXTRA, 0); + litest_dispatch(li); + + /* This might be a false positive */ + litest_assert_button_event(li, + BTN_EXTRA, + LIBINPUT_BUTTON_STATE_PRESSED); + litest_assert_button_event(li, + BTN_EXTRA, + LIBINPUT_BUTTON_STATE_RELEASED); + } + + litest_log_group("P2 should map BTN_SIDE to BTN_EXTRA") { + litest_button_click_debounced(device, li, BTN_SIDE, 1); + litest_button_click_debounced(device, li, BTN_SIDE, 0); + litest_dispatch(li); + litest_timeout_debounce(li); + litest_dispatch(li); + + litest_assert_button_event(li, + BTN_EXTRA, + LIBINPUT_BUTTON_STATE_PRESSED); + litest_assert_button_event(li, + BTN_EXTRA, + LIBINPUT_BUTTON_STATE_RELEASED); + } + + if (in_timer) { + litest_log_group( + "P2 should inject left on middle via timer, P1 changes that left to right in timer") { + litest_button_click_debounced(device, li, BTN_MIDDLE, 1); + litest_dispatch(li); + + litest_assert_button_event(li, + BTN_MIDDLE, + LIBINPUT_BUTTON_STATE_PRESSED); + litest_assert_button_event(li, + BTN_RIGHT, + LIBINPUT_BUTTON_STATE_PRESSED); + + litest_button_click_debounced(device, li, BTN_MIDDLE, 0); + litest_dispatch(li); + litest_assert_button_event(li, + BTN_MIDDLE, + LIBINPUT_BUTTON_STATE_RELEASED); + /* We only inject BTN_RIGHT down, so the second inject should + * get filtered */ + litest_assert_empty_queue(li); + } + } else { + litest_log_group("P2 tries to inject during frame, gets unloaded") { + litest_set_log_handler_bug(li); + litest_button_click_debounced(device, li, BTN_MIDDLE, 1); + litest_restore_log_handler(li); + litest_button_click_debounced(device, li, BTN_MIDDLE, 0); + litest_dispatch(li); + + litest_assert_button_event(li, + BTN_MIDDLE, + LIBINPUT_BUTTON_STATE_PRESSED); + litest_assert_button_event(li, + BTN_MIDDLE, + LIBINPUT_BUTTON_STATE_RELEASED); + } + + litest_log_group("P2 is unloaded, expect BTN_SIDE") { + litest_button_click_debounced(device, li, BTN_SIDE, 1); + litest_button_click_debounced(device, li, BTN_SIDE, 0); + litest_dispatch(li); + + litest_assert_button_event(li, + BTN_SIDE, + LIBINPUT_BUTTON_STATE_PRESSED); + litest_assert_button_event(li, + BTN_SIDE, + LIBINPUT_BUTTON_STATE_RELEASED); + } + } +} +END_TEST + +TEST_COLLECTION(lua) +{ + /* clang-format off */ + litest_add_no_device(lua_load_failure); + litest_with_parameters(params, + "content", 'I', 6, + litest_named_i32(EMPTY), + litest_named_i32(BASIC), + litest_named_i32(COMMENT), + litest_named_i32(DUPLICATE_CALL), + litest_named_i32(MISSING_REGISTER), + litest_named_i32(VERSION_NOT_A_TABLE)) { + litest_add_parametrized_no_device(lua_load_success_but_no_register, + params); + } + litest_add_no_device(lua_register_noop); + litest_with_parameters(params, "when", 's', 2, "run", "connect") { + litest_add_parametrized_no_device(lua_unregister_is_last, params); + } + litest_add_no_device(lua_test_evdev_global); + litest_add_no_device(lua_test_libinput_now); + litest_with_parameters(params, + "mode", 's', 2, "absolute", "relative", + "reschedule", 'b') { + litest_add_parametrized_no_device(lua_test_libinput_timer, params); + } + + litest_with_parameters(params, + "priority", 'I', 3, + litest_named_i32(LIBINPUT_LOG_PRIORITY_DEBUG), + litest_named_i32(LIBINPUT_LOG_PRIORITY_INFO), + litest_named_i32(LIBINPUT_LOG_PRIORITY_ERROR)) { + litest_add_parametrized_no_device(lua_test_log_global, params); + } + + litest_with_parameters(params, + "handler", 's', 2, "new-evdev-device", "timer-expired", + "error", 'I', 3, + litest_named_i32(BAD_TYPE), + litest_named_i32(TOO_FEW_ARGS), + litest_named_i32(TOO_MANY_ARGS)) { + litest_add_parametrized_no_device(lua_bad_connect, params); + } + + litest_add_no_device(lua_register_multiversions); + litest_add_no_device(lua_allowed_functions); + litest_add_no_device(lua_disallowed_functions); + + litest_add_no_device(lua_frame_handler); + litest_add_no_device(lua_device_info); + litest_add_no_device(lua_set_absinfo); + litest_add_no_device(lua_enable_disable_evdev_usage); + + litest_with_parameters(params, + "which", 'I', 3, + litest_named_i32(LITEST_MOUSE), + litest_named_i32(LITEST_TRACKPOINT), + litest_named_i32(LITEST_GENERIC_MULTITOUCH_SCREEN)) { + litest_add_parametrized_no_device(lua_udev_properties, params); + } + + litest_with_parameters(params, "append", 'b', "in_timer", 'b') { + litest_add_parametrized_no_device(lua_append_prepend_frame, params); + } + + litest_with_parameters(params, "in_timer", 'b') { + litest_add_parametrized_no_device(lua_inject_frame, params); + } + /* clang-format on */ +}