From 97a1acd2286f56e12570a43bad3d1f01e8eb0794 Mon Sep 17 00:00:00 2001 From: Anna Rose Wiggins Date: Fri, 18 Jul 2025 23:10:12 +0000 Subject: [PATCH] Add more deadzone specification options. (#9) Reviewed-on: https://git.annabunches.net/anna/joyful/pulls/9 Co-authored-by: Anna Rose Wiggins Co-committed-by: Anna Rose Wiggins --- .vscode/tasks.json | 17 ++ docs/examples/ruletypes.yml | 28 +++ docs/readme.md | 10 + internal/config/interfaces.go | 7 + internal/config/make_rule_targets.go | 74 ++++++- internal/config/make_rule_targets_test.go | 199 +++++++++++++----- internal/config/make_rules.go | 41 ++-- internal/config/schema.go | 17 +- internal/mappingrules/interfaces.go | 10 +- internal/mappingrules/mapping_rule_axis.go | 2 +- .../mapping_rule_axis_to_button.go | 4 +- .../mapping_rule_axis_to_relaxis.go | 2 +- internal/mappingrules/mapping_rule_button.go | 4 +- .../mappingrules/mapping_rule_button_combo.go | 6 +- .../mapping_rule_button_latched.go | 4 +- .../mappingrules/mapping_rule_mode_select.go | 2 +- internal/mappingrules/rule_target_axis.go | 8 +- internal/mappingrules/rule_target_button.go | 6 +- internal/mappingrules/rule_target_relaxis.go | 6 +- readme.md | 5 +- 20 files changed, 344 insertions(+), 108 deletions(-) create mode 100644 internal/config/interfaces.go diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ad0bca5..43ac506 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -28,6 +28,23 @@ } }, "problemMatcher": [] + }, + { + "label": "Test Project", + "args": [ + "test", + "./..." + ], + "group": { + "kind": "test", + "isDefault": true + }, + "options": { + "env": { + "CGO_ENABLED": "0" + } + }, + "problemMatcher": [] } ], } \ No newline at end of file diff --git a/docs/examples/ruletypes.yml b/docs/examples/ruletypes.yml index 74a6b67..2c976e4 100644 --- a/docs/examples/ruletypes.yml +++ b/docs/examples/ruletypes.yml @@ -26,6 +26,34 @@ rules: device: main axis: ABS_X + - type: axis + input: + device: flightstick + # An alternate way to specify deadzones is to define the deadzone's center and then a + # size value. This will create a deadzone that covers a range of deadzone_size, + # centered on the center value. Note that if your deadzone_center is at the lower or upper end + # of the axis, the total size will still be as given; the deadzone will be "shifted" into bounds. + deadzone_center: 29000 + deadzone_size: 2000 + inverted: false + axis: Y # The ABS_ prefix is optional + output: + device: main + axis: ABS_Y + + - type: axis + input: + device: flightstick + # A final way to specify deadzones is to use a size percentage instead of an absolute size. + # This works exactly like deadzone_size, but calculates a percentage of the axis' total range. + deadzone_center: 29000 + deadzone_size_percent: 5 + inverted: false + axis: Y # The ABS_ prefix is optional + output: + device: main + axis: ABS_Y + # Straightforward button mapping - type: button input: diff --git a/docs/readme.md b/docs/readme.md index 340bcd3..f777d04 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -55,6 +55,16 @@ For input, you can figure out what event codes your device is emitting by runnin evtest | grep BTN_ ``` +### Axis Deadzones + +For most axis inputs, you will want to define deadzones. There are three possible approaches: + +* Define `deadzone_start` and `deadzone_end` to explicitly set the deadzone bounds. +* Define `deadzone_center` and `deadzone_size`; this will create a deadzone of the indicated size centered at the given axis position. +* Define `deadzone_center` and `deadzone_size_percent` to use a percentage of the total axis size. + +See for usage examples. + ## Modes Modes are optional, and also have the simplest configuration. To define modes, add this to your configuration: diff --git a/internal/config/interfaces.go b/internal/config/interfaces.go new file mode 100644 index 0000000..0b9fa42 --- /dev/null +++ b/internal/config/interfaces.go @@ -0,0 +1,7 @@ +package config + +import "github.com/holoplot/go-evdev" + +type Device interface { + AbsInfos() (map[evdev.EvCode]evdev.AbsInfo, error) +} diff --git a/internal/config/make_rule_targets.go b/internal/config/make_rule_targets.go index 5b38347..73f184d 100644 --- a/internal/config/make_rule_targets.go +++ b/internal/config/make_rule_targets.go @@ -4,11 +4,12 @@ import ( "errors" "fmt" + "git.annabunches.net/annabunches/joyful/internal/logger" "git.annabunches.net/annabunches/joyful/internal/mappingrules" "github.com/holoplot/go-evdev" ) -func makeRuleTargetButton(targetConfig RuleTargetConfig, devs map[string]*evdev.InputDevice) (*mappingrules.RuleTargetButton, error) { +func makeRuleTargetButton(targetConfig RuleTargetConfig, devs map[string]Device) (*mappingrules.RuleTargetButton, error) { device, ok := devs[targetConfig.Device] if !ok { return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device) @@ -27,7 +28,7 @@ func makeRuleTargetButton(targetConfig RuleTargetConfig, devs map[string]*evdev. ) } -func makeRuleTargetAxis(targetConfig RuleTargetConfig, devs map[string]*evdev.InputDevice) (*mappingrules.RuleTargetAxis, error) { +func makeRuleTargetAxis(targetConfig RuleTargetConfig, devs map[string]Device) (*mappingrules.RuleTargetAxis, error) { device, ok := devs[targetConfig.Device] if !ok { return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device) @@ -42,17 +43,22 @@ func makeRuleTargetAxis(targetConfig RuleTargetConfig, devs map[string]*evdev.In return nil, err } + deadzoneStart, deadzoneEnd, err := calculateDeadzones(targetConfig, device, eventCode) + if err != nil { + return nil, err + } + return mappingrules.NewRuleTargetAxis( targetConfig.Device, device, eventCode, targetConfig.Inverted, - targetConfig.DeadzoneStart, - targetConfig.DeadzoneEnd, + deadzoneStart, + deadzoneEnd, ) } -func makeRuleTargetRelaxis(targetConfig RuleTargetConfig, devs map[string]*evdev.InputDevice) (*mappingrules.RuleTargetRelaxis, error) { +func makeRuleTargetRelaxis(targetConfig RuleTargetConfig, devs map[string]Device) (*mappingrules.RuleTargetRelaxis, error) { device, ok := devs[targetConfig.Device] if !ok { return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device) @@ -83,3 +89,61 @@ func makeRuleTargetModeSelect(targetConfig RuleTargetConfig, allModes []string) func hasError(_ any, err error) bool { return err != nil } + +// 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, +// passing even more parameters to NewRuleTargetAxis feels terrible +func calculateDeadzones(targetConfig RuleTargetConfig, device Device, axis evdev.EvCode) (int32, int32, error) { + + var deadzoneStart, deadzoneEnd int32 + deadzoneStart = 0 + deadzoneEnd = 0 + + if targetConfig.DeadzoneStart != 0 || targetConfig.DeadzoneEnd != 0 { + return targetConfig.DeadzoneStart, targetConfig.DeadzoneEnd, nil + } + + var min, max int32 + absInfoMap, err := device.AbsInfos() + + if err != nil { + min = mappingrules.AxisValueMin + max = mappingrules.AxisValueMax + } else { + absInfo := absInfoMap[axis] + min = absInfo.Minimum + max = absInfo.Maximum + } + + if targetConfig.DeadzoneCenter < min || targetConfig.DeadzoneCenter > max { + return 0, 0, fmt.Errorf("deadzone_center '%d' is out of bounds", targetConfig.DeadzoneCenter) + } + + switch { + case targetConfig.DeadzoneSize != 0: + deadzoneStart = targetConfig.DeadzoneCenter - targetConfig.DeadzoneSize/2 + deadzoneEnd = targetConfig.DeadzoneCenter + targetConfig.DeadzoneSize/2 + case targetConfig.DeadzoneSizePercent != 0: + deadzoneSize := (max - min) / targetConfig.DeadzoneSizePercent + deadzoneStart = targetConfig.DeadzoneCenter - deadzoneSize/2 + deadzoneEnd = targetConfig.DeadzoneCenter + deadzoneSize/2 + } + + deadzoneStart, deadzoneEnd = clampAndShift(deadzoneStart, deadzoneEnd, min, max) + return deadzoneStart, deadzoneEnd, nil +} + +func clampAndShift(start, end, min, max int32) (int32, int32) { + logger.Logf("DEBUG: %d %d %d %d", start, end, min, max) + if start < min { + end += min - start + start = min + logger.Logf("DEBUG: %d %d %d %d", start, end, min, max) + } + if end > max { + start -= end - max + end = max + } + + return start, end +} diff --git a/internal/config/make_rule_targets_test.go b/internal/config/make_rule_targets_test.go index 02de987..6e71fa6 100644 --- a/internal/config/make_rule_targets_test.go +++ b/internal/config/make_rule_targets_test.go @@ -4,12 +4,24 @@ import ( "testing" "github.com/holoplot/go-evdev" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" ) type MakeRuleTargetsTests struct { suite.Suite - devs map[string]*evdev.InputDevice + devs map[string]Device + deviceMock *DeviceMock + config RuleTargetConfig +} + +type DeviceMock struct { + mock.Mock +} + +func (m *DeviceMock) AbsInfos() (map[evdev.EvCode]evdev.AbsInfo, error) { + args := m.Called() + return args.Get(0).(map[evdev.EvCode]evdev.AbsInfo), args.Error(1) } func TestRunnerMakeRuleTargets(t *testing.T) { @@ -17,131 +29,216 @@ func TestRunnerMakeRuleTargets(t *testing.T) { } func (t *MakeRuleTargetsTests) SetupSuite() { - t.devs = map[string]*evdev.InputDevice{ - "test": {}, + t.deviceMock = new(DeviceMock) + t.deviceMock.On("AbsInfos").Return( + map[evdev.EvCode]evdev.AbsInfo{ + evdev.ABS_X: { + Minimum: 0, + Maximum: 10000, + }, + evdev.ABS_Y: { + Minimum: 0, + Maximum: 10000, + }, + }, nil, + ) + t.devs = map[string]Device{ + "test": t.deviceMock, + } +} + +func (t *MakeRuleTargetsTests) SetupSubTest() { + t.config = RuleTargetConfig{ + Device: "test", } } func (t *MakeRuleTargetsTests) TestMakeRuleTargetButton() { - config := RuleTargetConfig{ - Device: "test", - } t.Run("Standard keycode", func() { - config.Button = "BTN_TRIGGER" - rule, err := makeRuleTargetButton(config, t.devs) + t.config.Button = "BTN_TRIGGER" + rule, err := makeRuleTargetButton(t.config, t.devs) t.Nil(err) t.EqualValues(evdev.BTN_TRIGGER, rule.Button) }) t.Run("Hex code", func() { - config.Button = "0x2fd" - rule, err := makeRuleTargetButton(config, t.devs) + t.config.Button = "0x2fd" + rule, err := makeRuleTargetButton(t.config, t.devs) t.Nil(err) t.EqualValues(evdev.EvCode(0x2fd), rule.Button) }) t.Run("Index", func() { - config.Button = "3" - rule, err := makeRuleTargetButton(config, t.devs) + t.config.Button = "3" + rule, err := makeRuleTargetButton(t.config, t.devs) t.Nil(err) t.EqualValues(evdev.BTN_TOP, rule.Button) }) t.Run("Index too high", func() { - config.Button = "74" - _, err := makeRuleTargetButton(config, t.devs) + t.config.Button = "74" + _, err := makeRuleTargetButton(t.config, t.devs) t.NotNil(err) }) t.Run("Un-prefixed keycode", func() { - config.Button = "pinkie" - rule, err := makeRuleTargetButton(config, t.devs) + t.config.Button = "pinkie" + rule, err := makeRuleTargetButton(t.config, t.devs) t.Nil(err) t.EqualValues(evdev.BTN_PINKIE, rule.Button) }) t.Run("Invalid keycode", func() { - config.Button = "foo" - _, err := makeRuleTargetButton(config, t.devs) + t.config.Button = "foo" + _, err := makeRuleTargetButton(t.config, t.devs) t.NotNil(err) }) } func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() { - config := RuleTargetConfig{ - Device: "test", - } - - t.Run("Standard keycode", func() { - config.Axis = "ABS_X" - rule, err := makeRuleTargetAxis(config, t.devs) + t.Run("Standard code", func() { + t.config.Axis = "ABS_X" + rule, err := makeRuleTargetAxis(t.config, t.devs) t.Nil(err) t.EqualValues(evdev.ABS_X, rule.Axis) }) - t.Run("Hex keycode", func() { - config.Axis = "0x01" - rule, err := makeRuleTargetAxis(config, t.devs) + t.Run("Hex code", func() { + t.config.Axis = "0x01" + rule, err := makeRuleTargetAxis(t.config, t.devs) t.Nil(err) t.EqualValues(evdev.ABS_Y, rule.Axis) }) - t.Run("Un-prefixed keycode", func() { - config.Axis = "x" - rule, err := makeRuleTargetAxis(config, t.devs) + t.Run("Un-prefixed code", func() { + t.config.Axis = "x" + rule, err := makeRuleTargetAxis(t.config, t.devs) t.Nil(err) t.EqualValues(evdev.ABS_X, rule.Axis) }) - t.Run("Invalid keycode", func() { - config.Axis = "foo" - _, err := makeRuleTargetAxis(config, t.devs) + t.Run("Invalid code", func() { + t.config.Axis = "foo" + _, err := makeRuleTargetAxis(t.config, t.devs) t.NotNil(err) }) t.Run("Invalid deadzone", func() { - config.DeadzoneEnd = 100 - config.DeadzoneStart = 1000 - _, err := makeRuleTargetAxis(config, t.devs) + t.config.Axis = "x" + t.config.DeadzoneEnd = 100 + t.config.DeadzoneStart = 1000 + _, err := makeRuleTargetAxis(t.config, t.devs) + t.NotNil(err) + }) + + t.Run("Deadzone center/size", func() { + t.config.Axis = "x" + t.config.DeadzoneCenter = 5000 + t.config.DeadzoneSize = 1000 + rule, err := makeRuleTargetAxis(t.config, t.devs) + t.Nil(err) + t.EqualValues(4500, rule.DeadzoneStart) + t.EqualValues(5500, rule.DeadzoneEnd) + }) + + t.Run("Deadzone center/size lower boundary", func() { + t.config.Axis = "x" + t.config.DeadzoneCenter = 0 + t.config.DeadzoneSize = 500 + rule, err := makeRuleTargetAxis(t.config, t.devs) + t.Nil(err) + t.EqualValues(0, rule.DeadzoneStart) + t.EqualValues(500, rule.DeadzoneEnd) + }) + + t.Run("Deadzone center/size upper boundary", func() { + t.config.Axis = "x" + t.config.DeadzoneCenter = 10000 + t.config.DeadzoneSize = 500 + rule, err := makeRuleTargetAxis(t.config, t.devs) + t.Nil(err) + t.EqualValues(9500, rule.DeadzoneStart) + t.EqualValues(10000, rule.DeadzoneEnd) + }) + + t.Run("Deadzone center/size invalid center", func() { + t.config.Axis = "x" + t.config.DeadzoneCenter = 20000 + t.config.DeadzoneSize = 500 + _, err := makeRuleTargetAxis(t.config, t.devs) + t.NotNil(err) + }) + + t.Run("Deadzone center/percent", func() { + t.config.Axis = "x" + t.config.DeadzoneCenter = 5000 + t.config.DeadzoneSizePercent = 10 + rule, err := makeRuleTargetAxis(t.config, t.devs) + t.Nil(err) + t.EqualValues(4500, rule.DeadzoneStart) + t.EqualValues(5500, rule.DeadzoneEnd) + }) + + t.Run("Deadzone center/percent lower boundary", func() { + t.config.Axis = "x" + t.config.DeadzoneCenter = 0 + t.config.DeadzoneSizePercent = 10 + rule, err := makeRuleTargetAxis(t.config, t.devs) + t.Nil(err) + t.EqualValues(0, rule.DeadzoneStart) + t.EqualValues(1000, rule.DeadzoneEnd) + }) + + t.Run("Deadzone center/percent upper boundary", func() { + t.config.Axis = "x" + t.config.DeadzoneCenter = 10000 + t.config.DeadzoneSizePercent = 10 + rule, err := makeRuleTargetAxis(t.config, t.devs) + t.Nil(err) + t.EqualValues(9000, rule.DeadzoneStart) + t.EqualValues(10000, rule.DeadzoneEnd) + }) + + t.Run("Deadzone center/percent invalid center", func() { + t.config.Axis = "x" + t.config.DeadzoneCenter = 20000 + t.config.DeadzoneSizePercent = 10 + _, err := makeRuleTargetAxis(t.config, t.devs) t.NotNil(err) }) } func (t *MakeRuleTargetsTests) TestMakeRuleTargetRelaxis() { - config := RuleTargetConfig{ - Device: "test", - } - t.Run("Standard keycode", func() { - config.Axis = "REL_WHEEL" - rule, err := makeRuleTargetRelaxis(config, t.devs) + t.config.Axis = "REL_WHEEL" + rule, err := makeRuleTargetRelaxis(t.config, t.devs) t.Nil(err) t.EqualValues(evdev.REL_WHEEL, rule.Axis) }) t.Run("Hex keycode", func() { - config.Axis = "0x00" - rule, err := makeRuleTargetRelaxis(config, t.devs) + t.config.Axis = "0x00" + rule, err := makeRuleTargetRelaxis(t.config, t.devs) t.Nil(err) t.EqualValues(evdev.REL_X, rule.Axis) }) t.Run("Un-prefixed keycode", func() { - config.Axis = "wheel" - rule, err := makeRuleTargetRelaxis(config, t.devs) + t.config.Axis = "wheel" + rule, err := makeRuleTargetRelaxis(t.config, t.devs) t.Nil(err) t.EqualValues(evdev.REL_WHEEL, rule.Axis) }) t.Run("Invalid keycode", func() { - config.Axis = "foo" - _, err := makeRuleTargetRelaxis(config, t.devs) + t.config.Axis = "foo" + _, err := makeRuleTargetRelaxis(t.config, t.devs) t.NotNil(err) }) t.Run("Incorrect axis type", func() { - config.Axis = "ABS_X" - _, err := makeRuleTargetRelaxis(config, t.devs) + t.config.Axis = "ABS_X" + _, err := makeRuleTargetRelaxis(t.config, t.devs) t.NotNil(err) }) } diff --git a/internal/config/make_rules.go b/internal/config/make_rules.go index 6d75d58..7c1365c 100644 --- a/internal/config/make_rules.go +++ b/internal/config/make_rules.go @@ -10,14 +10,25 @@ import ( ) // TODO: At some point it would *very likely* make sense to map each rule to all of the physical devices that can -// trigger it, and return that instead. Something like a map[*evdev.InputDevice][]mappingrule.MappingRule. +// 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[][]rule // For very large rule-bases this may be helpful for staying performant. -func (parser *ConfigParser) BuildRules(pDevs map[string]*evdev.InputDevice, vDevs map[string]*evdev.InputDevice) []mappingrules.MappingRule { +func (parser *ConfigParser) BuildRules(pInputDevs map[string]*evdev.InputDevice, vInputDevs map[string]*evdev.InputDevice) []mappingrules.MappingRule { rules := make([]mappingrules.MappingRule, 0) modes := parser.GetModes() + // 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 parser.config.Rules { var newRule mappingrules.MappingRule var err error @@ -60,8 +71,8 @@ func (parser *ConfigParser) BuildRules(pDevs map[string]*evdev.InputDevice, vDev } func makeMappingRuleButton(ruleConfig RuleConfig, - pDevs map[string]*evdev.InputDevice, - vDevs map[string]*evdev.InputDevice, + pDevs map[string]Device, + vDevs map[string]Device, base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButton, error) { input, err := makeRuleTargetButton(ruleConfig.Input, pDevs) @@ -78,8 +89,8 @@ func makeMappingRuleButton(ruleConfig RuleConfig, } func makeMappingRuleCombo(ruleConfig RuleConfig, - pDevs map[string]*evdev.InputDevice, - vDevs map[string]*evdev.InputDevice, + pDevs map[string]Device, + vDevs map[string]Device, base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButtonCombo, error) { inputs := make([]*mappingrules.RuleTargetButton, 0) @@ -100,8 +111,8 @@ func makeMappingRuleCombo(ruleConfig RuleConfig, } func makeMappingRuleLatched(ruleConfig RuleConfig, - pDevs map[string]*evdev.InputDevice, - vDevs map[string]*evdev.InputDevice, + pDevs map[string]Device, + vDevs map[string]Device, base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButtonLatched, error) { input, err := makeRuleTargetButton(ruleConfig.Input, pDevs) @@ -118,8 +129,8 @@ func makeMappingRuleLatched(ruleConfig RuleConfig, } func makeMappingRuleAxis(ruleConfig RuleConfig, - pDevs map[string]*evdev.InputDevice, - vDevs map[string]*evdev.InputDevice, + pDevs map[string]Device, + vDevs map[string]Device, base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxis, error) { input, err := makeRuleTargetAxis(ruleConfig.Input, pDevs) @@ -136,8 +147,8 @@ func makeMappingRuleAxis(ruleConfig RuleConfig, } func makeMappingRuleAxisToButton(ruleConfig RuleConfig, - pDevs map[string]*evdev.InputDevice, - vDevs map[string]*evdev.InputDevice, + pDevs map[string]Device, + vDevs map[string]Device, base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxisToButton, error) { input, err := makeRuleTargetAxis(ruleConfig.Input, pDevs) @@ -154,8 +165,8 @@ func makeMappingRuleAxisToButton(ruleConfig RuleConfig, } func makeMappingRuleAxisToRelaxis(ruleConfig RuleConfig, - pDevs map[string]*evdev.InputDevice, - vDevs map[string]*evdev.InputDevice, + pDevs map[string]Device, + vDevs map[string]Device, base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxisToRelaxis, error) { input, err := makeRuleTargetAxis(ruleConfig.Input, pDevs) @@ -176,7 +187,7 @@ func makeMappingRuleAxisToRelaxis(ruleConfig RuleConfig, } func makeMappingRuleModeSelect(ruleConfig RuleConfig, - pDevs map[string]*evdev.InputDevice, + pDevs map[string]Device, modes []string, base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleModeSelect, error) { diff --git a/internal/config/schema.go b/internal/config/schema.go index c869804..5f52756 100644 --- a/internal/config/schema.go +++ b/internal/config/schema.go @@ -41,11 +41,14 @@ type RuleConfig struct { } type RuleTargetConfig struct { - Device string `yaml:"device,omitempty"` - Button string `yaml:"button,omitempty"` - Axis string `yaml:"axis,omitempty"` - DeadzoneStart int32 `yaml:"deadzone_start,omitempty"` - DeadzoneEnd int32 `yaml:"deadzone_end,omitempty"` - Inverted bool `yaml:"inverted,omitempty"` - Modes []string `yaml:"modes,omitempty"` + Device string `yaml:"device,omitempty"` + Button string `yaml:"button,omitempty"` + Axis string `yaml:"axis,omitempty"` + 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 `yaml:"inverted,omitempty"` + Modes []string `yaml:"modes,omitempty"` } diff --git a/internal/mappingrules/interfaces.go b/internal/mappingrules/interfaces.go index bc10e9b..33b290a 100644 --- a/internal/mappingrules/interfaces.go +++ b/internal/mappingrules/interfaces.go @@ -7,7 +7,7 @@ import ( ) type MappingRule interface { - MatchEvent(RuleTargetDevice, *evdev.InputEvent, *string) (*evdev.InputDevice, *evdev.InputEvent) + MatchEvent(Device, *evdev.InputEvent, *string) (*evdev.InputDevice, *evdev.InputEvent) } type TimedEventEmitter interface { @@ -35,13 +35,13 @@ type RuleTarget interface { // for most implementations. CreateEvent(int32, *string) *evdev.InputEvent - MatchEvent(device RuleTargetDevice, event *evdev.InputEvent) bool + MatchEvent(device Device, event *evdev.InputEvent) bool } -// RuleTargetDevice is an interface abstraction on top of evdev.InputDevice, implementing +// Device is an interface abstraction on top of evdev.InputDevice, implementing // only the methods we need in this package. This is used for testing, and the -// RuleTargetDevice can be safely cast to an *evdev.InputDevice when necessary. -type RuleTargetDevice interface { +// Device can be safely cast to an *evdev.InputDevice when necessary. +type Device interface { AbsInfos() (map[evdev.EvCode]evdev.AbsInfo, error) } diff --git a/internal/mappingrules/mapping_rule_axis.go b/internal/mappingrules/mapping_rule_axis.go index 7b3e778..a2ab41d 100644 --- a/internal/mappingrules/mapping_rule_axis.go +++ b/internal/mappingrules/mapping_rule_axis.go @@ -17,7 +17,7 @@ func NewMappingRuleAxis(base MappingRuleBase, input *RuleTargetAxis, output *Rul } } -func (rule *MappingRuleAxis) MatchEvent(device RuleTargetDevice, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { +func (rule *MappingRuleAxis) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { if !rule.MappingRuleBase.modeCheck(mode) || !rule.Input.MatchEvent(device, event) { return nil, nil diff --git a/internal/mappingrules/mapping_rule_axis_to_button.go b/internal/mappingrules/mapping_rule_axis_to_button.go index 3e15312..3356dbe 100644 --- a/internal/mappingrules/mapping_rule_axis_to_button.go +++ b/internal/mappingrules/mapping_rule_axis_to_button.go @@ -39,7 +39,7 @@ func NewMappingRuleAxisToButton(base MappingRuleBase, input *RuleTargetAxis, out } } -func (rule *MappingRuleAxisToButton) MatchEvent(device RuleTargetDevice, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { +func (rule *MappingRuleAxisToButton) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { if !rule.MappingRuleBase.modeCheck(mode) || !rule.Input.MatchEventDeviceAndCode(device, event) { @@ -105,5 +105,5 @@ func (rule *MappingRuleAxisToButton) TimerEvent() *evdev.InputEvent { } func (rule *MappingRuleAxisToButton) GetOutputDevice() *evdev.InputDevice { - return rule.Output.Device + return rule.Output.Device.(*evdev.InputDevice) } diff --git a/internal/mappingrules/mapping_rule_axis_to_relaxis.go b/internal/mappingrules/mapping_rule_axis_to_relaxis.go index 16c3912..153b992 100644 --- a/internal/mappingrules/mapping_rule_axis_to_relaxis.go +++ b/internal/mappingrules/mapping_rule_axis_to_relaxis.go @@ -43,7 +43,7 @@ func NewMappingRuleAxisToRelaxis( } func (rule *MappingRuleAxisToRelaxis) MatchEvent( - device RuleTargetDevice, + device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { diff --git a/internal/mappingrules/mapping_rule_button.go b/internal/mappingrules/mapping_rule_button.go index a13d5a6..69a7cfe 100644 --- a/internal/mappingrules/mapping_rule_button.go +++ b/internal/mappingrules/mapping_rule_button.go @@ -21,7 +21,7 @@ func NewMappingRuleButton( } } -func (rule *MappingRuleButton) MatchEvent(device RuleTargetDevice, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { +func (rule *MappingRuleButton) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { if !rule.MappingRuleBase.modeCheck(mode) { return nil, nil } @@ -31,5 +31,5 @@ func (rule *MappingRuleButton) MatchEvent(device RuleTargetDevice, event *evdev. return nil, nil } - return rule.Output.Device, rule.Output.CreateEvent(rule.Input.NormalizeValue(event.Value), mode) + return rule.Output.Device.(*evdev.InputDevice), rule.Output.CreateEvent(rule.Input.NormalizeValue(event.Value), mode) } diff --git a/internal/mappingrules/mapping_rule_button_combo.go b/internal/mappingrules/mapping_rule_button_combo.go index 4f488ef..a7b7c23 100644 --- a/internal/mappingrules/mapping_rule_button_combo.go +++ b/internal/mappingrules/mapping_rule_button_combo.go @@ -23,7 +23,7 @@ func NewMappingRuleButtonCombo( } } -func (rule *MappingRuleButtonCombo) MatchEvent(device RuleTargetDevice, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { +func (rule *MappingRuleButtonCombo) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { if !rule.MappingRuleBase.modeCheck(mode) { return nil, nil } @@ -53,10 +53,10 @@ func (rule *MappingRuleButtonCombo) MatchEvent(device RuleTargetDevice, event *e targetState := len(rule.Inputs) if oldState == targetState-1 && rule.State == targetState { - return rule.Output.Device, rule.Output.CreateEvent(1, mode) + return rule.Output.Device.(*evdev.InputDevice), rule.Output.CreateEvent(1, mode) } if oldState == targetState && rule.State == targetState-1 { - return rule.Output.Device, rule.Output.CreateEvent(0, mode) + return rule.Output.Device.(*evdev.InputDevice), rule.Output.CreateEvent(0, mode) } return nil, nil } diff --git a/internal/mappingrules/mapping_rule_button_latched.go b/internal/mappingrules/mapping_rule_button_latched.go index 1204968..d8e5bec 100644 --- a/internal/mappingrules/mapping_rule_button_latched.go +++ b/internal/mappingrules/mapping_rule_button_latched.go @@ -22,7 +22,7 @@ func NewMappingRuleButtonLatched( } } -func (rule *MappingRuleButtonLatched) MatchEvent(device RuleTargetDevice, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { +func (rule *MappingRuleButtonLatched) MatchEvent(device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { if !rule.MappingRuleBase.modeCheck(mode) { return nil, nil } @@ -42,5 +42,5 @@ func (rule *MappingRuleButtonLatched) MatchEvent(device RuleTargetDevice, event value = 0 } - return rule.Output.Device, rule.Output.CreateEvent(value, mode) + return rule.Output.Device.(*evdev.InputDevice), rule.Output.CreateEvent(value, mode) } diff --git a/internal/mappingrules/mapping_rule_mode_select.go b/internal/mappingrules/mapping_rule_mode_select.go index 1bb13fa..69afd0b 100644 --- a/internal/mappingrules/mapping_rule_mode_select.go +++ b/internal/mappingrules/mapping_rule_mode_select.go @@ -22,7 +22,7 @@ func NewMappingRuleModeSelect( } func (rule *MappingRuleModeSelect) MatchEvent( - device RuleTargetDevice, + device Device, event *evdev.InputEvent, mode *string) (*evdev.InputDevice, *evdev.InputEvent) { diff --git a/internal/mappingrules/rule_target_axis.go b/internal/mappingrules/rule_target_axis.go index c0d4b95..79b4492 100644 --- a/internal/mappingrules/rule_target_axis.go +++ b/internal/mappingrules/rule_target_axis.go @@ -9,7 +9,7 @@ import ( type RuleTargetAxis struct { DeviceName string - Device RuleTargetDevice + Device Device Axis evdev.EvCode Inverted bool DeadzoneStart int32 @@ -19,7 +19,7 @@ type RuleTargetAxis struct { } func NewRuleTargetAxis(device_name string, - device RuleTargetDevice, + device Device, axis evdev.EvCode, inverted bool, deadzoneStart int32, @@ -89,13 +89,13 @@ func (target *RuleTargetAxis) CreateEvent(value int32, mode *string) *evdev.Inpu } } -func (target *RuleTargetAxis) MatchEvent(device RuleTargetDevice, event *evdev.InputEvent) bool { +func (target *RuleTargetAxis) MatchEvent(device Device, event *evdev.InputEvent) bool { return target.MatchEventDeviceAndCode(device, event) && !target.InDeadZone(event.Value) } // TODO: Add tests -func (target *RuleTargetAxis) MatchEventDeviceAndCode(device RuleTargetDevice, event *evdev.InputEvent) bool { +func (target *RuleTargetAxis) MatchEventDeviceAndCode(device Device, event *evdev.InputEvent) bool { return device == target.Device && event.Type == evdev.EV_ABS && event.Code == target.Axis diff --git a/internal/mappingrules/rule_target_button.go b/internal/mappingrules/rule_target_button.go index 93534c7..68fd252 100644 --- a/internal/mappingrules/rule_target_button.go +++ b/internal/mappingrules/rule_target_button.go @@ -4,12 +4,12 @@ import "github.com/holoplot/go-evdev" type RuleTargetButton struct { DeviceName string - Device *evdev.InputDevice + Device Device Button evdev.EvCode Inverted bool } -func NewRuleTargetButton(device_name string, device *evdev.InputDevice, code evdev.EvCode, inverted bool) (*RuleTargetButton, error) { +func NewRuleTargetButton(device_name string, device Device, code evdev.EvCode, inverted bool) (*RuleTargetButton, error) { return &RuleTargetButton{ DeviceName: device_name, Device: device, @@ -36,7 +36,7 @@ func (target *RuleTargetButton) CreateEvent(value int32, _ *string) *evdev.Input } } -func (target *RuleTargetButton) MatchEvent(device RuleTargetDevice, event *evdev.InputEvent) bool { +func (target *RuleTargetButton) MatchEvent(device Device, event *evdev.InputEvent) bool { return device == target.Device && event.Type == evdev.EV_KEY && event.Code == target.Button diff --git a/internal/mappingrules/rule_target_relaxis.go b/internal/mappingrules/rule_target_relaxis.go index 648d7fc..8de8c0b 100644 --- a/internal/mappingrules/rule_target_relaxis.go +++ b/internal/mappingrules/rule_target_relaxis.go @@ -6,13 +6,13 @@ import ( type RuleTargetRelaxis struct { DeviceName string - Device RuleTargetDevice + Device Device Axis evdev.EvCode Inverted bool } func NewRuleTargetRelaxis(device_name string, - device RuleTargetDevice, + device Device, axis evdev.EvCode, inverted bool) (*RuleTargetRelaxis, error) { @@ -41,6 +41,6 @@ func (target *RuleTargetRelaxis) CreateEvent(value int32, mode *string) *evdev.I } // Relative axis is only supported for output. -func (target *RuleTargetRelaxis) MatchEvent(device RuleTargetDevice, event *evdev.InputEvent) bool { +func (target *RuleTargetRelaxis) MatchEvent(device Device, event *evdev.InputEvent) bool { return false } diff --git a/readme.md b/readme.md index d38fd3f..d6a9021 100644 --- a/readme.md +++ b/readme.md @@ -17,8 +17,8 @@ Joyful is ideal for Linux gamers who enjoy space and flight sims and miss the fe * "Split" axis mapping: map sections of an axis to different outputs using deadzones. * Axis -> button mapping with optional "proportional" repeat speed (i.e. repeat faster as the axis is engaged further) * Axis -> Relative Axis mapping, for converting a joystick axis to mouse movement and scrollwheel events. +* Configure per-rule configurable deadzones for axes, with multiple ways to specify deadzones. * Define multiple modes with per-mode behavior. -* Configure per-rule configurable deadzones for axes. ### Possible Future Features @@ -27,7 +27,6 @@ Joyful is ideal for Linux gamers who enjoy space and flight sims and miss the fe * Output keyboard button presses * Hat support * HIDRAW support for more button options. -* Percentage-based deadzones. * Sensitivity Curves. ## Configuration @@ -40,7 +39,7 @@ Configuration can be fairly complicated and repetitive. If anyone wants to creat ## Usage -After building (see below) and writing your configuration (see above), just run `joyful`. (Feel free to move this somewhere in your path. You can use `--config ` to specify different configuration profiles. +After building (see below) and writing your configuration (see above), just run `joyful`. You can use `joyful --config ` to specify different configuration profiles; just put all the YAML files for a given profile in a unique directory. ## Technical details