From 02a3d1859abd811b7a208d97889ef3c81d41206e Mon Sep 17 00:00:00 2001 From: Allin Demopolis Date: Thu, 27 Nov 2025 17:18:26 -0500 Subject: [PATCH 1/4] Implement uinput support for Wayland --- configure | 9 ++ src/cfgfile.c | 16 ++- src/cfgfile.h | 7 ++ src/kbemu.c | 249 +++++++++++++++++++++++++++++++++------------ src/kbemu.h | 24 +++++ src/kbemu_uinput.c | 169 ++++++++++++++++++++++++++++++ src/kbemu_x11.c | 123 ++++++++++++++++++++++ src/keymap.c | 193 +++++++++++++++++++++++++++++++++++ src/keymap.h | 37 +++++++ src/spnavd.c | 5 + 10 files changed, 767 insertions(+), 65 deletions(-) create mode 100644 src/kbemu_uinput.c create mode 100644 src/kbemu_x11.c create mode 100644 src/keymap.c create mode 100644 src/keymap.h 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..e5e0013 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_KBEMU_BACKEND, 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_backend = KBEMU_BACKEND_AUTO; for(i=0; i<6; i++) { cfg->map_axis[i] = i; @@ -426,6 +427,19 @@ int read_cfg(const char *fname, struct cfg *cfg) } } + } else if(strcmp(key_str, "kbemu-backend") == 0) { + lptr->opt = CFG_KBEMU_BACKEND; + if(strcmp(val_str, "auto") == 0) { + cfg->kbemu_backend = KBEMU_BACKEND_AUTO; + } else if(strcmp(val_str, "x11") == 0) { + cfg->kbemu_backend = KBEMU_BACKEND_X11; + } else if(strcmp(val_str, "uinput") == 0) { + cfg->kbemu_backend = KBEMU_BACKEND_UINPUT; + } else { + logmsg(LOG_WARNING, "invalid configuration value for %s, expected auto, x11, or uinput.\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..c5c6bce 100644 --- a/src/cfgfile.h +++ b/src/cfgfile.h @@ -32,6 +32,12 @@ enum { LED_AUTO = 2 }; +enum { + KBEMU_BACKEND_AUTO, /* auto-detect based on display server */ + KBEMU_BACKEND_X11, /* force X11 backend */ + KBEMU_BACKEND_UINPUT /* force uinput backend */ +}; + /* button actions (XXX: must correspond to SPNAV_BNACT_* in libspnav) */ enum { BNACT_NONE, @@ -57,6 +63,7 @@ struct cfg { char *kbmap_str[MAX_BUTTONS]; int swapyz; int led, grab_device; + int kbemu_backend; char serial_dev[PATH_MAX]; int repeat_msec; diff --git a/src/kbemu.c b/src/kbemu.c index 115f78c..341e11f 100644 --- a/src/kbemu.c +++ b/src/kbemu.c @@ -15,108 +15,229 @@ 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) + /* If config forces uinput, initialize it now */ + if(cfg.kbemu_backend == KBEMU_BACKEND_UINPUT) { + if(backend == KBEMU_NONE) { + logmsg(LOG_INFO, "Config forces uinput backend, initializing...\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, don't override */ + if(backend != KBEMU_NONE) { + logmsg(LOG_DEBUG, "kbemu backend already initialized\n"); + return; + } + + /* Check config file for forced backend selection */ +#if defined(__linux__) && defined(HAVE_UINPUT_H) + if(cfg.kbemu_backend == KBEMU_BACKEND_UINPUT) { + logmsg(LOG_INFO, "Config forces uinput backend\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, falling back to X11\n"); + } +#endif - if(d) { -#ifdef HAVE_XTEST_H - int tmp; - use_xtest = XTestQueryExtension(dpy, &tmp, &tmp, &tmp, &tmp); + if(cfg.kbemu_backend == KBEMU_BACKEND_X11) { + logmsg(LOG_INFO, "Config forces X11 backend\n"); + /* Skip auto-detection, go straight to X11 */ + goto use_x11; + } - if(use_xtest) - logmsg(LOG_DEBUG, "Using XTEST to send key events\n"); - else + /* Auto-detect: On Wayland, prefer uinput over X11 (XWayland) + * X11 through XWayland only works for X11 apps, not native Wayland apps. + * uinput works universally for all apps on Wayland. + */ +#if defined(__linux__) && defined(HAVE_UINPUT_H) + if(cfg.kbemu_backend == KBEMU_BACKEND_AUTO) { + int is_wayland = 0; + char *session_type, *wayland_display; + + /* Check multiple indicators of Wayland session */ + session_type = getenv("XDG_SESSION_TYPE"); + wayland_display = getenv("WAYLAND_DISPLAY"); + + logmsg(LOG_DEBUG, "Auto-detecting display server: XDG_SESSION_TYPE=%s, WAYLAND_DISPLAY=%s\n", + session_type ? session_type : "unset", + wayland_display ? wayland_display : "unset"); + + if((session_type && strcmp(session_type, "wayland") == 0) || wayland_display) { + is_wayland = 1; + logmsg(LOG_INFO, "Detected Wayland session\n"); + } + + if(is_wayland) { + logmsg(LOG_INFO, "Trying uinput backend for Wayland compatibility\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, falling back to X11\n"); + } else { + logmsg(LOG_DEBUG, "No Wayland session detected, will use X11 backend\n"); + } + } #endif - logmsg(LOG_DEBUG, "Using XSendEvent to send key events\n"); + +use_x11: + + /* Use X11 backend (either on native X11 or as fallback on Wayland) */ + if(kbemu_x11_init(dpy) == 0) { + backend = KBEMU_X11; + logmsg(LOG_INFO, "Using X11 keyboard emulation backend\n"); + return; } + + 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 + +#if defined(__linux__) && defined(HAVE_UINPUT_H) + case KBEMU_UINPUT: + kbemu_uinput_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); + 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 (call after reading 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 (internal use) */ +#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..ac2f7c8 --- /dev/null +++ b/src/keymap.c @@ -0,0 +1,193 @@ +/* +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 */ + {0x0041, KEY_A}, /* XK_A */ + {0x0042, KEY_B}, /* XK_B */ + {0x0043, KEY_C}, /* XK_C */ + {0x0044, KEY_D}, /* XK_D */ + {0x0045, KEY_E}, /* XK_E */ + {0x0046, KEY_F}, /* XK_F */ + {0x0047, KEY_G}, /* XK_G */ + {0x0048, KEY_H}, /* XK_H */ + {0x0049, KEY_I}, /* XK_I */ + {0x004a, KEY_J}, /* XK_J */ + {0x004b, KEY_K}, /* XK_K */ + {0x004c, KEY_L}, /* XK_L */ + {0x004d, KEY_M}, /* XK_M */ + {0x004e, KEY_N}, /* XK_N */ + {0x004f, KEY_O}, /* XK_O */ + {0x0050, KEY_P}, /* XK_P */ + {0x0051, KEY_Q}, /* XK_Q */ + {0x0052, KEY_R}, /* XK_R */ + {0x0053, KEY_S}, /* XK_S */ + {0x0054, KEY_T}, /* XK_T */ + {0x0055, KEY_U}, /* XK_U */ + {0x0056, KEY_V}, /* XK_V */ + {0x0057, KEY_W}, /* XK_W */ + {0x0058, KEY_X}, /* XK_X */ + {0x0059, KEY_Y}, /* XK_Y */ + {0x005a, 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 */ + {0x0021, KEY_1}, /* XK_exclam ! */ + {0x0022, KEY_APOSTROPHE}, /* XK_quotedbl " */ + {0x0023, KEY_3}, /* XK_numbersign # */ + {0x0024, KEY_4}, /* XK_dollar $ */ + {0x0025, KEY_5}, /* XK_percent % */ + {0x0026, KEY_7}, /* XK_ampersand & */ + {0x0027, KEY_APOSTROPHE}, /* XK_apostrophe ' */ + {0x0028, KEY_9}, /* XK_parenleft ( */ + {0x0029, KEY_0}, /* XK_parenright ) */ + {0x002a, KEY_8}, /* XK_asterisk * */ + {0x002b, KEY_EQUAL}, /* XK_plus + */ + {0x002c, KEY_COMMA}, /* XK_comma , */ + {0x002d, KEY_MINUS}, /* XK_minus - */ + {0x002e, KEY_DOT}, /* XK_period . */ + {0x002f, KEY_SLASH}, /* XK_slash / */ + {0x003a, KEY_SEMICOLON}, /* XK_colon : */ + {0x003b, KEY_SEMICOLON}, /* XK_semicolon ; */ + {0x003c, KEY_COMMA}, /* XK_less < */ + {0x003d, KEY_EQUAL}, /* XK_equal = */ + {0x003e, KEY_DOT}, /* XK_greater > */ + {0x003f, KEY_SLASH}, /* XK_question ? */ + {0x0040, KEY_2}, /* XK_at @ */ + {0x005b, KEY_LEFTBRACE}, /* XK_bracketleft [ */ + {0x005c, KEY_BACKSLASH}, /* XK_backslash \ */ + {0x005d, KEY_RIGHTBRACE}, /* XK_bracketright ] */ + {0x005e, KEY_6}, /* XK_asciicircum ^ */ + {0x005f, KEY_MINUS}, /* XK_underscore _ */ + {0x0060, KEY_GRAVE}, /* XK_grave ` */ + {0x007b, KEY_LEFTBRACE}, /* XK_braceleft { */ + {0x007c, KEY_BACKSLASH}, /* XK_bar | */ + {0x007d, KEY_RIGHTBRACE}, /* XK_braceright } */ + {0x007e, KEY_GRAVE}, /* XK_asciitilde ~ */ + + {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); From 7e44c70a7bcbd517bd2035b22bf4391474544c5f Mon Sep 17 00:00:00 2001 From: Allin Demopolis Date: Thu, 27 Nov 2025 17:55:32 -0500 Subject: [PATCH 2/4] remove duplicate keys, accessible from shift --- src/keymap.c | 47 ----------------------------------------------- 1 file changed, 47 deletions(-) diff --git a/src/keymap.c b/src/keymap.c index ac2f7c8..b3a2a7a 100644 --- a/src/keymap.c +++ b/src/keymap.c @@ -55,32 +55,6 @@ static struct keymap_entry keymap[] = { {0x0078, KEY_X}, /* XK_x */ {0x0079, KEY_Y}, /* XK_y */ {0x007a, KEY_Z}, /* XK_z */ - {0x0041, KEY_A}, /* XK_A */ - {0x0042, KEY_B}, /* XK_B */ - {0x0043, KEY_C}, /* XK_C */ - {0x0044, KEY_D}, /* XK_D */ - {0x0045, KEY_E}, /* XK_E */ - {0x0046, KEY_F}, /* XK_F */ - {0x0047, KEY_G}, /* XK_G */ - {0x0048, KEY_H}, /* XK_H */ - {0x0049, KEY_I}, /* XK_I */ - {0x004a, KEY_J}, /* XK_J */ - {0x004b, KEY_K}, /* XK_K */ - {0x004c, KEY_L}, /* XK_L */ - {0x004d, KEY_M}, /* XK_M */ - {0x004e, KEY_N}, /* XK_N */ - {0x004f, KEY_O}, /* XK_O */ - {0x0050, KEY_P}, /* XK_P */ - {0x0051, KEY_Q}, /* XK_Q */ - {0x0052, KEY_R}, /* XK_R */ - {0x0053, KEY_S}, /* XK_S */ - {0x0054, KEY_T}, /* XK_T */ - {0x0055, KEY_U}, /* XK_U */ - {0x0056, KEY_V}, /* XK_V */ - {0x0057, KEY_W}, /* XK_W */ - {0x0058, KEY_X}, /* XK_X */ - {0x0059, KEY_Y}, /* XK_Y */ - {0x005a, KEY_Z}, /* XK_Z */ /* Number keys */ {0x0030, KEY_0}, /* XK_0 */ @@ -141,38 +115,17 @@ static struct keymap_entry keymap[] = { {0xff54, KEY_DOWN}, /* XK_Down */ /* Punctuation and symbols */ - {0x0021, KEY_1}, /* XK_exclam ! */ - {0x0022, KEY_APOSTROPHE}, /* XK_quotedbl " */ - {0x0023, KEY_3}, /* XK_numbersign # */ - {0x0024, KEY_4}, /* XK_dollar $ */ - {0x0025, KEY_5}, /* XK_percent % */ - {0x0026, KEY_7}, /* XK_ampersand & */ {0x0027, KEY_APOSTROPHE}, /* XK_apostrophe ' */ - {0x0028, KEY_9}, /* XK_parenleft ( */ - {0x0029, KEY_0}, /* XK_parenright ) */ - {0x002a, KEY_8}, /* XK_asterisk * */ - {0x002b, KEY_EQUAL}, /* XK_plus + */ {0x002c, KEY_COMMA}, /* XK_comma , */ {0x002d, KEY_MINUS}, /* XK_minus - */ {0x002e, KEY_DOT}, /* XK_period . */ {0x002f, KEY_SLASH}, /* XK_slash / */ - {0x003a, KEY_SEMICOLON}, /* XK_colon : */ {0x003b, KEY_SEMICOLON}, /* XK_semicolon ; */ - {0x003c, KEY_COMMA}, /* XK_less < */ {0x003d, KEY_EQUAL}, /* XK_equal = */ - {0x003e, KEY_DOT}, /* XK_greater > */ - {0x003f, KEY_SLASH}, /* XK_question ? */ - {0x0040, KEY_2}, /* XK_at @ */ {0x005b, KEY_LEFTBRACE}, /* XK_bracketleft [ */ {0x005c, KEY_BACKSLASH}, /* XK_backslash \ */ {0x005d, KEY_RIGHTBRACE}, /* XK_bracketright ] */ - {0x005e, KEY_6}, /* XK_asciicircum ^ */ - {0x005f, KEY_MINUS}, /* XK_underscore _ */ {0x0060, KEY_GRAVE}, /* XK_grave ` */ - {0x007b, KEY_LEFTBRACE}, /* XK_braceleft { */ - {0x007c, KEY_BACKSLASH}, /* XK_bar | */ - {0x007d, KEY_RIGHTBRACE}, /* XK_braceright } */ - {0x007e, KEY_GRAVE}, /* XK_asciitilde ~ */ {0, 0} /* terminator */ }; From eae3355c152ba69b7747a16a497bde62d8f54f5e Mon Sep 17 00:00:00 2001 From: Allin Demopolis Date: Thu, 27 Nov 2025 18:20:26 -0500 Subject: [PATCH 3/4] clean up some comments --- src/kbemu.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/kbemu.h b/src/kbemu.h index 204a39c..0ada340 100644 --- a/src/kbemu.h +++ b/src/kbemu.h @@ -28,7 +28,7 @@ void kbemu_set_display(Display *dpy); typedef unsigned long KeySym; #endif -/* Initialize keyboard emulation based on config (call after reading config) */ +/* Initialize keyboard emulation based on config */ void kbemu_init(void); KeySym kbemu_keysym(const char *str); @@ -37,7 +37,7 @@ 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 (internal use) */ +/* Backend-specific functions */ #ifdef USE_X11 int kbemu_x11_init(Display *dpy); void kbemu_x11_cleanup(void); From 8175a0a435e0bcf35cc91d6a96bb14d12094a388 Mon Sep 17 00:00:00 2001 From: Allin Demopolis Date: Sat, 29 Nov 2025 18:47:07 -0500 Subject: [PATCH 4/4] reconfigured flag to default to uinput if installed, option to force x11 --- src/cfgfile.c | 26 +++++++++++--------- src/cfgfile.h | 8 +----- src/kbemu.c | 68 ++++++--------------------------------------------- 3 files changed, 22 insertions(+), 80 deletions(-) diff --git a/src/cfgfile.c b/src/cfgfile.c index e5e0013..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_KBEMU_BACKEND, + CFG_LED, CFG_GRAB, CFG_KBMAP_USE_X11, CFG_SERIAL, CFG_DEVID, NUM_CFG_OPTIONS @@ -96,7 +96,7 @@ void default_cfg(struct cfg *cfg) cfg->led = LED_ON; cfg->grab_device = 1; - cfg->kbemu_backend = KBEMU_BACKEND_AUTO; + cfg->kbemu_use_x11 = 0; /* default to uinput when available */ for(i=0; i<6; i++) { cfg->map_axis[i] = i; @@ -427,17 +427,19 @@ int read_cfg(const char *fname, struct cfg *cfg) } } - } else if(strcmp(key_str, "kbemu-backend") == 0) { - lptr->opt = CFG_KBEMU_BACKEND; - if(strcmp(val_str, "auto") == 0) { - cfg->kbemu_backend = KBEMU_BACKEND_AUTO; - } else if(strcmp(val_str, "x11") == 0) { - cfg->kbemu_backend = KBEMU_BACKEND_X11; - } else if(strcmp(val_str, "uinput") == 0) { - cfg->kbemu_backend = KBEMU_BACKEND_UINPUT; + } else if(strcmp(key_str, "kbmap_use_x11") == 0) { + lptr->opt = CFG_KBMAP_USE_X11; + if(isint) { + cfg->kbemu_use_x11 = ival; } else { - logmsg(LOG_WARNING, "invalid configuration value for %s, expected auto, x11, or uinput.\n", key_str); - continue; + 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) { diff --git a/src/cfgfile.h b/src/cfgfile.h index c5c6bce..f0ddf1c 100644 --- a/src/cfgfile.h +++ b/src/cfgfile.h @@ -32,12 +32,6 @@ enum { LED_AUTO = 2 }; -enum { - KBEMU_BACKEND_AUTO, /* auto-detect based on display server */ - KBEMU_BACKEND_X11, /* force X11 backend */ - KBEMU_BACKEND_UINPUT /* force uinput backend */ -}; - /* button actions (XXX: must correspond to SPNAV_BNACT_* in libspnav) */ enum { BNACT_NONE, @@ -63,7 +57,7 @@ struct cfg { char *kbmap_str[MAX_BUTTONS]; int swapyz; int led, grab_device; - int kbemu_backend; + int kbemu_use_x11; char serial_dev[PATH_MAX]; int repeat_msec; diff --git a/src/kbemu.c b/src/kbemu.c index 341e11f..75badf7 100644 --- a/src/kbemu.c +++ b/src/kbemu.c @@ -37,10 +37,10 @@ extern struct cfg cfg; void kbemu_init(void) { #if defined(__linux__) && defined(HAVE_UINPUT_H) - /* If config forces uinput, initialize it now */ - if(cfg.kbemu_backend == KBEMU_BACKEND_UINPUT) { + /* For uinput builds, default to uinput unless kbmap_use_x11 is set */ + if(!cfg.kbemu_use_x11) { if(backend == KBEMU_NONE) { - logmsg(LOG_INFO, "Config forces uinput backend, initializing...\n"); + 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"); @@ -66,70 +66,16 @@ void kbemu_set_display(Display *dpy) return; } - /* If we already have a backend, don't override */ + /* 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; } - /* Check config file for forced backend selection */ -#if defined(__linux__) && defined(HAVE_UINPUT_H) - if(cfg.kbemu_backend == KBEMU_BACKEND_UINPUT) { - logmsg(LOG_INFO, "Config forces uinput backend\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, falling back to X11\n"); - } -#endif - - if(cfg.kbemu_backend == KBEMU_BACKEND_X11) { - logmsg(LOG_INFO, "Config forces X11 backend\n"); - /* Skip auto-detection, go straight to X11 */ - goto use_x11; - } - - /* Auto-detect: On Wayland, prefer uinput over X11 (XWayland) - * X11 through XWayland only works for X11 apps, not native Wayland apps. - * uinput works universally for all apps on Wayland. + /* 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 defined(__linux__) && defined(HAVE_UINPUT_H) - if(cfg.kbemu_backend == KBEMU_BACKEND_AUTO) { - int is_wayland = 0; - char *session_type, *wayland_display; - - /* Check multiple indicators of Wayland session */ - session_type = getenv("XDG_SESSION_TYPE"); - wayland_display = getenv("WAYLAND_DISPLAY"); - - logmsg(LOG_DEBUG, "Auto-detecting display server: XDG_SESSION_TYPE=%s, WAYLAND_DISPLAY=%s\n", - session_type ? session_type : "unset", - wayland_display ? wayland_display : "unset"); - - if((session_type && strcmp(session_type, "wayland") == 0) || wayland_display) { - is_wayland = 1; - logmsg(LOG_INFO, "Detected Wayland session\n"); - } - - if(is_wayland) { - logmsg(LOG_INFO, "Trying uinput backend for Wayland compatibility\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, falling back to X11\n"); - } else { - logmsg(LOG_DEBUG, "No Wayland session detected, will use X11 backend\n"); - } - } -#endif - -use_x11: - - /* Use X11 backend (either on native X11 or as fallback on Wayland) */ if(kbemu_x11_init(dpy) == 0) { backend = KBEMU_X11; logmsg(LOG_INFO, "Using X11 keyboard emulation backend\n");