Start rulemapping refactor to be more explicit about typing intentions.

This commit is contained in:
Anna Rose Wiggins 2025-07-06 17:22:05 -04:00
parent 08fc828b46
commit a0949e719f
10 changed files with 128 additions and 100 deletions

View file

@ -103,11 +103,11 @@ func main() {
case evdev.EV_KEY, evdev.EV_ABS: case evdev.EV_KEY, evdev.EV_ABS:
// We have a matchable event type. Check all the events // We have a matchable event type. Check all the events
for _, rule := range rules { for _, rule := range rules {
outputEvent := rule.MatchEvent(channelEvent.Device, channelEvent.Event, &mode) device, outputEvent := rule.MatchEvent(channelEvent.Device, channelEvent.Event, &mode)
if outputEvent == nil { if device == nil || outputEvent == nil {
continue continue
} }
vBuffersByName[rule.OutputName()].AddEvent(outputEvent) vBuffersByDevice[device].AddEvent(outputEvent)
} }
} }

View file

@ -23,7 +23,7 @@ func (parser *ConfigParser) BuildRules(pDevs map[string]*evdev.InputDevice, vDev
var newRule mappingrules.MappingRule var newRule mappingrules.MappingRule
var err error var err error
baseParams, err := setBaseRuleParameters(ruleConfig, vDevs, modes) baseParams, err := setBaseRuleParameters(ruleConfig, modes)
if err != nil { if err != nil {
logger.LogErrorf(err, "couldn't set output parameters, skipping rule '%s'", ruleConfig.Name) logger.LogErrorf(err, "couldn't set output parameters, skipping rule '%s'", ruleConfig.Name)
continue continue
@ -31,11 +31,11 @@ func (parser *ConfigParser) BuildRules(pDevs map[string]*evdev.InputDevice, vDev
switch strings.ToLower(ruleConfig.Type) { switch strings.ToLower(ruleConfig.Type) {
case RuleTypeSimple: case RuleTypeSimple:
newRule, err = makeSimpleRule(ruleConfig, pDevs, baseParams) newRule, err = makeMappingRuleButton(ruleConfig, pDevs, vDevs, baseParams)
case RuleTypeCombo: case RuleTypeCombo:
newRule, err = makeComboRule(ruleConfig, pDevs, baseParams) newRule, err = makeComboRule(ruleConfig, pDevs, vDevs, baseParams)
case RuleTypeLatched: case RuleTypeLatched:
newRule, err = makeLatchedRule(ruleConfig, pDevs, baseParams) newRule, err = makeLatchedRule(ruleConfig, pDevs, vDevs, baseParams)
} }
if err != nil { if err != nil {
@ -49,7 +49,7 @@ func (parser *ConfigParser) BuildRules(pDevs map[string]*evdev.InputDevice, vDev
return rules return rules
} }
func setBaseRuleParameters(ruleConfig RuleConfig, vDevs map[string]*evdev.InputDevice, modes []string) (mappingrules.MappingRuleBase, error) { func setBaseRuleParameters(ruleConfig RuleConfig, modes []string) (mappingrules.MappingRuleBase, error) {
// We perform this check here instead of in makeRuleTarget because only Output targets // We perform this check here instead of in makeRuleTarget because only Output targets
// can meaningfully have ModeSelect; this lets us avoid plumbing the modes in on every // can meaningfully have ModeSelect; this lets us avoid plumbing the modes in on every
// makeRuleTarget call. // makeRuleTarget call.
@ -60,12 +60,7 @@ func setBaseRuleParameters(ruleConfig RuleConfig, vDevs map[string]*evdev.InputD
} }
} }
output, err := makeRuleTarget(ruleConfig.Output, vDevs) err := validateModes(ruleConfig.Modes, modes)
if err != nil {
return mappingrules.MappingRuleBase{}, err
}
err = validateModes(ruleConfig.Modes, modes)
if err != nil { if err != nil {
return mappingrules.MappingRuleBase{}, err return mappingrules.MappingRuleBase{}, err
} }
@ -73,28 +68,41 @@ func setBaseRuleParameters(ruleConfig RuleConfig, vDevs map[string]*evdev.InputD
ruleModes := ensureModes(ruleConfig.Modes) ruleModes := ensureModes(ruleConfig.Modes)
return mappingrules.MappingRuleBase{ return mappingrules.MappingRuleBase{
Output: output,
Modes: ruleModes, Modes: ruleModes,
Name: ruleConfig.Name, Name: ruleConfig.Name,
}, nil }, nil
} }
func makeSimpleRule(ruleConfig RuleConfig, pDevs map[string]*evdev.InputDevice, base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleSimple, error) { func makeMappingRuleButton(ruleConfig RuleConfig,
pDevs map[string]*evdev.InputDevice,
vDevs map[string]*evdev.InputDevice,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButton, error) {
input, err := makeRuleTarget(ruleConfig.Input, pDevs) input, err := makeRuleTarget(ruleConfig.Input, pDevs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &mappingrules.MappingRuleSimple{ output, err := makeRuleTarget(ruleConfig.Output, vDevs)
if err != nil {
return nil, err
}
return &mappingrules.MappingRuleButton{
MappingRuleBase: base, MappingRuleBase: base,
Input: input, Input: input.(*mappingrules.RuleTargetButton),
Output: output.(*mappingrules.RuleTargetButton),
}, nil }, nil
} }
func makeComboRule(ruleConfig RuleConfig, pDevs map[string]*evdev.InputDevice, base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleCombo, error) { func makeComboRule(ruleConfig RuleConfig,
inputs := make([]mappingrules.RuleTarget, 0) pDevs map[string]*evdev.InputDevice,
vDevs map[string]*evdev.InputDevice,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleCombo, error) {
inputs := make([]*mappingrules.RuleTargetButton, 0)
for _, inputConfig := range ruleConfig.Inputs { for _, inputConfig := range ruleConfig.Inputs {
input, err := makeRuleTarget(inputConfig, pDevs) input, err := makeRuleTargetButton(inputConfig, pDevs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -108,8 +116,12 @@ func makeComboRule(ruleConfig RuleConfig, pDevs map[string]*evdev.InputDevice, b
}, nil }, nil
} }
func makeLatchedRule(ruleConfig RuleConfig, pDevs map[string]*evdev.InputDevice, base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleLatched, error) { func makeLatchedRule(ruleConfig RuleConfig,
input, err := makeRuleTarget(ruleConfig.Input, pDevs) pDevs map[string]*evdev.InputDevice,
vDevs map[string]*evdev.InputDevice,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleLatched, error) {
input, err := makeRuleTargetButton(ruleConfig.Input, pDevs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -121,7 +133,30 @@ func makeLatchedRule(ruleConfig RuleConfig, pDevs map[string]*evdev.InputDevice,
}, nil }, nil
} }
// makeInputRuleTarget takes an Input declaration from the YAML and returns a fully formed RuleTarget. func makeRuleTargetButton(targetConfig RuleTargetConfig, devs map[string]*evdev.InputDevice) (*mappingrules.RuleTargetButton, error) {
device, ok := devs[targetConfig.Device]
if !ok {
return nil, fmt.Errorf("couldn't build rule due to non-existent device '%s'", targetConfig.Device)
}
_, eventCode, err := decodeRuleTargetValues(targetConfig)
if err != nil {
return nil, err
}
baseParams := mappingrules.RuleTargetBase{
DeviceName: targetConfig.Device,
Device: device,
Inverted: targetConfig.Inverted,
Code: eventCode,
}
return &mappingrules.RuleTargetButton{
RuleTargetBase: baseParams,
}, nil
}
// makeRuleTarget takes an Input declaration from the YAML and returns a fully formed RuleTarget.
func makeRuleTarget(targetConfig RuleTargetConfig, devs map[string]*evdev.InputDevice) (mappingrules.RuleTarget, error) { func makeRuleTarget(targetConfig RuleTargetConfig, devs map[string]*evdev.InputDevice) (mappingrules.RuleTarget, error) {
if len(targetConfig.ModeSelect) > 0 { if len(targetConfig.ModeSelect) > 0 {
return &mappingrules.RuleTargetModeSelect{ return &mappingrules.RuleTargetModeSelect{

View file

@ -3,8 +3,7 @@ package mappingrules
import "github.com/holoplot/go-evdev" import "github.com/holoplot/go-evdev"
type MappingRule interface { type MappingRule interface {
MatchEvent(*evdev.InputDevice, *evdev.InputEvent, *string) *evdev.InputEvent MatchEvent(*evdev.InputDevice, *evdev.InputEvent, *string) (*evdev.InputDevice, *evdev.InputEvent)
OutputName() string
} }
// RuleTargets represent either a device input to match on, or an output to produce. // RuleTargets represent either a device input to match on, or an output to produce.
@ -21,8 +20,4 @@ type RuleTarget interface {
// //
// TODO: should we normalize inside this function to simplify the interface? // TODO: should we normalize inside this function to simplify the interface?
CreateEvent(int32, *string) *evdev.InputEvent CreateEvent(int32, *string) *evdev.InputEvent
GetCode() evdev.EvCode
GetDeviceName() string
GetDevice() *evdev.InputDevice
} }

View file

@ -4,14 +4,9 @@ import "slices"
type MappingRuleBase struct { type MappingRuleBase struct {
Name string Name string
Output RuleTarget
Modes []string Modes []string
} }
func (rule *MappingRuleBase) OutputName() string {
return rule.Output.GetDeviceName()
}
func (rule *MappingRuleBase) modeCheck(mode *string) bool { func (rule *MappingRuleBase) modeCheck(mode *string) bool {
if rule.Modes[0] == "*" { if rule.Modes[0] == "*" {
return true return true

View file

@ -0,0 +1,23 @@
package mappingrules
import "github.com/holoplot/go-evdev"
// A Simple Mapping Rule can map a button to a button or an axis to an axis.
type MappingRuleButton struct {
MappingRuleBase
Input *RuleTargetButton
Output *RuleTargetButton
}
func (rule *MappingRuleButton) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {
if !rule.MappingRuleBase.modeCheck(mode) {
return nil, nil
}
if device != rule.Input.GetDevice() ||
event.Code != rule.Input.GetCode() {
return nil, nil
}
return rule.Output.Device, rule.Output.CreateEvent(rule.Input.NormalizeValue(event.Value), mode)
}

View file

@ -7,17 +7,17 @@ import (
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
) )
type SimpleMappingRuleTests struct { type MappingRuleButtonTests struct {
suite.Suite suite.Suite
inputDevice *evdev.InputDevice inputDevice *evdev.InputDevice
wrongInputDevice *evdev.InputDevice wrongInputDevice *evdev.InputDevice
outputDevice *evdev.InputDevice outputDevice *evdev.InputDevice
mode *string mode *string
sampleRule *MappingRuleSimple sampleRule *MappingRuleButton
invertedRule *MappingRuleSimple invertedRule *MappingRuleButton
} }
func (t *SimpleMappingRuleTests) SetupTest() { func (t *MappingRuleButtonTests) SetupTest() {
t.inputDevice = &evdev.InputDevice{} t.inputDevice = &evdev.InputDevice{}
t.wrongInputDevice = &evdev.InputDevice{} t.wrongInputDevice = &evdev.InputDevice{}
t.outputDevice = &evdev.InputDevice{} t.outputDevice = &evdev.InputDevice{}
@ -25,24 +25,24 @@ func (t *SimpleMappingRuleTests) SetupTest() {
t.mode = &mode t.mode = &mode
// TODO: implement a constructor function... // TODO: implement a constructor function...
t.sampleRule = &MappingRuleSimple{ t.sampleRule = &MappingRuleButton{
MappingRuleBase: MappingRuleBase{ MappingRuleBase: MappingRuleBase{
Output: NewRuleTargetButton("", t.outputDevice, evdev.BTN_TRIGGER, false),
Modes: []string{"*"}, Modes: []string{"*"},
}, },
Input: NewRuleTargetButton("", t.inputDevice, evdev.BTN_TRIGGER, false), Input: NewRuleTargetButton("", t.inputDevice, evdev.BTN_TRIGGER, false),
Output: NewRuleTargetButton("", t.outputDevice, evdev.BTN_TRIGGER, false),
} }
t.invertedRule = &MappingRuleSimple{ t.invertedRule = &MappingRuleButton{
MappingRuleBase: MappingRuleBase{ MappingRuleBase: MappingRuleBase{
Output: NewRuleTargetButton("", t.outputDevice, evdev.BTN_TRIGGER, false),
Modes: []string{"*"}, Modes: []string{"*"},
}, },
Output: NewRuleTargetButton("", t.outputDevice, evdev.BTN_TRIGGER, false),
Input: NewRuleTargetButton("", t.inputDevice, evdev.BTN_TRIGGER, true), Input: NewRuleTargetButton("", t.inputDevice, evdev.BTN_TRIGGER, true),
} }
} }
func (t *SimpleMappingRuleTests) TestMatchEvent() { func (t *MappingRuleButtonTests) TestMatchEvent() {
// A matching input event should produce an output event // A matching input event should produce an output event
correctOutput := &evdev.InputEvent{ correctOutput := &evdev.InputEvent{
Type: evdev.EV_KEY, Type: evdev.EV_KEY,
@ -50,25 +50,25 @@ func (t *SimpleMappingRuleTests) TestMatchEvent() {
Value: 1, Value: 1,
} }
event := t.sampleRule.MatchEvent( _, event := t.sampleRule.MatchEvent(
t.inputDevice, t.inputDevice,
&evdev.InputEvent{Code: evdev.BTN_TRIGGER, Value: 1}, t.mode) &evdev.InputEvent{Code: evdev.BTN_TRIGGER, Value: 1}, t.mode)
t.EqualValues(correctOutput, event) t.EqualValues(correctOutput, event)
// An input event from the wrong device should produce a nil event // An input event from the wrong device should produce a nil event
event = t.sampleRule.MatchEvent( _, event = t.sampleRule.MatchEvent(
t.wrongInputDevice, t.wrongInputDevice,
&evdev.InputEvent{Code: evdev.BTN_TRIGGER, Value: 1}, t.mode) &evdev.InputEvent{Code: evdev.BTN_TRIGGER, Value: 1}, t.mode)
t.Nil(event) t.Nil(event)
// An input event from the wrong button should produce a nil event // An input event from the wrong button should produce a nil event
event = t.sampleRule.MatchEvent( _, event = t.sampleRule.MatchEvent(
t.inputDevice, t.inputDevice,
&evdev.InputEvent{Code: evdev.BTN_TOP, Value: 1}, t.mode) &evdev.InputEvent{Code: evdev.BTN_TOP, Value: 1}, t.mode)
t.Nil(event) t.Nil(event)
} }
func (t *SimpleMappingRuleTests) TestMatchEventInverted() { func (t *MappingRuleButtonTests) TestMatchEventInverted() {
// A matching input event should produce an output event // A matching input event should produce an output event
correctOutput := &evdev.InputEvent{ correctOutput := &evdev.InputEvent{
Type: evdev.EV_KEY, Type: evdev.EV_KEY,
@ -77,18 +77,18 @@ func (t *SimpleMappingRuleTests) TestMatchEventInverted() {
// Should get the opposite value out that we send in // Should get the opposite value out that we send in
correctOutput.Value = 0 correctOutput.Value = 0
event := t.invertedRule.MatchEvent( _, event := t.invertedRule.MatchEvent(
t.inputDevice, t.inputDevice,
&evdev.InputEvent{Code: evdev.BTN_TRIGGER, Value: 1}, t.mode) &evdev.InputEvent{Code: evdev.BTN_TRIGGER, Value: 1}, t.mode)
t.EqualValues(correctOutput, event) t.EqualValues(correctOutput, event)
correctOutput.Value = 1 correctOutput.Value = 1
event = t.invertedRule.MatchEvent( _, event = t.invertedRule.MatchEvent(
t.inputDevice, t.inputDevice,
&evdev.InputEvent{Code: evdev.BTN_TRIGGER, Value: 0}, t.mode) &evdev.InputEvent{Code: evdev.BTN_TRIGGER, Value: 0}, t.mode)
t.EqualValues(correctOutput, event) t.EqualValues(correctOutput, event)
} }
func TestRunnerMatching(t *testing.T) { func TestRunnerMatching(t *testing.T) {
suite.Run(t, new(SimpleMappingRuleTests)) suite.Run(t, new(MappingRuleButtonTests))
} }

View file

@ -5,13 +5,14 @@ import "github.com/holoplot/go-evdev"
// A Combo Mapping Rule can require multiple physical button presses for a single output button // A Combo Mapping Rule can require multiple physical button presses for a single output button
type MappingRuleCombo struct { type MappingRuleCombo struct {
MappingRuleBase MappingRuleBase
Inputs []RuleTarget Inputs []*RuleTargetButton
Output *RuleTargetButton
State int State int
} }
func (rule *MappingRuleCombo) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent, mode *string) *evdev.InputEvent { func (rule *MappingRuleCombo) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {
if !rule.MappingRuleBase.modeCheck(mode) { if !rule.MappingRuleBase.modeCheck(mode) {
return nil return nil, nil
} }
// Check each of the inputs, and if we find a match, proceed // Check each of the inputs, and if we find a match, proceed
@ -24,7 +25,7 @@ func (rule *MappingRuleCombo) MatchEvent(device *evdev.InputDevice, event *evdev
} }
if match == nil { if match == nil {
return nil return nil, nil
} }
// Get the value and add/subtract it from State // Get the value and add/subtract it from State
@ -39,10 +40,10 @@ func (rule *MappingRuleCombo) MatchEvent(device *evdev.InputDevice, event *evdev
targetState := len(rule.Inputs) targetState := len(rule.Inputs)
if oldState == targetState-1 && rule.State == targetState { if oldState == targetState-1 && rule.State == targetState {
return rule.Output.CreateEvent(1, mode) return rule.Output.GetDevice(), rule.Output.CreateEvent(1, mode)
} }
if oldState == targetState && rule.State == targetState-1 { if oldState == targetState && rule.State == targetState-1 {
return rule.Output.CreateEvent(0, mode) return rule.Output.GetDevice(), rule.Output.CreateEvent(0, mode)
} }
return nil return nil, nil
} }

View file

@ -4,19 +4,20 @@ import "github.com/holoplot/go-evdev"
type MappingRuleLatched struct { type MappingRuleLatched struct {
MappingRuleBase MappingRuleBase
Input RuleTarget Input *RuleTargetButton
Output *RuleTargetButton
State bool State bool
} }
func (rule *MappingRuleLatched) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent, mode *string) *evdev.InputEvent { func (rule *MappingRuleLatched) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {
if !rule.MappingRuleBase.modeCheck(mode) { if !rule.MappingRuleBase.modeCheck(mode) {
return nil return nil, nil
} }
if device != rule.Input.GetDevice() || if device != rule.Input.Device ||
event.Code != rule.Input.GetCode() || event.Code != rule.Input.Code ||
rule.Input.NormalizeValue(event.Value) == 0 { rule.Input.NormalizeValue(event.Value) == 0 {
return nil return nil, nil
} }
// Input is pressed, so toggle state and emit event // Input is pressed, so toggle state and emit event
@ -28,5 +29,5 @@ func (rule *MappingRuleLatched) MatchEvent(device *evdev.InputDevice, event *evd
value = 0 value = 0
} }
return rule.Output.CreateEvent(value, mode) return rule.Output.Device, rule.Output.CreateEvent(value, mode)
} }

View file

@ -16,19 +16,19 @@ type MappingRuleProportionalAxis struct {
LastEvent time.Time LastEvent time.Time
} }
func (rule *MappingRuleProportionalAxis) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent, mode *string) *evdev.InputEvent { func (rule *MappingRuleProportionalAxis) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {
if !rule.MappingRuleBase.modeCheck(mode) { if !rule.MappingRuleBase.modeCheck(mode) {
return nil return nil, nil
} }
if device != rule.Input.GetDevice() || if device != rule.Input.GetDevice() ||
event.Code != rule.Input.GetCode() { event.Code != rule.Input.GetCode() {
return nil return nil, nil
} }
// set the last value to the normalized input value // set the last value to the normalized input value
rule.LastValue = rule.Input.NormalizeValue(event.Value) rule.LastValue = rule.Input.NormalizeValue(event.Value)
return nil return nil, nil
} }
// TimerEvent returns an event when enough time has passed (compared to the last recorded axis value) // TimerEvent returns an event when enough time has passed (compared to the last recorded axis value)

View file

@ -1,22 +0,0 @@
package mappingrules
import "github.com/holoplot/go-evdev"
// A Simple Mapping Rule can map a button to a button or an axis to an axis.
type MappingRuleSimple struct {
MappingRuleBase
Input RuleTarget
}
func (rule *MappingRuleSimple) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent, mode *string) *evdev.InputEvent {
if !rule.MappingRuleBase.modeCheck(mode) {
return nil
}
if device != rule.Input.GetDevice() ||
event.Code != rule.Input.GetCode() {
return nil
}
return rule.Output.CreateEvent(rule.Input.NormalizeValue(event.Value), mode)
}