From a325b2395cefc5f4edc661e7a68f49dc42b375bd Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Sat, 20 Jun 2026 14:03:56 -0400 Subject: [PATCH] refactor(sonar): remove duplication --- README.md | 3 +- src/core/runtime.cpp | 116 ++++++++++++----------- src/platform/linux/uhid_backend.cpp | 139 ++++++++++++---------------- tests/unit/test_linux_consumers.cpp | 115 ++++++++++++----------- 4 files changed, 178 insertions(+), 195 deletions(-) diff --git a/README.md b/README.md index 5c3929f..fefdedd 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,8 @@ but reports the affected capability as unavailable and returns The Linux backend uses `libevdev` internally to construct uinput keyboard, mouse, touchscreen, trackpad, and pen tablet devices. Consumers still use the same platform-neutral C++ API; `libevdev` is a Linux build dependency, not a -public API dependency. +public API dependency. UTF-8 keyboard text submission is supported through the +same Unicode compose sequence for both uinput keyboards and the XTest fallback. The Linux packaging model needs `/dev/uinput` and `/dev/uhid` access. Install a udev rules file such as `/etc/udev/rules.d/60-libvirtualhid.rules` with: diff --git a/src/core/runtime.cpp b/src/core/runtime.cpp index bacdaba..fce2171 100644 --- a/src/core/runtime.cpp +++ b/src/core/runtime.cpp @@ -269,6 +269,30 @@ namespace lvh { }); } + template + OperationStatus submit_touch_event( + const auto &device_ptr, + const char *closed_message, + BackendAction backend_action, + DeviceUpdate device_update + ) { + return with_device(device_ptr, [closed_message, backend_action, device_update](auto &device) { + if (!device.open) { + return OperationStatus::failure(ErrorCode::device_closed, closed_message); + } + + if (device.backend) { + if (const auto status = backend_action(*device.backend); !status.ok()) { + return status; + } + } + + device_update(device); + ++device.submitted_events; + return OperationStatus::success(); + }); + } + template std::size_t count_open_devices(const DeviceList &devices) { std::size_t count = 0; @@ -707,21 +731,16 @@ namespace lvh { return validation; } - return with_device(device_, [&contact](auto &device) { - if (!device.open) { - return OperationStatus::failure(ErrorCode::device_closed, "touchscreen is closed"); - } - - if (device.backend) { - if (const auto status = device.backend->place_contact(contact); !status.ok()) { - return status; - } + return submit_touch_event( + device_, + "touchscreen is closed", + [&contact](auto &backend) { + return backend.place_contact(contact); + }, + [&contact](auto &device) { + device.last_contact = contact; } - - device.last_contact = contact; - ++device.submitted_events; - return OperationStatus::success(); - }); + ); } OperationStatus Touchscreen::release_contact(std::int32_t contact_id) { @@ -729,21 +748,16 @@ namespace lvh { return OperationStatus::failure(ErrorCode::invalid_argument, "touch contact id must not be negative"); } - return with_device(device_, [contact_id](auto &device) { - if (!device.open) { - return OperationStatus::failure(ErrorCode::device_closed, "touchscreen is closed"); + return submit_touch_event( + device_, + "touchscreen is closed", + [contact_id](auto &backend) { + return backend.release_contact(contact_id); + }, + [contact_id](auto &device) { + device.last_contact.id = contact_id; } - - if (device.backend) { - if (const auto status = device.backend->release_contact(contact_id); !status.ok()) { - return status; - } - } - - device.last_contact.id = contact_id; - ++device.submitted_events; - return OperationStatus::success(); - }); + ); } TouchContact Touchscreen::last_submitted_contact() const { @@ -814,21 +828,16 @@ namespace lvh { return validation; } - return with_device(device_, [&contact](auto &device) { - if (!device.open) { - return OperationStatus::failure(ErrorCode::device_closed, "trackpad is closed"); + return submit_touch_event( + device_, + "trackpad is closed", + [&contact](auto &backend) { + return backend.place_contact(contact); + }, + [&contact](auto &device) { + device.last_contact = contact; } - - if (device.backend) { - if (const auto status = device.backend->place_contact(contact); !status.ok()) { - return status; - } - } - - device.last_contact = contact; - ++device.submitted_events; - return OperationStatus::success(); - }); + ); } OperationStatus Trackpad::release_contact(std::int32_t contact_id) { @@ -836,21 +845,16 @@ namespace lvh { return OperationStatus::failure(ErrorCode::invalid_argument, "touch contact id must not be negative"); } - return with_device(device_, [contact_id](auto &device) { - if (!device.open) { - return OperationStatus::failure(ErrorCode::device_closed, "trackpad is closed"); - } - - if (device.backend) { - if (const auto status = device.backend->release_contact(contact_id); !status.ok()) { - return status; - } + return submit_touch_event( + device_, + "trackpad is closed", + [contact_id](auto &backend) { + return backend.release_contact(contact_id); + }, + [contact_id](auto &device) { + device.last_contact.id = contact_id; } - - device.last_contact.id = contact_id; - ++device.submitted_events; - return OperationStatus::success(); - }); + ); } OperationStatus Trackpad::button(bool pressed) { diff --git a/src/platform/linux/uhid_backend.cpp b/src/platform/linux/uhid_backend.cpp index a6d8633..c22a375 100644 --- a/src/platform/linux/uhid_backend.cpp +++ b/src/platform/linux/uhid_backend.cpp @@ -713,6 +713,57 @@ namespace lvh::detail { return static_cast(0x41 + (digit - 'A')); } + template + OperationStatus submit_keyboard_events(const std::array &events, SubmitKeyEvent &submit_key_event) { + for (const auto &event : events) { + if (const auto status = submit_key_event(event); !status.ok()) { + return status; + } + } + return OperationStatus::success(); + } + + template + OperationStatus type_text_with_unicode_hex(std::string_view text, SubmitKeyEvent submit_key_event) { + static constexpr std::array unicode_hex_prefix {{ + {.key_code = 0xA2, .pressed = true}, + {.key_code = 0xA0, .pressed = true}, + {.key_code = 0x55, .pressed = true}, + {.key_code = 0x55, .pressed = false}, + {.key_code = 0xA0, .pressed = false}, + {.key_code = 0xA2, .pressed = false}, + }}; + static constexpr std::array unicode_hex_suffix {{ + {.key_code = 0x0D, .pressed = true}, + {.key_code = 0x0D, .pressed = false}, + }}; + + for (const auto codepoint : decode_utf8(text)) { + const auto hex = uppercase_hex(codepoint); + + if (const auto status = submit_keyboard_events(unicode_hex_prefix, submit_key_event); !status.ok()) { + return status; + } + + for (const auto digit : hex) { + const auto key_code = hex_digit_key_code(digit); + const std::array digit_events {{ + {.key_code = key_code, .pressed = true}, + {.key_code = key_code, .pressed = false}, + }}; + if (const auto status = submit_keyboard_events(digit_events, submit_key_event); !status.ok()) { + return status; + } + } + + if (const auto status = submit_keyboard_events(unicode_hex_suffix, submit_key_event); !status.ok()) { + return status; + } + } + + return OperationStatus::success(); + } + [[maybe_unused]] int legacy_scroll_steps(std::int32_t distance) { if (distance == 0) { return 0; @@ -1158,47 +1209,9 @@ namespace lvh::detail { } OperationStatus type_text(const KeyboardTextEvent &event) override { - for (const auto codepoint : decode_utf8(event.text)) { - const auto hex = uppercase_hex(codepoint); - - if (const auto status = submit({.key_code = 0xA2, .pressed = true}); !status.ok()) { - return status; - } - if (const auto status = submit({.key_code = 0xA0, .pressed = true}); !status.ok()) { - return status; - } - if (const auto status = submit({.key_code = 0x55, .pressed = true}); !status.ok()) { - return status; - } - if (const auto status = submit({.key_code = 0x55, .pressed = false}); !status.ok()) { - return status; - } - if (const auto status = submit({.key_code = 0xA0, .pressed = false}); !status.ok()) { - return status; - } - if (const auto status = submit({.key_code = 0xA2, .pressed = false}); !status.ok()) { - return status; - } - - for (const auto digit : hex) { - const auto key_code = hex_digit_key_code(digit); - if (const auto status = submit({.key_code = key_code, .pressed = true}); !status.ok()) { - return status; - } - if (const auto status = submit({.key_code = key_code, .pressed = false}); !status.ok()) { - return status; - } - } - - if (const auto status = submit({.key_code = 0x0D, .pressed = true}); !status.ok()) { - return status; - } - if (const auto status = submit({.key_code = 0x0D, .pressed = false}); !status.ok()) { - return status; - } - } - - return OperationStatus::success(); + return type_text_with_unicode_hex(event.text, [this](const KeyboardEvent &key_event) { + return submit(key_event); + }); } OperationStatus close() override { @@ -1952,47 +1965,9 @@ namespace lvh::detail { } OperationStatus type_text(const KeyboardTextEvent &event) override { - for (const auto codepoint : decode_utf8(event.text)) { - const auto hex = uppercase_hex(codepoint); - - if (const auto status = submit({.key_code = 0xA2, .pressed = true}); !status.ok()) { - return status; - } - if (const auto status = submit({.key_code = 0xA0, .pressed = true}); !status.ok()) { - return status; - } - if (const auto status = submit({.key_code = 0x55, .pressed = true}); !status.ok()) { - return status; - } - if (const auto status = submit({.key_code = 0x55, .pressed = false}); !status.ok()) { - return status; - } - if (const auto status = submit({.key_code = 0xA0, .pressed = false}); !status.ok()) { - return status; - } - if (const auto status = submit({.key_code = 0xA2, .pressed = false}); !status.ok()) { - return status; - } - - for (const auto digit : hex) { - const auto key_code = hex_digit_key_code(digit); - if (const auto status = submit({.key_code = key_code, .pressed = true}); !status.ok()) { - return status; - } - if (const auto status = submit({.key_code = key_code, .pressed = false}); !status.ok()) { - return status; - } - } - - if (const auto status = submit({.key_code = 0x0D, .pressed = true}); !status.ok()) { - return status; - } - if (const auto status = submit({.key_code = 0x0D, .pressed = false}); !status.ok()) { - return status; - } - } - - return OperationStatus::success(); + return type_text_with_unicode_hex(event.text, [this](const KeyboardEvent &key_event) { + return submit(key_event); + }); } OperationStatus close() override { diff --git a/tests/unit/test_linux_consumers.cpp b/tests/unit/test_linux_consumers.cpp index 27f3498..d15e8fa 100644 --- a/tests/unit/test_linux_consumers.cpp +++ b/tests/unit/test_linux_consumers.cpp @@ -313,8 +313,7 @@ namespace { const auto controller_axis_moved = sdl_controller_has_moved_axis(controller); const auto joystick_button_pressed = joystick != nullptr && sdl_joystick_has_pressed_button(joystick); - if (const auto joystick_axis_moved = joystick != nullptr && sdl_joystick_has_moved_axis(joystick); - (controller_button_pressed || joystick_button_pressed) && (controller_axis_moved || joystick_axis_moved)) { + if (const auto joystick_axis_moved = joystick != nullptr && sdl_joystick_has_moved_axis(joystick); (controller_button_pressed || joystick_button_pressed) && (controller_axis_moved || joystick_axis_moved)) { return true; } @@ -339,24 +338,10 @@ namespace { return runtime.create_gamepad(options); } - void expect_sdl_joystick_profile(SDL_Joystick *joystick, const lvh::DeviceProfile &profile, int minimum_buttons, int minimum_axes) { - EXPECT_EQ(SDL_JoystickGetVendor(joystick), profile.vendor_id); - EXPECT_EQ(SDL_JoystickGetProduct(joystick), profile.product_id); - EXPECT_GE(SDL_JoystickNumButtons(joystick), minimum_buttons); - EXPECT_GE(SDL_JoystickNumAxes(joystick), minimum_axes); - } - - void expect_sdl_dualsense_controller_profile(SDL_GameController *controller) { - auto *mapping = SDL_GameControllerMapping(controller); - EXPECT_NE(mapping, nullptr) << SDL_GetError(); - if (mapping != nullptr) { - SDL_free(mapping); - } - } - - void run_sdl_uhid_joystick_test(const SdlGamepadConsumerCase &test_case) { + template + void run_sdl_gamepad_test(const SdlGamepadConsumerCase &test_case, Uint32 init_flags, TestBody test_body) { configure_sdl_hidapi_hints(); - ASSERT_EQ(SDL_Init(SDL_INIT_JOYSTICK | SDL_INIT_EVENTS), 0) << SDL_GetError(); + ASSERT_EQ(SDL_Init(init_flags), 0) << SDL_GetError(); ScopeExit sdl_quit {[]() { SDL_Quit(); }}; @@ -378,46 +363,30 @@ namespace { const auto joystick_index = wait_for_sdl_joystick(expected_profile); ASSERT_GE(joystick_index, 0); - SdlJoystick joystick {SDL_JoystickOpen(joystick_index), &SDL_JoystickClose}; - ASSERT_NE(joystick.get(), nullptr) << SDL_GetError(); - expect_sdl_joystick_profile( - joystick.get(), - expected_profile, - test_case.minimum_buttons, - test_case.minimum_axes - ); - - lvh::GamepadState state; - state.buttons.set(lvh::GamepadButton::a); - state.left_stick = {0.75F, -0.5F}; - ASSERT_TRUE(created.gamepad->submit(state).ok()); - - EXPECT_TRUE(wait_for_sdl_gamepad_input(joystick.get())) << describe_sdl_state(joystick.get()); + test_body(expected_profile, joystick_index, *created.gamepad); } - void run_sdl_dualsense_controller_test(const SdlGamepadConsumerCase &test_case) { - configure_sdl_hidapi_hints(); - ASSERT_EQ(SDL_Init(SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_EVENTS), 0) << SDL_GetError(); - ScopeExit sdl_quit {[]() { - SDL_Quit(); - }}; - - lvh::RuntimeOptions runtime_options; - runtime_options.backend = lvh::BackendKind::platform_default; - auto runtime = lvh::Runtime::create(runtime_options); - ASSERT_TRUE(runtime->capabilities().supports_gamepad); - - const auto expected_profile = [&test_case]() { - auto profile = test_case.profile; - profile.name = unique_device_name(test_case.name_suffix); - return profile; - }(); + void expect_sdl_joystick_profile(SDL_Joystick *joystick, const lvh::DeviceProfile &profile, int minimum_buttons, int minimum_axes) { + EXPECT_EQ(SDL_JoystickGetVendor(joystick), profile.vendor_id); + EXPECT_EQ(SDL_JoystickGetProduct(joystick), profile.product_id); + EXPECT_GE(SDL_JoystickNumButtons(joystick), minimum_buttons); + EXPECT_GE(SDL_JoystickNumAxes(joystick), minimum_axes); + } - auto created = create_sdl_gamepad(*runtime, test_case); - ASSERT_TRUE(created) << created.status.message(); + void expect_sdl_dualsense_controller_profile(SDL_GameController *controller) { + auto *mapping = SDL_GameControllerMapping(controller); + EXPECT_NE(mapping, nullptr) << SDL_GetError(); + if (mapping != nullptr) { + SDL_free(mapping); + } + } - const auto joystick_index = wait_for_sdl_joystick(expected_profile); - ASSERT_GE(joystick_index, 0); + void exercise_sdl_dualsense_controller( + const SdlGamepadConsumerCase &test_case, + const lvh::DeviceProfile &expected_profile, + int joystick_index, + lvh::Gamepad &gamepad + ) { ASSERT_EQ(SDL_IsGameController(joystick_index), SDL_TRUE) << SDL_GetError(); SdlGameController controller {SDL_GameControllerOpen(joystick_index), &SDL_GameControllerClose}; @@ -445,7 +414,7 @@ namespace { state.gyroscope = lvh::Vector3 {.x = 4.0F, .y = 5.0F, .z = 6.0F}; state.battery = lvh::GamepadBattery {.state = lvh::GamepadBatteryState::charging, .percentage = 80}; state.touchpad_contacts[0] = {.id = 1, .active = true, .x = 0.5F, .y = 0.25F}; - ASSERT_TRUE(created.gamepad->submit(state).ok()); + ASSERT_TRUE(gamepad.submit(state).ok()); expect_sdl_dualsense_controller_profile(controller.get()); if (test_case.expect_live_input) { @@ -453,6 +422,40 @@ namespace { } } + void run_sdl_uhid_joystick_test(const SdlGamepadConsumerCase &test_case) { + run_sdl_gamepad_test( + test_case, + SDL_INIT_JOYSTICK | SDL_INIT_EVENTS, + [&test_case](const auto &expected_profile, int joystick_index, lvh::Gamepad &gamepad) { + SdlJoystick joystick {SDL_JoystickOpen(joystick_index), &SDL_JoystickClose}; + ASSERT_NE(joystick.get(), nullptr) << SDL_GetError(); + expect_sdl_joystick_profile( + joystick.get(), + expected_profile, + test_case.minimum_buttons, + test_case.minimum_axes + ); + + lvh::GamepadState state; + state.buttons.set(lvh::GamepadButton::a); + state.left_stick = {0.75F, -0.5F}; + ASSERT_TRUE(gamepad.submit(state).ok()); + + EXPECT_TRUE(wait_for_sdl_gamepad_input(joystick.get())) << describe_sdl_state(joystick.get()); + } + ); + } + + void run_sdl_dualsense_controller_test(const SdlGamepadConsumerCase &test_case) { + run_sdl_gamepad_test( + test_case, + SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_EVENTS, + [&test_case](const auto &expected_profile, int joystick_index, lvh::Gamepad &gamepad) { + exercise_sdl_dualsense_controller(test_case, expected_profile, joystick_index, gamepad); + } + ); + } + void destroy_libinput_event(libinput_event *event) { if (event != nullptr) { libinput_event_destroy(event);