clinuxiodriverwayland

Is there a way to set absolute cursor position for uinput virtual device (Wayland)?


I followed the code example on the linux uinput documentation in order to simulate a virtual pointing device. I have the following lines of code which can successfully move the cursor down and to the right.

#include <linux/uinput.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

void emit(int fd, int type, int code, int val)
{
   struct input_event ie;
   struct input_event ie;

   ie.type = type;
   ie.code = code;
   ie.value = val;
   /* timestamp values below are ignored */
   ie.time.tv_sec = 0;
   ie.time.tv_usec = 0;

   write(fd, &ie, sizeof(ie));
}
/* emit function is identical to of the first example */

void emit(int fd, int type, int code, int val)
{
   struct input_event ie;

   ie.type = type;
   ie.code = code;
   ie.value = val;
   /* timestamp values below are ignored */
   ie.time.tv_sec = 0;
   ie.time.tv_usec = 0;

   write(fd, &ie, sizeof(ie));
}

int main(void)
{
   struct uinput_setup usetup;
   int i = 50;

   int fd = open("/dev/uinput", O_WRONLY | O_NONBLOCK);

   /* enable mouse button left and relative events */
   ioctl(fd, UI_SET_EVBIT, EV_KEY);
   ioctl(fd, UI_SET_KEYBIT, BTN_LEFT);

   ioctl(fd, UI_SET_EVBIT, EV_REL);
   ioctl(fd, UI_SET_RELBIT, REL_X);
   ioctl(fd, UI_SET_RELBIT, REL_Y);

   memset(&usetup, 0, sizeof(usetup));
   usetup.id.bustype = BUS_USB;
   usetup.id.vendor = 0x1234; /* sample vendor */
   usetup.id.product = 0x5678; /* sample product */
   strcpy(usetup.name, "Example device");

   ioctl(fd, UI_DEV_SETUP, &usetup);
   ioctl(fd, UI_DEV_CREATE);

   /*
    * On UI_DEV_CREATE the kernel will create the device node for this
    * device. We are inserting a pause here so that userspace has time
    * to detect, initialize the new device, and can start listening to
    * the event, otherwise it will not notice the event we are about
    * to send. This pause is only needed in our example code!
    */
   sleep(1);

   /* Move the mouse diagonally, 5 units per axis */
   while (i--) {
      emit(fd, EV_REL, REL_X, 5);
      emit(fd, EV_REL, REL_Y, 5);
      emit(fd, EV_SYN, SYN_REPORT, 0);
      usleep(15000);
   }

   /*
    * Give userspace some time to read the events before we destroy the
    * device with UI_DEV_DESTROY.
    */
   sleep(1);

   ioctl(fd, UI_DEV_DESTROY);
   close(fd);

   return 0;
}

Now, I would like to set the cursor's position absolutely to x=500,y=500 on the screen, for example.

Changing the instances of "REL" to "ABS" in the code doesn't seem to have done the trick (still moved relatively). Since I couldn't find the documentation for this capability, I was wondering where to find it, or if it is possible to set absolute cursor position with uinput on Wayland (Gnome mutter), or if there is something else I can use.

I have seen that behavior (setting absolute position of the cursor) in "libinput replay", and am wondering if I can replicate it. Cheers.


Solution

  • For absolute movement, the ioctl requests to use are

    ioctl(fd, UI_SET_EVBIT, EV_ABS)
    

    and (one or more)

    ioctl(fd, UI_ABS_SETUP, &abs)
    

    where abs is of type struct uinput_abs_setup (wraps struct input_absinfo).

    Here is a modified example, that should work.

    #include <errno.h>
    #include <fcntl.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    
    #include <linux/uinput.h>
    #include <sys/ioctl.h>
    
    static void fatal(const char *msg)
    {
        fprintf(stderr, "fatal: ");
    
        if (errno)
            perror(msg);
        else
            fprintf(stderr, "%s\n", msg);
    
        exit(EXIT_FAILURE);
    }
    
    static void setup_abs(int fd, int type, int min, int max, int res)
    {
        struct uinput_abs_setup abs = {
            .code = type,
            .absinfo = {
                .minimum = min,
                .maximum = max,
                .resolution = res
            }
        };
    
        if (-1 == ioctl(fd, UI_ABS_SETUP, &abs))
            fatal("ioctl UI_ABS_SETUP");
    }
    
    static void init(int fd, int width, int height, int dpi)
    {
        if (-1 == ioctl(fd, UI_SET_EVBIT, EV_SYN))
            fatal("ioctl UI_SET_EVBIT EV_SYN");
    
        if (-1 == ioctl(fd, UI_SET_EVBIT, EV_KEY))
            fatal("ioctl UI_SET_EVBIT EV_KEY");
        if (-1 == ioctl(fd, UI_SET_KEYBIT, BTN_LEFT))
            fatal("ioctl UI_SET_KEYBIT BTN_LEFT");
    
        if (-1 == ioctl(fd, UI_SET_EVBIT, EV_ABS))
            fatal("ioctl UI_SET_EVBIT EV_ABS");
        /* the ioctl UI_ABS_SETUP enables these automatically, when appropriate:
            ioctl(fd, UI_SET_ABSBIT, ABS_X);
            ioctl(fd, UI_SET_ABSBIT, ABS_Y);
        */
    
        struct uinput_setup device = {
            .id = {
                .bustype = BUS_USB
            },
            .name = "Emulated Absolute Positioning Device"
        };
    
        if (-1 == ioctl(fd, UI_DEV_SETUP, &device))
            fatal("ioctl UI_DEV_SETUP");
    
        setup_abs(fd, ABS_X, 0, width, dpi);
        setup_abs(fd, ABS_Y, 0, height, dpi);
    
        if (-1 == ioctl(fd, UI_DEV_CREATE))
            fatal("ioctl UI_DEV_CREATE");
    
        /* give time for device creation */
        sleep(1);
    }
    
    static void emit(int fd, int type, int code, int value)
    {
        struct input_event ie = {
            .type = type,
            .code = code,
            .value = value
        };
    
        write(fd, &ie, sizeof ie);
    }
    
    int main(int argc, char **argv)
    {
        /* These values are very device specific */
        int w = argc > 1 ? atoi(argv[1]) : 1920;
        int h = argc > 2 ? atoi(argv[2]) : 1080;
        int d = argc > 3 ? atoi(argv[3]) : 96;
    
        if (w < 1 || h < 1 || d < 1)
            fatal("Bad initial value(s).");
    
        int fd = open("/dev/uinput", O_WRONLY | O_NONBLOCK);
    
        if (-1 == fd)
            fatal("open");
    
        printf("Initializing device screen map as %dx%d @ %ddpi\n", w, h, d);
    
        init(fd, w, h, d);
    
        while (1) {
            printf("Enter x & y: ");
            fflush(stdout);
    
            char input[128];
            if (!fgets(input, sizeof input, stdin) || 0 == strncmp(".exit", input, 5))
                break;
    
            int x, y;
            if (2 != sscanf(input, "%d%d", &x, &y) || x < 0 || y < 0) {
                fprintf(stderr, "Invalid input.\n");
                continue;
            }
    
            printf("Moving cursor to %d,%d\n", x, y);
    
            /* input is zero-based, but event positions are one-based */
            emit(fd, EV_ABS, ABS_X, 1 + x);
            emit(fd, EV_ABS, ABS_Y, 1 + y);
            emit(fd, EV_SYN, SYN_REPORT, 0);
        }
    
        puts("Cleaning up...");
    
        /* give time for events to finish */
        sleep(1);
    
        if (-1 == ioctl(fd, UI_DEV_DESTROY))
            fatal("ioctl UI_DEV_DESTROY");
    
        close(fd);
        puts("Goodbye.");
    }
    
    ./a.out 4640 1080 96
    Initializing device screen map as 4640x1080 @ 96dpi
    Enter x & y: 10 10
    Moving cursor to 10,10
    Enter x & y: 123 321
    Moving cursor to 123,321
    Enter x & y: .exit
    Cleaning up...
    Goodbye
    

    If the above does not work, one uinput work-around (hack) that I have come across is to relatively move the mouse an extremely negative amount in both axes, which should place the cursor at 0,0. Then you make a second relative movement to the intended position. The problem here is that mouse acceleration can influence the final position.

    This is the same hack used by ydotool.

    void move_cursor_to_xy(int fd, int x, int y)
    {
        emit(fd, EV_REL, REL_X, INT_MIN);
        emit(fd, EV_REL, REL_Y, INT_MIN);
        emit(fd, EV_SYN, SYN_REPORT, 0);
    
        emit(fd, EV_REL, REL_X, x);
        emit(fd, EV_REL, REL_Y, y);
        emit(fd, EV_SYN, SYN_REPORT, 0);
    }
    

    See also: Simulating mouse and keyboard input on Wayland and X11

    (Note: Under X11, there is XWarpPointer, but Wayland does not appear to support directly controlling the mouse from user code (potential security risk?).)