diff --git a/configure b/configure
index 38ee05e..35f84d9 100755
--- a/configure
+++ b/configure
@@ -213,6 +213,14 @@ fi
HAVE_VSNPRINTF=`check_func vsnprintf`
+# Check for uinput support (Linux only)
+if [ "$sys" = "Linux" ]; then
+ HAVE_UINPUT_H=`check_header linux/uinput.h`
+ if [ -n "$HAVE_UINPUT_H" ]; then
+ echo "Found uinput support (keyboard emulation for Wayland)"
+ fi
+fi
+
# create Makefile
echo 'creating Makefile ...'
echo "PREFIX = $PREFIX" >Makefile
@@ -282,6 +290,7 @@ echo >>$cfgheader
[ -n "$HAVE_INTTYPES_H" ] && echo $HAVE_INTTYPES_H >>$cfgheader
[ -n "$HAVE_XINPUT2_H" ] && echo $HAVE_XINPUT2_H >>$cfgheader
[ -n "$HAVE_XTEST_H" ] && echo $HAVE_XTEST_H >>$cfgheader
+[ -n "$HAVE_UINPUT_H" ] && echo $HAVE_UINPUT_H >>$cfgheader
[ -n "$HAVE_VSNPRINTF" ] && echo $HAVE_VSNPRINTF >>$cfgheader
echo >>$cfgheader
diff --git a/src/cfgfile.c b/src/cfgfile.c
index 00038e8..1cd96b7 100644
--- a/src/cfgfile.c
+++ b/src/cfgfile.c
@@ -46,7 +46,7 @@ enum {
CFG_SENS_ROT, CFG_SENS_RX, CFG_SENS_RY, CFG_SENS_RZ,
CFG_INVROT, CFG_INVTRANS, CFG_SWAPYZ,
CFG_AXISMAP_N, CFG_BNMAP_N, CFG_BNACT_N, CFG_KBMAP_N,
- CFG_LED, CFG_GRAB,
+ CFG_LED, CFG_GRAB, CFG_KBMAP_USE_X11,
CFG_SERIAL, CFG_DEVID,
NUM_CFG_OPTIONS
@@ -96,6 +96,7 @@ void default_cfg(struct cfg *cfg)
cfg->led = LED_ON;
cfg->grab_device = 1;
+ cfg->kbemu_use_x11 = 0; /* default to uinput when available */
for(i=0; i<6; i++) {
cfg->map_axis[i] = i;
@@ -426,6 +427,21 @@ int read_cfg(const char *fname, struct cfg *cfg)
}
}
+ } else if(strcmp(key_str, "kbmap_use_x11") == 0) {
+ lptr->opt = CFG_KBMAP_USE_X11;
+ if(isint) {
+ cfg->kbemu_use_x11 = ival;
+ } else {
+ if(strcmp(val_str, "true") == 0 || strcmp(val_str, "on") == 0 || strcmp(val_str, "yes") == 0) {
+ cfg->kbemu_use_x11 = 1;
+ } else if(strcmp(val_str, "false") == 0 || strcmp(val_str, "off") == 0 || strcmp(val_str, "no") == 0) {
+ cfg->kbemu_use_x11 = 0;
+ } else {
+ logmsg(LOG_WARNING, "invalid configuration value for %s, expected a boolean value.\n", key_str);
+ continue;
+ }
+ }
+
} else if(strcmp(key_str, "grab") == 0) {
lptr->opt = CFG_GRAB;
if(isint) {
diff --git a/src/cfgfile.h b/src/cfgfile.h
index ead825e..f0ddf1c 100644
--- a/src/cfgfile.h
+++ b/src/cfgfile.h
@@ -57,6 +57,7 @@ struct cfg {
char *kbmap_str[MAX_BUTTONS];
int swapyz;
int led, grab_device;
+ int kbemu_use_x11;
char serial_dev[PATH_MAX];
int repeat_msec;
diff --git a/src/kbemu.c b/src/kbemu.c
index 115f78c..75badf7 100644
--- a/src/kbemu.c
+++ b/src/kbemu.c
@@ -15,108 +15,175 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
-
#include "config.h"
-
-#ifdef USE_X11
#include
+#include
#include
#include "logger.h"
#include "kbemu.h"
+#include "cfgfile.h"
+
+enum {
+ KBEMU_NONE,
+ KBEMU_X11,
+ KBEMU_UINPUT
+};
-#ifdef HAVE_XTEST_H
-#include
-static int use_xtest;
+static int backend = KBEMU_NONE;
+
+extern struct cfg cfg;
+
+/* Initialize keyboard emulation based on config, independent of X11 */
+void kbemu_init(void)
+{
+#if defined(__linux__) && defined(HAVE_UINPUT_H)
+ /* For uinput builds, default to uinput unless kbmap_use_x11 is set */
+ if(!cfg.kbemu_use_x11) {
+ if(backend == KBEMU_NONE) {
+ logmsg(LOG_INFO, "Initializing uinput keyboard emulation backend\n");
+ if(kbemu_uinput_init() == 0) {
+ backend = KBEMU_UINPUT;
+ logmsg(LOG_INFO, "Using uinput keyboard emulation backend\n");
+ } else {
+ logmsg(LOG_WARNING, "Failed to initialize uinput backend\n");
+ }
+ }
+ }
#endif
+}
-static Display *dpy;
+#ifdef USE_X11
+#include
-void kbemu_set_display(Display *d)
+/* Called by proto_x11.c when X11 connection is established */
+void kbemu_set_display(Display *dpy)
{
- dpy = d;
+ if(!dpy) {
+ if(backend == KBEMU_X11) {
+ kbemu_x11_cleanup();
+ backend = KBEMU_NONE;
+ }
+ return;
+ }
+
+ /* If we already have a backend (from kbemu_init), don't override */
+ if(backend != KBEMU_NONE) {
+ logmsg(LOG_DEBUG, "kbemu backend already initialized\n");
+ return;
+ }
- if(d) {
-#ifdef HAVE_XTEST_H
- int tmp;
- use_xtest = XTestQueryExtension(dpy, &tmp, &tmp, &tmp, &tmp);
+ /* Use X11 backend as fallback if uinput wasn't initialized
+ * (either because HAVE_UINPUT_H not defined, or kbmap_use_x11 was set,
+ * or uinput initialization failed)
+ */
+ if(kbemu_x11_init(dpy) == 0) {
+ backend = KBEMU_X11;
+ logmsg(LOG_INFO, "Using X11 keyboard emulation backend\n");
+ return;
+ }
- if(use_xtest)
- logmsg(LOG_DEBUG, "Using XTEST to send key events\n");
- else
-#endif
- logmsg(LOG_DEBUG, "Using XSendEvent to send key events\n");
+ logmsg(LOG_WARNING, "Failed to initialize X11 keyboard emulation\n");
+}
+
+#else /* !USE_X11 */
+
+/* When X11 is not available, try uinput */
+static void kbemu_init_fallback(void)
+{
+ if(backend != KBEMU_NONE) {
+ return; /* already initialized */
+ }
+
+#if defined(__linux__) && defined(HAVE_UINPUT_H)
+ logmsg(LOG_INFO, "X11 not available, trying uinput keyboard emulation\n");
+ if(kbemu_uinput_init() == 0) {
+ backend = KBEMU_UINPUT;
+ logmsg(LOG_INFO, "Using uinput keyboard emulation backend\n");
+ return;
}
+ logmsg(LOG_WARNING, "Failed to initialize uinput keyboard emulation\n");
+#else
+ logmsg(LOG_WARNING, "No keyboard emulation backend available\n");
+#endif
}
+#endif /* USE_X11 */
+
KeySym kbemu_keysym(const char *str)
{
+#ifdef USE_X11
return XStringToKeysym(str);
+#else
+ /* Without X11, we can't parse key names properly.
+ * This function is mainly used during config parsing. */
+ logmsg(LOG_WARNING, "kbemu_keysym: X11 not available, cannot parse key name: %s\n", str);
+ return 0;
+#endif
}
const char *kbemu_keyname(KeySym sym)
{
+#ifdef USE_X11
return XKeysymToString(sym);
+#else
+ static char buf[32];
+ snprintf(buf, sizeof buf, "0x%lx", sym);
+ return buf;
+#endif
}
void send_kbevent(KeySym key, int press)
{
- XEvent xevent;
- Window win;
- int rev_state;
- KeyCode kc;
-
- if(!dpy) return;
-
- if(!(kc = XKeysymToKeycode(dpy, key))) {
- logmsg(LOG_WARNING, "failed to convert keysym %lu to keycode\n", key);
- return;
+#ifndef USE_X11
+ /* Auto-initialize if not already done */
+ if(backend == KBEMU_NONE) {
+ kbemu_init_fallback();
}
+#endif
-#ifdef HAVE_XTEST_H
- if(use_xtest) {
- XTestFakeKeyEvent(dpy, kc, press, 0);
- XFlush(dpy);
- return;
- }
+ switch(backend) {
+#ifdef USE_X11
+ case KBEMU_X11:
+ kbemu_x11_send_key(key, press);
+ break;
#endif
- XGetInputFocus(dpy, &win, &rev_state);
-
- xevent.type = press ? KeyPress : KeyRelease;
- xevent.xkey.display = dpy;
- xevent.xkey.root = DefaultRootWindow(dpy);
- xevent.xkey.window = win;
- xevent.xkey.subwindow = None;
- xevent.xkey.keycode = kc;
- xevent.xkey.state = 0;
- xevent.xkey.time = CurrentTime;
- xevent.xkey.x = xevent.xkey.y = 1;
- xevent.xkey.x_root = xevent.xkey.y_root = 1;
-
- XSendEvent(dpy, win, True, press ? KeyPressMask : KeyReleaseMask, &xevent);
- XFlush(dpy);
+#if defined(__linux__) && defined(HAVE_UINPUT_H)
+ case KBEMU_UINPUT:
+ kbemu_uinput_send_key(key, press);
+ break;
+#endif
+
+ default:
+ /* No backend available or not initialized */
+ break;
+ }
}
void send_kbevent_combo(KeySym *keys, int count, int press)
{
- int i;
-
- if(!dpy || count <= 0) return;
-
- if(press) {
- return;
+#ifndef USE_X11
+ /* Auto-initialize if not already done */
+ if(backend == KBEMU_NONE) {
+ kbemu_init_fallback();
}
+#endif
- /* send press events for all keys */
- for(i=0; i.
#ifndef KBEMU_H_
#define KBEMU_H_
+#ifdef USE_X11
#include
#include
void kbemu_set_display(Display *dpy);
+#else
+/* When X11 is not available, define KeySym for API compatibility */
+typedef unsigned long KeySym;
+#endif
+
+/* Initialize keyboard emulation based on config */
+void kbemu_init(void);
+
KeySym kbemu_keysym(const char *str);
const char *kbemu_keyname(KeySym sym);
void send_kbevent(KeySym key, int press);
void send_kbevent_combo(KeySym *keys, int count, int press);
+/* Backend-specific functions */
+#ifdef USE_X11
+int kbemu_x11_init(Display *dpy);
+void kbemu_x11_cleanup(void);
+void kbemu_x11_send_key(KeySym key, int press);
+void kbemu_x11_send_key_combo(KeySym *keys, int count, int press);
+#endif
+
+#if defined(__linux__) && defined(HAVE_UINPUT_H)
+int kbemu_uinput_init(void);
+void kbemu_uinput_cleanup(void);
+void kbemu_uinput_send_key(KeySym key, int press);
+void kbemu_uinput_send_key_combo(KeySym *keys, int count, int press);
+#endif
+
#endif /* KBEMU_H_ */
diff --git a/src/kbemu_uinput.c b/src/kbemu_uinput.c
new file mode 100644
index 0000000..9a1f236
--- /dev/null
+++ b/src/kbemu_uinput.c
@@ -0,0 +1,169 @@
+/*
+spacenavd - a free software replacement driver for 6dof space-mice.
+Copyright (C) 2025 Allin Demopolis
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+#include "config.h"
+
+#if defined(__linux__) && defined(HAVE_UINPUT_H)
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "logger.h"
+#include "kbemu.h"
+#include "keymap.h"
+
+static int uinput_fd = -1;
+
+static void emit_event(int type, int code, int val);
+
+int kbemu_uinput_init(void)
+{
+ int i;
+ struct uinput_setup usetup;
+
+ if(uinput_fd >= 0) {
+ return 0; /* already initialized */
+ }
+
+ /* Open uinput device */
+ if((uinput_fd = open("/dev/uinput", O_WRONLY | O_NONBLOCK)) == -1) {
+ logmsg(LOG_ERR, "failed to open /dev/uinput: %s\n", strerror(errno));
+ logmsg(LOG_ERR, "Make sure you have permissions to access /dev/uinput\n");
+ return -1;
+ }
+
+ /* Enable key events */
+ if(ioctl(uinput_fd, UI_SET_EVBIT, EV_KEY) == -1) {
+ logmsg(LOG_ERR, "UI_SET_EVBIT failed: %s\n", strerror(errno));
+ goto err_close;
+ }
+
+ /* Enable all keyboard keys */
+ for(i = KEY_ESC; i < KEY_MAX; i++) {
+ if(ioctl(uinput_fd, UI_SET_KEYBIT, i) == -1) {
+ /* Some keys may not be supported, continue anyway */
+ }
+ }
+
+ /* Set up the virtual device */
+ memset(&usetup, 0, sizeof usetup);
+ usetup.id.bustype = BUS_VIRTUAL;
+ usetup.id.vendor = 0x1234; /* arbitrary vendor ID */
+ usetup.id.product = 0x5678; /* arbitrary product ID */
+ snprintf(usetup.name, UINPUT_MAX_NAME_SIZE, "spacenavd virtual keyboard");
+
+ if(ioctl(uinput_fd, UI_DEV_SETUP, &usetup) == -1) {
+ logmsg(LOG_ERR, "UI_DEV_SETUP failed: %s\n", strerror(errno));
+ goto err_close;
+ }
+
+ /* Create the device */
+ if(ioctl(uinput_fd, UI_DEV_CREATE) == -1) {
+ logmsg(LOG_ERR, "UI_DEV_CREATE failed: %s\n", strerror(errno));
+ goto err_close;
+ }
+
+ logmsg(LOG_INFO, "uinput virtual keyboard created successfully\n");
+ return 0;
+
+err_close:
+ close(uinput_fd);
+ uinput_fd = -1;
+ return -1;
+}
+
+void kbemu_uinput_cleanup(void)
+{
+ if(uinput_fd >= 0) {
+ ioctl(uinput_fd, UI_DEV_DESTROY);
+ close(uinput_fd);
+ uinput_fd = -1;
+ logmsg(LOG_DEBUG, "uinput virtual keyboard destroyed\n");
+ }
+}
+
+void kbemu_uinput_send_key(KeySym keysym, int press)
+{
+ unsigned int keycode;
+
+ if(uinput_fd < 0) {
+ return;
+ }
+
+ /* Convert X11 KeySym to Linux keycode */
+ keycode = keysym_to_linux_keycode(keysym);
+ if(!keycode) {
+ logmsg(LOG_WARNING, "failed to convert keysym %lu to Linux keycode\n", keysym);
+ return;
+ }
+
+ /* Emit key event */
+ emit_event(EV_KEY, keycode, press ? 1 : 0);
+ emit_event(EV_SYN, SYN_REPORT, 0);
+}
+
+void kbemu_uinput_send_key_combo(KeySym *keys, int count, int press)
+{
+ int i;
+
+ if(uinput_fd < 0 || count <= 0) {
+ return;
+ }
+
+ if(press) {
+ return;
+ }
+
+ /* Send press events for all keys */
+ for(i = 0; i < count; i++) {
+ unsigned int keycode = keysym_to_linux_keycode(keys[i]);
+ if(keycode) {
+ emit_event(EV_KEY, keycode, 1);
+ }
+ }
+ emit_event(EV_SYN, SYN_REPORT, 0);
+
+ /* Send release events in reverse order */
+ for(i = count - 1; i >= 0; i--) {
+ unsigned int keycode = keysym_to_linux_keycode(keys[i]);
+ if(keycode) {
+ emit_event(EV_KEY, keycode, 0);
+ }
+ }
+ emit_event(EV_SYN, SYN_REPORT, 0);
+}
+
+static void emit_event(int type, int code, int val)
+{
+ struct input_event ev;
+
+ memset(&ev, 0, sizeof ev);
+ ev.type = type;
+ ev.code = code;
+ ev.value = val;
+
+ if(write(uinput_fd, &ev, sizeof ev) != sizeof ev) {
+ logmsg(LOG_WARNING, "failed to write uinput event: %s\n", strerror(errno));
+ }
+}
+
+#else
+int spacenavd_kbemu_uinput_shut_up_empty_source_warning;
+#endif /* __linux__ && HAVE_UINPUT_H */
diff --git a/src/kbemu_x11.c b/src/kbemu_x11.c
new file mode 100644
index 0000000..f93884c
--- /dev/null
+++ b/src/kbemu_x11.c
@@ -0,0 +1,123 @@
+/*
+spacenavd - a free software replacement driver for 6dof space-mice.
+Copyright (C) 2025 Allin Demopolis
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+#include "config.h"
+
+#ifdef USE_X11
+#include
+#include
+#include "logger.h"
+#include "kbemu.h"
+
+#ifdef HAVE_XTEST_H
+#include
+static int use_xtest;
+#endif
+
+static Display *dpy;
+
+int kbemu_x11_init(Display *d)
+{
+ if(!d) {
+ return -1;
+ }
+
+ dpy = d;
+
+#ifdef HAVE_XTEST_H
+ int tmp;
+ use_xtest = XTestQueryExtension(dpy, &tmp, &tmp, &tmp, &tmp);
+
+ if(use_xtest) {
+ logmsg(LOG_DEBUG, "Using XTEST to send key events\n");
+ } else
+#endif
+ logmsg(LOG_DEBUG, "Using XSendEvent to send key events\n");
+
+ return 0;
+}
+
+void kbemu_x11_cleanup(void)
+{
+ dpy = 0;
+}
+
+void kbemu_x11_send_key(KeySym key, int press)
+{
+ XEvent xevent;
+ Window win;
+ int rev_state;
+ KeyCode kc;
+
+ if(!dpy) return;
+
+ kc = XKeysymToKeycode(dpy, key);
+ if(!kc) {
+ logmsg(LOG_WARNING, "failed to convert keysym %lu to keycode\n", key);
+ return;
+ }
+
+#ifdef HAVE_XTEST_H
+ if(use_xtest) {
+ XTestFakeKeyEvent(dpy, kc, press, 0);
+ XFlush(dpy);
+ return;
+ }
+#endif
+
+ XGetInputFocus(dpy, &win, &rev_state);
+
+ xevent.type = press ? KeyPress : KeyRelease;
+ xevent.xkey.display = dpy;
+ xevent.xkey.root = DefaultRootWindow(dpy);
+ xevent.xkey.window = win;
+ xevent.xkey.subwindow = None;
+ xevent.xkey.keycode = kc;
+ xevent.xkey.state = 0;
+ xevent.xkey.time = CurrentTime;
+ xevent.xkey.x = xevent.xkey.y = 1;
+ xevent.xkey.x_root = xevent.xkey.y_root = 1;
+
+ XSendEvent(dpy, win, True, press ? KeyPressMask : KeyReleaseMask, &xevent);
+ XFlush(dpy);
+}
+
+void kbemu_x11_send_key_combo(KeySym *keys, int count, int press)
+{
+ int i;
+
+ if(!dpy || count <= 0) return;
+
+ if(press) {
+ return;
+ }
+
+ /* send press events for all keys */
+ for(i=0; i=0; i--) {
+ kbemu_x11_send_key(keys[i], 0);
+ }
+}
+
+#else
+int spacenavd_kbemu_shut_up_empty_source_warning;
+#endif /* USE_X11 */
diff --git a/src/keymap.c b/src/keymap.c
new file mode 100644
index 0000000..b3a2a7a
--- /dev/null
+++ b/src/keymap.c
@@ -0,0 +1,146 @@
+/*
+spacenavd - a free software replacement driver for 6dof space-mice.
+Copyright (C) 2025 Allin Demopolis
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+#include "config.h"
+
+#ifdef __linux__
+#include
+#include
+#include "keymap.h"
+
+struct keymap_entry {
+ unsigned long xkey;
+ unsigned int linkey;
+};
+
+static struct keymap_entry keymap[] = {
+ /* Alphanumeric keys */
+ {0x0061, KEY_A}, /* XK_a */
+ {0x0062, KEY_B}, /* XK_b */
+ {0x0063, KEY_C}, /* XK_c */
+ {0x0064, KEY_D}, /* XK_d */
+ {0x0065, KEY_E}, /* XK_e */
+ {0x0066, KEY_F}, /* XK_f */
+ {0x0067, KEY_G}, /* XK_g */
+ {0x0068, KEY_H}, /* XK_h */
+ {0x0069, KEY_I}, /* XK_i */
+ {0x006a, KEY_J}, /* XK_j */
+ {0x006b, KEY_K}, /* XK_k */
+ {0x006c, KEY_L}, /* XK_l */
+ {0x006d, KEY_M}, /* XK_m */
+ {0x006e, KEY_N}, /* XK_n */
+ {0x006f, KEY_O}, /* XK_o */
+ {0x0070, KEY_P}, /* XK_p */
+ {0x0071, KEY_Q}, /* XK_q */
+ {0x0072, KEY_R}, /* XK_r */
+ {0x0073, KEY_S}, /* XK_s */
+ {0x0074, KEY_T}, /* XK_t */
+ {0x0075, KEY_U}, /* XK_u */
+ {0x0076, KEY_V}, /* XK_v */
+ {0x0077, KEY_W}, /* XK_w */
+ {0x0078, KEY_X}, /* XK_x */
+ {0x0079, KEY_Y}, /* XK_y */
+ {0x007a, KEY_Z}, /* XK_z */
+
+ /* Number keys */
+ {0x0030, KEY_0}, /* XK_0 */
+ {0x0031, KEY_1}, /* XK_1 */
+ {0x0032, KEY_2}, /* XK_2 */
+ {0x0033, KEY_3}, /* XK_3 */
+ {0x0034, KEY_4}, /* XK_4 */
+ {0x0035, KEY_5}, /* XK_5 */
+ {0x0036, KEY_6}, /* XK_6 */
+ {0x0037, KEY_7}, /* XK_7 */
+ {0x0038, KEY_8}, /* XK_8 */
+ {0x0039, KEY_9}, /* XK_9 */
+
+ /* Function keys */
+ {0xffbe, KEY_F1}, /* XK_F1 */
+ {0xffbf, KEY_F2}, /* XK_F2 */
+ {0xffc0, KEY_F3}, /* XK_F3 */
+ {0xffc1, KEY_F4}, /* XK_F4 */
+ {0xffc2, KEY_F5}, /* XK_F5 */
+ {0xffc3, KEY_F6}, /* XK_F6 */
+ {0xffc4, KEY_F7}, /* XK_F7 */
+ {0xffc5, KEY_F8}, /* XK_F8 */
+ {0xffc6, KEY_F9}, /* XK_F9 */
+ {0xffc7, KEY_F10}, /* XK_F10 */
+ {0xffc8, KEY_F11}, /* XK_F11 */
+ {0xffc9, KEY_F12}, /* XK_F12 */
+
+ /* Modifier keys */
+ {0xffe1, KEY_LEFTSHIFT}, /* XK_Shift_L */
+ {0xffe2, KEY_RIGHTSHIFT}, /* XK_Shift_R */
+ {0xffe3, KEY_LEFTCTRL}, /* XK_Control_L */
+ {0xffe4, KEY_RIGHTCTRL}, /* XK_Control_R */
+ {0xffe5, KEY_CAPSLOCK}, /* XK_Caps_Lock */
+ {0xffe7, KEY_LEFTMETA}, /* XK_Meta_L */
+ {0xffe8, KEY_RIGHTMETA}, /* XK_Meta_R */
+ {0xffe9, KEY_LEFTALT}, /* XK_Alt_L */
+ {0xffea, KEY_RIGHTALT}, /* XK_Alt_R */
+ {0xffeb, KEY_LEFTMETA}, /* XK_Super_L */
+ {0xffec, KEY_RIGHTMETA}, /* XK_Super_R */
+
+ /* Special keys */
+ {0xff1b, KEY_ESC}, /* XK_Escape */
+ {0xff08, KEY_BACKSPACE}, /* XK_BackSpace */
+ {0xff09, KEY_TAB}, /* XK_Tab */
+ {0xff0d, KEY_ENTER}, /* XK_Return */
+ {0x0020, KEY_SPACE}, /* XK_space */
+ {0xffff, KEY_DELETE}, /* XK_Delete */
+ {0xff63, KEY_INSERT}, /* XK_Insert */
+ {0xff50, KEY_HOME}, /* XK_Home */
+ {0xff57, KEY_END}, /* XK_End */
+ {0xff55, KEY_PAGEUP}, /* XK_Page_Up */
+ {0xff56, KEY_PAGEDOWN}, /* XK_Page_Down */
+
+ /* Arrow keys */
+ {0xff51, KEY_LEFT}, /* XK_Left */
+ {0xff52, KEY_UP}, /* XK_Up */
+ {0xff53, KEY_RIGHT}, /* XK_Right */
+ {0xff54, KEY_DOWN}, /* XK_Down */
+
+ /* Punctuation and symbols */
+ {0x0027, KEY_APOSTROPHE}, /* XK_apostrophe ' */
+ {0x002c, KEY_COMMA}, /* XK_comma , */
+ {0x002d, KEY_MINUS}, /* XK_minus - */
+ {0x002e, KEY_DOT}, /* XK_period . */
+ {0x002f, KEY_SLASH}, /* XK_slash / */
+ {0x003b, KEY_SEMICOLON}, /* XK_semicolon ; */
+ {0x003d, KEY_EQUAL}, /* XK_equal = */
+ {0x005b, KEY_LEFTBRACE}, /* XK_bracketleft [ */
+ {0x005c, KEY_BACKSLASH}, /* XK_backslash \ */
+ {0x005d, KEY_RIGHTBRACE}, /* XK_bracketright ] */
+ {0x0060, KEY_GRAVE}, /* XK_grave ` */
+
+ {0, 0} /* terminator */
+};
+
+unsigned int keysym_to_linux_keycode(unsigned long sym)
+{
+ int i;
+
+ for(i = 0; keymap[i].xkey; i++) {
+ if(keymap[i].xkey == sym) {
+ return keymap[i].linkey;
+ }
+ }
+
+ return 0; /* not found */
+}
+
+#endif /* __linux__ */
diff --git a/src/keymap.h b/src/keymap.h
new file mode 100644
index 0000000..f7d442d
--- /dev/null
+++ b/src/keymap.h
@@ -0,0 +1,37 @@
+/*
+spacenavd - a free software replacement driver for 6dof space-mice.
+Copyright (C) 2025 Allin Demopolis
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+#ifndef KEYMAP_H_
+#define KEYMAP_H_
+
+#ifdef USE_X11
+#include
+#else
+/* Define minimal X11 KeySym values when X11 is not available */
+typedef unsigned long KeySym;
+#define XK_VoidSymbol 0xffffff
+#endif
+
+#ifdef __linux__
+
+/* Map X11 KeySym to Linux KEY_* code
+ * Note: KeySym is defined above, either from X11 or as typedef */
+unsigned int keysym_to_linux_keycode(unsigned long sym);
+
+#endif /* __linux__ */
+
+#endif /* KEYMAP_H_ */
diff --git a/src/spnavd.c b/src/spnavd.c
index aae783a..fdb58fe 100644
--- a/src/spnavd.c
+++ b/src/spnavd.c
@@ -34,6 +34,7 @@ along with this program. If not, see .
#include "hotplug.h"
#include "client.h"
#include "proto_unix.h"
+#include "kbemu.h"
#ifdef USE_X11
#include "proto_x11.h"
#endif
@@ -162,6 +163,10 @@ opt_pidfile: if(!argv[++i]) {
read_cfg(cfgfile, &cfg);
prev_cfg = cfg;
+
+ /* Initialize keyboard emulation backend based on config */
+ kbemu_init();
+
pipe(pfd);
signal(SIGINT, sig_handler);