(WIP) Refactor marshal schema to be more declarative.

This commit is contained in:
Anna Rose Wiggins 2025-08-08 17:18:08 -04:00
parent 1a7b288083
commit 74f49c0dc6
7 changed files with 253 additions and 164 deletions

View file

@ -22,6 +22,8 @@ func (parser *ConfigParser) CreateVirtualDevices() map[string]*evdev.InputDevice
continue continue
} }
deviceConfig := deviceConfig.Config.(DeviceConfigVirtual)
name := fmt.Sprintf("joyful-%s", deviceConfig.Name) name := fmt.Sprintf("joyful-%s", deviceConfig.Name)
var capabilities map[evdev.EvType][]evdev.EvCode var capabilities map[evdev.EvType][]evdev.EvCode
@ -88,6 +90,8 @@ func (parser *ConfigParser) ConnectPhysicalDevices() map[string]*evdev.InputDevi
continue continue
} }
deviceConfig := deviceConfig.Config.(DeviceConfigPhysical)
var infoName string var infoName string
var device *evdev.InputDevice var device *evdev.InputDevice
var err error var err error

View file

@ -8,7 +8,7 @@ import (
"github.com/holoplot/go-evdev" "github.com/holoplot/go-evdev"
) )
func makeRuleTargetButton(targetConfig RuleTargetConfig, devs map[string]Device) (*mappingrules.RuleTargetButton, error) { func makeRuleTargetButton(targetConfig RuleTargetConfigButton, devs map[string]Device) (*mappingrules.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)
@ -27,7 +27,7 @@ func makeRuleTargetButton(targetConfig RuleTargetConfig, devs map[string]Device)
) )
} }
func makeRuleTargetAxis(targetConfig RuleTargetConfig, devs map[string]Device) (*mappingrules.RuleTargetAxis, error) { func makeRuleTargetAxis(targetConfig RuleTargetConfigAxis, devs map[string]Device) (*mappingrules.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)
@ -57,7 +57,7 @@ func makeRuleTargetAxis(targetConfig RuleTargetConfig, devs map[string]Device) (
) )
} }
func makeRuleTargetRelaxis(targetConfig RuleTargetConfig, devs map[string]Device) (*mappingrules.RuleTargetRelaxis, error) { func makeRuleTargetRelaxis(targetConfig RuleTargetConfigRelaxis, devs map[string]Device) (*mappingrules.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)
@ -72,11 +72,10 @@ func makeRuleTargetRelaxis(targetConfig RuleTargetConfig, devs map[string]Device
targetConfig.Device, targetConfig.Device,
device, device,
eventCode, eventCode,
targetConfig.Inverted,
) )
} }
func makeRuleTargetModeSelect(targetConfig RuleTargetConfig, allModes []string) (*mappingrules.RuleTargetModeSelect, error) { func makeRuleTargetModeSelect(targetConfig RuleTargetConfigModeSelect, allModes []string) (*mappingrules.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")
} }
@ -92,7 +91,7 @@ func hasError(_ any, err error) bool {
// 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, // TODO: on the one hand, this logic feels betten encapsulated in mappingrules. On the other hand,
// passing even more parameters to NewRuleTargetAxis feels terrible // passing even more parameters to NewRuleTargetAxis feels terrible
func calculateDeadzones(targetConfig RuleTargetConfig, device Device, axis evdev.EvCode) (int32, int32, error) { func calculateDeadzones(targetConfig RuleTargetConfigAxis, device Device, axis evdev.EvCode) (int32, int32, error) {
var deadzoneStart, deadzoneEnd int32 var deadzoneStart, deadzoneEnd int32
deadzoneStart = 0 deadzoneStart = 0

View file

@ -12,7 +12,6 @@ type MakeRuleTargetsTests struct {
suite.Suite suite.Suite
devs map[string]Device devs map[string]Device
deviceMock *DeviceMock deviceMock *DeviceMock
config RuleTargetConfig
} }
type DeviceMock struct { type DeviceMock struct {
@ -47,198 +46,198 @@ func (t *MakeRuleTargetsTests) SetupSuite() {
} }
} }
func (t *MakeRuleTargetsTests) SetupSubTest() {
t.config = RuleTargetConfig{
Device: "test",
}
}
func (t *MakeRuleTargetsTests) TestMakeRuleTargetButton() { func (t *MakeRuleTargetsTests) TestMakeRuleTargetButton() {
config := RuleTargetConfigButton{Device: "test"}
t.Run("Standard keycode", func() { t.Run("Standard keycode", func() {
t.config.Button = "BTN_TRIGGER" config.Button = "BTN_TRIGGER"
rule, err := makeRuleTargetButton(t.config, t.devs) rule, err := makeRuleTargetButton(config, t.devs)
t.Nil(err) t.Nil(err)
t.EqualValues(evdev.BTN_TRIGGER, rule.Button) t.EqualValues(evdev.BTN_TRIGGER, rule.Button)
}) })
t.Run("Hex code", func() { t.Run("Hex code", func() {
t.config.Button = "0x2fd" config.Button = "0x2fd"
rule, err := makeRuleTargetButton(t.config, t.devs) rule, err := makeRuleTargetButton(config, t.devs)
t.Nil(err) t.Nil(err)
t.EqualValues(evdev.EvCode(0x2fd), rule.Button) t.EqualValues(evdev.EvCode(0x2fd), rule.Button)
}) })
t.Run("Index", func() { t.Run("Index", func() {
t.config.Button = "3" config.Button = "3"
rule, err := makeRuleTargetButton(t.config, t.devs) rule, err := makeRuleTargetButton(config, t.devs)
t.Nil(err) t.Nil(err)
t.EqualValues(evdev.BTN_TOP, rule.Button) t.EqualValues(evdev.BTN_TOP, rule.Button)
}) })
t.Run("Index too high", func() { t.Run("Index too high", func() {
t.config.Button = "74" config.Button = "74"
_, err := makeRuleTargetButton(t.config, t.devs) _, err := makeRuleTargetButton(config, t.devs)
t.NotNil(err) t.NotNil(err)
}) })
t.Run("Un-prefixed keycode", func() { t.Run("Un-prefixed keycode", func() {
t.config.Button = "pinkie" config.Button = "pinkie"
rule, err := makeRuleTargetButton(t.config, t.devs) rule, err := makeRuleTargetButton(config, t.devs)
t.Nil(err) t.Nil(err)
t.EqualValues(evdev.BTN_PINKIE, rule.Button) t.EqualValues(evdev.BTN_PINKIE, rule.Button)
}) })
t.Run("Invalid keycode", func() { t.Run("Invalid keycode", func() {
t.config.Button = "foo" config.Button = "foo"
_, err := makeRuleTargetButton(t.config, t.devs) _, err := makeRuleTargetButton(config, t.devs)
t.NotNil(err) t.NotNil(err)
}) })
} }
func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() { func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() {
config := RuleTargetConfigAxis{Device: "test"}
t.Run("Standard code", func() { t.Run("Standard code", func() {
t.config.Axis = "ABS_X" config.Axis = "ABS_X"
rule, err := makeRuleTargetAxis(t.config, t.devs) rule, err := makeRuleTargetAxis(config, t.devs)
t.Nil(err) t.Nil(err)
t.EqualValues(evdev.ABS_X, rule.Axis) t.EqualValues(evdev.ABS_X, rule.Axis)
}) })
t.Run("Hex code", func() { t.Run("Hex code", func() {
t.config.Axis = "0x01" config.Axis = "0x01"
rule, err := makeRuleTargetAxis(t.config, t.devs) rule, err := makeRuleTargetAxis(config, t.devs)
t.Nil(err) t.Nil(err)
t.EqualValues(evdev.ABS_Y, rule.Axis) t.EqualValues(evdev.ABS_Y, rule.Axis)
}) })
t.Run("Un-prefixed code", func() { t.Run("Un-prefixed code", func() {
t.config.Axis = "x" config.Axis = "x"
rule, err := makeRuleTargetAxis(t.config, t.devs) rule, err := makeRuleTargetAxis(config, t.devs)
t.Nil(err) t.Nil(err)
t.EqualValues(evdev.ABS_X, rule.Axis) t.EqualValues(evdev.ABS_X, rule.Axis)
}) })
t.Run("Invalid code", func() { t.Run("Invalid code", func() {
t.config.Axis = "foo" config.Axis = "foo"
_, err := makeRuleTargetAxis(t.config, t.devs) _, err := makeRuleTargetAxis(config, t.devs)
t.NotNil(err) t.NotNil(err)
}) })
t.Run("Invalid deadzone", func() { t.Run("Invalid deadzone", func() {
t.config.Axis = "x" config.Axis = "x"
t.config.DeadzoneEnd = 100 config.DeadzoneEnd = 100
t.config.DeadzoneStart = 1000 config.DeadzoneStart = 1000
_, err := makeRuleTargetAxis(t.config, t.devs) _, err := makeRuleTargetAxis(config, t.devs)
t.NotNil(err) t.NotNil(err)
}) })
t.Run("Deadzone center/size", func() { t.Run("Deadzone center/size", func() {
t.config.Axis = "x" config.Axis = "x"
t.config.DeadzoneCenter = 5000 config.DeadzoneCenter = 5000
t.config.DeadzoneSize = 1000 config.DeadzoneSize = 1000
rule, err := makeRuleTargetAxis(t.config, t.devs) rule, err := makeRuleTargetAxis(config, t.devs)
t.Nil(err) t.Nil(err)
t.EqualValues(4500, rule.DeadzoneStart) t.EqualValues(4500, rule.DeadzoneStart)
t.EqualValues(5500, rule.DeadzoneEnd) t.EqualValues(5500, rule.DeadzoneEnd)
}) })
t.Run("Deadzone center/size lower boundary", func() { t.Run("Deadzone center/size lower boundary", func() {
t.config.Axis = "x" config.Axis = "x"
t.config.DeadzoneCenter = 0 config.DeadzoneCenter = 0
t.config.DeadzoneSize = 500 config.DeadzoneSize = 500
rule, err := makeRuleTargetAxis(t.config, t.devs) rule, err := makeRuleTargetAxis(config, t.devs)
t.Nil(err) t.Nil(err)
t.EqualValues(0, rule.DeadzoneStart) t.EqualValues(0, rule.DeadzoneStart)
t.EqualValues(500, rule.DeadzoneEnd) t.EqualValues(500, rule.DeadzoneEnd)
}) })
t.Run("Deadzone center/size upper boundary", func() { t.Run("Deadzone center/size upper boundary", func() {
t.config.Axis = "x" config.Axis = "x"
t.config.DeadzoneCenter = 10000 config.DeadzoneCenter = 10000
t.config.DeadzoneSize = 500 config.DeadzoneSize = 500
rule, err := makeRuleTargetAxis(t.config, t.devs) rule, err := makeRuleTargetAxis(config, t.devs)
t.Nil(err) t.Nil(err)
t.EqualValues(9500, rule.DeadzoneStart) t.EqualValues(9500, rule.DeadzoneStart)
t.EqualValues(10000, rule.DeadzoneEnd) t.EqualValues(10000, rule.DeadzoneEnd)
}) })
t.Run("Deadzone center/size invalid center", func() { t.Run("Deadzone center/size invalid center", func() {
t.config.Axis = "x" config.Axis = "x"
t.config.DeadzoneCenter = 20000 config.DeadzoneCenter = 20000
t.config.DeadzoneSize = 500 config.DeadzoneSize = 500
_, err := makeRuleTargetAxis(t.config, t.devs) _, err := makeRuleTargetAxis(config, t.devs)
t.NotNil(err) t.NotNil(err)
}) })
t.Run("Deadzone center/percent", func() { t.Run("Deadzone center/percent", func() {
t.config.Axis = "x" config.Axis = "x"
t.config.DeadzoneCenter = 5000 config.DeadzoneCenter = 5000
t.config.DeadzoneSizePercent = 10 config.DeadzoneSizePercent = 10
rule, err := makeRuleTargetAxis(t.config, t.devs) rule, err := makeRuleTargetAxis(config, t.devs)
t.Nil(err) t.Nil(err)
t.EqualValues(4500, rule.DeadzoneStart) t.EqualValues(4500, rule.DeadzoneStart)
t.EqualValues(5500, rule.DeadzoneEnd) t.EqualValues(5500, rule.DeadzoneEnd)
}) })
t.Run("Deadzone center/percent lower boundary", func() { t.Run("Deadzone center/percent lower boundary", func() {
t.config.Axis = "x" config.Axis = "x"
t.config.DeadzoneCenter = 0 config.DeadzoneCenter = 0
t.config.DeadzoneSizePercent = 10 config.DeadzoneSizePercent = 10
rule, err := makeRuleTargetAxis(t.config, t.devs) rule, err := makeRuleTargetAxis(config, t.devs)
t.Nil(err) t.Nil(err)
t.EqualValues(0, rule.DeadzoneStart) t.EqualValues(0, rule.DeadzoneStart)
t.EqualValues(1000, rule.DeadzoneEnd) t.EqualValues(1000, rule.DeadzoneEnd)
}) })
t.Run("Deadzone center/percent upper boundary", func() { t.Run("Deadzone center/percent upper boundary", func() {
t.config.Axis = "x" config.Axis = "x"
t.config.DeadzoneCenter = 10000 config.DeadzoneCenter = 10000
t.config.DeadzoneSizePercent = 10 config.DeadzoneSizePercent = 10
rule, err := makeRuleTargetAxis(t.config, t.devs) rule, err := makeRuleTargetAxis(config, t.devs)
t.Nil(err) t.Nil(err)
t.EqualValues(9000, rule.DeadzoneStart) t.EqualValues(9000, rule.DeadzoneStart)
t.EqualValues(10000, rule.DeadzoneEnd) t.EqualValues(10000, rule.DeadzoneEnd)
}) })
t.Run("Deadzone center/percent invalid center", func() { t.Run("Deadzone center/percent invalid center", func() {
t.config.Axis = "x" config.Axis = "x"
t.config.DeadzoneCenter = 20000 config.DeadzoneCenter = 20000
t.config.DeadzoneSizePercent = 10 config.DeadzoneSizePercent = 10
_, err := makeRuleTargetAxis(t.config, t.devs) _, err := makeRuleTargetAxis(config, t.devs)
t.NotNil(err) t.NotNil(err)
}) })
} }
func (t *MakeRuleTargetsTests) TestMakeRuleTargetRelaxis() { func (t *MakeRuleTargetsTests) TestMakeRuleTargetRelaxis() {
config := RuleTargetConfigRelaxis{Device: "test"}
t.Run("Standard keycode", func() { t.Run("Standard keycode", func() {
t.config.Axis = "REL_WHEEL" config.Axis = "REL_WHEEL"
rule, err := makeRuleTargetRelaxis(t.config, t.devs) rule, err := makeRuleTargetRelaxis(config, t.devs)
t.Nil(err) t.Nil(err)
t.EqualValues(evdev.REL_WHEEL, rule.Axis) t.EqualValues(evdev.REL_WHEEL, rule.Axis)
}) })
t.Run("Hex keycode", func() { t.Run("Hex keycode", func() {
t.config.Axis = "0x00" config.Axis = "0x00"
rule, err := makeRuleTargetRelaxis(t.config, t.devs) rule, err := makeRuleTargetRelaxis(config, t.devs)
t.Nil(err) t.Nil(err)
t.EqualValues(evdev.REL_X, rule.Axis) t.EqualValues(evdev.REL_X, rule.Axis)
}) })
t.Run("Un-prefixed keycode", func() { t.Run("Un-prefixed keycode", func() {
t.config.Axis = "wheel" config.Axis = "wheel"
rule, err := makeRuleTargetRelaxis(t.config, t.devs) rule, err := makeRuleTargetRelaxis(config, t.devs)
t.Nil(err) t.Nil(err)
t.EqualValues(evdev.REL_WHEEL, rule.Axis) t.EqualValues(evdev.REL_WHEEL, rule.Axis)
}) })
t.Run("Invalid keycode", func() { t.Run("Invalid keycode", func() {
t.config.Axis = "foo" config.Axis = "foo"
_, err := makeRuleTargetRelaxis(t.config, t.devs) _, err := makeRuleTargetRelaxis(config, t.devs)
t.NotNil(err) t.NotNil(err)
}) })
t.Run("Incorrect axis type", func() { t.Run("Incorrect axis type", func() {
t.config.Axis = "ABS_X" config.Axis = "ABS_X"
_, err := makeRuleTargetRelaxis(t.config, t.devs) _, err := makeRuleTargetRelaxis(config, t.devs)
t.NotNil(err) t.NotNil(err)
}) })
} }

View file

@ -42,21 +42,21 @@ func (parser *ConfigParser) BuildRules(pInputDevs map[string]*evdev.InputDevice,
switch strings.ToLower(ruleConfig.Type) { switch strings.ToLower(ruleConfig.Type) {
case RuleTypeButton: case RuleTypeButton:
newRule, err = makeMappingRuleButton(ruleConfig, pDevs, vDevs, base) newRule, err = makeMappingRuleButton(ruleConfig.Config.(RuleConfigButton), pDevs, vDevs, base)
case RuleTypeButtonCombo: case RuleTypeButtonCombo:
newRule, err = makeMappingRuleCombo(ruleConfig, pDevs, vDevs, base) newRule, err = makeMappingRuleCombo(ruleConfig.Config.(RuleConfigButtonCombo), pDevs, vDevs, base)
case RuleTypeLatched: case RuleTypeButtonLatched:
newRule, err = makeMappingRuleLatched(ruleConfig, pDevs, vDevs, base) newRule, err = makeMappingRuleLatched(ruleConfig.Config.(RuleConfigButtonLatched), pDevs, vDevs, base)
case RuleTypeAxis: case RuleTypeAxis:
newRule, err = makeMappingRuleAxis(ruleConfig, pDevs, vDevs, base) newRule, err = makeMappingRuleAxis(ruleConfig.Config.(RuleConfigAxis), pDevs, vDevs, base)
case RuleTypeAxisCombined: case RuleTypeAxisCombined:
newRule, err = makeMappingRuleAxisCombined(ruleConfig, pDevs, vDevs, base) newRule, err = makeMappingRuleAxisCombined(ruleConfig.Config.(RuleConfigAxisCombined), pDevs, vDevs, base)
case RuleTypeAxisToButton: case RuleTypeAxisToButton:
newRule, err = makeMappingRuleAxisToButton(ruleConfig, pDevs, vDevs, base) newRule, err = makeMappingRuleAxisToButton(ruleConfig.Config.(RuleConfigAxisToButton), pDevs, vDevs, base)
case RuleTypeAxisToRelaxis: case RuleTypeAxisToRelaxis:
newRule, err = makeMappingRuleAxisToRelaxis(ruleConfig, pDevs, vDevs, base) newRule, err = makeMappingRuleAxisToRelaxis(ruleConfig.Config.(RuleConfigAxisToRelaxis), pDevs, vDevs, base)
case RuleTypeModeSelect: case RuleTypeModeSelect:
newRule, err = makeMappingRuleModeSelect(ruleConfig, pDevs, modes, base) newRule, err = makeMappingRuleModeSelect(ruleConfig.Config.(RuleConfigModeSelect), pDevs, modes, base)
default: default:
err = fmt.Errorf("bad rule type '%s' for rule '%s'", ruleConfig.Type, ruleConfig.Name) err = fmt.Errorf("bad rule type '%s' for rule '%s'", ruleConfig.Type, ruleConfig.Name)
} }
@ -72,7 +72,14 @@ func (parser *ConfigParser) BuildRules(pInputDevs map[string]*evdev.InputDevice,
return rules return rules
} }
func makeMappingRuleButton(ruleConfig RuleConfig, // 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, pDevs map[string]Device,
vDevs map[string]Device, vDevs map[string]Device,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButton, error) { base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButton, error) {
@ -90,7 +97,7 @@ func makeMappingRuleButton(ruleConfig RuleConfig,
return mappingrules.NewMappingRuleButton(base, input, output), nil return mappingrules.NewMappingRuleButton(base, input, output), nil
} }
func makeMappingRuleCombo(ruleConfig RuleConfig, func makeMappingRuleCombo(ruleConfig RuleConfigButtonCombo,
pDevs map[string]Device, pDevs map[string]Device,
vDevs map[string]Device, vDevs map[string]Device,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButtonCombo, error) { base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButtonCombo, error) {
@ -112,7 +119,7 @@ func makeMappingRuleCombo(ruleConfig RuleConfig,
return mappingrules.NewMappingRuleButtonCombo(base, inputs, output), nil return mappingrules.NewMappingRuleButtonCombo(base, inputs, output), nil
} }
func makeMappingRuleLatched(ruleConfig RuleConfig, func makeMappingRuleLatched(ruleConfig RuleConfigButtonLatched,
pDevs map[string]Device, pDevs map[string]Device,
vDevs map[string]Device, vDevs map[string]Device,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButtonLatched, error) { base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButtonLatched, error) {
@ -130,7 +137,7 @@ func makeMappingRuleLatched(ruleConfig RuleConfig,
return mappingrules.NewMappingRuleButtonLatched(base, input, output), nil return mappingrules.NewMappingRuleButtonLatched(base, input, output), nil
} }
func makeMappingRuleAxis(ruleConfig RuleConfig, func makeMappingRuleAxis(ruleConfig RuleConfigAxis,
pDevs map[string]Device, pDevs map[string]Device,
vDevs map[string]Device, vDevs map[string]Device,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxis, error) { base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxis, error) {
@ -148,7 +155,7 @@ func makeMappingRuleAxis(ruleConfig RuleConfig,
return mappingrules.NewMappingRuleAxis(base, input, output), nil return mappingrules.NewMappingRuleAxis(base, input, output), nil
} }
func makeMappingRuleAxisCombined(ruleConfig RuleConfig, func makeMappingRuleAxisCombined(ruleConfig RuleConfigAxisCombined,
pDevs map[string]Device, pDevs map[string]Device,
vDevs map[string]Device, vDevs map[string]Device,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxisCombined, error) { base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxisCombined, error) {
@ -171,7 +178,7 @@ func makeMappingRuleAxisCombined(ruleConfig RuleConfig,
return mappingrules.NewMappingRuleAxisCombined(base, inputLower, inputUpper, output), nil return mappingrules.NewMappingRuleAxisCombined(base, inputLower, inputUpper, output), nil
} }
func makeMappingRuleAxisToButton(ruleConfig RuleConfig, func makeMappingRuleAxisToButton(ruleConfig RuleConfigAxisToButton,
pDevs map[string]Device, pDevs map[string]Device,
vDevs map[string]Device, vDevs map[string]Device,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxisToButton, error) { base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxisToButton, error) {
@ -189,7 +196,7 @@ func makeMappingRuleAxisToButton(ruleConfig RuleConfig,
return mappingrules.NewMappingRuleAxisToButton(base, input, output, ruleConfig.RepeatRateMin, ruleConfig.RepeatRateMax), nil return mappingrules.NewMappingRuleAxisToButton(base, input, output, ruleConfig.RepeatRateMin, ruleConfig.RepeatRateMax), nil
} }
func makeMappingRuleAxisToRelaxis(ruleConfig RuleConfig, func makeMappingRuleAxisToRelaxis(ruleConfig RuleConfigAxisToRelaxis,
pDevs map[string]Device, pDevs map[string]Device,
vDevs map[string]Device, vDevs map[string]Device,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxisToRelaxis, error) { base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxisToRelaxis, error) {
@ -211,7 +218,7 @@ func makeMappingRuleAxisToRelaxis(ruleConfig RuleConfig,
ruleConfig.Increment), nil ruleConfig.Increment), nil
} }
func makeMappingRuleModeSelect(ruleConfig RuleConfig, func makeMappingRuleModeSelect(ruleConfig RuleConfigModeSelect,
pDevs map[string]Device, pDevs map[string]Device,
modes []string, modes []string,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleModeSelect, error) { base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleModeSelect, error) {

View file

@ -1,25 +1,39 @@
// These types comprise the YAML schema for configuring Joyful. // These types comprise the YAML schema for configuring Joyful.
// The config files will be combined and then unmarshalled into this // The config files will be combined and then unmarshalled into this
//
// TODO: currently the types in here aren't especially strong; each one is
// decomposed into a different object based on the Type fields. We should implement
// some sort of delayed unmarshalling technique, for example see ideas at
// https://stackoverflow.com/questions/70635636/unmarshaling-yaml-into-different-struct-based-off-yaml-field
// Then we can be more explicit about the interface here.
package config package config
import "fmt"
type Config struct { type Config struct {
Devices []DeviceConfig `yaml:"devices"` Devices []DeviceConfig
Modes []string `yaml:"modes,omitempty"` Modes []string
Rules []RuleConfig `yaml:"rules"` Rules []RuleConfig
} }
// These structs use custom unmarshaling to inline each available sub-type
type DeviceConfig struct { type DeviceConfig struct {
Type string
Config interface{} `yaml:",inline"`
}
type RuleConfig struct {
Type string
Name string
Modes []string
Config interface{} `yaml:",inline"`
}
type DeviceConfigPhysical struct {
Name string `yaml:"name"`
DeviceName string `yaml:"device_name,omitempty"`
DevicePath string `yaml:"device_path,omitempty"`
Lock bool `yaml:"lock,omitempty"`
}
// TODO: configure custom unmarshaling so we can overload Buttons, Axes, and RelativeAxes...
type DeviceConfigVirtual struct {
Name string `yaml:"name"` Name string `yaml:"name"`
Type string `yaml:"type"`
DeviceName string `yaml:"device_name,omitempty"`
DevicePath string `yaml:"device_path,omitempty"`
Preset string `yaml:"preset,omitempty"` Preset string `yaml:"preset,omitempty"`
NumButtons int `yaml:"num_buttons,omitempty"` NumButtons int `yaml:"num_buttons,omitempty"`
NumAxes int `yaml:"num_axes,omitempty"` NumAxes int `yaml:"num_axes,omitempty"`
@ -27,53 +41,130 @@ type DeviceConfig struct {
Buttons []string `yaml:"buttons,omitempty"` Buttons []string `yaml:"buttons,omitempty"`
Axes []string `yaml:"axes,omitempty"` Axes []string `yaml:"axes,omitempty"`
RelativeAxes []string `yaml:"rel_axes,omitempty"` RelativeAxes []string `yaml:"rel_axes,omitempty"`
Lock bool `yaml:"lock,omitempty"`
} }
type RuleConfig struct { type RuleConfigButton struct {
Name string `yaml:"name,omitempty"` Input RuleTargetConfigButton
Type string `yaml:"type"` Output RuleTargetConfigButton
Input RuleTargetConfig `yaml:"input,omitempty"`
InputLower RuleTargetConfig `yaml:"input_lower,omitempty"`
InputUpper RuleTargetConfig `yaml:"input_upper,omitempty"`
Inputs []RuleTargetConfig `yaml:"inputs,omitempty"`
Output RuleTargetConfig `yaml:"output"`
Modes []string `yaml:"modes,omitempty"`
RepeatRateMin int `yaml:"repeat_rate_min,omitempty"`
RepeatRateMax int `yaml:"repeat_rate_max,omitempty"`
Increment int `yaml:"increment,omitempty"`
} }
type RuleTargetConfig struct { type RuleConfigButtonCombo struct {
Device string `yaml:"device,omitempty"` Inputs []RuleTargetConfigButton
Button string `yaml:"button,omitempty"` Output RuleTargetConfigButton
Axis string `yaml:"axis,omitempty"` }
DeadzoneCenter int32 `yaml:"deadzone_center,omitempty"`
DeadzoneSize int32 `yaml:"deadzone_size,omitempty"` type RuleConfigButtonLatched struct {
DeadzoneSizePercent int32 `yaml:"deadzone_size_percent,omitempty"` Input RuleTargetConfigButton
DeadzoneStart int32 `yaml:"deadzone_start,omitempty"` Output RuleTargetConfigButton
DeadzoneEnd int32 `yaml:"deadzone_end,omitempty"` }
Inverted bool `yaml:"inverted,omitempty"`
Modes []string `yaml:"modes,omitempty"` type RuleConfigAxis struct {
Input RuleTargetConfigAxis
Output RuleTargetConfigAxis
}
type RuleConfigAxisCombined struct {
InputLower RuleTargetConfigAxis `yaml:"input_lower,omitempty"`
InputUpper RuleTargetConfigAxis `yaml:"input_upper,omitempty"`
Output RuleTargetConfigAxis
}
type RuleConfigAxisToButton struct {
RepeatRateMin int `yaml:"repeat_rate_min,omitempty"`
RepeatRateMax int `yaml:"repeat_rate_max,omitempty"`
Input RuleTargetConfigAxis
Output RuleTargetConfigButton
}
type RuleConfigAxisToRelaxis struct {
RepeatRateMin int `yaml:"repeat_rate_min"`
RepeatRateMax int `yaml:"repeat_rate_max"`
Increment int
Input RuleTargetConfigAxis
Output RuleTargetConfigRelaxis
}
type RuleConfigModeSelect struct {
Input RuleTargetConfigButton
Output RuleTargetConfigModeSelect
}
type RuleTargetConfigButton struct {
Device string
Button string
Inverted bool
}
type RuleTargetConfigAxis struct {
Device string
Axis string
DeadzoneCenter int32 `yaml:"deadzone_center,omitempty"`
DeadzoneSize int32 `yaml:"deadzone_size,omitempty"`
DeadzoneSizePercent int32 `yaml:"deadzone_size_percent,omitempty"`
DeadzoneStart int32 `yaml:"deadzone_start,omitempty"`
DeadzoneEnd int32 `yaml:"deadzone_end,omitempty"`
Inverted bool
}
type RuleTargetConfigRelaxis struct {
Device string
Axis string
}
type RuleTargetConfigModeSelect struct {
Modes []string
}
func (dc *DeviceConfig) UnmarshalYAML(unmarshal func(data interface{}) error) error {
var config interface{}
dc.Config = config
switch dc.Type {
case DeviceTypePhysical:
config = &DeviceConfigPhysical{}
case DeviceTypeVirtual:
config = &DeviceConfigVirtual{}
default:
return fmt.Errorf("invalid device type '%s'", dc.Type)
}
return unmarshal(&config)
}
func (dc *RuleConfig) UnmarshalYAML(unmarshal func(data interface{}) error) error {
switch dc.Type {
case RuleTypeButton:
dc.Config = &RuleConfigButton{}
case RuleTypeButtonCombo:
dc.Config = &RuleConfigButtonCombo{}
case RuleTypeButtonLatched:
dc.Config = &RuleConfigButtonLatched{}
case RuleTypeAxis:
dc.Config = &RuleConfigAxis{}
case RuleTypeAxisCombined:
dc.Config = &RuleConfigAxisCombined{}
case RuleTypeAxisToButton:
dc.Config = &RuleConfigAxisToButton{}
case RuleTypeAxisToRelaxis:
dc.Config = &RuleConfigAxisToRelaxis{}
default:
return fmt.Errorf("invalid rule type '%s'", dc.Type)
}
return unmarshal(&dc.Config)
} }
// TODO: custom yaml unmarshaling is obtuse; do we really need to do all of this work // TODO: custom yaml unmarshaling is obtuse; do we really need to do all of this work
// just to set a single default value? // just to set a single default value?
func (dc *DeviceConfig) UnmarshalYAML(unmarshal func(data interface{}) error) error { func (dc *DeviceConfigPhysical) UnmarshalYAML(unmarshal func(data interface{}) error) error {
var raw struct { var raw struct {
Name string Name string
Type string DeviceName string `yaml:"device_name"`
DeviceName string `yaml:"device_name"` DevicePath string `yaml:"device_path"`
DevicePath string `yaml:"device_path"` Lock bool `yaml:"lock,omitempty"`
Preset string
NumButtons int `yaml:"num_buttons"`
NumAxes int `yaml:"num_axes"`
NumRelativeAxes int `yaml:"num_rel_axes"`
Buttons []string
Axes []string
RelativeAxes []string `yaml:"relative_axes"`
Lock bool `yaml:"lock,omitempty"`
} }
// Set non-standard defaults
raw.Lock = true raw.Lock = true
err := unmarshal(&raw) err := unmarshal(&raw)
@ -81,19 +172,11 @@ func (dc *DeviceConfig) UnmarshalYAML(unmarshal func(data interface{}) error) er
return err return err
} }
*dc = DeviceConfig{ *dc = DeviceConfigPhysical{
Name: raw.Name, Name: raw.Name,
Type: raw.Type, DeviceName: raw.DeviceName,
DeviceName: raw.DeviceName, DevicePath: raw.DevicePath,
DevicePath: raw.DevicePath, Lock: raw.Lock,
Preset: raw.Preset,
NumButtons: raw.NumButtons,
NumAxes: raw.NumAxes,
NumRelativeAxes: raw.NumRelativeAxes,
Buttons: raw.Buttons,
Axes: raw.Axes,
RelativeAxes: raw.RelativeAxes,
Lock: raw.Lock,
} }
return nil return nil
} }

View file

@ -15,12 +15,12 @@ const (
RuleTypeButton = "button" RuleTypeButton = "button"
RuleTypeButtonCombo = "button-combo" RuleTypeButtonCombo = "button-combo"
RuleTypeLatched = "button-latched" RuleTypeButtonLatched = "button-latched"
RuleTypeAxis = "axis" RuleTypeAxis = "axis"
RuleTypeAxisCombined = "axis-combined" RuleTypeAxisCombined = "axis-combined"
RuleTypeModeSelect = "mode-select"
RuleTypeAxisToButton = "axis-to-button" RuleTypeAxisToButton = "axis-to-button"
RuleTypeAxisToRelaxis = "axis-to-relaxis" RuleTypeAxisToRelaxis = "axis-to-relaxis"
RuleTypeModeSelect = "mode-select"
CodePrefixButton = "BTN" CodePrefixButton = "BTN"
CodePrefixKey = "KEY" CodePrefixKey = "KEY"

View file

@ -8,19 +8,16 @@ type RuleTargetRelaxis struct {
DeviceName string DeviceName string
Device Device Device Device
Axis evdev.EvCode Axis evdev.EvCode
Inverted bool
} }
func NewRuleTargetRelaxis(device_name string, func NewRuleTargetRelaxis(device_name string,
device Device, device Device,
axis evdev.EvCode, axis evdev.EvCode) (*RuleTargetRelaxis, error) {
inverted bool) (*RuleTargetRelaxis, error) {
return &RuleTargetRelaxis{ return &RuleTargetRelaxis{
DeviceName: device_name, DeviceName: device_name,
Device: device, Device: device,
Axis: axis, Axis: axis,
Inverted: inverted,
}, nil }, nil
} }