diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 42805fbc..084ec55b 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -124,6 +124,7 @@ set(SOURCES src/eglutil.c src/overlay_utils.c src/render_queue.c + src/evdev.c src/overlay/splash.c src/overlay/alert.c diff --git a/client/displayservers/Wayland/input.c b/client/displayservers/Wayland/input.c index 52d7243e..207d5296 100644 --- a/client/displayservers/Wayland/input.c +++ b/client/displayservers/Wayland/input.c @@ -209,7 +209,7 @@ done: close(fd); } -static int getCharcode(uint32_t key) +int waylandGetCharCode(int key) { key += 8; // xkb scancode is evdev scancode + 8 xkb_keysym_t sym = xkb_state_key_get_one_sym(wlWm.xkbState, key); @@ -232,7 +232,7 @@ static void keyboardEnterHandler(void * data, struct wl_keyboard * keyboard, uint32_t * key; wl_array_for_each(key, keys) - app_handleKeyPress(*key, getCharcode(*key)); + app_handleKeyPress(*key); } static void keyboardLeaveHandler(void * data, struct wl_keyboard * keyboard, @@ -253,9 +253,9 @@ static void keyboardKeyHandler(void * data, struct wl_keyboard * keyboard, return; if (state == WL_KEYBOARD_KEY_STATE_PRESSED) - app_handleKeyPress(key, getCharcode(key)); + app_handleKeyPress(key); else - app_handleKeyRelease(key, getCharcode(key)); + app_handleKeyRelease(key); if (!wlWm.xkbState || !app_isOverlayMode() || state != WL_KEYBOARD_KEY_STATE_PRESSED) return; diff --git a/client/displayservers/Wayland/wayland.c b/client/displayservers/Wayland/wayland.c index df776062..da64c26e 100644 --- a/client/displayservers/Wayland/wayland.c +++ b/client/displayservers/Wayland/wayland.c @@ -263,6 +263,7 @@ struct LG_DisplayServerOps LGDS_Wayland = .uncapturePointer = waylandUncapturePointer, .grabKeyboard = waylandGrabKeyboard, .ungrabKeyboard = waylandUngrabKeyboard, + .getCharCode = waylandGetCharCode, .warpPointer = waylandWarpPointer, .realignPointer = waylandRealignPointer, .isValidPointerPos = waylandIsValidPointerPos, diff --git a/client/displayservers/Wayland/wayland.h b/client/displayservers/Wayland/wayland.h index 928e29fe..bca7d082 100644 --- a/client/displayservers/Wayland/wayland.h +++ b/client/displayservers/Wayland/wayland.h @@ -277,6 +277,7 @@ void waylandUncapturePointer(void); void waylandRealignPointer(void); void waylandWarpPointer(int x, int y, bool exiting); void waylandGuestPointerUpdated(double x, double y, double localX, double localY); +int waylandGetCharCode(int key); // output module bool waylandOutputInit(void); diff --git a/client/displayservers/X11/x11.c b/client/displayservers/X11/x11.c index 82b2afc2..8ad14a09 100644 --- a/client/displayservers/X11/x11.c +++ b/client/displayservers/X11/x11.c @@ -1087,8 +1087,9 @@ static void setFocus(bool focused, double x, double y) app_handleFocusEvent(focused); } -static int getCharcode(int detail) +static int x11GetCharCode(int detail) { + detail += x11.minKeycode; if (detail < x11.minKeycode || detail > x11.maxKeycode) return 0; @@ -1229,8 +1230,7 @@ static void x11XInputEvent(XGenericEventCookie *cookie) return; XIDeviceEvent *device = cookie->data; - app_handleKeyPress(device->detail - x11.minKeycode, - getCharcode(device->detail)); + app_handleKeyPress(device->detail - x11.minKeycode); if (!x11.xic || !app_isOverlayMode()) return; @@ -1280,8 +1280,7 @@ static void x11XInputEvent(XGenericEventCookie *cookie) return; XIDeviceEvent *device = cookie->data; - app_handleKeyRelease(device->detail - x11.minKeycode, - getCharcode(device->detail)); + app_handleKeyRelease(device->detail - x11.minKeycode); if (!x11.xic || !app_isOverlayMode()) return; @@ -1310,8 +1309,7 @@ static void x11XInputEvent(XGenericEventCookie *cookie) return; XIRawEvent *raw = cookie->data; - app_handleKeyPress(raw->detail - x11.minKeycode, - getCharcode(raw->detail)); + app_handleKeyPress(raw->detail - x11.minKeycode); return; } @@ -1321,8 +1319,7 @@ static void x11XInputEvent(XGenericEventCookie *cookie) return; XIRawEvent *raw = cookie->data; - app_handleKeyRelease(raw->detail - x11.minKeycode, - getCharcode(raw->detail)); + app_handleKeyRelease(raw->detail - x11.minKeycode); return; } @@ -2017,6 +2014,7 @@ struct LG_DisplayServerOps LGDS_X11 = .ungrabPointer = x11UngrabPointer, .capturePointer = x11CapturePointer, .uncapturePointer = x11UncapturePointer, + .getCharCode = x11GetCharCode, .grabKeyboard = x11GrabKeyboard, .ungrabKeyboard = x11UngrabKeyboard, .warpPointer = x11WarpPointer, diff --git a/client/include/app.h b/client/include/app.h index dfc9fdae..bfba0632 100644 --- a/client/include/app.h +++ b/client/include/app.h @@ -59,8 +59,8 @@ void app_handleButtonPress(int button); void app_handleButtonRelease(int button); void app_handleWheelMotion(double motion); void app_handleKeyboardTyped(const char * typed); -void app_handleKeyPress(int scancode, int charcode); -void app_handleKeyRelease(int scancode, int charcode); +void app_handleKeyPress(int scancode); +void app_handleKeyRelease(int scancode); void app_handleKeyboardModifiers(bool ctrl, bool shift, bool alt, bool super); void app_handleKeyboardLEDs(bool numLock, bool capsLock, bool scrollLock); void app_handleEnterEvent(bool entered); diff --git a/client/include/evdev.h b/client/include/evdev.h new file mode 100644 index 00000000..03c74b3a --- /dev/null +++ b/client/include/evdev.h @@ -0,0 +1,46 @@ +/** + * Looking Glass + * Copyright © 2017-2025 The Looking Glass Authors + * https://looking-glass.io + * + * 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 2 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, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include + +/** + * initialize configuration options + */ +void evdev_earlyInit(void); + +/** + * start the evdev layer + */ +bool evdev_start(void); + +/** + * stop the evdev layer + */ +void evdev_stop(void); + +/** + * grab the keyboard for exclusive access + */ +void evdev_grabKeyboard(void); + +/** + * ungrab the keyboard + */ +void evdev_ungrabKeyboard(void); diff --git a/client/include/interface/displayserver.h b/client/include/interface/displayserver.h index 0707e75b..68f8b204 100644 --- a/client/include/interface/displayserver.h +++ b/client/include/interface/displayserver.h @@ -183,6 +183,9 @@ struct LG_DisplayServerOps void (*capturePointer)(void); void (*uncapturePointer)(void); + /* get the character code for the provided scancode */ + int (*getCharCode)(int sc); + /* exiting = true if the warp is to leave the window */ void (*warpPointer)(int x, int y, bool exiting); @@ -253,6 +256,7 @@ struct LG_DisplayServerOps DEBUG_ASSERT((x)->ungrabPointer ); \ DEBUG_ASSERT((x)->capturePointer ); \ DEBUG_ASSERT((x)->uncapturePointer ); \ + DEBUG_ASSERT((x)->getCharCode ); \ DEBUG_ASSERT((x)->warpPointer ); \ DEBUG_ASSERT((x)->realignPointer ); \ DEBUG_ASSERT((x)->isValidPointerPos ); \ diff --git a/client/src/app.c b/client/src/app.c index 7b87fe1f..7deeab48 100644 --- a/client/src/app.c +++ b/client/src/app.c @@ -119,7 +119,7 @@ void app_handleFocusEvent(bool focused) if (g_params.releaseKeysOnFocusLoss) for (int key = 0; key < KEY_MAX; key++) if (g_state.keyDown[key]) - app_handleKeyRelease(key, 0); + app_handleKeyRelease(key); g_state.escapeActive = false; @@ -311,7 +311,7 @@ void app_handleWheelMotion(double motion) g_state.io->MouseWheel -= motion; } -void app_handleKeyPress(int sc, int charcode) +void app_handleKeyPress(int sc) { if (!app_isOverlayMode() || !g_state.io->WantCaptureKeyboard) { @@ -327,6 +327,7 @@ void app_handleKeyPress(int sc, int charcode) { g_state.escapeAction = sc; KeybindHandle handle; + int charcode = g_state.ds->getCharCode(sc); ll_forEachNL(g_state.bindings, item, handle) { if ((handle->sc && handle->sc == sc ) || @@ -374,7 +375,7 @@ void app_handleKeyPress(int sc, int charcode) } } -void app_handleKeyRelease(int sc, int charcode) +void app_handleKeyRelease(int sc) { if (g_state.escapeActive) { diff --git a/client/src/evdev.c b/client/src/evdev.c new file mode 100644 index 00000000..6a9534cd --- /dev/null +++ b/client/src/evdev.c @@ -0,0 +1,238 @@ +/** + * Looking Glass + * Copyright © 2017-2025 The Looking Glass Authors + * https://looking-glass.io + * + * 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 2 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, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "evdev.h" +#include +#include +#include +#include +#include +#include +#include + +#include "app.h" + +#include "common/debug.h" +#include "common/option.h" +#include "common/stringlist.h" +#include "common/thread.h" + +typedef struct +{ + char * path; + int fd; + bool grabbed; +} +EvdevDevice; + +struct EvdevState +{ + char * deviceList; + EvdevDevice * devices; + int deviceCount; + int epoll; + LGThread * thread; + bool grabbed; +}; + +static struct EvdevState state = {}; + +static struct Option options[] = +{ + { + .module = "input", + .name = "evdev", + .description = "csv list of evdev keyboard devices to use " + "for capture mode (ie: /dev/input/by-id/usb-some_device-event-kbd)", + .type = OPTION_TYPE_STRING, + .value.x_string = NULL, + }, + {0} +}; + +void evdev_earlyInit(void) +{ + option_register(options); +} + +static int evdev_thread(void * opaque) +{ + struct epoll_event * events = alloca(sizeof(*events) * state.deviceCount); + DEBUG_INFO("evdev_thread Started"); + while(app_isRunning()) + { + int waiting = epoll_wait(state.epoll, events, state.deviceCount, 100); + for(int i = 0; i < waiting; ++i) + { + struct input_event ev; + size_t n = read(events[i].data.fd, &ev, sizeof(ev)); + if (n != sizeof(ev)) + { + DEBUG_WARN("Failed to read evdev event"); + continue; + } + + if (!state.grabbed || ev.type != EV_KEY) + continue; + + if (ev.value == 1) + app_handleKeyPress(ev.code); + else if (ev.value == 0) + app_handleKeyRelease(ev.code); + } + } + DEBUG_INFO("evdev_thread Stopped"); + return 0; +} + +bool evdev_start(void) +{ + const char * deviceList = option_get_string("input", "evdev"); + if (!deviceList) + return false; + + state.deviceList = strdup(deviceList); + StringList sl = stringlist_new(false); + + char * token = strtok(state.deviceList, ","); + while(token != NULL) + { + stringlist_push(sl, token); + token = strtok(NULL, ","); + } + + state.deviceCount = stringlist_count(sl); + state.devices = calloc(state.deviceCount, sizeof(*state.devices)); + for(int i = 0; i < state.deviceCount; ++i) + state.devices[i].path = stringlist_at(sl, i); + stringlist_free(&sl); + + // nothing to do if there are no configured devices + if (state.deviceCount == 0) + return false; + + state.epoll = epoll_create1(0); + if (state.epoll < 0) + { + DEBUG_ERROR("Failed to create epoll (%s)", strerror(errno)); + return false; + } + + for(int i = 0; i < state.deviceCount; ++i) + { + EvdevDevice * device = &state.devices[i]; + device->fd = open(device->path, O_RDWR); + if (device->fd < 0) + { + DEBUG_ERROR("Unable to open %s (%s)", device->path, strerror(errno)); + return false; + } + + struct epoll_event event = + { + .events = EPOLLIN, + .data.fd = device->fd + }; + + if (epoll_ctl(state.epoll, EPOLL_CTL_ADD, device->fd, &event) != 0) + { + DEBUG_ERROR("Failed to add fd to epoll"); + return false; + } + + DEBUG_INFO("Opened: %s", device->path); + } + + if (!lgCreateThread("Evdev", evdev_thread, NULL, &state.thread)) + { + DEBUG_ERROR("Failed to create the evdev thread"); + return false; + } + + return true; +} + +void evdev_stop(void) +{ + if (state.deviceList) + { + free(state.deviceList); + state.deviceList = NULL; + } + + if (state.thread) + { + lgJoinThread(state.thread, NULL); + state.thread = NULL; + } + + if (state.epoll >= 0) + { + close(state.epoll); + state.epoll = 0; + } + + for(EvdevDevice * device = state.devices; device->path; ++device) + { + if (device->fd <= 0) + continue; + + close(device->fd); + device->fd = 0; + } +} + +void evdev_grabKeyboard(void) +{ + for(EvdevDevice * device = state.devices; device->path; ++device) + { + if (device->fd <= 0 || device->grabbed) + continue; + + if (ioctl(device->fd, EVIOCGRAB, (void *)1) < 0) + { + DEBUG_ERROR("EVIOCGRAB=1 failed: %s", strerror(errno)); + continue; + } + + DEBUG_INFO("Grabbed %s", device->path); + device->grabbed = true; + } + state.grabbed = true; +} + +void evdev_ungrabKeyboard(void) +{ + for(EvdevDevice * device = state.devices; device->path; ++device) + { + if (device->fd <= 0 || !device->grabbed) + continue; + + if (ioctl(device->fd, EVIOCGRAB, (void *)0) < 0) + { + DEBUG_ERROR("EVIOCGRAB=0 failed: %s", strerror(errno)); + continue; + } + + DEBUG_INFO("Ungrabbed %s", device->path); + device->grabbed = false; + } + state.grabbed = false; +} diff --git a/client/src/main.c b/client/src/main.c index 14c4623b..e15b80f6 100644 --- a/client/src/main.c +++ b/client/src/main.c @@ -64,6 +64,7 @@ #include "overlay_utils.h" #include "util.h" #include "render_queue.h" +#include "evdev.h" // forwards static int renderThread(void * unused); @@ -1240,6 +1241,14 @@ static int lg_run(void) return -1; } + if (evdev_start()) + { + DEBUG_INFO("Using evdev for keyboard capture"); + //override the display server's grab methods if we are using evdev + g_state.ds->grabKeyboard = &evdev_grabKeyboard; + g_state.ds->ungrabKeyboard = &evdev_ungrabKeyboard; + } + // override the SIGINIT handler so that we can tell the difference between // SIGINT and the user sending a close event, such as ALT+F4 signal(SIGINT , intHandler); @@ -1891,6 +1900,8 @@ int main(int argc, char * argv[]) if (LG_AudioDevs[i]->earlyInit) LG_AudioDevs[i]->earlyInit(); + evdev_earlyInit(); + if (!config_load(argc, argv)) return -1;