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,