feat: Add virtual keyboard/mouse library support

Previously, x52d had conditional compilation flags linking to libevdev
on Linux. However, I wanted to change this so that we use an abstraction
layer that will link to the appropriate backend (evdev on Linux only for
now). The idea is that we get rid of all conditional compilation blocks
and deal with the backend through the vkm library.

This new library handles the mouse scrolling, clicking, as well as
adding the ability to include keyboard events so that we can support the
profiles feature in a future commit.
mouse-isometric-mode
nirenjan 2026-03-30 10:01:50 -07:00
parent 230951a232
commit 75e6f253c9
20 changed files with 3206 additions and 158 deletions

View File

@ -986,6 +986,7 @@ INPUT_FILE_ENCODING =
FILE_PATTERNS = libx52.h \
libx52io.h \
libx52util.h \
vkm.h \
x52_cli.c \
x52ctl.c \
x52dcomm.h \

View File

@ -26,19 +26,14 @@ x52d_sources = [
'x52d_notify.c',
'x52d_led.c',
'x52d_command.c',
'x52d_io.c',
'x52d_mouse_handler.c',
]
dep_threads = dependency('threads')
x52d_linkwith = [lib_libx52, lib_libx52dcomm]
x52d_linkwith = [lib_libx52, lib_libx52dcomm, lib_vkm, lib_libx52io]
x52d_deps = [dep_pinelog, dep_inih, dep_threads, dep_intl]
x52d_cflags = []
if dep_evdev.found()
x52d_sources += 'x52d_io.c'
x52d_sources += 'x52d_mouse_evdev.c'
x52d_cflags += '-DHAVE_EVDEV'
x52d_linkwith += lib_libx52io
x52d_deps += dep_evdev
endif
exe_x52d = executable('x52d', x52d_sources,
install: true,

View File

@ -329,10 +329,8 @@ int main(int argc, char **argv)
goto cleanup;
}
x52d_notify_init(notify_sock);
#if defined(HAVE_EVDEV)
x52d_io_init();
x52d_mouse_evdev_init();
#endif
x52d_mouse_handler_init();
// Re-enable signals
rc = pthread_sigmask(SIG_UNBLOCK, &sigblockset, NULL);
@ -371,10 +369,8 @@ cleanup:
x52d_dev_exit();
x52d_command_exit();
x52d_notify_exit();
#if defined(HAVE_EVDEV)
x52d_mouse_evdev_exit();
x52d_mouse_handler_exit();
x52d_io_exit();
#endif
// Remove the PID file
PINELOG_TRACE("Removing PID file %s", pid_file);

View File

@ -30,9 +30,7 @@ void x52d_cfg_set_Mouse_Enabled(bool enabled)
{
PINELOG_DEBUG(_("Setting mouse enable to %s"),
enabled ? _("on") : _("off"));
#if defined HAVE_EVDEV
x52d_mouse_evdev_thread_control(enabled);
#endif
x52d_mouse_thread_control(enabled);
}
void x52d_cfg_set_Mouse_Speed(int speed)

View File

@ -18,9 +18,9 @@ extern volatile int mouse_scroll_dir;
#define MOUSE_MULT_FACTOR 4
void x52d_mouse_evdev_thread_control(bool enabled);
void x52d_mouse_evdev_init(void);
void x52d_mouse_evdev_exit(void);
void x52d_mouse_thread_control(bool enabled);
void x52d_mouse_handler_init(void);
void x52d_mouse_handler_exit(void);
void x52d_mouse_report_event(libx52io_report *report);
#endif // !defined X52D_MOUSE_H

View File

@ -1,7 +1,7 @@
/*
* Saitek X52 Pro MFD & LED driver - Mouse driver
*
* Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org)
* Copyright (C) 2021-2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
*
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
*/
@ -12,10 +12,10 @@
#include <pthread.h>
#include <unistd.h>
#include "libevdev/libevdev.h"
#include "libevdev/libevdev-uinput.h"
#include "libx52io.h"
#include "vkm.h"
#define PINELOG_MODULE X52D_MOD_MOUSE
#include "pinelog.h"
#include "x52d_config.h"
#include "x52d_const.h"
@ -24,36 +24,47 @@
static pthread_t mouse_thr;
static bool mouse_thr_enabled = false;
static struct libevdev_uinput *mouse_uidev;
static bool mouse_uidev_created = false;
static vkm_context *mouse_context;
static volatile libx52io_report old_report;
static volatile libx52io_report new_report;
static int report_button_change(int button, int index)
static int report_button_change(vkm_mouse_button button, int index)
{
int rc = 1;
vkm_result rc;
bool old_button = old_report.button[index];
bool new_button = new_report.button[index];
vkm_button_state state;
if (old_button != new_button) {
rc = libevdev_uinput_write_event(mouse_uidev, EV_KEY, button,
(int)new_button);
if (rc != 0) {
PINELOG_ERROR(_("Error writing mouse button event (button %d, state %d)"),
button, (int)new_button);
state = new_button ? VKM_BUTTON_PRESSED : VKM_BUTTON_RELEASED;
rc = vkm_mouse_click(mouse_context, button, state);
if (rc != VKM_SUCCESS && rc != VKM_ERROR_NO_CHANGE) {
PINELOG_ERROR(_("Error %d writing mouse button event (button %d, state %d)"),
rc, button, (int)new_button);
}
}
return rc;
return (rc == VKM_SUCCESS);
}
static int report_wheel(void)
{
int rc = 1;
vkm_result rc;
int wheel = 0;
bool scroll_up = new_report.button[LIBX52IO_BTN_MOUSE_SCROLL_UP];
bool scroll_dn = new_report.button[LIBX52IO_BTN_MOUSE_SCROLL_DN];
bool old_scroll_up = old_report.button[LIBX52IO_BTN_MOUSE_SCROLL_UP];
bool old_scroll_dn = old_report.button[LIBX52IO_BTN_MOUSE_SCROLL_DN];
vkm_mouse_scroll_direction dir;
/*
* Handle multiple scroll button presses in sequence. This happens if a
* hardware axis is very noisy and the firmware sends a sequence of reports
* with button down, even though this is technically a momentary button.
*/
scroll_up = (scroll_up ^ old_scroll_up) & scroll_up;
scroll_dn = (scroll_dn ^ old_scroll_dn) & scroll_dn;
if (scroll_up) {
// Scroll up event
@ -64,19 +75,18 @@ static int report_wheel(void)
}
if (wheel != 0) {
rc = libevdev_uinput_write_event(mouse_uidev, EV_REL, REL_WHEEL, wheel);
if (rc != 0) {
PINELOG_ERROR(_("Error writing mouse wheel event %d"), wheel);
dir = (wheel == 1) ? VKM_MOUSE_SCROLL_UP : VKM_MOUSE_SCROLL_DOWN;
rc = vkm_mouse_scroll(mouse_context, dir);
if (rc != VKM_SUCCESS) {
PINELOG_ERROR(_("Error writing mouse wheel event %d"), dir);
}
}
return rc;
return (rc == VKM_SUCCESS);
}
static int report_axis(int axis, int index)
static int get_axis_val(int index)
{
int rc = 1;
int axis_val = new_report.axis[index];
/*
@ -96,22 +106,29 @@ static int report_axis(int axis, int index)
*/
axis_val = (axis_val * mouse_mult) / MOUSE_MULT_FACTOR;
if (axis_val) {
rc = libevdev_uinput_write_event(mouse_uidev, EV_REL, axis, axis_val);
if (rc != 0) {
PINELOG_ERROR(_("Error writing mouse axis event (axis %d, value %d)"),
axis, axis_val);
}
return axis_val;
}
static int report_axis(void)
{
vkm_result rc;
int dx = get_axis_val(LIBX52IO_AXIS_THUMBX);
int dy = get_axis_val(LIBX52IO_AXIS_THUMBY);
rc = vkm_mouse_move(mouse_context, dx, dy);
if (rc != VKM_SUCCESS && rc != VKM_ERROR_NO_CHANGE) {
PINELOG_ERROR(_("Error %d writing mouse axis event (dx %d, dy %d)"),
rc, dx, dy);
}
return rc;
return (rc == VKM_SUCCESS);
}
static void report_sync(void)
{
int rc;
rc = libevdev_uinput_write_event(mouse_uidev, EV_SYN, SYN_REPORT, 0);
if (rc != 0) {
vkm_result rc;
rc = vkm_sync(mouse_context);
if (rc != VKM_SUCCESS) {
PINELOG_ERROR(_("Error writing mouse sync event"));
} else {
memcpy((void *)&old_report, (void *)&new_report, sizeof(old_report));
@ -129,16 +146,11 @@ static void reset_reports(void)
static void * x52_mouse_thr(void *param)
{
bool state_changed;
(void)param;
PINELOG_INFO(_("Starting X52 virtual mouse driver thread"));
for (;;) {
state_changed = false;
state_changed |= (0 == report_axis(REL_X, LIBX52IO_AXIS_THUMBX));
state_changed |= (0 == report_axis(REL_Y, LIBX52IO_AXIS_THUMBY));
if (state_changed) {
if (report_axis()) {
report_sync();
}
@ -166,9 +178,9 @@ static void x52d_mouse_thr_exit(void)
pthread_cancel(mouse_thr);
}
void x52d_mouse_evdev_thread_control(bool enabled)
void x52d_mouse_thread_control(bool enabled)
{
if (!mouse_uidev_created) {
if (!vkm_is_ready(mouse_context)) {
PINELOG_INFO(_("Virtual mouse not created. Ignoring thread state change"));
return;
}
@ -198,13 +210,13 @@ void x52d_mouse_report_event(libx52io_report *report)
if (report) {
memcpy((void *)&new_report, report, sizeof(new_report));
if (!mouse_uidev_created || !mouse_thr_enabled) {
if (!vkm_is_ready(mouse_context) || !mouse_thr_enabled) {
return;
}
state_changed = false;
state_changed |= (0 == report_button_change(BTN_LEFT, LIBX52IO_BTN_MOUSE_PRIMARY));
state_changed |= (0 == report_button_change(BTN_RIGHT, LIBX52IO_BTN_MOUSE_SECONDARY));
state_changed |= (0 == report_button_change(VKM_MOUSE_BTN_LEFT, LIBX52IO_BTN_MOUSE_PRIMARY));
state_changed |= (0 == report_button_change(VKM_MOUSE_BTN_RIGHT, LIBX52IO_BTN_MOUSE_SECONDARY));
state_changed |= (0 == report_wheel());
if (state_changed) {
@ -215,35 +227,27 @@ void x52d_mouse_report_event(libx52io_report *report)
}
}
void x52d_mouse_evdev_init(void)
void x52d_mouse_handler_init(void)
{
int rc;
struct libevdev *dev;
vkm_result rc;
/* Create a new mouse device */
dev = libevdev_new();
libevdev_set_name(dev, "X52 virtual mouse");
libevdev_enable_event_type(dev, EV_REL);
libevdev_enable_event_code(dev, EV_REL, REL_X, NULL);
libevdev_enable_event_code(dev, EV_REL, REL_Y, NULL);
libevdev_enable_event_code(dev, EV_REL, REL_WHEEL, NULL);
libevdev_enable_event_type(dev, EV_KEY);
libevdev_enable_event_code(dev, EV_KEY, BTN_LEFT, NULL);
libevdev_enable_event_code(dev, EV_KEY, BTN_RIGHT, NULL);
rc = vkm_init(&mouse_context);
if (rc != VKM_SUCCESS) {
PINELOG_ERROR(_("Error %d creating X52 virtual mouse"), rc);
return;
}
rc = libevdev_uinput_create_from_device(dev, LIBEVDEV_UINPUT_OPEN_MANAGED,
&mouse_uidev);
if (rc != 0) {
PINELOG_ERROR(_("Error %d creating X52 virtual mouse: %s"),
-rc, strerror(-rc));
} else {
mouse_uidev_created = true;
vkm_set_option(mouse_context, VKM_OPT_DEVICE_NAME, "X52 virtual mouse");
rc = vkm_start(mouse_context);
if (rc != VKM_SUCCESS) {
PINELOG_ERROR(_("Error %d creating X52 virtual mouse"), rc);
}
}
void x52d_mouse_evdev_exit(void)
void x52d_mouse_handler_exit(void)
{
x52d_mouse_evdev_thread_control(false);
mouse_uidev_created = false;
libevdev_uinput_destroy(mouse_uidev);
x52d_mouse_thread_control(false);
vkm_exit(mouse_context);
mouse_context = NULL;
}

View File

@ -19,22 +19,18 @@
#include "x52d_const.h"
#include "x52d_mouse.h"
#if defined HAVE_EVDEV
/* Stub for evdev */
void x52d_mouse_evdev_thread_control(bool enabled)
/* Stub for handler */
void x52d_mouse_thread_control(bool enabled)
{
function_called();
check_expected(enabled);
}
#endif
static void test_mouse_thread_enabled(void **state)
{
(void)state;
#if defined HAVE_EVDEV
expect_function_calls(x52d_mouse_evdev_thread_control, 1);
expect_value(x52d_mouse_evdev_thread_control, enabled, true);
#endif
expect_function_calls(x52d_mouse_thread_control, 1);
expect_value(x52d_mouse_thread_control, enabled, true);
x52d_cfg_set_Mouse_Enabled(true);
}
@ -42,10 +38,8 @@ static void test_mouse_thread_enabled(void **state)
static void test_mouse_thread_disabled(void **state)
{
(void)state;
#if defined HAVE_EVDEV
expect_function_calls(x52d_mouse_evdev_thread_control, 1);
expect_value(x52d_mouse_evdev_thread_control, enabled, false);
#endif
expect_function_calls(x52d_mouse_thread_control, 1);
expect_value(x52d_mouse_thread_control, enabled, false);
x52d_cfg_set_Mouse_Enabled(false);
}

View File

@ -9,7 +9,12 @@ if not dep_hidapi.found()
dep_hidapi = dependency('hidapi', required: true)
endif
dep_evdev = dependency('libevdev', required: false)
if host_machine.system() == 'linux'
dep_evdev = dependency('libevdev', required: false)
else
# Create a dummy dependency
dep_evdev = dependency('', required: false)
endif
dep_systemd = dependency('systemd', required: false)
dep_udev = dependency('udev', required: false)
@ -108,11 +113,12 @@ dep_inih = dependency('inih')
# Shared libraries and programs
#######################################################################
# Includes
includes = include_directories('.', 'libx52', 'libx52io', 'libx52util')
includes = include_directories('.', 'libx52', 'libx52io', 'libx52util', 'vkm')
subdir('libx52')
subdir('libx52io')
subdir('libx52util')
subdir('vkm')
subdir('bugreport')
subdir('cli')
subdir('joytest')

View File

@ -4,6 +4,8 @@ libx52/x52_stringify.c
libx52io/io_strings.c
vkm/vkm_common.c
evtest/ev_test.c
joytest/x52_test.c
@ -22,6 +24,6 @@ daemon/x52d_config_parser.c
daemon/x52d_device.c
daemon/x52d_io.c
daemon/x52d_mouse.c
daemon/x52d_mouse_evdev.c
daemon/x52d_mouse_handler.c
daemon/x52d_notify.c
daemon/x52ctl.c

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: libx52 0.3.3\n"
"Report-Msgid-Bugs-To: https://github.com/nirenjan/libx52/issues\n"
"POT-Creation-Date: 2026-03-19 00:09-0700\n"
"POT-Creation-Date: 2026-03-27 20:52-0700\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
#: libx52/x52_strerror.c:23 libx52io/io_strings.c:101
#: libx52/x52_strerror.c:23 libx52io/io_strings.c:101 vkm/vkm_common.c:25
msgid "Success"
msgstr ""
@ -29,7 +29,7 @@ msgstr ""
msgid "Insufficient memory"
msgstr ""
#: libx52/x52_strerror.c:26
#: libx52/x52_strerror.c:26 vkm/vkm_common.c:29
msgid "Invalid parameter"
msgstr ""
@ -85,7 +85,7 @@ msgstr ""
msgid "System call interrupted"
msgstr ""
#: libx52/x52_strerror.c:66 libx52io/io_strings.c:125
#: libx52/x52_strerror.c:66 libx52io/io_strings.c:125 vkm/vkm_common.c:52
#, c-format
msgid "Unknown error %d"
msgstr ""
@ -143,12 +143,12 @@ msgid "Unknown LED state %d"
msgstr ""
#: libx52/x52_stringify.c:47 daemon/x52d_clock.c:29 daemon/x52d_mouse.c:32
#: daemon/x52d_mouse.c:68
#: daemon/x52d_mouse.c:66
msgid "off"
msgstr ""
#: libx52/x52_stringify.c:48 daemon/x52d_clock.c:29 daemon/x52d_mouse.c:32
#: daemon/x52d_mouse.c:68
#: daemon/x52d_mouse.c:66
msgid "on"
msgstr ""
@ -233,6 +233,34 @@ msgstr ""
msgid "Read timeout"
msgstr ""
#: vkm/vkm_common.c:26
msgid "Unknown error"
msgstr ""
#: vkm/vkm_common.c:27
msgid "Not ready"
msgstr ""
#: vkm/vkm_common.c:28
msgid "Out of memory"
msgstr ""
#: vkm/vkm_common.c:30
msgid "Not supported"
msgstr ""
#: vkm/vkm_common.c:31
msgid "Virtual device failure"
msgstr ""
#: vkm/vkm_common.c:32
msgid "Unable to write event"
msgstr ""
#: vkm/vkm_common.c:33
msgid "No state change"
msgstr ""
#: evtest/ev_test.c:110
#, c-format
msgid "Device ID: vendor 0x%04x product 0x%04x version 0x%04x\n"
@ -576,25 +604,25 @@ msgstr ""
msgid "Error %d blocking signals on child threads: %s"
msgstr ""
#: daemon/x52d_main.c:340
#: daemon/x52d_main.c:338
#, c-format
msgid "Error %d unblocking signals on child threads: %s"
msgstr ""
#: daemon/x52d_main.c:353
#: daemon/x52d_main.c:351
msgid "Reloading X52 configuration"
msgstr ""
#: daemon/x52d_main.c:360
#: daemon/x52d_main.c:358
msgid "Saving X52 configuration to disk"
msgstr ""
#: daemon/x52d_main.c:366
#: daemon/x52d_main.c:364
#, c-format
msgid "Received termination signal %s"
msgstr ""
#: daemon/x52d_main.c:383
#: daemon/x52d_main.c:379
msgid "Shutting down X52 daemon"
msgstr ""
@ -841,60 +869,60 @@ msgstr ""
msgid "Setting mouse enable to %s"
msgstr ""
#: daemon/x52d_mouse.c:47
#: daemon/x52d_mouse.c:45
#, c-format
msgid "Ignoring mouse speed %d outside supported range (0-%d)"
msgstr ""
#: daemon/x52d_mouse.c:59
#: daemon/x52d_mouse.c:57
#, c-format
msgid "Setting mouse speed to %d (delay %d ms, multiplier %f)"
msgstr ""
#: daemon/x52d_mouse.c:67
#: daemon/x52d_mouse.c:65
#, c-format
msgid "Setting mouse reverse scroll to %s"
msgstr ""
#: daemon/x52d_mouse_evdev.c:43
#: daemon/x52d_mouse_handler.c:43
#, c-format
msgid "Error writing mouse button event (button %d, state %d)"
msgid "Error %d writing mouse button event (button %d, state %d)"
msgstr ""
#: daemon/x52d_mouse_evdev.c:69
#: daemon/x52d_mouse_handler.c:81
#, c-format
msgid "Error writing mouse wheel event %d"
msgstr ""
#: daemon/x52d_mouse_evdev.c:102
#: daemon/x52d_mouse_handler.c:120
#, c-format
msgid "Error writing mouse axis event (axis %d, value %d)"
msgid "Error %d writing mouse axis event (dx %d, dy %d)"
msgstr ""
#: daemon/x52d_mouse_evdev.c:115
#: daemon/x52d_mouse_handler.c:132
msgid "Error writing mouse sync event"
msgstr ""
#: daemon/x52d_mouse_evdev.c:135
#: daemon/x52d_mouse_handler.c:151
msgid "Starting X52 virtual mouse driver thread"
msgstr ""
#: daemon/x52d_mouse_evdev.c:158
#: daemon/x52d_mouse_handler.c:170
#, c-format
msgid "Error %d initializing mouse thread: %s"
msgstr ""
#: daemon/x52d_mouse_evdev.c:165
#: daemon/x52d_mouse_handler.c:177
msgid "Shutting down X52 virtual mouse driver thread"
msgstr ""
#: daemon/x52d_mouse_evdev.c:172
#: daemon/x52d_mouse_handler.c:184
msgid "Virtual mouse not created. Ignoring thread state change"
msgstr ""
#: daemon/x52d_mouse_evdev.c:237
#: daemon/x52d_mouse_handler.c:236 daemon/x52d_mouse_handler.c:244
#, c-format
msgid "Error %d creating X52 virtual mouse: %s"
msgid "Error %d creating X52 virtual mouse"
msgstr ""
#: daemon/x52d_notify.c:46

View File

@ -7,17 +7,17 @@ msgid ""
msgstr ""
"Project-Id-Version: libx52 0.2.3\n"
"Report-Msgid-Bugs-To: https://github.com/nirenjan/libx52/issues\n"
"POT-Creation-Date: 2026-03-19 00:09-0700\n"
"PO-Revision-Date: 2023-01-04 08:40-0800\n"
"POT-Creation-Date: 2026-03-27 20:52-0700\n"
"PO-Revision-Date: 2026-03-27 08:33-0700\n"
"Last-Translator: Nirenjan Krishnan <nirenjan@gmail.com>\n"
"Language-Team: Dummy Language for testing i18n\n"
"Language: xx_PL\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.0.1\n"
"X-Generator: Poedit 3.4.2\n"
#: libx52/x52_strerror.c:23 libx52io/io_strings.c:101
#: libx52/x52_strerror.c:23 libx52io/io_strings.c:101 vkm/vkm_common.c:25
msgid "Success"
msgstr "Uccesssay"
@ -29,7 +29,7 @@ msgstr "Initializationay ailurefay"
msgid "Insufficient memory"
msgstr "Insufficientay emorymay"
#: libx52/x52_strerror.c:26
#: libx52/x52_strerror.c:26 vkm/vkm_common.c:29
msgid "Invalid parameter"
msgstr "Invaliday arameterpay"
@ -85,7 +85,7 @@ msgstr "Ipepay erroray"
msgid "System call interrupted"
msgstr "Ystemsay allcay interrupteday"
#: libx52/x52_strerror.c:66 libx52io/io_strings.c:125
#: libx52/x52_strerror.c:66 libx52io/io_strings.c:125 vkm/vkm_common.c:52
#, c-format
msgid "Unknown error %d"
msgstr "Unknownay erroray %d"
@ -143,12 +143,12 @@ msgid "Unknown LED state %d"
msgstr "Unknownay EDLay atestay %d"
#: libx52/x52_stringify.c:47 daemon/x52d_clock.c:29 daemon/x52d_mouse.c:32
#: daemon/x52d_mouse.c:68
#: daemon/x52d_mouse.c:66
msgid "off"
msgstr "offay"
#: libx52/x52_stringify.c:48 daemon/x52d_clock.c:29 daemon/x52d_mouse.c:32
#: daemon/x52d_mouse.c:68
#: daemon/x52d_mouse.c:66
msgid "on"
msgstr "onay"
@ -233,6 +233,36 @@ msgstr "I/O erroray"
msgid "Read timeout"
msgstr "Eadray imeouttay"
#: vkm/vkm_common.c:26
#, fuzzy
msgid "Unknown error"
msgstr "Unknownay erroray %d"
#: vkm/vkm_common.c:27
msgid "Not ready"
msgstr ""
#: vkm/vkm_common.c:28
msgid "Out of memory"
msgstr ""
#: vkm/vkm_common.c:30
#, fuzzy
msgid "Not supported"
msgstr "Operationay otnay upportedsay"
#: vkm/vkm_common.c:31
msgid "Virtual device failure"
msgstr ""
#: vkm/vkm_common.c:32
msgid "Unable to write event"
msgstr ""
#: vkm/vkm_common.c:33
msgid "No state change"
msgstr ""
#: evtest/ev_test.c:110
#, c-format
msgid "Device ID: vendor 0x%04x product 0x%04x version 0x%04x\n"
@ -625,25 +655,25 @@ msgstr "Otifynay ocketsay = %s"
msgid "Error %d blocking signals on child threads: %s"
msgstr "Erroray %d ockingblay ignalssay onay ildchay eadsthray: %s"
#: daemon/x52d_main.c:340
#: daemon/x52d_main.c:338
#, c-format
msgid "Error %d unblocking signals on child threads: %s"
msgstr "Erroray %d unblockingay ignalssay onay ildchay eadsthray: %s"
#: daemon/x52d_main.c:353
#: daemon/x52d_main.c:351
msgid "Reloading X52 configuration"
msgstr "Eloadingray X52 onfigurationcay"
#: daemon/x52d_main.c:360
#: daemon/x52d_main.c:358
msgid "Saving X52 configuration to disk"
msgstr "Avingsay X52 onfigurationcay otay iskday"
#: daemon/x52d_main.c:366
#: daemon/x52d_main.c:364
#, c-format
msgid "Received termination signal %s"
msgstr "Eceivedray erminationtay ignalsay %s"
#: daemon/x52d_main.c:383
#: daemon/x52d_main.c:379
msgid "Shutting down X52 daemon"
msgstr "Uttingshay ownday X52 aemonday"
@ -894,61 +924,62 @@ msgstr "Uttingshay ownday X52 I/O iverdray eadthray"
msgid "Setting mouse enable to %s"
msgstr "Ettingsay ousemay enableay otay %s"
#: daemon/x52d_mouse.c:47
#: daemon/x52d_mouse.c:45
#, c-format
msgid "Ignoring mouse speed %d outside supported range (0-%d)"
msgstr "Ignoringay ousemay eedspay %d outsideay upportedsay angeray (0-%d)"
#: daemon/x52d_mouse.c:59
#: daemon/x52d_mouse.c:57
#, c-format
msgid "Setting mouse speed to %d (delay %d ms, multiplier %f)"
msgstr "Ettingsay ousemay eedspay otay %d (elayday %d ms, ultipliermay %f)"
#: daemon/x52d_mouse.c:67
#: daemon/x52d_mouse.c:65
#, c-format
msgid "Setting mouse reverse scroll to %s"
msgstr "Ettingsay ousemay everseray ollscray otay %s"
#: daemon/x52d_mouse_evdev.c:43
#: daemon/x52d_mouse_handler.c:43
#, c-format
msgid "Error writing mouse button event (button %d, state %d)"
msgstr "Erroray itingwray ousemay uttonbay eventay (uttonbay %d, atestay %d)"
msgid "Error %d writing mouse button event (button %d, state %d)"
msgstr ""
"Erroray %d itingwray ousemay uttonbay eventay (uttonbay %d, atestay %d)"
#: daemon/x52d_mouse_evdev.c:69
#: daemon/x52d_mouse_handler.c:81
#, c-format
msgid "Error writing mouse wheel event %d"
msgstr "Erroray itingwray ousemay eelwhay eventay %d"
#: daemon/x52d_mouse_evdev.c:102
#: daemon/x52d_mouse_handler.c:120
#, c-format
msgid "Error writing mouse axis event (axis %d, value %d)"
msgstr "Erroray itingwray ousemay axisay eventay (axisay %d, aluevay %d)"
msgid "Error %d writing mouse axis event (dx %d, dy %d)"
msgstr "Erroray %d itingwray ousemay axisay eventay (xday %d, yday %d)"
#: daemon/x52d_mouse_evdev.c:115
#: daemon/x52d_mouse_handler.c:132
msgid "Error writing mouse sync event"
msgstr "Erroray itingwray ousemay yncsay eventay"
#: daemon/x52d_mouse_evdev.c:135
#: daemon/x52d_mouse_handler.c:151
msgid "Starting X52 virtual mouse driver thread"
msgstr "Artingstay X52 irtualvay ousemay iverdray eadthray"
#: daemon/x52d_mouse_evdev.c:158
#: daemon/x52d_mouse_handler.c:170
#, c-format
msgid "Error %d initializing mouse thread: %s"
msgstr "Erroray %d initializingay ousemay eadthray: %s"
#: daemon/x52d_mouse_evdev.c:165
#: daemon/x52d_mouse_handler.c:177
msgid "Shutting down X52 virtual mouse driver thread"
msgstr "Uttingshay ownday X52 irtualvay ousemay iverdray eadthray"
#: daemon/x52d_mouse_evdev.c:172
#: daemon/x52d_mouse_handler.c:184
msgid "Virtual mouse not created. Ignoring thread state change"
msgstr "Irtualvay ousemay otnay eatedcray. Ignoringa eadthray atestay angechay"
#: daemon/x52d_mouse_evdev.c:237
#: daemon/x52d_mouse_handler.c:236 daemon/x52d_mouse_handler.c:244
#, c-format
msgid "Error %d creating X52 virtual mouse: %s"
msgstr "Erroray %d eatingcray X52 irtualvay ousemay: %s"
msgid "Error %d creating X52 virtual mouse"
msgstr "Erroray %d eatingcray X52 irtualvay ousemay"
#: daemon/x52d_notify.c:46
#, c-format

50
vkm/README.md 100644
View File

@ -0,0 +1,50 @@
Virtual keyboard/mouse infrastructure
=====================================
The virtual keyboard/mouse infrastructure (or VKM), is an API used by an
application to inject keyboard and mouse events into the OS stack. The advantage
of using a separate API for this is so that this can be handled in a
cross-platform manner without having to sprinkle `#ifdef`'s throughout the
program.
Base API
========
The API is based around a context, which is an opaque pointer returned by
`vkm_init`. All subsequent VKM calls will take in this pointer as the first
argument, and return a signed 32-bit status code. Once done, `vkm_exit` will
clean up any data structures and close any file descriptors that were opened as
part of the VKM calls.
VKM can also be configured through the `vkm_config` API call.
Device handling
===============
`vkm_new_device` is the API to use when creating a new VKM device. While VKM
will support both keyboard and mouse events from a single device, there may be
cases where the application needs to separate out keyboard and mouse events into
different devices. The flags will enable keyboard and/or mouse support.
Note that the supported event codes (on Linux) are fixed, and cannot be updated.
The keyboard will emulate a standard US keyboard, while the mouse will emulate a
standard 3 button mouse with a scroll wheel.
Mouse handling
==============
The mouse is handled as a single API call that passes in dx and dy to move the
mouse in the relative X and Y axes. VKM will take care of internally translating
the calls to the appropriate framework. This is handled in `vkm_mouse_move`
The scroll wheel is handled through `vkm_mouse_scroll`. By default, the mouse
motion uses standard scrolling, but high resolution scrolling may be enabled.
The buttons are handled by `vkm_mouse_click`. The API will send the state,
depending on the input (pressed or not)
Keyboard handling
=================
The keyboard is handled through a single call `vkm_keyboard_send`. This sends a
single key event, with modifiers enabled (Ctrl, Shift, Alt, GUI).

60
vkm/meson.build 100644
View File

@ -0,0 +1,60 @@
vkm_version = '0.1.0'
vkm_files = files(
'vkm_common.c'
)
vkm_stub_files = files(
'vkm_stub.c'
)
if host_machine.system() == 'linux'
vkm_dep = dep_evdev
if dep_evdev.found()
vkm_platform_files = files(
'vkm_linux_evdev.c'
)
else
vkm_platform_files = vkm_stub_files
endif
else
vkm_dep = dependency('', required: false)
vkm_platform_files = vkm_stub_files
endif
lib_vkm = library('vkm', vkm_files + vkm_platform_files,
install: true,
version: vkm_version,
dependencies: [vkm_dep, dep_intl],
include_directories: [includes])
vkm_strerror_test = executable('vkm-strerror-test',
'test_strerror.c',
'vkm_common.c',
build_by_default: false,
dependencies: [dep_cmocka, dep_intl],
include_directories: [includes],
)
test('vkm-strerror', vkm_strerror_test, protocol: 'tap')
if host_machine.system() == 'linux' and dep_evdev.found()
dep_evdev_headers = dep_evdev.partial_dependency(compile_args: true, link_args: false)
vkm_linux_evdev_test = executable('vkm_linux_evdev_test',
files(
'vkm_linux_evdev_test.c',
'vkm_linux_evdev.c',
'vkm_common.c',
),
include_directories: [includes],
dependencies: [dep_cmocka, dep_evdev_headers, dep_intl],
)
test('vkm_linux_evdev', vkm_linux_evdev_test)
endif
install_headers('vkm.h', subdir: 'vkm')
pkgconfig.generate(lib_vkm,
name: 'vkm',
description: 'Linux/Unix library to control Saitek X52/X52Pro joystick extended functionality.',
subdirs: meson.project_name(),
version: vkm_version)

View File

@ -0,0 +1,57 @@
/*
* VKM strerror unit tests
*
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
*
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
*/
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
#include <stdio.h>
#include <string.h>
#include "vkm.h"
static void test_strerror(void **state)
{
(void)state;
static const char *error_map[VKM_ERROR_MAX] = {
[VKM_SUCCESS] = "Success",
[VKM_ERROR_UNKNOWN] = "Unknown error",
[VKM_ERROR_NOT_READY] = "Not ready",
[VKM_ERROR_OUT_OF_MEMORY] = "Out of memory",
[VKM_ERROR_INVALID_PARAM] = "Invalid parameter",
[VKM_ERROR_NOT_SUPPORTED] = "Not supported",
[VKM_ERROR_DEV_FAILURE] = "Virtual device failure",
[VKM_ERROR_EVENT] = "Unable to write event",
[VKM_ERROR_NO_CHANGE] = "No state change",
};
static const char *unknown_fmt = "Unknown error %d";
char expected[256];
for (int i = -1; i <= (int)VKM_ERROR_MAX + 1; i++) {
if (i < 0 || i >= (int)VKM_ERROR_MAX || error_map[i] == NULL) {
snprintf(expected, sizeof(expected), unknown_fmt, i);
} else {
strncpy(expected, error_map[i], sizeof(expected));
}
assert_string_equal(expected, vkm_strerror(i));
}
}
static const struct CMUnitTest tests[] = {
cmocka_unit_test(test_strerror),
};
int main(void)
{
cmocka_set_message_output(CM_OUTPUT_TAP);
cmocka_run_group_tests(tests, NULL, NULL);
return 0;
}

22
vkm/vkm-internal.h 100644
View File

@ -0,0 +1,22 @@
/*
* VKM common functions
*
* Copyright (C) 2026 Nirenjan Krishnan <nirenjan@nirenjan.org>
*
* SPDX-LicenseIdentifier: GPL-2.0-only WITH Classpath-exception-2.0
*/
#include <stdbool.h>
#include "vkm.h"
#ifndef VKM_INTERNAL_H
#define VKM_INTERNAL_H
struct vkm_mouse_button_state {
vkm_button_state pressed[VKM_MOUSE_BTN_MAX];
};
vkm_button_state _vkm_get_mouse_button_state(struct vkm_mouse_button_state *state, vkm_mouse_button button);
void _vkm_set_mouse_button_state(struct vkm_mouse_button_state *sstate, vkm_mouse_button button, vkm_button_state state);
#endif // !defined VKM_INTERNAL_H

609
vkm/vkm.h 100644
View File

@ -0,0 +1,609 @@
/*
* Virtual keyboard/mouse interface
*
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
*
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
*/
/**
* @file vkm.h
* @brief Functions, structures and enumerations for the virtual
* keyboard/mouse interface library (VKM).
*
* This file contains the type, enum and function prototypes for VKM.
* These functions allow an application to inject keyboard/mouse events
* into the host OS, as long as it has the necessary permissions.
*
* @author Nirenjan Krishnan (nirenjan@nirenjan.org)
*/
#ifndef VKM_H
#define VKM_H
#include <stdint.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Opaque structure used by the VKM framework
*/
struct vkm_context;
/**
* @brief Virtual device context structure used by the VKM framework
*
* All VKM API functions require the application to pass in a pointer to
* a valid context structure. A pointer can be obtained by calling
* \ref vkm_init
*/
typedef struct vkm_context vkm_context;
/**
* @brief Return type used by VKM API functions
*/
typedef int32_t vkm_result;
/**
* @brief Feature identifiers for \ref vkm_feature_supported
*
* Bit flags describing optional VKM capabilities on the current platform.
* Pass one enumerator at a time to \ref vkm_feature_supported.
*/
typedef enum {
VKM_FEAT_MOUSE = (1 << 0), /**< Relative mouse move, buttons, and wheel */
/** Full \ref vkm_keyboard_send key map (platform virtual keyboard) */
VKM_FEAT_KEYBOARD = (1 << 1),
/** Separate left/right logical modifiers in \ref vkm_keyboard_send */
VKM_FEAT_KEYBOARD_MODIFIERS = (1 << 2),
/* Additional flags may be added in the future */
} vkm_feature;
/**
* @brief Error code list
*/
typedef enum {
/** No error, indicates success */
VKM_SUCCESS = 0,
/** Unknown error, used as a catch-all error state */
VKM_ERROR_UNKNOWN,
/** Not started, must call vkm_start first */
VKM_ERROR_NOT_READY,
/** Out of memory */
VKM_ERROR_OUT_OF_MEMORY,
/** Invalid parameter(s) */
VKM_ERROR_INVALID_PARAM,
/** Not supported */
VKM_ERROR_NOT_SUPPORTED,
/** Unable to create virtual devices */
VKM_ERROR_DEV_FAILURE,
/** Unable to write event */
VKM_ERROR_EVENT,
/** No state change in the event, please retry */
VKM_ERROR_NO_CHANGE,
/* Maximum error states, do not use in external code*/
VKM_ERROR_MAX,
} vkm_error_code;
/**
* @brief Return a short description for a \ref vkm_result / \ref vkm_error_code value
*
* The returned pointer refers to static storage and must not be freed. For
* unrecognized codes, the same static buffer may be overwritten by a later call.
*
* When native language support (NLS) is enabled at build time, these messages
* are translated like \ref libx52_strerror. Bind the \c libx52 text domain and
* set \c LC_MESSAGES as for other libx52 components.
*
* @param[in] code Value returned from a VKM API function
*
* @returns Pointer to a NUL-terminated description string
*/
const char *vkm_strerror(vkm_result code);
/**
* @brief Option list
*/
typedef enum {
/**
* @brief Set the high resolution scrolling behavior of the mouse
*
* This option must be passed a boolean which lets VKM know whether to
* enable or disable high resolution scrolling.
*
* Defaults to false. When enabled together with \ref vkm_start,
* \ref vkm_mouse_scroll emits high-resolution REL_*_HI_RES events (120 units
* per step) in addition to discrete REL_WHEEL / REL_HWHEEL ticks.
*/
VKM_OPT_HI_RES_SCROLL,
/**
* @brief Enable or disable horizontal scrolling of the mouse
*
* This option must be passed in a boolean which lets VKM know whether to
* enable or disable horizontal scrolling. If horizontal scrolling is
* disabled, then any requests to \ref vkm_mouse_scroll with
* \ref VKM_MOUSE_SCROLL_LEFT or \ref VKM_MOUSE_SCROLL_RIGHT will return
* \ref VKM_ERROR_INVALID_PARAM.
*
* Defaults to false.
*/
VKM_OPT_HORIZONTAL_SCROLL,
/**
* @brief Set the virtual device name in the system.
*
* This option sets the name of the virtual input device in the system.
* If not set, the virtual device will have a name determined by the
* timestamp at which it was initialized.
*
* Only applicable on Linux.
*/
VKM_OPT_DEVICE_NAME,
/* Max number of options, do not use in external code */
VKM_OPT_MAX
} vkm_option;
/**
* @brief Button state
*/
typedef enum {
/** Button is released */
VKM_BUTTON_RELEASED,
/** Button is pressed */
VKM_BUTTON_PRESSED,
/* Max number of button states, do not use in external code */
VKM_BUTTON_STATE_MAX
} vkm_button_state;
/**
* @brief Mouse button identifiers
*/
typedef enum {
/** Mouse left button */
VKM_MOUSE_BTN_LEFT,
/** Mouse right button */
VKM_MOUSE_BTN_RIGHT,
/** Mouse middle button */
VKM_MOUSE_BTN_MIDDLE,
/* Max number of mouse buttons, do not use in external code */
VKM_MOUSE_BTN_MAX
} vkm_mouse_button;
/**
* @brief Scroll directions
*/
typedef enum {
/** Scroll up */
VKM_MOUSE_SCROLL_UP,
/** Scroll down */
VKM_MOUSE_SCROLL_DOWN,
/** Scroll left (horizontal scrolling) */
VKM_MOUSE_SCROLL_LEFT,
/** Scroll right (horizontal scrolling) */
VKM_MOUSE_SCROLL_RIGHT,
/* Maximum number of scroll states, do not use in external code */
VKM_MOUSE_SCROLL_MAX
} vkm_mouse_scroll_direction;
/**
* @brief Physical key identifiers (105-key ISO / international PC set)
*
* Logical key codes for \ref vkm_keyboard_send. Identifiers follow the USB HID
* Usage Tables (keyboard/keypad page) in meaning; enumerator values are
* arbitrary and are not HID usage IDs.
*
* Shift keys are listed with the alphanumeric block; other modifiers are grouped
* at the start of the enumerator list.
*/
typedef enum {
VKM_KEY_NONE = 0, /**< Sentinel: no key (e.g. modifier-only update) */
VKM_KEY_LEFT_CTRL, /**< Left Control */
VKM_KEY_LEFT_ALT, /**< Left Alt */
VKM_KEY_LEFT_GUI, /**< Left GUI / Meta / Windows key */
VKM_KEY_RIGHT_CTRL, /**< Right Control */
VKM_KEY_RIGHT_ALT, /**< Right Alt / AltGr */
VKM_KEY_RIGHT_GUI, /**< Right GUI / Meta */
VKM_KEY_APPLICATION, /**< Application / Menu key */
VKM_KEY_ESCAPE, /**< Escape */
VKM_KEY_F1, /**< F1 */
VKM_KEY_F2, /**< F2 */
VKM_KEY_F3, /**< F3 */
VKM_KEY_F4, /**< F4 */
VKM_KEY_F5, /**< F5 */
VKM_KEY_F6, /**< F6 */
VKM_KEY_F7, /**< F7 */
VKM_KEY_F8, /**< F8 */
VKM_KEY_F9, /**< F9 */
VKM_KEY_F10, /**< F10 */
VKM_KEY_F11, /**< F11 */
VKM_KEY_F12, /**< F12 */
VKM_KEY_GRAVE_ACCENT, /**< Grave accent / tilde (` and ~ on US) */
VKM_KEY_1, /**< \c 1 / ! */
VKM_KEY_2, /**< \c 2 / @ */
VKM_KEY_3, /**< \c 3 / # */
VKM_KEY_4, /**< \c 4 / $ */
VKM_KEY_5, /**< \c 5 / % */
VKM_KEY_6, /**< \c 6 / ^ */
VKM_KEY_7, /**< \c 7 / & */
VKM_KEY_8, /**< \c 8 / * */
VKM_KEY_9, /**< \c 9 / ( */
VKM_KEY_0, /**< \c 0 / ) */
VKM_KEY_MINUS, /**< Minus / underscore */
VKM_KEY_EQUAL, /**< Equals / plus */
VKM_KEY_BACKSPACE, /**< Backspace */
VKM_KEY_TAB, /**< Tab */
VKM_KEY_Q, /**< \c Q */
VKM_KEY_W, /**< \c W */
VKM_KEY_E, /**< \c E */
VKM_KEY_R, /**< \c R */
VKM_KEY_T, /**< \c T */
VKM_KEY_Y, /**< \c Y */
VKM_KEY_U, /**< \c U */
VKM_KEY_I, /**< \c I */
VKM_KEY_O, /**< \c O */
VKM_KEY_P, /**< \c P */
VKM_KEY_LEFT_BRACKET, /**< Left bracket / brace */
VKM_KEY_RIGHT_BRACKET, /**< Right bracket / brace */
VKM_KEY_BACKSLASH, /**< Backslash / pipe (US placement; JIS Yen) */
VKM_KEY_CAPS_LOCK, /**< Caps Lock */
VKM_KEY_A, /**< \c A */
VKM_KEY_S, /**< \c S */
VKM_KEY_D, /**< \c D */
VKM_KEY_F, /**< \c F */
VKM_KEY_G, /**< \c G */
VKM_KEY_H, /**< \c H */
VKM_KEY_J, /**< \c J */
VKM_KEY_K, /**< \c K */
VKM_KEY_L, /**< \c L */
VKM_KEY_SEMICOLON, /**< Semicolon / colon */
VKM_KEY_APOSTROPHE, /**< Apostrophe / quote */
VKM_KEY_NONUS_HASH, /**< ISO non-US # / ~ (HID usage) */
VKM_KEY_ENTER, /**< Return / Enter */
VKM_KEY_LEFT_SHIFT, /**< Left Shift */
VKM_KEY_INTL_BACKSLASH, /**< ISO extra key (e.g. \| between left Shift and Z) */
VKM_KEY_Z, /**< \c Z */
VKM_KEY_X, /**< \c X */
VKM_KEY_C, /**< \c C */
VKM_KEY_V, /**< \c V */
VKM_KEY_B, /**< \c B */
VKM_KEY_N, /**< \c N */
VKM_KEY_M, /**< \c M */
VKM_KEY_COMMA, /**< Comma / less-than */
VKM_KEY_PERIOD, /**< Period / greater-than */
VKM_KEY_SLASH, /**< Slash / question */
VKM_KEY_RIGHT_SHIFT, /**< Right Shift */
VKM_KEY_SPACE, /**< Space bar */
VKM_KEY_PRINT_SCREEN, /**< Print Screen */
VKM_KEY_SCROLL_LOCK, /**< Scroll Lock */
VKM_KEY_PAUSE, /**< Pause / Break */
VKM_KEY_INSERT, /**< Insert */
VKM_KEY_HOME, /**< Home */
VKM_KEY_PAGE_UP, /**< Page Up */
VKM_KEY_DELETE_FORWARD, /**< Delete (forward) */
VKM_KEY_END, /**< End */
VKM_KEY_PAGE_DOWN, /**< Page Down */
VKM_KEY_RIGHT_ARROW, /**< Arrow right */
VKM_KEY_LEFT_ARROW, /**< Arrow left */
VKM_KEY_DOWN_ARROW, /**< Arrow down */
VKM_KEY_UP_ARROW, /**< Arrow up */
VKM_KEY_KEYPAD_NUM_LOCK, /**< Keypad Num Lock */
VKM_KEY_KEYPAD_DIVIDE, /**< Keypad \c / */
VKM_KEY_KEYPAD_MULTIPLY,/**< Keypad \c * */
VKM_KEY_KEYPAD_MINUS, /**< Keypad \c - */
VKM_KEY_KEYPAD_PLUS, /**< Keypad \c + */
VKM_KEY_KEYPAD_ENTER, /**< Keypad Enter */
VKM_KEY_KEYPAD_1, /**< Keypad \c 1 / End */
VKM_KEY_KEYPAD_2, /**< Keypad \c 2 / Down */
VKM_KEY_KEYPAD_3, /**< Keypad \c 3 / Page Down */
VKM_KEY_KEYPAD_4, /**< Keypad \c 4 / Left */
VKM_KEY_KEYPAD_5, /**< Keypad \c 5 */
VKM_KEY_KEYPAD_6, /**< Keypad \c 6 / Right */
VKM_KEY_KEYPAD_7, /**< Keypad \c 7 / Home */
VKM_KEY_KEYPAD_8, /**< Keypad \c 8 / Up */
VKM_KEY_KEYPAD_9, /**< Keypad \c 9 / Page Up */
VKM_KEY_KEYPAD_0, /**< Keypad \c 0 / Insert */
VKM_KEY_KEYPAD_DECIMAL, /**< Keypad decimal / Delete */
VKM_KEY_KEYPAD_COMMA, /**< Keypad comma (locale-specific layouts) */
VKM_KEY_MAX /**< Past last key; do not use in application code */
} vkm_key;
/**
* @brief Modifier bitmask for \ref vkm_keyboard_send
*
* Left and right modifier keys use the same bit layout as the USB HID keyboard
* modifier byte. Combine values with bitwise OR. These are separate from
* physical modifier key events in \ref vkm_key.
*
* Convenience macros \c VKM_KEY_MOD_CTRL, \c VKM_KEY_MOD_SHIFT, \c VKM_KEY_MOD_ALT,
* and \c VKM_KEY_MOD_GUI alias the left-hand modifiers only; use
* \c VKM_KEY_MOD_L* / \c VKM_KEY_MOD_R* when a specific side is required.
*/
typedef enum {
VKM_KEY_MOD_NONE = 0, /**< No modifiers */
VKM_KEY_MOD_LCTRL = (1 << 0), /**< Left Control (HID modifier byte bit 0) */
VKM_KEY_MOD_LSHIFT = (1 << 1), /**< Left Shift (bit 1) */
VKM_KEY_MOD_LALT = (1 << 2), /**< Left Alt (bit 2) */
VKM_KEY_MOD_LGUI = (1 << 3), /**< Left GUI / Meta (bit 3) */
VKM_KEY_MOD_RCTRL = (1 << 4), /**< Right Control (bit 4) */
VKM_KEY_MOD_RSHIFT = (1 << 5), /**< Right Shift (bit 5) */
VKM_KEY_MOD_RALT = (1 << 6), /**< Right Alt / AltGr (bit 6) */
VKM_KEY_MOD_RGUI = (1 << 7), /**< Right GUI / Meta (bit 7) */
} vkm_key_modifiers;
/** Convenience alias for \ref VKM_KEY_MOD_LCTRL */
#define VKM_KEY_MOD_CTRL VKM_KEY_MOD_LCTRL
/** Convenience alias for \ref VKM_KEY_MOD_LSHIFT */
#define VKM_KEY_MOD_SHIFT VKM_KEY_MOD_LSHIFT
/** Convenience alias for \ref VKM_KEY_MOD_LALT */
#define VKM_KEY_MOD_ALT VKM_KEY_MOD_LALT
/** Convenience alias for \ref VKM_KEY_MOD_LGUI */
#define VKM_KEY_MOD_GUI VKM_KEY_MOD_LGUI
/**
* @brief Physical key action (press or release) for \ref vkm_keyboard_send
*
* Determines the \c value sent with the non-modifier \ref vkm_key (and, on
* release, how the modifier mask is reconciled after the key event).
*/
typedef enum {
VKM_KEY_STATE_RELEASED, /**< Key or button is up */
VKM_KEY_STATE_PRESSED, /**< Key or button is down */
VKM_KEY_STATE_MAX /**< Sentinel; do not use in application code */
} vkm_key_state;
/**
* @brief Initialize the VKM library
*
* This function initializes the VKM library, sets up any internal data
* structures to send input events, and returns a \ref vkm_context pointer
* in the output parameter. All calls to VKM use the returned pointer to
* inject keyboard/mouse events.
*
* @par Example
* @code
* vkm_result rc;
* vkm_context *ctx;
* rc = vkm_init(&ctx);
* if (rc != LIBX52_SUCCESS) {
* // Error handling omitted for brevity
* }
* // Save ctx for use later
* @endcode
*
* @param[out] ctx Pointer to a \ref vkm_context *. This function will
* allocate a context and return the pointer to the context in this variable.
*
* @returns \ref vkm_error_code indicating status
*/
vkm_result vkm_init(vkm_context **ctx);
/**
* @brief Exit the VKM library and free up any resources used
*
* This function calls \ref vkm_reset, releases any resources allocated by
* \ref vkm_init, and terminates the library. Using the freed context after
* this returns is invalid and can cause errors.
*
* @param[in] ctx Context pointer
*/
void vkm_exit(vkm_context *ctx);
/**
* @brief Release all virtual keys and mouse buttons that are still down
*
* Synthesizes release events for any keys pressed through \ref vkm_keyboard_send,
* clears the logical modifier mask from that API, and releases mouse buttons that
* are still pressed. Internal bookkeeping is cleared even if the virtual device
* is not ready (no events are written until \ref vkm_start succeeds).
*
* \ref vkm_exit calls this automatically before freeing the context.
*
* @param[in] ctx Context pointer
*
* @returns
* - \ref VKM_SUCCESS on success or if there was nothing to release
* - \ref VKM_ERROR_INVALID_PARAM on bad pointer
* - \ref VKM_ERROR_EVENT if writing a release event failed
*/
vkm_result vkm_reset(vkm_context *ctx);
/**
* @brief Start any virtual keyboard/mouse devices on the platform
*
* This must be done before injecting any events, and after setting all
* options through \ref vkm_set_option.
*
* @param[in] ctx Context pointer
*
* @returns
* - \ref VKM_SUCCESS on successful start
* - \ref VKM_ERROR_INVALID_PARAM on bad pointer
* - \ref VKM_ERROR_UNKNOWN on other errors
*/
vkm_result vkm_start(vkm_context *ctx);
/**
* @brief check if VKM is started and ready
*
* @param[in] ctx Context pointer
*
* @returns boolean indicating if ready or not.
*/
bool vkm_is_ready(vkm_context *ctx);
/**
* @brief Check if VKM is supported on this platform
*
* On some platforms, there is no support yet for the virtual keyboard/mouse.
* This function will return a boolean indicating if it is supported or not.
*
* @returns boolean indicating support.
*/
bool vkm_platform_supported(void);
/**
* @brief Check if a particular feature is enabled on this platform
*
* Features may be limited on a per-platform basis.
*
* @param[in] feat Feature identifier
*
* @returns boolean indicating if feature is supported or not.
*/
bool vkm_feature_supported(vkm_feature feat);
/**
* @brief Set an option flag for VKM.
*
* Option flags control the behavior of VKM. All options must be set before
* calling \ref vkm_start.
*
* @param[in] ctx Context pointer
* @param[in] option Which option to set
* @param[in] ... Any required arguments for the specified option
*
* @returns
* - \ref VKM_SUCCESS on success
* - \ref VKM_ERROR_INVALID_PARAM if the option or arguments are invalid
* - \ref VKM_ERROR_NOT_SUPPORTED if the option is valid but not supported on this platform
*/
vkm_result vkm_set_option(vkm_context *ctx, vkm_option option, ...);
/**
* @brief Move the mouse by the specified amount
*
* The move mouse takes in a delta of x and y coordinates that tell the system
* to move the mouse by those relative numbers.
*
* @param[in] ctx Context pointer
* @param[in] dx Delta by which to move the mouse in the horizontal axis
* @param[in] dy Delta by which to move the mouse in the vertical axis
*
* @returns
* - \ref VKM_SUCCESS on successful move
* - \ref VKM_ERROR_UNKNOWN on a generic error
* - \ref VKM_ERROR_NOT_SUPPORTED if the mouse move is not supported on this platform
* - \ref VKM_ERROR_NOT_READY if VKM is not started
*/
vkm_result vkm_mouse_move(vkm_context *ctx, int dx, int dy);
/**
* @brief Click the mouse button
*
* Send a mouse button event, this may be either a button down or button up event.
*
* @param[in] ctx Context pointer
* @param[in] button Button identifier
* @param[in] state Button state (press or release)
*
* @returns
* - \ref VKM_SUCCESS on successful move
* - \ref VKM_ERROR_UNKNOWN on a generic error
* - \ref VKM_ERROR_NOT_SUPPORTED if the mouse button click is not supported on this platform
* - \ref VKM_ERROR_NOT_READY if VKM is not started
*/
vkm_result vkm_mouse_click(vkm_context *ctx, vkm_mouse_button button, vkm_button_state state);
/**
* @brief Scroll the mouse wheel
*
* Send a single scroll event to the mouse wheel (one detent in the chosen
* direction). If \ref VKM_OPT_HI_RES_SCROLL was enabled before \ref vkm_start,
* also emits REL_WHEEL_HI_RES / REL_HWHEEL_HI_RES using the standard 120
* units per detent scale before the corresponding discrete REL_WHEEL /
* REL_HWHEEL event.
*
* @param[in] ctx Context pointer
* @param[in] dir Scroll direction
*
* @returns
* - \ref VKM_SUCCESS on successful move
* - \ref VKM_ERROR_UNKNOWN on a generic error
* - \ref VKM_ERROR_INVALID_PARAM if horizontal scrolling is not enabled
* and \p dir is \ref VKM_MOUSE_SCROLL_LEFT or \ref VKM_MOUSE_SCROLL_RIGHT
* - \ref VKM_ERROR_NOT_SUPPORTED if the mouse scrolling is not supported on this platform
* - \ref VKM_ERROR_NOT_READY if VKM is not started
*/
vkm_result vkm_mouse_scroll(vkm_context *ctx, vkm_mouse_scroll_direction dir);
/**
* @brief Send a single keyboard event
*
* Send a single keyboard event to the OS. This will send a single key event,
* with modifiers enabled (Ctrl, Shift, Alt, GUI).
*
* @param[in] ctx Context pointer
* @param[in] key Key identifier
* @param[in] modifiers Modifier keys to enable (Ctrl, Shift, Alt, GUI)
* @param[in] state Key state (press or release)
*
* @returns
* - \ref VKM_SUCCESS on successful send
* - \ref VKM_ERROR_UNKNOWN on a generic error
* - \ref VKM_ERROR_INVALID_PARAM if parameters are invalid
* - \ref VKM_ERROR_NOT_SUPPORTED if the keyboard event is not enabled or
* supported on this platform
* - \ref VKM_ERROR_NOT_READY if VKM is not started
*/
vkm_result vkm_keyboard_send(vkm_context *ctx, vkm_key key, vkm_key_modifiers modifiers, vkm_key_state state);
/**
* @brief Send a sync packet to the OS
*
* On some platforms, a sync packet is necessary for the previously injected
* events to actually get reflected in the system. For platforms where this
* is not needed, this is a noop.
*
* @param[in] ctx Context pointer
*
* @returns
* - \ref VKM_SUCCESS on successful move
* - \ref VKM_ERROR_UNKNOWN on a generic error
* - \ref VKM_ERROR_INVALID_PARAM if parameters are invalid
* - \ref VKM_ERROR_NOT_READY if VKM is not started
*/
vkm_result vkm_sync(vkm_context *ctx);
#ifdef __cplusplus
}
#endif
#endif // !defined VKM_H

71
vkm/vkm_common.c 100644
View File

@ -0,0 +1,71 @@
/*
* VKM common functions
*
* Copyright (C) 2026 Nirenjan Krishnan <nirenjan@nirenjan.org>
*
* SPDX-LicenseIdentifier: GPL-2.0-only WITH Classpath-exception-2.0
*/
#include "config.h"
#include <stdio.h>
#include <stdlib.h>
#include "gettext.h"
#include "vkm-internal.h"
#define N_(str) gettext_noop(str)
#define _(str) dgettext(PACKAGE, str)
/* Error buffer used for building custom error strings */
static char error_buffer[256];
/* List of error strings (indices must match \ref vkm_error_code) */
static const char *error_string[] = {
N_("Success"),
N_("Unknown error"),
N_("Not ready"),
N_("Out of memory"),
N_("Invalid parameter"),
N_("Not supported"),
N_("Virtual device failure"),
N_("Unable to write event"),
N_("No state change"),
};
const char *vkm_strerror(vkm_result code)
{
switch ((vkm_error_code)code) {
case VKM_SUCCESS:
case VKM_ERROR_UNKNOWN:
case VKM_ERROR_NOT_READY:
case VKM_ERROR_OUT_OF_MEMORY:
case VKM_ERROR_INVALID_PARAM:
case VKM_ERROR_NOT_SUPPORTED:
case VKM_ERROR_DEV_FAILURE:
case VKM_ERROR_EVENT:
case VKM_ERROR_NO_CHANGE:
return _(error_string[code]);
default:
snprintf(error_buffer, sizeof(error_buffer),
_("Unknown error %d"), (int)code);
return error_buffer;
}
}
vkm_button_state _vkm_get_mouse_button_state(struct vkm_mouse_button_state *state, vkm_mouse_button button)
{
if (state == NULL) {
return false;
}
return state->pressed[button];
}
void _vkm_set_mouse_button_state(struct vkm_mouse_button_state *sstate, vkm_mouse_button button, vkm_button_state state)
{
if (sstate != NULL) {
sstate->pressed[button] = state;
}
}

View File

@ -0,0 +1,708 @@
/*
* 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)
{
char *name;
struct libevdev_uinput *uidev;
struct libevdev *dev;
if (ctx == NULL) {
return;
}
(void)vkm_reset(ctx);
dev = ctx->dev;
uidev = ctx->uidev;
name = ctx->name;
memset(ctx, 0, sizeof(*ctx));
free(name);
if (uidev) {
libevdev_uinput_destroy(uidev);
}
if (dev) {
libevdev_free(dev);
}
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;
}

File diff suppressed because it is too large Load Diff

222
vkm/vkm_stub.c 100644
View File

@ -0,0 +1,222 @@
/*
* VKM stub implementation
*
* Copyright (C) 2026 Nirenjan Krishnan <nirenjan@nirenjan.org>
*
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
*/
#include <stdlib.h>
#include <stdarg.h>
#include "vkm.h"
struct vkm_context {
bool started;
bool mouse_hi_res_scroll;
bool mouse_horizontal_scroll;
};
vkm_result vkm_init(vkm_context **ctx)
{
if (ctx == NULL) {
return VKM_ERROR_INVALID_PARAM;
}
*ctx = calloc(1, sizeof(**ctx));
if (*ctx == NULL) {
return VKM_ERROR_OUT_OF_MEMORY;
}
return VKM_SUCCESS;
}
vkm_result vkm_reset(vkm_context *ctx)
{
if (ctx == NULL) {
return VKM_ERROR_INVALID_PARAM;
}
if (vkm_is_ready(ctx)) {
return vkm_sync(ctx);
}
return VKM_SUCCESS;
}
void vkm_exit(vkm_context *ctx)
{
if (ctx == NULL) {
return;
}
(void)vkm_reset(ctx);
free(ctx);
}
vkm_result vkm_start(vkm_context *ctx)
{
if (ctx == NULL) {
return VKM_ERROR_INVALID_PARAM;
}
ctx->started = true;
return VKM_SUCCESS;
}
bool vkm_is_ready(vkm_context *ctx)
{
if (ctx == NULL) {
return false;
}
return ctx->started;
}
bool vkm_platform_supported(void)
{
return false;
}
bool vkm_feature_supported(vkm_feature feat)
{
(void)feat;
return false;
}
vkm_result vkm_set_option(vkm_context *ctx, vkm_option option, ...)
{
va_list ap;
bool flag;
char *name;
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:
flag = (bool)va_arg(ap, int);
ctx->mouse_hi_res_scroll = flag;
break;
case VKM_OPT_HORIZONTAL_SCROLL:
flag = (bool)va_arg(ap, int);
ctx->mouse_horizontal_scroll = flag;
break;
case VKM_OPT_DEVICE_NAME:
name = va_arg(ap, char *);
(void)name;
rc = VKM_ERROR_NOT_SUPPORTED;
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)
{
if (ctx == NULL) {
return VKM_ERROR_INVALID_PARAM;
}
if (!vkm_is_ready(ctx)) {
return VKM_ERROR_NOT_READY;
}
if (dx == 0 && dy == 0) {
return VKM_ERROR_NO_CHANGE;
}
return VKM_ERROR_NOT_SUPPORTED;
}
vkm_result vkm_mouse_click(vkm_context *ctx, vkm_mouse_button button, vkm_button_state state)
{
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;
}
return VKM_ERROR_NOT_SUPPORTED;
}
vkm_result vkm_mouse_scroll(vkm_context *ctx, vkm_mouse_scroll_direction dir)
{
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;
}
}
return VKM_ERROR_NOT_SUPPORTED;
}
vkm_result vkm_sync(vkm_context *ctx)
{
if (ctx == NULL) {
return VKM_ERROR_INVALID_PARAM;
}
if (!vkm_is_ready(ctx)) {
return VKM_ERROR_NOT_READY;
}
return VKM_SUCCESS;
}
vkm_result vkm_keyboard_send(vkm_context *ctx, vkm_key key, vkm_key_modifiers modifiers,
vkm_key_state state)
{
(void)modifiers;
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;
}
return VKM_ERROR_NOT_SUPPORTED;
}