From ff38db659645dc507fa601fb6f9fac14e9a038dc Mon Sep 17 00:00:00 2001 From: Anna Rose Wiggins Date: Tue, 8 Jul 2025 03:26:24 +0000 Subject: [PATCH] Big Refactor (#2) Refactor Everything. Co-authored-by: Anna Rose Wiggins Co-committed-by: Anna Rose Wiggins --- cmd/joyful/main.go | 2 +- cmd/joyful/threads.go | 2 +- internal/config/make_rule_targets.go | 56 +++++ internal/config/make_rules.go | 216 ++++++------------ internal/config/modes.go | 19 ++ internal/config/schema.go | 14 +- internal/config/variables.go | 21 +- internal/logger/logger.go | 2 +- internal/mappingrules/interfaces.go | 13 +- internal/mappingrules/mapping_rule_axis.go | 27 +++ .../mapping_rule_axis_to_button.go | 51 +++++ internal/mappingrules/mapping_rule_base.go | 14 ++ internal/mappingrules/mapping_rule_button.go | 16 +- ..._combo.go => mapping_rule_button_combo.go} | 27 ++- ...ched.go => mapping_rule_button_latched.go} | 19 +- .../mappingrules/mapping_rule_mode_select.go | 40 ++++ .../mapping_rule_proportional_axis.go | 54 ----- internal/mappingrules/rule_target_axis.go | 42 ++-- internal/mappingrules/rule_target_base.go | 35 --- internal/mappingrules/rule_target_button.go | 20 +- .../mappingrules/rule_target_modeselect.go | 32 +-- 21 files changed, 413 insertions(+), 309 deletions(-) create mode 100644 internal/config/make_rule_targets.go create mode 100644 internal/config/modes.go create mode 100644 internal/mappingrules/mapping_rule_axis.go create mode 100644 internal/mappingrules/mapping_rule_axis_to_button.go rename internal/mappingrules/{mapping_rule_combo.go => mapping_rule_button_combo.go} (56%) rename internal/mappingrules/{mapping_rule_latched.go => mapping_rule_button_latched.go} (50%) create mode 100644 internal/mappingrules/mapping_rule_mode_select.go delete mode 100644 internal/mappingrules/mapping_rule_proportional_axis.go delete mode 100644 internal/mappingrules/rule_target_base.go diff --git a/cmd/joyful/main.go b/cmd/joyful/main.go index 5c6b373..9ce1d66 100644 --- a/cmd/joyful/main.go +++ b/cmd/joyful/main.go @@ -75,7 +75,7 @@ func main() { timerCount := 0 for _, rule := range rules { - if timedRule, ok := rule.(*mappingrules.MappingRuleProportionalAxis); ok { + if timedRule, ok := rule.(*mappingrules.MappingRuleAxisToButton); ok { go timerWatcher(timedRule, eventChannel) timerCount++ } diff --git a/cmd/joyful/threads.go b/cmd/joyful/threads.go index d8990cd..9f28101 100644 --- a/cmd/joyful/threads.go +++ b/cmd/joyful/threads.go @@ -28,7 +28,7 @@ func eventWatcher(device *evdev.InputDevice, channel chan<- ChannelEvent) { } } -func timerWatcher(rule *mappingrules.MappingRuleProportionalAxis, channel chan<- ChannelEvent) { +func timerWatcher(rule *mappingrules.MappingRuleAxisToButton, channel chan<- ChannelEvent) { for { event := rule.TimerEvent() if event != nil { diff --git a/internal/config/make_rule_targets.go b/internal/config/make_rule_targets.go new file mode 100644 index 0000000..36b5e3c --- /dev/null +++ b/internal/config/make_rule_targets.go @@ -0,0 +1,56 @@ +package config + +import ( + "errors" + "fmt" + + "git.annabunches.net/annabunches/joyful/internal/mappingrules" + "github.com/holoplot/go-evdev" +) + +func makeRuleTargetButton(targetConfig RuleTargetConfig, devs map[string]*evdev.InputDevice) (*mappingrules.RuleTargetButton, error) { + device, ok := devs[targetConfig.Device] + if !ok { + 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) + } + + return mappingrules.NewRuleTargetButton( + targetConfig.Device, + device, + eventCode, + targetConfig.Inverted, + ), nil +} + +func makeRuleTargetAxis(targetConfig RuleTargetConfig, devs map[string]*evdev.InputDevice) (*mappingrules.RuleTargetAxis, error) { + device, ok := devs[targetConfig.Device] + if !ok { + 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) + } + + return mappingrules.NewRuleTargetAxis( + targetConfig.Device, + device, + eventCode, + targetConfig.Inverted, + 0, 0, 0, // TODO: replace these with real values + ), nil +} + +func makeRuleTargetModeSelect(targetConfig RuleTargetConfig, allModes []string) (*mappingrules.RuleTargetModeSelect, error) { + if ok := validateModes(targetConfig.Modes, allModes); !ok { + return nil, errors.New("undefined mode in mode select list") + } + + return mappingrules.NewRuleTargetModeSelect(targetConfig.Modes) +} diff --git a/internal/config/make_rules.go b/internal/config/make_rules.go index 2748bae..5e7e9de 100644 --- a/internal/config/make_rules.go +++ b/internal/config/make_rules.go @@ -1,8 +1,8 @@ package config import ( + "errors" "fmt" - "slices" "strings" "git.annabunches.net/annabunches/joyful/internal/logger" @@ -23,23 +23,32 @@ func (parser *ConfigParser) BuildRules(pDevs map[string]*evdev.InputDevice, vDev var newRule mappingrules.MappingRule var err error - baseParams, err := setBaseRuleParameters(ruleConfig, modes) - if err != nil { - logger.LogErrorf(err, "couldn't set output parameters, skipping rule '%s'", ruleConfig.Name) + if ok := validateModes(ruleConfig.Modes, modes); !ok { + logger.Logf("Skipping rule '%s', mode list specifies undefined mode.", ruleConfig.Name) continue } + base := mappingrules.NewMappingRuleBase(ruleConfig.Name, ruleConfig.Modes) + switch strings.ToLower(ruleConfig.Type) { - case RuleTypeSimple: - newRule, err = makeMappingRuleButton(ruleConfig, pDevs, vDevs, baseParams) - case RuleTypeCombo: - newRule, err = makeComboRule(ruleConfig, pDevs, vDevs, baseParams) + case RuleTypeButton: + newRule, err = makeMappingRuleButton(ruleConfig, pDevs, vDevs, base) + case RuleTypeButtonCombo: + newRule, err = makeMappingRuleCombo(ruleConfig, pDevs, vDevs, base) case RuleTypeLatched: - newRule, err = makeLatchedRule(ruleConfig, pDevs, vDevs, baseParams) + newRule, err = makeMappingRuleLatched(ruleConfig, pDevs, vDevs, base) + case RuleTypeAxis: + newRule, err = makeMappingRuleAxis(ruleConfig, pDevs, vDevs, base) + case RuleTypeAxisToButton: + newRule, err = makeMappingRuleAxisToButton(ruleConfig, pDevs, vDevs, base) + case RuleTypeModeSelect: + newRule, err = makeMappingRuleModeSelect(ruleConfig, pDevs, modes, base) + default: + err = fmt.Errorf("bad rule type '%s' for rule '%s'", ruleConfig.Type, ruleConfig.Name) } if err != nil { - logger.LogError(err, "failed to build rule") + logger.LogErrorf(err, "Failed to build rule '%s'", ruleConfig.Name) continue } @@ -49,56 +58,28 @@ func (parser *ConfigParser) BuildRules(pDevs map[string]*evdev.InputDevice, vDev return rules } -func setBaseRuleParameters(ruleConfig RuleConfig, modes []string) (mappingrules.MappingRuleBase, error) { - // 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 - // makeRuleTarget call. - if len(ruleConfig.Output.ModeSelect) > 0 { - err := validateModes(ruleConfig.Output.ModeSelect, modes) - if err != nil { - return mappingrules.MappingRuleBase{}, err - } - } - - err := validateModes(ruleConfig.Modes, modes) - if err != nil { - return mappingrules.MappingRuleBase{}, err - } - - ruleModes := ensureModes(ruleConfig.Modes) - - return mappingrules.MappingRuleBase{ - Modes: ruleModes, - Name: ruleConfig.Name, - }, nil -} - 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 := makeRuleTargetButton(ruleConfig.Input, pDevs) if err != nil { return nil, err } - output, err := makeRuleTarget(ruleConfig.Output, vDevs) + output, err := makeRuleTargetButton(ruleConfig.Output, vDevs) if err != nil { return nil, err } - return &mappingrules.MappingRuleButton{ - MappingRuleBase: base, - Input: input.(*mappingrules.RuleTargetButton), - Output: output.(*mappingrules.RuleTargetButton), - }, nil + return mappingrules.NewMappingRuleButton(base, input, output), nil } -func makeComboRule(ruleConfig RuleConfig, +func makeMappingRuleCombo(ruleConfig RuleConfig, pDevs map[string]*evdev.InputDevice, vDevs map[string]*evdev.InputDevice, - base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleCombo, error) { + base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButtonCombo, error) { inputs := make([]*mappingrules.RuleTargetButton, 0) for _, inputConfig := range ruleConfig.Inputs { @@ -109,142 +90,73 @@ func makeComboRule(ruleConfig RuleConfig, inputs = append(inputs, input) } - return &mappingrules.MappingRuleCombo{ - MappingRuleBase: base, - Inputs: inputs, - State: 0, - }, nil + output, err := makeRuleTargetButton(ruleConfig.Output, vDevs) + if err != nil { + return nil, err + } + + return mappingrules.NewMappingRuleButtonCombo(base, inputs, output), nil } -func makeLatchedRule(ruleConfig RuleConfig, +func makeMappingRuleLatched(ruleConfig RuleConfig, pDevs map[string]*evdev.InputDevice, vDevs map[string]*evdev.InputDevice, - base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleLatched, error) { + base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButtonLatched, error) { input, err := makeRuleTargetButton(ruleConfig.Input, pDevs) if err != nil { return nil, err } - return &mappingrules.MappingRuleLatched{ - MappingRuleBase: base, - Input: input, - State: false, - }, nil -} - -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) + output, err := makeRuleTargetButton(ruleConfig.Output, vDevs) 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 + return mappingrules.NewMappingRuleButtonLatched(base, input, output), 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) { - if len(targetConfig.ModeSelect) > 0 { - return &mappingrules.RuleTargetModeSelect{ - ModeSelect: targetConfig.ModeSelect, - }, nil - } +func makeMappingRuleAxis(ruleConfig RuleConfig, + pDevs map[string]*evdev.InputDevice, + vDevs map[string]*evdev.InputDevice, + base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxis, error) { - device, ok := devs[targetConfig.Device] - if !ok { - return nil, fmt.Errorf("couldn't build rule due to non-existent device '%s'", targetConfig.Device) - } - - eventType, eventCode, err := decodeRuleTargetValues(targetConfig) + input, err := makeRuleTargetAxis(ruleConfig.Input, pDevs) if err != nil { return nil, err } - baseParams := mappingrules.RuleTargetBase{ - DeviceName: targetConfig.Device, - Device: device, - Inverted: targetConfig.Inverted, - Code: eventCode, + output, err := makeRuleTargetAxis(ruleConfig.Output, vDevs) + if err != nil { + return nil, err } - switch eventType { - case evdev.EV_KEY: - return &mappingrules.RuleTargetButton{ - RuleTargetBase: baseParams, - }, nil - - case evdev.EV_ABS: - return &mappingrules.RuleTargetAxis{ - RuleTargetBase: baseParams, - AxisStart: targetConfig.AxisStart, - AxisEnd: targetConfig.AxisEnd, - }, nil - - default: - return nil, fmt.Errorf("skipping rule due to unsupported event type '%d'", eventType) - } + return mappingrules.NewMappingRuleAxis(base, input, output), nil } -// decodeRuleTargetValues returns the appropriate evdev.EvType and evdev.EvCode values -// for a given RuleTargetConfig, converting the config file strings into appropriate constants -// -// Todo: support different formats for key specification -func decodeRuleTargetValues(target RuleTargetConfig) (evdev.EvType, evdev.EvCode, error) { - var eventType evdev.EvType - var eventCode evdev.EvCode - var ok bool +// STUB +func makeMappingRuleAxisToButton(ruleConfig RuleConfig, + pDevs map[string]*evdev.InputDevice, + vDevs map[string]*evdev.InputDevice, + base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxisToButton, error) { - if target.Button != "" { - eventType = evdev.EV_KEY - eventCode, ok = evdev.KEYFromString[target.Button] - if !ok { - return 0, 0, fmt.Errorf("skipping rule due to invalid button code '%s'", target.Button) - } - } else if target.Axis != "" { - eventType = evdev.EV_ABS - eventCode, ok = evdev.ABSFromString[target.Axis] - if !ok { - return 0, 0, fmt.Errorf("skipping rule due to invalid axis code '%s'", target.Button) - } - } - - return eventType, eventCode, nil + return nil, errors.New("stub: makeMappingRuleAxisToButton") } -// ensureModes either returns the mode list, or if it is empty returns []string{"*"} -func ensureModes(modes []string) []string { - if len(modes) == 0 { - return []string{"*"} +func makeMappingRuleModeSelect(ruleConfig RuleConfig, + pDevs map[string]*evdev.InputDevice, + modes []string, + base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleModeSelect, error) { + + input, err := makeRuleTargetButton(ruleConfig.Input, pDevs) + if err != nil { + return nil, err } - return modes -} - -// validateModes checks the provided modes against a larger subset of modes (usually all defined ones) -// and returns an error if any of the modes are not defined. -func validateModes(modes []string, allModes []string) error { - if len(modes) == 0 { - return nil - } - - for _, mode := range modes { - if !slices.Contains(allModes, mode) { - return fmt.Errorf("mode list specifies undefined mode '%s'", mode) - } - } - - return nil + + output, err := makeRuleTargetModeSelect(ruleConfig.Output, modes) + if err != nil { + return nil, err + } + + return mappingrules.NewMappingRuleModeSelect(base, input, output), nil } diff --git a/internal/config/modes.go b/internal/config/modes.go new file mode 100644 index 0000000..ad3dee2 --- /dev/null +++ b/internal/config/modes.go @@ -0,0 +1,19 @@ +package config + +import "slices" + +// validateModes checks the provided modes against a larger subset of modes (usually all defined ones) +// and returns false if any of the modes are not defined. +func validateModes(modes []string, allModes []string) bool { + if len(modes) == 0 { + return true + } + + for _, mode := range modes { + if !slices.Contains(allModes, mode) { + return false + } + } + + return true +} diff --git a/internal/config/schema.go b/internal/config/schema.go index 2ea4c97..42bd339 100644 --- a/internal/config/schema.go +++ b/internal/config/schema.go @@ -28,11 +28,11 @@ type RuleConfig struct { } type RuleTargetConfig struct { - Device string `yaml:"device,omitempty"` - Button string `yaml:"button,omitempty"` - Axis string `yaml:"axis,omitempty"` - AxisStart int32 `yaml:"axis_start,omitempty"` - AxisEnd int32 `yaml:"axis_end,omitempty"` - Inverted bool `yaml:"inverted,omitempty"` - ModeSelect []string `yaml:"mode_select,omitempty"` + Device string `yaml:"device,omitempty"` + Button string `yaml:"button,omitempty"` + Axis string `yaml:"axis,omitempty"` + DeadzoneStart int32 `yaml:"axis_start,omitempty"` + DeadzoneEnd int32 `yaml:"axis_end,omitempty"` + Inverted bool `yaml:"inverted,omitempty"` + Modes []string `yaml:"modes,omitempty"` } diff --git a/internal/config/variables.go b/internal/config/variables.go index 15d4ca7..f352474 100644 --- a/internal/config/variables.go +++ b/internal/config/variables.go @@ -4,6 +4,18 @@ import ( "github.com/holoplot/go-evdev" ) +const ( + DeviceTypePhysical = "physical" + DeviceTypeVirtual = "virtual" + + RuleTypeButton = "button" + RuleTypeButtonCombo = "button-combo" + RuleTypeLatched = "button-latched" + RuleTypeAxis = "axis" + RuleTypeModeSelect = "mode-select" + RuleTypeAxisToButton = "axis-to-button" +) + var ( ButtonFromIndex = []evdev.EvCode{ evdev.BTN_TRIGGER, @@ -60,12 +72,3 @@ var ( evdev.BTN_TRIGGER_HAPPY40, } ) - -const ( - DeviceTypePhysical = "physical" - DeviceTypeVirtual = "virtual" - - RuleTypeSimple = "simple" - RuleTypeCombo = "combo" - RuleTypeLatched = "latched" -) diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 4648296..35fc11e 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -25,7 +25,7 @@ func LogErrorf(err error, msg string, params ...interface{}) { if msg == "" { fmt.Printf("%s\n", err.Error()) } else { - fmt.Printf("%s: %s\n", err.Error(), fmt.Sprintf(msg, params...)) + fmt.Printf("%s: %s\n", fmt.Sprintf(msg, params...), err.Error()) } } diff --git a/internal/mappingrules/interfaces.go b/internal/mappingrules/interfaces.go index 802b484..a3f3b70 100644 --- a/internal/mappingrules/interfaces.go +++ b/internal/mappingrules/interfaces.go @@ -13,11 +13,16 @@ type RuleTarget interface { // (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. - // + // MatchEvent returns true if the provided device and input event are a match for this rule target + ValidateEvent(*evdev.InputDevice, *evdev.InputEvent) bool + + // CreateEvent creates 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 does not do any error checking; it assumes it is receiving verified and sanitized output. + // The caller is responsible for determining whether an event *should* be emitted. + // + // Typically int32 is the input event's normalized value. *string is the current mode, but is optional + // for most implementations. CreateEvent(int32, *string) *evdev.InputEvent } diff --git a/internal/mappingrules/mapping_rule_axis.go b/internal/mappingrules/mapping_rule_axis.go new file mode 100644 index 0000000..b190963 --- /dev/null +++ b/internal/mappingrules/mapping_rule_axis.go @@ -0,0 +1,27 @@ +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 MappingRuleAxis struct { + MappingRuleBase + Input *RuleTargetAxis + Output *RuleTargetAxis +} + +func NewMappingRuleAxis(base MappingRuleBase, input *RuleTargetAxis, output *RuleTargetAxis) *MappingRuleAxis { + return &MappingRuleAxis{ + MappingRuleBase: base, + Input: input, + Output: output, + } +} + +func (rule *MappingRuleAxis) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { + if !rule.MappingRuleBase.modeCheck(mode) || + !rule.Input.MatchEvent(device, event) { + return nil, nil + } + + return rule.Output.Device, rule.Output.CreateEvent(rule.Input.NormalizeValue(event.Value), mode) +} diff --git a/internal/mappingrules/mapping_rule_axis_to_button.go b/internal/mappingrules/mapping_rule_axis_to_button.go new file mode 100644 index 0000000..f28703b --- /dev/null +++ b/internal/mappingrules/mapping_rule_axis_to_button.go @@ -0,0 +1,51 @@ +package mappingrules + +import ( + "time" + + "github.com/holoplot/go-evdev" +) + +// TODO: This whole file is still WIP +type MappingRuleAxisToButton struct { + MappingRuleBase + Input *RuleTargetAxis + Output *RuleTargetButton + RepeatSpeedMin int32 + RepeatSpeedMax int32 + lastValue int32 + lastEvent time.Time +} + +func (rule *MappingRuleAxisToButton) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { + if !rule.MappingRuleBase.modeCheck(mode) || + !rule.Input.MatchEvent(device, event) { + return nil, nil + } + + // set the last value to the normalized input value + rule.lastValue = rule.Input.NormalizeValue(event.Value) + return nil, nil +} + +// TimerEvent returns an event when enough time has passed (compared to the last recorded axis value) +// to emit an event. +func (rule *MappingRuleAxisToButton) TimerEvent() *evdev.InputEvent { + // This is tighter coupling than we'd like, but it will do for now. + // TODO: maybe it would be better to just be more declarative about event types and their inputs and outputs. + if rule.lastValue < rule.Input.DeadzoneStart { + rule.lastEvent = time.Now() + return nil + } + + // calculate target time until next event press + // nextEvent := rule.LastEvent + (rule.LastValue) + + // TODO: figure out what the condition should be + if false { + // TODO: emit event + rule.lastEvent = time.Now() + } + + return nil +} diff --git a/internal/mappingrules/mapping_rule_base.go b/internal/mappingrules/mapping_rule_base.go index afdf4ee..fbf1ff4 100644 --- a/internal/mappingrules/mapping_rule_base.go +++ b/internal/mappingrules/mapping_rule_base.go @@ -7,6 +7,20 @@ type MappingRuleBase struct { Modes []string } +func NewMappingRuleBase( + name string, + modes []string, +) MappingRuleBase { + if len(modes) == 0 { + modes = []string{"*"} + } + + return MappingRuleBase{ + Name: name, + Modes: modes, + } +} + func (rule *MappingRuleBase) modeCheck(mode *string) bool { if rule.Modes[0] == "*" { return true diff --git a/internal/mappingrules/mapping_rule_button.go b/internal/mappingrules/mapping_rule_button.go index 57c123d..bf47609 100644 --- a/internal/mappingrules/mapping_rule_button.go +++ b/internal/mappingrules/mapping_rule_button.go @@ -9,13 +9,25 @@ type MappingRuleButton struct { Output *RuleTargetButton } +func NewMappingRuleButton( + base MappingRuleBase, + input *RuleTargetButton, + output *RuleTargetButton) *MappingRuleButton { + + return &MappingRuleButton{ + MappingRuleBase: base, + Input: input, + Output: output, + } +} + 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() { + if device != rule.Input.Device || + event.Code != rule.Input.Button { return nil, nil } diff --git a/internal/mappingrules/mapping_rule_combo.go b/internal/mappingrules/mapping_rule_button_combo.go similarity index 56% rename from internal/mappingrules/mapping_rule_combo.go rename to internal/mappingrules/mapping_rule_button_combo.go index d663fe5..8bfbda8 100644 --- a/internal/mappingrules/mapping_rule_combo.go +++ b/internal/mappingrules/mapping_rule_button_combo.go @@ -3,24 +3,37 @@ package mappingrules import "github.com/holoplot/go-evdev" // A Combo Mapping Rule can require multiple physical button presses for a single output button -type MappingRuleCombo struct { +type MappingRuleButtonCombo struct { MappingRuleBase Inputs []*RuleTargetButton Output *RuleTargetButton State int } -func (rule *MappingRuleCombo) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { +func NewMappingRuleButtonCombo( + base MappingRuleBase, + inputs []*RuleTargetButton, + output *RuleTargetButton) *MappingRuleButtonCombo { + + return &MappingRuleButtonCombo{ + MappingRuleBase: base, + Inputs: inputs, + Output: output, + State: 0, + } +} + +func (rule *MappingRuleButtonCombo) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { if !rule.MappingRuleBase.modeCheck(mode) { return nil, nil } // Check each of the inputs, and if we find a match, proceed - var match RuleTarget + var match *RuleTargetButton for _, input := range rule.Inputs { - if device == input.GetDevice() && - event.Code == input.GetCode() { + if input.MatchEvent(device, event) { match = input + break } } @@ -40,10 +53,10 @@ func (rule *MappingRuleCombo) MatchEvent(device *evdev.InputDevice, event *evdev targetState := len(rule.Inputs) if oldState == targetState-1 && rule.State == targetState { - return rule.Output.GetDevice(), rule.Output.CreateEvent(1, mode) + return rule.Output.Device, rule.Output.CreateEvent(1, mode) } if oldState == targetState && rule.State == targetState-1 { - return rule.Output.GetDevice(), rule.Output.CreateEvent(0, mode) + return rule.Output.Device, rule.Output.CreateEvent(0, mode) } return nil, nil } diff --git a/internal/mappingrules/mapping_rule_latched.go b/internal/mappingrules/mapping_rule_button_latched.go similarity index 50% rename from internal/mappingrules/mapping_rule_latched.go rename to internal/mappingrules/mapping_rule_button_latched.go index f8c074a..0915550 100644 --- a/internal/mappingrules/mapping_rule_latched.go +++ b/internal/mappingrules/mapping_rule_button_latched.go @@ -2,20 +2,33 @@ package mappingrules import "github.com/holoplot/go-evdev" -type MappingRuleLatched struct { +type MappingRuleButtonLatched struct { MappingRuleBase Input *RuleTargetButton Output *RuleTargetButton State bool } -func (rule *MappingRuleLatched) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { +func NewMappingRuleButtonLatched( + base MappingRuleBase, + input *RuleTargetButton, + output *RuleTargetButton) *MappingRuleButtonLatched { + + return &MappingRuleButtonLatched{ + MappingRuleBase: base, + Input: input, + Output: output, + State: false, + } +} + +func (rule *MappingRuleButtonLatched) 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.Device || - event.Code != rule.Input.Code || + event.Code != rule.Input.Button || rule.Input.NormalizeValue(event.Value) == 0 { return nil, nil } diff --git a/internal/mappingrules/mapping_rule_mode_select.go b/internal/mappingrules/mapping_rule_mode_select.go new file mode 100644 index 0000000..c858e10 --- /dev/null +++ b/internal/mappingrules/mapping_rule_mode_select.go @@ -0,0 +1,40 @@ +package mappingrules + +import "github.com/holoplot/go-evdev" + +type MappingRuleModeSelect struct { + MappingRuleBase + Input *RuleTargetButton + Output *RuleTargetModeSelect +} + +func NewMappingRuleModeSelect( + base MappingRuleBase, + input *RuleTargetButton, + output *RuleTargetModeSelect, +) *MappingRuleModeSelect { + + return &MappingRuleModeSelect{ + MappingRuleBase: base, + Input: input, + Output: output, + } +} + +func (rule *MappingRuleModeSelect) 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.Device || + event.Code != rule.Input.Button || + rule.Input.NormalizeValue(event.Value) == 0 { + return nil, nil + } + + return nil, rule.Output.CreateEvent(event.Value, mode) +} diff --git a/internal/mappingrules/mapping_rule_proportional_axis.go b/internal/mappingrules/mapping_rule_proportional_axis.go deleted file mode 100644 index 564c9ab..0000000 --- a/internal/mappingrules/mapping_rule_proportional_axis.go +++ /dev/null @@ -1,54 +0,0 @@ -package mappingrules - -import ( - "time" - - "github.com/holoplot/go-evdev" -) - -// TODO: How are we going to implement this? It needs to operate on a timer... -type MappingRuleProportionalAxis struct { - MappingRuleBase - Input *RuleTargetAxis - Output *RuleTargetButton - Sensitivity int32 - LastValue int32 - LastEvent time.Time -} - -func (rule *MappingRuleProportionalAxis) 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 - } - - // set the last value to the normalized input value - rule.LastValue = rule.Input.NormalizeValue(event.Value) - return nil, nil -} - -// TimerEvent returns an event when enough time has passed (compared to the last recorded axis value) -// to emit an event. -func (rule *MappingRuleProportionalAxis) TimerEvent() *evdev.InputEvent { - // This is tighter coupling than we'd like, but it will do for now. - // TODO: maybe it would be better to just be more declarative about event types and their inputs and outputs. - if rule.LastValue < rule.Input.AxisStart { - rule.LastEvent = time.Now() - return nil - } - - // calculate target time until next event press - // nextEvent := rule.LastEvent + (rule.LastValue) - - // TODO: figure out what the condition should be - if false { - // TODO: emit event - rule.LastEvent = time.Now() - } - - return nil -} diff --git a/internal/mappingrules/rule_target_axis.go b/internal/mappingrules/rule_target_axis.go index 938ad41..680c6d5 100644 --- a/internal/mappingrules/rule_target_axis.go +++ b/internal/mappingrules/rule_target_axis.go @@ -5,35 +5,43 @@ import ( ) type RuleTargetAxis struct { - RuleTargetBase - AxisStart int32 - AxisEnd int32 - Sensitivity float64 + DeviceName string + Device *evdev.InputDevice + Axis evdev.EvCode + Inverted bool + DeadzoneStart int32 + DeadzoneEnd int32 + Sensitivity float64 } func NewRuleTargetAxis(device_name string, device *evdev.InputDevice, - code evdev.EvCode, + axis evdev.EvCode, inverted bool, - axis_start int32, - axis_end int32, + deadzone_start int32, + deadzone_end int32, sensitivity float64) *RuleTargetAxis { return &RuleTargetAxis{ - RuleTargetBase: NewRuleTargetBase(device_name, device, code, inverted), - AxisStart: axis_start, - AxisEnd: axis_end, - Sensitivity: sensitivity, + DeviceName: device_name, + Device: device, + Axis: axis, + Inverted: inverted, + DeadzoneStart: deadzone_start, + DeadzoneEnd: deadzone_end, + Sensitivity: sensitivity, } } +// TODO: lots of fixes and decisions to make here. Should we normalize all axes to the same range? +// How do we handle deadzones in light of that? func (target *RuleTargetAxis) NormalizeValue(value int32) int32 { if !target.Inverted { return value } - axisRange := target.AxisEnd - target.AxisStart - axisMid := target.AxisEnd - axisRange/2 + axisRange := target.DeadzoneEnd - target.DeadzoneStart + axisMid := target.DeadzoneEnd - axisRange/2 delta := value - axisMid if delta < 0 { delta = -delta @@ -55,7 +63,13 @@ func (target *RuleTargetAxis) CreateEvent(value int32, mode *string) *evdev.Inpu // TODO: oh no we need center deadzones actually... return &evdev.InputEvent{ Type: evdev.EV_ABS, - Code: target.Code, + Code: target.Axis, Value: value, } } + +func (target *RuleTargetAxis) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent) bool { + return device == target.Device && + event.Type == evdev.EV_ABS && + event.Code == target.Axis +} diff --git a/internal/mappingrules/rule_target_base.go b/internal/mappingrules/rule_target_base.go deleted file mode 100644 index 7d03130..0000000 --- a/internal/mappingrules/rule_target_base.go +++ /dev/null @@ -1,35 +0,0 @@ -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 index b34b498..67fe5d6 100644 --- a/internal/mappingrules/rule_target_button.go +++ b/internal/mappingrules/rule_target_button.go @@ -3,12 +3,18 @@ package mappingrules import "github.com/holoplot/go-evdev" type RuleTargetButton struct { - RuleTargetBase + DeviceName string + Device *evdev.InputDevice + Button evdev.EvCode + Inverted bool } func NewRuleTargetButton(device_name string, device *evdev.InputDevice, code evdev.EvCode, inverted bool) *RuleTargetButton { return &RuleTargetButton{ - RuleTargetBase: NewRuleTargetBase(device_name, device, code, inverted), + DeviceName: device_name, + Device: device, + Button: code, + Inverted: inverted, } } @@ -22,10 +28,16 @@ func (target *RuleTargetButton) NormalizeValue(value int32) int32 { return value } -func (target *RuleTargetButton) CreateEvent(value int32, mode *string) *evdev.InputEvent { +func (target *RuleTargetButton) CreateEvent(value int32, _ *string) *evdev.InputEvent { return &evdev.InputEvent{ Type: evdev.EV_KEY, - Code: target.Code, + Code: target.Button, Value: value, } } + +func (target *RuleTargetButton) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent) bool { + return device == target.Device && + event.Type == evdev.EV_KEY && + event.Code == target.Button +} diff --git a/internal/mappingrules/rule_target_modeselect.go b/internal/mappingrules/rule_target_modeselect.go index 7ed4332..55c8f46 100644 --- a/internal/mappingrules/rule_target_modeselect.go +++ b/internal/mappingrules/rule_target_modeselect.go @@ -1,6 +1,7 @@ package mappingrules import ( + "errors" "slices" "git.annabunches.net/annabunches/joyful/internal/logger" @@ -8,34 +9,35 @@ import ( ) type RuleTargetModeSelect struct { - RuleTargetBase - ModeSelect []string + Modes []string } -func NewRuleTargetModeSelect(modes []string) *RuleTargetModeSelect { - return &RuleTargetModeSelect{ - RuleTargetBase: NewRuleTargetBase("", nil, 0, false), - ModeSelect: modes, +func NewRuleTargetModeSelect(modes []string) (*RuleTargetModeSelect, error) { + if len(modes) == 0 { + return nil, errors.New("cannot create RuleTargetModeSelect: mode list is empty") } + return &RuleTargetModeSelect{ + Modes: modes, + }, nil } // RuleTargetModeSelect doesn't make sense as an input type -func (target *RuleTargetModeSelect) NormalizeValue(value int32) int32 { +func (target *RuleTargetModeSelect) NormalizeValue(_ int32) int32 { return -1 } -func (target *RuleTargetModeSelect) CreateEvent(value int32, mode *string) *evdev.InputEvent { - if value == 0 { - return nil - } - +func (target *RuleTargetModeSelect) CreateEvent(_ int32, mode *string) *evdev.InputEvent { index := 0 - if currentMode := slices.Index(target.ModeSelect, *mode); currentMode != -1 { + if currentMode := slices.Index(target.Modes, *mode); currentMode != -1 { // find the next mode - index = (currentMode + 1) % len(target.ModeSelect) + index = (currentMode + 1) % len(target.Modes) } - *mode = target.ModeSelect[index] + *mode = target.Modes[index] logger.Logf("Mode changed to '%s'", *mode) return nil } + +func (target *RuleTargetModeSelect) MatchEvent(_ *evdev.InputDevice, _ *evdev.InputEvent) bool { + return false +}