diff --git a/go.mod b/go.mod index f7c13dd..2388397 100644 --- a/go.mod +++ b/go.mod @@ -5,4 +5,11 @@ go 1.24.4 require ( github.com/goccy/go-yaml v1.18.0 github.com/holoplot/go-evdev v0.0.0-20240306072622-217e18f17db1 + github.com/stretchr/testify v1.10.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 69c3bf7..539dcc7 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,14 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/holoplot/go-evdev v0.0.0-20240306072622-217e18f17db1 h1:92OsBIf5KB1Tatx+uUGOhah73jyNUrt7DmfDRXXJ5Xo= github.com/holoplot/go-evdev v0.0.0-20240306072622-217e18f17db1/go.mod h1:iHAf8OIncO2gcQ8XOjS7CMJ2aPbX2Bs0wl5pZyanEqk= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/mappingrules/interfaces.go b/internal/mappingrules/interfaces.go new file mode 100644 index 0000000..ef54fbf --- /dev/null +++ b/internal/mappingrules/interfaces.go @@ -0,0 +1,28 @@ +package mappingrules + +import "github.com/holoplot/go-evdev" + +type MappingRule interface { + MatchEvent(*evdev.InputDevice, *evdev.InputEvent, *string) *evdev.InputEvent + OutputName() string +} + +// RuleTargets represent either a device input to match on, or an output to produce. +// Some RuleTarget types may work via side effects, such as RuleTargetModeSelect. +type RuleTarget interface { + // NormalizeValue takes the raw input value and possibly modifies it based on the Target settings. + // (e.g., inverting the value if Inverted == true) + NormalizeValue(int32) int32 + + // CreateEvent typically takes the (probably normalized) value and returns an event that can be emitted + // on a virtual device. + // + // For RuleTargetModeSelect, this method modifies the active mode and returns nil. + // + // TODO: should we normalize inside this function to simplify the interface? + CreateEvent(int32, *string) *evdev.InputEvent + + GetCode() evdev.EvCode + GetDeviceName() string + GetDevice() *evdev.InputDevice +} diff --git a/internal/mappingrules/matching_test.go b/internal/mappingrules/matching_test.go index e7e6e3b..00a6da5 100644 --- a/internal/mappingrules/matching_test.go +++ b/internal/mappingrules/matching_test.go @@ -4,63 +4,91 @@ import ( "testing" "github.com/holoplot/go-evdev" + "github.com/stretchr/testify/suite" ) -func TestSimpleRuleMatchEvent(t *testing.T) { - inputDevice := &evdev.InputDevice{} - wrongInputDevice := &evdev.InputDevice{} - outputDevice := &evdev.InputDevice{} +type SimpleMappingRuleTests struct { + suite.Suite + inputDevice *evdev.InputDevice + wrongInputDevice *evdev.InputDevice + outputDevice *evdev.InputDevice + mode *string + sampleRule *SimpleMappingRule + invertedRule *SimpleMappingRule +} - rule := &SimpleMappingRule{ - MappingRuleBase: MappingRuleBase{ - Output: &RuleTargetButton{ - RuleTargetBase{ - DeviceName: "test_output", - Device: outputDevice, - Code: evdev.BTN_TRIGGER, - }, - }, - Modes: []string{"*"}, - }, - Input: &RuleTargetButton{ - RuleTargetBase{ - DeviceName: "test_input", - Device: inputDevice, - Code: evdev.BTN_TRIGGER, - }, - }, - } +func (t *SimpleMappingRuleTests) SetupTest() { + t.inputDevice = &evdev.InputDevice{} + t.wrongInputDevice = &evdev.InputDevice{} + t.outputDevice = &evdev.InputDevice{} mode := "*" + t.mode = &mode + // TODO: implement a constructor function... + t.sampleRule = &SimpleMappingRule{ + MappingRuleBase: MappingRuleBase{ + Output: NewRuleTargetButton("", t.outputDevice, evdev.BTN_TRIGGER, false), + Modes: []string{"*"}, + }, + Input: NewRuleTargetButton("", t.inputDevice, evdev.BTN_TRIGGER, false), + } + + t.invertedRule = &SimpleMappingRule{ + MappingRuleBase: MappingRuleBase{ + Output: NewRuleTargetButton("", t.outputDevice, evdev.BTN_TRIGGER, false), + Modes: []string{"*"}, + }, + Input: NewRuleTargetButton("", t.inputDevice, evdev.BTN_TRIGGER, true), + } +} + +func (t *SimpleMappingRuleTests) TestMatchEvent() { // A matching input event should produce an output event - event := rule.MatchEvent(inputDevice, &evdev.InputEvent{Code: evdev.BTN_TRIGGER, Value: 1}, &mode) - outputEvent := &evdev.InputEvent{ + correctOutput := &evdev.InputEvent{ Type: evdev.EV_KEY, Code: evdev.BTN_TRIGGER, Value: 1, } - if event == nil || *event != *outputEvent { - t.Errorf("Expected event to match %v, but got %v", outputEvent, event) - } - - // if event.Type != outputEvent.Type || - // event.Code != outputEvent.Code || - // event.Value != outputEvent.Value { - // t.Errorf("Expected event to match %v, but got %v", outputEvent, event) - // } + event := t.sampleRule.MatchEvent( + t.inputDevice, + &evdev.InputEvent{Code: evdev.BTN_TRIGGER, Value: 1}, t.mode) + t.EqualValues(correctOutput, event) // An input event from the wrong device should produce a nil event - event = rule.MatchEvent(wrongInputDevice, &evdev.InputEvent{Code: evdev.BTN_TRIGGER, Value: 1}, &mode) - if event != nil { - t.Errorf("Expected event not to match, but got non-nil event %v", event) - } + event = t.sampleRule.MatchEvent( + t.wrongInputDevice, + &evdev.InputEvent{Code: evdev.BTN_TRIGGER, Value: 1}, t.mode) + t.Nil(event) - // An input event from the wrong device should produce a nil event - event = rule.MatchEvent(wrongInputDevice, &evdev.InputEvent{Code: evdev.BTN_TRIGGER, Value: 1}, &mode) - if event != nil { - t.Errorf("Expected event not to match, but got non-nil event %v", event) - } - - // TODO: test inversion, and everything else... + // An input event from the wrong button should produce a nil event + event = t.sampleRule.MatchEvent( + t.inputDevice, + &evdev.InputEvent{Code: evdev.BTN_TOP, Value: 1}, t.mode) + t.Nil(event) +} + +func (t *SimpleMappingRuleTests) TestMatchEventInverted() { + // A matching input event should produce an output event + correctOutput := &evdev.InputEvent{ + Type: evdev.EV_KEY, + Code: evdev.BTN_TRIGGER, + } + + // Should get the opposite value out that we send in + correctOutput.Value = 0 + event := t.invertedRule.MatchEvent( + t.inputDevice, + &evdev.InputEvent{Code: evdev.BTN_TRIGGER, Value: 1}, t.mode) + t.EqualValues(correctOutput, event) + + correctOutput.Value = 1 + event = t.invertedRule.MatchEvent( + t.inputDevice, + &evdev.InputEvent{Code: evdev.BTN_TRIGGER, Value: 0}, t.mode) + t.EqualValues(correctOutput, event) +} + +func TestRunnerMatching(t *testing.T) { + suite.Run(t, new(SimpleMappingRuleTests)) } diff --git a/internal/mappingrules/rule_target_axis.go b/internal/mappingrules/rule_target_axis.go new file mode 100644 index 0000000..938ad41 --- /dev/null +++ b/internal/mappingrules/rule_target_axis.go @@ -0,0 +1,61 @@ +package mappingrules + +import ( + "github.com/holoplot/go-evdev" +) + +type RuleTargetAxis struct { + RuleTargetBase + AxisStart int32 + AxisEnd int32 + Sensitivity float64 +} + +func NewRuleTargetAxis(device_name string, + device *evdev.InputDevice, + code evdev.EvCode, + inverted bool, + axis_start int32, + axis_end int32, + sensitivity float64) *RuleTargetAxis { + + return &RuleTargetAxis{ + RuleTargetBase: NewRuleTargetBase(device_name, device, code, inverted), + AxisStart: axis_start, + AxisEnd: axis_end, + Sensitivity: sensitivity, + } +} + +func (target *RuleTargetAxis) NormalizeValue(value int32) int32 { + if !target.Inverted { + return value + } + + axisRange := target.AxisEnd - target.AxisStart + axisMid := target.AxisEnd - axisRange/2 + delta := value - axisMid + if delta < 0 { + delta = -delta + } + + if value < axisMid { + return axisMid + delta + } else if value > axisMid { + return axisMid - delta + } + + // If we reach here, we're either exactly at the midpoint or something + // strange has happened. Either way, just return the value. + return value +} + +func (target *RuleTargetAxis) CreateEvent(value int32, mode *string) *evdev.InputEvent { + // TODO: we can use the axis begin/end to decide whether to emit the event + // TODO: oh no we need center deadzones actually... + return &evdev.InputEvent{ + Type: evdev.EV_ABS, + Code: target.Code, + Value: value, + } +} diff --git a/internal/mappingrules/rule_target_base.go b/internal/mappingrules/rule_target_base.go new file mode 100644 index 0000000..7d03130 --- /dev/null +++ b/internal/mappingrules/rule_target_base.go @@ -0,0 +1,35 @@ +package mappingrules + +import "github.com/holoplot/go-evdev" + +type RuleTargetBase struct { + DeviceName string + Device *evdev.InputDevice + Code evdev.EvCode + Inverted bool +} + +func NewRuleTargetBase(device_name string, + device *evdev.InputDevice, + code evdev.EvCode, + inverted bool) RuleTargetBase { + + return RuleTargetBase{ + DeviceName: device_name, + Device: device, + Code: code, + Inverted: inverted, + } +} + +func (target *RuleTargetBase) GetCode() evdev.EvCode { + return target.Code +} + +func (target *RuleTargetBase) GetDeviceName() string { + return target.DeviceName +} + +func (target *RuleTargetBase) GetDevice() *evdev.InputDevice { + return target.Device +} diff --git a/internal/mappingrules/rule_target_button.go b/internal/mappingrules/rule_target_button.go new file mode 100644 index 0000000..b34b498 --- /dev/null +++ b/internal/mappingrules/rule_target_button.go @@ -0,0 +1,31 @@ +package mappingrules + +import "github.com/holoplot/go-evdev" + +type RuleTargetButton struct { + RuleTargetBase +} + +func NewRuleTargetButton(device_name string, device *evdev.InputDevice, code evdev.EvCode, inverted bool) *RuleTargetButton { + return &RuleTargetButton{ + RuleTargetBase: NewRuleTargetBase(device_name, device, code, inverted), + } +} + +func (target *RuleTargetButton) NormalizeValue(value int32) int32 { + if target.Inverted { + if value == 0 { + return 1 + } + return 0 + } + return value +} + +func (target *RuleTargetButton) CreateEvent(value int32, mode *string) *evdev.InputEvent { + return &evdev.InputEvent{ + Type: evdev.EV_KEY, + Code: target.Code, + Value: value, + } +} diff --git a/internal/mappingrules/rule_target_modeselect.go b/internal/mappingrules/rule_target_modeselect.go new file mode 100644 index 0000000..7ed4332 --- /dev/null +++ b/internal/mappingrules/rule_target_modeselect.go @@ -0,0 +1,41 @@ +package mappingrules + +import ( + "slices" + + "git.annabunches.net/annabunches/joyful/internal/logger" + "github.com/holoplot/go-evdev" +) + +type RuleTargetModeSelect struct { + RuleTargetBase + ModeSelect []string +} + +func NewRuleTargetModeSelect(modes []string) *RuleTargetModeSelect { + return &RuleTargetModeSelect{ + RuleTargetBase: NewRuleTargetBase("", nil, 0, false), + ModeSelect: modes, + } +} + +// RuleTargetModeSelect doesn't make sense as an input type +func (target *RuleTargetModeSelect) NormalizeValue(value int32) int32 { + return -1 +} + +func (target *RuleTargetModeSelect) CreateEvent(value int32, mode *string) *evdev.InputEvent { + if value == 0 { + return nil + } + + index := 0 + if currentMode := slices.Index(target.ModeSelect, *mode); currentMode != -1 { + // find the next mode + index = (currentMode + 1) % len(target.ModeSelect) + } + + *mode = target.ModeSelect[index] + logger.Logf("Mode changed to '%s'", *mode) + return nil +} diff --git a/internal/mappingrules/targets.go b/internal/mappingrules/targets.go deleted file mode 100644 index bc2129c..0000000 --- a/internal/mappingrules/targets.go +++ /dev/null @@ -1,90 +0,0 @@ -package mappingrules - -import ( - "slices" - - "git.annabunches.net/annabunches/joyful/internal/logger" - "github.com/holoplot/go-evdev" -) - -func (target *RuleTargetBase) GetCode() evdev.EvCode { - return target.Code -} - -func (target *RuleTargetBase) GetDeviceName() string { - return target.DeviceName -} - -func (target *RuleTargetBase) GetDevice() *evdev.InputDevice { - return target.Device -} - -func (target *RuleTargetButton) NormalizeValue(value int32) int32 { - if target.Inverted { - if value == 0 { - return 1 - } - return 0 - } - return value -} - -func (target *RuleTargetButton) CreateEvent(value int32, mode *string) *evdev.InputEvent { - return &evdev.InputEvent{ - Type: evdev.EV_KEY, - Code: target.Code, - Value: value, - } -} - -func (target *RuleTargetAxis) NormalizeValue(value int32) int32 { - if !target.Inverted { - return value - } - - axisRange := target.AxisEnd - target.AxisStart - axisMid := target.AxisEnd - axisRange/2 - delta := value - axisMid - if delta < 0 { - delta = -delta - } - - if value < axisMid { - return axisMid + delta - } else if value > axisMid { - return axisMid - delta - } - - // If we reach here, we're either exactly at the midpoint or something - // strange has happened. Either way, just return the value. - return value -} - -func (target *RuleTargetAxis) CreateEvent(value int32, mode *string) *evdev.InputEvent { - return &evdev.InputEvent{ - Type: evdev.EV_ABS, - Code: target.Code, - Value: value, - } -} - -// RuleTargetModeSelect doesn't make sense as an input type -func (target *RuleTargetModeSelect) NormalizeValue(value int32) int32 { - return -1 -} - -func (target *RuleTargetModeSelect) CreateEvent(value int32, mode *string) *evdev.InputEvent { - if value == 0 { - return nil - } - - index := 0 - if currentMode := slices.Index(target.ModeSelect, *mode); currentMode != -1 { - // find the next mode - index = (currentMode + 1) % len(target.ModeSelect) - } - - *mode = target.ModeSelect[index] - logger.Logf("Mode changed to '%s'", *mode) - return nil -} diff --git a/internal/mappingrules/types.go b/internal/mappingrules/types.go index e7aa72f..35b1c77 100644 --- a/internal/mappingrules/types.go +++ b/internal/mappingrules/types.go @@ -2,15 +2,8 @@ package mappingrules import ( "time" - - "github.com/holoplot/go-evdev" ) -type MappingRule interface { - MatchEvent(*evdev.InputDevice, *evdev.InputEvent, *string) *evdev.InputEvent - OutputName() string -} - type MappingRuleBase struct { Name string Output RuleTarget @@ -43,46 +36,3 @@ type ProportionalAxisMappingRule struct { Output RuleTarget LastEvent time.Time } - -// RuleTargets represent either a device input to match on, or an output to produce. -// Some RuleTarget types may work via side effects, such as RuleTargetModeSelect. -type RuleTarget interface { - // NormalizeValue takes the raw input value and possibly modifies it based on the Target settings. - // (e.g., inverting the value if Inverted == true) - NormalizeValue(int32) int32 - - // CreateEvent typically takes the (probably normalized) value and returns an event that can be emitted - // on a virtual device. - // - // For RuleTargetModeSelect, this method modifies the active mode and returns nil. - // - // TODO: should we normalize inside this function to simplify the interface? - CreateEvent(int32, *string) *evdev.InputEvent - - GetCode() evdev.EvCode - GetDeviceName() string - GetDevice() *evdev.InputDevice -} - -type RuleTargetBase struct { - DeviceName string - Device *evdev.InputDevice - Code evdev.EvCode - Inverted bool -} - -type RuleTargetButton struct { - RuleTargetBase -} - -type RuleTargetAxis struct { - RuleTargetBase - AxisStart int32 - AxisEnd int32 - Sensitivity float64 -} - -type RuleTargetModeSelect struct { - RuleTargetBase - ModeSelect []string -}