Support keyboard buttons and add presets. (#14)
Reviewed-on: #14 Co-authored-by: Anna Rose Wiggins <annabunches@gmail.com> Co-committed-by: Anna Rose Wiggins <annabunches@gmail.com>
This commit is contained in:
parent
61fe5208e6
commit
838449000c
12 changed files with 492 additions and 133 deletions
|
@ -52,8 +52,8 @@ func getVirtualDevices(buffers map[string]*virtualdevice.EventBuffer) map[string
|
||||||
return devices
|
return devices
|
||||||
}
|
}
|
||||||
|
|
||||||
func initPhysicalDevices(config *config.ConfigParser, lock bool) map[string]*evdev.InputDevice {
|
func initPhysicalDevices(config *config.ConfigParser) map[string]*evdev.InputDevice {
|
||||||
pDeviceMap := config.ConnectPhysicalDevices(lock)
|
pDeviceMap := config.ConnectPhysicalDevices()
|
||||||
if len(pDeviceMap) == 0 {
|
if len(pDeviceMap) == 0 {
|
||||||
logger.Log("Warning: no physical devices found in configuration. No rules will work.")
|
logger.Log("Warning: no physical devices found in configuration. No rules will work.")
|
||||||
}
|
}
|
||||||
|
@ -63,9 +63,7 @@ func initPhysicalDevices(config *config.ConfigParser, lock bool) map[string]*evd
|
||||||
func main() {
|
func main() {
|
||||||
// parse command-line
|
// parse command-line
|
||||||
var configFlag string
|
var configFlag string
|
||||||
var noLockFlag bool
|
|
||||||
flag.BoolVarP(&logger.IsDebugMode, "debug", "d", false, "Output very verbose debug messages.")
|
flag.BoolVarP(&logger.IsDebugMode, "debug", "d", false, "Output very verbose debug messages.")
|
||||||
flag.BoolVar(&noLockFlag, "no-lock", false, "Disable locking the physical devices for exclusive reading.")
|
|
||||||
flag.StringVarP(&configFlag, "config", "c", "~/.config/joyful", "Directory to read configuration from.")
|
flag.StringVarP(&configFlag, "config", "c", "~/.config/joyful", "Directory to read configuration from.")
|
||||||
ttsOps := addTTSFlags()
|
ttsOps := addTTSFlags()
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
@ -82,7 +80,7 @@ func main() {
|
||||||
vBuffersByName, vBuffersByDevice := initVirtualBuffers(config)
|
vBuffersByName, vBuffersByDevice := initVirtualBuffers(config)
|
||||||
|
|
||||||
// Initialize physical devices
|
// Initialize physical devices
|
||||||
pDevices := initPhysicalDevices(config, !noLockFlag)
|
pDevices := initPhysicalDevices(config)
|
||||||
|
|
||||||
// Load the rules
|
// Load the rules
|
||||||
rules, eventChannel, cancel, wg := loadRules(config, pDevices, getVirtualDevices(vBuffersByName))
|
rules, eventChannel, cancel, wg := loadRules(config, pDevices, getVirtualDevices(vBuffersByName))
|
||||||
|
|
|
@ -1,19 +1,7 @@
|
||||||
devices:
|
devices:
|
||||||
- name: primary
|
- name: primary
|
||||||
type: virtual
|
type: virtual
|
||||||
num_axes: 6
|
preset: gamepad
|
||||||
buttons:
|
|
||||||
- BTN_EAST
|
|
||||||
- BTN_SOUTH
|
|
||||||
- BTN_NORTH
|
|
||||||
- BTN_WEST
|
|
||||||
- BTN_TL
|
|
||||||
- BTN_TR
|
|
||||||
- BTN_SELECT
|
|
||||||
- BTN_START
|
|
||||||
- BTN_MODE
|
|
||||||
- BTN_THUMBL
|
|
||||||
- BTN_THUMBR
|
|
||||||
- name: right-stick
|
- name: right-stick
|
||||||
type: physical
|
type: physical
|
||||||
device_name: VIRPIL Controls 20220407 R-VPC Stick MT-50CM2
|
device_name: VIRPIL Controls 20220407 R-VPC Stick MT-50CM2
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
## joystick -> gamepad mapping
|
## joystick -> gamepad mapping
|
||||||
|
|
||||||
This is an incomplete example for mapping dual flightsticks (Virpil Constellation Alphas) to gamepad outputs, to support dual-joystick play in games that expect a console-style gamepad. This has been tested on Steam, and it successfully recognizes this as a gamepad.
|
This is an incomplete example for mapping dual flightsticks (Virpil Constellation Alphas) to gamepad outputs, to support dual-joystick play in games that expect a console-style gamepad. This has been tested on Outer Wilds running in Steam.
|
||||||
|
|
||||||
Not every possible input is mapped here, this is just a somewhat minimal example.
|
Not every possible input is mapped here, this is just a somewhat minimal example.
|
|
@ -1,18 +1,13 @@
|
||||||
devices:
|
devices:
|
||||||
- name: primary
|
- name: primary
|
||||||
type: virtual
|
type: virtual
|
||||||
num_buttons: 74
|
preset: joystick
|
||||||
num_axes: 8
|
|
||||||
- name: secondary
|
- name: secondary
|
||||||
type: virtual
|
type: virtual
|
||||||
num_buttons: 74
|
preset: joystick
|
||||||
num_axes: 3
|
|
||||||
- name: mouse
|
- name: mouse
|
||||||
type: virtual
|
type: virtual
|
||||||
num_buttons: 0
|
preset: mouse
|
||||||
num_axes: 0
|
|
||||||
rel_axes:
|
|
||||||
- REL_WHEEL
|
|
||||||
- name: right-stick
|
- name: right-stick
|
||||||
type: physical
|
type: physical
|
||||||
device_name: VIRPIL Controls 20220407 R-VPC Stick MT-50CM2
|
device_name: VIRPIL Controls 20220407 R-VPC Stick MT-50CM2
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
Configuration is divided into three sections: `devices`, `modes`, and `rules`. Each yaml file can have any number of these sections; joyful will combine the configuration from all files at runtime.
|
Configuration is divided into three sections: `devices`, `modes`, and `rules`. Each yaml file can have any number of these sections; joyful will combine the configuration from all files at runtime.
|
||||||
|
|
||||||
### Device configuration
|
## Device configuration
|
||||||
|
|
||||||
Each entry in `devices` must have a couple of parameters:
|
Each entry in `devices` must have a couple of parameters:
|
||||||
|
|
||||||
|
@ -15,16 +15,18 @@ Each entry in `devices` must have a couple of parameters:
|
||||||
|
|
||||||
`virtual` devices can additionally define these parameters:
|
`virtual` devices can additionally define these parameters:
|
||||||
|
|
||||||
|
* `preset` - Can be `joystick`, `gamepad`, `mouse`, or `keyboard`, and will configure the virtual device to look like and emit an appropriate set of outputs based on the name. For exactly which axes and buttons are defined for each type, see the `Capabilities` values in [internal/config/variables.go](internal/config/variables.go).
|
||||||
* `buttons` or `num_buttons` - Either a list of explicit buttons or a number of buttons to create. (max 74 buttons) Linux-native games may not recognize all buttons created by Joyful.
|
* `buttons` or `num_buttons` - Either a list of explicit buttons or a number of buttons to create. (max 74 buttons) Linux-native games may not recognize all buttons created by Joyful.
|
||||||
* `axes` or `num_axes` - An explicit list of `ABS_` axes or a number to create.
|
* `axes` or `num_axes` - An explicit list of `ABS_` axes or a number to create.
|
||||||
* `relative_axes` or `num_relative_axes` - As above, but for `REL_` axes.
|
* `relative_axes` or `num_relative_axes` - As above, but for `REL_` axes.
|
||||||
|
|
||||||
A couple of additional notes on virtual devices:
|
A couple of additional notes on virtual devices:
|
||||||
|
|
||||||
* For all 3 of the above options, an explicit list will override the `num_` parameters if both are present.
|
* Users are encouraged to use the `preset` options whenever possible. They have the highest probability of working the way you expect. If you need to output to multiple types of device, the best approach is to create multiple virtual devices.
|
||||||
* Some environments will only register mouse events if the device *only* supports mouse-like events, so it can be useful to isolate your `relative_axes` to their own virtual device and explicitly define the axes.
|
* For all 3 of the above options, there is a priority order. If you specify a `preset`, it will be used ignoring any other settings. An explicit list will override the corresponding `num_` parameter.
|
||||||
|
* Some environments/applications are prescriptive about what combinations make sense; for example, they will only register mouse events if the device *only* supports mouse-like events. The `presets` attempt to take this into account. If you are defining capabilities manually and attempt to mix and match button codes, you may also run into this problem.
|
||||||
|
|
||||||
### Rules configuration
|
## Rules configuration
|
||||||
|
|
||||||
All `rules` must have a `type` parameter. Valid values for this parameter are:
|
All `rules` must have a `type` parameter. Valid values for this parameter are:
|
||||||
|
|
||||||
|
|
|
@ -8,13 +8,23 @@ import (
|
||||||
"github.com/holoplot/go-evdev"
|
"github.com/holoplot/go-evdev"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func parseCodeButton(code string) (evdev.EvCode, error) {
|
||||||
|
prefix := CodePrefixButton
|
||||||
|
|
||||||
|
if strings.HasPrefix(code, CodePrefixKey+"_") {
|
||||||
|
prefix = CodePrefixKey
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseCode(code, prefix)
|
||||||
|
}
|
||||||
|
|
||||||
func parseCode(code, prefix string) (evdev.EvCode, error) {
|
func parseCode(code, prefix string) (evdev.EvCode, error) {
|
||||||
code = strings.ToUpper(code)
|
code = strings.ToUpper(code)
|
||||||
|
|
||||||
var codeLookup map[string]evdev.EvCode
|
var codeLookup map[string]evdev.EvCode
|
||||||
|
|
||||||
switch prefix {
|
switch prefix {
|
||||||
case CodePrefixButton:
|
case CodePrefixButton, CodePrefixKey:
|
||||||
codeLookup = evdev.KEYFromString
|
codeLookup = evdev.KEYFromString
|
||||||
case CodePrefixAxis:
|
case CodePrefixAxis:
|
||||||
codeLookup = evdev.ABSFromString
|
codeLookup = evdev.ABSFromString
|
||||||
|
|
|
@ -16,7 +16,7 @@ func TestRunnerEventCodeParserTests(t *testing.T) {
|
||||||
suite.Run(t, new(EventCodeParserTests))
|
suite.Run(t, new(EventCodeParserTests))
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseCodeTestCase(t *EventCodeParserTests, in string, out int, prefix string) {
|
func parseCodeTestCase(t *EventCodeParserTests, in string, out evdev.EvCode, prefix string) {
|
||||||
t.Run(fmt.Sprintf("%s: %s", prefix, in), func() {
|
t.Run(fmt.Sprintf("%s: %s", prefix, in), func() {
|
||||||
code, err := parseCode(in, prefix)
|
code, err := parseCode(in, prefix)
|
||||||
t.Nil(err)
|
t.Nil(err)
|
||||||
|
@ -24,10 +24,33 @@ func parseCodeTestCase(t *EventCodeParserTests, in string, out int, prefix strin
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *EventCodeParserTests) TestParseCodeABS() {
|
func (t *EventCodeParserTests) TestParseCodeButton() {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
in string
|
in string
|
||||||
out int
|
out evdev.EvCode
|
||||||
|
}{
|
||||||
|
{"BTN_A", evdev.BTN_A},
|
||||||
|
{"A", evdev.BTN_A},
|
||||||
|
{"BTN_TRIGGER_HAPPY", evdev.BTN_TRIGGER_HAPPY},
|
||||||
|
{"KEY_A", evdev.KEY_A},
|
||||||
|
{"KEY_ESC", evdev.KEY_ESC},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.in, func() {
|
||||||
|
code, err := parseCodeButton(testCase.in)
|
||||||
|
t.Nil(err)
|
||||||
|
t.EqualValues(code, testCase.out)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *EventCodeParserTests) TestParseCode() {
|
||||||
|
|
||||||
|
t.Run("ABS", func() {
|
||||||
|
testCases := []struct {
|
||||||
|
in string
|
||||||
|
out evdev.EvCode
|
||||||
}{
|
}{
|
||||||
{"ABS_X", evdev.ABS_X},
|
{"ABS_X", evdev.ABS_X},
|
||||||
{"ABS_Y", evdev.ABS_Y},
|
{"ABS_Y", evdev.ABS_Y},
|
||||||
|
@ -50,12 +73,12 @@ func (t *EventCodeParserTests) TestParseCodeABS() {
|
||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
parseCodeTestCase(t, testCase.in, testCase.out, "ABS")
|
parseCodeTestCase(t, testCase.in, testCase.out, "ABS")
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
func (t *EventCodeParserTests) TestParseCodeREL() {
|
t.Run("REL", func() {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
in string
|
in string
|
||||||
out int
|
out evdev.EvCode
|
||||||
}{
|
}{
|
||||||
{"REL_X", evdev.REL_X},
|
{"REL_X", evdev.REL_X},
|
||||||
{"REL_Y", evdev.REL_Y},
|
{"REL_Y", evdev.REL_Y},
|
||||||
|
@ -77,12 +100,12 @@ func (t *EventCodeParserTests) TestParseCodeREL() {
|
||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
parseCodeTestCase(t, testCase.in, testCase.out, "REL")
|
parseCodeTestCase(t, testCase.in, testCase.out, "REL")
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
func (t *EventCodeParserTests) TestParseCodeBTN() {
|
t.Run("BTN", func() {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
in string
|
in string
|
||||||
out int
|
out evdev.EvCode
|
||||||
}{
|
}{
|
||||||
{"BTN_TRIGGER", evdev.BTN_TRIGGER},
|
{"BTN_TRIGGER", evdev.BTN_TRIGGER},
|
||||||
{"trigger", evdev.BTN_TRIGGER},
|
{"trigger", evdev.BTN_TRIGGER},
|
||||||
|
@ -93,9 +116,9 @@ func (t *EventCodeParserTests) TestParseCodeBTN() {
|
||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
parseCodeTestCase(t, testCase.in, testCase.out, "BTN")
|
parseCodeTestCase(t, testCase.in, testCase.out, "BTN")
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
func (t *EventCodeParserTests) TestParseCodeInvalid() {
|
t.Run("Invalid", func() {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
in string
|
in string
|
||||||
prefix string
|
prefix string
|
||||||
|
@ -115,4 +138,5 @@ func (t *EventCodeParserTests) TestParseCodeInvalid() {
|
||||||
t.NotNil(err)
|
t.NotNil(err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,11 +23,26 @@ func (parser *ConfigParser) CreateVirtualDevices() map[string]*evdev.InputDevice
|
||||||
}
|
}
|
||||||
|
|
||||||
name := fmt.Sprintf("joyful-%s", deviceConfig.Name)
|
name := fmt.Sprintf("joyful-%s", deviceConfig.Name)
|
||||||
capabilities := map[evdev.EvType][]evdev.EvCode{
|
|
||||||
|
var capabilities map[evdev.EvType][]evdev.EvCode
|
||||||
|
|
||||||
|
// todo: add tests for presets
|
||||||
|
switch deviceConfig.Preset {
|
||||||
|
case DevicePresetGamepad:
|
||||||
|
capabilities = CapabilitiesPresetGamepad
|
||||||
|
case DevicePresetKeyboard:
|
||||||
|
capabilities = CapabilitiesPresetKeyboard
|
||||||
|
case DevicePresetJoystick:
|
||||||
|
capabilities = CapabilitiesPresetJoystick
|
||||||
|
case DevicePresetMouse:
|
||||||
|
capabilities = CapabilitiesPresetMouse
|
||||||
|
default:
|
||||||
|
capabilities = map[evdev.EvType][]evdev.EvCode{
|
||||||
evdev.EV_KEY: makeButtons(deviceConfig.NumButtons, deviceConfig.Buttons),
|
evdev.EV_KEY: makeButtons(deviceConfig.NumButtons, deviceConfig.Buttons),
|
||||||
evdev.EV_ABS: makeAxes(deviceConfig.NumAxes, deviceConfig.Axes),
|
evdev.EV_ABS: makeAxes(deviceConfig.NumAxes, deviceConfig.Axes),
|
||||||
evdev.EV_REL: makeRelativeAxes(deviceConfig.NumRelativeAxes, deviceConfig.RelativeAxes),
|
evdev.EV_REL: makeRelativeAxes(deviceConfig.NumRelativeAxes, deviceConfig.RelativeAxes),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
device, err := evdev.CreateDevice(
|
device, err := evdev.CreateDevice(
|
||||||
name,
|
name,
|
||||||
|
@ -60,13 +75,12 @@ func (parser *ConfigParser) CreateVirtualDevices() map[string]*evdev.InputDevice
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConnectPhysicalDevices will create InputDevices corresponding to any registered
|
// ConnectPhysicalDevices will create InputDevices corresponding to any registered
|
||||||
// devices with type = physical. It will also attempt to acquire exclusive access
|
// devices with type = physical.
|
||||||
// to those devices, to prevent the same inputs from being read on multiple devices.
|
|
||||||
//
|
//
|
||||||
// This function assumes you have already called Parse() on the config directory.
|
// This function assumes you have already called Parse() on the config directory.
|
||||||
//
|
//
|
||||||
// This function should only be called once.
|
// This function should only be called once.
|
||||||
func (parser *ConfigParser) ConnectPhysicalDevices(lock bool) map[string]*evdev.InputDevice {
|
func (parser *ConfigParser) ConnectPhysicalDevices() map[string]*evdev.InputDevice {
|
||||||
deviceMap := make(map[string]*evdev.InputDevice)
|
deviceMap := make(map[string]*evdev.InputDevice)
|
||||||
|
|
||||||
for _, deviceConfig := range parser.config.Devices {
|
for _, deviceConfig := range parser.config.Devices {
|
||||||
|
@ -80,7 +94,8 @@ func (parser *ConfigParser) ConnectPhysicalDevices(lock bool) map[string]*evdev.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if lock {
|
if deviceConfig.Lock {
|
||||||
|
logger.LogDebugf("Locking device '%s'", deviceConfig.DeviceName)
|
||||||
err := device.Grab()
|
err := device.Grab()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogError(err, "Failed to grab device for exclusive access")
|
logger.LogError(err, "Failed to grab device for exclusive access")
|
||||||
|
|
|
@ -14,7 +14,7 @@ func makeRuleTargetButton(targetConfig RuleTargetConfig, devs map[string]Device)
|
||||||
return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device)
|
return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device)
|
||||||
}
|
}
|
||||||
|
|
||||||
eventCode, err := parseCode(targetConfig.Button, "BTN")
|
eventCode, err := parseCodeButton(targetConfig.Button)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@ func makeRuleTargetAxis(targetConfig RuleTargetConfig, devs map[string]Device) (
|
||||||
return nil, errors.New("deadzone_end must be greater than deadzone_start")
|
return nil, errors.New("deadzone_end must be greater than deadzone_start")
|
||||||
}
|
}
|
||||||
|
|
||||||
eventCode, err := parseCode(targetConfig.Axis, "ABS")
|
eventCode, err := parseCode(targetConfig.Axis, CodePrefixAxis)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -63,7 +63,7 @@ func makeRuleTargetRelaxis(targetConfig RuleTargetConfig, devs map[string]Device
|
||||||
return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device)
|
return nil, fmt.Errorf("non-existent device '%s'", targetConfig.Device)
|
||||||
}
|
}
|
||||||
|
|
||||||
eventCode, err := parseCode(targetConfig.Axis, "REL")
|
eventCode, err := parseCode(targetConfig.Axis, CodePrefixRelaxis)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,12 +20,14 @@ type DeviceConfig struct {
|
||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"`
|
||||||
DeviceName string `yaml:"device_name,omitempty"`
|
DeviceName string `yaml:"device_name,omitempty"`
|
||||||
Uuid string `yaml:"uuid,omitempty"`
|
Uuid string `yaml:"uuid,omitempty"`
|
||||||
|
Preset string `yaml:"preset,omitempty"`
|
||||||
NumButtons int `yaml:"num_buttons,omitempty"`
|
NumButtons int `yaml:"num_buttons,omitempty"`
|
||||||
NumAxes int `yaml:"num_axes,omitempty"`
|
NumAxes int `yaml:"num_axes,omitempty"`
|
||||||
NumRelativeAxes int `yaml:"num_rel_axes"`
|
NumRelativeAxes int `yaml:"num_rel_axes"`
|
||||||
Buttons []string `yaml:"buttons,omitempty"`
|
Buttons []string `yaml:"buttons,omitempty"`
|
||||||
Axes []string `yaml:"axes,omitempty"`
|
Axes []string `yaml:"axes,omitempty"`
|
||||||
RelativeAxes []string `yaml:"rel_axes,omitempty"`
|
RelativeAxes []string `yaml:"rel_axes,omitempty"`
|
||||||
|
Lock bool `yaml:"lock,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RuleConfig struct {
|
type RuleConfig struct {
|
||||||
|
@ -54,3 +56,44 @@ type RuleTargetConfig struct {
|
||||||
Inverted bool `yaml:"inverted,omitempty"`
|
Inverted bool `yaml:"inverted,omitempty"`
|
||||||
Modes []string `yaml:"modes,omitempty"`
|
Modes []string `yaml:"modes,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 *DeviceConfig) UnmarshalYAML(unmarshal func(data interface{}) error) error {
|
||||||
|
var raw struct {
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
DeviceName string `yaml:"device_name"`
|
||||||
|
Uuid string
|
||||||
|
Preset string
|
||||||
|
NumButtons int `yaml:"num_buttons"`
|
||||||
|
NumAxes int `yaml:"num_axes"`
|
||||||
|
NumRelativeAxes int `yaml:"num_rel_axes"`
|
||||||
|
Buttons []string
|
||||||
|
Axes []string
|
||||||
|
RelativeAxes []string `yaml:"relative_axes"`
|
||||||
|
Lock bool `yaml:"lock,omitempty"`
|
||||||
|
}
|
||||||
|
raw.Lock = true
|
||||||
|
|
||||||
|
err := unmarshal(&raw)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*dc = DeviceConfig{
|
||||||
|
Name: raw.Name,
|
||||||
|
Type: raw.Type,
|
||||||
|
DeviceName: raw.DeviceName,
|
||||||
|
Uuid: raw.Uuid,
|
||||||
|
Preset: raw.Preset,
|
||||||
|
NumButtons: raw.NumButtons,
|
||||||
|
NumAxes: raw.NumAxes,
|
||||||
|
NumRelativeAxes: raw.NumRelativeAxes,
|
||||||
|
Buttons: raw.Buttons,
|
||||||
|
Axes: raw.Axes,
|
||||||
|
RelativeAxes: raw.RelativeAxes,
|
||||||
|
Lock: raw.Lock,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,11 @@ const (
|
||||||
DeviceTypePhysical = "physical"
|
DeviceTypePhysical = "physical"
|
||||||
DeviceTypeVirtual = "virtual"
|
DeviceTypeVirtual = "virtual"
|
||||||
|
|
||||||
|
DevicePresetKeyboard = "keyboard"
|
||||||
|
DevicePresetGamepad = "gamepad"
|
||||||
|
DevicePresetJoystick = "joystick"
|
||||||
|
DevicePresetMouse = "mouse"
|
||||||
|
|
||||||
RuleTypeButton = "button"
|
RuleTypeButton = "button"
|
||||||
RuleTypeButtonCombo = "button-combo"
|
RuleTypeButtonCombo = "button-combo"
|
||||||
RuleTypeLatched = "button-latched"
|
RuleTypeLatched = "button-latched"
|
||||||
|
@ -18,6 +23,7 @@ const (
|
||||||
RuleTypeAxisToRelaxis = "axis-to-relaxis"
|
RuleTypeAxisToRelaxis = "axis-to-relaxis"
|
||||||
|
|
||||||
CodePrefixButton = "BTN"
|
CodePrefixButton = "BTN"
|
||||||
|
CodePrefixKey = "KEY"
|
||||||
CodePrefixAxis = "ABS"
|
CodePrefixAxis = "ABS"
|
||||||
CodePrefixRelaxis = "REL"
|
CodePrefixRelaxis = "REL"
|
||||||
|
|
||||||
|
@ -102,3 +108,281 @@ var (
|
||||||
evdev.EvCode(0x2ff),
|
evdev.EvCode(0x2ff),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Device Presets
|
||||||
|
var (
|
||||||
|
CapabilitiesPresetGamepad = map[evdev.EvType][]evdev.EvCode{
|
||||||
|
evdev.EV_ABS: {
|
||||||
|
evdev.ABS_X,
|
||||||
|
evdev.ABS_Y,
|
||||||
|
evdev.ABS_Z,
|
||||||
|
evdev.ABS_RX,
|
||||||
|
evdev.ABS_RY,
|
||||||
|
evdev.ABS_RZ,
|
||||||
|
evdev.ABS_HAT0X,
|
||||||
|
evdev.ABS_HAT0Y,
|
||||||
|
},
|
||||||
|
evdev.EV_KEY: {
|
||||||
|
evdev.BTN_NORTH, // Xbox 'X', Playstation 'Square'
|
||||||
|
evdev.BTN_SOUTH, // Xbox 'A', Plastation 'X'
|
||||||
|
evdev.BTN_WEST, // Xbox 'Y', Playstation 'Triangle'
|
||||||
|
evdev.BTN_EAST, // Xbox 'B', Playstation 'O'
|
||||||
|
evdev.BTN_THUMBL,
|
||||||
|
evdev.BTN_THUMBR,
|
||||||
|
evdev.BTN_TL,
|
||||||
|
evdev.BTN_TR,
|
||||||
|
evdev.BTN_SELECT,
|
||||||
|
evdev.BTN_START,
|
||||||
|
evdev.BTN_MODE,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
CapabilitiesPresetJoystick = map[evdev.EvType][]evdev.EvCode{
|
||||||
|
evdev.EV_ABS: {
|
||||||
|
evdev.ABS_X,
|
||||||
|
evdev.ABS_Y,
|
||||||
|
evdev.ABS_Z,
|
||||||
|
evdev.ABS_RX,
|
||||||
|
evdev.ABS_RY,
|
||||||
|
evdev.ABS_RZ,
|
||||||
|
evdev.ABS_THROTTLE, // Also called "Slider" or "Slider1"
|
||||||
|
evdev.ABS_RUDDER, // Also called "Dial", "Slider2", or "RSlider"
|
||||||
|
},
|
||||||
|
evdev.EV_KEY: {
|
||||||
|
evdev.BTN_TRIGGER,
|
||||||
|
evdev.BTN_THUMB,
|
||||||
|
evdev.BTN_THUMB2,
|
||||||
|
evdev.BTN_TOP,
|
||||||
|
evdev.BTN_TOP2,
|
||||||
|
evdev.BTN_PINKIE,
|
||||||
|
evdev.BTN_BASE,
|
||||||
|
evdev.BTN_BASE2,
|
||||||
|
evdev.BTN_BASE3,
|
||||||
|
evdev.BTN_BASE4,
|
||||||
|
evdev.BTN_BASE5,
|
||||||
|
evdev.BTN_BASE6,
|
||||||
|
evdev.EvCode(0x12c), // decimal 300
|
||||||
|
evdev.EvCode(0x12d), // decimal 301
|
||||||
|
evdev.EvCode(0x12e), // decimal 302
|
||||||
|
evdev.BTN_DEAD,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY1,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY2,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY3,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY4,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY5,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY6,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY7,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY8,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY9,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY10,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY11,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY12,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY13,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY14,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY15,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY16,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY17,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY18,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY19,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY20,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY21,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY22,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY23,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY24,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY25,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY26,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY27,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY28,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY29,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY30,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY31,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY32,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY33,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY34,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY35,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY36,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY37,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY38,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY39,
|
||||||
|
evdev.BTN_TRIGGER_HAPPY40,
|
||||||
|
evdev.EvCode(0x2e8),
|
||||||
|
evdev.EvCode(0x2e9),
|
||||||
|
evdev.EvCode(0x2f0),
|
||||||
|
evdev.EvCode(0x2f1),
|
||||||
|
evdev.EvCode(0x2f2),
|
||||||
|
evdev.EvCode(0x2f3),
|
||||||
|
evdev.EvCode(0x2f4),
|
||||||
|
evdev.EvCode(0x2f5),
|
||||||
|
evdev.EvCode(0x2f6),
|
||||||
|
evdev.EvCode(0x2f7),
|
||||||
|
evdev.EvCode(0x2f8),
|
||||||
|
evdev.EvCode(0x2f9),
|
||||||
|
evdev.EvCode(0x2fa),
|
||||||
|
evdev.EvCode(0x2fb),
|
||||||
|
evdev.EvCode(0x2fc),
|
||||||
|
evdev.EvCode(0x2fd),
|
||||||
|
evdev.EvCode(0x2fe),
|
||||||
|
evdev.EvCode(0x2ff),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
CapabilitiesPresetKeyboard = map[evdev.EvType][]evdev.EvCode{
|
||||||
|
evdev.EV_KEY: {
|
||||||
|
evdev.KEY_ESC,
|
||||||
|
evdev.KEY_1,
|
||||||
|
evdev.KEY_2,
|
||||||
|
evdev.KEY_3,
|
||||||
|
evdev.KEY_4,
|
||||||
|
evdev.KEY_5,
|
||||||
|
evdev.KEY_6,
|
||||||
|
evdev.KEY_7,
|
||||||
|
evdev.KEY_8,
|
||||||
|
evdev.KEY_9,
|
||||||
|
evdev.KEY_0,
|
||||||
|
evdev.KEY_MINUS,
|
||||||
|
evdev.KEY_EQUAL,
|
||||||
|
evdev.KEY_BACKSPACE,
|
||||||
|
evdev.KEY_TAB,
|
||||||
|
evdev.KEY_Q,
|
||||||
|
evdev.KEY_W,
|
||||||
|
evdev.KEY_E,
|
||||||
|
evdev.KEY_R,
|
||||||
|
evdev.KEY_T,
|
||||||
|
evdev.KEY_Y,
|
||||||
|
evdev.KEY_U,
|
||||||
|
evdev.KEY_I,
|
||||||
|
evdev.KEY_O,
|
||||||
|
evdev.KEY_P,
|
||||||
|
evdev.KEY_LEFTBRACE,
|
||||||
|
evdev.KEY_RIGHTBRACE,
|
||||||
|
evdev.KEY_ENTER,
|
||||||
|
evdev.KEY_LEFTCTRL,
|
||||||
|
evdev.KEY_A,
|
||||||
|
evdev.KEY_S,
|
||||||
|
evdev.KEY_D,
|
||||||
|
evdev.KEY_F,
|
||||||
|
evdev.KEY_G,
|
||||||
|
evdev.KEY_H,
|
||||||
|
evdev.KEY_J,
|
||||||
|
evdev.KEY_K,
|
||||||
|
evdev.KEY_L,
|
||||||
|
evdev.KEY_SEMICOLON,
|
||||||
|
evdev.KEY_APOSTROPHE,
|
||||||
|
evdev.KEY_GRAVE,
|
||||||
|
evdev.KEY_LEFTSHIFT,
|
||||||
|
evdev.KEY_BACKSLASH,
|
||||||
|
evdev.KEY_Z,
|
||||||
|
evdev.KEY_X,
|
||||||
|
evdev.KEY_C,
|
||||||
|
evdev.KEY_V,
|
||||||
|
evdev.KEY_B,
|
||||||
|
evdev.KEY_N,
|
||||||
|
evdev.KEY_M,
|
||||||
|
evdev.KEY_COMMA,
|
||||||
|
evdev.KEY_DOT,
|
||||||
|
evdev.KEY_SLASH,
|
||||||
|
evdev.KEY_RIGHTSHIFT,
|
||||||
|
evdev.KEY_KPASTERISK,
|
||||||
|
evdev.KEY_LEFTALT,
|
||||||
|
evdev.KEY_SPACE,
|
||||||
|
evdev.KEY_CAPSLOCK,
|
||||||
|
evdev.KEY_F1,
|
||||||
|
evdev.KEY_F2,
|
||||||
|
evdev.KEY_F3,
|
||||||
|
evdev.KEY_F4,
|
||||||
|
evdev.KEY_F5,
|
||||||
|
evdev.KEY_F6,
|
||||||
|
evdev.KEY_F7,
|
||||||
|
evdev.KEY_F8,
|
||||||
|
evdev.KEY_F9,
|
||||||
|
evdev.KEY_F10,
|
||||||
|
evdev.KEY_NUMLOCK,
|
||||||
|
evdev.KEY_SCROLLLOCK,
|
||||||
|
evdev.KEY_KP7,
|
||||||
|
evdev.KEY_KP8,
|
||||||
|
evdev.KEY_KP9,
|
||||||
|
evdev.KEY_KPMINUS,
|
||||||
|
evdev.KEY_KP4,
|
||||||
|
evdev.KEY_KP5,
|
||||||
|
evdev.KEY_KP6,
|
||||||
|
evdev.KEY_KPPLUS,
|
||||||
|
evdev.KEY_KP1,
|
||||||
|
evdev.KEY_KP2,
|
||||||
|
evdev.KEY_KP3,
|
||||||
|
evdev.KEY_KP0,
|
||||||
|
evdev.KEY_KPDOT,
|
||||||
|
evdev.KEY_ZENKAKUHANKAKU,
|
||||||
|
evdev.KEY_102ND,
|
||||||
|
evdev.KEY_F11,
|
||||||
|
evdev.KEY_F12,
|
||||||
|
evdev.KEY_RO,
|
||||||
|
evdev.KEY_KATAKANA,
|
||||||
|
evdev.KEY_HIRAGANA,
|
||||||
|
evdev.KEY_HENKAN,
|
||||||
|
evdev.KEY_KATAKANAHIRAGANA,
|
||||||
|
evdev.KEY_MUHENKAN,
|
||||||
|
evdev.KEY_KPJPCOMMA,
|
||||||
|
evdev.KEY_KPENTER,
|
||||||
|
evdev.KEY_RIGHTCTRL,
|
||||||
|
evdev.KEY_KPSLASH,
|
||||||
|
evdev.KEY_SYSRQ,
|
||||||
|
evdev.KEY_RIGHTALT,
|
||||||
|
evdev.KEY_LINEFEED,
|
||||||
|
evdev.KEY_HOME,
|
||||||
|
evdev.KEY_UP,
|
||||||
|
evdev.KEY_PAGEUP,
|
||||||
|
evdev.KEY_LEFT,
|
||||||
|
evdev.KEY_RIGHT,
|
||||||
|
evdev.KEY_END,
|
||||||
|
evdev.KEY_DOWN,
|
||||||
|
evdev.KEY_PAGEDOWN,
|
||||||
|
evdev.KEY_INSERT,
|
||||||
|
evdev.KEY_DELETE,
|
||||||
|
evdev.KEY_MACRO,
|
||||||
|
evdev.KEY_MUTE,
|
||||||
|
evdev.KEY_VOLUMEDOWN,
|
||||||
|
evdev.KEY_VOLUMEUP,
|
||||||
|
evdev.KEY_KPEQUAL,
|
||||||
|
evdev.KEY_KPPLUSMINUS,
|
||||||
|
evdev.KEY_PAUSE,
|
||||||
|
evdev.KEY_SCALE,
|
||||||
|
evdev.KEY_KPCOMMA,
|
||||||
|
evdev.KEY_HANGEUL,
|
||||||
|
evdev.KEY_HANJA,
|
||||||
|
evdev.KEY_YEN,
|
||||||
|
evdev.KEY_LEFTMETA,
|
||||||
|
evdev.KEY_RIGHTMETA,
|
||||||
|
evdev.KEY_COMPOSE,
|
||||||
|
evdev.KEY_F13,
|
||||||
|
evdev.KEY_F14,
|
||||||
|
evdev.KEY_F15,
|
||||||
|
evdev.KEY_F16,
|
||||||
|
evdev.KEY_F17,
|
||||||
|
evdev.KEY_F18,
|
||||||
|
evdev.KEY_F19,
|
||||||
|
evdev.KEY_F20,
|
||||||
|
evdev.KEY_F21,
|
||||||
|
evdev.KEY_F22,
|
||||||
|
evdev.KEY_F23,
|
||||||
|
evdev.KEY_F24,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
CapabilitiesPresetMouse = map[evdev.EvType][]evdev.EvCode{
|
||||||
|
evdev.EV_REL: {
|
||||||
|
evdev.REL_X,
|
||||||
|
evdev.REL_Y,
|
||||||
|
evdev.REL_WHEEL,
|
||||||
|
evdev.REL_HWHEEL,
|
||||||
|
},
|
||||||
|
evdev.EV_KEY: {
|
||||||
|
evdev.BTN_LEFT,
|
||||||
|
evdev.BTN_MIDDLE,
|
||||||
|
evdev.BTN_RIGHT,
|
||||||
|
evdev.BTN_SIDE,
|
||||||
|
evdev.BTN_EXTRA,
|
||||||
|
evdev.BTN_FORWARD,
|
||||||
|
evdev.BTN_BACK,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
12
readme.md
12
readme.md
|
@ -18,6 +18,7 @@ Joyful is ideal for Linux gamers who enjoy space and flight sims and miss the fe
|
||||||
* "Combined" axis mapping: map two physical axes to one virtual axis.
|
* "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.
|
* 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.
|
* 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.
|
||||||
* Text-to-speech engine that announces the current mode when it changes.
|
* Text-to-speech engine that announces the current mode when it changes.
|
||||||
|
@ -26,10 +27,9 @@ 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.
|
* Macros - have a single input produce a sequence of button presses with configurable pauses.
|
||||||
* Sequence combos - Button1, Button2, Button3 -> VirtualButtonA
|
* Sequence combos - Button1, Button2, Button3 -> VirtualButtonA
|
||||||
* Output keyboard button presses
|
|
||||||
* Hat support
|
* Hat support
|
||||||
* HIDRAW support for more button options.
|
* HIDRAW support for more button options.
|
||||||
* Sensitivity Curves.
|
* Sensitivity Curves?
|
||||||
* Packaged builds for Arch and possibly other distributions.
|
* Packaged builds for Arch and possibly other distributions.
|
||||||
|
|
||||||
## Configure
|
## Configure
|
||||||
|
@ -87,10 +87,10 @@ Pressing `<enter>` in the running terminal window will reload the `rules` sectio
|
||||||
|
|
||||||
## Technical details
|
## Technical details
|
||||||
|
|
||||||
Joyful is written in golang, and uses `evdev`/`uinput` to manage devices, `piper` and `oto` for TTS. See [cmd/joyful/main.go](cmd/joyful/main.go) for the program's entry point.
|
Joyful is written in golang, and uses `evdev`/`uinput` to manage devices and `espeak-ng` for TTS. See [cmd/joyful/main.go](cmd/joyful/main.go) for the program's entry point.
|
||||||
|
|
||||||
|
This was originally going to be a Rust project, but the author's Rust skills weren't quite up to the task yet. Please look forward to the inevitable Rust rewrite.
|
||||||
|
|
||||||
### Contributing
|
### Contributing
|
||||||
|
|
||||||
Send patches and questions to [annabunches@gmail.com](mailto:annabunches@gmail.com). Make sure the subject of your email starts with `[Joyful]`.
|
Issues and pull requests should be made on the [Codeberg mirror](https://codeberg.org/annabunches/joyful).
|
||||||
|
|
||||||
If enough people show an interest in contributing, I'll consider mirroring the repository on Github.
|
|
Loading…
Add table
Add a link
Reference in a new issue