From 4b16b6aad79103c6fb3af4b4db2f8e60a0cf3274 Mon Sep 17 00:00:00 2001 From: Joel Beckmeyer Date: Mon, 31 Aug 2026 08:44:50 -0400 Subject: [PATCH 1/2] add Undo/Cut/Copy/Paste keycodes --- src/input.cpp | 53 +++++++++++++++++++++++++++++++++++--- src/input.h | 11 ++++++++ tests/unit/test_input.cpp | 54 +++++++++++++++++++++++++++++++++++++++ third-party/libvirtualhid | 2 +- 4 files changed, 116 insertions(+), 4 deletions(-) diff --git a/src/input.cpp b/src/input.cpp index bac39561448..23670ddfbfc 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -66,6 +66,16 @@ namespace input { constexpr auto VKEY_LMENU = 0xA4; ///< Windows virtual-key code for lmenu. constexpr auto VKEY_RMENU = 0xA5; ///< Windows virtual-key code for rmenu. + constexpr auto SS_KEY_UNDO = 0x0100; ///< Non-normalized client keycode for undo. + constexpr auto SS_KEY_CUT = 0x0101; ///< Non-normalized client keycode for cut. + constexpr auto SS_KEY_COPY = 0x0102; ///< Non-normalized client keycode for copy. + constexpr auto SS_KEY_PASTE = 0x0103; ///< Non-normalized client keycode for paste. + + constexpr auto SS_SCANCODE_UNDO = 0x7A; ///< SDL scancode delivered for undo when non-normalized. + constexpr auto SS_SCANCODE_CUT = 0x7B; ///< SDL scancode delivered for cut when non-normalized. + constexpr auto SS_SCANCODE_COPY = 0x7C; ///< SDL scancode delivered for copy when non-normalized. + constexpr auto SS_SCANCODE_PASTE = 0x7D; ///< SDL scancode delivered for paste when non-normalized. + /** * @brief Enumerates supported button state options. */ @@ -1047,7 +1057,36 @@ namespace input { } auto release = util::endian::little(packet->header.magic) == KEY_UP_EVENT_MAGIC; - auto keyCode = packet->keyCode & 0x00FF; + // Keycodes carrying the legacy 0x8000 flag are normalized Windows virtual-key + // codes for which only the low byte is significant. Non-normalized keycodes + // (such as the Undo/Cut/Copy/Paste editing keys) are interpreted as-is. + auto keyCode = (packet->keyCode & 0x8000) ? (packet->keyCode & 0x00FF) : (packet->keyCode & 0xFFFF); + + int modifiers = packet->modifiers; + + // Clients deliver the Undo/Cut/Copy/Paste editing keys as non-normalized + // keycodes, either as the SDL scancode or a dedicated value. Normalize the + // scancode form to the canonical keycode that the platform backend maps to + // the native editing key. The flag gate prevents the F11-F14 virtual keys + // (0x7A-0x7D) from being reinterpreted. + if (packet->flags & SS_KBE_FLAG_NON_NORMALIZED) { + switch (keyCode) { + case SS_SCANCODE_UNDO: + keyCode = SS_KEY_UNDO; + break; + case SS_SCANCODE_CUT: + keyCode = SS_KEY_CUT; + break; + case SS_SCANCODE_COPY: + keyCode = SS_KEY_COPY; + break; + case SS_SCANCODE_PASTE: + keyCode = SS_KEY_PASTE; + break; + default: + break; + } + } if (keyCode == VKEY_LMENU) { input->left_alt_pressed = !release; @@ -1056,7 +1095,6 @@ namespace input { } // Right-alt maps to meta, so it must not also register as ALT - int modifiers = packet->modifiers; if (config::input.key_rightalt_to_key_win && input->right_alt_pressed && !input->left_alt_pressed) { modifiers &= ~MODIFIER_ALT; } @@ -1966,7 +2004,7 @@ namespace input { void reset_keyboard_keys() { for (auto &[key, pressed] : key_press) { if (pressed) { - platf::keyboard_update(platf_input, vk_from_kpid(key) & 0x00FF, true, flags_from_kpid(key)); + platf::keyboard_update(platf_input, vk_from_kpid(key), true, flags_from_kpid(key)); pressed = false; } } @@ -2156,6 +2194,15 @@ namespace input { } return input->gamepads[client_index].id; } + + void keyboard_passthrough(std::shared_ptr &input, std::uint16_t key_code, std::uint8_t modifiers, std::uint8_t flags, bool release) { + NV_KEYBOARD_PACKET packet {}; + packet.header.magic = util::endian::little(static_cast(release ? KEY_UP_EVENT_MAGIC : KEY_DOWN_EVENT_MAGIC)); + packet.keyCode = static_cast(key_code); + packet.modifiers = static_cast(modifiers); + packet.flags = static_cast(flags); + passthrough(input, &packet); + } } // namespace testing #endif } // namespace input diff --git a/src/input.h b/src/input.h index eda3c713952..31404dd875c 100644 --- a/src/input.h +++ b/src/input.h @@ -106,6 +106,17 @@ namespace input { * @return Assigned global gamepad slot, or -1 when unallocated. */ int gamepad_id(const std::shared_ptr &input, std::uint8_t client_index); + + /** + * @brief Synchronously dispatch a keyboard event through the passthrough path for a unit test. + * + * @param input Retained input state. + * @param key_code Client keyboard packet key code. + * @param modifiers Client modifier flags. + * @param flags Client keyboard packet flags. + * @param release Whether the event is a key release. + */ + void keyboard_passthrough(std::shared_ptr &input, std::uint16_t key_code, std::uint8_t modifiers, std::uint8_t flags, bool release); } // namespace testing #endif diff --git a/tests/unit/test_input.cpp b/tests/unit/test_input.cpp index db6e0ee73c8..63d29d48557 100644 --- a/tests/unit/test_input.cpp +++ b/tests/unit/test_input.cpp @@ -4,6 +4,7 @@ */ // standard includes +#include #include #include @@ -116,3 +117,56 @@ TEST_F(InputGamepadSessionTest, RefreshesSharedMouseAfterLicenseStateChanges) { EXPECT_NE(context().mouse->device_id(), original_mouse_id); EXPECT_EQ(runtime().active_device_count(), active_devices); } + +TEST_F(InputGamepadSessionTest, TranslatesEditingKeysToCanonicalKeycodes) { + config::input.keyboard = true; + config::input.key_repeat_delay = std::chrono::milliseconds {0}; + + auto mail = std::make_shared(); + auto session = input::alloc(mail, "editing-keys-client"); + ASSERT_NE(session, nullptr); + ASSERT_NE(context().keyboard, nullptr); + + constexpr std::uint8_t non_normalized = 0x01; // SS_KBE_FLAG_NON_NORMALIZED + + struct editing_key_case { + std::uint16_t client_key; ///< Keycode sent by the client. + std::uint16_t expected_key; ///< Canonical keycode forwarded to the backend. + }; + const editing_key_case cases[] = { + {0x7A, 0x0100}, // SDL scancode Undo + {0x7B, 0x0101}, // SDL scancode Cut + {0x7C, 0x0102}, // SDL scancode Copy + {0x7D, 0x0103}, // SDL scancode Paste + {0x0100, 0x0100}, // Dedicated Undo + {0x0101, 0x0101}, // Dedicated Cut + {0x0102, 0x0102}, // Dedicated Copy + {0x0103, 0x0103}, // Dedicated Paste + }; + + for (const auto &editing_key : cases) { + const auto before = context().keyboard->submit_count(); + + input::testing::keyboard_passthrough(session, editing_key.client_key, 0, non_normalized, false); + EXPECT_EQ(context().keyboard->submit_count(), before + 1); + const auto press_event = context().keyboard->last_submitted_event(); + EXPECT_EQ(press_event.key_code, editing_key.expected_key); + EXPECT_TRUE(press_event.pressed); + + input::testing::keyboard_passthrough(session, editing_key.client_key, 0, non_normalized, true); + EXPECT_EQ(context().keyboard->submit_count(), before + 2); + const auto release_event = context().keyboard->last_submitted_event(); + EXPECT_EQ(release_event.key_code, editing_key.expected_key); + EXPECT_FALSE(release_event.pressed); + } + + // Without the non-normalized flag, 0x7A-0x7D are the F11-F14 virtual keys and + // must pass through untranslated. + const auto before_fn = context().keyboard->submit_count(); + input::testing::keyboard_passthrough(session, 0x7A, 0, 0, false); + EXPECT_EQ(context().keyboard->submit_count(), before_fn + 1); + const auto fn_event = context().keyboard->last_submitted_event(); + EXPECT_EQ(fn_event.key_code, 0x7A); + EXPECT_TRUE(fn_event.pressed); + input::testing::keyboard_passthrough(session, 0x7A, 0, 0, true); +} diff --git a/third-party/libvirtualhid b/third-party/libvirtualhid index 0fcb5d8476d..15ae4416f54 160000 --- a/third-party/libvirtualhid +++ b/third-party/libvirtualhid @@ -1 +1 @@ -Subproject commit 0fcb5d8476d9f54a747a99ca9030d15e26ad5bad +Subproject commit 15ae4416f542279c515b380289e5fd6884230974 From 06caa05cafbc26e4ea770cbc3230d8879bbcb80c Mon Sep 17 00:00:00 2001 From: Joel Beckmeyer Date: Tue, 1 Sep 2026 08:33:41 -0400 Subject: [PATCH 2/2] address code smell warnings for editing keys --- src/input.cpp | 3 ++- tests/unit/test_input.cpp | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/input.cpp b/src/input.cpp index 23670ddfbfc..e09a9e9af0c 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -13,6 +13,7 @@ extern "C" { #include #include #include +#include #include #include #include @@ -1069,7 +1070,7 @@ namespace input { // scancode form to the canonical keycode that the platform backend maps to // the native editing key. The flag gate prevents the F11-F14 virtual keys // (0x7A-0x7D) from being reinterpreted. - if (packet->flags & SS_KBE_FLAG_NON_NORMALIZED) { + if ((static_cast(packet->flags) & static_cast(SS_KBE_FLAG_NON_NORMALIZED)) != std::byte {}) { switch (keyCode) { case SS_SCANCODE_UNDO: keyCode = SS_KEY_UNDO; diff --git a/tests/unit/test_input.cpp b/tests/unit/test_input.cpp index 63d29d48557..486bca52c22 100644 --- a/tests/unit/test_input.cpp +++ b/tests/unit/test_input.cpp @@ -4,6 +4,7 @@ */ // standard includes +#include #include #include #include @@ -133,7 +134,7 @@ TEST_F(InputGamepadSessionTest, TranslatesEditingKeysToCanonicalKeycodes) { std::uint16_t client_key; ///< Keycode sent by the client. std::uint16_t expected_key; ///< Canonical keycode forwarded to the backend. }; - const editing_key_case cases[] = { + const std::array cases {{ {0x7A, 0x0100}, // SDL scancode Undo {0x7B, 0x0101}, // SDL scancode Cut {0x7C, 0x0102}, // SDL scancode Copy @@ -142,7 +143,7 @@ TEST_F(InputGamepadSessionTest, TranslatesEditingKeysToCanonicalKeycodes) { {0x0101, 0x0101}, // Dedicated Cut {0x0102, 0x0102}, // Dedicated Copy {0x0103, 0x0103}, // Dedicated Paste - }; + }}; for (const auto &editing_key : cases) { const auto before = context().keyboard->submit_count();