Add more deadzone configuration options.

This commit is contained in:
Anna Rose Wiggins 2025-07-18 19:08:42 -04:00
parent 5b9dfe0967
commit 3bfcdc830f
9 changed files with 317 additions and 81 deletions

17
.vscode/tasks.json vendored
View file

@ -28,6 +28,23 @@
} }
}, },
"problemMatcher": [] "problemMatcher": []
},
{
"label": "Test Project",
"args": [
"test",
"./..."
],
"group": {
"kind": "test",
"isDefault": true
},
"options": {
"env": {
"CGO_ENABLED": "0"
}
},
"problemMatcher": []
} }
], ],
} }

View file

@ -26,6 +26,34 @@ rules:
device: main device: main
axis: ABS_X 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 # Straightforward button mapping
- type: button - type: button
input: input:

View file

@ -55,6 +55,16 @@ For input, you can figure out what event codes your device is emitting by runnin
evtest | grep BTN_ 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 <examples/ruletypes.yml> for usage examples.
## Modes ## Modes
Modes are optional, and also have the simplest configuration. To define modes, add this to your configuration: Modes are optional, and also have the simplest configuration. To define modes, add this to your configuration:

View file

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

View file

@ -4,11 +4,12 @@ import (
"errors" "errors"
"fmt" "fmt"
"git.annabunches.net/annabunches/joyful/internal/logger"
"git.annabunches.net/annabunches/joyful/internal/mappingrules" "git.annabunches.net/annabunches/joyful/internal/mappingrules"
"github.com/holoplot/go-evdev" "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] 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 +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] 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)
@ -42,17 +43,22 @@ func makeRuleTargetAxis(targetConfig RuleTargetConfig, devs map[string]*evdev.In
return nil, err return nil, err
} }
deadzoneStart, deadzoneEnd, err := calculateDeadzones(targetConfig, device, eventCode)
if err != nil {
return nil, err
}
return mappingrules.NewRuleTargetAxis( return mappingrules.NewRuleTargetAxis(
targetConfig.Device, targetConfig.Device,
device, device,
eventCode, eventCode,
targetConfig.Inverted, targetConfig.Inverted,
targetConfig.DeadzoneStart, deadzoneStart,
targetConfig.DeadzoneEnd, 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] 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)
@ -83,3 +89,61 @@ func makeRuleTargetModeSelect(targetConfig RuleTargetConfig, allModes []string)
func hasError(_ any, err error) bool { func hasError(_ any, err error) bool {
return err != nil 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
}

View file

@ -4,12 +4,24 @@ import (
"testing" "testing"
"github.com/holoplot/go-evdev" "github.com/holoplot/go-evdev"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
) )
type MakeRuleTargetsTests struct { type MakeRuleTargetsTests struct {
suite.Suite 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) { func TestRunnerMakeRuleTargets(t *testing.T) {
@ -17,131 +29,216 @@ func TestRunnerMakeRuleTargets(t *testing.T) {
} }
func (t *MakeRuleTargetsTests) SetupSuite() { func (t *MakeRuleTargetsTests) SetupSuite() {
t.devs = map[string]*evdev.InputDevice{ t.deviceMock = new(DeviceMock)
"test": {}, 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() { func (t *MakeRuleTargetsTests) TestMakeRuleTargetButton() {
config := RuleTargetConfig{
Device: "test",
}
t.Run("Standard keycode", func() { t.Run("Standard keycode", func() {
config.Button = "BTN_TRIGGER" t.config.Button = "BTN_TRIGGER"
rule, err := makeRuleTargetButton(config, t.devs) rule, err := makeRuleTargetButton(t.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() {
config.Button = "0x2fd" t.config.Button = "0x2fd"
rule, err := makeRuleTargetButton(config, t.devs) rule, err := makeRuleTargetButton(t.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() {
config.Button = "3" t.config.Button = "3"
rule, err := makeRuleTargetButton(config, t.devs) rule, err := makeRuleTargetButton(t.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() {
config.Button = "74" t.config.Button = "74"
_, err := makeRuleTargetButton(config, t.devs) _, err := makeRuleTargetButton(t.config, t.devs)
t.NotNil(err) t.NotNil(err)
}) })
t.Run("Un-prefixed keycode", func() { t.Run("Un-prefixed keycode", func() {
config.Button = "pinkie" t.config.Button = "pinkie"
rule, err := makeRuleTargetButton(config, t.devs) rule, err := makeRuleTargetButton(t.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() {
config.Button = "foo" t.config.Button = "foo"
_, err := makeRuleTargetButton(config, t.devs) _, err := makeRuleTargetButton(t.config, t.devs)
t.NotNil(err) t.NotNil(err)
}) })
} }
func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() { func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() {
config := RuleTargetConfig{ t.Run("Standard code", func() {
Device: "test", t.config.Axis = "ABS_X"
} rule, err := makeRuleTargetAxis(t.config, t.devs)
t.Run("Standard keycode", func() {
config.Axis = "ABS_X"
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 keycode", func() { t.Run("Hex code", func() {
config.Axis = "0x01" t.config.Axis = "0x01"
rule, err := makeRuleTargetAxis(config, t.devs) rule, err := makeRuleTargetAxis(t.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 keycode", func() { t.Run("Un-prefixed code", func() {
config.Axis = "x" t.config.Axis = "x"
rule, err := makeRuleTargetAxis(config, t.devs) rule, err := makeRuleTargetAxis(t.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 keycode", func() { t.Run("Invalid code", func() {
config.Axis = "foo" t.config.Axis = "foo"
_, err := makeRuleTargetAxis(config, t.devs) _, err := makeRuleTargetAxis(t.config, t.devs)
t.NotNil(err) t.NotNil(err)
}) })
t.Run("Invalid deadzone", func() { t.Run("Invalid deadzone", func() {
config.DeadzoneEnd = 100 t.config.Axis = "x"
config.DeadzoneStart = 1000 t.config.DeadzoneEnd = 100
_, err := makeRuleTargetAxis(config, t.devs) 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) t.NotNil(err)
}) })
} }
func (t *MakeRuleTargetsTests) TestMakeRuleTargetRelaxis() { func (t *MakeRuleTargetsTests) TestMakeRuleTargetRelaxis() {
config := RuleTargetConfig{
Device: "test",
}
t.Run("Standard keycode", func() { t.Run("Standard keycode", func() {
config.Axis = "REL_WHEEL" t.config.Axis = "REL_WHEEL"
rule, err := makeRuleTargetRelaxis(config, t.devs) rule, err := makeRuleTargetRelaxis(t.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() {
config.Axis = "0x00" t.config.Axis = "0x00"
rule, err := makeRuleTargetRelaxis(config, t.devs) rule, err := makeRuleTargetRelaxis(t.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() {
config.Axis = "wheel" t.config.Axis = "wheel"
rule, err := makeRuleTargetRelaxis(config, t.devs) rule, err := makeRuleTargetRelaxis(t.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() {
config.Axis = "foo" t.config.Axis = "foo"
_, err := makeRuleTargetRelaxis(config, t.devs) _, err := makeRuleTargetRelaxis(t.config, t.devs)
t.NotNil(err) t.NotNil(err)
}) })
t.Run("Incorrect axis type", func() { t.Run("Incorrect axis type", func() {
config.Axis = "ABS_X" t.config.Axis = "ABS_X"
_, err := makeRuleTargetRelaxis(config, t.devs) _, err := makeRuleTargetRelaxis(t.config, t.devs)
t.NotNil(err) t.NotNil(err)
}) })
} }

View file

@ -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 // 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. // 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 // We could take this further and make it a map[<struct of *inputdevice, type, and code>][]rule
// For very large rule-bases this may be helpful for staying performant. // For very large rule-bases this may be helpful for staying performant.
func (parser *ConfigParser) BuildRules(pDevs map[string]*evdev.InputDevice, vDevs map[string]*evdev.InputDevice) []mappingrules.MappingRule { func (parser *ConfigParser) BuildRules(pInputDevs map[string]*evdev.InputDevice, vInputDevs map[string]*evdev.InputDevice) []mappingrules.MappingRule {
rules := make([]mappingrules.MappingRule, 0) rules := make([]mappingrules.MappingRule, 0)
modes := parser.GetModes() 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 { for _, ruleConfig := range parser.config.Rules {
var newRule mappingrules.MappingRule var newRule mappingrules.MappingRule
var err error var err error
@ -60,8 +71,8 @@ func (parser *ConfigParser) BuildRules(pDevs map[string]*evdev.InputDevice, vDev
} }
func makeMappingRuleButton(ruleConfig RuleConfig, func makeMappingRuleButton(ruleConfig RuleConfig,
pDevs map[string]*evdev.InputDevice, pDevs map[string]Device,
vDevs map[string]*evdev.InputDevice, vDevs map[string]Device,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButton, error) { base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButton, error) {
input, err := makeRuleTargetButton(ruleConfig.Input, pDevs) input, err := makeRuleTargetButton(ruleConfig.Input, pDevs)
@ -78,8 +89,8 @@ func makeMappingRuleButton(ruleConfig RuleConfig,
} }
func makeMappingRuleCombo(ruleConfig RuleConfig, func makeMappingRuleCombo(ruleConfig RuleConfig,
pDevs map[string]*evdev.InputDevice, pDevs map[string]Device,
vDevs map[string]*evdev.InputDevice, vDevs map[string]Device,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButtonCombo, error) { base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButtonCombo, error) {
inputs := make([]*mappingrules.RuleTargetButton, 0) inputs := make([]*mappingrules.RuleTargetButton, 0)
@ -100,8 +111,8 @@ func makeMappingRuleCombo(ruleConfig RuleConfig,
} }
func makeMappingRuleLatched(ruleConfig RuleConfig, func makeMappingRuleLatched(ruleConfig RuleConfig,
pDevs map[string]*evdev.InputDevice, pDevs map[string]Device,
vDevs map[string]*evdev.InputDevice, vDevs map[string]Device,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButtonLatched, error) { base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleButtonLatched, error) {
input, err := makeRuleTargetButton(ruleConfig.Input, pDevs) input, err := makeRuleTargetButton(ruleConfig.Input, pDevs)
@ -118,8 +129,8 @@ func makeMappingRuleLatched(ruleConfig RuleConfig,
} }
func makeMappingRuleAxis(ruleConfig RuleConfig, func makeMappingRuleAxis(ruleConfig RuleConfig,
pDevs map[string]*evdev.InputDevice, pDevs map[string]Device,
vDevs map[string]*evdev.InputDevice, vDevs map[string]Device,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxis, error) { base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxis, error) {
input, err := makeRuleTargetAxis(ruleConfig.Input, pDevs) input, err := makeRuleTargetAxis(ruleConfig.Input, pDevs)
@ -136,8 +147,8 @@ func makeMappingRuleAxis(ruleConfig RuleConfig,
} }
func makeMappingRuleAxisToButton(ruleConfig RuleConfig, func makeMappingRuleAxisToButton(ruleConfig RuleConfig,
pDevs map[string]*evdev.InputDevice, pDevs map[string]Device,
vDevs map[string]*evdev.InputDevice, vDevs map[string]Device,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxisToButton, error) { base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxisToButton, error) {
input, err := makeRuleTargetAxis(ruleConfig.Input, pDevs) input, err := makeRuleTargetAxis(ruleConfig.Input, pDevs)
@ -154,8 +165,8 @@ func makeMappingRuleAxisToButton(ruleConfig RuleConfig,
} }
func makeMappingRuleAxisToRelaxis(ruleConfig RuleConfig, func makeMappingRuleAxisToRelaxis(ruleConfig RuleConfig,
pDevs map[string]*evdev.InputDevice, pDevs map[string]Device,
vDevs map[string]*evdev.InputDevice, vDevs map[string]Device,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxisToRelaxis, error) { base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleAxisToRelaxis, error) {
input, err := makeRuleTargetAxis(ruleConfig.Input, pDevs) input, err := makeRuleTargetAxis(ruleConfig.Input, pDevs)
@ -176,7 +187,7 @@ func makeMappingRuleAxisToRelaxis(ruleConfig RuleConfig,
} }
func makeMappingRuleModeSelect(ruleConfig RuleConfig, func makeMappingRuleModeSelect(ruleConfig RuleConfig,
pDevs map[string]*evdev.InputDevice, pDevs map[string]Device,
modes []string, modes []string,
base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleModeSelect, error) { base mappingrules.MappingRuleBase) (*mappingrules.MappingRuleModeSelect, error) {

View file

@ -41,11 +41,14 @@ type RuleConfig struct {
} }
type RuleTargetConfig struct { type RuleTargetConfig struct {
Device string `yaml:"device,omitempty"` Device string `yaml:"device,omitempty"`
Button string `yaml:"button,omitempty"` Button string `yaml:"button,omitempty"`
Axis string `yaml:"axis,omitempty"` Axis string `yaml:"axis,omitempty"`
DeadzoneStart int32 `yaml:"deadzone_start,omitempty"` DeadzoneCenter int32 `yaml:"deadzone_center,omitempty"`
DeadzoneEnd int32 `yaml:"deadzone_end,omitempty"` DeadzoneSize int32 `yaml:"deadzone_size,omitempty"`
Inverted bool `yaml:"inverted,omitempty"` DeadzoneSizePercent int32 `yaml:"deadzone_size_percent,omitempty"`
Modes []string `yaml:"modes,omitempty"` DeadzoneStart int32 `yaml:"deadzone_start,omitempty"`
DeadzoneEnd int32 `yaml:"deadzone_end,omitempty"`
Inverted bool `yaml:"inverted,omitempty"`
Modes []string `yaml:"modes,omitempty"`
} }

View file

@ -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. * "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 -> 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. * 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. * Define multiple modes with per-mode behavior.
* Configure per-rule configurable deadzones for axes.
### Possible Future Features ### 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 * Output keyboard button presses
* Hat support * Hat support
* HIDRAW support for more button options. * HIDRAW support for more button options.
* Percentage-based deadzones.
* Sensitivity Curves. * Sensitivity Curves.
## Configuration ## Configuration
@ -40,7 +39,7 @@ Configuration can be fairly complicated and repetitive. If anyone wants to creat
## Usage ## 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 <directory>` to specify different configuration profiles. After building (see below) and writing your configuration (see above), just run `joyful`. You can use `joyful --config <directory>` to specify different configuration profiles; just put all the YAML files for a given profile in a unique directory.
## Technical details ## Technical details