diff --git a/CMakeLists.txt b/CMakeLists.txt index 2d87d49f..546a7b22 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,12 +20,12 @@ cmake_minimum_required(VERSION 3.10) project(uiohook VERSION 1.3.0 LANGUAGES C) -if (WIN32 OR WIN64) +if(WIN32 OR WIN64) set(UIOHOOK_SOURCE_DIR "windows") -elseif (APPLE) +elseif(APPLE) set(UIOHOOK_SOURCE_DIR "darwin") else() - set(UIOHOOK_SOURCE_DIR "x11") + set(UIOHOOK_SOURCE_DIR "evdev") endif() add_library(uiohook @@ -71,8 +71,8 @@ export(TARGETS uiohook FILE "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}-config. install(EXPORT ${PROJECT_NAME}-config DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}) -if (BUILD_DEMO) - if (NOT BUILD_SHARED_LIBS) +if(BUILD_DEMO) + if(NOT BUILD_SHARED_LIBS) set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}") endif() @@ -111,7 +111,7 @@ if (BUILD_DEMO) add_compile_definitions(all_demos PRIVATE inline=__inline) add_compile_definitions(all_demos PRIVATE _CRT_SECURE_NO_WARNINGS) - if (MSVC_VERSION LESS "1900") + if(MSVC_VERSION LESS "1900") add_compile_definitions(all_demos PRIVATE snprintf=_snprintf) endif() endif() @@ -139,6 +139,36 @@ endif() if(UNIX AND NOT APPLE) find_package(PkgConfig REQUIRED) + pkg_check_modules(EVDEV REQUIRED libevdev) + target_include_directories(uiohook PRIVATE "${EVDEV_INCLUDE_DIRS}") + target_link_libraries(uiohook "${EVDEV_LDFLAGS}") + + pkg_check_modules(UDEV REQUIRED libudev) + target_include_directories(uiohook PRIVATE "${UDEV_INCLUDE_DIRS}") + target_link_libraries(uiohook "${UDEV_LDFLAGS}") + + #pkg_check_modules(XCB REQUIRED xcb) + #target_include_directories(uiohook PRIVATE "${XCB_INCLUDE_DIRS}") + #target_link_libraries(uiohook "${XCB_LDFLAGS}") + + pkg_check_modules(XCB_X11 REQUIRED x11-xcb) + target_include_directories(uiohook PRIVATE "${XCB_X11_INCLUDE_DIRS}") + target_link_libraries(uiohook "${XCB_X11_LDFLAGS}") + + pkg_check_modules(XKB REQUIRED xkbcommon) + target_include_directories(uiohook PRIVATE "${XKB_INCLUDE_DIRS}") + target_link_libraries(uiohook "${XKB_LDFLAGS}") + + pkg_check_modules(XKB_X11 REQUIRED xkbcommon-x11) + target_include_directories(uiohook PRIVATE "${XKB_X11_INCLUDE_DIRS}") + target_link_libraries(uiohook "${XKB_X11_LDFLAGS}") + + # FIXME Check for header X11/extensions/XKB.h + include(CheckIncludeFile) + check_include_file(X11/extensions/XKB.h HAVE_XKB_H) + + + pkg_check_modules(X11 REQUIRED x11) target_include_directories(uiohook PRIVATE "${X11_INCLUDE_DIRS}") target_link_libraries(uiohook "${X11_LDFLAGS}") @@ -147,11 +177,18 @@ if(UNIX AND NOT APPLE) target_include_directories(uiohook PRIVATE "${XTST_INCLUDE_DIRS}") target_link_libraries(uiohook "${XTST_LDFLAGS}") + pkg_check_modules(XINPUT REQUIRED xi) + target_include_directories(uiohook PRIVATE "${XINPUT_INCLUDE_DIRS}") + target_link_libraries(uiohook "${XINPUT_LDFLAGS}") + + + + include(CMakePrintHelpers) + cmake_print_variables(EVDEV_INCLUDE_DIRS) + include(CheckLibraryExists) check_library_exists(Xtst XRecordQueryVersion "" HAVE_XRECORD) - include(CheckIncludeFile) - check_include_file(X11/extensions/record.h HAVE_RECORD_H "-include X11/Xlib.h") option(USE_XT "X Toolkit Extension (default: ON)" ON) if(USE_XT) diff --git a/demo/demo_hook.c b/demo/demo_hook.c index c852c2a4..df5c272e 100644 --- a/demo/demo_hook.c +++ b/demo/demo_hook.c @@ -23,14 +23,13 @@ #include #include #include -#include #include -#include #include static void logger_proc(unsigned int level, void *user_data, const char *format, va_list args) { switch (level) { + default: case LOG_LEVEL_INFO: vfprintf(stdout, format, args); break; diff --git a/include/uiohook.h b/include/uiohook.h index 669e5f6e..b7c34ed4 100644 --- a/include/uiohook.h +++ b/include/uiohook.h @@ -39,6 +39,8 @@ #define UIOHOOK_ERROR_X_RECORD_ENABLE_CONTEXT 0x24 #define UIOHOOK_ERROR_X_RECORD_GET_CONTEXT 0x25 +#define UIOHOOK_ERROR_EPOLL_CREATE 0x26 + // Windows specific errors. #define UIOHOOK_ERROR_SET_WINDOWS_HOOK_EX 0x30 #define UIOHOOK_ERROR_GET_MODULE_HANDLE 0x31 diff --git a/src/evdev/dispatch_event.c b/src/evdev/dispatch_event.c new file mode 100644 index 00000000..705846f3 --- /dev/null +++ b/src/evdev/dispatch_event.c @@ -0,0 +1,474 @@ +/* libUIOHook: Cross-platform keyboard and mouse hooking from userland. + * Copyright (C) 2006-2024 Alexander Barker. All Rights Reserved. + * https://github.com/kwhat/libuiohook/ + * + * libUIOHook is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libUIOHook 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 Lesser General Public License + * along with this program. If not, see . + */ + +#include +#include +#include + + +#include "dispatch_event.h" +#include "input_helper.h" +#include "logger.h" + +typedef struct _mouse_click { + uint16_t count; + uint64_t time; + uint16_t button; +} mouse_click; +static mouse_click click = { + .count = 0, + .time = 0, + .button = MOUSE_NOBUTTON +}; + +// Virtual event pointer. +static uiohook_event uio_event; + +// Event dispatch callback. +static dispatcher_t dispatch = NULL; +static void *dispatch_data = NULL; + + +UIOHOOK_API void hook_set_dispatch_proc(dispatcher_t dispatch_proc, void *user_data) { + logger(LOG_LEVEL_DEBUG, "%s [%u]: Setting new dispatch callback to %#p.\n", + __FUNCTION__, __LINE__, dispatch_proc); + + dispatch = dispatch_proc; + dispatch_data = user_data; +} + + +// Send out an event if a dispatcher was set. +// FIXME Should be static void dispatch_event( +void dispatch_event(uiohook_event *const uio_event) { + if (dispatch != NULL) { + logger(LOG_LEVEL_DEBUG, "%s [%u]: Dispatching event type %u.\n", + __FUNCTION__, __LINE__, uio_event->type); + + dispatch(uio_event, dispatch_data); + } else { + logger(LOG_LEVEL_WARN, "%s [%u]: No dispatch callback set!\n", + __FUNCTION__, __LINE__); + } +} + +void dispatch_hook_enabled() { + // Initialize native input helper functions. + load_input_helper(); + + #ifdef USE_EPOCH_TIME + // Get the local system time in UTC. + struct timeval system_time; + gettimeofday(&system_time, NULL); + + uint64_t timestamp = get_unix_timestamp(&system_time); + #else + uint64_t timestamp = get_seq_timestamp(); + #endif + + // Populate the hook start event. + uio_event.time = timestamp; + uio_event.reserved = 0x00; + + uio_event.type = EVENT_HOOK_ENABLED; + uio_event.mask = 0x00; + + // Fire the hook start event. + dispatch_event(&uio_event); +} + +void dispatch_hook_disabled() { + #ifdef USE_EPOCH_TIME + struct timeval system_time; + gettimeofday(&system_time, NULL); + + uint64_t timestamp = get_unix_timestamp(&system_time); + #else + uint64_t timestamp = get_seq_timestamp(); + #endif + + // Populate the hook stop event. + uio_event.time = timestamp; + uio_event.reserved = 0x00; + + uio_event.type = EVENT_HOOK_DISABLED; + uio_event.mask = 0x00; + + // Fire the hook stop event. + dispatch_event(&uio_event); + + // Uninitialize native input helper functions. + unload_input_helper(); +} + +bool dispatch_key_press(uint64_t timestamp, xkb_keycode_t keycode, xkb_keysym_t keysym) { + uint16_t uiocode = keysym_to_uiocode(keysym); + + // Populate key pressed event. + uio_event.time = timestamp; + uio_event.reserved = 0x00; + + uio_event.type = EVENT_KEY_PRESSED; + uio_event.mask = get_modifiers(); + + uio_event.data.keyboard.keycode = uiocode; + uio_event.data.keyboard.rawcode = keycode; + uio_event.data.keyboard.keychar = CHAR_UNDEFINED; + + logger(LOG_LEVEL_DEBUG, "%s [%u]: Key %#X pressed. (%#X)\n", + __FUNCTION__, __LINE__, + uio_event.data.keyboard.keycode, uio_event.data.keyboard.rawcode); + + // Fire key pressed event. + dispatch_event(&uio_event); + + // If the pressed event was not consumed and we got a char in the buffer. + if (uio_event.reserved ^ 0x01) { + wchar_t surrogate[4] = {}; + size_t count = keycode_to_utf8(keycode, surrogate, sizeof(surrogate)); + for (unsigned int i = 0; i < count; i++) { + // Populate key typed event. + uio_event.time = timestamp; + uio_event.reserved = 0x00; + + uio_event.type = EVENT_KEY_TYPED; + uio_event.mask = get_modifiers(); + + uio_event.data.keyboard.keycode = VC_UNDEFINED; + uio_event.data.keyboard.rawcode = keycode; + uio_event.data.keyboard.keychar = surrogate[i]; + + logger(LOG_LEVEL_DEBUG, "%s [%u]: Key %#X typed. (%lc)\n", + __FUNCTION__, __LINE__, + uio_event.data.keyboard.keycode, uio_event.data.keyboard.keychar); + + // Fire key typed event. + dispatch_event(&uio_event); + } + } + + return uio_event.reserved & 0x01; +} + +bool dispatch_key_release(uint64_t timestamp, xkb_keycode_t keycode, xkb_keysym_t keysym) { + uint16_t uiocode = keysym_to_uiocode(keysym); + + // Populate key released event. + uio_event.time = timestamp; + uio_event.reserved = 0x00; + + uio_event.type = EVENT_KEY_RELEASED; + uio_event.mask = get_modifiers(); + + uio_event.data.keyboard.keycode = uiocode; + uio_event.data.keyboard.rawcode = keycode; + uio_event.data.keyboard.keychar = CHAR_UNDEFINED; + + logger(LOG_LEVEL_DEBUG, "%s [%u]: Key %#X released. (%#X)\n", + __FUNCTION__, __LINE__, + uio_event.data.keyboard.keycode, uio_event.data.keyboard.rawcode); + + // Fire key released event. + dispatch_event(&uio_event); + + return uio_event.reserved & 0x01; +} + +bool dispatch_mouse_press(struct input_event *const ev) { +/* + switch (x_event->button) { + case Button1: + x_event->button = MOUSE_BUTTON1; + set_modifier_mask(MASK_BUTTON1); + break; + + case Button2: + x_event->button = MOUSE_BUTTON2; + set_modifier_mask(MASK_BUTTON2); + break; + + case Button3: + x_event->button = MOUSE_BUTTON3; + set_modifier_mask(MASK_BUTTON3); + break; + + case XButton1: + x_event->button = MOUSE_BUTTON4; + set_modifier_mask(MASK_BUTTON5); + break; + + case XButton2: + x_event->button = MOUSE_BUTTON5; + set_modifier_mask(MASK_BUTTON5); + break; + + default: + if (x_event->button > XButton2) { + // Do not set modifier masks past button MASK_BUTTON5. + x_event->button = MOUSE_BUTTON5 + (x_event->button - XButton2); + } else { + // Something screwed up, default to MOUSE_NOBUTTON + x_event->button = MOUSE_NOBUTTON; + } + } + + // Track the number of clicks, the button must match the previous button. + if (x_event->button == click.button && x_event->serial - click.time <= hook_get_multi_click_time()) { + if (click.count < UINT16_MAX) { + click.count++; + } else { + logger(LOG_LEVEL_WARN, "%s [%u]: Click count overflow detected!\n", + __FUNCTION__, __LINE__); + } + } else { + // Reset the click count. + click.count = 1; + + // Set the last clicked button. + click.button = x_event->button; + } + + // Save this events time to calculate multi-clicks. + click.time = x_event->serial; + + // Populate mouse pressed event. + uio_event.time = x_event->serial; + uio_event.reserved = 0x00; + + uio_event.type = EVENT_MOUSE_PRESSED; + uio_event.mask = get_modifiers(); + + uio_event.data.mouse.button = x_event->button; + uio_event.data.mouse.clicks = click.count; + uio_event.data.mouse.x = x_event->x_root; + uio_event.data.mouse.y = x_event->y_root; + + #if defined(USE_XINERAMA) || defined(USE_XRANDR) + // FIXME There is something still broken about this. + uint8_t count; + screen_data *screens = hook_create_screen_info(&count); + if (count > 1) { + uio_event.data.mouse.x -= screens[0].x; + uio_event.data.mouse.y -= screens[0].y; + } + + if (screens != NULL) { + free(screens); + } + #endif + + logger(LOG_LEVEL_DEBUG, "%s [%u]: Button %u pressed %u time(s). (%u, %u)\n", + __FUNCTION__, __LINE__, + uio_event.data.mouse.button, uio_event.data.mouse.clicks, + uio_event.data.mouse.x, uio_event.data.mouse.y); + + // Fire mouse pressed event. + dispatch_event(&uio_event); + */ + + return false; +} + +bool dispatch_mouse_release(struct input_event *const ev) { + switch (0 /* FIXME button_map_lookup(x_event->button) */) { + /* + case Button1: + x_event->button = MOUSE_BUTTON1; + unset_modifier_mask(MASK_BUTTON1); + break; + + case Button2: + x_event->button = MOUSE_BUTTON2; + unset_modifier_mask(MASK_BUTTON2); + break; + + case Button3: + x_event->button = MOUSE_BUTTON3; + unset_modifier_mask(MASK_BUTTON3); + break; + + case XButton1: + x_event->button = MOUSE_BUTTON4; + unset_modifier_mask(MASK_BUTTON5); + break; + + case XButton2: + x_event->button = MOUSE_BUTTON5; + unset_modifier_mask(MASK_BUTTON5); + break; + + default: + if (x_event->button > XButton2) { + // Do not set modifier masks past button MASK_BUTTON5. + x_event->button = MOUSE_BUTTON5 + (x_event->button - XButton2); + } else { + // Something screwed up, default to MOUSE_NOBUTTON + x_event->button = MOUSE_NOBUTTON; + } + */ + } +/* + // Populate mouse released event. + uio_event.time = x_event->serial; + uio_event.reserved = 0x00; + + uio_event.type = EVENT_MOUSE_RELEASED; + uio_event.mask = get_modifiers(); + + uio_event.data.mouse.button = x_event->button; + uio_event.data.mouse.clicks = click.count; + uio_event.data.mouse.x = x_event->x_root; + uio_event.data.mouse.y = x_event->y_root; + + #if defined(USE_XINERAMA) || defined(USE_XRANDR) + uint8_t count; + screen_data *screens = hook_create_screen_info(&count); + if (count > 1) { + uio_event.data.mouse.x -= screens[0].x; + uio_event.data.mouse.y -= screens[0].y; + } + + if (screens != NULL) { + free(screens); + } + #endif + + logger(LOG_LEVEL_DEBUG, "%s [%u]: Button %u released %u time(s). (%u, %u)\n", + __FUNCTION__, __LINE__, + uio_event.data.mouse.button, uio_event.data.mouse.clicks, + uio_event.data.mouse.x, uio_event.data.mouse.y); + + // Fire mouse released event. + dispatch_event(&uio_event); +*/ + + return false; +} + +/* +static void dispatch_mouse_button_clicked(XButtonEvent *const x_event) { + // Populate mouse clicked event. + uio_event.time = x_event->serial; + uio_event.reserved = 0x00; + + uio_event.type = EVENT_MOUSE_CLICKED; + uio_event.mask = get_modifiers(); + + uio_event.data.mouse.button = x_event->button; + uio_event.data.mouse.clicks = click.count; + uio_event.data.mouse.x = x_event->x_root; + uio_event.data.mouse.y = x_event->y_root; + + #if defined(USE_XINERAMA) || defined(USE_XRANDR) + uint8_t count; + screen_data *screens = hook_create_screen_info(&count); + if (count > 1) { + uio_event.data.mouse.x -= screens[0].x; + uio_event.data.mouse.y -= screens[0].y; + } + + if (screens != NULL) { + free(screens); + } + #endif + + logger(LOG_LEVEL_DEBUG, "%s [%u]: Button %u clicked %u time(s). (%u, %u)\n", + __FUNCTION__, __LINE__, + uio_event.data.mouse.button, uio_event.data.mouse.clicks, + uio_event.data.mouse.x, uio_event.data.mouse.y); + + // Fire mouse clicked event. + dispatch_event(&uio_event); +} +*/ + +bool dispatch_mouse_move(uint64_t timestamp, int16_t x, int16_t y) { + // Reset the click count. + /* FIXME This needs to happen somewhere, just not here. + if (click.count != 0 && x_event->serial - click.time > hook_get_multi_click_time()) { + click.count = 0; + }*/ + + // Populate mouse move event. + uio_event.time = timestamp; + uio_event.reserved = 0x00; + + uio_event.mask = get_modifiers(); + + // Check the upper half of virtual modifiers for non-zero values and set the mouse + // dragged flag. The last 3 bits are reserved for lock masks. + bool is_dragged = (bool) (uio_event.mask & 0x1F00); + if (is_dragged) { + uio_event.type = EVENT_MOUSE_DRAGGED; + } else { + uio_event.type = EVENT_MOUSE_MOVED; + } + + uio_event.data.mouse.button = MOUSE_NOBUTTON; + uio_event.data.mouse.clicks = click.count; + + // FIXME These are the relative coordinates + uio_event.data.mouse.x = x; + uio_event.data.mouse.y = y; + + logger(LOG_LEVEL_DEBUG, "%s [%u]: Mouse %s to %i, %i. (%#X)\n", + __FUNCTION__, __LINE__, + is_dragged ? "dragged" : "moved", + uio_event.data.mouse.x, uio_event.data.mouse.y, + uio_event.mask); + + // Fire mouse move event. + dispatch_event(&uio_event); + + return uio_event.reserved & 0x01; +} + +bool dispatch_mouse_wheel(uint64_t timestamp, int16_t rotation, uint8_t direction) { + // Reset the click count and previous button. + click.count = 0; + click.button = MOUSE_NOBUTTON; + + // Populate mouse wheel event. + uio_event.time = timestamp; + uio_event.reserved = 0x00; + + uio_event.type = EVENT_MOUSE_WHEEL; + uio_event.mask = get_modifiers(); + + // FIXME Need to calculate abs pos + //uio_event.data.wheel.x = x_event->x_root; + //uio_event.data.wheel.y = x_event->y_root; + + /* Linux does not have an API call for acquiring the mouse scroll type or amount. For the time being we will just + * use the unit scroll at 100. */ + uio_event.data.wheel.type = WHEEL_UNIT_SCROLL; + uio_event.data.wheel.delta = 120; + uio_event.data.wheel.rotation = 3 * uio_event.data.wheel.delta * rotation; // TODO Im not sure this calcuation is correct, compare to windows. + uio_event.data.wheel.direction = direction; + + logger(LOG_LEVEL_DEBUG, "%s [%u]: Mouse wheel %i / %u of type %u in the %u direction at %u, %u.\n", + __FUNCTION__, __LINE__, + uio_event.data.wheel.rotation, uio_event.data.wheel.delta, + uio_event.data.wheel.type, uio_event.data.wheel.direction, + uio_event.data.wheel.x, uio_event.data.wheel.y); + + // Fire mouse wheel event. + dispatch_event(&uio_event); + + return uio_event.reserved & 0x01; +} diff --git a/src/evdev/dispatch_event.h b/src/evdev/dispatch_event.h new file mode 100644 index 00000000..8adf7cd0 --- /dev/null +++ b/src/evdev/dispatch_event.h @@ -0,0 +1,39 @@ +/* libUIOHook: Cross-platform keyboard and mouse hooking from userland. + * Copyright (C) 2006-2023 Alexander Barker. All Rights Reserved. + * https://github.com/kwhat/libuiohook/ + * + * libUIOHook is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libUIOHook 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 Lesser General Public License + * along with this program. If not, see . + */ + +#include +#include +#include + +extern void dispatch_event(uiohook_event *const uio_event); + +extern void dispatch_hook_enabled(); + +extern void dispatch_hook_disabled(); + +extern bool dispatch_key_press(uint64_t timestamp, xkb_keycode_t keycode, xkb_keysym_t keysym); + +extern bool dispatch_key_release(uint64_t timestamp, xkb_keycode_t keycode, xkb_keysym_t keysym); + +extern bool dispatch_mouse_press(struct input_event *const ev); + +extern bool dispatch_mouse_release(struct input_event *const ev); + +extern bool dispatch_mouse_move(uint64_t timestamp, int16_t x, int16_t y); + +extern bool dispatch_mouse_wheel(uint64_t timestamp, int16_t rotation, uint8_t direction); diff --git a/src/evdev/input_helper.c b/src/evdev/input_helper.c new file mode 100644 index 00000000..30302da5 --- /dev/null +++ b/src/evdev/input_helper.c @@ -0,0 +1,768 @@ +/* libUIOHook: Cross-platform keyboard and mouse hooking from userland. + * Copyright (C) 2006-2024 Alexander Barker. All Rights Reserved. + * https://github.com/kwhat/libuiohook/ + * + * libUIOHook is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libUIOHook 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 Lesser General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include + +#ifdef USE_EPOCH_TIME +#include +#endif + +#include +#include +#include +#include + +#ifdef HAVE_XKB_H +#include +#else +#define XkbMinLegalKeyCode 8 +/* TODO Should we use XDisplayKeycodes +int min_keycodes, max_keycodes; +XDisplayKeycodes(display, &min_keycodes, &max_keycodes); +*/ +/* TODO What about using xkb_keycode_t xkb_keymap_min_keycode(struct xkb_keymap *keymap)? +*/ +#endif + + +#include +#include + +// TODO It's unclear if these XKB_KEY's should still be used. +#ifndef XKB_KEY_osfPrior +#define XKB_KEY_osfPrior 0x1004FF55 +#endif + +#ifndef XKB_KEY_osfNext +#define XKB_KEY_osfNext 0x1004FF56 +#endif + +#ifndef XKB_KEY_apCopy +#define XKB_KEY_apCopy 0x1000FF02 +#endif + +#ifndef XKB_KEY_apCut +#define XKB_KEY_apCut 0x1000FF03 +#endif + +#ifndef XKB_KEY_apPaste +#define XKB_KEY_apPaste 0x1000FF04 +#endif + + +#include +static XkbDescPtr keyboard_map; + +#include "input_helper.h" +#include "logger.h" + +#define BUTTON_TABLE_MAX 16 + +static unsigned char *mouse_button_table; + +// FIXME This should be static +Display *display = NULL; +static struct xkb_context *ctx = NULL; +static struct xkb_compose_state *compose_state; +static struct xkb_compose_table *compose_table = NULL; +static struct xkb_keymap *keymap = NULL; +static struct xkb_state *state = NULL; + + +static uint16_t modifier_mask; + +static const uint32_t keysym_vcode_table[][2] = { + { VC_ESCAPE, XKB_KEY_Escape }, + { VC_ESCAPE, XKB_KEY_osfEscape }, // HP OSF KeySym in HPkeysym.h + + // Begin Function Keys + { VC_F1, XKB_KEY_F1 }, + { VC_F2, XKB_KEY_F2 }, + { VC_F3, XKB_KEY_F3 }, + { VC_F4, XKB_KEY_F4 }, + { VC_F5, XKB_KEY_F5 }, + { VC_F6, XKB_KEY_F6 }, + { VC_F7, XKB_KEY_F7 }, + { VC_F8, XKB_KEY_F8 }, + { VC_F9, XKB_KEY_F9 }, + { VC_F10, XKB_KEY_F10 }, + { VC_F11, XKB_KEY_F11 }, + { VC_F11, XKB_KEY_SunF36 }, // Labeled F11 in Sunkeysym.h + { VC_F12, XKB_KEY_F12 }, + { VC_F12, XKB_KEY_SunF37 }, // Labeled F12 in Sunkeysym.h + + { VC_F13, XKB_KEY_F13 }, + { VC_F14, XKB_KEY_F14 }, + { VC_F15, XKB_KEY_F15 }, + { VC_F16, XKB_KEY_F16 }, + { VC_F17, XKB_KEY_F17 }, + { VC_F18, XKB_KEY_F18 }, + { VC_F19, XKB_KEY_F19 }, + { VC_F20, XKB_KEY_F20 }, + { VC_F21, XKB_KEY_F21 }, + { VC_F22, XKB_KEY_F22 }, + { VC_F23, XKB_KEY_F23 }, + { VC_F24, XKB_KEY_F24 }, + // End Function Keys + + + // Begin Alphanumeric Zone + { VC_BACK_QUOTE, XKB_KEY_grave }, + + { VC_0, XKB_KEY_0 }, + { VC_1, XKB_KEY_1 }, + { VC_2, XKB_KEY_2 }, + { VC_3, XKB_KEY_3 }, + { VC_4, XKB_KEY_4 }, + { VC_5, XKB_KEY_5 }, + { VC_6, XKB_KEY_6 }, + { VC_7, XKB_KEY_7 }, + { VC_8, XKB_KEY_8 }, + { VC_9, XKB_KEY_9 }, + + { VC_MINUS, XKB_KEY_minus }, + { VC_PLUS, XKB_KEY_plus }, + { VC_EQUALS, XKB_KEY_equal }, + { VC_ASTERISK, XKB_KEY_asterisk }, + + { VC_AT, XKB_KEY_at }, + { VC_AMPERSAND, XKB_KEY_ampersand }, + { VC_DOLLAR, XKB_KEY_dollar }, + { VC_EXCLAMATION_MARK, XKB_KEY_exclam }, + { VC_EXCLAMATION_DOWN, XKB_KEY_exclamdown }, + + { VC_BACKSPACE, XKB_KEY_BackSpace }, + { VC_BACKSPACE, XKB_KEY_osfBackSpace }, // HP OSF KeySym in HPkeysym.h + + { VC_TAB, XKB_KEY_Tab }, + { VC_TAB, XKB_KEY_ISO_Left_Tab }, + { VC_CAPS_LOCK, XKB_KEY_Caps_Lock }, + { VC_CAPS_LOCK, XKB_KEY_Shift_Lock }, + + { VC_A, XKB_KEY_A }, + { VC_B, XKB_KEY_B }, + { VC_C, XKB_KEY_C }, + { VC_D, XKB_KEY_D }, + { VC_E, XKB_KEY_E }, + { VC_F, XKB_KEY_F }, + { VC_G, XKB_KEY_G }, + { VC_H, XKB_KEY_H }, + { VC_I, XKB_KEY_I }, + { VC_J, XKB_KEY_J }, + { VC_K, XKB_KEY_K }, + { VC_L, XKB_KEY_L }, + { VC_M, XKB_KEY_M }, + { VC_N, XKB_KEY_N }, + { VC_O, XKB_KEY_O }, + { VC_P, XKB_KEY_P }, + { VC_Q, XKB_KEY_Q }, + { VC_R, XKB_KEY_R }, + { VC_S, XKB_KEY_S }, + { VC_T, XKB_KEY_T }, + { VC_U, XKB_KEY_U }, + { VC_V, XKB_KEY_V }, + { VC_W, XKB_KEY_W }, + { VC_X, XKB_KEY_X }, + { VC_Y, XKB_KEY_Y }, + { VC_Z, XKB_KEY_Z }, + + { VC_OPEN_BRACKET, XKB_KEY_bracketleft }, + { VC_CLOSE_BRACKET, XKB_KEY_bracketright }, + { VC_BACK_SLASH, XKB_KEY_backslash }, + + { VC_COLON, XKB_KEY_colon }, + { VC_SEMICOLON, XKB_KEY_semicolon }, + { VC_QUOTE, XKB_KEY_apostrophe }, + { VC_QUOTEDBL, XKB_KEY_quotedbl }, + { VC_ENTER, XKB_KEY_Return, }, + { VC_ENTER, XKB_KEY_Linefeed, }, + + { VC_LESS, XKB_KEY_less }, + { VC_GREATER, XKB_KEY_greater }, + { VC_COMMA, XKB_KEY_comma }, + { VC_PERIOD, XKB_KEY_period }, + { VC_SLASH, XKB_KEY_slash }, + { VC_NUMBER_SIGN, XKB_KEY_numbersign }, + + { VC_OPEN_BRACE, XKB_KEY_braceleft }, + { VC_CLOSE_BRACE, XKB_KEY_braceright }, + + { VC_OPEN_PARENTHESIS, XKB_KEY_parenleft }, + { VC_CLOSE_PARENTHESIS, XKB_KEY_parenright }, + + { VC_SPACE, XKB_KEY_space }, + // End Alphanumeric Zone + + + // Begin Edit Key Zone + { VC_PRINT_SCREEN, XKB_KEY_Print }, + { VC_PRINT_SCREEN, XKB_KEY_SunPrint_Screen }, // Same as XK_Print in Sunkeysym.h + { VC_PRINT_SCREEN, XKB_KEY_SunSys_Req }, // SysReq should be the same as Print Screen + { VC_SCROLL_LOCK, XKB_KEY_Scroll_Lock, }, + { VC_PAUSE, XKB_KEY_Pause }, + { VC_CANCEL, XKB_KEY_Cancel }, + { VC_CANCEL, XKB_KEY_osfCancel }, // HP OSF KeySym in HPkeysym.h + { VC_INSERT, XKB_KEY_Insert }, + { VC_INSERT, XKB_KEY_osfInsert }, // HP OSF KeySym in HPkeysym.h + { VC_DELETE, XKB_KEY_Delete }, + { VC_DELETE, XKB_KEY_osfDelete }, // HP OSF KeySym in HPkeysym.h + { VC_HOME, XKB_KEY_Home }, + { VC_END, XKB_KEY_End }, + { VC_END, XKB_KEY_osfEndLine }, // HP OSF KeySym in HPkeysym.h + { VC_PAGE_UP, XKB_KEY_Page_Up }, + { VC_PAGE_UP, XKB_KEY_Prior }, + { VC_PAGE_UP, XKB_KEY_osfPageUp }, // HP OSF KeySym in HPkeysym.h + { VC_PAGE_UP, XKB_KEY_osfPrior }, // HP OSF KeySym in HPkeysym.h + { VC_PAGE_DOWN, XKB_KEY_Page_Down }, + { VC_PAGE_DOWN, XKB_KEY_Next }, + { VC_PAGE_DOWN, XKB_KEY_osfPageDown }, // HP OSF KeySym in HPkeysym.h + { VC_PAGE_DOWN, XKB_KEY_osfNext }, // HP OSF KeySym in HPkeysym.h + // End Edit Key Zone + + + // Begin Cursor Key Zone + { VC_UP, XKB_KEY_Up }, + { VC_UP, XKB_KEY_osfUp }, // HP OSF KeySym in HPkeysym.h + { VC_LEFT, XKB_KEY_Left }, + { VC_LEFT, XKB_KEY_osfLeft }, // HP OSF KeySym in HPkeysym.h + { VC_BEGIN, XKB_KEY_Begin }, + { VC_RIGHT, XKB_KEY_Right }, + { VC_RIGHT, XKB_KEY_osfRight }, // HP OSF KeySym in HPkeysym.h + { VC_DOWN, XKB_KEY_Down }, + { VC_DOWN, XKB_KEY_osfDown }, // HP OSF KeySym in HPkeysym.h + // End Cursor Key Zone + + + // Begin Numeric Zone + { VC_NUM_LOCK, XKB_KEY_Num_Lock }, + { VC_KP_CLEAR, XKB_KEY_Clear, }, + { VC_KP_CLEAR, XKB_KEY_osfClear }, // HP OSF KeySym in HPkeysym.h + + { VC_KP_DIVIDE, XKB_KEY_KP_Divide }, + { VC_KP_MULTIPLY, XKB_KEY_KP_Multiply }, + { VC_KP_SUBTRACT, XKB_KEY_KP_Subtract }, + { VC_KP_EQUALS, XKB_KEY_KP_Equal }, + { VC_KP_ADD, XKB_KEY_KP_Add }, + { VC_KP_ENTER, XKB_KEY_KP_Enter }, + { VC_KP_DECIMAL, XKB_KEY_KP_Decimal }, + { VC_KP_SEPARATOR, XKB_KEY_KP_Separator }, + + /* TODO Implement + { VC_KP_SPACE, XKB_KEY_KP_Space }, + { VC_KP_TAB, XKB_KEY_KP_Tab }, + //*/ + + { VC_KP_0, XKB_KEY_KP_0 }, + { VC_KP_1, XKB_KEY_KP_1 }, + { VC_KP_2, XKB_KEY_KP_2 }, + { VC_KP_3, XKB_KEY_KP_3 }, + { VC_KP_4, XKB_KEY_KP_4 }, + { VC_KP_5, XKB_KEY_KP_5 }, + { VC_KP_6, XKB_KEY_KP_6 }, + { VC_KP_7, XKB_KEY_KP_7 }, + { VC_KP_8, XKB_KEY_KP_8 }, + { VC_KP_9, XKB_KEY_KP_9 }, + + { VC_KP_END, XKB_KEY_KP_End }, + { VC_KP_DOWN, XKB_KEY_KP_Down }, + { VC_KP_PAGE_DOWN, XKB_KEY_KP_Page_Down }, + { VC_KP_PAGE_DOWN, XKB_KEY_KP_Next }, + { VC_KP_LEFT, XKB_KEY_KP_Left }, + { VC_KP_BEGIN, XKB_KEY_KP_Begin, }, + { VC_KP_RIGHT, XKB_KEY_KP_Right }, + { VC_KP_HOME, XKB_KEY_KP_Home }, + { VC_KP_UP, XKB_KEY_KP_Up }, + { VC_KP_PAGE_UP, XKB_KEY_KP_Page_Up }, + { VC_KP_PAGE_UP, XKB_KEY_KP_Prior }, + { VC_KP_INSERT, XKB_KEY_KP_Insert }, + { VC_KP_DELETE, XKB_KEY_KP_Delete }, + // End Numeric Zone + + + // Begin Modifier and Control Keys + { VC_SHIFT_L, XKB_KEY_Shift_L }, + { VC_SHIFT_R, XKB_KEY_Shift_R }, + { VC_CONTROL_L, XKB_KEY_Control_L }, + { VC_CONTROL_R, XKB_KEY_Control_R }, + { VC_ALT_L, XKB_KEY_Alt_L }, + { VC_ALT_R, XKB_KEY_Alt_R }, + { VC_ALT_GRAPH, XKB_KEY_ISO_Level3_Shift }, + { VC_META_L, XKB_KEY_Meta_L }, + { VC_META_R, XKB_KEY_Meta_R }, + { VC_CONTEXT_MENU, XKB_KEY_Menu }, + // End Modifier and Control Keys + + + // Begin Shortcut Keys + { VC_POWER, XKB_KEY_XF86PowerOff }, + { VC_SLEEP, XKB_KEY_XF86Sleep }, + { VC_WAKE, XKB_KEY_XF86WakeUp }, + + { VC_MEDIA_PLAY, XKB_KEY_XF86AudioPlay }, + { VC_MEDIA_STOP, XKB_KEY_XF86AudioStop }, + { VC_MEDIA_PREVIOUS, XKB_KEY_XF86AudioPrev }, + { VC_MEDIA_NEXT, XKB_KEY_XF86AudioNext }, + { VC_MEDIA_SELECT, XKB_KEY_XF86Select }, + { VC_MEDIA_EJECT, XKB_KEY_XF86Eject }, + + { VC_VOLUME_MUTE, XKB_KEY_XF86AudioMute }, + { VC_VOLUME_MUTE, XKB_KEY_SunAudioMute }, + { VC_VOLUME_DOWN, XKB_KEY_XF86AudioLowerVolume }, + { VC_VOLUME_DOWN, XKB_KEY_SunAudioLowerVolume }, + { VC_VOLUME_UP, XKB_KEY_XF86AudioRaiseVolume }, + { VC_VOLUME_UP, XKB_KEY_SunAudioRaiseVolume }, + + { VC_APP_BROWSER, XKB_KEY_XF86WWW }, + { VC_APP_CALCULATOR, XKB_KEY_XF86Calculator }, + { VC_APP_MAIL, XKB_KEY_XF86Mail }, + { VC_APP_MUSIC, XKB_KEY_XF86Music }, + { VC_APP_PICTURES, XKB_KEY_XF86Pictures }, + + { VC_BROWSER_SEARCH, XKB_KEY_XF86Search }, + { VC_BROWSER_HOME, XKB_KEY_XF86HomePage }, + { VC_BROWSER_BACK, XKB_KEY_XF86Back }, + { VC_BROWSER_FORWARD, XKB_KEY_XF86Forward }, + { VC_BROWSER_STOP, XKB_KEY_XF86Stop }, + { VC_BROWSER_REFRESH, XKB_KEY_XF86Refresh }, + { VC_BROWSER_FAVORITES, XKB_KEY_XF86Favorites }, + // End Shortcut Keys + + + // Begin European Language Keys + { VC_CIRCUMFLEX, XKB_KEY_asciicircum }, + + { VC_DEAD_GRAVE, XKB_KEY_dead_grave }, + { VC_DEAD_GRAVE, XKB_KEY_SunFA_Grave }, + { VC_DEAD_GRAVE, XKB_KEY_Dgrave_accent }, // DEC private keysym in DECkeysym.h + { VC_DEAD_GRAVE, XKB_KEY_hpmute_grave }, // HP OSF KeySym in HPkeysym.h + + { VC_DEAD_ACUTE, XKB_KEY_dead_acute }, + { VC_DEAD_ACUTE, XKB_KEY_SunFA_Acute }, + { VC_DEAD_ACUTE, XKB_KEY_Dacute_accent }, // DEC private keysym in DECkeysym.h + { VC_DEAD_ACUTE, XKB_KEY_hpmute_acute }, // HP OSF KeySym in HPkeysym.h + + { VC_DEAD_CIRCUMFLEX, XKB_KEY_dead_circumflex }, + { VC_DEAD_CIRCUMFLEX, XKB_KEY_SunFA_Circum }, + { VC_DEAD_CIRCUMFLEX, XKB_KEY_Dcircumflex_accent }, // DEC private keysym in DECkeysym.h + { VC_DEAD_CIRCUMFLEX, XKB_KEY_hpmute_asciicircum }, // HP OSF KeySym in HPkeysym.h + + { VC_DEAD_TILDE, XKB_KEY_dead_tilde }, + { VC_DEAD_TILDE, XKB_KEY_SunFA_Tilde }, + { VC_DEAD_TILDE, XKB_KEY_Dtilde }, // DEC private keysym in DECkeysym.h + { VC_DEAD_TILDE, XKB_KEY_hpmute_asciitilde }, // HP OSF KeySym in HPkeysym.h + + { VC_DEAD_MACRON, XKB_KEY_dead_macron }, + { VC_DEAD_BREVE, XKB_KEY_dead_breve }, + { VC_DEAD_ABOVEDOT, XKB_KEY_dead_abovedot }, + + { VC_DEAD_DIAERESIS, XKB_KEY_dead_diaeresis }, + { VC_DEAD_DIAERESIS, XKB_KEY_SunFA_Diaeresis }, + { VC_DEAD_DIAERESIS, XKB_KEY_Ddiaeresis }, // DEC private keysym in DECkeysym.h + { VC_DEAD_DIAERESIS, XKB_KEY_hpmute_diaeresis }, // HP OSF KeySym in HPkeysym.h + + { VC_DEAD_ABOVERING, XKB_KEY_dead_abovering }, + { VC_DEAD_ABOVERING, XKB_KEY_Dring_accent }, // DEC private keysym in DECkeysym.h + { VC_DEAD_DOUBLEACUTE, XKB_KEY_dead_doubleacute }, + { VC_DEAD_CARON, XKB_KEY_dead_caron }, + + { VC_DEAD_CEDILLA, XKB_KEY_dead_cedilla }, + { VC_DEAD_CEDILLA, XKB_KEY_SunFA_Cedilla }, + { VC_DEAD_CEDILLA, XKB_KEY_Dcedilla_accent }, // DEC private keysym in DECkeysym.h + + { VC_DEAD_OGONEK, XKB_KEY_dead_ogonek }, + { VC_DEAD_IOTA, XKB_KEY_dead_iota }, + { VC_DEAD_VOICED_SOUND, XKB_KEY_dead_voiced_sound }, + { VC_DEAD_SEMIVOICED_SOUND, XKB_KEY_dead_semivoiced_sound }, + // End European Language Keys + + + // Begin Asian Language Keys + { VC_KATAKANA, XKB_KEY_Katakana }, + { VC_KANA, XKB_KEY_Kana_Shift }, + { VC_KANA_LOCK, XKB_KEY_Kana_Lock }, + + { VC_KANJI, XKB_KEY_Kanji }, + { VC_HIRAGANA, XKB_KEY_Hiragana }, + + { VC_ACCEPT, XKB_KEY_Execute }, // Type 5c Japanese keyboard: kakutei + { VC_CONVERT, XKB_KEY_Kanji }, // Type 5c Japanese keyboard: henkan + { VC_COMPOSE, XKB_KEY_Multi_key }, + { VC_INPUT_METHOD_ON_OFF, XKB_KEY_Henkan_Mode }, // Type 5c Japanese keyboard: nihongo + + { VC_ALL_CANDIDATES, XKB_KEY_Zen_Koho }, + { VC_ALPHANUMERIC, XKB_KEY_Eisu_Shift }, + { VC_ALPHANUMERIC, XKB_KEY_Eisu_toggle }, + { VC_CODE_INPUT, XKB_KEY_Kanji_Bangou }, + { VC_FULL_WIDTH, XKB_KEY_Zenkaku }, + { VC_HALF_WIDTH, XKB_KEY_Hankaku }, + { VC_NONCONVERT, XKB_KEY_Muhenkan }, + { VC_PREVIOUS_CANDIDATE, XKB_KEY_Mae_Koho }, + { VC_ROMAN_CHARACTERS, XKB_KEY_Romaji }, + + { VC_UNDERSCORE, XKB_KEY_underscore }, + // End Asian Language Keys + + + // Begin Sun Keys + { VC_SUN_HELP, XKB_KEY_Help }, + { VC_SUN_HELP, XKB_KEY_osfHelp }, + + { VC_SUN_STOP, XKB_KEY_Cancel }, // FIXME Already used... + { VC_SUN_STOP, XKB_KEY_SunStop }, // Same as XK_Cancel in Sunkeysym.h + { VC_SUN_STOP, XKB_KEY_L1 }, + + { VC_SUN_PROPS, XKB_KEY_SunProps }, + { VC_SUN_PROPS, XKB_KEY_L3 }, + + { VC_SUN_FRONT, XKB_KEY_SunFront }, + { VC_SUN_OPEN, XKB_KEY_SunOpen }, + + { VC_SUN_FIND, XKB_KEY_Find }, + { VC_SUN_FIND, XKB_KEY_L9 }, + { VC_SUN_FIND, XKB_KEY_SunFind }, // Same as XK_Find in Sunkeysym.h + + { VC_SUN_AGAIN, XKB_KEY_Redo }, + { VC_SUN_AGAIN, XKB_KEY_L2 }, + { VC_SUN_AGAIN, XKB_KEY_SunAgain }, // Same as XK_Redo in Sunkeysym.h + + { VC_SUN_UNDO, XKB_KEY_Undo }, + { VC_SUN_UNDO, XKB_KEY_L4 }, + { VC_SUN_UNDO, XKB_KEY_SunUndo }, // Same as XK_Undo in Sunkeysym.h + { VC_SUN_UNDO, XKB_KEY_osfUndo }, + + { VC_SUN_COPY, XKB_KEY_L6 }, + { VC_SUN_COPY, XKB_KEY_apCopy }, + { VC_SUN_COPY, XKB_KEY_SunCopy }, + { VC_SUN_COPY, XKB_KEY_osfCopy }, + + { VC_SUN_PASTE, XKB_KEY_L8 }, + { VC_SUN_PASTE, XKB_KEY_SunPaste }, + { VC_SUN_PASTE, XKB_KEY_apPaste }, + { VC_SUN_PASTE, XKB_KEY_osfPaste }, + + { VC_SUN_CUT, XKB_KEY_L10 }, + { VC_SUN_CUT, XKB_KEY_SunCut }, + { VC_SUN_CUT, XKB_KEY_apCut }, + { VC_SUN_CUT, XKB_KEY_osfCut }, + // End Sun Keys + + { VC_UNDEFINED, XKB_KEY_NoSymbol } +}; + + +uint16_t keysym_to_uiocode(xkb_keysym_t keysym) { + uint16_t uiocode = VC_UNDEFINED; + + keysym = xkb_keysym_to_upper(keysym); + for (unsigned int i = 0; i < sizeof(keysym_vcode_table) / sizeof(keysym_vcode_table[0]); i++) { + if (keysym == keysym_vcode_table[i][1]) { + uiocode = keysym_vcode_table[i][0]; + break; + } + } + + // TODO VC_ALT_GRAPH MASK? + if (uiocode == VC_SHIFT_L) { set_modifier_mask(MASK_SHIFT_L); } + else if (uiocode == VC_SHIFT_R) { set_modifier_mask(MASK_SHIFT_R); } + else if (uiocode == VC_CONTROL_L) { set_modifier_mask(MASK_CTRL_L); } + else if (uiocode == VC_CONTROL_R) { set_modifier_mask(MASK_CTRL_R); } + else if (uiocode == VC_ALT_L) { set_modifier_mask(MASK_ALT_L); } + else if (uiocode == VC_ALT_R) { set_modifier_mask(MASK_ALT_R); } + else if (uiocode == VC_META_L) { set_modifier_mask(MASK_META_L); } + else if (uiocode == VC_META_R) { set_modifier_mask(MASK_META_R); } + + // FIXME We shouldn't be doing this on each key press, do something similar to above. + //initialize_locks(); + + if ((get_modifiers() & MASK_NUM_LOCK) == 0) { + switch (uiocode) { + case VC_KP_SEPARATOR: + case VC_KP_1: + case VC_KP_2: + case VC_KP_3: + case VC_KP_4: + case VC_KP_5: + case VC_KP_6: + case VC_KP_7: + case VC_KP_8: + case VC_KP_0: + case VC_KP_9: + uiocode |= 0xEE00; + break; + } + } + + return uiocode; +} + +xkb_keycode_t uiocode_to_keycode(uint16_t vcode) { + KeyCode keycode = 0x0000; + KeyCode keysym = NoSymbol; + + for (unsigned int i = 0; i < sizeof(keysym_vcode_table) / sizeof(keysym_vcode_table[0]); i++) { + if (vcode == keysym_vcode_table[i][0]) { + keycode = keysym_vcode_table[i][1]; + if (keysym = XKeysymToKeycode(display, keycode)) { + break; + } + } + } + + return keysym; +} + +xkb_keycode_t event_to_keycode(uint16_t code) { + return XkbMinLegalKeyCode + code; +} + +xkb_keysym_t event_to_keysym(xkb_keycode_t keycode, enum xkb_key_state_t key_state) { + xkb_keysym_t keysym = xkb_state_key_get_one_sym(state, keycode); + + if (key_state != KEY_STATE_REPEAT || xkb_keymap_key_repeats(keymap, keycode)) { + if (key_state != KEY_STATE_RELEASE) { + xkb_compose_state_feed(compose_state, keysym); + } + + enum xkb_compose_status status = xkb_compose_state_get_status(compose_state); + if (status == XKB_COMPOSE_CANCELLED || status == XKB_COMPOSE_COMPOSED) { + xkb_compose_state_reset(compose_state); + } + + if (key_state == KEY_STATE_RELEASE) { + xkb_state_update_key(state, keycode, XKB_KEY_UP); + } else { + xkb_state_update_key(state, keycode, XKB_KEY_DOWN); + } + } + + return keysym; +} + +uint8_t button_map_lookup(uint8_t button) { + unsigned int map_button = button; + + if (display != NULL) { + if (mouse_button_table != NULL) { + int map_size = XGetPointerMapping(display, mouse_button_table, BUTTON_TABLE_MAX); + if (map_button > 0 && map_button <= map_size) { + map_button = mouse_button_table[map_button -1]; + } + } else { + logger(LOG_LEVEL_WARN, "%s [%u]: Mouse button map memory is unavailable!\n", + __FUNCTION__, __LINE__); + } + } else { + logger(LOG_LEVEL_WARN, "%s [%u]: XDisplay helper_disp is unavailable!\n", + __FUNCTION__, __LINE__); + } + + // X11 numbers buttons 2 & 3 backwards from other platforms so we normalize them. + if (map_button == Button2) { map_button = Button3; } + else if (map_button == Button3) { map_button = Button2; } + + return map_button; +} + +// Set the native modifier mask for future events. +void set_modifier_mask(uint16_t mask) { + modifier_mask |= mask; +} + +// Unset the native modifier mask for future events. +void unset_modifier_mask(uint16_t mask) { + modifier_mask &= ~mask; +} + +// Get the current native modifier mask state. +uint16_t get_modifiers() { + return modifier_mask; +} + +// Initialize the modifier lock masks. +static void initialize_locks() { + if (xkb_state_led_name_is_active(state, XKB_LED_NAME_CAPS) > 0) { + set_modifier_mask(MASK_CAPS_LOCK); + } else { + unset_modifier_mask(MASK_CAPS_LOCK); + } + + if (xkb_state_led_name_is_active(state, XKB_LED_NAME_NUM) > 0) { + set_modifier_mask(MASK_NUM_LOCK); + } else { + unset_modifier_mask(MASK_NUM_LOCK); + } + + if (xkb_state_led_name_is_active(state, XKB_LED_NAME_SCROLL) > 0) { + set_modifier_mask(MASK_SCROLL_LOCK); + } else { + unset_modifier_mask(MASK_SCROLL_LOCK); + } +} + +#ifdef USE_EPOCH_TIME +/* Get the current timestamp in unix epoch time. */ +uint64_t get_unix_timestamp(struct timeval *event_time) { + // Convert the event time to a Unix epoch in MS. + uint64_t timestamp = (event_time->tv_sec * 1000) + (event_time->tv_usec / 1000); + + return timestamp; +} +#else +static uint64_t seq_timestamp = 0; +uint64_t get_seq_timestamp() { + if (seq_timestamp == UINT64_MAX) { + // TODO Warning + seq_timestamp = 0; + } + return seq_timestamp++; +} +#endif + +size_t keycode_to_utf8(xkb_keycode_t keycode, wchar_t *surrogate, size_t length) { + size_t count = 0; + if (surrogate == NULL || length == 0) { + return count; + } + + char buffer[5] = {}; + count = xkb_state_key_get_utf8(state, keycode, buffer, sizeof(buffer)); + + + // If we produced a string and we have a buffer, convert to 16-bit surrogate pairs. + if (count > 0) { + // TODO Can we just replace all this with `count = mbstowcs(surrogate, buffer, count);`? + + // See https://en.wikipedia.org/wiki/UTF-8#Examples + const uint8_t utf8_bitmask_table[] = { + 0x3F, // 00111111, non-first (if > 1 byte) + 0x7F, // 01111111, first (if 1 byte) + 0x1F, // 00011111, first (if 2 bytes) + 0x0F, // 00001111, first (if 3 bytes) + 0x07 // 00000111, first (if 4 bytes) + }; + + uint32_t codepoint = utf8_bitmask_table[count] & buffer[0]; + for (unsigned int i = 1; i < count; i++) { + codepoint = (codepoint << 6) | (utf8_bitmask_table[0] & buffer[i]); + } + + if (codepoint <= 0xFFFF) { + count = 1; + surrogate[0] = codepoint; + } else if (length > 1) { + // if codepoint > 0xFFFF, split into lead (high) / trail (low) surrogate ranges + // See https://unicode.org/faq/utf_bom.html#utf16-4 + const uint32_t lead_offset = 0xD800 - (0x10000 >> 10); + + count = 2; + surrogate[0] = lead_offset + (codepoint >> 10); // lead, first [0] + surrogate[1] = 0xDC00 + (codepoint & 0x3FF); // trail, second [1] + } else { + count = 0; + logger(LOG_LEVEL_WARN, "%s [%u]: Surrogate buffer overflow detected!\n", + __FUNCTION__, __LINE__); + } + } + + return count; +} + +void load_input_helper() { + display = XOpenDisplay(NULL); + if (display == NULL) { + logger(LOG_LEVEL_ERROR, "%s [%u]: Failed to open X11 display!\n", + __FUNCTION__, __LINE__); + } + xcb_connection_t *xcb_connection = XGetXCBConnection(display); + + ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS); + if (!ctx) { + logger(LOG_LEVEL_ERROR, "%s [%u]: Failed to create xkb context!\n", + __FUNCTION__, __LINE__); + } + + const char *locale = setlocale(LC_CTYPE, NULL); + compose_table = xkb_compose_table_new_from_locale(ctx, locale, XKB_COMPOSE_COMPILE_NO_FLAGS); + if (!compose_table) { + logger(LOG_LEVEL_ERROR, "%s [%u]: Failed to create compose table from locale!\n", + __FUNCTION__, __LINE__); + } + + compose_state = xkb_compose_state_new(compose_table, XKB_COMPOSE_STATE_NO_FLAGS); + if (!compose_state) { + logger(LOG_LEVEL_ERROR, "%s [%u]: Failed to create compose state from table!\n", + __FUNCTION__, __LINE__); + } + + int32_t device_id = xkb_x11_get_core_keyboard_device_id(xcb_connection); + if (device_id == -1) { + logger(LOG_LEVEL_ERROR, "%s [%u]: Failed to get core keyboard device id!\n", + __FUNCTION__, __LINE__); + } + + keymap = xkb_x11_keymap_new_from_device(ctx, xcb_connection, device_id, XKB_KEYMAP_COMPILE_NO_FLAGS); + if (!keymap) { + logger(LOG_LEVEL_ERROR, "%s [%u]: Failed to create x11 keymap from device!\n", + __FUNCTION__, __LINE__); + } + state = xkb_x11_state_new_from_device(keymap, xcb_connection, device_id); + + // Setup memory for mouse button mapping. + mouse_button_table = malloc(sizeof(unsigned char) * BUTTON_TABLE_MAX); + if (mouse_button_table == NULL) { + logger(LOG_LEVEL_ERROR, "%s [%u]: Failed to allocate memory for mouse button map!\n", + __FUNCTION__, __LINE__); + + //return UIOHOOK_ERROR_OUT_OF_MEMORY; + } +} + +void unload_input_helper() { + if (state != NULL) { + xkb_state_unref(state); + state = NULL; + } + + if (keymap != NULL) { + xkb_keymap_unref(keymap); + keymap = NULL; + } + + if (compose_table != NULL) { + xkb_compose_table_unref(compose_table); + compose_table = NULL; + } + + if (ctx != NULL) { + xkb_context_unref(ctx); + ctx = NULL; + } + + if (display != NULL) { + XCloseDisplay(display); + display = NULL; + } + + if (mouse_button_table != NULL) { + free(mouse_button_table); + mouse_button_table = NULL; + } +} diff --git a/src/evdev/input_helper.h b/src/evdev/input_helper.h new file mode 100644 index 00000000..00d9f572 --- /dev/null +++ b/src/evdev/input_helper.h @@ -0,0 +1,79 @@ +/* libUIOHook: Cross-platform keyboard and mouse hooking from userland. + * Copyright (C) 2006-2024 Alexander Barker. All Rights Reserved. + * https://github.com/kwhat/libuiohook/ + * + * libUIOHook is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libUIOHook 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 Lesser General Public License + * along with this program. If not, see . + */ + +#ifndef _included_input_helper +#define _included_input_helper + +#include +#include +#include + +/* The meaning of the input_event 'value' field. */ +enum xkb_key_state_t { + KEY_STATE_RELEASE = 0, + KEY_STATE_PRESS = 1, + KEY_STATE_REPEAT = 2 +}; + +// Helper display used by input helper, properties and post event. +#ifdef USE_EPOCH_TIME +extern uint64_t get_unix_timestamp(struct timeval *event_time); +#else +extern uint64_t get_seq_timestamp(); +#endif + +/* Converts a XKB key code to the appropriate uiohook virtual code. */ +extern uint16_t keysym_to_uiocode(xkb_keysym_t keysym); + +/* Converts a uiohook virtual code to the appropriate XKB key code. */ +extern xkb_keycode_t uiocode_to_keycode(uint16_t uiocode); + +/* FIXME Write Doc */ +extern size_t keycode_to_utf8(xkb_keycode_t keycode, wchar_t *surrogate, size_t length); + +/* FIXME Write Doc */ +extern xkb_keycode_t event_to_keycode(uint16_t code); + +/* FIXME Write Doc */ +extern xkb_keysym_t event_to_keysym(xkb_keycode_t keycode, enum xkb_key_state_t key_state); + +/* Set the native modifier mask for future events. */ +extern void set_modifier_mask(uint16_t mask); + +/* Unset the native modifier mask for future events. */ +extern void unset_modifier_mask(uint16_t mask); + +/* Get the current native modifier mask state. */ +extern uint16_t get_modifiers(); + +/* Enable detectable auto-repeat for keys */ +extern bool enable_key_repeat(); + +/* Initialize items required for KeyCodeToKeySym() and KeySymToUnicode() + * functionality. This method is called by OnLibraryLoad() and may need to be + * called in combination with UnloadInputHelper() if the native keyboard layout + * is changed. */ +extern void load_input_helper(); + +/* De-initialize items required for KeyCodeToKeySym() and KeySymToUnicode() + * functionality. This method is called by OnLibraryUnload() and may need to be + * called in combination with LoadInputHelper() if the native keyboard layout + * is changed. */ +extern void unload_input_helper(); + +#endif diff --git a/src/evdev/input_hook.c b/src/evdev/input_hook.c new file mode 100644 index 00000000..46512b6c --- /dev/null +++ b/src/evdev/input_hook.c @@ -0,0 +1,447 @@ +/* libUIOHook: Cross-platform keyboard and mouse hooking from userland. + * Copyright (C) 2006-2024 Alexander Barker. All Rights Reserved. + * https://github.com/kwhat/libuiohook/ + * + * libUIOHook is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libUIOHook 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 Lesser General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dispatch_event.h" +#include "input_helper.h" +#include "logger.h" + +struct input_hook { + struct libevdev *evdev; + struct libevdev_uinput *uinput; +}; + +struct epoll_event_listener { + struct epoll_event epoll; + struct epoll_event_listener *next; +}; + +static int rel_x = 0, rel_y = 0; +static int wheel_h = 0, wheel_v = 0; + + +static bool hook_key_proc(struct input_event *ev) { + bool consumed = false; + + #ifdef USE_EPOCH_TIME + uint64_t timestamp = get_unix_timestamp(&ev->time); + #else + uint64_t timestamp = get_seq_timestamp(); + #endif + + xkb_keycode_t keycode = event_to_keycode(ev->code); + xkb_keysym_t keysym = event_to_keysym(keycode, ev->value); + + if (ev->value > 0) { + consumed = dispatch_key_press(timestamp, keycode, keysym); + } else { + consumed = dispatch_key_release(timestamp, keycode, keysym); + } + + return consumed; +} + +static bool hook_button_proc(struct input_event *ev) { + bool consumed = false; + + if (ev->value > 0) { + consumed = dispatch_mouse_press(ev); + } else { + consumed = dispatch_mouse_release(ev); + } + + return consumed; +} + +static bool hook_sync_proc(struct input_event *ev) { + bool consumed = false; + + #ifdef USE_EPOCH_TIME + uint64_t timestamp = get_unix_timestamp(&ev->time); + #else + uint64_t timestamp = get_seq_timestamp(); + #endif + + if (rel_x != 0 || rel_y != 0) { + consumed = dispatch_mouse_move(timestamp, rel_x, rel_y); + rel_x = rel_y = 0; + } + + if (wheel_v != 0) { + consumed = dispatch_mouse_wheel(timestamp, wheel_v, WHEEL_VERTICAL_DIRECTION); + wheel_v = 0; + } + + if (wheel_h != 0) { + consumed = dispatch_mouse_wheel(timestamp, wheel_v, WHEEL_HORIZONTAL_DIRECTION); + wheel_h = 0; + } + + return consumed; +} + +static bool hook_event_proc(struct input_event *ev) { + bool consumed = false; + + switch (ev->type) { + case EV_KEY: + switch (ev->code) { + case BTN_LEFT: + case BTN_RIGHT: + case BTN_MIDDLE: + case BTN_SIDE: + case BTN_EXTRA: + case BTN_FORWARD: + case BTN_BACK: + consumed = hook_button_proc(ev); + break; + + default: + consumed = hook_key_proc(ev); + } + break; + + case EV_REL: + switch (ev->code) { + case REL_X: + rel_x += ev->value; + case REL_Y: + rel_y += ev->value; + break; + + case REL_WHEEL_HI_RES: + wheel_v += ev->value; + break; + case REL_HWHEEL_HI_RES: + wheel_h += ev->value; + break; + } + break; + + case EV_SYN: + consumed = hook_sync_proc(ev); + break; + } + + return consumed; +} + +static int create_hook(const char *path, struct input_hook **hook) { + int fd = open(path, O_RDONLY | O_NONBLOCK); + if (fd < 0) { + logger(LOG_LEVEL_ERROR, "%s [%u]: Failed to open input device: %s! (%d)\n", + __FUNCTION__, __LINE__, + path, errno); + return UIOHOOK_FAILURE; + } + + *hook = malloc(sizeof(struct input_hook)); + if (*hook == NULL) { + logger(LOG_LEVEL_ERROR, "%s [%u]: Failed to allocate memory for evdev buffer!\n", + __FUNCTION__, __LINE__); + return UIOHOOK_ERROR_OUT_OF_MEMORY; + } + + int err = libevdev_new_from_fd(fd, &(*hook)->evdev); + if (err < 0) { + logger(LOG_LEVEL_ERROR, "%s [%u]: Failed to create evdev from file descriptor: %s! (%d)\n", + __FUNCTION__, __LINE__, + path, err); + return UIOHOOK_FAILURE; + } + + // We need to wait until no key is being pressed because we are going to grab below and that will cause keys to stick. + struct input_event ev; + for (unsigned int i = 0; i < KEY_MAX; i++) { + while (libevdev_get_event_value((*hook)->evdev, EV_KEY, i)) { + int status = -EAGAIN; + unsigned int flags = LIBEVDEV_READ_FLAG_NORMAL; + do { + status = libevdev_next_event((*hook)->evdev, flags, &ev); + if (status == LIBEVDEV_READ_STATUS_SYNC) { + flags = LIBEVDEV_READ_FLAG_SYNC; + } + } while (status == LIBEVDEV_READ_STATUS_SYNC); + } + } + + err = libevdev_uinput_create_from_device((*hook)->evdev, LIBEVDEV_UINPUT_OPEN_MANAGED, &(*hook)->uinput); + if (err < 0) { + logger(LOG_LEVEL_WARN, "%s [%u]: Failed to create uinput from device! (%d)\n", + __FUNCTION__, __LINE__, + err); + + (*hook)->uinput = NULL; + } else { + // On success, grab the device so we can consume events. + err = libevdev_grab((*hook)->evdev, LIBEVDEV_GRAB); + if (err != 0) { + logger(LOG_LEVEL_WARN, "%s [%u]: Failed to grab evdev! (%d)\n", + __FUNCTION__, __LINE__, + err); + + libevdev_uinput_destroy((*hook)->uinput); + (*hook)->uinput = NULL; + } + } + + char *label; + if (libevdev_has_event_type((*hook)->evdev, EV_REP) && libevdev_has_event_code((*hook)->evdev, EV_KEY, KEY_ESC)) { + label = "keyboard"; + } else if (libevdev_has_event_type((*hook)->evdev, EV_REL) && libevdev_has_event_code((*hook)->evdev, EV_KEY, BTN_LEFT)) { + label = "pointing"; + } else { + logger(LOG_LEVEL_DEBUG, "%s [%u]: Unsupported input device: %s.\n", + __FUNCTION__, __LINE__, + path); + return UIOHOOK_FAILURE; + } + + logger(LOG_LEVEL_DEBUG, "%s [%u]: Registering %s device: %s.\n", + __FUNCTION__, __LINE__, + label, path); + + return UIOHOOK_SUCCESS; +} + +static void destroy_hook(struct input_hook **hook) { + if (*hook != NULL) { + return; + } + + const int fd = libevdev_get_fd((*hook)->evdev); + + if ((*hook)->evdev != NULL) { + libevdev_grab((*hook)->evdev, LIBEVDEV_UNGRAB); + libevdev_free((*hook)->evdev); + (*hook)->evdev = NULL; + } + + if ((*hook)->uinput != NULL) { + libevdev_uinput_destroy((*hook)->uinput); + (*hook)->uinput = NULL; + } + + free(*hook); + *hook = NULL; + + if (fd >= 0) { + close(fd); + } +} + +static int create_event_listeners(int epoll_fd, struct epoll_event_listener **listeners) { + struct udev *udev = udev_new(); + if (udev == NULL) { + logger(LOG_LEVEL_ERROR, "%s [%u]: Failed to create new udev context!\n", + __FUNCTION__, __LINE__); + return UIOHOOK_FAILURE; + } + + struct udev_enumerate *enumerate = udev_enumerate_new(udev); + udev_enumerate_add_match_subsystem(enumerate, "input"); + udev_enumerate_add_match_sysname(enumerate, "event*"); + udev_enumerate_scan_devices(enumerate); + + struct epoll_event_listener **listener_next = listeners; + + struct udev_list_entry *devices = udev_enumerate_get_list_entry(enumerate); + struct udev_list_entry *dev_list_entry; + + udev_list_entry_foreach(dev_list_entry, devices) { + const char *path = udev_list_entry_get_name(dev_list_entry); + if (path == NULL) { + logger(LOG_LEVEL_WARN, "%s [%u]: Failed to get udev entry name!\n", + __FUNCTION__, __LINE__); + continue; + } + + struct udev_device *dev = udev_device_new_from_syspath(udev, path); + if (dev == NULL) { + logger(LOG_LEVEL_WARN, "%s [%u]: Failed to create new udev device: %s!\n", + __FUNCTION__, __LINE__, + path); + continue; + } + + const char *devnode = udev_device_get_devnode(dev); + if (devnode == NULL) { + logger(LOG_LEVEL_WARN, "%s [%u]: Failed to get udev device node: %s!\n", + __FUNCTION__, __LINE__, + path); + udev_device_unref(dev); + continue; + } + + const char *is_keyboard = udev_device_get_property_value(dev, "ID_INPUT_KEYBOARD"); + const char *is_mouse = udev_device_get_property_value(dev, "ID_INPUT_MOUSE"); + if ((is_keyboard && strcmp(is_keyboard, "1") == 0) || (is_mouse && strcmp(is_mouse, "1") == 0)) { + logger(LOG_LEVEL_DEBUG, "%s [%u]: Found udev input device: %s %s.\n", + __FUNCTION__, __LINE__, + udev_device_get_property_value(dev, "ID_VENDOR"), + udev_device_get_property_value(dev, "ID_MODEL")); + + *listener_next = malloc(sizeof(struct epoll_event_listener)); + if (*listener_next == NULL) { + logger(LOG_LEVEL_ERROR, "%s [%u]: Failed to allocate memory for epoll event devices!\n", + __FUNCTION__, __LINE__); + udev_device_unref(dev); + continue; + } + + struct input_hook *hook = NULL; + if (create_hook(devnode, &hook) != UIOHOOK_SUCCESS) { + logger(LOG_LEVEL_ERROR, "%s [%u]: Staring destroy_hook 0x%p\n", + __FUNCTION__, __LINE__, hook); + destroy_hook(&hook); + udev_device_unref(dev); + free(*listener_next); + *listener_next = NULL; + continue; + } + + struct epoll_event_listener *listener_current = *listener_next; + listener_current->epoll.events = EPOLLIN; + listener_current->epoll.data.ptr = hook; + listener_current->next = NULL; + + const int fd = libevdev_get_fd(hook->evdev); + if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &listener_current->epoll) < 0) { + logger(LOG_LEVEL_ERROR, "%s [%u]: Failed to add file descriptor to epoll! (%d)\n", + __FUNCTION__, __LINE__); + + destroy_hook(&hook); + listener_current->epoll.data.ptr = NULL; + udev_device_unref(dev); + free(*listener_next); + *listener_next = NULL; + continue; + } + + listener_next = &listener_current->next; + } + + udev_device_unref(dev); + } + + udev_enumerate_unref(enumerate); + udev_unref(udev); + + return UIOHOOK_SUCCESS; +} + +static int destroy_event_listeners(int epoll_fd, struct epoll_event **listeners) { + // FIXME Implement +} + + +UIOHOOK_API int hook_run() { + int epoll_fd = epoll_create1(0); + if (epoll_fd == -1) { + logger(LOG_LEVEL_ERROR, "%s [%u]: Failed to call epoll_create1!\n", + __FUNCTION__, __LINE__); + + return UIOHOOK_ERROR_EPOLL_CREATE; + } + + struct epoll_event_listener *listeners = NULL; + int status = create_event_listeners(epoll_fd, &listeners); + if (status != UIOHOOK_SUCCESS) { + close(epoll_fd); + return status; + } + + // FIXME EVENT_BUFFER_SIZE should really just be the number of listeners we got. + #define EVENT_BUFFER_SIZE 8 + #define EVENT_BUFFER_WAIT 30 + + struct epoll_event *event_buffer = malloc(sizeof(struct epoll_event) * EVENT_BUFFER_SIZE); + if (event_buffer == NULL) { + logger(LOG_LEVEL_ERROR, "%s [%u]: Failed to allocate memory for device buffer!\n", + __FUNCTION__, __LINE__); + + close(epoll_fd); + return UIOHOOK_ERROR_OUT_OF_MEMORY; + } + + // FIXME This should happen on hook start event. + load_input_helper(); + + + int err; + struct input_event ev; + + // FIXME We need some way of turning this off. + while (1) { + int event_count = epoll_wait(epoll_fd, event_buffer, EVENT_BUFFER_SIZE, EVENT_BUFFER_WAIT * 1000); + + for (int i = 0; i < event_count; i++) { + struct input_hook *hook = event_buffer[i].data.ptr; + + int status = -EAGAIN; + unsigned int flags = LIBEVDEV_READ_FLAG_NORMAL; + do { + status = libevdev_next_event(hook->evdev, flags, &ev); + if (status == LIBEVDEV_READ_STATUS_SYNC) { + flags = LIBEVDEV_READ_FLAG_SYNC; + } + + if (status != -EAGAIN) { + bool consumed = hook_event_proc(&ev); + + if (hook->uinput != NULL && !consumed) { + libevdev_uinput_write_event(hook->uinput, ev.type, ev.code, ev.value); + } + } + } while (status != -EAGAIN); + } + } + + if (close(epoll_fd)) { + logger(LOG_LEVEL_ERROR, "%s [%u]: Failed to close epoll file descriptor!\n", + __FUNCTION__, __LINE__); + // FIXME What exactly should we do here? Return error, set epoll_fd to null? + } + + logger(LOG_LEVEL_DEBUG, "%s [%u]: Something, something, something, complete.\n", + __FUNCTION__, __LINE__); + + return UIOHOOK_SUCCESS; +} + + +UIOHOOK_API int hook_stop() { + int status = UIOHOOK_FAILURE; + + // FIXME Implement + + logger(LOG_LEVEL_DEBUG, "%s [%u]: Status: %#X.\n", + __FUNCTION__, __LINE__, status); + + return status; +} diff --git a/src/evdev/post_event.c b/src/evdev/post_event.c new file mode 100644 index 00000000..41802562 --- /dev/null +++ b/src/evdev/post_event.c @@ -0,0 +1,227 @@ +/* libUIOHook: Cross-platform keyboard and mouse hooking from userland. + * Copyright (C) 2006-2024 Alexander Barker. All Rights Reserved. + * https://github.com/kwhat/libuiohook/ + * + * libUIOHook is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libUIOHook 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 Lesser General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "input_helper.h" +#include "logger.h" + +// TODO use uinput, see: https://www.freedesktop.org/software/libevdev/doc/0.5/group__uinput.html +static int post_key_event(uiohook_event * const event) { +/* + KeyCode keycode = vcode_to_keycode(event->data.keyboard.keycode); + if (keycode == 0x0000) { + logger(LOG_LEVEL_WARN, "%s [%u]: Unable to lookup scancode: %li\n", + __FUNCTION__, __LINE__, event->data.keyboard.keycode); + return UIOHOOK_FAILURE; + } + + Bool is_pressed; + if (event->type == EVENT_KEY_PRESSED) { + is_pressed = True; + } else if (event->type == EVENT_KEY_RELEASED) { + is_pressed = False; + } else { + logger(LOG_LEVEL_DEBUG, "%s [%u]: Invalid event for keyboard post event: %#X.\n", + __FUNCTION__, __LINE__, event->type); + return UIOHOOK_FAILURE; + } + + if (XTestFakeKeyEvent(display, keycode, is_pressed, 0) != Success) { + logger(LOG_LEVEL_ERROR, "%s [%u]: XTestFakeKeyEvent() failed!\n", + __FUNCTION__, __LINE__, event->type); + return UIOHOOK_FAILURE; + } +*/ + return UIOHOOK_SUCCESS; +} + +static int post_mouse_button_event(uiohook_event * const event) { +/* + XButtonEvent btn_event = { + .serial = 0, + .send_event = False, + .display = display, + + .window = None, //* “event” window it is reported relative to + .root = None, //* root window that the event occurred on + .subwindow = XDefaultRootWindow(display), //* child window + + .time = CurrentTime, + + .x = event->data.mouse.x, // pointer x, y coordinates in event window + .y = event->data.mouse.y, + + .x_root = 0, // coordinates relative to root + .y_root = 0, + + .state = 0x00, // key or button mask + .same_screen = True + }; + + // Move the pointer to the specified position. + XTestFakeMotionEvent(btn_event.display, -1, btn_event.x, btn_event.y, 0); + + int status = UIOHOOK_FAILURE; + switch (event->type) { + case EVENT_MOUSE_PRESSED: + if (event->data.mouse.button < MOUSE_BUTTON1 || event->data.mouse.button > MOUSE_BUTTON5) { + logger(LOG_LEVEL_WARN, "%s [%u]: Invalid button specified for mouse pressed event! (%u)\n", + __FUNCTION__, __LINE__, event->data.mouse.button); + return UIOHOOK_FAILURE; + } + + if (XTestFakeButtonEvent(display, event->data.mouse.button, True, 0) != 0) { + status = UIOHOOK_SUCCESS; + } + break; + + case EVENT_MOUSE_RELEASED: + if (event->data.mouse.button < MOUSE_BUTTON1 || event->data.mouse.button > MOUSE_BUTTON5) { + logger(LOG_LEVEL_WARN, "%s [%u]: Invalid button specified for mouse released event! (%u)\n", + __FUNCTION__, __LINE__, event->data.mouse.button); + return UIOHOOK_FAILURE; + } + + if (XTestFakeButtonEvent(display, event->data.mouse.button, False, 0) != 0) { + status = UIOHOOK_SUCCESS; + } + break; + + default: + logger(LOG_LEVEL_DEBUG, "%s [%u]: Invalid mouse button event: %#X.\n", + __FUNCTION__, __LINE__, event->type); + status = UIOHOOK_FAILURE; + } + + return status; +*/ + return UIOHOOK_SUCCESS; +} + +static int post_mouse_wheel_event(uiohook_event * const event) { +/* + int status = UIOHOOK_FAILURE; + + XButtonEvent btn_event = { + .serial = 0, + .send_event = False, + .display = display, + + .window = None, // “event” window it is reported relative to + .root = None, // root window that the event occurred on + .subwindow = XDefaultRootWindow(display), // child window + + .time = CurrentTime, + + .x = event->data.wheel.x, // pointer x, y coordinates in event window + .y = event->data.wheel.y, + + .x_root = 0, // coordinates relative to root + .y_root = 0, + + .state = 0x00, // key or button mask + .same_screen = True + }; + + // Wheel events should be the same as click events on X11. + // type, amount and rotation + unsigned int button = 0; // FIXME button_map_lookup(event->data.wheel.rotation < 0 ? WheelUp : WheelDown); + + if (XTestFakeButtonEvent(display, button, True, 0) != 0) { + status = UIOHOOK_SUCCESS; + } + + if (status == UIOHOOK_SUCCESS && XTestFakeButtonEvent(display, button, False, 0) == 0) { + status = UIOHOOK_FAILURE; + } +*/ + return UIOHOOK_SUCCESS; +} + +static int post_mouse_motion_event(uiohook_event * const event) { +/* + int status = UIOHOOK_FAILURE; + + if (XTestFakeMotionEvent(display, -1, event->data.mouse.x, event->data.mouse.y, 0) != 0) { + status = UIOHOOK_SUCCESS; + } + + return status; +*/ + return UIOHOOK_SUCCESS; +} + +// TODO This should return a status code, UIOHOOK_SUCCESS or otherwise. +UIOHOOK_API int hook_post_event(uiohook_event * const event) { +/* + if (display == NULL) { + logger(LOG_LEVEL_ERROR, "%s [%u]: XDisplay display is unavailable!\n", + __FUNCTION__, __LINE__); + return UIOHOOK_ERROR_X_OPEN_DISPLAY; + } + + XLockDisplay(display); + + int status = UIOHOOK_FAILURE; + switch (event->type) { + case EVENT_KEY_PRESSED: + case EVENT_KEY_RELEASED: + status = post_key_event(event); + break; + + case EVENT_MOUSE_PRESSED: + case EVENT_MOUSE_RELEASED: + status = post_mouse_button_event(event); + break; + + case EVENT_MOUSE_WHEEL: + status = post_mouse_wheel_event(event); + break; + + case EVENT_MOUSE_MOVED: + case EVENT_MOUSE_DRAGGED: + status = post_mouse_motion_event(event); + break; + + case EVENT_KEY_TYPED: + case EVENT_MOUSE_CLICKED: + + case EVENT_HOOK_ENABLED: + case EVENT_HOOK_DISABLED: + + default: + logger(LOG_LEVEL_WARN, "%s [%u]: Ignoring post event type %#X\n", + __FUNCTION__, __LINE__, event->type); + status = UIOHOOK_FAILURE; + } + + // Don't forget to flush! + XSync(display, True); + XUnlockDisplay(display); + + return status; + */ + return UIOHOOK_SUCCESS; +} diff --git a/src/evdev/system_properties.c b/src/evdev/system_properties.c new file mode 100644 index 00000000..d5410a13 --- /dev/null +++ b/src/evdev/system_properties.c @@ -0,0 +1,497 @@ +/* libUIOHook: Cross-platform keyboard and mouse hooking from userland. + * Copyright (C) 2006-2024 Alexander Barker. All Rights Reserved. + * https://github.com/kwhat/libuiohook/ + * + * libUIOHook is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libUIOHook 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 Lesser General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include +#include + +#ifdef USE_XF86MISC +#include +#include +#endif + +#if defined(USE_XINERAMA) && !defined(USE_XRANDR) +#include +#elif defined(USE_XRANDR) +#include +#include +#endif + +#ifdef USE_XT +#include + +static XtAppContext xt_context; +static Display *xt_disp; +#endif + +static Display *display; + +#include "input_helper.h" +#include "logger.h" + +#ifdef USE_XRANDR +static pthread_mutex_t xrandr_mutex = PTHREAD_MUTEX_INITIALIZER; +static XRRScreenResources *xrandr_resources = NULL; + +static void settings_cleanup_proc(void *arg) { + if (pthread_mutex_trylock(&xrandr_mutex) == 0) { + if (xrandr_resources != NULL) { + XRRFreeScreenResources(xrandr_resources); + xrandr_resources = NULL; + } + + if (arg != NULL) { + XCloseDisplay((Display *) arg); + arg = NULL; + } + + pthread_mutex_unlock(&xrandr_mutex); + } +} + +static void *settings_thread_proc(void *arg) { + Display *settings_disp = XOpenDisplay(XDisplayName(NULL));; + if (settings_disp != NULL) { + logger(LOG_LEVEL_DEBUG, "%s [%u]: %s\n", + __FUNCTION__, __LINE__, "XOpenDisplay success."); + + pthread_cleanup_push(settings_cleanup_proc, settings_disp); + + int event_base = 0; + int error_base = 0; + if (XRRQueryExtension(settings_disp, &event_base, &error_base)) { + Window root = XDefaultRootWindow(settings_disp); + unsigned long event_mask = RRScreenChangeNotifyMask; + XRRSelectInput(settings_disp, root, event_mask); + + XEvent ev; + + while(settings_disp != NULL) { + XNextEvent(settings_disp, &ev); + + if (ev.type == event_base + RRScreenChangeNotifyMask) { + logger(LOG_LEVEL_DEBUG, "%s [%u]: Received XRRScreenChangeNotifyEvent.\n", + __FUNCTION__, __LINE__); + + pthread_mutex_lock(&xrandr_mutex); + if (xrandr_resources != NULL) { + XRRFreeScreenResources(xrandr_resources); + } + + xrandr_resources = XRRGetScreenResources(settings_disp, root); + if (xrandr_resources == NULL) { + logger(LOG_LEVEL_WARN, "%s [%u]: XRandR could not get screen resources!\n", + __FUNCTION__, __LINE__); + } + pthread_mutex_unlock(&xrandr_mutex); + } else { + logger(LOG_LEVEL_WARN, "%s [%u]: XRandR is not currently available!\n", + __FUNCTION__, __LINE__); + } + } + } + + // Execute the thread cleanup handler. + pthread_cleanup_pop(1); + + } else { + logger(LOG_LEVEL_ERROR, "%s [%u]: XOpenDisplay failure!\n", + __FUNCTION__, __LINE__); + } + + return NULL; +} +#endif + +// FIXME XINERAMA and XRADNR can prob be replaced by https://wayland.app/protocols/xdg-output-unstable-v1#zxdg_output_v1:event:logical_size on wayland. +UIOHOOK_API screen_data* hook_create_screen_info(unsigned char *count) { + *count = 0; + screen_data *screens = NULL; + + // Check and make sure we could connect to the x server. + if (display != NULL) { + #if defined(USE_XINERAMA) && !defined(USE_XRANDR) + if (XineramaIsActive(display)) { + int xine_count = 0; + XineramaScreenInfo *xine_info = XineramaQueryScreens(display, &xine_count); + + if (xine_info != NULL) { + if (xine_count > UINT8_MAX) { + *count = UINT8_MAX; + + logger(LOG_LEVEL_WARN, "%s [%u]: Screen count overflow detected!\n", + __FUNCTION__, __LINE__); + } else { + *count = (uint8_t) xine_count; + } + + screens = malloc(sizeof(screen_data) * xine_count); + + if (screens != NULL) { + for (int i = 0; i < xine_count; i++) { + screens[i] = (screen_data) { + .number = xine_info[i].screen_number, + .x = xine_info[i].x_org, + .y = xine_info[i].y_org, + .width = xine_info[i].width, + .height = xine_info[i].height + }; + } + } + + XFree(xine_info); + } + } + #elif defined(USE_XRANDR) + pthread_mutex_lock(&xrandr_mutex); + if (xrandr_resources != NULL) { + int xrandr_count = xrandr_resources->ncrtc; + if (xrandr_count > UINT8_MAX) { + *count = UINT8_MAX; + + logger(LOG_LEVEL_WARN, "%s [%u]: Screen count overflow detected!\n", + __FUNCTION__, __LINE__); + } else { + *count = (uint8_t) xrandr_count; + } + + screens = malloc(sizeof(screen_data) * xrandr_count); + + if (screens != NULL) { + for (int i = 0; i < xrandr_count; i++) { + XRRCrtcInfo *crtc_info = XRRGetCrtcInfo(display, xrandr_resources, xrandr_resources->crtcs[i]); + + if (crtc_info != NULL) { + screens[i] = (screen_data) { + .number = i + 1, + .x = crtc_info->x, + .y = crtc_info->y, + .width = crtc_info->width, + .height = crtc_info->height + }; + + XRRFreeCrtcInfo(crtc_info); + } else { + logger(LOG_LEVEL_WARN, "%s [%u]: XRandr failed to return crtc information! (%#X)\n", + __FUNCTION__, __LINE__, xrandr_resources->crtcs[i]); + } + } + } + } + pthread_mutex_unlock(&xrandr_mutex); + #else + Screen* default_screen = DefaultScreenOfDisplay(display); + + if (default_screen->width > 0 && default_screen->height > 0) { + screens = malloc(sizeof(screen_data)); + + if (screens != NULL) { + *count = 1; + screens[0] = (screen_data) { + .number = 1, + .x = 0, + .y = 0, + .width = default_screen->width, + .height = default_screen->height + }; + } + } + #endif + } else { + logger(LOG_LEVEL_WARN, "%s [%u]: XDisplay display is unavailable!\n", + __FUNCTION__, __LINE__); + } + + return screens; +} + +UIOHOOK_API long int hook_get_auto_repeat_rate() { + bool successful = false; + long int value = -1; + unsigned int delay = 0, rate = 0; + + // Check and make sure we could connect to the x server. + if (display != NULL) { + // Attempt to acquire the keyboard auto repeat rate using the XKB extension. + if (!successful) { + successful = XkbGetAutoRepeatRate(display, XkbUseCoreKbd, &delay, &rate); + + if (successful) { + logger(LOG_LEVEL_DEBUG, "%s [%u]: XkbGetAutoRepeatRate: %u.\n", + __FUNCTION__, __LINE__, rate); + } + } + + #ifdef USE_XF86MISC + // Fallback to the XF86 Misc extension if available and other efforts failed. + if (!successful) { + XF86MiscKbdSettings kb_info; + successful = (bool) XF86MiscGetKbdSettings(display, &kb_info); + if (successful) { + logger(LOG_LEVEL_DEBUG, "%s [%u]: XF86MiscGetKbdSettings: %i.\n", + __FUNCTION__, __LINE__, kbdinfo.rate); + + delay = (unsigned int) kbdinfo.delay; + rate = (unsigned int) kbdinfo.rate; + } + } + #endif + } else { + logger(LOG_LEVEL_WARN, "%s [%u]: XDisplay display is unavailable!\n", + __FUNCTION__, __LINE__); + } + + if (successful) { + value = (long int) rate; + } + + return value; +} + +UIOHOOK_API long int hook_get_auto_repeat_delay() { + bool successful = false; + long int value = -1; + unsigned int delay = 0, rate = 0; + + // Check and make sure we could connect to the x server. + if (display != NULL) { + // Attempt to acquire the keyboard auto repeat rate using the XKB extension. + if (!successful) { + successful = XkbGetAutoRepeatRate(display, XkbUseCoreKbd, &delay, &rate); + + if (successful) { + logger(LOG_LEVEL_DEBUG, "%s [%u]: XkbGetAutoRepeatRate: %u.\n", + __FUNCTION__, __LINE__, delay); + } + } + + #ifdef USE_XF86MISC + // Fallback to the XF86 Misc extension if available and other efforts failed. + if (!successful) { + XF86MiscKbdSettings kb_info; + successful = (bool) XF86MiscGetKbdSettings(display, &kb_info); + if (successful) { + logger(LOG_LEVEL_DEBUG, "%s [%u]: XF86MiscGetKbdSettings: %i.\n", + __FUNCTION__, __LINE__, kbdinfo.delay); + + delay = (unsigned int) kbdinfo.delay; + rate = (unsigned int) kbdinfo.rate; + } + } + #endif + } else { + logger(LOG_LEVEL_WARN, "%s [%u]: XDisplay display is unavailable!\n", + __FUNCTION__, __LINE__); + } + + if (successful) { + value = (long int) delay; + } + + return value; +} + +UIOHOOK_API long int hook_get_pointer_acceleration_multiplier() { + long int value = -1; + int accel_numerator, accel_denominator, threshold; + + // Check and make sure we could connect to the x server. + if (display != NULL) { + XGetPointerControl(display, &accel_numerator, &accel_denominator, &threshold); + if (accel_denominator >= 0) { + logger(LOG_LEVEL_DEBUG, "%s [%u]: XGetPointerControl: %i.\n", + __FUNCTION__, __LINE__, accel_denominator); + + value = (long int) accel_denominator; + } + } else { + logger(LOG_LEVEL_WARN, "%s [%u]: XDisplay display is unavailable!\n", + __FUNCTION__, __LINE__); + } + + return value; +} + +UIOHOOK_API long int hook_get_pointer_acceleration_threshold() { + long int value = -1; + int accel_numerator, accel_denominator, threshold; + + // Check and make sure we could connect to the x server. + if (display != NULL) { + XGetPointerControl(display, &accel_numerator, &accel_denominator, &threshold); + if (threshold >= 0) { + logger(LOG_LEVEL_DEBUG, "%s [%u]: XGetPointerControl: %i.\n", + __FUNCTION__, __LINE__, threshold); + + value = (long int) threshold; + } + } else { + logger(LOG_LEVEL_WARN, "%s [%u]: XDisplay display is unavailable!\n", + __FUNCTION__, __LINE__); + } + + return value; +} + +UIOHOOK_API long int hook_get_pointer_sensitivity() { + long int value = -1; + int accel_numerator, accel_denominator, threshold; + + // Check and make sure we could connect to the x server. + if (display != NULL) { + XGetPointerControl(display, &accel_numerator, &accel_denominator, &threshold); + if (accel_numerator >= 0) { + logger(LOG_LEVEL_DEBUG, "%s [%u]: XGetPointerControl: %i.\n", + __FUNCTION__, __LINE__, accel_numerator); + + value = (long int) accel_numerator; + } + } else { + logger(LOG_LEVEL_WARN, "%s [%u]: XDisplay display is unavailable!\n", + __FUNCTION__, __LINE__); + } + + return value; +} + +UIOHOOK_API long int hook_get_multi_click_time() { + long int value = 200; + int click_time; + bool successful = false; + + #ifdef USE_XT + // Check and make sure we could connect to the x server. + if (xt_disp != NULL) { + // Try and use the Xt extention to get the current multi-click. + if (!successful) { + // Fall back to the X Toolkit extension if available and other efforts failed. + click_time = XtGetMultiClickTime(xt_disp); + if (click_time >= 0) { + logger(LOG_LEVEL_DEBUG, "%s [%u]: XtGetMultiClickTime: %i.\n", + __FUNCTION__, __LINE__, click_time); + + successful = true; + } + } + } else { + logger(LOG_LEVEL_ERROR, "%s [%u]: %s\n", + __FUNCTION__, __LINE__, "XOpenDisplay failure!"); + } + #endif + + // Check and make sure we could connect to the x server. + if (display != NULL) { + // Try and acquire the multi-click time from the user defined X defaults. + if (!successful) { + char *xprop = XGetDefault(display, "*", "multiClickTime"); + if (xprop != NULL && sscanf(xprop, "%4i", &click_time) != EOF) { + logger(LOG_LEVEL_DEBUG, "%s [%u]: X default 'multiClickTime' property: %i.\n", + __FUNCTION__, __LINE__, click_time); + + successful = true; + } + } + + if (!successful) { + char *xprop = XGetDefault(display, "OpenWindows", "MultiClickTimeout"); + if (xprop != NULL && sscanf(xprop, "%4i", &click_time) != EOF) { + logger(LOG_LEVEL_DEBUG, "%s [%u]: X default 'MultiClickTimeout' property: %i.\n", + __FUNCTION__, __LINE__, click_time); + + successful = true; + } + } + } else { + logger(LOG_LEVEL_WARN, "%s [%u]: XDisplay display is unavailable!\n", + __FUNCTION__, __LINE__); + } + + if (successful) { + value = (long int) click_time; + } + + return value; +} + +// Create a shared object constructor. +__attribute__ ((constructor)) +void on_library_load() { + // Make sure we are initialized for threading. + XInitThreads(); + + // Open local display. + display = XOpenDisplay(XDisplayName(NULL)); + if (display == NULL) { + logger(LOG_LEVEL_ERROR, "%s [%u]: %s\n", + __FUNCTION__, __LINE__, "XOpenDisplay failure!"); + } else { + logger(LOG_LEVEL_DEBUG, "%s [%u]: %s\n", + __FUNCTION__, __LINE__, "XOpenDisplay success."); + } + + #ifdef USE_XRANDR + // Create the thread attribute. + pthread_attr_t settings_thread_attr; + pthread_attr_init(&settings_thread_attr); + + pthread_t settings_thread_id; + if (pthread_create(&settings_thread_id, &settings_thread_attr, settings_thread_proc, NULL) == 0) { + logger(LOG_LEVEL_DEBUG, "%s [%u]: Successfully created settings thread.\n", + __FUNCTION__, __LINE__); + } else { + logger(LOG_LEVEL_ERROR, "%s [%u]: Failed to create settings thread!\n", + __FUNCTION__, __LINE__); + } + + // Make sure the thread attribute is removed. + pthread_attr_destroy(&settings_thread_attr); + #endif + + #ifdef USE_XT + XtToolkitInitialize(); + xt_context = XtCreateApplicationContext(); + + int argc = 0; + char ** argv = { NULL }; + xt_disp = XtOpenDisplay(xt_context, NULL, "UIOHook", "libuiohook", NULL, 0, &argc, argv); + #endif +} + +// Create a shared object destructor. +__attribute__ ((destructor)) +void on_library_unload() { + // Disable the event hook. + //hook_stop(); + + // Cleanup. + unload_input_helper(); + + #ifdef USE_XT + XtCloseDisplay(xt_disp); + XtDestroyApplicationContext(xt_context); + #endif + + // Destroy the native displays. + if (display != NULL) { + XCloseDisplay(display); + display = NULL; + } +}