global-mousemove 0.1.1

A minimal library to listen for global mousemove events.
Documentation
use std::ffi::CString;
use std::os::raw::{c_char, c_int, c_short, c_uchar, c_uint, c_ushort};
use std::ptr::null;

use x11::{xlib, xrecord};

use crate::{GLOBAL_CALLBACK, ListenError, MouseMoveEvent};

/// The key/button/pointer event data structure.
///
/// This is the `.u.keyButtonPointer` union field of the `xEvent` struct. The
/// definition references the xproto crate:
///
/// - https://docs.rs/xproto/2.0.1/xproto/struct._xEvent.html
/// - https://docs.rs/xproto/2.0.1/xproto/struct._xEvent__bindgen_ty_1__bindgen_ty_2.html
#[repr(C)]
#[derive(Debug)]
struct XRecordDatum {
    xtype: c_uint, // pad00 in keyButtonPointer for type
    time: c_uint,
    root: c_uint,
    event: c_uint,
    child: c_uint,
    root_x: c_short,
    root_y: c_short,
    event_x: c_short,
    event_y: c_short,
    state: c_ushort,
    same_screen: c_uchar,
    pad1: c_uchar,
}

unsafe extern "C" fn raw_callback(
    _closure: *mut c_char,
    recorded_data: *mut xrecord::XRecordInterceptData,
) {
    let data = match unsafe { recorded_data.as_ref() } {
        Some(d) => d,
        None => return,
    };
    if data.category != xrecord::XRecordFromServer {
        unsafe { xrecord::XRecordFreeData(recorded_data) };
        return;
    }

    debug_assert!(data.data_len * 4 >= std::mem::size_of::<XRecordDatum>() as u64);

    let xdatum = unsafe { &*(data.data as *const XRecordDatum) };
    if xdatum.xtype as c_int == xlib::MotionNotify {
        let event = MouseMoveEvent {
            x: xdatum.root_x as f64,
            y: xdatum.root_y as f64,
        };
        GLOBAL_CALLBACK.with(|cell| {
            if let Some(callback) = cell.borrow_mut().as_mut() {
                callback(event);
            }
        });
    }

    unsafe { xrecord::XRecordFreeData(recorded_data) };
}

pub fn listen<T>(callback: T) -> Result<(), ListenError>
where
    T: FnMut(MouseMoveEvent) + 'static,
{
    GLOBAL_CALLBACK.with(|cell| {
        *cell.borrow_mut() = Some(Box::new(callback));
    });

    unsafe {
        let dpy = xlib::XOpenDisplay(null());
        if dpy.is_null() {
            return Err(ListenError::XOpenDisplay);
        }
        let ext_name = CString::new("RECORD").unwrap();
        if xlib::XInitExtension(dpy, ext_name.as_ptr()).is_null() {
            return Err(ListenError::XRecordExtensionMissing);
        }

        let range = xrecord::XRecordAllocRange();
        if range.is_null() {
            return Err(ListenError::XRecordAllocRange);
        }
        (*range).device_events.first = xlib::MotionNotify as c_uchar;
        (*range).device_events.last = xlib::MotionNotify as c_uchar;

        let mut all_clients = xrecord::XRecordAllClients;
        let mut range_ptr: *mut xrecord::XRecordRange = range;
        let context = xrecord::XRecordCreateContext(dpy, 0, &mut all_clients, 1, &mut range_ptr, 1);
        if context == 0 {
            return Err(ListenError::XRecordCreateContext);
        }

        xlib::XSync(dpy, 0);
        let ok = xrecord::XRecordEnableContext(dpy, context, Some(raw_callback), &mut 0);
        if ok == 0 {
            return Err(ListenError::XRecordEnableContext);
        }
    }

    Ok(())
}