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 */ +}