Add more deadzone specification options. #9

Merged
anna merged 2 commits from deadzone-options into main 2025-07-18 23:10:13 +00:00
20 changed files with 344 additions and 108 deletions

17
.vscode/tasks.json vendored
View file

@ -28,6 +28,23 @@
}
},
"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
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:

View file

@ -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 <examples/ruletypes.yml> for usage examples.
## Modes
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"
"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
}

View file

@ -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)
})
}

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
// 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[<struct of *inputdevice, type, and code>][]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) {

View file

@ -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"`
}

View file

@ -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)
}

View file

@ -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

View file

@ -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)
}

View file

@ -43,7 +43,7 @@ func NewMappingRuleAxisToRelaxis(
}
func (rule *MappingRuleAxisToRelaxis) MatchEvent(
device RuleTargetDevice,
device Device,
event *evdev.InputEvent,
mode *string) (*evdev.InputDevice, *evdev.InputEvent) {

View file

@ -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)
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -22,7 +22,7 @@ func NewMappingRuleModeSelect(
}
func (rule *MappingRuleModeSelect) MatchEvent(
device RuleTargetDevice,
device Device,
event *evdev.InputEvent,
mode *string) (*evdev.InputDevice, *evdev.InputEvent) {

View file

@ -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

View file

@ -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

View file

@ -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
}

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.
* 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 <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