diff --git a/cmd/joyful/main.go b/cmd/joyful/main.go index 9c56829..51da9c6 100644 --- a/cmd/joyful/main.go +++ b/cmd/joyful/main.go @@ -34,6 +34,7 @@ func initVirtualBuffers(config *config.ConfigParser) map[string]*virtualdevice.E return vBuffers } +// Extracts the evdev devices from a list of virtual buffers and returns them. func getVirtualDevices(buffers map[string]*virtualdevice.EventBuffer) map[string]*evdev.InputDevice { devices := make(map[string]*evdev.InputDevice) for name, buffer := range buffers { @@ -79,6 +80,9 @@ func mapEvents(vBuffers map[string]*virtualdevice.EventBuffer, pDevices map[stri go eventWatcher(device, eventChannel) } + // initialize the mode variable + mode := "main" + fmt.Println("Joyful Running! Press Ctrl+C to quit.") for { // Get an event (blocks if necessary) @@ -95,7 +99,7 @@ func mapEvents(vBuffers map[string]*virtualdevice.EventBuffer, pDevices map[stri case evdev.EV_ABS: // We have a matchable event type. Check all the events for _, rule := range rules { - outputEvent := rule.MatchEvent(wrapper.Device, wrapper.Event) + outputEvent := rule.MatchEvent(wrapper.Device, wrapper.Event, &mode) if outputEvent == nil { continue } diff --git a/internal/config/configparser.go b/internal/config/configparser.go index 2e5904a..33d2df1 100644 --- a/internal/config/configparser.go +++ b/internal/config/configparser.go @@ -3,10 +3,10 @@ // Example usage: // config := &config.ConfigParser{} // config.Parse() -// virtualDevices, err := config.CreateVirtualDevices() -// physicalDevices, err := config.ConnectVirtualDevices() -// modes, err := config.GetModes() -// rules, err := config.BuildRules(physicalDevices, virtualDevices, modes) +// virtualDevices := config.CreateVirtualDevices() +// physicalDevices := config.ConnectVirtualDevices() +// modes := config.GetModes() +// rules := config.BuildRules(physicalDevices, virtualDevices, modes) // // nb: there are methods defined on ConfigParser in other files in this package! @@ -58,7 +58,7 @@ func (parser *ConfigParser) Parse(directory string) error { logger.LogIfError(err, "Error parsing YAML") parser.config.Rules = append(parser.config.Rules, newConfig.Rules...) parser.config.Devices = append(parser.config.Devices, newConfig.Devices...) - // parser.config.Groups = append(parser.config.Groups, newConfig.Groups...) + parser.config.Modes = append(parser.config.Modes, newConfig.Modes...) } } @@ -68,3 +68,7 @@ func (parser *ConfigParser) Parse(directory string) error { return nil } + +func (parser *ConfigParser) getModes() []string { + return append([]string{"main"}, parser.config.Modes...) +} diff --git a/internal/config/rules.go b/internal/config/rules.go index 6a5fa4d..adc7dd2 100644 --- a/internal/config/rules.go +++ b/internal/config/rules.go @@ -2,6 +2,7 @@ package config import ( "fmt" + "slices" "strings" "git.annabunches.net/annabunches/joyful/internal/logger" @@ -12,54 +13,71 @@ import ( // TODO: At some point it would *very likely* make sense to map each rule to all of the physical devices that can // trigger it, and return that instead. Something like a map[*evdev.InputDevice][]mappingrule.MappingRule. // This would speed up rule matching by only checking relevant rules for a given input event. -// We could take this further and make it a map[*evdev.InputDevice]map[evdev.InputType]map[evdev.InputCode][]mappingrule.MappingRule +// We could take this further and make it a map[][]rule // For very large rule-bases this may be helpful for staying performant. func (parser *ConfigParser) BuildRules(pDevs map[string]*evdev.InputDevice, vDevs map[string]*evdev.InputDevice) []mappingrules.MappingRule { rules := make([]mappingrules.MappingRule, 0) + modes := parser.getModes() for _, ruleConfig := range parser.config.Rules { var newRule mappingrules.MappingRule var err error + + baseParams, err := setBaseRuleParameters(ruleConfig, vDevs, modes) + if err != nil { + logger.LogError(err, "couldn't set output parameters, skipping rule") + continue + } + + logger.Logf("DEBUG: Modes for rule '%s': %v", baseParams.Name, baseParams.Modes) + switch strings.ToLower(ruleConfig.Type) { case RuleTypeSimple: - newRule, err = makeSimpleRule(ruleConfig, pDevs, vDevs) + newRule, err = makeSimpleRule(ruleConfig, pDevs, baseParams) case RuleTypeCombo: - newRule, err = makeComboRule(ruleConfig, pDevs, vDevs) + newRule, err = makeComboRule(ruleConfig, pDevs, baseParams) case RuleTypeLatched: - newRule, err = makeLatchedRule(ruleConfig, pDevs, vDevs) + newRule, err = makeLatchedRule(ruleConfig, pDevs, baseParams) } if err != nil { logger.LogError(err, "") continue } + rules = append(rules, newRule) } return rules } -func makeSimpleRule(ruleConfig RuleConfig, pDevs map[string]*evdev.InputDevice, vDevs map[string]*evdev.InputDevice) (*mappingrules.SimpleMappingRule, error) { +func setBaseRuleParameters(ruleConfig RuleConfig, vDevs map[string]*evdev.InputDevice, modes []string) (mappingrules.MappingRuleBase, error) { + output, err := makeRuleTarget(ruleConfig.Output, vDevs) + if err != nil { + return mappingrules.MappingRuleBase{}, err + } + ruleModes := verifyModes(ruleConfig, modes) + + return mappingrules.MappingRuleBase{ + Output: output, + Modes: ruleModes, + Name: ruleConfig.Name, + }, nil +} + +func makeSimpleRule(ruleConfig RuleConfig, pDevs map[string]*evdev.InputDevice, base mappingrules.MappingRuleBase) (*mappingrules.SimpleMappingRule, error) { input, err := makeRuleTarget(ruleConfig.Input, pDevs) if err != nil { return nil, err } - output, err := makeRuleTarget(ruleConfig.Output, vDevs) - if err != nil { - return nil, err - } - return &mappingrules.SimpleMappingRule{ - MappingRuleBase: mappingrules.MappingRuleBase{ - Output: output, - }, - Input: input, - Name: ruleConfig.Name, + MappingRuleBase: base, + Input: input, }, nil } -func makeComboRule(ruleConfig RuleConfig, pDevs map[string]*evdev.InputDevice, vDevs map[string]*evdev.InputDevice) (*mappingrules.ComboMappingRule, error) { +func makeComboRule(ruleConfig RuleConfig, pDevs map[string]*evdev.InputDevice, base mappingrules.MappingRuleBase) (*mappingrules.ComboMappingRule, error) { inputs := make([]mappingrules.RuleTarget, 0) for _, inputConfig := range ruleConfig.Inputs { input, err := makeRuleTarget(inputConfig, pDevs) @@ -69,39 +87,23 @@ func makeComboRule(ruleConfig RuleConfig, pDevs map[string]*evdev.InputDevice, v inputs = append(inputs, input) } - output, err := makeRuleTarget(ruleConfig.Output, vDevs) - if err != nil { - return nil, err - } - return &mappingrules.ComboMappingRule{ - MappingRuleBase: mappingrules.MappingRuleBase{ - Output: output, - }, - Inputs: inputs, - State: 0, - Name: ruleConfig.Name, + MappingRuleBase: base, + Inputs: inputs, + State: 0, }, nil } -func makeLatchedRule(ruleConfig RuleConfig, pDevs map[string]*evdev.InputDevice, vDevs map[string]*evdev.InputDevice) (*mappingrules.LatchedMappingRule, error) { +func makeLatchedRule(ruleConfig RuleConfig, pDevs map[string]*evdev.InputDevice, base mappingrules.MappingRuleBase) (*mappingrules.LatchedMappingRule, error) { input, err := makeRuleTarget(ruleConfig.Input, pDevs) if err != nil { return nil, err } - output, err := makeRuleTarget(ruleConfig.Output, vDevs) - if err != nil { - return nil, err - } - return &mappingrules.LatchedMappingRule{ - MappingRuleBase: mappingrules.MappingRuleBase{ - Output: output, - }, - Input: input, - Name: ruleConfig.Name, - State: false, + MappingRuleBase: base, + Input: input, + State: false, }, nil } @@ -152,3 +154,21 @@ func decodeRuleTargetValues(target RuleTargetConfig) (evdev.EvType, evdev.EvCode return eventType, eventCode, nil } + +func verifyModes(ruleConfig RuleConfig, modes []string) []string { + verifiedModes := make([]string, 0) + + for _, configMode := range ruleConfig.Modes { + if !slices.Contains(modes, configMode) { + logger.Logf("rule '%s' specifies undefined mode '%s', skipping", ruleConfig.Name, configMode) + continue + } + + verifiedModes = append(verifiedModes, configMode) + } + if len(verifiedModes) == 0 { + verifiedModes = []string{"main"} + } + + return verifiedModes +} diff --git a/internal/config/schema.go b/internal/config/schema.go index bdc3826..60490c8 100644 --- a/internal/config/schema.go +++ b/internal/config/schema.go @@ -5,9 +5,8 @@ package config type Config struct { Devices []DeviceConfig `yaml:"devices"` - // TODO: add groups - // Groups []GroupConfig `yaml:"groups,omitempty"` - Rules []RuleConfig `yaml:"rules"` + Modes []string `yaml:"modes,omitempty"` + Rules []RuleConfig `yaml:"rules"` } type DeviceConfig struct { @@ -25,12 +24,12 @@ type RuleConfig struct { Input RuleTargetConfig `yaml:"input,omitempty"` Inputs []RuleTargetConfig `yaml:"inputs,omitempty"` Output RuleTargetConfig `yaml:"output"` + Modes []string `yaml:"modes,omitempty"` } type RuleTargetConfig struct { - Device string `yaml:"device"` - Button string `yaml:"button,omitempty"` - Axis string `yaml:"axis,omitempty"` - Inverted bool `yaml:"inverted,omitempty"` - Groups []string `yaml:"groups,omitempty"` + Device string `yaml:"device"` + Button string `yaml:"button,omitempty"` + Axis string `yaml:"axis,omitempty"` + Inverted bool `yaml:"inverted,omitempty"` } diff --git a/internal/mappingrules/matching.go b/internal/mappingrules/matching.go index 4b38348..716a48d 100644 --- a/internal/mappingrules/matching.go +++ b/internal/mappingrules/matching.go @@ -1,6 +1,8 @@ package mappingrules import ( + "slices" + "git.annabunches.net/annabunches/joyful/internal/logger" "github.com/holoplot/go-evdev" ) @@ -9,6 +11,10 @@ func (rule *MappingRuleBase) OutputName() string { return rule.Output.DeviceName } +func (rule *MappingRuleBase) modeCheck(mode *string) bool { + return slices.Contains(rule.Modes, *mode) +} + // eventFromTarget creates an outputtable event from a RuleTarget func eventFromTarget(output RuleTarget, value int32) *evdev.InputEvent { return &evdev.InputEvent{ @@ -40,7 +46,11 @@ func valueFromTarget(rule RuleTarget, event *evdev.InputEvent) int32 { return value } -func (rule *SimpleMappingRule) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent) *evdev.InputEvent { +func (rule *SimpleMappingRule) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent, mode *string) *evdev.InputEvent { + if !rule.MappingRuleBase.modeCheck(mode) { + return nil + } + if device != rule.Input.Device || event.Code != rule.Input.Code { return nil @@ -49,7 +59,11 @@ func (rule *SimpleMappingRule) MatchEvent(device *evdev.InputDevice, event *evde return eventFromTarget(rule.Output, valueFromTarget(rule.Input, event)) } -func (rule *ComboMappingRule) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent) *evdev.InputEvent { +func (rule *ComboMappingRule) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent, mode *string) *evdev.InputEvent { + if !rule.MappingRuleBase.modeCheck(mode) { + return nil + } + // Check each of the inputs, and if we find a match, proceed var match *RuleTarget for _, input := range rule.Inputs { @@ -83,7 +97,11 @@ func (rule *ComboMappingRule) MatchEvent(device *evdev.InputDevice, event *evdev return nil } -func (rule *LatchedMappingRule) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent) *evdev.InputEvent { +func (rule *LatchedMappingRule) MatchEvent(device *evdev.InputDevice, event *evdev.InputEvent, mode *string) *evdev.InputEvent { + if !rule.MappingRuleBase.modeCheck(mode) { + return nil + } + if device != rule.Input.Device || event.Code != rule.Input.Code || valueFromTarget(rule.Input, event) == 0 { @@ -93,10 +111,10 @@ func (rule *LatchedMappingRule) MatchEvent(device *evdev.InputDevice, event *evd // Input is pressed, so toggle state and emit event var value int32 rule.State = !rule.State - if rule.State { - value = 1 - } else { - value = 0 + if rule.State { + value = 1 + } else { + value = 0 } return eventFromTarget(rule.Output, value) diff --git a/internal/mappingrules/types.go b/internal/mappingrules/types.go index ff39ef7..192c919 100644 --- a/internal/mappingrules/types.go +++ b/internal/mappingrules/types.go @@ -3,34 +3,33 @@ package mappingrules import "github.com/holoplot/go-evdev" type MappingRule interface { - MatchEvent(*evdev.InputDevice, *evdev.InputEvent) *evdev.InputEvent + MatchEvent(*evdev.InputDevice, *evdev.InputEvent, *string) *evdev.InputEvent OutputName() string } type MappingRuleBase struct { + Name string Output RuleTarget + Modes []string } // A Simple Mapping Rule can map a button to a button or an axis to an axis. type SimpleMappingRule struct { MappingRuleBase - Input RuleTarget - Name string + Input RuleTarget } // A Combo Mapping Rule can require multiple physical button presses for a single output button type ComboMappingRule struct { MappingRuleBase Inputs []RuleTarget - Name string State int } type LatchedMappingRule struct { MappingRuleBase - Input RuleTarget - Name string - State bool + Input RuleTarget + State bool } type RuleTarget struct { diff --git a/readme.md b/readme.md index bd7870c..d6ecf77 100644 --- a/readme.md +++ b/readme.md @@ -25,6 +25,7 @@ Joyful might be the tool for you. * Multiple modes with per-mode behavior. * Partial axis mapping: map sections of an axis to different outputs. +* Highly configurable deadzones * Macros - have a single input produce a sequence of button presses with configurable pauses. * Sequence combos - Button1, Button2, Button3 -> VirtualButtonA @@ -62,6 +63,10 @@ All `rules` must have a `type` field. Valid values for this field are: Configuration options for each type vary. See for an example of each type with all options specified. +### Modes + +All rules can have a `modes` field that is a list of strings. + ## Technical details