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

commitaa60a537dd97eec8fa5edc9ed9ad10a36d63b518
parentd72c3b9117
authorLucas Galante <[email protected]>
date2026-05-27 23:25
Implement libinput physical device configuration and river_input_management_v1 integration

 src/input_device.rs          | 418 +++++++++++++++++++++-
 src/input_manager.rs         | 216 +++++++++++-
 src/libinput_accel_config.rs | 157 +++++++++
 src/libinput_config.rs       |  64 +---
 src/libinput_device.rs       | 813 +++++++++++++++++++++++++++++++++++++++++++
 src/main.rs                  |   2 +
 src/seat.rs                  |   2 +-
 wrapper.h                    |   1 +
 8 files changed, 1619 insertions(+), 54 deletions(-)

diff --git a/src/input_device.rs b/src/input_device.rs
index 40d455e..1ca38f8 100644
--- a/src/input_device.rs
+++ b/src/input_device.rs
@@ -1,6 +1,9 @@
+// SPDX-FileCopyrightText: © 2026 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+
 use crate::ffi;
 use crate::seat::Seat;
-use crate::server::{WlListener, wl_listener_remove};
+use crate::server::{WlListener, wl_listener_remove, WlList, wl_list_insert, wl_list_remove};
 
 pub struct InputDeviceConfig {
     pub scroll_factor: f64,
@@ -16,6 +19,16 @@ pub struct InputDevice {
     pub config: InputDeviceConfig,
     pub destroy_fn: Option<unsafe extern "C" fn(*mut std::ffi::c_void)>,
     pub destroy_data: *mut std::ffi::c_void,
+
+    pub objects: ffi::wl_list, // list of InputDeviceObject
+    pub link: ffi::wl_list,    // link inside InputManager::devices
+    pub libinput: Option<Box<crate::libinput_device::LibinputDevice>>,
+}
+
+pub struct InputDeviceObject {
+    pub device: *mut InputDevice,
+    pub resource: *mut ffi::wl_resource,
+    pub link: ffi::wl_list,
 }
 
 impl InputDevice {
@@ -24,6 +37,8 @@ impl InputDevice {
         wlr_device: *mut ffi::wlr_input_device,
         virtual_device: bool,
     ) -> *mut Self {
+        let server = (*seat).server;
+
         let device = Box::into_raw(Box::new(Self {
             seat,
             wlr_device,
@@ -36,8 +51,18 @@ impl InputDevice {
             },
             destroy_fn: None,
             destroy_data: std::ptr::null_mut(),
+            objects: std::mem::zeroed(),
+            link: std::mem::zeroed(),
+            libinput: None,
         }));
 
+        ffi::wl_list_init(&mut (*device).objects);
+        ffi::wl_list_init(&mut (*device).link);
+
+        // Insert into InputManager::devices
+        let manager_devices_head = &mut (*server).input_manager.devices as *mut ffi::wl_list as *mut WlList;
+        wl_list_insert(manager_devices_head, &mut (*device).link as *mut ffi::wl_list as *mut WlList);
+
         ffi::river_wlr_input_device_set_data(wlr_device, device as *mut _);
 
         let destroy_listener_ptr = &mut (*device).destroy_listener as *mut ffi::wl_listener as *mut WlListener;
@@ -46,19 +71,81 @@ impl InputDevice {
         let destroy_signal = ffi::river_wlr_input_device_get_destroy_signal(wlr_device);
         crate::server::wl_signal_add(destroy_signal, &mut (*device).destroy_listener);
 
+        if !virtual_device {
+            let config_objects = &mut (*server).input_manager.objects as *mut ffi::wl_list as *mut WlList;
+            let mut curr = (*config_objects).next;
+            while curr != config_objects {
+                let next = (*curr).next;
+                let im_obj = crate::container_of!(curr, crate::input_manager::InputManagerObject, link);
+                (*device).create_object((*im_obj).resource);
+                curr = next;
+            }
+
+            let handle = ffi::wlr_libinput_get_device_handle(wlr_device);
+            if !handle.is_null() {
+                (*device).libinput = Some(crate::libinput_device::LibinputDevice::init(device, handle));
+            }
+        }
+
+        // Output mapping logic for pointers and touch screens (as done in Zig version)
+        let output_name = match ffi::river_wlr_input_device_get_type(wlr_device) {
+            ffi::wlr_input_device_type_WLR_INPUT_DEVICE_POINTER => {
+                let ptr = ffi::wlr_pointer_from_input_device(wlr_device);
+                if !ptr.is_null() && !(*ptr).output_name.is_null() {
+                    Some(std::ffi::CStr::from_ptr((*ptr).output_name))
+                } else {
+                    None
+                }
+            }
+            ffi::wlr_input_device_type_WLR_INPUT_DEVICE_TOUCH => {
+                let touch = ffi::wlr_touch_from_input_device(wlr_device);
+                if !touch.is_null() && !(*touch).output_name.is_null() {
+                    Some(std::ffi::CStr::from_ptr((*touch).output_name))
+                } else {
+                    None
+                }
+            }
+            _ => None,
+        };
+
+        if let Some(name) = output_name {
+            let outputs_head = &mut (*server).om.outputs as *mut ffi::wl_list as *mut WlList;
+            let mut curr = (*outputs_head).next;
+            while curr != outputs_head {
+                let next = (*curr).next;
+                let output = crate::container_of!(curr, crate::output::Output, link);
+                let wlr_output = (*output).wlr_output;
+                if !wlr_output.is_null() {
+                    let wlr_name = std::ffi::CStr::from_ptr(ffi::river_wlr_output_get_name(wlr_output));
+                    if wlr_name == name {
+                        (*device).config.map_to_output = wlr_output;
+                        break;
+                    }
+                }
+                curr = next;
+            }
+        }
+
         device
     }
 
     pub unsafe fn assign_to_seat(&mut self, new_seat: *mut Seat) {
-        if self.seat == new_seat {
+        let old_seat = self.seat;
+        if old_seat == new_seat {
             return;
         }
-        if !self.seat.is_null() {
-            (*self.seat).detach_device(self);
+        if !old_seat.is_null() {
+            (*old_seat).detach_device(self);
         }
         self.seat = new_seat;
-        if !self.seat.is_null() {
-            (*self.seat).attach_device(self);
+        if !new_seat.is_null() {
+            (*new_seat).attach_device(self);
+        }
+        if !old_seat.is_null() {
+            (*old_seat).update_capabilities();
+        }
+        if !new_seat.is_null() {
+            (*new_seat).update_capabilities();
         }
     }
 
@@ -73,6 +160,81 @@ impl InputDevice {
         }
         mapping
     }
+
+    pub unsafe fn create_object(&mut self, im_v1_resource: *mut ffi::wl_resource) {
+        if self.virtual_device {
+            return;
+        }
+
+        let dev_type = ffi::river_wlr_input_device_get_type(self.wlr_device);
+        let proto_type = match dev_type {
+            ffi::wlr_input_device_type_WLR_INPUT_DEVICE_KEYBOARD => 0, // keyboard
+            ffi::wlr_input_device_type_WLR_INPUT_DEVICE_POINTER => 1,  // pointer
+            ffi::wlr_input_device_type_WLR_INPUT_DEVICE_TOUCH => 2,    // touch
+            ffi::wlr_input_device_type_WLR_INPUT_DEVICE_TABLET => 3,   // tablet
+            _ => return,
+        };
+
+        let client = ffi::wl_resource_get_client(im_v1_resource);
+        let version = ffi::wl_resource_get_version(im_v1_resource);
+
+        let resource = ffi::wl_resource_create(
+            client,
+            &ffi::river_input_device_v1_interface,
+            version,
+            0,
+        );
+        if resource.is_null() {
+            log::error!("out of memory creating river_input_device_v1");
+            ffi::wl_client_post_no_memory(client);
+            return;
+        }
+
+        let obj = Box::into_raw(Box::new(InputDeviceObject {
+            device: self,
+            resource,
+            link: std::mem::zeroed(),
+        }));
+
+        ffi::wl_resource_set_implementation(
+            resource,
+            &INPUT_DEVICE_INTERFACE as *const _ as *const _,
+            obj as *mut _,
+            Some(handle_device_object_destroy),
+        );
+
+        // Insert into self.objects
+        let list_head = &mut self.objects as *mut ffi::wl_list as *mut WlList;
+        wl_list_insert(list_head, &mut (*obj).link as *mut ffi::wl_list as *mut WlList);
+
+        // Send input_device event to the manager resource
+        ffi::wl_resource_post_event(im_v1_resource, 0, resource); // opcode 0 is input_device in river_input_manager_v1
+
+        // Send type and name to client
+        ffi::wl_resource_post_event(resource, 1, proto_type); // type event
+        
+        let name = ffi::river_wlr_input_device_get_name(self.wlr_device);
+        let name_ptr = if !name.is_null() { name } else { b"\0".as_ptr() as *const _ };
+        ffi::wl_resource_post_event(resource, 2, name_ptr); // name event
+
+        if version >= 2 {
+            ffi::wl_resource_post_event(resource, 3); // done event
+        }
+
+        // Connect client-side libinput device if matching exists
+        if let Some(ref mut libinput) = self.libinput {
+            let libinput_config_objects = &mut (*(*self.seat).server).libinput_config.objects as *mut ffi::wl_list as *mut WlList;
+            let mut curr = (*libinput_config_objects).next;
+            while curr != libinput_config_objects {
+                let next = (*curr).next;
+                let config_obj = crate::container_of!(curr, crate::libinput_config::LibinputConfigObject, link);
+                if ffi::wl_resource_get_client((*config_obj).resource) == client {
+                    libinput.create_object((*config_obj).resource);
+                }
+                curr = next;
+            }
+        }
+    }
 }
 
 unsafe extern "C" fn handle_device_destroy(listener: *mut ffi::wl_listener, _data: *mut std::ffi::c_void) {
@@ -84,6 +246,32 @@ unsafe extern "C" fn handle_device_destroy(listener: *mut ffi::wl_listener, _dat
         ffi::river_wlr_input_device_get_type(device.wlr_device)
     );
 
+    // Free objects and set inert
+    let objects_head = &mut device.objects as *mut ffi::wl_list as *mut WlList;
+    let mut curr = (*objects_head).next;
+    while curr != objects_head {
+        let next = (*curr).next;
+        let obj = crate::container_of!(curr, InputDeviceObject, link);
+
+        wl_list_remove(curr);
+        ffi::wl_list_init(curr as *mut ffi::wl_list);
+
+        ffi::wl_resource_post_event((*obj).resource, 0); // removed event
+
+        ffi::wl_resource_set_implementation(
+            (*obj).resource,
+            &INPUT_DEVICE_INERT_INTERFACE as *const _ as *const _,
+            obj as *mut _,
+            Some(handle_device_object_destroy),
+        );
+
+        curr = next;
+    }
+
+    if let Some(mut libinput) = device.libinput.take() {
+        libinput.deinit();
+    }
+
     // Call custom destroy callback if set (e.g. to clean up Tablet/Keyboard wrappers)
     if let Some(destroy_fn) = device.destroy_fn {
         destroy_fn(device.destroy_data);
@@ -92,11 +280,229 @@ unsafe extern "C" fn handle_device_destroy(listener: *mut ffi::wl_listener, _dat
     // Remove destroy listener
     wl_listener_remove(&mut device.destroy_listener);
 
+    // Remove from InputManager::devices
+    wl_list_remove(&mut device.link as *mut ffi::wl_list as *mut WlList);
+
     // Detach from seat if attached
     if !device.seat.is_null() {
         (*device.seat).detach_device(device);
+        (*device.seat).update_capabilities();
     }
 
     // Free wrapper memory
     let _boxed = Box::from_raw(device_ptr);
 }
+
+unsafe extern "C" fn handle_device_object_destroy(resource: *mut ffi::wl_resource) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut InputDeviceObject;
+    if !obj.is_null() {
+        wl_list_remove(&mut (*obj).link as *mut ffi::wl_list as *mut WlList);
+        let _ = Box::from_raw(obj);
+    }
+}
+
+static INPUT_DEVICE_INTERFACE: ffi::river_input_device_v1_interface = ffi::river_input_device_v1_interface {
+    destroy: Some(input_device_destroy),
+    assign_to_seat: Some(input_device_assign_to_seat),
+    set_repeat_info: Some(input_device_set_repeat_info),
+    set_scroll_factor: Some(input_device_set_scroll_factor),
+    map_to_output: Some(input_device_map_to_output),
+    map_to_rectangle: Some(input_device_map_to_rectangle),
+};
+
+static INPUT_DEVICE_INERT_INTERFACE: ffi::river_input_device_v1_interface = ffi::river_input_device_v1_interface {
+    destroy: Some(input_device_destroy),
+    assign_to_seat: None,
+    set_repeat_info: None,
+    set_scroll_factor: None,
+    map_to_output: None,
+    map_to_rectangle: None,
+};
+
+unsafe extern "C" fn input_device_destroy(
+    _client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+) {
+    ffi::wl_resource_destroy(resource);
+}
+
+unsafe extern "C" fn input_device_assign_to_seat(
+    _client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    name: *const std::os::raw::c_char,
+) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut InputDeviceObject;
+    if obj.is_null() {
+        return;
+    }
+    let device = (*obj).device;
+    if name.is_null() {
+        return;
+    }
+    let server = (*(*device).seat).server;
+    let seats_head = &mut (*server).input_manager.seats as *mut ffi::wl_list as *mut WlList;
+    let mut curr = (*seats_head).next;
+    let mut found = false;
+    let name_str = std::ffi::CStr::from_ptr(name).to_string_lossy();
+    while curr != seats_head {
+        let next = (*curr).next;
+        let seat = crate::container_of!(curr, Seat, link);
+        let seat_name = std::ffi::CStr::from_ptr(ffi::river_wlr_seat_get_name((*seat).wlr_seat)).to_string_lossy();
+        if seat_name == name_str {
+            (*device).assign_to_seat(seat);
+            found = true;
+            break;
+        }
+        curr = next;
+    }
+    if !found {
+        log::info!("client requested input device be assigned to non-existent seat '{}'", name_str);
+    }
+}
+
+unsafe extern "C" fn input_device_set_repeat_info(
+    _client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    rate: i32,
+    delay: i32,
+) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut InputDeviceObject;
+    if obj.is_null() {
+        return;
+    }
+    let device = (*obj).device;
+    if rate < 0 || delay < 0 {
+        ffi::wl_resource_post_error(
+            resource,
+            ffi::river_input_device_v1_error_RIVER_INPUT_DEVICE_V1_ERROR_INVALID_REPEAT_INFO,
+            b"negative rate/delay\0".as_ptr() as *const _,
+        );
+        return;
+    }
+    let dev_type = ffi::river_wlr_input_device_get_type((*device).wlr_device);
+    if dev_type == ffi::wlr_input_device_type_WLR_INPUT_DEVICE_KEYBOARD {
+        let keyboard = (*device).destroy_data as *mut crate::keyboard::Keyboard;
+        if !keyboard.is_null() {
+            (*keyboard).set_repeat_info(rate, delay);
+        }
+    }
+}
+
+unsafe extern "C" fn input_device_set_scroll_factor(
+    _client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    factor_raw: ffi::wl_fixed_t,
+) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut InputDeviceObject;
+    if obj.is_null() {
+        return;
+    }
+    let device = (*obj).device;
+    let factor = factor_raw as f64 / 256.0;
+    if factor < 0.0 {
+        ffi::wl_resource_post_error(
+            resource,
+            ffi::river_input_device_v1_error_RIVER_INPUT_DEVICE_V1_ERROR_INVALID_SCROLL_FACTOR,
+            b"negative scroll factor\0".as_ptr() as *const _,
+        );
+        return;
+    }
+    (*device).config.scroll_factor = factor;
+}
+
+unsafe extern "C" fn input_device_map_to_output(
+    _client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    output_resource: *mut ffi::wl_resource,
+) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut InputDeviceObject;
+    if obj.is_null() {
+        return;
+    }
+    let device = (*obj).device;
+
+    let dev_type = ffi::river_wlr_input_device_get_type((*device).wlr_device);
+    match dev_type {
+        ffi::wlr_input_device_type_WLR_INPUT_DEVICE_POINTER |
+        ffi::wlr_input_device_type_WLR_INPUT_DEVICE_TOUCH |
+        ffi::wlr_input_device_type_WLR_INPUT_DEVICE_TABLET => {},
+        _ => return,
+    }
+
+    let wlr_output = if !output_resource.is_null() {
+        let out = ffi::wlr_output_from_resource(output_resource);
+        if out.is_null() {
+            return;
+        }
+        out
+    } else {
+        std::ptr::null_mut()
+    };
+
+    (*device).config.map_to_output = wlr_output;
+
+    match dev_type {
+        ffi::wlr_input_device_type_WLR_INPUT_DEVICE_TOUCH |
+        ffi::wlr_input_device_type_WLR_INPUT_DEVICE_TABLET => {
+            if !(*device).seat.is_null() {
+                let cursor = &mut (*(*device).seat).cursor;
+                ffi::wlr_cursor_map_input_to_output(cursor.wlr_cursor, (*device).wlr_device, wlr_output);
+            }
+        }
+        _ => {}
+    }
+}
+
+unsafe extern "C" fn input_device_map_to_rectangle(
+    _client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    x: i32,
+    y: i32,
+    width: i32,
+    height: i32,
+) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut InputDeviceObject;
+    if obj.is_null() {
+        return;
+    }
+    let device = (*obj).device;
+
+    if width < 0 || height < 0 {
+        ffi::wl_resource_post_error(
+            resource,
+            ffi::river_input_device_v1_error_RIVER_INPUT_DEVICE_V1_ERROR_INVALID_MAP_TO_RECTANGLE,
+            b"negative rectangle width/height\0".as_ptr() as *const _,
+        );
+        return;
+    }
+
+    let dev_type = ffi::river_wlr_input_device_get_type((*device).wlr_device);
+    match dev_type {
+        ffi::wlr_input_device_type_WLR_INPUT_DEVICE_POINTER |
+        ffi::wlr_input_device_type_WLR_INPUT_DEVICE_TOUCH |
+        ffi::wlr_input_device_type_WLR_INPUT_DEVICE_TABLET => {},
+        _ => return,
+    }
+
+    (*device).config.map_to_rectangle = ffi::wlr_box {
+        x,
+        y,
+        width,
+        height,
+    };
+
+    match dev_type {
+        ffi::wlr_input_device_type_WLR_INPUT_DEVICE_TOUCH |
+        ffi::wlr_input_device_type_WLR_INPUT_DEVICE_TABLET => {
+            if !(*device).seat.is_null() {
+                let cursor = &mut (*(*device).seat).cursor;
+                ffi::wlr_cursor_map_input_to_region(
+                    cursor.wlr_cursor,
+                    (*device).wlr_device,
+                    &mut (*device).config.map_to_rectangle,
+                );
+            }
+        }
+        _ => {}
+    }
+}
diff --git a/src/input_manager.rs b/src/input_manager.rs
index b6db300..8841715 100644
--- a/src/input_manager.rs
+++ b/src/input_manager.rs
@@ -1,14 +1,17 @@
-// SPDX-FileCopyrightText: © 2020 The River Developers
+// SPDX-FileCopyrightText: © 2026 The River Developers
 // SPDX-License-Identifier: GPL-3.0-only
 
 use crate::ffi;
-use crate::server::{Server, wl_signal_add, WlListener, wl_listener_remove};
+use crate::server::{Server, wl_signal_add, WlListener, wl_listener_remove, WlList, wl_list_insert, wl_list_remove};
 use crate::seat::Seat;
 
 pub struct InputManager {
     pub server: *mut Server,
     pub default_seat: *mut Seat,
     pub seats: ffi::wl_list,
+    pub devices: ffi::wl_list, // list of InputDevice
+    pub objects: ffi::wl_list, // list of InputManagerObject
+    pub global: *mut ffi::wl_global,
     
     pub idle_notifier: *mut ffi::wlr_idle_notifier_v1,
     pub relative_pointer_manager: *mut ffi::wlr_relative_pointer_manager_v1,
@@ -25,6 +28,12 @@ pub struct InputManager {
     pub new_input_method: ffi::wl_listener,
 }
 
+pub struct InputManagerObject {
+    pub manager: *mut InputManager,
+    pub resource: *mut ffi::wl_resource,
+    pub link: ffi::wl_list,
+}
+
 impl Default for InputManager {
     fn default() -> Self {
         unsafe { std::mem::zeroed() }
@@ -37,6 +46,8 @@ impl InputManager {
         let wl_server = (*server).wl_server;
 
         ffi::wl_list_init(&mut self.seats);
+        ffi::wl_list_init(&mut self.devices);
+        ffi::wl_list_init(&mut self.objects);
 
         self.idle_notifier = ffi::wlr_idle_notifier_v1_create(wl_server);
         self.relative_pointer_manager = ffi::wlr_relative_pointer_manager_v1_create(wl_server);
@@ -51,6 +62,17 @@ impl InputManager {
         // Create default seat
         self.default_seat = Seat::create(server, "default")?;
 
+        self.global = ffi::wl_global_create(
+            wl_server,
+            &ffi::river_input_manager_v1_interface,
+            2,
+            self as *mut InputManager as *mut _,
+            Some(bind_input_manager),
+        );
+        if self.global.is_null() {
+            return Err("Failed to create river_input_manager_v1 global");
+        }
+
         let new_input_ptr = &mut self.new_input_listener as *mut ffi::wl_listener as *mut WlListener;
         (*new_input_ptr).notify = Some(handle_new_input);
         
@@ -71,6 +93,20 @@ impl InputManager {
     }
 
     pub unsafe fn deinit(&mut self) {
+        if !self.global.is_null() {
+            ffi::wl_global_destroy(self.global);
+            self.global = std::ptr::null_mut();
+        }
+
+        let objects_head = &mut self.objects as *mut ffi::wl_list as *mut WlList;
+        let mut curr = (*objects_head).next;
+        while curr != objects_head {
+            let next = (*curr).next;
+            let obj = crate::container_of!(curr, InputManagerObject, link);
+            ffi::wl_resource_destroy((*obj).resource);
+            curr = next;
+        }
+
         if !self.default_seat.is_null() {
             Seat::destroy(self.default_seat);
             self.default_seat = std::ptr::null_mut();
@@ -81,6 +117,179 @@ impl InputManager {
     }
 }
 
+unsafe extern "C" fn bind_input_manager(
+    client: *mut ffi::wl_client,
+    data: *mut std::ffi::c_void,
+    version: u32,
+    id: u32,
+) {
+    let im = data as *mut InputManager;
+    let resource = ffi::wl_resource_create(client, &ffi::river_input_manager_v1_interface, version as i32, id);
+    if resource.is_null() {
+        ffi::wl_client_post_no_memory(client);
+        return;
+    }
+
+    let obj = Box::into_raw(Box::new(InputManagerObject {
+        manager: im,
+        resource,
+        link: std::mem::zeroed(),
+    }));
+
+    ffi::wl_resource_set_implementation(
+        resource,
+        &INPUT_MANAGER_INTERFACE as *const _ as *const _,
+        obj as *mut _,
+        Some(handle_manager_object_destroy),
+    );
+
+    let list_head = &mut (*im).objects as *mut ffi::wl_list as *mut WlList;
+    wl_list_insert((*list_head).prev, &mut (*obj).link as *mut ffi::wl_list as *mut WlList);
+
+    // Send existing devices to the client
+    let devices_head = &mut (*im).devices as *mut ffi::wl_list as *mut WlList;
+    let mut curr = (*devices_head).next;
+    while curr != devices_head {
+        let next = (*curr).next;
+        let device = crate::container_of!(curr, crate::input_device::InputDevice, link);
+        if !(*device).virtual_device {
+            (*device).create_object(resource);
+        }
+        curr = next;
+    }
+}
+
+unsafe extern "C" fn handle_manager_object_destroy(resource: *mut ffi::wl_resource) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut InputManagerObject;
+    if !obj.is_null() {
+        wl_list_remove(&mut (*obj).link as *mut ffi::wl_list as *mut WlList);
+        let _ = Box::from_raw(obj);
+    }
+}
+
+static INPUT_MANAGER_INTERFACE: ffi::river_input_manager_v1_interface = ffi::river_input_manager_v1_interface {
+    stop: Some(input_manager_stop),
+    destroy: Some(input_manager_destroy),
+    create_seat: Some(input_manager_create_seat),
+    destroy_seat: Some(input_manager_destroy_seat),
+};
+
+static INPUT_MANAGER_INERT_INTERFACE: ffi::river_input_manager_v1_interface = ffi::river_input_manager_v1_interface {
+    stop: None,
+    destroy: Some(input_manager_inert_destroy),
+    create_seat: None,
+    destroy_seat: None,
+};
+
+unsafe extern "C" fn input_manager_stop(
+    _client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut InputManagerObject;
+    if obj.is_null() {
+        return;
+    }
+
+    wl_list_remove(&mut (*obj).link as *mut ffi::wl_list as *mut WlList);
+    ffi::wl_list_init(&mut (*obj).link);
+
+    ffi::wl_resource_post_event(resource, 0); // finished event
+
+    ffi::wl_resource_set_implementation(
+        resource,
+        &INPUT_MANAGER_INERT_INTERFACE as *const _ as *const _,
+        obj as *mut _,
+        Some(handle_manager_object_destroy),
+    );
+}
+
+unsafe extern "C" fn input_manager_destroy(
+    _client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+) {
+    ffi::wl_resource_post_error(
+        resource,
+        ffi::river_input_manager_v1_error_RIVER_INPUT_MANAGER_V1_ERROR_INVALID_DESTROY,
+        b"destroy before finished event sent\0".as_ptr() as *const _,
+    );
+}
+
+unsafe extern "C" fn input_manager_inert_destroy(
+    _client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+) {
+    ffi::wl_resource_destroy(resource);
+}
+
+unsafe extern "C" fn input_manager_create_seat(
+    _client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    name: *const std::os::raw::c_char,
+) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut InputManagerObject;
+    if obj.is_null() || name.is_null() {
+        return;
+    }
+    let im = (*obj).manager;
+    let name_str = std::ffi::CStr::from_ptr(name).to_string_lossy();
+
+    // Check if seat already exists
+    let seats_head = &mut (*im).seats as *mut ffi::wl_list as *mut WlList;
+    let mut curr = (*seats_head).next;
+    let mut exists = false;
+    while curr != seats_head {
+        let next = (*curr).next;
+        let seat = crate::container_of!(curr, Seat, link);
+        let seat_name = std::ffi::CStr::from_ptr(ffi::river_wlr_seat_get_name((*seat).wlr_seat)).to_string_lossy();
+        if seat_name == name_str {
+            exists = true;
+            break;
+        }
+        curr = next;
+    }
+
+    if !exists {
+        let server = (*im).server;
+        if let Err(e) = Seat::create(server, &name_str) {
+            log::error!("failed to create seat '{}': {}", name_str, e);
+            ffi::wl_resource_post_no_memory(resource);
+        }
+    }
+}
+
+unsafe extern "C" fn input_manager_destroy_seat(
+    _client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    name: *const std::os::raw::c_char,
+) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut InputManagerObject;
+    if obj.is_null() || name.is_null() {
+        return;
+    }
+    let im = (*obj).manager;
+    let name_str = std::ffi::CStr::from_ptr(name).to_string_lossy();
+
+    if name_str == "default" {
+        return;
+    }
+
+    let seats_head = &mut (*im).seats as *mut ffi::wl_list as *mut WlList;
+    let mut curr = (*seats_head).next;
+    // Skip default seat (which is the first seat in the list)
+    curr = (*curr).next;
+    while curr != seats_head {
+        let next = (*curr).next;
+        let seat = crate::container_of!(curr, Seat, link);
+        let seat_name = std::ffi::CStr::from_ptr(ffi::river_wlr_seat_get_name((*seat).wlr_seat)).to_string_lossy();
+        if seat_name == name_str {
+            (*seat).destroying = true;
+            (*(*im).server).wm.dirty_windowing();
+            break;
+        }
+        curr = next;
+    }
+}
+
 unsafe extern "C" fn handle_new_input(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
     let im = &mut *crate::container_of!(listener, InputManager, new_input_listener);
     let wlr_device = data as *mut ffi::wlr_input_device;
@@ -95,6 +304,9 @@ unsafe extern "C" fn handle_new_input(listener: *mut ffi::wl_listener, data: *mu
     } else if dev_type == ffi::wlr_input_device_type_WLR_INPUT_DEVICE_TABLET {
         let _ = crate::tablet::Tablet::create(im.default_seat, wlr_device, false);
     }
+
+    // Attach device to the default seat
+    (*im.default_seat).attach_device(device);
 }
 
 unsafe extern "C" fn handle_new_text_input(listener: *mut ffi::wl_listener, data: *mut std::ffi::c_void) {
diff --git a/src/libinput_accel_config.rs b/src/libinput_accel_config.rs
new file mode 100644
index 0000000..2dff102
--- /dev/null
+++ b/src/libinput_accel_config.rs
@@ -0,0 +1,157 @@
+// SPDX-FileCopyrightText: © 2026 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+
+use crate::ffi;
+
+pub struct LibinputAccelConfig {
+    pub resource: *mut ffi::wl_resource,
+    pub libinput: *mut ffi::libinput_config_accel,
+}
+
+impl LibinputAccelConfig {
+    pub unsafe fn create(
+        client: *mut ffi::wl_client,
+        version: u32,
+        id: u32,
+        profile: u32,
+    ) -> Result<*mut Self, &'static str> {
+        let libinput = ffi::libinput_config_accel_create(profile);
+        if libinput.is_null() {
+            return Err("Failed to create libinput_config_accel");
+        }
+
+        let resource = ffi::wl_resource_create(
+            client,
+            &ffi::river_libinput_accel_config_v1_interface,
+            version as i32,
+            id,
+        );
+        if resource.is_null() {
+            ffi::libinput_config_accel_destroy(libinput);
+            return Err("Failed to create river_libinput_accel_config_v1 resource");
+        }
+
+        let accel_config = Box::into_raw(Box::new(Self {
+            resource,
+            libinput,
+        }));
+
+        ffi::wl_resource_set_implementation(
+            resource,
+            &ACCEL_CONFIG_INTERFACE as *const _ as *const _,
+            accel_config as *mut _,
+            Some(handle_accel_config_destroy_resource),
+        );
+
+        Ok(accel_config)
+    }
+}
+
+unsafe extern "C" fn handle_accel_config_destroy_resource(resource: *mut ffi::wl_resource) {
+    let accel_config = ffi::wl_resource_get_user_data(resource) as *mut LibinputAccelConfig;
+    if !accel_config.is_null() {
+        if !(*accel_config).libinput.is_null() {
+            ffi::libinput_config_accel_destroy((*accel_config).libinput);
+        }
+        let _ = Box::from_raw(accel_config);
+    }
+}
+
+static ACCEL_CONFIG_INTERFACE: ffi::river_libinput_accel_config_v1_interface = ffi::river_libinput_accel_config_v1_interface {
+    destroy: Some(accel_config_destroy),
+    set_points: Some(accel_config_set_points),
+};
+
+unsafe extern "C" fn accel_config_destroy(_client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
+    ffi::wl_resource_destroy(resource);
+}
+
+unsafe extern "C" fn accel_config_set_points(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    result_id: u32,
+    accel_type_raw: u32,
+    step_arr: *mut ffi::wl_array,
+    points_arr: *mut ffi::wl_array,
+) {
+    let accel_config = ffi::wl_resource_get_user_data(resource) as *mut LibinputAccelConfig;
+    if accel_config.is_null() {
+        return;
+    }
+
+    let accel_type = match accel_type_raw {
+        0 => ffi::libinput_config_accel_type_LIBINPUT_ACCEL_TYPE_FALLBACK,
+        1 => ffi::libinput_config_accel_type_LIBINPUT_ACCEL_TYPE_MOTION,
+        2 => ffi::libinput_config_accel_type_LIBINPUT_ACCEL_TYPE_SCROLL,
+        _ => {
+            ffi::wl_resource_post_error(
+                resource,
+                ffi::river_libinput_accel_config_v1_error_RIVER_LIBINPUT_ACCEL_CONFIG_V1_ERROR_INVALID_ARG,
+                b"invalid accel_type enum value\0".as_ptr() as *const _,
+            );
+            return;
+        }
+    };
+
+    if (*step_arr).size != std::mem::size_of::<f64>() as usize {
+        ffi::wl_resource_post_error(
+            resource,
+            ffi::river_libinput_accel_config_v1_error_RIVER_LIBINPUT_ACCEL_CONFIG_V1_ERROR_INVALID_ARG,
+            b"invalid step argument\0".as_ptr() as *const _,
+        );
+        return;
+    }
+
+    let step = *((*step_arr).data as *const f64);
+
+    if (*points_arr).size == 0 || (*points_arr).size % std::mem::size_of::<f64>() as usize != 0 {
+        ffi::wl_resource_post_error(
+            resource,
+            ffi::river_libinput_accel_config_v1_error_RIVER_LIBINPUT_ACCEL_CONFIG_V1_ERROR_INVALID_ARG,
+            b"invalid points argument\0".as_ptr() as *const _,
+        );
+        return;
+    }
+
+    let points_count = (*points_arr).size / std::mem::size_of::<f64>();
+    let points_ptr = (*points_arr).data as *const f64;
+
+    let result_res = ffi::wl_resource_create(
+        client,
+        &ffi::river_libinput_result_v1_interface,
+        ffi::wl_resource_get_version(resource),
+        result_id,
+    );
+    if result_res.is_null() {
+        ffi::wl_client_post_no_memory(client);
+        return;
+    }
+
+    let libinput = (*accel_config).libinput;
+    if libinput.is_null() {
+        ffi::wl_resource_post_event(result_res, 2); // invalid
+        ffi::wl_resource_destroy(result_res);
+        return;
+    }
+
+    let status = ffi::libinput_config_accel_set_points(
+        libinput,
+        accel_type,
+        step,
+        points_count,
+        points_ptr,
+    );
+
+    match status {
+        ffi::libinput_config_status_LIBINPUT_CONFIG_STATUS_SUCCESS => {
+            ffi::wl_resource_post_event(result_res, 0); // success
+        }
+        ffi::libinput_config_status_LIBINPUT_CONFIG_STATUS_UNSUPPORTED => {
+            ffi::wl_resource_post_event(result_res, 1); // unsupported
+        }
+        _ => {
+            ffi::wl_resource_post_event(result_res, 2); // invalid
+        }
+    }
+    ffi::wl_resource_destroy(result_res);
+}
diff --git a/src/libinput_config.rs b/src/libinput_config.rs
index 653ec9a..494a33d 100644
--- a/src/libinput_config.rs
+++ b/src/libinput_config.rs
@@ -2,12 +2,13 @@
 // SPDX-License-Identifier: GPL-3.0-only
 
 use crate::ffi;
-use crate::server::{Server, WlListener, wl_signal_add, wl_listener_remove, WlList, wl_list_insert, wl_list_remove};
+use crate::server::{Server, WlList, wl_list_insert, wl_list_remove};
 
 pub struct LibinputConfig {
     pub server: *mut Server,
     pub global: *mut ffi::wl_global,
     pub objects: ffi::wl_list,
+    pub devices: ffi::wl_list,
 }
 
 pub struct LibinputConfigObject {
@@ -26,6 +27,7 @@ impl LibinputConfig {
     pub unsafe fn init(&mut self, server: *mut Server) -> Result<(), &'static str> {
         self.server = server;
         ffi::wl_list_init(&mut self.objects);
+        ffi::wl_list_init(&mut self.devices);
 
         self.global = ffi::wl_global_create(
             (*server).wl_server,
@@ -86,6 +88,15 @@ unsafe extern "C" fn bind_libinput_config(
 
     let list_head = &mut (*config).objects as *mut ffi::wl_list as *mut WlList;
     wl_list_insert((*list_head).prev, &mut (*obj).link as *mut ffi::wl_list as *mut WlList);
+
+    let devices_head = &mut (*config).devices as *mut ffi::wl_list as *mut WlList;
+    let mut curr = (*devices_head).next;
+    while curr != devices_head {
+        let next = (*curr).next;
+        let dev = crate::container_of!(curr, crate::libinput_device::LibinputDevice, link);
+        (*dev).create_object(resource);
+        curr = next;
+    }
 }
 
 unsafe extern "C" fn handle_object_destroy(resource: *mut ffi::wl_resource) {
@@ -143,52 +154,15 @@ unsafe extern "C" fn libinput_config_create_accel_config(
     client: *mut ffi::wl_client,
     resource: *mut ffi::wl_resource,
     id: u32,
-    _profile: u32,
+    profile: u32,
 ) {
-    let accel_res = ffi::wl_resource_create(
+    if let Err(e) = crate::libinput_accel_config::LibinputAccelConfig::create(
         client,
-        &ffi::river_libinput_accel_config_v1_interface,
-        ffi::wl_resource_get_version(resource),
+        ffi::wl_resource_get_version(resource) as u32,
         id,
-    );
-    if accel_res.is_null() {
-        ffi::wl_client_post_no_memory(client);
-        return;
-    }
-
-    static LIBINPUT_ACCEL_CONFIG_INTERFACE: ffi::river_libinput_accel_config_v1_interface = ffi::river_libinput_accel_config_v1_interface {
-        destroy: Some(libinput_accel_config_destroy),
-        set_points: Some(libinput_accel_config_set_points),
-    };
-
-    ffi::wl_resource_set_implementation(
-        accel_res,
-        &LIBINPUT_ACCEL_CONFIG_INTERFACE as *const _ as *const _,
-        std::ptr::null_mut(),
-        None,
-    );
-}
-
-unsafe extern "C" fn libinput_accel_config_destroy(_client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
-    ffi::wl_resource_destroy(resource);
-}
-
-unsafe extern "C" fn libinput_accel_config_set_points(
-    client: *mut ffi::wl_client,
-    resource: *mut ffi::wl_resource,
-    result_id: u32,
-    _accel_type: u32,
-    _step: *mut ffi::wl_array,
-    _points: *mut ffi::wl_array,
-) {
-    let result_res = ffi::wl_resource_create(
-        client,
-        &ffi::river_libinput_result_v1_interface,
-        ffi::wl_resource_get_version(resource),
-        result_id,
-    );
-    if !result_res.is_null() {
-        ffi::wl_resource_post_event(result_res, 1); // unsupported event
-        ffi::wl_resource_destroy(result_res);
+        profile,
+    ) {
+        log::error!("Failed to create LibinputAccelConfig: {}", e);
+        ffi::wl_resource_post_no_memory(resource);
     }
 }
diff --git a/src/libinput_device.rs b/src/libinput_device.rs
new file mode 100644
index 0000000..7dbd900
--- /dev/null
+++ b/src/libinput_device.rs
@@ -0,0 +1,813 @@
+// SPDX-FileCopyrightText: © 2026 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+
+use crate::ffi;
+use crate::input_device::InputDevice;
+use crate::server::WlList;
+
+pub struct LibinputDevice {
+    pub parent_device: *mut InputDevice,
+    pub libinput: *mut ffi::libinput_device,
+    pub objects: ffi::wl_list, // list of LibinputDeviceObject
+    pub link: ffi::wl_list,    // link inside LibinputConfig::devices
+}
+
+pub struct LibinputDeviceObject {
+    pub device: *mut LibinputDevice,
+    pub resource: *mut ffi::wl_resource,
+    pub link: ffi::wl_list,
+}
+
+impl LibinputDevice {
+    pub unsafe fn init(
+        parent_device: *mut InputDevice,
+        handle: *mut ffi::libinput_device,
+    ) -> Box<Self> {
+        let mut dev = Box::new(Self {
+            parent_device,
+            libinput: handle,
+            objects: std::mem::zeroed(),
+            link: std::mem::zeroed(),
+        });
+
+        ffi::wl_list_init(&mut dev.objects);
+        ffi::wl_list_init(&mut dev.link);
+
+        let server = (*(*parent_device).seat).server;
+        let devices_head = &mut (*server).libinput_config.devices as *mut ffi::wl_list as *mut WlList;
+        crate::server::wl_list_insert((*devices_head).prev, &mut dev.link as *mut ffi::wl_list as *mut WlList);
+
+        let config_objects = &mut (*server).libinput_config.objects as *mut ffi::wl_list as *mut WlList;
+        let mut curr = (*config_objects).next;
+        while curr != config_objects {
+            let next = (*curr).next;
+            let config_obj = crate::container_of!(curr, crate::libinput_config::LibinputConfigObject, link);
+            dev.create_object((*config_obj).resource);
+            curr = next;
+        }
+
+        dev
+    }
+
+    pub unsafe fn deinit(&mut self) {
+        let objects_head = &mut self.objects as *mut ffi::wl_list as *mut WlList;
+        let mut curr = (*objects_head).next;
+        while curr != objects_head {
+            let next = (*curr).next;
+            let obj = crate::container_of!(curr, LibinputDeviceObject, link);
+
+            crate::server::wl_list_remove(curr);
+            ffi::wl_resource_post_event((*obj).resource, ffi::RIVER_LIBINPUT_DEVICE_V1_REMOVED);
+
+            ffi::wl_resource_set_implementation(
+                (*obj).resource,
+                std::ptr::null(),
+                std::ptr::null_mut(),
+                None,
+            );
+
+            let _ = Box::from_raw(obj);
+            curr = next;
+        }
+
+        crate::server::wl_list_remove(&mut self.link as *mut ffi::wl_list as *mut WlList);
+    }
+
+    pub unsafe fn create_object(&mut self, config_v1_resource: *mut ffi::wl_resource) {
+        let client = ffi::wl_resource_get_client(config_v1_resource);
+        let version = ffi::wl_resource_get_version(config_v1_resource);
+
+        let resource = ffi::wl_resource_create(
+            client,
+            &ffi::river_libinput_device_v1_interface,
+            version,
+            0,
+        );
+        if resource.is_null() {
+            log::error!("out of memory creating river_libinput_device_v1");
+            ffi::wl_client_post_no_memory(client);
+            return;
+        }
+
+        let obj = Box::into_raw(Box::new(LibinputDeviceObject {
+            device: self,
+            resource,
+            link: std::mem::zeroed(),
+        }));
+
+        ffi::wl_resource_set_implementation(
+            resource,
+            &LIBINPUT_DEVICE_INTERFACE as *const _ as *const _,
+            obj as *mut _,
+            Some(handle_device_object_destroy),
+        );
+
+        let objects_head = &mut self.objects as *mut ffi::wl_list as *mut WlList;
+        let link_ptr = &mut (*obj).link as *mut ffi::wl_list as *mut WlList;
+        crate::server::wl_list_insert(objects_head, link_ptr);
+
+        // Send libinput_device event
+        ffi::wl_resource_post_event(config_v1_resource, ffi::RIVER_LIBINPUT_CONFIG_V1_LIBINPUT_DEVICE, resource);
+
+        // Send corresponding river input device
+        let input_dev_objects = &mut (*self.parent_device).objects as *mut ffi::wl_list as *mut WlList;
+        let mut curr = (*input_dev_objects).next;
+        while curr != input_dev_objects {
+            let next = (*curr).next;
+            let input_dev_obj = crate::container_of!(curr, crate::input_device::InputDeviceObject, link);
+            if ffi::wl_resource_get_client((*input_dev_obj).resource) == client {
+                ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_INPUT_DEVICE, (*input_dev_obj).resource);
+            }
+            curr = next;
+        }
+
+        // SendEvents support and defaults
+        let send_events_modes = ffi::libinput_device_config_send_events_get_modes(self.libinput);
+        ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_SEND_EVENTS_SUPPORT, send_events_modes);
+        let send_events_default = ffi::libinput_device_config_send_events_get_default_mode(self.libinput);
+        ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_SEND_EVENTS_DEFAULT, send_events_default);
+        let send_events_current = ffi::libinput_device_config_send_events_get_mode(self.libinput);
+        ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_SEND_EVENTS_CURRENT, send_events_current);
+
+        // Tap support and defaults
+        let tap_finger_count = ffi::libinput_device_config_tap_get_finger_count(self.libinput);
+        ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_TAP_SUPPORT, tap_finger_count as i32);
+        if tap_finger_count > 0 {
+            ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_TAP_DEFAULT, ffi::libinput_device_config_tap_get_default_enabled(self.libinput));
+            ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_TAP_CURRENT, ffi::libinput_device_config_tap_get_enabled(self.libinput));
+            ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_TAP_BUTTON_MAP_DEFAULT, ffi::libinput_device_config_tap_get_default_button_map(self.libinput));
+            ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_TAP_BUTTON_MAP_CURRENT, ffi::libinput_device_config_tap_get_button_map(self.libinput));
+            ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_DRAG_DEFAULT, ffi::libinput_device_config_tap_get_default_drag_enabled(self.libinput));
+            ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_DRAG_CURRENT, ffi::libinput_device_config_tap_get_drag_enabled(self.libinput));
+            ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_DRAG_LOCK_DEFAULT, ffi::libinput_device_config_tap_get_default_drag_lock_enabled(self.libinput));
+            ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_DRAG_LOCK_CURRENT, ffi::libinput_device_config_tap_get_drag_lock_enabled(self.libinput));
+        }
+
+        // Three finger drag
+        let three_finger_drag_finger_count = ffi::libinput_device_config_3fg_drag_get_finger_count(self.libinput);
+        ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_THREE_FINGER_DRAG_SUPPORT, three_finger_drag_finger_count as i32);
+        if three_finger_drag_finger_count >= 3 {
+            ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_THREE_FINGER_DRAG_DEFAULT, ffi::libinput_device_config_3fg_drag_get_default_enabled(self.libinput));
+            ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_THREE_FINGER_DRAG_CURRENT, ffi::libinput_device_config_3fg_drag_get_enabled(self.libinput));
+        }
+
+        // Calibration matrix
+        let has_matrix = ffi::libinput_device_config_calibration_has_matrix(self.libinput);
+        ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_CALIBRATION_MATRIX_SUPPORT, has_matrix as i32);
+        if has_matrix != 0 {
+            let mut matrix = [0.0f32; 6];
+            ffi::libinput_device_config_calibration_get_default_matrix(self.libinput, matrix.as_mut_ptr());
+            let mut arr_default = ffi::wl_array {
+                size: std::mem::size_of_val(&matrix),
+                alloc: std::mem::size_of_val(&matrix),
+                data: matrix.as_mut_ptr() as *mut _,
+            };
+            ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_CALIBRATION_MATRIX_DEFAULT, &mut arr_default as *mut _);
+
+            ffi::libinput_device_config_calibration_get_matrix(self.libinput, matrix.as_mut_ptr());
+            let mut arr_current = ffi::wl_array {
+                size: std::mem::size_of_val(&matrix),
+                alloc: std::mem::size_of_val(&matrix),
+                data: matrix.as_mut_ptr() as *mut _,
+            };
+            ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_CALIBRATION_MATRIX_CURRENT, &mut arr_current as *mut _);
+        }
+
+        // Acceleration configs
+        let profiles = ffi::libinput_device_config_accel_get_profiles(self.libinput);
+        ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_ACCEL_PROFILES_SUPPORT, profiles);
+        if profiles != 0 {
+            ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_ACCEL_PROFILE_DEFAULT, ffi::libinput_device_config_accel_get_default_profile(self.libinput));
+            ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_ACCEL_PROFILE_CURRENT, ffi::libinput_device_config_accel_get_profile(self.libinput));
+
+            let default_speed = ffi::libinput_device_config_accel_get_default_speed(self.libinput);
+            let mut arr_default = ffi::wl_array {
+                size: std::mem::size_of::<f64>(),
+                alloc: std::mem::size_of::<f64>(),
+                data: &default_speed as *const f64 as *mut _,
+            };
+            ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_ACCEL_SPEED_DEFAULT, &mut arr_default as *mut _);
+
+            let current_speed = ffi::libinput_device_config_accel_get_speed(self.libinput);
+            let mut arr_current = ffi::wl_array {
+                size: std::mem::size_of::<f64>(),
+                alloc: std::mem::size_of::<f64>(),
+                data: &current_speed as *const f64 as *mut _,
+            };
+            ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_ACCEL_SPEED_CURRENT, &mut arr_current as *mut _);
+        }
+
+        // Natural scroll
+        let natural_scroll = ffi::libinput_device_config_scroll_has_natural_scroll(self.libinput);
+        ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_NATURAL_SCROLL_SUPPORT, natural_scroll as i32);
+        if natural_scroll != 0 {
+            ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_NATURAL_SCROLL_DEFAULT, ffi::libinput_device_config_scroll_get_default_natural_scroll_enabled(self.libinput) as u32);
+            ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_NATURAL_SCROLL_CURRENT, ffi::libinput_device_config_scroll_get_natural_scroll_enabled(self.libinput) as u32);
+        }
+
+        // Left handed mode
+        let left_handed = ffi::libinput_device_config_left_handed_is_available(self.libinput);
+        ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_LEFT_HANDED_SUPPORT, left_handed as i32);
+        if left_handed != 0 {
+            ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_LEFT_HANDED_DEFAULT, ffi::libinput_device_config_left_handed_get_default(self.libinput) as u32);
+            ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_LEFT_HANDED_CURRENT, ffi::libinput_device_config_left_handed_get(self.libinput) as u32);
+        }
+
+        // Click methods
+        let click_methods = ffi::libinput_device_config_click_get_methods(self.libinput);
+        ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_CLICK_METHOD_SUPPORT, click_methods);
+        if click_methods != 0 {
+            ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_CLICK_METHOD_DEFAULT, ffi::libinput_device_config_click_get_default_method(self.libinput));
+            ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_CLICK_METHOD_CURRENT, ffi::libinput_device_config_click_get_method(self.libinput));
+            if (click_methods & ffi::libinput_config_click_method_LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER) != 0 {
+                ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_CLICKFINGER_BUTTON_MAP_DEFAULT, ffi::libinput_device_config_click_get_default_clickfinger_button_map(self.libinput));
+                ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_CLICKFINGER_BUTTON_MAP_CURRENT, ffi::libinput_device_config_click_get_clickfinger_button_map(self.libinput));
+            }
+        }
+
+        // Middle mouse emulation
+        let middle_emulation = ffi::libinput_device_config_middle_emulation_is_available(self.libinput);
+        ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_MIDDLE_EMULATION_SUPPORT, middle_emulation as i32);
+        if middle_emulation != 0 {
+            ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_MIDDLE_EMULATION_DEFAULT, ffi::libinput_device_config_middle_emulation_get_default_enabled(self.libinput));
+            ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_MIDDLE_EMULATION_CURRENT, ffi::libinput_device_config_middle_emulation_get_enabled(self.libinput));
+        }
+
+        // Scroll methods
+        let scroll_methods = ffi::libinput_device_config_scroll_get_methods(self.libinput);
+        ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_SCROLL_METHOD_SUPPORT, scroll_methods);
+        if scroll_methods != 0 {
+            ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_SCROLL_METHOD_DEFAULT, ffi::libinput_device_config_scroll_get_default_method(self.libinput));
+            ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_SCROLL_METHOD_CURRENT, ffi::libinput_device_config_scroll_get_method(self.libinput));
+            if (scroll_methods & ffi::libinput_config_scroll_method_LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN) != 0 {
+                ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_SCROLL_BUTTON_DEFAULT, ffi::libinput_device_config_scroll_get_default_button(self.libinput));
+                ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_SCROLL_BUTTON_CURRENT, ffi::libinput_device_config_scroll_get_button(self.libinput));
+                ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_SCROLL_BUTTON_LOCK_DEFAULT, ffi::libinput_device_config_scroll_get_default_button_lock(self.libinput));
+                ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_SCROLL_BUTTON_LOCK_CURRENT, ffi::libinput_device_config_scroll_get_button_lock(self.libinput));
+            }
+        }
+
+        // Disable While Typing (DWT)
+        let dwt = ffi::libinput_device_config_dwt_is_available(self.libinput);
+        ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_DWT_SUPPORT, dwt as i32);
+        if dwt != 0 {
+            ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_DWT_DEFAULT, ffi::libinput_device_config_dwt_get_default_enabled(self.libinput) as u32);
+            ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_DWT_CURRENT, ffi::libinput_device_config_dwt_get_enabled(self.libinput) as u32);
+        }
+
+        // Disable While Trackpointing (DWTP)
+        let dwtp = ffi::libinput_device_config_dwtp_is_available(self.libinput);
+        ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_DWTP_SUPPORT, dwtp as i32);
+        if dwtp != 0 {
+            ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_DWTP_DEFAULT, ffi::libinput_device_config_dwtp_get_default_enabled(self.libinput) as u32);
+            ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_DWTP_CURRENT, ffi::libinput_device_config_dwtp_get_enabled(self.libinput) as u32);
+        }
+
+        // Rotation
+        let rotation = ffi::libinput_device_config_rotation_is_available(self.libinput);
+        ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_ROTATION_SUPPORT, rotation as i32);
+        if rotation != 0 {
+            ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_ROTATION_DEFAULT, ffi::libinput_device_config_rotation_get_default_angle(self.libinput) as u32);
+            ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_ROTATION_CURRENT, ffi::libinput_device_config_rotation_get_angle(self.libinput) as u32);
+        }
+
+        if version >= 2 {
+            ffi::wl_resource_post_event(resource, ffi::RIVER_LIBINPUT_DEVICE_V1_DONE);
+        }
+    }
+}
+
+unsafe extern "C" fn handle_device_object_destroy(resource: *mut ffi::wl_resource) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut LibinputDeviceObject;
+    if !obj.is_null() {
+        crate::server::wl_list_remove(&mut (*obj).link as *mut ffi::wl_list as *mut WlList);
+        let _ = Box::from_raw(obj);
+    }
+}
+
+static LIBINPUT_DEVICE_INTERFACE: ffi::river_libinput_device_v1_interface = ffi::river_libinput_device_v1_interface {
+    destroy: Some(device_destroy),
+    set_send_events: Some(device_set_send_events),
+    set_tap: Some(device_set_tap),
+    set_tap_button_map: Some(device_set_tap_button_map),
+    set_drag: Some(device_set_drag),
+    set_drag_lock: Some(device_set_drag_lock),
+    set_three_finger_drag: Some(device_set_three_finger_drag),
+    set_calibration_matrix: Some(device_set_calibration_matrix),
+    set_accel_profile: Some(device_set_accel_profile),
+    set_accel_speed: Some(device_set_accel_speed),
+    apply_accel_config: Some(device_apply_accel_config),
+    set_natural_scroll: Some(device_set_natural_scroll),
+    set_left_handed: Some(device_set_left_handed),
+    set_click_method: Some(device_set_click_method),
+    set_clickfinger_button_map: Some(device_set_clickfinger_button_map),
+    set_middle_emulation: Some(device_set_middle_emulation),
+    set_scroll_method: Some(device_set_scroll_method),
+    set_scroll_button: Some(device_set_scroll_button),
+    set_scroll_button_lock: Some(device_set_scroll_button_lock),
+    set_dwt: Some(device_set_dwt),
+    set_dwtp: Some(device_set_dwtp),
+    set_rotation: Some(device_set_rotation),
+};
+
+unsafe fn make_result(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    result_id: u32,
+    status: ffi::libinput_config_status,
+) -> bool {
+    let result_res = ffi::wl_resource_create(
+        client,
+        &ffi::river_libinput_result_v1_interface,
+        ffi::wl_resource_get_version(resource),
+        result_id,
+    );
+    if result_res.is_null() {
+        ffi::wl_client_post_no_memory(client);
+        return false;
+    }
+
+    match status {
+        ffi::libinput_config_status_LIBINPUT_CONFIG_STATUS_SUCCESS => {
+            ffi::wl_resource_post_event(result_res, 0); // success
+        }
+        ffi::libinput_config_status_LIBINPUT_CONFIG_STATUS_UNSUPPORTED => {
+            ffi::wl_resource_post_event(result_res, 1); // unsupported
+        }
+        _ => {
+            ffi::wl_resource_post_event(result_res, 2); // invalid
+        }
+    }
+    ffi::wl_resource_destroy(result_res);
+    status == ffi::libinput_config_status_LIBINPUT_CONFIG_STATUS_SUCCESS
+}
+
+unsafe fn broadcast_update(dev: *mut LibinputDevice, opcode: u32, arg: u32) {
+    let objects_head = &mut (*dev).objects as *mut ffi::wl_list as *mut WlList;
+    let mut curr = (*objects_head).next;
+    while curr != objects_head {
+        let next = (*curr).next;
+        let obj = crate::container_of!(curr, LibinputDeviceObject, link);
+        ffi::wl_resource_post_event((*obj).resource, opcode, arg);
+        if ffi::wl_resource_get_version((*obj).resource) >= 2 {
+            ffi::wl_resource_post_event((*obj).resource, ffi::RIVER_LIBINPUT_DEVICE_V1_DONE);
+        }
+        curr = next;
+    }
+}
+
+unsafe extern "C" fn device_destroy(_client: *mut ffi::wl_client, resource: *mut ffi::wl_resource) {
+    ffi::wl_resource_destroy(resource);
+}
+
+unsafe extern "C" fn device_set_send_events(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    result_id: u32,
+    mode: u32,
+) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut LibinputDeviceObject;
+    if obj.is_null() { return; }
+    let dev = (*obj).device;
+
+    let status = ffi::libinput_device_config_send_events_set_mode((*dev).libinput, mode);
+    if make_result(client, resource, result_id, status) {
+        let current = ffi::libinput_device_config_send_events_get_mode((*dev).libinput);
+        broadcast_update(dev, ffi::RIVER_LIBINPUT_DEVICE_V1_SEND_EVENTS_CURRENT, current);
+    }
+}
+
+unsafe extern "C" fn device_set_tap(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    result_id: u32,
+    state: u32,
+) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut LibinputDeviceObject;
+    if obj.is_null() { return; }
+    let dev = (*obj).device;
+
+    let status = ffi::libinput_device_config_tap_set_enabled((*dev).libinput, state);
+    if make_result(client, resource, result_id, status) {
+        let current = ffi::libinput_device_config_tap_get_enabled((*dev).libinput);
+        broadcast_update(dev, ffi::RIVER_LIBINPUT_DEVICE_V1_TAP_CURRENT, current);
+    }
+}
+
+unsafe extern "C" fn device_set_tap_button_map(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    result_id: u32,
+    button_map: u32,
+) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut LibinputDeviceObject;
+    if obj.is_null() { return; }
+    let dev = (*obj).device;
+
+    let status = ffi::libinput_device_config_tap_set_button_map((*dev).libinput, button_map);
+    if make_result(client, resource, result_id, status) {
+        let current = ffi::libinput_device_config_tap_get_button_map((*dev).libinput);
+        broadcast_update(dev, ffi::RIVER_LIBINPUT_DEVICE_V1_TAP_BUTTON_MAP_CURRENT, current);
+    }
+}
+
+unsafe extern "C" fn device_set_drag(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    result_id: u32,
+    state: u32,
+) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut LibinputDeviceObject;
+    if obj.is_null() { return; }
+    let dev = (*obj).device;
+
+    let status = ffi::libinput_device_config_tap_set_drag_enabled((*dev).libinput, state);
+    if make_result(client, resource, result_id, status) {
+        let current = ffi::libinput_device_config_tap_get_drag_enabled((*dev).libinput);
+        broadcast_update(dev, ffi::RIVER_LIBINPUT_DEVICE_V1_DRAG_CURRENT, current);
+    }
+}
+
+unsafe extern "C" fn device_set_drag_lock(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    result_id: u32,
+    state: u32,
+) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut LibinputDeviceObject;
+    if obj.is_null() { return; }
+    let dev = (*obj).device;
+
+    let status = ffi::libinput_device_config_tap_set_drag_lock_enabled((*dev).libinput, state);
+    if make_result(client, resource, result_id, status) {
+        let current = ffi::libinput_device_config_tap_get_drag_lock_enabled((*dev).libinput);
+        broadcast_update(dev, ffi::RIVER_LIBINPUT_DEVICE_V1_DRAG_LOCK_CURRENT, current);
+    }
+}
+
+unsafe extern "C" fn device_set_three_finger_drag(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    result_id: u32,
+    state: u32,
+) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut LibinputDeviceObject;
+    if obj.is_null() { return; }
+    let dev = (*obj).device;
+
+    let status = ffi::libinput_device_config_3fg_drag_set_enabled((*dev).libinput, state);
+    if make_result(client, resource, result_id, status) {
+        let current = ffi::libinput_device_config_3fg_drag_get_enabled((*dev).libinput);
+        broadcast_update(dev, ffi::RIVER_LIBINPUT_DEVICE_V1_THREE_FINGER_DRAG_CURRENT, current);
+    }
+}
+
+unsafe extern "C" fn device_set_calibration_matrix(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    result_id: u32,
+    matrix_arr: *mut ffi::wl_array,
+) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut LibinputDeviceObject;
+    if obj.is_null() { return; }
+    let dev = (*obj).device;
+
+    if (*matrix_arr).size != std::mem::size_of::<[f32; 6]>() {
+        ffi::wl_resource_post_error(
+            resource,
+            ffi::river_libinput_device_v1_error_RIVER_LIBINPUT_DEVICE_V1_ERROR_INVALID_ARG,
+            b"invalid calibration matrix\0".as_ptr() as *const _,
+        );
+        return;
+    }
+
+    let matrix_ptr = (*matrix_arr).data as *const f32;
+    let status = ffi::libinput_device_config_calibration_set_matrix((*dev).libinput, matrix_ptr);
+    if make_result(client, resource, result_id, status) {
+        let mut current = [0.0f32; 6];
+        ffi::libinput_device_config_calibration_get_matrix((*dev).libinput, current.as_mut_ptr());
+        let mut arr = ffi::wl_array {
+            size: std::mem::size_of_val(&current),
+            alloc: std::mem::size_of_val(&current),
+            data: current.as_mut_ptr() as *mut _,
+        };
+
+        let objects_head = &mut (*dev).objects as *mut ffi::wl_list as *mut WlList;
+        let mut curr = (*objects_head).next;
+        while curr != objects_head {
+            let next = (*curr).next;
+            let dest_obj = crate::container_of!(curr, LibinputDeviceObject, link);
+            ffi::wl_resource_post_event((*dest_obj).resource, ffi::RIVER_LIBINPUT_DEVICE_V1_CALIBRATION_MATRIX_CURRENT, &mut arr as *mut _);
+            if ffi::wl_resource_get_version((*dest_obj).resource) >= 2 {
+                ffi::wl_resource_post_event((*dest_obj).resource, ffi::RIVER_LIBINPUT_DEVICE_V1_DONE);
+            }
+            curr = next;
+        }
+    }
+}
+
+unsafe extern "C" fn device_set_accel_profile(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    result_id: u32,
+    profile: u32,
+) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut LibinputDeviceObject;
+    if obj.is_null() { return; }
+    let dev = (*obj).device;
+
+    let status = ffi::libinput_device_config_accel_set_profile((*dev).libinput, profile);
+    if make_result(client, resource, result_id, status) {
+        let current = ffi::libinput_device_config_accel_get_profile((*dev).libinput);
+        broadcast_update(dev, ffi::RIVER_LIBINPUT_DEVICE_V1_ACCEL_PROFILE_CURRENT, current);
+    }
+}
+
+unsafe extern "C" fn device_set_accel_speed(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    result_id: u32,
+    speed_arr: *mut ffi::wl_array,
+) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut LibinputDeviceObject;
+    if obj.is_null() { return; }
+    let dev = (*obj).device;
+
+    if (*speed_arr).size != std::mem::size_of::<f64>() {
+        ffi::wl_resource_post_error(
+            resource,
+            ffi::river_libinput_device_v1_error_RIVER_LIBINPUT_DEVICE_V1_ERROR_INVALID_ARG,
+            b"invalid accel speed\0".as_ptr() as *const _,
+        );
+        return;
+    }
+
+    let speed = *((*speed_arr).data as *const f64);
+    let status = ffi::libinput_device_config_accel_set_speed((*dev).libinput, speed);
+    if make_result(client, resource, result_id, status) {
+        let current = ffi::libinput_device_config_accel_get_speed((*dev).libinput);
+        let mut arr = ffi::wl_array {
+            size: std::mem::size_of::<f64>(),
+            alloc: std::mem::size_of::<f64>(),
+            data: &current as *const f64 as *mut _,
+        };
+
+        let objects_head = &mut (*dev).objects as *mut ffi::wl_list as *mut WlList;
+        let mut curr = (*objects_head).next;
+        while curr != objects_head {
+            let next = (*curr).next;
+            let dest_obj = crate::container_of!(curr, LibinputDeviceObject, link);
+            ffi::wl_resource_post_event((*dest_obj).resource, ffi::RIVER_LIBINPUT_DEVICE_V1_ACCEL_SPEED_CURRENT, &mut arr as *mut _);
+            if ffi::wl_resource_get_version((*dest_obj).resource) >= 2 {
+                ffi::wl_resource_post_event((*dest_obj).resource, ffi::RIVER_LIBINPUT_DEVICE_V1_DONE);
+            }
+            curr = next;
+        }
+    }
+}
+
+unsafe extern "C" fn device_apply_accel_config(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    result_id: u32,
+    config_res: *mut ffi::wl_resource,
+) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut LibinputDeviceObject;
+    if obj.is_null() { return; }
+    let dev = (*obj).device;
+
+    let accel_config = ffi::wl_resource_get_user_data(config_res) as *mut crate::libinput_accel_config::LibinputAccelConfig;
+    let config = if !accel_config.is_null() {
+        (*accel_config).libinput
+    } else {
+        std::ptr::null_mut()
+    };
+
+    let result_res = ffi::wl_resource_create(
+        client,
+        &ffi::river_libinput_result_v1_interface,
+        ffi::wl_resource_get_version(resource),
+        result_id,
+    );
+    if result_res.is_null() {
+        ffi::wl_client_post_no_memory(client);
+        return;
+    }
+
+    if config.is_null() {
+        ffi::wl_resource_post_event(result_res, 2); // invalid
+        ffi::wl_resource_destroy(result_res);
+        return;
+    }
+
+    let status = ffi::libinput_device_config_accel_apply((*dev).libinput, config);
+
+    let success = match status {
+        ffi::libinput_config_status_LIBINPUT_CONFIG_STATUS_SUCCESS => {
+            ffi::wl_resource_post_event(result_res, 0); // success
+            true
+        }
+        ffi::libinput_config_status_LIBINPUT_CONFIG_STATUS_UNSUPPORTED => {
+            ffi::wl_resource_post_event(result_res, 1); // unsupported
+            false
+        }
+        _ => {
+            ffi::wl_resource_post_event(result_res, 2); // invalid
+            false
+        }
+    };
+
+    ffi::wl_resource_destroy(result_res);
+
+    if success {
+        let current = ffi::libinput_device_config_accel_get_profile((*dev).libinput);
+        broadcast_update(dev, ffi::RIVER_LIBINPUT_DEVICE_V1_ACCEL_PROFILE_CURRENT, current);
+    }
+}
+
+unsafe extern "C" fn device_set_natural_scroll(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    result_id: u32,
+    state: u32,
+) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut LibinputDeviceObject;
+    if obj.is_null() { return; }
+    let dev = (*obj).device;
+
+    let status = ffi::libinput_device_config_scroll_set_natural_scroll_enabled((*dev).libinput, state as i32);
+    if make_result(client, resource, result_id, status) {
+        let current = ffi::libinput_device_config_scroll_get_natural_scroll_enabled((*dev).libinput);
+        broadcast_update(dev, ffi::RIVER_LIBINPUT_DEVICE_V1_NATURAL_SCROLL_CURRENT, current as u32);
+    }
+}
+
+unsafe extern "C" fn device_set_left_handed(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    result_id: u32,
+    state: u32,
+) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut LibinputDeviceObject;
+    if obj.is_null() { return; }
+    let dev = (*obj).device;
+
+    let status = ffi::libinput_device_config_left_handed_set((*dev).libinput, state as i32);
+    if make_result(client, resource, result_id, status) {
+        let current = ffi::libinput_device_config_left_handed_get((*dev).libinput);
+        broadcast_update(dev, ffi::RIVER_LIBINPUT_DEVICE_V1_LEFT_HANDED_CURRENT, current as u32);
+    }
+}
+
+unsafe extern "C" fn device_set_click_method(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    result_id: u32,
+    method: u32,
+) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut LibinputDeviceObject;
+    if obj.is_null() { return; }
+    let dev = (*obj).device;
+
+    let status = ffi::libinput_device_config_click_set_method((*dev).libinput, method);
+    if make_result(client, resource, result_id, status) {
+        let current = ffi::libinput_device_config_click_get_method((*dev).libinput);
+        broadcast_update(dev, ffi::RIVER_LIBINPUT_DEVICE_V1_CLICK_METHOD_CURRENT, current);
+    }
+}
+
+unsafe extern "C" fn device_set_clickfinger_button_map(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    result_id: u32,
+    button_map: u32,
+) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut LibinputDeviceObject;
+    if obj.is_null() { return; }
+    let dev = (*obj).device;
+
+    let status = ffi::libinput_device_config_click_set_clickfinger_button_map((*dev).libinput, button_map);
+    if make_result(client, resource, result_id, status) {
+        let current = ffi::libinput_device_config_click_get_clickfinger_button_map((*dev).libinput);
+        broadcast_update(dev, ffi::RIVER_LIBINPUT_DEVICE_V1_CLICKFINGER_BUTTON_MAP_CURRENT, current);
+    }
+}
+
+unsafe extern "C" fn device_set_middle_emulation(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    result_id: u32,
+    state: u32,
+) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut LibinputDeviceObject;
+    if obj.is_null() { return; }
+    let dev = (*obj).device;
+
+    let status = ffi::libinput_device_config_middle_emulation_set_enabled((*dev).libinput, state);
+    if make_result(client, resource, result_id, status) {
+        let current = ffi::libinput_device_config_middle_emulation_get_enabled((*dev).libinput);
+        broadcast_update(dev, ffi::RIVER_LIBINPUT_DEVICE_V1_MIDDLE_EMULATION_CURRENT, current);
+    }
+}
+
+unsafe extern "C" fn device_set_scroll_method(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    result_id: u32,
+    method: u32,
+) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut LibinputDeviceObject;
+    if obj.is_null() { return; }
+    let dev = (*obj).device;
+
+    let status = ffi::libinput_device_config_scroll_set_method((*dev).libinput, method);
+    if make_result(client, resource, result_id, status) {
+        let current = ffi::libinput_device_config_scroll_get_method((*dev).libinput);
+        broadcast_update(dev, ffi::RIVER_LIBINPUT_DEVICE_V1_SCROLL_METHOD_CURRENT, current);
+    }
+}
+
+unsafe extern "C" fn device_set_scroll_button(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    result_id: u32,
+    button: u32,
+) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut LibinputDeviceObject;
+    if obj.is_null() { return; }
+    let dev = (*obj).device;
+
+    let status = ffi::libinput_device_config_scroll_set_button((*dev).libinput, button);
+    if make_result(client, resource, result_id, status) {
+        let current = ffi::libinput_device_config_scroll_get_button((*dev).libinput);
+        broadcast_update(dev, ffi::RIVER_LIBINPUT_DEVICE_V1_SCROLL_BUTTON_CURRENT, current);
+    }
+}
+
+unsafe extern "C" fn device_set_scroll_button_lock(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    result_id: u32,
+    state: u32,
+) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut LibinputDeviceObject;
+    if obj.is_null() { return; }
+    let dev = (*obj).device;
+
+    let status = ffi::libinput_device_config_scroll_set_button_lock((*dev).libinput, state);
+    if make_result(client, resource, result_id, status) {
+        let current = ffi::libinput_device_config_scroll_get_button_lock((*dev).libinput);
+        broadcast_update(dev, ffi::RIVER_LIBINPUT_DEVICE_V1_SCROLL_BUTTON_LOCK_CURRENT, current);
+    }
+}
+
+unsafe extern "C" fn device_set_dwt(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    result_id: u32,
+    state: u32,
+) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut LibinputDeviceObject;
+    if obj.is_null() { return; }
+    let dev = (*obj).device;
+
+    let status = ffi::libinput_device_config_dwt_set_enabled((*dev).libinput, state);
+    if make_result(client, resource, result_id, status) {
+        let current = ffi::libinput_device_config_dwt_get_enabled((*dev).libinput);
+        broadcast_update(dev, ffi::RIVER_LIBINPUT_DEVICE_V1_DWT_CURRENT, current as u32);
+    }
+}
+
+unsafe extern "C" fn device_set_dwtp(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    result_id: u32,
+    state: u32,
+) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut LibinputDeviceObject;
+    if obj.is_null() { return; }
+    let dev = (*obj).device;
+
+    let status = ffi::libinput_device_config_dwtp_set_enabled((*dev).libinput, state);
+    if make_result(client, resource, result_id, status) {
+        let current = ffi::libinput_device_config_dwtp_get_enabled((*dev).libinput);
+        broadcast_update(dev, ffi::RIVER_LIBINPUT_DEVICE_V1_DWTP_CURRENT, current as u32);
+    }
+}
+
+unsafe extern "C" fn device_set_rotation(
+    client: *mut ffi::wl_client,
+    resource: *mut ffi::wl_resource,
+    result_id: u32,
+    angle: u32,
+) {
+    let obj = ffi::wl_resource_get_user_data(resource) as *mut LibinputDeviceObject;
+    if obj.is_null() { return; }
+    let dev = (*obj).device;
+
+    let status = ffi::libinput_device_config_rotation_set_angle((*dev).libinput, angle);
+    if make_result(client, resource, result_id, status) {
+        let current = ffi::libinput_device_config_rotation_get_angle((*dev).libinput);
+        broadcast_update(dev, ffi::RIVER_LIBINPUT_DEVICE_V1_ROTATION_CURRENT, current);
+    }
+}
diff --git a/src/main.rs b/src/main.rs
index 780802a..58be2a4 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -15,6 +15,8 @@ mod output;
 mod output_manager;
 mod input_manager;
 mod libinput_config;
+pub mod libinput_device;
+pub mod libinput_accel_config;
 mod xkb_config;
 mod idle_inhibit_manager;
 mod lock_manager;
diff --git a/src/seat.rs b/src/seat.rs
index 8614c8c..63e179d 100644
--- a/src/seat.rs
+++ b/src/seat.rs
@@ -225,7 +225,7 @@ impl Seat {
         let dev_type = ffi::river_wlr_input_device_get_type((*device).wlr_device);
         match dev_type {
             ffi::wlr_input_device_type_WLR_INPUT_DEVICE_KEYBOARD => {
-                let keyboard = ffi::river_wlr_input_device_get_data((*device).wlr_device) as *mut crate::keyboard::Keyboard;
+                let keyboard = (*device).destroy_data as *mut crate::keyboard::Keyboard;
                 if !keyboard.is_null() {
                     (*keyboard).set_group();
                     if !(*keyboard).group.is_null() {
diff --git a/wrapper.h b/wrapper.h
index 2a030bd..35d94c7 100644
--- a/wrapper.h
+++ b/wrapper.h
@@ -97,6 +97,7 @@
 #include "river-libinput-config-v1-protocol.h"
 #include "river-xkb-config-v1-protocol.h"
 #include "virtual-keyboard-unstable-v1-protocol.h"
+#include <wlr/backend/libinput.h>
 
 // Custom FFI helpers defined in wlroots_log_wrapper.c
 struct wlr_surface *river_scene_node_get_surface(struct wlr_scene_node *node);