app/internal/window: replace X11 input handling with xkb

Unifies Wayland and X11 keyboard handling.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2019-11-22 18:27:29 +01:00
parent 5a34b6f2c3
commit d5424ef7fc
3 changed files with 156 additions and 240 deletions
+16 -9
View File
@@ -628,18 +628,14 @@ func (w *window) resetFling() {
func gio_onKeyboardKeymap(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, format C.uint32_t, fd C.int32_t, size C.uint32_t) { func gio_onKeyboardKeymap(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, format C.uint32_t, fd C.int32_t, size C.uint32_t) {
defer syscall.Close(int(fd)) defer syscall.Close(int(fd))
conn.repeat.Stop(0) conn.repeat.Stop(0)
if conn.xkb != nil { conn.xkb.DestroyKeymapState()
conn.xkb.Destroy()
}
if format != C.WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1 { if format != C.WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1 {
return return
} }
xkb, err := xkb.New(int(format), int(fd), int(size)) if err := conn.xkb.LoadKeymap(int(format), int(fd), int(size)); err != nil {
if err != nil {
// TODO: Do better. // TODO: Do better.
panic(err) panic(err)
} }
conn.xkb = xkb
} }
//export gio_onKeyboardEnter //export gio_onKeyboardEnter
@@ -663,10 +659,10 @@ func gio_onKeyboardKey(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, seri
w := winMap[keyboard] w := winMap[keyboard]
w.resetFling() w.resetFling()
conn.repeat.Stop(t) conn.repeat.Stop(t)
if state != C.WL_KEYBOARD_KEY_STATE_PRESSED || conn.xkb == nil { if state != C.WL_KEYBOARD_KEY_STATE_PRESSED {
return return
} }
kc := uint32(keyCode) kc := mapXKBKeycode(uint32(keyCode))
for _, e := range conn.xkb.DispatchKey(kc) { for _, e := range conn.xkb.DispatchKey(kc) {
w.w.Event(e) w.w.Event(e)
} }
@@ -675,6 +671,11 @@ func gio_onKeyboardKey(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, seri
} }
} }
func mapXKBKeycode(keyCode uint32) uint32 {
// According to the xkb_v1 spec: "to determine the xkb keycode, clients must add 8 to the key event keycode."
return keyCode + 8
}
func (r *repeatState) Start(w *window, keyCode uint32, t time.Duration) { func (r *repeatState) Start(w *window, keyCode uint32, t time.Duration) {
if r.rate <= 0 { if r.rate <= 0 {
return return
@@ -867,7 +868,7 @@ func gio_onKeyboardModifiers(data unsafe.Pointer, keyboard *C.struct_wl_keyboard
if conn.xkb == nil { if conn.xkb == nil {
return return
} }
conn.xkb.UpdateMask(uint32(depressed), uint32(latched), uint32(locked), uint32(group)) conn.xkb.UpdateMask(uint32(depressed), uint32(latched), uint32(locked), uint32(group), uint32(group), uint32(group))
} }
//export gio_onKeyboardRepeatInfo //export gio_onKeyboardRepeatInfo
@@ -1082,6 +1083,12 @@ func detectUIScale() float32 {
func waylandConnect() error { func waylandConnect() error {
c := new(wlConn) c := new(wlConn)
conn = c conn = c
xkb, err := xkb.New()
if err != nil {
c.destroy()
return fmt.Errorf("wayland: %v", err)
}
c.xkb = xkb
c.disp = C.wl_display_connect(nil) c.disp = C.wl_display_connect(nil)
if c.disp == nil { if c.disp == nil {
c.destroy() c.destroy()
+79 -188
View File
@@ -5,38 +5,17 @@
package window package window
/* /*
#cgo LDFLAGS: -lX11 #cgo LDFLAGS: -lX11 -lxkbcommon -lxkbcommon-x11 -lX11-xcb
#include <stdlib.h> #include <stdlib.h>
#include <locale.h> #include <locale.h>
#include <X11/Xlib.h> #include <X11/Xlib.h>
#include <X11/Xatom.h> #include <X11/Xatom.h>
#include <X11/Xutil.h> #include <X11/Xutil.h>
#include <X11/Xresource.h> #include <X11/Xresource.h>
#include <X11/XKBlib.h>
#include <X11/Xlib-xcb.h>
#include <xkbcommon/xkbcommon-x11.h>
void gio_x11_init_ime(Display *dpy, Window win, XIM *xim, XIC *xic) {
// adjust locale temporarily for XOpenIM
char *lc = setlocale(LC_CTYPE, NULL);
setlocale(LC_CTYPE, "");
XSetLocaleModifiers("");
*xim = XOpenIM(dpy, 0, 0, 0);
if (!*xim) {
// fallback to internal input method
XSetLocaleModifiers("@im=none");
*xim = XOpenIM(dpy, 0, 0, 0);
}
// revert locale to prevent any unexpected side effects
setlocale(LC_CTYPE, lc);
*xic = XCreateIC(*xim,
XNInputStyle, XIMPreeditNothing | XIMStatusNothing,
XNClientWindow, win,
XNFocusWindow, win,
NULL);
XSetICFocus(*xic);
}
*/ */
import "C" import "C"
import ( import (
@@ -46,29 +25,29 @@ import (
"strconv" "strconv"
"sync" "sync"
"time" "time"
"unicode"
"unicode/utf8"
"unsafe" "unsafe"
"gioui.org/f32" "gioui.org/f32"
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/system" "gioui.org/io/system"
"gioui.org/app/internal/xkb"
syscall "golang.org/x/sys/unix" syscall "golang.org/x/sys/unix"
) )
type x11Window struct { type x11Window struct {
w Callbacks w Callbacks
x *C.Display x *C.Display
xw C.Window xkb *xkb.Context
xkbEventBase C.int
xw C.Window
evDelWindow C.Atom evDelWindow C.Atom
stage system.Stage stage system.Stage
cfg config cfg config
width int width int
height int height int
xim C.XIM
xic C.XIC
notify struct { notify struct {
read, write int read, write int
} }
@@ -192,8 +171,10 @@ func (w *x11Window) destroy() {
syscall.Close(w.notify.read) syscall.Close(w.notify.read)
w.notify.read = 0 w.notify.read = 0
} }
C.XDestroyIC(w.xic) if w.xkb != nil {
C.XCloseIM(w.xim) w.xkb.Destroy()
w.xkb = nil
}
C.XDestroyWindow(w.x, w.xw) C.XDestroyWindow(w.x, w.xw)
C.XCloseDisplay(w.x) C.XCloseDisplay(w.x)
} }
@@ -235,60 +216,22 @@ func (h *x11EventHandler) handleEvents() bool {
continue continue
} }
switch _type := (*C.XAnyEvent)(unsafe.Pointer(xev))._type; _type { switch _type := (*C.XAnyEvent)(unsafe.Pointer(xev))._type; _type {
case h.w.xkbEventBase:
xkbEvent := (*C.XkbAnyEvent)(unsafe.Pointer(xev))
switch xkbEvent.xkb_type {
case C.XkbNewKeyboardNotify, C.XkbMapNotify:
if err := h.w.updateXkbKeymap(); err != nil {
panic(err)
}
case C.XkbStateNotify:
state := (*C.XkbStateNotifyEvent)(unsafe.Pointer(xev))
h.w.xkb.UpdateMask(uint32(state.base_mods), uint32(state.latched_mods), uint32(state.locked_mods),
uint32(state.base_group), uint32(state.latched_group), uint32(state.locked_group))
}
case C.KeyPress: case C.KeyPress:
kevt := (*C.XKeyPressedEvent)(unsafe.Pointer(xev)) kevt := (*C.XKeyPressedEvent)(unsafe.Pointer(xev))
lookup: for _, e := range h.w.xkb.DispatchKey(uint32(kevt.keycode)) {
// Save state then clear CTRL & Shift bits in order to have w.w.Event(e)
// Xutf8LookupString return the unmodified key name in text[:l].
// This addresses an issue on some non US keyboard layouts where
// CTRL-[0..9] do not behave consistently.
//
// Note that this enables sending a key.Event for key combinations
// like CTRL-SHIFT-/ on QWERTY layouts, but CTRL-? is completely
// masked. The same applies to AZERTY layouts where CTRL-SHIFT-É is
// available but not CTRL-2.
state := kevt.state
mods := x11KeyStateToModifiers(state)
if mods.Contain(key.ModCtrl) {
kevt.state &^= (C.uint(C.ControlMask) | C.uint(C.ShiftMask))
}
l := int(C.Xutf8LookupString(w.xic, kevt,
(*C.char)(unsafe.Pointer(&h.text[0])), C.int(len(h.text)),
&h.keysym, &h.status))
switch h.status {
case C.XBufferOverflow:
h.text = make([]byte, l)
goto lookup
case C.XLookupChars:
// Synthetic event from XIM.
w.w.Event(key.EditEvent{Text: string(h.text[:l])})
case C.XLookupKeySym:
// Special keys.
if n, m, ok := x11ConvertKeysym(h.keysym, mods); ok {
w.w.Event(key.Event{Name: n, Modifiers: m})
}
case C.XLookupBoth:
if n, m, ok := x11ConvertKeysym(h.keysym, mods); ok {
w.w.Event(key.Event{Name: n, Modifiers: m})
}
// Do not send EditEvent for CTRL key combinations.
if mods.Contain(key.ModCtrl) {
break
}
// Report only printable runes.
str := h.text[:l]
for n := 0; n < len(str); {
r, s := utf8.DecodeRune(str)
if unicode.IsPrint(r) {
n += s
} else {
copy(str[n:], str[n+s:])
str = str[:len(str)-s]
}
}
if len(str) > 0 {
w.w.Event(key.EditEvent{Text: string(str)})
}
} }
case C.KeyRelease: case C.KeyRelease:
case C.ButtonPress, C.ButtonRelease: case C.ButtonPress, C.ButtonRelease:
@@ -369,98 +312,6 @@ func (h *x11EventHandler) handleEvents() bool {
return redraw return redraw
} }
func x11KeyStateToModifiers(s C.uint) key.Modifiers {
var m key.Modifiers
if s&C.Mod1Mask != 0 {
m |= key.ModAlt
}
if s&C.Mod4Mask != 0 {
m |= key.ModSuper
}
if s&C.ControlMask != 0 {
m |= key.ModCtrl
}
if s&C.ShiftMask != 0 {
m |= key.ModShift
}
return m
}
// x11ConvertKeysym returns the Gio special key that matches keysym s. For
// portability reasons, some keysyms might be translated and modifiers changed
// (like BackTab -> ModShift+Tab)
func x11ConvertKeysym(s C.KeySym, mods key.Modifiers) (string, key.Modifiers, bool) {
if '0' <= s && s <= '9' || 'A' <= s && s <= 'Z' {
return string(s), mods, true
}
if 'a' <= s && s <= 'z' {
return string(s - 0x20), mods, true
}
var n string
switch s {
case C.XK_Escape:
n = key.NameEscape
case C.XK_Left, C.XK_KP_Left:
n = key.NameLeftArrow
case C.XK_Right, C.XK_KP_Right:
n = key.NameRightArrow
case C.XK_Return:
n = key.NameReturn
case C.XK_KP_Enter:
n = key.NameEnter
case C.XK_Up, C.XK_KP_Up:
n = key.NameUpArrow
case C.XK_Down, C.XK_KP_Down:
n = key.NameDownArrow
case C.XK_Home, C.XK_KP_Home:
n = key.NameHome
case C.XK_End, C.XK_KP_End:
n = key.NameEnd
case C.XK_BackSpace:
n = key.NameDeleteBackward
case C.XK_Delete, C.XK_KP_Delete:
n = key.NameDeleteForward
case C.XK_Page_Up, C.XK_KP_Prior:
n = key.NamePageUp
case C.XK_Page_Down, C.XK_KP_Next:
n = key.NamePageDown
case C.XK_F1:
n = "F1"
case C.XK_F2:
n = "F2"
case C.XK_F3:
n = "F3"
case C.XK_F4:
n = "F4"
case C.XK_F5:
n = "F5"
case C.XK_F6:
n = "F6"
case C.XK_F7:
n = "F7"
case C.XK_F8:
n = "F8"
case C.XK_F9:
n = "F9"
case C.XK_F10:
n = "F10"
case C.XK_F11:
n = "F11"
case C.XK_F12:
n = "F12"
case C.XK_ISO_Left_Tab:
mods |= key.ModShift
fallthrough
case C.XK_Tab:
n = key.NameTab
case 0x20, C.XK_KP_Space:
n = "Space"
default:
return "", mods, false
}
return n, mods, true
}
var ( var (
x11Threads sync.Once x11Threads sync.Once
) )
@@ -490,6 +341,22 @@ func newX11Window(gioWin Callbacks, opts *Options) error {
if dpy == nil { if dpy == nil {
return errors.New("x11: cannot connect to the X server") return errors.New("x11: cannot connect to the X server")
} }
var major, minor C.int = C.XkbMajorVersion, C.XkbMinorVersion
var xkbEventBase C.int
if C.XkbQueryExtension(dpy, nil, &xkbEventBase, nil, &major, &minor) != C.True {
C.XCloseDisplay(dpy)
return errors.New("x11: XkbQueryExtension failed")
}
const bits = C.uint(C.XkbNewKeyboardNotifyMask | C.XkbMapNotifyMask | C.XkbStateNotifyMask)
if C.XkbSelectEvents(dpy, C.XkbUseCoreKbd, bits, bits) != C.True {
C.XCloseDisplay(dpy)
return errors.New("x11: XkbSelectEvents failed")
}
xkb, err := xkb.New()
if err != nil {
C.XCloseDisplay(dpy)
return fmt.Errorf("x11: %v", err)
}
ppsp := x11DetectUIScale(dpy) ppsp := x11DetectUIScale(dpy)
cfg := config{pxPerDp: ppsp, pxPerSp: ppsp} cfg := config{pxPerDp: ppsp, pxPerSp: ppsp}
@@ -506,23 +373,23 @@ func newX11Window(gioWin Callbacks, opts *Options) error {
0, 0, C.uint(cfg.Px(opts.Width)), C.uint(cfg.Px(opts.Height)), 0, 0, C.uint(cfg.Px(opts.Width)), C.uint(cfg.Px(opts.Height)),
0, C.CopyFromParent, C.InputOutput, nil, 0, C.CopyFromParent, C.InputOutput, nil,
C.CWEventMask|C.CWBackPixmap|C.CWOverrideRedirect, &swa) C.CWEventMask|C.CWBackPixmap|C.CWOverrideRedirect, &swa)
var (
xim C.XIM
xic C.XIC
)
C.gio_x11_init_ime(dpy, win, &xim, &xic)
w := &x11Window{ w := &x11Window{
w: gioWin, x: dpy, xw: win, w: gioWin, x: dpy, xw: win,
width: cfg.Px(opts.Width), width: cfg.Px(opts.Width),
height: cfg.Px(opts.Height), height: cfg.Px(opts.Height),
cfg: cfg, cfg: cfg,
xim: xim, xkb: xkb,
xic: xic, xkbEventBase: xkbEventBase,
} }
w.notify.read = pipe[0] w.notify.read = pipe[0]
w.notify.write = pipe[1] w.notify.write = pipe[1]
if err := w.updateXkbKeymap(); err != nil {
w.destroy()
return err
}
var hints C.XWMHints var hints C.XWMHints
hints.input = C.True hints.input = C.True
hints.flags = C.InputHint hints.flags = C.InputHint
@@ -591,3 +458,27 @@ func x11DetectUIScale(dpy *C.Display) float32 {
return scale return scale
} }
func (w *x11Window) updateXkbKeymap() error {
w.xkb.DestroyKeymapState()
ctx := (*C.struct_xkb_context)(unsafe.Pointer(w.xkb.Ctx))
xcb := C.XGetXCBConnection(w.x)
if xcb == nil {
return errors.New("x11: XGetXCBConnection failed")
}
xkbDevID := C.xkb_x11_get_core_keyboard_device_id(xcb)
if xkbDevID == -1 {
return errors.New("x11: xkb_x11_get_core_keyboard_device_id failed")
}
keymap := C.xkb_x11_keymap_new_from_device(ctx, xcb, xkbDevID, C.XKB_KEYMAP_COMPILE_NO_FLAGS)
if keymap == nil {
return errors.New("x11: xkb_x11_keymap_new_from_device failed")
}
state := C.xkb_x11_state_new_from_device(keymap, xcb, xkbDevID)
if state == nil {
C.xkb_keymap_unref(keymap)
return errors.New("x11: xkb_x11_keymap_new_from_device failed")
}
w.xkb.SetKeymap(unsafe.Pointer(keymap), unsafe.Pointer(state))
return nil
}
+61 -43
View File
@@ -9,8 +9,8 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "os"
"unicode"
"syscall" "syscall"
"unicode"
"unicode/utf8" "unicode/utf8"
"unsafe" "unsafe"
@@ -30,7 +30,7 @@ import (
import "C" import "C"
type Context struct { type Context struct {
ctx *C.struct_xkb_context Ctx *C.struct_xkb_context
keyMap *C.struct_xkb_keymap keyMap *C.struct_xkb_keymap
state *C.struct_xkb_state state *C.struct_xkb_state
compTable *C.struct_xkb_compose_table compTable *C.struct_xkb_compose_table
@@ -54,25 +54,18 @@ func (x *Context) Destroy() {
C.xkb_compose_table_unref(x.compTable) C.xkb_compose_table_unref(x.compTable)
x.compTable = nil x.compTable = nil
} }
if x.state != nil { x.DestroyKeymapState()
C.xkb_state_unref(x.state) if x.Ctx != nil {
x.state = nil C.xkb_context_unref(x.Ctx)
} x.Ctx = nil
if x.keyMap != nil {
C.xkb_keymap_unref(x.keyMap)
x.keyMap = nil
}
if x.ctx != nil {
C.xkb_context_unref(x.ctx)
x.ctx = nil
} }
} }
func New(format int, fd int, size int) (*Context, error) { func New() (*Context, error) {
ctx := &Context{ ctx := &Context{
ctx: C.xkb_context_new(C.XKB_CONTEXT_NO_FLAGS), Ctx: C.xkb_context_new(C.XKB_CONTEXT_NO_FLAGS),
} }
if ctx.ctx == nil { if ctx.Ctx == nil {
return nil, errors.New("newXKB: xkb_context_new failed") return nil, errors.New("newXKB: xkb_context_new failed")
} }
locale := os.Getenv("LC_ALL") locale := os.Getenv("LC_ALL")
@@ -87,7 +80,7 @@ func New(format int, fd int, size int) (*Context, error) {
} }
cloc := C.CString(locale) cloc := C.CString(locale)
defer C.free(unsafe.Pointer(cloc)) defer C.free(unsafe.Pointer(cloc))
ctx.compTable = C.xkb_compose_table_new_from_locale(ctx.ctx, cloc, C.XKB_COMPOSE_COMPILE_NO_FLAGS) ctx.compTable = C.xkb_compose_table_new_from_locale(ctx.Ctx, cloc, C.XKB_COMPOSE_COMPILE_NO_FLAGS)
if ctx.compTable == nil { if ctx.compTable == nil {
ctx.Destroy() ctx.Destroy()
return nil, errors.New("newXKB: xkb_compose_table_new_from_locale failed") return nil, errors.New("newXKB: xkb_compose_table_new_from_locale failed")
@@ -97,27 +90,54 @@ func New(format int, fd int, size int) (*Context, error) {
ctx.Destroy() ctx.Destroy()
return nil, errors.New("newXKB: xkb_compose_state_new failed") return nil, errors.New("newXKB: xkb_compose_state_new failed")
} }
mapData, err := syscall.Mmap(int(fd), 0, int(size), syscall.PROT_READ, syscall.MAP_SHARED)
if err != nil {
ctx.Destroy()
return nil, fmt.Errorf("newXKB: mmap of keymap failed: %v", err)
}
defer syscall.Munmap(mapData)
ctx.keyMap = C.xkb_keymap_new_from_buffer(ctx.ctx, (*C.char)(unsafe.Pointer(&mapData[0])), C.size_t(size-1), C.XKB_KEYMAP_FORMAT_TEXT_V1, C.XKB_KEYMAP_COMPILE_NO_FLAGS)
if ctx.keyMap == nil {
ctx.Destroy()
return nil, errors.New("newXKB: xkb_keymap_new_from_buffer failed")
}
ctx.state = C.xkb_state_new(ctx.keyMap)
if ctx.state == nil {
ctx.Destroy()
return nil, errors.New("newXKB: xkb_state_new failed")
}
return ctx, nil return ctx, nil
} }
func (x *Context) DestroyKeymapState() {
if x.state != nil {
C.xkb_state_unref(x.state)
x.state = nil
}
if x.keyMap != nil {
C.xkb_keymap_unref(x.keyMap)
x.keyMap = nil
}
}
// SetKeymap sets the keymap and state. The context takes ownership of the
// keymap and state and frees them in Destroy.
func (x *Context) SetKeymap(xkbKeyMap, xkbState unsafe.Pointer) {
x.DestroyKeymapState()
x.keyMap = (*C.struct_xkb_keymap)(xkbKeyMap)
x.state = (*C.struct_xkb_state)(xkbState)
}
func (x *Context) LoadKeymap(format int, fd int, size int) error {
x.DestroyKeymapState()
mapData, err := syscall.Mmap(int(fd), 0, int(size), syscall.PROT_READ, syscall.MAP_SHARED)
if err != nil {
return fmt.Errorf("newXKB: mmap of keymap failed: %v", err)
}
defer syscall.Munmap(mapData)
keyMap := C.xkb_keymap_new_from_buffer(x.Ctx, (*C.char)(unsafe.Pointer(&mapData[0])), C.size_t(size-1), C.XKB_KEYMAP_FORMAT_TEXT_V1, C.XKB_KEYMAP_COMPILE_NO_FLAGS)
if keyMap == nil {
return errors.New("newXKB: xkb_keymap_new_from_buffer failed")
}
state := C.xkb_state_new(keyMap)
if state == nil {
C.xkb_keymap_unref(keyMap)
return errors.New("newXKB: xkb_state_new failed")
}
x.keyMap = keyMap
x.state = state
return nil
}
func (x *Context) DispatchKey(keyCode uint32) (events []event.Event) { func (x *Context) DispatchKey(keyCode uint32) (events []event.Event) {
kc := mapXKBKeyCode(keyCode) if x.state == nil {
return
}
kc := C.xkb_keycode_t(keyCode)
if len(x.utf8Buf) == 0 { if len(x.utf8Buf) == 0 {
x.utf8Buf = make([]byte, 1) x.utf8Buf = make([]byte, 1)
} }
@@ -186,18 +206,16 @@ func (x *Context) charsForKeycode(keyCode C.xkb_keycode_t) []byte {
} }
func (x *Context) IsRepeatKey(keyCode uint32) bool { func (x *Context) IsRepeatKey(keyCode uint32) bool {
kc := mapXKBKeyCode(keyCode) kc := C.xkb_keycode_t(keyCode)
return C.xkb_keymap_key_repeats(x.keyMap, kc) == 1 return C.xkb_keymap_key_repeats(x.keyMap, kc) == 1
} }
func (x *Context) UpdateMask(depressed, latched, locked, group uint32) { func (x *Context) UpdateMask(depressed, latched, locked, depressedGroup, latchedGroup, lockedGroup uint32) {
xkbGrp := C.xkb_layout_index_t(group) if x.state == nil {
C.xkb_state_update_mask(x.state, C.xkb_mod_mask_t(depressed), C.xkb_mod_mask_t(latched), C.xkb_mod_mask_t(locked), xkbGrp, xkbGrp, xkbGrp) return
} }
C.xkb_state_update_mask(x.state, C.xkb_mod_mask_t(depressed), C.xkb_mod_mask_t(latched), C.xkb_mod_mask_t(locked),
func mapXKBKeyCode(keyCode uint32) C.xkb_keycode_t { C.xkb_layout_index_t(depressedGroup), C.xkb_layout_index_t(latchedGroup), C.xkb_layout_index_t(lockedGroup))
// According to the xkb_v1 spec: "to determine the xkb keycode, clients must add 8 to the key event keycode."
return C.xkb_keycode_t(keyCode + 8)
} }
func convertKeysym(s C.xkb_keysym_t) (string, bool) { func convertKeysym(s C.xkb_keysym_t) (string, bool) {