diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index debca7df5a3ad6b2f77e14805d0e4f147cfa8f86..e1a366e8f658efcb3f0361c4c97221fb4f796f82 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
@@ -930,6 +930,25 @@ vm-valgrind-pointer:
- if: $GITLAB_USER_LOGIN != "marge-bot"
+vm-plugin-tests:
+ extends:
+ - .fedora:42@test-suite-vm
+ variables:
+ SUITE_NAMES: plugins
+
+vm-valgrind-plugin-tests:
+ stage: valgrind
+ extends:
+ - vm-plugin-tests
+ variables:
+ MESON_TEST_ARGS: '--setup=valgrind'
+ LITEST_JOBS: 0
+ retry:
+ max: 2
+ rules:
+ - if: $GITLAB_USER_LOGIN != "marge-bot"
+
+
.fedora-build@template:
extends:
- .fdo.distribution-image@fedora
@@ -977,6 +996,20 @@ build-no-libwacom-nodeps@fedora:42:
before_script:
- dnf remove -y libwacom libwacom-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
diff --git a/.gitlab-ci/ci.template b/.gitlab-ci/ci.template
index e691235d0b92dd6c3a4a7a50eac1478dd6c49ca8..85ca753a40f97d0f437e55ae713f2583a039ee0b 100644
--- a/.gitlab-ci/ci.template
+++ b/.gitlab-ci/ci.template
@@ -481,6 +481,25 @@ vm-valgrind-{{suite.name}}:
- if: $GITLAB_USER_LOGIN != "marge-bot"
{% endfor %}
+
+vm-plugin-tests:
+ extends:
+ - .{{distro.name}}:{{version}}@test-suite-vm
+ variables:
+ SUITE_NAMES: plugins
+
+vm-valgrind-plugin-tests:
+ stage: valgrind
+ extends:
+ - vm-plugin-tests
+ variables:
+ MESON_TEST_ARGS: '--setup=valgrind'
+ LITEST_JOBS: 0
+ retry:
+ max: 2
+ rules:
+ - if: $GITLAB_USER_LOGIN != "marge-bot"
+
{% endfor %}{# for if distro.use_for_qemu_tests #}
{% for distro in distributions if distro.use_for_custom_build_tests %}
@@ -532,6 +551,20 @@ build-no-libwacom-nodeps@{{distro.name}}:{{version}}:
before_script:
- dnf remove -y libwacom libwacom-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 426245945ffb9e16af6c15fc863d1805413a02ad..3e3e7332e3c56075a5134c5af150bfd7681013f4 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"
diff --git a/.gitlab-ci/libinput.spec.in b/.gitlab-ci/libinput.spec.in
index bd0d30dcf8bd68a9e9420d73c1e0b07f8d7cc539..fb78cc51092316d759f55cb4c56204def9656641 100644
--- a/.gitlab-ci/libinput.spec.in
+++ b/.gitlab-ci/libinput.spec.in
@@ -139,6 +139,7 @@ intended to be run by users.
%{_libexecdir}/libinput/libinput-test
%{_libexecdir}/libinput/libinput-test-suite
%{_libexecdir}/libinput/libinput-test-utils
+%{_libexecdir}/libinput/libinput-plugin-test-suite
%{_mandir}/man1/libinput-test.1*
%{_mandir}/man1/libinput-test-suite.1*
diff --git a/doc/user/index.rst b/doc/user/index.rst
index 611e991db9ec9af81ff3dce9532fbd79070825bb..92099ae354320af2d760e8624c7acb6758ba138a 100644
--- a/doc/user/index.rst
+++ b/doc/user/index.rst
@@ -12,6 +12,7 @@
troubleshooting
contributing
development
+ plugins
API documentation
diff --git a/doc/user/meson.build b/doc/user/meson.build
index bad2503ae2604b5f0fca4b1f03d565ec7f0e6049..90b224cdc9a1e0ac87974f9c037d9925541d3d76 100644
--- a/doc/user/meson.build
+++ b/doc/user/meson.build
@@ -153,6 +153,7 @@ src_rst = files(
'middle-button-emulation.rst',
'normalization-of-relative-motion.rst',
'palm-detection.rst',
+ 'plugins.rst',
'pointer-acceleration.rst',
'reporting-bugs.rst',
'scrolling.rst',
diff --git a/doc/user/plugins.rst b/doc/user/plugins.rst
new file mode 100644
index 0000000000000000000000000000000000000000..36b3d4d25147f62ab6e15f0a16edcf8c860bf8c5
--- /dev/null
+++ b/doc/user/plugins.rst
@@ -0,0 +1,532 @@
+.. _plugins:
+
+==============================================================================
+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::
+
+ _VERSION assert error ipairs next pairs tonumber
+ pcall select print tostring type xpcall require
+ 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.
+
+--------------------------------------------------------------------------------
+Lua Plugin API Reference
+--------------------------------------------------------------------------------
+
+
+libinput provides the following globals and types:
+
+.. _plugins_api_evdevframe:
+
+................................................................................
+Evdev frames
+................................................................................
+
+Evdev frames represent a single frame of evdev events for a device. A frame
+has one timestamp and a list of one or more events, terminated by an
+``EV_SYN`` ``SYN_REPORT`` with a value of zero.
+
+.. note:: A ``SYN_REPORT`` event always terminates the event frame, even if
+ the table contains other events after the ``SYN_REPORT``.
+
+In our API they are exposed as nested tables with the following structure:
+
+.. code-block:: lua
+
+ {
+ timestamp = 123456789,
+ events = {
+ { type = evdev.EV_ABS, code = evdev.ABS_X, value = 123 },
+ { type = evdev.EV_ABS, code = evdev.ABS_Y, value = 456 },
+ { type = evdev.EV_SYN, code = evdev.SYN_REPORT, value = 0 }
+ }
+ }
+
+.. 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.
+
+.. _plugins_api_evdevglobal:
+
+................................................................................
+The ``evdev`` global
+................................................................................
+
+The ``evdev`` global represents all known evdev codes by name, effectively in
+the form:
+
+.. code-block:: lua
+
+ evdev = {
+ EV_ABS = 3,
+ ABS_X = 0,
+ ABS_Y = 1,
+ ...
+ EV_REL = 2,
+ REL_X = 0,
+ REL_Y = 1,
+ ...
+ }
+
+
+This global is provided for convenience to improve readability in the code.
+This is a flat table, evdev codes are not grouped by type because each evdev
+code's name already has a type-specific prefix.
+
+See the ``linux/input-event-codes.h`` header file provided by your kernel
+for a list of all evdev types and codes.
+
+Note that evdev codes are only unique by type, e.g. the following assertions
+are true:
+
+.. code-block:: lua
+
+ assert(evdev.REL_X == evdev.ABS_X)
+ assert(evdev.KEY_7 == evdev.ABS_WHEEL)
+
+Never use the evdev code on its own, always use a type and code tuple.
+
+.. _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(version)
+
+ Register this plugin with the given version number and returns the
+ version number actually active for this plugin. The active version number
+ is either the one supported by libinput or the given version number, whichever
+ is lower.
+
+ 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 (plugin, 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:id()
+
+ A unique numeric ID of this device, useful for correlating devices between
+ callbacks.
+
+.. function:: EvdevDevice:bustype()
+
+ The numeric bustype of the device. See the ``BUS_*`` defines in
+ ``linux/input.h`` for the list of possible values.
+
+ Use :ref:`plugins_api_evdevglobal` for better readability, e.g.
+ ``if device:bustype() == evdev.BUS_USB``.
+
+.. function:: EvdevDevice:vid()
+
+ The 16-bit vendor ID of the device
+
+.. function:: EvdevDevice:pid()
+
+ The 16-bit product ID of the device
+
+.. function:: EvdevDevice:name()
+
+ The device name as set by the kernel
+
+.. function:: EvdevDevice:event_codes()
+
+ Returns a nested table of all event types and codes 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.EV_REL = {
+ [evdev.REL_X] = true,
+ [evdev.REL_Y] = true,
+ },
+ }
+
+ All other types and codes are ``nil``, so that the following code
+ is possible:
+
+ .. code-block:: lua
+
+ if codes[evdev.EV_REL] and codes[evdev.EV_REL][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_event_code(type, code)
+
+ Enable the given evdev event code for this device. Use :ref:`plugins_api_evdevglobal`
+ for better readability, e.g. ``device:enable_event_code(evdev.EV_REL, evdev.REL_X)``.
+ This function must not be used for ``EV_ABS`` events, use ``set_absinfo()`` instead.
+
+ If the device has since been discarded by libinput, this function does nothing.
+
+.. function:: EvdevDevice:disable_event_code(type, code)
+
+ Disable the given evdev event code for this device. Use :ref:`plugins_api_evdevglobal`
+ for better readability, e.g. ``device:disable_event_code(evdev.EV_REL, evdev.REL_X)``.
+
+ If the device has since been discarded by libinput, this function does nothing.
+
+.. function:: EvdevDevice:set_absinfo(code, absinfo)
+
+ Set the absolute axis information for the given code, enabling this event code
+ 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 corresonding
+ 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_evdevglobal` 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``, that value is the frame with any modified events.
+
+ .. code-block:: lua
+
+ device:connect("evdev-frame", function (device, frame)
+ -- change any event into a movement left by 1 pixel
+ move_left = {
+ time = frame.time,
+ events = {
+ { type = evdev.EV_REL, code = evdev.REL_X, value = -1, },
+ { type = evdev.EV_SYN, code = evdev.SYN_REPORT, value = 0, }
+ }
+ }
+ return move_left
+ end
+
+ The timestamp of an event frame is read-only, any changes to it will be
+ ignored by libinput.
+
+ 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. ``event_codes()``) 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:frame(frame)
+
+ Inject an :ref:`evdev frame ` into the event stream
+ for this device. This emulates that same event frame being sent by the kernel
+ at the current time. The timestamp in the frame is ignored.
diff --git a/meson.build b/meson.build
index 98e500b200b5b07331f8c49d54d05f0f6f8e3d55..efb2f41de4a44928802d1d947e9fbaedc4874b2f 100644
--- a/meson.build
+++ b/meson.build
@@ -2,7 +2,7 @@ project('libinput', 'c',
version : '1.28.1',
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('.')
@@ -170,6 +170,15 @@ config_h.set10('HAVE_LIBEVDEV_DISABLE_PROPERTY',
dep_lm = cc.find_library('m', required : false)
dep_rt = cc.find_library('rt', required : false)
+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)
+
# Include directories
includes_include = include_directories('include')
includes_src = include_directories('src')
@@ -334,7 +343,7 @@ src_libfilter = [
'src/filter-trackpoint-flat.c',
]
libfilter = static_library('filter', src_libfilter,
- dependencies : [dep_udev, dep_libwacom],
+ dependencies : [dep_udev, dep_libwacom, dep_libevdev],
include_directories : includes_include)
dep_libfilter = declare_dependency(link_with : libfilter)
@@ -370,9 +379,13 @@ endif
############ libinput.so ############
config_h.set10('EVENT_DEBUGGING', get_option('internal-event-debugging'))
+config_h.set_quoted('LIBINPUT_PLUGIN_LIBDIR', dir_lib / 'libinput' / 'plugins')
+config_h.set_quoted('LIBINPUT_PLUGIN_ETCDIR', dir_etc / 'libinput' / 'plugins')
+
install_headers('src/libinput.h')
src_libinput = src_libfilter + [
'src/libinput.c',
+ 'src/libinput-plugin.c',
'src/libinput-private-config.c',
'src/evdev.c',
'src/evdev-debounce.c',
@@ -394,6 +407,11 @@ src_libinput = src_libfilter + [
'src/timer.c',
'src/util-libinput.c',
]
+if dep_lua.found()
+ src_libinput += [
+ 'src/libinput-lua.c',
+ ]
+endif
deps_libinput = [
dep_mtdev,
@@ -404,7 +422,8 @@ deps_libinput = [
dep_rt,
dep_libwacom,
dep_libinput_util,
- dep_libquirks
+ dep_libquirks,
+ dep_lua,
]
libinput_version_h_config = configuration_data()
@@ -452,6 +471,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')
@@ -1021,6 +1042,27 @@ if get_option('tests')
timeout : 1200)
endforeach
+ if have_plugins
+ plugin_test_runner_sources = src_libinput + litest_sources
+ if have_lua
+ plugin_test_runner_sources += [
+ 'test/test-plugins-lua.c',
+ ]
+ endif
+ plugin_test_runner = executable('libinput-plugin-test-suite',
+ plugin_test_runner_sources,
+ include_directories : [includes_src, includes_include],
+ dependencies : deps_litest,
+ install_dir : libinput_tool_path,
+ install : get_option('install-tests'))
+ # libinput-test-suite prefix for easier CI integration
+ test('libinput-test-suite-plugins',
+ plugin_test_runner,
+ suite : ['all', 'valgrind', 'root', 'hardware'],
+ is_parallel : false,
+ timeout: 1200)
+ endif
+
test('libinput-test-deviceless',
libinput_test_runner,
suite : ['all', 'valgrind'],
diff --git a/meson_options.txt b/meson_options.txt
index 047647f7e089523054a4788abffaeb1dcfc32a81..a5c170856b75388eacb686e442fd1cfebc3172f2 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -38,3 +38,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 0000000000000000000000000000000000000000..1f2e496f9d5457b387b760005a9a4750a544a7bc
--- /dev/null
+++ b/plugins/10-delay-motion.lua
@@ -0,0 +1,71 @@
+-- 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 = {}
+
+-- events injected via device:frame() will get sent to the frame callback too
+-- so we need a marker to know that we can ignore this frame.
+is_replaying = false
+
+function timer_expired(plugin, time_in_microseconds)
+ next_timer_expiry = 0
+ for device, frames in pairs(devices) do
+ while #frames > 0 and frames[1].time = 1.0 then
+ local i = math.floor(math.abs(v))
+ local r = math.abs(v) % 1.0
+ if v plugin_system,
+ &device->base,
+ frame);
+
+ for (size_t i = 0; i evdev,
LIBEVDEV_READ_FLAG_SYNC, &ev);
if (rc maxevents really should never happen */
+ evdev_frame_append(frame, &ev, 1);
} while (rc == LIBEVDEV_READ_STATUS_SYNC);
+ evdev_device_dispatch_frame(device, frame);
+
return rc == -EAGAIN ? 0 : rc;
}
@@ -1150,6 +1181,7 @@ evdev_device_dispatch(void *data)
struct input_event ev;
int rc;
bool once = false;
+ _unref_(evdev_frame) *frame = evdev_frame_new(64);
/* If the compositor is repainting, this function is called only once
* per frame and we have to process all the events available on the
@@ -1166,7 +1198,13 @@ evdev_device_dispatch(void *data)
currently pending events before we sync up
to the current state */
ev.code = SYN_REPORT;
- evdev_device_dispatch_one(device, &ev);
+
+ if (evdev_frame_append(frame, &ev, 1) == -ENOMEM) {
+ evdev_log_bug_libinput(device,
+ "event frame overflow, discarding events.\n");
+ }
+ evdev_device_dispatch_frame(device, frame);
+ evdev_frame_reset(frame);
rc = evdev_sync_device(device);
if (rc == 0)
@@ -1176,13 +1214,27 @@ evdev_device_dispatch(void *data)
evdev_note_time_delay(device, &ev);
once = true;
}
- evdev_device_dispatch_one(device, &ev);
+
+ if (evdev_frame_append(frame, &ev, 1) == -ENOMEM) {
+ evdev_log_bug_libinput(device,
+ "event frame overflow, discarding events.\n");
+ }
+ if (ev.type == EV_SYN && ev.code == SYN_REPORT) {
+ evdev_device_dispatch_frame(device, frame);
+ evdev_frame_reset(frame);
+ }
} else if (rc == -ENODEV) {
evdev_device_remove(device);
return;
}
} while (rc == LIBEVDEV_READ_STATUS_SUCCESS);
+ /* This should never happen, the kernel flushes only on SYN_REPORT */
+ if (evdev_frame_get_count(frame) > 1) {
+ evdev_log_bug_kernel(device, "event frame missing SYN_REPORT, forcing frame.\n");
+ evdev_device_dispatch_frame(device, frame);
+ }
+
if (rc != -EAGAIN && rc != -EINTR) {
libinput_remove_source(libinput, device->source);
device->source = NULL;
@@ -1970,36 +2022,13 @@ evdev_device_is_joystick_or_gamepad(struct evdev_device *device)
}
static struct evdev_dispatch *
-evdev_configure_device(struct evdev_device *device)
+evdev_configure_device(struct evdev_device *device,
+ enum evdev_device_udev_tags udev_tags)
{
struct libevdev *evdev = device->evdev;
- enum evdev_device_udev_tags udev_tags;
unsigned int tablet_tags;
struct evdev_dispatch *dispatch;
- udev_tags = evdev_device_get_udev_tags(device, device->udev_device);
-
- if ((udev_tags & EVDEV_UDEV_TAG_INPUT) == 0 ||
- (udev_tags & ~EVDEV_UDEV_TAG_INPUT) == 0) {
- evdev_log_info(device,
- "not tagged as supported input device\n");
- return NULL;
- }
-
- evdev_log_info(device,
- "is tagged by udev as:%s%s%s%s%s%s%s%s%s%s%s\n",
- udev_tags & EVDEV_UDEV_TAG_KEYBOARD ? " Keyboard" : "",
- udev_tags & EVDEV_UDEV_TAG_MOUSE ? " Mouse" : "",
- udev_tags & EVDEV_UDEV_TAG_TOUCHPAD ? " Touchpad" : "",
- udev_tags & EVDEV_UDEV_TAG_TOUCHSCREEN ? " Touchscreen" : "",
- udev_tags & EVDEV_UDEV_TAG_TABLET ? " Tablet" : "",
- udev_tags & EVDEV_UDEV_TAG_POINTINGSTICK ? " Pointingstick" : "",
- udev_tags & EVDEV_UDEV_TAG_JOYSTICK ? " Joystick" : "",
- udev_tags & EVDEV_UDEV_TAG_ACCELEROMETER ? " Accelerometer" : "",
- udev_tags & EVDEV_UDEV_TAG_TABLET_PAD ? " TabletPad" : "",
- udev_tags & EVDEV_UDEV_TAG_TRACKBALL ? " Trackball" : "",
- udev_tags & EVDEV_UDEV_TAG_SWITCH ? " Switch" : "");
-
/* Ignore pure accelerometers, but accept devices that are
* accelerometers with other axes */
if (udev_tags == (EVDEV_UDEV_TAG_INPUT|EVDEV_UDEV_TAG_ACCELEROMETER)) {
@@ -2470,24 +2499,58 @@ evdev_device_create(struct libinput_seat *seat,
evdev_pre_configure_model_quirks(device);
- device->dispatch = evdev_configure_device(device);
- if (device->dispatch == NULL || device->seat_caps == EVDEV_DEVICE_NO_CAPABILITIES)
+ enum evdev_device_udev_tags udev_tags = evdev_device_get_udev_tags(device,
+ device->udev_device);
+ if ((udev_tags & EVDEV_UDEV_TAG_INPUT) == 0 ||
+ (udev_tags & ~EVDEV_UDEV_TAG_INPUT) == 0) {
+ evdev_log_info(device,
+ "not tagged as supported input device\n");
goto err;
+ }
+
+ evdev_log_info(device,
+ "is tagged by udev as:%s%s%s%s%s%s%s%s%s%s%s\n",
+ udev_tags & EVDEV_UDEV_TAG_KEYBOARD ? " Keyboard" : "",
+ udev_tags & EVDEV_UDEV_TAG_MOUSE ? " Mouse" : "",
+ udev_tags & EVDEV_UDEV_TAG_TOUCHPAD ? " Touchpad" : "",
+ udev_tags & EVDEV_UDEV_TAG_TOUCHSCREEN ? " Touchscreen" : "",
+ udev_tags & EVDEV_UDEV_TAG_TABLET ? " Tablet" : "",
+ udev_tags & EVDEV_UDEV_TAG_POINTINGSTICK ? " Pointingstick" : "",
+ udev_tags & EVDEV_UDEV_TAG_JOYSTICK ? " Joystick" : "",
+ udev_tags & EVDEV_UDEV_TAG_ACCELEROMETER ? " Accelerometer" : "",
+ udev_tags & EVDEV_UDEV_TAG_TABLET_PAD ? " TabletPad" : "",
+ udev_tags & EVDEV_UDEV_TAG_TRACKBALL ? " Trackball" : "",
+ udev_tags & EVDEV_UDEV_TAG_SWITCH ? " Switch" : "");
+
+ libinput_plugin_system_notify_device_new(&libinput->plugin_system,
+ &device->base,
+ device->evdev,
+ device->udev_device);
+
+ device->dispatch = evdev_configure_device(device, udev_tags);
+ if (device->dispatch == NULL || device->seat_caps == EVDEV_DEVICE_NO_CAPABILITIES)
+ goto err_notify;
device->source =
libinput_add_fd(libinput, fd, evdev_device_dispatch, device);
if (!device->source)
- goto err;
+ goto err_notify;
if (!evdev_set_device_group(device, udev_device))
- goto err;
+ goto err_notify;
list_insert(seat->devices_list.prev, &device->base.link);
+ device->base.inject_evdev_frame = libinput_device_dispatch_frame;
+
evdev_notify_added_device(device);
return device;
+err_notify:
+ libinput_plugin_system_notify_device_ignored(&libinput->plugin_system,
+ &device->base);
+
err:
if (fd >= 0) {
close_restricted(libinput, fd);
diff --git a/src/evdev.h b/src/evdev.h
index 65d599cede13f122acc083a3f1fcb9164e42da01..e1851ff51c8e6034a04579dc8df64d78f0c31d20 100644
--- a/src/evdev.h
+++ b/src/evdev.h
@@ -778,7 +778,7 @@ evdev_log_msg(struct evdev_device *device,
va_list args;
char buf[1024];
- if (!is_logged(evdev_libinput_context(device), priority))
+ if (!log_is_logged(evdev_libinput_context(device), priority))
return;
/* Anything info and above is user-visible, use the device name */
@@ -812,7 +812,7 @@ evdev_log_msg_ratelimit(struct evdev_device *device,
enum ratelimit_state state;
- if (!is_logged(evdev_libinput_context(device), priority))
+ if (!log_is_logged(evdev_libinput_context(device), priority))
return;
state = ratelimit_test(ratelimit);
diff --git a/src/libinput-log.h b/src/libinput-log.h
new file mode 100644
index 0000000000000000000000000000000000000000..c7bb0f89071794a65653b6d81e2fc9867f9d1693
--- /dev/null
+++ b/src/libinput-log.h
@@ -0,0 +1,70 @@
+/*
+ * Copyright © 2013 Jonas Ã
dahl
+ * Copyright © 2013-2015 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
+
+#include "util-ratelimit.h"
+#include "libinput.h"
+
+#define log_debug(li_, ...) log_msg((li_), LIBINPUT_LOG_PRIORITY_DEBUG, __VA_ARGS__)
+#define log_info(li_, ...) log_msg((li_), LIBINPUT_LOG_PRIORITY_INFO, __VA_ARGS__)
+#define log_error(li_, ...) log_msg((li_), LIBINPUT_LOG_PRIORITY_ERROR, __VA_ARGS__)
+#define log_bug_kernel(li_, ...) log_msg((li_), LIBINPUT_LOG_PRIORITY_ERROR, "kernel bug: " __VA_ARGS__)
+#define log_bug_libinput(li_, ...) log_msg((li_), LIBINPUT_LOG_PRIORITY_ERROR, "libinput bug: " __VA_ARGS__)
+#define log_bug_client(li_, ...) log_msg((li_), LIBINPUT_LOG_PRIORITY_ERROR, "client bug: " __VA_ARGS__)
+
+#define log_debug_ratelimit(li_, r_, ...) log_msg_ratelimit((li_), (r_), LIBINPUT_LOG_PRIORITY_DEBUG, __VA_ARGS__)
+#define log_info_ratelimit(li_, r_, ...) log_msg_ratelimit((li_), (r_), LIBINPUT_LOG_PRIORITY_INFO, __VA_ARGS__)
+#define log_error_ratelimit(li_, r_, ...) log_msg_ratelimit((li_), (r_), LIBINPUT_LOG_PRIORITY_ERROR, __VA_ARGS__)
+#define log_bug_kernel_ratelimit(li_, r_, ...) log_msg_ratelimit((li_), (r_), LIBINPUT_LOG_PRIORITY_ERROR, "kernel bug: " __VA_ARGS__)
+#define log_bug_libinput_ratelimit(li_, r_, ...) log_msg_ratelimit((li_), (r_), LIBINPUT_LOG_PRIORITY_ERROR, "libinput bug: " __VA_ARGS__)
+#define log_bug_client_ratelimit(li_, r_, ...) log_msg_ratelimit((li_), (r_), LIBINPUT_LOG_PRIORITY_ERROR, "client bug: " __VA_ARGS__)
+
+bool
+log_is_logged(const struct libinput *libinput,
+ enum libinput_log_priority priority);
+
+void
+log_msg_ratelimit(struct libinput *libinput,
+ struct ratelimit *ratelimit,
+ enum libinput_log_priority priority,
+ const char *format, ...)
+ LIBINPUT_ATTRIBUTE_PRINTF(4, 5);
+
+void
+log_msg(struct libinput *libinput,
+ enum libinput_log_priority priority,
+ const char *format, ...)
+ LIBINPUT_ATTRIBUTE_PRINTF(3, 4);
+
+void
+log_msg_va(struct libinput *libinput,
+ enum libinput_log_priority priority,
+ const char *format,
+ va_list args)
+ LIBINPUT_ATTRIBUTE_PRINTF(3, 0);
diff --git a/src/libinput-lua.c b/src/libinput-lua.c
new file mode 100644
index 0000000000000000000000000000000000000000..31712c648173963e4b9b53ac7971ade9e2a4b6eb
--- /dev/null
+++ b/src/libinput-lua.c
@@ -0,0 +1,1309 @@
+/*
+ * 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 "util-input-event.h"
+
+#include "libinput-lua.h"
+#include "libinput-log.h"
+#include "libinput-util.h"
+#include "libinput-plugin.h"
+#include "timer.h"
+
+static const int LIBINPUT_PLUGIN_VERSION = 1;
+
+#define PLUGIN_METATABLE "LibinputPlugin"
+#define EVDEV_DEVICE_METATABLE "EvdevDevice"
+#define EVDEV_FRAME_METATABLE "EvdevFrame"
+
+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; /* for comparison only */
+
+ unsigned int id;
+ unsigned int bustype;
+ unsigned int vid;
+ unsigned int pid;
+ char *name;
+ struct list udev_properties_list;
+
+ struct libevdev *evdev;
+
+ int device_added_refid;
+ int device_removed_refid;
+ int frame_refid;
+} EvdevDevice;
+
+typedef struct {
+ uint64_t time;
+ struct input_event events[64];
+} EvdevFrame;
+
+struct libinput_lua_plugin {
+ struct libinput_plugin *parent;
+ lua_State *L;
+ bool register_called;
+ int refid;
+
+ struct list evdev_devices; /* EvdevDevice */
+
+ size_t version;
+ int device_new_refid;
+ int timer_expired_refid;
+
+ struct libinput_timer timer;
+};
+
+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;
+}
+
+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 = 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_added_refid = LUA_NOREF;
+ 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)
+{
+ uint64_t time = evdev_frame_get_time(frame);
+ size_t nevents;
+ struct input_event *events = evdev_frame_get_events(frame, &nevents);
+
+ lua_newtable(L);
+ lua_pushinteger(L, time);
+ lua_setfield(L, -2, "time");
+
+ lua_newtable(L); /* { { "type" = EV_REL, "code" = REL_X, "value" = 1 }, ...} */
+ for (size_t i = 0; i type);
+ lua_setfield(L, -2, "type");
+ lua_pushinteger(L, e->code);
+ lua_setfield(L, -2, "code");
+ lua_pushinteger(L, e->value);
+ lua_setfield(L, -2, "value");
+ lua_rawseti(L, -2, i + 1);
+
+ if (e->type == EV_SYN && e->code == SYN_REPORT)
+ break;
+ }
+ lua_setfield(L, -2, "events");
+}
+
+static void
+lua_pop_evdev_frame(struct libinput_lua_plugin *plugin, struct evdev_frame *frame_out)
+{
+ _unref_(evdev_frame) *frame = evdev_frame_new(64);
+ lua_State *L = plugin->L;
+
+ if (lua_isnil(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;
+ }
+
+ uint64_t time = evdev_frame_get_time(frame_out);
+ lua_getfield(L, -1, "time");
+ if (lua_isinteger(L, -1))
+ time = luaL_checkinteger(L, -1);
+ lua_pop(L, 1);
+
+ lua_getfield(L, -1, "events");
+ if (lua_istable(L, -1)) {
+ lua_pushnil(L);
+ while (lua_next(L, -2) != 0) {
+ unsigned int type, code;
+ int value;
+
+ /* -2 is the index, -1 our { type = ... code = } 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, "type");
+ type = luaL_checkinteger(L, -1);
+ lua_pop(L, 1);
+
+ lua_getfield(L, -1, "code");
+ code = luaL_checkinteger(L, -1);
+ lua_pop(L, 1);
+
+ lua_getfield(L, -1, "value");
+ value = luaL_checkinteger(L, -1);
+ lua_pop(L, 1);
+
+ lua_pop(L, 1); /* pop { type = ..., code = ..., value = ...} */
+
+ struct input_event e = {
+ .type = type,
+ .code = code,
+ .value = value,
+ };
+ input_event_set_time(&e, time);
+ if (evdev_frame_append(frame, &e, 1) == -ENOMEM) {
+ plugin_log_bug(plugin->parent,
+ "too many events in frame");
+ }
+
+ if (e.type == EV_SYN && e.code == SYN_REPORT) {
+ lua_pop(L, 1); /* force-pop the nil */
+ break;
+ }
+ }
+ }
+ lua_pop(L, 1); /* pop events table */
+
+ size_t nevents;
+ struct input_event *events = evdev_frame_get_events(frame, &nevents);
+ evdev_frame_set(frame_out, events, nevents);
+}
+
+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(plugin->parent, "unloading after error: %s\n", errormsg);
+ }
+ lua_pop(L, 1); /* pop error message */
+
+ libinput_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_rawgeti(plugin->L, LUA_REGISTRYINDEX, plugin->refid);
+ lua_push_evdev_device(plugin->L, plugin, device, evdev, udev_device);
+
+ libinput_lua_pcall(plugin, 2, 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;
+
+ /* 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);
+
+ if (!libinput_lua_pcall(plugin, 2, 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 {
+ luaL_error(L, "Unknown name: %s", name);
+ return 0;
+ }
+
+ 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) {
+ luaL_error(L, "Plugin already registered");
+ return 0;
+ }
+
+ int version = luaL_checkinteger(L, -1);
+ plugin->version = min(version, LIBINPUT_PLUGIN_VERSION);
+ plugin->register_called = true;
+
+ plugin->refid = luaL_ref(L, LUA_REGISTRYINDEX); /* ref to plugin */
+
+ lua_pushinteger(L, plugin->version);
+
+ return 1;
+}
+
+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 */
+ luaL_error(L, "@@unregistering@@");
+
+ return 0;
+}
+
+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)
+ libinput_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;
+}
+
+static void
+plugin_timer_func(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_rawgeti(L, LUA_REGISTRYINDEX, plugin->refid);
+ lua_pushinteger(L, now);
+ libinput_lua_pcall(plugin, 2, 0);
+}
+
+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.timer_name) {
+ struct libinput *libinput = lua_get_libinput(L);
+ libinput_timer_init(&plugin->timer,
+ libinput,
+ libinput_plugin_get_name(plugin->parent),
+ plugin_timer_func,
+ plugin);
+ }
+
+ libinput_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)
+ libinput_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_id(lua_State *L)
+{
+ EvdevDevice *device = luaL_checkudata(L, 1, EVDEV_DEVICE_METATABLE);
+ luaL_argcheck(L, device != NULL, 1, EVDEV_DEVICE_METATABLE "expected");
+
+ lua_pushinteger(L, device->id);
+
+ return 1;
+}
+
+static int
+evdevdevice_bustype(lua_State *L)
+{
+ EvdevDevice *device = luaL_checkudata(L, 1, EVDEV_DEVICE_METATABLE);
+ luaL_argcheck(L, device != NULL, 1, EVDEV_DEVICE_METATABLE "expected");
+
+ lua_pushinteger(L, device->bustype);
+
+ return 1;
+}
+
+static int
+evdevdevice_vid(lua_State *L)
+{
+ EvdevDevice *device = luaL_checkudata(L, 1, EVDEV_DEVICE_METATABLE);
+ luaL_argcheck(L, device != NULL, 1, EVDEV_DEVICE_METATABLE "expected");
+
+ lua_pushinteger(L, device->vid);
+
+ return 1;
+}
+
+static int
+evdevdevice_pid(lua_State *L)
+{
+ EvdevDevice *device = luaL_checkudata(L, 1, EVDEV_DEVICE_METATABLE);
+ luaL_argcheck(L, device != NULL, 1, EVDEV_DEVICE_METATABLE "expected");
+
+ lua_pushinteger(L, device->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_event_codes(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); /* { EV_REL: { ... }, ... } */
+
+ if (device->evdev == NULL)
+ return 1;
+
+ for (unsigned int t = 0; t evdev, t))
+ continue;
+
+ lua_newtable(L);
+ int max = libevdev_event_type_get_max(t);
+ for (unsigned int code = 0; (int)code evdev, t, code))
+ continue;
+
+ lua_pushboolean(L, true);
+ lua_rawseti(L, -2, code);
+ }
+
+ lua_rawseti(L, -2, t); /* Assign to top-level table */
+ }
+
+ 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 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");
+
+ lua_rawseti(L, -2, code); /* 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_event_code(lua_State *L)
+{
+ EvdevDevice *device = luaL_checkudata(L, 1, EVDEV_DEVICE_METATABLE);
+ luaL_argcheck(L, device != NULL, 1, EVDEV_DEVICE_METATABLE "expected");
+
+ int type = luaL_checkinteger(L, 2);
+ int code = luaL_checkinteger(L, 3);
+
+ if (device->evdev == NULL || type == EV_ABS)
+ return 0;
+
+ libevdev_enable_event_code(device->evdev, type, code, NULL);
+
+ return 0;
+}
+
+static int
+evdevdevice_disable_event_code(lua_State *L)
+{
+ EvdevDevice *device = luaL_checkudata(L, 1, EVDEV_DEVICE_METATABLE);
+ luaL_argcheck(L, device != NULL, 1, EVDEV_DEVICE_METATABLE "expected");
+
+ int type = luaL_checkinteger(L, 2);
+ int code = luaL_checkinteger(L, 3);
+
+ if (device->evdev == NULL)
+ 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");
+
+ int code = luaL_checkinteger(L, 1);
+ luaL_checktype(L, 2, LUA_TTABLE);
+
+ if (!device->evdev)
+ return 0;
+
+ const struct input_absinfo *absinfo =
+ libevdev_get_abs_info(device->evdev, code);
+ struct input_absinfo abs = {};
+ if (absinfo)
+ abs = *absinfo;
+
+ lua_getfield(L, 2, "minimum");
+ if (lua_isnumber(L, -1))
+ abs.minimum = luaL_checkinteger(L, -1);
+ lua_getfield(L, 2, "maximum");
+ if (lua_isnumber(L, -1))
+ abs.maximum = luaL_checkinteger(L, -1);
+ lua_getfield(L, 2, "resolution");
+ if (lua_isnumber(L, -1))
+ abs.resolution = luaL_checkinteger(L, -1);
+ lua_getfield(L, 2, "fuzz");
+ if (lua_isnumber(L, -1))
+ abs.fuzz = luaL_checkinteger(L, -1);
+ lua_getfield(L, 2, "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")) {
+ register_func(L, 3, &device->frame_refid);
+ } else {
+ luaL_error(L, "Unknown name: %s", name);
+ return 0;
+ }
+
+ 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);
+ luaL_checktype(L, 3, LUA_TFUNCTION);
+
+ /* 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")) {
+ unregister_func(L, &device->frame_refid);
+ } else {
+ luaL_error(L, "Unknown name: %s", name);
+ return 0;
+ }
+
+ return 0;
+}
+
+static int
+evdevdevice_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 = 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);
+
+ /* FIXME: need to really ensure that the device can never be dangling */
+ libinput_plugin_inject_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 [] = {
+ { "id", evdevdevice_id },
+ { "bustype", evdevdevice_bustype },
+ { "vid", evdevdevice_vid },
+ { "pid", evdevdevice_pid },
+ { "name", evdevdevice_name },
+ { "event_codes", evdevdevice_event_codes },
+ { "absinfos", evdevdevice_absinfos },
+ { "udev_properties", evdevdevice_udev_properties },
+ { "enable_event_code", evdevdevice_enable_event_code },
+ { "disable_event_code", evdevdevice_disable_event_code },
+ { "set_absinfo", evdevdevice_set_absinfo },
+ { "connect", evdevdevice_connect },
+ { "disconnect", evdevdevice_disconnect },
+ { "frame", evdevdevice_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
+evdevframe_time(lua_State *L)
+{
+ EvdevFrame *frame = luaL_checkudata(L, 1, EVDEV_FRAME_METATABLE);
+ luaL_argcheck(L, frame != NULL, 1, EVDEV_FRAME_METATABLE "expected");
+
+ lua_pushinteger(L, frame->time);
+
+ return 1;
+}
+
+static int
+evdevframe_events(lua_State *L)
+{
+ EvdevFrame *frame = luaL_checkudata(L, 1, EVDEV_FRAME_METATABLE);
+ luaL_argcheck(L, frame != NULL, 1, EVDEV_FRAME_METATABLE "expected");
+
+ lua_newtable(L); /* { { "type" = EV_REL, "code" = REL_X, "value" = 1 }, ...} */
+
+ for (size_t i = 0; i events); i++) {
+ struct input_event *e = &frame->events[i];
+
+ lua_newtable(L);
+ lua_pushinteger(L, e->type);
+ lua_setfield(L, -2, "type");
+ lua_pushinteger(L, e->code);
+ lua_setfield(L, -2, "code");
+ lua_pushinteger(L, e->value);
+ lua_setfield(L, -2, "value");
+ lua_rawseti(L, -2, i + 1);
+
+ if (e->type == EV_SYN && e->code == SYN_REPORT)
+ break;
+ }
+
+ return 1;
+}
+
+static const struct luaL_Reg evdevframe_vtable [] = {
+ { "time", evdevframe_time },
+ { "events", evdevframe_events },
+ { NULL, NULL}
+};
+
+static void
+evdevframe_init(lua_State *L)
+{
+ luaL_newmetatable(L, EVDEV_FRAME_METATABLE);
+ lua_pushstring(L, "__index");
+ lua_pushvalue(L, -2); /* push metatable */
+ lua_settable(L, -3); /* metatable.__index = metatable */
+ luaL_setfuncs(L, evdevframe_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)
+{
+ libinput_timer_cancel(&plugin->timer);
+
+ EvdevDevice *evdev;
+ list_for_each_safe(evdev, &plugin->evdev_devices, link) {
+ remove_device(plugin, evdev);
+ }
+
+ libinput_timer_destroy(&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);
+ 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);
+
+ 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)
+{
+ lua_newtable(L);
+ for (unsigned int t = 0; t func; lib++) {
+ luaL_requiref(L, lib->name, lib->func, 1);
+ lua_pop(L, 1);
+ }
+
+ /* Our objects */
+ libinputplugin_init(L);
+ evdevdevice_init(L);
+ evdevframe_init(L);
+
+ /* Our globals */
+ luaL_newlib(L, log_funcs);
+ lua_setglobal(L, "log");
+ libinput_lua_init_evdev_global(L);
+
+ /* The libinput global object */
+ lua_newuserdata(L, sizeof(LibinputPlugin));
+ luaL_getmetatable(L, PLUGIN_METATABLE);
+ lua_setmetatable(L, -2);
+ lua_setglobal(L, "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);
+
+ /* Now that we finished our setup, disable anything that could
+ * be remotely unsafe.
+ * http://lua-users.org/wiki/SandBoxes
+ *
+ * A better way is to only allow the functions we want rather
+ * than hoping the below is exhaustive.
+ */
+ static const char *disabled_functions[] = {
+ "load",
+ "loadstring",
+ "loadfile",
+ "rawequal",
+ "rawget",
+ "rawset",
+ "setfenv",
+ "setmetatable",
+ "module",
+ "require",
+ };
+ ARRAY_FOR_EACH(disabled_functions, func) {
+ lua_pushnil(L);
+ lua_setglobal(L, *func);
+ }
+
+ 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));
+
+ plugin->parent = NULL;
+ 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);
+
+ _cleanup_(lua_closep) lua_State *L = libinput_lua_plugin_init_lua(libinput, plugin);
+ if (!L) {
+ log_bug_libinput(libinput, "Failed to create lua state for %s\n", name);
+ return NULL;
+ }
+
+ int ret = luaL_loadfile(L, path);
+ if (ret == LUA_OK) {
+ plugin->L = steal(&L);
+
+ /* 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;
+ 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 */
+
+ return NULL;
+ }
+}
diff --git a/src/libinput-lua.h b/src/libinput-lua.h
new file mode 100644
index 0000000000000000000000000000000000000000..8551d2b86d6bcd3b960d87c5d3da1c42ae896e6c
--- /dev/null
+++ b/src/libinput-lua.h
@@ -0,0 +1,33 @@
+/*
+ * 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.h"
+#include "libinput-plugin.h"
+
+struct libinput_plugin *
+libinput_lua_plugin_new_from_path(struct libinput *libinput,
+ const char *path);
diff --git a/src/libinput-plugin-private.h b/src/libinput-plugin-private.h
new file mode 100644
index 0000000000000000000000000000000000000000..32d456495bdcd78386c1533ab5b3aaab6b939511
--- /dev/null
+++ b/src/libinput-plugin-private.h
@@ -0,0 +1,54 @@
+/*
+ * 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"
+
+void
+libinput_plugin_run(struct libinput_plugin *plugin);
+
+void
+libinput_plugin_notify_device_new(struct libinput_plugin *plugin,
+ struct libinput_device *device,
+ struct libevdev *evdev,
+ struct udev_device *udev_device);
+
+void
+libinput_plugin_notify_device_added(struct libinput_plugin *plugin,
+ struct libinput_device *device);
+
+void
+libinput_plugin_notify_device_ignored(struct libinput_plugin *plugin,
+ struct libinput_device *device);
+
+void
+libinput_plugin_notify_device_removed(struct libinput_plugin *plugin,
+ struct libinput_device *device);
+
+void
+libinput_plugin_notify_evdev_frame(struct libinput_plugin *plugin,
+ struct libinput_device *device,
+ struct evdev_frame *frame);
diff --git a/src/libinput-plugin-system.h b/src/libinput-plugin-system.h
new file mode 100644
index 0000000000000000000000000000000000000000..aa993654bf8d67b450740cc9e44042a2a1e0f283
--- /dev/null
+++ b/src/libinput-plugin-system.h
@@ -0,0 +1,84 @@
+/*
+ * 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
+#include
+#include
+
+#include "util-input-event.h"
+#include "util-list.h"
+
+#include "libinput.h"
+
+struct libinput_plugin;
+
+struct libinput_plugin_system {
+ char **directories; /* NULL once loaded == true */
+ bool loaded;
+
+ struct list plugins;
+ struct list removed_plugins;
+};
+
+void
+libinput_plugin_system_init(struct libinput_plugin_system *system);
+
+void
+libinput_plugin_system_destroy(struct libinput_plugin_system *system);
+
+void
+libinput_plugin_system_run(struct libinput_plugin_system *system);
+
+void
+libinput_plugin_system_register_plugin(struct libinput_plugin_system *system,
+ struct libinput_plugin *plugin);
+void
+libinput_plugin_system_unregister_plugin(struct libinput_plugin_system *system,
+ struct libinput_plugin *plugin);
+
+void
+libinput_plugin_system_notify_device_new(struct libinput_plugin_system *system,
+ struct libinput_device *device,
+ struct libevdev *evdev,
+ struct udev_device *udev);
+
+void
+libinput_plugin_system_notify_device_added(struct libinput_plugin_system *system,
+ struct libinput_device *device);
+
+void
+libinput_plugin_system_notify_device_removed(struct libinput_plugin_system *system,
+ struct libinput_device *device);
+
+void
+libinput_plugin_system_notify_device_ignored(struct libinput_plugin_system *system,
+ struct libinput_device *device);
+
+void
+libinput_plugin_system_notify_evdev_frame(struct libinput_plugin_system *system,
+ struct libinput_device *device,
+ struct evdev_frame *frame);
diff --git a/src/libinput-plugin.c b/src/libinput-plugin.c
new file mode 100644
index 0000000000000000000000000000000000000000..18294968d1169ca121609382483f3851935e59cb
--- /dev/null
+++ b/src/libinput-plugin.c
@@ -0,0 +1,393 @@
+/*
+ * 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 "util-files.h"
+#include "util-list.h"
+
+#include "libinput-plugin.h"
+#include "libinput-plugin-private.h"
+#include "libinput-plugin-system.h"
+#if HAVE_LUA
+#include "libinput-lua.h"
+#endif
+
+#include "libinput-util.h"
+#include "libinput-private.h"
+
+struct libinput_plugin {
+ struct libinput *libinput;
+ char *name;
+ int refcount;
+ struct list link;
+ void *user_data;
+
+ bool registered;
+
+ const struct libinput_plugin_interface *interface;
+};
+
+LIBINPUT_ATTRIBUTE_PRINTF(3, 4)
+void
+plugin_log_msg(struct libinput_plugin *plugin,
+ enum libinput_log_priority priority,
+ const char *format,
+ ...)
+{
+
+ if (!log_is_logged(plugin->libinput, priority))
+ return;
+
+ _autofree_ char *prefix = strdup_printf("Plugin:%-15s - ", plugin->name);
+ va_list args;
+ va_start(args, format);
+ _autofree_ char *message = strdup_vprintf(format, args);
+ va_end(args);
+
+ log_msg(plugin->libinput, priority, "%s%s", prefix, message);
+}
+
+struct libinput_plugin *
+libinput_plugin_new(struct libinput *libinput,
+ const char *name,
+ const struct libinput_plugin_interface *interface,
+ void *user_data)
+{
+ struct libinput_plugin *plugin = zalloc(sizeof(*plugin));
+
+ plugin->registered = true;
+ plugin->libinput = libinput;
+ plugin->refcount = 1;
+ plugin->interface = interface;
+ plugin->user_data = user_data;
+ plugin->name = strdup(name);
+
+ libinput_plugin_system_register_plugin(&libinput->plugin_system, plugin);
+
+ return plugin;
+}
+
+void
+libinput_plugin_unregister(struct libinput_plugin *plugin)
+{
+ struct libinput *libinput = plugin->libinput;
+ if (!plugin->registered)
+ return;
+
+ plugin->registered = false;
+
+ libinput_plugin_system_unregister_plugin(&libinput->plugin_system,
+ plugin);
+}
+
+struct libinput_plugin *
+libinput_plugin_ref(struct libinput_plugin *plugin)
+{
+ assert(plugin->refcount > 0);
+ ++plugin->refcount;
+ return plugin;
+}
+
+struct libinput_plugin *
+libinput_plugin_unref(struct libinput_plugin *plugin)
+{
+ assert(plugin->refcount > 0);
+ if (--plugin->refcount == 0) {
+ list_remove(&plugin->link);
+ if (plugin->interface->destroy)
+ plugin->interface->destroy(plugin);
+ free(plugin->name);
+ free(plugin);
+ }
+ return NULL;
+}
+
+void
+libinput_plugin_set_user_data(struct libinput_plugin *plugin,
+ void *user_data)
+{
+ plugin->user_data = user_data;
+}
+
+void *
+libinput_plugin_get_user_data(struct libinput_plugin *plugin)
+{
+ return plugin->user_data;
+}
+
+const char *
+libinput_plugin_get_name(struct libinput_plugin *plugin)
+{
+ return plugin->name;
+}
+
+struct libinput *
+libinput_plugin_get_context(struct libinput_plugin *plugin)
+{
+ return plugin->libinput;
+}
+
+void
+libinput_plugin_inject_evdev_frame(struct libinput_plugin *plugin,
+ struct libinput_device *device,
+ struct evdev_frame *frame)
+{
+ if (device->inject_evdev_frame)
+ device->inject_evdev_frame(device, frame);
+}
+
+void
+libinput_plugin_run(struct libinput_plugin *plugin)
+{
+ if (plugin->interface->run)
+ plugin->interface->run(plugin);
+}
+
+void
+libinput_plugin_notify_device_new(struct libinput_plugin *plugin,
+ struct libinput_device *device,
+ struct libevdev *evdev,
+ struct udev_device *udev_device)
+{
+ if (plugin->interface->device_new)
+ plugin->interface->device_new(plugin, device, evdev, udev_device);
+}
+
+void
+libinput_plugin_notify_device_added(struct libinput_plugin *plugin,
+ struct libinput_device *device)
+{
+ if (plugin->interface->device_added)
+ plugin->interface->device_added(plugin, device);
+}
+
+void
+libinput_plugin_notify_device_ignored(struct libinput_plugin *plugin,
+ struct libinput_device *device)
+{
+ if (plugin->interface->device_ignored)
+ plugin->interface->device_ignored(plugin, device);
+}
+
+void
+libinput_plugin_notify_device_removed(struct libinput_plugin *plugin,
+ struct libinput_device *device)
+{
+ if (plugin->interface->device_removed)
+ plugin->interface->device_removed(plugin, device);
+}
+
+void
+libinput_plugin_notify_evdev_frame(struct libinput_plugin *plugin,
+ struct libinput_device *device,
+ struct evdev_frame *frame)
+{
+ if (plugin->interface->evdev_frame)
+ plugin->interface->evdev_frame(plugin, device, frame);
+}
+
+LIBINPUT_EXPORT void
+libinput_plugin_system_append_path(struct libinput *libinput,
+ const char *path)
+{
+ if (libinput->plugin_system.loaded) {
+ log_bug_client(libinput,
+ "plugin system already initialized\n");
+ return;
+ }
+
+ if (strv_find(libinput->plugin_system.directories, path, NULL))
+ return;
+
+ libinput->plugin_system.directories =
+ strv_append_strdup(libinput->plugin_system.directories, path);
+}
+
+LIBINPUT_EXPORT void
+libinput_plugin_system_append_default_paths(struct libinput *libinput)
+{
+ if (libinput->plugin_system.loaded) {
+ log_bug_client(libinput,
+ "plugin system already initialized\n");
+ return;
+ }
+
+ libinput_plugin_system_append_path(libinput, LIBINPUT_PLUGIN_ETCDIR);
+ libinput_plugin_system_append_path(libinput, LIBINPUT_PLUGIN_LIBDIR);
+}
+
+LIBINPUT_EXPORT void
+libinput_plugin_system_load_plugins(struct libinput *libinput,
+ enum libinput_plugins_flags flags)
+{
+ if (libinput->plugin_system.loaded) {
+ log_bug_client(libinput,
+ "%s() called twice\n", __func__);
+ return;
+ }
+ libinput->plugin_system.loaded = true;
+
+ _autostrvfree_ char **directories = steal(&libinput->plugin_system.directories);
+#if !HAVE_PLUGINS
+ log_info(libinput,
+ "libinput was built without plugin support. "
+ "No plugins will be loaded.\n");
+#endif
+
+#if HAVE_LUA
+ size_t nfiles = 0;
+ _autostrvfree_ char **plugin_files = list_files((const char **)directories,
+ ".lua",
+ &nfiles);
+ for (size_t i = 0; i plugin_system);
+}
+
+void
+libinput_plugin_system_run(struct libinput_plugin_system *system)
+{
+ struct libinput_plugin *plugin;
+ list_for_each_safe(plugin,
+ &system->plugins,
+ link) {
+ libinput_plugin_run(plugin);
+ }
+}
+
+void
+libinput_plugin_system_register_plugin(struct libinput_plugin_system *system,
+ struct libinput_plugin *plugin)
+{
+ libinput_plugin_ref(plugin);
+ list_append(&system->plugins, &plugin->link);
+}
+
+void
+libinput_plugin_system_unregister_plugin(struct libinput_plugin_system *system,
+ struct libinput_plugin *plugin)
+{
+ struct libinput_plugin *p;
+ list_for_each(p, &system->plugins, link) {
+ if (p == plugin) {
+ list_remove(&plugin->link);
+ list_append(&system->removed_plugins, &plugin->link);
+ return;
+ }
+ }
+}
+
+static void
+libinput_plugin_system_drop_unregistered_plugins(struct libinput_plugin_system *system)
+{
+ struct libinput_plugin *plugin;
+ list_for_each_safe(plugin, &system->removed_plugins, link) {
+ libinput_plugin_unref(plugin);
+ }
+}
+
+void
+libinput_plugin_system_init(struct libinput_plugin_system *system)
+{
+ list_init(&system->plugins);
+ list_init(&system->removed_plugins);
+}
+
+void
+libinput_plugin_system_destroy(struct libinput_plugin_system *system)
+{
+ struct libinput_plugin *plugin;
+ list_for_each_safe(plugin, &system->plugins, link) {
+ libinput_plugin_unregister(plugin);
+ }
+
+ libinput_plugin_system_drop_unregistered_plugins(system);
+
+ strv_free(system->directories);
+}
+
+void
+libinput_plugin_system_notify_device_new(struct libinput_plugin_system *system,
+ struct libinput_device *device,
+ struct libevdev *evdev,
+ struct udev_device *udev_device)
+{
+ struct libinput_plugin *plugin;
+ list_for_each_safe(plugin, &system->plugins, link) {
+ libinput_plugin_notify_device_new(plugin, device, evdev, udev_device);
+ }
+ libinput_plugin_system_drop_unregistered_plugins(system);
+}
+
+void
+libinput_plugin_system_notify_device_added(struct libinput_plugin_system *system,
+ struct libinput_device *device)
+{
+ struct libinput_plugin *plugin;
+ list_for_each_safe(plugin, &system->plugins, link) {
+ libinput_plugin_notify_device_added(plugin, device);
+ }
+ libinput_plugin_system_drop_unregistered_plugins(system);
+}
+
+void
+libinput_plugin_system_notify_device_removed(struct libinput_plugin_system *system,
+ struct libinput_device *device)
+{
+ struct libinput_plugin *plugin;
+ list_for_each_safe(plugin, &system->plugins, link) {
+ libinput_plugin_notify_device_removed(plugin, device);
+ }
+ libinput_plugin_system_drop_unregistered_plugins(system);
+}
+
+void
+libinput_plugin_system_notify_device_ignored(struct libinput_plugin_system *system,
+ struct libinput_device *device)
+{
+ struct libinput_plugin *plugin;
+ list_for_each_safe(plugin, &system->plugins, link) {
+ libinput_plugin_notify_device_ignored(plugin, device);
+ }
+ libinput_plugin_system_drop_unregistered_plugins(system);
+}
+
+void
+libinput_plugin_system_notify_evdev_frame(struct libinput_plugin_system *system,
+ struct libinput_device *device,
+ struct evdev_frame *frame)
+{
+ struct libinput_plugin *plugin;
+ list_for_each_safe(plugin, &system->plugins, link) {
+ libinput_plugin_notify_evdev_frame(plugin, device, frame);
+ }
+ libinput_plugin_system_drop_unregistered_plugins(system);
+}
diff --git a/src/libinput-plugin.h b/src/libinput-plugin.h
new file mode 100644
index 0000000000000000000000000000000000000000..d9c9f1b848ee02e4831b0bcf808cff370f3c3ed9
--- /dev/null
+++ b/src/libinput-plugin.h
@@ -0,0 +1,154 @@
+/*
+ * 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
+#include
+#include
+
+/* Forward declarations instead of #includes to make
+ * this header self-contained (bindgen, etc.) */
+struct evdev_frame;
+struct libinput;
+struct libinput_device;
+struct libinput_plugin;
+enum libinput_log_priority;
+
+#define plugin_log_debug(p_, ...) plugin_log_msg((p_), LIBINPUT_LOG_PRIORITY_DEBUG, __VA_ARGS__)
+#define plugin_log_info(p_, ...) plugin_log_msg((p_), LIBINPUT_LOG_PRIORITY_INFO, __VA_ARGS__)
+#define plugin_log_error(p_, ...) plugin_log_msg((p_), LIBINPUT_LOG_PRIORITY_ERROR, __VA_ARGS__)
+#define plugin_log_bug_kernel(p_, ...) plugin_log_msg((p_), LIBINPUT_LOG_PRIORITY_ERROR, "kernel bug: " __VA_ARGS__)
+#define plugin_log_bug_libinput(p_, ...) plugin_log_msg((p_), LIBINPUT_LOG_PRIORITY_ERROR, "libinput bug: " __VA_ARGS__)
+#define plugin_log_bug_client(p_, ...) plugin_log_msg((p_), LIBINPUT_LOG_PRIORITY_ERROR, "client bug: " __VA_ARGS__)
+#define plugin_log_bug(p_, ...) plugin_log_msg((p_), LIBINPUT_LOG_PRIORITY_ERROR, "plugin bug: " __VA_ARGS__)
+
+void
+plugin_log_msg(struct libinput_plugin *plugin,
+ enum libinput_log_priority priority,
+ const char *format,
+ ...);
+
+struct libinput_plugin_interface {
+ void (*run)(struct libinput_plugin *plugin);
+ /**
+ * Notification that the plugin is about to be destroyed.
+ * When this function is called, the plugin has already
+ * been unregistered. The plugin
+ */
+ void (*destroy)(struct libinput_plugin *plugin);
+ /**
+ * Notification about a newly added device that has **not** yet
+ * been added by libinput as struct libinput_device.
+ */
+ void (*device_new)(struct libinput_plugin *plugin,
+ struct libinput_device *device,
+ struct libevdev *evdev,
+ struct udev_device *udev_device);
+ /**
+ * Notification that a device (previously announced with device_new)
+ * was ignored by libinput and was **never** added as struct
+ * libinput_device.
+ *
+ * If a device was added (device_added) then this callback will
+ * not be called for that device.
+ */
+ void (*device_ignored)(struct libinput_plugin *plugin,
+ struct libinput_device *device);
+ /**
+ * Notification that a device was added to libinput. Called
+ * after the device_new callback if the device matches libinput's
+ * expectations.
+ */
+ void (*device_added)(struct libinput_plugin *plugin,
+ struct libinput_device *device);
+ /**
+ * Notification that a previosly added event device was removed.
+ */
+ void (*device_removed)(struct libinput_plugin *plugin,
+ struct libinput_device *device);
+ /**
+ * Notification that a device submitted a frame event.
+ */
+ void (*evdev_frame)(struct libinput_plugin *plugin,
+ struct libinput_device *device,
+ struct evdev_frame *frame);
+};
+
+/**
+ * Returns a new plugin with the given interface and, optionally,
+ * the user data. The returned plugin has a refcount of at least 1
+ * and must be unref'd by the caller.
+ * Should an error occur, the plugin must be unregistered by
+ * the caller:
+ *
+ * ```
+ * struct libinput_plugin *plugin = libinput_plugin_new(libinput, ...);
+ * if (some_error_condition) {
+ * libinput_plugin_unregister(plugin);
+ * }
+ * libinput_plugin_unref(plugin);
+ * ```
+ */
+struct libinput_plugin *
+libinput_plugin_new(struct libinput *libinput,
+ const char *name,
+ const struct libinput_plugin_interface *interface,
+ void *user_data);
+
+const char *
+libinput_plugin_get_name(struct libinput_plugin *plugin);
+
+struct libinput *
+libinput_plugin_get_context(struct libinput_plugin *plugin);
+
+void
+libinput_plugin_unregister(struct libinput_plugin *plugin);
+
+void
+libinput_plugin_set_user_data(struct libinput_plugin *plugin,
+ void *user_data);
+void *
+libinput_plugin_get_user_data(struct libinput_plugin *plugin);
+
+struct libinput_plugin *
+libinput_plugin_ref(struct libinput_plugin *plugin);
+
+struct libinput_plugin *
+libinput_plugin_unref(struct libinput_plugin *plugin);
+
+#ifdef DEFINE_UNREF_CLEANUP_FUNC
+DEFINE_UNREF_CLEANUP_FUNC(libinput_plugin);
+#endif
+
+/**
+ * Inject a new event frame from the given plugin. This
+ * frame is treated as if it was just sent by the kernel's
+ * event node.
+ */
+void
+libinput_plugin_inject_evdev_frame(struct libinput_plugin *libinput,
+ struct libinput_device *device,
+ struct evdev_frame *frame);
diff --git a/src/libinput-private.h b/src/libinput-private.h
index 80fff4bf4c3e5d6068b305846af94513bca90936..45e1e40d919c65a4d9956593c96c38d4d3d666fb 100644
--- a/src/libinput-private.h
+++ b/src/libinput-private.h
@@ -38,6 +38,9 @@
#include "linux/input.h"
#include "libinput.h"
+#include "libinput-log.h"
+#include "libinput-plugin.h"
+#include "libinput-plugin-system.h"
#include "libinput-private-config.h"
#include "libinput-util.h"
#include "libinput-version.h"
@@ -205,6 +208,8 @@ struct libinput {
bool quirks_initialized;
struct quirks_context *quirks;
+ struct libinput_plugin_system plugin_system;
+
#if HAVE_LIBWACOM
struct {
WacomDeviceDatabase *db;
@@ -475,6 +480,9 @@ struct libinput_device {
void *user_data;
int refcount;
struct libinput_device_config config;
+
+ void (*inject_evdev_frame)(struct libinput_device *device,
+ struct evdev_frame *frame);
};
enum libinput_tablet_tool_axis {
@@ -594,48 +602,6 @@ struct libinput_event_listener {
typedef void (*libinput_source_dispatch_t)(void *data);
-#define log_debug(li_, ...) log_msg((li_), LIBINPUT_LOG_PRIORITY_DEBUG, __VA_ARGS__)
-#define log_info(li_, ...) log_msg((li_), LIBINPUT_LOG_PRIORITY_INFO, __VA_ARGS__)
-#define log_error(li_, ...) log_msg((li_), LIBINPUT_LOG_PRIORITY_ERROR, __VA_ARGS__)
-#define log_bug_kernel(li_, ...) log_msg((li_), LIBINPUT_LOG_PRIORITY_ERROR, "kernel bug: " __VA_ARGS__)
-#define log_bug_libinput(li_, ...) log_msg((li_), LIBINPUT_LOG_PRIORITY_ERROR, "libinput bug: " __VA_ARGS__)
-#define log_bug_client(li_, ...) log_msg((li_), LIBINPUT_LOG_PRIORITY_ERROR, "client bug: " __VA_ARGS__)
-
-#define log_debug_ratelimit(li_, r_, ...) log_msg_ratelimit((li_), (r_), LIBINPUT_LOG_PRIORITY_DEBUG, __VA_ARGS__)
-#define log_info_ratelimit(li_, r_, ...) log_msg_ratelimit((li_), (r_), LIBINPUT_LOG_PRIORITY_INFO, __VA_ARGS__)
-#define log_error_ratelimit(li_, r_, ...) log_msg_ratelimit((li_), (r_), LIBINPUT_LOG_PRIORITY_ERROR, __VA_ARGS__)
-#define log_bug_kernel_ratelimit(li_, r_, ...) log_msg_ratelimit((li_), (r_), LIBINPUT_LOG_PRIORITY_ERROR, "kernel bug: " __VA_ARGS__)
-#define log_bug_libinput_ratelimit(li_, r_, ...) log_msg_ratelimit((li_), (r_), LIBINPUT_LOG_PRIORITY_ERROR, "libinput bug: " __VA_ARGS__)
-#define log_bug_client_ratelimit(li_, r_, ...) log_msg_ratelimit((li_), (r_), LIBINPUT_LOG_PRIORITY_ERROR, "client bug: " __VA_ARGS__)
-
-static inline bool
-is_logged(const struct libinput *libinput,
- enum libinput_log_priority priority)
-{
- return libinput->log_handler &&
- libinput->log_priority log_handler &&
+ libinput->log_priority log_handler(libinput, priority, format, args);
}
@@ -1871,6 +1879,8 @@ libinput_init(struct libinput *libinput,
list_init(&libinput->device_group_list);
list_init(&libinput->tool_list);
+ libinput_plugin_system_init(&libinput->plugin_system);
+
if (libinput_timer_subsys_init(libinput) != 0) {
free(libinput->events);
close(libinput->epoll_fd);
@@ -1969,6 +1979,8 @@ libinput_unref(struct libinput *libinput)
free(libinput->events);
+ libinput_plugin_system_destroy(&libinput->plugin_system);
+
list_for_each_safe(seat, &libinput->seat_list, link) {
list_for_each_safe(device,
&seat->devices_list,
@@ -2334,6 +2346,9 @@ post_device_event(struct libinput_device *device,
void
notify_added_device(struct libinput_device *device)
{
+ struct libinput *libinput = device->seat->libinput;
+ libinput_plugin_system_notify_device_added(&libinput->plugin_system, device);
+
struct libinput_event_device_notify *added_device_event;
added_device_event = zalloc(sizeof *added_device_event);
@@ -2352,6 +2367,9 @@ notify_added_device(struct libinput_device *device)
void
notify_removed_device(struct libinput_device *device)
{
+ struct libinput *libinput = device->seat->libinput;
+ libinput_plugin_system_notify_device_removed(&libinput->plugin_system, device);
+
struct libinput_event_device_notify *removed_device_event;
removed_device_event = zalloc(sizeof *removed_device_event);
diff --git a/src/libinput.h b/src/libinput.h
index 8d58b5af892054a601a491e9a87e5992c02513e8..7f06f4b2a0d4cf42f28843f89610e0c6cacd0f23 100644
--- a/src/libinput.h
+++ b/src/libinput.h
@@ -3706,6 +3706,86 @@ libinput_path_add_device(struct libinput *libinput,
void
libinput_path_remove_device(struct libinput_device *device);
+/**
+ * @ingroup base
+ *
+ * Appends the given directory path to the libinput plugin lookup path.
+ * If the path is already in the lookup paths, it is ignored.
+ *
+ * A path's priority is determined by its position in the list; the first
+ * path in the list has the highest priority.
+ *
+ * Plugin lookup is performed across all paths in lexical order. If
+ * a plugin exists in multiple paths, the one in the highest priority
+ * path (i.e. front of the list) is used.
+ *
+ * Paths are not traversed recursively.
+ *
+ * Plugins that have a 0 byte size shadow any plugins with the same name
+ * but do not provide any fuctionality. This allows disabling a plugin
+ *
+ * This function must be called before i
+ * libinput_plugin_system_load_plugins().
+ *
+ * @see libinput_plugin_system_append_default_paths
+ *
+ * @since 1.29
+ */
+void
+libinput_plugin_system_append_path(struct libinput *libinput,
+ const char *path);
+
+/**
+ * @ingroup base
+ *
+ * Add the default plugin lookup paths, typically:
+ * - /etc/libinput/plugins/
+ * - /usr/lib{64}/libinput/plugins/
+ *
+ * These paths are inserted at the current priority - to add
+ * paths with a higher priority than these, call
+ * libinput_plugin_system_append_path() prior to this function.
+ *
+ * See libinput_plugin_system_append_path() for more details.
+ *
+ * This function must be called before
+ * libinput_plugin_system_load_plugins().
+ *
+ * @see libinput_plugin_system_append_paths
+ *
+ * @since 1.29
+ */
+void
+libinput_plugin_system_append_default_paths(struct libinput *libinput);
+
+enum libinput_plugins_flags {
+ LIBINPUT_PLUGIN_FLAG_NONE = 0,
+};
+
+/**
+ * @ingroup base
+ *
+ * Load the plugins from the set of lookup paths. This function does nothing
+ * if no plugin paths have been configured, see
+ * libinput_plugin_system_append_default_paths() and
+ * libinput_plugin_system_append_path().
+ *
+ * The typical use of this function is:
+ * ```
+ * struct libinput *li = libinput_udev_create_context(...);
+ * libinput_plugin_system_append_default_paths(li);
+ * libinput_plugin_system_load(li, flags);
+ * ```
+ *
+ * This function must be called before libinput iterates through the
+ * devices, i.e. before libinput_udev_assign_seat() or libinput_path_add_device().
+ *
+ * @since 1.29
+ */
+void
+libinput_plugin_system_load_plugins(struct libinput *libinput,
+ enum libinput_plugins_flags flags);
+
/**
* @ingroup base
*
diff --git a/src/libinput.sym b/src/libinput.sym
index 68c8651f29234cf7691e1ef1507f0b8b2413df47..134b11f776185e55e03d3cdee28b1d9a2ee9c114 100644
--- a/src/libinput.sym
+++ b/src/libinput.sym
@@ -364,3 +364,9 @@ LIBINPUT_1.28 {
libinput_device_config_3fg_drag_get_enabled;
libinput_device_config_3fg_drag_get_default_enabled;
} LIBINPUT_1.27;
+
+LIBINPUT_1.29 {
+ libinput_plugin_system_append_default_paths;
+ libinput_plugin_system_append_path;
+ libinput_plugin_system_load_plugins;
+} LIBINPUT_1.28;
diff --git a/src/timer.c b/src/timer.c
index da3f59318143de5fd10d3016780fc3b0a8649c7e..07c2341954478365cf6e0b3586e5531e00ef80f7 100644
--- a/src/timer.c
+++ b/src/timer.c
@@ -250,3 +250,17 @@ libinput_timer_flush(struct libinput *libinput, uint64_t now)
libinput_timer_handler(libinput, now);
}
+
+uint64_t
+libinput_now(struct libinput *libinput)
+{
+ uint64_t now;
+ int rc = now_in_us(&now);
+
+ if (rc
+#include
#include
static inline struct input_event
@@ -97,3 +100,205 @@ absinfo_convert_to_mm(const struct input_absinfo *absinfo, double v)
double value = v - absinfo->minimum;
return value/absinfo->resolution;
}
+
+/* A wrapper around a SYN_REPORT-terminated set of input events.
+ *
+ * This struct always has a count of >= 1 (the SYN_REPORT)
+ * and the timestamp of the SYN_REPORT is always that of the
+ * most recently appended event (if nonzero)
+ *
+ * The event frame is of a fixed size given in
+ * evdev_frame_new() and cannot be resized via helpers.
+ *
+ * The struct should be considered opaque, use the helpers
+ * to access the various fields.
+ */
+struct evdev_frame {
+ int refcount;
+ struct input_event *events;
+ size_t max_size;
+ size_t count;
+};
+
+static inline struct evdev_frame *
+evdev_frame_ref(struct evdev_frame *frame)
+{
+ assert(frame->refcount > 0);
+ ++frame->refcount;
+ return frame;
+}
+
+static inline struct evdev_frame *
+evdev_frame_unref(struct evdev_frame *frame)
+{
+ if (frame) {
+ assert(frame->refcount > 0);
+ if (--frame->refcount == 0) {
+ frame->max_size = 0;
+ frame->count = 0;
+ frame->events = NULL;
+ free(frame);
+ }
+ }
+ return NULL;
+}
+
+DEFINE_UNREF_CLEANUP_FUNC(evdev_frame);
+
+static inline bool
+evdev_frame_is_empty(const struct evdev_frame *frame)
+{
+ return frame->count == 1;
+}
+
+static inline size_t
+evdev_frame_get_count(const struct evdev_frame *frame)
+{
+ return frame->count;
+}
+
+static inline struct input_event *
+evdev_frame_get_events(const struct evdev_frame *frame, size_t *nevents)
+{
+ if (nevents)
+ *nevents = frame->count;
+
+ return frame->events;
+}
+
+/**
+ * Set the timestamp for all events in this event frame.
+ */
+static inline void
+evdev_frame_set_time(struct evdev_frame *frame, uint64_t time)
+{
+ assert(frame->count > 0);
+ for (size_t i = 0; i count; i++)
+ input_event_set_time(&frame->events[i], time);
+}
+
+static inline uint64_t
+evdev_frame_get_time(const struct evdev_frame *frame)
+{
+ assert(frame->count > 0);
+ return input_event_time(&frame->events[frame->count - 1]);
+}
+
+static inline int
+evdev_frame_reset(struct evdev_frame *frame)
+{
+ memset(frame->events, 0, frame->max_size * sizeof(struct input_event));
+ frame->count = 1; /* SYN_REPORT is always there */
+
+ return 0;
+}
+
+static inline struct evdev_frame *
+evdev_frame_new(size_t max_size)
+{
+ struct evdev_frame *frame = zalloc(max_size * sizeof(struct input_event) + sizeof(*frame));
+
+ frame->refcount = 1;
+ frame->max_size = max_size;
+ frame->count = 1; /* SYN_REPORT is always there */
+ frame->events = (struct input_event *)&frame[1];
+
+ return frame;
+}
+
+/**
+ * Append events to the event frame. nevents must be larger than 0
+ * and specifies the number of elements in events. If any events in
+ * the given events is a EV_SYN/SYN_REPORT event, that event is the last
+ * one appended even if nevents states a higher number of events (roughly
+ * equivalent to having a \0 inside a string).
+ *
+ * This function guarantees the frame is terminated with a SYN_REPORT event.
+ * Appending SYN_REPORTS to a frame does not increase the count of events in the
+ * frame - the new SYN_REPORT will simply replace the existing SYN_REPORT.
+ *
+ * The timestamp of the SYN_REPORT (if any) is used for this event
+ * frame. If the appended sequence does not contain a SYN_REPORT, the highest
+ * timestamp of any event appended is used. This timestamp will overwrite the
+ * frame's timestamp even if the timestamp of the frame is higher.
+ *
+ * If all to-be-appended events (including the SYN_REPORT) have a timestamp of
+ * 0, the existing frame's timestamp is left as-is.
+ *
+ * The caller SHOULD terminate the events with a SYN_REPORT event with a
+ * valid timestamp to ensure correct behavior.
+ *
+ * Returns 0 on success, or a negative errno on failure
+ */
+static inline int
+evdev_frame_append(struct evdev_frame *frame,
+ const struct input_event *events,
+ size_t nevents)
+{
+ assert(nevents > 0);
+
+ uint64_t time = 0;
+
+ for (size_t i = 0; i 0) {
+ if (frame->count + nevents > frame->max_size)
+ return -ENOMEM;
+
+ memcpy(frame->events + frame->count - 1, events, nevents * sizeof(struct input_event));
+ frame->count += nevents;
+ }
+ if (time)
+ evdev_frame_set_time(frame, time);
+
+ return 0;
+}
+
+/**
+ * Behaves like evdev_frame_append() but resets the frame before appending.
+ *
+ * On error the frame is left as-is.
+ *
+ * Returns 0 on success, or a negative errno on failure
+ */
+static inline int
+evdev_frame_set(struct evdev_frame *frame,
+ const struct input_event *events,
+ size_t nevents)
+{
+ assert(nevents > 0);
+
+ size_t count = nevents;
+
+ for (size_t i = 0; i frame->max_size - 1)
+ return -ENOMEM;
+
+ evdev_frame_reset(frame);
+ return evdev_frame_append(frame, events, nevents);
+}
+
+static inline struct evdev_frame *
+evdev_frame_clone(const struct evdev_frame *frame)
+{
+ size_t nevents;
+ struct input_event *events = evdev_frame_get_events(frame, &nevents);
+ struct evdev_frame *clone = evdev_frame_new(nevents);
+
+ evdev_frame_append(clone, events, nevents);
+
+ return clone;
+}
diff --git a/test/litest.c b/test/litest.c
index 172645479c143577215054470ac6029405586868..326154d00c10533859da58d41da1cdebe889f3a0 100644
--- a/test/litest.c
+++ b/test/litest.c
@@ -1319,7 +1319,8 @@ litest_log_handler(struct libinput *libinput,
fprintf(stderr, ANSI_NORMAL);
if (strstr(format, "client bug: ") ||
- strstr(format, "libinput bug: ")) {
+ strstr(format, "libinput bug: ") ||
+ strstr(format, "plugin bug: ")) {
/* valgrind is too slow and some of our offsets are too
* short, don't abort if during a valgrind run we get a
* negative offset */
@@ -2093,6 +2094,9 @@ litest_create_context(void)
libinput = libinput_path_create_context(&interface, ctx);
litest_assert_notnull(libinput);
+ libinput_plugin_system_load_plugins(libinput,
+ LIBINPUT_PLUGIN_FLAG_NONE);
+
libinput_log_set_handler(libinput, litest_log_handler);
if (verbose)
libinput_log_set_priority(libinput, LIBINPUT_LOG_PRIORITY_DEBUG);
diff --git a/test/test-plugins-lua.c b/test/test-plugins-lua.c
new file mode 100644
index 0000000000000000000000000000000000000000..333f98fc5b36e97397e193301341d80d732168ba
--- /dev/null
+++ b/test/test-plugins-lua.c
@@ -0,0 +1,440 @@
+/*
+ * 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 "libinput.h"
+#include "libinput-plugin-private.h"
+#include "litest.h"
+
+#include "libinput-lua.h"
+#include "util-strings.h"
+#include "util-files.h"
+#include "util-time.h"
+
+static char *
+_litest_write_plugin(const char *tmpdir,
+ const char *filename,
+ const char *content)
+{
+ char *path = strdup_printf("%s/%s.lua", tmpdir, 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_)
+
+struct logcapture {
+ char **errors;
+ char **infos;
+ char **debugs;
+};
+
+static void
+logcapture_destroy(struct logcapture *c)
+{
+ strv_free(c->errors);
+ strv_free(c->infos);
+ strv_free(c->debugs);
+ free(c);
+}
+
+DEFINE_DESTROY_CLEANUP_FUNC(logcapture);
+
+static void
+log_handler_msgcapture(struct libinput *libinput,
+ enum libinput_log_priority pri,
+ const char *format,
+ va_list args)
+{
+ struct litest_user_data *user_data = libinput_get_user_data(libinput);
+ struct logcapture *capture = user_data->private;
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wformat-nonliteral"
+ switch (pri) {
+ case LIBINPUT_LOG_PRIORITY_ERROR:
+ capture->errors = strv_append_vprintf(capture->errors, format, args);
+ break;
+ case LIBINPUT_LOG_PRIORITY_INFO:
+ capture->infos = strv_append_vprintf(capture->infos, format, args);
+ break;
+ case LIBINPUT_LOG_PRIORITY_DEBUG:
+ capture->debugs = strv_append_vprintf(capture->debugs, format, args);
+ break;
+ }
+#pragma GCC diagnostic pop
+}
+
+START_TEST(plugin_load_failure)
+{
+ _litest_context_destroy_ struct libinput *li = litest_create_context();
+
+ _destroy_(logcapture) *capture = zalloc(sizeof(*capture));
+ litest_context_set_user_data(li, capture);
+
+ libinput_log_set_handler(li, log_handler_msgcapture);
+
+ const char *path = litest_test_param_get_string(test_env->params, "path");
+ libinput_lua_plugin_new_from_path(li, path);
+ litest_restore_log_handler(li);
+
+ litest_assert_ptr_notnull(capture->errors);
+ 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,
+ PLUGIN_CREATED,
+ MULTIPLE_PLUGINS_CREATED,
+};
+
+START_TEST(plugin_load_success_but_no_register)
+{
+ enum content content = litest_test_param_get_i32(test_env->params, "content");
+
+ _litest_context_destroy_ struct libinput *li = litest_create_context();
+ _destroy_(tmpdir) *tmpdir = tmpdir_create(NULL);
+
+ const char *lua = NULL;
+ switch (content) {
+ case MULTIPLE_PLUGINS_CREATED:
+ lua = "p1 = libinput:register(1)\np2 = libinput:register(2)\n";
+ break;
+ case PLUGIN_CREATED:
+ lua = "plugin = libinput.register(1)";
+ 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);
+
+ /* valgrind-only test, we don't get notifications */
+ libinput_lua_plugin_new_from_path(li, path);
+}
+END_TEST
+
+START_TEST(plugin_register_noop)
+{
+ _litest_context_destroy_ struct libinput *li = litest_create_context();
+ _destroy_(tmpdir) *tmpdir = tmpdir_create(NULL);
+
+ const char *lua = "p = libinput:register(1)";
+ _autofree_ char *path = litest_write_plugin(tmpdir->path, lua);
+
+ /* valgrind-only test, we don't get notifications */
+ libinput_lua_plugin_new_from_path(li, path);
+}
+END_TEST
+
+START_TEST(plugin_test_log_global)
+{
+ _litest_context_destroy_ struct libinput *li = litest_create_context();
+ _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);
+
+ _destroy_(logcapture) *capture = zalloc(sizeof(*capture));
+ litest_context_set_user_data(li, capture);
+ libinput_log_set_priority(li, LIBINPUT_LOG_PRIORITY_DEBUG);
+ libinput_log_set_handler(li, log_handler_msgcapture);
+ struct libinput_plugin *plugin = libinput_lua_plugin_new_from_path(li, path);
+ libinput_plugin_run(plugin);
+ litest_restore_log_handler(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(plugin_test_evdev_global)
+{
+ _litest_context_destroy_ struct libinput *li = litest_create_context();
+ _destroy_(tmpdir) *tmpdir = tmpdir_create(NULL);
+
+ /* This is generated, if a few of them work the
+ * rest should work too */
+ const char *lua = "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);
+ struct libinput_plugin *plugin = libinput_lua_plugin_new_from_path(li, path);
+ libinput_plugin_run(plugin);
+}
+END_TEST
+
+START_TEST(plugin_test_libinput_now)
+{
+ _litest_context_destroy_ struct libinput *li = litest_create_context();
+ _destroy_(tmpdir) *tmpdir = tmpdir_create(NULL);
+
+ const char *lua = "log.error(\">>>\" .. libinput:now())";
+ _autofree_ char *path = litest_write_plugin(tmpdir->path, lua);
+
+ _destroy_(logcapture) *capture = zalloc(sizeof(*capture));
+ litest_context_set_user_data(li, capture);
+ libinput_log_set_handler(li, log_handler_msgcapture);
+ struct libinput_plugin *plugin = libinput_lua_plugin_new_from_path(li, path);
+ libinput_plugin_run(plugin);
+ litest_restore_log_handler(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(plugin_test_libinput_timer)
+{
+ _litest_context_destroy_ struct libinput *li = litest_create_context();
+ _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");
+
+ libinput_dispatch(li);
+ litest_drain_events(li);
+
+ _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(p, 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);
+
+ _destroy_(logcapture) *capture = zalloc(sizeof(*capture));
+ litest_context_set_user_data(li, capture);
+ libinput_log_set_handler(li, log_handler_msgcapture);
+ struct libinput_plugin *plugin = libinput_lua_plugin_new_from_path(li, path);
+ libinput_plugin_run(plugin);
+
+ litest_assert_ptr_null(capture->errors);
+
+ size_t nloops = reschedule ? 4 : 1;
+ for (size_t i = 0; i 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_ptr_null(capture->errors);
+
+ litest_restore_log_handler(li);
+}
+END_TEST
+
+enum connect_error {
+ BAD_TYPE,
+ TOO_FEW_ARGS,
+ TOO_MANY_ARGS,
+};
+
+START_TEST(plugin_bad_connect)
+{
+ _litest_context_destroy_ struct libinput *li = litest_create_context();
+ _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("a = 10\n"
+ "libinput:connect(\"%s\", %s)\n"
+ "libinput:register()",
+ handler,
+ func);
+
+ _autofree_ char *path = litest_write_plugin(tmpdir->path, lua);
+
+ /* crashtest/valgrind-only test, we don't get notifications */
+ struct libinput_plugin *plugin = libinput_lua_plugin_new_from_path(li, path);
+ litest_disable_log_handler(li);
+ libinput_plugin_run(plugin);
+ litest_restore_log_handler(li);
+}
+END_TEST
+
+TEST_COLLECTION(lua_plugins)
+{
+ litest_with_parameters(params,
+ "path", 's', 2, "/does/not/exist", "/proc/cpuinfo") {
+ litest_add_parametrized_no_device(plugin_load_failure, params);
+ }
+ litest_with_parameters(params,
+ "content", 'I', 5,
+ litest_named_i32(EMPTY),
+ litest_named_i32(BASIC),
+ litest_named_i32(COMMENT),
+ litest_named_i32(PLUGIN_CREATED),
+ litest_named_i32(MULTIPLE_PLUGINS_CREATED)) {
+ litest_add_parametrized_no_device(plugin_load_success_but_no_register, params);
+ }
+ litest_add_no_device(plugin_register_noop);
+ litest_add_no_device(plugin_test_evdev_global);
+ litest_add_no_device(plugin_test_libinput_now);
+ litest_with_parameters(params,
+ "mode", 's', 2, "absolute", "relative",
+ "reschedule", 'b') {
+ litest_add_parametrized_no_device(plugin_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(plugin_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(plugin_bad_connect, params);
+ }
+}
diff --git a/test/test-utils.c b/test/test-utils.c
index f823b3636b58b48eedcda742aa9535252e6b832b..ff08a1fb10b511ea60c6d1bb992be466775aeae5 100644
--- a/test/test-utils.c
+++ b/test/test-utils.c
@@ -220,6 +220,11 @@ START_TEST(find_files_test)
litest_assert_int_eq(nfiles, (size_t)0);
litest_assert_ptr_notnull(empty_path);
litest_assert_ptr_null(empty_path[0]);
+
+ _autostrvfree_ char** also_empty_path = list_files(NULL, "suf", &nfiles);
+ litest_assert_int_eq(nfiles, (size_t)0);
+ litest_assert_ptr_notnull(also_empty_path);
+ litest_assert_ptr_null(also_empty_path[0]);
}
END_TEST
@@ -2554,6 +2559,150 @@ START_TEST(macros_expand)
}
END_TEST
+START_TEST(evdev_frames)
+{
+ {
+ evdev_frame_unref(NULL); /* unref on NULL is permitted */
+ }
+ {
+ _unref_(evdev_frame) *frame = evdev_frame_new(3);
+ litest_assert_int_eq(evdev_frame_get_count(frame), 1U); /* SYN_REPORT */
+
+ litest_assert_ptr_eq(evdev_frame_ref(frame), frame);
+ litest_assert_ptr_eq(evdev_frame_unref(frame), NULL);
+ }
+ {
+ _unref_(evdev_frame) *frame = evdev_frame_new(3);
+ struct input_event toobig[] = {
+ { .type = EV_ABS, .code = ABS_X, .value = 1, },
+ { .type = EV_ABS, .code = ABS_Y, .value = 2, },
+ { .type = EV_ABS, .code = ABS_Z, .value = 3, },
+ { .type = EV_SYN, .code = SYN_REPORT, .value = 0, },
+ };
+
+ int rc = evdev_frame_set(frame, toobig, ARRAY_LENGTH(toobig));
+ litest_assert_int_eq(rc, -ENOMEM);
+ }
+ {
+ struct input_event events[] = {
+ { .type = EV_ABS, .code = ABS_X, .value = 1, },
+ { .type = EV_ABS, .code = ABS_Y, .value = 2, },
+ { .type = EV_SYN, .code = SYN_REPORT, .value = 0, },
+ };
+
+ _unref_(evdev_frame) *frame = evdev_frame_new(3);
+ int rc = evdev_frame_set(frame, events, ARRAY_LENGTH(events));
+ litest_assert_neg_errno_success(rc);
+ litest_assert_int_eq(evdev_frame_get_count(frame), ARRAY_LENGTH(events));
+ litest_assert_int_eq(frame->max_size, ARRAY_LENGTH(events));
+
+ size_t nevents;
+ rc = memcmp(evdev_frame_get_events(frame, &nevents), events, sizeof(events));
+ litest_assert_int_eq(rc, 0);
+ litest_assert_int_eq(nevents, ARRAY_LENGTH(events));
+
+ /* Already full, can't append */
+ rc = evdev_frame_append(frame, events, 1);
+ litest_assert_int_eq(rc, -ENOMEM);
+ }
+ {
+ struct input_event events[] = {
+ { .type = EV_ABS, .code = ABS_X, .value = 1, },
+ { .type = EV_ABS, .code = ABS_Y, .value = 2, },
+ { .type = EV_SYN, .code = SYN_REPORT, .value = 0, },
+ };
+
+ _unref_(evdev_frame) *frame = evdev_frame_new(3);
+ int rc = evdev_frame_set(frame, events, 1);
+ litest_assert_neg_errno_success(rc);
+ litest_assert_int_eq(evdev_frame_get_count(frame), 2U); /* we appended SYN_REPORT */
+ rc = evdev_frame_append(frame, events + 1, 1);
+ litest_assert_neg_errno_success(rc);
+ litest_assert_int_eq(evdev_frame_get_count(frame), 3U); /* we appended SYN_REPORT */
+ rc = evdev_frame_append(frame, events + 2, 1);
+ litest_assert_neg_errno_success(rc);
+ litest_assert_int_eq(evdev_frame_get_count(frame), 3U); /* SYN_REPORT already there */
+ }
+ {
+ struct input_event interrupted[] = {
+ { .type = EV_ABS, .code = ABS_X, .value = 1, },
+ { .type = EV_ABS, .code = ABS_Y, .value = 2, },
+ { .type = EV_SYN, .code = SYN_REPORT, .value = 0, },
+ { .type = EV_ABS, .code = ABS_RX, .value = 1, },
+ { .type = EV_ABS, .code = ABS_RY, .value = 2, },
+ { .type = EV_SYN, .code = SYN_REPORT, .value = 0, },
+ };
+
+ _unref_(evdev_frame) *frame = evdev_frame_new(5);
+ int rc = evdev_frame_set(frame, interrupted, ARRAY_LENGTH(interrupted));
+ litest_assert_neg_errno_success(rc);
+ litest_assert_int_eq(evdev_frame_get_count(frame), 3U);
+
+ rc = evdev_frame_set(frame, &interrupted[2], 1);
+ litest_assert_neg_errno_success(rc);
+ litest_assert_int_eq(evdev_frame_get_count(frame), 1U);
+
+ rc = evdev_frame_set(frame, &interrupted[1], ARRAY_LENGTH(interrupted) - 1);
+ litest_assert_neg_errno_success(rc);
+ litest_assert_int_eq(evdev_frame_get_count(frame), 2U);
+
+ /* We never appended a timestamp */
+ litest_assert_int_eq(evdev_frame_get_time(frame), 0U);
+ }
+ {
+ struct input_event e = {
+ .type = EV_ABS,
+ .code = ABS_X,
+ .value = 1,
+ .input_event_sec = 1234,
+ .input_event_usec = 567,
+
+ };
+
+ _unref_(evdev_frame) *frame = evdev_frame_new(3);
+ litest_assert_int_eq(evdev_frame_get_time(frame), 0U);
+
+ evdev_frame_append(frame, &e, 1);
+ litest_assert_int_eq(evdev_frame_get_time(frame), 1234000567U);
+ evdev_frame_append(frame, &e, 1);
+ litest_assert_int_eq(evdev_frame_get_time(frame), 1234000567U);
+
+ struct input_event syn = {
+ .type = EV_SYN,
+ .code = SYN_REPORT,
+ .value = 0,
+ .input_event_sec = 111,
+ .input_event_usec = 333,
+
+ };
+
+ litest_assert_neg_errno_success(evdev_frame_append(frame, &syn, 1));
+ litest_assert_int_eq(evdev_frame_get_time(frame), 111000333U);
+
+ /* SYN_REPORT overwrites lower timestamp */
+ syn.input_event_usec = 111;
+ litest_assert_neg_errno_success(evdev_frame_append(frame, &syn, 1));
+ litest_assert_int_eq(evdev_frame_get_time(frame), 111000111U);
+ }
+ {
+ /* Expect highest timestamp */
+ _unref_(evdev_frame) *frame = evdev_frame_new(4);
+ struct input_event mixed_times[] = {
+ { .type = EV_ABS, .code = ABS_X, .value = 1, .input_event_sec = 12, .input_event_usec = 700, },
+ { .type = EV_ABS, .code = ABS_Y, .value = 2, .input_event_sec = 56, .input_event_usec = 800, },
+ { .type = EV_ABS, .code = ABS_Z, .value = 3, .input_event_sec = 34, .input_event_usec = 900, },
+ { .type = EV_SYN, .code = SYN_REPORT, .value = 0, .input_event_sec = 0, .input_event_usec = 1, },
+ };
+ evdev_frame_set(frame, mixed_times, 3);
+ litest_assert_int_eq(evdev_frame_get_time(frame), 56000800U);
+
+ /* but SYN_REPORT overwrites any other timestamp */
+ evdev_frame_set(frame, mixed_times, 4);
+ litest_assert_int_eq(evdev_frame_get_time(frame), 1U);
+ }
+}
+END_TEST
+
int main(void)
{
struct litest_runner *runner = litest_runner_new();
@@ -2635,6 +2784,8 @@ int main(void)
ADD_TEST(attribute_cleanup);
ADD_TEST(macros_expand);
+ ADD_TEST(evdev_frames);
+
enum litest_runner_result result = litest_runner_run_tests(runner);
litest_runner_destroy(runner);
diff --git a/tools/libinput-debug-events.c b/tools/libinput-debug-events.c
index a6d7a69d173dea991e4c01caf0569d6c6fbdd515..62819262a25eeb353fe03248731cc12603dd68f3 100644
--- a/tools/libinput-debug-events.c
+++ b/tools/libinput-debug-events.c
@@ -314,7 +314,12 @@ main(int argc, char **argv)
if (verbose)
printf("libinput version: %s\n", LIBINPUT_VERSION);
- li = tools_open_backend(backend, seat_or_devices, verbose, &grab);
+ bool with_plugins = (options.plugins == 1);
+ li = tools_open_backend(backend,
+ seat_or_devices,
+ verbose,
+ &grab,
+ with_plugins);
if (!li)
return EXIT_FAILURE;
diff --git a/tools/libinput-debug-gui.c b/tools/libinput-debug-gui.c
index 498e031e4dae92f4502eb6ee109397995209f946..32d44b739fcfba9d3eba91ed96a64ddae8ea2340 100644
--- a/tools/libinput-debug-gui.c
+++ b/tools/libinput-debug-gui.c
@@ -1985,7 +1985,12 @@ main(int argc, char **argv)
backend = BACKEND_UDEV;
}
- li = tools_open_backend(backend, seat_or_device, verbose, &w.grab);
+ bool with_plugins = (options.plugins == 1);
+ li = tools_open_backend(backend,
+ seat_or_device,
+ verbose,
+ &w.grab,
+ with_plugins);
if (!li)
return EXIT_FAILURE;
diff --git a/tools/libinput-debug-tablet.c b/tools/libinput-debug-tablet.c
index d17f6dc6faa9f4badbb2d0b47a972736bcd94a7e..8353dd4e8728f4f7d67992cf4795974ba85c78b5 100644
--- a/tools/libinput-debug-tablet.c
+++ b/tools/libinput-debug-tablet.c
@@ -591,7 +591,8 @@ main(int argc, char **argv)
return EXIT_FAILURE;
}
- li = tools_open_backend(backend, seat_or_device, false, &grab);
+ bool with_plugins = (options.plugins == 1);
+ li = tools_open_backend(backend, seat_or_device, false, &grab, with_plugins);
if (!li)
return EXIT_FAILURE;
diff --git a/tools/libinput-list-devices.c b/tools/libinput-list-devices.c
index 681d618d54172904651989b0bb525b55162f20cf..658526b3d7731cb9906eef2c4e776dee7812aae1 100644
--- a/tools/libinput-list-devices.c
+++ b/tools/libinput-list-devices.c
@@ -557,10 +557,10 @@ main(int argc, char **argv)
}
devices[ndevices++] = argv[optind];
} while (++optind plugins = -1;
options->tapping = -1;
options->tap_map = -1;
options->drag = -1;
@@ -147,6 +148,12 @@ tools_parse_option(int option,
struct tools_options *options)
{
switch(option) {
+ case OPT_PLUGINS_ENABLE:
+ options->plugins = 1;
+ break;
+ case OPT_PLUGINS_DISABLE:
+ options->plugins = 0;
+ break;
case OPT_TAP_ENABLE:
options->tapping = 1;
break;
@@ -467,8 +474,21 @@ static const struct libinput_interface interface = {
.close_restricted = close_restricted,
};
+static void
+tools_load_plugins(struct libinput *libinput)
+{
+ _autofree_ char *builddir = NULL;
+ if (builddir_lookup(&builddir)) {
+ _autofree_ char *plugindir =
+ strdup_printf("%s/plugins", builddir);
+ libinput_plugin_system_append_path(libinput, plugindir);
+ }
+ libinput_plugin_system_append_default_paths(libinput);
+ libinput_plugin_system_load_plugins(libinput, LIBINPUT_PLUGIN_FLAG_NONE);
+}
+
static struct libinput *
-tools_open_udev(const char *seat, bool verbose, bool *grab)
+tools_open_udev(const char *seat, bool verbose, bool *grab, bool with_plugins)
{
_unref_(udev) *udev = udev_new();
if (!udev) {
@@ -486,6 +506,9 @@ tools_open_udev(const char *seat, bool verbose, bool *grab)
if (verbose)
libinput_log_set_priority(li, LIBINPUT_LOG_PRIORITY_DEBUG);
+ if (with_plugins)
+ tools_load_plugins(li);
+
if (libinput_udev_assign_seat(li, seat)) {
fprintf(stderr, "Failed to set seat\n");
return NULL;
@@ -495,7 +518,7 @@ tools_open_udev(const char *seat, bool verbose, bool *grab)
}
static struct libinput *
-tools_open_device(const char **paths, bool verbose, bool *grab)
+tools_open_device(const char **paths, bool verbose, bool *grab, bool with_plugins)
{
_unref_(libinput) *li = libinput_path_create_context(&interface, grab);
if (!li) {
@@ -508,6 +531,9 @@ tools_open_device(const char **paths, bool verbose, bool *grab)
libinput_log_set_priority(li, LIBINPUT_LOG_PRIORITY_DEBUG);
}
+ if (with_plugins)
+ tools_load_plugins(li);
+
const char **p = paths;
while (*p) {
struct libinput_device *device = libinput_path_add_device(li, *p);
@@ -532,7 +558,8 @@ struct libinput *
tools_open_backend(enum tools_backend which,
const char **seat_or_device,
bool verbose,
- bool *grab)
+ bool *grab,
+ bool with_plugins)
{
struct libinput *li;
@@ -540,10 +567,10 @@ tools_open_backend(enum tools_backend which,
switch (which) {
case BACKEND_UDEV:
- li = tools_open_udev(seat_or_device[0], verbose, grab);
+ li = tools_open_udev(seat_or_device[0], verbose, grab, with_plugins);
break;
case BACKEND_DEVICE:
- li = tools_open_device(seat_or_device, verbose, grab);
+ li = tools_open_device(seat_or_device, verbose, grab, with_plugins);
break;
default:
abort();
diff --git a/tools/shared.h b/tools/shared.h
index 2cf14f95332a18ca8b991979f673d32ae730cd1e..29e1798e03e15ba3430c7ee27fc5dd334392325c 100644
--- a/tools/shared.h
+++ b/tools/shared.h
@@ -76,10 +76,14 @@ enum configuration_options {
OPT_AREA,
OPT_3FG_DRAG,
OPT_SENDEVENTS,
+ OPT_PLUGINS_DISABLE,
+ OPT_PLUGINS_ENABLE,
};
#define CONFIGURATION_OPTIONS \
{ "disable-sendevents", required_argument, 0, OPT_DISABLE_SENDEVENTS }, \
+ { "enable-plugins", no_argument, 0, OPT_PLUGINS_ENABLE }, \
+ { "disable-plugins", no_argument, 0, OPT_PLUGINS_DISABLE }, \
{ "enable-tap", no_argument, 0, OPT_TAP_ENABLE }, \
{ "disable-tap", no_argument, 0, OPT_TAP_DISABLE }, \
{ "enable-drag", no_argument, 0, OPT_DRAG_ENABLE }, \
@@ -143,6 +147,7 @@ enum tools_backend {
struct tools_options {
char match[256];
+ int plugins;
int tapping;
int drag;
int drag_lock;
@@ -179,7 +184,8 @@ int tools_parse_option(int option,
struct libinput* tools_open_backend(enum tools_backend which,
const char **seat_or_devices,
bool verbose,
- bool *grab);
+ bool *grab,
+ bool with_plugins);
void tools_device_apply_config(struct libinput_device *device,
struct tools_options *options);
void tools_tablet_tool_apply_config(struct libinput_tablet_tool *tool,