xgbkb/keyboard.go

367 lines
12 KiB
Go

// 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)
}