// SPDX-FileCopyrightText: © 2026 Vladimir Zorin // SPDX-License-Identifier: LicenseRef-OWL-1.0-or-later // Licensed under OWL v1.0+. See LICENSE. // evdev.core — a thin, game-agnostic binding over the Linux evdev input API // (/dev/input/event*). It opens a device, reports its identity and axis/button // capabilities, and streams raw {type, code, value} events. All semantics // (which axis is pitch, which button is fire, deadzones, curves) live in Lua. // // Modeled on src/inotify/inotify.c: a single-fd userdata with new/read/close. #include #include #include #include #include #include #include #include #define EVDEV_MT "evdev.handle" #define BITS_PER_LONG (sizeof(unsigned long) * 8) #define NBITS(x) ((((x) - 1) / BITS_PER_LONG) + 1) #define TEST_BIT(bit, array) (((array)[(bit) / BITS_PER_LONG] >> ((bit) % BITS_PER_LONG)) & 1UL) struct evdev_handle { int fd; }; static struct evdev_handle *check_handle(lua_State *L) { struct evdev_handle *h = (struct evdev_handle *)luaL_checkudata(L, 1, EVDEV_MT); if (h->fd < 0) luaL_error(L, "evdev handle is closed"); return h; } /* helper: push (nil, strerror(err), err) for a failed syscall */ static int push_errno(lua_State *L, int err) { lua_pushnil(L); lua_pushstring(L, strerror(err)); lua_pushinteger(L, err); return 3; } /* evdev.open(path [, nonblock=true]) -> handle | nil, err, errno * * Opens read-only + close-on-exec. Non-blocking by default: the frame loop * drains events with read() and must never block waiting for the next one. */ static int l_evdev_open(lua_State *L) { const char *path = luaL_checkstring(L, 1); int nonblock = lua_isnoneornil(L, 2) ? 1 : lua_toboolean(L, 2); int flags = O_RDONLY | O_CLOEXEC; if (nonblock) flags |= O_NONBLOCK; int fd = open(path, flags); if (fd < 0) return push_errno(L, errno); struct evdev_handle *h = (struct evdev_handle *)lua_newuserdata(L, sizeof(*h)); h->fd = fd; luaL_getmetatable(L, EVDEV_MT); lua_setmetatable(L, -2); return 1; } /* handle:close() */ static int l_evdev_close(lua_State *L) { struct evdev_handle *h = (struct evdev_handle *)luaL_checkudata(L, 1, EVDEV_MT); if (h->fd >= 0) { close(h->fd); h->fd = -1; } return 0; } /* __gc metamethod: same as close() */ static int l_evdev_gc(lua_State *L) { return l_evdev_close(L); } /* handle:fileno() -> fd (for lev.wait_readable / epoll integration) */ static int l_evdev_fileno(lua_State *L) { struct evdev_handle *h = check_handle(L); lua_pushinteger(L, h->fd); return 1; } /* handle:info() -> { name, bustype, vendor, product, version } | nil, err, errno * * vendor/product are the stable USB VID/PID used to match binding profiles * (e.g. two otherwise-identical sticks that must map to different axes). */ static int l_evdev_info(lua_State *L) { struct evdev_handle *h = check_handle(L); struct input_id id; if (ioctl(h->fd, EVIOCGID, &id) < 0) return push_errno(L, errno); char name[256]; memset(name, 0, sizeof(name)); /* EVIOCGNAME may fail (device without a name) — leave it empty, not fatal */ ioctl(h->fd, EVIOCGNAME(sizeof(name) - 1), name); lua_newtable(L); lua_pushstring(L, name); lua_setfield(L, -2, "name"); lua_pushinteger(L, id.bustype); lua_setfield(L, -2, "bustype"); lua_pushinteger(L, id.vendor); lua_setfield(L, -2, "vendor"); lua_pushinteger(L, id.product); lua_setfield(L, -2, "product"); lua_pushinteger(L, id.version); lua_setfield(L, -2, "version"); return 1; } /* handle:axes() -> { {code, value, min, max, fuzz, flat, resolution}, ... } * * Every supported EV_ABS axis with its calibration. `flat` is the hardware * deadzone; min/max drive normalization to [-1,1]. Read once at open. */ static int l_evdev_axes(lua_State *L) { struct evdev_handle *h = check_handle(L); unsigned long absbits[NBITS(ABS_MAX + 1)]; memset(absbits, 0, sizeof(absbits)); if (ioctl(h->fd, EVIOCGBIT(EV_ABS, sizeof(absbits)), absbits) < 0) return push_errno(L, errno); lua_newtable(L); int idx = 1; for (int code = 0; code <= ABS_MAX; code++) { if (!TEST_BIT(code, absbits)) continue; struct input_absinfo ai; if (ioctl(h->fd, EVIOCGABS(code), &ai) < 0) continue; /* skip an axis we can't query rather than fail the lot */ lua_pushinteger(L, idx++); lua_newtable(L); lua_pushinteger(L, code); lua_setfield(L, -2, "code"); lua_pushinteger(L, ai.value); lua_setfield(L, -2, "value"); lua_pushinteger(L, ai.minimum); lua_setfield(L, -2, "min"); lua_pushinteger(L, ai.maximum); lua_setfield(L, -2, "max"); lua_pushinteger(L, ai.fuzz); lua_setfield(L, -2, "fuzz"); lua_pushinteger(L, ai.flat); lua_setfield(L, -2, "flat"); lua_pushinteger(L, ai.resolution); lua_setfield(L, -2, "resolution"); lua_settable(L, -3); } return 1; } /* handle:buttons() -> { code, code, ... } — every supported EV_KEY code */ static int l_evdev_buttons(lua_State *L) { struct evdev_handle *h = check_handle(L); unsigned long keybits[NBITS(KEY_MAX + 1)]; memset(keybits, 0, sizeof(keybits)); if (ioctl(h->fd, EVIOCGBIT(EV_KEY, sizeof(keybits)), keybits) < 0) return push_errno(L, errno); lua_newtable(L); int idx = 1; for (int code = 0; code <= KEY_MAX; code++) { if (!TEST_BIT(code, keybits)) continue; lua_pushinteger(L, idx++); lua_pushinteger(L, code); lua_settable(L, -3); } return 1; } /* handle:read() -> { {type, code, value}, ... } | nil, err, errno * * Drains whatever the kernel has queued. Non-blocking fd with nothing pending * returns an empty table (EAGAIN). A vanished device (ENODEV, e.g. unplugged) * returns nil, err, errno so the caller can drop it. */ static int l_evdev_read(lua_State *L) { struct evdev_handle *h = check_handle(L); struct input_event evs[64]; ssize_t len = read(h->fd, evs, sizeof(evs)); if (len < 0) { int err = errno; if (err == EAGAIN || err == EWOULDBLOCK) { lua_newtable(L); /* no events pending */ return 1; } return push_errno(L, err); } if (len == 0) { lua_newtable(L); return 1; } int n = (int)(len / sizeof(struct input_event)); lua_newtable(L); for (int i = 0; i < n; i++) { lua_pushinteger(L, i + 1); lua_newtable(L); lua_pushinteger(L, evs[i].type); lua_setfield(L, -2, "type"); lua_pushinteger(L, evs[i].code); lua_setfield(L, -2, "code"); lua_pushinteger(L, evs[i].value); lua_setfield(L, -2, "value"); lua_settable(L, -3); } return 1; } /* Register the EV, ABS and BTN constants a Lua binding table refers to by name */ static void set_constants(lua_State *L) { struct { const char *name; int value; } c[] = { {"EV_SYN", EV_SYN }, {"EV_KEY", EV_KEY }, {"EV_ABS", EV_ABS }, {"EV_MSC", EV_MSC }, {"SYN_REPORT", SYN_REPORT }, {"ABS_X", ABS_X }, {"ABS_Y", ABS_Y }, {"ABS_Z", ABS_Z }, {"ABS_RX", ABS_RX }, {"ABS_RY", ABS_RY }, {"ABS_RZ", ABS_RZ }, {"ABS_THROTTLE", ABS_THROTTLE}, {"ABS_RUDDER", ABS_RUDDER }, {"ABS_HAT0X", ABS_HAT0X }, {"ABS_HAT0Y", ABS_HAT0Y }, {"BTN_JOYSTICK", BTN_JOYSTICK}, {"BTN_TRIGGER", BTN_TRIGGER }, {"BTN_THUMB", BTN_THUMB }, {"BTN_THUMB2", BTN_THUMB2 }, {"BTN_TOP", BTN_TOP }, {"BTN_TOP2", BTN_TOP2 }, {"BTN_PINKIE", BTN_PINKIE }, {"BTN_BASE", BTN_BASE }, {"BTN_BASE2", BTN_BASE2 }, {"BTN_BASE3", BTN_BASE3 }, {"BTN_BASE4", BTN_BASE4 }, {"BTN_BASE5", BTN_BASE5 }, {"BTN_BASE6", BTN_BASE6 }, }; size_t n = sizeof(c) / sizeof(c[0]); for (size_t i = 0; i < n; i++) { lua_pushinteger(L, c[i].value); lua_setfield(L, -2, c[i].name); } } static const luaL_Reg evdev_methods[] = { {"info", l_evdev_info }, {"axes", l_evdev_axes }, {"buttons", l_evdev_buttons}, {"read", l_evdev_read }, {"fileno", l_evdev_fileno }, {"close", l_evdev_close }, {NULL, NULL } }; static const luaL_Reg evdev_functions[] = { {"open", l_evdev_open}, {NULL, NULL } }; int luaopen_evdev_core(lua_State *L) { /* per-handle metatable */ luaL_newmetatable(L, EVDEV_MT); lua_newtable(L); luaL_register(L, NULL, evdev_methods); lua_setfield(L, -2, "__index"); lua_pushcfunction(L, l_evdev_gc); lua_setfield(L, -2, "__gc"); lua_pop(L, 1); /* module table + constants (no global pollution) */ luaL_newlib(L, evdev_functions); set_constants(L); return 1; }