git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commit49af8d3f950ed95c82f1eae9e244332456066f3c
parent1a9801707c
authorIsaac Freund <[email protected]>
date2025-12-28 19:58
protocol: implement river-libinput-config-v1

 build.zig                             |   2 +
 build.zig.zon                         |   4 +-
 protocol/river-libinput-config-v1.xml | 885 ++++++++++++++++++++++++++++++++++
 river/InputDevice.zig                 |  13 +-
 river/LibinputAccelConfig.zig         | 124 +++++
 river/LibinputConfig.zig              | 118 +++++
 river/LibinputDevice.zig              | 561 +++++++++++++++++++++
 river/Server.zig                      |   5 +
 8 files changed, 1709 insertions(+), 3 deletions(-)

diff --git a/build.zig b/build.zig
index a689d0d..1bb009a 100644
--- a/build.zig
+++ b/build.zig
@@ -80,6 +80,7 @@ pub fn build(b: *Build) !void {
     scanner.addCustomProtocol(b.path("protocol/river-xkb-bindings-v1.xml"));
     scanner.addCustomProtocol(b.path("protocol/river-layer-shell-v1.xml"));
     scanner.addCustomProtocol(b.path("protocol/river-input-management-v1.xml"));
+    scanner.addCustomProtocol(b.path("protocol/river-libinput-config-v1.xml"));
 
     scanner.addCustomProtocol(b.path("protocol/wlr-layer-shell-unstable-v1.xml"));
     scanner.addCustomProtocol(b.path("protocol/wlr-output-power-management-unstable-v1.xml"));
@@ -110,6 +111,7 @@ pub fn build(b: *Build) !void {
     scanner.generate("river_xkb_bindings_v1", 1);
     scanner.generate("river_layer_shell_v1", 1);
     scanner.generate("river_input_manager_v1", 1);
+    scanner.generate("river_libinput_config_v1", 1);
 
     scanner.generate("zwlr_output_power_manager_v1", 1);
     scanner.generate("zwlr_layer_shell_v1", 4);
diff --git a/build.zig.zon b/build.zig.zon
index a5a1fd2..65cf0e5 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -14,8 +14,8 @@
             .hash = "pixman-0.3.0-LClMnz2VAAAs7QSCGwLimV5VUYx0JFnX5xWU6HwtMuDX",
         },
         .wayland = .{
-            .url = "git+https://codeberg.org/ifreund/zig-wayland?ref=main#52b54b71f1665b1b7ba61bf71a9319726e618fb6",
-            .hash = "wayland-0.5.0-dev-lQa1ki3YAQCEdP63PCLFnVQzfXAixkrXsPavlEl8UkPe",
+            .url = "git+https://codeberg.org/ifreund/zig-wayland?ref=main#f2480d25764a50ed2fe29f49e4209c074a557f46",
+            .hash = "wayland-0.5.0-dev-lQa1kvTUAQCsD8AobfOXJA_-TVG-WTYXju68OZ8L85RM",
         },
         .wlroots = .{
             .url = "https://codeberg.org/ifreund/zig-wlroots/archive/v0.19.3.tar.gz",
diff --git a/protocol/river-libinput-config-v1.xml b/protocol/river-libinput-config-v1.xml
new file mode 100644
index 0000000..869878a
--- /dev/null
+++ b/protocol/river-libinput-config-v1.xml
@@ -0,0 +1,885 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<protocol name="river_libinput_config_v1">
+  <copyright>
+    Copyright 2025 The River Developers
+
+    Permission to use, copy, modify, and/or distribute this software for any
+    purpose with or without fee is hereby granted, provided that the above
+    copyright notice and this permission notice appear in all copies.
+
+    THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+    WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+    MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+    SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+    WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+    OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+    CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+  </copyright>
+
+  <description summary="configure libinput devices">
+    This protocol exposes libinput device configuration APIs. The libinput
+    documentation should be referred to for detailed information on libinput's
+    behavior.
+
+    This protocol is designed so that (hopefully) any backwards compatible
+    change to libinput's API can be matched with a backwards compatible change
+    to this protocol.
+
+    Note: the libinput API uses floating point types (float and double in C)
+    which are not (yet?) natively supported by the Wayland protocol. However,
+    the Wayland protocol does support sending arbitrary bytes through the array
+    argument type. This protocol uses e.g. type="array" summary="double" to
+    indicate a native-endian IEEE-754 64-bit double value.
+
+    The key words "must", "must not", "required", "shall", "shall not",
+    "should", "should not", "recommended", "may", and "optional" in this
+    document are to be interpreted as described in IETF RFC 2119.
+
+    Warning! The protocol described in this file is currently in the testing
+    phase. Backward compatible changes may be added together with the
+    corresponding interface version bump. Backward incompatible changes can only
+    be done by creating a new major version of the extension.
+  </description>
+
+  <interface name="river_libinput_config_v1" version="1">
+    <description summary="libinput config global interface">
+      Global interface for configuring libinput devices. This global should
+      only be advertised if river_input_manager_v1 is advertised as well.
+    </description>
+
+    <enum name="error">
+      <entry name="invalid_arg" value="0"
+        summary="invalid enum value or similar"/>
+      <entry name="invalid_destroy" value="1"/>
+    </enum>
+
+    <request name="stop">
+      <description summary="stop sending events">
+        This request indicates that the client no longer wishes to receive
+        events on this object.
+
+        The Wayland protocol is asynchronous, which means the server may send
+        further events until the stop request is processed. The client must wait
+        for a river_libinput_config_v1.finished event before destroying this
+        object.
+      </description>
+    </request>
+
+    <event name="finished">
+      <description summary="the server has finished with the object">
+        This event indicates that the server will send no further events on this
+        object. The client should destroy the object. See
+        river_libinput_config_v1.destroy for more information.
+      </description>
+    </event>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the river_libinput_config_v1 object">
+        This request should be called after the finished event has been received
+        to complete destruction of the object.
+
+        It is a protocol error to make this request before the finished event
+        has been received.
+
+        If a client wishes to destroy this object it should send a
+        river_libinput_config_v1.stop request and wait for a
+        river_libinput_config_v1.finished event. Once the finished event is
+        received it is safe to destroy this object and any other objects created
+        through this interface.
+      </description>
+    </request>
+
+    <event name="libinput_device">
+      <description summary="new libinput device">
+        A new libinput device has been created. Not every river_input_device_v1
+        is necessarily a libinput device as well.
+      </description>
+      <arg name="id" type="new_id" interface="river_libinput_device_v1"/>
+    </event>
+
+    <request name="create_accel_config">
+      <description summary="create a acceleration config">
+        Create a acceleration config which can be applied
+        with river_libinput_device_v1.apply_accel_config.
+      </description>
+      <arg name="id" type="new_id"
+        interface="river_libinput_accel_config_v1"/>
+      <arg name="profile" type="uint"
+        enum="river_libinput_device_v1.accel_profile"/>
+    </request>
+  </interface>
+
+  <interface name="river_libinput_device_v1" version="1">
+    <description summary="a libinput device">
+      In general, *_support events will be sent exactly once directly after the
+      river_libinput_device_v1 is created. *_default events will be sent after
+      *_support events if the config option is supported, and *_current events
+      willl be sent after the *_default events and again whenever the config
+      option is changed.
+    </description>
+
+    <enum name="error">
+      <entry name="invalid_arg" value="0"
+        summary="invalid enum value or similar"/>
+    </enum>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the libinput device object">
+        This request indicates that the client will no longer use the input
+        device object and that it may be safely destroyed.
+      </description>
+    </request>
+
+    <event name="removed">
+      <description summary="the libinput device is removed">
+        This event indicates that the libinput device has been removed.
+
+        The server will send no further events on this object and ignore any
+        request (other than river_libinput_device_v1.destroy) made after this
+        event is sent. The client should destroy this object with the
+        river_libinput_device_v1.destroy request to free up resources.
+      </description>
+    </event>
+
+    <event name="input_device">
+      <description summary="corresponding river input device">
+        The river_input_device_v1 corresponding to this libinput device.
+        This event will always be the first event sent on the
+        river_libinput_device_v1 object, and it will be sent exactly once.
+      </description>
+      <arg name="device" type="object" interface="river_input_device_v1"/>
+    </event>
+
+    <enum name="send_events_modes" bitfield="true">
+      <entry name="enabled" value="0"/>
+      <entry name="disabled" value="1"/>
+      <entry name="disabled_on_external_mouse" value="2"/>
+    </enum>
+
+    <event name="send_events_support">
+      <description summary="supported send events modes">
+        Supported send events modes.
+      </description>
+      <arg name="modes" type="uint" enum="send_events_modes"/>
+    </event>
+
+    <event name="send_events_default">
+      <description summary="default send events mode">
+        Default send events mode.
+      </description>
+      <arg name="mode" type="uint" enum="send_events_modes"/>
+    </event>
+
+    <event name="send_events_current">
+      <description summary="current send events mode">
+        Current send events mode.
+      </description>
+      <arg name="mode" type="uint" enum="send_events_modes"/>
+    </event>
+
+    <request name="set_send_events">
+      <description summary="set send events mode">
+        Set the send events mode for the device.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="mode" type="uint" enum="send_events_modes"/>
+    </request>
+
+    <enum name="tap_state">
+      <entry name="disabled" value="0"/>
+      <entry name="enabled" value="1"/>
+    </enum>
+
+    <event name="tap_support">
+      <description summary="tap-to-click/drag support">
+        The number of fingers supported for tap-to-click/drag.
+        If finger_count is 0, tap-to-click and drag are unsupported.
+      </description>
+      <arg name="finger_count" type="int"/>
+    </event>
+
+    <event name="tap_default">
+      <description summary="default tap-to-click state">
+        Default tap-to-click state.
+      </description>
+      <arg name="state" type="uint" enum="tap_state"/>
+    </event>
+
+    <event name="tap_current">
+      <description summary="current tap-to-click state">
+        Current tap-to-click state.
+      </description>
+      <arg name="state" type="uint" enum="tap_state"/>
+    </event>
+
+    <request name="set_tap">
+      <description summary="enable/disable tap-to-click">
+        Configure tap-to-click on this device, with a default mapping of
+        1, 2, 3 finger tap mapping to left, right, middle click, respectively.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="state" type="uint" enum="tap_state"/>
+    </request>
+
+    <enum name="tap_button_map">
+      <entry name="lrm" value="0"
+        summary="1/2/3 finger tap maps to left/right/middle"/>
+      <entry name="lmr" value="1"
+        summary="1/2/3 finger tap maps to left/middle/right"/>
+    </enum>
+
+    <event name="tap_button_map_default">
+      <description summary="default tap-to-click button map">
+        Default tap-to-click button map.
+      </description>
+      <arg name="button_map" type="uint" enum="tap_button_map"/>
+    </event>
+
+    <event name="tap_button_map_current">
+      <description summary="current tap-to-click button map">
+        Current tap-to-click button map.
+      </description>
+      <arg name="button_map" type="uint" enum="tap_button_map"/>
+    </event>
+
+    <request name="set_tap_button_map">
+      <description summary="set tap-to-click button map">
+        Set the finger number to button number mapping for tap-to-click. The
+        default mapping on most devices is to have a 1, 2 and 3 finger tap to
+        map to the left, right and middle button, respectively.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="button_map" type="uint" enum="tap_button_map"/>
+    </request>
+
+    <enum name="drag_state">
+      <entry name="disabled" value="0"/>
+      <entry name="enabled" value="1"/>
+    </enum>
+
+    <event name="drag_default">
+      <description summary="default tap-and-drag state">
+        Default tap-and-drag state.
+      </description>
+      <arg name="state" type="uint" enum="drag_state"/>
+    </event>
+
+    <event name="drag_current">
+      <description summary="current tap-and-drag state">
+        Current tap-and-drag state.
+      </description>
+      <arg name="state" type="uint" enum="drag_state"/>
+    </event>
+
+    <request name="set_drag">
+      <description summary="set tap-and-drag state">
+        Configure tap-and-drag functionality on the device.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="state" type="uint" enum="drag_state"/>
+    </request>
+
+    <enum name="drag_lock_state">
+      <entry name="disabled" value="0"/>
+      <entry name="enabled_timeout" value="1"/>
+      <entry name="enabled_sticky" value="2"/>
+    </enum>
+
+    <event name="drag_lock_default">
+      <description summary="default drag lock state">
+        Default drag lock state.
+      </description>
+      <arg name="state" type="uint" enum="drag_lock_state"/>
+    </event>
+
+    <event name="drag_lock_current">
+      <description summary="current drag lock state">
+        Current drag lock state.
+      </description>
+      <arg name="state" type="uint" enum="drag_lock_state"/>
+    </event>
+
+    <request name="set_drag_lock">
+      <description summary="set drag lock state">
+        Configure drag-lock during tapping on this device. When enabled, a
+        finger may be lifted and put back on the touchpad and the drag process
+        continues. A timeout for lifting the finger is optional. When disabled,
+        lifting the finger during a tap-and-drag will immediately stop the drag.
+        See the libinput documentation for more details.
+       </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="state" type="uint" enum="drag_lock_state"/>
+    </request>
+
+    <event name="three_finger_drag_support">
+      <description summary="three finger drag support">
+        The number of fingers supported for three/four finger drag.
+        If finger_count is less than 3, three finger drag is unsupported.
+      </description>
+      <arg name="finger_count" type="int"/>
+    </event>
+
+    <enum name="three_finger_drag_state">
+      <entry name="disabled" value="0"/>
+      <entry name="enabled_3fg" value="1"/>
+      <entry name="enabled_4fg" value="2"/>
+    </enum>
+
+    <event name="three_finger_drag_default">
+      <description summary="default three finger drag state">
+        Default three finger drag state.
+      </description>
+      <arg name="state" type="uint" enum="three_finger_drag_state"/>
+    </event>
+
+    <event name="three_finger_drag_current">
+      <description summary="current three finger drag state">
+        Current three finger drag state.
+      </description>
+      <arg name="state" type="uint" enum="three_finger_drag_state"/>
+    </event>
+
+    <request name="set_three_finger_drag">
+      <description summary="set three finger drag state">
+        Configure three finger drag functionality for the device.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="state" type="uint" enum="three_finger_drag_state"/>
+    </request>
+
+    <event name="calibration_matrix_support">
+      <description summary="support for a calibration matrix">
+        A calibration matrix is supported if the supported argument is non-zero.
+      </description>
+      <arg name="supported" type="int" summary="boolean"/>
+    </event>
+
+    <event name="calibration_matrix_default">
+      <description summary="default calibration matrix">
+        Default calibration matrix.
+      </description>
+      <arg name="matrix" type="array" summary="array of 6 floats"/>
+    </event>
+
+    <event name="calibration_matrix_current">
+      <description summary="current calibration matrix">
+        Current calibration matrix.
+      </description>
+      <arg name="matrix" type="array" summary="array of 6 floats"/>
+    </event>
+
+    <request name="set_calibration_matrix">
+      <description summary="set calibration matrix">
+        Set calibration matrix.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="matrix" type="array" summary="array of 6 floats"/>
+    </request>
+
+    <enum name="accel_profile">
+      <entry name="none" value="0"/>
+      <entry name="flat" value="1"/>
+      <entry name="adaptive" value="2"/>
+      <entry name="custom" value="4"/>
+    </enum>
+
+    <enum name="accel_profiles" bitfield="true">
+      <entry name="none" value="0"/>
+      <entry name="flat" value="1"/>
+      <entry name="adaptive" value="2"/>
+      <entry name="custom" value="4"/>
+    </enum>
+
+    <event name="accel_profiles_support">
+      <description summary="supported acceleration profiles">
+        Supported acceleration profiles.
+      </description>
+      <arg name="profiles" type="uint" enum="accel_profiles"/>
+    </event>
+
+    <event name="accel_profile_default">
+      <description summary="default acceleration profile">
+        Default acceleration profile.
+      </description>
+      <arg name="profile" type="uint" enum="accel_profile"/>
+    </event>
+
+    <event name="accel_profile_current">
+      <description summary="current send events mode">
+        Current acceleration profile.
+      </description>
+      <arg name="profile" type="uint" enum="accel_profile"/>
+    </event>
+
+    <request name="set_accel_profile">
+      <description summary="set send events mode">
+        Set the acceleration profile.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="profile" type="uint" enum="accel_profile"/>
+    </request>
+
+    <event name="accel_speed_default">
+      <description summary="default acceleration speed">
+        Default acceleration speed.
+      </description>
+      <arg name="speed" type="array" summary="double"/>
+    </event>
+
+    <event name="accel_speed_current">
+      <description summary="current acceleration speed">
+        Current acceleration speed.
+      </description>
+      <arg name="speed" type="array" summary="double"/>
+    </event>
+
+    <request name="set_accel_speed">
+      <description summary="set acceleration speed">
+        Set the acceleration speed within a range of [-1, 1], where 0 is
+        the default acceleration for this device, -1 is the slowest acceleration
+        and 1 is the maximum acceleration available on this device.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="speed" type="array" summary="double"/>
+    </request>
+
+    <request name="apply_accel_config">
+      <description summary="apply acceleration config">
+        Apply a pointer accleration config.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="config" type="object" interface="river_libinput_accel_config_v1"/>
+    </request>
+
+    <event name="natural_scroll_support">
+      <description summary="support for natural scroll">
+        Natural scroll is supported if the supported argument is non-zero.
+      </description>
+      <arg name="supported" type="int" summary="boolean"/>
+    </event>
+
+    <enum name="natural_scroll_state">
+      <entry name="disabled" value="0"/>
+      <entry name="enabled" value="1"/>
+    </enum>
+
+    <event name="natural_scroll_default">
+      <description summary="default natural scroll">
+        Default natural scroll.
+      </description>
+      <arg name="state" type="uint" enum="natural_scroll_state"/>
+    </event>
+
+    <event name="natural_scroll_current">
+      <description summary="current natural scroll state">
+        Current natural scroll.
+      </description>
+      <arg name="state" type="uint" enum="natural_scroll_state"/>
+    </event>
+
+    <request name="set_natural_scroll">
+      <description summary="set natural scroll state">
+        Set natural scroll state.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="state" type="uint" enum="natural_scroll_state"/>
+    </request>
+
+    <event name="left_handed_support">
+      <description summary="support for left-handed mode">
+        Left-handed mode is supported if the supported argument is non-zero.
+      </description>
+      <arg name="supported" type="int" summary="boolean"/>
+    </event>
+
+    <enum name="left_handed_state">
+      <entry name="disabled" value="0"/>
+      <entry name="enabled" value="1"/>
+    </enum>
+
+    <event name="left_handed_default">
+      <description summary="default left-handed mode">
+        Default left-handed mode.
+      </description>
+      <arg name="state" type="uint" enum="left_handed_state"/>
+    </event>
+
+    <event name="left_handed_current">
+      <description summary="current left-handed mode state">
+        Current left-handed mode.
+      </description>
+      <arg name="state" type="uint" enum="left_handed_state"/>
+    </event>
+
+    <request name="set_left_handed">
+      <description summary="set left-handed mode state">
+        Set left-handed mode state.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="state" type="uint" enum="left_handed_state"/>
+    </request>
+
+    <enum name="click_method">
+      <entry name="none" value="0"/>
+      <entry name="button_areas" value="1"/>
+      <entry name="clickfinger" value="2"/>
+    </enum>
+
+    <enum name="click_methods" bitfield="true">
+      <entry name="none" value="0"/>
+      <entry name="button_areas" value="1"/>
+      <entry name="clickfinger" value="2"/>
+    </enum>
+
+    <event name="click_method_support">
+      <description summary="supported click methods">
+        The click methods suppported by the device.
+      </description>
+      <arg name="methods" type="uint" enum="click_methods"/>
+    </event>
+
+    <event name="click_method_default">
+      <description summary="default click method">
+        Default click method.
+      </description>
+      <arg name="method" type="uint" enum="click_method"/>
+    </event>
+
+    <event name="click_method_current">
+      <description summary="current click method">
+        Current click method.
+      </description>
+      <arg name="method" type="uint" enum="click_method"/>
+    </event>
+
+    <request name="set_click_method">
+      <description summary="set click method">
+        Set click method.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="method" type="uint" enum="click_method"/>
+    </request>
+
+    <enum name="clickfinger_button_map">
+      <entry name="lrm" value="0"/>
+      <entry name="lmr" value="1"/>
+    </enum>
+
+    <event name="clickfinger_button_map_default">
+      <description summary="default clickfinger button map">
+        Default clickfinger button map.
+        Supported if click_methods.clickfinger is supported.
+      </description>
+      <arg name="button_map" type="uint" enum="clickfinger_button_map"/>
+    </event>
+
+    <event name="clickfinger_button_map_current">
+      <description summary="current clickfinger button map">
+        Current clickfinger button map.
+        Supported if click_methods.clickfinger is supported.
+      </description>
+      <arg name="button_map" type="uint" enum="clickfinger_button_map"/>
+    </event>
+
+    <request name="set_clickfinger_button_map">
+      <description summary="set clickfinger button map">
+        Set clickfinger button map.
+        Supported if click_methods.clickfinger is supported.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="button_map" type="uint" enum="clickfinger_button_map"/>
+    </request>
+
+    <event name="middle_emulation_support">
+      <description summary="support for middle mouse button emulation">
+        Middle mouse button emulation is supported if the supported argument is
+        non-zero.
+      </description>
+      <arg name="supported" type="int" summary="boolean"/>
+    </event>
+
+    <enum name="middle_emulation_state">
+      <entry name="disabled" value="0"/>
+      <entry name="enabled" value="1"/>
+    </enum>
+
+    <event name="middle_emulation_default">
+      <description summary="default middle mouse button emulation">
+        Default middle mouse button emulation.
+      </description>
+      <arg name="state" type="uint" enum="middle_emulation_state"/>
+    </event>
+
+    <event name="middle_emulation_current">
+      <description summary="current middle mouse button emulation state">
+        Current middle mouse button emulation.
+      </description>
+      <arg name="state" type="uint" enum="middle_emulation_state"/>
+    </event>
+
+    <request name="set_middle_emulation">
+      <description summary="set middle mouse button emulation state">
+        Set middle mouse button emulation state.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="state" type="uint" enum="middle_emulation_state"/>
+    </request>
+
+    <enum name="scroll_method">
+      <entry name="no_scroll" value="0"/>
+      <entry name="two_finger" value="1"/>
+      <entry name="edge" value="2"/>
+      <entry name="on_button_down" value="4"/>
+    </enum>
+
+    <enum name="scroll_methods" bitfield="true">
+      <entry name="no_scroll" value="0"/>
+      <entry name="two_finger" value="1"/>
+      <entry name="edge" value="2"/>
+      <entry name="on_button_down" value="4"/>
+    </enum>
+
+    <event name="scroll_method_support">
+      <description summary="supported scroll methods">
+        The scroll methods suppported by the device.
+      </description>
+      <arg name="methods" type="uint" enum="scroll_methods"/>
+    </event>
+
+    <event name="scroll_method_default">
+      <description summary="default scroll method">
+        Default scroll method.
+      </description>
+      <arg name="method" type="uint" enum="scroll_method"/>
+    </event>
+
+    <event name="scroll_method_current">
+      <description summary="current scroll method">
+        Current scroll method.
+      </description>
+      <arg name="method" type="uint" enum="scroll_method"/>
+    </event>
+
+    <request name="set_scroll_method">
+      <description summary="set scroll method">
+        Set scroll method.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="method" type="uint" enum="scroll_method"/>
+    </request>
+
+    <event name="scroll_button_default">
+      <description summary="default scroll button">
+        Default scroll button.
+        Supported if scroll_methods.on_button_down is supported.
+      </description>
+      <arg name="button" type="uint"/>
+    </event>
+
+    <event name="scroll_button_current">
+      <description summary="current scroll button">
+        Current scroll button.
+        Supported if scroll_methods.on_button_down is supported.
+      </description>
+      <arg name="button" type="uint"/>
+    </event>
+
+    <request name="set_scroll_button">
+      <description summary="set scroll button">
+        Set scroll button.
+        Supported if scroll_methods.on_button_down is supported.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="button" type="uint"/>
+    </request>
+
+    <enum name="scroll_button_lock_state">
+      <entry name="disabled" value="0"/>
+      <entry name="enabled" value="1"/>
+    </enum>
+
+    <event name="scroll_button_lock_default">
+      <description summary="default scroll button lock state">
+        Default scroll button lock state.
+        Supported if scroll_methods.on_button_down is supported.
+      </description>
+      <arg name="state" type="uint" enum="scroll_button_lock_state"/>
+    </event>
+
+    <event name="scroll_button_lock_current">
+      <description summary="current scroll button lock state">
+        Current scroll button lock state.
+        Supported if scroll_methods.on_button_down is supported.
+      </description>
+      <arg name="state" type="uint" enum="scroll_button_lock_state"/>
+    </event>
+
+    <request name="set_scroll_button_lock">
+      <description summary="set scroll button lock state">
+        Set scroll button lock state.
+        Supported if scroll_methods.on_button_down is supported.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="state" type="uint" enum="scroll_button_lock_state"/>
+    </request>
+
+    <event name="dwt_support">
+      <description summary="support for disable-while-typing">
+        Disable-while-typing is supported if the supported argument is
+        non-zero.
+      </description>
+      <arg name="supported" type="int" summary="boolean"/>
+    </event>
+
+    <enum name="dwt_state">
+      <entry name="disabled" value="0"/>
+      <entry name="enabled" value="1"/>
+    </enum>
+
+    <event name="dwt_default">
+      <description summary="default disable-while-typing state">
+        Default disable-while-typing state.
+      </description>
+      <arg name="state" type="uint" enum="dwt_state"/>
+    </event>
+
+    <event name="dwt_current">
+      <description summary="current disable-while-typing state">
+        Current disable-while-typing state.
+      </description>
+      <arg name="state" type="uint" enum="dwt_state"/>
+    </event>
+
+    <request name="set_dwt">
+      <description summary="set disable-while-typing state">
+        Set disable-while-typing state.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="state" type="uint" enum="dwt_state"/>
+    </request>
+
+    <event name="dwtp_support">
+      <description summary="support for disable-while-trackpointing">
+        Disable-while-trackpointing is supported if the supported argument is
+        non-zero.
+      </description>
+      <arg name="supported" type="int" summary="boolean"/>
+    </event>
+
+    <enum name="dwtp_state">
+      <entry name="disabled" value="0"/>
+      <entry name="enabled" value="1"/>
+    </enum>
+
+    <event name="dwtp_default">
+      <description summary="default disable-while-trackpointing state">
+        Default disable-while-trackpointing state.
+      </description>
+      <arg name="state" type="uint" enum="dwtp_state"/>
+    </event>
+
+    <event name="dwtp_current">
+      <description summary="current disable-while-trackpointing state">
+        Current disable-while-trackpointing state.
+      </description>
+      <arg name="state" type="uint" enum="dwtp_state"/>
+    </event>
+
+    <request name="set_dwtp">
+      <description summary="set disable-while-trackpointing state">
+        Set disable-while-trackpointing state.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="state" type="uint" enum="dwtp_state"/>
+    </request>
+
+    <event name="rotation_support">
+      <description summary="support for rotation">
+        Rotation is supported if the supported argument is non-zero.
+      </description>
+      <arg name="supported" type="int" summary="boolean"/>
+    </event>
+
+    <event name="rotation_default">
+      <description summary="default rotation angle">
+        Default rotation angle.
+      </description>
+      <arg name="angle" type="uint"/>
+    </event>
+
+    <event name="rotation_current">
+      <description summary="current rotation angle">
+        Current rotation angle.
+      </description>
+      <arg name="angle" type="uint"/>
+    </event>
+
+    <request name="set_rotation">
+      <description summary="set rotation angle">
+        Set rotation angle in degrees clockwise off the logical neutral
+        position. Angle must be in the range [0-360).
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="angle" type="uint"/>
+    </request>
+  </interface>
+
+  <interface name="river_libinput_accel_config_v1" version="1">
+    <description summary="acceleration config">
+      The result returned by libinput on setting configuration for a device.
+    </description>
+
+    <enum name="error">
+      <entry name="invalid_arg" value="0"
+        summary="invalid enum value or similar"/>
+    </enum>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the accel object">
+        This request indicates that the client will no longer use the accel
+        config object and that it may be safely destroyed.
+      </description>
+    </request>
+
+    <enum name="accel_type">
+      <entry name="fallback" value="0"/>
+      <entry name="motion" value="1"/>
+      <entry name="scroll" value="2"/>
+    </enum>
+
+    <request name="set_points">
+      <description summary="define custom acceleration function">
+        Defines the acceleration function for a given movement type
+        in an acceleration configuration with custom accel profile.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="type" type="uint" enum="accel_type"/>
+      <arg name="step" type="array" summary="double"/>
+      <arg name="points" type="array" summary="array of doubles"/>
+    </request>
+  </interface>
+
+  <interface name="river_libinput_result_v1" version="1">
+    <description summary="config application result">
+      The result returned by libinput on setting configuration for a device.
+    </description>
+
+    <event name="success" type="destructor">
+      <description summary="config success">
+        The configuration was successfully applied to the device.
+      </description>
+    </event>
+
+    <event name="unsupported" type="destructor">
+      <description summary="config unsupported">
+        The configuration is unsupported by the device and was ignored.
+      </description>
+    </event>
+
+    <event name="invalid" type="destructor">
+      <description summary="config invalid">
+        The configuration is invalid and was ignored.
+      </description>
+    </event>
+  </interface>
+</protocol>
diff --git a/river/InputDevice.zig b/river/InputDevice.zig
index 0a5c66f..e92570f 100644
--- a/river/InputDevice.zig
+++ b/river/InputDevice.zig
@@ -27,8 +27,9 @@ const c = @import("c.zig").c;
 const server = &@import("main.zig").server;
 const util = @import("util.zig");
 
-const Seat = @import("Seat.zig");
 const Keyboard = @import("Keyboard.zig");
+const LibinputDevice = @import("LibinputDevice.zig");
+const Seat = @import("Seat.zig");
 const Tablet = @import("Tablet.zig");
 
 const log = std.log.scoped(.input);
@@ -37,6 +38,8 @@ seat: *Seat,
 wlr_device: *wlr.InputDevice,
 objects: wl.list.Head(river.InputDeviceV1, null),
 
+libinput: LibinputDevice,
+
 remove: wl.Listener(*wlr.InputDevice) = .init(handleRemove),
 
 config: struct {
@@ -57,6 +60,7 @@ pub fn init(
     device.* = .{
         .seat = seat,
         .wlr_device = wlr_device,
+        .libinput = undefined,
         .objects = undefined,
         .link = undefined,
     };
@@ -75,6 +79,9 @@ pub fn init(
         var it = server.input_manager.objects.safeIterator(.forward);
         while (it.next()) |im_v1| device.createObject(im_v1);
     }
+    if (wlr_device.getLibinputDevice()) |handle| {
+        device.libinput.init(@ptrCast(handle));
+    }
 }
 
 pub fn createObject(device: *InputDevice, im_v1: *river.InputManagerV1) void {
@@ -101,6 +108,10 @@ pub fn deinit(device: *InputDevice) void {
     assert(device.objects.empty());
     device.remove.link.remove();
 
+    if (device.wlr_device.getLibinputDevice() != null) {
+        device.libinput.deinit();
+    }
+
     device.link.remove();
     device.seat.updateCapabilities();
 
diff --git a/river/LibinputAccelConfig.zig b/river/LibinputAccelConfig.zig
new file mode 100644
index 0000000..ee49319
--- /dev/null
+++ b/river/LibinputAccelConfig.zig
@@ -0,0 +1,124 @@
+// This file is part of river, a dynamic tiling wayland compositor.
+//
+// Copyright 2025 The River Developers
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+const LibinputAccelConfig = @This();
+
+const std = @import("std");
+const assert = std.debug.assert;
+const wayland = @import("wayland");
+const wl = wayland.server.wl;
+const river = wayland.server.river;
+
+const c = @import("c.zig").c;
+const server = &@import("main.zig").server;
+const util = @import("util.zig");
+
+const Keyboard = @import("Keyboard.zig");
+const Seat = @import("Seat.zig");
+
+const log = std.log.scoped(.input);
+
+object: *river.LibinputAccelConfigV1,
+libinput: ?*c.libinput_config_accel,
+
+pub fn create(
+    client: *wl.Client,
+    version: u32,
+    id: u32,
+    profile: c.enum_libinput_config_accel_profile,
+) !void {
+    const accel_config = try util.gpa.create(LibinputAccelConfig);
+    errdefer util.gpa.destroy(accel_config);
+    const object = try river.LibinputAccelConfigV1.create(client, version, id);
+    errdefer comptime unreachable;
+    accel_config.* = .{
+        .object = object,
+        .libinput = c.libinput_config_accel_create(profile),
+    };
+    object.setHandler(*LibinputAccelConfig, handleRequest, handleDestroy, accel_config);
+}
+
+fn handleDestroy(_: *river.LibinputAccelConfigV1, accel_config: *LibinputAccelConfig) void {
+    if (accel_config.libinput) |libinput| {
+        c.libinput_config_accel_destroy(libinput);
+    }
+    util.gpa.destroy(accel_config);
+}
+
+fn handleRequest(
+    object: *river.LibinputAccelConfigV1,
+    request: river.LibinputAccelConfigV1.Request,
+    accel_config: *LibinputAccelConfig,
+) void {
+    assert(accel_config.object == object);
+    switch (request) {
+        .destroy => object.destroy(),
+        .set_points => |args| {
+            const accel_type: c.enum_libinput_config_accel_type = switch (args.type) {
+                .fallback => c.LIBINPUT_ACCEL_TYPE_FALLBACK,
+                .motion => c.LIBINPUT_ACCEL_TYPE_MOTION,
+                .scroll => c.LIBINPUT_ACCEL_TYPE_SCROLL,
+                _ => {
+                    object.postError(.invalid_arg, "invalid accel_type enum value");
+                    return;
+                },
+            };
+            if (args.step.size != @sizeOf(f64)) {
+                object.postError(.invalid_arg, "invalid step argument");
+                return;
+            }
+            const step: f64 = args.step.slice(f64)[0];
+            if (args.points.size % @sizeOf(f64) != 0) {
+                object.postError(.invalid_arg, "invalid points argument");
+                return;
+            }
+            const points = util.gpa.alloc(f64, @divExact(args.points.size, @sizeOf(f64))) catch {
+                log.err("out of memory", .{});
+                object.postNoMemory();
+                return;
+            };
+            defer util.gpa.free(points);
+            @memcpy(
+                @as([]u8, @ptrCast(points)),
+                @as([*]u8, @ptrCast(args.points.data))[0..args.points.size],
+            );
+            const result = river.LibinputResultV1.create(
+                object.getClient(),
+                object.getVersion(),
+                args.result,
+            ) catch {
+                log.err("out of memory", .{});
+                object.postNoMemory();
+                return;
+            };
+            const libinput = accel_config.libinput orelse {
+                result.destroySendInvalid();
+                return;
+            };
+            switch (c.libinput_config_accel_set_points(
+                libinput,
+                accel_type,
+                step,
+                points.len,
+                points.ptr,
+            )) {
+                c.LIBINPUT_CONFIG_STATUS_SUCCESS => result.destroySendSuccess(),
+                c.LIBINPUT_CONFIG_STATUS_UNSUPPORTED => result.destroySendUnsupported(),
+                else => result.destroySendInvalid(),
+            }
+        },
+    }
+}
diff --git a/river/LibinputConfig.zig b/river/LibinputConfig.zig
new file mode 100644
index 0000000..a529db8
--- /dev/null
+++ b/river/LibinputConfig.zig
@@ -0,0 +1,118 @@
+// This file is part of river, a dynamic tiling wayland compositor.
+//
+// Copyright 2025 The River Developers
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+const LibinputConfig = @This();
+
+const std = @import("std");
+const assert = std.debug.assert;
+const wl = @import("wayland").server.wl;
+const river = @import("wayland").server.river;
+
+const c = @import("c.zig").c;
+const server = &@import("main.zig").server;
+
+const LibinputAccelConfig = @import("LibinputAccelConfig.zig");
+const LibinputDevice = @import("LibinputDevice.zig");
+
+const log = std.log.scoped(.input);
+
+global: *wl.Global,
+objects: wl.list.Head(river.LibinputConfigV1, null),
+devices: wl.list.Head(LibinputDevice, .link),
+
+server_destroy: wl.Listener(*wl.Server) = .init(handleServerDestroy),
+
+pub fn init(config: *LibinputConfig) !void {
+    config.* = .{
+        .global = try wl.Global.create(server.wl_server, river.LibinputConfigV1, 1, *LibinputConfig, config, bind),
+        .objects = undefined,
+        .devices = undefined,
+    };
+    config.objects.init();
+    config.devices.init();
+    server.wl_server.addDestroyListener(&config.server_destroy);
+}
+
+fn handleServerDestroy(listener: *wl.Listener(*wl.Server), _: *wl.Server) void {
+    const config: *LibinputConfig = @fieldParentPtr("server_destroy", listener);
+
+    config.global.destroy();
+}
+
+fn bind(client: *wl.Client, config: *LibinputConfig, version: u32, id: u32) void {
+    const object = river.LibinputConfigV1.create(client, version, id) catch {
+        client.postNoMemory();
+        log.err("out of memory", .{});
+        return;
+    };
+    object.setHandler(*LibinputConfig, handleRequest, handleDestroy, config);
+    config.objects.append(object);
+    {
+        var it = config.devices.iterator(.forward);
+        while (it.next()) |device| device.createObject(object);
+    }
+}
+
+fn handleRequestInert(
+    object: *river.LibinputConfigV1,
+    request: river.LibinputConfigV1.Request,
+    _: ?*anyopaque,
+) void {
+    if (request == .destroy) object.destroy();
+}
+
+fn handleDestroy(object: *river.LibinputConfigV1, _: *LibinputConfig) void {
+    object.getLink().remove();
+}
+
+fn handleRequest(
+    object: *river.LibinputConfigV1,
+    request: river.LibinputConfigV1.Request,
+    _: *LibinputConfig,
+) void {
+    switch (request) {
+        .stop => {
+            object.getLink().remove();
+            object.sendFinished();
+            object.setHandler(?*anyopaque, handleRequestInert, null, null);
+        },
+        .destroy => {
+            object.postError(.invalid_destroy, "destroy before finished event sent");
+        },
+        .create_accel_config => |args| {
+            const profile: c.enum_libinput_config_accel_profile = switch (args.profile) {
+                .none => c.LIBINPUT_CONFIG_ACCEL_PROFILE_NONE,
+                .flat => c.LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT,
+                .adaptive => c.LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE,
+                .custom => c.LIBINPUT_CONFIG_ACCEL_PROFILE_CUSTOM,
+                _ => {
+                    object.postError(.invalid_arg, "invalid accel_profile enum value");
+                    return;
+                },
+            };
+            LibinputAccelConfig.create(
+                object.getClient(),
+                object.getVersion(),
+                args.id,
+                profile,
+            ) catch {
+                log.err("out of memory", .{});
+                object.postNoMemory();
+                return;
+            };
+        },
+    }
+}
diff --git a/river/LibinputDevice.zig b/river/LibinputDevice.zig
new file mode 100644
index 0000000..53dd4d7
--- /dev/null
+++ b/river/LibinputDevice.zig
@@ -0,0 +1,561 @@
+// This file is part of river, a dynamic tiling wayland compositor.
+//
+// Copyright 2025 The River Developers
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+const LibinputDevice = @This();
+
+const std = @import("std");
+const assert = std.debug.assert;
+const mem = std.mem;
+const wlr = @import("wlroots");
+const wl = @import("wayland").server.wl;
+const river = @import("wayland").server.river;
+
+const c = @import("c.zig").c;
+const server = &@import("main.zig").server;
+const util = @import("util.zig");
+
+const InputDevice = @import("InputDevice.zig");
+const LibinputAccelConfig = @import("LibinputAccelConfig.zig");
+
+const log = std.log.scoped(.input);
+
+libinput: *c.libinput_device,
+objects: wl.list.Head(river.LibinputDeviceV1, null),
+
+/// LibinputConfig.devices
+link: wl.list.Link,
+
+pub fn init(device: *LibinputDevice, handle: *c.libinput_device) void {
+    device.* = .{
+        .libinput = handle,
+        .objects = undefined,
+        .link = undefined,
+    };
+    device.objects.init();
+    server.libinput_config.devices.append(device);
+    {
+        var it = server.libinput_config.objects.iterator(.forward);
+        while (it.next()) |config_v1| device.createObject(config_v1);
+    }
+}
+
+pub fn createObject(device: *LibinputDevice, config_v1: *river.LibinputConfigV1) void {
+    const object = river.LibinputDeviceV1.create(config_v1.getClient(), config_v1.getVersion(), 0) catch {
+        log.err("out of memory", .{});
+        config_v1.postNoMemory();
+        return;
+    };
+    device.objects.append(object);
+    object.setHandler(*LibinputDevice, handleRequest, handleDestroy, device);
+    config_v1.sendLibinputDevice(object);
+    {
+        const base: *InputDevice = @fieldParentPtr("libinput", device);
+        var it = base.objects.iterator(.forward);
+        while (it.next()) |input_device_v1| {
+            if (object.getClient() == input_device_v1.getClient()) {
+                object.sendInputDevice(input_device_v1);
+            }
+        }
+    }
+    object.sendSendEventsSupport(@bitCast(c.libinput_device_config_send_events_get_modes(device.libinput)));
+    object.sendSendEventsDefault(@bitCast(c.libinput_device_config_send_events_get_default_mode(device.libinput)));
+    object.sendSendEventsCurrent(@bitCast(c.libinput_device_config_send_events_get_mode(device.libinput)));
+    const tap_finger_count = c.libinput_device_config_tap_get_finger_count(device.libinput);
+    object.sendTapSupport(tap_finger_count);
+    if (tap_finger_count > 0) {
+        object.sendTapDefault(@enumFromInt(c.libinput_device_config_tap_get_default_enabled(device.libinput)));
+        object.sendTapCurrent(@enumFromInt(c.libinput_device_config_tap_get_enabled(device.libinput)));
+        object.sendTapButtonMapDefault(@enumFromInt(c.libinput_device_config_tap_get_default_button_map(device.libinput)));
+        object.sendTapButtonMapCurrent(@enumFromInt(c.libinput_device_config_tap_get_button_map(device.libinput)));
+        object.sendDragDefault(@enumFromInt(c.libinput_device_config_tap_get_default_drag_enabled(device.libinput)));
+        object.sendDragCurrent(@enumFromInt(c.libinput_device_config_tap_get_drag_enabled(device.libinput)));
+        object.sendDragLockDefault(@enumFromInt(c.libinput_device_config_tap_get_default_drag_lock_enabled(device.libinput)));
+        object.sendDragLockCurrent(@enumFromInt(c.libinput_device_config_tap_get_drag_lock_enabled(device.libinput)));
+    }
+    const three_finger_drag_finger_count = c.libinput_device_config_3fg_drag_get_finger_count(device.libinput);
+    object.sendThreeFingerDragSupport(three_finger_drag_finger_count);
+    if (three_finger_drag_finger_count >= 3) {
+        object.sendThreeFingerDragDefault(@enumFromInt(c.libinput_device_config_3fg_drag_get_default_enabled(device.libinput)));
+        object.sendThreeFingerDragCurrent(@enumFromInt(c.libinput_device_config_3fg_drag_get_enabled(device.libinput)));
+    }
+    const has_matrix = c.libinput_device_config_calibration_has_matrix(device.libinput);
+    object.sendCalibrationMatrixSupport(has_matrix);
+    if (has_matrix != 0) {
+        var matrix: [6]f32 = undefined;
+        const bytes: []u8 = @ptrCast(&matrix);
+        var array: wl.Array = .{ .size = bytes.len, .alloc = bytes.len, .data = bytes.ptr };
+        _ = c.libinput_device_config_calibration_get_default_matrix(device.libinput, &matrix);
+        object.sendCalibrationMatrixDefault(&array);
+        _ = c.libinput_device_config_calibration_get_matrix(device.libinput, &matrix);
+        object.sendCalibrationMatrixCurrent(&array);
+    }
+    const profiles = c.libinput_device_config_accel_get_profiles(device.libinput);
+    object.sendAccelProfilesSupport(@bitCast(profiles));
+    if (profiles != 0) {
+        object.sendAccelProfileDefault(@enumFromInt(c.libinput_device_config_accel_get_default_profile(device.libinput)));
+        object.sendAccelProfileCurrent(@enumFromInt(c.libinput_device_config_accel_get_profile(device.libinput)));
+        var speed: [1]f64 = .{c.libinput_device_config_accel_get_default_speed(device.libinput)};
+        const bytes: []u8 = @ptrCast(&speed);
+        var array: wl.Array = .{ .size = bytes.len, .alloc = bytes.len, .data = bytes.ptr };
+        object.sendAccelSpeedDefault(&array);
+        speed = .{c.libinput_device_config_accel_get_speed(device.libinput)};
+        object.sendAccelSpeedCurrent(&array);
+    }
+    const natural_scroll = c.libinput_device_config_scroll_has_natural_scroll(device.libinput);
+    object.sendNaturalScrollSupport(natural_scroll);
+    if (natural_scroll != 0) {
+        const default = c.libinput_device_config_scroll_get_default_natural_scroll_enabled(device.libinput);
+        object.sendNaturalScrollDefault(if (default != 0) .enabled else .disabled);
+        const current = c.libinput_device_config_scroll_get_natural_scroll_enabled(device.libinput);
+        object.sendNaturalScrollCurrent(if (current != 0) .enabled else .disabled);
+    }
+    const left_handed = c.libinput_device_config_left_handed_is_available(device.libinput);
+    object.sendLeftHandedSupport(left_handed);
+    if (left_handed != 0) {
+        const default = c.libinput_device_config_left_handed_get_default(device.libinput);
+        object.sendLeftHandedDefault(if (default != 0) .enabled else .disabled);
+        const current = c.libinput_device_config_left_handed_get(device.libinput);
+        object.sendLeftHandedCurrent(if (current != 0) .enabled else .disabled);
+    }
+    const click_methods = c.libinput_device_config_click_get_methods(device.libinput);
+    object.sendClickMethodSupport(@bitCast(click_methods));
+    if (click_methods != 0) {
+        object.sendClickMethodDefault(@enumFromInt(c.libinput_device_config_click_get_default_method(device.libinput)));
+        object.sendClickMethodCurrent(@enumFromInt(c.libinput_device_config_click_get_method(device.libinput)));
+        if (click_methods & c.LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER != 0) {
+            object.sendClickfingerButtonMapDefault(@enumFromInt(c.libinput_device_config_click_get_default_clickfinger_button_map(device.libinput)));
+            object.sendClickfingerButtonMapCurrent(@enumFromInt(c.libinput_device_config_click_get_clickfinger_button_map(device.libinput)));
+        }
+    }
+    const middle_emulation = c.libinput_device_config_middle_emulation_is_available(device.libinput);
+    object.sendMiddleEmulationSupport(middle_emulation);
+    if (middle_emulation != 0) {
+        object.sendMiddleEmulationDefault(@enumFromInt(c.libinput_device_config_middle_emulation_get_default_enabled(device.libinput)));
+        object.sendMiddleEmulationCurrent(@enumFromInt(c.libinput_device_config_middle_emulation_get_enabled(device.libinput)));
+    }
+    const scroll_methods = c.libinput_device_config_scroll_get_methods(device.libinput);
+    object.sendScrollMethodSupport(@bitCast(scroll_methods));
+    if (scroll_methods != 0) {
+        object.sendScrollMethodDefault(@enumFromInt(c.libinput_device_config_scroll_get_default_method(device.libinput)));
+        object.sendScrollMethodCurrent(@enumFromInt(c.libinput_device_config_scroll_get_method(device.libinput)));
+        if (scroll_methods & c.LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN != 0) {
+            object.sendScrollButtonDefault(c.libinput_device_config_scroll_get_default_button(device.libinput));
+            object.sendScrollButtonCurrent(c.libinput_device_config_scroll_get_button(device.libinput));
+            object.sendScrollButtonLockDefault(@enumFromInt(c.libinput_device_config_scroll_get_default_button_lock(device.libinput)));
+            object.sendScrollButtonLockCurrent(@enumFromInt(c.libinput_device_config_scroll_get_button_lock(device.libinput)));
+        }
+    }
+    const dwt = c.libinput_device_config_dwt_is_available(device.libinput);
+    object.sendDwtSupport(dwt);
+    if (dwt != 0) {
+        const default = c.libinput_device_config_dwt_get_default_enabled(device.libinput);
+        object.sendDwtDefault(if (default != 0) .enabled else .disabled);
+        const current = c.libinput_device_config_dwt_get_enabled(device.libinput);
+        object.sendDwtCurrent(if (current != 0) .enabled else .disabled);
+    }
+    const dwtp = c.libinput_device_config_dwtp_is_available(device.libinput);
+    object.sendDwtpSupport(dwtp);
+    if (dwtp != 0) {
+        const default = c.libinput_device_config_dwtp_get_default_enabled(device.libinput);
+        object.sendDwtpDefault(if (default != 0) .enabled else .disabled);
+        const current = c.libinput_device_config_dwtp_get_enabled(device.libinput);
+        object.sendDwtpCurrent(if (current != 0) .enabled else .disabled);
+    }
+    const rotation = c.libinput_device_config_rotation_is_available(device.libinput);
+    object.sendRotationSupport(rotation);
+    if (rotation != 0) {
+        object.sendRotationDefault(c.libinput_device_config_rotation_get_default_angle(device.libinput));
+        object.sendRotationCurrent(c.libinput_device_config_rotation_get_angle(device.libinput));
+    }
+}
+
+pub fn deinit(device: *LibinputDevice) void {
+    {
+        var it = device.objects.iterator(.forward);
+        while (it.next()) |object| {
+            object.getLink().remove();
+            object.sendRemoved();
+            object.setHandler(?*anyopaque, handleRequestInert, null, null);
+        }
+    }
+    assert(device.objects.empty());
+    device.link.remove();
+}
+
+fn handleRequestInert(
+    object: *river.LibinputDeviceV1,
+    request: river.LibinputDeviceV1.Request,
+    _: ?*anyopaque,
+) void {
+    if (request == .destroy) object.destroy();
+}
+
+fn handleDestroy(object: *river.LibinputDeviceV1, _: *LibinputDevice) void {
+    object.getLink().remove();
+}
+
+fn handleRequest(
+    object: *river.LibinputDeviceV1,
+    request: river.LibinputDeviceV1.Request,
+    device: *LibinputDevice,
+) void {
+    switch (request) {
+        .destroy => object.destroy(),
+        .set_send_events => |args| {
+            const result = Result.create(object, args.result) orelse return;
+            const status = c.libinput_device_config_send_events_set_mode(device.libinput, @bitCast(args.mode));
+            if (result.send(status)) {
+                const current = c.libinput_device_config_send_events_get_mode(device.libinput);
+                var it = device.objects.iterator(.forward);
+                while (it.next()) |o| o.sendSendEventsCurrent(@bitCast(current));
+            }
+        },
+        .set_tap => |args| {
+            const result = Result.create(object, args.result) orelse return;
+            const status = c.libinput_device_config_tap_set_enabled(device.libinput, switch (args.state) {
+                .disabled => c.LIBINPUT_CONFIG_TAP_DISABLED,
+                .enabled => c.LIBINPUT_CONFIG_TAP_ENABLED,
+                _ => {
+                    object.postError(.invalid_arg, "invalid tap_state enum value");
+                    return;
+                },
+            });
+            if (result.send(status)) {
+                const current = c.libinput_device_config_tap_get_enabled(device.libinput);
+                var it = device.objects.iterator(.forward);
+                while (it.next()) |o| o.sendTapCurrent(@enumFromInt(current));
+            }
+        },
+        .set_tap_button_map => |args| {
+            const result = Result.create(object, args.result) orelse return;
+            const status = c.libinput_device_config_tap_set_button_map(device.libinput, switch (args.button_map) {
+                .lrm => c.LIBINPUT_CONFIG_TAP_MAP_LRM,
+                .lmr => c.LIBINPUT_CONFIG_TAP_MAP_LMR,
+                _ => {
+                    object.postError(.invalid_arg, "invalid tap_button_map enum value");
+                    return;
+                },
+            });
+            if (result.send(status)) {
+                const current = c.libinput_device_config_tap_get_button_map(device.libinput);
+                var it = device.objects.iterator(.forward);
+                while (it.next()) |o| o.sendTapButtonMapCurrent(@enumFromInt(current));
+            }
+        },
+        .set_drag => |args| {
+            const result = Result.create(object, args.result) orelse return;
+            const status = c.libinput_device_config_tap_set_drag_enabled(device.libinput, switch (args.state) {
+                .disabled => c.LIBINPUT_CONFIG_DRAG_DISABLED,
+                .enabled => c.LIBINPUT_CONFIG_DRAG_ENABLED,
+                _ => {
+                    object.postError(.invalid_arg, "invalid drag_state enum value");
+                    return;
+                },
+            });
+            if (result.send(status)) {
+                const current = c.libinput_device_config_tap_get_drag_enabled(device.libinput);
+                var it = device.objects.iterator(.forward);
+                while (it.next()) |o| o.sendDragCurrent(@enumFromInt(current));
+            }
+        },
+        .set_drag_lock => |args| {
+            const result = Result.create(object, args.result) orelse return;
+            const status = c.libinput_device_config_tap_set_drag_lock_enabled(device.libinput, switch (args.state) {
+                .disabled => c.LIBINPUT_CONFIG_DRAG_LOCK_DISABLED,
+                .enabled_timeout => c.LIBINPUT_CONFIG_DRAG_LOCK_ENABLED_TIMEOUT,
+                .enabled_sticky => c.LIBINPUT_CONFIG_DRAG_LOCK_ENABLED_STICKY,
+                _ => {
+                    object.postError(.invalid_arg, "invalid drag_lock_state enum value");
+                    return;
+                },
+            });
+            if (result.send(status)) {
+                const current = c.libinput_device_config_tap_get_drag_lock_enabled(device.libinput);
+                var it = device.objects.iterator(.forward);
+                while (it.next()) |o| o.sendDragLockCurrent(@enumFromInt(current));
+            }
+        },
+        .set_three_finger_drag => |args| {
+            const result = Result.create(object, args.result) orelse return;
+            const status = c.libinput_device_config_3fg_drag_set_enabled(device.libinput, switch (args.state) {
+                .disabled => c.LIBINPUT_CONFIG_3FG_DRAG_DISABLED,
+                .enabled_3fg => c.LIBINPUT_CONFIG_3FG_DRAG_ENABLED_3FG,
+                .enabled_4fg => c.LIBINPUT_CONFIG_3FG_DRAG_ENABLED_4FG,
+                _ => {
+                    object.postError(.invalid_arg, "invalid three_finger_drag_state enum value");
+                    return;
+                },
+            });
+            if (result.send(status)) {
+                const current = c.libinput_device_config_3fg_drag_get_enabled(device.libinput);
+                var it = device.objects.iterator(.forward);
+                while (it.next()) |o| o.sendThreeFingerDragCurrent(@enumFromInt(current));
+            }
+        },
+        .set_calibration_matrix => |args| {
+            if (args.matrix.size != @sizeOf([6]f32)) {
+                object.postError(.invalid_arg, "invalid calibration matrix");
+                return;
+            }
+            const matrix = args.matrix.slice(f32)[0..6];
+            const result = Result.create(object, args.result) orelse return;
+            const status = c.libinput_device_config_calibration_set_matrix(device.libinput, matrix);
+            if (result.send(status)) {
+                var current: [6]f32 = undefined;
+                const bytes: []u8 = @ptrCast(&current);
+                var array: wl.Array = .{ .size = bytes.len, .alloc = bytes.len, .data = bytes.ptr };
+                _ = c.libinput_device_config_calibration_get_matrix(device.libinput, &current);
+                var it = device.objects.iterator(.forward);
+                while (it.next()) |o| o.sendCalibrationMatrixCurrent(&array);
+            }
+        },
+        .set_accel_profile => |args| {
+            const result = Result.create(object, args.result) orelse return;
+            const status = c.libinput_device_config_accel_set_profile(device.libinput, switch (args.profile) {
+                .none => c.LIBINPUT_CONFIG_ACCEL_PROFILE_NONE,
+                .flat => c.LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT,
+                .adaptive => c.LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE,
+                .custom => c.LIBINPUT_CONFIG_ACCEL_PROFILE_CUSTOM,
+                _ => {
+                    object.postError(.invalid_arg, "invalid accel_profile enum value");
+                    return;
+                },
+            });
+            if (result.send(status)) {
+                const current = c.libinput_device_config_accel_get_profile(device.libinput);
+                var it = device.objects.iterator(.forward);
+                while (it.next()) |o| o.sendAccelProfileCurrent(@enumFromInt(current));
+            }
+        },
+        .set_accel_speed => |args| {
+            if (args.speed.size != @sizeOf(f32)) {
+                object.postError(.invalid_arg, "invalid accel speed");
+                return;
+            }
+            const speed = args.speed.slice(f32)[0];
+            const result = Result.create(object, args.result) orelse return;
+            const status = c.libinput_device_config_accel_set_speed(device.libinput, speed);
+            if (result.send(status)) {
+                var current: [1]f64 = .{c.libinput_device_config_accel_get_speed(device.libinput)};
+                const bytes: []u8 = @ptrCast(&current);
+                var array: wl.Array = .{ .size = bytes.len, .alloc = bytes.len, .data = bytes.ptr };
+                var it = device.objects.iterator(.forward);
+                while (it.next()) |o| o.sendAccelSpeedCurrent(&array);
+            }
+        },
+        .apply_accel_config => |args| {
+            const result = Result.create(object, args.result) orelse return;
+            const accel_config: *LibinputAccelConfig = @ptrCast(@alignCast(args.config.getUserData()));
+            const config = accel_config.libinput orelse {
+                _ = result.send(c.LIBINPUT_CONFIG_STATUS_INVALID);
+                return;
+            };
+            const status = c.libinput_device_config_accel_apply(device.libinput, config);
+            if (result.send(status)) {
+                const current = c.libinput_device_config_accel_get_profile(device.libinput);
+                var it = device.objects.iterator(.forward);
+                while (it.next()) |o| o.sendAccelProfileCurrent(@enumFromInt(current));
+            }
+        },
+        .set_natural_scroll => |args| {
+            const result = Result.create(object, args.result) orelse return;
+            const status = c.libinput_device_config_scroll_set_natural_scroll_enabled(device.libinput, switch (args.state) {
+                .disabled => 0,
+                .enabled => 1,
+                _ => {
+                    object.postError(.invalid_arg, "invalid natural_scroll_state enum value");
+                    return;
+                },
+            });
+            if (result.send(status)) {
+                const current = c.libinput_device_config_scroll_get_natural_scroll_enabled(device.libinput);
+                var it = device.objects.iterator(.forward);
+                while (it.next()) |o| o.sendNaturalScrollCurrent(if (current != 0) .enabled else .disabled);
+            }
+        },
+        .set_left_handed => |args| {
+            const result = Result.create(object, args.result) orelse return;
+            const status = c.libinput_device_config_left_handed_set(device.libinput, switch (args.state) {
+                .disabled => 0,
+                .enabled => 1,
+                _ => {
+                    object.postError(.invalid_arg, "invalid left_handed_state enum value");
+                    return;
+                },
+            });
+            if (result.send(status)) {
+                const current = c.libinput_device_config_left_handed_get(device.libinput);
+                var it = device.objects.iterator(.forward);
+                while (it.next()) |o| o.sendLeftHandedCurrent(if (current != 0) .enabled else .disabled);
+            }
+        },
+        .set_click_method => |args| {
+            const result = Result.create(object, args.result) orelse return;
+            const status = c.libinput_device_config_click_set_method(device.libinput, switch (args.method) {
+                .none => c.LIBINPUT_CONFIG_CLICK_METHOD_NONE,
+                .button_areas => c.LIBINPUT_CONFIG_CLICK_METHOD_BUTTON_AREAS,
+                .clickfinger => c.LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER,
+                _ => {
+                    object.postError(.invalid_arg, "invalid click_method enum value");
+                    return;
+                },
+            });
+            if (result.send(status)) {
+                const current = c.libinput_device_config_click_get_method(device.libinput);
+                var it = device.objects.iterator(.forward);
+                while (it.next()) |o| o.sendClickMethodCurrent(@enumFromInt(current));
+            }
+        },
+        .set_clickfinger_button_map => |args| {
+            const result = Result.create(object, args.result) orelse return;
+            const status = c.libinput_device_config_click_set_clickfinger_button_map(device.libinput, switch (args.button_map) {
+                .lrm => c.LIBINPUT_CONFIG_CLICKFINGER_MAP_LRM,
+                .lmr => c.LIBINPUT_CONFIG_CLICKFINGER_MAP_LMR,
+                _ => {
+                    object.postError(.invalid_arg, "invalid clickfinger_button_map enum value");
+                    return;
+                },
+            });
+            if (result.send(status)) {
+                const current = c.libinput_device_config_click_get_clickfinger_button_map(device.libinput);
+                var it = device.objects.iterator(.forward);
+                while (it.next()) |o| o.sendClickfingerButtonMapCurrent(@enumFromInt(current));
+            }
+        },
+        .set_middle_emulation => |args| {
+            const result = Result.create(object, args.result) orelse return;
+            const status = c.libinput_device_config_middle_emulation_set_enabled(device.libinput, switch (args.state) {
+                .disabled => c.LIBINPUT_CONFIG_MIDDLE_EMULATION_DISABLED,
+                .enabled => c.LIBINPUT_CONFIG_MIDDLE_EMULATION_ENABLED,
+                _ => {
+                    object.postError(.invalid_arg, "invalid middle_emulation_state enum value");
+                    return;
+                },
+            });
+            if (result.send(status)) {
+                const current = c.libinput_device_config_middle_emulation_get_enabled(device.libinput);
+                var it = device.objects.iterator(.forward);
+                while (it.next()) |o| o.sendMiddleEmulationCurrent(@enumFromInt(current));
+            }
+        },
+        .set_scroll_method => |args| {
+            const result = Result.create(object, args.result) orelse return;
+            const status = c.libinput_device_config_scroll_set_method(device.libinput, switch (args.method) {
+                .no_scroll => c.LIBINPUT_CONFIG_SCROLL_NO_SCROLL,
+                .two_finger => c.LIBINPUT_CONFIG_SCROLL_2FG,
+                .edge => c.LIBINPUT_CONFIG_SCROLL_EDGE,
+                .on_button_down => c.LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN,
+                _ => {
+                    object.postError(.invalid_arg, "invalid scroll_method enum value");
+                    return;
+                },
+            });
+            if (result.send(status)) {
+                const current = c.libinput_device_config_scroll_get_method(device.libinput);
+                var it = device.objects.iterator(.forward);
+                while (it.next()) |o| o.sendScrollMethodCurrent(@enumFromInt(current));
+            }
+        },
+        .set_scroll_button => |args| {
+            const result = Result.create(object, args.result) orelse return;
+            const status = c.libinput_device_config_scroll_set_button(device.libinput, args.button);
+            if (result.send(status)) {
+                const current = c.libinput_device_config_scroll_get_button(device.libinput);
+                var it = device.objects.iterator(.forward);
+                while (it.next()) |o| o.sendScrollButtonCurrent(current);
+            }
+        },
+        .set_scroll_button_lock => |args| {
+            const result = Result.create(object, args.result) orelse return;
+            const status = c.libinput_device_config_scroll_set_button_lock(device.libinput, switch (args.state) {
+                .disabled => c.LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_DISABLED,
+                .enabled => c.LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_ENABLED,
+                _ => {
+                    object.postError(.invalid_arg, "invalid scroll_button_lock_state enum value");
+                    return;
+                },
+            });
+            if (result.send(status)) {
+                const current = c.libinput_device_config_scroll_get_button_lock(device.libinput);
+                var it = device.objects.iterator(.forward);
+                while (it.next()) |o| o.sendScrollButtonLockCurrent(@enumFromInt(current));
+            }
+        },
+        .set_dwt => |args| {
+            const result = Result.create(object, args.result) orelse return;
+            const status = c.libinput_device_config_dwt_set_enabled(device.libinput, switch (args.state) {
+                .disabled => c.LIBINPUT_CONFIG_DWT_DISABLED,
+                .enabled => c.LIBINPUT_CONFIG_DWT_ENABLED,
+                _ => {
+                    object.postError(.invalid_arg, "invalid dwt_state enum value");
+                    return;
+                },
+            });
+            if (result.send(status)) {
+                const current = c.libinput_device_config_dwt_get_enabled(device.libinput);
+                var it = device.objects.iterator(.forward);
+                while (it.next()) |o| o.sendDwtCurrent(if (current != 0) .enabled else .disabled);
+            }
+        },
+        .set_dwtp => |args| {
+            const result = Result.create(object, args.result) orelse return;
+            const status = c.libinput_device_config_dwtp_set_enabled(device.libinput, switch (args.state) {
+                .disabled => c.LIBINPUT_CONFIG_DWTP_DISABLED,
+                .enabled => c.LIBINPUT_CONFIG_DWTP_ENABLED,
+                _ => {
+                    object.postError(.invalid_arg, "invalid dwtp_state enum value");
+                    return;
+                },
+            });
+            if (result.send(status)) {
+                const current = c.libinput_device_config_dwtp_get_enabled(device.libinput);
+                var it = device.objects.iterator(.forward);
+                while (it.next()) |o| o.sendDwtpCurrent(if (current != 0) .enabled else .disabled);
+            }
+        },
+        .set_rotation => |args| {
+            const result = Result.create(object, args.result) orelse return;
+            const status = c.libinput_device_config_rotation_set_angle(device.libinput, args.angle);
+            if (result.send(status)) {
+                const current = c.libinput_device_config_rotation_get_angle(device.libinput);
+                var it = device.objects.iterator(.forward);
+                while (it.next()) |o| o.sendRotationCurrent(current);
+            }
+        },
+    }
+}
+
+const Result = struct {
+    object: *river.LibinputResultV1,
+
+    pub fn create(object: *river.LibinputDeviceV1, id: u32) ?Result {
+        const result = river.LibinputResultV1.create(object.getClient(), object.getVersion(), id) catch {
+            log.err("out of memory", .{});
+            object.postNoMemory();
+            return null;
+        };
+        return .{ .object = result };
+    }
+
+    pub fn send(result: *const Result, status: u32) bool {
+        switch (status) {
+            c.LIBINPUT_CONFIG_STATUS_SUCCESS => result.object.destroySendSuccess(),
+            c.LIBINPUT_CONFIG_STATUS_UNSUPPORTED => result.object.destroySendUnsupported(),
+            else => result.object.destroySendInvalid(),
+        }
+        return status == c.LIBINPUT_CONFIG_STATUS_SUCCESS;
+    }
+};
diff --git a/river/Server.zig b/river/Server.zig
index 8b65066..d9396da 100644
--- a/river/Server.zig
+++ b/river/Server.zig
@@ -40,6 +40,7 @@ const TabletTool = @import("TabletTool.zig");
 const WindowManager = @import("WindowManager.zig");
 const XkbBindings = @import("XkbBindings.zig");
 const LayerShell = @import("LayerShell.zig");
+const LibinputConfig = @import("LibinputConfig.zig");
 const XdgDecoration = @import("XdgDecoration.zig");
 const XdgToplevel = @import("XdgToplevel.zig");
 const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig");
@@ -86,6 +87,7 @@ foreign_toplevel_list: *wlr.ExtForeignToplevelListV1,
 
 scene: Scene,
 input_manager: InputManager,
+libinput_config: LibinputConfig,
 om: OutputManager,
 config: Config,
 idle_inhibit_manager: IdleInhibitManager,
@@ -156,6 +158,7 @@ pub fn init(server: *Server, runtime_xwayland: bool) !void {
         .scene = undefined,
         .om = undefined,
         .input_manager = undefined,
+        .libinput_config = undefined,
         .idle_inhibit_manager = undefined,
         .lock_manager = undefined,
         .wm = undefined,
@@ -184,6 +187,7 @@ pub fn init(server: *Server, runtime_xwayland: bool) !void {
     try server.scene.init();
     try server.om.init();
     try server.input_manager.init();
+    try server.libinput_config.init();
     try server.idle_inhibit_manager.init();
     try server.lock_manager.init();
 
@@ -335,6 +339,7 @@ fn blocklist(server: *Server, global: *const wl.Global) bool {
         global == server.om.wlr_output_manager.global or
         global == server.om.power_manager.global or
         global == server.om.gamma_control_manager.global or
+        global == server.libinput_config.global or
         global == server.input_manager.global or
         global == server.input_manager.idle_notifier.global or
         global == server.input_manager.virtual_pointer_manager.global or