// Package xgbkb provides keyboard input utilities to be used alongside xgb and // xgbutil. package xgbkb import "unicode" import "github.com/jezek/xgbutil" import "github.com/jezek/xgb/xproto" import "github.com/jezek/xgbutil/keybind" // TODO: support dead keys/compose key // when making changes to this file, look at keysymdef.h and // https://tronche.com/gui/x/xlib/input/keyboard-encoding.html var x *xgbutil.XUtil var modifierMasks struct { capsLock uint16 shiftLock uint16 numLock uint16 modeSwitch uint16 alt uint16 meta uint16 super uint16 hyper uint16 } // IsOnNumpad returns whether the given keysym resides on the numpad. func IsOnNumpad (symbol xproto.Keysym) bool { return symbol >= 0xFF80 && symbol <= 0xFFB9 } // Initialize grabs keyboard mapping information from the X server. This // function must be called before calling any other functions in this package. // keybind.Initialize must also be called before this function is called. func Initialize (connection *xgbutil.XUtil) { x = connection modifierMasks.capsLock = KeysymToMask(0xFFE5) modifierMasks.shiftLock = KeysymToMask(0xFFE6) modifierMasks.numLock = KeysymToMask(0xFF7F) modifierMasks.modeSwitch = KeysymToMask(0xFF7E) modifierMasks.hyper = KeysymToMask(0xffed) modifierMasks.super = KeysymToMask(0xffeb) modifierMasks.meta = KeysymToMask(0xffe7) modifierMasks.alt = KeysymToMask(0xffe9) } // Modifiers lists which modifier keys are toggled on or being pressed. type Modifiers struct { CapsLock bool ShiftLock bool NumLock bool ModeSwitch bool Alt bool Meta bool Super bool Hyper bool } // String returns a human-readable comma-separated list of all active modifiers. func (modifiers Modifiers) String () (out string) { add := func (name string) { if out != "" { out += ", " } out += name } if modifiers.Hyper { add("Hyper") } if modifiers.Super { add("Super") } if modifiers.Meta { add("Meta") } if modifiers.Alt { add("Alt") } if modifiers.ModeSwitch { add("ModeSwitch") } if modifiers.NumLock { add("NumLock") } if modifiers.ShiftLock { add("ShiftLock") } if modifiers.CapsLock { add("CapsLock") } return } // StateToModifiers converts a modifier state given by a keyboard or mouse event // to a Modifiers struct. func StateToModifiers (state uint16) Modifiers { return Modifiers { CapsLock: 0 < state & modifierMasks.capsLock, ShiftLock: 0 < state & modifierMasks.shiftLock, NumLock: 0 < state & modifierMasks.numLock, ModeSwitch: 0 < state & modifierMasks.modeSwitch, Alt: 0 < state & modifierMasks.alt, Meta: 0 < state & modifierMasks.meta, Super: 0 < state & modifierMasks.super, Hyper: 0 < state & modifierMasks.hyper, } } // KeysymToKeycode converts an X keysym to an X keycode, instead of the other // way around. func KeysymToKeycode ( symbol xproto.Keysym, ) ( code xproto.Keycode, ) { mapping := keybind.KeyMapGet(x) for index, testSymbol := range mapping.Keysyms { if testSymbol == symbol { code = xproto.Keycode ( index / int(mapping.KeysymsPerKeycode) + int(x.Setup().MinKeycode)) break } } return } // KeysymToMask returns the X modmask for a given modifier key. func KeysymToMask (symbol xproto.Keysym) uint16 { return keybind.ModGet(x, KeysymToKeycode(symbol)) } // KeycodeToButton converts an X keycode to a tomo keycode. It implements a more // fleshed out version of some of the logic found in xgbutil/keybind/encoding.go // to get a full keycode to keysym conversion. To avoid redundant work, this // function will also return the rune corresponding to the keycode. func KeycodeToKeysym (keycode xproto.Keycode, state uint16) (xproto.Keysym, rune) { // PARAGRAPH 3 // // A list of KeySyms is associated with each KeyCode. The list is // intended to convey the set of symbols on the corresponding key. If // the list (ignoring trailing NoSymbol entries) is a single KeySym // ``K'', then the list is treated as if it were the list ``K NoSymbol // K NoSymbol''. If the list (ignoring trailing NoSymbol entries) is a // pair of KeySyms ``K1 K2'', then the list is treated as if it were the // list ``K1 K2 K1 K2''. If the list (ignoring trailing NoSymbol // entries) is a triple of KeySyms ``K1 K2 K3'', then the list is // treated as if it were the list ``K1 K2 K3 NoSymbol''. When an // explicit ``void'' element is desired in the list, the value // VoidSymbol can be used. symbol1 := keybind.KeysymGet(x, keycode, 0) symbol2 := keybind.KeysymGet(x, keycode, 1) symbol3 := keybind.KeysymGet(x, keycode, 2) symbol4 := keybind.KeysymGet(x, keycode, 3) switch { case symbol2 == 0 && symbol3 == 0 && symbol4 == 0: symbol3 = symbol1 case symbol3 == 0 && symbol4 == 0: symbol3 = symbol1 symbol4 = symbol2 case symbol4 == 0: symbol4 = 0 } symbol1Rune := KeysymToRune(symbol1) symbol2Rune := KeysymToRune(symbol2) symbol3Rune := KeysymToRune(symbol3) symbol4Rune := KeysymToRune(symbol4) // PARAGRAPH 4 // // The first four elements of the list are split into two groups of // KeySyms. Group 1 contains the first and second KeySyms; Group 2 // contains the third and fourth KeySyms. Within each group, if the // second element of the group is NoSymbol , then the group should be // treated as if the second element were the same as the first element, // except when the first element is an alphabetic KeySym ``K'' for which // both lowercase and uppercase forms are defined. In that case, the // group should be treated as if the first element were the lowercase // form of ``K'' and the second element were the uppercase form of // ``K.'' cased := false if symbol2 == 0 { upper := unicode.IsUpper(symbol1Rune) lower := unicode.IsLower(symbol1Rune) if upper || lower { symbol1Rune = unicode.ToLower(symbol1Rune) symbol2Rune = unicode.ToUpper(symbol1Rune) cased = true } else { symbol2 = symbol1 symbol2Rune = symbol1Rune } } if symbol4 == 0 { upper := unicode.IsUpper(symbol3Rune) lower := unicode.IsLower(symbol3Rune) if upper || lower { symbol3Rune = unicode.ToLower(symbol3Rune) symbol4Rune = unicode.ToUpper(symbol3Rune) cased = true } else { symbol4 = symbol3 symbol4Rune = symbol3Rune } } // PARAGRAPH 5 // // The standard rules for obtaining a KeySym from a KeyPress event make // use of only the Group 1 and Group 2 KeySyms; no interpretation of/ // other KeySyms in the list is given. Which group to use is determined // by the modifier state. Switching between groups is controlled by the // KeySym named MODE SWITCH, by attaching that KeySym to some KeyCode // and attaching that KeyCode to any one of the modifiers Mod1 through // Mod5. This modifier is called the group modifier. For any KeyCode, // Group 1 is used when the group modifier is off, and Group 2 is used // when the group modifier is on. modeSwitch := state & modifierMasks.modeSwitch > 0 if modeSwitch { symbol1 = symbol3 symbol1Rune = symbol3Rune symbol2 = symbol4 symbol2Rune = symbol4Rune } // PARAGRAPH 6 // // The Lock modifier is interpreted as CapsLock when the KeySym named // XK_Caps_Lock is attached to some KeyCode and that KeyCode is attached // to the Lock modifier. The Lock modifier is interpreted as ShiftLock // when the KeySym named XK_Shift_Lock is attached to some KeyCode and // that KeyCode is attached to the Lock modifier. If the Lock modifier // could be interpreted as both CapsLock and ShiftLock, the CapsLock // interpretation is used. shift := state & xproto.ModMaskShift > 0 || state & modifierMasks.shiftLock > 0 capsLock := state & modifierMasks.capsLock > 0 // PARAGRAPH 7 // // The operation of keypad keys is controlled by the KeySym named // XK_Num_Lock, by attaching that KeySym to some KeyCode and attaching // that KeyCode to any one of the modifiers Mod1 through Mod5 . This // modifier is called the numlock modifier. The standard KeySyms with // the prefix ``XK_KP_'' in their name are called keypad KeySyms; these // are KeySyms with numeric value in the hexadecimal range 0xFF80 to // 0xFFBD inclusive. In addition, vendor-specific KeySyms in the // hexadecimal range 0x11000000 to 0x1100FFFF are also keypad KeySyms. numLock := state & modifierMasks.numLock > 0 // PARAGRAPH 8 // // Within a group, the choice of KeySym is determined by applying the // first rule that is satisfied from the following list: var selectedKeysym xproto.Keysym var selectedRune rune symbol2IsNumPad := IsOnNumpad(symbol2) switch { case numLock && symbol2IsNumPad: // The numlock modifier is on and the second KeySym is a keypad // KeySym. In this case, if the Shift modifier is on, or if the // Lock modifier is on and is interpreted as ShiftLock, then the // first KeySym is used, otherwise the second KeySym is used. if shift { selectedKeysym = symbol1 selectedRune = symbol1Rune } else { selectedKeysym = symbol2 selectedRune = symbol2Rune } case !shift && !capsLock: // The Shift and Lock modifiers are both off. In this case, the // first KeySym is used. selectedKeysym = symbol1 selectedRune = symbol1Rune case !shift && capsLock: // The Shift modifier is off, and the Lock modifier is on and is // interpreted as CapsLock. In this case, the first KeySym is // used, but if that KeySym is lowercase alphabetic, then the // corresponding uppercase KeySym is used instead. if cased && unicode.IsLower(symbol1Rune) { selectedRune = symbol2Rune } else { selectedKeysym = symbol1 selectedRune = symbol1Rune } case shift && capsLock: // The Shift modifier is on, and the Lock modifier is on and is // interpreted as CapsLock. In this case, the second KeySym is // used, but if that KeySym is lowercase alphabetic, then the // corresponding uppercase KeySym is used instead. if cased && unicode.IsLower(symbol2Rune) { selectedRune = unicode.ToUpper(symbol2Rune) } else { selectedKeysym = symbol2 selectedRune = symbol2Rune } case shift: // The Shift modifier is on, or the Lock modifier is on and is // interpreted as ShiftLock, or both. In this case, the second // KeySym is used. selectedKeysym = symbol2 selectedRune = symbol2Rune } return selectedKeysym, selectedRune } // runeExceptions contains runes that violate the heuristics in KeysymToRune. var runeExceptions = map[xproto.Keysym] rune { // TODO: flesh out this list // number pad 0xFF9F: '\x7F', // delete 0xFF80: ' ', 0xFF89: '\t', 0xFF8D: '\r', // enter 0xFFBD: '=', 0xFFAA: '*', 0xFFAB: '+', 0xFFAC: ',', // FIXME: not always comma, could be dot 0xFFAD: '-', 0xFFAE: '.', // FIXME: not always dot, could be comma 0xFFAF: '/', // misc. control 0xFFFF: '\x7F', // delete 0xFF09: '\t', 0xFE20: '\t', 0xFE34: '\r', // enter } // KeysymToRune takes in an X keysym and outputs a utf32 code point. If a // corresponding code point was not found, zero will be returned. func KeysymToRune (keysym xproto.Keysym) rune { // there are a few keysyms that can be shoehorned into runes that don't // fit the below rules or warrant their own if char, ok := runeExceptions[keysym]; ok { return char } // TTY function keys if keysym >= 0xFF08 && keysym <= 0xFF1B { return rune(keysym & 0xF) } // number pad numbers if keysym >= 0xFFB0 && keysym <= 0xFFB9 { return '0' + rune(keysym & 0xF) } // X keysyms like 0xFF.. or 0xFE.. are non-character keys. these cannot // be converted so we return a zero. if (keysym >> 8) == 0xFF || (keysym >> 8) == 0xFE { return 0 } // some X keysyms have a single bit set to 1 here. i believe this is to // prevent conflicts with existing codes. if we mask it off we will get // a correct utf-32 code point. if keysym & 0xF000000 == 0x1000000 { return rune(keysym & 0x0111111) } // if none of these things happened, we can safely (i think) assume that // the keysym is an exact utf-32 code point. return rune(keysym) }