libx52/vkm/vkm_linux_evdev.c

707 lines
17 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/*
* VKM Linux evdev implementation
*
* Copyright (C) 2026 Nirenjan Krishnan <nirenjan@nirenjan.org>
*
* SPDX-LicenseIdentifier: GPL-2.0-only WITH Classpath-exception-2.0
*/
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#include <time.h>
#include <inttypes.h>
#include "libevdev/libevdev.h"
#include "libevdev/libevdev-uinput.h"
#include "vkm-internal.h"
/** evdev codes; index by \ref vkm_key. \c -1 for \ref VKM_KEY_NONE only. */
static const int vkm_key_to_evdev[VKM_KEY_MAX] = {
[VKM_KEY_NONE] = -1,
[VKM_KEY_LEFT_CTRL] = KEY_LEFTCTRL,
[VKM_KEY_LEFT_ALT] = KEY_LEFTALT,
[VKM_KEY_LEFT_GUI] = KEY_LEFTMETA,
[VKM_KEY_RIGHT_CTRL] = KEY_RIGHTCTRL,
[VKM_KEY_RIGHT_ALT] = KEY_RIGHTALT,
[VKM_KEY_RIGHT_GUI] = KEY_RIGHTMETA,
[VKM_KEY_APPLICATION] = KEY_MENU,
[VKM_KEY_ESCAPE] = KEY_ESC,
[VKM_KEY_F1] = KEY_F1,
[VKM_KEY_F2] = KEY_F2,
[VKM_KEY_F3] = KEY_F3,
[VKM_KEY_F4] = KEY_F4,
[VKM_KEY_F5] = KEY_F5,
[VKM_KEY_F6] = KEY_F6,
[VKM_KEY_F7] = KEY_F7,
[VKM_KEY_F8] = KEY_F8,
[VKM_KEY_F9] = KEY_F9,
[VKM_KEY_F10] = KEY_F10,
[VKM_KEY_F11] = KEY_F11,
[VKM_KEY_F12] = KEY_F12,
[VKM_KEY_GRAVE_ACCENT] = KEY_GRAVE,
[VKM_KEY_1] = KEY_1,
[VKM_KEY_2] = KEY_2,
[VKM_KEY_3] = KEY_3,
[VKM_KEY_4] = KEY_4,
[VKM_KEY_5] = KEY_5,
[VKM_KEY_6] = KEY_6,
[VKM_KEY_7] = KEY_7,
[VKM_KEY_8] = KEY_8,
[VKM_KEY_9] = KEY_9,
[VKM_KEY_0] = KEY_0,
[VKM_KEY_MINUS] = KEY_MINUS,
[VKM_KEY_EQUAL] = KEY_EQUAL,
[VKM_KEY_BACKSPACE] = KEY_BACKSPACE,
[VKM_KEY_TAB] = KEY_TAB,
[VKM_KEY_Q] = KEY_Q,
[VKM_KEY_W] = KEY_W,
[VKM_KEY_E] = KEY_E,
[VKM_KEY_R] = KEY_R,
[VKM_KEY_T] = KEY_T,
[VKM_KEY_Y] = KEY_Y,
[VKM_KEY_U] = KEY_U,
[VKM_KEY_I] = KEY_I,
[VKM_KEY_O] = KEY_O,
[VKM_KEY_P] = KEY_P,
[VKM_KEY_LEFT_BRACKET] = KEY_LEFTBRACE,
[VKM_KEY_RIGHT_BRACKET] = KEY_RIGHTBRACE,
[VKM_KEY_BACKSLASH] = KEY_BACKSLASH,
[VKM_KEY_CAPS_LOCK] = KEY_CAPSLOCK,
[VKM_KEY_A] = KEY_A,
[VKM_KEY_S] = KEY_S,
[VKM_KEY_D] = KEY_D,
[VKM_KEY_F] = KEY_F,
[VKM_KEY_G] = KEY_G,
[VKM_KEY_H] = KEY_H,
[VKM_KEY_J] = KEY_J,
[VKM_KEY_K] = KEY_K,
[VKM_KEY_L] = KEY_L,
[VKM_KEY_SEMICOLON] = KEY_SEMICOLON,
[VKM_KEY_APOSTROPHE] = KEY_APOSTROPHE,
[VKM_KEY_NONUS_HASH] = KEY_NUMERIC_POUND,
[VKM_KEY_ENTER] = KEY_ENTER,
[VKM_KEY_LEFT_SHIFT] = KEY_LEFTSHIFT,
[VKM_KEY_INTL_BACKSLASH] = KEY_102ND,
[VKM_KEY_Z] = KEY_Z,
[VKM_KEY_X] = KEY_X,
[VKM_KEY_C] = KEY_C,
[VKM_KEY_V] = KEY_V,
[VKM_KEY_B] = KEY_B,
[VKM_KEY_N] = KEY_N,
[VKM_KEY_M] = KEY_M,
[VKM_KEY_COMMA] = KEY_COMMA,
[VKM_KEY_PERIOD] = KEY_DOT,
[VKM_KEY_SLASH] = KEY_SLASH,
[VKM_KEY_RIGHT_SHIFT] = KEY_RIGHTSHIFT,
[VKM_KEY_SPACE] = KEY_SPACE,
[VKM_KEY_PRINT_SCREEN] = KEY_PRINT,
[VKM_KEY_SCROLL_LOCK] = KEY_SCROLLLOCK,
[VKM_KEY_PAUSE] = KEY_PAUSE,
[VKM_KEY_INSERT] = KEY_INSERT,
[VKM_KEY_HOME] = KEY_HOME,
[VKM_KEY_PAGE_UP] = KEY_PAGEUP,
[VKM_KEY_DELETE_FORWARD] = KEY_DELETE,
[VKM_KEY_END] = KEY_END,
[VKM_KEY_PAGE_DOWN] = KEY_PAGEDOWN,
[VKM_KEY_RIGHT_ARROW] = KEY_RIGHT,
[VKM_KEY_LEFT_ARROW] = KEY_LEFT,
[VKM_KEY_DOWN_ARROW] = KEY_DOWN,
[VKM_KEY_UP_ARROW] = KEY_UP,
[VKM_KEY_KEYPAD_NUM_LOCK] = KEY_NUMLOCK,
[VKM_KEY_KEYPAD_DIVIDE] = KEY_KPSLASH,
[VKM_KEY_KEYPAD_MULTIPLY] = KEY_KPASTERISK,
[VKM_KEY_KEYPAD_MINUS] = KEY_KPMINUS,
[VKM_KEY_KEYPAD_PLUS] = KEY_KPPLUS,
[VKM_KEY_KEYPAD_ENTER] = KEY_KPENTER,
[VKM_KEY_KEYPAD_1] = KEY_KP1,
[VKM_KEY_KEYPAD_2] = KEY_KP2,
[VKM_KEY_KEYPAD_3] = KEY_KP3,
[VKM_KEY_KEYPAD_4] = KEY_KP4,
[VKM_KEY_KEYPAD_5] = KEY_KP5,
[VKM_KEY_KEYPAD_6] = KEY_KP6,
[VKM_KEY_KEYPAD_7] = KEY_KP7,
[VKM_KEY_KEYPAD_8] = KEY_KP8,
[VKM_KEY_KEYPAD_9] = KEY_KP9,
[VKM_KEY_KEYPAD_0] = KEY_KP0,
[VKM_KEY_KEYPAD_DECIMAL] = KEY_KPDOT,
[VKM_KEY_KEYPAD_COMMA] = KEY_KPCOMMA,
};
struct vkm_context {
struct libevdev *dev;
struct libevdev_uinput *uidev;
char *name;
bool mouse_hi_res_scroll;
bool mouse_horizontal_scroll;
/** Last modifier byte emitted (\ref vkm_key_modifiers bits 07). */
uint8_t keyboard_modifier_mask;
/** Per-key depressed state from \ref vkm_keyboard_send (non-\c NONE keys). */
bool keyboard_key_pressed[VKM_KEY_MAX];
// Existing state of buttons
struct vkm_mouse_button_state mouse_buttons;
};
static const int mouse_button_map[VKM_MOUSE_BTN_MAX] = {
[VKM_MOUSE_BTN_LEFT] = BTN_LEFT,
[VKM_MOUSE_BTN_RIGHT] = BTN_RIGHT,
[VKM_MOUSE_BTN_MIDDLE] = BTN_MIDDLE,
};
static const int button_value_map[VKM_BUTTON_STATE_MAX] = {
[VKM_BUTTON_PRESSED] = 1,
[VKM_BUTTON_RELEASED] = 0,
};
/*
* High-resolution scroll uses the same scale as REL_WHEEL_HI_RES in the kernel
* (120 units per “click” / detent; aligns with libinput and the Windows convention).
*/
#define VKM_SCROLL_HI_RES_PER_DETENT 120
vkm_result vkm_init(vkm_context **ctx)
{
char name_buf[32];
char *name;
vkm_result rc;
if (ctx == NULL) {
return VKM_ERROR_INVALID_PARAM;
}
*ctx = calloc(1, sizeof(**ctx));
if (*ctx == NULL) {
return VKM_ERROR_OUT_OF_MEMORY;
}
rc = VKM_SUCCESS;
// Create a default name for the virtual device, this will be replaced when
// the user calls vkm_set_option with the appropriate name field.
snprintf(name_buf, sizeof(name_buf), "VKM virtual device @%08" PRIx64,
(uint64_t)(time(NULL)));
name = strdup(name_buf);
if (name == NULL) {
rc = VKM_ERROR_OUT_OF_MEMORY;
goto error;
}
(*ctx)->name = name;
// Set defaults for the flags
return VKM_SUCCESS;
error:
if (name) {
free(name);
}
free(*ctx);
*ctx = NULL;
return rc;
}
void vkm_exit(vkm_context *ctx)
{
volatile unsigned char *vp;
if (ctx == NULL) {
return;
}
(void)vkm_reset(ctx);
free(ctx->name);
if (ctx->uidev) {
libevdev_uinput_destroy(ctx->uidev);
}
if (ctx->dev) {
libevdev_free(ctx->dev);
}
/* Clear the memory to prevent reuse */
vp = (volatile unsigned char *)ctx;
for (int i = 0; i < sizeof(*ctx); i++) {
vp[i] = (unsigned char)0;
}
free(ctx);
}
// Enable mouse events
static void enable_mouse_events(vkm_context *ctx)
{
libevdev_enable_event_type(ctx->dev, EV_REL);
libevdev_enable_event_code(ctx->dev, EV_REL, REL_X, NULL);
libevdev_enable_event_code(ctx->dev, EV_REL, REL_Y, NULL);
libevdev_enable_event_code(ctx->dev, EV_REL, REL_WHEEL, NULL);
if (ctx->mouse_hi_res_scroll) {
libevdev_enable_event_code(ctx->dev, EV_REL, REL_WHEEL_HI_RES, NULL);
}
if (ctx->mouse_horizontal_scroll) {
libevdev_enable_event_code(ctx->dev, EV_REL, REL_HWHEEL, NULL);
if (ctx->mouse_hi_res_scroll) {
libevdev_enable_event_code(ctx->dev, EV_REL, REL_HWHEEL_HI_RES, NULL);
}
}
libevdev_enable_event_type(ctx->dev, EV_KEY);
libevdev_enable_event_code(ctx->dev, EV_KEY, BTN_LEFT, NULL);
libevdev_enable_event_code(ctx->dev, EV_KEY, BTN_RIGHT, NULL);
libevdev_enable_event_code(ctx->dev, EV_KEY, BTN_MIDDLE, NULL);
}
static void enable_keyboard_events(vkm_context *ctx)
{
vkm_key k;
for (k = (vkm_key)0; k < VKM_KEY_MAX; k++) {
int code = vkm_key_to_evdev[k];
if (code >= 0) {
libevdev_enable_event_code(ctx->dev, EV_KEY, (unsigned int)code, NULL);
}
}
}
static const int vkm_modifier_evdev[8] = {
KEY_LEFTCTRL,
KEY_LEFTSHIFT,
KEY_LEFTALT,
KEY_LEFTMETA,
KEY_RIGHTCTRL,
KEY_RIGHTSHIFT,
KEY_RIGHTALT,
KEY_RIGHTMETA,
};
static vkm_result apply_modifier_mask(vkm_context *ctx, uint32_t want)
{
uint32_t have = ctx->keyboard_modifier_mask;
uint32_t diff = want ^ have;
unsigned bit;
if (diff == 0) {
return VKM_SUCCESS;
}
for (bit = 0; bit < 8; bit++) {
uint32_t b = (uint32_t)1 << bit;
if (diff & b) {
int val = (want & b) ? 1 : 0;
int rc;
rc = libevdev_uinput_write_event(ctx->uidev, EV_KEY, vkm_modifier_evdev[bit], val);
if (rc != 0) {
return VKM_ERROR_EVENT;
}
}
}
ctx->keyboard_modifier_mask = (uint8_t)want;
return VKM_SUCCESS;
}
vkm_result vkm_start(vkm_context *ctx)
{
int rc;
if (ctx == NULL) {
return VKM_ERROR_INVALID_PARAM;
}
if (ctx->uidev) {
// Already initialized, no need to do anything else
return VKM_SUCCESS;
}
if (!(ctx->dev)) {
// This will happen on the first time this function is called, but
// will get cleared up if the uinput device is not created, so this
// is just a safety check.
ctx->dev = libevdev_new();
if (ctx->dev == NULL) {
return VKM_ERROR_DEV_FAILURE;
}
libevdev_set_name(ctx->dev, ctx->name);
enable_mouse_events(ctx);
enable_keyboard_events(ctx);
}
rc = libevdev_uinput_create_from_device(ctx->dev, LIBEVDEV_UINPUT_OPEN_MANAGED,
&(ctx->uidev));
if (rc != 0) {
goto error;
}
return VKM_SUCCESS;
error:
libevdev_free(ctx->dev);
ctx->dev = NULL;
return VKM_ERROR_DEV_FAILURE;
}
bool vkm_is_ready(vkm_context *ctx) {
if (ctx == NULL) {
return false;
}
return (ctx->dev != NULL && ctx->uidev != NULL);
}
bool vkm_platform_supported(void)
{
return true;
}
bool vkm_feature_supported(vkm_feature feat)
{
switch (feat) {
case VKM_FEAT_MOUSE:
return true;
case VKM_FEAT_KEYBOARD:
case VKM_FEAT_KEYBOARD_MODIFIERS:
return true;
default:
return false;
}
}
vkm_result vkm_set_option(vkm_context *ctx, vkm_option option, ...)
{
va_list ap;
bool flag;
char *name;
char *name2;
vkm_result rc;
if (ctx == NULL) {
return VKM_ERROR_INVALID_PARAM;
}
va_start(ap, option);
rc = VKM_SUCCESS;
switch (option) {
case VKM_OPT_HI_RES_SCROLL:
// read as int, as bool is promoted to int when passed as a va_arg
flag = va_arg(ap, int);
ctx->mouse_hi_res_scroll = (bool)flag;
break;
case VKM_OPT_HORIZONTAL_SCROLL:
// read as int, as bool is promoted to int when passed as a va_arg
flag = va_arg(ap, int);
ctx->mouse_horizontal_scroll = (bool)flag;
break;
case VKM_OPT_DEVICE_NAME:
name = va_arg(ap, char *);
name2 = strdup(name);
if (name2 == NULL) {
rc = VKM_ERROR_OUT_OF_MEMORY;
} else {
free(ctx->name);
ctx->name = name2;
}
break;
default:
rc = VKM_ERROR_INVALID_PARAM;
break;
}
va_end(ap);
return rc;
}
vkm_result vkm_mouse_move(vkm_context *ctx, int dx, int dy)
{
int rc;
if (ctx == NULL) {
return VKM_ERROR_INVALID_PARAM;
}
if (!vkm_is_ready(ctx)) {
return VKM_ERROR_NOT_READY;
}
if (!dx && !dy) {
return VKM_ERROR_NO_CHANGE;
}
if (dx) {
rc = libevdev_uinput_write_event(ctx->uidev, EV_REL, REL_X, dx);
if (rc != 0) {
return VKM_ERROR_EVENT;
}
}
if (dy) {
rc = libevdev_uinput_write_event(ctx->uidev, EV_REL, REL_Y, dy);
if (rc != 0) {
return VKM_ERROR_EVENT;
}
}
return VKM_SUCCESS;
}
vkm_result vkm_mouse_click(vkm_context *ctx, vkm_mouse_button button, vkm_button_state state)
{
int rc;
vkm_button_state existing;
if (ctx == NULL) {
return VKM_ERROR_INVALID_PARAM;
}
if (button >= VKM_MOUSE_BTN_MAX) {
return VKM_ERROR_INVALID_PARAM;
}
if (state >= VKM_BUTTON_STATE_MAX) {
return VKM_ERROR_INVALID_PARAM;
}
if (!vkm_is_ready(ctx)) {
return VKM_ERROR_NOT_READY;
}
existing = _vkm_get_mouse_button_state(&(ctx->mouse_buttons), button);
if (existing == state) {
return VKM_ERROR_NO_CHANGE;
}
_vkm_set_mouse_button_state(&(ctx->mouse_buttons), button, state);
rc = libevdev_uinput_write_event(ctx->uidev, EV_KEY,
mouse_button_map[button], button_value_map[state]);
if (rc != 0) {
return VKM_ERROR_EVENT;
}
return VKM_SUCCESS;
}
vkm_result vkm_mouse_scroll(vkm_context *ctx, vkm_mouse_scroll_direction dir)
{
int rc;
int sign;
if (ctx == NULL) {
return VKM_ERROR_INVALID_PARAM;
}
if (dir >= VKM_MOUSE_SCROLL_MAX) {
return VKM_ERROR_INVALID_PARAM;
}
if (!vkm_is_ready(ctx)) {
return VKM_ERROR_NOT_READY;
}
if (dir == VKM_MOUSE_SCROLL_LEFT || dir == VKM_MOUSE_SCROLL_RIGHT) {
if (!ctx->mouse_horizontal_scroll) {
return VKM_ERROR_NOT_SUPPORTED;
}
sign = (dir == VKM_MOUSE_SCROLL_LEFT) ? -1 : 1;
if (ctx->mouse_hi_res_scroll) {
rc = libevdev_uinput_write_event(ctx->uidev, EV_REL, REL_HWHEEL_HI_RES,
sign * VKM_SCROLL_HI_RES_PER_DETENT);
if (rc != 0) {
return VKM_ERROR_EVENT;
}
}
rc = libevdev_uinput_write_event(ctx->uidev, EV_REL, REL_HWHEEL, sign);
if (rc != 0) {
return VKM_ERROR_EVENT;
}
return VKM_SUCCESS;
}
sign = (dir == VKM_MOUSE_SCROLL_UP) ? 1 : -1;
if (ctx->mouse_hi_res_scroll) {
rc = libevdev_uinput_write_event(ctx->uidev, EV_REL, REL_WHEEL_HI_RES,
sign * VKM_SCROLL_HI_RES_PER_DETENT);
if (rc != 0) {
return VKM_ERROR_EVENT;
}
}
rc = libevdev_uinput_write_event(ctx->uidev, EV_REL, REL_WHEEL, sign);
if (rc != 0) {
return VKM_ERROR_EVENT;
}
return VKM_SUCCESS;
}
vkm_result vkm_keyboard_send(vkm_context *ctx, vkm_key key, vkm_key_modifiers modifiers,
vkm_key_state state)
{
vkm_result res;
int evcode;
int val;
uint32_t modmask = (uint32_t)modifiers & 0xffu;
if (ctx == NULL) {
return VKM_ERROR_INVALID_PARAM;
}
if (key >= VKM_KEY_MAX) {
return VKM_ERROR_INVALID_PARAM;
}
if (state >= VKM_KEY_STATE_MAX) {
return VKM_ERROR_INVALID_PARAM;
}
if (!vkm_is_ready(ctx)) {
return VKM_ERROR_NOT_READY;
}
val = (state == VKM_KEY_STATE_PRESSED) ? 1 : 0;
if (state == VKM_KEY_STATE_PRESSED) {
res = apply_modifier_mask(ctx, modmask);
if (res != VKM_SUCCESS) {
return res;
}
if (key == VKM_KEY_NONE) {
return VKM_SUCCESS;
}
evcode = vkm_key_to_evdev[key];
if (evcode < 0) {
return VKM_ERROR_INVALID_PARAM;
}
if (libevdev_uinput_write_event(ctx->uidev, EV_KEY, (unsigned int)evcode, val) != 0) {
return VKM_ERROR_EVENT;
}
ctx->keyboard_key_pressed[key] = true;
} else {
if (key != VKM_KEY_NONE) {
evcode = vkm_key_to_evdev[key];
if (evcode < 0) {
return VKM_ERROR_INVALID_PARAM;
}
if (libevdev_uinput_write_event(ctx->uidev, EV_KEY, (unsigned int)evcode, val) != 0) {
return VKM_ERROR_EVENT;
}
ctx->keyboard_key_pressed[key] = false;
}
res = apply_modifier_mask(ctx, modmask);
if (res != VKM_SUCCESS) {
return res;
}
}
return VKM_SUCCESS;
}
static void keyboard_state_clear_tracking(vkm_context *ctx)
{
vkm_mouse_button b;
memset(ctx->keyboard_key_pressed, 0, sizeof(ctx->keyboard_key_pressed));
ctx->keyboard_modifier_mask = 0;
for (b = 0; b < VKM_MOUSE_BTN_MAX; b++) {
_vkm_set_mouse_button_state(&ctx->mouse_buttons, b, VKM_BUTTON_RELEASED);
}
}
vkm_result vkm_reset(vkm_context *ctx)
{
vkm_key k;
vkm_mouse_button b;
int rc;
if (ctx == NULL) {
return VKM_ERROR_INVALID_PARAM;
}
if (!vkm_is_ready(ctx)) {
keyboard_state_clear_tracking(ctx);
return VKM_SUCCESS;
}
for (k = (vkm_key)0; k < VKM_KEY_MAX; k++) {
if (ctx->keyboard_key_pressed[k]) {
int code = vkm_key_to_evdev[k];
if (code >= 0) {
rc = libevdev_uinput_write_event(ctx->uidev, EV_KEY, (unsigned int)code, 0);
if (rc != 0) {
return VKM_ERROR_EVENT;
}
}
ctx->keyboard_key_pressed[k] = false;
}
}
rc = apply_modifier_mask(ctx, 0);
if (rc != VKM_SUCCESS) {
return rc;
}
for (b = 0; b < VKM_MOUSE_BTN_MAX; b++) {
if (_vkm_get_mouse_button_state(&ctx->mouse_buttons, b) == VKM_BUTTON_PRESSED) {
rc = libevdev_uinput_write_event(ctx->uidev, EV_KEY,
mouse_button_map[b], button_value_map[VKM_BUTTON_RELEASED]);
if (rc != 0) {
return VKM_ERROR_EVENT;
}
_vkm_set_mouse_button_state(&ctx->mouse_buttons, b, VKM_BUTTON_RELEASED);
}
}
return vkm_sync(ctx);
}
vkm_result vkm_sync(vkm_context *ctx)
{
int rc;
if (ctx == NULL) {
return VKM_ERROR_INVALID_PARAM;
}
if (!vkm_is_ready(ctx)) {
return VKM_ERROR_NOT_READY;
}
rc = libevdev_uinput_write_event(ctx->uidev, EV_SYN, SYN_REPORT, 0);
if (rc != 0) {
return VKM_ERROR_EVENT;
}
return VKM_SUCCESS;
}