Nimbin2git.christianimmanuel.de / Linux & System / Linux-disable-Middle-Mouse-Click / middle_button_disable.c

Linux-disable-Middle-Mouse-Click git · main

git clone https://git.christianimmanuel.de/linux-system/Linux-disable-Middle-Mouse-Click.gitwget https://git.christianimmanuel.de/linux-system/Linux-disable-Middle-Mouse-Click/archive/Linux-disable-Middle-Mouse-Click.tar.gz
middle_button_disable.c 24.3 KB · 782 lines raw
/*=====================================================================
 *  middle_button_disable.c
 *
 *  Disables the TrackPoint middle button (no BTN_MIDDLE ever reaches
 *  userspace, so no primary-selection paste) while keeping middle-button
 *  scrolling alive.
 *
 *  How:
 *    - Grab the real TrackPoint device (EVIOCGRAB) and re-emit its events
 *      through a uinput clone.
 *    - BTN_MIDDLE is swallowed and only tracked internally.
 *    - While the physical middle button is held, REL_X/REL_Y motion is
 *      translated into wheel events instead of pointer motion.
 *
 *  Wheel events are emitted as high-resolution (REL_WHEEL_HI_RES,
 *  120 units = one detent) *and* legacy (REL_WHEEL) events. libinput
 *  ignores legacy events on devices that advertise hi-res axes, so
 *  emitting only REL_WHEEL results in no scrolling at all.
 *
 *  Build:
 *      gcc -Wall -Wextra -O2 -D_GNU_SOURCE \
 *          -o middle_button_disable middle_button_disable.c -ludev
 *
 *  Run (needs access to /dev/input/event* and /dev/uinput):
 *      sudo ./middle_button_disable --auto
 *====================================================================*/

#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif

#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <limits.h>
#include <linux/input.h>
#include <linux/uinput.h>
#include <poll.h>
#include <regex.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>

#include <libudev.h>

/* Older headers may lack the hi-res wheel codes. */
#ifndef REL_WHEEL_HI_RES
#define REL_WHEEL_HI_RES 0x0b
#endif
#ifndef REL_HWHEEL_HI_RES
#define REL_HWHEEL_HI_RES 0x0c
#endif

/* -----------------------------------------------------------------
 *  Constants
 * ----------------------------------------------------------------- */
#define PROG_VERSION        "1.1.0"
#define MAX_DEVICES         32
#define UINPUT_NAME_PREFIX  "middle-disable: "
#define HI_RES_PER_DETENT   120     /* kernel ABI: 120 units == one click */
#define DEFAULT_THRESHOLD   8       /* raw REL units per detent */

/* -----------------------------------------------------------------
 *  Runtime options
 * ----------------------------------------------------------------- */
struct options {
    int threshold;      /* raw REL units per wheel detent (higher = slower) */
    int natural;        /* invert scroll direction */
    int hscroll;        /* horizontal scrolling enabled */
    int pointing_stick; /* tag clone as INPUT_PROP_POINTING_STICK */
};

static struct options opts = {
    .threshold      = DEFAULT_THRESHOLD,
    .natural        = 0,
    .hscroll        = 1,
    .pointing_stick = 1,
};

/* -----------------------------------------------------------------
 *  Per-device state
 * ----------------------------------------------------------------- */
struct device {
    char *path;                 /* /dev/input/eventX */
    char *name;                 /* source device name */
    int   fd;                   /* real device */
    int   ufd;                  /* uinput clone */

    int   middle_down;          /* physical BTN_MIDDLE is held */
    int   native_wheel;         /* device emitted wheel itself while held */

    /* Scroll accumulators. sub_* holds raw motion scaled by
     * HI_RES_PER_DETENT; det_* holds hi-res units not yet converted
     * into a legacy detent. */
    long  sub_v, sub_h;
    long  det_v, det_h;
};

/* -----------------------------------------------------------------
 *  udev filters for --auto
 * ----------------------------------------------------------------- */
struct filters {
    const char *vendor_id;      /* ID_VENDOR_ID, checked only if present */
    const char *product_id;     /* ID_MODEL_ID,  checked only if present */
    int         have_regex;
    regex_t     regex;          /* matched against ID_MODEL */
};

/* -----------------------------------------------------------------
 *  Signals
 * ----------------------------------------------------------------- */
static volatile sig_atomic_t running = 1;
static void stop_handler(int sig) { (void)sig; running = 0; }

static inline int test_bit(unsigned int nr, const unsigned long *addr)
{
    return (addr[nr / (8 * sizeof(unsigned long))] >>
            (nr % (8 * sizeof(unsigned long)))) & 1;
}

/* -----------------------------------------------------------------
 *  uinput output
 * ----------------------------------------------------------------- */
static void emit_event(struct device *dev, const struct input_event *ev)
{
    ssize_t n = write(dev->ufd, ev, sizeof(*ev));
    (void)n;    /* uinput writes are all-or-nothing; nothing to retry */
}

static void emit_rel(struct device *dev, const struct input_event *src,
                     int code, int value)
{
    struct input_event ev = *src;
    ev.type  = EV_REL;
    ev.code  = code;
    ev.value = value;
    emit_event(dev, &ev);
}

/* -----------------------------------------------------------------
 *  Scroll conversion
 *
 *  raw   : motion delta on this axis, sign already normalised so that
 *          positive == wheel up / wheel right
 *  sub   : leftover raw motion, scaled by HI_RES_PER_DETENT
 *  det   : leftover hi-res units toward the next legacy detent
 * ----------------------------------------------------------------- */
static void scroll_axis(struct device *dev, const struct input_event *src,
                        int raw, long *sub, long *det,
                        int code_hi, int code_lo)
{
    *sub += (long)raw * HI_RES_PER_DETENT;

    long hi = *sub / opts.threshold;    /* truncates toward zero: symmetric */
    if (hi == 0)
        return;
    *sub -= hi * opts.threshold;

    emit_rel(dev, src, code_hi, (int)hi);

    *det += hi;
    long steps = *det / HI_RES_PER_DETENT;
    if (steps != 0) {
        *det -= steps * HI_RES_PER_DETENT;
        emit_rel(dev, src, code_lo, (int)steps);
    }
}

static void reset_scroll(struct device *dev)
{
    dev->sub_v = dev->sub_h = 0;
    dev->det_v = dev->det_h = 0;
    dev->native_wheel = 0;
}

/* -----------------------------------------------------------------
 *  Core event filter
 * ----------------------------------------------------------------- */
static void process_event(struct device *dev, const struct input_event *ev)
{
    /* Middle button: never forwarded, only tracked. */
    if (ev->type == EV_KEY && ev->code == BTN_MIDDLE) {
        if (ev->value == 1) {
            dev->middle_down = 1;
            reset_scroll(dev);
        } else if (ev->value == 0) {
            dev->middle_down = 0;
            reset_scroll(dev);
        }
        return;
    }

    if (!dev->middle_down) {
        emit_event(dev, ev);
        return;
    }

    /* ---- middle button held: scroll mode ---- */

    if (ev->type == EV_REL) {
        switch (ev->code) {
        case REL_WHEEL:
        case REL_HWHEEL:
        case REL_WHEEL_HI_RES:
        case REL_HWHEEL_HI_RES:
            /* Device scrolls on its own; don't emulate on top of it. */
            dev->native_wheel = 1;
            emit_event(dev, ev);
            return;

        case REL_Y:
            if (dev->native_wheel)
                return;
            /* REL_Y is positive downward, wheel up is positive. */
            scroll_axis(dev, ev,
                        opts.natural ? ev->value : -ev->value,
                        &dev->sub_v, &dev->det_v,
                        REL_WHEEL_HI_RES, REL_WHEEL);
            return;

        case REL_X:
            if (dev->native_wheel || !opts.hscroll)
                return;
            scroll_axis(dev, ev,
                        opts.natural ? -ev->value : ev->value,
                        &dev->sub_h, &dev->det_h,
                        REL_HWHEEL_HI_RES, REL_HWHEEL);
            return;

        default:
            return;     /* swallow other motion while scrolling */
        }
    }

    /* Buttons, SYN, everything else passes through unchanged. */
    emit_event(dev, ev);
}

/* -----------------------------------------------------------------
 *  uinput clone creation
 * ----------------------------------------------------------------- */
static int create_uinput(struct device *dev)
{
    struct input_id id;
    struct uinput_setup setup;

    dev->ufd = open("/dev/uinput", O_WRONLY);
    if (dev->ufd < 0) {
        perror("/dev/uinput");
        return -1;
    }

    if (ioctl(dev->ufd, UI_SET_EVBIT, EV_KEY) < 0 ||
        ioctl(dev->ufd, UI_SET_EVBIT, EV_REL) < 0 ||
        ioctl(dev->ufd, UI_SET_EVBIT, EV_SYN) < 0)
        goto fail;

    /* BTN_MIDDLE is declared but never emitted. It must be declared:
     * libinput enables middle-button emulation on any pointer without a
     * middle button, and that emulation buffers and re-times the left and
     * right button events, which breaks modifier+right-drag gestures. */
    if (ioctl(dev->ufd, UI_SET_KEYBIT, BTN_LEFT)   < 0 ||
        ioctl(dev->ufd, UI_SET_KEYBIT, BTN_RIGHT)  < 0 ||
        ioctl(dev->ufd, UI_SET_KEYBIT, BTN_MIDDLE) < 0)
        goto fail;

    if (ioctl(dev->ufd, UI_SET_RELBIT, REL_X)             < 0 ||
        ioctl(dev->ufd, UI_SET_RELBIT, REL_Y)             < 0 ||
        ioctl(dev->ufd, UI_SET_RELBIT, REL_WHEEL)         < 0 ||
        ioctl(dev->ufd, UI_SET_RELBIT, REL_HWHEEL)        < 0 ||
        ioctl(dev->ufd, UI_SET_RELBIT, REL_WHEEL_HI_RES)  < 0 ||
        ioctl(dev->ufd, UI_SET_RELBIT, REL_HWHEEL_HI_RES) < 0)
        goto fail;

    /* Advertise as a pointing stick so libinput applies TrackPoint
     * acceleration instead of generic mouse acceleration. */
#ifdef INPUT_PROP_POINTING_STICK
    if (opts.pointing_stick)
        ioctl(dev->ufd, UI_SET_PROPBIT, INPUT_PROP_POINTING_STICK);
#endif
#ifdef INPUT_PROP_POINTER
    ioctl(dev->ufd, UI_SET_PROPBIT, INPUT_PROP_POINTER);
#endif

    memset(&setup, 0, sizeof(setup));
    memset(&id, 0, sizeof(id));

    /* Inherit vendor/product from the source, but stay on BUS_VIRTUAL so
     * the clone lands under /sys/devices/virtual/ and our own hotplug
     * filter can exclude it. */
    if (ioctl(dev->fd, EVIOCGID, &id) == 0) {
        setup.id.vendor  = id.vendor;
        setup.id.product = id.product;
        setup.id.version = id.version;
    } else {
        setup.id.vendor  = 0xfeed;
        setup.id.product = 0x0001;
        setup.id.version = 1;
    }
    setup.id.bustype = BUS_VIRTUAL;

    snprintf(setup.name, UINPUT_MAX_NAME_SIZE, "%s%s",
             UINPUT_NAME_PREFIX, dev->name ? dev->name : "TrackPoint");

    if (ioctl(dev->ufd, UI_DEV_SETUP, &setup) < 0) goto fail;
    if (ioctl(dev->ufd, UI_DEV_CREATE, 0)     < 0) goto fail;

    usleep(100000);     /* let udev settle before events start flowing */
    return 0;

fail:
    perror("uinput setup");
    close(dev->ufd);
    dev->ufd = -1;
    return -1;
}

/* -----------------------------------------------------------------
 *  Device helpers
 * ----------------------------------------------------------------- */
static int has_middle_button(const char *path)
{
    unsigned long bits[(KEY_MAX / (8 * sizeof(unsigned long))) + 1];
    int fd = open(path, O_RDONLY);
    int ok;

    if (fd < 0)
        return 0;

    memset(bits, 0, sizeof(bits));
    ok = (ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(bits)), bits) >= 0) &&
         test_bit(BTN_MIDDLE, bits);
    close(fd);
    return ok;
}

static char *read_device_name(int fd)
{
    char buf[256];

    memset(buf, 0, sizeof(buf));
    if (ioctl(fd, EVIOCGNAME(sizeof(buf)), buf) < 0 || buf[0] == '\0')
        return strdup("TrackPoint");
    return strdup(buf);
}

static ssize_t find_device_idx(struct device *devs, size_t count, const char *path)
{
    for (size_t i = 0; i < count; ++i)
        if (devs[i].path && strcmp(devs[i].path, path) == 0)
            return (ssize_t)i;
    return -1;
}

static void remove_device(struct device *devs, size_t *count, size_t idx)
{
    if (idx >= *count)
        return;

    if (devs[idx].ufd >= 0) {
        ioctl(devs[idx].ufd, UI_DEV_DESTROY);
        close(devs[idx].ufd);
    }
    if (devs[idx].fd >= 0) {
        ioctl(devs[idx].fd, EVIOCGRAB, 0);
        close(devs[idx].fd);
    }

    free(devs[idx].path);
    free(devs[idx].name);

    if (idx != *count - 1)
        devs[idx] = devs[*count - 1];
    memset(&devs[*count - 1], 0, sizeof(struct device));
    devs[*count - 1].fd = -1;
    devs[*count - 1].ufd = -1;

    (*count)--;
}

static void cleanup_all(struct device *devs, size_t *count)
{
    while (*count > 0)
        remove_device(devs, count, *count - 1);
}

static int add_device(struct device *devs, size_t *count, const char *path)
{
    struct device dev;

    if (*count >= MAX_DEVICES) {
        fprintf(stderr, "Too many devices (max %d)\n", MAX_DEVICES);
        return -1;
    }
    if (find_device_idx(devs, *count, path) >= 0)
        return 0;

    memset(&dev, 0, sizeof(dev));
    dev.fd  = -1;
    dev.ufd = -1;
    dev.path = strdup(path);
    if (!dev.path)
        return -1;

    /* Hotplugged nodes can appear slightly before they are openable. */
    for (int attempt = 0; attempt < 20; ++attempt) {
        dev.fd = open(path, O_RDONLY);
        if (dev.fd >= 0)
            break;
        if (errno != ENOENT && errno != EACCES)
            break;
        usleep(50000);
    }
    if (dev.fd < 0) {
        perror(path);
        free(dev.path);
        return -1;
    }

    dev.name = read_device_name(dev.fd);

    if (ioctl(dev.fd, EVIOCGRAB, 1) < 0) {
        perror("EVIOCGRAB");
        goto fail;
    }

    if (create_uinput(&dev) < 0) {
        fprintf(stderr, "Failed to create uinput clone for %s\n", path);
        ioctl(dev.fd, EVIOCGRAB, 0);
        goto fail;
    }

    devs[*count] = dev;
    (*count)++;

    fprintf(stderr, "Filtering %s (%s)\n", path, dev.name);
    return 0;

fail:
    close(dev.fd);
    free(dev.path);
    free(dev.name);
    return -1;
}

/* -----------------------------------------------------------------
 *  udev matching
 * ----------------------------------------------------------------- */
static int is_virtual(struct udev_device *d)
{
    const char *syspath = udev_device_get_syspath(d);
    return syspath && strstr(syspath, "/devices/virtual/") != NULL;
}

static int hex_match_opt(const char *opt, const char *udev_val)
{
    if (!opt || !*opt)          return 1;
    if (!udev_val || !*udev_val) return 1;

    while (!strncasecmp(opt, "0x", 2))
        opt += 2;
    return strcasecmp(opt, udev_val) == 0;
}

static int device_matches(struct udev_device *d, const struct filters *f)
{
    const char *flag, *model, *vid, *pid;

    if (is_virtual(d))
        return 0;

    flag = udev_device_get_property_value(d, "ID_INPUT_POINTINGSTICK");
    if (!flag || strcmp(flag, "1") != 0)
        return 0;

    model = udev_device_get_property_value(d, "ID_MODEL");
    vid   = udev_device_get_property_value(d, "ID_VENDOR_ID");
    pid   = udev_device_get_property_value(d, "ID_MODEL_ID");

    if (!hex_match_opt(f->vendor_id, vid))  return 0;
    if (!hex_match_opt(f->product_id, pid)) return 0;

    if (f->have_regex) {
        if (!model)
            return 0;
        if (regexec((regex_t *)&f->regex, model, 0, NULL, 0) != 0)
            return 0;
    }
    return 1;
}

static void enumerate_trackpoints(struct udev *udev, const struct filters *f,
                                  struct device *devs, size_t *count)
{
    struct udev_enumerate *e = udev_enumerate_new(udev);
    struct udev_list_entry *entry;

    if (!e)
        return;

    udev_enumerate_add_match_subsystem(e, "input");
    udev_enumerate_add_match_property(e, "ID_INPUT_POINTINGSTICK", "1");
    udev_enumerate_scan_devices(e);

    udev_list_entry_foreach(entry, udev_enumerate_get_list_entry(e)) {
        struct udev_device *d =
            udev_device_new_from_syspath(udev, udev_list_entry_get_name(entry));
        const char *devnode;

        if (!d)
            continue;

        devnode = udev_device_get_devnode(d);
        if (devnode &&
            strncmp(devnode, "/dev/input/event", 16) == 0 &&
            device_matches(d, f) &&
            has_middle_button(devnode))
            add_device(devs, count, devnode);

        udev_device_unref(d);
    }

    udev_enumerate_unref(e);
}

/* -----------------------------------------------------------------
 *  CLI
 * ----------------------------------------------------------------- */
static void usage(const char *prog)
{
    fprintf(stderr,
        "Usage: %s [options]\n"
        "\n"
        "Device selection:\n"
        "  -d, --device PATH    Filter a specific /dev/input/eventX (repeatable).\n"
        "      --auto           Auto-detect TrackPoints and follow hotplug.\n"
        "      --match REGEX    Restrict --auto by udev ID_MODEL (case-insensitive).\n"
        "      --vendor HEX     Restrict --auto by udev ID_VENDOR_ID.\n"
        "      --product HEX    Restrict --auto by udev ID_MODEL_ID.\n"
        "\n"
        "Scrolling:\n"
        "  -t, --threshold N    Motion units per wheel detent (default %d,\n"
        "                       higher = slower scrolling).\n"
        "  -n, --natural        Invert scroll direction.\n"
        "      --no-hscroll     Disable horizontal scrolling.\n"
        "\n"
        "Compatibility:\n"
        "      --no-pointing-stick  Do not tag the clone as a pointing stick\n"
        "                       (use if pointer acceleration feels wrong).\n"
        "\n"
        "  -V, --version        Show version.\n"
        "  -h, --help           Show this help.\n"
        "\n"
        "Build:\n"
        "  gcc -Wall -Wextra -O2 -D_GNU_SOURCE -o %s %s.c -ludev\n",
        prog, DEFAULT_THRESHOLD, prog, prog);
}

int main(int argc, char *argv[])
{
    static const struct option long_opts[] = {
        {"device",     required_argument, NULL, 'd'},
        {"threshold",  required_argument, NULL, 't'},
        {"natural",    no_argument,       NULL, 'n'},
        {"auto",       no_argument,       NULL,  1 },
        {"match",      required_argument, NULL,  2 },
        {"vendor",     required_argument, NULL,  3 },
        {"product",    required_argument, NULL,  4 },
        {"no-hscroll", no_argument,       NULL,  5 },
        {"no-pointing-stick", no_argument, NULL, 6 },
        {"version",    no_argument,       NULL, 'V'},
        {"help",       no_argument,       NULL, 'h'},
        {0, 0, 0, 0}
    };

    const char *explicit_paths[MAX_DEVICES];
    size_t explicit_cnt = 0;
    const char *match_regex = NULL;
    int do_auto = 0;

    struct filters filt;
    struct device devs[MAX_DEVICES];
    size_t total = 0;

    struct udev *udev = NULL;
    struct udev_monitor *mon = NULL;
    int mon_fd = -1;

    struct pollfd fds[MAX_DEVICES + 1];
    size_t devidx[MAX_DEVICES + 1];

    memset(&filt, 0, sizeof(filt));

    for (int c; (c = getopt_long(argc, argv, "d:t:nVh", long_opts, NULL)) != -1; ) {
        switch (c) {
        case 'd':
            if (explicit_cnt >= MAX_DEVICES) {
                fprintf(stderr, "Too many -d arguments (max %d)\n", MAX_DEVICES);
                return EXIT_FAILURE;
            }
            explicit_paths[explicit_cnt++] = optarg;
            break;
        case 't': {
            int v = atoi(optarg);
            if (v < 1 || v > 1000) {
                fprintf(stderr, "--threshold must be between 1 and 1000\n");
                return EXIT_FAILURE;
            }
            opts.threshold = v;
            break;
        }
        case 'n': opts.natural = 1;      break;
        case  1:  do_auto = 1;           break;
        case  2:  match_regex = optarg;  break;
        case  3:  filt.vendor_id = optarg;  break;
        case  4:  filt.product_id = optarg; break;
        case  5:  opts.hscroll = 0;         break;
        case  6:  opts.pointing_stick = 0;  break;
        case 'V':
            printf("middle_button_disable %s\n", PROG_VERSION);
            return EXIT_SUCCESS;
        case 'h':
        default:
            usage(argv[0]);
            return (c == 'h') ? EXIT_SUCCESS : EXIT_FAILURE;
        }
    }

    if (!do_auto && explicit_cnt == 0) {
        fprintf(stderr, "No devices selected - use -d or --auto.\n\n");
        usage(argv[0]);
        return EXIT_FAILURE;
    }

    if (match_regex) {
        int rc = regcomp(&filt.regex, match_regex, REG_NOSUB | REG_ICASE);
        if (rc != 0) {
            char err[128];
            regerror(rc, &filt.regex, err, sizeof(err));
            fprintf(stderr, "Invalid regex \"%s\": %s\n", match_regex, err);
            return EXIT_FAILURE;
        }
        filt.have_regex = 1;
    }

    signal(SIGINT,  stop_handler);
    signal(SIGTERM, stop_handler);

    memset(devs, 0, sizeof(devs));
    for (size_t i = 0; i < MAX_DEVICES; ++i) {
        devs[i].fd  = -1;
        devs[i].ufd = -1;
    }

    if (do_auto) {
        udev = udev_new();
        if (!udev) {
            fprintf(stderr, "udev_new() failed\n");
            goto done;
        }

        mon = udev_monitor_new_from_netlink(udev, "udev");
        if (!mon) {
            fprintf(stderr, "udev_monitor_new_from_netlink() failed\n");
            goto done;
        }

        udev_monitor_filter_add_match_subsystem_devtype(mon, "input", NULL);
        udev_monitor_enable_receiving(mon);
        mon_fd = udev_monitor_get_fd(mon);

        enumerate_trackpoints(udev, &filt, devs, &total);
    }

    for (size_t i = 0; i < explicit_cnt; ++i)
        add_device(devs, &total, explicit_paths[i]);

    if (total == 0)
        fprintf(stderr, "Warning: no matching device found yet.\n");

    while (running) {
        nfds_t nfds = 0;
        size_t base;
        int dead[MAX_DEVICES];

        if (mon_fd >= 0) {
            fds[nfds].fd      = mon_fd;
            fds[nfds].events  = POLLIN;
            fds[nfds].revents = 0;
            nfds++;
        }
        base = nfds;

        for (size_t i = 0; i < total; ++i) {
            fds[nfds].fd      = devs[i].fd;
            fds[nfds].events  = POLLIN;
            fds[nfds].revents = 0;
            devidx[nfds]      = i;
            nfds++;
        }

        if (poll(fds, nfds, -1) < 0) {
            if (errno == EINTR)
                continue;
            perror("poll");
            break;
        }

        /* --- hotplug --- */
        if (mon_fd >= 0 && (fds[0].revents & POLLIN)) {
            struct udev_device *ud;
            while ((ud = udev_monitor_receive_device(mon)) != NULL) {
                const char *action  = udev_device_get_action(ud);
                const char *devnode = udev_device_get_devnode(ud);

                if (action && devnode &&
                    strncmp(devnode, "/dev/input/event", 16) == 0) {
                    if (strcmp(action, "add") == 0) {
                        if (device_matches(ud, &filt) && has_middle_button(devnode))
                            add_device(devs, &total, devnode);
                    } else if (strcmp(action, "remove") == 0) {
                        ssize_t idx = find_device_idx(devs, total, devnode);
                        if (idx >= 0) {
                            fprintf(stderr, "Device removed: %s\n", devnode);
                            remove_device(devs, &total, (size_t)idx);
                        }
                    }
                }
                udev_device_unref(ud);
            }
            /* The device list may have changed; rebuild the poll set. */
            continue;
        }

        /* --- input events --- */
        memset(dead, 0, sizeof(dead));

        for (nfds_t k = base; k < nfds; ++k) {
            struct input_event evbuf[64];
            size_t i = devidx[k];
            ssize_t nbytes;

            if (fds[k].revents & (POLLERR | POLLHUP | POLLNVAL)) {
                dead[i] = 1;
                continue;
            }
            if (!(fds[k].revents & POLLIN))
                continue;

            nbytes = read(devs[i].fd, evbuf, sizeof(evbuf));
            if (nbytes < 0) {
                if (errno == EAGAIN || errno == EINTR)
                    continue;
                fprintf(stderr, "%s: %s\n", devs[i].path, strerror(errno));
                dead[i] = 1;
                continue;
            }

            for (size_t j = 0; j < (size_t)nbytes / sizeof(struct input_event); ++j)
                process_event(&devs[i], &evbuf[j]);
        }

        /* Remove in descending order: remove_device() swaps in the last entry. */
        for (size_t i = total; i-- > 0; ) {
            if (dead[i]) {
                fprintf(stderr, "Dropping %s\n", devs[i].path);
                remove_device(devs, &total, i);
            }
        }
    }

done:
    cleanup_all(devs, &total);
    if (mon)  udev_monitor_unref(mon);
    if (udev) udev_unref(udev);
    if (filt.have_regex) regfree(&filt.regex);

    return EXIT_SUCCESS;
}