Replies: 6 comments
-
|
a long time ago i played with xgb and wrote a wrapper for it to easily spawn windows or sent keystrokes or move mouse... i recall fighting the same problem when trying to type a character that was mapped to keyboard... my solution that time was to temporarily map the character to a unused key, press the key and then unmap (the code is in this function maybe it helps, but the code is old, undocumented and unmaintained)... it partially worked, but i think there were some issues and it didn't work reliably... i'm sorry, this is all i can help, i don't have time to work on this issue of yours... you can try consult the disscussion part of this repo... if you find a solution, feel free to tell us (in discussion or wiki) and/or make a PR if there is a nice fix... i'm closing this issue, if you don't agree, reopen and provide reasoning why it should be open... have fun and don't give up ;) |
Beta Was this translation helpful? Give feedback.
-
|
i've also tasked chatgpt to give you some hints... if you already consulted with ai and it didn't help, ignore this comment... here is the reply: One more hint: this is probably an XKB layout-group issue rather than something that can be solved reliably with only core The core X11 keyboard mapping is only a compatibility view. XKB has explicit layout groups and shift levels, and the active group is part of XKB state. Since xgb currently does not have working XKB support, decoding non-Latin layouts reliably with xgb alone is likely not practical. If your goal is to translate real
If your goal is to synthesize text input, then XTEST sends keycodes, not Unicode. The usual workaround is the one I used in my old
This is still a workaround and can be fragile, especially with toolkits, IMEs, cached keymaps, and non-BMP characters. For arbitrary Unicode text, clipboard paste is often more reliable than fake key events. |
Beta Was this translation helpful? Give feedback.
-
|
I tried direct x11 calls and it worked for me: I call Xutf8LookupString and get the right character. Although avoiding cgo this method still needs libX11.so.6 to work. The good news is that it's API/ABI is not changed for years, so it's usage will not break portability at least between Linux distros. Bad news: it can't be used on Macs or Windows. But those platforms have their own GUI libs, so this doesn't look like a real problem. Also this introduces x11 dependency if you plan to use your app on one pc and render it on another with, for example, x forwarding. But this way of remote apps usage is not so popular nowadays. Finally, I use xgb for drawing in pure go, and direct x11 calls for correct input. Demo app is below. There is also Anyway, the world is switching to Wayland which provides utf8 chars directly in text-input-unstable-v3 protocol. |
Beta Was this translation helpful? Give feedback.
-
|
The example above contains several shortcomings discovered after its initial implementation. Variadic functions like XCreateIC require all arguments to be strictly 8 bytes wide and must be terminated with an explicit uintptr(0) to prevent stack corruption on 64-bit systems in some environments (reported to reproduce under XWayland). Also, on ARM64 macOS, native variadic calls must pass arguments via the stack rather than registers, requiring custom assembly trampolines to handle the stack frame and calling convention correctly. For a fully functional implementation of this hybrid approach, please refer to vtui X11 backend source code (see |
Beta Was this translation helpful? Give feedback.
-
|
Finally, I found a pure Go solution. At least, it works for me :) You will need the patched xkb-go from here. package main
import (
"bytes"
"context"
"fmt"
"log"
"os"
"os/exec"
"strings"
"github.com/jezek/xgb"
"github.com/jezek/xgb/xproto"
xkb "github.com/unxed/xkb-go"
)
// XkbState holds the full response from XkbGetState
type XkbState struct {
BaseMods byte
LatchedMods byte
LockedMods byte
BaseGroup int16
LatchedGroup int16
LockedGroup byte
}
// rawRequest is a wrapper for sending arbitrary bytes via XGB
type rawRequest []byte
func (r rawRequest) Bytes() []byte { return r }
func main() {
// 1. Connect to X server
X, err := xgb.NewConn()
if err != nil {
log.Fatalf("Failed to connect to X server: %v", err)
}
defer X.Close()
// 2. Check and initialize XKEYBOARD extension
extCookie := xproto.QueryExtension(X, uint16(len("XKEYBOARD")), "XKEYBOARD")
extReply, err := extCookie.Reply()
if err != nil || !extReply.Present {
log.Fatal("XKEYBOARD extension is not supported by the server")
}
xkbOpcode := extReply.MajorOpcode
if err := initXkbExtension(X, xkbOpcode); err != nil {
log.Fatalf("Error initializing XKB: %v", err)
}
// 3. KEYMAP LOADING (Hybrid strategy for robustness)
fmt.Println("=== KEYMAP LOADING ===")
keymap, method, err := loadHybridKeymap(X)
if err != nil {
log.Fatalf("CRITICAL ERROR: Failed to load keymap using any method.\n%v", err)
}
fmt.Printf("[SUCCESS] Keymap loaded via: %s\n", method)
fmt.Printf("Detected layouts (groups): %d\n", keymap.NumGroups())
for i := 0; i < keymap.NumGroups(); i++ {
fmt.Printf(" [%d]: %s\n", i, keymap.GroupName(xkb.Group(i)))
}
// Create an xkb-go state object
state := keymap.NewState()
// 4. Create a test window
setup := xproto.Setup(X)
screen := setup.DefaultScreen(X)
win, _ := xproto.NewWindowId(X)
xproto.CreateWindow(X, screen.RootDepth, win, screen.Root,
0, 0, 450, 300, 0,
xproto.WindowClassInputOutput, screen.RootVisual,
xproto.CwBackPixel|xproto.CwEventMask, []uint32{
screen.WhitePixel,
xproto.EventMaskKeyPress | xproto.EventMaskKeyRelease, // Also listen for KeyRelease for modifier state
})
title := fmt.Sprintf("xkb-go Test (Mode: %s)", method)
xproto.ChangeProperty(X, xproto.PropModeReplace, win, xproto.AtomWmName, xproto.AtomString, 8, uint32(len(title)), []byte(title))
xproto.MapWindow(X, win)
fmt.Println("\n=== INPUT TEST ACTIVATED ===")
fmt.Println("Focus the window and press keys. Try AltGr + , (or other Level3 symbols).")
fmt.Println("Switch layouts to see xkb-go correctly track the active group.")
// 5. Main event loop
for {
ev, err := X.WaitForEvent()
if err != nil {
continue
}
// Handle both KeyPress and KeyRelease to update modifier state accurately
switch e := ev.(type) {
case xproto.KeyPressEvent:
handleKeyEvent(X, xkbOpcode, keymap, state, e.Detail, "Press")
case xproto.KeyReleaseEvent:
handleKeyEvent(X, xkbOpcode, keymap, state, e.Detail, "Release")
}
}
}
// loadHybridKeymap attempts to load the keymap via xkbcomp,
// falling back to RMLVO if the first method fails.
func loadHybridKeymap(X *xgb.Conn) (*xkb.Keymap, string, error) {
var errs []string
// Attempt 1: xkbcomp (Best method for XWayland, Linux, macOS)
fmt.Println("Attempt 1: Retrieving compiled keymap via xkbcomp...")
km, err := getKeymapViaXkbcomp()
if err == nil && km != nil && km.NumGroups() > 0 {
return km, "xkbcomp", nil
} else {
errs = append(errs, fmt.Sprintf("xkbcomp failed: %v", err))
fmt.Printf(" [!] xkbcomp failed: %v\n", err)
}
// Attempt 2: RMLVO (Fallback for Windows X servers or systems without xkbcomp)
fmt.Println("Attempt 2: Falling back to RMLVO compilation (_XKB_RULES_NAMES property)...")
km, err = getKeymapViaRMLVO(X)
if err == nil && km != nil && km.NumGroups() > 0 {
return km, "RMLVO (Pure Go compile)", nil
} else {
errs = append(errs, fmt.Sprintf("RMLVO fallback failed: %v", err))
fmt.Printf(" [!] RMLVO fallback failed: %v\n", err)
}
return nil, "", fmt.Errorf("all keymap loading methods failed:\n%s", strings.Join(errs, "\n"))
}
// getKeymapViaXkbcomp executes the `xkbcomp` utility and parses its output.
func getKeymapViaXkbcomp() (*xkb.Keymap, error) {
if _, err := exec.LookPath("xkbcomp"); err != nil {
return nil, fmt.Errorf("executable 'xkbcomp' not found in PATH")
}
display := os.Getenv("DISPLAY")
if display == "" {
display = ":0" // Default display
}
cmd := exec.Command("xkbcomp", display, "-")
var out bytes.Buffer
cmd.Stdout = &out
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("'xkbcomp' command failed: %v", err)
}
if out.Len() == 0 {
return nil, fmt.Errorf("'xkbcomp' returned empty output")
}
xkbCtx := xkb.NewContext(context.Background(), xkb.ContextNoFlags)
return xkbCtx.NewKeymapFromString(out.Bytes(), xkb.KeymapFormatTextV1)
}
// getKeymapViaRMLVO reads _XKB_RULES_NAMES property and compiles the keymap.
func getKeymapViaRMLVO(X *xgb.Conn) (*xkb.Keymap, error) {
setup := xproto.Setup(X)
root := setup.DefaultScreen(X).Root
rulesAtomCookie := xproto.InternAtom(X, true, uint16(len("_XKB_RULES_NAMES")), "_XKB_RULES_NAMES")
rulesAtomReply, err := rulesAtomCookie.Reply()
if err != nil {
return nil, fmt.Errorf("failed to get _XKB_RULES_NAMES atom: %v", err)
}
propCookie := xproto.GetProperty(X, false, root, rulesAtomReply.Atom, xproto.AtomAny, 0, 1024)
prop, err := propCookie.Reply()
if err != nil {
return nil, fmt.Errorf("failed to read _XKB_RULES_NAMES property: %v", err)
}
var rmlvo xkb.RuleNames
if prop != nil && len(prop.Value) > 0 {
parts := bytes.Split(prop.Value, []byte{0})
if len(parts) > 0 { rmlvo.Rules = string(parts[0]) }
if len(parts) > 1 { rmlvo.Model = string(parts[1]) }
if len(parts) > 2 { rmlvo.Layout = string(parts[2]) }
if len(parts) > 3 { rmlvo.Variant = string(parts[3]) }
if len(parts) > 4 { rmlvo.Options = string(parts[4]) }
}
fmt.Printf(" RMLVO properties -> Rules: %q, Model: %q, Layout: %q, Variant: %q, Options: %q\n",
rmlvo.Rules, rmlvo.Model, rmlvo.Layout, rmlvo.Variant, rmlvo.Options)
if rmlvo.Layout == "" {
return nil, fmt.Errorf("_XKB_RULES_NAMES property is empty or missing Layout")
}
xkbCtx := xkb.NewContext(context.Background(), xkb.ContextNoFlags)
return xkbCtx.NewKeymapFromNames(&rmlvo)
}
// handleKeyEvent processes a key event, synchronizes state, and prints diagnostics.
func handleKeyEvent(X *xgb.Conn, xkbOpcode byte, km *xkb.Keymap, s *xkb.State, detail xproto.Keycode, evType string) {
// 1. Query the X server for the true current keyboard state
srvState, err := queryXkbState(X, xkbOpcode)
if err != nil {
log.Printf("XkbGetState request error: %v", err)
return
}
// 2. Update xkb-go's internal state with the full server state
s.UpdateMask(
xkb.ModMask(srvState.BaseMods),
xkb.ModMask(srvState.LatchedMods),
xkb.ModMask(srvState.LockedMods),
xkb.Group(srvState.BaseGroup),
xkb.Group(srvState.LatchedGroup),
xkb.Group(srvState.LockedGroup),
)
kc := xkb.Keycode(detail)
sym := s.KeyGetOneSym(kc)
char := s.KeyGetUTF8(kc)
name := km.KeyGetName(kc)
fmt.Printf("\n=== Key %s Event ===\n", evType)
fmt.Printf("X11 Keycode: %d (%s)\n", detail, name)
fmt.Printf("Server BaseMods: 0x%02x\n", srvState.BaseMods)
fmt.Printf("Server LatchedMods: 0x%02x\n", srvState.LatchedMods)
fmt.Printf("Server LockedMods: 0x%02x\n", srvState.LockedMods)
fmt.Printf("Server BaseGroup: %d\n", srvState.BaseGroup)
fmt.Printf("Server LatchedGroup: %d\n", srvState.LatchedGroup)
fmt.Printf("Server LockedGroup: %d\n", srvState.LockedGroup)
fmt.Printf("Current Effective Group: %d (%s)\n", s.SerializeGroup(xkb.StateGroupEffective), km.GroupName(s.SerializeGroup(xkb.StateGroupEffective)))
fmt.Printf("Current Effective Mods: 0x%02x\n", s.SerializeMods(xkb.StateModEffective))
charDisp := char
if charDisp == "" { charDisp = "<none>" }
fmt.Printf("xkb-go Result: Keysym=%s | Char=%q\n", xkb.KeysymGetName(sym), charDisp)
// 3. Introspection: Show what this key produces in all available layouts
fmt.Println("Symbols for this key across all layouts:")
for i := 0; i < km.NumGroups(); i++ {
// Temporarily update state for introspection (only group changes)
// We use current BaseMods/LatchedMods from srvState and current LockedMods from s.LockedMods
// to simulate switching only the group while keeping other modifiers consistent.
s.UpdateMask(
xkb.ModMask(srvState.BaseMods),
xkb.ModMask(srvState.LatchedMods),
s.SerializeMods(xkb.StateModLocked), // Use current LockedMods from the state object
xkb.Group(srvState.BaseGroup),
xkb.Group(srvState.LatchedGroup),
xkb.Group(i), // Target group for introspection
)
gSym := s.KeyGetOneSym(kc)
gChar := s.KeyGetUTF8(kc)
gSymName := xkb.KeysymGetName(gSym)
gCharStr := gChar
if gCharStr == "" { gCharStr = "Ø" }
fmt.Printf(" [%d] %-15s : %-6q (Keysym: %s)\n", i, km.GroupName(xkb.Group(i)), gCharStr, gSymName)
}
// Restore original state after introspection
s.UpdateMask(
xkb.ModMask(srvState.BaseMods),
xkb.ModMask(srvState.LatchedMods),
xkb.ModMask(srvState.LockedMods),
xkb.Group(srvState.BaseGroup),
xkb.Group(srvState.LatchedGroup),
xkb.Group(srvState.LockedGroup),
)
fmt.Println("======================================================")
}
// initXkbExtension enables XKB support for the current XGB session.
func initXkbExtension(X *xgb.Conn, xkbOpcode byte) error {
buf := make([]byte, 8)
buf[0] = xkbOpcode
buf[1] = 0 // XkbUseExtension
xgb.Put16(buf[2:], 2) // Request length
xgb.Put16(buf[4:], 1) // wantedMajor
xgb.Put16(buf[6:], 0) // wantedMinor
cookie := X.NewCookie(true, true)
X.NewRequest(rawRequest(buf), cookie)
_, err := cookie.Reply()
return err
}
// queryXkbState sends a raw XkbGetState request (opcode 4) to the X server.
func queryXkbState(X *xgb.Conn, xkbOpcode byte) (*XkbState, error) {
buf := make([]byte, 8)
buf[0] = xkbOpcode
buf[1] = 4 // XkbGetState
xgb.Put16(buf[2:], 2) // Request length
xgb.Put16(buf[4:], 0x0100) // deviceSpec = XkbUseCoreKbd (Core keyboard device)
cookie := X.NewCookie(true, true)
X.NewRequest(rawRequest(buf), cookie)
reply, err := cookie.Reply()
if err != nil {
return nil, err
}
// Check for minimum reply length to avoid panics
if len(reply) < 18 { // Minimum length for the fields we care about (up to LatchedGroup at index 17)
return nil, fmt.Errorf("XKBGetState reply too short (%d bytes)", len(reply))
}
// Extract the relevant fields from the raw reply
return &XkbState{
BaseMods: reply[9], // Current physical modifiers pressed
LatchedMods: reply[10], // One-shot modifiers
LockedMods: reply[11], // Toggled modifiers (Caps Lock, Num Lock)
BaseGroup: int16(xgb.Get16(reply[14:])), // Base group index
LatchedGroup: int16(xgb.Get16(reply[16:])), // Latched group index
LockedGroup: reply[13], // Toggled group index
}, nil
} |
Beta Was this translation helpful? Give feedback.
-
|
For anyone facing the same issue: just use keytrans! I wrote it specifically for this purpose. It uses 4 different methods for handling X11 input and automatically selects the most reliable one for your environment. Two of these methods require FFI, and the other two are pure Go. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
...or if they are already supported, please point me to correct way of using them. All my tries to get typed unicode char if non-latin kb layout is selected failed.
Beta Was this translation helpful? Give feedback.
All reactions