#2900·glfw

[Bug][X11] CharMods callback is not executed when ctrl key is pressed while typing [+ proposed solution]

Author: tomoltCreated Sep 15, 2026Updated Sep 15, 2026

On X11, the CharMods callback is not executed when the user is holding the ctrl key pressed while he types.

I wrote a small example program to demonstrate the problem: showkeys.c

Key(key=65, scancode=38, mods=0)
CharMods(codepoint='a' (97), mods=0)
Key(key=340, scancode=50, mods=0)
Key(key=65, scancode=38, mods=1)
CharMods(codepoint='A' (65), mods=1)
Key(key=341, scancode=37, mods=0)
Key(key=65, scancode=38, mods=2)
Key(key=342, scancode=64, mods=0)
Key(key=65, scancode=38, mods=4)
CharMods(codepoint='a' (97), mods=4)
Key(key=343, scancode=133, mods=0)
Key(key=65, scancode=38, mods=8)
CharMods(codepoint='a' (97), mods=8)

This is the output I receive when I press A, Shift-A, Ctrl-A, Alt-A, Super-A, in that order. Note how the CharMods invocation for mods=2 is missing.

I have already found the culprit behind this issue. In function processEvent in x11_window.c, GLFW uses XLookupString() and its variant Xutf8LookupString() to turn key presses into unicode codepoints. These functions have an unexpected quirk with respect to the control key modifier. From the X11 specification:

The XLookupString function translates a key event to a KeySym and a string. The KeySym is obtained by using the standard interpretation of the Shift, Lock, group, and numlock modifiers as defined in the X Protocol specification. If the KeySym has been rebound (see XRebindKeysym), the bound string will be stored in the buffer. Otherwise, the KeySym is mapped, if possible, to an ISO Latin-1 character or (if the Control modifier is on) to an ASCII control character, and that character is stored in the buffer.

GLFW expects to always receive back a valid Unicode codepoint. As a result, when the output of LookupString is passed into _glfwCharInput(), the function exits early because the 'codepoint' lies within the ASCII control character range.

I believe that the best solution to this problem is to hand a modified copy of the KeyPressEvent to LookupString, in which the control key has been masked out.

Cheers, Thomas