From 9a3d5b37980e3bec617ccfdf1f5909d6107b2cc7 Mon Sep 17 00:00:00 2001 From: Levi Rak Date: Tue, 31 Mar 2026 19:16:00 -0600 Subject: [PATCH] Add flag to emulate QWERTY in OTG mode Add the flag `--otg-emulate-qwerty` to allow for behaving as though the controlled QWERTY-expecting device had the local keymap. --- app/src/cli.c | 17 +++++++ app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 2 +- app/src/trait/key_processor.h | 8 ++++ app/src/usb/keyboard_aoa.c | 5 +- app/src/usb/keyboard_aoa.h | 4 +- app/src/usb/scrcpy_otg.c | 2 +- app/src/usb/screen_otg.c | 89 ++++++++++++++++++++++++++++++++++- 9 files changed, 123 insertions(+), 6 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index b2e3e30a..cb12a98c 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -61,6 +61,7 @@ enum { OPT_RAW_KEY_EVENTS, OPT_NO_DOWNSIZE_ON_ERROR, OPT_OTG, + OPT_OTG_EMULATE_QWERTY, OPT_NO_CLEANUP, OPT_PRINT_FPS, OPT_NO_POWER_ON, @@ -745,6 +746,14 @@ static const struct sc_option options[] = { "It may only work over USB.\n" "See --keyboard, --mouse and --gamepad.", }, + { + .longopt_id = OPT_OTG_EMULATE_QWERTY, + .longopt = "otg-emulate-qwerty", + .text = "When running in OTG mode, send the keypress corresponding to " + "the logical layout rather than the physical layout.\n" + "Useful when using an AZERTY, Dvorak or Colemak layout to " + "control a device configured to use QWERTY.", + }, { .shortopt = 'p', .longopt = "port", @@ -2685,6 +2694,14 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], #else LOGE("OTG mode (--otg) is disabled."); return false; +#endif + case OPT_OTG_EMULATE_QWERTY: +#ifdef HAVE_USB + opts->otg_emulate_qwerty = true; + break; +#else + LOGE("OTG mode (--otg) is disabled."); + return false; #endif case OPT_V4L2_SINK: #ifdef HAVE_V4L2 diff --git a/app/src/options.c b/app/src/options.c index 0fe82d29..fb5cc1e7 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -73,6 +73,7 @@ const struct scrcpy_options scrcpy_options_default = { #endif #ifdef HAVE_USB .otg = false, + .otg_emulate_qwerty = false, #endif .show_touches = false, .fullscreen = false, diff --git a/app/src/options.h b/app/src/options.h index 03b42913..82285708 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -283,6 +283,7 @@ struct scrcpy_options { #endif #ifdef HAVE_USB bool otg; + bool otg_emulate_qwerty; #endif bool show_touches; bool fullscreen; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index aedfdf9c..4079ce9c 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -702,7 +702,7 @@ scrcpy(struct scrcpy_options *options) { bool aoa_fail = false; if (use_keyboard_aoa) { - if (sc_keyboard_aoa_init(&s->keyboard_aoa, &s->aoa)) { + if (sc_keyboard_aoa_init(&s->keyboard_aoa, &s->aoa, false)) { keyboard_aoa_initialized = true; kp = &s->keyboard_aoa.key_processor; } else { diff --git a/app/src/trait/key_processor.h b/app/src/trait/key_processor.h index 9e9bb86e..a71dd033 100644 --- a/app/src/trait/key_processor.h +++ b/app/src/trait/key_processor.h @@ -29,6 +29,14 @@ struct sc_key_processor { */ bool hid; + /** + * Set by the implementation to indicate that scancodes should be falsified + * such that keystrokes are sent as though the logical keymap were the + * physical keymap. Useful when using an AZERTY, Dvorak or Colemak layout + * to control a device configured to use QWERTY. + */ + bool use_logical_scancodes; + const struct sc_key_processor_ops *ops; }; diff --git a/app/src/usb/keyboard_aoa.c b/app/src/usb/keyboard_aoa.c index 8f5cb755..a669b8b2 100644 --- a/app/src/usb/keyboard_aoa.c +++ b/app/src/usb/keyboard_aoa.c @@ -63,7 +63,9 @@ sc_key_processor_process_key(struct sc_key_processor *kp, } bool -sc_keyboard_aoa_init(struct sc_keyboard_aoa *kb, struct sc_aoa *aoa) { +sc_keyboard_aoa_init(struct sc_keyboard_aoa *kb, + struct sc_aoa *aoa, + bool use_logical_scancodes) { kb->aoa = aoa; struct sc_hid_open hid_open; @@ -91,6 +93,7 @@ sc_keyboard_aoa_init(struct sc_keyboard_aoa *kb, struct sc_aoa *aoa) { // to be acknowledged by the device before injecting Ctrl+v. kb->key_processor.async_paste = true; kb->key_processor.hid = true; + kb->key_processor.use_logical_scancodes = use_logical_scancodes; kb->key_processor.ops = &ops; return true; diff --git a/app/src/usb/keyboard_aoa.h b/app/src/usb/keyboard_aoa.h index 9e9500a3..6dc5dee5 100644 --- a/app/src/usb/keyboard_aoa.h +++ b/app/src/usb/keyboard_aoa.h @@ -19,7 +19,9 @@ struct sc_keyboard_aoa { }; bool -sc_keyboard_aoa_init(struct sc_keyboard_aoa *kb, struct sc_aoa *aoa); +sc_keyboard_aoa_init(struct sc_keyboard_aoa *kb, + struct sc_aoa *aoa, + bool use_logical_scancodes); void sc_keyboard_aoa_destroy(struct sc_keyboard_aoa *kb); diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index 1a9cc46e..03c78fa4 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -157,7 +157,7 @@ scrcpy_otg(struct scrcpy_options *options) { options->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_AOA; if (enable_keyboard) { - ok = sc_keyboard_aoa_init(&s->keyboard, &s->aoa); + ok = sc_keyboard_aoa_init(&s->keyboard, &s->aoa, options->otg_emulate_qwerty); if (!ok) { goto end; } diff --git a/app/src/usb/screen_otg.c b/app/src/usb/screen_otg.c index 5c580df9..265fcdce 100644 --- a/app/src/usb/screen_otg.c +++ b/app/src/usb/screen_otg.c @@ -100,16 +100,101 @@ sc_screen_otg_destroy(struct sc_screen_otg *screen) { SDL_DestroyWindow(screen->window); } +static const enum sc_scancode keycode_to_scancode[] = { + /* SDL2 has SDL_GetScancodeFromKey, but that uses the current keymap */ + + [SC_KEYCODE_RETURN] = SC_SCANCODE_RETURN, + [SC_KEYCODE_ESCAPE] = SC_SCANCODE_ESCAPE, + [SC_KEYCODE_BACKSPACE] = SC_SCANCODE_BACKSPACE, + [SC_KEYCODE_TAB] = SC_SCANCODE_TAB, + [SC_KEYCODE_SPACE] = SC_SCANCODE_SPACE, + [SC_KEYCODE_EXCLAIM] = SC_SCANCODE_1, + [SC_KEYCODE_QUOTEDBL] = SC_SCANCODE_APOSTROPHE, + [SC_KEYCODE_HASH] = SC_SCANCODE_3, + [SC_KEYCODE_PERCENT] = SC_SCANCODE_5, + [SC_KEYCODE_DOLLAR] = SC_SCANCODE_4, + [SC_KEYCODE_AMPERSAND] = SC_SCANCODE_7, + [SC_KEYCODE_QUOTE] = SC_SCANCODE_APOSTROPHE, + [SC_KEYCODE_LEFTPAREN] = SC_SCANCODE_9, + [SC_KEYCODE_RIGHTPAREN] = SC_SCANCODE_0, + [SC_KEYCODE_ASTERISK] = SC_SCANCODE_8, + [SC_KEYCODE_PLUS] = SC_SCANCODE_EQUALS, + [SC_KEYCODE_COMMA] = SC_SCANCODE_COMMA, + [SC_KEYCODE_MINUS] = SC_SCANCODE_MINUS, + [SC_KEYCODE_PERIOD] = SC_SCANCODE_PERIOD, + [SC_KEYCODE_SLASH] = SC_SCANCODE_SLASH, + [SC_KEYCODE_0] = SC_SCANCODE_0, + [SC_KEYCODE_1] = SC_SCANCODE_1, + [SC_KEYCODE_2] = SC_SCANCODE_2, + [SC_KEYCODE_3] = SC_SCANCODE_3, + [SC_KEYCODE_4] = SC_SCANCODE_4, + [SC_KEYCODE_5] = SC_SCANCODE_5, + [SC_KEYCODE_6] = SC_SCANCODE_6, + [SC_KEYCODE_7] = SC_SCANCODE_7, + [SC_KEYCODE_8] = SC_SCANCODE_8, + [SC_KEYCODE_9] = SC_SCANCODE_9, + [SC_KEYCODE_COLON] = SC_SCANCODE_SEMICOLON, + [SC_KEYCODE_SEMICOLON] = SC_SCANCODE_SEMICOLON, + [SC_KEYCODE_LESS] = SC_SCANCODE_COMMA, + [SC_KEYCODE_EQUALS] = SC_SCANCODE_EQUALS, + [SC_KEYCODE_GREATER] = SC_SCANCODE_PERIOD, + [SC_KEYCODE_QUESTION] = SC_SCANCODE_SLASH, + [SC_KEYCODE_AT] = SC_SCANCODE_2, + + [SC_KEYCODE_LEFTBRACKET] = SC_SCANCODE_LEFTBRACKET, + [SC_KEYCODE_BACKSLASH] = SC_SCANCODE_BACKSLASH, + [SC_KEYCODE_RIGHTBRACKET] = SC_SCANCODE_RIGHTBRACKET, + [SC_KEYCODE_CARET] = SC_SCANCODE_6, + [SC_KEYCODE_UNDERSCORE] = SC_SCANCODE_MINUS, + [SC_KEYCODE_BACKQUOTE] = SC_SCANCODE_GRAVE, + [SC_KEYCODE_a] = SC_SCANCODE_A, + [SC_KEYCODE_b] = SC_SCANCODE_B, + [SC_KEYCODE_c] = SC_SCANCODE_C, + [SC_KEYCODE_d] = SC_SCANCODE_D, + [SC_KEYCODE_e] = SC_SCANCODE_E, + [SC_KEYCODE_f] = SC_SCANCODE_F, + [SC_KEYCODE_g] = SC_SCANCODE_G, + [SC_KEYCODE_h] = SC_SCANCODE_H, + [SC_KEYCODE_i] = SC_SCANCODE_I, + [SC_KEYCODE_j] = SC_SCANCODE_J, + [SC_KEYCODE_k] = SC_SCANCODE_K, + [SC_KEYCODE_l] = SC_SCANCODE_L, + [SC_KEYCODE_m] = SC_SCANCODE_M, + [SC_KEYCODE_n] = SC_SCANCODE_N, + [SC_KEYCODE_o] = SC_SCANCODE_O, + [SC_KEYCODE_p] = SC_SCANCODE_P, + [SC_KEYCODE_q] = SC_SCANCODE_Q, + [SC_KEYCODE_r] = SC_SCANCODE_R, + [SC_KEYCODE_s] = SC_SCANCODE_S, + [SC_KEYCODE_t] = SC_SCANCODE_T, + [SC_KEYCODE_u] = SC_SCANCODE_U, + [SC_KEYCODE_v] = SC_SCANCODE_V, + [SC_KEYCODE_w] = SC_SCANCODE_W, + [SC_KEYCODE_x] = SC_SCANCODE_X, + [SC_KEYCODE_y] = SC_SCANCODE_Y, + [SC_KEYCODE_z] = SC_SCANCODE_Z, +}; + static void sc_screen_otg_process_key(struct sc_screen_otg *screen, const SDL_KeyboardEvent *event) { assert(screen->keyboard); struct sc_key_processor *kp = &screen->keyboard->key_processor; + enum sc_keycode keycode = sc_keycode_from_sdl(event->keysym.sym); + enum sc_scancode scancode = sc_scancode_from_sdl(event->keysym.scancode); + + if (kp->use_logical_scancodes && keycode < ARRAY_LEN(keycode_to_scancode)) { + enum sc_scancode logical_scancode = keycode_to_scancode[keycode]; + if (logical_scancode != 0) { + scancode = logical_scancode; + } + } + struct sc_key_event evt = { .action = sc_action_from_sdl_keyboard_type(event->type), - .keycode = sc_keycode_from_sdl(event->keysym.sym), - .scancode = sc_scancode_from_sdl(event->keysym.scancode), + .keycode = keycode, + .scancode = scancode, .repeat = event->repeat, .mods_state = sc_mods_state_from_sdl(event->keysym.mod), };