diff --git a/cmd/joyful-config/main.go b/cmd/joyful-config/main.go
new file mode 100644
index 0000000..8ca321a
--- /dev/null
+++ b/cmd/joyful-config/main.go
@@ -0,0 +1,33 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "strings"
+
+ "git.annabunches.net/annabunches/joyful/internal/configparser"
+ "git.annabunches.net/annabunches/joyful/internal/logger"
+ flag "github.com/spf13/pflag"
+)
+
+func getConfigDir(dir string) string {
+ configDir := strings.ReplaceAll(dir, "~", "${HOME}")
+ return os.ExpandEnv(configDir)
+}
+
+func main() {
+ var configDir string
+ flag.StringVarP(&configDir, "config", "c", "~/.config/joyful", "Directory to read configuration from.")
+ flag.Parse()
+ configDir = getConfigDir(configDir)
+
+ config, err := configparser.ParseConfig(configDir)
+ switch err.(type) {
+ case *configparser.EmptyConfigError:
+ config = &configparser.Config{}
+ default:
+ logger.FatalIfError(err, "Fatal error reading config")
+ }
+
+ fmt.Printf("Config: %v\n", config)
+}
diff --git a/cmd/joyful/config.go b/cmd/joyful/config.go
index 64d6b2d..2b43380 100644
--- a/cmd/joyful/config.go
+++ b/cmd/joyful/config.go
@@ -2,6 +2,7 @@ package main
import (
"context"
+ "strings"
"sync"
"git.annabunches.net/annabunches/joyful/internal/configparser"
@@ -15,7 +16,7 @@ func initPhysicalDevices(conf *configparser.Config) map[string]*evdev.InputDevic
pDeviceMap := make(map[string]*evdev.InputDevice)
for _, devConfig := range conf.Devices {
- if devConfig.Type != configparser.DeviceTypePhysical {
+ if strings.ToLower(devConfig.Type) != configparser.DeviceTypePhysical {
continue
}
@@ -70,7 +71,7 @@ func initVirtualBuffers(config *configparser.Config) (map[string]*evdev.InputDev
vBuffersByDevice := make(map[*evdev.InputDevice]*virtualdevice.EventBuffer)
for _, devConfig := range config.Devices {
- if devConfig.Type != configparser.DeviceTypeVirtual {
+ if strings.ToLower(devConfig.Type) != configparser.DeviceTypeVirtual {
continue
}
diff --git a/docs/examples/multiple_files/axes.yml b/docs/examples/multiple_files/axes.yml
index 6f7947d..3056df3 100644
--- a/docs/examples/multiple_files/axes.yml
+++ b/docs/examples/multiple_files/axes.yml
@@ -92,9 +92,8 @@ rules:
input:
device: left-stick
axis: RY
- deadzones:
- - start: 0
- end: 30500
+ deadzone_start: 0
+ deadzone_end: 30500
output:
device: mouse
axis: REL_WHEEL
@@ -109,9 +108,8 @@ rules:
input:
device: left-stick
axis: RY
- deadzones:
- - start: 29500
- end: 64000
+ deadzone_start: 29500
+ deadzone_end: 64000
inverted: true
output:
device: mouse
diff --git a/docs/examples/multiple_files/devices.yml b/docs/examples/multiple_files/devices.yml
index 779f0f5..391e4c8 100644
--- a/docs/examples/multiple_files/devices.yml
+++ b/docs/examples/multiple_files/devices.yml
@@ -1,6 +1,6 @@
devices:
- name: primary
- type: Virtual
+ type: virtual
preset: joystick
- name: secondary
type: virtual
diff --git a/docs/examples/ruletypes.yml b/docs/examples/ruletypes.yml
index 8bc0fe8..7cb4b3a 100644
--- a/docs/examples/ruletypes.yml
+++ b/docs/examples/ruletypes.yml
@@ -18,9 +18,8 @@ rules:
input:
device: flightstick
# To find reasonable values for your device's deadzones, use the evtest command
- deadzones:
- - start: 28000
- end: 30000
+ deadzone_start: 28000
+ deadzone_end: 30000
inverted: false
axis: ABS_X
output:
@@ -34,9 +33,8 @@ rules:
# 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.
- deadzones:
- - center: 29000
- size: 2000
+ deadzone_center: 29000
+ deadzone_size: 2000
inverted: false
axis: Y # The ABS_ prefix is optional
output:
@@ -48,9 +46,8 @@ rules:
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.
- deadzones:
- - center: 29000
- size_percent: 5
+ deadzone_center: 29000
+ deadzone_size_percent: 5
inverted: false
axis: Y # The ABS_ prefix is optional
output:
@@ -70,17 +67,6 @@ rules:
device: main
axis: RZ
- # Hat mapping. Hats are technically an axis, but only output -1, 0, or 1, so we don't normalize
- # them to an output range, we just pass them through mostly unmodified
- - type: hat
- input:
- device: flightstick
- inverted: true # hats do support inversion. As with other rule types, this only has an effect on *inputs*.
- hat: hat0x # a typical joystick hat actually has 2 hat axes: x and y
- output:
- device: main
- hat: hat0x
-
# Straightforward button mapping
- type: button
input:
@@ -122,9 +108,8 @@ rules:
input:
device: flightstick
axis: ABS_RY # This axis commonly represents thumbsticks
- deadzones:
- - start: 0
- end: 30000
+ deadzone_start: 0
+ deadzone_end: 30000
output:
device: main
button: BTN_BASE4
@@ -141,9 +126,8 @@ rules:
input:
device: flightstick
axis: ABS_Z
- deadzones:
- - start: 0
- end: 500
+ deadzone_start: 0
+ deadzone_end: 500
output:
device: mouse
button: REL_WHEEL
\ No newline at end of file
diff --git a/docs/readme.md b/docs/readme.md
index 7ac1945..f6e7f37 100644
--- a/docs/readme.md
+++ b/docs/readme.md
@@ -48,7 +48,6 @@ All `rules` must have a `type` parameter. Valid values for this parameter are:
* `axis-combined` - a mapping that combines 2 input axes into a single output axis.
* `axis-to-button` - causes an axis input to produce a button output. This can be repeated with variable speed proportional to the axis' input value
* `axis-to-relaxis` - like axis-to-button, but produces a "relative axis" output value. This is useful for simulating mouse scrollwheel and movement events.
-* `hat` - a special type of axis with ternary output. Each joystick hat will typically be 2 hat axes named `ABS_HAT0X` / `ABS_HAT0Y`, where the `0` is an index between 0 - 3. So for a typical hat you would define 2 `hat` rules.
Configuration options for each rule type vary. See [examples/ruletypes.yml](examples/ruletypes.yml) for an example of each type with all options specified.
@@ -74,17 +73,13 @@ evtest | grep BTN_
**NOTE: For most axis mappings, you probably don't want to specify a deadzone!** Use deadzone configurations in your target game instead. Joyful-configured deadzones are intended to be used in conjunction with the `axis-to-button` and `axis-to-relaxis` input types, or when splitting an axis into multiple outputs. Using them with standard `axis` mappings may result in a loss of fidelity and "stuck" inputs.
-Axis inputs can define a list of deadzones. Each deadzone can be specified a few ways:
+There are three ways to specify deadzones:
-* Define `start` and `end` to explicitly set the deadzone bounds.
-* Define `center` and `size`; this will create a deadzone of the indicated size centered at the given axis position.
-* Define `center` and `size_percent` to use a percentage of the total axis size.
+* 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.
-In addition, deadzones can set `emit` to `true` and `emit_value` to a value that should be emitted when inside the deadzone.
-
-**Note**: The `emit_value` is the final output value and should be between -32,768 and 32,767.
-
-See the directory for usage examples.
+See for usage examples.
## Modes
diff --git a/internal/configparser/configparser.go b/internal/configparser/configparser.go
index 3daa217..deceb8a 100644
--- a/internal/configparser/configparser.go
+++ b/internal/configparser/configparser.go
@@ -50,7 +50,7 @@ func getConfigFilePaths(directory string) ([]string, error) {
if err != nil {
return nil, errors.New("failed to create config directory at " + directory)
} else {
- return nil, errors.New("no config files found at " + directory)
+ return nil, &EmptyConfigError{directory}
}
}
@@ -63,5 +63,9 @@ func getConfigFilePaths(directory string) ([]string, error) {
paths = append(paths, filepath.Join(directory, file.Name()))
}
+ if len(paths) == 0 {
+ return nil, &EmptyConfigError{directory}
+ }
+
return paths, nil
}
diff --git a/internal/configparser/deviceconfig.go b/internal/configparser/deviceconfig.go
deleted file mode 100644
index eafd8ca..0000000
--- a/internal/configparser/deviceconfig.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package configparser
-
-// These top-level structs use custom unmarshaling to unpack each available sub-type
-type DeviceConfig struct {
- Type DeviceType
- Config interface{}
-}
-
-func (dc *DeviceConfig) UnmarshalYAML(unmarshal func(data interface{}) error) error {
- metaConfig := &struct {
- Type DeviceType
- }{}
- err := unmarshal(metaConfig)
- if err != nil {
- return err
- }
- dc.Type = metaConfig.Type
-
- err = nil
- switch metaConfig.Type {
- case DeviceTypePhysical:
- config := DeviceConfigPhysical{}
- err = unmarshal(&config)
- dc.Config = config
- case DeviceTypeVirtual:
- config := DeviceConfigVirtual{}
- err = unmarshal(&config)
- dc.Config = config
- }
- return err
-}
diff --git a/internal/configparser/deviceconfigphysical.go b/internal/configparser/deviceconfigphysical.go
deleted file mode 100644
index ecb5255..0000000
--- a/internal/configparser/deviceconfigphysical.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package configparser
-
-type DeviceConfigPhysical struct {
- Name string
- DeviceName string `yaml:"device_name,omitempty"`
- DevicePath string `yaml:"device_path,omitempty"`
- Lock bool
-}
-
-// TODO: custom yaml unmarshaling is obtuse; do we really need to do all of this work
-// just to set a single default value?
-func (dc *DeviceConfigPhysical) UnmarshalYAML(unmarshal func(data interface{}) error) error {
- var raw struct {
- Name string
- DeviceName string `yaml:"device_name"`
- DevicePath string `yaml:"device_path"`
- Lock bool `yaml:"lock,omitempty"`
- }
-
- // Set non-standard defaults
- raw.Lock = true
-
- err := unmarshal(&raw)
- if err != nil {
- return err
- }
-
- *dc = DeviceConfigPhysical{
- Name: raw.Name,
- DeviceName: raw.DeviceName,
- DevicePath: raw.DevicePath,
- Lock: raw.Lock,
- }
- return nil
-}
diff --git a/internal/configparser/devicetype.go b/internal/configparser/devicetype.go
deleted file mode 100644
index 7640304..0000000
--- a/internal/configparser/devicetype.go
+++ /dev/null
@@ -1,40 +0,0 @@
-package configparser
-
-import (
- "fmt"
- "strings"
-)
-
-type DeviceType string
-
-const (
- DeviceTypeNone DeviceType = ""
- DeviceTypePhysical DeviceType = "physical"
- DeviceTypeVirtual DeviceType = "virtual"
-)
-
-var (
- deviceTypeMap = map[string]DeviceType{
- "physical": DeviceTypePhysical,
- "virtual": DeviceTypeVirtual,
- }
-)
-
-func ParseDeviceType(in string) (DeviceType, error) {
- deviceType, ok := deviceTypeMap[strings.ToLower(in)]
- if !ok {
- return DeviceTypeNone, fmt.Errorf("invalid rule type '%s'", in)
- }
- return deviceType, nil
-}
-
-func (rt *DeviceType) UnmarshalYAML(unmarshal func(data interface{}) error) error {
- var raw string
- err := unmarshal(&raw)
- if err != nil {
- return err
- }
-
- *rt, err = ParseDeviceType(raw)
- return err
-}
diff --git a/internal/configparser/errors.go b/internal/configparser/errors.go
new file mode 100644
index 0000000..4698152
--- /dev/null
+++ b/internal/configparser/errors.go
@@ -0,0 +1,11 @@
+package configparser
+
+import "fmt"
+
+type EmptyConfigError struct {
+ directory string
+}
+
+func (e *EmptyConfigError) Error() string {
+ return fmt.Sprintf("no config files found at %s", e.directory)
+}
diff --git a/internal/configparser/ruleconfig.go b/internal/configparser/ruleconfig.go
deleted file mode 100644
index 53c3c35..0000000
--- a/internal/configparser/ruleconfig.go
+++ /dev/null
@@ -1,64 +0,0 @@
-package configparser
-
-type RuleConfig struct {
- Type RuleType
- Name string
- Modes []string
- Config interface{}
-}
-
-func (dc *RuleConfig) UnmarshalYAML(unmarshal func(data interface{}) error) error {
- metaConfig := &struct {
- Type RuleType
- Name string
- Modes []string
- }{}
- err := unmarshal(metaConfig)
- if err != nil {
- return err
- }
- dc.Type = metaConfig.Type
- dc.Name = metaConfig.Name
- dc.Modes = metaConfig.Modes
-
- switch dc.Type {
- case RuleTypeButton:
- config := RuleConfigButton{}
- err = unmarshal(&config)
- dc.Config = config
- case RuleTypeButtonCombo:
- config := RuleConfigButtonCombo{}
- err = unmarshal(&config)
- dc.Config = config
- case RuleTypeButtonLatched:
- config := RuleConfigButtonLatched{}
- err = unmarshal(&config)
- dc.Config = config
- case RuleTypeAxis:
- config := RuleConfigAxis{}
- err = unmarshal(&config)
- dc.Config = config
- case RuleTypeAxisCombined:
- config := RuleConfigAxisCombined{}
- err = unmarshal(&config)
- dc.Config = config
- case RuleTypeAxisToButton:
- config := RuleConfigAxisToButton{}
- err = unmarshal(&config)
- dc.Config = config
- case RuleTypeAxisToRelaxis:
- config := RuleConfigAxisToRelaxis{}
- err = unmarshal(&config)
- dc.Config = config
- case RuleTypeModeSelect:
- config := RuleConfigModeSelect{}
- err = unmarshal(&config)
- dc.Config = config
- case RuleTypeHat:
- config := RuleConfigHat{}
- err = unmarshal(&config)
- dc.Config = config
- }
-
- return err
-}
diff --git a/internal/configparser/ruletarget.go b/internal/configparser/ruletarget.go
deleted file mode 100644
index 2a2a12a..0000000
--- a/internal/configparser/ruletarget.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package configparser
-
-type RuleTargetConfigButton struct {
- Device string
- Button string
- Inverted bool
-}
-
-type RuleTargetConfigAxis struct {
- Device string
- Axis string
- Inverted bool
- Deadzones []DeadzoneConfig
-}
-
-type DeadzoneConfig struct {
- Center int32 `yaml:"center,omitempty"`
- Size int32 `yaml:"size,omitempty"`
- SizePercent int32 `yaml:"size_percent,omitempty"`
- Start int32 `yaml:"start,omitempty"`
- End int32 `yaml:"end,omitempty"`
- Emit bool `yaml:"emit,omitempty"`
- Value int32 `yaml:"emit_value,omitempty"`
-}
-
-type RuleTargetConfigRelaxis struct {
- Device string
- Axis string
-}
-
-type RuleTargetConfigModeSelect struct {
- Modes []string
-}
-
-type RuleTargetConfigHat struct {
- Device string
- Hat string
- Inverted bool
-}
diff --git a/internal/configparser/ruletype.go b/internal/configparser/ruletype.go
deleted file mode 100644
index d305570..0000000
--- a/internal/configparser/ruletype.go
+++ /dev/null
@@ -1,55 +0,0 @@
-package configparser
-
-import (
- "fmt"
- "strings"
-)
-
-// TODO: maybe these want to live somewhere other than configparser?
-type RuleType string
-
-const (
- RuleTypeNone RuleType = ""
- RuleTypeButton RuleType = "button"
- RuleTypeButtonCombo RuleType = "button-combo"
- RuleTypeButtonLatched RuleType = "button-latched"
- RuleTypeAxis RuleType = "axis"
- RuleTypeAxisCombined RuleType = "axis-combined"
- RuleTypeAxisToButton RuleType = "axis-to-button"
- RuleTypeAxisToRelaxis RuleType = "axis-to-relaxis"
- RuleTypeModeSelect RuleType = "mode-select"
- RuleTypeHat RuleType = "hat"
-)
-
-var (
- ruleTypeMap = map[string]RuleType{
- "button": RuleTypeButton,
- "button-combo": RuleTypeButtonCombo,
- "button-latched": RuleTypeButtonLatched,
- "axis": RuleTypeAxis,
- "axis-combined": RuleTypeAxisCombined,
- "axis-to-button": RuleTypeAxisToButton,
- "axis-to-relaxis": RuleTypeAxisToRelaxis,
- "mode-select": RuleTypeModeSelect,
- "hat": RuleTypeHat,
- }
-)
-
-func ParseRuleType(in string) (RuleType, error) {
- ruleType, ok := ruleTypeMap[strings.ToLower(in)]
- if !ok {
- return RuleTypeNone, fmt.Errorf("invalid rule type '%s'", in)
- }
- return ruleType, nil
-}
-
-func (rt *RuleType) UnmarshalYAML(unmarshal func(data interface{}) error) error {
- var raw string
- err := unmarshal(&raw)
- if err != nil {
- return err
- }
-
- *rt, err = ParseRuleType(raw)
- return err
-}
diff --git a/internal/configparser/schema.go b/internal/configparser/schema.go
index 55ddb24..8b70521 100644
--- a/internal/configparser/schema.go
+++ b/internal/configparser/schema.go
@@ -1,13 +1,38 @@
-// These types comprise the YAML schema that doesn't need custom unmarshalling.
+// These types comprise the YAML schema for configuring Joyful.
+// The config files will be combined and then unmarshalled into this
package configparser
+import (
+ "fmt"
+)
+
type Config struct {
Devices []DeviceConfig
Modes []string
Rules []RuleConfig
}
+// These top-level structs use custom unmarshaling to unpack each available sub-type
+type DeviceConfig struct {
+ Type string
+ Config interface{}
+}
+
+type RuleConfig struct {
+ Type string
+ Name string
+ Modes []string
+ Config interface{}
+}
+
+type DeviceConfigPhysical struct {
+ Name string
+ DeviceName string `yaml:"device_name,omitempty"`
+ DevicePath string `yaml:"device_path,omitempty"`
+ Lock bool
+}
+
// TODO: configure custom unmarshaling so we can overload Buttons, Axes, and RelativeAxes...
type DeviceConfigVirtual struct {
Name string
@@ -40,11 +65,6 @@ type RuleConfigAxis struct {
Output RuleTargetConfigAxis
}
-type RuleConfigHat struct {
- Input RuleTargetConfigHat
- Output RuleTargetConfigHat
-}
-
type RuleConfigAxisCombined struct {
InputLower RuleTargetConfigAxis `yaml:"input_lower,omitempty"`
InputUpper RuleTargetConfigAxis `yaml:"input_upper,omitempty"`
@@ -70,3 +90,136 @@ 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 {
+ metaConfig := &struct {
+ Type string
+ }{}
+ err := unmarshal(metaConfig)
+ if err != nil {
+ return err
+ }
+ dc.Type = metaConfig.Type
+
+ err = nil
+ switch metaConfig.Type {
+ case DeviceTypePhysical:
+ config := DeviceConfigPhysical{}
+ err = unmarshal(&config)
+ dc.Config = config
+ case DeviceTypeVirtual:
+ config := DeviceConfigVirtual{}
+ err = unmarshal(&config)
+ dc.Config = config
+ default:
+ err = fmt.Errorf("invalid device type '%s'", dc.Type)
+ }
+ return err
+}
+
+func (dc *RuleConfig) UnmarshalYAML(unmarshal func(data interface{}) error) error {
+ metaConfig := &struct {
+ Type string
+ Name string
+ Modes []string
+ }{}
+ err := unmarshal(metaConfig)
+ if err != nil {
+ return err
+ }
+ dc.Type = metaConfig.Type
+ dc.Name = metaConfig.Name
+ dc.Modes = metaConfig.Modes
+
+ switch dc.Type {
+ case RuleTypeButton:
+ config := RuleConfigButton{}
+ err = unmarshal(&config)
+ dc.Config = config
+ case RuleTypeButtonCombo:
+ config := RuleConfigButtonCombo{}
+ err = unmarshal(&config)
+ dc.Config = config
+ case RuleTypeButtonLatched:
+ config := RuleConfigButtonLatched{}
+ err = unmarshal(&config)
+ dc.Config = config
+ case RuleTypeAxis:
+ config := RuleConfigAxis{}
+ err = unmarshal(&config)
+ dc.Config = config
+ case RuleTypeAxisCombined:
+ config := RuleConfigAxisCombined{}
+ err = unmarshal(&config)
+ dc.Config = config
+ case RuleTypeAxisToButton:
+ config := RuleConfigAxisToButton{}
+ err = unmarshal(&config)
+ dc.Config = config
+ case RuleTypeAxisToRelaxis:
+ config := RuleConfigAxisToRelaxis{}
+ err = unmarshal(&config)
+ dc.Config = config
+ case RuleTypeModeSelect:
+ config := RuleConfigModeSelect{}
+ err = unmarshal(&config)
+ dc.Config = config
+ default:
+ err = fmt.Errorf("invalid rule type '%s'", dc.Type)
+ }
+
+ return err
+}
+
+// TODO: custom yaml unmarshaling is obtuse; do we really need to do all of this work
+// just to set a single default value?
+func (dc *DeviceConfigPhysical) UnmarshalYAML(unmarshal func(data interface{}) error) error {
+ var raw struct {
+ Name string
+ DeviceName string `yaml:"device_name"`
+ DevicePath string `yaml:"device_path"`
+ Lock bool `yaml:"lock,omitempty"`
+ }
+
+ // Set non-standard defaults
+ raw.Lock = true
+
+ err := unmarshal(&raw)
+ if err != nil {
+ return err
+ }
+
+ *dc = DeviceConfigPhysical{
+ Name: raw.Name,
+ DeviceName: raw.DeviceName,
+ DevicePath: raw.DevicePath,
+ Lock: raw.Lock,
+ }
+ return nil
+}
diff --git a/internal/configparser/variables.go b/internal/configparser/variables.go
new file mode 100644
index 0000000..77e2b9c
--- /dev/null
+++ b/internal/configparser/variables.go
@@ -0,0 +1,15 @@
+package configparser
+
+const (
+ DeviceTypePhysical = "physical"
+ DeviceTypeVirtual = "virtual"
+
+ RuleTypeButton = "button"
+ RuleTypeButtonCombo = "button-combo"
+ RuleTypeButtonLatched = "button-latched"
+ RuleTypeAxis = "axis"
+ RuleTypeAxisCombined = "axis-combined"
+ RuleTypeAxisToButton = "axis-to-button"
+ RuleTypeAxisToRelaxis = "axis-to-relaxis"
+ RuleTypeModeSelect = "mode-select"
+)
diff --git a/internal/mappingrules/deadzone.go b/internal/mappingrules/deadzone.go
deleted file mode 100644
index 23af465..0000000
--- a/internal/mappingrules/deadzone.go
+++ /dev/null
@@ -1,98 +0,0 @@
-package mappingrules
-
-import (
- "errors"
- "fmt"
-
- "git.annabunches.net/annabunches/joyful/internal/configparser"
- "github.com/holoplot/go-evdev"
-)
-
-// TODO: need tests for multiple deadzones
-// TODO: need tests for emitting deadzones
-
-type Deadzone struct {
- Start int32
- End int32
- Size int32
- Emit bool
- EmitValue int32
-}
-
-// DeadzoneState indicates whether a value is in a Deadzone and, if it is, whether the deadzone
-// should emit an event
-type DeadzoneState int
-
-const (
- // DeadzoneClear indicates the value is *not* in the deadzone.
- DeadzoneClear DeadzoneState = iota
- DeadzoneEmit
- DeadzoneNoEmit
-)
-
-// calculateDeadzones produces the deadzone start and end values in absolute terms
-func NewDeadzoneFromConfig(dzConfig configparser.DeadzoneConfig, device Device, axis evdev.EvCode) (Deadzone, error) {
- dz := Deadzone{}
- dz.Emit = dzConfig.Emit
- dz.EmitValue = dzConfig.Value
-
- var min, max int32
- absInfoMap, err := device.AbsInfos()
-
- if err != nil {
- return dz, err
- } else {
- absInfo := absInfoMap[axis]
- min = absInfo.Minimum
- max = absInfo.Maximum
- }
-
- if dzConfig.Start != 0 || dzConfig.End != 0 {
- dz.Start = Clamp(dzConfig.Start, min, max)
- dz.End = Clamp(dzConfig.End, min, max)
- if dz.Start > dz.End {
- return dz, errors.New("deadzone end must be greater than deadzone start")
- }
- } else {
- center := Clamp(dzConfig.Center, min, max)
- var deadzoneSize int32
-
- switch {
- case dzConfig.Size != 0:
- deadzoneSize = dzConfig.Size
- case dzConfig.SizePercent != 0:
- deadzoneSize = (max - min) / dzConfig.SizePercent
- default:
- return dz, fmt.Errorf("deadzone configured incorrectly; must define start and end or center and size")
- }
-
- dz.Start = center - deadzoneSize/2
- dz.End = center + deadzoneSize/2
- dz.Start, dz.End = clampAndShift(dz.Start, dz.End, min, max)
- }
-
- dz.Size = dz.End - dz.Start
- return dz, nil
-}
-
-func CalculateDeadzoneSize(dzs []Deadzone) int32 {
- var size int32
-
- for _, dz := range dzs {
- size += dz.Size
- }
-
- return size
-}
-
-// Match checks whether the target value is inside the deadzone.
-// It returns a DeadzoneState enum and possibly an int32.
-func (dz Deadzone) Match(value int32) (DeadzoneState, int32) {
- if value < dz.Start || value > dz.End {
- return DeadzoneClear, value
- }
- if dz.Emit {
- return DeadzoneEmit, dz.EmitValue
- }
- return DeadzoneNoEmit, value
-}
diff --git a/internal/mappingrules/init_rule_targets_test.go b/internal/mappingrules/init_rule_targets_test.go
index 3b349e9..168b02d 100644
--- a/internal/mappingrules/init_rule_targets_test.go
+++ b/internal/mappingrules/init_rule_targets_test.go
@@ -125,12 +125,8 @@ func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() {
t.Run("Invalid deadzone", func() {
config := configparser.RuleTargetConfigAxis{Device: "test"}
config.Axis = "x"
- config.Deadzones = []configparser.DeadzoneConfig{
- {
- End: 100,
- Start: 1000,
- },
- }
+ config.DeadzoneEnd = 100
+ config.DeadzoneStart = 1000
_, err := NewRuleTargetAxisFromConfig(config, t.devs)
t.NotNil(err)
})
@@ -149,21 +145,30 @@ func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() {
for _, tc := range relDeadzoneTestCases {
t.Run(fmt.Sprintf("Relative Deadzone %d +- %d", tc.inCenter, tc.inSize), func() {
config := configparser.RuleTargetConfigAxis{
- Device: "test",
- Axis: "x",
- Deadzones: []configparser.DeadzoneConfig{{
- Center: tc.inCenter,
- Size: tc.inSize,
- }},
+ Device: "test",
+ Axis: "x",
+ DeadzoneCenter: tc.inCenter,
+ DeadzoneSize: tc.inSize,
}
rule, err := NewRuleTargetAxisFromConfig(config, t.devs)
t.Nil(err)
- t.Equal(tc.outStart, rule.Deadzones[0].Start)
- t.Equal(tc.outEnd, rule.Deadzones[0].End)
+ t.Equal(tc.outStart, rule.DeadzoneStart)
+ t.Equal(tc.outEnd, rule.DeadzoneEnd)
})
}
+ t.Run("Deadzone center/size invalid center", func() {
+ config := configparser.RuleTargetConfigAxis{
+ Device: "test",
+ Axis: "x",
+ DeadzoneCenter: 20000,
+ DeadzoneSize: 500,
+ }
+ _, err := NewRuleTargetAxisFromConfig(config, t.devs)
+ t.NotNil(err)
+ })
+
relDeadzonePercentTestCases := []struct {
inCenter int32
inSizePercent int32
@@ -178,20 +183,29 @@ func (t *MakeRuleTargetsTests) TestMakeRuleTargetAxis() {
for _, tc := range relDeadzonePercentTestCases {
t.Run(fmt.Sprintf("Relative percent deadzone %d +- %d%%", tc.inCenter, tc.inSizePercent), func() {
config := configparser.RuleTargetConfigAxis{
- Device: "test",
- Axis: "x",
- Deadzones: []configparser.DeadzoneConfig{{
- Center: tc.inCenter,
- SizePercent: tc.inSizePercent,
- }},
+ Device: "test",
+ Axis: "x",
+ DeadzoneCenter: tc.inCenter,
+ DeadzoneSizePercent: tc.inSizePercent,
}
rule, err := NewRuleTargetAxisFromConfig(config, t.devs)
t.Nil(err)
- t.Equal(tc.outStart, rule.Deadzones[0].Start)
- t.Equal(tc.outEnd, rule.Deadzones[0].End)
+ t.Equal(tc.outStart, rule.DeadzoneStart)
+ t.Equal(tc.outEnd, rule.DeadzoneEnd)
})
}
+
+ t.Run("Deadzone center/percent invalid center", func() {
+ config := configparser.RuleTargetConfigAxis{
+ Device: "test",
+ Axis: "x",
+ DeadzoneCenter: 20000,
+ DeadzoneSizePercent: 10,
+ }
+ _, err := NewRuleTargetAxisFromConfig(config, t.devs)
+ t.NotNil(err)
+ })
}
func (t *MakeRuleTargetsTests) TestMakeRuleTargetRelaxis() {
diff --git a/internal/mappingrules/init_rules.go b/internal/mappingrules/init_rules.go
index 28d4ea8..7ea0ea4 100644
--- a/internal/mappingrules/init_rules.go
+++ b/internal/mappingrules/init_rules.go
@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"slices"
+ "strings"
"git.annabunches.net/annabunches/joyful/internal/configparser"
"git.annabunches.net/annabunches/joyful/internal/logger"
@@ -32,27 +33,24 @@ func NewRule(config configparser.RuleConfig, pDevs map[string]Device, vDevs map[
base := NewMappingRuleBase(config.Name, config.Modes)
- switch config.Type {
- case configparser.RuleTypeButton:
+ switch strings.ToLower(config.Type) {
+ case RuleTypeButton:
newRule, err = NewMappingRuleButton(config.Config.(configparser.RuleConfigButton), pDevs, vDevs, base)
- case configparser.RuleTypeButtonCombo:
+ case RuleTypeButtonCombo:
newRule, err = NewMappingRuleButtonCombo(config.Config.(configparser.RuleConfigButtonCombo), pDevs, vDevs, base)
- case configparser.RuleTypeButtonLatched:
+ case RuleTypeButtonLatched:
newRule, err = NewMappingRuleButtonLatched(config.Config.(configparser.RuleConfigButtonLatched), pDevs, vDevs, base)
- case configparser.RuleTypeAxis:
+ case RuleTypeAxis:
newRule, err = NewMappingRuleAxis(config.Config.(configparser.RuleConfigAxis), pDevs, vDevs, base)
- case configparser.RuleTypeAxisCombined:
+ case RuleTypeAxisCombined:
newRule, err = NewMappingRuleAxisCombined(config.Config.(configparser.RuleConfigAxisCombined), pDevs, vDevs, base)
- case configparser.RuleTypeAxisToButton:
+ case RuleTypeAxisToButton:
newRule, err = NewMappingRuleAxisToButton(config.Config.(configparser.RuleConfigAxisToButton), pDevs, vDevs, base)
- case configparser.RuleTypeAxisToRelaxis:
+ case RuleTypeAxisToRelaxis:
newRule, err = NewMappingRuleAxisToRelaxis(config.Config.(configparser.RuleConfigAxisToRelaxis), pDevs, vDevs, base)
- case configparser.RuleTypeModeSelect:
+ case RuleTypeModeSelect:
newRule, err = NewMappingRuleModeSelect(config.Config.(configparser.RuleConfigModeSelect), pDevs, modes, base)
- case configparser.RuleTypeHat:
- newRule, err = NewMappingRuleHat(config.Config.(configparser.RuleConfigHat), pDevs, vDevs, base)
default:
- // Shouldn't actually be possible to get here...
err = fmt.Errorf("bad rule type '%s' for rule '%s'", config.Type, config.Name)
}
diff --git a/internal/mappingrules/interfaces.go b/internal/mappingrules/interfaces.go
index 96594c6..33b290a 100644
--- a/internal/mappingrules/interfaces.go
+++ b/internal/mappingrules/interfaces.go
@@ -22,6 +22,9 @@ type RuleTarget interface {
// (e.g., inverting the value if Inverted == true)
NormalizeValue(int32) int32
+ // MatchEvent returns true if the provided device and input event are a match for this rule target
+ ValidateEvent(*evdev.InputDevice, *evdev.InputEvent) bool
+
// CreateEvent creates an event that can be emitted on a virtual device.
// For RuleTargetModeSelect, this method modifies the active mode and returns nil.
//
@@ -32,7 +35,6 @@ type RuleTarget interface {
// for most implementations.
CreateEvent(int32, *string) *evdev.InputEvent
- // MatchEvent returns true if the provided device and input event are a match for this rule target
MatchEvent(device Device, event *evdev.InputEvent) bool
}
diff --git a/internal/mappingrules/mapping_rule_axis_combined_test.go b/internal/mappingrules/mapping_rule_axis_combined_test.go
index 967f454..c514ed7 100644
--- a/internal/mappingrules/mapping_rule_axis_combined_test.go
+++ b/internal/mappingrules/mapping_rule_axis_combined_test.go
@@ -28,7 +28,6 @@ func TestRunnerMappingRuleAxisCombined(t *testing.T) {
}
func (t *MappingRuleAxisCombinedTests) SetupTest() {
- noDeadzone := make([]Deadzone, 0)
mode := "*"
t.mode = &mode
@@ -38,13 +37,13 @@ func (t *MappingRuleAxisCombinedTests) SetupTest() {
evdev.ABS_Y: {Minimum: 0, Maximum: 10000},
}, nil)
- t.inputTargetLower, _ = NewRuleTargetAxis("test-input", t.inputDevice, evdev.ABS_X, true, noDeadzone)
+ t.inputTargetLower, _ = NewRuleTargetAxis("test-input", t.inputDevice, evdev.ABS_X, true, 0, 0)
t.inputTargetLower.OutputMax = 0
- t.inputTargetUpper, _ = NewRuleTargetAxis("test-input", t.inputDevice, evdev.ABS_Y, false, noDeadzone)
+ t.inputTargetUpper, _ = NewRuleTargetAxis("test-input", t.inputDevice, evdev.ABS_Y, false, 0, 0)
t.inputTargetUpper.OutputMin = 0
t.outputDevice = &evdev.InputDevice{}
- t.outputTarget, _ = NewRuleTargetAxis("test-output", t.outputDevice, evdev.ABS_X, false, noDeadzone)
+ t.outputTarget, _ = NewRuleTargetAxis("test-output", t.outputDevice, evdev.ABS_X, false, 0, 0)
t.base = NewMappingRuleBase("", []string{"*"})
@@ -68,10 +67,10 @@ func (t *MappingRuleAxisCombinedTests) TestNewMappingRuleAxisCombined() {
}, nil)
rule := &MappingRuleAxisCombined{
- // MappingRuleBase: t.base,
- InputLower: t.inputTargetLower,
- InputUpper: t.inputTargetUpper,
- // Output: t.outputTarget,
+ MappingRuleBase: t.base,
+ InputLower: t.inputTargetLower,
+ InputUpper: t.inputTargetUpper,
+ Output: t.outputTarget,
}
t.EqualValues(0, rule.InputLower.OutputMax)
t.EqualValues(0, rule.InputUpper.OutputMin)
diff --git a/internal/mappingrules/mapping_rule_axis_to_button_test.go b/internal/mappingrules/mapping_rule_axis_to_button_test.go
index 24fcd64..0da086a 100644
--- a/internal/mappingrules/mapping_rule_axis_to_button_test.go
+++ b/internal/mappingrules/mapping_rule_axis_to_button_test.go
@@ -67,7 +67,7 @@ func (t *MappingRuleAxisToButtonTests) SetupTest() {
Maximum: 10000,
},
}, nil)
- t.inputRule, _ = NewRuleTargetAxis("test-input", t.inputDevice, evdev.ABS_X, false, []Deadzone{{Start: 0, End: 1000}})
+ t.inputRule, _ = NewRuleTargetAxis("test-input", t.inputDevice, evdev.ABS_X, false, int32(0), int32(1000))
t.outputDevice = &evdev.InputDevice{}
t.outputRule, _ = NewRuleTargetButton("test-output", t.outputDevice, evdev.ABS_X, false)
@@ -113,16 +113,14 @@ func (t *MappingRuleAxisToButtonTests) TestMatchEvent() {
Code: evdev.ABS_X,
Value: 1001,
}, t.mode)
- // Allow leeway since time passes during the test
- t.True(testRule.nextEvent > time.Duration(650*time.Millisecond))
+ t.True(testRule.nextEvent > time.Duration(700*time.Millisecond))
testRule.MatchEvent(t.inputDevice, &evdev.InputEvent{
Type: evdev.EV_ABS,
Code: evdev.ABS_X,
Value: 5500,
}, t.mode)
- // Allow up to 50 ms leeway since time passes during the test
- t.InDelta(time.Duration(500*time.Millisecond), testRule.nextEvent, 50000000)
+ t.Equal(time.Duration(500*time.Millisecond), testRule.nextEvent)
})
}
diff --git a/internal/mappingrules/mapping_rule_hat.go b/internal/mappingrules/mapping_rule_hat.go
deleted file mode 100644
index ba04323..0000000
--- a/internal/mappingrules/mapping_rule_hat.go
+++ /dev/null
@@ -1,45 +0,0 @@
-package mappingrules
-
-import (
- "git.annabunches.net/annabunches/joyful/internal/configparser"
- "github.com/holoplot/go-evdev"
-)
-
-// A Simple Mapping Rule can map a button to a button or an axis to an axis.
-type MappingRuleHat struct {
- MappingRuleBase
- Input *RuleTargetHat
- Output *RuleTargetHat
-}
-
-func NewMappingRuleHat(ruleConfig configparser.RuleConfigHat,
- pDevs map[string]Device,
- vDevs map[string]Device,
- base MappingRuleBase) (*MappingRuleHat, error) {
-
- input, err := NewRuleTargetHatFromConfig(ruleConfig.Input, pDevs)
- if err != nil {
- return nil, err
- }
-
- output, err := NewRuleTargetHatFromConfig(ruleConfig.Output, vDevs)
- if err != nil {
- return nil, err
- }
-
- return &MappingRuleHat{
- MappingRuleBase: base,
- Input: input,
- Output: output,
- }, nil
-}
-
-func (rule *MappingRuleHat) 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
- }
-
- // The cast here is safe because the interface is only ever different for unit tests
- return rule.Output.Device.(*evdev.InputDevice), rule.Output.CreateEvent(rule.Input.NormalizeValue(event.Value), mode)
-}
diff --git a/internal/mappingrules/rule_target_axis.go b/internal/mappingrules/rule_target_axis.go
index fcf1dcb..1d92d37 100644
--- a/internal/mappingrules/rule_target_axis.go
+++ b/internal/mappingrules/rule_target_axis.go
@@ -10,15 +10,16 @@ import (
)
type RuleTargetAxis struct {
- DeviceName string
- Device Device
- Axis evdev.EvCode
- Inverted bool
- Deadzones []Deadzone
- OutputMin int32
- OutputMax int32
- axisSize int32
- deadzoneSize int32
+ DeviceName string
+ Device Device
+ Axis evdev.EvCode
+ Inverted bool
+ DeadzoneStart int32
+ DeadzoneEnd int32
+ OutputMin int32
+ OutputMax int32
+ axisSize int32
+ deadzoneSize int32
}
func NewRuleTargetAxisFromConfig(targetConfig configparser.RuleTargetConfigAxis, devs map[string]Device) (*RuleTargetAxis, error) {
@@ -27,18 +28,18 @@ func NewRuleTargetAxisFromConfig(targetConfig configparser.RuleTargetConfigAxis,
return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device)
}
+ if targetConfig.DeadzoneEnd < targetConfig.DeadzoneStart {
+ return nil, errors.New("deadzone_end must be greater than deadzone_start")
+ }
+
eventCode, err := eventcodes.ParseCode(targetConfig.Axis, eventcodes.CodePrefixAxis)
if err != nil {
return nil, err
}
- deadzones := make([]Deadzone, 0)
- for _, dzConfig := range targetConfig.Deadzones {
- dz, err := NewDeadzoneFromConfig(dzConfig, device, eventCode)
- if err != nil {
- return nil, err
- }
- deadzones = append(deadzones, dz)
+ deadzoneStart, deadzoneEnd, err := calculateDeadzones(targetConfig, device, eventCode)
+ if err != nil {
+ return nil, err
}
return NewRuleTargetAxis(
@@ -46,15 +47,58 @@ func NewRuleTargetAxisFromConfig(targetConfig configparser.RuleTargetConfigAxis,
device,
eventCode,
targetConfig.Inverted,
- deadzones,
+ deadzoneStart,
+ deadzoneEnd,
)
}
+// calculateDeadzones produces the deadzone start and end values in absolute terms
+func calculateDeadzones(targetConfig configparser.RuleTargetConfigAxis, 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 = AxisValueMin
+ max = 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 NewRuleTargetAxis(device_name string,
device Device,
axis evdev.EvCode,
inverted bool,
- deadzones []Deadzone) (*RuleTargetAxis, error) {
+ deadzoneStart int32,
+ deadzoneEnd int32) (*RuleTargetAxis, error) {
info, err := device.AbsInfos()
@@ -73,7 +117,11 @@ func NewRuleTargetAxis(device_name string,
return nil, fmt.Errorf("device does not support axis %v", axis)
}
- deadzoneSize := CalculateDeadzoneSize(deadzones)
+ if deadzoneStart > deadzoneEnd {
+ return nil, errors.New("deadzone_end must be a higher value than deadzone_start")
+ }
+
+ deadzoneSize := Abs(deadzoneEnd - deadzoneStart)
// Our output range is limited to 16 bits, but we represent values internally with 32 bits.
// As a result, we shouldn't need to worry about integer overruns
@@ -84,15 +132,16 @@ func NewRuleTargetAxis(device_name string,
}
return &RuleTargetAxis{
- DeviceName: device_name,
- Device: device,
- Axis: axis,
- Inverted: inverted,
- OutputMin: AxisValueMin,
- OutputMax: AxisValueMax,
- Deadzones: deadzones,
- deadzoneSize: deadzoneSize,
- axisSize: axisSize,
+ DeviceName: device_name,
+ Device: device,
+ Axis: axis,
+ Inverted: inverted,
+ OutputMin: AxisValueMin,
+ OutputMax: AxisValueMax,
+ DeadzoneStart: deadzoneStart,
+ DeadzoneEnd: deadzoneEnd,
+ deadzoneSize: deadzoneSize,
+ axisSize: axisSize,
}, nil
}
@@ -101,17 +150,9 @@ func NewRuleTargetAxis(device_name string,
// Axis inputs are normalized to the full signed int32 range to match the virtual device's axis
// characteristics.
//
-// If the raw value is inside the deadzone, we either emit no event, or we emit the deadzoneValue.
// Typically this function is called after RuleTargetAxis.MatchEvent, which checks whether we are
// in the deadzone, among other things.
func (target *RuleTargetAxis) NormalizeValue(value int32) int32 {
- for _, dz := range target.Deadzones {
- state, dzValue := dz.Match(value)
- if state == DeadzoneEmit {
- return Clamp(dzValue, target.OutputMin, target.OutputMax)
- }
- }
-
axisStrength := target.GetAxisStrength(value)
return LerpInt(target.OutputMin, target.OutputMax, axisStrength)
}
@@ -137,30 +178,19 @@ func (target *RuleTargetAxis) MatchEventDeviceAndCode(device Device, event *evde
event.Code == target.Axis
}
-// InDeadZone checks each deadzone for whether the target value falls within it.
-// If *any* non-emitting deadzone matches, we return true.
// TODO: Add tests
func (target *RuleTargetAxis) InDeadZone(value int32) bool {
- for _, dz := range target.Deadzones {
- state, _ := dz.Match(value)
- if state == DeadzoneNoEmit {
- return true
- }
- }
- return false
+ return target.deadzoneSize > 0 && value >= target.DeadzoneStart && value <= target.DeadzoneEnd
}
// GetAxisStrength returns a float between 0.0 and 1.0, representing the proportional
// position along the axis' full range. (after factoring in deadzones)
// Calling this function with `value` inside the deadzone range will produce undefined behavior
func (target *RuleTargetAxis) GetAxisStrength(value int32) float64 {
- adjValue := value
- for _, dz := range target.Deadzones {
- if value > dz.End {
- adjValue -= dz.Size
- }
+ if value > target.DeadzoneEnd {
+ value -= target.deadzoneSize
}
- strength := float64(adjValue) / float64(target.axisSize)
+ strength := float64(value) / float64(target.axisSize)
if target.Inverted {
strength = 1.0 - strength
}
diff --git a/internal/mappingrules/rule_target_axis_test.go b/internal/mappingrules/rule_target_axis_test.go
index 6e1d3c3..5125b94 100644
--- a/internal/mappingrules/rule_target_axis_test.go
+++ b/internal/mappingrules/rule_target_axis_test.go
@@ -38,42 +38,42 @@ func (t *RuleTargetAxisTests) TearDownTest() {
}
func (t *RuleTargetAxisTests) TestNewRuleTargetAxis() {
- noDeadzone := make([]Deadzone, 0)
-
// RuleTargets should get created
- ruleTarget, err := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, noDeadzone)
+ ruleTarget, err := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, 0, 0)
t.Nil(err)
t.EqualValues(10000, ruleTarget.axisSize)
- ruleTarget, err = NewRuleTargetAxis("", t.mock, evdev.ABS_Y, false, noDeadzone)
+ ruleTarget, err = NewRuleTargetAxis("", t.mock, evdev.ABS_Y, false, 0, 0)
t.Nil(err)
t.EqualValues(20000, ruleTarget.axisSize)
// Creating a rule with a deadzone should work and reduce the axisSize
- ruleTarget, err = NewRuleTargetAxis("", t.mock, evdev.ABS_Y, false, []Deadzone{{Start: -500, End: 500, Size: 1000}})
+ ruleTarget, err = NewRuleTargetAxis("", t.mock, evdev.ABS_Y, false, -500, 500)
t.Nil(err)
t.EqualValues(19000, ruleTarget.axisSize)
- t.EqualValues(-500, ruleTarget.Deadzones[0].Start)
- t.EqualValues(500, ruleTarget.Deadzones[0].End)
+ t.EqualValues(-500, ruleTarget.DeadzoneStart)
+ t.EqualValues(500, ruleTarget.DeadzoneEnd)
+
+ // Creating a rule with a deadzone should fail if end > start
+ _, err = NewRuleTargetAxis("", t.mock, evdev.ABS_Y, false, 500, -500)
+ t.NotNil(err)
// Creating a rule on a non-existent axis should err
- _, err = NewRuleTargetAxis("", t.mock, evdev.ABS_Z, false, noDeadzone)
+ _, err = NewRuleTargetAxis("", t.mock, evdev.ABS_Z, false, 0, 0)
t.NotNil(err)
// If Absinfo has an error, we should create a device with permissive bounds
t.call.Unset()
t.mock.On("AbsInfos").Return(map[evdev.EvCode]evdev.AbsInfo{}, errors.New("Test Error"))
- ruleTarget, err = NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, noDeadzone)
+ ruleTarget, err = NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, 0, 0)
t.Nil(err)
t.Equal(AxisValueMax-AxisValueMin, ruleTarget.axisSize)
}
func (t *RuleTargetAxisTests) TestNormalizeValue() {
- noDeadzone := make([]Deadzone, 0)
-
// Basic normalization should work
t.Run("Simple normalization", func() {
- ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, noDeadzone)
+ ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, 0, 0)
t.Equal(AxisValueMax, ruleTarget.NormalizeValue(int32(10000)))
t.Equal(AxisValueMin, ruleTarget.NormalizeValue(int32(0)))
t.EqualValues(0, ruleTarget.NormalizeValue(int32(5000)))
@@ -81,26 +81,26 @@ func (t *RuleTargetAxisTests) TestNormalizeValue() {
// Normalization with a deadzone should work
t.Run("With Deadzone", func() {
- ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, []Deadzone{{Start: 0, End: 5000, Size: 5000}})
+ ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, 0, 5000)
t.Equal(AxisValueMax, ruleTarget.NormalizeValue(int32(10000)))
- t.InDelta(int32(-32000), ruleTarget.NormalizeValue(int32(5001)), 1000)
+ t.True(ruleTarget.NormalizeValue(int32(5001)) < int32(-31000))
t.EqualValues(0, ruleTarget.NormalizeValue(int32(7500)))
})
t.Run("Inverted", func() {
- ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, true, noDeadzone)
+ ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, true, 0, 0)
t.Equal(AxisValueMax, ruleTarget.NormalizeValue(int32(0)))
t.Equal(AxisValueMin, ruleTarget.NormalizeValue(int32(10000)))
})
t.Run("Out of bounds", func() { // Normalization past the stated axis bounds should clamp
- ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, noDeadzone)
+ ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, 0, 0)
t.Equal(AxisValueMin, ruleTarget.NormalizeValue(int32(-30000)))
t.Equal(AxisValueMax, ruleTarget.NormalizeValue(int32(30000)))
})
t.Run("With partial output range", func() {
- ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, noDeadzone)
+ ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, 0, 0)
ruleTarget.OutputMin = 0
ruleTarget.OutputMax = AxisValueMax
t.EqualValues(0, ruleTarget.NormalizeValue(int32(0)))
@@ -110,7 +110,7 @@ func (t *RuleTargetAxisTests) TestNormalizeValue() {
}
func (t *RuleTargetAxisTests) TestMatchEvent() {
- ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_Y, false, []Deadzone{{Start: -500, End: 500}})
+ ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_Y, false, -500, 500)
validEvent := &evdev.InputEvent{
Type: evdev.EV_ABS,
Code: evdev.ABS_Y,
@@ -133,9 +133,7 @@ func (t *RuleTargetAxisTests) TestMatchEvent() {
}
func (t *RuleTargetAxisTests) TestCreateEvent() {
- noDeadzone := make([]Deadzone, 0)
-
- ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, noDeadzone)
+ ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, 0, 0)
expected := &evdev.InputEvent{
Type: evdev.EV_ABS,
Code: evdev.ABS_X,
@@ -157,45 +155,43 @@ func (t *RuleTargetAxisTests) TestCreateEvent() {
}
func (t *RuleTargetAxisTests) TestGetAxisStrength() {
- noDeadzone := make([]Deadzone, 0)
-
t.Run("With no deadzone", func() {
- ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, noDeadzone)
+ ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, 0, 0)
t.Equal(0.0, ruleTarget.GetAxisStrength(0))
t.Equal(1.0, ruleTarget.GetAxisStrength(10000))
t.Equal(0.5, ruleTarget.GetAxisStrength(5000))
})
t.Run("With low deadzone", func() {
- ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, []Deadzone{{Start: 0, End: 5000, Size: 5000}})
+ ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, 0, 5000)
t.InDelta(0.0, ruleTarget.GetAxisStrength(5001), 0.01)
t.InDelta(0.5, ruleTarget.GetAxisStrength(7500), 0.01)
t.Equal(1.0, ruleTarget.GetAxisStrength(10000))
})
t.Run("With high deadzone", func() {
- ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, []Deadzone{{Start: 5000, End: 10000, Size: 5000}})
+ ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, false, 5000, 10000)
t.Equal(0.0, ruleTarget.GetAxisStrength(0))
t.InDelta(0.5, ruleTarget.GetAxisStrength(2500), 0.01)
t.InDelta(1.0, ruleTarget.GetAxisStrength(4999), 0.01)
})
t.Run("Inverted", func() {
- ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, true, noDeadzone)
+ ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, true, 0, 0)
t.Equal(1.0, ruleTarget.GetAxisStrength(0))
t.Equal(0.5, ruleTarget.GetAxisStrength(5000))
t.Equal(0.0, ruleTarget.GetAxisStrength(10000))
})
t.Run("Inverted with low deadzone", func() {
- ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, true, []Deadzone{{Start: 0, End: 5000, Size: 5000}})
+ ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, true, 0, 5000)
t.InDelta(1.0, ruleTarget.GetAxisStrength(5001), 0.01)
t.InDelta(0.5, ruleTarget.GetAxisStrength(7500), 0.01)
t.Equal(0.0, ruleTarget.GetAxisStrength(10000))
})
t.Run("Inverted with high deadzone", func() {
- ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, true, []Deadzone{{Start: 5000, End: 10000, Size: 5000}})
+ ruleTarget, _ := NewRuleTargetAxis("", t.mock, evdev.ABS_X, true, 5000, 10000)
t.InDelta(0.0, ruleTarget.GetAxisStrength(4999), 0.01)
t.InDelta(0.5, ruleTarget.GetAxisStrength(2500), 0.01)
t.Equal(1.0, ruleTarget.GetAxisStrength(0))
diff --git a/internal/mappingrules/rule_target_hat.go b/internal/mappingrules/rule_target_hat.go
deleted file mode 100644
index 464e559..0000000
--- a/internal/mappingrules/rule_target_hat.go
+++ /dev/null
@@ -1,53 +0,0 @@
-package mappingrules
-
-import (
- "fmt"
-
- "git.annabunches.net/annabunches/joyful/internal/configparser"
- "git.annabunches.net/annabunches/joyful/internal/eventcodes"
- "github.com/holoplot/go-evdev"
-)
-
-type RuleTargetHat struct {
- Device Device
- Hat evdev.EvCode
- Inverted bool
-}
-
-func NewRuleTargetHatFromConfig(config configparser.RuleTargetConfigHat, devs map[string]Device) (*RuleTargetHat, error) {
- dev, ok := devs[config.Device]
- if !ok {
- return nil, fmt.Errorf("device '%s' not found", config.Device)
- }
-
- code, err := eventcodes.ParseCode(config.Hat, eventcodes.CodePrefixAxis)
- if err != nil {
- return nil, err
- }
-
- return &RuleTargetHat{
- Device: dev,
- Hat: code,
- Inverted: config.Inverted,
- }, nil
-}
-
-func (target *RuleTargetHat) NormalizeValue(value int32) int32 {
- if !target.Inverted {
- return value
- }
-
- return value * -1
-}
-
-func (target *RuleTargetHat) CreateEvent(value int32, _ *string) *evdev.InputEvent {
- return &evdev.InputEvent{
- Type: evdev.EV_ABS,
- Code: target.Hat,
- Value: value,
- }
-}
-
-func (target *RuleTargetHat) MatchEvent(device Device, event *evdev.InputEvent) bool {
- return device == target.Device && event.Code == target.Hat
-}
diff --git a/internal/mappingrules/variables.go b/internal/mappingrules/variables.go
new file mode 100644
index 0000000..d9a171b
--- /dev/null
+++ b/internal/mappingrules/variables.go
@@ -0,0 +1,12 @@
+package mappingrules
+
+const (
+ RuleTypeButton = "button"
+ RuleTypeButtonCombo = "button-combo"
+ RuleTypeButtonLatched = "button-latched"
+ RuleTypeAxis = "axis"
+ RuleTypeAxisCombined = "axis-combined"
+ RuleTypeAxisToButton = "axis-to-button"
+ RuleTypeAxisToRelaxis = "axis-to-relaxis"
+ RuleTypeModeSelect = "mode-select"
+)
diff --git a/internal/virtualdevice/variables.go b/internal/virtualdevice/variables.go
index 7102bd5..11adb46 100644
--- a/internal/virtualdevice/variables.go
+++ b/internal/virtualdevice/variables.go
@@ -49,15 +49,6 @@ var (
evdev.ABS_RZ,
evdev.ABS_THROTTLE, // Also called "Slider" or "Slider1"
evdev.ABS_RUDDER, // Also called "Dial", "Slider2", or "RSlider"
- // Hats
- evdev.ABS_HAT0X,
- evdev.ABS_HAT0Y,
- evdev.ABS_HAT1X,
- evdev.ABS_HAT1Y,
- evdev.ABS_HAT2X,
- evdev.ABS_HAT2Y,
- evdev.ABS_HAT3X,
- evdev.ABS_HAT3Y,
},
evdev.EV_KEY: {
evdev.BTN_TRIGGER,
diff --git a/readme.md b/readme.md
index afa5c8f..f9c0e88 100644
--- a/readme.md
+++ b/readme.md
@@ -10,13 +10,13 @@ Joyful is ideal for Linux gamers who enjoy space and flight sims and miss the fe
### Current Features
-* Create virtual devices with up to 8 axes, 4 hats, and 74 buttons.
+* Create virtual devices with up to 8 axes and 74 buttons.
* Flexible rule system that allows several different types of rules, including:
- * Simple 1:1 mappings of buttons, axes, and hats: Button1 -> VirtualButtonA
+ * Simple 1:1 mappings of buttons and axes: Button1 -> VirtualButtonA
* Combination mappings: Button1 + Button2 -> VirtualButtonA
* "Split" axis mapping: map sections of an axis to different outputs using deadzones.
* "Combined" axis mapping: map two physical axes to one virtual axis.
- * 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.
* Define keyboard, mouse, and gamepad outputs in addition to joysticks.
* Configure per-rule configurable deadzones for axes, with multiple ways to specify deadzones.
@@ -27,10 +27,10 @@ Joyful is ideal for Linux gamers who enjoy space and flight sims and miss the fe
* Macros - have a single input produce a sequence of button presses with configurable pauses.
* Sequence combos - Button1, Button2, Button3 -> VirtualButtonA
-* Hat -> Button and Button -> Hat support.
-* HIDRAW support for more button options
+* Hat support
+* HIDRAW support for more button options.
* Sensitivity Curves?
-* Packaged builds for non-Arch distributions.
+* Packaged builds non-Arch distributions.
## Configure