plugins: add support for lua plugins to change evdev event streams

This patch adds support for Lua scripts to modify evdev devices and
event frames before libinput sees those events.

Plugins in Lua are sandboxed and restricted in what they can do: no IO,
no network, not much of anything else.

A plugin is notified about a new device before libinput handles it and
it can thus modify that device (changes are passed through to our libevdev
context). A plugin can then also connect an evdev frame handler which
gives it access to the evdev frames before libinput processes them. The
example plugin included shows how to swap left/right mouse buttons:

    libinput:register({1})

    function frame(device, frame)
        for _, v in ipairs(frame.events) do
            if v.usage == evdev.BTN_RIGHT then
                v.usage = evdev.BTN_LEFT
            elseif v.usage == evdev.BTN_LEFT then
                v.usage = evdev.BTN_RIGHT
            end
        end
        return frame
    end

    function device_new(plugin, device)
        local usages = device:usages()
        if usages[evdev.BTN_LEFT] and usages[evdev.BTN_RIGHT] then
            device:connect("evdev-frame", frame)
        end
    end

    libinput:connect("new-evdev-device", device_new)

A few other example plugins are included in this patch

Part-of: <https://gitlab.freedesktop.org/libinput/libinput/-/merge_requests/1192>
This commit is contained in:
Peter Hutterer 2025-04-22 10:25:55 +10:00
parent 0c65b1069b
commit 9e37bc0cfa
19 changed files with 3562 additions and 15 deletions

View file

@ -98,11 +98,11 @@ variables:
# See the documentation here: # # See the documentation here: #
# https://wayland.freedesktop.org/libinput/doc/latest/building.html # # 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' 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' 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' 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' 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' 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' FREEBSD_PACKAGES: 'git pkgconf meson libepoll-shim libudev-devd libevdev libwacom gtk3 libmtdev bash wayland'
############################ end of package lists ############################# ############################ end of package lists #############################
@ -110,12 +110,12 @@ variables:
# changing these will force rebuilding the associated image # changing these will force rebuilding the associated image
# Note: these tags have no meaning and are not tied to a particular # Note: these tags have no meaning and are not tied to a particular
# libinput version # libinput version
FEDORA_TAG: '2025-05-19.0' FEDORA_TAG: '2025-05-20.0'
DEBIAN_TAG: '2025-05-19.0' DEBIAN_TAG: '2025-05-20.0'
UBUNTU_TAG: '2025-05-19.0' UBUNTU_TAG: '2025-05-20.0'
ARCH_TAG: '2025-05-19.0' ARCH_TAG: '2025-05-20.0'
ALPINE_TAG: '2025-05-19.0' ALPINE_TAG: '2025-05-20.0'
FREEBSD_TAG: '2025-05-19.0' FREEBSD_TAG: '2025-05-20.0'
FDO_UPSTREAM_REPO: libinput/libinput FDO_UPSTREAM_REPO: libinput/libinput
@ -796,6 +796,19 @@ vm-pointer-no-libwacom:
variables: variables:
MESON_ARGS: '-Dlibwacom=false' MESON_ARGS: '-Dlibwacom=false'
vm-lua:
extends:
- .fedora:42@test-suite-vm
variables:
SUITE_NAMES: 'lua'
vm-lua-no-libwacom:
extends:
- vm-lua
stage: test-suite-no-libwacom
variables:
MESON_ARGS: '-Dlibwacom=false'
vm-valgrind-touchpad: vm-valgrind-touchpad:
stage: valgrind stage: valgrind
@ -1005,6 +1018,20 @@ vm-valgrind-pointer:
rules: rules:
- if: $GITLAB_USER_LOGIN != "marge-bot" - if: $GITLAB_USER_LOGIN != "marge-bot"
vm-valgrind-lua:
stage: valgrind
extends:
- vm-lua
- .policy-retry-on-failure
variables:
MESON_TEST_ARGS: '--setup=valgrind'
LITEST_JOBS: 0
retry:
max: 2
rules:
- if: $GITLAB_USER_LOGIN != "marge-bot"
.fedora-build@template: .fedora-build@template:
extends: extends:
@ -1067,6 +1094,20 @@ build-no-mtdev-nodeps@fedora:42:
before_script: before_script:
- dnf remove -y mtdev mtdev-devel - dnf remove -y mtdev mtdev-devel
build-no-lua@fedora:42:
extends:
- .fedora-build@template
variables:
MESON_ARGS: "-Dlua-plugins=disabled"
build-no-lua-nodeps@fedora:42:
extends:
- .fedora-build@template
variables:
MESON_ARGS: "-Dlua-plugins=disabled"
before_script:
- dnf remove -y lua lua-devel
build-docs@fedora:42: build-docs@fedora:42:
extends: extends:
- .fedora-build@template - .fedora-build@template
@ -1163,6 +1204,7 @@ check-test-suites:
libinput-test-suite-totem libinput-test-suite-totem
libinput-test-suite-touch libinput-test-suite-touch
libinput-test-suite-pointer libinput-test-suite-pointer
libinput-test-suite-lua
EOF EOF
- sort -o ci-testsuites ci-testsuites - sort -o ci-testsuites ci-testsuites
- diff -u8 -w ci-testsuites meson-testsuites || (echo "Some test suites are not run in the CI" && false) - diff -u8 -w ci-testsuites meson-testsuites || (echo "Some test suites are not run in the CI" && false)

View file

@ -492,6 +492,7 @@ vm-valgrind-{{suite.name}}:
- if: $GITLAB_USER_LOGIN != "marge-bot" - if: $GITLAB_USER_LOGIN != "marge-bot"
{% endfor %} {% endfor %}
{% endfor %}{# for if distro.use_for_qemu_tests #} {% endfor %}{# for if distro.use_for_qemu_tests #}
{% for distro in distributions if distro.use_for_custom_build_tests %} {% for distro in distributions if distro.use_for_custom_build_tests %}
@ -557,6 +558,20 @@ build-no-mtdev-nodeps@{{distro.name}}:{{version}}:
before_script: before_script:
- dnf remove -y mtdev mtdev-devel - dnf remove -y mtdev mtdev-devel
build-no-lua@{{distro.name}}:{{version}}:
extends:
- .{{distro.name}}-build@template
variables:
MESON_ARGS: "-Dlua-plugins=disabled"
build-no-lua-nodeps@{{distro.name}}:{{version}}:
extends:
- .{{distro.name}}-build@template
variables:
MESON_ARGS: "-Dlua-plugins=disabled"
before_script:
- dnf remove -y lua lua-devel
build-docs@{{distro.name}}:{{version}}: build-docs@{{distro.name}}:{{version}}:
extends: extends:
- .{{distro.name}}-build@template - .{{distro.name}}-build@template

View file

@ -3,7 +3,7 @@
# #
# We're happy to rebuild all containers when one changes. # 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: distributions:
- name: fedora - name: fedora
@ -50,6 +50,7 @@ distributions:
- python3-click - python3-click
- python3-rich - python3-rich
- virtme-ng - virtme-ng
- lua-devel
- name: debian - name: debian
tag: *default_tag tag: *default_tag
versions: versions:
@ -75,6 +76,7 @@ distributions:
- libglib2.0-dev - libglib2.0-dev
- libmtdev-dev - libmtdev-dev
- curl # for the coverity job - curl # for the coverity job
- lua5.4-dev
- name: ubuntu - name: ubuntu
tag: *default_tag tag: *default_tag
versions: versions:
@ -99,6 +101,7 @@ distributions:
- libgtk-3-dev - libgtk-3-dev
- libglib2.0-dev - libglib2.0-dev
- libmtdev-dev - libmtdev-dev
- lua5.4-dev
- name: arch - name: arch
tag: *default_tag tag: *default_tag
versions: versions:
@ -116,6 +119,7 @@ distributions:
- gtk4 - gtk4
- mtdev - mtdev
- diffutils - diffutils
- lua
build: build:
extra_variables: extra_variables:
- "MESON_ARGS: '-Ddocumentation=false'" # python-recommonmark is no longer in the repos - "MESON_ARGS: '-Ddocumentation=false'" # python-recommonmark is no longer in the repos
@ -136,6 +140,7 @@ distributions:
- gtk4.0-dev - gtk4.0-dev
- mtdev-dev - mtdev-dev
- bash - bash
- lua5.4-dev
build: build:
extra_variables: extra_variables:
- "MESON_ARGS: '-Ddocumentation=false' # alpine does not have python-recommonmark" - "MESON_ARGS: '-Ddocumentation=false' # alpine does not have python-recommonmark"
@ -227,6 +232,9 @@ test_suites:
- name: pointer - name: pointer
suites: suites:
- pointer - pointer
- name: lua
suites:
- lua
vng: vng:
kernel: https://gitlab.freedesktop.org/api/v4/projects/libevdev%2Fhid-tools/packages/generic/kernel-x86_64/v6.14/bzImage kernel: https://gitlab.freedesktop.org/api/v4/projects/libevdev%2Fhid-tools/packages/generic/kernel-x86_64/v6.14/bzImage

View file

@ -12,6 +12,7 @@
troubleshooting troubleshooting
contributing contributing
development development
lua-plugins
API documentation <@HTTP_DOC_LINK@/api/> API documentation <@HTTP_DOC_LINK@/api/>

633
doc/user/lua-plugins.rst Normal file
View file

@ -0,0 +1,633 @@
.. _lua_plugins:
==============================================================================
Lua Plugins
==============================================================================
libinput provides a plugin system that allows users to modify the behavior
of devices. For example, a plugin may add or remove axes and/or buttons on a
device and/or modify the event stream seen by this device before it is passed
to libinput.
Plugins are implemented in `Lua <https://www.lua.org/>`_ (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 <https://www.lua.org/manual/5.4/manual.html>`_ for
details on the Lua language.
.. note:: Plugins are **not** loaded by default, it is up to the compositor
whether to allow plugins. An explicit call to
``libinput_plugin_system_load_plugins()`` is required.
------------------------------------------------------------------------------
Limitations
------------------------------------------------------------------------------
Each script runs in its own sandbox and cannot communicate or share state with
other scripts.
Tables that hold API methods are not writable, i.e. it is not possible
to overwrite the default functionality of those APIs.
The Lua API available to plugins is limited to the following calls::
assert error ipairs next pairs tonumber
pcall select print tostring type xpcall
table string math
It is not possible to e.g. use the ``io`` module from a script.
To use methods on instantiated objects, the method call syntax must be used.
For example:
.. code-block:: lua
libinput:register()
libinput.register() -- this will fail
------------------------------------------------------------------------------
When to use plugins
------------------------------------------------------------------------------
libinput plugins are a relatively niche use-case that typically need to
address either once-off issues (e.g. those caused by worn-out hardware) or
user preferences that libinput does not and will not cater for.
Plugins should not be used for issues that can be fixed generically, for
example via :ref:`device-quirks`.
As a rule of thumb: a plugin should be a once-off that only works for one
user's hardware. If a plugin can be shared with many users then the plugin
implements functionality that should be integrated into libinput proper.
------------------------------------------------------------------------------
Testing plugins
------------------------------------------------------------------------------
Our :ref:`tools` support plugins if passed the ``--enable-plugins`` commandline
option. For implementing and testing plugins the easiest commands to test are
- ``libinput debug-events --enable-plugins`` (see :ref:`libinput-debug-events` docs)
- ``libinput debug-gui --enable-plugins`` (see :ref:`libinput-debug-gui` docs)
Where libinput is built and run from git, the tools will also look for plugins
in the meson build directory. See the ``plugins/meson.build`` file for details.
.. _plugins_api_lua:
--------------------------------------------------------------------------------
Lua Plugin API
--------------------------------------------------------------------------------
Lua plugins sit effectively below libinput and the API is not a
representation of the libinput API. The API revolves around two types:
``libinput`` and ``EvdevDevice``. The former is used to register a
plugin from a script, the latter represents one device that is present
in the system (but may not have yet been added by libinput).
Typically a script does the following steps:
- register with libinput via ``libinput:register(version)``
- connect to the ``"new-evdev-device"`` event
- receive an ``EvdevDevice`` object in the ``"new-evdev-device"`` callback
- check and/or modify the evdev event codes on the device
- connect to the device's ``"evdev-frame"`` event
- receive an :ref:`evdev frame <plugins_api_evdev_frame>` in the device's
``"evdev-frame"`` callback
- check and/or modify the events in that frame
Where multiple plugins are active, the evdev frame passed to the callback is
the combined frame as processed by all previous plugins in ascending sort order.
For example, if one plugin discards all button events subsequent plugins will
never see those button events in the frame.
.. _plugins_api_version_stability:
..............................................................................
Plugin version stability
..............................................................................
Plugin API version stability is provided on a best effort basis. We aim to provide
stable plugin versions for as long as feasible but may need to retire some older
versions over time. For this reason a plugin can select multiple versions it
implements, libinput will pick one supported version and adjust the plugin
behavior to match that version. See the ``libinput:register()`` call for details.
--------------------------------------------------------------------------------
Lua Plugin API Reference
--------------------------------------------------------------------------------
libinput provides the following globals and types:
.. _plugins_api_evdev_usage:
................................................................................
Evdev Usages
................................................................................
Evdev usages are a libinput-specific wrapper around the ``linux/input-event-codes.h``
evdev types and codes. They are used by libinput internally and are a 32-bit
combination of ``type << 16 | code``. Each usage carries the type and code and
is thus simpler to pass around and less prone to type confusion.
For the case where the :ref:`evdev global <plugins_api_evdev_global>` does not
provide a named constant the value can be crafted manually:
.. code-block:: lua
type = 0x3 -- EV_REL
code = 0x1 -- REL_Y
usage = (type << 16) | code
.. _plugins_api_evdev_global:
................................................................................
The ``evdev`` global
................................................................................
The ``evdev`` global represents all known :ref:`plugins_api_evdev_usage`,
effectively in the form:
.. code-block:: lua
evdev = {
ABS_X = (3 << 16) | 0,
ABS_Y = (3 << 16) | 1,
...
REL_X = (2 << 16) | 0,
REL_Y = (2 << 16) | 1,
...
}
This global is provided for convenience to improve readability in the code.
Note that the name uses the event code name only but the value is an
:ref:`Evdev Usage <plugins_api_evdev_usage>` (type and code).
See the ``linux/input-event-codes.h`` header file provided by your kernel
for a list of all evdev types and codes.
The evdev global also provides the bus type constants, e.g. ``evdev.BUS_USB``.
See the ``linux/input.h`` header file provided by your kernel
for a list of bus types.
.. _plugins_api_evdev_frame:
................................................................................
Evdev frames
................................................................................
Evdev frames represent a single frame of evdev events for a device. A frame
is a group of events that occured at the same time. The frame usually only
contains state that has changed compared to the previous frame.
In our API a frame is exposed as a nested table with the following structure:
.. code-block:: lua
frame1 = {
{ usage = evdev.ABS_X, value = 123 },
{ usage = evdev.ABS_Y, value = 456 },
{ usage = evdev.BTN_LEFT, value = 1 },
}
frame2 = {
{ sage = evdev.ABS_Y, value = 457 },
}
frame3 = {
{ sage = evdev.ABS_X, value = 124 },
{ usage = evdev.BTN_LEFT, value = 0 },
}
.. note:: This API does not use ``SYN_REPORT`` events, it is implied at the
end of the table. Where a plugin writes a ``SYN_REPORT`` into the
list of events, that ``SYN_REPORT`` terminates the event frame
(similar to writing a ``\0`` into the middle of a C string).
A frame containing only a ``SYN_REPORT`` is functionally equivalent
to an empty frame.
Events or frames do not have a timestamp. Where a timestamp is required, that
timestamp is passed as additional argument to the function or return value.
See :ref:`plugins_api_evdev_global` for a list of known usages.
.. warning:: Evdev frames have an implementation-defined size limit of how many
events can be added to a single frame. This limit should never be
hit by valid plugins.
.. _plugins_api_logglobal:
................................................................................
The ``log`` global
................................................................................
The ``log`` global is used to log messages from the plugin through libinput.
Whether a message is displayed in the log depends on libinput's log priority,
set by the caller.
.. function:: log.debug(message)
Log a debug message.
.. function:: log.info(message)
Log an info message.
.. function:: log.error(message)
Log an error message.
A compositor may disable stdout and stderr. Log messages should be preferred
over Lua's ``print()`` function to ensure the messages end up in the same
location as other libinput log messages and are not discarded.
.. _plugins_api_libinputglobal:
................................................................................
The ``libinput`` global object
................................................................................
The core of our plugin's API is the ``libinput`` global object. A script must
immediately ``register()`` to be active, otherwise it is unloaded immediately.
All libinput-specific APIs can be accessed through the ``libinput`` object.
.. function:: libinput:register({1, 2, ...})
Register this plugin with the given table of supported version numbers and
returns the version number selected by libinput for this plugin. See
:ref:`plugins_api_version_stability` for details.
.. code-block:: lua
-- this plugin can support versions 1, 4 and 5
version = libinput:register({1, 4, 5})
if version == 1:
....
This function must be the first function called.
If the plugin calls any other functions before ``register()``, those functions
return ``nil``, 0, an empty table, etc.
If the plugin does not call ``register()`` it will be removed immediately.
Once registered, any connected callbacks will be invoked whenever libinput
detects new devices, removes devices, etc.
This function must only be called once.
.. function:: libinput:unregister()
Unregister this plugin. This removes the plugin from libinput and releases
any resources. This call must be the last call in your plugin, it is
effectively equivalent to Lua's
`os.exit() <https://www.lua.org/manual/5.4/manual.html#pdf-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 <https://man7.org/linux/man-pages/man3/clock_gettime.3.html>`_
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 <plugins_api_evdevdevice>`
has been seen by libinput but not yet added.
.. code-block:: lua
libinput:connect("new-evdev-device", function (device) ... end)
- ``"timer-expired"``: The timer for this plugin has expired. This event is
only sent if the plugin has set a timer with ``timer_set()``.
.. code-block:: lua
libinput:connect("timer-expired", function (plugin, now) ... end)
The ``now`` argument is the current time in microseconds in
``CLOCK_MONOTONIC`` (see ``libinput.now()``).
.. function:: libinput:timer_cancel()
Cancel the timer for this plugin. This is a no-op if the timer
has not been set or has already expired.
.. function:: libinput:timer_set_absolute(time)
Set a timer for this plugin, with the given time in microseconds.
The timeout specifies an absolute time in microseconds (see
``libinput.now()``) The timer will expire once and then call the
``"timer-expired"`` event handler (if any).
See ``libinput:timer_set_relative()`` for a relative timer.
The following two lines of code are equivalent:
.. code-block:: lua
libinput:timer_set_relative(1000000) -- 1 second from now
libinput:timer_set_absolute(libinput.now() + 1000000) -- 1 second from now
Calling this function will cancel any existing (relative or absolute) timer.
.. function:: libinput:timer_set_relative(timeout)
Set a timer for this plugin, with the given timeout in microseconds from
the current time. The timer will expire once and then call the
``"timer-expired"`` event handler (if any).
See ``libinput:timer_set_absolute()`` for a relative timer.
The following two lines of code are equivalent:
.. code-block:: lua
libinput:timer_set_relative(1000000) -- 1 second from now
libinput:timer_set_absolute(libinput.now() + 1000000) -- 1 second from now
Calling this function will cancel any existing (relative or absolute) timer.
.. _plugins_api_evdevdevice:
................................................................................
The ``EvdevDevice`` type
................................................................................
The ``EvdevDevice`` type represents a device available in the system
but not (yet) added by libinput. This device may be used to modify
a device's capabilities before the device is processed by libinput.
.. function:: EvdevDevice:info()
A table containing static information about the device, e.g.
.. code-block:: lua
{
bustype = evdev.BUS_USB,
vid = 0x1234,
pid = 0x5678,
}
A plugin must ignore keys it does not know about.
Version 1 of the plugin API supports the following keys and values:
- ``bustype``: The numeric bustype of the device. See the
``BUS_*`` defines in ``linux/input.h`` for the list of possible values.
- ``vid``: The 16-bit vendor ID of the device
- ``pid``: The 16-bit product ID of the device
.. function:: EvdevDevice:name()
The device name as set by the kernel
.. function:: EvdevDevice:usages()
Returns a nested table of all usages that are currently enabled for this
device. Any type that exists on the device has a table assigned and in this
table any code that exists on the device is a boolean true.
For example:
.. code-block:: lua
{
evdev.REL_X = true,
evdev.REL_Y = true,
evdev.BTN_LEFT = true,
}
All other usage ``nil``, so that the following code is possible:
.. code-block:: lua
if code[evdev.REL_X] then
-- do something
end
If the device has since been discarded by libinput, this function returns an
empty table.
.. function:: EvdevDevice:absinfos()
Returns a table of all ``EV_ABS`` codes that are currently enabled for this device.
The event code is the key, each value is a table containing the following keys:
``minimum``, ``maximum``, ``fuzz``, ``flat``, ``resolution``.
.. code-block:: lua
{
evdev.ABS_X = {
minimum = 0,
maximum = 1234,
fuzz = 0,
flat = 0,
resolution = 45,
},
}
If the device has since been discarded by libinput, this function returns an
empty table.
.. function:: EvdevDevice:udev_properties()
Returns a table containing a filtered list of udev properties available on this device
in the form ``{ property_name = property_value, ... }``.
udev properties used as a boolean (e.g. ``ID_INPUT``) are only present if their
value is a logical true.
Version 1 of the plugin API supports the following udev properties:
- ``ID_INPUT`` and all of ``ID_INPUT_*`` that denote the device type as assigned
by udev. This information is usually used by libinput to determine a
device type. Note that for historical reasons these properties have
varying rules - some properties may be mutually exclusive, others are
independent, others may only be set if another property is set. Refer to
the udev documentation (if any) for details. ``ID_INPUT_WIDTH_MM`` and
``ID_INPUT_HEIGHT_MM`` are excluded from this set.
If the device has since been discarded by libinput, this function returns an
empty table.
.. function:: EvdevDevice:enable_evdev_usage(usage)
Enable the given :ref:`evdev usage <plugins_api_evdev_usage>` for this device.
Use :ref:`plugins_api_evdev_global` for better readability,
e.g. ``device:enable_evdev_usage(evdev.REL_X)``.
This function must not be used for ``ABS_*`` events, use ``set_absinfo()`` instead.
If the device has since been discarded by libinput, this function does nothing.
.. function:: EvdevDevice:disable_evdev_usage(usage)
Disable the given :ref:`evdev usage <plugins_api_evdev_usage>` for this device.
Use :ref:`plugins_api_evdev_global` for better readability,
e.g. ``device:disable_evdev_usage(evdev.REL_X)``.
If the device has since been discarded by libinput, this function does nothing.
.. function:: EvdevDevice:set_absinfo(usage, absinfo)
Set the absolute axis information for the given :ref:`evdev usage <plugins_api_evdev_usage>`
if it does not yet exist on the device. The ``absinfo`` argument is a table
containing zero or more of the following keys: ``min``, ``max``, ``fuzz``,
``flat``, ``resolution``. Any missing key defaults the corresponding
value from the device if the device already has this event code or zero otherwise.
In other words the following code is enough to change the resolution but leave
everything else as-is:
.. code-block:: lua
local absinfo = {
resolution = 40,
}
device:set_absinfo(evdev.ABS_X, absinfo)
device:set_absinfo(evdev.ABS_Y, absinfo)
Use :ref:`plugins_api_evdev_global` for better readability as shown in the
example above.
If the device has since been discarded by libinput, this function does nothing.
.. note:: Overriding the absinfo values often indicates buggy firmware. This should
typically be fixed with an entry in the
`60-evdev.hwdb <https://github.com/systemd/systemd/blob/main/hwdb.d/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 <plugins_api_evdev_frame>` has
started for this device. If the callback returns a value other than
``nil`` or an empty table, that value is the frame with any modified
events.
.. code-block:: lua
device:connect("evdev-frame", function (device, frame, timestamp)
-- change any event into a movement left by 1 pixel
move_left = {
{ usage = evdev.EV_REL, code = evdev.REL_X, value = -1, },
}
return move_left
end
The timestamp of an event frame is in microseconds in ``CLOCK_MONOTONIC``, see
``libinput.now()`` for details.
For performance reasons plugins that do not modify the event frame should
return ``nil`` (or nothing) instead of the event frame given as argument.
- ``"device-removed"``: This device was removed by libinput. This may happen
without the device ever becoming a libinput device as seen by libinput's
public API (e.g. if the device does not meet the requirements to be
added). Once this callback is invoked, the plugin should remove any
references to this device and stop using it.
.. code-block:: lua
device:connect("new-evdev-device", function (device) ... end)
Functions to query the device's capabilities (e.g. ``usages()``) will
return an empty table.
.. function:: EvdevDevice:disconnect(name)
Disconnect the existing callback (if any) for the given event name. See
``EvdevDevice:connect()`` for a list of supported names.
.. function:: EvdevDevice:inject_frame(frame)
.. warning:: This function is only available from inside a timer callback.
Inject an :ref:`evdev frame <plugins_api_evdev_frame>` into the event stream
for this device. This emulates that same event frame being sent by the kernel
immediately with the current time.
Assuming three plugins P1, P2 and P3, if P2 injects a frame the frame is
seen by P1, P2 and P3.
This is rarely the right API to use. Injecting frames at the lowest level
may make other plugins behave unexpectedly. Use ``prepend_frame`` or
``append_frame`` instead.
.. warning:: The injected frame will be seen by all plugins, including the
injecting frame. Ensure a guard is in place to prevent recursion.
.. function:: EvdevDevice:prepend_frame(frame)
Prepend an :ref:`evdev frame <plugins_api_evdev_frame>` for this device
**before** the current frame (if any). This function can only be called from
within a device's ``frame()`` handler or from within the plugin's timer
callback function.
Assuming three plugins P1, P2 and P3, if P2 injects a frame the frame is
seen only by P3.
For example, to change a single event into a drag, prepend a button
down and append a button up before each event:
.. code:: lua
function frame_handler(device, frame, timestamp)
device:prepend_frame({
{ usage = evdev.BTN_LEFT, value = 1}
})
device:append_frame({
{ usage = evdev.BTN_LEFT, value = 0}
})
return nil -- return the frame unmodified
-- this results in the event sequence
-- button down, frame, button up
-- to be passed to the next plugin
end
If called from within the plugin's timer there is no current frame and this
function is identical to ``append_frame()``.
.. function:: EvdevDevice:append_frame(frame)
Appends an :ref:`evdev frame <plugins_api_evdev_frame>` for this device
**after** the current frame (if any). This function can only be called from
within a device's ``frame()`` handler or from within the plugin's timer
callback function.
If called from within the plugin's timer there is no current frame and this
function is identical to ``prepend_frame()``.
See ``prepend_frame()`` for more details.

View file

@ -155,6 +155,7 @@ src_rst = files(
'middle-button-emulation.rst', 'middle-button-emulation.rst',
'normalization-of-relative-motion.rst', 'normalization-of-relative-motion.rst',
'palm-detection.rst', 'palm-detection.rst',
'lua-plugins.rst',
'pointer-acceleration.rst', 'pointer-acceleration.rst',
'reporting-bugs.rst', 'reporting-bugs.rst',
'scrolling.rst', 'scrolling.rst',

View file

@ -2,7 +2,7 @@ project('libinput', 'c',
version : '1.29.0', version : '1.29.0',
license : 'MIT/Expat', license : 'MIT/Expat',
default_options : [ 'c_std=gnu99', 'warning_level=2' ], default_options : [ 'c_std=gnu99', 'warning_level=2' ],
meson_version : '>= 0.56.0') meson_version : '>= 0.64.0')
libinput_version = meson.project_version().split('.') libinput_version = meson.project_version().split('.')
@ -168,7 +168,21 @@ dep_libevdev = dependency('libevdev', version: '>= 1.10.0')
dep_lm = cc.find_library('m', required : false) dep_lm = cc.find_library('m', required : false)
dep_rt = cc.find_library('rt', required : false) dep_rt = cc.find_library('rt', required : false)
config_h.set10('HAVE_PLUGINS', true) dep_lua = dependency('lua-5.4', 'lua5.4', 'lua',
version : '>= 5.4',
required : get_option('lua-plugins'))
have_lua = dep_lua.found()
config_h.set10('HAVE_LUA', have_lua)
have_plugins = dep_lua.found()
config_h.set10('HAVE_PLUGINS', have_plugins)
summary({
'Plugins enabled' : have_plugins,
'Lua Plugin support' : have_lua,
},
section : 'Plugins',
bool_yn : true)
# Include directories # Include directories
includes_include = include_directories('include') includes_include = include_directories('include')
@ -417,10 +431,17 @@ src_libinput = src_libfilter + [
'src/timer.c', 'src/timer.c',
'src/util-libinput.c', 'src/util-libinput.c',
] ]
if have_mtdev if have_mtdev
src_libinput += ['src/libinput-plugin-mtdev.c'] src_libinput += ['src/libinput-plugin-mtdev.c']
endif endif
if dep_lua.found()
src_libinput += [
'src/libinput-plugin-lua.c',
]
endif
deps_libinput = [ deps_libinput = [
dep_mtdev, dep_mtdev,
dep_udev, dep_udev,
@ -430,7 +451,8 @@ deps_libinput = [
dep_rt, dep_rt,
dep_libwacom, dep_libwacom,
dep_libinput_util, dep_libinput_util,
dep_libquirks dep_libquirks,
dep_lua,
] ]
libinput_version_h_config = configuration_data() libinput_version_h_config = configuration_data()
@ -479,6 +501,8 @@ git_version_h = vcs_tag(command : ['git', 'describe'],
input : 'src/libinput-git-version.h.in', input : 'src/libinput-git-version.h.in',
output :'libinput-git-version.h') output :'libinput-git-version.h')
subdir('plugins')
############ documentation ############ ############ documentation ############
if get_option('documentation') if get_option('documentation')
@ -1024,6 +1048,10 @@ if get_option('tests')
'test/test-switch.c', 'test/test-switch.c',
'test/test-quirks.c', 'test/test-quirks.c',
] ]
if have_plugins and have_lua
tests_sources += ['test/test-plugins-lua.c']
endif
libinput_test_runner_sources = litest_sources + tests_sources libinput_test_runner_sources = litest_sources + tests_sources
libinput_test_runner = executable('libinput-test-suite', libinput_test_runner = executable('libinput-test-suite',
libinput_test_runner_sources, libinput_test_runner_sources,
@ -1065,6 +1093,9 @@ if get_option('tests')
'trackpoint', 'trackpoint',
'udev', 'udev',
] ]
if have_plugins and have_lua
collections += ['lua']
endif
foreach group : collections foreach group : collections
test('libinput-test-suite-@0@'.format(group), test('libinput-test-suite-@0@'.format(group),

View file

@ -42,3 +42,7 @@ option('internal-event-debugging',
type: 'boolean', type: 'boolean',
value: false, 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') 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')

View file

@ -0,0 +1,61 @@
-- SPDX-License-Identifier: MIT
--
-- This is an example libinput plugin
--
-- This plugin delays any event with relative motion by the given DELAY
-- by storing it in a table and replaying it via a timer callback later.
-- UNCOMMENT THIS LINE TO ACTIVATE THE PLUGIN
-- libinput:register({1})
DELAY = 1500 * 1000 -- 1.5s
next_timer_expiry = 0
devices = {}
function timer_expired(time_in_microseconds)
next_timer_expiry = 0
for device, frames in pairs(devices) do
while #frames > 0 and frames[1].time <= time_in_microseconds do
--- we don't have a current frame so it doesn't matter
--- whether we prepend or append
device:prepend_frame(frames[1].frame)
table.remove(frames, 1)
end
local next_frame = frames[1]
if next_frame and (next_timer_expiry == 0 or next_frame.time < next_timer_expiry) then
next_timer_expiry = next_frame.time
end
end
if next_timer_expiry ~= 0 then
libinput:timer_set_absolute(next_timer_expiry)
end
end
function frame(device, frame, timestamp)
for _, v in ipairs(frame) do
if v.usage == evdev.REL_X or v.usage == evdev.REL_Y then
next_time = timestamp + DELAY
table.insert(devices[device], {
time = next_time,
frame = frame
})
if next_timer_expiry == 0 then
next_timer_expiry = next_time
libinput:timer_set_absolute(next_timer_expiry)
end
return {} -- discard frame
end
end
return nil
end
function device_new(device)
local usages = device:usages()
if usages[evdev.REL_X] then
devices[device] = {}
device:connect("evdev-frame", frame)
end
end
libinput:connect("new-evdev-device", device_new)
libinput:connect("timer-expired", timer_expired)

40
plugins/10-dwt.lua Normal file
View file

@ -0,0 +1,40 @@
-- SPDX-License-Identifier: MIT
--
-- This plugin implements a very simple version of disable-while-typing.
-- It monitors all keyboard devices and if any of them send an event,
-- any touchpad device is disabled for 2 seconds.
-- And "disabled" means any event from that touchpad is simply
-- discarded.
--
-- Install this file in /etc/libinput/plugins and
--
-- UNCOMMENT THIS LINE TO ACTIVATE THE PLUGIN
-- libinput:register({1})
tp_enabled = true
libinput:connect("timer-expired", function(_, now)
log.debug("touchpad enabled")
tp_enabled = true
end)
libinput:connect("new-evdev-device", function (_, device)
local props = device:udev_properties()
if props.ID_INPUT_KEYBOARD then
device:connect("evdev-frame", function (_, _, _)
libinput:timer_set_relative(2000000)
if tp_enabled then
log.debug("touchpad disabled")
tp_enabled = false
end
end)
elseif props.ID_INPUT_TOUCHPAD then
log.debug("Touchpad detected: " .. device:name())
device:connect("evdev-frame", function (_, frame, _)
if not tp_enabled then
-- Returning an empty table discards the event.
return {}
end
end)
end
end)

87
plugins/10-example.lua Normal file
View file

@ -0,0 +1,87 @@
-- SPDX-License-Identifier: MIT
--
-- This is an example libinput plugin
--
-- This plugin swaps left and right buttons on any device that has both buttons.
-- Let's create a plugin. A single Lua script may create more than one
-- plugin instance but that's a bit of a nice use-case. Most scripts
-- should be a single plugin.
-- A plugin needs to be registered to activate. If it isn't, it is
-- cleaned up immediately. In the register call we supply
-- the list of plugin versions we support. Currently we only
-- have version 1.
--
-- UNCOMMENT THIS LINE TO ACTIVATE THE PLUGIN
-- libinput:register({1})
-- Note to the reader: it will be easier to understand this example
-- if you read it bottom-up from here on.
-- The callback for our "evdev-frame" signal.
-- These frames are sent *before* libinput gets to handle them so
-- any modifications will affect libinput.
function frame(device, frame, time_in_microseconds)
-- Frame is a table in the form
-- { { usage: 123, value: 3 }, ... }
-- Let's use the evdev module to make it more readable, evdev.KEY_A
-- is simply the value (0x1 << 16) | 0x1 (see linux/input-event-codes.h)
for _, v in ipairs(frame) do
-- If we get a right button event, change it to left, and
-- vice versa. Because this happens before libinput (or the next
-- plugin in the precedence order) sees the
-- frame it doesn't know that the swap happend.
if v.usage == evdev.BTN_RIGHT then
v.usage = evdev.BTN_LEFT
elseif v.usage == evdev.BTN_LEFT then
v.usage = evdev.BTN_RIGHT
end
end
-- We changed the frame, let's return it. If we return nil
-- the original frame is being used as-is.
return frame
end
-- This is a timer callback. It is invoked after one second
-- (see below) but only once - re-set the timer so
-- it goes off every second.
function timer_expired(time_in_microseconds)
libinput:timer_set_absolute(time_in_microseconds + 1000000)
end
-- Callback for the "new-evdev-device" signal, see below
-- The argument is the EvdevDevice object, see the documentation.
function device_new(device)
-- A table of evdev usages available on our device.
-- Using the evdev module makes it more readable but you can
-- use numbers (which is what evdev.EV_KEY and
-- friends resolve to anyway).
local usages = device:usages()
if usages[evdev.BTN_LEFT] and usages[evdev.BTN_RIGHT] then
-- The "evdev-frame" callback is invoked whenever the device
-- provided us with one evdev frame, i.e. a bunch of events up
-- to excluding EV_SYN SYN_REPORT.
device:connect("evdev-frame", frame)
end
-- The device has udev information, let's print it out. Right
-- now all we get are the ID_INPUT_ bits.
-- If this is empty we know libinput will ignore this device anyway
local udev_info = device:udev_properties()
for k, v in pairs(udev_info) do
log.debug(k .. "=" .. v)
end
end
-- Let's connect to the "new-evdev-device" signal. This function
-- is invoked when libinput detects a new evdev device (but before
-- that device is actually available to libinput as libinput device).
-- This allows us to e.g. change properties on the device.
libinput:connect("new-evdev-device", device_new)
-- Set our timer to expire 1s from now (in microseconds).
-- Timers are absolute, so they need to be added to the
-- current time
libinput:connect("timer-expired", timer_expired)
libinput:timer_set_relative(1000000)

View file

@ -0,0 +1,32 @@
-- SPDX-License-Identifier: MIT
--
-- This plugin inverts the horizontal scroll direction of
-- the Logitech MX Master mouse. OOTB the mouse scrolls
-- in the opposite direction to all other mice out there.
--
-- This plugin is only needed when the mouse is connected
-- to the Logitech Bolt receiver - on that receiver we cannot
-- tell which device is connected.
--
-- For the Logitech Unifying receiver and Bluetooth please
-- add the ModelInvertHorizontalScrolling=1 quirk
-- in quirks/30-vendor-logitech.quirks.
--
--
-- Install this file in /etc/libinput/plugins and
--
-- UNCOMMENT THIS LINE TO ACTIVATE THE PLUGIN
-- libinput:register({1})
libinput:connect("new-evdev-device", function (_, device)
local info = device:info()
if info.vid == 0x046D and info.pid == 0xC548 then
device:connect("evdev-frame", function (_, frame, timestamp)
for _, event in ipairs(frame) do
if event.usage == evdev.REL_HWHEEL or event.usage == evdev.REL_HWHEEL_HI_RES then
event.value = -event.value
end
end
return frame
end)
end
end)

View file

@ -0,0 +1,22 @@
-- SPDX-License-Identifier: MIT
--
-- An example plugin to make the pointer go three times as fast
--
-- Install this file in /etc/libinput/plugins and
--
-- UNCOMMENT THIS LINE TO ACTIVATE THE PLUGIN
-- libinput:register({1})
libinput:connect("new-evdev-device", function(_, device)
local usages = device:usages()
if usages[evdev.REL_X] then
device:connect("evdev-frame", function(_, frame, timestamp)
for _, v in ipairs(frame) do
if v.usage == evdev.REL_X or v.usage == evdev.REL_Y then
-- Multiply the relative motion by 3
v.value = v.value * 3
end
end
return frame
end)
end
end)

View file

@ -0,0 +1,53 @@
-- SPDX-License-Identifier: MIT
--
-- An example plugin to make the pointer go three times as slow
--
-- Install this file in /etc/libinput/plugins and
--
-- UNCOMMENT THIS LINE TO ACTIVATE THE PLUGIN
-- libinput:register({1})
remainders = {}
function split(v)
if math.abs(v) >= 1.0 then
local i = math.floor(math.abs(v))
local r = math.abs(v) % 1.0
if v < 0.0 then
i = -i
r = -r
end
return i, r
else
return 0, v
end
end
function decelerate(device, x, y)
local remainder = remainders[device]
local rx, ry = 0, 0
if x ~= 0.0 then
rx, remainder.x = split(remainder.x + x/3.0)
end
if y ~= 0.0 then
ry, remainder.y = split(remainder.y + y/3.0)
end
return rx, ry
end
libinput:connect("new-evdev-device", function(device)
local usages = device:usages()
if usages[evdev.REL_X] then
remainders[device] = { x = 0.0, y = 0.0 }
device:connect("evdev-frame", function(_, frame, timestamp)
for _, v in ipairs(frame) do
if v.usage == evdev.REL_X then
v.value, _ = decelerate(device, v.value, 0.0)
elseif v.usage == evdev.REL_Y then
_, v.value = decelerate(device, 0.0, v.value)
end
end
return frame
end)
end
end)

13
plugins/meson.build Normal file
View file

@ -0,0 +1,13 @@
plugins = [
'10-example.lua',
'10-dwt.lua',
'10-logitech-mx-master-horiz-scroll.lua',
'10-pointer-go-faster.lua',
'10-pointer-go-slower.lua',
'10-delay-motion.lua',
]
fs = import('fs')
foreach plugin : plugins
fs.copyfile(plugin)
endforeach

1361
src/libinput-plugin-lua.c Normal file

File diff suppressed because it is too large Load diff

32
src/libinput-plugin-lua.h Normal file
View file

@ -0,0 +1,32 @@
/*
* Copyright © 2025 Red Hat, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice (including the next
* paragraph) shall be included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
#pragma once
#include "config.h"
#include "libinput-plugin.h"
#include "libinput.h"
struct libinput_plugin *
libinput_lua_plugin_new_from_path(struct libinput *libinput, const char *path);

View file

@ -31,6 +31,7 @@
#include "evdev-frame.h" #include "evdev-frame.h"
#include "evdev-plugin.h" #include "evdev-plugin.h"
#include "libinput-plugin-button-debounce.h" #include "libinput-plugin-button-debounce.h"
#include "libinput-plugin-lua.h"
#include "libinput-plugin-mouse-wheel-lowres.h" #include "libinput-plugin-mouse-wheel-lowres.h"
#include "libinput-plugin-mouse-wheel.h" #include "libinput-plugin-mouse-wheel.h"
#include "libinput-plugin-mtdev.h" #include "libinput-plugin-mtdev.h"
@ -363,6 +364,18 @@ libinput_plugin_system_load_plugins(struct libinput *libinput,
return 0; return 0;
} }
#if HAVE_LUA
_autostrvfree_ char **directories = steal(&libinput->plugin_system.directories);
size_t nfiles = 0;
_autostrvfree_ char **plugin_files =
list_files((const char **)directories, ".lua", &nfiles);
for (size_t i = 0; i < nfiles; i++) {
char *path = plugin_files[i];
log_debug(libinput, "Loading plugin from %s\n", path);
libinput_lua_plugin_new_from_path(libinput, path);
}
#endif
libinput_plugin_system_load_internal_plugins(libinput, libinput_plugin_system_load_internal_plugins(libinput,
&libinput->plugin_system); &libinput->plugin_system);
libinput->plugin_system.loaded = true; libinput->plugin_system.loaded = true;

1098
test/test-plugins-lua.c Normal file

File diff suppressed because it is too large Load diff