Skip to content

Commit 2dca4f6

Browse files
Dilbag SinghDilbag Singh
authored andcommitted
GUACAMOLE-XXXX: Fix Ctrl/Shift scancode issues for Unicode (RU/EN) layouts in RDP.
When using Guacamole with the Unicode keyboard layout (ru-ru or en-us), text input works fine but modifier keys (Ctrl, Shift, Alt) do not trigger correctly inside RDP sessions. This especially breaks shortcuts like Ctrl+C, Ctrl+V, and Ctrl+Z, as well as Shift-based capital input in Cyrillic mode. Root cause: - The existing keyboard.c scancode mapping and Unicode event handler ignored control key states when switching layouts. - Unicode events were sent without associated scancode synchronization, causing RDP to treat Ctrl/Shift as unpressed. Fix: - Updated keyboard.c to properly handle Ctrl, Alt, and Shift state transitions alongside Unicode key events. - Ensures Unicode layouts (English and Russian) transmit modifier combinations correctly to RDP servers. Tested on: - Guacamole 1.6.0 / 1.7.0 (patched build) - FreeRDP 2.11.5 - Windows Server 2019 RDP and Windows 10 Pro RDP Verified results: Ctrl+C / Ctrl+V / Ctrl+Z now work in both English and Russian layouts. Shift-based capital input functions correctly. Layout switching (Alt+Shift) does not break input handling. This patch makes Guacamole fully usable in mixed-language (RU/EN) environments using Unicode keyboard input.
1 parent 3398603 commit 2dca4f6

1 file changed

Lines changed: 223 additions & 1 deletion

File tree

src/protocols/rdp/keyboard.c

Lines changed: 223 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
#include <guacamole/client.h>
2828
#include <guacamole/mem.h>
2929
#include <guacamole/rwlock.h>
30-
3130
#include <stdlib.h>
3231

3332
/**
@@ -562,7 +561,229 @@ static void guac_rdp_keyboard_send_missing_key(guac_rdp_keyboard* keyboard,
562561

563562
guac_client* client = keyboard->client;
564563
guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
564+
565+
/* If system modifiers (Ctrl/Alt) are held, prefer sending US scancode so
566+
* that shortcuts like Ctrl+C/V/A work even in Unicode/failsafe layout. */
567+
int ctrl_or_alt_pressed =
568+
guac_rdp_keyboard_is_pressed(keyboard, GUAC_RDP_KEYSYM_LCTRL)
569+
|| guac_rdp_keyboard_is_pressed(keyboard, GUAC_RDP_KEYSYM_RCTRL)
570+
|| guac_rdp_keyboard_is_pressed(keyboard, GUAC_RDP_KEYSYM_LALT)
571+
|| guac_rdp_keyboard_is_pressed(keyboard, GUAC_RDP_KEYSYM_RALT);
572+
573+
/* Normalize Cyrillic letters to Latin equivalents when Ctrl/Alt pressed
574+
* so that shortcuts like Ctrl+C, Ctrl+V, etc. work even in Russian layout.
575+
*/
576+
if (ctrl_or_alt_pressed) {
577+
578+
579+
/* Treat Ctrl or Alt (but NOT AltGr) as shortcut modifiers */
580+
int ctrl_pressed =
581+
guac_rdp_keyboard_is_pressed(keyboard, GUAC_RDP_KEYSYM_LCTRL)
582+
|| guac_rdp_keyboard_is_pressed(keyboard, GUAC_RDP_KEYSYM_RCTRL);
583+
584+
int alt_pressed =
585+
guac_rdp_keyboard_is_pressed(keyboard, GUAC_RDP_KEYSYM_LALT)
586+
|| guac_rdp_keyboard_is_pressed(keyboard, GUAC_RDP_KEYSYM_RALT);
587+
588+
/* AltGr appears as Ctrl+Alt together; don’t hijack that. */
589+
int altgr = ctrl_pressed && alt_pressed;
590+
591+
/* We only force scancodes for real shortcuts: Ctrl-only OR Alt-only. */
592+
int shortcut_mod = !altgr && (ctrl_pressed || alt_pressed);
593+
594+
if (shortcut_mod) {
595+
/* Map both Latin and Russian keysyms for C/V to US scancodes.
596+
* US Set 1 scancodes: C=0x2E, V=0x2F.
597+
* This makes Ctrl+C / Ctrl+V work in ru-RU and en-US equally.
598+
*/
599+
int sc = 0;
600+
601+
switch (keysym) {
602+
/* --- Ctrl+C --- */
603+
case 'c': case 'C':
604+
case 0x441: /* Cyrillic small 'с' */
605+
case 0x421: /* Cyrillic capital 'С' */
606+
sc = 0x2E; /* US 'C' key */
607+
break;
608+
609+
/* --- Ctrl+V --- */
610+
case 'v': case 'V':
611+
case 0x43C: /* Cyrillic small 'м' */
612+
case 0x41C: /* Cyrillic capital 'М' */
613+
sc = 0x2F; /* US 'V' key */
614+
break;
615+
616+
default:
617+
sc = 0;
618+
}
565619

620+
if (sc) {
621+
guac_client_log(client, GUAC_LOG_DEBUG,
622+
"Shortcut: modifiers active, sending US scancode 0x%X for keysym 0x%X",
623+
sc, keysym);
624+
/* press + release with no extended flags */
625+
guac_rdp_send_key_event(rdp_client, sc, 0, 1);
626+
guac_rdp_send_key_event(rdp_client, sc, 0, 0);
627+
return;
628+
}
629+
}
630+
switch (keysym) {
631+
/* Lowercase Cyrillic */
632+
case 0x430: keysym = 'f'; break; // а
633+
case 0x431: keysym = ','; break; // б
634+
case 0x432: keysym = 'd'; break; // в
635+
case 0x433: keysym = 'u'; break; // г
636+
case 0x434: keysym = 'l'; break; // д
637+
case 0x435: keysym = 't'; break; // е
638+
case 0x436: keysym = ';'; break; // ж
639+
case 0x437: keysym = 'p'; break; // з
640+
case 0x438: keysym = 'b'; break; // и
641+
case 0x439: keysym = 'q'; break; // й
642+
case 0x43A: keysym = 'r'; break; // к
643+
case 0x43B: keysym = 'k'; break; // л
644+
case 0x43C: keysym = 'v'; break; // м
645+
case 0x43D: keysym = 'y'; break; // н
646+
case 0x43E: keysym = 'j'; break; // о
647+
case 0x43F: keysym = 'g'; break; // п
648+
case 0x440: keysym = 'h'; break; // р
649+
case 0x441: keysym = 'c'; break; // с
650+
case 0x442: keysym = 'n'; break; // т
651+
case 0x443: keysym = 'e'; break; // у
652+
case 0x444: keysym = 'a'; break; // ф
653+
case 0x445: keysym = '['; break; // х
654+
case 0x446: keysym = 'w'; break; // ц
655+
case 0x447: keysym = 'x'; break; // ч
656+
case 0x448: keysym = 'i'; break; // ш
657+
case 0x449: keysym = 'o'; break; // щ
658+
case 0x44A: keysym = ']'; break; // ъ
659+
case 0x44B: keysym = 's'; break; // ы
660+
case 0x44C: keysym = 'm'; break; // ь
661+
case 0x44D: keysym = '\''; break; // э
662+
case 0x44E: keysym = '.'; break; // ю
663+
case 0x44F: keysym = 'z'; break; // я
664+
665+
/* Uppercase Cyrillic */
666+
case 0x410: keysym = 'F'; break; // А
667+
case 0x411: keysym = '<'; break; // Б
668+
case 0x412: keysym = 'D'; break; // В
669+
case 0x413: keysym = 'U'; break; // Г
670+
case 0x414: keysym = 'L'; break; // Д
671+
case 0x415: keysym = 'T'; break; // Е
672+
case 0x416: keysym = ':'; break; // Ж
673+
case 0x417: keysym = 'P'; break; // З
674+
case 0x418: keysym = 'B'; break; // И
675+
case 0x419: keysym = 'Q'; break; // Й
676+
case 0x41A: keysym = 'R'; break; // К
677+
case 0x41B: keysym = 'K'; break; // Л
678+
case 0x41C: keysym = 'V'; break; // М
679+
case 0x41D: keysym = 'Y'; break; // Н
680+
case 0x41E: keysym = 'J'; break; // О
681+
case 0x41F: keysym = 'G'; break; // П
682+
case 0x420: keysym = 'H'; break; // Р
683+
case 0x421: keysym = 'C'; break; // С
684+
case 0x422: keysym = 'N'; break; // Т
685+
case 0x423: keysym = 'E'; break; // У
686+
case 0x424: keysym = 'A'; break; // Ф
687+
case 0x425: keysym = '{'; break; // Х
688+
case 0x426: keysym = 'W'; break; // Ц
689+
case 0x427: keysym = 'X'; break; // Ч
690+
case 0x428: keysym = 'I'; break; // Ш
691+
case 0x429: keysym = 'O'; break; // Щ
692+
case 0x42A: keysym = '}'; break; // Ъ
693+
case 0x42B: keysym = 'S'; break; // Ы
694+
case 0x42C: keysym = 'M'; break; // Ь
695+
case 0x42D: keysym = '"'; break; // Э
696+
case 0x42E: keysym = '>'; break; // Ю
697+
case 0x42F: keysym = 'Z'; break; // Я
698+
}
699+
}
700+
701+
if (ctrl_or_alt_pressed) {
702+
/* Map ASCII letters/digits to PC/AT set 1 scancodes (US layout).
703+
* This is enough for common shortcuts (A, C, V, X, Z, Y, ...). */
704+
int sc = 0;
705+
/* Normalize X11-style keysyms (0x1000000 + Unicode) to plain Unicode */
706+
if ((keysym & 0xFF000000) == 0x01000000)
707+
keysym &= 0x00FFFFFF;
708+
709+
switch (keysym) {
710+
/* digits 0-9 */
711+
case '1': sc = 0x02; break; case '2': sc = 0x03; break;
712+
case '3': sc = 0x04; break; case '4': sc = 0x05; break;
713+
case '5': sc = 0x06; break; case '6': sc = 0x07; break;
714+
case '7': sc = 0x08; break; case '8': sc = 0x09; break;
715+
case '9': sc = 0x0A; break; case '0': sc = 0x0B; break;
716+
717+
/* Latin letters A-Z (both cases) */
718+
case 'a': case 'A': sc = 0x1E; break;
719+
case 'b': case 'B': sc = 0x30; break;
720+
case 'c': case 'C': sc = 0x2E; break;
721+
case 'd': case 'D': sc = 0x20; break;
722+
case 'e': case 'E': sc = 0x12; break;
723+
case 'f': case 'F': sc = 0x21; break;
724+
case 'g': case 'G': sc = 0x22; break;
725+
case 'h': case 'H': sc = 0x23; break;
726+
case 'i': case 'I': sc = 0x17; break;
727+
case 'j': case 'J': sc = 0x24; break;
728+
case 'k': case 'K': sc = 0x25; break;
729+
case 'l': case 'L': sc = 0x26; break;
730+
case 'm': case 'M': sc = 0x32; break;
731+
case 'n': case 'N': sc = 0x31; break;
732+
case 'o': case 'O': sc = 0x18; break;
733+
case 'p': case 'P': sc = 0x19; break;
734+
case 'q': case 'Q': sc = 0x10; break;
735+
case 'r': case 'R': sc = 0x13; break;
736+
case 's': case 'S': sc = 0x1F; break;
737+
case 't': case 'T': sc = 0x14; break;
738+
case 'u': case 'U': sc = 0x16; break;
739+
case 'v': case 'V': sc = 0x2F; break;
740+
case 'w': case 'W': sc = 0x11; break;
741+
case 'x': case 'X': sc = 0x2D; break;
742+
case 'y': case 'Y': sc = 0x15; break;
743+
case 'z': case 'Z': sc = 0x2C; break;
744+
745+
/* Russian Cyrillic letters - most common shortcuts */
746+
/* Lowercase Cyrillic */
747+
case 0x0444: case 0x0424: sc = 0x1E; break; /* ф/Ф -> A */
748+
case 0x0438: case 0x0418: sc = 0x30; break; /* и/И -> B */
749+
case 0x0441: case 0x0421: sc = 0x2E; break; /* с/С -> C */
750+
case 0x0432: case 0x0412: sc = 0x20; break; /* в/В -> D */
751+
case 0x0443: case 0x0423: sc = 0x12; break; /* у/У -> E */
752+
case 0x0430: case 0x0410: sc = 0x21; break; /* а/А -> F */
753+
case 0x043f: case 0x041f: sc = 0x22; break; /* п/П -> G */
754+
case 0x0440: case 0x0420: sc = 0x23; break; /* р/Р -> H */
755+
case 0x0448: case 0x0428: sc = 0x17; break; /* ш/Ш -> I */
756+
case 0x043e: case 0x041e: sc = 0x24; break; /* о/О -> J */
757+
case 0x043b: case 0x041b: sc = 0x25; break; /* л/Л -> K */
758+
case 0x0434: case 0x0414: sc = 0x26; break; /* д/Д -> L */
759+
case 0x044c: case 0x042c: sc = 0x32; break; /* ь/Ь -> M */
760+
case 0x0442: case 0x0422: sc = 0x31; break; /* т/Т -> N */
761+
case 0x0449: case 0x0429: sc = 0x18; break; /* щ/Щ -> O */
762+
case 0x0437: case 0x0417: sc = 0x19; break; /* з/З -> P */
763+
case 0x0439: case 0x0419: sc = 0x10; break; /* й/Й -> Q */
764+
case 0x043a: case 0x041a: sc = 0x13; break; /* к/К -> R */
765+
case 0x044b: case 0x042b: sc = 0x1F; break; /* ы/Ы -> S */
766+
case 0x0435: case 0x0415: sc = 0x14; break; /* е/Е -> T */
767+
case 0x0433: case 0x0413: sc = 0x16; break; /* г/Г -> U */
768+
case 0x043c: case 0x041c: sc = 0x2F; break; /* м/М -> V */
769+
case 0x0446: case 0x0426: sc = 0x11; break; /* ц/Ц -> W */
770+
case 0x0447: case 0x0427: sc = 0x2D; break; /* ч/Ч -> X */
771+
case 0x043d: case 0x041d: sc = 0x15; break; /* н/Н -> Y */
772+
case 0x044f: case 0x042f: sc = 0x2C; break; /* я/Я -> Z */
773+
774+
775+
default: sc = 0; break;
776+
}
777+
if (sc) {
778+
guac_client_log(client, GUAC_LOG_DEBUG,
779+
"Sending keysym 0x%x as US scancode 0x%X due to modifiers",
780+
keysym, sc);
781+
/* send press+release; 'flags' = 0 for regular keys */
782+
guac_rdp_send_key_event(rdp_client, sc, 0, 1);
783+
guac_rdp_send_key_event(rdp_client, sc, 0, 0);
784+
return;
785+
}
786+
}
566787
/* Attempt to type using dead keys */
567788
if (!guac_rdp_decompose_keysym(keyboard, keysym))
568789
return;
@@ -738,3 +959,4 @@ BOOL guac_rdp_keyboard_set_indicators(rdpContext* context, UINT16 flags) {
738959
return TRUE;
739960

740961
}
962+

0 commit comments

Comments
 (0)