Merge branch 'main' into device-config

This commit is contained in:
Anna Rose Wiggins 2025-07-17 13:03:13 -04:00
commit 623cd12407
7 changed files with 198 additions and 83 deletions

62
internal/config/codes.go Normal file
View file

@ -0,0 +1,62 @@
package config
import (
"fmt"
"strconv"
"strings"
"github.com/holoplot/go-evdev"
)
func parseCode(code, prefix string) (evdev.EvCode, error) {
code = strings.ToUpper(code)
var codeLookup map[string]evdev.EvCode
switch prefix {
case CodePrefixButton:
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,63 @@
package config
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) TestMakeAxes() {
t.Run("8 axes", func() {
axes := makeAxes(8)
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)
t.Equal(8, len(axes))
})
t.Run("3 axes", func() {
axes := makeAxes(3)
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))
})
}
func (t *DevicesConfigTests) TestMakeButtons() {
t.Run("Maximum buttons", func() {
buttons := makeButtons(VirtualDeviceMaxButtons)
t.Equal(VirtualDeviceMaxButtons, len(buttons))
})
t.Run("Truncated buttons", func() {
buttons := makeButtons(VirtualDeviceMaxButtons + 1)
t.Equal(VirtualDeviceMaxButtons, len(buttons))
})
t.Run("16 buttons", func() {
buttons := makeButtons(16)
t.Equal(16, len(buttons))
t.Contains(buttons, evdev.EvCode(evdev.BTN_DEAD))
t.NotContains(buttons, evdev.EvCode(evdev.BTN_TRIGGER_HAPPY))
})
}

View file

@ -3,8 +3,6 @@ package config
import (
"errors"
"fmt"
"strconv"
"strings"
"git.annabunches.net/annabunches/joyful/internal/mappingrules"
"github.com/holoplot/go-evdev"
@ -16,39 +14,9 @@ func makeRuleTargetButton(targetConfig RuleTargetConfig, devs map[string]*evdev.
return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device)
}
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, 0)
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)
}
eventCode, err := parseCode(targetConfig.Button, "BTN")
if err != nil {
return nil, err
}
return mappingrules.NewRuleTargetButton(
@ -69,27 +37,9 @@ func makeRuleTargetAxis(targetConfig RuleTargetConfig, devs map[string]*evdev.In
return nil, errors.New("deadzone_end must be greater than deadzone_start")
}
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)
}
eventCode, err := parseCode(targetConfig.Axis, "ABS")
if err != nil {
return nil, err
}
return mappingrules.NewRuleTargetAxis(
@ -108,28 +58,11 @@ func makeRuleTargetRelaxis(targetConfig RuleTargetConfig, devs map[string]*evdev
return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device)
}
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.RELFromString["REL_"+axisConfig]
if !ok {
return nil, fmt.Errorf("invalid axis code '%s'", axisConfig)
}
eventCode, err := parseCode(targetConfig.Axis, "REL")
if err != nil {
return nil, err
}
return mappingrules.NewRuleTargetRelaxis(
targetConfig.Device,
device,

View file

@ -12,6 +12,10 @@ type MakeRuleTargetsTests struct {
devs map[string]*evdev.InputDevice
}
func TestRunnerMakeRuleTargets(t *testing.T) {
suite.Run(t, new(MakeRuleTargetsTests))
}
func (t *MakeRuleTargetsTests) SetupSuite() {
t.devs = map[string]*evdev.InputDevice{
"test": {},
@ -141,7 +145,3 @@ func (t *MakeRuleTargetsTests) TestMakeRuleTargetRelaxis() {
t.NotNil(err)
})
}
func TestRunnerMakeRuleTargets(t *testing.T) {
suite.Run(t, new(MakeRuleTargetsTests))
}

View file

@ -16,6 +16,10 @@ const (
RuleTypeAxisToButton = "axis-to-button"
RuleTypeAxisToRelaxis = "axis-to-relaxis"
CodePrefixButton = "BTN"
CodePrefixAxis = "ABS"
CodePrefixRelaxis = "REL"
VirtualDeviceMaxButtons = 74
)

View file

@ -0,0 +1,53 @@
package mappingrules
import (
"testing"
"github.com/stretchr/testify/suite"
)
type MappingRuleBaseTests struct {
suite.Suite
}
func TestRunnerMappingRuleBaseTests(t *testing.T) {
suite.Run(t, new(MappingRuleBaseTests))
}
func (t *MappingRuleBaseTests) TestNewMappingRuleBase() {
t.Run("No Modes", func() {
base := NewMappingRuleBase("foo", []string{})
t.Equal("foo", base.Name)
t.EqualValues([]string{"*"}, base.Modes)
})
t.Run("Has Modes", func() {
base := NewMappingRuleBase("foo", []string{"bar", "baz"})
t.Equal("foo", base.Name)
t.Contains(base.Modes, "bar")
t.Contains(base.Modes, "baz")
t.NotContains(base.Modes, "*")
})
}
func (t *MappingRuleBaseTests) TestModeCheck() {
t.Run("* works on all modes", func() {
base := NewMappingRuleBase("", []string{})
mode := "bar"
t.True(base.modeCheck(&mode))
mode = "baz"
t.True(base.modeCheck(&mode))
})
t.Run("single mode only works in that mode", func() {
base := NewMappingRuleBase("", []string{"bar"})
mode := "bar"
t.True(base.modeCheck(&mode))
mode = "baz"
t.False(base.modeCheck(&mode))
})
t.Run("multiple modes work in each mode", func() {
})
}

View file

@ -23,9 +23,9 @@ Joyful is ideal for Linux gamers who enjoy space and flight sims and miss the fe
* Macros - have a single input produce a sequence of button presses with configurable pauses.
* Sequence combos - Button1, Button2, Button3 -> VirtualButtonA
* More ways to specify keycodes
* Output keyboard button presses
* Input and output from gamepad-like devices.
* Explicit input and output from gamepad-like devices.
* HIDRAW support for more button options.
## Configuration