From fbb26fd93a59f2a4893ffae4113bd1422c7c2f17 Mon Sep 17 00:00:00 2001 From: Anna Rose Wiggins Date: Wed, 16 Jul 2025 20:45:51 -0400 Subject: [PATCH 1/3] Add more tests and refactor code parsing logic. --- internal/config/codes.go | 62 ++++++++++++++++ internal/config/devices_test.go | 63 ++++++++++++++++ internal/config/make_rule_targets.go | 87 +++-------------------- internal/config/make_rule_targets_test.go | 8 +-- internal/config/variables.go | 4 ++ 5 files changed, 143 insertions(+), 81 deletions(-) create mode 100644 internal/config/codes.go create mode 100644 internal/config/devices_test.go diff --git a/internal/config/codes.go b/internal/config/codes.go new file mode 100644 index 0000000..fc819cb --- /dev/null +++ b/internal/config/codes.go @@ -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 + } +} diff --git a/internal/config/devices_test.go b/internal/config/devices_test.go new file mode 100644 index 0000000..62a6b5f --- /dev/null +++ b/internal/config/devices_test.go @@ -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)) + }) +} diff --git a/internal/config/make_rule_targets.go b/internal/config/make_rule_targets.go index d46c83c..5b38347 100644 --- a/internal/config/make_rule_targets.go +++ b/internal/config/make_rule_targets.go @@ -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, diff --git a/internal/config/make_rule_targets_test.go b/internal/config/make_rule_targets_test.go index 770a4fe..02de987 100644 --- a/internal/config/make_rule_targets_test.go +++ b/internal/config/make_rule_targets_test.go @@ -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)) -} diff --git a/internal/config/variables.go b/internal/config/variables.go index f954372..fa60e6c 100644 --- a/internal/config/variables.go +++ b/internal/config/variables.go @@ -16,6 +16,10 @@ const ( RuleTypeAxisToButton = "axis-to-button" RuleTypeAxisToRelaxis = "axis-to-relaxis" + CodePrefixButton = "BTN" + CodePrefixAxis = "ABS" + CodePrefixRelaxis = "REL" + VirtualDeviceMaxButtons = 74 ) -- 2.47.2 From a34a90238ceb051f87219e394c9eb29a7e16fe0b Mon Sep 17 00:00:00 2001 From: Anna Rose Wiggins Date: Wed, 16 Jul 2025 21:00:23 -0400 Subject: [PATCH 2/3] Add tests for mode check. --- .../mappingrules/mapping_rule_base_test.go | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 internal/mappingrules/mapping_rule_base_test.go diff --git a/internal/mappingrules/mapping_rule_base_test.go b/internal/mappingrules/mapping_rule_base_test.go new file mode 100644 index 0000000..04de075 --- /dev/null +++ b/internal/mappingrules/mapping_rule_base_test.go @@ -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() { + + }) +} -- 2.47.2 From b2156e7dabec1def3ba4108668b33abff9687df1 Mon Sep 17 00:00:00 2001 From: Anna Rose Wiggins Date: Thu, 17 Jul 2025 12:41:10 -0400 Subject: [PATCH 3/3] Update readme. --- readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 4ff591d..cfe3fff 100644 --- a/readme.md +++ b/readme.md @@ -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 -- 2.47.2