libinput-rs 0.3.4

100% drop-in Rust replacement for the libinput 1.31.3 C ABI and tools
Documentation
use crate::evdev::{AbsoluteAxisCode, Device};
use std::ffi::{CStr, CString};
use std::os::fd::AsRawFd;
use std::path::Path;

#[link(name = "udev")]
extern "C" {
    fn udev_new() -> *mut libc::c_void;
    fn udev_unref(udev: *mut libc::c_void) -> *mut libc::c_void;
    fn udev_device_new_from_syspath(
        udev: *mut libc::c_void,
        syspath: *const libc::c_char,
    ) -> *mut libc::c_void;
    fn udev_device_unref(device: *mut libc::c_void) -> *mut libc::c_void;
    fn udev_device_get_devnode(device: *mut libc::c_void) -> *const libc::c_char;
    fn udev_device_get_parent(device: *mut libc::c_void) -> *mut libc::c_void;
    fn udev_device_get_property_value(
        device: *mut libc::c_void,
        key: *const libc::c_char,
    ) -> *const libc::c_char;
    fn udev_device_get_sysattr_value(
        device: *mut libc::c_void,
        key: *const libc::c_char,
    ) -> *const libc::c_char;
}

pub const FUZZ_AXES: [AbsoluteAxisCode; 4] = [
    AbsoluteAxisCode::ABS_X,
    AbsoluteAxisCode::ABS_Y,
    AbsoluteAxisCode::ABS_MT_POSITION_X,
    AbsoluteAxisCode::ABS_MT_POSITION_Y,
];

pub struct DeviceHandle {
    udev: *mut libc::c_void,
    device: *mut libc::c_void,
}

impl DeviceHandle {
    pub fn from_syspath(path: &Path) -> Option<Self> {
        let path = CString::new(path.as_os_str().as_encoded_bytes()).ok()?;
        unsafe {
            let udev = udev_new();
            if udev.is_null() {
                return None;
            }
            let device = udev_device_new_from_syspath(udev, path.as_ptr());
            if device.is_null() {
                udev_unref(udev);
                return None;
            }
            Some(Self { udev, device })
        }
    }

    pub fn devnode(&self) -> Option<String> {
        unsafe { string_from_ptr(udev_device_get_devnode(self.device)) }
    }

    pub fn property(&self, key: &str) -> Option<String> {
        let key = CString::new(key).ok()?;
        unsafe { string_from_ptr(udev_device_get_property_value(self.device, key.as_ptr())) }
    }

    pub fn first_parent_value(&self, key: &str) -> Option<(String, Option<String>)> {
        let key = CString::new(key).ok()?;
        let product = CString::new("PRODUCT").ok()?;
        unsafe {
            let mut device = self.device;
            while !device.is_null() {
                if let Some(value) =
                    string_from_ptr(udev_device_get_sysattr_value(device, key.as_ptr()))
                {
                    let product =
                        string_from_ptr(udev_device_get_property_value(device, product.as_ptr()));
                    return Some((value, product));
                }
                device = udev_device_get_parent(device);
            }
        }
        None
    }
}

impl Drop for DeviceHandle {
    fn drop(&mut self) {
        unsafe {
            udev_device_unref(self.device);
            udev_unref(self.udev);
        }
    }
}

unsafe fn string_from_ptr(value: *const libc::c_char) -> Option<String> {
    (!value.is_null()).then(|| CStr::from_ptr(value).to_string_lossy().into_owned())
}

pub fn set_abs_fuzz(device: &Device, axis: AbsoluteAxisCode, fuzz: i32) -> bool {
    let Ok(absinfo) = device.get_absinfo() else {
        return false;
    };
    let Some((_, info)) = absinfo
        .into_iter()
        .find(|(candidate, _)| *candidate == axis)
    else {
        return true;
    };
    if info.fuzz() == fuzz {
        return true;
    }

    let replacement = crate::evdev::AbsInfo::new(
        info.value(),
        info.minimum(),
        info.maximum(),
        fuzz,
        info.flat(),
        info.resolution(),
    );
    let request = (1_u64 << 30)
        | ((std::mem::size_of::<crate::evdev::AbsInfo>() as u64) << 16)
        | (u64::from(b'E') << 8)
        | u64::from(0xc0_u16 + axis.0);
    unsafe { libc::ioctl(device.as_raw_fd(), request as libc::c_ulong, &replacement) >= 0 }
}

pub fn parse_evdev_abs_fuzz(value: &str) -> Option<i32> {
    let field = value.split(':').nth(3)?;
    if field.is_empty() {
        return None;
    }
    field.parse().ok()
}

#[cfg(test)]
mod tests {
    use super::parse_evdev_abs_fuzz;

    #[test]
    fn extracts_the_fourth_evdev_abs_field() {
        assert_eq!(parse_evdev_abs_fuzz("0:100:20:7"), Some(7));
        assert_eq!(parse_evdev_abs_fuzz("::20:12"), Some(12));
        assert_eq!(parse_evdev_abs_fuzz("0:100:20:"), None);
    }
}