wm/vend/xgbutil/keybind/keybind.go

387 lines
12 KiB
Go
Raw Normal View History

2023-06-11 14:21:08 +00:00
package keybind
import (
"fmt"
"strings"
"github.com/jezek/xgb/xproto"
"github.com/jezek/xgbutil"
"github.com/jezek/xgbutil/xevent"
)
var (
Modifiers []uint16 = []uint16{ // order matters!
xproto.ModMaskShift, xproto.ModMaskLock, xproto.ModMaskControl,
xproto.ModMask1, xproto.ModMask2, xproto.ModMask3,
xproto.ModMask4, xproto.ModMask5,
xproto.ModMaskAny,
}
NiceModifiers = []string{
"shift", "lock", "control", "mod1", "mod2", "mod3", "mod4", "mod5", "",
}
)
// Initialize attaches the appropriate callbacks to make key bindings easier.
// i.e., update state of the world on a MappingNotify.
func Initialize(xu *xgbutil.XUtil) {
// Listen to mapping notify events
xevent.MappingNotifyFun(updateMaps).Connect(xu, xevent.NoWindow)
// Give us an initial mapping state...
keyMap, modMap := MapsGet(xu)
KeyMapSet(xu, keyMap)
ModMapSet(xu, modMap)
}
// updateMaps runs in response to MappingNotify events.
// It is responsible for making sure our view of the world's keyboard
// and modifier maps is correct. (Pointer mappings should be handled in
// a similar callback in the mousebind package.)
func updateMaps(xu *xgbutil.XUtil, e xevent.MappingNotifyEvent) {
keyMap, modMap := MapsGet(xu)
// So we used to go through the old mapping and the new mapping and pick
// out precisely where there are changes. But after allowing for a
// one-to-many mapping from keysym to keycodes, this process became too
// complex. So we're going to bust out our hammer and rebind everything
// based on the initial key strings.
if e.Request == xproto.MappingKeyboard {
// We must ungrab everything first, in case two keys are being swapped.
keys := keyKeys(xu)
for _, key := range keys {
Ungrab(xu, key.Win, key.Mod, key.Code)
detach(xu, key.Evtype, key.Win)
}
// Wipe the slate clean.
xu.KeybindsLck.Lock()
xu.Keybinds = make(map[xgbutil.KeyKey][]xgbutil.CallbackKey, len(keys))
xu.Keygrabs = make(map[xgbutil.KeyKey]int, len(keys))
keyStrs := xu.Keystrings
xu.KeybindsLck.Unlock()
// Update our mappings before rebinding.
KeyMapSet(xu, keyMap)
ModMapSet(xu, modMap)
// Now rebind everything in Keystrings
for _, ks := range keyStrs {
err := connect(xu,
ks.Callback, ks.Evtype, ks.Win, ks.Str, ks.Grab, true)
if err != nil {
xgbutil.Logger.Println(err)
}
}
} else {
// We don't have to do something with MappingModifier like we do with
// MappingKeyboard. This is due to us requiring that key strings use
// modifier names built into X. (i.e., the names seen in the output of
// `xmodmap`.) This means that the modifier mappings happen on the X
// server side, so we don't *typically* have to care what key is
// actually being pressed to trigger a modifier. (There are some
// exceptional cases, and when that happens, we simply query on-demand
// which keys are modifiers. See the RunKey{Press,Release}Callbacks
// functions in keybind/callback.go for the deets.)
KeyMapSet(xu, keyMap)
ModMapSet(xu, modMap)
}
}
// minMaxKeycodeGet a simple accessor to the X setup info to return the
// minimum and maximum keycodes. They are typically 8 and 255, respectively.
func minMaxKeycodeGet(xu *xgbutil.XUtil) (xproto.Keycode, xproto.Keycode) {
return xu.Setup().MinKeycode, xu.Setup().MaxKeycode
}
// A convenience function to grab the KeyboardMapping and ModifierMapping
// from X. We need to do this on startup (see Initialize) and whenever we
// get a MappingNotify event.
func MapsGet(xu *xgbutil.XUtil) (*xproto.GetKeyboardMappingReply,
*xproto.GetModifierMappingReply) {
min, max := minMaxKeycodeGet(xu)
newKeymap, keyErr := xproto.GetKeyboardMapping(xu.Conn(), min,
byte(max-min+1)).Reply()
newModmap, modErr := xproto.GetModifierMapping(xu.Conn()).Reply()
// If there are errors, we really need to panic. We just can't do
// any key binding without a mapping from the server.
if keyErr != nil {
panic(fmt.Sprintf("COULD NOT GET KEYBOARD MAPPING: %v\n"+
"THIS IS AN UNRECOVERABLE ERROR.\n",
keyErr))
}
if modErr != nil {
panic(fmt.Sprintf("COULD NOT GET MODIFIER MAPPING: %v\n"+
"THIS IS AN UNRECOVERABLE ERROR.\n",
keyErr))
}
return newKeymap, newModmap
}
// ParseString takes a string of the format '[Mod[-Mod[...]]]-KEY',
// i.e., 'Mod4-j', and returns a modifiers/keycode combo.
// An error is returned if the string is malformed, or if no valid KEY can
// be found.
// Valid values of KEY should include almost anything returned by pressing
// keys with the 'xev' program. Alternatively, you may reference the keys
// of the 'keysyms' map defined in keybind/keysymdef.go.
func ParseString(
xu *xgbutil.XUtil, s string) (uint16, []xproto.Keycode, error) {
mods, kcs := uint16(0), []xproto.Keycode{}
for _, part := range strings.Split(s, "-") {
switch strings.ToLower(part) {
case "shift":
mods |= xproto.ModMaskShift
case "lock":
mods |= xproto.ModMaskLock
case "control":
mods |= xproto.ModMaskControl
case "mod1":
mods |= xproto.ModMask1
case "mod2":
mods |= xproto.ModMask2
case "mod3":
mods |= xproto.ModMask3
case "mod4":
mods |= xproto.ModMask4
case "mod5":
mods |= xproto.ModMask5
case "any":
mods |= xproto.ModMaskAny
default: // a key code!
if len(kcs) == 0 { // only accept the first keycode we see
kcs = StrToKeycodes(xu, part)
}
}
}
if len(kcs) == 0 {
return 0, nil, fmt.Errorf("Could not find a valid keycode in the "+
"string '%s'. Key binding failed.", s)
}
return mods, kcs, nil
}
// StrToKeycodes is a wrapper around keycodesGet meant to make our search
// a bit more flexible if needed. (i.e., case-insensitive)
func StrToKeycodes(xu *xgbutil.XUtil, str string) []xproto.Keycode {
// Do some fancy case stuff before we give up.
sym, ok := keysyms[str]
if !ok {
sym, ok = keysyms[strings.Title(str)]
}
if !ok {
sym, ok = keysyms[strings.ToLower(str)]
}
if !ok {
sym, ok = keysyms[strings.ToUpper(str)]
}
// If we don't know what 'str' is, return 0.
// There will probably be a bad access. We should do better than that...
if !ok {
return []xproto.Keycode{}
}
return keycodesGet(xu, sym)
}
// keysymsPer gets the number of keysyms per keycode for the current key map.
func keysymsPer(xu *xgbutil.XUtil) int {
return int(KeyMapGet(xu).KeysymsPerKeycode)
}
// Given a keysym, find all keycodes mapped to it in the current X environment.
// keybind.Initialize MUST have been called before using this function.
func keycodesGet(xu *xgbutil.XUtil, keysym xproto.Keysym) []xproto.Keycode {
min, max := minMaxKeycodeGet(xu)
keyMap := KeyMapGet(xu)
if keyMap == nil {
panic("keybind.Initialize must be called before using the keybind " +
"package.")
}
var c byte
var keycode xproto.Keycode
keycodes := make([]xproto.Keycode, 0)
set := make(map[xproto.Keycode]bool, 0)
for kc := int(min); kc <= int(max); kc++ {
keycode = xproto.Keycode(kc)
for c = 0; c < keyMap.KeysymsPerKeycode; c++ {
if keysym == KeysymGet(xu, keycode, c) && !set[keycode] {
keycodes = append(keycodes, keycode)
set[keycode] = true
}
}
}
return keycodes
}
// KeysymToStr converts a keysym to a string if one is available.
// If one is found, KeysymToStr also checks the 'weirdKeysyms' map, which
// contains a map from multi-character strings to single character
// representations (i.e., 'braceleft' to '{').
// If no match is found initially, an empty string is returned.
func KeysymToStr(keysym xproto.Keysym) string {
symStr, ok := strKeysyms[keysym]
if !ok {
return ""
}
shortSymStr, ok := weirdKeysyms[symStr]
if ok {
return string(shortSymStr)
}
return symStr
}
// KeysymGet is a shortcut alias for 'KeysymGetWithMap' using the current
// keymap stored in XUtil.
// keybind.Initialize MUST have been called before using this function.
func KeysymGet(xu *xgbutil.XUtil, keycode xproto.Keycode,
column byte) xproto.Keysym {
return KeysymGetWithMap(xu, KeyMapGet(xu), keycode, column)
}
// KeysymGetWithMap uses the given key map and finds a keysym associated
// with the given keycode in the current X environment.
func KeysymGetWithMap(xu *xgbutil.XUtil, keyMap *xgbutil.KeyboardMapping,
keycode xproto.Keycode, column byte) xproto.Keysym {
min, _ := minMaxKeycodeGet(xu)
i := (int(keycode)-int(min))*int(keyMap.KeysymsPerKeycode) + int(column)
return keyMap.Keysyms[i]
}
// ModGet finds the modifier currently associated with a given keycode.
// If a modifier doesn't exist for this keycode, then 0 is returned.
func ModGet(xu *xgbutil.XUtil, keycode xproto.Keycode) uint16 {
modMap := ModMapGet(xu)
var i byte
for i = 0; int(i) < len(modMap.Keycodes); i++ {
if modMap.Keycodes[i] == keycode {
return Modifiers[i/modMap.KeycodesPerModifier]
}
}
return 0
}
// Grab grabs a key with mods on a particular window.
// This will also grab all combinations of modifiers found in xevent.IgnoreMods.
func Grab(xu *xgbutil.XUtil, win xproto.Window,
mods uint16, key xproto.Keycode) {
for _, m := range xevent.IgnoreMods {
xproto.GrabKey(xu.Conn(), true, win, mods|m, key,
xproto.GrabModeAsync, xproto.GrabModeAsync)
}
}
// GrabChecked Grabs a key with mods on a particular window.
// This is the same as Grab, except that it issue a checked request.
// Which means that an error could be returned and handled on the spot.
// (Checked requests are slower than unchecked requests.)
// This will also grab all combinations of modifiers found in xevent.IgnoreMods.
func GrabChecked(xu *xgbutil.XUtil, win xproto.Window,
mods uint16, key xproto.Keycode) error {
var err error
for _, m := range xevent.IgnoreMods {
err = xproto.GrabKeyChecked(xu.Conn(), true, win, mods|m, key,
xproto.GrabModeAsync, xproto.GrabModeAsync).Check()
if err != nil {
return err
}
}
return nil
}
// Ungrab undoes Grab. It will handle all combinations od modifiers found
// in xevent.IgnoreMods.
func Ungrab(xu *xgbutil.XUtil, win xproto.Window,
mods uint16, key xproto.Keycode) {
for _, m := range xevent.IgnoreMods {
xproto.UngrabKeyChecked(xu.Conn(), key, win, mods|m).Check()
}
}
// GrabKeyboard grabs the entire keyboard.
// Returns whether GrabStatus is successful and an error if one is reported by
// XGB. It is possible to not get an error and the grab to be unsuccessful.
// The purpose of 'win' is that after a grab is successful, ALL Key*Events will
// be sent to that window. Make sure you have a callback attached :-)
func GrabKeyboard(xu *xgbutil.XUtil, win xproto.Window) error {
reply, err := xproto.GrabKeyboard(xu.Conn(), false, win, 0,
xproto.GrabModeAsync, xproto.GrabModeAsync).Reply()
if err != nil {
return fmt.Errorf("GrabKeyboard: Error grabbing keyboard on "+
"window '%x': %s", win, err)
}
switch reply.Status {
case xproto.GrabStatusSuccess:
// all is well
case xproto.GrabStatusAlreadyGrabbed:
return fmt.Errorf("GrabKeyboard: Could not grab keyboard. " +
"Status: AlreadyGrabbed.")
case xproto.GrabStatusInvalidTime:
return fmt.Errorf("GrabKeyboard: Could not grab keyboard. " +
"Status: InvalidTime.")
case xproto.GrabStatusNotViewable:
return fmt.Errorf("GrabKeyboard: Could not grab keyboard. " +
"Status: NotViewable.")
case xproto.GrabStatusFrozen:
return fmt.Errorf("GrabKeyboard: Could not grab keyboard. " +
"Status: Frozen.")
}
return nil
}
// UngrabKeyboard undoes GrabKeyboard.
func UngrabKeyboard(xu *xgbutil.XUtil) {
xproto.UngrabKeyboard(xu.Conn(), 0)
}
// SmartGrab grabs the keyboard for the given window, and redirects all
// key events in the xevent main event loop to avoid races.
func SmartGrab(xu *xgbutil.XUtil, win xproto.Window) error {
err := GrabKeyboard(xu, win)
if err != nil {
return fmt.Errorf("SmartGrab: %s", err)
}
// Now redirect all key events to the dummy window to prevent races
xevent.RedirectKeyEvents(xu, win)
return nil
}
// SmartUngrab reverses SmartGrab and stops redirecting all key events.
func SmartUngrab(xu *xgbutil.XUtil) {
UngrabKeyboard(xu)
// Stop redirecting all key events
xevent.RedirectKeyEvents(xu, 0)
}
// DummyGrab grabs the keyboard and sends all key events to the dummy window.
func DummyGrab(xu *xgbutil.XUtil) error {
return SmartGrab(xu, xu.Dummy())
}
// DummyUngrab ungrabs the keyboard from the dummy window.
func DummyUngrab(xu *xgbutil.XUtil) {
SmartUngrab(xu)
}