Support keyboard buttons and add presets. (#14)

Reviewed-on: #14
Co-authored-by: Anna Rose Wiggins <annabunches@gmail.com>
Co-committed-by: Anna Rose Wiggins <annabunches@gmail.com>
This commit is contained in:
Anna Rose Wiggins 2025-08-04 19:55:56 +00:00 committed by Anna Rose Wiggins
parent 61fe5208e6
commit 838449000c
12 changed files with 492 additions and 133 deletions

View file

@ -8,13 +8,23 @@ import (
"github.com/holoplot/go-evdev"
)
func parseCodeButton(code string) (evdev.EvCode, error) {
prefix := CodePrefixButton
if strings.HasPrefix(code, CodePrefixKey+"_") {
prefix = CodePrefixKey
}
return parseCode(code, prefix)
}
func parseCode(code, prefix string) (evdev.EvCode, error) {
code = strings.ToUpper(code)
var codeLookup map[string]evdev.EvCode
switch prefix {
case CodePrefixButton:
case CodePrefixButton, CodePrefixKey:
codeLookup = evdev.KEYFromString
case CodePrefixAxis:
codeLookup = evdev.ABSFromString

View file

@ -16,7 +16,7 @@ func TestRunnerEventCodeParserTests(t *testing.T) {
suite.Run(t, new(EventCodeParserTests))
}
func parseCodeTestCase(t *EventCodeParserTests, in string, out int, prefix string) {
func parseCodeTestCase(t *EventCodeParserTests, in string, out evdev.EvCode, prefix string) {
t.Run(fmt.Sprintf("%s: %s", prefix, in), func() {
code, err := parseCode(in, prefix)
t.Nil(err)
@ -24,95 +24,119 @@ func parseCodeTestCase(t *EventCodeParserTests, in string, out int, prefix strin
})
}
func (t *EventCodeParserTests) TestParseCodeABS() {
func (t *EventCodeParserTests) TestParseCodeButton() {
testCases := []struct {
in string
out int
out evdev.EvCode
}{
{"ABS_X", evdev.ABS_X},
{"ABS_Y", evdev.ABS_Y},
{"ABS_Z", evdev.ABS_Z},
{"ABS_RX", evdev.ABS_RX},
{"ABS_RY", evdev.ABS_RY},
{"ABS_RZ", evdev.ABS_RZ},
{"ABS_THROTTLE", evdev.ABS_THROTTLE},
{"ABS_RUDDER", evdev.ABS_RUDDER},
{"x", evdev.ABS_X},
{"y", evdev.ABS_Y},
{"z", evdev.ABS_Z},
{"throttle", evdev.ABS_THROTTLE},
{"rudder", evdev.ABS_RUDDER},
{"0x0", evdev.ABS_X},
{"0x1", evdev.ABS_Y},
{"0x2", evdev.ABS_Z},
{"BTN_A", evdev.BTN_A},
{"A", evdev.BTN_A},
{"BTN_TRIGGER_HAPPY", evdev.BTN_TRIGGER_HAPPY},
{"KEY_A", evdev.KEY_A},
{"KEY_ESC", evdev.KEY_ESC},
}
for _, testCase := range testCases {
parseCodeTestCase(t, testCase.in, testCase.out, "ABS")
}
}
func (t *EventCodeParserTests) TestParseCodeREL() {
testCases := []struct {
in string
out int
}{
{"REL_X", evdev.REL_X},
{"REL_Y", evdev.REL_Y},
{"REL_Z", evdev.REL_Z},
{"REL_RX", evdev.REL_RX},
{"REL_RY", evdev.REL_RY},
{"REL_RZ", evdev.REL_RZ},
{"REL_WHEEL", evdev.REL_WHEEL},
{"REL_HWHEEL", evdev.REL_HWHEEL},
{"REL_MISC", evdev.REL_MISC},
{"x", evdev.REL_X},
{"y", evdev.REL_Y},
{"wheel", evdev.REL_WHEEL},
{"0x0", evdev.REL_X},
{"0x1", evdev.REL_Y},
{"0x2", evdev.REL_Z},
}
for _, testCase := range testCases {
parseCodeTestCase(t, testCase.in, testCase.out, "REL")
}
}
func (t *EventCodeParserTests) TestParseCodeBTN() {
testCases := []struct {
in string
out int
}{
{"BTN_TRIGGER", evdev.BTN_TRIGGER},
{"trigger", evdev.BTN_TRIGGER},
{"0", evdev.BTN_TRIGGER},
{"0x120", evdev.BTN_TRIGGER},
}
for _, testCase := range testCases {
parseCodeTestCase(t, testCase.in, testCase.out, "BTN")
}
}
func (t *EventCodeParserTests) TestParseCodeInvalid() {
testCases := []struct {
in string
prefix string
}{
{"badbutton", "BTN"},
{"ABS_X", "BTN"},
{"!@#$%^&*(){}-_", "BTN"},
{"REL_X", "ABS"},
{"ABS_W", "ABS"},
{"0", "ABS"},
{"0xg", "ABS"},
}
for _, testCase := range testCases {
t.Run(fmt.Sprintf("%s - '%s'", testCase.prefix, testCase.in), func() {
_, err := parseCode(testCase.in, testCase.prefix)
t.NotNil(err)
t.Run(testCase.in, func() {
code, err := parseCodeButton(testCase.in)
t.Nil(err)
t.EqualValues(code, testCase.out)
})
}
}
func (t *EventCodeParserTests) TestParseCode() {
t.Run("ABS", func() {
testCases := []struct {
in string
out evdev.EvCode
}{
{"ABS_X", evdev.ABS_X},
{"ABS_Y", evdev.ABS_Y},
{"ABS_Z", evdev.ABS_Z},
{"ABS_RX", evdev.ABS_RX},
{"ABS_RY", evdev.ABS_RY},
{"ABS_RZ", evdev.ABS_RZ},
{"ABS_THROTTLE", evdev.ABS_THROTTLE},
{"ABS_RUDDER", evdev.ABS_RUDDER},
{"x", evdev.ABS_X},
{"y", evdev.ABS_Y},
{"z", evdev.ABS_Z},
{"throttle", evdev.ABS_THROTTLE},
{"rudder", evdev.ABS_RUDDER},
{"0x0", evdev.ABS_X},
{"0x1", evdev.ABS_Y},
{"0x2", evdev.ABS_Z},
}
for _, testCase := range testCases {
parseCodeTestCase(t, testCase.in, testCase.out, "ABS")
}
})
t.Run("REL", func() {
testCases := []struct {
in string
out evdev.EvCode
}{
{"REL_X", evdev.REL_X},
{"REL_Y", evdev.REL_Y},
{"REL_Z", evdev.REL_Z},
{"REL_RX", evdev.REL_RX},
{"REL_RY", evdev.REL_RY},
{"REL_RZ", evdev.REL_RZ},
{"REL_WHEEL", evdev.REL_WHEEL},
{"REL_HWHEEL", evdev.REL_HWHEEL},
{"REL_MISC", evdev.REL_MISC},
{"x", evdev.REL_X},
{"y", evdev.REL_Y},
{"wheel", evdev.REL_WHEEL},
{"0x0", evdev.REL_X},
{"0x1", evdev.REL_Y},
{"0x2", evdev.REL_Z},
}
for _, testCase := range testCases {
parseCodeTestCase(t, testCase.in, testCase.out, "REL")
}
})
t.Run("BTN", func() {
testCases := []struct {
in string
out evdev.EvCode
}{
{"BTN_TRIGGER", evdev.BTN_TRIGGER},
{"trigger", evdev.BTN_TRIGGER},
{"0", evdev.BTN_TRIGGER},
{"0x120", evdev.BTN_TRIGGER},
}
for _, testCase := range testCases {
parseCodeTestCase(t, testCase.in, testCase.out, "BTN")
}
})
t.Run("Invalid", func() {
testCases := []struct {
in string
prefix string
}{
{"badbutton", "BTN"},
{"ABS_X", "BTN"},
{"!@#$%^&*(){}-_", "BTN"},
{"REL_X", "ABS"},
{"ABS_W", "ABS"},
{"0", "ABS"},
{"0xg", "ABS"},
}
for _, testCase := range testCases {
t.Run(fmt.Sprintf("%s - '%s'", testCase.prefix, testCase.in), func() {
_, err := parseCode(testCase.in, testCase.prefix)
t.NotNil(err)
})
}
})
}

View file

@ -23,10 +23,25 @@ func (parser *ConfigParser) CreateVirtualDevices() map[string]*evdev.InputDevice
}
name := fmt.Sprintf("joyful-%s", deviceConfig.Name)
capabilities := map[evdev.EvType][]evdev.EvCode{
evdev.EV_KEY: makeButtons(deviceConfig.NumButtons, deviceConfig.Buttons),
evdev.EV_ABS: makeAxes(deviceConfig.NumAxes, deviceConfig.Axes),
evdev.EV_REL: makeRelativeAxes(deviceConfig.NumRelativeAxes, deviceConfig.RelativeAxes),
var capabilities map[evdev.EvType][]evdev.EvCode
// todo: add tests for presets
switch deviceConfig.Preset {
case DevicePresetGamepad:
capabilities = CapabilitiesPresetGamepad
case DevicePresetKeyboard:
capabilities = CapabilitiesPresetKeyboard
case DevicePresetJoystick:
capabilities = CapabilitiesPresetJoystick
case DevicePresetMouse:
capabilities = CapabilitiesPresetMouse
default:
capabilities = map[evdev.EvType][]evdev.EvCode{
evdev.EV_KEY: makeButtons(deviceConfig.NumButtons, deviceConfig.Buttons),
evdev.EV_ABS: makeAxes(deviceConfig.NumAxes, deviceConfig.Axes),
evdev.EV_REL: makeRelativeAxes(deviceConfig.NumRelativeAxes, deviceConfig.RelativeAxes),
}
}
device, err := evdev.CreateDevice(
@ -60,13 +75,12 @@ func (parser *ConfigParser) CreateVirtualDevices() map[string]*evdev.InputDevice
}
// ConnectPhysicalDevices will create InputDevices corresponding to any registered
// devices with type = physical. It will also attempt to acquire exclusive access
// to those devices, to prevent the same inputs from being read on multiple devices.
// devices with type = physical.
//
// This function assumes you have already called Parse() on the config directory.
//
// This function should only be called once.
func (parser *ConfigParser) ConnectPhysicalDevices(lock bool) map[string]*evdev.InputDevice {
func (parser *ConfigParser) ConnectPhysicalDevices() map[string]*evdev.InputDevice {
deviceMap := make(map[string]*evdev.InputDevice)
for _, deviceConfig := range parser.config.Devices {
@ -80,7 +94,8 @@ func (parser *ConfigParser) ConnectPhysicalDevices(lock bool) map[string]*evdev.
continue
}
if lock {
if deviceConfig.Lock {
logger.LogDebugf("Locking device '%s'", deviceConfig.DeviceName)
err := device.Grab()
if err != nil {
logger.LogError(err, "Failed to grab device for exclusive access")

View file

@ -14,7 +14,7 @@ func makeRuleTargetButton(targetConfig RuleTargetConfig, devs map[string]Device)
return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device)
}
eventCode, err := parseCode(targetConfig.Button, "BTN")
eventCode, err := parseCodeButton(targetConfig.Button)
if err != nil {
return nil, err
}
@ -37,7 +37,7 @@ func makeRuleTargetAxis(targetConfig RuleTargetConfig, devs map[string]Device) (
return nil, errors.New("deadzone_end must be greater than deadzone_start")
}
eventCode, err := parseCode(targetConfig.Axis, "ABS")
eventCode, err := parseCode(targetConfig.Axis, CodePrefixAxis)
if err != nil {
return nil, err
}
@ -63,7 +63,7 @@ func makeRuleTargetRelaxis(targetConfig RuleTargetConfig, devs map[string]Device
return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device)
}
eventCode, err := parseCode(targetConfig.Axis, "REL")
eventCode, err := parseCode(targetConfig.Axis, CodePrefixRelaxis)
if err != nil {
return nil, err
}

View file

@ -20,12 +20,14 @@ type DeviceConfig struct {
Type string `yaml:"type"`
DeviceName string `yaml:"device_name,omitempty"`
Uuid string `yaml:"uuid,omitempty"`
Preset string `yaml:"preset,omitempty"`
NumButtons int `yaml:"num_buttons,omitempty"`
NumAxes int `yaml:"num_axes,omitempty"`
NumRelativeAxes int `yaml:"num_rel_axes"`
Buttons []string `yaml:"buttons,omitempty"`
Axes []string `yaml:"axes,omitempty"`
RelativeAxes []string `yaml:"rel_axes,omitempty"`
Lock bool `yaml:"lock,omitempty"`
}
type RuleConfig struct {
@ -54,3 +56,44 @@ type RuleTargetConfig struct {
Inverted bool `yaml:"inverted,omitempty"`
Modes []string `yaml:"modes,omitempty"`
}
// TODO: custom yaml unmarshaling is obtuse; do we really need to do all of this work
// just to set a single default value?
func (dc *DeviceConfig) UnmarshalYAML(unmarshal func(data interface{}) error) error {
var raw struct {
Name string
Type string
DeviceName string `yaml:"device_name"`
Uuid string
Preset string
NumButtons int `yaml:"num_buttons"`
NumAxes int `yaml:"num_axes"`
NumRelativeAxes int `yaml:"num_rel_axes"`
Buttons []string
Axes []string
RelativeAxes []string `yaml:"relative_axes"`
Lock bool `yaml:"lock,omitempty"`
}
raw.Lock = true
err := unmarshal(&raw)
if err != nil {
return err
}
*dc = DeviceConfig{
Name: raw.Name,
Type: raw.Type,
DeviceName: raw.DeviceName,
Uuid: raw.Uuid,
Preset: raw.Preset,
NumButtons: raw.NumButtons,
NumAxes: raw.NumAxes,
NumRelativeAxes: raw.NumRelativeAxes,
Buttons: raw.Buttons,
Axes: raw.Axes,
RelativeAxes: raw.RelativeAxes,
Lock: raw.Lock,
}
return nil
}

View file

@ -8,6 +8,11 @@ const (
DeviceTypePhysical = "physical"
DeviceTypeVirtual = "virtual"
DevicePresetKeyboard = "keyboard"
DevicePresetGamepad = "gamepad"
DevicePresetJoystick = "joystick"
DevicePresetMouse = "mouse"
RuleTypeButton = "button"
RuleTypeButtonCombo = "button-combo"
RuleTypeLatched = "button-latched"
@ -18,6 +23,7 @@ const (
RuleTypeAxisToRelaxis = "axis-to-relaxis"
CodePrefixButton = "BTN"
CodePrefixKey = "KEY"
CodePrefixAxis = "ABS"
CodePrefixRelaxis = "REL"
@ -102,3 +108,281 @@ var (
evdev.EvCode(0x2ff),
}
)
// Device Presets
var (
CapabilitiesPresetGamepad = map[evdev.EvType][]evdev.EvCode{
evdev.EV_ABS: {
evdev.ABS_X,
evdev.ABS_Y,
evdev.ABS_Z,
evdev.ABS_RX,
evdev.ABS_RY,
evdev.ABS_RZ,
evdev.ABS_HAT0X,
evdev.ABS_HAT0Y,
},
evdev.EV_KEY: {
evdev.BTN_NORTH, // Xbox 'X', Playstation 'Square'
evdev.BTN_SOUTH, // Xbox 'A', Plastation 'X'
evdev.BTN_WEST, // Xbox 'Y', Playstation 'Triangle'
evdev.BTN_EAST, // Xbox 'B', Playstation 'O'
evdev.BTN_THUMBL,
evdev.BTN_THUMBR,
evdev.BTN_TL,
evdev.BTN_TR,
evdev.BTN_SELECT,
evdev.BTN_START,
evdev.BTN_MODE,
},
}
CapabilitiesPresetJoystick = map[evdev.EvType][]evdev.EvCode{
evdev.EV_ABS: {
evdev.ABS_X,
evdev.ABS_Y,
evdev.ABS_Z,
evdev.ABS_RX,
evdev.ABS_RY,
evdev.ABS_RZ,
evdev.ABS_THROTTLE, // Also called "Slider" or "Slider1"
evdev.ABS_RUDDER, // Also called "Dial", "Slider2", or "RSlider"
},
evdev.EV_KEY: {
evdev.BTN_TRIGGER,
evdev.BTN_THUMB,
evdev.BTN_THUMB2,
evdev.BTN_TOP,
evdev.BTN_TOP2,
evdev.BTN_PINKIE,
evdev.BTN_BASE,
evdev.BTN_BASE2,
evdev.BTN_BASE3,
evdev.BTN_BASE4,
evdev.BTN_BASE5,
evdev.BTN_BASE6,
evdev.EvCode(0x12c), // decimal 300
evdev.EvCode(0x12d), // decimal 301
evdev.EvCode(0x12e), // decimal 302
evdev.BTN_DEAD,
evdev.BTN_TRIGGER_HAPPY1,
evdev.BTN_TRIGGER_HAPPY2,
evdev.BTN_TRIGGER_HAPPY3,
evdev.BTN_TRIGGER_HAPPY4,
evdev.BTN_TRIGGER_HAPPY5,
evdev.BTN_TRIGGER_HAPPY6,
evdev.BTN_TRIGGER_HAPPY7,
evdev.BTN_TRIGGER_HAPPY8,
evdev.BTN_TRIGGER_HAPPY9,
evdev.BTN_TRIGGER_HAPPY10,
evdev.BTN_TRIGGER_HAPPY11,
evdev.BTN_TRIGGER_HAPPY12,
evdev.BTN_TRIGGER_HAPPY13,
evdev.BTN_TRIGGER_HAPPY14,
evdev.BTN_TRIGGER_HAPPY15,
evdev.BTN_TRIGGER_HAPPY16,
evdev.BTN_TRIGGER_HAPPY17,
evdev.BTN_TRIGGER_HAPPY18,
evdev.BTN_TRIGGER_HAPPY19,
evdev.BTN_TRIGGER_HAPPY20,
evdev.BTN_TRIGGER_HAPPY21,
evdev.BTN_TRIGGER_HAPPY22,
evdev.BTN_TRIGGER_HAPPY23,
evdev.BTN_TRIGGER_HAPPY24,
evdev.BTN_TRIGGER_HAPPY25,
evdev.BTN_TRIGGER_HAPPY26,
evdev.BTN_TRIGGER_HAPPY27,
evdev.BTN_TRIGGER_HAPPY28,
evdev.BTN_TRIGGER_HAPPY29,
evdev.BTN_TRIGGER_HAPPY30,
evdev.BTN_TRIGGER_HAPPY31,
evdev.BTN_TRIGGER_HAPPY32,
evdev.BTN_TRIGGER_HAPPY33,
evdev.BTN_TRIGGER_HAPPY34,
evdev.BTN_TRIGGER_HAPPY35,
evdev.BTN_TRIGGER_HAPPY36,
evdev.BTN_TRIGGER_HAPPY37,
evdev.BTN_TRIGGER_HAPPY38,
evdev.BTN_TRIGGER_HAPPY39,
evdev.BTN_TRIGGER_HAPPY40,
evdev.EvCode(0x2e8),
evdev.EvCode(0x2e9),
evdev.EvCode(0x2f0),
evdev.EvCode(0x2f1),
evdev.EvCode(0x2f2),
evdev.EvCode(0x2f3),
evdev.EvCode(0x2f4),
evdev.EvCode(0x2f5),
evdev.EvCode(0x2f6),
evdev.EvCode(0x2f7),
evdev.EvCode(0x2f8),
evdev.EvCode(0x2f9),
evdev.EvCode(0x2fa),
evdev.EvCode(0x2fb),
evdev.EvCode(0x2fc),
evdev.EvCode(0x2fd),
evdev.EvCode(0x2fe),
evdev.EvCode(0x2ff),
},
}
CapabilitiesPresetKeyboard = map[evdev.EvType][]evdev.EvCode{
evdev.EV_KEY: {
evdev.KEY_ESC,
evdev.KEY_1,
evdev.KEY_2,
evdev.KEY_3,
evdev.KEY_4,
evdev.KEY_5,
evdev.KEY_6,
evdev.KEY_7,
evdev.KEY_8,
evdev.KEY_9,
evdev.KEY_0,
evdev.KEY_MINUS,
evdev.KEY_EQUAL,
evdev.KEY_BACKSPACE,
evdev.KEY_TAB,
evdev.KEY_Q,
evdev.KEY_W,
evdev.KEY_E,
evdev.KEY_R,
evdev.KEY_T,
evdev.KEY_Y,
evdev.KEY_U,
evdev.KEY_I,
evdev.KEY_O,
evdev.KEY_P,
evdev.KEY_LEFTBRACE,
evdev.KEY_RIGHTBRACE,
evdev.KEY_ENTER,
evdev.KEY_LEFTCTRL,
evdev.KEY_A,
evdev.KEY_S,
evdev.KEY_D,
evdev.KEY_F,
evdev.KEY_G,
evdev.KEY_H,
evdev.KEY_J,
evdev.KEY_K,
evdev.KEY_L,
evdev.KEY_SEMICOLON,
evdev.KEY_APOSTROPHE,
evdev.KEY_GRAVE,
evdev.KEY_LEFTSHIFT,
evdev.KEY_BACKSLASH,
evdev.KEY_Z,
evdev.KEY_X,
evdev.KEY_C,
evdev.KEY_V,
evdev.KEY_B,
evdev.KEY_N,
evdev.KEY_M,
evdev.KEY_COMMA,
evdev.KEY_DOT,
evdev.KEY_SLASH,
evdev.KEY_RIGHTSHIFT,
evdev.KEY_KPASTERISK,
evdev.KEY_LEFTALT,
evdev.KEY_SPACE,
evdev.KEY_CAPSLOCK,
evdev.KEY_F1,
evdev.KEY_F2,
evdev.KEY_F3,
evdev.KEY_F4,
evdev.KEY_F5,
evdev.KEY_F6,
evdev.KEY_F7,
evdev.KEY_F8,
evdev.KEY_F9,
evdev.KEY_F10,
evdev.KEY_NUMLOCK,
evdev.KEY_SCROLLLOCK,
evdev.KEY_KP7,
evdev.KEY_KP8,
evdev.KEY_KP9,
evdev.KEY_KPMINUS,
evdev.KEY_KP4,
evdev.KEY_KP5,
evdev.KEY_KP6,
evdev.KEY_KPPLUS,
evdev.KEY_KP1,
evdev.KEY_KP2,
evdev.KEY_KP3,
evdev.KEY_KP0,
evdev.KEY_KPDOT,
evdev.KEY_ZENKAKUHANKAKU,
evdev.KEY_102ND,
evdev.KEY_F11,
evdev.KEY_F12,
evdev.KEY_RO,
evdev.KEY_KATAKANA,
evdev.KEY_HIRAGANA,
evdev.KEY_HENKAN,
evdev.KEY_KATAKANAHIRAGANA,
evdev.KEY_MUHENKAN,
evdev.KEY_KPJPCOMMA,
evdev.KEY_KPENTER,
evdev.KEY_RIGHTCTRL,
evdev.KEY_KPSLASH,
evdev.KEY_SYSRQ,
evdev.KEY_RIGHTALT,
evdev.KEY_LINEFEED,
evdev.KEY_HOME,
evdev.KEY_UP,
evdev.KEY_PAGEUP,
evdev.KEY_LEFT,
evdev.KEY_RIGHT,
evdev.KEY_END,
evdev.KEY_DOWN,
evdev.KEY_PAGEDOWN,
evdev.KEY_INSERT,
evdev.KEY_DELETE,
evdev.KEY_MACRO,
evdev.KEY_MUTE,
evdev.KEY_VOLUMEDOWN,
evdev.KEY_VOLUMEUP,
evdev.KEY_KPEQUAL,
evdev.KEY_KPPLUSMINUS,
evdev.KEY_PAUSE,
evdev.KEY_SCALE,
evdev.KEY_KPCOMMA,
evdev.KEY_HANGEUL,
evdev.KEY_HANJA,
evdev.KEY_YEN,
evdev.KEY_LEFTMETA,
evdev.KEY_RIGHTMETA,
evdev.KEY_COMPOSE,
evdev.KEY_F13,
evdev.KEY_F14,
evdev.KEY_F15,
evdev.KEY_F16,
evdev.KEY_F17,
evdev.KEY_F18,
evdev.KEY_F19,
evdev.KEY_F20,
evdev.KEY_F21,
evdev.KEY_F22,
evdev.KEY_F23,
evdev.KEY_F24,
},
}
CapabilitiesPresetMouse = map[evdev.EvType][]evdev.EvCode{
evdev.EV_REL: {
evdev.REL_X,
evdev.REL_Y,
evdev.REL_WHEEL,
evdev.REL_HWHEEL,
},
evdev.EV_KEY: {
evdev.BTN_LEFT,
evdev.BTN_MIDDLE,
evdev.BTN_RIGHT,
evdev.BTN_SIDE,
evdev.BTN_EXTRA,
evdev.BTN_FORWARD,
evdev.BTN_BACK,
},
}
)