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.
This commit is contained in:
Levi Rak 2026-03-31 19:16:00 -06:00
parent 3fcc177da5
commit 9a3d5b3798
9 changed files with 123 additions and 6 deletions

View file

@ -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

View file

@ -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,

View file

@ -283,6 +283,7 @@ struct scrcpy_options {
#endif
#ifdef HAVE_USB
bool otg;
bool otg_emulate_qwerty;
#endif
bool show_touches;
bool fullscreen;

View file

@ -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 {

View file

@ -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;
};

View file

@ -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;

View file

@ -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);

View file

@ -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;
}

View file

@ -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),
};