387 lines
12 KiB
Go
387 lines
12 KiB
Go
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)
|
|
}
|