From 23422fac3ca45f17ff9191c565b10d6e0b3a691f Mon Sep 17 00:00:00 2001 From: Anna Rose Wiggins Date: Tue, 15 Jul 2025 22:17:53 -0400 Subject: [PATCH] Add support for multiple types of button/axis specification. --- docs/{examples => }/readme.md | 15 +++-- internal/config/make_rule_targets.go | 92 +++++++++++++++++++++++++--- internal/config/variables.go | 22 +++++++ 3 files changed, 115 insertions(+), 14 deletions(-) rename docs/{examples => }/readme.md (71%) diff --git a/docs/examples/readme.md b/docs/readme.md similarity index 71% rename from docs/examples/readme.md rename to docs/readme.md index a38bd89..958bf64 100644 --- a/docs/examples/readme.md +++ b/docs/readme.md @@ -15,7 +15,7 @@ Each entry in `devices` must have a couple of parameters: `virtual` devices must additionally define these parameters: -* `buttons` - a number between 0 and 80. Linux may not recognize buttons greater than 56. +* `buttons` - a number between 0 and 74. Linux may not recognize buttons greater than 56. * `axes` - a number between 0 and 8. Virtual devices can also define a `relative_axes` parameter; this must be a list of `REL_` event keycodes, and can be useful for a simulated mouse device. Some environments will only register mouse events if the device *only* supports mouse-like events, so it can be useful to isolate your `relative_axes` to their own virtual device. @@ -35,13 +35,20 @@ Configuration options for each rule type vary. See for ### Keycodes -Currently, there is only one way to specify a button or axis: using evdev's Keycodes. These look like `ABS_X` for axes and `BTN_TRIGGER` -for buttons. See for a full list of these codes, but note that Joyful's virtual devices currently only uses a subset. Specifically, the axes from `ABS_X` to `ABS_RUDDER`, and the buttons from `BTN_JOYSTICK` to `BTN_DEAD`, as well as all of the `BTN_TRIGGER_HAPPY*` codes. +Keycodes are the values that identify buttons and axes. There are several ways to configure keycodes. All of them are case-insensitive. + +Ways to specify keycodes are: + +* Using evdev's Keycodes. This is the best way to be absolutely certain about which axis you're referencing. You can specify these keycodes in two forms: + * Using the code's identifier from . e.g., `ABS_X`, `REL_WHEEL`, `BTN_TRIGGER`. + * Alternately, you can omit the `ABS_` type prefix, and Joyful will automatically add it from context. So for a button input, you can simply specify `button: trigger` instead of `BTN_TRIGGER`. +* You can use the hexadecimal value of the keycode directly, via `0x`. This can be useful if you want to force a specific numeric value that isn't represented by a Linux keycode directly. Note however that not all keycodes will work. Only the first 8 axes are available, and see for a list of valid button outputs. +* For buttons, you can specify the button number, as in `button: 3`. There are 74 buttons available, and the first button is button number `0`. As a result, valid values are 0-73. Note that buttons 12-14 and buttons 55-73 may not work in all Linux-native games. For input, you can figure out what keycodes your device is emitting by running the Linux utility `evtest`. `evtest` works well with `grep`, so if you just want to see button inputs, you can do: ``` -evtest | grep KEY_ +evtest | grep BTN_ ``` The authors of this tool recognize that this is currently a pain in the ass. Easier ways to represent keycodes (as well as outputting additional keycodes) is planned for the future. diff --git a/internal/config/make_rule_targets.go b/internal/config/make_rule_targets.go index cc6c458..b5165f8 100644 --- a/internal/config/make_rule_targets.go +++ b/internal/config/make_rule_targets.go @@ -3,6 +3,8 @@ package config import ( "errors" "fmt" + "strconv" + "strings" "git.annabunches.net/annabunches/joyful/internal/mappingrules" "github.com/holoplot/go-evdev" @@ -14,9 +16,39 @@ func makeRuleTargetButton(targetConfig RuleTargetConfig, devs map[string]*evdev. return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device) } - eventCode, ok := evdev.KEYFromString[targetConfig.Button] - if !ok { - return nil, fmt.Errorf("invalid button code '%s'", targetConfig.Button) + var eventCode evdev.EvCode + buttonConfig := strings.ToUpper(targetConfig.Button) + switch { + case strings.HasPrefix(buttonConfig, "BTN_"): + eventCode, ok = evdev.KEYFromString[buttonConfig] + if !ok { + return nil, fmt.Errorf("invalid button specification '%s'", buttonConfig) + } + + case strings.HasPrefix(buttonConfig, "0X"): + codeInt, err := strconv.ParseUint(buttonConfig[2:], 16, 32) + if err != nil { + return nil, err + } + eventCode = evdev.EvCode(codeInt) + + case !hasError(strconv.Atoi(buttonConfig)): + index, err := strconv.Atoi(buttonConfig) + if err != nil { + return nil, err + } + + if index >= len(ButtonFromIndex) { + return nil, fmt.Errorf("button index '%d' out of bounds", index) + } + + eventCode = ButtonFromIndex[index] + + default: + eventCode, ok = evdev.KEYFromString["BTN_"+buttonConfig] + if !ok { + return nil, fmt.Errorf("invalid button specification '%s'", buttonConfig) + } } return mappingrules.NewRuleTargetButton( @@ -33,9 +65,27 @@ func makeRuleTargetAxis(targetConfig RuleTargetConfig, devs map[string]*evdev.In return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device) } - eventCode, ok := evdev.ABSFromString[targetConfig.Axis] - if !ok { - return nil, fmt.Errorf("invalid button code '%s'", targetConfig.Button) + var eventCode evdev.EvCode + axisConfig := strings.ToUpper(targetConfig.Axis) + switch { + case strings.HasPrefix(axisConfig, "ABS_"): + eventCode, ok = evdev.ABSFromString[axisConfig] + if !ok { + return nil, fmt.Errorf("invalid axis code '%s'", axisConfig) + } + + case strings.HasPrefix(axisConfig, "0X"): + codeInt, err := strconv.ParseUint(axisConfig[2:], 16, 32) + if err != nil { + return nil, err + } + eventCode = evdev.EvCode(codeInt) + + default: + eventCode, ok = evdev.ABSFromString["ABS_"+axisConfig] + if !ok { + return nil, fmt.Errorf("invalid axis code '%s'", axisConfig) + } } return mappingrules.NewRuleTargetAxis( @@ -54,11 +104,28 @@ func makeRuleTargetRelaxis(targetConfig RuleTargetConfig, devs map[string]*evdev return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device) } - eventCode, ok := evdev.RELFromString[targetConfig.Axis] - if !ok { - return nil, fmt.Errorf("invalid button code '%s'", targetConfig.Button) - } + var eventCode evdev.EvCode + axisConfig := strings.ToUpper(targetConfig.Axis) + switch { + case strings.HasPrefix(axisConfig, "REL_"): + eventCode, ok = evdev.RELFromString[axisConfig] + if !ok { + return nil, fmt.Errorf("invalid axis code '%s'", axisConfig) + } + case strings.HasPrefix(axisConfig, "0X"): + codeInt, err := strconv.ParseUint(axisConfig[2:], 16, 32) + if err != nil { + return nil, err + } + eventCode = evdev.EvCode(codeInt) + + default: + eventCode, ok = evdev.ABSFromString["REL_"+axisConfig] + if !ok { + return nil, fmt.Errorf("invalid axis code '%s'", axisConfig) + } + } return mappingrules.NewRuleTargetRelaxis( targetConfig.Device, device, @@ -74,3 +141,8 @@ func makeRuleTargetModeSelect(targetConfig RuleTargetConfig, allModes []string) return mappingrules.NewRuleTargetModeSelect(targetConfig.Modes) } + +// hasError exists solely to switch on errors in case statements +func hasError(_ interface{}, err error) bool { + return err != nil +} diff --git a/internal/config/variables.go b/internal/config/variables.go index c0276c4..97390d6 100644 --- a/internal/config/variables.go +++ b/internal/config/variables.go @@ -31,6 +31,10 @@ var ( evdev.BTN_BASE4, evdev.BTN_BASE5, evdev.BTN_BASE6, + evdev.EvCode(0x12c), + evdev.EvCode(0x12d), + evdev.EvCode(0x12e), + evdev.BTN_DEAD, evdev.BTN_TRIGGER_HAPPY1, evdev.BTN_TRIGGER_HAPPY2, evdev.BTN_TRIGGER_HAPPY3, @@ -71,5 +75,23 @@ var ( 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), } )