From f2f3cbceed5f36924af2d6ad89254ffc8d8b48c0 Mon Sep 17 00:00:00 2001 From: Twilight0 Date: Mon, 20 Jul 2026 17:39:39 +0300 Subject: [PATCH] X11: reduce typing delay for non-Latin characters (Greek, etc.) Previously, typing non-Latin characters (e.g. Greek) via the X11 backend was extremely slow (~1000ms per character) because the server had to dynamically remap an unused keycode to the desired keysym via XChangeKeyboardMapping. A blind 500ms sleep was used twice per character: once before sending the key event (to wait for the mapping to propagate), and once after (to wait for clients to process MappingNotify). The root cause is that X11 key events only carry keycodes, not keysyms. When a keysym has no existing keycode in the keyboard mapping, the app must temporarily remap a keycode. Clients cache the keyboard mapping locally and re-read it upon receiving a MappingNotify event from the server. The race condition is that a client may process the key event before it has processed the MappingNotify, interpreting the keycode with the stale (old) mapping. This patch replaces the blind pre-send sleep with a server-side verification loop: after XChangeKeyboardMapping, it polls XGetKeyboardMapping to confirm the server has applied the new mapping before sending the key event. This is safe because XGetKeyboardMapping reads directly from the server, which is the authority on the current mapping. The post-send delay is reduced from 500ms to 50ms, which is sufficient for clients to process MappingNotify on modern systems. Expected improvement: non-Latin characters go from ~1000ms/char to ~55-60ms/char (verification ~5ms + post-send 50ms). Latin characters remain instant as they already have existing keycode mappings. The 500ms constant was overly conservative. The verification loop provides a correct and responsive solution: it returns as soon as the server confirms the mapping, and times out after 200ms as a safety net. Fixes #46 --- inputcontrol/controller_x11.go | 38 ++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/inputcontrol/controller_x11.go b/inputcontrol/controller_x11.go index bb37d37..9665c12 100644 --- a/inputcontrol/controller_x11.go +++ b/inputcontrol/controller_x11.go @@ -42,7 +42,9 @@ import ( ) const ( - keyboardMappingDelay time.Duration = 500 * time.Millisecond + keyboardMappingDelay time.Duration = 50 * time.Millisecond + keyMappingTimeout time.Duration = 200 * time.Millisecond + keyMappingPoll time.Duration = 5 * time.Millisecond scrollDiv int = 20 ) @@ -150,6 +152,35 @@ func (p *x11Controller) changeKeyMappingLocked(keysymsPerKeycode C.int, C.XFlush(p.display) } +func (p *x11Controller) verifyKeyMappingLocked(keycode C.KeyCode, + keysymsPerKeycode C.int, keysym Keysym) { + deadline := time.Now().Add(keyMappingTimeout) + for { + var currentKeysymsPerKeycode C.int + currentKeysyms := C.XGetKeyboardMapping(p.display, keycode, 1, + ¤tKeysymsPerKeycode) + if currentKeysyms != nil { + found := false + for i := 0; i < int(currentKeysymsPerKeycode); i++ { + ks := *(*C.KeySym)(unsafe.Pointer(uintptr(unsafe.Pointer(currentKeysyms)) + + uintptr(i)*unsafe.Sizeof(*currentKeysyms))) + if ks == C.KeySym(keysym) { + found = true + break + } + } + C.XFree(unsafe.Pointer(currentKeysyms)) + if found { + return + } + } + if time.Now().After(deadline) { + return + } + time.Sleep(keyMappingPoll) + } +} + func (p *x11Controller) getModKeycodesLocked() map[uint]C.KeyCode { modKeymap := C.XGetModifierMapping(p.display) defer C.XFreeModifiermap(modKeymap) @@ -253,8 +284,7 @@ func (p *x11Controller) keyboardKeys(keys []Keysym) error { } keycode = emptyKeycode p.changeKeyMappingLocked(keysymsPerKeycode, keycode, keysym) - // race condition! - time.Sleep(keyboardMappingDelay) + p.verifyKeyMappingLocked(keycode, keysymsPerKeycode, keysym) } else { pressMods = mods & ^activeMods releaseMods = activeMods & ^mods @@ -267,7 +297,7 @@ func (p *x11Controller) keyboardKeys(keys []Keysym) error { p.sendModsLocked(modKeycodes, releaseMods, true) C.XFlush(p.display) if keycode == emptyKeycode { - // race condition! + // Wait for clients to process MappingNotify before next remap time.Sleep(keyboardMappingDelay) } }