Initial implementation of modes, though they're not quite working.

This commit is contained in:
Anna Rose Wiggins 2025-07-03 12:19:57 -04:00
parent 15b9fa6ac0
commit cc37904fad
7 changed files with 116 additions and 67 deletions

View file

@ -34,6 +34,7 @@ func initVirtualBuffers(config *config.ConfigParser) map[string]*virtualdevice.E
return vBuffers 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 { func getVirtualDevices(buffers map[string]*virtualdevice.EventBuffer) map[string]*evdev.InputDevice {
devices := make(map[string]*evdev.InputDevice) devices := make(map[string]*evdev.InputDevice)
for name, buffer := range buffers { for name, buffer := range buffers {
@ -79,6 +80,9 @@ func mapEvents(vBuffers map[string]*virtualdevice.EventBuffer, pDevices map[stri
go eventWatcher(device, eventChannel) go eventWatcher(device, eventChannel)
} }
// initialize the mode variable
mode := "main"
fmt.Println("Joyful Running! Press Ctrl+C to quit.") fmt.Println("Joyful Running! Press Ctrl+C to quit.")
for { for {
// Get an event (blocks if necessary) // Get an event (blocks if necessary)
@ -95,7 +99,7 @@ func mapEvents(vBuffers map[string]*virtualdevice.EventBuffer, pDevices map[stri
case evdev.EV_ABS: case 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(wrapper.Device, wrapper.Event) outputEvent := rule.MatchEvent(wrapper.Device, wrapper.Event, &mode)
if outputEvent == nil { if outputEvent == nil {
continue continue
} }

View file

@ -3,10 +3,10 @@
// Example usage: // Example usage:
// config := &config.ConfigParser{} // config := &config.ConfigParser{}
// config.Parse(<some directory containing YAML files>) // config.Parse(<some directory containing YAML files>)
// virtualDevices, err := config.CreateVirtualDevices() // virtualDevices := config.CreateVirtualDevices()
// physicalDevices, err := config.ConnectVirtualDevices() // physicalDevices := config.ConnectVirtualDevices()
// modes, err := config.GetModes() // modes := config.GetModes()
// rules, err := config.BuildRules(physicalDevices, virtualDevices, modes) // rules := config.BuildRules(physicalDevices, virtualDevices, modes)
// //
// nb: there are methods defined on ConfigParser in other files in this package! // 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") logger.LogIfError(err, "Error parsing YAML")
parser.config.Rules = append(parser.config.Rules, newConfig.Rules...) parser.config.Rules = append(parser.config.Rules, newConfig.Rules...)
parser.config.Devices = append(parser.config.Devices, newConfig.Devices...) 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 return nil
} }
func (parser *ConfigParser) getModes() []string {
return append([]string{"main"}, parser.config.Modes...)
}

View file

@ -2,6 +2,7 @@ package config
import ( import (
"fmt" "fmt"
"slices"
"strings" "strings"
"git.annabunches.net/annabunches/joyful/internal/logger" "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 // 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. // 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. // 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[<struct of *inputdevice, type, and code>][]rule
// For very large rule-bases this may be helpful for staying performant. // 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 { func (parser *ConfigParser) BuildRules(pDevs map[string]*evdev.InputDevice, vDevs map[string]*evdev.InputDevice) []mappingrules.MappingRule {
rules := make([]mappingrules.MappingRule, 0) rules := make([]mappingrules.MappingRule, 0)
modes := parser.getModes()
for _, ruleConfig := range parser.config.Rules { for _, ruleConfig := range parser.config.Rules {
var newRule mappingrules.MappingRule var newRule mappingrules.MappingRule
var err error 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) { switch strings.ToLower(ruleConfig.Type) {
case RuleTypeSimple: case RuleTypeSimple:
newRule, err = makeSimpleRule(ruleConfig, pDevs, vDevs) newRule, err = makeSimpleRule(ruleConfig, pDevs, baseParams)
case RuleTypeCombo: case RuleTypeCombo:
newRule, err = makeComboRule(ruleConfig, pDevs, vDevs) newRule, err = makeComboRule(ruleConfig, pDevs, baseParams)
case RuleTypeLatched: case RuleTypeLatched:
newRule, err = makeLatchedRule(ruleConfig, pDevs, vDevs) newRule, err = makeLatchedRule(ruleConfig, pDevs, baseParams)
} }
if err != nil { if err != nil {
logger.LogError(err, "") logger.LogError(err, "")
continue continue
} }
rules = append(rules, newRule) rules = append(rules, newRule)
} }
return rules 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) input, err := makeRuleTarget(ruleConfig.Input, pDevs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
output, err := makeRuleTarget(ruleConfig.Output, vDevs)
if err != nil {
return nil, err
}
return &mappingrules.SimpleMappingRule{ return &mappingrules.SimpleMappingRule{
MappingRuleBase: mappingrules.MappingRuleBase{ MappingRuleBase: base,
Output: output, Input: input,
},
Input: input,
Name: ruleConfig.Name,
}, nil }, 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) inputs := make([]mappingrules.RuleTarget, 0)
for _, inputConfig := range ruleConfig.Inputs { for _, inputConfig := range ruleConfig.Inputs {
input, err := makeRuleTarget(inputConfig, pDevs) input, err := makeRuleTarget(inputConfig, pDevs)
@ -69,39 +87,23 @@ func makeComboRule(ruleConfig RuleConfig, pDevs map[string]*evdev.InputDevice, v
inputs = append(inputs, input) inputs = append(inputs, input)
} }
output, err := makeRuleTarget(ruleConfig.Output, vDevs)
if err != nil {
return nil, err
}
return &mappingrules.ComboMappingRule{ return &mappingrules.ComboMappingRule{
MappingRuleBase: mappingrules.MappingRuleBase{ MappingRuleBase: base,
Output: output, Inputs: inputs,
}, State: 0,
Inputs: inputs,
State: 0,
Name: ruleConfig.Name,
}, nil }, 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) input, err := makeRuleTarget(ruleConfig.Input, pDevs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
output, err := makeRuleTarget(ruleConfig.Output, vDevs)
if err != nil {
return nil, err
}
return &mappingrules.LatchedMappingRule{ return &mappingrules.LatchedMappingRule{
MappingRuleBase: mappingrules.MappingRuleBase{ MappingRuleBase: base,
Output: output, Input: input,
}, State: false,
Input: input,
Name: ruleConfig.Name,
State: false,
}, nil }, nil
} }
@ -152,3 +154,21 @@ func decodeRuleTargetValues(target RuleTargetConfig) (evdev.EvType, evdev.EvCode
return eventType, eventCode, nil 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
}

View file

@ -5,9 +5,8 @@ package config
type Config struct { type Config struct {
Devices []DeviceConfig `yaml:"devices"` Devices []DeviceConfig `yaml:"devices"`
// TODO: add groups Modes []string `yaml:"modes,omitempty"`
// Groups []GroupConfig `yaml:"groups,omitempty"` Rules []RuleConfig `yaml:"rules"`
Rules []RuleConfig `yaml:"rules"`
} }
type DeviceConfig struct { type DeviceConfig struct {
@ -25,12 +24,12 @@ type RuleConfig struct {
Input RuleTargetConfig `yaml:"input,omitempty"` Input RuleTargetConfig `yaml:"input,omitempty"`
Inputs []RuleTargetConfig `yaml:"inputs,omitempty"` Inputs []RuleTargetConfig `yaml:"inputs,omitempty"`
Output RuleTargetConfig `yaml:"output"` Output RuleTargetConfig `yaml:"output"`
Modes []string `yaml:"modes,omitempty"`
} }
type RuleTargetConfig struct { type RuleTargetConfig struct {
Device string `yaml:"device"` Device string `yaml:"device"`
Button string `yaml:"button,omitempty"` Button string `yaml:"button,omitempty"`
Axis string `yaml:"axis,omitempty"` Axis string `yaml:"axis,omitempty"`
Inverted bool `yaml:"inverted,omitempty"` Inverted bool `yaml:"inverted,omitempty"`
Groups []string `yaml:"groups,omitempty"`
} }

View file

@ -1,6 +1,8 @@
package mappingrules package mappingrules
import ( import (
"slices"
"git.annabunches.net/annabunches/joyful/internal/logger" "git.annabunches.net/annabunches/joyful/internal/logger"
"github.com/holoplot/go-evdev" "github.com/holoplot/go-evdev"
) )
@ -9,6 +11,10 @@ func (rule *MappingRuleBase) OutputName() string {
return rule.Output.DeviceName 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 // eventFromTarget creates an outputtable event from a RuleTarget
func eventFromTarget(output RuleTarget, value int32) *evdev.InputEvent { func eventFromTarget(output RuleTarget, value int32) *evdev.InputEvent {
return &evdev.InputEvent{ return &evdev.InputEvent{
@ -40,7 +46,11 @@ func valueFromTarget(rule RuleTarget, event *evdev.InputEvent) int32 {
return value 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 || if device != rule.Input.Device ||
event.Code != rule.Input.Code { event.Code != rule.Input.Code {
return nil return nil
@ -49,7 +59,11 @@ func (rule *SimpleMappingRule) MatchEvent(device *evdev.InputDevice, event *evde
return eventFromTarget(rule.Output, valueFromTarget(rule.Input, event)) 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 // Check each of the inputs, and if we find a match, proceed
var match *RuleTarget var match *RuleTarget
for _, input := range rule.Inputs { for _, input := range rule.Inputs {
@ -83,7 +97,11 @@ func (rule *ComboMappingRule) MatchEvent(device *evdev.InputDevice, event *evdev
return nil 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 || if device != rule.Input.Device ||
event.Code != rule.Input.Code || event.Code != rule.Input.Code ||
valueFromTarget(rule.Input, event) == 0 { valueFromTarget(rule.Input, event) == 0 {

View file

@ -3,34 +3,33 @@ 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) *evdev.InputEvent MatchEvent(*evdev.InputDevice, *evdev.InputEvent, *string) *evdev.InputEvent
OutputName() string OutputName() string
} }
type MappingRuleBase struct { type MappingRuleBase struct {
Name string
Output RuleTarget Output RuleTarget
Modes []string
} }
// A Simple Mapping Rule can map a button to a button or an axis to an axis. // A Simple Mapping Rule can map a button to a button or an axis to an axis.
type SimpleMappingRule struct { type SimpleMappingRule struct {
MappingRuleBase MappingRuleBase
Input RuleTarget Input RuleTarget
Name string
} }
// 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 ComboMappingRule struct { type ComboMappingRule struct {
MappingRuleBase MappingRuleBase
Inputs []RuleTarget Inputs []RuleTarget
Name string
State int State int
} }
type LatchedMappingRule struct { type LatchedMappingRule struct {
MappingRuleBase MappingRuleBase
Input RuleTarget Input RuleTarget
Name string State bool
State bool
} }
type RuleTarget struct { type RuleTarget struct {

View file

@ -25,6 +25,7 @@ Joyful might be the tool for you.
* Multiple modes with per-mode behavior. * Multiple modes with per-mode behavior.
* Partial axis mapping: map sections of an axis to different outputs. * 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. * Macros - have a single input produce a sequence of button presses with configurable pauses.
* Sequence combos - Button1, Button2, Button3 -> VirtualButtonA * 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 <examples/ruletypes.yml> for an example of each type with all options specified. Configuration options for each type vary. See <examples/ruletypes.yml> 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 ## Technical details