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
This commit is contained in:
Twilight0 2026-07-20 17:39:39 +03:00
parent 996edb0501
commit f2f3cbceed

View file

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