// SPDX-License-Identifier: Unlicense OR MIT //go:build (linux && !android) || freebsd || openbsd // +build linux,!android freebsd openbsd // Package xkb implements a Go interface for the X Keyboard Extension library. package xkb import ( "errors" "fmt" "os" "syscall" "unicode" "unicode/utf8" "unsafe" "gioui.org/io/event" "gioui.org/io/key" ) /* #cgo linux pkg-config: xkbcommon #cgo freebsd openbsd CFLAGS: -I/usr/local/include #cgo freebsd openbsd LDFLAGS: -L/usr/local/lib -lxkbcommon #include #include #include */ import "C" type Context struct { Ctx *C.struct_xkb_context keyMap *C.struct_xkb_keymap state *C.struct_xkb_state compTable *C.struct_xkb_compose_table compState *C.struct_xkb_compose_state utf8Buf []byte } var ( _XKB_MOD_NAME_CTRL = []byte("Control\x00") _XKB_MOD_NAME_SHIFT = []byte("Shift\x00") _XKB_MOD_NAME_ALT = []byte("Mod1\x00") _XKB_MOD_NAME_LOGO = []byte("Mod4\x00") ) func (x *Context) Destroy() { if x.compState != nil { C.xkb_compose_state_unref(x.compState) x.compState = nil } if x.compTable != nil { C.xkb_compose_table_unref(x.compTable) x.compTable = nil } x.DestroyKeymapState() if x.Ctx != nil { C.xkb_context_unref(x.Ctx) x.Ctx = nil } } func New() (*Context, error) { ctx := &Context{ Ctx: C.xkb_context_new(C.XKB_CONTEXT_NO_FLAGS), } if ctx.Ctx == nil { return nil, errors.New("newXKB: xkb_context_new failed") } locale := os.Getenv("LC_ALL") if locale == "" { locale = os.Getenv("LC_CTYPE") } if locale == "" { locale = os.Getenv("LANG") } if locale == "" { locale = "C" } cloc := C.CString(locale) defer C.free(unsafe.Pointer(cloc)) ctx.compTable = C.xkb_compose_table_new_from_locale(ctx.Ctx, cloc, C.XKB_COMPOSE_COMPILE_NO_FLAGS) if ctx.compTable == nil { ctx.Destroy() return nil, errors.New("newXKB: xkb_compose_table_new_from_locale failed") } ctx.compState = C.xkb_compose_state_new(ctx.compTable, C.XKB_COMPOSE_STATE_NO_FLAGS) if ctx.compState == nil { ctx.Destroy() return nil, errors.New("newXKB: xkb_compose_state_new failed") } 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) Modifiers() key.Modifiers { var mods key.Modifiers if x.state == nil { return mods } if C.xkb_state_mod_name_is_active(x.state, (*C.char)(unsafe.Pointer(&_XKB_MOD_NAME_CTRL[0])), C.XKB_STATE_MODS_EFFECTIVE) == 1 { mods |= key.ModCtrl } if C.xkb_state_mod_name_is_active(x.state, (*C.char)(unsafe.Pointer(&_XKB_MOD_NAME_SHIFT[0])), C.XKB_STATE_MODS_EFFECTIVE) == 1 { mods |= key.ModShift } if C.xkb_state_mod_name_is_active(x.state, (*C.char)(unsafe.Pointer(&_XKB_MOD_NAME_ALT[0])), C.XKB_STATE_MODS_EFFECTIVE) == 1 { mods |= key.ModAlt } if C.xkb_state_mod_name_is_active(x.state, (*C.char)(unsafe.Pointer(&_XKB_MOD_NAME_LOGO[0])), C.XKB_STATE_MODS_EFFECTIVE) == 1 { mods |= key.ModSuper } return mods } func (x *Context) DispatchKey(keyCode uint32, state key.State) (events []event.Event) { if x.state == nil { return } kc := C.xkb_keycode_t(keyCode) if len(x.utf8Buf) == 0 { x.utf8Buf = make([]byte, 1) } sym := C.xkb_state_key_get_one_sym(x.state, kc) if name, ok := convertKeysym(sym); ok { cmd := key.Event{ Name: name, Modifiers: x.Modifiers(), State: state, } // Ensure that a physical backtab key is translated to // Shift-Tab. if sym == C.XKB_KEY_ISO_Left_Tab { cmd.Modifiers |= key.ModShift } events = append(events, cmd) } C.xkb_compose_state_feed(x.compState, sym) var str []byte switch C.xkb_compose_state_get_status(x.compState) { case C.XKB_COMPOSE_CANCELLED, C.XKB_COMPOSE_COMPOSING: return case C.XKB_COMPOSE_COMPOSED: size := C.xkb_compose_state_get_utf8(x.compState, (*C.char)(unsafe.Pointer(&x.utf8Buf[0])), C.size_t(len(x.utf8Buf))) if int(size) >= len(x.utf8Buf) { x.utf8Buf = make([]byte, size+1) size = C.xkb_compose_state_get_utf8(x.compState, (*C.char)(unsafe.Pointer(&x.utf8Buf[0])), C.size_t(len(x.utf8Buf))) } C.xkb_compose_state_reset(x.compState) str = x.utf8Buf[:size] case C.XKB_COMPOSE_NOTHING: mod := x.Modifiers() if mod&(key.ModCtrl|key.ModAlt|key.ModSuper) == 0 { str = x.charsForKeycode(kc) } } // Report only printable runes. var n int for 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 state == key.Press && len(str) > 0 { events = append(events, key.EditEvent{Text: string(str)}) } return } func (x *Context) charsForKeycode(keyCode C.xkb_keycode_t) []byte { size := C.xkb_state_key_get_utf8(x.state, keyCode, (*C.char)(unsafe.Pointer(&x.utf8Buf[0])), C.size_t(len(x.utf8Buf))) if int(size) >= len(x.utf8Buf) { x.utf8Buf = make([]byte, size+1) size = C.xkb_state_key_get_utf8(x.state, keyCode, (*C.char)(unsafe.Pointer(&x.utf8Buf[0])), C.size_t(len(x.utf8Buf))) } return x.utf8Buf[:size] } func (x *Context) IsRepeatKey(keyCode uint32) bool { if x.state == nil { return false } kc := C.xkb_keycode_t(keyCode) return C.xkb_keymap_key_repeats(x.keyMap, kc) == 1 } func (x *Context) UpdateMask(depressed, latched, locked, depressedGroup, latchedGroup, lockedGroup uint32) { if x.state == nil { 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), C.xkb_layout_index_t(depressedGroup), C.xkb_layout_index_t(latchedGroup), C.xkb_layout_index_t(lockedGroup)) } func convertKeysym(s C.xkb_keysym_t) (key.Name, bool) { if 'a' <= s && s <= 'z' { return key.Name(rune(s - 'a' + 'A')), true } if C.XKB_KEY_KP_0 <= s && s <= C.XKB_KEY_KP_9 { return key.Name(rune(s - C.XKB_KEY_KP_0 + '0')), true } if ' ' < s && s <= '~' { return key.Name(rune(s)), true } var n key.Name switch s { case C.XKB_KEY_Escape: n = key.NameEscape case C.XKB_KEY_Left: n = key.NameLeftArrow case C.XKB_KEY_Right: n = key.NameRightArrow case C.XKB_KEY_Return: n = key.NameReturn case C.XKB_KEY_Up: n = key.NameUpArrow case C.XKB_KEY_Down: n = key.NameDownArrow case C.XKB_KEY_Home: n = key.NameHome case C.XKB_KEY_End: n = key.NameEnd case C.XKB_KEY_BackSpace: n = key.NameDeleteBackward case C.XKB_KEY_Delete: n = key.NameDeleteForward case C.XKB_KEY_Page_Up: n = key.NamePageUp case C.XKB_KEY_Page_Down: n = key.NamePageDown case C.XKB_KEY_F1: n = key.NameF1 case C.XKB_KEY_F2: n = key.NameF2 case C.XKB_KEY_F3: n = key.NameF3 case C.XKB_KEY_F4: n = key.NameF4 case C.XKB_KEY_F5: n = key.NameF5 case C.XKB_KEY_F6: n = key.NameF6 case C.XKB_KEY_F7: n = key.NameF7 case C.XKB_KEY_F8: n = key.NameF8 case C.XKB_KEY_F9: n = key.NameF9 case C.XKB_KEY_F10: n = key.NameF10 case C.XKB_KEY_F11: n = key.NameF11 case C.XKB_KEY_F12: n = key.NameF12 case C.XKB_KEY_Tab, C.XKB_KEY_ISO_Left_Tab: n = key.NameTab case 0x20: n = key.NameSpace case C.XKB_KEY_Control_L, C.XKB_KEY_Control_R: n = key.NameCtrl case C.XKB_KEY_Shift_L, C.XKB_KEY_Shift_R: n = key.NameShift case C.XKB_KEY_Alt_L, C.XKB_KEY_Alt_R: n = key.NameAlt case C.XKB_KEY_Super_L, C.XKB_KEY_Super_R: n = key.NameSuper case C.XKB_KEY_KP_Space: n = key.NameSpace case C.XKB_KEY_KP_Tab: n = key.NameTab case C.XKB_KEY_KP_Enter: n = key.NameEnter case C.XKB_KEY_KP_F1: n = key.NameF1 case C.XKB_KEY_KP_F2: n = key.NameF2 case C.XKB_KEY_KP_F3: n = key.NameF3 case C.XKB_KEY_KP_F4: n = key.NameF4 case C.XKB_KEY_KP_Home: n = key.NameHome case C.XKB_KEY_KP_Left: n = key.NameLeftArrow case C.XKB_KEY_KP_Up: n = key.NameUpArrow case C.XKB_KEY_KP_Right: n = key.NameRightArrow case C.XKB_KEY_KP_Down: n = key.NameDownArrow case C.XKB_KEY_KP_Prior: // not supported return "", false case C.XKB_KEY_KP_Next: // not supported return "", false case C.XKB_KEY_KP_End: n = key.NameEnd case C.XKB_KEY_KP_Begin: n = key.NameHome case C.XKB_KEY_KP_Insert: // not supported return "", false case C.XKB_KEY_KP_Delete: n = key.NameDeleteForward case C.XKB_KEY_KP_Multiply: n = "*" case C.XKB_KEY_KP_Add: n = "+" case C.XKB_KEY_KP_Separator: // not supported return "", false case C.XKB_KEY_KP_Subtract: n = "-" case C.XKB_KEY_KP_Decimal: // TODO(dh): does a German keyboard layout also translate the numpad key to XKB_KEY_KP_DECIMAL? Because in // German, the decimal is a comma, not a period. n = "." case C.XKB_KEY_KP_Divide: n = "/" case C.XKB_KEY_KP_Equal: n = "=" default: return "", false } return n, true }