(WIP) Move rule initialization into rule package.

This commit is contained in:
Anna Rose Wiggins 2025-08-11 19:09:37 -04:00
parent 727985f91c
commit 9e4062ba30
21 changed files with 366 additions and 489 deletions

View file

@ -93,6 +93,11 @@ func initVirtualBuffers(config *configparser.Config) (map[string]*evdev.InputDev
return vDevicesByName, vBuffersByName, vBuffersByDevice return vDevicesByName, vBuffersByName, vBuffersByDevice
} }
// 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[Device][]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[<struct of *inputdevice, type, and code>][]rule
// For very large rule-bases this may be helpful for staying performant.
func loadRules( func loadRules(
config *configparser.Config, config *configparser.Config,
pDevices map[string]*evdev.InputDevice, pDevices map[string]*evdev.InputDevice,
@ -103,8 +108,21 @@ func loadRules(
eventChannel := make(chan ChannelEvent, 1000) eventChannel := make(chan ChannelEvent, 1000)
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
// Setup device mapping for the mappingrules package
pDevs := mappingrules.ConvertDeviceMap(pDevices)
vDevs := mappingrules.ConvertDeviceMap(vDevices)
// Initialize rules // Initialize rules
rules := configparser.InitRules(config.Rules, pDevices, vDevices, modes) rules := make([]mappingrules.MappingRule, 0)
for _, ruleConfig := range config.Rules {
newRule, err := mappingrules.NewRule(ruleConfig, pDevs, vDevs, modes)
if err != nil {
logger.LogError(err, "Failed to create rule, skipping")
continue
}
rules = append(rules, newRule)
}
logger.Logf("Created %d mapping rules.", len(rules)) logger.Logf("Created %d mapping rules.", len(rules))
// start listening for events on devices and timers // start listening for events on devices and timers

View file

@ -1,15 +1,3 @@
// The ConfigParser is the main structure you'll interact with when using this package.
//
// Example usage:
// config := &config.ConfigParser{}
// config.Parse(<some directory containing YAML files>)
// 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!
package configparser package configparser
import ( import (
@ -22,60 +10,6 @@ import (
"github.com/goccy/go-yaml" "github.com/goccy/go-yaml"
) )
type ConfigParser struct {
config Config
}
// Parse all the config files and store the config data for further use
func (parser *ConfigParser) Parse(directory string) error {
parser.config = Config{}
// Find the config files in the directory
dirEntries, err := os.ReadDir(directory)
if err != nil {
err = os.Mkdir(directory, 0755)
if err != nil {
return errors.New("Failed to create config directory at " + directory)
}
}
// Open each yaml file and add its contents to the global config
for _, file := range dirEntries {
name := file.Name()
if file.IsDir() || !(strings.HasSuffix(name, ".yaml") || strings.HasSuffix(name, ".yml")) {
continue
}
filePath := filepath.Join(directory, name)
if strings.HasSuffix(filePath, ".yaml") || strings.HasSuffix(filePath, ".yml") {
data, err := os.ReadFile(filePath)
if err != nil {
logger.LogError(err, "Error while opening config file")
continue
}
newConfig := Config{}
err = yaml.Unmarshal(data, &newConfig)
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.Modes = append(parser.config.Modes, newConfig.Modes...)
}
}
if len(parser.config.Devices) == 0 {
return errors.New("Found no devices in configuration. Please add configuration at " + directory)
}
return nil
}
func (parser *ConfigParser) GetModes() []string {
if len(parser.config.Modes) == 0 {
return []string{"*"}
}
return parser.config.Modes
}
func ParseConfig(directory string) (*Config, error) { func ParseConfig(directory string) (*Config, error) {
config := new(Config) config := new(Config)

View file

@ -1,57 +0,0 @@
package configparser
import (
"fmt"
"strings"
"git.annabunches.net/annabunches/joyful/internal/logger"
"github.com/holoplot/go-evdev"
)
// InitPhysicalDevices will create InputDevices corresponding to any registered
// devices with type = physical.
//
// This function assumes Parse() has been called.
//
// This function should only be called once.
func (parser *ConfigParser) InitPhysicalDevices() map[string]*evdev.InputDevice {
deviceMap := make(map[string]*evdev.InputDevice)
for _, deviceConfig := range parser.config.Devices {
if strings.ToLower(deviceConfig.Type) != DeviceTypePhysical {
continue
}
deviceConfig := deviceConfig.Config.(DeviceConfigPhysical)
var infoName string
var device *evdev.InputDevice
var err error
if deviceConfig.DevicePath != "" {
infoName = deviceConfig.DevicePath
device, err = evdev.Open(deviceConfig.DevicePath)
} else {
infoName = deviceConfig.DeviceName
device, err = evdev.OpenByName(deviceConfig.DeviceName)
}
if err != nil {
logger.LogError(err, "Failed to open physical device, skipping. Confirm the device name or path with 'evinfo'")
continue
}
if deviceConfig.Lock {
logger.LogDebugf("Locking device '%s'", infoName)
err := device.Grab()
if err != nil {
logger.LogError(err, "Failed to grab device for exclusive access")
}
}
logger.Log(fmt.Sprintf("Connected to '%s' as '%s'", infoName, deviceConfig.Name))
deviceMap[deviceConfig.Name] = device
}
return deviceMap
}

View file

@ -1,7 +0,0 @@
package configparser
import "github.com/holoplot/go-evdev"
type Device interface {
AbsInfos() (map[evdev.EvCode]evdev.AbsInfo, error)
}

View file

@ -1,236 +0,0 @@
package configparser
import (
"fmt"
"strings"
"git.annabunches.net/annabunches/joyful/internal/logger"
"git.annabunches.net/annabunches/joyful/internal/mappingrules"
"github.com/holoplot/go-evdev"
)
// 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[Device][]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[<struct of *inputdevice, type, and code>][]rule
// For very large rule-bases this may be helpful for staying performant.
func InitRules(config []RuleConfig, pInputDevs map[string]*evdev.InputDevice, vInputDevs map[string]*evdev.InputDevice, modes []string) []mappingrules.MappingRule {
rules := make([]mappingrules.MappingRule, 0)
// Golang can't inspect the concrete map type to determine interface conformance,
// so we handle that here.
pDevs := make(map[string]Device)
for name, dev := range pInputDevs {
pDevs[name] = dev
}
vDevs := make(map[string]Device)
for name, dev := range vInputDevs {
vDevs[name] = dev
}
for _, ruleConfig := range config {
var newRule mappingrules.MappingRule
var err error
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 RuleTypeButton:
newRule, err = makeMappingRuleButton(ruleConfig.Config.(RuleConfigButton), pDevs, vDevs, base)
case RuleTypeButtonCombo:
newRule, err = makeMappingRuleCombo(ruleConfig.Config.(RuleConfigButtonCombo), pDevs, vDevs, base)
case RuleTypeButtonLatched:
newRule, err = makeMappingRuleLatched(ruleConfig.Config.(RuleConfigButtonLatched), pDevs, vDevs, base)
case RuleTypeAxis:
newRule, err = makeMappingRuleAxis(ruleConfig.Config.(RuleConfigAxis), pDevs, vDevs, base)
case RuleTypeAxisCombined:
newRule, err = makeMappingRuleAxisCombined(ruleConfig.Config.(RuleConfigAxisCombined), pDevs, vDevs, base)
case RuleTypeAxisToButton:
newRule, err = makeMappingRuleAxisToButton(ruleConfig.Config.(RuleConfigAxisToButton), pDevs, vDevs, base)
case RuleTypeAxisToRelaxis:
newRule, err = makeMappingRuleAxisToRelaxis(ruleConfig.Config.(RuleConfigAxisToRelaxis), pDevs, vDevs, base)
case RuleTypeModeSelect:
newRule, err = makeMappingRuleModeSelect(ruleConfig.Config.(RuleConfigModeSelect), pDevs, modes, base)
default:
err = fmt.Errorf("bad rule type '%s' for rule '%s'", ruleConfig.Type, ruleConfig.Name)
}
if err != nil {
logger.LogErrorf(err, "Failed to build rule '%s'", ruleConfig.Name)
continue
}
rules = append(rules, newRule)
}
return rules
}
// TODO: how much of these functions could we fold into the unmarshaling logic itself? The main problem
// is that we don't have access to the device maps in those functions... could we set device names
// as stand-ins and do a post-processing pass that *just* handles device linking and possibly mode
// checking?
//
// In other words - can we unmarshal the config directly into our target structs and remove most of
// this library?
func makeMappingRuleButton(ruleConfig RuleConfigButton,
pDevs map[string]Device,
vDevs map[string]Device,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButton, error) {
input, err := makeRuleTargetButton(ruleConfig.Input, pDevs)
if err != nil {
return nil, err
}
output, err := makeRuleTargetButton(ruleConfig.Output, vDevs)
if err != nil {
return nil, err
}
return mappingrules.NewMappingRuleButton(base, input, output), nil
}
func makeMappingRuleCombo(ruleConfig RuleConfigButtonCombo,
pDevs map[string]Device,
vDevs map[string]Device,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButtonCombo, error) {
inputs := make([]*mappingrules.RuleTargetButton, 0)
for _, inputConfig := range ruleConfig.Inputs {
input, err := makeRuleTargetButton(inputConfig, pDevs)
if err != nil {
return nil, err
}
inputs = append(inputs, input)
}
output, err := makeRuleTargetButton(ruleConfig.Output, vDevs)
if err != nil {
return nil, err
}
return mappingrules.NewMappingRuleButtonCombo(base, inputs, output), nil
}
func makeMappingRuleLatched(ruleConfig RuleConfigButtonLatched,
pDevs map[string]Device,
vDevs map[string]Device,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButtonLatched, error) {
input, err := makeRuleTargetButton(ruleConfig.Input, pDevs)
if err != nil {
return nil, err
}
output, err := makeRuleTargetButton(ruleConfig.Output, vDevs)
if err != nil {
return nil, err
}
return mappingrules.NewMappingRuleButtonLatched(base, input, output), nil
}
func makeMappingRuleAxis(ruleConfig RuleConfigAxis,
pDevs map[string]Device,
vDevs map[string]Device,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxis, error) {
input, err := makeRuleTargetAxis(ruleConfig.Input, pDevs)
if err != nil {
return nil, err
}
output, err := makeRuleTargetAxis(ruleConfig.Output, vDevs)
if err != nil {
return nil, err
}
return mappingrules.NewMappingRuleAxis(base, input, output), nil
}
func makeMappingRuleAxisCombined(ruleConfig RuleConfigAxisCombined,
pDevs map[string]Device,
vDevs map[string]Device,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxisCombined, error) {
inputLower, err := makeRuleTargetAxis(ruleConfig.InputLower, pDevs)
if err != nil {
return nil, err
}
inputUpper, err := makeRuleTargetAxis(ruleConfig.InputUpper, pDevs)
if err != nil {
return nil, err
}
output, err := makeRuleTargetAxis(ruleConfig.Output, vDevs)
if err != nil {
return nil, err
}
return mappingrules.NewMappingRuleAxisCombined(base, inputLower, inputUpper, output), nil
}
func makeMappingRuleAxisToButton(ruleConfig RuleConfigAxisToButton,
pDevs map[string]Device,
vDevs map[string]Device,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxisToButton, error) {
input, err := makeRuleTargetAxis(ruleConfig.Input, pDevs)
if err != nil {
return nil, err
}
output, err := makeRuleTargetButton(ruleConfig.Output, vDevs)
if err != nil {
return nil, err
}
return mappingrules.NewMappingRuleAxisToButton(base, input, output, ruleConfig.RepeatRateMin, ruleConfig.RepeatRateMax), nil
}
func makeMappingRuleAxisToRelaxis(ruleConfig RuleConfigAxisToRelaxis,
pDevs map[string]Device,
vDevs map[string]Device,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxisToRelaxis, error) {
input, err := makeRuleTargetAxis(ruleConfig.Input, pDevs)
if err != nil {
return nil, err
}
output, err := makeRuleTargetRelaxis(ruleConfig.Output, vDevs)
if err != nil {
return nil, err
}
return mappingrules.NewMappingRuleAxisToRelaxis(base,
input, output,
ruleConfig.RepeatRateMin,
ruleConfig.RepeatRateMax,
ruleConfig.Increment), nil
}
func makeMappingRuleModeSelect(ruleConfig RuleConfigModeSelect,
pDevs map[string]Device,
modes []string,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleModeSelect, error) {
input, err := makeRuleTargetButton(ruleConfig.Input, pDevs)
if err != nil {
return nil, err
}
output, err := makeRuleTargetModeSelect(ruleConfig.Output, modes)
if err != nil {
return nil, err
}
return mappingrules.NewMappingRuleModeSelect(base, input, output), nil
}

View file

@ -1,19 +0,0 @@
package configparser
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
}

View file

@ -1,15 +1,16 @@
package configparser package mappingrules
import ( import (
"errors" "errors"
"fmt" "fmt"
"slices"
"git.annabunches.net/annabunches/joyful/internal/configparser"
"git.annabunches.net/annabunches/joyful/internal/eventcodes" "git.annabunches.net/annabunches/joyful/internal/eventcodes"
"git.annabunches.net/annabunches/joyful/internal/mappingrules"
"github.com/holoplot/go-evdev" "github.com/holoplot/go-evdev"
) )
func makeRuleTargetButton(targetConfig RuleTargetConfigButton, devs map[string]Device) (*mappingrules.RuleTargetButton, error) { func makeRuleTargetButton(targetConfig configparser.RuleTargetConfigButton, devs map[string]Device) (*RuleTargetButton, error) {
device, ok := devs[targetConfig.Device] device, ok := devs[targetConfig.Device]
if !ok { if !ok {
return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device) return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device)
@ -20,7 +21,7 @@ func makeRuleTargetButton(targetConfig RuleTargetConfigButton, devs map[string]D
return nil, err return nil, err
} }
return mappingrules.NewRuleTargetButton( return NewRuleTargetButton(
targetConfig.Device, targetConfig.Device,
device, device,
eventCode, eventCode,
@ -28,7 +29,7 @@ func makeRuleTargetButton(targetConfig RuleTargetConfigButton, devs map[string]D
) )
} }
func makeRuleTargetAxis(targetConfig RuleTargetConfigAxis, devs map[string]Device) (*mappingrules.RuleTargetAxis, error) { func makeRuleTargetAxis(targetConfig configparser.RuleTargetConfigAxis, devs map[string]Device) (*RuleTargetAxis, error) {
device, ok := devs[targetConfig.Device] device, ok := devs[targetConfig.Device]
if !ok { if !ok {
return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device) return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device)
@ -48,7 +49,7 @@ func makeRuleTargetAxis(targetConfig RuleTargetConfigAxis, devs map[string]Devic
return nil, err return nil, err
} }
return mappingrules.NewRuleTargetAxis( return NewRuleTargetAxis(
targetConfig.Device, targetConfig.Device,
device, device,
eventCode, eventCode,
@ -58,7 +59,7 @@ func makeRuleTargetAxis(targetConfig RuleTargetConfigAxis, devs map[string]Devic
) )
} }
func makeRuleTargetRelaxis(targetConfig RuleTargetConfigRelaxis, devs map[string]Device) (*mappingrules.RuleTargetRelaxis, error) { func makeRuleTargetRelaxis(targetConfig configparser.RuleTargetConfigRelaxis, devs map[string]Device) (*RuleTargetRelaxis, error) {
device, ok := devs[targetConfig.Device] device, ok := devs[targetConfig.Device]
if !ok { if !ok {
return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device) return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device)
@ -69,25 +70,23 @@ func makeRuleTargetRelaxis(targetConfig RuleTargetConfigRelaxis, devs map[string
return nil, err return nil, err
} }
return mappingrules.NewRuleTargetRelaxis( return NewRuleTargetRelaxis(
targetConfig.Device, targetConfig.Device,
device, device,
eventCode, eventCode,
) )
} }
func makeRuleTargetModeSelect(targetConfig RuleTargetConfigModeSelect, allModes []string) (*mappingrules.RuleTargetModeSelect, error) { func makeRuleTargetModeSelect(targetConfig configparser.RuleTargetConfigModeSelect, allModes []string) (*RuleTargetModeSelect, error) {
if ok := validateModes(targetConfig.Modes, allModes); !ok { if ok := validateModes(targetConfig.Modes, allModes); !ok {
return nil, errors.New("undefined mode in mode select list") return nil, errors.New("undefined mode in mode select list")
} }
return mappingrules.NewRuleTargetModeSelect(targetConfig.Modes) return NewRuleTargetModeSelect(targetConfig.Modes)
} }
// calculateDeadzones produces the deadzone start and end values in absolute terms // calculateDeadzones produces the deadzone start and end values in absolute terms
// TODO: on the one hand, this logic feels betten encapsulated in mappingrules. On the other hand, func calculateDeadzones(targetConfig configparser.RuleTargetConfigAxis, device Device, axis evdev.EvCode) (int32, int32, error) {
// passing even more parameters to NewRuleTargetAxis feels terrible
func calculateDeadzones(targetConfig RuleTargetConfigAxis, device Device, axis evdev.EvCode) (int32, int32, error) {
var deadzoneStart, deadzoneEnd int32 var deadzoneStart, deadzoneEnd int32
deadzoneStart = 0 deadzoneStart = 0
@ -101,8 +100,8 @@ func calculateDeadzones(targetConfig RuleTargetConfigAxis, device Device, axis e
absInfoMap, err := device.AbsInfos() absInfoMap, err := device.AbsInfos()
if err != nil { if err != nil {
min = mappingrules.AxisValueMin min = AxisValueMin
max = mappingrules.AxisValueMax max = AxisValueMax
} else { } else {
absInfo := absInfoMap[axis] absInfo := absInfoMap[axis]
min = absInfo.Minimum min = absInfo.Minimum
@ -139,3 +138,19 @@ func clampAndShift(start, end, min, max int32) (int32, int32) {
return start, end return start, end
} }
// 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
}

View file

@ -1,9 +1,10 @@
package configparser package mappingrules
import ( import (
"fmt" "fmt"
"testing" "testing"
"git.annabunches.net/annabunches/joyful/internal/configparser"
"github.com/holoplot/go-evdev" "github.com/holoplot/go-evdev"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@ -48,7 +49,7 @@ func (t *MakeRuleTargetsTests) SetupSuite() {
} }
func (t *MakeRuleTargetsTests) TestMakeRuleTargetButton() { func (t *MakeRuleTargetsTests) TestMakeRuleTargetButton() {
config := RuleTargetConfigButton{Device: "test"} config := configparser.RuleTargetConfigButton{Device: "test"}
t.Run("Standard keycode", func() { t.Run("Standard keycode", func() {
config.Button = "BTN_TRIGGER" config.Button = "BTN_TRIGGER"
@ -103,7 +104,7 @@ func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() {
for _, tc := range codeTestCases { for _, tc := range codeTestCases {
t.Run(fmt.Sprintf("KeyCode %s", tc.input), func() { t.Run(fmt.Sprintf("KeyCode %s", tc.input), func() {
config := RuleTargetConfigAxis{Device: "test"} config := configparser.RuleTargetConfigAxis{Device: "test"}
config.Axis = tc.input config.Axis = tc.input
rule, err := makeRuleTargetAxis(config, t.devs) rule, err := makeRuleTargetAxis(config, t.devs)
t.Nil(err) t.Nil(err)
@ -113,14 +114,14 @@ func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() {
} }
t.Run("Invalid code", func() { t.Run("Invalid code", func() {
config := RuleTargetConfigAxis{Device: "test"} config := configparser.RuleTargetConfigAxis{Device: "test"}
config.Axis = "foo" config.Axis = "foo"
_, err := makeRuleTargetAxis(config, t.devs) _, err := makeRuleTargetAxis(config, t.devs)
t.NotNil(err) t.NotNil(err)
}) })
t.Run("Invalid deadzone", func() { t.Run("Invalid deadzone", func() {
config := RuleTargetConfigAxis{Device: "test"} config := configparser.RuleTargetConfigAxis{Device: "test"}
config.Axis = "x" config.Axis = "x"
config.DeadzoneEnd = 100 config.DeadzoneEnd = 100
config.DeadzoneStart = 1000 config.DeadzoneStart = 1000
@ -141,7 +142,7 @@ func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() {
for _, tc := range relDeadzoneTestCases { for _, tc := range relDeadzoneTestCases {
t.Run(fmt.Sprintf("Relative Deadzone %d +- %d", tc.inCenter, tc.inSize), func() { t.Run(fmt.Sprintf("Relative Deadzone %d +- %d", tc.inCenter, tc.inSize), func() {
config := RuleTargetConfigAxis{ config := configparser.RuleTargetConfigAxis{
Device: "test", Device: "test",
Axis: "x", Axis: "x",
DeadzoneCenter: tc.inCenter, DeadzoneCenter: tc.inCenter,
@ -156,7 +157,7 @@ func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() {
} }
t.Run("Deadzone center/size invalid center", func() { t.Run("Deadzone center/size invalid center", func() {
config := RuleTargetConfigAxis{ config := configparser.RuleTargetConfigAxis{
Device: "test", Device: "test",
Axis: "x", Axis: "x",
DeadzoneCenter: 20000, DeadzoneCenter: 20000,
@ -179,7 +180,7 @@ func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() {
for _, tc := range relDeadzonePercentTestCases { for _, tc := range relDeadzonePercentTestCases {
t.Run(fmt.Sprintf("Relative percent deadzone %d +- %d%%", tc.inCenter, tc.inSizePercent), func() { t.Run(fmt.Sprintf("Relative percent deadzone %d +- %d%%", tc.inCenter, tc.inSizePercent), func() {
config := RuleTargetConfigAxis{ config := configparser.RuleTargetConfigAxis{
Device: "test", Device: "test",
Axis: "x", Axis: "x",
DeadzoneCenter: tc.inCenter, DeadzoneCenter: tc.inCenter,
@ -194,7 +195,7 @@ func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() {
} }
t.Run("Deadzone center/percent invalid center", func() { t.Run("Deadzone center/percent invalid center", func() {
config := RuleTargetConfigAxis{ config := configparser.RuleTargetConfigAxis{
Device: "test", Device: "test",
Axis: "x", Axis: "x",
DeadzoneCenter: 20000, DeadzoneCenter: 20000,
@ -206,7 +207,7 @@ func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() {
} }
func (t *MakeRuleTargetsTests) TestMakeRuleTargetRelaxis() { func (t *MakeRuleTargetsTests) TestMakeRuleTargetRelaxis() {
config := RuleTargetConfigRelaxis{Device: "test"} config := configparser.RuleTargetConfigRelaxis{Device: "test"}
t.Run("Standard keycode", func() { t.Run("Standard keycode", func() {
config.Axis = "REL_WHEEL" config.Axis = "REL_WHEEL"

View file

@ -0,0 +1,62 @@
package mappingrules
import (
"errors"
"fmt"
"strings"
"git.annabunches.net/annabunches/joyful/internal/configparser"
"git.annabunches.net/annabunches/joyful/internal/logger"
"github.com/holoplot/go-evdev"
)
func ConvertDeviceMap(inputDevs map[string]*evdev.InputDevice) map[string]Device {
// Golang can't inspect the concrete map type to determine interface conformance,
// so we handle that here.
devices := make(map[string]Device)
for name, dev := range inputDevs {
devices[name] = dev
}
return devices
}
// NewRule parses a RuleConfig struct and creates and returns the appropriate rule type.
// You can remap a map[string]*evdev.InputDevice to our interface type with ConvertDeviceMap
func NewRule(config configparser.RuleConfig, pDevs map[string]Device, vDevs map[string]Device, modes []string) (MappingRule, error) {
var newRule MappingRule
var err error
if !validateModes(config.Modes, modes) {
return nil, errors.New("mode list specifies undefined mode")
}
base := NewMappingRuleBase(config.Name, config.Modes)
switch strings.ToLower(config.Type) {
case RuleTypeButton:
newRule, err = NewMappingRuleButton(config.Config.(configparser.RuleConfigButton), pDevs, vDevs, base)
case RuleTypeButtonCombo:
newRule, err = NewMappingRuleButtonCombo(config.Config.(configparser.RuleConfigButtonCombo), pDevs, vDevs, base)
case RuleTypeButtonLatched:
newRule, err = NewMappingRuleButtonLatched(config.Config.(configparser.RuleConfigButtonLatched), pDevs, vDevs, base)
case RuleTypeAxis:
newRule, err = NewMappingRuleAxis(config.Config.(configparser.RuleConfigAxis), pDevs, vDevs, base)
case RuleTypeAxisCombined:
newRule, err = NewMappingRuleAxisCombined(config.Config.(configparser.RuleConfigAxisCombined), pDevs, vDevs, base)
case RuleTypeAxisToButton:
newRule, err = NewMappingRuleAxisToButton(config.Config.(configparser.RuleConfigAxisToButton), pDevs, vDevs, base)
case RuleTypeAxisToRelaxis:
newRule, err = NewMappingRuleAxisToRelaxis(config.Config.(configparser.RuleConfigAxisToRelaxis), pDevs, vDevs, base)
case RuleTypeModeSelect:
newRule, err = NewMappingRuleModeSelect(config.Config.(configparser.RuleConfigModeSelect), pDevs, modes, base)
default:
err = fmt.Errorf("bad rule type '%s' for rule '%s'", config.Type, config.Name)
}
if err != nil {
logger.LogErrorf(err, "Failed to build rule '%s'", config.Name)
return nil, err
}
return newRule, nil
}

View file

@ -1,6 +1,9 @@
package mappingrules package mappingrules
import "github.com/holoplot/go-evdev" import (
"git.annabunches.net/annabunches/joyful/internal/configparser"
"github.com/holoplot/go-evdev"
)
// 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 MappingRuleAxis struct { type MappingRuleAxis struct {
@ -9,12 +12,26 @@ type MappingRuleAxis struct {
Output *RuleTargetAxis Output *RuleTargetAxis
} }
func NewMappingRuleAxis(base MappingRuleBase, input *RuleTargetAxis, output *RuleTargetAxis) *MappingRuleAxis { func NewMappingRuleAxis(ruleConfig configparser.RuleConfigAxis,
pDevs map[string]Device,
vDevs map[string]Device,
base MappingRuleBase) (*MappingRuleAxis, error) {
input, err := makeRuleTargetAxis(ruleConfig.Input, pDevs)
if err != nil {
return nil, err
}
output, err := makeRuleTargetAxis(ruleConfig.Output, vDevs)
if err != nil {
return nil, err
}
return &MappingRuleAxis{ return &MappingRuleAxis{
MappingRuleBase: base, MappingRuleBase: base,
Input: input, Input: input,
Output: output, Output: output,
} }, nil
} }
func (rule *MappingRuleAxis) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { func (rule *MappingRuleAxis) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {

View file

@ -1,6 +1,7 @@
package mappingrules package mappingrules
import ( import (
"git.annabunches.net/annabunches/joyful/internal/configparser"
"git.annabunches.net/annabunches/joyful/internal/logger" "git.annabunches.net/annabunches/joyful/internal/logger"
"github.com/holoplot/go-evdev" "github.com/holoplot/go-evdev"
) )
@ -12,7 +13,26 @@ type MappingRuleAxisCombined struct {
Output *RuleTargetAxis Output *RuleTargetAxis
} }
func NewMappingRuleAxisCombined(base MappingRuleBase, inputLower *RuleTargetAxis, inputUpper *RuleTargetAxis, output *RuleTargetAxis) *MappingRuleAxisCombined { func NewMappingRuleAxisCombined(ruleConfig configparser.RuleConfigAxisCombined,
pDevs map[string]Device,
vDevs map[string]Device,
base MappingRuleBase) (*MappingRuleAxisCombined, error) {
inputLower, err := makeRuleTargetAxis(ruleConfig.InputLower, pDevs)
if err != nil {
return nil, err
}
inputUpper, err := makeRuleTargetAxis(ruleConfig.InputUpper, pDevs)
if err != nil {
return nil, err
}
output, err := makeRuleTargetAxis(ruleConfig.Output, vDevs)
if err != nil {
return nil, err
}
inputLower.OutputMax = 0 inputLower.OutputMax = 0
inputUpper.OutputMin = 0 inputUpper.OutputMin = 0
return &MappingRuleAxisCombined{ return &MappingRuleAxisCombined{
@ -20,7 +40,7 @@ func NewMappingRuleAxisCombined(base MappingRuleBase, inputLower *RuleTargetAxis
InputLower: inputLower, InputLower: inputLower,
InputUpper: inputUpper, InputUpper: inputUpper,
Output: output, Output: output,
} }, nil
} }
func (rule *MappingRuleAxisCombined) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { func (rule *MappingRuleAxisCombined) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {

View file

@ -38,7 +38,9 @@ func (t *MappingRuleAxisCombinedTests) SetupTest() {
}, nil) }, nil)
t.inputTargetLower, _ = NewRuleTargetAxis("test-input", t.inputDevice, evdev.ABS_X, true, 0, 0) t.inputTargetLower, _ = NewRuleTargetAxis("test-input", t.inputDevice, evdev.ABS_X, true, 0, 0)
t.inputTargetLower.OutputMax = 0
t.inputTargetUpper, _ = NewRuleTargetAxis("test-input", t.inputDevice, evdev.ABS_Y, false, 0, 0) t.inputTargetUpper, _ = NewRuleTargetAxis("test-input", t.inputDevice, evdev.ABS_Y, false, 0, 0)
t.inputTargetUpper.OutputMin = 0
t.outputDevice = &evdev.InputDevice{} t.outputDevice = &evdev.InputDevice{}
t.outputTarget, _ = NewRuleTargetAxis("test-output", t.outputDevice, evdev.ABS_X, false, 0, 0) t.outputTarget, _ = NewRuleTargetAxis("test-output", t.outputDevice, evdev.ABS_X, false, 0, 0)
@ -63,13 +65,23 @@ func (t *MappingRuleAxisCombinedTests) TestNewMappingRuleAxisCombined() {
evdev.ABS_Y: {Minimum: 0, Maximum: 10000}, evdev.ABS_Y: {Minimum: 0, Maximum: 10000},
}, nil) }, nil)
rule := NewMappingRuleAxisCombined(t.base, t.inputTargetLower, t.inputTargetUpper, t.outputTarget) rule := &MappingRuleAxisCombined{
MappingRuleBase: t.base,
InputLower: t.inputTargetLower,
InputUpper: t.inputTargetUpper,
Output: t.outputTarget,
}
t.EqualValues(0, rule.InputLower.OutputMax) t.EqualValues(0, rule.InputLower.OutputMax)
t.EqualValues(0, rule.InputUpper.OutputMin) t.EqualValues(0, rule.InputUpper.OutputMin)
} }
func (t *MappingRuleAxisCombinedTests) TestMatchEvent() { func (t *MappingRuleAxisCombinedTests) TestMatchEvent() {
rule := NewMappingRuleAxisCombined(t.base, t.inputTargetLower, t.inputTargetUpper, t.outputTarget) rule := &MappingRuleAxisCombined{
MappingRuleBase: t.base,
InputLower: t.inputTargetLower,
InputUpper: t.inputTargetUpper,
Output: t.outputTarget,
}
t.Run("Lower Input", func() { t.Run("Lower Input", func() {
testCases := []struct{ in, out int32 }{ testCases := []struct{ in, out int32 }{

View file

@ -3,6 +3,7 @@ package mappingrules
import ( import (
"time" "time"
"git.annabunches.net/annabunches/joyful/internal/configparser"
"github.com/holoplot/go-evdev" "github.com/holoplot/go-evdev"
"github.com/jonboulle/clockwork" "github.com/jonboulle/clockwork"
) )
@ -23,20 +24,34 @@ type MappingRuleAxisToButton struct {
clock clockwork.Clock clock clockwork.Clock
} }
func NewMappingRuleAxisToButton(base MappingRuleBase, input *RuleTargetAxis, output *RuleTargetButton, repeatRateMin, repeatRateMax int) *MappingRuleAxisToButton { func NewMappingRuleAxisToButton(ruleConfig configparser.RuleConfigAxisToButton,
pDevs map[string]Device,
vDevs map[string]Device,
base MappingRuleBase) (*MappingRuleAxisToButton, error) {
input, err := makeRuleTargetAxis(ruleConfig.Input, pDevs)
if err != nil {
return nil, err
}
output, err := makeRuleTargetButton(ruleConfig.Output, vDevs)
if err != nil {
return nil, err
}
return &MappingRuleAxisToButton{ return &MappingRuleAxisToButton{
MappingRuleBase: base, MappingRuleBase: base,
Input: input, Input: input,
Output: output, Output: output,
RepeatRateMin: repeatRateMin, RepeatRateMin: ruleConfig.RepeatRateMin,
RepeatRateMax: repeatRateMax, RepeatRateMax: ruleConfig.RepeatRateMax,
lastEvent: time.Now(), lastEvent: time.Now(),
nextEvent: NoNextEvent, nextEvent: NoNextEvent,
repeat: repeatRateMin != 0 && repeatRateMax != 0, repeat: ruleConfig.RepeatRateMin != 0 && ruleConfig.RepeatRateMax != 0,
pressed: false, pressed: false,
active: false, active: false,
clock: clockwork.NewRealClock(), clock: clockwork.NewRealClock(),
} }, nil
} }
func (rule *MappingRuleAxisToButton) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { func (rule *MappingRuleAxisToButton) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {

View file

@ -19,6 +19,44 @@ type MappingRuleAxisToButtonTests struct {
base MappingRuleBase base MappingRuleBase
} }
func TestRunnerMappingRuleAxisToButtonTests(t *testing.T) {
suite.Run(t, new(MappingRuleAxisToButtonTests))
}
// buildTimerRule creates a MappingRuleAxisToButton with a mocked clock
func (t *MappingRuleAxisToButtonTests) buildTimerRule(
repeatMin,
repeatMax int,
nextEvent time.Duration) (*MappingRuleAxisToButton, *clockwork.FakeClock) {
mockClock := clockwork.NewFakeClock()
testRule := t.buildRule(repeatMin, repeatMax)
testRule.clock = mockClock
testRule.lastEvent = testRule.clock.Now()
testRule.nextEvent = nextEvent
if nextEvent != NoNextEvent {
testRule.active = true
}
return testRule, mockClock
}
// Todo: don't love this repeated logic...
func (t *MappingRuleAxisToButtonTests) buildRule(repeatMin, repeatMax int) *MappingRuleAxisToButton {
return &MappingRuleAxisToButton{
MappingRuleBase: t.base,
Input: t.inputRule,
Output: t.outputRule,
RepeatRateMin: repeatMin,
RepeatRateMax: repeatMax,
lastEvent: time.Now(),
nextEvent: NoNextEvent,
repeat: repeatMin != 0 && repeatMax != 0,
pressed: false,
active: false,
clock: clockwork.NewRealClock(),
}
}
func (t *MappingRuleAxisToButtonTests) SetupTest() { func (t *MappingRuleAxisToButtonTests) SetupTest() {
mode := "*" mode := "*"
t.mode = &mode t.mode = &mode
@ -40,7 +78,7 @@ func (t *MappingRuleAxisToButtonTests) TestMatchEvent() {
// A valid input should set a nextevent // A valid input should set a nextevent
t.Run("No Repeat", func() { t.Run("No Repeat", func() {
testRule := NewMappingRuleAxisToButton(t.base, t.inputRule, t.outputRule, 0, 0) testRule := t.buildRule(0, 0)
t.Run("Valid Input", func() { t.Run("Valid Input", func() {
testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{ testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{
@ -62,7 +100,7 @@ func (t *MappingRuleAxisToButtonTests) TestMatchEvent() {
}) })
t.Run("Repeat", func() { t.Run("Repeat", func() {
testRule := NewMappingRuleAxisToButton(t.base, t.inputRule, t.outputRule, 750, 250) testRule := t.buildRule(750, 250)
testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{ testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{
Type: evdev.EV_ABS, Type: evdev.EV_ABS,
Code: evdev.ABS_X, Code: evdev.ABS_X,
@ -90,7 +128,7 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() {
t.Run("No Repeat", func() { t.Run("No Repeat", func() {
// Get event if called immediately // Get event if called immediately
t.Run("Event is available immediately", func() { t.Run("Event is available immediately", func() {
testRule, _ := buildTimerRule(t, 0, 0, 0) testRule, _ := t.buildTimerRule(0, 0, 0)
event := testRule.TimerEvent() event := testRule.TimerEvent()
@ -100,7 +138,7 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() {
// Off event on second call // Off event on second call
t.Run("Event emits off on second call", func() { t.Run("Event emits off on second call", func() {
testRule, _ := buildTimerRule(t, 0, 0, 0) testRule, _ := t.buildTimerRule(0, 0, 0)
testRule.TimerEvent() testRule.TimerEvent()
event := testRule.TimerEvent() event := testRule.TimerEvent()
@ -111,7 +149,7 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() {
// No further event, even if we wait a while // No further event, even if we wait a while
t.Run("Additional events are not emitted while still active.", func() { t.Run("Additional events are not emitted while still active.", func() {
testRule, mockClock := buildTimerRule(t, 0, 0, 0) testRule, mockClock := t.buildTimerRule(0, 0, 0)
testRule.TimerEvent() testRule.TimerEvent()
testRule.TimerEvent() testRule.TimerEvent()
@ -125,13 +163,13 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() {
t.Run("Repeat", func() { t.Run("Repeat", func() {
t.Run("No event if called immediately", func() { t.Run("No event if called immediately", func() {
testRule, _ := buildTimerRule(t, 100, 10, 50*time.Millisecond) testRule, _ := t.buildTimerRule(100, 10, 50*time.Millisecond)
event := testRule.TimerEvent() event := testRule.TimerEvent()
t.Nil(event) t.Nil(event)
}) })
t.Run("No event after 49ms", func() { t.Run("No event after 49ms", func() {
testRule, mockClock := buildTimerRule(t, 100, 10, 50*time.Millisecond) testRule, mockClock := t.buildTimerRule(100, 10, 50*time.Millisecond)
mockClock.Advance(49 * time.Millisecond) mockClock.Advance(49 * time.Millisecond)
event := testRule.TimerEvent() event := testRule.TimerEvent()
@ -140,7 +178,7 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() {
}) })
t.Run("Event after 50ms", func() { t.Run("Event after 50ms", func() {
testRule, mockClock := buildTimerRule(t, 100, 10, 50*time.Millisecond) testRule, mockClock := t.buildTimerRule(100, 10, 50*time.Millisecond)
mockClock.Advance(50 * time.Millisecond) mockClock.Advance(50 * time.Millisecond)
event := testRule.TimerEvent() event := testRule.TimerEvent()
@ -150,7 +188,7 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() {
}) })
t.Run("Additional event at 100ms", func() { t.Run("Additional event at 100ms", func() {
testRule, mockClock := buildTimerRule(t, 100, 10, 50*time.Millisecond) testRule, mockClock := t.buildTimerRule(100, 10, 50*time.Millisecond)
mockClock.Advance(50 * time.Millisecond) mockClock.Advance(50 * time.Millisecond)
testRule.TimerEvent() testRule.TimerEvent()
@ -163,24 +201,3 @@ func (t *MappingRuleAxisToButtonTests) TestTimerEvent() {
}) })
}) })
} }
func TestRunnerMappingRuleAxisToButtonTests(t *testing.T) {
suite.Run(t, new(MappingRuleAxisToButtonTests))
}
// buildTimerRule creates a MappingRuleAxisToButton with a mocked clock
func buildTimerRule(t *MappingRuleAxisToButtonTests,
repeatMin,
repeatMax int,
nextEvent time.Duration) (*MappingRuleAxisToButton, *clockwork.FakeClock) {
mockClock := clockwork.NewFakeClock()
testRule := NewMappingRuleAxisToButton(t.base, t.inputRule, t.outputRule, repeatMin, repeatMax)
testRule.clock = mockClock
testRule.lastEvent = testRule.clock.Now()
testRule.nextEvent = nextEvent
if nextEvent != NoNextEvent {
testRule.active = true
}
return testRule, mockClock
}

View file

@ -3,6 +3,7 @@ package mappingrules
import ( import (
"time" "time"
"git.annabunches.net/annabunches/joyful/internal/configparser"
"github.com/holoplot/go-evdev" "github.com/holoplot/go-evdev"
"github.com/jonboulle/clockwork" "github.com/jonboulle/clockwork"
) )
@ -23,23 +24,32 @@ type MappingRuleAxisToRelaxis struct {
clock clockwork.Clock clock clockwork.Clock
} }
func NewMappingRuleAxisToRelaxis( func NewMappingRuleAxisToRelaxis(ruleConfig configparser.RuleConfigAxisToRelaxis,
base MappingRuleBase, pDevs map[string]Device,
input *RuleTargetAxis, vDevs map[string]Device,
output *RuleTargetRelaxis, base MappingRuleBase) (*MappingRuleAxisToRelaxis, error) {
repeatRateMin, repeatRateMax, increment int) *MappingRuleAxisToRelaxis {
input, err := makeRuleTargetAxis(ruleConfig.Input, pDevs)
if err != nil {
return nil, err
}
output, err := makeRuleTargetRelaxis(ruleConfig.Output, vDevs)
if err != nil {
return nil, err
}
return &MappingRuleAxisToRelaxis{ return &MappingRuleAxisToRelaxis{
MappingRuleBase: base, MappingRuleBase: base,
Input: input, Input: input,
Output: output, Output: output,
RepeatRateMin: repeatRateMin, RepeatRateMin: ruleConfig.RepeatRateMin,
RepeatRateMax: repeatRateMax, RepeatRateMax: ruleConfig.RepeatRateMax,
Increment: int32(increment), Increment: int32(ruleConfig.Increment),
lastEvent: time.Now(), lastEvent: time.Now(),
nextEvent: NoNextEvent, nextEvent: NoNextEvent,
clock: clockwork.NewRealClock(), clock: clockwork.NewRealClock(),
} }, nil
} }
func (rule *MappingRuleAxisToRelaxis) MatchEvent( func (rule *MappingRuleAxisToRelaxis) MatchEvent(

View file

@ -1,6 +1,9 @@
package mappingrules package mappingrules
import "github.com/holoplot/go-evdev" import (
"git.annabunches.net/annabunches/joyful/internal/configparser"
"github.com/holoplot/go-evdev"
)
// 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 MappingRuleButton struct { type MappingRuleButton struct {
@ -9,16 +12,26 @@ type MappingRuleButton struct {
Output *RuleTargetButton Output *RuleTargetButton
} }
func NewMappingRuleButton( func NewMappingRuleButton(ruleConfig configparser.RuleConfigButton,
base MappingRuleBase, pDevs map[string]Device,
input *RuleTargetButton, vDevs map[string]Device,
output *RuleTargetButton) *MappingRuleButton { base MappingRuleBase) (*MappingRuleButton, error) {
input, err := makeRuleTargetButton(ruleConfig.Input, pDevs)
if err != nil {
return nil, err
}
output, err := makeRuleTargetButton(ruleConfig.Output, vDevs)
if err != nil {
return nil, err
}
return &MappingRuleButton{ return &MappingRuleButton{
MappingRuleBase: base, MappingRuleBase: base,
Input: input, Input: input,
Output: output, Output: output,
} }, nil
} }
func (rule *MappingRuleButton) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { func (rule *MappingRuleButton) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {

View file

@ -1,6 +1,9 @@
package mappingrules package mappingrules
import "github.com/holoplot/go-evdev" import (
"git.annabunches.net/annabunches/joyful/internal/configparser"
"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 MappingRuleButtonCombo struct { type MappingRuleButtonCombo struct {
@ -10,17 +13,31 @@ type MappingRuleButtonCombo struct {
State int State int
} }
func NewMappingRuleButtonCombo( func NewMappingRuleButtonCombo(ruleConfig configparser.RuleConfigButtonCombo,
base MappingRuleBase, pDevs map[string]Device,
inputs []*RuleTargetButton, vDevs map[string]Device,
output *RuleTargetButton) *MappingRuleButtonCombo { base MappingRuleBase) (*MappingRuleButtonCombo, error) {
inputs := make([]*RuleTargetButton, 0)
for _, inputConfig := range ruleConfig.Inputs {
input, err := makeRuleTargetButton(inputConfig, pDevs)
if err != nil {
return nil, err
}
inputs = append(inputs, input)
}
output, err := makeRuleTargetButton(ruleConfig.Output, vDevs)
if err != nil {
return nil, err
}
return &MappingRuleButtonCombo{ return &MappingRuleButtonCombo{
MappingRuleBase: base, MappingRuleBase: base,
Inputs: inputs, Inputs: inputs,
Output: output, Output: output,
State: 0, State: 0,
} }, nil
} }
func (rule *MappingRuleButtonCombo) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { func (rule *MappingRuleButtonCombo) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {

View file

@ -1,6 +1,9 @@
package mappingrules package mappingrules
import "github.com/holoplot/go-evdev" import (
"git.annabunches.net/annabunches/joyful/internal/configparser"
"github.com/holoplot/go-evdev"
)
type MappingRuleButtonLatched struct { type MappingRuleButtonLatched struct {
MappingRuleBase MappingRuleBase
@ -9,17 +12,27 @@ type MappingRuleButtonLatched struct {
State bool State bool
} }
func NewMappingRuleButtonLatched( func NewMappingRuleButtonLatched(ruleConfig configparser.RuleConfigButtonLatched,
base MappingRuleBase, pDevs map[string]Device,
input *RuleTargetButton, vDevs map[string]Device,
output *RuleTargetButton) *MappingRuleButtonLatched { base MappingRuleBase) (*MappingRuleButtonLatched, error) {
input, err := makeRuleTargetButton(ruleConfig.Input, pDevs)
if err != nil {
return nil, err
}
output, err := makeRuleTargetButton(ruleConfig.Output, vDevs)
if err != nil {
return nil, err
}
return &MappingRuleButtonLatched{ return &MappingRuleButtonLatched{
MappingRuleBase: base, MappingRuleBase: base,
Input: input, Input: input,
Output: output, Output: output,
State: false, State: false,
} }, nil
} }
func (rule *MappingRuleButtonLatched) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { func (rule *MappingRuleButtonLatched) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) {

View file

@ -28,7 +28,11 @@ func (t *MappingRuleButtonTests) SetupTest() {
func (t *MappingRuleButtonTests) TestMatchEvent() { func (t *MappingRuleButtonTests) TestMatchEvent() {
inputButton, _ := NewRuleTargetButton("", t.inputDevice, evdev.BTN_TRIGGER, false) inputButton, _ := NewRuleTargetButton("", t.inputDevice, evdev.BTN_TRIGGER, false)
outputButton, _ := NewRuleTargetButton("", t.outputDevice, evdev.BTN_TRIGGER, false) outputButton, _ := NewRuleTargetButton("", t.outputDevice, evdev.BTN_TRIGGER, false)
testRule := NewMappingRuleButton(t.base, inputButton, outputButton) testRule := &MappingRuleButton{
MappingRuleBase: t.base,
Input: inputButton,
Output: outputButton,
}
// A matching input event should produce an output event // A matching input event should produce an output event
expected := &evdev.InputEvent{ expected := &evdev.InputEvent{
@ -58,7 +62,11 @@ func (t *MappingRuleButtonTests) TestMatchEvent() {
func (t *MappingRuleButtonTests) TestMatchEventInverted() { func (t *MappingRuleButtonTests) TestMatchEventInverted() {
inputButton, _ := NewRuleTargetButton("", t.inputDevice, evdev.BTN_TRIGGER, true) inputButton, _ := NewRuleTargetButton("", t.inputDevice, evdev.BTN_TRIGGER, true)
outputButton, _ := NewRuleTargetButton("", t.outputDevice, evdev.BTN_TRIGGER, false) outputButton, _ := NewRuleTargetButton("", t.outputDevice, evdev.BTN_TRIGGER, false)
testRule := NewMappingRuleButton(t.base, inputButton, outputButton) testRule := &MappingRuleButton{
MappingRuleBase: t.base,
Input: inputButton,
Output: outputButton,
}
// A matching input event should produce an output event // A matching input event should produce an output event
expected := &evdev.InputEvent{ expected := &evdev.InputEvent{

View file

@ -1,6 +1,9 @@
package mappingrules package mappingrules
import "github.com/holoplot/go-evdev" import (
"git.annabunches.net/annabunches/joyful/internal/configparser"
"github.com/holoplot/go-evdev"
)
type MappingRuleModeSelect struct { type MappingRuleModeSelect struct {
MappingRuleBase MappingRuleBase
@ -8,17 +11,26 @@ type MappingRuleModeSelect struct {
Output *RuleTargetModeSelect Output *RuleTargetModeSelect
} }
func NewMappingRuleModeSelect( func NewMappingRuleModeSelect(ruleConfig configparser.RuleConfigModeSelect,
base MappingRuleBase, pDevs map[string]Device,
input *RuleTargetButton, modes []string,
output *RuleTargetModeSelect, base MappingRuleBase) (*MappingRuleModeSelect, error) {
) *MappingRuleModeSelect {
input, err := makeRuleTargetButton(ruleConfig.Input, pDevs)
if err != nil {
return nil, err
}
output, err := makeRuleTargetModeSelect(ruleConfig.Output, modes)
if err != nil {
return nil, err
}
return &MappingRuleModeSelect{ return &MappingRuleModeSelect{
MappingRuleBase: base, MappingRuleBase: base,
Input: input, Input: input,
Output: output, Output: output,
} }, nil
} }
func (rule *MappingRuleModeSelect) MatchEvent( func (rule *MappingRuleModeSelect) MatchEvent(

View file

@ -0,0 +1,12 @@
package mappingrules
const (
RuleTypeButton = "button"
RuleTypeButtonCombo = "button-combo"
RuleTypeButtonLatched = "button-latched"
RuleTypeAxis = "axis"
RuleTypeAxisCombined = "axis-combined"
RuleTypeAxisToButton = "axis-to-button"
RuleTypeAxisToRelaxis = "axis-to-relaxis"
RuleTypeModeSelect = "mode-select"
)