Begin to overhaul config to couple initialization logic closer to the structs themselves.

This commit is contained in:
Anna Rose Wiggins 2025-08-11 12:38:07 -04:00
parent d9babf5dc0
commit 1b374bccc6
15 changed files with 122 additions and 33 deletions

View file

@ -0,0 +1,72 @@
package configparser
import (
"fmt"
"strconv"
"strings"
"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, CodePrefixKey:
codeLookup = evdev.KEYFromString
case CodePrefixAxis:
codeLookup = evdev.ABSFromString
case CodePrefixRelaxis:
codeLookup = evdev.RELFromString
default:
return 0, fmt.Errorf("invalid EvCode prefix '%s'", prefix)
}
switch {
case strings.HasPrefix(code, prefix+"_"):
eventCode, ok := codeLookup[code]
if !ok {
return 0, fmt.Errorf("invalid keycode specification '%s'", code)
}
return eventCode, nil
case strings.HasPrefix(code, "0X"):
codeInt, err := strconv.ParseUint(code[2:], 16, 0)
if err != nil {
return 0, err
}
return evdev.EvCode(codeInt), nil
case prefix == CodePrefixButton && !hasError(strconv.Atoi(code)):
index, err := strconv.Atoi(code)
if err != nil {
return 0, err
}
if index >= len(ButtonFromIndex) {
return 0, fmt.Errorf("button index '%d' out of bounds", index)
}
return ButtonFromIndex[index], nil
default:
eventCode, ok := codeLookup[prefix+"_"+code]
if !ok {
return 0, fmt.Errorf("invalid keycode specification '%s'", code)
}
return eventCode, nil
}
}

View file

@ -0,0 +1,142 @@
package configparser
import (
"fmt"
"testing"
"github.com/holoplot/go-evdev"
"github.com/stretchr/testify/suite"
)
type EventCodeParserTests struct {
suite.Suite
}
func TestRunnerEventCodeParserTests(t *testing.T) {
suite.Run(t, new(EventCodeParserTests))
}
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)
t.EqualValues(out, code)
})
}
func (t *EventCodeParserTests) TestParseCodeButton() {
testCases := []struct {
in string
out evdev.EvCode
}{
{"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 {
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

@ -0,0 +1,133 @@
// The ConfigParser is the main structure you'll interact with when using this package.
//
// Example usage:
// config := &config.ConfigParser{}
// config.Parse(<some directory containing YAML files>)
// virtualDevices := config.CreateVirtualDevices()
// physicalDevices := config.ConnectVirtualDevices()
// modes := config.GetModes()
// rules := config.BuildRules(physicalDevices, virtualDevices, modes)
//
// nb: there are methods defined on ConfigParser in other files in this package!
package configparser
import (
"errors"
"os"
"path/filepath"
"strings"
"git.annabunches.net/annabunches/joyful/internal/logger"
"github.com/goccy/go-yaml"
)
type ConfigParser struct {
config Config
}
// Parse all the config files and store the config data for further use
func (parser *ConfigParser) Parse(directory string) error {
parser.config = Config{}
// Find the config files in the directory
dirEntries, err := os.ReadDir(directory)
if err != nil {
err = os.Mkdir(directory, 0755)
if err != nil {
return errors.New("Failed to create config directory at " + directory)
}
}
// Open each yaml file and add its contents to the global config
for _, file := range dirEntries {
name := file.Name()
if file.IsDir() || !(strings.HasSuffix(name, ".yaml") || strings.HasSuffix(name, ".yml")) {
continue
}
filePath := filepath.Join(directory, name)
if strings.HasSuffix(filePath, ".yaml") || strings.HasSuffix(filePath, ".yml") {
data, err := os.ReadFile(filePath)
if err != nil {
logger.LogError(err, "Error while opening config file")
continue
}
newConfig := Config{}
err = yaml.Unmarshal(data, &newConfig)
logger.LogIfError(err, "Error parsing YAML")
parser.config.Rules = append(parser.config.Rules, newConfig.Rules...)
parser.config.Devices = append(parser.config.Devices, newConfig.Devices...)
parser.config.Modes = append(parser.config.Modes, newConfig.Modes...)
}
}
if len(parser.config.Devices) == 0 {
return errors.New("Found no devices in configuration. Please add configuration at " + directory)
}
return nil
}
func (parser *ConfigParser) GetModes() []string {
if len(parser.config.Modes) == 0 {
return []string{"*"}
}
return parser.config.Modes
}
func ParseConfig(directory string) (*Config, error) {
config := new(Config)
configFiles, err := getConfigFilePaths(directory)
if err != nil {
return nil, err
}
// Open each yaml file and add its contents to the global config
for _, filePath := range configFiles {
data, err := os.ReadFile(filePath)
if err != nil {
logger.LogError(err, "Error while opening config file")
continue
}
newConfig := Config{}
err = yaml.Unmarshal(data, &newConfig)
logger.LogIfError(err, "Error parsing YAML")
config.Rules = append(config.Rules, newConfig.Rules...)
config.Devices = append(config.Devices, newConfig.Devices...)
config.Modes = append(config.Modes, newConfig.Modes...)
}
if len(config.Devices) == 0 {
return nil, errors.New("Found no devices in configuration. Please add configuration at " + directory)
}
return config, nil
}
func getConfigFilePaths(directory string) ([]string, error) {
paths := make([]string, 0)
dirEntries, err := os.ReadDir(directory)
if err != nil {
err = os.Mkdir(directory, 0755)
if err != nil {
return nil, errors.New("failed to create config directory at " + directory)
} else {
return nil, errors.New("no config files found at " + directory)
}
}
for _, file := range dirEntries {
name := strings.ToLower(file.Name())
if file.IsDir() || !(strings.HasSuffix(name, ".yaml") || strings.HasSuffix(name, ".yml")) {
continue
}
paths = append(paths, filepath.Join(directory, file.Name()))
}
return paths, nil
}

View file

@ -0,0 +1,221 @@
package configparser
import (
"fmt"
"strings"
"git.annabunches.net/annabunches/joyful/internal/logger"
"github.com/holoplot/go-evdev"
)
// InitVirtualDevices will register any configured devices with type = virtual
// using /dev/uinput, and return a map of those devices.
//
// This function assumes Parse() has been called.
//
// This function should only be called once, unless we want to create duplicate devices for some reason.
func (parser *ConfigParser) InitVirtualDevices() map[string]*evdev.InputDevice {
deviceMap := make(map[string]*evdev.InputDevice)
for _, deviceConfig := range parser.config.Devices {
if strings.ToLower(deviceConfig.Type) != DeviceTypeVirtual {
continue
}
deviceConfig := deviceConfig.Config.(DeviceConfigVirtual)
name := fmt.Sprintf("joyful-%s", deviceConfig.Name)
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(
name,
// TODO: who knows what these should actually be
evdev.InputID{
BusType: 0x03,
Vendor: 0x4711,
Product: 0x0816,
Version: 1,
},
capabilities,
)
if err != nil {
logger.LogIfError(err, "Failed to create virtual device")
continue
}
deviceMap[deviceConfig.Name] = device
logger.Log(fmt.Sprintf(
"Created virtual device '%s' with %d buttons, %d axes, and %d relative axes",
name,
len(capabilities[evdev.EV_KEY]),
len(capabilities[evdev.EV_ABS]),
len(capabilities[evdev.EV_REL]),
))
}
return deviceMap
}
// InitPhysicalDevices will create InputDevices corresponding to any registered
// devices with type = physical.
//
// This function assumes Parse() has been called.
//
// This function should only be called once.
func (parser *ConfigParser) InitPhysicalDevices() map[string]*evdev.InputDevice {
deviceMap := make(map[string]*evdev.InputDevice)
for _, deviceConfig := range parser.config.Devices {
if strings.ToLower(deviceConfig.Type) != DeviceTypePhysical {
continue
}
deviceConfig := deviceConfig.Config.(DeviceConfigPhysical)
var infoName string
var device *evdev.InputDevice
var err error
if deviceConfig.DevicePath != "" {
infoName = deviceConfig.DevicePath
device, err = evdev.Open(deviceConfig.DevicePath)
} else {
infoName = deviceConfig.DeviceName
device, err = evdev.OpenByName(deviceConfig.DeviceName)
}
if err != nil {
logger.LogError(err, "Failed to open physical device, skipping. Confirm the device name or path with 'evinfo'")
continue
}
if deviceConfig.Lock {
logger.LogDebugf("Locking device '%s'", infoName)
err := device.Grab()
if err != nil {
logger.LogError(err, "Failed to grab device for exclusive access")
}
}
logger.Log(fmt.Sprintf("Connected to '%s' as '%s'", infoName, deviceConfig.Name))
deviceMap[deviceConfig.Name] = device
}
return deviceMap
}
// TODO: these functions have a lot of duplication; we need to figure out how to refactor it cleanly
// without losing logging context...
func makeButtons(numButtons int, buttonList []string) []evdev.EvCode {
if numButtons > 0 && len(buttonList) > 0 {
logger.Log("'num_buttons' and 'buttons' both specified, ignoring 'num_buttons'")
}
if numButtons > VirtualDeviceMaxButtons {
numButtons = VirtualDeviceMaxButtons
logger.Logf("Limiting virtual device buttons to %d", VirtualDeviceMaxButtons)
}
if len(buttonList) > 0 {
buttons := make([]evdev.EvCode, 0, len(buttonList))
for _, codeStr := range buttonList {
code, err := parseCode(codeStr, "BTN")
if err != nil {
logger.LogError(err, "Failed to create button, skipping")
continue
}
buttons = append(buttons, code)
}
return buttons
}
buttons := make([]evdev.EvCode, numButtons)
for i := 0; i < numButtons; i++ {
buttons[i] = ButtonFromIndex[i]
}
return buttons
}
func makeAxes(numAxes int, axisList []string) []evdev.EvCode {
if numAxes > 0 && len(axisList) > 0 {
logger.Log("'num_axes' and 'axes' both specified, ignoring 'num_axes'")
}
if len(axisList) > 0 {
axes := make([]evdev.EvCode, 0, len(axisList))
for _, codeStr := range axisList {
code, err := parseCode(codeStr, "ABS")
if err != nil {
logger.LogError(err, "Failed to create axis, skipping")
continue
}
axes = append(axes, code)
}
return axes
}
if numAxes > 8 {
numAxes = 8
logger.Log("Limiting virtual device axes to 8")
}
axes := make([]evdev.EvCode, numAxes)
for i := 0; i < numAxes; i++ {
axes[i] = evdev.EvCode(i)
}
return axes
}
func makeRelativeAxes(numAxes int, axisList []string) []evdev.EvCode {
if numAxes > 0 && len(axisList) > 0 {
logger.Log("'num_rel_axes' and 'rel_axes' both specified, ignoring 'num_rel_axes'")
}
if len(axisList) > 0 {
axes := make([]evdev.EvCode, 0, len(axisList))
for _, codeStr := range axisList {
code, err := parseCode(codeStr, "REL")
if err != nil {
logger.LogError(err, "Failed to create axis, skipping")
continue
}
axes = append(axes, code)
}
return axes
}
if numAxes > 10 {
numAxes = 10
logger.Log("Limiting virtual device relative axes to 10")
}
axes := make([]evdev.EvCode, numAxes)
for i := 0; i < numAxes; i++ {
axes[i] = evdev.EvCode(i)
}
return axes
}

View file

@ -0,0 +1,119 @@
package configparser
import (
"testing"
"github.com/holoplot/go-evdev"
"github.com/stretchr/testify/suite"
)
type DevicesConfigTests struct {
suite.Suite
}
func TestRunnerDevicesConfig(t *testing.T) {
suite.Run(t, new(DevicesConfigTests))
}
func (t *DevicesConfigTests) TestMakeButtons() {
t.Run("Maximum buttons", func() {
buttons := makeButtons(VirtualDeviceMaxButtons, []string{})
t.Equal(VirtualDeviceMaxButtons, len(buttons))
})
t.Run("Truncated buttons", func() {
buttons := makeButtons(VirtualDeviceMaxButtons+1, []string{})
t.Equal(VirtualDeviceMaxButtons, len(buttons))
})
t.Run("16 buttons", func() {
buttons := makeButtons(16, []string{})
t.Equal(16, len(buttons))
t.Contains(buttons, evdev.EvCode(evdev.BTN_DEAD))
t.NotContains(buttons, evdev.EvCode(evdev.BTN_TRIGGER_HAPPY))
})
t.Run("Explicit buttons", func() {
buttonConfig := []string{"BTN_THUMB", "top", "btn_top2", "0x2fe", "0x300", "15"}
buttons := makeButtons(0, buttonConfig)
t.Equal(len(buttonConfig), len(buttons))
t.Contains(buttons, evdev.EvCode(0x2fe))
t.Contains(buttons, evdev.EvCode(0x300))
t.Contains(buttons, evdev.EvCode(evdev.BTN_TOP))
t.Contains(buttons, evdev.EvCode(evdev.BTN_DEAD))
})
}
func (t *DevicesConfigTests) TestMakeAxes() {
t.Run("8 axes", func() {
axes := makeAxes(8, []string{})
t.Equal(8, len(axes))
t.Contains(axes, evdev.EvCode(evdev.ABS_X))
t.Contains(axes, evdev.EvCode(evdev.ABS_Y))
t.Contains(axes, evdev.EvCode(evdev.ABS_Z))
t.Contains(axes, evdev.EvCode(evdev.ABS_RX))
t.Contains(axes, evdev.EvCode(evdev.ABS_RY))
t.Contains(axes, evdev.EvCode(evdev.ABS_RZ))
t.Contains(axes, evdev.EvCode(evdev.ABS_THROTTLE))
t.Contains(axes, evdev.EvCode(evdev.ABS_RUDDER))
})
t.Run("9 axes is truncated", func() {
axes := makeAxes(9, []string{})
t.Equal(8, len(axes))
})
t.Run("3 axes", func() {
axes := makeAxes(3, []string{})
t.Equal(3, len(axes))
t.Contains(axes, evdev.EvCode(evdev.ABS_X))
t.Contains(axes, evdev.EvCode(evdev.ABS_Y))
t.Contains(axes, evdev.EvCode(evdev.ABS_Z))
})
t.Run("4 explicit axis", func() {
axes := makeAxes(0, []string{"x", "y", "throttle", "rudder"})
t.Equal(4, len(axes))
t.Contains(axes, evdev.EvCode(evdev.ABS_X))
t.Contains(axes, evdev.EvCode(evdev.ABS_Y))
t.Contains(axes, evdev.EvCode(evdev.ABS_THROTTLE))
t.Contains(axes, evdev.EvCode(evdev.ABS_RUDDER))
})
}
func (t *DevicesConfigTests) TestMakeRelativeAxes() {
t.Run("10 axes", func() {
axes := makeRelativeAxes(10, []string{})
t.Equal(10, len(axes))
t.Contains(axes, evdev.EvCode(evdev.REL_X))
t.Contains(axes, evdev.EvCode(evdev.REL_MISC))
})
t.Run("11 axes", func() {
axes := makeRelativeAxes(11, []string{})
t.Equal(10, len(axes))
})
t.Run("3 axes", func() {
axes := makeRelativeAxes(3, []string{})
t.Equal(3, len(axes))
t.Contains(axes, evdev.EvCode(evdev.REL_X))
t.Contains(axes, evdev.EvCode(evdev.REL_Y))
t.Contains(axes, evdev.EvCode(evdev.REL_Z))
})
t.Run("1 explicit axis", func() {
axes := makeRelativeAxes(0, []string{"wheel"})
t.Equal(1, len(axes))
t.Contains(axes, evdev.EvCode(evdev.REL_WHEEL))
})
t.Run("4 explicit axis", func() {
axes := makeRelativeAxes(0, []string{"x", "y", "wheel", "hwheel"})
t.Equal(4, len(axes))
t.Contains(axes, evdev.EvCode(evdev.REL_X))
t.Contains(axes, evdev.EvCode(evdev.REL_Y))
t.Contains(axes, evdev.EvCode(evdev.REL_WHEEL))
t.Contains(axes, evdev.EvCode(evdev.REL_HWHEEL))
})
}

View file

@ -0,0 +1,7 @@
package configparser
import "github.com/holoplot/go-evdev"
type Device interface {
AbsInfos() (map[evdev.EvCode]evdev.AbsInfo, error)
}

View file

@ -0,0 +1,145 @@
package configparser
import (
"errors"
"fmt"
"git.annabunches.net/annabunches/joyful/internal/mappingrules"
"github.com/holoplot/go-evdev"
)
func makeRuleTargetButton(targetConfig RuleTargetConfigButton, devs map[string]Device) (*mappingrules.RuleTargetButton, error) {
device, ok := devs[targetConfig.Device]
if !ok {
return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device)
}
eventCode, err := parseCodeButton(targetConfig.Button)
if err != nil {
return nil, err
}
return mappingrules.NewRuleTargetButton(
targetConfig.Device,
device,
eventCode,
targetConfig.Inverted,
)
}
func makeRuleTargetAxis(targetConfig RuleTargetConfigAxis, devs map[string]Device) (*mappingrules.RuleTargetAxis, error) {
device, ok := devs[targetConfig.Device]
if !ok {
return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device)
}
if targetConfig.DeadzoneEnd < targetConfig.DeadzoneStart {
return nil, errors.New("deadzone_end must be greater than deadzone_start")
}
eventCode, err := parseCode(targetConfig.Axis, CodePrefixAxis)
if err != nil {
return nil, err
}
deadzoneStart, deadzoneEnd, err := calculateDeadzones(targetConfig, device, eventCode)
if err != nil {
return nil, err
}
return mappingrules.NewRuleTargetAxis(
targetConfig.Device,
device,
eventCode,
targetConfig.Inverted,
deadzoneStart,
deadzoneEnd,
)
}
func makeRuleTargetRelaxis(targetConfig RuleTargetConfigRelaxis, devs map[string]Device) (*mappingrules.RuleTargetRelaxis, error) {
device, ok := devs[targetConfig.Device]
if !ok {
return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device)
}
eventCode, err := parseCode(targetConfig.Axis, CodePrefixRelaxis)
if err != nil {
return nil, err
}
return mappingrules.NewRuleTargetRelaxis(
targetConfig.Device,
device,
eventCode,
)
}
func makeRuleTargetModeSelect(targetConfig RuleTargetConfigModeSelect, allModes []string) (*mappingrules.RuleTargetModeSelect, error) {
if ok := validateModes(targetConfig.Modes, allModes); !ok {
return nil, errors.New("undefined mode in mode select list")
}
return mappingrules.NewRuleTargetModeSelect(targetConfig.Modes)
}
// hasError exists solely to switch on errors in case statements
func hasError(_ any, err error) bool {
return err != nil
}
// calculateDeadzones produces the deadzone start and end values in absolute terms
// TODO: on the one hand, this logic feels betten encapsulated in mappingrules. On the other hand,
// passing even more parameters to NewRuleTargetAxis feels terrible
func calculateDeadzones(targetConfig RuleTargetConfigAxis, device Device, axis evdev.EvCode) (int32, int32, error) {
var deadzoneStart, deadzoneEnd int32
deadzoneStart = 0
deadzoneEnd = 0
if targetConfig.DeadzoneStart != 0 || targetConfig.DeadzoneEnd != 0 {
return targetConfig.DeadzoneStart, targetConfig.DeadzoneEnd, nil
}
var min, max int32
absInfoMap, err := device.AbsInfos()
if err != nil {
min = mappingrules.AxisValueMin
max = mappingrules.AxisValueMax
} else {
absInfo := absInfoMap[axis]
min = absInfo.Minimum
max = absInfo.Maximum
}
if targetConfig.DeadzoneCenter < min || targetConfig.DeadzoneCenter > max {
return 0, 0, fmt.Errorf("deadzone_center '%d' is out of bounds", targetConfig.DeadzoneCenter)
}
switch {
case targetConfig.DeadzoneSize != 0:
deadzoneStart = targetConfig.DeadzoneCenter - targetConfig.DeadzoneSize/2
deadzoneEnd = targetConfig.DeadzoneCenter + targetConfig.DeadzoneSize/2
case targetConfig.DeadzoneSizePercent != 0:
deadzoneSize := (max - min) / targetConfig.DeadzoneSizePercent
deadzoneStart = targetConfig.DeadzoneCenter - deadzoneSize/2
deadzoneEnd = targetConfig.DeadzoneCenter + deadzoneSize/2
}
deadzoneStart, deadzoneEnd = clampAndShift(deadzoneStart, deadzoneEnd, min, max)
return deadzoneStart, deadzoneEnd, nil
}
func clampAndShift(start, end, min, max int32) (int32, int32) {
if start < min {
end += min - start
start = min
}
if end > max {
start -= end - max
end = max
}
return start, end
}

View file

@ -0,0 +1,243 @@
package configparser
import (
"fmt"
"testing"
"github.com/holoplot/go-evdev"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
)
type MakeRuleTargetsTests struct {
suite.Suite
devs map[string]Device
deviceMock *DeviceMock
}
type DeviceMock struct {
mock.Mock
}
func (m *DeviceMock) AbsInfos() (map[evdev.EvCode]evdev.AbsInfo, error) {
args := m.Called()
return args.Get(0).(map[evdev.EvCode]evdev.AbsInfo), args.Error(1)
}
func TestRunnerMakeRuleTargets(t *testing.T) {
suite.Run(t, new(MakeRuleTargetsTests))
}
func (t *MakeRuleTargetsTests) SetupSuite() {
t.deviceMock = new(DeviceMock)
t.deviceMock.On("AbsInfos").Return(
map[evdev.EvCode]evdev.AbsInfo{
evdev.ABS_X: {
Minimum: 0,
Maximum: 10000,
},
evdev.ABS_Y: {
Minimum: 0,
Maximum: 10000,
},
}, nil,
)
t.devs = map[string]Device{
"test": t.deviceMock,
}
}
func (t *MakeRuleTargetsTests) TestMakeRuleTargetButton() {
config := RuleTargetConfigButton{Device: "test"}
t.Run("Standard keycode", func() {
config.Button = "BTN_TRIGGER"
rule, err := makeRuleTargetButton(config, t.devs)
t.Nil(err)
t.EqualValues(evdev.BTN_TRIGGER, rule.Button)
})
t.Run("Hex code", func() {
config.Button = "0x2fd"
rule, err := makeRuleTargetButton(config, t.devs)
t.Nil(err)
t.EqualValues(evdev.EvCode(0x2fd), rule.Button)
})
t.Run("Index", func() {
config.Button = "3"
rule, err := makeRuleTargetButton(config, t.devs)
t.Nil(err)
t.EqualValues(evdev.BTN_TOP, rule.Button)
})
t.Run("Index too high", func() {
config.Button = "74"
_, err := makeRuleTargetButton(config, t.devs)
t.NotNil(err)
})
t.Run("Un-prefixed keycode", func() {
config.Button = "pinkie"
rule, err := makeRuleTargetButton(config, t.devs)
t.Nil(err)
t.EqualValues(evdev.BTN_PINKIE, rule.Button)
})
t.Run("Invalid keycode", func() {
config.Button = "foo"
_, err := makeRuleTargetButton(config, t.devs)
t.NotNil(err)
})
}
func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() {
codeTestCases := []struct {
input string
output evdev.EvCode
}{
{"ABS_X", evdev.ABS_X},
{"0x01", evdev.ABS_Y},
{"x", evdev.ABS_X},
}
for _, tc := range codeTestCases {
t.Run(fmt.Sprintf("KeyCode %s", tc.input), func() {
config := RuleTargetConfigAxis{Device: "test"}
config.Axis = tc.input
rule, err := makeRuleTargetAxis(config, t.devs)
t.Nil(err)
t.EqualValues(tc.output, rule.Axis)
})
}
t.Run("Invalid code", func() {
config := RuleTargetConfigAxis{Device: "test"}
config.Axis = "foo"
_, err := makeRuleTargetAxis(config, t.devs)
t.NotNil(err)
})
t.Run("Invalid deadzone", func() {
config := RuleTargetConfigAxis{Device: "test"}
config.Axis = "x"
config.DeadzoneEnd = 100
config.DeadzoneStart = 1000
_, err := makeRuleTargetAxis(config, t.devs)
t.NotNil(err)
})
relDeadzoneTestCases := []struct {
inCenter int32
inSize int32
outStart int32
outEnd int32
}{
{5000, 1000, 4500, 5500},
{0, 500, 0, 500},
{10000, 500, 9500, 10000},
}
for _, tc := range relDeadzoneTestCases {
t.Run(fmt.Sprintf("Relative Deadzone %d +- %d", tc.inCenter, tc.inSize), func() {
config := RuleTargetConfigAxis{
Device: "test",
Axis: "x",
DeadzoneCenter: tc.inCenter,
DeadzoneSize: tc.inSize,
}
rule, err := makeRuleTargetAxis(config, t.devs)
t.Nil(err)
t.Equal(tc.outStart, rule.DeadzoneStart)
t.Equal(tc.outEnd, rule.DeadzoneEnd)
})
}
t.Run("Deadzone center/size invalid center", func() {
config := RuleTargetConfigAxis{
Device: "test",
Axis: "x",
DeadzoneCenter: 20000,
DeadzoneSize: 500,
}
_, err := makeRuleTargetAxis(config, t.devs)
t.NotNil(err)
})
relDeadzonePercentTestCases := []struct {
inCenter int32
inSizePercent int32
outStart int32
outEnd int32
}{
{5000, 10, 4500, 5500},
{0, 10, 0, 1000},
{10000, 10, 9000, 10000},
}
for _, tc := range relDeadzonePercentTestCases {
t.Run(fmt.Sprintf("Relative percent deadzone %d +- %d%%", tc.inCenter, tc.inSizePercent), func() {
config := RuleTargetConfigAxis{
Device: "test",
Axis: "x",
DeadzoneCenter: tc.inCenter,
DeadzoneSizePercent: tc.inSizePercent,
}
rule, err := makeRuleTargetAxis(config, t.devs)
t.Nil(err)
t.Equal(tc.outStart, rule.DeadzoneStart)
t.Equal(tc.outEnd, rule.DeadzoneEnd)
})
}
t.Run("Deadzone center/percent invalid center", func() {
config := RuleTargetConfigAxis{
Device: "test",
Axis: "x",
DeadzoneCenter: 20000,
DeadzoneSizePercent: 10,
}
_, err := makeRuleTargetAxis(config, t.devs)
t.NotNil(err)
})
}
func (t *MakeRuleTargetsTests) TestMakeRuleTargetRelaxis() {
config := RuleTargetConfigRelaxis{Device: "test"}
t.Run("Standard keycode", func() {
config.Axis = "REL_WHEEL"
rule, err := makeRuleTargetRelaxis(config, t.devs)
t.Nil(err)
t.EqualValues(evdev.REL_WHEEL, rule.Axis)
})
t.Run("Hex keycode", func() {
config.Axis = "0x00"
rule, err := makeRuleTargetRelaxis(config, t.devs)
t.Nil(err)
t.EqualValues(evdev.REL_X, rule.Axis)
})
t.Run("Un-prefixed keycode", func() {
config.Axis = "wheel"
rule, err := makeRuleTargetRelaxis(config, t.devs)
t.Nil(err)
t.EqualValues(evdev.REL_WHEEL, rule.Axis)
})
t.Run("Invalid keycode", func() {
config.Axis = "foo"
_, err := makeRuleTargetRelaxis(config, t.devs)
t.NotNil(err)
})
t.Run("Incorrect axis type", func() {
config.Axis = "ABS_X"
_, err := makeRuleTargetRelaxis(config, t.devs)
t.NotNil(err)
})
}

View file

@ -0,0 +1,237 @@
package configparser
import (
"fmt"
"strings"
"git.annabunches.net/annabunches/joyful/internal/logger"
"git.annabunches.net/annabunches/joyful/internal/mappingrules"
"github.com/holoplot/go-evdev"
)
// TODO: At some point it would *very likely* make sense to map each rule to all of the physical devices that can
// trigger it, and return that instead. Something like a map[Device][]mappingrule.MappingRule.
// This would speed up rule matching by only checking relevant rules for a given input event.
// We could take this further and make it a map[<struct of *inputdevice, type, and code>][]rule
// For very large rule-bases this may be helpful for staying performant.
func (parser *ConfigParser) InitRules(pInputDevs map[string]*evdev.InputDevice, vInputDevs map[string]*evdev.InputDevice) []mappingrules.MappingRule {
rules := make([]mappingrules.MappingRule, 0)
modes := parser.GetModes()
// Golang can't inspect the concrete map type to determine interface conformance,
// so we handle that here.
pDevs := make(map[string]Device)
for name, dev := range pInputDevs {
pDevs[name] = dev
}
vDevs := make(map[string]Device)
for name, dev := range vInputDevs {
vDevs[name] = dev
}
for _, ruleConfig := range parser.config.Rules {
var newRule mappingrules.MappingRule
var err error
if ok := validateModes(ruleConfig.Modes, modes); !ok {
logger.Logf("Skipping rule '%s', mode list specifies undefined mode.", ruleConfig.Name)
continue
}
base := mappingrules.NewMappingRuleBase(ruleConfig.Name, ruleConfig.Modes)
switch strings.ToLower(ruleConfig.Type) {
case RuleTypeButton:
newRule, err = makeMappingRuleButton(ruleConfig.Config.(RuleConfigButton), pDevs, vDevs, base)
case RuleTypeButtonCombo:
newRule, err = makeMappingRuleCombo(ruleConfig.Config.(RuleConfigButtonCombo), pDevs, vDevs, base)
case RuleTypeButtonLatched:
newRule, err = makeMappingRuleLatched(ruleConfig.Config.(RuleConfigButtonLatched), pDevs, vDevs, base)
case RuleTypeAxis:
newRule, err = makeMappingRuleAxis(ruleConfig.Config.(RuleConfigAxis), pDevs, vDevs, base)
case RuleTypeAxisCombined:
newRule, err = makeMappingRuleAxisCombined(ruleConfig.Config.(RuleConfigAxisCombined), pDevs, vDevs, base)
case RuleTypeAxisToButton:
newRule, err = makeMappingRuleAxisToButton(ruleConfig.Config.(RuleConfigAxisToButton), pDevs, vDevs, base)
case RuleTypeAxisToRelaxis:
newRule, err = makeMappingRuleAxisToRelaxis(ruleConfig.Config.(RuleConfigAxisToRelaxis), pDevs, vDevs, base)
case RuleTypeModeSelect:
newRule, err = makeMappingRuleModeSelect(ruleConfig.Config.(RuleConfigModeSelect), pDevs, modes, base)
default:
err = fmt.Errorf("bad rule type '%s' for rule '%s'", ruleConfig.Type, ruleConfig.Name)
}
if err != nil {
logger.LogErrorf(err, "Failed to build rule '%s'", ruleConfig.Name)
continue
}
rules = append(rules, newRule)
}
return rules
}
// TODO: how much of these functions could we fold into the unmarshaling logic itself? The main problem
// is that we don't have access to the device maps in those functions... could we set device names
// as stand-ins and do a post-processing pass that *just* handles device linking and possibly mode
// checking?
//
// In other words - can we unmarshal the config directly into our target structs and remove most of
// this library?
func makeMappingRuleButton(ruleConfig RuleConfigButton,
pDevs map[string]Device,
vDevs map[string]Device,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButton, error) {
input, err := makeRuleTargetButton(ruleConfig.Input, pDevs)
if err != nil {
return nil, err
}
output, err := makeRuleTargetButton(ruleConfig.Output, vDevs)
if err != nil {
return nil, err
}
return mappingrules.NewMappingRuleButton(base, input, output), nil
}
func makeMappingRuleCombo(ruleConfig RuleConfigButtonCombo,
pDevs map[string]Device,
vDevs map[string]Device,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButtonCombo, error) {
inputs := make([]*mappingrules.RuleTargetButton, 0)
for _, inputConfig := range ruleConfig.Inputs {
input, err := makeRuleTargetButton(inputConfig, pDevs)
if err != nil {
return nil, err
}
inputs = append(inputs, input)
}
output, err := makeRuleTargetButton(ruleConfig.Output, vDevs)
if err != nil {
return nil, err
}
return mappingrules.NewMappingRuleButtonCombo(base, inputs, output), nil
}
func makeMappingRuleLatched(ruleConfig RuleConfigButtonLatched,
pDevs map[string]Device,
vDevs map[string]Device,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButtonLatched, error) {
input, err := makeRuleTargetButton(ruleConfig.Input, pDevs)
if err != nil {
return nil, err
}
output, err := makeRuleTargetButton(ruleConfig.Output, vDevs)
if err != nil {
return nil, err
}
return mappingrules.NewMappingRuleButtonLatched(base, input, output), nil
}
func makeMappingRuleAxis(ruleConfig RuleConfigAxis,
pDevs map[string]Device,
vDevs map[string]Device,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxis, error) {
input, err := makeRuleTargetAxis(ruleConfig.Input, pDevs)
if err != nil {
return nil, err
}
output, err := makeRuleTargetAxis(ruleConfig.Output, vDevs)
if err != nil {
return nil, err
}
return mappingrules.NewMappingRuleAxis(base, input, output), nil
}
func makeMappingRuleAxisCombined(ruleConfig RuleConfigAxisCombined,
pDevs map[string]Device,
vDevs map[string]Device,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxisCombined, error) {
inputLower, err := makeRuleTargetAxis(ruleConfig.InputLower, pDevs)
if err != nil {
return nil, err
}
inputUpper, err := makeRuleTargetAxis(ruleConfig.InputUpper, pDevs)
if err != nil {
return nil, err
}
output, err := makeRuleTargetAxis(ruleConfig.Output, vDevs)
if err != nil {
return nil, err
}
return mappingrules.NewMappingRuleAxisCombined(base, inputLower, inputUpper, output), nil
}
func makeMappingRuleAxisToButton(ruleConfig RuleConfigAxisToButton,
pDevs map[string]Device,
vDevs map[string]Device,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxisToButton, error) {
input, err := makeRuleTargetAxis(ruleConfig.Input, pDevs)
if err != nil {
return nil, err
}
output, err := makeRuleTargetButton(ruleConfig.Output, vDevs)
if err != nil {
return nil, err
}
return mappingrules.NewMappingRuleAxisToButton(base, input, output, ruleConfig.RepeatRateMin, ruleConfig.RepeatRateMax), nil
}
func makeMappingRuleAxisToRelaxis(ruleConfig RuleConfigAxisToRelaxis,
pDevs map[string]Device,
vDevs map[string]Device,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxisToRelaxis, error) {
input, err := makeRuleTargetAxis(ruleConfig.Input, pDevs)
if err != nil {
return nil, err
}
output, err := makeRuleTargetRelaxis(ruleConfig.Output, vDevs)
if err != nil {
return nil, err
}
return mappingrules.NewMappingRuleAxisToRelaxis(base,
input, output,
ruleConfig.RepeatRateMin,
ruleConfig.RepeatRateMax,
ruleConfig.Increment), nil
}
func makeMappingRuleModeSelect(ruleConfig RuleConfigModeSelect,
pDevs map[string]Device,
modes []string,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleModeSelect, error) {
input, err := makeRuleTargetButton(ruleConfig.Input, pDevs)
if err != nil {
return nil, err
}
output, err := makeRuleTargetModeSelect(ruleConfig.Output, modes)
if err != nil {
return nil, err
}
return mappingrules.NewMappingRuleModeSelect(base, input, output), nil
}

View file

@ -0,0 +1,19 @@
package configparser
import "slices"
// validateModes checks the provided modes against a larger subset of modes (usually all defined ones)
// and returns false if any of the modes are not defined.
func validateModes(modes []string, allModes []string) bool {
if len(modes) == 0 {
return true
}
for _, mode := range modes {
if !slices.Contains(allModes, mode) {
return false
}
}
return true
}

View file

@ -0,0 +1,225 @@
// These types comprise the YAML schema for configuring Joyful.
// The config files will be combined and then unmarshalled into this
package configparser
import (
"fmt"
)
type Config struct {
Devices []DeviceConfig
Modes []string
Rules []RuleConfig
}
// These top-level structs use custom unmarshaling to unpack each available sub-type
type DeviceConfig struct {
Type string
Config interface{}
}
type RuleConfig struct {
Type string
Name string
Modes []string
Config interface{}
}
type DeviceConfigPhysical struct {
Name string
DeviceName string `yaml:"device_name,omitempty"`
DevicePath string `yaml:"device_path,omitempty"`
Lock bool
}
// TODO: configure custom unmarshaling so we can overload Buttons, Axes, and RelativeAxes...
type DeviceConfigVirtual struct {
Name string
Preset string
NumButtons int `yaml:"num_buttons,omitempty"`
NumAxes int `yaml:"num_axes,omitempty"`
NumRelativeAxes int `yaml:"num_rel_axes"`
Buttons []string
Axes []string
RelativeAxes []string `yaml:"rel_axes,omitempty"`
}
type RuleConfigButton struct {
Input RuleTargetConfigButton
Output RuleTargetConfigButton
}
type RuleConfigButtonCombo struct {
Inputs []RuleTargetConfigButton
Output RuleTargetConfigButton
}
type RuleConfigButtonLatched struct {
Input RuleTargetConfigButton
Output RuleTargetConfigButton
}
type RuleConfigAxis struct {
Input RuleTargetConfigAxis
Output RuleTargetConfigAxis
}
type RuleConfigAxisCombined struct {
InputLower RuleTargetConfigAxis `yaml:"input_lower,omitempty"`
InputUpper RuleTargetConfigAxis `yaml:"input_upper,omitempty"`
Output RuleTargetConfigAxis
}
type RuleConfigAxisToButton struct {
RepeatRateMin int `yaml:"repeat_rate_min,omitempty"`
RepeatRateMax int `yaml:"repeat_rate_max,omitempty"`
Input RuleTargetConfigAxis
Output RuleTargetConfigButton
}
type RuleConfigAxisToRelaxis struct {
RepeatRateMin int `yaml:"repeat_rate_min"`
RepeatRateMax int `yaml:"repeat_rate_max"`
Increment int
Input RuleTargetConfigAxis
Output RuleTargetConfigRelaxis
}
type RuleConfigModeSelect struct {
Input RuleTargetConfigButton
Output RuleTargetConfigModeSelect
}
type RuleTargetConfigButton struct {
Device string
Button string
Inverted bool
}
type RuleTargetConfigAxis struct {
Device string
Axis string
DeadzoneCenter int32 `yaml:"deadzone_center,omitempty"`
DeadzoneSize int32 `yaml:"deadzone_size,omitempty"`
DeadzoneSizePercent int32 `yaml:"deadzone_size_percent,omitempty"`
DeadzoneStart int32 `yaml:"deadzone_start,omitempty"`
DeadzoneEnd int32 `yaml:"deadzone_end,omitempty"`
Inverted bool
}
type RuleTargetConfigRelaxis struct {
Device string
Axis string
}
type RuleTargetConfigModeSelect struct {
Modes []string
}
func (dc *DeviceConfig) UnmarshalYAML(unmarshal func(data interface{}) error) error {
metaConfig := &struct {
Type string
}{}
err := unmarshal(metaConfig)
if err != nil {
return err
}
dc.Type = metaConfig.Type
err = nil
switch metaConfig.Type {
case DeviceTypePhysical:
config := DeviceConfigPhysical{}
err = unmarshal(&config)
dc.Config = config
case DeviceTypeVirtual:
config := DeviceConfigVirtual{}
err = unmarshal(&config)
dc.Config = config
default:
err = fmt.Errorf("invalid device type '%s'", dc.Type)
}
return err
}
func (dc *RuleConfig) UnmarshalYAML(unmarshal func(data interface{}) error) error {
metaConfig := &struct {
Type string
Name string
Modes []string
}{}
err := unmarshal(metaConfig)
if err != nil {
return err
}
dc.Type = metaConfig.Type
dc.Name = metaConfig.Name
dc.Modes = metaConfig.Modes
switch dc.Type {
case RuleTypeButton:
config := RuleConfigButton{}
err = unmarshal(&config)
dc.Config = config
case RuleTypeButtonCombo:
config := RuleConfigButtonCombo{}
err = unmarshal(&config)
dc.Config = config
case RuleTypeButtonLatched:
config := RuleConfigButtonLatched{}
err = unmarshal(&config)
dc.Config = config
case RuleTypeAxis:
config := RuleConfigAxis{}
err = unmarshal(&config)
dc.Config = config
case RuleTypeAxisCombined:
config := RuleConfigAxisCombined{}
err = unmarshal(&config)
dc.Config = config
case RuleTypeAxisToButton:
config := RuleConfigAxisToButton{}
err = unmarshal(&config)
dc.Config = config
case RuleTypeAxisToRelaxis:
config := RuleConfigAxisToRelaxis{}
err = unmarshal(&config)
dc.Config = config
case RuleTypeModeSelect:
config := RuleConfigModeSelect{}
err = unmarshal(&config)
dc.Config = config
default:
err = fmt.Errorf("invalid rule type '%s'", dc.Type)
}
return err
}
// 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 *DeviceConfigPhysical) UnmarshalYAML(unmarshal func(data interface{}) error) error {
var raw struct {
Name string
DeviceName string `yaml:"device_name"`
DevicePath string `yaml:"device_path"`
Lock bool `yaml:"lock,omitempty"`
}
// Set non-standard defaults
raw.Lock = true
err := unmarshal(&raw)
if err != nil {
return err
}
*dc = DeviceConfigPhysical{
Name: raw.Name,
DeviceName: raw.DeviceName,
DevicePath: raw.DevicePath,
Lock: raw.Lock,
}
return nil
}

View file

@ -0,0 +1,388 @@
package configparser
import (
"github.com/holoplot/go-evdev"
)
const (
DeviceTypePhysical = "physical"
DeviceTypeVirtual = "virtual"
DevicePresetKeyboard = "keyboard"
DevicePresetGamepad = "gamepad"
DevicePresetJoystick = "joystick"
DevicePresetMouse = "mouse"
RuleTypeButton = "button"
RuleTypeButtonCombo = "button-combo"
RuleTypeButtonLatched = "button-latched"
RuleTypeAxis = "axis"
RuleTypeAxisCombined = "axis-combined"
RuleTypeAxisToButton = "axis-to-button"
RuleTypeAxisToRelaxis = "axis-to-relaxis"
RuleTypeModeSelect = "mode-select"
CodePrefixButton = "BTN"
CodePrefixKey = "KEY"
CodePrefixAxis = "ABS"
CodePrefixRelaxis = "REL"
VirtualDeviceMaxButtons = 74
)
var (
ButtonFromIndex = []evdev.EvCode{
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),
}
)
// 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,
},
}
)